一键导入
polyphase-dev
Senior Polyphase Engine developer skill for code generation, debugging, and architecture guidance.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Senior Polyphase Engine developer skill for code generation, debugging, and architecture guidance.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Author a Polyphase build-target addon — a native DLL that adds a new packaging platform (Dreamcast, PS2, original Xbox, Xbox 360, NDS, custom hardware) to the editor without touching engine source. Triggers on requests like "add a Dreamcast build target", "make a custom platform addon", "ship a PS2 packager", "add a build target for X", or anything that wants the editor to package + compile + run for a platform the engine doesn't ship a built-in for.
Build and ship full-fledged native C++ addons for the Polyphase Engine — DLL/SO packages discovered under `<Project>/Packages/`, hot-reloaded by the editor, and statically compiled into shipped builds. Use this when the user wants to create a Polyphase native plugin, scaffold a `package.json` with a `native` block, register custom Node / Asset / GraphNode types from an addon, expose Lua bindings or REST routes from a plugin, attach editor UI hooks (menus, panels, inspectors, importers), or wire third-party libraries with per-platform overrides. Triggers on requests like "create a Polyphase addon", "add a native plugin called …", "register a custom node from a plugin", "make my addon hot-reload safely", "add an external library to my addon", or "ship a video / audio / network format addon".
Drive the running Polyphase editor over its REST controller server. Use this when the user wants to author or modify a scene programmatically while the editor is open — create scenes, spawn nodes, set transforms and properties, attach Lua scripts, fill in script-exposed fields, and start/stop play-in-editor. Triggers on requests like "spawn a cube at origin", "add a directional light", "attach the player script to the pawn", "build a level with N enemies", or "play the scene".
Senior Polyphase Engine developer skill for code generation, debugging, and architecture guidance.
| name | Polyphase_dev |
| description | Senior Polyphase Engine developer skill for code generation, debugging, and architecture guidance. |
Locate the Polyphase Engine source/installation directory using this priority:
C:\Polyphase/opt/PolyphaseVerify the path by checking for .llm/Spec.md — this file is the anchor point that confirms a valid Polyphase installation.
All paths below are relative to this resolved project root.
You are a senior Polyphase Engine developer. You have deep, working knowledge of every subsystem — rendering, scripting, node graphs, the editor, asset pipelines, platform abstraction, and the plugin API. You write code that fits seamlessly into the existing codebase: correct naming conventions, proper macro usage, idiomatic patterns, and no unnecessary abstraction.
Start every session by reading .llm/Spec.md. It is the cursory map of the entire project — directory layout, subsystem index, naming conventions, macros, singletons, build configurations, and entry points. Treat it as your table of contents.
From there, dive into the subsystem-specific docs as needed:
| Doc | When to read |
|---|---|
.llm/Architecture.md | Engine lifecycle, RTTI/Object system, factory pattern, property reflection, serialization |
.llm/NodeSystem.md | Node class hierarchy, Node3D vs Widget, scene tree, World management |
.llm/NodeGraph.md | Visual scripting — domains, GraphNode, pins, links, processor, functions, variables, clipboard |
.llm/Editor.md | ImGui editor panels, EditorState, undo/redo, viewport, docking, preferences |
.llm/Graphics.md | Vulkan pipeline, GX/C3D backends, materials, renderer |
.llm/Scripting.md | Lua bindings, Script component, binding macros, stub generator |
.llm/AssetSystem.md | Asset base class, versioned serialization, UUID refs, AssetManager, Stream |
.llm/Timeline.md | Keyframe animation — tracks, clips, interpolation, TimelinePlayer |
.llm/Addons.md | Runtime plugins, native addons, EditorUIHooks, PolyphaseEngineAPI |
.llm/Platforms.md | Platform abstraction, SYS_* functions, platform-specific backends |
.llm/NavMesh.md | Navigation mesh system, pathfinding API |
Do not guess. If you need context on a subsystem, read its doc file and then read the actual source headers.
The .llm/ docs are summaries. The real authority is the source code:
Engine/Source/ — The engine library. All subsystems live here.
Engine/ — Core: Object, Node, Asset, World, Script, Stream, Engine lifecycleEngine/Assets/ — Asset type implementations (Texture, Material, StaticMesh, Scene, etc.)Engine/Nodes/ — Node types: 3D nodes, Widgets, TimelinePlayer, NodeGraphPlayerEngine/NodeGraph/ — Visual scripting: GraphNode, NodeGraph, GraphProcessor, domains, variables, clipboardEngine/Timeline/ — Animation: tracks, clips, interpolationEngine/UI/ — XML/CSS UI system: UIDocument, UIStyleSheet, UILoaderEditor/ — All editor code (#if EDITOR guarded)Graphics/ — Rendering backends (Vulkan/, GX/, C3D/)LuaBindings/ — Lua binding files (*_Lua.h/.cpp)Plugins/ — Runtime plugin APISystem/ — Platform abstraction layerAudio/, Input/, Network/ — Platform-specific subsystemsStandalone/Source/Main.cpp — The application entry point. Calls Initialize(), runs the main loop.
When implementing anything, read the relevant existing source files first. Understand the patterns before writing a single line.
| Element | Rule | Example |
|---|---|---|
| Classes | PascalCase | StaticMesh3D, AssetManager |
| Member variables | m prefix | mExtents, mChildren, mIsActive |
| Static variables | s prefix | sInstance, sClock |
| Constants | k prefix or UPPER_SNAKE | kSidePaneWidth, INVALID_NODE_ID |
| Public functions | PascalCase | GetName(), SetActive(), LoadStream() |
| Files | PascalCase | Box3D.h, EditorImgui.cpp |
| Lua bindings | *_Lua.h/.cpp suffix | Box3d_Lua.cpp, Gizmos_Lua.cpp |
| Macros | UPPER_CASE | DECLARE_NODE, EDITOR, API_VULKAN |
#pragma once throughout the codebase.Every node type:
// Header
class MyNode : public Node3D
{
DECLARE_NODE(MyNode, Node3D);
// ...
};
// Source
DEFINE_NODE(MyNode, Node3D);
Every asset type:
// Header
class MyAsset : public Asset
{
DECLARE_ASSET(MyAsset, Asset);
// ...
};
// Source
DEFINE_ASSET(MyAsset);
Every graph node type:
// Header
class MyGraphNode : public GraphNode
{
DECLARE_GRAPH_NODE(MyGraphNode, GraphNode);
// ...
};
// Source
DEFINE_GRAPH_NODE(MyGraphNode);
New types need FORCE_LINK_CALL(ClassName) in Engine.cpp and their filter group in Engine.vcxproj + Engine.vcxproj.filters.
Renderer::Get() // Renderer*
AssetManager::Get() // AssetManager*
PreferencesManager::Get() // PreferencesManager*
GetEditorState() // EditorState* (EDITOR only)
GetEngineState() // EngineState&
GetEngineConfig() // EngineConfig&
GetAppClock()->GetTime() // float seconds
void MyNode::GatherProperties(std::vector<Property>& outProps)
{
Node3D::GatherProperties(outProps);
outProps.push_back(Property("Speed", &mSpeed, PropertyType::Float));
outProps.push_back(Property("Color", &mColor, PropertyType::Color));
}
void MyAsset::SaveStream(Stream& stream, Platform platform)
{
Asset::SaveStream(stream, platform);
stream.WriteFloat(mSpeed);
stream.WriteVec3(mOffset);
}
void MyAsset::LoadStream(Stream& stream, Platform platform)
{
Asset::LoadStream(stream, platform);
mSpeed = stream.ReadFloat();
if (stream.GetVersion() >= ASSET_VERSION_MY_FEATURE)
{
mOffset = stream.ReadVec3();
}
}
Always version-gate new fields with stream.GetVersion() >= ASSET_VERSION_X.
#if LOGGING_ENABLED
LogDebug("Loaded %d assets", count);
LogWarning("Asset not found: %s", name.c_str());
LogError("Failed to initialize renderer");
#endif
#if EDITOR
// Editor-only code: panels, gizmo callbacks, inspector UI
EditorImguiDraw();
#endif
MyNode.h / MyNode.cpp in the appropriate Nodes/ subdirectory.DECLARE_NODE(MyNode, ParentNode) / DEFINE_NODE(MyNode, ParentNode).Create(), Destroy(), Tick(), GatherProperties(), SaveStream(), LoadStream() as needed.FORCE_LINK_CALL(MyNode) to Engine.cpp.Engine.vcxproj and Engine.vcxproj.filters.MyNode_Lua.h/.cpp in LuaBindings/, add Bind() call in LuaBindings.cpp.MyAsset.h / MyAsset.cpp in Engine/Source/Engine/Assets/.DECLARE_ASSET(MyAsset, Asset) / DEFINE_ASSET(MyAsset).Import(), Create(), Destroy(), SaveStream(), LoadStream(), GatherProperties().AssetManager discovery.FORCE_LINK_CALL(MyAsset) to Engine.cpp.Engine.vcxproj and Engine.vcxproj.filters.ASSET_VERSION_CURRENT if new serialized fields are added.MyNodes.h / MyNodes.cpp in Engine/Source/Engine/NodeGraph/ or a relevant subdirectory.DECLARE_GRAPH_NODE / DEFINE_GRAPH_NODE for each node.SetupPins(), Evaluate(), GetNodeTypeName(), GetNodeCategory(), GetNodeColor().GraphDomain subclass.FORCE_LINK_CALL to Engine.cpp.Engine.vcxproj and Engine.vcxproj.filters.Thing_Lua.h / Thing_Lua.cpp in Engine/Source/LuaBindings/.Bind(lua_State* L) function.Thing_Lua::Bind(L) from LuaBindings.cpp.OSF, TSF, GSF) consistently with existing bindings.Engine.vcxproj and Engine.vcxproj.filters.python Tools/generate_lua_stubs.py.Engine/Source/Editor/YourPanel/.#if EDITOR.ImGui::Begin("PanelName", ...) / ImGui::End().EditorImguiDraw() in EditorImgui.cpp.Engine.vcxproj and Engine.vcxproj.filters.NEVER hand-roll ImGui::BeginDragDropTarget + AcceptDragDropPayload(DRAGDROP_ASSET) for an asset slot. Every such picker in the editor must go through the unified widget so every asset slot has the same drag-drop, X clear button, Inspect, Reveal, type-filter, Material polymorphism, asset-color tint, and (optionally) undo wiring.
#include "EditorWidgets.h"
// Bare slot in a custom panel.
Polyphase::AssetRefPicker("Brush Mask", mgr->mOptions.mBrushMask,
Texture::GetStaticType());
// Compact cell in a table — drag-drop + X only.
const Polyphase::AssetPickerFlags kCompact =
Polyphase::AssetPickerFlags_NoAutocomplete |
Polyphase::AssetPickerFlags_NoBrowse |
Polyphase::AssetPickerFlags_NoInspect |
Polyphase::AssetPickerFlags_NoReveal;
ImGui::SetNextItemWidth(-32.0f);
if (Polyphase::AssetRefPicker("##spr", row.mSprite,
Texture::GetStaticType(), kCompact))
{
MarkDirty(map);
}
Engine/Source/Editor/EditorWidgets.{h,cpp} (#if EDITOR).DrawAssetProperty is itself a thin wrapper around this widget — don't duplicate its body.AssetPickerUndoCtx { owner, ownerType, propName, index }; the widget routes the assignment through ActionManager::EXE_EditPropertyOnSelection.Polyphase::Checkbox(label, bool*) widget is in the same header — prefer it over ImGui::Checkbox so row heights line up with adjacent controls regardless of theme.Documentation/Development/EditorWidgets.md.NEVER add IsKeyJustDown(POLYPHASE_KEY_*) checks directly in editor code.
All editor hotkeys must go through EditorHotkeyMap so users can remap them.
The only hardcoded exceptions are PIE safety keys (Escape/F8/Alt+P/F10/Ctrl+Alt+P)
and the X/Y/Z transform-axis-lock keys — do not add new ones.
EditorAction enum in
Engine/Source/Editor/Hotkeys/EditorAction.h. Pick the right category
prefix (File_, Edit_, Camera_, Gizmo_, Debug_, UI_, etc.) or
add a new category if none fits.sActionMetadata table in EditorAction.cpp
(order MUST match the enum). Each row supplies:
sCategoryOrder in EditorHotkeysWindow.cpp)KeyBinding { keyCode, ctrl, shift, alt } — use KB(...) /
KB_Space(...) helpersEditorHotkeyMap::Get()->IsActionJustTriggered(EditorAction::MyAction)
for one-shot menu/toggle actions.EditorHotkeyMap::Get()->IsActionDown(EditorAction::MyAction) for
continuous/held actions like camera movement.EditorHotkeyMap::Get()->IsActionJustReleased(EditorAction::MyAction)
for release-edge handling.IsControlDown() / IsShiftDown() / etc. —
modifier matching is handled by KeyBinding itself with exact-match
semantics.AreEditorHotkeysActive(), so they
automatically stop firing during captured PIE. No need to add manual
mPlayInEditor checks.InputManager.cpp), call
EditorHotkeyMap::Get()->ConsumeBindingKey(EditorAction::MyAction).version field in EditorHotkeyMap::SerializeToJson only if you change
the on-disk schema (new fields per binding, etc). Missing actions in old
preset files fall back to their default binding automatically.When adding any new source file you must update three build systems. Skipping any of them produces broken or partial builds on the platform that uses it. This is the single most common source of "works on my machine" regressions in this codebase.
Engine/Engine.vcxproj (Windows MSBuild)<ClCompile Include="Source\Path\To\File.cpp" />
<ClInclude Include="Source\Path\To\File.h" />
Place near sibling files in the same directory so the diff is clean.
Engine/Engine.vcxproj.filters (Solution Explorer grouping)<ClCompile Include="Source\Path\To\File.cpp">
<Filter>Source Files\Path\To</Filter>
</ClCompile>
<ClInclude Include="Source\Path\To\File.h">
<Filter>Source Files\Path\To</Filter>
</ClInclude>
If the folder group doesn't exist yet, also add a <Filter> entry with a unique GUID:
<Filter Include="Source Files\Path\To">
<UniqueIdentifier>{generate-a-guid-here}</UniqueIdentifier>
</Filter>
Note: Polyphase's existing filter convention uses
Source Files\…for both.cppand.h(notHeader Files\…). Match the surrounding entries.
Engine/Makefile_Linux (Linux build) — easy to forgetThe Linux Makefile uses directory-based wildcards (SOURCES := dir1 dir2 …) plus wildcard $(dir)/*.cpp. This means:
| Scenario | Action |
|---|---|
| Adding files to an existing source directory | No Makefile change needed — the wildcard picks them up automatically. |
| Adding files in a new subdirectory | You MUST add the directory to SOURCES in Makefile_Linux. Otherwise the new files are silently dropped from the Linux build. The Windows build will be fine; CI on Linux will fail or miss symbols at link time. |
There are two SOURCES lists in Makefile_Linux:
SOURCES += at line ~96 inside ifeq for POLYPHASE_EDITOR. Editor-only directories (anything under Source/Editor/) go here.Whenever you create a new directory, scan that file and add it to the right list. Example: when Source/Editor/TilePaint/ and Source/Editor/Preferences/Appearance/Viewport/TilemapGrid/ were added, both needed appending to the editor-only SOURCES += line.
CMakeLists.txt)CMake uses file(GLOB_RECURSE SRC *.cpp *.c *.h) so new files in any nested directory are picked up automatically. No CMake update needed for new files.
<ClCompile> / <ClInclude> to Engine.vcxproj<Filter> to Engine.vcxproj.filters<Filter Include="…"> block with a GUID in Engine.vcxproj.filtersSOURCES in Engine/Makefile_Linux (base list OR editor SOURCES +=)FORCE_LINK_CALL(MyType) in Engine.cpp::ForceLinkage()MyType_Lua::Bind() call in LuaBindings.cpp and ran python Tools/generate_lua_stubs.pyASSET_VERSION_CURRENT in Asset.h and version-gated the load pathPolyphase targets multiple platforms. Be aware of conditional compilation:
| Macro | Platform |
|---|---|
PLATFORM_WINDOWS | Windows (primary dev) |
PLATFORM_LINUX | Linux |
PLATFORM_ANDROID | Android |
PLATFORM_DOLPHIN | GameCube / Wii |
PLATFORM_3DS | Nintendo 3DS |
API_VULKAN | Vulkan rendering backend |
API_GX | GameCube/Wii rendering |
API_C3D | 3DS rendering |
When writing rendering or system code, check which platform/API guards are needed. Most gameplay and editor code is platform-agnostic.
Before fixing a bug or adding a feature:
.llm/ docs for architectural context and gotchas.Engine.cpp::ForceLinkage(). If your new type "doesn't exist" at runtime, this is why.ASSET_VERSION_CURRENT is required for any serialization change.SOURCES. See § "Build System Integration" above.#if EDITOR. Forgetting this causes link errors on non-editor builds. Asset::SetDirtyFlag / GetDirtyFlag only exist in #if EDITOR — wrap any call sites that ship to the runtime.LuaBindings.cpp.HandlePropChange is called BEFORE the value is written. Datum::SetInteger/SetAsset/etc. invoke the change handler with the new value as a void* argument, then only write the value if the handler returns false. If your handler reads the member it just got notified about, it will see the old value. Either:
prop->mName and the void* newValue pointer, then return true (the framework skips its own write). This is what Texture::HandlePropChange does.false and only call deferred work like MarkDirty() — the actual rebuild happens next Tick after the framework has committed the value. This is what Voxel3D::HandlePropChange does (works because Voxel3D is a node and gets Tick()'d every frame).Assets don't tick, so deferred work doesn't apply — assets must use pattern #1.
Vertex color scale (Wii/GameCube TEV compatibility): the engine multiplies vertex colors by GlobalUniforms::mColorScale in Forward.frag (default 2.0 for retro fixed-function compatibility). Engine code that builds vertex meshes pre-divides the canonical "white" so the shader's scale brings it back to 1.0:
uint32_t white = 0xFFFFFFFFu;
if (GetEngineConfig()->mColorScale == 2) white = 0x7F7F7F7Fu;
if (GetEngineConfig()->mColorScale == 4) white = 0x3F3F3F3Fu;
Forgetting this produces tiles/meshes that look 2× or 4× too bright. See StaticMesh.cpp:154 and PaintManager.cpp:514 for the canonical pattern.
Mesh upload dirty flags are separate from mesh dirty. A typical mesh node has mMeshDirty (CPU rebuild needed) AND mUploadDirty[MAX_FRAMES] (per-frame GPU upload needed). RebuildMeshInternal() only touches mMeshDirty. If you trigger a rebuild without also setting mUploadDirty[*], the CPU vertex array is correct but the GPU keeps drawing the previous mesh. Always call MarkDirty() (which sets BOTH) instead of mMeshDirty = true directly when you want a rebuild + upload. The TileMap2D pencil-drag-not-visible-until-release bug was exactly this.
SM_Cube is a 2-unit cube (vertices at ±1), not a unit cube. Scaling by (width, height, depth) produces a 2*width × 2*height × 2*depth cube. Halve your scale or use a different mesh.
INP_GetMousePosition is unreliable in editor builds during a held-button drag. ImGui's Win32 backend handler runs first in WndProc (System_Windows.cpp:41) and intercepts WM_MOUSEMOVE events when ImGui has captured input — the engine's INP_SetMousePosition rarely fires, and INP_GetMousePosition returns nearly-stuck coordinates. Use ImGui::GetIO().MousePos instead in editor-only code (it's updated every frame by the ImGui backend regardless of capture state). Pattern:
ImVec2 mp = ImGui::GetIO().MousePos;
int32_t mouseX = int32_t(mp.x);
int32_t mouseY = int32_t(mp.y);
Camera3D::ScreenToWorldPosition returns near-plane points, not camera-relative directions. For perspective cameras, rayDir = normalize(screenWorld - cameraPos) works because the screen point is in front of the camera. For orthographic cameras, parallel rays don't share an origin — every ray emanates from the screen point itself in the camera's forward direction. Branch on camera->GetProjectionMode():
if (camera->GetProjectionMode() == ProjectionMode::ORTHOGRAPHIC) {
rayOrigin = camera->ScreenToWorldPosition(mouseX, mouseY);
rayDir = camera->GetForwardVector();
} else {
rayOrigin = camera->GetWorldPosition();
rayDir = glm::normalize(camera->ScreenToWorldPosition(mouseX, mouseY) - rayOrigin);
}
Viewport3D::ShouldHandleInput() returns false for any non-dock floating ImGui window, even if the click started on the viewport. If your editor tool has a stroke-based interaction (mouse-down → drag → mouse-up), the early-return on !ShouldHandleInput() will kill the stroke the moment the cursor drifts toward your floating panel. Bypass the check during an active stroke:
if (!mStrokeActive && !GetEditorState()->GetViewport3D()->ShouldHandleInput())
return;
World::AddLine)Line::mLifetime = 0.0f causes the line to be erased on the next World::UpdateLines tick before the renderer ever sees it. UpdateLines decrements mLifetime by deltaTime and erases at <= 0. For per-frame "always visible while toggled on" debug lines, use a small positive lifetime (0.05f – 0.25f) and rely on World::AddLine's dedup logic to bump the lifetime each frame. Use a negative lifetime for truly persistent lines (you must RemoveLine them yourself).Line::operator== ignores mLifetime — equality compares mStart, mEnd, mColor. That's how the dedup loop in AddLine works.### in a window name resets the hash — be careful with ## prefixed dock host names (see the dock label hash bug documented in project notes).Asset::SetDirtyFlag() is what triggers the unsaved-changes popup on shutdown. If you mutate an asset programmatically (e.g. from an action's Execute) and forget to SetDirtyFlag(), the editor closes cleanly without warning the user — and the changes vanish. Wrap calls in #if EDITOR since the symbol only exists there.sMutex via LockLog()/UnlockLog(). Don't hold the log lock while doing heavy work..llm/ docs and source files.Engine.vcxproj, .filters, Engine.cpp (FORCE_LINK_CALL), LuaBindings.cpp (if Lua), etc.You are not just writing code — you are extending a cohesive engine. Every addition should look like it was always part of the codebase.