Lucky Robots Blog Open Roles

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.

Doc source: .claude/docs/Conventions.md Enforced by: /cr, /send-pr Helpers ship as Hazel::* namespaces
Before writing a new utility, look here Hazel::FileSystem paths, archives, OS Hazel::Utils::String validate, case, trim ImGuiEx RAII scopes, widgets Colors::Theme / SimpleUX named colour constants Project / AssetManager active-project entry points ScriptBuilder premake, build Mujoco Hz ↔ Mj never swizzle ad-hoc HZ_CORE_*_TAG tag, assert, verify
Canonical helper namespaces. If you're about to roll a new one, grep here first.

Style

Namespace qualification

When already inside namespace Hazel { ... } or namespace Hazel::Sub { ... }, omit the Hazel:: prefix for symbols that resolve via ordinary name lookup.

Good
// Inside namespace Hazel::ProjectTemplate
if (!FileSystem::Exists(path))
    return;
Bad
// 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.

Good
// No braces, body on next line
if (oldStem.empty())
    return;

// Braces, body on next line
if (oldStem.empty())
{
    return;
}
Bad
// 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.

Good
ImGuiEx::ScopedID rowID(
    reinterpret_cast<const void*>(
        static_cast<uint64_t>(uuid)));

auto* derived = static_cast<DerivedClass*>(base);
Bad
// 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_tvoid*), 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

CategoryFunctions
Path queriesExists, IsDirectory, IsNewer, GetUniqueFileName, GetLastWriteTime
MutationsCreateDirectory, DeleteFile, MoveFile, CopyFile, Move, Copy, Rename, RenameFilename, WriteBytes
ReadsReadBytes, TryOpenFile, TryOpenFileAndWait
ArchivesExtractZip, CreateZip, DownloadToFile
Shell / OSRunCommandCapture, 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 paired Push / Pop calls.
  • Widgets and helpers: SetTooltip (with hover-delay), FitPath, DrawSpinner, DrawMaskedProgressBar, GetTextureID, Fonts::Push / Pop.
Good
ImGuiEx::ScopedColour col(
    ImGuiCol_Text, Colors::Theme::TextDim);
ImGuiEx::SetTooltip("explanation");
Bad
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 .hproj files directly when these accessors will do.
  • AssetManager::* — the static facade for assets from outside the asset module. Don't reach for EditorAssetManager or RuntimeAssetManager directly 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 / MjToHz for direct vec3 / quat conversion.
  • Or MujocoSceneInstance::GetAxisHzToMj / GetAxisMjToHz for the rotation quaternions.
  • Methods on these classes are suffixed Hz (returns Hazel space) or Mj (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_ASSERT for debug invariants.
  • HZ_CORE_VERIFY for 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

TopicDoDon't
NamespaceDrop Hazel:: when inside Hazel::*Mix qualified and unqualified within a file
if bodyBody on its own line, with or without bracesif (cond) return; trailing
Castsstatic_cast / reinterpret_cast / const_cast / dynamic_castC-style (T)x, function-style T(x) on non-class types
FilesHazel::FileSystem::*std::filesystem::* directly when a wrapper exists
ImGui stateImGuiEx::Scoped* RAIIPaired Push / Pop by hand
ColoursColors::Theme::* / Colors::SimpleUX::*Hardcoded IM_COL32(...) / ImVec4(...)
Asset accessAssetManager::* / Project::Get*Reaching into EditorAssetManager / RuntimeAssetManager from outside the asset module
Script buildScriptBuilder::*Manual premake5 / msbuild shell-outs
MuJoCo axesHzToMj / MjToHz + suffixed methodsAd-hoc swizzles
LoggingHZ_CORE_*_TAG("System", ...)Untagged HZ_CORE_INFO in shipped code
AssertsHZ_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.

  • 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.