Dynamic Lights(动态光源)

基本信息

属性 值
目录 com.gtnewhorizons.angelica.dynamiclights + dynamiclights/config/
文件数 9(7 个根目录 + 2 个 config/)
总行数 1299
总开关 AngelicaConfig.enableDynamicLights(默认 true,AngelicaConfig.java:210-212)
入口 EntityLightConfig.init(new File(mc.mcDataDir, "config"))(ClientProxy.java:193)
配置文件 angelica-dynamiclights-entities.txt(EntityLightConfig.java 构造)

它解决什么问题

原版 1.7.10 的光照是烘焙进 chunk 的:火把、灯笼之类的方块光源写死在方块状态里,手持火把 / 掉落物 / 实体发光不会照亮周围。Dynamic Lights 让任意实体或物品光源能在运行期提供光照。

9 个文件全清单(集合运算)

# 文件 行数 角色
1 DynamicLights.java 669 核心单例:光源集合、光照计算、调度
2 ChunkRebuildManager.java 152 待重建区块的排队与节流
3 AdaptiveTickCalculator.java 70 按距离/朝向决定 tick 频率
4 AdaptiveTickMode.java 32 tick 频率枚举(4 项)
5 DynamicLightsMode.java 36 用户可选模式枚举(5 项)
6 IDynamicLightSource.java 91 光源 SPI(mod 实现的接口)
7 IDynamicLightWorldRenderer.java 17 支持 chunk 重建的世界渲染器接口
8 config/EntityLightConfig.java 185 按实体类型禁用光源的配置文件读写
9 config/EntityTypeEntry.java 47 配置文件的一行记录

核心类:DynamicLights(669 行)

5 个静态状态字段(:47-52)

字段 初值 行 语义
Mode DynamicLightsMode.OFF :47 用户选择的更新模式
ShaderForce false :48 着色器强制开启
FrustumCullingEnabled true :49 视锥剔除(省重建)
AdaptiveTickingEnabled true :50 自适应 tick 频率
configEnabled AngelicaConfig.enableDynamicLights :52 static final 配置快照

configEnabled 是 public static final(:52)——在类初始化时对 AngelicaConfig.enableDynamicLights 取一次快照,之后不可变。与其它可运行期读取的开关(FrustumCullingEnabled)性质不同。

光照半径(:54-55)

private static final double MAX_RADIUS = 7.75;
private static final double MAX_RADIUS_SQUARED = MAX_RADIUS * MAX_RADIUS;

衰减公式(:322-323):multiplier = 1.0 - sqrt(distanceSquared) / MAX_RADIUS,即半径边缘处衰减到 0。

:378 的注释给出一个具体推导:「Add MAX_RADIUS to get the search radius (13.86 + 7.75)^2 ≈ 467」——即搜索半径按 (13.86 + 7.75)² ≈ 467 选取。13.86 对应水平方向的最远对角距离。

3 个 maxDynamicLightLevel 重载

重载 行 用途
maxDynamicLightLevel(int x, int y, int z, source, current) :310-334 按整数坐标(偏 0.5,:314-316)—— 面向方块位置
maxDynamicLightLevelExact(double x, double y, double z, source, current) :336-361 按精确 double 坐标(不偏 0.5,:339-341)
maxDynamicLightLevel(BlockPos, source, current) :363 便捷重载

两个整数/浮点版本的唯一差别是 +0.5 偏移 —— 前者把光源位置对齐到方块中心,后者保留亚方块精度。

两个枚举

DynamicLightsMode(5 项,用户可见)

项 显示名 delay(tick) 行
OFF Off 0 :7
FASTEST Fastest 500 :8
FAST Fast 250 :9
FANCY Fancy 50 :10
REALTIME Realtime 0 :11

isEnabled()(:33-35)判据是 !equals(OFF)。注意 delay 语义反直觉:FASTEST 的 delay 是 500 tick(更新最慢),REALTIME 是 0(每 tick)。即 delay 是「等待多少 tick 才更新一次」。

AdaptiveTickMode(4 项,内部)

项 delay 行
REAL_TIME 1 :8
SLOW 5 :10
SLOWER 10 :12
BACKGROUND 20 :14

shouldTickThisFrame(worldTick, sourceHash)(:26-31):(worldTick % delay) == Math.abs(sourceHash % delay) —— 用光源哈希错开同一模式内各光源的更新帧,避免所有光源同帧更新造成尖峰。

AdaptiveTickCalculator 的距离阈值

:11-13 的 static int(非 final,注释标注「configurable via settings」):

