<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>IzzelAliz&#39;s Blog</title>
  
  
  <link href="/atom.xml" rel="self"/>
  
  <link href="https://izzel.io/"/>
  <updated>2025-07-13T12:15:43.524Z</updated>
  <id>https://izzel.io/</id>
  
  <author>
    <name>IzzelAliz</name>
    
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Java 双重锁定模式真的需要 volatile 吗？</title>
    <link href="https://izzel.io/2024/03/05/is-dcl-volatile-essential/"/>
    <id>https://izzel.io/2024/03/05/is-dcl-volatile-essential/</id>
    <published>2024-03-05T21:35:10.000Z</published>
    <updated>2025-07-13T12:15:43.524Z</updated>
    
    <content type="html"><![CDATA[<p>作为 Java 面试八股常见问题之一，单例模式怎么写自然有很多说法，比如下面这种就是天然的单例模式：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">enum</span> Singleton <span class="token punctuation">{</span>    <span class="token function">INSTANCE</span><span class="token punctuation">(</span><span class="token number">42</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">private</span> <span class="token keyword">int</span> answer<span class="token punctuation">;</span>    <span class="token function">Singleton</span><span class="token punctuation">(</span><span class="token keyword">int</span> answer<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">this</span><span class="token punctuation">.</span>answer <span class="token operator">=</span> answer<span class="token punctuation">;</span>    <span class="token punctuation">}</span>    <span class="token keyword">public</span> <span class="token keyword">int</span> <span class="token function">getAnswer</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>answer<span class="token punctuation">;</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>但是如果在面试，面试官大概率是不会满意这份代码的。令人满意的代码，通常是下面这份双重检查锁定式的单例实现：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Singleton</span> <span class="token punctuation">{</span>    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">volatile</span> Singleton INSTANCE<span class="token punctuation">;</span>    <span class="token keyword">public</span> <span class="token keyword">static</span> Singleton <span class="token function">getInstance</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>        Singleton instance <span class="token operator">=</span> INSTANCE<span class="token punctuation">;</span>        <span class="token keyword">if</span> <span class="token punctuation">(</span>instance <span class="token operator">==</span> null<span class="token punctuation">)</span> <span class="token punctuation">{</span>            <span class="token keyword">synchronized</span> <span class="token punctuation">(</span>Singleton<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>                <span class="token keyword">if</span> <span class="token punctuation">(</span>INSTANCE <span class="token operator">==</span> null<span class="token punctuation">)</span> <span class="token punctuation">{</span>                    INSTANCE <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Singleton</span><span class="token punctuation">(</span><span class="token number">42</span><span class="token punctuation">)</span><span class="token punctuation">;</span>                <span class="token punctuation">}</span>            <span class="token punctuation">}</span>        <span class="token punctuation">}</span>        <span class="token keyword">return</span> instance<span class="token punctuation">;</span>    <span class="token punctuation">}</span>    <span class="token keyword">private</span> <span class="token keyword">int</span> answer<span class="token punctuation">;</span>    <span class="token keyword">public</span> <span class="token function">Singleton</span><span class="token punctuation">(</span><span class="token keyword">int</span> answer<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">this</span><span class="token punctuation">.</span>answer <span class="token operator">=</span> answer<span class="token punctuation">;</span>    <span class="token punctuation">}</span>    <span class="token keyword">public</span> <span class="token keyword">int</span> <span class="token function">getAnswer</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>answer<span class="token punctuation">;</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>这份代码无疑是正确的，<code>synchronized</code> 保证了单例不会被重复实例化，<code>volatile</code> 保证了 <code>INSTANCE</code> 对其他线程可见。但是如果去掉 <code>volatile</code> 呢？</p><h2 id="volatile-做了什么？"><a href="#volatile-做了什么？" class="headerlink" title="volatile 做了什么？"></a>volatile 做了什么？</h2><p>在去掉 <code>volatile</code> 之后，如果我们并发的调用 <code>Singleton.getInstance().getAnswer()</code>，可能会得到 <code>42</code>，以及 <code>0</code>。解释这个结果，需要介绍一点 Java 内存模型的知识。</p><p>对于并发代码，我们会关注的是读写的同步：我们希望能读到”在此之前应当已经写入的东西”，不要读到”未来才会发生的东西”，但是在复杂的现代计算机系统中，这并不是一件简单的事。在硬件上，乱序执行会导致读写的顺序改变；软件上，编译器会优化读写，相互无依赖的读写可能以任意顺序进行。</p><p>如何获得确定的执行顺序呢？Java 内存模型通过一系列 <a href="https://docs.oracle.com/javase/specs/jls/se21/html/jls-17.html#jls-17.4.5" target="_blank" rel="noopener">happens-before 顺序</a>约束执行顺序。如果我们有两个操作 x, y （操作 action，比如读变量、写变量、锁同步）满足 happens-before 规则，那么程序上可以认为 y 操作进行时可以观察到 x 的写入，记作 <code>hb(x, y)</code>。</p><p>部分顺序如下（原始定义更为复杂，此处已经化简过）：</p><ul><li>x y 有同步关系，则有 hb(x, y)<ul><li>对一个 volatile 变量 v 写的操作在后续对 v 的读操作前</li><li>释放锁的操作在后续锁定同一个锁的操作前</li></ul></li><li>传递性，即如果 hb(x, y) 且 hb(y, z)，那么 hb(x, z)</li></ul><p>没有定义 happens before 的情形，就可能观察到任意的顺序。例如，线程 A 按顺序写入 a b 两个普通变量，但是线程 B 不一定能观察到 a b 按顺序写入了，相反，线程 B 可能只能看到 b 被写入，但是 a 尚未写入。这种情形是最为普遍的未同步情景。</p><p>回到刚刚的代码，我们把不包含 volatile 的版本抽象成一串操作：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Singleton</span> <span class="token punctuation">{</span>    <span class="token keyword">private</span> <span class="token keyword">static</span> Singleton INSTANCE<span class="token punctuation">;</span>    <span class="token keyword">public</span> <span class="token keyword">static</span> Singleton <span class="token function">getInstance</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>        Singleton instance <span class="token operator">=</span> INSTANCE<span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// a: 读取 INSTANCE</span>        <span class="token keyword">if</span> <span class="token punctuation">(</span>instance <span class="token operator">==</span> null<span class="token punctuation">)</span> <span class="token punctuation">{</span>            <span class="token keyword">synchronized</span> <span class="token punctuation">(</span>Singleton<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// b: 对 Singleton.class 上锁</span>                <span class="token keyword">if</span> <span class="token punctuation">(</span>INSTANCE <span class="token operator">==</span> null<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// c: 读取 INSTANCE</span>                    INSTANCE <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Singleton</span><span class="token punctuation">(</span><span class="token number">42</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// d: 写入 INSTANCE</span>                <span class="token punctuation">}</span>            <span class="token punctuation">}</span> <span class="token comment" spellcheck="true">// e: 对 Singleton.class 解锁</span>        <span class="token punctuation">}</span>        <span class="token keyword">return</span> instance<span class="token punctuation">;</span>    <span class="token punctuation">}</span>    <span class="token keyword">private</span> <span class="token keyword">int</span> answer<span class="token punctuation">;</span>    <span class="token keyword">public</span> <span class="token function">Singleton</span><span class="token punctuation">(</span><span class="token keyword">int</span> answer<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">this</span><span class="token punctuation">.</span>answer <span class="token operator">=</span> answer<span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// f: 写入 answer</span>    <span class="token punctuation">}</span>    <span class="token keyword">public</span> <span class="token keyword">int</span> <span class="token function">getAnswer</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>answer<span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// g: 读取 answer</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>我们期望最终的结果是，两个线程调用 <code>getInstance().getAnswer()</code> 都返回 42，假设两个线程的操作分别为 a1, a2, b1, b2 …，，为了方便我们将 hb(x, y) 记作 x &lt; y，那么有：</p><ul><li>假设线程 1 首先抢到对 Singleton.class 的锁</li><li>线程内的操作如下<ul><li>a1 &lt; b1 &lt; c1 &lt; f1 &lt; d1 &lt; e1 &lt; g1</li><li>a2 &lt; b2 &lt; c2 &lt; f2 &lt; d2 &lt; e2 &lt; g2</li></ul></li><li>释放锁的操作在后续锁定同一个锁的操作前，所以 e1 &lt; b2</li></ul><p>线程 1 完成同步块内的操作后，我们观察线程 2 的执行。如果 a2 的结果是 <code>INSTANCE == null</code>，那么：</p><ul><li>线程 2 的执行情况是 a2 &lt; b2 &lt; c2 &lt; e2 &lt; h2</li><li>此时由传递性可以得到：f1 &lt; e1 &lt; b2 &lt; g2，也就是说 h2 可以观察到 f1 写入的 42</li></ul><p>而如果 a2 的结果是 <code>INSTANCE != null</code>，那么：</p><ul><li>线程 2 的执行情况是 a2 &lt; g2</li><li>此时 g2 和 f1 之间不能建立 happens before 关系，因此可能观察到 g2 在 f1 之前发生，也就是线程 2 读到了 0</li></ul><p>那么 volatile 做了什么呢？volatile 引入了写后读的顺序，也就是说加入了 d1 &lt; a2，那么：</p><ul><li>如果 a2 的结果是 <code>INSTANCE == null</code>，那么同上</li><li>如果 a2 的结果是 <code>INSTANCE != null</code>，我们仍然有 f1 &lt; d1 &lt; a2 &lt; g2，因此 g2 能观察到 f1，线程 2 读到了 42</li></ul><h3 id="损坏的-DCL-模式？"><a href="#损坏的-DCL-模式？" class="headerlink" title="损坏的 DCL 模式？"></a>损坏的 DCL 模式？</h3><p><code>volatile</code> 修饰符当前的语义是在 Java 1.5 引入的，准确的说是 JSR 133。在更早的版本，<code>volatile</code>没有 happens before 语义，因此不能用于实现 DCL 模式，这也是 Effective Java 中提到的 DCL is broken 的原因。</p><h2 id="但是也许我们不一定需要-volatile"><a href="#但是也许我们不一定需要-volatile" class="headerlink" title="但是也许我们不一定需要 volatile"></a>但是也许我们不一定需要 volatile</h2><p>如果读者真的去看了 JLS 的 Memory Model 部分，应该会很容易注意到<a href="https://docs.oracle.com/javase/specs/jls/se21/html/jls-17.html#jls-17.5" target="_blank" rel="noopener">接下来的一个章节</a>，其中提到了 final 字段的特殊语义：</p><blockquote><p>An object is considered to be <em>completely initialized</em> when its constructor finishes. A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object’s final fields.</p></blockquote><p>线程如果观察到一个完成初始化的对象，Java 可以保证同时能观察到初始化后的 final 字段。</p><p>实际的实现上，JVM 为写入 final 字段的构造方法退出时插入了一个 StoreStore|StoreLoad 内存屏障，保证了写入 final 字段的操作发生在”发布”这个对象之前。<a href="https://github.com/openjdk/jdk/blob/d6f2a174fcf41f0b091ef7eabea5d06fae90e0b2/src/hotspot/share/opto/parse1.cpp#L977-L1044" target="_blank" rel="noopener">相关代码</a>如下：</p><pre class="line-numbers language-cpp"><code class="language-cpp"><span class="token keyword">void</span> Parse<span class="token operator">::</span><span class="token function">do_exits</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>  <span class="token comment" spellcheck="true">// ...</span>  <span class="token comment" spellcheck="true">// Figure out if we need to emit the trailing barrier. The barrier is only</span>  <span class="token comment" spellcheck="true">// needed in the constructors, and only in three cases:</span>  <span class="token comment" spellcheck="true">//</span>  <span class="token comment" spellcheck="true">// 1. The constructor wrote a final. The effects of all initializations</span>  <span class="token comment" spellcheck="true">//    must be committed to memory before any code after the constructor</span>  <span class="token comment" spellcheck="true">//    publishes the reference to the newly constructed object. Rather</span>  <span class="token comment" spellcheck="true">//    than wait for the publication, we simply block the writes here.</span>  <span class="token comment" spellcheck="true">//    Rather than put a barrier on only those writes which are required</span>  <span class="token comment" spellcheck="true">//    to complete, we force all writes to complete.</span>  <span class="token comment" spellcheck="true">//</span>  <span class="token comment" spellcheck="true">// 2. ...</span>  <span class="token comment" spellcheck="true">// ...</span>  <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">method</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">is_initializer</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">&amp;&amp;</span>       <span class="token punctuation">(</span><span class="token function">wrote_final</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">||</span>         <span class="token punctuation">(</span>AlwaysSafeConstructors <span class="token operator">&amp;&amp;</span> <span class="token function">wrote_fields</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">||</span>         <span class="token punctuation">(</span>support_IRIW_for_not_multiple_copy_atomic_cpu <span class="token operator">&amp;&amp;</span> <span class="token function">wrote_volatile</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>    _exits<span class="token punctuation">.</span><span class="token function">insert_mem_bar</span><span class="token punctuation">(</span>Op_MemBarRelease<span class="token punctuation">,</span> <span class="token function">alloc_with_final</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token comment" spellcheck="true">// ...</span>  <span class="token punctuation">}</span>  <span class="token comment" spellcheck="true">// ...</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>也就是说，文章开始的代码，也可以这么写：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Singleton</span> <span class="token punctuation">{</span>    <span class="token keyword">private</span> <span class="token keyword">static</span> Singleton INSTANCE<span class="token punctuation">;</span>    <span class="token keyword">public</span> <span class="token keyword">static</span> Singleton <span class="token function">getInstance</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>        Singleton instance <span class="token operator">=</span> INSTANCE<span class="token punctuation">;</span>        <span class="token keyword">if</span> <span class="token punctuation">(</span>instance <span class="token operator">==</span> null<span class="token punctuation">)</span> <span class="token punctuation">{</span>            <span class="token keyword">synchronized</span> <span class="token punctuation">(</span>Singleton<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>                <span class="token keyword">if</span> <span class="token punctuation">(</span>INSTANCE <span class="token operator">==</span> null<span class="token punctuation">)</span> <span class="token punctuation">{</span>                    INSTANCE <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Singleton</span><span class="token punctuation">(</span><span class="token number">42</span><span class="token punctuation">)</span><span class="token punctuation">;</span>                <span class="token punctuation">}</span>            <span class="token punctuation">}</span>        <span class="token punctuation">}</span>        <span class="token keyword">return</span> instance<span class="token punctuation">;</span>    <span class="token punctuation">}</span>    <span class="token keyword">private</span> <span class="token keyword">final</span> <span class="token keyword">int</span> answer<span class="token punctuation">;</span>    <span class="token keyword">public</span> <span class="token function">Singleton</span><span class="token punctuation">(</span><span class="token keyword">int</span> answer<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">this</span><span class="token punctuation">.</span>answer <span class="token operator">=</span> answer<span class="token punctuation">;</span>    <span class="token punctuation">}</span>    <span class="token keyword">public</span> <span class="token keyword">int</span> <span class="token function">getAnswer</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>answer<span class="token punctuation">;</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>当然，这里的字段是 <code>int</code>，如果是一个普通的引用类型字段，这里可能还是需要 volatile 保证正确性。</p><h2 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h2><p><a href="https://www.cs.umd.edu/\~pugh/java/memoryModel/jsr-133-faq.html#finalRight" target="_blank" rel="noopener">https://www.cs.umd.edu/\~pugh/java/memoryModel/jsr-133-faq.html#finalRight</a></p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;DCL（Double-checked locking）是一种包含了一个可有可无的 &lt;code&gt;volatile&lt;/code&gt; 关键字的常见设计模式 ...&lt;/p&gt;
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>聊聊 Java 的 mlvm</title>
    <link href="https://izzel.io/2022/12/05/dynamic-java/"/>
    <id>https://izzel.io/2022/12/05/dynamic-java/</id>
    <published>2022-12-05T22:23:02.000Z</published>
    <updated>2025-07-13T12:15:43.523Z</updated>
    
    <content type="html"><![CDATA[<p>这可不是 Graal VM …</p><hr><p>你听说过这些东西吗：</p><ul><li>MethodHandle</li><li>invokedynamic</li><li>Continuation</li><li>值类型</li><li>…</li></ul><h2 id="达芬奇的直升机"><a href="#达芬奇的直升机" class="headerlink" title="达芬奇的直升机"></a>达芬奇的直升机</h2><p>上面的东西大多都是在 Java 7 出现的，也是 <a href="https://openjdk.org/projects/mlvm/" target="_blank" rel="noopener">mlvm 项目</a>的主要产出，意图打造一个可以允许多种语言的虚拟机。这个项目又被称为 the Da Vinci Machine Project。</p><p><img src="helicopter.jpg" alt></p><p>这张图就是出自达芬奇，已经快有 600 年的历史，而它描述的自然是一个直升机的设想。</p><p>mlvm 项目的历史也超过十年了。在 2009 年，mlvm 项目展现了雄伟的野心：</p><h2 id="动态调用"><a href="#动态调用" class="headerlink" title="动态调用"></a>动态调用</h2><p>动态调用也就是 Dynamic Invocation，已经在 Java 7 中很好地实现了 —— <code>invokedynamic</code> 指令。</p><p>这条指令，不同于已存在的四个指令，是允许动态调用的，同时也包含了其他的许多好处，比如消除了 int 这样的原始类型装箱拆箱的消耗。</p><p>它的动态调用允许 Java 代码更改某处调用点（调用点，当然就是 <code>CallSite</code>）实际调用的方法，就像写入一个字段一样轻松简单。而这里“实际调用的方法”，就是下面的轻量方法对象。</p><h3 id="可是动态调用呢？"><a href="#可是动态调用呢？" class="headerlink" title="可是动态调用呢？"></a>可是动态调用呢？</h3><p>在本文写作时（Java 19 已发布），Java 本身不带有任何涉及动态调用的代码。虽说如此，Java 中还是有不少地方使用到了 <code>invokedynamic</code>。</p><p>Lambda 表达式中，<code>invokedynamic</code> 指令用于生成接口的实现类。</p><p>从 Java 9 开始，<code>invokedynamic</code> 用于实现字符串拼接。</p><p>从 Java 16 开始，Record 类中 <code>invokedynamic</code> 用于生成 <code>toString</code>（该实现其实很不好）、<code>equals</code>、<code>hashCode</code> 这样的方法。</p><p>从 Java 17 开始，switch 表达式也使用 <code>invokedynamic</code> 生成 switch table。</p><p>除了 Lambda 表达式的实现之外，其他几个调用的 MH 都是使用 <code>MethodHandles</code> 中的方法进行组合而实现。</p><p>以上内容都不是动态调用，反而倒更像是某种意义上 Java 的宏了。</p><h2 id="轻量方法对象"><a href="#轻量方法对象" class="headerlink" title="轻量方法对象"></a>轻量方法对象</h2><p>Lightweight method objects，在 Java 7 中最终作为 <code>MethodHandle</code> 落地；更准确地描述可能是“一段代码”。</p><p>提到 <code>MethodHandle</code> 或者 MH，大多数人和互联网上的文章都会拿来和反射 API 进行比较，这里自然也拿来比较一下。</p><p>对于方法调用，反射 API 是有设计缺陷的：</p><ul><li><code>getMethod</code>、<code>getField</code> 这样的操作，并不允许提供方法返回值或者字段类型作为参数进行查询 —— 分明 JVM 中存在这样的机制，更别说 JVM 允许名称和参数列表项目而返回值不同的方法多个存在了；</li><li>什么是 <code>getMethod</code>，什么又是 <code>getDeclaredMethod</code> 呢？在语言层面和 JVM 层面，只有“可访问”和“不可访问”的区别，反射 API（很长一段时间）没有区别可访问的能力；</li><li>更别说 <code>Method#invoke</code> 的时候的装箱拆箱、可变长参数的数组创建、调用时权限检查等性能问题了。</li></ul><p>Java 7 出现的 MH 解决了上面的问题，尽管它相比反射 API 缺少了很多功能。与反射 API 相比，MH 更符合一个动态调用 API 的定位：</p><ul><li>字节码怎么写，<code>Lookup#findXXX</code> 就怎么写；</li><li>语义和字节码中这里能访问的东西，MH 就能访问；语义不能的，MH 就不能；</li><li>字节码里怎么调用，MH 就怎么调用，不会多装箱，或者把抛出的一场包装一层 <code>InvocationTargetException</code>；</li><li>直接调用什么性能，MH 调用就是什么性能（一定条件下）。</li></ul><p>当然，MH 缺少了调用的方法名称、声明的类之类的信息，只保存了调用方法的类型，所以称之为轻量方法对象。</p><h3 id="LambdaForm-另一个-JIT？"><a href="#LambdaForm-另一个-JIT？" class="headerlink" title="LambdaForm - 另一个 JIT？"></a>LambdaForm - 另一个 JIT？</h3><p>MH 不只是某种反射的替代品。在 <a href="https://openjdk.org/jeps/160" target="_blank" rel="noopener">JEP 160</a> 中 JDK 引入了 LambdaForm，用于表达 MethodHandle。</p><p>LambdaForm 是一系列方法调用、若干个参数和临时变量的“一段代码”。为了简化表达，这些参数和变量只有五种类型，也就是引用类型和四个基础类型（<code>int</code>、<code>long</code>、<code>float</code>、<code>double</code>）和 <code>void</code>。</p><p>以 LambdaForm 的文档为例：</p><pre><code>(a0:J)=&gt;{ a0 }    == identity(long)(a0:I)=&gt;{ t1:V = System.out#println(a0); void }    == System.out#println(int)(a0:L)=&gt;{ t1:V = System.out#println(a0); a0 }    == identity, with printing side-effect(a0:L, a1:L)=&gt;{ t2:L = BoundMethodHandle#argument(a0);                t3:L = Class#cast(t2,a1); t3 }    == invoker for identity method handle which performs cast</code></pre><p>LambdaForm 设计为可以轻易地翻译到字节码和 JIT IR，可以看作是字节码的抽象表达形式。</p><h2 id="轻量字节码加载"><a href="#轻量字节码加载" class="headerlink" title="轻量字节码加载"></a>轻量字节码加载</h2><p>Lightweight bytecode loading，可能你从来没有见过？</p><p>Java 15 包括了 <a href="https://openjdk.org/jeps/371" target="_blank" rel="noopener">JEP 371: Hidden Classes</a>，或者说是隐藏类；而在 Java 15 之前，这样的功能通过 <code>Unsafe#defineAnonymousClass</code> 实现。</p><p>这个功能用于生成一些轻量的、用于调用其他方法的没有名称的类。</p><p>如果是调用方法的话，是不是和 MH 非常类似？它与 MH 最大的区别是，它是类，所以可以有字段，被用于 Lambda 表达式中的变量捕获。</p><h2 id="接口注入"><a href="#接口注入" class="headerlink" title="接口注入"></a>接口注入</h2><p>Interface injection，接口注入，动态语言中通常有这样的功能，可惜 JVM 尚且没有。</p><p>这个设计的原型描述<sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup>是相对保守的，注入接口后不会影响已有类的解析和调用，但是 <code>instanceof</code> 和类型转换会反馈注入的接口。</p><p>运行时接口注入虽然没有被 JDK 加入，但是 DCEVM 项目（现在是 JetbrainsRuntime）可以实现这一功能。<a href="https://github.com/SpongePowered/Mixin" target="_blank" rel="noopener">Mixin</a> 这样的项目也可以在类加载的时候注入接口。</p><hr><p><a href="#Continuations">什么是 Continuation？</a></p><h2 id="Continuations"><a href="#Continuations" class="headerlink" title="Continuations"></a>Continuations</h2><p><a href="https://izzel.io/2022/09/04/what-color-is-your-function">Continuation</a> 就是程序后面的部分，或者说剩下的部分。</p><pre class="line-numbers language-java"><code class="language-java">System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"ザ・ワールド 時よ止まれ！"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>Thread<span class="token punctuation">.</span><span class="token function">sleep</span><span class="token punctuation">(</span><span class="token number">5000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"ザ・ワールド 時は動き出す"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>在执行到 <code>Thread#sleep</code> 时，自然最后一个 <code>System.out.println</code> 是当前的 Continuation。而将 Continuation 明确表示出来（比如一个 Lambda 表达式），并把它传递（passing）到其他地方的代码，就叫 continuation-passing style。</p><pre class="line-numbers language-java"><code class="language-java">System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"ザ・ワールド 時よ止まれ！"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>Runnable continuation <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"ザ・ワールド 時は動き出す"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>CompletableFuture<span class="token punctuation">.</span><span class="token function">delayedExecutor</span><span class="token punctuation">(</span><span class="token number">5000</span><span class="token punctuation">,</span> TimeUnit<span class="token punctuation">.</span>MILLISECONDS<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">execute</span><span class="token punctuation">(</span>continuation<span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>而不将 Continuation 明确表现出来，而是让 JVM 管理，就是这个项目的目标。</p><p>尽管 mlvm 项目的 continuation 并没有最终实现，但是另一个项目 loom 最终在 Java 19 实现了 JVM 上的 Continuation 和<a href="https://openjdk.org/jeps/425" target="_blank" rel="noopener">虚拟线程</a>。</p><h2 id="尾递归"><a href="#尾递归" class="headerlink" title="尾递归"></a>尾递归</h2><p>Tail calls/tail recursion，也就是尾调用、尾递归，在函数式编程中更为常见。对于尾递归，我们可以借用 Continuation 的概念，定义为“方法最后的调用的 Continuation 是其自身”。</p><p>mlvm 项目对于尾递归的实现是添加一系列新指令，比如 <code>tailcallinvokestatic</code> 这样。当然，这样的实现最终没有落地。</p><p>loom 项目似乎也会提供对于尾调用的支持，虽然目前还没有。</p><h2 id="元组和值类型"><a href="#元组和值类型" class="headerlink" title="元组和值类型"></a>元组和值类型</h2><p>元组（Tuple），可以简单的理解为几个东西的组合，好比一个坐标 <code>Vector3i</code> 就是 <code>(x: int, y: int, z: int)</code> 这样的元组。</p><p>值类型，是相对于引用类型而言的概念，这样的类型在 JVM 中直接保存在栈上，如 <code>int</code>、<code>long</code> 一样。</p><p>在 mlvm 项目中，值类型会被实现为类似 <code>{LBlockPos;III}</code> 这样的类型签名，通过 <code>vaccess getfield</code> 这样的指令读取字段，并且值类型是可变的。在现在看来，这样的设计自然是复杂且不好的。</p><p>值类型自然在 mlvm 项目没有落地。随后的 <a href="https://openjdk.org/projects/valhalla" target="_blank" rel="noopener">valhalla 项目</a>接过了<a href="https://openjdk.org/jeps/401" target="_blank" rel="noopener">值类型</a>的担子，并且同时准备提供泛型特化的特性。</p><h2 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h2><p>mlvm 项目未尝不是某种伊卡洛斯的坠落，它的目标在十年后仍在进行。</p><p>虽然 Graal 项目本身的目标是用 Java 编写 compiler，却实现了 mlvm 设想的 Multi Language VM。</p><p>写这篇文章的想法来自于我的一个项目 <a href="https://github.com/IzzelAliz/Arcturus" target="_blank" rel="noopener">Arcturus</a>，其实现了 <a href="https://openjdk.org/jeps/303" target="_blank" rel="noopener">JEP 303</a> 和 Java 上的尾递归消除。</p><div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="display: inline-block; vertical-align: top; padding-right: 10px;">1.</span><span style="display: inline-block; vertical-align: top;"><a href="https://web.archive.org/web/20160922185657/https://blogs.oracle.com/jrose/entry/interface_injection_in_the_vm" target="_blank" rel="noopener">blogs.oracle.com/jrose/entry/interface_injection_in_the_vm</a></span><a href="#fnref:1" rev="footnote"> ↩</a></li></ol></div></div>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;这可不是 Graal VM …&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;你听说过这些东西吗：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MethodHandle&lt;/li&gt;
&lt;li&gt;invokedynamic&lt;/li&gt;
&lt;li&gt;Continuation&lt;/li&gt;
&lt;li&gt;值类型&lt;/li&gt;
&lt;li&gt;…&lt;/li
      
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>DataFixerUpper 序列化简介</title>
    <link href="https://izzel.io/2022/10/07/dfu-serialisation/"/>
    <id>https://izzel.io/2022/10/07/dfu-serialisation/</id>
    <published>2022-10-07T12:00:00.000Z</published>
    <updated>2025-07-13T12:15:43.523Z</updated>
    
    <content type="html"><![CDATA[<p>Minecraft 在 1.13 提供了模块化数据访问和序列化的类库 DataFixerUpper，本文简单介绍其序列化部分。</p><p>DataFixerUpper 中的核心类是 <code>Codec</code>，结合了 <code>Encoder</code> 和 <code>Decoder</code> 两个类的功能。它们都是无状态的不可变对象，用于变换不可变数据。</p><p>其中，<code>Codec&lt;A&gt;</code> 代表 <code>A</code> 的数据编码，本质上是两个方法，序列化和反序列化方法的组合。</p><pre class="line-numbers language-java"><code class="language-java"><span class="token comment" spellcheck="true">// Decoder，也就是反序列化</span><span class="token keyword">int</span> result <span class="token operator">=</span> <span class="token function">parseAsInt</span><span class="token punctuation">(</span><span class="token function">readAsString</span><span class="token punctuation">(</span><span class="token function">decompressBytes</span><span class="token punctuation">(</span><span class="token comment" spellcheck="true">/* 某种数据 */</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment" spellcheck="true">// Encoder，也就是序列化</span><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span> result <span class="token operator">=</span> <span class="token function">compressBytes</span><span class="token punctuation">(</span><span class="token function">stringToBytes</span><span class="token punctuation">(</span><span class="token function">intToString</span><span class="token punctuation">(</span><span class="token number">42</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p><code>Codec</code> 就是上面这些方法的抽象表示。</p><h2 id="某种数据"><a href="#某种数据" class="headerlink" title="某种数据"></a>某种数据</h2><p>DFU 中使用 <code>DynamicOps&lt;T&gt;</code> 表示数据的序列化格式，在 Minecraft 环境下有 JsonOps 和 NbtOps。DynamicOps 的参数 <code>T</code> 用于表示数据的基本类型，对于 NbtOps 而言为 <code>Tag</code> 类。</p><p>一个 <code>DynamicOps&lt;T&gt;</code> 和一个 <code>T</code> 的实例，或者说 <code>new Dynamic&lt;T&gt;(DynamicOps&lt;T&gt;, T)</code>，组成了反序列化的数据来源，通过 <code>parse</code> 和其他一系列方法进行反序列化：</p><pre class="line-numbers language-java"><code class="language-java">DataResult<span class="token operator">&lt;</span>Integer<span class="token operator">></span> result <span class="token operator">=</span> Codec<span class="token punctuation">.</span>INT<span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span>JsonOps<span class="token punctuation">.</span>INSTANCE<span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token class-name">JsonPrimitive</span><span class="token punctuation">(</span><span class="token number">42</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">assert</span> result<span class="token punctuation">.</span><span class="token function">result</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">==</span> <span class="token number">42</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>反序列化得到的 <code>DataResult&lt;A&gt;</code> 是反序列化的结果或者错误，分别可以用 <code>result()</code> 和 <code>error()</code> 获取。</p><h2 id="数据组合和变换"><a href="#数据组合和变换" class="headerlink" title="数据组合和变换"></a>数据组合和变换</h2><p>DFU 编写 Codec 的方式大多是通过组合和变换已有的 Codec 进行的，绝大部分情况下不需要自行实现 <code>Encoder</code> 和 <code>Decoder</code> 中的方法。</p><table><thead><tr><th>Codec</th><th>Java 类型</th></tr></thead><tbody><tr><td>BOOL</td><td>Boolean</td></tr><tr><td>BYTE</td><td>Byte</td></tr><tr><td>SHORT</td><td>Short</td></tr><tr><td>INT</td><td>Integer</td></tr><tr><td>LONG</td><td>Long</td></tr><tr><td>FLOAT</td><td>Float</td></tr><tr><td>DOUBLE</td><td>Double</td></tr><tr><td>STRING</td><td>String</td></tr><tr><td>BYTE_BUFFER</td><td>ByteBuffer(byte[])</td></tr><tr><td>INT_STREAM</td><td>IntStream(int[])</td></tr><tr><td>LONG_STREAM</td><td>LongStream(long[])</td></tr></tbody></table><p>除去以上的基本类型数据，如果想自定义更复杂的数据类型就需要组合了。</p><h3 id="Record"><a href="#Record" class="headerlink" title="Record"></a>Record</h3><p>Record，和 Java 16 正式加入的 Records 对应，表示一个不可变的 Java 对象，以键值对编码。</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> record <span class="token function">User</span><span class="token punctuation">(</span>String id<span class="token punctuation">,</span> <span class="token keyword">double</span> balance<span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token keyword">static</span> Codec<span class="token operator">&lt;</span>User<span class="token operator">></span> CODEC <span class="token operator">=</span> RecordCodecBuilder<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span>it <span class="token operator">-</span><span class="token operator">></span> it<span class="token punctuation">.</span><span class="token function">group</span><span class="token punctuation">(</span>        Codec<span class="token punctuation">.</span>STRING<span class="token punctuation">.</span><span class="token function">fieldOf</span><span class="token punctuation">(</span><span class="token string">"id"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">forGetter</span><span class="token punctuation">(</span>User<span class="token operator">:</span><span class="token operator">:</span>id<span class="token punctuation">)</span><span class="token punctuation">,</span>        Codec<span class="token punctuation">.</span>DOUBLE<span class="token punctuation">.</span><span class="token function">fieldOf</span><span class="token punctuation">(</span><span class="token string">"balance"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">forGetter</span><span class="token punctuation">(</span>User<span class="token operator">:</span><span class="token operator">:</span>balance<span class="token punctuation">)</span>    <span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">apply</span><span class="token punctuation">(</span>it<span class="token punctuation">,</span> User<span class="token operator">:</span><span class="token operator">:</span><span class="token keyword">new</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>如上，创建一个 Record Codec 通常通过 <code>RecordCodecBuilder.create</code> 进行，通过向 <code>Instance</code> 实例（那个 <code>it</code>）提供数个对应字段的 Codec 和构造方法引用，创建对应 Codec。</p><p>为了方便，通常按照构造方法参数的顺序提供字段 Codec，这样最后调用 <code>apply</code> 传入的构造方法就可以直接写成方法引用的形式。</p><p>除 <code>fieldOf</code> 以外，还可以用 <code>optionalFieldOf</code> 提供一个 <code>Optional&lt;A&gt;</code> 作为参数，或者通过 <code>optionalFieldOf</code> 的第二个参数提供默认值。</p><h3 id="其他的数据类型"><a href="#其他的数据类型" class="headerlink" title="其他的数据类型"></a>其他的数据类型</h3><p>其他的复杂数据类型，其实也就是 <code>List&lt;A&gt;</code> 和 <code>Map&lt;A, B&gt;</code> 了。</p><p>通过调用 <code>listOf</code> 就可以获得 <code>Codec&lt;A&gt;</code> 的列表形式 <code>Codec&lt;List&lt;A&gt;&gt;</code>：</p><pre class="line-numbers language-java"><code class="language-java">Codec<span class="token operator">&lt;</span>List<span class="token operator">&lt;</span>User<span class="token operator">>></span> usersCodec <span class="token operator">=</span> User<span class="token punctuation">.</span>CODEC<span class="token punctuation">.</span><span class="token function">listOf</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>通过调用 <code>Codec.unboundedMap(Codec&lt;A&gt;, Codec&lt;B&gt;)</code> 就可以获得一个基础的 <code>Codec&lt;Map&lt;A, B&gt;&gt;</code>：</p><pre class="line-numbers language-java"><code class="language-java">Codec<span class="token operator">&lt;</span>Map<span class="token operator">&lt;</span>Integer<span class="token punctuation">,</span> User<span class="token operator">>></span> usersCodec <span class="token operator">=</span> Codec<span class="token punctuation">.</span><span class="token function">unboundedMap</span><span class="token punctuation">(</span>Codec<span class="token punctuation">.</span>STRING<span class="token punctuation">,</span> User<span class="token punctuation">.</span>CODEC<span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>DFU 还内置了 <code>Either&lt;A, B&gt;</code> 用于表示 A 或 B，<code>Pair&lt;A, B&gt;</code> 用于表示 A 和 B，可用 Codec 类下同名静态方法创建。</p><p>DFU 中表示“空”的值是 <code>Unit</code>，可以用 <code>Codec.EMPTY.codec()</code> 获得其 Codec。</p><p>Codec 中有一类方法 <code>Codec.dispatch</code> 可以根据不同键名对值提供不同 Codec，Minecraft 中类似 ParticleType 中根据不同注册 ID 提供不同 Codec 即是这样实现的。</p><h3 id="Codec-变换"><a href="#Codec-变换" class="headerlink" title="Codec 变换"></a>Codec 变换</h3><p>另一种创建数据 Codec 的方法是对已有的 Codec 进行变换。</p><p>对于两边互相兼容的类型，可以用 <code>xmap</code> 变换：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> record <span class="token function">Bank</span><span class="token punctuation">(</span>List<span class="token operator">&lt;</span>User<span class="token operator">></span> users<span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token keyword">static</span> Codec<span class="token operator">&lt;</span>Bank<span class="token operator">></span> CODEC <span class="token operator">=</span> User<span class="token punctuation">.</span>CODEC<span class="token punctuation">.</span><span class="token function">listOf</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">xmap</span><span class="token punctuation">(</span>Bank<span class="token operator">:</span><span class="token operator">:</span><span class="token keyword">new</span><span class="token punctuation">,</span> Bank<span class="token operator">:</span><span class="token operator">:</span>users<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>对于可能不兼容的类型，可以用 <code>comapFlatMap</code> 或者 <code>flatXmap</code> 进行变换：</p><pre class="line-numbers language-java"><code class="language-java">Codec<span class="token operator">&lt;</span>UUID<span class="token operator">></span> UUID_CODEC <span class="token operator">=</span> Codec<span class="token punctuation">.</span>STRING<span class="token punctuation">.</span><span class="token function">comapFlatMap</span><span class="token punctuation">(</span>it <span class="token operator">-</span><span class="token operator">></span> <span class="token punctuation">{</span>    <span class="token keyword">try</span> <span class="token punctuation">{</span>        <span class="token keyword">return</span> DataResult<span class="token punctuation">.</span><span class="token function">success</span><span class="token punctuation">(</span>UUID<span class="token punctuation">.</span><span class="token function">fromString</span><span class="token punctuation">(</span>it<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">IllegalArgumentException</span> e<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">return</span> DataResult<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span>it <span class="token operator">+</span> <span class="token string">" is not UUID"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">,</span> UUID<span class="token operator">:</span><span class="token operator">:</span>toString<span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>虽然经过组合和变换后的 <code>Codec&lt;A&gt;</code> 类型是一样的，但是不同的组合和变换方式会导致序列化后的结果不同。例如，上文 Bank 的 JSON 序列化形式可能是：</p><pre class="line-numbers language-json"><code class="language-json"><span class="token punctuation">[</span>  <span class="token punctuation">{</span>    <span class="token property">"user"</span><span class="token operator">:</span> <span class="token string">"zzzz"</span><span class="token punctuation">,</span>    <span class="token property">"balance"</span><span class="token operator">:</span> <span class="token number">42.0</span>  <span class="token punctuation">}</span><span class="token punctuation">]</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>如果按照 <code>RecordCodecBuilder</code> 来创建 Bank Codec 的话，就可能是这样的：</p><pre class="line-numbers language-json"><code class="language-json"><span class="token punctuation">{</span>  <span class="token property">"users"</span><span class="token operator">:</span> <span class="token punctuation">[</span>    <span class="token punctuation">{</span>      <span class="token property">"user"</span><span class="token operator">:</span> <span class="token string">"zzzz"</span><span class="token punctuation">,</span>      <span class="token property">"balance"</span><span class="token operator">:</span> <span class="token number">42.0</span>    <span class="token punctuation">}</span>  <span class="token punctuation">]</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>又比如，Minecraft 提供在 <code>SerializableUUID.CODEC</code> 的 UUID Codec 是用长度为 4 的 int 数组存储，而上文的则是字符串。</p><p>可以参照 <a href="https://docs.minecraftforge.net/en/1.19.x/datastorage/codecs/" target="_blank" rel="noopener">https://docs.minecraftforge.net/en/1.19.x/datastorage/codecs/</a> 更多内容进行代码编写。</p><p>Minecraft 给一些常见的类都提供了 Codec，位于对应类的 <code>CODEC</code> 字段或者 <code>ExtraCodecs</code> 类中。</p><p>我自己也写了一个<a href="https://github.com/IzzelAliz/extra-codecs" target="_blank" rel="noopener">小工具</a>，可以生成基于基础 Java 类型的 Record 类的 Codec。例如，上文 Bank 可以直接调用 <code>TypeCodec.of(Bank.class)</code> 获得对应 Codec。</p><h2 id="什么是-App"><a href="#什么是-App" class="headerlink" title="什么是 App"></a>什么是 App</h2><p>在 DFU 中随处可见 <code>App&lt;F, A&gt;</code> 的形式，但是这个类里面没有任何方法，这是什么呢？</p><p>在 Java 中，<code>Stream&lt;A&gt;</code> 和 <code>Optional&lt;A&gt;</code> 都有 <code>flatMap</code> 方法，类似：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Optional</span><span class="token operator">&lt;</span>A<span class="token operator">></span> <span class="token punctuation">{</span>    <span class="token keyword">public</span> <span class="token operator">&lt;</span>B<span class="token operator">></span> Optional<span class="token operator">&lt;</span>B<span class="token operator">></span> <span class="token function">flatMap</span><span class="token punctuation">(</span>Function<span class="token operator">&lt;</span>A<span class="token punctuation">,</span> Optional<span class="token operator">&lt;</span>B<span class="token operator">>></span> f<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Stream</span><span class="token operator">&lt;</span>A<span class="token operator">></span> <span class="token punctuation">{</span>    <span class="token keyword">public</span> <span class="token operator">&lt;</span>B<span class="token operator">></span> Stream<span class="token operator">&lt;</span>B<span class="token operator">></span> <span class="token function">flatMap</span><span class="token punctuation">(</span>Function<span class="token operator">&lt;</span>A<span class="token punctuation">,</span> Stream<span class="token operator">&lt;</span>B<span class="token operator">>></span> f<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>仅有 <code>flatMap</code> 方法就可以轻松实现一个功能 <code>flatten</code>，可以将类似 <code>Optional&lt;Optional&lt;A&gt;&gt;</code> 和 <code>Stream&lt;Stream&lt;A&gt;&gt;</code> 拉平为 <code>Optional&lt;A&gt;</code> 和 <code>Stream&lt;A&gt;</code>，比如：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">static</span> <span class="token operator">&lt;</span>A<span class="token operator">></span> Optional<span class="token operator">&lt;</span>A<span class="token operator">></span> <span class="token function">flatten</span><span class="token punctuation">(</span>Optional<span class="token operator">&lt;</span>Optional<span class="token operator">&lt;</span>A<span class="token operator">>></span> optional<span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token keyword">return</span> optional<span class="token punctuation">.</span><span class="token function">flatMap</span><span class="token punctuation">(</span>x <span class="token operator">-</span><span class="token operator">></span> x<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">static</span> <span class="token operator">&lt;</span>A<span class="token operator">></span> Stream<span class="token operator">&lt;</span>A<span class="token operator">></span> <span class="token function">flatten</span><span class="token punctuation">(</span>Stream<span class="token operator">&lt;</span>Stream<span class="token operator">&lt;</span>A<span class="token operator">>></span> stream<span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token keyword">return</span> stream<span class="token punctuation">.</span><span class="token function">flatMap</span><span class="token punctuation">(</span>x <span class="token operator">-</span><span class="token operator">></span> x<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>自然而然，一样的代码不应该写这么多遍，所以我们会希望有一种长成这样的代码：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">interface</span> <span class="token class-name">FlatMap</span><span class="token operator">&lt;</span>A<span class="token operator">></span> <span class="token punctuation">{</span>    <span class="token operator">&lt;</span>B<span class="token operator">></span> FlatMap<span class="token operator">&lt;</span>B<span class="token operator">></span> <span class="token function">flatMap</span><span class="token punctuation">(</span>Function<span class="token operator">&lt;</span>A<span class="token punctuation">,</span> FlatMap<span class="token operator">&lt;</span>B<span class="token operator">>></span> f<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">static</span> <span class="token operator">&lt;</span>A<span class="token operator">></span> FlatMap<span class="token operator">&lt;</span>A<span class="token operator">></span> <span class="token function">flatten</span><span class="token punctuation">(</span>FlatMap<span class="token operator">&lt;</span><span class="token operator">?</span> <span class="token keyword">extends</span> <span class="token class-name">FlatMap</span><span class="token operator">&lt;</span>A<span class="token operator">>></span> flatMap<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">return</span> flatMap<span class="token punctuation">.</span><span class="token function">flatMap</span><span class="token punctuation">(</span>x <span class="token operator">-</span><span class="token operator">></span> x<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token keyword">class</span> <span class="token class-name">Optional</span><span class="token operator">&lt;</span>A<span class="token operator">></span> <span class="token keyword">implements</span> <span class="token class-name">FlatMap</span><span class="token operator">&lt;</span>A<span class="token operator">></span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">/* ... */</span> <span class="token punctuation">}</span><span class="token keyword">class</span> <span class="token class-name">Stream</span><span class="token operator">&lt;</span>A<span class="token operator">></span> <span class="token keyword">implements</span> <span class="token class-name">FlatMap</span><span class="token operator">&lt;</span>A<span class="token operator">></span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">/* ... */</span> <span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>但是问题出现了，经过 <code>flatten</code> 后类型丢失了，我们实际上想要的是这种方法：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">static</span> <span class="token operator">&lt;</span>F <span class="token keyword">extends</span> <span class="token class-name">FlatMap</span><span class="token operator">&lt;</span><span class="token operator">?</span><span class="token operator">></span><span class="token punctuation">,</span> A<span class="token operator">></span> F<span class="token operator">&lt;</span>A<span class="token operator">></span> <span class="token function">flatten</span><span class="token punctuation">(</span>F<span class="token operator">&lt;</span>F<span class="token operator">&lt;</span>A<span class="token operator">>></span> flatMap<span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token keyword">return</span> flatMap<span class="token punctuation">.</span><span class="token function">flatMap</span><span class="token punctuation">(</span>x <span class="token operator">-</span><span class="token operator">></span> x<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>但是在 Java 中不能这样做（这当然不是什么符合标准的 Java 语法），所以我们退而求其次，把这个 F 加在 <code>FlatMap</code> 上：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">interface</span> <span class="token class-name">FlatMap</span><span class="token operator">&lt;</span>F<span class="token punctuation">,</span> A<span class="token operator">></span> <span class="token punctuation">{</span>    <span class="token operator">&lt;</span>B<span class="token operator">></span> FlatMap<span class="token operator">&lt;</span>F<span class="token punctuation">,</span> B<span class="token operator">></span> <span class="token function">flatMap</span><span class="token punctuation">(</span>Function<span class="token operator">&lt;</span>A<span class="token punctuation">,</span> FlatMap<span class="token operator">&lt;</span>F<span class="token punctuation">,</span> B<span class="token operator">>></span> f<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">static</span> <span class="token operator">&lt;</span>F<span class="token punctuation">,</span> A<span class="token operator">></span> FlatMap<span class="token operator">&lt;</span>F<span class="token punctuation">,</span> A<span class="token operator">></span> <span class="token function">flatten</span><span class="token punctuation">(</span>FlatMap<span class="token operator">&lt;</span>F<span class="token punctuation">,</span> <span class="token operator">?</span> <span class="token keyword">extends</span> <span class="token class-name">FlatMap</span><span class="token operator">&lt;</span>F<span class="token punctuation">,</span> A<span class="token operator">>></span> flatMap<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">return</span> flatMap<span class="token punctuation">.</span><span class="token function">flatMap</span><span class="token punctuation">(</span>x <span class="token operator">-</span><span class="token operator">></span> x<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token keyword">class</span> <span class="token class-name">Optional</span><span class="token operator">&lt;</span>A<span class="token operator">></span> <span class="token keyword">implements</span> <span class="token class-name">FlatMap</span><span class="token operator">&lt;</span>Optional<span class="token operator">&lt;</span><span class="token operator">?</span><span class="token operator">></span><span class="token punctuation">,</span> A<span class="token operator">></span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">/* ... */</span> <span class="token punctuation">}</span><span class="token keyword">class</span> <span class="token class-name">Stream</span><span class="token operator">&lt;</span>A<span class="token operator">></span> <span class="token keyword">implements</span> <span class="token class-name">FlatMap</span><span class="token operator">&lt;</span>Stream<span class="token operator">&lt;</span><span class="token operator">?</span><span class="token operator">></span><span class="token punctuation">,</span> A<span class="token operator">></span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">/* ... */</span> <span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>这里 <code>implements FlatMap&lt;Optional&lt;?&gt;, ...&gt;</code> 使用 wildcard type 而不是 <code>Optional&lt;A&gt;</code> 是因为我们希望传入 <code>flatten</code> 的参数 <code>F&lt;F&lt;A&gt;&gt;</code> 中两个 <code>F</code> 的类型是一致的，而不是根据 <code>A</code> 的不同变化的（仔细想想这里的 F 到底表示什么）。</p><p>所以我们就能写出这样的代码：</p><pre class="line-numbers language-java"><code class="language-java">Optional<span class="token operator">&lt;</span>Optional<span class="token operator">&lt;</span>String<span class="token operator">>></span> optional <span class="token operator">=</span> <span class="token comment" spellcheck="true">/* ... */</span><span class="token punctuation">;</span>FlatMap<span class="token operator">&lt;</span>Optional<span class="token operator">&lt;</span><span class="token operator">?</span><span class="token operator">></span><span class="token punctuation">,</span> String<span class="token operator">></span> flatten <span class="token operator">=</span> FlatMap<span class="token punctuation">.</span><span class="token function">flatten</span><span class="token punctuation">(</span>optional<span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>现在 Optional 的类型保留了，虽然返回的仍然是 FlatMap，但是只需要加上一个简单的工具方法转换一次就可以了：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">static</span> <span class="token operator">&lt;</span>A<span class="token operator">></span> Optional<span class="token operator">&lt;</span>A<span class="token operator">></span> <span class="token function">unbox</span><span class="token punctuation">(</span>FlatMap<span class="token operator">&lt;</span>Optional<span class="token operator">&lt;</span><span class="token operator">?</span><span class="token operator">></span><span class="token punctuation">,</span> A<span class="token operator">></span> f<span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token keyword">return</span> <span class="token punctuation">(</span>Optional<span class="token operator">&lt;</span>A<span class="token operator">></span><span class="token punctuation">)</span> f<span class="token punctuation">;</span><span class="token punctuation">}</span>Optional<span class="token operator">&lt;</span>String<span class="token operator">></span> flatten <span class="token operator">=</span> <span class="token function">unbox</span><span class="token punctuation">(</span>FlatMap<span class="token punctuation">.</span><span class="token function">flatten</span><span class="token punctuation">(</span>optional<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>最后，因为 <code>FlatMap&lt;F, A&gt;</code> 中的 <code>F</code> 实际上仅在编译期存在，作为类型约束的代码提示（不是吗？），所以我们可以定义一个空的类 <code>Optional.Mu</code> 来代表 <code>Optional</code>：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">class</span> <span class="token class-name">Optional</span><span class="token operator">&lt;</span>A<span class="token operator">></span> <span class="token keyword">implements</span> <span class="token class-name">FlatMap</span><span class="token operator">&lt;</span>Optional<span class="token punctuation">.</span>Mu<span class="token punctuation">,</span> A<span class="token operator">></span> <span class="token punctuation">{</span>    <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">Mu</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>    <span class="token keyword">static</span> <span class="token operator">&lt;</span>A<span class="token operator">></span> Optional<span class="token operator">&lt;</span>A<span class="token operator">></span> <span class="token function">unbox</span><span class="token punctuation">(</span>FlatMap<span class="token operator">&lt;</span>Optional<span class="token punctuation">.</span>Mu<span class="token punctuation">,</span> A<span class="token operator">></span> f<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">return</span> <span class="token punctuation">(</span>Optional<span class="token operator">&lt;</span>A<span class="token operator">></span><span class="token punctuation">)</span> f<span class="token punctuation">;</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>像这样的代码，我们可以在 DataFixerUpper 里面找到很多。</p><p>再回头看到 <code>FlatMap</code> 这个接口，它表示了两个意思：</p><ul><li>一个叫 <code>F&lt;A&gt;</code> 的类型</li><li>实现了叫 <code>flatMap</code> 的方法</li></ul><p>自然，我们可以单独有一个概念只表示 <code>F&lt;A&gt;</code>，比如 —— <code>App&lt;F, A&gt;</code>。</p><p>最终揭秘：<code>App&lt;F, A&gt;</code> 用于表示高阶类型（higher kinded type）<code>F&lt;A&gt;</code>，这个接口是因为 Java 本身不支持高阶类型而出现的，作为一种标记使用。</p><p>在 DFU 中，它实际的签名是 <code>App&lt;F extends K1, A&gt;</code>，其中：</p><ul><li><code>K1</code> 用于代表类似 <code>F&lt;_&gt;</code> 一样的类型，或者说一个单参数的类型构造器（type constructor），比如说 <code>List&lt;_&gt;</code>；</li><li><code>App&lt;F, A&gt;</code> 代表用类型 <code>A</code> 应用在 <code>F</code> 这个类型构造器上，比如说 <code>App&lt;List, String&gt;</code> 代表 <code>List&lt;String&gt;</code>；</li></ul><p>在实际使用中，通常是让类似 FlatMap 的类去继承 App 这样的类，类似：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">interface</span> <span class="token class-name">FlatMap</span><span class="token operator">&lt;</span>F <span class="token keyword">extends</span> <span class="token class-name">FlatMap<span class="token punctuation">.</span>Mu</span><span class="token punctuation">,</span> A<span class="token operator">></span> <span class="token keyword">extends</span> <span class="token class-name">App</span><span class="token operator">&lt;</span>F<span class="token punctuation">,</span> A<span class="token operator">></span> <span class="token punctuation">{</span>    <span class="token keyword">interface</span> <span class="token class-name">Mu</span> <span class="token keyword">extends</span> <span class="token class-name">K1</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>    <span class="token operator">&lt;</span>B<span class="token operator">></span> FlatMap<span class="token operator">&lt;</span>F<span class="token punctuation">,</span> B<span class="token operator">></span> <span class="token function">flatMap</span><span class="token punctuation">(</span>Function<span class="token operator">&lt;</span>A<span class="token punctuation">,</span> FlatMap<span class="token operator">&lt;</span>F<span class="token punctuation">,</span> B<span class="token operator">>></span> f<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p>同样的，DFU 也有 <code>App2&lt;F extends K2, A, B&gt;</code> 这样的类型，代表了有两个参数的高阶类型 <code>F&lt;A, B&gt;</code>。</p><p>结束之前，我们可以通过 Scala 这个支持高阶类型的语言来看看，为什么 <code>App</code> 这个接口是因为 Java 不支持高阶类型而存在的（代码来自 <a href="https://github.com/typelevel/cats/blob/main/core/src/main/scala/cats/FlatMap.scala" target="_blank" rel="noopener">cats</a>）：</p><pre class="line-numbers language-scala"><code class="language-scala"><span class="token keyword">trait</span> FlatMap<span class="token punctuation">[</span>F<span class="token punctuation">[</span>_<span class="token punctuation">]</span><span class="token punctuation">]</span> <span class="token punctuation">{</span>  <span class="token keyword">def</span> flatMap<span class="token punctuation">[</span>A<span class="token punctuation">,</span> B<span class="token punctuation">]</span><span class="token punctuation">(</span>fa<span class="token operator">:</span> F<span class="token punctuation">[</span>A<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">(</span>f<span class="token operator">:</span> A <span class="token keyword">=></span> F<span class="token punctuation">[</span>B<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token operator">:</span> F<span class="token punctuation">[</span>B<span class="token punctuation">]</span>  <span class="token keyword">def</span> flatten<span class="token punctuation">[</span>A<span class="token punctuation">]</span><span class="token punctuation">(</span>ffa<span class="token operator">:</span> F<span class="token punctuation">[</span>F<span class="token punctuation">[</span>A<span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token operator">:</span> F<span class="token punctuation">[</span>A<span class="token punctuation">]</span> <span class="token operator">=</span> flatMap<span class="token punctuation">(</span>ffa<span class="token punctuation">)</span><span class="token punctuation">(</span>x <span class="token keyword">=></span> x<span class="token punctuation">)</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>本文没有介绍 <code>RecordCodecBuilder.Instance</code> 为何物（type class），虽然已经很接近了，读者可以阅读 cats 的 <a href="https://typelevel.org/cats/typeclasses/applicative.html" target="_blank" rel="noopener">Applicative</a> 文档进行了解。</p><p>本文也没有介绍 DFU 的 optics 包和其他几个包的内容，因为序列化部分并不涉及这部分代码，读者可以阅读 <a href="https://arxiv.org/ftp/arxiv/papers/1703/1703.10857.pdf" target="_blank" rel="noopener">Pickering, M., Gibbons, J., &amp; Wu, N. (2017). Profunctor Optics: Modular Data Accessors</a> 进行了解。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;Minecraft 在 1.13 提供了模块化数据访问和序列化的类库 DataFixerUpper，本文简单介绍其序列化部分。&lt;/p&gt;
&lt;p&gt;DataFixerUpper 中的核心类是 &lt;code&gt;Codec&lt;/code&gt;，结合了 &lt;code&gt;Encoder&lt;/code&gt; 
      
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>聊聊 Forge 工具链</title>
    <link href="https://izzel.io/2022/09/07/forge-toolchain-teardown/"/>
    <id>https://izzel.io/2022/09/07/forge-toolchain-teardown/</id>
    <published>2022-09-07T19:19:34.000Z</published>
    <updated>2025-07-13T12:15:43.523Z</updated>
    
    <content type="html"><![CDATA[<p><a href="https://github.com/MinecraftForge/MinecraftForge" target="_blank" rel="noopener">MinecraftForge</a> 由十数个项目组合而成，本文对此做一个简单的介绍。</p><p>这篇文章以 Minecraft 1.19.2，Forge 43.1.0 为例进行分析。</p><p>你可以在<a href="https://files.minecraftforge.net/project_index.html" target="_blank" rel="noopener">这里</a>清晰明了的看到 Forge 的所有项目。</p><h2 id="Forge-本体"><a href="#Forge-本体" class="headerlink" title="Forge 本体"></a>Forge 本体</h2><p>Forge 可以简单地定义为一个 Mod 加载器和一套帮助 Mod 兼容的 API。后者，也就是这套 API 并不是本文关注的重点，虽然不可避免地会讲到一些。</p><p>回想一下你是如何安装 Mod 的，就可以很容易地总结出 Mod 加载器是干什么的：把 <code>mods</code> 文件夹里的 JAR 文件加载到游戏里。</p><h2 id="编译期"><a href="#编译期" class="headerlink" title="编译期"></a>编译期</h2><p>Forge 的开发工作依赖于一系列项目。</p><h3 id="ForgeFlower"><a href="#ForgeFlower" class="headerlink" title="ForgeFlower"></a>ForgeFlower</h3><p>这是一个 Java 反编译器。</p><p>Forge 项目对 FernFlower 的修改，修复了一些怪异和错误的编译结果，并且增加了多线程反编译的支持，因此比其他（比如 Spigot）的反编译快了许多。</p><h3 id="MCPConfig"><a href="#MCPConfig" class="headerlink" title="MCPConfig"></a>MCPConfig</h3><p>MCP 项目唯一的遗产。MCPConfig 用于提供包括：</p><ul><li>混淆名称到 srg 名的映射，用 TSRG 格式的文件保存</li><li>反编译后的代码文件的修复，用 patch 文件保存</li></ul><p>在内的功能。</p><h3 id="MCP-映射表格式"><a href="#MCP-映射表格式" class="headerlink" title="MCP 映射表格式"></a>MCP 映射表格式</h3><p>MCP 项目同时带有一个映射表，是社区维护的，当然现在已经停止维护了。</p><p>这个映射表导出之后，是一个 zip 压缩包，里面可以含有包含方法、字段、参数的 csv 文件。</p><p>例如，ForgeGradle 生成的 1.19.2 官方映射表格式为：</p><pre><code>mapping-1.19.2-official.zip  | fields.csv  \ methods.csv</code></pre><p>csv 文件格式类似：</p><pre><code>searge,name,side,descm_100003_,lambda$init$1,0,m_100013_,updateList,0,</code></pre><p>含有 searge 名，翻译名，仅客户端/服务端的信息和 Javadoc 注释。</p><h3 id="MappingVerifier"><a href="#MappingVerifier" class="headerlink" title="MappingVerifier"></a>MappingVerifier</h3><p>检查一个映射表会不会破坏继承关系。</p><p>这里的破坏是什么呢？很简单，如果调用一个方法，被 remap 之后最终调用的实现不一样，继承关系就被破坏了。</p><p>考虑以下情况：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">abstract</span> <span class="token keyword">class</span> <span class="token class-name">A</span> <span class="token punctuation">{</span>    <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">foo</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>        System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"A"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token keyword">interface</span> <span class="token class-name">B</span> <span class="token punctuation">{</span>    <span class="token keyword">void</span> <span class="token function">foo</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">class</span> <span class="token class-name">C</span> <span class="token keyword">extends</span> <span class="token class-name">A</span> <span class="token keyword">implements</span> <span class="token class-name">B</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>此时接口 B 的 <code>foo()</code> 通过子类 C 的父类 A 间接实现了；而如果一个映射错误的把 <code>A.foo()</code> 和 <code>B.foo()</code> 分配了两个不一样的名字，那么调用 <code>B.foo()</code> 就不会正确的调用 A 中的实现，继承被破坏了。</p><p>当然这是最为复杂的一种情形，也会有把 <code>String foo()</code> 映射成 <code>String toString()</code> 这样错误覆盖了继承等等。</p><h3 id="SpecialSource"><a href="#SpecialSource" class="headerlink" title="SpecialSource"></a>SpecialSource</h3><p><a href="https://github.com/md-5/SpecialSource" target="_blank" rel="noopener">https://github.com/md-5/SpecialSource</a></p><p>一个根据某个映射表对 JAR 进行 remap 的工具。</p><p>基本上用法就是</p><pre><code>java -jar SpecialSource.jar -i in.jar -o out.jar -m mappings.srg</code></pre><p>这个工具是 md-5 写给 Spigot 项目的，Forge 一直用到了大概 2021 年。</p><p>同类型工具里支持格式大概是最多的，也很好用。</p><h3 id="ForgeAutoRenamingTool"><a href="#ForgeAutoRenamingTool" class="headerlink" title="ForgeAutoRenamingTool"></a>ForgeAutoRenamingTool</h3><p>平替 SpecialSource 的工具。这个名字的缩写大概也是 LexManos 的恶趣味。large lols</p><p>能平替 SpecialSource 的工具还有 CadixDev 的 <a href="https://github.com/LexManos/Vignette" target="_blank" rel="noopener">Vignette</a>，Forge 似乎有过用这个的意愿，后来又换掉了。</p><h3 id="SrgUtils"><a href="#SrgUtils" class="headerlink" title="SrgUtils"></a>SrgUtils</h3><p>对映射表这个概念做抽象形成的类库，也有 remap 的功能。</p><p>其实不是那么好用，比较好用的是 CadixDev 的 <a href="https://github.com/CadixDev/Lorenz" target="_blank" rel="noopener">Lorenz</a>。</p><h3 id="Srg2Source"><a href="#Srg2Source" class="headerlink" title="Srg2Source"></a>Srg2Source</h3><p>前面的 SpecialSource 等都是对已经编译过的 .class 文件进行 remap，这个项目是对 Java 源文件进行 remap。</p><p>项目是基于 Eclipse JDT 项目开发的，基本可以算作一个修改过的编译器。</p><p>比较不好用，因为要生成一个 RangeMap 用于存储需要 remap 的符号的位置。比较好用的还是 CadixDev 那边的 Mercury，不需要生成中间文件。</p><h3 id="artifactural"><a href="#artifactural" class="headerlink" title="artifactural"></a>artifactural</h3><p>一个支持 Artifact Transform 的 Gradle 插件。</p><p>什么意思呢？在 Gradle 5.5 发布之前，Gradle 并不支持对依赖进行例如反混淆的修改。这个插件提供了动态生成和修改一个依赖的功能，Forge 用来生成 MCPConfig 处理过的 MC Jar 和经过反混淆的依赖。</p><p>内部的实现方式应该是实现了一个 Repository 之类的接口。</p><p>Forge 似乎有过迁移到 Gradle Artifact Transform 这套系统上，但是没有后话了。</p><h3 id="AccessTransformers"><a href="#AccessTransformers" class="headerlink" title="AccessTransformers"></a>AccessTransformers</h3><p>编译器（和运行时）修改字段和方法访问级别的工具。</p><p>唯一不好的一点就是修改了 AT 文件之后需要重新反编译。</p><h3 id="JarCompatibilityChecker"><a href="#JarCompatibilityChecker" class="headerlink" title="JarCompatibilityChecker"></a>JarCompatibilityChecker</h3><p>检查两个 JAR 文件之间的字节码兼容，也就是不会有 LinkageError 和 IncompatibleClassChangeError 之类的错误，现在用来检验 AT 有效性。</p><p>可能是 Forge 为引入更多编译期字节码变换（接口注入？）做的准备。</p><h3 id="ForgeGradle"><a href="#ForgeGradle" class="headerlink" title="ForgeGradle"></a>ForgeGradle</h3><p>Forge 的 Gradle 插件。</p><p>使用了上面的项目，帮助开发者引入反编译反混淆后的 Minecraft 依赖。</p><h2 id="运行时"><a href="#运行时" class="headerlink" title="运行时"></a>运行时</h2><h3 id="bootstraplauncher-BSL"><a href="#bootstraplauncher-BSL" class="headerlink" title="bootstraplauncher (BSL)"></a>bootstraplauncher (BSL)</h3><p>引导 modlauncher 的前置项目，基本上就是把 modlauncher 需要的依赖全部加载成 Java 模块，然后调用 modlauncher 主类。</p><h3 id="securejarhandler-SJH"><a href="#securejarhandler-SJH" class="headerlink" title="securejarhandler (SJH)"></a>securejarhandler (SJH)</h3><p>提供了多个功能的类库。</p><p>提供 JAR 签名验证支持。Java 自带的 zipfs 不支持 JAR 签名验证（毕竟不是 jarfs），但是 cpw 等人认为需要支持这个功能。最终的实现是反射调用 JarFile 的内部签名验证实现。</p><p>为 JPMS 提供一个专用的类加载器，主要的类是 ModuleClassLoader。</p><p>提供一个 UnionFileSystem，可以把多个文件系统组合在一起，应该就是模仿 Docker 使用的文件系统。</p><h3 id="modlauncher-ML"><a href="#modlauncher-ML" class="headerlink" title="modlauncher (ML)"></a>modlauncher (ML)</h3><p>模组加载器，提供在类加载期进行字节码增强功能，同时也负责 Minecraft 的引导。理论上来说，ML 支持运行任何 Java 程序。</p><p>modlauncher 下一共有四个 ModuleLayer，分别是：</p><ul><li>BOOT，也就是 BSL 里的那个模块。</li><li>PLUGIN，供一个 <code>ILaunchPluginService</code> 接口作为 Java SPI 使用，暴露给 Forge 自身。AccessTransformer 和 Mixin 之类的东西都是以这个实现功能。</li><li>SERVICE，主要供 <code>ITransformationService</code>、<code>IModLocator</code> 和 <code>IDependencyLocator</code> 接口使用，暴露给 Modder，可以使用这个 Layer 进行字节码增强操作。</li><li>GAME，运行 Minecraft 和模组的 Layer，唯一一个支持字节码被增强的。</li></ul><p>其中 PLUGIN 和 SERVICE 都只能访问自身和 BOOT 中的类，GAME 可以访问所有的类，以此进行类加载隔离。</p><p>Launch target 这个概念也是在 ML 里提供的，是一个 <code>ILaunchHandlerService</code> SPI，在 Forge 中用来启动不同的环境，比如客户端、服务端、GameTest、Data Generator 的开发环境和生产环境。</p><p>ML 是在 Minecraft 1.13 Forge 工具链大更新时期诞生的项目，解决了 LaunchWrapper 不支持 Java 9 以上版本的问题。</p><p>2020 年（Minecraft 1.17）以前，ML 并没有集成 JPMS 支持，也没有对 LaunchPlugin 和 Transformer 进行隔离。后来的 SJH 项目实际上是从 ML 拆分出去的。</p><h3 id="JarJar"><a href="#JarJar" class="headerlink" title="JarJar"></a>JarJar</h3><p>实现了 Jar in Jar 文件系统，用于 Forge Mod 的依赖加载。</p><p>实际上实现没有特别复杂，是基于路径进行转换再代理到 zipfs。</p><h3 id="coremods"><a href="#coremods" class="headerlink" title="coremods"></a>coremods</h3><p>实现了一套基于 Nashorn JS 的字节码修改系统。</p><p>这套系统解决了 1.12 时代不小心在 coremod 类里引用 Minecraft 类的问题，因为对类加载做了严格隔离。使用这套系统的 JS 通过类加载器限制只能引用指定的几个 ASM 类和指定的几个基础 Java 包。</p><p>就是太难用了，也没有补全。</p><p>在 ModLauncher 升级到 JPMS 和几个 ModuleLayer 的隔离机制后，这套系统的初衷也不成立了。</p><h3 id="eventbus"><a href="#eventbus" class="headerlink" title="eventbus"></a>eventbus</h3><p>实现了 Forge 的事件总线。</p><p>具体实现是基于 ASM 生成调用类，性能比较好。</p><p>这套系统的实现非常依赖 context classloader，如果没有设置就会有一些莫名其妙的 NoClassDefFoundError。这套系统也只能在 GAME Layer 的那个类加载器环境里使用，如果模组创建了一个子类加载器，也是不能用的。</p><p>在我看来完全可以迁移到 MethodHandles.Lookup 的 defineHiddenClass 去。</p><h2 id="拓展阅读"><a href="#拓展阅读" class="headerlink" title="拓展阅读"></a>拓展阅读</h2><p>我在<a href="/2021/11/13/how-to-minecraft-server/">另一篇文章</a>里写了一些关于服务端的实现原理。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;&lt;a href=&quot;https://github.com/MinecraftForge/MinecraftForge&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MinecraftForge&lt;/a&gt; 由十数个项目组合而成，本文对此做一个简单的介绍。&lt;/p&gt;
      
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>[译] 你的函数什么色？</title>
    <link href="https://izzel.io/2022/09/04/what-color-is-your-function/"/>
    <id>https://izzel.io/2022/09/04/what-color-is-your-function/</id>
    <published>2022-09-04T21:19:26.000Z</published>
    <updated>2025-07-13T12:15:43.526Z</updated>
    
    <content type="html"><![CDATA[<p>原文 <a href="https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/" target="_blank" rel="noopener">https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/</a>，Robert Nystrom 版权所有。</p><hr><p>不知道你如何，但是每天早上起来看编程语言论战使人精神一振。看一些人荒废整天探讨一些 <a href="http://www.paulgraham.com/avg.html" target="_blank" rel="noopener">blub</a> 语言总是那么挑动神经。</p><blockquote><p>blub 语言是 PaulGraham 提出的一种假设语言，他假设不同语言之间分出优劣，而 blub 语言是位于中间的语言。<br>会这门语言的程序员看向更低级的语言时，会认为其缺少重要的功能；看向更高级的语言时，却不能意识到为什么其更高级，而是认为其只是多了一些不必要的花哨功能。 —— 译注</p></blockquote><p>（不过，大家都只会用那些趁手的语言，为我们这样的能工巧匠量身定做的精工利器。）</p><p>不过作为那篇文章的作者还是很危险的，因为我整的那个语言可能就是你得意用的。可能下一秒，这篇博客就会挤满明火执仗的暴徒。</p><p>为了自保，也是为了保护你脆弱的自尊心，这里我就生造一个新语言，作为我面对暴徒的替身。</p><p>保持耐心，看到最后，别错过精彩。</p><h2 id="新语言"><a href="#新语言" class="headerlink" title="新语言"></a>新语言</h2><p>为一篇文章学一个新语言有些骇人听闻，所以我们用耳熟能详的举例。就假设这个东西比较像 JS，花括号，分号结尾，有 <code>if</code> 和 <code>while</code> 什么的，就像编程里的通用语。</p><p>选择 JS 并不是因为这篇文章要讲，而是因为大部分读者都能懂这些东西是什么：</p><pre class="line-numbers language-javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">thisIsAFunction</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>  <span class="token keyword">return</span> <span class="token string">"It's awesome"</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>这个语言得“现代”一点，支持函数作为一等公民，你就可以这样：</p><pre class="line-numbers language-javascript"><code class="language-javascript"><span class="token comment" spellcheck="true">// 返回一个含有满足集合中所有符合条件元素的列表</span><span class="token keyword">function</span> <span class="token function">filter</span><span class="token punctuation">(</span>collection<span class="token punctuation">,</span> predicate<span class="token punctuation">)</span> <span class="token punctuation">{</span>  <span class="token keyword">var</span> result <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>  <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">var</span> i <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> i <span class="token operator">&lt;</span> collection<span class="token punctuation">.</span>length<span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">predicate</span><span class="token punctuation">(</span>collection<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span> result<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>collection<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token punctuation">}</span>  <span class="token keyword">return</span> result<span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>这就是“高阶”函数，顾名思义，很上流也很好用。你会先在集合上面用它，很快你逐渐理解了一切，就开始到处用它。</p><p>比如测试框架：</p><pre class="line-numbers language-javascript"><code class="language-javascript"><span class="token function">describe</span><span class="token punctuation">(</span><span class="token string">"An apple"</span><span class="token punctuation">,</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>  <span class="token function">it</span><span class="token punctuation">(</span><span class="token string">"ain't no orange"</span><span class="token punctuation">,</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token function">expect</span><span class="token punctuation">(</span><span class="token string">"Apple"</span><span class="token punctuation">)</span><span class="token punctuation">.</span>not<span class="token punctuation">.</span><span class="token function">toBe</span><span class="token punctuation">(</span><span class="token string">"Orange"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>或者解析一些数据：</p><pre class="line-numbers language-javascript"><code class="language-javascript">tokens<span class="token punctuation">.</span><span class="token function">match</span><span class="token punctuation">(</span>Token<span class="token punctuation">.</span>LEFT_BRACKET<span class="token punctuation">,</span> <span class="token keyword">function</span><span class="token punctuation">(</span>token<span class="token punctuation">)</span> <span class="token punctuation">{</span>  <span class="token comment" spellcheck="true">// Parse a list literal...</span>  tokens<span class="token punctuation">.</span><span class="token function">consume</span><span class="token punctuation">(</span>Token<span class="token punctuation">.</span>RIGHT_BRACKET<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p>然后你就有了许多美妙的可重用轮子和应用，传递函数，调用函数，返回函数。函数盛宴！</p><h2 id="你的函数什么色？"><a href="#你的函数什么色？" class="headerlink" title="你的函数什么色？"></a>你的函数什么色？</h2><p>且慢。我们的新语言有些离奇了，因为它有一个妙妙特性：</p><p><strong>1. 每个函数都有色</strong></p><p>每个函数 —— 匿名的或者有名字的那些 —— 要么是红的或者蓝的。函数的关键字不是一个简单的 <code>function</code>，而是两个：</p><blockquote><p>azure 是天蓝色，carnelian 是橙红色的玛瑙 —— 译注</p></blockquote><pre class="line-numbers language-javascript"><code class="language-javascript">blue_function <span class="token function">doSomethingAzure</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>  <span class="token comment" spellcheck="true">// 这是个蓝色的函数</span><span class="token punctuation">}</span>red_function <span class="token function">doSomethingCarnelian</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>  <span class="token comment" spellcheck="true">// 而这是个红的</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>这个语言不可以颜色平平，写一个函数就必须得挑个色儿。这是规矩，而且还有别的规矩你得遵守：</p><p><strong>2. 函数的色决定如何调用它</strong></p><p>想象“蓝色调用”和“红色调用”，就比如：</p><pre class="line-numbers language-javascript"><code class="language-javascript"><span class="token function">doSomethingAzure</span><span class="token punctuation">(</span><span class="token punctuation">)</span>blue<span class="token punctuation">;</span><span class="token function">doSomethingCarnelian</span><span class="token punctuation">(</span><span class="token punctuation">)</span>red<span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>调用函数的时候，你就要按照颜色来调。如果错了 —— 红色的函数括号后面跟了 <code>blue</code>，或者反过来，大事就会不妙：光脚走来走去就会踩上乐高，完整的乐高也会神秘消失一两个。</p><p>挺讨厌的吧？还有一个：</p><p><strong>3. 你只能在红色函数里调用红色函数</strong></p><p>红色函数里面<strong>可以</strong>调用蓝色的，就像这样：</p><pre class="line-numbers language-javascript"><code class="language-javascript">red_function <span class="token function">doSomethingCarnelian</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>  <span class="token function">doSomethingAzure</span><span class="token punctuation">(</span><span class="token punctuation">)</span>blue<span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>但是反过来就不行。如果你想这样：</p><pre class="line-numbers language-javascript"><code class="language-javascript">blue_function <span class="token function">doSomethingAzure</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>  <span class="token function">doSomethingCarnelian</span><span class="token punctuation">(</span><span class="token punctuation">)</span>red<span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>那你就会被请去西伯利亚一日游。</p><p>这样，写一个 <code>filter()</code> 就比较有挑战性了。得给函数选一个颜色，这个颜色会限制哪些函数可以传进去调用。</p><p>显而易见，让 <code>filter()</code> 是红色的比较好，这样红蓝来者不拒。</p><p>但是下一个规矩，就开始像领子里的头发丝儿一样让你痒痒了：</p><p><strong>4. 红色的函数调用起来更加痛苦</strong></p><p>痛苦究竟是什么先按下不提，可以先暂且想象成每个程序员写了任何函数都必须写上八百字的文档。</p><p>可能红色的函数非常冗长，又或许在某些代码块里不能调用红色的函数，还可能你只能在行数为质数的时候才能调用。这其中的重点是，如果你把一个函数弄成了红色，用了你写的东西的程序员都想给你的咖啡里加醋，或者往披萨上放煮熟的草莓。</p><p>显而易见，这样的话最好就<strong>永远</strong>不要用红色的函数了。我们又重回了理智的世界，所有函数都是蓝色，和它们都没有颜色一样，我们的新语言也不会那么蠢。</p><p>可惜，语言设计者抽风了 —— 而且众所周知所有语言设计者都喜欢调教用户，不是吗？最终的致命一击：</p><p><strong>5. 标准库里有些函数是红的</strong></p><p>这个语言里面有一些函数，我们<strong>必须</strong>去用的，不能自己写一套的，是红色。此时，正常人大概会认为这个语言不想让他用。</p><h2 id="炮打函数式编程！"><a href="#炮打函数式编程！" class="headerlink" title="炮打函数式编程！"></a>炮打函数式编程！</h2><p>问题是不是出在高阶函数上呢？如果现在就停止尽享函数奢华，写一写简单的初等函数，想必头发也会少掉很多吧。</p><p>我们在只调用蓝色函数的时候，就把函数作为蓝色的，否则就是红色的。只要不写一些接受函数的函数，我们就不需要考虑什么函数的“颜色多态”（多态？）之类的东西。</p><p>遗憾的是，高阶函数只是一个例子而已。每当你想要把代码拆开重用时，这个问题总会像大蟒蛇一样缠上你。</p><p>再举个例子，假设我们有一些代码，实现了 Dijkstra 算法用来处理你的社交关系图（这到底有什么用？）。很快，你就得在别的地方用这些代码了，所以一个新的函数诞生了。</p><p>它是什么色？当然是蓝色比较好，但是如果它调用了标准库的红色函数呢？如果新的调用点也是蓝色的？你就得把它改写成红色了。当然还有它的上层调用。无论如何，你总是在想着颜色，颜色仿佛成了鞋里的石子儿。</p><h2 id="颜色仅仅是比喻"><a href="#颜色仅仅是比喻" class="headerlink" title="颜色仅仅是比喻"></a>颜色仅仅是比喻</h2><p>显而易见，这篇文章不是真的在讲颜色。这只是个比方，文字上的小把戏。土匪斗恶霸，可不是真的在讲土匪斗恶霸。</p><p>现在，机灵的读者可能已经有点感觉了，但你或许还蒙在鼓里，那么现在就进行大揭秘：</p><p><strong>红色函数就是异步函数</strong></p><p>如果你在 Node.js 上写代码，每当你写出一个通过调用回调（callback）返回值的函数，你就制造了一个红色函数。回头看看那五条规则，解释一下那些比喻：</p><ol><li><p>同步函数返回值，异步函数调用回调；</p></li><li><p>蓝色函数直接调用获得值，红色函数需要提供一个回调；</p></li><li><p>你不能在同步函数里调用异步函数，因为直到异步函数完成之前你不知道返回的值；</p></li><li><p>异步函数和表达式不搭，因为它们不返回值，错误处理方式也不同，所以不能在 <code>try/catch</code> 和大量其他控制语句中使用；</p></li><li><p>Node 的标准库充满了异步函数（虽然他们意识到了这点开始加入 <code>___Sync()</code> 的同样版本）。</p></li></ol><p>人们提及“回调地狱”时，他们实际上在抱怨某个语言里的红色函数。当人们创建了 <a href="https://www.npmjs.com/search?q=async" target="_blank" rel="noopener">4,089 个库用于异步编程</a>，他们实际上在应付一个语言强加给的问题。</p><blockquote><p>2021/12/03: 至今已有 1,5118 个异步库</p></blockquote><h2 id="未来是光明的"><a href="#未来是光明的" class="headerlink" title="未来是光明的"></a>未来是光明的</h2><p>Node 社区的人们意识到了回调之痛苦，于是他们发明了 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise" target="_blank" rel="noopener"><em>Promise</em></a>，或许你已经通过另一个名字学习过了 —— Future。</p><p>Promise 本质上是一个回调和一个错误处理的包装。如果给一个函数传递回调和错误处理器是一个<strong>概念</strong>，那么 Promise 就是这个概念的 <strong>特化</strong>。它是一个表示异步操作的头等对象。</p><p>听了我这段话你可能觉得 Promise 是个好东西了，其实不然。Promise 确实<strong>可以</strong>让你的编写体验提升一点，第四条规则不会<strong>那么</strong>严重。但老实说，其实就是肚子下面一点儿的地方和肚子被来上一拳的区别，可能确实不那么难受，但是应该没人会对此感到满足。</p><p>异常处理和其他控制语句仍然是不可用的状态，你也不能在同步函数里调用一个返回 Future 的函数。就算能，之后维护这段代码的人也会穿越回来对你使用蓄意冲拳。</p><p>Promise 的世界仍然被分为红蓝两色，所以就算你的语言提供了 Promise 或者 Future 这样的特性，它仍然如同我们假设的新语言一样糟糕。</p><p>（这甚至包括本人正在使用的 <a href="https://dart.dev/" target="_blank" rel="noopener">Dart</a>，因此我对于能解决这一点的 <a href="https://github.com/floitschG/fletch" target="_blank" rel="noopener">fletch</a> 相当期待。）</p><blockquote><p>Dart 的 fletch 计划可以提供用户态线程，但已经流产，上方提供的链接是第三方 fork 的远古遗迹 —— 译注</p></blockquote><h2 id="但我仍在寻找"><a href="#但我仍在寻找" class="headerlink" title="但我仍在寻找"></a>但我仍在寻找</h2><p>C# 程序员现在大概跃跃欲试了（他们在微软提供的越来越多语法糖中不断沉陷），因为可以用 <a href="https://msdn.microsoft.com/en-us/library/hh191443.aspx" target="_blank" rel="noopener"><code>await</code> 关键字</a>调用一个异步函数。</p><p>这个东西可以让你调用异步函数和同步函数一样简单，只需要简单地加上一个小小的关键字。表达式里的 <code>await</code> 调用可以嵌套，也可以用在异常处理代码和控制流里。想必和你刚开始学习高阶函数一样，<code>await</code> 也会被到处使用起来。</p><p>Async-await <strong>是</strong>好的，所以 Dart 里也有。<strong>编写</strong>异步代码变得简单多了，但是——如你所想的<strong>但是</strong>——世界仍然是红蓝两半的。尽管更加容易编写，但仍然是异步函数。Async-await 解决了四号规则，红色的函数调用不再那么痛苦，但是到此为止了：</p><ol><li><p>同步函数返回值，异步函数返回 <code>Task&lt;T&gt;</code>（在 Dart 中是 <code>Future&lt;T&gt;</code>）包装起来的值；</p></li><li><p>同步函数直接调用，异步需要一个 <code>await</code>；</p></li><li><p>当你实际上想要 <code>T</code> 的时候，异步方法却返回了包装的值，而且除非把调用的函数也变成异步的，否则你不能拆出实际的 <code>T</code>（不过见下）；</p></li><li><p>除了多出来的 <code>await</code>，至少这个问题被解决了；</p></li><li><p>C# 的核心库比较古老，没有那么多问题。</p></li></ol><p>变好了点儿，至少相对于单纯的回调来说，但是认为一切都完全解决了则是在骗自己。一旦我们开始接触高阶函数，或者尝试着重用代码，颜色的身影就会频频出现。</p><h2 id="什么语言无色？"><a href="#什么语言无色？" class="headerlink" title="什么语言无色？"></a>什么语言无色？</h2><p>所以，JS, Dart, C# 和 Python 都有这个问题。CoffeeScript 和其他编译到 JS 的语言也有（所以 Dart 也有）。甚至 ClojureScript 都会有，尽管他们用 <a href="https://github.com/clojure/core.async" target="_blank" rel="noopener">core.async</a> 尽力避免了。</p><p>想知道没有的吗？<strong>Java</strong>，想不到吧。你有多久没说过“Java 这方面做的不错”了？但是事实如此。可惜 Java 也在试着转移到 Future 和异步 IO 去，仿佛在争倒数第一。</p><p>C# 本可以绕过这个问题，不过他们选择了颜色。在 C# 加入 async-await 和 <code>Task&lt;T&gt;</code> 这样的东西之前，你只需要普通的用一些同步的 API。还有一些语言没有颜色：Go, Lua 与 Ruby。</p><p>有何共同之处呢？</p><p>线程。准确的说，是多个互相独立的调用栈来回切换。它们不需要严格是操作系统线程。Go 的 Goroutine，Lua 的 coroutine 或者 Ruby 的 fiber 都很好。</p><blockquote><p>接上，所以 C# 可以通过使用线程避免 async 的问题。</p></blockquote><h2 id="回忆调用之处"><a href="#回忆调用之处" class="headerlink" title="回忆调用之处"></a>回忆调用之处</h2><p>底层的实际问题可以表述为“该如何在异步操作完成时，回到上一次代码执行的地方”。</p><p>在堆着巨大调用栈时调用了 IO 操作的函数，而为了性能，这个函数用了操作系统底层的异步 API。你不可以等待其完成，因为它是异步的。你必须从这个调用栈返回到语言的事件循环里，让操作系统有点时间完成 IO 操作。</p><p>操作完成后，你需要回到上次调用 IO 操作的地方。一般来说，语言“记住它执行到哪里”的方式是调用栈（callstack），追踪当前正在执行的一系列函数和指令的指针位置。</p><p>但是异步 IO 就必须完整的回退（unwind）并丢弃掉整个调用栈，这似乎就和上面的需求矛盾了：只要我们不在意 IO 的结果，IO 就可以飞快！每个支持异步 IO 的语言 —— JS 则是浏览器的事件循环 —— 都某种程度上受此影响。</p><p>Node 把所谓的调用帧栈用嵌套的回调闭包（closure）实现。你在写下这样的代码时：</p><pre class="line-numbers language-javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">makeSundae</span><span class="token punctuation">(</span>callback<span class="token punctuation">)</span> <span class="token punctuation">{</span>  <span class="token function">scoopIceCream</span><span class="token punctuation">(</span><span class="token keyword">function</span> <span class="token punctuation">(</span>iceCream<span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token function">warmUpCaramel</span><span class="token punctuation">(</span><span class="token keyword">function</span> <span class="token punctuation">(</span>caramel<span class="token punctuation">)</span> <span class="token punctuation">{</span>      <span class="token function">callback</span><span class="token punctuation">(</span><span class="token function">pourOnIceCream</span><span class="token punctuation">(</span>iceCream<span class="token punctuation">,</span> caramel<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>每个函数“闭合”了所有的上下文，<code>iceCream</code> 和 <code>caramel</code> 这样的参数就从调用栈上进入了堆中。外层的函数返回，调用栈销毁后，这些数据仍然四散在堆里。</p><p>问题是你得<strong>手动</strong>去做这些事情，而这个步骤实际上有一个名字：<a href="https://en.wikipedia.org/wiki/Continuation-passing_style" target="_blank" rel="noopener">continuation-passing<br>style</a>，在七十年代时有人发明其作为编译器内部使用。它可以作为一种更容易被编译器优化的代码表示方式。</p><p>不会有任何人想要像这样写代码，但是 Node 偏偏就是这样，将程序员变成了编译器后端。怎么回事呢？</p><p>Promise 和 Future 同样没有解决这些问题，你仍然在手写很多的函数字面量，区别仅仅是这些函数传入了 <code>.then()</code> 而不是直接写出来。</p><blockquote><p>本文写作时（2015/02）包含 async await 的 ES6 尚未发布（2015/06） —— 译注</p></blockquote><h2 id="等待生成结果"><a href="#等待生成结果" class="headerlink" title="等待生成结果"></a>等待生成结果</h2><p>Async-await 确实有点用。如果你开了编译器的瓢，就能在里面看到 CPS 变换。C# 中使用 <code>await</code> 就是为了告诉编译器：“在这里拆分函数”，在 <code>await</code> 之后的部分由编译器合成一个新的函数。</p><p>因此，.NET 框架的 async-await 不需要运行时的支持：编译器将其编译为一系列运行时可以处理的闭包。（并且，闭包实际上也不需要运行时支持，它们被编译成了匿名类。C# 中的闭包是名副其实的<a href="http://c2.com/cgi/wiki?ClosuresAndObjectsAreEquivalent" target="_blank" rel="noopener">穷人的对象</a>）。</p><p>提到生成器（generator），你会想到什么吗？如果某个语言有 <code>yield</code> 关键字，那说不定它也可以实现类似的事情。</p><p>（实际上，我认为生成器和 async await 是同构的。我的硬盘角落里有一些呆了很久的代码，实现了一个只用了 async-await 的生成器风格游戏循环。）</p><p>回到正题，使用回调、Promise、async-await 和生成器，最终得到的结果就是一堆异步函数，包裹在一堆闭包里，散落在堆中。</p><p>这些函数的最外层由运行时管理，在事件循环或者 IO 操作完成后，调用函数回到之前返回的地方。但是在此之前，你还是得先返回一次，也就是说回退整个栈。</p><p>这就是为什么“红色的函数只能由红色函数调用”，因为你需要一路保存调用栈直到最上层的 <code>main()</code> 或者事件循环里。</p><h2 id="特化（Reified）调用栈"><a href="#特化（Reified）调用栈" class="headerlink" title="特化（Reified）调用栈"></a>特化（Reified）调用栈</h2><p>但是如果可以用线程（操作系统或者绿色线程）的话，这些就不会是问题：挂起整个线程不需要让所有的函数返回。</p><p>我认为 Go 在这方面做的最优雅，遇上 IO 操作时直接挂起 goroutine 并恢复另一个没有被 IO 阻塞的。</p><p>而在标准库中，这些 IO 操作看起来就是同步的，也就是说，它们在完成时直接返回值。但这又与 JS 中的同步不同，因为其他的代码可以同时运行。Go 只是消除了同步和异步代码的区别。</p><p>Go 的并发编程中，你可以自行决定如何编程，而无需受到颜色的束缚。前文提到的五条规则在此完整而彻底地被消除了。</p><p>所以，下次你再因为某个语言有优雅的异步 API 而向我传教时，发现我在咬牙切齿也就了然了。你这是回到了红色和蓝色的世界。</p><script>(function() {document.querySelectorAll("code").forEach(function (e) {e.innerHTML = e.innerHTML.replace(/(red_function|(?<!p)red)/g, "<span style=\"color: #e61300;\">$1</span>").replace(/(blue_function|blue)/g, "<span style=\"color: #1a66ff;\">$1</span>");});})();</script>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;原文 &lt;a href=&quot;https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://journal.st
      
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>一种沙盒游戏服务器并行化设想</title>
    <link href="https://izzel.io/2022/03/07/proposal-to-minecraft-server-parallel/"/>
    <id>https://izzel.io/2022/03/07/proposal-to-minecraft-server-parallel/</id>
    <published>2022-03-07T13:06:53.000Z</published>
    <updated>2025-07-13T12:15:43.525Z</updated>
    
    <content type="html"><![CDATA[<p>本文将会以 Minecraft 为例介绍 3D 沙盒游戏服务器的运行模型、该单线程模型的限制，提出一种可行的、基于区域锁的并行化模型，分析其可行性，并给出对应的实现细节、调度优化方法等。</p><h2 id="介绍"><a href="#介绍" class="headerlink" title="介绍"></a>介绍</h2><p>Minecraft 是一个 3D 建造沙盒游戏，发行内容包含一个单独的服务端程序可供多人联机游玩。Minecraft 服务器使用单线程运行，一次完整的逻辑循环称为一个游戏刻。</p><p>正常情况下服务器每秒运行 20 次逻辑循环，即以 20 TPS(Ticks per second) 运行。当性能达到瓶颈时，服务器难以在 50 毫秒内处理一次游戏刻，客户端将会体验到服务器出现明显卡顿，服务器性能下降的数值体现为 TPS 低于 20。通常情况下，服务器的计算任务与客户端连接数量成线性关系，由于服务器上几乎所有逻辑都运行于单个线程，其性能取决于处理器的运算能力，因此单个服务器能够为客户端提供良好服务的数量受到单个处理器性能限制。一般认为，单个服务器最多能为数十个客户端提供服务。</p><p>目前来说，提升服务器处理能力有三种方法。第一种可靠的方法是优化代码以减少计算量，以 Spigot、Paper 等项目为代表，但这种方法并没有改变服务器的单线程模型，因此伸缩性仍然较差。第二种可靠的方法是通过转发软件如 BungeeCord 进行负载均衡，但这种方法降低了客户端的服务体验，对于同一场景下多个客户端的场景仍然不能提供良好服务，但这一方法可以让单个负载均衡集群处理高达数万个客户端连接。第三种方法是改变服务器的处理模型，引入并行化处理，其中 Aikar 提出了一种被动拆分的并行化模型<sup id="fnref:5"><a href="#fn:5" rel="footnote">5</a></sup><sup id="fnref:6"><a href="#fn:6" rel="footnote">6</a></sup>，但该模型的最小并行粒度是区块，并暴露了线程安全问题给插件开发者；部分服务端实现了并行的光照计算<sup id="fnref:7"><a href="#fn:7" rel="footnote">7</a></sup>、异步实体寻路计算、多个维度并行计算<sup id="fnref:8"><a href="#fn:8" rel="footnote">8</a></sup>，但都未达到生产环境可用，并且对第三方注入的代码（插件及模组）做出了能够正确处理并行的假设，而这一点通常是无法达到的。</p><p>并行化服务器由于以下原因变得十分有挑战性。</p><ol><li>在线问题。服务器需处理客户端的网络包，因此存在不可预测的逻辑计算。对于单个游戏对象的逻辑计算，我们不知道其何时会停止。</li><li>存在嵌套。游戏在处理方块更新时会传播其方块更新，而这样的方块更新会引入新的方块更新，该过程是不可预测的<sup id="fnref:9"><a href="#fn:9" rel="footnote">9</a></sup>。对于含有外部代码如插件和模组事件的逻辑，可能存在递归调用。</li><li>存在时序（依赖）。服务器内的对象在逻辑循环内有一定的顺序，有时这样的顺序是重要的，例如红石电路对于处理顺序有一定要求<sup id="fnref:9"><a href="#fn:9" rel="footnote">9</a></sup>。对于严格依赖时序的游戏内容，并行化后自然会乱序执行，因此这种情况超出了讨论范围，这里认为其本身为未定义行为，任何情况皆可接受。</li><li>存在外部代码。几乎所有第三方服务端都提供了加载外部代码的功能，不限于模组和插件，因此应该存在一种机制正确处理外部代码。部分框架提供了字节码处理的功能，这种情况显然超出了讨论范围。</li><li>存在全局状态。红石线方块存在被享元对象修改的全局状态（<code>shouldSignal</code>），其他部分代码也有类似情形。</li></ol><p>为解决这些问题，本文将包含以下内容。</p><ol><li>提供一种并行化运行模型。我们将 Minecraft 服务器的各类逻辑运算抽象为事务，并以合适的方式调度之，利用细粒度的互斥锁和共享锁保证其并发安全性。本文还将讨论该模型与一般的事务性数据库的区别。</li><li>提出一种线程池的实现，并对运行的计算任务以合适的方式、或利用 JVM 的 Continuation 机制进行管理，并使用一种启发式算法进行调度优化。</li><li>提出一种对拓展及事件开放的 API 设计，用于描述事务的属性。</li></ol><h2 id="问题描述"><a href="#问题描述" class="headerlink" title="问题描述"></a>问题描述</h2><p>在这一节内，我们将分析 Minecraft 服务器的计算任务架构，并提出我们的并行化运行模型。</p><h3 id="游戏刻循环"><a href="#游戏刻循环" class="headerlink" title="游戏刻循环"></a>游戏刻循环</h3><p>如同大部分游戏服务器，Minecraft 服务器使用单线程运行，一次完整的逻辑循环称为一个游戏刻。</p><p>在一个游戏刻内，服务器会按顺序更新（tick）每个世界及其中的每个区块，而区块通常被认为是更新的最小单位。区块更新包含了对于实体的更新和对于方块的更新，其中实体更新包括 AI 计算、寻路、移动和碰撞等，方块更新包括计划刻更新、随机刻更新和方块实体更新，这几个内容组成了多数服务器的主要计算任务。</p><p>分析几个主要的计算任务之后不难得出，对于单个对象如实体而言，其自身的计算任务是有依赖关系的，因此我们不能首先计算移动再计算寻路；而对于不同对象之间，尽管它们之间可能会互相影响，如两个 TNT 实体的爆炸顺序会导致不同的结果，但对于 Minecraft 而言，并没有明确定义的依赖关系：对于实体更新列表使用哈希表存储，方块更新顺序使用哈希表存储因此表现为随机<sup id="fnref:9"><a href="#fn:9" rel="footnote">9</a></sup>。因此，我们可以将不同对象的更新并行化，同时保留单个对象更新任务的顺序。</p><p>对于单个对象不同阶段的更新任务而言，我们可以分析其对于服务器内存数据这一资源的访问模式。大部分更新任务都具有良好的“空间局部性”，例如对于实体碰撞而言，更新中只会读取附近一小部分的实体并修改它们的速度值。同时，大部分更新任务需要读写的部分具有不同的类型，如实体寻路会读取世界的方块数据并写入自身实体的目标寻路数据。因此，对于不同的更新任务，我们可以在空间上使用锁保证其执行的正确性，同时对不同的读写类型分别进行细粒度锁定。</p><h3 id="运行模型"><a href="#运行模型" class="headerlink" title="运行模型"></a>运行模型</h3><p>我们接下来提出一个基于以上分析得出的模型。</p><p>对于每个更新任务，我们定义区域（Extent）用于描述其对于资源的使用属性。每个区域 E 描述了其类型 T 、其范围形状 R 和其是否互斥 F，F 为 S (Share) 共享或 X (Exclusive) 独占。为简化模型，我们将形状 R 定义为正方体，记作 <code>[l,x,y,z]:r</code>，例如对于世界 1 坐标 0,0,0 处附近 16 格的范围记作 <code>[1,0,0,0]:16</code>，其为从 -16,-16,-16 到 16,16,16 的 32x32x32 正方体。</p><p>我们接下来定义类型 T 的树形结构。由前分析得知，不同的更新任务可以读写分离的不同部分世界数据，如实体寻路读取方块数据写入实体数据。同时，某些类型的更新任务可能包含全局状态，因此我们可以得出一个类型的树形结构，以 <code>GLOBAL</code> 为根。</p><pre><code>        GLOBAL          |        LEVEL      ___/ \___     /         \   BLOCK      ENTITY     |BLOCK_ENTITY</code></pre><p>对于任意一个类型 T，T 包含了所有子类型。对于这一类型树，可以拓展其以获得更细的粒度。</p><p>接下来我们定义区域 E 的重叠。对于区域 <code>E1{T1,R1,F1}</code> 和 <code>E2{T2,R2,F2}</code>，有以下规则：</p><ul><li>如果 T1 为 <code>GLOBAL</code> 或 T2 为 <code>GLOBAL</code>，则重叠；</li><li>否则，如果 T1 和 T2 互相不包含对方，且 T1 T2 不相等，则不重叠；</li><li>否则，如果 R1 R2 的维度 <code>l</code> 不相等，则不重叠；</li><li>否则，如果 E1 E2 都不独占，即 <code>F1 == F2 == S</code>，则不重叠；</li><li>否则，如果 R1 R2 的距离（任意坐标差绝对值的最大值 <code>max{abs(x1-x2), abs(y1-y2), abs(z1-z2)}</code>）小于 R1 R2 的 r 之和，则重叠。</li><li>否则不重叠</li></ul><p>每个更新任务由一系列区域 E 组成，当两个更新任务的每个区域互不重叠，则它们不重叠。重叠的更新任务不能并发进行。至此，我们的模型与一个事务数据库相似，但仍有一定不同。</p><h3 id="处理死锁"><a href="#处理死锁" class="headerlink" title="处理死锁"></a>处理死锁</h3><p>当一个线程会请求对资源的独占控制，且已经占有资源后再次请求其他资源独占，且除了中断（Abort）以外无法释放资源时，死锁将会发生<sup id="fnref:10"><a href="#fn:10" rel="footnote">10</a></sup>。对于我们的模型，可能会包含嵌套的更新任务，这三个条件均可以满足，因此会发生死锁。</p><p>在事务性数据库中，事务可以被中断，因此可以使用类似 2PL 的技术，在发生死锁时中断某个事务的执行进行重试<sup id="fnref:11"><a href="#fn:11" rel="footnote">11</a></sup>。我们的模型不同之处在于，其更新任务无法被中断。同时，对于一个更新任务而言，一定需要对一个区域的独占访问以保证执行的正确性。因此，我们需要破坏“已经占有资源后再次请求其他资源独占”这一死锁发生的条件。</p><p>常见的防止发生死锁的方法有几种，分别是串行化执行、一次性请求所有资源、抢占式资源占有和对资源排序<sup id="fnref:10"><a href="#fn:10" rel="footnote">10</a></sup><sup id="fnref:12"><a href="#fn:12" rel="footnote">12</a></sup>。串行化执行完全抛弃了并发，与本文的目标不符，因此不作考虑。抢占式的资源需要修改已有代码以支持对一个更新任务的中断，同时不对拓展开放，因此不作考虑。在本模型中，难以对三维空间中的范围这一资源进行排序，因此不作考虑。</p><p>本模型使用一次性请求所有资源进行死锁避免，因此需要要求所有的更新任务提供其占有资源的范围，即一系列区域。在每个更新任务开始前，对这一系列区域进行锁定。如果无法在任务开始前确定占有资源的范围，则进行 <code>GLOBAL</code> 的锁定。</p><h2 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h2><p>本节将会介绍以上提出的模型的实现方式。</p><h3 id="区域锁"><a href="#区域锁" class="headerlink" title="区域锁"></a>区域锁</h3><p>本模型需要对不能并行的更新任务进行正确的约束，因此需要一个对区域进行锁定的实现。我们在这里提出一种“无锁”的区域锁（ExtentLock）实现，并且支持 lock 和 tryLock 两种锁定方式。这里的无锁指多个线程可以并发地查询、插入该区域锁对象，而不需要对区域锁对象本身进行互斥访问。</p><p>因为我们需要满足一次性请求所有资源，因此所有的锁定操作都接受一系列区域作为参数。</p><p>区域锁将会有三种基本的操作：锁定、尝试锁定和释放。锁定操作将会阻塞至成功锁定对应区域；尝试锁定将会尝试锁定区域，并在存在重叠的区域时立刻返回失败；释放将会释放一个已经锁定的区域。</p><p>区域锁将使用一个线程安全队列实现，满足其锁定的发生顺序。</p><p>对于锁定操作，我们希望 1) 对重叠的区域释放的操作发生在返回之前，同时 2) 对于两个并行发生并且互相互斥的锁定操作，我们希望先成功的锁定操作对应的释放操作发生在后成功的锁定操作之前。对于这样的语义，我们可以使用 <code>CountDownLatch</code> 保证发生的先后顺序：CountDownLatch 的 countDown 操作发生在 await 操作返回之前。因此锁定操作的实现如下：</p><ul><li>新建一个 CountDownLatch，记作 L，传入的一系列区域对应该 L</li><li>向队列插入传入的一系列区域</li><li>对队列内所有插入时已经存在的区域，检查其是否与请求锁定的区域重叠<ul><li>如果重叠，则等待其 L 释放</li></ul></li><li>返回成功</li></ul><p>对于尝试锁定操作，其实现与锁定类似，但在重叠时不进行等待而直接返回：</p><ul><li>新建一个 CountDownLatch，记作 L，传入的一系列区域对应该 L</li><li>向队列插入传入的一系列区域</li><li>对队列内所有插入时已经存在的区域，检查其是否与请求锁定的区域重叠<ul><li>如果重叠，则移除插入的一系列区域，然后释放 L，返回失败</li></ul></li><li>返回成功</li></ul><p>对于释放操作的实现如下：</p><ul><li>移除插入的一系列区域</li><li>释放 L</li></ul><p>对于该实现，锁定返回之前等待了之前锁定所有区域的释放，因此满足了第一个条件；插入的先后顺序由队列的实现保证，而插入后检查了插入前的所有区域，因此满足了第二个条件，故该实现正确。</p><h3 id="执行更新"><a href="#执行更新" class="headerlink" title="执行更新"></a>执行更新</h3><p>在具体执行游戏对象的更新时，我们将需要并行化的更新任务投入线程池，并等待所有任务完成。大部分耗时且有并行可能的任务通常由一个循环开始，对于该循环，我们将其中的每个元素更新的调用和该更新任务的描述（即其一系列区域）提交至线程池，使用区域锁进行互斥保护，并等待线程池中所有任务执行完成。在等待并行执行时，主线程被挂起。</p><p>前文提到了我们可以对同一个对象的更新任务进行阶段的切分。切分的主要目的是在完成某个任务后释放其占有的资源，并等待申请新资源的锁。切分的实现可参考 Minecraft 对于 Profiler 的实现方式，在每个阶段切换时通过一个 <code>popPush</code> 调用，完成对原有资源的释放和新资源的锁定。该方法对于代码的修改较小，实现为插入一条方法调用，通常不会影响类似 <code>Mixin</code> 等字节码操作框架的执行。</p><h3 id="任务调度算法"><a href="#任务调度算法" class="headerlink" title="任务调度算法"></a>任务调度算法</h3><p>在对任务切分并提交至线程池后，我们的目标变为将这一系列任务的执行以最短的时间完成。该目标与一个事务型数据库的事务处理相似，需调度一系列存在竞争的任务。对这一方面的研究称为竞争感知调度<sup id="fnref:13"><a href="#fn:13" rel="footnote">13</a></sup><sup id="fnref:14"><a href="#fn:14" rel="footnote">14</a></sup>（Contention aware scheduling），但与一般的事务型数据库不同的是，本模型既不能中断事务也不能部分分配一个锁，因此大部分适用于事务数据库的优化不适用此处。同时，我们不在意尾延迟（tail latency）、平均事务延迟等一般事务数据库需要考虑的指标，唯一的目标是最大化吞吐量，即最小化总延迟。</p><p>对于任务的调度，本模型采用在每个阶段开始之前挂起线程后，通过调度器编程进行指定任务的线程启动。同时对于支持协程<sup id="fnref:15"><a href="#fn:15" rel="footnote">15</a></sup>的 JVM 而言，可以在更新任务切换阶段时挂起该协程，进行任务的调度。理论上而言，协程并不是一个必须的技术，因为对于线程模型而言，我们也可以挂起线程进行调度，但达到相同的调度效率需要启动与调度集大小相同的线程数，这可能是数千，而几千个线程将消耗数 GB 的内存。</p><p>对于本模型的任务，我们可以使用一种启发式算法进行调度。每个更新任务可以提供其将要占用的资源，即一系列区域。我们引入竞争度优先调度，利用前文定义的更新任务重叠，定义一个更新任务的竞争度为该任务与更新任务队列中参与调度所有任务的重叠数量。对于所有任务，优先执行竞争度最大的。该启发式算法对于仅有互斥锁的情况下可降低一半以上的调度时间消耗，对于存在共享锁的情况优化能力稍弱，但相比原始实现仍有显著提升。</p><p>我们也进行了对于该调度问题的其他算法尝试。一个尝试是进行 k-means 聚类<sup id="fnref:16"><a href="#fn:16" rel="footnote">16</a></sup>后按 cluster 进行调度，相比于原始算法有所提升，但弱于竞争度优先算法。另一个尝试是实现了一个一般的事务型数据库基于图的调度器，但在对于区域切分时遇到困难，同时难以实现一次性分配所有锁的同时对锁进行调度。</p><h3 id="处理嵌套"><a href="#处理嵌套" class="headerlink" title="处理嵌套"></a>处理嵌套</h3><p>本模型中存在这样一种情况，在一个更新任务运行时会启动另一个任务，即存在任务的嵌套。对于一般的线程池而言，其遵循一个先进先出的队列模型，而对于嵌套的任务，其完成发生在上一级任务完成之前，即遵循后进先出的栈模型。</p><p>对于嵌套的任务执行，一种解决方法是立刻执行，不向线程池提交，但这种方法在一种情况下会降低并行性，即当前任务可以生成多个嵌套任务时；另一种方法是向线程池提交，但 Java 默认的线程池实现将会使得一个线程等待另一个线程完成，对于固定大小线程池可能会导致死锁，也可能造成运行时产生过多线程数影响性能。如果提高并行性，我们就需要在原来任务等待时执行嵌套任务，执行完嵌套任务后执行原来任务省下的部分<sup id="fnref:17"><a href="#fn:17" rel="footnote">17</a></sup><sup id="fnref:18"><a href="#fn:18" rel="footnote">18</a></sup>。</p><p>对于这种情况通常有三种方式实现。第一种方式是类似异步函数一般直接返回，但这种方法需要对代码进行转换，因此不作考虑。第二种方法是在等待时直接执行其他任务，这种方法需要充足的栈空间。第三种方法是将栈直接保存，重新执行其他任务后恢复原来的栈继续执行，这种方法需要 JVM 支持。<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:5"><span style="display: inline-block; vertical-align: top; padding-right: 10px;">5.</span><span style="display: inline-block; vertical-align: top;"><a href="https://github.com/PaperMC/Paper/issues/1001" target="_blank" rel="noopener">https://github.com/PaperMC/Paper/issues/1001</a></span><a href="#fnref:5" rev="footnote"> ↩</a></li><li id="fn:6"><span style="display: inline-block; vertical-align: top; padding-right: 10px;">6.</span><span style="display: inline-block; vertical-align: top;"><a href="https://github.com/PaperMC/Paper/issues/1031" target="_blank" rel="noopener">https://github.com/PaperMC/Paper/issues/1031</a></span><a href="#fnref:6" rev="footnote"> ↩</a></li><li id="fn:7"><span style="display: inline-block; vertical-align: top; padding-right: 10px;">7.</span><span style="display: inline-block; vertical-align: top;"><a href="https://github.com/TorchSpigot/Torch/blob/master/sources/src/main/java/io/akarin/server/mixin/lighting/MixinWorldServer.java" target="_blank" rel="noopener">https://github.com/TorchSpigot/Torch/blob/master/sources/src/main/java/io/akarin/server/mixin/lighting/MixinWorldServer.java</a></span><a href="#fnref:7" rev="footnote"> ↩</a></li><li id="fn:8"><span style="display: inline-block; vertical-align: top; padding-right: 10px;">8.</span><span style="display: inline-block; vertical-align: top;"><a href="https://github.com/WearBlackAllDay/DimensionalThreading" target="_blank" rel="noopener">https://github.com/WearBlackAllDay/DimensionalThreading</a></span><a href="#fnref:8" rev="footnote"> ↩</a></li><li id="fn:9"><span style="display: inline-block; vertical-align: top; padding-right: 10px;">9.</span><span style="display: inline-block; vertical-align: top;"><a href="https://bugs.mojang.com/browse/MC-11193" target="_blank" rel="noopener">https://bugs.mojang.com/browse/MC-11193</a></span><a href="#fnref:9" rev="footnote"> ↩</a></li><li id="fn:10"><span style="display: inline-block; vertical-align: top; padding-right: 10px;">10.</span><span style="display: inline-block; vertical-align: top;">Isloor S S, Marsland T A. The Deadlock Problem: An Overview[J]. Computer, 1980, 13(9): 58-78. <a href="https://doi.org/10.1109/MC.1980.1653786" target="_blank" rel="noopener">https://doi.org/10.1109/MC.1980.1653786</a></span><a href="#fnref:10" rev="footnote"> ↩</a></li><li id="fn:11"><span style="display: inline-block; vertical-align: top; padding-right: 10px;">11.</span><span style="display: inline-block; vertical-align: top;">Philip P. Macri. Deadlock detection and resolution in a CODASYL based data management system[C]. , 1976. <a href="https://doi.org/10.1145/509383.509392" target="_blank" rel="noopener">https://doi.org/10.1145/509383.509392</a></span><a href="#fnref:11" rev="footnote"> ↩</a></li><li id="fn:12"><span style="display: inline-block; vertical-align: top; padding-right: 10px;">12.</span><span style="display: inline-block; vertical-align: top;">Havender J W. Avoiding deadlock in multitasking systems[J]. IBM systems journal, 1968, 7(2): 74-84. <a href="https://doi.org/10.1147/sj.72.0074" target="_blank" rel="noopener">https://doi.org/10.1147/sj.72.0074</a></span><a href="#fnref:12" rev="footnote"> ↩</a></li><li id="fn:13"><span style="display: inline-block; vertical-align: top; padding-right: 10px;">13.</span><span style="display: inline-block; vertical-align: top;">Blagodurov S, Zhuravlev S, Fedorova A. Contention-aware scheduling on multicore systems[J]. ACM Transactions on Computer Systems (TOCS), 2010, 28(4): 1-45. <a href="https://doi.org/10.1145/1880018.1880019" target="_blank" rel="noopener">https://doi.org/10.1145/1880018.1880019</a></span><a href="#fnref:13" rev="footnote"> ↩</a></li><li id="fn:14"><span style="display: inline-block; vertical-align: top; padding-right: 10px;">14.</span><span style="display: inline-block; vertical-align: top;">Tian B, Huang J, Mozafari B, et al. Contention-aware lock scheduling for transactional databases[J]. Proceedings of the VLDB Endowment, 2018, 11(5): 648-662. <a href="https://doi.org/10.1145/3177732.3177740" target="_blank" rel="noopener">https://doi.org/10.1145/3177732.3177740</a></span><a href="#fnref:14" rev="footnote"> ↩</a></li><li id="fn:15"><span style="display: inline-block; vertical-align: top; padding-right: 10px;">15.</span><span style="display: inline-block; vertical-align: top;"><a href="https://bugs.openjdk.java.net/browse/JDK-8277131" target="_blank" rel="noopener">https://bugs.openjdk.java.net/browse/JDK-8277131</a></span><a href="#fnref:15" rev="footnote"> ↩</a></li><li id="fn:16"><span style="display: inline-block; vertical-align: top; padding-right: 10px;">16.</span><span style="display: inline-block; vertical-align: top;">Hartigan J A, Wong M A. Algorithm AS 136: A k-means clustering algorithm[J]. Journal of the royal statistical society. series c (applied statistics), 1979, 28(1): 100-108. <a href="https://doi.org/10.2307/2346830" target="_blank" rel="noopener">https://doi.org/10.2307/2346830</a></span><a href="#fnref:16" rev="footnote"> ↩</a></li><li id="fn:17"><span style="display: inline-block; vertical-align: top; padding-right: 10px;">17.</span><span style="display: inline-block; vertical-align: top;">Felleisen M. The theory and practice of first-class prompts[C]//Proceedings of the 15th ACM SIGPLAN-SIGACT symposium on Principles of programming languages. 1988: 180-190. <a href="https://doi.org/10.1145%2F73560.73576" target="_blank" rel="noopener">https://doi.org/10.1145%2F73560.73576</a></span><a href="#fnref:17" rev="footnote"> ↩</a></li><li id="fn:18"><span style="display: inline-block; vertical-align: top; padding-right: 10px;">18.</span><span style="display: inline-block; vertical-align: top;">Bob N. What Color is Your Function[EB/OL]. 2016. <a href="https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/" target="_blank" rel="noopener">https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/</a></span><a href="#fnref:18" rev="footnote"> ↩</a></li></ol></div></div></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;本文将会以 Minecraft 为例介绍 3D 沙盒游戏服务器的运行模型、该单线程模型的限制，提出一种可行的、基于区域锁的并行化模型，分析其可行性，并给出对应的实现细节、调度优化方法等。&lt;/p&gt;
&lt;h2 id=&quot;介绍&quot;&gt;&lt;a href=&quot;#介绍&quot; class=&quot;header
      
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>聊聊生物和 AI</title>
    <link href="https://izzel.io/2021/12/19/living-things/"/>
    <id>https://izzel.io/2021/12/19/living-things/</id>
    <published>2021-12-19T22:19:15.000Z</published>
    <updated>2025-07-13T12:15:43.524Z</updated>
    
    <content type="html"><![CDATA[<p>一只羊停下吃草，抬头望着你… 这其实不会发生。</p><p>本文介绍 Minecraft 实体相关的目标选择器（Goal Selector）、脑（Brain）等内容，基于 Minecraft 1.18 版本，使用官方映射表。</p><h2 id="有感知的实体-Goal"><a href="#有感知的实体-Goal" class="headerlink" title="有感知的实体 - Goal"></a>有感知的实体 - Goal</h2><p>如果一款游戏里面的生物不会对玩家的行为产生反馈，那就太无聊了。想象这样的场景，NPC 对经过的玩家熟视无睹，和玩家对话时机械地盯着虚空…</p><p>在 Minecraft 里，玩家可以很容易地将实体分为两类：无感知的实体，比如船；和有感知的「生物」，比如羊。</p><p>回顾一下，羊会干什么呢？</p><p>和大部分生物一样，羊会望向接近的玩家；羊也会跟着拿着小麦的玩家，还会吃草…将这些能力一一组合起来，便成为了一个「有感知」的生物。而将这些东西组合在一起的系统，被称为目标选择器。</p><p>望向玩家、吃草、跟随玩家都是目标（<code>Goal</code>），它们以不同的优先度出现在目标选择器中。对于一个普通的羊，它会有这些目标：</p><table><thead><tr><th align="left">Priority</th><th align="left">Goal</th></tr></thead><tbody><tr><td align="left">0</td><td align="left">FloatGoal（浮在水面上）</td></tr><tr><td align="left">1</td><td align="left">PanicGoal</td></tr><tr><td align="left">2</td><td align="left">BreedGoal</td></tr><tr><td align="left">3</td><td align="left">TemptGoal（跟随小麦）</td></tr><tr><td align="left">4</td><td align="left">FollowParentGoal</td></tr><tr><td align="left">5</td><td align="left">EatBlockGoal（吃草）</td></tr><tr><td align="left">6</td><td align="left">WaterAvoidingRandomStrollGoal</td></tr><tr><td align="left">7</td><td align="left">LookAtPlayerGoal（望向玩家）</td></tr><tr><td align="left">8</td><td align="left">RandomLookAroundGoal</td></tr></tbody></table><p>这些目标以某种形式共同工作：毕竟羊不是时时刻刻都在吃草，也不是时时刻刻都在随机乱走，必然存在一种机制管理它们何时运行，使得它们互相协调。</p><h3 id="Goal-生命周期"><a href="#Goal-生命周期" class="headerlink" title="Goal 生命周期"></a>Goal 生命周期</h3><p>目光转向 <code>Goal</code> 类的几个方法：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">package</span> net<span class="token punctuation">.</span>minecraft<span class="token punctuation">.</span>world<span class="token punctuation">.</span>entity<span class="token punctuation">.</span>ai<span class="token punctuation">.</span>goal<span class="token punctuation">;</span><span class="token keyword">public</span> <span class="token keyword">abstract</span> <span class="token keyword">class</span> <span class="token class-name">Goal</span> <span class="token punctuation">{</span>  <span class="token keyword">public</span> <span class="token keyword">abstract</span> <span class="token keyword">boolean</span> <span class="token function">canUse</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">public</span> <span class="token keyword">boolean</span> <span class="token function">canContinueToUse</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">start</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">stop</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">tick</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">public</span> <span class="token keyword">boolean</span> <span class="token function">isInterruptable</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">setFlags</span><span class="token punctuation">(</span>EnumSet<span class="token operator">&lt;</span>Goal<span class="token punctuation">.</span>Flag<span class="token operator">></span> flags<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">public</span> EnumSet<span class="token operator">&lt;</span>Goal<span class="token punctuation">.</span>Flag<span class="token operator">></span> <span class="token function">getFlags</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">public</span> <span class="token keyword">enum</span> Flag <span class="token punctuation">{</span>    MOVE<span class="token punctuation">,</span> LOOK<span class="token punctuation">,</span> JUMP<span class="token punctuation">,</span> TARGET  <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>前五个方法顾名思义，按照这样的流程运行：</p><p><img src="goal.png" alt></p><p>图中黑圈为未运行的 Goal，红圈为正在运行的。以 <code>TemptGoal</code> 为例，这个 Goal 会这样编写：</p><ul><li><code>canUse</code> 进行开始条件的判断：寻找附近拿着小麦的玩家。为了性能考虑，这里还可以尽量的缓存一些需要查询的东西，比如对应的玩家、坐标…等等</li><li><code>start</code> 执行目标开始的逻辑，当然也可以什么都不干。</li><li><code>tick</code> 每个游戏刻更新目标，比如让羊向玩家走一步。</li><li><code>canContinueToUse</code> 检查是否仍然应该继续执行：玩家可能走远了，或者把小麦收起来了，或者有新的玩家拿着小麦走来了。</li><li><code>stop</code> 执行停止的逻辑，这里应该把一开始缓存的东西全部重置掉。</li></ul><p>读者可以参考许多源码进一步加深 Goal 生命周期的理解。</p><h3 id="Goal-协调机制"><a href="#Goal-协调机制" class="headerlink" title="Goal 协调机制"></a>Goal 协调机制</h3><p>我们知道如果羊被玩家打了一下，会到处跑来跑去，此时玩家拿出小麦的话，合理的现象应该是羊还会继续跑来跑去。从上面的表中可以得知，<code>PanicGoal</code> 的优先级高于 <code>TemptGoal</code>，这很合理。</p><p>但这引起了一个新的问题，<code>FloatGoal</code>（让生物浮在水面上）的优先度是最高的，但是玩家还是可以用小麦去吸引浮在水面的羊。实际上，这是一个比单纯的优先度更为复杂的机制。</p><p>每个 Goal 可能会有一些 <code>Flag</code>，或者把它叫做锁。上面的例子里，<code>FloatGoal</code> 有 <code>JUMP</code> 的锁，<code>PanicGoal</code> 和 <code>TemptGoal</code> 都有 <code>MOVE</code> 的锁。这些锁是「互斥」的，也就是说，同一时间某一种 Flag 只会对应一个 Goal。自然，不持有任何锁的 Goal 不会与其他 Goal 互斥，它们不受影响始终执行。</p><p>那么如果同时有两个 <code>canUse</code> 的 Goal 都有 <code>MOVE</code> 呢？显而易见，优先度高（数字小）的执行。但这又带来了新的问题：如果玩家拿着小麦打了一下羊，优先度高的 Goal 突然可用了，优先度低的跟随小麦会怎样呢？当然就应该中断了。</p><p>回到 <code>Goal</code> 类的后三个方法：</p><ul><li><code>isInterruptable</code> 表明一个 Goal 能否被其他 Goal 中断</li><li><code>setFlags</code> 用来设置 Goal Flag，一般在构造方法中调用</li></ul><p>在 Minecraft 1.18 中，Mojang 引入了一个小优化：观察到通常 <code>canUse</code> 执行最多的代码逻辑来判断和缓存状态，但未运行的 Goal 并不影响实体，并且让羊慢一两刻再对玩家的小麦进行响应通常不会破坏游戏的沉浸感。</p><p>优化的方法是，每 N 刻中只有最后一刻进行 <code>canUse</code>/<code>canContinueToUse</code> 检查，其他 N-1 刻只进行 <code>tick</code> 的调用。在 1.18 这个 N 是 2，但也不排除以后会增加，所以不要对这个 N 的具体数值做假设。</p><p>回到 <code>Goal</code> 类，1.18 起提供了以下两个方法：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">abstract</span> <span class="token keyword">class</span> <span class="token class-name">Goal</span> <span class="token punctuation">{</span>  <span class="token keyword">public</span> <span class="token keyword">boolean</span> <span class="token function">requiresUpdateEveryTick</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">protected</span> <span class="token keyword">int</span> <span class="token function">adjustedTickDelay</span><span class="token punctuation">(</span><span class="token keyword">int</span> tick<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p>其中：</p><ul><li><code>requiresUpdateEveryTick</code> 让这个 Goal 每一刻都更新，可以用于实时性高的情景</li><li><code>adjustedTickDelay</code> 相当于 <code>tick/N</code></li></ul><p>考虑 <code>PanicGoal</code>，如果我们想让羊到处乱跑十秒钟，就会这么写（并非 Mojang 实现）：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">PanicGoal</span> <span class="token keyword">extends</span> <span class="token class-name">Goal</span> <span class="token punctuation">{</span>  <span class="token keyword">private</span> <span class="token keyword">int</span> ticks<span class="token punctuation">;</span>  <span class="token annotation punctuation">@Override</span> <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">start</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token keyword">this</span><span class="token punctuation">.</span>ticks <span class="token operator">=</span> <span class="token function">adjustedTickDelay</span><span class="token punctuation">(</span><span class="token number">10</span> <span class="token operator">*</span> <span class="token number">20</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 10s</span>  <span class="token punctuation">}</span>  <span class="token annotation punctuation">@Override</span> <span class="token keyword">public</span> <span class="token keyword">boolean</span> <span class="token function">canContinueToUse</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>ticks<span class="token operator">--</span> <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">;</span>  <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h3 id="Goal-注册"><a href="#Goal-注册" class="headerlink" title="Goal 注册"></a>Goal 注册</h3><p>实际在 Minecraft 中提供了两个 GoalSelector，另一个用于攻击目标的选择。对于有攻击性的生物，一般在 <code>goalSelector</code> 中注册一个 <code>MeleeAttackGoal</code>（近战攻击）或者 <code>RangedAttackGoal</code>（远程攻击），而在 <code>targetSelector</code> 中注册选择某个敌人进行攻击的 Goal，比如 <code>HurtByTargetGoal</code>（攻击生物的敌人）或者 <code>NearestAttackableTargetGoal</code>（最近的敌人）。</p><p>目标通常在 <code>Mob#registerGoals</code> 方法内注册，考虑烈焰人：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Blaze</span> <span class="token keyword">extends</span> <span class="token class-name">Mob</span> <span class="token punctuation">{</span>  <span class="token keyword">protected</span> <span class="token keyword">void</span> <span class="token function">registerGoals</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token keyword">this</span><span class="token punctuation">.</span>goalSelector<span class="token punctuation">.</span><span class="token function">addGoal</span><span class="token punctuation">(</span><span class="token number">4</span><span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token class-name">Blaze<span class="token punctuation">.</span>BlazeAttackGoal</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">this</span><span class="token punctuation">.</span>goalSelector<span class="token punctuation">.</span><span class="token function">addGoal</span><span class="token punctuation">(</span><span class="token number">5</span><span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token class-name">MoveTowardsRestrictionGoal</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">,</span> <span class="token number">1.0D</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">this</span><span class="token punctuation">.</span>goalSelector<span class="token punctuation">.</span><span class="token function">addGoal</span><span class="token punctuation">(</span><span class="token number">7</span><span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token class-name">WaterAvoidingRandomStrollGoal</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">,</span> <span class="token number">1.0D</span><span class="token punctuation">,</span> <span class="token number">0.0F</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">this</span><span class="token punctuation">.</span>goalSelector<span class="token punctuation">.</span><span class="token function">addGoal</span><span class="token punctuation">(</span><span class="token number">8</span><span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token class-name">LookAtPlayerGoal</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">,</span> Player<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">,</span> <span class="token number">8.0F</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">this</span><span class="token punctuation">.</span>goalSelector<span class="token punctuation">.</span><span class="token function">addGoal</span><span class="token punctuation">(</span><span class="token number">8</span><span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token class-name">RandomLookAroundGoal</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">this</span><span class="token punctuation">.</span>targetSelector<span class="token punctuation">.</span><span class="token function">addGoal</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">HurtByTargetGoal</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">setAlertOthers</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">this</span><span class="token punctuation">.</span>targetSelector<span class="token punctuation">.</span><span class="token function">addGoal</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token class-name">NearestAttackableTargetGoal</span><span class="token operator">&lt;</span><span class="token operator">></span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">,</span> Player<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">,</span> <span class="token boolean">true</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h2 id="有意识的生物-Brain"><a href="#有意识的生物-Brain" class="headerlink" title="有意识的生物 - Brain"></a>有意识的生物 - Brain</h2><p>回顾 GoalSelector 系统，我们有什么呢：</p><ul><li>简单的生命周期 - 大多数时候够用，可如果让羊晚上去睡觉呢？</li><li>简单的协调机制 - 通过优先度进行替换，Goal 之间难以沟通</li><li>缺失的持久化</li></ul><p>为解决这些痛点，村民更新后出现了一套新的系统：Brain、Sensor（知觉）、Memory（记忆）、Behavior（行为）、Activity（活动）、Schedule（计划）。</p><p>核心自然是 Brain 了，它负责存储记忆，按照计划管理活动和行为的运行停止，时不时感知环境变化：</p><p><img src="brain.png" alt></p><p>可见，一个行为被激活的要求变得更多了：需要有对应的记忆，也需要正在进行对应的活动。</p><h3 id="Memory"><a href="#Memory" class="headerlink" title="Memory"></a>Memory</h3><p>Memory，也就是记忆，补齐了 Goal 系统中最大的不足：没有持久化。</p><p>想象一下，假如你是一个铁匠，一个玩家<del>不死人</del>打了你一下，如果退出重进之后你就忘了，那也太便宜他了。</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">MemoryModuleType</span><span class="token operator">&lt;</span>U<span class="token operator">></span> <span class="token punctuation">{</span>  <span class="token keyword">public</span> <span class="token function">MemoryModuleType</span><span class="token punctuation">(</span>Optional<span class="token operator">&lt;</span>Codec<span class="token operator">&lt;</span>U<span class="token operator">>></span> codec<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Brain</span><span class="token operator">&lt;</span>E <span class="token keyword">extends</span> <span class="token class-name">LivingEntity</span><span class="token operator">></span> <span class="token punctuation">{</span>    <span class="token keyword">public</span> <span class="token keyword">boolean</span> <span class="token function">hasMemoryValue</span><span class="token punctuation">(</span>MemoryModuleType<span class="token operator">&lt;</span><span class="token operator">?</span><span class="token operator">></span> type<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">public</span> <span class="token operator">&lt;</span>U<span class="token operator">></span> <span class="token keyword">void</span> <span class="token function">eraseMemory</span><span class="token punctuation">(</span>MemoryModuleType<span class="token operator">&lt;</span>U<span class="token operator">></span> type<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">public</span> <span class="token operator">&lt;</span>U<span class="token operator">></span> <span class="token keyword">void</span> <span class="token function">setMemory</span><span class="token punctuation">(</span>MemoryModuleType<span class="token operator">&lt;</span>U<span class="token operator">></span> type<span class="token punctuation">,</span> <span class="token annotation punctuation">@Nullable</span> U value<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">public</span> <span class="token operator">&lt;</span>U<span class="token operator">></span> <span class="token keyword">void</span> <span class="token function">setMemoryWithExpiry</span><span class="token punctuation">(</span>MemoryModuleType<span class="token operator">&lt;</span>U<span class="token operator">></span> type<span class="token punctuation">,</span> U value<span class="token punctuation">,</span> <span class="token keyword">long</span> ticks<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">public</span> <span class="token operator">&lt;</span>U<span class="token operator">></span> Optional<span class="token operator">&lt;</span>U<span class="token operator">></span> <span class="token function">getMemory</span><span class="token punctuation">(</span>MemoryModuleType<span class="token operator">&lt;</span>U<span class="token operator">></span> type<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>记忆可以被持久化，需要向 <code>MemoryModuleType</code> 的构造方法中传入对应的 <code>Codec</code>。Codec 类中带有一些基础类型的实例，对于更复杂的数据类型比如一个 Map，则可以使用 <code>RecordCodecBuilder</code> 进行构造。对于 Codec 和 <a href="https://github.com/Mojang/DataFixerUpper" target="_blank" rel="noopener">DataFixerUpper</a> 的具体使用方法这里不作更多介绍。</p><p>记忆也可以过期，也就是「遗忘」。只需要在写入时调用 <code>setMemoryWithExpiry</code>，以刻计算。</p><h3 id="Sensor"><a href="#Sensor" class="headerlink" title="Sensor"></a>Sensor</h3><p>Sensor 类似于「眼」，它们每隔一段时间进行感知，并将感知到的结果写入记忆中。</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">abstract</span> <span class="token keyword">class</span> <span class="token class-name">Sensor</span><span class="token operator">&lt;</span>E <span class="token keyword">extends</span> <span class="token class-name">LivingEntity</span><span class="token operator">></span> <span class="token punctuation">{</span>  <span class="token keyword">public</span> <span class="token function">Sensor</span><span class="token punctuation">(</span><span class="token keyword">int</span> scanRate<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">protected</span> <span class="token keyword">abstract</span> <span class="token keyword">void</span> <span class="token function">doTick</span><span class="token punctuation">(</span>ServerLevel level<span class="token punctuation">,</span> E entity<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">public</span> <span class="token keyword">abstract</span> Set<span class="token operator">&lt;</span>MemoryModuleType<span class="token operator">&lt;</span><span class="token operator">?</span><span class="token operator">>></span> <span class="token function">requires</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>唯一的一个构造参数是扫描（<code>doTick</code>）的频率，按游戏刻计算。</p><p>以感知附近拿着小麦的玩家来举例：</p><ul><li><code>requires</code> 代表这个 Sensor 会写入哪些记忆，比如拿着小麦的玩家</li></ul><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> Set<span class="token operator">&lt;</span>MemoryModuleType<span class="token operator">&lt;</span><span class="token operator">?</span><span class="token operator">>></span> <span class="token function">requires</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>  <span class="token keyword">return</span> Set<span class="token punctuation">.</span><span class="token function">of</span><span class="token punctuation">(</span>MemoryModuleType<span class="token punctuation">.</span>TEMPTING_PLAYER<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><ul><li><code>doTick</code> 执行实际的逻辑，并写入记忆；如果并没有搜索到结果，则擦除记忆</li></ul><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">protected</span> <span class="token keyword">void</span> <span class="token function">doTick</span><span class="token punctuation">(</span>ServerLevel level<span class="token punctuation">,</span> LivingEntity entity<span class="token punctuation">)</span> <span class="token punctuation">{</span>  var brain <span class="token operator">=</span> entity<span class="token punctuation">.</span><span class="token function">getBrain</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">for</span> <span class="token punctuation">(</span>var player <span class="token operator">:</span> level<span class="token punctuation">.</span><span class="token function">players</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span>player<span class="token punctuation">.</span><span class="token function">getMainHandItem</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getItem</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">==</span> Items<span class="token punctuation">.</span>WHEAT<span class="token punctuation">)</span> <span class="token punctuation">{</span>      brain<span class="token punctuation">.</span><span class="token function">setMemory</span><span class="token punctuation">(</span>MemoryModuleType<span class="token punctuation">.</span>TEMPTING_PLAYER<span class="token punctuation">,</span> player<span class="token punctuation">)</span><span class="token punctuation">;</span>      <span class="token keyword">return</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span>  <span class="token punctuation">}</span>  brain<span class="token punctuation">.</span><span class="token function">eraseMemory</span><span class="token punctuation">(</span>MemoryModuleType<span class="token punctuation">.</span>TEMPTING_PLAYER<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h3 id="Behavior"><a href="#Behavior" class="headerlink" title="Behavior"></a>Behavior</h3><p>Behavior 的设计与 Goal 类似，主要有三点不同：</p><ul><li>构造参数提供了一个 <code>entryCondition</code>，用于判断某个记忆存在（<code>VALUE_PRESENT</code>）、不存在（<code>VALUE_ABSENT</code>）或者无所谓（<code>REGISTERED</code>）。这样的设计分离了进入逻辑 —— 它被转移到 Sensor 了。不过在行为中自定义进入逻辑也是可以的，覆盖 <code>checkExtraStartConditions</code> 即可。</li><li>行为开始有两个必要的条件，一个是上面的 <code>entryCondition</code> 满足对应的记忆要求，另一个是提供它的 Activity 正激活。</li><li>退出条件增加了一个超时，也就是说每个行为只有 <code>[minDuration, maxDuration]</code> 中随机的一点执行时间，超时或者不能继续进行（<code>canStillUse</code>）都会导致行为停止。<code>timedOut</code> 方法可以重写，比如「浮在水面上」我们一般不希望让它超时。</li></ul><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">abstract</span> <span class="token keyword">class</span> <span class="token class-name">Behavior</span><span class="token operator">&lt;</span>E <span class="token keyword">extends</span> <span class="token class-name">LivingEntity</span><span class="token operator">></span> <span class="token punctuation">{</span>  <span class="token keyword">public</span> <span class="token function">Behavior</span><span class="token punctuation">(</span>Map<span class="token operator">&lt;</span>MemoryModuleType<span class="token operator">&lt;</span><span class="token operator">?</span><span class="token operator">></span><span class="token punctuation">,</span> MemoryStatus<span class="token operator">></span> entryCondition<span class="token punctuation">,</span>                  <span class="token keyword">int</span> minDuration<span class="token punctuation">,</span> <span class="token keyword">int</span> maxDuration<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">protected</span> <span class="token keyword">void</span> <span class="token function">start</span><span class="token punctuation">(</span>ServerLevel level<span class="token punctuation">,</span> E entity<span class="token punctuation">,</span> <span class="token keyword">long</span> gameTime<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">protected</span> <span class="token keyword">void</span> <span class="token function">tick</span><span class="token punctuation">(</span>ServerLevel level<span class="token punctuation">,</span> E entity<span class="token punctuation">,</span> <span class="token keyword">long</span> gameTime<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">protected</span> <span class="token keyword">void</span> <span class="token function">stop</span><span class="token punctuation">(</span>ServerLevel level<span class="token punctuation">,</span> E entity<span class="token punctuation">,</span> <span class="token keyword">long</span> gameTime<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">protected</span> <span class="token keyword">boolean</span> <span class="token function">canStillUse</span><span class="token punctuation">(</span>ServerLevel level<span class="token punctuation">,</span> E entity<span class="token punctuation">,</span> <span class="token keyword">long</span> gameTime<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">protected</span> <span class="token keyword">boolean</span> <span class="token function">timedOut</span><span class="token punctuation">(</span><span class="token keyword">long</span> gameTime<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">protected</span> <span class="token keyword">boolean</span> <span class="token function">checkExtraStartConditions</span><span class="token punctuation">(</span>ServerLevel level<span class="token punctuation">,</span> E entity<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">public</span> <span class="token keyword">enum</span> MemoryStatus <span class="token punctuation">{</span>  VALUE_PRESENT<span class="token punctuation">,</span>  VALUE_ABSENT<span class="token punctuation">,</span>  REGISTERED<span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h3 id="Activity"><a href="#Activity" class="headerlink" title="Activity"></a>Activity</h3><p>Activity 是一系列有优先度的行为，这一点与 Goal 系统很类似。</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Brain</span><span class="token operator">&lt;</span>E <span class="token keyword">extends</span> <span class="token class-name">LivingEntity</span><span class="token operator">></span> <span class="token punctuation">{</span>  <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">setCoreActivities</span><span class="token punctuation">(</span>Set<span class="token operator">&lt;</span>Activity<span class="token operator">></span> activities<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">useDefaultActivity</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">setActiveActivityIfPossible</span><span class="token punctuation">(</span>Activity activity<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">setDefaultActivity</span><span class="token punctuation">(</span>Activity activity<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">addActivity</span><span class="token punctuation">(</span>Activity activity<span class="token punctuation">,</span> <span class="token keyword">int</span> priority<span class="token punctuation">,</span> List<span class="token operator">&lt;</span>Behavior<span class="token operator">&lt;</span>E<span class="token operator">>></span> behaviors<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">addActivityAndRemoveMemoriesWhenStopped</span><span class="token punctuation">(</span>    Activity activity<span class="token punctuation">,</span> List<span class="token operator">&lt;</span>Pair<span class="token operator">&lt;</span>Integer<span class="token punctuation">,</span> Behavior<span class="token operator">&lt;</span>E<span class="token operator">>>></span> behaviors<span class="token punctuation">,</span>    Set<span class="token operator">&lt;</span>Pair<span class="token operator">&lt;</span>MemoryModuleType<span class="token operator">&lt;</span><span class="token operator">?</span><span class="token operator">></span><span class="token punctuation">,</span> MemoryStatus<span class="token operator">>></span> memoryRequirements<span class="token punctuation">,</span>    Set<span class="token operator">&lt;</span>MemoryModuleType<span class="token operator">&lt;</span><span class="token operator">?</span><span class="token operator">>></span> memoryToErase<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">public</span> <span class="token keyword">boolean</span> <span class="token function">isActive</span><span class="token punctuation">(</span>Activity activity<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>Brain 中有两类活动，<code>Core</code>（核心）和其他的活动。核心活动是一系列任何时候都激活的活动，一般类似「浮在水面上」这种行为就放在里面（<code>setCoreActivities</code>）。其他添加的活动中，有且只有一个会被激活。Activity 类中提供了一些常用的活动，比如 <code>IDLE</code>，这也是默认的活动。</p><p>类似行为，活动可以有需要满足的记忆条件（<code>memoryRequirements</code>），因此 Sensor 也可以控制活动。活动也可以指定退出时自动擦除某些记忆（<code>memoryToErase</code>）。如果除了核心活动以外的活动都不满足条件，则默认的活动（<code>setDefaultActivity</code>）被激活。</p><p>可能与直觉不同的是，活动退出时，正在运行的行为不会结束。</p><h3 id="Schedule"><a href="#Schedule" class="headerlink" title="Schedule"></a>Schedule</h3><p>Schedule 是用于按照游戏时间定时切换活动的系统。激活计划需要手动添加一个行为 <code>UpdateActivityFromSchedule</code>，计划如何编写可以直接参考 Schedule 类中的代码。</p><p>村民便是使用这套计划系统来进行白天劳作、晚上睡觉。</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Brain</span><span class="token operator">&lt;</span>E <span class="token keyword">extends</span> <span class="token class-name">LivingEntity</span><span class="token operator">></span> <span class="token punctuation">{</span>  <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">setSchedule</span><span class="token punctuation">(</span>Schedule schedule<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><h2 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h2><p>无论是 Goal 还是 Brain 系统的设计，都无法很好地处理「事件」，类似被玩家攻击的事件是通过一个每刻进行刷新的字段实现的。</p><p>如果想要很好地设计能够处理事件的 AI，可以参考这篇文章：</p><p><a href="https://cs.uns.edu.ar/~ragis/Agis%20et%20al.%20(2020)%20-%20An%20event-driven%20behavior%20trees%20extension%20to%20facilitate%20non-player%20multi-agent%20coordination%20in%20video%20games.pdf" target="_blank" rel="noopener">An event-driven behavior trees extension to facilitate non-player multi-agent coordination in video games.</a> DOI:10.1016/j.eswa.2020.113457.</p><p>所以你知道为什么一只羊不会停下吃草抬头望着你了吗？</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;一只羊停下吃草，抬头望着你… 这其实不会发生。&lt;/p&gt;
&lt;p&gt;本文介绍 Minecraft 实体相关的目标选择器（Goal Selector）、脑（Brain）等内容，基于 Minecraft 1.18 版本，使用官方映射表。&lt;/p&gt;
&lt;h2 id=&quot;有感知的实体-Goal
      
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>Minecraft 服务端开发指北</title>
    <link href="https://izzel.io/2021/11/13/how-to-minecraft-server/"/>
    <id>https://izzel.io/2021/11/13/how-to-minecraft-server/</id>
    <published>2021-11-13T21:29:57.000Z</published>
    <updated>2025-07-13T12:15:43.524Z</updated>
    
    <content type="html"><![CDATA[<p>本文介绍 <a href="https://github.com/IzzelAliz/Arclight" target="_blank" rel="noopener">Arclight</a> 和其他常见 Minecraft 服务端原理及关键技术。</p><p>文章信息密度较高，读者请善用搜索引擎。</p><h2 id="起因：我们想-…"><a href="#起因：我们想-…" class="headerlink" title="起因：我们想 …"></a>起因：我们想 …</h2><p>在 Minecraft 支持多人模式后，玩家们发现了其面向更多用户时存在的不足。这可能是权限管理的缺失，易用性不足，或是缺少一些娱乐性的功能。</p><p>面对这些不足，开发者们自然会想到一件事：改代码，可是 Minecraft 是一款商业程序，并不向用户开放源代码，我们也不能直接发布这些代码。</p><p>可以说，这些商业程序的限制决定了 Minecraft 模组/服务端特殊的开发流程。</p><p>经过长时间的摸索，社区逐渐形成了两种主流的「改代码」方式。</p><p>较为常见的方式是，反编译 Minecraft 并在此基础上修改添加功能，代表性的项目有：</p><ul><li><a href="https://bukkit.org/pages/about-us" target="_blank" rel="noopener">CraftBukkit</a>, <a href="https://www.spigotmc.org/" target="_blank" rel="noopener">Spigot</a>, <a href="https://papermc.io/" target="_blank" rel="noopener">Paper</a> 和它们的衍生服务端。</li><li><a href="https://github.com/MinecraftForge/MinecraftForge" target="_blank" rel="noopener">MinecraftForge</a></li><li>MCPC, Cauldron, Thermos 和大部分 Forge Bukkit 混合服务端。</li></ul><p>另一种方式是，在 Minecraft 服务端启动时动态地修改加载的代码，代表性的项目有：</p><ul><li><p><a href="https://github.com/IzzelAliz/Arclight" target="_blank" rel="noopener">Arclight</a></p></li><li><p><a href="https://www.spongepowered.org/" target="_blank" rel="noopener">Sponge</a></p></li><li><p><a href="https://fabricmc.net/" target="_blank" rel="noopener">Fabric</a>, <a href="http://www.liteloader.com/" target="_blank" rel="noopener">LiteLoader</a></p></li><li><p><a href="https://github.com/MinecraftForge/MinecraftForge" target="_blank" rel="noopener">MinecraftForge</a>*</p><p>  Minecraft 1.12 及之前版本的 MinecraftForge 同时利用了这两种技术，即对反编译的源码进行修改，并在启动时加载这些修改。该项目同时自带一些「CoreMod」用于运行时修改代码，例如 AccessTransformer。</p></li></ul><h2 id="反编译与反混淆"><a href="#反编译与反混淆" class="headerlink" title="反编译与反混淆"></a>反编译与反混淆</h2><blockquote><p>本文不提供 Minecraft 游戏文件分发，所述内容仅供学习参考。</p></blockquote><p>Minecraft 作为一款商业游戏，使用了混淆技术来保护其不被轻易地盗用。我们以 1.17.1 版本为例，可以在 <a href="https://launcher.mojang.com/v1/objects/a16d67e5807f57fc4e550299cf20226194497dc2/server.jar" target="_blank" rel="noopener">该链接</a> 下载到对应的服务端文件。</p><p>打开这个 JAR 文件后，我们可以看到以下内容：</p><p><img src="server_files.png" alt></p><p>其中的文件夹是 Minecraft 引用的第三方库文件和其自身使用的资源文件，而剩余的 <code>.class</code> 文件即为 Minecraft 的代码文件。</p><p>现在我们遇到了第一个问题，<code>.class</code> 文件并不是源代码，不能直接阅读。对此，社区会使用反编译器进行代码文件的逆向工程，常用的反编译器为 <a href="https://github.com/fesh0r/fernflower" target="_blank" rel="noopener">FernFlower</a>，被上文介绍的所有基于反编译修改的项目采用。<br><a href="https://github.com/Col-E/Recaf" target="_blank" rel="noopener">Recaf</a> 提供了 FernFlower 的图形界面程序。</p><p>我们拿 <code>abv.class</code> 举例，反编译后如下：</p><p><img src="obf_decompile.png" alt></p><p>显而易见我们遇到了第二个问题，所有的方法和字段都被<strong>混淆</strong>了，需要一种方法来将它变得人类可读。例如，上文的 <code>abv</code> 实际上的类名为 <code>Ticket</code>。</p><p>在 Minecraft 1.14.4 以前，社区采用的办法是，根据自身的开发经验，猜出或者说给出这些类混淆前的名称。类文件中的字符串常量、继承和调用关系无不暗示着这些类本来的用途，经验丰富的开发者便可以通过这些蛛丝马迹猜出它们本来的名称。而这些从混淆后到混淆前的名称的映射，我们称之为混淆表或者映射表。</p><p>社区一共总结了三套常用的混淆表，它们分别是</p><ul><li><a href="http://export.mcpbot.bspk.rs/" target="_blank" rel="noopener">MCP</a> 被 Forge 项目和大部分混合服务端采用</li><li><a href="https://github.com/FabricMC/yarn" target="_blank" rel="noopener">Yarn</a> 被 Fabric 项目采用，Paper 项目部分使用了它的参数名</li><li><a href="https://hub.spigotmc.org/stash/projects/SPIGOT/repos/builddata/browse" target="_blank" rel="noopener">BuildData</a> 被 Spigot 项目采用</li></ul><p>自 Minecraft 1.14.4 起，微软开始公布 Minecraft 的官方混淆表，声称其能帮助 Modding 社区更好发展。官方映射已被 Arclight、Forge、Sponge 和 Paper 项目采用，Spigot 项目部分采用了官方映射的字段名用于开发。Fabric 侧允许用户使用官方表，但并非默认。</p><p>因为微软仅公布了类名、方法名与字段名的混淆数据，并未公布方法参数名称，因此 <a href="https://github.com/ParchmentMC/Parchment" target="_blank" rel="noopener">Parchment</a> 项目成立，仅提供参数名称。</p><p>反混淆技术的具体实现细节位于<a href="#反混淆的实现">反混淆的实现</a>节。</p><p>对于使用反编译后的代码修改进行开发的项目，因为反编译器不够完善，反编译的代码通常充满错误而不能直接编译，我们还需要一个额外的步骤：手动修复反编译的错误。</p><p>对于 Forge 项目，所有的修复代码和工具提供自 <a href="https://github.com/MinecraftForge/MCPConfig" target="_blank" rel="noopener">MCPConfig</a> 项目，而该项目的前身是 <a href="http://www.modcoderpack.com/" target="_blank" rel="noopener">ModCoderPack</a>。MCPConfig 项目包含了每个反编译源代码文件的 <code>patch</code>。对于 Spigot 项目，这些修复直接包含在功能实现的 patch 中。</p><h2 id="避免分发官方代码"><a href="#避免分发官方代码" class="headerlink" title="避免分发官方代码"></a>避免分发官方代码</h2><p>分发问题通常只出现在使用反编译代码进行修改的项目上。知名的 CraftBukkit 项目因为直接提供修改后的 Minecraft 代码和二进制文件而受到 <a href="https://github.com/github/dmca/blob/master/2014/2014-09-05-CraftBukkit.md" target="_blank" rel="noopener">DMCA Takedown</a> 请求，随后社区对这种情形进行了规避。</p><p>一共有两种情况可能会涉及到 Minecraft 代码文件的分发：通过网络与他人共同开发一个项目，以及将这个项目最终分发给用户。社区对这两种情况都进行了规避。</p><p>在开发阶段，互联网上公开发布的是 <code>patch</code> 文件，包含了对源码修改的描述。<code>patch</code> 文件如下所示：</p><p><img src="diff.png" alt></p><p>该文件描述了 <code>pom.xml</code> 文件从第一行开始后的 11 行内，应该删去哪些内容再加入哪些内容。通过 <code>patch</code> 文件，开发者可以避免发布整个源代码，而仅仅发布修改的片段。</p><p><code>patch</code> 文件可以通过比较修改前和修改后的文件来生成，也可以用 <code>patch</code> 文件结合修改前的文件生成修改后的文件。</p><p>在 Minecraft 1.13 以前，Forge 模组在开发前需要输入 <code>gradle setupDecompWorkspace</code>，该命令实际上便是进行了 Minecraft 文件的下载、反混淆、反编译、源码修复与应用 <code>patch</code> 文件。Minecraft 1.13 后 Forge 团队优化了工具链，从此开发者不需要手动运行这个任务了。</p><p>而面向最终用户，社区同样不能将他们修改过的游戏 JAR 文件直接发布。</p><p>Forge 项目与 Paper 项目选择发布一种类似 <code>patch</code> 文件的 <code>binpatch</code>，描述了对于二进制文件的修改内容，从而避免了直接分发。</p><p>Spigot 项目选择了另一种方式，发布 <a href="https://www.spigotmc.org/wiki/buildtools/" target="_blank" rel="noopener">BuildTools</a> 让用户自行构建最终的 JAR 文件。BuildTools 隐藏了所有的开发环境细节，使得任何一个安装了 Java 的普通人可以通过一个命令构建出 Spigot，而他只需要等<strong>一会儿</strong>。</p><h2 id="事件模型"><a href="#事件模型" class="headerlink" title="事件模型"></a>事件模型</h2><p>我们已经知道了怎么修改 Minecraft 的代码了，接下来继续关注如何实现功能，并向其他开发者开放。本文只关注服务端侧的功能。</p><p>对原版代码的修改，大部分情况下可以概括成「在某一行前执行我们的代码，修改一些变量，决定是否继续执行」，比如：</p><ul><li>让玩家不能使用 <code>/talk</code> 命令：在执行 <code>/talk</code> 命令的代码前，决定不执行接下来的代码</li><li>让玩家用腐肉烧出皮革：在熔炉熔炼结束的代码前，修改产物为皮革，并继续执行</li></ul><p>这样的模型被归纳为事件模型。每个事件提供了在事件发生时获取上下文信息（比如执行的命令是 <code>/talk</code>），执行外部代码，修改上下文和取消执行的能力。</p><p>上文介绍的几乎所有项目都采用了事件模型为第三方开发者提供拓展和修改 Minecraft 自带逻辑的 API。Fabric 项目提供了 <code>Callback</code> 作为 API，也可以看做另一种形式的事件。</p><p>通过提供丰富的事件，例如玩家加入服务器的事件、玩家攻击生物的事件等，各大开发框架得以更好的为第三方开发者服务。一个统一的事件同样避免了多个第三方开发者同时修改一个地方的代码而引入潜在的冲突。</p><h2 id="实现功能：从类说起"><a href="#实现功能：从类说起" class="headerlink" title="实现功能：从类说起"></a>实现功能：从类说起</h2><p>对于基于反编译修改的项目来说，实现一个事件很简单：在对应的代码出写一行调用事件的代码就可以了。</p><p>而对于运行时修改的项目，情况就变得复杂一些，有两个东西必须介绍：类加载器与类修改技术。</p><p>类是如何加载的呢？以 URLClassLoader 为例，在进行类加载时，类加载器首先查找已经加载的类，如果不存在则请求父加载器加载，如果不存在则尝试自己加载。这样的加载模型叫做双亲委派，伪代码如下：</p><pre class="line-numbers language-java"><code class="language-java">Class<span class="token operator">&lt;</span><span class="token operator">?</span><span class="token operator">></span> <span class="token function">loadClass</span><span class="token punctuation">(</span>String name<span class="token punctuation">)</span> <span class="token keyword">throws</span> ClassNotFoundException <span class="token punctuation">{</span>    <span class="token comment" spellcheck="true">// First, check if the class has already been loaded</span>    Class<span class="token operator">&lt;</span><span class="token operator">?</span><span class="token operator">></span> c <span class="token operator">=</span> <span class="token function">findLoadedClass</span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span>c <span class="token operator">==</span> null<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">try</span> <span class="token punctuation">{</span>            c <span class="token operator">=</span> parent<span class="token punctuation">.</span><span class="token function">loadClass</span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">ClassNotFoundException</span> e<span class="token punctuation">)</span> <span class="token punctuation">{</span>            <span class="token comment" spellcheck="true">// ClassNotFoundException thrown if class not found</span>            <span class="token comment" spellcheck="true">// from the parent class loader</span>        <span class="token punctuation">}</span>        <span class="token keyword">if</span> <span class="token punctuation">(</span>c <span class="token operator">==</span> null<span class="token punctuation">)</span> <span class="token punctuation">{</span>            <span class="token comment" spellcheck="true">// If still not found, then invoke findClass in order</span>            <span class="token comment" spellcheck="true">// to find the class.</span>            c <span class="token operator">=</span> <span class="token function">findClass</span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token punctuation">}</span>    <span class="token punctuation">}</span>    <span class="token keyword">return</span> c<span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>如果我们可以在「自己加载」这一阶段，对即将加载的类文件进行一些修改，便可以达成「改代码」的目标。于是我们可以写出这样的代码：</p><pre class="line-numbers language-java"><code class="language-java">Class<span class="token operator">&lt;</span><span class="token operator">?</span><span class="token operator">></span> <span class="token function">findClass</span><span class="token punctuation">(</span>String name<span class="token punctuation">)</span> <span class="token keyword">throws</span> ClassNotFoundException <span class="token punctuation">{</span>    <span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span> bytes <span class="token operator">=</span> <span class="token function">loadFromDisk</span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span>bytes <span class="token operator">==</span> null<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">ClassNotFoundException</span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span>    bytes <span class="token operator">=</span> <span class="token function">transformClass</span><span class="token punctuation">(</span>bytes<span class="token punctuation">)</span><span class="token punctuation">;</span>    Class<span class="token operator">&lt;</span><span class="token operator">?</span><span class="token operator">></span> c <span class="token operator">=</span> <span class="token function">defineClass</span><span class="token punctuation">(</span>name<span class="token punctuation">,</span> bytes<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">return</span> c<span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p><code>defineClass</code> 将字节流的类文件转换为内存中可用的 Java 类，这一过程由 Java 虚拟机（JVM）执行，我们不需要知道其原理也无法干涉。自然，奥秘包含在 <code>transformClass</code> 方法中，至此我们终于可以开始介绍类修改技术。</p><p>尽管不像 <code>.java</code> 文件一样可供程序员直接阅读，<code>.class</code> 是有规范格式和结构的文件。本文不对类文件格式做详细介绍，仅描述我们关心的地方。</p><p>我们可以使用 <code>javap -c abv.class</code> 查看前文所述 abv 类的类文件内容（部分内容已省略）：</p><pre><code>public final class abv&lt;T extends java.lang.Object&gt; implements java.lang.Comparable&lt;abv&lt;?&gt;&gt; {  ...  public int a(abv&lt;?&gt;);    Code:       0: aload_0       1: getfield      #33                 // Field b:I       4: aload_1       5: getfield      #33                 // Field b:I       8: invokestatic  #47                 // Method java/lang/Integer.compare:(II)I       ...       62: ireturn  ...</code></pre><p>类文件中我们最关心的「逻辑」存在于方法的 <code>Code</code> 属性中，为操作码（Opcode）序列的形式。在反编译修改的项目中，我们说「在某一行插入我们自己的代码」，对应到类文件也就是在某个 Opcode 处加入我们自己的 Opcode 序列。</p><p>操作类文件时，我们通常使用 <a href="https://asm.ow2.io/" target="_blank" rel="noopener">ASM</a> 库解析 <code>.class</code>。ASM 库有两套面向开发者的 API，通常称为 Core API 和 Tree API。Core API 使用 Visitor 模式，这种编程范式尤其适合对固定结构的数据提供多种操作。Tree API 相对易于理解，也可能更易于操作，同时性能相较 Core API 略低。</p><p>接下来两个小节将会展示对于类修改的基本理念。</p><h3 id="反混淆的实现"><a href="#反混淆的实现" class="headerlink" title="反混淆的实现"></a>反混淆的实现</h3><p>前文所述，反混淆可以将 <code>abv</code> 变成 <code>Ticket</code>，那么这是怎么做到的呢？</p><p>我们首先介绍类文件的「签名」，也即类型在 JVM 中的表示方法。</p><table><thead><tr><th>类型</th><th>签名</th></tr></thead><tbody><tr><td>boolean</td><td>Z</td></tr><tr><td>byte</td><td>B</td></tr><tr><td>char</td><td>C</td></tr><tr><td>short</td><td>S</td></tr><tr><td>int</td><td>I</td></tr><tr><td>long</td><td>J</td></tr><tr><td>float</td><td>F</td></tr><tr><td>double</td><td>D</td></tr><tr><td>java.lang.Object</td><td>Ljava/lang/Object;</td></tr><tr><td>T 的数组类型</td><td>[T</td></tr><tr><td>方法</td><td>(参数签名)返回类型签名</td></tr></tbody></table><p>JVM 中，引用类型的内部表示（internal name）以斜线（<code>/</code>）分割，签名则是在其前后加上 <code>L</code> 和 <code>;</code>。方法的签名则是在括号内写上所有参数签名的拼接，最后加上返回值的签名。因此，方法</p><pre><code>String[][] foo(String s, double d, int[] array)</code></pre><p>的类型签名为 </p><pre><code>(Ljava/lang/String;D[I)[[Ljava/lang/String;</code></pre><p>因此，如果我们想把 <code>abv</code> 翻译成 <code>net.minecraft.server.level.Ticket</code>，就需要将类文件中所有的 <code>Labv;</code> 替换为 <code>Lnet/minecraft/server/level/Ticket;</code>。</p><p>同时，我们也想对字段和方法进行重命名，其原理是类似的，但是我们需要介绍几条特定的 Opcode。</p><p>对于字段，我们会有 <code>GETFIELD</code>, <code>PUTFIELD</code>, <code>GETSTATIC</code>, <code>PUTSTATIC</code> 指令，分别用来读取写入普通字段和静态字段的值。例如，对于 <code>abv</code> 类中的 <code>int b</code> 字段，我们要将其命名为 <code>ticketLevel</code>，则需要将所有的 </p><pre><code>GET/PUTFIELD abv b:I</code></pre><p>翻译成 </p><pre><code>GET/PUTFIELD net/minecraft/server/level/Ticket ticketLevel:I</code></pre><p>类似的，对于方法，我们有 <code>INVOKEVIRTUAL</code>, <code>INVOKESTATIC</code> 等五条指令用来调用方法，将其对应地替换即可。</p><p>如果使用 ASM 库来实现这样的功能，我们可以写出这样的代码（并不完整！）：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">class</span> <span class="token class-name">RemapVisitor</span> <span class="token keyword">extends</span> <span class="token class-name">ClassVisitor</span> <span class="token punctuation">{</span>  <span class="token annotation punctuation">@Override</span>  <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">visit</span><span class="token punctuation">(</span><span class="token keyword">int</span> version<span class="token punctuation">,</span> <span class="token keyword">int</span> access<span class="token punctuation">,</span> String name<span class="token punctuation">,</span> String signature<span class="token punctuation">,</span> String superName<span class="token punctuation">,</span> String<span class="token punctuation">[</span><span class="token punctuation">]</span> interfaces<span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span>name<span class="token punctuation">.</span><span class="token function">equals</span><span class="token punctuation">(</span><span class="token string">"abv"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>      <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">visit</span><span class="token punctuation">(</span>version<span class="token punctuation">,</span> access<span class="token punctuation">,</span> <span class="token string">"net/minecraft/server/level/Ticket"</span><span class="token punctuation">,</span> signature<span class="token punctuation">,</span> superName<span class="token punctuation">,</span> interfaces<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>      <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">visit</span><span class="token punctuation">(</span>version<span class="token punctuation">,</span> access<span class="token punctuation">,</span> name<span class="token punctuation">,</span> signature<span class="token punctuation">,</span> superName<span class="token punctuation">,</span> interfaces<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span>  <span class="token punctuation">}</span>  <span class="token annotation punctuation">@Override</span>  <span class="token keyword">public</span> MethodVisitor <span class="token function">visitMethod</span><span class="token punctuation">(</span><span class="token keyword">int</span> access<span class="token punctuation">,</span> String name<span class="token punctuation">,</span> String descriptor<span class="token punctuation">,</span> String signature<span class="token punctuation">,</span> String<span class="token punctuation">[</span><span class="token punctuation">]</span> exceptions<span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">RemapMethodVisitor</span><span class="token punctuation">(</span>Opcodes<span class="token punctuation">.</span>ASM9<span class="token punctuation">,</span> <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">visitMethod</span><span class="token punctuation">(</span>access<span class="token punctuation">,</span> name<span class="token punctuation">,</span> descriptor<span class="token punctuation">,</span> signature<span class="token punctuation">,</span> exceptions<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token punctuation">}</span>  <span class="token keyword">class</span> <span class="token class-name">RemapMethodVisitor</span> <span class="token keyword">extends</span> <span class="token class-name">MethodVisitor</span> <span class="token punctuation">{</span>    <span class="token annotation punctuation">@Override</span>    <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">visitFieldInsn</span><span class="token punctuation">(</span><span class="token keyword">int</span> opcode<span class="token punctuation">,</span> String owner<span class="token punctuation">,</span> String name<span class="token punctuation">,</span> String descriptor<span class="token punctuation">)</span> <span class="token punctuation">{</span>      <span class="token keyword">if</span> <span class="token punctuation">(</span>owner<span class="token punctuation">.</span><span class="token function">equals</span><span class="token punctuation">(</span><span class="token string">"abv"</span><span class="token punctuation">)</span> <span class="token operator">&amp;&amp;</span> name<span class="token punctuation">.</span><span class="token function">equals</span><span class="token punctuation">(</span><span class="token string">"b"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">visitFieldInsn</span><span class="token punctuation">(</span>opcode<span class="token punctuation">,</span> <span class="token string">"net/minecraft/server/level/Ticket"</span><span class="token punctuation">,</span> <span class="token string">"ticketLevel"</span><span class="token punctuation">,</span> descriptor<span class="token punctuation">)</span><span class="token punctuation">;</span>      <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>        <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">visitFieldInsn</span><span class="token punctuation">(</span>opcode<span class="token punctuation">,</span> owner<span class="token punctuation">,</span> name<span class="token punctuation">,</span> descriptor<span class="token punctuation">)</span><span class="token punctuation">;</span>      <span class="token punctuation">}</span>    <span class="token punctuation">}</span>  <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>实际使用中，一般使用一个 Map 保存所有可能的映射。</p><p>对于「所有可能的映射」，社区一般使用一个文本文件进行表示，而这个文本文件衍生出了很多种格式：</p><ul><li><code>TSRG</code>，由 Forge 项目使用</li><li><code>SRG</code> 及其衍生 <code>CSRG</code>，由 Forge 项目和 Spigot 项目使用</li><li><code>TINY</code>，由 Fabric 项目使用</li></ul><p>以 <code>TSRG</code> 格式举例，对于 <code>abv</code> 类的所有映射，有以下内容：</p><pre><code>abv net/minecraft/server/level/Ticket    a type    b ticketLevel    c key    d createdTick    &lt;init&gt; (Labw;ILjava/lang/Object;)V &lt;init&gt;    a ()Labw; getType    a (Labv;)I compareTo    a (J)V setCreatedTick    b ()I getTicketLevel    b (J)Z timedOut    compareTo (Ljava/lang/Object;)I compareTo    equals (Ljava/lang/Object;)Z equals    hashCode ()I hashCode    toString ()Ljava/lang/String; toString</code></pre><p>以上介绍的反混淆技术，包括对类文件的处理和映射表文件格式的读取，社区都提供了对应的工具。最常用的库是 <a href="https://github.com/md-5/SpecialSource" target="_blank" rel="noopener">SpecialSource</a>，提供了上述所有功能的支持，被广泛的使用于除了 Fabric 的几乎所有项目中。Fabric 社区使用 <a href="https://github.com/FabricMC/tiny-remapper" target="_blank" rel="noopener">tiny-remapper</a>。Cadix Dev Team 提供了一系列用于各种字节码操作的库，<a href="https://github.com/CadixDev/Lorenz" target="_blank" rel="noopener">Lorenz</a> 可用于表示映射，<a href="https://github.com/CadixDev/Atlas" target="_blank" rel="noopener">Atlas</a> 可用于反混淆类文件。</p><p>除了对类文件进行反混淆外，社区同样开发了对源代码进行反混淆的软件，如 <a href="https://github.com/MinecraftForge/Srg2Source" target="_blank" rel="noopener">Srg2Source</a> 和 <a href="https://github.com/CadixDev/Mercury" target="_blank" rel="noopener">Mercury</a>。</p><h3 id="禅与代码注入技术"><a href="#禅与代码注入技术" class="headerlink" title="禅与代码注入技术"></a>禅与代码注入技术</h3><p>反混淆仅仅做了对类已有代码的重命名，在大多数情况下，这并不能满足开发者对添加功能的需要。</p><p>我们对一个常见的功能举例分析：每 Tick 执行任务。理想情况下，我们只需要在服务端的大循环的任意一个地方插入一行 <code>MyTask.run()</code> 就可以了。</p><p>假设服务端的循环如下（经过修改）：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">MinecraftServer</span> <span class="token punctuation">{</span>  <span class="token keyword">protected</span> <span class="token keyword">void</span> <span class="token function">runServer</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token keyword">while</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>running<span class="token punctuation">)</span> <span class="token punctuation">{</span>      <span class="token keyword">long</span> i <span class="token operator">=</span> Util<span class="token punctuation">.</span><span class="token function">getMillis</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token keyword">this</span><span class="token punctuation">.</span>nextTickTime<span class="token punctuation">;</span>      <span class="token keyword">if</span> <span class="token punctuation">(</span>i <span class="token operator">></span> 2000L <span class="token operator">&amp;&amp;</span> <span class="token keyword">this</span><span class="token punctuation">.</span>nextTickTime <span class="token operator">-</span> <span class="token keyword">this</span><span class="token punctuation">.</span>lastOverloadWarning <span class="token operator">>=</span> 15000L<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">long</span> j <span class="token operator">=</span> i <span class="token operator">/</span> 50L<span class="token punctuation">;</span>        LOGGER<span class="token punctuation">.</span><span class="token function">warn</span><span class="token punctuation">(</span><span class="token string">"Can't keep up! Is the server overloaded? Running {}ms or {} ticks behind"</span><span class="token punctuation">,</span> i<span class="token punctuation">,</span> j<span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token keyword">this</span><span class="token punctuation">.</span>nextTickTime <span class="token operator">+=</span> j <span class="token operator">*</span> 50L<span class="token punctuation">;</span>        <span class="token keyword">this</span><span class="token punctuation">.</span>lastOverloadWarning <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>nextTickTime<span class="token punctuation">;</span>      <span class="token punctuation">}</span>      <span class="token keyword">this</span><span class="token punctuation">.</span>nextTickTime <span class="token operator">+=</span> 50L<span class="token punctuation">;</span>      <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">tickServer</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token operator">:</span><span class="token operator">:</span>haveTime<span class="token punctuation">)</span><span class="token punctuation">;</span>      <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">waitUntilNextTick</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span>  <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>我们可能就会想在 <code>this.tickServer(this::haveTime);</code> 前面插入自身的任务调用代码。这个插入调用的代码和上文相当类似（仍然有省略！）：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">class</span> <span class="token class-name">TaskVisitor</span> <span class="token keyword">extends</span> <span class="token class-name">ClassVisitor</span> <span class="token punctuation">{</span>  <span class="token annotation punctuation">@Override</span>  <span class="token keyword">public</span> MethodVisitor <span class="token function">visitMethod</span><span class="token punctuation">(</span><span class="token keyword">int</span> access<span class="token punctuation">,</span> String name<span class="token punctuation">,</span> String descriptor<span class="token punctuation">,</span> String signature<span class="token punctuation">,</span> String<span class="token punctuation">[</span><span class="token punctuation">]</span> exceptions<span class="token punctuation">)</span> <span class="token punctuation">{</span>    MethodVisitor mv <span class="token operator">=</span> <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">visitMethod</span><span class="token punctuation">(</span>access<span class="token punctuation">,</span> name<span class="token punctuation">,</span> descriptor<span class="token punctuation">,</span> signature<span class="token punctuation">,</span> exceptions<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span>name<span class="token punctuation">.</span><span class="token function">equals</span><span class="token punctuation">(</span><span class="token string">"runServer"</span><span class="token punctuation">)</span> <span class="token operator">&amp;&amp;</span> descriptor<span class="token punctuation">.</span><span class="token function">equals</span><span class="token punctuation">(</span><span class="token string">"()V"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>      <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">TaskMethodVisitor</span><span class="token punctuation">(</span>Opcodes<span class="token punctuation">.</span>ASM9<span class="token punctuation">,</span> mv<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>      <span class="token keyword">return</span> mv<span class="token punctuation">;</span>    <span class="token punctuation">}</span>  <span class="token punctuation">}</span>  <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">TaskMethodVisitor</span> <span class="token keyword">extends</span> <span class="token class-name">MethodVisitor</span> <span class="token punctuation">{</span>    <span class="token annotation punctuation">@Override</span>    <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">visitMethodInsn</span><span class="token punctuation">(</span><span class="token keyword">int</span> opcode<span class="token punctuation">,</span> String owner<span class="token punctuation">,</span> String name<span class="token punctuation">,</span> String descriptor<span class="token punctuation">,</span> <span class="token keyword">boolean</span> isInterface<span class="token punctuation">)</span> <span class="token punctuation">{</span>      <span class="token keyword">if</span> <span class="token punctuation">(</span>name<span class="token punctuation">.</span><span class="token function">equals</span><span class="token punctuation">(</span><span class="token string">"tickServer"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">visitMethodInsn</span><span class="token punctuation">(</span>Opcodes<span class="token punctuation">.</span>INVOKESTATIC<span class="token punctuation">,</span> <span class="token string">"MyTask"</span><span class="token punctuation">,</span> <span class="token string">"run"</span><span class="token punctuation">,</span> <span class="token string">"()V"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>      <span class="token punctuation">}</span>      <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">visitMethodInsn</span><span class="token punctuation">(</span>opcode<span class="token punctuation">,</span> owner<span class="token punctuation">,</span> name<span class="token punctuation">,</span> descriptor<span class="token punctuation">,</span> isInterface<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span>  <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>我们便出色的完成了「插入代码」的工作 —— 但是！当要插入的代码逐渐变多，为每个调用单独写两个类的工作量变的令人无法接受。社区不断有简化这些重复劳动的尝试，最终出现了一个叫 <a href="https://github.com/SpongePowered/Mixin" target="_blank" rel="noopener">Mixin</a> 的项目。</p><p>使用 Mixin 后，对于上文的代码，我们只需要写：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token annotation punctuation">@Mixin</span><span class="token punctuation">(</span>MinecraftServer<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">)</span><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">MinecraftServerMixin</span> <span class="token punctuation">{</span>  <span class="token annotation punctuation">@Inject</span><span class="token punctuation">(</span>method <span class="token operator">=</span> <span class="token string">"runServer"</span><span class="token punctuation">,</span> at <span class="token operator">=</span> <span class="token annotation punctuation">@At</span><span class="token punctuation">(</span>value <span class="token operator">=</span> <span class="token string">"INVOKE"</span><span class="token punctuation">,</span>           target <span class="token operator">=</span> <span class="token string">"Lnet/minecraft/server/MinecraftServer;tickServer(Ljava/util/function/BooleanSupplier;)V"</span><span class="token punctuation">)</span><span class="token punctuation">)</span>  <span class="token keyword">void</span> <span class="token function">beforeTickServer</span><span class="token punctuation">(</span>CallbackInfo ci<span class="token punctuation">)</span> <span class="token punctuation">{</span>    MyTask<span class="token punctuation">.</span><span class="token function">run</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>而这个代码本身可读性比两个 Visitor 类也高出不少：注入（Inject）方法 runServer，目标（target）为 <code>MinecraftServer:tickServer</code> 的方法调用（INVOKE）。</p><p>Mixin 起源于 LiteLoader 项目，皆由 Mumfrey 开发，随后转入 Sponge 社区并被完整采用。Sponge 的两个服务端可能是最先采用 Mixin 的大型项目。</p><p>本文无意介绍 ASM 与 Mixin 具体的使用方式和更多更复杂的类文件修改技巧。</p><h2 id="不同的运行时环境：重混淆"><a href="#不同的运行时环境：重混淆" class="headerlink" title="不同的运行时环境：重混淆"></a>不同的运行时环境：重混淆</h2><p>作为一个商业程序（似乎已经说过很多遍了），Minecraft 发布时被混淆。混淆不仅使得用户很难查看它的代码，还带来了一个新的问题：不同版本之间，同一个类、同一个方法可能有不同的混淆名字。</p><p>针对这一问题，Forge 和 Fabric 不约而同地采用了一个做法：为相同的类/方法分配相同的名字（序号）。天哪，我们已经有三套名称了！它们分别是：Mojang 发布的混淆名，跨版本稳定的中间名，开发者编写代码使用的人类可读名。</p><p>在 Forge 侧，这样的三套名称分别被称为 <code>notch name</code>, <code>srg name</code> 和 <code>mcp name</code>，所有的 srg 文件位于 MCPConfig 项目；在Fabric 侧，后两套为 <a href="https://github.com/FabricMC/intermediary" target="_blank" rel="noopener">Intermediary</a> 和 Yarn。</p><p>对于不同版本之间不同混淆名的类，我们需要一个工具来对它进行匹配。Forge 社区使用 <a href="https://github.com/MinecraftForge/DePigifier" target="_blank" rel="noopener">DePigifier</a>。</p><p>对于 Forge 和 Fabric，Minecraft 服务器运行时，实际使用的名称便是这一套中间名。</p><p>采取中间名作为实际运行的环境还有另一个好处，不同开发者使用不同的人类可读名开发也可以同时运行（MCP/官方映射/Yarn/?）。同时中间名带来另一个麻烦，开发环境和运行环境完全不一致，编译完成后还需要一次额外的重混淆。</p><p>至此，我们终于可以完整地总结 Forge 和 Paper 这样的反编译修改项目是如何开发的了：</p><ul><li>我们下载了 <code>notch name</code> 的 minecraft_server.1.17.1.jar</li><li>使用 MCPConfig 项目中的 notch-&gt;srg 混淆表反混淆它</li><li>反编译，并用 MCPConfig 中的 patch 修复它</li><li>使用 Srg2Source 把修复完成的源代码反混淆成 MCP 名</li><li>修改源代码，添加功能，并生成 patch 文件</li><li>编译代码，将编译结果重混淆为中间名</li><li>与原始的服务端 JAR 文件比较，生成一个 binpatch 文件</li><li>将包含 binpatch 文件的安装器发布给用户，用户得以启动</li></ul><p>至于像 Sponge 这样的运行时修改项目，则是这样：</p><ul><li>前两步仍然一致</li><li>选择一个人类可读名，继续反混淆</li><li>基于这个反混淆的结果，进行开发；特别地，Sponge 采用编写 Mixin</li><li>编译，重混淆，发布</li><li>用户启动时，程序动态地修改类</li></ul><h2 id="混合服务端"><a href="#混合服务端" class="headerlink" title="混合服务端"></a>混合服务端</h2><blockquote><p>本人是 Arclight 服务端作者，这里使用 Arclight 举例</p></blockquote><p>混合服务端究竟做了什么呢？</p><p>答案是明显的：在 B 上实现 A 的所有功能。这样功能的实现可以通过任何一种「改代码」手段来完成：Arclight 使用 Mixin，而大多数 Forge Bukkit 服务端使用反编译修改。</p><p>混合服务端还需要处理一件事情，即目标环境和原环境的映射名称可能完全不同。例如，Forge 的运行时名称是 SRG，而 Spigot 的运行时名称是 BuildData 中的名称。对于不同名称的处理可以使用上文所述反混淆/重混淆手段。</p><p>对于从原环境到目标环境的映射，我们需要额外生成一套映射表；也就是说，如果 Forge 的映射表是 notch-&gt;srg，而 Spigot 是 notch-&gt;spigot，我们需要生成 spigot-&gt;srg 的映射。而由于 Java 中存在返回类型协变、方法参数协变逆变、接口方法由实现类的父类方法隐性实现等等因素，导致生成这样的映射表并不是一件简单的事。</p><p><a href="https://github.com/CadixDev/Lorenz" target="_blank" rel="noopener">Lorenz</a> 库可以合并两个映射表并输出，尽管其并不完善。目前来说，可能只有 <a href="https://github.com/ArclightPowered/arclight-gradle-plugin" target="_blank" rel="noopener">arclight-gradle-plugin</a> 有能力正确地生成这样的映射表。</p><p>尽管如此，上述手段无法处理一种情况，也就是 Java 的反射技术。</p><p>Bukkit API 的目标是用其提供的版本无关接口，完成所有对 Minecraft 服务器的控制，但是总有或多或少的理由，开发者绕过这套 API，去直接调用 Minecraft 本身的代码。</p><p>而由于 Spigot 系列服务端会在构建时将 Minecraft 自带类的包名混淆成如同 <code>net.minecraft.server.v1_17_R1</code> 的形式，并且每个版本都不同，所以大部分开发者逐渐总结了一套「接触」Minecraft 原生代码的方法：</p><ul><li>拿到<code>net.minecraft.server.v1_17_R1</code> 这样的包名</li><li>取出其中的版本号节，如 <code>v1_17_R1</code></li><li>拼接出完整类名，比如 <code>&quot;net.minecraft.server.&quot; + version + &quot;.EntityPlayer&quot;</code></li><li>用反射技术操作这个类</li></ul><p>从而让一套代码可以在多个 Spigot 版本下使用。</p><p>这样的方法对于开发者自然是非常方便的，但是对于混合服务端开发者来说，处理这些代码让其正确执行就不大容易了。</p><p>Arclight 将所有反射调用进行了重定向。考虑</p><pre><code>abv net/minecraft/server/level/Ticket    f_9421_ ticketLevel</code></pre><p>就能写出这样的代码：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">static</span> Field <span class="token function">redirectGetField</span><span class="token punctuation">(</span>Class <span class="token class-name">cl</span><span class="token punctuation">,</span> String name<span class="token punctuation">)</span> <span class="token punctuation">{</span>  <span class="token keyword">if</span> <span class="token punctuation">(</span>cl<span class="token punctuation">.</span><span class="token function">getName</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">equals</span><span class="token punctuation">(</span><span class="token string">"net.minecraft.server.level.Ticket"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span>name<span class="token punctuation">.</span><span class="token function">equals</span><span class="token punctuation">(</span><span class="token string">"ticketLevel"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>      name <span class="token operator">=</span> <span class="token string">"f_9421_"</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span>  <span class="token punctuation">}</span>  <span class="token keyword">return</span> cl<span class="token punctuation">.</span><span class="token function">getField</span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>同时在对应的 <code>Field#getName</code> 调用再转换回原本的名称就可以了。</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">static</span> String <span class="token function">redirectGetName</span><span class="token punctuation">(</span>Field f<span class="token punctuation">)</span> <span class="token punctuation">{</span>  <span class="token keyword">if</span> <span class="token punctuation">(</span>f<span class="token punctuation">.</span><span class="token function">getDeclaringClass</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getName</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">equals</span><span class="token punctuation">(</span><span class="token string">"net.minecraft.server.level.Ticket"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span>f<span class="token punctuation">.</span><span class="token function">getName</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">equals</span><span class="token punctuation">(</span><span class="token string">"f_9421_"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>      <span class="token keyword">return</span> <span class="token string">"ticketLevel"</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span>  <span class="token punctuation">}</span>  <span class="token keyword">return</span> f<span class="token punctuation">.</span><span class="token function">getName</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>同样的方法可以适用于类和方法的相关反射调用。</p><p>运行时重混淆技术还有更多有趣的话题：</p><ul><li>插件动态地定义新类</li><li>插件在运行时编译源码</li><li>插件读取 Minecraft 的类文件进行处理</li></ul><p>Arclight 都有对应的处理，文章在此按下不表，可以参考 Arclight 的源码。</p><h2 id="拓展阅读"><a href="#拓展阅读" class="headerlink" title="拓展阅读"></a>拓展阅读</h2><ul><li><a href="https://xfl03.gitee.io/coremodtutor/" target="_blank" rel="noopener">xfl03 的 CoreMod 教程</a> —— 关于「改代码」的方法论</li><li><a href="https://harbinger.covertdragon.team/chapter-01/mcp.html" target="_blank" rel="noopener">Harbinger 第一节</a> 关于 MCP 与 Forge 的介绍</li><li><a href="https://github.com/SpongePowered/Mixin/wiki/Introduction-to-Mixins---The-Mixin-Environment" target="_blank" rel="noopener">Mixin Wiki Environment 节</a> （英）介绍了 Mixin 是如何在运行时处理类的</li><li><a href="https://bdn.tdiant.net/#/unit/5-1" target="_blank" rel="noopener">tdiant 的 Bukkit 开发教程</a> 5-1 节介绍了一般 Bukkit 开发者处理 Minecraft 原生代码的方法</li><li><a href="https://fabricmc.net/" target="_blank" rel="noopener">Fabric 项目官网</a> （英）含有该项目工具链介绍</li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;本文介绍 &lt;a href=&quot;https://github.com/IzzelAliz/Arclight&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Arclight&lt;/a&gt; 和其他常见 Minecraft 服务端原理及关键技术。&lt;/p&gt;
&lt;p&gt;文章信息密
      
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>上线了新的 Maven 仓库</title>
    <link href="https://izzel.io/2020/10/08/new-maven-repo/"/>
    <id>https://izzel.io/2020/10/08/new-maven-repo/</id>
    <published>2020-10-08T21:09:55.000Z</published>
    <updated>2025-07-13T12:15:43.525Z</updated>
    
    <content type="html"><![CDATA[<p>地址 <a href="https://maven.izzel.io" target="_blank" rel="noopener">https://maven.izzel.io</a>，采用 Reposilite。</p><p>如果有需求可以找我要个 token。</p><p>顺便把本博客套了一层 Cloudflare。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;地址 &lt;a href=&quot;https://maven.izzel.io&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://maven.izzel.io&lt;/a&gt;，采用 Reposilite。&lt;/p&gt;
&lt;p&gt;如果有需求可以找我要个 token。&lt;/p
      
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>聊聊区块和 Ticket</title>
    <link href="https://izzel.io/2020/09/09/chunks-and-tickets/"/>
    <id>https://izzel.io/2020/09/09/chunks-and-tickets/</id>
    <published>2020-09-09T22:46:58.000Z</published>
    <updated>2025-07-13T12:15:43.515Z</updated>
    
    <content type="html"><![CDATA[<p>Minecraft 在 1.14 引入了 Ticket 系统来管理区块的加载和卸载。</p><p>本文采用 Minecraft 1.15.2，MCP 的 <code>20200904-1.15.1</code> 映射表写就。</p><p>让我们开始吧。</p><hr><p>有一日，一名玩家百无聊赖下，在 Minecraft 中开始了一次新的冒险，于是万物伊始。</p><p><img src="loading_screen.png" alt></p><p>这个界面，是新的区块加载方式最为直观的体现，稍后我们就会讲解这些颜色对应着什么。</p><p>在 Minecraft 1.14 中，Mojang 引入了 Ticket 用于管理区块是否、何时加载，而一个 Ticket 由以下属性组成：</p><ol><li>一个 <code>net.minecraft.world.server.TicketType</code>，用于表示这个 Ticket 的种类；种类指定了它的存活时间 <code>lifespan</code>，单位是游戏刻；</li><li>一个加载等级 <code>level</code>，用于加载周围区块的距离。</li></ol><h2 id="加载等级-Level"><a href="#加载等级-Level" class="headerlink" title="加载等级 Level"></a>加载等级 Level</h2><p>相信这篇文章的读者，对于区块加载可能有一个感性的认识。</p><p>出生点的区块总是加载，而且范围较大，可能有十多个区块；玩家周围一段距离的区块总是加载，看起来和 <code>view-distance</code> 视距有关。</p><p>实际上，这些加载的距离由加载等级指定。</p><p>游戏中，区块的加载等级以 33 为界，分为四种区块位置类型（<code>net.minecraft.world.server.ChunkHolder.LocationType</code>）：</p><ul><li>大于 33 的加载等级 <code>INACCESSIBLE</code> 不会让区块加载入内存，但世界生成会进行</li><li>33 的加载等级称为 <code>BORDER</code>，仅有少量游戏逻辑会运行</li><li>32 为 <code>TICKING</code>，除了对生物进行 tick 以外，大部分逻辑都会运行</li><li>31 以下为 <code>ENTITY_TICKING</code>，意思是什么想必不需要多说</li></ul><p>游戏内与加载等级对应的是加载距离，算法是 <code>distance = 33 - level</code>，表达的字面意思就是「加载周围的多少个方块」；由于地形生成的存在，实际的加载距离会大于指定的距离。</p><p>加载等级最大是 44，至于为什么是这个数字，我们一会儿再说。</p><p>可以见得，Ticket 的加载等级越低，就会加载更大范围的区块，而区块的加载等级越低，区块进行的计算就会越多，而读者你这时说不定已经猜出了区块的加载计算规则了。</p><h2 id="等级扩散-Level-Propagation"><a href="#等级扩散-Level-Propagation" class="headerlink" title="等级扩散 Level Propagation"></a>等级扩散 Level Propagation</h2><p>当一个 Ticket 被提供给某个区块后，它会向周围的八个区块扩散，加载等级逐渐上升。这个过程被称为 Propagation。</p><p>假设我们往某一个区块提供了一个加载等级为 30 的 Ticket，那么这一片本身全部为 44 的区域，会变成这样：</p><pre><code>   33   33   33   33   33   33   33    33   32   32   32   32   32   33    33   32   31   31   31   32   33    33   32   31   30   31   32   33    33   32   31   31   31   32   33    33   32   32   32   32   32   33    33   33   33   33   33   33   33 </code></pre><p>而等级换算成对应的区块位置类型，游戏就会按照规则计算区块中的活动。</p><p>上面的内容足以描述游戏中的区块加载与卸载，但是对世界生成仍然不能清晰的描述：</p><ul><li>区块生成一定是比玩家的视距更远的，也就是说，<code>INACCESSIBLE</code> 等级的区块仍需更加细分。</li><li>熟悉 Mod 开发的读者可能会知道，<code>IWorld</code> 接口的实现会有两个，分别是 <code>World</code> 和 <code>WorldGenRegion</code>，也就是说，世界生成和游戏运行的世界有一定差距，以至于需要使用两个实现来描述。</li><li>类似的，<code>IChunk</code> 的主要实现同样有两个。</li></ul><p>所以我们可以挖深一些。</p><h2 id="区块状态-ChunkStatus"><a href="#区块状态-ChunkStatus" class="headerlink" title="区块状态 ChunkStatus"></a>区块状态 ChunkStatus</h2><p>在 Minecraft 中，区块的实现有两种，分别是 <code>ChunkPrimer</code> 和 <code>Chunk</code>，或者可以称呼为 <code>PROTOCHUNK</code> 和 <code>LEVELCHUNK</code>。</p><p>前者用于世界生成，而后者用于正常的游戏逻辑。</p><p>Minecraft 的世界生成是分阶段的，我们可以想象，游戏先生成了地形，然后放置树，接着…. 区块状态用于表示，在区块完成生成之前的状态。</p><p>游戏中共有 13 种区块状态：</p><table><thead><tr><th>状态</th><th>颜色</th><th>用途</th></tr></thead><tbody><tr><td>empty</td><td><span style="color:#545454"> <strong>#545454</strong> </span></td><td>表达一个空的区块</td></tr><tr><td>structure_starts</td><td><span style="color:#999999"> <strong>#999999</strong> </span></td><td>计算生成结构的位置</td></tr><tr><td>structure_references</td><td><span style="color:#5F6191"> <strong>#5F6191</strong> </span></td><td>保存上一步生成的结构位置</td></tr><tr><td>biomes</td><td><span style="color:#80B252"> <strong>#80B252</strong> </span></td><td>生成生物群系并将它们保存</td></tr><tr><td>noise</td><td><span style="color:#D1D1D1"> <strong>#D1D1D1</strong> </span></td><td>生成世界的基础地形，包括之前的结构</td></tr><tr><td>surface</td><td><span style="color:#726809"> <strong>#726809</strong> </span></td><td>生成地形的表面，以及基岩</td></tr><tr><td>carvers</td><td><span style="color:#6D665C"> <strong>#6D665C</strong> </span></td><td>「凿空」地形，也就是生成洞穴</td></tr><tr><td>liquid_carvers</td><td><span style="color:#303572"> <strong>#303572</strong> </span></td><td>如上，不过是用液体凿空</td></tr><tr><td>features</td><td><span style="color:#21C600"> <strong>#21C600</strong> </span></td><td>进行地形生成的 decoration 阶段</td></tr><tr><td>light</td><td><span style="color:#CCCCCC"> <strong>#CCCCCC</strong> </span></td><td>计算区块的光照</td></tr><tr><td>spawn</td><td><span style="color:#F26060"> <strong>#F26060</strong> </span></td><td>为区块生成最初的一些生物</td></tr><tr><td>heightmaps</td><td><strong>#EEEEEE</strong></td><td>看起来什么也没做</td></tr><tr><td>full</td><td><strong>#FFFFFF</strong></td><td>区块加载完成，从 <code>PROTOCHUNK</code> 转换为 <code>LEVELCHUNK</code></td></tr></tbody></table><p>这些区块状态对应了世界生成的过程，同时它们按照表中的顺序排列；同时，它们各自依赖前一个状态，也就是说，我们请求一个 <code>full</code> 的区块时，这个区块的状态会依次经过 <code>empty</code>, <code>structure_starts</code> …</p><p>以下是在 33 以上的加载等级对应的区块状态：</p><table><thead><tr><th align="left">加载等级</th><th align="left">ChunkStatus</th></tr></thead><tbody><tr><td align="left">33-</td><td align="left">full</td></tr><tr><td align="left">34</td><td align="left">features</td></tr><tr><td align="left">35</td><td align="left">liquid_carvers</td></tr><tr><td align="left">36-43</td><td align="left">structure_starts</td></tr><tr><td align="left">44+</td><td align="left">empty</td></tr></tbody></table><p>可以看到，Minecraft 并不是沿着加载等级依次排列区块状态的，因此在世界加载时，颜色的显示就并没有那么五彩斑斓了，不过如果愿意，我们仍然可以在这里观察到若干种颜色。</p><p><img src="loading_screen.png" alt></p><p>可以看到，右边多出来的那三个区块，从上到下分别处于 <code>liquid_carvers</code>, <code>surface</code> 和 <code>biomes</code> 阶段。</p><h2 id="Ticket-种类"><a href="#Ticket-种类" class="headerlink" title="Ticket 种类"></a>Ticket 种类</h2><p>Minecraft 中共有 8 种 <code>net.minecraft.world.server.TicketType</code>，它们分别是</p><ul><li><code>start</code>，用于出生点区块的加载，加载距离是 11 格，也就是 22 加载等级；由 43-22 得到 21，因此出生点的这个 Ticket 会在加载世界时一共加载 441 个区块<br>世界生成初始时会注册该 Ticket，世界加载的界面反应了该 Ticket 对区块造成的变化</li><li><code>dragon</code>，在与末影龙战斗时提供给区块，加载 9 格距离</li><li><code>player</code>，自然就是玩家加载区块的方法，加载等级 31<ul><li>读者可能会问，玩家显然加载不止两格区块。实际上游戏会将玩家周围一段距离内的区块全部添加这个 Ticket，而这个距离与 <code>view-distance</code>（服务器）或渲染距离（客户端）有关</li></ul></li><li><code>forced</code>，用于 <code>/forceload</code> 指令和出生点区块的强制加载</li><li><code>light</code>，加载等级 34，在区块状态为 <code>light</code> 时提供给区块，将区块加载以便计算光照</li><li><code>portal</code>，在生成或寻找（这也就包含了生物使用传送门）对应的传送门后提供给区块，加载距离 3，存活 300 游戏刻</li><li><code>post_teleport</code>，将生物传送到对应区块后，将区块保持加载一小段时间（5 gt），这可以让 <code>/tp</code> 指令传送生物后，使生物有机会更新到达的区块</li><li><code>unknown</code>，用于游戏内任意代码调用了 <code>getChunk</code> 后加载区块，比如 <code>getBlockState</code>，使区块加载 1 游戏刻以便获取区块信息</li></ul><p>需要注意的是，加载距离并不由 TicketType 指定，比如 <code>post_teleport</code> 在 <code>/tp</code> 指令使用时加载距离为 1，而其他情况下为 0</p><h2 id="屁话多！如何使用？"><a href="#屁话多！如何使用？" class="headerlink" title="屁话多！如何使用？"></a>屁话多！如何使用？</h2><p>为世界的某个区块添加一个 Ticket 是再简单不过的事：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">import</span> net<span class="token punctuation">.</span>minecraft<span class="token punctuation">.</span>world<span class="token punctuation">.</span>server<span class="token punctuation">.</span>TicketType<span class="token punctuation">;</span><span class="token keyword">this</span><span class="token punctuation">.</span>world<span class="token punctuation">.</span><span class="token function">getChunkProvider</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">registerTicket</span><span class="token punctuation">(</span>    TicketType<span class="token punctuation">.</span>DRAGON<span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token class-name">ChunkPos</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token number">9</span><span class="token punctuation">,</span> Unit<span class="token punctuation">.</span>INSTANCE<span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>或许不限于原版的 TicketType，那么可以自行注册一个：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">import</span> net<span class="token punctuation">.</span>minecraft<span class="token punctuation">.</span>world<span class="token punctuation">.</span>server<span class="token punctuation">.</span>TicketType<span class="token punctuation">;</span><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">final</span> TicketType<span class="token operator">&lt;</span>Unit<span class="token operator">></span> CUSTOM <span class="token operator">=</span>     TicketType<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token string">"some_ticket"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span>a<span class="token punctuation">,</span> b<span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">20</span> <span class="token comment" spellcheck="true">/* 可选的存活时间 */</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p>顺便一提，<code>Unit</code> 是 Mojang 的工具类，用来表示单元值，姑且可以理解为 <code>void</code>。</p><p>还值得一提的是，加入游戏的 <code>Ticket</code> 是不持久化的，也就是说它们会随着游戏重启而消失。</p><p>Bukkit 平台提供了另外两种 <code>TicketType</code>，看起来是这样的：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">final</span> TicketType<span class="token operator">&lt;</span>Unit<span class="token operator">></span> PLUGIN <span class="token operator">=</span>    <span class="token function">create</span><span class="token punctuation">(</span><span class="token string">"plugin"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span>a<span class="token punctuation">,</span> b<span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">final</span> TicketType<span class="token operator">&lt;</span>org<span class="token punctuation">.</span>bukkit<span class="token punctuation">.</span>plugin<span class="token punctuation">.</span>Plugin<span class="token operator">></span> PLUGIN_TICKET <span class="token operator">=</span>    <span class="token function">create</span><span class="token punctuation">(</span><span class="token string">"plugin_ticket"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span>a<span class="token punctuation">,</span> b<span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> a<span class="token punctuation">.</span><span class="token function">getClass</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getName</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">compareTo</span><span class="token punctuation">(</span>b<span class="token punctuation">.</span><span class="token function">getClass</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getName</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p>前一种用于实现高版本的 <code>chunk-gc.period-in-ticks</code> 设置，具体细节就不过多说明了。</p><p>后一种则是提供给插件的，用于强制加载区块的方法，具体的 API 可以如下调用：</p><pre class="line-numbers language-java"><code class="language-java">World world <span class="token operator">=</span> Bukkit<span class="token punctuation">.</span><span class="token function">getWorld</span><span class="token punctuation">(</span><span class="token string">"..."</span><span class="token punctuation">)</span><span class="token punctuation">;</span>world<span class="token punctuation">.</span><span class="token function">addPluginTicket</span><span class="token punctuation">(</span>chunkX<span class="token punctuation">,</span> chunkZ<span class="token punctuation">,</span> plugin<span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>这会给对应区块一个加载等级 31 的 Ticket，以防止区块被卸载。</p><h2 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h2><p>Mojang 实际上提供了一套显示世界区块的加载等级和位置类型的渲染器，不过不能通过游戏内的方式启用，可以手动调用 <code>net.minecraft.client.renderer.debug.ChunkInfoDebugRenderer</code> 来渲染。</p><p><img src="debug_renderer.png" alt></p><p>由此图可以轻松找到加载等级 22 的区块为我们需要的加载区块。</p><p>由于玩家会加载区块，因此我们需要设置游戏规则 <code>/gamerule spectatorsGenerateChunks false</code>，同时切换到旁观者模式，以阻止玩家自身的 Ticket。</p><p><img src="chunk_border.png" alt></p><p>由此可以看到一个 Ticket 的加载边界，位置等级逐渐降低。</p><h2 id="关于谁加载了区块"><a href="#关于谁加载了区块" class="headerlink" title="关于谁加载了区块"></a>关于谁加载了区块</h2><p>其实本文到此本应结束了，但是在举办 TeaCon 2020 时，我们正好遇到了和这篇文章或许扯得上关系的一件事，因此一并在此记下。</p><p>事件的经过可以查看 sj 博客的<a href="https://blog.seraphjack.top/posts/2020/08/teacon-2020-server-attack/" target="_blank" rel="noopener">这篇文章</a>。</p><p>攻击刚刚开始时，我们发现服务器卡死，线程转储指向了 Chunk IO Worker，也就是说区块加载卡住了。同时世界文件夹中出现了大量数值特别大的 <code>.mca</code> 文件，因此可以判定有人在恶意利用漏洞加载区块。</p><p>但是 Forge 提供的加载区块事件是在 Chunk IO Worker 上调用的，因此异常栈信息并不能告诉我们是谁请求了这次区块加载。</p><p>经过研究发现，区块的加载等级更新逻辑位于 <code>ChunkHolder#processUpdates</code>，并且是同步调用的，因此假如可以在这里设置一个断点，那么就可以找到哪些逻辑在加载区块了。</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">protected</span> <span class="token keyword">void</span> <span class="token function">processUpdates</span><span class="token punctuation">(</span>ChunkManager chunkManagerIn<span class="token punctuation">)</span> <span class="token punctuation">{</span>   ChunkStatus chunkstatus <span class="token operator">=</span> <span class="token function">getChunkStatusFromLevel</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>prevChunkLevel<span class="token punctuation">)</span><span class="token punctuation">;</span>   ChunkStatus chunkstatus1 <span class="token operator">=</span> <span class="token function">getChunkStatusFromLevel</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>chunkLevel<span class="token punctuation">)</span><span class="token punctuation">;</span>   <span class="token keyword">boolean</span> flag <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>prevChunkLevel <span class="token operator">&lt;=</span> ChunkManager<span class="token punctuation">.</span>MAX_LOADED_LEVEL<span class="token punctuation">;</span>   <span class="token keyword">boolean</span> flag1 <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>chunkLevel <span class="token operator">&lt;=</span> ChunkManager<span class="token punctuation">.</span>MAX_LOADED_LEVEL<span class="token punctuation">;</span>   ChunkHolder<span class="token punctuation">.</span>LocationType chunkholder$locationtype <span class="token operator">=</span> <span class="token function">getLocationTypeFromLevel</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>prevChunkLevel<span class="token punctuation">)</span><span class="token punctuation">;</span>   ChunkHolder<span class="token punctuation">.</span>LocationType chunkholder$locationtype1 <span class="token operator">=</span> <span class="token function">getLocationTypeFromLevel</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>chunkLevel<span class="token punctuation">)</span><span class="token punctuation">;</span>   <span class="token keyword">if</span> <span class="token punctuation">(</span>flag<span class="token punctuation">)</span> <span class="token punctuation">{</span>      <span class="token comment" spellcheck="true">// unload</span>   <span class="token punctuation">}</span>   <span class="token keyword">boolean</span> flag5 <span class="token operator">=</span> chunkholder$locationtype<span class="token punctuation">.</span><span class="token function">isAtLeast</span><span class="token punctuation">(</span>ChunkHolder<span class="token punctuation">.</span>LocationType<span class="token punctuation">.</span>BORDER<span class="token punctuation">)</span><span class="token punctuation">;</span>   <span class="token keyword">boolean</span> flag6 <span class="token operator">=</span> chunkholder$locationtype1<span class="token punctuation">.</span><span class="token function">isAtLeast</span><span class="token punctuation">(</span>ChunkHolder<span class="token punctuation">.</span>LocationType<span class="token punctuation">.</span>BORDER<span class="token punctuation">)</span><span class="token punctuation">;</span>   <span class="token keyword">this</span><span class="token punctuation">.</span>accessible <span class="token operator">|=</span> flag6<span class="token punctuation">;</span>   <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>flag5 <span class="token operator">&amp;&amp;</span> flag6<span class="token punctuation">)</span> <span class="token punctuation">{</span>      <span class="token keyword">this</span><span class="token punctuation">.</span>borderFuture <span class="token operator">=</span> chunkManagerIn<span class="token punctuation">.</span><span class="token function">func_222961_b</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span>      <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">chain</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>borderFuture<span class="token punctuation">)</span><span class="token punctuation">;</span>   <span class="token punctuation">}</span>   <span class="token keyword">if</span> <span class="token punctuation">(</span>flag5 <span class="token operator">&amp;&amp;</span> <span class="token operator">!</span>flag6<span class="token punctuation">)</span> <span class="token punctuation">{</span>      <span class="token comment" spellcheck="true">// unload</span>   <span class="token punctuation">}</span>   <span class="token keyword">boolean</span> flag7 <span class="token operator">=</span> chunkholder$locationtype<span class="token punctuation">.</span><span class="token function">isAtLeast</span><span class="token punctuation">(</span>ChunkHolder<span class="token punctuation">.</span>LocationType<span class="token punctuation">.</span>TICKING<span class="token punctuation">)</span><span class="token punctuation">;</span>   <span class="token keyword">boolean</span> flag2 <span class="token operator">=</span> chunkholder$locationtype1<span class="token punctuation">.</span><span class="token function">isAtLeast</span><span class="token punctuation">(</span>ChunkHolder<span class="token punctuation">.</span>LocationType<span class="token punctuation">.</span>TICKING<span class="token punctuation">)</span><span class="token punctuation">;</span>   <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>flag7 <span class="token operator">&amp;&amp;</span> flag2<span class="token punctuation">)</span> <span class="token punctuation">{</span>      <span class="token keyword">this</span><span class="token punctuation">.</span>tickingFuture <span class="token operator">=</span> chunkManagerIn<span class="token punctuation">.</span><span class="token function">func_219179_a</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span>      <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">chain</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>tickingFuture<span class="token punctuation">)</span><span class="token punctuation">;</span>   <span class="token punctuation">}</span>   <span class="token keyword">if</span> <span class="token punctuation">(</span>flag7 <span class="token operator">&amp;&amp;</span> <span class="token operator">!</span>flag2<span class="token punctuation">)</span> <span class="token punctuation">{</span>      <span class="token comment" spellcheck="true">// unload</span>   <span class="token punctuation">}</span>   <span class="token keyword">boolean</span> flag3 <span class="token operator">=</span> chunkholder$locationtype<span class="token punctuation">.</span><span class="token function">isAtLeast</span><span class="token punctuation">(</span>ChunkHolder<span class="token punctuation">.</span>LocationType<span class="token punctuation">.</span>ENTITY_TICKING<span class="token punctuation">)</span><span class="token punctuation">;</span>   <span class="token keyword">boolean</span> flag4 <span class="token operator">=</span> chunkholder$locationtype1<span class="token punctuation">.</span><span class="token function">isAtLeast</span><span class="token punctuation">(</span>ChunkHolder<span class="token punctuation">.</span>LocationType<span class="token punctuation">.</span>ENTITY_TICKING<span class="token punctuation">)</span><span class="token punctuation">;</span>   <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>flag3 <span class="token operator">&amp;&amp;</span> flag4<span class="token punctuation">)</span> <span class="token punctuation">{</span>      <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>entityTickingFuture <span class="token operator">!=</span> UNLOADED_CHUNK_FUTURE<span class="token punctuation">)</span> <span class="token punctuation">{</span>         <span class="token keyword">throw</span> <span class="token punctuation">(</span>IllegalStateException<span class="token punctuation">)</span>Util<span class="token punctuation">.</span><span class="token function">pauseDevMode</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">IllegalStateException</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>      <span class="token punctuation">}</span>      <span class="token keyword">this</span><span class="token punctuation">.</span>entityTickingFuture <span class="token operator">=</span> chunkManagerIn<span class="token punctuation">.</span><span class="token function">func_219188_b</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>pos<span class="token punctuation">)</span><span class="token punctuation">;</span>      <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">chain</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>entityTickingFuture<span class="token punctuation">)</span><span class="token punctuation">;</span>   <span class="token punctuation">}</span>   <span class="token keyword">if</span> <span class="token punctuation">(</span>flag3 <span class="token operator">&amp;&amp;</span> <span class="token operator">!</span>flag4<span class="token punctuation">)</span> <span class="token punctuation">{</span>      <span class="token comment" spellcheck="true">// unload</span>   <span class="token punctuation">}</span>   <span class="token keyword">this</span><span class="token punctuation">.</span>field_219327_v<span class="token punctuation">.</span><span class="token function">func_219066_a</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>pos<span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token operator">:</span><span class="token operator">:</span>func_219281_j<span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>chunkLevel<span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token operator">:</span><span class="token operator">:</span>func_219275_d<span class="token punctuation">)</span><span class="token punctuation">;</span>   <span class="token keyword">this</span><span class="token punctuation">.</span>prevChunkLevel <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>chunkLevel<span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>这个方法在区块的加载等级变化后，更新了 <code>prevChunkLevel</code> 的值，同时向区块安排任务。</p><p>前文讲到，在调用 <code>getBlockState</code> 一类的方法时，游戏会为没有加载的区块提供一个 <code>unknown</code> 的 Ticket，因此这类方法调用如果加载了区块，就应该会反应在这个方法。</p><p>接下来就简单了，在这里打一个断点，输出一下堆栈信息，轻松的就发现了 TheOneProbe 的漏洞。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>区块加载系统在 1.14 迎来了较大的变化，Mojang 以一种更加合理的方式来管理区块，大部分老版本用于卸载不需要的区块的服务端插件也逐渐退出了历史舞台。</p><p>如果你对世界生成中的区块生成有兴趣，可以查看 Yaossg 的<a href="https://www.mcbbs.net/thread-846195-1-1.html" target="_blank" rel="noopener">这篇文章</a>，尽管是 1.13 的，但是仍然有价值一读。</p><p>同时 Yaossg 对世界生成的过程也做了较为详细的解释，具体可以查看<a href="https://github.com/Yaossg/biome" target="_blank" rel="noopener">这个仓库</a>。</p><p>官方 Wiki 中的<a href="https://minecraft-zh.gamepedia.com/%E5%8C%BA%E5%9D%97" target="_blank" rel="noopener">区块</a>一节也对这个系统进行了一定的讲解，尽管 Wiki 会更注重游戏细节而非技术细节。</p><p>感谢最初介绍这个系统的 Drovolon，以及他的<a href="https://gist.github.com/Drovolon/24bfaae00d57e7a8ca64b792e14fa7c6" target="_blank" rel="noopener">文章</a>。官方 Wiki 大部分内容来源于此。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;Minecraft 在 1.14 引入了 Ticket 系统来管理区块的加载和卸载。&lt;/p&gt;
&lt;p&gt;本文采用 Minecraft 1.15.2，MCP 的 &lt;code&gt;20200904-1.15.1&lt;/code&gt; 映射表写就。&lt;/p&gt;
&lt;p&gt;让我们开始吧。&lt;/p&gt;
&lt;hr&gt;
      
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>2 + 2 == 5</title>
    <link href="https://izzel.io/2020/03/03/two-plus-two-equals-five/"/>
    <id>https://izzel.io/2020/03/03/two-plus-two-equals-five/</id>
    <published>2020-03-03T10:27:03.000Z</published>
    <updated>2025-07-13T12:15:43.525Z</updated>
    
    <content type="html"><![CDATA[<p>所谓自由就是可以说二加二等于四的自由。</p><h2 id="简单的代码"><a href="#简单的代码" class="headerlink" title="简单的代码"></a>简单的代码</h2><p>我们都知道：</p><pre><code>2 + 2 == 5</code></pre><p>有力的佐证如下：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Main</span> <span class="token punctuation">{</span>    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">void</span> <span class="token function">main</span><span class="token punctuation">(</span>String<span class="token punctuation">[</span><span class="token punctuation">]</span> args<span class="token punctuation">)</span> <span class="token keyword">throws</span> Exception <span class="token punctuation">{</span>        Class<span class="token operator">&lt;</span><span class="token operator">?</span><span class="token operator">></span> cache <span class="token operator">=</span> Integer<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">.</span><span class="token function">getDeclaredClasses</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">;</span>        Field c <span class="token operator">=</span> cache<span class="token punctuation">.</span><span class="token function">getDeclaredField</span><span class="token punctuation">(</span><span class="token string">"cache"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        c<span class="token punctuation">.</span><span class="token function">setAccessible</span><span class="token punctuation">(</span><span class="token boolean">true</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        Integer<span class="token punctuation">[</span><span class="token punctuation">]</span> array <span class="token operator">=</span> <span class="token punctuation">(</span>Integer<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span> c<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span>cache<span class="token punctuation">)</span><span class="token punctuation">;</span>        array<span class="token punctuation">[</span><span class="token number">132</span><span class="token punctuation">]</span> <span class="token operator">=</span> array<span class="token punctuation">[</span><span class="token number">133</span><span class="token punctuation">]</span><span class="token punctuation">;</span>        System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">printf</span><span class="token punctuation">(</span><span class="token string">"%d"</span><span class="token punctuation">,</span> <span class="token number">2</span> <span class="token operator">+</span> <span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>输出为 <code>5</code>，结果与事实相符，2 + 2 可以等于 5，也可以根据需要等于任何数字。</p><p>但是这是为什么呢？</p><h2 id="Java-的自动装箱"><a href="#Java-的自动装箱" class="headerlink" title="Java 的自动装箱"></a>Java 的自动装箱</h2><p>我们都知道，Java 中数据分为原生类型（Primitive Types）和引用类型，比如 <code>int</code> 就是原生类型，<code>Integer</code> 就是引用类型。</p><p>原生类型并不能作为泛型参数，因此以下的代码是不可以通过编译的：</p><pre class="line-numbers language-java"><code class="language-java">List<span class="token operator">&lt;</span><span class="token keyword">int</span><span class="token operator">></span> list <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ArrayList</span><span class="token operator">&lt;</span><span class="token operator">></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>这是因为，泛型在运行时会 <code>擦除</code>，Java 的泛型是假的泛型，不能在运行时获得泛型的类型，除非保存 <code>TypeToken</code> 之类的东西。</p><p>我们如果想往 <code>List</code> 里放入 <code>int</code>，则可以使用其引用类型：</p><pre class="line-numbers language-java"><code class="language-java">List<span class="token operator">&lt;</span>Integer<span class="token operator">></span> list <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ArrayList</span><span class="token operator">&lt;</span><span class="token operator">></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>list<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span>list<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">int</span> i <span class="token operator">=</span> list<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p>但是这样就有了一个问题，就是在 <code>list.add(1)</code> 中，根据泛型参数我们的 <code>add</code> 方法接受的是 <code>Integer</code> 类型，而传入的 <code>1</code> 实际上是 <code>int</code> 类型，这样类型不就冲突了吗？</p><p>因此，Java 在 1.5 的版本与泛型同时引入了自动装箱拆箱（Auto boxing/unboxing）机制，自动完成原生类型与引用类型之间的互相转换。</p><p>当需要一个引用类型而实际传入了原生类型，Java 就会为我们自动装箱，把原生类型变成引用类型，如上文的 <code>list.add(1)</code>，编译完成后实际上是：</p><pre class="line-numbers language-java"><code class="language-java">list<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span>Integer<span class="token punctuation">.</span><span class="token function">valueOf</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>而需要原生类型而实际为引用类型时，Java 就会尝试自动拆箱，比如 <code>int i = list.get(0)</code>，最后会变成：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">int</span> i <span class="token operator">=</span> list<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">intValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>而观察 Integer 类的源码，可以看出来大概长这样：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Integer</span> <span class="token keyword">extends</span> <span class="token class-name">Number</span> <span class="token keyword">implements</span> <span class="token class-name">Comparable</span><span class="token operator">&lt;</span>Integer<span class="token operator">></span> <span class="token punctuation">{</span>    <span class="token keyword">private</span> <span class="token keyword">final</span> <span class="token keyword">int</span> value<span class="token punctuation">;</span>    <span class="token keyword">public</span> <span class="token function">Integer</span><span class="token punctuation">(</span><span class="token keyword">int</span> value<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">this</span><span class="token punctuation">.</span>value <span class="token operator">=</span> value<span class="token punctuation">;</span>    <span class="token punctuation">}</span>    <span class="token keyword">public</span> <span class="token keyword">int</span> <span class="token function">intValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">return</span> value<span class="token punctuation">;</span>    <span class="token punctuation">}</span>    <span class="token keyword">public</span> <span class="token keyword">static</span> Integer <span class="token function">valueOf</span><span class="token punctuation">(</span><span class="token keyword">int</span> i<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">if</span> <span class="token punctuation">(</span>i <span class="token operator">>=</span> IntegerCache<span class="token punctuation">.</span>low <span class="token operator">&amp;&amp;</span> i <span class="token operator">&lt;=</span> IntegerCache<span class="token punctuation">.</span>high<span class="token punctuation">)</span>            <span class="token keyword">return</span> IntegerCache<span class="token punctuation">.</span>cache<span class="token punctuation">[</span>i <span class="token operator">+</span> <span class="token punctuation">(</span><span class="token operator">-</span>IntegerCache<span class="token punctuation">.</span>low<span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">;</span>        <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Integer</span><span class="token punctuation">(</span>i<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>然后你注意到了什么。</p><h2 id="Java-的-Integer-Cache"><a href="#Java-的-Integer-Cache" class="headerlink" title="Java 的 Integer Cache"></a>Java 的 Integer Cache</h2><p>你可能会想（不会想也得必须给我想）：</p><blockquote><p>如果许多数字都装箱，那不会白白消耗许多内存吗？</p></blockquote><p>因此，Java 为 Integer 类加入了缓存，对于小整数会直接引用提前创建好的实例作为 <code>valueOf</code> 的返回值。而提前创建好的这些实例，被放在 Integer 类里的 IntegerCache 类中。</p><p>Java 默认的缓存范围是 -128 到 127，因此 4 就在 132 的位置。</p><p>如此一来，把这里本应表示 <code>4</code> 的数字换成 <code>5</code> 的实例，就可以证明 <code>2 + 2</code> 实际上等于 <code>5</code> 了，并且这个数字可以根据需要任意改变。</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">int</span> i <span class="token operator">=</span> <span class="token number">2</span> <span class="token operator">+</span> <span class="token number">2</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 4</span>Integer boxed <span class="token operator">=</span> Integer<span class="token punctuation">.</span><span class="token function">valueOf</span><span class="token punctuation">(</span>i<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 返回 IntegerCache[132]，被修改为了 5</span>System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">printf</span><span class="token punctuation">(</span><span class="token string">"%d"</span><span class="token punctuation">,</span> boxed<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 5</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>顺便，缓存的范围可以自行增加，通过一个 <code>-XX:AutoBoxCacheMax</code> 参数指定上限，不过必须大于 <code>127</code>。</p><p>当然，不要把它带进生产环境。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;所谓自由就是可以说二加二等于四的自由。&lt;/p&gt;
&lt;h2 id=&quot;简单的代码&quot;&gt;&lt;a href=&quot;#简单的代码&quot; class=&quot;headerlink&quot; title=&quot;简单的代码&quot;&gt;&lt;/a&gt;简单的代码&lt;/h2&gt;&lt;p&gt;我们都知道：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2 + 2 == 
      
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>如何问玩家“吾与徐公孰美？”</title>
    <link href="https://izzel.io/2020/02/12/chat-with-future/"/>
    <id>https://izzel.io/2020/02/12/chat-with-future/</id>
    <published>2020-02-12T23:09:27.000Z</published>
    <updated>2025-07-13T12:15:43.515Z</updated>
    
    <content type="html"><![CDATA[<p>使用 CompletableFuture 实现一个简单的对话。</p><h2 id="关于对话"><a href="#关于对话" class="headerlink" title="关于对话"></a>关于对话</h2><p>对话并不罕见，在 QuickShop 中，插件会向玩家询问“你要买几个东西”，玩家则在聊天栏中输入对应的值；在 PlotSquared 中，玩家需要不断地输入对应的命令来配置地皮生成的参数，而输入命令也是另一种形式的对话。</p><p>对话的实现，Bukkit 为开发者准备了一套 Conversations API，编程开发板块的<a href="https://www.mcbbs.net/thread-619632-1-1.html" target="_blank" rel="noopener">这篇帖子</a>有简单的介绍。</p><p>本篇将会介绍另一种实现它的方法，简单的可以概括为，在一个方法里流畅的处理对话，比如这样：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">import</span> org<span class="token punctuation">.</span>bukkit<span class="token punctuation">.</span>entity<span class="token punctuation">.</span>Player<span class="token punctuation">;</span><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">SomeClass</span> <span class="token punctuation">{</span>    <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">ask</span><span class="token punctuation">(</span>Player player<span class="token punctuation">)</span> <span class="token punctuation">{</span>        player<span class="token punctuation">.</span><span class="token function">sendMessage</span><span class="token punctuation">(</span><span class="token string">"吾与徐公孰美？"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        String answer <span class="token operator">=</span> <span class="token function">getAnswer</span><span class="token punctuation">(</span>player<span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token keyword">assert</span> answer<span class="token punctuation">.</span><span class="token function">equals</span><span class="token punctuation">(</span><span class="token string">"徐公不若君之美也！"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>接下来的篇幅，就将讨论上文的 <code>getAnswer</code> 如何实现。</p><hr><p>实现对话，无非是这种逻辑：</p><ol><li>发送给玩家问题</li><li>监听玩家的回复</li><li>处理玩家的回复</li><li>如果还有问题，跳到 1</li></ol><p>按照正常的方法编写，我们需要一个监听器监听 <code>PlayerChatEvent</code> 或者它的异步版本，需要记录玩家的状态（玩家是不是在一个对话中 / 对话进行到了哪里），如果是诸葛亮王朗量级的超长对话，那么判断状态 / 更新状态的代码量将不堪设想。你还需要考虑玩家掉线/玩家不回答的情况，这就会又引入别的监听器和定时器。</p><p>因此，我们会想用上文代码一样的方式处理，无需记录状态，但是显然，<code>getAnswer</code> 不可能在调用的时候就有结果，玩家这时可能还不知道他被提了一个美不美的问题，而答案<strong>有可能</strong>会在未来提供，因此我们有了 <code>Future</code> 接口。</p><h2 id="关于-Future"><a href="#关于-Future" class="headerlink" title="关于 Future"></a>关于 Future</h2><p>这是 Future 接口的定义：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">package</span> java<span class="token punctuation">.</span>util<span class="token punctuation">.</span>concurrent<span class="token punctuation">;</span><span class="token keyword">public</span> <span class="token keyword">interface</span> <span class="token class-name">Future</span><span class="token operator">&lt;</span>V<span class="token operator">></span> <span class="token punctuation">{</span>    <span class="token keyword">boolean</span> <span class="token function">cancel</span><span class="token punctuation">(</span><span class="token keyword">boolean</span> mayInterruptIfRunning<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">boolean</span> <span class="token function">isCancelled</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">boolean</span> <span class="token function">isDone</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    V <span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">throws</span> InterruptedException<span class="token punctuation">,</span> ExecutionException<span class="token punctuation">;</span>    V <span class="token function">get</span><span class="token punctuation">(</span><span class="token keyword">long</span> timeout<span class="token punctuation">,</span> TimeUnit unit<span class="token punctuation">)</span>        <span class="token keyword">throws</span> InterruptedException<span class="token punctuation">,</span> ExecutionException<span class="token punctuation">,</span> TimeoutException<span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>注意到上面加粗的<strong>有可能</strong>了吗？为什么会 有可能 呢？</p><ul><li>你可能不想问了（<code>cancel</code>）</li><li>玩家可能并不想理睬你（<code>get</code> 方法超时了）</li><li>玩家掉线了（<code>get</code> 方法抛出了异常）</li><li>齐威王突然召见你进宫（<code>get</code> 方法被中断了）</li><li>玩家回答：徐公不若君之美也！</li></ul><p>可以得知，你能准确的从玩家嘴里问出有效的答案实属不易，而 <code>Future</code> 接口就可以表示一个可能出现异常的、会在未来得到结果的东西。<code>Future</code> 的泛型 <code>V</code>，则表示得到的值。</p><p>因此，上面的代码可以改成这样：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">SomeClass</span> <span class="token punctuation">{</span>    <span class="token keyword">public</span> Future<span class="token operator">&lt;</span>String<span class="token operator">></span> <span class="token function">getAnswer</span><span class="token punctuation">(</span>Player player<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">return</span> <span class="token comment" spellcheck="true">// ???</span>    <span class="token punctuation">}</span>    <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">ask</span><span class="token punctuation">(</span>Player player<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">try</span> <span class="token punctuation">{</span>            player<span class="token punctuation">.</span><span class="token function">sendMessage</span><span class="token punctuation">(</span><span class="token string">"吾与徐公孰美？"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>            String answer <span class="token operator">=</span> <span class="token function">getAnswer</span><span class="token punctuation">(</span>player<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>            <span class="token keyword">assert</span> answer<span class="token punctuation">.</span><span class="token function">equals</span><span class="token punctuation">(</span><span class="token string">"徐公不若君之美也！"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">Exception</span> e<span class="token punctuation">)</span> <span class="token punctuation">{</span>            e<span class="token punctuation">.</span><span class="token function">printStackTrace</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token punctuation">}</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>但是，<code>Future</code> 从哪里来呢？本篇的答案是 <code>CompletableFuture</code>。</p><h2 id="关于-CompletableFuture"><a href="#关于-CompletableFuture" class="headerlink" title="关于 CompletableFuture"></a>关于 CompletableFuture</h2><p>望文生义，<code>CompletableFuture</code> 代表着可以<strong>完成</strong>的 <code>Future</code>，这与本篇的目的不谋而合（不然呢）：玩家输入消息后，<code>getAnswer</code> 方法返回的 <code>Future</code> 就该完成了。</p><p>我们来了解一下 <code>CompletableFuture</code> 中比较重要的几个方法：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">package</span> java<span class="token punctuation">.</span>util<span class="token punctuation">.</span>concurrent<span class="token punctuation">;</span><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">CompletableFuture</span><span class="token operator">&lt;</span>T<span class="token operator">></span> <span class="token keyword">implements</span> <span class="token class-name">Future</span><span class="token operator">&lt;</span>T<span class="token operator">></span><span class="token punctuation">,</span> CompletionStage<span class="token operator">&lt;</span>T<span class="token operator">></span> <span class="token punctuation">{</span>    <span class="token keyword">public</span> <span class="token function">CompletableFuture</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token punctuation">}</span>    <span class="token keyword">public</span> T <span class="token function">join</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">public</span> <span class="token keyword">boolean</span> <span class="token function">complete</span><span class="token punctuation">(</span>T value<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">public</span> <span class="token keyword">boolean</span> <span class="token function">completeExceptionally</span><span class="token punctuation">(</span>Throwable ex<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><ul><li><code>CompletableFuture</code> 实现了 Future 接口，自然有 <code>Future</code> 接口的所有方法</li><li>无参构造方法得到一个崭新出厂的 <code>Future</code></li><li><code>join</code> 方法与 <code>get</code> 的效果类似，但有些许不同，在使用者看来最显著的区别就是，<code>join</code> 并不让你强制处理异常，虽然异常永远都在</li><li><code>complete</code> 和 <code>completeExceptionally</code> 分别代表正常完成和异常完成</li></ul><p>因此，我们不难将上面的代码改成这样：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">SomeClass</span> <span class="token punctuation">{</span>    <span class="token keyword">public</span> Future<span class="token operator">&lt;</span>String<span class="token operator">></span> <span class="token function">getAnswer</span><span class="token punctuation">(</span>Player player<span class="token punctuation">)</span> <span class="token punctuation">{</span>        CompletableFuture<span class="token operator">&lt;</span>String<span class="token operator">></span> future <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">CompletableFuture</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token comment" spellcheck="true">// 在别的地方调用 future.complete()</span>        <span class="token keyword">return</span> future<span class="token punctuation">;</span>    <span class="token punctuation">}</span>    <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">ask</span><span class="token punctuation">(</span>Player player<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">try</span> <span class="token punctuation">{</span>            player<span class="token punctuation">.</span><span class="token function">sendMessage</span><span class="token punctuation">(</span><span class="token string">"吾与徐公孰美？"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>            String answer <span class="token operator">=</span> <span class="token function">getAnswer</span><span class="token punctuation">(</span>player<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>            <span class="token keyword">assert</span> answer<span class="token punctuation">.</span><span class="token function">equals</span><span class="token punctuation">(</span><span class="token string">"徐公不若君之美也！"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">Exception</span> e<span class="token punctuation">)</span> <span class="token punctuation">{</span>            e<span class="token punctuation">.</span><span class="token function">printStackTrace</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token punctuation">}</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>于是，我们也就不难写出以下的代码：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">SomeClass</span> <span class="token punctuation">{</span>    <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">ask</span><span class="token punctuation">(</span>Player player<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// ...</span>    <span class="token keyword">public</span> Future<span class="token operator">&lt;</span>String<span class="token operator">></span> <span class="token function">getAnswer</span><span class="token punctuation">(</span>Player player<span class="token punctuation">)</span> <span class="token punctuation">{</span>        CompletableFuture<span class="token operator">&lt;</span>String<span class="token operator">></span> future <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">CompletableFuture</span><span class="token operator">&lt;</span><span class="token operator">></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        AskLifeExperience listener <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">AskLifeExperience</span><span class="token punctuation">(</span>player<span class="token punctuation">.</span><span class="token function">getUniqueId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> future<span class="token punctuation">)</span><span class="token punctuation">;</span>        Bukkit<span class="token punctuation">.</span><span class="token function">getPluginManager</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">registerEvents</span><span class="token punctuation">(</span>listener<span class="token punctuation">,</span> plugin<span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token keyword">return</span> future<span class="token punctuation">;</span>    <span class="token punctuation">}</span>    <span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">AskLifeExperience</span> <span class="token keyword">implements</span> <span class="token class-name">Listener</span> <span class="token punctuation">{</span>        <span class="token keyword">private</span> <span class="token keyword">final</span> UUID uuid<span class="token punctuation">;</span>        <span class="token keyword">private</span> <span class="token keyword">final</span> CompletableFuture<span class="token operator">&lt;</span>String<span class="token operator">></span> future<span class="token punctuation">;</span>        <span class="token keyword">public</span> <span class="token function">AskLifeExperience</span><span class="token punctuation">(</span>UUID uuid<span class="token punctuation">,</span> CompletableFuture<span class="token operator">&lt;</span>String<span class="token operator">></span> future<span class="token punctuation">)</span> <span class="token punctuation">{</span>            <span class="token keyword">this</span><span class="token punctuation">.</span>uuid <span class="token operator">=</span> uuid<span class="token punctuation">;</span>            <span class="token keyword">this</span><span class="token punctuation">.</span>future <span class="token operator">=</span> future<span class="token punctuation">;</span>        <span class="token punctuation">}</span>        <span class="token annotation punctuation">@EventHandler</span>        <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">on</span><span class="token punctuation">(</span>AsyncPlayerChatEvent event<span class="token punctuation">)</span> <span class="token punctuation">{</span>            <span class="token keyword">if</span> <span class="token punctuation">(</span>event<span class="token punctuation">.</span><span class="token function">getPlayer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getUniqueId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">equals</span><span class="token punctuation">(</span>uuid<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>                future<span class="token punctuation">.</span><span class="token function">complete</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span><span class="token function">getMessage</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>                HandlerList<span class="token punctuation">.</span><span class="token function">unregisterAll</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span>            <span class="token punctuation">}</span>        <span class="token punctuation">}</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>逻辑清晰明了，注册一个监听器，在玩家聊天的时候完成 <code>Future</code>。</p><p>转眼一想，既然 <code>getAnswer</code> 需要一定时间才会取得答案，那 <code>ask</code> 方法不就会消耗很多时间了吗？因此，我们要异步调用 <code>ask</code>。</p><h2 id="关于-Minecraft-服务器的同步与异步"><a href="#关于-Minecraft-服务器的同步与异步" class="headerlink" title="关于 Minecraft 服务器的同步与异步"></a>关于 Minecraft 服务器的同步与异步</h2><p>当不在主线程进行操作的时候，我们都应该想一想，这样安全吗？</p><p>从上到下看一遍，不难问出这些问题：</p><ul><li><code>sendMessage</code> 安全吗？</li><li>异步注册事件是安全的吗？</li><li><code>CompletableFuture#complete</code> 安全吗？（不然呢）</li><li><code>Future#get</code> 方法一定会返回吗？</li></ul><p>根据<a href="http://bdn.tdiant.net/#/brm-2-5" target="_blank" rel="noopener">一篇写的很不错的文档</a>（这篇文档对水桶的 scheduler 有较为详细的介绍），这几个东西是线程安全的：</p><ul><li><code>sendMessage</code> （发包）</li><li>Bukkit 的 <code>scheduler</code> 包</li><li><code>PluginManager#callEvent(event)</code></li></ul><p>因此应该将注册事件部分的代码通过 Scheduler 转移到主线程完成。</p><p>最终的完整（但不完善）的方法如下，监听器与上文相同：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">SomeClass</span> <span class="token punctuation">{</span>    <span class="token keyword">private</span> Plugin plugin <span class="token operator">=</span> null<span class="token punctuation">;</span>    <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">ask</span><span class="token punctuation">(</span>Player player<span class="token punctuation">)</span> <span class="token punctuation">{</span>        Bukkit<span class="token punctuation">.</span><span class="token function">getScheduler</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">runTaskAsynchronously</span><span class="token punctuation">(</span>plugin<span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token punctuation">{</span>            <span class="token keyword">try</span> <span class="token punctuation">{</span>                player<span class="token punctuation">.</span><span class="token function">sendMessage</span><span class="token punctuation">(</span><span class="token string">"吾与徐公孰美？"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>                String answer <span class="token operator">=</span> <span class="token function">getAnswer</span><span class="token punctuation">(</span>player<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token number">15</span><span class="token punctuation">,</span> TimeUnit<span class="token punctuation">.</span>SECONDS<span class="token punctuation">)</span><span class="token punctuation">;</span>                <span class="token keyword">assert</span> answer<span class="token punctuation">.</span><span class="token function">equals</span><span class="token punctuation">(</span><span class="token string">"徐公不若君之美也！"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>            <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">Exception</span> e<span class="token punctuation">)</span> <span class="token punctuation">{</span>                e<span class="token punctuation">.</span><span class="token function">printStackTrace</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>            <span class="token punctuation">}</span>        <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span>    <span class="token keyword">public</span> Future<span class="token operator">&lt;</span>String<span class="token operator">></span> <span class="token function">getAnswer</span><span class="token punctuation">(</span>Player player<span class="token punctuation">)</span> <span class="token punctuation">{</span>        CompletableFuture<span class="token operator">&lt;</span>String<span class="token operator">></span> future <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">CompletableFuture</span><span class="token operator">&lt;</span><span class="token operator">></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        Bukkit<span class="token punctuation">.</span><span class="token function">getScheduler</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">runTask</span><span class="token punctuation">(</span>plugin<span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token punctuation">{</span>            AskLifeExperience listener <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">AskLifeExperience</span><span class="token punctuation">(</span>player<span class="token punctuation">.</span><span class="token function">getUniqueId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> future<span class="token punctuation">)</span><span class="token punctuation">;</span>            Bukkit<span class="token punctuation">.</span><span class="token function">getPluginManager</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">registerEvents</span><span class="token punctuation">(</span>listener<span class="token punctuation">,</span> plugin<span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token keyword">return</span> future<span class="token punctuation">;</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>当然，代码写完，还应该问自己几个问题：</p><ul><li>我们在监听器里在事件触发的时候取消注册，万一事件永远不触发呢？</li><li>玩家离线后，<code>Player</code> 实例不再可用，怎么办呢？</li></ul><p>这些问题不是本篇重点，就不说了。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>可以看出，优美的写一串对话，所需代码量其实并不多，寥寥数十行就可以了。</p><p><strong>线程安全十分重要。</strong></p><p><code>CompletableFuture</code> 还有许多实用的方法，可以用于各种耗时的操作，如 <code>获取数据库的信息后，将其应用于服务器中</code>。希望读者能够自行多加了解。</p><p>zzzz 编写了<a href="https://blog.ustc-zzzz.net/coroutine-in-minecraft-mod/" target="_blank" rel="noopener">一篇协程教程</a>，可以写出与本篇主方法非常类似的代码，虽然背后的原理大不相同，比如它全部在主线程上运行。</p><p>tdiant 编写了<a href="http://bdn.tdiant.net/" target="_blank" rel="noopener">一篇十分全面的水桶教程</a>，对 Scheduler 和其他部分都有很多讲解。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;使用 CompletableFuture 实现一个简单的对话。&lt;/p&gt;
&lt;h2 id=&quot;关于对话&quot;&gt;&lt;a href=&quot;#关于对话&quot; class=&quot;headerlink&quot; title=&quot;关于对话&quot;&gt;&lt;/a&gt;关于对话&lt;/h2&gt;&lt;p&gt;对话并不罕见，在 QuickShop 中，插件
      
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>关于 Mixin 升级到 0.8 和 ModLauncher 的那些事</title>
    <link href="https://izzel.io/2020/02/06/mixin-0-8-guide/"/>
    <id>https://izzel.io/2020/02/06/mixin-0-8-guide/</id>
    <published>2020-02-06T18:20:10.000Z</published>
    <updated>2025-07-13T12:15:43.525Z</updated>
    
    <content type="html"><![CDATA[<p>仿生 ModLauncher 不会梦到 LaunchWrapper</p><h2 id="MixinConnector"><a href="#MixinConnector" class="headerlink" title="MixinConnector"></a>MixinConnector</h2><p>从 1.13 起，Forge 抛弃了原来的 LauncherWrapper，改用了 cpw 编写的 ModLauncher，也就是说，Mixin 原来基于 Tweaker 的那一套不能用了，为此，Mixin 引入了 <code>MixinConnector</code>。</p><p>首先，你需要让一个类实现 <code>IMixinConnector</code>，比如：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">import</span> org<span class="token punctuation">.</span>spongepowered<span class="token punctuation">.</span>asm<span class="token punctuation">.</span>mixin<span class="token punctuation">.</span>connect<span class="token punctuation">.</span>IMixinConnector<span class="token punctuation">;</span><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">ExampleConnector</span> <span class="token keyword">implements</span> <span class="token class-name">IMixinConnector</span> <span class="token punctuation">{</span>    <span class="token annotation punctuation">@Override</span>    <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">connect</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>然后在其中添加需要添加的 Mixin 配置文件：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">import</span> org<span class="token punctuation">.</span>spongepowered<span class="token punctuation">.</span>asm<span class="token punctuation">.</span>mixin<span class="token punctuation">.</span>Mixins<span class="token punctuation">;</span><span class="token keyword">import</span> org<span class="token punctuation">.</span>spongepowered<span class="token punctuation">.</span>asm<span class="token punctuation">.</span>mixin<span class="token punctuation">.</span>connect<span class="token punctuation">.</span>IMixinConnector<span class="token punctuation">;</span><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">ExampleConnector</span> <span class="token keyword">implements</span> <span class="token class-name">IMixinConnector</span> <span class="token punctuation">{</span>    <span class="token annotation punctuation">@Override</span>    <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">connect</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>        Mixins<span class="token punctuation">.</span><span class="token function">addConfiguration</span><span class="token punctuation">(</span><span class="token string">"mixins.example.json"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>最后，你需要在 MANIFEST.MF 文件中，指定这个 jar 文件使用的 Mixin Connector：</p><p><code>文件 META-INF/MANIFEST.MF</code></p><pre><code>Manifest-Version: 1.0MixinConnector: ExampleConnector</code></pre><p>记得在最后空一行。</p><p>当然，也可以使用构建工具，比如 Gradle：</p><pre class="line-numbers language-groovy"><code class="language-groovy">jar <span class="token punctuation">{</span>    manifest<span class="token operator">.</span><span class="token function">attributes</span><span class="token punctuation">(</span>        <span class="token string">'MixinConnector'</span><span class="token punctuation">:</span> <span class="token string">'ExampleConnector'</span>    <span class="token punctuation">)</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>剩余的内容就是普通的 Mixin 编写了，包括编写 Mixin 类，配置文件。</p><h2 id="ModLauncher-支持"><a href="#ModLauncher-支持" class="headerlink" title="ModLauncher 支持"></a>ModLauncher 支持</h2><p>当然，截至本文发出时，ModLauncher 生态仍然没有官方的 Mixin 支持，因此在 ModLauncher 引导 Mixin 需要一点奇淫巧计。</p><p>比如我的项目 <a href="https://github.com/IzzelAliz/MixinLoader" target="_blank" rel="noopener">https://github.com/IzzelAliz/MixinLoader</a> ，下载之后扔到 mods 文件夹，你的 Forge 就有了 Mixin 环境。</p><h2 id="其他特性"><a href="#其他特性" class="headerlink" title="其他特性"></a>其他特性</h2><p>Mixin 0.8 带来了新的 <code>tsrg</code> 混淆表格式支持，以及 MixinGradle 对 ForgeGradle 3+ 的支持。升级至 MixinGradle 0.7 就可以快乐生成 <code>refmap</code> 了。</p><p>同时，现在 Mixin Config 可以继承自其他的，只需要</p><p><code>文件 mixins.example.json</code></p><pre class="line-numbers language-json"><code class="language-json"><span class="token punctuation">{</span>  <span class="token property">"parent"</span><span class="token operator">:</span> <span class="token string">"mixins.parent.json"</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>更多特性可以阅读官方的 <a href="https://github.com/SpongePowered/Mixin/wiki/Release-Notes---Mixin-0.8" target="_blank" rel="noopener">Release note</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;仿生 ModLauncher 不会梦到 LaunchWrapper&lt;/p&gt;
&lt;h2 id=&quot;MixinConnector&quot;&gt;&lt;a href=&quot;#MixinConnector&quot; class=&quot;headerlink&quot; title=&quot;MixinConnector&quot;&gt;&lt;/a&gt;Mix
      
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>Bukkit 持久化数据存储</title>
    <link href="https://izzel.io/2019/10/05/bukkit-persistent-api/"/>
    <id>https://izzel.io/2019/10/05/bukkit-persistent-api/</id>
    <published>2019-10-05T16:51:28.000Z</published>
    <updated>2025-07-13T12:15:43.515Z</updated>
    
    <content type="html"><![CDATA[<p>在 Minecraft 1.14 的时候，Bukkit 终于添加了持久化数据存储相关的 API。</p><p>简单的看了一下，一共添加了 4 个接口，其中 <code>PersistentDataHolder</code> 接口标记了对应的实现可以存储数据。</p><p>实现该接口的主要有三类比较重要：</p><ul><li>一类是所有的实体，也就是说我们可以在任何实体（比如玩家）身上存储永久的数据，比如玩家的属性、职业啥的；</li><li>一类是所有附带 TileEntity 的 BlockState，对应的接口命名为 <code>TileState</code>，就是说可以往部分方块里存数据；</li><li>一类是 <code>ItemMeta</code>，也就是我们可以正大光明的往物品里存数据了。</li></ul><h2 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h2><ul><li><p>接口 <code>PersistentDataHolder</code>：<br><code>getPersistentDataContainer</code> 用于获取持久化数据的存储容器，所有数据的读写都在这里</p></li><li><p>接口 <code>PersistentDataContainer</code>：</p></li></ul><p>有 <code>get</code> <code>has</code> <code>set</code> 等一系列方法，类似于一个 Map，键为 <code>NamespacedKey</code>，用于区分不用插件的不同数据。</p><ul><li>接口 <code>PersistentDataType&lt;T, Z&gt;</code>：</li></ul><p>表示存储的数据类型，其中接口内部的字段定义了常用的一些数据类型，比如 <code>Integer</code>, <code>String</code> 等等。<br>可以通过实现该接口实现自定义数据的序列化和反序列化。</p><p>接口的两个泛型参数中，</p><ul><li><code>T</code> 表示存储的原生类型，目测必须为那几个内置的字段的类型之一</li><li><code>Z</code> 表示你的自定义类型</li></ul><h2 id="简单样例"><a href="#简单样例" class="headerlink" title="简单样例"></a>简单样例</h2><p>拿我<a href="/2019/03/03/binary-data-storage">上一篇文</a>做例子</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">PlayerData</span> <span class="token punctuation">{</span>    <span class="token keyword">int</span> hp <span class="token operator">=</span> <span class="token number">20</span><span class="token punctuation">;</span>    <span class="token keyword">double</span> strength <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>    <span class="token keyword">boolean</span> haveJob<span class="token punctuation">;</span>    Job job<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">Job</span> <span class="token punctuation">{</span>    String name<span class="token punctuation">;</span>    <span class="token keyword">int</span> level<span class="token punctuation">;</span>    <span class="token keyword">int</span> exp<span class="token punctuation">;</span>    String prefix<span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>仍然是这两个类，这次我们不用保存在别的文件里了。<br>利用新的 Bukkit API，可以直接存到 Player 里去，因为 <code>Player</code> 继承了 <code>PersistentDataHolder</code>。</p><p>首先要把我们的类转换成原生数据类型，按照上一篇，我们用 <code>byte[]</code> 存数据。</p><p>首先，实现一个 <code>PersistentDataType&lt;byte[], Job&gt;</code>，按照上一篇教程，大概长这样：<br>（不重复展示怎么读写字符串的方法）</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">JobDataType</span> <span class="token keyword">implements</span> <span class="token class-name">PersistentDataType</span><span class="token operator">&lt;</span><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span> Job<span class="token operator">></span> <span class="token punctuation">{</span>    <span class="token keyword">static</span> <span class="token keyword">final</span> JobDataType INSTANCE <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">JobDataType</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token annotation punctuation">@Override</span>    <span class="token keyword">public</span> Class<span class="token operator">&lt;</span><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token operator">></span> <span class="token function">getPrimitiveType</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">return</span> <span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span>    <span class="token annotation punctuation">@Override</span>    <span class="token keyword">public</span> Class<span class="token operator">&lt;</span>Job<span class="token operator">></span> <span class="token function">getComplexType</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">return</span> Job<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span>    <span class="token annotation punctuation">@Override</span>    <span class="token keyword">public</span> <span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token function">toPrimitive</span><span class="token punctuation">(</span>Job complex<span class="token punctuation">,</span> PersistentDataAdapterContext context<span class="token punctuation">)</span> <span class="token punctuation">{</span>        ByteBuf buffer <span class="token operator">=</span> Unpooled<span class="token punctuation">.</span><span class="token function">buffer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token function">writeString</span><span class="token punctuation">(</span>buffer<span class="token punctuation">,</span> complex<span class="token punctuation">.</span>name<span class="token punctuation">)</span><span class="token punctuation">;</span>        buffer<span class="token punctuation">.</span><span class="token function">writeInt</span><span class="token punctuation">(</span>complex<span class="token punctuation">.</span>level<span class="token punctuation">)</span><span class="token punctuation">;</span>        buffer<span class="token punctuation">.</span><span class="token function">writeInt</span><span class="token punctuation">(</span>complex<span class="token punctuation">.</span>exp<span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token function">writeString</span><span class="token punctuation">(</span>buffer<span class="token punctuation">,</span> complex<span class="token punctuation">.</span>prefix<span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token keyword">return</span> buffer<span class="token punctuation">.</span><span class="token function">array</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span>    <span class="token annotation punctuation">@Override</span>    <span class="token keyword">public</span> Job <span class="token function">fromPrimitive</span><span class="token punctuation">(</span><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span> primitive<span class="token punctuation">,</span> PersistentDataAdapterContext context<span class="token punctuation">)</span> <span class="token punctuation">{</span>        Job job <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Job</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        ByteBuf buffer <span class="token operator">=</span> Unpooled<span class="token punctuation">.</span><span class="token function">wrappedBuffer</span><span class="token punctuation">(</span>primitive<span class="token punctuation">)</span><span class="token punctuation">;</span>        job<span class="token punctuation">.</span>name <span class="token operator">=</span> <span class="token function">readString</span><span class="token punctuation">(</span>buffer<span class="token punctuation">)</span><span class="token punctuation">;</span>        job<span class="token punctuation">.</span>level <span class="token operator">=</span> buffer<span class="token punctuation">.</span><span class="token function">readInt</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        job<span class="token punctuation">.</span>exp <span class="token operator">=</span> buffer<span class="token punctuation">.</span><span class="token function">readInt</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        job<span class="token punctuation">.</span>prefix <span class="token operator">=</span> <span class="token function">readString</span><span class="token punctuation">(</span>buffer<span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token keyword">return</span> job<span class="token punctuation">;</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>接着，把数据存到 Player 里：</p><pre class="line-numbers language-java"><code class="language-java">Plugin plugin <span class="token operator">=</span> <span class="token comment" spellcheck="true">/*...*/</span><span class="token punctuation">;</span>Player player <span class="token operator">=</span> <span class="token comment" spellcheck="true">/*...*/</span><span class="token punctuation">;</span>PersistentDataContainer container <span class="token operator">=</span> player<span class="token punctuation">.</span><span class="token function">getPersistentDataContainer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment" spellcheck="true">// 存</span>Job job <span class="token operator">=</span> <span class="token comment" spellcheck="true">/*...*/</span><span class="token punctuation">;</span>container<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">NamespacedKey</span><span class="token punctuation">(</span>plugin<span class="token punctuation">,</span> <span class="token string">"playerJob"</span><span class="token punctuation">)</span><span class="token punctuation">,</span> JobDataType<span class="token punctuation">.</span>INSTANCE<span class="token punctuation">,</span> job<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment" spellcheck="true">// 取</span>Job get <span class="token operator">=</span> container<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">NamespacedKey</span><span class="token punctuation">(</span>plugin<span class="token punctuation">,</span> <span class="token string">"playerJob"</span><span class="token punctuation">)</span><span class="token punctuation">,</span> JobDataType<span class="token punctuation">.</span>INSTANCE<span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>十分的简单。</p><p>把 <code>Player</code> 换成 <code>ItemMeta</code>，就是向物品中存储数据。</p><h2 id="使用-Bukkit-API-而不是操作-byte-数组"><a href="#使用-Bukkit-API-而不是操作-byte-数组" class="headerlink" title="使用 Bukkit API 而不是操作 byte 数组"></a>使用 Bukkit API 而不是操作 byte 数组</h2><p>注意到 <code>PersistentDataType</code> 里有一个名为 <code>CONTAINER</code> 的字段，可以合理猜测内部原生类型支持 <code>PersistentDataContainer</code>，因此不难写出这样的代码：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">JobContainerType</span> <span class="token keyword">implements</span> <span class="token class-name">PersistentDataType</span><span class="token operator">&lt;</span>PersistentDataContainer<span class="token punctuation">,</span> Job<span class="token operator">></span> <span class="token punctuation">{</span>    <span class="token keyword">static</span> JobContainerType INSTANCE <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">JobContainerType</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token annotation punctuation">@Override</span>    <span class="token keyword">public</span> Class<span class="token operator">&lt;</span>PersistentDataContainer<span class="token operator">></span> <span class="token function">getPrimitiveType</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">return</span> PersistentDataContainer<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span>    <span class="token annotation punctuation">@Override</span>    <span class="token keyword">public</span> Class<span class="token operator">&lt;</span>Job<span class="token operator">></span> <span class="token function">getComplexType</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">return</span> Job<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span>    <span class="token annotation punctuation">@Override</span>    <span class="token keyword">public</span> PersistentDataContainer <span class="token function">toPrimitive</span><span class="token punctuation">(</span>Job complex<span class="token punctuation">,</span> PersistentDataAdapterContext context<span class="token punctuation">)</span> <span class="token punctuation">{</span>        PersistentDataContainer container <span class="token operator">=</span> context<span class="token punctuation">.</span><span class="token function">newPersistentDataContainer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        container<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">NamespacedKey</span><span class="token punctuation">(</span>plugin<span class="token punctuation">,</span> <span class="token string">"name"</span><span class="token punctuation">)</span><span class="token punctuation">,</span> PersistentDataType<span class="token punctuation">.</span>STRING<span class="token punctuation">,</span> complex<span class="token punctuation">.</span>name<span class="token punctuation">)</span><span class="token punctuation">;</span>        container<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">NamespacedKey</span><span class="token punctuation">(</span>plugin<span class="token punctuation">,</span> <span class="token string">"level"</span><span class="token punctuation">)</span><span class="token punctuation">,</span> PersistentDataType<span class="token punctuation">.</span>INTEGER<span class="token punctuation">,</span> complex<span class="token punctuation">.</span>level<span class="token punctuation">)</span><span class="token punctuation">;</span>        container<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">NamespacedKey</span><span class="token punctuation">(</span>plugin<span class="token punctuation">,</span> <span class="token string">"exp"</span><span class="token punctuation">)</span><span class="token punctuation">,</span> PersistentDataType<span class="token punctuation">.</span>INTEGER<span class="token punctuation">,</span> complex<span class="token punctuation">.</span>exp<span class="token punctuation">)</span><span class="token punctuation">;</span>        container<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">NamespacedKey</span><span class="token punctuation">(</span>plugin<span class="token punctuation">,</span> <span class="token string">"prefix"</span><span class="token punctuation">)</span><span class="token punctuation">,</span> PersistentDataType<span class="token punctuation">.</span>STRING<span class="token punctuation">,</span> complex<span class="token punctuation">.</span>prefix<span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token keyword">return</span> container<span class="token punctuation">;</span>    <span class="token punctuation">}</span>    <span class="token annotation punctuation">@Override</span>    <span class="token keyword">public</span> Job <span class="token function">fromPrimitive</span><span class="token punctuation">(</span>PersistentDataContainer primitive<span class="token punctuation">,</span> PersistentDataAdapterContext context<span class="token punctuation">)</span> <span class="token punctuation">{</span>        Job job <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Job</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        job<span class="token punctuation">.</span>name <span class="token operator">=</span> primitive<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">NamespacedKey</span><span class="token punctuation">(</span>plugin<span class="token punctuation">,</span> <span class="token string">"name"</span><span class="token punctuation">)</span><span class="token punctuation">,</span> PersistentDataType<span class="token punctuation">.</span>STRING<span class="token punctuation">)</span><span class="token punctuation">;</span>        job<span class="token punctuation">.</span>level <span class="token operator">=</span> primitive<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">NamespacedKey</span><span class="token punctuation">(</span>plugin<span class="token punctuation">,</span> <span class="token string">"level"</span><span class="token punctuation">)</span><span class="token punctuation">,</span> PersistentDataType<span class="token punctuation">.</span>INTEGER<span class="token punctuation">)</span><span class="token punctuation">;</span>        job<span class="token punctuation">.</span>exp <span class="token operator">=</span> primitive<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">NamespacedKey</span><span class="token punctuation">(</span>plugin<span class="token punctuation">,</span> <span class="token string">"exp"</span><span class="token punctuation">)</span><span class="token punctuation">,</span> PersistentDataType<span class="token punctuation">.</span>INTEGER<span class="token punctuation">)</span><span class="token punctuation">;</span>        job<span class="token punctuation">.</span>prefix <span class="token operator">=</span> primitive<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">NamespacedKey</span><span class="token punctuation">(</span>plugin<span class="token punctuation">,</span> <span class="token string">"primitive"</span><span class="token punctuation">)</span><span class="token punctuation">,</span> PersistentDataType<span class="token punctuation">.</span>STRING<span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token keyword">return</span> job<span class="token punctuation">;</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>看起来也还行。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;在 Minecraft 1.14 的时候，Bukkit 终于添加了持久化数据存储相关的 API。&lt;/p&gt;
&lt;p&gt;简单的看了一下，一共添加了 4 个接口，其中 &lt;code&gt;PersistentDataHolder&lt;/code&gt; 接口标记了对应的实现可以存储数据。&lt;/p&gt;
&lt;p
      
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>更换高亮插件为 Prism</title>
    <link href="https://izzel.io/2019/10/05/theme-highlight-change/"/>
    <id>https://izzel.io/2019/10/05/theme-highlight-change/</id>
    <published>2019-10-05T13:02:28.000Z</published>
    <updated>2025-07-13T12:15:43.525Z</updated>
    
    <content type="html"><![CDATA[<p>今天折腾了一会儿，给这个 noise 主题换上了 prism 的 darcula 主题，然后又把某些地方魔改了一下。</p><p>总之香的不行，上一篇的 Groovy 代码高亮混乱的问题也解决了。</p><p>测试一下，</p><pre class="line-numbers language-java"><code class="language-java"><span class="token annotation punctuation">@Override</span><span class="token keyword">public</span> <span class="token operator">&lt;</span>C <span class="token keyword">extends</span> <span class="token class-name">CastingSkill</span><span class="token operator">&lt;</span>E<span class="token operator">></span><span class="token punctuation">,</span> E <span class="token keyword">extends</span> <span class="token class-name">EntitySkill</span><span class="token operator">&lt;</span><span class="token operator">?</span><span class="token punctuation">,</span> C<span class="token operator">>></span> Optional<span class="token operator">&lt;</span>C<span class="token operator">></span> <span class="token function">operate</span><span class="token punctuation">(</span>Class<span class="token operator">&lt;</span>E<span class="token operator">></span> cl<span class="token punctuation">,</span>     SkillOperation<span class="token operator">&lt;</span><span class="token operator">?</span> <span class="token keyword">super</span> C<span class="token operator">></span> operation<span class="token punctuation">)</span> <span class="token keyword">throws</span> UnsupportedOperationException <span class="token punctuation">{</span>    Optional<span class="token operator">&lt;</span>Entity<span class="token operator">></span> entity <span class="token operator">=</span> <span class="token function">getEntity</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span>entity<span class="token punctuation">.</span><span class="token function">isPresent</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">if</span> <span class="token punctuation">(</span>multimap<span class="token punctuation">.</span><span class="token function">containsKey</span><span class="token punctuation">(</span>cl<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>            Collection<span class="token operator">&lt;</span>C<span class="token operator">></span> castingSkills <span class="token operator">=</span> <span class="token function">getCastingSkills</span><span class="token punctuation">(</span>cl<span class="token punctuation">)</span><span class="token punctuation">;</span>            <span class="token keyword">if</span> <span class="token punctuation">(</span>castingSkills<span class="token punctuation">.</span><span class="token function">size</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>                C skill <span class="token operator">=</span> castingSkills<span class="token punctuation">.</span><span class="token function">iterator</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">next</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>                <span class="token keyword">return</span> Optional<span class="token punctuation">.</span><span class="token function">of</span><span class="token punctuation">(</span><span class="token function">operate</span><span class="token punctuation">(</span>skill<span class="token punctuation">,</span> operation<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>            <span class="token punctuation">}</span>        <span class="token punctuation">}</span>        ProfessionSubject subject <span class="token operator">=</span> ProfessionService<span class="token punctuation">.</span><span class="token function">instance</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getOrCreate</span><span class="token punctuation">(</span>entity<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        SkillTree skillTree <span class="token operator">=</span> subject<span class="token punctuation">.</span><span class="token function">getMerged</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        Optional<span class="token operator">&lt;</span>E<span class="token operator">></span> optional <span class="token operator">=</span> skillTree<span class="token punctuation">.</span><span class="token function">find</span><span class="token punctuation">(</span>cl<span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token keyword">if</span> <span class="token punctuation">(</span>optional<span class="token punctuation">.</span><span class="token function">isPresent</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>            E skill <span class="token operator">=</span> optional<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>            C cast <span class="token operator">=</span> skill<span class="token punctuation">.</span><span class="token function">createCast</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span>            <span class="token keyword">return</span> Optional<span class="token punctuation">.</span><span class="token function">of</span><span class="token punctuation">(</span><span class="token function">operate</span><span class="token punctuation">(</span>cast<span class="token punctuation">,</span> operation<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token punctuation">}</span>    <span class="token punctuation">}</span>    <span class="token keyword">return</span> Optional<span class="token punctuation">.</span><span class="token function">empty</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>没 IDEA 里面好看。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;今天折腾了一会儿，给这个 noise 主题换上了 prism 的 darcula 主题，然后又把某些地方魔改了一下。&lt;/p&gt;
&lt;p&gt;总之香的不行，上一篇的 Groovy 代码高亮混乱的问题也解决了。&lt;/p&gt;
&lt;p&gt;测试一下，&lt;/p&gt;
&lt;pre class=&quot;line-num
      
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>域名更改</title>
    <link href="https://izzel.io/2019/06/21/domain-change/"/>
    <id>https://izzel.io/2019/06/21/domain-change/</id>
    <published>2019-06-21T11:50:04.000Z</published>
    <updated>2025-07-13T12:15:43.523Z</updated>
    
    <content type="html"><![CDATA[<p>从今天起，海螺的域名更换为 <a href="https://izzel.io">izzel.io</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;从今天起，海螺的域名更换为 &lt;a href=&quot;https://izzel.io&quot;&gt;izzel.io&lt;/a&gt;&lt;/p&gt;

      
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>开发中的数据存储</title>
    <link href="https://izzel.io/2019/03/03/binary-data-storage/"/>
    <id>https://izzel.io/2019/03/03/binary-data-storage/</id>
    <published>2019-03-03T14:46:29.000Z</published>
    <updated>2025-07-13T12:15:43.515Z</updated>
    
    <content type="html"><![CDATA[<p>这篇文章是来源于<a href="http://www.mcbbs.net/thread-848236-1-1.html" target="_blank" rel="noopener">这个问题</a>，<br>在回答这个问题之后我思考了一下，怎样存储数据才算优雅又高效，<br>因此有了这篇文章。</p><h2 id="开发中的数据存储"><a href="#开发中的数据存储" class="headerlink" title="开发中的数据存储"></a>开发中的数据存储</h2><p>本文的代码是 Groovy，你可以看做是没有行末分号、异常不用捕获的 Java。<br>本文代码以 MIT License 开源。</p><p>那么开始。</p><h3 id="配置文件"><a href="#配置文件" class="headerlink" title="配置文件"></a>配置文件</h3><p>配置文件是最易用也最常用的方法之一，<br>但是这不是本文的重点，因为配置文件的使用教程实在是太多了。</p><p>尽管配置文件的本意是给使用者自定义你的插件/Mod的行为，但是用来存储数据也是可以的。</p><p>为方便读者，这里给出一些其他人的教程。</p><ul><li>tdiant 的教程<ul><li><a href="https://github.com/tdiant/BukkitDevelopmentNote/blob/master/brm-1-4.md" target="_blank" rel="noopener">Bukkit 配置 API</a></li><li><a href="https://github.com/tdiant/BukkitDevelopmentNote/blob/master/brm-2-4.md" target="_blank" rel="noopener">Bukkit 配置 API 序列化</a></li></ul></li><li>ustc_zzz 的 <a href="https://fmltutor.ustc-zzzz.net/1.8-%E5%88%9B%E5%BB%BA%E4%B8%80%E4%BB%BD%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6.html" target="_blank" rel="noopener">Forge 配置文件</a> 教程</li><li><a href="https://docs.spongepowered.org/stable/zh-CN/plugin/configuration/index.html" target="_blank" rel="noopener">Sponge 配置文件</a> 教程</li></ul><h3 id="数据库"><a href="#数据库" class="headerlink" title="数据库"></a>数据库</h3><p>数据库是个挺好的话题，给出一个 <a href="http://www.mcbbs.net/thread-783267-1-1.html" target="_blank" rel="noopener">MySQL 教程</a>，</p><p>这篇文章不会讲解数据库，因为数据库也不是本文重点。但是额外提醒一点，用数据库注意阻塞和线程安全。</p><h3 id="手动写二进制数据"><a href="#手动写二进制数据" class="headerlink" title="手动写二进制数据"></a>手动写二进制数据</h3><p>存配置文件看起来很不优雅，还占空间；而存数据库对于一般插件好像又有点多此一举了，那么有没有又快又省空间的方法呢？</p><p>这节的标题就是，这一节也是真正的重点。</p><p>例子，我们要做一个玩家属性插件：</p><pre class="line-numbers language-groovy"><code class="language-groovy"><span class="token keyword">class</span> <span class="token class-name">PlayerData</span> <span class="token punctuation">{</span>    <span class="token keyword">int</span> hp <span class="token operator">=</span> <span class="token number">20</span>    <span class="token keyword">double</span> strength <span class="token operator">=</span> <span class="token number">0</span>    <span class="token keyword">boolean</span> haveJob    Job job<span class="token punctuation">}</span><span class="token keyword">class</span> <span class="token class-name">Job</span> <span class="token punctuation">{</span>    String name    <span class="token keyword">int</span> level    <span class="token keyword">int</span> exp    String prefix <span class="token comment" spellcheck="true">// 假设我们的插件甚至还要搞聊天前缀</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>然后把这个实例变成二进制数据。</p><p>模仿 Bukkit 的那个 Serializable，利用 Netty Buffer 库，我们可以搞这么一个二进制数据序列化/反序列化的系统出来：</p><pre class="line-numbers language-groovy"><code class="language-groovy"><span class="token keyword">import</span> com<span class="token operator">.</span>google<span class="token operator">.</span>common<span class="token operator">.</span>collect<span class="token operator">.</span>Maps<span class="token keyword">import</span> io<span class="token operator">.</span>netty<span class="token operator">.</span>buffer<span class="token operator">.</span>ByteBuf<span class="token keyword">import</span> io<span class="token operator">.</span>netty<span class="token operator">.</span>buffer<span class="token operator">.</span>Unpooled<span class="token keyword">import</span> org<span class="token operator">.</span>bukkit<span class="token operator">.</span>plugin<span class="token operator">.</span>java<span class="token operator">.</span>JavaPlugin<span class="token keyword">import</span> java<span class="token operator">.</span>nio<span class="token operator">.</span>charset<span class="token operator">.</span>StandardCharsets<span class="token keyword">import</span> java<span class="token operator">.</span>util<span class="token operator">.</span>function<span class="token operator">.</span>Function<span class="token keyword">interface</span> <span class="token class-name">Serializable</span> <span class="token punctuation">{</span>    <span class="token keyword">void</span> <span class="token function">serialize</span><span class="token punctuation">(</span>ByteBuf buf<span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token keyword">class</span> <span class="token class-name">PlayerData</span> <span class="token keyword">implements</span> <span class="token class-name">Serializable</span> <span class="token punctuation">{</span>    <span class="token keyword">int</span> hp <span class="token operator">=</span> <span class="token number">20</span>    <span class="token keyword">double</span> strength <span class="token operator">=</span> <span class="token number">0</span>    <span class="token keyword">boolean</span> haveJob <span class="token operator">=</span> <span class="token boolean">false</span>    Job job    <span class="token annotation punctuation">@Override</span>    <span class="token keyword">void</span> <span class="token function">serialize</span><span class="token punctuation">(</span>ByteBuf buf<span class="token punctuation">)</span> <span class="token punctuation">{</span>        buf<span class="token operator">.</span><span class="token function">writeInt</span><span class="token punctuation">(</span>hp<span class="token punctuation">)</span>        buf<span class="token operator">.</span><span class="token function">writeDouble</span><span class="token punctuation">(</span>strength<span class="token punctuation">)</span>        buf<span class="token operator">.</span><span class="token function">writeBoolean</span><span class="token punctuation">(</span>haveJob<span class="token punctuation">)</span>        <span class="token keyword">if</span> <span class="token punctuation">(</span>haveJob<span class="token punctuation">)</span> <span class="token punctuation">{</span>            job<span class="token operator">.</span><span class="token function">serialize</span><span class="token punctuation">(</span>buf<span class="token punctuation">)</span>        <span class="token punctuation">}</span>    <span class="token punctuation">}</span>    <span class="token keyword">static</span> PlayerData <span class="token function">deserialize</span><span class="token punctuation">(</span>ByteBuf buf<span class="token punctuation">)</span> <span class="token punctuation">{</span>        PlayerData data <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">PlayerData</span><span class="token punctuation">(</span><span class="token punctuation">)</span>        data<span class="token operator">.</span>hp <span class="token operator">=</span> buf<span class="token operator">.</span><span class="token function">readInt</span><span class="token punctuation">(</span><span class="token punctuation">)</span>        data<span class="token operator">.</span>strength <span class="token operator">=</span> buf<span class="token operator">.</span><span class="token function">readDouble</span><span class="token punctuation">(</span><span class="token punctuation">)</span>        data<span class="token operator">.</span>haveJob <span class="token operator">=</span> buf<span class="token operator">.</span><span class="token function">readBoolean</span><span class="token punctuation">(</span><span class="token punctuation">)</span>        <span class="token keyword">if</span> <span class="token punctuation">(</span>data<span class="token operator">.</span>haveJob<span class="token punctuation">)</span> <span class="token punctuation">{</span>            data<span class="token operator">.</span>job <span class="token operator">=</span> Job<span class="token operator">.</span><span class="token function">deserialize</span><span class="token punctuation">(</span>buf<span class="token punctuation">)</span>        <span class="token punctuation">}</span>        <span class="token keyword">return</span> data    <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token keyword">class</span> <span class="token class-name">Job</span> <span class="token keyword">implements</span> <span class="token class-name">Serializable</span> <span class="token punctuation">{</span>    String name    <span class="token keyword">int</span> level    <span class="token keyword">int</span> exp    String prefix <span class="token comment" spellcheck="true">// 假设我们的插件甚至还要搞聊天前缀</span>    <span class="token annotation punctuation">@Override</span>    <span class="token keyword">void</span> <span class="token function">serialize</span><span class="token punctuation">(</span>ByteBuf buf<span class="token punctuation">)</span> <span class="token punctuation">{</span>        Util<span class="token operator">.</span><span class="token function">writeString</span><span class="token punctuation">(</span>buf<span class="token punctuation">,</span> name<span class="token punctuation">)</span>        buf<span class="token operator">.</span><span class="token function">writeInt</span><span class="token punctuation">(</span>level<span class="token punctuation">)</span>        buf<span class="token operator">.</span><span class="token function">writeInt</span><span class="token punctuation">(</span>exp<span class="token punctuation">)</span>        Util<span class="token operator">.</span><span class="token function">writeString</span><span class="token punctuation">(</span>buf<span class="token punctuation">,</span> prefix<span class="token punctuation">)</span>    <span class="token punctuation">}</span>    <span class="token keyword">static</span> Job <span class="token function">deserialize</span><span class="token punctuation">(</span>ByteBuf buf<span class="token punctuation">)</span> <span class="token punctuation">{</span>        Job job <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Job</span><span class="token punctuation">(</span><span class="token punctuation">)</span>        job<span class="token operator">.</span>name <span class="token operator">=</span> Util<span class="token operator">.</span><span class="token function">readString</span><span class="token punctuation">(</span>buf<span class="token punctuation">)</span>        job<span class="token operator">.</span>level <span class="token operator">=</span> buf<span class="token operator">.</span><span class="token function">readInt</span><span class="token punctuation">(</span><span class="token punctuation">)</span>        job<span class="token operator">.</span>exp <span class="token operator">=</span> buf<span class="token operator">.</span><span class="token function">readInt</span><span class="token punctuation">(</span><span class="token punctuation">)</span>        job<span class="token operator">.</span>prefix <span class="token operator">=</span> Util<span class="token operator">.</span><span class="token function">readString</span><span class="token punctuation">(</span>buf<span class="token punctuation">)</span>        <span class="token keyword">return</span> job    <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token keyword">class</span> <span class="token class-name">Util</span> <span class="token punctuation">{</span>    <span class="token keyword">static</span> Map<span class="token operator">&lt;</span>Class<span class="token operator">&lt;</span><span class="token operator">?</span><span class="token operator">></span><span class="token punctuation">,</span> Function<span class="token operator">&lt;</span>ByteBuf<span class="token punctuation">,</span> <span class="token operator">?</span><span class="token operator">>></span> deserializers <span class="token operator">=</span> Maps<span class="token operator">.</span><span class="token function">newHashMap</span><span class="token punctuation">(</span><span class="token punctuation">)</span>    <span class="token keyword">static</span> <span class="token operator">&lt;</span>T<span class="token operator">></span> <span class="token keyword">void</span> <span class="token function">registerDeserializer</span><span class="token punctuation">(</span>Class<span class="token operator">&lt;</span>T<span class="token operator">></span> cl<span class="token punctuation">,</span> Function<span class="token operator">&lt;</span>ByteBuf<span class="token punctuation">,</span> T<span class="token operator">></span> function<span class="token punctuation">)</span> <span class="token punctuation">{</span>        deserializers<span class="token operator">.</span><span class="token function">put</span><span class="token punctuation">(</span>cl <span class="token punctuation">,</span>function<span class="token punctuation">)</span>    <span class="token punctuation">}</span>    <span class="token keyword">static</span> <span class="token operator">&lt;</span>T<span class="token operator">></span> T <span class="token function">deserialize</span><span class="token punctuation">(</span>Class<span class="token operator">&lt;</span>T<span class="token operator">></span> cl<span class="token punctuation">)</span> <span class="token punctuation">{</span>        Function<span class="token operator">&lt;</span>ByteBuf<span class="token punctuation">,</span> <span class="token operator">?</span><span class="token operator">></span> function <span class="token operator">=</span> deserializers<span class="token operator">.</span><span class="token function">get</span><span class="token punctuation">(</span>cl<span class="token punctuation">)</span>        <span class="token keyword">if</span> <span class="token punctuation">(</span>function <span class="token operator">!=</span> null<span class="token punctuation">)</span> <span class="token punctuation">{</span>            <span class="token keyword">return</span> <span class="token punctuation">(</span>T<span class="token punctuation">)</span> function<span class="token operator">.</span><span class="token function">apply</span><span class="token punctuation">(</span>Unpooled<span class="token operator">.</span><span class="token function">buffer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>        <span class="token punctuation">}</span>        <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Exception</span><span class="token punctuation">(</span><span class="token string">"Not registered class $cl"</span><span class="token punctuation">)</span>    <span class="token punctuation">}</span>    <span class="token keyword">static</span> <span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token function">serialize</span><span class="token punctuation">(</span>Object obj<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">if</span> <span class="token punctuation">(</span>obj <span class="token keyword">instanceof</span> <span class="token class-name">Serializable</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>            ByteBuf buf <span class="token operator">=</span> Unpooled<span class="token operator">.</span><span class="token function">buffer</span><span class="token punctuation">(</span><span class="token punctuation">)</span>            obj<span class="token operator">.</span><span class="token function">serialize</span><span class="token punctuation">(</span>buf<span class="token punctuation">)</span>            <span class="token keyword">return</span> buf<span class="token operator">.</span><span class="token function">array</span><span class="token punctuation">(</span><span class="token punctuation">)</span>        <span class="token punctuation">}</span>        <span class="token keyword">return</span> null    <span class="token punctuation">}</span>    <span class="token keyword">static</span> <span class="token keyword">void</span> <span class="token function">writeString</span><span class="token punctuation">(</span>ByteBuf buf<span class="token punctuation">,</span> String value<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">if</span> <span class="token punctuation">(</span>value <span class="token operator">==</span> null<span class="token punctuation">)</span> <span class="token punctuation">{</span>            buf<span class="token operator">.</span><span class="token function">writeInt</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span>            <span class="token keyword">return</span>        <span class="token punctuation">}</span>        <span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span> arr <span class="token operator">=</span> value<span class="token operator">.</span><span class="token function">getBytes</span><span class="token punctuation">(</span>StandardCharsets<span class="token operator">.</span>UTF_8<span class="token punctuation">)</span>        buf<span class="token operator">.</span><span class="token function">writeInt</span><span class="token punctuation">(</span>arr<span class="token operator">.</span>length<span class="token punctuation">)</span>        buf<span class="token operator">.</span><span class="token function">writeBytes</span><span class="token punctuation">(</span>arr<span class="token punctuation">)</span>    <span class="token punctuation">}</span>    <span class="token keyword">static</span> String <span class="token function">readString</span><span class="token punctuation">(</span>ByteBuf buf<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">int</span> len <span class="token operator">=</span> buf<span class="token operator">.</span><span class="token function">readInt</span><span class="token punctuation">(</span><span class="token punctuation">)</span>        <span class="token keyword">if</span> <span class="token punctuation">(</span>len <span class="token operator">==</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token keyword">return</span> null        <span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span> arr <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">byte</span><span class="token punctuation">[</span>len<span class="token punctuation">]</span>        buf<span class="token operator">.</span><span class="token function">readBytes</span><span class="token punctuation">(</span>arr<span class="token punctuation">)</span>        <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">String</span><span class="token punctuation">(</span>arr<span class="token punctuation">,</span> StandardCharsets<span class="token operator">.</span>UTF_8<span class="token punctuation">)</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token keyword">class</span> <span class="token class-name">Plugin</span> <span class="token keyword">extends</span> <span class="token class-name">JavaPlugin</span> <span class="token punctuation">{</span>    <span class="token annotation punctuation">@Override</span>    <span class="token keyword">void</span> <span class="token function">onEnable</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>        Util<span class="token operator">.</span><span class="token function">registerDeserializer</span><span class="token punctuation">(</span>PlayerData<span class="token punctuation">,</span> PlayerData<span class="token punctuation">:</span><span class="token punctuation">:</span>deserialize<span class="token punctuation">)</span>        Util<span class="token operator">.</span><span class="token function">registerDeserializer</span><span class="token punctuation">(</span>Job<span class="token punctuation">,</span> Job<span class="token punctuation">:</span><span class="token punctuation">:</span>deserialize<span class="token punctuation">)</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;这篇文章是来源于&lt;a href=&quot;http://www.mcbbs.net/thread-848236-1-1.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;这个问题&lt;/a&gt;，&lt;br&gt;在回答这个问题之后我思考了一下，怎样存储数据才算优雅又高效，&lt;
      
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>字符串与正则表达式</title>
    <link href="https://izzel.io/2018/12/01/string-regex/"/>
    <id>https://izzel.io/2018/12/01/string-regex/</id>
    <published>2018-12-01T16:32:16.000Z</published>
    <updated>2025-07-13T12:15:43.525Z</updated>
    
    <content type="html"><![CDATA[<p>由于时常能看到一些关于 ItemStack 里面提取 lore 的属性信息的，并且正好最近写了个属性插件，那就来讲一讲正则吧。</p><h1 id="Ⅰ-普通的字符串概念"><a href="#Ⅰ-普通的字符串概念" class="headerlink" title="Ⅰ 普通的字符串概念"></a>Ⅰ 普通的字符串概念</h1><h2 id="一些关于字符串的术语"><a href="#一些关于字符串的术语" class="headerlink" title="一些关于字符串的术语"></a>一些关于字符串的术语</h2><p>以下术语会用于这篇教程中：</p><ul><li><p><code>Σ</code>：字母表集合，在这篇教程中为能打在这个帖子中的任何字符，除了 <code>ε</code> ，因为要用它表示空串</p></li><li><p><code>字符串</code>：来自 <code>Σ</code> 的0或N个字符的有限序列，也称串</p><blockquote><p>在这篇教程中，字符串会用 <code>这种格式</code> 包围</p></blockquote></li><li><p><code>空串</code>：含有0个字符的字符串，也会用 <code>ε</code> 表示</p></li><li><p><code>子串</code>：字符串掐头去尾（0或N个字符）得到的新字符串</p><blockquote><p><code>abcbabc</code> 的子串有 <code>abcbabc</code> <code>abcb</code> <code>babc</code> <code>ba</code> ε<br>也就是说在 Java 里，某串A的子串B 可以让 <code>A.contains(B) = true</code></p></blockquote></li><li><p><code>子序列</code>：字符串随机取出字符（0或N个）得到的新字符串</p><blockquote><p><code>abcbabc</code> 的子序列有 <code>abcbabc</code> <code>abab</code> <code>ca</code> ε</p></blockquote></li><li><p><code>真子串</code>：除了自身以外的子串</p></li><li><p><code>真子序列</code>：除了自身以外的子序列</p></li></ul><h2 id="一些关于语言的术语"><a href="#一些关于语言的术语" class="headerlink" title="一些关于语言的术语"></a>一些关于语言的术语</h2><p>同上，而且这些术语会用的很多：</p><ul><li><p><code>字符串的连接</code>：两个串的连接为第一个串后紧跟着第二个串的串</p><blockquote><p>假设 A 是字符串 <code>hailuo</code> ，B 是字符串 <code>is</code> ，C 是字符串 <code>handsome</code>，那么 A B C 的连接表示为 <code>ABC</code>，也就是 <code>hailuo is handsome</code></p></blockquote></li><li><p><code>字符串的或</code>：两个串的或为 为第一个串或第二个串的串</p><blockquote><p>假设同上，A C 的或表示为 <code>A|C</code>，也就是 <code>hailuo</code> 字符串 或者 <code>handsome</code> 字符串</p></blockquote></li><li><p><code>字符串的Kleene闭包</code>：为0或N个某个串的连接</p><blockquote><p>假设 A 为字符串 <code>ab</code>，<code>A*</code> <del>为一个启发式寻路算法</del> 为 ε <code>ab</code> <code>abab</code> <code>ababababab</code> ….</p></blockquote></li><li><p><code>(字符串)</code>：和 <code>字符串</code> 是一样的</p></li></ul><h2 id="字符串操作的优先级"><a href="#字符串操作的优先级" class="headerlink" title="字符串操作的优先级"></a>字符串操作的优先级</h2><p>在描述一个字符串时，你可能已经遇到了这类似的问题：</p><ul><li><code>aac|dbb</code> 是指 {<code>aac</code>,<code>dbb</code>} 还是 {<code>aacbb</code>, <code>aadbb</code>} 呢</li><li><code>abcd*</code> 是指 {ε, <code>abcd</code>, <code>abcdabcd</code>, ….} 还是 {<code>abc</code>, <code>abcd</code>, <code>abcdddd</code>, …} 呢</li></ul><p>为了解决这些问题，我们可以加括号：</p><ul><li><code>(aac)|(dbb)</code> 指 {<code>aac</code>,<code>dbb</code>}，而 <code>aa(c|d)bb</code> 指 {<code>aacbb</code>, <code>aadbb</code>}</li><li><code>(abcd)*</code> 指 {ε, <code>abcd</code>, <code>abcdabcd</code>, ….}，而 <code>abc((d)*)</code> 指 {<code>abc</code>, <code>abcd</code>, <code>abcdddd</code>, …}</li></ul><p>但是，大量使用括号显然不美观，所以定义以下操作的优先级：</p><p><code>(a) &gt; a* &gt; ab &gt; a|b</code></p><p>也就是说，最上面的两个问题的答案分别为 {<code>aac</code>,<code>dbb</code>} 和 {<code>abc</code>, <code>abcd</code>, <code>abcdddd</code>, …}</p><p>理解以上内容后，你就有足够的基础理解正则表达式了。<br>为了防止你的理解不够深刻，你可以思考一下以下几个问题：</p><ol><li>列出 <code>abcd</code> 的子串、真子串、子序列、真子序列</li><li><code>(a|b)*</code> 描述的是什么串</li><li><code>a((a|(b*))((b)b|(cc)*)|dd)</code> 可以去掉哪些括号并不影响其表达的字符串</li></ol><p>答案见文末</p><hr><h1 id="Ⅱ-普通正则表达式"><a href="#Ⅱ-普通正则表达式" class="headerlink" title="Ⅱ 普通正则表达式"></a>Ⅱ 普通正则表达式</h1><p>正则表达式，可用于匹配给出的字符串是否满足要求，也可以用于从字符串中提取需要的信息。</p><p>初代的正则表达式只有4种语法：</p><ul><li>连接 <code>ab</code></li><li>或 <code>a|b</code></li><li>0个或多个 <code>a*</code></li><li><code>(a)</code> 等于 <code>a</code></li></ul><p>和上一章介绍的优先级是相同的。</p><p>本章从一个例子开始：</p><h2 id="匹配浮点数（floating-point-number）"><a href="#匹配浮点数（floating-point-number）" class="headerlink" title="匹配浮点数（floating point number）"></a>匹配浮点数（floating point number）</h2><p>先来看看定义，我们可以这么表示一个小数：</p><blockquote><p>   $123.456×2^{789}$<br>   $整数.零数×2^{指数}$</p></blockquote><p>Java 的浮点数可以表示为以下格式：</p><blockquote><p>0000.00000E0000</p></blockquote><p>包含一个整数，一个可选的 . 以及一个零数，一个可选的 E 以及指数</p><p>我们将会尝试用正则表达式匹配这个浮点数字符串。</p><p>由于正则表达式一眼望上去不是很直观（做了上一章的第三道题的应该能理解为何），我们先用一种其他的方式表达正则表达式：</p><h2 id="正则定义"><a href="#正则定义" class="headerlink" title="正则定义"></a>正则定义</h2><p>用以下的方式表达一个正则表达式：</p><pre><code>n1 -&gt; r1n2 -&gt; r2n3 -&gt; r3....</code></pre><p>其中 ni 表示一个名字，为了和正则区分开来名字使用斜体。每个 ri 都指一个正则表达式，并可以引用 j &lt; i 的 nj 来代替之前定义的正则。</p><p>如前所述，浮点数包含整数：</p><pre><code>digit -&gt; 0|1|2|3|4|5|6|7|8|9sign -&gt; + | - | εnumber -&gt; digit digit *</code></pre><p>以及一个可选的零数：</p><pre><code>optional_fraction -&gt; .number | ε</code></pre><p>以及一个可选的指数：</p><pre><code>optional_exponent -&gt; ( e | E ) sign number | ε</code></pre><p>浮点数就可以表示为：</p><pre><code>floatingPointNumber -&gt; sign number optional_fraction optional_exponent</code></pre><p>而将名称替换回正则表达式后，最终结果为：</p><pre><code>(+|-|)((0|1|2|3|4|5|6|7|8|9)(0|1|2|3|4|5|6|7|8|9)*)(.((0|1|2|3|4|5|6|7|8|9)(0|1|2|3|4|5|6|7|8|9)*)|)((e|E)(+|-|)((0|1|2|3|4|5|6|7|8|9)(0|1|2|3|4|5|6|7|8|9)*)|)</code></pre><p>这个正则表达式只使用了上面的四种语法，这也是你正式写出的第一个正则表达式（如果你之前没写过的话），尽管它又长又丑，但是它可以匹配各种能在 Java 中编译通过?的浮点数。</p><h2 id="拓展正则表达式语法"><a href="#拓展正则表达式语法" class="headerlink" title="拓展正则表达式语法"></a>拓展正则表达式语法</h2><p>考虑</p><pre><code>digit -&gt; 0|1|2|3|4|5|6|7|8|9</code></pre><p>太麻烦了，我们添加一种语法：<code>[abc]</code> 表示 <code>a|b|c</code> 即提供的字符中的任意一个，并且还提供了语法 <code>[a-b]</code> 表示从 a 到 b 的连续的字符，如 <code>[a-z]</code> 表示所有小写字母，<code>[a-zA-Z]</code> 表示所有字母。</p><p>因此上面的定义可以更换为</p><pre><code>digit -&gt; [0-9]</code></pre><p>考虑</p><pre><code>number -&gt; digit digit *</code></pre><p>太麻烦了，我们添加一种语法：<code>a+</code> 表示 1或N个a，因此上面的定义可以更换为</p><pre><code>number -&gt; digit +</code></pre><p>考虑</p><pre><code>sign -&gt; + | - | εoptional_fraction -&gt; .number | εoptional_exponent -&gt; ( e | E ) sign number | ε</code></pre><p>它们都使用了 <code>xxxx | ε</code> 表达可选的字符串，太麻烦了，我们添加一种语法 <code>a?</code> 表达0或1个a，因此上面的定义可以更换为：</p><pre><code>sign -&gt; [+-]?optional_fraction -&gt; ( .number )?optional_exponent -&gt; ( [eE] sign number )?</code></pre><p>因此，浮点数的正则表达式可以化简为：</p><pre><code>[+-]?[0-9]+(.[0-9]+)?([Ee][+-]?[0-9]+)?</code></pre><p>这是你写的第二个正则表达式，它短了许多，功能与上面第一个一模一样。但是，这还不是最短的。</p><h2 id="转义与元字符"><a href="#转义与元字符" class="headerlink" title="转义与元字符"></a>转义与元字符</h2><p>你可能注意到了上面出现了3次 <code>[0-9]</code> ，并且你以后可能还会用无数次 <code>[0-9]</code> 来匹配数字，为了防止这种情况发生，元字符出现了。</p><p>元字符比较多，但是有几个最好记住（标红），鉴于网上一搜就能知道元字符有哪些，但是这里还是会提供一份。</p><p>同时，既然有了元字符以及上面新加的新语法，类似 . + - \ ^ $ 的符号都有了特殊的意义，在作为字符使用时应在前面加上 <code>\</code></p><table><thead><tr><th>代码</th><th>说明</th></tr></thead><tbody><tr><td>.</td><td>匹配除换行符以外的任意字符</td></tr><tr><td>\w</td><td>匹配字母或数字或下划线或汉字</td></tr><tr><td>\s</td><td>匹配任意的空白符</td></tr><tr><td>\d</td><td>匹配数字</td></tr><tr><td>\b</td><td>匹配单词的开始或结束</td></tr><tr><td>^</td><td>匹配字符串的开始</td></tr><tr><td>$</td><td>匹配字符串的结束</td></tr><tr><td>\w</td><td>匹配任意不是字母，数字，下划线，汉字的字符</td></tr><tr><td>\S</td><td>匹配任意不是空白符的字符</td></tr><tr><td>\D</td><td>匹配任意非数字的字符</td></tr><tr><td>\B</td><td>匹配不是单词开头或结束的位置</td></tr><tr><td>\a</td><td>报警字符(打印它的效果是电脑嘀一声)</td></tr><tr><td>\b</td><td>通常是单词分界位置，但如果在字符类里使用代表退格</td></tr><tr><td>\t</td><td>制表符，Tab</td></tr><tr><td>\r</td><td>回车</td></tr><tr><td>\v</td><td>竖向制表符</td></tr><tr><td>\f</td><td>换页符</td></tr><tr><td>\n</td><td>换行符</td></tr><tr><td>\e</td><td>Escape</td></tr><tr><td>\0nn</td><td>ASCII代码中八进制代码为nn的字符</td></tr><tr><td>\xnn</td><td>ASCII代码中十六进制代码为nn的字符</td></tr><tr><td>\unnnn</td><td>Unicode代码中十六进制代码为nnnn的字符</td></tr><tr><td>\cN</td><td>ASCII控制字符。比如\cC代表Ctrl+C</td></tr><tr><td>\A</td><td>字符串开头(类似^，但不受处理多行选项的影响)</td></tr><tr><td>\Z</td><td>字符串结尾或行尾(不受处理多行选项的影响)</td></tr><tr><td>\z</td><td>字符串结尾(类似$，但不受处理多行选项的影响)</td></tr><tr><td>\G</td><td>当前搜索的开头</td></tr><tr><td>\p{name}</td><td>Unicode中命名为name的字符类，例如\p{IsGreek}</td></tr></tbody></table><p>也就是说，浮点数的正则可以写作：（注意小数点及正负号的转义）</p><pre><code>[\+\-]?\d+(\.\d+)?([Ee][\+\-]?\d+)?</code></pre><h2 id="其他的语法"><a href="#其他的语法" class="headerlink" title="其他的语法"></a>其他的语法</h2><ul><li>重复：</li></ul><blockquote><p>   <code>a{m,n}</code> 表示重复m到n遍的a<br>    <code>a{n}</code> 表示重复n遍的a</p></blockquote><ul><li>字符类：<br>对于类似 <code>[abc]</code> 的语法（叫字符类），还有更多用途：</li></ul><blockquote><p>   <code>[^a-f]</code> 匹配除了 <code>abcdef</code> 的任意字符<br>    <code>[a-d[m-p]]</code> 取 <code>a-z</code>和<code>m-p</code>的并集，即 <code>[a-dm-p]</code><br>    <code>[a-z&amp;&amp;[def]]</code> 取 <code>a-z</code>和<code>def</code>的交集，即 <code>[def]</code><br>    <code>[a-z&amp;&amp;[^bc]]</code> 除了 <code>bc</code> 以外的 <code>a-z</code>，即 <code>[ad-z]</code> （补集）<br>    <code>[a-z&amp;&amp;[^m-p]]</code> 除了 <code>m-p</code> 以外的 <code>a-z</code>，即<code>[a-lq-z]</code> （补集）</p></blockquote><p>到此，你已经掌握了正则表达式中的大部分的内容了，如果不需要更高级的用途，你可以在此停止了。</p><p>当然，在此提供一些题目供思考练习</p><ol start="0"><li>设计匹配 Minecraft 中物品 lore 的正则表达式</li><li>本章例中，浮点数匹配仍有一些问题，比如无法辨别 0001.2 这类非法浮点数，并且对 abc123.5e6def 仍能匹配成功，请尝试完善这个正则表达式，并尝试化简。</li><li>设计匹配 ipv4 ip地址的正则定义，再转化为正则表达式</li></ol><p>答案见文末</p><hr><h1 id="Ⅲ-高级的正则表达式"><a href="#Ⅲ-高级的正则表达式" class="headerlink" title="Ⅲ 高级的正则表达式"></a>Ⅲ 高级的正则表达式</h1><h2 id="组"><a href="#组" class="headerlink" title="组"></a>组</h2><p>在之前，我们介绍了括号操作符，用于改变优先级。括号不止这点用处。</p><p>括号可用于分组，我们再次拿出这个匹配浮点数的正则表达式作为例子：</p><pre><code>[\+\-]?\d+(\.\d+)?([Ee][\+\-]?\d+)?</code></pre><p>在此例中，共有2个括号对，我们称之为两个组：<code>(\.\d+)</code> 和 <code>([Ee][\+\-]?\d+)</code>，分别为组 1 组 2；同时，整个正则表达式作为默认的组 0。</p><p>当然，按照组数一个一个数实在是太麻烦了，好在我们可以给组命名：</p><p><code>(?&lt;name&gt;regex)</code> 为一个名为 name 的组。<br><code>(?:regex)</code> 为一个无名且不进行计数的组。</p><p>组有何用呢？接下来你就能看到了。</p><h2 id="后向引用-Backreference"><a href="#后向引用-Backreference" class="headerlink" title="后向引用 Backreference"></a>后向引用 Backreference</h2><p>后向引用可用于匹配之前出现过的文本，使用组作为标记。</p><p>其中，我们可以使用 <code>\1 \2 \n</code> 代表数字组匹配的字符串，也可以使用 <code>\k&lt;name&gt;</code> 匹配之前 name 组匹配的字符串。</p><p>举个例子，假如我们只想匹配整数和零数相同的小数，我们可以写：</p><pre><code>(\d+)\.\1</code></pre><p>其中后面的 \1 为前一个组 <code>(\d+)</code> 匹配的数字，所以这个正则表达式可以匹配 123.123，却不匹配 123.124。</p><p>当然，既然可以给组命名，那么也就可以这么写：</p><pre><code>(?&lt;number&gt;\d+)\.\k&lt;number&gt;</code></pre><p>这个正则表达式作用和上面相同。</p><h2 id="零宽度匹配-zero-width"><a href="#零宽度匹配-zero-width" class="headerlink" title="零宽度匹配 zero-width"></a>零宽度匹配 zero-width</h2><p>零宽度匹配，也有人叫它零宽断言。</p><p>零宽度是指，这个匹配组并不会消耗字符：<br>假如说你想匹配1.某个前方或后方满足特殊要求的字符串，但是2.前方或者后方的字符可能还需要用于其他的匹配，<br>普通的匹配会吃掉这些字符用于1.满足要求的字符，而导致用于2.还需要匹配的部分匹配失败。<br>也就是说，零宽匹配中的正则表达式仅用于要求测试，不影响其他匹配。读不懂这段话没关系，可以结合后例。</p><blockquote><p>   零宽肯定先行断言：<code>reg1(?=reg2)</code> 断言 reg1 匹配的字符串后方出现匹配 reg2 的字符串<br>    零宽否定先行断言：<code>reg1(?!reg2)</code> 断言 reg1 匹配的字符串后方不出现匹配 reg2 的字符串<br>    零宽肯定后行断言：<code>(?&lt;=reg2)reg1</code> 断言 reg1 匹配的字符串前方出现匹配 reg2 的字符串<br>    零宽否定后行断言：<code>(?&lt;!reg2)reg1</code> 断言reg1 匹配的字符串前方不出现匹配 reg2 的字符串</p></blockquote><p>这里的先行后行是指，在匹配的回溯过程中，当找到 reg1 的内容后，如果向文本前方（正向）查找断言，则为先行（lookahead）；<br>若找到 reg1 后，需要向文本后方（倒着）查找断言，则为后行（lookbehind）。方便记忆的方法就是，先行 reg1 放前，后行 reg1 放后。</p><p>接着举几个例子：</p><blockquote><p><code>aaa(?=bbb)bbb</code>，可以匹配 <code>aaabbb</code>，此例说明何为零宽：不占用后续匹配的字符串<br><code>abc(?=def)</code>，不能匹配任何东西，因为整个正则表达式需要满足仅含有 abc 三个字符（断言是不会消耗字符的），但是断言又要求 abc 后跟随着 def<br><code>abc(?=def).*def(?=ghi).*</code>，匹配文段中跟随者def的abc，并且在不远的后面出现了跟随着ghi的def，所以这个例子可以匹配 <code>abcdefxxxxxxdefghi</code>，也可以匹配 <code>abcdefghi</code> 。<br><code>abc(?=def).*def(?=ghi)</code>，无法匹配 <code>abcdefghi</code> ，原因参见第二个例子。</p></blockquote><h2 id="贪婪、懒惰与占有"><a href="#贪婪、懒惰与占有" class="headerlink" title="贪婪、懒惰与占有"></a>贪婪、懒惰与占有</h2><p>在之前我们讲到重复时，如果你自己做过测试，那么你会发现，<code>a.*b</code> 会匹配 <code>ababab</code> 中的 <code>ababab</code> 而不是 <code>ab</code>；<br>也就是说，默认的重复语 <code>*</code> 尝试匹配最长的那个字符串。假如我们想匹配更短一些的呢？</p><blockquote><p>贪婪量词：<code>regex</code>，表示能匹配 regex 的最长字符串，比如 <code>a*</code> 匹配 <code>aaaaa</code> 会匹配 <code>aaaaa</code><br>懒惰量词：<code>regex?</code>，表示能匹配 regex 的最短字符串，比如 <code>a*?</code> 匹配 <code>aaaaa</code> 会匹配 <code>ε</code><br>占有量词：<code>regex+</code>，表示能不回溯地匹配 regex 的最长字符串，比如 <code>a*+</code> 匹配 <code>aaaaa</code> 会匹配 <code>aaaaa</code></p></blockquote><p>这样简略的介绍可能没人能理解什么是占有量词，并且对其他两种没有一个直观的认识，那么来看例子：</p><p>字符串模板为 <code>abbbabbcbbabbc</code></p><blockquote><p>贪婪的正则表达式为 <code>[abc]*c</code> ，会匹配 <code>abbbabbcbbabbc</code> （尽可能匹配长）<br>懒惰的正则表达式为 <code>[abc]*?c</code> ，会匹配 <code>abbbabbc</code> 和 <code>bbabbc</code> （尽可能匹配短）<br>占有的正则表达式为 <code>[abc]*+c</code> ，什么也不会匹配。</p></blockquote><p>为什么呢？</p><p>占有模式下，<code>[abc]*+</code> 这一部分，可以完全匹配整个字符串，而占有模式下不进行回溯，也就是说 <code>[abc]*+</code> 会用掉所有字符，而使最后一个 c 没有任何字符匹配，因此匹配失败。</p><p>而贪婪模式下，尽管 <code>[abc]*</code> 可以匹配整个字符串（abbbabbcbbabbc），但是因为还有一个 c 没有匹配，因此回溯向前查找，最终<code>[abc]*</code> 匹配的是 abbbabbcbbabb 。</p><h2 id="独立的非非捕获性的组"><a href="#独立的非非捕获性的组" class="headerlink" title="独立的非非捕获性的组"></a>独立的非非捕获性的组</h2><p><code>(?&gt;regex)</code>，表示一旦这个组匹配成功后，不再对这个组进行回溯。</p><p>例子：</p><blockquote><p><code>a(bc|b)c</code> 可以匹配 abcc 和 abc，因为在处理第一个或操作<code>bc|b</code>，正则引擎记住了要在这里回溯，因此会对bcc和bc都进行匹配。<br><code>a(?&gt;bc|b)c</code> 可以匹配 abcc，但不会匹配 abc，因为第一遍匹配 abc 时，<code>a(?&gt;bc|b)</code> 这一部分已经匹配了 abc，因此不在这里回溯，而最后的一个 c 当然就匹配失败了。</p></blockquote><h2 id="非转义匹配"><a href="#非转义匹配" class="headerlink" title="非转义匹配"></a>非转义匹配</h2><p><code>\Q</code> 表示非转义字符串的开始，<code>\E</code> 表示非转义字符串的结束。<br>比如 <code>\Q[\+\-]?\d+(\.\d+)?([Ee][\+\-]?\d+)?\E</code> 会匹配 <code>[\+\-]?\d+(\.\d+)?([Ee][\+\-]?\d+)?</code> 这个字符串。</p><p>本章有一定难度，请对提供的所有例子都进行思考，了解”为什么会这样匹配”。</p><p>本章到此结束。</p><p>按照惯例，提供一些题目进行思考练习。</p><ol><li><code>\b(?&lt;first&gt;\w+)\b\s+(\b\k&lt;first&gt;\b\s*)+</code> 是干什么用的</li><li><code>^((?!RegularExpression).)*$</code> 是干什么用的</li></ol><p>答案见文末</p><h1 id="Ⅳ-两个正则使用实例"><a href="#Ⅳ-两个正则使用实例" class="headerlink" title="Ⅳ 两个正则使用实例"></a>Ⅳ 两个正则使用实例</h1><h2 id="Notepad-Sublime-Text-中搜索替换"><a href="#Notepad-Sublime-Text-中搜索替换" class="headerlink" title="Notepad++ / Sublime Text 中搜索替换"></a>Notepad++ / Sublime Text 中搜索替换</h2><p>比如把 markdown 的图片链接转换为 discuz 的</p><p><img src="search.png" alt></p><p>点击 replace 后变成了</p><p><img src="replace.png" alt></p><p>如图，可以使用组</p><h2 id="Java-中全文搜索"><a href="#Java-中全文搜索" class="headerlink" title="Java 中全文搜索"></a>Java 中全文搜索</h2><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">package</span> io<span class="token punctuation">.</span>izzel<span class="token punctuation">.</span>strtutor<span class="token punctuation">;</span><span class="token keyword">import</span> java<span class="token punctuation">.</span>util<span class="token punctuation">.</span>regex<span class="token punctuation">.</span>Matcher<span class="token punctuation">;</span><span class="token keyword">import</span> java<span class="token punctuation">.</span>util<span class="token punctuation">.</span>regex<span class="token punctuation">.</span>Pattern<span class="token punctuation">;</span><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Main</span> <span class="token punctuation">{</span>    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">void</span> <span class="token function">main</span><span class="token punctuation">(</span>String<span class="token punctuation">[</span><span class="token punctuation">]</span> args<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token comment" spellcheck="true">// 正则表达式</span>        Pattern pattern <span class="token operator">=</span> Pattern<span class="token punctuation">.</span><span class="token function">compile</span><span class="token punctuation">(</span><span class="token string">"\\b(?&lt;first>\\w+)\\b\\s+(\\b\\k&lt;first>\\b\\s*)+"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token comment" spellcheck="true">// 输入的内容</span>        Matcher input <span class="token operator">=</span> pattern<span class="token punctuation">.</span><span class="token function">matcher</span><span class="token punctuation">(</span><span class="token string">"A b c c b a b f f d."</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token keyword">while</span> <span class="token punctuation">(</span>input<span class="token punctuation">.</span><span class="token function">find</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>            <span class="token comment" spellcheck="true">// 默认第 0 组，还记得前面讲的怎么分组吗</span>            System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>input<span class="token punctuation">.</span><span class="token function">group</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>            <span class="token comment" spellcheck="true">// 叫 first 的组</span>            System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>input<span class="token punctuation">.</span><span class="token function">group</span><span class="token punctuation">(</span><span class="token string">"first"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>            <span class="token comment" spellcheck="true">// 第二个组</span>            System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>input<span class="token punctuation">.</span><span class="token function">group</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token punctuation">}</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>更多 Java 相关的 API 比如 <code>String#replaceAll</code> <code>String#split</code> <code>String#matches</code> 以及 <code>Pattern</code> <code>Matcher</code> 的高级用法请结合搜索引擎了解。</p><hr><h1 id="答案"><a href="#答案" class="headerlink" title="答案"></a>答案</h1><h2 id="1"><a href="#1" class="headerlink" title="1"></a>1</h2><ol><li><blockquote><p>子串 {ε, a, b, c, d, ab, bc, cd, abc, bcd, abcd}<br>真子串 {ε, a, b, c, d, ab, bc, cd, abc, bcd}<br>子序列 {ε, a, b, c, d, ab, ac, ad, bc, bd, cd, abc, abd, acd, bcd, abcd}<br>真子序列 {ε, a, b, c, d, ab, ac, ad, bc, bd, cd, abc, abd, acd, bcd}</p></blockquote></li><li>任意 a b 组成的任意长度的字符串<blockquote><p>{ε, a, b, aa, bb, ab, ba, aaa, bbb, aab, aba, babbababbab, …}</p></blockquote></li><li><code>a((a|b*)(bb|(cc)*)|dd)</code></li></ol><h2 id="2"><a href="#2" class="headerlink" title="2"></a>2</h2><ol><li><code>^[\+\-]?(0|0?[1-9][0-9]*)(\.\d+)?([Ee][\+\-]?0?[1-9]+)?$</code></li><li><pre><code>25x -&gt; 25[0-5]2xx -&gt; 2[0-4]\dany -&gt; [01]?\d\d?number -&gt; 2xx | 25x | anyip -&gt; number . number . number . number</code></pre></li></ol><p>((2[0-4]\d|25[0-5]|[01]?\d\d?).){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)<br>```</p><h2 id="3"><a href="#3" class="headerlink" title="3"></a>3</h2><ol><li>匹配连续出现两次以上相同单词的字符串，比如 <code>mc mc mc</code></li><li>匹配一行不包含 <code>RegularExpression</code> 这个单词的字符串</li></ol>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;由于时常能看到一些关于 ItemStack 里面提取 lore 的属性信息的，并且正好最近写了个属性插件，那就来讲一讲正则吧。&lt;/p&gt;
&lt;h1 id=&quot;Ⅰ-普通的字符串概念&quot;&gt;&lt;a href=&quot;#Ⅰ-普通的字符串概念&quot; class=&quot;headerlink&quot; title=&quot;Ⅰ
      
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>Hello World</title>
    <link href="https://izzel.io/2018/12/01/hello-world/"/>
    <id>https://izzel.io/2018/12/01/hello-world/</id>
    <published>2018-12-01T00:00:00.000Z</published>
    <updated>2025-07-13T12:15:43.524Z</updated>
    
    <content type="html"><![CDATA[<p>海螺的blog就在2018/12/1启用了。</p><p>QAQ</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;海螺的blog就在2018/12/1启用了。&lt;/p&gt;
&lt;p&gt;QAQ&lt;/p&gt;

      
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>如何写一个世界生成器</title>
    <link href="https://izzel.io/2018/08/04/write-a-world-generator/"/>
    <id>https://izzel.io/2018/08/04/write-a-world-generator/</id>
    <published>2018-08-04T12:00:00.000Z</published>
    <updated>2025-07-13T12:15:43.526Z</updated>
    
    <content type="html"><![CDATA[<p>本文基于 Minecraft 1.13，介绍 Bukkit 中的世界生成器，和地形生成的部分原理与运用。</p><h2 id="加工现有-World-Generator"><a href="#加工现有-World-Generator" class="headerlink" title="加工现有 World Generator"></a>加工现有 World Generator</h2><p>在 Bukkit 中，有一个 <code>org.bukkit.WorldCreator</code> 类，可以用于创建新的世界；而这个类中，有一个名为 <code>generator()</code> 的方法可以提供自定义的地形生成器（这也是下一节将会讲到的东西），如果不提供 generator 的话，Bukkit 将会使用内部的生成器。</p><p>Minecraft 原版的世界生成分为两个阶段，Generation 和 Population。我们将会对 Population 阶段进行修改：<br>世界加载时会触发 WorldInitEvent，这时世界已经加载好了所需的相关设置，即将进行区块生成。我们需要为这个世界添加自定义的 BlockPopulator：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">ExtendVanillaGenerator</span> <span class="token keyword">implements</span> <span class="token class-name">Listener</span> <span class="token punctuation">{</span>    <span class="token annotation punctuation">@EventHandler</span>    <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">onInit</span><span class="token punctuation">(</span>WorldInitEvent event<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token string">"world"</span><span class="token punctuation">.</span><span class="token function">equals</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span><span class="token function">getWorld</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getName</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>            event<span class="token punctuation">.</span><span class="token function">getWorld</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getPopulators</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">PumpkinPopulator</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token punctuation">}</span>    <span class="token punctuation">}</span>    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">PumpkinPopulator</span> <span class="token keyword">extends</span> <span class="token class-name">BlockPopulator</span> <span class="token punctuation">{</span>        <span class="token annotation punctuation">@Override</span>        <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">populate</span><span class="token punctuation">(</span>World world<span class="token punctuation">,</span> Random random<span class="token punctuation">,</span> Chunk chunk<span class="token punctuation">)</span> <span class="token punctuation">{</span>            <span class="token comment" spellcheck="true">// 随机生成一些南瓜的数量</span>            <span class="token keyword">int</span> amount <span class="token operator">=</span> random<span class="token punctuation">.</span><span class="token function">nextInt</span><span class="token punctuation">(</span><span class="token number">8</span><span class="token punctuation">)</span><span class="token punctuation">;</span>            <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">int</span> i <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> i <span class="token operator">&lt;</span> amount<span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>                <span class="token comment" spellcheck="true">// 随机位置</span>                <span class="token keyword">int</span> x <span class="token operator">=</span> random<span class="token punctuation">.</span><span class="token function">nextInt</span><span class="token punctuation">(</span><span class="token number">16</span><span class="token punctuation">)</span><span class="token punctuation">;</span>                <span class="token keyword">int</span> z <span class="token operator">=</span> random<span class="token punctuation">.</span><span class="token function">nextInt</span><span class="token punctuation">(</span><span class="token number">16</span><span class="token punctuation">)</span><span class="token punctuation">;</span>                <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">int</span> y <span class="token operator">=</span> <span class="token number">255</span><span class="token punctuation">;</span> y <span class="token operator">>=</span> <span class="token number">0</span><span class="token punctuation">;</span> y<span class="token operator">--</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>                    <span class="token keyword">if</span> <span class="token punctuation">(</span>chunk<span class="token punctuation">.</span><span class="token function">getBlock</span><span class="token punctuation">(</span>x<span class="token punctuation">,</span> y<span class="token punctuation">,</span> z<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getType</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">!=</span> Material<span class="token punctuation">.</span>AIR<span class="token punctuation">)</span> <span class="token punctuation">{</span>                        <span class="token comment" spellcheck="true">// 只让南瓜生成在草方块上</span>                        <span class="token keyword">if</span> <span class="token punctuation">(</span>chunk<span class="token punctuation">.</span><span class="token function">getBlock</span><span class="token punctuation">(</span>x<span class="token punctuation">,</span> y<span class="token punctuation">,</span> z<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getType</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">==</span> Material<span class="token punctuation">.</span>GRASS_BLOCK<span class="token operator">&amp;&amp;</span> chunk<span class="token punctuation">.</span><span class="token function">getBlock</span><span class="token punctuation">(</span>x<span class="token punctuation">,</span> y <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">,</span> z<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getType</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">==</span> Material<span class="token punctuation">.</span>AIR<span class="token punctuation">)</span>                            chunk<span class="token punctuation">.</span><span class="token function">getBlock</span><span class="token punctuation">(</span>x<span class="token punctuation">,</span> y <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">,</span> z<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">setType</span><span class="token punctuation">(</span>Material<span class="token punctuation">.</span>PUMPKIN<span class="token punctuation">)</span><span class="token punctuation">;</span>                        <span class="token keyword">break</span><span class="token punctuation">;</span>                    <span class="token punctuation">}</span>                <span class="token punctuation">}</span>            <span class="token punctuation">}</span>        <span class="token punctuation">}</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>我们想让世界在任何地方都随机生成一些南瓜，那么启动游戏看看效果：</p><p><img src="pumpkins.png" alt></p><p>南瓜的确变多了。</p><p>Minecraft 原版的所有世界生成的类都在 nms 包内以 WorldGen 开头，可以自行反编译查看他们的实现：</p><p><img src="worldgen-classes.png" alt></p><h2 id="简单的超平坦生成器"><a href="#简单的超平坦生成器" class="headerlink" title="简单的超平坦生成器"></a>简单的超平坦生成器</h2><p>假设你对 Bukkit 插件已经有了一些了解，主类大概看起来是这样的：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">WorldGenTutor</span> <span class="token keyword">extends</span> <span class="token class-name">JavaPlugin</span> <span class="token punctuation">{</span>    <span class="token annotation punctuation">@Override</span>    <span class="token keyword">public</span> ChunkGenerator <span class="token function">getDefaultWorldGenerator</span><span class="token punctuation">(</span>String worldName<span class="token punctuation">,</span> String id<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token keyword">return</span> null<span class="token punctuation">;</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>覆盖 getDefaultWorldGenerator 方法，然后编辑 bukkit.yml ：</p><pre class="line-numbers language-yaml"><code class="language-yaml"><span class="token key atrule">worlds</span><span class="token punctuation">:</span>  <span class="token key atrule">world</span><span class="token punctuation">:</span>    <span class="token key atrule">generator</span><span class="token punctuation">:</span> 插件名<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>当然，你也可以为这个世界指定同一个插件的不同生成器（当然你的插件需要根据 id 判断并实现对应功能），将 generator 后编辑为 <code>插件名:id</code> 即可，这里的 id 将会作为上文的方法的第二个 String 参数。</p><p>最后，如果你想要控制主世界的生成，你需要在 <code>plugin.yml</code> 中加上 <code>load: startup</code>。</p><p>届时，Bukkit 已经认可你的插件是可以提供世界生成器了，但是目前而言，这个方法仍然返回 null，我们需要给他加上对应的功能。</p><h3 id="实现一个-ChunkGenerator"><a href="#实现一个-ChunkGenerator" class="headerlink" title="实现一个 ChunkGenerator"></a>实现一个 ChunkGenerator</h3><p>创建一个新的类，看起来是这样的：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">FlatGenerator</span> <span class="token keyword">extends</span> <span class="token class-name">ChunkGenerator</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>超平坦应该是这样的：</p><p><img src="flat.png" alt></p><p>最下两层为基岩，第三层为草方块，其他什么也不要：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">FlatGenerator</span> <span class="token keyword">extends</span> <span class="token class-name">ChunkGenerator</span> <span class="token punctuation">{</span>    <span class="token annotation punctuation">@Override</span>    <span class="token keyword">public</span> ChunkData <span class="token function">generateChunkData</span><span class="token punctuation">(</span>World world<span class="token punctuation">,</span> Random random<span class="token punctuation">,</span> <span class="token keyword">int</span> x<span class="token punctuation">,</span> <span class="token keyword">int</span> z<span class="token punctuation">,</span> BiomeGrid biome<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token comment" spellcheck="true">// 创建区块数据</span>        ChunkData chunkData <span class="token operator">=</span> <span class="token function">createChunkData</span><span class="token punctuation">(</span>world<span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token comment" spellcheck="true">// 一个区块的大小为 16*16，高度为 0-255</span>        <span class="token comment" spellcheck="true">// 将这个区块的 (0,0,0) 到 (16,2,16) ，即最低两层填充为基岩</span>        chunkData<span class="token punctuation">.</span><span class="token function">setRegion</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">16</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token number">16</span><span class="token punctuation">,</span> Material<span class="token punctuation">.</span>BEDROCK<span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token comment" spellcheck="true">// 将第三层填充为草方块</span>        chunkData<span class="token punctuation">.</span><span class="token function">setRegion</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">16</span><span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">,</span> <span class="token number">16</span><span class="token punctuation">,</span> Material<span class="token punctuation">.</span>GRASS_BLOCK<span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token comment" spellcheck="true">// 将整个区块的生物群系设置为平原（PLAINS）</span>        <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">int</span> i <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> i <span class="token operator">&lt;</span> <span class="token number">16</span><span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>            <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">int</span> j <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> j <span class="token operator">&lt;</span> <span class="token number">16</span><span class="token punctuation">;</span> j<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>                biome<span class="token punctuation">.</span><span class="token function">setBiome</span><span class="token punctuation">(</span>i<span class="token punctuation">,</span> j<span class="token punctuation">,</span> Biome<span class="token punctuation">.</span>PLAINS<span class="token punctuation">)</span><span class="token punctuation">;</span>            <span class="token punctuation">}</span>        <span class="token punctuation">}</span>        <span class="token keyword">return</span> chunkData<span class="token punctuation">;</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>ChunkData 类用于存储世界的方块信息，BiomeGrid 用于存储生物群系信息。</p><p>这一节就在这里结束了，至此，如果认真阅读了源码，你应该已经了解了如何创建一个 Bukkit 上的地图生成器了。</p><h2 id="基于噪声的生成器与-BlockPopulator"><a href="#基于噪声的生成器与-BlockPopulator" class="headerlink" title="基于噪声的生成器与 BlockPopulator"></a>基于噪声的生成器与 BlockPopulator</h2><p>目前，网络上能找到的各种世界生成器的教程/资源，按照以下的方式生成一个地图：</p><ul><li>使用噪声函数生成一串随机但是连续的数字</li><li>使用这些数字的大小表示地形的高度/湿度/其他属性</li><li>使用这些属性决定生物群系</li></ul><p>什么是噪声函数呢？</p><p>噪声函数，基本上是一个种子随机发生器。它需要一个数作为参数，然后根据这个参数返回一个随机数。<strong>如果两次都传同一个参数进来，它就会产生两次相同的数。</strong>这个性质决定了 Minecraft 使用相同的种子总是生成相同的地形，</p><p>如果每个相近的参数生成的随机数相差太大，那么 Minecraft 的地形将是无比混乱的。噪声函数在传入连续的数字时，返回的随机数的差值是不大的，整体数值呈现随机但连续起伏。</p><p>关于噪声函数，你可以去<a href="https://blog.csdn.net/candycat1992/article/details/50346469" target="_blank" rel="noopener">这里</a>看看。如果你能够硬肛全洋文文档，你也可以去<a href="https://www.redblobgames.com/articles/noise/introduction.html" target="_blank" rel="noopener">这里</a>看看，这一篇详细的讲解了各种噪声的区别，</p><p>那么 Mojang 的生成器是如何运作的呢？</p><p>根据<a href="https://www.zhihu.com/question/20754279/answer/133715741" target="_blank" rel="noopener">土球的答案</a>，我们可以了解到 Minecraft 的地形生成与上文不同，是先生成生物群系，再通过群系来决定地形（如高度）。生成的地形分为两个大阶段，Generation 时期生成主要的地形，Population 时期生成点缀，比如树。</p><p>土球回答的下面也有一个答案，那个答案比较详细的讲述了 Minecraft 的地形生成过程。</p><h3 id="我们的生成器"><a href="#我们的生成器" class="headerlink" title="我们的生成器"></a>我们的生成器</h3><p>我们采用地形决定群系的方式。首先是生成地形。常用的噪声函数有 Perlin Noise 和 Simplex，我们采用 Simplex。</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">NormalGenerator</span> <span class="token keyword">extends</span> <span class="token class-name">ChunkGenerator</span> <span class="token punctuation">{</span>    <span class="token keyword">private</span> SimplexOctaveGenerator noise<span class="token punctuation">;</span>    <span class="token annotation punctuation">@Override</span>    <span class="token keyword">public</span> ChunkData <span class="token function">generateChunkData</span><span class="token punctuation">(</span>World world<span class="token punctuation">,</span> Random random<span class="token punctuation">,</span> <span class="token keyword">int</span> chunkX<span class="token punctuation">,</span> <span class="token keyword">int</span> chunkZ<span class="token punctuation">,</span> BiomeGrid biome<span class="token punctuation">)</span> <span class="token punctuation">{</span>        ChunkData chunkData <span class="token operator">=</span> <span class="token function">createChunkData</span><span class="token punctuation">(</span>world<span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token comment" spellcheck="true">// 我们需要的噪声生成器</span>        <span class="token keyword">if</span> <span class="token punctuation">(</span>noise <span class="token operator">==</span> null<span class="token punctuation">)</span> <span class="token punctuation">{</span>            noise <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">SimplexOctaveGenerator</span><span class="token punctuation">(</span>world<span class="token punctuation">.</span><span class="token function">getSeed</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span>            <span class="token comment" spellcheck="true">// 我们需要更平缓的地形，所以需要设置 scale</span>            <span class="token comment" spellcheck="true">// 该值越大，地形变化更大</span>            <span class="token comment" spellcheck="true">// 微调即可</span>            noise<span class="token punctuation">.</span><span class="token function">setScale</span><span class="token punctuation">(</span><span class="token number">0.005D</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token punctuation">}</span>        <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">int</span> x <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> x <span class="token operator">&lt;</span> <span class="token number">16</span><span class="token punctuation">;</span> x<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>            <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">int</span> z <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> z <span class="token operator">&lt;</span> <span class="token number">16</span><span class="token punctuation">;</span> z<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>                <span class="token comment" spellcheck="true">// 方块的真实坐标</span>                <span class="token keyword">int</span> realX <span class="token operator">=</span> chunkX <span class="token operator">*</span> <span class="token number">16</span> <span class="token operator">+</span> x<span class="token punctuation">;</span>                <span class="token keyword">int</span> realZ <span class="token operator">=</span> chunkZ <span class="token operator">*</span> <span class="token number">16</span> <span class="token operator">+</span> z<span class="token punctuation">;</span>                <span class="token comment" spellcheck="true">// noise 方法返回 -1 到 1 之间的随机数</span>                <span class="token keyword">double</span> noiseValue <span class="token operator">=</span> noise<span class="token punctuation">.</span><span class="token function">noise</span><span class="token punctuation">(</span>realX<span class="token punctuation">,</span> realZ<span class="token punctuation">,</span> <span class="token number">0.5D</span><span class="token punctuation">,</span> <span class="token number">0.5D</span><span class="token punctuation">)</span><span class="token punctuation">;</span>                <span class="token comment" spellcheck="true">// 将 noise 值放大，作为该点的高度</span>                <span class="token keyword">int</span> height <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token keyword">int</span><span class="token punctuation">)</span> <span class="token punctuation">(</span>noiseValue <span class="token operator">*</span> <span class="token number">40D</span> <span class="token operator">+</span> <span class="token number">100D</span><span class="token punctuation">)</span><span class="token punctuation">;</span>                <span class="token comment" spellcheck="true">// 底层基岩</span>                chunkData<span class="token punctuation">.</span><span class="token function">setBlock</span><span class="token punctuation">(</span>x<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> z<span class="token punctuation">,</span> Material<span class="token punctuation">.</span>BEDROCK<span class="token punctuation">)</span><span class="token punctuation">;</span>                <span class="token comment" spellcheck="true">// 中间石头</span>                <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">int</span> y <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> y <span class="token operator">&lt;</span> height <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">;</span> y<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>                    chunkData<span class="token punctuation">.</span><span class="token function">setBlock</span><span class="token punctuation">(</span>x<span class="token punctuation">,</span> y<span class="token punctuation">,</span> z<span class="token punctuation">,</span> Material<span class="token punctuation">.</span>STONE<span class="token punctuation">)</span><span class="token punctuation">;</span>                <span class="token punctuation">}</span>                <span class="token comment" spellcheck="true">// 表面覆盖泥土和草方块</span>                chunkData<span class="token punctuation">.</span><span class="token function">setBlock</span><span class="token punctuation">(</span>x<span class="token punctuation">,</span> height <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">,</span> z<span class="token punctuation">,</span> Material<span class="token punctuation">.</span>DIRT<span class="token punctuation">)</span><span class="token punctuation">;</span>                chunkData<span class="token punctuation">.</span><span class="token function">setBlock</span><span class="token punctuation">(</span>x<span class="token punctuation">,</span> height<span class="token punctuation">,</span> z<span class="token punctuation">,</span> Material<span class="token punctuation">.</span>GRASS_BLOCK<span class="token punctuation">)</span><span class="token punctuation">;</span>            <span class="token punctuation">}</span>        <span class="token punctuation">}</span>        <span class="token keyword">return</span> chunkData<span class="token punctuation">;</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p><img src="noise-generator.png" alt></p><p>有点单调，除了默认生成的生物以外没有任何东西，并且地形看起来也很单调。</p><h3 id="加点东西"><a href="#加点东西" class="headerlink" title="加点东西"></a>加点东西</h3><p>如果只是小小地点缀一下地图，BlockPopulator 是再适合不过的选择了。</p><p>重写 <code>getDefaultPopulators</code> 方法，返回我们自定义的：树！</p><pre class="line-numbers language-java"><code class="language-java"><span class="token annotation punctuation">@Override</span><span class="token keyword">public</span> List<span class="token operator">&lt;</span>BlockPopulator<span class="token operator">></span> <span class="token function">getDefaultPopulators</span><span class="token punctuation">(</span>World world<span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token keyword">return</span> ImmutableList<span class="token punctuation">.</span><span class="token function">of</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">TreePopulator</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">TreePopulator</span> <span class="token keyword">extends</span> <span class="token class-name">BlockPopulator</span> <span class="token punctuation">{</span>    <span class="token annotation punctuation">@Override</span>    <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">populate</span><span class="token punctuation">(</span>World world<span class="token punctuation">,</span> Random random<span class="token punctuation">,</span> Chunk chunk<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token comment" spellcheck="true">// 假设只有 1/4 的区块生成树</span>        <span class="token keyword">if</span> <span class="token punctuation">(</span>random<span class="token punctuation">.</span><span class="token function">nextInt</span><span class="token punctuation">(</span><span class="token number">4</span><span class="token punctuation">)</span> <span class="token operator">&lt;</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>            <span class="token comment" spellcheck="true">// 假设每个区块生成 1-3 颗树</span>            <span class="token keyword">int</span> amount <span class="token operator">=</span> random<span class="token punctuation">.</span><span class="token function">nextInt</span><span class="token punctuation">(</span><span class="token number">3</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">;</span>            <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">int</span> i <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> i <span class="token operator">&lt;</span> amount<span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>                <span class="token comment" spellcheck="true">// 随机生成树的坐标</span>                <span class="token keyword">int</span> x <span class="token operator">=</span> random<span class="token punctuation">.</span><span class="token function">nextInt</span><span class="token punctuation">(</span><span class="token number">16</span><span class="token punctuation">)</span><span class="token punctuation">;</span>                <span class="token keyword">int</span> z <span class="token operator">=</span> random<span class="token punctuation">.</span><span class="token function">nextInt</span><span class="token punctuation">(</span><span class="token number">16</span><span class="token punctuation">)</span><span class="token punctuation">;</span>                <span class="token keyword">int</span> y <span class="token operator">=</span> <span class="token number">255</span><span class="token punctuation">;</span>                <span class="token comment" spellcheck="true">// 找到最高的方块来生成树</span>                <span class="token keyword">while</span> <span class="token punctuation">(</span>chunk<span class="token punctuation">.</span><span class="token function">getBlock</span><span class="token punctuation">(</span>x<span class="token punctuation">,</span> y<span class="token punctuation">,</span> z<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getType</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">==</span> Material<span class="token punctuation">.</span>AIR<span class="token punctuation">)</span> y<span class="token operator">--</span><span class="token punctuation">;</span>                <span class="token comment" spellcheck="true">// 生成树</span>                world<span class="token punctuation">.</span><span class="token function">generateTree</span><span class="token punctuation">(</span>chunk<span class="token punctuation">.</span><span class="token function">getBlock</span><span class="token punctuation">(</span>x<span class="token punctuation">,</span> y<span class="token punctuation">,</span> z<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getLocation</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>                        <span class="token comment" spellcheck="true">// 搞点有趣的，我们随机选择不同的树生成</span>                        TreeType<span class="token punctuation">.</span><span class="token function">values</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">[</span>random<span class="token punctuation">.</span><span class="token function">nextInt</span><span class="token punctuation">(</span>TreeType<span class="token punctuation">.</span><span class="token function">values</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>length<span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>            <span class="token punctuation">}</span>        <span class="token punctuation">}</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>效果拔群。在下面这个实例里，我把 scale 从 0.005 调成了 0.0025，地图变得非常平缓。</p><p>（最终还是没有蘑菇树，果然还是不行呢）</p><p><img src="trees.png" alt></p><p>地面上不是那么单调了，但是 Minecraft 的特色可不止地面上。现在，我们往这个世界加一点矿物：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token annotation punctuation">@Override</span><span class="token keyword">public</span> List<span class="token operator">&lt;</span>BlockPopulator<span class="token operator">></span> <span class="token function">getDefaultPopulators</span><span class="token punctuation">(</span>World world<span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token keyword">return</span> ImmutableList<span class="token punctuation">.</span><span class="token function">of</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">TreePopulator</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token class-name">DiamondPopulator</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">DiamondPopulator</span> <span class="token keyword">extends</span> <span class="token class-name">BlockPopulator</span> <span class="token punctuation">{</span>    <span class="token annotation punctuation">@Override</span>    <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">populate</span><span class="token punctuation">(</span>World world<span class="token punctuation">,</span> Random random<span class="token punctuation">,</span> Chunk chunk<span class="token punctuation">)</span> <span class="token punctuation">{</span>        <span class="token comment" spellcheck="true">// 假设每个区块只有一个钻石矿</span>        <span class="token comment" spellcheck="true">// 钻石矿脉随机生成在高度 16 以下</span>        <span class="token keyword">int</span> x <span class="token operator">=</span> random<span class="token punctuation">.</span><span class="token function">nextInt</span><span class="token punctuation">(</span><span class="token number">16</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token comment" spellcheck="true">// 不要生成在基岩上</span>        <span class="token keyword">int</span> y <span class="token operator">=</span> random<span class="token punctuation">.</span><span class="token function">nextInt</span><span class="token punctuation">(</span><span class="token number">15</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">;</span>        <span class="token keyword">int</span> z <span class="token operator">=</span> random<span class="token punctuation">.</span><span class="token function">nextInt</span><span class="token punctuation">(</span><span class="token number">16</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token comment" spellcheck="true">// 继续生成的几率</span>        <span class="token keyword">while</span> <span class="token punctuation">(</span>random<span class="token punctuation">.</span><span class="token function">nextDouble</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">&lt;</span> <span class="token number">0.8D</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>            <span class="token comment" spellcheck="true">// 只替换岩石</span>            <span class="token keyword">if</span> <span class="token punctuation">(</span>chunk<span class="token punctuation">.</span><span class="token function">getBlock</span><span class="token punctuation">(</span>x<span class="token punctuation">,</span> y<span class="token punctuation">,</span> z<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getType</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">==</span> Material<span class="token punctuation">.</span>STONE<span class="token punctuation">)</span> <span class="token punctuation">{</span>                chunk<span class="token punctuation">.</span><span class="token function">getBlock</span><span class="token punctuation">(</span>x<span class="token punctuation">,</span> y<span class="token punctuation">,</span> z<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">setType</span><span class="token punctuation">(</span>Material<span class="token punctuation">.</span>DIAMOND_ORE<span class="token punctuation">)</span><span class="token punctuation">;</span>            <span class="token punctuation">}</span>            <span class="token comment" spellcheck="true">// 向某个方向随机继续生成</span>            <span class="token keyword">switch</span> <span class="token punctuation">(</span>random<span class="token punctuation">.</span><span class="token function">nextInt</span><span class="token punctuation">(</span><span class="token number">6</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>                <span class="token keyword">case</span> <span class="token number">0</span><span class="token operator">:</span> x<span class="token operator">++</span><span class="token punctuation">;</span> <span class="token keyword">break</span><span class="token punctuation">;</span>                <span class="token keyword">case</span> <span class="token number">1</span><span class="token operator">:</span> y<span class="token operator">++</span><span class="token punctuation">;</span> <span class="token keyword">break</span><span class="token punctuation">;</span>                <span class="token keyword">case</span> <span class="token number">2</span><span class="token operator">:</span> z<span class="token operator">++</span><span class="token punctuation">;</span> <span class="token keyword">break</span><span class="token punctuation">;</span>                <span class="token keyword">case</span> <span class="token number">3</span><span class="token operator">:</span> x<span class="token operator">--</span><span class="token punctuation">;</span> <span class="token keyword">break</span><span class="token punctuation">;</span>                <span class="token comment" spellcheck="true">// 不要生成到基岩下面去了</span>                <span class="token keyword">case</span> <span class="token number">4</span><span class="token operator">:</span> y <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">max</span><span class="token punctuation">(</span>y<span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">break</span><span class="token punctuation">;</span>                <span class="token keyword">default</span><span class="token operator">:</span> z<span class="token operator">--</span><span class="token punctuation">;</span> <span class="token keyword">break</span><span class="token punctuation">;</span>            <span class="token punctuation">}</span>        <span class="token punctuation">}</span>    <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>经过暴力开采后找到了我们的钻石矿。</p><p><img src="ores.png" alt></p><p>你可以仿照这个方法生成其他矿物。</p><h2 id="玩转-NoiseGenerator-OctaveGenerator"><a href="#玩转-NoiseGenerator-OctaveGenerator" class="headerlink" title="玩转 NoiseGenerator / OctaveGenerator"></a>玩转 NoiseGenerator / OctaveGenerator</h2><p>读了上面几章以后，你一定对代码中出现的 PerlinNoiseGenerator / SimplexOctaveGenerator 感到疑惑了。这一章将会教会你 噪声函数 的各种概念，以及如何使用。</p><p>如果你能够访问 archive.org 并且可以阅读英文，那么你可以看看<a href="https://web.archive.org/web/20160530124230/http://freespace.virgin.net/hugo.elias/models/m_perlin.htm" target="_blank" rel="noopener">这一篇文章</a>。</p><h3 id="噪声函数？"><a href="#噪声函数？" class="headerlink" title="噪声函数？"></a>噪声函数？</h3><p>各位想必用过随机数生成器，即 Java 的 Random 类，虽然这个类很好的满足了我们对于不可预测性的需求，但是它的输出过于随机；在这种情况下，Ken Perlin 发明了 Perlin 噪声函数。Perlin 函数看起来是这样的：</p><p><img src="noise1.png" alt></p><p>如果传入更连续的参数，最终的结果会是这样的：</p><p><img src="noise2.png" alt></p><p>噪声函数的形状被我们用于生成地形。</p><h3 id="函数的一些特性"><a href="#函数的一些特性" class="headerlink" title="函数的一些特性"></a>函数的一些特性</h3><p>这是一个普通的正弦波</p><p><img src="sin-wave.png" alt></p><ul><li>amplitude：振幅，为波的高度</li><li>wavelength：波长，为每个峰之间的距离</li><li>frequency：频率，为 1/波长</li></ul><p>这是一个噪声函数</p><p><img src="noise3.png" alt></p><ul><li>每个红点代表函数的随机值</li><li>振幅为函数可能取得的最大值和最小值的差值</li><li>波长为每个红点之间的距离</li><li>频率仍然为 1/波长</li></ul><p>现在，脑补一个随机的噪声，脑补一下增加/减少它的频率，增加/减少它的振幅。</p><p>当振幅减少时，函数将会变「矮」，当频率增加时，函数起伏更加剧烈。</p><p>如果我们把低频高幅的函数和高频低幅的函数混合起来：</p><p><img src="frac1.png" alt></p><p>就会得到和原有单调的噪声函数完全不同的、<strong>更为复杂</strong>的函数图像：</p><p><img src="frac2.png" alt></p><h3 id="数学表达式？"><a href="#数学表达式？" class="headerlink" title="数学表达式？"></a>数学表达式？</h3><p>我们将 噪声函数 定义为 noise(x) ，返回值为 0-1 的小数。</p><p>那么上文的混合函数写出来可能就是这样的：</p><pre><code>f(x) = 1*noise(x) + 0.5*noise(2x) + 0.25*noise(4x) + 0.125*noise(8x) + 0.0625*noise(16x)</code></pre><p>在数学表达式中，我们可以简单的把 2x 4x 8x 里的数字称为频率，将 0.5 0.25 0.125 称为振幅。</p><p>到这里，有心的读者可能会注意到了，这个 f(x) 最后的值不是可以大于 1 了，还能叫噪声函数吗？</p><p>别担心，记住这个问题，继续往下看。</p><h3 id="代码？"><a href="#代码？" class="headerlink" title="代码？"></a>代码？</h3><p>上文的混合函数可以写成这样：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token comment" spellcheck="true">/*** 假设此函数返回 0-1 之间的随机数，并且满足噪声函数的相关定义** @param x 参数* @return 0-1 的随机数*/</span><span class="token keyword">static</span> <span class="token keyword">double</span> <span class="token function">noise</span><span class="token punctuation">(</span><span class="token keyword">double</span> x<span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token keyword">return</span> <span class="token number">0</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token comment" spellcheck="true">/*** 噪声函数** @param x    参数* @param freq 频率* @param amp  振幅* @return 函数值*/</span><span class="token keyword">static</span> <span class="token keyword">double</span> <span class="token function">f</span><span class="token punctuation">(</span><span class="token keyword">double</span> x<span class="token punctuation">,</span> <span class="token keyword">double</span> freq<span class="token punctuation">,</span> <span class="token keyword">double</span> amp<span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token keyword">return</span> amp <span class="token operator">*</span> <span class="token function">noise</span><span class="token punctuation">(</span>x <span class="token operator">*</span> freq<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">static</span> <span class="token keyword">double</span> <span class="token function">f</span><span class="token punctuation">(</span><span class="token keyword">double</span> x<span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token keyword">return</span> <span class="token function">f</span><span class="token punctuation">(</span>x<span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token function">f</span><span class="token punctuation">(</span>x<span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token number">0.5</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token function">f</span><span class="token punctuation">(</span>x<span class="token punctuation">,</span> <span class="token number">4</span><span class="token punctuation">,</span> <span class="token number">0.25</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token function">f</span><span class="token punctuation">(</span>x<span class="token punctuation">,</span> <span class="token number">8</span><span class="token punctuation">,</span> <span class="token number">0.125</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>这项技术被称为<strong>分形</strong>：</p><blockquote><p>维基百科：<br>分形噪声是上述 Perlin 1985年的文章中提出的将符合上文所述三条件的噪声通过计算分形和构造更复杂效果的算法。<br>在一维的情况下，设噪声函数为noise(x)，则通过 noise(2x), noise(4x) 等就可以构造更高频率的噪声。</p></blockquote><p><img src="frac3.png" alt><br><img src="frac4.png" alt></p><p>你可能也注意到了，分形函数中，我们可以将分形次数提取出来，作为单独的一个参数，这个参数我们称之为 octave。那么上文代码里最后一个方法可以写成这样：</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">static</span> <span class="token keyword">double</span> <span class="token function">f</span><span class="token punctuation">(</span><span class="token keyword">double</span> x<span class="token punctuation">,</span> <span class="token keyword">double</span> freq<span class="token punctuation">,</span> <span class="token keyword">double</span> amp<span class="token punctuation">,</span> <span class="token keyword">int</span> octaves<span class="token punctuation">,</span> <span class="token keyword">boolean</span> normalized<span class="token punctuation">)</span> <span class="token punctuation">{</span>    <span class="token keyword">double</span> result <span class="token operator">=</span> <span class="token number">0.0D</span><span class="token punctuation">;</span>    <span class="token keyword">double</span> a <span class="token operator">=</span> <span class="token number">1.0D</span><span class="token punctuation">;</span>    <span class="token keyword">double</span> f <span class="token operator">=</span> <span class="token number">1.0D</span><span class="token punctuation">;</span>    <span class="token keyword">double</span> max <span class="token operator">=</span> <span class="token number">0.0D</span><span class="token punctuation">;</span>    <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">int</span> i <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> i <span class="token operator">&lt;</span> octaves<span class="token punctuation">;</span> <span class="token operator">++</span>i<span class="token punctuation">)</span> <span class="token punctuation">{</span>        result <span class="token operator">+=</span> <span class="token function">f</span><span class="token punctuation">(</span>x<span class="token punctuation">,</span> f<span class="token punctuation">,</span> a<span class="token punctuation">)</span><span class="token punctuation">;</span>        max <span class="token operator">+=</span> amp<span class="token punctuation">;</span>        f <span class="token operator">*=</span> freq<span class="token punctuation">;</span>        a <span class="token operator">*=</span> amp<span class="token punctuation">;</span>    <span class="token punctuation">}</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span>normalized<span class="token punctuation">)</span> <span class="token punctuation">{</span>        result <span class="token operator">/=</span> max<span class="token punctuation">;</span>    <span class="token punctuation">}</span>    <span class="token keyword">return</span> result<span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>前文代码中最后一个 <code>f()</code> 方法就与目前我们的 <code>f(x, 2, 0.5, 4, false)</code> 的作用相同了，仔细想想，是不是这样？</p><p>这 normalized 参数是干啥的？上文中我（也可能是读者你）提出了一个问题，这个参数即是解决这个问题的。仔细想想，是不是这样。</p><h3 id="NoiseGenerator-与-OctaveGenerator"><a href="#NoiseGenerator-与-OctaveGenerator" class="headerlink" title="NoiseGenerator 与 OctaveGenerator"></a>NoiseGenerator 与 OctaveGenerator</h3><p>到了这里，我们只剩一个问题了：最开始的代码里的 noise 方法，如何实现呢？</p><p>org.bukkit.util.noise 包内有 4 个类，用于提供生成噪声函数的方法。</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">static</span> <span class="token keyword">double</span> <span class="token function">bukkitNoise</span><span class="token punctuation">(</span><span class="token keyword">double</span> x<span class="token punctuation">,</span> <span class="token keyword">double</span> freq<span class="token punctuation">,</span> <span class="token keyword">double</span> amp<span class="token punctuation">,</span> <span class="token keyword">int</span> octaves<span class="token punctuation">,</span> <span class="token keyword">boolean</span> normalized<span class="token punctuation">)</span> <span class="token punctuation">{</span>    SimplexNoiseGenerator generator <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">SimplexNoiseGenerator</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">return</span> generator<span class="token punctuation">.</span><span class="token function">noise</span><span class="token punctuation">(</span>x<span class="token punctuation">,</span> octaves<span class="token punctuation">,</span> freq<span class="token punctuation">,</span> amp<span class="token punctuation">,</span> normalized<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">static</span> <span class="token keyword">double</span> <span class="token function">bukkitOctave</span><span class="token punctuation">(</span><span class="token keyword">double</span> x<span class="token punctuation">,</span> <span class="token keyword">double</span> freq<span class="token punctuation">,</span> <span class="token keyword">double</span> amp<span class="token punctuation">,</span> <span class="token keyword">int</span> octaves<span class="token punctuation">,</span> <span class="token keyword">boolean</span> normalized<span class="token punctuation">)</span> <span class="token punctuation">{</span>    SimplexOctaveGenerator generator <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">SimplexOctaveGenerator</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> octaves<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">return</span> generator<span class="token punctuation">.</span><span class="token function">noise</span><span class="token punctuation">(</span>x<span class="token punctuation">,</span> freq<span class="token punctuation">,</span> amp<span class="token punctuation">,</span> normalized<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>如果你认真的看了上面的所有文章，并且认真的思考过后，你应该已经掌握了这几个类的用处了。</p><p>几点需要注意的地方：</p><ul><li>Bukkit 的噪声函数类返回 -1 到 1 的值，与上文的 0 到 1 不同，自行处理即可</li><li>Bukkit 的噪声函数类默认频率很大，所以需要使用小频率（OctaveGenerator 的 setScale(0.005)）</li><li>对于给定的相同种子，调用同一个点的函数总返回相同值</li></ul><p>最后一个问题：为什么会有 PerlinXxxxGenerator 和 SimplexXxxxGenerator 两种呢？</p><p>Perlin 是初代噪声函数，Simplex 基于 Perlin 优化，得到的图像更好看，在高维度的速度也更快。</p><h2 id="噪声函数使用技巧"><a href="#噪声函数使用技巧" class="headerlink" title="噪声函数使用技巧"></a>噪声函数使用技巧</h2><h3 id="Math-pow"><a href="#Math-pow" class="headerlink" title="Math.pow"></a>Math.pow</h3><p>这个函数的图像是这样的：</p><p><img src="pow.png" alt></p><p>你可以用这个函数让山峰更加陡峭，让山谷更加平坦。</p><pre class="line-numbers language-java"><code class="language-java">n1 <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">PerlinNoiseGenerator</span><span class="token punctuation">(</span>random<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">double</span> e <span class="token operator">=</span> n1<span class="token punctuation">.</span><span class="token function">noise</span><span class="token punctuation">(</span>x <span class="token operator">*</span> <span class="token number">0.01F</span><span class="token punctuation">,</span> z <span class="token operator">*</span> <span class="token number">0.01F</span><span class="token punctuation">,</span> <span class="token number">6</span><span class="token punctuation">,</span> <span class="token number">2.0F</span><span class="token punctuation">,</span> <span class="token number">0.5F</span><span class="token punctuation">)</span><span class="token punctuation">;</span>e <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">pow</span><span class="token punctuation">(</span>e<span class="token punctuation">,</span> <span class="token number">2.5</span><span class="token punctuation">)</span><span class="token punctuation">;</span>elevation<span class="token punctuation">[</span>x<span class="token punctuation">]</span><span class="token punctuation">[</span>z<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token keyword">int</span><span class="token punctuation">)</span> <span class="token punctuation">(</span><span class="token number">64</span> <span class="token operator">+</span> e <span class="token operator">*</span> <span class="token number">64F</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p><img src="pow-result.png" alt></p><h3 id="Math-abs"><a href="#Math-abs" class="headerlink" title="Math.abs"></a>Math.abs</h3><p>你可以用此创建锋利的山脊。</p><pre class="line-numbers language-javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">ridgenoise</span><span class="token punctuation">(</span>x<span class="token punctuation">,</span> z<span class="token punctuation">)</span> <span class="token punctuation">{</span>  <span class="token keyword">return</span> <span class="token number">2</span> <span class="token operator">*</span> <span class="token punctuation">(</span><span class="token number">0.5</span> <span class="token operator">-</span> <span class="token function">abs</span><span class="token punctuation">(</span><span class="token number">0.5</span> <span class="token operator">-</span> <span class="token function">noise</span><span class="token punctuation">(</span>x<span class="token punctuation">,</span> z<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>e0 <span class="token operator">=</span>  <span class="token number">1</span> <span class="token operator">*</span> <span class="token function">ridgenoise</span><span class="token punctuation">(</span><span class="token number">1</span> <span class="token operator">*</span> x<span class="token punctuation">,</span> <span class="token number">1</span> <span class="token operator">*</span> z<span class="token punctuation">)</span><span class="token punctuation">;</span>e1 <span class="token operator">=</span>  <span class="token number">0.5</span> <span class="token operator">*</span> <span class="token function">ridgenoise</span><span class="token punctuation">(</span><span class="token number">2</span> <span class="token operator">*</span> x<span class="token punctuation">,</span> <span class="token number">2</span> <span class="token operator">*</span> z<span class="token punctuation">)</span> <span class="token operator">*</span> e0<span class="token punctuation">;</span>e2 <span class="token operator">=</span> <span class="token number">0.25</span> <span class="token operator">*</span> <span class="token function">ridgenoise</span><span class="token punctuation">(</span><span class="token number">4</span> <span class="token operator">*</span> x<span class="token punctuation">,</span> <span class="token number">4</span> <span class="token operator">*</span> z<span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token punctuation">(</span>e0<span class="token operator">+</span>e1<span class="token punctuation">)</span><span class="token punctuation">;</span>e <span class="token operator">=</span> e0 <span class="token operator">+</span> e1 <span class="token operator">+</span> e2<span class="token punctuation">;</span>elevation<span class="token punctuation">[</span>x<span class="token punctuation">]</span><span class="token punctuation">[</span>z<span class="token punctuation">]</span> <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">pow</span><span class="token punctuation">(</span>e<span class="token punctuation">,</span> <span class="token number">2.5</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h2 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h2><p>本文中所有源码位于 <a href="https://github.com/PluginsCDTribe/WorldGenTutor" target="_blank" rel="noopener">https://github.com/PluginsCDTribe/WorldGenTutor</a> 。</p><p>本文部分参考 <a href="https://www.redblobgames.com/maps/terrain-from-noise/" target="_blank" rel="noopener">https://www.redblobgames.com/maps/terrain-from-noise/</a> 。</p><p>作为补充可以阅读 Yaossg 的 1.13 世界生成介绍 <a href="https://yaossg.com/blog/1-13-worldgen/" target="_blank" rel="noopener">https://yaossg.com/blog/1-13-worldgen/</a> 。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;本文基于 Minecraft 1.13，介绍 Bukkit 中的世界生成器，和地形生成的部分原理与运用。&lt;/p&gt;
&lt;h2 id=&quot;加工现有-World-Generator&quot;&gt;&lt;a href=&quot;#加工现有-World-Generator&quot; class=&quot;headerlink&quot;
      
    
    </summary>
    
    
  </entry>
  
</feed>
