返回提交历史
Modified
README.md
+80
-78
Added
README.zh-hans.md
+134
-0
Modified
build.gradle
+1
-0
Modified
src/main/java/com/xfestudio/mydimension/MyDimensionEvents.java
+18
-1
Added
src/main/java/com/xfestudio/mydimension/world/MindServerLevelData.java
+235
-0
Added
src/main/java/com/xfestudio/mydimension/world/MindTimeController.java
+40
-0
Added
src/main/java/com/xfestudio/mydimension/world/MindTimeGameTests.java
+43
-0
Added
src/main/java/com/xfestudio/mydimension/world/MindTimeSavedData.java
+52
-0
Added
src/main/resources/META-INF/accesstransformer.cfg
+2
-0
Added
src/test/java/com/xfestudio/mydimension/world/MindTimeSavedDataTest.java
+73
-0
XFEstudio/MyDimension
心境维度实现独立昼夜时间与持久化支持
为所有心境维度实现了独立的昼夜时间系统,不再与主世界共享天数与昼夜进度。新增 `MindTimeController`、`MindServerLevelData`、`MindTimeSavedData` 三个类,分别负责挂载、代理和持久化心境维度的时间数据。事件监听确保心境维度加载时自动挂载独立时间,每 tick 独立推进 DayTime,并调整睡眠事件以适配新系统。新增单元测试和游戏测试,验证独立时间的正确性。`accesstransformer.cfg` 放开相关字段访问权限,`build.gradle` 注册配置。`README.md` 增加英文版并同步说明,新增简体中文版。
ad31373
代码差异
10 个文件
+678
-79
@@ -1,48 +1,50 @@
1
# 我的维度
1
# MyDimension
2
2
3
适用于 Minecraft 1.20.1 / Forge 的心境维度模组,添加裂罅、五种心境、双向心境传送门,以及可选的机械动力列车兼容。
3
English | [Simplified Chinese](README.zh-hans.md)
4
4
5
## 环境与依赖
5
A Mind-dimension mod for Minecraft 1.20.1 / Forge. It adds the Rift, five Minds, two-way Mind Portals, and optional Create train compatibility.
6
6
7
- Minecraft 1.20.1。
8
- Forge 47.x(项目开发配置为 47.4.0)。
9
- 机械动力(Create)不是必需依赖;未安装时不影响模组的其他功能。
10
- 明确支持 Minecraft 1.20.1 上的机械动力 0.5.1 与 6.x;检测到受支持版本时会自动启用列车传送门兼容。
7
## Requirements and Dependencies
11
8
12
## 主要内容
9
- Minecraft 1.20.1.
10
- Forge 47.x (the development environment uses 47.4.0).
11
- Create is optional; the rest of the mod works normally when it is not installed.
12
- Create 0.5.1 and 6.x for Minecraft 1.20.1 are explicitly supported. Train portal compatibility is enabled automatically when a supported version is detected.
13
13
14
### 裂罅
14
## Main Features
15
15
16
- 手持裂罅时,按住 Shift 并右键打开动作轮盘。
17
- 在轮盘中选择动作后,普通右键执行当前动作。
18
- 可前往私有或共享心境、传送生物、设置心境锚点,以及访问已获授权的其他玩家心境。
19
- 使用同一把裂罅再次离开所选心境时,会返回这把裂罅记录的出发点。
20
- “设置锚点”只能在心境内执行,并且只决定之后使用裂罅传送生物时的落点,不影响玩家或传送门的落点。
21
- 复制共享心境内容的操作仅限创造模式玩家,并会覆盖私有心境中当前坐标附近 `3×3` 区块的方块,操作前请确认目标区域。
16
### Rift
22
17
23
### 心境维度
18
- Hold Shift and right-click while holding a Rift to open the action wheel.
19
- Select an action from the wheel, then right-click normally to perform it.
20
- Travel to private or shared Minds, send mobs, set Mind anchors, or visit another player's Minds after receiving access.
21
- When leaving the selected Mind with the same Rift, you return to the departure point recorded by that Rift.
22
- "Set Anchor" can only be used inside a Mind. It controls the destination used when sending mobs with a Rift, but does not affect player or portal destinations.
23
- Copying content from a shared Mind is limited to players in Creative mode. It overwrites blocks in the `3×3` chunks around the current coordinates in the private Mind, so check the target area first.
24
24
25
| 心境 | 地形概述 |
25
### Mind Dimensions
26
27
| Mind | Terrain |
26
28
| --- | --- |
27
| 空灵心境 | 超平坦维度:60 层石头、3 层泥土、3 层石英块。 |
28
| 镜之心境 | 使用与主世界相同的地形生成配置和世界种子。 |
29
| 水之心境 | 超平坦水域:20 层石头、5 层砂砾、50 层水。 |
30
| 自然心境 | 草原超平坦:50 层石头、9 层泥土、1 层草方块,并启用自然特征。 |
31
| 翱翔心境 | 由多种主世界生态组成的大型悬浮岛屿。 |
29
| Ethereal Mind | Superflat: 60 layers of stone, 3 layers of dirt, and 3 layers of quartz blocks. |
30
| Mirror Mind | Uses the same terrain-generation settings and world seed as the Overworld. |
31
| Water Mind | Superflat ocean: 20 layers of stone, 5 layers of gravel, and 50 layers of water. |
32
| Nature Mind | Superflat grassland: 50 layers of stone, 9 layers of dirt, and 1 layer of grass blocks, with natural features enabled. |
33
| Soaring Mind | Large floating islands composed of several Overworld biomes. |
32
34
33
心境维度内不会自然生成生物。处于心境中的玩家不能被其他玩家通过 `/tp` 或 `/teleport` 强制传送。
35
Mobs do not spawn naturally inside Mind dimensions. Players inside a Mind cannot be forcibly teleported by another player with `/tp` or `/teleport`.
34
36
35
### 心境传送门
37
### Mind Portals
36
38
37
心境传送门可在主世界和目标心境之间双向通行。门面使用紫色、品红色和少量青色粒子构成动态视差效果,进入传送门时的屏幕过渡也会继续流动和闪烁。
39
Mind Portals provide two-way travel between the Overworld and a target Mind. Their surface uses purple, magenta, and subtle cyan particles to create an animated parallax effect that continues flowing and flickering during the portal transition.
38
40
39
1. 在主世界竖直搭建一个 `5×6` 的心境门框外框,内部留出 `3×4` 空间。
40
2. 手持裂罅,通过动作轮盘选择一个“前往”类心境目标,或已获授权的其他玩家心境。传送生物、复制和设置锚点动作不能用于绑定传送门。
41
3. 使用裂罅右键任意一个心境门框方块以激活传送门。
42
4. 模组会在目标心境的安全位置自动生成配对传送门。
43
5. 玩家连续接触门面约 2 秒后完成传送;目标端可以原路返回主世界,传送后约有 4 秒冷却。
41
1. In the Overworld, build a vertical `5×6` outer frame with a `3×4` opening.
42
2. Hold a Rift and use its action wheel to select a Mind travel target or another player's authorized Mind. Send Mob, Copy, and Set Anchor actions cannot be bound to a portal.
43
3. Right-click any Mind Portal Frame block with the Rift to activate the portal.
44
4. The mod automatically creates a paired portal at a safe location in the target Mind.
45
5. Remain in contact with the portal surface for about 2 seconds to travel. The destination portal leads back to the Overworld, and teleportation has a cooldown of about 4 seconds.
44
46
45
门框示意如下,其中 `F` 表示心境门框,`.` 表示内部空间:
47
The frame layout is shown below. `F` is a Mind Portal Frame and `.` is the opening:
46
48
47
49
```text
48
50
F F F F F
@@ -53,80 +55,80 @@ F . . . F
53
55
F F F F F
54
56
```
55
57
56
一座主世界入口共需要 18 个心境门框。传送门的创建者可以重新选择裂罅目标并再次右键门框来更新连接;创造模式玩家不受所有者限制。门面只会自动传送玩家,普通生物需要通过裂罅的“传送生物”动作移动。
58
A complete Overworld entrance requires 18 Mind Portal Frames. The portal's creator can select a new Rift target and right-click the frame again to update the connection; players in Creative mode are not restricted by ownership. The portal surface automatically teleports players only. Use the Rift's "Send Mob" action for other entities.
57
59
58
### 机械动力列车兼容
60
### Create Train Compatibility
59
61
60
安装受支持版本的机械动力后,需要在两个传送门端点都铺设铁轨,并让铁轨沿垂直于门面的方向紧邻门面接入。门面沿 X 轴展开时铁轨沿 Z 轴接入;门面沿 Z 轴展开时铁轨沿 X 轴接入。配对传送门会自动生成,但目标端铁轨不会自动生成。
62
With a supported version of Create installed, place tracks at both portal endpoints so they connect directly to the portal surface along the axis perpendicular to it. If the portal surface extends along the X axis, tracks must enter along the Z axis; if it extends along the Z axis, tracks must enter along the X axis. The paired portal is created automatically, but tracks are not generated at the destination.
61
63
62
完成两端铺轨后,列车即可像穿越下界传送门一样在主世界与心境之间双向通行。
64
After tracks are installed at both ends, trains can travel in either direction between the Overworld and the Mind, much like they travel through Nether portals.
63
65
64
兼容层会在检测到机械动力时自动注册,同时支持 Minecraft 1.20.1 上机械动力 0.5.1 与 6.x 使用的传送门轨道 API。模组没有对机械动力类进行静态引用,因此未安装机械动力时仍可正常启动和游玩。
66
The compatibility layer registers automatically when Create is detected. It supports the portal-track APIs used by both Create 0.5.1 and 6.x on Minecraft 1.20.1. The mod does not statically reference Create classes, so it starts and runs normally without Create installed.
65
67
66
## 合成表
68
## Recipes
67
69
68
### 裂罅
70
### Rift
69
71
70
| 左 | 中 | 右 |
72
| Left | Center | Right |
71
73
| --- | --- | --- |
72
| 玻璃 | 末影珍珠 | 玻璃 |
73
| 末影珍珠 | 时钟 | 末影珍珠 |
74
| 玻璃 | 末影珍珠 | 玻璃 |
74
| Glass | Ender Pearl | Glass |
75
| Ender Pearl | Clock | Ender Pearl |
76
| Glass | Ender Pearl | Glass |
75
77
76
- 材料:玻璃 ×4、末影珍珠 ×4、时钟 ×1。
77
- 产出:裂罅 ×1。
78
- 数据配方:[rift.json](src/main/resources/data/mydimension/recipes/rift.json)。
78
- Ingredients: 4× Glass, 4× Ender Pearl, and 1× Clock.
79
- Output: 1× Rift.
80
- Data recipe: [rift.json](src/main/resources/data/mydimension/recipes/rift.json).
79
81
80
### 心境门框
82
### Mind Portal Frame
81
83
82
| 左 | 中 | 右 |
84
| Left | Center | Right |
83
85
| --- | --- | --- |
84
| 紫水晶碎片 | 下界石英 | 紫水晶碎片 |
85
| 下界石英 | 哭泣的黑曜石 | 下界石英 |
86
| 紫水晶碎片 | 下界石英 | 紫水晶碎片 |
86
| Amethyst Shard | Nether Quartz | Amethyst Shard |
87
| Nether Quartz | Crying Obsidian | Nether Quartz |
88
| Amethyst Shard | Nether Quartz | Amethyst Shard |
87
89
88
- 材料:紫水晶碎片 ×4、下界石英 ×4、哭泣的黑曜石 ×1。
89
- 产出:心境门框 ×6。
90
- 数据配方:[mind_portal_frame.json](src/main/resources/data/mydimension/recipes/mind_portal_frame.json)。
90
- Ingredients: 4× Amethyst Shard, 4× Nether Quartz, and 1× Crying Obsidian.
91
- Output: 6× Mind Portal Frame.
92
- Data recipe: [mind_portal_frame.json](src/main/resources/data/mydimension/recipes/mind_portal_frame.json).
91
93
92
完整搭建一座 `5×6` 传送门外框需要合成 3 次,共消耗紫水晶碎片 ×12、下界石英 ×12、哭泣的黑曜石 ×3,正好得到 18 个心境门框。心境传送门方块本身没有独立合成配方,而是在激活门框时自动生成。
94
Building a complete `5×6` portal frame requires crafting this recipe three times. The total cost is 12× Amethyst Shard, 12× Nether Quartz, and 3× Crying Obsidian, producing exactly 18 Mind Portal Frames. The Mind Portal block itself has no crafting recipe; it is generated automatically when the frame is activated.
93
95
94
## 筑境权杖与共鸣供材锚
96
## Realmwright's Scepter and Resonant Supply Anchor
95
97
96
筑境权杖可在任意维度进行连片铺层、拆层和蓝图施工;主手持有时方块交互距离按服务端配置提升(默认 64 格),但不会增加实体攻击距离。铺层的八邻域遍历只沿准星命中方向上真正可见的连续表面扩展,被地面、背板或其他完整方块遮住的地下部分不会再被预览或施工。`Shift+中键` 打开五页工具菜单,中键切换建造/拆除,`Ctrl+右键` 选择蓝图两端点,`Ctrl+Z`/`Ctrl+Y` 撤销或重做。箱子、按钮、供材锚及其他可交互方块会优先接收普通右键且不显示表面预览;按住 `Shift` 才会显示预览并由右键执行权杖施工。按住 `Ctrl` 时蓝框实时显示候选点;同一次按住期间滚动滚轮后会切换为空气选点,滚轮调整沿视线的距离,松开 `Ctrl` 即重置。部署蓝图时也可用同样方式指定空中锚点。全部快捷操作在菜单中也有入口。
98
The Realmwright's Scepter can place or remove connected surface layers and construct from blueprints in any dimension. While held in the main hand, it extends block interaction range according to the server configuration (64 blocks by default), without increasing entity attack range. Its eight-neighbor surface traversal expands only across genuinely visible, connected surfaces in the direction hit by the crosshair; underground sections hidden by floors, backing blocks, or other full blocks are not previewed or modified. `Shift+middle click` opens the five-page tool menu, middle click switches between build and demolish modes, `Ctrl+right-click` selects the two blueprint corners, and `Ctrl+Z` / `Ctrl+Y` undo or redo. Chests, buttons, Supply Anchors, and other interactive blocks receive normal right-clicks first and do not display the surface preview; hold `Shift` to show the preview and perform a Scepter operation with right-click. While holding `Ctrl`, a blue outline shows the candidate point in real time. Scrolling during the same hold switches to air selection, and the mouse wheel adjusts distance along the view direction; releasing `Ctrl` resets the mode. The same method can place a blueprint anchor in midair. Every shortcut is also available from the menu.
97
99
98
- 建造按“绑定锚点 → 主背包 → 快捷栏 → 副手”取材;材料不足的位置保留黄色虚影,右键聚焦任一虚影继续补齐。准星射线进入黄色方格或蓝色选区/部署框的边线与内部体积都会取得唯一焦点并加粗;相连缺料格按 26 邻接视为一组并整体加粗,焦点粗框宽度以约 160 毫秒平滑过渡到普通框的四倍。部署框优先,其余对象按入射距离选择最近者;左键只取消当前焦点,取消部署不会清除源选区。普通建造预览仅显示绿色框,不渲染具体方块;黄色缺料与蓝图投影使用更实的半透明模型,并通过轻微内缩避免相邻投影面共面闪烁。大型预览按 16³ 分区缓存实体 QUADS 线框和投影模型,六面蓝紫裂罅波纹只用于黄色缺料投影;预览距离至少 160 格并随客户端渲染距离提高到最多 512 格,128 格外自动保留完整框架与缺料波纹 LOD,避免近中距离缺块及大型蓝图瞬时烘焙卡顿。
99
- 手动铺层/拆层在一次交互中立即执行本次设定上限,不再拆成跨 tick 的施工队列。作业页提供每把权杖独立的“历史记录”开关,默认关闭;关闭时不会创建撤销事务、方块实体前后镜像或物资账,从而避开批量施工最重的序列化开销,此时新操作不可撤销/重做。开启后才保留完整事务语义。入口仍会合并拒绝同一时刻的重复快速点击,避免重复射线、BFS 与供材扫描;蓝图自动施工继续使用自身的队列与建造上限。
100
- 拆除只有在副手工具类型和等级均合格时才保留正常掉落;每个成功拆除的方块消耗一次工具耐久,不生成经验。副手为空或工具类型不匹配时直接无粒子删除,不走原版 `2001` 破坏特效,也不创建掉落列表。每次批量操作只播放一个代表方块音效:建造使用该方块的放置音效,拆除使用拆前方块的破坏音效。
101
- 共鸣供材锚潜行贴附在容器面上使用。它优先访问目标面的 Forge 物品 Capability,并兼容 `WorldlyContainer`、普通 `Container` 和可配置的无侧面 Capability 回退;普通非容器表面会拒绝放置且不扣物品,容器消失或第三方旁路产生非法锚时会正常掉落并保留 UUID/ACL。跨区块重验使用目标区块反向索引唤醒,不扫描方块、不强制加载区块也不常驻 tick。锚点默认私有,可设置公开或白名单。主手持权杖时,只为当前这把权杖有序绑定、且位于当前维度的供材锚绘制紫色方框;客户端用权杖绑定 UUID 与服务端轻量索引快照求交,不扫描附近区块。换物品、换权杖、解绑、换维度或退出世界都会立即清理或重算缓存。
102
- 蓝图保存在客户端全局目录 `<游戏目录>/mydimension/blueprints/`,文件扩展名为 `.mindbp`,可跨存档和服务器导入、导出。自动施工每 tick 使用权杖当前“建造上限”作为处理预算;已加载目标跨区块合并成一次供材扫描与历史提交,未加载目标按区块稳定分组并临时租赁,避免以旧的 64 格节流或在区块间逐行往返。服务器会重新验证尺寸、权限、材料、目标方块及完整方块实体 NBT。
103
- 按住 `Alt` 会立即显示裂罅风格的暗紫径向轮盘;十项操作全部使用独立图标(翻转轴、旋转、偏移、重置、复制选区和保存),选中扇区使用青紫辉光,中央只放大当前图标,并在轮盘外显示当前动作提示。松开后按原状态机关闭或确认调整。完整源选区存在时始终保留复制与保存图标;取消移动中的复制预览后,可直接用“复制选区”从保留的蓝色源选区重新生成预览。服务端选区不再按空闲时间过期,只在重新框选、明确取消、换权杖/维度、退出或停服时清理。保存弹窗将方块状态策略独占第一行、保存与取消并排放在第二行;保存后在同一弹窗等待服务端捕获并直接写入本地蓝图库,不再跳转到第二个保存界面。
104
- 服务端配置位于 `[builder]`;`enabled=false` 会停用配方、创造栏入口、增距、施工、绑定、远程容器访问和临时区块加载,但不会删除物品、NBT、历史或客户端蓝图库。
105
- 权杖与供材锚均使用裂罅风格的原生 3D 模型。权杖采用黑灰虚空石、紫蓝裂晶、扭曲杖身和断裂晶簇,不再使用黄铜、齿轮或扳手机械造型;物品栏以约 45° 斜向展示,第一人称模型缩小并右移以保留视野。模式切换只替换悬浮核心的青蓝/紫蓝反向漩涡动画,本体几何和材质不会整体刷新。供材锚的物品视图为 3D 侧视图,世界模型使用非遮挡几何,避免内缩模型裁掉相邻方块表面。
100
- Building draws materials in this order: bound anchors → main inventory → hotbar → offhand. Positions missing materials remain as yellow ghosts; right-click any focused ghost to continue filling them. When the crosshair ray enters a yellow cell or the outline or interior volume of a blue selection/deployment box, it selects one unambiguous focus and thickens its outline. Connected missing-material cells use 26-neighbor connectivity and are emphasized as a group; the focused outline transitions smoothly over about 160 ms to four times its normal width. Deployment boxes take priority, while other objects select the nearest ray entry. Left-click cancels only the current focus, and cancelling a deployment does not clear the source selection. Normal build previews show green outlines without rendering actual blocks; yellow missing-material and blueprint projections use more opaque translucent models with a slight inset to prevent z-fighting between adjacent faces. Large previews cache entity QUADS outlines and projected models in `16³` sections. Six-sided blue-purple Rift ripples are used only for yellow missing-material projections. Preview range is at least 160 blocks and scales with the client's render distance up to 512 blocks; beyond 128 blocks, a level of detail preserves complete frames and missing-material ripples to avoid gaps at medium range and stalls while baking large blueprints.
101
- Manual surface building and demolition complete the configured batch limit in a single interaction instead of being split into a multi-tick queue. The Work page provides a per-Scepter History toggle, disabled by default. When disabled, operations do not create undo transactions, before-and-after block entity snapshots, or material ledgers, avoiding the most expensive batch-operation serialization; these new operations cannot be undone or redone. Full transaction semantics are retained only when History is enabled. The interaction entry point still coalesces and rejects simultaneous rapid duplicate clicks to prevent duplicate raycasts, BFS traversals, and supply scans. Automated blueprint construction continues to use its own queue and build limit.
102
- Demolition preserves normal drops only when the offhand tool has both the correct type and tier. Every successfully demolished block consumes one point of tool durability and produces no experience. If the offhand is empty or the tool type is wrong, the block is deleted directly without particles, the vanilla `2001` block-break effect, or creation of a drop list. Each batch operation plays only one representative block sound: building uses the block's placement sound, while demolition uses the original block's break sound.
103
- Place a Resonant Supply Anchor by sneaking and using it against the face of a container. It first accesses the Forge item capability on the target face, while also supporting `WorldlyContainer`, regular `Container`, and a configurable unsided-capability fallback. Placement against an ordinary non-container surface is rejected without consuming the item. If the container disappears, or a third-party bypass creates an invalid anchor, the anchor drops normally and retains its UUID and ACL. Cross-chunk revalidation wakes through a reverse index for the target chunk; it does not scan blocks, force-load chunks, or remain permanently ticking. Anchors are private by default and can be made public or assigned a whitelist. While a Scepter is held in the main hand, purple boxes are drawn only for Supply Anchors that are bound to that specific Scepter, in binding order, and located in the current dimension. The client intersects the Scepter's bound UUIDs with a lightweight index snapshot from the server instead of scanning nearby chunks. Changing items or Scepters, unbinding, changing dimensions, or leaving the world immediately clears or recalculates the cache.
104
- Blueprints are stored in the global client directory `<game directory>/mydimension/blueprints/` with the `.mindbp` extension, and can be imported or exported across saves and servers. Automated construction uses the Scepter's current Build Batch Limit as its per-tick budget. Loaded targets across chunk boundaries are consolidated into a single supply scan and history commit; unloaded targets are stably grouped by chunk and leased temporarily, avoiding the previous 64-block throttling and row-by-row traversal between chunks. The server revalidates dimensions, permissions, materials, target blocks, and complete block entity NBT.
105
- Holding `Alt` immediately opens a dark-purple, Rift-themed radial wheel. All ten operations have dedicated icons: axis flips, rotation, offsets, reset, copy selection, and save. The selected sector glows cyan-purple, the center displays only an enlarged version of the current icon, and the current action hint appears outside the wheel. Releasing `Alt` closes the wheel or confirms the adjustment according to the current state. The Copy Selection and Save icons remain available whenever a complete source selection exists. After cancelling a moving copy preview, Copy Selection can immediately create another preview from the retained blue source selection. Server-side selections no longer expire through inactivity; they are cleared only when a new selection begins, they are explicitly cancelled, the Scepter or dimension changes, the player leaves, or the server stops. In the save dialog, the block-state policy occupies the first row, with Save and Cancel side by side on the second row. After saving, the same dialog waits for the server capture and writes it directly to the local blueprint library instead of opening a second save screen.
106
- Server configuration is located under `[builder]`. Setting `enabled=false` disables the recipes, Creative inventory entries, extended reach, construction, binding, remote container access, and temporary chunk loading, but does not delete items, NBT, history, or the client-side blueprint library.
107
- Both the Scepter and Supply Anchor use native Rift-themed 3D models. The Scepter is made of black-gray void stone, blue-purple Rift crystals, a twisted shaft, and fractured crystal clusters, replacing brass, gears, and wrench-like mechanical styling. It is displayed at an angle of about 45° in the inventory, while its first-person model is smaller and shifted to the right to preserve visibility. Switching modes replaces only the floating core's counter-rotating cyan-blue or blue-purple vortex animation; it does not refresh the entire model geometry or material. The Supply Anchor uses a 3D side view as its item model and non-occluding geometry in the world, preventing the inset model from clipping the surface of adjacent blocks.
106
108
107
默认合成表:
109
Default recipes:
108
110
109
| 筑境权杖 | 中 | 右 |
111
| Realmwright's Scepter | Center | Right |
110
112
| --- | --- | --- |
111
| 回响碎片 | 下界之星 | 回响碎片 |
112
| 下界合金锭 | 裂罅 | 下界合金锭 |
113
| 哭泣的黑曜石 | 烈焰棒 | 哭泣的黑曜石 |
113
| Echo Shard | Nether Star | Echo Shard |
114
| Netherite Ingot | Rift | Netherite Ingot |
115
| Crying Obsidian | Blaze Rod | Crying Obsidian |
114
116
115
| 共鸣供材锚 | 中 | 右 |
117
| Resonant Supply Anchor | Center | Right |
116
118
| --- | --- | --- |
117
| 回响碎片 | 末影之眼 | 回响碎片 |
118
| 漏斗 | 末影箱 | 漏斗 |
119
| 心境门框 | 磁石 | 心境门框 |
119
| Echo Shard | Eye of Ender | Echo Shard |
120
| Hopper | Ender Chest | Hopper |
121
| Mind Portal Frame | Lodestone | Mind Portal Frame |
120
122
121
模组提供 `mydimension:construction_protected` 与 `mydimension:transaction_unsafe` 两个数据包标签。第三方机器若会修改外部网络、全局 `SavedData`、异步任务或自定义实体,应通过兼容适配器或 `transaction_unsafe` 标签排除;Forge 没有能够通用撤销这类模组外部副作用的接口。
123
The mod provides the `mydimension:construction_protected` and `mydimension:transaction_unsafe` data-pack tags. Third-party machines that modify external networks, global `SavedData`, asynchronous tasks, or custom entities should be excluded through a compatibility adapter or the `transaction_unsafe` tag, because Forge has no general-purpose API for undoing such external mod side effects.
122
124
123
## 构建
125
## Building
124
126
125
127
```powershell
126
128
.\gradlew.bat build
127
129
```
128
130
129
构建完成后的 jar 位于 `build/libs/`:
131
Release artifacts are written to `build/libs/`:
130
132
131
- `mydimension-<版本>.jar`:完整版本,包含私有心境、共享心境、组队入口与复制功能。
132
- `mydimension-<版本>-no-private.jar`:仅保留共享心境,不包含私有心境、组队入口和复制共享心境功能。
133
- `mydimension-<version>.jar`: full build with private Minds, shared Minds, team access, and shared-Mind copying.
134
- `mydimension-<version>-no-private.jar`: shared Minds only, without private Minds, team access, or shared-Mind copying.
@@ -0,0 +1,134 @@
1
# 我的维度
2
3
[English](README.md) | 简体中文
4
5
适用于 Minecraft 1.20.1 / Forge 的心境维度模组,添加裂罅、五种心境、双向心境传送门,以及可选的机械动力列车兼容。
6
7
## 环境与依赖
8
9
- Minecraft 1.20.1。
10
- Forge 47.x(项目开发配置为 47.4.0)。
11
- 机械动力(Create)不是必需依赖;未安装时不影响模组的其他功能。
12
- 明确支持 Minecraft 1.20.1 上的机械动力 0.5.1 与 6.x;检测到受支持版本时会自动启用列车传送门兼容。
13
14
## 主要内容
15
16
### 裂罅
17
18
- 手持裂罅时,按住 Shift 并右键打开动作轮盘。
19
- 在轮盘中选择动作后,普通右键执行当前动作。
20
- 可前往私有或共享心境、传送生物、设置心境锚点,以及访问已获授权的其他玩家心境。
21
- 使用同一把裂罅再次离开所选心境时,会返回这把裂罅记录的出发点。
22
- “设置锚点”只能在心境内执行,并且只决定之后使用裂罅传送生物时的落点,不影响玩家或传送门的落点。
23
- 复制共享心境内容的操作仅限创造模式玩家,并会覆盖私有心境中当前坐标附近 `3×3` 区块的方块,操作前请确认目标区域。
24
25
### 心境维度
26
27
| 心境 | 地形概述 |
28
| --- | --- |
29
| 空灵心境 | 超平坦维度:60 层石头、3 层泥土、3 层石英块。 |
30
| 镜之心境 | 使用与主世界相同的地形生成配置和世界种子。 |
31
| 水之心境 | 超平坦水域:20 层石头、5 层砂砾、50 层水。 |
32
| 自然心境 | 草原超平坦:50 层石头、9 层泥土、1 层草方块,并启用自然特征。 |
33
| 翱翔心境 | 由多种主世界生态组成的大型悬浮岛屿。 |
34
35
心境维度内不会自然生成生物。处于心境中的玩家不能被其他玩家通过 `/tp` 或 `/teleport` 强制传送。
36
37
### 心境传送门
38
39
心境传送门可在主世界和目标心境之间双向通行。门面使用紫色、品红色和少量青色粒子构成动态视差效果,进入传送门时的屏幕过渡也会继续流动和闪烁。
40
41
1. 在主世界竖直搭建一个 `5×6` 的心境门框外框,内部留出 `3×4` 空间。
42
2. 手持裂罅,通过动作轮盘选择一个“前往”类心境目标,或已获授权的其他玩家心境。传送生物、复制和设置锚点动作不能用于绑定传送门。
43
3. 使用裂罅右键任意一个心境门框方块以激活传送门。
44
4. 模组会在目标心境的安全位置自动生成配对传送门。
45
5. 玩家连续接触门面约 2 秒后完成传送;目标端可以原路返回主世界,传送后约有 4 秒冷却。
46
47
门框示意如下,其中 `F` 表示心境门框,`.` 表示内部空间:
48
49
```text
50
F F F F F
51
F . . . F
52
F . . . F
53
F . . . F
54
F . . . F
55
F F F F F
56
```
57
58
一座主世界入口共需要 18 个心境门框。传送门的创建者可以重新选择裂罅目标并再次右键门框来更新连接;创造模式玩家不受所有者限制。门面只会自动传送玩家,普通生物需要通过裂罅的“传送生物”动作移动。
59
60
### 机械动力列车兼容
61
62
安装受支持版本的机械动力后,需要在两个传送门端点都铺设铁轨,并让铁轨沿垂直于门面的方向紧邻门面接入。门面沿 X 轴展开时铁轨沿 Z 轴接入;门面沿 Z 轴展开时铁轨沿 X 轴接入。配对传送门会自动生成,但目标端铁轨不会自动生成。
63
64
完成两端铺轨后,列车即可像穿越下界传送门一样在主世界与心境之间双向通行。
65
66
兼容层会在检测到机械动力时自动注册,同时支持 Minecraft 1.20.1 上机械动力 0.5.1 与 6.x 使用的传送门轨道 API。模组没有对机械动力类进行静态引用,因此未安装机械动力时仍可正常启动和游玩。
67
68
## 合成表
69
70
### 裂罅
71
72
| 左 | 中 | 右 |
73
| --- | --- | --- |
74
| 玻璃 | 末影珍珠 | 玻璃 |
75
| 末影珍珠 | 时钟 | 末影珍珠 |
76
| 玻璃 | 末影珍珠 | 玻璃 |
77
78
- 材料:玻璃 ×4、末影珍珠 ×4、时钟 ×1。
79
- 产出:裂罅 ×1。
80
- 数据配方:[rift.json](src/main/resources/data/mydimension/recipes/rift.json)。
81
82
### 心境门框
83
84
| 左 | 中 | 右 |
85
| --- | --- | --- |
86
| 紫水晶碎片 | 下界石英 | 紫水晶碎片 |
87
| 下界石英 | 哭泣的黑曜石 | 下界石英 |
88
| 紫水晶碎片 | 下界石英 | 紫水晶碎片 |
89
90
- 材料:紫水晶碎片 ×4、下界石英 ×4、哭泣的黑曜石 ×1。
91
- 产出:心境门框 ×6。
92
- 数据配方:[mind_portal_frame.json](src/main/resources/data/mydimension/recipes/mind_portal_frame.json)。
93
94
完整搭建一座 `5×6` 传送门外框需要合成 3 次,共消耗紫水晶碎片 ×12、下界石英 ×12、哭泣的黑曜石 ×3,正好得到 18 个心境门框。心境传送门方块本身没有独立合成配方,而是在激活门框时自动生成。
95
96
## 筑境权杖与共鸣供材锚
97
98
筑境权杖可在任意维度进行连片铺层、拆层和蓝图施工;主手持有时方块交互距离按服务端配置提升(默认 64 格),但不会增加实体攻击距离。铺层的八邻域遍历只沿准星命中方向上真正可见的连续表面扩展,被地面、背板或其他完整方块遮住的地下部分不会再被预览或施工。`Shift+中键` 打开五页工具菜单,中键切换建造/拆除,`Ctrl+右键` 选择蓝图两端点,`Ctrl+Z`/`Ctrl+Y` 撤销或重做。箱子、按钮、供材锚及其他可交互方块会优先接收普通右键且不显示表面预览;按住 `Shift` 才会显示预览并由右键执行权杖施工。按住 `Ctrl` 时蓝框实时显示候选点;同一次按住期间滚动滚轮后会切换为空气选点,滚轮调整沿视线的距离,松开 `Ctrl` 即重置。部署蓝图时也可用同样方式指定空中锚点。全部快捷操作在菜单中也有入口。
99
100
- 建造按“绑定锚点 → 主背包 → 快捷栏 → 副手”取材;材料不足的位置保留黄色虚影,右键聚焦任一虚影继续补齐。准星射线进入黄色方格或蓝色选区/部署框的边线与内部体积都会取得唯一焦点并加粗;相连缺料格按 26 邻接视为一组并整体加粗,焦点粗框宽度以约 160 毫秒平滑过渡到普通框的四倍。部署框优先,其余对象按入射距离选择最近者;左键只取消当前焦点,取消部署不会清除源选区。普通建造预览仅显示绿色框,不渲染具体方块;黄色缺料与蓝图投影使用更实的半透明模型,并通过轻微内缩避免相邻投影面共面闪烁。大型预览按 16³ 分区缓存实体 QUADS 线框和投影模型,六面蓝紫裂罅波纹只用于黄色缺料投影;预览距离至少 160 格并随客户端渲染距离提高到最多 512 格,128 格外自动保留完整框架与缺料波纹 LOD,避免近中距离缺块及大型蓝图瞬时烘焙卡顿。
101
- 手动铺层/拆层在一次交互中立即执行本次设定上限,不再拆成跨 tick 的施工队列。作业页提供每把权杖独立的“历史记录”开关,默认关闭;关闭时不会创建撤销事务、方块实体前后镜像或物资账,从而避开批量施工最重的序列化开销,此时新操作不可撤销/重做。开启后才保留完整事务语义。入口仍会合并拒绝同一时刻的重复快速点击,避免重复射线、BFS 与供材扫描;蓝图自动施工继续使用自身的队列与建造上限。
102
- 拆除只有在副手工具类型和等级均合格时才保留正常掉落;每个成功拆除的方块消耗一次工具耐久,不生成经验。副手为空或工具类型不匹配时直接无粒子删除,不走原版 `2001` 破坏特效,也不创建掉落列表。每次批量操作只播放一个代表方块音效:建造使用该方块的放置音效,拆除使用拆前方块的破坏音效。
103
- 共鸣供材锚潜行贴附在容器面上使用。它优先访问目标面的 Forge 物品 Capability,并兼容 `WorldlyContainer`、普通 `Container` 和可配置的无侧面 Capability 回退;普通非容器表面会拒绝放置且不扣物品,容器消失或第三方旁路产生非法锚时会正常掉落并保留 UUID/ACL。跨区块重验使用目标区块反向索引唤醒,不扫描方块、不强制加载区块也不常驻 tick。锚点默认私有,可设置公开或白名单。主手持权杖时,只为当前这把权杖有序绑定、且位于当前维度的供材锚绘制紫色方框;客户端用权杖绑定 UUID 与服务端轻量索引快照求交,不扫描附近区块。换物品、换权杖、解绑、换维度或退出世界都会立即清理或重算缓存。
104
- 蓝图保存在客户端全局目录 `<游戏目录>/mydimension/blueprints/`,文件扩展名为 `.mindbp`,可跨存档和服务器导入、导出。自动施工每 tick 使用权杖当前“建造上限”作为处理预算;已加载目标跨区块合并成一次供材扫描与历史提交,未加载目标按区块稳定分组并临时租赁,避免以旧的 64 格节流或在区块间逐行往返。服务器会重新验证尺寸、权限、材料、目标方块及完整方块实体 NBT。
105
- 按住 `Alt` 会立即显示裂罅风格的暗紫径向轮盘;十项操作全部使用独立图标(翻转轴、旋转、偏移、重置、复制选区和保存),选中扇区使用青紫辉光,中央只放大当前图标,并在轮盘外显示当前动作提示。松开后按原状态机关闭或确认调整。完整源选区存在时始终保留复制与保存图标;取消移动中的复制预览后,可直接用“复制选区”从保留的蓝色源选区重新生成预览。服务端选区不再按空闲时间过期,只在重新框选、明确取消、换权杖/维度、退出或停服时清理。保存弹窗将方块状态策略独占第一行、保存与取消并排放在第二行;保存后在同一弹窗等待服务端捕获并直接写入本地蓝图库,不再跳转到第二个保存界面。
106
- 服务端配置位于 `[builder]`;`enabled=false` 会停用配方、创造栏入口、增距、施工、绑定、远程容器访问和临时区块加载,但不会删除物品、NBT、历史或客户端蓝图库。
107
- 权杖与供材锚均使用裂罅风格的原生 3D 模型。权杖采用黑灰虚空石、紫蓝裂晶、扭曲杖身和断裂晶簇,不再使用黄铜、齿轮或扳手机械造型;物品栏以约 45° 斜向展示,第一人称模型缩小并右移以保留视野。模式切换只替换悬浮核心的青蓝/紫蓝反向漩涡动画,本体几何和材质不会整体刷新。供材锚的物品视图为 3D 侧视图,世界模型使用非遮挡几何,避免内缩模型裁掉相邻方块表面。
108
109
默认合成表:
110
111
| 筑境权杖 | 中 | 右 |
112
| --- | --- | --- |
113
| 回响碎片 | 下界之星 | 回响碎片 |
114
| 下界合金锭 | 裂罅 | 下界合金锭 |
115
| 哭泣的黑曜石 | 烈焰棒 | 哭泣的黑曜石 |
116
117
| 共鸣供材锚 | 中 | 右 |
118
| --- | --- | --- |
119
| 回响碎片 | 末影之眼 | 回响碎片 |
120
| 漏斗 | 末影箱 | 漏斗 |
121
| 心境门框 | 磁石 | 心境门框 |
122
123
模组提供 `mydimension:construction_protected` 与 `mydimension:transaction_unsafe` 两个数据包标签。第三方机器若会修改外部网络、全局 `SavedData`、异步任务或自定义实体,应通过兼容适配器或 `transaction_unsafe` 标签排除;Forge 没有能够通用撤销这类模组外部副作用的接口。
124
125
## 构建
126
127
```powershell
128
.\gradlew.bat build
129
```
130
131
构建完成后的 jar 位于 `build/libs/`:
132
133
- `mydimension-<版本>.jar`:完整版本,包含私有心境、共享心境、组队入口与复制功能。
134
- `mydimension-<版本>-no-private.jar`:仅保留共享心境,不包含私有心境、组队入口和复制共享心境功能。
@@ -14,6 +14,7 @@ java.toolchain.languageVersion = JavaLanguageVersion.of(17)
14
14
15
15
minecraft {
16
16
mappings channel: 'official', version: project.minecraft_version
17
accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg')
17
18
18
19
runs {
19
20
configureEach {
@@ -7,6 +7,7 @@ import com.xfestudio.mydimension.item.RiftItem;
7
7
import com.xfestudio.mydimension.registry.ModEntities;
8
8
import com.xfestudio.mydimension.registry.ModItems;
9
9
import com.xfestudio.mydimension.network.ModNetwork;
10
import com.xfestudio.mydimension.world.MindTimeController;
10
11
import com.xfestudio.mydimension.world.ModDimensions;
11
12
import com.xfestudio.mydimension.world.entity.RiftAnchorEntity;
12
13
import net.minecraft.commands.CommandSourceStack;
@@ -33,6 +34,7 @@ import net.minecraftforge.event.TickEvent;
33
34
import net.minecraftforge.event.entity.living.MobSpawnEvent;
34
35
import net.minecraftforge.event.entity.player.PlayerEvent;
35
36
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
37
import net.minecraftforge.event.level.LevelEvent;
36
38
import net.minecraftforge.event.level.SleepFinishedTimeEvent;
37
39
import net.minecraftforge.eventbus.api.Event;
38
40
import net.minecraftforge.eventbus.api.EventPriority;
@@ -101,8 +103,23 @@ public class MyDimensionEvents {
101
103
}
102
104
}
103
105
106
@SubscribeEvent(priority = EventPriority.HIGHEST)
107
public void attachIndependentMindTime(LevelEvent.Load event) {
108
if (event.getLevel() instanceof ServerLevel level && ModDimensions.isMindDimension(level.dimension())) {
109
MindTimeController.attach(level);
110
}
111
}
112
104
113
@SubscribeEvent
105
public void finishSleepInEtherealMind(SleepFinishedTimeEvent event) {
114
public void advanceIndependentMindTime(TickEvent.LevelTickEvent event) {
115
if (event.phase == TickEvent.Phase.END && event.level instanceof ServerLevel level
116
&& ModDimensions.isMindDimension(level.dimension())) {
117
MindTimeController.tick(level);
118
}
119
}
120
121
@SubscribeEvent(priority = EventPriority.LOWEST)
122
public void finishSleepInMind(SleepFinishedTimeEvent event) {
106
123
if (!(event.getLevel() instanceof ServerLevel level) || !ModDimensions.isMindDimension(level.dimension())) {
107
124
return;
108
125
}
@@ -0,0 +1,235 @@
1
package com.xfestudio.mydimension.world;
2
3
import net.minecraft.core.BlockPos;
4
import net.minecraft.server.MinecraftServer;
5
import net.minecraft.world.Difficulty;
6
import net.minecraft.world.level.GameRules;
7
import net.minecraft.world.level.GameType;
8
import net.minecraft.world.level.border.WorldBorder;
9
import net.minecraft.world.level.storage.ServerLevelData;
10
import net.minecraft.world.level.timers.TimerQueue;
11
12
import javax.annotation.Nullable;
13
import java.util.UUID;
14
15
/** Delegates normal level metadata while replacing only the shared daylight clock. */
16
final class MindServerLevelData implements ServerLevelData {
17
private final ServerLevelData delegate;
18
private final MindTimeSavedData time;
19
20
MindServerLevelData(ServerLevelData delegate, MindTimeSavedData time) {
21
this.delegate = delegate;
22
this.time = time;
23
}
24
25
@Override
26
public int getXSpawn() {
27
return delegate.getXSpawn();
28
}
29
30
@Override
31
public int getYSpawn() {
32
return delegate.getYSpawn();
33
}
34
35
@Override
36
public int getZSpawn() {
37
return delegate.getZSpawn();
38
}
39
40
@Override
41
public float getSpawnAngle() {
42
return delegate.getSpawnAngle();
43
}
44
45
@Override
46
public long getGameTime() {
47
return delegate.getGameTime();
48
}
49
50
@Override
51
public long getDayTime() {
52
return time.dayTime();
53
}
54
55
@Override
56
public boolean isThundering() {
57
return delegate.isThundering();
58
}
59
60
@Override
61
public boolean isRaining() {
62
return delegate.isRaining();
63
}
64
65
@Override
66
public void setRaining(boolean raining) {
67
delegate.setRaining(raining);
68
}
69
70
@Override
71
public boolean isHardcore() {
72
return delegate.isHardcore();
73
}
74
75
@Override
76
public GameRules getGameRules() {
77
return delegate.getGameRules();
78
}
79
80
@Override
81
public Difficulty getDifficulty() {
82
return delegate.getDifficulty();
83
}
84
85
@Override
86
public boolean isDifficultyLocked() {
87
return delegate.isDifficultyLocked();
88
}
89
90
@Override
91
public void setXSpawn(int x) {
92
delegate.setXSpawn(x);
93
}
94
95
@Override
96
public void setYSpawn(int y) {
97
delegate.setYSpawn(y);
98
}
99
100
@Override
101
public void setZSpawn(int z) {
102
delegate.setZSpawn(z);
103
}
104
105
@Override
106
public void setSpawnAngle(float angle) {
107
delegate.setSpawnAngle(angle);
108
}
109
110
@Override
111
public void setSpawn(BlockPos position, float angle) {
112
delegate.setSpawn(position, angle);
113
}
114
115
@Override
116
public String getLevelName() {
117
return delegate.getLevelName();
118
}
119
120
@Override
121
public void setThundering(boolean thundering) {
122
delegate.setThundering(thundering);
123
}
124
125
@Override
126
public int getRainTime() {
127
return delegate.getRainTime();
128
}
129
130
@Override
131
public void setRainTime(int rainTime) {
132
delegate.setRainTime(rainTime);
133
}
134
135
@Override
136
public void setThunderTime(int thunderTime) {
137
delegate.setThunderTime(thunderTime);
138
}
139
140
@Override
141
public int getThunderTime() {
142
return delegate.getThunderTime();
143
}
144
145
@Override
146
public int getClearWeatherTime() {
147
return delegate.getClearWeatherTime();
148
}
149
150
@Override
151
public void setClearWeatherTime(int clearWeatherTime) {
152
delegate.setClearWeatherTime(clearWeatherTime);
153
}
154
155
@Override
156
public int getWanderingTraderSpawnDelay() {
157
return delegate.getWanderingTraderSpawnDelay();
158
}
159
160
@Override
161
public void setWanderingTraderSpawnDelay(int delay) {
162
delegate.setWanderingTraderSpawnDelay(delay);
163
}
164
165
@Override
166
public int getWanderingTraderSpawnChance() {
167
return delegate.getWanderingTraderSpawnChance();
168
}
169
170
@Override
171
public void setWanderingTraderSpawnChance(int chance) {
172
delegate.setWanderingTraderSpawnChance(chance);
173
}
174
175
@Nullable
176
@Override
177
public UUID getWanderingTraderId() {
178
return delegate.getWanderingTraderId();
179
}
180
181
@Override
182
public void setWanderingTraderId(UUID id) {
183
delegate.setWanderingTraderId(id);
184
}
185
186
@Override
187
public GameType getGameType() {
188
return delegate.getGameType();
189
}
190
191
@Override
192
public void setWorldBorder(WorldBorder.Settings settings) {
193
delegate.setWorldBorder(settings);
194
}
195
196
@Override
197
public WorldBorder.Settings getWorldBorder() {
198
return delegate.getWorldBorder();
199
}
200
201
@Override
202
public boolean isInitialized() {
203
return delegate.isInitialized();
204
}
205
206
@Override
207
public void setInitialized(boolean initialized) {
208
delegate.setInitialized(initialized);
209
}
210
211
@Override
212
public boolean getAllowCommands() {
213
return delegate.getAllowCommands();
214
}
215
216
@Override
217
public void setGameType(GameType gameType) {
218
delegate.setGameType(gameType);
219
}
220
221
@Override
222
public TimerQueue<MinecraftServer> getScheduledEvents() {
223
return delegate.getScheduledEvents();
224
}
225
226
@Override
227
public void setGameTime(long gameTime) {
228
delegate.setGameTime(gameTime);
229
}
230
231
@Override
232
public void setDayTime(long dayTime) {
233
time.setDayTime(dayTime);
234
}
235
}
@@ -0,0 +1,40 @@
1
package com.xfestudio.mydimension.world;
2
3
import net.minecraft.server.level.ServerLevel;
4
import net.minecraft.world.level.GameRules;
5
import net.minecraft.world.level.storage.ServerLevelData;
6
7
/** Installs and advances an independent daylight clock for every concrete mind level. */
8
public final class MindTimeController {
9
private MindTimeController() {
10
}
11
12
public static void attach(ServerLevel level) {
13
if (!ModDimensions.isMindDimension(level.dimension())
14
|| level.serverLevelData instanceof MindServerLevelData) {
15
return;
16
}
17
18
ServerLevelData original = level.serverLevelData;
19
MindTimeSavedData savedTime = MindTimeSavedData.get(level, original.getDayTime());
20
MindServerLevelData independent = new MindServerLevelData(original, savedTime);
21
level.serverLevelData = independent;
22
level.levelData = independent;
23
}
24
25
public static void tick(ServerLevel level) {
26
if (!ModDimensions.isMindDimension(level.dimension())) {
27
return;
28
}
29
30
attach(level);
31
if (level.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) {
32
level.setDayTime(level.getDayTime() + 1L);
33
}
34
}
35
36
static boolean hasIndependentTime(ServerLevel level) {
37
return level.serverLevelData instanceof MindServerLevelData
38
&& level.levelData == level.serverLevelData;
39
}
40
}
@@ -0,0 +1,43 @@
1
package com.xfestudio.mydimension.world;
2
3
import com.xfestudio.mydimension.MyDimension;
4
import net.minecraft.gametest.framework.GameTest;
5
import net.minecraft.gametest.framework.GameTestHelper;
6
import net.minecraft.server.level.ServerLevel;
7
import net.minecraftforge.gametest.GameTestHolder;
8
import net.minecraftforge.gametest.PrefixGameTestTemplate;
9
10
/** Runtime check that the transformed mind level data no longer writes the overworld clock. */
11
@GameTestHolder(MyDimension.MOD_ID)
12
@PrefixGameTestTemplate(false)
13
public final class MindTimeGameTests {
14
private MindTimeGameTests() {
15
}
16
17
@GameTest(template = "empty")
18
public static void mindClockIsIndependentFromOverworld(GameTestHelper helper) {
19
ServerLevel overworld = helper.getLevel().getServer().overworld();
20
ServerLevel mind = helper.getLevel().getServer().getLevel(ModDimensions.ETHEREAL_MIND);
21
helper.assertTrue(mind != null, "Ethereal mind level was not loaded");
22
if (mind == null) {
23
return;
24
}
25
26
MindTimeController.attach(mind);
27
helper.assertTrue(MindTimeController.hasIndependentTime(mind),
28
"Mind level did not receive independent level data");
29
30
long overworldBefore = overworld.getDayTime();
31
long mindBefore = mind.getDayTime();
32
long mindTarget = mindBefore + 7_321L;
33
mind.setDayTime(mindTarget);
34
35
helper.assertTrue(mind.getDayTime() == mindTarget,
36
"Mind level rejected an independent daylight update");
37
helper.assertTrue(overworld.getDayTime() == overworldBefore,
38
"Changing mind daylight also changed the overworld");
39
40
mind.setDayTime(mindBefore);
41
helper.succeed();
42
}
43
}
@@ -0,0 +1,52 @@
1
package com.xfestudio.mydimension.world;
2
3
import com.xfestudio.mydimension.MyDimension;
4
import net.minecraft.nbt.CompoundTag;
5
import net.minecraft.server.level.ServerLevel;
6
import net.minecraft.world.level.saveddata.SavedData;
7
8
/** Persistent daylight clock stored independently in each concrete mind dimension. */
9
final class MindTimeSavedData extends SavedData {
10
static final String DATA_NAME = MyDimension.MOD_ID + "_mind_time";
11
private static final String DAY_TIME_TAG = "DayTime";
12
13
private long dayTime;
14
15
private MindTimeSavedData(long dayTime, boolean dirty) {
16
this.dayTime = dayTime;
17
if (dirty) {
18
setDirty();
19
}
20
}
21
22
static MindTimeSavedData get(ServerLevel level, long initialDayTime) {
23
return level.getDataStorage().computeIfAbsent(
24
MindTimeSavedData::load,
25
() -> new MindTimeSavedData(initialDayTime, true),
26
DATA_NAME
27
);
28
}
29
30
static MindTimeSavedData load(CompoundTag tag) {
31
return new MindTimeSavedData(tag.getLong(DAY_TIME_TAG), false);
32
}
33
34
long dayTime() {
35
return dayTime;
36
}
37
38
void setDayTime(long dayTime) {
39
if (this.dayTime == dayTime) {
40
return;
41
}
42
43
this.dayTime = dayTime;
44
setDirty();
45
}
46
47
@Override
48
public CompoundTag save(CompoundTag tag) {
49
tag.putLong(DAY_TIME_TAG, dayTime);
50
return tag;
51
}
52
}
@@ -0,0 +1,2 @@
1
public-f net.minecraft.world.level.Level f_46442_ # levelData
2
public-f net.minecraft.server.level.ServerLevel f_8549_ # serverLevelData
@@ -0,0 +1,73 @@
1
package com.xfestudio.mydimension.world;
2
3
import net.minecraft.nbt.CompoundTag;
4
import net.minecraft.world.level.storage.ServerLevelData;
5
import org.junit.jupiter.api.Test;
6
7
import java.lang.reflect.Proxy;
8
import java.util.concurrent.atomic.AtomicLong;
9
10
import static org.junit.jupiter.api.Assertions.assertEquals;
11
import static org.junit.jupiter.api.Assertions.assertFalse;
12
import static org.junit.jupiter.api.Assertions.assertTrue;
13
14
class MindTimeSavedDataTest {
15
@Test
16
void roundTripsIndependentDayTime() {
17
MindTimeSavedData data = MindTimeSavedData.load(tagWithDayTime(18_750L));
18
assertEquals(18_750L, data.dayTime());
19
assertFalse(data.isDirty());
20
21
data.setDayTime(24_000L);
22
assertTrue(data.isDirty());
23
24
CompoundTag saved = data.save(new CompoundTag());
25
MindTimeSavedData reloaded = MindTimeSavedData.load(saved);
26
assertEquals(24_000L, reloaded.dayTime());
27
assertFalse(reloaded.isDirty());
28
}
29
30
@Test
31
void separateMindDataObjectsNeverShareTheirClock() {
32
MindTimeSavedData first = MindTimeSavedData.load(tagWithDayTime(1_000L));
33
MindTimeSavedData second = MindTimeSavedData.load(tagWithDayTime(13_000L));
34
35
first.setDayTime(6_000L);
36
37
assertEquals(6_000L, first.dayTime());
38
assertEquals(13_000L, second.dayTime());
39
}
40
41
@Test
42
void levelDataWrapperReadsAndWritesOnlyTheMindClock() {
43
AtomicLong delegatedDayTime = new AtomicLong(Long.MIN_VALUE);
44
ServerLevelData delegate = (ServerLevelData) Proxy.newProxyInstance(
45
ServerLevelData.class.getClassLoader(),
46
new Class<?>[]{ServerLevelData.class},
47
(proxy, method, arguments) -> switch (method.getName()) {
48
case "getDayTime" -> 2_000L;
49
case "setDayTime" -> {
50
delegatedDayTime.set((long) arguments[0]);
51
yield null;
52
}
53
case "getGameTime" -> 99L;
54
default -> throw new AssertionError("Unexpected delegate call: " + method.getName());
55
}
56
);
57
MindTimeSavedData time = MindTimeSavedData.load(tagWithDayTime(14_000L));
58
MindServerLevelData wrapped = new MindServerLevelData(delegate, time);
59
60
assertEquals(14_000L, wrapped.getDayTime());
61
assertEquals(99L, wrapped.getGameTime());
62
wrapped.setDayTime(24_000L);
63
64
assertEquals(24_000L, wrapped.getDayTime());
65
assertEquals(Long.MIN_VALUE, delegatedDayTime.get());
66
}
67
68
private static CompoundTag tagWithDayTime(long dayTime) {
69
CompoundTag tag = new CompoundTag();
70
tag.putLong("DayTime", dayTime);
71
return tag;
72
}
73
}