字段 初值 约合区块数 行
slowDistance 32 ~2 chunks :11
slowerDistance 64 ~4 chunks :12
backgroundDistance 128 ~8 chunks :13

已知问题:这三个字段是 static int 而非 static final,:19-21 的三个 *Sq 平方值却只在类初始化时算一次。若运行期通过反射改写 slowDistance,*Sq 不会同步更新,calculate() 仍按旧阈值判断。注释说「configurable via settings」但仓库内无任何写这三个字段的代码。

判定顺序(calculate):先算水平点积 dot = dx*lookDirX + dz*lookDirZ,dot < 0(在相机背后)直接返回 BACKGROUND(:37-40);否则按距离降级 BACKGROUND > SLOWER > SLOW > REAL_TIME。

ChunkRebuildManager

成员 行 说明
pendingRebuilds :19 Long2IntOpenHashMap,key = 打包坐标,value = 已等待 tick 数
lock :20 ReentrantReadWriteLock
maxTicksWaiting :23 static int = 100,超过则放弃该区块重建
candidates / toRebuild / toIncrement :26-28 3 个 LongArrayList 复用缓冲

requestRebuild(:34)用 CoordinatePacker.pack(x, y, z) 打包坐标。processVisible(Viewport, renderer)(:45)遍历待办,用 long2IntEntrySet().fastIterator()(:57)避免装箱。

IDynamicLightSource:mod 侧 SPI

91 行,7 个抽象方法 + 4 个 default。方法名统一用 angelica$ 前缀(Angelica 的 mixin 命名约定,避免与 mod 自身方法冲突)。

方法 行 性质
angelica$getDynamicLightX/Y/Z() :13/:20/:27 抽象
angelica$getDynamicLightWorld() :34-36 default → DynamicLights.getActiveRenderer()
angelica$isDynamicLightEnabled() :43-45 default → 查全局集合
angelica$setDynamicLightEnabled(boolean) :54-61 @ApiStatus.Internal,Javadoc 明确警告「不要在你的 mod 里调这个,否则会搞坏东西」
angelica$resetDynamicLight() :63 抽象
angelica$getLuminance() :71 抽象,Javadoc:「最大 15,低于 1 的值被忽略」
angelica$dynamicLightTick() :76 抽象
angelica$updateLights() :77 default no-op
angelica$updateDynamicLight(renderer) :79 抽象
angelica$scheduleTrackedChunksRebuild(renderer) :81 抽象
angelica$getLuminanceRGB() :87-90 default → BlockLightProvider.packRGB(l,l,l)(白光)

跨子系统耦合点:angelica$getLuminanceRGB() 的 default 实现调用 API 层 的 BlockLightProvider.packRGB —— dynamiclights 直接依赖 api 的颜色打包约定。

耦合点汇总

方向 目标 位置
出 → 渲染 CeleritasWorldRenderer 实现 IDynamicLightWorldRenderer CeleritasWorldRenderer.java:158 setActiveRenderer(this) / :163 置 null
出 → 渲染 processChunkRebuilds(viewport) CeleritasWorldRenderer.java:292、:325(含阴影视口)
出 → chunk 构建 WorldSlice / AngelicaChunkBuildContext 缓存 DynamicLights.get() WorldSlice.java:215-216、AngelicaChunkBuildContext.java:108-109
出 → 调试 CeleritasDebugScreenHandler 读 configEnabled :64、:134、:138
出 → 性能 TracyFramePlots 统计 :284-288
出 → API BlockLightProvider.packRGB IDynamicLightSource.java:89
入 → 配置 ClientProxy.init → EntityLightConfig.init ClientProxy.java:193
入 → 配置 ClientProxy 清空光源 ClientProxy.java:262 removeAllLightSources()

实体配置 GUI(client/gui/,3 个文件)

client/gui/ 共 7 个文件,本子系统占用 3 个。其余 4 个未在本文展开。

# 文件 行数 可见性 角色
1 DynamicLightsEntityScreen.java 242 public class extends GuiScreen —
2 DynamicLightsEntityFilter.java 96 public record —
3 DynamicLightsOptionPages.java 32 public class —

DynamicLightsOptionPages:设置页的静态构建(32 行)

整个类只有一个 public static 方法 dynamicLights()(:11),返回 OptionPage(内嵌 Sodium 的类,me.jellysquid.mods.sodium.client.gui.options.OptionPage)。

4 个 OptionGroup(:13-30):

