phantasia

Phantasia - 2D SDL3 RPG prototype.
git clone git://git.beep.wimdupont.com/phantasia.git
Log | Files | Refs | README | LICENSE

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