main.c (12933B)
1 #include "engine/battle.h" 2 #include "engine/area.h" 3 #include "engine/audio.h" 4 #include "engine/sdl.h" 5 #include "engine/save.h" 6 #include "game/content.h" 7 #include "game/presentation.h" 8 #include "game/replay.h" 9 10 #include <stdbool.h> 11 #include <stdio.h> 12 #include <stdlib.h> 13 #include <string.h> 14 #include <errno.h> 15 #include <stdint.h> 16 17 #if PH_USE_SDL 18 #include <SDL3/SDL.h> 19 #endif 20 21 #include "config.h" 22 23 #ifndef PH_CONFIG_VERSION 24 #error "config.h is outdated; merge config.def.h or replace config.h" 25 #elif PH_CONFIG_VERSION != PH_CONFIG_API_VERSION 26 #error "config.h version does not match the engine; merge config.def.h" 27 #else 28 29 #if PH_USE_SDL 30 static int 31 ph_game_music(const PhGameContext *context) 32 { 33 return *context->mode == PH_GAME_WORLD ? 34 ph_game_area_music(context->world->area_id) : PH_MUSIC_BATTLE; 35 } 36 37 static float 38 ph_world_sound_gain(const PhWorld *world, const PhWorldEvent *event) 39 { 40 const PhEntity *player = ph_world_player(world); 41 42 if (!player || !event || PH_SPATIAL_SOUND_DISTANCE <= 0.0f) return 0.0f; 43 return ph_audio_distance_gain(player->pos.x, player->pos.y, 44 event->pos.x, event->pos.y, PH_SPATIAL_SOUND_DISTANCE); 45 } 46 47 static void 48 ph_game_audio(PhGameContext *context, PhAudio *audio, int effect) 49 { 50 { 51 int battle_effect = ph_game_battle_sound(ph_battle_take_event(context->battle)); 52 53 if (battle_effect != PH_AUDIO_NONE) effect = battle_effect; 54 if (context->spellbook->spell_used) effect = PH_SOUND_SPELL; 55 context->spellbook->spell_used = 0; 56 } 57 if (audio) { 58 PhWorldEvent world_event; 59 60 ph_audio_music(audio, ph_game_music(context)); 61 ph_audio_set_enabled(audio, context->options->music_enabled, 62 context->options->effects_enabled); 63 if (effect != PH_AUDIO_NONE) ph_audio_effect(audio, effect, 1.0f); 64 while (ph_world_take_event(context->world, &world_event)) { 65 int world_effect = ph_game_world_sound(context->world, &world_event); 66 67 if (world_effect != PH_AUDIO_NONE) 68 ph_audio_effect(audio, world_effect, 69 ph_world_sound_gain(context->world, &world_event)); 70 } 71 ph_audio_update(audio); 72 } else { 73 PhWorldEvent world_event; 74 75 while (ph_world_take_event(context->world, &world_event)) { } 76 } 77 } 78 79 static PhAction 80 ph_sdl_action(SDL_Scancode key) 81 { 82 if (key == PH_KEY_MENU_UP_1 || key == PH_KEY_MENU_UP_2) return PH_ACTION_UP; 83 if (key == PH_KEY_MENU_DOWN_1 || key == PH_KEY_MENU_DOWN_2) return PH_ACTION_DOWN; 84 if (key == PH_KEY_MENU_LEFT_1 || key == PH_KEY_MENU_LEFT_2) return PH_ACTION_LEFT; 85 if (key == PH_KEY_MENU_RIGHT_1 || key == PH_KEY_MENU_RIGHT_2) return PH_ACTION_RIGHT; 86 if (key == PH_KEY_MENU_ACCEPT) return PH_ACTION_ACCEPT; 87 if (key == PH_KEY_MENU_CANCEL) return PH_ACTION_CANCEL; 88 if (key == PH_KEY_MENU_BACK) return PH_ACTION_BACK; 89 if (key == PH_KEY_CHARACTER) return PH_ACTION_CHARACTER; 90 if (key == PH_KEY_INVENTORY) return PH_ACTION_INVENTORY; 91 if (key == PH_KEY_SPELLBOOK) return PH_ACTION_SPELLBOOK; 92 return PH_ACTION_NONE; 93 } 94 95 static PhInput 96 ph_read_input(const bool *keys) 97 { 98 PhInput input = { 0 }; 99 100 if (keys[PH_KEY_LEFT_1] || keys[PH_KEY_LEFT_2] || keys[PH_KEY_LEFT_3]) 101 input.move_x -= 1; 102 if (keys[PH_KEY_RIGHT_1] || keys[PH_KEY_RIGHT_2] || keys[PH_KEY_RIGHT_3]) 103 input.move_x += 1; 104 if (keys[PH_KEY_UP_1] || keys[PH_KEY_UP_2] || keys[PH_KEY_UP_3]) 105 input.move_y -= 1; 106 if (keys[PH_KEY_DOWN_1] || keys[PH_KEY_DOWN_2] || keys[PH_KEY_DOWN_3]) 107 input.move_y += 1; 108 if (keys[PH_KEY_INTERACT_1] || keys[PH_KEY_INTERACT_2]) input.interact = 1; 109 return input; 110 } 111 112 static int 113 ph_create_window_renderer(SDL_Window **window, SDL_Renderer **renderer) 114 { 115 *window = SDL_CreateWindow(PH_WINDOW_TITLE, PH_VIEW_W * PH_SCALE, 116 PH_VIEW_H * PH_SCALE, SDL_WINDOW_RESIZABLE); 117 if (!*window) { 118 fprintf(stderr, "SDL_CreateWindow failed: %s\n", SDL_GetError()); 119 return -1; 120 } 121 122 *renderer = SDL_CreateRenderer(*window, NULL); 123 if (!*renderer) { 124 fprintf(stderr, "SDL_CreateRenderer default backend failed: %s\n", 125 SDL_GetError()); 126 *renderer = SDL_CreateRenderer(*window, "software"); 127 } 128 if (!*renderer) { 129 fprintf(stderr, "SDL_CreateRenderer software fallback failed: %s\n", 130 SDL_GetError()); 131 SDL_DestroyWindow(*window); 132 *window = NULL; 133 return -1; 134 } 135 return 0; 136 } 137 138 static int 139 ph_run_game(FILE *recording, unsigned int seed) 140 { 141 SDL_Window *window; 142 SDL_Renderer *target; 143 PhRenderer *renderer; 144 PhControlLabels labels = { 145 .accept = SDL_GetScancodeName(PH_KEY_MENU_ACCEPT), 146 .back = SDL_GetScancodeName(PH_KEY_MENU_BACK), 147 .cancel = SDL_GetScancodeName(PH_KEY_MENU_CANCEL), 148 .character = SDL_GetScancodeName(PH_KEY_CHARACTER), 149 .inventory = SDL_GetScancodeName(PH_KEY_INVENTORY), 150 .spellbook = SDL_GetScancodeName(PH_KEY_SPELLBOOK), 151 }; 152 PhWorld world; 153 PhInventoryUi inventory_ui = { 0 }; 154 PhSpellbookUi spellbook_ui = { 0 }; 155 PhShopUi shop_ui = { 0 }; 156 PhOptionsUi options_ui = { .music_enabled = 1, .effects_enabled = 1 }; 157 PhStartUi start_ui = { 0 }; 158 PhGameMode game = PH_GAME_WORLD; 159 PhBattle battle = { 0 }; 160 PhSpellBook spells = ph_player_spellbook; 161 PhAudio *audio = NULL; 162 PhDebugView debug = { 0 }; 163 PhGameContext context = { 164 .world = &world, 165 .inventory = &inventory_ui, 166 .spellbook = &spellbook_ui, 167 .shop = &shop_ui, 168 .options = &options_ui, 169 .start = &start_ui, 170 .mode = &game, 171 .battle = &battle, 172 .spells = &spells, 173 .menu = PH_MENU_START, 174 }; 175 Uint64 last_ticks; 176 int running = 1; 177 int interact_pending = 0; 178 int save_status; 179 int result = 0; 180 const char *save_path = recording ? NULL : PH_SAVE_PATH; 181 char content_error[256]; 182 183 if (ph_game_validate_content(content_error, sizeof(content_error)) < 0) { 184 fprintf(stderr, "content: %s\n", content_error); 185 return 1; 186 } 187 188 if (!SDL_Init(SDL_INIT_VIDEO)) { 189 fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError()); 190 return 1; 191 } 192 save_status = recording ? 0 : ph_save_load(save_path, &world, &ph_game_world, &spells); 193 if (save_status < 0) { 194 fprintf(stderr, "save is invalid; starting a new game\n"); 195 spells = ph_player_spellbook; 196 } 197 if (save_status <= 0 && ph_world_start(&world, &ph_game_world) < 0) { 198 SDL_Quit(); 199 return 1; 200 } 201 if (recording) { 202 world.rng_state = seed; 203 context.menu = PH_MENU_NONE; 204 } 205 start_ui.can_continue = save_status > 0; 206 start_ui.selected = start_ui.can_continue ? 0 : 1; 207 if (ph_create_window_renderer(&window, &target) < 0) { 208 ph_world_destroy(&world); 209 SDL_Quit(); 210 return 1; 211 } 212 if (!(renderer = ph_render_sdl_create(target, ph_asset_defs, PH_ASSET_COUNT))) { 213 fprintf(stderr, "render initialization failed: %s\n", SDL_GetError()); 214 SDL_DestroyRenderer(target); 215 SDL_DestroyWindow(window); 216 ph_world_destroy(&world); 217 SDL_Quit(); 218 return 1; 219 } 220 if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) { 221 fprintf(stderr, "audio disabled: %s\n", SDL_GetError()); 222 } else if (!(audio = ph_audio_sdl_create(ph_audio_defs, PH_AUDIO_COUNT))) { 223 fprintf(stderr, "audio disabled: %s\n", SDL_GetError()); 224 } 225 226 SDL_SetRenderLogicalPresentation(target, PH_VIEW_W, PH_VIEW_H, 227 SDL_LOGICAL_PRESENTATION_INTEGER_SCALE); 228 last_ticks = SDL_GetTicksNS(); 229 while (running) { 230 SDL_Event event; 231 Uint64 now_ticks, update_start, draw_start, draw_end; 232 Uint64 frame_start = SDL_GetTicksNS(); 233 int executed = 0; 234 int steps, step; 235 PhInput input; 236 int effect = PH_AUDIO_NONE; 237 238 while (SDL_PollEvent(&event)) { 239 if (event.type == SDL_EVENT_QUIT) { 240 running = 0; 241 } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) { 242 int request; 243 PhAction action; 244 245 if (event.key.scancode == PH_KEY_DEBUG) { 246 debug.enabled = !debug.enabled; 247 continue; 248 } 249 action = ph_sdl_action(event.key.scancode); 250 if (recording && action != PH_ACTION_NONE && 251 ph_replay_write(recording, PH_REPLAY_ACTION, action, (PhInput){ 0 }) < 0) { 252 result = 1; 253 running = 0; 254 break; 255 } 256 request = ph_game_input(&context, action); 257 258 if (context.menu == PH_MENU_NONE && game == PH_GAME_WORLD && 259 (event.key.scancode == PH_KEY_INTERACT_1 || 260 event.key.scancode == PH_KEY_INTERACT_2)) interact_pending = 1; 261 262 if (request & PH_GAME_MENU_SOUND) effect = PH_SOUND_MENU; 263 if (request & PH_GAME_REQUEST_EXIT) { running = 0; break; } 264 if (request & PH_GAME_REQUEST_NEW) { 265 ph_world_destroy(&world); 266 spells = ph_player_spellbook; 267 if (ph_world_start(&world, &ph_game_world) < 0) running = 0; 268 else { 269 remove(PH_SAVE_PATH); 270 context.menu = PH_MENU_NONE; 271 } 272 } 273 } 274 } 275 276 if (!running) break; 277 now_ticks = SDL_GetTicksNS(); 278 if (context.time_reset || context.menu != PH_MENU_NONE || game != PH_GAME_WORLD) 279 interact_pending = 0; 280 steps = ph_game_steps(&context, now_ticks - last_ticks); 281 last_ticks = now_ticks; 282 input = ph_read_input(SDL_GetKeyboardState(NULL)); 283 update_start = SDL_GetTicksNS(); 284 ph_game_audio(&context, audio, effect); 285 for (step = 0; step < steps; ++step) { 286 PhInput tick_input = input; 287 288 ++executed; 289 tick_input.interact |= interact_pending; 290 interact_pending = 0; 291 if (recording && ph_replay_write(recording, PH_REPLAY_TICK, 292 PH_ACTION_NONE, tick_input) < 0) { 293 result = 1; 294 running = 0; 295 break; 296 } 297 if (ph_game_update(&context, tick_input, 1.0f / PH_SIMULATION_HZ, save_path) && 298 save_path && ph_save_write(save_path, &world, &spells) < 0) 299 fprintf(stderr, "failed to autosave\n"); 300 ph_game_interaction(&context); 301 /* Drain each tick before the next can replace battle events or travel. */ 302 ph_game_audio(&context, audio, PH_AUDIO_NONE); 303 if (context.menu != PH_MENU_NONE || game == PH_GAME_VICTORY || game == PH_GAME_OVER) { 304 context.clock.credit = 0; 305 break; 306 } 307 } 308 309 draw_start = SDL_GetTicksNS(); 310 ph_present_frame(renderer, &context, &labels, &debug); 311 draw_end = SDL_GetTicksNS(); 312 SDL_Delay(PH_FRAME_DELAY_MS); 313 debug.frame_ms = (SDL_GetTicksNS() - frame_start) / 1000000.0; 314 debug.update_ms = (draw_start - update_start) / 1000000.0; 315 debug.draw_ms = (draw_end - draw_start) / 1000000.0; 316 debug.steps = executed; 317 } 318 319 if (audio) ph_audio_destroy(audio); 320 ph_render_destroy(renderer); 321 SDL_DestroyRenderer(target); 322 SDL_DestroyWindow(window); 323 if (save_path && (game == PH_GAME_WORLD || game == PH_GAME_VICTORY) && 324 ph_save_write(PH_SAVE_PATH, &world, &spells) < 0) 325 fprintf(stderr, "failed to save game\n"); 326 ph_world_destroy(&world); 327 SDL_Quit(); 328 return result; 329 } 330 #endif 331 332 static int 333 ph_run_replay(FILE *file, unsigned int seed) 334 { 335 PhWorld world = { 0 }; 336 PhBattle battle = { 0 }; 337 PhSpellBook spells = ph_player_spellbook; 338 PhInventoryUi inventory = { 0 }; 339 PhSpellbookUi spellbook = { 0 }; 340 PhShopUi shop = { 0 }; 341 PhOptionsUi options = { .music_enabled = 1, .effects_enabled = 1 }; 342 PhStartUi start = { 0 }; 343 PhGameMode mode = PH_GAME_WORLD; 344 PhGameContext context = { .world = &world, .battle = &battle, .spells = &spells, 345 .inventory = &inventory, .spellbook = &spellbook, .shop = &shop, 346 .options = &options, .start = &start, .mode = &mode }; 347 uint64_t ticks; 348 int result; 349 char error[256]; 350 351 if (ph_game_validate_content(error, sizeof(error)) < 0) { 352 fprintf(stderr, "content: %s\n", error); 353 return 1; 354 } 355 if (ph_world_start(&world, &ph_game_world) < 0) return 1; 356 world.rng_state = seed; 357 result = ph_replay_run(file, &context, &ticks); 358 if (result < 0) fprintf(stderr, "invalid replay at tick %llu\n", (unsigned long long)ticks); 359 else printf("replay: %llu ticks, area %d, mode %d, RNG %u\n", 360 (unsigned long long)ticks, world.area_id, mode, world.rng_state); 361 ph_world_destroy(&world); 362 return result < 0; 363 } 364 365 int 366 main(int argc, char **argv) 367 { 368 FILE *file; 369 unsigned int seed = 0x5048414eu; 370 int recording, result; 371 372 if (argc == 1) { 373 #if PH_USE_SDL 374 return ph_run_game(NULL, seed); 375 #else 376 fprintf(stderr, "SDL3 not available in this build; run `make smoke` or --replay\n"); 377 return 1; 378 #endif 379 } 380 if ((argc != 3 && argc != 5) || 381 (strcmp(argv[1], "--record") && strcmp(argv[1], "--replay"))) goto usage; 382 recording = !strcmp(argv[1], "--record"); 383 if (argc == 5) { 384 char *end; 385 unsigned long value; 386 387 if (!recording || strcmp(argv[3], "--seed") || argv[4][0] == '-') goto usage; 388 errno = 0; 389 value = strtoul(argv[4], &end, 0); 390 if (errno || end == argv[4] || *end || value > UINT32_MAX) goto usage; 391 seed = (unsigned int)value; 392 } 393 #if !PH_USE_SDL 394 if (recording) { 395 fprintf(stderr, "recording requires SDL3\n"); 396 return 1; 397 } 398 #endif 399 file = fopen(argv[2], recording ? "wb" : "rb"); 400 if (!file) { perror(argv[2]); return 1; } 401 result = ph_replay_header(file, &seed, recording); 402 if (result < 0) { 403 fprintf(stderr, "invalid or unreadable replay header\n"); 404 result = 1; 405 } else if (!recording) result = ph_run_replay(file, seed); 406 #if PH_USE_SDL 407 else result = ph_run_game(file, seed); 408 #endif 409 if (recording && !result && 410 ph_replay_write(file, PH_REPLAY_END, PH_ACTION_NONE, (PhInput){ 0 }) < 0) result = 1; 411 if (fclose(file)) result = 1; 412 if (recording && result) fprintf(stderr, "recording failed; file is incomplete\n"); 413 return result; 414 usage: 415 fprintf(stderr, "usage: %s [--record FILE [--seed N] | --replay FILE]\n", argv[0]); 416 return 1; 417 } 418 419 #endif