组 选项 行
1 DYNAMIC_LIGHTS、DYNAMIC_LIGHTS_SHADER_FORCE :14-15
2 DYNAMIC_LIGHTS_FRUSTUM_CULLING、DYNAMIC_LIGHTS_ADAPTIVE_TICKING、DYNAMIC_LIGHTS_CULL_TIMEOUT :19-21
3 DYNAMIC_LIGHTS_SLOW_DIST、DYNAMIC_LIGHTS_SLOWER_DIST、DYNAMIC_LIGHTS_BACKGROUND_DIST :24-26
4 SubScreenOption → DynamicLightsEntityScreen::new :28

10 个选项全部来自 jss.notfine.core.Settings(:4 import)—— 即 NotFinite 的 Settings 枚举,不是 AngelicaConfig。⚠️ 动态光相关的 10 个设置项在 NotFinite 枚举里,不在 AngelicaConfig —— 见 NotFinite。

⚠️ ImmutableList.of(...) 硬编码 4 组(:12)—— 增删选项需改此方法。每次调用都新建 OptionPage 与 4 个 OptionGroup(无缓存)—— ⚠️ dynamicLights() 若被多次调用会重复构建(调用点不在本条目范围)。

⚠️ 本地化 key 有 3 个(:12、:28 两次):options.dynamiclights.page、options.dynamiclights.entities、options.dynamiclights.entities.tooltip —— 见 资源与语言键。

DynamicLightsEntityFilter:不可变过滤器 record(96 行)

public record DynamicLightsEntityFilter(List<String> modPrefixes, List<String> nameTerms, State state) {   // :7
    public enum State { ANY, ENABLED, DISABLED }   // :9-13
    private static final Pattern WHITESPACE = Pattern.compile("\\s+");   // :15
    private static final DynamicLightsEntityFilter MATCH_ALL = new DynamicLightsEntityFilter(List.of(), List.of(), State.ANY);   // :16

3 个组件 + 嵌套枚举 State(3 值)。

紧凑构造器做防御性拷贝(:18-21):

public DynamicLightsEntityFilter {
    modPrefixes = List.copyOf(modPrefixes);
    nameTerms = List.copyOf(nameTerms);
}

⚠️ List.copyOf 会拒绝 null 元素(抛 NPE)—— 比 Collections.unmodifiableList 更严格。这是有意的强不可变。

parse(String query) 的三个早退(:23-):

输入 返回 行
null MATCH_ALL :24-26
strip().toLowerCase(Locale.ROOT) 后为空 MATCH_ALL :29-31
正常 解析出的 filter :33-

⚠️ toLowerCase(Locale.ROOT)(:28)—— 显式指定 ROOT 避免土耳其语 I 问题,是正确的。

⚠️ ⚠️ String.strip()(:28)是 Java 11+,String.isBlank() 亦然。本项目的编译 JDK 版本未独立核实(version_gate.py 只确认 minecraftVersion = 1.7.10)。⚠️ WHITESPACE 的 Pattern(:15)在已读的 :15-33 范围内只被声明,未见使用 —— 它应在 :33 之后的 parse 分词逻辑里使用,该部分本条目未读。⚠️ 同时用 strip() 和 WHITESPACE 正则做空白处理—— 可能冗余,也可能用于 strip() 之外的内部拆分。

⚠️ ⚠️ MATCH_ALL 是 static final 单例(:16)—— 但它用的是 List.of()(不可变空列表)且通过了紧凑构造器的 List.copyOf,安全可共享。⚠️ 相比之下 parse(null) 直接返回 MATCH_ALL 而非新建(:25)—— 正确(不可变值可安全共享)。

⚠️ parse 的返回类型是 DynamicLightsEntityFilter(record),语义为「包含规则」还是「排除规则」由 State 三值区分(ANY / ENABLED / DISABLED)。⚠️ 匹配逻辑(modPrefixes 前缀匹配 / nameTerms 词匹配 / State 过滤)的方法在 :33 之后,本条目未读。

DynamicLightsEntityScreen:实体列表 GUI(242 行)

public class DynamicLightsEntityScreen extends GuiScreen {   // :22
    private static final String ENABLED_PREFIX  = EnumChatFormatting.GREEN + "[x] " + EnumChatFormatting.WHITE;   // :24
    private static final String DISABLED_PREFIX = EnumChatFormatting.DARK_GRAY + "[ ] " + EnumChatFormatting.GRAY;  // :25

两个复选框前缀字符串([x] / [ ])—— ENABLED_PREFIX 是绿色,DISABLED_PREFIX 是深灰,与 State 枚举的 3 值对应 Enabled/Disabled 两态显示(ANY 态在 GUI 上如何呈现本条目未读)。

⚠️ ⚠️ 构造器 DynamicLightsEntityScreen(GuiScreen parent)(:42) —— 接受父界面,说明它是从设置页 SubScreenOption push 出来的(对应 DynamicLightsOptionPages.java:28)。⚠️ 与 Iris 的 IrisGuiSlot 有关联:内部类 EntityRowList extends IrisGuiSlot(:195)—— ⚠️ ⚠️ 这是「内嵌 Iris 的 GUI 基类」,但本条目未读其 import,无法确认包名。IrisGuiSlot 的定义不在本条目覆盖范围。

已确认的方法(18 个):

方法 行 类别
DynamicLightsEntityScreen(GuiScreen) :42 构造
initGui() :55 覆写
onGuiClosed() :71 覆写
onClose() :75 私有
refilter() :83 私有
updateToggleButton() :109 私有
toggleVisible() :123 私有
drawScreen(int, int, float) :133 覆写
updateScreen() :148 覆写
mouseClicked(int, int, int) :153 覆写
mouseMovedOrUp(int, int, int) :161 覆写
actionPerformed(GuiButton) :168 覆写
keyTyped(char, int) :175 覆写
EntityRowList extends IrisGuiSlot :195 内部类
drawBackground() :213 覆写,空实现

⚠️ drawBackground() 是空实现(:213,{})—— 刻意不画默认背景(自定义布局)。这是覆写而非遗漏。

⚠️ keyTyped(char, int)(:175)同时处理搜索框输入与翻页 —— ⚠️ 1.7.10 的 keyTyped 签名是 (char, int),现代版是 (char, int, ScanEvent)。这是 1.7.10 特有签名,正确。

⚠️ EntityRowList 是 private 非静态内部类(:195)—— 隐式持有外层 DynamicLightsEntityScreen 实例(drawScreen 需访问 refilter / toggleVisible)。⚠️ 本条目未读 :195-242 的 47 行(EntityRowList 的实体行渲染逻辑)—— 本条目的已知缺口。

⚠️ 全部 UI 文本硬编码英文或走 I18n.format(DynamicLightsOptionPages 走 I18n,DynamicLightsEntityScreen 的 [x]/[ ] 是硬编码)—— 两种风格并存。

已知问题

