本文是由 DeepSeek 生成的 DDNet 地图编辑器模块分析。仅作为方便看的存档使用。
注意
注意,本文几乎 100% 包含 AI 生成内容!
相关信息
master @ a0dfc77b8src/game/editor/,约 2.6 万行),通过 IEditor 接口注册进 engine;用配置变量 cl_editor(快捷键 Ctrl+Shift+E)在游戏内切换。它不单独起进程,也没有独立渲染器——渲染复用游戏客户端的 CRenderMap。CEditor(应用外壳 / 多标签页) → CEditorMap(一个打开的 .map 文档) → CLayerGroup(图层组,含视差/裁剪) → CLayer 子类(tiles / quads / sounds / game / tele / speedup / switch / tune / front)。std::shared_ptr,由 CEditorMap 的 vector 拥有;元素之间的引用一律是整数索引,因此每次增删都要显式 remap。IEditorAction 对象,压进 CEditorHistory 栈;连续拖拽通过 *PropTracker 的 Begin/End 模式聚合成一条 action。mapitems/map_io.cpp,保存是异步的(CDataFileWriterFinishJob 交给 engine job 队列)。CUi(void *pId 地址即身份 + hot/active 模式);工具栏/快捷键由 X-macro 表 quick_actions.h 声明(但工具栏不会自动遍历它,见 §7.5)。规模速查:editor.cpp 5205 行、popups.cpp 2855 行、editor_server_settings.cpp 2203 行、envelope_editor.cpp 2376 行、editor_actions.cpp 2119 行、map_io.cpp 1289 行、layer_tiles.cpp 1408 行;组件 10 个、具体 action 类 41 个、quick action 55 个、popup 渲染函数 30+ 个。
src/game/editor/)| 文件 | 行数 | 职责 |
|---|---|---|
editor.cpp / editor.h | 5205 / 737 | 应用外壳:生命周期、主 Render()、工具栏、图层/图片/声音列表、quad 编辑与对齐、画笔逻辑、图片/声音增删、RenderEditorHistory |
editor_actions.cpp / .h | 2119 / 698 | 所有 IEditorAction 具体实现(撤销/重做的最小单元),41 个具体类 |
editor_history.cpp / .h | 246 / 67 | CEditorHistory 双栈 + bulk 机制 + 历史面板 UI |
editor_trackers.cpp / .h | 699 / 315 | CPropTracker<T,E> 模板与各具体 tracker:把连续拖拽聚合成一条 action |
editor_action.h | 23 | IEditorAction 接口(Undo/Redo/IsEmpty/DisplayText) |
editor_object.h / .cpp | 58 / 33 | CEditorObject:持有 CEditor*,提供 Editor()/Input()/Graphics()... 访问器(依赖注入点) |
map_object.h / .cpp | 37 / 79 | CMapObject:持有 CEditorMap*,提供 Map()/Editor()/Graphics()...;多文档迁移靠 OnAttach() |
component.h / .cpp | 44 / 48 | CEditorComponent:在 CEditorObject 上加 OnReset/OnMapLoad/OnInput/OnUpdate 钩子 + 子组件注册 |
editor_ui.cpp / .h | 333 / 38 | 编辑器专用控件:DoButton_Editor/DoButton_Ex/UiDoValueSelector/DoEditBox... |
editor_props.cpp | 304 | DoProperties / DoPropertiesWithState<E>:属性面板 → tracker → action 的桥 |
popups.cpp | 2855 | 所有弹出菜单(文件/工具/设置,组/层/quad/点/图片/声音,选择器,tele/speedup/switch/tune,goto/entities/proof/动画/quad art) |
quick_actions.h / .cpp | 486 / 270 | X-macro 快捷键/按钮表与实现(55 项) |
editor_server_settings.cpp / .h | 2203 / 446 | 服务器设置(sv_* / tune 等命令)编辑器,含解析、校验、补全、独立历史 |
envelope_editor.cpp / .h | 2376 / 146 | 曲线(envelope)编辑器:通道、控制点、贝塞尔、切线、独立历史 |
file_browser.cpp / .h | 996 / 215 | 文件浏览器(打开/保存/附加地图、图片、声音) |
map_view.cpp / .h | 883 / 95 | 画布:平移/缩放、坐标换算、画笔抓取/绘制/填充、框选、proof/grid 子组件 |
map_grid.cpp / .h | 187 / 42 | 网格开关与间距 |
proof_mode.cpp / .h | 267 / 55 | proof 模式(可视范围边框) |
layer_selector.cpp / .h | 103 / 30 | Ctrl+右键 按 tile 反查图层 |
quad_knife.cpp / .h | 285 / 26 | 四边形切割工具 |
tile_art.cpp / .h | 215 / 46 | 图片 → tile 图层(量化/抖动) |
quad_art.cpp / .h | 310 / 46 | 图片 → quad 拼图 |
font_typer.cpp / .h | 361 / 58 | 用字体在 canvas 上排出 tile 文本 |
prompt.cpp / .h | 169 / 30 | 命令面板(Ctrl+P),唯一遍历 quick actions 的地方 |
smooth_value.cpp / .h | 98 / 54 | 平滑插值(缩放动画,三次贝塞尔) |
auto_map.cpp / .h | 621 / 94 | automapper 规则引擎 |
explanations.cpp / .h | 758 / 24 | 悬浮解释文本注册表 |
references.h / .cpp | 58 / 42 | envelope 反向引用(undo 时重新绑定 shared_ptr) |
enums.h | 49 | EGameTileOp、automapper 引用名 |
mapitems.h | 128 | 编辑器侧属性枚举(EQuadProp、ELayerProp、ETilesProp…) |
src/game/editor/mapitems/)| 文件 | 行数 | 职责 |
|---|---|---|
map.h / map.cpp | 287 / 1064 | CEditorMap(DOM 根):容器、特殊图层指针、选择状态、索引 remap、组/层增删 |
map_io.cpp | 1289 | Load / Save / Append / PerformSanityChecks / PerformAutosave + 异步写任务 |
layer.h / layer.cpp | 59 / 21 | CLayer 抽象基类 |
layer_group.h / .cpp | 56 / 212 | CLayerGroup:视差、偏移、裁剪、渲染排序 |
layer_tiles.h / .cpp | 219 / 1408 | CLayerTiles:tile 数组、画笔、automapper、tile 变更历史;CLayerGame/Front |
layer_tele/speedup/switch/tune | ~1.7k | DDNet 物理实体层 |
layer_quads.h / .cpp | 40 / 313 | quad(几何贴图)图层 |
layer_sounds.h / .cpp | 35 / 223 | 音源图层 |
envelope.h / .cpp | 53 / 150 | CEnvelope:点、通道、求值入口 |
envelope_evaluator.h / .cpp | 23 / 21 | CMapEnvelopeEvaluator:编辑器动画时间 + IEnvelopeEval 实现 |
image.h / .cpp、sound.h / .cpp | — | CEditorImage / CEditorSound |
| 文件 | 为什么重要 |
|---|---|
src/engine/editor.h | IEditor 接口定义 |
src/engine/client/client.cpp | 编辑器状态机:g_Config.m_ClEditor 切换、m_pEditor->OnUpdate()/OnRender() |
src/game/client/gameclient.cpp | m_pEditor 持有者;HasUnsavedData() 被各种"退出确认"引用 |
src/game/client/components/menus_start.cpp | 主菜单 "Editor" 按钮 / E 快捷键 |
src/game/map/render_map.{h,cpp} | 编辑器与游戏共用的地图渲染(tile/quad/overlay/envelope 求值) |
src/game/map/render_layer.{h,cpp} | 游戏内 layer 渲染基类 |
src/game/mapitems.h | 磁盘格式结构体与常量(CMapItem*、LAYERTYPE_*、TILE_*、ENTITY_*) |
src/game/mapitems_ex_types.h | DDNet 扩展 item 的 UUID 注册 |
src/game/client/tooltips.h / .cpp | 客户端通用 tooltip 系统(CTooltips / CUi::DoToolTip)。编辑器目前没有接入,仍用自己的 m_aTooltip,见 §11 待办 49 |
src/engine/shared/datafile.{h,cpp} | 底层 datafile 容器(item/type/uuid/zlib 压缩) |
src/game/layers.{h,cpp} | 游戏/服务器侧的图层加载(与编辑器 map_io.cpp 平行,改格式要同步) |
data/editor/automap/*.rules | automapper 规则文件(内置规则集) |
构建上,编辑器源文件归 GAME_EDITOR,只被链接进客户端(CMakeLists.txt:2800、2917);专用服务器不含编辑器。
地图格式、CRenderMap 渲染、envelope 求值(CRenderMap::RenderEvalEnvelope,render_map.cpp:249)、CTile/CEntity 常量都与游戏共用;但加载/保存是两份实现:
src/game/layers.cpp + IMap::Loadsrc/game/editor/mapitems/map_io.cpp因此改动地图格式必须同时改两边,这是最容易漏的地方。另外注意:map_io.cpp 里没有独立的 "DDNet / 0.7 文件格式"分支——兼容性完全靠 item version + flag(见 §5)。
CEditorObject 持 CEditor* —— 应用级(多文档之上)组件基类 └── CEditorComponent 生命周期钩子 + 子组件树 CMapObject 持 CEditorMap* —— 文档级对象基类(layer/action/history/tracker)
CMapObject 只存裸 CEditorMap*(map_object.h:34),因此对象不能跨文档直接复用,必须 OnAttach(pMap) 重新绑定(map_object.cpp:19;CLayerGroup::OnAttach 会递归到每个 layer)。Append(把另一张地图合并进来)正是利用这一点搬迁对象。
相关信息
没有任何 action 或 tracker 重写 OnAttach——因为它们是 CEditorMap 的成员,天然绑定同一张地图。
OnInit,全模块拿到接口editor_object.cpp:5-8 的 OnInit(CEditor*) 把 CEditor* 存下来,之后 Editor()/Input()/Client()/Config()/Engine()/Graphics()/Sound()/Storage()/TextRender()/Ui()/RenderMap() 全部转发到 CEditor 的成员(editor_object.cpp:10-33)。没有全局单例,也没有构造函数注入:组件拿不到 Kernel(),只能用这些访问器。
CEditor::Init() 里的注册顺序决定了事件分发顺序(editor.cpp:4612-4621):
cppm_vComponents.emplace_back(m_MapView);
m_vComponents.emplace_back(m_EnvelopeEditor);
m_vComponents.emplace_back(m_MapSettingsBackend);
m_vComponents.emplace_back(m_LayerSelector);
m_vComponents.emplace_back(m_FileBrowser);
m_vComponents.emplace_back(m_Prompt); // Ctrl+P 命令面板
m_vComponents.emplace_back(m_FontTyper);
m_vComponents.emplace_back(m_QuadKnife);
for(CEditorComponent &Component : m_vComponents)
Component.OnInit(this); // 默认实现会调用 OnReset()
CMapView 自己再注册两个子组件(map_view.cpp:27-33):CMapGrid 与 CProofMode,形成两层组件树。全仓库 RegisterSubComponent/InitSubComponents 只有这一处使用。
撤销单元是 IEditorAction(editor_action.h:6),每个 action 只保存回滚所需的最小差异。为何不快照整张地图:
MAX_MAPIMAGES=256 张图、MAX_MAPSOUNDS=64、上百万 tile;一个 1000×1000 的 game layer 单层就 4 MB+,50 步历史会到数百 MB。DisplayText()(历史面板只能显示"恢复地图状态")。具体做法:
EditorTileStateChangeHistory(std::map<int y, std::map<int x, T>>,layer_tiles.h:22),笔画结束时 action 把这份"变更日志"取走并清空(见 §8.1)。vPreviousPoints / vCurrentPoints。Begin 记原值、End 时若真的变了才创建 action。CEditorMap 持有 m_vpGroups / m_vpImages / m_vpEnvelopes / m_vpSounds / m_vSettings(map.h:102-106)。所有跨对象引用(m_Image、m_Sound、m_ColorEnv、quad 的 m_PosEnv…)都是 int,-1 表示"无"。因此:
ModifyImageIndex / ModifyEnvelopeIndex / ModifySoundIndex,map.h:185-187);references.h + VisitEnvelopeReferences)用于 undo 时把引用重新绑到新的 shared_ptr 上。CEditor 持有 std::vector<std::unique_ptr<CEditorMap>> m_vpMaps 与 m_SelectedMap(editor.h:729-730)。所有按 map 的 UI/工具状态都挂到 CEditorMap 上,组件通过 Map()->... 访问,因此切换标签页无需重新初始化组件(map.h:168-178):
cppCMapView::CState m_MapViewState; // 缩放/偏移/当前操作
CMapGrid::CState m_MapGridState;
CProofMode::CState m_ProofModeState;
CQuadKnife::CState m_QuadKnifeState;
CMapEnvelopeEvaluator m_EnvelopeEvaluator;
CEnvelopeEditor::CState m_EnvelopeEditorState;
CMapSettingsBackend::CContextWithInput m_MapSettingsCommandContext;
CFontTyper::CState m_FontTyperState;
CEditorUiElements m_EditorUiElements; // 三个 CScrollRegion 滚动位置
CEditorHistoryUiState m_EditorHistoryUiState;
这是一个重要约定:给组件加"每个地图独立"的状态时,加到这里而不是 CEditor。
注意
不对称点:CEditor::m_RenderLayersState(editor.h:671,图层列表拖拽状态)在 CEditor 上而不是 CEditorMap 上。切标签页时图层列表滚动位置会重置,而拖拽状态却是跨标签共享的。
所有控件调用都传一个稳定的 void *pId(通常是成员地址或字符串字面量地址),CUi 用它维护 hot/active/drag 状态。编辑器在此之上包了一层 editor_ui.cpp。
工具栏/快捷键组合("quick actions")用 X-macro 声明(quick_actions.h),宏共展开三次:
cpp// editor.h:187 声明成员
#define REGISTER_QUICK_ACTION(name, text, callback, disabled, active, button_color, description) \
CQuickAction m_QuickAction##name;
// editor.h:192 构造函数初始化列表
#define REGISTER_QUICK_ACTION(...) m_QuickAction##name(text, description, callback, disabled, active, button_color),
// prompt.cpp:67 收集进命令面板
#define REGISTER_QUICK_ACTION(...) m_vQuickActions.emplace_back(&Editor()->m_QuickAction##name);
注意
常见误解:工具栏并不会自动遍历 quick actions。按钮是逐个点名引用 m_QuickActionX 的(editor.cpp:534 Proof、editor.cpp:562 ToggleGrid、editor.cpp:714 Refocus…;popups.cpp:29-111 文件菜单)。唯一的自动遍历在 CPrompt(Ctrl+P 命令面板):模糊匹配 label、跳过 Disabled()、用 ActionButtonId() 作 listbox id(prompt.cpp:107-160)。
所以新增 quick action 只"自动"获得一个成员、一次构造初始化和一条命令面板条目;要出现在按钮上仍需手动引用。
PerformPreSaveSanityChecks;读文件后 PerformSanityChecks + CheckIntegrity。map_io.cpp:206,290,323)。map_io.cpp:303-309,335-341)。m_Data 会冗余写一份全零 CTile 数组(map_io.cpp:240-259),供只看 m_Data 的旧实现使用。LAYERTYPE_SOUNDS_DEPRECATED 转换;老地图"名字全 -1"有 m_aName[0] != -1 的 hack(map_io.cpp:1038)。src/engine/editor.h:27 extern IEditor *CreateEditor(); src/engine/client/client.cpp:5153 pKernel->RegisterInterface(CreateEditor(), false); src/game/editor/editor.cpp:5205 IEditor *CreateEditor() { return new CEditor; } src/engine/client/client.cpp:3149 m_pEditor = Kernel()->RequestInterface<IEditor>(); src/engine/client/client.cpp:3273 m_pEditor->Init(); src/game/client/gameclient.cpp:111 m_pEditor = Kernel()->RequestInterface<IEditor>(); // 同一实例
CEditor 的接口指针在 CEditor::Init()(editor.cpp:4589)里用 Kernel()->RequestInterface<>() 一次性取齐(IInput/IClient/IConfigManager/IEngine/IGraphics/ITextRender/IStorage/ISound),随后初始化 CUi、CRenderMap,Reset() + AddDefaultMap(),注册组件,加载光标/棋盘贴图,并建立"工具地图" m_pToolsMap(用于调色板与画笔 m_pTilesetPicker / m_pQuadsetPicker / m_pBrush,editor.cpp:4628-4639)。
相关信息
编辑器在 CClient::Run() 里无条件初始化(client.cpp:3273),紧接着 client.cpp:3304 又 g_Config.m_ClEditor = 0; // never start with the editor。也就是说即使从不打开编辑器,纹理/组件/默认地图都已创建。
g_Config.m_ClEditor(config_variables.h:343),加上 CClient::m_EditorActive(client.h:112)做帧分发。E 键:menus_start.cpp:138-141 设 g_Config.m_ClEditor = 1。Ctrl+Shift+E(client.cpp:3397-3402)切换;关闭时调用 m_pEditor->OnClose()。client.cpp:3407-3416):首次进入时 Input()->MouseModeRelative() → GameClient()->OnActivateEditor()(实现只有 OnRelease(),即释放游戏组件)→ m_pEditor->OnActivate() → m_EditorActive = true。-edit <map> / 拖入 .map:client.cpp:3354-3361 调 m_pEditor->HandleMapDrop(...),成功则置 m_ClEditor = true。注意
最容易误解的一环:编辑器没有"请求关闭"的接口。它通过直接写 g_Config.m_ClEditor = 0 来请求退出(ESC editor.cpp:4055、菜单关闭按钮 editor.cpp:3712、Exit() editor.cpp:3728),由 CClient::Run 下一帧轮询配置变量来完成实际切换。IEditor::OnClose 只做清理(暂停音频预览、通知文件浏览器)。
CClient::Update() (client.cpp:2826) └─ if (m_EditorActive) m_pEditor->OnUpdate() (client.cpp:2832) ├─ CUIElementBase::Init(Ui()) // 编辑器与游戏各有一套 CUi ├─ m_pContainerPannedLast = m_pContainerPanned ├─ 鼠标相对移动 + MouseAxisLock() + Ui()->OnCursorMove() (editor.cpp:4843-4850) ├─ Input()->ConsumeEvents(OnInput) (editor.cpp:4853-4855) ├─ MapView()->UpdateMouseWorld() (editor.cpp:4857) ├─ LayerSelector()->UpdateHoveredTiles() (editor.cpp:4858) ├─ HandleAutosave() / HandleWriterFinishJobs()(editor.cpp:4859-4860) └─ for (Component : m_vComponents) Component.OnUpdate() (editor.cpp:4862-4863) CClient::Render() (client.cpp:1214) └─ if (m_EditorActive) m_pEditor->OnRender() (client.cpp:1218) ├─ 切换 GUI 显隐(Tab)、F10 截图 (editor.cpp:4891-4902) ├─ EnvelopeEvaluator 动画时间推进 (editor.cpp:4904-4905) ├─ Ui()->StartCheck(); Ui()->Update(); DoBackButton(); (editor.cpp:4908-4912) ├─ Render() (editor.cpp:4914, 定义在 3731) ├─ MapView()->ResetMouseDeltaWorld() (editor.cpp:4916) ├─ Ui()->FinishCheck(); ClearHotkeys(); Input()->Clear(); └─ #if CONF_DEBUG Map()->CheckIntegrity(); (editor.cpp:4933-4935)
CEditor::OnInput 是组件优先的(editor.cpp:4866-4885):
cppfor(CEditorComponent &Component : m_vComponents)
{
// FLAG_RELEASE 必须广播给所有组件(否则禁用组件会吞掉 key-up)
if(Component.OnInput(Event) && (Event.m_Flags & ~IInput::FLAG_RELEASE) != 0)
return;
}
Ui()->OnInput(Event);
CEditorComponent::OnInput 默认会把事件转发给自己的 m_vSubComponents(component.cpp:17-32),所以 CMapView 的子组件 CMapGrid/CProofMode 也能消费输入。
生命周期钩子一览:
| 钩子 | 触发点 |
|---|---|
OnInit | CEditor::Init()(editor.cpp:4620-4621);默认实现转调 OnReset()(component.cpp:3-7) |
OnReset | CEditor::Reset()(editor.cpp:4482-4483),在切/关地图、新建地图时调用 |
OnMapLoad | 仅在 CEditor::Load() 成功加载新地图后(editor.cpp:5021-5022) |
OnInput | 每帧输入消费(editor.cpp:4877;默认转发给子组件) |
OnUpdate | 每帧更新(editor.cpp:4862;默认实现为空,不转发子组件) |
实际覆盖情况很稀疏:
OnMapLoad:CMapView(手动转发给 m_ProofMode)、CProofMode、CMapSettingsBackend(重新校验 m_vSettings)。OnReset:只有 CEnvelopeEditor。OnInput:只有 CFontTyper 和 CPrompt。OnUpdate:没有任何组件覆盖;MapView()->UpdateMouseWorld() 等由 CEditor::OnUpdate 显式调用。注意
切换标签页不会触发 OnMapLoad——这正是为什么工具状态必须存在 CEditorMap 上(§2.6)。
组件分发不对称:OnInput 会转发给子组件,OnUpdate/OnMapLoad/OnReset 不会(CMapView 手动补了 OnMapLoad)。新增子组件极易踩坑。
CEditor::Save/Load/HandleMapDrop(editor.cpp:4984/4993/4987)只是包装 Map()->... 并注入统一的 ErrorHandler(弹窗 + log_error("editor/save"|"editor/load"))。Load 会先 Reset(),新建一个 CEditorMap,成功后才 push 进 m_vpMaps 并把它设为当前,然后广播 OnMapLoad()(editor.cpp:5004-5027)。
注意
Reset() 在 Load 之前无条件执行:加载失败也会重置当前 UI 状态(不丢已打开的地图)。
CEditorMap::Save 把数据写进临时文件并构造 CDataFileWriterFinishJob 交给 Engine()->AddJob(map_io.cpp:431-436);CEditor::HandleWriterFinishJobs(editor.cpp:4735)轮询完成后 rename 并可选 rcon hot_reload。UI 通过 IsSaving(filename) 禁用重入(editor.cpp:4779)。
注意
HandleWriterFinishJobs 只看队首、每帧最多处理一个;某个 job 卡住会阻塞后面所有地图的"Saving"状态清除。
Save 返回 true 只代表排队成功,不代表写盘完成。HandleAutosave(editor.cpp:4687)按 ed_autosave_interval 分钟遍历所有地图检查 m_ModifiedAuto,写入 maps/auto/<autosaveName>_<timestamp>.map,并按 ed_autosave_max 用 CFileCollection 清理(map_io.cpp:1263)。CloseMap(editor.cpp:4506)——保存中拒绝关闭;有改动时弹 POPEVENT_CLOSE_MAP 确认;关掉最后一个地图会自动 AddDefaultMap()。UpdateMapDisplayNames(editor.cpp:4786)给同名地图追加 (n) / 自动保存名追加 _0n;无名地图为 Unnamed / unnamed。DoMapTabs(editor.cpp:343)用 CScrollRegion 横向滚动,每张地图用自带的稳定 id(map.h:99-100);保存中显示 spinner,未修改时显示圆点而非 ×。CMapObject (map_object.h:11,持 CEditorMap*) ├── CLayerGroup (mapitems/layer_group.h:11) m_vpLayers: vector<shared_ptr<CLayer>> ├── CLayer (抽象) (mapitems/layer.h:15) │ ├── CLayerTiles (mapitems/layer_tiles.h:43) │ │ ├── CLayerGame (:6) m_HasGame — 物理/游戏层 │ │ ├── CLayerFront (:6) m_HasFront — 前景装饰 │ │ ├── CLayerTele (:17) m_HasTele + m_pTeleTile │ │ ├── CLayerSpeedup (:19) m_HasSpeedup + m_pSpeedupTile │ │ ├── CLayerSwitch (:19) m_HasSwitch + m_pSwitchTile │ │ └── CLayerTune (:17) m_HasTune + m_pTuneTile │ ├── CLayerQuads (mapitems/layer_quads.h:6) m_vQuads │ └── CLayerSounds (mapitems/layer_sounds.h:6) m_vSources ├── CEditorImage : CImageInfo (mapitems/image.h:11) ├── CEditorSound (mapitems/sound.h:8) └── CMapEnvelopeEvaluator : IEnvelopeEval (mapitems/envelope_evaluator.h:9) CEnvelope (mapitems/envelope.h:10) — 纯数据,无 Map 指针 CEditorMap (mapitems/map.h:63) — DOM 根,无 Map 指针 IEditorEnvelopeReference (references.h:12) — envelope 反向引用抽象
CLayer 的虚接口(layer.h:21-50)定义了"一种图层能做什么":
cppvirtual void BrushSelecting(CUIRect Rect);
virtual int BrushGrab(CLayerGroup *pBrush, CUIRect Rect);
virtual void FillSelection(bool Empty, CLayer *pBrush, CUIRect Rect);
virtual void BrushDraw(CLayer *pBrush, vec2 WorldPos);
virtual void BrushPlace(CLayer *pBrush, vec2 WorldPos);
virtual void BrushFlipX/Y();
virtual void BrushRotate(float Amount);
virtual bool IsEntitiesLayer() const;
virtual void Render(const CEditorMap *pRenderMap);
virtual CUi::EPopupMenuFunctionResult RenderProperties(CUIRect *pToolbox);
virtual bool IsEnvelopeUsed/IsImageUsed/IsSoundUsed(int) const;
virtual void ModifyImageIndex/ModifyEnvelopeIndex/ModifySoundIndex(const FIndexModifyFunction &);
virtual std::shared_ptr<CLayer> Duplicate() const = 0;
virtual const char *TypeName() const = 0;
virtual void GetSize(float *pWidth, float *pHeight);
shared_ptr 存在 CEditorMap 的 vector 里(map.h:102-106);顺序 = 渲染顺序 = 文件顺序。map.h:108-114):m_pGameGroup / m_pGameLayer / m_pTeleLayer / m_pSpeedupLayer / m_pFrontLayer / m_pSwitchLayer / m_pTuneLayer。
MakeGameGroup 设 m_GameGroup=true 并强制命名 "Game"(map.cpp:320)。MakeGameLayer/Tele/... 一律 std::static_pointer_cast(map.cpp:431-459),无运行时校验。CLayer(const CLayer&) 会重置 m_Readonly=false; m_Visible=true(layer.cpp:13-21),所以 Duplicate() 出来的层总是可编辑可见。~CLayerTiles delete[] m_pTiles;~CEditorImage 释放纹理;~CEditorSound UnloadSample + free(m_pData)。CheckIntegrity,map.cpp:152-253)必须恰好存在一个 m_GameGroup(m_GameGroup == true)和一个 m_pGameLayer(m_HasGame == true),且后者在前者内部;m_pFrontLayer 等若存在也必须在 game group 内。
其它不变量:
m_StartLayer/m_NumLayers 由 writer 生成。-1 表示"无引用";加载后必须落在 vector 范围内。CFixedTime 内部单位是毫秒。m_aName 编辑器侧 12 字节(layer.h:52)↔ 磁盘 int[3],组名/层名/envelope 名都用 StrToInts/IntsToStr。注意
CheckIntegrity 只在 debug 构建里每帧调用(editor.cpp:4933-4935);release 下损坏的地图会带病运行。
磁盘结构(src/game/mapitems.h) | 编辑器结构 | 备注 |
|---|---|---|
CMapItemVersion (=1) | — | 恒写 1(map_io.cpp:86-90) |
CMapItemInfoSettings | CMapInfo + m_vSettings | settings 是 \0 分隔的命令串 |
CMapItemImage_v2 | CEditorImage | 带 m_aTileFlags[256];m_MustBe1 != 1 则拒绝 |
CMapItemSound | CEditorSound | m_SoundDataSize 已废弃 |
CMapItemGroup (v1–v3) | CLayerGroup | v3 才有 m_aName |
CMapItemLayerTilemap (v2 / v2Legacy / v3) | CLayerTiles 及其子类 | 由 m_Flags & TILESLAYERFLAG_* 决定具体子类;4 个 static_assert 锁死布局 |
CMapItemLayerQuads | CLayerQuads | quad 数据用 AddDataSwapped(LE 32bit) |
CMapItemLayerSounds | CLayerSounds | LAYERTYPE_SOUNDS_DEPRECATED=9 走老结构转换 |
CMapItemEnvelope + MAPITEMTYPE_ENVPOINTS(+_BEZIER) | CEnvelope | 所有 envelope 的点拉平进一个全局数组 |
CMapItemAutomapperConfig | CLayerTiles::m_AutomapperConfig/m_Seed/m_AutoAutomapper | 只对非物理 tile 层保存 |
索引语义:layer 在整张地图内连续编号,每个 group 用 m_StartLayer/m_NumLayers 指向这段全局数组;写入时先写该组的所有 layer item、再写 group item(所以 group 知道 layer 数量,map_io.cpp:354)。image/sound/envelope 的索引就是各自 vector 下标。envelope 点用 m_StartPoint/m_NumPoints 切分全局点数组。
关于 "DDNet"/"0.7" 变体:map_io.cpp 没有独立的 0.7 文件格式分支。兼容性完全靠 item version + flag:Teeworlds v2 tilemap(60B)、DDRace 追加字段的 v2Legacy(80B)、Teeworlds v3(72B)、DDNet 最终 v3(92B)通过同一 CMapItemLayerTilemap 前缀读取;老 sound layer 走 LAYERTYPE_SOUNDS_DEPRECATED。编辑器里出现的 "DDNet"/"Vanilla"/"FNG"/"Race"/"blockworlds" 只是实体贴图集选择 CEditor::m_SelectEntitiesImage(editor.h:213,374),影响 IsAllowPlaceUnusedTiles 之类的放置校验,不是文件格式变体。
src/game/mapitems.h:LAYERTYPE_*、TILESLAYERFLAG_*、可能的 TILE_*/ENTITY_*、新 CMapItemLayer* 结构(注意 4 字节对齐/static_assert)。src/game/editor/mapitems/layer_xxx.{h,cpp}:新 CLayerXxx,实现 TypeName/Duplicate/Render/RenderProperties/Modify*Index/IsEmpty 等。mapitems/map.h/.cpp:新增特殊指针字段 + MakeXxxLayer()。mapitems/map_io.cpp:Save 的序列化分支 + Load 的 flag 分派分支(197-348 / 710-863)。mapitems/layer_group.cpp:CLayerGroup::Render 的渲染/跳过逻辑(m_HasXxx,97-121)。quick_actions.h/.cpp + popups.cpp:AddXxxLayer() 与菜单项。editor_actions.cpp:CEditorActionAddLayer/DeleteLayer 里的特殊指针维护分支(最容易漏,见 §11)。editor_props.cpp / 该层的 RenderProperties:属性面板枚举。src/game/layers.cpp(CLayers::Init 与 InitTilemapSkip)、src/game/map/render_map.cpp(overlay 渲染)。CEnvelope(mapitems/envelope.h:10),EType{POSITION,COLOR,SOUND} 决定通道数(POSITION=3、COLOR=4、SOUND=1);从文件加载时用 CEnvelope(int NumChannels) 反推类型,1/3/4 之外 dbg_assert_failed。点存 std::vector<CEnvPoint_runtime>(含 m_Bezier),AddPoint 后总是按时间 Resort()。CEnvelope::Eval 把 vector 包成 IEnvelopePointAccess 适配器,转发给共享的 CRenderMap::RenderEvalEnvelope(render_map.cpp:249)——编辑器与游戏用同一份求值代码。CMapEnvelopeEvaluator::EnvelopeEval 在其上叠加动画时间与 TimeOffsetMillis。m_PosEnv/m_ColorEnv、tile 层的 m_ColorEnv、音源的 m_PosEnv/m_SoundEnv。IsEnvelopeUsed 由各 layer override 后向上聚合(map.cpp:694-707)。CEnvelope::GetValueRange 在 m_vPoints 为空、或 ChannelMask 未命中任何通道时返回 {0.0f, 1.0f}(envelope.cpp:62-67),避免下游拿到 ±∞,也让新建的空 envelope 直接有可用的 Y 轴范围。ModifyEnvelopeIndex:遍历所有组/层统一改写(map.cpp:264-271)。VisitEnvelopeReferences(map.cpp:643)+ references.h:构造 CLayerQuads/Sound/TilesEnvelopeReference 并记录被命中的元素下标,DeleteEnvelope 用它把引用置 -1/递减并返回引用对象,undo 时 UpdateEnvelopeReferences 重新绑到新 shared_ptr。MoveEnvelope 同时处理两个方向的位移。template<typename F> VisitEnvelopeReferences 声明在 map.h:239-240 但定义在 map.cpp:643 且没有显式实例化——在其它 .cpp 里调用会链接失败。目前只有 map.cpp 内部使用。CLayerSoundEnvelopeReference::SetEnvelope 的边界检查用了 <= (int)size()(references.cpp:34);当前调用路径不会触发,但属于隐患。mapitems/map_io.cpp)map_io.cpp:64-437)PerformPreSaveSanityChecks() → Storage()->FormatTmpPath(... ".map.tmp") → CDataFileWriter::Open → 依次写:version / info+settings / images / sounds / groups+layers / envelopes / envpoints(+bezier) → 构造 CDataFileWriterFinishJob → Engine()->AddJob → CEditor::m_WriterFinishJobs
要点:
AnalyseTileFlags(),源码里带 // TODO! 注释(本应在加载时做,map_io.cpp:130-132)。CTile 数组到 m_Data,真实数据放各自 m_Tele/m_Speedup/... 索引(map_io.cpp:240-259)。Item.m_Layer.m_Version = 0; // was previously uninitialized(:206,290,323),m_Tele..m_Tune 全置 -1(:233-238)。m_Data != -1(:303-309,335-341)。CURVETYPE_BEZIER 时才写 MAPITEMTYPE_ENVPOINTS_BEZIER(:377-429);in-tangent 只在前一个点是 bezier 时才拷贝(:411-415)。HandleWriterFinishJobs 负责收尾与错误弹窗。map_io.cpp:467-1086)按 item type 分块,加载时的 sanitization 是关键:
m_Version==1 && ItemSize >= sizeof(CMapItemInfoSettings) && m_Settings > -1 才解析。m_External → mapres/%s.png,非 16 整除尺寸时 TextureLoadFlag=0;内嵌数据校验宽度/高度/指针/GetDataSize。mapres/%s.opus;内嵌数据 malloc + mem_copy。m_Version < 1 || > 3 直接跳过整组(:682)。IsValidXxxTile() 成立时同步 m_pTiles[i].m_Index,否则清零(:775-851);switch 有 ENTITY_OFFSET 区间特判;image/sound 索引越界一律置 -1(:760,874,913)。:886,925)。<=0 || ==2 || >4)时回落到 MAX_CHANNELS 并报错(:1015-1025);m_aName[0] != -1 是"老地图名字全 -1"的兼容 hack(:1038-1039)。(m_GroupId, m_LayerId) 双下标回填,越界丢弃;且只对非物理 tile 层生效(:1055-1071)。CheckIntegrity() → PerformSanityChecks() → SortImages() → SelectGameLayer() → ResetModifiedState()。map_io.cpp:1088-1226)把新文件加载进临时 CEditorMap NewMap,再用两个 lambda(ReplaceIndex / ModifyAddIndex)配合 std::map<int*, bool> ReplacedIndicesMap 做一次性索引替换;图片按"同名+同数据则复用,同名不同数据则重命名 "%s (%d)" 后追加"去重;m_pGameGroup 显式跳过,避免出现两个 game group。可撤销靠 CEditorActionAppendMap。
FErrorHandler = std::function<void(const char *)>(map.h:61):同步阶段直接回调 UI 弹窗,异步写出阶段把错误存进 CDataFileWriterFinishJob::m_aErrorMessage 由 HandleWriterFinishJobs 弹出。日志域统一 "editor/save"。PerformPreSaveSanityChecks 只检查图片/声音数据缺失(map_io.cpp:439-465)——不检查越界索引、不存在的 envelope 引用等。PerformSanityChecks 只处理"图片尺寸非 16 整除的 tile layer"(map_io.cpp:1228-1261)。src/engine/shared/datafile.*)MAX_ITEM_TYPE=0xFFFF;ITEMTYPE_EX=0xFFFF 用于携带 UUID 的扩展类型,高编号 type 通过 OFFSET_UUID_TYPE=0x8000 双向翻译。src/game/mapitems_ex_types.h 用 UUID(id, "...@ddnet.tw") 注册,目前有 test / automapper config / envpoints bezier 三种。AddData 用 zlib 压缩;AddDataSwapped = swap_endian(...,4);AddItem 要求 Size % 4 == 0 且 Size==0 || pData(所以空 settings 用 malloc(std::max(Size,1)) 绕过,map_io.cpp:110)。Sha256()/Crc(),但编辑器保存路径不主动校验/写入 checksum,依赖 zlib 流完整性。cpp// editor_action.h:6
class IEditorAction : public CMapObject
{
public:
virtual void Undo() = 0;
virtual void Redo() = 0;
virtual bool IsEmpty() { return false; } // 空 action 不入栈
const char *DisplayText() const; // 历史面板显示,固定 256 字节
protected:
char m_aDisplayText[256];
};
CEditorHistory(editor_history.h:13):
cppvoid RecordAction(pAction, pDisplay = nullptr); // 记一条"已生效"的改动
void Execute(pAction, pDisplay = nullptr); // Redo() 后再 Record
bool Undo(); bool Redo();
void Clear();
void BeginBulk(); void EndBulk(const char *pDisplay = nullptr); void EndBulk(int DisplayToUse);
std::deque<std::shared_ptr<IEditorAction>> m_vpUndoActions, m_vpRedoActions; // public,UI 直接遍历
deque 是为了 O(1) 从头部丢弃超配额的老动作。RecordAction 会清空 redo 栈,并受 cl_editor_max_history(默认 50)限制,超限 pop_front。Execute = "先 Redo() 再记录"。因此约定:action 构造函数不得修改地图,否则会重复执行一次。pDisplay 时会把单动作包一层 CEditorActionBulk 以覆盖显示名。BeginBulk/EndBulk 把多条 action 包成一个 CEditorActionBulk(editor_history.cpp:75-92),用于"一次操作产生多条 action"的场景;Reverse 标志控制 Undo 顺序(删除多个元素时需要)。Undo/Redo 只弹栈、调用动作、压入另一个栈,不检查 IsEmpty();undo/redo 栈共享同一份 shared_ptr,所以 Apply 必须是纯函数式重放。cpp// map.h:130-133
CEditorHistory m_EditorHistory; // 主编辑器
CEditorHistory m_ServerSettingsHistory; // 服务器设置编辑器
CEditorHistory m_EnvelopeEditorHistory; // 曲线编辑器
CEditor::ActiveHistory()(editor.cpp:5041)根据 m_ActiveExtraEditor 选择当前生效的那套:
cppif(m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS) return Map()->m_ServerSettingsHistory;
else if(m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES) return Map()->m_EnvelopeEditorHistory;
else return Map()->m_EditorHistory;
所以 Ctrl+Z(editor.cpp:3791)作用于"你当前正在用的那个编辑器"。历史面板(EXTRAEDITOR_HISTORY)可以切换查看三套历史(editor_history.cpp:99-246),列表 = 先 redo(灰字)再 undo(倒序),最底下是固定的 "Loaded map" 基准;点击某一项会连续 Undo()/Redo() 到该位置。
| 基类 | 声明 | 语义 |
|---|---|---|
IEditorAction | editor_action.h:6 | 根接口 |
CEditorActionLayerBase | editor_actions.h:23 | 记住 (GroupIndex, LayerIndex) 并在构造时缓存 shared_ptr<CLayer> m_pLayer(editor_actions.cpp:577-581),保证 layer 不被销毁 |
CEditorActionEditLayerPropBase<E> | editor_actions.h:259 | 模板:(prop, previous, current) 单 int 属性 |
CEditorActionBulk | editor_actions.h:173 | 组合 N 个 action 为一步 |
41 个具体 action 按领域分组:
CEditorBrushDrawAction(跨层收集 m_vTileChanges + tele/tune/switch/speedup 四张专属 change map)、CEditorActionTileChanges(automapper 等批量变更)。CEditorActionQuadPlace、CEditorActionDeleteQuad(索引降序保存避免移位)、CEditorActionNewQuad、CEditorActionNewEmptyQuad、CEditorActionEditQuadPoint、CEditorActionEditQuadColor、CEditorActionEditQuadProp、CEditorActionEditQuadPointProp。CEditorActionSoundPlace、CEditorActionDeleteSoundSource、CEditorActionNewEmptySound、CEditorActionMoveSoundSource、CEditorActionEditSoundSourceShape、CEditorActionEditSoundSourceProp、CEditorActionEditRectSoundSourceShapeProp、CEditorActionEditCircleSoundSourceShapeProp、CEditorActionEditLayerSoundsProp。CEditorActionAddLayer(含 duplicate)、CEditorActionDeleteLayer、CEditorActionGroup、CEditorActionEditGroupProp、CEditorActionEditLayerProp、CEditorActionEditLayerTilesProp(12 个属性)、CEditorActionEditLayerQuadsProp、CEditorActionEditLayersGroupAndOrder(拖拽改变层级与顺序)。CEditorActionAppendMap、CEditorActionTileArt、CEditorActionQuadArt、CEditorCommandAction(服务器设置命令的 DELETE/ADD/EDIT/MOVE_UP/MOVE_DOWN)。CEditorActionEnvelopeAdd/Delete/Edit、CEditorActionEnvelopeEditPointTime、CEditorActionEnvelopeEditPoint、CEditorActionAddEnvelopePoint、CEditorActionDeleteEnvelopePoint、CEditorActionEditEnvelopePointValue、CEditorActionResetEnvelopePointTangent。EEditState(ui.h:24):NONE / START / EDITING / END / ONE_GO。控件(UiDoValueSelector 等)返回 SEditResult<T>{ m_State, m_Value },DoPropertiesWithState<E>(editor_props.cpp:16)把状态透传给 tracker。
CPropTracker<T, E>(editor_trackers.h:131-198)是核心模板:
cppvoid Begin(const T *pObject, E Prop, EEditState State, int GroupIndex = -1, int LayerIndex = -1);
void End (E Prop, EEditState State, int GroupIndex = -1, int LayerIndex = -1);
protected:
virtual void OnStart(E Prop);
virtual void OnEnd(E Prop, int Value); // 这里通常 RecordAction
virtual int PropToValue(E Prop);
virtual bool EndChecker(E Prop, int Value) { return Value != m_OriginalValue; }
START 或 ONE_GO → m_Tracking = true; m_OriginalValue = Value; OnStart()END 或 ONE_GO → 若 EndChecker 通过则 OnEnd()于是:点击 +/-(ONE_GO)Begin+End 同一帧直接产生 action;拖动滑条(START … EDITING … END)只在松手时产生一条 action。
各 tracker 挂在 CEditorMap 上(map.h:134-145),映射关系:
| Tracker | 产出 action | 收尾位置 |
|---|---|---|
m_QuadTracker(CQuadEditTracker) | EditQuadPoint / EditQuadColor / EditQuadProp / EditQuadPointProp,包 bulk | editor.cpp:1659,1689,1934,1938;popups.cpp:963,995,1016,1056,1161,1431 |
m_EnvOpTracker(CEnvelopeEditorOperationTracker) | EnvelopeEditPointTime + EditEnvelopePoint,bulk "Envelope point drag" | envelope_editor.cpp:838,840,1442 |
m_LayerGroupPropTracker | EditGroupProp | popups.cpp:731,779 |
m_LayerPropTracker | EditLayerProp;GROUP 特化为 EditLayersGroupAndOrder | popups.cpp:869,894 |
m_LayerTilesPropTracker | EditLayerTilesProp(+SetSavedLayers) | layer_tiles.cpp:1067,1162 |
m_LayerTilesCommonPropTracker | N× EditLayerTilesProp 包 bulk(多选图层公共属性) | layer_tiles.cpp:1301,1337 |
m_LayerQuadPropTracker(类型是 CLayerQuadsPropTracker) | EditLayerQuadsProp | layer_quads.cpp:251,261 |
m_LayerSoundsPropTracker | EditLayerSoundsProp | layer_sounds.cpp:174,184 |
m_SoundSourceOperationTracker | MoveSoundSource | editor.cpp:884,891 |
m_SoundSourcePropTracker | EditSoundSourceProp | popups.cpp:1229,1290 |
m_SoundSourceRectShapePropTracker / ...Circle... | EditRectSoundSourceShapeProp / ...Circle... | popups.cpp:1310,1317,1337,1348 |
需要注意 CLayerTilesPropTracker 这类会在 OnStart 里保存整个图层的副本(m_SavedLayers),因为改变宽/高等属性需要保留原始像素数据;CEditorActionEditLayerTilesProp::SetSavedLayers() 把副本交给 action。这是历史里最占内存的路径;EndChecker 对 SHIFT 特判为"总是记录",因为它是动作型属性(editor_trackers.cpp:474-477)。
以 CLayerTiles::RenderProperties(layer_tiles.cpp:1067-1180)为主线:
DoPropertiesWithState<ETilesProp> 返回 {State=START, Prop=WIDTH}。Map()->m_LayerTilesPropTracker.Begin(this, Prop, State)(layer_tiles.cpp:1067)。PropToValue(WIDTH) 读 m_Width 存为 m_OriginalValue;OnStart(WIDTH) 因为 WIDTH/HEIGHT 会被实体层跟随缩放,所以 Duplicate() 本层以及若干相关层存进 m_SavedLayers。此时地图尚未修改,快照是干净的。Map()->m_EditorHistory.BeginBulk()(:1068)。Resize(NewVal, m_Height) 直接改 m_Width 与 tile 数组;Begin() 因 m_Tracking 已 true 提前返回,不重复快照。End(Prop, State) → EndChecker 通过 → OnEnd 构造 CEditorActionEditLayerTilesProp、SetSavedLayers(...),RecordAction。此刻 m_IsBulk == true,动作进入 m_vpBulkActions 缓冲,redo 栈尚未清空。m_AutoAutomapper,再把 m_TilesHistory 包成 CEditorActionTileChanges 记录,同样进 bulk 缓冲(:1165-1176)。EndBulk(0)(:1180)→ 取第 0 个动作的 DisplayText() 作为整体显示名,构造 CEditorActionBulk(..., /*Reverse=*/true) 正式入栈。Reverse=true 保证 Undo 时先回滚 automapper 的 tile 变化、再回滚 resize。结果:整个拖拽 + automapper 在 History 面板里是一行,Undo 一次全部回滚。这正是 layer_tiles.cpp:1177-1179 注释所写的意图。
CEditor::RenderEditorHistory(editor_history.cpp:99):
m_HistoryType;每条历史有各自的 CListBox 与选中索引(CEditorHistoryUiState,实例在 map.h:178)。Undo()/Redo() 到该位置;垃圾桶按钮 Clear()。CEditor::Render,editor.cpp:3742)┌──────────────────────────────────────────────────────────────────────────────┐ │ MenuBar (20) [File][Tools][Settings] 坐标/缩放信息 [?][×] │ RenderMenubar 3644 ├───────────────┬──────────────────────────────────────────────────────────────┤ │ ModeBar │ MapTabs: [map1 ×][map2 ×]… (CScrollRegion 横向滚动)│ DoMapTabs 343 │ (在 ToolBar ├──────────────────────────────────────────────────────────────┤ │ 左侧切出) │ ToolBar (78) 当前模式的工具条 │ DoToolbarLayers 449 │ [Layers| │ │ DoToolbarImages 826 │ Images| │ │ DoToolbarSounds 840 │ Sounds] │ │ ├───────────────┼──────────────────────────────────────────────────────────────┤ │ ToolBox │ │ │ (m_ToolBox- │ MapView 画布 │ CMapView::Render │ Width) │ 网格 / 组边框 / 画笔预览 / quad 点 / envelope 预览 / │ (map_view.cpp:121) │ RenderLayers │ proof 边框 / ingame 实体 overlay │ + DoQuad/DoQuadPoint │ 2336 │ │ + DoSoundSource │ RenderImages │ (仅 MODE_LAYERS 才渲染,editor.cpp:3782) │ │ List 3228 │ │ │ RenderSounds │ │ │ 3361 │ │ ├───────────────┴──────────────────────────────────────────────────────────────┤ │ ExtraEditor(高度=m_aExtraEditorSplits[i],editor.cpp:3935-3950) │ │ CEnvelopeEditor::Render | RenderServerSettingsEditor | RenderEditorHistory │ ├──────────────────────────────────────────────────────────────────────────────┤ │ StatusBar (16) [Envelopes][ServerSettings][History] + tooltip 区 │ RenderStatusbar 3547 └──────────────────────────────────────────────────────────────────────────────┘
对应代码:
editor.cpp:3761-3771(MenuBar/ToolBar/ToolBox/StatusBar/ExtraEditor)。ModeBar 不是独立区域,而是 ToolBar 左侧切出来的一块(editor.cpp:3829),因此与 ToolBox 同宽(m_ToolBoxWidth)。Tab 隐藏整个 GUI(editor.cpp:4891-4899);此时工具栏被搬到屏幕外 (-100,-100) —— 注释明说这是 "hack to get keyboard inputs from toolbar even when GUI is not active"(editor.cpp:3774-3779),因为快捷键检测是写在按钮旁边的。MapView()->Render(View)(editor.cpp:3783)。RenderLayers / RenderImagesList / RenderSounds(editor.cpp:3910-3919),宽度可通过 DoEditorDragBar 拖拽(clamp 到 [100,400])。editor.cpp:3938-3949,高度存 m_aExtraEditorSplits[]。editor.cpp:4035-4037,之后 Ui()->RenderPopupMenus() + FreeDynamicPopupMenus()(:4036-4037)。RenderTooltip → RenderMousePointer(:4048-4052)。没有统一的 tool 枚举。工具语义由三层合成:
m_Mode ∈ {MODE_LAYERS, MODE_IMAGES, MODE_SOUNDS}(editor.h:60-66)—— 只影响工具栏/左栏以及是否渲染画布。m_pBrush(本质是 CLayerGroup,editor.cpp:4639)+ m_pTilesetPicker / m_pQuadsetPicker(:4619/:4624)。Map()->m_MapViewState.m_ActiveOp(map_view.h:16-24):cppenum class EActiveOp { NONE, BRUSH_GRAB, BRUSH_DRAW, BRUSH_PAINT, PAN_WORLD, PAN_EDITOR };
CMapView::Render 里:空画笔 + 左键 → BRUSH_GRAB;非空画笔 + 左键 → 先 BrushPlace 再 BRUSH_DRAW;Shift → BRUSH_PAINT(map_view.cpp:465-483)。
平移/缩放:
cppconst bool ShouldPan = Ui()->HotItem() == Editor()->MapView() &&
((Input()->ModifierIsPressed() && Ui()->MouseButton(0)) || Ui()->MouseButton(2)); // map_view.cpp:249
m_pContainerPanned 是跨帧拖拽句柄(const void*,值就是 MapView() 自身地址):设为 PAN_WORLD(Shift 时 PAN_EDITOR,分别写 m_WorldOffset / m_EditorOffset),松开置回 nullptr。缩放是 CSmoothValue(三次贝塞尔),滚轮 1.1 倍、小键盘 +/-/* 为 1.331 倍/重置,ed_smooth_zoom_time 控制动画,ed_zoom_target 控制是否以鼠标为中心。WASD 平移。
Alt 轴锁定:CEditor::MouseAxisLock(editor.cpp:4647)在按住 Alt、画笔非空、且拖动距离超过 1 格后把移动锁到水平/垂直。
quad 轴约束:另一套,用 Shift(GetDragAxis / ApplyAxisAlignment / DrawAxis,editor.cpp:1002-1443)。
网格吸附:CMapGrid::SnapToGrid(map_grid.cpp:110);Alt 会忽略网格。
对象对齐/吸附:ComputePointAlignments / ComputePointsAlignments / ComputeAABBAlignments / ApplyAlignments / DrawPointAlignments / DrawAABB(editor.cpp:1035-1456),总开关 ed_align_quads;ed_show_quads_rect 控制包围盒显示。
拖拽状态机:
CRenderLayersState(ELayerOperation{CLICK, LAYER_DRAG, GROUP_DRAG}),RenderLayers(editor.cpp:2347)。用 m_PreviousOperation 在回到 NONE 时补记 undo;active item 变了就取消拖拽(安全阀)。DoQuad 的静态 s_Operation ∈ {OP_SELECT, OP_MOVE_ALL, OP_MOVE_PIVOT, OP_ROTATE, OP_CONTEXT_MENU, OP_DELETE};id 直接是 pivot 地址 &pQuad->m_aPoints[4]。DoQuadPoint,id 是 &pQuad->m_aPoints[V],Shift 进入 UV 编辑。DoSoundSource / UpdateHotSoundSource(hot item 也是指向数据的指针)。DoColorPickerButton(弹窗关闭后补一次 EEditState::END 的 SetColor,保证撤销记录拿到终值);吸管用"覆盖全屏的隐形按钮"截获点击。CUi 维护 m_pHotItem / m_pBecomingHotItem / m_pActiveItem。StartCheck()/FinishCheck() 在帧首尾做"active item 若本帧未被任何控件确认则清空"的垃圾回收——所以每个控件每帧都必须调用一次 CheckActiveItem。
void *pId 的约定是地址即身份,必须唯一且跨帧稳定:
&m_aSavedColors[i]、&m_QuickActionProof、&pQuad->m_aPoints[4]、&Source;((char *)&pIds[i]) + 1 / + 2 给"减号/加号"按钮(editor_props.cpp:59,75);&Map()->m_vpImages[i] 这类"容器元素地址"在 vector 扩容后失效。Popup API(ui.h:737,实现 ui_popups.cpp:14-27)要点:
Active = i == size()-1,Active 弹窗屏蔽下层;点外部/Escape 关闭。因此弹窗一旦打开就独立于打开它的代码存活,必须显式关闭(Reset() 里 Ui()->ClosePopupMenus())。SPopupMenuId* 指针相等,所以 ID 必须是 static 或成员对象地址。POPUP_KEEP_OPEN / POPUP_CLOSE_CURRENT / POPUP_CLOSE_CURRENT_AND_DESCENDANTS。if(!Ui()->IsPopupOpen(&s_Id)) 之类的守卫。pContext 是 void*,渲染函数里 static_cast,无类型检查;部分 context 持有 layer 指针/索引(SLayerPopupContext),所以弹窗开头都要做范围校验。m_PopupMapTab、m_QuadPopupContext、m_PointPopupContext、State.m_LayerPopupContext);多数弹窗干脆把 this 当 context。ShowFileDialogError 把 new CUi::SMessagePopupContext 放进以消息文本为 key 的 std::map,由 FreeDynamicPopupMenus() 回收——editor.h:289 的 TODO 就在吐槽这个设计。editor_ui.cpp 提供的控件| 函数 | 行 | 说明 |
|---|---|---|
UpdateTooltip | 8 | 写进单一 m_aTooltip 缓冲,延迟到帧尾渲染 |
GetButtonColor | 16 | Checked 魔数 1..10:1=selected、2/3=unused、4/5=should-be-embedded、6/7=game layer、8=invisible、9=DANGEROUS、10=POSITIVE |
DoButtonLogic | 71 | 包 Ui()->DoButtonLogic,顺带记录 m_pUiGotContext(右键) |
DoButton_Editor | 83 | 文本按钮,Checked %= 2 |
DoButton_Env | 92 | envelope 专用配色按钮 |
DoButton_Ex | 104 | 可控 corners/字号/对齐 |
DoButton_FontIcon | 119 | 渲染 FontIcon::* |
DoButton_MenuItem | 132 | 菜单行;Checked < 0 显示灰色禁用 |
DoButton_DraggableEx | 152 | 输出 pClicked/pAbrupted,图层列表拖拽基础 |
DoEditBox / DoClearableEditBox | 174 / 180 | 带 tooltip 的 editbox |
UiDoValueSelector | 186 | 数值控件:左键拖动(Shift 更精确)、右键转文本输入、返回 EEditState |
RenderBackground | 324 | 平铺贴图背景 |
quick_action.h:CQuickAction 持有 label/description + 四个 std::function(Callback/Disabled/Active/Color)与 m_ActionButtonId;ActionButtonId() 返回对象自身地址,因此天然唯一稳定。LabelShort() 取第一个 ": " 之后的部分。
quick_actions.h 是纯 X-macro 表(55 项)。宏展开三次(见 §2.7)。
注意
工具栏不会自动遍历 quick actions。按钮逐个点名引用 m_QuickActionX;唯一的自动遍历是 CPrompt(Ctrl+P 命令面板):模糊匹配 label、跳过 Disabled()、激活时 Call()。
55 项按用途分组:
FillGameTiles(EGameTileOp::*))。两层变换:CLayerGroup::MapScreen() → pGroup->Mapping()(处理 parallax / offset / clipping),再 Graphics()->MapScreen(ScreenRect)。鼠标世界坐标由 CMapView::UpdateMouseWorld() 反推:
cppm_MouseWorldScale = WorldWidth / Graphics()->WindowWidth();
m_MouseWorldPos.x = GroupRect.m_TopLeft.x + WorldWidth * (MousePos.x / WindowWidth());
m_MouseWorldPos.y = GroupRect.m_TopLeft.y + WorldHeight * (MousePos.y / WindowHeight());
注意
m_MouseWorldScale 是单一标量(取宽度比),纵向各向异性时会被忽略。另有基于 game group 的 m_MouseWorldNoParaPos(proof 菜单选点用)。
编辑器视口偏移走 Graphics()->MapScreenToWorld(...) 做"向鼠标缩放"的反算;m_WorldZoom = Zoom()->GetValue() / 100.0f。OffsetWorld/OffsetEditor 区分世界平移与"编辑器专属偏移"(Shift+拖动、ResetZoom 清零)。
Grid:GridLineDistance() 按 zoom 阶梯取 4/8/16/32/64/128/256/512,Factor()(1..15)画强线/弱线。
画布内的额外渲染:
map_view.cpp:102-111);zoom ≤ 300 时显示 tile 信息。RenderIngameEntities 用 CGameClient::m_Items 画门激光/武器/护盾/雪花/旗帜,受 ed_show_ingame_entities 控制。UpdateHotQuadPoint,再对每个 quad 调 4 次 DoQuadPoint + 1 次 DoQuad(点标记本身就用 quad 画)。m_ShowEnvelopePreview + m_ActiveEnvelopePreview{NONE, SELECTED, ALL};帧尾 DoQuadEnvelopes / DoQuadEnvPoint。CProofMode 画玩家可见范围边框。CEditor::Render() → MapView()->Render(View) → CMapView::RenderEditorMap() (map_view.cpp:72) → 对每个可见 group pGroup->Render(Map()) (map_view.cpp:96-100) → CLayerGroup::Render:MapScreen() + ClipEnable 第一遍:跳过所有物理实体层,渲染普通层 (layer_group.cpp:103-104) 第二遍:单独渲染 game/front/tele/speedup/tune/switch (layer_group.cpp:111-121) → game group 再 MapScreen() 一次,只渲染 IsEntitiesLayer() 的层(压在最上层) (map_view.cpp:102-111)
CLayerTiles::Render:按 m_HasGame/Front/Tele/Speedup/Switch/Tune 选内置贴图,否则用 m_Image 的 texture(要求尺寸 16 整除);颜色 = m_Color × EnvelopeEval(m_ColorEnvOffset, m_ColorEnv, ...);两次 RenderTilemap(opaque/transparent)+ 各物理层 overlay。
CLayerQuads::Render:设置 texture 后 ForceRenderQuads 两遍。CRenderMap::ForceRenderQuads 是立即模式、逐 quad 提交 IGraphics::CFreeformItem:每 quad 查 color envelope(4 通道)与 position envelope(3 通道,r/g=偏移、b=旋转角度),非零 rotation 时围绕 m_aPoints[4](pivot)旋转。
注意
没有 portal / DeactivatePortal / quad tessellation 优化,也没有按帧缓存;CRenderMap 里的 opaque 判定实际恒为 false(纹理分析是 // TODO)。
tele/switch/tune 在 render_layer.cpp 里只渲染 transparent 那一遍,没有冗余的 BlendNone() + LAYERRENDERFLAG_OPAQUE 调用——排查"某层少画了一遍"时要注意这一点。
CLayerSounds::Render:用 EnvelopeEval(...,2) 取位置偏移,画 falloff 圆/矩形 + IMAGE_AUDIO_SOURCE 精灵。
① 用户把画笔放到 canvas,按住左键拖动 CMapView::Render (map_view.cpp:461-484) ├─ m_MapViewState.m_ActiveOp = BRUSH_DRAW └─ CLayerTiles::BrushPlace(pBrush, worldPos) // 立即写入,所见即所得 ② 拖动过程中每帧 CMapView::Render (map_view.cpp:364-388) └─ CLayerTiles::BrushDraw(pBrush, worldPos) └─ SetTile() 先写 SetTileIgnoreHistory,再 RecordStateChange(x, y, Previous, Tile) (layer_tiles.cpp:84-124) └─ 第一次写:m_TilesHistory[y][x] = {true, Previous, Tile} 再次写同一格:只更新 .m_Current tele/speedup/switch/tune 各自记进自己的 m_History ③ 松开左键 CMapView::Render (map_view.cpp:664-680) └─ std::make_shared<CEditorBrushDrawAction>(Map(), m_SelectedGroup) └─ 构造函数 (editor_actions.cpp:15) 遍历该组所有 tile 层, 把 m_TilesHistory "取走 + ClearHistory()", 汇总成 m_vTileChanges / m_TeleTileChanges / ... SetInfos() 统计后 str_format(m_aDisplayText, "Brush draw (x%d) on %d layers", ...) └─ if(!pAction->IsEmpty()) Map()->m_EditorHistory.RecordAction(pAction); └─ editor_history.cpp:20 清空 redo 栈、按 cl_editor_max_history 限长、入栈 ④ Ctrl+Z CEditor::Render (editor.cpp:3788-3794) └─ ActiveHistory().Undo() → CEditorHistory::Undo() (editor_history.cpp:41) └─ CEditorBrushDrawAction::Undo() → Apply(true) (editor_actions.cpp:138)
设计要点:tile 的"旧值"不是靠快照整个图层,而是写入时就地记录 diff(STileStateChange{bool m_Changed; CTile m_Previous; CTile m_Current;});画笔笔画结束才把 diff 提升成 action。Apply 对普通层用 SetTileIgnoreHistory(避免再次写历史),对 speedup/tele/switch/tune 直接按 y*Width+x 写各字段。这既省内存,又能精确到"这一笔画了哪些格子"。
CQuadEditTracker 不走 EEditState,而由拖拽生命周期驱动:
① 鼠标按下顶点 CEditor::DoQuadPoint (editor.cpp:1768) / DoQuad (editor.cpp:1458) └─ Map()->m_QuadTracker.BeginQuadTrack(pLayer, Map()->m_vSelectedQuads, -1, LayerIndex) └─ 把每个选中 quad 的 m_aPoints 全量拷进 m_InitialPoints (editor_trackers.cpp:43-48) ② 拖动中:PreparePointDrag / DoPointDrag / DrawAxis / DrawAABB 实时修改 quad 点坐标,但不产生 action (DoPointDrag 用 "原始值 + Offset" 落值,因为对齐数学需要未修改的起点) ③ 松开左键 Map()->m_QuadTracker.EndQuadTrack() (editor.cpp:1659/1689; editor_trackers.cpp:51-69) └─ 逐 quad 比较 m_InitialPoints 与当前点,只为真正变化的 quad 生成 CEditorActionEditQuadPoint,非空则包一层 CEditorActionBulk 记录
右键取消旋转时只回滚内存、不记录,所以取消不产生历史项——这是正确行为。popup 里的 Aspect ratio / Center pivot / Align / Square 按钮复用同一套 tracker,实现"按钮改多处点 → 一个历史项"。
Envelope 侧对应 EEnvelopeEditorOp{SELECT, DRAG_POINT, DRAG_POINT_X, DRAG_POINT_Y, CONTEXT_MENU, BOX_SELECT, DRAG_TIME_BAR, SCALE}:Begin(op) 中若已有其他 op 会先 Stop(true);Stop(Switch=true) 对非 SCALE 的拖拽直接丢弃,只有鼠标松开时的 Stop(false) 才写历史。
① 用户拖动属性滑条 CLayerTiles::RenderProperties → CEditor::DoPropertiesWithState<int> (editor_props.cpp:16) └─ UiDoValueSelector 返回 SEditResult<int>{ m_State=START/EDITING/END/ONE_GO, m_Value } ② DoPropertiesWithState 把 (prop, state, value) 交给 tracker Map()->m_LayerTilesPropTracker.Begin(this, ETilesProp::WIDTH, State, ...) // 若 state==START Map()->m_LayerTilesPropTracker.End (ETilesProp::WIDTH, State, ...) // 若 state==END ├─ OnStart: 保存原始图层副本 / 原值 └─ OnEnd: 若值变化 → RecordAction(CEditorActionEditLayerTilesProp(...)) (多选场景还会先把多层变更 BeginBulk/EndBulk 成一条) ③ 属性面板实时更新数值;撤销时 action::Apply(previous) 恢复整层数据
因此"属性面板"和"画布拖拽"共用同一套撤销基础设施,区别只在于谁负责发 Begin/End。
注意
DoProperties()(无 State 版,editor_props.cpp:9)会丢弃 EEditState,因此仍使用它的面板无法获得拖拽合并。目前只用于笔刷配置类面板(网格设置、quad art 等),这些本来就不该进历史——但没有编译期保护,把地图属性面板迁回它就会静默丢失拖拽合并。
envelope_editor.{h,cpp}模型:CEnvelope(mapitems/envelope.h:10-51)持有 std::vector<CEnvPoint_runtime> m_vPoints、m_aName[32]、m_Synchronized,类型 EType{POSITION,COLOR,SOUND} 决定通道数(POSITION=3、COLOR=4、SOUND=1)。CEnvPoint 是 4 通道模型:CFixedTime m_Time(毫秒定点)、m_Curvetype(CURVETYPE_STEP..CURVETYPE_BEZIER,未知值按 LINEAR 处理)、m_aValues[4](22.10 定点)。Bezier 切线每通道各存一对 in/out 的 DeltaX(时间)与 DeltaY(值),运行时合成 CEnvPoint_runtime。求值走共享的 CRenderMap::RenderEvalEnvelope(见 §4.6)。
状态:CEnvelopeEditor::CState(envelope_editor.h:21-31):CSmoothValue m_ZoomX/m_ZoomY、m_Offset、m_ActiveChannels(通道位掩码,默认全开)。CSmoothValue::UpdateValue() 返回"是否仍在变化",用它做缩放动画并同步平移。屏幕 ↔ 包络坐标转换集中在 ScreenToEnvelopeX/Y / EnvelopeToScreenX/Y(Y 轴翻转),平移量以"屏幕归一化单位"保存,所以窗口缩放安全。
接入方式:Quick action "Envelopes" 切 m_ActiveExtraEditor = EXTRAEDITOR_ENVELOPES;面板在 editor.cpp:3938-3940 绘制,顶部有 DoEditorDragBar 调高度。
注意
它只覆写 OnReset() 并公开 Render(CUIRect)——没有覆写 OnInput:所有键盘操作都在 Render 内用 Input()->KeyPress 轮询(且只在 HotItem() 命中时生效)。这绕过了 OnInput 的事件优先级机制,与其它组件的输入处理耦合较脆弱。
UI 工作流(要点):工具栏支持 undo/redo、新建 Sound/Color/Position 包络、删除、左右移动、缩放;未使用的包络显示绿色、被引用的显示红色。画布上右键/Ctrl+左键平移;悬停高亮在"点 / in 切线 / out 切线"三类手柄中取最近者;双击空白处用 Eval() 取当前曲线值生成新点(时间重复则忽略,负时间会把首点归零);左键拖拽框选;点/切线拖拽有 X/Y 轴锁定变体;时间条可拖动;选中点后按 S 进入缩放模式(Shift 缩时间轴、Alt 以中点为基准、Ctrl 精细);曲线类型通过曲线段中点的小按钮循环切换(Shift 反向)。
拖拽时的实时数值 tooltip:拖动过程中会在光标旁显示精确数值,按操作类型分派到 RenderPointTimeTooltip(X 轴拖点 → 时间)、RenderPointValueTooltip(Y 轴拖点 → 值)、RenderTangentIn/OutTooltip(贝塞尔切线)、RenderScaleTooltip(缩放模式)、RenderTimebarTooltip(时间条),统一由 RenderTooltip(CUIRect *pTooltipRect, CUIRect Boundary) 收口(envelope_editor.cpp:1677-1900 一带,RenderTooltip 定义在 :1866)。这些 tooltip 走的是编辑器自己的 m_aTooltip 机制,不是 §7.3 提到的通用 CUi::DoToolTip(见 §11 待办)。对外暴露 bool IsPointOperationActive() const(envelope_editor.cpp:1568),供主编辑器判断"当前是否正在拖包络"。
相关信息
缩放模式(EEnvelopeEditorOp::SCALE)会额外做两件事:Ui()->SetActiveItem(&m_ScaleOperationId) 锁住 active item,防止拖缩放时误触其它控件;并把 m_ActiveEnvelopePreview 强制设为 SELECTED(envelope_editor.cpp:1367-1368)。
如何变成可撤销:CEnvelopeEditorOperationTracker(EEnvelopeEditorOp 共 9 种操作)。拖拽开始时 Begin(op) 快照所有选中点的 m_Time 与各通道值;结束时逐字段比较,生成 CEditorActionEnvelopeEditPointTime / CEditorActionEnvelopeEditPoint(VALUE),包成一个 CEditorActionBulk(..., "Envelope point drag") 记入 Map()->m_EnvelopeEditorHistory。一次拖拽 = 一条撤销项。
与主编辑器的选中态:CEditorMap::m_SelectedEnvelope、m_vSelectedEnvelopePoints(vector<pair<pointIndex, channel>>)、m_SelectedTangentInPoint/OutPoint(map.h:155-158)。主编辑器 quad 属性里的 "Pos. Env" / "Color Env" 与 envelope 面板共用同一包络索引;EEnvelopePreview{NONE, SELECTED, ALL} 让地图视图叠加显示选中包络;DoQuadEnvelopes / DoQuadEnvPoint 允许直接在地图上拖动 quad 的包络点(MOVE/ROTATE 两种模式)。
相关信息
quad 包络预览的"选中点"已经和 envelope 编辑器的选中点解耦。CEditorMap 用 m_SelectedQuadEnvelopePoint(map.h:153)单独记录 quad 包络预览的选中点;DoQuadEnvPoint 只写 m_SelectedQuadEnvelope / m_SelectedQuadEnvelopePoint,不再调用 SelectEnvPoint()/DeselectEnvPoints()。所以在地图上点一个 quad 包络点时,envelope 面板里的多选状态不会被清空;反过来,只有在没有 quad 包络点被选中时(m_SelectedQuadEnvelope == -1)面板才按 IsEnvPointSelected 高亮。DoQuadEnvelopes 的 alpha 高亮同时接受这两种来源(editor.cpp:2085-2091)。
动作重放时必须重新施加不变量:CEditorActionEditEnvelopePointValue::Apply 会重新做"点时间单调递增、首点时间恒为 0、颜色通道钳制到 [0,1]"——这些约束拖动时由 UI 施加一次,Undo/Redo 时必须再施加一次,否则历史会回放出非法状态。另:CEditorActionDeleteEnvelopePoint 构造时整个 CEnvPoint_runtime 拷贝(含 bezier 切线);Redo 时还要把该点从 m_vSelectedEnvelopePoints 中摘除,否则选择状态悬指。
editor_server_settings.{h,cpp}表示:加载后的设置就是字符串——std::vector<CEditorMapSetting> CEditorMap::m_vSettings(map.h:106),每项只有 char m_aCommand[256]。不保存结构化解析结果,解析只在编辑/校验时临时进行。
设置从哪来(CMapSettingsBackend::OnInit):
CFGFLAG_GAME 的 int 配置变量:ConfigManager()->PossibleConfigVariables("", CFGFLAG_GAME, ...),把 script name / help / default / min / max 装进 SMapSettingInt。这就是"对照真实配置变量校验"的来源。tune "s[tuning] f[value]"、tune_zone "i[zone] s[tuning] f[value]"、tune_zone_enter/leave "i[zone] r[message]"、mapbug "s[mapbug]"、switch_open "i[switch]"。i/f/s/r(int / float / string / rest-of-line),运行时用 'u' 表示未知。Builder("tune").Argument(0).Add(...),内部是 map<命令名, map<参数下标, vector<可用值>>>。EArgConstraint{DEFAULT, UNIQUE, MULTIPLE}:tune.0 UNIQUE、tune_zone.0 MULTIPLE、tune_zone.1 UNIQUE、tune_zone_enter/leave.0 UNIQUE、switch_open.0 UNIQUE、mapbug.0 UNIQUE。解析/校验流水线:CContext::Update() → UpdateFromString()(先扫描注释 #,引号内忽略并支持 \ 转义;取第一个词与设置表不区分大小写匹配)→ ParseArgs()(逐 token;NextArg 处理引号字符串并检测 "abcd"ef 这类非法拼接;按声明类型做基础校验;再 ValidateArg() 做范围/可能值校验,返回 VALID / INCOMPLETE(前缀匹配)/ UNKNOWN / OUT_OF_RANGE)。错误归一化为 SCommandParseError::EErrorType,只保留第一个错误。
每个参数的屏幕 X 位置由 TextRender()->TextWidth 累加得出,用来在输入框上叠加语法着色:string/rest 青色、number 绿色、unknown 灰色、错误红色。UpdatePossibleMatches() 产出下拉补全(命令名或参数值前缀匹配)。
合法性与冲突检测:Valid() 要求命令被识别、所有参数无错、参数个数等于声明个数。CheckCollision() 返回 ECollisionCheckResult{ERROR, REPLACE, ADD} 与冲突行号:int setting 按"首个 token 相同"即冲突(只能 REPLACE);command setting 把所有现有行按空格切分、按参数索引逐层收窄候选集,遇到 UNIQUE 约束的相同值就判 REPLACE。UI 用它决定 Add/Replace 按钮。
加载时校验:OnMapLoad() 用 CHeadlessContext 逐条解析 m_vSettings,合法项进 m_vSettingsValid,非法项记为 SInvalidSetting{index, setting, TYPE_INVALID|TYPE_DUPLICATE, collidingIndex, unknown, context{...}};只要存在非法项就强制 m_Dialog = DIALOG_MAPSETTINGS_ERROR。该对话框允许逐条 "Fix" 就地编辑或标为删除,然后重新校验。
UI 上下文:CContextWithInput(内嵌 CLineInputBuffered<256> + CListBox)挂在 Map()->m_MapSettingsCommandContext(按地图),而后端 CMapSettingsBackend 是 CEditor 的全局成员(配置表全局、输入上下文按图——不对称)。
为什么需要独立历史:(1) 面板激活时 Ctrl+Z 应作用于文本编辑历史,与 envelope/map 的条目无关;(2) 面板有自己的撤销/重做按钮,需要独立的 CanUndo/CanRedo;(3) 历史查看器要能单独展示 "Settings"。动作用 CEditorCommandAction(DELETE/ADD/EDIT/MOVE_UP/MOVE_DOWN);因为一次输入提交可能触发"删除冲突行 + 编辑当前行"的复合操作,所以用 BeginBulk/EndBulk 合成一条。
file_browser.{h,cpp}API:ShowFileDialog(StorageType, EFileType, pTitle, pButtonText, pInitialPath, pInitialFilename, pfnOpenCallback, pUser),回调签名 bool (*)(const char *pFilename, int StorageType, void *pUser)。EFileType{MAP, IMAGE, SOUND} 决定扩展名过滤器 {.map, .png, .opus}。
导航模型:"文件夹 + 链接"双路径:m_aCurrentFolder(初始目录)与 m_aCurrentLink(伪链接,如 downloadedmaps、themes、各存储根),m_pCurrentPath 指向其一;m_ShowingRoot 表示当前展示存储选择列表("All combined" + 每个存在该目录的存储)。.. 先回初始目录,已在初始目录则回 root。
排序/过滤/预览:按名/按修改时间两个互斥排序方向;过滤框做 str_find_nocase 且尽量保持选中项;预览按选中项惰性加载图片纹理或 LoadOpus 音频(音频复用 CEditor::DoAudioPreview),OnDialogClose 释放、OnEditorClose 暂停。
回调如何进 CEditor:浏览器从不直接改 CEditorMap,全部通过回调。CEditor 里的静态回调:CallbackOpenMap / CallbackAppendMap / CallbackSaveMap / CallbackSaveCopyMap / CallbackAddTileArt / CallbackAddQuadArt / CallbackSaveImage / CallbackSaveSound / CallbackCustomEntities,内部再转调对应的 quick action 或 ReplaceImage/ReplaceSound。这是"同一浏览器服务 map / image / sound / custom-entities 五种场景"的解耦手段。
注意
IsValidSaveFilename() 的语义很绕:只有"当前路径仍是初始目录"或"当前路径是 themes 链接"才返回 true,用于决定 Map()->m_ValidSaveFilename(即能否直接覆盖保存)。这是接手时容易被绊到的地方。
auto_map.{h,cpp}类设计:本仓库没有上游 Teeworlds 的 CMapper/CAutoMapper 组合;只有 CAutomapper : CMapObject,而且一个 CEditorImage 一个实例(存在 CEditorImage 内)。规则文件按图片名定位:editor/automap/%s.rules,所以 ddnet_walls.rules 对应 mapres 图片 ddnet_walls。加载时机:AddImage / ReplaceImage / 地图加载。文件不存在时静默返回。游戏端不运行 automapper,纯编辑期工具。
数据结构:CConfiguration{name, m_vRuns, StartX/Y, EndX/Y} → CRun{m_vIndexRules, m_AutomapCopy} → CIndexRule{m_Id, m_vRules, m_Flag, m_RandomProbability, m_vModuloRules, m_DefaultRule, m_SkipEmpty, m_SkipFull} → CPosRule{m_X, m_Y, m_Value∈{NORULE,INDEX,NOTINDEX}, m_vIndexList, m_IsGuide} → CIndexInfo{m_Id, m_Flag, m_TestFlag},外加 CModuloRule{m_ModX, m_ModY, m_OffsetX, m_OffsetY}。
.rules 文法(按行,# 注释):
| 指令 | 含义 |
|---|---|
[ConfigName] | 开新配置,并隐式创建第一个 run(AutomapCopy=true) |
NewRun | 追加一个 run。run 之间默认会重新从输入层拷一份缓冲,所以 run N 看不到 run N−1 的写入结果——除非 NoLayerCopy |
Index <id> [XFLIP|YFLIP|ROTATE]... | 开新的 index 规则,最多 3 个 flag |
Pos <dx> <dy> EMPTY|FULL|INDEX <ids>|NOTINDEX <ids> | 位置匹配规则;id 列表用 OR 续接;OR 前可带 flag 表示"索引+朝向"精确匹配 |
Random <f> / Random <f>% | 带 % 是概率,否则是 1/f |
Modulo <modx> <mody> <offx> <offy> | 取模规则(0 会被修正为 1) |
NoDefaultRule | 关闭自动补的默认规则 |
NoLayerCopy | 该 run 直接在目标层上就地读写 |
解析后处理:若某 index 规则没有显式的 Pos 0 0 INDEX 0 且仍允许默认规则,会补 Pos 0 0 NOTINDEX 0 并置 SkipEmpty——即"什么都没匹配到时清空"的默认行为。配置包围盒由所有 Pos 规则的 min/max 得出,用于局部重跑。
真实例子(data/editor/automap/fadeout.rules,全文很短):
[Fadeout] Index 6 Index 7 Pos 0 -1 FULL Pos -1 0 EMPTY Pos 0 1 FULL Pos 1 0 FULL Pos 1 -1 FULL Pos 1 1 FULL
含义:配置名 Fadeout;默认规则是"读层为空则写成 index 6";第二条规则在读层 (0,−1)/(0,1)/(1,0)/(1,−1)/(1,1) 都是实心、左侧为空时写 index 7。generic_unhookable.rules 展示了 Random 32、NewRun、Pos 0 0 INDEX 3 OR 5 OR 80 OR 67 这种 OR 列表、以及 NOTINDEX -1 表示"邻居必须是实心"。
运行语义:入口 Proceed(layer, gameLayer, ReferenceId, ConfigId, Seed, SeedOffsetX, SeedOffsetY)。Seed==0 时取 rand();对每个 run:可过滤时缓冲层是游戏层,否则是目标层自身;若 m_AutomapCopy,把缓冲拷进临时层(按 reference 只保留对应索引的游戏 tile)再 AutoMap(目标层, 拷贝层, run)——即"读输入、写目标";否则直接就地覆盖(用于需要看到上一步结果的 pass)。ProceedLocalized 把更新区扩大 3*Start/End 后只回写核心区,并用 RecordStateChange 记录 undo(拖拽绘制时的自动重跑路径)。
相关信息
ProceedLocalized 在同步 game layer 前会检查 x/y 是否越出 game layer 范围并跳过(auto_map.cpp:429、:452),避免"目标层比 game layer 大"时越界崩溃。写这类越界同步代码时要记得:目标图层和 game layer 的尺寸/裁剪区不一定一致。
AutoMap 对每个 tile 顺序遍历该 run 的所有 index 规则,后者可覆盖前者(文件里越后面的规则越优先)。Pos 规则越界时 CheckIndex = -1,因此 EMPTY 会匹配地图外、NOTINDEX -1 表示"邻居必须是实心"。Random 用确定性位置哈希(triple32inc 变体),所以同 seed 同位置结果可复现。
Automapper reference:AUTOMAP_REFERENCE_NAMES = {"Game Layer", "Hookable", "Death", "Unhookable", "Freeze", "Unfreeze", "Deep Freeze", "Deep Unfreeze", "Live Freeze", "Live Unfreeze"} 与 s_aTileIndex 一一对应,并有 static_assert(std::size(AUTOMAP_REFERENCE_NAMES) == std::size(s_aTileIndex)+1) 强制同步——名字表多出的 index 0 "Game Layer" 是隐式约定(筛选用的 tile 索引从 1 开始)。
UI 与持久化:图层属性里的 "Auto Rule"(PROPTYPE_AUTOMAPPER)与 "Reference"(PROPTYPE_AUTOMAPPER_REFERENCE)。CLayerTiles 用 m_AutomapperConfig / m_AutomapperReference / m_Seed / m_AutoAutomapper 保存配置。种子非 0 且开启自动时,每次 FlagModified() 会局部重跑;"Automap" 按钮全层跑一次并记录 CEditorActionTileChanges。持久化用 UUID map item CMapItemAutomapperConfig,只对非物理 tile 层读写。
注意
实现瑕疵:AutoMap 在内层循环里每 tile 调一次 pLayer->Map()->OnModify(),纯属浪费;代码里还带 // TODO: This is a lazy workaround 的注释。
tile_art.cpp / quad_art.cppTile art(CEditorMap::AddTileArt,入口 CEditor::CallbackAddTileArt):
Graphics()->LoadPng 载入;w*h > 10000 触发 POPEVENT_TILE_ART_BIG_IMAGE 询问。a > 0 的颜色,std::set 去重);颜色数决定需要的图片数 ceil(unique/255);若 现有图片数 + 组数 >= 64 触发 POPEVENT_TILE_ART_TOO_MANY_COLORS 并放弃(客户端 64 图上限)。CEditorImage;颜色按 255 个一组切片(索引 0 保留给"空/透明")。注意
没有抖动(no dithering),也没有颜色量化/最近色匹配——只有精确相等。所以抗锯齿/渐变图会直接爆颜色数并几乎必然触发 64 图上限。
撤销用 CEditorActionTileArt(记录 m_PreviousImageCount + 索引重映射),但 Redo 需要重新从磁盘加载 PNG——文件被删/改则 Redo 失败。
Quad art(CQuadArt + CQuadArtParameters):
PopupQuadArt 中暴露 4 个):ImagePixelSize[1,1024]、QuadPixelSize[1,1024]、Optimize、Centralize;默认 1 / 4 / false / true。(w/ImagePixelSize)*(h/ImagePixelSize) > 100000 触发 POPEVENT_QUAD_ART_BIG_IMAGE。ImagePixelSize 为步长扫描,跳过透明与已访问像素;Optimize 时贪心求"同色最大矩形"(先向下扩高、再向右扩宽)并标记已访问;最终 quad 尺寸 = scale * QuadPixelSize,中心 = ((x/ImagePixelSize) + scale.x/2) * QuadPixelSize。四角同色,纹理坐标 (0,0)(1,0)(0,1)(1,1),m_PosEnv = m_ColorEnv = -1,pivot 取中心或 Centralize 时的对角。CEditorActionQuadArt(持有 group 的 shared_ptr,Undo 移出 m_vpGroups、Redo 加回)。FindSuperPixelSize 定义了但无人调用,是死代码。proof_mode.cpp:CProofMode 是 CMapView 的子组件。EProofBorder{OFF, INGAME, MENU};OnMapLoad → InitMenuBackgroundPositions() 先用默认坐标生成,再用游戏层中 TILE_TIME_CHECKPOINT_* 的 tile 覆盖,并建立"坐标重合"碰撞表用于合并显示。RenderScreenSizes() 画 20 档视野网格、4:3 红框/16:9 绿框、玩家出生点蓝圈。
注意
proof mode 只是可视化辅助,不做地图合法性检查。真正的完整性检查在 CheckIntegrity / PerformSanityChecks。容易混淆的"Place border"是另一个独立工具:Tools 菜单 → POPEVENT_PLACE_BORDER_TILES → CEditorMap::PlaceBorderTiles()(在选中 tile 层边缘 2 格写入 index 1,并记 CEditorBrushDrawAction)。
layer_selector.cpp:Ctrl+右键 tile → 反查并选中所在图层(ed_layer_selector)。quad_knife.cpp:CQuadKnife,由 Quad 弹窗的 "Slice" 按钮激活,之后由地图视图每帧驱动 DoSlice()(激活时会屏蔽普通 quad 拖拽)。用两个三角形判定点在 quad 内(先归一化提高精度);吸附优先用网格,否则按 4*MouseWorldScale 吸附到角或边;左键依次记录 4 个点,凑满后若有凹序则交换成凸序,再新建 quad;新 quad 的颜色与纹理坐标由重心坐标在源三角形上双线性插值得到,pivot 取四点均值,最后记 CEditorActionNewQuad。font_typer.cpp:用内置字体把小写字母排成 tile 图案;CState 在 Map();是本项目少数覆盖 OnInput 的组件之一。prompt.cpp:命令面板(Ctrl+P),唯一遍历全部 quick actions 的地方(模糊匹配 label、跳过 Disabled())。explanations.cpp:纯静态查表的 tile 解释,调用点在 map_view.cpp:309-328:gametype 由 m_SelectEntitiesImage 字符串推断,Tile 由鼠标世界坐标换算,结果直接写 m_aTooltip。RACE 与 BLOCKWORLDS 目前直接返回 nullptr(带 TODO)。| 想做什么 | 改哪里 |
|---|---|
| 加工具栏按钮 | 选 DoToolbarLayers(449)/DoToolbarImages(826)/DoToolbarSounds(840),用 DoButton_Editor / DoButton_FontIcon / DoButton_Ex,最后一个参数是 tooltip(写进 m_aTooltip,帧尾统一渲染) |
| 加弹窗菜单 | editor.h 声明 static CUi::EPopupMenuFunctionResult PopupX(void *pContext, CUIRect View, bool Active);,popups.cpp 实现;调用处 static SPopupMenuId s_Id; Ui()->DoPopupMenu(&s_Id, x, y, w, h, this, PopupX);,重复打开前用 IsPopupOpen 守卫 |
| 加 quick action | quick_actions.h 加一条 X-macro + quick_actions.cpp 写回调。自动获得成员、构造初始化、Ctrl+P 面板条目;按钮需要在工具栏/菜单里手动引用 |
| 加可撤销操作 | ①editor_actions.{h,cpp} 写 action 类(继承 CEditorActionLayerBase 或 IEditorAction);②在操作结束处 RecordAction();③支持连续拖拽就加/复用 tracker 发 Begin/End;④Undo/Redo 末尾调用 Map()->OnModify() |
| 加图层类型 | 见 §4.5 的 9 步清单 |
| 加图层属性 | mapitems.h 的属性枚举加一项;该层 RenderProperties 加控件;*PropTracker::PropToValue/OnEnd 加分支;CEditorActionEditLayer*Prop::Apply 加分支。顺序必须是 Begin(改之前)→ 改值 → End(改之后) |
| 加按地图隔离的工具状态 | 加到 CEditorMap(map.h:168-178),用 Map()->... 访问 |
| 加组件 | 写 class CXxx : public CEditorComponent,在 CEditor::Init() 的 m_vComponents.emplace_back(...) 里注册(顺序即输入优先级);要嵌套子组件就 RegisterSubComponent + InitSubComponents(参考 CMapView::OnInit),并注意 OnUpdate/OnMapLoad 不会自动转发 |
| 加"工具"/拖拽模式 | 扩展 CMapView::EActiveOp + CMapView::Render 的拖拽段,或新增组件 |
| 改地图格式 | 必须同时改 editor/mapitems/map_io.cpp 和 src/game/layers.cpp / src/game/mapitems.h,并保留向后兼容分支 |
| 加 automapper 规则 | 在 data/editor/automap/ 加 .rules 并注册进 CMakeLists.txt 的资源列表 |
按"踩到概率 × 代价"排序:
g_Config.m_ClEditor = 0 请求退出,没有 IEditor 关闭接口;OnClose() 只做清理。阅读退出逻辑时容易找错地方。HandleWriterFinishJobs 队头阻塞:只看 front() 且每帧最多一个;某个 job 卡住会阻塞后面所有地图的 "Saving" 状态清除。OnInput 会转发给子组件,OnUpdate/OnMapLoad/OnReset 不会(CMapView 手动补了 OnMapLoad)。新增子组件极易踩坑。CEditor::Reset() 在 Init() 中早于组件注册(editor.cpp:4609 vs 4612),第一次 Reset 对组件是 no-op,随后由 OnInit→OnReset 补上;Load() 无条件先 Reset(),加载失败也会重置 UI 状态。HandleCursorMovement() 只有声明没有定义(editor.h:265),全仓无实现也无调用——死声明。static 的隐式全局 UI 状态(s_ShowServerSettingsEditorLast、g_SelectImageSelected、s_Operation、static SLayerPopupContext 等)与"per-map state"设计形成张力,多标签下需留意。CEditor::Render() 里大量 Input()->KeyPress 检查(缩放/平移、Ctrl+A/N/O/L/S),而 OnRender() 末尾会 Input()->Clear()。当 gfx_refresh_rate < cl_refresh_rate 时,这些只在 Render 中检查的按键可能被吞掉。m_pGameGroup/m_pGameLayer/m_pTeleLayer/... 的维护逻辑散落在 CEditorActionAddLayer/DeleteLayer 的 Undo/Redo 里(editor_actions.cpp:591-754)。新增图层类型时最易漏改,漏了就是悬垂 shared_ptr 或静默错位。CEditorMap::DeleteGroup 本身完全不更新 m_pGameGroup。CheckIntegrity() 只在 CONF_DEBUG 每帧跑;PerformPreSaveSanityChecks 只查图片/声音数据缺失,不查越界索引、不存在的 envelope 引用。static_pointer_cast 无校验:MakeGameLayer/Tele/...(map.cpp:431-459)不做类型检查,类型不符即 UB。m_pTiles 与 m_pTeleTile 等需保持一致,加载时只在 IsValidXxxTile() 通过时同步——写新逻辑时要同时维护两份。CMapItemAutomapperConfig::m_GroupId/m_LayerId 是"组下标 / 组内 layer 下标",不是 layer 的全局下标。TILEFLAG_OPAQUE 的 AnalyseTileFlags() 在保存时才调用(map_io.cpp:130 有 // TODO!);CLayerTiles::Render 的 opaque 分析也是 TODO,CRenderMap 的 opaque 判定恒为 false——即没有做真正的 opaque/transparent 分层优化(tele/switch/tune 只渲染 transparent 那一遍,见 §7.7)。VisitEnvelopeReferences 模板链接陷阱:定义在 .cpp 且无显式实例化,其它 TU 调用会链接失败。CLayerSoundEnvelopeReference::SetEnvelope 的 <= off-by-one(references.cpp:34)。m_aName 长度:编辑器侧 12 字节 ↔ 磁盘 int[3],超长会静默截断。map_io.cpp 与游戏 layers.cpp 是独立实现,改格式时务必两边都改。CEditor::AddImage(editor.cpp:3072 起,m_vpImages.push_back 在 :3116)直接 m_vpImages.push_back + SortImages(),AddSound 同理;popups.cpp:1562,1951(image)、:1671,1956(sound)直接 erase + ModifyImageIndex/ModifySoundIndex。只有 tile art / quad art / append map 三种批量素材引入有 action 类。这是当前最明显的功能缺口。Apply 不调用 Map()->OnModify():EditQuadPoint/EditQuadColor/EditQuadProp/EditQuadPointProp、NewQuad::Undo/Redo、MoveSoundSource::Undo/Redo、EditLayersGroupAndOrder::Undo/Redo、QuadArt::Undo/Redo。经 bulk 触发时靠外层补齐,但直接 Undo/Redo 时不会刷新 modified 标记。这是最一致的不一致。CEditorHistory::Clear() 不清理 bulk 缓冲、也不复位 m_IsBulk(editor_history.cpp:69-73)。若在 BeginBulk()/EndBulk() 之间 Clear(),后续 RecordAction 会被静默吞进一个永不提交的 bulk。CEditorActionEnvelopeAdd::Undo 盲 pop_back,不校验末尾是否真是它加的那一个。CEditorActionAddEnvelopePoint::Undo 按时间值线性查找删除:若存在同时间的点会删错,且是 O(n)。CEditorActionDeleteSoundSource::Redo 无条件 m_SelectedSoundSource--,在 0 时会变 -1,未做钳制。Order 属性没有历史:PopupQuad 用 pLayer->SwapQuads(...) 换序,既无对应动作,tracker 对 EQuadProp::ORDER 也会静默跳过。CSoundSourceOperationTracker 保存 const CSoundSource *(拖动期间容器 reallocate 或元素被删即 use-after-free);CEditorCommandAction 保存 int *m_pSelectedCommandIndex 指向 m_MapSettingsCommandContext(Context 重建即悬空)。CEditorActionEditSoundSourceShape::m_vOriginalValues;CEditorActionEditLayerTilesProp 里的 pSavedLayerTiles 声明后从未使用。DisplayText 固定 256 字节,长地图名会截断。Execute 与构造函数的约定无编译期约束(构造函数不得改地图);且 m_IsBulk 不配对时 EndBulk(0) 静默失败。editor_history.cpp:107 用 Map()->m_vSettings.size() 判定历史项选中有效性——SelectedActionIndex 的值域其实是 [0, redo+undo],与服务器设置条数无关。看起来是复制粘贴遗留(只影响 Delete 快捷键的启用条件)。CEditorHistoryUiState 的 id 成员是 const(editor_history.h:62-64),把"只读零字节"当地址用作 UI id,破坏 const 语义。DoPopupMenu 不去重:弹窗生命周期与打开代码解耦,切地图必须 ClosePopupMenus()。void *pId 用地址:&Map()->m_vpImages[i] 这类容器元素地址在 vector 扩容后失效;(char*)&pIds[i]+1 的指针算术 id 是隐式契约。GetButtonColor 用 1..10 魔数(editor_ui.h:26 的 TODO 正是要补常量)。m_ToolBoxWidth 同时决定 ToolBox 与 ModeBar 宽度,拖 ToolBox 会连带 ModeBar 变宽。m_RenderLayersState 在 CEditor 而非 CEditorMap 上(§2.6 的不对称)。CAutomapper 挂在 CEditorImage 上,规则文件按图片名定位。换图片名就换规则,容易误以为它挂在图层或地图上。AutoMap 每个 tile 都调一次 OnModify()(内层循环里),纯属浪费;过滤逻辑还带 // TODO: This is a lazy workaround。CQuadArt::FindSuperPixelSize 是死代码;CAutomapper 里也有一段被注释掉的 NewIndexInfo2,说明 FULL 的语义历史上摇摆过。static_assert 兜底。CFileBrowser::IsValidSaveFilename() 语义绕:只有"当前路径仍是初始目录"或"themes 链接"才为真,直接影响能否直接覆盖保存。CheckIntegrity/PlaceBorderTiles 混淆。CMapSettingsBackend 是 CEditor 成员(配置表只在启动时加载一次,运行期改配置变量不会刷新下拉框),而 m_MapSettingsCommandContext 在 CEditorMap 上。Render() 里轮询键盘(不覆写 OnInput),且只在 HotItem() 命中时生效——它绕过了组件的事件优先级机制,与其它组件的输入处理耦合较脆弱。SInvalidSetting::SContext 的标志位由对话框直接持有并修改,把校验结果与 UI 状态混在了一起。src/game/client/tooltips.h 的 CTooltips 与 CUi::DoToolTip / CUi::OnRenderTooltip() / CUi::OnReset()(SLabelProperties::m_pTooltipId 用来给被截断的文本挂 tooltip)。CGameClient 用的是这套(OnReset()、OnRenderTooltip()),但编辑器一处都没调用——CEditor::Render() 仍只渲染自己的 m_aTooltip。所以编辑器里因宽度不足被截断的文本(图层名、图片名、设置命令等)不会弹出提示。这是现成的、低风险的接入点。如果你从旧 issue、旧 PR 或旧笔记里翻到下面这些说法,先别急着复现——当前代码里都没有这些问题。列在这里只是为了少走弯路:
ProceedLocalized 在同步 game layer 前会做 x/y 越界检查(auto_map.cpp:429、:452),越界时直接跳过。map_io.cpp 里 UnloadData(m_Switch) / UnloadData(m_Tune) 在 if(pData != nullptr) 之外无条件调用,数据为空时也会释放(:836 与 :853)。CFileBrowser::CPopupConfirmDelete 用 m_StorageType 重新列举,而不是硬编码的 TYPE_SAVE(file_browser.cpp:938、:949)。CheckCollision 在判定为 ADD 时返回 -1,不会把冲突行号当成"要替换的行"(editor_server_settings.cpp:2029)。GetValueRange 在无点或无有效通道时返回 {0, 1}(envelope.cpp:62-67)。render_layer.cpp 里只保留 transparent 那一遍,没有冗余的 BlendNone() + LAYERRENDERFLAG_OPAQUE 调用。CLineInput 输入错乱 / 密码被截断」(影响编辑器所有输入框,包括 server settings 命令框)—— 实际 SetRange 的 mem_move 长度用 m_Len - Begin - RemovedCharSize;OffsetFromDisplayToActual 用 str_utf8_offset_chars_to_bytes;隐藏输入的星号缓冲用 std::string,不会把长密码截断到 128 字符。m_vSelectedEnvelopePoints vs m_SelectedQuadEnvelopePoint),见 §9.1。editor.h:289:ShowFileDialogError 名字与用途不符;弹窗 context 应改成 shared_ptr。editor_ui.h:26:为 GetButtonColor 的其余特殊 Checked 值补常量。map_io.cpp:131:// TODO! 图片分析应在加载时做。map_view.cpp:408:// TODO: do all layers(画笔抓取时只处理首个 layer 的 BrushSelecting)。popups.cpp:493:选择文件后应直接选中该文件而非打开父目录(issue #11334)。auto_map.cpp:486:// TODO: This is a lazy workaround。explanations.cpp:8,10,751,755:补充/改进 tile 解释。quick_actions.h 的 description)| 快捷键 | 功能 |
|---|---|
Ctrl+Shift+E | 打开/关闭编辑器 |
F1 | 打开 DDNet Wiki 地图编辑器页 |
Escape | 退出编辑器 |
Tab | 显示/隐藏 GUI |
F10 | 截图 |
Ctrl+P | 命令面板(可搜索执行所有 quick action) |
Ctrl+S / Ctrl+Shift+S / Ctrl+Shift+Alt+S | 保存 / 另存为 / 保存副本 |
Ctrl+L / Ctrl+Shift+L / Ctrl+Shift+O | 打开地图 / 打开当前游戏内地图 / 加载 |
Ctrl+A / Ctrl+N / Ctrl+F4 | 追加地图 / 新建地图 / 关闭当前地图 |
Ctrl+Z / Ctrl+Y(或 Ctrl+Shift+Z) | 撤销 / 重做(作用于当前活动的编辑器面板) |
Ctrl+Q | 加 quad / 音源 |
Ctrl+Space / 按住 Space | 画笔选择器 |
Ctrl+G | 网格开关 |
Ctrl+I / Ctrl+Shift+I | tile 信息(十进制/十六进制) |
Ctrl+Shift+C | 取色器 |
Numpad +/-/*、Home | 缩放 / 重置缩放 / 聚焦 |
Ctrl+Tab / Ctrl+Shift+Tab | 循环切换地图标签页 |
Alt+拖动 | 画笔轴锁定;Alt 同时忽略网格吸附 |
Shift+拖动(quad) | quad 轴约束;多选;画笔 UV/精度 |
Ctrl+右键 | 反查 tile 所在图层 |
Ctrl+1..0 / 1..0 | 保存画笔到槽 / 从槽载入画笔 |
Ctrl+F(画笔激活时) | 把画笔里的 tile 编号改成下一个空号 |
| 变量 | 默认 | 说明 |
|---|---|---|
cl_editor | 0 | 是否处于编辑器(config_variables.h:343) |
cl_editor_max_history | 50 | 每套历史的 undo 上限(三套不共享配额) |
ed_autosave_interval | 10 | 自动保存间隔(分钟,0=关) |
ed_autosave_max | 10 | 每张地图保留的自动保存数量 |
ed_smooth_zoom_time | 250 | 缩放平滑动画时长(ms) |
ed_limit_max_zoom_level | 1 | 是否限制最大缩放 |
ed_zoom_target | 0 | 是否朝鼠标位置缩放 |
ed_showkeys | 0 | 显示按键 |
ed_align_quads | 1 | quad 对齐/吸附辅助线 |
ed_show_quads_rect | 0 | 显示选中 quad 的包围盒 |
ed_auto_map_reload | 1 | 保存后对本地服务器 hot_reload |
ed_layer_selector | 1 | Ctrl+右键 选层 |
ed_show_ingame_entities | 0 | 在编辑器里按游戏样式显示武器/护盾/雪花/旗帜 |
| 词 | 含义 |
|---|---|
| map | 一个 .map 文件 / 一个 CEditorMap 文档 |
| group | 图层组,决定视差/偏移/裁剪,是渲染与保存的单位 |
| layer | 图层:tile / quad / sound / game / tele / speedup / switch / tune / front |
| game layer | 物理层,决定可钩/死亡/冻结等,是"地图逻辑"的载体 |
| automapper | 根据规则自动从 game layer 生成装饰 tile 的工具 |
| envelope | 随时间变化的曲线,可驱动颜色/位置/音量 |
| quad | 一个可自由变形的四边形贴图(含 5 个点:4 角 + 1 轴心/pivot) |
| brush | 从图层抓取的一块内容,可反复盖印;本质是 CLayerGroup |
| action | 一条可撤销操作(IEditorAction) |
| tracker | 把连续拖拽聚合成一条 action 的辅助对象 |
| bulk | CEditorActionBulk,把多条 action 合并成一步撤销 |
| extra editor | 底部可展开的额外面板:Envelopes / Server settings / History |
| proof mode | 显示默认视角可视范围的边框 |
| tile art / quad art | 把图片转换成 tile / quad 的工具 |
Ctrl+Z 感受行为。DisplayText 如何对应操作。editor.cpp 的 OnUpdate/OnRender/Render(约 500 行)。文档结束。若发现与代码不符之处,请以代码为准并回来更新本文件。
本文作者:Zerol Acqua
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!