mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-05-04 02:34:52 +00:00
Add MD3 model rendering and fix Q3 bot/weapon visuals
- Add workflow_q3_md3_load_step: load MD3 models from PK3 with correct CCW winding, 1/64 xyz scale + 0.03125 world scale, and per-frame VBs - Add workflow_q3_md3_draw_step: viewmodel (weapon) + world-space MD3 drawing; barrel oriented along model X → world forward - Add workflow_q3_bots_draw_step: 3-part bot model chain (lower→upper→head via MD3 tags) + weapon attachment - Add workflow_q3_bots_spawn_step: spawn bots at indices 1+ so they never overlap the player's spawn point (index 0) - Add workflow_q3_bots_update_step: bot AI (idle/chase/shoot/dead FSM) - Fix workflow_q3_menu_update_step: menu starts closed (was open) - Fix workflow_postfx_composite_step: GPU readback via SDL_DownloadFromGPUTexture captures full 3D scene for screenshots - Fix Metal GPU validation: always bind 2 sampler slots; reuse albedo as dummy shadow map when no shadow texture is available - Reorder q3_frame.json: screenshot trigger nodes run before postfx_composite so screenshot_output_path is set in time Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -288,6 +288,11 @@ if(BUILD_SDL3_APP)
|
||||
src/services/impl/workflow/quake3/workflow_q3_overlay_draw_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
|
||||
src/services/impl/workflow/quake3/workflow_q3_md3_draw_step.cpp
|
||||
src/services/impl/workflow/quake3/workflow_q3_bots_spawn_step.cpp
|
||||
src/services/impl/workflow/quake3/workflow_q3_bots_update_step.cpp
|
||||
src/services/impl/workflow/quake3/workflow_q3_bots_draw_step.cpp
|
||||
src/services/impl/workflow/rendering/workflow_bsp_build_collision_step.cpp
|
||||
src/services/impl/workflow/rendering/workflow_bsp_build_geometry_step.cpp
|
||||
src/services/impl/workflow/rendering/workflow_bsp_entity_update_step.cpp
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Q3 Frame Tick",
|
||||
"description": "Per-frame: poll input, FPS move, render BSP map with post-FX.",
|
||||
"description": "Per-frame: poll input, FPS move, render BSP map with player models, bots, and weapon.",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "input_poll",
|
||||
@@ -111,6 +111,18 @@
|
||||
"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",
|
||||
@@ -152,6 +164,32 @@
|
||||
"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",
|
||||
@@ -203,30 +241,33 @@
|
||||
}
|
||||
],
|
||||
"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": "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": "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 }] } }
|
||||
"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 }] } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Q3 Map Session",
|
||||
"description": "Loads a BSP map and runs the frame loop. Repeated by the outer loop for each map change.",
|
||||
"description": "Loads a BSP map, player/weapon MD3 models, spawns bots, then runs the frame loop.",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "load_bsp",
|
||||
@@ -12,14 +12,12 @@
|
||||
},
|
||||
{
|
||||
"id": "bsp_lightmap",
|
||||
"name": "BSP Lightmap Atlas",
|
||||
"type": "bsp.lightmap_atlas",
|
||||
"typeVersion": 1,
|
||||
"position": [100, 0]
|
||||
},
|
||||
{
|
||||
"id": "bsp_geometry",
|
||||
"name": "BSP Build Geometry",
|
||||
"type": "bsp.build_geometry",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
@@ -27,28 +25,24 @@
|
||||
},
|
||||
{
|
||||
"id": "bsp_textures",
|
||||
"name": "BSP Extract Textures",
|
||||
"type": "bsp.extract_textures",
|
||||
"typeVersion": 1,
|
||||
"position": [300, 0]
|
||||
},
|
||||
{
|
||||
"id": "bsp_upload",
|
||||
"name": "BSP Upload Geometry",
|
||||
"type": "bsp.upload_geometry",
|
||||
"typeVersion": 1,
|
||||
"position": [400, 0]
|
||||
},
|
||||
{
|
||||
"id": "bsp_collision",
|
||||
"name": "BSP Build Collision",
|
||||
"type": "bsp.build_collision",
|
||||
"typeVersion": 1,
|
||||
"position": [500, 0]
|
||||
},
|
||||
{
|
||||
"id": "bsp_spawn",
|
||||
"name": "BSP Parse Spawn",
|
||||
"type": "bsp.parse_spawn",
|
||||
"typeVersion": 1,
|
||||
"position": [600, 0]
|
||||
@@ -59,6 +53,54 @@
|
||||
"typeVersion": 1,
|
||||
"position": [700, 0]
|
||||
},
|
||||
{
|
||||
"id": "load_lower",
|
||||
"type": "q3.md3.load",
|
||||
"typeVersion": 1,
|
||||
"position": [720, 0],
|
||||
"parameters": {
|
||||
"prefix": "lower",
|
||||
"path": "models/players/keel/lower.md3",
|
||||
"anim": "models/players/keel/animation.cfg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "load_upper",
|
||||
"type": "q3.md3.load",
|
||||
"typeVersion": 1,
|
||||
"position": [730, 0],
|
||||
"parameters": {
|
||||
"prefix": "upper",
|
||||
"path": "models/players/keel/upper.md3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "load_head",
|
||||
"type": "q3.md3.load",
|
||||
"typeVersion": 1,
|
||||
"position": [740, 0],
|
||||
"parameters": {
|
||||
"prefix": "head",
|
||||
"path": "models/players/keel/head.md3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "load_weapon_mg",
|
||||
"type": "q3.md3.load",
|
||||
"typeVersion": 1,
|
||||
"position": [750, 0],
|
||||
"parameters": {
|
||||
"prefix": "weapon_mg",
|
||||
"path": "models/weapons2/machinegun/machinegun.md3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bots_spawn",
|
||||
"type": "q3.bots.spawn",
|
||||
"typeVersion": 1,
|
||||
"position": [760, 0],
|
||||
"parameters": { "count": 4 }
|
||||
},
|
||||
{
|
||||
"id": "set_running",
|
||||
"type": "value.literal",
|
||||
@@ -83,15 +125,20 @@
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"load_bsp": { "main": { "0": [{ "node": "bsp_lightmap", "type": "main", "index": 0 }] } },
|
||||
"bsp_lightmap": { "main": { "0": [{ "node": "bsp_geometry", "type": "main", "index": 0 }] } },
|
||||
"bsp_geometry": { "main": { "0": [{ "node": "bsp_textures", "type": "main", "index": 0 }] } },
|
||||
"bsp_textures": { "main": { "0": [{ "node": "bsp_upload", "type": "main", "index": 0 }] } },
|
||||
"bsp_upload": { "main": { "0": [{ "node": "bsp_collision", "type": "main", "index": 0 }] } },
|
||||
"bsp_collision":{ "main": { "0": [{ "node": "bsp_spawn", "type": "main", "index": 0 }] } },
|
||||
"bsp_spawn": { "main": { "0": [{ "node": "spawn_apply", "type": "main", "index": 0 }] } },
|
||||
"spawn_apply": { "main": { "0": [{ "node": "set_running", "type": "main", "index": 0 }] } },
|
||||
"set_running": { "main": { "0": [{ "node": "game_loop", "type": "main", "index": 0 }] } },
|
||||
"game_loop": { "main": { "0": [{ "node": "check_quit", "type": "main", "index": 0 }] } }
|
||||
"load_bsp": { "main": { "0": [{ "node": "bsp_lightmap", "type": "main", "index": 0 }] } },
|
||||
"bsp_lightmap": { "main": { "0": [{ "node": "bsp_geometry", "type": "main", "index": 0 }] } },
|
||||
"bsp_geometry": { "main": { "0": [{ "node": "bsp_textures", "type": "main", "index": 0 }] } },
|
||||
"bsp_textures": { "main": { "0": [{ "node": "bsp_upload", "type": "main", "index": 0 }] } },
|
||||
"bsp_upload": { "main": { "0": [{ "node": "bsp_collision", "type": "main", "index": 0 }] } },
|
||||
"bsp_collision": { "main": { "0": [{ "node": "bsp_spawn", "type": "main", "index": 0 }] } },
|
||||
"bsp_spawn": { "main": { "0": [{ "node": "spawn_apply", "type": "main", "index": 0 }] } },
|
||||
"spawn_apply": { "main": { "0": [{ "node": "load_lower", "type": "main", "index": 0 }] } },
|
||||
"load_lower": { "main": { "0": [{ "node": "load_upper", "type": "main", "index": 0 }] } },
|
||||
"load_upper": { "main": { "0": [{ "node": "load_head", "type": "main", "index": 0 }] } },
|
||||
"load_head": { "main": { "0": [{ "node": "load_weapon_mg","type": "main", "index": 0 }] } },
|
||||
"load_weapon_mg":{ "main": { "0": [{ "node": "bots_spawn", "type": "main", "index": 0 }] } },
|
||||
"bots_spawn": { "main": { "0": [{ "node": "set_running", "type": "main", "index": 0 }] } },
|
||||
"set_running": { "main": { "0": [{ "node": "game_loop", "type": "main", "index": 0 }] } },
|
||||
"game_loop": { "main": { "0": [{ "node": "check_quit", "type": "main", "index": 0 }] } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Q3 Frame Tick",
|
||||
"description": "Per-frame: poll input, FPS move, render BSP map with post-FX.",
|
||||
"description": "Per-frame: poll input, FPS move, render BSP map with bots and weapon. Screenshots after 240 frames then exits.",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "input_poll",
|
||||
@@ -111,6 +111,18 @@
|
||||
"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",
|
||||
@@ -152,6 +164,32 @@
|
||||
"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",
|
||||
@@ -237,34 +275,37 @@
|
||||
}
|
||||
],
|
||||
"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": "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": "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 }] } },
|
||||
"postfx_composite": { "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 }] } }
|
||||
"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 }] } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Q3 Map Session",
|
||||
"description": "Loads a BSP map and runs the frame loop. Repeated by the outer loop for each map change.",
|
||||
"description": "Loads a BSP map, player/weapon MD3 models, spawns bots, then runs the frame loop.",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "load_bsp",
|
||||
@@ -12,14 +12,12 @@
|
||||
},
|
||||
{
|
||||
"id": "bsp_lightmap",
|
||||
"name": "BSP Lightmap Atlas",
|
||||
"type": "bsp.lightmap_atlas",
|
||||
"typeVersion": 1,
|
||||
"position": [100, 0]
|
||||
},
|
||||
{
|
||||
"id": "bsp_geometry",
|
||||
"name": "BSP Build Geometry",
|
||||
"type": "bsp.build_geometry",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
@@ -27,28 +25,24 @@
|
||||
},
|
||||
{
|
||||
"id": "bsp_textures",
|
||||
"name": "BSP Extract Textures",
|
||||
"type": "bsp.extract_textures",
|
||||
"typeVersion": 1,
|
||||
"position": [300, 0]
|
||||
},
|
||||
{
|
||||
"id": "bsp_upload",
|
||||
"name": "BSP Upload Geometry",
|
||||
"type": "bsp.upload_geometry",
|
||||
"typeVersion": 1,
|
||||
"position": [400, 0]
|
||||
},
|
||||
{
|
||||
"id": "bsp_collision",
|
||||
"name": "BSP Build Collision",
|
||||
"type": "bsp.build_collision",
|
||||
"typeVersion": 1,
|
||||
"position": [500, 0]
|
||||
},
|
||||
{
|
||||
"id": "bsp_spawn",
|
||||
"name": "BSP Parse Spawn",
|
||||
"type": "bsp.parse_spawn",
|
||||
"typeVersion": 1,
|
||||
"position": [600, 0]
|
||||
@@ -59,6 +53,54 @@
|
||||
"typeVersion": 1,
|
||||
"position": [700, 0]
|
||||
},
|
||||
{
|
||||
"id": "load_lower",
|
||||
"type": "q3.md3.load",
|
||||
"typeVersion": 1,
|
||||
"position": [720, 0],
|
||||
"parameters": {
|
||||
"prefix": "lower",
|
||||
"path": "models/players/keel/lower.md3",
|
||||
"anim": "models/players/keel/animation.cfg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "load_upper",
|
||||
"type": "q3.md3.load",
|
||||
"typeVersion": 1,
|
||||
"position": [730, 0],
|
||||
"parameters": {
|
||||
"prefix": "upper",
|
||||
"path": "models/players/keel/upper.md3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "load_head",
|
||||
"type": "q3.md3.load",
|
||||
"typeVersion": 1,
|
||||
"position": [740, 0],
|
||||
"parameters": {
|
||||
"prefix": "head",
|
||||
"path": "models/players/keel/head.md3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "load_weapon_mg",
|
||||
"type": "q3.md3.load",
|
||||
"typeVersion": 1,
|
||||
"position": [750, 0],
|
||||
"parameters": {
|
||||
"prefix": "weapon_mg",
|
||||
"path": "models/weapons2/machinegun/machinegun.md3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bots_spawn",
|
||||
"type": "q3.bots.spawn",
|
||||
"typeVersion": 1,
|
||||
"position": [760, 0],
|
||||
"parameters": { "count": 4 }
|
||||
},
|
||||
{
|
||||
"id": "set_running",
|
||||
"type": "value.literal",
|
||||
@@ -83,15 +125,20 @@
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"load_bsp": { "main": { "0": [{ "node": "bsp_lightmap", "type": "main", "index": 0 }] } },
|
||||
"bsp_lightmap": { "main": { "0": [{ "node": "bsp_geometry", "type": "main", "index": 0 }] } },
|
||||
"bsp_geometry": { "main": { "0": [{ "node": "bsp_textures", "type": "main", "index": 0 }] } },
|
||||
"bsp_textures": { "main": { "0": [{ "node": "bsp_upload", "type": "main", "index": 0 }] } },
|
||||
"bsp_upload": { "main": { "0": [{ "node": "bsp_collision", "type": "main", "index": 0 }] } },
|
||||
"bsp_collision":{ "main": { "0": [{ "node": "bsp_spawn", "type": "main", "index": 0 }] } },
|
||||
"bsp_spawn": { "main": { "0": [{ "node": "spawn_apply", "type": "main", "index": 0 }] } },
|
||||
"spawn_apply": { "main": { "0": [{ "node": "set_running", "type": "main", "index": 0 }] } },
|
||||
"set_running": { "main": { "0": [{ "node": "game_loop", "type": "main", "index": 0 }] } },
|
||||
"game_loop": { "main": { "0": [{ "node": "check_quit", "type": "main", "index": 0 }] } }
|
||||
"load_bsp": { "main": { "0": [{ "node": "bsp_lightmap", "type": "main", "index": 0 }] } },
|
||||
"bsp_lightmap": { "main": { "0": [{ "node": "bsp_geometry", "type": "main", "index": 0 }] } },
|
||||
"bsp_geometry": { "main": { "0": [{ "node": "bsp_textures", "type": "main", "index": 0 }] } },
|
||||
"bsp_textures": { "main": { "0": [{ "node": "bsp_upload", "type": "main", "index": 0 }] } },
|
||||
"bsp_upload": { "main": { "0": [{ "node": "bsp_collision", "type": "main", "index": 0 }] } },
|
||||
"bsp_collision": { "main": { "0": [{ "node": "bsp_spawn", "type": "main", "index": 0 }] } },
|
||||
"bsp_spawn": { "main": { "0": [{ "node": "spawn_apply", "type": "main", "index": 0 }] } },
|
||||
"spawn_apply": { "main": { "0": [{ "node": "load_lower", "type": "main", "index": 0 }] } },
|
||||
"load_lower": { "main": { "0": [{ "node": "load_upper", "type": "main", "index": 0 }] } },
|
||||
"load_upper": { "main": { "0": [{ "node": "load_head", "type": "main", "index": 0 }] } },
|
||||
"load_head": { "main": { "0": [{ "node": "load_weapon_mg", "type": "main", "index": 0 }] } },
|
||||
"load_weapon_mg": { "main": { "0": [{ "node": "bots_spawn", "type": "main", "index": 0 }] } },
|
||||
"bots_spawn": { "main": { "0": [{ "node": "set_running", "type": "main", "index": 0 }] } },
|
||||
"set_running": { "main": { "0": [{ "node": "game_loop", "type": "main", "index": 0 }] } },
|
||||
"game_loop": { "main": { "0": [{ "node": "check_quit", "type": "main", "index": 0 }] } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_bots_draw_step.hpp"
|
||||
#include "services/interfaces/workflow/rendering/rendering_types.hpp"
|
||||
#include "services/interfaces/workflow/workflow_step_parameter_resolver.hpp"
|
||||
#include "services/interfaces/workflow_context.hpp"
|
||||
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
namespace {
|
||||
|
||||
// Build a column-major glm matrix from a tag stored in engine Y-up coords.
|
||||
// Tags are stored with axis[i] as the i-th row of the Q3 rotation matrix.
|
||||
// In column-major (glm), we must transpose: column j = (axis[0][j], axis[1][j], axis[2][j]).
|
||||
glm::mat4 TagMatrix(const nlohmann::json& tag) {
|
||||
const auto& ax = tag["axis"];
|
||||
const auto& o = tag["origin"];
|
||||
return glm::mat4(
|
||||
glm::vec4(ax[0][0].get<float>(), ax[1][0].get<float>(), ax[2][0].get<float>(), 0.0f),
|
||||
glm::vec4(ax[0][1].get<float>(), ax[1][1].get<float>(), ax[2][1].get<float>(), 0.0f),
|
||||
glm::vec4(ax[0][2].get<float>(), ax[1][2].get<float>(), ax[2][2].get<float>(), 0.0f),
|
||||
glm::vec4(o[0].get<float>(), o[1].get<float>(), o[2].get<float>(), 1.0f)
|
||||
);
|
||||
}
|
||||
|
||||
// Draw all surfaces of a named MD3 model at the given world transform.
|
||||
void DrawMd3(const std::string& prefix, int frame, const glm::mat4& modelMat,
|
||||
const glm::mat4& view, const glm::mat4& proj, const glm::vec3& camPos,
|
||||
const glm::mat4& shadowVP, const rendering::FragmentUniformData& fu,
|
||||
SDL_GPURenderPass* pass, SDL_GPUCommandBuffer* cmd,
|
||||
SDL_GPUTexture* shadowTex, SDL_GPUSampler* shadowSamp,
|
||||
WorkflowContext& context) {
|
||||
const int nSurfs = context.Get<int>("q3.md3." + prefix + "_num_surfs", 0);
|
||||
if (nSurfs <= 0) return;
|
||||
const int nFrames = context.Get<int>("q3.md3." + prefix + "_num_frames", 1);
|
||||
const int clampedFrame = std::max(0, std::min(frame, nFrames - 1));
|
||||
|
||||
const glm::mat4 mvp = proj * view * modelMat;
|
||||
rendering::VertexUniformData vu = {};
|
||||
std::memcpy(vu.mvp, glm::value_ptr(mvp), sizeof(float) * 16);
|
||||
std::memcpy(vu.model_mat, glm::value_ptr(modelMat), sizeof(float) * 16);
|
||||
vu.normal[1] = 1.0f;
|
||||
vu.uv_scale[0] = 1.0f; vu.uv_scale[1] = 1.0f;
|
||||
vu.camera_pos[0] = camPos.x; vu.camera_pos[1] = camPos.y; vu.camera_pos[2] = camPos.z;
|
||||
std::memcpy(vu.shadow_vp, glm::value_ptr(shadowVP), sizeof(float) * 16);
|
||||
|
||||
for (int s = 0; s < nSurfs; ++s) {
|
||||
const std::string sk = "q3.md3." + prefix + "_surf" + std::to_string(s);
|
||||
auto* vb = context.Get<SDL_GPUBuffer*>(
|
||||
sk + "_f" + std::to_string(clampedFrame) + "_vb", nullptr);
|
||||
auto* ib = context.Get<SDL_GPUBuffer*>(sk + "_ib", nullptr);
|
||||
const int numIdx = context.Get<int>(sk + "_num_idx", 0);
|
||||
if (!vb || !ib || numIdx <= 0) continue;
|
||||
auto* tex = context.Get<SDL_GPUTexture*>(sk + "_tex", nullptr);
|
||||
auto* samp = context.Get<SDL_GPUSampler*>(sk + "_samp", nullptr);
|
||||
if (!tex || !samp) continue;
|
||||
|
||||
// Always bind 2 samplers — shader slot 1 is the shadow map.
|
||||
// Reuse albedo when no shadow texture to avoid Metal null-slot validation errors.
|
||||
{
|
||||
auto* stex = shadowTex ? shadowTex : tex;
|
||||
auto* ssamp = shadowSamp ? shadowSamp : samp;
|
||||
SDL_GPUTextureSamplerBinding b[2] = {{tex, samp}, {stex, ssamp}};
|
||||
SDL_BindGPUFragmentSamplers(pass, 0, b, 2);
|
||||
}
|
||||
SDL_GPUBufferBinding vbb = {vb, 0};
|
||||
SDL_BindGPUVertexBuffers(pass, 0, &vbb, 1);
|
||||
SDL_GPUBufferBinding ibb = {ib, 0};
|
||||
SDL_BindGPUIndexBuffer(pass, &ibb, SDL_GPU_INDEXELEMENTSIZE_16BIT);
|
||||
SDL_PushGPUVertexUniformData(cmd, 0, &vu, sizeof(vu));
|
||||
SDL_PushGPUFragmentUniformData(cmd, 0, &fu, sizeof(fu));
|
||||
SDL_DrawGPUIndexedPrimitives(pass, (uint32_t)numIdx, 1, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Get a tag matrix from the context for a given MD3 prefix + frame + tag name.
|
||||
// Returns identity if the tag is not found.
|
||||
glm::mat4 GetTagMatrix(const std::string& prefix, int frame, const std::string& tagName,
|
||||
WorkflowContext& context) {
|
||||
const auto* tagsJson = context.TryGet<nlohmann::json>("q3.md3." + prefix + "_tags");
|
||||
if (!tagsJson || !tagsJson->is_array() || (int)tagsJson->size() <= frame) return glm::mat4(1.0f);
|
||||
const auto& frameObj = (*tagsJson)[(size_t)frame];
|
||||
if (!frameObj.contains(tagName)) return glm::mat4(1.0f);
|
||||
return TagMatrix(frameObj[tagName]);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
WorkflowQ3BotsDrawStep::WorkflowQ3BotsDrawStep(std::shared_ptr<ILogger> logger)
|
||||
: logger_(std::move(logger)) {}
|
||||
|
||||
std::string WorkflowQ3BotsDrawStep::GetPluginId() const { return "q3.bots.draw"; }
|
||||
|
||||
void WorkflowQ3BotsDrawStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) {
|
||||
if (context.GetBool("frame_skip", false)) return;
|
||||
|
||||
auto* pass = context.Get<SDL_GPURenderPass*>("gpu_render_pass", nullptr);
|
||||
auto* cmd = context.Get<SDL_GPUCommandBuffer*>("gpu_command_buffer", nullptr);
|
||||
auto* pipeline = context.Get<SDL_GPUGraphicsPipeline*>("gpu_pipeline_textured", nullptr);
|
||||
const auto* botsPtr = context.TryGet<nlohmann::json>("q3.bots");
|
||||
if (!pass || !cmd || !pipeline || !botsPtr || !botsPtr->is_array()) return;
|
||||
|
||||
WorkflowStepParameterResolver params;
|
||||
auto getStr = [&](const char* k, const std::string& def) -> std::string {
|
||||
const auto* p = params.FindParameter(step, k);
|
||||
return (p && p->type == WorkflowParameterValue::Type::String) ? p->stringValue : def;
|
||||
};
|
||||
|
||||
const std::string lowerPfx = getStr("lower_prefix", "lower");
|
||||
const std::string upperPfx = getStr("upper_prefix", "upper");
|
||||
const std::string headPfx = getStr("head_prefix", "head");
|
||||
const std::string weaponPfx = getStr("weapon_prefix", "weapon_mg");
|
||||
|
||||
const bool hasLower = context.Get<int>("q3.md3." + lowerPfx + "_num_frames", 0) > 0;
|
||||
const bool hasUpper = context.Get<int>("q3.md3." + upperPfx + "_num_frames", 0) > 0;
|
||||
const bool hasHead = context.Get<int>("q3.md3." + headPfx + "_num_frames", 0) > 0;
|
||||
const bool hasWeapon = context.Get<int>("q3.md3." + weaponPfx + "_num_frames", 0) > 0;
|
||||
if (!hasLower) return; // nothing to draw without lower
|
||||
|
||||
auto view = context.Get<glm::mat4>("render.view_matrix", glm::mat4(1.0f));
|
||||
auto proj = context.Get<glm::mat4>("render.proj_matrix", glm::mat4(1.0f));
|
||||
auto camPos = context.Get<glm::vec3>("render.camera_pos", glm::vec3(0.0f));
|
||||
auto shadowVP = context.Get<glm::mat4>("render.shadow_vp", glm::mat4(1.0f));
|
||||
auto fu = context.Get<rendering::FragmentUniformData>("render.frag_uniforms",
|
||||
rendering::FragmentUniformData{});
|
||||
fu.material[0] = 0.6f; fu.material[1] = 0.1f; // roughness/metallic
|
||||
|
||||
auto* shadowTex = context.Get<SDL_GPUTexture*>("shadow_depth_texture", nullptr);
|
||||
auto* shadowSamp = context.Get<SDL_GPUSampler*>("shadow_depth_sampler", nullptr);
|
||||
|
||||
SDL_BindGPUGraphicsPipeline(pass, pipeline);
|
||||
|
||||
for (const auto& bot : *botsPtr) {
|
||||
if (bot.value("state", std::string{}) == "dead") continue;
|
||||
|
||||
const auto& posJ = bot["pos"];
|
||||
const glm::vec3 bpos(posJ[0].get<float>(), posJ[1].get<float>(), posJ[2].get<float>());
|
||||
const float yaw = bot.value("yaw", 0.0f);
|
||||
const int legFrame = bot.value("leg_frame", 0);
|
||||
const int torsoFrame = bot.value("torso_frame", 0);
|
||||
|
||||
// ── lower.md3: root transform ────────────────────────────────────────
|
||||
const glm::mat4 lowerMat =
|
||||
glm::translate(glm::mat4(1.0f), bpos)
|
||||
* glm::rotate(glm::mat4(1.0f), yaw, glm::vec3(0.0f, 1.0f, 0.0f));
|
||||
|
||||
DrawMd3(lowerPfx, legFrame, lowerMat,
|
||||
view, proj, camPos, shadowVP, fu,
|
||||
pass, cmd, shadowTex, shadowSamp, context);
|
||||
|
||||
if (!hasUpper) continue;
|
||||
|
||||
// ── upper.md3: attached at tag_torso from lower ───────────────────────
|
||||
const glm::mat4 tagTorso = GetTagMatrix(lowerPfx, legFrame, "tag_torso", context);
|
||||
const glm::mat4 upperMat = lowerMat * tagTorso;
|
||||
|
||||
DrawMd3(upperPfx, torsoFrame, upperMat,
|
||||
view, proj, camPos, shadowVP, fu,
|
||||
pass, cmd, shadowTex, shadowSamp, context);
|
||||
|
||||
if (!hasHead) continue;
|
||||
|
||||
// ── head.md3: attached at tag_head from upper ─────────────────────────
|
||||
const glm::mat4 tagHead = GetTagMatrix(upperPfx, torsoFrame, "tag_head", context);
|
||||
const glm::mat4 headMat = upperMat * tagHead;
|
||||
|
||||
DrawMd3(headPfx, 0, headMat,
|
||||
view, proj, camPos, shadowVP, fu,
|
||||
pass, cmd, shadowTex, shadowSamp, context);
|
||||
|
||||
if (!hasWeapon) continue;
|
||||
|
||||
// ── weapon.md3: attached at tag_weapon from upper ─────────────────────
|
||||
const glm::mat4 tagWeapon = GetTagMatrix(upperPfx, torsoFrame, "tag_weapon", context);
|
||||
const glm::mat4 weaponMat = upperMat * tagWeapon;
|
||||
|
||||
DrawMd3(weaponPfx, 0, weaponMat,
|
||||
view, proj, camPos, shadowVP, fu,
|
||||
pass, cmd, shadowTex, shadowSamp, context);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
@@ -0,0 +1,74 @@
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_bots_spawn_step.hpp"
|
||||
#include "services/interfaces/workflow/workflow_step_parameter_resolver.hpp"
|
||||
#include "services/interfaces/workflow_context.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
WorkflowQ3BotsSpawnStep::WorkflowQ3BotsSpawnStep(std::shared_ptr<ILogger> logger)
|
||||
: logger_(std::move(logger)) {}
|
||||
|
||||
std::string WorkflowQ3BotsSpawnStep::GetPluginId() const { return "q3.bots.spawn"; }
|
||||
|
||||
void WorkflowQ3BotsSpawnStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) {
|
||||
// Idempotent: only spawn once
|
||||
const auto* existing = context.TryGet<nlohmann::json>("q3.bots");
|
||||
if (existing && existing->is_array() && !existing->empty()) return;
|
||||
|
||||
WorkflowStepParameterResolver params;
|
||||
const auto* cp = params.FindParameter(step, "count");
|
||||
const int maxBots = (cp && cp->type == WorkflowParameterValue::Type::Number)
|
||||
? (int)cp->numberValue : 4;
|
||||
|
||||
// Collect all deathmatch spawn points from BSP entities
|
||||
const auto* entities = context.TryGet<nlohmann::json>("bsp.entities");
|
||||
std::vector<nlohmann::json> spawnPts;
|
||||
if (entities && entities->is_array()) {
|
||||
for (const auto& ent : *entities) {
|
||||
const std::string cls = ent.value("classname", std::string{});
|
||||
if ((cls == "info_player_deathmatch" || cls == "info_player_start")
|
||||
&& ent.contains("position")) {
|
||||
spawnPts.push_back(ent["position"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (spawnPts.empty()) {
|
||||
// Fallback: scatter bots near origin
|
||||
for (int i = 0; i < maxBots; ++i) {
|
||||
const float a = (float)i * 1.57f;
|
||||
spawnPts.push_back(nlohmann::json::array(
|
||||
{std::cos(a) * 3.0f, 0.5f, std::sin(a) * 3.0f}));
|
||||
}
|
||||
}
|
||||
|
||||
nlohmann::json bots = nlohmann::json::array();
|
||||
// Skip index 0 — that spawn point is reserved for the player (same one
|
||||
// bsp.parse_spawn selects). If there are 2+ spawn points start bots from
|
||||
// index 1 so they never overlap the player. Fall back to index 0 only
|
||||
// when the map has a single spawn point (no better option).
|
||||
const int startIdx = (spawnPts.size() > 1) ? 1 : 0;
|
||||
const int available = (int)spawnPts.size() - startIdx;
|
||||
const int count = std::min(maxBots, available > 0 ? available : maxBots);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const auto& sp = spawnPts[(size_t)(startIdx + i) % spawnPts.size()];
|
||||
nlohmann::json bot;
|
||||
bot["id"] = i;
|
||||
bot["pos"] = sp;
|
||||
bot["yaw"] = 0.0f;
|
||||
bot["health"] = 100;
|
||||
bot["state"] = "idle"; // idle | chase | shoot | dead
|
||||
bot["leg_frame"] = 0;
|
||||
bot["torso_frame"] = 0;
|
||||
bot["weapon"] = "weapon_machinegun";
|
||||
bot["last_shot"] = 0;
|
||||
bots.push_back(bot);
|
||||
}
|
||||
context.Set("q3.bots", bots);
|
||||
if (logger_) logger_->Info("q3.bots.spawn: spawned " + std::to_string(count) + " bots");
|
||||
}
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
@@ -0,0 +1,119 @@
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_bots_update_step.hpp"
|
||||
#include "services/interfaces/workflow/workflow_step_parameter_resolver.hpp"
|
||||
#include "services/interfaces/workflow_context.hpp"
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
WorkflowQ3BotsUpdateStep::WorkflowQ3BotsUpdateStep(std::shared_ptr<ILogger> logger)
|
||||
: logger_(std::move(logger)) {}
|
||||
|
||||
std::string WorkflowQ3BotsUpdateStep::GetPluginId() const { return "q3.bots.update"; }
|
||||
|
||||
void WorkflowQ3BotsUpdateStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) {
|
||||
auto* botsPtr = context.TryGet<nlohmann::json>("q3.bots");
|
||||
if (!botsPtr || !botsPtr->is_array() || botsPtr->empty()) return;
|
||||
|
||||
WorkflowStepParameterResolver params;
|
||||
auto getNum = [&](const char* k, float def) -> float {
|
||||
const auto* p = params.FindParameter(step, k);
|
||||
return (p && p->type == WorkflowParameterValue::Type::Number) ? (float)p->numberValue : def;
|
||||
};
|
||||
|
||||
const float chaseRange = getNum("chase_range", 20.0f);
|
||||
const float shootRange = getNum("shoot_range", 6.0f);
|
||||
const float moveSpeed = getNum("move_speed", 3.0f);
|
||||
const int legIdle = (int)getNum("leg_idle", 162); // Q3 keel defaults
|
||||
const int legRun = (int)getNum("leg_run", 167);
|
||||
const int legRunCnt = (int)getNum("leg_run_cnt", 8);
|
||||
const int torsoStand = (int)getNum("torso_stand", 101);
|
||||
const int torsoAtk = (int)getNum("torso_attack", 107);
|
||||
const int torsoAtkCnt = (int)getNum("torso_atk_cnt", 6);
|
||||
const int shootInterv = (int)getNum("shoot_interval", 30); // frames
|
||||
|
||||
// Player position from camera state
|
||||
const auto camState = context.Get<nlohmann::json>("camera.state", nlohmann::json::object());
|
||||
glm::vec3 playerPos(0.0f);
|
||||
if (camState.contains("position") && camState["position"].is_array()) {
|
||||
const auto& cp = camState["position"];
|
||||
if (cp.size() >= 3)
|
||||
playerPos = {cp[0].get<float>(), cp[1].get<float>(), cp[2].get<float>()};
|
||||
}
|
||||
|
||||
const double dt = context.GetDouble("frame.delta_time", 0.016);
|
||||
const double elapsed = context.GetDouble("frame.elapsed", 0.0);
|
||||
const int globalFrame = (int)(elapsed * 60.0); // ~60fps frame counter
|
||||
|
||||
nlohmann::json bots = *botsPtr;
|
||||
nlohmann::json shots = nlohmann::json::array();
|
||||
|
||||
for (auto& bot : bots) {
|
||||
if (bot.value("state", std::string{}) == "dead") continue;
|
||||
|
||||
const auto& posJ = bot["pos"];
|
||||
glm::vec3 bpos(posJ[0].get<float>(), posJ[1].get<float>(), posJ[2].get<float>());
|
||||
|
||||
const glm::vec3 toPlayer = playerPos - bpos;
|
||||
const float dist = std::sqrt(toPlayer.x * toPlayer.x +
|
||||
toPlayer.y * toPlayer.y +
|
||||
toPlayer.z * toPlayer.z);
|
||||
|
||||
// Face player: yaw = atan2 of horizontal direction (XZ plane in Y-up)
|
||||
const float targetYaw = std::atan2(toPlayer.x, toPlayer.z);
|
||||
bot["yaw"] = targetYaw;
|
||||
|
||||
// State transitions
|
||||
if (dist < shootRange) {
|
||||
bot["state"] = "shoot";
|
||||
} else if (dist < chaseRange) {
|
||||
bot["state"] = "chase";
|
||||
} else {
|
||||
bot["state"] = "idle";
|
||||
}
|
||||
|
||||
const std::string state = bot["state"].get<std::string>();
|
||||
|
||||
// Movement
|
||||
if (state == "chase" && dist > 0.5f) {
|
||||
const glm::vec3 dir = toPlayer / dist;
|
||||
bpos.x += dir.x * moveSpeed * (float)dt;
|
||||
bpos.z += dir.z * moveSpeed * (float)dt;
|
||||
// Keep Y from spawn (basic ground-snapping; proper would do physics)
|
||||
bot["pos"] = nlohmann::json::array({bpos.x, bpos.y, bpos.z});
|
||||
}
|
||||
|
||||
// Animation frame selection
|
||||
const double fps = 15.0;
|
||||
const int baseFrame = (int)(elapsed * fps);
|
||||
if (state == "chase") {
|
||||
bot["leg_frame"] = legRun + (legRunCnt > 0 ? (baseFrame % legRunCnt) : 0);
|
||||
bot["torso_frame"] = torsoStand;
|
||||
} else if (state == "shoot") {
|
||||
bot["leg_frame"] = legIdle;
|
||||
bot["torso_frame"] = torsoAtk + (torsoAtkCnt > 0 ? (baseFrame % torsoAtkCnt) : 0);
|
||||
|
||||
// Fire shot
|
||||
const int lastShot = bot.value("last_shot", 0);
|
||||
if (globalFrame >= lastShot + shootInterv) {
|
||||
bot["last_shot"] = globalFrame;
|
||||
shots.push_back({
|
||||
{"bot_id", bot["id"]},
|
||||
{"from", bot["pos"]},
|
||||
{"to", nlohmann::json::array({playerPos.x, playerPos.y, playerPos.z})}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
bot["leg_frame"] = legIdle;
|
||||
bot["torso_frame"] = torsoStand;
|
||||
}
|
||||
}
|
||||
|
||||
context.Set("q3.bots", bots);
|
||||
if (!shots.empty()) context.Set("q3.bot_shots", shots);
|
||||
}
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
@@ -0,0 +1,186 @@
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_md3_draw_step.hpp"
|
||||
#include "services/interfaces/workflow/rendering/rendering_types.hpp"
|
||||
#include "services/interfaces/workflow/workflow_step_parameter_resolver.hpp"
|
||||
#include "services/interfaces/workflow_context.hpp"
|
||||
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
namespace {
|
||||
|
||||
// Build a glm column-major matrix from a Q3/MD3 tag stored in engine Y-up coords.
|
||||
// The tag axis[i] rows (already coord-converted) must be transposed to form
|
||||
// the columns of a column-major matrix.
|
||||
glm::mat4 TagMatrix(const nlohmann::json& tag) {
|
||||
const auto& ax = tag["axis"]; // 3 rows in engine space
|
||||
const auto& o = tag["origin"];
|
||||
// Transpose: row i of Q3 axis → column i of glm matrix
|
||||
return glm::mat4(
|
||||
glm::vec4(ax[0][0].get<float>(), ax[1][0].get<float>(), ax[2][0].get<float>(), 0.0f),
|
||||
glm::vec4(ax[0][1].get<float>(), ax[1][1].get<float>(), ax[2][1].get<float>(), 0.0f),
|
||||
glm::vec4(ax[0][2].get<float>(), ax[1][2].get<float>(), ax[2][2].get<float>(), 0.0f),
|
||||
glm::vec4(o[0].get<float>(), o[1].get<float>(), o[2].get<float>(), 1.0f)
|
||||
);
|
||||
}
|
||||
|
||||
// Draw all surfaces of a named MD3 at the given model matrix.
|
||||
void DrawMd3Surfaces(const std::string& prefix, int frame,
|
||||
const glm::mat4& model, const glm::mat4& view, const glm::mat4& proj,
|
||||
const glm::vec3& camPos, const glm::mat4& shadowVP,
|
||||
const rendering::FragmentUniformData& fu,
|
||||
SDL_GPURenderPass* pass, SDL_GPUCommandBuffer* cmd,
|
||||
SDL_GPUTexture* shadowTex, SDL_GPUSampler* shadowSamp,
|
||||
WorkflowContext& context) {
|
||||
const int nSurfs = context.Get<int>("q3.md3." + prefix + "_num_surfs", 0);
|
||||
if (nSurfs <= 0) return;
|
||||
|
||||
const glm::mat4 mvp = proj * view * model;
|
||||
rendering::VertexUniformData vu = {};
|
||||
std::memcpy(vu.mvp, glm::value_ptr(mvp), sizeof(float) * 16);
|
||||
std::memcpy(vu.model_mat, glm::value_ptr(model), sizeof(float) * 16);
|
||||
vu.normal[1] = 1.0f;
|
||||
vu.uv_scale[0] = 1.0f; vu.uv_scale[1] = 1.0f;
|
||||
vu.camera_pos[0] = camPos.x; vu.camera_pos[1] = camPos.y; vu.camera_pos[2] = camPos.z;
|
||||
std::memcpy(vu.shadow_vp, glm::value_ptr(shadowVP), sizeof(float) * 16);
|
||||
|
||||
for (int s = 0; s < nSurfs; ++s) {
|
||||
const std::string sk = "q3.md3." + prefix + "_surf" + std::to_string(s);
|
||||
auto* vb = context.Get<SDL_GPUBuffer*>(sk + "_f" + std::to_string(frame) + "_vb", nullptr);
|
||||
auto* ib = context.Get<SDL_GPUBuffer*>(sk + "_ib", nullptr);
|
||||
const int numIdx = context.Get<int>(sk + "_num_idx", 0);
|
||||
if (!vb || !ib || numIdx <= 0) continue;
|
||||
auto* tex = context.Get<SDL_GPUTexture*>(sk + "_tex", nullptr);
|
||||
auto* samp = context.Get<SDL_GPUSampler*>(sk + "_samp", nullptr);
|
||||
if (!tex || !samp) continue;
|
||||
|
||||
// Always bind 2 samplers — shader slot 1 is the shadow map.
|
||||
// When no shadow texture is available, reuse the albedo texture so slot 1
|
||||
// is never null (Metal validation error). The shadow UV will be out of bounds
|
||||
// (shadow_vp is identity) so ComputeShadowPCF returns 1.0 (no shadow).
|
||||
{
|
||||
auto* stex = shadowTex ? shadowTex : tex;
|
||||
auto* ssamp = shadowSamp ? shadowSamp : samp;
|
||||
SDL_GPUTextureSamplerBinding b[2] = {{tex, samp}, {stex, ssamp}};
|
||||
SDL_BindGPUFragmentSamplers(pass, 0, b, 2);
|
||||
}
|
||||
SDL_GPUBufferBinding vbBind = {vb, 0};
|
||||
SDL_BindGPUVertexBuffers(pass, 0, &vbBind, 1);
|
||||
SDL_GPUBufferBinding ibBind = {ib, 0};
|
||||
SDL_BindGPUIndexBuffer(pass, &ibBind, SDL_GPU_INDEXELEMENTSIZE_16BIT);
|
||||
SDL_PushGPUVertexUniformData(cmd, 0, &vu, sizeof(vu));
|
||||
SDL_PushGPUFragmentUniformData(cmd, 0, &fu, sizeof(fu));
|
||||
SDL_DrawGPUIndexedPrimitives(pass, (uint32_t)numIdx, 1, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
WorkflowQ3Md3DrawStep::WorkflowQ3Md3DrawStep(std::shared_ptr<ILogger> logger)
|
||||
: logger_(std::move(logger)) {}
|
||||
|
||||
std::string WorkflowQ3Md3DrawStep::GetPluginId() const { return "q3.md3.draw"; }
|
||||
|
||||
void WorkflowQ3Md3DrawStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) {
|
||||
if (context.GetBool("frame_skip", false)) return;
|
||||
|
||||
WorkflowStepParameterResolver params;
|
||||
auto getStr = [&](const char* k, const std::string& def) -> std::string {
|
||||
const auto* p = params.FindParameter(step, k);
|
||||
return (p && p->type == WorkflowParameterValue::Type::String) ? p->stringValue : def;
|
||||
};
|
||||
auto getNum = [&](const char* k, float def) -> float {
|
||||
const auto* p = params.FindParameter(step, k);
|
||||
return (p && p->type == WorkflowParameterValue::Type::Number) ? (float)p->numberValue : def;
|
||||
};
|
||||
auto getBool = [&](const char* k, bool def) -> bool {
|
||||
const auto* p = params.FindParameter(step, k);
|
||||
return (p && p->type == WorkflowParameterValue::Type::Bool) ? p->boolValue : def;
|
||||
};
|
||||
|
||||
const std::string prefix = getStr("prefix", "model");
|
||||
const std::string pos_key = getStr("pos_key", "");
|
||||
const std::string yaw_key = getStr("yaw_key", "");
|
||||
const std::string frame_key = getStr("frame_key", "");
|
||||
const float fps = getNum("fps", 15.0f);
|
||||
const int animFirst = (int)getNum("anim_first", 0.0f);
|
||||
const int animCount = (int)getNum("anim_count", 0.0f);
|
||||
const bool viewmodel = getBool("viewmodel", false);
|
||||
const float vmRight = getNum("vm_right", 0.35f);
|
||||
const float vmDown = getNum("vm_down", -0.3f);
|
||||
const float vmFwd = getNum("vm_fwd", 0.5f);
|
||||
|
||||
auto* pass = context.Get<SDL_GPURenderPass*>("gpu_render_pass", nullptr);
|
||||
auto* cmd = context.Get<SDL_GPUCommandBuffer*>("gpu_command_buffer", nullptr);
|
||||
auto* pipeline = context.Get<SDL_GPUGraphicsPipeline*>("gpu_pipeline_textured", nullptr);
|
||||
if (!pass || !cmd || !pipeline) return;
|
||||
|
||||
const int nFrames = context.Get<int>("q3.md3." + prefix + "_num_frames", 0);
|
||||
if (nFrames <= 0) return;
|
||||
|
||||
// ── Determine animation frame ─────────────────────────────────────────────
|
||||
int frame = 0;
|
||||
if (!frame_key.empty()) {
|
||||
frame = context.Get<int>(frame_key, 0);
|
||||
} else {
|
||||
const double elapsed = context.GetDouble("frame.elapsed", 0.0);
|
||||
const int totalFrame = (int)(elapsed * fps);
|
||||
frame = (animCount > 0)
|
||||
? animFirst + (totalFrame % animCount)
|
||||
: (totalFrame % nFrames);
|
||||
}
|
||||
frame = std::max(0, std::min(frame, nFrames - 1));
|
||||
|
||||
// ── Build world model matrix ──────────────────────────────────────────────
|
||||
auto view = context.Get<glm::mat4>("render.view_matrix", glm::mat4(1.0f));
|
||||
auto proj = context.Get<glm::mat4>("render.proj_matrix", glm::mat4(1.0f));
|
||||
auto camPos = context.Get<glm::vec3>("render.camera_pos", glm::vec3(0.0f));
|
||||
auto shadowVP = context.Get<glm::mat4>("render.shadow_vp", glm::mat4(1.0f));
|
||||
auto fu = context.Get<rendering::FragmentUniformData>("render.frag_uniforms",
|
||||
rendering::FragmentUniformData{});
|
||||
|
||||
glm::mat4 model(1.0f);
|
||||
if (viewmodel) {
|
||||
glm::vec3 right (view[0][0], view[1][0], view[2][0]);
|
||||
glm::vec3 up (view[0][1], view[1][1], view[2][1]);
|
||||
glm::vec3 forward(-view[0][2],-view[1][2],-view[2][2]);
|
||||
glm::vec3 pos = camPos + right * vmRight + up * vmDown + forward * vmFwd;
|
||||
// MD3 weapon models have barrel along local +X.
|
||||
// Map: model X → world forward (barrel points into screen)
|
||||
// model Y → world up
|
||||
// model Z → world right (right-handed: right × forward = up ✓)
|
||||
glm::mat4 orient(1.0f);
|
||||
orient[0] = glm::vec4(forward, 0.0f);
|
||||
orient[1] = glm::vec4(up, 0.0f);
|
||||
orient[2] = glm::vec4(right, 0.0f);
|
||||
model = glm::translate(glm::mat4(1.0f), pos) * orient;
|
||||
} else {
|
||||
glm::vec3 pos(0.0f);
|
||||
if (!pos_key.empty()) {
|
||||
const auto* pv = context.TryGet<nlohmann::json>(pos_key);
|
||||
if (pv && pv->is_array() && pv->size() >= 3)
|
||||
pos = glm::vec3((*pv)[0].get<float>(), (*pv)[1].get<float>(), (*pv)[2].get<float>());
|
||||
}
|
||||
float yaw = 0.0f;
|
||||
if (!yaw_key.empty()) yaw = context.Get<float>(yaw_key, 0.0f);
|
||||
model = glm::translate(glm::mat4(1.0f), pos)
|
||||
* glm::rotate(glm::mat4(1.0f), yaw, glm::vec3(0.0f, 1.0f, 0.0f));
|
||||
}
|
||||
|
||||
auto* shadowTex = context.Get<SDL_GPUTexture*>("shadow_depth_texture", nullptr);
|
||||
auto* shadowSamp = context.Get<SDL_GPUSampler*>("shadow_depth_sampler", nullptr);
|
||||
|
||||
SDL_BindGPUGraphicsPipeline(pass, pipeline);
|
||||
DrawMd3Surfaces(prefix, frame, model, view, proj, camPos, shadowVP, fu,
|
||||
pass, cmd, shadowTex, shadowSamp, context);
|
||||
}
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
@@ -0,0 +1,353 @@
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_md3_load_step.hpp"
|
||||
#include "services/interfaces/workflow/workflow_step_parameter_resolver.hpp"
|
||||
#include "services/interfaces/workflow_context.hpp"
|
||||
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <stb_image.h>
|
||||
#include <zip.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <stdint.h>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
namespace {
|
||||
|
||||
// MD3 XYZ scale: Q3 stores vertices as int16 * (1/64) = Q3 world units
|
||||
constexpr float kMd3XyzScale = 1.0f / 64.0f;
|
||||
|
||||
// BSP world scale: 1 Q3 world unit → 0.03125 engine units (matches bsp.load scale=0.03125)
|
||||
// Combined scale: raw_xyz * kMd3XyzScale * kMd3WorldScale → engine units
|
||||
constexpr float kMd3WorldScale = 0.03125f;
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct Md3Header {
|
||||
int32_t ident, version;
|
||||
char name[64];
|
||||
int32_t flags, numFrames, numTags, numSurfaces, numSkins;
|
||||
int32_t ofsFrames, ofsTags, ofsSurfaces, ofsEnd;
|
||||
};
|
||||
struct Md3Tag {
|
||||
char name[64];
|
||||
float origin[3];
|
||||
float axis[3][3]; // row-major: axis[0]=right, axis[1]=fwd, axis[2]=up in Q3 space
|
||||
};
|
||||
struct Md3Surface {
|
||||
int32_t ident;
|
||||
char name[64];
|
||||
int32_t flags, numFrames, numShaders, numVerts, numTriangles;
|
||||
int32_t ofsTriangles, ofsShaders, ofsSt, ofsXyzNormals, ofsEnd;
|
||||
};
|
||||
struct Md3Triangle { int32_t indexes[3]; };
|
||||
struct Md3Shader { char name[64]; int32_t shaderIndex; };
|
||||
struct Md3St { float st[2]; };
|
||||
struct Md3XyzNormal { int16_t xyz[3]; int16_t normal; };
|
||||
#pragma pack(pop)
|
||||
|
||||
// ── PK3 helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
static std::vector<uint8_t> ReadFromPk3(const std::string& pk3, const std::string& entry) {
|
||||
int err = 0;
|
||||
zip_t* arc = zip_open(pk3.c_str(), ZIP_RDONLY, &err);
|
||||
if (!arc) return {};
|
||||
zip_stat_t st;
|
||||
if (zip_stat(arc, entry.c_str(), 0, &st) != 0) { zip_close(arc); return {}; }
|
||||
std::vector<uint8_t> buf(st.size);
|
||||
zip_file_t* zf = zip_fopen(arc, entry.c_str(), 0);
|
||||
if (!zf) { zip_close(arc); return {}; }
|
||||
zip_fread(zf, buf.data(), st.size);
|
||||
zip_fclose(zf);
|
||||
zip_close(arc);
|
||||
return buf;
|
||||
}
|
||||
|
||||
// ── GPU upload helpers ───────────────────────────────────────────────────────
|
||||
|
||||
static SDL_GPUBuffer* UploadBuffer(SDL_GPUDevice* dev, SDL_GPUBufferUsageFlags usage,
|
||||
const void* data, uint32_t size) {
|
||||
SDL_GPUBufferCreateInfo bi = {}; bi.usage = usage; bi.size = size;
|
||||
auto* buf = SDL_CreateGPUBuffer(dev, &bi);
|
||||
if (!buf) return nullptr;
|
||||
SDL_GPUTransferBufferCreateInfo tbi = {};
|
||||
tbi.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; tbi.size = size;
|
||||
auto* tb = SDL_CreateGPUTransferBuffer(dev, &tbi);
|
||||
std::memcpy(SDL_MapGPUTransferBuffer(dev, tb, false), data, size);
|
||||
SDL_UnmapGPUTransferBuffer(dev, tb);
|
||||
auto* cmd = SDL_AcquireGPUCommandBuffer(dev);
|
||||
auto* cp = SDL_BeginGPUCopyPass(cmd);
|
||||
SDL_GPUTransferBufferLocation src = {tb, 0};
|
||||
SDL_GPUBufferRegion dst = {buf, 0, size};
|
||||
SDL_UploadToGPUBuffer(cp, &src, &dst, false);
|
||||
SDL_EndGPUCopyPass(cp); SDL_SubmitGPUCommandBuffer(cmd);
|
||||
SDL_ReleaseGPUTransferBuffer(dev, tb);
|
||||
return buf;
|
||||
}
|
||||
|
||||
static SDL_GPUTexture* UploadTexture(SDL_GPUDevice* dev, const uint8_t* px, int w, int h) {
|
||||
SDL_GPUTextureCreateInfo ti = {};
|
||||
ti.type = SDL_GPU_TEXTURETYPE_2D;
|
||||
ti.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
|
||||
ti.width = (uint32_t)w; ti.height = (uint32_t)h;
|
||||
ti.layer_count_or_depth = 1; ti.num_levels = 1;
|
||||
ti.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
|
||||
auto* tex = SDL_CreateGPUTexture(dev, &ti);
|
||||
if (!tex) return nullptr;
|
||||
uint32_t bytes = (uint32_t)(w * h * 4);
|
||||
SDL_GPUTransferBufferCreateInfo tbi = {};
|
||||
tbi.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; tbi.size = bytes;
|
||||
auto* tb = SDL_CreateGPUTransferBuffer(dev, &tbi);
|
||||
std::memcpy(SDL_MapGPUTransferBuffer(dev, tb, false), px, bytes);
|
||||
SDL_UnmapGPUTransferBuffer(dev, tb);
|
||||
auto* cmd = SDL_AcquireGPUCommandBuffer(dev);
|
||||
auto* cp = SDL_BeginGPUCopyPass(cmd);
|
||||
SDL_GPUTextureTransferInfo src = {}; src.transfer_buffer = tb;
|
||||
src.pixels_per_row = (uint32_t)w; src.rows_per_layer = (uint32_t)h;
|
||||
SDL_GPUTextureRegion dst = {};
|
||||
dst.texture = tex; dst.w = (uint32_t)w; dst.h = (uint32_t)h; dst.d = 1;
|
||||
SDL_UploadToGPUTexture(cp, &src, &dst, false);
|
||||
SDL_EndGPUCopyPass(cp); SDL_SubmitGPUCommandBuffer(cmd);
|
||||
SDL_ReleaseGPUTransferBuffer(dev, tb);
|
||||
return tex;
|
||||
}
|
||||
|
||||
static SDL_GPUSampler* MakeLinearSampler(SDL_GPUDevice* dev) {
|
||||
SDL_GPUSamplerCreateInfo si = {};
|
||||
si.min_filter = SDL_GPU_FILTER_LINEAR; si.mag_filter = SDL_GPU_FILTER_LINEAR;
|
||||
si.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_LINEAR;
|
||||
si.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_REPEAT;
|
||||
si.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_REPEAT;
|
||||
si.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_REPEAT;
|
||||
return SDL_CreateGPUSampler(dev, &si);
|
||||
}
|
||||
|
||||
static SDL_GPUTexture* TryLoadTexture(SDL_GPUDevice* dev, const std::string& pk3,
|
||||
const std::vector<std::string>& candidates) {
|
||||
for (const auto& entry : candidates) {
|
||||
auto raw = ReadFromPk3(pk3, entry);
|
||||
if (raw.empty()) continue;
|
||||
int w, h, ch;
|
||||
unsigned char* px = stbi_load_from_memory(raw.data(), (int)raw.size(), &w, &h, &ch, 4);
|
||||
if (!px) continue;
|
||||
auto* tex = UploadTexture(dev, px, w, h);
|
||||
stbi_image_free(px);
|
||||
if (tex) return tex;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ── animation.cfg parser ─────────────────────────────────────────────────────
|
||||
|
||||
static nlohmann::json ParseAnimCfg(const std::vector<uint8_t>& data) {
|
||||
nlohmann::json result = nlohmann::json::array();
|
||||
std::string txt(data.begin(), data.end());
|
||||
std::istringstream ss(txt);
|
||||
std::string line;
|
||||
while (std::getline(ss, line)) {
|
||||
auto cp = line.find("//");
|
||||
if (cp != std::string::npos) line = line.substr(0, cp);
|
||||
std::istringstream ls(line);
|
||||
int first, num, loop, fps;
|
||||
if (ls >> first >> num >> loop >> fps) {
|
||||
result.push_back({{"first", first}, {"num", num},
|
||||
{"loop", loop}, {"fps", fps}});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ── Step implementation ──────────────────────────────────────────────────────
|
||||
|
||||
WorkflowQ3Md3LoadStep::WorkflowQ3Md3LoadStep(std::shared_ptr<ILogger> logger)
|
||||
: logger_(std::move(logger)) {}
|
||||
|
||||
std::string WorkflowQ3Md3LoadStep::GetPluginId() const { return "q3.md3.load"; }
|
||||
|
||||
void WorkflowQ3Md3LoadStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) {
|
||||
WorkflowStepParameterResolver params;
|
||||
auto getStr = [&](const char* k, const std::string& def) -> std::string {
|
||||
const auto* p = params.FindParameter(step, k);
|
||||
return (p && p->type == WorkflowParameterValue::Type::String) ? p->stringValue : def;
|
||||
};
|
||||
|
||||
const std::string prefix = getStr("prefix", "model");
|
||||
const std::string path = getStr("path", "");
|
||||
const std::string skin = getStr("skin", "");
|
||||
const std::string anim = getStr("anim", "");
|
||||
|
||||
// Idempotent: skip if already loaded
|
||||
if (context.Get<int>("q3.md3." + prefix + "_num_frames", -1) >= 0) return;
|
||||
|
||||
// Read pk3 path from bsp_config (set by bsp.load step)
|
||||
const auto bspCfg = context.Get<nlohmann::json>("bsp_config", nlohmann::json{});
|
||||
const std::string pk3 = bspCfg.value("pk3_path", std::string(""));
|
||||
auto* device = context.Get<SDL_GPUDevice*>("gpu_device", nullptr);
|
||||
if (pk3.empty() || !device || path.empty()) {
|
||||
if (logger_) logger_->Warn("q3.md3.load[" + prefix + "]: pk3/device/path missing");
|
||||
return;
|
||||
}
|
||||
|
||||
auto md3raw = ReadFromPk3(pk3, path);
|
||||
if (md3raw.size() < sizeof(Md3Header)) {
|
||||
if (logger_) logger_->Warn("q3.md3.load[" + prefix + "]: cannot read " + path);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t* base = md3raw.data();
|
||||
const auto& hdr = *reinterpret_cast<const Md3Header*>(base);
|
||||
const int nFrames = hdr.numFrames, nTags = hdr.numTags, nSurfs = hdr.numSurfaces;
|
||||
if (nFrames <= 0 || nSurfs <= 0) return;
|
||||
|
||||
// ── Tags: convert from Q3 Z-up → engine Y-up ─────────────────────────────
|
||||
// Q3 tag axis[i] are rows of the rotation matrix (right, forward, up in Q3 space).
|
||||
// In engine space: x'=x, y'=z, z'=-y (for both origin and each basis vector).
|
||||
nlohmann::json tagsJson = nlohmann::json::array();
|
||||
if (nTags > 0 && hdr.ofsTags > 0) {
|
||||
const auto* rawTags = reinterpret_cast<const Md3Tag*>(base + hdr.ofsTags);
|
||||
for (int f = 0; f < nFrames; ++f) {
|
||||
nlohmann::json frameObj = nlohmann::json::object();
|
||||
for (int t = 0; t < nTags; ++t) {
|
||||
const auto& tag = rawTags[f * nTags + t];
|
||||
std::string tname(tag.name, strnlen(tag.name, 64));
|
||||
|
||||
// Origin in engine coords (scaled to match BSP world)
|
||||
nlohmann::json tagObj;
|
||||
tagObj["origin"] = nlohmann::json::array({
|
||||
tag.origin[0] * kMd3XyzScale * kMd3WorldScale,
|
||||
tag.origin[2] * kMd3XyzScale * kMd3WorldScale, // Q3 Z → eng Y
|
||||
-tag.origin[1] * kMd3XyzScale * kMd3WorldScale // -Q3 Y → eng Z
|
||||
});
|
||||
|
||||
// Axis rows converted to engine space
|
||||
nlohmann::json axisArr = nlohmann::json::array();
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
axisArr.push_back(nlohmann::json::array({
|
||||
tag.axis[i][0],
|
||||
tag.axis[i][2], // Q3 Z → eng Y
|
||||
-tag.axis[i][1] // -Q3 Y → eng Z
|
||||
}));
|
||||
}
|
||||
tagObj["axis"] = axisArr;
|
||||
frameObj[tname] = tagObj;
|
||||
}
|
||||
tagsJson.push_back(frameObj);
|
||||
}
|
||||
}
|
||||
context.Set("q3.md3." + prefix + "_tags", tagsJson);
|
||||
context.Set<int>("q3.md3." + prefix + "_num_frames", nFrames);
|
||||
context.Set<int>("q3.md3." + prefix + "_num_surfs", nSurfs);
|
||||
context.Set<int>("q3.md3." + prefix + "_num_tags", nTags);
|
||||
|
||||
// ── animation.cfg ─────────────────────────────────────────────────────────
|
||||
if (!anim.empty()) {
|
||||
auto animData = ReadFromPk3(pk3, anim);
|
||||
if (!animData.empty())
|
||||
context.Set("q3.md3." + prefix + "_anim", ParseAnimCfg(animData));
|
||||
}
|
||||
|
||||
// Derive the directory from path (for relative skin lookups)
|
||||
std::string pathDir = path;
|
||||
auto lastSlash = pathDir.rfind('/');
|
||||
if (lastSlash != std::string::npos) pathDir = pathDir.substr(0, lastSlash + 1);
|
||||
|
||||
// ── Surfaces ──────────────────────────────────────────────────────────────
|
||||
const uint8_t* surfPtr = base + hdr.ofsSurfaces;
|
||||
|
||||
struct PosUv { float x, y, z, u, v; }; // matches pipeline "position_uv"
|
||||
|
||||
for (int s = 0; s < nSurfs; ++s) {
|
||||
const auto& surf = *reinterpret_cast<const Md3Surface*>(surfPtr);
|
||||
const std::string sk = "q3.md3." + prefix + "_surf" + std::to_string(s);
|
||||
|
||||
if (surf.numVerts <= 0 || surf.numTriangles <= 0) {
|
||||
surfPtr += surf.ofsEnd; continue;
|
||||
}
|
||||
|
||||
// Index buffer (uint16) — MD3 triangles are already CCW (OpenGL convention), no swap needed
|
||||
const auto* tris = reinterpret_cast<const Md3Triangle*>(surfPtr + surf.ofsTriangles);
|
||||
std::vector<uint16_t> indices;
|
||||
indices.reserve(surf.numTriangles * 3);
|
||||
for (int i = 0; i < surf.numTriangles; ++i) {
|
||||
indices.push_back((uint16_t)tris[i].indexes[0]);
|
||||
indices.push_back((uint16_t)tris[i].indexes[1]);
|
||||
indices.push_back((uint16_t)tris[i].indexes[2]);
|
||||
}
|
||||
auto* ib = UploadBuffer(device, SDL_GPU_BUFFERUSAGE_INDEX,
|
||||
indices.data(), (uint32_t)(indices.size() * 2));
|
||||
context.Set<SDL_GPUBuffer*>(sk + "_ib", ib);
|
||||
context.Set<int>(sk + "_num_idx", (int)indices.size());
|
||||
|
||||
// UV coordinates (shared across all frames)
|
||||
const auto* uvs = reinterpret_cast<const Md3St*>(surfPtr + surf.ofsSt);
|
||||
|
||||
// Per-frame vertex buffers: decode int16 xyz → float, convert coords, bake scale
|
||||
const auto* xyzn = reinterpret_cast<const Md3XyzNormal*>(surfPtr + surf.ofsXyzNormals);
|
||||
const int nv = surf.numVerts;
|
||||
std::vector<PosUv> verts((size_t)nv);
|
||||
|
||||
for (int f = 0; f < nFrames; ++f) {
|
||||
const Md3XyzNormal* fxyz = xyzn + f * nv;
|
||||
for (int vi = 0; vi < nv; ++vi) {
|
||||
// Q3 Z-up → engine Y-up, scaled to BSP world
|
||||
verts[vi].x = fxyz[vi].xyz[0] * kMd3XyzScale * kMd3WorldScale;
|
||||
verts[vi].y = fxyz[vi].xyz[2] * kMd3XyzScale * kMd3WorldScale;
|
||||
verts[vi].z = -fxyz[vi].xyz[1] * kMd3XyzScale * kMd3WorldScale;
|
||||
verts[vi].u = uvs[vi].st[0];
|
||||
verts[vi].v = uvs[vi].st[1];
|
||||
}
|
||||
auto* vb = UploadBuffer(device, SDL_GPU_BUFFERUSAGE_VERTEX,
|
||||
verts.data(), (uint32_t)(nv * sizeof(PosUv)));
|
||||
context.Set<SDL_GPUBuffer*>(sk + "_f" + std::to_string(f) + "_vb", vb);
|
||||
}
|
||||
|
||||
// ── Texture: prefer shader name from surface, then path-derived candidates ──
|
||||
std::string shaderName;
|
||||
if (surf.numShaders > 0) {
|
||||
const auto* sh = reinterpret_cast<const Md3Shader*>(surfPtr + surf.ofsShaders);
|
||||
shaderName = std::string(sh[0].name, strnlen(sh[0].name, 64));
|
||||
}
|
||||
std::vector<std::string> texCandidates;
|
||||
if (!shaderName.empty()) {
|
||||
texCandidates.push_back(shaderName + ".tga");
|
||||
texCandidates.push_back(shaderName + ".jpg");
|
||||
texCandidates.push_back(shaderName);
|
||||
}
|
||||
if (!skin.empty()) {
|
||||
texCandidates.push_back(skin);
|
||||
auto dot = skin.rfind('.');
|
||||
if (dot != std::string::npos)
|
||||
texCandidates.push_back(skin.substr(0, dot) + ".jpg");
|
||||
}
|
||||
// Fallback: same name as MD3 but with texture extensions
|
||||
{
|
||||
std::string noext = path;
|
||||
auto dot = noext.rfind('.');
|
||||
if (dot != std::string::npos) noext = noext.substr(0, dot);
|
||||
texCandidates.push_back(noext + ".tga");
|
||||
texCandidates.push_back(noext + ".jpg");
|
||||
}
|
||||
|
||||
auto* tex = TryLoadTexture(device, pk3, texCandidates);
|
||||
if (!tex) {
|
||||
// 1×1 grey fallback
|
||||
const uint8_t grey[4] = {128, 128, 128, 255};
|
||||
tex = UploadTexture(device, grey, 1, 1);
|
||||
}
|
||||
auto* samp = MakeLinearSampler(device);
|
||||
context.Set<SDL_GPUTexture*>(sk + "_tex", tex);
|
||||
context.Set<SDL_GPUSampler*>(sk + "_samp", samp);
|
||||
|
||||
surfPtr += surf.ofsEnd;
|
||||
}
|
||||
|
||||
if (logger_) logger_->Info("q3.md3.load[" + prefix + "]: loaded " +
|
||||
path + " (" + std::to_string(nSurfs) + " surfs, " +
|
||||
std::to_string(nFrames) + " frames)");
|
||||
}
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
@@ -53,7 +53,7 @@ void WorkflowQ3MenuUpdateStep::Execute(const WorkflowStepDefinition& step, Workf
|
||||
const std::string defaultScreen = config_.value("default_screen", std::string("main"));
|
||||
|
||||
// --- toggle open/close ---
|
||||
bool open = context.GetBool("q3.menu_open", true);
|
||||
bool open = context.GetBool("q3.menu_open", false);
|
||||
const bool escPressed = context.GetBool("input_key_escape_pressed", false);
|
||||
if (escPressed) {
|
||||
if (open) {
|
||||
|
||||
@@ -329,6 +329,50 @@ void WorkflowPostfxCompositeStep::Execute(
|
||||
if (!cmd || !pipeline || !hdr_texture || !sampler || !swapchain_tex) {
|
||||
if (logger_) logger_->Warn("postfx.composite: Missing required resources");
|
||||
if (cmd) {
|
||||
// GPU screenshot: before submitting, blit swapchain to a download buffer
|
||||
// so the screenshot captures the full 3D scene (not just the CPU overlay).
|
||||
auto* gswap = context.Get<SDL_GPUTexture*>("gpu_swapchain_texture", nullptr);
|
||||
const auto* ssPath = context.TryGet<std::string>("screenshot_output_path");
|
||||
if (device && gswap && ssPath && !ssPath->empty()) {
|
||||
const uint32_t sw = context.Get<uint32_t>("frame_width", 1);
|
||||
const uint32_t sh = context.Get<uint32_t>("frame_height", 1);
|
||||
SDL_GPUTransferBufferCreateInfo tbci = {};
|
||||
tbci.usage = SDL_GPU_TRANSFERBUFFERUSAGE_DOWNLOAD;
|
||||
tbci.size = sw * sh * 4;
|
||||
auto* tbuf = SDL_CreateGPUTransferBuffer(device, &tbci);
|
||||
if (tbuf) {
|
||||
auto* copy = SDL_BeginGPUCopyPass(cmd);
|
||||
if (copy) {
|
||||
SDL_GPUTextureRegion src = {};
|
||||
src.texture = gswap;
|
||||
src.w = sw; src.h = sh; src.d = 1;
|
||||
SDL_GPUTextureTransferInfo dst = {};
|
||||
dst.transfer_buffer = tbuf;
|
||||
dst.pixels_per_row = sw;
|
||||
dst.rows_per_layer = sh;
|
||||
SDL_DownloadFromGPUTexture(copy, &src, &dst);
|
||||
SDL_EndGPUCopyPass(copy);
|
||||
}
|
||||
SDL_SubmitGPUCommandBuffer(cmd);
|
||||
SDL_WaitForGPUIdle(device);
|
||||
void* mapped = SDL_MapGPUTransferBuffer(device, tbuf, false);
|
||||
if (mapped) {
|
||||
SDL_Surface* surf = SDL_CreateSurfaceFrom(
|
||||
(int)sw, (int)sh, SDL_PIXELFORMAT_ABGR8888,
|
||||
mapped, (int)(sw * 4));
|
||||
if (surf) {
|
||||
SDL_SaveBMP(surf, ssPath->c_str());
|
||||
SDL_DestroySurface(surf);
|
||||
if (logger_) logger_->Info("postfx.composite: GPU screenshot saved to " + *ssPath);
|
||||
}
|
||||
SDL_UnmapGPUTransferBuffer(device, tbuf);
|
||||
}
|
||||
SDL_ReleaseGPUTransferBuffer(device, tbuf);
|
||||
context.Set<std::string>("screenshot_output_path", std::string(""));
|
||||
context.Remove("gpu_command_buffer");
|
||||
return;
|
||||
}
|
||||
}
|
||||
SDL_SubmitGPUCommandBuffer(cmd);
|
||||
context.Remove("gpu_command_buffer");
|
||||
}
|
||||
|
||||
@@ -118,6 +118,11 @@
|
||||
#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_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"
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_bots_spawn_step.hpp"
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_bots_update_step.hpp"
|
||||
#include "services/interfaces/workflow/quake3/workflow_q3_bots_draw_step.hpp"
|
||||
|
||||
// Audio (service-dependent, registered with nullptr)
|
||||
#include "services/interfaces/workflow/workflow_generic_steps/workflow_audio_pause_step.hpp"
|
||||
@@ -327,7 +332,12 @@ void WorkflowRegistrar::RegisterSteps(std::shared_ptr<IWorkflowStepRegistry> reg
|
||||
registry->RegisterStep(std::make_shared<WorkflowQ3WeaponUpdateStep>(logger_));
|
||||
registry->RegisterStep(std::make_shared<WorkflowQ3PickupsDrawStep>(logger_));
|
||||
registry->RegisterStep(std::make_shared<WorkflowQ3OverlayDrawStep>(logger_));
|
||||
count += 18;
|
||||
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;
|
||||
|
||||
// ── Texture ───────────────────────────────────────────────
|
||||
registry->RegisterStep(std::make_shared<WorkflowTextureLoadStep>(logger_));
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "services/interfaces/i_workflow_step.hpp"
|
||||
#include "services/interfaces/i_logger.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
/**
|
||||
* Plugin ID: q3.bots.draw
|
||||
*
|
||||
* Renders each bot as a three-part player model chain:
|
||||
* lower.md3 (legs) at bot world position
|
||||
* → upper.md3 (torso) attached via lower's tag_torso
|
||||
* → head.md3 attached via upper's tag_head
|
||||
* → weapon.md3 attached via upper's tag_weapon (optional)
|
||||
*
|
||||
* Parameters:
|
||||
* lower_prefix (string) – MD3 prefix for lower.md3 (default "lower")
|
||||
* upper_prefix (string) – MD3 prefix for upper.md3 (default "upper")
|
||||
* head_prefix (string) – MD3 prefix for head.md3 (default "head")
|
||||
* weapon_prefix (string) – MD3 prefix for weapon (default "weapon_mg")
|
||||
*
|
||||
* Reads: q3.bots, gpu_render_pass, gpu_command_buffer, gpu_pipeline_textured,
|
||||
* render.* matrices, q3.md3.{prefix}_* from q3.md3.load
|
||||
*/
|
||||
class WorkflowQ3BotsDrawStep final : public IWorkflowStep {
|
||||
public:
|
||||
explicit WorkflowQ3BotsDrawStep(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,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "services/interfaces/i_workflow_step.hpp"
|
||||
#include "services/interfaces/i_logger.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
/**
|
||||
* Plugin ID: q3.bots.spawn
|
||||
*
|
||||
* One-time step: reads BSP spawn points (info_player_deathmatch entities)
|
||||
* and creates bot entities spaced around the map.
|
||||
*
|
||||
* Parameters:
|
||||
* count (int) – number of bots to spawn (default 4)
|
||||
*
|
||||
* Context writes:
|
||||
* q3.bots nlohmann::json array of bot objects:
|
||||
* { id, pos:[x,y,z], yaw, health, state, leg_frame, torso_frame, weapon }
|
||||
*/
|
||||
class WorkflowQ3BotsSpawnStep final : public IWorkflowStep {
|
||||
public:
|
||||
explicit WorkflowQ3BotsSpawnStep(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,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "services/interfaces/i_workflow_step.hpp"
|
||||
#include "services/interfaces/i_logger.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
/**
|
||||
* Plugin ID: q3.bots.update
|
||||
*
|
||||
* Per-frame bot AI: chase player, face player, animate legs/torso.
|
||||
* Bots transition: idle ↔ chase ↔ shoot.
|
||||
*
|
||||
* Parameters:
|
||||
* chase_range (float) – distance to start chasing (default 20.0)
|
||||
* shoot_range (float) – distance to start shooting (default 6.0)
|
||||
* move_speed (float) – bot movement speed per second (default 3.0)
|
||||
* leg_idle (int) – first frame of legs idle anim
|
||||
* leg_run (int) – first frame of legs run anim
|
||||
* leg_run_cnt (int) – frame count for run anim
|
||||
* torso_stand (int) – first frame of torso stand
|
||||
* torso_attack (int) – first frame of torso attack
|
||||
* torso_atk_cnt(int) – frame count for attack anim
|
||||
*
|
||||
* Reads: camera.state (player position), q3.bots, frame.delta_time
|
||||
* Writes: q3.bots (updated), q3.bot_shots (list of shot events)
|
||||
*/
|
||||
class WorkflowQ3BotsUpdateStep final : public IWorkflowStep {
|
||||
public:
|
||||
explicit WorkflowQ3BotsUpdateStep(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,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "services/interfaces/i_workflow_step.hpp"
|
||||
#include "services/interfaces/i_logger.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
/**
|
||||
* Plugin ID: q3.md3.draw
|
||||
*
|
||||
* Renders all surfaces of a named MD3 model at a world transform.
|
||||
* Supports both free-standing (world-space position + yaw) and
|
||||
* viewmodel (camera-relative) modes.
|
||||
*
|
||||
* Parameters:
|
||||
* prefix (string) – context namespace from q3.md3.load ("lower", "upper", etc.)
|
||||
* pos_key (string) – context key holding position as JSON [x,y,z] (world mode)
|
||||
* yaw_key (string) – context key holding yaw float in radians (world mode)
|
||||
* frame_key (string) – override frame index from context (optional)
|
||||
* fps (float) – animation playback FPS (default 15)
|
||||
* anim_first (int) – first frame of animation clip
|
||||
* anim_count (int) – frame count (0 = loop all frames)
|
||||
* viewmodel (bool) – if true, render camera-relative (weapon viewmodel)
|
||||
* vm_right (float) – viewmodel right offset (default 0.35)
|
||||
* vm_down (float) – viewmodel down offset (default -0.3)
|
||||
* vm_fwd (float) – viewmodel forward offset (default 0.5)
|
||||
*
|
||||
* Reads from context:
|
||||
* gpu_render_pass, gpu_command_buffer, gpu_pipeline_textured
|
||||
* render.view_matrix, render.proj_matrix, render.camera_pos
|
||||
* render.shadow_vp, render.frag_uniforms, frame.elapsed
|
||||
*/
|
||||
class WorkflowQ3Md3DrawStep final : public IWorkflowStep {
|
||||
public:
|
||||
explicit WorkflowQ3Md3DrawStep(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,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "services/interfaces/i_workflow_step.hpp"
|
||||
#include "services/interfaces/i_logger.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
/**
|
||||
* Plugin ID: q3.md3.load
|
||||
*
|
||||
* Reads an MD3 model from the Q3 PK3 archive, decodes all animation frames,
|
||||
* and uploads per-frame vertex buffers + one index buffer per surface to the GPU.
|
||||
* Coordinates are converted from Q3 Z-up to the engine's Y-up system.
|
||||
*
|
||||
* Parameters:
|
||||
* prefix (string) – context key namespace for this model, e.g. "lower"
|
||||
* path (string) – PK3 entry path, e.g. "models/players/keel/lower.md3"
|
||||
* skin (string) – optional explicit skin texture PK3 path
|
||||
* anim (string) – optional animation.cfg PK3 path
|
||||
*
|
||||
* Context outputs (prefix = e.g. "lower"):
|
||||
* q3.md3.{prefix}_num_frames int
|
||||
* q3.md3.{prefix}_num_surfs int
|
||||
* q3.md3.{prefix}_num_tags int
|
||||
* q3.md3.{prefix}_tags nlohmann::json (array[frame] → map tag_name → {origin,axis})
|
||||
* q3.md3.{prefix}_anim nlohmann::json (parsed animation.cfg, optional)
|
||||
* q3.md3.{prefix}_surf{s}_ib SDL_GPUBuffer*
|
||||
* q3.md3.{prefix}_surf{s}_num_idx int
|
||||
* q3.md3.{prefix}_surf{s}_f{f}_vb SDL_GPUBuffer*
|
||||
* q3.md3.{prefix}_surf{s}_tex SDL_GPUTexture*
|
||||
* q3.md3.{prefix}_surf{s}_samp SDL_GPUSampler*
|
||||
*
|
||||
* Reads from context:
|
||||
* q3.pk3_path string – path to pak0.pk3 (set by bsp.load)
|
||||
* gpu_device SDL_GPUDevice*
|
||||
*/
|
||||
class WorkflowQ3Md3LoadStep final : public IWorkflowStep {
|
||||
public:
|
||||
explicit WorkflowQ3Md3LoadStep(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
|
||||
Reference in New Issue
Block a user