一款适用于《A Dance of Fire and Ice》的按键显示 Mod:实时按键、KPS 统计、雨滴特效,以及完整的 FreeMake 自定义布局编辑器。 A keyboard overlay mod for A Dance of Fire and Ice: real-time key display, KPS counters, rain effects, and a full FreeMake custom-layout editor.
本仓库是一个 C# Mod 工程,包含:A C# mod project containing:
- 单一变体 Mod(默认资源 DEFLATE 内嵌 DLL,首启自释放),支持 UnityModManager 与 MelonLoader 双加载器 / single variant with DLL-embedded default assets, UnityModManager & MelonLoader
- 固定布局按键显示(8K–24K / 108 键全键盘 / 脚键 2K–16K)/ fixed layouts (8K–24K, 108-key, foot keys 2K–16K)
- FreeMake 自定义布局编辑器(节点式、IMGUI 独立弹窗、内置预设、图层组、排列/分布/阵列)/ FreeMake node editor (IMGUI window, presets, layer groups, align/distribute/array)
- 视频按键/装饰节点(VideoPlayer → RenderTexture,跨重建存活)与
.jkv分享包(配置+图片+视频+自定义字体一键导出导入)/ video key & decoration nodes (VideoPlayer → RenderTexture, survives rebuilds) and.jkvshare packages (profile + images + videos + custom fonts in one archive) - 合并网格渲染的按键框与雨滴系统(对象池、热路径零 GC)/ merged-mesh rendering with pooling, zero hot-path GC
核心信息 / Key facts:
| 项 / Item | 值 / Value |
|---|---|
| 目标游戏 / Target game | A Dance of Fire and Ice (Unity 6000.3.10f1, Mono) |
| 语言 / Language | C# 9 (LangVersion 9, SDK-style csproj) |
| 目标框架 / Target | .NET Framework 4.8.1 |
| 加载器 / Loaders | UnityModManager 0.33+ / MelonLoader |
| 持久化 / Persistence | Newtonsoft.Json 13.0.2 (game-shipped) + JsonUtility (meta only) |
| 界面 / UI | Unity IMGUI (editor/settings) + uGUI/TMP (overlay) |
| 许可证 / License | MIT |
- .NET SDK — 目标框架引用程序集由 csproj 自动还原,无需装 4.8.1 Developer Pack / reference assemblies auto-restored, no Developer Pack needed
libs/目录下的引用 DLL(游戏程序集 + Newtonsoft + 加载器 API)/ reference DLLs underlibs/- Git
git clone https://github.com/adofaiex/JipperKeyViewer.git
cd JipperKeyViewer
# 生成内嵌默认资源(gitignored 构建输入,全新克隆必跑一次) / generate embedded assets (gitignored build input — run once per fresh clone)
./tools/Pack-EmbeddedAssets.ps1
# 构建 / build
dotnet build JipperKeyViewer/JipperKeyViewer.csproj -c Release将仓库根 bin\ 的三个 DLL 连同 Info.json 拷入游戏的 Mods/(见 §9)——无需任何资源文件,首次启动自动释放到 assets/ / Copy the three DLLs from the repo-root bin\ + Info.json into the game's Mods/ folder (see §9) — no asset files needed, they self-extract to assets/ on first launch.
JipperKeyViewer/
├─ JipperKeyViewer/ # 共享核心源码树(由主工程链接编译) / shared core source tree (linked into the mod project)
│ └─ KeyViewer/
│ ├─ Core/ # Runtime core / 运行时核心
│ │ ├─ KeyViewer.cs # Lifecycle, config/profile management, migrations / 生命周期、配置管理、版本迁移
│ │ ├─ KeyViewerInput.cs # Input polling, KPS/Total pipelines (per-group) / 输入轮询、KPS/Total 管线(按组)
│ │ ├─ KeyViewerLayout.cs # Overlay construction, fixed layouts, 108K geometry / 覆盖层构建、固定布局、108K 几何
│ │ ├─ CustomLayout.cs # FreeMake runtime: per-node input/count/rain, groups, caps / FreeMake 运行时
│ │ ├─ KeyViewerPackages.cs # .jkv share packages: export/import, zip-slip guard, aspect rescale / .jkv 分享包
│ │ ├─ KeySource.cs # Unified key source (physical + replay shim) / 统一按键源(物理 + 回放垫片)
│ │ └─ Key.cs # Per-key runtime state / 每键运行时状态
│ ├─ Settings/ # Data model / 数据模型
│ │ ├─ KeyViewerSettings.cs # ProfileData/FmNode/FmLayerGroup + Newtonsoft persistence / 数据模型与持久化
│ │ ├─ KeyviewerStyle.cs # Layout enums / 布局枚举
│ │ └─ FootKeyviewerStyle.cs # Foot-key enums / 脚键枚举
│ ├─ Editor/ # FreeMake editor / FreeMake 编辑器
│ │ ├─ KeyViewerEditor.cs # IMGUI editor window / 编辑器弹窗
│ │ └─ EditorHistory.cs # Timeline undo (snapshot cursor) / 时间线撤销
│ ├─ GUI/ # Settings window partials / 设置窗口各页
│ │ └─ KeyViewerGUI.cs / KeyViewerSettingsGUI / KeyViewerColorGUI / KeyViewerRainGUI / KeyViewerBindingGUI
│ ├─ Rendering/ # Merged meshes / 合并网格渲染
│ │ ├─ KeyShapeLayer.cs # Key-box meshes (bg + outline, rounded/square) / 按键框 mesh
│ │ ├─ RainLayer.cs # Rain meshes (quads + ghost sprite) / 雨滴 mesh
│ │ ├─ KvVideoTextureManager.cs # VideoPlayer → RenderTexture lifecycle (generation-stamped) / 视频纹理生命周期
│ │ └─ KvTextStyle.cs # TMP outline/shadow resolve + material cache / 文字样式解析与材质缓存
│ ├─ Rain/ # Rain simulation / 雨滴模拟
│ │ ├─ RainSystem.cs # Pooled simulation, per-node overrides / 对象池模拟、逐节点覆盖
│ │ └─ RawRain.cs # Per-drop data record & kinematics / 单滴数据与运动学
│ ├─ Loader/ # Loading glue / 加载胶合
│ │ ├─ ModLoader.cs # Loader glue & logging / 加载器胶合与日志
│ │ └─ KeyViewerResources.cs # Unified resource loading + embedded-asset self-extract / 统一资源加载与内嵌资源自释放
│ └─ Util/ # Shared utilities / 共享工具
│ ├─ I18n.cs # EN/ZH/KO strings / 三语词条
│ ├─ KvEasing.cs # 27 named easings for press/counter animations / 27 种命名缓动
│ └─ KvImageLoader.cs # Reflection PNG loader (shared) / 反射 PNG 加载器
├─ JipperKeyViewer/ # 主工程(源码树 + 内嵌默认资源 + Info.json) / the mod project
├─ JipperKeyViewer.Loader.UMM/ # UMM loader entry / UMM 加载入口
├─ JipperKeyViewer.Loader.Melon/ # Melon loader entry / Melon 加载入口
├─ TgtCompat/ # 回放同步垫片工程(编译为 KeyViewer.dll,内嵌进主 DLL) / replay-sync shim (builds as KeyViewer.dll, embedded into the main DLL)
├─ libs/ # Reference DLLs / 引用 DLL
└─ CHANGELOG.md / 更新日志.md
TgtCompat/ 是一个独立的小工程(4 个源文件、约 280 行),编译产物是名为 KeyViewer.dll 的极小程序集——程序集名、UMM 入口(KeyViewer.Main.Load)与公开输入 API(KeyViewer.Core.Input.KeyInput / WinInput / AsyncInputCompat)逐字镜像社区参考版 KeyViewer mod 的表面,但不含其任何功能。/ A tiny separate project (4 source files, ~280 lines) that builds a minimal assembly named KeyViewer.dll — its assembly name, UMM entry and public input API mirror the reference community KeyViewer mod's surface verbatim, without any of its functionality.
为什么需要它 / Why it exists:TogetherBootstrap(TGT)类回放直接驱动游戏判定,回放按键不经过任何输入层(OS 键状态表、Unity Input、游戏异步输入掩码与键事件队列在回放期间全部无动静),任何轮询输入的按键显示器原理上都看不到回放按键。该回放引导器转而在启动时检测名为 "KeyViewer" 的 mod,对其输入总线打 Harmony 补丁以注入回放录制的按键状态。/ TogetherBootstrap (TGT) replays drive the game's judgment directly — replayed presses never pass through any input layer (the OS key table, Unity Input, the game's async-input masks and key event queue all stay silent), so no polling viewer can see them. The bootstrap instead detects a mod named "KeyViewer" at startup and Harmony-patches its input funnel to feed the replay's recorded key state.
怎么用 / How it's used:垫片以资源形式内嵌于 JipperKeyViewer.dll、初始化时按需加载——仅当未安装独立 KeyViewer mod 时(独立版优先,届时内嵌垫片不加载)。补丁挂上后,KeySource(见 Core/KeySource.cs)经反射读取补丁后的输入总线,画面实时还原录制时的手法,计数/KPS/雨滴与物理按压行为完全一致;物理按键仍走 Unity 原生输入,不受影响。不产生额外 mod 列表条目。/ The shim is embedded as a resource inside JipperKeyViewer.dll and loaded on demand at init — only when the standalone KeyViewer mod is absent (the standalone takes precedence and the embedded shim stays unloaded). Once patched, KeySource (see Core/KeySource.cs) reads the patched funnel by reflection: the viewer reproduces the recorded fingering live, with counts/KPS/rain behaving exactly like physical presses; physical keys keep flowing through plain Unity Input. No extra mod-list entry is created.
- 游戏程序集 / Game assemblies:
UnityEngine.*(CoreModule/UIModule/IMGUIModule/InputLegacyModule/VideoModule…),Assembly-CSharp,Unity.TextMeshPro;System.IO.Compression(内嵌资源解压 / 配置包 zip,Unity Mono 自带 / embedded-asset decompression + package zips, shipped by Unity Mono) - 视频节点 / Video nodes:
UnityEngine.VideoModule(VideoPlayer)——同样从游戏 Managed 目录取得,编译期需libs/内该 DLL / same story: taken from the game's Managed dir, needs the DLL underlibs/to compile - 序列化 / Serialization:
Newtonsoft.Json— resolved from the game's Managed dir at runtime;libs/copy for compile time - 加载器 API / Loader APIs:
UnityModManager,MelonLoader,0Harmony(loader entries only — the core project does not depend on Harmony / 仅加载器入口引用,主工程不依赖 Harmony) - 无 NuGet 运行时依赖;
Microsoft.NETFramework.ReferenceAssemblies自动还原 / no NuGet runtime deps; reference assemblies auto-restored
工作分层用于快速定位,不是强制架构边界。 / Working layers for orientation, not enforced boundaries.
- 加载层 / Loading: Loader entries (UMM/Melon) → inject the core DLL →
ModLoaderglue & logging;TgtCompatshim (embedded, loaded only without the standalone KeyViewer mod) - 生命周期层 / Lifecycle:
KeyViewer— Awake/Enable/SceneLoaded/Quit; config load, profile switching, migrations v1→v6;KeyViewerResources— fonts/sprites,UpdateAllFonts, text-style material cache - 设置界面层 / Settings GUI:
GUI/*partials (display/color/rain/binding/profile tabs) +KeyViewerPackages(.jkv export/import with assets & custom fonts) - 编辑器层 / Editor:
KeyViewerEditor(IMGUI window) +EditorHistory— touches only the data model, never rendering directly - 运行时层 / Runtime:
KeySource(unified key source: physical input + replay injection via the shim) →CustomLayout(per-node input/counting) +KeyViewerInput(KPS/Total pipelines) +RainSystem(rain simulation) - 渲染层 / Rendering:
KeyViewerLayout(overlay construction) +KeyShapeLayer/RainLayer(merged meshes) +KvVideoTextureManager(video RenderTextures) +KvTextStyle(outline/shadow resolve) + TMP text sub-canvas - 数据层 / Data:
KeyViewerSettings— ProfileData / FmNode / FmLayerGroup + Newtonsoft Fields contract
flowchart TB
subgraph Loaders["加载层 / Loading"]
UMM["UMM Loader 入口"]
Melon["Melon Loader 入口"]
ModLoader["ModLoader\n胶合/日志/Mod路径"]
TgtShim["TgtCompat 垫片\n(内嵌 KeyViewer.dll)\n回放补丁挂载点"]
end
subgraph Lifecycle["生命周期层 / Lifecycle"]
KV["KeyViewer\nAwake/OnEnable/SceneLoaded\n配置加载/Profile切换/迁移v1→v6\n原子写保存(去抖)"]
Resources["KeyViewerResources\n字体/贴图加载 UpdateAllFonts\n文字样式材质缓存"]
Settings["KeyViewerSettings\nProfileData(全局设置)\nFmNode(节点)/FmLayerGroup(组)\nNewtonsoft Fields 契约\n旧格式一次性导入"]
end
subgraph SettingsGui["设置界面层 / Settings GUI"]
Gui["GUI/* 五个分部\n显示/颜色/雨滴/绑定/配置页"]
Packages["KeyViewerPackages\n.jkv 导出/导入\nzip-slip 防护·宽高比重缩放\n资源+自定义字体打包"]
end
subgraph Editor["编辑器层 (IMGUI 独立弹窗) / Editor"]
KVE["KeyViewerEditor\n画布手势(拖拽/吸附/框选/双击循环)\n排列/分布/阵列\n内置预设(8种,自动建组)\n属性面板(节点级覆盖矩阵)\n多选(活动节点/混合值—)"]
History["EditorHistory\n时间线撤销\n快照游标+0.4s合并窗口"]
end
subgraph Runtime["运行时层 / Runtime"]
KeySource["KeySource\n统一按键源(反射接入)\n物理输入 + 回放注入"]
Custom["CustomLayout\n逐节点输入边沿→计数/配色/雨滴\n图层组可见性门控\n按组KPS队列/总数\n校验钳制与按组上限"]
Input["KeyViewerInput\nKPS/Total管线\n每键KPS/鬼键边沿"]
Rain["RainSystem\n对象池雨滴\n逐节点参数覆盖(宽/高/速/阴影/描边/渐隐)\n排参数回退"]
end
subgraph Render["渲染层 (uGUI + TMP) / Rendering"]
Layout["KeyViewerLayout\n覆盖层构建\n固定布局/108K几何\nKPS/Total文本模式"]
ShapeLayer["KeyShapeLayer\n合并按键框mesh×2\n(背景+描边·圆角/直角)"]
VideoTex["KvVideoTextureManager\nVideoPlayer→RenderTexture\n代次标记·跨重建存活"]
RainLayerM["RainLayer / GhostRainLayer\n合并雨滴mesh×2\n(四边形+鬼雨贴图)"]
TextCanvas["文本子画布 (TMP)\n标签/计数/每键字号"]
TextStyle["KvTextStyle\n描边/阴影解析+缓存键"]
end
UMM --> ModLoader
Melon --> ModLoader
ModLoader --> KV
TgtShim -. "仅未装独立版时加载\n(loaded only w/o standalone)" .-> KeySource
KV --> Settings
KV --> Resources
KV --> Layout
Gui -- "SaveSettingsFromGui\n开关/滑杆→保存+重建" --> KV
Gui --> Packages
Packages -- "读写 Profile/CustomImages/CustomFont" --> Settings
KVE --> Settings
KVE --> History
KVE -- "EditorMutated/PropertyChanged\n(保存+重建)" --> KV
KV --> Custom
KV --> Input
KV --> Rain
KeySource --> Custom
KeySource --> Input
Custom --> Rain
Custom --> Input
Custom -- "视频节点 GetOrCreate" --> VideoTex
Layout --> ShapeLayer
Layout --> RainLayerM
Layout --> TextCanvas
Layout -- "ConfigureText 全局样式" --> TextStyle
Resources -- "字体/样式材质" --> TextCanvas
Resources -- "九宫格贴图 SetSprites" --> ShapeLayer
Rain --> RainLayerM
Settings -.-> Custom
Settings -.-> Layout
flowchart LR
A["物理按键 / 回放注入"] --> B["CustomLayout.ProcessCustomKeysInUpdate\nKeySource.GetKey 轮询(缓存解析的绑定)"]
B --> C{"边沿变化?"}
C -- 按下 --> D["ApplyCustomKeyEdge\n图片换按压贴图/配色切换/按压文案\n计数+1(节点自身)→组队列/全局队列\n弹跳动画启动→雨滴触发"]
C -- 松开 --> E["雨滴回收/淡出\n配色/文案恢复"]
D --> F["RainSystem.UpdateEffects\n运动学:速度=节点覆盖/排参数(÷300)\n轨迹锚定节点顶边(trackBottom)"]
F --> G["RainLayer.OnPopulateMesh\n全部雨滴画进2个mesh"]
D --> H["KeyViewerInput.ProcessKpsInUpdate\n按组KPS=组队列1s窗口计数\n组Total=组内节点计数求和"]
H --> I["面板文本刷新(TMP)"]
D --> J["KeyShapeLayer.SetColors/SetScale\n合并mesh槽位更新"]
编辑器操作 / 设置页改动
→ EditorMutated / SaveSettingsFromGui(去抖合并 / debounced)
→ SyncListsToArrays(列表刷入数组字段 / flush lists to arrays)
→ JsonConvert.SerializeObject(Fields 契约 + UnityStructConverter)
→ WriteAllTextSafe(临时文件 + 原子替换 / temp + atomic replace)
游戏启动 / Game start
→ LoadSettings(meta 用 JsonUtility)
→ LoadProfile(Newtonsoft PopulateObject + 旧格式一次性导入 / legacy import)
→ EnsureCustomNodes(NaN 净化 / 钳制 / 按组上限 / 空组剔除)
| 模块 / Module | 主要职责 / Responsibility | 备注 / Notes |
|---|---|---|
Editor/KeyViewerEditor.cs |
画布手势、预设生成、属性面板、图层组管理 / canvas gestures, presets, property panel, groups | 最大单文件 / largest file |
Core/KeyViewerLayout.cs |
覆盖层构建、108K 槽位表、CreateKey 管线 / overlay build, 108K slot table | 105 项槽位表 |
Core/CustomLayout.cs |
FreeMake 运行时全部逻辑 / full FreeMake runtime | partial |
Settings/KeyViewerSettings.cs |
数据模型 + 序列化 / data model + serialization | FmNode 81 字段 / fields |
Rain/RainSystem.cs |
雨滴池/模拟/参数覆盖 / rain pool, sim, overrides | — |
Editor/EditorHistory.cs |
撤销栈 / undo stack | ~100 行 / lines |
布局页切到自定义 → 打开 FreeMake 编辑器。独立浮动窗,改动即时生效。/ Switch the layout to Custom, click Open FreeMake Editor — an independent floating window; edits apply live.
| 能力 / Feature | 说明 / Details |
|---|---|
| 画布 / Canvas | 拖拽/框选/Ctrl多选/双击循环/八向缩放/小地图/滚轮缩放/右键平移/方向键微调 / drag, marquee, Ctrl multi-select, double-click cycle, 8-way resize, minimap, wheel zoom, RMB pan, arrow nudge |
| 吸附 / Snapping | 节点边/中心 + 屏幕边/中心,屏幕恒定阈值;Alt 临时关闭 / node & screen edges/centers, screen-constant; Alt disables |
| 撤销 / Undo | Ctrl+Z/Y,含属性修改;连续调整按 0.4s 窗口合并为一步 / includes property edits; bursts coalesce |
| 排列条 / Arrange toolbar | 左/中/右、顶/中/底对齐(≥2 选,保留按键间原有间距)+ 横/纵等距分布(≥3 选,两端钉死)+ 阵列复制(数量 × 间距,守按组预算) / L/C/R + T/C/B align (gap-preserving), even distribution, array stamping |
| 内置预设 / Presets | 12K/16K/20K/10K/8K/14K/24K/108K;「保存为新配置」(默认)或追加进当前画布;继承键位/计数/文本;自动建图层组 / save as new profile (default) or add into current canvas; inherits bindings/counts/texts; auto-creates a layer group |
| 图层组 / Layer groups | 整组显隐(隐藏=不渲染不响应不计数);按组 KPS/Total;每组一对面板;按组预算 112 键类 + 8 图片;空组自动清理;组名复用最小空闲编号 / group visibility gates everything; per-group KPS/Total; one panel pair per group; per-group budgets 112+8; empty groups pruned; names reuse free numbers |
| 节点级覆盖 / Per-node overrides | 配色六件套、文本、文字描边/阴影、计数格式(千分位)、盒子形状(圆角/边框厚度)、雨滴形状、雨滴样式、鬼雨独立三组、动画、面板文本布局、杂项 / colors ×6, text, text outline/shadow, count format (thousands sep), box shape (radius/border), rain shape, rain style, ghost rain ×3 sets, animations, panel text layout, misc |
| 多选编辑 / Multi-select | 字段显示活动节点值(青色框标注),改动应用全选;混合值显示 — / shows active node (cyan frame), applies to all; mixed values show — |
| 图片/视频 / Images & video | CustomImages/ 或绝对路径;窗口内导入;按压图切换;视频节点(工具栏按钮或图片节点填视频路径,mp4/mov/webm/avi/wmv/m4v,循环开关,自动起播) / from CustomImages/ or absolute; in-window import; pressed-image swap; video nodes (toolbar button or a video path on an image node, auto-play + loop toggle) |
| 分享包 / Share packages (.jkv) | 设置页「配置」折叠区内导出/导入:配置 + 全部图片视频 + 自定义字体打包为 ZIP,导入永远新建配置并按本机宽高比重缩放 / export/import inside the Profile foldout: profile + all assets + custom font as one ZIP; import always creates a NEW profile and rescales to the local aspect ratio |
UMMMods/JipperKeyViewer/
├── JipperKeyViewer.dll
├── JipperKeyViewer.Loader.UMM.dll # Info.json 引用的入口 / entry referenced by Info.json
└── Info.json
Mods/JipperKeyViewer/
├── JipperKeyViewer.dll
├── JipperKeyViewer.Loader.Melon.dll
└── Info.json
不再随包分发资源文件:默认贴图/字体 DEFLATE 内嵌于主 DLL,首次启动自动释放到 assets/(已存在的文件——包括用户替换过的——永不覆盖)。 / No asset files ship with the package: default sprites/fonts are deflated inside the main DLL and self-extract to assets/ on first launch (existing files — including user replacements — are never overwritten).
首次启动后创建 config/(settings.json + profiles/ 每配置一个 JSON,游戏内切换);CustomFont/ 放字体,CustomImages/ 放 FreeMake 图片/视频,Packages/ 存导出的 .jkv 分享包(导入的图片视频落到 CustomImages/、自定义字体落到 CustomFont/,均不覆盖同名文件)。/ First launch creates config/ (meta + one JSON per profile, switchable in-game); fonts in CustomFont/, FreeMake images/videos in CustomImages/, exported .jkv packages in Packages/ (imported assets land in CustomImages/ and custom fonts in CustomFont/, never overwriting same-named files).
Profile 变成 *.corrupt / 配置丢失:不可解析文件会先备份为 .corrupt 再回退默认,不会覆盖原件;旧过渡格式自动导入。/ Unparseable files are backed up as .corrupt before falling back; legacy interim formats import automatically.
旧版本 Mod 读到 Custom 配置 / Old builds reading a Custom profile:按枚举钳制回落 Key16,属预期。/ Clamped back to Key16 by design.
自定义布局雨滴异常 / Odd rain behavior in custom layouts:逐节点速度与排滑杆同单位;手改配置写入的 0/NaN 加载时自动净化。/ Per-node speeds share the row-slider unit; hand-edited 0/NaN values are sanitized at load.
- MIT License — see LICENSE.