CV · Code Conventions
Project-wide style and helper-reuse rules. Read before writing or reviewing code. This page complements the rule list in .claude/skills/send-pr/SKILL.md (which enforces these via /cr and /send-pr). The send-pr rules are general engineering quality; the rules below are the specific stylistic and helper-reuse choices LuckyEngine has already made.
Hazel::* namespaces
Style
Namespace qualification
When already inside namespace Hazel { ... } or namespace Hazel::Sub { ... }, omit the Hazel:: prefix for symbols that resolve via ordinary name lookup.
// Inside namespace Hazel::ProjectTemplate
if (!FileSystem::Exists(path))
return;
// Redundant Hazel:: when already inside Hazel::*
if (!Hazel::FileSystem::Exists(path))
return;
Exceptions (qualifier required for clarity, not lookup):
- Inside a
Hazel::*namespace but referencing a sibling sub-namespace whose unqualified name is ambiguous with a local type. - In a header where the file may be
#included from multiple translation units in different namespaces.
If you find yourself adding Hazel:: and the surrounding code doesn't, drop it. If you find yourself dropping it and the surrounding code uses it, fix the surrounding code in the same change so the file is internally consistent.
if body — no inline single-line ifs
The body of an if always goes on its own line, whether or not braces are used.
// No braces, body on next line
if (oldStem.empty())
return;
// Braces, body on next line
if (oldStem.empty())
{
return;
}
// Body on same line as condition
if (oldStem.empty()) return;
The same rule applies to else, while, for, do, and any other control-flow construct: the body lives on its own line, never trailing the keyword line.
Exception: getter / setter one-liners on a class declaration (e.g., bool IsValid() const { return m_Valid; }) — those are member-function definitions, not control-flow bodies.
Casts — named C++ casts, never C-style
Use static_cast, reinterpret_cast, const_cast, or dynamic_cast. Never (T)x or T(x) (the function-style cast on non-class types). Named casts make intent explicit and let grep find the dangerous ones.
ImGuiEx::ScopedID rowID(
reinterpret_cast<const void*>(
static_cast<uint64_t>(uuid)));
auto* derived = static_cast<DerivedClass*>(base);
// Hides the kind of conversion happening
ImGuiEx::ScopedID rowID(
(const void*)(uint64_t)uuid);
auto* derived = (DerivedClass*)base;
If a value needs both a numeric narrowing / widening and a pointer reinterpretation (e.g., UUID → uint64_t → void*), spell both casts out — one static_cast for the integer conversion, one reinterpret_cast for the pointer reinterpretation. Don't fold them into a single C-style cast.
Exception: aggregate-initialisation-style T{x} for constructing a value of class type T is not a cast — it's construction, and remains the right tool.
If you find an existing C-style cast in code you're already editing, replace it. Don't open a separate cleanup PR — that's the kind of drive-by send-pr § 1 prohibits.
Helper reuse — where to look before reinventing
Before writing a utility function, check if one already exists. The canonical helper namespaces below cover most cross-cutting needs. Read the header of the relevant module and grep for similar names before adding new code.
File and directory operations — Hazel::FileSystem
Header: Hazel/src/Hazel/Utilities/FileSystem.h
| Category | Functions |
|---|---|
| Path queries | Exists, IsDirectory, IsNewer, GetUniqueFileName, GetLastWriteTime |
| Mutations | CreateDirectory, DeleteFile, MoveFile, CopyFile, Move, Copy, Rename, RenameFilename, WriteBytes |
| Reads | ReadBytes, TryOpenFile, TryOpenFileAndWait |
| Archives | ExtractZip, CreateZip, DownloadToFile |
| Shell / OS | RunCommandCapture, ShowFileInExplorer, OpenDirectoryInExplorer, OpenExternally, OpenFileDialog, OpenFolderDialog, GetWorkingDirectory, GetTempStoragePath |
If you're about to call std::filesystem::* directly, first check whether a matching Hazel::FileSystem wrapper exists.
String utilities — Hazel::Utils::String
Header: Hazel/src/Hazel/Utilities/StringUtils.h
- Validation:
IsValidProjectName,IsValidIdentifier, etc. - Sanitization:
ToValidCSharpNamespace. - Plus the usual case / split / trim helpers — check the header.
ImGui — ImGuiEx
Header: Hazel/src/Hazel/ImGui/ImGuiEx.h
- RAII style / colour scopes:
ScopedStyle,ScopedColour,ScopedFont,ScopedDisable,ScopedID. Use these instead of pairedPush/Popcalls. - Widgets and helpers:
SetTooltip(with hover-delay),FitPath,DrawSpinner,DrawMaskedProgressBar,GetTextureID,Fonts::Push/Pop.
ImGuiEx::ScopedColour col(
ImGuiCol_Text, Colors::Theme::TextDim);
ImGuiEx::SetTooltip("explanation");
ImGui::PushStyleColor(ImGuiCol_Text,
IM_COL32(170, 176, 194, 255));
// ...
ImGui::PopStyleColor();
ImGui::SetTooltip("explanation");
If you're about to call ImGui::PushStyleVar / PopStyleVar or ImGui::SetTooltip directly, check whether ImGuiEx already has a wrapper.
Colour constants — Colors::Theme / Colors::SimpleUX
Header: Hazel/src/Hazel/ImGui/Colors.h
Never hardcode IM_COL32(...) or ImVec4(...) literals for theme colours. Use named constants from Colors::Theme::* (general engine UI) or Colors::SimpleUX::* (Welcome / SimpleUX surfaces).
Project / asset access — Project, AssetManager
Project::GetActive,Project::GetProjectDirectory,Project::GetEditorAssetManager,Project::GetActiveAssetDirectory— the entry points to the active project's state. Don't read.hprojfiles directly when these accessors will do.AssetManager::*— the static facade for assets from outside the asset module. Don't reach forEditorAssetManagerorRuntimeAssetManagerdirectly from non-asset code.
Script project regeneration — ScriptBuilder
Hazel/src/Hazel/Script/ScriptBuilder.h. Use ScriptBuilder::RegenerateProjectScriptSolution to re-run premake; don't shell out manually. Use ScriptBuilder::BuildScriptAssembly for the build step.
MuJoCo conversions
For axis / quaternion conversions between Hazel (Y-up, x,y,z,w) and MuJoCo (Z-up, w,x,y,z):
- Use
MujocoSceneAsset::HzToMj/MjToHzfor direct vec3 / quat conversion. - Or
MujocoSceneInstance::GetAxisHzToMj/GetAxisMjToHzfor the rotation quaternions. - Methods on these classes are suffixed
Hz(returns Hazel space) orMj(returns MuJoCo space). Never swizzle axes ad-hoc.
For recorder helpers, see Hazel/src/Hazel/Data/Recorders/MujocoRecorderUtils.h.
Logging — HZ_CORE_*_TAG
HZ_CORE_INFO_TAG / WARN_TAG / ERROR_TAG / TRACE_TAG / DEBUG_TAG — always tag log lines with the system name (e.g. "ContentVault", "Project", "ScriptBuilder") so the editor's tag filter can isolate them.
HZ_CORE_ASSERTfor debug invariants.HZ_CORE_VERIFYfor invariants that must hold in distribution builds.
Console messages — HZ_CONSOLE_LOG_*
For messages the editor user should see in the in-app console (not just the log file), use HZ_CONSOLE_LOG_INFO / WARN / ERROR.
Dos and don'ts summary
| Topic | Do | Don't |
|---|---|---|
| Namespace | Drop Hazel:: when inside Hazel::* | Mix qualified and unqualified within a file |
if body | Body on its own line, with or without braces | if (cond) return; trailing |
| Casts | static_cast / reinterpret_cast / const_cast / dynamic_cast | C-style (T)x, function-style T(x) on non-class types |
| Files | Hazel::FileSystem::* | std::filesystem::* directly when a wrapper exists |
| ImGui state | ImGuiEx::Scoped* RAII | Paired Push / Pop by hand |
| Colours | Colors::Theme::* / Colors::SimpleUX::* | Hardcoded IM_COL32(...) / ImVec4(...) |
| Asset access | AssetManager::* / Project::Get* | Reaching into EditorAssetManager / RuntimeAssetManager from outside the asset module |
| Script build | ScriptBuilder::* | Manual premake5 / msbuild shell-outs |
| MuJoCo axes | HzToMj / MjToHz + suffixed methods | Ad-hoc swizzles |
| Logging | HZ_CORE_*_TAG("System", ...) | Untagged HZ_CORE_INFO in shipped code |
| Asserts | HZ_CORE_ASSERT (debug), HZ_CORE_VERIFY (dist) | Bare assert() / silent if-return on invariant breach |
Adding to this doc
When you encounter:
- A helper that exists but isn't listed here → add it.
- A convention that's enforced de-facto by review but not written down → add it.
- A convention that's contradicted between two places in the codebase → add the resolution here and bring the codebase into line.
Keep entries terse. Link to the canonical header rather than enumerating every function — function lists drift; headers don't.
Related
- Cross-Cutting — smart-pointer policy, error handling, the rest of the engine-wide rule set.
- Threading — concurrency rules and worker patterns.
- Recording Integrity — rules that override or extend these in recording-critical paths.
.claude/docs/Conventions.md— canonical source..claude/skills/send-pr/SKILL.md— the general engineering rule list that pairs with this one.