diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8af3f83 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,184 @@ +# 贡献指南 + +如果你不会写代码,可以在 issues 分享你的想法,只要符合这几个条件: + +1. SFCraft 是原版生存服,新特性设想(尽量)不破坏原版平衡性,不干涉原版机制。(典型例子: tpa,死亡不掉落) + 如果死亡都是一件不足挂齿的事情,那么死亡的意义何在呢?如果我们可以到处传送,那么交通工程的意义何在呢? +2. 新特性对于大多数人有价值。 + 否则没有理由开发它。 + +也可以在讨论群内直接提出。但是最好还是发一个 feature request 这样方便开发者们管理请求。 + +如果你不仅有想法,还会开发(Java),那么请往下看 +## 开始之前 + +首先,光会一些基本的 Java 语法可能不足以支撑你开发新特性。在这种情况下,不建议勉强。 +其次,开发 Mod 你必须要有科学上网环境,否则配置工程时可能会遇到很多问题,以至于根本没法开始开发工作。 + +SFCraft 是一个纯粹的服务端模组,这意味着**我们不能添加原版游戏没有的物品/方块/生物**,因此你可能不太用得上 Fabric API 的内容。然而,了解一些基本的模组开发常识也是有帮助的 + +[FabricMC Wiki 官方教程(全英)](https://fabricmc.net/wiki/tutorial:start) + +最后,请将这篇文章看完。 + +## 项目结构 + +SFCraft 是一个 Fabric Mod,他的所有内容都是放在一起的,下列是一些关键的类: + + - `io.ib67.sfcraft.config.SFConfig` 这是 Mod 的配置文件。 + 在此类定义的字段将会自动作为配置文件的一部分生成。你可以通过调用 `SFCraft.getInstance().getConfig()` 得到配置对象。 + - `io.ib67.sfcraft.Listener` 是事件监听器。大多数回调/事件的处理代码放在这里。 + - `io.ib67.sfcraft.Commands` 是处理命令逻辑的地方,而命令在 `SFCraft#registerCommands` 方法注册。 + - `io.ib67.sfcraft.SFConsts` 存放一些频繁使用的常量,例如权限。 + - `io.ib67.sfcraft.mixin.server` 下是一些 `Mixin` 类,接下来会讲。 + +最后,`io.ib67.sfcraft.SFCraft` 是整个 Mod 的入口,也是核心部分。你可以从 `SFCraft.getInstance()` 返回的对象得到 Mod 里的大多数组件。 + +## 注册事件 +Fabric API 有事件的情况下可以直接效仿着在 SFCraft `onInitialize` 时注册: + +```java + ServerPlayConnectionEvents.JOIN.register(listener::onPlayerJoin); + CommandRegistrationCallback.EVENT.register(this::registerCommands); + ServerLifecycleEvents.SERVER_STARTED.register(this::onServerStarted); +``` + +通常你应该把复杂的逻辑放到 `Listener` 里。这里的 `registerCommands` 和 `onServerStarted` 是个特例,因为他们并没有太多要做的事。 + +然而,没有的情况下则需要你自己 Mixin。SFCraft 的运行环境 100% 为服务端,请完全按照服务端 Mod 的写法来写。 +写 Mixin 是为了将事件转发到 Listener: +```java +@Mixin(ServerPlayerEntity.class) +public class PlayerEntityMixin { + @Inject(method = "onDeath", at = @At("TAIL")) + public void onDeath(DamageSource damageSource, CallbackInfo ci) { + SFCraft.getInstance().getListener().onPlayerDeath((ServerPlayerEntity) (Object) this, damageSource); + } + + @Inject(method = "trySleep", at = @At("HEAD"), cancellable = true) + public void onSleep(BlockPos pos, CallbackInfoReturnable> cir) { + var result = SFCraft.getInstance().getListener().onPlayerSleep((ServerPlayerEntity) (Object) this, pos); + if(result.left().isPresent()){ + cir.setReturnValue(result); + } + } +} +``` +除了针对性特别强或者必要,不要把太多代码放在 Mixin 里面。 + +## 指令 & 权限 + +SFCraft 有自己的一套权限系统(可能会被换掉) + +### 命令 + +SFCraft 的所有指令在 `SFCraft#registerCommands` 内注册,使用 Brigadier. +然而,命令的逻辑需要移动到 `Commands` 以防代码囤积在 `SFCraft` 内。 + +### 权限 + +SFCraft 使用 `Permission` 类实现权限。所有在代码中使用的权限(除了 `special`)都要堆放在 `SFConsts` 常量类下: + +```java +@UtilityClass +public class SFConsts { + public static final String SPECIAL_SUDO = "sfcraft.special.state.sudo"; + public static final Permission USE_AT = ofSFCPermission("chat.at", true); + // ..... +} +``` + +具体用法可以看 `SFConsts#ofSFCPermission` 的参数命名,以及 `Permission` 类本身,在此不过多赘述。要注意的是,`Permission` 局限在于他只能处理有玩家实体的情况。 +虽然这足够应付大多数情况,然而有的命令不需要玩家存在也可以使用(比如 `addwl`),这种情况下,注册命令时需要使用如下技巧: + +```java +LiteralArgumentBuilder.literal("addwl") + .requires(it -> it.hasPermissionLevel(2) || SFConsts.COMMAND_ADDWL.hasPermission(it.getPlayer())) +``` + +以此保持命令方块和控制台兼容性,注意不要写反。 + +## 随机事件 +随机事件在 `SFRandomEventRegistry` 的构造方法内注册。其本身是 `RandomEvent` 的子类。 + +```java +public class SFRandomEventRegistry extends SimpleRandomEventRegistry { + public SFRandomEventRegistry() { + registerEvent(LongNightEvent::new, World.OVERWORLD + , world -> !LongNightEvent.isRunning() + && world.getTimeOfDay() % 24000 == 18000 + && world.getRandom().nextBetween(0,100) < 5); // At midnight + registerEvent(DawnAfterLongNightEvent::new, World.OVERWORLD + , world -> world.getTimeOfDay() % 22200 == 0); // At dawn + } +} +``` + +由于从 "REG" 查询事件是否发生性能较慢,因此建议写随机事件的时候在 `start` 方法内标记状态,如上例中的 `LongNightEvent.isRunning()` + +下面是一个非常简单的随机事件例子: +```java +@RequiredArgsConstructor +public class DawnAfterLongNightEvent extends RandomEvent { + private final World world; + + @Override + public int start() { + if (LongNightEvent.justExpirencedLongNight) { + LongNightEvent.justExpirencedLongNight = false; + world.getServer().getPlayerManager().broadcast( + Text.literal("\"月亮\"离开了。") + .withColor(new Color(235, 91, 0).getRGB()) + .append(Text.literal("(永夜事件结束)").withColor(Colors.LIGHT_GRAY)) + , false); + } + return -1; + } + + @Override + public void onUpdate(int ticks) { + throw new UnsupportedOperationException(); + } + + @Override + public void end() { + throw new UnsupportedOperationException(); + } +} +``` + +当注册时的 Predicate 通过后他的 `start` 就会被运行,提供事件持续的时长(在本例中,`-1` 表示立即停止)。随后每一 World tick 它的 `onUpdate` 都会被执行。 +最后会被调用 `end` 结束事件。由于永夜事件结束是在 tick=18000 时,天还没亮,因此特地写了一个 `DawnAfterLongNightEvent` 实现延迟到黎明发消息。 + +## 聊天修改 + +类似 `.xyz`, `@xx` 的功能都位于 `message.feature` 包下,且在 `SFMessageDecorator` 的构造方法中被注册。 +它们实际上是 Minecraft 提供的 `MessageDecorator` 接口的实现。 + +通常,一条消息进入服务器后会轮流经过许多 decorator 的处理。因此为了避免 siblings 嵌套问题,`SimpleMessageDecorator` (也就是 `SFMessageDecorator` 的超类) 会自动将 decorator 返回的 Text 展开—— +具体的说,将返回的结果 copy,去除 siblings,然后将 siblings 逐个加入一个缓存队列中,下一个 Decorator 将会逐个处理队列中的 `Text`,且每次处理完后都会清理掉原本的缓存队列。因此,在没有特殊需求的情况下,请总是返回你 Decorator 不关注的信息。 + +一个简单的例子: +```java + @Override + public Text decorate(@Nullable ServerPlayerEntity sender, Text message) { + if (sender != null) { + if (!SFConsts.USE_AT.hasPermission(sender)) { + return message; + } + } + var text = message.getLiteralString(); + if (text == null) return message; +} +``` +## 配置文件 + +它比较像摆设。 +与一般的 Mod 不同,SFCraft 没有 Client-Side Translatable Key 可用,因此一些不常修改的信息可以直接硬编码字符。 +在未来可能会加入多语言支持,如果有外国人的话。 + +## 其他 + +一些工具函数可以堆放在 `Helper` 类。 + +提交的新功能负责人要顺便去 wiki 写一下章节,因为新功能以后还会越来越多 \ No newline at end of file diff --git a/README.md b/README.md index 1af256c..b5911c1 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ 本仓库为 S.F Craft 服务器的功能 Mod, S.F Craft 是一个邀请制的纯原版生存服务器。 本插件专门为 SFC 定制,因此您可能需要花费大多功夫来将其用在你的服务器上(比如修改大量的硬编码字符串,强制启用的功能,等等),因此,并不建议这么做。 -本仓库开源供有能力的玩家贡献。 +本仓库开源供有能力的玩家贡献。[贡献指南](./CONTRIBUTING.md) # 特性 太多,懒得写 \ No newline at end of file