From 5bbf4bda1a345e27cf11dcf4a088de33af3ba5cf Mon Sep 17 00:00:00 2001 From: cat <114403771+makaseloli@users.noreply.github.com> Date: Mon, 11 Aug 2025 22:01:04 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=E6=9C=80=E6=96=B0=E3=81=AEtsukichat?= =?UTF-8?q?=E3=82=92=E7=A7=BB=E6=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitattributes | 13 +- .gitignore | 48 ++-- build.gradle | 89 ++------ gradle.properties | 27 +-- gradle/wrapper/gradle-wrapper.jar | Bin 43583 -> 43453 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 5 +- gradlew.bat | 2 - settings.gradle | 2 +- .../github/meatwo310/tsukichat/TsukiChat.java | 44 +++- .../tsukichat/commands/ConfigCommand.java | 30 +++ .../tsukichat/commands/CustomCommand.java | 215 ++++++++++++++++++ .../tsukichat/commands/PermissionCommand.java | 35 +++ .../commands/ServerDictionaryCommand.java | 152 +++++++++++++ .../tsukichat/commands/TsukiChatCommand.java | 163 +++++++++++++ .../commands/UserDictionaryCommand.java | 133 +++++++++++ .../compat/SDLinkServerChatEvent.java | 35 +++ .../tsukichat/compat/mohist/MohistHelper.java | 37 +++ .../tsukichat/config/CommonConfigs.java | 195 ++++++++++++++++ .../tsukichat/event/CommandRegisterer.java | 14 ++ .../event/MohistCompatPluginChecker.java | 26 +++ .../tsukichat/event/PlayerCloneEvent.java | 14 ++ .../meatwo310/tsukichat/event/ServerChat.java | 127 +++++++++++ .../mixin/SDLinkServerEventsMixin.java | 23 ++ .../tsukichat/mixin/TeamMsgCommandMixin.java | 73 ++++++ .../tsukichat/util/ChatCustomizer.java | 106 +++++++++ .../meatwo310/tsukichat/util/Converter.java | 207 +++++++++++++++++ .../tsukichat/util/CustomizedChat.java | 59 +++++ .../tsukichat/util/PlayerNbtUtil.java | 86 +++++++ .../assets/tsukichat/lang/en_us.json | 5 + src/main/resources/tsukichat.mixins.json | 18 ++ .../templates/META-INF/neoforge.mods.toml | 93 ++++---- 32 files changed, 1909 insertions(+), 169 deletions(-) create mode 100644 src/main/java/io/github/meatwo310/tsukichat/commands/ConfigCommand.java create mode 100644 src/main/java/io/github/meatwo310/tsukichat/commands/CustomCommand.java create mode 100644 src/main/java/io/github/meatwo310/tsukichat/commands/PermissionCommand.java create mode 100644 src/main/java/io/github/meatwo310/tsukichat/commands/ServerDictionaryCommand.java create mode 100644 src/main/java/io/github/meatwo310/tsukichat/commands/TsukiChatCommand.java create mode 100644 src/main/java/io/github/meatwo310/tsukichat/commands/UserDictionaryCommand.java create mode 100644 src/main/java/io/github/meatwo310/tsukichat/compat/SDLinkServerChatEvent.java create mode 100644 src/main/java/io/github/meatwo310/tsukichat/compat/mohist/MohistHelper.java create mode 100644 src/main/java/io/github/meatwo310/tsukichat/config/CommonConfigs.java create mode 100644 src/main/java/io/github/meatwo310/tsukichat/event/CommandRegisterer.java create mode 100644 src/main/java/io/github/meatwo310/tsukichat/event/MohistCompatPluginChecker.java create mode 100644 src/main/java/io/github/meatwo310/tsukichat/event/PlayerCloneEvent.java create mode 100644 src/main/java/io/github/meatwo310/tsukichat/event/ServerChat.java create mode 100644 src/main/java/io/github/meatwo310/tsukichat/mixin/SDLinkServerEventsMixin.java create mode 100644 src/main/java/io/github/meatwo310/tsukichat/mixin/TeamMsgCommandMixin.java create mode 100644 src/main/java/io/github/meatwo310/tsukichat/util/ChatCustomizer.java create mode 100644 src/main/java/io/github/meatwo310/tsukichat/util/Converter.java create mode 100644 src/main/java/io/github/meatwo310/tsukichat/util/CustomizedChat.java create mode 100644 src/main/java/io/github/meatwo310/tsukichat/util/PlayerNbtUtil.java create mode 100644 src/main/resources/assets/tsukichat/lang/en_us.json create mode 100644 src/main/resources/tsukichat.mixins.json diff --git a/.gitattributes b/.gitattributes index f811f6a..a6e97c7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,8 @@ -# Disable autocrlf on generated files, they always generate with LF -# Add any extra files or paths here to make git stop saying they -# are changed when only line endings change. -src/generated/**/.cache/cache text eol=lf -src/generated/**/*.json text eol=lf +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf \ No newline at end of file diff --git a/.gitignore b/.gitignore index 31d2550..a233ec2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,26 +1,40 @@ +# gradle + +.gradle/ +build/ +out/ +classes/ + # eclipse -bin + *.launch -.settings -.metadata -.classpath -.project # idea -out + +.idea/ +*.iml *.ipr *.iws -*.iml -.idea -# gradle -build -.gradle +# vscode + +.settings/ +.vscode/ +bin/ +.classpath +.project + +# macos + +*.DS_Store + +# fabric + +run/ -# other -eclipse -run -runs -run-data +# java -repo \ No newline at end of file +hs_err_*.log +replay_*.log +*.hprof +*.jfr \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8780749..fef04f7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,17 +1,8 @@ plugins { id 'java-library' id 'maven-publish' - id 'net.neoforged.moddev' version '2.0.78' id 'idea' -} - -tasks.named('wrapper', Wrapper).configure { - // Define wrapper values here so as to not have to always do so when updating gradlew.properties. - // Switching this to Wrapper.DistributionType.ALL will download the full gradle sources that comes with - // documentation attached on cursor hover of gradle classes and methods. However, this comes with increased - // file size for Gradle. If you do switch this to ALL, run the Gradle wrapper task twice afterwards. - // (Verify by checking gradle/wrapper/gradle-wrapper.properties to see if distributionUrl now points to `-all`) - distributionType = Wrapper.DistributionType.BIN + id 'net.neoforged.moddev' version '2.0.107' } version = mod_version @@ -19,13 +10,16 @@ group = mod_group_id repositories { mavenLocal() + + maven { + url "https://cursemaven.com" + } } base { archivesName = mod_id } -// Mojang ships Java 21 to end users starting in 1.20.5, so mods should target Java 21. java.toolchain.languageVersion = JavaLanguageVersion.of(21) neoForge { @@ -38,7 +32,7 @@ neoForge { } // This line is optional. Access Transformers are automatically detected - // accessTransformers = project.files('src/main/resources/META-INF/accesstransformer.cfg') + // accessTransformers.add('src/main/resources/META-INF/accesstransformer.cfg') // Default run configurations. // These can be tweaked, removed, or duplicated as needed. @@ -93,42 +87,24 @@ neoForge { mods { // define mod <-> source bindings // these are used to tell the game which sources are for which mod - // multi mod projects should define one per mod + // mostly optional in a single mod project + // but multi mod projects should define one per mod "${mod_id}" { sourceSet(sourceSets.main) } } - - unitTest { - // Enable JUnit support in the moddev plugin - enable() - // Configure which mod is being tested. - // This allows NeoForge to load the test/ classes and resources as belonging to the mod. - testedMod = mods."${mod_id}" // must match the name in the mods { } block. - // Configure which mods are loaded in the test environment, if the default (all declared mods) is not appropriate. - // This must contain testedMod, and can include other mods as well. - // loadedMods = [mods., mods.] - } } // Include resources generated by data generators. sourceSets.main.resources { srcDir 'src/generated/resources' } -// Sets up a dependency configuration called 'localRuntime'. -// This configuration should be used instead of 'runtimeOnly' to declare -// a dependency that will be present for runtime testing but that is -// "optional", meaning it will not be pulled by dependents of this mod. -configurations { - runtimeClasspath.extendsFrom localRuntime -} dependencies { - // Example optional mod dependency with JEI + // Example mod dependency with JEI // The JEI API is declared for compile time use, while the full JEI artifact is used at runtime // compileOnly "mezz.jei:jei-${mc_version}-common-api:${jei_version}" - // compileOnly "mezz.jei:jei-${mc_version}-neoforge-api:${jei_version}" - // We add the full version to localRuntime, not runtimeOnly, so that we do not publish a dependency on it - // localRuntime "mezz.jei:jei-${mc_version}-neoforge:${jei_version}" + // compileOnly "mezz.jei:jei-${mc_version}-forge-api:${jei_version}" + // runtimeOnly "mezz.jei:jei-${mc_version}-forge:${jei_version}" // Example mod dependency using a mod jar from ./libs with a flat dir repository // This maps to ./libs/coolmod-${mc_version}-${coolmod_version}.jar @@ -144,34 +120,32 @@ dependencies { // For more info: // http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html // http://www.gradle.org/docs/current/userguide/dependency_management.html + implementation "curse.maven:simple-discord-link-bot-forge-fabric-spigot-541320:6736713" + implementation "curse.maven:craterlib-867099:6534798" - testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.3' } // This block of code expands all declared replace properties in the specified resource targets. // A missing property will result in an error. Properties are expanded using ${} Groovy notation. var generateModMetadata = tasks.register("generateModMetadata", ProcessResources) { - var replaceProperties = [ - minecraft_version : minecraft_version, - minecraft_version_range: minecraft_version_range, - neo_version : neo_version, - neo_version_range : neo_version_range, - loader_version_range : loader_version_range, - mod_id : mod_id, - mod_name : mod_name, - mod_license : mod_license, - mod_version : mod_version, - mod_authors : mod_authors, - mod_description : mod_description - ] + var replaceProperties = [minecraft_version : minecraft_version, + minecraft_version_range: minecraft_version_range, + neo_version : neo_version, + neo_version_range : neo_version_range, + loader_version_range : loader_version_range, + mod_id : mod_id, + mod_name : mod_name, + mod_license : mod_license, + mod_version : mod_version, + mod_authors : mod_authors, + mod_description : mod_description] inputs.properties replaceProperties expand replaceProperties from "src/main/templates" into "build/generated/sources/modMetadata" } + // Include the output of "generateModMetadata" as an input directory for the build // this works with both building through Gradle and the IDE. sourceSets.main.resources.srcDir generateModMetadata @@ -192,10 +166,6 @@ publishing { } } -tasks.withType(JavaCompile).configureEach { - options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation -} - // IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior. idea { module { @@ -203,12 +173,3 @@ idea { downloadJavadoc = true } } - -test { - useJUnitPlatform() - testLogging { - events "passed", "skipped", "failed" - exceptionFormat "full" - showStandardStreams true - } -} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index efe99ca..c340b2b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,31 +1,26 @@ # Sets default memory used for gradle commands. Can be overridden by user or command line properties. -org.gradle.jvmargs=-Xmx1G +org.gradle.jvmargs=-Xmx2G org.gradle.daemon=true org.gradle.parallel=true org.gradle.caching=true org.gradle.configuration-cache=true - -#read more on this at https://github.com/neoforged/ModDevGradle?tab=readme-ov-file#better-minecraft-parameter-names--javadoc-parchment -# you can also find the latest versions at: https://parchmentmc.org/docs/getting-started -parchment_minecraft_version=1.21.1 -parchment_mappings_version=2024.11.17 -# Environment Properties +## Environment Properties # You can find the latest versions here: https://projects.neoforged.net/neoforged/neoforge # The Minecraft version must agree with the Neo version to get a valid artifact minecraft_version=1.21.1 # The Minecraft version range can use any release version of Minecraft as bounds. # Snapshots, pre-releases, and release candidates are not guaranteed to sort properly # as they do not follow standard versioning conventions. -minecraft_version_range=[1.21.1] +minecraft_version_range=[1.21.1,1.22) # The Neo version must agree with the Minecraft version to get a valid artifact -neo_version=21.1.132 +neo_version=21.1.200 # The Neo version range can use any version of Neo as bounds -neo_version_range=[21.1.132,) +neo_version_range=[21,) # The loader version range can only use the major version of FML as bounds -loader_version_range=[1,) - +loader_version_range=[4,) +parchment_minecraft_version=1.21.8 +parchment_mappings_version=2025.07.20 ## Mod Properties - # The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63} # Must match the String constant located in the main mod class annotated with @Mod. mod_id=tsukichat @@ -34,12 +29,12 @@ mod_name=Tsuki Chat # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=MIT # The mod version. See https://semver.org/ -mod_version=2.0.0 +mod_version=1.4.0-alpha.5 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. # See https://maven.apache.org/guides/mini/guide-naming-conventions.html -mod_group_id=io.github.meatwo310.tsukichat +mod_group_id=io.github.meatwo310 # The authors of the mod. This is a simple text string that is used for display purposes in the mod list. mod_authors=Meatwo310 # The description of the mod. This is a simple multiline text string that is used for display purposes in the mod list. -mod_description=Converts Romaji to Japanese in chat. /tsukichat to customize features. Highly configurable. +mod_description=Converts Romaji to Japanese in chat. diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b9530d66f5e68d973ea569d8e19de379189..e6441136f3d4ba8a0da8d277868979cfbc8ad796 100644 GIT binary patch delta 12460 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 delta 12612 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 HJ /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 9d21a21..25da30d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,8 +13,6 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## diff --git a/settings.gradle b/settings.gradle index 90ae98f..ada876e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,5 +7,5 @@ pluginManagement { } plugins { - id 'org.gradle.toolchains.foojay-resolver-convention' version '0.9.0' + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' } diff --git a/src/main/java/io/github/meatwo310/tsukichat/TsukiChat.java b/src/main/java/io/github/meatwo310/tsukichat/TsukiChat.java index 7eed562..d10e9ae 100644 --- a/src/main/java/io/github/meatwo310/tsukichat/TsukiChat.java +++ b/src/main/java/io/github/meatwo310/tsukichat/TsukiChat.java @@ -1,18 +1,52 @@ package io.github.meatwo310.tsukichat; import com.mojang.logging.LogUtils; -import io.github.meatwo310.tsukichat.config.CommonConfig; -import net.neoforged.fml.ModContainer; +import io.github.meatwo310.tsukichat.compat.SDLinkServerChatEvent; +import io.github.meatwo310.tsukichat.compat.mohist.MohistHelper; +import io.github.meatwo310.tsukichat.config.CommonConfigs; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.ModList; +import net.neoforged.fml.ModLoadingContext; import net.neoforged.fml.common.Mod; import net.neoforged.fml.config.ModConfig; +import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent; +import net.neoforged.fml.loading.FMLEnvironment; +import net.neoforged.neoforge.common.NeoForge; import org.slf4j.Logger; +// The value here should match an entry in the META-INF/mods.toml file @Mod(TsukiChat.MODID) public class TsukiChat { public static final String MODID = "tsukichat"; - private static final Logger LOGGER = LogUtils.getLogger(); + public static final Logger LOGGER = LogUtils.getLogger(); - public TsukiChat(ModContainer modContainer) { - modContainer.registerConfig(ModConfig.Type.COMMON, CommonConfig.SPEC); + public TsukiChat(IEventBus modEventBus) { + modEventBus.addListener(this::commonSetup); + ModLoadingContext.get().getActiveContainer().registerConfig(ModConfig.Type.COMMON, CommonConfigs.COMMON_SPEC, "tsukichat-common.toml"); + + // sdlink compat + if (FMLEnvironment.dist == Dist.DEDICATED_SERVER) { + if (ModList.get().isLoaded("sdlink")) { + LOGGER.info("Simple Discord Link is present!"); + LOGGER.info("Replacing the default chat event handler with Forge's event handler 😡"); + NeoForge.EVENT_BUS.register(new SDLinkServerChatEvent()); + } + } + + // Mohist Compat + if (FMLEnvironment.dist == Dist.DEDICATED_SERVER) { + if (!MohistHelper.isMohistLoaded()) return; + if (CommonConfigs.mohistCompat.get()) { + LOGGER.warn("Mohist is present! Oh no!"); + LOGGER.warn("We will check if the compat plugin is loaded..."); + } else { + LOGGER.warn("Mohist is present!"); + LOGGER.warn("Although the compat feature is disabled in the config so we will not do anything."); + } + } + } + + private void commonSetup(final FMLCommonSetupEvent event) { } } diff --git a/src/main/java/io/github/meatwo310/tsukichat/commands/ConfigCommand.java b/src/main/java/io/github/meatwo310/tsukichat/commands/ConfigCommand.java new file mode 100644 index 0000000..f5d43b0 --- /dev/null +++ b/src/main/java/io/github/meatwo310/tsukichat/commands/ConfigCommand.java @@ -0,0 +1,30 @@ +package io.github.meatwo310.tsukichat.commands; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.context.CommandContext; +import io.github.meatwo310.tsukichat.config.CommonConfigs; +import net.minecraft.commands.CommandSourceStack; + +public class ConfigCommand { + public static int defaultTeamMsg(CommandContext ctx) { + boolean currentValue = CommonConfigs.defaultTeamMsg.get(); + try { + boolean newValue = ctx.getArgument("value", Boolean.class); + if (currentValue == newValue) { + ctx.getSource().sendSuccess(() -> TsukiChatCommand.getComponent( + "チームメッセージはデフォルトで" + TsukiChatCommand.boolToStr(currentValue) + "のまま変更されませんでした" + ), true); + } else { + CommonConfigs.defaultTeamMsg.set(newValue); + ctx.getSource().sendSuccess(() -> TsukiChatCommand.getComponent( + "チームメッセージはデフォルトで" + TsukiChatCommand.boolToStr(newValue) + "に変更されました" + ), true); + } + } catch (IllegalArgumentException e) { + ctx.getSource().sendSuccess(() -> TsukiChatCommand.getComponent( + "チームメッセージはデフォルトで" + TsukiChatCommand.boolToStr(currentValue) + "です" + ), false); + } + return Command.SINGLE_SUCCESS; + } +} diff --git a/src/main/java/io/github/meatwo310/tsukichat/commands/CustomCommand.java b/src/main/java/io/github/meatwo310/tsukichat/commands/CustomCommand.java new file mode 100644 index 0000000..02ad3a2 --- /dev/null +++ b/src/main/java/io/github/meatwo310/tsukichat/commands/CustomCommand.java @@ -0,0 +1,215 @@ +package io.github.meatwo310.tsukichat.commands; + +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import net.minecraft.SharedConstants; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.ModList; +import net.neoforged.neoforge.internal.versions.neoforge.NeoForgeVersion; +import net.neoforged.neoforgespi.language.IModInfo; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import oshi.SystemInfo; +import oshi.hardware.Baseboard; +import oshi.hardware.CentralProcessor; +import oshi.hardware.ComputerSystem; +import oshi.hardware.HardwareAbstractionLayer; +import oshi.software.os.OSFileStore; +import oshi.software.os.OperatingSystem; +import oshi.util.Util; + +import java.util.Arrays; + +public class CustomCommand { + private static final DynamicCommandExceptionType ERROR_UNKNOWN_ARG = new DynamicCommandExceptionType(arg -> + Component.literal("不明な引数: " + arg) + ); + + public static int execute(CommandContext ctx) throws CommandSyntaxException { + String arg = ctx.getArgument("arg", String.class); + return switch (arg) { + case "fetch", "neofetch" -> { + ctx.getSource().sendSuccess(() -> TsukiChatCommand.getComponent(neofetch()), false); + yield 1; + } + default -> throw ERROR_UNKNOWN_ARG.create(arg); + }; + } + + public static String neofetch() { + SystemInfo si = new SystemInfo(); + OperatingSystem os = si.getOperatingSystem(); + HardwareAbstractionLayer hardware = si.getHardware(); + ComputerSystem hardwareSystem = hardware.getComputerSystem(); + Baseboard baseboard = hardwareSystem.getBaseboard(); + CentralProcessor processor = hardware.getProcessor(); + + StringBuilder result = new StringBuilder("§8===========§r §cn§6e§eo§af§3e§9t§5c§ch§r §8===========§r"); + + // Disk Usage + addInfo(result, "Disk Usage"); + hardware.getDiskStores().forEach(disk -> { + addInfo(result, disk.getName(), disk.getModel()); + disk.getPartitions().forEach(partition -> { + String mountPoint = partition.getMountPoint(); + OSFileStore osFileStore = os.getFileSystem().getFileStores().stream() + .filter(fs -> fs.getMount().equals(mountPoint)) + .findFirst().orElse(null); + long used = osFileStore == null ? 0 : osFileStore.getTotalSpace() - osFileStore.getUsableSpace(); + addInfo(result, "|-" + partition.getIdentification(), + "[" + partition.getType() + "]", + osFileStore == null ? "unknown" : humanReadableByteCount(used), + "/", + osFileStore == null ? humanReadableByteCount(partition.getSize()) : + String.format("%s (%.1f%% used) %s", + humanReadableByteCount(osFileStore.getTotalSpace()), + used * 100.0 / osFileStore.getTotalSpace(), + mountPoint.replaceAll("\\\\040", " ") + ) + ); + }); + }); + + result.append("\n§8= = = = = = = = = = = = = = = = = = = = = = =§r"); + + // Various Info + addInfo(result, "OS", os.getManufacturer(), os.getFamily(), os.getVersionInfo().toString(), System.getProperty("os.arch")); + addInfo(result, "Host", os.getNetworkParams().getHostName()); + addInfo(result, "Model", hardwareSystem.getManufacturer(), hardwareSystem.getModel()); + addInfo(result, "Baseboard", baseboard.getManufacturer(), baseboard.getModel()); + addInfo(result, "Uptime", formatUptime(os.getSystemUptime())); + addFormattedInfo(result, "CPU", "%s (%d cores, %d threads)", + processor.getProcessorIdentifier().getName(), + processor.getPhysicalProcessorCount(), + processor.getLogicalProcessorCount() + ); + + // Get CPU Usage of: + // // - this process + // - system + // - each core + + long[] oldTicks = processor.getSystemCpuLoadTicks(); + long[][] oldProcTicks = processor.getProcessorCpuLoadTicks(); + Util.sleep(400); + double systemCpuUsage = processor.getSystemCpuLoadBetweenTicks(oldTicks); + double[] processorCpuUsages = processor.getProcessorCpuLoadBetweenTicks(oldProcTicks); + + addInfo(result, "CPU Usage", +// String.format("thr %.1f%%;"), + String.format("sys %.1f%%;", systemCpuUsage * 100), + "cores: " + String.join(" ", Arrays.stream(processorCpuUsages) + .mapToObj(d -> String.format("%.0f%%", d * 100)) + .toArray(String[]::new) + ) + ); + + + // GPU + hardware.getGraphicsCards().forEach(graphicsCard -> addInfo( + result, "GPU", graphicsCard.getName(), + "(" + humanReadableByteCount(graphicsCard.getVRam()) + " VRAM)" + )); + + // Minecraft + if (ModList.get() != null) addInfo(result, "Game", + "Minecraft %s with".formatted(SharedConstants.getCurrentVersion().getName()), + "Forge %s [%s]".formatted( + NeoForgeVersion.getVersion(), + ModList.get().getModContainerById(NeoForgeVersion.MOD_ID) + .map(ModContainer::getModInfo) + .map(IModInfo::getDisplayName) + .orElse("Forge") + ) + ); + + addFormattedInfo(result, "Java", "%s; %s %s (%s) [%s]", + System.getProperty("java.version"), + System.getProperty("java.vm.name"), + System.getProperty("java.vm.version"), + System.getProperty("java.vm.info"), + System.getProperty("java.vendor") + ); + + // JVM Memory + long jvmAllocated = Runtime.getRuntime().totalMemory(); + long jvmFree = Runtime.getRuntime().freeMemory(); + long jvmUsed = jvmAllocated - jvmFree; + long jvmAllocatedMax = Runtime.getRuntime().maxMemory(); + addInfo(result, "JVM Memory", String.format("%s / %s (%s used) [allocated: %s]", + humanReadableByteCount(jvmUsed), + humanReadableByteCount(jvmAllocatedMax), + jvmUsed * 100 / jvmAllocatedMax + "%", + humanReadableByteCount(jvmAllocated) + )); + + // System Memory + long systotalmem = hardware.getMemory().getTotal(); + long sysfreemem = hardware.getMemory().getAvailable(); + long sysusedmem = systotalmem - sysfreemem; + addInfo(result, "System Memory", + humanReadableByteCount(sysusedmem), "/", humanReadableByteCount(systotalmem), + "(" + sysusedmem * 100 / systotalmem + "% used)" + ); + + // Get System Swap Usage + long swapTotal = hardware.getMemory().getVirtualMemory().getSwapTotal(); + long swapUsed = hardware.getMemory().getVirtualMemory().getSwapUsed(); + if (swapTotal <= 0) { + addInfo(result, "System Swap", "not available"); + } else { + addInfo(result, "System Swap", + humanReadableByteCount(swapUsed), "/", humanReadableByteCount(swapTotal), + "(" + swapUsed * 100 / swapTotal + "% used)" + ); + } + + // Get Virtual Memory Usage +// long virtualMemTotal = hardware.getMemory().getVirtualMemory().getVirtualMax(); +// long virtualMemUsed = hardware.getMemory().getVirtualMemory().getVirtualInUse(); +// addInfo(result, "Virtual Memory", +// humanReadableByteCount(virtualMemUsed), "/", humanReadableByteCount(virtualMemTotal), +// "(" + virtualMemUsed * 100 / virtualMemTotal + "% used)" +// ); + + result.append("\n§8========================================§r"); + + return result.toString(); + } + + private static void addInfo(StringBuilder result, String title, String... info) { + if (!result.isEmpty()) result.append("\n"); + result.append("§7").append(title).append(":§r "); + for (String s : info) { + result.append(s).append(" "); + } + result.deleteCharAt(result.length() - 1); + } + + private static void addFormattedInfo(@NotNull StringBuilder result, @NotNull String title, @NotNull String format, @Nullable Object... args) { + if (!result.isEmpty()) result.append("\n"); + result.append("§7").append(title).append(":§r "); + result.append(String.format(format, args)); + } + + private static String formatUptime(long seconds) { + long hours = seconds / 3600; + long minutes = (seconds % 3600) / 60; + return String.format("%dh %dm", hours, minutes); + } + + private static String humanReadableByteCount(long bytes) { + int unit = 1024; + if (bytes < unit) return bytes + " B"; + int exp = (int) (Math.log(bytes) / Math.log(unit)); + String pre = "KMGTPE".charAt(exp - 1) + "i"; + return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); + } + + public static void main(String[] args) { + System.out.println(CustomCommand.neofetch().replaceAll("§.", "")); + } +} diff --git a/src/main/java/io/github/meatwo310/tsukichat/commands/PermissionCommand.java b/src/main/java/io/github/meatwo310/tsukichat/commands/PermissionCommand.java new file mode 100644 index 0000000..c47faca --- /dev/null +++ b/src/main/java/io/github/meatwo310/tsukichat/commands/PermissionCommand.java @@ -0,0 +1,35 @@ +package io.github.meatwo310.tsukichat.commands; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.context.CommandContext; +import io.github.meatwo310.tsukichat.config.CommonConfigs; +import net.minecraft.commands.CommandSourceStack; + +public class PermissionCommand { + public static int allowPersonalSettings(CommandContext ctx) { + CommonConfigs.allowPersonalSettings.set(ctx.getArgument("value", Boolean.class)); + ctx.getSource().sendSuccess(() -> TsukiChatCommand.getComponent( + "個人設定の変更を", CommonConfigs.allowPersonalSettings.get() ? "許可" : "禁止", "しました。"), + true + ); + return Command.SINGLE_SUCCESS; + } + + public static int allowAddingServerDictionary(CommandContext ctx) { + CommonConfigs.allowAddingServerDictionary.set(ctx.getArgument("value", Boolean.class)); + ctx.getSource().sendSuccess(() -> TsukiChatCommand.getComponent( + "サーバー辞書への単語の追加を", CommonConfigs.allowAddingServerDictionary.get() ? "許可" : "禁止", "しました。"), + true + ); + return Command.SINGLE_SUCCESS; + } + + public static int allowRemovingServerDictionary(CommandContext ctx) { + CommonConfigs.allowRemovingServerDictionary.set(ctx.getArgument("value", Boolean.class)); + ctx.getSource().sendSuccess(() -> TsukiChatCommand.getComponent( + "サーバー辞書からの単語の削除を", CommonConfigs.allowRemovingServerDictionary.get() ? "許可" : "禁止", "しました。"), + true + ); + return Command.SINGLE_SUCCESS; + } +} diff --git a/src/main/java/io/github/meatwo310/tsukichat/commands/ServerDictionaryCommand.java b/src/main/java/io/github/meatwo310/tsukichat/commands/ServerDictionaryCommand.java new file mode 100644 index 0000000..6254cfe --- /dev/null +++ b/src/main/java/io/github/meatwo310/tsukichat/commands/ServerDictionaryCommand.java @@ -0,0 +1,152 @@ +package io.github.meatwo310.tsukichat.commands; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import io.github.meatwo310.tsukichat.config.CommonConfigs; +import net.minecraft.commands.CommandSourceStack; + +import java.util.LinkedHashMap; +import java.util.List; + +public class ServerDictionaryCommand { + private static final SimpleCommandExceptionType ERROR_NO_PERMISSION_TO_ADD = new SimpleCommandExceptionType( + TsukiChatCommand.getErrorComponent("サーバー辞書に単語を追加する権限がありません。") + ); + private static final SimpleCommandExceptionType ERROR_NO_PERMISSION_TO_REMOVE = new SimpleCommandExceptionType( + TsukiChatCommand.getErrorComponent("サーバー辞書から単語を削除する権限がありません。") + ); + private static final DynamicCommandExceptionType ERROR_WORD_NOT_FOUND = new DynamicCommandExceptionType(key -> + TsukiChatCommand.getErrorComponent("サーバー辞書にキー " + key + " は存在しません。") + ); + private static final SimpleCommandExceptionType ERROR_NOT_CONFIRMED = new SimpleCommandExceptionType( + TsukiChatCommand.getErrorComponent("サーバー辞書を全消去するには、 引数に§4YES§cを付加してください。") + ); + + static int add(CommandContext ctx) throws CommandSyntaxException { + if (!checkPermission(ctx, PermissionActionType.ADD)) { + throw ERROR_NO_PERMISSION_TO_ADD.create(); + } + + LinkedHashMap serverDictionary = getServerDictionary(); + String key = ctx.getArgument("key", String.class); + String value = ctx.getArgument("value", String.class); + + if (serverDictionary.containsKey(key)) { + ctx.getSource().sendSuccess(() -> TsukiChatCommand.getComponent( + "サーバー辞書の単語を更新しました: ", key, " → ", value, " (元の値: ", serverDictionary.get(key), ")"), + true + ); + } else { + ctx.getSource().sendSuccess(() -> TsukiChatCommand.getComponent( + "サーバー辞書に単語を追加しました: ", key, " → ", value), + true + ); + } + serverDictionary.put(key, value); + setServerDictionary(serverDictionary); + + return Command.SINGLE_SUCCESS; + } + + static int remove(CommandContext ctx) throws CommandSyntaxException { + if (!checkPermission(ctx, PermissionActionType.REMOVE)) { + throw ERROR_NO_PERMISSION_TO_REMOVE.create(); + } + + LinkedHashMap serverDictionary = getServerDictionary(); + String key = ctx.getArgument("key", String.class); + + if (serverDictionary.containsKey(key)) { + serverDictionary.remove(key); + setServerDictionary(serverDictionary); + ctx.getSource().sendSuccess( + () -> TsukiChatCommand.getComponent( + "サーバー辞書から単語を削除しました: ", key), + true + ); + } else { + throw ERROR_WORD_NOT_FOUND.create(key); + } + + return Command.SINGLE_SUCCESS; + } + + static int removeAll(CommandContext ctx) throws CommandSyntaxException { + if (!checkPermission(ctx, PermissionActionType.REMOVE)) { + throw ERROR_NO_PERMISSION_TO_REMOVE.create(); + } + + String confirm = ctx.getArgument("type_YES_if_you_are_sure", String.class); + if (!confirm.equals("YES")) { + throw ERROR_NOT_CONFIRMED.create(); + } + + LinkedHashMap serverDictionary = getServerDictionary(); + serverDictionary.clear(); + setServerDictionary(serverDictionary); + ctx.getSource().sendSuccess(() -> TsukiChatCommand.getComponent( + "サーバー辞書をクリアしました。"), + true + ); + + return Command.SINGLE_SUCCESS; + } + + static int list(CommandContext ctx) { + LinkedHashMap serverDictionary = getServerDictionary(); + if (serverDictionary.isEmpty()) { + ctx.getSource().sendSuccess(() -> TsukiChatCommand.getComponent( + "サーバー辞書は空です。"), + false + ); + return Command.SINGLE_SUCCESS; + } + + String contents = serverDictionary.entrySet().stream() + .map(entry -> entry.getKey() + " → " + entry.getValue()) + .reduce((a, b) -> a + ",\n" + b) + .orElse(""); + + ctx.getSource().sendSuccess(() -> TsukiChatCommand.getComponent( + "サーバー辞書の内容:\n", contents), + false + ); + + return Command.SINGLE_SUCCESS; + } + + public static LinkedHashMap getServerDictionary() { + List serverDictionary = CommonConfigs.serverDictionary.get(); + return serverDictionary.stream() + .collect(LinkedHashMap::new, (map, entry) -> { + String[] keyValue = entry.split("\t", 2); + if (keyValue.length == 1) { + map.put(keyValue[0], ""); + } else if (keyValue.length == 2) { + map.put(keyValue[0], keyValue[1]); + } + }, LinkedHashMap::putAll); + } + + public static void setServerDictionary(LinkedHashMap serverDictionary) { + CommonConfigs.serverDictionary.set(serverDictionary.entrySet().stream() + .map(entry -> entry.getKey() + "\t" + entry.getValue()) + .toList()); + } + + private static boolean checkPermission(CommandContext ctx, PermissionActionType actionType) { + if (ctx.getSource().hasPermission(2)) return true; + return switch (actionType) { + case ADD -> CommonConfigs.allowAddingServerDictionary.get(); + case REMOVE -> CommonConfigs.allowRemovingServerDictionary.get(); + }; + } + + private enum PermissionActionType { + ADD, + REMOVE + } +} diff --git a/src/main/java/io/github/meatwo310/tsukichat/commands/TsukiChatCommand.java b/src/main/java/io/github/meatwo310/tsukichat/commands/TsukiChatCommand.java new file mode 100644 index 0000000..50e5139 --- /dev/null +++ b/src/main/java/io/github/meatwo310/tsukichat/commands/TsukiChatCommand.java @@ -0,0 +1,163 @@ +package io.github.meatwo310.tsukichat.commands; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import io.github.meatwo310.tsukichat.config.CommonConfigs; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Player; + +import java.util.Set; + +public class TsukiChatCommand { + static final String PLACEHOLDER = "§e[TsukiChat]§r "; + private static final SimpleCommandExceptionType ERROR_PERSONAL_SETTINGS_DISABLED = new SimpleCommandExceptionType( + TsukiChatCommand.getErrorComponent("個人設定はサーバーによって無効化されています。") + ); + + public static Component getComponent(String ...message) { + return Component.literal(PLACEHOLDER + String.join("", message)); + } + + public static Component getErrorComponent(String ...message) { + return Component.literal(PLACEHOLDER + "§c" + String.join("", message)); + } + + public static String boolToStr(boolean value) { + return value ? "§a有効§r" : "§c無効§r"; + } + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(Commands.literal("tsukichat") + .then(Commands.literal("mode") + .then(Commands.literal("enable") + .executes(commandContext -> mode(commandContext, ModeType.ENABLE))) + .then(Commands.literal("markdown") + .executes(commandContext -> mode(commandContext, ModeType.MARKDOWN))) + .then(Commands.literal("disable") + .executes(commandContext -> mode(commandContext, ModeType.DISABLE))) + .then(Commands.literal("toggle") + .executes(commandContext -> mode(commandContext, ModeType.TOGGLE))) + .executes(commandContext -> mode(commandContext, ModeType.TOGGLE)) + ) + .then(Commands.literal("userdict") + .then(Commands.literal("add") + .then(Commands.argument("key", StringArgumentType.string()) + .then(Commands.argument("value", StringArgumentType.string()) + .executes(UserDictionaryCommand::add))) + ) + .then(Commands.literal("remove") + .then(Commands.argument("key", StringArgumentType.string()) + .executes(UserDictionaryCommand::remove)) + ) + .then(Commands.literal("removeall") + .then(Commands.argument("type_YES_if_you_are_sure", StringArgumentType.string()) + .executes(UserDictionaryCommand::removeAll)) + ) + .then(Commands.literal("list") + .executes(UserDictionaryCommand::list) + ) + ).then(Commands.literal("serverdict") + .then(Commands.literal("add") + .then(Commands.argument("key", StringArgumentType.string()) + .then(Commands.argument("value", StringArgumentType.string()) + .executes(ServerDictionaryCommand::add))) + ) + .then(Commands.literal("remove") + .then(Commands.argument("key", StringArgumentType.string()) + .executes(ServerDictionaryCommand::remove)) + ) + .then(Commands.literal("removeall") + .then(Commands.argument("type_YES_if_you_are_sure", StringArgumentType.string()) + .executes(ServerDictionaryCommand::removeAll)) + ) + .then(Commands.literal("list") + .executes(ServerDictionaryCommand::list) + ) + ).then(Commands.literal("permission") + .requires(commandSourceStack -> commandSourceStack.hasPermission(2)) + .then(Commands.literal("allow_personal_settings") + .then(Commands.argument("value", BoolArgumentType.bool()) + .executes(PermissionCommand::allowPersonalSettings))) + .then(Commands.literal("allow_adding_server_dictionary") + .then(Commands.argument("value", BoolArgumentType.bool()) + .executes(PermissionCommand::allowAddingServerDictionary))) + .then(Commands.literal("allow_removing_server_dictionary") + .then(Commands.argument("value", BoolArgumentType.bool()) + .executes(PermissionCommand::allowRemovingServerDictionary))) + ).then(Commands.literal("config") + .requires(commandSourceStack -> commandSourceStack.hasPermission(2)) + .then(Commands.literal("defaultTeamMsg") + .then(Commands.argument("value", BoolArgumentType.bool()) + .executes(ConfigCommand::defaultTeamMsg) + ).executes(ConfigCommand::defaultTeamMsg) + ) + ).then(Commands.argument("arg", StringArgumentType.string()) + .executes((CustomCommand::execute))) + ); + } + + private enum ModeType { + ENABLE, + MARKDOWN, + DISABLE, + TOGGLE, + } + + private static int mode(CommandContext ctx, ModeType modeType) throws CommandSyntaxException { + Player player = (Player) ctx.getSource().getEntity(); + + if (!(player instanceof Player)) return Command.SINGLE_SUCCESS; + + boolean allowPersonalSettings = CommonConfigs.allowPersonalSettings.get(); + if (!allowPersonalSettings) { + throw ERROR_PERSONAL_SETTINGS_DISABLED.create(); + } + + Set tags = player.getTags(); + String ignoreCompletelyTag = CommonConfigs.ignoreCompletelyTag.get(); + String ignoreTag = CommonConfigs.ignoreTag.get(); + + StringBuilder message = new StringBuilder("個人設定を変更しました: "); + switch (modeType) { + case ENABLE -> { + tags.remove(ignoreCompletelyTag); + tags.remove(ignoreTag); + message.append("§a有効§r"); + } + case MARKDOWN -> { + tags.remove(ignoreCompletelyTag); + tags.add(ignoreTag); + message.append("§eMarkdownのみ§r"); + } + case DISABLE -> { + tags.remove(ignoreTag); + tags.add(ignoreCompletelyTag); + message.append("§c無効§r"); + } + case TOGGLE -> { + if (tags.contains(ignoreCompletelyTag)) { + tags.remove(ignoreCompletelyTag); + tags.remove(ignoreTag); + message.append("§a有効§r"); + } else if (tags.contains(ignoreTag)) { + tags.remove(ignoreTag); + tags.add(ignoreCompletelyTag); + message.append("§c無効§r"); + } else { + tags.add(ignoreTag); + message.append("§eMarkdownのみ§r"); + } + } + } + + ctx.getSource().sendSuccess(() -> TsukiChatCommand.getComponent(message.toString()), false); + return Command.SINGLE_SUCCESS; + } +} diff --git a/src/main/java/io/github/meatwo310/tsukichat/commands/UserDictionaryCommand.java b/src/main/java/io/github/meatwo310/tsukichat/commands/UserDictionaryCommand.java new file mode 100644 index 0000000..556741e --- /dev/null +++ b/src/main/java/io/github/meatwo310/tsukichat/commands/UserDictionaryCommand.java @@ -0,0 +1,133 @@ +package io.github.meatwo310.tsukichat.commands; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import io.github.meatwo310.tsukichat.compat.mohist.MohistHelper; +import io.github.meatwo310.tsukichat.util.PlayerNbtUtil; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.entity.player.Player; + +public class UserDictionaryCommand { + public static final String KEY_NAME = "dict"; + + private static final SimpleCommandExceptionType ERROR_NOT_IMPLEMENTED_ON_MOHIST = new SimpleCommandExceptionType( + TsukiChatCommand.getErrorComponent("Mohist環境下では未実装です。") + ); + private static final SimpleCommandExceptionType ERROR_NOT_CONFIRMED = new SimpleCommandExceptionType( + TsukiChatCommand.getErrorComponent("ユーザー辞書を全消去するには、 引数に§cYES§rを付加してください。") + ); + private static final SimpleCommandExceptionType ERROR_FAILED_TO_REMOVE = new SimpleCommandExceptionType( + TsukiChatCommand.getErrorComponent("ユーザー辞書の削除に失敗しました。") + ); + + private static void checkMohist() throws CommandSyntaxException { + if (MohistHelper.isMohistLoaded() && MohistHelper.isCompatPluginLoaded()) { + throw ERROR_NOT_IMPLEMENTED_ON_MOHIST.create(); + } + } + + static int add(CommandContext ctx) throws CommandSyntaxException { + if (!(ctx.getSource().getEntity() instanceof Player player)) return Command.SINGLE_SUCCESS; + checkMohist(); + + String key = ctx.getArgument("key", String.class); + String value = ctx.getArgument("value", String.class); + + CompoundTag entry = new CompoundTag(); + entry.putString(key, value); + + if (PlayerNbtUtil.saveCompoundTag(player, KEY_NAME, entry)) { + ctx.getSource().sendSuccess(() -> TsukiChatCommand.getComponent( + "ユーザー辞書を更新しました: ", key, " → ", value + ), false); + } else { + ctx.getSource().sendSuccess(() -> TsukiChatCommand.getComponent( + "ユーザー辞書を設定しました: ", key, " → ", value + ), false); + } + + return Command.SINGLE_SUCCESS; + } + + static int remove(CommandContext ctx) throws CommandSyntaxException { + if (!(ctx.getSource().getEntity() instanceof Player player)) return Command.SINGLE_SUCCESS; + checkMohist(); + + String key = ctx.getArgument("key", String.class); + if (PlayerNbtUtil.removeTag(player, KEY_NAME, key)) { + ctx.getSource().sendSuccess(() -> TsukiChatCommand.getComponent( + "ユーザー辞書から削除しました: ", key + ), false); + } else { + ctx.getSource().sendSuccess(() -> TsukiChatCommand.getComponent( + "ユーザー辞書にキー ", key, " は存在しません。" + ), false); + } + + return Command.SINGLE_SUCCESS; + } + + static int removeAll(CommandContext ctx) throws CommandSyntaxException { + if (!(ctx.getSource().getEntity() instanceof Player player)) return Command.SINGLE_SUCCESS; + checkMohist(); + + String confirm = ctx.getArgument("type_YES_if_you_are_sure", String.class); + + CompoundTag tag = PlayerNbtUtil.loadCompoundTag(player, KEY_NAME); + + if (tag.isEmpty()) { + ctx.getSource().sendSuccess(() -> TsukiChatCommand.getComponent( + "ユーザー辞書は既に空です。" + ), false); + return Command.SINGLE_SUCCESS; + } + + if (!confirm.equals("YES")) { + throw ERROR_NOT_CONFIRMED.create(); + } + + if (PlayerNbtUtil.removeWhole(player, KEY_NAME)) { + ctx.getSource().sendSuccess(() -> TsukiChatCommand.getComponent( + "ユーザー辞書を全て削除しました。" + ), false); + } else { + throw ERROR_FAILED_TO_REMOVE.create(); + } + + return Command.SINGLE_SUCCESS; + } + + static int list(CommandContext ctx) throws CommandSyntaxException { + if (!(ctx.getSource().getEntity() instanceof Player player)) return Command.SINGLE_SUCCESS; + checkMohist(); + + CompoundTag tag = PlayerNbtUtil.loadCompoundTag(player, KEY_NAME); + + if (tag.isEmpty()) { + ctx.getSource().sendSuccess(() -> TsukiChatCommand.getComponent( + "ユーザー辞書は空です。" + ), false); + return Command.SINGLE_SUCCESS; + } + + StringBuilder message = new StringBuilder(); + + message.append("現在のユーザー辞書("); + message.append(tag.getAllKeys().size()); + message.append("エントリ): "); + + tag.getAllKeys().forEach(k -> { + message.append("\n"); + message.append(k); + message.append(" → "); + message.append(tag.getString(k)); + }); + + ctx.getSource().sendSuccess(() -> TsukiChatCommand.getComponent(message.toString()), false); + + return Command.SINGLE_SUCCESS; + } +} diff --git a/src/main/java/io/github/meatwo310/tsukichat/compat/SDLinkServerChatEvent.java b/src/main/java/io/github/meatwo310/tsukichat/compat/SDLinkServerChatEvent.java new file mode 100644 index 0000000..9a4976f --- /dev/null +++ b/src/main/java/io/github/meatwo310/tsukichat/compat/SDLinkServerChatEvent.java @@ -0,0 +1,35 @@ +package io.github.meatwo310.tsukichat.compat; + +import com.hypherionmc.craterlib.nojang.world.entity.player.BridgedPlayer; +import com.hypherionmc.sdlink.core.managers.HiddenPlayersManager; +import com.hypherionmc.sdlink.platform.SDLinkMCPlatform; +import com.hypherionmc.sdlink.server.ServerEvents; +import io.github.meatwo310.tsukichat.config.CommonConfigs; +import net.minecraft.core.HolderLookup; +import net.minecraft.network.chat.Component; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.event.ServerChatEvent; +import shadow.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import com.mojang.serialization.JsonOps; + +public class SDLinkServerChatEvent { + @SubscribeEvent + public void onServerChat(ServerChatEvent event) { + if (!CommonConfigs.sdlinkCompat.get()) return; + + BridgedPlayer player = BridgedPlayer.of(event.getPlayer()); + String messageString = Component.Serializer.toJson(event.getMessage(), (HolderLookup.Provider) JsonOps.INSTANCE); + shadow.kyori.adventure.text.Component adventureComponent = GsonComponentSerializer.gson().deserialize(messageString); + + if (SDLinkMCPlatform.INSTANCE.playerIsActive(player)) { + if (!HiddenPlayersManager.INSTANCE.isPlayerHidden(event.getPlayer().getStringUUID())) { + ServerEvents.getInstance().onServerChatEvent( + adventureComponent, + player.getDisplayName(), + SDLinkMCPlatform.INSTANCE.getPlayerSkinUUID(player), + player.getGameProfile(), + false); + } + } + } +} diff --git a/src/main/java/io/github/meatwo310/tsukichat/compat/mohist/MohistHelper.java b/src/main/java/io/github/meatwo310/tsukichat/compat/mohist/MohistHelper.java new file mode 100644 index 0000000..2f6e788 --- /dev/null +++ b/src/main/java/io/github/meatwo310/tsukichat/compat/mohist/MohistHelper.java @@ -0,0 +1,37 @@ +package io.github.meatwo310.tsukichat.compat.mohist; + +import io.github.meatwo310.tsukichat.config.CommonConfigs; + +public class MohistHelper { + private static final boolean mohistLoaded; + public static boolean compatPluginLoaded = false; + + static { + boolean loaded; + try { + Class.forName("com.mohistmc.MohistMC"); + loaded = true; + } catch (ClassNotFoundException e) { + loaded = false; + } + mohistLoaded = loaded; + } + + public static boolean isMohistCompatEnabled() { + return CommonConfigs.mohistCompat.get(); + } + + public static boolean isMohistLoaded() { + return mohistLoaded; + } + + public static boolean isCompatPluginLoaded() { +// try { +// Class.forName("io.github.meatwo310.tsukichat.compat.mohist.TsukiChatPlugin"); +// return true; +// } catch (ClassNotFoundException e) { +// return false; +// } + return compatPluginLoaded; + } +} diff --git a/src/main/java/io/github/meatwo310/tsukichat/config/CommonConfigs.java b/src/main/java/io/github/meatwo310/tsukichat/config/CommonConfigs.java new file mode 100644 index 0000000..82028b2 --- /dev/null +++ b/src/main/java/io/github/meatwo310/tsukichat/config/CommonConfigs.java @@ -0,0 +1,195 @@ +package io.github.meatwo310.tsukichat.config; + +import io.github.meatwo310.tsukichat.TsukiChat; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.common.ModConfigSpec; + +import java.util.List; + +public class CommonConfigs { + private static final ModConfigSpec.Builder builder = new ModConfigSpec.Builder(); + + public static ModConfigSpec.BooleanValue transliterate = builder + .comment("ローマ字を漢字混じりの日本語に変換するかどうか。\n" + + "無効にすると、ローマ字変換のみ行います。") + .define("transliterate", true); + public static ModConfigSpec.BooleanValue ignoreNonAscii = builder + .comment("ASCII文字以外を含むメッセージのローマ字変換と日本語変換を無視するかどうか。\n" + + "このオプションは、マークダウン変換を行うかどうかに影響しません。") + .define("ignore_non_ascii", true); + public static ModConfigSpec.BooleanValue ampersand = builder + .comment("&を§に変換するかどうか。") + .define("ampersand", true); + public static ModConfigSpec.BooleanValue markdown = builder + .comment("単純なマークダウンを装飾コードに変換するかどうか。\n" + + "複雑なマークダウンはうまく変換されない場合があります。") + .define("markdown", true); + public static ModConfigSpec.BooleanValue allowPersonalSettings = builder + .comment("プレイヤーが /tsukichat で個人設定を変更できるようにするかどうか。\n" + + "無効にした場合でも、ignoreTagとignoreCompletelyTagは使用されます。") + .define("allow_personal_settings", true); + public static ModConfigSpec.BooleanValue multiThreading = builder + .comment(""" + [非推奨] ローマ字から日本語への変換をTsukiChat側のマルチスレッドで行うかどうか。 + 比較的新しいMinecraftではForgeがイベントをマルチスレッドで処理するため、 + この設定を有効化する必要はありません。""") + .define("multi_threading", false); + public static ModConfigSpec.BooleanValue sdlinkCompat = builder + .comment("Simple Discord Link上でtsukichatを作用させるかどうか。\n" + + "無効にすると、Simple Discord Linkは従来どおり変換前のメッセージを使用します。") + .define("sdlink_compat", true); + public static ModConfigSpec.BooleanValue allowAddingServerDictionary = builder + .comment("通常のプレイヤーがサーバー辞書に単語を追加できるようにするかどうか。\n" + + "無効にした場合でも、OP権限を持つプレイヤーは辞書を管理することができます。") + .define("allow_add_global_dictionary", true); + public static ModConfigSpec.BooleanValue allowRemovingServerDictionary = builder + .comment("通常のプレイヤーがサーバー辞書から単語を削除できるようにするかどうか。\n" + + "無効にした場合でも、OP権限を持つプレイヤーは辞書を管理することができます。") + .define("allow_remove_global_dictionary", true); + public static ModConfigSpec.BooleanValue mohistCompat = builder + .comment(""" + Mohist上でSpigotのイベントシステムを使用するかどうか。 + Mohist上でForgeのServerChatEventが発火しない問題の回避策となります。 + Mohist環境下でない場合、このオプションは単に無視されます。""") + .define("mohist_compat", true); + + public static ModConfigSpec.ConfigValue> ignore = builder + .comment("TsukiChatは、以下の接頭辞から始まるメッセージのローマ字変換や日本語変換を行いません。\n" + + "ただし、マークダウンの変換は行われます。") + .define("ignore", List.of("#", ";")); + public static ModConfigSpec.ConfigValue> ignoreCompletely = builder + .comment("TsukiChatは、以下の接頭辞から始まるメッセージについて、一切の変換を行いません。") + .define("ignore_completely", List.of(":", "./")); + public static ModConfigSpec.ConfigValue> ignoreMessages = builder + .comment("メッセージが以下のリストのいずれかと一致する場合、一切の変換を行いません。") + .define("ignore_messages", List.of( + "afk", + "bad", + "brb", + "btw", + "cool", + "ez", + "false", + "fine", + "gg", + "gj", + "gl", + "good", + "great", + "hello", + "hf", + "hi", + "idk", + "jk", + "lmao", + "lol", + "maybe", + "nc", + "nice", + "no", + "nope", + "np", + "nvm", + "oh", + "ok", + "okay", + "omg", + "please", + "pls", + "plz", + "rofl", + "sorry", + "sry", + "sure", + "thanks", + "thx", + "true", + "ty", + "tysm", + "w", + "wow", + "wtf", + "ww", + "www", + "wwww", + "wwwww", + "wwwwww", + "wwwwwww", + "wwwwwwww", + "wwwwwwwww", + "wwwwwwwwww", + "yay", + "yeah", + "yes", + "yw" + ), o -> true); + + public static ModConfigSpec.ConfigValue> serverDictionary = builder + .comment("サーバー辞書。ここに登録された単語は、すべてのプレイヤーのメッセージに対して適用されます。\n" + + "キーと値は、タブ文字\\tで区切ってください。") + .define("server_dictionary", List.of( + "TsukiChat\tTsukiChat", + "Minecraft\tMinecraft", + "Forge\tForge", + "Fabric\tFabric", + "Mod\tMod", + "NeoForge\tNeoForge", + "Google\tGoogle", + "GitHub\tGitHub", + "Java\tJava", + "っw\tww" + )); + + public static ModConfigSpec.IntValue ignoreLength = builder + .comment("変換前のメッセージの長さがこの値以下の場合、ローマ字変換や日本語変換を行いません。\n" + + "ただし、マークダウンの変換は行われます。") + .defineInRange("ignore_length", 3, 0, Integer.MAX_VALUE); + + public static ModConfigSpec.ConfigValue formatOriginal = builder + .comment("変換前のメッセージをどう表示するかを指定します。\n" + + "$0は変換前のメッセージに置き換えられます。") + .define("format_original", "§7$0§r"); + public static ModConfigSpec.ConfigValue formatConverted = builder + .comment("変換後のメッセージをどう表示するかを指定します。\n" + + "$0は変換後のメッセージに置き換えられます。") + .define("format_converted", "→ $0"); + public static ModConfigSpec.ConfigValue formatOriginalIgnored = builder + .comment("コンフィグignoreで設定された接頭辞で始まるメッセージがMarkdown変換されなかった際にどう表示するかを指定します。\n" + + "$0は接頭辞、$1はメッセージのうち接頭辞以外の部分に置き換えられます。") + .define("format_original_ignored", "§7$0§r$1§r"); + public static ModConfigSpec.ConfigValue formatConvertedIgnored = builder + .comment("コンフィグignoreで設定された接頭辞から始まるメッセージがMarkdown変換された際にどう表示するかを指定します。\n" + + "$0は接頭辞、$1は変換後のメッセージのうち接頭辞以外の部分に置き換えられます。") + .define("format_converted_ignored", "→ §7$0§r$1"); + + public static ModConfigSpec.ConfigValue ignoreTag = builder + .comment("TsukiChatは、以下のタグを持つプレイヤーのメッセージのローマ字変換や日本語変換を行いません。\n" + + "ただし、マークダウンの変換は行われます。") + .define("ignore_tag", "tsukichat_no_romaji"); + public static ModConfigSpec.ConfigValue ignoreCompletelyTag = builder + .comment("TsukiChatは、以下のタグを持つプレイヤーのメッセージについて、一切の変換を行いません。") + .define("ignore_completely_tag", "tsukichat_ignore"); + + public static final ModConfigSpec.BooleanValue formatTeamMsg = builder + .comment("チームメッセージを変換するかどうか。") + .define("format_team_msg", true); + + public static final ModConfigSpec.BooleanValue defaultTeamMsg = builder + .comment(""" + 送信されたメッセージをデフォルトでチームメッセージとして送信するかどうか。 + チームに所属していない場合は機能しません。 + コンフィグforce_globalにマッチするメッセージはチームメッセージとして送信されません。""") + .define("default_team_msg", false); + + public static final ModConfigSpec.ConfigValue> forceGlobal = builder + .comment("TsukiChatは、以下の接頭辞から始まるメッセージを強制的にグローバルチャットとして扱います。\n" + + "コンフィグdefault_team_msgが無効の場合は機能しません。") + .define("force_global", List.of("!")); + + public static final ModConfigSpec.IntValue forwardTeamMsgLevel = builder + .comment("すべてのチームメッセージを指定された権限レベルを持つプレイヤーへ転送します。\n" + + "-1が指定されている場合、チームメッセージは転送されません。") + .defineInRange("forward_team_msg_level", -1, -1, Integer.MAX_VALUE); + + public static final ModConfigSpec COMMON_SPEC = builder.build(); +} diff --git a/src/main/java/io/github/meatwo310/tsukichat/event/CommandRegisterer.java b/src/main/java/io/github/meatwo310/tsukichat/event/CommandRegisterer.java new file mode 100644 index 0000000..bc12ac5 --- /dev/null +++ b/src/main/java/io/github/meatwo310/tsukichat/event/CommandRegisterer.java @@ -0,0 +1,14 @@ +package io.github.meatwo310.tsukichat.event; + +import io.github.meatwo310.tsukichat.commands.TsukiChatCommand; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.event.RegisterCommandsEvent; + +@EventBusSubscriber +public class CommandRegisterer { + @SubscribeEvent + public static void registerCommands(RegisterCommandsEvent event) { + TsukiChatCommand.register(event.getDispatcher()); + } +} diff --git a/src/main/java/io/github/meatwo310/tsukichat/event/MohistCompatPluginChecker.java b/src/main/java/io/github/meatwo310/tsukichat/event/MohistCompatPluginChecker.java new file mode 100644 index 0000000..3f26e9e --- /dev/null +++ b/src/main/java/io/github/meatwo310/tsukichat/event/MohistCompatPluginChecker.java @@ -0,0 +1,26 @@ +package io.github.meatwo310.tsukichat.event; + +import io.github.meatwo310.tsukichat.TsukiChat; +import io.github.meatwo310.tsukichat.compat.mohist.MohistHelper; +import io.github.meatwo310.tsukichat.config.CommonConfigs; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.event.server.ServerStartedEvent; + +@EventBusSubscriber(modid = TsukiChat.MODID, value = Dist.DEDICATED_SERVER) +public class MohistCompatPluginChecker { + @SubscribeEvent + public static void check(ServerStartedEvent event) { + if (!MohistHelper.isMohistLoaded()) return; + if (!CommonConfigs.mohistCompat.get()) return; + + if (MohistHelper.isCompatPluginLoaded()) { + TsukiChat.LOGGER.info("Mohist compat plugin is loaded! Proceeding..."); + } else { + throw new IllegalStateException("Forge's ServerChatEvent is NOT compatible with Mohist! " + + "Install the compat plugin at https://github.com/Meatwo310/tsuki-chat/blob/main/mohist-compat.md ! " + + "Note that you can disable this check in the config."); + } + } +} diff --git a/src/main/java/io/github/meatwo310/tsukichat/event/PlayerCloneEvent.java b/src/main/java/io/github/meatwo310/tsukichat/event/PlayerCloneEvent.java new file mode 100644 index 0000000..d48e2bd --- /dev/null +++ b/src/main/java/io/github/meatwo310/tsukichat/event/PlayerCloneEvent.java @@ -0,0 +1,14 @@ +package io.github.meatwo310.tsukichat.event; + +import io.github.meatwo310.tsukichat.util.PlayerNbtUtil; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; + +@EventBusSubscriber +public class PlayerCloneEvent { + @SubscribeEvent + public static void onPlayerClone(PlayerEvent.Clone event) { + if (event.isWasDeath()) PlayerNbtUtil.clonePlayerData(event.getOriginal(), event.getEntity()); + } +} diff --git a/src/main/java/io/github/meatwo310/tsukichat/event/ServerChat.java b/src/main/java/io/github/meatwo310/tsukichat/event/ServerChat.java new file mode 100644 index 0000000..70a99e7 --- /dev/null +++ b/src/main/java/io/github/meatwo310/tsukichat/event/ServerChat.java @@ -0,0 +1,127 @@ +package io.github.meatwo310.tsukichat.event; + +import io.github.meatwo310.tsukichat.TsukiChat; +import io.github.meatwo310.tsukichat.commands.ServerDictionaryCommand; +import io.github.meatwo310.tsukichat.commands.UserDictionaryCommand; +import io.github.meatwo310.tsukichat.config.CommonConfigs; +import io.github.meatwo310.tsukichat.util.ChatCustomizer; +import io.github.meatwo310.tsukichat.util.PlayerNbtUtil; +import net.minecraft.FieldsAreNonnullByDefault; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.HoverEvent; +import net.minecraft.network.chat.Style; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.scores.PlayerTeam; +import net.neoforged.bus.api.EventPriority; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.event.ServerChatEvent; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Set; + +@ParametersAreNonnullByDefault +@FieldsAreNonnullByDefault +@EventBusSubscriber(modid = TsukiChat.MODID) +public class ServerChat { + private static final Style TEAMMSG_SUGGEST_STYLE = Style.EMPTY + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.translatable("chat.type.team.hover"))) + .withClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/teammsg ")); + + @SubscribeEvent(priority = EventPriority.HIGHEST) + public static void onChat(ServerChatEvent event) { + ServerPlayer player = event.getPlayer(); + if (player.level().isClientSide()) return; + + String original = event.getRawText(); + + if (CommonConfigs.defaultTeamMsg.get()) { + if (CommonConfigs.forceGlobal.get().stream().anyMatch(original::startsWith)) { + original = original.substring(1); + } else { + try { + sendTeamMessage(event.getPlayer(), original); + event.setCanceled(true); + return; + } catch (NotOnTeamException ignored) { + // Continue to process the message as a normal chat message + } + } + } + + Set playerTags = player.getTags(); + + CompoundTag dict = PlayerNbtUtil.loadCompoundTag(player, UserDictionaryCommand.KEY_NAME); + LinkedHashMap userDictionary = new LinkedHashMap<>(); + dict.getAllKeys().forEach(k -> userDictionary.put(k, dict.getString(k))); + var serverDictionary = ServerDictionaryCommand.getServerDictionary(); + + var result = ChatCustomizer.recognizeChat(original, playerTags, userDictionary, serverDictionary); + + result.ifMessagePresent(s -> event.setMessage(Component.literal(s))); + result.ifDeferredMessagePresent(s -> + player.server.getPlayerList().broadcastSystemMessage(Component.literal(s), false) + ); + } + + private static void sendTeamMessage(ServerPlayer sender, String message) throws NotOnTeamException { + PlayerTeam team = sender.getTeam(); + if (team == null) throw new NotOnTeamException(sender); + + MinecraftServer server = sender.getServer(); + if (server == null) return; + + List players = server.getPlayerList().getPlayers(); + List recipients = players.stream().filter(player -> + player == sender || player.getTeam() == team + ).toList(); + if (recipients.isEmpty()) return; + + Set playerTags = sender.getTags(); + + CompoundTag dict = PlayerNbtUtil.loadCompoundTag(sender, UserDictionaryCommand.KEY_NAME); + LinkedHashMap userDictionary = new LinkedHashMap<>(); + dict.getAllKeys().forEach(k -> userDictionary.put(k, dict.getString(k))); + var serverDictionary = ServerDictionaryCommand.getServerDictionary(); + + var result = ChatCustomizer.recognizeChat(message, playerTags, userDictionary, serverDictionary); + var converted = result.getMessageSynced(); + sendTeamMessage(server, sender, team, recipients, converted == null ? message : converted); + } + + public static void sendTeamMessage(MinecraftServer server, ServerPlayer sender, PlayerTeam team, List recipients, String message) { + int forwardLevel = CommonConfigs.forwardTeamMsgLevel.get(); + ArrayList listModified = new ArrayList<>(recipients); + if (forwardLevel >= 0) listModified.addAll(server.getPlayerList().getPlayers().stream() + .filter(player -> player.hasPermissions(forwardLevel) && !listModified.contains(player)) + .toList() + ); + + var teamComponent = team.getFormattedDisplayName().withStyle(TEAMMSG_SUGGEST_STYLE); + var senderComponent = sender.getDisplayName(); + var component = Component + .literal("-> ") + .append(teamComponent) + .append(" <") + .append(senderComponent) + .append("> ") + .append(message); + + for (ServerPlayer serverPlayer : listModified) { + serverPlayer.sendSystemMessage(component); + } + } + + private static class NotOnTeamException extends RuntimeException { + public NotOnTeamException(Entity entity) { + super("Entity " + entity.getName().getString() + " is not on a team."); + } + } +} diff --git a/src/main/java/io/github/meatwo310/tsukichat/mixin/SDLinkServerEventsMixin.java b/src/main/java/io/github/meatwo310/tsukichat/mixin/SDLinkServerEventsMixin.java new file mode 100644 index 0000000..64ddf58 --- /dev/null +++ b/src/main/java/io/github/meatwo310/tsukichat/mixin/SDLinkServerEventsMixin.java @@ -0,0 +1,23 @@ +package io.github.meatwo310.tsukichat.mixin; + +import com.hypherionmc.craterlib.api.events.server.CraterServerChatEvent; +import com.hypherionmc.sdlink.server.ServerEvents; +import io.github.meatwo310.tsukichat.config.CommonConfigs; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value = ServerEvents.class) +public class SDLinkServerEventsMixin { + @Inject( + method = "onServerChatEvent(Lcom/hypherionmc/craterlib/api/events/server/CraterServerChatEvent;)V", + at = @At("HEAD"), + cancellable = true, + remap = false // ← 難読化されてない場合falseにしないとエラー(1敗) + ) + private void onServerChatEvent(CraterServerChatEvent event, CallbackInfo ci) { + if (!CommonConfigs.sdlinkCompat.get()) return; + ci.cancel(); + } +} diff --git a/src/main/java/io/github/meatwo310/tsukichat/mixin/TeamMsgCommandMixin.java b/src/main/java/io/github/meatwo310/tsukichat/mixin/TeamMsgCommandMixin.java new file mode 100644 index 0000000..ecaf1e7 --- /dev/null +++ b/src/main/java/io/github/meatwo310/tsukichat/mixin/TeamMsgCommandMixin.java @@ -0,0 +1,73 @@ +package io.github.meatwo310.tsukichat.mixin; + +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import io.github.meatwo310.tsukichat.commands.ServerDictionaryCommand; +import io.github.meatwo310.tsukichat.commands.UserDictionaryCommand; +import io.github.meatwo310.tsukichat.event.ServerChat; +import io.github.meatwo310.tsukichat.util.ChatCustomizer; +import io.github.meatwo310.tsukichat.util.PlayerNbtUtil; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.MessageArgument; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.server.commands.TeamMsgCommand; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.scores.PlayerTeam; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Set; + +@Mixin(TeamMsgCommand.class) +public class TeamMsgCommandMixin { + @Inject(at = @At( + value = "INVOKE", + target = "net/minecraft/commands/arguments/MessageArgument.resolveChatMessage(" + + "Lcom/mojang/brigadier/context/CommandContext;" + + "Ljava/lang/String;" + + "Ljava/util/function/Consumer;" + + ")V"), + method = "lambda$register$2(" + + "Lcom/mojang/brigadier/context/CommandContext;" + + ")I", + locals = LocalCapture.CAPTURE_FAILHARD, + cancellable = true + ) + private static void resolveChatMessage( + CommandContext ctx, + CallbackInfoReturnable cir, + CommandSourceStack commandsourcestack, + Entity entity, + PlayerTeam playerteam, + List list + ) throws CommandSyntaxException { + Component message = MessageArgument.getMessage(ctx, "message"); + String original = message.getString(); + Set playerTags = entity.getTags(); + CompoundTag dict = PlayerNbtUtil.loadCompoundTag((ServerPlayer) entity, UserDictionaryCommand.KEY_NAME); + LinkedHashMap userDictionary = new LinkedHashMap<>(); + dict.getAllKeys().forEach(k -> userDictionary.put(k, dict.getString(k))); + var serverDictionary = ServerDictionaryCommand.getServerDictionary(); + + var result = ChatCustomizer.recognizeChat(original, playerTags, userDictionary, serverDictionary); + var converted = result.getMessageSynced(); + if (converted == null) return; + + ServerChat.sendTeamMessage( + commandsourcestack.getServer(), + (ServerPlayer) entity, + playerteam, + list, + converted + ); + + cir.setReturnValue(list.size()); + } +} diff --git a/src/main/java/io/github/meatwo310/tsukichat/util/ChatCustomizer.java b/src/main/java/io/github/meatwo310/tsukichat/util/ChatCustomizer.java new file mode 100644 index 0000000..bf61315 --- /dev/null +++ b/src/main/java/io/github/meatwo310/tsukichat/util/ChatCustomizer.java @@ -0,0 +1,106 @@ +package io.github.meatwo310.tsukichat.util; + +import io.github.meatwo310.tsukichat.config.CommonConfigs; + +import java.util.LinkedHashMap; +import java.util.Set; +import java.util.function.Function; + +public class ChatCustomizer { + static final String DICT_PREFIX = "[DICT_"; + static final String DICT_SUFFIX = "]"; + + public static CustomizedChat recognizeChat(String original, Set playerTags, LinkedHashMap userDictionary, LinkedHashMap serverDictionary) { + CustomizedChat empty = new CustomizedChat(); + + // メッセージが空の場合は何もしない + if (original.isEmpty()) return empty; + // Tsukichat無効化タグが付与されている場合は何もしない + if (playerTags.contains(CommonConfigs.ignoreCompletelyTag.get())) return empty; + // 無視リストにあるメッセージの場合は何もしない + if (CommonConfigs.ignoreCompletely.get().stream().anyMatch(original::startsWith)) return empty; + + // 一部のコンフィグを取得 + String formatOriginal = CommonConfigs.formatOriginal.get(); + String formatConverted = CommonConfigs.formatConverted.get(); + String formatOriginalIgnored = CommonConfigs.formatOriginalIgnored.get(); + String formatConvertedIgnored = CommonConfigs.formatConvertedIgnored.get(); + + // 無視タグを持っているかどうか + boolean onlyMarkdown = playerTags.contains(CommonConfigs.ignoreTag.get()); + + // 辞書をマージ + LinkedHashMap mergedDictionary = new LinkedHashMap<>(serverDictionary); + mergedDictionary.putAll(userDictionary); + + // メッセージを変換 + String amp = CommonConfigs.ampersand.get() ? Converter.ampersandToFormattingCode(original) : original; + String converted = CommonConfigs.markdown.get() ? Converter.markdownToFormattingCode(amp) : amp; + + String result; + + if (CommonConfigs.ignoreMessages.get().contains(original)) { + // メッセージが無視リストにある場合 + return empty; + } else if (!onlyMarkdown && CommonConfigs.ignore.get().stream().anyMatch(original::startsWith)) { + // 接頭辞がコンフィグの無視リストにある場合 + if (original.equals(converted)) result = formatOriginalIgnored + .replace("$0", original.substring(0, 1)) + .replace("$1", original.substring(1)); + else result = formatOriginal.replace("$0", original) + "\n" + + formatConvertedIgnored + .replace("$0", original.substring(0, 1)) + .replace("$1", converted.substring(1)); + } else if (onlyMarkdown || + (CommonConfigs.ignoreNonAscii.get() && !original.matches("^[!-~\\s§]+$")) || + !original.matches(".*(? { + String hiragana = applyDictionary(converted, mergedDictionary, Converter::romajiToHiragana); + String japanese = applyDictionary(hiragana, mergedDictionary, Converter::hiraganaToJapanese); + return formatConverted.replace("$0", japanese); + } + ); + } else { + String hiragana = applyDictionary(converted, mergedDictionary, Converter::romajiToHiragana); + String japanese = applyDictionary(hiragana, mergedDictionary, Converter::hiraganaToJapanese); + result = formatOriginal.replace("$0", original) + "\n" + + formatConverted.replace("$0", japanese); + } + } + + return new CustomizedChat(result); + } + + public static String applyDictionary(String original, LinkedHashMap dictionary, Function function) { + String intermediate = original; + int i = 0; + for (String key : dictionary.keySet()) { + intermediate = intermediate.replace(key, DICT_PREFIX + i++ + DICT_SUFFIX); + } + + String output = function.apply(intermediate); + i = 0; + for (String key : dictionary.keySet()) { + output = output.replace(DICT_PREFIX + i++ + DICT_SUFFIX, dictionary.get(key)); + } + return output; + } +} diff --git a/src/main/java/io/github/meatwo310/tsukichat/util/Converter.java b/src/main/java/io/github/meatwo310/tsukichat/util/Converter.java new file mode 100644 index 0000000..0ccfb63 --- /dev/null +++ b/src/main/java/io/github/meatwo310/tsukichat/util/Converter.java @@ -0,0 +1,207 @@ +package io.github.meatwo310.tsukichat.util; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import io.github.meatwo310.tsukichat.TsukiChat; +import org.slf4j.Logger; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Objects; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +public class Converter { + /** + * ローマ字からひらがなへの変換テーブル。 + * キーにローマ字、値にひらがなと巻き戻り数を持つ + */ + private static final LinkedHashMap hiraganaMap = new LinkedHashMap<>(); + private static final Logger LOGGER = TsukiChat.LOGGER; + + /** + * ひらがな変換テーブルを初期化する + * @param resourceName リソース名 + */ + private static void initMap(String resourceName) { + URL pathToTable = Converter.class.getResource(resourceName); + + if (Objects.isNull(pathToTable)) { + LOGGER.warn("Could not find resource: {}", resourceName); + return; + } + + // テーブルを読み込む + try (java.io.InputStream stream = pathToTable.openStream()) { + BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)); + String line; + while ((line = reader.readLine()) != null) { + if (line.isBlank()) continue; + String[] parts = line.split(":"); // 0: ローマ字, 1: ひらがな, 2: 巻き戻り数 + String romaji = parts[0]; + String hiragana = parts[1]; + String back; + + if (parts.length == 2) { // ローマ字+ひらがな + back = "0"; + } else if (parts.length == 3) { // ローマ字+ひらがな+巻き戻り数 + // Int型に変換可能か確認 + try { + Integer.parseInt(parts[2]); + } catch (Exception e) { + LOGGER.warn("Could not parse int in {}; This line will be ignored: {}", resourceName, line); + continue; + } + back = parts[2]; + } else { + LOGGER.warn("Invalid line in {}; This line will be ignored: {}", resourceName, line); + continue; + } + + // ローマ字をキーにしてひらがなと巻き戻り数を保存 + hiraganaMap.put(romaji, new String[]{hiragana, back}); + } + } catch (java.io.IOException e) { + throw new RuntimeException(e); + } + } + + static { + initMap("/assets/japaneseromajiconverter/romaji_to_hiragana.txt"); + } + + /** + * ローマ字をひらがなに変換する + * @param romaji ローマ字 + * @return ひらがな + */ + public static String romajiToHiragana(String romaji) { + StringBuilder hiragana = new StringBuilder(); + int i = 0; + while (i < romaji.length()) { + boolean found = false; + // ↓ MAGIC NUMBER !!!! + for (int j = 4; j >= 1; j--) { + if (!(i + j <= romaji.length())) continue; + String substring = romaji.substring(i, i + j); + + if (!hiraganaMap.containsKey(substring)) continue; + hiragana.append(hiraganaMap.get(substring)[0]); + i += j + Integer.parseInt(hiraganaMap.get(substring)[1]); + found = true; + break; + } + if (!found) { + hiragana.append(romaji.charAt(i)); + i++; + } + } + LOGGER.debug("Converted romaji to hiragana: {}", hiragana); + return hiragana.toString(); + } + + /** + * ひらがなを必要に応じて分割しながら日本語に変換する + * @param hiragana ひらがな + * @return 日本語 + */ + public static String hiraganaToJapanese(String hiragana) { + // 空白で分割して並列で変換し、変換結果に空の要素があれば代わりにひらがなにフォールバックしてエラーを付加 + String[] parts = hiragana.split(" "); + String[] result = Arrays.stream(parts) + .parallel() + .map(Converter::hiraganaPartsToJapanese) + .toArray(String[]::new); + boolean fallback = Arrays.stream(result).anyMatch(String::isEmpty); + if (!fallback) return String.join(" ", result); + for (int i = 0; i < result.length; i++) { + if (!result[i].isEmpty()) continue; + result[i] = parts[i]; + } + return String.join(" ", result) + " §8(長すぎます、スペースで区切ってください)§r"; + } + + /** + * ひらがなを日本語に変換する + * @param hiragana ひらがな + * @return 日本語 + */ + private static String hiraganaPartsToJapanese(String hiragana) { + try { + // GoogleのAPIを使って変換 + URI uri = new URI("https://www.google.com/transliterate?langpair=ja-Hira|ja&text=" + + URLEncoder.encode(hiragana, StandardCharsets.UTF_8)); + HttpURLConnection conn = (HttpURLConnection) uri.toURL().openConnection(); + conn.setRequestMethod("GET"); + BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)); + StringBuilder content; + try (Stream lines = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)).lines()) { + content = lines.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append); + } + in.close(); + conn.disconnect(); + + // GSONを使ってJSONとして解析 + JsonElement jsonElement = JsonParser.parseString(content.toString()); + LOGGER.debug("JSON Response: {}", jsonElement); + JsonArray jsonArray = jsonElement.getAsJsonArray(); + + // 配列をループし、最初の変換結果を選択 + StringBuilder result = new StringBuilder(); + for (JsonElement element : jsonArray) { + JsonArray innerArray = element.getAsJsonArray(); + String firstConversionResult = innerArray.get(1).getAsJsonArray().get(0).getAsString(); + result.append(firstConversionResult); + } + + // 全角ASCII文字を半角に変換して返す + return Pattern + .compile("([!-~])") + .matcher(result) + .replaceAll(m -> String.valueOf((char) (m.group().charAt(0) - 0xFEE0))); + } catch (Exception e) { + LOGGER.error("Error during conversion to Japanese: ", e); + return hiragana + " §8(エラー)§r"; + } + } + + /** + * ローマ字を日本語に変換する + * @param romaji ローマ字 + * @return 日本語 + */ + public static String romajiToJapanese(String romaji) { + return hiraganaToJapanese(romajiToHiragana(romaji)); + } + + /** + * &を§に変換する + * @param text テキスト + * @return 変換後のテキスト + */ + public static String ampersandToFormattingCode(String text) { + return text.replaceAll("&([0-9a-fk-or])", "§$1"); + } + + /** + * マークダウンをフォーマットコードに変換する + * @param text テキスト + * @return 変換後のテキスト + */ + public static String markdownToFormattingCode(String text) { + // TODO:入り組んだMarkdownを上手に変換する + return text + .replaceAll("[**][**](.*?)[**][**]", "§l$1§r") + .replaceAll("[**](.*?)[**]", "§o$1§r") + .replaceAll("[__][__](.*?)[__][__]", "§n$1§r") + .replaceAll("[~〜][~〜](.*?)[~〜][~〜]", "§m$1§r"); + } +} diff --git a/src/main/java/io/github/meatwo310/tsukichat/util/CustomizedChat.java b/src/main/java/io/github/meatwo310/tsukichat/util/CustomizedChat.java new file mode 100644 index 0000000..4fbde2b --- /dev/null +++ b/src/main/java/io/github/meatwo310/tsukichat/util/CustomizedChat.java @@ -0,0 +1,59 @@ +package io.github.meatwo310.tsukichat.util; + +import io.github.meatwo310.tsukichat.TsukiChat; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + +public class CustomizedChat { + private static final ExecutorService executorService = Executors.newCachedThreadPool(); + + @Nullable + public final String message; + @Nullable + public final Callable deferredMessage; + + public CustomizedChat() { + this(null, null); + } + + public CustomizedChat(@Nullable String message) { + this(message, null); + } + + public CustomizedChat(@Nullable String message, @Nullable Callable deferredMessage) { + this.message = message; + this.deferredMessage = deferredMessage; + } + + public void ifMessagePresent(Consumer consumer) { + if (message == null) return; + consumer.accept(message); + } + + public void ifDeferredMessagePresent(Consumer asyncConsumer) { + if (deferredMessage == null) return; + executorService.submit(() -> { + try { + asyncConsumer.accept(deferredMessage.call()); + } catch (Exception e) { + TsukiChat.LOGGER.error("Failed to get deferred message: ", e); + } + }); + } + + public @Nullable String getMessageSynced() { + if (message != null) return message; + if (deferredMessage != null) { + try { + return deferredMessage.call(); + } catch (Exception e) { + TsukiChat.LOGGER.error("Failed to get deferred message: ", e); + } + } + return null; + } +} diff --git a/src/main/java/io/github/meatwo310/tsukichat/util/PlayerNbtUtil.java b/src/main/java/io/github/meatwo310/tsukichat/util/PlayerNbtUtil.java new file mode 100644 index 0000000..b701548 --- /dev/null +++ b/src/main/java/io/github/meatwo310/tsukichat/util/PlayerNbtUtil.java @@ -0,0 +1,86 @@ +package io.github.meatwo310.tsukichat.util; + +import io.github.meatwo310.tsukichat.TsukiChat; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.world.entity.player.Player; + +public class PlayerNbtUtil { + private static final String KEY_NAME = TsukiChat.MODID; + + /** + * @return キーがすでに存在していて上書きした場合true + */ + public static boolean saveCompoundTag(Player player, String name, CompoundTag compoundTag) { + /* + { + "tsukichat": { + "name": { + "key": "value", + ... + } + } + } + */ + + CompoundTag playerData = player.getPersistentData(); + CompoundTag tsukichatData = playerData.contains(KEY_NAME) ? playerData.getCompound(KEY_NAME) : new CompoundTag(); + boolean exists = tsukichatData.contains(name); + CompoundTag keyData = exists ? tsukichatData.getCompound(name) : new CompoundTag(); + + compoundTag.getAllKeys().forEach(k -> { + Tag tag = compoundTag.get(k); + if (tag != null) keyData.put(k, tag); + }); + + tsukichatData.put(name, keyData); + playerData.put(KEY_NAME, tsukichatData); + return exists; + } + + + /** + * @return キーの削除に成功した場合true + */ + public static boolean removeTag(Player player, String name, String key) { + CompoundTag playerData = player.getPersistentData(); + if (!playerData.contains(KEY_NAME)) return false; + CompoundTag tsukichatData = playerData.getCompound(KEY_NAME); + if (!tsukichatData.contains(name)) return false; + CompoundTag keyData = tsukichatData.getCompound(name); + if (!keyData.contains(key)) return false; + + keyData.remove(key); + return true; + } + + /** + * @return nameの削除に成功した場合true + */ + public static boolean removeWhole(Player player, String name) { + CompoundTag playerData = player.getPersistentData(); + if (!playerData.contains(KEY_NAME)) return false; + CompoundTag tsukichatData = playerData.getCompound(KEY_NAME); + if (!tsukichatData.contains(name)) return false; + + tsukichatData.remove(name); + return true; + } + + public static CompoundTag loadCompoundTag(Player player, String name) { + CompoundTag playerData = player.getPersistentData(); + if (!playerData.contains(KEY_NAME)) return new CompoundTag(); + CompoundTag tsukichatData = playerData.getCompound(KEY_NAME); + + return tsukichatData.contains(name) ? tsukichatData.getCompound(name) : new CompoundTag(); + } + + public static void clonePlayerData(Player originalPlayer, Player newPlayer) { + CompoundTag originalPlayerData = originalPlayer.getPersistentData(); + if (!originalPlayerData.contains(KEY_NAME)) return; + CompoundTag tsukichatData = originalPlayerData.getCompound(KEY_NAME); + + CompoundTag newPlayerData = newPlayer.getPersistentData(); + newPlayerData.put(KEY_NAME, tsukichatData); + } +} diff --git a/src/main/resources/assets/tsukichat/lang/en_us.json b/src/main/resources/assets/tsukichat/lang/en_us.json new file mode 100644 index 0000000..54292cd --- /dev/null +++ b/src/main/resources/assets/tsukichat/lang/en_us.json @@ -0,0 +1,5 @@ +{ + "itemGroup.tsukichat": "Example Mod Tab", + "block.tsukichat.example_block": "Example Block", + "item.tsukichat.example_item": "Example Item" +} diff --git a/src/main/resources/tsukichat.mixins.json b/src/main/resources/tsukichat.mixins.json new file mode 100644 index 0000000..d4a4d99 --- /dev/null +++ b/src/main/resources/tsukichat.mixins.json @@ -0,0 +1,18 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "io.github.meatwo310.tsukichat.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [ + "SDLinkServerEventsMixin", + "TeamMsgCommandMixin" + ], + "client": [ + ], + "injectors": { + "defaultRequire": 1 + }, + "overwrites": { + "requireAnnotations": true + } +} diff --git a/src/main/templates/META-INF/neoforge.mods.toml b/src/main/templates/META-INF/neoforge.mods.toml index d528477..a190222 100644 --- a/src/main/templates/META-INF/neoforge.mods.toml +++ b/src/main/templates/META-INF/neoforge.mods.toml @@ -1,56 +1,42 @@ -# This is an example neoforge.mods.toml file. It contains the data relating to the loading mods. +# This is an example mods.toml file. It contains the data relating to the loading mods. # There are several mandatory fields (#mandatory), and many more that are optional (#optional). # The overall format is standard TOML format, v0.5.0. # Note that there are a couple of TOML lists in this file. # Find more information on toml format here: https://github.com/toml-lang/toml # The name of the mod loader type to load - for regular FML @Mod mods it should be javafml -modLoader="javafml" #mandatory - -# A version range to match for said mod loader - for regular FML @Mod it will be the FML version. This is currently 2. -loaderVersion="${loader_version_range}" #mandatory - +modLoader = "javafml" #mandatory +# A version range to match for said mod loader - for regular FML @Mod it will be the the FML version. This is currently 47. +loaderVersion = "${loader_version_range}" #mandatory # The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. # Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. -license="${mod_license}" - +license = "${mod_license}" # A URL to refer people to when problems occur with this mod #issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional - # A list of mods - how many allowed here is determined by the individual mod loader [[mods]] #mandatory - # The modid of the mod -modId="${mod_id}" #mandatory - +modId = "${mod_id}" #mandatory # The version number of the mod -version="${mod_version}" #mandatory - +version = "${mod_version}" #mandatory # A display name for the mod -displayName="${mod_name}" #mandatory - -# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforged.net/docs/misc/updatechecker/ +displayName = "${mod_name}" #mandatory +# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforge.net/docs/misc/updatechecker/ #updateJSONURL="https://change.me.example.invalid/updates.json" #optional - # A URL for the "homepage" for this mod, displayed in the mod UI #displayURL="https://change.me.to.your.mods.homepage.example.invalid/" #optional -displayURL="https://github.com/Meatwo310/tsuki-chat/" - # A file name (in the root of the mod JAR) containing a logo for display -#logoFile="examplemod.png" #optional -logoFile="tsukichat.png" - +#logoFile="tsukichat.png" #optional # A text field displayed in the mod UI #credits="" #optional - # A text field displayed in the mod UI -authors="${mod_authors}" #optional +authors = "${mod_authors}" #optional # The description text for the mod (multi line!) (#mandatory) -description='''${mod_description}''' +description = '''${mod_description}''' # The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded. -#[[mixins]] -#config="${mod_id}.mixins.json" +[[mixins]] +config = "${mod_id}.mixins.json" # The [[accessTransformers]] block allows you to declare where your AT file is. # If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg @@ -60,35 +46,34 @@ description='''${mod_description}''' # The coremods config file path is not configurable and is always loaded from META-INF/coremods.json # A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. -[[dependencies.${mod_id}]] #optional - # the modid of the dependency - modId="neoforge" #mandatory - # The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive). - # 'required' requires the mod to exist, 'optional' does not - # 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning - type="required" #mandatory - # Optional field describing why the dependency is required or why it is incompatible - # reason="..." - # The version range of the dependency - versionRange="${neo_version_range}" #mandatory - # An ordering relationship for the dependency. - # BEFORE - This mod is loaded BEFORE the dependency - # AFTER - This mod is loaded AFTER the dependency - ordering="NONE" - # Side this dependency is applied on - BOTH, CLIENT, or SERVER - side="BOTH" - +[[dependencies."${mod_id}"]] #optional +# the modid of the dependency +modId = "neoforge" #mandatory +# The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive). +# 'required' requires the mod to exist, 'optional' does not +# 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning +type = "required" #mandatory +# Optional field describing why the dependency is required or why it is incompatible +# reason="..." +# The version range of the dependency +versionRange = "${neo_version_range}" #mandatory +# An ordering relationship for the dependency. +# BEFORE - This mod is loaded BEFORE the dependency +# AFTER - This mod is loaded AFTER the dependency +ordering = "NONE" +# Side this dependency is applied on - BOTH, CLIENT, or SERVER +side = "BOTH" # Here's another dependency -[[dependencies.${mod_id}]] - modId="minecraft" - type="required" - # This version range declares a minimum of the current minecraft version up to but not including the next major version - versionRange="${minecraft_version_range}" - ordering="NONE" - side="BOTH" +[[dependencies."${mod_id}"]] +modId = "minecraft" +type = "required" +# This version range declares a minimum of the current minecraft version up to but not including the next major version +versionRange = "${minecraft_version_range}" +ordering = "NONE" +side = "BOTH" # Features are specific properties of the game environment, that you may want to declare you require. This example declares # that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't # stop your mod loading on the server for example. -#[features.${mod_id}] +#[features."${mod_id}"] #openGLVersion="[3.2,)" From 49991f7c572712ae07dae8f6e7fc97c83ec905e1 Mon Sep 17 00:00:00 2001 From: cat <114403771+makaseloli@users.noreply.github.com> Date: Mon, 11 Aug 2025 22:40:01 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=E6=AD=B4=E5=8F=B2=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tsukichat/common/TsukiChatUtil.java | 12 -- .../tsukichat/common/converter/Converter.java | 12 -- .../converter/KanaJapaneseConverter.java | 67 ---------- .../common/converter/RomajiKanaConverter.java | 115 ------------------ .../tsukichat/config/CommonConfig.java | 9 -- ...i_table.txt => romaji_to_hiragana.txt.txt} | 0 6 files changed, 215 deletions(-) delete mode 100644 src/main/java/io/github/meatwo310/tsukichat/common/TsukiChatUtil.java delete mode 100644 src/main/java/io/github/meatwo310/tsukichat/common/converter/Converter.java delete mode 100644 src/main/java/io/github/meatwo310/tsukichat/common/converter/KanaJapaneseConverter.java delete mode 100644 src/main/java/io/github/meatwo310/tsukichat/common/converter/RomajiKanaConverter.java delete mode 100644 src/main/java/io/github/meatwo310/tsukichat/config/CommonConfig.java rename src/main/resources/assets/tsukichat/{romaji_table.txt => romaji_to_hiragana.txt.txt} (100%) diff --git a/src/main/java/io/github/meatwo310/tsukichat/common/TsukiChatUtil.java b/src/main/java/io/github/meatwo310/tsukichat/common/TsukiChatUtil.java deleted file mode 100644 index 3120cc9..0000000 --- a/src/main/java/io/github/meatwo310/tsukichat/common/TsukiChatUtil.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.github.meatwo310.tsukichat.common; - -public class TsukiChatUtil { - public static boolean isClassAvailable(String className) { - try { - Class.forName(className); - return true; - } catch (ClassNotFoundException e) { - return false; - } - } -} diff --git a/src/main/java/io/github/meatwo310/tsukichat/common/converter/Converter.java b/src/main/java/io/github/meatwo310/tsukichat/common/converter/Converter.java deleted file mode 100644 index 426e7a4..0000000 --- a/src/main/java/io/github/meatwo310/tsukichat/common/converter/Converter.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.github.meatwo310.tsukichat.common.converter; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class Converter { - public static final String MODID = "tsukichat"; - private static final Logger LOGGER = LoggerFactory.getLogger(Converter.class); - - private Converter() { - } -} diff --git a/src/main/java/io/github/meatwo310/tsukichat/common/converter/KanaJapaneseConverter.java b/src/main/java/io/github/meatwo310/tsukichat/common/converter/KanaJapaneseConverter.java deleted file mode 100644 index 7e4a4ef..0000000 --- a/src/main/java/io/github/meatwo310/tsukichat/common/converter/KanaJapaneseConverter.java +++ /dev/null @@ -1,67 +0,0 @@ -package io.github.meatwo310.tsukichat.common.converter; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.utils.URIBuilder; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; -import org.apache.http.util.EntityUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; - -public class KanaJapaneseConverter { - public static final Logger LOGGER = LoggerFactory.getLogger(KanaJapaneseConverter.class); - private static final String API_URL = "https://www.google.com/transliterate"; - private static final ObjectMapper MAPPER = new ObjectMapper(); - - private KanaJapaneseConverter() { - } - - public static String convertToJapanese(String hiragana) { - if (hiragana == null || hiragana.isEmpty()) { - return hiragana; - } - - try { - // Google CGI API for Japanese Inputを呼び出す - URI uri = new URIBuilder(API_URL) - .addParameter("langpair", "ja-Hira|ja") - .addParameter("text", hiragana) - .build(); - - try (CloseableHttpClient httpClient = HttpClients.createDefault()) { - HttpGet request = new HttpGet(uri); - try (CloseableHttpResponse response = httpClient.execute(request)) { - String jsonResponse = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); - - // レスポンスをパース - JsonNode root = MAPPER.readTree(jsonResponse); - StringBuilder result = new StringBuilder(); - - for (JsonNode segment : root) { - if (segment.isArray() && segment.size() >= 2) { - JsonNode candidatesNode = segment.get(1); - if (candidatesNode.isArray() && !candidatesNode.isEmpty()) { - // 最初の候補を選択 - result.append(candidatesNode.get(0).asText()); - } - } - } - - return result.toString(); - } - } - } catch (URISyntaxException | IOException e) { - LOGGER.error("Failed to convert hiragana to kanji", e); - // 変換に失敗した場合は元の文字列を返す - return hiragana; - } - } -} \ No newline at end of file diff --git a/src/main/java/io/github/meatwo310/tsukichat/common/converter/RomajiKanaConverter.java b/src/main/java/io/github/meatwo310/tsukichat/common/converter/RomajiKanaConverter.java deleted file mode 100644 index d3fd1cd..0000000 --- a/src/main/java/io/github/meatwo310/tsukichat/common/converter/RomajiKanaConverter.java +++ /dev/null @@ -1,115 +0,0 @@ -package io.github.meatwo310.tsukichat.common.converter; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; - -public class RomajiKanaConverter { - private static final Logger LOGGER = LoggerFactory.getLogger(RomajiKanaConverter.class); - private static final String TABLE_PATH = "assets/%s/romaji_table.txt".formatted(Converter.MODID); - private static final int MAX_LENGTH = 4; - - static final Map ROMAJI_TO_KANA = new HashMap<>(); - static final Map ROMAJI_ROLLBACK = new HashMap<>(); - - static { - loadRomajiTable(); - } - - private RomajiKanaConverter() { - } - - private static void loadRomajiTable() { - InputStream is = RomajiKanaConverter.class.getClassLoader().getResourceAsStream(TABLE_PATH); - if (is == null) { - throw new TableNotFoundException("Romaji table not found at " + TABLE_PATH); - } - - try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { - String line; - while ((line = reader.readLine()) != null) { - // Skip empty lines and comments - if (line.isEmpty() || line.startsWith("#")) { - continue; - } - - // Analyze the line - String[] parts = line.split(":"); - - if (parts.length < 2) { - continue; - } - - String key = parts[0]; - String value = parts[1]; - ROMAJI_TO_KANA.put(key, value); - - if (parts.length < 3) { - continue; - } - - try { - int rollback = Integer.parseInt(parts[2]); - ROMAJI_ROLLBACK.put(key, rollback); - } catch (NumberFormatException e) { - LOGGER.error("Illegal rollback value for key '{}': {}", key, parts[2], e); - } - } - LOGGER.info( - "Successfully loaded Romaji table with {} entries and {} rollback entries", - ROMAJI_TO_KANA.size(), - ROMAJI_ROLLBACK.size() - ); - } catch (IOException e) { - throw new TableLoadingException("Failed to load Romaji table", e); - } - } - - public static String convertToKana(String romaji) { - StringBuilder result = new StringBuilder(); - final int length = romaji.length(); - for (int i = 0; i < length;) { - int remaining = length - i; - for (int j = Math.min(MAX_LENGTH, remaining); j > 0; j--) { - String sub = romaji.substring(i, i + j); - if (ROMAJI_TO_KANA.containsKey(sub)) { - result.append(ROMAJI_TO_KANA.get(sub)); - i += j; - break; - } - if (j == 1) { - result.append(romaji.charAt(i)); - i++; - } - } - } - return result.toString(); - } - - private static class TableNotFoundException extends RuntimeException { - public TableNotFoundException(String message) { - super(message); - } - - public TableNotFoundException(String message, Throwable cause) { - super(message, cause); - } - } - - private static class TableLoadingException extends RuntimeException { - public TableLoadingException(String message) { - super(message); - } - - public TableLoadingException(String message, Throwable cause) { - super(message, cause); - } - } -} \ No newline at end of file diff --git a/src/main/java/io/github/meatwo310/tsukichat/config/CommonConfig.java b/src/main/java/io/github/meatwo310/tsukichat/config/CommonConfig.java deleted file mode 100644 index 0f4602f..0000000 --- a/src/main/java/io/github/meatwo310/tsukichat/config/CommonConfig.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.meatwo310.tsukichat.config; - -import net.neoforged.neoforge.common.ModConfigSpec; - -public class CommonConfig { - private static final ModConfigSpec.Builder BUILDER = new ModConfigSpec.Builder(); - - public static final ModConfigSpec SPEC = BUILDER.build(); -} diff --git a/src/main/resources/assets/tsukichat/romaji_table.txt b/src/main/resources/assets/tsukichat/romaji_to_hiragana.txt.txt similarity index 100% rename from src/main/resources/assets/tsukichat/romaji_table.txt rename to src/main/resources/assets/tsukichat/romaji_to_hiragana.txt.txt From 20cbde9415451d93eb7e16b56d0a2c8801f00eec Mon Sep 17 00:00:00 2001 From: cat <114403771+makaseloli@users.noreply.github.com> Date: Mon, 11 Aug 2025 22:40:01 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=E6=AD=B4=E5=8F=B2=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tsukichat/common/TsukiChatUtilTest.java | 17 ------- .../common/converter/ConverterTest.java | 21 -------- .../converter/KanaJapaneseConverterTest.java | 21 -------- .../converter/RomajiKanaConverterTest.java | 49 ------------------- 4 files changed, 108 deletions(-) delete mode 100644 src/test/java/io/github/meatwo310/tsukichat/common/TsukiChatUtilTest.java delete mode 100644 src/test/java/io/github/meatwo310/tsukichat/common/converter/ConverterTest.java delete mode 100644 src/test/java/io/github/meatwo310/tsukichat/common/converter/KanaJapaneseConverterTest.java delete mode 100644 src/test/java/io/github/meatwo310/tsukichat/common/converter/RomajiKanaConverterTest.java diff --git a/src/test/java/io/github/meatwo310/tsukichat/common/TsukiChatUtilTest.java b/src/test/java/io/github/meatwo310/tsukichat/common/TsukiChatUtilTest.java deleted file mode 100644 index 343bfc1..0000000 --- a/src/test/java/io/github/meatwo310/tsukichat/common/TsukiChatUtilTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.github.meatwo310.tsukichat.common; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class TsukiChatUtilTest { - @Test - void testIsClassAvailable() { - // Test with a class that exists - assertTrue(TsukiChatUtil.isClassAvailable("java.lang.String")); - - // Test with a class that does not exist - assertFalse(TsukiChatUtil.isClassAvailable("com.example.NonExistentClass")); - } -} diff --git a/src/test/java/io/github/meatwo310/tsukichat/common/converter/ConverterTest.java b/src/test/java/io/github/meatwo310/tsukichat/common/converter/ConverterTest.java deleted file mode 100644 index db7daa2..0000000 --- a/src/test/java/io/github/meatwo310/tsukichat/common/converter/ConverterTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.github.meatwo310.tsukichat.common.converter; - -import io.github.meatwo310.tsukichat.TsukiChat; -import io.github.meatwo310.tsukichat.common.TsukiChatUtil; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIf; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class ConverterTest { - @Test - @EnabledIf("isTsukiChatClassAvailable") - void checkModId() { - assertEquals(TsukiChat.MODID, Converter.MODID); - } - - // TsukiChatクラスが存在するかどうかを確認する静的メソッド - static boolean isTsukiChatClassAvailable() { - return TsukiChatUtil.isClassAvailable("io.github.meatwo310.tsukichat.TsukiChat"); - } -} diff --git a/src/test/java/io/github/meatwo310/tsukichat/common/converter/KanaJapaneseConverterTest.java b/src/test/java/io/github/meatwo310/tsukichat/common/converter/KanaJapaneseConverterTest.java deleted file mode 100644 index d40144d..0000000 --- a/src/test/java/io/github/meatwo310/tsukichat/common/converter/KanaJapaneseConverterTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.github.meatwo310.tsukichat.common.converter; - -import org.junit.jupiter.api.Test; - -import java.util.HashMap; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class KanaJapaneseConverterTest { - @Test - void convertToJapanese() { - Map testCases = new HashMap<>(); - testCases.put("わがはいはねこである", "吾輩は猫である"); - - testCases.forEach((input, expected) -> { - String result = KanaJapaneseConverter.convertToJapanese(input); - assertEquals(expected, result); - }); - } -} \ No newline at end of file diff --git a/src/test/java/io/github/meatwo310/tsukichat/common/converter/RomajiKanaConverterTest.java b/src/test/java/io/github/meatwo310/tsukichat/common/converter/RomajiKanaConverterTest.java deleted file mode 100644 index 6f984e6..0000000 --- a/src/test/java/io/github/meatwo310/tsukichat/common/converter/RomajiKanaConverterTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package io.github.meatwo310.tsukichat.common.converter; - -import org.junit.jupiter.api.Test; - -import java.util.HashMap; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; - -class RomajiKanaConverterTest { - @Test - void getRomajiToKanaMap() { - var romajiToKanaMap = RomajiKanaConverter.ROMAJI_TO_KANA; - assertFalse(romajiToKanaMap.isEmpty()); - assertEquals("か", romajiToKanaMap.get("ka")); - } - - @Test - void getRomajiRollbackMap() { - var romajiRollbackMap = RomajiKanaConverter.ROMAJI_ROLLBACK; - assertFalse(romajiRollbackMap.isEmpty()); - assertEquals(1, romajiRollbackMap.get("tt")); - } - - @Test - void convertToKana() { - Map testCases = new HashMap<>(); - testCases.put("wagahaihanekodearu", "わがはいはねこである"); - testCases.put("namaehamadanai", "なまえはまだない"); - testCases.put("dokodeumaretakatontokentougatukanu", "どこでうまれたかとんとけんとうがつかぬ"); - testCases.put("dokodeumaretakatonntokenntougatsukanu", "どこでうまれたかとんとけんとうがつかぬ"); - testCases.put( - "nandemousuguraijimejimesitatokorodenya-nya-naiteitakotodakehakiokusiteiru", - "なんでもうすぐらいじめじめしたところでにゃーにゃーないていたことだけはきおくしている" - ); - testCases.put( - "nanndemousuguraizimezimeshitatokorodenilya-nilya-naiteitakotodakehakiokushiteiru", - "なんでもうすぐらいじめじめしたところでにゃーにゃーないていたことだけはきおくしている" - ); - testCases.put("MINECRAFT", "MINECRAFT"); - testCases.put("Minecraft", "Mいねcらft"); - - testCases.forEach((input, expected) -> { - String result = RomajiKanaConverter.convertToKana(input); - assertEquals(expected, result); - }); - } -} From f67879a2ebf417011862d92f6408107b99e4ddc0 Mon Sep 17 00:00:00 2001 From: cat <114403771+makaseloli@users.noreply.github.com> Date: Mon, 11 Aug 2025 23:12:20 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=E5=A4=89=E6=8F=9B=E3=83=86=E3=83=BC?= =?UTF-8?q?=E3=83=96=E3=83=AB=E3=81=AE=E8=AA=AD=E3=81=BF=E8=BE=BC=E3=81=BF?= =?UTF-8?q?=E9=83=A8=E5=88=86=E3=81=A8=E3=80=81API=E3=81=AE=E5=91=BC?= =?UTF-8?q?=E3=81=B3=E5=87=BA=E3=81=97=E9=83=A8=E5=88=86=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../meatwo310/tsukichat/util/Converter.java | 6 ++- .../romaji_to_hiragana.txt} | 53 ++++++++----------- 2 files changed, 26 insertions(+), 33 deletions(-) rename src/main/resources/assets/{tsukichat/romaji_to_hiragana.txt.txt => japaneseromajiconverter/romaji_to_hiragana.txt} (91%) diff --git a/src/main/java/io/github/meatwo310/tsukichat/util/Converter.java b/src/main/java/io/github/meatwo310/tsukichat/util/Converter.java index 0ccfb63..04bddea 100644 --- a/src/main/java/io/github/meatwo310/tsukichat/util/Converter.java +++ b/src/main/java/io/github/meatwo310/tsukichat/util/Converter.java @@ -137,8 +137,10 @@ public static String hiraganaToJapanese(String hiragana) { private static String hiraganaPartsToJapanese(String hiragana) { try { // GoogleのAPIを使って変換 - URI uri = new URI("https://www.google.com/transliterate?langpair=ja-Hira|ja&text=" + - URLEncoder.encode(hiragana, StandardCharsets.UTF_8)); + String encodedUrl = "https://www.google.com/transliterate?langpair=" + + URLEncoder.encode("ja-Hira|ja", StandardCharsets.UTF_8) + + "&text=" + URLEncoder.encode(hiragana, StandardCharsets.UTF_8); + URI uri = URI.create(encodedUrl); HttpURLConnection conn = (HttpURLConnection) uri.toURL().openConnection(); conn.setRequestMethod("GET"); BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)); diff --git a/src/main/resources/assets/tsukichat/romaji_to_hiragana.txt.txt b/src/main/resources/assets/japaneseromajiconverter/romaji_to_hiragana.txt similarity index 91% rename from src/main/resources/assets/tsukichat/romaji_to_hiragana.txt.txt rename to src/main/resources/assets/japaneseromajiconverter/romaji_to_hiragana.txt index 672e0e6..715e7dc 100644 --- a/src/main/resources/assets/tsukichat/romaji_to_hiragana.txt.txt +++ b/src/main/resources/assets/japaneseromajiconverter/romaji_to_hiragana.txt @@ -1,4 +1,3 @@ -# starts with "#" are comments §0:§0 §1:§1 §2:§2 @@ -21,7 +20,6 @@ §n:§n §o:§o §r:§r - a:あ i:い u:う @@ -241,26 +239,27 @@ wyi:ゐ wye:ゑ n:ん -qq:っ:1 -vv:っ:1 -ll:っ:1 -xx:っ:1 -kk:っ:1 -gg:っ:1 -ss:っ:1 -zz:っ:1 -jj:っ:1 -tt:っ:1 -dd:っ:1 -hh:っ:1 -ff:っ:1 -bb:っ:1 -pp:っ:1 -mm:っ:1 -yy:っ:1 -rr:っ:1 -ww:っ:1 -cc:っ:1 +qq:っ:-1 +vv:っ:-1 +ll:っ:-1 +xx:っ:-1 +kk:っ:-1 +gg:っ:-1 +ss:っ:-1 +zz:っ:-1 +jj:っ:-1 +tt:っ:-1 +dd:っ:-1 +hh:っ:-1 +ff:っ:-1 +bb:っ:-1 +pp:っ:-1 +mm:っ:-1 +yy:っ:-1 +rr:っ:-1 +ww:っ:-1 +www:w:-2 +cc:っ:-1 tsa:つぁ tsi:つぃ @@ -331,17 +330,9 @@ whi:うぃ whu:う whe:うぇ who:うぉ - zh:← zj:↓ zk:↑ zl:→ -z/:・ -z[:『 -z]:』 -z-:〜 -z.:… -z,:‥ - .:。 -,:、 +,:、 \ No newline at end of file