Skip to content

Commit b031ec9

Browse files
committed
update s&c
1 parent 541e7fe commit b031ec9

1 file changed

Lines changed: 88 additions & 7 deletions

File tree

docs/holojaneway/0.2 服务端和客户端.md

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,28 @@ public void bothSideMethod(Level level){
3232

3333
借助`level.isClientSide`,我们可以判断传入的参数是对应逻辑服务端还是逻辑客户端。
3434

35+
<div style={{
36+
backgroundColor: 'transparent',
37+
border: '2px solid #3c91ff',
38+
borderRadius: '0.5em',
39+
padding: '1em',
40+
}}>
41+
<Tabs groupId="mc-version">
42+
<TabItem value="1211" label="1.21.1">
43+
44+
如果需要在没有level对象的情况下“凭空”分辨两端,使用`DistExecutor`来分辨两端并执行逻辑,但上述的方法在绝大多数时候已经足够了。
45+
46+
</TabItem>
47+
<TabItem value="261" label="26.1">
48+
49+
如果需要在没有level对象的情况下“凭空”分辨两端,你也可以使用`FMLEnvironment.getDist()`来分辨物理客户端和服务端,或者`EffectiveSide.get()`来分辨逻辑客户端和服务端。
50+
51+
</TabItem>
52+
</Tabs>
53+
<p></p>
54+
</div>
55+
<p></p>
56+
3557
## 获取服务端和客户端
3658

3759
在多数时候,方法的形参带有`level`,我们可以直接使用上面的方法来实现想要的逻辑。但偶尔我们需要“凭空”获取两端。
@@ -43,6 +65,15 @@ Minecraft minecraft = Minecraft.getInstance();
4365
//如果你还想要Level
4466
ClientLevel clientLevel = minecraft.level;
4567
```
68+
<div style={{
69+
backgroundColor: 'transparent',
70+
border: '2px solid #3c91ff',
71+
borderRadius: '0.5em',
72+
padding: '1em',
73+
}}>
74+
<Tabs groupId="mc-version">
75+
<TabItem value="1211" label="1.21.1">
76+
:::caution
4677

4778
在服务端,你需要借助`LogicalSidedProvider`
4879

@@ -54,10 +85,27 @@ ServerLevel serverLevel = minecraftServer.getLevel(Level.OVERWORLD);
5485
//当然,你可以遍历所有的level,通过某种条件来找到自己想要的。但请尽量不要这样做,除非真的很有必要,没有其他方法,或者你已经尽了最大可能减少性能开销。
5586
```
5687

57-
当然你也可以使用`LogicalSidedProvider`来获取客户端,但是在多数时候显然不如直接使用`Minecraft.getInstance()`来的方便。
88+
:::
5889

59-
你也可以使用`DistExecutor`来分辨两端并执行逻辑,但上述的方法在绝大多数时候已经足够了。
90+
</TabItem>
91+
<TabItem value="261" label="26.1">
6092

93+
在服务端,你需要借助`ServerLifecycleHooks`
94+
95+
```java
96+
MinecraftServer minecraftServer = ServerLifecycleHooks.getCurrentServer()
97+
;
98+
//如果你还想要Level,由于有多个Level存在,你需要提前通过其他方法获取ResourceKey<Level>
99+
//原版三个世界的ResourceKey在Level类中可以找到,以主世界为例:
100+
ServerLevel serverLevel = minecraftServer.getLevel(Level.OVERWORLD);
101+
//当然,你可以遍历所有的level,通过某种条件来找到自己想要的。但请尽量不要这样做,除非真的很有必要,没有其他方法,或者你已经尽了最大可能减少性能开销。
102+
```
103+
104+
</TabItem>
105+
</Tabs>
106+
<p></p>
107+
</div>
108+
<p></p>
61109

62110

63111
## OnlyIn!
@@ -71,17 +119,16 @@ ServerLevel serverLevel = minecraftServer.getLevel(Level.OVERWORLD);
71119
<Tabs groupId="mc-version">
72120
<TabItem value="1217" label="1.21.7">
73121
:::caution
74-
75122
在1.21.7,OnlyIn遭到了彻底的降级。现在OnlyIn不再起任何实际作用,NeoForge会检查你是否还在用,并且在游戏启动时给出警告——如果你正在升级版本,应该很快就能发现这个问题。这也意味着,你需要自行处理这个双端分离的问题。参考[OnlyIn,但是为什么?](#onlyin但是为什么)了解更多内容。
76123

124+
**虽然我们现在不能使用`@OnlyIn`,但我仍然建议你了解以下内容。**
125+
77126
:::
78127

79128
</TabItem>
80129
</Tabs>
81-
82130
<p></p>
83131
</div>
84-
85132
<p></p>
86133

87134
如果你因为好奇而查看了`ClientLevel`的具体内容,你可能已经注意到了这样一个注解`@OnlyIn(Dist.CLIENT)`。括号中的value一般是`Dist.CLIENT`,极少数时候会用到`Dist.DEDICATED_SERVER`。字段、方法等一旦被`@OnlyIn`注解,他们对于另一端就是逻辑上不可见的(即使这几行代码确实存在于磁盘之中)。请注意,`@OnlyIn`区分的是物理两端而不是逻辑两端。这意味着单人游戏中的服务端可以执行被注解`@OnlyIn(Dist.CLIENT)`的代码,而`@OnlyIn(Dist.DEDICATED_SERVER)`在单人游戏时意味着谁也不能执行到它。
@@ -203,6 +250,8 @@ public class Example {
203250

204251
然后,boom——但是这样听起来很不合理,对吧?我明明可以信心十足地约定或保证`foo()`不会在服务端被用到,何苦一定要拆成`ExampleC`和`ExampleS`两个类呢?
205252

253+
#### 邪修之一:执行单端代码
254+
206255
当然也有办法,我们注意到刚才只是说“JVM可以把类的方法中局部用到的类也一并加载”,那我们可以把`foo()`的内容用一个`Runnable`套起来,就像这样:
207256

208257
```java
@@ -218,10 +267,42 @@ public class Example {
218267

219268
当`Example`加载的时候,就只会加载到`Runnable`这个类——而这个类本身里面什么也没有。等到真正开始执行`foo()`,才会尝试加载`Minecraft`类。
220269

221-
:::info
270+
:::warning
222271

223-
编译器或许还会提示你,`new Runnable()`可以写成lambda形式——在这里是万万不能的。`new Runnable(){}`是创建了一个新的匿名内部类,而lambda表达式则会直接调用`lambdaMetaFactory`来进行处理,在此过程中仍然会导致加载方法体内的内容。这是一个值得注意的微妙差别。
272+
IDE或许还会提示你,`new Runnable()`可以写成lambda形式——在这里是万万不能的。`new Runnable(){}`是创建了一个新的匿名内部类,而lambda表达式则会直接调用`lambdaMetaFactory`来进行处理,在此过程中仍然会导致加载方法体内的内容。这是一个值得注意的微妙差别。
224273

225274
:::
226275

227276
当然这样并不方便,你或许可以像T88一样搞个编译器插件,甚至是直接用Manifold插件写个宏——但尽量还是直接分成两个类吧。
277+
278+
#### 邪修之二:存储单端数据
279+
280+
我们可以利用JVM的懒加载机制,只要我们能够保证服务端代码执行路径上一定不会触碰到单端字段,就可以在一个双端类中写出一个单端字段:
281+
282+
```java
283+
public class SignBlockEntity extends BlockEntity {
284+
public ...
285+
protected SingleVariant disguiseModel;
286+
287+
public SignBlockEntity(BlockPos pPos, BlockState pBlockState) {
288+
this(pPos, pBlockState, new Vector3f(0, 0, 0), Integer.MAX_VALUE, 16, 16, 0);
289+
}
290+
291+
public SignBlockEntity(BlockPos pPos, BlockState pBlockState, Vector3f screenStart16, int defaultScreenLength16, int screenHeight16, int screenThick16, int screenMargin16) {
292+
super(ModBlockEntityTypeRegistry.TEST_SIGN.get(), pPos, pBlockState);
293+
...
294+
}
295+
```
296+
297+
这里的`SingleVariant`是一个原版的客户端类。
298+
299+
在服务端加载`SignBlockEntity`时,`SingleVariant disguiseModel`实际上会被解释为`Reference<SingleVariant> disguiseModel`,又因为泛型擦除机制,最终变成`Reference disguiseModel`。只要JVM接下来不需要了解`disguiseModel`到底代表什么,就不会去尝试加载不存在的`SingleVariant`类。
300+
301+
当然这种方法非常危险——你需要保证你的代码中,服务端逻辑绝对不会接触到这个字段,否则就会立即爆炸。
302+
303+
:::danger
304+
305+
更要命的是,危险可能由别人引起。如果其他mod或者NeoForge出于什么原因,利用反射对你的类动手动脚,比如`getDeclaredFields`加上`Field.getType()`,或者是`Field.getGenericType()`、`Method.getReturnType()`、`Method.getParameterTypes()`等等等等,都会引起类加载,从而导致游戏爆炸。
306+
307+
:::
308+

0 commit comments

Comments
 (0)