From 60740a53c26fbc2d269aa80ead7aec1f953a8f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Phillip=20K=C3=BChne?= Date: Thu, 1 Oct 2026 22:00:55 +0200 Subject: [PATCH] refactor: optimize snake sprite rendering using preloaded directional assets and implement HUD caching --- AGENTS.md | 4 +- include/game_render.h | 24 ++- source/game_input.cpp | 3 + source/game_render.cpp | 361 +++++++++++++++++++++++++---------------- source/main.cpp | 28 +++- 5 files changed, 268 insertions(+), 152 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8def986..518ebb7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -179,7 +179,9 @@ The repository includes preconfigured tasks in [`.vscode/tasks.json`](.vscode/ta ### 6.2 Asset Pipeline - **Graphics Pipeline:** - [`gfx/tilemap.png`](gfx/tilemap.png) + [`gfx/tilemap.grit`](gfx/tilemap.grit) are processed by `grit` into `${CMAKE_CURRENT_BINARY_DIR}/tilemap.s` and `tilemap.h`. - - Sprites are referenced by index: tile 1 (segment), 2 (small segment), 3 (head), 5/6 (apple flash frames), 7/8/9/10 (tail variations), 11 (rotten apple). + - Sprites are referenced by index: tile 1 (segment), 2 (small segment), 3 (head horizontal), 4 (head vertical), 5/6 (apple flash frames), 7/8/9/10 (tail variations), 11 (rotten apple). All 12 sprite tiles are preloaded once into VRAM at startup; directional and blinking variants are rendered via pointer selection and hardware flipping flags without mid-frame DMA transfers. + - Active OAM sprites are clamped at hardware limit (128) and trailing unused sprites are hidden every frame via `oamClear(&oamSub, oamId, 128 - oamId)`. + - Console text HUD rendering in `draw_stats()` is throttled via dirty-flag caching (`HudCache`), eliminating redundant `iprintf` calls on VBlank. - **Audio Pipeline:** - Audio files in [`sounds/`](sounds/) are compiled by `mmutil` into `soundbank.bin` and header `soundbank.h`. - `bin2s` embeds `soundbank.bin` into assembly source `soundbank_bin.S` and header `soundbank_bin.h`. diff --git a/include/game_render.h b/include/game_render.h index 2ca215c..9c8fd76 100644 --- a/include/game_render.h +++ b/include/game_render.h @@ -5,22 +5,38 @@ #include "game_state.h" +struct HudCache { + int score = -1; + bool cursed = false; + bool paused = false; + bool gameOver = false; + bool musicEnabled = false; + bool hardMode = false; + bool dirty = true; +}; + struct RenderAssets { PrintConsole *topScreen = nullptr; PrintConsole *bottomScreen = nullptr; u16 *gfx_segment = nullptr; u16 *gfx_segment_small = nullptr; - u16 *gfx_head = nullptr; + u16 *gfx_head_h = nullptr; + u16 *gfx_head_v = nullptr; u16 *gfx_apple = nullptr; - u16 *gfx_tail = nullptr; + u16 *gfx_apple_blink = nullptr; + u16 *gfx_tail_h[2] = {nullptr, nullptr}; + u16 *gfx_tail_v[2] = {nullptr, nullptr}; u16 *gfx_apple_rotten = nullptr; u16 *gfx_apple_cursed = nullptr; + + HudCache hudCache; }; +void invalidate_hud(RenderAssets &assets); void draw_game(const GameState &state, const RenderAssets &assets); -void draw_stats(const GameState &state, bool internals, bool musicEnabled, uint8_t frame); -void draw_menu(const GameState &state, const RenderAssets &assets, bool musicEnabled); +void draw_stats(const GameState &state, RenderAssets &assets, bool internals, bool musicEnabled, uint8_t frame); +void draw_menu(const GameState &state, RenderAssets &assets, bool musicEnabled); void blink_apples(const GameState &state, u16 *gfx_apple); #endif diff --git a/source/game_input.cpp b/source/game_input.cpp index 04949d8..b679ff4 100644 --- a/source/game_input.cpp +++ b/source/game_input.cpp @@ -26,6 +26,7 @@ bool handle_input_frame(InputContext &ctx) { setBackdropColor(RGB15(0, 0, 0)); consoleSelect(ctx.topScreen); consoleClear(); + invalidate_hud(*ctx.renderAssets); } } @@ -115,11 +116,13 @@ bool handle_input_frame(InputContext &ctx) { audio_play_pause(*ctx.audio); audio_pause_music_if_enabled(*ctx.audio); puts("\x1b[11;12HPaused"); + invalidate_hud(*ctx.renderAssets); } else { audio_resume_music_if_enabled(*ctx.audio); audio_play_unpause(*ctx.audio); consoleSelect(ctx.topScreen); consoleClear(); + invalidate_hud(*ctx.renderAssets); } } else { ctx.init_game(); diff --git a/source/game_render.cpp b/source/game_render.cpp index 4eea852..4bb1ddd 100644 --- a/source/game_render.cpp +++ b/source/game_render.cpp @@ -8,190 +8,273 @@ namespace { -const void *get_sprite_pointer(uint16_t spriteIndex) { - return &tilemapTiles[spriteIndex * 16]; +void draw_snake_head(const GameState &state, const RenderAssets &assets, + uint16_t &oamId) { + if (oamId >= 128 || state.snake.empty()) { + return; + } + + const std::array dot = state.snake.front(); + u16 *headGfx = nullptr; + bool hflip = false; + bool vflip = false; + + switch (state.snakeDirection) { + case 0: // Up + headGfx = assets.gfx_head_v; + break; + case 1: // Right + headGfx = assets.gfx_head_h; + break; + case 2: // Down + headGfx = assets.gfx_head_v; + vflip = true; + break; + case 3: // Left + headGfx = assets.gfx_head_h; + hflip = true; + break; + default: + headGfx = assets.gfx_head_h; + break; + } + + oamSet(&oamSub, oamId, dot[0] * 8, dot[1] * 8, 0, 0, SpriteSize_8x8, + SpriteColorFormat_256Color, headGfx, -1, false, false, hflip, vflip, + false); + oamId++; } -void draw_snake_head(const GameState &state, const RenderAssets &assets, uint16_t &oamId) { - const std::array dot = state.snake.front(); - bool hflip = false; - bool vflip = false; +void draw_snake_tail(const GameState &state, const RenderAssets &assets, + uint16_t &oamId) { + if (oamId >= 128 || state.snake.size() < 2) { + return; + } - switch (state.snakeDirection) { - case 0: - dmaCopy(&tilemapTiles[64], assets.gfx_head, 64); - break; - case 1: - dmaCopy(&tilemapTiles[48], assets.gfx_head, 64); - break; - case 2: - dmaCopy(&tilemapTiles[64], assets.gfx_head, 64); - vflip = true; - break; - case 3: - dmaCopy(&tilemapTiles[48], assets.gfx_head, 64); - hflip = true; - break; - default: - break; + const std::array dot = state.snake.back(); + const size_t var = (state.snake.size() - 1) % 2; + u16 *tailGfx = nullptr; + bool hflip = false; + bool vflip = false; + + switch (state.snake.at(state.snake.size() - 2).at(2)) { + case 0: // Up + tailGfx = assets.gfx_tail_v[var]; + vflip = true; + break; + case 1: // Right + tailGfx = assets.gfx_tail_h[var]; + hflip = true; + break; + case 2: // Down + tailGfx = assets.gfx_tail_v[var]; + break; + case 3: // Left + tailGfx = assets.gfx_tail_h[var]; + break; + default: + tailGfx = assets.gfx_tail_h[var]; + break; + } + + oamSet(&oamSub, oamId, dot[0] * 8, dot[1] * 8, 0, 0, SpriteSize_8x8, + SpriteColorFormat_256Color, tailGfx, -1, false, false, hflip, vflip, + false); + oamId++; +} + +void draw_snake(const GameState &state, const RenderAssets &assets, + uint16_t &oamId) { + if (state.snake.empty()) { + return; + } + + draw_snake_head(state, assets, oamId); + + for (size_t i = 1; i + 1 < state.snake.size(); i++) { + if (oamId >= 128) { + return; } - - oamSet(&oamSub, oamId, dot[0] * 8, dot[1] * 8, 0, 0, SpriteSize_8x8, SpriteColorFormat_256Color, - assets.gfx_head, -1, false, false, hflip, vflip, false); + oamSet(&oamSub, oamId, state.snake[i][0] * 8, state.snake[i][1] * 8, 0, 0, + SpriteSize_8x8, SpriteColorFormat_256Color, + (i % 2) ? assets.gfx_segment_small : assets.gfx_segment, -1, false, + false, false, false, false); oamId++; -} - -void draw_snake_tail(const GameState &state, const RenderAssets &assets, uint16_t &oamId) { - const std::array dot = state.snake.back(); - bool hflip = false; - bool vflip = false; - - switch (state.snake.at(state.snake.size() - 2).at(2)) { - case 0: - dmaCopy(get_sprite_pointer(9 + ((state.snake.size() - 1) % 2)), assets.gfx_tail, 64); - vflip = true; - break; - case 1: - dmaCopy(get_sprite_pointer(7 + ((state.snake.size() - 1) % 2)), assets.gfx_tail, 64); - hflip = true; - break; - case 2: - dmaCopy(get_sprite_pointer(9 + ((state.snake.size() - 1) % 2)), assets.gfx_tail, 64); - break; - case 3: - dmaCopy(get_sprite_pointer(7 + ((state.snake.size() - 1) % 2)), assets.gfx_tail, 64); - break; - default: - break; - } - - oamSet(&oamSub, oamId, dot[0] * 8, dot[1] * 8, 0, 0, SpriteSize_8x8, SpriteColorFormat_256Color, - assets.gfx_tail, -1, false, false, hflip, vflip, false); - oamId++; -} - -void draw_snake(const GameState &state, const RenderAssets &assets, uint16_t &oamId) { - draw_snake_head(state, assets, oamId); - - for (size_t i = 1; i < state.snake.size() - 1; i++) { - oamSet(&oamSub, oamId, state.snake[i][0] * 8, state.snake[i][1] * 8, 0, 0, SpriteSize_8x8, - SpriteColorFormat_256Color, (i % 2) ? assets.gfx_segment_small : assets.gfx_segment, -1, - false, false, false, false, false); - oamId++; - } + } + if (state.snake.size() > 1) { draw_snake_tail(state, assets, oamId); + } } -void draw_apples(const GameState &state, const RenderAssets &assets, uint16_t &oamId) { - for (const std::array &apple : state.apples) { - oamSet(&oamSub, oamId, apple[0] * 8, apple[1] * 8, 0, 0, SpriteSize_8x8, - SpriteColorFormat_256Color, assets.gfx_apple, -1, false, false, false, false, false); - oamId++; +void draw_apples(const GameState &state, const RenderAssets &assets, + uint16_t &oamId) { + u16 *appleGfx = + (state.ticks % 4 == 0) ? assets.gfx_apple_blink : assets.gfx_apple; + for (const std::array &apple : state.apples) { + if (oamId >= 128) { + return; } + oamSet(&oamSub, oamId, apple[0] * 8, apple[1] * 8, 0, 0, SpriteSize_8x8, + SpriteColorFormat_256Color, appleGfx, -1, false, false, false, false, + false); + oamId++; + } } -void draw_apples_rotten(const GameState &state, const RenderAssets &assets, uint16_t &oamId) { - for (const std::array &apple : state.bad_apples) { - oamSet(&oamSub, oamId, apple[0] * 8, apple[1] * 8, 0, 0, SpriteSize_8x8, - SpriteColorFormat_256Color, assets.gfx_apple_rotten, -1, false, false, false, false, - false); - oamId++; +void draw_apples_rotten(const GameState &state, const RenderAssets &assets, + uint16_t &oamId) { + for (const std::array &apple : state.bad_apples) { + if (oamId >= 128) { + return; } + oamSet(&oamSub, oamId, apple[0] * 8, apple[1] * 8, 0, 0, SpriteSize_8x8, + SpriteColorFormat_256Color, assets.gfx_apple_rotten, -1, false, + false, false, false, false); + oamId++; + } } -void draw_apples_cursed(const GameState &state, const RenderAssets &assets, uint16_t &oamId) { - for (const std::array &apple : state.cursed_apples) { - oamSet(&oamSub, oamId, apple[0] * 8, apple[1] * 8, 0, 0, SpriteSize_8x8, - SpriteColorFormat_256Color, assets.gfx_apple_cursed, -1, false, false, false, false, - false); - oamId++; +void draw_apples_cursed(const GameState &state, const RenderAssets &assets, + uint16_t &oamId) { + for (const std::array &apple : state.cursed_apples) { + if (oamId >= 128) { + return; } + oamSet(&oamSub, oamId, apple[0] * 8, apple[1] * 8, 0, 0, SpriteSize_8x8, + SpriteColorFormat_256Color, assets.gfx_apple_cursed, -1, false, + false, false, false, false); + oamId++; + } } } // namespace +void invalidate_hud(RenderAssets &assets) { assets.hudCache.dirty = true; } + void draw_game(const GameState &state, const RenderAssets &assets) { - uint16_t oamId = 0; + uint16_t oamId = 0; - if (!state.gameOver) { - draw_apples(state, assets, oamId); - draw_apples_rotten(state, assets, oamId); - draw_apples_cursed(state, assets, oamId); - draw_snake(state, assets, oamId); - return; + if (!state.gameOver) { + draw_apples(state, assets, oamId); + draw_apples_rotten(state, assets, oamId); + draw_apples_cursed(state, assets, oamId); + draw_snake(state, assets, oamId); + + // Hide all unused sprites up to hardware limit (128) to eliminate trailing + // ghost sprites + if (oamId < 128) { + oamClear(&oamSub, oamId, 128 - oamId); } + return; + } - iprintf("\x1b[11;12HGAME OVER!"); - consoleSelect(assets.topScreen); - iprintf("\x1b[10;13HGAME OVER!"); + // On Game Over, hide all sprites so none are left dangling + oamClear(&oamSub, 0, 128); + + iprintf("\x1b[11;12HGAME OVER!"); + consoleSelect(assets.topScreen); + iprintf("\x1b[10;13HGAME OVER!"); } -void draw_stats(const GameState &state, bool internals, bool musicEnabled, uint8_t frame) { - if (internals) { - iprintf("\x1b[0;0HFrame = %i\nTick = %i\ndir = %i", frame, state.ticks, state.snakeDirection); - iprintf("\x1b[3;0HLength: %i", state.snakelength); - iprintf("\x1b[4;0Hx=%i\ny=%i", state.snake.front()[0], state.snake.front()[1]); +void draw_stats(const GameState &state, RenderAssets &assets, bool internals, + bool musicEnabled, uint8_t frame) { + if (internals) { + iprintf("\x1b[0;0HFrame = %i\nTick = %i\ndir = %i", frame, state.ticks, + state.snakeDirection); + iprintf("\x1b[3;0HLength: %i", state.snakelength); + if (!state.snake.empty()) { + iprintf("\x1b[4;0Hx=%i\ny=%i", state.snake.front()[0], + state.snake.front()[1]); } + } + HudCache &cache = assets.hudCache; + const bool cursed = (state.curseTicksRemaining > 0); + + const bool needs_score_update = cache.dirty || (cache.score != state.score); + const bool needs_curse_update = cache.dirty || (cache.cursed != cursed); + const bool needs_pause_update = + cache.dirty || (cache.paused != state.paused) || + (cache.musicEnabled != musicEnabled) || + (cache.hardMode != state.hardMode) || (cache.gameOver != state.gameOver); + + if (needs_score_update) { iprintf("\x1b[10;12HScore: %i", state.score); + cache.score = state.score; + } - if (state.curseTicksRemaining > 0) { - iprintf("\x1b[1;7HCONTROLS INVERTED!"); + if (needs_curse_update) { + if (cursed) { + iprintf("\x1b[1;7HCONTROLS INVERTED!"); } else { - iprintf("\x1b[1;7H "); + iprintf("\x1b[1;7H "); } + cache.cursed = cursed; + } + if (needs_pause_update) { if (!state.paused) { - if (state.gameOver) { - iprintf("\x1b[23;0HB = Main Menu"); - } - return; - } - - iprintf("\x1b[21;0HB = Main Menu"); - - if (musicEnabled) { - iprintf("\x1b[23;0HX = Disable Music"); + if (state.gameOver) { + iprintf("\x1b[23;0HB = Main Menu"); + } else if (cache.paused) { + // Clear any leftover pause strings if previously paused + iprintf("\x1b[21;0H "); + iprintf("\x1b[22;0H "); + iprintf("\x1b[23;0H "); + } } else { - iprintf("\x1b[23;0HX = Enable Music "); - } - - if (state.hardMode) { + iprintf("\x1b[21;0HB = Main Menu"); + if (state.hardMode) { iprintf("\x1b[22;0HY = Restart in normal mode"); - } else { - iprintf("\x1b[22;0HY = Restart in Hard mode"); + } else { + iprintf("\x1b[22;0HY = Restart in Hard mode "); + } + if (musicEnabled) { + iprintf("\x1b[23;0HX = Disable Music"); + } else { + iprintf("\x1b[23;0HX = Enable Music "); + } } + cache.paused = state.paused; + cache.musicEnabled = musicEnabled; + cache.hardMode = state.hardMode; + cache.gameOver = state.gameOver; + } + + cache.dirty = false; } -void draw_menu(const GameState &state, const RenderAssets &assets, bool musicEnabled) { - oamClear(&oamSub, 0, 0); - oamUpdate(&oamSub); +void draw_menu(const GameState &state, RenderAssets &assets, + bool musicEnabled) { + invalidate_hud(assets); - consoleSelect(assets.bottomScreen); - consoleClear(); + oamClear(&oamSub, 0, 128); + oamUpdate(&oamSub); - consoleSelect(assets.topScreen); - consoleClear(); + consoleSelect(assets.bottomScreen); + consoleClear(); - iprintf("\x1b[3;10HSNAKE PLUS"); - iprintf("\x1b[5;5H---------------------"); + consoleSelect(assets.topScreen); + consoleClear(); - const char *hardModeLabel = state.hardMode ? "ON " : "OFF"; - const char *musicLabel = musicEnabled ? "ON " : "OFF"; + iprintf("\x1b[3;10HSNAKE PLUS"); + iprintf("\x1b[5;5H---------------------"); - iprintf("\x1b[8;7H%c Start Game", state.menuSelection == 0 ? '>' : ' '); - iprintf("\x1b[10;7H%c Hard Mode: %s", state.menuSelection == 1 ? '>' : ' ', hardModeLabel); - iprintf("\x1b[12;7H%c Music: %s", state.menuSelection == 2 ? '>' : ' ', musicLabel); + const char *hardModeLabel = state.hardMode ? "ON " : "OFF"; + const char *musicLabel = musicEnabled ? "ON " : "OFF"; - iprintf("\x1b[19;3HUP/DOWN: Navigate"); - iprintf("\x1b[20;3HA/START: Select"); + iprintf("\x1b[8;7H%c Start Game", state.menuSelection == 0 ? '>' : ' '); + iprintf("\x1b[10;7H%c Hard Mode: %s", state.menuSelection == 1 ? '>' : ' ', + hardModeLabel); + iprintf("\x1b[12;7H%c Music: %s", state.menuSelection == 2 ? '>' : ' ', + musicLabel); + + iprintf("\x1b[19;3HUP/DOWN: Navigate"); + iprintf("\x1b[20;3HA/START: Select"); } -void blink_apples(const GameState &state, u16 *gfx_apple) { - if (state.ticks % 4) { - dmaCopy(get_sprite_pointer(5), gfx_apple, 64); - } else { - dmaCopy(get_sprite_pointer(6), gfx_apple, 64); - } +void blink_apples(const GameState & /*state*/, u16 * /*gfx_apple*/) { + // Blinking is now handled directly by draw_apples() via sprite pointer + // selection without DMA. } diff --git a/source/main.cpp b/source/main.cpp index 62c0994..a8298dd 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -47,7 +47,7 @@ AudioContext audioContext; RenderAssets renderAssets; -const void * get_sprite_pointer(uint16 spriteIndex) { +const void * get_sprite_pointer(uint16_t spriteIndex) { return &tilemapTiles[spriteIndex*16]; } @@ -69,16 +69,27 @@ void setup() { renderAssets.gfx_segment = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); renderAssets.gfx_segment_small = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); - renderAssets.gfx_head = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); + renderAssets.gfx_head_h = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); + renderAssets.gfx_head_v = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); renderAssets.gfx_apple = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); + renderAssets.gfx_apple_blink = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); + renderAssets.gfx_tail_h[0] = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); + renderAssets.gfx_tail_h[1] = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); + renderAssets.gfx_tail_v[0] = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); + renderAssets.gfx_tail_v[1] = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); renderAssets.gfx_apple_rotten = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); renderAssets.gfx_apple_cursed = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); - renderAssets.gfx_tail = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); dmaCopy(get_sprite_pointer(1), renderAssets.gfx_segment, 64); dmaCopy(get_sprite_pointer(2), renderAssets.gfx_segment_small, 64); - dmaCopy(get_sprite_pointer(3), renderAssets.gfx_head, 64); + dmaCopy(get_sprite_pointer(3), renderAssets.gfx_head_h, 64); + dmaCopy(get_sprite_pointer(4), renderAssets.gfx_head_v, 64); dmaCopy(get_sprite_pointer(5), renderAssets.gfx_apple, 64); + dmaCopy(get_sprite_pointer(6), renderAssets.gfx_apple_blink, 64); + dmaCopy(get_sprite_pointer(7), renderAssets.gfx_tail_h[0], 64); + dmaCopy(get_sprite_pointer(8), renderAssets.gfx_tail_h[1], 64); + dmaCopy(get_sprite_pointer(9), renderAssets.gfx_tail_v[0], 64); + dmaCopy(get_sprite_pointer(10), renderAssets.gfx_tail_v[1], 64); dmaCopy(get_sprite_pointer(11), renderAssets.gfx_apple_rotten, 64); consoleSelect(&topScreen); @@ -118,7 +129,7 @@ void handle_vblank() { frame++; if (gameState.scene == Scene::Playing) { consoleSelect(&topScreen); - draw_stats(gameState, internals, audio_is_music_enabled(audioContext), frame); + draw_stats(gameState, renderAssets, internals, audio_is_music_enabled(audioContext), frame); } oamUpdate(&oamSub); //flashy_colors(); @@ -155,7 +166,6 @@ void do_tick() { audio_stop_music(); audio_play_fail(audioContext); } - blink_apples(gameState, renderAssets.gfx_apple); //consoleSelect(&bottomScreen); draw_game(gameState, renderAssets); } @@ -179,9 +189,10 @@ void init_game() { gameState.cursed_apples.clear(); consoleSelect(&topScreen); consoleClear(); + invalidate_hud(renderAssets); consoleSelect(&bottomScreen); consoleClear(); - oamClear(&oamSub,0,0); + oamClear(&oamSub,0,128); audio_start_music(audioContext); } else @@ -198,9 +209,10 @@ void init_game() { gameState.cursed_apples.clear(); consoleSelect(&topScreen); consoleClear(); + invalidate_hud(renderAssets); consoleSelect(&bottomScreen); consoleClear(); - oamClear(&oamSub,0,0); + oamClear(&oamSub,0,128); audio_start_music(audioContext); }