diff --git a/src/main/java/top/r3944realms/eroticdungeongame/EroticDungeon.java b/src/main/java/top/r3944realms/eroticdungeongame/EroticDungeon.java index 2ec8d3fb..3993c553 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/EroticDungeon.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/EroticDungeon.java @@ -19,13 +19,16 @@ package top.r3944realms.eroticdungeongame; import net.minecraft.resources.ResourceLocation; import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.ModLoadingContext; import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.config.ModConfig; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.bernie.geckolib.GeckoLib; +import top.r3944realms.eroticdungeongame.config.EDGConfig; import top.r3944realms.eroticdungeongame.content.recipe.EDGRecipeBookTypes; import top.r3944realms.eroticdungeongame.content.recipe.EDGRecipeTypeCategories; import top.r3944realms.eroticdungeongame.core.network.EDGNetworkHandler; @@ -58,6 +61,7 @@ public class EroticDungeon { EDGRecipeBookTypes.init(); EDGRecipeTypeCategories.init(); EDGNetworkHandler.register(); + ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, EDGConfig.SPEC); } @Contract("_ -> new") public static @NotNull ResourceLocation rl(String path) { diff --git a/src/main/java/top/r3944realms/eroticdungeongame/api/capability/IPlayerDungeonData.java b/src/main/java/top/r3944realms/eroticdungeongame/api/capability/IPlayerDungeonData.java index ee37d496..131a3c34 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/api/capability/IPlayerDungeonData.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/api/capability/IPlayerDungeonData.java @@ -19,6 +19,7 @@ package top.r3944realms.eroticdungeongame.api.capability; import net.minecraft.core.BlockPos; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.phys.AABB; +import org.intellij.lang.annotations.MagicConstant; /** * The interface Player dungeon data. @@ -79,4 +80,43 @@ public interface IPlayerDungeonData { * @param eyeHeight the eye height */ void setEyeHeight(float eyeHeight); + + int INVALID = -1; + /** + * The constant IDLE. + */ + int IDLE = 0; + /** + * The constant REQUEST. + */ + int REQUEST = 1; + + + /** + * Sets norp state. + * + * @param state the state + */ + void setNorpState(@MagicConstant(intValues = {INVALID, IDLE, REQUEST}) int state); + + /** + * Gets norp state. + * + * @return the norp state + */ + int getNorpState(); + + /** + * Sets norp time. + * + * @param norpTime the norp time + */ + void setNorpTime(long norpTime); + + /** + * Gets norp time. + * + * @return the norp time + */ + long getNorpTime(); } diff --git a/src/main/java/top/r3944realms/eroticdungeongame/config/EDGConfig.java b/src/main/java/top/r3944realms/eroticdungeongame/config/EDGConfig.java new file mode 100644 index 00000000..847009c3 --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/config/EDGConfig.java @@ -0,0 +1,158 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.config; + +import net.minecraftforge.common.ForgeConfigSpec; + +public class EDGConfig { + //todo: 配置 + // 是否开启norp、 + // norp间隔时间、 + // norp成功后冷却时间、 + // 爱机是否开启伤害 + // 爱机伤害系数 + public static final ForgeConfigSpec.Builder BUILDER = new ForgeConfigSpec.Builder(); + /** + * The constant SPEC. + */ + public static final ForgeConfigSpec SPEC; + /** + * The constant COMMON. + */ + public static final Common COMMON; + static { + BUILDER.comment("EroticDungeon Common Configuration / 通用配置").push("EroticDungeon"); + + COMMON = new Common(BUILDER); + + BUILDER.pop(); + SPEC = BUILDER.build(); + } + + public static class Common { + + // NORP相关配置 + public final ForgeConfigSpec.BooleanValue ENABLE_NORP; + public final ForgeConfigSpec.IntValue NORP_INTERVAL_TIME; + public final ForgeConfigSpec.IntValue NORP_SUCCESSFUL_COOLDOWN; + + // 爱机相关配置 + public final ForgeConfigSpec.BooleanValue ENABLE_LOVE_MACHINE_DAMAGE; + public final ForgeConfigSpec.DoubleValue LOVE_MACHINE_DAMAGE_FACTOR; + + public Common(ForgeConfigSpec.Builder builder) { + + // NORP配置组 - 中英文说明 + builder.comment( + "NORP (No Role Play) Mode Settings / NORP(非角色扮演)模式设置", + "----------------------------------------", + "EN: Settings related to No Role Play mode functionality", + "CN: 与非角色扮演模式功能相关的设置" + ).push("norp_settings"); + + ENABLE_NORP = builder + .comment( + "EN: Enable No Role Play mode", + " If set to true, NORP mode will be enabled", + " Default: true", + "", + "CN: 启用非角色扮演模式", + " 如果设为true,将启用NORP模式", + " 默认值: true" + ) + .define("enableNorp", true); + + NORP_INTERVAL_TIME = builder + .comment( + "EN: NORP interval time (in seconds)", + " Time between NORP interactions", + " Default: 30 seconds", + " Min: 5, Max: 3600", + "", + "CN: NORP间隔时间(秒)", + " NORP交互之间的间隔时间", + " 默认值: 30秒", + " 最小值: 5, 最大值: 3600" + ) + .defineInRange("norpIntervalTime", 30, 5, 3600); + + NORP_SUCCESSFUL_COOLDOWN = builder + .comment( + "EN: NORP cooldown time after successful interaction (in seconds)", + " Time before another NORP interaction can occur", + " Default: 60 seconds", + " Min: 10, Max: 7200", + "", + "CN: NORP成功交互后的冷却时间(秒)", + " 进行下一次NORP交互前需要等待的时间", + " 默认值: 60秒", + " 最小值: 10, 最大值: 7200" + ) + .defineInRange("norpSuccessfulCooldown", 60, 10, 7200); + + builder.pop(); // 退出NORP配置组 + + // 爱机配置组 - 中英文说明 + builder.comment( + "Love Machine Settings / 爱机设置", + "----------------------------------------", + "EN: Settings related to Love Machine functionality", + "CN: 与爱机功能相关的设置" + ).push("love_machine_settings"); + + ENABLE_LOVE_MACHINE_DAMAGE = builder + .comment( + "EN: Enable Love Machine damage", + " If set to true, love machines can deal damage", + " Default: false", + "", + "CN: 启用爱机伤害", + " 如果设为true,爱机可以造成伤害", + " 默认值: false" + ) + .define("enableLoveMachineDamage", false); + + LOVE_MACHINE_DAMAGE_FACTOR = builder + .comment( + "EN: Love Machine damage factor", + " Damage multiplier for love machines", + " Default: 1.0", + " Min: 0.1, Max: 10.0", + "", + "CN: 爱机伤害系数", + " 爱机的伤害倍率", + " 默认值: 1.0", + " 最小值: 0.1, 最大值: 10.0" + ) + .defineInRange("loveMachineDamageFactor", 1.0, 0.1, 10.0); + + builder.pop(); // 退出爱机配置组 + + // 可选:添加更多配置组 + builder.comment( + "General Settings / 通用设置", + "----------------------------------------", + "EN: General mod settings", + "CN: 模组通用设置" + ).push("general_settings"); + + // 这里可以添加其他通用配置 + + builder.pop(); // 退出通用配置组 + } + } +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/content/block/blockentity/LoveMachineBlockEntity.java b/src/main/java/top/r3944realms/eroticdungeongame/content/block/blockentity/LoveMachineBlockEntity.java index ddebfd8e..987ae7a6 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/content/block/blockentity/LoveMachineBlockEntity.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/content/block/blockentity/LoveMachineBlockEntity.java @@ -47,6 +47,7 @@ import software.bernie.geckolib.util.GeckoLibUtil; import top.r3944realms.eroticdungeongame.EroticDungeon; import top.r3944realms.eroticdungeongame.api.event.LoveMachineTickEvent; import top.r3944realms.eroticdungeongame.client.util.RendererUtil; +import top.r3944realms.eroticdungeongame.config.EDGConfig; import top.r3944realms.eroticdungeongame.content.EDGDamageTypes; import top.r3944realms.eroticdungeongame.content.block.type.LoveMachineBlock; import top.r3944realms.eroticdungeongame.core.register.EDGBlockEntities; @@ -131,20 +132,20 @@ public class LoveMachineBlockEntity extends BlockEntity implements GeoBlockEntit ACTION.getOrDefault(loveMachineBlock.type, SpecialAction.EMPTY).doTick(pLevel, pBlockEntity, entity, pPos, pState, pLevel.getGameTime()); if (pBlockEntity.speed < 0.05f) { if (pLevel.getGameTime() % 100 == 0) { - if (pBlockEntity.hasLubricant()) { + if (pBlockEntity.hasLubricant() || !EDGConfig.COMMON.ENABLE_LOVE_MACHINE_DAMAGE.get()) { if (entity instanceof LivingEntity livingEntity) { livingEntity.addEffect(new MobEffectInstance(MobEffects.HEAL, 20, 1, true, true, true)); livingEntity.addEffect(new MobEffectInstance(MobEffects.DIG_SPEED, 20, 1, true, true, true)); } - } else entity.hurt(EDGDamageTypes.causeFuckedDamage(pLevel.registryAccess()), calcAnimSpeed(pBlockEntity.speed) * 10f); + } else entity.hurt(EDGDamageTypes.causeFuckedDamage(pLevel.registryAccess()), (float) (calcAnimSpeed(pBlockEntity.speed) * EDGConfig.COMMON.LOVE_MACHINE_DAMAGE_FACTOR.get() * 10f)); } } else { - if (pBlockEntity.hasLubricant()) { + if (pBlockEntity.hasLubricant() || !EDGConfig.COMMON.ENABLE_LOVE_MACHINE_DAMAGE.get()) { if (entity instanceof LivingEntity livingEntity) { livingEntity.addEffect(new MobEffectInstance(MobEffects.HEAL, 20, 1, true, true, true)); livingEntity.addEffect(new MobEffectInstance(MobEffects.DIG_SPEED, 20, 1, true, true, true)); } - } else entity.hurt(EDGDamageTypes.causeFuckedDamage(pLevel.registryAccess()), calcAnimSpeed(pBlockEntity.speed)); + } else entity.hurt(EDGDamageTypes.causeFuckedDamage(pLevel.registryAccess()), (float) (calcAnimSpeed(pBlockEntity.speed) * EDGConfig.COMMON.LOVE_MACHINE_DAMAGE_FACTOR.get())); } } diff --git a/src/main/java/top/r3944realms/eroticdungeongame/content/command/EDGCommand.java b/src/main/java/top/r3944realms/eroticdungeongame/content/command/EDGCommand.java index cfebebbd..ae3a3c6a 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/content/command/EDGCommand.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/content/command/EDGCommand.java @@ -42,12 +42,14 @@ import net.minecraftforge.common.util.FakePlayerFactory; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import top.r3944realms.eroticdungeongame.api.workspace.Services; +import top.r3944realms.eroticdungeongame.config.EDGConfig; import top.r3944realms.eroticdungeongame.content.block.ISeatBlock; import top.r3944realms.eroticdungeongame.content.block.blockentity.BaseSeatBlockEntity; import top.r3944realms.eroticdungeongame.content.entity.SeatEntity; import top.r3944realms.eroticdungeongame.content.item.DeviceKeyItem; import top.r3944realms.eroticdungeongame.content.item.FilterItem; import top.r3944realms.eroticdungeongame.content.util.FurnitureHelper; +import top.r3944realms.eroticdungeongame.core.service.NorpService; import top.r3944realms.eroticdungeongame.core.service.SeatService; import top.r3944realms.lib39.core.command.ICommandHelpManager; import top.r3944realms.lib39.core.command.SimpleHelpCommand; @@ -87,6 +89,7 @@ public class EDGCommand extends SimpleHelpCommand { private void registerCommands(@NotNull CommandDispatcher dispatcher) { // edg eroticdungeongame // edg help toggle + // edg norp // edg device bind [player] [code] 将[player] (没有则命令执行者) 绑定在处的设备上 (如果有[code] 则顺便上锁) // unbind 解绑处的设备 // [player] 解绑 (没有则命令执行者) @@ -114,6 +117,10 @@ public class EDGCommand extends SimpleHelpCommand { // reset 重置 getRoot() .executes(this::handleHelp) + .then( + Commands.literal("norp") + .executes(this::edg$norp) + ) .then( Commands.literal("device") .requires(source -> source.hasPermission(2)) @@ -341,6 +348,9 @@ public class EDGCommand extends SimpleHelpCommand { commandHelpManager.registerCommands(builder -> builder.root("edg", "commands.eroticdungeongame.root") .expanded(true) + .branch("norp", "commands.help.eroticdungeongame.norp", norpBuilder -> { + norpBuilder.leaf("norp", "commands.help.eroticdungeongame.norp.desc"); + }) // device 命令分支 .branch("device", "commands.help.eroticdungeongame.device", stack -> stack.hasPermission(2), deviceBuilder -> { deviceBuilder.expanded(false) @@ -486,7 +496,15 @@ public class EDGCommand extends SimpleHelpCommand { ); } - + private int edg$norp(@NotNull CommandContext context) throws CommandSyntaxException { + //todo: 完善下内容 + CommandSourceStack source = context.getSource(); + ServerPlayer player = source.getPlayerOrException(); + if (EDGConfig.COMMON.ENABLE_NORP.get()) { + boolean norp = NorpService.norp(player); + } + return 1; + } // ============ Device Bind Methods ============ private int edg$device$bind$pos$self(CommandContext context) throws CommandSyntaxException { BlockPos pos = BlockPosArgument.getBlockPos(context, "pos"); diff --git a/src/main/java/top/r3944realms/eroticdungeongame/core/capability/AbstractPlayerDungeonData.java b/src/main/java/top/r3944realms/eroticdungeongame/core/capability/AbstractPlayerDungeonData.java index d95b56e5..d4a1868c 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/core/capability/AbstractPlayerDungeonData.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/core/capability/AbstractPlayerDungeonData.java @@ -50,11 +50,13 @@ public sealed abstract class AbstractPlayerDungeonData extends NBTEntitySyncData setPlayerBoundingBox(null); AABB aabb = player.getDimensions(player.getPose()).makeBoundingBox(player.position()); player.setBoundingBox(aabb); + setNorpTime(INVALID); } private void setDungeonData_(@NotNull SeatEntity seat, @NotNull ISeatType type) { setEyeHeight(type.getEyeHeight()); setPlayerBoundingBox(type.getPlayerBB()); setDeviceMainBlockPos(seat.getLinkedBlockPos()); + setNorpState(IDLE); } } diff --git a/src/main/java/top/r3944realms/eroticdungeongame/core/capability/PlayerDungeonData.java b/src/main/java/top/r3944realms/eroticdungeongame/core/capability/PlayerDungeonData.java index 5e986ff4..589d5557 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/core/capability/PlayerDungeonData.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/core/capability/PlayerDungeonData.java @@ -23,6 +23,7 @@ import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.player.Player; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.Vec3; +import org.intellij.lang.annotations.MagicConstant; import org.jetbrains.annotations.NotNull; import top.r3944realms.eroticdungeongame.core.event.CommonHandler; import top.r3944realms.lib39.util.nbt.NBTReader; @@ -35,12 +36,17 @@ public final class PlayerDungeonData extends AbstractPlayerDungeonData { public static final String SEAT_ANIMATION = "seat_animation"; public static final String CURRENT_BB = "current_bb"; public static final String CURRENT_EYE_HEIGHT = "current_eye_height"; + public static final String NORP_STATE = "norp_state"; + public static final String NORP_TIME = "norp_time"; public static final AABB ZERO = AABB.ofSize(Vec3.ZERO, 0, 0, 0); public Player player; public BlockPos pos; public ResourceLocation anim; public AABB currentBB; public float currentEyeHeight; + public long norpTime; + @MagicConstant(intValues = {INVALID, IDLE, REQUEST}) + public int norpState; public PlayerDungeonData(Player player) { super(CommonHandler.Game.DUNGEON_SYNC); @@ -54,6 +60,8 @@ public final class PlayerDungeonData extends AbstractPlayerDungeonData { .stringIf(SEAT_ANIMATION, anim != null, () -> anim.toString()) .compoundIf(CURRENT_BB, currentBB != null, () -> writeBB(currentBB)) .floatValue(CURRENT_EYE_HEIGHT, currentEyeHeight, -1) + .longValue(NORP_TIME, norpTime, -1) + .intValue(NORP_STATE, norpState, INVALID) .build(); } @@ -92,7 +100,9 @@ public final class PlayerDungeonData extends AbstractPlayerDungeonData { } }); }, null) - .floatValue(CURRENT_EYE_HEIGHT, this::setEyeHeight, -1.0f); + .floatValue(CURRENT_EYE_HEIGHT, this::setEyeHeight, -1.0f) + .intValue(NORP_STATE, this::setNorpState) + .longValue(NORP_TIME, this::setNorpTime); } @@ -144,4 +154,26 @@ public final class PlayerDungeonData extends AbstractPlayerDungeonData { this.currentEyeHeight = eyeHeight; markDirty(); } + + @Override + public void setNorpState(int state) { + this.norpState = state; + markDirty(); + } + + @Override + public int getNorpState() { + return norpState; + } + + @Override + public void setNorpTime(long norpTime) { + this.norpTime = norpTime; + markDirty(); + } + + @Override + public long getNorpTime() { + return norpTime; + } } diff --git a/src/main/java/top/r3944realms/eroticdungeongame/core/service/NorpService.java b/src/main/java/top/r3944realms/eroticdungeongame/core/service/NorpService.java new file mode 100644 index 00000000..289af180 --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/core/service/NorpService.java @@ -0,0 +1,61 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.core.service; + +import net.minecraft.world.entity.player.Player; +import top.r3944realms.eroticdungeongame.api.capability.IPlayerDungeonData; +import top.r3944realms.eroticdungeongame.api.workspace.Services; +import top.r3944realms.eroticdungeongame.config.EDGConfig; +import top.r3944realms.eroticdungeongame.core.capability.PlayerDungeonDataProvider; +import top.r3944realms.eroticdungeongame.util.IEDGEntity; + +public class NorpService { + public static boolean checkCanNorp(Player player) { + return Services.WORK_SPACE.isInDevice(player); + } + public static boolean norp(Player player) { + if (checkCanNorp(player)) + return player.getCapability(PlayerDungeonDataProvider.PLAYER_DUNGEON_DATA_CAP) + .map(data -> { + boolean flag = false; + switch (data.getNorpState()) { + case IPlayerDungeonData.IDLE -> { + flag = System.currentTimeMillis() - data.getNorpTime() >= EDGConfig.COMMON.NORP_SUCCESSFUL_COOLDOWN.get() * 1000; + if (flag) { + data.setNorpState(data.REQUEST); + //todo 提醒玩家下次生效 + } else { + // todo 还在冷却中 + } + } + case IPlayerDungeonData.REQUEST -> { + flag = System.currentTimeMillis() - data.getNorpTime() >= EDGConfig.COMMON.NORP_INTERVAL_TIME.get() * 1000; + if (flag) { + data.setNorpState(data.IDLE); + ((IEDGEntity)player).releaseEntityFromEDGDevice(true); + //todo 提醒玩家成功 + } else { + // todo 提醒玩家等待间隔 + } + } + default -> {} + } + return flag; + }).orElse(false); + else return false; + } +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/util/NorpLogger.java b/src/main/java/top/r3944realms/eroticdungeongame/util/NorpLogger.java new file mode 100644 index 00000000..330ed4eb --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/util/NorpLogger.java @@ -0,0 +1,253 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.util; + +import net.minecraftforge.fml.loading.FMLPaths; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +import java.io.BufferedWriter; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.concurrent.locks.ReentrantLock; + +/** + * NORP Logger - 简单的日志记录工具 + * 仅在调用时检查并写入日志,自动按天轮换 + */ +public class NorpLogger { + + private static final Logger LOGGER = LoggerFactory.getLogger("NorpLogger"); + + // 日志文件配置 + private static final String LOG_DIR = "norp"; + private static final String LOG_FILE_PREFIX = "norp_"; + private static final String LOG_FILE_SUFFIX = ".log"; + private static final String DATE_FORMAT = "yyyy-MM-dd"; + private static final String TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm:ss"; + + // 日志级别枚举 + public enum LogLevel { + INFO, WARNING, ERROR, DEBUG, SUCCESS + } + + private static final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(DATE_FORMAT); + private static final DateTimeFormatter timestampFormatter = DateTimeFormatter.ofPattern(TIMESTAMP_FORMAT); + private static final ReentrantLock lock = new ReentrantLock(); + + // 当前日志文件信息缓存 + private static Path currentLogPath = null; + private static String currentDateStr = null; + + /** + * 私有构造函数 - 工具类不需要实例化 + */ + private NorpLogger() {} + + /** + * 获取今天的日志文件路径 + */ + private static Path getTodayLogPath() { + Path mcPath = FMLPaths.GAMEDIR.get(); + String today = LocalDate.now().format(dateFormatter); + String fileName = LOG_FILE_PREFIX + today + LOG_FILE_SUFFIX; + return mcPath.resolve(LOG_DIR).resolve(fileName); + } + + /** + * 确保日志目录存在 + */ + private static void ensureLogDirectoryExists() { + try { + Path logDir = FMLPaths.GAMEDIR.get().resolve(LOG_DIR); + if (!Files.exists(logDir)) { + Files.createDirectories(logDir); + } + } catch (IOException e) { + LOGGER.error("Failed to create log directory", e); + } + } + + /** + * 检查是否需要切换日志文件 + */ + private static boolean shouldRotateLog() { + String today = LocalDate.now().format(dateFormatter); + return !today.equals(currentDateStr) || currentLogPath == null; + } + + /** + * 获取当前应该写入的文件路径 + */ + private static Path getCurrentLogPath() { + lock.lock(); + try { + if (shouldRotateLog()) { + currentDateStr = LocalDate.now().format(dateFormatter); + currentLogPath = getTodayLogPath(); + ensureLogDirectoryExists(); + } + return currentLogPath; + } finally { + lock.unlock(); + } + } + + /** + * 写入日志到文件 + */ + private static void writeToFile(String content) { + Path logPath = getCurrentLogPath(); + + lock.lock(); + try (BufferedWriter writer = new BufferedWriter( + new OutputStreamWriter( + new FileOutputStream(logPath.toFile(), true), + StandardCharsets.UTF_8 + ) + )) { + writer.write(content); + writer.newLine(); + writer.flush(); + } catch (IOException e) { + LOGGER.error("Failed to write to log file: {}", logPath, e); + } finally { + lock.unlock(); + } + } + + /** + * 格式化日志消息 + */ + private static String formatMessage(String level, String message, Object... args) { + String formattedMsg = args.length > 0 ? String.format(message, args) : message; + return String.format("%s [%s] %s", + LocalDateTime.now().format(timestampFormatter), + level, + formattedMsg); + } + + /** + * 记录日志(核心方法) + */ + public static void log(String level, String message, Object... args) { + String formattedMessage = formatMessage(level, message, args); + writeToFile(formattedMessage); + } + + /** + * 使用LogLevel枚举记录日志 + */ + public static void log(LogLevel level, String message, Object... args) { + log(level.name(), message, args); + } + + /** + * 记录INFO级别日志 + */ + public static void info(String message, Object... args) { + log(LogLevel.INFO, message, args); + } + + /** + * 记录WARNING级别日志 + */ + public static void warning(String message, Object... args) { + log(LogLevel.WARNING, message, args); + } + + /** + * 记录ERROR级别日志 + */ + public static void error(String message, Object... args) { + log(LogLevel.ERROR, message, args); + } + + /** + * 记录DEBUG级别日志 + */ + public static void debug(String message, Object... args) { + log(LogLevel.DEBUG, message, args); + } + + /** + * 记录SUCCESS级别日志 + */ + public static void success(String message, Object... args) { + log(LogLevel.SUCCESS, message, args); + } + + /** + * 记录NORP事件 + */ + public static void logNorpEvent(String playerName, String action, String result) { + info("NORP Event - Player: %s, Action: %s, Result: %s", playerName, action, result); + } + + /** + * 记录NORP成功 + */ + public static void logNorpSuccess(String playerName, String action) { + success("NORP Success - Player: %s, Action: %s", playerName, action); + } + + /** + * 记录NORP失败 + */ + public static void logNorpFailure(String playerName, String action, String reason) { + warning("NORP Failure - Player: %s, Action: %s, Reason: %s", playerName, action, reason); + } + + /** + * 记录带标题的分隔线 + */ + public static void logSeparator(String title) { + String separator = "════════════════════════════════════════════"; + info("%s %s %s", separator, title, separator); + } + + /** + * 记录空行 + */ + public static void logEmptyLine() { + writeToFile(""); + } + + /** + * 获取当前日志文件路径 + */ + public static Path getCurrentLogFilePath() { + return getCurrentLogPath(); + } + + /** + * 检查指定日期的日志文件是否存在 + */ + public static boolean logExistsForDate(LocalDate date) { + String fileName = LOG_FILE_PREFIX + date.format(dateFormatter) + LOG_FILE_SUFFIX; + Path logPath = FMLPaths.GAMEDIR.get().resolve(LOG_DIR).resolve(fileName); + return Files.exists(logPath); + } +}