@@ -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
4466ClientLevel 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