  1. configEnabled 是 static final 快照(:52):类初始化后不可变。仓库内 AngelicaConfig 由 coremod 阶段装载(AngelicaClientTweaker :71),早于本类初始化,因此实际安全——但这是隐式时序依赖,无断言保障。
  2. AdaptiveTickCalculator 的 *Sq 字段不随 * 字段更新(见上),是真实的静态状态一致性缺陷。
  3. angelica$getLuminance() 下限未强制:Javadoc 说「低于 1 的值被忽略」(:68),但接口无 @Min 约束,mod 实现返回 0 不会被拒绝。
  4. EntityLightConfig 用 EntityRegistry.instance().lookupModSpawn(...)(:108)读取实体所属 mod —— 这是查询不是注册,不要误计为 Entity 注册(见 §④ 裁定)。
  5. ChunkRebuildManager.maxTicksWaiting 是 static int = 100(:23),有 setMaxTicksWaiting(:30)可改,但无上限校验。
  6. AdaptiveTickMode 只有 4 档、DynamicLightsMode 只有 5 档,两套枚举的 delay 语义不同(后者是毫秒级 50/250/500,前者是 tick 级 1/5/10/20),容易混淆。
  7. DynamicLightsEntityFilter 同时用 String.strip()(Java 11+)与 WHITESPACE 正则(:15、:28)做空白处理,可能冗余;本项目的编译 JDK 版本未独立核实。
  8. DynamicLightsEntityScreen.EntityRowList extends IrisGuiSlot(:195)—— 「内嵌 Iris 的 GUI 基类」被 GUI 列表继承,但 IrisGuiSlot 的包名与定义不在本条目覆盖范围(未读)。:195-242 的 47 行本条目未读。
  9. DynamicLightsOptionPages.dynamicLights() 每次调用都重建 OptionPage + 4 个 OptionGroup(:11-31),无缓存。
  10. 10 个动态光设置项在 NotFinite 的 Settings 枚举里,不在 AngelicaConfig(DynamicLightsOptionPages.java:4、:14-26)。
  11. DynamicLightsEntityScreen 的 [x] / [ ] 前缀硬编码(:24-25),而 DynamicLightsOptionPages 走 I18n.format —— 两种本地化风格并存。

相关条目