Always-on band above the prompt showing the 5-hour rate-limit usage, limit and reset countdown

A C++20 game engine built on top of NVRHI with Vulkan as the primary graphics back-end.
msdf-atlas-genScriptableEntity behaviours and a SceneManager for multi-scene games; supports both 2D and 3D entities (ECS backend kept internal)RigidBody2D / BoxCollider2D / CircleCollider2D components, gravity, forces/impulses; physics backend kept internal)Physics3D, usable standalone or wired into the ECS (Transform3D / MeshRenderer / RigidBody3D / Box+SphereCollider3D components), with 3D meshes drawn through Renderer3D and a perspective camera (physics backend kept internal)Renderer3D with coloured directional, point and spot lights (up to 32 point/spot lights per scene, the most relevant picked each frame), ambient light, Blinn-Phong specular, and lit materials with emissive and an albedo texture. Lights are ECS components (PointLightComponent / SpotLightComponent / AmbientLightComponent / DirectionalLightComponent), and the lit shader hot-reloads in Debug builds when asset hot-reload is enabledEntity::SetParent), world values computed per pass, subtree destroy/duplicate, and physics that follows parentsScene::GetLightVisibility)Soft curve that leaves everything below 0.8 untouched, ACES, Khronos Neutral), bloom and ambient occlusion, per camera through PostProcessComponent--profileAnimator with cross-fades, Blend1D blends, masked layers and one-shots; clip events (code or a .events file beside the model) delivered to scripts; joint sockets; retargeting by joint name; models and their events hot-reload in place; an F7 Animation tabUsage guides for building games with the engine live in docs/:
.lib + headers, and a minimal appPhysics3D, standalone or ECS-integrated, including mesh collidersAssetManager, UUID handles, async loading, and hot-reloadCurrently at v0.8.3, which closes every open bug issue, from animation event ranges to Vulkan frame ordering, and adds what Headstone asked for: text that turns and keeps its z, scenes rendered into textures, saving textures as PNGs, and gamepad rumble. Before it, v0.8.2 let DirectX 12 start on a machine without a GPU and v0.8.1 fixed back-face culling for custom materials. All three sit on v0.8.0: the animation engine work (skinned models, an animator with blending, layers and timeline events, joint sockets, and in-place model hot-reload) and its example game, Marionette. Before it, v0.7.0 shipped the lighting engine and Candlewick, v0.7.1 the transform hierarchy, and v0.7.2 a choice to keep updating in the background (UpdateInBackground). Every milestone ships with an example game that exercises it — see ROADMAP.md for the full plan, the point releases (v0.4.1–v0.4.3, v0.5.1, v0.6.1–v0.6.3, v0.7.1–v0.7.2, v0.8.1–v0.8.3) and what each example is built to demonstrate.
| Version | Milestone | Example game | Status |
|---|---|---|---|
| v0.1 | Core Foundation — windowing, input, basic rendering pipeline | FlappyBird | shipped |
| v0.2 | Extended Rendering Pipeline — D3D11/D3D12 back-ends, Renderer3D, render thread | Breakout3D | shipped |
| v0.3 | Scenes & ECS | SpaceInvaders | shipped |
| v0.4 | Physics & Collision — Box2D 2D and Jolt 3D, then 3D inside the ECS | AngryBirds, DungeonCrawler3D | shipped |
| v0.5 | Audio & Gameplay-Grade Physics — miniaudio, character controller, ray/shape casts | EchoVault | shipped |
| v0.6 | Asset Pipeline & Hot-Reload — AssetManager, async loading, live reload | ArenaShooter | shipped |
| v0.7 | Lighting & Shading — point/spot lights on a capped forward multi-light path, specular | Candlewick | shipped |
| v0.7.1 | Transform Hierarchy — parent-child transforms in 3D and 2D, world-space rendering, lights, audio and physics | DungeonCrawler3D, EchoVault | shipped |
| v0.8 | Animation & Character Fidelity — GPU-skinned meshes, clips, blending and layers, timeline events, joint sockets | Marionette | shipped |
| v0.9 | Shadows, Post-processing & VFX — cascaded and local shadows, tone mapping, bloom, AO, GPU particles | Candlewick, Marionette upgrades | shipped |
| v1.0 | Stability, Performance & Polish — docs, Linux validation, culling + instancing | Dungeon Crawler (full release) | planned |
Shipped out of band: scripting (C# or Lua) and networking/multiplayer are optional modules layered onto a released engine rather than numbered milestones — neither blocks the version train, and no single-player game carries a transport layer it never uses. Online co-op is therefore not part of the 1.0 launch.
Prerequisites
VULKAN_SDK environment variable set (on Linux, LunarG's tarball and its setup-env.sh; distro packages are too old)1. Clone the repository
git clone --recursive https://github.com/KingoBoiii/DingoEngine.git
If cloned non-recursively, fetch submodules afterwards:
git submodule update --init
2. Generate project files
On Windows, run Generate-Windows.bat from the root directory. This will invoke Premake5 and produce a Visual Studio solution with all projects and dependencies configured.
On Linux, first build assimp 6.0.4 as a static library into vendor/assimp/lib/linux-x86_64/ (see Getting Started), then run Generate-Linux.sh, which runs the repo's premake (vendor/premake/bin/premake5 gmake). VS Code users get Linux build tasks and gdb launch entries in .vscode.
3. Build & run
On Windows, open the generated DingoEngine.slnx in Visual Studio, set one of the example projects (FlappyBird, Breakout3D, DungeonCrawler, SpaceInvaders, AngryBirds, DungeonCrawler3D, EchoVault, ArenaShooter, Candlewick, or Marionette) as the startup project, and build.
On Linux, run make -j"$(nproc)" config=debug (or release, distribution, debug-asan; add a project name to build just that one), then start an example from its own directory so its assets/ resolve:
cd examples/FlappyBird && ../../build/bin/Debug-linux-x86_64/FlappyBird/FlappyBird
Linux has been verified headless on Mesa's software Vulkan driver; real GPUs and desktops are still being validated (see LINUX-SUPPORT.md).
| Project | Description |
|---|---|
FlappyBird | Complete 2D game — sprites, input, collision, audio, score rendering |
Breakout3D | 3D Breakout — perspective camera, mesh rendering, manual AABB collision |
DungeonCrawler | Top-down 2D slice — tile collision, chasing enemies, melee combat, loot |
SpaceInvaders | Scene/ECS showcase — EnTT entities and a multi-scene SceneManager |
AngryBirds | 2D physics showcase — slingshot launching, destructible block towers, and pig targets on the Box2D-backed physics world |
DungeonCrawler3D | 3D dungeon-crawler prototype — the first ECS-integrated 3D scene: procedurally generated dungeons (rooms + corridors), player/enemies/walls as RigidBody3D entities on the Jolt-backed Physics3D, melee combat (SPACE) with enemy health + a player health bar, treasure to collect, a follow camera, drawn via Renderer3D; run with --night for a dark dungeon lit by a lantern and point-lit treasure |
EchoVault | v0.5 showcase — capsule character controller on floating platforms (slopes, stairs, moving kinematic platforms), ray/shape-cast gameplay (patrolling sentry line-of-sight), and 3D positional audio you navigate by, with full gamepad play; since v0.7 its orbs and sentries are lit emissive materials that carry point lights |
ArenaShooter | v0.6 showcase — wave-based top-down shooter driven entirely by the AssetManager: async loading behind a progress bar, all sprites/audio/fonts via UUID handles, and live hot-reload (edit assets/shaders/background.glsl or a sprite PNG while it runs) |
Candlewick | v0.7 showcase — a stealth crawl through a dark keep where every light is a gameplay object: the lantern you carry is a point light whose radius is your oil, wardens carry point lights and see through spot-light vision cones tested with the renderer's own light query (GetLightAttenuation), so the cone drawn on the floor is the cone that catches you, a game-side light budget keeps the decorative flames and the gameplay lights inside the engine's 32, and lit emissive braziers (hold to light) are the checkpoints; light the Chapel altar to win. Move with WASD / arrows / left stick, snuff or relight the lantern with Q / (X), hold E / (A) beside a brazier, pause with Esc / Start; synthesised 3D positional audio, full gamepad play, and --debug-cone to draw what the wardens test. The v0.9 upgrade: every gameplay light casts shadows you can hide in (wardens and braziers ask the renderer's own shadows through shadow probes), tone mapping and bloom on the flames, particle flames, embers and smoke, and AO |
Marionette | v0.8 showcase — a melee duel against three escalating opponents where no combat timing lives in code: every wind-up, hit window, combo window and dodge's invulnerability is a timeline event in a .events file beside the clips, swords and hit spheres ride joint sockets, a masked upper-body layer blocks while the legs keep walking, a Blend1D locomotion blend feeds positional footsteps from step events, and one set of clip libraries is retargeted onto four characters. Three AI tiers read the opponent's wind-up through a reaction delay and parry, block or dodge it; edit a .events file while it runs (--hot-reload, or --live-edit-demo) and the fight changes live. Move with WASD / left stick, light attack J / left mouse / X, heavy K / right mouse / Y, hold Shift / right bumper to block (tap to parry), dodge with Space / A, pause with Esc / Start; --debug-hitbox draws the hit and hurt spheres, --check runs its built-in checks, and --autoplay or --tournament=N play AI against AI. The v0.9 upgrade: footfall dust and blade trails fired straight from the clips' events (ParticleEventComponent), sparks where a blade lands, a parry flash that blooms, knock-out dust, particle braziers, and a moon and braziers casting the fighters' skinned shadows, with tone mapping and AO |
include/DingoEngine/ Public API
src/DingoEngine/ Engine implementation
vendor/ Third-party dependencies (submodules)
examples/ Example projects
| Library | Purpose |
|---|---|
| NVRHI | Graphics API abstraction (Vulkan / D3D12) |
| GLFW | Windowing and input |
| GLM | Math (vectors, matrices, quaternions) |
| spdlog | Logging |
| Dear ImGui | Debug UI |
| stb | Image loading |
| msdf-atlas-gen | Font SDF atlas generation |
| EnTT | Entity-component system (scenes) |
| Box2D | 2D rigid-body physics simulation |
| Jolt Physics | 3D rigid-body physics simulation |
*Marionette*'s characters, weapons and animations: KayKit by Kay Lousberg, www.kaylousberg.com — CC0. Each pack's licence text is kept in examples/Marionette/assets/.
The test app's Fox (test/assets/models/Fox): model by PixelMannen, rigging and animation by @tomkranis, glTF conversion by @AsoboStudio with @scurest — CC-BY 4.0, from the Khronos glTF Sample Models.
hooks/register.tsx 180 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, SessionRateLimit } from 'claude-code'
3
4import type { FiveHourReading } from '../types'
5
6const reading = atom({ plugin: 'usage-meter', key: 'reading' } as const, null)
7const now = atom({ plugin: 'usage-meter', key: 'now' } as const, 0)
8
9// Shared across sessions, so every open session shows the newest reading.
10const STORE_KEY = 'fiveHour'
11const TICK_MS = 30_000
12// Label column width, shared with session-meter so the two bars line up.
13const LABEL_CELLS = 9
14const BAR_TRACK = '#3a3a3a'
15
16function pickFiveHour(limits: SessionRateLimit[], seenAt: number): FiveHourReading | null {
17 const window = limits.find(l => l.kind === 'five_hour')
18
19 return window ? { percentUsed: window.percentUsed, resetsAt: window.resetsAt, seenAt } : null
20}
21
22function isReading(value: unknown): value is FiveHourReading {
23 const v = value as FiveHourReading | null
24
25 return typeof v === 'object' && v !== null && typeof v.percentUsed === 'number' && typeof v.seenAt === 'number'
26}
27
28async function save($: EngineInterface, fresh: FiveHourReading) {
29 await update($, reading, () => fresh)
30 await $.store.set(STORE_KEY, fresh)
31}
32
33async function syncFromStore($: EngineInterface) {
34 const stored = await $.store.get(STORE_KEY)
35 if (!isReading(stored)) {
36 return
37 }
38
39 const current = await read($, reading)
40 if (current === null || stored.seenAt > current.seenAt) {
41 await update($, reading, () => stored)
42 }
43}
44
45async function tick($: EngineInterface) {
46 try {
47 await syncFromStore($)
48 const t = await $.clock.now()
49 await update($, now, () => t)
50 } catch {
51 // A missed tick only delays the countdown by one period.
52 }
53}
54
55function formatDuration(ms: number): string {
56 const totalMinutes = Math.max(0, Math.ceil(ms / 60_000))
57 const hours = Math.floor(totalMinutes / 60)
58 const minutes = totalMinutes % 60
59
60 return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`
61}
62
63export const register: Register = on => {
64 on('session.start', async ($, e, next) => {
65 const started = await next(e)
66
67 await syncFromStore($)
68 const fresh = pickFiveHour((await $.session.usage()).rateLimits, await $.clock.now())
69 if (fresh) {
70 await save($, fresh)
71 }
72
73 $.clock.every(TICK_MS, () => void tick($))
74
75 return started
76 })
77
78 on('session.measure', async ($, e, next) => {
79 if (e.changed.includes('rateLimits')) {
80 const fresh = pickFiveHour(e.rateLimits, await $.clock.now())
81 if (fresh) {
82 await save($, fresh)
83 }
84 }
85
86 return next(e)
87 })
88
89 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
90 const below = await next(e)
91 if (e.props.hasSurvey) {
92 return below
93 }
94
95 const { Box } = $.ui.resolve(e)
96 const mine = await (async () => {
97 const r = await read($, reading)
98 await read($, now) // subscribes the band to the countdown ticker
99 const t = await $.clock.now()
100 const { Text } = $.ui.resolve(e)
101
102 if (r === null) {
103 return (
104 <Box>
105 <Box width={LABEL_CELLS} flexShrink={0}>
106 <Text bold>5h limit</Text>
107 </Box>
108 <Text dimColor>no reading yet · shows after your next message</Text>
109 </Box>
110 )
111 }
112
113 const resetAt = r.resetsAt ? Date.parse(r.resetsAt) : NaN
114 const hasReset = Number.isFinite(resetAt) && resetAt <= t
115 const used = hasReset ? 0 : r.percentUsed
116 const left = Math.max(0, Math.round((100 - used) * 10) / 10)
117
118 const width = Math.max(10, Math.min(24, e.props.bodyColumns - 70))
119 const filled = Math.round((Math.min(used, 100) / 100) * width)
120 const color = used >= 80 ? 'red' : used >= 50 ? 'yellow' : 'green'
121
122 // Solid cells, not glyphs: a proportional font draws █ and ░ wider than a cell.
123 const parts = []
124 if (filled > 0) {
125 parts.push(
126 <Box width={filled} flexShrink={0} backgroundColor={color}>
127 <Text>{' '.repeat(filled)}</Text>
128 </Box>,
129 )
130 }
131 if (filled < width) {
132 parts.push(
133 <Box width={width - filled} flexShrink={0} backgroundColor={BAR_TRACK}>
134 <Text>{' '.repeat(width - filled)}</Text>
135 </Box>,
136 )
137 }
138 const bar = (
139 <Box width={width} flexShrink={0} overflow="hidden">
140 {parts}
141 </Box>
142 )
143
144 const resetText = hasReset
145 ? 'window reset · refreshes after next message'
146 : Number.isFinite(resetAt)
147 ? `resets in ${formatDuration(resetAt - t)}`
148 : 'reset time unknown'
149 const isStale = !hasReset && t - r.seenAt >= 5 * 60_000
150 const ageText = isStale ? ` · as of ${formatDuration(t - r.seenAt)} ago` : ''
151
152 return (
153 <Box>
154 <Box width={LABEL_CELLS} flexShrink={0}>
155 <Text bold>5h limit</Text>
156 </Box>
157 {bar}
158 <Text color={color} bold>
159 {' '}
160 {used}% used
161 </Text>
162 <Text dimColor>
163 {' '}
164 · {left}% left · {resetText}
165 {ageText}
166 </Text>
167 </Box>
168 )
169 })()
170
171 // Stack above any band another plugin drew beneath this one.
172 return below.type === 'engine' ? mine : (
173 <Box flexDirection="column">
174 {mine}
175 {below}
176 </Box>
177 )
178 })
179}
180types/index.d.ts 15 lines1export type FiveHourReading = {
2 /** 0 to 100: share of the 5-hour limit used. */
3 percentUsed: number
4 /** ISO 8601 time the window resets, when the API reported one. */
5 resetsAt?: string
6 /** When this reading was taken, epoch ms. */
7 seenAt: number
8}
9
10declare module 'claude-code' {
11 interface PluginState {
12 'usage-meter': { reading: FiveHourReading | null; now: number }
13 }
14}
15