mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-05-05 11:09:39 +00:00
Split Q3 workflow into composable sub-workflows
Breaks the monolithic Quake3 per-frame workflows into reusable include-based sub-workflows (input, physics, combat, render, overlay, postfx) and adds a screenshot-check component for the screenshot package. Introduces new workflow JSONs (q3_input, q3_physics, q3_combat, q3_render, q3_overlay, q3_postfx, q3_screenshot_check) and updates q3_frame and screenshot q3_frame to include them. Implements SDL software overlay steps (hud, crosshair, hitmarker, mapselect, menu_frame) and software overlay begin/end rendering steps with corresponding headers and source files; removes the old q3_overlay_draw implementation. Updates CMakeLists and workflow parser/registrar to register and build the new sources. This refactor improves composability and reusability of workflow sections.
This commit is contained in:
@@ -189,6 +189,7 @@ if(BUILD_SDL3_APP)
|
||||
src/services/impl/workflow/workflow_definition_parser.cpp
|
||||
src/services/impl/workflow/workflow_definition_parser_nodes.cpp
|
||||
src/services/impl/workflow/workflow_definition_parser_variables.cpp
|
||||
src/services/impl/workflow/workflow_definition_parser_includes.cpp
|
||||
src/services/impl/workflow/workflow_connection_resolver.cpp
|
||||
src/services/impl/workflow/workflow_execute_step.cpp
|
||||
src/stb_image.cpp
|
||||
@@ -285,7 +286,13 @@ if(BUILD_SDL3_APP)
|
||||
src/services/impl/workflow/geometry/workflow_geometry_create_plane_step.cpp
|
||||
src/services/impl/workflow/geometry/workflow_geometry_cube_generate_step.cpp
|
||||
src/services/impl/workflow/quake3/workflow_q3_menu_update_step.cpp
|
||||
src/services/impl/workflow/quake3/workflow_q3_overlay_draw_step.cpp
|
||||
src/services/impl/workflow/rendering/workflow_overlay_sw_begin_step.cpp
|
||||
src/services/impl/workflow/rendering/workflow_overlay_sw_end_step.cpp
|
||||
src/services/impl/workflow/quake3/workflow_q3_hud_step.cpp
|
||||
src/services/impl/workflow/quake3/workflow_q3_crosshair_step.cpp
|
||||
src/services/impl/workflow/quake3/workflow_q3_hitmarker_step.cpp
|
||||
src/services/impl/workflow/quake3/workflow_q3_menu_frame_step.cpp
|
||||
src/services/impl/workflow/quake3/workflow_q3_mapselect_step.cpp
|
||||
src/services/impl/workflow/quake3/workflow_q3_pickups_draw_step.cpp
|
||||
src/services/impl/workflow/quake3/workflow_q3_weapon_update_step.cpp
|
||||
src/services/impl/workflow/quake3/workflow_q3_md3_load_step.cpp
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "Q3 Combat",
|
||||
"description": "Weapon fire + bot AI update.",
|
||||
"steps": [
|
||||
{
|
||||
"id": "q3_weapon",
|
||||
"plugin": "q3.weapon.update"
|
||||
},
|
||||
{
|
||||
"id": "q3_bots_update",
|
||||
"plugin": "q3.bots.update",
|
||||
"parameters": {
|
||||
"chase_range": 20.0,
|
||||
"shoot_range": 6.0,
|
||||
"move_speed": 3.5,
|
||||
"shoot_interval": 30
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,273 +1,36 @@
|
||||
{
|
||||
"name": "Q3 Frame Tick",
|
||||
"description": "Per-frame: poll input, FPS move, render BSP map with player models, bots, and weapon.",
|
||||
"nodes": [
|
||||
"description": "Per-frame composition: each section is a reusable sub-workflow included like a React component.",
|
||||
"steps": [
|
||||
{
|
||||
"id": "input_poll",
|
||||
"type": "input.poll",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0]
|
||||
"id": "input_group",
|
||||
"plugin": "workflow.include",
|
||||
"parameters": { "path": "packages/quake3/workflows/q3_input.json" }
|
||||
},
|
||||
{
|
||||
"id": "q3_menu",
|
||||
"type": "q3.menu.update",
|
||||
"typeVersion": 1,
|
||||
"position": [100, 0]
|
||||
"id": "physics_group",
|
||||
"plugin": "workflow.include",
|
||||
"parameters": { "path": "packages/quake3/workflows/q3_physics.json" }
|
||||
},
|
||||
{
|
||||
"id": "should_stop",
|
||||
"type": "bool.or",
|
||||
"typeVersion": 1,
|
||||
"position": [140, 0],
|
||||
"inputs": { "left": "q3.menu_quit_pressed", "right": "q3.menu_map_selected" },
|
||||
"outputs": { "value": "q3.stop_game" }
|
||||
"id": "combat_group",
|
||||
"plugin": "workflow.include",
|
||||
"parameters": { "path": "packages/quake3/workflows/q3_combat.json" }
|
||||
},
|
||||
{
|
||||
"id": "stop_game_loop",
|
||||
"type": "value.set_if",
|
||||
"typeVersion": 1,
|
||||
"position": [160, 0],
|
||||
"inputs": { "condition": "q3.stop_game" },
|
||||
"parameters": { "value": false },
|
||||
"outputs": { "value": "game_running" }
|
||||
"id": "render_group",
|
||||
"plugin": "workflow.include",
|
||||
"parameters": { "path": "packages/quake3/workflows/q3_render.json" }
|
||||
},
|
||||
{
|
||||
"id": "set_quit_requested",
|
||||
"type": "value.set_if",
|
||||
"typeVersion": 1,
|
||||
"position": [170, 0],
|
||||
"inputs": { "condition": "q3.menu_quit_pressed" },
|
||||
"parameters": { "value": true },
|
||||
"outputs": { "value": "q3.quit_requested" }
|
||||
"id": "overlay_group",
|
||||
"plugin": "workflow.include",
|
||||
"parameters": { "path": "packages/quake3/workflows/q3_overlay.json" }
|
||||
},
|
||||
{
|
||||
"id": "compute_movement_active",
|
||||
"type": "bool.not",
|
||||
"typeVersion": 1,
|
||||
"position": [190, 0],
|
||||
"inputs": { "value": "q3.menu_open" },
|
||||
"outputs": { "value": "movement_active" }
|
||||
},
|
||||
{
|
||||
"id": "update_mouse_grab",
|
||||
"type": "input.mouse.grab",
|
||||
"typeVersion": 1,
|
||||
"position": [195, 0],
|
||||
"inputs": { "enabled": "movement_active" }
|
||||
},
|
||||
{
|
||||
"id": "physics_move",
|
||||
"type": "physics.fps.move",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
"parameters": {
|
||||
"move_speed": 6.5,
|
||||
"sprint_multiplier": 1.5,
|
||||
"crouch_multiplier": 0.45,
|
||||
"jump_velocity": 5.5,
|
||||
"air_control": 0.25,
|
||||
"gravity_scale": 0.5,
|
||||
"ground_accel": 30.0,
|
||||
"ground_friction": 24.0,
|
||||
"step_height": 0.6,
|
||||
"crouch_height": 0.5,
|
||||
"stand_height": 1.4
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "physics_step",
|
||||
"type": "physics.step",
|
||||
"typeVersion": 1,
|
||||
"position": [400, 0]
|
||||
},
|
||||
{
|
||||
"id": "sync_transforms",
|
||||
"type": "physics.sync_transforms",
|
||||
"typeVersion": 1,
|
||||
"position": [500, 0]
|
||||
},
|
||||
{
|
||||
"id": "bsp_entities_update",
|
||||
"type": "bsp.entities.update",
|
||||
"typeVersion": 1,
|
||||
"position": [550, 0]
|
||||
},
|
||||
{
|
||||
"id": "camera_update",
|
||||
"type": "camera.fps.update",
|
||||
"typeVersion": 1,
|
||||
"position": [600, 0],
|
||||
"parameters": {
|
||||
"sensitivity": 0.003,
|
||||
"eye_height": 1.4,
|
||||
"fov": 90.0,
|
||||
"near": 0.1,
|
||||
"far": 500.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q3_weapon",
|
||||
"type": "q3.weapon.update",
|
||||
"typeVersion": 1,
|
||||
"position": [690, 0]
|
||||
},
|
||||
{
|
||||
"id": "q3_bots_update",
|
||||
"type": "q3.bots.update",
|
||||
"typeVersion": 1,
|
||||
"position": [695, 0],
|
||||
"parameters": {
|
||||
"chase_range": 20.0,
|
||||
"shoot_range": 6.0,
|
||||
"move_speed": 3.5,
|
||||
"shoot_interval": 30
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "render_prepare",
|
||||
"type": "render.prepare",
|
||||
"typeVersion": 1,
|
||||
"position": [750, 0]
|
||||
},
|
||||
{
|
||||
"id": "portal_view",
|
||||
"type": "bsp.portal_view",
|
||||
"typeVersion": 1,
|
||||
"position": [775, 0]
|
||||
},
|
||||
{
|
||||
"id": "frame_begin",
|
||||
"type": "frame.gpu.begin",
|
||||
"typeVersion": 1,
|
||||
"position": [800, 0],
|
||||
"parameters": {
|
||||
"clear_r": 0.3,
|
||||
"clear_g": 0.5,
|
||||
"clear_b": 0.8
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "draw_map",
|
||||
"type": "draw.map",
|
||||
"typeVersion": 1,
|
||||
"position": [1000, 0],
|
||||
"parameters": {
|
||||
"default_texture": "walls_texture",
|
||||
"pipeline_key": "gpu_pipeline_bsp",
|
||||
"roughness": 0.7,
|
||||
"metallic": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "draw_pickups",
|
||||
"type": "q3.pickups.draw",
|
||||
"typeVersion": 1,
|
||||
"position": [1100, 0]
|
||||
},
|
||||
{
|
||||
"id": "draw_bots",
|
||||
"type": "q3.bots.draw",
|
||||
"typeVersion": 1,
|
||||
"position": [1150, 0],
|
||||
"parameters": {
|
||||
"lower_prefix": "lower",
|
||||
"upper_prefix": "upper",
|
||||
"head_prefix": "head",
|
||||
"weapon_prefix": "weapon_mg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "draw_weapon_viewmodel",
|
||||
"type": "q3.md3.draw",
|
||||
"typeVersion": 1,
|
||||
"position": [1160, 0],
|
||||
"parameters": {
|
||||
"prefix": "weapon_mg",
|
||||
"viewmodel": true,
|
||||
"vm_right": 0.25,
|
||||
"vm_down": -0.22,
|
||||
"vm_fwd": 0.45,
|
||||
"fps": 15.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "end_scene",
|
||||
"type": "frame.gpu.end_scene",
|
||||
"typeVersion": 1,
|
||||
"position": [1200, 0]
|
||||
},
|
||||
{
|
||||
"id": "overlay_fps",
|
||||
"type": "overlay.fps",
|
||||
"typeVersion": 1,
|
||||
"position": [1225, 0]
|
||||
},
|
||||
{
|
||||
"id": "q3_overlay",
|
||||
"type": "q3.overlay.draw",
|
||||
"typeVersion": 1,
|
||||
"position": [1235, 0]
|
||||
},
|
||||
{
|
||||
"id": "postfx_taa",
|
||||
"type": "postfx.taa",
|
||||
"typeVersion": 1,
|
||||
"position": [1250, 0],
|
||||
"parameters": { "blend_factor": 0.05 }
|
||||
},
|
||||
{
|
||||
"id": "postfx_ssao",
|
||||
"type": "postfx.ssao",
|
||||
"typeVersion": 1,
|
||||
"position": [1300, 0]
|
||||
},
|
||||
{
|
||||
"id": "bloom_extract",
|
||||
"type": "postfx.bloom_extract",
|
||||
"typeVersion": 1,
|
||||
"position": [1400, 0]
|
||||
},
|
||||
{
|
||||
"id": "bloom_blur",
|
||||
"type": "postfx.bloom_blur",
|
||||
"typeVersion": 1,
|
||||
"position": [1500, 0]
|
||||
},
|
||||
{
|
||||
"id": "postfx_composite",
|
||||
"type": "postfx.composite",
|
||||
"typeVersion": 1,
|
||||
"position": [1600, 0]
|
||||
"id": "postfx_group",
|
||||
"plugin": "workflow.include",
|
||||
"parameters": { "path": "packages/quake3/workflows/q3_postfx.json" }
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"input_poll": { "main": { "0": [{ "node": "q3_menu", "type": "main", "index": 0 }] } },
|
||||
"q3_menu": { "main": { "0": [{ "node": "should_stop", "type": "main", "index": 0 }] } },
|
||||
"should_stop": { "main": { "0": [{ "node": "stop_game_loop", "type": "main", "index": 0 }] } },
|
||||
"stop_game_loop": { "main": { "0": [{ "node": "set_quit_requested", "type": "main", "index": 0 }] } },
|
||||
"set_quit_requested": { "main": { "0": [{ "node": "compute_movement_active", "type": "main", "index": 0 }] } },
|
||||
"compute_movement_active": { "main": { "0": [{ "node": "update_mouse_grab", "type": "main", "index": 0 }] } },
|
||||
"update_mouse_grab": { "main": { "0": [{ "node": "physics_move", "type": "main", "index": 0 }] } },
|
||||
"physics_move": { "main": { "0": [{ "node": "physics_step", "type": "main", "index": 0 }] } },
|
||||
"physics_step": { "main": { "0": [{ "node": "sync_transforms", "type": "main", "index": 0 }] } },
|
||||
"sync_transforms": { "main": { "0": [{ "node": "bsp_entities_update", "type": "main", "index": 0 }] } },
|
||||
"bsp_entities_update": { "main": { "0": [{ "node": "camera_update", "type": "main", "index": 0 }] } },
|
||||
"camera_update": { "main": { "0": [{ "node": "q3_weapon", "type": "main", "index": 0 }] } },
|
||||
"q3_weapon": { "main": { "0": [{ "node": "q3_bots_update", "type": "main", "index": 0 }] } },
|
||||
"q3_bots_update": { "main": { "0": [{ "node": "render_prepare", "type": "main", "index": 0 }] } },
|
||||
"render_prepare": { "main": { "0": [{ "node": "portal_view", "type": "main", "index": 0 }] } },
|
||||
"portal_view": { "main": { "0": [{ "node": "frame_begin", "type": "main", "index": 0 }] } },
|
||||
"frame_begin": { "main": { "0": [{ "node": "draw_map", "type": "main", "index": 0 }] } },
|
||||
"draw_map": { "main": { "0": [{ "node": "draw_pickups", "type": "main", "index": 0 }] } },
|
||||
"draw_pickups": { "main": { "0": [{ "node": "draw_bots", "type": "main", "index": 0 }] } },
|
||||
"draw_bots": { "main": { "0": [{ "node": "draw_weapon_viewmodel", "type": "main", "index": 0 }] } },
|
||||
"draw_weapon_viewmodel": { "main": { "0": [{ "node": "end_scene", "type": "main", "index": 0 }] } },
|
||||
"end_scene": { "main": { "0": [{ "node": "overlay_fps", "type": "main", "index": 0 }] } },
|
||||
"overlay_fps": { "main": { "0": [{ "node": "q3_overlay", "type": "main", "index": 0 }] } },
|
||||
"q3_overlay": { "main": { "0": [{ "node": "postfx_taa", "type": "main", "index": 0 }] } },
|
||||
"postfx_taa": { "main": { "0": [{ "node": "postfx_ssao", "type": "main", "index": 0 }] } },
|
||||
"postfx_ssao": { "main": { "0": [{ "node": "bloom_extract", "type": "main", "index": 0 }] } },
|
||||
"bloom_extract": { "main": { "0": [{ "node": "bloom_blur", "type": "main", "index": 0 }] } },
|
||||
"bloom_blur": { "main": { "0": [{ "node": "postfx_composite", "type": "main", "index": 0 }] } }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "Q3 Input & Menu",
|
||||
"description": "Poll input, update menu state, compute movement gate.",
|
||||
"steps": [
|
||||
{
|
||||
"id": "input_poll",
|
||||
"plugin": "input.poll"
|
||||
},
|
||||
{
|
||||
"id": "q3_menu",
|
||||
"plugin": "q3.menu.update"
|
||||
},
|
||||
{
|
||||
"id": "should_stop",
|
||||
"plugin": "bool.or",
|
||||
"inputs": { "left": "q3.menu_quit_pressed", "right": "q3.menu_map_selected" },
|
||||
"outputs": { "value": "q3.stop_game" }
|
||||
},
|
||||
{
|
||||
"id": "stop_game_loop",
|
||||
"plugin": "value.set_if",
|
||||
"inputs": { "condition": "q3.stop_game" },
|
||||
"parameters": { "value": false },
|
||||
"outputs": { "value": "game_running" }
|
||||
},
|
||||
{
|
||||
"id": "set_quit_requested",
|
||||
"plugin": "value.set_if",
|
||||
"inputs": { "condition": "q3.menu_quit_pressed" },
|
||||
"parameters": { "value": true },
|
||||
"outputs": { "value": "q3.quit_requested" }
|
||||
},
|
||||
{
|
||||
"id": "compute_movement_active",
|
||||
"plugin": "bool.not",
|
||||
"inputs": { "value": "q3.menu_open" },
|
||||
"outputs": { "value": "movement_active" }
|
||||
},
|
||||
{
|
||||
"id": "update_mouse_grab",
|
||||
"plugin": "input.mouse.grab",
|
||||
"inputs": { "enabled": "movement_active" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "Q3 Overlay",
|
||||
"description": "SDL software overlay: FPS counter, HUD, crosshair, hitmarker, menu, map-select. Composable like a React component.",
|
||||
"steps": [
|
||||
{
|
||||
"id": "overlay_fps",
|
||||
"plugin": "overlay.fps"
|
||||
},
|
||||
{
|
||||
"id": "overlay_begin",
|
||||
"plugin": "overlay.sw.begin",
|
||||
"parameters": {
|
||||
"vert_shader_path_msl": "packages/quake3/shaders/msl/overlay.vert.metal",
|
||||
"frag_shader_path_msl": "packages/quake3/shaders/msl/overlay.frag.metal",
|
||||
"vert_shader_path_spirv": "packages/quake3/shaders/spirv/overlay.vert.spv",
|
||||
"frag_shader_path_spirv": "packages/quake3/shaders/spirv/overlay.frag.spv"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q3_hud",
|
||||
"plugin": "q3.hud"
|
||||
},
|
||||
{
|
||||
"id": "q3_crosshair",
|
||||
"plugin": "q3.crosshair"
|
||||
},
|
||||
{
|
||||
"id": "q3_hitmarker",
|
||||
"plugin": "q3.hitmarker"
|
||||
},
|
||||
{
|
||||
"id": "q3_menu_frame",
|
||||
"plugin": "q3.menu.frame"
|
||||
},
|
||||
{
|
||||
"id": "q3_mapselect",
|
||||
"plugin": "q3.mapselect"
|
||||
},
|
||||
{
|
||||
"id": "overlay_end",
|
||||
"plugin": "overlay.sw.end"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "Q3 Physics",
|
||||
"description": "FPS movement, physics step, transform sync, BSP entity update, camera.",
|
||||
"steps": [
|
||||
{
|
||||
"id": "physics_move",
|
||||
"plugin": "physics.fps.move",
|
||||
"parameters": {
|
||||
"move_speed": 6.5,
|
||||
"sprint_multiplier": 1.5,
|
||||
"crouch_multiplier": 0.45,
|
||||
"jump_velocity": 5.5,
|
||||
"air_control": 0.25,
|
||||
"gravity_scale": 0.5,
|
||||
"ground_accel": 30.0,
|
||||
"ground_friction": 24.0,
|
||||
"step_height": 0.6,
|
||||
"crouch_height": 0.5,
|
||||
"stand_height": 1.4
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "physics_step",
|
||||
"plugin": "physics.step"
|
||||
},
|
||||
{
|
||||
"id": "sync_transforms",
|
||||
"plugin": "physics.sync_transforms"
|
||||
},
|
||||
{
|
||||
"id": "bsp_entities_update",
|
||||
"plugin": "bsp.entities.update"
|
||||
},
|
||||
{
|
||||
"id": "camera_update",
|
||||
"plugin": "camera.fps.update",
|
||||
"parameters": {
|
||||
"sensitivity": 0.003,
|
||||
"eye_height": 1.4,
|
||||
"fov": 90.0,
|
||||
"near": 0.1,
|
||||
"far": 500.0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "Q3 Post FX",
|
||||
"description": "TAA, SSAO, bloom extract+blur, composite.",
|
||||
"steps": [
|
||||
{
|
||||
"id": "postfx_taa",
|
||||
"plugin": "postfx.taa",
|
||||
"parameters": { "blend_factor": 0.05 }
|
||||
},
|
||||
{
|
||||
"id": "postfx_ssao",
|
||||
"plugin": "postfx.ssao"
|
||||
},
|
||||
{
|
||||
"id": "bloom_extract",
|
||||
"plugin": "postfx.bloom_extract"
|
||||
},
|
||||
{
|
||||
"id": "bloom_blur",
|
||||
"plugin": "postfx.bloom_blur"
|
||||
},
|
||||
{
|
||||
"id": "postfx_composite",
|
||||
"plugin": "postfx.composite"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "Q3 3D Render",
|
||||
"description": "Prepare render state, cull with portal PVS, GPU frame, draw world + bots + viewmodel.",
|
||||
"steps": [
|
||||
{
|
||||
"id": "render_prepare",
|
||||
"plugin": "render.prepare"
|
||||
},
|
||||
{
|
||||
"id": "portal_view",
|
||||
"plugin": "bsp.portal_view"
|
||||
},
|
||||
{
|
||||
"id": "frame_begin",
|
||||
"plugin": "frame.gpu.begin",
|
||||
"parameters": {
|
||||
"clear_r": 0.3,
|
||||
"clear_g": 0.5,
|
||||
"clear_b": 0.8
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "draw_map",
|
||||
"plugin": "draw.map",
|
||||
"parameters": {
|
||||
"default_texture": "walls_texture",
|
||||
"pipeline_key": "gpu_pipeline_bsp",
|
||||
"roughness": 0.7,
|
||||
"metallic": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "draw_pickups",
|
||||
"plugin": "q3.pickups.draw"
|
||||
},
|
||||
{
|
||||
"id": "draw_bots",
|
||||
"plugin": "q3.bots.draw",
|
||||
"parameters": {
|
||||
"lower_prefix": "lower",
|
||||
"upper_prefix": "upper",
|
||||
"head_prefix": "head",
|
||||
"weapon_prefix": "weapon_mg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "draw_weapon_viewmodel",
|
||||
"plugin": "q3.md3.draw",
|
||||
"parameters": {
|
||||
"prefix": "weapon_mg",
|
||||
"viewmodel": true,
|
||||
"vm_right": 0.25,
|
||||
"vm_down": -0.22,
|
||||
"vm_fwd": 0.45,
|
||||
"fps": 15.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "end_scene",
|
||||
"plugin": "frame.gpu.end_scene"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,311 +1,41 @@
|
||||
{
|
||||
"name": "Q3 Frame Tick",
|
||||
"description": "Per-frame: poll input, FPS move, render BSP map with bots and weapon. Screenshots after 240 frames then exits.",
|
||||
"nodes": [
|
||||
"name": "Q3 Frame Tick (Screenshot)",
|
||||
"description": "Per-frame composition: shared Q3 sub-workflows + screenshot-check component. Exits after frame 240.",
|
||||
"steps": [
|
||||
{
|
||||
"id": "input_poll",
|
||||
"type": "input.poll",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0]
|
||||
"id": "input_group",
|
||||
"plugin": "workflow.include",
|
||||
"parameters": { "path": "packages/quake3/workflows/q3_input.json" }
|
||||
},
|
||||
{
|
||||
"id": "q3_menu",
|
||||
"type": "q3.menu.update",
|
||||
"typeVersion": 1,
|
||||
"position": [100, 0]
|
||||
"id": "physics_group",
|
||||
"plugin": "workflow.include",
|
||||
"parameters": { "path": "packages/quake3/workflows/q3_physics.json" }
|
||||
},
|
||||
{
|
||||
"id": "should_stop",
|
||||
"type": "bool.or",
|
||||
"typeVersion": 1,
|
||||
"position": [140, 0],
|
||||
"inputs": { "left": "q3.menu_quit_pressed", "right": "q3.menu_map_selected" },
|
||||
"outputs": { "value": "q3.stop_game" }
|
||||
"id": "combat_group",
|
||||
"plugin": "workflow.include",
|
||||
"parameters": { "path": "packages/quake3/workflows/q3_combat.json" }
|
||||
},
|
||||
{
|
||||
"id": "stop_game_loop",
|
||||
"type": "value.set_if",
|
||||
"typeVersion": 1,
|
||||
"position": [160, 0],
|
||||
"inputs": { "condition": "q3.stop_game" },
|
||||
"parameters": { "value": false },
|
||||
"outputs": { "value": "game_running" }
|
||||
"id": "render_group",
|
||||
"plugin": "workflow.include",
|
||||
"parameters": { "path": "packages/quake3/workflows/q3_render.json" }
|
||||
},
|
||||
{
|
||||
"id": "set_quit_requested",
|
||||
"type": "value.set_if",
|
||||
"typeVersion": 1,
|
||||
"position": [170, 0],
|
||||
"inputs": { "condition": "q3.menu_quit_pressed" },
|
||||
"parameters": { "value": true },
|
||||
"outputs": { "value": "q3.quit_requested" }
|
||||
"id": "screenshot_group",
|
||||
"plugin": "workflow.include",
|
||||
"parameters": { "path": "packages/quake3_screenshot/workflows/q3_screenshot_check.json" }
|
||||
},
|
||||
{
|
||||
"id": "compute_movement_active",
|
||||
"type": "bool.not",
|
||||
"typeVersion": 1,
|
||||
"position": [190, 0],
|
||||
"inputs": { "value": "q3.menu_open" },
|
||||
"outputs": { "value": "movement_active" }
|
||||
"id": "overlay_group",
|
||||
"plugin": "workflow.include",
|
||||
"parameters": { "path": "packages/quake3/workflows/q3_overlay.json" }
|
||||
},
|
||||
{
|
||||
"id": "update_mouse_grab",
|
||||
"type": "input.mouse.grab",
|
||||
"typeVersion": 1,
|
||||
"position": [195, 0],
|
||||
"inputs": { "enabled": "movement_active" }
|
||||
},
|
||||
{
|
||||
"id": "physics_move",
|
||||
"type": "physics.fps.move",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
"parameters": {
|
||||
"move_speed": 6.5,
|
||||
"sprint_multiplier": 1.5,
|
||||
"crouch_multiplier": 0.45,
|
||||
"jump_velocity": 5.5,
|
||||
"air_control": 0.25,
|
||||
"gravity_scale": 0.5,
|
||||
"ground_accel": 30.0,
|
||||
"ground_friction": 24.0,
|
||||
"step_height": 0.6,
|
||||
"crouch_height": 0.5,
|
||||
"stand_height": 1.4
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "physics_step",
|
||||
"type": "physics.step",
|
||||
"typeVersion": 1,
|
||||
"position": [400, 0]
|
||||
},
|
||||
{
|
||||
"id": "sync_transforms",
|
||||
"type": "physics.sync_transforms",
|
||||
"typeVersion": 1,
|
||||
"position": [500, 0]
|
||||
},
|
||||
{
|
||||
"id": "bsp_entities_update",
|
||||
"type": "bsp.entities.update",
|
||||
"typeVersion": 1,
|
||||
"position": [550, 0]
|
||||
},
|
||||
{
|
||||
"id": "camera_update",
|
||||
"type": "camera.fps.update",
|
||||
"typeVersion": 1,
|
||||
"position": [600, 0],
|
||||
"parameters": {
|
||||
"sensitivity": 0.003,
|
||||
"eye_height": 1.4,
|
||||
"fov": 90.0,
|
||||
"near": 0.1,
|
||||
"far": 500.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q3_weapon",
|
||||
"type": "q3.weapon.update",
|
||||
"typeVersion": 1,
|
||||
"position": [690, 0]
|
||||
},
|
||||
{
|
||||
"id": "q3_bots_update",
|
||||
"type": "q3.bots.update",
|
||||
"typeVersion": 1,
|
||||
"position": [695, 0],
|
||||
"parameters": {
|
||||
"chase_range": 20.0,
|
||||
"shoot_range": 6.0,
|
||||
"move_speed": 3.5,
|
||||
"shoot_interval": 30
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "render_prepare",
|
||||
"type": "render.prepare",
|
||||
"typeVersion": 1,
|
||||
"position": [750, 0]
|
||||
},
|
||||
{
|
||||
"id": "portal_view",
|
||||
"type": "bsp.portal_view",
|
||||
"typeVersion": 1,
|
||||
"position": [775, 0]
|
||||
},
|
||||
{
|
||||
"id": "frame_begin",
|
||||
"type": "frame.gpu.begin",
|
||||
"typeVersion": 1,
|
||||
"position": [800, 0],
|
||||
"parameters": {
|
||||
"clear_r": 0.3,
|
||||
"clear_g": 0.5,
|
||||
"clear_b": 0.8
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "draw_map",
|
||||
"type": "draw.map",
|
||||
"typeVersion": 1,
|
||||
"position": [1000, 0],
|
||||
"parameters": {
|
||||
"default_texture": "walls_texture",
|
||||
"pipeline_key": "gpu_pipeline_bsp",
|
||||
"roughness": 0.7,
|
||||
"metallic": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "draw_pickups",
|
||||
"type": "q3.pickups.draw",
|
||||
"typeVersion": 1,
|
||||
"position": [1100, 0]
|
||||
},
|
||||
{
|
||||
"id": "draw_bots",
|
||||
"type": "q3.bots.draw",
|
||||
"typeVersion": 1,
|
||||
"position": [1150, 0],
|
||||
"parameters": {
|
||||
"lower_prefix": "lower",
|
||||
"upper_prefix": "upper",
|
||||
"head_prefix": "head",
|
||||
"weapon_prefix": "weapon_mg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "draw_weapon_viewmodel",
|
||||
"type": "q3.md3.draw",
|
||||
"typeVersion": 1,
|
||||
"position": [1160, 0],
|
||||
"parameters": {
|
||||
"prefix": "weapon_mg",
|
||||
"viewmodel": true,
|
||||
"vm_right": 0.25,
|
||||
"vm_down": -0.22,
|
||||
"vm_fwd": 0.45,
|
||||
"fps": 15.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "end_scene",
|
||||
"type": "frame.gpu.end_scene",
|
||||
"typeVersion": 1,
|
||||
"position": [1200, 0]
|
||||
},
|
||||
{
|
||||
"id": "overlay_fps",
|
||||
"type": "overlay.fps",
|
||||
"typeVersion": 1,
|
||||
"position": [1225, 0]
|
||||
},
|
||||
{
|
||||
"id": "q3_overlay",
|
||||
"type": "q3.overlay.draw",
|
||||
"typeVersion": 1,
|
||||
"position": [1235, 0]
|
||||
},
|
||||
{
|
||||
"id": "postfx_taa",
|
||||
"type": "postfx.taa",
|
||||
"typeVersion": 1,
|
||||
"position": [1250, 0],
|
||||
"parameters": { "blend_factor": 0.05 }
|
||||
},
|
||||
{
|
||||
"id": "postfx_ssao",
|
||||
"type": "postfx.ssao",
|
||||
"typeVersion": 1,
|
||||
"position": [1300, 0]
|
||||
},
|
||||
{
|
||||
"id": "bloom_extract",
|
||||
"type": "postfx.bloom_extract",
|
||||
"typeVersion": 1,
|
||||
"position": [1400, 0]
|
||||
},
|
||||
{
|
||||
"id": "bloom_blur",
|
||||
"type": "postfx.bloom_blur",
|
||||
"typeVersion": 1,
|
||||
"position": [1500, 0]
|
||||
},
|
||||
{
|
||||
"id": "postfx_composite",
|
||||
"type": "postfx.composite",
|
||||
"typeVersion": 1,
|
||||
"position": [1600, 0]
|
||||
},
|
||||
{
|
||||
"id": "set_threshold",
|
||||
"type": "value.literal",
|
||||
"typeVersion": 1,
|
||||
"position": [1650, 0],
|
||||
"parameters": { "value": 240.0 },
|
||||
"outputs": { "value": "screenshot_threshold" }
|
||||
},
|
||||
{
|
||||
"id": "check_frame_240",
|
||||
"type": "compare.gte",
|
||||
"typeVersion": 1,
|
||||
"position": [1700, 0],
|
||||
"inputs": { "left": "loop.iteration", "right": "screenshot_threshold" },
|
||||
"outputs": { "value": "screenshot_due" }
|
||||
},
|
||||
{
|
||||
"id": "set_screenshot_path",
|
||||
"type": "value.set_if",
|
||||
"typeVersion": 1,
|
||||
"position": [1750, 0],
|
||||
"inputs": { "condition": "screenshot_due" },
|
||||
"parameters": { "value": "/tmp/q3_screenshot.bmp" },
|
||||
"outputs": { "value": "screenshot_output_path" }
|
||||
},
|
||||
{
|
||||
"id": "stop_after_screenshot",
|
||||
"type": "value.set_if",
|
||||
"typeVersion": 1,
|
||||
"position": [1800, 0],
|
||||
"inputs": { "condition": "screenshot_due" },
|
||||
"parameters": { "value": false },
|
||||
"outputs": { "value": "game_running" }
|
||||
"id": "postfx_group",
|
||||
"plugin": "workflow.include",
|
||||
"parameters": { "path": "packages/quake3/workflows/q3_postfx.json" }
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"input_poll": { "main": { "0": [{ "node": "q3_menu", "type": "main", "index": 0 }] } },
|
||||
"q3_menu": { "main": { "0": [{ "node": "should_stop", "type": "main", "index": 0 }] } },
|
||||
"should_stop": { "main": { "0": [{ "node": "stop_game_loop", "type": "main", "index": 0 }] } },
|
||||
"stop_game_loop": { "main": { "0": [{ "node": "set_quit_requested", "type": "main", "index": 0 }] } },
|
||||
"set_quit_requested": { "main": { "0": [{ "node": "compute_movement_active", "type": "main", "index": 0 }] } },
|
||||
"compute_movement_active": { "main": { "0": [{ "node": "update_mouse_grab", "type": "main", "index": 0 }] } },
|
||||
"update_mouse_grab": { "main": { "0": [{ "node": "physics_move", "type": "main", "index": 0 }] } },
|
||||
"physics_move": { "main": { "0": [{ "node": "physics_step", "type": "main", "index": 0 }] } },
|
||||
"physics_step": { "main": { "0": [{ "node": "sync_transforms", "type": "main", "index": 0 }] } },
|
||||
"sync_transforms": { "main": { "0": [{ "node": "bsp_entities_update", "type": "main", "index": 0 }] } },
|
||||
"bsp_entities_update": { "main": { "0": [{ "node": "camera_update", "type": "main", "index": 0 }] } },
|
||||
"camera_update": { "main": { "0": [{ "node": "q3_weapon", "type": "main", "index": 0 }] } },
|
||||
"q3_weapon": { "main": { "0": [{ "node": "q3_bots_update", "type": "main", "index": 0 }] } },
|
||||
"q3_bots_update": { "main": { "0": [{ "node": "render_prepare", "type": "main", "index": 0 }] } },
|
||||
"render_prepare": { "main": { "0": [{ "node": "portal_view", "type": "main", "index": 0 }] } },
|
||||
"portal_view": { "main": { "0": [{ "node": "frame_begin", "type": "main", "index": 0 }] } },
|
||||
"frame_begin": { "main": { "0": [{ "node": "draw_map", "type": "main", "index": 0 }] } },
|
||||
"draw_map": { "main": { "0": [{ "node": "draw_pickups", "type": "main", "index": 0 }] } },
|
||||
"draw_pickups": { "main": { "0": [{ "node": "draw_bots", "type": "main", "index": 0 }] } },
|
||||
"draw_bots": { "main": { "0": [{ "node": "draw_weapon_viewmodel", "type": "main", "index": 0 }] } },
|
||||
"draw_weapon_viewmodel": { "main": { "0": [{ "node": "end_scene", "type": "main", "index": 0 }] } },
|
||||
"end_scene": { "main": { "0": [{ "node": "overlay_fps", "type": "main", "index": 0 }] } },
|
||||
"overlay_fps": { "main": { "0": [{ "node": "q3_overlay", "type": "main", "index": 0 }] } },
|
||||
"q3_overlay": { "main": { "0": [{ "node": "set_threshold", "type": "main", "index": 0 }] } },
|
||||
"set_threshold": { "main": { "0": [{ "node": "check_frame_240", "type": "main", "index": 0 }] } },
|
||||
"check_frame_240": { "main": { "0": [{ "node": "set_screenshot_path", "type": "main", "index": 0 }] } },
|
||||
"set_screenshot_path": { "main": { "0": [{ "node": "stop_after_screenshot", "type": "main", "index": 0 }] } },
|
||||
"stop_after_screenshot": { "main": { "0": [{ "node": "postfx_taa", "type": "main", "index": 0 }] } },
|
||||
"postfx_taa": { "main": { "0": [{ "node": "postfx_ssao", "type": "main", "index": 0 }] } },
|
||||
"postfx_ssao": { "main": { "0": [{ "node": "bloom_extract", "type": "main", "index": 0 }] } },
|
||||
"bloom_extract": { "main": { "0": [{ "node": "bloom_blur", "type": "main", "index": 0 }] } },
|
||||
"bloom_blur": { "main": { "0": [{ "node": "postfx_composite", "type": "main", "index": 0 }] } }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "Q3 Screenshot Check",
|
||||
"description": "After frame 240: write screenshot path, stop the frame loop, and signal quit so the outer loop exits to desktop.",
|
||||
"steps": [
|
||||
{
|
||||
"id": "set_threshold",
|
||||
"plugin": "value.literal",
|
||||
"parameters": { "value": 240.0 },
|
||||
"outputs": { "value": "screenshot_threshold" }
|
||||
},
|
||||
{
|
||||
"id": "check_frame_240",
|
||||
"plugin": "compare.gte",
|
||||
"inputs": { "left": "loop.iteration", "right": "screenshot_threshold" },
|
||||
"outputs": { "value": "screenshot_due" }
|
||||
},
|
||||
{
|
||||
"id": "set_screenshot_path",
|
||||
"plugin": "value.set_if",
|
||||
"inputs": { "condition": "screenshot_due" },
|
||||
"parameters": { "value": "/tmp/q3_screenshot.bmp" },
|
||||
"outputs": { "value": "screenshot_output_path" }
|
||||
},
|
||||
{
|
||||
"id": "stop_after_screenshot",
|
||||
"plugin": "value.set_if",
|
||||
"inputs": { "condition": "screenshot_due" },
|
||||
"parameters": { "value": false },
|
||||
"outputs": { "value": "game_running" }
|
||||
},
|
||||
{
|
||||
"id": "quit_after_screenshot",
|
||||
"plugin": "value.set_if",
|
||||
"inputs": { "condition": "screenshot_due" },
|
||||
"parameters": { "value": true },
|
||||
"outputs": { "value": "q3.quit_requested" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_crosshair_step.hpp"
|
||||
#include "services/interfaces/workflow/quake3/q3_overlay_utils.hpp"
|
||||
#include <SDL3/SDL_render.h>
|
||||
namespace sdl3cpp::services::impl {
|
||||
using namespace q3overlay;
|
||||
WorkflowQ3CrosshairStep::WorkflowQ3CrosshairStep(std::shared_ptr<ILogger> l) : logger_(std::move(l)) {}
|
||||
std::string WorkflowQ3CrosshairStep::GetPluginId() const { return "q3.crosshair"; }
|
||||
void WorkflowQ3CrosshairStep::Execute(const WorkflowStepDefinition&, WorkflowContext& context) {
|
||||
if (!context.GetBool("overlay.ready", false)) return;
|
||||
if (context.GetBool("q3.menu_open", false)) return;
|
||||
auto* r = context.Get<SDL_Renderer*>("overlay.renderer", nullptr);
|
||||
if (!r) return;
|
||||
auto* ch = context.Get<SDL_Texture*>("overlay.tex.crosshair", nullptr);
|
||||
constexpr float kSize = 24.f;
|
||||
const float x = (kW - kSize) * 0.5f, y = (kH - kSize) * 0.5f;
|
||||
if (ch) {
|
||||
SDL_SetTextureAlphaMod(ch, 200); SDL_SetTextureColorMod(ch,255,255,255);
|
||||
SDL_SetTextureBlendMode(ch, SDL_BLENDMODE_BLEND);
|
||||
SDL_FRect dst{x, y, kSize, kSize}; SDL_RenderTexture(r, ch, nullptr, &dst);
|
||||
} else {
|
||||
SDL_SetRenderDrawColor(r, 255, 255, 255, 200);
|
||||
SDL_FRect dot{(kW-4.f)*0.5f,(kH-4.f)*0.5f,4.f,4.f};
|
||||
SDL_RenderFillRect(r, &dot);
|
||||
}
|
||||
}
|
||||
} // namespace sdl3cpp::services::impl
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_hitmarker_step.hpp"
|
||||
#include "services/interfaces/workflow/quake3/q3_overlay_utils.hpp"
|
||||
#include <SDL3/SDL_render.h>
|
||||
#include <cstdint>
|
||||
namespace sdl3cpp::services::impl {
|
||||
using namespace q3overlay;
|
||||
WorkflowQ3HitmarkerStep::WorkflowQ3HitmarkerStep(std::shared_ptr<ILogger> l) : logger_(std::move(l)) {}
|
||||
std::string WorkflowQ3HitmarkerStep::GetPluginId() const { return "q3.hitmarker"; }
|
||||
void WorkflowQ3HitmarkerStep::Execute(const WorkflowStepDefinition&, WorkflowContext& context) {
|
||||
if (!context.GetBool("overlay.ready", false)) return;
|
||||
if (context.GetBool("q3.menu_open", false)) return;
|
||||
auto* r = context.Get<SDL_Renderer*>("overlay.renderer", nullptr);
|
||||
if (!r) return;
|
||||
const auto frame = (uint32_t)context.GetDouble("loop.iteration", 0.0);
|
||||
if (frame < context.Get<uint32_t>("q3.hit_marker_until_frame", 0u)) {
|
||||
SDL_SetRenderDrawColor(r, 255, 80, 55, 255);
|
||||
SDL_RenderLine(r, 308,172,320,160); SDL_RenderLine(r,332,172,320,160);
|
||||
SDL_RenderLine(r, 308,188,320,200); SDL_RenderLine(r,332,188,320,200);
|
||||
}
|
||||
if (frame < context.Get<uint32_t>("q3.weapon_flash_until_frame", 0u)) {
|
||||
SDL_SetRenderDrawColor(r, 255, 200, 60, 180);
|
||||
SDL_FRect flash{kW - 40.f, kH * 0.58f, 24.f, 40.f};
|
||||
SDL_RenderFillRect(r, &flash);
|
||||
}
|
||||
}
|
||||
} // namespace sdl3cpp::services::impl
|
||||
@@ -0,0 +1,71 @@
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_hud_step.hpp"
|
||||
#include "services/interfaces/workflow/quake3/q3_overlay_utils.hpp"
|
||||
#include <SDL3/SDL_render.h>
|
||||
namespace sdl3cpp::services::impl {
|
||||
using namespace q3overlay;
|
||||
|
||||
WorkflowQ3HudStep::WorkflowQ3HudStep(std::shared_ptr<ILogger> l) : logger_(std::move(l)) {}
|
||||
std::string WorkflowQ3HudStep::GetPluginId() const { return "q3.hud"; }
|
||||
|
||||
void WorkflowQ3HudStep::Execute(
|
||||
const WorkflowStepDefinition&, WorkflowContext& context) {
|
||||
if (!context.GetBool("overlay.ready", false)) return;
|
||||
if (context.GetBool("q3.menu_open", false)) return;
|
||||
|
||||
auto* r = context.Get<SDL_Renderer*>("overlay.renderer", nullptr);
|
||||
if (!r) return;
|
||||
|
||||
SDL_Texture* digits[11] = {};
|
||||
for (int i = 0; i < 11; ++i)
|
||||
digits[i] = context.Get<SDL_Texture*>("overlay.tex.num." + std::to_string(i), nullptr);
|
||||
|
||||
auto* iArmor = context.Get<SDL_Texture*>("overlay.tex.icon_armor", nullptr);
|
||||
auto* iFace = context.Get<SDL_Texture*>("overlay.tex.icon_face", nullptr);
|
||||
auto* iWeapon = context.Get<SDL_Texture*>("overlay.tex.icon_weapon", nullptr);
|
||||
|
||||
const int health = context.Get<int>("q3.player_health", 100);
|
||||
const int armor = context.Get<int>("q3.player_armor", 0);
|
||||
const int ammo = context.Get<int>("q3.player_ammo", 50);
|
||||
|
||||
constexpr float kNS = 1.0f; // digit sprite scale → 32×32 px (Q3A native)
|
||||
constexpr float kNH = 32.f * kNS; // digit height = 32 px
|
||||
constexpr float kHudY = kH - kNH - 6.f;
|
||||
|
||||
auto drawIcon = [&](SDL_Texture* t, float x, float y, float w, float h) {
|
||||
if (!t) return;
|
||||
SDL_SetTextureAlphaMod(t, 255); SDL_SetTextureColorMod(t, 255, 255, 255);
|
||||
SDL_SetTextureBlendMode(t, SDL_BLENDMODE_BLEND);
|
||||
SDL_FRect dst{x, y, w, h}; SDL_RenderTexture(r, t, nullptr, &dst);
|
||||
};
|
||||
|
||||
auto digitWidth = [&](int val) -> float {
|
||||
return (val >= 100 ? 3 : val >= 10 ? 2 : 1) * 32.f * kNS;
|
||||
};
|
||||
|
||||
// ── Left cluster: [armor#] [armor icon] ─────────────────────────────────
|
||||
// Real Q3A: armor sits at far left, icon to its right.
|
||||
float cx = 10.f;
|
||||
cx = DrawHudNumber(r, digits, cx, kHudY, armor, kNS);
|
||||
drawIcon(iArmor, cx + 4.f, kHudY, kNH, kNH);
|
||||
|
||||
// ── Center cluster: [health#] [face icon] ───────────────────────────────
|
||||
// Real Q3A: health number + mugshot centered on screen.
|
||||
{
|
||||
const float hNumW = digitWidth(health);
|
||||
const float cluster = hNumW + 4.f + kNH;
|
||||
const float hx = kW * 0.5f - cluster * 0.5f;
|
||||
DrawHudNumber(r, digits, hx, kHudY, health, kNS);
|
||||
drawIcon(iFace, hx + hNumW + 4.f, kHudY, kNH, kNH);
|
||||
}
|
||||
|
||||
// ── Right cluster: [ammo#] [weapon icon] ─────────────────────────────────
|
||||
// Real Q3A: ammo number then weapon icon at far-right edge.
|
||||
{
|
||||
const float wIconW = iWeapon ? kNH : 0.f;
|
||||
const float aNumW = digitWidth(ammo);
|
||||
const float rx = kW - 10.f - wIconW - (wIconW > 0 ? 6.f : 0.f) - aNumW;
|
||||
DrawHudNumber(r, digits, rx, kHudY, ammo, kNS);
|
||||
drawIcon(iWeapon, rx + aNumW + 6.f, kHudY, wIconW, wIconW);
|
||||
}
|
||||
}
|
||||
} // namespace sdl3cpp::services::impl
|
||||
@@ -0,0 +1,117 @@
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_mapselect_step.hpp"
|
||||
#include "services/interfaces/workflow/quake3/q3_overlay_utils.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <SDL3/SDL_render.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
using namespace q3overlay;
|
||||
WorkflowQ3MapSelectStep::WorkflowQ3MapSelectStep(std::shared_ptr<ILogger> l) : logger_(std::move(l)) {}
|
||||
std::string WorkflowQ3MapSelectStep::GetPluginId() const { return "q3.mapselect"; }
|
||||
void WorkflowQ3MapSelectStep::Execute(const WorkflowStepDefinition&, WorkflowContext& context) {
|
||||
if (!context.GetBool("overlay.ready", false)) return;
|
||||
if (!context.GetBool("q3.menu_open", false)) return;
|
||||
if (context.Get<std::string>("q3.menu_screen","main") != "map_select") return;
|
||||
|
||||
auto* r = context.Get<SDL_Renderer*>("overlay.renderer", nullptr);
|
||||
auto* prop = context.Get<SDL_Texture*>("overlay.tex.prop", nullptr);
|
||||
auto* bchars = context.Get<SDL_Texture*>("overlay.tex.bigchars", nullptr);
|
||||
if (!r) return;
|
||||
|
||||
auto arenas = context.Get<std::shared_ptr<ArenaMap>>("overlay.arena_data", nullptr);
|
||||
auto shots = context.Get<std::shared_ptr<LevelshotCache>>("overlay.levelshot_cache", nullptr);
|
||||
const auto bspCfg = context.Get<nlohmann::json>("bsp_config", nlohmann::json{});
|
||||
const std::string pk3 = bspCfg.value("pk3_path", std::string(""));
|
||||
|
||||
// Black background
|
||||
SDL_SetRenderDrawColor(r, 0, 0, 0, 255);
|
||||
SDL_FRect bg{0,0,(float)kW,(float)kH}; SDL_RenderFillRect(r, &bg);
|
||||
|
||||
constexpr float kCX = kW * 0.5f;
|
||||
DrawQ3Text(r, bchars, kCX-96.f, 12.f, "CHOOSE LEVEL", {200,50,20,255}, 1.0f);
|
||||
|
||||
const auto maps = context.Get<nlohmann::json>("q3.maps", nlohmann::json::array({"q3dm7"}));
|
||||
const int sel = context.Get<int>("q3.menu_selected_item", 0);
|
||||
const int nMaps = (int)maps.size();
|
||||
const int idx = (nMaps>0) ? std::max(0,std::min(sel,nMaps-1)) : 0;
|
||||
std::string mapName = (nMaps>0) ? maps[(size_t)idx].get<std::string>() : "q3dm7";
|
||||
|
||||
// Levelshot
|
||||
constexpr float kLW=162.f, kLH=162.f, kLX=(kW-kLW)*0.5f, kLY=58.f;
|
||||
SDL_Texture* shot = nullptr;
|
||||
if (shots && !mapName.empty() && !pk3.empty()) {
|
||||
std::string key = mapName;
|
||||
std::transform(key.begin(),key.end(),key.begin(),::toupper);
|
||||
auto it = shots->find(key);
|
||||
if (it != shots->end()) { shot = it->second; }
|
||||
else {
|
||||
shot = LoadTextureFromPk3(r, pk3, ("levelshots/"+key+".jpg").c_str());
|
||||
if (!shot) shot = LoadTextureFromPk3(r, pk3, ("levelshots/"+mapName+".jpg").c_str());
|
||||
(*shots)[key] = shot;
|
||||
}
|
||||
}
|
||||
if (shot) {
|
||||
SDL_FRect dst{kLX,kLY,kLW,kLH}; SDL_SetTextureAlphaMod(shot,255);
|
||||
SDL_SetTextureBlendMode(shot,SDL_BLENDMODE_NONE);
|
||||
SDL_RenderTexture(r,shot,nullptr,&dst);
|
||||
} else {
|
||||
SDL_SetRenderDrawColor(r,30,20,15,255); SDL_FRect fb{kLX,kLY,kLW,kLH}; SDL_RenderFillRect(r,&fb);
|
||||
}
|
||||
SDL_SetRenderDrawColor(r,180,50,20,255);
|
||||
SDL_FRect border{kLX-2.f,kLY-2.f,kLW+4.f,kLH+4.f}; SDL_RenderRect(r,&border);
|
||||
|
||||
std::string mU=mapName; std::transform(mU.begin(),mU.end(),mU.begin(),::toupper);
|
||||
DrawPropText(r,prop,kCX,kLY+kLH+6.f,mU.c_str(),{255,200,50,255},0.7f,true);
|
||||
|
||||
std::string mKey=mapName; std::transform(mKey.begin(),mKey.end(),mKey.begin(),::tolower);
|
||||
if (arenas) {
|
||||
auto aIt = arenas->find(mKey);
|
||||
if (aIt != arenas->end() && !aIt->second.longname.empty()) {
|
||||
std::string ln = mU+": "+aIt->second.longname;
|
||||
std::transform(ln.begin(),ln.end(),ln.begin(),::toupper);
|
||||
DrawPropText(r,prop,kCX,kLY+kLH+34.f,ln.c_str(),{200,130,40,255},0.75f,true);
|
||||
if (!aIt->second.bot.empty() && shots && !pk3.empty()) {
|
||||
const std::string botKey=aIt->second.bot;
|
||||
std::string bk=botKey; std::transform(bk.begin(),bk.end(),bk.begin(),::tolower);
|
||||
SDL_Texture* botIcon = nullptr;
|
||||
auto bIt = shots->find("bot_"+bk);
|
||||
if (bIt!=shots->end()) { botIcon=bIt->second; }
|
||||
else {
|
||||
botIcon=LoadTextureFromPk3(r,pk3,("models/players/"+bk+"/icon_default.tga").c_str());
|
||||
(*shots)["bot_"+bk]=botIcon;
|
||||
}
|
||||
if (botIcon) {
|
||||
SDL_FRect dst{kCX-22.f,kLY+kLH+76.f,44.f,44.f};
|
||||
SDL_SetTextureAlphaMod(botIcon,255); SDL_SetTextureBlendMode(botIcon,SDL_BLENDMODE_BLEND);
|
||||
SDL_RenderTexture(r,botIcon,nullptr,&dst);
|
||||
}
|
||||
std::string bU=botKey; std::transform(bU.begin(),bU.end(),bU.begin(),::toupper);
|
||||
DrawPropText(r,prop,kCX,kLY+kLH+124.f,bU.c_str(),{180,130,50,220},0.65f,true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nav arrows
|
||||
auto* al = context.Get<SDL_Texture*>("overlay.tex.arrow_l", nullptr);
|
||||
auto* ar = context.Get<SDL_Texture*>("overlay.tex.arrow_r", nullptr);
|
||||
constexpr float kAY = kLY+kLH*0.5f-16.f;
|
||||
if (al && idx>0) { SDL_FRect d{8.f,kAY,64.f,32.f}; SDL_RenderTexture(r,al,nullptr,&d); }
|
||||
if (ar && idx<nMaps-1) { SDL_FRect d{kW-72.f,kAY,64.f,32.f}; SDL_RenderTexture(r,ar,nullptr,&d); }
|
||||
|
||||
// Buttons
|
||||
auto* bb = context.Get<SDL_Texture*>("overlay.tex.btn_back", nullptr);
|
||||
auto* bf = context.Get<SDL_Texture*>("overlay.tex.btn_fight", nullptr);
|
||||
auto* bs = context.Get<SDL_Texture*>("overlay.tex.btn_skirmish", nullptr);
|
||||
constexpr float kBY=(float)kH-52.f, kBW=128.f, kBH=46.f;
|
||||
auto dbtn=[&](SDL_Texture* t,float x){
|
||||
if(!t) return; SDL_SetTextureAlphaMod(t,255); SDL_SetTextureBlendMode(t,SDL_BLENDMODE_BLEND);
|
||||
SDL_FRect d{x,kBY,kBW,kBH}; SDL_RenderTexture(r,t,nullptr,&d);
|
||||
};
|
||||
dbtn(bb, kW*0.12f-kBW*0.5f);
|
||||
dbtn(bs, kW*0.50f-kBW*0.5f);
|
||||
dbtn(bf, kW*0.88f-kBW*0.5f);
|
||||
}
|
||||
} // namespace sdl3cpp::services::impl
|
||||
@@ -0,0 +1,60 @@
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_menu_frame_step.hpp"
|
||||
#include "services/interfaces/workflow/quake3/q3_overlay_utils.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <SDL3/SDL_render.h>
|
||||
namespace sdl3cpp::services::impl {
|
||||
using namespace q3overlay;
|
||||
WorkflowQ3MenuFrameStep::WorkflowQ3MenuFrameStep(std::shared_ptr<ILogger> l) : logger_(std::move(l)) {}
|
||||
std::string WorkflowQ3MenuFrameStep::GetPluginId() const { return "q3.menu.frame"; }
|
||||
void WorkflowQ3MenuFrameStep::Execute(const WorkflowStepDefinition&, WorkflowContext& context) {
|
||||
if (!context.GetBool("overlay.ready", false)) return;
|
||||
if (!context.GetBool("q3.menu_open", false)) return;
|
||||
const std::string screen = context.Get<std::string>("q3.menu_screen", "main");
|
||||
if (screen == "map_select") return; // handled by q3.mapselect
|
||||
|
||||
auto* r = context.Get<SDL_Renderer*>("overlay.renderer", nullptr);
|
||||
auto* prop = context.Get<SDL_Texture*>("overlay.tex.prop", nullptr);
|
||||
auto* glo = context.Get<SDL_Texture*>("overlay.tex.prop_glo", nullptr);
|
||||
auto* fl = context.Get<SDL_Texture*>("overlay.tex.frame_l", nullptr);
|
||||
auto* fr = context.Get<SDL_Texture*>("overlay.tex.frame_r", nullptr);
|
||||
if (!r) return;
|
||||
|
||||
// Black background
|
||||
SDL_SetRenderDrawColor(r, 0, 0, 0, 255);
|
||||
SDL_FRect bg{0,0,(float)kW,(float)kH}; SDL_RenderFillRect(r, &bg);
|
||||
|
||||
// Ring emblem
|
||||
constexpr float kRY=120.f, kRH=185.f, kHalf=kW*0.5f;
|
||||
if (fl) { SDL_FRect d{0.f,kRY,kHalf,kRH}; SDL_SetTextureAlphaMod(fl,255); SDL_RenderTexture(r,fl,nullptr,&d); }
|
||||
if (fr) { SDL_FRect d{kHalf,kRY,kHalf,kRH}; SDL_SetTextureAlphaMod(fr,255); SDL_RenderTexture(r,fr,nullptr,&d); }
|
||||
|
||||
// Title with glow
|
||||
constexpr float kCX=kW*0.5f, kTY=20.f, kTS=1.5f, kSS=1.1f;
|
||||
const float kSY = kTY + kPropHeight*kTS + 2.f;
|
||||
if (glo) {
|
||||
DrawPropText(r,glo,kCX+2.f,kTY+2.f,"QUAKE III",{255,80,0,120},kTS,true);
|
||||
DrawPropText(r,glo,kCX+2.f,kSY+2.f,"ARENA", {255,80,0,120},kSS,true);
|
||||
}
|
||||
DrawPropText(r,prop,kCX,kTY,"QUAKE III",{255,200,50,255},kTS,true);
|
||||
DrawPropText(r,prop,kCX,kSY,"ARENA", {220,100,10,255},kSS,true);
|
||||
|
||||
// Menu items
|
||||
const auto items = context.Get<nlohmann::json>("q3.menu_items", nlohmann::json::array());
|
||||
const int sel = context.Get<int>("q3.menu_selected_item", 0);
|
||||
const int nItems = (int)items.size();
|
||||
constexpr float kIS=1.0f, kIStep=kPropHeight*kIS+8.f, kITop=185.f;
|
||||
for (int i=0; i<nItems; ++i) {
|
||||
const std::string lbl = items[i].value("label","");
|
||||
const float iy = kITop + i*kIStep;
|
||||
if (i==sel) {
|
||||
if (glo) { DrawPropText(r,glo,kCX-1.f,iy,lbl.c_str(),{255,180,0,100},kIS,true);
|
||||
DrawPropText(r,glo,kCX+1.f,iy,lbl.c_str(),{255,180,0,100},kIS,true); }
|
||||
DrawPropText(r,prop,kCX,iy,lbl.c_str(),{255,220,80,255},kIS,true);
|
||||
} else {
|
||||
DrawPropText(r,prop,kCX,iy,lbl.c_str(),{200,55,35,210},kIS,true);
|
||||
}
|
||||
}
|
||||
DrawPropText(r,prop,kCX,(float)(kH-18),
|
||||
"Quake III Arena(c) 1999-2000, Id Software, Inc.",{180,50,30,200},0.42f,true);
|
||||
}
|
||||
} // namespace sdl3cpp::services::impl
|
||||
@@ -1,885 +0,0 @@
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_overlay_draw_step.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <stb_image.h>
|
||||
#include <zip.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
namespace {
|
||||
|
||||
std::vector<uint8_t> LoadBinary(const char* path) {
|
||||
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
||||
if (!f.is_open()) return {};
|
||||
auto size = f.tellg();
|
||||
std::vector<uint8_t> data(static_cast<size_t>(size));
|
||||
f.seekg(0);
|
||||
f.read(reinterpret_cast<char*>(data.data()), size);
|
||||
return data;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Construction / destruction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
WorkflowQ3OverlayDrawStep::WorkflowQ3OverlayDrawStep(std::shared_ptr<ILogger> logger)
|
||||
: logger_(std::move(logger)) {}
|
||||
|
||||
WorkflowQ3OverlayDrawStep::~WorkflowQ3OverlayDrawStep() {
|
||||
if (renderer_) {
|
||||
if (bigchars_tex_) SDL_DestroyTexture(bigchars_tex_);
|
||||
if (prop_font_tex_) SDL_DestroyTexture(prop_font_tex_);
|
||||
if (prop_glo_tex_) SDL_DestroyTexture(prop_glo_tex_);
|
||||
if (frame_bg_tex_) SDL_DestroyTexture(frame_bg_tex_);
|
||||
if (frame_l_tex_) SDL_DestroyTexture(frame_l_tex_);
|
||||
if (frame_r_tex_) SDL_DestroyTexture(frame_r_tex_);
|
||||
if (frame2_l_tex_) SDL_DestroyTexture(frame2_l_tex_);
|
||||
for (auto* t : num_digits_) if (t) SDL_DestroyTexture(t);
|
||||
if (icon_armor_tex_) SDL_DestroyTexture(icon_armor_tex_);
|
||||
if (icon_health_tex_) SDL_DestroyTexture(icon_health_tex_);
|
||||
if (icon_face_tex_) SDL_DestroyTexture(icon_face_tex_);
|
||||
if (icon_ammo_tex_) SDL_DestroyTexture(icon_ammo_tex_);
|
||||
if (icon_crosshair_) SDL_DestroyTexture(icon_crosshair_);
|
||||
if (btn_back_tex_) SDL_DestroyTexture(btn_back_tex_);
|
||||
if (btn_fight_tex_) SDL_DestroyTexture(btn_fight_tex_);
|
||||
if (btn_skirmish_tex_)SDL_DestroyTexture(btn_skirmish_tex_);
|
||||
if (btn_arrow_l_tex_) SDL_DestroyTexture(btn_arrow_l_tex_);
|
||||
if (btn_arrow_r_tex_) SDL_DestroyTexture(btn_arrow_r_tex_);
|
||||
for (auto& [k, v] : levelshot_cache_) if (v) SDL_DestroyTexture(v);
|
||||
SDL_DestroyRenderer(renderer_);
|
||||
}
|
||||
if (surface_) SDL_DestroySurface(surface_);
|
||||
if (device_) {
|
||||
if (sampler_) SDL_ReleaseGPUSampler(device_, sampler_);
|
||||
if (vtx_buf_) SDL_ReleaseGPUBuffer(device_, vtx_buf_);
|
||||
if (transfer_) SDL_ReleaseGPUTransferBuffer(device_, transfer_);
|
||||
if (tex_) SDL_ReleaseGPUTexture(device_, tex_);
|
||||
if (pipeline_) SDL_ReleaseGPUGraphicsPipeline(device_, pipeline_);
|
||||
}
|
||||
}
|
||||
|
||||
std::string WorkflowQ3OverlayDrawStep::GetPluginId() const {
|
||||
return "q3.overlay.draw";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GPU pipeline init (unchanged from before)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void WorkflowQ3OverlayDrawStep::TryInit(SDL_GPUDevice* device, SDL_Window* window) {
|
||||
if (disabled_ || ready_) return;
|
||||
device_ = device;
|
||||
|
||||
const char* driver = SDL_GetGPUDeviceDriver(device);
|
||||
const std::string driverName = driver ? driver : "";
|
||||
SDL_GPUShaderFormat shaderFormat = SDL_GPU_SHADERFORMAT_INVALID;
|
||||
std::vector<uint8_t> vert, frag;
|
||||
const char* entry = "main";
|
||||
if (driverName == "metal") {
|
||||
shaderFormat = SDL_GPU_SHADERFORMAT_MSL;
|
||||
vert = LoadBinary("packages/quake3/shaders/msl/overlay.vert.metal");
|
||||
frag = LoadBinary("packages/quake3/shaders/msl/overlay.frag.metal");
|
||||
entry = "main0";
|
||||
} else if (driverName == "vulkan") {
|
||||
shaderFormat = SDL_GPU_SHADERFORMAT_SPIRV;
|
||||
vert = LoadBinary("packages/quake3/shaders/spirv/overlay.vert.spv");
|
||||
frag = LoadBinary("packages/quake3/shaders/spirv/overlay.frag.spv");
|
||||
} else {
|
||||
disabled_ = true; return;
|
||||
}
|
||||
if (vert.empty() || frag.empty()) { disabled_ = true; return; }
|
||||
|
||||
SDL_GPUShaderCreateInfo vsi = {};
|
||||
vsi.code = vert.data(); vsi.code_size = vert.size();
|
||||
vsi.entrypoint = entry; vsi.format = shaderFormat;
|
||||
vsi.stage = SDL_GPU_SHADERSTAGE_VERTEX;
|
||||
SDL_GPUShaderCreateInfo fsi = {};
|
||||
fsi.code = frag.data(); fsi.code_size = frag.size();
|
||||
fsi.entrypoint = entry; fsi.format = shaderFormat;
|
||||
fsi.stage = SDL_GPU_SHADERSTAGE_FRAGMENT;
|
||||
fsi.num_samplers = 1;
|
||||
auto* vs = SDL_CreateGPUShader(device, &vsi);
|
||||
auto* fs = SDL_CreateGPUShader(device, &fsi);
|
||||
if (!vs || !fs) {
|
||||
if (vs) SDL_ReleaseGPUShader(device, vs);
|
||||
if (fs) SDL_ReleaseGPUShader(device, fs);
|
||||
disabled_ = true; return;
|
||||
}
|
||||
|
||||
SDL_GPUVertexBufferDescription vbd = {};
|
||||
vbd.slot = 0; vbd.pitch = sizeof(float) * 5;
|
||||
vbd.input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX;
|
||||
SDL_GPUVertexAttribute attrs[2] = {};
|
||||
attrs[0] = {0, 0, SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, 0};
|
||||
attrs[1] = {1, 0, SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2, sizeof(float) * 3};
|
||||
SDL_GPUVertexInputState vis = {};
|
||||
vis.vertex_buffer_descriptions = &vbd; vis.num_vertex_buffers = 1;
|
||||
vis.vertex_attributes = attrs; vis.num_vertex_attributes = 2;
|
||||
|
||||
SDL_GPUColorTargetDescription ctd = {};
|
||||
ctd.format = window
|
||||
? SDL_GetGPUSwapchainTextureFormat(device, window)
|
||||
: SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
|
||||
ctd.blend_state.enable_blend = true;
|
||||
ctd.blend_state.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA;
|
||||
ctd.blend_state.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
|
||||
ctd.blend_state.color_blend_op = SDL_GPU_BLENDOP_ADD;
|
||||
ctd.blend_state.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE;
|
||||
ctd.blend_state.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ZERO;
|
||||
ctd.blend_state.alpha_blend_op = SDL_GPU_BLENDOP_ADD;
|
||||
|
||||
SDL_GPUGraphicsPipelineCreateInfo pci = {};
|
||||
pci.vertex_shader = vs; pci.fragment_shader = fs;
|
||||
pci.vertex_input_state = vis;
|
||||
pci.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||
pci.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL;
|
||||
pci.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE;
|
||||
pci.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE;
|
||||
pci.depth_stencil_state.enable_depth_test = false;
|
||||
pci.depth_stencil_state.enable_depth_write = false;
|
||||
pci.target_info.num_color_targets = 1;
|
||||
pci.target_info.color_target_descriptions = &ctd;
|
||||
pipeline_ = SDL_CreateGPUGraphicsPipeline(device, &pci);
|
||||
SDL_ReleaseGPUShader(device, vs);
|
||||
SDL_ReleaseGPUShader(device, fs);
|
||||
if (!pipeline_) { disabled_ = true; return; }
|
||||
|
||||
SDL_GPUTextureCreateInfo tci = {};
|
||||
tci.type = SDL_GPU_TEXTURETYPE_2D;
|
||||
tci.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
|
||||
tci.width = kW; tci.height = kH;
|
||||
tci.layer_count_or_depth = 1; tci.num_levels = 1;
|
||||
tci.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
|
||||
tex_ = SDL_CreateGPUTexture(device, &tci);
|
||||
|
||||
SDL_GPUTransferBufferCreateInfo tbci = {};
|
||||
tbci.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
||||
tbci.size = kW * kH * 4;
|
||||
transfer_ = SDL_CreateGPUTransferBuffer(device, &tbci);
|
||||
|
||||
SDL_GPUBufferCreateInfo bci = {};
|
||||
bci.usage = SDL_GPU_BUFFERUSAGE_VERTEX;
|
||||
bci.size = 6u * 5u * static_cast<uint32_t>(sizeof(float));
|
||||
vtx_buf_ = SDL_CreateGPUBuffer(device, &bci);
|
||||
|
||||
SDL_GPUSamplerCreateInfo sci = {};
|
||||
sci.min_filter = SDL_GPU_FILTER_NEAREST;
|
||||
sci.mag_filter = SDL_GPU_FILTER_NEAREST;
|
||||
sci.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST;
|
||||
sci.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||
sci.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||
sampler_ = SDL_CreateGPUSampler(device, &sci);
|
||||
|
||||
surface_ = SDL_CreateSurface(kW, kH, SDL_PIXELFORMAT_RGBA32);
|
||||
renderer_ = surface_ ? SDL_CreateSoftwareRenderer(surface_) : nullptr;
|
||||
ready_ = tex_ && transfer_ && vtx_buf_ && sampler_ && surface_ && renderer_;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Load a texture from inside a PK3 (zip) archive using stb_image
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
SDL_Texture* WorkflowQ3OverlayDrawStep::LoadTextureFromPk3(
|
||||
const std::string& pk3Path, const char* entry) {
|
||||
if (pk3Path.empty() || !renderer_) return nullptr;
|
||||
|
||||
int zip_err = 0;
|
||||
zip_t* arc = zip_open(pk3Path.c_str(), ZIP_RDONLY, &zip_err);
|
||||
if (!arc) return nullptr;
|
||||
|
||||
zip_stat_t st;
|
||||
if (zip_stat(arc, entry, 0, &st) != 0) { zip_close(arc); return nullptr; }
|
||||
|
||||
std::vector<uint8_t> buf(st.size);
|
||||
zip_file_t* zf = zip_fopen(arc, entry, 0);
|
||||
if (!zf) { zip_close(arc); return nullptr; }
|
||||
zip_fread(zf, buf.data(), st.size);
|
||||
zip_fclose(zf);
|
||||
zip_close(arc);
|
||||
|
||||
int w = 0, h = 0, ch = 0;
|
||||
unsigned char* px = stbi_load_from_memory(buf.data(),
|
||||
static_cast<int>(buf.size()), &w, &h, &ch, 4);
|
||||
if (!px) return nullptr;
|
||||
|
||||
SDL_Surface* surf = SDL_CreateSurfaceFrom(
|
||||
w, h, SDL_PIXELFORMAT_RGBA32, px, w * 4);
|
||||
SDL_Texture* tex = surf ? SDL_CreateTextureFromSurface(renderer_, surf) : nullptr;
|
||||
if (surf) SDL_DestroySurface(surf);
|
||||
stbi_image_free(px);
|
||||
return tex;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lazy-load all menu textures from the same PK3 as the current BSP
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void WorkflowQ3OverlayDrawStep::TryLoadMenuTextures(const std::string& pk3Path) {
|
||||
if (menu_tex_loaded_) return;
|
||||
menu_tex_loaded_ = true; // set early so we don't retry on failure
|
||||
|
||||
bigchars_tex_ = LoadTextureFromPk3(pk3Path, "gfx/2d/bigchars.tga");
|
||||
prop_font_tex_ = LoadTextureFromPk3(pk3Path, "menu/art/font1_prop.tga");
|
||||
prop_glo_tex_ = LoadTextureFromPk3(pk3Path, "menu/art/font1_prop_glo.tga");
|
||||
frame_bg_tex_ = LoadTextureFromPk3(pk3Path, "menu/art/cut_frame.tga");
|
||||
frame_l_tex_ = LoadTextureFromPk3(pk3Path, "menu/art/frame1_l.tga");
|
||||
frame_r_tex_ = LoadTextureFromPk3(pk3Path, "menu/art/frame1_r.tga");
|
||||
frame2_l_tex_ = LoadTextureFromPk3(pk3Path, "menu/art/frame2_l.tga");
|
||||
|
||||
// HUD large-number sprites (gfx/2d/numbers/*_32b.tga, each 32x32)
|
||||
static const char* kDigitNames[11] = {
|
||||
"gfx/2d/numbers/zero_32b.tga", "gfx/2d/numbers/one_32b.tga",
|
||||
"gfx/2d/numbers/two_32b.tga", "gfx/2d/numbers/three_32b.tga",
|
||||
"gfx/2d/numbers/four_32b.tga", "gfx/2d/numbers/five_32b.tga",
|
||||
"gfx/2d/numbers/six_32b.tga", "gfx/2d/numbers/seven_32b.tga",
|
||||
"gfx/2d/numbers/eight_32b.tga", "gfx/2d/numbers/nine_32b.tga",
|
||||
"gfx/2d/numbers/minus_32b.tga"
|
||||
};
|
||||
for (int i = 0; i < 11; ++i)
|
||||
num_digits_[i] = LoadTextureFromPk3(pk3Path, kDigitNames[i]);
|
||||
|
||||
// HUD icon sprites
|
||||
icon_armor_tex_ = LoadTextureFromPk3(pk3Path, "icons/iconr_yellow.tga");
|
||||
icon_health_tex_ = LoadTextureFromPk3(pk3Path, "icons/iconh_red.tga");
|
||||
icon_face_tex_ = LoadTextureFromPk3(pk3Path, "models/players/keel/icon_default.tga");
|
||||
icon_ammo_tex_ = LoadTextureFromPk3(pk3Path, "icons/icona_machinegun.tga");
|
||||
icon_crosshair_ = LoadTextureFromPk3(pk3Path, "gfx/2d/crosshaira.tga");
|
||||
|
||||
// Map-select buttons and nav arrows
|
||||
btn_back_tex_ = LoadTextureFromPk3(pk3Path, "menu/art/back_0.tga");
|
||||
btn_fight_tex_ = LoadTextureFromPk3(pk3Path, "menu/art/fight_0.tga");
|
||||
btn_skirmish_tex_ = LoadTextureFromPk3(pk3Path, "menu/art/skirmish_0.tga");
|
||||
btn_arrow_l_tex_ = LoadTextureFromPk3(pk3Path, "menu/art/gs_arrows_l.tga");
|
||||
btn_arrow_r_tex_ = LoadTextureFromPk3(pk3Path, "menu/art/gs_arrows_r.tga");
|
||||
|
||||
ParseArenas(pk3Path);
|
||||
|
||||
if (logger_) {
|
||||
logger_->Info(std::string("q3.overlay: textures loaded — "
|
||||
"nums:") + (num_digits_[0] ? "ok" : "MISS") +
|
||||
" armor:" + (icon_armor_tex_ ? "ok" : "MISS") +
|
||||
" face:" + (icon_face_tex_ ? "ok" : "MISS") +
|
||||
" fight:" + (btn_fight_tex_ ? "ok" : "MISS") +
|
||||
" arenas:" + std::to_string(arena_data_.size()));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parse scripts/arenas.txt from the PK3 for map long-names and bot opponents
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void WorkflowQ3OverlayDrawStep::ParseArenas(const std::string& pk3Path) {
|
||||
if (arena_data_loaded_) return;
|
||||
arena_data_loaded_ = true;
|
||||
|
||||
int err = 0;
|
||||
zip_t* arc = zip_open(pk3Path.c_str(), ZIP_RDONLY, &err);
|
||||
if (!arc) return;
|
||||
|
||||
zip_stat_t st;
|
||||
if (zip_stat(arc, "scripts/arenas.txt", 0, &st) != 0) { zip_close(arc); return; }
|
||||
std::vector<char> buf(st.size + 1, '\0');
|
||||
zip_file_t* zf = zip_fopen(arc, "scripts/arenas.txt", 0);
|
||||
if (zf) { zip_fread(zf, buf.data(), st.size); zip_fclose(zf); }
|
||||
zip_close(arc);
|
||||
|
||||
// Simple tokenizer: split on '{' '}' and parse key/value pairs
|
||||
std::string text(buf.data());
|
||||
size_t pos = 0;
|
||||
while ((pos = text.find('{', pos)) != std::string::npos) {
|
||||
size_t end = text.find('}', pos);
|
||||
if (end == std::string::npos) break;
|
||||
std::string block = text.substr(pos + 1, end - pos - 1);
|
||||
pos = end + 1;
|
||||
|
||||
// Extract key-value pairs (Q3 config format: key "value")
|
||||
std::string mapName, longName, bots;
|
||||
std::istringstream ss(block);
|
||||
std::string tok;
|
||||
while (ss >> tok) {
|
||||
auto readVal = [&]() -> std::string {
|
||||
std::string val;
|
||||
ss >> std::ws;
|
||||
if (ss.peek() == '"') {
|
||||
ss.get();
|
||||
std::getline(ss, val, '"');
|
||||
} else { ss >> val; }
|
||||
return val;
|
||||
};
|
||||
if (tok == "map") mapName = readVal();
|
||||
else if (tok == "longname") longName = readVal();
|
||||
else if (tok == "bots") bots = readVal();
|
||||
}
|
||||
if (!mapName.empty()) {
|
||||
// Store under lowercase key
|
||||
std::string key = mapName;
|
||||
std::transform(key.begin(), key.end(), key.begin(), ::tolower);
|
||||
// First bot only
|
||||
std::string firstBot = bots.substr(0, bots.find(' '));
|
||||
arena_data_[key] = { longName, firstBot };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Load or retrieve a cached levelshot texture for a map name
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
SDL_Texture* WorkflowQ3OverlayDrawStep::LoadOrGetLevelshot(
|
||||
const std::string& mapName, const std::string& pk3Path) {
|
||||
// Levelshot paths use uppercase names (e.g. levelshots/Q3DM7.jpg)
|
||||
std::string key = mapName;
|
||||
std::transform(key.begin(), key.end(), key.begin(), ::toupper);
|
||||
|
||||
auto it = levelshot_cache_.find(key);
|
||||
if (it != levelshot_cache_.end()) return it->second;
|
||||
|
||||
// Try uppercase then lowercase
|
||||
std::string upperPath = "levelshots/" + key + ".jpg";
|
||||
std::string lowerPath = "levelshots/" + mapName + ".jpg";
|
||||
SDL_Texture* tex = LoadTextureFromPk3(pk3Path, upperPath.c_str());
|
||||
if (!tex) tex = LoadTextureFromPk3(pk3Path, lowerPath.c_str());
|
||||
levelshot_cache_[key] = tex;
|
||||
return tex;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Draw an integer using the number sprites; returns x position after last digit
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
float WorkflowQ3OverlayDrawStep::DrawHudNumber(
|
||||
float x, float y, int value, float scale) {
|
||||
if (!renderer_) return x;
|
||||
// Convert to string; use minus sprite for negative values
|
||||
std::string s;
|
||||
if (value < 0) { s = "-"; s += std::to_string(-value); }
|
||||
else { s = std::to_string(value); }
|
||||
|
||||
const float dw = 32.f * scale;
|
||||
float cx = x;
|
||||
for (char c : s) {
|
||||
int idx = (c == '-') ? 10 : (c - '0');
|
||||
if (idx < 0 || idx > 10) { cx += dw; continue; }
|
||||
if (SDL_Texture* t = num_digits_[idx]) {
|
||||
SDL_SetTextureColorMod(t, 255, 220, 60); // Q3A gold tint
|
||||
SDL_SetTextureAlphaMod(t, 255);
|
||||
SDL_SetTextureBlendMode(t, SDL_BLENDMODE_BLEND);
|
||||
SDL_FRect dst{cx, y, dw, dw};
|
||||
SDL_RenderTexture(renderer_, t, nullptr, &dst);
|
||||
}
|
||||
cx += dw;
|
||||
}
|
||||
return cx;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Q3 bitmap-font text renderer
|
||||
// In bigchars.tga each ASCII character N occupies the cell:
|
||||
// col = N % 16, row = N / 16 (16 columns × 16 rows, each cell 16×16 px)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void WorkflowQ3OverlayDrawStep::DrawQ3Text(
|
||||
float x, float y, const char* text, SDL_Color color, float scale) {
|
||||
if (!text || !renderer_) return;
|
||||
|
||||
if (bigchars_tex_) {
|
||||
SDL_SetTextureColorMod(bigchars_tex_, color.r, color.g, color.b);
|
||||
SDL_SetTextureAlphaMod(bigchars_tex_, color.a);
|
||||
SDL_SetTextureBlendMode(bigchars_tex_, SDL_BLENDMODE_BLEND);
|
||||
|
||||
const float cw = kGlyphSrc * scale;
|
||||
const float ch = kGlyphSrc * scale;
|
||||
float cx = x;
|
||||
for (const char* p = text; *p; ++p) {
|
||||
const int code = static_cast<unsigned char>(*p);
|
||||
SDL_FRect src = { static_cast<float>((code % 16) * kGlyphSrc),
|
||||
static_cast<float>((code / 16) * kGlyphSrc),
|
||||
static_cast<float>(kGlyphSrc),
|
||||
static_cast<float>(kGlyphSrc) };
|
||||
SDL_FRect dst = { cx, y, cw, ch };
|
||||
SDL_RenderTexture(renderer_, bigchars_tex_, &src, &dst);
|
||||
cx += cw;
|
||||
}
|
||||
} else {
|
||||
// Fallback: built-in debug text
|
||||
SDL_SetRenderDrawColor(renderer_, color.r, color.g, color.b, color.a);
|
||||
SDL_RenderDebugText(renderer_, x, y, text);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Q3 proportional font renderer (font1_prop.tga)
|
||||
// Source: ioquake3 code/q3_ui/ui_atoms.c propMap[128][3] = {src_x, src_y, width}
|
||||
// PROP_HEIGHT=27, PROP_GAP_WIDTH=3, PROP_SPACE_WIDTH=8
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static const int kPropMap[128][3] = {
|
||||
{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},
|
||||
{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},
|
||||
{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},
|
||||
{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},
|
||||
// 32 space
|
||||
{0, 0, 8},
|
||||
// 33 ! 34 " 35 # 36 $ 37 % 38 & 39 '
|
||||
{11,122,7},{154,181,14},{55,122,17},{79,122,18},{101,122,23},{153,122,18},{9,93,7},
|
||||
// 40 ( 41 ) 42 * 43 + 44 , 45 - 46 . 47 /
|
||||
{207,122,8},{230,122,9},{177,122,18},{30,152,18},{85,181,7},{34,93,11},{110,181,6},{130,152,14},
|
||||
// 48-57: 0-9
|
||||
{22,64,17},{41,64,12},{58,64,17},{78,64,18},{98,64,19},{120,64,18},{141,64,18},{204,64,16},{162,64,17},{182,64,18},
|
||||
// 58 : 59 ; 60 < 61 = 62 > 63 ? 64 @
|
||||
{59,181,7},{35,181,7},{203,152,14},{56,93,14},{228,152,14},{177,181,18},{28,122,22},
|
||||
// 65-90: A-Z
|
||||
{5,4,18},{27,4,18},{48,4,18},{69,4,17},{90,4,13},{106,4,13},{121,4,18},{143,4,17},
|
||||
{164,4,8},{175,4,16},{195,4,18},{216,4,12},{230,4,23},{6,34,18},{27,34,18},{48,34,18},
|
||||
{68,34,18},{90,34,17},{110,34,18},{130,34,14},{146,34,18},{166,34,19},{185,34,29},
|
||||
{215,34,18},{234,34,18},{5,64,14},
|
||||
// 91 [ 92 \ 93 ] 94 ^ 95 _ 96 `
|
||||
{60,152,7},{106,151,13},{83,152,7},{128,122,17},{4,152,21},{134,181,5},
|
||||
// 97-122: a-z (map to uppercase in prop font)
|
||||
{5,4,18},{27,4,18},{48,4,18},{69,4,17},{90,4,13},{106,4,13},{121,4,18},{143,4,17},
|
||||
{164,4,8},{175,4,16},{195,4,18},{216,4,12},{230,4,23},{6,34,18},{27,34,18},{48,34,18},
|
||||
{68,34,18},{90,34,17},{110,34,18},{130,34,14},{146,34,18},{166,34,19},{185,34,29},
|
||||
{215,34,18},{234,34,18},{5,64,14},
|
||||
// 123 { 124 | 125 } 126 ~ 127 DEL
|
||||
{153,152,13},{11,181,5},{180,152,13},{79,93,17},{0,0,-1}
|
||||
};
|
||||
|
||||
float WorkflowQ3OverlayDrawStep::PropStringWidth(const char* text) const {
|
||||
if (!text) return 0.f;
|
||||
float w = 0.f;
|
||||
for (const char* p = text; *p; ++p) {
|
||||
const int ch = static_cast<unsigned char>(*p) & 127;
|
||||
const int cw = kPropMap[ch][2];
|
||||
if (cw == -1) continue;
|
||||
w += static_cast<float>(cw == 8 ? 8 : cw + kPropGap);
|
||||
}
|
||||
return w - kPropGap; // no trailing gap
|
||||
}
|
||||
|
||||
void WorkflowQ3OverlayDrawStep::DrawPropText(
|
||||
float x, float y, const char* text, SDL_Color color, float scale, bool center) {
|
||||
if (!text || !renderer_) return;
|
||||
|
||||
SDL_Texture* fnt = prop_font_tex_ ? prop_font_tex_ : nullptr;
|
||||
if (!fnt) {
|
||||
// Fallback to debug text if prop font failed to load
|
||||
SDL_SetRenderDrawColor(renderer_, color.r, color.g, color.b, color.a);
|
||||
SDL_RenderDebugText(renderer_, x, y, text);
|
||||
return;
|
||||
}
|
||||
|
||||
if (center)
|
||||
x -= PropStringWidth(text) * scale * 0.5f;
|
||||
|
||||
SDL_SetTextureColorMod(fnt, color.r, color.g, color.b);
|
||||
SDL_SetTextureAlphaMod(fnt, color.a);
|
||||
SDL_SetTextureBlendMode(fnt, SDL_BLENDMODE_BLEND);
|
||||
|
||||
float cx = x;
|
||||
for (const char* p = text; *p; ++p) {
|
||||
const int ch = static_cast<unsigned char>(*p) & 127;
|
||||
const int cw = kPropMap[ch][2];
|
||||
if (cw == -1) continue;
|
||||
if (cw == kPropSpace) { cx += kPropSpace * scale; continue; }
|
||||
SDL_FRect src = { static_cast<float>(kPropMap[ch][0]),
|
||||
static_cast<float>(kPropMap[ch][1]),
|
||||
static_cast<float>(cw),
|
||||
static_cast<float>(kPropHeight) };
|
||||
SDL_FRect dst = { cx, y, cw * scale, kPropHeight * scale };
|
||||
SDL_RenderTexture(renderer_, fnt, &src, &dst);
|
||||
cx += (cw + kPropGap) * scale;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Q3A "Choose Level" / map-select screen
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void WorkflowQ3OverlayDrawStep::DrawMapSelectScreen(
|
||||
WorkflowContext& context, const std::string& pk3Path) {
|
||||
constexpr float kCX = kW * 0.5f;
|
||||
|
||||
// ── "CHOOSE LEVEL" title ─────────────────────────────────────────────
|
||||
// Q3A uses bigchars font at large scale for this heading.
|
||||
DrawQ3Text(kCX - 96.f, 12.f, "CHOOSE LEVEL", {200, 50, 20, 255}, 1.0f);
|
||||
|
||||
// ── Determine which map is selected and load its levelshot ───────────
|
||||
const auto maps = context.Get<nlohmann::json>("q3.maps", nlohmann::json::array({"q3dm7"}));
|
||||
const int sel = context.Get<int>("q3.menu_selected_item", 0);
|
||||
const int nMaps = static_cast<int>(maps.size());
|
||||
const int idx = (nMaps > 0) ? std::max(0, std::min(sel, nMaps-1)) : 0;
|
||||
|
||||
std::string mapName;
|
||||
if (nMaps > 0) mapName = maps[(size_t)idx].get<std::string>();
|
||||
|
||||
// Levelshot image — centred, bordered in Q3A orange-red
|
||||
constexpr float kLW = 162.f, kLH = 162.f;
|
||||
constexpr float kLX = (kW - kLW) * 0.5f;
|
||||
constexpr float kLY = 58.f;
|
||||
|
||||
SDL_Texture* shot = (!mapName.empty() && !pk3Path.empty())
|
||||
? LoadOrGetLevelshot(mapName, pk3Path) : nullptr;
|
||||
|
||||
if (shot) {
|
||||
SDL_FRect dst{kLX, kLY, kLW, kLH};
|
||||
SDL_SetTextureAlphaMod(shot, 255);
|
||||
SDL_SetTextureColorMod(shot, 255, 255, 255);
|
||||
SDL_SetTextureBlendMode(shot, SDL_BLENDMODE_NONE);
|
||||
SDL_RenderTexture(renderer_, shot, nullptr, &dst);
|
||||
} else {
|
||||
SDL_SetRenderDrawColor(renderer_, 30, 20, 15, 255);
|
||||
SDL_FRect fbk{kLX, kLY, kLW, kLH};
|
||||
SDL_RenderFillRect(renderer_, &fbk);
|
||||
}
|
||||
// Red border
|
||||
SDL_SetRenderDrawColor(renderer_, 180, 50, 20, 255);
|
||||
SDL_FRect border{kLX - 2.f, kLY - 2.f, kLW + 4.f, kLH + 4.f};
|
||||
SDL_RenderRect(renderer_, &border);
|
||||
|
||||
// Map name label under image (e.g. "Q3DM7")
|
||||
std::string mapUpper = mapName;
|
||||
std::transform(mapUpper.begin(), mapUpper.end(), mapUpper.begin(), ::toupper);
|
||||
DrawPropText(kCX, kLY + kLH + 6.f, mapUpper.c_str(), {255, 200, 50, 255}, 0.7f, true);
|
||||
|
||||
// Long name from arenas.txt (e.g. "TEMPLE OF RETRIBUTION")
|
||||
std::string mapKey = mapName;
|
||||
std::transform(mapKey.begin(), mapKey.end(), mapKey.begin(), ::tolower);
|
||||
auto aIt = arena_data_.find(mapKey);
|
||||
if (aIt != arena_data_.end() && !aIt->second.longname.empty()) {
|
||||
std::string longUpper = mapUpper + ": " + aIt->second.longname;
|
||||
std::transform(longUpper.begin(), longUpper.end(), longUpper.begin(), ::toupper);
|
||||
DrawPropText(kCX, kLY + kLH + 34.f, longUpper.c_str(), {200, 130, 40, 255}, 0.75f, true);
|
||||
}
|
||||
|
||||
// ── Opponent bot icon + name ─────────────────────────────────────────
|
||||
float botY = kLY + kLH + 76.f;
|
||||
if (aIt != arena_data_.end() && !aIt->second.bot.empty()) {
|
||||
const std::string botName = aIt->second.bot;
|
||||
std::string botKey = botName;
|
||||
std::transform(botKey.begin(), botKey.end(), botKey.begin(), ::tolower);
|
||||
const std::string iconPath = "models/players/" + botKey + "/icon_default.tga";
|
||||
SDL_Texture* botIcon = LoadOrGetLevelshot("bot_" + botKey, pk3Path);
|
||||
if (!botIcon) {
|
||||
// Try loading from pk3 directly (not a levelshot — use raw loader)
|
||||
botIcon = LoadTextureFromPk3(pk3Path, iconPath.c_str());
|
||||
if (botIcon) levelshot_cache_["bot_" + botKey] = botIcon;
|
||||
}
|
||||
if (botIcon) {
|
||||
SDL_FRect dst{kCX - 22.f, botY, 44.f, 44.f};
|
||||
SDL_SetTextureAlphaMod(botIcon, 255);
|
||||
SDL_SetTextureColorMod(botIcon, 255, 255, 255);
|
||||
SDL_SetTextureBlendMode(botIcon, SDL_BLENDMODE_BLEND);
|
||||
SDL_RenderTexture(renderer_, botIcon, nullptr, &dst);
|
||||
}
|
||||
std::string botUpper = botName;
|
||||
std::transform(botUpper.begin(), botUpper.end(), botUpper.begin(), ::toupper);
|
||||
DrawPropText(kCX, botY + 48.f, botUpper.c_str(), {180, 130, 50, 220}, 0.65f, true);
|
||||
botY += 80.f;
|
||||
}
|
||||
|
||||
// ── Navigation arrows (left / right) ─────────────────────────────────
|
||||
constexpr float kArrowY = kLY + kLH * 0.5f - 16.f;
|
||||
if (btn_arrow_l_tex_ && idx > 0) {
|
||||
SDL_FRect dst{8.f, kArrowY, 64.f, 32.f};
|
||||
SDL_RenderTexture(renderer_, btn_arrow_l_tex_, nullptr, &dst);
|
||||
}
|
||||
if (btn_arrow_r_tex_ && idx < nMaps - 1) {
|
||||
SDL_FRect dst{kW - 72.f, kArrowY, 64.f, 32.f};
|
||||
SDL_RenderTexture(renderer_, btn_arrow_r_tex_, nullptr, &dst);
|
||||
}
|
||||
|
||||
// ── Bottom buttons: BACK SKIRMISH FIGHT ────────────────────────────
|
||||
constexpr float kBtnY = static_cast<float>(kH) - 52.f;
|
||||
constexpr float kBtnW = 128.f, kBtnH = 46.f;
|
||||
auto drawBtn = [&](SDL_Texture* t, float x) {
|
||||
if (!t) return;
|
||||
SDL_SetTextureAlphaMod(t, 255);
|
||||
SDL_SetTextureColorMod(t, 255, 255, 255);
|
||||
SDL_SetTextureBlendMode(t, SDL_BLENDMODE_BLEND);
|
||||
SDL_FRect dst{x, kBtnY, kBtnW, kBtnH};
|
||||
SDL_RenderTexture(renderer_, t, nullptr, &dst);
|
||||
};
|
||||
drawBtn(btn_back_tex_, kW * 0.12f - kBtnW * 0.5f);
|
||||
drawBtn(btn_skirmish_tex_, kW * 0.50f - kBtnW * 0.5f);
|
||||
drawBtn(btn_fight_tex_, kW * 0.88f - kBtnW * 0.5f);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Software-render the overlay surface each frame
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void WorkflowQ3OverlayDrawStep::DrawSurface(
|
||||
WorkflowContext& context, uint32_t /*frameW*/, uint32_t /*frameH*/) {
|
||||
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 0);
|
||||
SDL_RenderClear(renderer_);
|
||||
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
|
||||
|
||||
// ---- HUD (always visible unless menu is open) -----------------------
|
||||
const bool menuOpen = context.GetBool("q3.menu_open", false);
|
||||
|
||||
if (!menuOpen) {
|
||||
const auto bspCfg2 = context.Get<nlohmann::json>("bsp_config", nlohmann::json{});
|
||||
const std::string pk3hud = bspCfg2.value("pk3_path", std::string(""));
|
||||
if (!menu_tex_loaded_ && !pk3hud.empty()) TryLoadMenuTextures(pk3hud);
|
||||
|
||||
const int health = context.Get<int>("q3.player_health", 100);
|
||||
const int armor = context.Get<int>("q3.player_armor", 0);
|
||||
const int ammo = context.Get<int>("q3.player_ammo", 50);
|
||||
|
||||
// ── Large number HUD — bottom-left (matches Q3A layout) ────────────
|
||||
// Scale 1.75 → each digit 56×56 px. Bottom edge at kH-6.
|
||||
constexpr float kNS = 1.75f; // number sprite scale
|
||||
constexpr float kNH = 32.f * kNS; // digit height = 56 px
|
||||
constexpr float kHudY = kH - kNH - 6.f;
|
||||
constexpr float kIS = 1.25f; // icon scale: 32×1.25 = 40 px
|
||||
|
||||
// Armor value (left)
|
||||
float cx = 10.f;
|
||||
cx = DrawHudNumber(cx, kHudY, armor, kNS);
|
||||
// Armor icon immediately after digits
|
||||
if (icon_armor_tex_) {
|
||||
SDL_SetTextureAlphaMod(icon_armor_tex_, 255);
|
||||
SDL_SetTextureColorMod(icon_armor_tex_, 255, 255, 255);
|
||||
SDL_SetTextureBlendMode(icon_armor_tex_, SDL_BLENDMODE_BLEND);
|
||||
SDL_FRect dst{cx + 4.f, kHudY + (kNH - 32.f*kIS)*0.5f, 32.f*kIS, 32.f*kIS};
|
||||
SDL_RenderTexture(renderer_, icon_armor_tex_, nullptr, &dst);
|
||||
}
|
||||
cx += 4.f + 32.f*kIS + 14.f;
|
||||
|
||||
// Health value
|
||||
cx = DrawHudNumber(cx, kHudY, health, kNS);
|
||||
// Face icon after health digits
|
||||
if (icon_face_tex_) {
|
||||
SDL_SetTextureAlphaMod(icon_face_tex_, 255);
|
||||
SDL_SetTextureColorMod(icon_face_tex_, 255, 255, 255);
|
||||
SDL_SetTextureBlendMode(icon_face_tex_, SDL_BLENDMODE_BLEND);
|
||||
SDL_FRect dst{cx + 4.f, kHudY, kNH, kNH}; // face is 64×64 → scaled to digit height
|
||||
SDL_RenderTexture(renderer_, icon_face_tex_, nullptr, &dst);
|
||||
}
|
||||
|
||||
// ── Ammo / weapon — bottom-right ────────────────────────────────────
|
||||
// Show ammo icon + ammo count right-aligned at kW-10
|
||||
const float ammoNumW = (ammo >= 100 ? 3 : ammo >= 10 ? 2 : 1) * 32.f * kNS;
|
||||
const float ammoIconW = icon_ammo_tex_ ? 32.f * kIS : 0.f;
|
||||
const float ammoX = kW - 10.f - ammoNumW - ammoIconW - 8.f;
|
||||
if (icon_ammo_tex_) {
|
||||
SDL_SetTextureAlphaMod(icon_ammo_tex_, 255);
|
||||
SDL_SetTextureColorMod(icon_ammo_tex_, 255, 255, 255);
|
||||
SDL_SetTextureBlendMode(icon_ammo_tex_, SDL_BLENDMODE_BLEND);
|
||||
SDL_FRect dst{ammoX, kHudY + (kNH - 32.f*kIS)*0.5f, 32.f*kIS, 32.f*kIS};
|
||||
SDL_RenderTexture(renderer_, icon_ammo_tex_, nullptr, &dst);
|
||||
}
|
||||
DrawHudNumber(ammoX + ammoIconW + 6.f, kHudY, ammo, kNS);
|
||||
|
||||
// ── Crosshair ────────────────────────────────────────────────────────
|
||||
constexpr float kCHSize = 24.f;
|
||||
const float chx = (kW - kCHSize) * 0.5f;
|
||||
const float chy = (kH - kCHSize) * 0.5f;
|
||||
if (icon_crosshair_) {
|
||||
SDL_SetTextureAlphaMod(icon_crosshair_, 200);
|
||||
SDL_SetTextureColorMod(icon_crosshair_, 255, 255, 255);
|
||||
SDL_SetTextureBlendMode(icon_crosshair_, SDL_BLENDMODE_BLEND);
|
||||
SDL_FRect dst{chx, chy, kCHSize, kCHSize};
|
||||
SDL_RenderTexture(renderer_, icon_crosshair_, nullptr, &dst);
|
||||
} else {
|
||||
// Fallback dot crosshair
|
||||
SDL_SetRenderDrawColor(renderer_, 255, 255, 255, 200);
|
||||
SDL_FRect dot{(kW - 4.f) * 0.5f, (kH - 4.f) * 0.5f, 4.f, 4.f};
|
||||
SDL_RenderFillRect(renderer_, &dot);
|
||||
}
|
||||
|
||||
// ── Hit marker ───────────────────────────────────────────────────────
|
||||
const uint32_t frame2 = static_cast<uint32_t>(context.GetDouble("loop.iteration", 0.0));
|
||||
if (frame2 < context.Get<uint32_t>("q3.hit_marker_until_frame", 0u)) {
|
||||
SDL_SetRenderDrawColor(renderer_, 255, 80, 55, 255);
|
||||
SDL_RenderLine(renderer_, 308, 172, 320, 160);
|
||||
SDL_RenderLine(renderer_, 332, 172, 320, 160);
|
||||
SDL_RenderLine(renderer_, 308, 188, 320, 200);
|
||||
SDL_RenderLine(renderer_, 332, 188, 320, 200);
|
||||
}
|
||||
|
||||
// ── Muzzle flash ─────────────────────────────────────────────────────
|
||||
const uint32_t frame3 = static_cast<uint32_t>(context.GetDouble("loop.iteration", 0.0));
|
||||
if (frame3 < context.Get<uint32_t>("q3.weapon_flash_until_frame", 0u)) {
|
||||
SDL_SetRenderDrawColor(renderer_, 255, 200, 60, 180);
|
||||
SDL_FRect flash{kW - 40.f, kH * 0.58f, 24.f, 40.f};
|
||||
SDL_RenderFillRect(renderer_, &flash);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Menu screens (full-screen overlays) --------------------------------
|
||||
if (menuOpen) {
|
||||
const auto bspCfg = context.Get<nlohmann::json>("bsp_config", nlohmann::json{});
|
||||
const std::string pk3 = bspCfg.value("pk3_path", std::string(""));
|
||||
if (!menu_tex_loaded_ && !pk3.empty()) TryLoadMenuTextures(pk3);
|
||||
|
||||
const std::string screen = context.Get<std::string>("q3.menu_screen", "main");
|
||||
|
||||
// Black background for all menu screens
|
||||
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255);
|
||||
SDL_FRect bg{0, 0, static_cast<float>(kW), static_cast<float>(kH)};
|
||||
SDL_RenderFillRect(renderer_, &bg);
|
||||
|
||||
if (screen == "map_select") {
|
||||
DrawMapSelectScreen(context, pk3);
|
||||
} else {
|
||||
// ── Q3A main menu (ring + title + item list) ──────────────────
|
||||
constexpr float kRingY = 120.f, kRingH = 185.f, kHalf = kW * 0.5f;
|
||||
if (frame_l_tex_) {
|
||||
SDL_SetTextureAlphaMod(frame_l_tex_, 255);
|
||||
SDL_FRect d{0.f, kRingY, kHalf, kRingH};
|
||||
SDL_RenderTexture(renderer_, frame_l_tex_, nullptr, &d);
|
||||
}
|
||||
if (frame_r_tex_) {
|
||||
SDL_SetTextureAlphaMod(frame_r_tex_, 255);
|
||||
SDL_FRect d{kHalf, kRingY, kHalf, kRingH};
|
||||
SDL_RenderTexture(renderer_, frame_r_tex_, nullptr, &d);
|
||||
}
|
||||
|
||||
constexpr float kCX = kW * 0.5f;
|
||||
constexpr float kTY = 20.f, kTS = 1.5f, kSS = 1.1f;
|
||||
const float kSY = kTY + kPropHeight * kTS + 2.f;
|
||||
if (prop_glo_tex_) {
|
||||
SDL_Texture* sv = prop_font_tex_; prop_font_tex_ = prop_glo_tex_;
|
||||
DrawPropText(kCX+2.f, kTY+2.f, "QUAKE III", {255,80,0,120}, kTS, true);
|
||||
DrawPropText(kCX+2.f, kSY+2.f, "ARENA", {255,80,0,120}, kSS, true);
|
||||
prop_font_tex_ = sv;
|
||||
}
|
||||
DrawPropText(kCX, kTY, "QUAKE III", {255,200,50,255}, kTS, true);
|
||||
DrawPropText(kCX, kSY, "ARENA", {220,100,10,255}, kSS, true);
|
||||
|
||||
const auto items = context.Get<nlohmann::json>("q3.menu_items", nlohmann::json::array());
|
||||
const int sel = context.Get<int>("q3.menu_selected_item", 0);
|
||||
const int nItems = static_cast<int>(items.size());
|
||||
constexpr float kIS = 1.0f, kIStep = kPropHeight * kIS + 8.f, kITop = 185.f;
|
||||
|
||||
for (int i = 0; i < nItems; ++i) {
|
||||
const std::string lbl = items[i].value("label", "");
|
||||
const float iy = kITop + i * kIStep;
|
||||
if (i == sel) {
|
||||
if (prop_glo_tex_) {
|
||||
SDL_Texture* sv = prop_font_tex_; prop_font_tex_ = prop_glo_tex_;
|
||||
for (float ox : {-1.f, 1.f})
|
||||
DrawPropText(kCX+ox, iy, lbl.c_str(), {255,180,0,100}, kIS, true);
|
||||
prop_font_tex_ = sv;
|
||||
}
|
||||
DrawPropText(kCX, iy, lbl.c_str(), {255,220,80,255}, kIS, true);
|
||||
} else {
|
||||
DrawPropText(kCX, iy, lbl.c_str(), {200,55,35,210}, kIS, true);
|
||||
}
|
||||
}
|
||||
DrawPropText(kCX, static_cast<float>(kH-18),
|
||||
"Quake III Arena(c) 1999-2000, Id Software, Inc.",
|
||||
{180,50,30,200}, 0.42f, true);
|
||||
}
|
||||
}
|
||||
|
||||
SDL_RenderPresent(renderer_);
|
||||
|
||||
// ---- Screenshot (CPU path: surface is already in RAM) --------------------
|
||||
const auto* ssPath = context.TryGet<std::string>("screenshot_output_path");
|
||||
if (ssPath && !ssPath->empty() && surface_) {
|
||||
if (SDL_SaveBMP(surface_, ssPath->c_str()) == true) {
|
||||
if (logger_) logger_->Info("q3.overlay: screenshot saved to " + *ssPath);
|
||||
context.Set<bool>("screenshot_saved", true);
|
||||
} else {
|
||||
if (logger_) logger_->Warn("q3.overlay: SDL_SaveBMP failed for " + *ssPath);
|
||||
context.Set<bool>("screenshot_saved", false);
|
||||
}
|
||||
// Clear the path so we only save once
|
||||
context.Set<std::string>("screenshot_output_path", std::string(""));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Upload surface → GPU texture → full-screen quad
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void WorkflowQ3OverlayDrawStep::Render(
|
||||
SDL_GPUCommandBuffer* cmd, SDL_GPUTexture* swapchainTex,
|
||||
SDL_GPUDevice* device, uint32_t /*frameW*/, uint32_t /*frameH*/) {
|
||||
void* mapped = SDL_MapGPUTransferBuffer(device, transfer_, false);
|
||||
if (!mapped) return;
|
||||
std::memcpy(mapped, surface_->pixels, kW * kH * 4);
|
||||
SDL_UnmapGPUTransferBuffer(device, transfer_);
|
||||
|
||||
auto* copy = SDL_BeginGPUCopyPass(cmd);
|
||||
if (copy) {
|
||||
SDL_GPUTextureTransferInfo src = {};
|
||||
src.transfer_buffer = transfer_;
|
||||
src.pixels_per_row = kW;
|
||||
src.rows_per_layer = kH;
|
||||
SDL_GPUTextureRegion dst = {};
|
||||
dst.texture = tex_; dst.w = kW; dst.h = kH; dst.d = 1;
|
||||
SDL_UploadToGPUTexture(copy, &src, &dst, false);
|
||||
SDL_EndGPUCopyPass(copy);
|
||||
}
|
||||
|
||||
if (!vbuf_uploaded_) {
|
||||
const float verts[6][5] = {
|
||||
{-1, 1,0,0,0},{1,1,0,1,0},{1,-1,0,1,1},
|
||||
{-1,1,0,0,0},{1,-1,0,1,1},{-1,-1,0,0,1},
|
||||
};
|
||||
const uint32_t sz = sizeof(verts);
|
||||
SDL_GPUTransferBufferCreateInfo tb = {};
|
||||
tb.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; tb.size = sz;
|
||||
auto* tmp = SDL_CreateGPUTransferBuffer(device, &tb);
|
||||
if (tmp) {
|
||||
void* ptr = SDL_MapGPUTransferBuffer(device, tmp, false);
|
||||
if (ptr) { std::memcpy(ptr, verts, sz); SDL_UnmapGPUTransferBuffer(device, tmp); }
|
||||
auto* cp = SDL_BeginGPUCopyPass(cmd);
|
||||
if (cp) {
|
||||
SDL_GPUTransferBufferLocation s = {tmp, 0};
|
||||
SDL_GPUBufferRegion d = {vtx_buf_, 0, sz};
|
||||
SDL_UploadToGPUBuffer(cp, &s, &d, false);
|
||||
SDL_EndGPUCopyPass(cp);
|
||||
}
|
||||
SDL_ReleaseGPUTransferBuffer(device, tmp);
|
||||
}
|
||||
vbuf_uploaded_ = true;
|
||||
}
|
||||
|
||||
SDL_GPUColorTargetInfo target = {};
|
||||
target.texture = swapchainTex;
|
||||
target.load_op = SDL_GPU_LOADOP_LOAD;
|
||||
target.store_op = SDL_GPU_STOREOP_STORE;
|
||||
auto* pass = SDL_BeginGPURenderPass(cmd, &target, 1, nullptr);
|
||||
if (!pass) return;
|
||||
SDL_BindGPUGraphicsPipeline(pass, pipeline_);
|
||||
SDL_GPUBufferBinding vb = {vtx_buf_, 0};
|
||||
SDL_BindGPUVertexBuffers(pass, 0, &vb, 1);
|
||||
SDL_GPUTextureSamplerBinding ts = {tex_, sampler_};
|
||||
SDL_BindGPUFragmentSamplers(pass, 0, &ts, 1);
|
||||
SDL_DrawGPUPrimitives(pass, 6, 1, 0, 0);
|
||||
SDL_EndGPURenderPass(pass);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void WorkflowQ3OverlayDrawStep::Execute(
|
||||
const WorkflowStepDefinition& step, WorkflowContext& context) {
|
||||
if (context.GetBool("frame_skip", false)) return;
|
||||
auto* cmd = context.Get<SDL_GPUCommandBuffer*>("gpu_command_buffer", nullptr);
|
||||
auto* swapchain = context.Get<SDL_GPUTexture*>("gpu_swapchain_texture", nullptr);
|
||||
auto* device = context.Get<SDL_GPUDevice*>("gpu_device", nullptr);
|
||||
if (!cmd || !swapchain || !device) return;
|
||||
if (!ready_) TryInit(device, context.Get<SDL_Window*>("sdl_window", nullptr));
|
||||
if (!ready_) return;
|
||||
const auto fw = context.Get<uint32_t>("frame_width", 1280u);
|
||||
const auto fh = context.Get<uint32_t>("frame_height", 960u);
|
||||
DrawSurface(context, fw, fh);
|
||||
Render(cmd, swapchain, device, fw, fh);
|
||||
}
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
@@ -0,0 +1,166 @@
|
||||
#include "services/interfaces/workflow/rendering/workflow_overlay_sw_begin_step.hpp"
|
||||
#include "services/interfaces/workflow/workflow_step_parameter_resolver.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <SDL3/SDL.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
using namespace q3overlay;
|
||||
|
||||
WorkflowOverlaySwBeginStep::WorkflowOverlaySwBeginStep(std::shared_ptr<ILogger> l)
|
||||
: logger_(std::move(l)) {}
|
||||
|
||||
WorkflowOverlaySwBeginStep::~WorkflowOverlaySwBeginStep() {
|
||||
if (renderer_) {
|
||||
auto destroy = [&](SDL_Texture*& t) { if (t) { SDL_DestroyTexture(t); t=nullptr; } };
|
||||
destroy(bigchars_); destroy(prop_); destroy(prop_glo_);
|
||||
destroy(frame_l_); destroy(frame_r_);
|
||||
for (auto& d : digits_) destroy(d);
|
||||
destroy(icon_armor_); destroy(icon_health_); destroy(icon_face_); destroy(icon_weapon_);
|
||||
destroy(crosshair_); destroy(btn_back_); destroy(btn_fight_); destroy(btn_skirmish_);
|
||||
destroy(arrow_l_); destroy(arrow_r_);
|
||||
// Levelshot textures owned by the cache
|
||||
if (levelshot_cache_) {
|
||||
for (auto& [k,v] : *levelshot_cache_) if (v) SDL_DestroyTexture(v);
|
||||
}
|
||||
SDL_DestroyRenderer(renderer_);
|
||||
}
|
||||
if (surface_) SDL_DestroySurface(surface_);
|
||||
}
|
||||
|
||||
std::string WorkflowOverlaySwBeginStep::GetPluginId() const { return "overlay.sw.begin"; }
|
||||
|
||||
void WorkflowOverlaySwBeginStep::TryLoadTextures(const std::string& pk3) {
|
||||
if (tex_loaded_) return;
|
||||
tex_loaded_ = true;
|
||||
|
||||
auto load = [&](const char* e) { return LoadTextureFromPk3(renderer_, pk3, e); };
|
||||
|
||||
bigchars_ = load("gfx/2d/bigchars.tga");
|
||||
prop_ = load("menu/art/font1_prop.tga");
|
||||
prop_glo_ = load("menu/art/font1_prop_glo.tga");
|
||||
frame_l_ = load("menu/art/frame1_l.tga");
|
||||
frame_r_ = load("menu/art/frame1_r.tga");
|
||||
|
||||
static const char* kDigits[11] = {
|
||||
"gfx/2d/numbers/zero_32b.tga", "gfx/2d/numbers/one_32b.tga",
|
||||
"gfx/2d/numbers/two_32b.tga", "gfx/2d/numbers/three_32b.tga",
|
||||
"gfx/2d/numbers/four_32b.tga", "gfx/2d/numbers/five_32b.tga",
|
||||
"gfx/2d/numbers/six_32b.tga", "gfx/2d/numbers/seven_32b.tga",
|
||||
"gfx/2d/numbers/eight_32b.tga", "gfx/2d/numbers/nine_32b.tga",
|
||||
"gfx/2d/numbers/minus_32b.tga"
|
||||
};
|
||||
for (int i = 0; i < 11; ++i) digits_[i] = load(kDigits[i]);
|
||||
|
||||
icon_armor_ = load("icons/iconr_yellow.tga");
|
||||
icon_health_ = load("icons/iconh_red.tga");
|
||||
icon_face_ = load("models/players/keel/icon_default.tga");
|
||||
icon_weapon_ = load("icons/iconw_machinegun.tga"); // weapon icon (right HUD), not ammo pickup
|
||||
crosshair_ = load("gfx/2d/crosshaira.tga");
|
||||
btn_back_ = load("menu/art/back_0.tga");
|
||||
btn_fight_ = load("menu/art/fight_0.tga");
|
||||
btn_skirmish_ = load("menu/art/skirmish_0.tga");
|
||||
arrow_l_ = load("menu/art/gs_arrows_l.tga");
|
||||
arrow_r_ = load("menu/art/gs_arrows_r.tga");
|
||||
|
||||
// Parse arenas.txt
|
||||
arena_data_ = std::make_shared<ArenaMap>();
|
||||
levelshot_cache_ = std::make_shared<LevelshotCache>();
|
||||
int ze = 0;
|
||||
zip_t* arc = zip_open(pk3.c_str(), ZIP_RDONLY, &ze);
|
||||
if (arc) {
|
||||
zip_stat_t st;
|
||||
if (zip_stat(arc, "scripts/arenas.txt", 0, &st) == 0) {
|
||||
std::vector<char> buf(st.size + 1, '\0');
|
||||
zip_file_t* zf = zip_fopen(arc, "scripts/arenas.txt", 0);
|
||||
if (zf) { zip_fread(zf, buf.data(), st.size); zip_fclose(zf); }
|
||||
std::string text(buf.data());
|
||||
size_t pos = 0;
|
||||
while ((pos = text.find('{', pos)) != std::string::npos) {
|
||||
size_t end = text.find('}', pos);
|
||||
if (end == std::string::npos) break;
|
||||
std::string block = text.substr(pos+1, end-pos-1);
|
||||
pos = end + 1;
|
||||
std::string mapName, longName, bots;
|
||||
std::istringstream ss(block);
|
||||
std::string tok;
|
||||
while (ss >> tok) {
|
||||
auto readVal = [&]() -> std::string {
|
||||
std::string v; ss >> std::ws;
|
||||
if (ss.peek()=='"') { ss.get(); std::getline(ss,v,'"'); }
|
||||
else { ss >> v; }
|
||||
return v;
|
||||
};
|
||||
if (tok=="map") mapName = readVal();
|
||||
else if (tok=="longname") longName = readVal();
|
||||
else if (tok=="bots") bots = readVal();
|
||||
}
|
||||
if (!mapName.empty()) {
|
||||
std::string key = mapName;
|
||||
std::transform(key.begin(), key.end(), key.begin(), ::tolower);
|
||||
std::string firstBot = bots.substr(0, bots.find(' '));
|
||||
(*arena_data_)[key] = {longName, firstBot};
|
||||
}
|
||||
}
|
||||
}
|
||||
zip_close(arc);
|
||||
}
|
||||
|
||||
if (logger_) logger_->Info("overlay.sw.begin: textures loaded from " + pk3);
|
||||
}
|
||||
|
||||
void WorkflowOverlaySwBeginStep::Execute(
|
||||
const WorkflowStepDefinition& /*step*/, WorkflowContext& context) {
|
||||
if (context.GetBool("frame_skip", false)) return;
|
||||
|
||||
if (!ready_) {
|
||||
surface_ = SDL_CreateSurface(kW, kH, SDL_PIXELFORMAT_RGBA32);
|
||||
renderer_ = surface_ ? SDL_CreateSoftwareRenderer(surface_) : nullptr;
|
||||
ready_ = surface_ && renderer_;
|
||||
}
|
||||
if (!ready_) return;
|
||||
|
||||
const auto bspCfg = context.Get<nlohmann::json>("bsp_config", nlohmann::json{});
|
||||
const std::string pk3 = bspCfg.value("pk3_path", std::string(""));
|
||||
if (!tex_loaded_ && !pk3.empty()) TryLoadTextures(pk3);
|
||||
|
||||
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 0);
|
||||
SDL_RenderClear(renderer_);
|
||||
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
|
||||
|
||||
// Expose surface/renderer so drawing steps can use them
|
||||
context.Set<SDL_Renderer*>("overlay.renderer", renderer_);
|
||||
context.Set<SDL_Surface*> ("overlay.surface", surface_);
|
||||
context.Set<bool> ("overlay.ready", ready_);
|
||||
|
||||
// Font textures
|
||||
context.Set<SDL_Texture*>("overlay.tex.bigchars", bigchars_);
|
||||
context.Set<SDL_Texture*>("overlay.tex.prop", prop_);
|
||||
context.Set<SDL_Texture*>("overlay.tex.prop_glo", prop_glo_);
|
||||
context.Set<SDL_Texture*>("overlay.tex.frame_l", frame_l_);
|
||||
context.Set<SDL_Texture*>("overlay.tex.frame_r", frame_r_);
|
||||
for (int i = 0; i < 11; ++i)
|
||||
context.Set<SDL_Texture*>("overlay.tex.num." + std::to_string(i), digits_[i]);
|
||||
|
||||
// HUD icons
|
||||
context.Set<SDL_Texture*>("overlay.tex.icon_armor", icon_armor_);
|
||||
context.Set<SDL_Texture*>("overlay.tex.icon_health", icon_health_);
|
||||
context.Set<SDL_Texture*>("overlay.tex.icon_face", icon_face_);
|
||||
context.Set<SDL_Texture*>("overlay.tex.icon_weapon", icon_weapon_);
|
||||
context.Set<SDL_Texture*>("overlay.tex.crosshair", crosshair_);
|
||||
|
||||
// Map-select UI
|
||||
context.Set<SDL_Texture*>("overlay.tex.btn_back", btn_back_);
|
||||
context.Set<SDL_Texture*>("overlay.tex.btn_fight", btn_fight_);
|
||||
context.Set<SDL_Texture*>("overlay.tex.btn_skirmish", btn_skirmish_);
|
||||
context.Set<SDL_Texture*>("overlay.tex.arrow_l", arrow_l_);
|
||||
context.Set<SDL_Texture*>("overlay.tex.arrow_r", arrow_r_);
|
||||
|
||||
context.Set<std::shared_ptr<ArenaMap>> ("overlay.arena_data", arena_data_);
|
||||
context.Set<std::shared_ptr<LevelshotCache>>("overlay.levelshot_cache", levelshot_cache_);
|
||||
}
|
||||
} // namespace sdl3cpp::services::impl
|
||||
@@ -0,0 +1,202 @@
|
||||
#include "services/interfaces/workflow/rendering/workflow_overlay_sw_end_step.hpp"
|
||||
#include "services/interfaces/workflow/workflow_step_parameter_resolver.hpp"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
namespace {
|
||||
std::vector<uint8_t> LoadBin(const char* path) {
|
||||
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
||||
if (!f.is_open()) return {};
|
||||
auto sz = f.tellg(); std::vector<uint8_t> d(sz);
|
||||
f.seekg(0); f.read((char*)d.data(), sz); return d;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
WorkflowOverlaySwEndStep::WorkflowOverlaySwEndStep(std::shared_ptr<ILogger> l)
|
||||
: logger_(std::move(l)) {}
|
||||
|
||||
WorkflowOverlaySwEndStep::~WorkflowOverlaySwEndStep() {
|
||||
if (device_) {
|
||||
if (sampler_) SDL_ReleaseGPUSampler(device_, sampler_);
|
||||
if (vtx_buf_) SDL_ReleaseGPUBuffer(device_, vtx_buf_);
|
||||
if (transfer_) SDL_ReleaseGPUTransferBuffer(device_, transfer_);
|
||||
if (tex_) SDL_ReleaseGPUTexture(device_, tex_);
|
||||
if (pipeline_) SDL_ReleaseGPUGraphicsPipeline(device_, pipeline_);
|
||||
}
|
||||
}
|
||||
|
||||
std::string WorkflowOverlaySwEndStep::GetPluginId() const { return "overlay.sw.end"; }
|
||||
|
||||
void WorkflowOverlaySwEndStep::TryInit(
|
||||
SDL_GPUDevice* device, SDL_Window* window,
|
||||
const std::string& vertPath, const std::string& fragPath) {
|
||||
device_ = device;
|
||||
const char* driver = SDL_GetGPUDeviceDriver(device);
|
||||
const std::string drv = driver ? driver : "";
|
||||
SDL_GPUShaderFormat fmt = SDL_GPU_SHADERFORMAT_INVALID;
|
||||
auto vert = LoadBin(vertPath.c_str());
|
||||
auto frag = LoadBin(fragPath.c_str());
|
||||
const char* entry = (drv == "metal") ? "main0" : "main";
|
||||
if (drv == "metal") fmt = SDL_GPU_SHADERFORMAT_MSL;
|
||||
else if (drv == "vulkan") fmt = SDL_GPU_SHADERFORMAT_SPIRV;
|
||||
else { disabled_ = true; return; }
|
||||
if (vert.empty() || frag.empty()) { disabled_ = true; return; }
|
||||
|
||||
SDL_GPUShaderCreateInfo vsi{}, fsi{};
|
||||
vsi.code=vert.data(); vsi.code_size=vert.size(); vsi.entrypoint=entry;
|
||||
vsi.format=fmt; vsi.stage=SDL_GPU_SHADERSTAGE_VERTEX;
|
||||
fsi.code=frag.data(); fsi.code_size=frag.size(); fsi.entrypoint=entry;
|
||||
fsi.format=fmt; fsi.stage=SDL_GPU_SHADERSTAGE_FRAGMENT; fsi.num_samplers=1;
|
||||
auto* vs = SDL_CreateGPUShader(device, &vsi);
|
||||
auto* fs = SDL_CreateGPUShader(device, &fsi);
|
||||
if (!vs || !fs) {
|
||||
if (vs) SDL_ReleaseGPUShader(device, vs);
|
||||
if (fs) SDL_ReleaseGPUShader(device, fs);
|
||||
disabled_ = true; return;
|
||||
}
|
||||
SDL_GPUVertexBufferDescription vbd{}; vbd.slot=0; vbd.pitch=sizeof(float)*5;
|
||||
vbd.input_rate=SDL_GPU_VERTEXINPUTRATE_VERTEX;
|
||||
SDL_GPUVertexAttribute attrs[2]{};
|
||||
attrs[0]={0,0,SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3,0};
|
||||
attrs[1]={1,0,SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2,sizeof(float)*3};
|
||||
SDL_GPUVertexInputState vis{}; vis.vertex_buffer_descriptions=&vbd; vis.num_vertex_buffers=1;
|
||||
vis.vertex_attributes=attrs; vis.num_vertex_attributes=2;
|
||||
SDL_GPUColorTargetDescription ctd{};
|
||||
ctd.format = window ? SDL_GetGPUSwapchainTextureFormat(device,window)
|
||||
: SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
|
||||
ctd.blend_state.enable_blend=true;
|
||||
ctd.blend_state.src_color_blendfactor=SDL_GPU_BLENDFACTOR_SRC_ALPHA;
|
||||
ctd.blend_state.dst_color_blendfactor=SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
|
||||
ctd.blend_state.color_blend_op=SDL_GPU_BLENDOP_ADD;
|
||||
ctd.blend_state.src_alpha_blendfactor=SDL_GPU_BLENDFACTOR_ONE;
|
||||
ctd.blend_state.dst_alpha_blendfactor=SDL_GPU_BLENDFACTOR_ZERO;
|
||||
ctd.blend_state.alpha_blend_op=SDL_GPU_BLENDOP_ADD;
|
||||
SDL_GPUGraphicsPipelineCreateInfo pci{};
|
||||
pci.vertex_shader=vs; pci.fragment_shader=fs; pci.vertex_input_state=vis;
|
||||
pci.primitive_type=SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||
pci.rasterizer_state.fill_mode=SDL_GPU_FILLMODE_FILL;
|
||||
pci.rasterizer_state.cull_mode=SDL_GPU_CULLMODE_NONE;
|
||||
pci.rasterizer_state.front_face=SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE;
|
||||
pci.depth_stencil_state.enable_depth_test=false;
|
||||
pci.depth_stencil_state.enable_depth_write=false;
|
||||
pci.target_info.num_color_targets=1;
|
||||
pci.target_info.color_target_descriptions=&ctd;
|
||||
pipeline_=SDL_CreateGPUGraphicsPipeline(device,&pci);
|
||||
SDL_ReleaseGPUShader(device,vs); SDL_ReleaseGPUShader(device,fs);
|
||||
if (!pipeline_) { disabled_=true; return; }
|
||||
|
||||
SDL_GPUTextureCreateInfo tci{}; tci.type=SDL_GPU_TEXTURETYPE_2D;
|
||||
tci.format=SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
|
||||
tci.width=kW; tci.height=kH; tci.layer_count_or_depth=1; tci.num_levels=1;
|
||||
tci.usage=SDL_GPU_TEXTUREUSAGE_SAMPLER;
|
||||
tex_=SDL_CreateGPUTexture(device,&tci);
|
||||
SDL_GPUTransferBufferCreateInfo tbci{}; tbci.usage=SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
||||
tbci.size=kW*kH*4; transfer_=SDL_CreateGPUTransferBuffer(device,&tbci);
|
||||
SDL_GPUBufferCreateInfo bci{}; bci.usage=SDL_GPU_BUFFERUSAGE_VERTEX;
|
||||
bci.size=6u*5u*(uint32_t)sizeof(float); vtx_buf_=SDL_CreateGPUBuffer(device,&bci);
|
||||
SDL_GPUSamplerCreateInfo sci{}; sci.min_filter=SDL_GPU_FILTER_NEAREST;
|
||||
sci.mag_filter=SDL_GPU_FILTER_NEAREST; sci.mipmap_mode=SDL_GPU_SAMPLERMIPMAPMODE_NEAREST;
|
||||
sci.address_mode_u=SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||
sci.address_mode_v=SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||
sampler_=SDL_CreateGPUSampler(device,&sci);
|
||||
ready_ = tex_ && transfer_ && vtx_buf_ && sampler_ && pipeline_;
|
||||
}
|
||||
|
||||
void WorkflowOverlaySwEndStep::Execute(
|
||||
const WorkflowStepDefinition& step, WorkflowContext& context) {
|
||||
if (context.GetBool("frame_skip", false)) return;
|
||||
if (!context.GetBool("overlay.ready", false)) return;
|
||||
|
||||
auto* renderer = context.Get<SDL_Renderer*>("overlay.renderer", nullptr);
|
||||
auto* surface = context.Get<SDL_Surface*> ("overlay.surface", nullptr);
|
||||
auto* cmd = context.Get<SDL_GPUCommandBuffer*>("gpu_command_buffer", nullptr);
|
||||
auto* swapchain = context.Get<SDL_GPUTexture*>("gpu_swapchain_texture", nullptr);
|
||||
auto* device = context.Get<SDL_GPUDevice*>("gpu_device", nullptr);
|
||||
if (!surface || !cmd || !swapchain || !device) return;
|
||||
|
||||
if (renderer) SDL_RenderPresent(renderer);
|
||||
|
||||
if (!ready_ && !disabled_) {
|
||||
WorkflowStepParameterResolver params;
|
||||
auto getStr = [&](const char* k, const char* def) -> std::string {
|
||||
const auto* p = params.FindParameter(step, k);
|
||||
return (p && p->type == WorkflowParameterValue::Type::String)
|
||||
? p->stringValue : def;
|
||||
};
|
||||
const char* driver = SDL_GetGPUDeviceDriver(device);
|
||||
const std::string drv = driver ? driver : "";
|
||||
std::string vp, fp;
|
||||
if (drv == "metal") {
|
||||
vp = getStr("vert_shader_path_msl",
|
||||
"packages/quake3/shaders/msl/overlay.vert.metal");
|
||||
fp = getStr("frag_shader_path_msl",
|
||||
"packages/quake3/shaders/msl/overlay.frag.metal");
|
||||
} else {
|
||||
vp = getStr("vert_shader_path_spirv",
|
||||
"packages/quake3/shaders/spirv/overlay.vert.spv");
|
||||
fp = getStr("frag_shader_path_spirv",
|
||||
"packages/quake3/shaders/spirv/overlay.frag.spv");
|
||||
}
|
||||
TryInit(device, context.Get<SDL_Window*>("sdl_window", nullptr), vp, fp);
|
||||
}
|
||||
if (!ready_) return;
|
||||
|
||||
// Upload surface pixels to GPU texture
|
||||
void* mapped = SDL_MapGPUTransferBuffer(device, transfer_, false);
|
||||
if (!mapped) return;
|
||||
std::memcpy(mapped, surface->pixels, kW * kH * 4);
|
||||
SDL_UnmapGPUTransferBuffer(device, transfer_);
|
||||
auto* copy = SDL_BeginGPUCopyPass(cmd);
|
||||
if (copy) {
|
||||
SDL_GPUTextureTransferInfo src{}; src.transfer_buffer=transfer_;
|
||||
src.pixels_per_row=kW; src.rows_per_layer=kH;
|
||||
SDL_GPUTextureRegion dst{}; dst.texture=tex_; dst.w=kW; dst.h=kH; dst.d=1;
|
||||
SDL_UploadToGPUTexture(copy, &src, &dst, false);
|
||||
SDL_EndGPUCopyPass(copy);
|
||||
}
|
||||
// Upload vertex buffer once
|
||||
if (!vbuf_uploaded_) {
|
||||
const float verts[6][5]={{-1,1,0,0,0},{1,1,0,1,0},{1,-1,0,1,1},
|
||||
{-1,1,0,0,0},{1,-1,0,1,1},{-1,-1,0,0,1}};
|
||||
const uint32_t sz=sizeof(verts);
|
||||
SDL_GPUTransferBufferCreateInfo tb{}; tb.usage=SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; tb.size=sz;
|
||||
auto* tmp=SDL_CreateGPUTransferBuffer(device,&tb);
|
||||
if (tmp) {
|
||||
void* ptr=SDL_MapGPUTransferBuffer(device,tmp,false);
|
||||
if (ptr) { std::memcpy(ptr,verts,sz); SDL_UnmapGPUTransferBuffer(device,tmp); }
|
||||
auto* cp=SDL_BeginGPUCopyPass(cmd);
|
||||
if (cp) {
|
||||
SDL_GPUTransferBufferLocation s{tmp,0};
|
||||
SDL_GPUBufferRegion d{vtx_buf_,0,sz};
|
||||
SDL_UploadToGPUBuffer(cp,&s,&d,false);
|
||||
SDL_EndGPUCopyPass(cp);
|
||||
}
|
||||
SDL_ReleaseGPUTransferBuffer(device,tmp);
|
||||
}
|
||||
vbuf_uploaded_=true;
|
||||
}
|
||||
// Render full-screen quad
|
||||
SDL_GPUColorTargetInfo target{}; target.texture=swapchain;
|
||||
target.load_op=SDL_GPU_LOADOP_LOAD; target.store_op=SDL_GPU_STOREOP_STORE;
|
||||
auto* pass=SDL_BeginGPURenderPass(cmd,&target,1,nullptr);
|
||||
if (!pass) return;
|
||||
SDL_BindGPUGraphicsPipeline(pass,pipeline_);
|
||||
SDL_GPUBufferBinding vb{vtx_buf_,0}; SDL_BindGPUVertexBuffers(pass,0,&vb,1);
|
||||
SDL_GPUTextureSamplerBinding ts{tex_,sampler_}; SDL_BindGPUFragmentSamplers(pass,0,&ts,1);
|
||||
SDL_DrawGPUPrimitives(pass,6,1,0,0);
|
||||
SDL_EndGPURenderPass(pass);
|
||||
|
||||
// Screenshot
|
||||
const auto* ssPath = context.TryGet<std::string>("screenshot_output_path");
|
||||
if (ssPath && !ssPath->empty()) {
|
||||
if (SDL_SaveBMP(surface, ssPath->c_str()))
|
||||
context.Set<bool>("screenshot_saved", true);
|
||||
context.Set<std::string>("screenshot_output_path", std::string(""));
|
||||
}
|
||||
}
|
||||
} // namespace sdl3cpp::services::impl
|
||||
@@ -4,7 +4,9 @@
|
||||
|
||||
#include <rapidjson/document.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <stdexcept>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
@@ -45,6 +47,15 @@ WorkflowDefinition WorkflowDefinitionParser::ParseFile(const std::filesystem::pa
|
||||
// Read workflow variables (n8n-style)
|
||||
ParseVariables(document, workflow);
|
||||
|
||||
// Build include-cycle detection set (canonical path of THIS file)
|
||||
std::unordered_set<std::string> visited;
|
||||
try {
|
||||
visited.insert(std::filesystem::canonical(path).string());
|
||||
} catch (...) {
|
||||
visited.insert(path.string());
|
||||
}
|
||||
const std::filesystem::path baseDir = path.parent_path();
|
||||
|
||||
// Handle "steps" format (simple sequential)
|
||||
if (hasSteps) {
|
||||
if (!document["steps"].IsArray()) {
|
||||
@@ -62,12 +73,17 @@ WorkflowDefinition WorkflowDefinitionParser::ParseFile(const std::filesystem::pa
|
||||
step.parameters = paramReader.ReadParameterMap(entry, "parameters");
|
||||
workflow.steps.push_back(std::move(step));
|
||||
}
|
||||
// Expand workflow.include nodes (parse-time composition, like React imports)
|
||||
ResolveIncludes(workflow.steps, baseDir, visited);
|
||||
return workflow;
|
||||
}
|
||||
|
||||
// Handle "nodes" format (n8n with connections)
|
||||
workflow.steps = ParseNodes(document);
|
||||
|
||||
// Expand workflow.include nodes (parse-time composition, like React imports)
|
||||
ResolveIncludes(workflow.steps, baseDir, visited);
|
||||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
#include "services/interfaces/workflow/workflow_definition_parser.hpp"
|
||||
#include "services/interfaces/workflow_step_definition.hpp"
|
||||
#include "services/interfaces/workflow_parameter_value.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
// Resolve the include path:
|
||||
// 1. Absolute path → use directly.
|
||||
// 2. Relative path → try CWD first (game assets live under CWD/packages/…),
|
||||
// then relative to the including file's directory.
|
||||
static fs::path ResolvePath(const std::string& rawPath, const fs::path& baseDir) {
|
||||
fs::path p(rawPath);
|
||||
if (p.is_absolute()) return p;
|
||||
|
||||
// CWD-relative first (matches how shader/asset paths work in workflows)
|
||||
const fs::path cwdCandidate = fs::current_path() / p;
|
||||
if (fs::exists(cwdCandidate)) return cwdCandidate;
|
||||
|
||||
// Fallback: relative to the including file
|
||||
const fs::path relCandidate = baseDir / p;
|
||||
if (fs::exists(relCandidate)) return relCandidate;
|
||||
|
||||
// Return CWD-relative even if it doesn't exist yet — ParseFile will throw a
|
||||
// better "file not found" message than we would.
|
||||
return cwdCandidate;
|
||||
}
|
||||
|
||||
void WorkflowDefinitionParser::ResolveIncludes(
|
||||
std::vector<WorkflowStepDefinition>& steps,
|
||||
const fs::path& baseDir,
|
||||
std::unordered_set<std::string>& visited) const {
|
||||
|
||||
std::vector<WorkflowStepDefinition> expanded;
|
||||
expanded.reserve(steps.size());
|
||||
|
||||
for (auto& step : steps) {
|
||||
if (step.plugin != "workflow.include") {
|
||||
expanded.push_back(std::move(step));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read the required "path" parameter
|
||||
auto pathIt = step.parameters.find("path");
|
||||
if (pathIt == step.parameters.end() ||
|
||||
pathIt->second.type != WorkflowParameterValue::Type::String ||
|
||||
pathIt->second.stringValue.empty()) {
|
||||
if (logger_) {
|
||||
logger_->Warn("workflow.include step '" + step.id +
|
||||
"' is missing a 'path' parameter — skipping");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const fs::path includePath = ResolvePath(pathIt->second.stringValue, baseDir);
|
||||
|
||||
// Cycle detection
|
||||
std::string canonicalKey;
|
||||
try {
|
||||
canonicalKey = fs::canonical(includePath).string();
|
||||
} catch (...) {
|
||||
canonicalKey = includePath.string();
|
||||
}
|
||||
if (visited.count(canonicalKey)) {
|
||||
throw std::runtime_error(
|
||||
"workflow.include cycle detected: '" + canonicalKey +
|
||||
"' is already on the include stack");
|
||||
}
|
||||
|
||||
if (logger_) {
|
||||
logger_->Info("workflow.include: expanding '" + step.id +
|
||||
"' → " + includePath.string());
|
||||
}
|
||||
|
||||
// Parse the included file (its own ResolveIncludes runs inside ParseFile,
|
||||
// so nested includes are handled automatically).
|
||||
visited.insert(canonicalKey);
|
||||
WorkflowDefinition sub = ParseFile(includePath);
|
||||
visited.erase(canonicalKey);
|
||||
|
||||
// Namespace included node IDs to prevent collisions when the same
|
||||
// sub-workflow is included more than once.
|
||||
// e.g. include id "overlay_group" + sub id "q3_hud" → "overlay_group/q3_hud"
|
||||
const std::string ns = step.id + "/";
|
||||
for (auto& subStep : sub.steps) {
|
||||
subStep.id = ns + subStep.id;
|
||||
expanded.push_back(std::move(subStep));
|
||||
}
|
||||
|
||||
if (logger_) {
|
||||
logger_->Info("workflow.include: '" + step.id + "' expanded to " +
|
||||
std::to_string(sub.steps.size()) + " steps");
|
||||
}
|
||||
}
|
||||
|
||||
steps = std::move(expanded);
|
||||
}
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
@@ -116,7 +116,13 @@
|
||||
// Quake 3
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_menu_update_step.hpp"
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_weapon_update_step.hpp"
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_overlay_draw_step.hpp"
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_hud_step.hpp"
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_crosshair_step.hpp"
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_hitmarker_step.hpp"
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_menu_frame_step.hpp"
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_mapselect_step.hpp"
|
||||
#include "services/interfaces/workflow/rendering/workflow_overlay_sw_begin_step.hpp"
|
||||
#include "services/interfaces/workflow/rendering/workflow_overlay_sw_end_step.hpp"
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_pickups_draw_step.hpp"
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_md3_load_step.hpp"
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_md3_draw_step.hpp"
|
||||
@@ -331,13 +337,19 @@ void WorkflowRegistrar::RegisterSteps(std::shared_ptr<IWorkflowStepRegistry> reg
|
||||
registry->RegisterStep(std::make_shared<WorkflowQ3MenuUpdateStep>(logger_));
|
||||
registry->RegisterStep(std::make_shared<WorkflowQ3WeaponUpdateStep>(logger_));
|
||||
registry->RegisterStep(std::make_shared<WorkflowQ3PickupsDrawStep>(logger_));
|
||||
registry->RegisterStep(std::make_shared<WorkflowQ3OverlayDrawStep>(logger_));
|
||||
registry->RegisterStep(std::make_shared<WorkflowOverlaySwBeginStep>(logger_));
|
||||
registry->RegisterStep(std::make_shared<WorkflowQ3HudStep>(logger_));
|
||||
registry->RegisterStep(std::make_shared<WorkflowQ3CrosshairStep>(logger_));
|
||||
registry->RegisterStep(std::make_shared<WorkflowQ3HitmarkerStep>(logger_));
|
||||
registry->RegisterStep(std::make_shared<WorkflowQ3MenuFrameStep>(logger_));
|
||||
registry->RegisterStep(std::make_shared<WorkflowQ3MapSelectStep>(logger_));
|
||||
registry->RegisterStep(std::make_shared<WorkflowOverlaySwEndStep>(logger_));
|
||||
registry->RegisterStep(std::make_shared<WorkflowQ3Md3LoadStep>(logger_));
|
||||
registry->RegisterStep(std::make_shared<WorkflowQ3Md3DrawStep>(logger_));
|
||||
registry->RegisterStep(std::make_shared<WorkflowQ3BotsSpawnStep>(logger_));
|
||||
registry->RegisterStep(std::make_shared<WorkflowQ3BotsUpdateStep>(logger_));
|
||||
registry->RegisterStep(std::make_shared<WorkflowQ3BotsDrawStep>(logger_));
|
||||
count += 23;
|
||||
count += 29;
|
||||
|
||||
// ── Texture ───────────────────────────────────────────────
|
||||
registry->RegisterStep(std::make_shared<WorkflowTextureLoadStep>(logger_));
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
#pragma once
|
||||
#include <SDL3/SDL_render.h>
|
||||
#include <stb_image.h>
|
||||
#include <zip.h>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace sdl3cpp::q3overlay {
|
||||
|
||||
struct ArenaInfo { std::string longname; std::string bot; };
|
||||
using ArenaMap = std::unordered_map<std::string, ArenaInfo>;
|
||||
using LevelshotCache = std::unordered_map<std::string, SDL_Texture*>;
|
||||
|
||||
static constexpr int kW = 640, kH = 360;
|
||||
static constexpr int kGlyphSrc = 16;
|
||||
static constexpr int kPropHeight = 27;
|
||||
static constexpr int kPropGap = 3;
|
||||
static constexpr int kPropSpace = 8;
|
||||
|
||||
// Q3 proportional font character map [128][3] = {src_x, src_y, width}
|
||||
// From ioquake3 code/q3_ui/ui_atoms.c propMap
|
||||
inline constexpr int kPropMap[128][3] = {
|
||||
{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},
|
||||
{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},
|
||||
{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},
|
||||
{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},{0,0,-1},
|
||||
{0,0,8},
|
||||
{11,122,7},{154,181,14},{55,122,17},{79,122,18},{101,122,23},{153,122,18},{9,93,7},
|
||||
{207,122,8},{230,122,9},{177,122,18},{30,152,18},{85,181,7},{34,93,11},{110,181,6},{130,152,14},
|
||||
{22,64,17},{41,64,12},{58,64,17},{78,64,18},{98,64,19},{120,64,18},{141,64,18},{204,64,16},{162,64,17},{182,64,18},
|
||||
{59,181,7},{35,181,7},{203,152,14},{56,93,14},{228,152,14},{177,181,18},{28,122,22},
|
||||
{5,4,18},{27,4,18},{48,4,18},{69,4,17},{90,4,13},{106,4,13},{121,4,18},{143,4,17},
|
||||
{164,4,8},{175,4,16},{195,4,18},{216,4,12},{230,4,23},{6,34,18},{27,34,18},{48,34,18},
|
||||
{68,34,18},{90,34,17},{110,34,18},{130,34,14},{146,34,18},{166,34,19},{185,34,29},
|
||||
{215,34,18},{234,34,18},{5,64,14},
|
||||
{60,152,7},{106,151,13},{83,152,7},{128,122,17},{4,152,21},{134,181,5},
|
||||
{5,4,18},{27,4,18},{48,4,18},{69,4,17},{90,4,13},{106,4,13},{121,4,18},{143,4,17},
|
||||
{164,4,8},{175,4,16},{195,4,18},{216,4,12},{230,4,23},{6,34,18},{27,34,18},{48,34,18},
|
||||
{68,34,18},{90,34,17},{110,34,18},{130,34,14},{146,34,18},{166,34,19},{185,34,29},
|
||||
{215,34,18},{234,34,18},{5,64,14},
|
||||
{153,152,13},{11,181,5},{180,152,13},{79,93,17},{0,0,-1}
|
||||
};
|
||||
|
||||
inline SDL_Texture* LoadTextureFromPk3(
|
||||
SDL_Renderer* renderer, const std::string& pk3Path, const char* entry) {
|
||||
if (pk3Path.empty() || !renderer || !entry) return nullptr;
|
||||
int ze = 0;
|
||||
zip_t* arc = zip_open(pk3Path.c_str(), ZIP_RDONLY, &ze);
|
||||
if (!arc) return nullptr;
|
||||
zip_stat_t st;
|
||||
if (zip_stat(arc, entry, 0, &st) != 0) { zip_close(arc); return nullptr; }
|
||||
std::vector<uint8_t> buf(st.size);
|
||||
zip_file_t* zf = zip_fopen(arc, entry, 0);
|
||||
if (!zf) { zip_close(arc); return nullptr; }
|
||||
zip_fread(zf, buf.data(), st.size);
|
||||
zip_fclose(zf); zip_close(arc);
|
||||
int w = 0, h = 0, ch = 0;
|
||||
unsigned char* px = stbi_load_from_memory(buf.data(), (int)buf.size(), &w, &h, &ch, 4);
|
||||
if (!px) return nullptr;
|
||||
SDL_Surface* surf = SDL_CreateSurfaceFrom(w, h, SDL_PIXELFORMAT_RGBA32, px, w * 4);
|
||||
SDL_Texture* tex = surf ? SDL_CreateTextureFromSurface(renderer, surf) : nullptr;
|
||||
if (surf) SDL_DestroySurface(surf);
|
||||
stbi_image_free(px);
|
||||
return tex;
|
||||
}
|
||||
|
||||
inline float PropStringWidth(const char* text) {
|
||||
if (!text) return 0.f;
|
||||
float w = 0.f;
|
||||
for (const char* p = text; *p; ++p) {
|
||||
int ch = (unsigned char)*p & 127;
|
||||
int cw = kPropMap[ch][2];
|
||||
if (cw < 0) continue;
|
||||
w += (float)(cw == kPropSpace ? kPropSpace : cw + kPropGap);
|
||||
}
|
||||
return w > 0.f ? w - kPropGap : 0.f;
|
||||
}
|
||||
|
||||
inline void DrawPropText(SDL_Renderer* r, SDL_Texture* font,
|
||||
float x, float y, const char* text, SDL_Color col, float scale, bool center) {
|
||||
if (!text || !r || !font) return;
|
||||
if (center) x -= PropStringWidth(text) * scale * 0.5f;
|
||||
SDL_SetTextureColorMod(font, col.r, col.g, col.b);
|
||||
SDL_SetTextureAlphaMod(font, col.a);
|
||||
SDL_SetTextureBlendMode(font, SDL_BLENDMODE_BLEND);
|
||||
float cx = x;
|
||||
for (const char* p = text; *p; ++p) {
|
||||
int ch = (unsigned char)*p & 127;
|
||||
int cw = kPropMap[ch][2];
|
||||
if (cw < 0) continue;
|
||||
if (cw == kPropSpace) { cx += kPropSpace * scale; continue; }
|
||||
SDL_FRect src{(float)kPropMap[ch][0], (float)kPropMap[ch][1], (float)cw, (float)kPropHeight};
|
||||
SDL_FRect dst{cx, y, cw * scale, kPropHeight * scale};
|
||||
SDL_RenderTexture(r, font, &src, &dst);
|
||||
cx += (cw + kPropGap) * scale;
|
||||
}
|
||||
}
|
||||
|
||||
inline void DrawQ3Text(SDL_Renderer* r, SDL_Texture* bigchars,
|
||||
float x, float y, const char* text, SDL_Color col, float scale) {
|
||||
if (!text || !r) return;
|
||||
if (bigchars) {
|
||||
SDL_SetTextureColorMod(bigchars, col.r, col.g, col.b);
|
||||
SDL_SetTextureAlphaMod(bigchars, col.a);
|
||||
SDL_SetTextureBlendMode(bigchars, SDL_BLENDMODE_BLEND);
|
||||
float cx = x;
|
||||
for (const char* p = text; *p; ++p) {
|
||||
int code = (unsigned char)*p;
|
||||
SDL_FRect src{(float)((code%16)*kGlyphSrc), (float)((code/16)*kGlyphSrc),
|
||||
(float)kGlyphSrc, (float)kGlyphSrc};
|
||||
SDL_FRect dst{cx, y, kGlyphSrc * scale, kGlyphSrc * scale};
|
||||
SDL_RenderTexture(r, bigchars, &src, &dst);
|
||||
cx += kGlyphSrc * scale;
|
||||
}
|
||||
} else {
|
||||
SDL_SetRenderDrawColor(r, col.r, col.g, col.b, col.a);
|
||||
SDL_RenderDebugText(r, x, y, text);
|
||||
}
|
||||
}
|
||||
|
||||
inline float DrawHudNumber(SDL_Renderer* r, SDL_Texture* digits[11],
|
||||
float x, float y, int value, float scale) {
|
||||
if (!r) return x;
|
||||
std::string s = (value < 0) ? ("-" + std::to_string(-value)) : std::to_string(value);
|
||||
const float dw = 32.f * scale;
|
||||
for (char c : s) {
|
||||
int idx = (c == '-') ? 10 : (c - '0');
|
||||
if (idx >= 0 && idx <= 10 && digits[idx]) {
|
||||
SDL_SetTextureColorMod(digits[idx], 255, 220, 60);
|
||||
SDL_SetTextureAlphaMod(digits[idx], 255);
|
||||
SDL_SetTextureBlendMode(digits[idx], SDL_BLENDMODE_BLEND);
|
||||
SDL_FRect dst{x, y, dw, dw};
|
||||
SDL_RenderTexture(r, digits[idx], nullptr, &dst);
|
||||
}
|
||||
x += dw;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
} // namespace sdl3cpp::q3overlay
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
#include "services/interfaces/i_workflow_step.hpp"
|
||||
#include "services/interfaces/i_logger.hpp"
|
||||
#include "services/interfaces/workflow_context.hpp"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
namespace sdl3cpp::services::impl {
|
||||
class WorkflowQ3CrosshairStep final : public IWorkflowStep {
|
||||
public:
|
||||
explicit WorkflowQ3CrosshairStep(std::shared_ptr<ILogger> logger);
|
||||
std::string GetPluginId() const override;
|
||||
void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override;
|
||||
private:
|
||||
std::shared_ptr<ILogger> logger_;
|
||||
};
|
||||
} // namespace sdl3cpp::services::impl
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
#include "services/interfaces/i_workflow_step.hpp"
|
||||
#include "services/interfaces/i_logger.hpp"
|
||||
#include "services/interfaces/workflow_context.hpp"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
namespace sdl3cpp::services::impl {
|
||||
class WorkflowQ3HitmarkerStep final : public IWorkflowStep {
|
||||
public:
|
||||
explicit WorkflowQ3HitmarkerStep(std::shared_ptr<ILogger> logger);
|
||||
std::string GetPluginId() const override;
|
||||
void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override;
|
||||
private:
|
||||
std::shared_ptr<ILogger> logger_;
|
||||
};
|
||||
} // namespace sdl3cpp::services::impl
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
#include "services/interfaces/i_workflow_step.hpp"
|
||||
#include "services/interfaces/i_logger.hpp"
|
||||
#include "services/interfaces/workflow_context.hpp"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
namespace sdl3cpp::services::impl {
|
||||
class WorkflowQ3HudStep final : public IWorkflowStep {
|
||||
public:
|
||||
explicit WorkflowQ3HudStep(std::shared_ptr<ILogger> logger);
|
||||
std::string GetPluginId() const override;
|
||||
void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override;
|
||||
private:
|
||||
std::shared_ptr<ILogger> logger_;
|
||||
};
|
||||
} // namespace sdl3cpp::services::impl
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
#include "services/interfaces/i_workflow_step.hpp"
|
||||
#include "services/interfaces/i_logger.hpp"
|
||||
#include "services/interfaces/workflow_context.hpp"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
namespace sdl3cpp::services::impl {
|
||||
class WorkflowQ3MapSelectStep final : public IWorkflowStep {
|
||||
public:
|
||||
explicit WorkflowQ3MapSelectStep(std::shared_ptr<ILogger> logger);
|
||||
std::string GetPluginId() const override;
|
||||
void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override;
|
||||
private:
|
||||
std::shared_ptr<ILogger> logger_;
|
||||
};
|
||||
} // namespace sdl3cpp::services::impl
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
#include "services/interfaces/i_workflow_step.hpp"
|
||||
#include "services/interfaces/i_logger.hpp"
|
||||
#include "services/interfaces/workflow_context.hpp"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
namespace sdl3cpp::services::impl {
|
||||
class WorkflowQ3MenuFrameStep final : public IWorkflowStep {
|
||||
public:
|
||||
explicit WorkflowQ3MenuFrameStep(std::shared_ptr<ILogger> logger);
|
||||
std::string GetPluginId() const override;
|
||||
void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override;
|
||||
private:
|
||||
std::shared_ptr<ILogger> logger_;
|
||||
};
|
||||
} // namespace sdl3cpp::services::impl
|
||||
@@ -1,110 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "services/interfaces/i_workflow_step.hpp"
|
||||
#include "services/interfaces/i_logger.hpp"
|
||||
#include "services/interfaces/workflow_context.hpp"
|
||||
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
#include <SDL3/SDL_render.h>
|
||||
#include <SDL3/SDL_surface.h>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
class WorkflowQ3OverlayDrawStep final : public IWorkflowStep {
|
||||
public:
|
||||
explicit WorkflowQ3OverlayDrawStep(std::shared_ptr<ILogger> logger);
|
||||
~WorkflowQ3OverlayDrawStep();
|
||||
|
||||
std::string GetPluginId() const override;
|
||||
void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override;
|
||||
|
||||
private:
|
||||
void TryInit(SDL_GPUDevice* device, SDL_Window* window);
|
||||
void TryLoadMenuTextures(const std::string& pk3Path);
|
||||
void ParseArenas(const std::string& pk3Path);
|
||||
void DrawSurface(WorkflowContext& context, uint32_t frameW, uint32_t frameH);
|
||||
void DrawMapSelectScreen(WorkflowContext& context, const std::string& pk3Path);
|
||||
void Render(SDL_GPUCommandBuffer* cmd, SDL_GPUTexture* swapchainTex,
|
||||
SDL_GPUDevice* device, uint32_t frameW, uint32_t frameH);
|
||||
|
||||
// Render a string using the Q3 bigchars grid font.
|
||||
void DrawQ3Text(float x, float y, const char* text, SDL_Color color, float scale = 1.0f);
|
||||
|
||||
// Render a string using Q3's proportional font (font1_prop.tga).
|
||||
// center=true → x is the horizontal centre of the string.
|
||||
void DrawPropText(float x, float y, const char* text, SDL_Color color,
|
||||
float scale = 1.0f, bool center = false);
|
||||
|
||||
// Draw an integer value using the gfx/2d/numbers/* sprites.
|
||||
// Returns the x position just past the last digit.
|
||||
float DrawHudNumber(float x, float y, int value, float scale);
|
||||
|
||||
// Compute pixel width of a proportional string at scale=1.
|
||||
float PropStringWidth(const char* text) const;
|
||||
|
||||
SDL_Texture* LoadTextureFromPk3(const std::string& pk3Path, const char* entry);
|
||||
SDL_Texture* LoadOrGetLevelshot(const std::string& mapName, const std::string& pk3Path);
|
||||
|
||||
std::shared_ptr<ILogger> logger_;
|
||||
|
||||
// GPU overlay pipeline state
|
||||
bool ready_ = false;
|
||||
bool disabled_ = false;
|
||||
bool vbuf_uploaded_= false;
|
||||
SDL_GPUDevice* device_ = nullptr;
|
||||
SDL_GPUGraphicsPipeline* pipeline_ = nullptr;
|
||||
SDL_GPUTexture* tex_ = nullptr;
|
||||
SDL_GPUTransferBuffer* transfer_ = nullptr;
|
||||
SDL_GPUBuffer* vtx_buf_ = nullptr;
|
||||
SDL_GPUSampler* sampler_ = nullptr;
|
||||
SDL_Surface* surface_ = nullptr;
|
||||
SDL_Renderer* renderer_ = nullptr;
|
||||
|
||||
// Menu / font textures loaded from PK3
|
||||
bool menu_tex_loaded_ = false;
|
||||
SDL_Texture* bigchars_tex_ = nullptr; // gfx/2d/bigchars.tga
|
||||
SDL_Texture* prop_font_tex_ = nullptr; // menu/art/font1_prop.tga
|
||||
SDL_Texture* prop_glo_tex_ = nullptr; // menu/art/font1_prop_glo.tga
|
||||
SDL_Texture* frame_bg_tex_ = nullptr; // menu/art/cut_frame.tga
|
||||
SDL_Texture* frame_l_tex_ = nullptr; // menu/art/frame1_l.tga (left ring half)
|
||||
SDL_Texture* frame_r_tex_ = nullptr; // menu/art/frame1_r.tga (right ring half)
|
||||
SDL_Texture* frame2_l_tex_ = nullptr; // menu/art/frame2_l.tga
|
||||
|
||||
// HUD number sprites: indices 0-9 = digits, index 10 = minus sign
|
||||
SDL_Texture* num_digits_[11] = {};
|
||||
|
||||
// HUD icon textures
|
||||
SDL_Texture* icon_armor_tex_ = nullptr; // icons/iconr_yellow.tga
|
||||
SDL_Texture* icon_health_tex_ = nullptr; // icons/iconh_red.tga
|
||||
SDL_Texture* icon_face_tex_ = nullptr; // models/players/keel/icon_default.tga
|
||||
SDL_Texture* icon_ammo_tex_ = nullptr; // icons/icona_machinegun.tga
|
||||
SDL_Texture* icon_crosshair_ = nullptr; // gfx/2d/crosshaira.tga
|
||||
|
||||
// Map-select UI textures
|
||||
SDL_Texture* btn_back_tex_ = nullptr; // menu/art/back_0.tga
|
||||
SDL_Texture* btn_fight_tex_ = nullptr; // menu/art/fight_0.tga
|
||||
SDL_Texture* btn_skirmish_tex_ = nullptr; // menu/art/skirmish_0.tga
|
||||
SDL_Texture* btn_arrow_l_tex_ = nullptr; // menu/art/gs_arrows_l.tga
|
||||
SDL_Texture* btn_arrow_r_tex_ = nullptr; // menu/art/gs_arrows_r.tga
|
||||
|
||||
// Levelshot cache: uppercase map name → software SDL_Texture
|
||||
std::unordered_map<std::string, SDL_Texture*> levelshot_cache_;
|
||||
|
||||
// Arena metadata parsed from scripts/arenas.txt
|
||||
struct ArenaInfo { std::string longname; std::string bot; };
|
||||
std::unordered_map<std::string, ArenaInfo> arena_data_; // key = lowercase map name
|
||||
bool arena_data_loaded_ = false;
|
||||
|
||||
static constexpr int kW = 640;
|
||||
static constexpr int kH = 360;
|
||||
static constexpr int kGlyphSrc = 16; // bigchars cell size in source texture
|
||||
static constexpr int kPropHeight = 27; // Q3 PROP_HEIGHT
|
||||
static constexpr int kPropGap = 3; // Q3 PROP_GAP_WIDTH
|
||||
static constexpr int kPropSpace = 8; // Q3 PROP_SPACE_WIDTH
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
#include "services/interfaces/i_workflow_step.hpp"
|
||||
#include "services/interfaces/i_logger.hpp"
|
||||
#include "services/interfaces/workflow_context.hpp"
|
||||
#include "services/interfaces/workflow/quake3/q3_overlay_utils.hpp"
|
||||
#include <SDL3/SDL_render.h>
|
||||
#include <SDL3/SDL_surface.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
class WorkflowOverlaySwBeginStep final : public IWorkflowStep {
|
||||
public:
|
||||
explicit WorkflowOverlaySwBeginStep(std::shared_ptr<ILogger> logger);
|
||||
~WorkflowOverlaySwBeginStep();
|
||||
std::string GetPluginId() const override;
|
||||
void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override;
|
||||
private:
|
||||
void TryLoadTextures(const std::string& pk3Path);
|
||||
std::shared_ptr<ILogger> logger_;
|
||||
SDL_Surface* surface_ = nullptr;
|
||||
SDL_Renderer* renderer_ = nullptr;
|
||||
bool ready_ = false;
|
||||
bool tex_loaded_ = false;
|
||||
SDL_Texture* bigchars_ = nullptr;
|
||||
SDL_Texture* prop_ = nullptr;
|
||||
SDL_Texture* prop_glo_ = nullptr;
|
||||
SDL_Texture* frame_l_ = nullptr;
|
||||
SDL_Texture* frame_r_ = nullptr;
|
||||
SDL_Texture* digits_[11]= {};
|
||||
SDL_Texture* icon_armor_ = nullptr;
|
||||
SDL_Texture* icon_health_ = nullptr;
|
||||
SDL_Texture* icon_face_ = nullptr;
|
||||
SDL_Texture* icon_weapon_ = nullptr; // iconw_machinegun — weapon icon on HUD right side
|
||||
SDL_Texture* crosshair_ = nullptr;
|
||||
SDL_Texture* btn_back_ = nullptr;
|
||||
SDL_Texture* btn_fight_ = nullptr;
|
||||
SDL_Texture* btn_skirmish_ = nullptr;
|
||||
SDL_Texture* arrow_l_ = nullptr;
|
||||
SDL_Texture* arrow_r_ = nullptr;
|
||||
std::shared_ptr<q3overlay::ArenaMap> arena_data_;
|
||||
std::shared_ptr<q3overlay::LevelshotCache> levelshot_cache_;
|
||||
};
|
||||
} // namespace sdl3cpp::services::impl
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
#include "services/interfaces/i_workflow_step.hpp"
|
||||
#include "services/interfaces/i_logger.hpp"
|
||||
#include "services/interfaces/workflow_context.hpp"
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
class WorkflowOverlaySwEndStep final : public IWorkflowStep {
|
||||
public:
|
||||
explicit WorkflowOverlaySwEndStep(std::shared_ptr<ILogger> logger);
|
||||
~WorkflowOverlaySwEndStep();
|
||||
std::string GetPluginId() const override;
|
||||
void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override;
|
||||
private:
|
||||
void TryInit(SDL_GPUDevice* device, SDL_Window* window,
|
||||
const std::string& vertPath, const std::string& fragPath);
|
||||
std::shared_ptr<ILogger> logger_;
|
||||
bool ready_ = false;
|
||||
bool disabled_ = false;
|
||||
bool vbuf_uploaded_ = false;
|
||||
SDL_GPUDevice* device_ = nullptr;
|
||||
SDL_GPUGraphicsPipeline* pipeline_ = nullptr;
|
||||
SDL_GPUTexture* tex_ = nullptr;
|
||||
SDL_GPUTransferBuffer* transfer_ = nullptr;
|
||||
SDL_GPUBuffer* vtx_buf_ = nullptr;
|
||||
SDL_GPUSampler* sampler_ = nullptr;
|
||||
static constexpr int kW = 640, kH = 360;
|
||||
};
|
||||
} // namespace sdl3cpp::services::impl
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
@@ -23,6 +25,13 @@ private:
|
||||
std::vector<WorkflowStepDefinition> ParseNodes(
|
||||
const rapidjson::Document& document) const;
|
||||
|
||||
// Expand any workflow.include steps in-place (parse-time composition).
|
||||
// baseDir — directory of the including file, for relative path resolution.
|
||||
// visited — canonical paths already on the include stack (cycle detection).
|
||||
void ResolveIncludes(std::vector<WorkflowStepDefinition>& steps,
|
||||
const std::filesystem::path& baseDir,
|
||||
std::unordered_set<std::string>& visited) const;
|
||||
|
||||
std::shared_ptr<ILogger> logger_;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user