update contributing.md
This commit is contained in:
1 file changed
+1
-159
+1
-159
@@ -23,162 +23,4 @@ SFCraft 是一个纯粹的服务端模组,这意味着**我们不能添加原
|
||||
|
||||
## 项目结构
|
||||
|
||||
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.util.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<Either<PlayerEntity.SleepFailureReason, Unit>> 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<PlayerEntity> USE_AT = ofSFCPermission("chat.at", true);
|
||||
// .....
|
||||
}
|
||||
```
|
||||
|
||||
具体用法可以看 `SFConsts#ofSFCPermission` 的参数命名,以及 `Permission` 类本身,在此不过多赘述。要注意的是,`Permission` 局限在于他只能处理有玩家实体的情况。
|
||||
虽然这足够应付大多数情况,然而有的命令不需要玩家存在也可以使用(比如 `addwl`),这种情况下,注册命令时需要使用如下技巧:
|
||||
|
||||
```java
|
||||
LiteralArgumentBuilder.<ServerCommandSource>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` 包下,且在 `SimpleMessageDecorator` 的构造方法中被注册。
|
||||
它们实际上是 Minecraft 提供的 `MessageDecorator` 接口的实现。
|
||||
|
||||
通常,一条消息进入服务器后会轮流经过许多 decorator 的处理。因此为了避免 siblings 嵌套问题,`SimpleMessageDecorator` (也就是 `SimpleMessageDecorator` 的超类) 会自动将 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 写一下章节,因为新功能以后还会越来越多
|
||||
# Obsoleted since refactor
|
||||
Reference in new issue
Block a user