Repository: LostArtefacts/TR1X Branch: develop Commit: 23607f8db368 Files: 1612 Total size: 8.5 MB Directory structure: gitextract_glsblazi/ ├── .clang-format ├── .dockerignore ├── .editorconfig ├── .github/ │ ├── CODEOWNERS │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.yml │ ├── actions/ │ │ └── prepare_macos_tooling/ │ │ └── action.yml │ ├── docker/ │ │ └── lint.Dockerfile │ ├── pull_request_template.md │ └── workflows/ │ ├── build_docker.yml │ ├── build_lint_image.yml │ ├── comment_build.yml │ ├── job_build.yml │ ├── job_build_macos.yml │ ├── job_release.yml │ ├── lint.yml │ ├── pr_builds.yml │ ├── prerelease.yml │ └── release.yml ├── .gitignore ├── .gitmodules ├── .pre-commit-config.yaml ├── BUG_REPORTING.md ├── CODE_OF_CONDUCT.md ├── COPYING.md ├── README.md ├── data/ │ ├── common/ │ │ └── glyphs/ │ │ ├── mapping.txt │ │ ├── mapping_basic_latin.txt │ │ ├── mapping_combining_diactrics.txt │ │ ├── mapping_controller.txt │ │ ├── mapping_cyrillic.txt │ │ ├── mapping_greek_and_coptic.txt │ │ ├── mapping_icons.txt │ │ ├── mapping_keyboard.txt │ │ ├── mapping_latin-1_supplement.txt │ │ ├── mapping_latin_extended-a.txt │ │ ├── mapping_latin_extended-b.txt │ │ ├── mapping_latin_extended_additional.txt │ │ ├── mapping_misc.txt │ │ └── mapping_small.txt │ ├── scripting/ │ │ ├── assault_stats.lua │ │ ├── camera.lua │ │ ├── catalog.lua │ │ ├── config.lua │ │ ├── console.lua │ │ ├── creatures.lua │ │ ├── events.lua │ │ ├── game.lua │ │ ├── items.lua │ │ ├── lara.lua │ │ ├── log.lua │ │ ├── music.lua │ │ ├── objects.lua │ │ ├── rooms.lua │ │ └── sound.lua │ ├── tomb-11.bdf │ ├── tr1/ │ │ └── mac/ │ │ ├── Info.plist │ │ └── icon.icns │ ├── tr2/ │ │ └── mac/ │ │ ├── Info.plist │ │ └── icon.icns │ ├── tr3/ │ │ ├── glyphs/ │ │ │ ├── mapping_basic_latin.txt │ │ │ ├── mapping_combining_diactrics.txt │ │ │ ├── mapping_cyrillic.txt │ │ │ ├── mapping_greek_and_coptic.txt │ │ │ ├── mapping_latin-1_supplement.txt │ │ │ ├── mapping_latin_extended-a.txt │ │ │ ├── mapping_latin_extended-b.txt │ │ │ ├── mapping_latin_extended_additional.txt │ │ │ ├── mapping_misc.txt │ │ │ └── mapping_small.txt │ │ └── mac/ │ │ ├── Info.plist │ │ └── icon.icns │ └── trx/ │ ├── icon.rc │ ├── ship/ │ │ ├── cfg/ │ │ │ ├── base_strings-de.json5 │ │ │ ├── base_strings-en-gb.json5 │ │ │ ├── base_strings-fr.json5 │ │ │ ├── base_strings-gd.json5 │ │ │ ├── base_strings-it.json5 │ │ │ ├── base_strings-pl.json5 │ │ │ ├── base_strings-ru.json5 │ │ │ ├── base_strings.json5 │ │ │ ├── outfits.json5 │ │ │ ├── poses.json5 │ │ │ ├── presets/ │ │ │ │ ├── tr1-pc.json5 │ │ │ │ ├── tr1-ps1.json5 │ │ │ │ ├── tr2-pc.json5 │ │ │ │ ├── tr2-ps1.json5 │ │ │ │ ├── tr3-pc.json5 │ │ │ │ └── tr3-ps1.json5 │ │ │ ├── shaders/ │ │ │ │ ├── 2d.glsl │ │ │ │ ├── billboard.glsl │ │ │ │ ├── common.glsl │ │ │ │ ├── fbo.glsl │ │ │ │ ├── lights.glsl │ │ │ │ ├── meshes.glsl │ │ │ │ ├── meshes_tr12.glsl │ │ │ │ ├── meshes_tr3.glsl │ │ │ │ └── ui.glsl │ │ │ └── ui.json5 │ │ └── games/ │ │ ├── tr1/ │ │ │ ├── catalog_item_actions.csv │ │ │ ├── catalog_lara_anims.csv │ │ │ ├── catalog_lara_states.csv │ │ │ ├── catalog_music.csv │ │ │ ├── catalog_objects.csv │ │ │ ├── catalog_samples.csv │ │ │ ├── gameflow.json5 │ │ │ ├── inv_ring.json5 │ │ │ ├── scripts/ │ │ │ │ └── gym.lua │ │ │ ├── strings-de.json5 │ │ │ ├── strings-en-gb.json5 │ │ │ ├── strings-fr.json5 │ │ │ ├── strings-gd.json5 │ │ │ ├── strings-it.json5 │ │ │ ├── strings-pl.json5 │ │ │ ├── strings-ru.json5 │ │ │ ├── strings.json5 │ │ │ └── weapons.json5 │ │ ├── tr1-demo-pc/ │ │ │ ├── gameflow.json5 │ │ │ ├── strings-de.json5 │ │ │ ├── strings-fr.json5 │ │ │ ├── strings-gd.json5 │ │ │ ├── strings-it.json5 │ │ │ ├── strings-pl.json5 │ │ │ ├── strings-ru.json5 │ │ │ └── strings.json5 │ │ ├── tr1-level/ │ │ │ ├── gameflow.json5 │ │ │ ├── strings-de.json5 │ │ │ ├── strings-fr.json5 │ │ │ ├── strings-gd.json5 │ │ │ ├── strings-it.json5 │ │ │ ├── strings-pl.json5 │ │ │ ├── strings-ru.json5 │ │ │ └── strings.json5 │ │ ├── tr1-ub/ │ │ │ ├── gameflow.json5 │ │ │ ├── strings-de.json5 │ │ │ ├── strings-fr.json5 │ │ │ ├── strings-gd.json5 │ │ │ ├── strings-it.json5 │ │ │ ├── strings-pl.json5 │ │ │ ├── strings-ru.json5 │ │ │ └── strings.json5 │ │ ├── tr2/ │ │ │ ├── catalog_item_actions.csv │ │ │ ├── catalog_lara_anims.csv │ │ │ ├── catalog_lara_states.csv │ │ │ ├── catalog_music.csv │ │ │ ├── catalog_objects.csv │ │ │ ├── catalog_samples.csv │ │ │ ├── gameflow.json5 │ │ │ ├── inv_ring.json5 │ │ │ ├── scripts/ │ │ │ │ ├── assault.lua │ │ │ │ ├── cut3.lua │ │ │ │ ├── floating.lua │ │ │ │ ├── house.lua │ │ │ │ ├── level1.lua │ │ │ │ ├── level3.lua │ │ │ │ ├── level4.lua │ │ │ │ └── monastry.lua │ │ │ ├── strings-de.json5 │ │ │ ├── strings-en-gb.json5 │ │ │ ├── strings-fr.json5 │ │ │ ├── strings-gd.json5 │ │ │ ├── strings-it.json5 │ │ │ ├── strings-pl.json5 │ │ │ ├── strings.json5 │ │ │ └── weapons.json5 │ │ ├── tr2-gm/ │ │ │ ├── gameflow.json5 │ │ │ ├── strings-de.json5 │ │ │ ├── strings-fr.json5 │ │ │ ├── strings-gd.json5 │ │ │ ├── strings-it.json5 │ │ │ ├── strings-pl.json5 │ │ │ └── strings.json5 │ │ ├── tr2-level/ │ │ │ ├── gameflow.json5 │ │ │ ├── strings-de.json5 │ │ │ ├── strings-fr.json5 │ │ │ ├── strings-gd.json5 │ │ │ ├── strings-it.json5 │ │ │ ├── strings-pl.json5 │ │ │ └── strings.json5 │ │ ├── tr3/ │ │ │ ├── catalog_item_actions.csv │ │ │ ├── catalog_lara_anims.csv │ │ │ ├── catalog_lara_states.csv │ │ │ ├── catalog_music.csv │ │ │ ├── catalog_objects.csv │ │ │ ├── catalog_samples.csv │ │ │ ├── gameflow.json5 │ │ │ ├── inv_ring.json5 │ │ │ ├── scripts/ │ │ │ │ ├── area51.lua │ │ │ │ ├── compound.lua │ │ │ │ ├── crash.lua │ │ │ │ ├── cut8.lua │ │ │ │ ├── jungle.lua │ │ │ │ ├── mines.lua │ │ │ │ ├── tower.lua │ │ │ │ └── zoo.lua │ │ │ ├── strings-de.json5 │ │ │ ├── strings-it.json5 │ │ │ ├── strings-pl.json5 │ │ │ ├── strings.json5 │ │ │ └── weapons.json5 │ │ ├── tr3-la/ │ │ │ ├── gameflow.json5 │ │ │ ├── strings-de.json5 │ │ │ ├── strings-it.json5 │ │ │ ├── strings-pl.json5 │ │ │ └── strings.json5 │ │ └── tr3-level/ │ │ ├── gameflow.json5 │ │ ├── strings-it.json5 │ │ ├── strings-pl.json5 │ │ └── strings.json5 │ └── version.rc ├── docs/ │ ├── BUILDING.md │ ├── BUILDING_ON_LINUX.md │ ├── BUILDING_ON_MACOS.md │ ├── BUILDING_ON_WINDOWS.md │ ├── CHANGELOG.md │ ├── CHANGE_SUBMISSION.md │ ├── CODING_GUIDELINES.md │ ├── CONTRIBUTING.md │ ├── GLOSSARY.md │ ├── RELEASING.md │ ├── SECRETS.md │ ├── gameflow.schema.json │ ├── tr1/ │ │ ├── CHANGELOG.md │ │ └── INSTALLING.md │ ├── tr2/ │ │ ├── CHANGELOG.md │ │ ├── INSTALLING.md │ │ └── symbols.txt │ ├── tr3/ │ │ └── INSTALLING.md │ └── trx/ │ ├── CATALOGS.md │ ├── COMMANDS.md │ ├── COMMAND_LINE.md │ ├── ENEMY_DEFAULTS.md │ ├── GAME_STRINGS.md │ ├── INJECTIONS.md │ ├── INSTALLING.md │ ├── LEVELS.md │ ├── MIGRATING.md │ ├── MUSIC.md │ ├── OUTFITS.md │ ├── SAMPLES.md │ ├── SUPPORT.md │ ├── WATER_COLORS.md │ ├── WEAPONS.md │ ├── game_flow/ │ │ ├── COMMANDS.md │ │ ├── GLOBAL_PROPERTIES.md │ │ ├── README.md │ │ ├── SEQUENCES.md │ │ ├── USER_CONFIGURATION.md │ │ └── levels/ │ │ ├── BONUS_LEVELS.md │ │ ├── CUTSCENE_PROPERTIES.md │ │ ├── DEMO_PROPERTIES.md │ │ ├── FMV_PROPERTIES.md │ │ ├── ITEM_DROPS.md │ │ ├── README.md │ │ └── REGULAR_LEVELS.md │ ├── lua/ │ │ ├── GETTING_STARTED.md │ │ ├── README.md │ │ ├── examples/ │ │ │ └── README.md │ │ └── reference/ │ │ ├── ASSAULT_STATS.md │ │ ├── CAMERA.md │ │ ├── CATALOG.md │ │ ├── CONFIG.md │ │ ├── CONSOLE.md │ │ ├── CREATURE.md │ │ ├── EVENTS.md │ │ ├── GAME.md │ │ ├── ITEMS.md │ │ ├── LARA.md │ │ ├── LOGGING.md │ │ ├── MISC.md │ │ ├── MUSIC.md │ │ ├── OBJECTS.md │ │ ├── README.md │ │ ├── ROOMS.md │ │ └── SOUND.md │ └── water_colors.yml ├── justfile ├── justfile.tr1 ├── justfile.tr2 ├── justfile.tr3 ├── src/ │ ├── meson.build │ ├── meson.options │ ├── subprojects/ │ │ ├── dwarfstack.wrap │ │ └── packagefiles/ │ │ └── dwarfstack/ │ │ └── meson.build │ └── trx/ │ ├── av/ │ │ ├── audio.c │ │ ├── audio.h │ │ ├── audio_internal.h │ │ ├── audio_reverb.c │ │ ├── audio_sample.c │ │ ├── audio_stream.c │ │ ├── image.c │ │ ├── image.h │ │ ├── video.c │ │ └── video.h │ ├── config/ │ │ ├── common.c │ │ ├── common.h │ │ ├── const.h │ │ ├── dynamic_enum.c │ │ ├── dynamic_enum.h │ │ ├── enum.c │ │ ├── enum.h │ │ ├── file.c │ │ ├── file.h │ │ ├── map.c │ │ ├── map.def │ │ ├── map_tr1.def │ │ ├── map_tr2.def │ │ ├── map_tr3.def │ │ ├── option.h │ │ ├── presets.c │ │ ├── presets.h │ │ ├── priv.c │ │ ├── priv.h │ │ ├── types.h │ │ ├── vars.c │ │ └── vars.h │ ├── config.h │ ├── core/ │ │ ├── benchmark.c │ │ ├── benchmark.h │ │ ├── bson/ │ │ │ ├── enum.h │ │ │ ├── parse.c │ │ │ ├── parse.h │ │ │ ├── types.h │ │ │ ├── write.c │ │ │ └── write.h │ │ ├── bson.h │ │ ├── colors.c │ │ ├── colors.h │ │ ├── enum_map.c │ │ ├── enum_map.h │ │ ├── event_manager.c │ │ ├── event_manager.h │ │ ├── filesystem.c │ │ ├── filesystem.h │ │ ├── hash.c │ │ ├── hash.h │ │ ├── json/ │ │ │ ├── base.c │ │ │ ├── base.h │ │ │ ├── enum.h │ │ │ ├── parse.c │ │ │ ├── parse.h │ │ │ ├── types.h │ │ │ ├── util/ │ │ │ │ ├── file.c │ │ │ │ ├── file.h │ │ │ │ ├── read_io.c │ │ │ │ ├── read_io.h │ │ │ │ ├── write_io.c │ │ │ │ └── write_io.h │ │ │ ├── write.c │ │ │ └── write.h │ │ ├── json.h │ │ ├── log.c │ │ ├── log.h │ │ ├── log_linux.c │ │ ├── log_unknown.c │ │ ├── log_windows.c │ │ ├── math/ │ │ │ ├── const.h │ │ │ ├── func.c │ │ │ ├── func.h │ │ │ ├── geom.c │ │ │ ├── geom.h │ │ │ ├── trig.c │ │ │ ├── trig.h │ │ │ ├── types.h │ │ │ ├── util.c │ │ │ └── util.h │ │ ├── math.h │ │ ├── memory.c │ │ ├── memory.h │ │ ├── shell.h │ │ ├── strings/ │ │ │ ├── case_funcs.c │ │ │ ├── case_map.def │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── fuzzy_match.c │ │ │ └── fuzzy_match.h │ │ ├── strings.h │ │ ├── thread_pool.c │ │ ├── thread_pool.h │ │ ├── utils.h │ │ ├── vector.c │ │ ├── vector.h │ │ ├── virtual_file.c │ │ └── virtual_file.h │ ├── debug.h │ ├── game/ │ │ ├── anims/ │ │ │ ├── commands.c │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── enum.h │ │ │ ├── frames.c │ │ │ └── types.h │ │ ├── anims.h │ │ ├── camera/ │ │ │ ├── box_camera.c │ │ │ ├── cinematic.c │ │ │ ├── cinematic.h │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── const.h │ │ │ ├── enum.h │ │ │ ├── environment.c │ │ │ ├── environment.h │ │ │ ├── fixed.c │ │ │ ├── fixed.h │ │ │ ├── los_camera.c │ │ │ ├── photo_mode.c │ │ │ ├── photo_mode.h │ │ │ ├── types.h │ │ │ ├── vars.c │ │ │ └── vars.h │ │ ├── camera.h │ │ ├── catalog/ │ │ │ ├── item_actions.def │ │ │ ├── lara_anims.def │ │ │ ├── lara_states.def │ │ │ ├── manager.c │ │ │ ├── manager.h │ │ │ ├── music.def │ │ │ ├── objects.def │ │ │ └── samples.def │ │ ├── clock/ │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── const.h │ │ │ ├── timer.c │ │ │ ├── timer.h │ │ │ ├── turbo.c │ │ │ └── turbo.h │ │ ├── clock.h │ │ ├── collision/ │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── los.c │ │ │ ├── los.h │ │ │ └── types.h │ │ ├── collision.h │ │ ├── console/ │ │ │ ├── cmd/ │ │ │ │ ├── clear.c │ │ │ │ ├── config.c │ │ │ │ ├── config.h │ │ │ │ ├── debug.c │ │ │ │ ├── die.c │ │ │ │ ├── easy_config.c │ │ │ │ ├── end_level.c │ │ │ │ ├── exit_game.c │ │ │ │ ├── exit_to_title.c │ │ │ │ ├── flipmap.c │ │ │ │ ├── flood.c │ │ │ │ ├── fly.c │ │ │ │ ├── give_item.c │ │ │ │ ├── give_secret.c │ │ │ │ ├── heal.c │ │ │ │ ├── help.c │ │ │ │ ├── immune.c │ │ │ │ ├── inf_sprint.c │ │ │ │ ├── kill.c │ │ │ │ ├── load_game.c │ │ │ │ ├── lua.c │ │ │ │ ├── mod.c │ │ │ │ ├── music.c │ │ │ │ ├── play_cutscene.c │ │ │ │ ├── play_demo.c │ │ │ │ ├── play_gym.c │ │ │ │ ├── play_level.c │ │ │ │ ├── pos.c │ │ │ │ ├── save_game.c │ │ │ │ ├── screenshot.c │ │ │ │ ├── set_health.c │ │ │ │ ├── sfx.c │ │ │ │ ├── spawn.c │ │ │ │ ├── speed.c │ │ │ │ ├── strings.c │ │ │ │ ├── teleport.c │ │ │ │ ├── test_text.c │ │ │ │ ├── trigger.c │ │ │ │ ├── weather.c │ │ │ │ └── winston.c │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── enum.h │ │ │ ├── history.c │ │ │ ├── history.h │ │ │ ├── internal.h │ │ │ ├── registry.c │ │ │ ├── registry.h │ │ │ └── types.h │ │ ├── console.h │ │ ├── const.h │ │ ├── creature/ │ │ │ ├── alert.c │ │ │ ├── alert.h │ │ │ ├── behavior.c │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── const.h │ │ │ ├── enum.h │ │ │ ├── shooting.c │ │ │ └── types.h │ │ ├── creature.h │ │ ├── cutscene.c │ │ ├── cutscene.h │ │ ├── demo.c │ │ ├── demo.h │ │ ├── effects/ │ │ │ ├── const.h │ │ │ ├── draw.c │ │ │ ├── draw.h │ │ │ ├── manager.c │ │ │ ├── manager.h │ │ │ └── types.h │ │ ├── effects.h │ │ ├── enum.c │ │ ├── events.c │ │ ├── events.h │ │ ├── fader.c │ │ ├── fader.h │ │ ├── fmv.c │ │ ├── fmv.h │ │ ├── fx/ │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── explosion_ring.c │ │ │ ├── explosion_ring.h │ │ │ ├── footprint.c │ │ │ ├── footprint.h │ │ │ ├── gun_flash.c │ │ │ ├── gun_flash.h │ │ │ ├── laser.c │ │ │ ├── laser.h │ │ │ ├── wake.c │ │ │ ├── wake.h │ │ │ ├── water.c │ │ │ ├── water.h │ │ │ ├── water_particles.c │ │ │ ├── water_particles.h │ │ │ ├── weather.c │ │ │ └── weather.h │ │ ├── fx.h │ │ ├── game/ │ │ │ ├── control.c │ │ │ ├── control.h │ │ │ ├── draw.c │ │ │ ├── draw.h │ │ │ ├── enum.h │ │ │ ├── state.c │ │ │ └── state.h │ │ ├── game.h │ │ ├── game_buf.c │ │ ├── game_buf.h │ │ ├── game_flow/ │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── enum.h │ │ │ ├── inventory.c │ │ │ ├── inventory.h │ │ │ ├── reader.c │ │ │ ├── reader.h │ │ │ ├── sequencer.c │ │ │ ├── sequencer.h │ │ │ ├── sequencer_events.c │ │ │ ├── sequencer_events.h │ │ │ ├── sequencer_misc.c │ │ │ ├── types.h │ │ │ ├── util.c │ │ │ ├── util.h │ │ │ ├── vars.c │ │ │ └── vars.h │ │ ├── game_flow.h │ │ ├── game_strings/ │ │ │ ├── entries.c │ │ │ ├── entries.def │ │ │ ├── entries.h │ │ │ ├── manager.c │ │ │ ├── manager.h │ │ │ ├── table/ │ │ │ │ ├── common.c │ │ │ │ ├── priv.c │ │ │ │ ├── priv.h │ │ │ │ └── reader.c │ │ │ └── table.h │ │ ├── gun/ │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── control.c │ │ │ ├── control.h │ │ │ ├── misc.c │ │ │ ├── misc.h │ │ │ ├── pistols.c │ │ │ ├── pistols.h │ │ │ ├── rifle.c │ │ │ ├── rifle.h │ │ │ ├── smashing.c │ │ │ ├── smashing.h │ │ │ ├── smoke.c │ │ │ ├── smoke.h │ │ │ ├── types.h │ │ │ ├── vars.c │ │ │ └── vars.h │ │ ├── gun.h │ │ ├── gym.c │ │ ├── gym.h │ │ ├── inject/ │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── data/ │ │ │ │ ├── anims.c │ │ │ │ ├── camera.c │ │ │ │ ├── meshes.c │ │ │ │ ├── objects.c │ │ │ │ ├── sound.c │ │ │ │ └── textures.c │ │ │ ├── editor.c │ │ │ ├── editor.h │ │ │ ├── editors/ │ │ │ │ ├── anims.c │ │ │ │ ├── floor_data.c │ │ │ │ ├── items.c │ │ │ │ ├── meshes.c │ │ │ │ ├── objects.c │ │ │ │ ├── rooms.c │ │ │ │ └── textures.c │ │ │ ├── enum.h │ │ │ ├── testers/ │ │ │ │ ├── items.c │ │ │ │ └── rooms.c │ │ │ ├── types.h │ │ │ ├── utils.c │ │ │ └── utils.h │ │ ├── inject.h │ │ ├── input/ │ │ │ ├── backends/ │ │ │ │ ├── base.h │ │ │ │ ├── controller.c │ │ │ │ ├── controller.def │ │ │ │ ├── controller.h │ │ │ │ ├── internal.c │ │ │ │ ├── internal.h │ │ │ │ ├── keyboard.c │ │ │ │ ├── keyboard.def │ │ │ │ └── keyboard.h │ │ │ ├── combo.c │ │ │ ├── combo.h │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── roles.def │ │ │ └── update.c │ │ ├── input.h │ │ ├── interpolation.c │ │ ├── interpolation.h │ │ ├── inventory.c │ │ ├── inventory.h │ │ ├── inventory_ring/ │ │ │ ├── control.c │ │ │ ├── control.h │ │ │ ├── draw.c │ │ │ ├── draw.h │ │ │ ├── enum.h │ │ │ ├── priv.c │ │ │ ├── priv.h │ │ │ ├── types.h │ │ │ ├── vars.c │ │ │ └── vars.h │ │ ├── inventory_ring.h │ │ ├── items/ │ │ │ ├── actions/ │ │ │ │ ├── common.c │ │ │ │ ├── effects.c │ │ │ │ ├── footprint.c │ │ │ │ ├── general.c │ │ │ │ ├── gym_tr3.c │ │ │ │ ├── ids.c │ │ │ │ ├── ids.h │ │ │ │ ├── items.c │ │ │ │ └── lara.c │ │ │ ├── actions.h │ │ │ ├── anim.c │ │ │ ├── anim.h │ │ │ ├── carrier.c │ │ │ ├── carrier.h │ │ │ ├── col.c │ │ │ ├── col.h │ │ │ ├── const.h │ │ │ ├── draw.c │ │ │ ├── draw.h │ │ │ ├── enum.h │ │ │ ├── manager.c │ │ │ ├── manager.h │ │ │ ├── types.h │ │ │ ├── utils.c │ │ │ ├── utils.h │ │ │ ├── walkable.c │ │ │ └── walkable.h │ │ ├── items.h │ │ ├── lara/ │ │ │ ├── breath.c │ │ │ ├── breath.h │ │ │ ├── cheat.c │ │ │ ├── cheat.h │ │ │ ├── cheat_keys.c │ │ │ ├── cheat_keys.h │ │ │ ├── col/ │ │ │ │ ├── climb.c │ │ │ │ ├── crouch.c │ │ │ │ ├── jump.c │ │ │ │ ├── land.c │ │ │ │ ├── monkey.c │ │ │ │ └── swim.c │ │ │ ├── col.c │ │ │ ├── col.h │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── const.h │ │ │ ├── control.c │ │ │ ├── control.h │ │ │ ├── draw.c │ │ │ ├── draw.h │ │ │ ├── electric.c │ │ │ ├── electric.h │ │ │ ├── enum.h │ │ │ ├── flare.c │ │ │ ├── flare.h │ │ │ ├── hair.c │ │ │ ├── hair.h │ │ │ ├── look.c │ │ │ ├── look.h │ │ │ ├── mesh.c │ │ │ ├── mesh.h │ │ │ ├── misc.c │ │ │ ├── misc.h │ │ │ ├── pose.c │ │ │ ├── pose.h │ │ │ ├── skin/ │ │ │ │ ├── common.c │ │ │ │ ├── common.h │ │ │ │ ├── enum.h │ │ │ │ ├── storage.c │ │ │ │ ├── storage.h │ │ │ │ └── types.h │ │ │ ├── skin.h │ │ │ ├── state/ │ │ │ │ ├── climb.c │ │ │ │ ├── crouch.c │ │ │ │ ├── extra.c │ │ │ │ ├── jump.c │ │ │ │ ├── land.c │ │ │ │ ├── monkey.c │ │ │ │ └── swim.c │ │ │ ├── state.c │ │ │ ├── state.h │ │ │ ├── types.h │ │ │ ├── util.h │ │ │ ├── vehicle.c │ │ │ └── vehicle.h │ │ ├── lara.h │ │ ├── level/ │ │ │ ├── cache.c │ │ │ ├── cache.h │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── context.c │ │ │ ├── context.h │ │ │ ├── finalize/ │ │ │ │ ├── animations.c │ │ │ │ ├── gameplay_objects.c │ │ │ │ ├── render_assets.c │ │ │ │ └── rooms.c │ │ │ ├── finalize.h │ │ │ ├── format/ │ │ │ │ ├── format.h │ │ │ │ ├── format_tr1.c │ │ │ │ ├── format_tr2.c │ │ │ │ ├── format_tr3.c │ │ │ │ ├── pipeline.c │ │ │ │ └── priv.h │ │ │ ├── pipeline.c │ │ │ ├── pipeline.h │ │ │ ├── sections/ │ │ │ │ ├── anims.c │ │ │ │ ├── append.h │ │ │ │ ├── audio.c │ │ │ │ ├── cinematics.c │ │ │ │ ├── meshes.c │ │ │ │ ├── objects.c │ │ │ │ ├── pathing.c │ │ │ │ ├── read.h │ │ │ │ ├── rooms.c │ │ │ │ └── textures.c │ │ │ ├── settings.c │ │ │ └── settings.h │ │ ├── level.h │ │ ├── los.h │ │ ├── lua/ │ │ │ ├── assault_stats.c │ │ │ ├── camera.c │ │ │ ├── catalog.c │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── config.c │ │ │ ├── console.c │ │ │ ├── creatures.c │ │ │ ├── embedded_scripts.h │ │ │ ├── events.c │ │ │ ├── events.h │ │ │ ├── game.c │ │ │ ├── items.c │ │ │ ├── lara.c │ │ │ ├── log.c │ │ │ ├── music.c │ │ │ ├── objects.c │ │ │ ├── rooms.c │ │ │ └── sound.c │ │ ├── lua.h │ │ ├── matrix.c │ │ ├── matrix.h │ │ ├── music/ │ │ │ ├── backend_cdaudio.c │ │ │ ├── backend_cdaudio.h │ │ │ ├── backend_cdaudio_wad.c │ │ │ ├── backend_cdaudio_wad.h │ │ │ ├── backend_files.c │ │ │ ├── backend_files.h │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── const.h │ │ │ ├── enum.h │ │ │ ├── ids.c │ │ │ ├── ids.h │ │ │ └── types.h │ │ ├── music.h │ │ ├── objects/ │ │ │ ├── col.c │ │ │ ├── col.h │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── creatures/ │ │ │ │ ├── ape.c │ │ │ │ ├── atlantean.c │ │ │ │ ├── atlantean.h │ │ │ │ ├── bacon_lara.c │ │ │ │ ├── bacon_lara.h │ │ │ │ ├── baldy.c │ │ │ │ ├── bandit_1.c │ │ │ │ ├── bandit_2.c │ │ │ │ ├── bandit_common.h │ │ │ │ ├── barracuda.c │ │ │ │ ├── bartoli.c │ │ │ │ ├── bat.c │ │ │ │ ├── bear.c │ │ │ │ ├── big_eel.c │ │ │ │ ├── big_spider.c │ │ │ │ ├── big_spider.h │ │ │ │ ├── bird.c │ │ │ │ ├── bird_guardian.c │ │ │ │ ├── centaur.c │ │ │ │ ├── centaur_statue.c │ │ │ │ ├── civilian.c │ │ │ │ ├── claw_mutant.c │ │ │ │ ├── claw_mutant_internal.h │ │ │ │ ├── claw_mutant_plasma_ball.c │ │ │ │ ├── cobra.c │ │ │ │ ├── compy.c │ │ │ │ ├── cowboy.c │ │ │ │ ├── crawler_mutant.c │ │ │ │ ├── crocodile.c │ │ │ │ ├── cultist_1.c │ │ │ │ ├── cultist_2.c │ │ │ │ ├── cultist_3.c │ │ │ │ ├── cultist_common.h │ │ │ │ ├── diver.c │ │ │ │ ├── dog.c │ │ │ │ ├── dragon.c │ │ │ │ ├── eel.c │ │ │ │ ├── hybrid_mutant.c │ │ │ │ ├── jelly.c │ │ │ │ ├── larson.c │ │ │ │ ├── lion.c │ │ │ │ ├── lizard.c │ │ │ │ ├── mercenary.c │ │ │ │ ├── monk.c │ │ │ │ ├── monkey.c │ │ │ │ ├── mouse.c │ │ │ │ ├── mp_1.c │ │ │ │ ├── mp_2.c │ │ │ │ ├── mummy.c │ │ │ │ ├── natla.c │ │ │ │ ├── natla_gun.c │ │ │ │ ├── orca.c │ │ │ │ ├── patrol_dog.c │ │ │ │ ├── pierre.c │ │ │ │ ├── pod.c │ │ │ │ ├── pod.h │ │ │ │ ├── prisoner.c │ │ │ │ ├── punk.c │ │ │ │ ├── raptor.c │ │ │ │ ├── rat.c │ │ │ │ ├── rx_worker_1.c │ │ │ │ ├── rx_worker_2.c │ │ │ │ ├── rx_worker_3.c │ │ │ │ ├── security_guard.c │ │ │ │ ├── shark.c │ │ │ │ ├── shiva.c │ │ │ │ ├── skate_kid.c │ │ │ │ ├── skidoo_driver.c │ │ │ │ ├── skidoo_driver.h │ │ │ │ ├── sophia.c │ │ │ │ ├── sophia_internal.h │ │ │ │ ├── sophia_laser_bolt.c │ │ │ │ ├── sophia_plasma_ball.c │ │ │ │ ├── spider.c │ │ │ │ ├── swat.c │ │ │ │ ├── tiger.c │ │ │ │ ├── tony.c │ │ │ │ ├── tony_fire_ball.c │ │ │ │ ├── tony_internal.h │ │ │ │ ├── torso.c │ │ │ │ ├── trex.c │ │ │ │ ├── trex_alpha.c │ │ │ │ ├── tribe_axeman.c │ │ │ │ ├── tribe_boss.c │ │ │ │ ├── tribe_boss.h │ │ │ │ ├── tribe_pipeman.c │ │ │ │ ├── wasp_mutant.c │ │ │ │ ├── willard.c │ │ │ │ ├── willard_internal.h │ │ │ │ ├── willard_plasma_ball.c │ │ │ │ ├── winston.c │ │ │ │ ├── winston_army.c │ │ │ │ ├── wolf.c │ │ │ │ ├── worker_1.c │ │ │ │ ├── worker_2.c │ │ │ │ ├── worker_3.c │ │ │ │ ├── worker_common.h │ │ │ │ ├── xian_common.c │ │ │ │ ├── xian_common.h │ │ │ │ ├── xian_knight.c │ │ │ │ ├── xian_spearman.c │ │ │ │ └── yeti.c │ │ │ ├── draw.c │ │ │ ├── draw.h │ │ │ ├── effects/ │ │ │ │ ├── blood.c │ │ │ │ ├── body_part.c │ │ │ │ ├── bubble.c │ │ │ │ ├── dart_effect.c │ │ │ │ ├── ember.c │ │ │ │ ├── explosion.c │ │ │ │ ├── flame.c │ │ │ │ ├── flame.h │ │ │ │ ├── glow.c │ │ │ │ ├── gun_flash.c │ │ │ │ ├── gun_shell.c │ │ │ │ ├── hot_liquid.c │ │ │ │ ├── missile.c │ │ │ │ ├── pickup_aid.c │ │ │ │ ├── ricochet.c │ │ │ │ ├── snow_sprite.c │ │ │ │ ├── splash.c │ │ │ │ ├── twinkle.c │ │ │ │ ├── twinkle.h │ │ │ │ └── water_sprite.c │ │ │ ├── general/ │ │ │ │ ├── ai_node.c │ │ │ │ ├── alarm_sound.c │ │ │ │ ├── animating.c │ │ │ │ ├── area_51_rocket.c │ │ │ │ ├── assault_target.c │ │ │ │ ├── bat_emitter.c │ │ │ │ ├── bell.c │ │ │ │ ├── big_bowl.c │ │ │ │ ├── bird_tweeter.c │ │ │ │ ├── boat.c │ │ │ │ ├── bridge_common.c │ │ │ │ ├── bridge_common.h │ │ │ │ ├── bridge_flat.c │ │ │ │ ├── bridge_tilt1.c │ │ │ │ ├── bridge_tilt2.c │ │ │ │ ├── cabin.c │ │ │ │ ├── camera_target.c │ │ │ │ ├── carcass.c │ │ │ │ ├── clock_chimes.c │ │ │ │ ├── cog.c │ │ │ │ ├── combat_end.c │ │ │ │ ├── combat_end.h │ │ │ │ ├── copter.c │ │ │ │ ├── cutscene_player.c │ │ │ │ ├── detonator_box.c │ │ │ │ ├── ding_dong.c │ │ │ │ ├── disposable_animating.c │ │ │ │ ├── door.c │ │ │ │ ├── door.h │ │ │ │ ├── drawbridge.c │ │ │ │ ├── dummy.c │ │ │ │ ├── earthquake.c │ │ │ │ ├── final_cutscene.c │ │ │ │ ├── flare_item.c │ │ │ │ ├── flare_item.h │ │ │ │ ├── fuse_box.c │ │ │ │ ├── gas_emitter.c │ │ │ │ ├── general.c │ │ │ │ ├── general.h │ │ │ │ ├── gong.c │ │ │ │ ├── gong_bonger.c │ │ │ │ ├── grenade.c │ │ │ │ ├── harpoon_bolt.c │ │ │ │ ├── keyhole.c │ │ │ │ ├── keyhole.h │ │ │ │ ├── kill_all_triggered.c │ │ │ │ ├── lara_alarm.c │ │ │ │ ├── lift.c │ │ │ │ ├── lights/ │ │ │ │ │ ├── beacon_light.c │ │ │ │ │ ├── colored_light.c │ │ │ │ │ ├── electrical_light.c │ │ │ │ │ ├── on_off_light.c │ │ │ │ │ ├── pulse_light.c │ │ │ │ │ └── strobe_light.c │ │ │ │ ├── mini_copter.c │ │ │ │ ├── moving_bar.c │ │ │ │ ├── pickup.c │ │ │ │ ├── pickup.h │ │ │ │ ├── puzzle_hole.c │ │ │ │ ├── rocket.c │ │ │ │ ├── save_crystal.c │ │ │ │ ├── scion1.c │ │ │ │ ├── scion3.c │ │ │ │ ├── scion4.c │ │ │ │ ├── scion_holder.c │ │ │ │ ├── shoal.c │ │ │ │ ├── shoal.h │ │ │ │ ├── smashable.c │ │ │ │ ├── smashable.h │ │ │ │ ├── smoke_emitter.c │ │ │ │ ├── sphere_of_doom.c │ │ │ │ ├── switch.c │ │ │ │ ├── switch.h │ │ │ │ ├── trapdoor.c │ │ │ │ ├── trigger_gate.c │ │ │ │ ├── waterfall.c │ │ │ │ └── zipline.c │ │ │ ├── ids.h │ │ │ ├── names.c │ │ │ ├── names.def │ │ │ ├── names.h │ │ │ ├── setup.c │ │ │ ├── setup.h │ │ │ ├── traps/ │ │ │ │ ├── blade.c │ │ │ │ ├── bubble_emitter.c │ │ │ │ ├── cleaner.c │ │ │ │ ├── common.c │ │ │ │ ├── common.h │ │ │ │ ├── damocles_sword.c │ │ │ │ ├── dart.c │ │ │ │ ├── dart_emitter.c │ │ │ │ ├── dying_monk.c │ │ │ │ ├── electric_fence.c │ │ │ │ ├── ember_emitter.c │ │ │ │ ├── falling_block.c │ │ │ │ ├── falling_ceiling.c │ │ │ │ ├── fire_head.c │ │ │ │ ├── flame_emitter.c │ │ │ │ ├── gondola.c │ │ │ │ ├── gondola.h │ │ │ │ ├── hook.c │ │ │ │ ├── icicle.c │ │ │ │ ├── killer_statue.c │ │ │ │ ├── lava_wedge.c │ │ │ │ ├── lightning_emitter.c │ │ │ │ ├── midas_touch.c │ │ │ │ ├── mine.c │ │ │ │ ├── movable_block.c │ │ │ │ ├── movable_block.h │ │ │ │ ├── pendulum.c │ │ │ │ ├── power_saw.c │ │ │ │ ├── propeller.c │ │ │ │ ├── propeller.h │ │ │ │ ├── raptor_emitter.c │ │ │ │ ├── rolling_ball.c │ │ │ │ ├── rotating_laser.c │ │ │ │ ├── security_laser.c │ │ │ │ ├── sentry_gun.c │ │ │ │ ├── sliding_pillar.c │ │ │ │ ├── spike_ceiling.c │ │ │ │ ├── spike_wall.c │ │ │ │ ├── spikes.c │ │ │ │ ├── spinning_blade.c │ │ │ │ ├── springboard.c │ │ │ │ ├── teeth_trap.c │ │ │ │ ├── thors_hammer.c │ │ │ │ ├── train.c │ │ │ │ └── wasp_emitter.c │ │ │ ├── types.h │ │ │ ├── vars.c │ │ │ ├── vars.h │ │ │ └── vehicles/ │ │ │ ├── boat.c │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── kayak.c │ │ │ ├── kayak.h │ │ │ ├── mine_cart.c │ │ │ ├── mine_cart.h │ │ │ ├── mounted_gun.c │ │ │ ├── mounted_gun.h │ │ │ ├── quad_bike.c │ │ │ ├── quad_bike.h │ │ │ ├── rib.c │ │ │ ├── skidoo_armed.c │ │ │ ├── skidoo_armed.h │ │ │ ├── skidoo_common.c │ │ │ ├── skidoo_common.h │ │ │ ├── skidoo_fast.c │ │ │ ├── upv.c │ │ │ └── upv.h │ │ ├── objects.h │ │ ├── option/ │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── controls.c │ │ │ ├── controls.h │ │ │ ├── examine.c │ │ │ ├── examine.h │ │ │ ├── gameplay.c │ │ │ ├── gameplay.h │ │ │ ├── globe_select.c │ │ │ ├── globe_select.h │ │ │ ├── graphics.c │ │ │ ├── graphics.h │ │ │ ├── passport.c │ │ │ ├── passport.h │ │ │ ├── sound.c │ │ │ ├── sound.h │ │ │ ├── stats.c │ │ │ └── stats.h │ │ ├── option.h │ │ ├── output/ │ │ │ ├── bind.c │ │ │ ├── bind.h │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── const.h │ │ │ ├── draw.c │ │ │ ├── draw.h │ │ │ ├── func.c │ │ │ ├── func.h │ │ │ ├── lights.c │ │ │ ├── lights.h │ │ │ ├── mesh_batcher/ │ │ │ │ ├── batcher.c │ │ │ │ ├── batcher.h │ │ │ │ ├── mesh.c │ │ │ │ ├── mesh.h │ │ │ │ ├── mesh_builder.c │ │ │ │ └── mesh_builder.h │ │ │ ├── overlay.h │ │ │ ├── quad.c │ │ │ ├── quad.h │ │ │ ├── scene_compositor.c │ │ │ ├── scene_compositor.h │ │ │ ├── scene_source.h │ │ │ ├── shaders/ │ │ │ │ ├── generic.c │ │ │ │ ├── generic.h │ │ │ │ ├── mesh.c │ │ │ │ ├── mesh.h │ │ │ │ ├── ui.c │ │ │ │ └── ui.h │ │ │ ├── sources/ │ │ │ │ ├── lightnings.c │ │ │ │ ├── lightnings.h │ │ │ │ ├── misc.c │ │ │ │ ├── misc.h │ │ │ │ ├── objects.c │ │ │ │ ├── objects.h │ │ │ │ ├── overlay.c │ │ │ │ ├── overlay.h │ │ │ │ ├── poly_fx.c │ │ │ │ ├── poly_fx.h │ │ │ │ ├── rooms.c │ │ │ │ ├── rooms.h │ │ │ │ ├── rooms_debug.c │ │ │ │ ├── rooms_debug.h │ │ │ │ ├── shadows.c │ │ │ │ ├── shadows.h │ │ │ │ ├── sprites.c │ │ │ │ ├── sprites.h │ │ │ │ ├── ui.c │ │ │ │ └── ui.h │ │ │ ├── state.c │ │ │ ├── state.h │ │ │ ├── textures.c │ │ │ ├── textures.h │ │ │ ├── types.h │ │ │ ├── uniforms.c │ │ │ ├── uniforms.h │ │ │ ├── utils.c │ │ │ ├── utils.h │ │ │ ├── vars.c │ │ │ ├── vars.h │ │ │ ├── vertex_range.c │ │ │ └── vertex_range.h │ │ ├── output.h │ │ ├── overlay.c │ │ ├── overlay.h │ │ ├── pathing/ │ │ │ ├── box.c │ │ │ ├── box.h │ │ │ ├── const.h │ │ │ ├── lot.c │ │ │ ├── lot.h │ │ │ └── types.h │ │ ├── pathing.h │ │ ├── phase/ │ │ │ ├── control.h │ │ │ ├── executor.c │ │ │ ├── executor.h │ │ │ ├── phase_cutscene.c │ │ │ ├── phase_cutscene.h │ │ │ ├── phase_demo.c │ │ │ ├── phase_demo.h │ │ │ ├── phase_game.c │ │ │ ├── phase_game.h │ │ │ ├── phase_globe_select.c │ │ │ ├── phase_globe_select.h │ │ │ ├── phase_inventory.c │ │ │ ├── phase_inventory.h │ │ │ ├── phase_pause.c │ │ │ ├── phase_pause.h │ │ │ ├── phase_photo_mode.c │ │ │ ├── phase_photo_mode.h │ │ │ ├── phase_picture.c │ │ │ ├── phase_picture.h │ │ │ ├── phase_stats.c │ │ │ ├── phase_stats.h │ │ │ └── types.h │ │ ├── phase.h │ │ ├── photo_mode.c │ │ ├── photo_mode.h │ │ ├── random.c │ │ ├── random.h │ │ ├── replay/ │ │ │ ├── test_recorder.c │ │ │ ├── test_recorder.h │ │ │ ├── test_replay.c │ │ │ └── test_replay.h │ │ ├── rooms/ │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── const.h │ │ │ ├── draw.c │ │ │ ├── draw.h │ │ │ ├── enum.h │ │ │ ├── floor_data.c │ │ │ ├── floor_data.h │ │ │ ├── geometry.c │ │ │ ├── geometry.h │ │ │ ├── types.h │ │ │ └── utils.h │ │ ├── rooms.h │ │ ├── savegame/ │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── enum.h │ │ │ ├── file.c │ │ │ ├── file.h │ │ │ ├── file_read.c │ │ │ ├── file_write.c │ │ │ └── types.h │ │ ├── savegame.h │ │ ├── screenshot.c │ │ ├── screenshot.h │ │ ├── shell/ │ │ │ ├── args.c │ │ │ ├── args.h │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── config.c │ │ │ ├── config.h │ │ │ ├── const.h │ │ │ ├── events.c │ │ │ ├── events.h │ │ │ ├── flow.c │ │ │ ├── flow.h │ │ │ ├── input.c │ │ │ ├── input.h │ │ │ ├── main.c │ │ │ ├── mod.c │ │ │ ├── mod.h │ │ │ ├── paths.c │ │ │ ├── paths.h │ │ │ ├── platform.c │ │ │ ├── platform.h │ │ │ ├── session.c │ │ │ ├── session.h │ │ │ ├── state.c │ │ │ └── state.h │ │ ├── shell.h │ │ ├── sound/ │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── enum.h │ │ │ ├── ids.c │ │ │ ├── ids.h │ │ │ └── types.h │ │ ├── sound.h │ │ ├── sparks/ │ │ │ ├── enum.h │ │ │ ├── manager.c │ │ │ ├── manager.h │ │ │ ├── spawners.c │ │ │ ├── spawners.h │ │ │ └── types.h │ │ ├── sparks.h │ │ ├── spawn.c │ │ ├── spawn.h │ │ ├── stats/ │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── const.h │ │ │ ├── init.c │ │ │ ├── init.h │ │ │ ├── scan.c │ │ │ ├── scan.h │ │ │ └── types.h │ │ ├── stats.h │ │ ├── types.h │ │ ├── ui/ │ │ │ ├── common.c │ │ │ ├── common.h │ │ │ ├── dialogs/ │ │ │ │ ├── base_passport.c │ │ │ │ ├── base_passport.h │ │ │ │ ├── color_editor.c │ │ │ │ ├── color_editor.h │ │ │ │ ├── config_presets.c │ │ │ │ ├── config_presets.h │ │ │ │ ├── controls.c │ │ │ │ ├── controls.h │ │ │ │ ├── controls_backend.c │ │ │ │ ├── controls_backend.h │ │ │ │ ├── controls_editor.c │ │ │ │ ├── controls_editor.h │ │ │ │ ├── gameplay_settings.c │ │ │ │ ├── gameplay_settings.h │ │ │ │ ├── graphic_settings.c │ │ │ │ ├── graphic_settings.h │ │ │ │ ├── new_game.c │ │ │ │ ├── new_game.h │ │ │ │ ├── pause.c │ │ │ │ ├── pause.h │ │ │ │ ├── photo_mode.c │ │ │ │ ├── photo_mode.h │ │ │ │ ├── play_any_level.c │ │ │ │ ├── play_any_level.h │ │ │ │ ├── save_slot.c │ │ │ │ ├── save_slot.h │ │ │ │ ├── select_level.c │ │ │ │ ├── select_level.h │ │ │ │ ├── setting_helpers/ │ │ │ │ │ ├── enums.c │ │ │ │ │ ├── enums.h │ │ │ │ │ ├── handlers.c │ │ │ │ │ ├── handlers.h │ │ │ │ │ └── handlers_language.c │ │ │ │ ├── setting_tabs/ │ │ │ │ │ ├── gameplay_controls.def │ │ │ │ │ ├── gameplay_fixes.def │ │ │ │ │ ├── gameplay_general.def │ │ │ │ │ ├── gameplay_mods.def │ │ │ │ │ ├── graphic_rendering.def │ │ │ │ │ ├── graphic_ui.def │ │ │ │ │ ├── graphic_ui_bars.def │ │ │ │ │ ├── graphic_ui_stats.def │ │ │ │ │ ├── graphic_visuals.def │ │ │ │ │ ├── sound_misc.def │ │ │ │ │ └── sound_volume.def │ │ │ │ ├── settings.c │ │ │ │ ├── settings.h │ │ │ │ ├── settings_editor.c │ │ │ │ ├── settings_editor.h │ │ │ │ ├── settings_tabs.c │ │ │ │ ├── settings_tabs.h │ │ │ │ ├── sound_settings.c │ │ │ │ ├── sound_settings.h │ │ │ │ ├── stats.c │ │ │ │ ├── stats.h │ │ │ │ ├── switch_mod.c │ │ │ │ ├── switch_mod.h │ │ │ │ ├── text.c │ │ │ │ └── text.h │ │ │ ├── dialogs.h │ │ │ ├── draw.c │ │ │ ├── draw.h │ │ │ ├── elements/ │ │ │ │ ├── ammo_label.c │ │ │ │ ├── ammo_label.h │ │ │ │ ├── anchor.c │ │ │ │ ├── anchor.h │ │ │ │ ├── bar.c │ │ │ │ ├── bar.h │ │ │ │ ├── bar_enemy_hp.c │ │ │ │ ├── bar_enemy_hp.h │ │ │ │ ├── bar_lara_air.c │ │ │ │ ├── bar_lara_air.h │ │ │ │ ├── bar_lara_exposure.c │ │ │ │ ├── bar_lara_exposure.h │ │ │ │ ├── bar_lara_hp.c │ │ │ │ ├── bar_lara_hp.h │ │ │ │ ├── bar_lara_sprint.c │ │ │ │ ├── bar_lara_sprint.h │ │ │ │ ├── button_label.c │ │ │ │ ├── button_label.h │ │ │ │ ├── color_swatch.c │ │ │ │ ├── color_swatch.h │ │ │ │ ├── flash.c │ │ │ │ ├── flash.h │ │ │ │ ├── fps_counter.c │ │ │ │ ├── fps_counter.h │ │ │ │ ├── frame.c │ │ │ │ ├── frame.h │ │ │ │ ├── gradient_slider.c │ │ │ │ ├── gradient_slider.h │ │ │ │ ├── hide.c │ │ │ │ ├── hide.h │ │ │ │ ├── horizontal_line.c │ │ │ │ ├── horizontal_line.h │ │ │ │ ├── label.c │ │ │ │ ├── label.h │ │ │ │ ├── modal.c │ │ │ │ ├── modal.h │ │ │ │ ├── offset.c │ │ │ │ ├── offset.h │ │ │ │ ├── pad.c │ │ │ │ ├── pad.h │ │ │ │ ├── progress_button.c │ │ │ │ ├── progress_button.h │ │ │ │ ├── prompt.c │ │ │ │ ├── prompt.h │ │ │ │ ├── requester.c │ │ │ │ ├── requester.h │ │ │ │ ├── resize.c │ │ │ │ ├── resize.h │ │ │ │ ├── row_arrows.c │ │ │ │ ├── row_arrows.h │ │ │ │ ├── scrollable_stack.c │ │ │ │ ├── scrollable_stack.h │ │ │ │ ├── sleek_bar.c │ │ │ │ ├── sleek_bar.h │ │ │ │ ├── spacer.c │ │ │ │ ├── spacer.h │ │ │ │ ├── span.c │ │ │ │ ├── span.h │ │ │ │ ├── stack.c │ │ │ │ ├── stack.h │ │ │ │ ├── tab_switch.c │ │ │ │ ├── tab_switch.h │ │ │ │ ├── window.c │ │ │ │ └── window.h │ │ │ ├── elements.h │ │ │ ├── events.c │ │ │ ├── events.h │ │ │ ├── helpers.c │ │ │ ├── helpers.h │ │ │ ├── hud/ │ │ │ │ ├── console.c │ │ │ │ ├── console.h │ │ │ │ ├── console_logs.c │ │ │ │ ├── console_logs.h │ │ │ │ ├── overlay.c │ │ │ │ └── overlay.h │ │ │ ├── hud.h │ │ │ ├── scaler.c │ │ │ ├── scaler.h │ │ │ ├── scrollable.c │ │ │ ├── scrollable.h │ │ │ ├── settings.c │ │ │ ├── settings.h │ │ │ ├── text.c │ │ │ ├── text.def │ │ │ ├── text.h │ │ │ └── text_autogen.def │ │ ├── ui.h │ │ ├── viewport.c │ │ └── viewport.h │ ├── gl/ │ │ ├── buffer.c │ │ ├── buffer.h │ │ ├── config.h │ │ ├── context.c │ │ ├── context.h │ │ ├── enum.c │ │ ├── enum.h │ │ ├── fbo.c │ │ ├── fbo.h │ │ ├── program.c │ │ ├── program.h │ │ ├── renderer.c │ │ ├── renderer.h │ │ ├── sampler.c │ │ ├── sampler.h │ │ ├── screenshot.c │ │ ├── screenshot.h │ │ ├── texture.c │ │ ├── texture.h │ │ ├── track.c │ │ ├── track.h │ │ ├── utils.c │ │ ├── utils.h │ │ ├── vertex_array.c │ │ └── vertex_array.h │ ├── version.c │ └── version.h └── tools/ ├── additional_lint ├── download_assets ├── embed_trx_lua.py ├── ffmpeg_flags.txt ├── generate_icon ├── generate_init ├── generate_rcfile ├── get_version ├── glyphs/ │ ├── README.md │ ├── generate_case_map │ ├── generate_compositions │ ├── generate_defs │ ├── generate_keyboard_map │ ├── test_alignment.html │ └── test_language ├── inspect_save ├── installer/ │ ├── .gitignore │ ├── TR1X_Installer/ │ │ ├── App.xaml │ │ ├── App.xaml.cs │ │ ├── Installers/ │ │ │ ├── CDRomInstallSource.cs │ │ │ ├── GOGInstallSource.cs │ │ │ ├── SteamInstallSource.cs │ │ │ ├── TR1XInstallSource.cs │ │ │ └── TombATIInstallSource.cs │ │ ├── Resources/ │ │ │ ├── Lang/ │ │ │ │ ├── en.json │ │ │ │ └── it.json │ │ │ └── const.json │ │ └── TR1X_Installer.csproj │ ├── TR2X_Installer/ │ │ ├── App.xaml │ │ ├── App.xaml.cs │ │ ├── Installers/ │ │ │ ├── CDRomInstallSource.cs │ │ │ ├── GOGInstallSource.cs │ │ │ ├── GenericInstallSource.cs │ │ │ ├── SteamInstallSource.cs │ │ │ └── TR2XInstallSource.cs │ │ ├── Resources/ │ │ │ ├── Lang/ │ │ │ │ ├── en.json │ │ │ │ └── it.json │ │ │ └── const.json │ │ └── TR2X_Installer.csproj │ ├── TRX_Installer/ │ │ ├── App.xaml │ │ ├── App.xaml.cs │ │ ├── BoolToVisibilityConverter.cs │ │ ├── CueFile.cs │ │ ├── CueTrack.cs │ │ ├── DiscImageInstallSource.cs │ │ ├── DownloadOption.cs │ │ ├── ExistingTRXInstallSource.cs │ │ ├── IInstallSource.cs │ │ ├── IInstallerProgress.cs │ │ ├── InstallComponent.cs │ │ ├── InstallComponentFactory.cs │ │ ├── InstallFileHelper.cs │ │ ├── InstallMappings.cs │ │ ├── InstallSourceOption.cs │ │ ├── InstallerService.cs │ │ ├── MainWindow.xaml │ │ ├── MainWindow.xaml.cs │ │ ├── OptionalDownload.cs │ │ ├── OriginalDirectoryInstallSource.cs │ │ ├── Resources/ │ │ │ ├── Lang/ │ │ │ │ ├── en.json │ │ │ │ └── it.json │ │ │ └── const.json │ │ └── TRX_Installer.csproj │ ├── TRX_Installer.sln │ └── TRX_InstallerLib/ │ ├── Controls/ │ │ ├── FinishStepControl.xaml │ │ ├── FinishStepControl.xaml.cs │ │ ├── InstallSettingsStepControl.xaml │ │ ├── InstallSettingsStepControl.xaml.cs │ │ ├── InstallSourceControl.xaml │ │ ├── InstallSourceControl.xaml.cs │ │ ├── InstallStepControl.xaml │ │ ├── InstallStepControl.xaml.cs │ │ ├── SourceStepControl.xaml │ │ ├── SourceStepControl.xaml.cs │ │ ├── TRXInstallWindow.xaml │ │ └── TRXInstallWindow.xaml.cs │ ├── Installers/ │ │ ├── BaseInstallSource.cs │ │ ├── IInstallSource.cs │ │ ├── InstallExecutor.cs │ │ └── InstallUtils.cs │ ├── Models/ │ │ ├── BaseLanguageViewModel.cs │ │ ├── ExpansionPackType.cs │ │ ├── FinishSettings.cs │ │ ├── FinishStep.cs │ │ ├── IStep.cs │ │ ├── InstallSettings.cs │ │ ├── InstallSettingsStep.cs │ │ ├── InstallSourceViewModel.cs │ │ ├── InstallStep.cs │ │ ├── Language.cs │ │ ├── Logger.cs │ │ ├── MainWindowViewModel.cs │ │ ├── SourceStep.cs │ │ └── TRXConstants.cs │ ├── Resources/ │ │ ├── Lang/ │ │ │ ├── en.json │ │ │ └── it.json │ │ ├── const.json │ │ └── styles.xaml │ ├── TRX_InstallerLib.csproj │ └── Utils/ │ ├── AssemblyUtils.cs │ ├── BaseNotifyPropertyChanged.cs │ ├── BinaryReaderExtensions.cs │ ├── BoolToVisibilityConverter.cs │ ├── ComparisonConverter.cs │ ├── ConditionalMarkupConverter.cs │ ├── ConditionalViewTextConverter.cs │ ├── CueFile.cs │ ├── CueTrack.cs │ ├── FileBrowser.cs │ ├── HttpProgressClient.cs │ ├── InstallProgress.cs │ ├── JsonUtils.cs │ ├── ProcessUtils.cs │ ├── RelayCommand.cs │ └── ShortcutUtils.cs ├── output_current_changelog ├── output_package_name ├── output_release_name ├── release ├── shared/ │ ├── __init__.py │ ├── changelog.py │ ├── docker/ │ │ ├── __init__.py │ │ ├── game-linux/ │ │ │ ├── Dockerfile │ │ │ └── entrypoint.sh │ │ ├── game-win/ │ │ │ ├── Dockerfile │ │ │ ├── entrypoint.sh │ │ │ └── meson_linux_mingw32.txt │ │ ├── game_entrypoint.py │ │ ├── installer/ │ │ │ ├── Dockerfile │ │ │ └── entrypoint.sh │ │ └── lua.pc │ ├── files.py │ ├── git.py │ ├── glyph_mapping.py │ ├── icons.py │ ├── ida_progress.py │ ├── import_sorter.py │ ├── json_utils.py │ ├── linting.py │ ├── mac/ │ │ ├── bundle_dylibs │ │ ├── create_installer │ │ ├── install_tree │ │ └── x86-64_cross_file.txt │ ├── packaging.py │ ├── paths.py │ ├── utils.py │ ├── versioning.py │ └── vfs.py ├── sort_imports ├── tr2/ │ ├── __init__.py │ ├── generate_ida_importer │ └── read_tombpc_script ├── tr3/ │ └── objects_tracker/ │ ├── objects_dump.json │ ├── objects_support.json │ ├── read_levels │ └── render ├── update_game_strings ├── update_install_trees └── update_water_colors ================================================ FILE CONTENTS ================================================ ================================================ FILE: .clang-format ================================================ --- AlignAfterOpenBracket: AlwaysBreak AllowShortFunctionsOnASingleLine: None AlwaysBreakAfterReturnType: None BasedOnStyle: Webkit BreakBeforeBinaryOperators: NonAssignment BreakAdjacentStringLiterals: true AlwaysBreakBeforeMultilineStrings: true ColumnLimit: 80 IndentPPDirectives: BeforeHash NamespaceIndentation: None PenaltyReturnTypeOnItsOwnLine: 1000 PointerAlignment: Right SortIncludes: false InsertBraces: true SpaceInEmptyBraces: Never ================================================ FILE: .dockerignore ================================================ /data /test /build ================================================ FILE: .editorconfig ================================================ [*] insert_final_newline = true charset = utf-8 indent_style = space end_of_line = lf [*.py] indent_size = 4 max_line_length = 79 trim_trailing_whitespace = true [*.lua] indent_size = 2 ================================================ FILE: .github/CODEOWNERS ================================================ * @LostArtefacts/dev @LostArtefacts/qa ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug report description: Report a reproducible problem in TRX title: "[Bug] " body: - type: markdown attributes: value: | Please open one issue per bug. Before submitting, read the bug reporting guide: https://github.com/LostArtefacts/TRX/blob/develop/BUG_REPORTING.md - type: dropdown id: game_type attributes: label: Game type description: What does this affect? options: - Original game - Custom level - Not sure validations: required: true - type: dropdown id: game attributes: label: Game options: - All games - TR1 - TR2 - TR3 - Other validations: required: true - type: markdown attributes: value: | If your report is about TR3, please note that current support is limited to Lara's Home, India, and South Pacific. Reports for other TR3 areas are still out of scope while that work is in progress, so those issues are not being accepted yet. - type: input id: trx_version attributes: label: TRX version description: Use the exact version. It can be found in the main menu, or .exe properties. placeholder: 1.2.3 validations: required: true - type: input id: os attributes: label: Operating system placeholder: Windows 11 validations: required: true - type: input id: gpu attributes: label: GPU placeholder: NVIDIA GeForce RTX 4070 validations: required: false - type: textarea id: repro_steps attributes: label: Describe the bug in plain terms, and write the shortest steps that reproduce it. placeholder: | Lara falls through the bridge after loading a save near the switch room. 1. Load the attached save. 2. Walk onto the bridge. 3. Pull the switch. 4. Observe Lara falling through the floor. validations: required: true - type: textarea id: expected_result attributes: label: What did you expect instead? placeholder: Lara should stay on the bridge and the switch should work normally. validations: required: true - type: textarea id: attachments attributes: label: Attachments description: Attach `TRX.log`, saves, screenshots, videos, and custom level files here if they help reproduce the issue. placeholder: Drag and drop `TRX.log`, save files, screenshots, videos, or custom level files here. validations: required: true - type: checkboxes id: confirmations attributes: label: Before submitting options: - label: I included enough detail to reproduce this. required: true - label: I attached `TRX.log` and any saves, media, or custom level files needed to reproduce this. required: true - label: This issue covers one bug. required: true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Bug reporting guidelines url: https://github.com/LostArtefacts/TRX/blob/develop/BUG_REPORTING.md about: Read this before opening a bug report, especially for logs, savegames, and custom levels. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: Feature request description: Suggest an improvement, enhancement, or new idea title: "[Feature] " body: - type: markdown attributes: value: | Feature requests can be lightweight. A clear problem statement is more useful than a fully detailed spec. - type: textarea id: idea attributes: label: What would you like to see? description: Describe the idea in plain terms. placeholder: I'd like photo mode to remember the last-used HUD visibility setting. validations: required: true - type: textarea id: problem attributes: label: What problem would this solve? description: Explain the pain point, limitation, or workflow this would improve. placeholder: Re-enabling the same setting every time is repetitive when taking several screenshots in a row. validations: required: false - type: textarea id: proposed_behavior attributes: label: Extra context description: Add mockups, examples, references, screenshots, videos, related issues, etc. placeholder: Add any extra context here that helps. validations: required: false ================================================ FILE: .github/actions/prepare_macos_tooling/action.yml ================================================ name: Cache TRX MacOS Dependencies inputs: FFMPEG_INSTALL_TMP_ARM64: required: false default: /opt/local/install_arm64 FFMPEG_INSTALL_TMP_X86_64: required: false default: /opt/local/install_x86_64 CACHE_SRC_DIR: required: false default: /opt/local CACHE_DIR: required: true runs: using: "composite" steps: - name: Select latest stable Xcode if: steps.restore-cache.outputs.cache-hit != 'true' uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: latest-stable - name: Install and update MacPorts if: steps.restore-cache.outputs.cache-hit != 'true' shell: bash run: | wget -O ${{ github.workspace }}/macports.pkg https://github.com/macports/macports-base/releases/download/v2.9.2/MacPorts-2.9.2-14-Sonoma.pkg sudo installer -pkg ${{ github.workspace }}/macports.pkg -target / - name: Install build and deployment tools if: steps.restore-cache.outputs.cache-hit != 'true' shell: bash run: | # Install Python first to avoid multiple Python in the dep tree later on. sudo port -N install python313 py313-pip sudo port select --set python python313 sudo port select --set python3 python313 sudo port select --set pip pip313 sudo port select --set pip3 pip313 # Install Clang to get better C23 support. sudo port -N install clang-16 sudo port select --set clang mp-clang-16 # Install the rest. sudo port -N install create-dmg meson ninja pkgconfig sudo pip3 install pyjson5 - name: "Build dependencies: Compression libraries (universal)" if: steps.restore-cache.outputs.cache-hit != 'true' shell: bash run: | sudo port -N install zlib +universal sudo port -N install bzip2 +universal sudo port -N install xz +universal - name: "Build dependency: pcre2 (universal)" if: steps.restore-cache.outputs.cache-hit != 'true' shell: bash run: sudo port -N install pcre2 +universal - name: "Build dependency: GLEW (universal)" if: steps.restore-cache.outputs.cache-hit != 'true' shell: bash run: sudo port -N install glew +universal - name: "Build dependency: libsdl2 (universal)" if: steps.restore-cache.outputs.cache-hit != 'true' shell: bash run: sudo port -N install libsdl2 +universal - name: "Build dependency: lua (universal)" if: steps.restore-cache.outputs.cache-hit != 'true' shell: bash run: sudo port -N install lua +universal - name: "Build dependency: uthash (universal)" if: steps.restore-cache.outputs.cache-hit != 'true' shell: bash run: sudo port -N install uthash +universal - name: "Build dependency: ffmpeg (universal)" if: steps.restore-cache.outputs.cache-hit != 'true' shell: bash run: | # Install to separate staging paths for all architectures in # preparation for fusing universal libraries in a follow-up step. cd "$RUNNER_TEMP" git clone https://github.com/FFmpeg/FFmpeg ffmpeg-arm64 cd ffmpeg-arm64 git checkout 066432ebcf # Common FFmpeg configure options FFMPEG_CONFIG_OPTIONS=" \ --enable-shared \ --disable-static \ $(cat $GITHUB_WORKSPACE/tools/ffmpeg_flags.txt)" # Configure for arm64. ./configure \ --arch=arm64 \ --prefix=${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }} \ --cc='clang' \ $FFMPEG_CONFIG_OPTIONS # Build and install. make -j$(sysctl -n hw.ncpu) sudo make install cd "$RUNNER_TEMP" git clone https://github.com/FFmpeg/FFmpeg ffmpeg-x86-64 cd ffmpeg-x86-64 git checkout 066432ebcf # Configure for x86-64. ./configure \ --arch=x86_64 \ --enable-cross-compile \ --prefix=${{ inputs.FFMPEG_INSTALL_TMP_X86_64 }} \ --cc='clang -arch x86_64' \ $FFMPEG_CONFIG_OPTIONS # Build and install. make -j$(sysctl -n hw.ncpu) sudo make install - name: "Build dependency: ffmpeg (fuse universal libraries)" if: steps.restore-cache.outputs.cache-hit != 'true' shell: bash run: | # Libs FFMPEG_LIBS=( "libavcodec" "libavdevice" "libavfilter" "libavformat" "libavutil" "libswresample" "libswscale" ) # Recreate include tree in MacPorts install prefix. sudo rsync -arvL ${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }}/include/ ${{ inputs.CACHE_SRC_DIR }}/include/ # Recreate library symlinks in MacPorts install prefix. sudo find ${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }}/lib/ -type l -exec cp -P '{}' ${{ inputs.CACHE_SRC_DIR }}/lib/ ';' # Fuse platform-specific binaries into a universal binary. for LIB in ${FFMPEG_LIBS[@]}; do RESOLVED_LIB=$(ls -l ${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }}/lib/${LIB}* \ | grep -v '^l' \ | awk -F'/' '{print $NF}') sudo lipo -create \ ${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }}/lib/$RESOLVED_LIB \ ${{ inputs.FFMPEG_INSTALL_TMP_X86_64 }}/lib/$RESOLVED_LIB \ -output ${{ inputs.CACHE_SRC_DIR }}/lib/$RESOLVED_LIB sudo ln -s -f \ ${{ inputs.CACHE_SRC_DIR }}/lib/$RESOLVED_LIB \ ${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }}/lib/$RESOLVED_LIB sudo ln -s -f \ ${{ inputs.CACHE_SRC_DIR }}/lib/$RESOLVED_LIB \ ${{ inputs.FFMPEG_INSTALL_TMP_X86_64 }}/lib/$RESOLVED_LIB done # Update and install pkgconfig files. for file in "${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }}/lib/pkgconfig"/*.pc; do sudo sed -i '' "s:${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }}:${{ inputs.CACHE_SRC_DIR }}:g" "$file" done sudo mv ${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }}/lib/pkgconfig/* ${{ inputs.CACHE_SRC_DIR }}/lib/pkgconfig/ - name: "Prepare dependencies for caching" if: steps.restore-cache.outputs.cache-hit != 'true' shell: bash run: | # Remove MacPorts leftover build and download files sudo rm -rf /opt/local/var/macports/build/* sudo rm -rf /opt/local/var/macports/distfiles/* sudo rm -rf /opt/local/var/macports/packages/* # Delete broken symlinks sudo find ${{ inputs.CACHE_SRC_DIR }} -type l ! -exec test -e {} \; -exec rm {} \; # Trying to cache the source directory directly leads to permission errors, # so copy it to an intermediate temporary directory. sudo rsync -arvq ${{ inputs.CACHE_SRC_DIR }}/ ${{ inputs.CACHE_DIR }} - name: "Save dependencies to cache" if: steps.restore-cache.outputs.cache-hit != 'true' uses: actions/cache/save@v4 with: key: ${{ runner.os }}-tooling-${{ hashFiles('.github/actions/prepare_macos_tooling/action.yml') }} path: | ${{ inputs.CACHE_DIR }} ================================================ FILE: .github/docker/lint.Dockerfile ================================================ FROM python:3.12-slim-bookworm ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ curl \ git \ gnupg \ xz-utils \ && rm -rf /var/lib/apt/lists/* RUN curl -fsSL https://apt.llvm.org/llvm-snapshot.gpg.key | gpg --dearmor -o /usr/share/keyrings/llvm-archive-keyring.gpg RUN echo 'deb [signed-by=/usr/share/keyrings/llvm-archive-keyring.gpg] https://apt.llvm.org/bookworm llvm-toolchain-bookworm-22 main' > /etc/apt/sources.list.d/llvm.list RUN apt-get update && apt-get install -y --no-install-recommends \ clang-format-22 \ && apt-get purge -y --auto-remove gnupg xz-utils \ && rm -rf /var/lib/apt/lists/* RUN ln -s /usr/bin/clang-format-22 /usr/local/bin/clang-format RUN just_version='1.40.0' && \ curl -fsSL "https://github.com/casey/just/releases/download/${just_version}/just-${just_version}-x86_64-unknown-linux-musl.tar.gz" \ | tar -xz -C /usr/local/bin just && \ chmod +x /usr/local/bin/just RUN python3 -m pip install --no-cache-dir \ prek \ pyjson5 ENV PATH="/usr/local/bin:${PATH}" ================================================ FILE: .github/pull_request_template.md ================================================ #### Checklist - [ ] I have read the [coding conventions](https://github.com/LostArtefacts/TRX/blob/develop/docs/CONTRIBUTING.md#coding-conventions) - [ ] I have added a changelog entry about what my pull request accomplishes, or it is an internal change - [ ] I have added a readme entry about my new feature or OG bug fix, or it is a different change #### Description ... ================================================ FILE: .github/workflows/build_docker.yml ================================================ name: Build Docker toolchain on: - workflow_dispatch jobs: publish_docker_image: name: Build Docker toolchain runs-on: ubuntu-latest strategy: matrix: include: - platform: win - platform: linux steps: - name: Login to Docker Hub uses: docker/login-action@v1 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Install dependencies uses: taiki-e/install-action@just - name: Checkout code uses: actions/checkout@v4 with: submodules: 'true' - name: Build Docker image (${{ matrix.platform }}) run: | just image-${{ matrix.platform }} just push-image-${{ matrix.platform }} ================================================ FILE: .github/workflows/build_lint_image.yml ================================================ name: Build lint image on: push: branches: - main - lint paths: - .github/docker/lint.Dockerfile - .github/workflows/build_lint_image.yml workflow_dispatch: permissions: contents: read packages: write jobs: build: name: Build and push lint image runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Normalize repository owner id: vars run: echo "owner_lc=${GITHUB_REPOSITORY_OWNER,,}" >> "$GITHUB_OUTPUT" - name: Build and push lint image uses: docker/build-push-action@v6 with: context: . file: .github/docker/lint.Dockerfile push: true tags: | ghcr.io/${{ steps.vars.outputs.owner_lc }}/trx-lint:latest ghcr.io/${{ steps.vars.outputs.owner_lc }}/trx-lint:${{ github.sha }} ================================================ FILE: .github/workflows/comment_build.yml ================================================ name: Post build links to pull request on: workflow_run: workflows: ['Create a test build'] types: [completed] permissions: actions: write contents: write pull-requests: write jobs: pr_comment: if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-latest steps: - uses: actions/github-script@v7 with: # This snippet is public-domain, combined from # https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml # https://github.com/AKSW/submission.d2r2.aksw.org/blob/main/.github/workflows/pr-comment.yml script: | // Function Definitions class NoMatchingPRError extends Error { constructor(message) { super(message); this.name = "NoMatchingPRError"; } } /** * Fetch PR details for a given commit SHA. * @returns {Object} PR details containing prNumber, prRef, prRepoId. * @throws {Error} If no matching PR is found. */ async function fetchPRDetails() { const iterator = github.paginate.iterator(github.rest.pulls.list, { owner: context.repo.owner, repo: context.repo.repo, }); for await (const { data } of iterator) { for (const pull of data) { if (pull.head.sha === '${{github.event.workflow_run.head_sha}}') { return { prNumber: pull.number, prRef: pull.head.ref, prRepoId: pull.head.repo.id }; } } } throw new NoMatchingPRError("No matching PR found for the commit SHA"); } /** * Fetch all artifacts for a given workflow run. * @returns {Object} All artifacts data. * @throws {Error} If no artifacts are found. */ async function fetchAllArtifacts() { const artifactsResponse = await github.rest.actions.listWorkflowRunArtifacts({ owner: context.repo.owner, repo: context.repo.repo, run_id: context.payload.workflow_run.id, }); if (!(artifactsResponse.data && artifactsResponse.data.artifacts && artifactsResponse.data.artifacts.length)) { throw new Error("No artifacts found for the workflow run"); } return artifactsResponse.data.artifacts; } /** * Create or update a comment on the PR. * @param {number} prNumber - The PR number. * @param {string} purpose - The purpose of the comment. * @param {string} body - The comment body. * @throws {Error} If the comment creation or update fails. */ async function upsertComment(prNumber, purpose, body) { const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, }); const marker = ``; body = marker + "\n" + body; const existing = comments.filter(c => c.body.includes(marker)); if (existing.length > 0) { const last = existing[existing.length - 1]; core.info(`Updating comment ${last.id}`); await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, body: body, comment_id: last.id, }); } else { core.info(`Creating a comment in PR #${prNumber}`); await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, body: body, issue_number: prNumber, }); } } /** * Handle and log errors with detailed context and exit the process. * @param {Error} error - The error object. * @param {string} description - Description of the context where the error occurred. */ function handleError(error, description, prNumber) { let exitCode = 1; let log = core.error; if (error instanceof NoMatchingPRError) { exitCode = 0; log = core.warning; } log(`Failed to ${description}`); log(`Message: ${error.message}`); log(`Stack Trace: ${error.stack || 'No stack trace available'}`); if (prNumber) { log(`PR Number: ${prNumber}`); } log(`PRs: https://api.github.com/repos/${context.repo.owner}/${context.repo.repo}/pulls`); log(`SHA: ${{github.event.workflow_run.head_sha}}`); process.exit(exitCode); } // Main Code Execution let prNumber, prRef, prRepoId; // Fetch PR details try { ({ prNumber, prRef, prRepoId } = await fetchPRDetails()); core.info(`Found PR: #${prNumber}, Ref: ${prRef}, Repo ID: ${prRepoId}`); } catch (error) { handleError(error, 'fetch PR details', undefined); } // Fetch all artifacts let allArtifacts; try { allArtifacts = await fetchAllArtifacts(); core.info(`Artifacts fetched successfully`); } catch (error) { handleError(error, 'fetch artifacts', prNumber); } // Construct the comment body let body = 'Download the built assets for this pull request:\n' + allArtifacts .filter(item => item.name !== "assets") .sort((a, b) => a.name.localeCompare(b.name)) .map(item => `* [${item.name}.zip](https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/artifacts/${item.id}.zip)`) .join('\n'); // Upsert the comment on the PR try { await upsertComment(prNumber, "nightly-link", body); core.info("Comment created/updated successfully"); } catch (error) { handleError(error, 'create/update comment', prNumber); } ================================================ FILE: .github/workflows/job_build.yml ================================================ name: Build TRX and the installer on: workflow_call: inputs: platform: type: string description: "Platform to build for" required: true target: type: string description: "Target to build for" required: true zip: type: boolean description: "Pack the artifacts into zip" required: true jobs: build: name: Build release assets runs-on: ubuntu-latest steps: - name: Install dependencies uses: taiki-e/install-action@just - name: Checkout code uses: actions/checkout@v4 with: submodules: 'true' fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha || github.sha }} - id: vars name: Prepare variables run: | echo "version=$(just output-current-version)" >> $GITHUB_OUTPUT echo "tr1_dir=build/artifacts/tr1/" >> $GITHUB_OUTPUT echo "tr2_dir=build/artifacts/tr2/" >> $GITHUB_OUTPUT echo "tr3_dir=build/artifacts/tr3/" >> $GITHUB_OUTPUT echo "trx_dir=build/artifacts/trx/" >> $GITHUB_OUTPUT echo "tr1_asset=$(just output-package-name --game 1 --platform ${{ inputs.platform }})" >> $GITHUB_OUTPUT echo "tr2_asset=$(just output-package-name --game 2 --platform ${{ inputs.platform }})" >> $GITHUB_OUTPUT echo "tr3_asset=$(just output-package-name --game 3 --platform ${{ inputs.platform }})" >> $GITHUB_OUTPUT echo "trx_asset=$(just output-package-name --platform ${{ inputs.platform }})" >> $GITHUB_OUTPUT - name: Download large assets if: ${{ inputs.target == 'release' }} run: | just download-assets 1 just download-assets 2 just download-assets 3 just download-assets --combined - name: Restore ccache if: ${{ inputs.platform == 'linux' || inputs.platform == 'win' }} uses: actions/cache@v4 with: path: .cache/ccache key: ccache-v1-${{ runner.os }}-${{ inputs.platform }}-${{ inputs.target }}-${{ hashFiles('justfile', 'tools/shared/docker/game-linux/Dockerfile', 'tools/shared/docker/game-win/Dockerfile', 'tools/shared/docker/game-win/meson_linux_mingw32.txt') }} restore-keys: | ccache-v1-${{ runner.os }}-${{ inputs.platform }}-${{ inputs.target }}- ccache-v1-${{ runner.os }}-${{ inputs.platform }}- - name: Package asset (${{ inputs.platform }}) env: CCACHE_DIR: /app/.cache/ccache/${{ inputs.platform }}-${{ inputs.target }} CCACHE_BASEDIR: /app CCACHE_COMPILERCHECK: content CCACHE_MAXSIZE: 1G run: | if [ "${{ inputs.platform }}" = "linux" ] || [ "${{ inputs.platform }}" = "win" ]; then mkdir -p ".cache/ccache/${{ inputs.platform }}-${{ inputs.target }}" if [ "${{ inputs.zip }}" = "1" ] || [ "${{ inputs.zip }}" = "true" ]; then just trx-package-${{ inputs.platform }} "${{ inputs.target }}" -o "build/artifacts/" else just trx-package-${{ inputs.platform }} "${{ inputs.target }}" -o "build/artifacts/" --no-zip fi else if [ "${{ inputs.zip }}" = "1" ] || [ "${{ inputs.zip }}" = "true" ]; then just tr1-package-${{ inputs.platform }} "${{ inputs.target }}" -o "${{ steps.vars.outputs.tr1_dir }}" just tr2-package-${{ inputs.platform }} "${{ inputs.target }}" -o "${{ steps.vars.outputs.tr2_dir }}" just trx-package-${{ inputs.platform }} "${{ inputs.target }}" -o "${{ steps.vars.outputs.trx_dir }}" else just tr1-package-${{ inputs.platform }} "${{ inputs.target }}" -o "${{ steps.vars.outputs.tr1_dir }}" --no-zip just tr2-package-${{ inputs.platform }} "${{ inputs.target }}" -o "${{ steps.vars.outputs.tr2_dir }}" --no-zip just trx-package-${{ inputs.platform }} "${{ inputs.target }}" -o "${{ steps.vars.outputs.trx_dir }}" --no-zip fi fi - name: Upload artifacts (tr1) uses: actions/upload-artifact@v4 with: name: ${{ steps.vars.outputs.tr1_asset }} path: ${{ steps.vars.outputs.tr1_dir }} compression-level: 0 - name: Upload artifacts (tr2) uses: actions/upload-artifact@v4 with: name: ${{ steps.vars.outputs.tr2_asset }} path: ${{ steps.vars.outputs.tr2_dir }} compression-level: 0 - name: Upload artifacts (tr3) if: ${{ inputs.platform != 'win-installer' }} uses: actions/upload-artifact@v4 with: name: ${{ steps.vars.outputs.tr3_asset }} path: ${{ steps.vars.outputs.tr3_dir }} compression-level: 0 - name: Upload artifacts (combined) uses: actions/upload-artifact@v4 with: name: ${{ steps.vars.outputs.trx_asset }} path: ${{ steps.vars.outputs.trx_dir }} compression-level: 0 ================================================ FILE: .github/workflows/job_build_macos.yml ================================================ name: Build TRX and the installer (macOS) on: workflow_call: inputs: target: type: string description: "Target to build for" required: true let_mac_fail: type: boolean description: "Do not require Mac builds to pass" required: false default: false env: FFMPEG_INSTALL_FINAL: /opt/local FFMPEG_INSTALL_TMP_UNIVERSAL: /tmp/install_universal FFMPEG_INSTALL_TMP_ARM64: /tmp/install_arm64 FFMPEG_INSTALL_TMP_X86_64: /tmp/install_x86_64 CACHE_TMP_DIR: /tmp/opt_local/ CACHE_DST_DIR: /opt/local/ C_INCLUDE_PATH: /opt/local/include/uthash/:/opt/local/include/ jobs: build: strategy: matrix: game: - { version: 1 } - { version: 2 } - { version: 3 } name: Build release assets runs-on: macos-14 continue-on-error: ${{ inputs.let_mac_fail == true || inputs.let_mac_fail == 'true' }} steps: - name: Set up signing certificate env: MACOS_KEYCHAIN_PWD: ${{ secrets.MACOS_KEYCHAIN_PWD }} MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }} run: | CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db echo -n "$MACOS_CERTIFICATE" | base64 --decode -o $CERTIFICATE_PATH security create-keychain -p "$MACOS_KEYCHAIN_PWD" $KEYCHAIN_PATH security set-keychain-settings -lut 21600 $KEYCHAIN_PATH security unlock-keychain -p "$MACOS_KEYCHAIN_PWD" $KEYCHAIN_PATH security import $CERTIFICATE_PATH -P "$MACOS_KEYCHAIN_PWD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH -T /usr/bin/codesign security list-keychain -d user -s $KEYCHAIN_PATH security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k $MACOS_KEYCHAIN_PWD $KEYCHAIN_PATH - name: Checkout uses: actions/checkout@v4 with: submodules: 'true' fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha || github.sha }} - id: vars name: Prepare variables run: | echo "version=$(tools/get_version)" >> $GITHUB_OUTPUT echo "tag=tr${{matrix.game.version}}" >> $GITHUB_OUTPUT echo "name=TR${{matrix.game.version}}X" >> $GITHUB_OUTPUT echo "asset_name=$(tools/output_package_name --game ${{matrix.game.version}} --platform mac)" >> $GITHUB_OUTPUT - name: Extend PATH for MacPorts run: | echo -e "/opt/local/bin" >> $GITHUB_PATH echo -e "/opt/local/sbin" >> $GITHUB_PATH - name: "Try restore dependencies from cache" id: restore-cache uses: actions/cache/restore@v4 with: key: ${{ runner.os }}-tooling-${{ hashFiles('.github/actions/prepare_macos_tooling/action.yml') }} path: | /tmp/opt_local/ - name: "Build MacOS dependencies" if: steps.restore-cache.outputs.cache-hit != 'true' uses: ./.github/actions/prepare_macos_tooling with: CACHE_DIR: /tmp/opt_local/ - name: "Prepare cached dependencies for use" if: steps.restore-cache.outputs.cache-hit == 'true' shell: bash run: | sudo rsync -arvq /tmp/opt_local/ /opt/local/ sudo dscl . -create /Groups/macports sudo dscl . -create /Groups/macports RealName "MacPorts" sudo dscl . -create /Groups/macports PrimaryGroupID 501 sudo dscl . -create /Groups/macports GeneratedUID 172D097F-351A-4579-BBD1-430D99BC4ABF sudo dscl . -append /Groups/macports GroupMembership macports sudo dscl . -create /Users/macports sudo dscl . -create /Users/macports RealName "MacPorts" sudo dscl . -create /Users/macports UniqueID 502 sudo dscl . -create /Users/macports PrimaryGroupID 501 sudo dscl . -create /Users/macports UserShell /usr/bin/false sudo dscl . -create /Users/macports NFSHomeDirectory /opt/local/var/macports/home sudo mkdir -p /opt/local/var/macports/home sudo chown -R macports:macports /opt/local/var/macports - name: Setup CA run: | sudo port -N install apple-pki-bundle curl-ca-bundle - name: Download large assets #if: ${{ inputs.target == 'release' }} run: tools/download_assets ${{ matrix.game.version }} - name: Build arm64 and create app bundle env: CC: clang run: | BUILD_DIR=build-arm64 BUILD_OPTIONS="src --prefix=/tmp/${{ steps.vars.outputs.name }}.app --bindir=Contents/MacOS --buildtype ${{ inputs.target }}" meson setup $BUILD_DIR $BUILD_OPTIONS meson install -C $BUILD_DIR --tags "${{ steps.vars.outputs.tag }},common" - name: Build x86-64 env: CC: clang run: | BUILD_DIR=build-x86-64 BUILD_OPTIONS="src --prefix=/tmp/${{ steps.vars.outputs.name }}.app --bindir=Contents/MacOS --cross-file tools/shared/mac/x86-64_cross_file.txt --buildtype ${{ inputs.target }}" meson setup $BUILD_DIR $BUILD_OPTIONS meson compile -C $BUILD_DIR - name: Fuse universal executable run: | BUNDLE_EXEC_DIR=/tmp/${{ steps.vars.outputs.name }}.app/Contents/MacOS # Fuse executable and move it into the app bundle. lipo -create build-x86-64/TRX $BUNDLE_EXEC_DIR/TRX -output $BUNDLE_EXEC_DIR/TRX_universal mv $BUNDLE_EXEC_DIR/TRX_universal $BUNDLE_EXEC_DIR/TRX # Update dynamic library links in the fused executable. ./tools/shared/mac/bundle_dylibs -a ${{ steps.vars.outputs.name }} --links-only - name: Sign app bundle run: | KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db IDENTITY=$(security find-identity -v -p codesigning $KEYCHAIN_PATH | awk -F'"' '{print $2}') xattr -cr /tmp/${{ steps.vars.outputs.name }}.app /usr/bin/codesign --force --deep --options runtime -s "${IDENTITY}" --keychain $KEYCHAIN_PATH -v /tmp/${{ steps.vars.outputs.name }}.app - name: Create, sign and notarize disk image env: MACOS_APPLEID: ${{ secrets.MACOS_APPLEID }} MACOS_APP_PWD: ${{ secrets.MACOS_APP_PWD }} MACOS_TEAMID: ${{ secrets.MACOS_TEAMID }} run: | KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db IDENTITY=$(security find-identity -v -p codesigning $KEYCHAIN_PATH | awk -F'"' '{print $2}') DMG_NAME="${{ steps.vars.outputs.asset_name }}.dmg" tools/shared/mac/create_installer -a ${{ steps.vars.outputs.name }} -i "data/${{ steps.vars.outputs.tag }}/mac/icon.icns" -d "${DMG_NAME}" xattr -cr "${DMG_NAME}" /usr/bin/codesign --force --options runtime -s "${IDENTITY}" --keychain $KEYCHAIN_PATH -v "${DMG_NAME}" xcrun notarytool submit --wait --apple-id "$MACOS_APPLEID" --password "$MACOS_APP_PWD" --team-id "$MACOS_TEAMID" "${DMG_NAME}" xcrun stapler staple -v "${DMG_NAME}" - name: Upload signed+notarized installer image uses: actions/upload-artifact@v4 with: name: ${{ steps.vars.outputs.asset_name }} path: | *.dmg compression-level: 0 # .dmg is already compressed. ================================================ FILE: .github/workflows/job_release.yml ================================================ name: Create a new release on: workflow_call: inputs: draft: type: boolean description: "Draft" required: true default: false prerelease: type: boolean description: "Prerelease" required: true default: false tag_name: type: string description: "Tag name" required: false default: github.ref_name jobs: release: name: Release runs-on: ubuntu-latest steps: - name: "Install dependencies" uses: taiki-e/install-action@just - name: "Checkout code" uses: actions/checkout@v4 with: submodules: 'true' fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: "Prepare release data" id: prepare_release_data run: | if [ "${{ inputs.prerelease }}" = "1" ] || [ "${{ inputs.prerelease }}" = "true" ]; then echo "release_name=Development snapshot" >> $GITHUB_OUTPUT echo "changelog<> $GITHUB_OUTPUT just output-current-changelog >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT else echo -n "release_name=" >> $GITHUB_OUTPUT just output-release-name >> $GITHUB_OUTPUT echo "changelog<> $GITHUB_OUTPUT just output-current-changelog --stable >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT fi - name: "Download built assets" uses: actions/download-artifact@v4 with: path: artifacts/ merge-multiple: true - name: "Get information on the latest pre-release" if: ${{ inputs.prerelease == true || inputs.prerelease == 'true' }} id: last_release uses: InsonusK/get-latest-release@v1.0.1 with: myToken: ${{ github.token }} exclude_types: "draft|release" - name: 'Mark the pre-release as latest' if: ${{ inputs.prerelease == true || inputs.prerelease == 'true' }} uses: EndBug/latest-tag@latest - name: "Delete old pre-release assets" if: ${{ inputs.prerelease == true || inputs.prerelease == 'true' }} uses: mknejp/delete-release-assets@v1 continue-on-error: true with: token: ${{ github.token }} tag: ${{ steps.last_release.outputs.tag_name }} assets: "*.*" - name: "Publish a release" uses: softprops/action-gh-release@v2.2.2 with: token: ${{ secrets.GITHUB_TOKEN }} tag_name: ${{ inputs.tag_name }} name: ${{ steps.prepare_release_data.outputs.release_name }} body: ${{ steps.prepare_release_data.outputs.changelog }} draft: ${{ inputs.draft == true || inputs.draft == 'true' }} prerelease: ${{ inputs.prerelease == true || inputs.prerelease == 'true' }} fail_on_unmatched_files: true files: | artifacts/* ================================================ FILE: .github/workflows/lint.yml ================================================ name: Run code linters on: - push - pull_request jobs: lint: name: Run code linters runs-on: ubuntu-latest env: PREK_HOME: ${{ github.workspace }}/.cache/prek permissions: contents: read packages: read container: image: ghcr.io/lostartefacts/trx-lint:latest steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 1 fetch-tags: false ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Configure git safe directory working-directory: ${{ github.workspace }} run: git config --global --add safe.directory "${GITHUB_WORKSPACE}" - name: Ensure prek cache dir exists run: mkdir -p "${PREK_HOME}" - name: Restore prek cache uses: actions/cache@v4 with: path: .cache/prek key: prek-v1-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }} restore-keys: | prek-v1-${{ runner.os }}- - name: Check formatted code differences working-directory: ${{ github.workspace }} run: | set +e just lint-format lint_format_status=$? set -e if ! git diff --quiet; then echo 'Formatting diffs detected in:' git diff --exit-code || ( clang-format --version echo 'Please run `just lint` and commit the changes.' exit 1 ) fi if [ "${lint_format_status}" -ne 0 ]; then clang-format --version echo 'just lint-format failed.' exit "${lint_format_status}" fi - name: Check imports working-directory: ${{ github.workspace }} run: | git add -u just lint-imports git diff --exit-code || ( include-what-you-use --version echo 'Please run `just lint` and commit the changes.' exit 1 ) ================================================ FILE: .github/workflows/pr_builds.yml ================================================ name: Create a test build permissions: contents: write on: pull_request: push: branches: - '!stable' - '!develop' jobs: package_linux: name: Linux uses: ./.github/workflows/job_build.yml with: platform: linux target: debug zip: false secrets: inherit package_win: name: Windows uses: ./.github/workflows/job_build.yml with: platform: win target: debug zip: false secrets: inherit # package_mac: # name: Mac # if: vars.MACOS_ENABLE == 'true' # uses: ./.github/workflows/job_build_macos.yml # with: # target: debug # let_mac_fail: true # secrets: inherit ================================================ FILE: .github/workflows/prerelease.yml ================================================ name: Publish a pre-release permissions: contents: write on: push: branches: - develop jobs: package_linux: name: Build Linux if: vars.PRERELEASE_ENABLE == 'true' uses: ./.github/workflows/job_build.yml with: platform: linux target: debug zip: true secrets: inherit package_win: name: Build Windows if: vars.PRERELEASE_ENABLE == 'true' uses: ./.github/workflows/job_build.yml with: platform: win target: debug zip: true secrets: inherit # package_win_installer: # name: Build Windows installer # if: vars.PRERELEASE_ENABLE == 'true' # uses: ./.github/workflows/job_build.yml # with: # platform: win-installer # target: debug # zip: false # secrets: inherit package_mac: name: Build Mac if: | vars.PRERELEASE_ENABLE == 'true' && vars.MACOS_ENABLE == 'true' uses: ./.github/workflows/job_build_macos.yml with: target: debug let_mac_fail: true secrets: inherit publish_prerelease: if: always() && (vars.PRERELEASE_ENABLE == 'true') name: Create a prerelease needs: - package_linux - package_mac - package_win # - package_win_installer with: draft: false prerelease: true tag_name: 'latest' uses: ./.github/workflows/job_release.yml ================================================ FILE: .github/workflows/release.yml ================================================ name: Publish a release permissions: contents: write on: push: branch: stable tags: - "trx-*" workflow_dispatch: inputs: draft: description: "Draft" required: true type: boolean default: false prerelease: description: "Prerelease" required: true type: boolean default: false tag_name: description: "Tag name" required: false type: string default: github.ref_name jobs: package_linux: name: Build Linux if: vars.RELEASE_ENABLE == 'true' uses: ./.github/workflows/job_build.yml with: platform: linux target: release zip: true secrets: inherit package_win: name: Build Windows if: vars.RELEASE_ENABLE == 'true' uses: ./.github/workflows/job_build.yml with: platform: win target: release zip: true secrets: inherit package_win_installer: name: Build Windows installer if: vars.RELEASE_ENABLE == 'true' uses: ./.github/workflows/job_build.yml with: platform: win-installer target: release zip: false secrets: inherit package_mac: name: Build Mac if: | vars.RELEASE_ENABLE == 'true' && vars.MACOS_ENABLE == 'true' uses: ./.github/workflows/job_build_macos.yml with: target: release let_mac_fail: ${{ inputs.let_mac_fail == true || inputs.let_mac_fail == 'true' }} secrets: inherit publish_release: if: always() && (vars.RELEASE_ENABLE == 'true') name: Create a GitHub release needs: - package_linux - package_win - package_win_installer - package_mac with: draft: ${{ inputs.draft || false }} prerelease: ${{ inputs.draft || false }} tag_name: ${{ inputs.tag_name || github.ref_name }} uses: ./.github/workflows/job_release.yml ================================================ FILE: .gitignore ================================================ *.cache.json TR1X.dll TR1X.exe TR1X.log TR2X.dll TR2X.exe TR2X.log # Docker builds garbage /build /.secrets /.local /workflow /TR1X*.zip /TR1X*.exe /TR2X*.zip /TR2X*.exe __pycache__/ # VS garbage v15/ v16/ *.suo *.o *.obj *.pdb *.lib *.exp Debug/ Release/ *.user *.ipch .vs/ *.vcxproj *.filters .dotnet/ # MacOS garbage .DS_Store # libtrx artefacts **/subprojects/packagecache/ **/subprojects/dwarfstack-*/ data/tr1/ship/data/images/ data/tr2/ship/data/images/ data/tr2/ship/data/level1.tr2 data/tr2/ship/data/level2.tr2 data/tr2/ship/data/level3.tr2 data/tr2/ship/data/level4.tr2 data/tr2/ship/data/level5.tr2 data/tr2/ship/data/main_gm.sfx data/tr2/ship/data/title_gm.tr2 data/tr2/ship/music/ data/tr3/ship/data/images/ tools/installer/TR1X_Installer/Resources/release.zip tools/installer/TR2X_Installer/Resources/release.zip tools/installer/TRX_Installer/Resources/release.zip ================================================ FILE: .gitmodules ================================================ [submodule "libtrx"] path = subprojects/libtrx url = https://github.com/LostArtefacts/libtrx.git ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: local hooks: - id: clang-format name: clang-format entry: clang-format args: ["-style=file", "-i"] language: system files: \.[ch](pp)?$ - id: additional-lint name: Run additional linters entry: tools/additional_lint language: python stages: [pre-commit] additional_dependencies: - pyjson5 - jsonschema - id: additional-lint -a name: Run additional linters (repo-wide) entry: tools/additional_lint -a language: python stages: [pre-commit] pass_filenames: false additional_dependencies: - pyjson5 - jsonschema - id: imports name: imports entry: tools/sort_imports language: system files: \.[ch](pp)?$ - id: update-game-strings name: Update game strings to match source code entry: tools/update_game_strings language: python stages: [pre-commit] pass_filenames: false additional_dependencies: - pyjson5 - repo: https://github.com/JohnnyMorganz/StyLua rev: v2.3.0 hooks: - id: stylua-github ================================================ FILE: BUG_REPORTING.md ================================================ # TRX Bug Reporting Guide Thanks for taking the time to report an issue. Good bug reports help us reproduce problems quickly and spend more time fixing them instead of guessing. The goal is simple: give us enough information to see the same bug you saw. ## 1. Where to report bugs - Please report bugs on GitHub issues. - If you cannot create a GitHub account, use `#trx-bugs` on Discord. - For Discord users, please **do not** report bugs in general chat. They are much harder to track there and usually miss important details. ## 2. One issue per report Please open one ticket per bug. Keeping reports separate makes it easier to reproduce, discuss, fix, and close each issue cleanly. ## 3. What every report should include Please include: - Clear step-by-step reproduction steps - Exact TRX version - Operating system - Logs, especially `TRX.log` - GPU details if you think it might be relevant Helpful extras: - Save files - Screenshots - Videos Extra requirement for custom levels: - Include the level files If a custom level bug does not include the level data, we usually cannot debug it properly. ## 4. How to write reproduction steps Try to describe the shortest reliable path to the issue. Good examples: - > 1. Load Escape from the Base > 2. Reach room 66 > 3. pull the lever > 4. game crashes. - > Play level X → do Y → push block disappears. Less helpful example: - `The game crashes sometimes when playing Bartoli's Bughouse.` That kind of report tells us something is wrong, but not how to reproduce it, and thus we can't do anything about it. ## 5. Why this matters Actionable reports are the fastest route to a fix. If a report does not include reproduction steps, version details, or the files needed to reproduce the issue, we may not be able to investigate it further and on a bad day may close it altogether. ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # TRX Community Guide and Code of Conduct TRX is a volunteer, maintainer-led open source project. We're happy to have you here. Our number one enemy is exhaustion. If discussion drains maintainers more than it helps the project, everyone loses. So we keep communication clean, constructive, and low-friction. There's no room for prolonged negativity, entitlement, or argument-as-a-hobby. ## 1. The vibe we want - Curious, practical, kind - Clear reports, clear proposals - Disagreement expressed as "here's a better idea" - Solutions over speeches You don't need to be an expert, but you do need to be respectful and actionable. ## 2. How to participate in a way that gets results When you want change, write it as a proposal. Good patterns: - "It would be great if [specific change], because [reason]". - "Current behavior: X. Expected behavior: Y. Evidence: Z." - "Comparison: Original game does [behavior]. TRX does [behavior]." - "Minimal reproduction case: [shortest possible steps to trigger issue]." - "After reading [documentation page/section], [specific information] remains unclear." - "Technical observation: [system/component] appears to ignore [condition/input]." - "Tested on: [version], [platform], [configuration]. Issue: [description]." If you can't describe the change clearly, it's not ready to be requested. Contributions are evaluated, not adopted automatically. Submitting research, proposals, or patches does not guarantee they will be merged in their current form, or at all. Maintainers may modify, defer, or decline contributions to keep the project aligned with its goals and plans. ## 3. What we will not engage with These are exhaustion-generators. They will be moderated quickly. - Drive-by bug reports (no steps, no version, no logs, “crashes sometimes”); reports outside bug channels - Complaints without a concrete proposal - Catastrophizing ("this ruins the game", "this breaks everything") instead of specifics - Re-litigating decisions after maintainers say it's decided - "Truth voice" posting: presenting personal preference as objective fact - Purity tests and vision wars ("real TR is X", "this feels like it drifts from the original vision", etc.) - Mislabeling TRX as “just a mod” or otherwise misrepresenting what the project is (TRX is a standalone, reverse engineered, build-from-source engine project). - Pressure tactics: guilt, demands, timelines, "you must", "you owe", "everyone agrees", "I think it is very important" ## 4. Governance TRX is maintainer-led. Maintainers have final say on: - Project vision and scope - Features and defaults - UI/UX and discoverability - What gets accepted, declined, closed, or locked You're welcome to suggest. Maintainers decide. If a decision is made, continuing to argue it is not "discussion", it's drain. ## 5. Moderation To protect the project, maintainers may: - Ask for a proposal format - Close issues that aren't actionable - Lock threads that turn circular or hostile - Remove disruptive participants from project spaces We don't want to do this. We will do this. ## 6. Basic respect Instant hard no: - Harassment or personal attacks - Discrimination - Threats or intimidation ## 7. If you disagree Totally fine. Choose a productive path: - Propose an alternative clearly - Adjust settings to your taste - Contribute a patch - Fork the project - Go play the OGs, Remasters, or something else altogether What's not fine is trying to force agreement through volume, repetition, or hostility. ## Closing > Push hard enough against the current, and even the river will stop trying to pull you along. When you consistently create friction rather than progress, you will lose your seat at the table. We're building something we care about, and we want the community (and ourselves) to feel good to be in. ================================================ FILE: COPYING.md ================================================ GNU GENERAL PUBLIC LICENSE ========================== Version 3, 29 June 2007 ========================== > Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. # Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. # TERMS AND CONDITIONS ## 0. Definitions. _"This License"_ refers to version 3 of the GNU General Public License. _"Copyright"_ also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. _"The Program"_ refers to any copyrightable work licensed under this License. Each licensee is addressed as _"you"_. _"Licensees"_ and "recipients" may be individuals or organizations. To _"modify"_ a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a _"modified version"_ of the earlier work or a work _"based on"_ the earlier work. A _"covered work"_ means either the unmodified Program or a work based on the Program. To _"propagate"_ a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To _"convey"_ a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. ## 1. Source Code. The _"source code"_ for a work means the preferred form of the work for making modifications to it. _"Object code"_ means any non-source form of a work. A _"Standard Interface"_ means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The _"System Libraries"_ of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The _"Corresponding Source"_ for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. ## 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. ## 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. ## 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. ## 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. ## 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A _"User Product"_ is either (1) a _"consumer product"_, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. _"Installation Information"_ for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. ## 7. Additional Terms. _"Additional permissions"_ are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. ## 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. ## 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. ## 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An _"entity transaction"_ is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. ## 11. Patents. A _"contributor"_ is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's _"essential patent claims"_ are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. ## 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. ## 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. ## 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. ## 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. ## 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. ## 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. # END OF TERMS AND CONDITIONS -------------------------------------------------------------------------- # How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type 'show w'. This is free software, and you are welcome to redistribute it under certain conditions; type 'show c' for details. The hypothetical commands _'show w'_ and _'show c'_ should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================

TRX – Tomb Raider I & II: Community Edition

TRX logo


Welcome to **TRX** – an open-source reimplementation of **Tomb Raider 1**, **Tomb Raider 2** and **Tomb Raider 3**. TRX aims to enhance these classic games through decompilation and the implementation of open-source alternatives to proprietary components. TRX is a single engine capable of running TR1, TR2, and custom levels respecting each of the distinct, classic engines' mechanics. ## Showcase
Restored braid in TR1 Enemy health bar and UI scaling
Photo mode 3D pickups
Skybox support Updated moveset including sprint
Customizable draw distance Developer console
Detailed level stats PS1 UI and expanded options
## Download Download the latest TRX release for TR1-TR3: ### Installation instructions * [Tomb Raider 1](docs/tr1/INSTALLING.md). * [Tomb Raider 2](docs/tr2/INSTALLING.md). * [Tomb Raider 3](docs/tr3/INSTALLING.md). * [Combined directory tree](docs/trx/INSTALLING.md). ### Changelog For the changelog for all of the games (TRX uses a unified engine capable of running all 3 games), please refer to [this document](docs/CHANGELOG.md). ## Q&A 1. **Are all three games fully playable from beginning to end?** TR1 and TR2 – yes, by all means! If you encounter a bug, please file a ticket. TR3 is still in the works, though, and the team is hard at work to make this happen! 2. **Can we get HD textures? What about other visual updates?** Regarding HD texture packs, that feature is not currently planned. As for other visual updates, being able to introduce reflections and skyboxes shows that quite literally the sky is the limit. TRX is constantly getting new rendering improvements and texture fixes. But great stuff takes time. 4. **Can I play this on Mac, Linux, Android...?** Currently supported platforms include Windows, Linux and macOS. 5. **Can I play this with a controller?** TRX supports a wide variety of controllers out of the box with no additional software required. The keyboard or controller controls can be fully customized in the Controls menu with multiple layouts. Multi-key combo shortcuts (up to 3 keys) and two binding slots per action are also supported. 6. **What about TR3 support?** TR3 work is well underway! Thanks to *TOMB3* serving as the backbone for many of its core systems, tons of native systems are already in place – new triangle geometry logic, new rendering effects, and more. Still early, but we're moving _fast_. ## Website The Lost Artefacts team is a small, passionate community of developers and creators with a deep love for the classic Tomb Raider titles. Our team builds and maintains freeware fan projects that celebrate Lara Croft's iconic adventures. Visit the website by clicking the logo below for more information on TRX and its documentation as well as other Tomb Raider projects.

TRX logo

## Credits - Endless GitHub contributors. - TR1 title screen image by Kidd Bowyer. HD assets by goblan and posix. - TR2 HD images by Arsunt. ================================================ FILE: data/common/glyphs/mapping.txt ================================================ # This file is used by the tooling in the tools/glyphs/ directory and is # crucial for the game's graphical text handling, serving two roles: # # 1. Hardcoding Unicode to sprite mapping # It generates C macros that map Unicode code points and escaped sequences # to O_ALPHABET's sprite indices, specify glyph dimensions, and instruct how # to compose compound characters - all getting hardcoded into the executable. # 2. Guidance for font.bin creation # It directs the injector tool in creating the font.bin file that contains # O_ALPHABET sprite bitmaps, along with additional positional information. # # Important notes: # - Some sprite indices are fixed. This is for compatibility with the original # game to retains original text format even if font.bin goes missing. include "mapping_basic_latin.txt" include "mapping_icons.txt" include "mapping_combining_diactrics.txt" include "mapping_latin-1_supplement.txt" include "mapping_latin_extended-a.txt" include "mapping_latin_extended-b.txt" include "mapping_greek_and_coptic.txt" include "mapping_cyrillic.txt" include "mapping_latin_extended_additional.txt" include "mapping_misc.txt" include "mapping_keyboard.txt" include "mapping_controller.txt" include "mapping_small.txt" ================================================ FILE: data/common/glyphs/mapping_basic_latin.txt ================================================ # -------------------------------------------------- # Unicode Block "Basic Latin" (U+0000 to U+007F) # -------------------------------------------------- # ASCII a-z U+0061:a 0 T manual_sprite("glyphs.png", 218, 41, 11, 18, index=26) U+0062:b 0 T manual_sprite("glyphs.png", 229, 41, 11, 18, index=27) U+0063:c 0 T manual_sprite("glyphs.png", 240, 41, 11, 18, index=28) U+0064:d 0 T manual_sprite("glyphs.png", 0, 59, 11, 18, index=29) U+0065:e 0 T manual_sprite("glyphs.png", 11, 59, 11, 18, index=30) U+0066:f 0 T manual_sprite("glyphs.png", 22, 59, 11, 18, index=31) U+0067:g 0 T manual_sprite("glyphs.png", 33, 59, 11, 18, index=32) U+0068:h 0 T manual_sprite("glyphs.png", 44, 59, 11, 18, index=33) U+0069:i 0 T manual_sprite("glyphs.png", 55, 59, 7, 18, index=34) U+006A:j 0 T manual_sprite("glyphs.png", 62, 59, 11, 18, index=35) U+006B:k 0 T manual_sprite("glyphs.png", 73, 59, 12, 18, index=36) U+006C:l 0 T manual_sprite("glyphs.png", 85, 59, 7, 18, index=37) U+006D:m 0 T manual_sprite("glyphs.png", 92, 59, 14, 18, index=38) U+006E:n 0 T manual_sprite("glyphs.png", 106, 59, 12, 18, index=39) U+006F:o 0 T manual_sprite("glyphs.png", 118, 59, 11, 18, index=40) U+0070:p 0 T manual_sprite("glyphs.png", 129, 59, 11, 18, index=41) U+0071:q 0 T manual_sprite("glyphs.png", 140, 59, 11, 18, index=42) U+0072:r 0 T manual_sprite("glyphs.png", 151, 59, 10, 18, index=43) U+0073:s 0 T manual_sprite("glyphs.png", 161, 59, 11, 18, index=44) U+0074:t 0 T manual_sprite("glyphs.png", 172, 59, 11, 18, index=45) U+0075:u 0 T manual_sprite("glyphs.png", 183, 59, 11, 18, index=46) U+0076:v 0 T manual_sprite("glyphs.png", 194, 59, 11, 18, index=47) U+0077:w 0 T manual_sprite("glyphs.png", 205, 59, 13, 18, index=48) U+0078:x 0 T manual_sprite("glyphs.png", 218, 59, 11, 18, index=49) U+0079:y 0 T manual_sprite("glyphs.png", 229, 59, 11, 18, index=50) U+007A:z 0 T manual_sprite("glyphs.png", 240, 59, 11, 18, index=51) # ASCII A-Z U+0041:A 0 T manual_sprite("glyphs.png", 65, 23, 17, 18, index=0) expand(-1) U+0042:B 0 T manual_sprite("glyphs.png", 82, 23, 13, 18, index=1) U+0043:C 0 T manual_sprite("glyphs.png", 95, 23, 13, 18, index=2) U+0044:D 0 T manual_sprite("glyphs.png", 108, 23, 13, 18, index=3) U+0045:E 0 T manual_sprite("glyphs.png", 121, 23, 13, 18, index=4) U+0046:F 0 T manual_sprite("glyphs.png", 134, 23, 13, 18, index=5) U+0047:G 0 T manual_sprite("glyphs.png", 147, 23, 13, 18, index=6) U+0048:H 0 T manual_sprite("glyphs.png", 160, 23, 15, 18, index=7) U+0049:I 0 T manual_sprite("glyphs.png", 175, 23, 10, 18, index=8) U+004A:J 0 T manual_sprite("glyphs.png", 185, 23, 13, 18, index=9) U+004B:K 0 T manual_sprite("glyphs.png", 198, 23, 14, 18, index=10) U+004C:L 0 T manual_sprite("glyphs.png", 212, 23, 13, 18, index=11) U+004D:M 0 T manual_sprite("glyphs.png", 225, 23, 15, 18, index=12) U+004E:N 0 T manual_sprite("glyphs.png", 240, 23, 15, 18, index=13) U+004F:O 0 T manual_sprite("glyphs.png", 0, 41, 14, 18, index=14) U+0050:P 0 T manual_sprite("glyphs.png", 14, 41, 13, 18, index=15) U+0051:Q 0 T manual_sprite("glyphs.png", 27, 41, 14, 18, index=16) U+0052:R 0 T manual_sprite("glyphs.png", 41, 41, 14, 18, index=17) U+0053:S 0 T manual_sprite("glyphs.png", 55, 41, 13, 18, index=18) U+0054:T 0 T manual_sprite("glyphs.png", 68, 41, 14, 18, index=19) U+0055:U 0 T manual_sprite("glyphs.png", 82, 41, 15, 18, index=20) U+0056:V 0 T manual_sprite("glyphs.png", 97, 41, 15, 18, index=21) U+0057:W 0 T manual_sprite("glyphs.png", 112, 41, 15, 18, index=22) U+0058:X 0 T manual_sprite("glyphs.png", 127, 41, 14, 18, index=23) U+0059:Y 0 T manual_sprite("glyphs.png", 141, 41, 14, 18, index=24) U+005A:Z 0 T manual_sprite("glyphs.png", 155, 41, 13, 18, index=25) # Digits 0-9 U+0030:0 0 T manual_sprite("glyphs.png", 135, 5, 14, 18, index=52) U+0031:1 0 T manual_sprite("glyphs.png", 149, 5, 11, 18, index=53) U+0032:2 0 T manual_sprite("glyphs.png", 160, 5, 12, 18, index=54) U+0033:3 0 T manual_sprite("glyphs.png", 172, 5, 12, 18, index=55) U+0034:4 0 T manual_sprite("glyphs.png", 184, 5, 12, 18, index=56) U+0035:5 0 T manual_sprite("glyphs.png", 196, 5, 12, 18, index=57) U+0036:6 0 T manual_sprite("glyphs.png", 208, 5, 12, 18, index=58) U+0037:7 0 T manual_sprite("glyphs.png", 220, 5, 12, 18, index=59) U+0038:8 0 T manual_sprite("glyphs.png", 232, 5, 12, 18, index=60) U+0039:9 0 T manual_sprite("glyphs.png", 244, 5, 12, 18, index=61) # Basic Punctuation U+0021:! 0 T manual_sprite("glyphs.png", 0, 5, 6, 18, index=64) U+0022:" 0 T manual_sprite("glyphs.png", 6, 5, 9, 18) U+0023:# 0 T manual_sprite("glyphs.png", 16, 5, 14, 18, index=78) U+0024:$ 0 T manual_sprite("glyphs.png", 30, 5, 11, 18) U+0025:% 0 T manual_sprite("glyphs.png", 41, 5, 13, 18) U+0026:& 0 T manual_sprite("glyphs.png", 54, 5, 11, 18) U+0027:' 0 T manual_sprite("glyphs.png", 65, 5, 6, 18, index=79) U+0028:( 0 T manual_sprite("glyphs.png", 71, 5, 7, 18) expand(w=-1) U+0029:) 0 T manual_sprite("glyphs.png", 78, 5, 7, 18) translate(x=1) expand(w=1) U+002A:* 0 T manual_sprite("glyphs.png", 85, 5, 9, 18) U+002B:+ 0 T manual_sprite("glyphs.png", 94, 5, 11, 18, index=72) U+002C:, 0 T manual_sprite("glyphs.png", 105, 5, 6, 18, index=63) U+002D:- 0 T manual_sprite("glyphs.png", 111, 5, 9, 18, index=71) U+002E:. 0 T manual_sprite("glyphs.png", 120, 5, 6, 18, index=62) U+002F:/ 0 T manual_sprite("glyphs.png", 126, 5, 9, 18, index=68) U+003A:: 0 T manual_sprite("glyphs.png", 0, 23, 6, 18, index=73) U+003B:; 0 T manual_sprite("glyphs.png", 6, 23, 6, 18) U+003C:< 0 T manual_sprite("glyphs.png", 13, 23, 9, 18) U+003D:= 0 T manual_sprite("glyphs.png", 22, 23, 9, 18) U+003E:> 0 T manual_sprite("glyphs.png", 31, 23, 9, 18) U+003F:? 0 T manual_sprite("glyphs.png", 41, 23, 12, 18, index=65) U+0040:@ 0 T manual_sprite("glyphs.png", 53, 23, 12, 18) U+005B:[ 0 T manual_sprite("glyphs.png", 168, 41, 8, 18, index=66) U+005C:\ 0 T manual_sprite("glyphs.png", 176, 41, 9, 18, index=76) U+005D:] 0 T manual_sprite("glyphs.png", 185, 41, 8, 18, index=75) U+005E:^ 0 T manual_sprite("glyphs.png", 193, 41, 9, 18) U+005F:_ 0 T manual_sprite("glyphs.png", 202, 41, 9, 18) U+0060:` 0 T manual_sprite("glyphs.png", 211, 41, 7, 18) U+007B:{ 0 T manual_sprite("glyphs.png", 0, 77, 8, 18) U+007C:| 0 T manual_sprite("glyphs.png", 8, 77, 6, 18) U+007D:} 0 T manual_sprite("glyphs.png", 14, 77, 8, 18) U+007E:~ 0 T manual_sprite("glyphs.png", 22, 77, 10, 18) ================================================ FILE: data/common/glyphs/mapping_combining_diactrics.txt ================================================ # -------------------------------------------------- # Combining diactrics # -------------------------------------------------- "\\{grave accent}" 0 c manual_sprite("glyphs.png", 0, 0, 7, 5, index=77) "\\{acute accent}" 0 c manual_sprite("glyphs.png", 7, 0, 7, 5, index=70) "\\{circumflex accent}" 0 c manual_sprite("glyphs.png", 32, 0, 9, 5, index=69) "\\{circumflex}" 0 T link("\\{circumflex accent}") "\\{macron}" 0 c manual_sprite("glyphs.png", 23, 0, 9, 5) "\\{breve}" 0 c manual_sprite("glyphs.png", 50, 0, 8, 5) "\\{dot above}" 0 c manual_sprite("glyphs.png", 58, 0, 6, 5) "\\{umlaut}" 0 c manual_sprite("glyphs.png", 14, 0, 9, 5, index=67) "\\{caron}" 0 c manual_sprite("glyphs.png", 41, 0, 9, 5) "\\{ring above}" 0 c manual_sprite("glyphs.png", 64, 0, 7, 5) "\\{tilde}" 0 c manual_sprite("glyphs.png", 71, 0, 10, 5) "\\{double acute accent}" 0 c manual_sprite("glyphs.png", 81, 0, 9, 5) "\\{acute umlaut}" 0 c manual_sprite("glyphs.png", 90, 0, 11, 5) ================================================ FILE: data/common/glyphs/mapping_controller.txt ================================================ # -------------------------------------------------- # Controller button icons # -------------------------------------------------- "\\{controller rstick}" 0 I manual_sprite("controller.png", 0, 0, 16, 16) translate(y=1) "\\{controller rstick up}" 0 I manual_sprite("controller.png", 16, 0, 16, 16) translate(y=1) "\\{controller rstick right}" 0 I manual_sprite("controller.png", 32, 0, 16, 16) translate(y=1) "\\{controller rstick down}" 0 I manual_sprite("controller.png", 48, 0, 16, 16) translate(y=1) "\\{controller rstick left}" 0 I manual_sprite("controller.png", 64, 0, 16, 16) translate(y=1) "\\{controller lstick}" 0 I manual_sprite("controller.png", 0, 16, 16, 16) translate(y=1) "\\{controller lstick up}" 0 I manual_sprite("controller.png", 16, 16, 16, 16) translate(y=1) "\\{controller lstick right}" 0 I manual_sprite("controller.png", 32, 16, 16, 16) translate(y=1) "\\{controller lstick down}" 0 I manual_sprite("controller.png", 48, 16, 16, 16) translate(y=1) "\\{controller lstick left}" 0 I manual_sprite("controller.png", 64, 16, 16, 16) translate(y=1) "\\{controller dpad up}" 0 I manual_sprite("controller.png", 0, 32, 15, 15) translate(y=1) "\\{controller dpad right}" 0 I manual_sprite("controller.png", 16, 32, 15, 15) translate(y=1) "\\{controller dpad down}" 0 I manual_sprite("controller.png", 32, 32, 15, 15) translate(y=1) "\\{controller dpad left}" 0 I manual_sprite("controller.png", 48, 32, 15, 15) translate(y=1) "\\{controller button l1}" 0 I manual_sprite("controller.png", 80, 0, 15, 15) translate(y=4) "\\{controller button r1}" 0 I manual_sprite("controller.png", 96, 0, 15, 15) translate(y=4) "\\{controller button l2}" 0 I manual_sprite("controller.png", 80, 16, 15, 15) translate(y=4) "\\{controller button r2}" 0 I manual_sprite("controller.png", 96, 16, 15, 15) translate(y=4) "\\{controller bumper left}" 0 I manual_sprite("controller.png", 80, 32, 16, 15) translate(y=4) "\\{controller bumper right}" 0 I manual_sprite("controller.png", 96, 32, 16, 15) translate(y=4) "\\{controller button zl}" 0 I manual_sprite("controller.png", 80, 48, 15, 15) translate(y=4) "\\{controller button zr}" 0 I manual_sprite("controller.png", 96, 48, 15, 15) translate(y=4) "\\{controller trigger left}" 0 I manual_sprite("controller.png", 80, 64, 15, 15) translate(y=1, x=2) "\\{controller trigger right}" 0 I manual_sprite("controller.png", 96, 64, 15, 15) translate(y=2, x=2) "\\{controller button a}" 0 I manual_sprite("controller.png", 0, 48, 15, 15) translate(y=2, x=1) "\\{controller button b}" 0 I manual_sprite("controller.png", 16, 48, 15, 15) translate(y=2, x=1) "\\{controller button x}" 0 I manual_sprite("controller.png", 32, 48, 15, 15) translate(y=2, x=1) "\\{controller button y}" 0 I manual_sprite("controller.png", 48, 48, 15, 15) translate(y=2, x=1) "\\{controller button xbox}" 0 I manual_sprite("controller.png", 64, 48, 15, 15) translate(y=2, x=1) "\\{controller button triangle}" 0 I manual_sprite("controller.png", 0, 64, 15, 15) translate(y=2, x=1) "\\{controller button square}" 0 I manual_sprite("controller.png", 16, 64, 15, 15) translate(y=2, x=1) "\\{controller button cross}" 0 I manual_sprite("controller.png", 32, 64, 15, 15) translate(y=2, x=1) "\\{controller button circle}" 0 I manual_sprite("controller.png", 48, 64, 15, 15) translate(y=2, x=1) "\\{controller button ps}" 0 I manual_sprite("controller.png", 64, 64, 15, 15) translate(y=2, x=1) "\\{controller button capture}" 0 I manual_sprite("controller.png", 0, 80, 64, 15) translate(y=2) "\\{controller button touchpad}" 0 I manual_sprite("controller.png", 0, 96, 64, 15) translate(y=2) "\\{controller button paddle 1}" 0 I manual_sprite("controller.png", 0, 112, 64, 15) translate(y=2) "\\{controller button paddle 2}" 0 I manual_sprite("controller.png", 0, 128, 64, 15) translate(y=2) "\\{controller button paddle 3}" 0 I manual_sprite("controller.png", 0, 144, 64, 15) translate(y=2) "\\{controller button paddle 4}" 0 I manual_sprite("controller.png", 0, 160, 64, 15) translate(y=2) "\\{controller button share}" 0 I manual_sprite("controller.png", 64, 80, 48, 15) translate(y=2) "\\{controller button back}" 0 I manual_sprite("controller.png", 64, 96, 48, 15) translate(y=2) "\\{controller button start}" 0 I manual_sprite("controller.png", 64, 112, 48, 15) translate(y=2) "\\{controller button mic}" 0 I manual_sprite("controller.png", 64, 128, 32, 15) translate(y=2) "\\{controller button home}" 0 I manual_sprite("controller.png", 64, 144, 48, 15) translate(y=2) "\\{controller button options}" 0 I manual_sprite("controller.png", 64, 160, 48, 15) translate(y=2) ================================================ FILE: data/common/glyphs/mapping_cyrillic.txt ================================================ # -------------------------------------------------- # Unicode Block "Cyrillic" (U+0400 to U+04FF) # -------------------------------------------------- U+0400:Ѐ 0 C combine(U+0415:Е, "\\{grave accent}") U+0401:Ё 0 C combine(U+0415:Е, "\\{umlaut}") U+0402:Ђ 0 T manual_sprite("glyphs.png", 73, 167, 15, 18) U+0403:Ѓ 0 C combine(U+0413:Г, "\\{acute accent}") U+0404:Є 0 T manual_sprite("glyphs.png", 88, 167, 13, 18) U+0405:Ѕ 0 T link("S") U+0406:І 0 T link("I") U+0407:Ї 0 C combine(U+0406:І, "\\{umlaut}") U+0408:Ј 0 T link("J") U+0409:Љ 0 T manual_sprite("glyphs.png", 101, 167, 18, 18) U+040A:Њ 0 T manual_sprite("glyphs.png", 119, 167, 18, 18) U+040B:Ћ 0 T manual_sprite("glyphs.png", 137, 167, 16, 18) U+040C:Ќ 0 C combine(U+041A:К, "\\{acute accent}") U+040D:Ѝ 0 C combine(U+0418:И, "\\{grave accent}") U+040E:Ў 0 C combine(U+0423:У, "\\{breve}") U+040F:Џ 0 T manual_sprite("glyphs.png", 153, 167, 14, 18) U+0410:А 0 T link("A") U+0411:Б 0 T manual_sprite("glyphs.png", 167, 167, 13, 18) U+0412:В 0 T link("B") U+0413:Г 0 T link(U+0393:Γ) U+0414:Д 0 T manual_sprite("glyphs.png", 180, 167, 16, 18) U+0415:Е 0 T link("E") U+0416:Ж 0 T manual_sprite("glyphs.png", 196, 167, 18, 18) U+0417:З 0 T manual_sprite("glyphs.png", 214, 167, 12, 18) U+0418:И 0 T manual_sprite("glyphs.png", 226, 167, 15, 18) U+0419:Й 0 C combine(U+0418:И, "\\{breve}") U+041A:К 0 T link("K") U+041B:Л 0 T manual_sprite("glyphs.png", 241, 167, 15, 18) U+041C:М 0 T link("M") U+041D:Н 0 T link("H") U+041E:О 0 T link("O") U+041F:П 0 T link(U+03A0:Π) U+0420:Р 0 T link("P") U+0421:С 0 T link("C") U+0422:Т 0 T link("T") U+0423:У 0 T manual_sprite("glyphs.png", 0, 185, 14, 18) U+0424:Ф 0 T link(U+03A6:Φ) U+0425:Х 0 T link("X") U+0426:Ц 0 T manual_sprite("glyphs.png", 14, 185, 15, 18) U+0427:Ч 0 T manual_sprite("glyphs.png", 29, 185, 15, 18) U+0428:Ш 0 T manual_sprite("glyphs.png", 44, 185, 18, 18) U+0429:Щ 0 T manual_sprite("glyphs.png", 62, 185, 18, 18) U+042A:Ъ 0 T manual_sprite("glyphs.png", 80, 185, 15, 18) U+042B:Ы 0 T manual_sprite("glyphs.png", 95, 185, 18, 18) U+042C:Ь 0 T manual_sprite("glyphs.png", 113, 185, 13, 18) U+042D:Э 0 T manual_sprite("glyphs.png", 126, 185, 13, 18) U+042E:Ю 0 T manual_sprite("glyphs.png", 139, 185, 18, 18) U+042F:Я 0 T manual_sprite("glyphs.png", 157, 185, 13, 18) U+0430:а 0 T link("a") U+0431:б 0 T manual_sprite("glyphs.png", 170, 185, 11, 18) U+0432:в 0 T manual_sprite("glyphs.png", 181, 185, 11, 18) U+0433:г 0 T manual_sprite("glyphs.png", 192, 185, 10, 18) U+0434:д 0 T manual_sprite("glyphs.png", 202, 185, 12, 18) U+0435:е 0 T link("e") U+0436:ж 0 T manual_sprite("glyphs.png", 214, 185, 17, 18) U+0437:з 0 T manual_sprite("glyphs.png", 232, 185, 11, 18) U+0438:и 0 T manual_sprite("glyphs.png", 243, 185, 12, 18) U+0439:й 0 C combine(U+0438:и, "\\{breve}") U+043A:к 0 T link(U+0138:ĸ) U+043B:л 0 T manual_sprite("glyphs.png", 0, 203, 11, 18) U+043C:м 0 T manual_sprite("glyphs.png", 11, 203, 13, 18) U+043D:н 0 T manual_sprite("glyphs.png", 24, 203, 11, 18) U+043E:о 0 T link("o") U+043F:п 0 T manual_sprite("glyphs.png", 35, 203, 11, 18) U+0440:р 0 T link("p") U+0441:с 0 T link("c") U+0442:т 0 T manual_sprite("glyphs.png", 46, 203, 10, 18) U+0443:у 0 T link("y") U+0444:ф 0 T manual_sprite("glyphs.png", 56, 203, 14, 18) U+0445:х 0 T link("x") U+0446:ц 0 T manual_sprite("glyphs.png", 70, 203, 12, 18) U+0447:ч 0 T manual_sprite("glyphs.png", 82, 203, 11, 18) U+0448:ш 0 T manual_sprite("glyphs.png", 93, 203, 14, 18) U+0449:щ 0 T manual_sprite("glyphs.png", 107, 203, 15, 18) U+044A:ъ 0 T manual_sprite("glyphs.png", 122, 203, 13, 18) U+044B:ы 0 T manual_sprite("glyphs.png", 135, 203, 14, 18) U+044C:ь 0 T manual_sprite("glyphs.png", 149, 203, 11, 18) U+044D:э 0 T manual_sprite("glyphs.png", 160, 203, 11, 18) U+044E:ю 0 T manual_sprite("glyphs.png", 171, 203, 14, 18) U+044F:я 0 T manual_sprite("glyphs.png", 185, 203, 11, 18) U+0450:ѐ 0 C combine("e", "\\{grave accent}") U+0451:ё 0 C combine("e", "\\{umlaut}") U+0452:ђ 0 T manual_sprite("glyphs.png", 196, 203, 12, 18) U+0453:ѓ 0 C combine(U+0433:г, "\\{acute accent}") U+0454:є 0 T manual_sprite("glyphs.png", 208, 203, 11, 18) U+0455:ѕ 0 T link("s") U+0456:і 0 T link("i") U+0457:ї 0 C combine(U+0131:ı, "\\{umlaut}") U+0458:ј 0 T link("j") U+0459:љ 0 T manual_sprite("glyphs.png", 219, 203, 16, 18) U+045A:њ 0 T manual_sprite("glyphs.png", 235, 203, 16, 18) U+045B:ћ 0 T link(U+0127:ħ) U+045C:ќ 0 C combine(U+043A:к, "\\{acute accent}") U+045D:ѝ 0 C combine(U+0438:и, "\\{grave accent}") U+045E:ў 0 C combine(U+0443:у, "\\{breve}") U+045F:џ 0 T manual_sprite("glyphs.png", 0, 221, 11, 18) U+0490:Ґ 0 T manual_sprite("glyphs.png", 11, 221, 13, 18) U+0491:ґ 0 T manual_sprite("glyphs.png", 24, 221, 10, 18) ================================================ FILE: data/common/glyphs/mapping_greek_and_coptic.txt ================================================ # -------------------------------------------------- # Unicode Block "Greek and Coptic" (U+0370 to U+03FF) # -------------------------------------------------- U+0393:Γ 0 T manual_sprite("glyphs.png", 194, 131, 13, 18) U+0394:Δ 0 T manual_sprite("glyphs.png", 207, 131, 15, 18) U+0395:Ε 0 T link("E") U+0396:Ζ 0 T link("Z") U+0397:Η 0 T link("H") U+0398:Θ 0 T manual_sprite("glyphs.png", 222, 131, 14, 18) U+0399:Ι 0 T link("I") U+039A:Κ 0 T link("K") U+039B:Λ 0 T manual_sprite("glyphs.png", 236, 131, 17, 18) U+039C:Μ 0 T link("M") U+039D:Ν 0 T link("N") U+039E:Ξ 0 T manual_sprite("glyphs.png", 0, 149, 14, 18) U+039F:Ο 0 T link("O") U+03A0:Π 0 T manual_sprite("glyphs.png", 14, 149, 15, 18) U+03A1:Ρ 0 T link("P") U+03A3:Σ 0 T manual_sprite("glyphs.png", 29, 149, 13, 18) U+03A4:Τ 0 T link("T") U+03A5:Υ 0 T link("Y") U+03A6:Φ 0 T manual_sprite("glyphs.png", 42, 149, 16, 18) U+03A7:Χ 0 T link("X") U+03A8:Ψ 0 T manual_sprite("glyphs.png", 58, 149, 18, 18) U+03A9:Ω 0 T manual_sprite("glyphs.png", 76, 149, 14, 18) U+03B1:α 0 T manual_sprite("glyphs.png", 90, 149, 12, 18) U+03B2:β 0 T manual_sprite("glyphs.png", 102, 149, 11, 18) U+03B3:γ 0 T manual_sprite("glyphs.png", 113, 149, 12, 18) U+03B4:δ 0 T manual_sprite("glyphs.png", 125, 149, 11, 18) U+03B5:ε 0 T manual_sprite("glyphs.png", 136, 149, 11, 18) U+03B6:ζ 0 T manual_sprite("glyphs.png", 147, 149, 11, 18) U+03B7:η 0 T manual_sprite("glyphs.png", 158, 149, 11, 18) U+03B8:θ 0 T manual_sprite("glyphs.png", 169, 149, 11, 18) U+03B9:ι 0 T link(U+0131:ı) U+03BA:κ 0 T link(U+0138:ĸ) U+03BB:λ 0 T manual_sprite("glyphs.png", 180, 149, 12, 18) U+03BC:μ 0 T link(U+00B5:µ) U+03BD:ν 0 T link("v") U+03BE:ξ 0 T manual_sprite("glyphs.png", 192, 149, 11, 18) U+03BF:ο 0 T link("o") U+03C0:π 0 T manual_sprite("glyphs.png", 203, 149, 13, 18) U+03C1:ρ 0 T manual_sprite("glyphs.png", 216, 149, 11, 18) U+03C2:ς 0 T manual_sprite("glyphs.png", 227, 149, 11, 18) U+03C3:σ 0 T manual_sprite("glyphs.png", 238, 149, 12, 18) U+03C4:τ 0 T manual_sprite("glyphs.png", 0, 167, 10, 18) U+03C5:υ 0 T manual_sprite("glyphs.png", 10, 167, 11, 18) U+03C6:φ 0 T manual_sprite("glyphs.png", 21, 167, 14, 18) U+03C7:χ 0 T manual_sprite("glyphs.png", 35, 167, 11, 18) U+03C8:ψ 0 T manual_sprite("glyphs.png", 46, 167, 14, 18) U+03C9:ω 0 T manual_sprite("glyphs.png", 60, 167, 13, 18) U+0386:Ά 0 C combine(U+0391:Α, "\\{acute accent}", offset_x=-4, offset_y=2) U+0388:Έ 0 C combine(U+0395:Ε, "\\{acute accent}", offset_x=-4, offset_y=2) U+0389:Ή 0 C combine(U+0397:Η, "\\{acute accent}", offset_x=-4, offset_y=2) U+038A:Ί 0 C combine(U+0399:Ι, "\\{acute accent}", offset_x=-4, offset_y=2) U+038C:Ό 0 C combine(U+039F:Ο, "\\{acute accent}", offset_x=-4, offset_y=2) U+038E:Ύ 0 C combine(U+03A5:Υ, "\\{acute accent}", offset_x=-4, offset_y=2) U+038F:Ώ 0 C combine(U+03A9:Ω, "\\{acute accent}", offset_x=-4, offset_y=2) U+0390:ΐ 0 C combine(U+03B9:ι, "\\{acute umlaut}") U+0391:Α 0 T link("A") U+0392:Β 0 T link("B") U+03AA:Ϊ 0 C combine(U+0399:Ι, "\\{umlaut}") U+03AB:Ϋ 0 C combine(U+03A5:Υ, "\\{umlaut}") U+03AC:ά 0 C combine(U+03B1:α, "\\{acute accent}") U+03AD:έ 0 C combine(U+03B5:ε, "\\{acute accent}") U+03AE:ή 0 C combine(U+03B7:η, "\\{acute accent}") U+03AF:ί 0 C combine(U+03B9:ι, "\\{acute accent}") U+03B0:ΰ 0 C combine(U+03C5:υ, "\\{acute umlaut}") U+03CA:ϊ 0 C combine(U+03B9:ι, "\\{umlaut}") U+03CB:ϋ 0 C combine(U+03C5:υ, "\\{umlaut}") U+03CC:ό 0 C combine(U+03BF:ο, "\\{acute accent}") U+03CD:ύ 0 C combine(U+03C5:υ, "\\{acute accent}") U+03CE:ώ 0 C combine(U+03C9:ω, "\\{acute accent}") ================================================ FILE: data/common/glyphs/mapping_icons.txt ================================================ # -------------------------------------------------- # Icons # -------------------------------------------------- "\\{button down}" 0 I grid_sprite("buttons.png", 0, 1, index=106) "\\{button up}" 0 I grid_sprite("buttons.png", 1, 1, index=107) "\\{button left}" 0 I grid_sprite("buttons.png", 2, 1, index=108) "\\{button right}" 0 I grid_sprite("buttons.png", 3, 1, index=109) "\\{button triangle}" 0 I grid_sprite("buttons.png", 0, 2, index=93) "\\{button circle}" 0 I grid_sprite("buttons.png", 1, 2, index=94) "\\{button x}" 0 I grid_sprite("buttons.png", 2, 2, index=95) "\\{button square}" 0 I grid_sprite("buttons.png", 3, 2, index=96) "\\{button empty}" 0 I grid_sprite("buttons.png", 0, 3, index=92) "\\{button l1}" 0 I grid_sprite("buttons.png", 1, 3, index=97) "\\{button r1}" 0 I grid_sprite("buttons.png", 2, 3, index=98) "\\{button l2}" 0 I grid_sprite("buttons.png", 3, 3, index=99) "\\{button r2}" 0 I grid_sprite("buttons.png", 4, 3, index=100) "\\{icon sound}" 0 I grid_sprite("buttons.png", 8, 3, index=101) "\\{icon music}" 0 I grid_sprite("buttons.png", 9, 3, index=102) "\\{ammo shotgun}" 0 I grid_sprite("buttons.png", 7, 1, index=103) "\\{ammo magnums}" 0 I grid_sprite("buttons.png", 8, 1, index=104) "\\{ammo uzis}" 0 I grid_sprite("buttons.png", 9, 1, index=105) "\\{arrow up}" 0 I grid_sprite("buttons.png", 8, 2, index=80) "\\{arrow down}" 0 I grid_sprite("buttons.png", 9, 2, index=81) translate(y=-2) "\\{review}" 0 R grid_sprite("buttons.png", 7, 2) ================================================ FILE: data/common/glyphs/mapping_keyboard.txt ================================================ # -------------------------------------------------- # Keyboard button icons # -------------------------------------------------- "\\{keyboard backspace}" 0 I manual_sprite("keyboard.png", 0, 0, 60, 14) translate(y=2) "\\{keyboard scroll_lock}" 0 I manual_sprite("keyboard.png", 60, 0, 60, 14) translate(y=2) "\\{keyboard return}" 0 I manual_sprite("keyboard.png", 120, 0, 50, 14) translate(y=2) "\\{keyboard caps_lock}" 0 I manual_sprite("keyboard.png", 170, 0, 50, 14) translate(y=2) "\\{keyboard print_screen}" 0 I manual_sprite("keyboard.png", 220, 0, 50, 14) translate(y=2) "\\{keyboard insert}" 0 I manual_sprite("keyboard.png", 270, 0, 50, 14) translate(y=2) "\\{keyboard num_lock}" 0 I manual_sprite("keyboard.png", 320, 0, 50, 14) translate(y=2) "\\{keyboard l_ctrl}" 0 I manual_sprite("keyboard.png", 370, 0, 32, 14) translate(y=2) "\\{keyboard r_ctrl}" 0 I manual_sprite("keyboard.png", 402, 0, 32, 14) translate(y=2) "\\{keyboard r_shift}" 0 I manual_sprite("keyboard.png", 434, 0, 32, 14) translate(y=2) "\\{keyboard l_shift}" 0 I manual_sprite("keyboard.png", 466, 0, 32, 14) translate(y=2) "\\{keyboard r_alt}" 0 I manual_sprite("keyboard.png", 0, 14, 32, 14) translate(y=2) "\\{keyboard l_alt}" 0 I manual_sprite("keyboard.png", 32, 14, 32, 14) translate(y=2) "\\{keyboard l_win}" 0 I manual_sprite("keyboard.png", 64, 14, 32, 14) translate(y=2) "\\{keyboard r_win}" 0 I manual_sprite("keyboard.png", 96, 14, 32, 14) translate(y=2) "\\{keyboard escape}" 0 I manual_sprite("keyboard.png", 128, 14, 32, 14) translate(y=2) "\\{keyboard tab}" 0 I manual_sprite("keyboard.png", 160, 14, 32, 14) translate(y=2) "\\{keyboard space}" 0 I manual_sprite("keyboard.png", 192, 14, 32, 14) translate(y=2) "\\{keyboard pause}" 0 I manual_sprite("keyboard.png", 224, 14, 32, 14) translate(y=2) "\\{keyboard home}" 0 I manual_sprite("keyboard.png", 256, 14, 32, 14) translate(y=2) "\\{keyboard page_up}" 0 I manual_sprite("keyboard.png", 288, 14, 32, 14) translate(y=2) "\\{keyboard delete}" 0 I manual_sprite("keyboard.png", 320, 14, 32, 14) translate(y=2) "\\{keyboard end}" 0 I manual_sprite("keyboard.png", 352, 14, 32, 14) translate(y=2) "\\{keyboard page_down}" 0 I manual_sprite("keyboard.png", 384, 14, 32, 14) translate(y=2) "\\{keyboard f10}" 0 I manual_sprite("keyboard.png", 416, 14, 32, 14) translate(y=2) "\\{keyboard f11}" 0 I manual_sprite("keyboard.png", 448, 14, 32, 14) translate(y=2) "\\{keyboard f12}" 0 I manual_sprite("keyboard.png", 0, 28, 32, 14) translate(y=2) "\\{keyboard f13}" 0 I manual_sprite("keyboard.png", 32, 28, 32, 14) translate(y=2) "\\{keyboard f14}" 0 I manual_sprite("keyboard.png", 64, 28, 32, 14) translate(y=2) "\\{keyboard f15}" 0 I manual_sprite("keyboard.png", 96, 28, 32, 14) translate(y=2) "\\{keyboard f16}" 0 I manual_sprite("keyboard.png", 128, 28, 32, 14) translate(y=2) "\\{keyboard f17}" 0 I manual_sprite("keyboard.png", 160, 28, 32, 14) translate(y=2) "\\{keyboard f18}" 0 I manual_sprite("keyboard.png", 192, 28, 32, 14) translate(y=2) "\\{keyboard f19}" 0 I manual_sprite("keyboard.png", 224, 28, 32, 14) translate(y=2) "\\{keyboard f20}" 0 I manual_sprite("keyboard.png", 256, 28, 32, 14) translate(y=2) "\\{keyboard f21}" 0 I manual_sprite("keyboard.png", 288, 28, 32, 14) translate(y=2) "\\{keyboard f22}" 0 I manual_sprite("keyboard.png", 320, 28, 32, 14) translate(y=2) "\\{keyboard f23}" 0 I manual_sprite("keyboard.png", 352, 28, 32, 14) translate(y=2) "\\{keyboard f24}" 0 I manual_sprite("keyboard.png", 384, 28, 32, 14) translate(y=2) "\\{keyboard num_0}" 0 I manual_sprite("keyboard.png", 416, 28, 32, 14) translate(y=2) "\\{keyboard num_1}" 0 I manual_sprite("keyboard.png", 448, 28, 32, 14) translate(y=2) "\\{keyboard num_2}" 0 I manual_sprite("keyboard.png", 0, 42, 32, 14) translate(y=2) "\\{keyboard num_3}" 0 I manual_sprite("keyboard.png", 32, 42, 32, 14) translate(y=2) "\\{keyboard num_4}" 0 I manual_sprite("keyboard.png", 64, 42, 32, 14) translate(y=2) "\\{keyboard num_5}" 0 I manual_sprite("keyboard.png", 96, 42, 32, 14) translate(y=2) "\\{keyboard num_6}" 0 I manual_sprite("keyboard.png", 128, 42, 32, 14) translate(y=2) "\\{keyboard num_7}" 0 I manual_sprite("keyboard.png", 160, 42, 32, 14) translate(y=2) "\\{keyboard num_8}" 0 I manual_sprite("keyboard.png", 192, 42, 32, 14) translate(y=2) "\\{keyboard num_9}" 0 I manual_sprite("keyboard.png", 224, 42, 32, 14) translate(y=2) "\\{keyboard num_period}" 0 I manual_sprite("keyboard.png", 256, 42, 32, 14) translate(y=2) "\\{keyboard num_divide}" 0 I manual_sprite("keyboard.png", 288, 42, 32, 14) translate(y=2) "\\{keyboard num_multiply}" 0 I manual_sprite("keyboard.png", 320, 42, 32, 14) translate(y=2) "\\{keyboard num_minus}" 0 I manual_sprite("keyboard.png", 352, 42, 32, 14) translate(y=2) "\\{keyboard num_plus}" 0 I manual_sprite("keyboard.png", 384, 42, 32, 14) translate(y=2) "\\{keyboard num_equals}" 0 I manual_sprite("keyboard.png", 416, 42, 32, 14) translate(y=2) "\\{keyboard num_comma}" 0 I manual_sprite("keyboard.png", 448, 42, 32, 14) translate(y=2) "\\{keyboard num_enter}" 0 I manual_sprite("keyboard.png", 0, 56, 32, 14) translate(y=2) "\\{keyboard unknown}" 0 I manual_sprite("keyboard.png", 32, 56, 32, 14) translate(y=2) "\\{keyboard f1}" 0 I manual_sprite("keyboard.png", 64, 56, 21, 14) translate(y=2) "\\{keyboard f2}" 0 I manual_sprite("keyboard.png", 85, 56, 21, 14) translate(y=2) "\\{keyboard f3}" 0 I manual_sprite("keyboard.png", 106, 56, 21, 14) translate(y=2) "\\{keyboard f4}" 0 I manual_sprite("keyboard.png", 127, 56, 21, 14) translate(y=2) "\\{keyboard f5}" 0 I manual_sprite("keyboard.png", 148, 56, 21, 14) translate(y=2) "\\{keyboard f6}" 0 I manual_sprite("keyboard.png", 169, 56, 21, 14) translate(y=2) "\\{keyboard f7}" 0 I manual_sprite("keyboard.png", 190, 56, 21, 14) translate(y=2) "\\{keyboard f8}" 0 I manual_sprite("keyboard.png", 211, 56, 21, 14) translate(y=2) "\\{keyboard f9}" 0 I manual_sprite("keyboard.png", 232, 56, 21, 14) translate(y=2) "\\{keyboard left}" 0 I manual_sprite("keyboard.png", 480, 14, 15, 14) translate(y=2) "\\{keyboard up}" 0 I manual_sprite("keyboard.png", 480, 28, 15, 14) translate(y=2) "\\{keyboard right}" 0 I manual_sprite("keyboard.png", 480, 42, 15, 14) translate(y=2) "\\{keyboard down}" 0 I manual_sprite("keyboard.png", 253, 56, 15, 14) translate(y=2) "\\{keyboard a}" 0 I manual_sprite("keyboard.png", 268, 56, 15, 14) translate(y=2) "\\{keyboard b}" 0 I manual_sprite("keyboard.png", 283, 56, 15, 14) translate(y=2) "\\{keyboard c}" 0 I manual_sprite("keyboard.png", 298, 56, 15, 14) translate(y=2) "\\{keyboard d}" 0 I manual_sprite("keyboard.png", 313, 56, 15, 14) translate(y=2) "\\{keyboard e}" 0 I manual_sprite("keyboard.png", 328, 56, 15, 14) translate(y=2) "\\{keyboard f}" 0 I manual_sprite("keyboard.png", 343, 56, 15, 14) translate(y=2) "\\{keyboard g}" 0 I manual_sprite("keyboard.png", 358, 56, 15, 14) translate(y=2) "\\{keyboard h}" 0 I manual_sprite("keyboard.png", 373, 56, 15, 14) translate(y=2) "\\{keyboard i}" 0 I manual_sprite("keyboard.png", 388, 56, 15, 14) translate(y=2) "\\{keyboard j}" 0 I manual_sprite("keyboard.png", 403, 56, 15, 14) translate(y=2) "\\{keyboard k}" 0 I manual_sprite("keyboard.png", 418, 56, 15, 14) translate(y=2) "\\{keyboard l}" 0 I manual_sprite("keyboard.png", 433, 56, 15, 14) translate(y=2) "\\{keyboard m}" 0 I manual_sprite("keyboard.png", 448, 56, 15, 14) translate(y=2) "\\{keyboard n}" 0 I manual_sprite("keyboard.png", 463, 56, 15, 14) translate(y=2) "\\{keyboard o}" 0 I manual_sprite("keyboard.png", 478, 56, 15, 14) translate(y=2) "\\{keyboard p}" 0 I manual_sprite("keyboard.png", 0, 70, 15, 14) translate(y=2) "\\{keyboard q}" 0 I manual_sprite("keyboard.png", 15, 70, 15, 14) translate(y=2) "\\{keyboard r}" 0 I manual_sprite("keyboard.png", 30, 70, 15, 14) translate(y=2) "\\{keyboard s}" 0 I manual_sprite("keyboard.png", 45, 70, 15, 14) translate(y=2) "\\{keyboard t}" 0 I manual_sprite("keyboard.png", 60, 70, 15, 14) translate(y=2) "\\{keyboard u}" 0 I manual_sprite("keyboard.png", 75, 70, 15, 14) translate(y=2) "\\{keyboard v}" 0 I manual_sprite("keyboard.png", 90, 70, 15, 14) translate(y=2) "\\{keyboard w}" 0 I manual_sprite("keyboard.png", 105, 70, 15, 14) translate(y=2) "\\{keyboard x}" 0 I manual_sprite("keyboard.png", 120, 70, 15, 14) translate(y=2) "\\{keyboard y}" 0 I manual_sprite("keyboard.png", 135, 70, 15, 14) translate(y=2) "\\{keyboard z}" 0 I manual_sprite("keyboard.png", 150, 70, 15, 14) translate(y=2) "\\{keyboard 0}" 0 I manual_sprite("keyboard.png", 165, 70, 15, 14) translate(y=2) "\\{keyboard 1}" 0 I manual_sprite("keyboard.png", 180, 70, 15, 14) translate(y=2) "\\{keyboard 2}" 0 I manual_sprite("keyboard.png", 195, 70, 15, 14) translate(y=2) "\\{keyboard 3}" 0 I manual_sprite("keyboard.png", 210, 70, 15, 14) translate(y=2) "\\{keyboard 4}" 0 I manual_sprite("keyboard.png", 225, 70, 15, 14) translate(y=2) "\\{keyboard 5}" 0 I manual_sprite("keyboard.png", 240, 70, 15, 14) translate(y=2) "\\{keyboard 6}" 0 I manual_sprite("keyboard.png", 255, 70, 15, 14) translate(y=2) "\\{keyboard 7}" 0 I manual_sprite("keyboard.png", 270, 70, 15, 14) translate(y=2) "\\{keyboard 8}" 0 I manual_sprite("keyboard.png", 285, 70, 15, 14) translate(y=2) "\\{keyboard 9}" 0 I manual_sprite("keyboard.png", 300, 70, 15, 14) translate(y=2) "\\{keyboard minus}" 0 I manual_sprite("keyboard.png", 315, 70, 15, 14) translate(y=2) "\\{keyboard equals}" 0 I manual_sprite("keyboard.png", 330, 70, 15, 14) translate(y=2) "\\{keyboard left_square_bracket}" 0 I manual_sprite("keyboard.png", 345, 70, 15, 14) translate(y=2) "\\{keyboard right_square_bracket}" 0 I manual_sprite("keyboard.png", 360, 70, 15, 14) translate(y=2) "\\{keyboard backslash}" 0 I manual_sprite("keyboard.png", 375, 70, 15, 14) translate(y=2) "\\{keyboard hash}" 0 I manual_sprite("keyboard.png", 390, 70, 15, 14) translate(y=2) "\\{keyboard semicolon}" 0 I manual_sprite("keyboard.png", 405, 70, 15, 14) translate(y=2) "\\{keyboard apostrophe}" 0 I manual_sprite("keyboard.png", 420, 70, 15, 14) translate(y=2) "\\{keyboard backtick}" 0 I manual_sprite("keyboard.png", 435, 70, 15, 14) translate(y=2) "\\{keyboard comma}" 0 I manual_sprite("keyboard.png", 450, 70, 15, 14) translate(y=2) "\\{keyboard period}" 0 I manual_sprite("keyboard.png", 465, 70, 15, 14) translate(y=2) "\\{keyboard slash}" 0 I manual_sprite("keyboard.png", 480, 70, 15, 14) translate(y=2) ================================================ FILE: data/common/glyphs/mapping_latin-1_supplement.txt ================================================ # -------------------------------------------------- # Unicode Block "Latin-1 Supplement" (U+0080 to U+00FF) # -------------------------------------------------- U+00A1:¡ 0 T manual_sprite("glyphs.png", 32, 77, 6, 18) U+00A2:¢ 0 T manual_sprite("glyphs.png", 38, 77, 11, 18) U+00A3:£ 0 T manual_sprite("glyphs.png", 49, 77, 13, 18) U+00A4:¤ 0 T manual_sprite("glyphs.png", 62, 77, 12, 18) U+00A5:¥ 0 T manual_sprite("glyphs.png", 74, 77, 14, 18) U+00A6:¦ 0 T manual_sprite("glyphs.png", 88, 77, 6, 18) U+00A7:§ 0 T manual_sprite("glyphs.png", 94, 77, 11, 18) U+00A9:© 0 T manual_sprite("glyphs.png", 105, 77, 15, 18) U+00AA:ª 0 T manual_sprite("glyphs.png", 121, 77, 7, 18) U+00AB:« 0 T manual_sprite("glyphs.png", 128, 77, 9, 18) U+00AC:¬ 0 T manual_sprite("glyphs.png", 137, 77, 11, 18) U+00AE:® 0 T manual_sprite("glyphs.png", 148, 77, 15, 18) U+00B0:° 0 T manual_sprite("glyphs.png", 163, 77, 8, 18) U+00B1:± 0 T manual_sprite("glyphs.png", 171, 77, 11, 18) U+00B2:² 0 T manual_sprite("glyphs.png", 20, 95, 7, 9) U+00B3:³ 0 T manual_sprite("glyphs.png", 13, 104, 7, 9) U+00B5:µ 0 T manual_sprite("glyphs.png", 182, 77, 11, 18) U+00B6:¶ 0 T manual_sprite("glyphs.png", 193, 77, 15, 18) U+00B7:· 0 T manual_sprite("glyphs.png", 208, 77, 6, 18) U+00B9:¹ 0 T manual_sprite("glyphs.png", 13, 95, 7, 9) U+00BA:º 0 T manual_sprite("glyphs.png", 214, 77, 7, 18) U+00BB:» 0 T manual_sprite("glyphs.png", 221, 77, 9, 18) U+00BC:¼ 0 T manual_sprite("glyphs.png", 230, 77, 13, 18) U+00BD:½ 0 T manual_sprite("glyphs.png", 243, 77, 13, 18) U+00BE:¾ 0 T manual_sprite("glyphs.png", 0, 95, 13, 18) U+00BF:¿ 0 T manual_sprite("glyphs.png", 27, 95, 12, 18) U+00C0:À 0 C combine("A", "\\{grave accent}") U+00C1:Á 0 C combine("A", "\\{acute accent}") U+00C2: 0 C combine("A", "\\{circumflex}") U+00C3:à 0 C combine("A", "\\{tilde}") U+00C4:Ä 0 C combine("A", "\\{umlaut}") U+00C5:Å 0 C combine("A", "\\{ring above}") U+00C6:Æ 0 T manual_sprite("glyphs.png", 39, 95, 18, 18) U+00C7:Ç 0 T manual_sprite("glyphs.png", 57, 95, 13, 18) U+00C8:È 0 C combine("E", "\\{grave accent}") U+00C9:É 0 C combine("E", "\\{acute accent}") U+00CA:Ê 0 C combine("E", "\\{circumflex}") U+00CB:Ë 0 C combine("E", "\\{umlaut}") U+00CC:Ì 0 C combine("I", "\\{grave accent}") U+00CD:Í 0 C combine("I", "\\{acute accent}") U+00CE:Î 0 C combine("I", "\\{circumflex}") U+00CF:Ï 0 C combine("I", "\\{umlaut}") U+00D0:Ð 0 T manual_sprite("glyphs.png", 70, 95, 13, 18) U+00D1:Ñ 0 C combine("N", "\\{tilde}") U+00D2:Ò 0 C combine("O", "\\{grave accent}") U+00D3:Ó 0 C combine("O", "\\{acute accent}") U+00D4:Ô 0 C combine("O", "\\{circumflex}") U+00D5:Õ 0 C combine("O", "\\{tilde}") U+00D6:Ö 0 C combine("O", "\\{umlaut}") U+00D7:× 0 T manual_sprite("glyphs.png", 83, 95, 10, 18) U+00D8:Ø 0 T manual_sprite("glyphs.png", 93, 95, 14, 18) U+00D9:Ù 0 C combine("U", "\\{grave accent}") U+00DA:Ú 0 C combine("U", "\\{acute accent}") U+00DB:Û 0 C combine("U", "\\{circumflex}") U+00DC:Ü 0 C combine("U", "\\{umlaut}") U+00DD:Ý 0 C combine("Y", "\\{acute accent}") U+00DE:Þ 0 T manual_sprite("glyphs.png", 107, 95, 13, 18) U+00DF:ß 0 T manual_sprite("glyphs.png", 120, 95, 11, 18, index=74) U+00E0:à 0 C combine("a", "\\{grave accent}") U+00E1:á 0 C combine("a", "\\{acute accent}") U+00E2:â 0 C combine("a", "\\{circumflex}") U+00E3:ã 0 C combine("a", "\\{tilde}") U+00E4:ä 0 C combine("a", "\\{umlaut}") U+00E5:å 0 C combine("a", "\\{ring above}") U+00E6:æ 0 T manual_sprite("glyphs.png", 131, 95, 16, 18) U+00E7:ç 0 T manual_sprite("glyphs.png", 147, 95, 11, 18) U+00E8:è 0 C combine("e", "\\{grave accent}") U+00E9:é 0 C combine("e", "\\{acute accent}") U+00EA:ê 0 C combine("e", "\\{circumflex}") U+00EB:ë 0 C combine("e", "\\{umlaut}") U+00EC:ì 0 C combine(U+0131:ı, "\\{grave accent}") U+00ED:í 0 C combine(U+0131:ı, "\\{acute accent}") U+00EE:î 0 C combine(U+0131:ı, "\\{circumflex}") U+00EF:ï 0 C combine(U+0131:ı, "\\{umlaut}") U+00F0:ð 0 T manual_sprite("glyphs.png", 158, 95, 11, 18) U+00F1:ñ 0 C combine("n", "\\{tilde}") U+00F2:ò 0 C combine("o", "\\{grave accent}") U+00F3:ó 0 C combine("o", "\\{acute accent}") U+00F4:ô 0 C combine("o", "\\{circumflex}") U+00F5:õ 0 C combine("o", "\\{tilde}") U+00F6:ö 0 C combine("o", "\\{umlaut}") U+00F7:÷ 0 T manual_sprite("glyphs.png", 169, 95, 10, 18) U+00F8:ø 0 T manual_sprite("glyphs.png", 179, 95, 13, 18) U+00F9:ù 0 C combine("u", "\\{grave accent}") U+00FA:ú 0 C combine("u", "\\{acute accent}") U+00FB:û 0 C combine("u", "\\{circumflex}") U+00FC:ü 0 C combine("u", "\\{umlaut}") U+00FD:ý 0 C combine("y", "\\{acute accent}") U+00FE:þ 0 T manual_sprite("glyphs.png", 192, 95, 11, 18) U+00FF:ÿ 0 C combine("y", "\\{umlaut}") ================================================ FILE: data/common/glyphs/mapping_latin_extended-a.txt ================================================ # -------------------------------------------------- # Unicode Block "Latin Extended-A" (U+0100 to U+017F) # -------------------------------------------------- U+0100:Ā 0 C combine("A", "\\{macron}") U+0101:ā 0 C combine("a", "\\{macron}") U+0102:Ă 0 C combine("A", "\\{breve}") U+0103:ă 0 C combine("a", "\\{breve}") U+0104:Ą 0 T manual_sprite("glyphs.png", 203, 95, 17, 18) U+0105:ą 0 T manual_sprite("glyphs.png", 220, 95, 11, 18) U+0106:Ć 0 C combine("C", "\\{acute accent}") U+0107:ć 0 C combine("c", "\\{acute accent}") U+0108:Ĉ 0 C combine("C", "\\{circumflex}") U+0109:ĉ 0 C combine("c", "\\{circumflex}") U+010A:Ċ 0 C combine("C", "\\{dot above}") U+010B:ċ 0 C combine("c", "\\{dot above}") U+010C:Č 0 C combine("C", "\\{caron}") U+010D:č 0 C combine("c", "\\{caron}") U+010E:Ď 0 C combine("D", "\\{caron}") U+010F:ď 0 C combine("d", "\\{caron}") U+0110:Đ 0 T link(U+00D0:Ð) U+0111:đ 0 T manual_sprite("glyphs.png", 231, 95, 12, 18) U+0112:Ē 0 C combine("E", "\\{macron}") U+0113:ē 0 C combine("e", "\\{macron}") U+0114:Ĕ 0 C combine("E", "\\{breve}") U+0115:ĕ 0 C combine("e", "\\{breve}") U+0116:Ė 0 C combine("E", "\\{dot above}") U+0117:ė 0 C combine("e", "\\{dot above}") U+0118:Ę 0 T manual_sprite("glyphs.png", 243, 95, 13, 18) U+0119:ę 0 T manual_sprite("glyphs.png", 0, 113, 11, 18) U+011A:Ě 0 C combine("E", "\\{caron}") U+011B:ě 0 C combine("e", "\\{caron}") U+011C:Ĝ 0 C combine("G", "\\{circumflex}") U+011D:ĝ 0 C combine("g", "\\{circumflex}") U+011E:Ğ 0 C combine("G", "\\{breve}") U+011F:ğ 0 C combine("g", "\\{breve}") U+0120:Ġ 0 C combine("G", "\\{dot above}") U+0121:ġ 0 C combine("g", "\\{dot above}") U+0122:Ģ 0 T manual_sprite("glyphs.png", 11, 113, 13, 18) U+0123:ģ 0 T manual_sprite("glyphs.png", 24, 113, 11, 18) U+0124:Ĥ 0 C combine("H", "\\{circumflex}") U+0125:ĥ 0 C combine("h", "\\{circumflex}") U+0126:Ħ 0 T manual_sprite("glyphs.png", 35, 113, 15, 18) U+0127:ħ 0 T manual_sprite("glyphs.png", 50, 113, 12, 18) U+0128:Ĩ 0 C combine("I", "\\{tilde}") U+0129:ĩ 0 C combine("i", "\\{tilde}") U+012A:Ī 0 C combine("I", "\\{macron}") U+012B:ī 0 C combine("i", "\\{macron}") U+012C:Ĭ 0 C combine("I", "\\{breve}") U+012D:ĭ 0 C combine("i", "\\{breve}") U+012E:Į 0 T manual_sprite("glyphs.png", 62, 113, 10, 18) U+012F:į 0 T manual_sprite("glyphs.png", 72, 113, 7, 18) U+0130:İ 0 C combine("I", "\\{dot above}") U+0131:ı 0 T manual_sprite("glyphs.png", 79, 113, 7, 18) U+0134:Ĵ 0 C combine("J", "\\{circumflex}") U+0135:ĵ 0 C combine("j", "\\{circumflex}") U+0136:Ķ 0 T manual_sprite("glyphs.png", 86, 113, 14, 18) U+0137:ķ 0 T manual_sprite("glyphs.png", 100, 113, 12, 18) U+0138:ĸ 0 T manual_sprite("glyphs.png", 112, 113, 11, 18) U+0139:Ĺ 0 C combine("L", "\\{acute accent}") U+013A:ĺ 0 C combine("l", "\\{acute accent}") U+013B:Ļ 0 T manual_sprite("glyphs.png", 123, 113, 13, 18) U+013C:ļ 0 T manual_sprite("glyphs.png", 136, 113, 7, 18) U+013D:Ľ 0 C combine("L", "\\{caron}") U+013E:ľ 0 C combine("l", "\\{caron}") U+013F:Ŀ 0 C combine("L", U+00B7:·, align="middle", offset_x=3, offset_y=-1) U+0140:ŀ 0 C combine("l", U+00B7:·, align="middle", offset_x=3, offset_y=-1) U+0141:Ł 0 T manual_sprite("glyphs.png", 143, 113, 14, 18) U+0142:ł 0 T manual_sprite("glyphs.png", 157, 113, 10, 18) U+0143:Ń 0 C combine("N", "\\{acute accent}") U+0144:ń 0 C combine("n", "\\{acute accent}") U+0145:Ņ 0 T manual_sprite("glyphs.png", 167, 113, 15, 18) U+0146:ņ 0 T manual_sprite("glyphs.png", 182, 113, 12, 18) U+0147:Ň 0 C combine("N", "\\{caron}") U+0148:ň 0 C combine("n", "\\{caron}") U+0149:ʼn 0 C combine("n", "\\{acute accent}", offset_x=-2) U+014A:Ŋ 0 T manual_sprite("glyphs.png", 194, 113, 15, 18) U+014B:ŋ 0 T manual_sprite("glyphs.png", 209, 113, 12, 18) U+014C:Ō 0 C combine("O", "\\{macron}") U+014D:ō 0 C combine("o", "\\{macron}") U+014E:Ŏ 0 C combine("O", "\\{breve}") U+014F:ŏ 0 C combine("o", "\\{breve}") U+0150:Ő 0 C combine("O", "\\{double acute accent}") U+0151:ő 0 C combine("o", "\\{double acute accent}") U+0152:Œ 0 T manual_sprite("glyphs.png", 221, 113, 18, 18) U+0153:œ 0 T manual_sprite("glyphs.png", 239, 113, 16, 18) U+0154:Ŕ 0 C combine("R", "\\{acute accent}") U+0155:ŕ 0 C combine("r", "\\{acute accent}") U+0156:Ŗ 0 T manual_sprite("glyphs.png", 0, 131, 14, 18) U+0157:ŗ 0 T manual_sprite("glyphs.png", 14, 131, 10, 18) U+0158:Ř 0 C combine("R", "\\{caron}") U+0159:ř 0 C combine("r", "\\{caron}") U+015A:Ś 0 C combine("S", "\\{acute accent}") U+015B:ś 0 C combine("s", "\\{acute accent}") U+015C:Ŝ 0 C combine("S", "\\{circumflex}") U+015D:ŝ 0 C combine("s", "\\{circumflex}") U+015E:Ş 0 T manual_sprite("glyphs.png", 24, 131, 13, 18) U+015F:ş 0 T manual_sprite("glyphs.png", 37, 131, 11, 18) U+0160:Š 0 C combine("S", "\\{caron}") U+0161:š 0 C combine("s", "\\{caron}") U+0162:Ţ 0 T manual_sprite("glyphs.png", 48, 131, 14, 18) U+0163:ţ 0 T manual_sprite("glyphs.png", 62, 131, 10, 18) U+0164:Ť 0 C combine("T", "\\{caron}") U+0165:ť 0 C combine("t", "\\{caron}") U+0166:Ŧ 0 T manual_sprite("glyphs.png", 72, 131, 14, 18) U+0167:ŧ 0 T manual_sprite("glyphs.png", 86, 131, 11, 18) U+0168:Ũ 0 C combine("U", "\\{tilde}") U+0169:ũ 0 C combine("u", "\\{tilde}") U+016A:Ū 0 C combine("U", "\\{macron}") U+016B:ū 0 C combine("u", "\\{macron}") U+016C:Ŭ 0 C combine("U", "\\{breve}") U+016D:ŭ 0 C combine("u", "\\{breve}") U+016E:Ů 0 C combine("U", "\\{ring above}") U+016F:ů 0 C combine("u", "\\{ring above}") U+0170:Ű 0 C combine("U", "\\{double acute accent}") U+0171:ű 0 C combine("u", "\\{double acute accent}") U+0172:Ų 0 T manual_sprite("glyphs.png", 97, 131, 15, 18) U+0173:ų 0 T manual_sprite("glyphs.png", 112, 131, 11, 18) U+0174:Ŵ 0 C combine("W", "\\{circumflex}") U+0175:ŵ 0 C combine("w", "\\{circumflex}") U+0176:Ŷ 0 C combine("Y", "\\{circumflex}") U+0177:ŷ 0 C combine("y", "\\{circumflex}") U+0178:Ÿ 0 C combine("Y", "\\{umlaut}") U+0179:Ź 0 C combine("Z", "\\{acute accent}") U+017A:ź 0 C combine("z", "\\{acute accent}") U+017B:Ż 0 C combine("Z", "\\{dot above}") U+017C:ż 0 C combine("z", "\\{dot above}") U+017D:Ž 0 C combine("Z", "\\{caron}") U+017E:ž 0 C combine("z", "\\{caron}") ================================================ FILE: data/common/glyphs/mapping_latin_extended-b.txt ================================================ # -------------------------------------------------- # Unicode Block "Latin Extended-B" (U+0180 to U+024F) # -------------------------------------------------- U+0192:ƒ 0 T manual_sprite("glyphs.png", 123, 131, 11, 18) U+01CD:Ǎ 0 C combine("A", "\\{caron}") U+01CE:ǎ 0 C combine("a", "\\{caron}") U+01CF:Ǐ 0 C combine("I", "\\{caron}") U+01D0:ǐ 0 C combine("i", "\\{caron}") U+01D1:Ǒ 0 C combine("O", "\\{caron}") U+01D2:ǒ 0 C combine("o", "\\{caron}") U+01D3:Ǔ 0 C combine("U", "\\{caron}") U+01D4:ǔ 0 C combine("u", "\\{caron}") U+01E6:Ǧ 0 C combine("G", "\\{caron}") U+01E7:ǧ 0 C combine("g", "\\{caron}") U+01E8:Ǩ 0 C combine("K", "\\{caron}") U+01E9:ǩ 0 C combine("k", "\\{caron}") U+01F0:ǰ 0 C combine("j", "\\{caron}") U+01F4:Ǵ 0 C combine("G", "\\{acute accent}") U+01F5:ǵ 0 C combine("g", "\\{acute accent}") U+01F8:Ǹ 0 C combine("N", "\\{grave accent}") U+01F9:ǹ 0 C combine("n", "\\{grave accent}") U+021E:Ȟ 0 C combine("H", "\\{caron}") U+021F:ȟ 0 C combine("h", "\\{caron}") U+0226:Ȧ 0 C combine("A", "\\{dot above}") U+0227:ȧ 0 C combine("a", "\\{dot above}") U+022E:Ȯ 0 C combine("O", "\\{dot above}") U+022F:ȯ 0 C combine("o", "\\{dot above}") U+0232:Ȳ 0 C combine("Y", "\\{macron}") U+0233:ȳ 0 C combine("y", "\\{macron}") U+0218:Ș 0 T manual_sprite("glyphs.png", 134, 131, 13, 18) U+0219:ș 0 T manual_sprite("glyphs.png", 147, 131, 11, 18) U+021A:Ț 0 T manual_sprite("glyphs.png", 158, 131, 14, 18) U+021B:ț 0 T manual_sprite("glyphs.png", 172, 131, 11, 18) ================================================ FILE: data/common/glyphs/mapping_latin_extended_additional.txt ================================================ # -------------------------------------------------- # Unicode Block "Latin Extended Additional" (U+1E00 to U+1EFF) # -------------------------------------------------- U+1E02:Ḃ 0 C combine("B", "\\{dot above}") U+1E03:ḃ 0 C combine("b", "\\{dot above}") U+1E0A:Ḋ 0 C combine("D", "\\{dot above}") U+1E0B:ḋ 0 C combine("d", "\\{dot above}") U+1E1E:Ḟ 0 C combine("F", "\\{dot above}") U+1E1F:ḟ 0 C combine("f", "\\{dot above}") U+1E20:Ḡ 0 C combine("G", "\\{macron}") U+1E21:ḡ 0 C combine("g", "\\{macron}") U+1E22:Ḣ 0 C combine("H", "\\{dot above}") U+1E23:ḣ 0 C combine("h", "\\{dot above}") U+1E26:Ḧ 0 C combine("H", "\\{umlaut}") U+1E27:ḧ 0 C combine("h", "\\{umlaut}") U+1E30:Ḱ 0 C combine("K", "\\{acute accent}") U+1E31:ḱ 0 C combine("k", "\\{acute accent}") U+1E3E:Ḿ 0 C combine("M", "\\{acute accent}") U+1E3F:ḿ 0 C combine("m", "\\{acute accent}") U+1E40:Ṁ 0 C combine("M", "\\{dot above}") U+1E41:ṁ 0 C combine("m", "\\{dot above}") U+1E44:Ṅ 0 C combine("N", "\\{dot above}") U+1E45:ṅ 0 C combine("n", "\\{dot above}") U+1E54:Ṕ 0 C combine("P", "\\{acute accent}") U+1E55:ṕ 0 C combine("p", "\\{acute accent}") U+1E56:Ṗ 0 C combine("P", "\\{dot above}") U+1E57:ṗ 0 C combine("p", "\\{dot above}") U+1E58:Ṙ 0 C combine("R", "\\{dot above}") U+1E59:ṙ 0 C combine("r", "\\{dot above}") U+1E60:Ṡ 0 C combine("S", "\\{dot above}") U+1E61:ṡ 0 C combine("s", "\\{dot above}") U+1E6A:Ṫ 0 C combine("T", "\\{dot above}") U+1E6B:ṫ 0 C combine("t", "\\{dot above}") U+1E7C:Ṽ 0 C combine("V", "\\{tilde}") U+1E7D:ṽ 0 C combine("v", "\\{tilde}") U+1E80:Ẁ 0 C combine("W", "\\{grave accent}") U+1E81:ẁ 0 C combine("w", "\\{grave accent}") U+1E82:Ẃ 0 C combine("W", "\\{acute accent}") U+1E83:ẃ 0 C combine("w", "\\{acute accent}") U+1E84:Ẅ 0 C combine("W", "\\{umlaut}") U+1E85:ẅ 0 C combine("w", "\\{umlaut}") U+1E86:Ẇ 0 C combine("W", "\\{dot above}") U+1E87:ẇ 0 C combine("w", "\\{dot above}") U+1E8A:Ẋ 0 C combine("X", "\\{dot above}") U+1E8B:ẋ 0 C combine("x", "\\{dot above}") U+1E8C:Ẍ 0 C combine("X", "\\{umlaut}") U+1E8D:ẍ 0 C combine("x", "\\{umlaut}") U+1E8E:Ẏ 0 C combine("Y", "\\{dot above}") U+1E8F:ẏ 0 C combine("y", "\\{dot above}") U+1E90:Ẑ 0 C combine("Z", "\\{circumflex}") U+1E91:ẑ 0 C combine("z", "\\{circumflex}") U+1E97:ẗ 0 C combine("t", "\\{umlaut}") U+1E98:ẘ 0 C combine("w", "\\{ring above}") U+1E99:ẙ 0 C combine("y", "\\{ring above}") U+1EBC:Ẽ 0 C combine("E", "\\{tilde}") U+1EBD:ẽ 0 C combine("e", "\\{tilde}") U+1EF2:Ỳ 0 C combine("Y", "\\{grave accent}") U+1EF3:ỳ 0 C combine("y", "\\{grave accent}") U+1EF8:Ỹ 0 C combine("Y", "\\{tilde}") U+1EF9:ỹ 0 C combine("y", "\\{tilde}") ================================================ FILE: data/common/glyphs/mapping_misc.txt ================================================ # -------------------------------------------------- # Unicode Block "General Punctuation" (U+2000 to U+206F) # -------------------------------------------------- U+2013:– 0 T manual_sprite("glyphs.png", 35, 221, 11, 18) U+2014:— 0 T manual_sprite("glyphs.png", 46, 221, 18, 18) U+2018:‘ 0 T manual_sprite("glyphs.png", 64, 221, 6, 9) U+2019:’ 0 T manual_sprite("glyphs.png", 64, 230, 6, 9) U+201C:“ 0 T manual_sprite("glyphs.png", 70, 221, 9, 9) U+201D:” 0 T manual_sprite("glyphs.png", 70, 230, 9, 9) U+2020:† 0 T manual_sprite("glyphs.png", 80, 221, 10, 18) U+2021:‡ 0 T manual_sprite("glyphs.png", 90, 221, 10, 18) U+2022:• 0 T manual_sprite("glyphs.png", 100, 221, 8, 18) U+2026:… 0 T manual_sprite("glyphs.png", 108, 221, 12, 18) U+2030:‰ 0 T manual_sprite("glyphs.png", 120, 221, 16, 18) U+2039:‹ 0 T manual_sprite("glyphs.png", 137, 221, 7, 18) U+203A:› 0 T manual_sprite("glyphs.png", 144, 221, 7, 18) # -------------------------------------------------- # Unicode Block "Superscripts and Subscripts" (U+2070 to U+209F) # -------------------------------------------------- U+2074:⁴ 0 T manual_sprite("glyphs.png", 20, 104, 7, 9) # -------------------------------------------------- # Unicode Block "Currency Symbols" (U+20A0 to U+20CF) # -------------------------------------------------- U+20AC:€ 0 T manual_sprite("glyphs.png", 170, 221, 14, 18) U+20AF:₯ 0 T manual_sprite("glyphs.png", 151, 221, 19, 18) # -------------------------------------------------- # Unicode Block "Letterlike Symbols" (U+2100 to U+214F) # -------------------------------------------------- U+2116:№ 0 T manual_sprite("glyphs.png", 184, 221, 18, 18) U+2122:™ 0 T manual_sprite("glyphs.png", 203, 221, 18, 18) # -------------------------------------------------- # Unicode Block "Alphabetic Presentation Forms" (U+FB00 to U+FB4F) # -------------------------------------------------- U+FB01:fi 0 T manual_sprite("glyphs.png", 221, 221, 13, 18) U+FB02:fl 0 T manual_sprite("glyphs.png", 234, 221, 13, 18) ================================================ FILE: data/common/glyphs/mapping_small.txt ================================================ # -------------------------------------------------- # Small text # -------------------------------------------------- U+0030:0 1 T grid_sprite("buttons.png", 0, 0) U+0031:1 1 T grid_sprite("buttons.png", 1, 0) U+0032:2 1 T grid_sprite("buttons.png", 2, 0) U+0033:3 1 T grid_sprite("buttons.png", 3, 0) U+0034:4 1 T grid_sprite("buttons.png", 4, 0) U+0035:5 1 T grid_sprite("buttons.png", 5, 0) U+0036:6 1 T grid_sprite("buttons.png", 6, 0) U+0037:7 1 T grid_sprite("buttons.png", 7, 0) U+0038:8 1 T grid_sprite("buttons.png", 8, 0) U+0039:9 1 T grid_sprite("buttons.png", 9, 0) U+002D:- 1 T grid_sprite("buttons.png", 4, 1) U+002C:, 1 T grid_sprite("buttons.png", 5, 1) U+00B0:° 1 T grid_sprite("buttons.png", 6, 1) ================================================ FILE: data/scripting/assault_stats.lua ================================================ local raw = trxc.assault_stats trx.assault_stats = { add_record = raw.record, remove_record = raw.remove, list_records = raw.list, } ================================================ FILE: data/scripting/camera.lua ================================================ local raw = trxc.camera local getters = { pos = raw.get_pos, room_num = raw.get_room, room = function() local room_num = raw.get_room() return room_num and trx.rooms[room_num] or nil end, target_pos = raw.get_target_pos, target_room_num = raw.get_target_room, } local camera = { shake = raw.shake, reset = raw.reset, } setmetatable(camera, { __index = function(self, key) local getter = getters[key] return getter and getter() or nil end, __newindex = function(self, key, value) error("Cannot set field '" .. key .. "' on trx.camera", 2) end, }) trx.camera = camera ================================================ FILE: data/scripting/catalog.lua ================================================ local raw = trxc.catalog local catalog = { objects = raw.objects, flip_effects = raw.flip_effects, lara_states = raw.lara_states, lara_anims = raw.lara_anims, music = raw.music, samples = raw.samples, weapons = raw.weapons, } trx.catalog = catalog ================================================ FILE: data/scripting/config.lua ================================================ local raw = trxc.config local config = {} function config.get(key) return raw.get(key) end function config.set(key, value) return raw.set(key, value) end function config.list() return raw.list() end trx.config = config ================================================ FILE: data/scripting/console.lua ================================================ local raw = trxc.console local LogLevel = trxc.log.LogLevel local log = { LogLevel = LogLevel } function log.generic(level, ...) raw.log(level, ...) end function log.info(...) raw.log(LogLevel.INFO, ...) end function log.warn(...) raw.log(LogLevel.WARNING, ...) end function log.warning(...) raw.log(LogLevel.WARNING, ...) end function log.error(...) raw.log(LogLevel.ERROR, ...) end function log.debug(...) raw.log(LogLevel.DEBUG, ...) end local console = { log = log, } setmetatable(console.log, { __call = function(_, ...) return console.log.info(...) end, }) function console.clear() return raw.clear() end function console.eval(cmd, opts) return raw.eval(cmd, opts) end trx.console = console ================================================ FILE: data/scripting/creatures.lua ================================================ local raw = trxc.creatures local creatures = { add_ally = raw.add_ally, add_ally_target = raw.add_ally_target, } local getters = { hostile_allies = raw.are_allies_hostile, } local setters = { hostile_allies = raw.set_allies_hostile, } local creatures_mt = { __index = function(self, key) local getter = getters[key] return getter and getter() or nil end, __newindex = function(self, key, value) local setter = setters[key] if setter then setter(value) return end error("Cannot set field '" .. key .. "' on trx.creatures", 2) end, } setmetatable(creatures, creatures_mt) trx.creatures = creatures ================================================ FILE: data/scripting/events.lua ================================================ local raw = trxc.events local types = trxc.events.EventType local Event = {} function Event.__call(self, callback) local et = self._type return raw.attach(et, callback) end local function to_event_name(name) if string.sub(name, 1, 7) == "BEFORE_" then return string.lower(name) elseif string.sub(name, 1, 6) == "AFTER_" then return string.lower(name) end return "on_" .. string.lower(name) end local events = { EventType = types } for name, et in pairs(types) do local proxy = { _type = et } setmetatable(proxy, Event) events[to_event_name(name)] = proxy end function events.detach(id) raw.detach(id) end trx.events = events ================================================ FILE: data/scripting/game.lua ================================================ local raw = trxc.game local function make_level(table_type, i) return { num = raw.get_level_num(table_type, i), name = raw.get_level_name(table_type, i), path = raw.get_level_path(table_type, i), type = raw.get_level_type(table_type, i), } end local function make_levels(table_type) local count = raw.count_levels(table_type) local levels = {} for i = 1, count do levels[i] = make_level(table_type, i) end return levels end local table_map = { levels = raw.LevelTable.MAIN, demos = raw.LevelTable.DEMOS, cutscenes = raw.LevelTable.CUTSCENES, } -- settings system local Settings = (function() local function config_entry(path) return { get = function() return trx.config.get(path) end, set = function(value) trx.config.set(path, value) end, } end local registry = { lockout_option_ring = config_entry("flow.lockout_option_ring"), load_save_disabled = config_entry("flow.load_save_disabled"), play_any_level = config_entry("flow.play_any_level"), demo_delay = config_entry("flow.demo_delay"), cheat_keys = config_entry("flow.cheat_keys"), } return setmetatable({}, { __index = function(_, key) local r = registry[key] return r and r.get() or nil end, __newindex = function(_, key, value) local r = registry[key] if not r then error("Cannot set field '" .. key .. "' on Settings") end r.set(value) end, }) end)() local dynamic_getters = { current_level = function() return make_level(raw.get_current_level_table(), raw.get_current_level_idx()) end, version = raw.get_version, trx_version = raw.get_trx_version, } trx.game = setmetatable({ LevelTable = raw.LevelTable, LevelType = raw.LevelType, settings = Settings, play_level = raw.play_level, play_cutscene = raw.play_cutscene, play_demo = raw.play_demo, }, { __index = function(self, key) local table_type = table_map[key] if table_type then local t = make_levels(table_type) rawset(self, key, t) return t end local getter = dynamic_getters[key] return getter and getter() or nil end, __newindex = function(self, key, value) error("Cannot set field '" .. key .. "' on trx.game") end, }) ================================================ FILE: data/scripting/items.lua ================================================ local raw = trxc.items -- Item proxy metatable local getters = { pos = raw.get_pos, rot = raw.get_rot, anim = raw.get_anim, frame = raw.get_frame, room_num = raw.get_room, room = function(idx) return trx.rooms[raw.get_room(idx)] end, status = raw.get_status, flags = raw.get_flags, timer = raw.get_timer, object_id = raw.get_object_id, hit_points = raw.get_hit_points, max_hit_points = raw.get_max_hit_points, name = raw.get_name, } local setters = { pos = raw.set_pos, rot = raw.set_rot, anim = raw.set_anim, frame = raw.set_frame, hit_points = raw.set_hit_points, max_hit_points = raw.set_max_hit_points, name = raw.set_name, object_id = raw.set_object_id, } local Item = {} Item.__index = function(self, key) local getter = getters[key] return getter and getter(self.idx) or nil end Item.__newindex = function(self, key, value) local setter = setters[key] if setter then setter(self.idx, value) return end error("Cannot set field '" .. key .. "' on trx.items.Item") end -- items metatable - functions local fn = {} function fn.get(arg) local idx = raw.get(arg) if not idx then return nil end return setmetatable({ idx = idx }, Item) end local find_query_keys = { object_id = true, room_num = true, } local function validate_find_query(query, fn_name) if type(query) ~= "table" then error("trx.items." .. fn_name .. " query must be a table", 2) end for key, _ in pairs(query) do if not find_query_keys[key] then trx.log.warn("trx.items." .. fn_name .. ": unknown property '" .. tostring(key) .. "'") end end end local function is_matching(item, query) if query.object_id ~= nil and item.object_id ~= query.object_id then return false end if query.room_num ~= nil and item.room_num ~= query.room_num then return false end return true end local function find_items(query, first_only) local matches = first_only and nil or {} local count = raw.count() for i = 1, count do local item = setmetatable({ idx = i }, Item) if is_matching(item, query) then if first_only then return item end table.insert(matches, item) end end return first_only and nil or matches end function fn.find(query) if query == nil then return {} end validate_find_query(query, "find") return find_items(query, false) end function fn.first(query) if query == nil then return nil end validate_find_query(query, "first") return find_items(query, true) end -- items metatable - metamethods trx.items = setmetatable({}, { Item = Item, __len = function() return raw.count() end, __index = function(_, key) if key == "fn" then return fn elseif key == "find" then return fn.find elseif key == "first" then return fn.first elseif type(key) == "number" or type(key) == "string" then return fn.get(key) end return nil end, }) ================================================ FILE: data/scripting/lara.lua ================================================ local raw = trxc.lara local lara = { set_extra_equipment = raw.set_extra_equipment, clear_equipment = raw.clear_equipment, mesh = raw.mesh, extra_mesh = raw.extra_mesh, } -- Item proxy metatable local getters = { exposure_bar = raw.get_exposure_bar, air_bar = raw.get_air_bar, outfit = raw.get_outfit, holsters_visible = raw.are_holsters_visible, has_pistol_weapon = raw.has_pistol_weapon, equipped_gun = raw.get_equipped_gun, extra_anim = raw.get_extra_anim, item = function() return trx.items[raw.get_item()] end, target = function() local target = raw.get_target() if target == nil then return nil end return trx.items[target] end, } local setters = { exposure_bar = raw.set_exposure_bar, air_bar = raw.set_air_bar, outfit = raw.set_outfit, holsters_visible = raw.set_holsters_visible, } local lara_mt = { __index = function(self, key) local getter = getters[key] return getter and getter() or nil end, __newindex = function(self, key, value) local setter = setters[key] if setter then setter(value) return end error("Cannot set field '" .. key .. "' on trx.lara", 2) end, } setmetatable(lara, lara_mt) trx.lara = lara ================================================ FILE: data/scripting/log.lua ================================================ local raw = trxc.log local LogLevel = trxc.log.LogLevel local log = { LogLevel = LogLevel } function log.generic(level, ...) raw.log(level, ...) end function log.info(...) raw.log(LogLevel.INFO, ...) end function log.warn(...) raw.log(LogLevel.WARNING, ...) end function log.warning(...) raw.log(LogLevel.WARNING, ...) end function log.error(...) raw.log(LogLevel.ERROR, ...) end function log.debug(...) raw.log(LogLevel.DEBUG, ...) end trx.log = log ================================================ FILE: data/scripting/music.lua ================================================ local raw = trxc.music local music = { PlayMode = raw.PlayMode, } function music.get_track() return raw.get_track() end function music.play(id, opts) opts = opts or {} mode = opts.mode or trx.music.PlayMode.ONCE raw.play(id, mode) end function music.pause() raw.pause() end function music.unpause() raw.unpause() end function music.stop() raw.stop() end music.play_track = music.play trx.music = music ================================================ FILE: data/scripting/objects.lua ================================================ local raw = trxc.objects local objects = { swap_mesh = raw.swap_mesh, } trx.objects = objects ================================================ FILE: data/scripting/rooms.lua ================================================ local raw = trxc.rooms -- Room proxy metatable local getters = { num = function(idx) return idx end, underwater = raw.get_underwater, wind = raw.get_wind, flip_status = raw.get_flip_status, flipped_room = function(self, key) local flipped_room = raw.get_flipped_room(self) if flipped_room == nil then return nil end return trx.rooms[flipped_room] end, bounds = raw.get_bounds, internal_bounds = function(self, key) local bounds = raw.get_bounds(self) return { min_x = bounds.min_x + 1024, min_y = bounds.min_y, min_z = bounds.min_z + 1024, max_x = bounds.max_x - 1024, max_y = bounds.max_y, max_z = bounds.max_z - 1024, } end, } local setters = { underwater = raw.set_underwater, wind = raw.set_wind, } local Room = {} Room.__index = function(self, key) local getter = getters[key] return getter and getter(self.idx) or nil end Room.__newindex = function(self, key, value) local setter = setters[key] if setter then setter(self.idx, value) return end error("Cannot set field '" .. key .. "' on trx.items.Room") end -- rooms metatable - functions local fn = { FlipStatus = raw.FlipStatus, Room = Room, flip = raw.flip, flip_effect = raw.flip_effect, } function fn.get(arg) local idx = raw.get(arg) if not idx then return nil end return setmetatable({ idx = idx }, Room) end trx.rooms = setmetatable({}, { __len = function() return raw.count() end, __index = function(_, key) if key == "fn" then return fn elseif key == "flip" or key == "flip_effect" then return fn[key] elseif type(key) == "number" or type(key) == "string" then return fn.get(key) end return nil end, }) ================================================ FILE: data/scripting/sound.lua ================================================ local raw = trxc.sound local sound = {} function sound.is_available(id) return raw.is_available(id) end function sound.play(id, opts) raw.play(id, opts) end function sound.stop(id) raw.stop(id) end function sound.stop_all() raw.stop_all() end trx.sound = sound ================================================ FILE: data/tomb-11.bdf ================================================ STARTFONT 2.1 FONT -nerdypepper-tomb-medium-r-normal--11-80-100-100-C-50-ISO10646-1 SIZE 11 75 75 FONTBOUNDINGBOX 7 10 0 -2 STARTPROPERTIES 34 FOUNDRY "nerdypepper" FAMILY_NAME "tomb" WEIGHT_NAME "medium" SLANT "r" SETWIDTH_NAME "normal" ADD_STYLE_NAME "" PIXEL_SIZE 11 POINT_SIZE 80 RESOLUTION_X 100 RESOLUTION_Y 100 SPACING "C" AVERAGE_WIDTH 50 CHARSET_REGISTRY "ISO10646" CHARSET_ENCODING "1" FONTNAME_REGISTRY "" CHARSET_COLLECTIONS "ISO8859-2 ISO8859-9 ISO8859-4 ISO10646-1" FONT_NAME "tomb" FACE_NAME "tomb" COPYRIGHT "Copyright (c) 2025 LostArtefacts" FONT_VERSION "1.0.0" FONT_ASCENT 9 FONT_DESCENT 2 UNDERLINE_POSITION -1 UNDERLINE_THICKNESS 1 X_HEIGHT 4 CAP_HEIGHT 4 RAW_ASCENT 818 RAW_DESCENT 181 NORM_SPACE 5 RELATIVE_WEIGHT 50 RELATIVE_SETWIDTH 50 FIGURE_WIDTH 5 AVG_LOWERCASE_WIDTH 50 AVG_UPPERCASE_WIDTH 50 ENDPROPERTIES CHARS 101 STARTCHAR space ENCODING 32 SWIDTH 454 0 DWIDTH 5 0 BBX 1 1 4 0 BITMAP 00 ENDCHAR STARTCHAR exclam ENCODING 33 SWIDTH 454 0 DWIDTH 5 0 BBX 1 7 2 0 BITMAP 80 80 80 80 80 00 80 ENDCHAR STARTCHAR quotedbl ENCODING 34 SWIDTH 454 0 DWIDTH 5 0 BBX 3 3 1 4 BITMAP A0 A0 A0 ENDCHAR STARTCHAR numbersign ENCODING 35 SWIDTH 545 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP 50 50 F8 50 F8 50 50 ENDCHAR STARTCHAR dollar ENCODING 36 SWIDTH 454 0 DWIDTH 5 0 BBX 5 9 0 -1 BITMAP 20 70 A8 A0 70 28 A8 70 20 ENDCHAR STARTCHAR percent ENCODING 37 SWIDTH 545 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP C8 C8 10 20 40 98 98 ENDCHAR STARTCHAR ampersand ENCODING 38 SWIDTH 454 0 DWIDTH 5 0 BBX 5 7 0 0 BITMAP 60 80 90 78 90 90 68 ENDCHAR STARTCHAR quotesingle ENCODING 39 SWIDTH 454 0 DWIDTH 5 0 BBX 1 3 2 4 BITMAP 80 80 80 ENDCHAR STARTCHAR parenleft ENCODING 40 SWIDTH 454 0 DWIDTH 5 0 BBX 4 8 0 -1 BITMAP 30 40 80 80 80 80 40 30 ENDCHAR STARTCHAR parenright ENCODING 41 SWIDTH 454 0 DWIDTH 5 0 BBX 4 8 0 -1 BITMAP C0 20 10 10 10 10 20 C0 ENDCHAR STARTCHAR asterisk ENCODING 42 SWIDTH 454 0 DWIDTH 5 0 BBX 5 5 0 0 BITMAP 50 20 F8 20 50 ENDCHAR STARTCHAR plus ENCODING 43 SWIDTH 454 0 DWIDTH 5 0 BBX 5 5 0 0 BITMAP 20 20 F8 20 20 ENDCHAR STARTCHAR comma ENCODING 44 SWIDTH 454 0 DWIDTH 5 0 BBX 2 3 1 -1 BITMAP 40 40 80 ENDCHAR STARTCHAR hyphen ENCODING 45 SWIDTH 454 0 DWIDTH 5 0 BBX 4 1 0 2 BITMAP F0 ENDCHAR STARTCHAR period ENCODING 46 SWIDTH 454 0 DWIDTH 5 0 BBX 1 1 2 0 BITMAP 80 ENDCHAR STARTCHAR slash ENCODING 47 SWIDTH 727 0 DWIDTH 8 0 BBX 7 7 0 0 BITMAP 02 04 08 10 20 40 80 ENDCHAR STARTCHAR zero ENCODING 48 SWIDTH 545 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP 70 88 98 A8 C8 88 70 ENDCHAR STARTCHAR one ENCODING 49 SWIDTH 545 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP 20 60 A0 20 20 20 F8 ENDCHAR STARTCHAR two ENCODING 50 SWIDTH 545 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP 70 88 08 10 20 40 F8 ENDCHAR STARTCHAR three ENCODING 51 SWIDTH 545 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP 70 88 08 10 08 88 70 ENDCHAR STARTCHAR four ENCODING 52 SWIDTH 545 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP 80 90 90 F8 10 10 10 ENDCHAR STARTCHAR five ENCODING 53 SWIDTH 545 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP F8 80 80 F0 08 08 F0 ENDCHAR STARTCHAR six ENCODING 54 SWIDTH 545 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP 70 88 80 F0 88 88 70 ENDCHAR STARTCHAR seven ENCODING 55 SWIDTH 454 0 DWIDTH 5 0 BBX 5 7 0 0 BITMAP F8 08 08 10 10 20 20 ENDCHAR STARTCHAR eight ENCODING 56 SWIDTH 454 0 DWIDTH 5 0 BBX 5 7 0 0 BITMAP 70 88 88 70 88 88 70 ENDCHAR STARTCHAR nine ENCODING 57 SWIDTH 454 0 DWIDTH 5 0 BBX 5 7 0 0 BITMAP 70 88 88 78 08 88 70 ENDCHAR STARTCHAR colon ENCODING 58 SWIDTH 454 0 DWIDTH 5 0 BBX 1 4 2 1 BITMAP 80 00 00 80 ENDCHAR STARTCHAR semicolon ENCODING 59 SWIDTH 454 0 DWIDTH 5 0 BBX 2 6 1 -1 BITMAP 40 00 00 40 40 80 ENDCHAR STARTCHAR less ENCODING 60 SWIDTH 454 0 DWIDTH 5 0 BBX 3 5 0 0 BITMAP 20 40 80 40 20 ENDCHAR STARTCHAR equal ENCODING 61 SWIDTH 454 0 DWIDTH 5 0 BBX 4 3 0 1 BITMAP F0 00 F0 ENDCHAR STARTCHAR greater ENCODING 62 SWIDTH 454 0 DWIDTH 5 0 BBX 3 5 1 0 BITMAP 80 40 20 40 80 ENDCHAR STARTCHAR question ENCODING 63 SWIDTH 454 0 DWIDTH 5 0 BBX 4 7 0 0 BITMAP 60 90 10 20 40 00 40 ENDCHAR STARTCHAR at ENCODING 64 SWIDTH 454 0 DWIDTH 5 0 BBX 5 7 0 0 BITMAP 70 88 B8 A8 B8 80 70 ENDCHAR STARTCHAR A ENCODING 65 SWIDTH 544 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP 20 50 88 F8 88 88 88 ENDCHAR STARTCHAR B ENCODING 66 SWIDTH 544 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP F0 88 88 F0 88 88 F0 ENDCHAR STARTCHAR C ENCODING 67 SWIDTH 544 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP 70 88 80 80 80 88 70 ENDCHAR STARTCHAR D ENCODING 68 SWIDTH 544 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP F0 88 88 88 88 88 F0 ENDCHAR STARTCHAR E ENCODING 69 SWIDTH 544 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP F8 80 80 F0 80 80 F8 ENDCHAR STARTCHAR F ENCODING 70 SWIDTH 544 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP F8 80 80 F0 80 80 80 ENDCHAR STARTCHAR G ENCODING 71 SWIDTH 544 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP 70 88 80 98 88 88 70 ENDCHAR STARTCHAR H ENCODING 72 SWIDTH 544 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP 88 88 88 F8 88 88 88 ENDCHAR STARTCHAR I ENCODING 73 SWIDTH 544 0 DWIDTH 6 0 BBX 3 7 1 0 BITMAP E0 40 40 40 40 40 E0 ENDCHAR STARTCHAR J ENCODING 74 SWIDTH 544 0 DWIDTH 6 0 BBX 4 7 0 0 BITMAP F0 10 10 10 10 10 E0 ENDCHAR STARTCHAR K ENCODING 75 SWIDTH 544 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP 88 90 A0 C0 A0 90 88 ENDCHAR STARTCHAR L ENCODING 76 SWIDTH 544 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP 80 80 80 80 80 80 F8 ENDCHAR STARTCHAR M ENCODING 77 SWIDTH 544 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP 88 D8 A8 88 88 88 88 ENDCHAR STARTCHAR N ENCODING 78 SWIDTH 544 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP 88 C8 A8 98 88 88 88 ENDCHAR STARTCHAR O ENCODING 79 SWIDTH 544 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP 70 88 88 88 88 88 70 ENDCHAR STARTCHAR P ENCODING 80 SWIDTH 544 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP F0 88 88 F0 80 80 80 ENDCHAR STARTCHAR Q ENCODING 81 SWIDTH 544 0 DWIDTH 6 0 BBX 6 8 0 -1 BITMAP 70 88 88 88 88 98 78 04 ENDCHAR STARTCHAR R ENCODING 82 SWIDTH 544 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP F0 88 88 F0 88 88 88 ENDCHAR STARTCHAR S ENCODING 83 SWIDTH 544 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP 70 88 80 70 08 88 70 ENDCHAR STARTCHAR T ENCODING 84 SWIDTH 544 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP F8 20 20 20 20 20 20 ENDCHAR STARTCHAR U ENCODING 85 SWIDTH 544 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP 88 88 88 88 88 88 70 ENDCHAR STARTCHAR V ENCODING 86 SWIDTH 544 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP 88 88 88 88 88 50 20 ENDCHAR STARTCHAR W ENCODING 87 SWIDTH 544 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP 88 88 88 88 A8 D8 88 ENDCHAR STARTCHAR X ENCODING 88 SWIDTH 544 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP 88 88 50 20 50 88 88 ENDCHAR STARTCHAR Y ENCODING 89 SWIDTH 544 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP 88 88 88 50 20 20 20 ENDCHAR STARTCHAR Z ENCODING 90 SWIDTH 544 0 DWIDTH 6 0 BBX 5 7 0 0 BITMAP F8 08 10 20 40 80 F8 ENDCHAR STARTCHAR bracketleft ENCODING 91 SWIDTH 363 0 DWIDTH 4 0 BBX 2 7 1 0 BITMAP C0 80 80 80 80 80 C0 ENDCHAR STARTCHAR backslash ENCODING 92 SWIDTH 727 0 DWIDTH 8 0 BBX 7 7 0 0 BITMAP 80 40 20 10 08 04 02 ENDCHAR STARTCHAR bracketright ENCODING 93 SWIDTH 363 0 DWIDTH 4 0 BBX 2 7 1 0 BITMAP C0 40 40 40 40 40 C0 ENDCHAR STARTCHAR asciicircum ENCODING 94 SWIDTH 454 0 DWIDTH 5 0 BBX 5 3 0 4 BITMAP 20 50 88 ENDCHAR STARTCHAR underscore ENCODING 95 SWIDTH 454 0 DWIDTH 5 0 BBX 4 1 0 0 BITMAP F0 ENDCHAR STARTCHAR grave ENCODING 96 SWIDTH 454 0 DWIDTH 5 0 BBX 3 3 1 4 BITMAP 80 40 20 ENDCHAR STARTCHAR a ENCODING 97 SWIDTH 454 0 DWIDTH 5 0 BBX 4 5 0 0 BITMAP 70 90 90 B0 50 ENDCHAR STARTCHAR b ENCODING 98 SWIDTH 454 0 DWIDTH 5 0 BBX 4 7 0 0 BITMAP 80 80 E0 90 90 90 E0 ENDCHAR STARTCHAR c ENCODING 99 SWIDTH 454 0 DWIDTH 5 0 BBX 4 5 0 0 BITMAP 60 90 80 90 60 ENDCHAR STARTCHAR d ENCODING 100 SWIDTH 454 0 DWIDTH 5 0 BBX 4 7 0 0 BITMAP 10 10 70 90 90 90 70 ENDCHAR STARTCHAR e ENCODING 101 SWIDTH 454 0 DWIDTH 5 0 BBX 4 5 0 0 BITMAP 60 90 F0 80 70 ENDCHAR STARTCHAR f ENCODING 102 SWIDTH 454 0 DWIDTH 5 0 BBX 4 7 0 0 BITMAP 30 40 40 E0 40 40 40 ENDCHAR STARTCHAR g ENCODING 103 SWIDTH 454 0 DWIDTH 5 0 BBX 4 7 0 -2 BITMAP 70 90 90 90 70 10 60 ENDCHAR STARTCHAR h ENCODING 104 SWIDTH 454 0 DWIDTH 5 0 BBX 4 7 0 0 BITMAP 80 80 E0 90 90 90 90 ENDCHAR STARTCHAR i ENCODING 105 SWIDTH 454 0 DWIDTH 5 0 BBX 4 7 0 0 BITMAP 40 00 C0 40 40 40 30 ENDCHAR STARTCHAR j ENCODING 106 SWIDTH 454 0 DWIDTH 5 0 BBX 3 9 0 -2 BITMAP 20 00 60 20 20 20 20 20 C0 ENDCHAR STARTCHAR k ENCODING 107 SWIDTH 454 0 DWIDTH 5 0 BBX 4 7 0 0 BITMAP 80 80 90 A0 C0 A0 90 ENDCHAR STARTCHAR l ENCODING 108 SWIDTH 454 0 DWIDTH 5 0 BBX 4 7 0 0 BITMAP C0 40 40 40 40 40 30 ENDCHAR STARTCHAR m ENCODING 109 SWIDTH 454 0 DWIDTH 5 0 BBX 4 5 0 0 BITMAP 90 F0 90 90 90 ENDCHAR STARTCHAR n ENCODING 110 SWIDTH 454 0 DWIDTH 5 0 BBX 4 5 0 0 BITMAP E0 90 90 90 90 ENDCHAR STARTCHAR o ENCODING 111 SWIDTH 454 0 DWIDTH 5 0 BBX 4 5 0 0 BITMAP 60 90 90 90 60 ENDCHAR STARTCHAR p ENCODING 112 SWIDTH 454 0 DWIDTH 5 0 BBX 4 7 0 -2 BITMAP E0 90 90 90 E0 80 80 ENDCHAR STARTCHAR q ENCODING 113 SWIDTH 454 0 DWIDTH 5 0 BBX 4 7 0 -2 BITMAP 70 90 90 90 70 10 10 ENDCHAR STARTCHAR r ENCODING 114 SWIDTH 454 0 DWIDTH 5 0 BBX 4 5 0 0 BITMAP E0 90 80 80 80 ENDCHAR STARTCHAR s ENCODING 115 SWIDTH 454 0 DWIDTH 5 0 BBX 4 5 0 0 BITMAP 70 80 60 10 E0 ENDCHAR STARTCHAR t ENCODING 116 SWIDTH 454 0 DWIDTH 5 0 BBX 4 7 0 0 BITMAP 40 40 F0 40 40 40 30 ENDCHAR STARTCHAR u ENCODING 117 SWIDTH 454 0 DWIDTH 5 0 BBX 4 5 0 0 BITMAP 90 90 90 90 70 ENDCHAR STARTCHAR v ENCODING 118 SWIDTH 454 0 DWIDTH 5 0 BBX 4 5 0 0 BITMAP 90 90 90 60 60 ENDCHAR STARTCHAR w ENCODING 119 SWIDTH 454 0 DWIDTH 5 0 BBX 4 5 0 0 BITMAP 90 90 90 F0 90 ENDCHAR STARTCHAR x ENCODING 120 SWIDTH 454 0 DWIDTH 5 0 BBX 4 5 0 0 BITMAP 90 90 60 90 90 ENDCHAR STARTCHAR y ENCODING 121 SWIDTH 454 0 DWIDTH 5 0 BBX 4 7 0 -2 BITMAP 90 90 90 90 70 10 60 ENDCHAR STARTCHAR z ENCODING 122 SWIDTH 454 0 DWIDTH 5 0 BBX 4 5 0 0 BITMAP F0 20 40 80 F0 ENDCHAR STARTCHAR braceleft ENCODING 123 SWIDTH 363 0 DWIDTH 4 0 BBX 3 7 0 0 BITMAP 20 40 40 C0 40 40 20 ENDCHAR STARTCHAR bar ENCODING 124 SWIDTH 454 0 DWIDTH 5 0 BBX 1 9 2 -1 BITMAP 80 80 80 80 80 80 80 80 80 ENDCHAR STARTCHAR braceright ENCODING 125 SWIDTH 363 0 DWIDTH 4 0 BBX 3 7 1 0 BITMAP 80 40 40 20 40 40 80 ENDCHAR STARTCHAR asciitilde ENCODING 126 SWIDTH 727 0 DWIDTH 8 0 BBX 7 3 0 1 BITMAP 60 92 0C ENDCHAR STARTCHAR uni00A0 ENCODING 160 SWIDTH 454 0 DWIDTH 5 0 BBX 1 1 0 0 BITMAP 00 ENDCHAR STARTCHAR arrowleft ENCODING 8592 SWIDTH 636 0 DWIDTH 7 0 BBX 6 5 0 1 BITMAP 20 60 FC 60 20 ENDCHAR STARTCHAR arrowup ENCODING 8593 SWIDTH 545 0 DWIDTH 6 0 BBX 5 6 0 0 BITMAP 20 70 F8 20 20 20 ENDCHAR STARTCHAR arrowright ENCODING 8594 SWIDTH 636 0 DWIDTH 7 0 BBX 6 5 0 1 BITMAP 10 18 FC 18 10 ENDCHAR STARTCHAR arrowdown ENCODING 8595 SWIDTH 545 0 DWIDTH 6 0 BBX 5 5 0 1 BITMAP 20 20 F8 70 20 ENDCHAR STARTCHAR carriagereturn ENCODING 8629 SWIDTH 636 0 DWIDTH 7 0 BBX 6 6 0 0 BITMAP 04 24 64 FC 60 20 ENDCHAR ENDFONT ================================================ FILE: data/tr1/mac/Info.plist ================================================ CFBundleExecutable TRX CFBundlePackageType APPL CFBundleIdentifier com.lostartefacts.tr1x CFBundleName TR1X CFBundleIconFile icon ================================================ FILE: data/tr2/mac/Info.plist ================================================ CFBundleExecutable TRX CFBundlePackageType APPL CFBundleIdentifier com.lostartefacts.tr2x CFBundleName TR2X CFBundleIconFile icon ================================================ FILE: data/tr3/glyphs/mapping_basic_latin.txt ================================================ # -------------------------------------------------- # Unicode Block "Basic Latin" (U+0000 to U+007F) # -------------------------------------------------- # ASCII a-z U+0061:a 0 T render("Timesbd.ttf", index=26) U+0062:b 0 T render("Timesbd.ttf", index=27) U+0063:c 0 T render("Timesbd.ttf", index=28) U+0064:d 0 T render("Timesbd.ttf", index=29) U+0065:e 0 T render("Timesbd.ttf", index=30) U+0066:f 0 T render("Timesbd.ttf", index=31) U+0067:g 0 T render("Timesbd.ttf", index=32) U+0068:h 0 T render("Timesbd.ttf", index=33) U+0069:i 0 T render("Timesbd.ttf", index=34) U+006A:j 0 T render("Timesbd.ttf", index=35) U+006B:k 0 T render("Timesbd.ttf", index=36) U+006C:l 0 T render("Timesbd.ttf", index=37) U+006D:m 0 T render("Timesbd.ttf", index=38) U+006E:n 0 T render("Timesbd.ttf", index=39) U+006F:o 0 T render("Timesbd.ttf", index=40) U+0070:p 0 T render("Timesbd.ttf", index=41) U+0071:q 0 T render("Timesbd.ttf", index=42) U+0072:r 0 T render("Timesbd.ttf", index=43) U+0073:s 0 T render("Timesbd.ttf", index=44) U+0074:t 0 T render("Timesbd.ttf", index=45) U+0075:u 0 T render("Timesbd.ttf", index=46) U+0076:v 0 T render("Timesbd.ttf", index=47) U+0077:w 0 T render("Timesbd.ttf", index=48) U+0078:x 0 T render("Timesbd.ttf", index=49) U+0079:y 0 T render("Timesbd.ttf", index=50) U+007A:z 0 T render("Timesbd.ttf", index=51) # ASCII A-Z U+0041:A 0 T render("Timesbd.ttf", index=0) U+0042:B 0 T render("Timesbd.ttf", index=1) U+0043:C 0 T render("Timesbd.ttf", index=2) U+0044:D 0 T render("Timesbd.ttf", index=3) U+0045:E 0 T render("Timesbd.ttf", index=4) U+0046:F 0 T render("Timesbd.ttf", index=5) U+0047:G 0 T render("Timesbd.ttf", index=6) U+0048:H 0 T render("Timesbd.ttf", index=7) U+0049:I 0 T render("Timesbd.ttf", index=8) U+004A:J 0 T render("Timesbd.ttf", index=9) U+004B:K 0 T render("Timesbd.ttf", index=10) U+004C:L 0 T render("Timesbd.ttf", index=11) U+004D:M 0 T render("Timesbd.ttf", index=12) U+004E:N 0 T render("Timesbd.ttf", index=13) U+004F:O 0 T render("Timesbd.ttf", index=14) U+0050:P 0 T render("Timesbd.ttf", index=15) U+0051:Q 0 T render("Timesbd.ttf", index=16) U+0052:R 0 T render("Timesbd.ttf", index=17) U+0053:S 0 T render("Timesbd.ttf", index=18) U+0054:T 0 T render("Timesbd.ttf", index=19) U+0055:U 0 T render("Timesbd.ttf", index=20) U+0056:V 0 T render("Timesbd.ttf", index=21) U+0057:W 0 T render("Timesbd.ttf", index=22) U+0058:X 0 T render("Timesbd.ttf", index=23) U+0059:Y 0 T render("Timesbd.ttf", index=24) U+005A:Z 0 T render("Timesbd.ttf", index=25) # Digits 0-9 U+0030:0 0 T render("Timesbd.ttf", index=52) U+0031:1 0 T render("Timesbd.ttf", index=53) U+0032:2 0 T render("Timesbd.ttf", index=54) U+0033:3 0 T render("Timesbd.ttf", index=55) U+0034:4 0 T render("Timesbd.ttf", index=56) U+0035:5 0 T render("Timesbd.ttf", index=57) U+0036:6 0 T render("Timesbd.ttf", index=58) U+0037:7 0 T render("Timesbd.ttf", index=59) U+0038:8 0 T render("Timesbd.ttf", index=60) U+0039:9 0 T render("Timesbd.ttf", index=61) # Basic Punctuation U+0021:! 0 T render("Timesbd.ttf", index=64) U+0022:" 0 T render("Timesbd.ttf") U+0023:# 0 T render("Timesbd.ttf", index=78) U+0024:$ 0 T render("Timesbd.ttf") U+0025:% 0 T render("Timesbd.ttf") U+0026:& 0 T render("Timesbd.ttf") U+0027:' 0 T render("Timesbd.ttf", index=79) U+0028:( 0 T render("Timesbd.ttf") U+0029:) 0 T render("Timesbd.ttf") U+002A:* 0 T render("Timesbd.ttf") U+002B:+ 0 T render("Timesbd.ttf", index=72) U+002C:, 0 T render("Timesbd.ttf", index=63) U+002D:- 0 T render("Timesbd.ttf", index=71, offset_y=-2) U+002E:. 0 T render("Timesbd.ttf", index=62) U+002F:/ 0 T render("Timesbd.ttf", index=68) U+003A:: 0 T render("Timesbd.ttf", index=73) U+003B:; 0 T render("Timesbd.ttf") U+003C:< 0 T render("Timesbd.ttf") U+003D:= 0 T render("Timesbd.ttf") U+003E:> 0 T render("Timesbd.ttf") U+003F:? 0 T render("Timesbd.ttf", index=65) U+0040:@ 0 T render("Timesbd.ttf") U+005B:[ 0 T render("Timesbd.ttf", index=66) U+005C:\ 0 T render("Timesbd.ttf", index=76) U+005D:] 0 T render("Timesbd.ttf", index=75) U+005E:^ 0 T render("Timesbd.ttf") U+005F:_ 0 T render("Timesbd.ttf") U+0060:` 0 T render("Timesbd.ttf") U+007B:{ 0 T render("Timesbd.ttf") U+007C:| 0 T render("Timesbd.ttf") U+007D:} 0 T render("Timesbd.ttf") U+007E:~ 0 T render("Timesbd.ttf") ================================================ FILE: data/tr3/glyphs/mapping_combining_diactrics.txt ================================================ # -------------------------------------------------- # Combining diactrics # -------------------------------------------------- "\\{grave accent}" 0 T render("Timesbd.ttf", index=77) "\\{acute accent}" 0 T render("Timesbd.ttf", index=70) "\\{circumflex accent}" 0 T render("Timesbd.ttf", index=69) "\\{circumflex}" 0 T link("\\{circumflex accent}") "\\{macron}" 0 T render("Timesbd.ttf") "\\{breve}" 0 T render("Timesbd.ttf") "\\{dot above}" 0 T render("Timesbd.ttf") "\\{umlaut}" 0 T render("Timesbd.ttf", index=67) "\\{caron}" 0 T render("Timesbd.ttf") "\\{ring above}" 0 T render("Timesbd.ttf") "\\{tilde}" 0 T render("Timesbd.ttf") "\\{double acute accent}" 0 T render("Timesbd.ttf") "\\{acute umlaut}" 0 T render("Timesbd.ttf") ================================================ FILE: data/tr3/glyphs/mapping_cyrillic.txt ================================================ # -------------------------------------------------- # Unicode Block "Cyrillic" (U+0400 to U+04FF) # -------------------------------------------------- U+0400:Ѐ 0 T render("Timesbd.ttf") U+0401:Ё 0 T render("Timesbd.ttf") U+0402:Ђ 0 T render("Timesbd.ttf") U+0403:Ѓ 0 T render("Timesbd.ttf") U+0404:Є 0 T render("Timesbd.ttf") U+0405:Ѕ 0 T link("S") U+0406:І 0 T link("I") U+0407:Ї 0 T render("Timesbd.ttf") U+0408:Ј 0 T link("J") U+0409:Љ 0 T render("Timesbd.ttf") U+040A:Њ 0 T render("Timesbd.ttf") U+040B:Ћ 0 T render("Timesbd.ttf") U+040C:Ќ 0 T render("Timesbd.ttf") U+040D:Ѝ 0 T render("Timesbd.ttf") U+040E:Ў 0 T render("Timesbd.ttf") U+040F:Џ 0 T render("Timesbd.ttf") U+0410:А 0 T link("A") U+0411:Б 0 T render("Timesbd.ttf") U+0412:В 0 T link("B") U+0413:Г 0 T link(U+0393:Γ) U+0414:Д 0 T render("Timesbd.ttf") U+0415:Е 0 T link("E") U+0416:Ж 0 T render("Timesbd.ttf") U+0417:З 0 T render("Timesbd.ttf") U+0418:И 0 T render("Timesbd.ttf") U+0419:Й 0 T render("Timesbd.ttf") U+041A:К 0 T link("K") U+041B:Л 0 T render("Timesbd.ttf") U+041C:М 0 T link("M") U+041D:Н 0 T link("H") U+041E:О 0 T link("O") U+041F:П 0 T link(U+03A0:Π) U+0420:Р 0 T link("P") U+0421:С 0 T link("C") U+0422:Т 0 T link("T") U+0423:У 0 T render("Timesbd.ttf") U+0424:Ф 0 T link(U+03A6:Φ) U+0425:Х 0 T link("X") U+0426:Ц 0 T render("Timesbd.ttf") U+0427:Ч 0 T render("Timesbd.ttf") U+0428:Ш 0 T render("Timesbd.ttf") U+0429:Щ 0 T render("Timesbd.ttf") U+042A:Ъ 0 T render("Timesbd.ttf") U+042B:Ы 0 T render("Timesbd.ttf") U+042C:Ь 0 T render("Timesbd.ttf") U+042D:Э 0 T render("Timesbd.ttf") U+042E:Ю 0 T render("Timesbd.ttf") U+042F:Я 0 T render("Timesbd.ttf") U+0430:а 0 T link("a") U+0431:б 0 T render("Timesbd.ttf") U+0432:в 0 T render("Timesbd.ttf") U+0433:г 0 T render("Timesbd.ttf") U+0434:д 0 T render("Timesbd.ttf") U+0435:е 0 T link("e") U+0436:ж 0 T render("Timesbd.ttf") U+0437:з 0 T render("Timesbd.ttf") U+0438:и 0 T render("Timesbd.ttf") U+0439:й 0 T render("Timesbd.ttf") U+043A:к 0 T link(U+0138:ĸ) U+043B:л 0 T render("Timesbd.ttf") U+043C:м 0 T render("Timesbd.ttf") U+043D:н 0 T render("Timesbd.ttf") U+043E:о 0 T link("o") U+043F:п 0 T render("Timesbd.ttf") U+0440:р 0 T link("p") U+0441:с 0 T link("c") U+0442:т 0 T render("Timesbd.ttf") U+0443:у 0 T link("y") U+0444:ф 0 T render("Timesbd.ttf") U+0445:х 0 T link("x") U+0446:ц 0 T render("Timesbd.ttf") U+0447:ч 0 T render("Timesbd.ttf") U+0448:ш 0 T render("Timesbd.ttf") U+0449:щ 0 T render("Timesbd.ttf") U+044A:ъ 0 T render("Timesbd.ttf") U+044B:ы 0 T render("Timesbd.ttf") U+044C:ь 0 T render("Timesbd.ttf") U+044D:э 0 T render("Timesbd.ttf") U+044E:ю 0 T render("Timesbd.ttf") U+044F:я 0 T render("Timesbd.ttf") U+0450:ѐ 0 T render("Timesbd.ttf") U+0451:ё 0 T render("Timesbd.ttf") U+0452:ђ 0 T render("Timesbd.ttf") U+0453:ѓ 0 T render("Timesbd.ttf") U+0454:є 0 T render("Timesbd.ttf") U+0455:ѕ 0 T link("s") U+0456:і 0 T link("i") U+0457:ї 0 T render("Timesbd.ttf") U+0458:ј 0 T link("j") U+0459:љ 0 T render("Timesbd.ttf") U+045A:њ 0 T render("Timesbd.ttf") U+045B:ћ 0 T link(U+0127:ħ) U+045C:ќ 0 T render("Timesbd.ttf") U+045D:ѝ 0 T render("Timesbd.ttf") U+045E:ў 0 T render("Timesbd.ttf") U+045F:џ 0 T render("Timesbd.ttf") U+0490:Ґ 0 T render("Timesbd.ttf") U+0491:ґ 0 T render("Timesbd.ttf") ================================================ FILE: data/tr3/glyphs/mapping_greek_and_coptic.txt ================================================ # -------------------------------------------------- # Unicode Block "Greek and Coptic" (U+0370 to U+03FF) # -------------------------------------------------- U+0393:Γ 0 T render("Timesbd.ttf") U+0394:Δ 0 T render("Timesbd.ttf") U+0395:Ε 0 T link("E") U+0396:Ζ 0 T link("Z") U+0397:Η 0 T link("H") U+0398:Θ 0 T render("Timesbd.ttf") U+0399:Ι 0 T link("I") U+039A:Κ 0 T link("K") U+039B:Λ 0 T render("Timesbd.ttf") U+039C:Μ 0 T link("M") U+039D:Ν 0 T link("N") U+039E:Ξ 0 T render("Timesbd.ttf") U+039F:Ο 0 T link("O") U+03A0:Π 0 T render("Timesbd.ttf") U+03A1:Ρ 0 T link("P") U+03A3:Σ 0 T render("Timesbd.ttf") U+03A4:Τ 0 T link("T") U+03A5:Υ 0 T link("Y") U+03A6:Φ 0 T render("Timesbd.ttf") U+03A7:Χ 0 T link("X") U+03A8:Ψ 0 T render("Timesbd.ttf") U+03A9:Ω 0 T render("Timesbd.ttf") U+03B1:α 0 T render("Timesbd.ttf") U+03B2:β 0 T render("Timesbd.ttf") U+03B3:γ 0 T render("Timesbd.ttf") U+03B4:δ 0 T render("Timesbd.ttf") U+03B5:ε 0 T render("Timesbd.ttf") U+03B6:ζ 0 T render("Timesbd.ttf") U+03B7:η 0 T render("Timesbd.ttf") U+03B8:θ 0 T render("Timesbd.ttf") U+03B9:ι 0 T link(U+0131:ı) U+03BA:κ 0 T link(U+0138:ĸ) U+03BB:λ 0 T render("Timesbd.ttf") U+03BC:μ 0 T link(U+00B5:µ) U+03BD:ν 0 T link("v") U+03BE:ξ 0 T render("Timesbd.ttf") U+03BF:ο 0 T link("o") U+03C0:π 0 T render("Timesbd.ttf") U+03C1:ρ 0 T render("Timesbd.ttf") U+03C2:ς 0 T render("Timesbd.ttf") U+03C3:σ 0 T render("Timesbd.ttf") U+03C4:τ 0 T render("Timesbd.ttf") U+03C5:υ 0 T render("Timesbd.ttf") U+03C6:φ 0 T render("Timesbd.ttf") U+03C7:χ 0 T render("Timesbd.ttf") U+03C8:ψ 0 T render("Timesbd.ttf") U+03C9:ω 0 T render("Timesbd.ttf") U+0386:Ά 0 T render("Timesbd.ttf") U+0388:Έ 0 T render("Timesbd.ttf") U+0389:Ή 0 T render("Timesbd.ttf") U+038A:Ί 0 T render("Timesbd.ttf") U+038C:Ό 0 T render("Timesbd.ttf") U+038E:Ύ 0 T render("Timesbd.ttf") U+038F:Ώ 0 T render("Timesbd.ttf") U+0390:ΐ 0 T render("Timesbd.ttf") U+0391:Α 0 T link("A") U+0392:Β 0 T link("B") U+03AA:Ϊ 0 T render("Timesbd.ttf") U+03AB:Ϋ 0 T render("Timesbd.ttf") U+03AC:ά 0 T render("Timesbd.ttf") U+03AD:έ 0 T render("Timesbd.ttf") U+03AE:ή 0 T render("Timesbd.ttf") U+03AF:ί 0 T render("Timesbd.ttf") U+03B0:ΰ 0 T render("Timesbd.ttf") U+03CA:ϊ 0 T render("Timesbd.ttf") U+03CB:ϋ 0 T render("Timesbd.ttf") U+03CC:ό 0 T render("Timesbd.ttf") U+03CD:ύ 0 T render("Timesbd.ttf") U+03CE:ώ 0 T render("Timesbd.ttf") ================================================ FILE: data/tr3/glyphs/mapping_latin-1_supplement.txt ================================================ # -------------------------------------------------- # Unicode Block "Latin-1 Supplement" (U+0080 to U+00FF) # -------------------------------------------------- U+00A1:¡ 0 T render("Timesbd.ttf") U+00A2:¢ 0 T render("Timesbd.ttf") U+00A3:£ 0 T render("Timesbd.ttf") U+00A4:¤ 0 T render("Timesbd.ttf") U+00A5:¥ 0 T render("Timesbd.ttf") U+00A6:¦ 0 T render("Timesbd.ttf") U+00A7:§ 0 T render("Timesbd.ttf") U+00A9:© 0 T render("Timesbd.ttf") U+00AA:ª 0 T render("Timesbd.ttf") U+00AB:« 0 T render("Timesbd.ttf") U+00AC:¬ 0 T render("Timesbd.ttf") U+00AE:® 0 T render("Timesbd.ttf") U+00B0:° 0 T render("Timesbd.ttf") U+00B1:± 0 T render("Timesbd.ttf") U+00B2:² 0 T render("Timesbd.ttf") U+00B3:³ 0 T render("Timesbd.ttf") U+00B5:µ 0 T render("Timesbd.ttf") U+00B6:¶ 0 T render("Timesbd.ttf") U+00B7:· 0 T render("Timesbd.ttf") U+00B9:¹ 0 T render("Timesbd.ttf") U+00BA:º 0 T render("Timesbd.ttf") U+00BB:» 0 T render("Timesbd.ttf") U+00BC:¼ 0 T render("Timesbd.ttf") U+00BD:½ 0 T render("Timesbd.ttf") U+00BE:¾ 0 T render("Timesbd.ttf") U+00BF:¿ 0 T render("Timesbd.ttf") U+00C0:À 0 T render("Timesbd.ttf") U+00C1:Á 0 T render("Timesbd.ttf") U+00C2: 0 T render("Timesbd.ttf") U+00C3:à 0 T render("Timesbd.ttf") U+00C4:Ä 0 T render("Timesbd.ttf") U+00C5:Å 0 T render("Timesbd.ttf") U+00C6:Æ 0 T render("Timesbd.ttf") U+00C7:Ç 0 T render("Timesbd.ttf") U+00C8:È 0 T render("Timesbd.ttf") U+00C9:É 0 T render("Timesbd.ttf") U+00CA:Ê 0 T render("Timesbd.ttf") U+00CB:Ë 0 T render("Timesbd.ttf") U+00CC:Ì 0 T render("Timesbd.ttf") U+00CD:Í 0 T render("Timesbd.ttf") U+00CE:Î 0 T render("Timesbd.ttf") U+00CF:Ï 0 T render("Timesbd.ttf") U+00D0:Ð 0 T render("Timesbd.ttf") U+00D1:Ñ 0 T render("Timesbd.ttf") U+00D2:Ò 0 T render("Timesbd.ttf") U+00D3:Ó 0 T render("Timesbd.ttf") U+00D4:Ô 0 T render("Timesbd.ttf") U+00D5:Õ 0 T render("Timesbd.ttf") U+00D6:Ö 0 T render("Timesbd.ttf") U+00D7:× 0 T render("Timesbd.ttf") U+00D8:Ø 0 T render("Timesbd.ttf") U+00D9:Ù 0 T render("Timesbd.ttf") U+00DA:Ú 0 T render("Timesbd.ttf") U+00DB:Û 0 T render("Timesbd.ttf") U+00DC:Ü 0 T render("Timesbd.ttf") U+00DD:Ý 0 T render("Timesbd.ttf") U+00DE:Þ 0 T render("Timesbd.ttf") U+00DF:ß 0 T render("Timesbd.ttf", index=74) U+00E0:à 0 T render("Timesbd.ttf") U+00E1:á 0 T render("Timesbd.ttf") U+00E2:â 0 T render("Timesbd.ttf") U+00E3:ã 0 T render("Timesbd.ttf") U+00E4:ä 0 T render("Timesbd.ttf") U+00E5:å 0 T render("Timesbd.ttf") U+00E6:æ 0 T render("Timesbd.ttf") U+00E7:ç 0 T render("Timesbd.ttf") U+00E8:è 0 T render("Timesbd.ttf") U+00E9:é 0 T render("Timesbd.ttf") U+00EA:ê 0 T render("Timesbd.ttf") U+00EB:ë 0 T render("Timesbd.ttf") U+00EC:ì 0 T render("Timesbd.ttf") U+00ED:í 0 T render("Timesbd.ttf") U+00EE:î 0 T render("Timesbd.ttf") U+00EF:ï 0 T render("Timesbd.ttf") U+00F0:ð 0 T render("Timesbd.ttf") U+00F1:ñ 0 T render("Timesbd.ttf") U+00F2:ò 0 T render("Timesbd.ttf") U+00F3:ó 0 T render("Timesbd.ttf") U+00F4:ô 0 T render("Timesbd.ttf") U+00F5:õ 0 T render("Timesbd.ttf") U+00F6:ö 0 T render("Timesbd.ttf") U+00F7:÷ 0 T render("Timesbd.ttf") U+00F8:ø 0 T render("Timesbd.ttf") U+00F9:ù 0 T render("Timesbd.ttf") U+00FA:ú 0 T render("Timesbd.ttf") U+00FB:û 0 T render("Timesbd.ttf") U+00FC:ü 0 T render("Timesbd.ttf") U+00FD:ý 0 T render("Timesbd.ttf") U+00FE:þ 0 T render("Timesbd.ttf") U+00FF:ÿ 0 T render("Timesbd.ttf") ================================================ FILE: data/tr3/glyphs/mapping_latin_extended-a.txt ================================================ # -------------------------------------------------- # Unicode Block "Latin Extended-A" (U+0100 to U+017F) # -------------------------------------------------- U+0100:Ā 0 T render("Timesbd.ttf") U+0101:ā 0 T render("Timesbd.ttf") U+0102:Ă 0 T render("Timesbd.ttf") U+0103:ă 0 T render("Timesbd.ttf") U+0104:Ą 0 T render("Timesbd.ttf") U+0105:ą 0 T render("Timesbd.ttf") U+0106:Ć 0 T render("Timesbd.ttf") U+0107:ć 0 T render("Timesbd.ttf") U+0108:Ĉ 0 T render("Timesbd.ttf") U+0109:ĉ 0 T render("Timesbd.ttf") U+010A:Ċ 0 T render("Timesbd.ttf") U+010B:ċ 0 T render("Timesbd.ttf") U+010C:Č 0 T render("Timesbd.ttf") U+010D:č 0 T render("Timesbd.ttf") U+010E:Ď 0 T render("Timesbd.ttf") U+010F:ď 0 T render("Timesbd.ttf") U+0110:Đ 0 T link(U+00D0:Ð) U+0111:đ 0 T render("Timesbd.ttf") U+0112:Ē 0 T render("Timesbd.ttf") U+0113:ē 0 T render("Timesbd.ttf") U+0114:Ĕ 0 T render("Timesbd.ttf") U+0115:ĕ 0 T render("Timesbd.ttf") U+0116:Ė 0 T render("Timesbd.ttf") U+0117:ė 0 T render("Timesbd.ttf") U+0118:Ę 0 T render("Timesbd.ttf") U+0119:ę 0 T render("Timesbd.ttf") U+011A:Ě 0 T render("Timesbd.ttf") U+011B:ě 0 T render("Timesbd.ttf") U+011C:Ĝ 0 T render("Timesbd.ttf") U+011D:ĝ 0 T render("Timesbd.ttf") U+011E:Ğ 0 T render("Timesbd.ttf") U+011F:ğ 0 T render("Timesbd.ttf") U+0120:Ġ 0 T render("Timesbd.ttf") U+0121:ġ 0 T render("Timesbd.ttf") U+0122:Ģ 0 T render("Timesbd.ttf") U+0123:ģ 0 T render("Timesbd.ttf") U+0124:Ĥ 0 T render("Timesbd.ttf") U+0125:ĥ 0 T render("Timesbd.ttf") U+0126:Ħ 0 T render("Timesbd.ttf") U+0127:ħ 0 T render("Timesbd.ttf") U+0128:Ĩ 0 T render("Timesbd.ttf") U+0129:ĩ 0 T render("Timesbd.ttf") U+012A:Ī 0 T render("Timesbd.ttf") U+012B:ī 0 T render("Timesbd.ttf") U+012C:Ĭ 0 T render("Timesbd.ttf") U+012D:ĭ 0 T render("Timesbd.ttf") U+012E:Į 0 T render("Timesbd.ttf") U+012F:į 0 T render("Timesbd.ttf") U+0130:İ 0 T render("Timesbd.ttf") U+0131:ı 0 T render("Timesbd.ttf") U+0134:Ĵ 0 T render("Timesbd.ttf") U+0135:ĵ 0 T render("Timesbd.ttf") U+0136:Ķ 0 T render("Timesbd.ttf") U+0137:ķ 0 T render("Timesbd.ttf") U+0138:ĸ 0 T render("Timesbd.ttf") U+0139:Ĺ 0 T render("Timesbd.ttf") U+013A:ĺ 0 T render("Timesbd.ttf") U+013B:Ļ 0 T render("Timesbd.ttf") U+013C:ļ 0 T render("Timesbd.ttf") U+013D:Ľ 0 T render("Timesbd.ttf") U+013E:ľ 0 T render("Timesbd.ttf") U+013F:Ŀ 0 T render("Timesbd.ttf") U+0140:ŀ 0 T render("Timesbd.ttf") U+0141:Ł 0 T render("Timesbd.ttf") U+0142:ł 0 T render("Timesbd.ttf") U+0143:Ń 0 T render("Timesbd.ttf") U+0144:ń 0 T render("Timesbd.ttf") U+0145:Ņ 0 T render("Timesbd.ttf") U+0146:ņ 0 T render("Timesbd.ttf") U+0147:Ň 0 T render("Timesbd.ttf") U+0148:ň 0 T render("Timesbd.ttf") U+0149:ʼn 0 T render("Timesbd.ttf") U+014A:Ŋ 0 T render("Timesbd.ttf") U+014B:ŋ 0 T render("Timesbd.ttf") U+014C:Ō 0 T render("Timesbd.ttf") U+014D:ō 0 T render("Timesbd.ttf") U+014E:Ŏ 0 T render("Timesbd.ttf") U+014F:ŏ 0 T render("Timesbd.ttf") U+0150:Ő 0 T render("Timesbd.ttf") U+0151:ő 0 T render("Timesbd.ttf") U+0152:Œ 0 T render("Timesbd.ttf") U+0153:œ 0 T render("Timesbd.ttf") U+0154:Ŕ 0 T render("Timesbd.ttf") U+0155:ŕ 0 T render("Timesbd.ttf") U+0156:Ŗ 0 T render("Timesbd.ttf") U+0157:ŗ 0 T render("Timesbd.ttf") U+0158:Ř 0 T render("Timesbd.ttf") U+0159:ř 0 T render("Timesbd.ttf") U+015A:Ś 0 T render("Timesbd.ttf") U+015B:ś 0 T render("Timesbd.ttf") U+015C:Ŝ 0 T render("Timesbd.ttf") U+015D:ŝ 0 T render("Timesbd.ttf") U+015E:Ş 0 T render("Timesbd.ttf") U+015F:ş 0 T render("Timesbd.ttf") U+0160:Š 0 T render("Timesbd.ttf") U+0161:š 0 T render("Timesbd.ttf") U+0162:Ţ 0 T render("Timesbd.ttf") U+0163:ţ 0 T render("Timesbd.ttf") U+0164:Ť 0 T render("Timesbd.ttf") U+0165:ť 0 T render("Timesbd.ttf") U+0166:Ŧ 0 T render("Timesbd.ttf") U+0167:ŧ 0 T render("Timesbd.ttf") U+0168:Ũ 0 T render("Timesbd.ttf") U+0169:ũ 0 T render("Timesbd.ttf") U+016A:Ū 0 T render("Timesbd.ttf") U+016B:ū 0 T render("Timesbd.ttf") U+016C:Ŭ 0 T render("Timesbd.ttf") U+016D:ŭ 0 T render("Timesbd.ttf") U+016E:Ů 0 T render("Timesbd.ttf") U+016F:ů 0 T render("Timesbd.ttf") U+0170:Ű 0 T render("Timesbd.ttf") U+0171:ű 0 T render("Timesbd.ttf") U+0172:Ų 0 T render("Timesbd.ttf") U+0173:ų 0 T render("Timesbd.ttf") U+0174:Ŵ 0 T render("Timesbd.ttf") U+0175:ŵ 0 T render("Timesbd.ttf") U+0176:Ŷ 0 T render("Timesbd.ttf") U+0177:ŷ 0 T render("Timesbd.ttf") U+0178:Ÿ 0 T render("Timesbd.ttf") U+0179:Ź 0 T render("Timesbd.ttf") U+017A:ź 0 T render("Timesbd.ttf") U+017B:Ż 0 T render("Timesbd.ttf") U+017C:ż 0 T render("Timesbd.ttf") U+017D:Ž 0 T render("Timesbd.ttf") U+017E:ž 0 T render("Timesbd.ttf") ================================================ FILE: data/tr3/glyphs/mapping_latin_extended-b.txt ================================================ # -------------------------------------------------- # Unicode Block "Latin Extended-B" (U+0180 to U+024F) # -------------------------------------------------- U+0192:ƒ 0 T render("Timesbd.ttf") U+01CD:Ǎ 0 T render("Timesbd.ttf") U+01CE:ǎ 0 T render("Timesbd.ttf") U+01CF:Ǐ 0 T render("Timesbd.ttf") U+01D0:ǐ 0 T render("Timesbd.ttf") U+01D1:Ǒ 0 T render("Timesbd.ttf") U+01D2:ǒ 0 T render("Timesbd.ttf") U+01D3:Ǔ 0 T render("Timesbd.ttf") U+01D4:ǔ 0 T render("Timesbd.ttf") U+01E6:Ǧ 0 T render("Timesbd.ttf") U+01E7:ǧ 0 T render("Timesbd.ttf") U+01E8:Ǩ 0 T render("Timesbd.ttf") U+01E9:ǩ 0 T render("Timesbd.ttf") U+01F0:ǰ 0 T render("Timesbd.ttf") U+01F4:Ǵ 0 T render("Timesbd.ttf") U+01F5:ǵ 0 T render("Timesbd.ttf") U+01F8:Ǹ 0 T render("Timesbd.ttf") U+01F9:ǹ 0 T render("Timesbd.ttf") U+021E:Ȟ 0 T render("Timesbd.ttf") U+021F:ȟ 0 T render("Timesbd.ttf") U+0226:Ȧ 0 T render("Timesbd.ttf") U+0227:ȧ 0 T render("Timesbd.ttf") U+022E:Ȯ 0 T render("Timesbd.ttf") U+022F:ȯ 0 T render("Timesbd.ttf") U+0232:Ȳ 0 T render("Timesbd.ttf") U+0233:ȳ 0 T render("Timesbd.ttf") U+0218:Ș 0 T render("Timesbd.ttf") U+0219:ș 0 T render("Timesbd.ttf") U+021A:Ț 0 T render("Timesbd.ttf") U+021B:ț 0 T render("Timesbd.ttf") ================================================ FILE: data/tr3/glyphs/mapping_latin_extended_additional.txt ================================================ # -------------------------------------------------- # Unicode Block "Latin Extended Additional" (U+1E00 to U+1EFF) # -------------------------------------------------- U+1E02:Ḃ 0 T render("Timesbd.ttf") U+1E03:ḃ 0 T render("Timesbd.ttf") U+1E0A:Ḋ 0 T render("Timesbd.ttf") U+1E0B:ḋ 0 T render("Timesbd.ttf") U+1E1E:Ḟ 0 T render("Timesbd.ttf") U+1E1F:ḟ 0 T render("Timesbd.ttf") U+1E20:Ḡ 0 T render("Timesbd.ttf") U+1E21:ḡ 0 T render("Timesbd.ttf") U+1E22:Ḣ 0 T render("Timesbd.ttf") U+1E23:ḣ 0 T render("Timesbd.ttf") U+1E26:Ḧ 0 T render("Timesbd.ttf") U+1E27:ḧ 0 T render("Timesbd.ttf") U+1E30:Ḱ 0 T render("Timesbd.ttf") U+1E31:ḱ 0 T render("Timesbd.ttf") U+1E3E:Ḿ 0 T render("Timesbd.ttf") U+1E3F:ḿ 0 T render("Timesbd.ttf") U+1E40:Ṁ 0 T render("Timesbd.ttf") U+1E41:ṁ 0 T render("Timesbd.ttf") U+1E44:Ṅ 0 T render("Timesbd.ttf") U+1E45:ṅ 0 T render("Timesbd.ttf") U+1E54:Ṕ 0 T render("Timesbd.ttf") U+1E55:ṕ 0 T render("Timesbd.ttf") U+1E56:Ṗ 0 T render("Timesbd.ttf") U+1E57:ṗ 0 T render("Timesbd.ttf") U+1E58:Ṙ 0 T render("Timesbd.ttf") U+1E59:ṙ 0 T render("Timesbd.ttf") U+1E60:Ṡ 0 T render("Timesbd.ttf") U+1E61:ṡ 0 T render("Timesbd.ttf") U+1E6A:Ṫ 0 T render("Timesbd.ttf") U+1E6B:ṫ 0 T render("Timesbd.ttf") U+1E7C:Ṽ 0 T render("Timesbd.ttf") U+1E7D:ṽ 0 T render("Timesbd.ttf") U+1E80:Ẁ 0 T render("Timesbd.ttf") U+1E81:ẁ 0 T render("Timesbd.ttf") U+1E82:Ẃ 0 T render("Timesbd.ttf") U+1E83:ẃ 0 T render("Timesbd.ttf") U+1E84:Ẅ 0 T render("Timesbd.ttf") U+1E85:ẅ 0 T render("Timesbd.ttf") U+1E86:Ẇ 0 T render("Timesbd.ttf") U+1E87:ẇ 0 T render("Timesbd.ttf") U+1E8A:Ẋ 0 T render("Timesbd.ttf") U+1E8B:ẋ 0 T render("Timesbd.ttf") U+1E8C:Ẍ 0 T render("Timesbd.ttf") U+1E8D:ẍ 0 T render("Timesbd.ttf") U+1E8E:Ẏ 0 T render("Timesbd.ttf") U+1E8F:ẏ 0 T render("Timesbd.ttf") U+1E90:Ẑ 0 T render("Timesbd.ttf") U+1E91:ẑ 0 T render("Timesbd.ttf") U+1E97:ẗ 0 T render("Timesbd.ttf") U+1E98:ẘ 0 T render("Timesbd.ttf") U+1E99:ẙ 0 T render("Timesbd.ttf") U+1EBC:Ẽ 0 T render("Timesbd.ttf") U+1EBD:ẽ 0 T render("Timesbd.ttf") U+1EF2:Ỳ 0 T render("Timesbd.ttf") U+1EF3:ỳ 0 T render("Timesbd.ttf") U+1EF8:Ỹ 0 T render("Timesbd.ttf") U+1EF9:ỹ 0 T render("Timesbd.ttf") ================================================ FILE: data/tr3/glyphs/mapping_misc.txt ================================================ # -------------------------------------------------- # Unicode Block "General Punctuation" (U+2000 to U+206F) # -------------------------------------------------- U+2013:– 0 T render("Timesbd.ttf") U+2014:— 0 T render("Timesbd.ttf") U+2018:‘ 0 T render("Timesbd.ttf") U+2019:’ 0 T render("Timesbd.ttf") U+201C:“ 0 T render("Timesbd.ttf") U+201D:” 0 T render("Timesbd.ttf") U+2020:† 0 T render("Timesbd.ttf") U+2021:‡ 0 T render("Timesbd.ttf") U+2022:• 0 T render("Timesbd.ttf") U+2026:… 0 T render("Timesbd.ttf") U+2030:‰ 0 T render("Timesbd.ttf") U+2039:‹ 0 T render("Timesbd.ttf") U+203A:› 0 T render("Timesbd.ttf") # -------------------------------------------------- # Unicode Block "Superscripts and Subscripts" (U+2070 to U+209F) # -------------------------------------------------- U+2074:⁴ 0 T render("Timesbd.ttf") # -------------------------------------------------- # Unicode Block "Currency Symbols" (U+20A0 to U+20CF) # -------------------------------------------------- U+20AC:€ 0 T render("Timesbd.ttf") U+20AF:₯ 0 T render("Timesbd.ttf") # -------------------------------------------------- # Unicode Block "Letterlike Symbols" (U+2100 to U+214F) # -------------------------------------------------- U+2116:№ 0 T render("Timesbd.ttf") U+2122:™ 0 T render("Timesbd.ttf") # -------------------------------------------------- # Unicode Block "Alphabetic Presentation Forms" (U+FB00 to U+FB4F) # -------------------------------------------------- U+FB01:fi 0 T render("Timesbd.ttf") U+FB02:fl 0 T render("Timesbd.ttf") ================================================ FILE: data/tr3/glyphs/mapping_small.txt ================================================ # -------------------------------------------------- # Small text # -------------------------------------------------- U+0030:0 1 T render("Times.ttf") U+0031:1 1 T render("Times.ttf") U+0032:2 1 T render("Times.ttf") U+0033:3 1 T render("Times.ttf") U+0034:4 1 T render("Times.ttf") U+0035:5 1 T render("Times.ttf") U+0036:6 1 T render("Times.ttf") U+0037:7 1 T render("Times.ttf") U+0038:8 1 T render("Times.ttf") U+0039:9 1 T render("Times.ttf") U+002D:- 1 T render("Times.ttf") U+002C:, 1 T render("Times.ttf") U+00B0:° 1 T render("Times.ttf") ================================================ FILE: data/tr3/mac/Info.plist ================================================ CFBundleExecutable TRX CFBundlePackageType APPL CFBundleIdentifier com.lostartefacts.tr3x CFBundleName TR3X CFBundleIconFile icon ================================================ FILE: data/trx/icon.rc ================================================ id ICON "{icon_path}" ================================================ FILE: data/trx/ship/cfg/base_strings-de.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "language_name": "Deutsch", "general": { "actions": { "examine_item": "Untersuchen", "hide_dialog": "Dialog ausblenden", "reset_defaults": "Auf Standard zurücksetzen", "rotate": "Drehen", "unbind": "Freigeben", "use_item": "Benutzen", }, "config_presets": { "applied": "\\{review}Voreinstellung angewendet.", "confirm_description": "\\{review}Die folgenden Einstellungen werden geändert:", "confirm_restart_note": "\\{review}Hinweis: Einige Einstellungen erfordern möglicherweise einen Neustart des Spiels, um wirksam zu werden.", "empty": "\\{review}Keine Voreinstellungen gefunden.", "no_changes": "\\{review}Keine Änderungen anzuwenden.", "title_fmt": "\\{review}Voreinstellung %s anwenden?", }, "globe_select": { "area_1": "\\{review}Bereich 1", "area_2": "\\{review}Bereich 2", "area_3": "\\{review}Bereich 3", "area_4": "\\{review}Bereich 4", "area_5": "\\{review}Bereich 5", "area_6": "\\{review}Bereich 6", }, "inventory_ring": { "heading_adventure": "\\{review}Abenteuer", "heading_fmt": "%s", "heading_game_over": "GAME OVER", "heading_inventory": "INVENTAR", "heading_items": "ITEMS", "heading_option": "OPTION", "item_count_fmt": "\\{small}%s", "object_name_fmt": "%s", }, "misc": { "demo_mode": "Demo Modus", "direction_keys_controller": "Steuerkreuz", "direction_keys_keyboard": "Pfeile", "empty_slot_fmt": "- LEERER SLOT -", "exit": "Beenden", "hold_fmt": "Gedrückt halten %s", "off": "Aus", "on": "Ein", "pagination_nav": "%d / %d", }, "osd": { "ambiguous_input_2": "Mehrdeutige Eingabe: %s und %s", "ambiguous_input_3": "Mehrdeutige Eingabe: %s, %s, ...", "bilinear_filter_off": "Bilinear-Filter: aus", "bilinear_filter_on": "Bilinear-Filter: ein", "command_bad_invocation": "Unzulässiger Aufruf: %s", "command_bool": "ein, aus", "command_decimal": "[Dezimal]", "command_integer": "[Integer]", "command_percent": "[Integer]", "command_unavailable": "Dieses Kommando ist momentan nicht ausführbar", "command_valid_values": "Zulässige Werte: %s", "complete_level": "Level abgeschlossen!", "config_option_get": "%s ist momentan eingestellt auf %s", "config_option_set": "%s geändert zu %s", "config_option_unknown_option": "Unbekannte Option: %s", "current_health_get": "Laras aktuelle Lebensenergie: %d", "current_health_set": "Laras Lebensenergie eingestellt auf %d", "door_close": "Sesam, schließe dich!", "door_open": "Sesam, öffne dich!", "door_open_fail": "Keine Türen in Laras Nähe", "flipmap_fail_already_off": "Flipmap ist bereits AUS", "flipmap_fail_already_on": "Flipmap ist bereits AN", "flipmap_off": "Flipmap ", "flipmap_on": "Flipmap eingeschaltet", "fly_mode_off": "Flug-Modus ausgeschaltet", "fly_mode_on": "Flug-Modus eingeschaltet", "fps_counter_off": "FPS-Counter ausgeschaltet", "fps_counter_on": "FPS-Counter eingeschaltet", "give_item": "Es wurde %s zu Laras Inventar hinzugefügt.", "give_item_all_guns": "Geladen und entsichert - Lara ist bis zu ihren Zähnen bewaffnet!", "give_item_all_keys": "Überraschung! Jedes Schlüsselitem, dass Lara benötigt, ist jetzt in ihrem Inventar.", "give_item_cheat": "Laras Rucksack ist schwerer geworden!", "heal_already_full_hp": "Laras Gesundheit ist schon vollständig gefüllt.", "heal_success": "Laras Gesundheit ist wieder vollständig wiederhergestellt.", "invalid_cutscene": "Unzulässige Zwischensequenz", "invalid_demo": "Unzulässige Demo", "invalid_item": "Unbekanntes Item: %s", "invalid_level": "Unzulässiger Level", "invalid_object": "Unzulässiges Objekt", "invalid_room": "Unzulässiger Raum: %d. Zulässige Räume sind 0-%d", "invalid_sample": "Unzulässiger Sound: %d", "kill": "Bye-bye!", "kill_all": "Poof! %d Feinde verschwunden!", "kill_all_fail": "Oh oh, es sind keine Feinde mehr zum Töten da...", "kill_fail": "Kein Feind in der Nähe...", "lighting_contrast_fmt": "Beleuchtungs-Kontrast: %s", "load_game": "Spiel laden von folgendem Speicher-Slot %d", "load_game_fail_invalid_slot": "Unzulässiger Speicher-Slot %d", "load_game_fail_unavailable_slot": "Speicher-Slot %d ist nicht verfügbar", "object_not_found": "Objekt nicht gefunden", "play_cutscene": "Lade Zwischensequenz %d", "play_demo": "Lade Demo %d", "play_level": "Lade %s", "pos_lara_missing": "Lara nicht vorhanden", "pos_lara_pos_fmt": "Raum: %d\nPosition: %.3f, %.3f, %.3f\nRotation: %.3f, %.3f, %.3f", "pos_level_fmt": "Level %d", "pos_level_fmt_cutscene": "Zwischensequenz %d", "pos_level_fmt_demo": "Demo %d", "quick_load": "\\{review}Schnell geladener Slot %d", "quick_load_fail_no_bound_slot": "\\{review}Kein Speicherplatz ist derzeit zugewiesen", "quick_load_fail_unavailable_bound_slot": "\\{review}Der zugewiesene Speicherplatz ist nicht verfügbar", "quick_save": "\\{review}Schnell gespeichert", "quick_save_fail_no_slots": "\\{review}Keine Schnellspeicherplätze konfiguriert", "save_game": "Spiel gespeichert auf Speicher-Slot %d", "save_game_fail_invalid_slot": "Unzulässiger Speicher-Slot %d", "sound_available_samples": "Verfügbare Sounds: %s", "sound_playing_sample": "Spiele Sound: %d", "speed_get": "Momentane Geschwindigkeit: %d", "speed_set": "Geschwindigkeit eingestellt auf %d", "strings_failed": "Neu Laden der Sprachdateien ist fehlgeschlagen", "strings_reloaded": "Sprachdateien neu geladen", "textures_off": "\\{review}Texturen: aus", "textures_on": "\\{review}Texturen: an", "trapezoid_filter_off": "Deaktiviert Trapezoid-Filter", "trapezoid_filter_on": "Aktiviert Trapezoid-Filter", "ui_off": "UI deakiviert", "ui_on": "UI aktiviert", "unknown_command": "Unbekanntes Kommando: %s", "upscaling_factor": "Uscaling-Faktor: x%d", "wireframe_mode_off": "Gitterrahmen-Modus: aus", "wireframe_mode_on": "Gitterrahmen-Modus: ein", }, "overlay": { "debug_animation": "Animation: ", "debug_animation_state": "\\{review}Status: ", "debug_camera_pos": "\\{review}Kamerastandort: ", "debug_camera_target": "\\{review}Kameraziel: ", "debug_immune": "Unverwundbarkeit ein", "debug_position": "Position: ", "debug_rotation": "Rotation: ", "debug_speed": "Geschwindigkeit: ", "item_count_fmt_pc": "\\{small}%s", "item_count_fmt_ps1": "\\{small}%s", }, "passport": { "delete_save": "\\{review}Löschen", "delete_save_confirm": "\\{review}Diesen Speicherstand löschen?", "delete_save_failed": "\\{review}Der ausgewählte Speicherstand konnte nicht gelöscht werden.", "delete_save_no": "\\{review}Nein", "delete_save_yes": "\\{review}Ja", "exit_game": "Spiel beenden", "exit_to_title": "Zurück zum Hauptmenü", "load_game": "Spiel laden", "mode_new_game": "Neues Spiel", "mode_new_game_jp": "Japanisch NS", "mode_new_game_jp_plus": "Japanisch NS+", "mode_new_game_plus": "Neues Spiel+", "new_game": "Neues Spiel", "play_previous_levels": "\\{review}Vorherige Level spielen", "restart_level": "Level Neu starten", "save_game": "Spiel speichern", "save_slot_unsupported": "\\{review}Dieser Speicherstand unterstützt diese Funktion nicht.", "select_level": "Level auswählen", "select_mod": "\\{review}Spiel auswählen", "select_mode": "Modus Auswählen", "select_save": "\\{review}Speicher auswählen", "story_so_far": "Bisherige Geschichte...", "switch_mod": "\\{review}Spiel wechseln", }, "pause": { "are_you_sure": "Sind sie sicher?", "continue": "Fortsetzen", "exit_to_title": "Wirklich zum Hauptmenü zurückkehren?", "no": "Nein", "paused": "Pause", "quit": "Beenden", "yes": "Ja", }, "photo_mode": { "advance_frame": "Frame Vorwärts", "camera_move_prompt": "Kamera bewegen", "camera_reset_prompt": "Kamera zurücksetzen", "camera_roll_prompt": "Rolle Kamera", "camera_rotate_90_prompt": "Rotiere um 90 Grad", "camera_rotate_prompt": "Rotiere Kamera", "change_lara_pose": "Pose ändern", "fov_prompt": "FOV einstellen", "lara_move_prompt": "\\{review}Bewege Lara", "lara_reset_prompt": "\\{review}Setze Lara zurück", "lara_roll_prompt": "\\{review}Rolle Lara", "lara_rotate_90_prompt": "\\{review}Drehe Lara um 90°", "lara_rotate_prompt": "\\{review}Drehe Lara", "snap_prompt": "Bild aufnehmen", "title_camera_pos": "Foto-Modus", "title_lara_pos": "\\{review}Lara bewegen", "toggle_help": "Hilfe Ein/Aus", }, "settings": { "common": { "all_hidden_disclaimer": "\\{review}Die Einstellungen sind für dieses Level-Set deaktiviert.", "chroma": "Chroma", "edit_value": "Wert bearbeiten", "frozen_option_disclaimer": "Diese Einstellung ist vom Ersteller des Levels erzwungen und kann nicht geändert werden.", "hue": "Farbton", "lightness": "Helligkeit", "restore_default": "Standard wiederherstellen", "toggle_help": "Hilfe Ein/Aus", }, "controls": { "backend": { "controller": "Kontroller", "keyboard": "Tastatur", }, "customize": "Steuerung anpassen", "layout": { "custom_1": "Eigene Steuerung 1", "custom_2": "Eigene Steuerung 2", "custom_3": "Eigene Steuerung 3", "default": "Standardtasten", }, "tabs": { "basics": "Bewegung", "items": "Items", "misc": "Sonstiges", "system": "System", } }, "gameplay": { "tabs": { "controls": "Steuerung", "fixes": "Fixes", "general": "Allgemeines", "mods": "Mods", "presets": "\\{review}Voreinstellungen", }, "title": "Gameplay-Optionen", }, "graphic_settings": { "tabs": { "bars": "\\{review}Leisten", "rendering": "\\{review}Rendern", "stats": "\\{review}Statistiken", "ui": "\\{review}Benutzeroberfläche", "visuals": "\\{review}Darstellung", }, "title": "Grafik Optionen", }, "sound": { "tabs": { "misc": "\\{review}Verschiedenes", "volume": "\\{review}Lautstärke", }, "title": "Sound-Optionen", } }, "stats": { "ammo": "Munition Treffer/Verbraucht", "ammo_hits": "Treffer", "ammo_used": "Benötigte Munition", "assault_best_time_fmt": "\\{review}%s", "assault_finish": "Zeit", "assault_no_times_set": "Keine Zeiten gesetzt", "assault_other_times_fmt": "\\{review}%s", "assault_title": "Bestzeiten", "basic_fmt": "%d", "bonus_statistics": "Bonusstatistiken", "crystals": "\\{review}Kristalle", "deaths": "Tode", "detail_fmt": "%d von %d", "distance_travelled": "Zurückgelegte Distanz", "final_statistics": "Finale Statistiken", "gym_assault_course": "\\{review}Angriffskurs", "gym_racetrack_course": "\\{review}Rennstrecke", "kills": "Besiegte Gegner", "level": "Level", "medipacks_used": "Benötigte Medi-Packs", "none": "None", "pickups": "Pickups", "secrets": "Gefundene Geheimnisse", "time_taken": "Benötigte Zeit", } }, "console": { "cmd": { "braid": { "help": "Aktiviert und deaktiviert Laras Zopf.", }, "cheats": { "help": "Schaltet In-Game Cheats ein oder aus.", }, "clear": { "help": "\\{review}Löscht sichtbare Konsolenprotokolle.", }, "debug": { "help": "Aktiviert und deaktiviert visuele Debuginformationen.", }, "drain": { "help": "Entfernt das gesamte Wasser aus dem Raum, in dem sie sich im Moment befinden.", }, "end_level": { "help": "Beendet das aktuelle Level.", }, "exit": { "help": "Beendet das Spiel.", }, "flipmap": { "help": "Aktiviert und deaktiviert die Flip-Map.", }, "flood": { "help": "Überflutet den Raum, in dem sie sich im Moment befinden, mit Wasser.", }, "fly": { "help": "Aktiviert und deaktiviert den Flug-Modus-Cheat.", }, "fps": { "help": "Erhöht oder senkt den FPS-Wert.", }, "give": { "help": "Fügt ein angegebenes Item zu Laras Inventar hinzu.", "invalid_secret": "Unzulässiges Geheimnis: #%d (zulässige Geheimnisse: %s)", "secret_given": "Geheimnis hinzugefügt %s", "secret_list": "Gesammelte Geheimnisse: %d von %d (%s)", "secret_none": "Gesammelte Geheimnisse: %d von %d", "secret_taken": "Geheimnis enfernt %s", }, "give_secret": { "help": "Listet Laras Geheimnisse auf oder nimmt/gibt ein nach Nummern sortiertes Geheimnis.", }, "heal": { "help": "Heilt Lara zurück auf volle Lebensenergie.", }, "help": { "help": "Zeigt Hilfe für alle Kommandos oder detaillierte Hilfe für ein bestimmtes.", "list": "Verfügbare Kommandos:", }, "hp": { "help": "Stellt Laras Lebensenergie auf den bestimmten Wert ein.", }, "immune": { "help": "Aktiviet oder deaktiviert Unverwundbarkeit. (Lara kann unter bestimmten Umständen immernoch sterben.)", "off": "Lara ist nun verwundbar", "on": "Lara ist nun unverwundbar", }, "inf_sprint": { "help": "\\{review}Schaltet unendliches Sprinten um.", "off": "\\{review}Lara kann nicht mehr ewig sprinten", "on": "\\{review}Lara kann jetzt ewig sprinten", }, "kill": { "help": "Tötet in der Nähe befindliche Feinde.", }, "lighting": { "help": "Aktiviet oder deaktiviert Beleuchtungs-Sytem.", }, "load": { "help": "\\{review}Lädt das Spiel aus dem angegebenen Speicherplatz oder von einem Schnell-Speicher.", }, "lua": { "help": "\\{review}Führt den angegebenen Lua-Code-String aus.", "runtime_error": "\\{review}Lua-Laufzeitfehler: %s", "syntax_error": "\\{review}Lua-Syntaxfehler: %s", }, "mod": { "help": "\\{review}Wechselt zum angegebenen Mod und startet das Spiel neu.", }, "music": { "help": "Spielt Musik-Titel mit dazugehöriger ID ab.", }, "play_cutscene": { "help": "Spielt Zwischensequenz mit dazugehöriger Nummer ab.", }, "play_demo": { "help": "Spielt eine Demo mit der angegebenen Nummer ab.", }, "play_gym": { "help": "Startet das Gym-Level.", }, "play_level": { "help": "Startet einen Level mit dem angegebenen Namen oder der dazugehörigen Nummer.", }, "play_music": { "invalid_track": "Unzulässiger Musik Titel", "stopped": "\\{review}Musik gestoppt", "track": "Spiele Musiktitel %d", }, "pos": { "help": "Zeigt Laras Position an.", }, "save": { "help": "\\{review}Speichert das Spiel im angegebenen Speicherplatz oder im nächsten Schnell-Speicherplatz.", }, "screenshot": { "help": "\\{review}Speichert einen Screenshot auf der Festplatte, mit optionalem Pfad.", }, "set": { "help": "Aktualisiert oder zeigt die angegebene Konfigurations-Einstellung an.", }, "sfx": { "help": "Spielt einen Sound-Effekt mit der angegebenen ID ab.", }, "spawn": { "fail": "\\{review}Erzeugen des angeforderten Objekts fehlgeschlagen", "success": "\\{review}Angefordertes Objekt in der Nähe von Lara erzeugt", }, "speed": { "help": "Ändert die Geschwindigkeit des Spiels.", }, "strings": { "help": "Lädt erneut die aktuellen Sprachdateien von der Festplatte.", }, "teleport": { "item": "Teleportiert zu Objekt: %d", "item_fail": "Fehlgeschlagene Teleportation zu Objekt: %d", "object": "Teleportiert zu Objekt: %s", "object_fail": "Fehlgeschlagene Teleportation zu Objekt: %s", "pos": "Teleportiere zu Position: %.3f %.3f %.3f", "pos_fail": "Fehlgeschlagene Teleportation zu Position: %.3f %.3f %.3f", "room": "Teleportiere zu Raum: %d", "room_fail": "Teleportation zu Raum: %d", }, "textures": { "help": "\\{review}Schaltet die Texturen ein oder aus.", }, "title": { "help": "Bringt sie zurück zum Titel-Bildschirm.", }, "tp": { "help": "Teleportiert Lara zu der angegebenen Position oder Raum-Nummer.", }, "trigger": { "help": "\\{review}Löst ein Element durch ID, Elementname oder Objektnamen aus oder setzt es zurück.", "invalid_item": "\\{review}Ungültiges Element: %s", "no_match": "\\{review}Unbekanntes Ziel: %s", "not_found": "\\{review}Keine passenden Elemente gefunden für: %s", "triggered": "\\{review}Ausgelöstes Element(e): %s", "untriggered": "\\{review}Nicht ausgelöstes Element(e): %s", }, "vsync": { "help": "Vertical Sync. Ein/Aus", }, "weather": { "help": "\\{review}Ändert den aktuellen Wettertyp.", "invalid": "\\{review}Ungültiges Wetter: %s (gültig: %s)", "set": "\\{review}Wetter auf %s eingestellt", }, "winston": { "dead": "Der Butler ist tot. Du Monster!", "spawn_failed": "Herbeirufen von Winston fehlgeschlagen", "spawned": "Winston herbeigerufen, nahe Lara", "teleported": "Winston herbeigerufen, nahe Lara", }, "wireframe": { "help": "Aktiviert und deaktiviert den Gitterrahmen-Render.", } } }, "dynamic": { "config_presets": { "tr1_pc": "\\{review}TR1 PC", "tr1_ps1": "\\{review}TR1 PS1", "tr2_pc": "\\{review}TR2 PC", "tr2_ps1": "\\{review}TR2 PS1", "tr3_pc": "\\{review}TR3 PC", "tr3_ps1": "\\{review}TR3 PS1", }, "enums": { "bar_look": { "tr1_pc": "TR1 PC", "tr2_pc": "\\{review}TR2 PC", "tr2_ps1": "\\{review}TR2 PS1", "tr3_pc": "\\{review}TR3 PC", "tr3_ps1": "TR3 PS1", }, "lara_outfit": { "default": "\\{review}Standard", "golden_sophia": "\\{review}Goldene Sophia", "sophia": "\\{review}Sophia", "tr1_bacon_lara": "\\{review}Bacon-Lara", "tr1_classic": "\\{review}TR1 Klassisch", "tr1_combo": "\\{review}TR1 Kombiniert", "tr1_golden_bacon_lara": "\\{review}Goldene Bacon-Lara", "tr1_golden_lara": "\\{review}TR1 Goldene Lara", "tr1_gym": "\\{review}TR1 Trainingsanzug", "tr1_mauled": "\\{review}TR1 Verletzt", "tr1_ngage": "\\{review}TR1 N-Gage", "tr23_golden_lara": "\\{review}TR2/3 Goldene Lara", "tr2_bomber_jacket": "\\{review}Bomberjacke", "tr2_classic": "\\{review}TR2 Klassisch", "tr2_diving_suit": "\\{review}Tauchanzug 1", "tr2_diving_suit_alpha": "\\{review}Tauchanzug 2", "tr2_gym": "\\{review}TR2 Trainingsanzug", "tr2_robe": "\\{review}Robe", "tr2_vegas": "\\{review}Las Vegas", "tr3_antarctica": "\\{review}Antarktis", "tr3_catsuit": "\\{review}Catsuit", "tr3_classic": "\\{review}TR3 Klassisch", "tr3_gym": "\\{review}TR3 Trainingsanzug", "tr3_nevada": "\\{review}Nevada", "tr3_south_pacific": "\\{review}Südpazifik", } }, "mods": { "tr1": { "title": "\\{review}Tomb Raider I", }, "tr1-demo-pc": { "title": "\\{review}Tomb Raider I Demo", }, "tr1-ub": { "title": "\\{review}Unvollendete Angelegenheiten", }, "tr2": { "title": "\\{review}Tomb Raider II", }, "tr2-gm": { "title": "\\{review}Die Goldene Maske", }, "tr3": { "title": "\\{review}Tomb Raider III", }, "tr3-la": { "title": "\\{review}Das Verlorene Artefakt", } } }, "enums": { "ALLY_HOSTILITY_POLICY": { "ALLY_HOSTILITY_POLICY_INDIVIDUAL": "\\{review}Individuell", "ALLY_HOSTILITY_POLICY_SHARED": "\\{review}Geteilt", }, "ASPECT_MODE": { "ASPECT_MODE_16_10": "16:10", "ASPECT_MODE_16_9": "16:9", "ASPECT_MODE_4_3": "4:3", "ASPECT_MODE_ANY": "\\{review}Auto", }, "BACKGROUND_TYPE": { "BK_BLACK": "\\{review}Schwarz", "BK_IMAGE": "\\{review}Bild", "BK_MONOCHROME": "\\{review}Monochrom", "BK_MONOCHROME_COOL": "\\{review}Monochrom (kühl)", "BK_MONOCHROME_WARM": "\\{review}Monochrom (warm)", "BK_NONE": "\\{review}Transparent", "BK_PATTERN_STATIC": "\\{review}Statisch", "BK_PATTERN_WAVE": "\\{review}Welle", "BK_TRANSPARENT_DARK": "\\{review}Sehr dunkel", "BK_TRANSPARENT_MEDIUM": "\\{review}Dunkel", }, "BAR_SHOW_MODE": { "BAR_SHOW_MODE_ALWAYS": "Immer", "BAR_SHOW_MODE_BOSS_ONLY": "Nur Boss-Gegner", "BAR_SHOW_MODE_NEVER": "Nie", }, "BILLBOARD_LOCK_MODE": { "BILLBOARD_LOCK_NONE": "\\{review}Keine", "BILLBOARD_LOCK_PERSPECTIVE": "\\{review}Perspektive", "BILLBOARD_LOCK_ROLL": "\\{review}Roll", "BILLBOARD_LOCK_ROLL_PITCH": "\\{review}Roll & Neigung", }, "BLOOD_EFFECTS": { "BLOOD_EFFECTS_DISABLED": "\\{review}Deaktiviert", "BLOOD_EFFECTS_PINK": "\\{review}Pink", "BLOOD_EFFECTS_RED": "\\{review}Rot", }, "CAMERA_MODE": { "CAMERA_MODE_TR1": "TR1", "CAMERA_MODE_TR2": "TR2", "CAMERA_MODE_TR3": "\\{review}TR3", }, "CREATURE_DROWN_POLICY": { "CREATURE_DROWN_POLICY_DEFAULT": "\\{review}Standard", "CREATURE_DROWN_POLICY_NEVER": "\\{review}Nie", "CREATURE_DROWN_POLICY_SUBMERGED": "\\{review}Untergetaucht", }, "INPUT_BACKEND": { "INPUT_BACKEND_CONTROLLER": "\\{review}Controller", "INPUT_BACKEND_KEYBOARD": "\\{review}Tastatur", }, "INPUT_ROLE": { "INPUT_ROLE_ACTION": "Aktion", "INPUT_ROLE_CAMERA_BACK": "Kamera zurück", "INPUT_ROLE_CAMERA_DOWN": "Kamera runter", "INPUT_ROLE_CAMERA_FORWARD": "Kamera Vorwärts", "INPUT_ROLE_CAMERA_LEFT": "Kamera Links", "INPUT_ROLE_CAMERA_RESET": "Kamera zurücksetzen", "INPUT_ROLE_CAMERA_RIGHT": "Kamera Rechts", "INPUT_ROLE_CAMERA_UP": "Kamera Hoch", "INPUT_ROLE_CHANGE_OUTFIT": "\\{review}Outfit wechseln", "INPUT_ROLE_CHANGE_TARGET": "Ziel wechseln", "INPUT_ROLE_CROUCH": "\\{review}Ducken", "INPUT_ROLE_CYCLE_LIGHTING_CONTRAST": "Beleuchtungs-Kontrast durchschalten", "INPUT_ROLE_DOWN": "Zurück", "INPUT_ROLE_DRAW_WEAPON": "Waffe ziehen", "INPUT_ROLE_ENTER_CONSOLE": "Entwickler-Konsole", "INPUT_ROLE_EQUIP_AUTOS": "Automatik-Pistolen ausrüsten", "INPUT_ROLE_EQUIP_DESERT_EAGLE": "\\{review}Desert Eagle ausrüsten", "INPUT_ROLE_EQUIP_GRENADE_LAUNCHER": "Granatenwerfer ausrüsten", "INPUT_ROLE_EQUIP_HARPOON": "Harpune ausrüsen", "INPUT_ROLE_EQUIP_M16": "M16-ausrüsten", "INPUT_ROLE_EQUIP_MAGNUMS": "Magnums ausrüsten", "INPUT_ROLE_EQUIP_MP5": "\\{review}MP5 ausrüsten", "INPUT_ROLE_EQUIP_PISTOLS": "Pistolen ausrüsten", "INPUT_ROLE_EQUIP_ROCKET_LAUNCHER": "\\{review}Raketenwerfer ausrüsten", "INPUT_ROLE_EQUIP_SHOTGUN": "Schrotflinte ausrüsten", "INPUT_ROLE_EQUIP_UZIS": "Uzis ausrüsten", "INPUT_ROLE_FLY_CHEAT": "Flug-Cheat", "INPUT_ROLE_FPS": "FPS anzeigen", "INPUT_ROLE_INVENTORY": "Inventar", "INPUT_ROLE_ITEM_CHEAT": "Item Cheat", "INPUT_ROLE_JUMP": "Springen", "INPUT_ROLE_LEFT": "Links", "INPUT_ROLE_LEVEL_SKIP_CHEAT": "Level überspringen", "INPUT_ROLE_LOAD": "\\{review}Laden", "INPUT_ROLE_LOOK": "Umsehen", "INPUT_ROLE_PAUSE": "Pause", "INPUT_ROLE_QUICK_LOAD": "\\{review}Schnellladen", "INPUT_ROLE_QUICK_SAVE": "\\{review}Schnellspeichern", "INPUT_ROLE_RIGHT": "Rechts", "INPUT_ROLE_ROLL": "Rolle", "INPUT_ROLE_SAVE": "\\{review}Speichern", "INPUT_ROLE_SCREENSHOT": "Bildschirmfoto", "INPUT_ROLE_SLOW": "Gehen", "INPUT_ROLE_SPRINT": "\\{review}Sprinten", "INPUT_ROLE_STEP_LEFT": "Seit-Schritt Links", "INPUT_ROLE_STEP_RIGHT": "Seit-Schritt Rechts", "INPUT_ROLE_SWITCH_BORDERS": "Wechsel zwischen Rahmen-Größen", "INPUT_ROLE_SWITCH_UPSCALING": "Wechsel zwischen Upscaling-Faktoren", "INPUT_ROLE_TOGGLE_BILINEAR_FILTER": "Bilinear-Filter Ein/Aus", "INPUT_ROLE_TOGGLE_FULLSCREEN": "Vollbild Ein/Aus", "INPUT_ROLE_TOGGLE_PHOTO_MODE": "Foto-Modus Ein/Aus", "INPUT_ROLE_TOGGLE_TEXTURES": "\\{review}Texturen umschalten", "INPUT_ROLE_TOGGLE_TRAPEZOID_FILTER": "Trapezoid-Filter Ein/Aus", "INPUT_ROLE_TOGGLE_UI": "UI Ein/Aus", "INPUT_ROLE_TOGGLE_WIREFRAME": "Drahtgitter Ein/Aus", "INPUT_ROLE_TURBO_CHEAT": "Turbo Geschwindigkeit", "INPUT_ROLE_UP": "Laufen", "INPUT_ROLE_USE_BIG_MEDI": "Großes Medi-Pack", "INPUT_ROLE_USE_FLARE": "Fackel", "INPUT_ROLE_USE_SMALL_MEDI": "Kleines Medi-Pack", }, "JUMP_LOCK_MODE": { "JUMP_LOCK_DISABLED": "Deaktiviert", "JUMP_LOCK_LEGACY": "Original", "JUMP_LOCK_TUNED": "Getuned", }, "LIGHTING_CONTRAST": { "LIGHTING_CONTRAST_HIGH": "Hoch", "LIGHTING_CONTRAST_LOW": "Niedrig", "LIGHTING_CONTRAST_MEDIUM": "Mittel", }, "LOADING_SCREENS_MODE": { "LOADING_SCREENS_ALWAYS": "\\{review}Immer", "LOADING_SCREENS_DISABLED": "\\{review}Deaktiviert", "LOADING_SCREENS_NEW_GAMES": "\\{review}Neue Spiele", }, "LOOK_MODE": { "LOOK_MODE_ENHANCED": "Erweitert", "LOOK_MODE_RESTRICTED": "Beschränkt", "LOOK_MODE_UNRESTRICTED": "Unbeschränkt", }, "MUSIC_LOAD_CONDITION": { "MUSIC_LOAD_CONDITION_ALWAYS": "Immer", "MUSIC_LOAD_CONDITION_NEVER": "Nie", "MUSIC_LOAD_CONDITION_NON_AMBIENT": "Nicht-Umgebung", }, "PROJECTILE_AREA_DAMAGE": { "PROJECTILE_AREA_DAMAGE_MULTI_SWEEP": "\\{review}Mehrfachfeger", "PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP": "\\{review}Einzelfeger", }, "QUICK_GUNS_MODE": { "QUICK_GUNS_MODE_DRAW_AND_HOLSTER": "Ziehen oder wegstecken", "QUICK_GUNS_MODE_DRAW_ONLY": "Nur ziehen", }, "SCREENSHOT_FORMAT": { "SCREENSHOT_FORMAT_JPEG": "JPG", "SCREENSHOT_FORMAT_PNG": "PNG", }, "SHADOW_TYPE": { "SHADOW_TYPE_CIRCLE": "\\{review}Kreis", "SHADOW_TYPE_OCTAGON": "\\{review}Achteck", "SHADOW_TYPE_SPRITE": "\\{review}Sprite", }, "STATS_STYLE": { "STATS_STYLE_BARE": "\\{review}Einfach", "STATS_STYLE_BORDERED": "\\{review}Mit Rahmen", }, "SUNGLASSES_MODE": { "SUNGLASSES_MODE_OFF": "\\{review}Aus", "SUNGLASSES_MODE_OPAQUE": "\\{review}Undurchsichtig", "SUNGLASSES_MODE_TRANSPARENT": "\\{review}Transparent", }, "TARGET_LOCK_MODE": { "TARGET_LOCK_MODE_FULL": "Volles Lock-On", "TARGET_LOCK_MODE_NONE": "Kein Lock-On", "TARGET_LOCK_MODE_SEMI": "Halbes Lock-On", }, "TEXTURE_FILTER": { "TEXTURE_FILTER_BILINEAR": "Bilinear", "TEXTURE_FILTER_POINT": "Aus", }, "UI_ELEMENT_LOCATION": { "UI_ELEMENT_LOCATION_BOTTOM_CENTER": "Unten Mitte", "UI_ELEMENT_LOCATION_BOTTOM_LEFT": "Unten links", "UI_ELEMENT_LOCATION_BOTTOM_RIGHT": "Unten rechts", "UI_ELEMENT_LOCATION_TOP_CENTER": "Oben Mitte", "UI_ELEMENT_LOCATION_TOP_LEFT": "Oben links", "UI_ELEMENT_LOCATION_TOP_RIGHT": "Oben rechts", }, "UI_STYLE": { "UI_STYLE_PC": "PC", "UI_STYLE_PS1": "PS1", }, "WALL_GLITCH_MODE": { "WALL_GLITCH_FIXED": "Gefixt", "WALL_GLITCH_TR1": "TR1", "WALL_GLITCH_TR2": "TR2", } }, "settings": { "audio.ambient_volume": { "title": "\\{review}Umgebungslautstärke", "description": "\\{review}Passt die Umgebungslautstärke an.", }, "audio.cutscene_volume": { "title": "\\{review}Zwischensequenz-Lautstärke", "description": "\\{review}Passt die Lautstärke der Zwischensequenzen im Spiel an.", }, "audio.enable_lara_mic": { "title": "Mikrofon nah an Lara", "description": "Setzt das Mikrofon auf Laras Position. Wenn deaktiviert, wird das Mikrofon auf der Position der Kamera gesetzt.", }, "audio.enable_music_in_inventory": { "title": "\\{review}Musik im Inventar abspielen", "description": "\\{review}Lässt Spielgeräusche, Umgebungsgeräusche und Musik im Inventarbildschirm weiterlaufen.", }, "audio.enable_music_in_menu": { "title": "Hauptmenü-Musik", "description": "Spielt Musik im Hauptmenü ab.", }, "audio.enable_pitched_sounds": { "title": "Gepitchte Sounds", "description": "Erlaubt die zufällige Wiedergabe von Sound-Effekten, leicht gepitched, um die Spiel-Sounds zu variieren.", }, "audio.enable_ps1_sfx": { "title": "\\{review}PS1 SFX-Ersetzungen", "description": "\\{review}Aktiviert bestimmte Soundeffekt-Ersetzungen mit PS1-Äquivalenten.\n\n- Uzi-Feuer (nur TR1)\n- Laras Barfußgeräusche (nur TR2)", }, "audio.enable_underwater_anim_sfx": { "title": "Tauch-Animations Sounds", "description": "Erlaubt Kontrolle über das Abspielen von bestimmten Animationen und Souneffekten - für Objekte, wie zum Beispiel Türen und Falltüren - wenn die Kamera unter Wasser ist.", }, "audio.fix_chainblock_secret_sound": { "title": "Fix: Falsch abspielender Sound", "description": "Verhindert, dass der Geheimnis-Ton inkorrekterweise abgespielt wird, wenn man den Goldenen Schlüssel im Grab von Tihocan benutzt.", }, "audio.fix_secrets_killing_music": { "title": "Überlagernde Geheimnis-Musik", "description": "Fixt dass der Ton vom Geheimnis-Einsammeln den momentan spielenden Musik-Titel unterbricht.", }, "audio.fix_speeches_killing_music": { "title": "Überlagernde Feind-Sprache", "description": "Fixt dass Feinde beim Sprechen den momentan spielenden Musiktitel unterbrechen.", }, "audio.fmv_volume": { "title": "\\{review}FMV-Lautstärke", "description": "Passt die Lautstärke der Videosequenzen an.", }, "audio.inventory_ambient_volume": { "title": "\\{review}Umgebungslautstärke (Inventar)", "description": "\\{review}Passt die Umgebungslautstärke im Inventarbildschirm an.", }, "audio.inventory_music_volume": { "title": "\\{review}Musiklautstärke (Inventar)", "description": "\\{review}Passt die Musiklautstärke im Inventarbildschirm an.", }, "audio.load_music_triggers": { "title": "Fix: Trigger für Musik, die nur einmal spielen soll", "description": "Lädt vorher getriggerte Musik, so dass Musik, die nur einmal spielen soll, nicht ein weiteres Mal abgespielt wird.", }, "audio.master_volume": { "title": "\\{review}\\{icon music} Hauptlautstärke", "description": "\\{review}Passt die gesamte Spiel-Lautstärke an. Die restlichen Einstellungen sind relativ zu dieser Lautstärke.", }, "audio.music_load_condition": { "title": "Musik beim Laden wiederherstellen", "description": "Lädt den Musiktitel der vor dem Speichern des Spiels abgespielt wurde.\n\n- Nie: keine Musik-Titel beim laden wiederherstellen.\n- Nicht-Umgebung: stellt nur Nicht-Umgebungs-Musiktitel beim laden wieder her.\n- Immer: stellt jede Art von Musiktitel beim Laden wieder her.", }, "audio.music_volume": { "title": "\\{review}Musiklautstärke", "description": "\\{review}Passt die Musiklautstärke an.", }, "audio.mute_out_of_focus": { "title": "Ton stummschalten bei Fokusverlust", "description": "Schaltet alle Musik und Soundeffekte stumm wenn das Fenster des Spiels nicht fokusiert ist.", }, "audio.sound_volume": { "title": "\\{icon sound} Sound-Lautstärke", "description": "Soundeffekt-Lautstärke einstellen.", }, "audio.underwater_ambient_volume": { "title": "\\{review}Umgebungslautstärke (unter Wasser)", "description": "\\{review}Passt die Umgebungslautstärke unter Wasser an.", }, "audio.underwater_music_volume": { "title": "\\{review}Musiklautstärke (unter Wasser)", "description": "\\{review}Passt die Musiklautstärke unter Wasser an.", }, "debug.enable_endless_flare_time": { "title": "\\{review}Unendliche Fackelzeit", "description": "\\{review}Verhindert, dass die tragbaren Fackeln jemals erlöschen. Geworfene Fackeln erlöschen jedoch weiterhin wie gewohnt.", }, "debug.enable_endless_sprint": { "title": "\\{review}Endloses Sprinten", "description": "\\{review}Verhindert, dass Lara beim Sprinten jemals müde wird. Hindernisse bringen sie jedoch weiterhin zum Stillstand.", }, "gameplay.ally_hostility_policy": { "title": "\\{review}Verbündeten-Feindseligkeitspolitik", "description": "\\{review}Steuert, wie freundliche Einheiten reagieren, wenn sie Schaden nehmen.\n\n- Individuell: Jeder Verbündete ändert seine Feindseligkeit eigenständig (TR3-Stil).\n- Geteilt: Alle Verbündeten werden gemeinsam feindlich (TR2-Mönch-Stil).", }, "gameplay.camera_speed": { "title": "Kamera-Geschwindigkeit", "description": "Ändert, wie schnell sich die Kamera bewegt.", }, "gameplay.change_pierre_spawn": { "title": "Ändere Pierre Spawn-Modus", "description": "Erstellt einen gerade erst ausgelösten (flüchtenden) Pierre und ersetzt einen bereits existierenden (flüchtenden) Pierre.", }, "gameplay.creature_drown_policy": { "title": "\\{review}Kreaturen-Ertrinkverhalten", "description": "\\{review}Steuert, wie sich Landkreaturen in Wasserräumen verhalten.\n\n- Nie: Landkreaturen ertrinken niemals (TR1-Stil).\n- Standard: Landkreaturen ertrinken in Wasser mit einer Tiefe von 2 Klicks oder mehr (TR2/3-Stil).\n- Untergetaucht: Landkreaturen ertrinken nur, wenn sie vollständig untergetaucht sind.", }, "gameplay.disable_extra_guns": { "title": "\\{review}Entferne zusätzliche Waffen", "description": "Entfernt alle Waffen- und Munitions-Pickups aus dem Spiel, außer Pistolen (für Pistolen-Only-Challenge-Läufe).", }, "gameplay.disable_healing_between_levels": { "title": "Bleibender Schaden", "description": "Verhindert, dass Lara beim Starten eines neuen Levels geheilt wird (für \"No Heal Challenge\" Runs).", }, "gameplay.disable_medpacks": { "title": "Entferne Medi-Packs", "description": "Entfernt alle Medi-Packs aus dem Spiel (für \"No Meds Challenge\" Runs).", }, "gameplay.disable_trex_collision": { "title": "Enferne die Kollision des toten T-Rex", "description": "Enfernt jegliche Kollision vom T-Rex sobald er stirbt. Dies ist sehr hilfreich, wenn die Leiche eines T-Rex den Ausgang blockiert.", }, "gameplay.enable_ally_targeting": { "title": "Erlaube das ziehlen auf freundliche Einheiten", "description": "Erlaubt Lara, auf freundliche Einheiten zu ziehlen, wie zum Beispiel Mönche. Wenn diese Option deaktiviert ist, sind freundliche Einheiten immun gegen Laras Munition.", }, "gameplay.enable_auto_item_selection": { "title": "Vorausgewählte Schlüssel-Items", "description": "Wenn Lara ein Schlüsselloch oder einen Rätsel-Slot verwendet und sie das dazu passende Item in ihrem Inventar hat, wird dieses automatisch ausgewählt.", }, "gameplay.enable_back_slope_stumble": { "title": "\\{review}Stolpern an rückwärtiger Schräge", "description": "\\{review}Lässt Lara stolpern, wenn sie rückwärts springt und sich hinter ihr eine Schräge befindet (TR3). Wenn deaktiviert, kommt Lara abrupt an der Schräge zum Stehen (TR1/2).", }, "gameplay.enable_body_bags": { "title": "\\{review}Leichensack-Auslöser", "description": "\\{review}Ermöglicht das Entfernen getöteter Gegner, wenn Lara in bestimmten Levels bestimmte Auslöser überquert. Wenn deaktiviert, werden tote Gegner immer angezeigt.", }, "gameplay.enable_boulder_shake": { "title": "\\{review}Felskamerawackeln aktivieren", "description": "\\{review}Wenn aktiviert, wackelt die Kamera, sobald sich ein Fels bewegt.", }, "gameplay.enable_bouncy_grenades": { "title": "\\{review}Sprunggranaten", "description": "\\{review}Aktiviert das Granatenverhalten im Stil von TR3: Sie prallen von Wänden und Hängen ab und erzeugen einen größeren Explosionsradius, jedoch auf Kosten einer reduzierten Geschwindigkeit.", }, "gameplay.enable_cheats": { "title": "Cheats", "description": "Aktiviert verschiedene Cheats:\n\n- L: Beende das Level sofort.\n- I: eine Erhöhung der Anzahl der Munition und Medipacks; und alle plotrelevanten Items des aktuellen Levels.\n- O: Aktiviere Flug-Cheat (in der Luft schwimmen).\n - GEHEN-Taste: beende Flug-Modus.\n - WAFFE ZIEHEN-Taste: öffne die nächstgelegene Tür (funktioniert an ein paar Stellen nicht).", }, "gameplay.enable_cinematics": { "title": "\\{review}Skriptsequenzen", "description": "\\{review}Aktiviert Skriptsequenzen zu Beginn bestimmter Level, sofern vorhanden.", }, "gameplay.enable_compass_stats": { "title": "Levelstatistiken im Kompass", "description": "Aktiviert die Anzeige von Levelstatistiken wenn der Kompass ausgewählt wird.", }, "gameplay.enable_console": { "title": "Konsole", "description": "Aktiviert die Entwickler-Konsole.", }, "gameplay.enable_controlled_drops": { "title": "\\{review}Kontrollierter Fall", "description": "\\{review}Ermöglicht Lara, sich in der Luft zu drehen und den Vorsprung zu greifen, den sie gerade verlassen hat, wenn die Aktionstaste beim Fallen gehalten wird.", }, "gameplay.enable_crawl_jump": { "title": "\\{review}Crawl-Absprung", "description": "\\{review}Ermöglicht es Lara, aus Kriechgängen herauszuspringen.", }, "gameplay.enable_crawl_tilt": { "title": "\\{review}Krabbelneigung", "description": "\\{review}Richtet Laras Rotation beim Krabbeln an der Bodengeometrie aus.", }, "gameplay.enable_crawling": { "title": "\\{review}Kriechen", "description": "\\{review}Ermöglicht es Lara, sich zu ducken und zu kriechen.", }, "gameplay.enable_credits": { "title": "Credits", "description": "Ermöglicht die Anzeige der Credits nachdem man das Spiel durchgespielt hat. Beeinflusst nicht die Anzeige der Finalen Statistik.", }, "gameplay.enable_crouch_roll": { "title": "\\{review}Hockrolle", "description": "\\{review}Erlaubt Lara, aus der Hocke mit der Sprinttaste eine Vorwärtsrolle zu machen.", }, "gameplay.enable_cutscenes": { "title": "Zwischensequenzen", "description": "Aktiviert das Abspielen von Zwischensequenzen.", }, "gameplay.enable_demo": { "title": "Demo-Modus", "description": "Aktiviert die Anzeige von Demos im Hauptmenü.", }, "gameplay.enable_enemy_rotation": { "title": "Zufällige Startwinkel für Gegner", "description": "Verwendet einen zusätzlichen zufälligen Startwinkel für ein paar Gegner, sobald sie initialisiert werden.", }, "gameplay.enable_enhanced_saves": { "title": "Speichere Effekte", "description": "Erweitert Spielstände so das Grafikeffekte, Wasserfall-Nebel, Flammenerzeuger und mehr gespeichert werden, anstatt beim Laden zu verschwinden.", }, "gameplay.enable_fmv": { "title": "FMVs", "description": "Aktiviert das Abspielen von FMVs.", }, "gameplay.enable_game_modes": { "title": "Spielmodi-Auswahl", "description": "Erlaubt die Auswahl der 'Neues Spiel Plus'-Optionen im 'Neues Spiel' Ausweis-Menü.\n\n- Neues Spiel+: schaltet alle Waffen mit unendlich Munition frei; Feinde haben verdoppelte Lebenserenergie.\n- Japanisch NS: Waffen verursachen doppelten Schaden und Fackel-Pickups enhalten 8 statt 6 Fackeln.\n- Japanisch NS+: eine Kombination aus Neus Spiel+ und Japanisch NS.", }, "gameplay.enable_idle_pose_camera": { "title": "\\{review}Leerlauf-Kamera", "description": "\\{review}Passt die Kamera an, sodass sie während der Posenanimation auf Lara gerichtet ist. Durch Drücken von Schauen wird die Kamera zurückgesetzt.", }, "gameplay.enable_inverted_look": { "title": "Invertiertes Umsehen", "description": "Invertiert die Y-Axe von Laras Sichtsteuerung.", }, "gameplay.enable_item_examining": { "title": "Item-Untersuchung", "description": "Für benutzergenerierte Level - erlaubt die Anzeige von Item-Beschreibungen im Inventar, wenn der Ersteller des Levels die nötigen Daten zur Verfügung gestellt hat.", }, "gameplay.enable_jump_twists": { "title": "Sprungdrehungen", "description": "Aktiviert TR2+-artige Sprungdrehungen und Purzelbäume, das heißt: Drücke Rolle während des Sprungs und Kopfsprung-Animationen.", }, "gameplay.enable_killer_pushblocks": { "title": "\\{review}Tödliche Schiebeblöcke aktivieren", "description": "Aktiviert, dass Lara sofort stirbt, wenn ein Schiebeblock von oben auf Lara herab fällt. Andernfalls wird Lara oben auf dem Block hängen bleiben und überleben.", }, "gameplay.enable_lean_jumping": { "title": "Schmales Springen", "description": "Erlaubt Lara, sich weiter voran oder rückwärts zu tasten, wenn man eine der Richtungstasten gedrückt hält, während sie einen neutralen Sprung ausführt.", }, "gameplay.enable_ledge_jumps": { "title": "\\{review}Vorsprung-Sprünge", "description": "\\{review}Ermöglicht Lara, nach oben oder rückwärts zu springen, während sie an einem Vorsprung hängt, sofern sich eine feste Fläche vor ihr befindet, um sich abzustoßen.", }, "gameplay.enable_legal": { "title": "Rechtshinweise", "description": "Aktiviert die Rechtshinweise und die Core Design FMV beim Starten des Spiels.", }, "gameplay.enable_manual_camera": { "title": "\\{review}Manuelle Kamera", "description": "\\{review}Aktiviert die Kameratasten (\\{input camera_forward}\\{input camera_back}\\{input camera_left}\\{input camera_right}), die zur Steuerung der Kamera im Fotomodus verwendet werden, um auch die Ingame-Kamera zu drehen.", }, "gameplay.enable_neutral_twists": { "title": "\\{review}Neutrale Drehung", "description": "\\{review}Ermöglicht Lara eine Drehung in der Luft bei einem Sprung aus dem Stand. Drücke gleichzeitig die Sprung- und Rollentaste, während du stillstehst.", }, "gameplay.enable_pickup_aids": { "title": "Gegenstände hervorheben", "description": "Aktiviert einen periodischen Funkel-Effekt in der Nähe von Items, um ihre Gegenwart sichtbarer zu machen.", }, "gameplay.enable_play_previous_levels": { "title": "\\{review}Vorherige Level spielen", "description": "\\{review}Aktiviert die Funktionen \"Vorherige Level spielen\" und \"Bisherige Geschichte...\" im Auswahlbildschirm für Neues Spiel.", }, "gameplay.enable_responsive_crawl": { "title": "\\{review}Reaktionsschnelles Kriechen", "description": "\\{review}Aktiviert Verbesserungen gegenüber der ursprünglichen Kriechmechanik.\n\n- Ermöglicht es, nach dem Anhalten schneller wieder mit dem Kriechen zu beginnen.\n- Ermöglicht den Übergang vom Laufen/Sprinten zum Kriechen, ohne zuvor anzuhalten.\n- Ermöglicht den Übergang vom Kriechen zur Kriechrolle (falls aktiviert), ohne sich zuvor manuell zu ducken.\n- Ermöglicht das Drehen im geduckten Zustand.\n- Stellt Laras Kriech-Aufhebanimation wieder her (Fackeln ausgenommen).", }, "gameplay.enable_responsive_sprint": { "title": "\\{review}Reaktives Sprinten", "description": "\\{review}Aktiviert einen reaktiveren Sprintzustand für Lara.\n\n- ermöglicht Sprinten, sobald Lara Energie hat, statt nur bei voller Ausdauer.\n- erlaubt Sprinten auf Treppen, ohne von Laras normaler Laufanimation unterbrochen zu werden.", }, "gameplay.enable_save_crystals": { "title": "Speicherkristalle", "description": "Beschränkt Speichern auf den Anfang des Levels und Speicherkristalle. Level haben eine limitierte Anzahl von Speicherkristallen. Die Speicherkristalle funktionieren nur einmal, so wie auf der PS1. Eine Änderung dieser Option erfordert einen Neustart.", }, "gameplay.enable_slide_to_run": { "title": "\\{review}Rutsch-Lauf-Übergang", "description": "\\{review}Ermöglicht es Lara, sofort loszulaufen, sobald sie nach dem Vorwärtsrutschen auf einer Schräge den Boden erreicht. Halte die Vorwärts-Taste gedrückt, um dies zu aktivieren.", }, "gameplay.enable_slow_ledge_swing": { "title": "\\{review}Langsames Schwingen an einer Kante", "description": "\\{review}Ermöglicht es Lara, langsam zu schwingen, wenn sie sich an einer sehr dünnen Kante festhält (TR3-Stil). Ist die Option deaktiviert, schwingt Lara nur kurz und kehrt dann in eine ruhende Hängeposition zurück (TR1/2-Stil).", }, "gameplay.enable_smooth_wall_deflect": { "title": "Sanftes Wand-Abprallen", "description": "Erlaubt Lara, sich schneller zu fangen, nachdem sie gegen eine Wand gestoßen ist und eine der Richtungstasten zusammen mit der Vorwärtstaste gedrückt gehalten wird..", }, "gameplay.enable_soft_statics": { "title": "\\{review}Weiche Netz-Kollision", "description": "\\{review}Ermöglicht es Lara, sich sanft gegen statische Meshes zu bewegen – ähnlich wie in TR4+ – anstatt abrupt anzuhalten.", }, "gameplay.enable_sprint": { "title": "\\{review}Sprinten", "description": "\\{review}Ermöglicht Lara zu sprinten, ähnlich wie in TR3+.", }, "gameplay.enable_step_roll_boost": { "title": "Stufen-Roll-Boost", "description": "Erlaubt Lara von einer ein-Click hohen Stufe (standard) geboostet zu werden, wenn nahe der Stufen-Kante Rollen gedrückt wird.", }, "gameplay.enable_swing_cancel": { "title": "Schwingen unterbrechen", "description": "Erlaubt, Laras Kanten-Schwinganimation abzubrechen, indem man kurz loslässt und sich schnell wieder festhält.", }, "gameplay.enable_target_change": { "title": "Zielwechsel", "description": "Ermöglicht TR4+ artiges wechseln zwischen den Zielen, während man mit den Waffen auf sie zielt. Drücke die Zielwechsel-Taste während des ziehlens, um das anvisierte Ziel zu wechseln.", }, "gameplay.enable_timer_in_inventory": { "title": "Timer zählt im Inventar", "description": "Lässt den In-Game-Timer weiterlaufen, selbst wenn während des Spiels das Inventar geöffnet ist.", }, "gameplay.enable_toggle_crouch": { "title": "\\{review}Ducken umschalten", "description": "\\{review}Ermöglicht es Lara, nach einmaligem Drücken der Ducken-Taste geduckt zu bleiben. Drücke Ducken erneut, um wieder aufzustehen.", }, "gameplay.enable_toggle_sprint": { "title": "\\{review}Sprint umschalten", "description": "\\{review}Ermöglicht es Lara, nach einmaligem Drücken der Sprint-Taste weiterzulaufen. Drücke erneut Sprint, um das Sprinten zu beenden.", }, "gameplay.enable_total_stats": { "title": "Finale Statistik-Anzeige", "description": "Aktiviert die Erstellung einer Gesamtstatistik vom ganzen Spiel, die nach dem Abspielen der Credits angezeigt wird.", }, "gameplay.enable_tr2_jumping": { "title": "Reaktionsschnelleres Springen", "description": "\\{review}Erlaubt Lara, während des Rennens jederzeit zu springen.", }, "gameplay.enable_tr2_swim_cancel": { "title": "Reaktionsschneller Schwimm-Abbruch", "description": "Erlaubt Lara, unter Wasser schneller stoppen zu können, wenn die Springen-Taste losgelassen wird.", }, "gameplay.enable_tr2_swimming": { "title": "Flüssiges Schwimmverhalten", "description": "Verpasst Laras Drehgeschwindigkeit unter Wasser eine Beschleunigungskurve für ein runderes Steuerungsverhalten, ähnlich wie in TR2+.", }, "gameplay.enable_uw_roll": { "title": "Rolle unter Wasser", "description": "Erlaubt Lara, eine Rolle unter Wasser durchzuführen, ähnlich wie in TR2+.", }, "gameplay.enable_wading": { "title": "Durchwaten", "description": "\\{review}Erlaubt Lara, flaches Wasser zu durchwaten, anstatt an der Wasseroberfläche stecken zu bleiben.", }, "gameplay.enable_walk_to_items": { "title": "Animierte Interaktionen", "description": " Lässt Lara zu Pickups und Schaltern hingehen, anstatt sie zu ihnen zu teleportieren.", }, "gameplay.fix_alligator_ai": { "title": "Fix: Alligator-AI", "description": "Fixt, dass die Alligatoren keinen Schaden machen, wenn Lara sich im Wasser befindet und sich nicht bewegt.", }, "gameplay.fix_bear_ai": { "title": "Fix: Bären-AI", "description": "Fixt, dass die aufrecht stehend ausgeführten Prankenhiebe von Bären Lara nicht mehr verfehlen.", }, "gameplay.fix_bridge_collision": { "title": "Fix: Brückenkollision", "description": "Fixt, dass Lara nicht in der Lage ist, sich an einigen Teilen von Brücken festzuhalten und unsichtbare Wände an den Kanten. Fixt auch Kollisions-Fehler mit Zugbrücken, Falltüren, und Brücken, wenn sie übereinander gestapelt sind, über Schrägen, und nahe dem Boden.", }, "gameplay.fix_descending_glitch": { "title": "Fix: Zerbrechliche Böden-Stürze", "description": "Fixt, dass Seit-Schritte und Vorwärtslaufen auf zerbrechlichen Platten dazu führen, dass Lara sofort herab zur darunter liegenden Platte fällt.", }, "gameplay.fix_flare_throw_priority": { "title": "Fix: Fackel-Wurf-Priorität", "description": "Fixt, dass Lara das Wegwerfen einer aufgebrauchten Fackel priorisiert, während sie sich in der Luft befindet. Dies kann manchmal dazu führen, dass sie nicht in der Lage ist, sich an Kanten festzuhalten.", }, "gameplay.fix_floor_data_issues": { "title": "Fix: Floor-Data-Fehler", "description": "Fixt originale Fehler mit Boden Daten/Triggern.", }, "gameplay.fix_free_flare_glitch": { "title": "Fix: gratis Fackel-Glitch", "description": "Fixt die Möglichkeit, eine gratis Fackel zu erschaffen, wenn man während des Aufhebens eines Gegenstandes die Fackel-Taste drückt.", }, "gameplay.fix_item_duplication_glitch": { "title": "Fix: Item-Vervielfältigungs-Glitch", "description": "Fixt die Möglichkeit, Schlüsselgegenstände im Inventar mehrfach nutzen zu können.", }, "gameplay.fix_lara_pickup_embed": { "title": "Fix: Pickup-Fehler", "description": "\\{review}Behebt ein Problem, bei dem Lara manchmal in Wände driftet, wenn sie unter Wasser Gegenstände einsammelt, sowie beim Einsammeln von Gegenständen über Wasser unter steil geneigten Decken.", }, "gameplay.fix_m16_accuracy": { "title": "\\{review}Fix: M16/MP5-Genauigkeit", "description": "\\{review}Fixt die Genauigkeit des M16/MP5, während Lara rennt.", }, "gameplay.fix_monkey_pickup_priority": { "title": "\\{review}Behebe die Priorität beim Aufheben von Affen", "description": "\\{review}Angegriffene Affen werden das Zurückschlagen priorisieren, bevor sie Medi-Packs und Schlüssel aufsammeln.", }, "gameplay.fix_pipeman_aim": { "title": "\\{review}Blasrohr-Zielen fixen", "description": "\\{review}Behebt ein Problem, bei dem der Blasrohrschütze Lara manchmal nicht korrekt mit Pfeilen anvisieren kann.", }, "gameplay.fix_qwop_glitch": { "title": "Fix: QWOP-Glitch", "description": "Fixt, dass Lara auf kleinen Stufen springt, was manchmal in einer seltsamen Lauf-Animation resultiert, bekannt als QWOP-Zustand.", }, "gameplay.fix_step_glitch": { "title": "Fix: Stufen-Glitch", "description": "Fixt, dass Lara manchmal in Wände, die an Treppen angrenzen, gedrückt wird, wenn sie auf eine bestimmte Weise an ihnen entlangläuft.", }, "gameplay.fix_wade_wall_hit": { "title": "Fix: Wand-Treffer beim Waten", "description": "Fixt, dass Lara beim waten nicht reagiert, wenn sie eine Wand trifft.", }, "gameplay.fix_walk_run_jump": { "title": "Fix: Gehen-Laufen-Sprung", "description": "Fixt, dass Lara manchmal nicht sofort springen kann, direkt nachdem sie von ihrer Gehen-Animation zur Laufen-Animation wechselt.", }, "gameplay.fix_wall_geometry": { "title": "\\{review}Behebt Wandgeometrie", "description": "\\{review}Behebt Fälle in der OG-Levelgeometrie, bei denen Neigungen innerhalb von Wänden zu ungenauen Höhenberechnungen führen können.", }, "gameplay.fix_water_exit": { "title": "Wasser Ausstieg", "description": "Fixt dass Lara direkt in einen angrenzenden, trockenen Raum oder in einen trockenen Raum unterhalb gelangen kann. Zusätzlich verhindert es, dass Lara aus dem Wasser heraus auf nicht begehbare Schrägen klettern kann.", }, "gameplay.harpoon_recoil": { "title": "Harpunenrückstoß", "description": "Legt fest, wie oft Lara die Harpune nachladen muss, basierend auf ihrer momentanen Munitionsanzahl. Zum Beispiel, wenn es auf 3 festgelegt ist, muss sie nach jedem dritten Schuss nachladen. Auf 0 eingestellt, deaktiviert das Nachladen vollständig.", }, "gameplay.idle_pose_timeout": { "title": "\\{review}Leerlauf-Zeit", "description": "\\{review}Ermöglicht Lara, nach der angegebenen Anzahl an Sekunden Inaktivität eine Posenanimation zu starten. Auf 0 setzen, um zu deaktivieren.", }, "gameplay.jump_lock_mode": { "title": "Sprung-Block Modus", "description": "Erlaubt einzustellen, wie früh es Lara erlaubt ist zu springen, nachdem sie begonnen hat zu laufen.\n\n- Original: entspricht dem originalen TR2 Timing.\n- Getuned: springen ist 2 Frames früher möglich.\n- Deaktiviert: springen ist sofort nachdem die Laufen-Animation begonnen hat möglich.", }, "gameplay.loading_screens": { "title": "Ladebildschirme", "description": "\\{review}Steuert Ladebildschirme vor dem Laden eines Levels.\n\n- Deaktiviert: Ladebildschirme nie anzeigen.\n- Immer: Ladebildschirme anzeigen.\n- Neue Spiele: Ladebildschirme beim Laden eines Spielstands überspringen.", }, "gameplay.look_mode": { "title": "Umsehen Modus", "description": "Erlaubt einzustellen wann Lara in der Lage ist sich umzusehen.\n\n- Beschränkt: Umsehen ist nur möglich wenn Lara still steht, und nie wenn man unter Wasser ist.\n- Erweitert: Umsehen ist während der meisten Animationen möglich, ausser ein paar Ausnahmen wie das Schieben eines Blockes.\n- Unbeschränkt: Umsehen ist jederzeit während der normalen Steuerung von Lara möglich.", }, "gameplay.maximum_quick_save_slots": { "title": "\\{review}Anzahl der Schnellspeicherplätze", "description": "\\{review}Ändert die Anzahl der verfügbaren Schnellspeicherplätze.", }, "gameplay.maximum_save_slots": { "title": "Anzahl der Speicher-Slots", "description": "Ändert die Anzahl der verfügbaren Speicher-Slots.", }, "gameplay.pause_on_focus_lost": { "title": "\\{review}Pause bei Fokusverlust", "description": "\\{review}Stoppt den Spielfortschritt, wenn das Spiel-Fenster den Fokus verliert.", }, "gameplay.projectile_area_damage": { "title": "\\{review}Flächenschaden durch Projektile", "description": "\\{review}Steuert, wie der Wirkungsbereich für Raketenwerfer und Granatwerfer sich ausbreitet.\n\n- Einzelfeger: Verhalten von TR1 & TR2.\n- Mehrfachfeger: Verhalten von TR3.\n\nDie Mehrfachfeger-Option führt oft zu doppeltem Schaden bei einzelnen Gegnern.", }, "gameplay.remember_gun_status": { "title": "Waffen zwischen den Leveln merken", "description": "Bringt Lara dazu, sich zu erinnern, welche Waffe sie als Letztes im vorherigen Level verwendet hat, wenn ein neues Level beginnt. Bei Deaktivierung wird Lara auf geholsterte Pistolen zurückgreifen.", }, "gameplay.restore_ps1_enemies": { "title": "PS1 Feinde wiederherstellen", "description": "\\{review}Fügt die Mumie hinzu, die in der PlayStation-Version von City of Khamoon, Raum 25, erscheint.\nDas Ändern dieser Option erfordert einen Neustart des Spiels.", }, "gameplay.start_lara_hitpoints": { "title": "Laras Lebensenergie beim Starten eines Levels", "description": "Legt die Höhe des Gesundheitswertes von Lara für den Start eines jeden Levels fest.", }, "gameplay.target_mode": { "title": "Modis für automatische Anvisierung", "description": "Ändert das automatische Anvisierungsverhalten von Laras Waffen gegenüber Zielen.\n\n- Volles Lock-On: Das Ziel immer bleibt anvisiert, selbst wenn sich der Feind außer Sicht ist oder stirbt (OG TR1-3).\n- Halbes Lock-On: Das Ziel bleibt anvisiert wenn der Feind sich außer Sicht bewegt, löst aber die Anvisierung sobald der Gegner stibt.\n- Kein Lock-On: Löse die Anvisierung sobald der Feind sich außer Sicht bewegt oder stibt (TR4+).", }, "gameplay.wall_glitch_mode": { "title": "Wand-Glitch Ein/Aus", "description": "Erlaubt, das Wand-Glitchverhalten von TR1 zu nutzen und umgekehrt; gleichermaßen erlaubt es, alle Arten von Wand-Glitches zu fixen.", }, "input.enable_buffering_func_keys": { "title": "\\{review}Pufferung (F-Tasten)", "description": "\\{review}Aktiviert F-Tasten (1-Frame) Pufferung, um eine präzise Steuerung von Laras Bewegung zu ermöglichen. Diese Funktion existiert ursprünglich nur im TombATI-Port (TR1).", }, "input.enable_buffering_inventory": { "title": "\\{review}Pufferung (Inventar)", "description": "\\{review}Aktiviert die Pufferung des Inventars (2 Frames), um eine präzise Steuerung von Laras Bewegung zu ermöglichen.", }, "input.enable_responsive_passport": { "title": "Reaktionsfähiger Ausweis", "description": "Deaktiviert, dass man die Umblättern-Animation des Ausweises abwarten muss, sie können nun so schnell umblättern, wie sie wollen.", }, "input.enable_tr3_sidesteps": { "title": "Erweiterte Seit-Schritte", "description": "Aktiviert Seitschritte im Stil von TR3+, e. g. Shift + Richtungspfeile. Deaktiviert die Seitschritt-Tasten.", }, "input.quick_guns_mode": { "title": "Schnell-Zieh-Tasten", "description": "Bestimmt das Verhalten der Schnell-Zieh-Tasten.\n\n- Nur ziehen: eine der Tasten zu drücken sorgt dafür, dass Lara die zugewiesene Waffe ausrüstet.\n- Ziehen oder wegstecken: das selbe wie bei 'Nur Ziehen', zusätzlich wird Lara die zugewiesene Waffe, die sie gerade benutzt, wegstecken.", }, "language": { "title": "Sprache", "description": "Ändert die Sprache des UI-Textes.", }, "rendering.anisotropy_filter": { "title": "Anisotropischer Filter", "description": "Erwiterte Textur-Filterung auf Entfernung.", }, "rendering.aspect_mode": { "title": "Seitenverhältnis", "description": "Erzwingt bestimmte Seitenverhältnisse mit Letterbox.", }, "rendering.borders": { "title": "Rahmen", "description": "Fügt schwarze Rahmen um das Spiel-Fenster herum hinzu.", }, "rendering.enable_trapezoid_filter": { "title": "Trapezoid-Filter", "description": "Korrigiert das Rendern von Vierecken.", }, "rendering.enable_vsync": { "title": "VSync", "description": "Schaltet V-Sync an oder aus.", }, "rendering.fps": { "title": "FPS", "description": "Stellt Spiel-Frames pro second ein.", }, "rendering.lighting_contrast": { "title": "Beleuchtungs-Kontrast", "description": "Erhöht den Kontrast für dynamische Lichtquellen, wie zum Beispiel Fackeln und Mündungsfeuer.", }, "rendering.screenshot_format": { "title": "Bildschirmfoto-Format", "description": "Dateiformat des Bildschirmfotos.", }, "rendering.sprite_lock_mode": { "title": "\\{review}Sprite-Sperrmodus", "description": "\\{review}Steuert, welche Achsen beim Anzeigen von Sprites auf dem Bildschirm gesperrt werden.\n\n- Keine: Sprites normal anzeigen.\n- Roll: Rollachse sperren – nur im Fotomodus nützlich.\n- Roll & Neigung: Sicherstellen, dass die Sprites aufrecht stehen und nicht auf dem Boden liegen, wenn man sie von oben betrachtet.\n- Perspektive: Roll- und Neigungsachsen sperren und zusätzlich die Sprites leicht zur Bildschirmmitte hin drehen.", }, "rendering.texture_filter": { "title": "Textur-Filter", "description": "Wechselt zwischen weichgezeichneten und pixeligen Objekt-Texturen.", }, "rendering.ui_filter": { "title": "UI-Filter", "description": "Schaltet zwischen weichgezeichneten und pixeligen UI-Texturen hin und her.", }, "rendering.upscaling_factor": { "title": "Upsaling-Faktor", "description": "Skaliert das Spiel um einen festgelegten Wert hoch, erhält dabei die pixelige Optik.", }, "rendering.upscaling_filter": { "title": "Upscaling-Filter", "description": "Schaltet zwischen weichgezeichneter oder pixeliger Optik für das gesamte Bild.", }, "ui.airbar_color": { "title": "Sauerstoffleistenfarbe", "description": "Farbe der Sauerstoffleiste.", }, "ui.airbar_color_ps1": { "title": "Sauerstoffleistenfarbe", "description": "Farbe der Sauerstoffleiste.", }, "ui.airbar_location": { "title": "Position der Sauerstoffleiste", "description": "Position, an der die Sauerstoffleiste angezeigt wird.", }, "ui.ammo_counter_location": { "title": "\\{review}Munitionszähler-Position", "description": "\\{review}Position, an der der Munitionszähler angezeigt wird.", }, "ui.bar_look": { "title": "\\{review}Aussehen der Balken", "description": "\\{review}Steuert das visuelle Erscheinungsbild der UI-Leisten.", }, "ui.bar_scale": { "title": "Leisten-Größe", "description": "Ändert die Größe der Gesundheitsleisten, Sauerstoffleisten und Gesundheitsleisten von Feinden.", }, "ui.enable_bar_flashing": { "title": "\\{review}Leisten blinken", "description": "\\{review}Lässt Laras Gesundheits- und Sauerstoffanzeigen blinken, wenn eine der Ressourcen knapp wird.", }, "ui.enable_smooth_bars": { "title": "Leisten-Farbverläufe", "description": "Sorgt bei Gesundheitsleisten, Sauerstoffleisten und Gesundheitsleisten von Feinden für einen fließenden Farbübergang.", }, "ui.enable_wraparound": { "title": "Scroll-Schleife", "description": "Ermöglicht die Richtungsnavigation in Menüs in einer Schleife.", }, "ui.enemy_healthbar_color": { "title": "Leisten-Farbe der Feinde", "description": "Gesundheitsleisten-Farbe der Feinde", }, "ui.enemy_healthbar_color_allies": { "title": "Freundliche Einheiten-Leisten Farbe", "description": "Farbe für die Gusundheitsleisten freundlich gesinnter Einheiten. Wird auf der gleiche Position angezeigt wie die Gesundheitsleisten der Feinde.", }, "ui.enemy_healthbar_color_allies_ps1": { "title": "Freundliche Einheiten-Leisten Farbe", "description": "Farbe für die Gusundheitsleisten freundlich gesinnter Einheiten. Wird auf der gleiche Position angezeigt wie die Gesundheitsleisten der Feinde.", }, "ui.enemy_healthbar_color_ps1": { "title": "Leisten-Farbe der Feinde", "description": "Gesundheitsleisten-Farbe der Feinde", }, "ui.enemy_healthbar_location": { "title": "Position der Gesundheitsleiste der Feinde", "description": "Die Position, an der die feindliche Gesundheitsleiste angezeigt wird.", }, "ui.enemy_healthbar_show_mode": { "title": "Gesundheitleiste der Feinde", "description": "Aktiviert die Anzeige einer Gesundheitleiste für aktive Feinde.", }, "ui.exposurebar_color": { "title": "\\{review}Farbe der Ausdauerleiste", "description": "\\{review}Farbe der Kaltwasser-Ausdauerleiste.", }, "ui.exposurebar_color_ps1": { "title": "\\{review}Farbe der Ausdauerleiste", "description": "\\{review}Farbe der Kaltwasser-Ausdauerleiste.", }, "ui.exposurebar_location": { "title": "\\{review}Position der Ausdauerleiste", "description": "\\{review}Position, an der die Kaltwasser-Ausdauerleiste angezeigt wird.", }, "ui.healthbar_color": { "title": "Gesundheitsleisten-Farbe", "description": "Farbe der Gesundheitsleiste.", }, "ui.healthbar_color_ps1": { "title": "Gesundheitsleisten-Farbe", "description": "Farbe der Gesundheitsleiste.", }, "ui.healthbar_location": { "title": "Position der Gesundheitsleiste", "description": "Position, an der die Gesundheitsleiste angezeigt wird.", }, "ui.healthbar_poison_color": { "title": "\\{review}Gift Gesundheitsbalken Farbe", "description": "\\{review}Farbe des Gesundheitsbalkens, wenn Lara vergiftet ist.", }, "ui.healthbar_poison_color_ps1": { "title": "\\{review}Gift Gesundheitsbalken Farbe", "description": "\\{review}Farbe des Gesundheitsbalkens, wenn Lara vergiftet ist.", }, "ui.inventory_background_style": { "title": "\\{review}Inventar-Hintergrund", "description": "\\{review}Ändert die Art und Weise, wie der Hintergrund für den Inventarring angezeigt wird.\n\n- Dunkel: TR1 (PC).\n- Sehr dunkel: TR1 (PS1).\n- Statisch: TR2 (PC).\n- Welle: TR2 (PS1).\n- Monochrom: TR3.", }, "ui.inventory_fade_effects": { "title": "\\{review}Inventar-Ausblend-Effekte", "description": "\\{review}Feinabstimmung, ob die Ausblend-Effekte im Inventarring des Spiels aktiviert oder deaktiviert sind. Die Option Ausblend-Effekte muss aktiviert sein, damit dies funktioniert.", }, "ui.menu_style": { "title": "Menü-Stil", "description": "Ändert, wie Menüs dargestellt werden.\n\n - PC: UI-Stil gleicht der PC-Version.\n - PS1: UI-Stil gleicht der PS1-Version.", }, "ui.pause_background_style": { "title": "\\{review}Pause-Hintergrund", "description": "\\{review}Ändert die Art und Weise, wie der Hintergrund für den Pausenbildschirm angezeigt wird.\n\n- Dunkel: TR1 (PC).\n- Sehr dunkel: TR1 (PS1).\n- Statisch: TR2 (PC).\n- Welle: TR2 (PS1).\n- Monochrom: TR3.", }, "ui.pause_fade_effects": { "title": "\\{review}Pause-Ausblend-Effekte", "description": "\\{review}Feinabstimmung, ob die Ausblend-Effekte im Pausenbildschirm aktiviert oder deaktiviert sind. Die Option Ausblend-Effekte muss aktiviert sein, damit dies funktioniert.", }, "ui.pickup_scale": { "title": "Pickup-Skala", "description": "\\{review}Ändert die Größe der im UI animierten Gegenstände, wenn Lara etwas aufnimmt.", }, "ui.show_bars": { "title": "\\{review}Leisten anzeigen", "description": "\\{review}Deaktiviert alle Ingame-Anzeigen und verdeckt Informationen über Laras Gesundheit und andere Ressourcen (für Herausforderungsdurchläufe).", }, "ui.show_pickups_overlay": { "title": "Pickup-Anzeige", "description": "Zeigt Gegenstände unten rechts an, wenn Lara etwas aufnimmt.", }, "ui.show_title_version": { "title": "\\{review}Titel Versionsanzeige", "description": "\\{review}Zeigt die TRX Versionszeichenfolge im Inventarring des Titels an.", }, "ui.sprintbar_color": { "title": "\\{review}Farbe der Sprintleiste", "description": "\\{review}Farbe der Sprintleiste", }, "ui.sprintbar_color_ps1": { "title": "\\{review}Farbe der Sprintleiste", "description": "\\{review}Farbe der Sprintleiste", }, "ui.sprintbar_location": { "title": "\\{review}Position der Sprintanleiste", "description": "\\{review}Position, an der die Sprinleiste angezeigt wird.", }, "ui.stats.show_ammo": { "title": "\\{review}Munition Treffer/Verbraucht", "description": "\\{review}Zeigt die Munitionszeile in den Levelstatistiken an.", }, "ui.stats.show_crystals": { "title": "\\{review}Kristalle", "description": "\\{review}Zeigt die Kristallreihe in den Levelstatistiken an.", }, "ui.stats.show_deaths": { "title": "\\{review}Tode", "description": "\\{review}Zeigt Laras Tode in den Kompassstatistiken und in den Levelstatistiken an. Die Todesanzahl wird im aktuell geladenen Speicherstand aktualisiert, sobald Lara stirbt.", }, "ui.stats.show_distance_travelled": { "title": "\\{review}Zurückgelegte Distanz", "description": "\\{review}Zeigt die Zeile der zurückgelegten Distanz in den Levelstatistiken an.", }, "ui.stats.show_kills": { "title": "\\{review}Kills", "description": "\\{review}Zeigt die Kills-Zeile in den Levelstatistiken an.", }, "ui.stats.show_level_header": { "title": "\\{review}Levelzähler", "description": "\\{review}Zeigt die aktuelle Levelnummer oben in den Levelstatistiken an.", }, "ui.stats.show_medipacks_used": { "title": "\\{review}Verwendete Gesundheitssets", "description": "\\{review}Zeigt die Zeile der verwendeten Gesundheitssets in den Levelstatistiken an.", }, "ui.stats.show_pickups": { "title": "\\{review}Aufnahmen", "description": "\\{review}Zeigt die Aufnahmen-Zeile in den Levelstatistiken an.", }, "ui.stats.show_secrets": { "title": "\\{review}Gefundene Geheimnisse", "description": "\\{review}Zeigt die Zeile der gefundenen Geheimnisse in den Levelstatistiken an.", }, "ui.stats.show_time_taken": { "title": "\\{review}Benötigte Zeit", "description": "\\{review}Zeigt die Zeile der benötigten Zeit in den Levelstatistiken an.", }, "ui.stats.show_totals": { "title": "\\{review}Summen anzeigen", "description": "\\{review}Zeigt Gesamtwerte neben den Statistiken an, wenn zutreffend. Geheimnisse bleiben von dieser Einstellung unberührt.", }, "ui.stats.style": { "title": "\\{review}Stil der Statistiken", "description": "\\{review}Steuert, wie der Statistikdialog angezeigt wird.\n\n- Einfach: zeigt das einfachere, rahmenlose Layout.\n- Mit Rahmen: zeigt das Layout mit Rahmen.", }, "ui.stats_background_style": { "title": "\\{review}Statistik-Hintergrund", "description": "\\{review}Ändert die Art und Weise, wie der Hintergrund für die Endlevel-Statistiken angezeigt wird.\n\n- Dunkel: TR1 (PC).\n- Sehr dunkel: TR1 (PS1).\n- Statisch: TR2 (PC).\n- Welle: TR2 (PS1).\n- Monochrom: TR3.", }, "ui.stats_fade_effects": { "title": "\\{review}Statistik-Ausblend-Effekte", "description": "\\{review}Feinabstimmung, ob die Ausblend-Effekte im Statistikbildschirm am Ende des Levels aktiviert oder deaktiviert sind. Die Option Ausblend-Effekte muss aktiviert sein, damit dies funktioniert.", }, "ui.text_scale": { "title": "Textgröße", "description": "Ändert die Größe des UI-Textes.", }, "visuals.blood_effects": { "title": "\\{review}Bluteffekte", "description": "\\{review}Steuert die Farben der Blutspritzer.\n\n- Deaktiviert: Es werden keine Blutspritzer angezeigt.\n- Pink: Standard in den deutschen PC-Versionen von TR3.\n- Rot: Standard in allen anderen Einzelhandelsversionen.", }, "visuals.camera_mode": { "title": "Kamera-Modus", "description": "Passt das Kameraverhalten während Aktionen, wie dem Benutzen von Schlüsseln, ausegführt werden.", }, "visuals.enable_3d_pickups": { "title": "3D-Pickups", "description": "Aktiviert die Darstellung von 3D-Modellen für Pickups anstatt der üblichen Sprites.", }, "visuals.enable_braid": { "title": "Laras Zopf", "description": "Aktiviert Laras Zopf.", }, "visuals.enable_breeze": { "title": "Brise", "description": "Aktiviert eine Brise, die Laras Zopf in den richtigen Räumen zum Wehen bringt.", }, "visuals.enable_exit_fade_effects": { "title": "Abblenden beim Beenden des Spiels", "description": "Aktiviert die Abblend-Effekte wenn man das Spiel vollständig beendet.", }, "visuals.enable_fade_effects": { "title": "Überganseffekte", "description": "Aktiviere Abblenden bei Übergängen, zum Beispiel bei Credits-, Grafikeinstellungs-, Inventar- und Pause-Bildübergängen.", }, "visuals.enable_fire_lighting": { "title": "Feuer-Beleuchtung", "description": "Ermöglicht die generierung von dynamischen Lichteffekten neben aktiven Flammen.", }, "visuals.enable_footprints": { "title": "\\{review}Fußspuren", "description": "\\{review}Ermöglicht das Anzeigen von Laras Fußspuren auf bestimmten Oberflächen in unterstützten Levels.", }, "visuals.enable_glide_cameras": { "title": "\\{review}Gleitende Kameras", "description": "\\{review}Ermöglicht bei festen Kameras, die Lara ansehen, eine Gleitbewegung mit einer sanften Geschwindigkeitskurve. Wenn deaktiviert, wechseln diese Kameras sofort zur Ansicht auf Lara.", }, "visuals.enable_gun_lighting": { "title": "Dynamisches Licht für Waffen", "description": "Aktiviert die verwendung von dynamischer Beleuchtung für Schüsse und Explosionen.", }, "visuals.enable_ps1_crystals": { "title": "PS1-Kristall-Farbton", "description": "Der Speicherkristall bekommt einen lila Farbton, ähnlich wie bei der PS1-Variante.", }, "visuals.enable_reflections": { "title": "Reflektionen", "description": "Aktiviert Reflektionen auf bestimmten Objekten.", }, "visuals.enable_responsive_mesh_tint": { "title": "\\{review}Reaktionsfähige Mesh-Tönung", "description": "\\{review}Ermöglicht es, Laras einzelne Meshes mit einer Wasserfärbung darzustellen, wenn sie sich selbst unter Wasser befinden (TR3-Stil). Andernfalls werden, wenn Lara im Wasser ist, alle ihre Meshes mit der Tönung dargestellt (TR1/2-Stil).", }, "visuals.enable_shotgun_flash": { "title": "Schrotflinten-Mündungsfeuer", "description": "Zeigt Mündungsfeuer an, wenn man mit der Schrotflinte schießt, wie bei anderen Waffen.", }, "visuals.enable_skybox": { "title": "Skyboxes", "description": "Aktiviert die Skybox in unterstützten Leveln.", }, "visuals.enable_weather": { "title": "\\{review}Wetter", "description": "\\{review}Aktiviert die Darstellung von Wettereffekten in unterstützten Levels.", }, "visuals.fix_animated_sprites": { "title": "Fix: Sprite-Animationen", "description": "\\{review}Behebt die ursprünglichen Unterwasser-Pflanzen-Sprites, sodass sie in Wasserbereichen richtig animiert werden.", }, "visuals.fix_item_rots": { "title": "Fix: Item-Rotations-Fehler", "description": "Fixt originale Fehler mit einigen falsch rotierenden Pickups, wenn man die 3D-Pickups-Option verwendet.", }, "visuals.fix_texture_issues": { "title": "Fix: Textur-Fehler", "description": "Fixt originale Fehler mit fehlenden oder inkorrekten Texturen/Meshes.", }, "visuals.fog_color": { "title": "\\{review}Nebel Farbe", "description": "\\{review}Farbe des Nebels.", }, "visuals.fog_end": { "title": "Ende des Nebels", "description": "Stellt die Distanz in Kacheln ein, in der der Nebel alles völlig verhüllt.", }, "visuals.fog_start": { "title": "Beginn des Nebels", "description": "Stellt die Distanz in Kacheln ein, in der der Nebel zu erscheinen beginnt.", }, "visuals.fog_transparency": { "title": "\\{review}Nebel Transparenz", "description": "Bestimmt ob die Überlagerung entfernter Geometrie mit 100% transparenten Flächen aktiviert werden soll.", }, "visuals.fov": { "title": "\\{review}Sichtfeld", "description": "\\{review}Blickwinkel in Grad. Größere Werte erweitern das Sichtfeld, kleinere verengen es.", }, "visuals.game_brightness": { "title": "Helligkeit", "description": "Ändert die Helligkeit des Spiels.", }, "visuals.gamma": { "title": "\\{review}Gamma", "description": "\\{review}Passt die Gammakurve an. Höhere Werte bedeuten hellere Beleuchtung. Der Wert 2,5 entspricht den Standardfarben.", }, "visuals.lara_outfit": { "title": "\\{review}Laras Outfit", "description": "\\{review}Ändert Laras Aussehen. Bei Auswahl von Standard werden die regulären Outfit-Wechsel zwischen den Levels beibehalten, andernfalls bleibt das gewählte Outfit aktiv, bis es manuell geändert wird.", }, "visuals.shadow_type": { "title": "\\{review}Schattenform", "description": "\\{review}Wählt aus, wie die Schatten von Entitäten dargestellt werden.\n\n- Achteck: alte TR1- und TR2-Schatten\n- Kreis: runde Schatten\n- Sprite: TR3 texturbasierte Schatten", }, "visuals.sunglasses_mode": { "title": "\\{review}Laras Sonnenbrille", "description": "\\{review}Ändert den Stil von Laras Sonnenbrille. Hinweis: Die Gläser sind reflektierend, wenn die entsprechende Option aktiviert ist.\n\n- Aus: Lara trägt keine Sonnenbrille.\n- Undurchsichtig: Laras Sonnenbrille hat undurchsichtige Gläser.\n- Transparent: Laras Sonnenbrille hat halbtransparente Gläser.", }, "visuals.ui_brightness": { "title": "UI-Helligkeit", "description": "Ändert die Helligkeit der Benutzeroberfläche.", }, "visuals.water_color": { "title": "Wasser-Farbe", "description": "Farbe des Wassers.", } }, "objects": { "alarm_sound": { "name": "Alarm", }, "alligator": { "name": "Alligator", }, "alphabet": { "name": "\\{review}Standard Schriftart", }, "alphabet_small": { "name": "\\{review}Kleine Schriftart", }, "amber_light": { "name": "\\{review}Bernsteinfarbenes Licht", }, "animating_1": { "name": "\\{review}Objekt 1 wird animiert", }, "animating_10": { "name": "\\{review}Objekt 10 wird animiert", }, "animating_2": { "name": "\\{review}Objekt 2 wird animiert", }, "animating_3": { "name": "\\{review}Objekt 3 wird animiert", }, "animating_4": { "name": "\\{review}Objekt 4 wird animiert", }, "animating_5": { "name": "\\{review}Objekt 5 wird animiert", }, "animating_6": { "name": "\\{review}Objekt 6 wird animiert", }, "animating_7": { "name": "\\{review}Objekt 7 wird animiert", }, "animating_8": { "name": "\\{review}Objekt 8 wird animiert", }, "animating_9": { "name": "\\{review}Objekt 9 wird animiert", }, "ape": { "name": "Affe", }, "area_51_rocket": { "name": "\\{review}Area 51 Rakete", }, "area_51_rocket_blast": { "name": "\\{review}Area 51 Raketenstart", }, "area_51_rocket_support": { "name": "\\{review}Area 51 Raketenunterstützung", }, "assault_digits": { "name": "Assault Digits", }, "assault_target": { "name": "\\{review}Angriffsziele", }, "atlantean_ground": { "name": "\\{review}Boden-Atlanter", }, "atlantean_shooter": { "name": "\\{review}Atlanter (Schießend)", }, "atlantean_winged": { "name": "\\{review}Geflügelter Atlanter", }, "autos": { "name": "Automatik", }, "autos_ammo": { "name": "Automatik-Munition", }, "bacon_lara": { "name": "Schinken Lara", }, "baldy": { "name": "Glatzkopf", }, "bandit_1": { "name": [ "Söldner 1", "Masked Goon 1", ] }, "bandit_2": { "name": [ "Söldner 2", "Maskierter Schläger 2", ] }, "bandit_2b": { "name": [ "Söldner 3", "Maskierter Schläger 3", ] }, "barracuda": { "name": "Barrakuda", }, "bartoli": { "name": "Marco Bartoli", }, "bat": { "name": "Fledermaus", }, "bat_emitter": { "name": "\\{review}Fledermaus-Emitter", }, "beacon_light": { "name": "\\{review}Leuchtfeuer", }, "bear": { "name": "Bär", }, "bell": { "name": "Bell", }, "big_bowl": { "name": "Lava-Schüssel", }, "big_eel": { "name": "Großer Aal", }, "big_pod": { "name": "Big Pod", }, "big_spider": { "name": "Riesen-Spider", }, "bird_guardian": { "name": "Vogel-Monster", }, "bird_tweeter_1": { "name": "Dripping Water", }, "bird_tweeter_2": { "name": "Singing Birds", }, "blade": { "name": "Wall-mounted Blade", }, "blood": { "name": "\\{review}Blut", }, "blood_pink": { "name": "\\{review}Blut (zensiert)", }, "blue_light": { "name": "\\{review}Blaues Licht", }, "boat": { "name": "Boot", }, "boat_bits": { "name": "Boots-Einzelteile", }, "body_part": { "name": "Körperteil", }, "bridge_flat": { "name": "Brücke Flach", }, "bridge_tilt_1": { "name": "Brücke Tilt 1", }, "bridge_tilt_2": { "name": "Brücke Tilt 2", }, "bubble_1": { "name": "Blase 1", }, "bubble_2": { "name": "Blase 2", }, "bubble_emitter": { "name": "Blasenerzeuger", }, "camera_target": { "name": "Kamera-Ziel", }, "carcass": { "name": "\\{review}Kadaver", }, "ceiling_spikes": { "name": "Spiky Ceiling", }, "centaur": { "name": "Zentaur", }, "centaur_statue": { "name": "Statue", }, "civilian": { "name": "\\{review}Zivilist", }, "claw_mutant": { "name": "\\{review}Klauenmutant", }, "clock_chimes": { "name": "Bartoli Hideout clock", }, "cog_1": { "name": "Zahnrad 1", }, "cog_2": { "name": "Zahnrad 2", }, "cog_3": { "name": "Zahnrad 3", }, "combat_end": { "name": "Kampf Ende", }, "compass": { "name": "Statistiken", }, "compy": { "name": "\\{review}Compsognathus", }, "controls": { "name": "Steuerung", }, "copter": { "name": "Helikopter", }, "cowboy": { "name": "Cowboy", }, "crawler_mutant": { "name": "\\{review}Kriechender Mutant", }, "crocodile": { "name": "Krokodil", }, "crow": { "name": "Krähe", }, "cult_1": { "name": "Maskierter Schläger 1", }, "cult_1a": { "name": "Maskierter Schläger 2", }, "cult_1b": { "name": "Maskierter Schläger 3", }, "cult_2": { "name": "Messerwerfer", }, "cult_3": { "name": "Schrotflinten Schläger", }, "cut_shotgun": { "name": "Schrotflinten Dusch Animation", }, "damocles_sword": { "name": "Schwert des Damokles", }, "dart": { "name": "Pfeil", }, "dart_effect": { "name": "Pfeil-Effekt", }, "dart_emitter": { "name": "Pfeilerzeuger", }, "desert_eagle": { "name": "\\{review}Desert Eagle", }, "desert_eagle_ammo": { "name": "\\{review}Magazine der Desert Eagle", }, "detonator_box": { "name": "Zünder", }, "ding_dong": { "name": "Türklingel", }, "dino_mutant": { "name": "Dino Mutant", }, "disc": { "name": "Scheibe", }, "disc_emitter": { "name": "Scheiben-Erzeuger", }, "disposable_animating_1": { "name": "\\{review}Einweg-Animierobjekt 1", }, "disposable_animating_10": { "name": "\\{review}Einweg-Animierobjekt 10", }, "disposable_animating_2": { "name": "\\{review}Einweg-Animierobjekt 2", }, "disposable_animating_3": { "name": "\\{review}Einweg-Animierobjekt 3", }, "disposable_animating_4": { "name": "\\{review}Einweg-Animierobjekt 4", }, "disposable_animating_5": { "name": "\\{review}Einweg-Animierobjekt 5", }, "disposable_animating_6": { "name": "\\{review}Einweg-Animierobjekt 6", }, "disposable_animating_7": { "name": "\\{review}Einweg-Animierobjekt 7", }, "disposable_animating_8": { "name": "\\{review}Einweg-Animierobjekt 8", }, "disposable_animating_9": { "name": "\\{review}Einweg-Animierobjekt 9", }, "diver": { "name": "Taucher", }, "dog": { "name": [ "Hund", "Dobermann", ] }, "door_1": { "name": "Tür 1", }, "door_2": { "name": "Tür 2", }, "door_3": { "name": "Tür 3", }, "door_4": { "name": "Tür 4", }, "door_5": { "name": "Tür 5", }, "door_6": { "name": "Tür 6", }, "door_7": { "name": "Tür 7", }, "door_8": { "name": "Tür 8", }, "dragon_back": { "name": "Drachen Hinterseite", }, "dragon_bones_1": { "name": "Platzhalter", }, "dragon_bones_2": { "name": "Drachen-Knochen Vorn", }, "dragon_bones_3": { "name": "Drachen-Knochen Hinten", }, "dragon_front": { "name": "Drache Vorderseite", }, "drawbridge": { "name": "Zugbrücke", }, "dust": { "name": "Staub", }, "dying_monk": { "name": "sterbender Mönch", }, "dying_mutant": { "name": "\\{review}Sterbender Mutant", }, "eagle": { "name": "Adler", }, "earthquake": { "name": "Erdbeben", }, "eel": { "name": "Aal", }, "electric_cleaner": { "name": "\\{review}Elektrischer Reiniger", }, "electric_fence": { "name": "\\{review}Elektrischer Zaun", }, "electrical_light": { "name": "\\{review}Elektrisches Licht", }, "ember": { "name": "Glut", }, "ember_emitter": { "name": "Gluterzeuger", }, "explosion_1": { "name": "Explosion 1", }, "explosion_2": { "name": "Explosion 2", }, "falling_block_1": { "name": [ "Fallender Block 1", "Zerbrechbarer Boden 1", "Zerbrechbare Kacheln 1", ] }, "falling_block_2": { "name": [ "Fallender Block 2", "Zerbrechbarer Boden 2", "Zerbrechbare Kacheln 2", ] }, "falling_block_3": { "name": [ "Fallender Block 3", "Zerbrechbarer Boden 3", "Zerbrechbare Kacheln 3", "Lose Bretter", ] }, "falling_ceiling_1": { "name": "Falling Ceiling 1", }, "falling_ceiling_2": { "name": "Fallende Decke 2", }, "fire_head": { "name": "\\{review}Feuerkopf", }, "fish_mutant": { "name": "Fisch Mutant", }, "flame": { "name": [ "Flamme", "Feuer", ] }, "flame_emitter": { "name": [ "Flamenerzeuger", "Feuererzeuger", ] }, "flame_emitter_big": { "name": "\\{review}Flammenwerfer (Groß)", }, "flame_emitter_jet": { "name": "\\{review}Flammenwerfer (Strahl)", }, "flame_emitter_side": { "name": "\\{review}Flammenwerfer (Seite)", }, "flame_emitter_small": { "name": "\\{review}Flammenwerfer (Klein)", }, "flare": { "name": "Fackel", }, "flare_fire": { "name": "Fackel-Funken", }, "flares_box": { "name": "Flares Box", }, "flickering_light": { "name": "\\{review}Flackerndes Licht", }, "fuse_box": { "name": "\\{review}Sicherungskasten", }, "fx_reserved": { "name": "Gray disk", }, "gamma": { "name": "\\{review}Gamma", }, "gas_emitter_green": { "name": "\\{review}Gasemitter (Grün)", }, "general": { "name": "Mini U-Boot", }, "globe": { "name": "\\{review}Globus", }, "glow": { "name": "Glühen", }, "glow_reserved": { "name": "Karten-Glühen", }, "gondola": { "name": "Gondel", }, "gong": { "name": "Gong", }, "gong_bonger": { "name": "Gong Schlegel", }, "graphics": { "name": "Grafikeinstellungen", }, "green_light": { "name": "\\{review}Grünes Licht", }, "grenade": { "name": "Granate", }, "grenade_launcher": { "name": "Granatenwerfer", }, "grenade_launcher_ammo": { "name": "Granaten", }, "gun_flash": { "name": "Waffen Blitz", }, "gun_shell": { "name": "\\{review}Gewehrpatrone", }, "harpoon_bolt": { "name": "Harpunen Pfeil", }, "harpoon_gun": { "name": "Harpune", }, "harpoon_gun_ammo": { "name": "Pfeile", }, "hook": { "name": "Hook", }, "hot_liquid": { "name": "Extra Feuer", }, "huskie": { "name": "\\{review}Hund", }, "hybrid_mutant": { "name": "\\{review}Hybridmutant", }, "icicle": { "name": "Icicles", }, "inv_background": { "name": "Menü Hintergrund", }, "jelly": { "name": "Qualle", }, "kayak": { "name": "\\{review}Kajak", }, "key_1": { "name": "Key 1", }, "key_2": { "name": "Key 2", }, "key_3": { "name": "Key 3", }, "key_4": { "name": "Key 4", }, "key_hole_1": { "name": "Keyhole 1", }, "key_hole_2": { "name": "Keyhole 2", }, "key_hole_3": { "name": "Keyhole 3", }, "key_hole_4": { "name": "Keyhole 4", }, "kill_all_triggered": { "name": "\\{review}Alle töten (ausgelöst)", }, "killer_statue": { "name": "Statue with Sword", }, "lara": { "name": "Lara", }, "lara_alarm": { "name": "Alarmglocke", }, "lara_autos": { "name": "\\{review}Animation automatischer Pistolen", }, "lara_boat": { "name": "Boots-Animation", }, "lara_desert_eagle": { "name": "\\{review}Animation der Desert Eagle", }, "lara_extra": { "name": "Laras Extra Animation", }, "lara_flare": { "name": "Fackel-Animation", }, "lara_grenade": { "name": "Grantenwerfer-Animation", }, "lara_hair": { "name": "Laras Zopf", }, "lara_harpoon": { "name": "Harpunen-Animation", }, "lara_m16": { "name": "M16-Animation", }, "lara_magnums": { "name": "Magnum-Animation", }, "lara_mp5": { "name": "\\{review}MP5-Animation", }, "lara_pistols": { "name": "Pistolen-Animation", }, "lara_rocket": { "name": "\\{review}Animation des Raketenwerfers", }, "lara_shotgun": { "name": "Schrotflinten-Animation", }, "lara_skidoo": { "name": "Schneeemobil-Animation", }, "lara_uzis": { "name": "Uzi-Animation", }, "larson": { "name": "Larson", }, "lava_wedge": { "name": "Lava-Keil", }, "lead_bar": { "name": "Bleibarren", }, "lift": { "name": "Aufzug", }, "lightning_emitter": { "name": "Blitzerzeuger", }, "lion": { "name": "Löwe", }, "lioness": { "name": [ "Löwin", "Löwe", ] }, "lizard": { "name": "\\{review}Echse", }, "m16": { "name": "M16", }, "m16_ammo": { "name": "M16-Munition", }, "m16_flash": { "name": "M16 Blitz", }, "magnums": { "name": "Magnums", }, "magnums_ammo": { "name": "Magnum Munition", }, "mesh_swap_1": { "name": "Mesh Swap 1", }, "mesh_swap_2": { "name": "Mesh Swap 2", }, "mesh_swap_3": { "name": "\\{review}Mesh Swap 3", }, "midas_touch": { "name": "Hand des Midas", }, "mine": { "name": "Wassermine", }, "mine_cart": { "name": "\\{review}Grubenwagen", }, "mini_copter": { "name": "Helikopter 2", }, "missile_atlantean_bomb": { "name": "\\{review}Geschoss (Atlantische Bombe)", }, "missile_atlantean_shard": { "name": "\\{review}Geschoss (Atlantischer Splitter)", }, "missile_flame": { "name": "\\{review}Geschoss (Flamme)", }, "missile_harpoon": { "name": "\\{review}Geschoss (Harpune)", }, "missile_knife": { "name": "\\{review}Geschoss (Messer)", }, "missile_poison": { "name": "\\{review}Geschoss (Gift)", }, "monk_1": { "name": "Mönch 1", }, "monk_2": { "name": "Mönch 2", }, "monkey": { "name": "\\{review}Affe", }, "mounted_gun": { "name": "\\{review}Montiertes Geschütz", }, "mouse": { "name": "Ratte", }, "movable_block_1": { "name": [ "Drückbarer Block 1", "Bewegbarer Block 1", ] }, "movable_block_2": { "name": [ "Drückbarer Block 2", "Bewegbarer Block 2", ] }, "movable_block_3": { "name": [ "Drückbarer Block 3", "Bewegbarer Block 3", ] }, "movable_block_4": { "name": [ "Drückbarer Block 4", "Bewegbarer Block 4", ] }, "moving_bar": { "name": "Moving Bar", }, "mp5": { "name": "\\{review}MP5", }, "mp5_ammo": { "name": "\\{review}MP5-Munition", }, "mp_1": { "name": "\\{review}MP 1", }, "mp_2": { "name": "\\{review}MP 2", }, "mummy": { "name": "Mumie", }, "natla": { "name": "Natla", }, "natla_gun": { "name": "\\{review}Natlas Waffe", }, "on_off_light": { "name": "\\{review}An/Aus Licht", }, "orca": { "name": "\\{review}Orca", }, "passport": { "name": "Spiel", }, "patrol_dog": { "name": "\\{review}Hund", }, "pda": { "name": "Gameplay", }, "pendulum_1": { "name": "Sandsack", }, "pendulum_2": { "name": "Swinging Box", }, "photo": { "name": "Laras Haus", }, "pickup_1": { "name": "Pickup Item 1", }, "pickup_2": { "name": "Pickup Item 2", }, "pickup_aid": { "name": "Pickup-Hilfe", }, "pierre": { "name": "Pierre", }, "pirahnas": { "name": "\\{review}Piranhas", }, "pistols": { "name": "Pistolen", }, "pistols_ammo": { "name": "Pistolen Munition", }, "player_1": { "name": "Zwischensequenz-Schauspieler 1", }, "player_10": { "name": "Zwischensequenz-Schauspieler 10", }, "player_2": { "name": "Zwischensequenz-Schauspieler 2", }, "player_3": { "name": "Zwischensequenz-Schauspieler 3", }, "player_4": { "name": "Zwischensequenz-Schauspieler 4", }, "player_5": { "name": "Zwischensequenz-Schauspieler 5", }, "player_6": { "name": "Zwischensequenz-Schauspieler 6", }, "player_7": { "name": "Zwischensequenz-Schauspieler 7", }, "player_8": { "name": "Zwischensequenz-Schauspieler 8", }, "player_9": { "name": "Zwischensequenz-Schauspieler 9", }, "pods": { "name": "Pod", }, "poison_dart": { "name": "\\{review}Giftpfeil", }, "poison_dart_emitter": { "name": "\\{review}Giftpfeilwerfer", }, "portacabin": { "name": "Portable Cabin", }, "power_saw": { "name": "Power Saw", }, "prisoner": { "name": "\\{review}Gefangener", }, "propeller_1": { "name": "Airplane Propeller", }, "propeller_2": { "name": "Underwater Propeller", }, "propeller_3": { "name": "Air Fan", }, "pulse_light": { "name": "\\{review}Pulsierendes Licht", }, "puma": { "name": "Puma", }, "punk_1": { "name": "\\{review}Punk 1", }, "punk_2": { "name": "\\{review}Punk 2", }, "puzzle_1": { "name": "Puzzle Item 1", }, "puzzle_2": { "name": "Puzzle Item 2", }, "puzzle_3": { "name": "Puzzle Item 3", }, "puzzle_4": { "name": "Puzzle Item 4", }, "puzzle_done_1": { "name": "Puzzle Hole 1 (Done)", }, "puzzle_done_2": { "name": "Puzzle Hole 2 (Done)", }, "puzzle_done_3": { "name": "Puzzle Hole 3 (Done)", }, "puzzle_done_4": { "name": "Puzzle Hole 4 (Done)", }, "puzzle_hole_1": { "name": "Puzzle Hole 1 (Empty)", }, "puzzle_hole_2": { "name": "Puzzle Hole 2 (Empty)", }, "puzzle_hole_3": { "name": "Puzzle Hole 3 (Empty)", }, "puzzle_hole_4": { "name": "Puzzle Hole 4 (Empty)", }, "quad_bike": { "name": "\\{review}Quad", }, "quest_1": { "name": "\\{review}Quest-Gegenstand 1", }, "quest_2": { "name": "\\{review}Quest-Gegenstand 2", }, "quest_3": { "name": "\\{review}Quest-Gegenstand 3", }, "quest_4": { "name": "\\{review}Quest-Gegenstand 4", }, "raptor": { "name": "Raptor", }, "raptor_emitter": { "name": "\\{review}Raptor-Emitter", }, "rat": { "name": [ "Ratte", "Landratte", ] }, "red_light": { "name": "\\{review}Rotes Licht", }, "rib": { "name": "\\{review}RIB", }, "ricochet": { "name": "Querschläger", }, "rocket": { "name": "\\{review}Rakete", }, "rocket_launcher": { "name": "\\{review}Raketenwerfer", }, "rocket_launcher_ammo": { "name": "\\{review}Raketen", }, "rolling_ball_1": { "name": [ "Fels 1", "Rollender Ball", ] }, "rolling_ball_2": { "name": [ "Boulder 2", "Rolling Ball 2", ] }, "rolling_ball_3": { "name": [ "Boulder 3", "Rolling Ball 3", ] }, "rolling_ball_4": { "name": [ "Boulder 4", "Rolling Ball 4", ] }, "rotating_laser": { "name": "\\{review}Rotierender Laser", }, "rx_worker_1": { "name": "\\{review}RX Arbeiter 1", }, "rx_worker_2": { "name": "\\{review}RX Arbeiter 2", }, "rx_worker_3": { "name": "\\{review}RX Arbeiter 3", }, "save_crystal": { "name": "Speicherkristall", }, "scion": { "name": "Scion", }, "scion_holder": { "name": "Scion-Halter", }, "secret_1": { "name": "Geheimnis 1", }, "secret_2": { "name": "Geheimnis 2", }, "secret_3": { "name": "Geheimnis 3", }, "security_guard": { "name": "\\{review}Sicherheitsmann", }, "security_laser_alarm": { "name": "\\{review}Sicherheitslaser (Alarm)", }, "security_laser_deadly": { "name": "\\{review}Sicherheitslaser (Tödlich)", }, "security_laser_killer": { "name": "\\{review}Sicherheitslaser (Killer)", }, "sentry_gun": { "name": "\\{review}Roboter-Wachgeschütz", }, "shadow": { "name": "\\{review}Schatten", }, "shark": { "name": "Hai", }, "shiva": { "name": "\\{review}Shiva", }, "shotgun": { "name": "Schrotflinte", }, "shotgun_ammo": { "name": "Schrot-Munition", }, "shotgun_shell": { "name": "\\{review}Schrotpatrone", }, "skate_kid": { "name": "Skate Kid", }, "skateboard": { "name": "Skateboard", }, "skidoo_armed": { "name": "Schwarzes Schneemobil", }, "skidoo_driver": { "name": "Fahrer schwarzes Schneemobil", }, "skidoo_fast": { "name": "Rotes Schneemobil", }, "skidoo_track": { "name": "Snowmobile Track", }, "skybox": { "name": "Skybox", }, "sliding_pillar": { "name": "Sliding Pillar", }, "smashable_1": { "name": "Breakable Window 1", }, "smashable_2": { "name": "Breakable Window 2", }, "smashable_3": { "name": "Breakable Window 3", }, "smashable_4": { "name": "Breakable Window 4", }, "smoke_emitter_black": { "name": "\\{review}Rauchgenerator (Schwarz)", }, "smoke_emitter_white": { "name": "\\{review}Rauchgenerator (Weiß)", }, "snake": { "name": "\\{review}Schlange", }, "snow_sprite": { "name": "Snowmobile Wake", }, "sophia": { "name": "\\{review}Sophia", }, "sound": { "name": "Sound", }, "sphere_of_doom_1": { "name": "Dragon Explosion 1", }, "sphere_of_doom_2": { "name": "Dragon Explosion 2", }, "sphere_of_doom_3": { "name": "Dragon Explosion 3", }, "spider": { "name": "Spinne", }, "spike_wall": { "name": "Spike Wall", }, "spikes": { "name": "Stacheln", }, "spinning_blade": { "name": "Spinning Blade", }, "splash_1": { "name": "Wasserkräuselungen 1", }, "splash_2": { "name": "Water-Kräuseln 2", }, "springboard": { "name": "Springboard", }, "steam_emitter": { "name": "\\{review}Dampfgenerator", }, "sthpac_mercenary": { "name": "\\{review}Südsee-Söldner", }, "stopwatch": { "name": "\\{review}Statistiken", }, "strobe_light": { "name": "\\{review}Stroboskoplicht", }, "swat_1": { "name": "\\{review}SWAT 1", }, "swat_2": { "name": "\\{review}SWAT 2", }, "swat_3": { "name": "\\{review}SWAT 3", }, "swinging_axe": { "name": "\\{review}Schwingende Axt", }, "switch_type_airlock": { "name": "Airlock Switch", }, "switch_type_button": { "name": [ "Button", "Push Button", "Switch", ] }, "switch_type_normal": { "name": [ "Lever", "Switch", ] }, "switch_type_small": { "name": "Small Switch", }, "switch_type_uw": { "name": [ "Underwater Lever", "Underwater Switch", ] }, "switch_type_wheel": { "name": "\\{review}Radswitch", }, "teeth_trap": { "name": "Zahn-Falle", }, "text_box": { "name": "UI Frame", }, "thors_handle": { "name": "Thors Hammer Griff", }, "thors_head": { "name": "Thors Hammer", }, "tiger": { "name": "Tiger", }, "tony": { "name": "\\{review}Tony", }, "torso": { "name": [ "Torso", "Adam", "Riesenmutant", ] }, "train": { "name": "\\{review}Zug", }, "trapdoor_1": { "name": "Falltür 1", }, "trapdoor_2": { "name": "Falltür 2", }, "trapdoor_3": { "name": "Falltür 3", }, "trex": { "name": "T-Rex", }, "trex_alpha": { "name": "\\{review}T-Rex-Alpha", }, "tribe_axeman": { "name": "\\{review}Stammesaxtkämpfer", }, "tribe_boss": { "name": "\\{review}Stammesanführer", }, "tribe_pipeman": { "name": "\\{review}Stammes-Blasrohrbenutzer", }, "tropical_fish": { "name": "\\{review}Tropischer Fisch", }, "twinkle": { "name": "Funken", }, "upv": { "name": "\\{review}Mini-U-Boot", }, "uzis": { "name": "Uzis", }, "uzis_ammo": { "name": "Uzi-Munition", }, "vole": { "name": [ "Wühlmaus", "Wasserratte", ] }, "vulture": { "name": "\\{review}Geier", }, "wasp_mutant": { "name": "\\{review}Wespenmutant", }, "wasp_mutant_emitter": { "name": "\\{review}Wespenmutanten-Emitter", }, "water_sprite": { "name": "Boat Wake", }, "waterfall": { "name": "Wasserfall Nebel", }, "white_light": { "name": "\\{review}Weißes Licht", }, "willard": { "name": "\\{review}Willard", }, "winston": { "name": "Winston", }, "winston_army": { "name": "\\{review}Winston (Armee)", }, "wolf": { "name": "Wolf", }, "worker_1": { "name": "Schläger Schütze 1", }, "worker_2": { "name": "Schläger Schütze 2", }, "worker_3": { "name": "Schlagstock schwingender Schläger 1", }, "worker_4": { "name": "Schlagstock schwingender Schläger 2", }, "worker_5": { "name": "Typ mit Flammenwerfer", }, "xian_knight": { "name": "Xian Ritter", }, "xian_knight_statue": { "name": "Xian Ritter Statue", }, "xian_spearman": { "name": "Xian Speerträger", }, "xian_spearman_statue": { "name": "Xian Speerträger Statue", }, "yeti": { "name": "Yeti", }, "zipline_handle": { "name": "Zipline Handle", } } } ================================================ FILE: data/trx/ship/cfg/base_strings-en-gb.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "extends": "en", "language_name": "English (British)", "general": { "settings": { "controls": { "customize": "Customise Controls", } }, "stats": { "distance_travelled": "Distance Travelled", } }, "dynamic": { "mods": { "tr3-la": { "title": "The Lost Artefact", } } }, "enums": { "UI_ELEMENT_LOCATION": { "UI_ELEMENT_LOCATION_BOTTOM_CENTER": "Bottom centre", "UI_ELEMENT_LOCATION_TOP_CENTER": "Top centre", } }, "settings": { "gameplay.enable_bouncy_grenades": { "description": "Enables TR3-style grenade behaviour: they ricochet off walls and slopes and produce a larger blast radius, but at the expense of reduced velocity.", }, "gameplay.enable_enemy_rotation": { "title": "Randomise enemy start angle", }, "gameplay.enable_swing_cancel": { "description": "Allows Lara's ledge-swinging animation to be cancelled by letting go and quickly grabbing again.", }, "gameplay.projectile_area_damage": { "description": "Controls how the area-of-effect for Rocket Launcher and Grenade Launcher propagates.\n\n- Single-sweep: TR1 & TR2 behaviour.\n- Multi-sweep: TR3 behaviour.\n\nThe multi-sweep option often ends up doing double damage to individual enemies.", }, "gameplay.target_mode": { "description": "Changes the behaviour of how weapons lock onto targets.\n\n- Full lock: always keep target lock even if the enemy moves out of sight or dies (OG TR1-3).\n- Semi lock: keep target lock if the enemy moves out of sight but lose target lock if the enemy dies.\n- No lock: lose target lock if the enemy goes out of sight or dies (TR4+).", }, "gameplay.wall_glitch_mode": { "description": "Allows using TR1 wall glitch behaviour in TR2 and vice-versa; equally allows fixing all types of wall glitch.", }, "input.quick_guns_mode": { "description": "Controls the behaviour of the quick gun equip keys.\n\n- Draw only: pressing a key will cause Lara to equip the assigned gun.\n- Draw or holster: same as above, plus Lara will undraw the assigned gun if she's currently carrying it.", }, "ui.airbar_color": { "title": "Airbar colour", "description": "Colour of the airbar.", }, "ui.airbar_color_ps1": { "title": "Airbar colour", "description": "Colour of the airbar.", }, "ui.enable_smooth_bars": { "description": "Makes the UI bars use smooth colour transitions.", }, "ui.enemy_healthbar_color": { "title": "Enemy bar colour", "description": "Colour of the enemy healthbar.", }, "ui.enemy_healthbar_color_allies": { "title": "Ally bar colour", "description": "Colour of the allies healthbar. Shown in the location of the enemy healthbars.", }, "ui.enemy_healthbar_color_allies_ps1": { "title": "Ally bar colour", "description": "Colour of the allies healthbar. Shown in the location of the enemy healthbars.", }, "ui.enemy_healthbar_color_ps1": { "title": "Enemy bar colour", "description": "Colour of the enemy healthbar.", }, "ui.exposurebar_color": { "title": "Exposure bar colour", "description": "Colour of the cold water exposure bar.", }, "ui.exposurebar_color_ps1": { "title": "Exposure bar colour", "description": "Colour of the cold water exposure bar.", }, "ui.healthbar_color": { "title": "Healthbar colour", "description": "Colour of the healthbar.", }, "ui.healthbar_color_ps1": { "title": "Healthbar colour", "description": "Colour of the healthbar.", }, "ui.healthbar_poison_color": { "title": "Poison healthbar colour", "description": "Colour of the healthbar when Lara is poisoned.", }, "ui.healthbar_poison_color_ps1": { "title": "Poison healthbar colour", "description": "Colour of the healthbar when Lara is poisoned.", }, "ui.sprintbar_color": { "title": "Sprintbar colour", "description": "Colour of the sprintbar.", }, "ui.sprintbar_color_ps1": { "title": "Sprintbar colour", "description": "Colour of the sprintbar.", }, "ui.stats.show_distance_travelled": { "title": "Distance travelled", "description": "Shows the distance travelled row in the level statistics.", }, "visuals.blood_effects": { "description": "Controls blood spark colours.\n\n- Disabled: no blood sparks are shown.\n- Pink: the default in German PC releases of TR3.\n- Red: the default in all other retail releases.", }, "visuals.fog_color": { "title": "Fog colour", "description": "Colour of the fog.", }, "visuals.gamma": { "description": "Adjusts the gamma curve. Higher values mean brighter lighting. The value of 2.5 means default colours.", }, "visuals.water_color": { "title": "Water colour", "description": "Colour of the water.", } } } ================================================ FILE: data/trx/ship/cfg/base_strings-fr.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "language_name": "Français", "general": { "actions": { "examine_item": "Examiner", "hide_dialog": "Masquer le dialogue", "reset_defaults": "Réinitialiser tout", "rotate": "Tourner", "unbind": "Dissocier", "use_item": "Utiliser", }, "config_presets": { "applied": "\\{review}Préréglage appliqué.", "confirm_description": "\\{review}Les paramètres suivants seront modifiés :", "confirm_restart_note": "\\{review}Remarque : certains paramètres peuvent nécessiter un redémarrage du jeu pour prendre effet.", "empty": "\\{review}Aucun préréglage trouvé.", "no_changes": "\\{review}Aucun changement à appliquer.", "title_fmt": "\\{review}Appliquer le préréglage %s ?", }, "globe_select": { "area_1": "\\{review}Zone 1", "area_2": "\\{review}Zone 2", "area_3": "\\{review}Zone 3", "area_4": "\\{review}Zone 4", "area_5": "\\{review}Zone 5", "area_6": "\\{review}Zone 6", }, "inventory_ring": { "heading_adventure": "\\{review}Aventure", "heading_fmt": "%s", "heading_game_over": "GAME OVER", "heading_inventory": "INVENTAIRE", "heading_items": "OBJETS", "heading_option": "OPTION", "item_count_fmt": "\\{small}%s", "object_name_fmt": "%s", }, "misc": { "demo_mode": "Mode Démo", "direction_keys_controller": "Croix directionnelle", "direction_keys_keyboard": "Flèches", "empty_slot_fmt": "- EMPLACEMENT VIDE %d -", "exit": "Sortie", "hold_fmt": "Maintenir %s", "off": "Désactivé", "on": "Activé", "pagination_nav": "%d / %d", }, "osd": { "ambiguous_input_2": "Entrée ambiguë : %s et %s", "ambiguous_input_3": "Entrée ambiguë : %s, %s, ...", "bilinear_filter_off": "\\{review}Filtre bilinéaire : désactivé", "bilinear_filter_on": "\\{review}Filtre bilinéaire : activé", "command_bad_invocation": "Invocation invalide : %s", "command_bool": "activé, désactivé", "command_decimal": "[décimal]", "command_integer": "[entier]", "command_percent": "[entier]", "command_unavailable": "Cette commande n'est pas disponible pour l'instant", "command_valid_values": "Valeurs acceptées : %s", "complete_level": "Niveau terminé !", "config_option_get": "%s est actuellement réglé sur %s", "config_option_set": "%s réglé sur %s", "config_option_unknown_option": "Option inconnue : %s", "current_health_get": "Santé actuelle de Lara : %d", "current_health_set": "Santé de Lara réglée sur %d", "door_close": "Sésame, ferme-la !", "door_open": "Sésame, ouvre-toi !", "door_open_fail": "Aucune porte à proximité de Lara", "flipmap_fail_already_off": "L'état secondaire des salles est déjà DÉSACTIVÉ", "flipmap_fail_already_on": "L'état secondaire des salles est déjà ACTIVÉ", "flipmap_off": "État secondaire des salles DÉSACTIVÉ", "flipmap_on": "État secondaire des salles ACTIVÉ", "fly_mode_off": "Mode vol désactivé", "fly_mode_on": "Mode vol activé", "fps_counter_off": "Compteur de FPS désactivé", "fps_counter_on": "Compteur de FPS activé", "give_item": "%s ajouté à l'inventaire de Lara", "give_item_all_guns": "A vos marques, prêts... feu ! Lara est armée jusqu'aux dents.", "give_item_all_keys": "Surprise ! Chaque clé dont Lara pourrait avoir besoin est maintenant dans son sac à dos.", "give_item_cheat": "Le sac à dos de Lara est soudainement beaucoup plus lourd !", "heal_already_full_hp": "Lara est déjà en pleine forme", "heal_success": "Lara est maintenant en pleine forme", "invalid_cutscene": "Cinématique invalide", "invalid_demo": "Démo invalide", "invalid_item": "Objet inconnu : %s", "invalid_level": "Niveau invalide", "invalid_object": "Objet invalide", "invalid_room": "Salle invalide : %d. Les salles valides sont 0-%d", "invalid_sample": "Effet sonore invalide : %d", "kill": "Bye-bye !", "kill_all": "Paf ! %d ennemis se sont volatisés !", "kill_all_fail": "Oh-oh, il n'y a plus un seul ennemi à tuer...", "kill_fail": "Aucun ennemi à proximité...", "lighting_contrast_fmt": "\\{review}Contraste de l'éclairage : %s", "load_game": "Partie chargée depuis l'emplacement de sauvegarde %d", "load_game_fail_invalid_slot": "Emplacement de sauvegarde %d invalide", "load_game_fail_unavailable_slot": "L'emplacement de sauvegarde %d n'est pas disponible", "object_not_found": "Objet non trouvé", "play_cutscene": "Chargement de la cinématique %d", "play_demo": "Chargement de la démo %d", "play_level": "Chargement de %s", "pos_lara_missing": "Lara n'est pas présente", "pos_lara_pos_fmt": "Salle : %d\nPosition : %.3f, %.3f, %.3f\nRotation : %.3f, %.3f, %.3f", "pos_level_fmt": "Niveau %d", "pos_level_fmt_cutscene": "Cinématique %d", "pos_level_fmt_demo": "Démo %d", "quick_load": "\\{review}Emplacement de chargement rapide %d", "quick_load_fail_no_bound_slot": "\\{review}Aucun emplacement de sauvegarde n'est actuellement assigné", "quick_load_fail_unavailable_bound_slot": "\\{review}L'emplacement de sauvegarde assigné n'est pas disponible", "quick_save": "\\{review}Sauvegarde rapide", "quick_save_fail_no_slots": "\\{review}Aucun emplacement de sauvegarde rapide n'est configuré", "save_game": "Partie sauvegardée dans l'emplacement de sauvegarde %d", "save_game_fail_invalid_slot": "Emplacement de sauvegarde %d invalide", "sound_available_samples": "Effets sonores disponibles : %s", "sound_playing_sample": "Lecture de l'effet sonore %d", "speed_get": "Vitesse actuelle : %d", "speed_set": "Vitesse réglée sur %d", "strings_failed": "\\{review}Échec du rechargement des fichiers de langue", "strings_reloaded": "\\{review}Fichiers de langue rechargés", "textures_off": "\\{review}Textures : désactivées", "textures_on": "\\{review}Textures : activées", "trapezoid_filter_off": "Filtre trapézoïdal désactivé", "trapezoid_filter_on": "Filtre trapézoïdal activé", "ui_off": "Interface désactivée", "ui_on": "Interface activée", "unknown_command": "Commande inconnue : %s", "upscaling_factor": "\\{review}Facteur de mise à l'échelle : x%d", "wireframe_mode_off": "\\{review}Mode fil de fer : désactivé", "wireframe_mode_on": "\\{review}Mode fil de fer : activé", }, "overlay": { "debug_animation": "\\{review}Animation : ", "debug_animation_state": "\\{review}État : ", "debug_camera_pos": "\\{review}Origine de la caméra : ", "debug_camera_target": "\\{review}Cible de la caméra : ", "debug_immune": "\\{review}Invulnérabilité activée", "debug_position": "\\{review}Position : ", "debug_rotation": "\\{review}Rotation : ", "debug_speed": "\\{review}Vitesse : ", "item_count_fmt_pc": "\\{small}%s", "item_count_fmt_ps1": "\\{small}%s", }, "passport": { "delete_save": "\\{review}Supprimer", "delete_save_confirm": "\\{review}Supprimer cette sauvegarde ?", "delete_save_failed": "\\{review}Échec de la suppression de la sauvegarde choisie.", "delete_save_no": "\\{review}Non", "delete_save_yes": "\\{review}Oui", "exit_game": "Quitter le jeu", "exit_to_title": "Retourner à l'écran titre", "load_game": "Charger une partie", "mode_new_game": "Nouvelle partie", "mode_new_game_jp": "Version japonaise", "mode_new_game_jp_plus": "Version japonaise+", "mode_new_game_plus": "Nouvelle partie+", "new_game": "Nouvelle partie", "play_previous_levels": "\\{review}Jouer les niveaux précédents", "restart_level": "Recommencer le niveau", "save_game": "Sauvegarder la partie", "save_slot_unsupported": "\\{review}Cette sauvegarde ne prend pas en charge cette fonctionnalité.", "select_level": "Sélectionner le niveau", "select_mod": "\\{review}Sélectionner le jeu", "select_mode": "Sélectionner le mode", "select_save": "\\{review}Sélectionner Enregistrer", "story_so_far": "L'histoire jusqu'à maintenant...", "switch_mod": "\\{review}Changer de jeu", }, "pause": { "are_you_sure": "Êtes-vous sûr ?", "continue": "Continuer", "exit_to_title": "Retourner à l'écran titre ?", "no": "Non", "paused": "Pause", "quit": "Quitter", "yes": "Oui", }, "photo_mode": { "advance_frame": "\\{review}Avancer le cadre", "camera_move_prompt": "Déplacer la caméra", "camera_reset_prompt": "Réinitialiser la caméra", "camera_roll_prompt": "Incliner la caméra", "camera_rotate_90_prompt": "Pivoter de 90 degrés", "camera_rotate_prompt": "Pivoter la caméra", "change_lara_pose": "\\{review}Changer de pose", "fov_prompt": "Ajuster le FOV", "lara_move_prompt": "\\{review}Déplacer Lara", "lara_reset_prompt": "\\{review}Réinitialiser Lara", "lara_roll_prompt": "\\{review}Rouler Lara", "lara_rotate_90_prompt": "\\{review}Faire pivoter Lara de 90°", "lara_rotate_prompt": "\\{review}Faire pivoter Lara", "snap_prompt": "Prendre une photo", "title_camera_pos": "Mode photo", "title_lara_pos": "\\{review}Déplacer Lara", "toggle_help": "Basculer l'aide", }, "settings": { "common": { "all_hidden_disclaimer": "\\{review}Les paramètres sont désactivés pour cet ensemble de niveaux.", "chroma": "Chroma", "edit_value": "Modifier la valeur", "frozen_option_disclaimer": "Ce paramètre est imposé par le créateur du niveau et ne peut pas être modifié.", "hue": "Teinte", "lightness": "Luminosité", "restore_default": "Restaurer", "toggle_help": "Active l'aide", }, "controls": { "backend": { "controller": "Manette", "keyboard": "Clavier", }, "customize": "Personnaliser les contrôles", "layout": { "custom_1": "Contrôles personnalisés 1", "custom_2": "Contrôles personnalisés 2", "custom_3": "Contrôles personnalisés 3", "default": "Touches par défaut", }, "tabs": { "basics": "Mouvement", "items": "Objets", "misc": "Divers", "system": "Système", } }, "gameplay": { "tabs": { "controls": "Contrôles", "fixes": "Correctifs", "general": "Général", "mods": "Mods", "presets": "\\{review}Préréglages", }, "title": "Options de jeu", }, "graphic_settings": { "tabs": { "bars": "\\{review}Barres", "rendering": "Rendu", "stats": "\\{review}Statistiques", "ui": "UI", "visuals": "Visuels", }, "title": "Options graphiques", }, "sound": { "tabs": { "misc": "\\{review}Divers", "volume": "\\{review}Volume", }, "title": "Options sonores", } }, "stats": { "ammo": "Tirs Réussis/Effectués", "ammo_hits": "\\{review}Coups", "ammo_used": "\\{review}Munitions utilisées", "assault_best_time_fmt": "\\{review}%s", "assault_finish": "\\{review}Fin", "assault_no_times_set": "\\{review}Aucun temps défini", "assault_other_times_fmt": "\\{review}%s", "assault_title": "\\{review}MEILLEURS TEMPS", "basic_fmt": "%d", "bonus_statistics": "Statistiques bonus", "crystals": "\\{review}Cristaux", "deaths": "Morts", "detail_fmt": "%d sur %d", "distance_travelled": "Distance parcourue", "final_statistics": "Statistiques finales", "gym_assault_course": "\\{review}Parcours d'assaut", "gym_racetrack_course": "\\{review}Circuit de course", "kills": "Ennemis Tués", "level": "\\{review}Niveau", "medipacks_used": "Trousses de soin utilisées", "none": "\\{review}Aucun", "pickups": "Objets ramassés", "secrets": "Secrets", "time_taken": "Temps écoulé", } }, "console": { "cmd": { "braid": { "help": "Active la tresse de Lara.", }, "cheats": { "help": "Active ou désactive les codes de triche.", }, "clear": { "help": "\\{review}Efface les journaux de la console visibles.", }, "debug": { "help": "Affiche les informations de débogage visuel.", }, "drain": { "help": "Retire toute l'eau de la salle actuelle.", }, "end_level": { "help": "Met fin au niveau actuel.", }, "exit": { "help": "Quitte le jeu.", }, "flipmap": { "help": "Bascule les salles à leur état secondaire.", }, "flood": { "help": "Remplit la salle actuelle d'eau.", }, "fly": { "help": "Active ou désactive le code pour voler.", }, "fps": { "help": "Change le nombre de FPS.", }, "give": { "help": "Ajoute un élément à l'inventaire de Lara.", "invalid_secret": "\\{review}Secret invalide : %s (secrets valides : %s)", "secret_given": "\\{review}Secret ajouté %s", "secret_list": "\\{review}Secrets collectés : %d sur %d (%s)", "secret_none": "\\{review}Secrets collectés : %d sur %d", "secret_taken": "\\{review}Secret retiré %s", }, "give_secret": { "help": "\\{review}Liste les secrets de Lara, ou prend/donne un secret par numéro.", }, "heal": { "help": "Redonne toute sa santé à Lara.", }, "help": { "help": "Affiche l'aide générale ou l'aide détaillée pour une commande.", "list": "Commandes disponibles :", }, "hp": { "help": "Fixe la santé de Lara à la valeur spécifiée.", }, "immune": { "help": "\\{review}Active l'invulnérabilité. (Lara peut toujours être tuée dans certaines circonstances.)", "off": "\\{review}Lara est maintenant vulnérable", "on": "\\{review}Lara est maintenant imperméable aux dégâts", }, "inf_sprint": { "help": "\\{review}Active ou désactive le sprint infini.", "off": "\\{review}Lara ne peut plus sprinter indéfiniment", "on": "\\{review}Lara peut maintenant sprinter indéfiniment", }, "kill": { "help": "Tue les ennemis à proximité.", }, "lighting": { "help": "\\{review}Active le système d'éclairage.", }, "load": { "help": "\\{review}Charge la partie depuis l'emplacement de sauvegarde donné ou depuis une sauvegarde rapide.", }, "lua": { "help": "\\{review}Exécute la chaîne de code Lua donnée.", "runtime_error": "\\{review}Erreur d'exécution Lua : %s", "syntax_error": "\\{review}Erreur de syntaxe Lua : %s", }, "mod": { "help": "\\{review}Bascule vers le mod spécifié et redémarre le jeu.", }, "music": { "help": "Lance la piste musicale pour l'identifiant donné.", }, "play_cutscene": { "help": "Lance la cinématique correspondant au numéro donné.", }, "play_demo": { "help": "Lance la démo correspondant au numéro donné.", }, "play_gym": { "help": "Lance le niveau de gym.", }, "play_level": { "help": "Lance un niveau correspondant au nom ou au numéro donné.", }, "play_music": { "invalid_track": "Piste musicale invalide", "stopped": "\\{review}Musique arrêtée", "track": "Lecture de la piste musicale %d", }, "pos": { "help": "Montre la position de Lara.", }, "save": { "help": "\\{review}Sauvegarde la partie dans l'emplacement de sauvegarde donné ou dans le prochain emplacement de sauvegarde rapide.", }, "screenshot": { "help": "\\{review}Enregistre une capture d'écran sur le disque, avec un chemin optionnel.", }, "set": { "help": "Affiche ou met à jour le paramètre de configuration donné.", }, "sfx": { "help": "Joue un effet sonore avec l'identifiant donné.", }, "spawn": { "fail": "\\{review}Échec de l'apparition de l'objet demandé", "success": "\\{review}Objet demandé apparu près de Lara", }, "speed": { "help": "Change la vitesse du jeu.", }, "strings": { "help": "\\{review}Recharge les fichiers de langue actuels depuis le disque.", }, "teleport": { "item": "Téléporté à l'objet : %d", "item_fail": "Échec du téléport vers l'objet : %d", "object": "Téléporté à l'objet : %s", "object_fail": "Échec du téléport vers l'objet : %s", "pos": "Téléporté à la position : %.3f %.3f %.3f", "pos_fail": "Échec du téléport vers la position : %.3f %.3f %.3f", "room": "Téléporté à la salle : %d", "room_fail": "Échec du téléport vers la salle : %d", }, "textures": { "help": "\\{review}Active ou désactive les textures.", }, "title": { "help": "Retourne à l'écran titre.", }, "tp": { "help": "Téléporte Lara à une position ou un numéro de salle donné.", }, "trigger": { "help": "\\{review}Déclenche ou annule le déclenchement d'un élément par id, nom de l'élément ou nom de l'objet.", "invalid_item": "\\{review}Élément invalide : %s", "no_match": "\\{review}Cible inconnue : %s", "not_found": "\\{review}Aucun élément correspondant trouvé pour : %s", "triggered": "\\{review}Élément(s) déclenché(s) : %s", "untriggered": "\\{review}Élément(s) non déclenché(s) : %s", }, "vsync": { "help": "Active/désactive la synchronisation verticale.", }, "weather": { "help": "\\{review}Change le type de météo actuel.", "invalid": "\\{review}Météo invalide : %s (valide : %s)", "set": "\\{review}Météo réglée sur %s", }, "winston": { "dead": "\\{review}Votre majordome est mort. Monstre !", "spawn_failed": "\\{review}Échec d'invoquer Winston", "spawned": "\\{review}Fait apparaître Winston près de Lara", "teleported": "\\{review}Téléporté Winston près de Lara", }, "wireframe": { "help": "Active/désactive le rendu en fil de fer.", } } }, "dynamic": { "config_presets": { "tr1_pc": "\\{review}TR1 PC", "tr1_ps1": "\\{review}TR1 PS1", "tr2_pc": "\\{review}TR2 PC", "tr2_ps1": "\\{review}TR2 PS1", "tr3_pc": "\\{review}TR3 PC", "tr3_ps1": "\\{review}TR3 PS1", }, "enums": { "bar_look": { "tr1_pc": "TR1 PC", "tr2_pc": "\\{review}TR2 PC", "tr2_ps1": "\\{review}TR2 PS1", "tr3_pc": "\\{review}TR3 PC", "tr3_ps1": "TR3 PS1", }, "lara_outfit": { "default": "\\{review}Par défaut", "golden_sophia": "\\{review}Sophia Dorée", "sophia": "\\{review}Sophia", "tr1_bacon_lara": "\\{review}Lara Bacon", "tr1_classic": "\\{review}TR1 Classique", "tr1_combo": "\\{review}TR1 Combinée", "tr1_golden_bacon_lara": "\\{review}Lara Bacon Dorée", "tr1_golden_lara": "\\{review}TR1 Lara Dorée", "tr1_gym": "\\{review}TR1 Entraînement", "tr1_mauled": "\\{review}TR1 Mutilée", "tr1_ngage": "\\{review}TR1 N-Gage", "tr23_golden_lara": "\\{review}TR2/3 Lara Dorée", "tr2_bomber_jacket": "\\{review}Blouson aviateur", "tr2_classic": "\\{review}TR2 Classique", "tr2_diving_suit": "\\{review}Combinaison de plongée 1", "tr2_diving_suit_alpha": "\\{review}Combinaison de plongée 2", "tr2_gym": "\\{review}TR2 Entraînement", "tr2_robe": "\\{review}Robe", "tr2_vegas": "\\{review}Las Vegas", "tr3_antarctica": "\\{review}Antarctique", "tr3_catsuit": "\\{review}Combinaison moulante", "tr3_classic": "\\{review}TR3 Classique", "tr3_gym": "\\{review}TR3 Entraînement", "tr3_nevada": "\\{review}Nevada", "tr3_south_pacific": "\\{review}Pacifique Sud", } }, "mods": { "tr1": { "title": "\\{review}Tomb Raider I", }, "tr1-demo-pc": { "title": "\\{review}Démo Tomb Raider I", }, "tr1-ub": { "title": "\\{review}Affaires Inachevées", }, "tr2": { "title": "\\{review}Tomb Raider II", }, "tr2-gm": { "title": "\\{review}Le Masque d'Or", }, "tr3": { "title": "\\{review}Tomb Raider III", }, "tr3-la": { "title": "\\{review}L'Artéfact Perdu", } } }, "enums": { "ALLY_HOSTILITY_POLICY": { "ALLY_HOSTILITY_POLICY_INDIVIDUAL": "\\{review}Individuel", "ALLY_HOSTILITY_POLICY_SHARED": "\\{review}Partagé", }, "ASPECT_MODE": { "ASPECT_MODE_16_10": "16:10", "ASPECT_MODE_16_9": "16:9", "ASPECT_MODE_4_3": "4:3", "ASPECT_MODE_ANY": "Automatique", }, "BACKGROUND_TYPE": { "BK_BLACK": "\\{review}Noir", "BK_IMAGE": "\\{review}Image", "BK_MONOCHROME": "\\{review}Monochrome", "BK_MONOCHROME_COOL": "\\{review}Monochrome (froid)", "BK_MONOCHROME_WARM": "\\{review}Monochrome (chaud)", "BK_NONE": "\\{review}Transparent", "BK_PATTERN_STATIC": "\\{review}Statique", "BK_PATTERN_WAVE": "\\{review}Vague", "BK_TRANSPARENT_DARK": "\\{review}Très sombre", "BK_TRANSPARENT_MEDIUM": "\\{review}Sombre", }, "BAR_SHOW_MODE": { "BAR_SHOW_MODE_ALWAYS": "Toujours", "BAR_SHOW_MODE_BOSS_ONLY": "Boss uniquement", "BAR_SHOW_MODE_NEVER": "Jamais", }, "BILLBOARD_LOCK_MODE": { "BILLBOARD_LOCK_NONE": "\\{review}Aucun", "BILLBOARD_LOCK_PERSPECTIVE": "\\{review}Perspective", "BILLBOARD_LOCK_ROLL": "\\{review}Roulis", "BILLBOARD_LOCK_ROLL_PITCH": "\\{review}Roulis et tangage", }, "BLOOD_EFFECTS": { "BLOOD_EFFECTS_DISABLED": "\\{review}Désactivé", "BLOOD_EFFECTS_PINK": "\\{review}Rose", "BLOOD_EFFECTS_RED": "\\{review}Rouge", }, "CAMERA_MODE": { "CAMERA_MODE_TR1": "TR1", "CAMERA_MODE_TR2": "TR2", "CAMERA_MODE_TR3": "\\{review}TR3", }, "CREATURE_DROWN_POLICY": { "CREATURE_DROWN_POLICY_DEFAULT": "\\{review}Par défaut", "CREATURE_DROWN_POLICY_NEVER": "\\{review}Jamais", "CREATURE_DROWN_POLICY_SUBMERGED": "\\{review}Submergé", }, "INPUT_BACKEND": { "INPUT_BACKEND_CONTROLLER": "\\{review}Manette", "INPUT_BACKEND_KEYBOARD": "\\{review}Clavier", }, "INPUT_ROLE": { "INPUT_ROLE_ACTION": "Action", "INPUT_ROLE_CAMERA_BACK": "Reculer la caméra", "INPUT_ROLE_CAMERA_DOWN": "Abaisser la caméra", "INPUT_ROLE_CAMERA_FORWARD": "Avancer la caméra", "INPUT_ROLE_CAMERA_LEFT": "Caméra vers la gauche", "INPUT_ROLE_CAMERA_RESET": "\\{review}Réinitialisation de la caméra", "INPUT_ROLE_CAMERA_RIGHT": "Caméra vers la droite", "INPUT_ROLE_CAMERA_UP": "Réhausser la caméra", "INPUT_ROLE_CHANGE_OUTFIT": "\\{review}Changer de tenue", "INPUT_ROLE_CHANGE_TARGET": "Changer de cible", "INPUT_ROLE_CROUCH": "\\{review}S'accroupir", "INPUT_ROLE_CYCLE_LIGHTING_CONTRAST": "\\{review}Contraste de l'éclairage en cycle", "INPUT_ROLE_DOWN": "Retour", "INPUT_ROLE_DRAW_WEAPON": "Dégainer", "INPUT_ROLE_ENTER_CONSOLE": "Console de développement", "INPUT_ROLE_EQUIP_AUTOS": "\\{review}Équiper les pistolets automatiques", "INPUT_ROLE_EQUIP_DESERT_EAGLE": "\\{review}Équiper le Desert Eagle", "INPUT_ROLE_EQUIP_GRENADE_LAUNCHER": "\\{review}Équiper le lance-grenades", "INPUT_ROLE_EQUIP_HARPOON": "\\{review}Équiper le harpon", "INPUT_ROLE_EQUIP_M16": "\\{review}Équiper le M16", "INPUT_ROLE_EQUIP_MAGNUMS": "S'équiper des magnums", "INPUT_ROLE_EQUIP_MP5": "\\{review}Équiper le MP5", "INPUT_ROLE_EQUIP_PISTOLS": "S'équiper des pistolets", "INPUT_ROLE_EQUIP_ROCKET_LAUNCHER": "\\{review}Équiper le lance-roquettes", "INPUT_ROLE_EQUIP_SHOTGUN": "S'équiper du fusil à pompe", "INPUT_ROLE_EQUIP_UZIS": "S'équiper des Uzi", "INPUT_ROLE_FLY_CHEAT": "Triche de vol", "INPUT_ROLE_FPS": "Afficher les FPS", "INPUT_ROLE_INVENTORY": "Inventaire", "INPUT_ROLE_ITEM_CHEAT": "Triche d'objet", "INPUT_ROLE_JUMP": "Sauter", "INPUT_ROLE_LEFT": "Gauche", "INPUT_ROLE_LEVEL_SKIP_CHEAT": "Passer le niveau", "INPUT_ROLE_LOAD": "\\{review}Charger", "INPUT_ROLE_LOOK": "Regarder", "INPUT_ROLE_PAUSE": "Pause", "INPUT_ROLE_QUICK_LOAD": "\\{review}Chargement rapide", "INPUT_ROLE_QUICK_SAVE": "\\{review}Sauvegarde rapide", "INPUT_ROLE_RIGHT": "Droite", "INPUT_ROLE_ROLL": "Roulade", "INPUT_ROLE_SAVE": "\\{review}Sauvegarder", "INPUT_ROLE_SCREENSHOT": "\\{review}Capture d'écran", "INPUT_ROLE_SLOW": "Marcher", "INPUT_ROLE_SPRINT": "\\{review}Sprint", "INPUT_ROLE_STEP_LEFT": "Pas à gauche", "INPUT_ROLE_STEP_RIGHT": "Pas à droite", "INPUT_ROLE_SWITCH_BORDERS": "\\{review}Changer la taille des bordures", "INPUT_ROLE_SWITCH_UPSCALING": "\\{review}Changer le facteur de mise à l'échelle", "INPUT_ROLE_TOGGLE_BILINEAR_FILTER": "\\{review}Basculer le filtre bilinéaire", "INPUT_ROLE_TOGGLE_FULLSCREEN": "\\{review}Basculer en plein écran", "INPUT_ROLE_TOGGLE_PHOTO_MODE": "Basculer le mode photo", "INPUT_ROLE_TOGGLE_TEXTURES": "\\{review}Basculer les textures", "INPUT_ROLE_TOGGLE_TRAPEZOID_FILTER": "\\{review}Basculer le filtre trapézoïdal", "INPUT_ROLE_TOGGLE_UI": "Basculer l'interface utilisateur", "INPUT_ROLE_TOGGLE_WIREFRAME": "\\{review}Basculer le fil de fer", "INPUT_ROLE_TURBO_CHEAT": "Vitesse turbo", "INPUT_ROLE_UP": "Courir", "INPUT_ROLE_USE_BIG_MEDI": "Grand trousse de soins", "INPUT_ROLE_USE_FLARE": "\\{review}Flare", "INPUT_ROLE_USE_SMALL_MEDI": "Petite trousse de soins", }, "JUMP_LOCK_MODE": { "JUMP_LOCK_DISABLED": "\\{review}Désactivé", "JUMP_LOCK_LEGACY": "\\{review}Hérité", "JUMP_LOCK_TUNED": "\\{review}Ajusté", }, "LIGHTING_CONTRAST": { "LIGHTING_CONTRAST_HIGH": "Élevé", "LIGHTING_CONTRAST_LOW": "Bas", "LIGHTING_CONTRAST_MEDIUM": "Moyen", }, "LOADING_SCREENS_MODE": { "LOADING_SCREENS_ALWAYS": "\\{review}Toujours", "LOADING_SCREENS_DISABLED": "\\{review}Désactivé", "LOADING_SCREENS_NEW_GAMES": "\\{review}Nouvelles parties", }, "LOOK_MODE": { "LOOK_MODE_ENHANCED": "\\{review}Amélioré", "LOOK_MODE_RESTRICTED": "\\{review}Restreint", "LOOK_MODE_UNRESTRICTED": "\\{review}Illimité", }, "MUSIC_LOAD_CONDITION": { "MUSIC_LOAD_CONDITION_ALWAYS": "Tout", "MUSIC_LOAD_CONDITION_NEVER": "Aucune", "MUSIC_LOAD_CONDITION_NON_AMBIENT": "Pistes musicales", }, "PROJECTILE_AREA_DAMAGE": { "PROJECTILE_AREA_DAMAGE_MULTI_SWEEP": "\\{review}Balayage multiple", "PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP": "\\{review}Balayage unique", }, "QUICK_GUNS_MODE": { "QUICK_GUNS_MODE_DRAW_AND_HOLSTER": "\\{review}Équiper & ranger", "QUICK_GUNS_MODE_DRAW_ONLY": "\\{review}Tirer seulement", }, "SCREENSHOT_FORMAT": { "SCREENSHOT_FORMAT_JPEG": "JPG", "SCREENSHOT_FORMAT_PNG": "PNG", }, "SHADOW_TYPE": { "SHADOW_TYPE_CIRCLE": "\\{review}Cercle", "SHADOW_TYPE_OCTAGON": "\\{review}Octogone", "SHADOW_TYPE_SPRITE": "\\{review}Sprite", }, "STATS_STYLE": { "STATS_STYLE_BARE": "\\{review}Simple", "STATS_STYLE_BORDERED": "\\{review}Encadré", }, "SUNGLASSES_MODE": { "SUNGLASSES_MODE_OFF": "\\{review}Désactivé", "SUNGLASSES_MODE_OPAQUE": "\\{review}Opaque", "SUNGLASSES_MODE_TRANSPARENT": "\\{review}Transparent", }, "TARGET_LOCK_MODE": { "TARGET_LOCK_MODE_FULL": "Complet", "TARGET_LOCK_MODE_NONE": "Aucun", "TARGET_LOCK_MODE_SEMI": "Hybride", }, "TEXTURE_FILTER": { "TEXTURE_FILTER_BILINEAR": "Bilinéraire", "TEXTURE_FILTER_POINT": "\\{review}Point", }, "UI_ELEMENT_LOCATION": { "UI_ELEMENT_LOCATION_BOTTOM_CENTER": "En bas centré", "UI_ELEMENT_LOCATION_BOTTOM_LEFT": "En bas à gauche", "UI_ELEMENT_LOCATION_BOTTOM_RIGHT": "En bas à droite", "UI_ELEMENT_LOCATION_TOP_CENTER": "En haut centré", "UI_ELEMENT_LOCATION_TOP_LEFT": "En haut à gauche", "UI_ELEMENT_LOCATION_TOP_RIGHT": "En haut à droit", }, "UI_STYLE": { "UI_STYLE_PC": "PC", "UI_STYLE_PS1": "PS1", }, "WALL_GLITCH_MODE": { "WALL_GLITCH_FIXED": "\\{review}Réparé", "WALL_GLITCH_TR1": "TR1", "WALL_GLITCH_TR2": "TR2", } }, "settings": { "audio.ambient_volume": { "title": "\\{review}Volume ambiant", "description": "\\{review}Ajuste le volume ambiant.", }, "audio.cutscene_volume": { "title": "\\{review}Volume des cinématiques", "description": "\\{review}Ajuste le volume des cinématiques en jeu.", }, "audio.enable_lara_mic": { "title": "Micro sur Lara", "description": "Place le micro au niveau de Lara. Si cette option est désactivée, le micro sera placé au niveau de la caméra.", }, "audio.enable_music_in_inventory": { "title": "\\{review}Jouer de la musique dans l'inventaire", "description": "\\{review}Permet aux sons du jeu, à l'ambiance et à la musique de continuer à jouer dans l'écran d'inventaire.", }, "audio.enable_music_in_menu": { "title": "Musique du menu principal", "description": "Active ou non la musique dans le menu principal.", }, "audio.enable_pitched_sounds": { "title": "Hauteurs aléatoires des sons", "description": "Modifie aléatoirement et légèrement la hauteur des effets sonores, afin de leur donner un peu de variation.", }, "audio.enable_ps1_sfx": { "title": "\\{review}PS1 remplacements SFX", "description": "\\{review}Active des remplacements spécifiques d'effets sonores en utilisant les équivalents PS1.\n\n- Tir d'Uzi (TR1 uniquement)\n- Sons de pieds nus de Lara (TR2 uniquement)", }, "audio.enable_underwater_anim_sfx": { "title": "\\{review}SFX anim. sous l'eau", "description": "\\{review}Permet de contrôler la lecture de certains effets sonores d'animation — pour des objets comme des portes ou des trappes — lorsque la caméra est sous l'eau.", }, "audio.fix_chainblock_secret_sound": { "title": "Corriger le son de chaîne des blocs", "description": "Corrige le fait que le son du secret se joue par erreur lors de l'utilisation de la Clé en Or dans la Tombe de Tihocan.", }, "audio.fix_secrets_killing_music": { "title": "Superposer le son du secret", "description": "Corrige le fait que la musique est coupée à la découverte d'un secret.", }, "audio.fix_speeches_killing_music": { "title": "Superposer les voix des ennemis", "description": "Corrige le fait que la musique est coupée lorsque les ennemis se mettent à parler.", }, "audio.fmv_volume": { "title": "\\{review}Volume des FMV", "description": "\\{review}Ajuste le volume des films.", }, "audio.inventory_ambient_volume": { "title": "\\{review}Volume ambiant (inventaire)", "description": "\\{review}Ajuste le volume ambiant dans l'écran d'inventaire.", }, "audio.inventory_music_volume": { "title": "\\{review}Volume de la musique (inventaire)", "description": "\\{review}Ajuste le volume de la musique dans l'écran d'inventaire.", }, "audio.load_music_triggers": { "title": "Mémoriser les musiques jouées", "description": "Mémorise les musiques qui ont déjà été jouées, pour ne pas les répéter après un rechargement de partie.", }, "audio.master_volume": { "title": "\\{review}\\{icon music} Volume maître", "description": "\\{review}Ajuste le volume de tous les sons du jeu. Les autres réglages sont relatifs à ce volume.", }, "audio.music_load_condition": { "title": "Reprendre la musique au chargement", "description": "Restaure la lecture des pistes audio qui étaient en cours de lecture lors de la sauvegarde.\n- Aucune : ne restaure jamais la lecture au chargement.\n- Pistes musicales : reprend uniquement les pistes musicales (hors ambiance) au chargement.\n- Tout : restaure tout type de piste audio au chargement.", }, "audio.music_volume": { "title": "\\{review}Volume de la musique", "description": "\\{review}Ajuste le volume de la musique.", }, "audio.mute_out_of_focus": { "title": "\\{review}Couper le son lorsque la fenêtre perd le focus", "description": "\\{review}Coupe toute la musique et les effets sonores lorsque la fenêtre du jeu n'est pas au premier plan.", }, "audio.sound_volume": { "title": "\\{icon sound} Volume du son", "description": "Ajuste le volume des effets sonores.", }, "audio.underwater_ambient_volume": { "title": "\\{review}Volume ambiant (sous l'eau)", "description": "\\{review}Ajuste le volume ambiant sous l'eau.", }, "audio.underwater_music_volume": { "title": "\\{review}Volume de la musique (sous l'eau)", "description": "\\{review}Ajuste le volume de la musique sous l'eau.", }, "debug.enable_endless_flare_time": { "title": "\\{review}Temps de torche sans fin", "description": "\\{review}Empêche les torches portatives de s'éteindre. Les torches lancées s'éteindront toujours normalement.", }, "debug.enable_endless_sprint": { "title": "\\{review}Sprint infini", "description": "\\{review}Empêche Lara de se fatiguer lorsqu'elle sprinte. Les obstacles l'arrêteront toujours.", }, "gameplay.ally_hostility_policy": { "title": "\\{review}Politique d'hostilité des alliés", "description": "\\{review}Contrôle la réaction des unités amies lorsqu'elles subissent des dégâts.\n\n- Individuel : chaque allié change d'hostilité de manière indépendante (style TR3).\n- Partagé : tous les alliés deviennent hostiles ensemble (style moine TR2).", }, "gameplay.camera_speed": { "title": "Vitesse de la caméra", "description": "Modifie la vitesse à laquelle se déplace la caméra manuelle.", }, "gameplay.change_pierre_spawn": { "title": "Modifier le comportement d'apparition de Pierre", "description": "Fait en sorte que l'apparition d'un nouveau Pierre (qui s'échappe) remplace l'éventuel Pierre (qui s'échappe) déjà existant.", }, "gameplay.creature_drown_policy": { "title": "\\{review}Comportement de noyade des créatures", "description": "\\{review}Contrôle le comportement des créatures terrestres dans les zones d’eau.\n\n- Jamais: les créatures terrestres ne se noient jamais (style TR1).\n- Par défaut: les créatures terrestres se noient dans une eau d’une profondeur de 2 clics ou plus (style TR2/3).\n- Submergé: les créatures terrestres se noient uniquement lorsqu’elles sont entièrement immergées.", }, "gameplay.disable_extra_guns": { "title": "\\{review}Supprimer les armes supplémentaires", "description": "\\{review}Supprime toutes les armes et munitions ramassables dans le jeu sauf les pistolets (pour les défis uniquement avec pistolets).", }, "gameplay.disable_healing_between_levels": { "title": "Garder les dégâts", "description": "Empêche que Lara ne guérisse automatiquement entre chaque niveau (pour les No Heal challenges).", }, "gameplay.disable_medpacks": { "title": "Supprimer les trousses de soin", "description": "Supprime toutes les trousses de soin de tous les niveaux (pour les No Meds challenges).", }, "gameplay.disable_trex_collision": { "title": "Supprimer la colision du le cadavre du T-Rex", "description": "Supprime toutes les collisions avec le cadavre T-Rex. Cela aide lorsque son cadavre bloque un passage.", }, "gameplay.enable_ally_targeting": { "title": "\\{review}Autoriser le ciblage des alliés", "description": "\\{review}Permet à Lara de cibler des alliés, tels que des moines. Si désactivé, les alliés seront immunisés contre les munitions de Lara.", }, "gameplay.enable_auto_item_selection": { "title": "Présélection des bonnes clés", "description": "Lorsque Lara appuie sur Action en face d'une serreure ou un emplacement de puzzle et qu'elle a l'objet correspondant dans son inventaire, cet objet sera présélectionné.", }, "gameplay.enable_back_slope_stumble": { "title": "\\{review}Trébuchement en pente arrière", "description": "\\{review}Fait trébucher Lara si elle saute en arrière et qu’il y a une pente derrière elle (TR3). Si désactivé, Lara s’arrête net contre la pente (TR1/2).", }, "gameplay.enable_body_bags": { "title": "\\{review}Déclencheurs de sacs mortuaires", "description": "\\{review}Permet la suppression des ennemis tués lorsque Lara traverse certains déclencheurs dans certains niveaux. Si désactivé, les ennemis morts seront toujours affichés.", }, "gameplay.enable_boulder_shake": { "title": "\\{review}Activer le tremblement des rochers", "description": "\\{review}Si activé, la caméra tremblera lorsqu’un rocher est en mouvement.", }, "gameplay.enable_bouncy_grenades": { "title": "\\{review}Grenades rebondissantes", "description": "\\{review}Active le comportement des grenades à la TR3 : elles ricochent sur les murs et les pentes et produisent un rayon d'explosion plus large, mais au prix d'une vitesse réduite.", }, "gameplay.enable_cheats": { "title": "Triche", "description": "Active plusieurs fonctions de triche :\n- L: termine immédiatement le niveau.\n- I: donne toutes les armes à Lara, le maximum en munitions et trousses de soin, et tous les éléments utiles au niveau en cours.\n- O: Fait voler Lara (nager dans les airs).\n - Touche MARCHER : Revenir au sol.\n - Touche ESPACE : Ouvre les portes fermées (peut ne pas fonctionner sur certaines).", }, "gameplay.enable_cinematics": { "title": "\\{review}Séquences scénarisées", "description": "\\{review}Active les séquences scénarisées au début de certains niveaux.", }, "gameplay.enable_compass_stats": { "title": "Statistiques dans la boussole", "description": "Affiche les statistiques du niveau lorsque la boussole est sélectionnée.", }, "gameplay.enable_console": { "title": "Console", "description": "Active la console de développement.", }, "gameplay.enable_controlled_drops": { "title": "\\{review}Chutes contrôlées", "description": "\\{review}Permet à Lara de se retourner en l'air et d’attraper la corniche qu'elle vient de quitter, si la touche d'action est maintenue pendant la chute.", }, "gameplay.enable_crawl_jump": { "title": "\\{review}Saut en sortie de conduit", "description": "\\{review}Permet à Lara de sauter hors des passages étroits.", }, "gameplay.enable_crawl_tilt": { "title": "\\{review}Inclinaison en rampant", "description": "\\{review}Aligne la rotation de Lara sur la géométrie du sol lorsqu'elle rampe.", }, "gameplay.enable_crawling": { "title": "\\{review}Ramper", "description": "\\{review}Permet à Lara de s’accroupir et de ramper.", }, "gameplay.enable_credits": { "title": "\\{review}Écrans de crédits", "description": "\\{review}Active les écrans de crédits affichés après avoir terminé le jeu. N'influence pas l'écran des statistiques finales.", }, "gameplay.enable_crouch_roll": { "title": "\\{review}Roulade accroupie", "description": "\\{review}Permet à Lara d'effectuer une roulade avant en position accroupie en appuyant sur sprint.", }, "gameplay.enable_cutscenes": { "title": "Activer les cutscenes", "description": "Active les cutscenes en jeu.", }, "gameplay.enable_demo": { "title": "Activer le mode démo", "description": "Active le mode démo dans le menu principal.", }, "gameplay.enable_enemy_rotation": { "title": "\\{review}Randomiser l'angle de départ des ennemis", "description": "\\{review}Applique un angle aléatoire supplémentaire à certains ennemis lorsqu'ils sont initialisés.", }, "gameplay.enable_enhanced_saves": { "title": "Sauvegarde les effets", "description": "Améliore les sauvegardes afin que les effets graphiques, la brume des cascades, les émetteurs de flammes, etc. soient sauvegardés au lieu de disparaître au chargement.", }, "gameplay.enable_fmv": { "title": "Activer les FMV", "description": "Active les FMV en jeu.", }, "gameplay.enable_game_modes": { "title": "Autres modes de jeu", "description": "\\{review}Permet de sélectionner de nouveaux modes de jeu dans le menu nouvelle partie du passeport au menu principal.\n- Nouvelle partie+ : Déverrouille toutes les armes avec munitions infinies ; les ennemis ont le double de points de vie.\n- Version Japonaise : Les armes font le double de dégâts.\n- Version Japonaise+ : Combine le mode Version Japonaise et le mode Nouvelle partie+.", }, "gameplay.enable_idle_pose_camera": { "title": "\\{review}Caméra pose", "description": "\\{review}Ajuste la caméra pour qu'elle soit orientée vers Lara pendant sa animation de pose. Appuyer sur regarder pour réinitialiser la caméra.", }, "gameplay.enable_inverted_look": { "title": "Inversion verticale du mode regarder", "description": "Inverse l'axe Y lorsque Lara regarde autour d'elle.", }, "gameplay.enable_item_examining": { "title": "Description des objets", "description": "Pour les niveaux personnalisés - permet d'afficher les descriptions des objets dans l'inventaire lorsque le créateur du niveau a fourni ces informations.", }, "gameplay.enable_jump_twists": { "title": "Sauts périlleux", "description": "\\{review}Activer les demi-tours en plein saut et les cabrioles.", }, "gameplay.enable_killer_pushblocks": { "title": "\\{review}Activer les blocs-poussoirs mortels", "description": "\\{review}Si activé, lorsqu'un bloc-poussoir tombe de l'air et atterrit sur Lara, il la tuera immédiatement. Sinon, Lara se retrouvera coincée sur le dessus du bloc et survivra.", }, "gameplay.enable_lean_jumping": { "title": "Contrôle du saut sur place", "description": "Permet à Lara d'avancer ou de reculer pendant les sauts sur place lorsque la touche correspondante est enfoncée, comme possible depuis TR2.", }, "gameplay.enable_ledge_jumps": { "title": "\\{review}Sauts depuis une corniche", "description": "\\{review}Permet à Lara de sauter vers le haut ou en arrière lorsqu'elle est suspendue à une corniche, à condition qu'il y ait une surface solide devant elle pour prendre appui.", }, "gameplay.enable_legal": { "title": "Afficher le contenu juridique", "description": "Affiche l'écran juridique et la FMV Core Design au début du jeu.", }, "gameplay.enable_manual_camera": { "title": "\\{review}Caméra manuelle", "description": "\\{review}Active les touches de la caméra (\\{input camera_forward}\\{input camera_back}\\{input camera_left}\\{input camera_right}) utilisées pour contrôler la caméra du mode photo, pour également faire pivoter la caméra en jeu.", }, "gameplay.enable_neutral_twists": { "title": "\\{review}Pirouettes sur place", "description": "\\{review}Permet à Lara d'effectuer une rotation en l'air lors d'un saut sur place. Appuyez simultanément sur les touches de saut et de roulade en étant immobile.", }, "gameplay.enable_pickup_aids": { "title": "Aides objets à ramasser", "description": "Active un scintillement intermittent à proximité des objets à ramasser pour les mettre en valeur.", }, "gameplay.enable_play_previous_levels": { "title": "\\{review}Jouer les niveaux précédents", "description": "\\{review}Active les fonctionnalités \"Jouer aux niveaux précédents\" et \"Histoire jusqu'à présent...\" dans l'écran de sélection Nouveau Jeu.", }, "gameplay.enable_responsive_crawl": { "title": "\\{review}Ramper réactif", "description": "\\{review}Active des améliorations par rapport aux mécaniques de ramper d’origine.\n\n- Permet de reprendre le rampement plus rapidement après un arrêt.\n- Permet de passer de la course/sprint au rampement sans devoir s’arrêter d’abord.\n- Permet de passer du rampement à la roulade accroupie (si activée) sans devoir s’accroupir manuellement.\n- Permet de se tourner en position accroupie.\n- Restaure l’animation de ramassage en rampant de Lara (hors torches).", }, "gameplay.enable_responsive_sprint": { "title": "\\{review}Sprint réactif", "description": "\\{review}Active un sprint plus réactif pour Lara.\n\n- permet de sprinter dès que Lara a de l'énergie, plutôt que d'attendre que son endurance soit pleine.\n- permet de sprinter dans les escaliers sans être interrompue par l'animation de course normale.", }, "gameplay.enable_save_crystals": { "title": "Cristaux de sauvegarde", "description": "Limite les sauvegardes aux débuts de niveau et aux cristaux de sauvegarde. Les niveaux ont un nombre limité de cristaux, à usage unique, comme sur PS1. Modifier cette option nécessite de recommencer le niveau.", }, "gameplay.enable_slide_to_run": { "title": "\\{review}Glissade-courir", "description": "\\{review}Permet à Lara de commencer à courir immédiatement lorsqu'elle touche le sol après avoir glissé en avant sur une pente. Maintenez l'entrée avant pour activer.", }, "gameplay.enable_slow_ledge_swing": { "title": "\\{review}Balancement lent sur corniche", "description": "\\{review}Permet à Lara de se balancer lentement lorsqu’elle s’est accrochée à une corniche très fine (style TR3). Si désactivé, Lara se balancera brièvement avant de revenir à une position suspendue immobile (style TR1/2).", }, "gameplay.enable_smooth_wall_deflect": { "title": "\\{review}Déviation murale fluide", "description": "\\{review}Permet à Lara de se remettre plus rapidement après avoir heurté un mur et qu'une touche de direction est maintenue avec l'avant.", }, "gameplay.enable_soft_statics": { "title": "\\{review}Collision de maillage souple", "description": "\\{review}Permet à Lara de se déplacer en douceur contre les maillages statiques – comme dans TR4+ – plutôt que de s'arrêter brutalement.", }, "gameplay.enable_sprint": { "title": "\\{review}Sprint", "description": "\\{review}Permet à Lara de sprinter, comme dans TR3 et les suivants.", }, "gameplay.enable_step_roll_boost": { "title": "\\{review}Boost de roulade de marche", "description": "\\{review}Permet à Lara d'être propulsée d'un pas haut à un clic si la roulade est pressée près du bord.", }, "gameplay.enable_swing_cancel": { "title": "Annulation du balancement", "description": "Permet d'annuler l'animation de balancement à un rebord de Lara en relâchant puis en se raccrochant aussitôt, comme possible depuis TR2.", }, "gameplay.enable_target_change": { "title": "Changement de cible", "description": "Rend possible le changement de cible comme possible depuis TR4. Appuyez sur la touche Regarder tout en visant pour changer de cible.", }, "gameplay.enable_timer_in_inventory": { "title": "Comptabiliser du temps de jeu dans l'inventaire", "description": "Fait avancer le compteur du jeu même quand l'inventaire est ouvert.", }, "gameplay.enable_toggle_crouch": { "title": "\\{review}Basculer l'accroupissement", "description": "\\{review}Permet à Lara de rester accroupie après avoir appuyé une fois sur la touche d'accroupissement. Appuyez de nouveau sur accroupissement pour vous relever.", }, "gameplay.enable_toggle_sprint": { "title": "\\{review}Basculer le sprint", "description": "\\{review}Permet à Lara de continuer à sprinter après avoir appuyé une fois sur la touche de sprint. Appuyez de nouveau sur sprint pour arrêter de sprinter.", }, "gameplay.enable_total_stats": { "title": "Écran des statistiques finales", "description": "Active un écran de statistiques finales du jeu qui apparait à la fin des crédits.", }, "gameplay.enable_tr2_jumping": { "title": "Saut réactif", "description": "\\{review}Permet à Lara de sauter à tout moment pendant qu'elle court.", }, "gameplay.enable_tr2_swim_cancel": { "title": "Arrêt de nage réactif", "description": "Permet à Lara de s'arrêter de manière plus réactive sous l'eau lorsque la touche de nage est relâchée.", }, "gameplay.enable_tr2_swimming": { "title": "Nage fluide", "description": "\\{review}Applique une accélération progressive aux rotations de Lara sous l'eau pour un mouvement plus naturel, comme c'est fait depuis TR2. Désactiver cette option donnera à Lara une vitesse de rotation plus rapide, comme dans TR1 d'origine.", }, "gameplay.enable_uw_roll": { "title": "Roulade sous-marine", "description": "\\{review}Permet à Lara de faire une roulade pour se retourner sous l'eau.", }, "gameplay.enable_wading": { "title": "Patauger", "description": "\\{review}Permet à Lara de patauger dans les eaux peu profondes, plutôt que de rester bloquée à la surface.", }, "gameplay.enable_walk_to_items": { "title": "Animées les placements", "description": "Fait marcher Lara vers les objets et interrupteurs à proximité, au lieu de se téléporter vers eux.", }, "gameplay.fix_alligator_ai": { "title": "Corriger l'IA des aligators", "description": "Corrige le fait que les alligators n'infligent aucun dégât si Lara reste immobile dans l'eau.", }, "gameplay.fix_bear_ai": { "title": "Corriger l'IA des ours", "description": "Corrige l'attaque par coup de patte des ours pour qu'ils ne ratent pas Lara.", }, "gameplay.fix_bridge_collision": { "title": "Corriger la colision des ponts", "description": "Corrige l'impossibilité pour Lara de s'accrocher à des certaines parties de ponts et les murs invisibles à leurs bordures. Corrige également les problèmes de colision pour les ponts-levis, les trappes et les ponts lorsqu'ils sont les uns au-dessus des autres, au-dessus de pentes, ou près du sol.", }, "gameplay.fix_descending_glitch": { "title": "Corriger les chutes sur sols fragiles", "description": "Corrige le fait que les pas de côté et la marche arrière sur des sols fragiles font que Lara se téléporte immédiatement au niveau inférieur.", }, "gameplay.fix_flare_throw_priority": { "title": "Corriger la priorité du jet des torches", "description": "Corrige le fait que Lara priorise le jet des torches usagées en plein vol, ce qui peut l'empêcher d'attraper les rebords.", }, "gameplay.fix_floor_data_issues": { "title": "Corriger les bugs 'floor data'", "description": "Corrige les bugs originaux liés aux informations des secteurs.", }, "gameplay.fix_free_flare_glitch": { "title": "Corrige le glitch de torche gratuite", "description": "Corrige la possibilité de faire apparaître une torche gratuite en appuyant sur la touche Torche pendant que Lara ramasse un objet.", }, "gameplay.fix_item_duplication_glitch": { "title": "Corriger le glitch de duplication d'objet", "description": "Corrige le fait de pouvoir dupliquer l'utilisation des clés dans l'inventaire.", }, "gameplay.fix_lara_pickup_embed": { "title": "\\{review}Bug de ramassage corrigé", "description": "\\{review}Corrige un problème où Lara dérivait parfois dans les murs en ramassant des objets sous l’eau, ainsi qu’en ramassant des objets au-dessus de l’eau sous des plafonds fortement inclinés.", }, "gameplay.fix_m16_accuracy": { "title": "\\{review}Corriger la précision du M16/MP5", "description": "\\{review}Corrige la précision du M16/MP5 pendant que Lara court.", }, "gameplay.fix_monkey_pickup_priority": { "title": "\\{review}Corriger la priorité de ramassage des singes", "description": "\\{review}Les singes attaqués privilégieront la riposte plutôt que de collecter les trousses de soins et les clés.", }, "gameplay.fix_pipeman_aim": { "title": "\\{review}Corriger la visée du pipeman", "description": "\\{review}Corrige le problème où le pipeman ne parvient parfois pas à viser correctement Lara avec ses fléchettes.", }, "gameplay.fix_qwop_glitch": { "title": "Corriger le glitch QWOP", "description": "Corrige le fait que sauter sur des petites marches entraîne parfois Lara à se bloquer dans une animation de course étrange, connu sous le nom du glitch QWOP.", }, "gameplay.fix_step_glitch": { "title": "Corriger le glitch des petites marches", "description": "Corrige le fait que Lara est parfois aspirée dans des murs adjacents à des petites marches lorsqu'elle monte dessus d'une manière spécifique.", }, "gameplay.fix_wade_wall_hit": { "title": "\\{review}Corriger le coup de mur en marchant", "description": "\\{review}Corrige le fait que Lara ne réagisse pas lorsqu'elle heurte un mur en marchant.", }, "gameplay.fix_walk_run_jump": { "title": "Corriger le saut après marche", "description": "Corrige le fait que Lara ne peut parfois pas sauter tout de suite après être passée de son animation de marche à celle de course.", }, "gameplay.fix_wall_geometry": { "title": "\\{review}Corriger la géométrie des murs", "description": "\\{review}Corrige les cas dans la géométrie des niveaux OG où des inclinaisons à l'intérieur des murs peuvent entraîner des calculs de hauteur inexacts.", }, "gameplay.fix_water_exit": { "title": "\\{review}Corriger la sortie de l'eau", "description": "\\{review}Corrige le fait que Lara puisse passer directement d'une pièce d'eau à une pièce sèche adjacente, ou à une pièce sèche en dessous. De plus, cela empêchera Lara de pouvoir sortir de l'eau sur des pentes non praticables.", }, "gameplay.harpoon_recoil": { "title": "\\{review}Recul du harpon", "description": "\\{review}Définit la fréquence à laquelle Lara doit recharger le harpon, en fonction de son nombre de munitions actuel. Par exemple, si réglé sur 3, elle devra recharger après chaque troisième tir. Réglez sur 0 pour désactiver complètement le rechargement.", }, "gameplay.idle_pose_timeout": { "title": "\\{review}Délai inactivité", "description": "\\{review}Permet à Lara d'entrer dans une animation de pose après le nombre de secondes d'inactivité défini. Réglez sur 0 pour désactiver.", }, "gameplay.jump_lock_mode": { "title": "\\{review}Verrou saut", "description": "\\{review}Pour un saut réactif, permet de contrôler à quel moment après le début de la course Lara est autorisée à sauter.\n\n- Hérité : correspond au timing original de TR2.\n- Ajusté : le saut est possible 2 images plus tôt que dans TR2 d'origine.\n- Désactivé : le saut est possible immédiatement après l'animation de départ en course.", }, "gameplay.loading_screens": { "title": "Activer les écrans de chargement", "description": "\\{review}Contrôle les écrans de chargement avant le chargement des niveaux.\n\n- Désactivé : n'affiche jamais les écrans de chargement.\n- Toujours : affiche les écrans de chargement.\n- Nouvelles parties : n'affiche pas les écrans de chargement lors du chargement d'une sauvegarde.", }, "gameplay.look_mode": { "title": "\\{review}Mode regard", "description": "\\{review}Contrôle quand Lara peut utiliser la vue.\n\n- Restreint : le regard est autorisé uniquement à l’arrêt, jamais sous l’eau.\n- Amélioré : le regard est autorisé pendant la plupart des animations, sauf certaines comme pousser un bloc.\n- Illimité : le regard est autorisé à tout moment lorsque Lara est contrôlable.", }, "gameplay.maximum_quick_save_slots": { "title": "\\{review}Nombre d'emplacements de sauvegarde rapide", "description": "\\{review}Modifie le nombre d'emplacements de sauvegarde rapide disponibles.", }, "gameplay.maximum_save_slots": { "title": "Nombre d'emplacements de sauvegarde", "description": "Change le nombre d'emplacements de sauvegarde disponibles.", }, "gameplay.pause_on_focus_lost": { "title": "\\{review}Pause lorsque la fenêtre perd le focus", "description": "\\{review}Interrompt la progression du jeu lorsque la fenêtre du jeu perd le focus.", }, "gameplay.projectile_area_damage": { "title": "\\{review}Dégâts de zone de projectile", "description": "\\{review}Contrôle la propagation de la zone d'effet pour le Lance-roquettes et le Lance-grenades.\n\n- Balayage unique : comportement de TR1 & TR2.\n- Balayage multiple : comportement de TR3.\n\nL'option de balayage multiple entraîne souvent des dégâts doublés sur les ennemis individuels.", }, "gameplay.remember_gun_status": { "title": "Mémoriser les armes entre les niveaux", "description": "\\{review}Mémoriser la dernière arme utilisée par Lara dans le niveau précédent au démarrage du suivant. Sans cette option, Lara reviendra aux pistolets rangés dans leurs holsters.", }, "gameplay.restore_ps1_enemies": { "title": "Restaure les ennemis PS1", "description": "\\{review}Ajoute la momie qui apparaît dans la version PlayStation de City of Khamoon, salle 25.\nModifier cette option nécessitera de redémarrer le jeu.", }, "gameplay.start_lara_hitpoints": { "title": "Points de vie de Lara", "description": "Définit les points de vie de Lara à chaque lancement de niveau.", }, "gameplay.target_mode": { "title": "Mode de verrouillage", "description": "Modifie le comportement de verrouillage des cibles.\n- Complet : Verrouille la cible même si elle sort du champ de vision ou meurt (TR1-3).\n- Hybride : Verrouille la cible si elle sort du champ de vision, mais déverrouille si elle meurt.\n- Aucun : Déverrouille la cible si elle sort du champ de vision ou meurt (TR4+).", }, "gameplay.wall_glitch_mode": { "title": "\\{review}Mode de glitch de mur", "description": "\\{review}Permet d'utiliser le comportement de glitch de mur de TR1 dans TR2 et vice-versa ; permet également de corriger tous les types de glitch de mur.", }, "input.enable_buffering_func_keys": { "title": "\\{review}Mise en mémoire tampon (touches F)", "description": "\\{review}Active la mise en mémoire tampon de la touche F (1 image) pour obtenir un contrôle précis des mouvements de Lara. Cette fonction existe à l'origine uniquement dans le port TombATI (TR1).", }, "input.enable_buffering_inventory": { "title": "\\{review}Mise en mémoire tampon (inventaire)", "description": "\\{review}Active la mise en mémoire tampon de l'inventaire (2 images) pour un contrôle précis des mouvements de Lara.", }, "input.enable_responsive_passport": { "title": "Passeport plus réactif", "description": "Evite de bloquer complètement les contrôles pendant que les pages tournent : à la place, repousse simplement leur effet.", }, "input.enable_tr3_sidesteps": { "title": "Pas de côtés améliorés", "description": "Permet d'effectuer des pas de côtés comme à partir de TR3 (exemple : Maj + touches directionelles). Les touches dédiées restent fonctionnelles.", }, "input.quick_guns_mode": { "title": "\\{review}Touches de l'arme rapide", "description": "\\{review}Contrôle le comportement des touches d'équipement de l'arme rapide.\n\n- Tirer seulement : appuyer sur une touche fera en sorte que Lara équipe l'arme assignée.\n- Tirer ou ranger : identique à ce qui précède, de plus Lara rangera l'arme assignée si elle la porte actuellement.", }, "language": { "title": "Langue", "description": "Change la langue du texte de l'interface utilisateur.", }, "rendering.anisotropy_filter": { "title": "Filtre anisotropique", "description": "Améliore le filtrage des textures au loin.", }, "rendering.aspect_mode": { "title": "Mode d'aspect", "description": "Force un rapport d'aspect du jeu avec des bandes noires.", }, "rendering.borders": { "title": "\\{review}Bordures", "description": "\\{review}Ajoute des bordures noires autour de la fenêtre du jeu.", }, "rendering.enable_trapezoid_filter": { "title": "Filtre trapézoïdal", "description": "Corrige le rendu des quadrilatères.", }, "rendering.enable_vsync": { "title": "VSync", "description": "Active ou désactive la synchronistaion verticale.", }, "rendering.fps": { "title": "FPS", "description": "Définit le nombre d'images par seconde du jeu.", }, "rendering.lighting_contrast": { "title": "Éclairage contrasté", "description": "Augmente le contraste pour les sources de lumière dynamiques telles que les torches et les flashs des armes.", }, "rendering.screenshot_format": { "title": "Format des captures d'écran", "description": "Format de fichier pour les captures d'écran.", }, "rendering.sprite_lock_mode": { "title": "\\{review}Mode verrouillage des sprites", "description": "\\{review}Contrôle les axes à verrouiller lors de l'affichage des sprites à l'écran.\n\n- Aucun : afficher les sprites normalement.\n- Roulis : verrouiller l'axe de roulis – utile uniquement en mode photo.\n- Roulis et tangage : garantir que les sprites restent debout et ne reposent pas au sol lorsqu'on les regarde d'en haut.\n- Perspective : verrouiller les axes de roulis et de tangage et, en plus, faire pivoter légèrement les sprites vers le centre de l'écran.", }, "rendering.texture_filter": { "title": "Filtre des texture", "description": "\\{review}Alterne entre des textures ingame lisses et pixelisées.", }, "rendering.ui_filter": { "title": "\\{review}Filtre UI", "description": "\\{review}Alterne entre des textures UI lisses et pixelisées.", }, "rendering.upscaling_factor": { "title": "\\{review}Facteur de mise à l'échelle", "description": "Augmente la taille du jeu selon un multiplicateur, en maintenant l'aspect pixellisé.", }, "rendering.upscaling_filter": { "title": "\\{review}Filtre de mise à l'échelle", "description": "Applique une apparence lisse ou pixelisée pour tout l'écran.", }, "ui.airbar_color": { "title": "Couleur de la barre d'air", "description": "Couleur de la barre d'air.", }, "ui.airbar_color_ps1": { "title": "Couleur de la barre d'air", "description": "Couleur de la barre d'air.", }, "ui.airbar_location": { "title": "Emplacement de la barre d'air", "description": "Emplacement de la barre d'air.", }, "ui.ammo_counter_location": { "title": "\\{review}Emplacement du compteur de munitions", "description": "\\{review}Emplacement où le compteur de munitions est affiché.", }, "ui.bar_look": { "title": "\\{review}Apparence des barres", "description": "\\{review}Contrôle l'apparence visuelle des barres de l'interface utilisateur.", }, "ui.bar_scale": { "title": "Échelle des barres", "description": "Change la taille des barres de vie, d'air et des ennemis.", }, "ui.enable_bar_flashing": { "title": "\\{review}Barres clignotantes", "description": "\\{review}Fait clignoter les barres de santé et d'oxygène de Lara lorsqu'elle est à court de l'une ou l'autre ressource.", }, "ui.enable_smooth_bars": { "title": "Barres fluides", "description": "Permet à la barre de santé et à la barre d'air d'utiliser des transitions de couleurs fluides.", }, "ui.enable_wraparound": { "title": "Boucler au défilement", "description": "Automatiquement retourner en haut après avoir atteint le bas d'un menu (et vice-versa).", }, "ui.enemy_healthbar_color": { "title": "Couleur de la barre des ennemis", "description": "Couleur de la barre de santé des ennemis.", }, "ui.enemy_healthbar_color_allies": { "title": "\\{review}Couleur de la barre des alliés", "description": "\\{review}Couleur de la barre de vie des alliés. Affichée à l'emplacement des barres de vie des ennemis.", }, "ui.enemy_healthbar_color_allies_ps1": { "title": "\\{review}Couleur de la barre des alliés", "description": "\\{review}Couleur de la barre de vie des alliés. Affichée à l'emplacement des barres de vie des ennemis.", }, "ui.enemy_healthbar_color_ps1": { "title": "Couleur de la barre des ennemis", "description": "Couleur de la barre de santé des ennemis.", }, "ui.enemy_healthbar_location": { "title": "Emplacement de la barre des ennemis", "description": "Emplacement de la barre de santé des ennemis.", }, "ui.enemy_healthbar_show_mode": { "title": "Comportement de la barre des ennemis", "description": "Affiche une barre de santé pour l'ennemi ciblé.", }, "ui.exposurebar_color": { "title": "\\{review}Couleur de la barre d'exposition", "description": "\\{review}Couleur de la barre d'exposition à l'eau froide.", }, "ui.exposurebar_color_ps1": { "title": "\\{review}Couleur de la barre d'exposition", "description": "\\{review}Couleur de la barre d'exposition à l'eau froide.", }, "ui.exposurebar_location": { "title": "\\{review}Emplacement de la barre d'exposition", "description": "\\{review}Emplacement où la barre d'exposition à l'eau froide est affichée.", }, "ui.healthbar_color": { "title": "Couleur de la barre de santé", "description": "Couleur de la barre de santé.", }, "ui.healthbar_color_ps1": { "title": "Couleur de la barre de santé", "description": "Couleur de la barre de santé.", }, "ui.healthbar_location": { "title": "Emplacement barre de santé", "description": "Emplacement de la barre de santé", }, "ui.healthbar_poison_color": { "title": "\\{review}Couleur de la barre de santé empoisonnée", "description": "\\{review}Couleur de la barre de santé lorsque Lara est empoisonnée.", }, "ui.healthbar_poison_color_ps1": { "title": "\\{review}Couleur de la barre de santé empoisonnée", "description": "\\{review}Couleur de la barre de santé lorsque Lara est empoisonnée.", }, "ui.inventory_background_style": { "title": "\\{review}Arrière-plan de l'inventaire", "description": "\\{review}Modifie la façon dont l'arrière-plan de l'anneau d'inventaire est affiché.\n\n- Sombre : TR1 (PC).\n- Très sombre : TR1 (PS1).\n- Statique : TR2 (PC).\n- Ondulé : TR2 (PS1).\n- Monochrome : TR3.", }, "ui.inventory_fade_effects": { "title": "\\{review}Effets de fondu de l'inventaire", "description": "\\{review}Affine les effets de fondu à activer ou désactiver dans l'anneau d'inventaire en jeu. Nécessite que l'option Effets de fondu soit activée pour fonctionner.", }, "ui.menu_style": { "title": "Style des menus", "description": "Changer la manière dont les menus sont affichés.\n - PC : Interface de la version PC.\n - PS1 : Interface de la version PlayStation.", }, "ui.pause_background_style": { "title": "\\{review}Pause en arrière-plan", "description": "\\{review}Modifie la façon dont l'arrière-plan de l'écran de pause est affiché.\n\n- Sombre : TR1 (PC).\n- Très sombre : TR1 (PS1).\n- Statique : TR2 (PC).\n- Ondulé : TR2 (PS1).\n- Monochrome : TR3.", }, "ui.pause_fade_effects": { "title": "\\{review}Effets de fondu de la pause", "description": "\\{review}Affine les effets de fondu à activer ou désactiver dans l'écran de pause. Nécessite que l'option Effets de fondu soit activée pour fonctionner.", }, "ui.pickup_scale": { "title": "\\{review}Échelle de ramassage", "description": "\\{review}Modifie la taille des objets animés dans l'interface utilisateur lorsque Lara ramasse quelque chose.", }, "ui.show_bars": { "title": "\\{review}Afficher les barres", "description": "\\{review}Désactive toutes les barres en jeu, masquant les informations sur la santé de Lara et d'autres ressources (pour les défis).", }, "ui.show_pickups_overlay": { "title": "Affichage des objets ramassés", "description": "Affiche les objets en bas à droite lorsque Lara ramasse quelque chose.", }, "ui.show_title_version": { "title": "\\{review}Texte de la version du titre", "description": "\\{review}Affiche la chaîne de version TRX dans l'anneau d'inventaire du titre.", }, "ui.sprintbar_color": { "title": "\\{review}Couleur de la barre de sprint", "description": "\\{review}Couleur de la barre de sprint.", }, "ui.sprintbar_color_ps1": { "title": "\\{review}Couleur de la barre de sprint", "description": "\\{review}Couleur de la barre de sprint.", }, "ui.sprintbar_location": { "title": "\\{review}Emplacement de la barre de sprint", "description": "\\{review}Emplacement où la barre de sprint est affichée.", }, "ui.stats.show_ammo": { "title": "\\{review}Munitions touchées/utilisées", "description": "\\{review}Affiche la ligne des munitions dans les statistiques du niveau.", }, "ui.stats.show_crystals": { "title": "\\{review}Cristaux", "description": "\\{review}Affiche la rangée des cristaux dans les statistiques du niveau.", }, "ui.stats.show_deaths": { "title": "\\{review}Décès", "description": "\\{review}Affiche les décès de Lara dans les statistiques de la boussole et dans les statistiques du niveau. Le nombre de décès est mis à jour dans la sauvegarde actuellement chargée dès que Lara meurt.", }, "ui.stats.show_distance_travelled": { "title": "\\{review}Distance parcourue", "description": "\\{review}Affiche la ligne de la distance parcourue dans les statistiques du niveau.", }, "ui.stats.show_kills": { "title": "\\{review}Éliminations", "description": "\\{review}Affiche la ligne des éliminations dans les statistiques du niveau.", }, "ui.stats.show_level_header": { "title": "\\{review}Compteur de niveau", "description": "\\{review}Affiche le numéro du niveau actuel en haut des statistiques du niveau.", }, "ui.stats.show_medipacks_used": { "title": "\\{review}Trousse de soins utilisées", "description": "\\{review}Affiche la ligne des trousses de soins utilisées dans les statistiques du niveau.", }, "ui.stats.show_pickups": { "title": "\\{review}Objets ramassés", "description": "\\{review}Affiche la ligne des objets ramassés dans les statistiques du niveau.", }, "ui.stats.show_secrets": { "title": "\\{review}Secrets trouvés", "description": "\\{review}Affiche la ligne des secrets trouvés dans les statistiques du niveau.", }, "ui.stats.show_time_taken": { "title": "\\{review}Temps écoulé", "description": "\\{review}Affiche la ligne du temps écoulé dans les statistiques du niveau.", }, "ui.stats.show_totals": { "title": "\\{review}Afficher les totaux", "description": "\\{review}Affiche les totaux à côté des statistiques lorsque cela est applicable. Les secrets ne sont pas affectés par ce paramètre.", }, "ui.stats.style": { "title": "\\{review}Style des statistiques", "description": "\\{review}Contrôle la façon dont la fenêtre des statistiques est affichée.\n\n- Simple : affiche la mise en page plus simple sans cadre.\n- Encadré : affiche la mise en page encadrée.", }, "ui.stats_background_style": { "title": "\\{review}Arrière-plan des statistiques", "description": "\\{review}Modifie la façon dont l'arrière-plan des statistiques de fin de niveau est affiché.\n\n- Sombre : TR1 (PC).\n- Très sombre : TR1 (PS1).\n- Statique : TR2 (PC).\n- Ondulé : TR2 (PS1).\n- Monochrome : TR3.", }, "ui.stats_fade_effects": { "title": "\\{review}Effets de fondu des statistiques", "description": "\\{review}Affine les effets de fondu à activer ou désactiver dans l'écran des statistiques de fin de niveau. Nécessite que l'option Effets de fondu soit activée pour fonctionner.", }, "ui.text_scale": { "title": "Échelle de texte", "description": "Change la taille du texte de l'interface utilisateur.", }, "visuals.blood_effects": { "title": "\\{review}Effets de sang", "description": "\\{review}Contrôle les couleurs des étincelles de sang.\n\n- Désactivé : aucune étincelle de sang n'est affichée.\n- Rose : la valeur par défaut dans les versions PC allemandes de TR3.\n- Rouge : la valeur par défaut dans toutes les autres versions commerciales.", }, "visuals.camera_mode": { "title": "Mode caméra", "description": "Ajuste le comportement de la caméra lors d'actions comme l'utilisation de clés.", }, "visuals.enable_3d_pickups": { "title": "Objets à ramasser 3D", "description": "Affiche les objets à ramasser en 3D au lieu de les afficher en sprites.", }, "visuals.enable_braid": { "title": "Tresse de Lara", "description": "Active la tresse de Lara.", }, "visuals.enable_breeze": { "title": "Vent", "description": "Active l'effet de vent sur la tresse de Lara dans les salles appropriées.", }, "visuals.enable_exit_fade_effects": { "title": "Fondu de sortie", "description": "Active les effets de fondu lors de la sortie du jeu vers le bureau.", }, "visuals.enable_fade_effects": { "title": "Effets de fondu", "description": "Active les transitions en fondu, par exemple entre les images de crédit ou pour les transitions de l'inventaire et de l'écran de pause.", }, "visuals.enable_fire_lighting": { "title": "\\{review}Allumage de feu", "description": "\\{review}Active l'éclairage dynamique généré à proximité des flammes actives.", }, "visuals.enable_footprints": { "title": "\\{review}Empreintes de pas", "description": "\\{review}Permet l'affichage des empreintes de Lara sur certaines surfaces dans les niveaux pris en charge.", }, "visuals.enable_glide_cameras": { "title": "\\{review}Caméras en glissement", "description": "\\{review}Active un effet de glissement sur les caméras fixes qui regardent Lara en adoptant une courbe de vitesse fluide. Si désactivé, ces caméras changeront immédiatement la vue pour regarder Lara.", }, "visuals.enable_gun_lighting": { "title": "Éclairage des armes", "description": "\\{review}Permet de générer un éclairage dynamique pour les coups de feu et les explosions.", }, "visuals.enable_ps1_crystals": { "title": "Teinte des cristaux PS1", "description": "Les cristaux de sauvegarde seront colorés avec une teinte violette, plus similaires à ceux de la PS1.", }, "visuals.enable_reflections": { "title": "Reflets", "description": "Active les reflets sur certains objets.", }, "visuals.enable_responsive_mesh_tint": { "title": "\\{review}Teinte réactive des meshes", "description": "\\{review}Permet d’afficher les meshes individuelles de Lara avec une teinte aquatique lorsqu’elles se trouvent elles-mêmes sous l’eau (style TR3). Sinon, si Lara est dans l’eau, toutes ses meshes seront affichées avec la teinte (style TR1/2).", }, "visuals.enable_shotgun_flash": { "title": "Flash du fusil", "description": "Génère un flash lorsque Lara tire avec le fusil à pompe, comme pour les autres armes.", }, "visuals.enable_skybox": { "title": "Ciel", "description": "Active le ciel dans les niveaux pris en charge.", }, "visuals.enable_weather": { "title": "\\{review}Météo", "description": "\\{review}Active le rendu des effets météorologiques dans les niveaux pris en charge.", }, "visuals.fix_animated_sprites": { "title": "Corriger les sprites animées", "description": "\\{review}Corrige les sprites originaux des plantes sous-marines pour qu'ils s'animent correctement dans les zones aquatiques.", }, "visuals.fix_item_rots": { "title": "Corriger l'orientation des objets à ramasser", "description": "Corrige les problèmes d'orientation de certains objets, lorsque l'option des objets à ramasser en 3D est utilisée.", }, "visuals.fix_texture_issues": { "title": "Corriger les problèmes de texture", "description": "Corrige des problèmes de textures/morceaux d'objets manquants ou incorrects.", }, "visuals.fog_color": { "title": "\\{review}Couleur du brouillard", "description": "\\{review}Couleur du brouillard.", }, "visuals.fog_end": { "title": "Fin de brouillard", "description": "Définit la distance en secteurs où le brouillard rend tout complètement obscurci.", }, "visuals.fog_start": { "title": "Début de brouillard", "description": "Définit la distance en secteurs où le brouillard commence à apparaître.", }, "visuals.fog_transparency": { "title": "\\{review}Transparence du brouillard", "description": "\\{review}Permet d'activer le mélange de la géométrie lointaine avec des faces 100% transparentes.", }, "visuals.fov": { "title": "\\{review}Champ de vision", "description": "\\{review}Angle de vue en degrés. Des valeurs plus grandes élargissent le champ de vision, des valeurs plus petites le rétrécissent.", }, "visuals.game_brightness": { "title": "Luminosité", "description": "Modifie la luminosité du jeu.", }, "visuals.gamma": { "title": "\\{review}Gamma", "description": "\\{review}Ajuste la courbe gamma. Des valeurs plus élevées signifient un éclairage plus lumineux. La valeur de 2,5 correspond aux couleurs par défaut.", }, "visuals.lara_outfit": { "title": "\\{review}Tenue de Lara", "description": "\\{review}Modifie l’apparence de Lara. Le choix Par défaut respecte les changements de tenue normaux entre les niveaux ; sinon, la tenue sélectionnée restera active jusqu’à modification manuelle.", }, "visuals.shadow_type": { "title": "\\{review}Forme des ombres", "description": "\\{review}Sélectionne la manière dont les ombres des entités sont rendues.\n\n- Octogone : ombres des anciens TR1 et TR2\n- Cercle : ombres rondes\n- Sprite : ombres basées sur des textures TR3", }, "visuals.sunglasses_mode": { "title": "\\{review}Lunettes de soleil de Lara", "description": "\\{review}Modifie le style des lunettes de soleil de Lara. Remarque : les verres seront réfléchissants si l’option correspondante est activée.\n\n- Désactivé : Lara ne portera pas de lunettes de soleil.\n- Opaque : Les lunettes de soleil de Lara auront des verres opaques.\n- Transparent : Les lunettes de soleil de Lara auront des verres semi-transparents.", }, "visuals.ui_brightness": { "title": "Luminosité de l'interface", "description": "Modifie la luminosité de l'interface.", }, "visuals.water_color": { "title": "Couleur de l'eau", "description": "\\{review}Couleur de l'eau.", } }, "objects": { "alarm_sound": { "name": "\\{review}Alarme", }, "alligator": { "name": "Alligator", }, "alphabet": { "name": "\\{review}Police par défaut", }, "alphabet_small": { "name": "\\{review}Petite police", }, "amber_light": { "name": "\\{review}Lumière Ambre", }, "animating_1": { "name": "\\{review}Animation de l'objet 1", }, "animating_10": { "name": "\\{review}Animation de l'objet 10", }, "animating_2": { "name": "\\{review}Animation de l'objet 2", }, "animating_3": { "name": "\\{review}Animation de l'objet 3", }, "animating_4": { "name": "\\{review}Animation de l'objet 4", }, "animating_5": { "name": "\\{review}Animation de l'objet 5", }, "animating_6": { "name": "\\{review}Animation de l'objet 6", }, "animating_7": { "name": "\\{review}Animation de l'objet 7", }, "animating_8": { "name": "\\{review}Animation de l'objet 8", }, "animating_9": { "name": "\\{review}Animation de l'objet 9", }, "ape": { "name": "Singe", }, "area_51_rocket": { "name": "\\{review}Fusée Zone 51", }, "area_51_rocket_blast": { "name": "\\{review}Explosion de la Fusée Zone 51", }, "area_51_rocket_support": { "name": "\\{review}Support de la Fusée Zone 51", }, "assault_digits": { "name": "\\{review}Chiffres d'assaut", }, "assault_target": { "name": "\\{review}Cible d'assaut", }, "atlantean_ground": { "name": "\\{review}Atlante Terrestre", }, "atlantean_shooter": { "name": "\\{review}Atlante (Tirant)", }, "atlantean_winged": { "name": "\\{review}Atlante Ailé", }, "autos": { "name": "\\{review}Pistolets automatiques", }, "autos_ammo": { "name": "\\{review}Chargeurs de pistolet automatique", }, "bacon_lara": { "name": "Bacon-Lara", }, "baldy": { "name": "Le Grand Chauve", }, "bandit_1": { "name": [ "\\{review}Mercenaire 1", "\\{review}Goon masqué 1", ] }, "bandit_2": { "name": [ "\\{review}Mercenaire 2", "\\{review}Goon masqué 2", ] }, "bandit_2b": { "name": [ "\\{review}Mercenaire 3", "\\{review}Goon masqué 3", ] }, "barracuda": { "name": "\\{review}Barracuda", }, "bartoli": { "name": "\\{review}Marco Bartoli", }, "bat": { "name": "Chauve-souris", }, "bat_emitter": { "name": "\\{review}Émetteur de chauve-souris", }, "beacon_light": { "name": "\\{review}Lumière Balise", }, "bear": { "name": "Ours", }, "bell": { "name": "\\{review}Cloche", }, "big_bowl": { "name": "\\{review}Bol de Lave", }, "big_eel": { "name": "\\{review}Grande anguille", }, "big_pod": { "name": "Gros gousse", }, "big_spider": { "name": "\\{review}Géante araignée", }, "bird_guardian": { "name": "\\{review}Monstre oiseau", }, "bird_tweeter_1": { "name": "\\{review}Eau qui goutte", }, "bird_tweeter_2": { "name": "\\{review}Oiseaux chantants", }, "blade": { "name": "\\{review}Lame Murale", }, "blood": { "name": "\\{review}Sang", }, "blood_pink": { "name": "\\{review}Sang (censuré)", }, "blue_light": { "name": "\\{review}Lumière Bleue", }, "boat": { "name": "\\{review}Bateau", }, "boat_bits": { "name": "\\{review}Morceaux de bateau", }, "body_part": { "name": "\\{review}Membre", }, "bridge_flat": { "name": "\\{review}Pont plat", }, "bridge_tilt_1": { "name": "\\{review}Pont incliné 1", }, "bridge_tilt_2": { "name": "\\{review}Pont incliné 2", }, "bubble_1": { "name": "\\{review}Bulle 1", }, "bubble_2": { "name": "Bulle 2", }, "bubble_emitter": { "name": "\\{review}Émetteur de bulles", }, "camera_target": { "name": "\\{review}Cible de caméra", }, "carcass": { "name": "\\{review}Carcasse", }, "ceiling_spikes": { "name": "\\{review}Plafond Épineux", }, "centaur": { "name": "Centaure", }, "centaur_statue": { "name": "Statue", }, "civilian": { "name": "\\{review}Civil", }, "claw_mutant": { "name": "\\{review}Mutant Griffu", }, "clock_chimes": { "name": "\\{review}Horloge de la cachette de Bartoli", }, "cog_1": { "name": "Roue dentée 1", }, "cog_2": { "name": "Roue dentée 2", }, "cog_3": { "name": "Roue dentée 3", }, "combat_end": { "name": "\\{review}Fin de combat", }, "compass": { "name": "\\{review}Statistiques", }, "compy": { "name": "\\{review}Compsognathus", }, "controls": { "name": "\\{review}Contrôles", }, "copter": { "name": "\\{review}Hélicoptère", }, "cowboy": { "name": "Le Cowboy", }, "crawler_mutant": { "name": "\\{review}Mutant Rampant", }, "crocodile": { "name": "Crocodile", }, "crow": { "name": "\\{review}Corbeau", }, "cult_1": { "name": "\\{review}Goon masqué 1", }, "cult_1a": { "name": "\\{review}Goon masqué 2", }, "cult_1b": { "name": "\\{review}Goon masqué 3", }, "cult_2": { "name": "\\{review}Lanceur de couteaux", }, "cult_3": { "name": "\\{review}Goon au fusil", }, "cut_shotgun": { "name": "\\{review}Animation de douche de fusil à pompe", }, "damocles_sword": { "name": "Épée de Damoclès", }, "dart": { "name": "Flèche", }, "dart_effect": { "name": "\\{review}Effet de fléchette", }, "dart_emitter": { "name": "Émetteur de flèches", }, "desert_eagle": { "name": "\\{review}Desert Eagle", }, "desert_eagle_ammo": { "name": "\\{review}Chargeurs du Desert Eagle", }, "detonator_box": { "name": "\\{review}Boîte de détonateur", }, "ding_dong": { "name": "\\{review}Sonnette", }, "dino_mutant": { "name": "Dino-mutant", }, "disc": { "name": "\\{review}Disque", }, "disc_emitter": { "name": "\\{review}Émetteur de Disque", }, "disposable_animating_1": { "name": "\\{review}Objet Animé Jetable 1", }, "disposable_animating_10": { "name": "\\{review}Objet Animé Jetable 10", }, "disposable_animating_2": { "name": "\\{review}Objet Animé Jetable 2", }, "disposable_animating_3": { "name": "\\{review}Objet Animé Jetable 3", }, "disposable_animating_4": { "name": "\\{review}Objet Animé Jetable 4", }, "disposable_animating_5": { "name": "\\{review}Objet Animé Jetable 5", }, "disposable_animating_6": { "name": "\\{review}Objet Animé Jetable 6", }, "disposable_animating_7": { "name": "\\{review}Objet Animé Jetable 7", }, "disposable_animating_8": { "name": "\\{review}Objet Animé Jetable 8", }, "disposable_animating_9": { "name": "\\{review}Objet Animé Jetable 9", }, "diver": { "name": "\\{review}Plongeur", }, "dog": { "name": [ "\\{review}Chien", "\\{review}Doberman", ] }, "door_1": { "name": "Porte 1", }, "door_2": { "name": "Porte 2", }, "door_3": { "name": "Porte 3", }, "door_4": { "name": "Porte 4", }, "door_5": { "name": "Porte 5", }, "door_6": { "name": "Porte 6", }, "door_7": { "name": "Porte 7", }, "door_8": { "name": "Porte 8", }, "dragon_back": { "name": "\\{review}Dragon arrière", }, "dragon_bones_1": { "name": "\\{review}Espace réservé", }, "dragon_bones_2": { "name": "\\{review}Os de dragon avant", }, "dragon_bones_3": { "name": "\\{review}Os de dragon arrière", }, "dragon_front": { "name": "\\{review}Dragon avant", }, "drawbridge": { "name": "\\{review}Pont-levis", }, "dust": { "name": "Poussière", }, "dying_monk": { "name": "\\{review}Moine mourant", }, "dying_mutant": { "name": "\\{review}Mutant Mourant", }, "eagle": { "name": "\\{review}Aigle", }, "earthquake": { "name": "\\{review}Tremblement de terre", }, "eel": { "name": "\\{review}Anguille", }, "electric_cleaner": { "name": "\\{review}Nettoyeur électrique", }, "electric_fence": { "name": "\\{review}Clôture électrique", }, "electrical_light": { "name": "\\{review}Lumière électrique", }, "ember": { "name": "\\{review}Braise", }, "ember_emitter": { "name": "\\{review}Émetteur de braise", }, "explosion_1": { "name": "\\{review}Explosion 1", }, "explosion_2": { "name": "Explosion 2", }, "falling_block_1": { "name": [ "\\{review}Bloc tombant 1", "\\{review}Sol rétractable 1", "\\{review}Tuiles rétractables 1", ] }, "falling_block_2": { "name": [ "\\{review}Bloc tombant 2", "\\{review}Sol rétractable 2", "\\{review}Tuiles rétractables 2", ] }, "falling_block_3": { "name": [ "\\{review}Bloc tombant 3", "\\{review}Sol rétractable 3", "\\{review}Tuiles rétractables 3", "\\{review}Planches lâches", ] }, "falling_ceiling_1": { "name": "\\{review}Plafond Tombant 1", }, "falling_ceiling_2": { "name": "Plafond tombant 2", }, "fire_head": { "name": "\\{review}Tête de Feu", }, "fish_mutant": { "name": "Poisson-mutant", }, "flame": { "name": [ "\\{review}Flamme", "\\{review}Feu", ] }, "flame_emitter": { "name": [ "\\{review}Émetteur de flamme", "\\{review}Émetteur de feu", ] }, "flame_emitter_big": { "name": "\\{review}Émetteur de flammes (Grand)", }, "flame_emitter_jet": { "name": "\\{review}Émetteur de flammes (Jet)", }, "flame_emitter_side": { "name": "\\{review}Émetteur de flammes (Latéral)", }, "flame_emitter_small": { "name": "\\{review}Émetteur de flammes (Petit)", }, "flare": { "name": "\\{review}Fusée éclairante", }, "flare_fire": { "name": "\\{review}Étincelles de fusée éclairante", }, "flares_box": { "name": "\\{review}Boîte de fusées éclairantes", }, "flickering_light": { "name": "\\{review}Lumière Vacillante", }, "fuse_box": { "name": "\\{review}Boîte à fusibles", }, "fx_reserved": { "name": "\\{review}Disque gris", }, "gamma": { "name": "\\{review}Gamma", }, "gas_emitter_green": { "name": "\\{review}Émetteur de Gaz (Vert)", }, "general": { "name": "\\{review}Minisous-marin", }, "globe": { "name": "\\{review}Globe", }, "glow": { "name": "\\{review}Lueur", }, "glow_reserved": { "name": "\\{review}Lueur de la carte", }, "gondola": { "name": "\\{review}Gondole", }, "gong": { "name": "\\{review}Gong", }, "gong_bonger": { "name": "\\{review}Bâton de gong", }, "graphics": { "name": "\\{review}Graphismes", }, "green_light": { "name": "\\{review}Lumière Verte", }, "grenade": { "name": "\\{review}Grenade", }, "grenade_launcher": { "name": "\\{review}Lance-grenades", }, "grenade_launcher_ammo": { "name": "\\{review}Grenades", }, "gun_flash": { "name": "\\{review}Éclair de pistolet", }, "gun_shell": { "name": "\\{review}Cartouche de pistolet", }, "harpoon_bolt": { "name": "\\{review}Bolt de harpon", }, "harpoon_gun": { "name": "\\{review}Fusil harpon", }, "harpoon_gun_ammo": { "name": "\\{review}Harpons", }, "hook": { "name": "\\{review}Crochet", }, "hot_liquid": { "name": "\\{review}Feu supplémentaire", }, "huskie": { "name": "\\{review}Chien", }, "hybrid_mutant": { "name": "\\{review}Mutant Hybride", }, "icicle": { "name": "\\{review}Glaçons", }, "inv_background": { "name": "\\{review}Fond de menu", }, "jelly": { "name": "\\{review}Méduse", }, "kayak": { "name": "\\{review}Kayak", }, "key_1": { "name": "\\{review}Clé 1", }, "key_2": { "name": "\\{review}Clé 2", }, "key_3": { "name": "\\{review}Clé 3", }, "key_4": { "name": "\\{review}Clé 4", }, "key_hole_1": { "name": "\\{review}Trou de clé 1", }, "key_hole_2": { "name": "\\{review}Trou de clé 2", }, "key_hole_3": { "name": "\\{review}Trou de clé 3", }, "key_hole_4": { "name": "\\{review}Trou de clé 4", }, "kill_all_triggered": { "name": "\\{review}Élimination totale déclenchée", }, "killer_statue": { "name": "\\{review}Statue avec Épée", }, "lara": { "name": "\\{review}Lara", }, "lara_alarm": { "name": "\\{review}Cloche d'alarme", }, "lara_autos": { "name": "\\{review}Animation des pistolets automatiques", }, "lara_boat": { "name": "\\{review}Animation du bateau", }, "lara_desert_eagle": { "name": "\\{review}Animation du Desert Eagle", }, "lara_extra": { "name": "\\{review}Animation supplémentaire de Lara", }, "lara_flare": { "name": "\\{review}Animation de la torche", }, "lara_grenade": { "name": "\\{review}Animation du lance-grenades", }, "lara_hair": { "name": "\\{review}Tresse de Lara", }, "lara_harpoon": { "name": "\\{review}Animation du harpon", }, "lara_m16": { "name": "\\{review}Animation du M16", }, "lara_magnums": { "name": "\\{review}Animation des magnums", }, "lara_mp5": { "name": "\\{review}Animation du MP5", }, "lara_pistols": { "name": "\\{review}Animation des pistolets", }, "lara_rocket": { "name": "\\{review}Animation du lance-roquettes", }, "lara_shotgun": { "name": "\\{review}Animation du fusil à pompe", }, "lara_skidoo": { "name": "\\{review}Animation de la motoneige", }, "lara_uzis": { "name": "\\{review}Animation des uzis", }, "larson": { "name": "Larson", }, "lava_wedge": { "name": "Coulée de lave", }, "lead_bar": { "name": "Barre de plomb", }, "lift": { "name": "\\{review}Ascenseur", }, "lightning_emitter": { "name": "Émetteur de foudre", }, "lion": { "name": "Lion", }, "lioness": { "name": "Lionne", }, "lizard": { "name": "\\{review}Lézard", }, "m16": { "name": "\\{review}M16", }, "m16_ammo": { "name": "\\{review}Chargeurs de M16", }, "m16_flash": { "name": "\\{review}Éclair de M16", }, "magnums": { "name": "Magnums", }, "magnums_ammo": { "name": "Chargeurs pour Magnums", }, "mesh_swap_1": { "name": "\\{review}Échange de Maille 1", }, "mesh_swap_2": { "name": "\\{review}Échange de Maille 2", }, "mesh_swap_3": { "name": "\\{review}Échange de Maille 3", }, "midas_touch": { "name": "Main de Midas", }, "mine": { "name": "\\{review}Mine aquatique", }, "mine_cart": { "name": "\\{review}Wagonnet de Mine", }, "mini_copter": { "name": "\\{review}Hélicoptère 2", }, "missile_atlantean_bomb": { "name": "\\{review}Missile (bombe atlante)", }, "missile_atlantean_shard": { "name": "\\{review}Missile (éclat atlante)", }, "missile_flame": { "name": "\\{review}Missile (flamme)", }, "missile_harpoon": { "name": "\\{review}Missile (harpon)", }, "missile_knife": { "name": "\\{review}Missile (couteau)", }, "missile_poison": { "name": "\\{review}Missile (poison)", }, "monk_1": { "name": "\\{review}Moine 1", }, "monk_2": { "name": "\\{review}Moine 2", }, "monkey": { "name": "\\{review}Singe", }, "mounted_gun": { "name": "\\{review}Mitrailleuse Fixe", }, "mouse": { "name": "\\{review}Rat", }, "movable_block_1": { "name": [ "\\{review}Bloc Poussé 1", "\\{review}Bloc Mobile 1", ] }, "movable_block_2": { "name": [ "\\{review}Bloc Poussé 2", "\\{review}Bloc Mobile 2", ] }, "movable_block_3": { "name": [ "\\{review}Bloc Poussé 3", "\\{review}Bloc Mobile 3", ] }, "movable_block_4": { "name": [ "\\{review}Bloc Poussé 4", "\\{review}Bloc Mobile 4", ] }, "moving_bar": { "name": "Barre mobile", }, "mp5": { "name": "\\{review}MP5", }, "mp5_ammo": { "name": "\\{review}Chargeurs de MP5", }, "mp_1": { "name": "\\{review}MP 1", }, "mp_2": { "name": "\\{review}MP 2", }, "mummy": { "name": "Momie", }, "natla": { "name": "Natla", }, "natla_gun": { "name": "\\{review}Arme de Natla", }, "on_off_light": { "name": "\\{review}Lumière Marche/Arrêt", }, "orca": { "name": "\\{review}Orque", }, "passport": { "name": "\\{review}Jeu", }, "patrol_dog": { "name": "\\{review}Chien", }, "pda": { "name": "\\{review}Gameplay", }, "pendulum_1": { "name": "\\{review}Sac de sable", }, "pendulum_2": { "name": "\\{review}Boîte Oscillante", }, "photo": { "name": "\\{review}Maison de Lara", }, "pickup_1": { "name": "\\{review}Objet à ramasser 1", }, "pickup_2": { "name": "\\{review}Objet à ramasser 2", }, "pickup_aid": { "name": "Indice au ramassage", }, "pierre": { "name": "Pierre", }, "pirahnas": { "name": "\\{review}Piranhas", }, "pistols": { "name": "\\{review}Pistolets", }, "pistols_ammo": { "name": "\\{review}Chargeurs de pistolet", }, "player_1": { "name": "\\{review}Acteur de cinématique 1", }, "player_10": { "name": "\\{review}Acteur de cinématique 10", }, "player_2": { "name": "\\{review}Acteur de cinématique 2", }, "player_3": { "name": "\\{review}Acteur de cinématique 3", }, "player_4": { "name": "\\{review}Acteur de cinématique 4", }, "player_5": { "name": "\\{review}Acteur de cinématique 5", }, "player_6": { "name": "\\{review}Acteur de cinématique 6", }, "player_7": { "name": "\\{review}Acteur de cinématique 7", }, "player_8": { "name": "\\{review}Acteur de cinématique 8", }, "player_9": { "name": "\\{review}Acteur de cinématique 9", }, "pods": { "name": "Gousse", }, "poison_dart": { "name": "\\{review}Dard empoisonné", }, "poison_dart_emitter": { "name": "\\{review}Lanceur de dards empoisonnés", }, "portacabin": { "name": "Préfabriqué portable", }, "power_saw": { "name": "\\{review}Scie Électrique", }, "prisoner": { "name": "\\{review}Prisonnier", }, "propeller_1": { "name": "\\{review}Hélice d'Avion", }, "propeller_2": { "name": "\\{review}Hélice Sous-marine", }, "propeller_3": { "name": "\\{review}Ventilateur", }, "pulse_light": { "name": "\\{review}Lumière Pulsée", }, "puma": { "name": "Puma", }, "punk_1": { "name": "\\{review}Punk 1", }, "punk_2": { "name": "\\{review}Punk 2", }, "puzzle_1": { "name": "\\{review}Objet de puzzle 1", }, "puzzle_2": { "name": "\\{review}Objet de puzzle 2", }, "puzzle_3": { "name": "\\{review}Objet de puzzle 3", }, "puzzle_4": { "name": "\\{review}Objet de puzzle 4", }, "puzzle_done_1": { "name": "\\{review}Trou de puzzle 1 (Fait)", }, "puzzle_done_2": { "name": "\\{review}Trou de puzzle 2 (Fait)", }, "puzzle_done_3": { "name": "\\{review}Trou de puzzle 3 (Fait)", }, "puzzle_done_4": { "name": "\\{review}Trou de puzzle 4 (Fait)", }, "puzzle_hole_1": { "name": "\\{review}Trou de puzzle 1 (Vide)", }, "puzzle_hole_2": { "name": "\\{review}Trou de puzzle 2 (Vide)", }, "puzzle_hole_3": { "name": "\\{review}Trou de puzzle 3 (Vide)", }, "puzzle_hole_4": { "name": "\\{review}Trou de puzzle 4 (Vide)", }, "quad_bike": { "name": "\\{review}Quad", }, "quest_1": { "name": "\\{review}Objet de quête 1", }, "quest_2": { "name": "\\{review}Objet de quête 2", }, "quest_3": { "name": "\\{review}Objet de quête 3", }, "quest_4": { "name": "\\{review}Objet de quête 4", }, "raptor": { "name": "Raptor", }, "raptor_emitter": { "name": "\\{review}Émetteur de Raptor", }, "rat": { "name": "Rat", }, "red_light": { "name": "\\{review}Lumière Rouge", }, "rib": { "name": "\\{review}RIB", }, "ricochet": { "name": "\\{review}Ricochet", }, "rocket": { "name": "\\{review}Roquette", }, "rocket_launcher": { "name": "\\{review}Lance-roquettes", }, "rocket_launcher_ammo": { "name": "\\{review}Roquettes", }, "rolling_ball_1": { "name": [ "\\{review}Rocher 1", "\\{review}Boule Roulante", ] }, "rolling_ball_2": { "name": [ "\\{review}Rocher 2", "\\{review}Boule Roulante 2", ] }, "rolling_ball_3": { "name": [ "\\{review}Rocher 3", "\\{review}Boule Roulante 3", ] }, "rolling_ball_4": { "name": [ "\\{review}Rocher 4", "\\{review}Boule Roulante 4", ] }, "rotating_laser": { "name": "\\{review}Laser rotatif", }, "rx_worker_1": { "name": "\\{review}RX Ouvrier 1", }, "rx_worker_2": { "name": "\\{review}RX Ouvrier 2", }, "rx_worker_3": { "name": "\\{review}RX Ouvrier 3", }, "save_crystal": { "name": "Cristal de Sauvegarde", }, "scion": { "name": "Scion", }, "scion_holder": { "name": "Emplacement du Scion", }, "secret_1": { "name": "\\{review}Secret 1", }, "secret_2": { "name": "\\{review}Secret 2", }, "secret_3": { "name": "\\{review}Secret 3", }, "security_guard": { "name": "\\{review}Agent de Sécurité", }, "security_laser_alarm": { "name": "\\{review}Laser de sécurité (Alarme)", }, "security_laser_deadly": { "name": "\\{review}Laser de sécurité (Mortel)", }, "security_laser_killer": { "name": "\\{review}Laser de sécurité (Tueur)", }, "sentry_gun": { "name": "\\{review}Tourelle robotisée", }, "shadow": { "name": "\\{review}Ombre", }, "shark": { "name": "\\{review}Requin", }, "shiva": { "name": "\\{review}Shiva", }, "shotgun": { "name": "\\{review}Fusil à pompe", }, "shotgun_ammo": { "name": "\\{review}Cartouches de fusil à pompe", }, "shotgun_shell": { "name": "\\{review}Cartouche de fusil à pompe", }, "skate_kid": { "name": "Le Skateur", }, "skateboard": { "name": "Skateboard", }, "skidoo_armed": { "name": "\\{review}Motoneige noire", }, "skidoo_driver": { "name": "\\{review}Conducteur de motoneige noire", }, "skidoo_fast": { "name": "\\{review}Motoneige rouge", }, "skidoo_track": { "name": "\\{review}Trace de Motoneige", }, "skybox": { "name": "\\{review}Ciel", }, "sliding_pillar": { "name": "Pilier coulissant", }, "smashable_1": { "name": "\\{review}Fenêtre Brisable 1", }, "smashable_2": { "name": "\\{review}Fenêtre Brisable 2", }, "smashable_3": { "name": "\\{review}Fenêtre Brisable 3", }, "smashable_4": { "name": "\\{review}Fenêtre Brisable 4", }, "smoke_emitter_black": { "name": "\\{review}Émetteur de fumée (noir)", }, "smoke_emitter_white": { "name": "\\{review}Émetteur de fumée (blanc)", }, "snake": { "name": "\\{review}Serpent", }, "snow_sprite": { "name": "\\{review}Sillage de Motoneige", }, "sophia": { "name": "\\{review}Sophia", }, "sound": { "name": "\\{review}Son", }, "sphere_of_doom_1": { "name": "\\{review}Explosion de dragon 1", }, "sphere_of_doom_2": { "name": "\\{review}Explosion de dragon 2", }, "sphere_of_doom_3": { "name": "\\{review}Explosion de dragon 3", }, "spider": { "name": "\\{review}Araignée", }, "spike_wall": { "name": "\\{review}Mur de Pics", }, "spikes": { "name": "\\{review}Pics", }, "spinning_blade": { "name": "\\{review}Lame Tournante", }, "splash_1": { "name": "\\{review}Ondulations de l'eau 1", }, "splash_2": { "name": "Éclaboussures 2", }, "springboard": { "name": "\\{review}Tremplin", }, "steam_emitter": { "name": "\\{review}Émetteur de vapeur", }, "sthpac_mercenary": { "name": "\\{review}Mercenaire du Pacifique Sud", }, "stopwatch": { "name": "\\{review}Statistiques", }, "strobe_light": { "name": "\\{review}Lumière Stroboscopique", }, "swat_1": { "name": "\\{review}SWAT 1", }, "swat_2": { "name": "\\{review}SWAT 2", }, "swat_3": { "name": "\\{review}SWAT 3", }, "swinging_axe": { "name": "\\{review}Hache Oscillante", }, "switch_type_airlock": { "name": "\\{review}Interrupteur de Sas", }, "switch_type_button": { "name": [ "\\{review}Bouton", "\\{review}Bouton poussoir", "\\{review}Interrupteur", ] }, "switch_type_normal": { "name": [ "\\{review}Levier", "\\{review}Interrupteur", ] }, "switch_type_small": { "name": "\\{review}Petit Interrupteur", }, "switch_type_uw": { "name": [ "\\{review}Levier sous-marin", "\\{review}Interrupteur sous-marin", ] }, "switch_type_wheel": { "name": "\\{review}Interrupteur à Roue", }, "teeth_trap": { "name": "\\{review}Piège à Dents", }, "text_box": { "name": "\\{review}Cadre UI", }, "thors_handle": { "name": "Poignée du marteau de Thor", }, "thors_head": { "name": "Marteau de Thor", }, "tiger": { "name": "\\{review}Tigre", }, "tony": { "name": "\\{review}Tony", }, "torso": { "name": "Torso", }, "train": { "name": "\\{review}Train", }, "trapdoor_1": { "name": "Trappe 1", }, "trapdoor_2": { "name": "Trappe 2", }, "trapdoor_3": { "name": "Trappe 3", }, "trex": { "name": "\\{review}T-Rex", }, "trex_alpha": { "name": "\\{review}T-Rex Alpha", }, "tribe_axeman": { "name": "\\{review}Hacheur de la tribu", }, "tribe_boss": { "name": "\\{review}Chef de tribu", }, "tribe_pipeman": { "name": "\\{review}Utilisateur de sarbacane de la tribu", }, "tropical_fish": { "name": "\\{review}Poisson tropical", }, "twinkle": { "name": "\\{review}Paillettes", }, "upv": { "name": "\\{review}Mini-sous-marin", }, "uzis": { "name": "\\{review}Uzis", }, "uzis_ammo": { "name": "\\{review}Chargeurs d'Uzi", }, "vole": { "name": "Campagnol", }, "vulture": { "name": "\\{review}Vautour", }, "wasp_mutant": { "name": "\\{review}Mutant Guêpe", }, "wasp_mutant_emitter": { "name": "\\{review}Émetteur de Mutant Guêpe", }, "water_sprite": { "name": "\\{review}Sillage de Bateau", }, "waterfall": { "name": "\\{review}Brume de cascade", }, "white_light": { "name": "\\{review}Lumière Blanche", }, "willard": { "name": "\\{review}Willard", }, "winston": { "name": "\\{review}Winston", }, "winston_army": { "name": "\\{review}Winston (armée)", }, "wolf": { "name": "Loup", }, "worker_1": { "name": "\\{review}Goon tireur 1", }, "worker_2": { "name": "\\{review}Goon tireur 2", }, "worker_3": { "name": "\\{review}Goon avec bâton 1", }, "worker_4": { "name": "\\{review}Goon avec bâton 2", }, "worker_5": { "name": "\\{review}Goon lance-flammes", }, "xian_knight": { "name": "\\{review}Chevalier Xian", }, "xian_knight_statue": { "name": "\\{review}Statue de chevalier Xian", }, "xian_spearman": { "name": "\\{review}Lancier Xian", }, "xian_spearman_statue": { "name": "\\{review}Statue de lancier Xian", }, "yeti": { "name": "\\{review}Yéti", }, "zipline_handle": { "name": "\\{review}Poignée de Tyrolienne", } } } ================================================ FILE: data/trx/ship/cfg/base_strings-gd.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "language_name": "Gàidhlig", "general": { "actions": { "examine_item": "Sgrùd", "hide_dialog": "Falaich an còmhradh", "reset_defaults": "Ath-shuidhich", "rotate": "Tionndaidh", "unbind": "Falamhaich", "use_item": "Cleachd", }, "config_presets": { "applied": "\\{review}Ro-shealladh air a chur an sàs.", "confirm_description": "\\{review}Bidh na suidheachaidhean a leanas air an atharrachadh:", "confirm_restart_note": "\\{review}Nota: dh’fhaodadh gum feum cuid de shuidheachaidhean ath-thòiseachadh a’ gheama airson buaidh fhaighinn.", "empty": "\\{review}Cha deach ro-shealladh sam bith a lorg.", "no_changes": "\\{review}Chan eil atharrachaidhean ri chur an gnìomh.", "title_fmt": "\\{review}Cuir an ro-shealladh %s an sàs?", }, "globe_select": { "area_1": "Raon 1", "area_2": "Raon 2", "area_3": "Raon 3", "area_4": "Raon 4", "area_5": "Raon 5", "area_6": "Raon 6", }, "inventory_ring": { "heading_adventure": "Cuairt-dànachd", "heading_fmt": "%s", "heading_game_over": "GEAMA DEIREANNACH", "heading_inventory": "CLÀR-SEILBHE", "heading_items": "NITHEAN", "heading_option": "ROGHAINN", "item_count_fmt": "\\{small}%s", "object_name_fmt": "%s", }, "misc": { "demo_mode": "Modh Taisbeanaidh", "direction_keys_controller": "D-pad", "direction_keys_keyboard": "Saigheadan", "empty_slot_fmt": "- SLOT FALAMH -", "exit": "Dùin", "hold_fmt": "Cùm %s", "off": "Dheth", "on": "Air", "pagination_nav": "%d / %d", }, "osd": { "ambiguous_input_2": "Cur-a-steach neo-shoilleir: %s agus %s", "ambiguous_input_3": "Cur-a-steach neo-shoilleir: %s, %s, ...", "bilinear_filter_off": "Criathradh teacsdaire: dheth", "bilinear_filter_on": "Criathradh teacsdaire: air", "command_bad_invocation": "Iarrtas mì-dhligheach: %s", "command_bool": "air, dheth", "command_decimal": "[deicheach]", "command_integer": "[àireamh slàn]", "command_percent": "[àireamh slàn]", "command_unavailable": "Chan eil an àithne seo ri làimh an-dràsta", "command_valid_values": "Luachan dligheach: %s", "complete_level": "Ìre deiseil!", "config_option_get": "Tha %s air a rèiteachadh gu %s", "config_option_set": "%s air atharrachadh gu %s", "config_option_unknown_option": "Roghainn neo-aithnichte: %s", "current_health_get": "Slàinte làithreach Lara: %d", "current_health_set": "Slàinte Lara air a shuidheachadh gu %d", "door_close": "Dùin Sesame!", "door_open": "Fosgail Sesame!", "door_open_fail": "Chan eil doras sam bith faisg air Lara", "flipmap_fail_already_off": "Tha am mapa-flip mar-thà DHETH", "flipmap_fail_already_on": "Tha am mapa-flip mar-thà AIR", "flipmap_off": "Mapa-flip air a chur DHETH", "flipmap_on": "Mapa-flip air a chur AIR", "fly_mode_off": "Modh itealaich air a chur dheth", "fly_mode_on": "Modh itealaich air a chur air", "fps_counter_off": "Cuntair FPS dheth", "fps_counter_on": "Cuntair FPS air", "give_item": "Chaidh %s air a chur ri clàr-seilbhe Lara", "give_item_all_guns": "Glasaich is losgaidh – tha Lara armaichte gu fiadhaich!", "give_item_all_keys": "Ionndrainn! Tha a h-uile iuchair aig Lara a-nis.", "give_item_cheat": "Tha baga-droma Lara mòran nas truime a-nis!", "heal_already_full_hp": "Tha slàinte làn aig Lara mu thràth", "heal_success": "Chaidh slàinte Lara a lìonadh gu lèir", "invalid_cutscene": "Sealladh-film mì-dhligheach", "invalid_demo": "Taisbeanadh mì-dhligheach", "invalid_item": "Nì neo-aithnichte: %s", "invalid_level": "Ìre mì-dhligheach", "invalid_object": "Oibseact mì-dhligheach", "invalid_room": "Seòmar mì-dhligheach: %d. Seòmraichean dligheach: 0 gu %d", "invalid_sample": "Fuaim mì-dhligheach: %d", "kill": "Beannachd leat!", "kill_all": "Puf! Chaidh %d nàimhdean air falbh!", "kill_all_fail": "Och, chan eil nàimhdean air fhàgail airson marbhadh...", "kill_fail": "Chan eil nàmhaid sam bith faisg air làimh...", "lighting_contrast_fmt": "Coimeas Solais: %s", "load_game": "Geama air a luchdadh bho slot %d", "load_game_fail_invalid_slot": "Slot mì-dhligheach: %d", "load_game_fail_unavailable_slot": "Chan eil slot sàbhalaidh %d ri làimh", "object_not_found": "Cha deach an t-oibseact a lorg", "play_cutscene": "A' luchdachadh sealladh-film %d", "play_demo": "A' luchdachadh taisbeanaidh %d", "play_level": "A' luchdachadh %s", "pos_lara_missing": "Chan eil Lara an làthair", "pos_lara_pos_fmt": "Seomair: %d\nSuidheachadh: %.3f, %.3f, %.3f\nCuairteachadh: %.3f, %.3f, %.3f", "pos_level_fmt": "Ìre %d", "pos_level_fmt_cutscene": "Sealladh-film %d", "pos_level_fmt_demo": "Taisbeanadh %d", "quick_load": "Slot luchdaichte gu luath %d", "quick_load_fail_no_bound_slot": "Chan eil slot sàbhalaidh sam bith ceangailte an-dràsta", "quick_load_fail_unavailable_bound_slot": "Chan eil an slot sàbhalaidh ceangailte rim faotainn", "quick_save": "Sàbhailte gu luath", "quick_save_fail_no_slots": "Chan eil slotan sàbhalaidh luath sam bith air an rèiteachadh", "save_game": "Geama air a shàbhaladh gu slot %d", "save_game_fail_invalid_slot": "Slot sàbhalaidh mì-dhligheach: %d", "sound_available_samples": "Fuaimean ri làimh: %s", "sound_playing_sample": "A' cluich fuaim %d", "speed_get": "Astar làithreach: %d", "speed_set": "Astar air a shuidheachadh air %d", "strings_failed": "Dh'fhàillig ath-luchdadh nam faidhlichean cànain", "strings_reloaded": "Chaidh na faidhlichean cànain ath-luchdachadh", "textures_off": "Teacsaichean: dheth", "textures_on": "Teacsaichean: air", "trapezoid_filter_off": "Criathradh trapeasoid dheth", "trapezoid_filter_on": "Criathradh trapeasoid air", "ui_off": "Eadar-aghaidh dheth", "ui_on": "Eadar-aghaidh air", "unknown_command": "Àithne neo-aithnichte: %s", "upscaling_factor": "Factar sgèileachaidh: x%d", "wireframe_mode_off": "Modh frèam-uèir: dheth", "wireframe_mode_on": "Modh frèam-uèir: air", }, "overlay": { "debug_animation": "Beothachadh: ", "debug_animation_state": "Stàit: ", "debug_camera_pos": "Tùs a' chamara: ", "debug_camera_target": "Targaid a' chamara: ", "debug_immune": "Neo-bhàsmhorachd air", "debug_position": "Suidheachadh: ", "debug_rotation": "Cuairteachadh: ", "debug_speed": "Luas: ", "item_count_fmt_pc": "\\{small}%s", "item_count_fmt_ps1": "\\{small}%s", }, "passport": { "delete_save": "\\{review}Sguab às", "delete_save_confirm": "\\{review}A bheil thu airson an t-sàbhail seo a sguabadh às?", "delete_save_failed": "\\{review}Dh'fhàillig an sguabadh às air an t-sàbhail a chaidh a thaghadh.", "delete_save_no": "\\{review}Chan eil", "delete_save_yes": "\\{review}Tha", "exit_game": "Dùin an Geama", "exit_to_title": "Till gu Tiotal", "load_game": "Luchdaich Geama", "mode_new_game": "Geama Ùr", "mode_new_game_jp": "GÙ Iapanach", "mode_new_game_jp_plus": "GÙ+ Iapanach", "mode_new_game_plus": "Geama Ùr+", "new_game": "Geama Ùr", "play_previous_levels": "Cluich ìrean roimhe", "restart_level": "Ath-thòisich an Ìre", "save_game": "Sàbhail Geama", "save_slot_unsupported": "Cha bheir an sàbhaladh seo taic don fheart seo.", "select_level": "Tagh Ìre", "select_mod": "\\{review}Tagh Geama", "select_mode": "Tagh Modh", "select_save": "Tagh Sàbhail", "story_so_far": "An sgeul gu ruige seo...", "switch_mod": "\\{review}Atharraich Geama", }, "pause": { "are_you_sure": "A bheil thu cinnteach?", "continue": "Lean air adhart", "exit_to_title": "Theid dhan tiotal?", "no": "Chan eil", "paused": "Air a stad", "quit": "Dùin", "yes": "Tha", }, "photo_mode": { "advance_frame": "Frèam air adhart", "camera_move_prompt": "Gluais an camara", "camera_reset_prompt": "Ath-shuidhich an camara", "camera_roll_prompt": "Rolla an camara", "camera_rotate_90_prompt": "Cuairtich 90 ceum", "camera_rotate_prompt": "Cuairtich an camara", "change_lara_pose": "Atharraich seasamh", "fov_prompt": "Atharraich FOV", "lara_move_prompt": "Gluais Lara", "lara_reset_prompt": "Ath-shuidhich Lara", "lara_roll_prompt": "Rolla Lara", "lara_rotate_90_prompt": "Tionndaidh Lara 90°", "lara_rotate_prompt": "Tionndaidh Lara", "snap_prompt": "Tog dealbh", "title_camera_pos": "Modh Dealbh", "title_lara_pos": "A' gluasad Lara", "toggle_help": "Tionndaidh cuideachadh", }, "settings": { "common": { "all_hidden_disclaimer": "Tha na roghainnean ciorramach airson nan ìrean seo.", "chroma": "Chroma", "edit_value": "Deasaich luach", "frozen_option_disclaimer": "Tha an suidheachadh seo air a chur an gnìomh le ùghdar nan ìrean agus chan urrainnear ga atharrachadh.", "hue": "Tuar", "lightness": "Soilleireachd", "restore_default": "Ath-shuidhich", "toggle_help": "Tionndaidh cuideachadh", }, "controls": { "backend": { "controller": "Rianadair", "keyboard": "Meur-chlàr", }, "customize": "Gnàthaich na Smachdan", "layout": { "custom_1": "Iuchraichean Cleachdaiche 1", "custom_2": "Iuchraichean Cleachdaiche 2", "custom_3": "Iuchraichean Cleachdaiche 3", "default": "Iuchraichean Bunaiteach", }, "tabs": { "basics": "Gluasad", "items": "Nithean", "misc": "Measgaichte", "system": "Siostam", } }, "gameplay": { "tabs": { "controls": "Smachdan", "fixes": "Càradh", "general": "Cumanta", "mods": "Mods", "presets": "\\{review}Ro-shealladh", }, "title": "Roghainnean Cluiche", }, "graphic_settings": { "tabs": { "bars": "Bàr", "rendering": "Reandaradh", "stats": "\\{review}Staitistig", "ui": "Eadar-aghaidh", "visuals": "Lèirsinneachd", }, "title": "Roghainnean Grafaigeach", }, "sound": { "tabs": { "misc": "Iomlan", "volume": "Tomhas", }, "title": "Roghainnean Fuaime", } }, "stats": { "ammo": "Buaidhean / Losgadh", "ammo_hits": "Buaidhean", "ammo_used": "Losgadh", "assault_best_time_fmt": "%s", "assault_finish": "Crìochnaich", "assault_no_times_set": "Gun Àmanan Fhathast", "assault_other_times_fmt": "%s", "assault_title": "NA H-ÀMANAN AS FHEÀRR", "basic_fmt": "%d", "bonus_statistics": "Staitistig a Bharrachd", "crystals": "\\{review}Criostalan", "deaths": "Bàsan", "detail_fmt": "%d de %d", "distance_travelled": "Astar air a Shiubhal", "final_statistics": "Staitistig Dheireannach", "gym_assault_course": "Cùrsa Ionnsaigh", "gym_racetrack_course": "Cùrsa Rèis", "kills": "Marbhain", "level": "Ìre", "medipacks_used": "Pasganan Cleachdte", "none": "Gin sam bith", "pickups": "Togailtean", "secrets": "Dìomhaireachdan", "time_taken": "Ùine air a Ghabhail", } }, "console": { "cmd": { "braid": { "help": "Tionndaidheas feaman-fuilt Lara.", }, "cheats": { "help": "Tionndaidheas cleasan a' gheama.", }, "clear": { "help": "Glanas logaichean consol follaiseach.", }, "debug": { "help": "Tionndaidheas fiosrachadh deasbug.", }, "drain": { "help": "Drèaneas an t-seomair làithreach.", }, "end_level": { "help": "Crìochnaicheas an ìre làithreach.", }, "exit": { "help": "Dùinidh an gèama.", }, "flipmap": { "help": "Tionndaidheas am mapa-flip.", }, "flood": { "help": "Lìonas an t-seomair làithreach le uisge.", }, "fly": { "help": "Tionndaidheas an cleas itealach.", }, "fps": { "help": "Atharraicheas an suidheachadh FPS.", }, "give": { "help": "Cuireas nì sònraichte anns clàr-seilbhe Lara.", "invalid_secret": "Dìomhaireachd mì-dhligheach: %s (dìomhaireachdan dligheach: %s)", "secret_given": "Chaidh dìomhaireachd %s air a chur ris", "secret_list": "Dìomhaireachdan air an cruinneachadh: %d de %d (%s)", "secret_none": "Dìomhaireachdan air an cruinneachadh: %d de %d", "secret_taken": "Chaidh dìomhaireachd %s air a thoirt air falbh", }, "give_secret": { "help": "Seall liosta de dhìomhaireachdan Lara, neo cuir/thoir air falbh fear a rèir àireamh.", }, "heal": { "help": "Leighis Lara gu slàinte làn a-rithist.", }, "help": { "help": "Seallas cuideachadh airson gach àithne neo cuideachadh mionaideach airson tè shonraichte.", "list": "Òrdughan rim faighinn:", }, "hp": { "help": "Suidhicheas slàinte Lara gu àireamh ainmichte.", }, "immune": { "help": "Tionndaidh neo-bhàsmhorachd. (Faodaidh Lara fhathast bàsachadh ann an cuid de shuidheachaidhean.)", "off": "Tha Lara so-leònte a-nis", "on": "Tha Lara do-ruigsinneach do mhilleadh a-nis", }, "inf_sprint": { "help": "Cuir às do ruith-luath gun chrìoch.", "off": "Chan urrainn do Lara ruith gu luath gun chrìoch tuilleadh", "on": "'S urrainn do Lara ruith gu luath gun chrìoch a-nis", }, "kill": { "help": "Cuir às do nàimhdean a tha faisg air làimh.", }, "lighting": { "help": "Tionndaidh siostam solais", }, "load": { "help": "Luchdaich geama bhon t-slot sàbhalaidh ainmichte neo slot luath.", }, "lua": { "help": "Ruith an t-sreang còd Lua a chaidh a thoirt seachad.", "runtime_error": "Mearachd a' ruith Lua: %s", "syntax_error": "Mearachd sìntacs Lua: %s", }, "mod": { "help": "\\{review}A' gluasad gu am mod sònraichte agus a' tòiseachadh a' gheama a-rithist.", }, "music": { "help": "Cluich slighe-ciùil leis an ID ainmichte.", }, "play_cutscene": { "help": "Cluich sealladh-film leis an àireamh ainmichte.", }, "play_demo": { "help": "Cluich taisbeanadh leis an àireamh ainmichte.", }, "play_gym": { "help": "Cluich ìre an t-Seòmair-eacarsaich.", }, "play_level": { "help": "Cluich ìre leis an ainm neo àireamh ainmichte.", }, "play_music": { "invalid_track": "Slighe-ciùil mì-dhligheach", "stopped": "Sguir an ceòl", "track": "A' cluich ciùil %d", }, "pos": { "help": "Seall suidheachadh Lara.", }, "save": { "help": "Sàbhail geama dhan t-slot ainmichte neo dhan ath shlot luath.", }, "screenshot": { "help": "Sàbhail glacadh-sgrìn gu diosc, le slighe fhaidhle roghainneil.", }, "set": { "help": "Seall neo ùraich roghainn rèiteachaidh ainmichte.", }, "sfx": { "help": "Cluich buaidh-fuaime leis an ID ainmichte.", }, "spawn": { "fail": "Cha deach an nì air iarraidh a chruthachadh", "success": "Chaidh an nì air iarraidh a chruthachadh faisg air Lara", }, "speed": { "help": "Atharraich astar a' gheama.", }, "strings": { "help": "Ath-luchdaich faidhlichean cànain làithreach on diosg.", }, "teleport": { "item": "Air a ghluasad gu nì: %d", "item_fail": "Dh'fhàillig gluasad gu nì: %d", "object": "Air a ghluasad gu oibseact: %s", "object_fail": "Dh'fhàillig gluasad gu oibseact: %s", "pos": "Air a ghluasad gu suidheachadh: %.3f %.3f %.3f", "pos_fail": "Dh'fhàillig gluasad gu suidheachadh: %.3f %.3f %.3f", "room": "Air a ghluasad gu seòmar: %d", "room_fail": "Dh'fhàillig gluasad gu seòmar: %d", }, "textures": { "help": "Cuir air neo dheth an gnìomh teacsaichean.", }, "title": { "help": "Till gu scrion a' thiotail.", }, "tp": { "help": "Gluais Lara gu suidheachadh neo àireamh rùm ainmichte.", }, "trigger": { "help": "Nì nìomhaichte neo gun nìomhaichte le id, ainm nì, neo ainm nì.", "invalid_item": "Nì ceàrr: %s", "no_match": "Targaid neo-aithnichte: %s", "not_found": "Cha deach nithean freagarrach a lorg airson: %s", "triggered": "Nìomhaichte nithean: %s", "untriggered": "Gun nìomhaichte nithean: %s", }, "vsync": { "help": "Tionndaidheas sioncronachadh-inghearach air neo dheth.", }, "weather": { "help": "Atharraichidh seo an aimsir.", "invalid": "Aimsir neo-dhligheach: %s (dligheach: %s)", "set": "Suidhich an aimsir gu %s", }, "winston": { "dead": "Tha am butlar agad marbh. 'S tu nad fuamhaire!", "spawn_failed": "Cha do shoirbhich le Winston a ghairm", "spawned": "Thàinig Winston faisg air Lara", "teleported": "Thàinig Winston faisg air Lara", }, "wireframe": { "help": "Tionndaidh reandaradh frèam-uèir air neo dheth.", } } }, "dynamic": { "config_presets": { "tr1_pc": "\\{review}TR1 PC", "tr1_ps1": "\\{review}TR1 PS1", "tr2_pc": "\\{review}TR2 PC", "tr2_ps1": "\\{review}TR2 PS1", "tr3_pc": "\\{review}TR3 PC", "tr3_ps1": "\\{review}TR3 PS1", }, "enums": { "bar_look": { "tr1_pc": "TR1 PC", "tr2_pc": "TR2 PC", "tr2_ps1": "TR2 PS1", "tr3_pc": "TR3 PC", "tr3_ps1": "TR3 PS1", }, "lara_outfit": { "default": "Bunaiteach", "golden_sophia": "Sophia Òir", "sophia": "Sophia", "tr1_bacon_lara": "Lara Bacon", "tr1_classic": "TR1 Clasaigeach", "tr1_combo": "TR1 Cothlamadh", "tr1_golden_bacon_lara": "Lara Bacon Òir", "tr1_golden_lara": "TR1 Lara Òir", "tr1_gym": "TR1 Trèanadh", "tr1_mauled": "TR1 Air a Leòn", "tr1_ngage": "TR1 N-Gage", "tr23_golden_lara": "TR2/3 Lara Òir", "tr2_bomber_jacket": "Seacaid Bomber", "tr2_classic": "TR2 Clasaigeach", "tr2_diving_suit": "Deise Dàibhidh 1", "tr2_diving_suit_alpha": "Deise Dàibhidh 2", "tr2_gym": "TR2 Trèanadh", "tr2_robe": "Ròba", "tr2_vegas": "Las Vegas", "tr3_antarctica": "An Antartaig", "tr3_catsuit": "Lunnainn", "tr3_classic": "TR3 Clasaigeach", "tr3_gym": "TR3 Trèanadh", "tr3_nevada": "Nevada", "tr3_south_pacific": "A' Chuain Sèimh a Deas", } }, "mods": { "tr1": { "title": "\\{review}Tomb Raider I", }, "tr1-demo-pc": { "title": "\\{review}Tomb Raider I Deuchainn", }, "tr1-ub": { "title": "\\{review}Gnothach Neo-chrìochnaichte", }, "tr2": { "title": "\\{review}Tomb Raider II", }, "tr2-gm": { "title": "\\{review}An Masg Òir", }, "tr3": { "title": "\\{review}Tomb Raider III", }, "tr3-la": { "title": "\\{review}An T-Seann Earraicht", } } }, "enums": { "ALLY_HOSTILITY_POLICY": { "ALLY_HOSTILITY_POLICY_INDIVIDUAL": "Fa leth", "ALLY_HOSTILITY_POLICY_SHARED": "Air a cho-roinn", }, "ASPECT_MODE": { "ASPECT_MODE_16_10": "16:10", "ASPECT_MODE_16_9": "16:9", "ASPECT_MODE_4_3": "4:3", "ASPECT_MODE_ANY": "Gin sam bith", }, "BACKGROUND_TYPE": { "BK_BLACK": "\\{review}Dubh", "BK_IMAGE": "\\{review}Ìomhaigh", "BK_MONOCHROME": "Aon-dhathach", "BK_MONOCHROME_COOL": "\\{review}Monochrome (fuar)", "BK_MONOCHROME_WARM": "\\{review}Monochrome (blàth)", "BK_NONE": "\\{review}Soilleir", "BK_PATTERN_STATIC": "Statach", "BK_PATTERN_WAVE": "Crathadh", "BK_TRANSPARENT_DARK": "Fìor dhorcha", "BK_TRANSPARENT_MEDIUM": "Dorcha", }, "BAR_SHOW_MODE": { "BAR_SHOW_MODE_ALWAYS": "An-còmhnaidh", "BAR_SHOW_MODE_BOSS_ONLY": "A-mhàin ri boss", "BAR_SHOW_MODE_NEVER": "Na seall idir", }, "BILLBOARD_LOCK_MODE": { "BILLBOARD_LOCK_NONE": "Gin sam bith", "BILLBOARD_LOCK_PERSPECTIVE": "Sealladh", "BILLBOARD_LOCK_ROLL": "Rothladh", "BILLBOARD_LOCK_ROLL_PITCH": "Rothladh is claonadh", }, "BLOOD_EFFECTS": { "BLOOD_EFFECTS_DISABLED": "\\{review}Ciorramh", "BLOOD_EFFECTS_PINK": "\\{review}Pinc", "BLOOD_EFFECTS_RED": "\\{review}Dearg", }, "CAMERA_MODE": { "CAMERA_MODE_TR1": "TR1", "CAMERA_MODE_TR2": "TR2", "CAMERA_MODE_TR3": "TR3", }, "CREATURE_DROWN_POLICY": { "CREATURE_DROWN_POLICY_DEFAULT": "Bunaiteach", "CREATURE_DROWN_POLICY_NEVER": "A-riamh", "CREATURE_DROWN_POLICY_SUBMERGED": "Fo uisge", }, "INPUT_BACKEND": { "INPUT_BACKEND_CONTROLLER": "\\{review}Smachdair", "INPUT_BACKEND_KEYBOARD": "\\{review}Meur-chlàr", }, "INPUT_ROLE": { "INPUT_ROLE_ACTION": "Gnìomh", "INPUT_ROLE_CAMERA_BACK": "Camara air Ais", "INPUT_ROLE_CAMERA_DOWN": "Camara Sios", "INPUT_ROLE_CAMERA_FORWARD": "Camara air Adhart", "INPUT_ROLE_CAMERA_LEFT": "Camara gu Chlì", "INPUT_ROLE_CAMERA_RESET": "Ath-shuidhich an camara", "INPUT_ROLE_CAMERA_RIGHT": "Camara gu Dheas", "INPUT_ROLE_CAMERA_UP": "Camara Suas", "INPUT_ROLE_CHANGE_OUTFIT": "Atharraich aodach", "INPUT_ROLE_CHANGE_TARGET": "Atharraich an Targaid", "INPUT_ROLE_CROUCH": "Crùb", "INPUT_ROLE_CYCLE_LIGHTING_CONTRAST": "Cuairt Coimeas Solais", "INPUT_ROLE_DOWN": "Air Ais", "INPUT_ROLE_DRAW_WEAPON": "Uidheamaich", "INPUT_ROLE_ENTER_CONSOLE": "Consol Leasachaidh", "INPUT_ROLE_EQUIP_AUTOS": "Uidheamaich Piostalan Fèin-obrachail", "INPUT_ROLE_EQUIP_DESERT_EAGLE": "Uidheamaich an Desert Eagle", "INPUT_ROLE_EQUIP_GRENADE_LAUNCHER": "Uidheamaich Lannsair Grenèad", "INPUT_ROLE_EQUIP_HARPOON": "Uidheamaich Harpùn", "INPUT_ROLE_EQUIP_M16": "Uidheamaich M16", "INPUT_ROLE_EQUIP_MAGNUMS": "Uidheamaich Magnuman", "INPUT_ROLE_EQUIP_MP5": "Uidheamaich MP5", "INPUT_ROLE_EQUIP_PISTOLS": "Uidheamaich Piostalan", "INPUT_ROLE_EQUIP_ROCKET_LAUNCHER": "Uidheamaich Lannsair Rocaid", "INPUT_ROLE_EQUIP_SHOTGUN": "Uidheamaich Gunna-sgaoil", "INPUT_ROLE_EQUIP_UZIS": "Uidheamaich Uzis", "INPUT_ROLE_FLY_CHEAT": "Cleas Itealaich", "INPUT_ROLE_FPS": "Seall FPS", "INPUT_ROLE_INVENTORY": "Clàr-seilbhe", "INPUT_ROLE_ITEM_CHEAT": "Cleas Nì", "INPUT_ROLE_JUMP": "Leum", "INPUT_ROLE_LEFT": "Gu Chlì", "INPUT_ROLE_LEVEL_SKIP_CHEAT": "Cleas Ìre", "INPUT_ROLE_LOAD": "Luchdaich", "INPUT_ROLE_LOOK": "Coimhead", "INPUT_ROLE_PAUSE": "Stad", "INPUT_ROLE_QUICK_LOAD": "Luchdaich gu luath", "INPUT_ROLE_QUICK_SAVE": "Sàbhail gu luath", "INPUT_ROLE_RIGHT": "Gu Dheas", "INPUT_ROLE_ROLL": "Rolla", "INPUT_ROLE_SAVE": "Sàbhail", "INPUT_ROLE_SCREENSHOT": "Glacadh-sgrìn", "INPUT_ROLE_SLOW": "Coisich", "INPUT_ROLE_SPRINT": "Ruith gu Luath", "INPUT_ROLE_STEP_LEFT": "Ceum air Chlì", "INPUT_ROLE_STEP_RIGHT": "Ceum air Dheas", "INPUT_ROLE_SWITCH_BORDERS": "Atharraich Meud nan Iomallan", "INPUT_ROLE_SWITCH_UPSCALING": "Atharraich Factar Sgèileachaidh", "INPUT_ROLE_TOGGLE_BILINEAR_FILTER": "Tionndaidh Criathradh Teacsdaire", "INPUT_ROLE_TOGGLE_FULLSCREEN": "Tionndaidh Modh-làn-sgrìn", "INPUT_ROLE_TOGGLE_PHOTO_MODE": "Tionndaidh Modh Dealbh", "INPUT_ROLE_TOGGLE_TEXTURES": "Cuir dheth Teacsaichean", "INPUT_ROLE_TOGGLE_TRAPEZOID_FILTER": "Tionndaidh Criathradh Trapeasoid", "INPUT_ROLE_TOGGLE_UI": "Tionndaidh an Eadar-aghaidh", "INPUT_ROLE_TOGGLE_WIREFRAME": "Tionndaidh air frèam-uèir", "INPUT_ROLE_TURBO_CHEAT": "Luas Turbo", "INPUT_ROLE_UP": "Ruith", "INPUT_ROLE_USE_BIG_MEDI": "Pasgan Mòr", "INPUT_ROLE_USE_FLARE": "Cleachd Lòchran", "INPUT_ROLE_USE_SMALL_MEDI": "Pasgan Beag", }, "JUMP_LOCK_MODE": { "JUMP_LOCK_DISABLED": "Dheth", "JUMP_LOCK_LEGACY": "Dualchasach", "JUMP_LOCK_TUNED": "Fìnealta", }, "LIGHTING_CONTRAST": { "LIGHTING_CONTRAST_HIGH": "Àrd", "LIGHTING_CONTRAST_LOW": "Ìosal", "LIGHTING_CONTRAST_MEDIUM": "Meadhanach", }, "LOADING_SCREENS_MODE": { "LOADING_SCREENS_ALWAYS": "An-còmhnaidh", "LOADING_SCREENS_DISABLED": "Ciorramach", "LOADING_SCREENS_NEW_GAMES": "Geamannan ùra", }, "LOOK_MODE": { "LOOK_MODE_ENHANCED": "Leasaichte", "LOOK_MODE_RESTRICTED": "Cuibhrichte", "LOOK_MODE_UNRESTRICTED": "Neo-chuibhrichte", }, "MUSIC_LOAD_CONDITION": { "MUSIC_LOAD_CONDITION_ALWAYS": "An-còmhnaidh", "MUSIC_LOAD_CONDITION_NEVER": "A-riamh", "MUSIC_LOAD_CONDITION_NON_AMBIENT": "Neo-àrainneachd", }, "PROJECTILE_AREA_DAMAGE": { "PROJECTILE_AREA_DAMAGE_MULTI_SWEEP": "Ioma-sgèile", "PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP": "Ioma-singilte", }, "QUICK_GUNS_MODE": { "QUICK_GUNS_MODE_DRAW_AND_HOLSTER": "Tarraing/falach", "QUICK_GUNS_MODE_DRAW_ONLY": "Tarraing a-mhàin", }, "SCREENSHOT_FORMAT": { "SCREENSHOT_FORMAT_JPEG": "JPG", "SCREENSHOT_FORMAT_PNG": "PNG", }, "SHADOW_TYPE": { "SHADOW_TYPE_CIRCLE": "Cearcall", "SHADOW_TYPE_OCTAGON": "Ochd-cheàrnach", "SHADOW_TYPE_SPRITE": "Spraide", }, "STATS_STYLE": { "STATS_STYLE_BARE": "\\{review}Furasta", "STATS_STYLE_BORDERED": "\\{review}Leacanach", }, "SUNGLASSES_MODE": { "SUNGLASSES_MODE_OFF": "Dheth", "SUNGLASSES_MODE_OPAQUE": "Neo-shoilleir", "SUNGLASSES_MODE_TRANSPARENT": "Follaiseach", }, "TARGET_LOCK_MODE": { "TARGET_LOCK_MODE_FULL": "Glas làn", "TARGET_LOCK_MODE_NONE": "Gun ghlas", "TARGET_LOCK_MODE_SEMI": "Leth-ghlas", }, "TEXTURE_FILTER": { "TEXTURE_FILTER_BILINEAR": "Da-lìnear", "TEXTURE_FILTER_POINT": "Dheth", }, "UI_ELEMENT_LOCATION": { "UI_ELEMENT_LOCATION_BOTTOM_CENTER": "Bonn na meadhan", "UI_ELEMENT_LOCATION_BOTTOM_LEFT": "Bonn-clì", "UI_ELEMENT_LOCATION_BOTTOM_RIGHT": "Bonn-deas", "UI_ELEMENT_LOCATION_TOP_CENTER": "Bàrr na meadhan", "UI_ELEMENT_LOCATION_TOP_LEFT": "Bàrr-clì", "UI_ELEMENT_LOCATION_TOP_RIGHT": "Bàrr-deas", }, "UI_STYLE": { "UI_STYLE_PC": "PC", "UI_STYLE_PS1": "PS1", }, "WALL_GLITCH_MODE": { "WALL_GLITCH_FIXED": "Air a chàradh", "WALL_GLITCH_TR1": "TR1", "WALL_GLITCH_TR2": "TR2", } }, "settings": { "audio.ambient_volume": { "title": "Tomhas àrainneachd", "description": "Cuiridh seo tomhas àrainneachd an àite.", }, "audio.cutscene_volume": { "title": "Tomhas nan seallaidhean", "description": "Cuiridh seo tomhas nan seallaidhean sa gheama.", }, "audio.enable_lara_mic": { "title": "Micreofon ri taobh Lara", "description": "Cuiridh seo am micreofon aig suidheachadh Lara. Ma thèid seo air a chur dheth, bidh e aig suidheachadh a' chamara.", }, "audio.enable_music_in_inventory": { "title": "\\{review}Cluich ceòl anns an stòras", "description": "\\{review}Leig le fuaimean a’ gheama, fuaimean àrainneachd agus ceòl a bhith a’ cluich fhathast air an sgrion clàr-stòraidh.", }, "audio.enable_music_in_menu": { "title": "Ceòl sa phrìomh chlàr", "description": "Cluichidh ceòl anns a' phrìomh chlàr.", }, "audio.enable_pitched_sounds": { "title": "Fuaimean atharraichte", "description": "Cuiridh seo cead air buaidhean fuaim le beagan caochlaidhean àirde-fuaim gus iomadachd a thoirt do dh'fhuaimean a' gheama.", }, "audio.enable_ps1_sfx": { "title": "Cleachd fuaimean a' PhS1", "description": "Cleachd fuaimean sònraichte bhon PhS1 an àite an fheaghainn PhC.\n\n - Fuaim Uzi (TR1 a-mhàin)\n- Fuaim casrùisgte Lara (TR2 a-mhàin)", }, "audio.enable_underwater_anim_sfx": { "title": "SFX beòthalachd fon uisge", "description": "Leigidh seo leat smachd a chumail air buaidhean-fuaim beòthalachd shònraichte - airson nithean mar dorsan is trapaichean - nuair a tha a' chamara fon uisge.", }, "audio.fix_chainblock_secret_sound": { "title": "Càraich fuaim bloca slabhraidh", "description": "Cuiridh seo casg air fuaim dìomhaireachd a chluich gu ceàrr nuair a thèid an iuchair òir a chleachdadh ann an Uaigh Tihocan.", }, "audio.fix_secrets_killing_music": { "title": "Ceòl dìomhaireachd ioma-fhilleadh", "description": "Cuiridh seo ceartachadh air far a bheil ceòl dìomhaireachd a' stad an slighe-ciùil a tha a' cluich.", }, "audio.fix_speeches_killing_music": { "title": "Labhairt nàmhaid ioma-fhilleadh", "description": "Cuiridh seo ceartachadh air far a bheil nàimhdean a' stad ciùil gnìomhach nuair a bhruidhneas iad.", }, "audio.fmv_volume": { "title": "Tomhas FMV", "description": "Atharraichidh tomhas nam filmichean.", }, "audio.inventory_ambient_volume": { "title": "Tomhas fuaim àrainneachd (stòradh)", "description": "Atharraichidh tomhas àrainneachd air scrion stòrais.", }, "audio.inventory_music_volume": { "title": "Tomhas ciùil (stòradh)", "description": "Atharraichidh tomhas ciùil air scrion stòrais.", }, "audio.load_music_triggers": { "title": "Càraich ciùil aon-ùine", "description": "Cuiridh seo an luchdachadh air ais de chriomagan ciùil aon-ùine gus nach cluich iad a-rithist gu mearachdach.", }, "audio.master_volume": { "title": "\\{icon music} Tomhas prìomh", "description": "Atharraichidh tomhas fuaim a' gheama gu lèir. Tha na tomhasan eile co-cheangailte ris an fhearr seo", }, "audio.music_load_condition": { "title": "Aisig ceòl aig luchdadh", "description": "Cuiridh seo an slighe-ciùil a bha a' cluich nuair a chaidh an geama a shàbhaladh air ais an sàs aig àm luchdaidh.\n\n- A-riamh: na bi ag ath-chluich ciùil idir.\n- Neo-àrainneachd: ath-chluich ceòl neo-àrainneachdail a-mhàin.\n- An-còmhnaidh: ath-chluich ceòl sam bith.", }, "audio.music_volume": { "title": "Tomhas ciùil", "description": "Atharraichidh tomhas ciùil.", }, "audio.mute_out_of_focus": { "title": "Balbhaich fuaim gun fòcas", "description": "Balbhaichidh seo a h-uile phìos ciùil is buaidh-fuaim nuair nach eil uinneag a' gheama ann am fòcas.", }, "audio.sound_volume": { "title": "\\{icon sound} Meud fuaime", "description": "Cuiridh seo atharrachadh air meud nam buaidhean fuaime.", }, "audio.underwater_ambient_volume": { "title": "Tomhas àrainneachd (fo uisge)", "description": "Atharraichidh tomhas àrainneachd fo uisge.", }, "audio.underwater_music_volume": { "title": "Tomhas ciùil (fo uisge)", "description": "Atharraichidh tomhas ciùil fo uisge.", }, "debug.enable_endless_flare_time": { "title": "Ùine lòchran gun chrìoch", "description": "Cùimidh seo lòchran os laimhe a' deàrrsadh gu bràth. Theid lòchran air an tilgeil a-mach mar as àbhaist.", }, "debug.enable_endless_sprint": { "title": "Ruith gun chrìoch", "description": "Cùimidh seo Lara a' ruith gu bràth fhad 's a tha am putan freagairteach air a phutadh.", }, "gameplay.ally_hostility_policy": { "title": "Poileasaidh nàimhdeas aonaid", "description": "Atharraichidh seo an doigh anns a bhios cairdean a' freagairt nuair a thèid an dochann.\n\n- Fa leth: bidh an caraid - agus an caraid seo a-mhàin - a-nis na nàmhaid (stoidhle TR3).\n- Air a cho-roinn: bidh gach caraid a-nis na nàmhaid (stoidhle TR2).", }, "gameplay.camera_speed": { "title": "Luas a' chamara", "description": "Atharraicheas dè cho luath 'sa ghluaiseas an camera.", }, "gameplay.change_pierre_spawn": { "title": "Atharraich modh nochdaidh Pierre", "description": "Leigidh seo le Pierre ùr a thigeas am bàrr a chuir an àite Pierre eile a tha fhathast beò.", }, "gameplay.creature_drown_policy": { "title": "Poileasaidh bàthadh chreutairean", "description": "Bheir buaidh air an doigh anns a bhios creutairean tìreil gan giùlan fhèin ann an seòmraichean uisge.\n\n- A-riamh: cha bhàsaichidh creutairean tìreil gu bràth (stoidhle TR1).\n- Bunaiteach: bàsaichidh creutairean tìreil ann an uisge 2 chlic no nas doimhne (stoidhle TR2/3).\n- Fo uisge: bàsaichidh creutairean tìreil a-mhàin nuair a tha iad gu tur fon uisge.", }, "gameplay.disable_extra_guns": { "title": "Thoir air falbh armachd a bharrachd", "description": "Thoir air falbh gach armachd agus ammo bhon gheama ach Pistols (airson ruith dùbhlain Pistols a-mhàin).", }, "gameplay.disable_healing_between_levels": { "title": "Droch staid leantainneach", "description": "Cuir stad air Lara bho bhith ag ath-shlànachadh nuair a thòisicheas i ìre ùr (airson ruith gun slànachadh).", }, "gameplay.disable_medpacks": { "title": "Thoir air falbh pasganan slàinte", "description": "Thèid na pasganan slàinte uile a thoirt air falbh às a' gheama (airson ruith 'Gun Leigheas').", }, "gameplay.disable_trex_collision": { "title": "Cuir às ri buaidh T-Rex marbh", "description": "Cuiridh seo às ri buaidh-bhualaidh corp T-Rex às deidh bàs, gus casg a chur air bacadh slighe a-mach.", }, "gameplay.enable_ally_targeting": { "title": "Targaidich air càirdean", "description": "Leigidh seo le Lara a targaidich air càirdean - manaich, 's a leithid. Mura tèid seo a chur an comas, bidh iad dìonach air peilearan Lara.", }, "gameplay.enable_auto_item_selection": { "title": "Roghainn ro-làimh air iuchraichean", "description": "Ma bhriogas Lara air gnìomh aig toll iuchrach neo àite tòimhseachain, taghaidh i an nì ceart gu fèin-obrachail.", }, "gameplay.enable_back_slope_stumble": { "title": "Tuiseal air leathad air chùl", "description": "Bheir seo air Lara tuisleadh ma leumas i air ais agus nuair a tha leathad air a cùlaibh (TR3). Ma tha e à comas, stadaidh Lara gu cruaidh an aghaidh an leathaid (TR1/2).", }, "gameplay.enable_body_bags": { "title": "Pocannan-cuirp", "description": "Cuirish seo an comas a chuir às ri nàimhdean a chaidh a mharbhadh nuair a thèid Lara thairis air brosnachaidhean sònraichte ann an cuid de dh'ìrean. Ma tha e à comas, bidh nàimhdean marbh air an taisbeanadh an-còmhnaidh.", }, "gameplay.enable_boulder_shake": { "title": "Crathadh ulpagan", "description": "Crathaidh an camara nuair a bhios ulpagan a' gluasad.", }, "gameplay.enable_bouncy_grenades": { "title": "Grenadan breabadh", "description": "Cleachd grenadan ann an stoidhle TR3: bualaidh iad air ballachan 's claisean, agus nì iad raon spreadhaidh nas motha, ach aig cosgais astar nas ìsle.", }, "gameplay.enable_cheats": { "title": "Cleasan", "description": "Cuiridh seo diofar chleasan an comas:\n\n- L: crìochnaich an ìre sa bhad.\n- I: bheir do Lara na h-armachd uile, pailteas lòin, pasganan slàinte agus stuthan cuilbheart na h-ìre làithreach.\n- O: cuir snàmh tro èadhar an comas.\n - Coisich: stad a' snàmh tro èadhar.\n - Uidheamaich: fosgail an doras as fhaisge (chan obraichidh seo an-còmhnaidh).", }, "gameplay.enable_cinematics": { "title": "Seallaidhean sgriobtaichte", "description": "Cuiridh seo an doigh seallaidhean sgriobtaichte aig toiseach cuid de dh'ìrean far a bheil iad air an sònrachadh.", }, "gameplay.enable_compass_stats": { "title": "Staitistig ìre anns a' chombaist", "description": "Seallaidh seo staitistig na h-ìre nuair a thaghas tu a' chombaist.", }, "gameplay.enable_console": { "title": "Co-chomhairle leasachaidh", "description": "Cuiridh seo an consol leasachaidh an comas.", }, "gameplay.enable_controlled_drops": { "title": "Tuit fo smachd", "description": "Leigidh seo le Lara tionndadh san adhar agus grèim a ghabhail air an oir a dh'fhàg i, fhad 's a tha am putan gnìomh air a chumail sios tron tuiteam.", }, "gameplay.enable_crawl_jump": { "title": "Leum às snàg", "description": "Leigidh le Lara leum a-mach à àiteachan snàgaidh.", }, "gameplay.enable_crawl_tilt": { "title": "Claonadh snàgail", "description": "Nì seo co-thaobhadh air cuairteachadh Lara a-reir geoimeatraidh an làir nuair a tha i a' snàgail.", }, "gameplay.enable_crawling": { "title": "Snàgail", "description": "Leigidh seo le Lara cromadh sìos agus snàgail.", }, "gameplay.enable_credits": { "title": "Sgrìoban creideis", "description": "Cuiridh seo sgrìoban creideis an comas às dèidh crìochnachadh a' gheama. Cha bheireas seo buaidh air an sgrion staitistig mu dheireadh.", }, "gameplay.enable_crouch_roll": { "title": "Rolla crom", "description": "Leigidh seo le Lara rolla air adhart a dhèanamh fhad 's a tha i crom le putan sprint.", }, "gameplay.enable_cutscenes": { "title": "Seallaidhean-film", "description": "Cuiridh seo cluich seallaidhean-film an comas.", }, "gameplay.enable_demo": { "title": "Modh taisbeanaidh", "description": "Leigidh le seo taisbeanadhan a chluich bhon phrìomh chlàr.", }, "gameplay.enable_enemy_rotation": { "title": "Ceàrn tùsail nàimhdean", "description": "Cuiridh ceàrn thuilleadh air thuaiream ri cuid de na nàimhdean nuair a thòisicheas iad.", }, "gameplay.enable_enhanced_saves": { "title": "Buaidhean sàbhalaidh", "description": "Leasaichidh sàbhaladh geama gus am bi buaidhean grafaigeach, ceò easan, sgaoiladairean lasraidh, agus tuilleadh, air an sàbhaladh an àite a dhol à sealladh nuair a luchdaichear iad.", }, "gameplay.enable_fmv": { "title": "Bhideothan", "description": "Cuiridh seo cluich bhideothan an comas.", }, "gameplay.enable_game_modes": { "title": "Taghadh modh geama", "description": "Leigeas le roghainnean Geama Ùr+ a bhith rim faotainn sa phasport aig toiseach a' gheama.\n\n- Geama Ùr+: fuasgladh nan armachd air fad le peilearan gun chrìoch; bidh HP dùbailte aig na nàimhdean.\n- GÙ Iapanach: bidh na h-armachd ag adhbhrachadh milleadh dùbailte, agus bheir togail lòchrain 8 seach 6.\n- GÙ+ Iapanach: measgachadh de GÙ+ is GÙ Iapanach.", }, "gameplay.enable_idle_pose_camera": { "title": "Camara seasaimh", "description": "Gluaisidh an camara gus aghaidh a thoirt air Lara rè a beòthalachd seasaimh. Put seall gus an camara a ath-shuidheachadh.", }, "gameplay.enable_inverted_look": { "title": "Sealladh ais-iompaichte", "description": "Tionndaidhidh an smachd Y-axis nuair a choimheadas Lara.", }, "gameplay.enable_item_examining": { "title": "Sgrùdadh nithean", "description": "Airson ìrean gnàthaichte — leigidh seo le tuairisgeulan nì a nochdadh san t-seilbh ma bheir ùghdar na h-ìre dàta freagarrach.", }, "gameplay.enable_jump_twists": { "title": "Snìomh leum", "description": "Cuiridh seo snìomh is leth-chasach TR2+ an comas — m.e. brùth rolla rè leum neo dàibheadh.", }, "gameplay.enable_killer_pushblocks": { "title": "Cleachd blocaichean putadh marbhach", "description": "Ma tha seo comasach, nuair a thuiteas bloc putadh bhon adhar air Lara, cuiridh e i gu bàs i. Mur nach eil, theid Lara troimh a' bhloc.", }, "gameplay.enable_lean_jumping": { "title": "Leum le claonadh", "description": "Leigidh Lara sleamhnachadh beagan air adhart/air ais le leum neodrach ma bhithear a' cumail iuchair stiùiridh.", }, "gameplay.enable_ledge_jumps": { "title": "Leum bho oir", "description": "Leigidh seo le Lara leum suas neo air ais nuair a tha i a' crochadh air oir, fhad 's a tha uachdar cruaidh os a comhair airson putadh às.", }, "gameplay.enable_legal": { "title": "Sgrionaichean laghail", "description": "Seallaidh seo scrion laghail agus bhideo Core Design aig toiseach a' gheama.", }, "gameplay.enable_manual_camera": { "title": "Camara làimhe", "description": "Cuir comas air putanan a' chamara (\\{input camera_forward}\\{input camera_back}\\{input camera_left}\\{input camera_right}) a thathas a' cleachdadh gus smachd a chumail air camara Modh Dealbh, gus cuideachd camara a' gheama a chuairteachadh.", }, "gameplay.enable_neutral_twists": { "title": "Snìomh neodrach", "description": "Leigidh seo le Lara tionndadh san adhar fhad 's a tha i a' leum gu neodrach. Brùth air na putanan leum is rola còmhla ri chèile nuair a tha i na seasamh.", }, "gameplay.enable_pickup_aids": { "title": "Taic togail", "description": "Cuiridh seo buaidh dealrach goirid faisg air nithean togail gus an dèanamh nas fhasa a dh'fhaicinn.", }, "gameplay.enable_play_previous_levels": { "title": "Cluich ìrean roimhe", "description": "Cuir comas air na feartan \"Cluich ìrean roimhe\" agus \"Sgeulachd gu ruige seo...\" anns an sgrion taghaidh Geama Ùr.", }, "gameplay.enable_responsive_crawl": { "title": "Snàgadh freagairteach", "description": "Cuiridh seo air dòigh leasachaidhean air meacanaig an t-snàgaidh thùsail.\n\n- Leigidh le snàgadh ath-thòiseachadh nas luaithe às dèidh stad.\n- Leigidh le gluasad bho ruith/spìonadh gu snàgadh gun stad an toiseach.\n- Leigidh le gluasad bho shnàgadh gu rola crom (ma tha e air a chur an comas) gun cromadh le làimh an toiseach.\n- Leigidh le Lara tionndadh fhad 's a tha i crom.\n- Leigidh le beòthachadh-togail sonraichte a chleachdadh fhad 's a tha Lara a' snàgadh (ach a-mhàin lòchran).", }, "gameplay.enable_responsive_sprint": { "title": "Ruith luath freagairteach", "description": "Cuiridh seo staid ruith luath nas freagairtiche an gnìomh do Lara.\n\n- leigidh le Lara ruith gu luath cho luath 's a tha neart aice, seach a bhith a' feitheamh gus am bi stamina làn aice.\n- leigidh le Lara ruith gu luath suas staidhre gun a bhith a stad le beòthalachd ruith àbhaisteach.", }, "gameplay.enable_save_crystals": { "title": "Criostalan sàbhalaidh", "description": "Cuiridh seo cuingealachadh air sàbhaladh gu toiseach ìrean is criostalan sàbhalaidh aon-chleachdadh, coltach ris a' PhS1. Feumar ath-thòiseachadh an ìre airson seo obrachadh.", }, "gameplay.enable_slide_to_run": { "title": "Sleamhnaich-gus-ruith", "description": "Leigidh seo le Lara ruith sa bhad nuair a ruigeas i air uachdar còmhnard às dèidh a bhith a' sleamhnachadh air leac leathann. Cum am putan air adhart sios gus a ghnìomhachadh.", }, "gameplay.enable_slow_ledge_swing": { "title": "Luascadh slaodach", "description": "Leigidh seo le Lara luascadh gu slaodach nuair a tha i air grèim fhaighinn air oir glè thana (stoidhle TR3). Ma tha seo à comas, luaisgidh Lara greiseag ghoirid mus till i gu suidheachadh crochte aig fois (stoidhle TR1/2).", }, "gameplay.enable_smooth_wall_deflect": { "title": "Ath-bheòthachadh bhalla rèidh", "description": "Cuidichidh Lara le tilleadh nas luaithe às dèidh buille ri balla ma thèid stiùireadh is 'air adhart' a chumail.", }, "gameplay.enable_soft_statics": { "title": "Bualadh mogal bog", "description": "Leigidh le Lara gluasad gu rèidh air aghaidh mogalan statach – coltach ri TR4+ – seach stad chruaidh a dhèanamh.", }, "gameplay.enable_sprint": { "title": "Ruith gu luath", "description": "Leigidh seo le Lara ruith gu luath, coltach ri TR3+.", }, "gameplay.enable_step_roll_boost": { "title": "Brùthadh le rolla bho cheum", "description": "Leigidh Lara leum bhon cheum aon-bhriog ma bhriogar rolla faisg air an oir.", }, "gameplay.enable_swing_cancel": { "title": "Sguir siùdadh", "description": "Leigidh seo le Lara leigeil às nuair a tha i a' siùdadh, agus grèim a ghlacadh sa bhad airson gluasad luath.", }, "gameplay.enable_target_change": { "title": "Atharraich targaid", "description": "Cuiridh seo an comas atharrachadh targaid TR4+ fhad 's a tha Lara ag amas.", }, "gameplay.enable_timer_in_inventory": { "title": "Gleoc sa chlàr-seilbhe", "description": "Leigidh seo leis a' ghleoc a leantainn air adhart fiù 's nuair a tha an clàr-seilbhe fosgailte.", }, "gameplay.enable_toggle_crouch": { "title": "\\{review}Cuir air adhart cromadh", "description": "\\{review}Leigidh seo le Lara fuireach crom às dèidh a bhith a' brùthadh air a' phutan cromadh aon turas. Brùth air a' phutan cromadh a-rithist gus seasamh suas.", }, "gameplay.enable_toggle_sprint": { "title": "\\{review}Cuir air adhart ruith gu luath", "description": "\\{review}Leigidh seo le Lara ruith gu luath às dèidh a bhith a' brùthadh air a' phutan ruith gu luath aon turas. Brùth air a' phutan ruith gu luath a-rithist gus stad a chur air ruith gu luath.", }, "gameplay.enable_total_stats": { "title": "Sgrion staitistig deireannach", "description": "Cuiridh seo sgrion staitistig iomlan air adhart às dèidh nan creideasan.", }, "gameplay.enable_tr2_jumping": { "title": "Leum freagairteach", "description": "Leigidh le Lara leum aig àm sam bith nuair a tha i a' ruith.", }, "gameplay.enable_tr2_swim_cancel": { "title": "Stad snàmh freagairteach", "description": "Leigidh Lara stad gu sgiobalta fon uisge nuair a thèid am putan snàmh a leigeil às.", }, "gameplay.enable_tr2_swimming": { "title": "Snàmh rèidh", "description": "Cuiridh seo lùb luathaidh air tionndadh Lara fon uisge airson gluasad nas rèidh, mar ann an TR2 bho thùs.", }, "gameplay.enable_uw_roll": { "title": "Rolla fon uisge", "description": "Leigidh le Lara rolla a dhèanamh fon uisge.", }, "gameplay.enable_wading": { "title": "Coiseachd tro uisge", "description": "Leigidh le Lara coiseachd tro uisge eu-dhomhain seach a bhith air a stad aig uachdar na h-aibhne.", }, "gameplay.enable_walk_to_items": { "title": "Eadar-obrachadh beòthail", "description": "Nì Lara coiseachd gu togailtean is suidsichean nuair a tha i faisg orra, an àite leum dìreach thuca.", }, "gameplay.fix_alligator_ai": { "title": "Càraich AI ailigeutair", "description": "Càradh far nach dèan ailigeutairean milleadh ma dh'fhanas Lara fhathast anns an uisge.", }, "gameplay.fix_bear_ai": { "title": "Càraich AI mathain", "description": "Càraichidh seo ionnsaigh a' mhathain gus nach caill e Lara.", }, "gameplay.fix_bridge_collision": { "title": "Càraich buaidh dhrochaidean", "description": "Càradh far nach urrainn do Lara grèim fhaighinn air cuid de dhrochaidean, agus bugan le ballachan neo-fhaicsinneach. Càraidh seo cuideachd duilgheadasan le drochaidean, dorsan-trapa is làr-lùbte nuair a tha iad air an cur thairis air chèile neo ri talamh claon.", }, "gameplay.fix_descending_glitch": { "title": "Càraich tuiteam air leacan bristeach", "description": "Càradh far am bi Lara a' tuiteam gu sgiobalta tro leacan briste nuair a dhèanas i ceum-taoibh neo coiseachd air ais orra.", }, "gameplay.fix_flare_throw_priority": { "title": "Càraich prìomhachas tilgeadh lòchran", "description": "Càradh far am bi Lara a' feuchainn ri lòchran a thilgeil ann am meadhan leum, a' cur casg air grèim air iomallan.", }, "gameplay.fix_floor_data_issues": { "title": "Càraich duilgheadasan dàta-làir", "description": "Càradh bugan tùsail le dàta-làir - trigearan 's a leithid.", }, "gameplay.fix_free_flare_glitch": { "title": "Càraich glitch lòchran an-asgaidh", "description": "Càradh far am faighear lòchran gun chosgais le bhith a' brùthadh am putan fhad 's a thogas Lara rud sam bith an àrd.", }, "gameplay.fix_item_duplication_glitch": { "title": "Càraich glitch dùblachaidh nì", "description": "Càradh far an gabh nithean prìomhach a chleachdadh dà thuras neo barrachd san t-seilbh.", }, "gameplay.fix_lara_pickup_embed": { "title": "Càraich cath-thogail", "description": "Càradh far am bi Lara uaireannan a' gluasad a-steach gu ballachan nuair a tha i a' togail rudeigin fon uisge, agus os cionn uisge fo mhullaichean cas leathann.", }, "gameplay.fix_m16_accuracy": { "title": "Càraich cruinneas M16/MP5", "description": "Càradh cruinneas M16/MP5 fhad 's a tha Lara a' ruith.", }, "gameplay.fix_monkey_pickup_priority": { "title": "Cuir prìomhachas air togail muncaidh ceart", "description": "Bidh muncaidhean air an ionnsaigh a' cur prìomhachas air freagairt air ais seach a bhith a' cruinneachadh pacaidean Medi agus iuchraichean.", }, "gameplay.fix_pipeman_aim": { "title": "Càradh amas an fhir-phìoba", "description": "Càraichidh seo duilgheadas far nach urrainn don fhear-phìoba uaireannan saighdean a chuimseachadh air Lara gu ceart.", }, "gameplay.fix_qwop_glitch": { "title": "Càraich glitch QWOP", "description": "Càradh glitch - ris an canar QWOP - far am bi Lara a' leum air ceumannan beaga agus a' toiseachadh gluasad gu neònach tron làr.", }, "gameplay.fix_step_glitch": { "title": "Càraich glitch ceumannan", "description": "Càradh far am bi Lara uaireannan air a putadh tro bhalla ri taobh ceuman nuair a ruitheas i suas orra.", }, "gameplay.fix_wade_wall_hit": { "title": "Càraich buille tro uisge eu-dhomhain", "description": "Cuiridh seo ceartachadh air freagairt Lara nuair a bhuail i balla fhad 's a tha i a' coiseachd tro uisge eu-dhomhain.", }, "gameplay.fix_walk_run_jump": { "title": "Càraich leum às dèidh ruith", "description": "Cuiridh seo ceartachadh air cùis far nach urrainn do Lara leum dìreach às dèidh gluasad bho choiseachd gu ruith.", }, "gameplay.fix_wall_geometry": { "title": "Càradh geoimeatraidh bhallachan", "description": "Càraichidh seo cùisean ann an geoimeatraidh ìrean OG far am faod claonaidhean taobh a-staigh bhallachan leantainn gu àireamhachadh àirde mearachdach.", }, "gameplay.fix_water_exit": { "title": "Càraich fàgail à seòmar uisge", "description": "Cuiridh seo casg air Lara a bhith comasach air a dhol gu seòmraichean tioram fodha neo ri taobh an seomair làithreach. Cuiridh seo cuideachd casg oirre a sreap air sleamhnagan nach gabh seasamh orra.", }, "gameplay.harpoon_recoil": { "title": "Ath-luchdadh harpùn", "description": "Suidhichidh seo dè cho tric 's a dh'fheumas Lara an harpùn ath-luchdadh, stèidhichte air an lòin a th'aice. M.e. 3 = ath-luchdaich às dèidh a chleachdadh trì tursan. 0 = na bi ag ath-luchdachadh a-riamh.", }, "gameplay.idle_pose_timeout": { "title": "Dàil seasaimh", "description": "Leigidh seo le Lara beòthalachd shònraichte a thòiseachadh às dèidh dhi dad a dhèanamh fad an àireamh de dhiogan suidhichte. Suidhich gu 0 gus a chur à comas.", }, "gameplay.jump_lock_mode": { "title": "Modh glasadh leum", "description": "Airson leum freagairteach, leigidh seo leat smachd a chumail air cuin a bhios Lara comasach air leum às dèidh tòiseachadh ruith.\n\n- Dualchasach: a rèir àm TR2 tùsail.\n- Fìnealta: tha leum comasach 2 fhrèam nas tràithe.\n- Dheth: tha leum comasach sa bhad às dèidh an beòthalachd tòiseachaidh.", }, "gameplay.loading_screens": { "title": "Sgrionaichean luchdachaidh", "description": "Smachdaichidh seo na sgrionaichean luchdachaidh mus luchdaich ìrean.\n\n- Ciorramach: na seall sgrionaichean luchdachaidh idir.\n- An-còmhnaidh: seall sgrionaichean luchdachaidh.\n- Geamannan ùra: leig seachad sgrionaichean luchdachaidh nuair a luchdaichear sàbhaladh.", }, "gameplay.look_mode": { "title": "Modh seallaidh", "description": "Leigidh seo leat smachd a chumail air cuin as urrainn do Lara sealladh a chleachdadh.\n\n- Cuibhrichte: chan eil sealladh comasach ach nuair a tha Lara na stad, agus a-riamh fon uisge.\n- Leasaichte: tha sealladh comasach rè a' mhòr-chuid de bheòthalachd, ach chan ann rè gluasadan mar putadh bloca.\n- Neo-chuibhrichte: tha sealladh comasach aig àm sam bith fo smachd Lara àbhaisteach.", }, "gameplay.maximum_quick_save_slots": { "title": "Àireamh nan slotan sàbhalaidh luath", "description": "Atharraichidh seo an àireamh de shlotan sàbhalaidh luath a tha rim faotainn.", }, "gameplay.maximum_save_slots": { "title": "Àireamh de shloinn sàbhalaidh", "description": "Atharraichidh seo an àireamh de shloinnean sàbhalaidh a tha rim faotainn.", }, "gameplay.pause_on_focus_lost": { "title": "\\{review}Cuir air fois nuair a chaill thu fòcas", "description": "\\{review}Bidh e a’ stad geama bho bhith a’ dol air adhart nuair a bhios uinneag a’ gheama a’ call fòcas.", }, "gameplay.projectile_area_damage": { "title": "Dàil sgìre pròiseactail", "description": "Smachd air mar a sgaoileas an raon-buaidh airson an Lannsair Grenèad agus Lannsair Rocaid.\n\n- Ioma-singilte: mar TR2.\n- Ioma-sgèile: mar TR3.\n\nBidh an roghainn Ioma-sgèile gu tric a' leantainn gu dùbailteachadh a' mhillidh air nàimhdean fa leth.", }, "gameplay.remember_gun_status": { "title": "Cuimhnich armachd eadar ìrean", "description": "Cuiridh seo air Lara cuimhne dè an armachd a bh'aice mu dheireadh nuair a thòisicheas i ìre ùr. Mur tèid seo a chomasachadh, tillidh i gu piostalan nan dìollaid.", }, "gameplay.restore_ps1_enemies": { "title": "Aisig nàimhdean PS1", "description": "Cuiridh seo spaoileadan ann an City of Khamoon (PS1), seòmar 25. Feumaidh ath-thòiseachadh a' gheama gu lèir gus obraicheas seo.", }, "gameplay.start_lara_hitpoints": { "title": "Slàinte tòiseachaidh Lara", "description": "Suidhichidh seo luach slàinte Lara aig toiseach gach ìre.", }, "gameplay.target_mode": { "title": "Modh glas armachd", "description": "Cuiridh seo atharrachadh air mar a bhios armachd a' glasadh air targaidean:\n\n- Glas làn: cùm glasadh fiù 's ma thèideas an nàmhaid a-mach à sealladh (TR1-3).\n- Leth-ghlas: cùm glasadh nuair a tha an nàmhaid a-mach à sealladh, ach caill e ma bhàsaicheas e.\n- Gun ghlas: caill glasadh ma thèideas an nàmhaid a-mach à sealladh neo nuair a bhàsaicheas e (TR4+).", }, "gameplay.wall_glitch_mode": { "title": "Modh glitch bhalla", "description": "Cuiridh seo modh glitch bhalla TR1 ann an TR2 agus a chaochladh an comas. Leigidh seo cuideachd le càradh iomlan air na glitchan uile.", }, "input.enable_buffering_func_keys": { "title": "Bùthadh (putanan F)", "description": "Leigeas leis nam putanan F smachd mionaideach (1-frèam) a thoirt air gluasad Lara. Tha am feart seo gu tùsail ri fhaighinn a-mhàin anns a' phort TombATI (TR1).", }, "input.enable_buffering_inventory": { "title": "Bùthadh (clàr-stòraidh)", "description": "Leigeas leis am putan clàr-stòraidh smachd mionaideach (2-fhraim) a thoirt air gluasad Lara.", }, "input.enable_responsive_passport": { "title": "Pasport freagairteach", "description": "Cuiridh seo às do bhacadh cuir a-steach le brath ro-luath air tionndadh duilleagan a' phasport; leigidh e leutha bhith air an clàradh an àite a bhith air an stad.", }, "input.enable_tr3_sidesteps": { "title": "Ceumannan-taoibh leasaichte", "description": "Cuiridh seo ceumannan-taoibh stoidhle TR3+ an comas, m.e. coisich + saigheadan. Bidh na putanan sònraichte fhathast ag obair.", }, "input.quick_guns_mode": { "title": "Iuchraichean luatha armachd", "description": "Smachd air mar a dh'èireas na h-iuchraichean uidheamachaidh armachd luatha.\n\n- Tarraing a-mhàin: bheir iuchair teth air Lara an arm ainmichte a tharraing.\n- Tarraing/falach: mar roimhe, ach bheir seo cuideachd air Lara an arm a chur air falach ma tha i ga ghiùlan.", }, "language": { "title": "Cànan", "description": "Atharraicheas cànan an eadar-aghaidh.", }, "rendering.anisotropy_filter": { "title": "Criathradh anisotropaigeach", "description": "Leasaichidh seo criathradh inneach aig astaran fada.", }, "rendering.aspect_mode": { "title": "Modh co-mheas sgrion", "description": "Èigneachadh cuid de cho-mheudan le bàr-dubh (bogsa-litreach).", }, "rendering.borders": { "title": "Iomallan", "description": "Cuiridh seo iomallan dubha timcheall air uinneag a' gheama.", }, "rendering.enable_trapezoid_filter": { "title": "Criathradh trapeasoid", "description": "Cuiridh seo ceartachadh air trapeasoidean (ceàrnan neo-chothromach) airson reandaradh nas fhearr.", }, "rendering.enable_vsync": { "title": "V-Sionc", "description": "Tionndaidheas sioncronachadh-inghearach air neo dheth.", }, "rendering.fps": { "title": "FPS", "description": "Suidhichidh an àireamh de frèamaichean a chluicheas gach diog 's a' gheama.", }, "rendering.lighting_contrast": { "title": "Iomsgaradh solais", "description": "Cuiridh seo atharrachadh air iomsgaradh solais fiùghantach, airson m.e. lasraichean neo losgadh armachd.", }, "rendering.screenshot_format": { "title": "Fòrmat glacadh-sgrìn", "description": "Fòrmat fhaidhle airson glacadh-sgrìn.", }, "rendering.sprite_lock_mode": { "title": "Glasadh sprìde", "description": "Bheir seo buaidh air na h-aisealean a tha glaiste nuair a thathar a' coimhead air sprìdean air an sgrion.\n\n- Gin sam bith: seall air na sprìdean gu àbhaisteach.\n- Rothladh: glasaich an aiseal rothlaidh – gu feumail a-mhàin ann am modh dealbh.\n- Rothladh is claonadh: bidh na sprìdean nan seasamh dìreach agus cha bhith iad nan laighe air an talamh nuair a thathar gan choimhead bho shuas.\n- Sealladh: glasaich na h-aisealean rothlaidh agus claonadh, 's a bharrachd air sin, rothlaich na sprìdean beagan a dh'ionnsaigh meadhan an sgrion.", }, "rendering.texture_filter": { "title": "Criathradh inneach", "description": "Cuiridh seo atharrachadh eadar inneach rèidh agus piogsaileach.", }, "rendering.ui_filter": { "title": "Criathradh EA", "description": "Tionndaidh eadar inneach an eadar-aghaidh rèidh neo piogsaileach.", }, "rendering.upscaling_factor": { "title": "Factar sgèileachaidh", "description": "Àrdaichidh seo an geama le factar stèidhichte, aig an aon àm a' cumail coltas piogsaileach.", }, "rendering.upscaling_filter": { "title": "Criathradh sgèileachaidh", "description": "Tionndaidh eadar coltas rèidh neo piogsaileach airson na sgrìn air fad.", }, "ui.airbar_color": { "title": "Dath bàr-èadhair", "description": "Dath a' bhàr-èadhair.", }, "ui.airbar_color_ps1": { "title": "Dath bàr-èadhair", "description": "Dath a' bhàr-èadhair.", }, "ui.airbar_location": { "title": "Suidheachadh bàr-èadhair", "description": "Far a nochdas am bàr-èadhair air an sgrion.", }, "ui.ammo_counter_location": { "title": "\\{review}Àite cunntair ammo", "description": "\\{review}Àite far am bi an cunntair ammo air a thaisbeanadh.", }, "ui.bar_look": { "title": "Coltas nam bàraichean", "description": "Atharraichidh seo coltas nam bàraichean EA.", }, "ui.bar_scale": { "title": "Meud bàr", "description": "Atharraicheas meud nam bàraichean.", }, "ui.enable_bar_flashing": { "title": "Bàraichean deàrrsail", "description": "Cuiridh seo an àite bàraichean slàinte agus ocsaidean Lara deàrrsail nuair a tha i a' ruith ìosal air na goireasan seo.", }, "ui.enable_smooth_bars": { "title": "Bàraichean rèidh", "description": "Cuiridh seo dath le gluasad rèidh air na bàraichean slàinte, èadhair is nàmhaid.", }, "ui.enable_wraparound": { "title": "Sgrolaich timcheall", "description": "Leigidh seòladh stiùiridh ann an clàran-bìdh lùbadh timcheall.", }, "ui.enemy_healthbar_color": { "title": "Dath bàr-nàmhaid", "description": "Dath bàr-shlàinte an nàmhaid.", }, "ui.enemy_healthbar_color_allies": { "title": "Dath bàr nan càirdean", "description": "Dath bàr-shlàinte nan càirdean. Seallaidh seo ann an àite bàraichean-shlàinte nan naimhdean.", }, "ui.enemy_healthbar_color_allies_ps1": { "title": "Dath bàr nan càirdean", "description": "Dath bàr-shlàinte nan càirdean. Seallaidh seo ann an àite bàraichean-shlàinte nan naimhdean.", }, "ui.enemy_healthbar_color_ps1": { "title": "Dath bàr-nàmhaid", "description": "Dath bàr-shlàinte an nàmhaid.", }, "ui.enemy_healthbar_location": { "title": "Suidheachadh bàr-nàmhaid", "description": "Far a nochdas bàr-shlàinte an nàmhaid air an sgrion.", }, "ui.enemy_healthbar_show_mode": { "title": "Modh bàr-nàmhaid", "description": "Cuiridh seo bàr-shlàinte an nàmhaid gnìomhaich an comas.", }, "ui.exposurebar_color": { "title": "Dath bàr-fuachd", "description": "Dath a' bhàir ann an uisge fuair.", }, "ui.exposurebar_color_ps1": { "title": "Dath bàr-fuachd", "description": "Dath a' bhàir ann an uisge fuair.", }, "ui.exposurebar_location": { "title": "Suidheachadh bàr-fuachd", "description": "Far a nochdas am bàr-fuachd air an sgrion.", }, "ui.healthbar_color": { "title": "Dath bàr-shlàinte", "description": "Dath a' bhàr-shlàinte.", }, "ui.healthbar_color_ps1": { "title": "Dath bàr-shlàinte", "description": "Dath a' bhàr-shlàinte.", }, "ui.healthbar_location": { "title": "Suidheachadh bàr-shlàinte", "description": "Far a nochdas am bàr-shlàinte air an sgrion.", }, "ui.healthbar_poison_color": { "title": "Dath bàr-shlàinte puinnseanta", "description": "Dath a' bhàr-shlàinte nuair a tha Lara air a puinnseanachadh.", }, "ui.healthbar_poison_color_ps1": { "title": "Dath bàr-shlàinte puinnseanta", "description": "Dath a' bhàr-shlàinte nuair a tha Lara air a puinnseanachadh.", }, "ui.inventory_background_style": { "title": "Dealbh-cùil sa chlàr-seilbhe", "description": "Atharraichidh an doigh anns a bhitheas an dealbh-cùil air a shealltainn sa chlàr-seilbhe.\n\n- Dorcha: TR1 (PC).\n- Fìor dhorcha: TR1 (PS1).\n- Statach: TR2 (PC).\n- Crathadh: TR2 (PS1).\n- Aon-dhathach: TR3.", }, "ui.inventory_fade_effects": { "title": "Buaidhean falamh clàr-innealan", "description": "A' rèiteachadh gu mionaideach na buaidhean falamh airson an cleachdadh neo an cur dheth anns a' chruinneachadh clàr-innealan sa gheama. Feumaidh an roghainn Buaidhean Falamh a bhith air a ghnìomhachadh gus seo obrachadh.", }, "ui.menu_style": { "title": "Stoidhle clàir", "description": "Cuiridh seo stoidhle a' chlàir an comas:\n\n- PC: stoidhle eadar-aghaidh PC.\n- PS1: stoidhle eadar-aghaidh a' PhS1.", }, "ui.pause_background_style": { "title": "Dealbh-cùil sgrion-stad", "description": "Atharraichidh an doigh anns a bhitheas an dealbh-cùil air a shealltainn san sgrion-stad.\n\n- Dorcha: TR1 (PC).\n- Fìor dhorcha: TR1 (PS1).\n- Statach: TR2 (PC).\n- Crathadh: TR2 (PS1).\n- Aon-dhathach: TR3.", }, "ui.pause_fade_effects": { "title": "Buaidhean falamh stad", "description": "A' rèiteachadh gu mionaideach na buaidhean falamh airson an cleachdadh neo an cur dheth air scrion stad. Feumaidh an roghainn Buaidhean Falamh a bhith air a ghnìomhachadh airson seo obrachadh.", }, "ui.pickup_scale": { "title": "Meud togailtean", "description": "Atharraicheas meud nan togailtean beòthail san EA nuair a thogas Lara an àird iad.", }, "ui.show_bars": { "title": "Cleachd bàraichean", "description": "Cuiridh seo dheth gach bàr sa gheama, a' falach fiosrachadh mu shlàinte Lara agus goireasan eile (airson ruith dhùbhlanach).", }, "ui.show_pickups_overlay": { "title": "Taisbeanadh thogailtean", "description": "Seallaidh seo togailtean san oisean deas-ìosal nuair a thogas Lara rudeigin.", }, "ui.show_title_version": { "title": "\\{review}Teacsa teacsa dreach", "description": "\\{review}Seallas an sreang teacsa dreach TRX ann an fàinne clàr-stòrais na tiotal.", }, "ui.sprintbar_color": { "title": "Dath bàr ruith luath", "description": "Dath a' bhàr ruith luath.", }, "ui.sprintbar_color_ps1": { "title": "Dath bàr ruith luath", "description": "Dath a' bhàr ruith luath.", }, "ui.sprintbar_location": { "title": "Suidheachadh bàr ruith luath", "description": "Far a nochdas am bàr ruith luath air an sgrion.", }, "ui.stats.show_ammo": { "title": "\\{review}Bualadh ammo / cleachdadh", "description": "\\{review}Seall an loidhne ammo anns na staitistig ìre.", }, "ui.stats.show_crystals": { "title": "\\{review}Criostalan", "description": "\\{review}Tha e a’ sealltainn an rìgh criostalan ann an staitistig an ìre.", }, "ui.stats.show_deaths": { "title": "\\{review}Bàs", "description": "\\{review}Seall bàsan Lara anns na staitistig compais agus anns na staitistig ìre. Tha cunntas bàis air ùrachadh anns an sàbhalaidh a tha air a luchdachadh an-dràsta cho luath ‘s a bhàsaich Lara.", }, "ui.stats.show_distance_travelled": { "title": "\\{review}Astar air a siubhal", "description": "\\{review}Seall an loidhne astar air a siubhal anns na staitistig ìre.", }, "ui.stats.show_kills": { "title": "\\{review}Murtan", "description": "\\{review}Seall an loidhne murtan anns na staitistig ìre.", }, "ui.stats.show_level_header": { "title": "\\{review}Àireamh ìre", "description": "\\{review}Seall àireamh an ìre làithreach aig mullach nan staitistig ìre.", }, "ui.stats.show_medipacks_used": { "title": "\\{review}Pacaidean slàinte air an cleachdadh", "description": "\\{review}Seall an loidhne pacaidean slàinte air an cleachdadh anns na staitistig ìre.", }, "ui.stats.show_pickups": { "title": "\\{review}Togalaichean", "description": "\\{review}Seall an loidhne togalaichean anns na staitistig ìre.", }, "ui.stats.show_secrets": { "title": "\\{review}Dìomhair air an lorg", "description": "\\{review}Seall an loidhne dìomhair air an lorg anns na staitistig ìre.", }, "ui.stats.show_time_taken": { "title": "\\{review}Ùine a ghabh", "description": "\\{review}Seall an loidhne ùine a ghabh anns na staitistig ìre.", }, "ui.stats.show_totals": { "title": "\\{review}Seall na suimean", "description": "\\{review}Sealltainn iomlanan ri taobh staitistig nuair a tha sin iomchaidh. Chan eil dìomhaireachdan air an toirt buaidh leis an suidheachadh seo.", }, "ui.stats.style": { "title": "\\{review}Stoidhle staitistig", "description": "\\{review}Smachd air mar a thèid an còmhradh staitistig a thaisbeanadh.\n\n- Furasta: seall an cruth nas sìmplidh gun fhrèam.\n- Leacanach: seall an cruth le bogsaichean.", }, "ui.stats_background_style": { "title": "Dealbh-cùil staitistig", "description": "Atharraichidh an doigh anns a bhitheas an dealbh-cùil air a shealltainn san sgrion-staitistig.\n\n- Dorcha: TR1 (PC).\n- Fìor dhorcha: TR1 (PS1).\n- Statach: TR2 (PC).\n- Crathadh: TR2 (PS1).\n- Aon-dhathach: TR3.", }, "ui.stats_fade_effects": { "title": "Buaidhean falamh staitistig", "description": "A' rèiteachadh gu mionaideach na buaidhean falamh airson an cleachdadh neo an cur dheth air scrion staitistig deireadh ìre. Feumaidh an roghainn Buaidhean Falamh a bhith air a thaghadh gus seo obrachadh.", }, "ui.text_scale": { "title": "Meud teacsa", "description": "Atharraicheas meud an teacsa.", }, "visuals.blood_effects": { "title": "\\{review}Buaidhean fuil", "description": "\\{review}Smachd air dathan spàirn fola.\n\n- Ciorramh: chan eilear a’ sealltainn spàirn fola sam bith.\n- Pinc: an àbhaist ann an sgaoilidhean PC Gearmailteach de TR3.\n- Dearg: an àbhaist anns a h-uile sgaoileadh reic eile.", }, "visuals.camera_mode": { "title": "Modh camara", "description": "Atharraichidh seo an doigh anns a bhios a' chamara ag obair, m.e. aig àm ghnìomhan shonraichte, mar a bhith a' cleachdadh iuchraichean.", }, "visuals.enable_3d_pickups": { "title": "Togail 3D", "description": "Cuiridh seo modailean 3D an àite spraidean airson nithean togail.", }, "visuals.enable_braid": { "title": "Feaman-fuilt Lara", "description": "Cuiridh seo feaman-fuilt Lara an comas.", }, "visuals.enable_breeze": { "title": "Gaoth", "description": "Cuiridh seo buaidh na gaoithe air feaman-fuilt Lara ann an seòmraichean freagarrach.", }, "visuals.enable_exit_fade_effects": { "title": "Dorchachadh aig fàgail", "description": "Cuiridh seo dorchachadh an comas nuair a dh'fhàgas tu an geama gu deasg.", }, "visuals.enable_fade_effects": { "title": "Buaidhean dorchachaidh", "description": "Cuiridh seo dorchachadh eadar gluasadan air adhart — m.e. eadar grafaigean creideis neo clàr-seilbhe.", }, "visuals.enable_fire_lighting": { "title": "Lasadh teine", "description": "Cuiridh seo comas air solas fiùghantach a bhith air a chruthachadh ri taobh lasraichean gnìomhach.", }, "visuals.enable_footprints": { "title": "Clachan-coise", "description": "Leigeas le clachan-coise Lara a nochdadh air uachdaran sònraichte ann an ìrean taicichte.", }, "visuals.enable_glide_cameras": { "title": "Camarathan sleamhainn", "description": "Cuir an comas gluasad sleamhainn air camarathan stèidhichte a bhios a' coimhead air Lara le lùb astair rèidh a chleachdadh. Ma tha e à comas, atharraichidh na camarathan seo an sealladh sa bhad.", }, "visuals.enable_gun_lighting": { "title": "Solais armachd", "description": "Cuiridh seo solais beòthail ri losgadh agus spreadhadh.", }, "visuals.enable_ps1_crystals": { "title": "Dath criostail PS1", "description": "Bidh na criostalan sàbhalaidh purpaidh mar a tha iad anns a' PhS1.", }, "visuals.enable_reflections": { "title": "Meòrachadh", "description": "Cuiridh seo meòrachadh air nithean sònraichte an comas.", }, "visuals.enable_responsive_mesh_tint": { "title": "Dathadh freagairteach lìonra", "description": "Leigidh seo le lìonra Lara fa leth a bhith air an sealltainn le dath uisge ma tha iad fhèin fo uisge (stoidhle TR3). Air neo, ma tha Lara san uisge, thèid gach lìonra aice a shealltainn leis an dath (stoidhle TR1/2).", }, "visuals.enable_shotgun_flash": { "title": "Lasadh gunna-sgaoil", "description": "Cuiridh seo lasair an sàs nuair a losgais an gunna-sgaoil, coltach ri armachd eile.", }, "visuals.enable_skybox": { "title": "Bogsa-nan-Speuran", "description": "Cuiridh seo bogsa-nan-speuran an comas ann an ìrean far a bheil e freagarrach.", }, "visuals.enable_weather": { "title": "Aimsir", "description": "Cuir air neo dheth an aimsir anns na h-ìrean anns a bheil i an àite.", }, "visuals.fix_animated_sprites": { "title": "Càraich beòthachaidhean spraidean", "description": "Ceartachaidh nan sprìodan lusan fo uisge tùsail gus an gluasad gu ceart ann an raointean uisge.", }, "visuals.fix_item_rots": { "title": "Càraich cuairteachadh nithean", "description": "Càradh bugan far a bheil cuid de thogailtean air an tionndadh gu ceàrr le togailtean 3D air a chomasachadh.", }, "visuals.fix_texture_issues": { "title": "Càraich duilgheadasan inneach", "description": "Càradh bugan tùsail le inneach neo modalan a tha ceàrr neo a dhìth.", }, "visuals.fog_color": { "title": "Dath ceò", "description": "Dath a' cheòth.", }, "visuals.fog_end": { "title": "Deireadh ceò", "description": "Suidhichidh seo an astar (ann an leacan) bhon chamara far a dhùnas ceò air a h-uile càil.", }, "visuals.fog_start": { "title": "Tòiseachadh ceò", "description": "Suidhichidh seo an astar (ann an leacan) bhon chamara far a thoisicheas ceò.", }, "visuals.fog_transparency": { "title": "Follaiseachd a' cheòth", "description": "Leigidh seo le sealladh geoimeatraidh a tha fada air falbh bhon a' chamara a bhith follaiseachd gu h-iomlan.", }, "visuals.fov": { "title": "Raonadh seallaidh", "description": "Ceàrn seallaidh ann an ceumannan. Bidh luachan nas motha a' leudachadh raon seallaidh, agus bidh luachan nas lugha ga chuingealachadh.", }, "visuals.game_brightness": { "title": "Soilleireachd", "description": "Cuiridh seo atharrachadh air soilleireachd a' gheama.", }, "visuals.gamma": { "title": "Gamma", "description": "Cuir buaidh air dorchadas a' gheama. Tha luachan nas àirde a' ciallachadh solas nas soilleire. Tha luach 2.5 a' ciallachadh dathan àbhaisteach.", }, "visuals.lara_outfit": { "title": "Aodach Lara", "description": "Atharraicheas seo coltas Lara. Ma thèid Bunaiteach a thaghadh, thèid atharrachaidhean aodaich àbhaisteach eadar ìrean a chumail; airson dad sam bith eile, mairidh an t-aodach a chaidh a thaghadh gus an tèid atharrachadh le làimh.", }, "visuals.shadow_type": { "title": "Cruth nan faileasan", "description": "Tagh mar a thèid faileasan nithean air an sealltainn.\n\n- Ochd-cheàrnach: faileas seann TR1 agus TR2\n- Cearcall: faileas cruinn\n- Spraide: faileas stèidhichte air teacsa TR3", }, "visuals.sunglasses_mode": { "title": "Speuclairean-grèine Lara", "description": "Atharraichidh seo stoidhle speuclairean-grèine Lara. Nòta: bidh na lionsan meòrachail ma tha an roghainn iomchaidh air a chur an comas.\n\n- Dheth: Cha bhi speuclairean-grèine air Lara.\n- Neo-shoilleir: Bidh lionsan neo-shoilleir air speuclairean-grèine Lara.\n- Follaiseach: Bidh lionsan leth-fhoilleiseach air speuclairean-grèine Lara.", }, "visuals.ui_brightness": { "title": "Soilleireachd EA", "description": "Cuiridh seo atharrachadh air soilleireachd na h-eadar-aghaidh.", }, "visuals.water_color": { "title": "Dath an uisge", "description": "Dath an uisge.", } }, "objects": { "alarm_sound": { "name": "Inneal-rabhaidh", }, "alligator": { "name": "Ailigeutair", }, "alphabet": { "name": "Clò àbhaisteach", }, "alphabet_small": { "name": "Clò beag", }, "amber_light": { "name": "Solas Òmar", }, "animating_1": { "name": "Beòthaladh 1", }, "animating_10": { "name": "Beòthaladh 10", }, "animating_2": { "name": "Beòthaladh 2", }, "animating_3": { "name": "Beòthaladh 3", }, "animating_4": { "name": "Beòthaladh 4", }, "animating_5": { "name": "Beòthaladh 5", }, "animating_6": { "name": "Beòthaladh 6", }, "animating_7": { "name": "Beòthaladh 7", }, "animating_8": { "name": "Beòthaladh 8", }, "animating_9": { "name": "Beòthaladh 9", }, "ape": { "name": "Apa", }, "area_51_rocket": { "name": "\\{review}Ròcaid Sgìre 51", }, "area_51_rocket_blast": { "name": "\\{review}Spreadh Ròcaid Sgìre 51", }, "area_51_rocket_support": { "name": "\\{review}Taic Ròcaid Sgìre 51", }, "assault_digits": { "name": "Àireamhan an t-Seòmair-eacarsaich", }, "assault_target": { "name": "Targaid Ionnsaigh", }, "atlantean_ground": { "name": "Atlantach Talmhainn", }, "atlantean_shooter": { "name": "Atlantach le Gunna", }, "atlantean_winged": { "name": "Atlantach Sgiathach", }, "autos": { "name": "Piostalan Fèin-obrachail", }, "autos_ammo": { "name": "Lòin Gunna Fèin-obrachail", }, "bacon_lara": { "name": "Lara Bèicein", }, "baldy": { "name": "Baldaidh", }, "bandit_1": { "name": [ "Saighdear-duais 1", "Ruspal le Masg 1", ] }, "bandit_2": { "name": [ "Saighdear-duais 2", "Ruspal le Masg 2", ] }, "bandit_2b": { "name": [ "Saighdear-duais 3", "Ruspal le Masg 3", ] }, "barracuda": { "name": "Barracuda", }, "bartoli": { "name": "Marco Bartoli", }, "bat": { "name": "Ialtag", }, "bat_emitter": { "name": "Sgaoiladair Bàta", }, "beacon_light": { "name": "Solas Beura", }, "bear": { "name": "Mathan", }, "bell": { "name": "Clag", }, "big_bowl": { "name": "Bobhla Teine-teinteach", }, "big_eel": { "name": "Easgann Mhòr", }, "big_pod": { "name": "Spàlag Mhòr", }, "big_spider": { "name": "Damhan-allaidh Fuamhaireil", }, "bird_guardian": { "name": "Eun-uilebheisteach", }, "bird_tweeter_1": { "name": "Sileadair Uisge", }, "bird_tweeter_2": { "name": "Eòin a' Seinn", }, "blade": { "name": "Iarann air Balla", }, "blood": { "name": "\\{review}Fuil", }, "blood_pink": { "name": "\\{review}Fuil (air a sgaradh)", }, "blue_light": { "name": "Solas Gorm", }, "boat": { "name": "Bàta", }, "boat_bits": { "name": "Pìosan a' Bhàta", }, "body_part": { "name": "Pàirt Corp", }, "bridge_flat": { "name": "Drochaid Rèidh", }, "bridge_tilt_1": { "name": "Drochaid Claonadh 1", }, "bridge_tilt_2": { "name": "Drochaid Claonadh 2", }, "bubble_1": { "name": "Builgean 1", }, "bubble_2": { "name": "Builgean 2", }, "bubble_emitter": { "name": "Sgaoiladair Builgean", }, "camera_target": { "name": "Targaid Camara", }, "carcass": { "name": "Closach", }, "ceiling_spikes": { "name": "Mullach-gheugan", }, "centaur": { "name": "Ceud-damh", }, "centaur_statue": { "name": "Ìomhaigh", }, "civilian": { "name": "Fear-sìobhalta", }, "claw_mutant": { "name": "Mùtant Spòganach", }, "clock_chimes": { "name": "Gleoc Bartoli", }, "cog_1": { "name": "Inneal Gèaraichean 1", }, "cog_2": { "name": "Inneal Gèaraichean 2", }, "cog_3": { "name": "Inneal Gèaraichean 3", }, "combat_end": { "name": "Crioch-shabaid", }, "compass": { "name": "Staitistig", }, "compy": { "name": "Compsognathus", }, "controls": { "name": "Smachdan", }, "copter": { "name": "Heileacoptair", }, "cowboy": { "name": "Gille-cruidh", }, "crawler_mutant": { "name": "Mùtant Snàigeach", }, "crocodile": { "name": [ "Crocadal", "Crogall", ] }, "crow": { "name": "Feannag", }, "cult_1": { "name": "Ruspal le Masg 1", }, "cult_1a": { "name": "Ruspal le Masg 2", }, "cult_1b": { "name": "Ruspal le Masg 3", }, "cult_2": { "name": "Fear-sgian", }, "cult_3": { "name": "Ruspal le Gunna-sgaoil", }, "cut_shotgun": { "name": "Beòthalachd Gunna-sgaoil Fhrais", }, "damocles_sword": { "name": "Claidheamh Damocles", }, "dart": { "name": "Dàrt", }, "dart_effect": { "name": "Buaidh Dàrt", }, "dart_emitter": { "name": "Sgaoiladair Dàrt", }, "desert_eagle": { "name": "Desert Eagle", }, "desert_eagle_ammo": { "name": "Lòin an Desert Eagle", }, "detonator_box": { "name": "Spreadhaichear", }, "ding_dong": { "name": "Clag an Dorais", }, "dino_mutant": { "name": "Dìneasair-mùthaidh", }, "disc": { "name": "Sgian-cruinnte", }, "disc_emitter": { "name": "Sgaoiladair Sgian-cruinnte", }, "disposable_animating_1": { "name": "Beòthaladh 1", }, "disposable_animating_10": { "name": "Beòthaladh 10", }, "disposable_animating_2": { "name": "Beòthaladh 2", }, "disposable_animating_3": { "name": "Beòthaladh 3", }, "disposable_animating_4": { "name": "Beòthaladh 4", }, "disposable_animating_5": { "name": "Beòthaladh 5", }, "disposable_animating_6": { "name": "Beòthaladh 6", }, "disposable_animating_7": { "name": "Beòthaladh 7", }, "disposable_animating_8": { "name": "Beòthaladh 8", }, "disposable_animating_9": { "name": "Beòthaladh 9", }, "diver": { "name": "Sgùba-dhaibhear", }, "dog": { "name": [ "Cù", "Doberman", ] }, "door_1": { "name": "Doras 1", }, "door_2": { "name": "Doras 2", }, "door_3": { "name": "Doras 3", }, "door_4": { "name": "Doras 4", }, "door_5": { "name": "Doras 5", }, "door_6": { "name": "Doras 6", }, "door_7": { "name": "Doras 7", }, "door_8": { "name": "Doras 8", }, "dragon_back": { "name": "Cùl an Dràgon", }, "dragon_bones_1": { "name": "Neach-àite", }, "dragon_bones_2": { "name": "Cnàmhan Aghaidh an Dràgon", }, "dragon_bones_3": { "name": "Cnàmhan Cùl an Dràgon", }, "dragon_front": { "name": "Aghaidh an Dràgon", }, "drawbridge": { "name": "Drochaid-tarraing", }, "dust": { "name": "Duslach", }, "dying_monk": { "name": "Manach a' Bàsachadh", }, "dying_mutant": { "name": "Mùtant a' Bàsachadh", }, "eagle": { "name": "Iolaire", }, "earthquake": { "name": "Crith-thalmhainn", }, "eel": { "name": "Easgann", }, "electric_cleaner": { "name": "Inneal-glanaidh", }, "electric_fence": { "name": "Feansa Dealain", }, "electrical_light": { "name": "Solais Dealain", }, "ember": { "name": "Èibhleag", }, "ember_emitter": { "name": "Sgaoiladair Èibhleag", }, "explosion_1": { "name": "Spreadhadh 1", }, "explosion_2": { "name": "Spreadhadh 2", }, "falling_block_1": { "name": [ "Bloc Tuiteamach 1", "Làr Bristeach 1", "Leacan Bristeach 1", ] }, "falling_block_2": { "name": [ "Bloc Tuiteamach 2", "Làr Bristeach 2", "Leacan Bristeach 2", ] }, "falling_block_3": { "name": [ "Bloc Tuiteamach 3", "Làr Bristeach 3", "Leacan Bristeach 3", "Bùird Cugalach", ] }, "falling_ceiling_1": { "name": "Mullach Tuiteamach 1", }, "falling_ceiling_2": { "name": "Mullach Tuiteamach 2", }, "fire_head": { "name": "Ceann Teine", }, "fish_mutant": { "name": "Iasg-mùthaidh", }, "flame": { "name": [ "Lasair", "Teine", ] }, "flame_emitter": { "name": [ "Sgaoiladair Lasair", "Sgaoiladair Teine", ] }, "flame_emitter_big": { "name": "Sgaoileadair Teine (Mòr)", }, "flame_emitter_jet": { "name": "Sgaoileadair Teine (Jet)", }, "flame_emitter_side": { "name": "Sgaoileadair Teine (Gu Taobh)", }, "flame_emitter_small": { "name": "Sgaoileadair Teine (Beag)", }, "flare": { "name": "Lòchran", }, "flare_fire": { "name": "Teinntreach Lòchran", }, "flares_box": { "name": "Bogsa Lòchrain", }, "flickering_light": { "name": "Solas Boillsgeach", }, "fuse_box": { "name": "\\{review}Bogsa Fuse", }, "fx_reserved": { "name": "Cearcall Glas", }, "gamma": { "name": "Gamma", }, "gas_emitter_green": { "name": "Sgaoileadair Ghasa (Uaine)", }, "general": { "name": "Bàt-aigeil Beag", }, "globe": { "name": "Cruinne", }, "glow": { "name": "Gàir", }, "glow_reserved": { "name": "Gàir Mapa", }, "gondola": { "name": "Carbad-crochte", }, "gong": { "name": "Gong", }, "gong_bonger": { "name": "Bata a' Ghong", }, "graphics": { "name": "Grafaigean", }, "green_light": { "name": "Solas Uaine", }, "grenade": { "name": "Grenèad", }, "grenade_launcher": { "name": "Lannsair Grenèad", }, "grenade_launcher_ammo": { "name": "Grenèadan", }, "gun_flash": { "name": "Lasadh Ghunna", }, "gun_shell": { "name": "Sgèith Gheir", }, "harpoon_bolt": { "name": "Harpùn", }, "harpoon_gun": { "name": "Harpùn", }, "harpoon_gun_ammo": { "name": "Harpùnaichean", }, "hook": { "name": "Dubhan", }, "hot_liquid": { "name": "Leaghan Teth", }, "huskie": { "name": "\\{review}Cù", }, "hybrid_mutant": { "name": "Mùtant Hibrid", }, "icicle": { "name": "Bioranan-deighe", }, "inv_background": { "name": "Inneach a' Chlàr-seilbhe", }, "jelly": { "name": "Muir-tèachd", }, "kayak": { "name": "Càbaig", }, "key_1": { "name": "Iuchair 1", }, "key_2": { "name": "Iuchair 2", }, "key_3": { "name": "Iuchair 3", }, "key_4": { "name": "Iuchair 4", }, "key_hole_1": { "name": "Toll Iuchrach 1", }, "key_hole_2": { "name": "Toll Iuchrach 2", }, "key_hole_3": { "name": "Toll Iuchrach 3", }, "key_hole_4": { "name": "Toll Iuchrach 4", }, "kill_all_triggered": { "name": "Cur às do gach rud", }, "killer_statue": { "name": "Ìomhaigh le Claidheamh", }, "lara": { "name": "Lara", }, "lara_alarm": { "name": "Rabhadair-mhèirleach", }, "lara_autos": { "name": "Beòthalachd Piostalan Fèin-obrachail", }, "lara_boat": { "name": "Beòthalachd Bàta", }, "lara_desert_eagle": { "name": "Beòthalachd an Desert Eagle", }, "lara_extra": { "name": "Beòthalachd Eile Lara", }, "lara_flare": { "name": "Beòthalachd Lòchran", }, "lara_grenade": { "name": "Beòthalachd Lannsair Grenèad", }, "lara_hair": { "name": "Feaman-fuilt Lara", }, "lara_harpoon": { "name": "Beòthalachd Harpùn", }, "lara_m16": { "name": "Beòthalachd M16", }, "lara_magnums": { "name": "Beòthalachd Magnuman", }, "lara_mp5": { "name": "Beòthalachd MP5", }, "lara_pistols": { "name": "Beòthalachd Piostalan", }, "lara_rocket": { "name": "Beòthalachd Lannsair Rocaid", }, "lara_shotgun": { "name": "Beòthalachd Gunna-sgaoil", }, "lara_skidoo": { "name": "Beòthalachd Cairt-shneachda", }, "lara_uzis": { "name": "Beòthalachd Uzis", }, "larson": { "name": "Larson", }, "lava_wedge": { "name": "Cliathaich Teine-teinteach", }, "lead_bar": { "name": "Bàr Luaidhe", }, "lift": { "name": "Lioft", }, "lightning_emitter": { "name": "Sgaoiladair Dealanaich", }, "lion": { "name": "Leòmhann", }, "lioness": { "name": [ "Leòmhann-Boireann", "Leòmhann", ] }, "lizard": { "name": "Laghairt", }, "m16": { "name": "M16", }, "m16_ammo": { "name": "Lòin M16", }, "m16_flash": { "name": "Lasadh M16", }, "magnums": { "name": "Magnuman", }, "magnums_ammo": { "name": "Lòin Magnum", }, "mesh_swap_1": { "name": "Suaip Lìonra 1", }, "mesh_swap_2": { "name": "Suaip Lìonra 2", }, "mesh_swap_3": { "name": "Suaip Lìonra 3", }, "midas_touch": { "name": "Làmh Mìdas", }, "mine": { "name": "Mèinn Uisgeach", }, "mine_cart": { "name": "Cairt-mhèinne", }, "mini_copter": { "name": "Heileacoptair 2", }, "missile_atlantean_bomb": { "name": "Missil (Boma Atlanteach)", }, "missile_atlantean_shard": { "name": "Missil (Slinneag Atlanteach)", }, "missile_flame": { "name": "Missil (Teine)", }, "missile_harpoon": { "name": "Missil (Harpùn)", }, "missile_knife": { "name": "Missil (Sgian)", }, "missile_poison": { "name": "Missil (Puinnsean)", }, "monk_1": { "name": "Manach 1", }, "monk_2": { "name": "Manach 2", }, "monkey": { "name": "Muncaidh", }, "mounted_gun": { "name": "Gunna Stèidhichte", }, "mouse": { "name": "Radan", }, "movable_block_1": { "name": [ "Bloc Putaidh 1", "Bloc Gluasadach 1", ] }, "movable_block_2": { "name": [ "Bloc Putaidh 2", "Bloc Gluasadach 2", ] }, "movable_block_3": { "name": [ "Bloc Putaidh 3", "Bloc Gluasadach 3", ] }, "movable_block_4": { "name": [ "Bloc Putaidh 4", "Bloc Gluasadach 4", ] }, "moving_bar": { "name": "Bàr Gluasadach", }, "mp5": { "name": "MP5", }, "mp5_ammo": { "name": "Lòin MP5", }, "mp_1": { "name": "MP 1", }, "mp_2": { "name": "MP 2", }, "mummy": { "name": "Spaoileadan", }, "natla": { "name": "Natla", }, "natla_gun": { "name": "Gunna Natla", }, "on_off_light": { "name": "Solas Air/Dheth", }, "orca": { "name": "\\{review}Orca", }, "passport": { "name": "Geama", }, "patrol_dog": { "name": "\\{review}Cù", }, "pda": { "name": "Cluiche", }, "pendulum_1": { "name": "Poca-gainmhich", }, "pendulum_2": { "name": "Bocsa", }, "photo": { "name": "Dachaigh Lara", }, "pickup_1": { "name": "Nì Togail 1", }, "pickup_2": { "name": "Nì Togail 2", }, "pickup_aid": { "name": "Cobhair Togail", }, "pierre": { "name": "Pierre", }, "pirahnas": { "name": "Pirahnan", }, "pistols": { "name": "Piostalan", }, "pistols_ammo": { "name": "Lòin Phiostail", }, "player_1": { "name": "Actair 1", }, "player_10": { "name": "Actair 10", }, "player_2": { "name": "Actair 2", }, "player_3": { "name": "Actair 3", }, "player_4": { "name": "Actair 4", }, "player_5": { "name": "Actair 5", }, "player_6": { "name": "Actair 6", }, "player_7": { "name": "Actair 7", }, "player_8": { "name": "Actair 8", }, "player_9": { "name": "Actair 9", }, "pods": { "name": "Spàlagan", }, "poison_dart": { "name": "Dàrt Phuinnseanta", }, "poison_dart_emitter": { "name": "Sgaoiladair Dàrt Phuinnseanta", }, "portacabin": { "name": "Caban So-ghiùlain", }, "power_saw": { "name": "Sàbh-dealain", }, "prisoner": { "name": "Prìosanach", }, "propeller_1": { "name": "Proipeilear-plèana", }, "propeller_2": { "name": "Proipeilear fo Uisge", }, "propeller_3": { "name": "Gaothran", }, "pulse_light": { "name": "Solas Cuisle", }, "puma": { "name": "Pùma", }, "punk_1": { "name": "Punc 1", }, "punk_2": { "name": "Punc 2", }, "puzzle_1": { "name": "Nì Tòimhseachain 1", }, "puzzle_2": { "name": "Nì Tòimhseachain 2", }, "puzzle_3": { "name": "Nì Tòimhseachain 3", }, "puzzle_4": { "name": "Nì Tòimhseachain 4", }, "puzzle_done_1": { "name": "Toll Tòimhseachain 1 (Deiseil)", }, "puzzle_done_2": { "name": "Toll Tòimhseachain 2 (Deiseil)", }, "puzzle_done_3": { "name": "Toll Tòimhseachain 3 (Deiseil)", }, "puzzle_done_4": { "name": "Toll Tòimhseachain 4 (Deiseil)", }, "puzzle_hole_1": { "name": "Toll Tòimhseachain 1 (Falamh)", }, "puzzle_hole_2": { "name": "Toll Tòimhseachain 2 (Falamh)", }, "puzzle_hole_3": { "name": "Toll Tòimhseachain 3 (Falamh)", }, "puzzle_hole_4": { "name": "Toll Tòimhseachain 4 (Falamh)", }, "quad_bike": { "name": "Quad-baidhsagal", }, "quest_1": { "name": "Nì Tòir 1", }, "quest_2": { "name": "Nì Tòir 2", }, "quest_3": { "name": "Nì Tòir 3", }, "quest_4": { "name": "Nì Tòir 4", }, "raptor": { "name": [ "Raptor", "Dìneasair-beag", ] }, "raptor_emitter": { "name": "Sgaoiladair Raptor", }, "rat": { "name": [ "Radan", "Radan-tìre", ] }, "red_light": { "name": "Solas Dearg", }, "rib": { "name": "RIB", }, "ricochet": { "name": "Leabaidh-loisgte", }, "rocket": { "name": "Rocaid", }, "rocket_launcher": { "name": "Lannsair Rocaid", }, "rocket_launcher_ammo": { "name": "Rocaidean", }, "rolling_ball_1": { "name": [ "Ulbhag 1", "Ball-rollaidh 1", ] }, "rolling_ball_2": { "name": [ "Ulbhag 2", "Ball-rollaidh 2", ] }, "rolling_ball_3": { "name": [ "Ulbhag 3", "Ball-rollaidh 3", ] }, "rolling_ball_4": { "name": [ "Ulbhag 4", "Ball-rollaidh 4", ] }, "rotating_laser": { "name": "\\{review}Laser rothlach", }, "rx_worker_1": { "name": "Neach-obrach RX 1", }, "rx_worker_2": { "name": "Neach-obrach RX 2", }, "rx_worker_3": { "name": "\\{review}Neach-obrach RX 3", }, "save_crystal": { "name": "Criostal Geama Sàbhalaidh", }, "scion": { "name": "Scion", }, "scion_holder": { "name": "Greimiche Scion", }, "secret_1": { "name": "Dìomhair 1", }, "secret_2": { "name": "Dìomhair 2", }, "secret_3": { "name": "Dìomhair 3", }, "security_guard": { "name": "Geàrd Tèarainteachd", }, "security_laser_alarm": { "name": "\\{review}Laser Tèarainteachd (Rabhadh)", }, "security_laser_deadly": { "name": "\\{review}Laser Tèarainteachd (Marbhtach)", }, "security_laser_killer": { "name": "\\{review}Laser Tèarainteachd (Mharbhadh)", }, "sentry_gun": { "name": "\\{review}Gun Sentry Robot", }, "shadow": { "name": "Faileas", }, "shark": { "name": "Cearban", }, "shiva": { "name": "Sìabha", }, "shotgun": { "name": "Gunna-sgaoil", }, "shotgun_ammo": { "name": "Sligean Gunna-sgaoil", }, "shotgun_shell": { "name": "Sgèith Gunna-sgaoil", }, "skate_kid": { "name": "Balach Speile-bhòrd", }, "skateboard": { "name": "Speile-bhòrd", }, "skidoo_armed": { "name": "Cairt-shneachda Dhubh", }, "skidoo_driver": { "name": "Draibhear Cairt-shneachda Dhubh", }, "skidoo_fast": { "name": "Cairt-shneachda Dhearg", }, "skidoo_track": { "name": "Crios Cairt-shneachda", }, "skybox": { "name": "Bogsa-na-Speuran", }, "sliding_pillar": { "name": "Colbh Sleamhnachaidh", }, "smashable_1": { "name": "Uinneag Bhristeach 1", }, "smashable_2": { "name": "Uinneag Bhristeach 2", }, "smashable_3": { "name": "Uinneag Bhristeach 3", }, "smashable_4": { "name": "Uinneag Bhristeach 4", }, "smoke_emitter_black": { "name": "Sgaoileadair Smùid (Dubh)", }, "smoke_emitter_white": { "name": "Sgaoileadair Smùid (Geal)", }, "snake": { "name": "Nathair", }, "snow_sprite": { "name": "Rotal Cairt-shneachda", }, "sophia": { "name": "\\{review}Sophia", }, "sound": { "name": "Fuaim", }, "sphere_of_doom_1": { "name": "Spreadhadh Dràgon 1", }, "sphere_of_doom_2": { "name": "Spreadhadh Dràgon 2", }, "sphere_of_doom_3": { "name": "Spreadhadh Dràgon 3", }, "spider": { "name": "Damhan-allaidh", }, "spike_wall": { "name": "Balla-gheugan", }, "spikes": { "name": "Geugan", }, "spinning_blade": { "name": "Iarann Rothachadh", }, "splash_1": { "name": "Riplean Uisge 1", }, "splash_2": { "name": "Riplean Uisge 2", }, "springboard": { "name": "Bòrd-flosgaidh", }, "steam_emitter": { "name": "Sgaoileadair Steimhe", }, "sthpac_mercenary": { "name": "Saighdear a' Chuain Shèimh a Deas", }, "stopwatch": { "name": "Staitistig", }, "strobe_light": { "name": "Solas Sròb", }, "swat_1": { "name": "SWAT 1", }, "swat_2": { "name": "SWAT 2", }, "swat_3": { "name": "SWAT 3", }, "swinging_axe": { "name": "Tuagh", }, "switch_type_airlock": { "name": "Suids Stopadh-adhair", }, "switch_type_button": { "name": [ "Putan", "Suids", ] }, "switch_type_normal": { "name": [ "Luaidhe", "Suids", ] }, "switch_type_small": { "name": "Suids Beag", }, "switch_type_uw": { "name": [ "Luaidhe fon-Uisge", "Suids fon-Uisge", ] }, "switch_type_wheel": { "name": "Suids Cuidhle", }, "teeth_trap": { "name": [ "Ribe Fiaclan", "Doras 'Clang-Clang'", ] }, "text_box": { "name": "Frèam Eadar-aghaidh", }, "thors_handle": { "name": "Làimhseachadh Òrd Thor", }, "thors_head": { "name": "Òrd Thor", }, "tiger": { "name": "Tìgear", }, "tony": { "name": "Tònaidh", }, "torso": { "name": [ "Torsachan", "Adhamh", "Mùthaidh Mòr", ] }, "train": { "name": "Trèan", }, "trapdoor_1": { "name": "Làr-lùbte 1", }, "trapdoor_2": { "name": "Làr-lùbte 2", }, "trapdoor_3": { "name": "Làr-lùbte 3", }, "trex": { "name": [ "T-Rex", "Dìneasair-mòr", ] }, "trex_alpha": { "name": "T-Rex Priomh", }, "tribe_axeman": { "name": "Fear-treubh le Tuagh", }, "tribe_boss": { "name": "Ceannard an Treubh", }, "tribe_pipeman": { "name": "Fear-treubh le Dàrt", }, "tropical_fish": { "name": "Iasg Tropaigeach", }, "twinkle": { "name": "Priobag", }, "upv": { "name": "Carbad fon uisge", }, "uzis": { "name": "Uzis", }, "uzis_ammo": { "name": "Lòin Uzi", }, "vole": { "name": [ "Luch-uisge", "Radan-uisge", ] }, "vulture": { "name": "Fitheach", }, "wasp_mutant": { "name": "Mùtant Speach", }, "wasp_mutant_emitter": { "name": "Sgaoiladair Mùtant Speach", }, "water_sprite": { "name": "Rotal Bàta", }, "waterfall": { "name": "Ceò Eas", }, "white_light": { "name": "Solas Geal", }, "willard": { "name": "\\{review}Willard", }, "winston": { "name": "Winston", }, "winston_army": { "name": "Winston (armachd)", }, "wolf": { "name": "Madadh-allaidh", }, "worker_1": { "name": "Ruspal-gunna 1", }, "worker_2": { "name": "Ruspal-gunna 2", }, "worker_3": { "name": "Ruspal-bata 1", }, "worker_4": { "name": "Ruspal-bata 2", }, "worker_5": { "name": "Ruspal Teine-tilgidh", }, "xian_knight": { "name": "Ridir Xian", }, "xian_knight_statue": { "name": "Ìomhaigh Ridir Xian", }, "xian_spearman": { "name": "Sleaghadair Xian", }, "xian_spearman_statue": { "name": "Ìomhaigh Sleaghadair Xian", }, "yeti": { "name": "Iètaidh", }, "zipline_handle": { "name": "Greim Càball-slaodaidh", } } } ================================================ FILE: data/trx/ship/cfg/base_strings-it.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "language_name": "Italiano", "general": { "actions": { "examine_item": "Esamina", "hide_dialog": "Nascondi dialogo", "reset_defaults": "Ripristina", "rotate": "Ruota", "unbind": "Dissocia", "use_item": "Usa", }, "config_presets": { "applied": "Profilo applicato.", "confirm_description": "Verranno modificate le seguenti impostazioni:", "confirm_restart_note": "Nota: per avere effetto, alcune impostazioni potrebbero richiedere il riavvio del gioco.", "empty": "Nessun profilo trovato.", "no_changes": "Nessuna modifica da applicare.", "title_fmt": "Applicare il profilo %s?", }, "globe_select": { "area_1": "Area 1", "area_2": "Area 2", "area_3": "Area 3", "area_4": "Area 4", "area_5": "Area 5", "area_6": "Area 6", }, "inventory_ring": { "heading_adventure": "Avventura", "heading_fmt": "%s", "heading_game_over": "GAME OVER", "heading_inventory": "INVENTARIO", "heading_items": "OGGETTI", "heading_option": "OPZIONI", "item_count_fmt": "\\{small}%s", "object_name_fmt": "%s", }, "misc": { "demo_mode": "Modalità Demo", "direction_keys_controller": "Croce direzionale", "direction_keys_keyboard": "Frecce", "empty_slot_fmt": "- SLOT VUOTO -", "exit": "Esci", "hold_fmt": "Premere %s", "off": "Spento", "on": "Acceso", "pagination_nav": "%d / %d", }, "osd": { "ambiguous_input_2": "Immissione ambigua: %s e %s", "ambiguous_input_3": "Immissione ambigua: %s, %s, ...", "bilinear_filter_off": "Filtro bilineare: disattivato", "bilinear_filter_on": "Filtro bilineare: attivato", "command_bad_invocation": "Invocazione non valida: %s", "command_bool": "acceso, spento", "command_decimal": "[decimale]", "command_integer": "[intero]", "command_percent": "[intero]", "command_unavailable": "Questo comando non è attualmente disponibile", "command_valid_values": "Valori validi: %s", "complete_level": "Livello completato!", "config_option_get": "%s è attualmente impostato su %s", "config_option_set": "%s è stato modificato in %s", "config_option_unknown_option": "Opzione sconosciuta: %s", "current_health_get": "Salute attuale di Lara: %d", "current_health_set": "Salute di Lara impostata su %d", "door_close": "Chiuditi Sesamo!", "door_open": "Apriti Sesamo!", "door_open_fail": "Nessuna porta nelle vicinanze di Lara", "flipmap_fail_already_off": "Mappa capovolta già disattivata", "flipmap_fail_already_on": "Mappa capovolta già attivata", "flipmap_off": "Mappa capovolta disattivata", "flipmap_on": "Mappa capovolta attivata", "fly_mode_off": "Modalità volo disabilitata", "fly_mode_on": "Modalità volo abilitata", "fps_counter_off": "Contatore FPS disabilitato", "fps_counter_on": "Contatore FPS abilitato", "give_item": "Aggiunto %s all'inventario di Lara", "give_item_all_guns": "Preparati - Lara è armata fino ai denti!", "give_item_all_keys": "Sorpresa! Ogni oggetto chiave di cui Lara ha bisogno è ora nel suo zaino.", "give_item_cheat": "Lo zaino di Lara è diventato molto più pesante!", "heal_already_full_hp": "Lara è già in piena salute", "heal_success": "Lara è tornata in piena salute", "invalid_cutscene": "Scena di intermezzo non valida", "invalid_demo": "Demo non valida", "invalid_item": "Oggetto sconosciuto: %s", "invalid_level": "Livello non valido", "invalid_object": "Oggetto non valido", "invalid_room": "Stanza non valida: %d. Le stanze valide sono 0-%d", "invalid_sample": "Suono non valido: %d", "kill": "Ciao!", "kill_all": "Puff! %d nemici scomparsi!", "kill_all_fail": "Oh-oh, non ci sono più nemici da uccidere...", "kill_fail": "Nessun nemico nelle vicinanze...", "lighting_contrast_fmt": "Contrasto: %s", "load_game": "Partita caricata dallo slot di salvataggio %d", "load_game_fail_invalid_slot": "Slot di salvataggio %d non valido", "load_game_fail_unavailable_slot": "Lo slot di salvataggio %d non è disponibile", "object_not_found": "Oggetto non trovato", "play_cutscene": "Caricamento scena di intermezzo %d", "play_demo": "Caricamento demo %d", "play_level": "Caricamento %s", "pos_lara_missing": "Lara non presente", "pos_lara_pos_fmt": "Stanza: %d\nPosizione: %.3f, %.3f, %.3f\nRotazione: %.3f, %.3f, %.3f", "pos_level_fmt": "Livello %d", "pos_level_fmt_cutscene": "Intermezzo %d", "pos_level_fmt_demo": "Demo %d", "quick_load": "Caricamento rapido dallo slot %d", "quick_load_fail_no_bound_slot": "Nessuno slot di salvataggio attualmente assegnato", "quick_load_fail_unavailable_bound_slot": "Lo slot di salvataggio assegnato non è disponibile", "quick_save": "Salvataggio rapido", "quick_save_fail_no_slots": "Nessuno slot di salvataggio rapido configurato", "save_game": "Partita salvata nello slot di salvataggio %d", "save_game_fail_invalid_slot": "Slot di salvataggio %d non valido", "sound_available_samples": "Suoni disponibili: %s", "sound_playing_sample": "Riproduzione del suono %d", "speed_get": "Velocità attuale: %d", "speed_set": "Velocità impostata su %d", "strings_failed": "Impossibile ricaricare i file di lingua", "strings_reloaded": "File di lingua ricaricati", "textures_off": "Texture: disattivate", "textures_on": "Texture: attivate", "trapezoid_filter_off": "Filtro trapezoidale: disattivato", "trapezoid_filter_on": "Filtro trapezoidale: attivato", "ui_off": "Interfaccia utente: disattivata", "ui_on": "Interfaccia utente: attivata", "unknown_command": "Comando sconosciuto: %s", "upscaling_factor": "Fattore di scala: x%d", "wireframe_mode_off": "Modalità reticolo: disattivato", "wireframe_mode_on": "Modalità reticolo: attivato", }, "overlay": { "debug_animation": "Animazione: ", "debug_animation_state": "Stato: ", "debug_camera_pos": "Origine telecamera: ", "debug_camera_target": "Obiettivo telecamera: ", "debug_immune": "Invulnerabilità attivata", "debug_position": "Posizione: ", "debug_rotation": "Rotazione: ", "debug_speed": "Velocità: ", "item_count_fmt_pc": "\\{small}%s", "item_count_fmt_ps1": "\\{small}%s", }, "passport": { "delete_save": "\\{review}Elimina", "delete_save_confirm": "\\{review}Eliminare questo salvataggio?", "delete_save_failed": "\\{review}Impossibile eliminare il salvataggio selezionato.", "delete_save_no": "\\{review}No", "delete_save_yes": "\\{review}Sì", "exit_game": "Esci dal Gioco", "exit_to_title": "Torna al Menu", "load_game": "Carica Partita", "mode_new_game": "Nuova Partita", "mode_new_game_jp": "NP Giapponese", "mode_new_game_jp_plus": "NP Giapponese+", "mode_new_game_plus": "Nuova Partita+", "new_game": "Nuova Partita", "play_previous_levels": "Gioca livelli precedenti", "restart_level": "Ricomincia Livello", "save_game": "Salva Partita", "save_slot_unsupported": "Questo salvataggio non supporta questa funzione.", "select_level": "Seleziona Livello", "select_mod": "Seleziona Gioco", "select_mode": "Seleziona Modalità", "select_save": "Seleziona Salvataggio", "story_so_far": "Un po' di storia...", "switch_mod": "Cambia Gioco", }, "pause": { "are_you_sure": "Sei sicuro?", "continue": "Continua", "exit_to_title": "Vuoi tornare al menu?", "no": "No", "paused": "Pausa", "quit": "Esci", "yes": "Sì", }, "photo_mode": { "advance_frame": "Avanza fotogramma", "camera_move_prompt": "Sposta telecamera", "camera_reset_prompt": "Ripristina telecamera", "camera_roll_prompt": "Inclina telecamera", "camera_rotate_90_prompt": "Ruota di 90 gradi", "camera_rotate_prompt": "Ruota telecamera", "change_lara_pose": "Cambia posa", "fov_prompt": "Regola campo visivo", "lara_move_prompt": "Sposta Lara", "lara_reset_prompt": "Ripristina Lara", "lara_roll_prompt": "Inclina Lara", "lara_rotate_90_prompt": "Ruota Lara di 90°", "lara_rotate_prompt": "Ruota Lara", "snap_prompt": "Scatta una foto", "title_camera_pos": "Modalità Foto", "title_lara_pos": "Spostamento Lara", "toggle_help": "Visualizza guida", }, "settings": { "common": { "all_hidden_disclaimer": "Le impostazioni sono disabilitate per questo gruppo di livelli.", "chroma": "Chroma", "edit_value": "Modifica valore", "frozen_option_disclaimer": "Questa impostazione è applicata dal generatore di livelli e non può essere modificata.", "hue": "Tonalità", "lightness": "Luminosità", "restore_default": "Ripristina predefiniti", "toggle_help": "Visualizza guida", }, "controls": { "backend": { "controller": "Controller", "keyboard": "Tastiera", }, "customize": "Personalizza comandi", "layout": { "custom_1": "Tasti utente 1", "custom_2": "Tasti utente 2", "custom_3": "Tasti utente 3", "default": "Tasti predefiniti", }, "tabs": { "basics": "Movimento", "items": "Oggetti", "misc": "Vari", "system": "Sistema", } }, "gameplay": { "tabs": { "controls": "Comandi", "fixes": "Correzioni", "general": "Generale", "mods": "Modifiche", "presets": "Profili", }, "title": "Opzioni di gioco", }, "graphic_settings": { "tabs": { "bars": "Barre", "rendering": "Schermo", "stats": "Statistiche", "ui": "Interfaccia", "visuals": "Grafica", }, "title": "Opzioni grafiche", }, "sound": { "tabs": { "misc": "Varie", "volume": "Volume", }, "title": "Opzioni audio", } }, "stats": { "ammo": "Munizioni a Segno/Usate", "ammo_hits": "Colpi a Segno", "ammo_used": "Munizioni Usate", "assault_best_time_fmt": "%s", "assault_finish": "Fine", "assault_no_times_set": "Nessun tempo impostato", "assault_other_times_fmt": "%s", "assault_title": "MIGLIORI TEMPI", "basic_fmt": "%d", "bonus_statistics": "Statistiche Bonus", "crystals": "Cristalli", "deaths": "Morti", "detail_fmt": "%d di %d", "distance_travelled": "Distanza Percorsa", "final_statistics": "Statistiche Finali", "gym_assault_course": "Corso d'Addestramento", "gym_racetrack_course": "Corso di Guida", "kills": "Uccisioni", "level": "Livello", "medipacks_used": "Kit Medici Usati", "none": "Nessuno", "pickups": "Oggetti", "secrets": "Segreti Scoperti", "time_taken": "Tempo Impiegato", } }, "console": { "cmd": { "braid": { "help": "Attiva o disattiva la treccia di Lara.", }, "cheats": { "help": "Attiva o disattiva i trucchi di gioco.", }, "clear": { "help": "Cancella la cronologia dei messaggi della console.", }, "debug": { "help": "Attiva o disattiva le informazioni di debug.", }, "drain": { "help": "Drena la stanza corrente, rimuovendo l'acqua.", }, "end_level": { "help": "Termina il livello attuale.", }, "exit": { "help": "Termina il gioco.", }, "flipmap": { "help": "Attiva o disattiva mappa capovolta.", }, "flood": { "help": "Immerge la stanza corrente nell'acqua.", }, "fly": { "help": "Attiva o disattiva il trucco Volo.", }, "fps": { "help": "Modifica il valore degli FPS.", }, "give": { "help": "Aggiunge un determinato oggetto all'inventario di Lara.", "invalid_secret": "Segreto non valido: %s (segreti validi: %s)", "secret_given": "Segreto %s aggiunto", "secret_list": "Segreti raccolti: %d di %d (%s)", "secret_none": "Segreti raccolti: %d di %d", "secret_taken": "Segreto %s rimosso", }, "give_secret": { "help": "Mostra i segreti ottenuti da Lara, oppure aggiunge/rimuove un segreto in base al numero specificato.", }, "heal": { "help": "Riporta Lara in piena salute.", }, "help": { "help": "Mostra la guida per tutti i comandi o la guida dettagliata per uno solo.", "list": "Comandi disponibili:", }, "hp": { "help": "Imposta la salute di Lara al valore specificato.", }, "immune": { "help": "Attiva o disattiva l'invulnerabilità. (In alcune circostanze, Lara può essere comunque uccisa.)", "off": "Lara è ora vulnerabile", "on": "Lara è ora immune ai danni", }, "inf_sprint": { "help": "Attiva o disattiva lo scatto infinito.", "off": "Lara non può più scattare all'infinito", "on": "Lara può ora scattare all'infinito", }, "kill": { "help": "Uccide i nemici vicini.", }, "lighting": { "help": "Attiva o disattiva il sistema di illuminazione.", }, "load": { "help": "Carica la partita dallo slot di salvataggio indicato o da un salvataggio rapido.", }, "lua": { "help": "Esegue la stringa di codice Lua specificata.", "runtime_error": "Errore di esecuzione Lua: %s", "syntax_error": "Errore di sintassi Lua: %s", }, "mod": { "help": "Passa alla mod specificata e riavvia il gioco.", }, "music": { "help": "Riproduce la traccia musicale con l'ID specificato.", }, "play_cutscene": { "help": "Riproduce la scena di intermezzo con il numero specificato.", }, "play_demo": { "help": "Riproduce la demo con il numero specificato.", }, "play_gym": { "help": "Gioca al livello di allenamento.", }, "play_level": { "help": "Gioca un livello con il nome o il numero specificato.", }, "play_music": { "invalid_track": "Traccia musicale non valida", "stopped": "Musica interrotta", "track": "Riproduzione traccia musicale %d", }, "pos": { "help": "Mostra la posizione di Lara.", }, "save": { "help": "Salva la partita nello slot di salvataggio indicato o nello slot di salvataggio rapido successivo.", }, "screenshot": { "help": "Salva un'istantanea dello schermo su disco in un percorso facoltativo.", }, "set": { "help": "Visualizza o aggiorna l'impostazione di configurazione specificata.", }, "sfx": { "help": "Riproduce un effetto sonoro con l'ID specificato.", }, "spawn": { "fail": "Impossibile generare l'oggetto richiesto", "success": "Oggetto richiesto generato vicino a Lara", }, "speed": { "help": "Modifica la velocità di gioco.", }, "strings": { "help": "Ricarica i file di lingua correnti dal disco.", }, "teleport": { "item": "Teletrasportato all'oggetto: %d", "item_fail": "Impossibile teletrasportarsi all'oggetto: %d", "object": "Teletrasportato all'oggetto: %s", "object_fail": "Impossibile teletrasportarsi all'oggetto: %s", "pos": "Teletrasportato in posizione: %.3f %.3f %.3f", "pos_fail": "Impossibile teletrasportarsi in posizione: %.3f %.3f %.3f", "room": "Teletrasportato nella stanza: %d", "room_fail": "Impossibile teletrasportarsi nella stanza: %d", }, "textures": { "help": "Attiva o disattiva le texture.", }, "title": { "help": "Ritorna al menu principale.", }, "tp": { "help": "Teletrasporta Lara in una posizione o in un numero di stanza specifici.", }, "trigger": { "help": "Attiva o disattiva un oggetto tramite ID o nome.", "invalid_item": "Oggetto non valido: %s", "no_match": "Obiettivo sconosciuto: %s", "not_found": "Nessun oggetto corrispondente trovato per: %s", "triggered": "Oggetto(i) attivato(i): %s", "untriggered": "Oggetto(i) non attivato(i): %s", }, "vsync": { "help": "Attiva o disattiva la sincronizzazione verticale.", }, "weather": { "help": "Modifica il tempo meteorologico attuale.", "invalid": "Tempo meteorologico non valido: %s (valore valido: %s)", "set": "Tempo meteorologico impostato su %s", }, "winston": { "dead": "Il tuo maggiordomo è morto. Sei un mostro!", "spawn_failed": "Impossibile convocare Winston", "spawned": "Winston è stato convocato vicino a Lara", "teleported": "Winston è stato teletrasportato vicino a Lara", }, "wireframe": { "help": "Attiva o disattiva la renderizzazione in modalità reticolo.", } } }, "dynamic": { "config_presets": { "tr1_pc": "TR1 PC", "tr1_ps1": "TR1 PS1", "tr2_pc": "TR2 PC", "tr2_ps1": "TR2 PS1", "tr3_pc": "TR3 PC", "tr3_ps1": "TR3 PS1", }, "enums": { "bar_look": { "tr1_pc": "TR1 PC", "tr2_pc": "TR2 PC", "tr2_ps1": "TR2 PS1", "tr3_pc": "TR3 PC", "tr3_ps1": "TR3 PS1", }, "lara_outfit": { "default": "Predefinito", "golden_sophia": "Sophia Dorata", "sophia": "Sophia", "tr1_bacon_lara": "Sosia di Lara", "tr1_classic": "Classico TR1", "tr1_combo": "Combinato TR1", "tr1_golden_bacon_lara": "Sosia di Lara Dorata", "tr1_golden_lara": "Lara Dorata TR1", "tr1_gym": "Allenamento TR1 ", "tr1_mauled": "Lara ferita TR1", "tr1_ngage": "TR1 N-Gage", "tr23_golden_lara": "Lara Dorata TR2/3", "tr2_bomber_jacket": "Giubbotto pesante", "tr2_classic": "Classico TR2", "tr2_diving_suit": "Muta subacquea 1", "tr2_diving_suit_alpha": "Muta subacquea 2", "tr2_gym": "Allenamento TR2", "tr2_robe": "Vestaglia", "tr2_vegas": "Las Vegas", "tr3_antarctica": "Antartide", "tr3_catsuit": "Tuta aderente", "tr3_classic": "Classico TR3", "tr3_gym": "Allenamento TR3", "tr3_nevada": "Nevada", "tr3_south_pacific": "Sud Pacifico", } }, "mods": { "tr1": { "title": "Tomb Raider I", }, "tr1-demo-pc": { "title": "Demo di Tomb Raider I", }, "tr1-ub": { "title": "Conti in Sospeso", }, "tr2": { "title": "Tomb Raider II", }, "tr2-gm": { "title": "La Maschera Dorata", }, "tr3": { "title": "Tomb Raider III", }, "tr3-la": { "title": "L'Artefatto Perduto", } } }, "enums": { "ALLY_HOSTILITY_POLICY": { "ALLY_HOSTILITY_POLICY_INDIVIDUAL": "Individuale", "ALLY_HOSTILITY_POLICY_SHARED": "Condiviso", }, "ASPECT_MODE": { "ASPECT_MODE_16_10": "16:10", "ASPECT_MODE_16_9": "16:9", "ASPECT_MODE_4_3": "4:3", "ASPECT_MODE_ANY": "Qualsiasi", }, "BACKGROUND_TYPE": { "BK_BLACK": "Nero", "BK_IMAGE": "Immagine", "BK_MONOCHROME": "Monocromatico", "BK_MONOCHROME_COOL": "Monocromatico (freddo)", "BK_MONOCHROME_WARM": "Monocromatico (caldo)", "BK_NONE": "Trasparente", "BK_PATTERN_STATIC": "Statico", "BK_PATTERN_WAVE": "Ondeggiante", "BK_TRANSPARENT_DARK": "Molto scuro", "BK_TRANSPARENT_MEDIUM": "Scuro", }, "BAR_SHOW_MODE": { "BAR_SHOW_MODE_ALWAYS": "Sempre", "BAR_SHOW_MODE_BOSS_ONLY": "Solo per i Boss", "BAR_SHOW_MODE_NEVER": "Mai", }, "BILLBOARD_LOCK_MODE": { "BILLBOARD_LOCK_NONE": "Nessuno", "BILLBOARD_LOCK_PERSPECTIVE": "Prospettiva", "BILLBOARD_LOCK_ROLL": "Rotazione", "BILLBOARD_LOCK_ROLL_PITCH": "Rotaz. e inclinaz.", }, "BLOOD_EFFECTS": { "BLOOD_EFFECTS_DISABLED": "\\{review}Disabilitato", "BLOOD_EFFECTS_PINK": "\\{review}Rosa", "BLOOD_EFFECTS_RED": "\\{review}Rosso", }, "CAMERA_MODE": { "CAMERA_MODE_TR1": "TR1", "CAMERA_MODE_TR2": "TR2", "CAMERA_MODE_TR3": "TR3", }, "CREATURE_DROWN_POLICY": { "CREATURE_DROWN_POLICY_DEFAULT": "\\{review}Predefinito", "CREATURE_DROWN_POLICY_NEVER": "\\{review}Mai", "CREATURE_DROWN_POLICY_SUBMERGED": "\\{review}Sommerso", }, "INPUT_BACKEND": { "INPUT_BACKEND_CONTROLLER": "Controller", "INPUT_BACKEND_KEYBOARD": "Tastiera", }, "INPUT_ROLE": { "INPUT_ROLE_ACTION": "Azione", "INPUT_ROLE_CAMERA_BACK": "Telecamera Posteriore", "INPUT_ROLE_CAMERA_DOWN": "Telecamera in Basso", "INPUT_ROLE_CAMERA_FORWARD": "Telecamera Anteriore", "INPUT_ROLE_CAMERA_LEFT": "Telecamera Sinistra", "INPUT_ROLE_CAMERA_RESET": "Ripristina Telecamera", "INPUT_ROLE_CAMERA_RIGHT": "Telecamera Destra", "INPUT_ROLE_CAMERA_UP": "Telecamera in Alto", "INPUT_ROLE_CHANGE_OUTFIT": "Cambia Costume", "INPUT_ROLE_CHANGE_TARGET": "Cambia Bersaglio", "INPUT_ROLE_CROUCH": "Accovacciati", "INPUT_ROLE_CYCLE_LIGHTING_CONTRAST": "Livello Contrasto", "INPUT_ROLE_DOWN": "Indietro", "INPUT_ROLE_DRAW_WEAPON": "Estrai Arma", "INPUT_ROLE_ENTER_CONSOLE": "Apri Console", "INPUT_ROLE_EQUIP_AUTOS": "Equipaggia Pistole Automatiche", "INPUT_ROLE_EQUIP_DESERT_EAGLE": "Equipaggia Desert Eagle", "INPUT_ROLE_EQUIP_GRENADE_LAUNCHER": "Equipaggia Lanciagranate", "INPUT_ROLE_EQUIP_HARPOON": "Equipaggia Fucile Subacqueo", "INPUT_ROLE_EQUIP_M16": "Equipaggia M16", "INPUT_ROLE_EQUIP_MAGNUMS": "Equipaggia Magnum", "INPUT_ROLE_EQUIP_MP5": "Equipaggia MP5", "INPUT_ROLE_EQUIP_PISTOLS": "Equipaggia Pistole", "INPUT_ROLE_EQUIP_ROCKET_LAUNCHER": "Equipaggia Lanciarazzi", "INPUT_ROLE_EQUIP_SHOTGUN": "Equipaggia Fucile a Pompa", "INPUT_ROLE_EQUIP_UZIS": "Equipaggia Uzi", "INPUT_ROLE_FLY_CHEAT": "Trucco Volo", "INPUT_ROLE_FPS": "Mostra FPS", "INPUT_ROLE_INVENTORY": "Inventario", "INPUT_ROLE_ITEM_CHEAT": "Trucco Oggetto", "INPUT_ROLE_JUMP": "Salta", "INPUT_ROLE_LEFT": "Sinistra", "INPUT_ROLE_LEVEL_SKIP_CHEAT": "Salta Livello", "INPUT_ROLE_LOAD": "Carica Partita", "INPUT_ROLE_LOOK": "Guarda", "INPUT_ROLE_PAUSE": "Pausa", "INPUT_ROLE_QUICK_LOAD": "Caricamento Rapido", "INPUT_ROLE_QUICK_SAVE": "Salvataggio Rapido", "INPUT_ROLE_RIGHT": "Destra", "INPUT_ROLE_ROLL": "Capriola", "INPUT_ROLE_SAVE": "Salva Partita", "INPUT_ROLE_SCREENSHOT": "Cattura Schermo", "INPUT_ROLE_SLOW": "Cammina", "INPUT_ROLE_SPRINT": "Scatto", "INPUT_ROLE_STEP_LEFT": "Passo a Sinistra", "INPUT_ROLE_STEP_RIGHT": "Passo a Destra", "INPUT_ROLE_SWITCH_BORDERS": "Modifica Dimensione Bordi", "INPUT_ROLE_SWITCH_UPSCALING": "Modifica Fattore di Scala", "INPUT_ROLE_TOGGLE_BILINEAR_FILTER": "Attiva/Disattiva Filtro Bilineare", "INPUT_ROLE_TOGGLE_FULLSCREEN": "Attiva/Disattiva Schermo Intero", "INPUT_ROLE_TOGGLE_PHOTO_MODE": "Attiva/Disattiva Modalità Foto", "INPUT_ROLE_TOGGLE_TEXTURES": "Attiva/Disattiva Texture", "INPUT_ROLE_TOGGLE_TRAPEZOID_FILTER": "Attiva/Disattiva Filtro Trapezoidale", "INPUT_ROLE_TOGGLE_UI": "Attiva/Disattiva Interfaccia", "INPUT_ROLE_TOGGLE_WIREFRAME": "Attiva/Disattiva Modalità Reticolo", "INPUT_ROLE_TURBO_CHEAT": "Velocità Turbo", "INPUT_ROLE_UP": "Corri", "INPUT_ROLE_USE_BIG_MEDI": "Kit Medico Grande", "INPUT_ROLE_USE_FLARE": "Razzo di Segnalazione", "INPUT_ROLE_USE_SMALL_MEDI": "Kit Medico Piccolo", }, "JUMP_LOCK_MODE": { "JUMP_LOCK_DISABLED": "Disattivato", "JUMP_LOCK_LEGACY": "Originale", "JUMP_LOCK_TUNED": "Modificato", }, "LIGHTING_CONTRAST": { "LIGHTING_CONTRAST_HIGH": "Alto", "LIGHTING_CONTRAST_LOW": "Basso", "LIGHTING_CONTRAST_MEDIUM": "Medio", }, "LOADING_SCREENS_MODE": { "LOADING_SCREENS_ALWAYS": "Sempre", "LOADING_SCREENS_DISABLED": "Disattivato", "LOADING_SCREENS_NEW_GAMES": "Nuovo livello", }, "LOOK_MODE": { "LOOK_MODE_ENHANCED": "Avanzato", "LOOK_MODE_RESTRICTED": "Limitato", "LOOK_MODE_UNRESTRICTED": "Illimitato", }, "MUSIC_LOAD_CONDITION": { "MUSIC_LOAD_CONDITION_ALWAYS": "Sempre", "MUSIC_LOAD_CONDITION_NEVER": "Mai", "MUSIC_LOAD_CONDITION_NON_AMBIENT": "Non ambientale", }, "PROJECTILE_AREA_DAMAGE": { "PROJECTILE_AREA_DAMAGE_MULTI_SWEEP": "Diffusione multipla", "PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP": "Diffusione singola", }, "QUICK_GUNS_MODE": { "QUICK_GUNS_MODE_DRAW_AND_HOLSTER": "Equipaggia o riponi", "QUICK_GUNS_MODE_DRAW_ONLY": "Solo equipaggia", }, "SCREENSHOT_FORMAT": { "SCREENSHOT_FORMAT_JPEG": "JPG", "SCREENSHOT_FORMAT_PNG": "PNG", }, "SHADOW_TYPE": { "SHADOW_TYPE_CIRCLE": "Cerchio", "SHADOW_TYPE_OCTAGON": "Ottagono", "SHADOW_TYPE_SPRITE": "Sprite", }, "STATS_STYLE": { "STATS_STYLE_BARE": "Senza bordi", "STATS_STYLE_BORDERED": "Con bordi", }, "SUNGLASSES_MODE": { "SUNGLASSES_MODE_OFF": "Disattivato", "SUNGLASSES_MODE_OPAQUE": "Opaco", "SUNGLASSES_MODE_TRANSPARENT": "Trasparente", }, "TARGET_LOCK_MODE": { "TARGET_LOCK_MODE_FULL": "Completo", "TARGET_LOCK_MODE_NONE": "Nessuno", "TARGET_LOCK_MODE_SEMI": "Parziale", }, "TEXTURE_FILTER": { "TEXTURE_FILTER_BILINEAR": "Bilineare", "TEXTURE_FILTER_POINT": "Nitido", }, "UI_ELEMENT_LOCATION": { "UI_ELEMENT_LOCATION_BOTTOM_CENTER": "In basso al centro", "UI_ELEMENT_LOCATION_BOTTOM_LEFT": "In basso a sinistra", "UI_ELEMENT_LOCATION_BOTTOM_RIGHT": "In basso a destra", "UI_ELEMENT_LOCATION_TOP_CENTER": "In alto al centro", "UI_ELEMENT_LOCATION_TOP_LEFT": "In alto a sinistra", "UI_ELEMENT_LOCATION_TOP_RIGHT": "In alto a destra", }, "UI_STYLE": { "UI_STYLE_PC": "PC", "UI_STYLE_PS1": "PS1", }, "WALL_GLITCH_MODE": { "WALL_GLITCH_FIXED": "Corretto", "WALL_GLITCH_TR1": "TR1", "WALL_GLITCH_TR2": "TR2", } }, "settings": { "audio.ambient_volume": { "title": "Volume ambientale", "description": "Regola il volume dei suoni ambientali.", }, "audio.cutscene_volume": { "title": "Volume scene di intermezzo", "description": "Regola il volume delle scene di intermezzo presenti nel gioco.", }, "audio.enable_lara_mic": { "title": "Microfono vicino a Lara", "description": "Imposta il microfono in modo che sia collocato nella posizione di Lara. Se disabilitato, il microfono sarà collocato nella posizione della telecamera.", }, "audio.enable_music_in_inventory": { "title": "Musica nell'inventario", "description": "Permette di riprodurre i suoni di gioco, gli effetti ambientali e la musica nella schermata dell'inventario.", }, "audio.enable_music_in_menu": { "title": "Musica menu principale", "description": "Riproduce la musica nel menu principale.", }, "audio.enable_pitched_sounds": { "title": "Tonalità suoni", "description": "Per una maggiore varietà, consente di modificare in modo leggero e casuale la tonalità degli effetti sonori.", }, "audio.enable_ps1_sfx": { "title": "Effetti sonori PS1", "description": "Sostituisce alcuni effetti sonori con quelli della versione PS1.\n\n- Suono dello sparo delle Uzi (solo TR1)\n- Suono dei passi a piedi nudi di Lara (solo TR2)", }, "audio.enable_underwater_anim_sfx": { "title": "Effetti sonori sott'acqua", "description": "Permette di gestire la riproduzione di specifici effetti sonori legati ad animazioni, per oggetti quali porte e botole, quando la telecamera è sott'acqua.", }, "audio.fix_chainblock_secret_sound": { "title": "Correggi suono dei segreti", "description": "Impedisce la riproduzione errata del suono dei segreti quando si utilizza la chiave d'oro nella Tomba di Tihocan.", }, "audio.fix_secrets_killing_music": { "title": "Musica segreti in polifonia", "description": "Risolve il problema per cui il suono riprodotto dalla scoperta di un segreto interrompe la traccia musicale attiva.", }, "audio.fix_speeches_killing_music": { "title": "Dialoghi nemici in polifonia", "description": "Risolve il problema per cui i nemici, quando parlano, interrompono la traccia musicale attiva.", }, "audio.fmv_volume": { "title": "Volume FMV", "description": "Regola il volume dei filmati.", }, "audio.inventory_ambient_volume": { "title": "Volume ambientale (inventario)", "description": "Regola il volume dei suoni ambientali nella schermata dell'inventario.", }, "audio.inventory_music_volume": { "title": "Volume musica (inventario)", "description": "Regola il volume della musica nella schermata dell'inventario.", }, "audio.load_music_triggers": { "title": "Ricorda musica riprodotta", "description": "Contrassegna le tracce musicali riprodotte nel file di salvataggio, in modo che le tracce legate ad eventi non vengano rieseguite dopo il caricamento della partita.", }, "audio.master_volume": { "title": "\\{icon music} Volume principale", "description": "Regola il volume di gioco. Le altre impostazioni sono relative a questo volume.", }, "audio.music_load_condition": { "title": "Ripristina traccia", "description": "Carica la traccia musicale che era in riproduzione al momento del salvataggio della partita.\n\n- Mai: non ripristina le tracce musicali al caricamento.\n- Non ambientale: ripristina le tracce musicali ma non i suoni ambientali al caricamento.\n- Sempre: ripristina qualsiasi tipo di traccia al caricamento.", }, "audio.music_volume": { "title": "Volume musica", "description": "Regola il volume della musica.", }, "audio.mute_out_of_focus": { "title": "Disattiva se in secondo piano", "description": "Disattiva tutta la musica e gli effetti sonori quando la finestra del gioco non è in primo piano.", }, "audio.sound_volume": { "title": "\\{icon sound} Volume effetti", "description": "Regola il volume degli effetti sonori.", }, "audio.underwater_ambient_volume": { "title": "Volume ambientale (sott'acqua)", "description": "Regola il volume dei suoni ambientali quando si è sott'acqua.", }, "audio.underwater_music_volume": { "title": "Volume musica (sott'acqua)", "description": "Regola il volume della musica quando si è sott'acqua.", }, "debug.enable_endless_flare_time": { "title": "Razzi inesauribili", "description": "Impedisce ai razzi di segnalazione di spegnersi. I razzi lanciati continueranno a spegnersi normalmente.", }, "debug.enable_endless_sprint": { "title": "Corsa infinita", "description": "Impedisce a Lara di stancarsi durante uno scatto. Gli ostacoli la faranno comunque fermare.", }, "gameplay.ally_hostility_policy": { "title": "Modalità ostilità alleati", "description": "Controlla il modo in cui le unità alleate reagiscono quando subiscono danni.\n\n- Individuale: ogni alleato passa in modalità ostile in modo autonomo (come in TR3).\n- Condiviso: tutti gli alleati diventano ostili insieme (come i monaci in TR2).", }, "gameplay.camera_speed": { "title": "Velocità telecamera", "description": "Modifica la velocità di movimento della telecamera manuale.", }, "gameplay.change_pierre_spawn": { "title": "Modifica generazione Pierre", "description": "Fa in modo che, alla sua generazione, un Pierre (in fuga) sostituisca un altro Pierre (in fuga) già esistente.", }, "gameplay.creature_drown_policy": { "title": "\\{review}Comportamento di annegamento delle creature", "description": "\\{review}Controlla come le creature terrestri si comportano nelle stanze d’acqua.\n\n- Mai: le creature terrestri non annegano mai (stile TR1).\n- Predefinito: le creature terrestri annegano in acqua profonda 2 clic o più (stile TR2/3).\n- Sommerso: le creature terrestri annegano solo quando sono completamente sommerse.", }, "gameplay.disable_extra_guns": { "title": "Rimuovi armi extra", "description": "Rimuove tutte le armi e le relative munizioni dal gioco tranne le pistole (per le sfide con solo pistole).", }, "gameplay.disable_healing_between_levels": { "title": "Danni persistenti", "description": "Lara non guarisce quando inizia un nuovo livello (per le sfide).", }, "gameplay.disable_medpacks": { "title": "Rimuovi i kit medici", "description": "Rimuove tutti i kit medici dal gioco (per le sfide).", }, "gameplay.disable_trex_collision": { "title": "Rimuovi collisione T-Rex morto", "description": "Rimuove tutte le collisioni con il T-Rex dopo la sua dipartita. Utile quando il corpo del T-Rex blocca il passaggio.", }, "gameplay.enable_ally_targeting": { "title": "Consenti fuoco amico", "description": "Permette a Lara di colpire gli alleati, come i monaci. Se disabilitato, gli alleati saranno immuni ai proiettili di Lara.", }, "gameplay.enable_auto_item_selection": { "title": "Preselezione oggetti chiave", "description": "Quando Lara preme azione contro un buco della serratura o un altro ricettacolo e ha l'oggetto corrispondente nell'inventario, quell'oggetto verrà preselezionato.", }, "gameplay.enable_back_slope_stumble": { "title": "Inciampo su pendenza", "description": "Fa in modo che Lara inciampi se effettua un salto all'indietro e, alle sue spalle, c'è una pendenza (TR3). Se disabilitato, Lara si fermerà bruscamente contro la pendenza (TR1/2).", }, "gameplay.enable_body_bags": { "title": "\\{review}Trigger dei sacchi per cadaveri", "description": "\\{review}Consente la rimozione dei nemici uccisi quando Lara attraversa specifici trigger in determinati livelli. Se disabilitato, i nemici morti verranno sempre visualizzati.", }, "gameplay.enable_boulder_shake": { "title": "Vibrazione massi", "description": "Se abilitata, la telecamera trema quando un masso è in movimento.", }, "gameplay.enable_bouncy_grenades": { "title": "Collisione granate", "description": "Abilita il comportamento delle granate in stile TR3: rimbalzano sui muri e sulle pendenze e producono un raggio d'esplosione più ampio, a scapito di una velocità ridotta.", }, "gameplay.enable_cheats": { "title": "Trucchi", "description": "Abilita vari trucchi:\n\n- L: termina immediatamente il livello.\n- I: dà a Lara tutte le armi; maggior quantità di munizioni e kit medici; tutti gli oggetti necessari per superare il livello.\n- O: abilita il trucco DOZY (nuotare a mezz'aria).\n - tasto CAMMINA: disabilita il trucco DOZY.\n - tasto ESTRAI ARMA: apre la porta più vicina (non funziona in certi casi).", }, "gameplay.enable_cinematics": { "title": "Scene iniziali", "description": "Abilita le scene iniziali nei livelli in cui sono previste.", }, "gameplay.enable_compass_stats": { "title": "Statistiche livello bussola", "description": "Abilita la visualizzazione delle statistiche del livello quando è selezionata la bussola.", }, "gameplay.enable_console": { "title": "Console", "description": "Abilita la console per sviluppatori.", }, "gameplay.enable_controlled_drops": { "title": "Cadute controllate", "description": "Permette a Lara di girare a mezz'aria e afferrare la sporgenza della superficie da cui è appena scivolata, se durante la caduta viene premuto il tasto Azione.", }, "gameplay.enable_crawl_jump": { "title": "Salto uscita cunicolo", "description": "Permette a Lara di saltare fuori dagli spazi angusti.", }, "gameplay.enable_crawl_tilt": { "title": "\\{review}Inclinazione in strisciamento", "description": "\\{review}Allinea la rotazione di Lara alla geometria del pavimento durante lo strisciamento.", }, "gameplay.enable_crawling": { "title": "Strisciamento", "description": "Permette a Lara di accovacciarsi e strisciare.", }, "gameplay.enable_credits": { "title": "Titoli di coda", "description": "Abilita i titoli di coda mostrati dopo aver completato il gioco. Non influisce sulla schermata finale delle statistiche.", }, "gameplay.enable_crouch_roll": { "title": "Capriola da accovacciata", "description": "Permette a Lara di eseguire una capriola in avanti mentre è accovacciata premendo il tasto Scatto.", }, "gameplay.enable_cutscenes": { "title": "Scene di intermezzo", "description": "Abilita la riproduzione delle scene di intermezzo.", }, "gameplay.enable_demo": { "title": "Modalità demo", "description": "Abilita la riproduzione delle demo nel menu principale.", }, "gameplay.enable_enemy_rotation": { "title": "Rotazione nemici casuale", "description": "Applica una rotazione casuale aggiuntiva ad alcuni nemici quando vengono inizializzati.", }, "gameplay.enable_enhanced_saves": { "title": "Salva gli effetti", "description": "Migliora il sistema di salvataggio in modo che gli effetti grafici, la foschia delle cascate, gli emettitori di fiamme e altro vengano salvati invece di scomparire dopo il caricamento della partita.", }, "gameplay.enable_fmv": { "title": "FMV", "description": "Abilita la riproduzione dei filmati.", }, "gameplay.enable_game_modes": { "title": "Selezione modalità di gioco", "description": "Consente di selezionare le modalità di gioco aggiuntive dal menu passaporto.\n\n- Nuova Partita+: sblocca tutte le armi con munizioni infinite; i nemici hanno punti vita doppi.\n- NP giapponese: le armi causano il doppio dei danni; le scatole di razzi contengono 8 razzi di segnalazione anziché 6.\n- NP giapponese+: combinazione di Nuova Partita+ e NP giapponese.", }, "gameplay.enable_idle_pose_camera": { "title": "Telecamera posa inattiva", "description": "Regola la telecamera in modo che sia rivolta verso Lara mentre viene riprodotta la sua animazione di posa inattiva. Premi Guarda per reimpostare la telecamera.", }, "gameplay.enable_inverted_look": { "title": "Visuale invertita", "description": "Inverte i controlli dell'asse Y quando Lara si guarda intorno.", }, "gameplay.enable_item_examining": { "title": "Esame degli oggetti", "description": "Per livelli personalizzati: consente di visualizzare le descrizioni degli oggetti nell'inventario quando fornite dall'autore del livello.", }, "gameplay.enable_jump_twists": { "title": "Capriole a mezz’aria", "description": "Abilita i salti mortali e le capriole a mezz'aria, ovvero premendo il tasto Capriola durante le animazioni del salto e del tuffo ad angelo.", }, "gameplay.enable_killer_pushblocks": { "title": "Blocchi mobili letali", "description": "Se abilitato, quando un blocco mobile cade e atterra su Lara, lei morirà all'istante. Se disabilitato, Lara si sposterà sopra il blocco e rimarrà illesa.", }, "gameplay.enable_lean_jumping": { "title": "Salti inclinati", "description": "Permette a Lara di spostarsi leggermente in avanti o indietro, tenendo premuto il relativo tasto, quando esegue salti sul posto.", }, "gameplay.enable_ledge_jumps": { "title": "Salti da sporgenza", "description": "Permette a Lara di saltare verso l'alto o all'indietro mentre è appesa a una sporgenza, a patto che ci sia una superficie solida sotto i suoi piedi con cui darsi uno slancio", }, "gameplay.enable_legal": { "title": "Contenuto legale", "description": "Abilita la schermata di introduzione e il video Core Design all'avvio del gioco.", }, "gameplay.enable_manual_camera": { "title": "Telecamera manuale", "description": "Usa i tasti della telecamera (\\{input camera_forward}\\{input camera_back}\\{input camera_left}\\{input camera_right}) utilizzati nella Modalità Foto, anche per ruotare la telecamera di gioco.", }, "gameplay.enable_neutral_twists": { "title": "Rotazione da fermo", "description": "Permette a Lara di ruotare in aria mentre esegue un salto da fermo. Mentre sei fermo, premi contemporaneamente i tasti Salta e Capriola.", }, "gameplay.enable_pickup_aids": { "title": "Segnalazione oggetti", "description": "Abilita un effetto scintillante intermittente vicino agli oggetti da raccogliere per evidenziarne la presenza.", }, "gameplay.enable_play_previous_levels": { "title": "Gioca livelli precedenti", "description": "Abilita le opzioni \"Gioca livelli precedenti\" e \"Un po' di storia...\" nella schermata di selezione Nuova Partita.", }, "gameplay.enable_responsive_crawl": { "title": "Strisciamento reattivo", "description": "Abilita miglioramenti rispetto alle meccaniche originali.\n\n- Consente di riprendere a strisciare più rapidamente dopo essersi fermati.\n- Consente di passare dalla corsa/scatto allo strisciare senza prima fermarsi.\n- Consente di passare dallo strisciare alla capriola da accovacciata (se abilitata) senza doversi prima accovacciare manualmente.\n- Consente di girarsi mentre si è accovacciati.\n- Ripristina l’animazione di Lara di raccolta degli oggetti mentre lei striscia (esclusi i razzi).", }, "gameplay.enable_responsive_sprint": { "title": "Scatto migliorato", "description": "Abilita per Lara una modalità di scatto migliorata.\n\n- consente di effettuare uno scatto non appena Lara ha energia, invece di dover aspettare che la barra della resistenza sia piena.\n- consente di effettuare uno scatto sulle scale senza essere interrotti dall’animazione di corsa normale.", }, "gameplay.enable_save_crystals": { "title": "Cristalli di salvataggio", "description": "Permette di salvare all'inizio di ogni livello o utilizzando i cristalli. I livelli hanno un numero limitato di cristalli di salvataggio che possono essere utilizzati una sola volta, come nella versione PS1. La modifica di questa opzione richiederà il riavvio del livello.", }, "gameplay.enable_slide_to_run": { "title": "Corri dopo scivolata", "description": "Permette a Lara di iniziare a correre immediatamente quando tocca terra dopo essere scivolata in avanti da una pendenza. Per farlo, tieni premuto il tasto Corri.", }, "gameplay.enable_slow_ledge_swing": { "title": "Oscillazione su sporgenza", "description": "Permette a Lara di oscillare lentamente quando è aggrappata ad una sporgenza molto sottile (come in TR3). Se disabilitato, Lara oscillerà solo nel momento in cui si aggrappa alla sporgenza (come in TR1/2).", }, "gameplay.enable_smooth_wall_deflect": { "title": "Deflessione rapida da muro", "description": "Permette a Lara di riprendersi più velocemente dopo aver urtato un muro e un tasto direzionale viene premuto insieme con il tasto Corri.", }, "gameplay.enable_soft_statics": { "title": "Collisione delicata con mesh", "description": "Permette a Lara di muoversi dolcemente contro le maglie poligonali statiche – simile a TR4+ – anziché fermarsi bruscamente.", }, "gameplay.enable_sprint": { "title": "Scatto", "description": "Permette a Lara di effettuare uno scatto, come in TR3+.", }, "gameplay.enable_step_roll_boost": { "title": "Spinta su gradino", "description": "Fa in modo che Lara sia spinta giù da un gradino se si preme il tasto Capriola vicino al bordo.", }, "gameplay.enable_swing_cancel": { "title": "Annulla oscillazione", "description": "Consente di annullare l'animazione di oscillazione di Lara dalle sporgenze, lasciandole andare e afferrandole di nuovo rapidamente", }, "gameplay.enable_target_change": { "title": "Cambio bersaglio", "description": "Abilita il cambio di bersaglio in stile TR4+ mentre si punta l'arma. Premi il tasto Cambia Bersaglio mentre miri per cambiare bersaglio.", }, "gameplay.enable_timer_in_inventory": { "title": "Cronometro nell'inventario", "description": "Fa in modo che il tempo di gioco scorra anche nel menu dell'inventario.", }, "gameplay.enable_toggle_crouch": { "title": "Attiva/Disattiva accovacciati", "description": "Permette a Lara di rimanere accovacciata dopo aver premuto una volta il tasto \"Accovacciati\". Premi di nuovo il tasto per rialzarti.", }, "gameplay.enable_toggle_sprint": { "title": "Attiva/Disattiva scatto", "description": "Permette a Lara di continuare a correre dopo aver premuto una volta il tasto \"Scatto\". Premi di nuovo il tasto per interrompere lo scatto.", }, "gameplay.enable_total_stats": { "title": "Schermata statistiche finali", "description": "Abilita la schermata delle statistiche totali di gioco che viene visualizzata dopo i titoli di coda.", }, "gameplay.enable_tr2_jumping": { "title": "Salti reattivi", "description": "Permette a Lara di saltare in qualsiasi momento mentre corre.", }, "gameplay.enable_tr2_swim_cancel": { "title": "Annullamento reattivo nuoto", "description": "Permette a Lara di fermarsi in modo più istantaneo sott'acqua quando viene rilasciato il tasto per nuotare.", }, "gameplay.enable_tr2_swimming": { "title": "Nuoto naturale", "description": "Fornisce alla velocità di virata subacquea di Lara una curva di accelerazione per movimenti più fluidi, come in TR2+. Disabilitando questa opzione, Lara avrà una velocità di virata più rapida, come in TR1.", }, "gameplay.enable_uw_roll": { "title": "Capriola sott'acqua", "description": "Permette a Lara di eseguire capriole mentre è sott'acqua.", }, "gameplay.enable_wading": { "title": "Guadare", "description": "Permette a Lara di guadare acque poco profonde, anziché rimanere bloccata sulla superficie dell'acqua.", }, "gameplay.enable_walk_to_items": { "title": "Interazioni animate", "description": "Fa in modo che Lara si avvicini agli oggetti, alle leve e agli interruttori quando questi sono nelle vicinanze, invece di teletrasportarsi su di loro.", }, "gameplay.fix_alligator_ai": { "title": "Correggi IA degli alligatori", "description": "Risolve il problema degli alligatori che non infliggono danni se Lara rimane ferma nell'acqua.", }, "gameplay.fix_bear_ai": { "title": "Correggi IA degli orsi", "description": "Risolve il problema per cui gli attacchi con la zampa degli orsi non colpiscono Lara.", }, "gameplay.fix_bridge_collision": { "title": "Correggi collisione ponti", "description": "Risolve il problema per cui Lara non è in grado di afferrare parti di alcuni ponti e muri invisibili ai bordi. Corregge anche i problemi di collisione con ponti levatoi, botole e ponti quando sono impilati l'uno sull'altro, su pendii o vicino al suolo.", }, "gameplay.fix_descending_glitch": { "title": "Correggi cadute da pavimenti", "description": "Risolve il problema per cui i passi laterali e il camminare all'indietro su pavimenti fragili fanno sì che Lara scenda immediatamente sul pavimento sottostante.", }, "gameplay.fix_flare_throw_priority": { "title": "Correggi priorità lancio razzi", "description": "Risolve il problema per cui Lara dà priorità al lancio di un razzo di segnalazione esaurito mentre è a mezz'aria, il che può impedirle di afferrare le sporgenze.", }, "gameplay.fix_floor_data_issues": { "title": "Correggi dati livelli", "description": "Risolve i problemi dei livelli originali relativi a dati/eventi.", }, "gameplay.fix_free_flare_glitch": { "title": "Correggi errore dei razzi", "description": "Risolve il problema in cui, se si preme il tasto di estrazione di un razzo di segnalazione mentre si raccoglie un qualsiasi oggetto, ne verrà creato uno dal nulla.", }, "gameplay.fix_item_duplication_glitch": { "title": "Correggi duplicazione oggetti", "description": "Risolve il problema per cui è possibile duplicare l'utilizzo degli oggetti chiave nell'inventario.", }, "gameplay.fix_lara_pickup_embed": { "title": "Correggi errore di raccolta", "description": "Risolve i problemi per cui Lara a volte scivola contro i muri quando raccoglie oggetti sott’acqua, e per cui si incastra sotto i soffitti fortemente inclinati quando raccoglie gli oggetti sopra l'acqua.", }, "gameplay.fix_m16_accuracy": { "title": "Correggi precisione M16/MP5", "description": "Corregge la precisione della mira dell'M16/MP5 mentre Lara corre.", }, "gameplay.fix_monkey_pickup_priority": { "title": "Correggi priorità scimmie", "description": "Le scimmie sotto attacco daranno priorità alla rappresaglia piuttosto che alla raccolta di Kit Medici e Chiavi.", }, "gameplay.fix_pipeman_aim": { "title": "Correggi mira indigeno", "description": "Risolve il problema per cui a volte l'indigeno con la cerbottana non riesce a mirare correttamente verso Lara.", }, "gameplay.fix_qwop_glitch": { "title": "Correggi errore QWOP", "description": "Risolve il problema per cui a volte Lara, saltando da piccoli gradini, provoca una strana animazione di corsa nota come stato QWOP.", }, "gameplay.fix_step_glitch": { "title": "Correggi errore gradini", "description": "Risolve il problema per cui a volte Lara viene spinta contro i muri adiacenti ai gradini quando corre su di loro in un certo modo.", }, "gameplay.fix_wade_wall_hit": { "title": "Correggi impatto muro", "description": "Risolve il problema per cui Lara non reagisce quando urta un muro mentre guada.", }, "gameplay.fix_walk_run_jump": { "title": "Correggi cammina-corsa-salto", "description": "Risolve il problema per cui a volte Lara non riesce a saltare immediatamente dopo essere passata dall'animazione di camminata a quella di corsa.", }, "gameplay.fix_wall_geometry": { "title": "\\{review}Corregge la geometria dei muri", "description": "\\{review}Corregge i casi nella geometria dei livelli OG in cui inclinazioni all'interno dei muri possono portare a calcoli di altezza imprecisi.", }, "gameplay.fix_water_exit": { "title": "Correggi uscita dall'acqua", "description": "Risolve il problema che permette a Lara di passare direttamente da una stanza con acqua ad una senza acqua adiacente o sottostante. Inoltre, questo impedirà a Lara di uscire dall'acqua su pendii non calpestabili.", }, "gameplay.harpoon_recoil": { "title": "Ricarica fucile subacqueo", "description": "Imposta la frequenza con cui Lara deve ricaricare il fucile subacqueo, in base al numero attuale di munizioni. Ad esempio, se impostato a 3, dovrà ricaricare dopo ogni terzo colpo. Impostare a 0 per disabilitare completamente la ricarica.", }, "gameplay.idle_pose_timeout": { "title": "Tempo di inattività", "description": "Permette a Lara di riprodurre la sua animazione di posa dopo il numero di secondi d'inattività specificato. Impostalo a 0 per disabilitarlo.", }, "gameplay.jump_lock_mode": { "title": "Modalità blocco salto", "description": "Per salti più reattivi, consente di modificare quanto tempo deve attendere Lara per poter saltare dopo aver iniziato a correre.\n\n- Originale: corrisponde alla durata di tempo in TR2.\n- Modificato: è possibile effettuare il salto 2 fotogrammi prima.\n- Disattivato: è possibile effettuare il salto subito dopo l'inizio della corsa.", }, "gameplay.loading_screens": { "title": "Schermate di caricamento", "description": "Controlla il comportamento delle schermate di caricamento dei livelli.\n\n- Disattivato: non mostra mai le schermate di caricamento.\n- Sempre: mostra le schermate di caricamento.\n- Nuovo livello: non mostra le schermate di caricamento quando si carica un salvataggio.", }, "gameplay.look_mode": { "title": "Modalità osservazione", "description": "Permette di gestire in quali situazioni Lara può guardarsi attorno (tasto 'Guarda').\n\n- Limitato: è consentito esaminare l'ambiente solo quando Lara è ferma e mai quando è sott'acqua.\n- Avanzato: è consentito esaminare l'ambiente durante la maggior parte delle situazioni, tranne in alcune come quando si spingono i blocchi.\n- Illimitato: è sempre consentito esaminare l'ambiente durante il normale controllo di Lara.", }, "gameplay.maximum_quick_save_slots": { "title": "Slot di salvataggio rapido", "description": "Modifica il numero di slot di salvataggio rapido disponibili.", }, "gameplay.maximum_save_slots": { "title": "Slot di salvataggio", "description": "Modifica il numero di slot di salvataggio disponibili.", }, "gameplay.pause_on_focus_lost": { "title": "\\{review}Pausa quando si perde il focus", "description": "\\{review}Interrompe il progresso del gioco quando la finestra del gioco perde il focus.", }, "gameplay.projectile_area_damage": { "title": "Area danni proiettile", "description": "Controlla la propagazione dei danni del Lanciarazzi e del Lanciagranate.\n\n- Diffusione singola: comportamento di TR1 e TR2.\n- Diffusione multipla: comportamento di TR3.\n\nL'opzione \"Diffusione multipla\" spesso causa danni doppi ai singoli nemici.", }, "gameplay.remember_gun_status": { "title": "Ricorda armi tra livelli", "description": "Fa sì che Lara ricordi quale arma ha usato per ultima nel livello precedente quando inizia un nuovo livello. Se disabilitato, Lara tornerà ad avere nella fondina le sue pistole.", }, "gameplay.restore_ps1_enemies": { "title": "Ripristina nemici PS1", "description": "Aggiunge la mummia che appare nella versione PlayStation del livello 'Città di Khamoon', stanza 25.\nLa modifica di questa opzione richiederà il riavvio della partita.", }, "gameplay.start_lara_hitpoints": { "title": "Salute iniziale di Lara", "description": "Imposta il valore di salute di Lara all'inizio di ogni livello.", }, "gameplay.target_mode": { "title": "Modalità aggancio bersaglio", "description": "Modifica il comportamento di aggancio dei bersagli.\n\n- Completo: mantieni sempre il bersaglio agganciato anche se il nemico esce dal campo visivo o muore (TR1-3).\n- Parziale: mantieni il bersaglio agganciato anche se il nemico esce dal campo visivo, ma lo perdi se muore.\n- Nessuno: perdi il bersaglio agganciato se il nemico esce dal campo visivo o muore (TR4+).", }, "gameplay.wall_glitch_mode": { "title": "Modalità errore muro", "description": "Consente di utilizzare il comportamento dell'errore del muro di TR1 in TR2 e viceversa; consente inoltre di correggere tutti i tipi di errore del muro.", }, "input.enable_buffering_func_keys": { "title": "Buffering (tasti funzione)", "description": "Abilita il buffering dei tasti funzione (1 fotogramma) per ottenere un controllo preciso dei movimenti di Lara. Questa funzione esisteva originariamente solo nella versione TombATI (TR1).", }, "input.enable_buffering_inventory": { "title": "Buffering (inventario)", "description": "Abilita il buffering dell'inventario (2 fotogrammi) per ottenere un controllo preciso dei movimenti di Lara.", }, "input.enable_responsive_passport": { "title": "Passaporto reattivo", "description": "Fa in modo che sfogliare le pagine del passaporto sia più istantaneo, tenendo conto dei tasti premuti dell'utente", }, "input.enable_tr3_sidesteps": { "title": "Passi laterali migliorati", "description": "Abilita i passi laterali in stile TR3+, ad es. MAIUSC+frecce direzionali. I pulsanti dedicati per i passi laterali continueranno a funzionare.", }, "input.quick_guns_mode": { "title": "Equipaggiamento rapido armi", "description": "Controlla il comportamento dei tasti per l'equipaggiamento rapido delle armi.\n\n- Solo equipaggia: premendo il tasto, Lara equipaggerà l'arma assegnata.\n- Equipaggia o riponi: premendo il tasto, Lara equipaggerà l'arma assegnata. In aggiunta, con lo stesso tasto, la riporrà se la sta attualmente tenendo in mano.", }, "language": { "title": "Lingua", "description": "Cambia la lingua dell'interfaccia utente.", }, "rendering.anisotropy_filter": { "title": "Filtro anisotropico", "description": "Migliora il filtraggio delle texture a distanza.", }, "rendering.aspect_mode": { "title": "Rapporto d'aspetto", "description": "Forza determinati rapporti d'aspetto del gioco con barre nere ai bordi.", }, "rendering.borders": { "title": "Bordi", "description": "Aggiunge dei bordi neri attorno alla finestra del gioco.", }, "rendering.enable_trapezoid_filter": { "title": "Filtro trapezoidale", "description": "Corregge la renderizzazione dei quadrilateri.", }, "rendering.enable_vsync": { "title": "Sincronizzazione verticale", "description": "Attiva o disattiva la sincronizzazione verticale.", }, "rendering.fps": { "title": "FPS", "description": "Imposta i fotogrammi al secondo.", }, "rendering.lighting_contrast": { "title": "Contrasto", "description": "Aumenta il contrasto per fonti di luce dinamiche come razzi di segnalazione e lampi di armi da fuoco.", }, "rendering.screenshot_format": { "title": "Formato cattura schermo", "description": "Formato del file da utilizzare per le istantanee dello schermo.", }, "rendering.sprite_lock_mode": { "title": "Modalità blocco sprite", "description": "Controlla quali assi bloccare durante la visualizzazione degli sprite sullo schermo.\n\n- Nessuno: mostra gli sprite normalmente.\n- Rotazione: blocca l'asse di rotazione – utile solo in modalità foto.\n- Rotazione e inclinazione: fa in modo che gli sprite rimangano in posizione verticale e non si inclinino verso il suolo quando li si guarda dall'alto.\n- Prospettiva: blocca gli assi di rotazione e inclinazione e, inoltre, ruota leggermente gli sprite verso il centro dello schermo.", }, "rendering.texture_filter": { "title": "Filtro texture", "description": "Passa dalle texture di gioco filtrate a quelle pixelate e viceversa.", }, "rendering.ui_filter": { "title": "Filtro interfaccia", "description": "Passa dall'interfaccia di gioco filtrata a quella pixelata e viceversa.", }, "rendering.upscaling_factor": { "title": "Fattore di scala", "description": "Ridimensiona l'immagine di gioco in base a un fattore di scala definito, mantenendo un aspetto pixelato.", }, "rendering.upscaling_filter": { "title": "Filtro ridimensionamento", "description": "Alterna l'aspetto smussato o pixelato per l'intero schermo.", }, "ui.airbar_color": { "title": "Colore barra ossigeno", "description": "Colore utilizzato per la barra dell'ossigeno.", }, "ui.airbar_color_ps1": { "title": "Colore barra ossigeno", "description": "Colore utilizzato per la barra dell'ossigeno.", }, "ui.airbar_location": { "title": "Posizione barra ossigeno", "description": "Posizione in cui viene visualizzata la barra dell'ossigeno.", }, "ui.ammo_counter_location": { "title": "Posizione indicatore munizioni", "description": "Posizione in cui viene visualizzato l'indicatore delle munizioni.", }, "ui.bar_look": { "title": "Aspetto barre", "description": "Controlla l'aspetto visivo delle barre dell'interfaccia utente.", }, "ui.bar_scale": { "title": "Dimensione barre", "description": "Modifica le dimensioni delle barre dell'interfaccia utente.", }, "ui.enable_bar_flashing": { "title": "Barre lampeggianti", "description": "Fa in modo che le barre della salute e dell'ossigeno di Lara lampeggino quando stanno per esaurirsi.", }, "ui.enable_smooth_bars": { "title": "Barre sfumate", "description": "Fa in modo che la barra della salute e la barra dell'ossigeno utilizzino transizioni di colore uniformi.", }, "ui.enable_wraparound": { "title": "Scorrimento continuo", "description": "Consente la navigazione direzionale nei menu senza soluzione di continuità.", }, "ui.enemy_healthbar_color": { "title": "Colore barra nemici", "description": "Colore utilizzato per la barra della salute dei nemici.", }, "ui.enemy_healthbar_color_allies": { "title": "Colore barra alleati", "description": "Colore utilizzato per la barra della salute degli alleati. La barra viene mostrata nella stessa posizione della barra della salute dei nemici.", }, "ui.enemy_healthbar_color_allies_ps1": { "title": "Colore barra alleati", "description": "Colore utilizzato per la barra della salute degli alleati. La barra viene mostrata nella stessa posizione della barra della salute dei nemici.", }, "ui.enemy_healthbar_color_ps1": { "title": "Colore barra nemici", "description": "Colore utilizzato per la barra della salute dei nemici.", }, "ui.enemy_healthbar_location": { "title": "Posizione barra nemici", "description": "Posizione in cui viene visualizzata la barra della salute dei nemici.", }, "ui.enemy_healthbar_show_mode": { "title": "Modalità barra nemici", "description": "Abilita la visualizzazione della barra della salute per il nemico ingaggiato.", }, "ui.exposurebar_color": { "title": "Colore barra esposizione", "description": "Colore utilizzato per la barra di esposizione all'acqua fredda.", }, "ui.exposurebar_color_ps1": { "title": "Colore barra esposizione", "description": "Colore utilizzato per la barra di esposizione all'acqua fredda.", }, "ui.exposurebar_location": { "title": "Posizione barra esposizione", "description": "Posizione in cui viene visualizzata la barra di esposizione all'acqua fredda.", }, "ui.healthbar_color": { "title": "Colore barra salute", "description": "Colore utilizzato per la barra della salute.", }, "ui.healthbar_color_ps1": { "title": "Colore barra salute", "description": "Colore utilizzato per la barra della salute.", }, "ui.healthbar_location": { "title": "Posizione barra salute", "description": "Posizione in cui viene visualizzata la barra della salute.", }, "ui.healthbar_poison_color": { "title": "Colore barra veleno", "description": "Colore utilizzato per la barra della salute quando Lara è avvelenata.", }, "ui.healthbar_poison_color_ps1": { "title": "Colore barra veleno", "description": "Colore utilizzato per la barra della salute quando Lara è avvelenata.", }, "ui.inventory_background_style": { "title": "Sfondo inventario", "description": "Modifica il modo in cui viene visualizzato lo sfondo dell'inventario.\n\n- Scuro: TR1 (PC).\n- Molto scuro: TR1 (PS1).\n- Statico: TR2 (PC).\n- Ondeggiante: TR2 (PS1).\n- Monocromatico: TR3.", }, "ui.inventory_fade_effects": { "title": "Dissolvenza inventario", "description": "Abilita o disabilita l'effetto di dissolvenza nell'inventario in gioco. Per funzionare, è necessario che l'opzione \"Effetti di dissolvenza\" sia abilitata.", }, "ui.menu_style": { "title": "Stile menu", "description": "Modifica la modalità di visualizzazione dei menu.\n\n - PC: lo stile dell'interfaccia utente corrisponde alla versione PC.\n - PS1: lo stile dell'interfaccia utente corrisponde alla versione PS1.", }, "ui.pause_background_style": { "title": "Sfondo di pausa", "description": "Modifica il modo in cui viene visualizzato lo sfondo della schermata di pausa.\n\n- Scuro: TR1 (PC).\n- Molto scuro: TR1 (PS1).\n- Statico: TR2 (PC).\n- Ondeggiante: TR2 (PS1).\n- Monocromatico: TR3.", }, "ui.pause_fade_effects": { "title": "Dissolvenza in pausa", "description": "Abilita o disabilita l'effetto di dissolvenza nella schermata di pausa. Per funzionare, è necessario che l'opzione \"Effetti di dissolvenza\" sia abilitata.", }, "ui.pickup_scale": { "title": "Scala oggetti raccolti", "description": "Modifica la dimensione degli oggetti animati visualizzati nell'interfaccia utente quando Lara raccoglie qualcosa.", }, "ui.show_bars": { "title": "Mostra barre", "description": "Disabilita tutte le barre di gioco, nascondendo le informazioni sulla salute di Lara e altre risorse (per le sfide).", }, "ui.show_pickups_overlay": { "title": "Notifiche raccolta", "description": "Mostra gli oggetti nell'angolo in basso a destra quando Lara raccoglie qualcosa.", }, "ui.show_title_version": { "title": "Mostra versione", "description": "Mostra la versione di TRX nel menu principale.", }, "ui.sprintbar_color": { "title": "Colore barra resistenza", "description": "Colore della barra della resistenza visualizzata durante uno scatto.", }, "ui.sprintbar_color_ps1": { "title": "Colore barra resistenza", "description": "Colore della barra della resistenza visualizzata durante uno scatto.", }, "ui.sprintbar_location": { "title": "Posizione barra resistenza", "description": "Posizione in cui viene visualizzata la barra della resistenza.", }, "ui.stats.show_ammo": { "title": "Munizioni usate", "description": "Mostra le informazioni sulle munizioni usate e dei colpi a segno nelle statistiche del livello.", }, "ui.stats.show_crystals": { "title": "Cristalli", "description": "Mostra le informazioni sui cristalli usati o raccolti nelle statistiche del livello.", }, "ui.stats.show_deaths": { "title": "Morti", "description": "Mostra il conteggio delle morti di Lara nella bussola e nelle statistiche del livello. Il conteggio viene aggiornato nel salvataggio attualmente caricato non appena Lara muore.", }, "ui.stats.show_distance_travelled": { "title": "Distanza percorsa", "description": "Mostra le informazioni sulla distanza percorsa nelle statistiche del livello.", }, "ui.stats.show_kills": { "title": "Uccisioni", "description": "Mostra le informazioni sulle uccisioni nelle statistiche del livello.", }, "ui.stats.show_level_header": { "title": "Indicatore livello", "description": "Mostra il numero del livello attuale nella parte superiore delle statistiche del livello.", }, "ui.stats.show_medipacks_used": { "title": "Kit medici usati", "description": "Mostra le informazioni sui kit medici usati nelle statistiche del livello.", }, "ui.stats.show_pickups": { "title": "Oggetti raccolti", "description": "Mostra le informazioni sugli oggetti raccolti nelle statistiche del livello.", }, "ui.stats.show_secrets": { "title": "Segreti scoperti", "description": "Mostra le informazioni sui segreti scoperti nelle statistiche del livello.", }, "ui.stats.show_time_taken": { "title": "Tempo impiegato", "description": "Mostra il tempo impiegato nelle statistiche del livello.", }, "ui.stats.show_totals": { "title": "Mostra totali", "description": "Mostra i totali accanto alle statistiche, ove applicabile. I segreti non sono influenzati da questa impostazione.", }, "ui.stats.style": { "title": "Stile statistiche", "description": "Controlla come viene visualizzata la finestra delle statistiche.\n\n- Senza bordi: mostra le informazioni come un semplice elenco, senza bordi.\n- Con bordi: mostra le informazioni all'interno di una finestra.", }, "ui.stats_background_style": { "title": "Sfondo statistiche", "description": "Modifica il modo in cui viene visualizzato lo sfondo delle statistiche di fine livello.\n\n- Scuro: TR1 (PC).\n- Molto scuro: TR1 (PS1).\n- Statico: TR2 (PC).\n- Ondeggiante: TR2 (PS1).\n- Monocromatico: TR3.", }, "ui.stats_fade_effects": { "title": "Dissolvenza statistiche", "description": "Abilita o disabilita l'effetto di dissolvenza nella schermata delle statistiche di fine livello. Per funzionare, è necessario che l'opzione \"Effetti di dissolvenza\" sia abilitata.", }, "ui.text_scale": { "title": "Scala testo", "description": "Modifica la dimensione del testo dell'interfaccia utente.", }, "visuals.blood_effects": { "title": "\\{review}Effetti sangue", "description": "\\{review}Controlla i colori delle scintille di sangue.\n\n- Disabilitato: nessuna scintilla di sangue viene mostrata.\n- Rosa: il default nelle versioni tedesche per PC di TR3.\n- Rosso: il default in tutte le altre versioni retail.", }, "visuals.camera_mode": { "title": "Modalità telecamera", "description": "Regola il comportamento della telecamera durante azioni come l'uso degli oggetti chiave.", }, "visuals.enable_3d_pickups": { "title": "Oggetti 3D", "description": "Sostituisce gli oggetti recuperabili 2D con i rispettivi modelli 3D.", }, "visuals.enable_braid": { "title": "Treccia di Lara", "description": "Abilita la treccia di Lara.", }, "visuals.enable_breeze": { "title": "Brezza", "description": "Abilita l'effetto brezza sulla treccia di Lara nelle stanze appropriate.", }, "visuals.enable_exit_fade_effects": { "title": "Dissolvenza in uscita", "description": "Abilita l'effetto di dissolvenza quando si esce dal gioco.", }, "visuals.enable_fade_effects": { "title": "Effetti di dissolvenza", "description": "Abilita le transizioni in dissolvenza, ad esempio tra le immagini dei titoli di coda o tra le schermate di inventario e pausa.", }, "visuals.enable_fire_lighting": { "title": "Illuminazione fuoco", "description": "Abilita l'illuminazione dinamica accanto alle sorgenti di fuoco attive.", }, "visuals.enable_footprints": { "title": "Impronte", "description": "Abilita, nei livelli supportati, la visualizzazione delle impronte di Lara su alcune superfici.", }, "visuals.enable_glide_cameras": { "title": "Inquadratura fluida", "description": "Abilita lo spostamento fluido dell'inquadratura delle telecamere fisse che osservano Lara. Se disabilitato, tali telecamere sposteranno immediatamente l'inquadratura su Lara.", }, "visuals.enable_gun_lighting": { "title": "Illuminazione spari", "description": "Abilita l'illuminazione dinamica per spari ed esplosioni.", }, "visuals.enable_ps1_crystals": { "title": "Tinta cristalli PS1", "description": "I cristalli di salvataggio saranno di color viola, più simili a quelli della versione PS1.", }, "visuals.enable_reflections": { "title": "Riflessi", "description": "Abilita i riflessi su alcuni oggetti.", }, "visuals.enable_responsive_mesh_tint": { "title": "Colorazione accurata mesh", "description": "Consente di renderizzare con la tonalità dell'acqua solo le parti del corpo di Lara che sono immerse (come in TR3). Se disabilitato, quando Lara è in acqua, tutto il suo corpo verra renderizzato con la tonalità dell'acqua (come in TR1/2).", }, "visuals.enable_shotgun_flash": { "title": "Lampo sparo fucile", "description": "Mostra il lampo dello sparo quando Lara fa fuoco con il fucile a pompa, come per le altre armi.", }, "visuals.enable_skybox": { "title": "Cieli", "description": "Abilita il cielo nei livelli supportati.", }, "visuals.enable_weather": { "title": "Tempo meteorologico", "description": "Abilita, nei livelli supportati, la visualizzazione degli effetti atmosferici.", }, "visuals.fix_animated_sprites": { "title": "Correggi animazioni sprite", "description": "Corregge gli sprite originali delle piante idrofite in modo che si animino correttamente nelle aree acquatiche.", }, "visuals.fix_item_rots": { "title": "Correggi rotazione oggetti", "description": "Risolve i problemi relativi all'orientamento errato di alcuni oggetti quando viene utilizzata l'opzione Oggetti 3D nei livelli originali.", }, "visuals.fix_texture_issues": { "title": "Correggi problemi texture", "description": "Risolve i problemi riguardanti texture o maglie poligonali mancanti o errate nei livelli originali.", }, "visuals.fog_color": { "title": "Colore nebbia", "description": "Colore della nebbia.", }, "visuals.fog_end": { "title": "Fine nebbia", "description": "Imposta la distanza nelle piastrelle in cui la nebbia oscura completamente ogni cosa.", }, "visuals.fog_start": { "title": "Inizio nebbia", "description": "Imposta la distanza nelle piastrelle in cui inizia ad apparire la nebbia.", }, "visuals.fog_transparency": { "title": "Trasparenza nebbia", "description": "Abilita la fusione di geometrie distanti con superfici completamente trasparenti.", }, "visuals.fov": { "title": "Campo visivo", "description": "Angolo di visione in gradi. Valori più grandi ampliano il campo visivo, valori più piccoli lo restringono.", }, "visuals.game_brightness": { "title": "Luminosità", "description": "Modifica la luminosità del gioco.", }, "visuals.gamma": { "title": "Gamma", "description": "Regola la curva gamma. Valori più alti restituiscono una luminosità maggiore. Il valore 2,5 equivale al livello dei colori predefinito.", }, "visuals.lara_outfit": { "title": "Costume di Lara", "description": "Modifica l’aspetto di Lara. Se si seleziona \"Predefinito\", verranno rispettati i normali cambi di costume tra i livelli; in caso contrario, il costume scelto resterà attivo finché non verrà modificato manualmente.", }, "visuals.shadow_type": { "title": "Forma delle ombre", "description": "Seleziona la modalità di renderizzazione delle ombre dei personaggi.\n\n- Ottagono: ombre in stile TR1 e TR2\n- Cerchio: ombre rotonde\n- Sprite: ombre basate su texture come in TR3", }, "visuals.sunglasses_mode": { "title": "Occhiali di Lara", "description": "Modifica lo stile degli occhiali da sole di Lara. Nota: le lenti saranno riflettenti se l’opzione corrispondente è abilitata.\n\n- Disattivato: Lara non indosserà gli occhiali da sole.\n- Opaco: Gli occhiali da sole di Lara avranno lenti opache.\n- Trasparente: Gli occhiali da sole di Lara avranno lenti semi-trasparenti.", }, "visuals.ui_brightness": { "title": "Luminosità interfaccia", "description": "Modifica la luminosità dell'interfaccia.", }, "visuals.water_color": { "title": "Colore acqua", "description": "Colore dell'acqua.", } }, "objects": { "alarm_sound": { "name": "Allarme", }, "alligator": { "name": "Alligatore", }, "alphabet": { "name": "Carattere Predefinito", }, "alphabet_small": { "name": "Carattere Piccolo", }, "amber_light": { "name": "Luce Ambra", }, "animating_1": { "name": "Oggetto Animato 1", }, "animating_10": { "name": "\\{review}Oggetto Animato 10", }, "animating_2": { "name": "Oggetto Animato 2", }, "animating_3": { "name": "Oggetto Animato 3", }, "animating_4": { "name": "Oggetto Animato 4", }, "animating_5": { "name": "Oggetto Animato 5", }, "animating_6": { "name": "Oggetto Animato 6", }, "animating_7": { "name": "\\{review}Oggetto Animato 7", }, "animating_8": { "name": "\\{review}Oggetto Animato 8", }, "animating_9": { "name": "\\{review}Oggetto Animato 9", }, "ape": { "name": "Scimmia", }, "area_51_rocket": { "name": "Razzo Area 51", }, "area_51_rocket_blast": { "name": "Esplosione Razzo Area 51", }, "area_51_rocket_support": { "name": "Supporto Razzo Area 51", }, "assault_digits": { "name": "Cifre Corso d'Addestramento", }, "assault_target": { "name": "Obiettivo Corso di Addestramento", }, "atlantean_ground": { "name": "\\{review}Atlantideo Alato", }, "atlantean_shooter": { "name": "\\{review}Atlantideo (Che Spara)", }, "atlantean_winged": { "name": "\\{review}Atlantideo Terrestre", }, "autos": { "name": "Pistole Automatiche", }, "autos_ammo": { "name": "Caricatori per Pistole Automatiche", }, "bacon_lara": { "name": "Sosia di Lara", }, "baldy": { "name": "Il Calvo", }, "bandit_1": { "name": [ "Mercenario 1", "Scagnozzo Mascherato 1", ] }, "bandit_2": { "name": [ "Mercenario 2", "Scagnozzo Mascherato 2", ] }, "bandit_2b": { "name": [ "Mercenario 3", "Scagnozzo Mascherato 3", ] }, "barracuda": { "name": "Barracuda", }, "bartoli": { "name": "Marco Bartoli", }, "bat": { "name": "Pipistrello", }, "bat_emitter": { "name": "Emettitore di Pipistrelli", }, "beacon_light": { "name": "Luce Segnaletica", }, "bear": { "name": "Orso", }, "bell": { "name": "Campana", }, "big_bowl": { "name": "Ciotola di Lava", }, "big_eel": { "name": "Anguilla Grande", }, "big_pod": { "name": "Guscio Grande", }, "big_spider": { "name": "Ragno Gigante", }, "bird_guardian": { "name": "Uccello Guardiano", }, "bird_tweeter_1": { "name": "Gocce d'Acqua", }, "bird_tweeter_2": { "name": "Uccelli Canterini", }, "blade": { "name": "Lama a Parete", }, "blood": { "name": "\\{review}Sangue", }, "blood_pink": { "name": "\\{review}Sangue (censurato)", }, "blue_light": { "name": "Luce Blu", }, "boat": { "name": "Barca", }, "boat_bits": { "name": "Pezzi di Barca", }, "body_part": { "name": "Parte del Corpo", }, "bridge_flat": { "name": "Ponte Piano", }, "bridge_tilt_1": { "name": "Ponte Inclinato 1", }, "bridge_tilt_2": { "name": "Ponte Inclinato 2", }, "bubble_1": { "name": "Bolla 1", }, "bubble_2": { "name": "Bolla 2", }, "bubble_emitter": { "name": "Emettitore di Bolle", }, "camera_target": { "name": "Obiettivo Telecamera", }, "carcass": { "name": "Carcassa", }, "ceiling_spikes": { "name": "Soffitto con Spuntoni", }, "centaur": { "name": "Centauro", }, "centaur_statue": { "name": "Statua Centauro", }, "civilian": { "name": "Civile", }, "claw_mutant": { "name": "\\{review}Mutante Artigliato", }, "clock_chimes": { "name": "Orologio Covo di Bartoli", }, "cog_1": { "name": "Ingranaggio 1", }, "cog_2": { "name": "Ingranaggio 2", }, "cog_3": { "name": "Ingranaggio 3", }, "combat_end": { "name": "Fine Combattimento", }, "compass": { "name": "Bussola", }, "compy": { "name": "Compsognathus", }, "controls": { "name": "Comandi", }, "copter": { "name": "Elicottero", }, "cowboy": { "name": "Il Cowboy", }, "crawler_mutant": { "name": "\\{review}Mutante Strisciante", }, "crocodile": { "name": "Coccodrillo", }, "crow": { "name": "Corvo", }, "cult_1": { "name": "Scagnozzo Mascherato 1", }, "cult_1a": { "name": "Scagnozzo Mascherato 2", }, "cult_1b": { "name": "Scagnozzo Mascherato 3", }, "cult_2": { "name": "Lanciatore di Coltelli", }, "cult_3": { "name": "Scagnozzo con Fucile", }, "cut_shotgun": { "name": "Animazione Fucile nella Doccia", }, "damocles_sword": { "name": "Spada di Damocle", }, "dart": { "name": "Dardo", }, "dart_effect": { "name": "Effetto Dardo", }, "dart_emitter": { "name": "Emettitore di Dardi", }, "desert_eagle": { "name": "Desert Eagle", }, "desert_eagle_ammo": { "name": "Caricatori per Desert Eagle", }, "detonator_box": { "name": "Detonatore", }, "ding_dong": { "name": "Campanello", }, "dino_mutant": { "name": "Dinosauro Mutante", }, "disc": { "name": "Disco", }, "disc_emitter": { "name": "Emettitore di Dischi", }, "disposable_animating_1": { "name": "\\{review}Animazione Usa e Getta 1", }, "disposable_animating_10": { "name": "\\{review}Animazione Usa e Getta 10", }, "disposable_animating_2": { "name": "\\{review}Animazione Usa e Getta 2", }, "disposable_animating_3": { "name": "\\{review}Animazione Usa e Getta 3", }, "disposable_animating_4": { "name": "\\{review}Animazione Usa e Getta 4", }, "disposable_animating_5": { "name": "\\{review}Animazione Usa e Getta 5", }, "disposable_animating_6": { "name": "\\{review}Animazione Usa e Getta 6", }, "disposable_animating_7": { "name": "\\{review}Animazione Usa e Getta 7", }, "disposable_animating_8": { "name": "\\{review}Animazione Usa e Getta 8", }, "disposable_animating_9": { "name": "\\{review}Animazione Usa e Getta 9", }, "diver": { "name": "Subacqueo", }, "dog": { "name": [ "Cane", "Dobermann", ] }, "door_1": { "name": "Porta 1", }, "door_2": { "name": "Porta 2", }, "door_3": { "name": "Porta 3", }, "door_4": { "name": "Porta 4", }, "door_5": { "name": "Porta 5", }, "door_6": { "name": "Porta 6", }, "door_7": { "name": "Porta 7", }, "door_8": { "name": "Porta 8", }, "dragon_back": { "name": "Retro Drago", }, "dragon_bones_1": { "name": "Segnaposto", }, "dragon_bones_2": { "name": "Fronte Ossa del Drago", }, "dragon_bones_3": { "name": "Retro Ossa del Drago", }, "dragon_front": { "name": "Fronte Drago", }, "drawbridge": { "name": "Ponte Levatoio", }, "dust": { "name": "Polvere", }, "dying_monk": { "name": "Monaco Morente", }, "dying_mutant": { "name": "\\{review}Mutante Morente", }, "eagle": { "name": "Aquila", }, "earthquake": { "name": "Terremoto", }, "eel": { "name": "Anguilla", }, "electric_cleaner": { "name": "Pulitore Elettrico", }, "electric_fence": { "name": "Recinzione Elettrica", }, "electrical_light": { "name": "Luce Elettrica", }, "ember": { "name": "Brace", }, "ember_emitter": { "name": "Emettitore di Brace", }, "explosion_1": { "name": "Esplosione 1", }, "explosion_2": { "name": "Esplosione 2", }, "falling_block_1": { "name": [ "Blocco Cedevole 1", "Pavimento Fragile 1", "Piastrelle Fragili 1", ] }, "falling_block_2": { "name": [ "Blocco Cedevole 2", "Pavimento Fragile 2", "Piastrelle Fragili 2", ] }, "falling_block_3": { "name": [ "Blocco Cedevole 3", "Pavimento Fragile 3", "Piastrelle Fragili 3", "Tavole Allentate", ] }, "falling_ceiling_1": { "name": "Soffitto Cedevole 1", }, "falling_ceiling_2": { "name": "Soffitto Cedevole 2", }, "fire_head": { "name": "\\{review}Testa di Fuoco", }, "fish_mutant": { "name": "Pesce Mutante", }, "flame": { "name": [ "Fiamma", "Fuoco", ] }, "flame_emitter": { "name": [ "Emettitore di Fiamme", "Emettitore di Fuoco", ] }, "flame_emitter_big": { "name": [ "Emettitore di Fiamme (Grande)", "Emettitore di Fuoco (Grande)", ] }, "flame_emitter_jet": { "name": [ "Emettitore di Fiamme (Getto)", "Emettitore di Fuoco (Getto)", ] }, "flame_emitter_side": { "name": [ "Emettitore di Fiamme (Laterale)", "Emettitore di Fuoco (Laterale)", ] }, "flame_emitter_small": { "name": [ "Emettitore di Fiamme (Piccolo)", "Emettitore di Fuoco (Piccolo)", ] }, "flare": { "name": "Razzo di Segnalazione", }, "flare_fire": { "name": "Scintille Fiamma", }, "flares_box": { "name": "Scatola Razzi", }, "flickering_light": { "name": "\\{review}Luce Tremolante", }, "fuse_box": { "name": "Scatola Fusibili", }, "fx_reserved": { "name": "Disco Grigio", }, "gamma": { "name": "Gamma", }, "gas_emitter_green": { "name": "Emettitore di Gas (Verde)", }, "general": { "name": "Minisommergibile", }, "globe": { "name": "Globo", }, "glow": { "name": "Bagliore", }, "glow_reserved": { "name": "Bagliore Mappa", }, "gondola": { "name": "Gondola", }, "gong": { "name": "Gong", }, "gong_bonger": { "name": "Martello del Gong", }, "graphics": { "name": "Grafica", }, "green_light": { "name": "Luce Verde", }, "grenade": { "name": "Granata", }, "grenade_launcher": { "name": "Lanciagranate", }, "grenade_launcher_ammo": { "name": "Granate", }, "gun_flash": { "name": "Lampo Pistola", }, "gun_shell": { "name": "Bossolo di Proiettile", }, "harpoon_bolt": { "name": "Dardo Arpione", }, "harpoon_gun": { "name": "Fucile Subacqueo", }, "harpoon_gun_ammo": { "name": "Arpioni", }, "hook": { "name": "Gancio", }, "hot_liquid": { "name": "Fuoco Extra", }, "huskie": { "name": [ "Cane", "Cane da Pattuglia", "Husky", ] }, "hybrid_mutant": { "name": "\\{review}Mutante Ibrido", }, "icicle": { "name": "Ghiaccioli", }, "inv_background": { "name": "Sfondo Menu", }, "jelly": { "name": "Medusa", }, "kayak": { "name": "Kayak", }, "key_1": { "name": "Chiave 1", }, "key_2": { "name": "Chiave 2", }, "key_3": { "name": "Chiave 3", }, "key_4": { "name": "Chiave 4", }, "key_hole_1": { "name": "Serratura 1", }, "key_hole_2": { "name": "Serratura 2", }, "key_hole_3": { "name": "Serratura 3", }, "key_hole_4": { "name": "Serratura 4", }, "kill_all_triggered": { "name": "Eliminazione totale attivata", }, "killer_statue": { "name": "Statua con Spada", }, "lara": { "name": "Lara", }, "lara_alarm": { "name": "Campanello d'Allarme", }, "lara_autos": { "name": "Animazione Pistole Automatiche", }, "lara_boat": { "name": "Animazione Barca", }, "lara_desert_eagle": { "name": "Animazione Desert Eagle", }, "lara_extra": { "name": "Animazione Aggiuntiva di Lara", }, "lara_flare": { "name": "Animazione Razzo di Segnalazione", }, "lara_grenade": { "name": "Animazione Lanciagranate", }, "lara_hair": { "name": "Treccia di Lara", }, "lara_harpoon": { "name": "Animazione Fucile Subacqueo", }, "lara_m16": { "name": "Animazione M16", }, "lara_magnums": { "name": "Animazione Magnum", }, "lara_mp5": { "name": "Animazione MP5", }, "lara_pistols": { "name": "Animazione Pistole", }, "lara_rocket": { "name": "Animazione Lanciarazzi", }, "lara_shotgun": { "name": "Animazione Fucile a Pompa", }, "lara_skidoo": { "name": "Animazione Motoslitta", }, "lara_uzis": { "name": "Animazione Uzi", }, "large_medipack": { "name": "Kit Medico Grande", }, "larson": { "name": "Larson", }, "lava_wedge": { "name": "Cascata di Lava", }, "lead_bar": { "name": "Barra di Piombo", }, "lift": { "name": "Ascensore", }, "lightning_emitter": { "name": "Emettitore di Fulmini", }, "lion": { "name": "Leone", }, "lioness": { "name": [ "Leonessa", "Leone", ] }, "lizard": { "name": "Lucertola", }, "m16": { "name": "M16", }, "m16_ammo": { "name": "Caricatori per M16", }, "m16_flash": { "name": "Lampo M16", }, "magnums": { "name": "Magnum", }, "magnums_ammo": { "name": "Caricatori per Magnum", }, "mesh_swap_1": { "name": "Cambio Costume 1", }, "mesh_swap_2": { "name": "Cambio Costume 2", }, "mesh_swap_3": { "name": "Cambio Costume 3", }, "midas_touch": { "name": "Mano di Mida", }, "mine": { "name": "Miniera Acquatica", }, "mine_cart": { "name": "\\{review}Vagoncino da miniera", }, "mini_copter": { "name": "Elicottero 2", }, "missile_atlantean_bomb": { "name": "Missile (Bomba atlantidea)", }, "missile_atlantean_shard": { "name": "Missile (Scheggia atlantidea)", }, "missile_flame": { "name": "Missile (Fiamma)", }, "missile_harpoon": { "name": "Missile (Arpione)", }, "missile_knife": { "name": "Missile (Coltello)", }, "missile_poison": { "name": "Missile (Veleno)", }, "monk_1": { "name": "Monaco 1", }, "monk_2": { "name": "Monaco 2", }, "monkey": { "name": "Scimmia", }, "mounted_gun": { "name": "Mitragliatrice Fissa", }, "mouse": { "name": "Ratto", }, "movable_block_1": { "name": [ "Blocco Spostabile 1", "Blocco Movibile 1", ] }, "movable_block_2": { "name": [ "Blocco Spostabile 2", "Blocco Movibile 2", ] }, "movable_block_3": { "name": [ "Blocco Spostabile 3", "Blocco Movibile 3", ] }, "movable_block_4": { "name": [ "Blocco Spostabile 4", "Blocco Movibile 4", ] }, "moving_bar": { "name": "Barra Mobile", }, "mp5": { "name": "MP5", }, "mp5_ammo": { "name": "Caricatori per MP5", }, "mp_1": { "name": "MP 1", }, "mp_2": { "name": "MP 2", }, "mummy": { "name": "Mummia", }, "natla": { "name": "Natla", }, "natla_gun": { "name": "Arma di Natla", }, "on_off_light": { "name": "Luce Accesa/Spenta", }, "orca": { "name": "Orca", }, "passport": { "name": "Partita", }, "patrol_dog": { "name": [ "Cane", "Cane da Pattuglia", ] }, "pda": { "name": "Opzioni di Gioco", }, "pendulum_1": { "name": [ "Pendolo", "Sacco di Sabbia", "Scatola Oscillante", ] }, "pendulum_2": { "name": [ "Pendolo", "Sacco di Sabbia", "Scatola Oscillante", ] }, "photo": { "name": "Casa di Lara", }, "pickup_1": { "name": "Oggetto Recuperabile 1", }, "pickup_2": { "name": "Oggetto Recuperabile 2", }, "pickup_aid": { "name": "Segnalazione Oggetti", }, "pierre": { "name": "Pierre", }, "pirahnas": { "name": "Piranha", }, "pistols": { "name": "Pistole", }, "pistols_ammo": { "name": "Caricatori per Pistole", }, "player_1": { "name": "Attore Intermezzo 1", }, "player_10": { "name": "Attore Intermezzo 10", }, "player_2": { "name": "Attore Intermezzo 2", }, "player_3": { "name": "Attore Intermezzo 3", }, "player_4": { "name": "Attore Intermezzo 4", }, "player_5": { "name": "Attore Intermezzo 5", }, "player_6": { "name": "Attore Intermezzo 6", }, "player_7": { "name": "Attore Intermezzo 7", }, "player_8": { "name": "Attore Intermezzo 8", }, "player_9": { "name": "Attore Intermezzo 9", }, "pods": { "name": "Guscio", }, "poison_dart": { "name": "Dardo Avvelenato", }, "poison_dart_emitter": { "name": "Emettitore di Dardi Avvelenati", }, "portacabin": { "name": "Cabina Portatile", }, "power_saw": { "name": "Sega Elettrica", }, "prisoner": { "name": "Prigioniero", }, "propeller_1": { "name": "Elica Aeroplano", }, "propeller_2": { "name": "Elica Sott'acqua", }, "propeller_3": { "name": "Ventilatore d'Aria", }, "pulse_light": { "name": "Luce a Impulsi", }, "puma": { "name": "Puma", }, "punk_1": { "name": "Punk 1", }, "punk_2": { "name": "Punk 2", }, "puzzle_1": { "name": "Oggetto Enigma 1", }, "puzzle_2": { "name": "Oggetto Enigma 2", }, "puzzle_3": { "name": "Oggetto Enigma 3", }, "puzzle_4": { "name": "Oggetto Enigma 4", }, "puzzle_done_1": { "name": "Serratura Enigma 1 (Usato)", }, "puzzle_done_2": { "name": "Serratura Enigma 2 (Usato)", }, "puzzle_done_3": { "name": "Serratura Enigma 3 (Usato)", }, "puzzle_done_4": { "name": "Serratura Enigma 4 (Usato)", }, "puzzle_hole_1": { "name": "Serratura Enigma 1 (Vuoto)", }, "puzzle_hole_2": { "name": "Serratura Enigma 2 (Vuoto)", }, "puzzle_hole_3": { "name": "Serratura Enigma 3 (Vuoto)", }, "puzzle_hole_4": { "name": "Serratura Enigma 4 (Vuoto)", }, "quad_bike": { "name": "Quad", }, "quest_1": { "name": "Oggetto Missione 1", }, "quest_2": { "name": "Oggetto Missione 2", }, "quest_3": { "name": "Oggetto Missione 3", }, "quest_4": { "name": "Oggetto Missione 4", }, "raptor": { "name": "Velociraptor", }, "raptor_emitter": { "name": "Emettitore di Velociraptor", }, "rat": { "name": [ "Ratto", "Ratto Terrestre", ] }, "red_light": { "name": "Luce Rossa", }, "rib": { "name": "\\{review}RIB", }, "ricochet": { "name": "Rimbalzo", }, "rocket": { "name": "Razzo", }, "rocket_launcher": { "name": "Lanciarazzi", }, "rocket_launcher_ammo": { "name": "Razzi", }, "rolling_ball_1": { "name": [ "Masso 1", "Masso Rotolante 1", ] }, "rolling_ball_2": { "name": [ "Masso 2", "Masso Rotolante 2", ] }, "rolling_ball_3": { "name": [ "Masso 3", "Masso Rotolante 3", ] }, "rolling_ball_4": { "name": [ "Masso 4", "Masso Rotolante 4", ] }, "rotating_laser": { "name": "Laser rotante", }, "rx_worker_1": { "name": "\\{review}RX Operaio 1", }, "rx_worker_2": { "name": "\\{review}RX Operaio 2", }, "rx_worker_3": { "name": "\\{review}RX Operaio 3", }, "save_crystal": { "name": "Cristallo di Salvataggio", }, "scion": { "name": "Scion", }, "scion_holder": { "name": "Supporto Scion", }, "secret_1": { "name": "Segreto 1", }, "secret_2": { "name": "Segreto 2", }, "secret_3": { "name": "Segreto 3", }, "security_guard": { "name": "Guardia di Sicurezza", }, "security_laser_alarm": { "name": "Laser di Sicurezza (Allarme)", }, "security_laser_deadly": { "name": "Laser di Sicurezza (Lesivo)", }, "security_laser_killer": { "name": "Laser di Sicurezza (Letale)", }, "sentry_gun": { "name": "Torretta di Guardia", }, "shadow": { "name": "Ombra", }, "shark": { "name": "Squalo", }, "shiva": { "name": "Shiva", }, "shotgun": { "name": "Fucile a Pompa", }, "shotgun_ammo": { "name": "Cartucce per Fucile a Pompa", }, "shotgun_shell": { "name": "Bossolo di Cartuccia", }, "skate_kid": { "name": "Skate Kid", }, "skateboard": { "name": "Skateboard", }, "skidoo_armed": { "name": "Motoslitta Nera", }, "skidoo_driver": { "name": "Pilota Motoslitta Nera", }, "skidoo_fast": { "name": "Motoslitta Rossa", }, "skidoo_track": { "name": "Tracciato Motoslitta", }, "skybox": { "name": "Cielo", }, "sliding_pillar": { "name": "Pilastro Scorrevole", }, "small_medipack": { "name": "Kit Medico Piccolo", }, "smashable_1": { "name": "Finestra Frangibile 1", }, "smashable_2": { "name": "Finestra Frangibile 2", }, "smashable_3": { "name": "Finestra Frangibile 3", }, "smashable_4": { "name": "Finestra Frangibile 4", }, "smoke_emitter_black": { "name": "Emettitore di Fumo (Nero)", }, "smoke_emitter_white": { "name": "Emettitore di Fumo (Bianco)", }, "snake": { "name": "Serpente", }, "snow_sprite": { "name": "Scia Motoslitta", }, "sophia": { "name": "Sophia", }, "sound": { "name": "Suono", }, "sphere_of_doom_1": { "name": "Esplosione Drago 1", }, "sphere_of_doom_2": { "name": "Esplosione Drago 2", }, "sphere_of_doom_3": { "name": "Esplosione Drago 3", }, "spider": { "name": "Ragno", }, "spike_wall": { "name": "Muro con Spuntoni", }, "spikes": { "name": "Spuntoni", }, "spinning_blade": { "name": "Lama Rotante", }, "splash_1": { "name": "Increspatura Acqua 1", }, "splash_2": { "name": "Increspatura Acqua 2", }, "springboard": { "name": "Trampolino", }, "steam_emitter": { "name": "Emettitore di Vapore", }, "sthpac_mercenary": { "name": "Mercenario del Sud Pacifico", }, "stopwatch": { "name": "Statistiche", }, "strobe_light": { "name": "Luce Stroboscopica", }, "swat_1": { "name": "SWAT 1", }, "swat_2": { "name": "SWAT 2", }, "swat_3": { "name": "SWAT 3", }, "swinging_axe": { "name": "Ascia Oscillante", }, "switch_type_airlock": { "name": "Interruttore Camera Stagna", }, "switch_type_button": { "name": [ "Pulsante", "Pulsante a Pressione", "Interruttore", ] }, "switch_type_normal": { "name": [ "Leva", "Interruttore", ] }, "switch_type_small": { "name": "Interruttore Piccolo", }, "switch_type_uw": { "name": [ "Leva Subacquea", "Interruttore Subacqueo", ] }, "switch_type_wheel": { "name": [ "Interruttore Rotante", "Interruttore a Puleggia", "Interruttore a Valvola", ] }, "teeth_trap": { "name": [ "Trappola con Spuntoni", "Porta che sbatte", ] }, "text_box": { "name": "Casella Interfaccia", }, "thors_handle": { "name": "Manico del Martello di Thor", }, "thors_head": { "name": "Martello di Thor", }, "tiger": { "name": "Tigre", }, "tony": { "name": "Tony", }, "torso": { "name": [ "Torso", "Adam", "Mutante Gigante", ] }, "train": { "name": "Treno", }, "trapdoor_1": { "name": "Botola 1", }, "trapdoor_2": { "name": "Botola 2", }, "trapdoor_3": { "name": "Botola 3", }, "trex": { "name": "T-Rex", }, "trex_alpha": { "name": "T-Rex Alpha", }, "tribe_axeman": { "name": "Indigeno con Ascia", }, "tribe_boss": { "name": "Capo Tribù", }, "tribe_pipeman": { "name": "Indigeno con Cerbottana", }, "tropical_fish": { "name": "Pesci Tropicali", }, "twinkle": { "name": "Scintille", }, "upv": { "name": "Mini Sottomarino", }, "uzis": { "name": "Uzi", }, "uzis_ammo": { "name": "Caricatori per Uzi", }, "vole": { "name": [ "Arvicola", "Ratto Acquatico", ] }, "vulture": { "name": "Avvoltoio", }, "wasp_mutant": { "name": "\\{review}Mutante Vespa", }, "wasp_mutant_emitter": { "name": "\\{review}Emettitore di Mutante Vespa", }, "water_sprite": { "name": "Scia Barca", }, "waterfall": { "name": "Foschia Cascata", }, "white_light": { "name": "Luce Bianca", }, "willard": { "name": "\\{review}Willard", }, "winston": { "name": "Winston", }, "winston_army": { "name": "Winston (in uniforme)", }, "wolf": { "name": "Lupo", }, "worker_1": { "name": "Scagnozzo Pistolero 1", }, "worker_2": { "name": "Scagnozzo Pistolero 2", }, "worker_3": { "name": "Scagnozzo con Bastone 1", }, "worker_4": { "name": "Scagnozzo con Bastone 2", }, "worker_5": { "name": "Scagnozzo con Lanciafiamme", }, "xian_knight": { "name": "Cavaliere di Xian", }, "xian_knight_statue": { "name": "Statua Cavaliere di Xian", }, "xian_spearman": { "name": "Lanciere di Xian", }, "xian_spearman_statue": { "name": "Statua Lanciere di Xian", }, "yeti": { "name": "Yeti", }, "zipline_handle": { "name": "Maniglia Teleferica", } } } ================================================ FILE: data/trx/ship/cfg/base_strings-pl.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "language_name": "Polski", "general": { "actions": { "examine_item": "Zbadaj", "hide_dialog": "Ukryj dialog", "reset_defaults": "Zresetuj układ", "rotate": "Obróć", "unbind": "Usuń przypisanie", "use_item": "Użyj", }, "config_presets": { "applied": "\\{review}Ustawienie wstępne zastosowane.", "confirm_description": "\\{review}Następujące ustawienia zostaną zmienione:", "confirm_restart_note": "\\{review}Uwaga: niektóre ustawienia mogą wymagać ponownego uruchomienia gry, aby zmiany weszły w życie.", "empty": "\\{review}Nie znaleziono presetów.", "no_changes": "\\{review}Brak zmian do zastosowania.", "title_fmt": "\\{review}Zastosować ustawienie wstępne %s?", }, "globe_select": { "area_1": "Obszar 1", "area_2": "Obszar 2", "area_3": "Obszar 3", "area_4": "Obszar 4", "area_5": "Obszar 5", "area_6": "Obszar 6", }, "inventory_ring": { "heading_adventure": "Przygoda", "heading_fmt": "%s", "heading_game_over": "KONIEC GRY", "heading_inventory": "EKWIPUNEK", "heading_items": "PRZEDMIOTY", "heading_option": "OPCJE", "item_count_fmt": "\\{small}%s", "object_name_fmt": "%s", }, "misc": { "demo_mode": "Tryb demo", "direction_keys_controller": "D-Pad", "direction_keys_keyboard": "Strzałki", "empty_slot_fmt": "- PUSTY SLOT -", "exit": "Wyjście", "hold_fmt": "%s", "off": "Wyłączone", "on": "Włączone", "pagination_nav": "%d / %d", }, "osd": { "ambiguous_input_2": "Niejednoznaczne wywołanie: %s i %s", "ambiguous_input_3": "Niejednoznaczne wywołanie: %s, %s, ...", "bilinear_filter_off": "Filtr tekstur: punktowy", "bilinear_filter_on": "Filtr tekstur: dwuliniowy", "command_bad_invocation": "Nieprawidłowe wywołanie: %s", "command_bool": "1, 0", "command_decimal": "[liczba ułamkowa]", "command_integer": "[liczba całkowita]", "command_percent": "[procent]", "command_unavailable": "Ta komenda nie jest obecnie dostępna", "command_valid_values": "Dopuszczalne wartości: %s", "complete_level": "Poziom ukończony!", "config_option_get": "%s jest obecnie ustawione na %s", "config_option_set": "Zmieniono %s na %s", "config_option_unknown_option": "Nieznana opcja: %s", "current_health_get": "Aktualne zdrowie Lary: %d", "current_health_set": "Zdrowie Lary ustawione na %d", "door_close": "Sezamie, zamknij się!", "door_open": "Sezamie, otwórz się!", "door_open_fail": "Brak drzwi w pobliżu Lary", "flipmap_fail_already_off": "Flipmapa jest już wyłączona", "flipmap_fail_already_on": "Flipmapa jest już włączona", "flipmap_off": "Flipmapa wyłączona", "flipmap_on": "Flipmapa włączona", "fly_mode_off": "Tryb latania wyłączony", "fly_mode_on": "Tryb latania włączony", "fps_counter_off": "Licznik klatek na sekundę wyłączony", "fps_counter_on": "Licznik klatek na sekundę włączony", "give_item": "Dodano %s do ekwipunku Lary", "give_item_all_guns": "Lara uzbrojona po zęby. Drżyj, świecie!", "give_item_all_keys": "Niespodzianka! Wszystkie kluczowe przedmioty zmaterializowały się w plecaku Lary.", "give_item_cheat": "Plecak Lary stał się znacznie cięższy!", "heal_already_full_hp": "Lara jest już w pełni zdrowa", "heal_success": "Lara uleczona do pełni zdrowia", "invalid_cutscene": "Nieprawidłowy przerywnik", "invalid_demo": "Nieprawidłowe demo", "invalid_item": "Nieznany przedmiot: %s", "invalid_level": "Nieznany poziom", "invalid_object": "Nieznany obiekt", "invalid_room": "Nieprawidłowe pomieszczenie: %d. Prawidłowe pomieszczenia to 0-%d", "invalid_sample": "Nieprawidłowy dźwięk: %d", "kill": "I po krzyku!", "kill_all": "Sayonara!", "kill_all_fail": "Uch, już nic nie zostało do zabicia...", "kill_fail": "Brak wrogów w pobliżu", "lighting_contrast_fmt": "Kontrast oświetlenia: %s", "load_game": "Wczytano grę ze slotu %d", "load_game_fail_invalid_slot": "Nieprawidłowy slot zapisu %d", "load_game_fail_unavailable_slot": "Slot zapisu %d jest niedostępny", "object_not_found": "Nie odnaleziono tego obiektu", "play_cutscene": "Wczytano przerywnik %d", "play_demo": "Wczytano demo %d", "play_level": "Wczytano %s", "pos_lara_missing": "Nie znaleziono Lary", "pos_lara_pos_fmt": "Pomieszczenie: %d\nPozycja: %.3f, %.3f, %.3f\nRotacja: %.3f, %.3f, %.3f", "pos_level_fmt": "Poziom %d", "pos_level_fmt_cutscene": "Przerywnik %d", "pos_level_fmt_demo": "Demo %d", "quick_load": "Wczytano szybki zapis %d", "quick_load_fail_no_bound_slot": "Obecnie nie przypisano żadnego zapisu", "quick_load_fail_unavailable_bound_slot": "Szybkie wczytywanie nie jest dostępne", "quick_save": "Szybki zapis", "quick_save_fail_no_slots": "Nie skonfigurowano slotów szybkiego zapisu", "save_game": "Zapisano grę na slocie zapisu %d", "save_game_fail_invalid_slot": "Nieprawidłowy slot zapisu %d", "sound_available_samples": "Dostępne dźwięki: %s", "sound_playing_sample": "Odtwarzanie dźwięku %d", "speed_get": "Aktualna prędkość: %d", "speed_set": "Prędkość ustawiona na %d", "strings_failed": "Nie udało się przeładować plików językowych", "strings_reloaded": "Pliki językowe zostały przeładowane", "textures_off": "Tekstury: wyłączone", "textures_on": "Tekstury: włączone", "trapezoid_filter_off": "Filtr trapezoidalny wyłączony", "trapezoid_filter_on": "Filtr trapezoidalny włączony", "ui_off": "Interfejs użytkownika wyłączony", "ui_on": "Interfejs użytkownika włączony", "unknown_command": "Nieznana komenda: %s", "upscaling_factor": "Współczynnik skalowania: x%d", "wireframe_mode_off": "Widok siatki: wyłączony", "wireframe_mode_on": "Widok siatki: włączony", }, "overlay": { "debug_animation": "Animacja: ", "debug_animation_state": "Stan: ", "debug_camera_pos": "Pozycja kamery: ", "debug_camera_target": "Cel kamery: ", "debug_immune": "Nieśmiertelność", "debug_position": "Pozycja: ", "debug_rotation": "Rotacja: ", "debug_speed": "Prędkość: ", "item_count_fmt_pc": "\\{small}%s", "item_count_fmt_ps1": "\\{small}%s", }, "passport": { "delete_save": "\\{review}Usuń", "delete_save_confirm": "\\{review}Usunąć ten zapis?", "delete_save_failed": "\\{review}Nie udało się usunąć wybranego zapisu.", "delete_save_no": "\\{review}Nie", "delete_save_yes": "\\{review}Tak", "exit_game": "Wyjdź z gry", "exit_to_title": "Menu główne", "load_game": "Wczytaj grę", "mode_new_game": "Nowa gra", "mode_new_game_jp": "Japońska wersja", "mode_new_game_jp_plus": "Japońska wersja, NG+", "mode_new_game_plus": "Nowa gra+", "new_game": "Nowa gra", "play_previous_levels": "Zagraj w poprzednie poziomy", "restart_level": "Restartuj poziom", "save_game": "Zapisz grę", "save_slot_unsupported": "Ten zapis nie obsługuje tej funkcji.", "select_level": "Wybierz poziom", "select_mod": "\\{review}Wybierz grę", "select_mode": "Wybierz tryb", "select_save": "Wybierz zapis", "story_so_far": "Co się wydarzyło do tej pory", "switch_mod": "\\{review}Zmień grę", }, "pause": { "are_you_sure": "Czy na pewno?", "continue": "Nie, kontynuuj grę", "exit_to_title": "Wyjść do menu głównego?", "no": "Nie", "paused": "Pauza", "quit": "Tak, wyjdź", "yes": "Tak", }, "photo_mode": { "advance_frame": "Pomiń klatkę", "camera_move_prompt": "Przesuń kamerę", "camera_reset_prompt": "Zresetuj kamerę", "camera_roll_prompt": "Obróć kamerę w pionie", "camera_rotate_90_prompt": "Obróć kamerę o 90°", "camera_rotate_prompt": "Obróć kamerę w poziomie", "change_lara_pose": "Zmień pozę", "fov_prompt": "Dostosuj kąt widzenia", "lara_move_prompt": "Przesuń Larę", "lara_reset_prompt": "Zresetuj Larę", "lara_roll_prompt": "Obróć Larę w pionie", "lara_rotate_90_prompt": "Obróć Larę o 90°", "lara_rotate_prompt": "Obróć Larę w poziomie", "snap_prompt": "Zrób zdjęcie", "title_camera_pos": "Tryb zdjęć", "title_lara_pos": "Przesuń Larę", "toggle_help": "Pokaż/ukryj pomoc", }, "settings": { "common": { "all_hidden_disclaimer": "Ustawienia są wyłączone dla tego zestawu poziomów.", "chroma": "Chroma", "edit_value": "Edytuj wartość", "frozen_option_disclaimer": "To ustawienie jest wymuszane przez twórcę poziomu i nie może być zmienione.", "hue": "Odcień", "lightness": "Jasność", "restore_default": "Przywróć domyślne", "toggle_help": "Pokaż pomoc", }, "controls": { "backend": { "controller": "Kontroler", "keyboard": "Klawiatura", }, "customize": "Dostosuj sterowanie", "layout": { "custom_1": "Ustawienia użytkownika 1", "custom_2": "Ustawienia użytkownika 2", "custom_3": "Ustawienia użytkownika 3", "default": "Domyślne ustawienia", }, "tabs": { "basics": "Ruch", "items": "Przedmioty", "misc": "Różne", "system": "System", } }, "gameplay": { "tabs": { "controls": "Sterowanie", "fixes": "Poprawki", "general": "Ogólne", "mods": "Mody", "presets": "\\{review}Presety", }, "title": "Ustawienia gry", }, "graphic_settings": { "tabs": { "bars": "Paski", "rendering": "Obraz", "stats": "\\{review}Statystyki", "ui": "Interfejs", "visuals": "Wygląd", }, "title": "Ustawienia grafiki", }, "sound": { "tabs": { "misc": "Różne", "volume": "Głośność", }, "title": "Ustawienia dźwięku", } }, "stats": { "ammo": "Amunicja/trafienia", "ammo_hits": "Trafienia", "ammo_used": "Zużyta amunicja", "assault_best_time_fmt": "%s", "assault_finish": "Finisz", "assault_no_times_set": "Brak czasów", "assault_other_times_fmt": "%s", "assault_title": "NAJLEPSZE CZASY", "basic_fmt": "%d", "bonus_statistics": "Statystyki bonusowe", "crystals": "\\{review}Kryształy", "deaths": "Śmierci", "detail_fmt": "%d z %d", "distance_travelled": "Przebyta odległość", "final_statistics": "Statystyki końcowe", "gym_assault_course": "Tor Przeszkód", "gym_racetrack_course": "Tor Wyścigowy", "kills": "Zabójstwa", "level": "Poziom", "medipacks_used": "Zużyte apteczki", "none": "Brak", "pickups": "Przedmioty", "secrets": "Odnalezione sekrety", "time_taken": "Spędzony czas", } }, "console": { "cmd": { "braid": { "help": "Włącza lub wyłącza warkocz Lary.", }, "cheats": { "help": "Włącza lub wyłącza cheaty.", }, "clear": { "help": "Czyści logi konsoli.", }, "debug": { "help": "Włącza lub wyłącza różne informacje diagnostyczne.", }, "drain": { "help": "Osusza aktualne pomieszczenie usuwając z niego wodę.", }, "end_level": { "help": "Kończy bieżący poziom.", }, "exit": { "help": "Zamyka grę.", }, "flipmap": { "help": "Przełącza flipmapę.", }, "flood": { "help": "Zalewa aktualne pomieszczenie wodą.", }, "fly": { "help": "Włącza lub wyłącza tryb latania.", }, "fps": { "help": "Zmienia liczbę klatek na sekundę.", }, "give": { "help": "Dodaje dany przedmiot do ekwipunku Lary.", "invalid_secret": "Nieprawidłowy sekret: %s (prawidłowe sekrety: %s)", "secret_given": "Dodano sekret %s", "secret_list": "Zebrane sekrety: %d z %d (%s)", "secret_none": "Zebrane sekrety: %d z %d", "secret_taken": "Usunięto sekret %s", }, "give_secret": { "help": "Wyświetla sekrety Lary lub zabiera/daje sekret według numeru.", }, "heal": { "help": "Leczy Larę do pełni zdrowia.", }, "help": { "help": "Pokazuje listę komend lub opis konkretnej komendy.", "list": "Dostępne komendy:", }, "hp": { "help": "Ustawia zdrowie Lary na określoną wartość.", }, "immune": { "help": "Przełącza nieśmiertelność. (Lara może być nadal zabita w niektórych okolicznościach.)", "off": "Lara jest z powrotem wrażliwa na obrażenia", "on": "Lara jest teraz odporna na obrażenia", }, "inf_sprint": { "help": "Przełącza nieskończony sprint.", "off": "Lara nie może już biegać w nieskończoność", "on": "Lara może teraz biegać w nieskończoność", }, "kill": { "help": "Zabija pobliskich przeciwników.", }, "lighting": { "help": "Włącza lub wyłącza system oświetlenia.", }, "load": { "help": "Ładuje grę z danego slotu zapisu lub szybkiego zapisu.", }, "lua": { "help": "Wykonuje podany kod Lua.", "runtime_error": "Błąd wykonania: %s", "syntax_error": "Błąd składni: %s", }, "mod": { "help": "\\{review}Przełącza na określony mod i ponownie uruchamia grę.", }, "music": { "help": "Odtwarza utwór muzyczny o podanym identyfikatorze.", }, "play_cutscene": { "help": "Wczytuje przerywnik o podanym numerze.", }, "play_demo": { "help": "Wczytuje demo o podanym numerze.", }, "play_gym": { "help": "Wczytuje poziom z domem Lary.", }, "play_level": { "help": "Wczytuje poziom o podanej nazwie lub numerze.", }, "play_music": { "invalid_track": "Nieprawidłowa ścieżka muzyczka", "stopped": "Muzyka zatrzymana", "track": "Odtwarzanie utworu muzycznego %d", }, "pos": { "help": "Pokazuje pozycję Lary.", }, "save": { "help": "Zapisuje grę do danego slotu zapisu, lub do pierwszego dostępnego slotu szybkiego zapisu.", }, "screenshot": { "help": "Zapisuje zrzut ekranu na dysku w opcjonalnej lokalizacji.", }, "set": { "help": "Wyświetla lub aktualizuje dane ustawienie.", }, "sfx": { "help": "Odtwarza efekt dźwiękowy o podanym numerze.", }, "spawn": { "fail": "Nie udało się przywołać żądanego obiektu", "success": "Żądany obiekt przywołany w pobliżu Lary", }, "speed": { "help": "Zmienia prędkość gry.", }, "strings": { "help": "Przeładowuje bieżące pliki językowe z dysku.", }, "teleport": { "item": "Teleportowano do obiektu: %d", "item_fail": "Nie udało się teleportować do obiektu: %d", "object": "Teleportowano do obiektu: %s", "object_fail": "Nie znaleziono obiektu: %s", "pos": "Teleportowano do pozycji: %.3f %.3f %.3f", "pos_fail": "Nie udało się teleportować do pozycji: %.3f %.3f %.3f", "room": "Teleportowano do pomieszczenia: %d", "room_fail": "Nie udało się teleportować do pomieszczenia: %d", }, "textures": { "help": "Włącza lub wyłącza rysowanie tekstur.", }, "title": { "help": "Wraca do menu głównego.", }, "tp": { "help": "Teleportuje Larę do podanej pozycji lub numeru pomieszczenia.", }, "trigger": { "help": "Aktywuje lub dezaktywuje element według id, nazwy elementu lub nazwy obiektu.", "invalid_item": "Nieprawidłowy cel: %s", "no_match": "Nieznany cel: %s", "not_found": "Nieznany cel: %s", "triggered": "Cel aktywowany: %s", "untriggered": "Cel dezaktywowany: %s", }, "vsync": { "help": "Włącza lub wyłącza synchronizację pionową.", }, "weather": { "help": "Zmienia aktualną pogodę.", "invalid": "Nieprawidłowa wartość: %s (prawidłowe: %s)", "set": "Pogoda ustawiona na %s", }, "winston": { "dead": "Twój lokaj jest martwy. Ty potworze!", "spawn_failed": "Nie udało się przywołać Winstona", "spawned": "Przywołano Winstona w pobliżu Lary", "teleported": "Przywołano Winstona w pobliżu Lary", }, "wireframe": { "help": "Włącza lub wyłącza rysowanie w widoku siatki.", } } }, "dynamic": { "config_presets": { "tr1_pc": "\\{review}TR1 PC", "tr1_ps1": "\\{review}TR1 PS1", "tr2_pc": "\\{review}TR2 PC", "tr2_ps1": "\\{review}TR2 PS1", "tr3_pc": "\\{review}TR3 PC", "tr3_ps1": "\\{review}TR3 PS1", }, "enums": { "bar_look": { "tr1_pc": "TR1 PC", "tr2_pc": "TR2 PC", "tr2_ps1": "TR2 PS1", "tr3_pc": "TR3 PC", "tr3_ps1": "TR3 PS1", }, "lara_outfit": { "default": "Domyślny", "golden_sophia": "\\{review}Złota Sophia", "sophia": "\\{review}Sophia", "tr1_bacon_lara": "TR1 klon Lary", "tr1_classic": "TR1 klasyczny", "tr1_combo": "TR1 alternatywny", "tr1_golden_bacon_lara": "TR1 złoty klon Lary", "tr1_golden_lara": "TR1 złota Lara", "tr1_gym": "TR1 dres", "tr1_mauled": "TR1 sponiewierany", "tr1_ngage": "\\{review}TR1 N-Gage", "tr23_golden_lara": "TR2/3 złota Lara", "tr2_bomber_jacket": "TR2 kurtka pilotka", "tr2_classic": "TR2 klasyczny", "tr2_diving_suit": "TR2 pianka do pływania 1", "tr2_diving_suit_alpha": "\\{review}TR2 pianka do pływania 2", "tr2_gym": "TR2 dres", "tr2_robe": "TR2 szlafrok", "tr2_vegas": "TR2 Las Vegas", "tr3_antarctica": "TR3 Antarktyda", "tr3_catsuit": "TR3 kombinezon", "tr3_classic": "TR3 klasyczny", "tr3_gym": "TR3 dres", "tr3_nevada": "TR3 Nevada", "tr3_south_pacific": "TR3 Południowy Pacyfik", } }, "mods": { "tr1": { "title": "\\{review}Tomb Raider I", }, "tr1-demo-pc": { "title": "\\{review}Tomb Raider I Demo", }, "tr1-ub": { "title": "\\{review}Niedokończona sprawa", }, "tr2": { "title": "\\{review}Tomb Raider II", }, "tr2-gm": { "title": "\\{review}Złota Maska", }, "tr3": { "title": "\\{review}Tomb Raider III", }, "tr3-la": { "title": "\\{review}Zaginiony Artefakt", } } }, "enums": { "ALLY_HOSTILITY_POLICY": { "ALLY_HOSTILITY_POLICY_INDIVIDUAL": "Indywidualne", "ALLY_HOSTILITY_POLICY_SHARED": "Współdzielone", }, "ASPECT_MODE": { "ASPECT_MODE_16_10": "16:10", "ASPECT_MODE_16_9": "16:9", "ASPECT_MODE_4_3": "4:3", "ASPECT_MODE_ANY": "Dowolne", }, "BACKGROUND_TYPE": { "BK_BLACK": "Czarne", "BK_IMAGE": "\\{review}Obraz", "BK_MONOCHROME": "Czarno-białe", "BK_MONOCHROME_COOL": "Czarno-białe (zimne)", "BK_MONOCHROME_WARM": "Czarno-białe (ciepłe)", "BK_NONE": "Przezroczyste", "BK_PATTERN_STATIC": "Stałe", "BK_PATTERN_WAVE": "Falujące", "BK_TRANSPARENT_DARK": "Bardzo ciemne", "BK_TRANSPARENT_MEDIUM": "Ciemne", }, "BAR_SHOW_MODE": { "BAR_SHOW_MODE_ALWAYS": "Zawsze", "BAR_SHOW_MODE_BOSS_ONLY": "Tylko bossowie", "BAR_SHOW_MODE_NEVER": "Wyłączony", }, "BILLBOARD_LOCK_MODE": { "BILLBOARD_LOCK_NONE": "Brak", "BILLBOARD_LOCK_PERSPECTIVE": "Perspektywa", "BILLBOARD_LOCK_ROLL": "Obrót", "BILLBOARD_LOCK_ROLL_PITCH": "Obrót i pochylenie", }, "BLOOD_EFFECTS": { "BLOOD_EFFECTS_DISABLED": "\\{review}Wyłączone", "BLOOD_EFFECTS_PINK": "\\{review}Różowy", "BLOOD_EFFECTS_RED": "\\{review}Czerwony", }, "CAMERA_MODE": { "CAMERA_MODE_TR1": "TR1", "CAMERA_MODE_TR2": "TR2", "CAMERA_MODE_TR3": "TR3", }, "CREATURE_DROWN_POLICY": { "CREATURE_DROWN_POLICY_DEFAULT": "\\{review}Domyślne", "CREATURE_DROWN_POLICY_NEVER": "\\{review}Nigdy", "CREATURE_DROWN_POLICY_SUBMERGED": "\\{review}Zanurzone", }, "INPUT_BACKEND": { "INPUT_BACKEND_CONTROLLER": "\\{review}Kontroler", "INPUT_BACKEND_KEYBOARD": "\\{review}Klawiatura", }, "INPUT_ROLE": { "INPUT_ROLE_ACTION": "Interakcja", "INPUT_ROLE_CAMERA_BACK": "Kamera do tyłu", "INPUT_ROLE_CAMERA_DOWN": "Kamera w dół", "INPUT_ROLE_CAMERA_FORWARD": "Kamera do przodu", "INPUT_ROLE_CAMERA_LEFT": "Kamera w lewo", "INPUT_ROLE_CAMERA_RESET": "Reset kamery", "INPUT_ROLE_CAMERA_RIGHT": "Kamera w prawo", "INPUT_ROLE_CAMERA_UP": "Kamera w górę", "INPUT_ROLE_CHANGE_OUTFIT": "Zmień strój", "INPUT_ROLE_CHANGE_TARGET": "Zmień cel", "INPUT_ROLE_CROUCH": "Kucnij", "INPUT_ROLE_CYCLE_LIGHTING_CONTRAST": "Zmiana kontrastu oświetlenia", "INPUT_ROLE_DOWN": "Cofanie", "INPUT_ROLE_DRAW_WEAPON": "Wyciągnij broń", "INPUT_ROLE_ENTER_CONSOLE": "Otwórz konsolę", "INPUT_ROLE_EQUIP_AUTOS": "Wyposaż pistolety automatyczne", "INPUT_ROLE_EQUIP_DESERT_EAGLE": "Wyposaż Desert Eagle", "INPUT_ROLE_EQUIP_GRENADE_LAUNCHER": "Wyposaż wyrzutnię granatów", "INPUT_ROLE_EQUIP_HARPOON": "Wyposaż wyrzutnię harpunów", "INPUT_ROLE_EQUIP_M16": "Wyposaż M16", "INPUT_ROLE_EQUIP_MAGNUMS": "Wyposaż magnumy", "INPUT_ROLE_EQUIP_MP5": "Wyposaż MP5", "INPUT_ROLE_EQUIP_PISTOLS": "Wyposaż pistolety", "INPUT_ROLE_EQUIP_ROCKET_LAUNCHER": "Wyposaż wyrzutnię rakiet", "INPUT_ROLE_EQUIP_SHOTGUN": "Wyposaż strzelbę", "INPUT_ROLE_EQUIP_UZIS": "Wyposaż uzi", "INPUT_ROLE_FLY_CHEAT": "Włącz cheat na latanie", "INPUT_ROLE_FPS": "Licznik klatek na sekundę", "INPUT_ROLE_INVENTORY": "Ekwipunek", "INPUT_ROLE_ITEM_CHEAT": "Daj wszystkie przedmioty", "INPUT_ROLE_JUMP": "Skakanie", "INPUT_ROLE_LEFT": "Lewo", "INPUT_ROLE_LEVEL_SKIP_CHEAT": "Pomiń aktualny poziom", "INPUT_ROLE_LOAD": "Wczytanie gry", "INPUT_ROLE_LOOK": "Rozglądanie", "INPUT_ROLE_PAUSE": "Pauza", "INPUT_ROLE_QUICK_LOAD": "Szybkie wczytanie", "INPUT_ROLE_QUICK_SAVE": "Szybki zapis", "INPUT_ROLE_RIGHT": "Prawo", "INPUT_ROLE_ROLL": "Przewrót", "INPUT_ROLE_SAVE": "Zapis gry", "INPUT_ROLE_SCREENSHOT": "Zrzut ekranu", "INPUT_ROLE_SLOW": "Chodzenie", "INPUT_ROLE_SPRINT": "Sprint", "INPUT_ROLE_STEP_LEFT": "Krok w lewo", "INPUT_ROLE_STEP_RIGHT": "Krok w prawo", "INPUT_ROLE_SWITCH_BORDERS": "Zmień rozmiar ramki", "INPUT_ROLE_SWITCH_UPSCALING": "Zmień współczynnik skalowania", "INPUT_ROLE_TOGGLE_BILINEAR_FILTER": "Przełącz filtr tekstur", "INPUT_ROLE_TOGGLE_FULLSCREEN": "Włącz/wyłącz tryb pełnoekranowy", "INPUT_ROLE_TOGGLE_PHOTO_MODE": "Włącz tryb zdjęć", "INPUT_ROLE_TOGGLE_TEXTURES": "Włącz/wyłącz tekstury", "INPUT_ROLE_TOGGLE_TRAPEZOID_FILTER": "Włącz/wyłącz filtr trapezowy", "INPUT_ROLE_TOGGLE_UI": "Ukryj lub pokaż UI", "INPUT_ROLE_TOGGLE_WIREFRAME": "Włącz/wyłącz widok siatki", "INPUT_ROLE_TURBO_CHEAT": "Zwiększ lub zmniejsz prędkość", "INPUT_ROLE_UP": "Bieg do przodu", "INPUT_ROLE_USE_BIG_MEDI": "Duża apteczka", "INPUT_ROLE_USE_FLARE": "Flara", "INPUT_ROLE_USE_SMALL_MEDI": "Mała apteczka", }, "JUMP_LOCK_MODE": { "JUMP_LOCK_DISABLED": "Wyłączony", "JUMP_LOCK_LEGACY": "Klasyczny", "JUMP_LOCK_TUNED": "Dostrojony", }, "LIGHTING_CONTRAST": { "LIGHTING_CONTRAST_HIGH": "Wysoki", "LIGHTING_CONTRAST_LOW": "Niski", "LIGHTING_CONTRAST_MEDIUM": "Średni", }, "LOADING_SCREENS_MODE": { "LOADING_SCREENS_ALWAYS": "Zawsze", "LOADING_SCREENS_DISABLED": "Wył.", "LOADING_SCREENS_NEW_GAMES": "Tylko nowa gra", }, "LOOK_MODE": { "LOOK_MODE_ENHANCED": "Rozszerzony", "LOOK_MODE_RESTRICTED": "Ograniczony", "LOOK_MODE_UNRESTRICTED": "Nieograniczony", }, "MUSIC_LOAD_CONDITION": { "MUSIC_LOAD_CONDITION_ALWAYS": "Zawsze", "MUSIC_LOAD_CONDITION_NEVER": "Nigdy", "MUSIC_LOAD_CONDITION_NON_AMBIENT": "Tylko muzyka", }, "PROJECTILE_AREA_DAMAGE": { "PROJECTILE_AREA_DAMAGE_MULTI_SWEEP": "Wielokrotny", "PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP": "Pojedynczy", }, "QUICK_GUNS_MODE": { "QUICK_GUNS_MODE_DRAW_AND_HOLSTER": "Wyposaż", "QUICK_GUNS_MODE_DRAW_ONLY": "Wyposaż lub schowaj", }, "SCREENSHOT_FORMAT": { "SCREENSHOT_FORMAT_JPEG": "JPG", "SCREENSHOT_FORMAT_PNG": "PNG", }, "SHADOW_TYPE": { "SHADOW_TYPE_CIRCLE": "Elipsa", "SHADOW_TYPE_OCTAGON": "Ośmiokąt", "SHADOW_TYPE_SPRITE": "Sprite", }, "STATS_STYLE": { "STATS_STYLE_BARE": "\\{review}Prosty", "STATS_STYLE_BORDERED": "\\{review}Z obramowaniem", }, "SUNGLASSES_MODE": { "SUNGLASSES_MODE_OFF": "\\{review}Wyłączone", "SUNGLASSES_MODE_OPAQUE": "\\{review}Nieprzezroczyste", "SUNGLASSES_MODE_TRANSPARENT": "\\{review}Przezroczyste", }, "TARGET_LOCK_MODE": { "TARGET_LOCK_MODE_FULL": "Całkowity", "TARGET_LOCK_MODE_NONE": "Brak", "TARGET_LOCK_MODE_SEMI": "Częściowy", }, "TEXTURE_FILTER": { "TEXTURE_FILTER_BILINEAR": "Dwuliniowy", "TEXTURE_FILTER_POINT": "Punktowy", }, "UI_ELEMENT_LOCATION": { "UI_ELEMENT_LOCATION_BOTTOM_CENTER": "Dół na środku", "UI_ELEMENT_LOCATION_BOTTOM_LEFT": "Dół po lewej", "UI_ELEMENT_LOCATION_BOTTOM_RIGHT": "Dół po prawej", "UI_ELEMENT_LOCATION_TOP_CENTER": "Góra na środku", "UI_ELEMENT_LOCATION_TOP_LEFT": "Góra po lewej", "UI_ELEMENT_LOCATION_TOP_RIGHT": "Góra po prawej", }, "UI_STYLE": { "UI_STYLE_PC": "PC", "UI_STYLE_PS1": "PS1", }, "WALL_GLITCH_MODE": { "WALL_GLITCH_FIXED": "Naprawione", "WALL_GLITCH_TR1": "TR1", "WALL_GLITCH_TR2": "TR2", } }, "settings": { "audio.ambient_volume": { "title": "Głośność otoczenia", "description": "Reguluje głośność otoczenia.", }, "audio.cutscene_volume": { "title": "Głośność przerywników", "description": "Reguluje głośność przerywników filmowych w grze.", }, "audio.enable_lara_mic": { "title": "Mikrofon przy Larze", "description": "Ustawia mikrofon na pozycji Lary. Domyślnie mikrofon jest na pozycji kamery. Wpływa to głównie na głośność efektów – czy przy kamerach daleko od Lary będą one cicho, czy głośno.", }, "audio.enable_music_in_inventory": { "title": "\\{review}Odtwarzaj muzykę w ekwipunku", "description": "\\{review}Pozwala na odtwarzanie dźwięków gry, dźwięków otoczenia i muzyki podczas przeglądania ekwipunku.", }, "audio.enable_music_in_menu": { "title": "Muzyka w menu głównym", "description": "Odtwarza muzykę w menu głównym.", }, "audio.enable_pitched_sounds": { "title": "Modulacja wysokości dźwięków", "description": "Pozwala na losowe, nieznaczne zmiany tonacji efektów dźwiękowych dla urozmaicenia rozgrywki.", }, "audio.enable_ps1_sfx": { "title": "Efekty dźwiękowe z PS1", "description": "Zamienia określone efekty dźwiękowe na odpowiedniki z PS1.\n\n- Pociski z Uzi (tylko TR1)\n- Odgłosy poruszania się boso (tylko TR2)", }, "audio.enable_underwater_anim_sfx": { "title": "Efekty dźwiękowe pod wodą", "description": "Włącza odtwarzanie określonych efektów dźwiękowych animacji – dla obiektów takich jak drzwi i włazy – gdy kamera jest pod wodą.", }, "audio.fix_chainblock_secret_sound": { "title": "Napraw dźwięku blokady łańcucha", "description": "Zapobiega nieprawidłowemu odtwarzaniu dźwięku sekretu po użyciu złotego klucza w Grobowcu Tihocana.", }, "audio.fix_secrets_killing_music": { "title": "Warstwowe dźwięki sekretów", "description": "Sprawia, że podnoszenie sekretów nie zatrzymuje aktualnie odtwarzanej muzyki.", }, "audio.fix_speeches_killing_music": { "title": "Warstwowe wypowiedzi wrogów", "description": "Sprawia, że wypowiedzi wrogów nie zatrzymuje aktualnie odtwarzanej muzyki.", }, "audio.fmv_volume": { "title": "Głośność filmów", "description": "Reguluje głośność filmów.", }, "audio.inventory_ambient_volume": { "title": "Głośność otoczenia (ekwipunek)", "description": "Reguluje głośność otoczenia na ekranie ekwipunku.", }, "audio.inventory_music_volume": { "title": "Głośność muzyki (ekwipunek)", "description": "Reguluje głośność muzyki na ekranie ekwipunku.", }, "audio.load_music_triggers": { "title": "Napraw wyzwalacze muzyki", "description": "Ładuje wcześniej wyzwolone utwory muzyczne, tak aby jednorazowe ścieżki nie były odtwarzane ponownie.", }, "audio.master_volume": { "title": "\\{icon music} Głośność", "description": "Reguluje głośność wszystkich dźwięków w grze. Pozostałe ustawienia są względem tej głośności.", }, "audio.music_load_condition": { "title": "Przywróć muzykę po wczytaniu", "description": "Ładuje ścieżkę dźwiękową graną podczas zapisu gry.\n\n- Nigdy: restartuje dźwięki otoczenia od 0.\n- Tylko muzyka: przywraca utwory muzyczne.\n- Zawsze: przywraca także dźwięki otoczenia od momentu, w którym gracz dokonał zapisu.", }, "audio.music_volume": { "title": "Głośność muzyki", "description": "Reguluje głośność muzyki.", }, "audio.mute_out_of_focus": { "title": "Wycisz dźwięk po zminimalizowaniu", "description": "Wycisza muzykę i efekty dźwiękowe, kiedy okno gry jest nieaktywne.", }, "audio.sound_volume": { "title": "\\{icon sound} Głośność dźwięku", "description": "Dostosowuje głośność efektów dźwiękowych.", }, "audio.underwater_ambient_volume": { "title": "Głośność otoczenia (pod wodą)", "description": "Reguluje głośność otoczenia pod wodą.", }, "audio.underwater_music_volume": { "title": "Głośność muzyki (pod wodą)", "description": "Reguluje głośność muzyki pod wodą.", }, "debug.enable_endless_flare_time": { "title": "Nielimitowany czas flar", "description": "Zapobiega wygasaniu flar. Rzucone flary nadal gasną normalnie.", }, "debug.enable_endless_sprint": { "title": "Nielimitowany sprint", "description": "Lara nie męczy się podczas biegu, lecz przeszkody wciąż ją zatrzymują.", }, "gameplay.ally_hostility_policy": { "title": "Wrogość sojuszników", "description": "Kontroluje zachowanie jednostek kiedy zostaną zranione przez gracza.\n\n- Indywidualne: każdy sojusznik zmienia wrogość samodzielnie (styl TR3).\n- Współdzielone: wszyscy sojusznicy stają się wrodzy jednocześnie (styl mnichów z TR2).", }, "gameplay.camera_speed": { "title": "Prędkość kamery", "description": "Zmienia prędkość, z jaką porusza się manualna kamera.", }, "gameplay.change_pierre_spawn": { "title": "Zmień tryb pojawiania się Pierre'a", "description": "Włączenie tej opcji sprawi, że podczas przechodzenia przez określone miejsca Pierre pojawi się bliżej. Jeśli opcja jest wyłączona, nic się nie zmieni. Dotyczy tylko sytuacji, gdzie nie Lara nie przegoniła Pierre'a.", }, "gameplay.creature_drown_policy": { "title": "\\{review}Zasady tonięcia stworzeń", "description": "\\{review}Określa zachowanie stworzeń lądowych w pomieszczeniach z wodą.\n\n- Nigdy: stworzenia lądowe nigdy nie toną (styl TR1).\n- Domyślne: stworzenia lądowe toną w wodzie o głębokości 2 klików lub większej (styl TR2/3).\n- Zanurzone: stworzenia lądowe toną tylko wtedy, gdy są całkowicie zanurzone.", }, "gameplay.disable_extra_guns": { "title": "Wyłącz dodatkowe bronie", "description": "Usuwa wszystkie bronie i amunicję z gry z wyjątkiem pistoletów.", }, "gameplay.disable_healing_between_levels": { "title": "Trwałe obrażenia", "description": "Wyłącza leczenie Lary, gdy gracz zaczyna nowy poziom.", }, "gameplay.disable_medpacks": { "title": "Wyłącz apteczki", "description": "Usuwa wszystkie apteczki z gry.", }, "gameplay.disable_trex_collision": { "title": "Usuń kolizję z ubitym T-Rexem", "description": "Usuwa wszelką kolizję z tyranozaurem po jego śmierci, co pomaga w sytuacjach, gdy jego cielsko uniemożliwia przejście.", }, "gameplay.enable_ally_targeting": { "title": "Pozwól na celowanie w sojuszników", "description": "Pozwala Larze celować w sojuszników, np. mnichów. Jeśli opcja jest wyłączona, sojusznicy będą całkowicie odporni na amunicję Lary.", }, "gameplay.enable_auto_item_selection": { "title": "Preselekcja przedmiotów", "description": "Kiedy Lara spróbuje wejść w interakcję z zamkiem lub slotem na przedmiot i ma odpowiednią rzecz w plecaku, zostanie ona automatycznie wybrana.", }, "gameplay.enable_back_slope_stumble": { "title": "Potknięcia na spadach do tyłu", "description": "Powoduje, że Lara potyka się, gdy się cofa, a za nią znajduje się pochylnia (TR3). Przy wyłączonej opcji, Lara zatrzymuje się gwałtownie na pochylniach (TR1/2).", }, "gameplay.enable_body_bags": { "title": "\\{review}Wyzwalacze worków na ciała", "description": "\\{review}Umożliwia usuwanie zabitych przeciwników, gdy Lara przekracza określone wyzwalacze na niektórych poziomach. Jeśli wyłączone, martwi przeciwnicy będą zawsze wyświetlani.", }, "gameplay.enable_boulder_shake": { "title": "Wstrząsy kamery od głazów", "description": "Włączenie opcji spowoduje wstrząsy kamery, kiedy w pobliżu Lary toczą się głazy.", }, "gameplay.enable_bouncy_grenades": { "title": "Odbicia granatów", "description": "Włącza zachowanie granatów w stylu TR3 – odbijają się od ścian i pochyłości i zyskują większy promień wybuchu kosztem mniejszej prędkości.", }, "gameplay.enable_cheats": { "title": "Cheaty", "description": "Włącza różne cheaty:\n\n- L: natychmiast kończy poziom.\n- I: daje Larze wszystkie bronie; zwiększa ilość amunicji i apteczek; daje wszystkie przedmioty z bieżącego poziomu.\n- O: włącza latanie.\n - Klawisz chodzenia: wyjście z trybu latani.\n - Klawisz broni: otwiera najbliższe drzwi.", }, "gameplay.enable_cinematics": { "title": "\\{review}Sekwencje skryptowe", "description": "\\{review}Włącza sekwencje skryptowe na początku niektórych poziomów, w których są dostępne.", }, "gameplay.enable_compass_stats": { "title": "Statystyki w kompasie", "description": "Włącza wyświetlanie statystyk aktualnego poziomu przy wybraniu kompasu.", }, "gameplay.enable_console": { "title": "Konsola", "description": "Włącza konsolę umożliwiającą wpisywanie różnych komend.", }, "gameplay.enable_controlled_drops": { "title": "Bezpieczne krawędzie", "description": "Pozwala Larze obrócić się w powietrzu i złapać krawędź z której gracz właśnie się ześliznął, jeśli trzymay jest przycisk akcji.", }, "gameplay.enable_crawl_jump": { "title": "Skok z wąskich przestrzeni", "description": "Umożliwia Larze wyskoczenie przy krawędziach w wąskich przestrzeniach, kiedy porusza się na czworaka.", }, "gameplay.enable_crawl_tilt": { "title": "\\{review}Nachylenie podczas czołgania", "description": "\\{review}Dopasowuje rotację Lary do geometrii podłoża podczas czołgania.", }, "gameplay.enable_crawling": { "title": "Kucanie", "description": "Pozwala Larze kucać i chodzić na czwroaka.", }, "gameplay.enable_credits": { "title": "Napisy końcowe", "description": "Włącza ekrany z napisami końcowymi wyświetlane po ukończeniu gry. Ustawienie nie ma wpływu na ekran końcowych statystyk.", }, "gameplay.enable_crouch_roll": { "title": "Przewrót z kucania", "description": "Pozwala Larze wykonać przewrót w przód z kucania po naciśnięciu klawisza sprintu.", }, "gameplay.enable_cutscenes": { "title": "Przerywniki", "description": "Włącza odtwarzanie przerywników (cutscenek).", }, "gameplay.enable_demo": { "title": "Tryb demo", "description": "Włącza odtwarzanie krótkich demonstracyjnych sekwencji w menu głównym.", }, "gameplay.enable_enemy_rotation": { "title": "Losuj rotację przeciwników", "description": "Zastosowuje dodatkowy losowy kąt do niektórych przeciwników podczas ładowania poziomu.", }, "gameplay.enable_enhanced_saves": { "title": "Zapis efektów", "description": "Zapisuje efekty graficzne, mgłę wodospadu, emitery płomieni itp., dzięki czemu nie znikają już po wczytaniu.", }, "gameplay.enable_fmv": { "title": "Filmy", "description": "Włącza odtwarzanie filmów.", }, "gameplay.enable_game_modes": { "title": "Wybór trybu gry", "description": "Pozwala na wybór opcji trybu przy rozpoczynaniu nowej gry.\n\n- Nowa Gra+: odblokowuje wszystkie bronie z nieskończoną amunicją; przeciwnicy mają podwójne zdrowie.\n- Japońska NG: bronie zadają podwójne obrażenia; znajdźki z flarami zawierają 8 flar zamiast 6.\n- Japońska NG+: połączenie Nowej Gry+ i Japońskiej NG.", }, "gameplay.enable_idle_pose_camera": { "title": "Kamera bezczynności", "description": "Dostosowuje kamerę tak, aby była skierowana na Larę podczas animacji bezczynności. Przycisk patrzenia resetuje kamerę.", }, "gameplay.enable_inverted_look": { "title": "Odwrócenie osi Y patrzenia", "description": "Odwraca sterowanie rozglądania się w osi Y.", }, "gameplay.enable_item_examining": { "title": "Badanie przedmiotów", "description": "Pozwala na wyświetlanie opisów przedmiotów w ekwipunku w niestandardowych poziomach, gdzie autor poziomu dostarczył odpowiednie dane.", }, "gameplay.enable_jump_twists": { "title": "Przewroty w powietrzu", "description": "Włącza możliwość przewrotu podczas skakania.", }, "gameplay.enable_killer_pushblocks": { "title": "Zabójcze bloki", "description": "Kiedy ta opcja jest włączona, spadające z wysokości bloki natychmiast zabijają Larę. W przeciwnym razie Lara stanie na powierzchni bloku i przeżyje (tak jak w oryginalnych grach).", }, "gameplay.enable_lean_jumping": { "title": "Dalekie skoki w miejscu", "description": "Pozwala Larze na przesuwanie się do przodu lub do tyłu dalej podczas wykonywania skoków w miejscu z wciśniętym odpowiednim klawiszem (tak jak w TR2+).", }, "gameplay.enable_ledge_jumps": { "title": "Skoki z krawędzi", "description": "Pozwala Larze skoczyć w górę lub do tyłu, gdy zwisa z krawędzi, o ile przed nią znajduje się solidna powierzchnia, od której może się odepchnąć.", }, "gameplay.enable_legal": { "title": "Informacje prawne", "description": "Włącza ekran z informacjami prawnymi oraz filmik z logiem Core Design na początku gry.", }, "gameplay.enable_manual_camera": { "title": "Manualna kamera", "description": "Umożliwia klawiszom kamery obracanie jej także podczas gry, nie tylko w trybie zdjęć.", }, "gameplay.enable_neutral_twists": { "title": "Salta w miejscu", "description": "Pozwala Larze obracać się w powietrzu podczas neutralnego skoku. Aby wykonać salto w miejscu, należy przytrzymać jednocześnie przyciski skoku i przewrotu.", }, "gameplay.enable_pickup_aids": { "title": "Pomoce do znajdziek", "description": "Włącza okazjonalne migotanie w pobliżu przedmiotów do podniesienia w celu zwiększenia ich widoczności.", }, "gameplay.enable_play_previous_levels": { "title": "Zagraj w poprzednie poziomy", "description": "Włącza funkcje \"Zagraj w poprzednie poziomy\" oraz \"Co się wydarzyło do tej pory\" na ekranie wyboru Nowej Gry.", }, "gameplay.enable_responsive_crawl": { "title": "Responsywne kucanie", "description": "Włącza ulepszenia względem oryginalnej mechaniki kucania i czołgania.\n\n- Umożliwia szybsze wznowienie czołgania po zatrzymaniu.\n- Umożliwia przejście z biegu/sprintu do kucnięcia bez konieczności wcześniejszego zatrzymania się.\n- Umożliwia przejście z czołgania do przewrotu w przysiadzie (jeśli jest włączony) bez konieczności przejścia w kucnięcie.\n- Umożliwia obracanie się podczas kucania.\n- Przywraca animację podnoszenia przedmiotów podczas czołgania Lary (z wyłączeniem flar).", }, "gameplay.enable_responsive_sprint": { "title": "Responsywny sprint", "description": "Włącza bardziej responsywny tryb sprintu dla Lary.\n\n- Pozwala sprintować, gdy Lara ma energię, a nie tylko przy pełnej wytrzymałości.\n- Umożliwia sprint po schodach bez przerywania zwykłą animacją biegu.", }, "gameplay.enable_save_crystals": { "title": "Kryształy zapisu", "description": "Ogranicza zapisywanie do początku poziomów oraz kryształów zapisu umieszczonych w konkretnych lokacjach. Poziomy mają ograniczone, jednorazowe kryształy zapisu jak w wersji PS1. Zmiana tej opcji wymaga ponownego uruchomienia poziomu.", }, "gameplay.enable_slide_to_run": { "title": "Bieg po zjechaniu", "description": "Umożliwia Larze natychmiastowe wznowienie biegu po zjechaniu w dół stoku. Aby aktywować, należy przytrzymać przycisk ruchu do przodu.", }, "gameplay.enable_slow_ledge_swing": { "title": "Powolne kołysanie na krawędzi", "description": "Umożliwia Larze powolne kołysanie się po uchwyceniu bardzo cienkiej krawędzi (styl TR3). Jeśli wyłączone, Lara zakołysze się tylko krótko, po czym wróci do nieruchomej pozycji wiszącej (styl TR1/2).", }, "gameplay.enable_smooth_wall_deflect": { "title": "Gładkie odbicia od ściany", "description": "Pozwala Larze szybciej się odbić po uderzeniu w ścianę, gdy jednocześnie trzymany jest klawisz kierunku oraz do przodu.", }, "gameplay.enable_soft_statics": { "title": "Miękka kolizja ze statycznymi obiektami", "description": "Pozwala Larze płynnie przesuwać się wzdłuż statycznych obiektów, jak w TR4+, zamiast nagle się zatrzymywać.", }, "gameplay.enable_sprint": { "title": "Sprint", "description": "Pozwala Larze sprintować, podobnie jak w TR3 i nowszych grach.", }, "gameplay.enable_step_roll_boost": { "title": "Przewroty na schodkach", "description": "Pozwala Larze uzyskać dodatkowy skok z małych schodków, jeśli spróbuje wykonać przewrót blisko krawędzi.", }, "gameplay.enable_swing_cancel": { "title": "Łapanie krawędzi po puszczeniu", "description": "Pozwala na anulowanie animacji huśtania Lary na krawędzi poprzez jej puszczenie i szybkie złapanie ponownie (tak jak w TR2+).", }, "gameplay.enable_target_change": { "title": "Zmiana celu", "description": "Włącza zmianę celu podczas celowania bronią (tak jak w TR4+).", }, "gameplay.enable_timer_in_inventory": { "title": "Upływ czasu w ekwipunku", "description": "Sprawia, że zegar w grze działa nawet podczas wyświetlania menu ekwipunku.", }, "gameplay.enable_toggle_crouch": { "title": "\\{review}Przełącz kucanie", "description": "\\{review}Pozwala Larze pozostać w kucki po jednokrotnym naciśnięciu przycisku kucania. Naciśnij kucanie ponownie, aby wstać.", }, "gameplay.enable_toggle_sprint": { "title": "\\{review}Przełącz sprint", "description": "\\{review}Pozwala Larze biec sprintem po jednokrotnym naciśnięciu przycisku sprintu. Naciśnij sprint ponownie, aby przestać sprintować.", }, "gameplay.enable_total_stats": { "title": "Statystyki końcowe", "description": "Włącza ekran finalnych statystyk z całej gry, odtwarzany po napisach końcowych.", }, "gameplay.enable_tr2_jumping": { "title": "Responsywne skakanie", "description": "Pozwala Larze skakać w dowolnym momencie podczas biegu.", }, "gameplay.enable_tr2_swim_cancel": { "title": "Mniejszy dryf pod wodą", "description": "Pozwala Larze na bardziej responsywne zatrzymanie się pod wodą po zwolnieniu klawisza pływania.", }, "gameplay.enable_tr2_swimming": { "title": "Zwinniejsze pływanie", "description": "Powoduje, że Lara przyspiesza płynniej, ale wolniej podczas obracania pod wodą (tak jak w TR2+). Wyłączenie tej opcji zapewni Larze szybszą prędkość skrętu (tak jak w TR1).", }, "gameplay.enable_uw_roll": { "title": "Przewroty pod wodą", "description": "Pozwala Larze na przewrót pod wodą.", }, "gameplay.enable_wading": { "title": "Brodzenie", "description": "Pozwala Larze na brodzenie w płytkiej wodzie. Wyłączenie powoduje utknięcie na powierzchni wody.", }, "gameplay.enable_walk_to_items": { "title": "Animowane interakcje", "description": "Sprawia, że Lara chodzi do przedmiotów i przełączników, gdy są w pobliżu, zamiast się do nich natychmiastowo przesuwać.", }, "gameplay.fix_alligator_ai": { "title": "Napraw AI aligatorów", "description": "Naprawia błąd w którym aligatory nie zadają obrażeń, jeśli Lara pozostaje nieruchomo w wodzie.", }, "gameplay.fix_bear_ai": { "title": "Napraw AI niedźwiedzi", "description": "Naprawia atak niedźwiedzia, aby nie chybiał Lary.", }, "gameplay.fix_bridge_collision": { "title": "Napraw kolizję mostów", "description": "Naprawia problem, w którym Lara nie może chwycić części niektórych mostów oraz krawędzi. Naprawia również problemy z kolizją mostów zwodzonych, pułapek i zwykłych mostów, gdy są nałożone na siebie, są przy zboczach lub blisko ziemi.", }, "gameplay.fix_descending_glitch": { "title": "Napraw glitch z łamliwą podłogą", "description": "Naprawia problem, w którym chodzenie w bok lub do tyłu na łamliwej podłodze powoduje natychmiastowy teleport Lary na podłogę poniżej.", }, "gameplay.fix_flare_throw_priority": { "title": "Napraw priorytet rzucania flar", "description": "Rozwiązuje problem, w którym Lara wyrzuca zużytą flarę w powietrze y nie może po tym złapać krawędzi.", }, "gameplay.fix_floor_data_issues": { "title": "Napraw triggery", "description": "Naprawia błędy oryginalnej gry zw. z triggerami / wyzwalaczami.", }, "gameplay.fix_free_flare_glitch": { "title": "Napraw darmową flarę", "description": "Naprawia możliwość pojawienia się darmowej flary, jeśli gracz naciśnie przycisk flary podczas zbierania jakiegoś przedmiotu.", }, "gameplay.fix_item_duplication_glitch": { "title": "Napraw klonowanie przedmiotów", "description": "Naprawia możliwość klonowania przedmiotów w górnym pierścieniu ekwipunku.", }, "gameplay.fix_lara_pickup_embed": { "title": "Napraw podnoszenie przedmiotów", "description": "Naprawia problem, w wyniku którego gra umieszczała Larę wewnątrz ścian podczas zbierania przedmiotów pod wodą lub pod stromymi sufitami.", }, "gameplay.fix_m16_accuracy": { "title": "Napraw celność M16/MP5", "description": "Naprawia celność M16/MP5 podczas biegania.", }, "gameplay.fix_monkey_pickup_priority": { "title": "Priorytet ataku u małp", "description": "Zaatakowane małpy skupią się na odwecie zamiast podnosić apteczki i klucze.", }, "gameplay.fix_pipeman_aim": { "title": "Popraw celowanie tubylców", "description": "Naprawia błąd, przez który tubylcy z dmuchawkami czasami nie potrafili dobrze wycelować w Larę.", }, "gameplay.fix_qwop_glitch": { "title": "Napraw glitch QWOP", "description": "Naprawia rzadki problem pojawiający się, kiedy Lara zeskakuje z małych schodków, co skutkuje dziwną animacją biegu znaną jako stan QWOP.", }, "gameplay.fix_step_glitch": { "title": "Napraw glitch ze schodami", "description": "Naprawia problem, w którym Lara czasami jest popychana w ściany przylegające do małych stopni, kiedy biegnie po nich w określony sposób.", }, "gameplay.fix_wade_wall_hit": { "title": "Zderzenia w wodzie po pas", "description": "Naprawia problem, w którym Lara nie reaguje na uderzenie w ścianę podczas brodzenia w wodzie po pas.", }, "gameplay.fix_walk_run_jump": { "title": "Napraw skakanie podczas biegu", "description": "Naprawia problem, w którym Lara czasami nie może skoczyć natychmiast po przejściu z animacji chodzenia do biegu.", }, "gameplay.fix_wall_geometry": { "title": "\\{review}Naprawa geometrii ścian", "description": "\\{review}Naprawia przypadki w geometrii poziomów OG, w których nachylenia wewnątrz ścian mogą prowadzić do niedokładnych obliczeń wysokości.", }, "gameplay.fix_water_exit": { "title": "Napraw wyjście z wody", "description": "Naprawia problem, w którym Lara może przejść bezpośrednio z zatopionego pomieszczenia do sąsiadującego suchego pomieszczenia. Dodatkowo, opcja zapobiega Larze wchodzeniu z wody na nachylone powierzchnie, na których nie można stać.", }, "gameplay.harpoon_recoil": { "title": "Odrzut harpuna", "description": "Ustawia, jak często Lara musi przeładowywać harpun w zależności od aktualnej ilości amunicji. Np. przy ustawieniu wartości na 3 Lara będzie musiała przeładować po każdym trzecim strzale. Ustawienie na 0 całkowicie wyłącza przeładowywanie.", }, "gameplay.idle_pose_timeout": { "title": "Czas bezczynności", "description": "Pozwala Larze przyjąć pozę po określonej liczbie sekund bezczynności. Wartość 0 wyłącza tę funkcję.", }, "gameplay.jump_lock_mode": { "title": "Tryb blokady skoku", "description": "Kontroluje, jak szybko Lara może skoczyć po rozpoczęciu biegu. Poszczególne tryby:\n\n- Klasyczny: odpowiada TR2.\n- Dostrojony: skok jest możliwy o 2 klatki wcześniej niż w TR2.\n- Wyłączony: skok jest możliwy natychmiast po animacji startu biegu.", }, "gameplay.loading_screens": { "title": "Ekrany ładowania", "description": "Kontroluje sposób pokazywania ekranów ładowania wyświetlanych przed rozgrywką.\n\n- Wyłączone: całkowicie wyłącza ekrany ładowania.\n- Zawsze: pokazuje ekrany ładowania zawsze przed zaczęciem gry.\n- Tylko nowa gra: pomija ekrany ładowania podczas wczytywania zapisanej gry.", }, "gameplay.look_mode": { "title": "Tryb rozglądania", "description": "Określa, kiedy Lara może się rozglądać.\n\n- Ograniczony: rozglądanie tylko w bezruchu, nigdy pod wodą.\n- Rozszerzony: rozglądanie dozwolone podczas większości animacji, z wyjątkiem np. pchania bloku.\n- Nieograniczony: rozglądanie zawsze dozwolone przy normalnym sterowaniu Larą.", }, "gameplay.maximum_quick_save_slots": { "title": "Liczba slotów szybkiego zapisu", "description": "Zmienia liczbę dostępnych slotów szybkiego zapisu.", }, "gameplay.maximum_save_slots": { "title": "Liczba slotów zapisu", "description": "Zmienia liczbę dostępnych slotów na zapisy.", }, "gameplay.pause_on_focus_lost": { "title": "\\{review}Pauza po utracie fokusu", "description": "\\{review}Zatrzymuje postęp rozgrywki, gdy okno gry traci fokus.", }, "gameplay.projectile_area_damage": { "title": "Przebieg eksplozji pocisków", "description": "Kontroluje metodę liczenia zasięgu eksplozji pocisków z wyrzutni rakiet i granatników.\n\n- Pojedynczy przebieg: zachowanie z TR1 i TR2.\n- Wielokrotny przebieg: zachowanie z TR3.\n\nWielokrotny przebieg często powoduje podwójne obrażenia dla pojedynczych celów.", }, "gameplay.remember_gun_status": { "title": "Zapamiętuj broń między poziomami", "description": "Włączenie tej opcji sprawi, że Lara zapamięta wybór broni z poprzedniego poziomu. W przeciwnym wypadku Lara zawsze będzie wracać do pistoletów, domyślnie schowanych w kaburach.", }, "gameplay.restore_ps1_enemies": { "title": "Przywróć przeciwników z PS1", "description": "Dodaje mumię, która pojawia się w wersji na PlayStation w City of Khamoon, pokój 25.\nZmiana tej opcji do poprawnego działania wymaga ponownego uruchomienia gry.", }, "gameplay.start_lara_hitpoints": { "title": "Startowe zdrowie Lary", "description": "Ustala wartość zdrowia, z którą Lara zaczyna każdy poziom.", }, "gameplay.target_mode": { "title": "Tryb blokady broni", "description": "Zmienia tryb, w jaki bronie blokują się na celach.\n\n- Całkowity: zawsze utrzymuje blokadę celu, nawet jeśli przeciwnik wyjdzie z pola widzenia lub zginie (tak jak w TR1-3).\n- Częściowy: utrzymuje blokadę celu, jeśli przeciwnik wyjdzie z pola widzenia, ale traci ją, kiedy przeciwnik zginie.\n- Brak: traci blokadę celu, kiedy przeciwnik wyjdzie z pola widzenia lub zginie (tak jak w TR4+).", }, "gameplay.wall_glitch_mode": { "title": "Tryb glitchy ścian", "description": "Pozwala na używanie glitcha ściany z TR1 w TR2 - i odwrotnie, umożliwia również naprawienie wszystkich typów tego błędu.", }, "input.enable_buffering_func_keys": { "title": "Buforowanie (klawisze funkcyjne)", "description": "Włącza buforowanie klawisza F w celu precyzyjnej kontroli ruchu Lary (1 klatka). Ta funkcja pierwotnie istniała tylko w porcie TombATI (TR1).", }, "input.enable_buffering_inventory": { "title": "Buforowanie (ekwipunek)", "description": "Włącza buforowanie ekranu ekwipunku, aby uzyskać precyzyjną kontrolę nad ruchem Lary (2 klatki).", }, "input.enable_responsive_passport": { "title": "Responsywny paszport", "description": "Wyłącza blokowanie klawiszy podczas przewracania stron paszportu, zamiast tego kolejkując pożądaną nawigację.", }, "input.enable_tr3_sidesteps": { "title": "Ulepszone kroki w bok", "description": "Umożliwia chodzenie w bok w stylu TR3+, tj. shift+strzałki kierunkowe. Naturalnie, przyciski dedykowane do kroków w bok nadal będą działać.", }, "input.quick_guns_mode": { "title": "Szybkie klawisze broni", "description": "Kontroluje zachowanie klawiszy do szybkiego wyposażania broni.\n\n- Wyposaż: naciśnięcie klawisza spowoduje, że Lara wyposaży daną broń.\n- Wyposaż lub schowaj: to samo co powyżej; dodatkowo Lara schowa daną broń, jeśli ma ją aktualnie wyposażoną.", }, "language": { "title": "Język", "description": "Zmienia język tekstu interfejsu.", }, "rendering.anisotropy_filter": { "title": "Filtr anizotropowy", "description": "Zwiększa filtrowanie tekstur w dużej odległości od kamery.", }, "rendering.aspect_mode": { "title": "Proporcje ekranu", "description": "Wymusza określone proporcje obrazu w grze przez dodanie czarnych pasków po bokach (letterboxing).", }, "rendering.borders": { "title": "Ramka", "description": "Dodaje czarną ramkę wokół okna gry.", }, "rendering.enable_trapezoid_filter": { "title": "Filtr trapezoidalny", "description": "Poprawia rysowanie czworokątów, rozciągając teksturę między wszystkimi wierzchołkami zamiast dzielić je na dwa trójkąty.", }, "rendering.enable_vsync": { "title": "Synchronizacja pionowa", "description": "Włącza lub wyłącza synchronizację pionową.", }, "rendering.fps": { "title": "Klatki na sekundę", "description": "Ustawia liczbę klatek na sekundę w grze.", }, "rendering.lighting_contrast": { "title": "Kontrast oświetlenia", "description": "Zwiększa kontrast dla dynamicznych źródeł światła, takich jak flary i rozbłyski broni.", }, "rendering.screenshot_format": { "title": "Format zrzutów ekranu", "description": "Format pliku użyty do zapisu zrzutów ekranu.", }, "rendering.sprite_lock_mode": { "title": "Tryb blokady sprite'ów", "description": "Blokuje dane osie podczas wyświetlania sprite'ów na ekranie.\n\n- Brak: wyświetlaj sprite'y normalnie.\n- Obrót: blokuje oś obrotu – przydatne tylko w trybie fotograficznym.\n- Obrót i pochylenie: zapewnia, że sprite'y stoją pionowo i nie kładą się na ziemi przy patrzeniu z góry.\n- Perspektywa: blokuje osie obrotu i pochylenia oraz dodatkowo lekko obraca sprite'y w kierunku środka ekranu.", }, "rendering.texture_filter": { "title": "Filtr tekstur", "description": "Przełącza między gładkimi a pikselowymi teksturami w grze.", }, "rendering.ui_filter": { "title": "Filtr UI", "description": "Przełącza między gładką a pikselową grafiką interfejsu.", }, "rendering.upscaling_factor": { "title": "Współczynnik skalowania", "description": "Skaluje grę o ustalony współczynnik, zachowując pikselowy wygląd.", }, "rendering.upscaling_filter": { "title": "Filtr skalowania", "description": "Przełącza gładki lub pikselowy wygląd dla całego ekranu.", }, "ui.airbar_color": { "title": "Kolor paska tlenu", "description": "Kolor paska tlenu.", }, "ui.airbar_color_ps1": { "title": "Kolor paska tlenu", "description": "Kolor paska tlenu.", }, "ui.airbar_location": { "title": "Lokalizacja paska tlenu", "description": "Miejsce, w którym wyświetlany jest pasek tlenu.", }, "ui.ammo_counter_location": { "title": "\\{review}Lokalizacja licznika amunicji", "description": "\\{review}Miejsce, w którym wyświetlany jest licznik amunicji.", }, "ui.bar_look": { "title": "Wygląd pasków", "description": "Steruje wyglądem pasków życia i tlenu.", }, "ui.bar_scale": { "title": "Skala pasków", "description": "Zmienia rozmiar pasków zdrowia i tlenu.", }, "ui.enable_bar_flashing": { "title": "Migające paski", "description": "Sprawia, że paski zdrowia i tlenu Lary zaczynają migać, kiedy dany zasób jest na wyczerpaniu.", }, "ui.enable_smooth_bars": { "title": "Wygładzanie pasków", "description": "Sprawia, że paski życia i tlenu używają gładkich przejść kolorów.", }, "ui.enable_wraparound": { "title": "Cykliczne przewijanie", "description": "Zawijanie poza ostatni element spowoduje powrót na początek listy i na odwrót.", }, "ui.enemy_healthbar_color": { "title": "Kolor paska zdrowia wrogów", "description": "Kolor paska zdrowia przeciwników.", }, "ui.enemy_healthbar_color_allies": { "title": "Kolor paska sojuszników", "description": "Kolor paska zdrowia sojuszników. Wyświetlany w miejscu pasków zdrowia wrogów.", }, "ui.enemy_healthbar_color_allies_ps1": { "title": "Kolor paska sojuszników", "description": "Kolor paska zdrowia sojuszników. Wyświetlany w miejscu pasków zdrowia wrogów.", }, "ui.enemy_healthbar_color_ps1": { "title": "Kolor paska zdrowia wrogów", "description": "Kolor paska zdrowia przeciwników.", }, "ui.enemy_healthbar_location": { "title": "Lokalizacja paska wrogów", "description": "Miejsce, w którym wyświetlany jest pasek zdrowia przeciwników.", }, "ui.enemy_healthbar_show_mode": { "title": "Tryb paska wroga", "description": "Zmienia sposób wyświetlania paska zdrowia dla aktywnego przeciwnika.", }, "ui.exposurebar_color": { "title": "Kolor paska wychłodzenia", "description": "Kolor paska wychłodzenia podczas przebywania w zimnej wodzie.", }, "ui.exposurebar_color_ps1": { "title": "Kolor paska wychłodzenia", "description": "Kolor paska wychłodzenia podczas przebywania w zimnej wodzie.", }, "ui.exposurebar_location": { "title": "Położenie paska wychłodzenia", "description": "Miejsce, w którym wyświetlany jest pasek wychłodzenia.", }, "ui.healthbar_color": { "title": "Kolor paska zdrowia", "description": "Kolor paska zdrowia Lary.", }, "ui.healthbar_color_ps1": { "title": "Kolor paska zdrowia", "description": "Kolor paska zdrowia Lary.", }, "ui.healthbar_location": { "title": "Lokalizacja paska zdrowia", "description": "Miejsce, w którym wyświetlany jest pasek zdrowia Lary.", }, "ui.healthbar_poison_color": { "title": "Kolor paska zdrowia przy zatruciu", "description": "Kolor paska zdrowia, gdy Lara jest zatruta.", }, "ui.healthbar_poison_color_ps1": { "title": "Kolor paska zdrowia przy zatruciu", "description": "Kolor paska zdrowia, gdy Lara jest zatruta.", }, "ui.inventory_background_style": { "title": "Tło ekwipunku", "description": "Zmienia sposób wyświetlania tła dla pierścienia ekwipunku.\n\n- Ciemne: TR1 (PC).\n- Bardzo ciemne: TR1 (PS1).\n- Stałe: TR2 (PC).\n- Falujące: TR2 (PS1).\n- Czarno-białe: TR3.", }, "ui.inventory_fade_effects": { "title": "Efekty przejścia w ekwipunku", "description": "Włącza lub wyłącza efekty przejścia w pierścieniu ekwipunku. Wymaga opcji ogólnych efektów przejścia do działania.", }, "ui.menu_style": { "title": "Styl menu", "description": "Zmienia sposób rysowania menu.\n\n - PC: styl interfejsu odpowiada wersji na PC.\n - PS1: styl interfejsu odpowiada wersji na PlayStation 1.", }, "ui.pause_background_style": { "title": "Tło pauzy", "description": "Zmienia sposób wyświetlania tła dla ekranu pauzy.\n\n- Ciemne: TR1 (PC).\n- Bardzo ciemne: TR1 (PS1).\n- Stałe: TR2 (PC).\n- Falujące: TR2 (PS1).\n- Czarno-białe: TR3.", }, "ui.pause_fade_effects": { "title": "Efekty przejścia podczas pauzy", "description": "Włącza lub wyłącza efekty przejścia na ekranie pauzy. Wymaga opcji ogólnych efektów przejścia do działania.", }, "ui.pickup_scale": { "title": "Skala przedmiotów", "description": "Zmienia rozmiar przedmiotów animowanych w rogu ekranu, kiedy Lara coś podnosi.", }, "ui.show_bars": { "title": "Pokaż paski", "description": "Wyłącza wszystkie paski w grze, ukrywając informacje o zdrowiu Lary i innych zasobach.", }, "ui.show_pickups_overlay": { "title": "Powiadomienia o podniesieniu", "description": "Wyświetla przedmioty w prawym dolnym rogu, gdy Lara coś podnosi.", }, "ui.show_title_version": { "title": "\\{review}Tekst wersji tytułu", "description": "\\{review}Wyświetla ciąg wersji TRX w pierścieniu ekwipunku tytułu.", }, "ui.sprintbar_color": { "title": "Kolor paska sprintu", "description": "Kolor paska sprintu", }, "ui.sprintbar_color_ps1": { "title": "Kolor paska sprintu", "description": "Kolor paska sprintu", }, "ui.sprintbar_location": { "title": "Pozycja paska sprintu", "description": "Lokalizacja, w której wyświetlany jest pasek sprintu.", }, "ui.stats.show_ammo": { "title": "\\{review}Trafienia/amunicja użyta", "description": "\\{review}Pokazuje wiersz amunicji w statystykach poziomu.", }, "ui.stats.show_crystals": { "title": "\\{review}Kryształy", "description": "\\{review}Pokazuje wiersz kryształów w statystykach poziomu.", }, "ui.stats.show_deaths": { "title": "\\{review}Śmierci", "description": "\\{review}Pokazuje śmierci Lary w statystykach kompasu i statystykach poziomu. Liczba śmierci jest aktualizowana w aktualnie załadowanym zapisie zaraz po śmierci Lary.", }, "ui.stats.show_distance_travelled": { "title": "\\{review}Przebyta odległość", "description": "\\{review}Pokazuje wiersz przebytej odległości w statystykach poziomu.", }, "ui.stats.show_kills": { "title": "\\{review}Zabójstwa", "description": "\\{review}Pokazuje wiersz zabójstw w statystykach poziomu.", }, "ui.stats.show_level_header": { "title": "\\{review}Licznik poziomu", "description": "\\{review}Pokazuje aktualny numer poziomu na górze statystyk poziomu.", }, "ui.stats.show_medipacks_used": { "title": "\\{review}Użyte apteczki", "description": "\\{review}Pokazuje wiersz użytych apteczek w statystykach poziomu.", }, "ui.stats.show_pickups": { "title": "\\{review}Zebrane przedmioty", "description": "\\{review}Pokazuje wiersz zebranych przedmiotów w statystykach poziomu.", }, "ui.stats.show_secrets": { "title": "\\{review}Odnalezione sekrety", "description": "\\{review}Pokazuje wiersz odnalezionych sekretów w statystykach poziomu.", }, "ui.stats.show_time_taken": { "title": "\\{review}Czas", "description": "\\{review}Pokazuje wiersz czasu w statystykach poziomu.", }, "ui.stats.show_totals": { "title": "\\{review}Pokaż sumy", "description": "\\{review}Pokazuje sumy obok statystyk, gdy jest to możliwe. Sekrety pozostają niezmienione przez to ustawienie.", }, "ui.stats.style": { "title": "\\{review}Styl statystyk", "description": "\\{review}Kontroluje sposób wyświetlania okna statystyk.\n\n- Prosty: pokazuje prostszy układ bez ramki.\n- Z obramowaniem: pokazuje układ z ramką.", }, "ui.stats_background_style": { "title": "Tło statystyk", "description": "Zmienia sposób wyświetlania tła dla statystyk na końcu poziomu.\n\n- Ciemne: TR1 (PC).\n- Bardzo ciemne: TR1 (PS1).\n- Stałe: TR2 (PC).\n- Falujące: TR2 (PS1).\n- Czarno-białe: TR3.", }, "ui.stats_fade_effects": { "title": "Efekty przejścia w statystykach", "description": "Włącza lub wyłącza efekty przejścia na ekranach statystyk. Wymaga opcji ogólnych efektów przejścia do działania.", }, "ui.text_scale": { "title": "Skala tekstu", "description": "Zmienia rozmiar tekstu interfejsu.", }, "visuals.blood_effects": { "title": "\\{review}Efekty krwi", "description": "\\{review}Kontroluje kolory iskier krwi.\n\n- Wyłączone: nie pokazuje iskier krwi.\n- Różowy: domyślny w niemieckich wersjach PC TR3.\n- Czerwony: domyślny we wszystkich innych wersjach detalicznych.", }, "visuals.camera_mode": { "title": "Tryb kamery", "description": "Zmienia zachowanie kamery podczas niektórych akcji, np. kiedy Lara używa kluczy lub dźwigni.", }, "visuals.enable_3d_pickups": { "title": "Znajdźki 3D", "description": "Rysuje przedmioty do podniesienia jako trójwymiarowe modele zamiast płaskich grafik.", }, "visuals.enable_braid": { "title": "Warkocz Lary", "description": "Włącza warkocz Lary.", }, "visuals.enable_breeze": { "title": "Wiatr", "description": "Włącza efekt wiatru na warkoczu Lary w miejscach, które to obsługują.", }, "visuals.enable_exit_fade_effects": { "title": "Przejścia przy wyjściu z gry", "description": "Włącza efekt płynnego przejścia przy wychodzeniu z gry do pulpitu.", }, "visuals.enable_fade_effects": { "title": "Efekty przejścia", "description": "Włącza efekty płynnych przejść, np. między grafikami napisów końcowych, lub przy włączaniu ekranu ekwipunku i ekranu pauzy.", }, "visuals.enable_fire_lighting": { "title": "Światło dookoła ogni", "description": "Włącza generowanie dynamicznego oświetlenia obok aktywnych płomieni.", }, "visuals.enable_footprints": { "title": "Ślady stóp", "description": "Włącza wyświetlanie śladów stóp Lary na niektórych powierzchniach w obsługiwanych poziomach.", }, "visuals.enable_glide_cameras": { "title": "Płynne kamery", "description": "Włącza efekt płynnych przejść dla kamer skierowanych na Larę. Wyłączenie tej opcji spowoduje natychmiastową zmianę widoku na Larę.", }, "visuals.enable_gun_lighting": { "title": "Efekty świetlne broni", "description": "Włącza dynamiczne oświetlenie przy strzelaniu z broni i eksplozjach.", }, "visuals.enable_ps1_crystals": { "title": "Fioletowe kryształy", "description": "Kryształy zapisu będą rysowane z fioletowym odcieniem, bardziej podobnym do odcienia z PS1.", }, "visuals.enable_reflections": { "title": "Odbicia", "description": "Włącza efekt lustrzanych odbić na niektórych obiektach.", }, "visuals.enable_responsive_mesh_tint": { "title": "Reaktywne barwienie wody", "description": "Nadaje częściom ciała Lary wodne zabarwienie tylko wtedy, gdy znajdują się pod wodą (styl TR3). Przy wyłączonej opcji, zabarwienie jest nadawane całemu ciału Lary (styl TR1/2).", }, "visuals.enable_shotgun_flash": { "title": "Rozbłyski ze strzelby", "description": "Rysuje płomienie podczas strzelania ze strzelby, jak w przypadku innych broni.", }, "visuals.enable_skybox": { "title": "Niebo", "description": "Włącza niebo w obsługiwanych poziomach.", }, "visuals.enable_weather": { "title": "Pogoda", "description": "Włącza efekty pogodowe w obsługiwanych poziomach.", }, "visuals.fix_animated_sprites": { "title": "Napraw animacje sprite'ów", "description": "Przywraca glonom efekt animacji w wodnych poziomach.", }, "visuals.fix_item_rots": { "title": "Napraw rotacje przedmiotów", "description": "Naprawia problemy z oryginalnej gry zw. z nieprawidłowo obróconymi przedmiotami podczas korzystania z opcji rysowania znajdziek jako obiekty 3D.", }, "visuals.fix_texture_issues": { "title": "Napraw tekstury", "description": "Naprawia błędy oryginalnej gry zw. z brakującymi lub nieprawidłowymi teksturami.", }, "visuals.fog_color": { "title": "Kolor mgły", "description": "Kolor mgły.", }, "visuals.fog_end": { "title": "Koniec mgły", "description": "Ustala odległość w sektorach, w której gra przestaje rysować poziom.", }, "visuals.fog_start": { "title": "Początek mgły", "description": "Ustala odległość w sektorach, w której gra zaczyna ograniczać widoczność.", }, "visuals.fog_transparency": { "title": "Przezroczystość mgły", "description": "Obiekty oddalone od kamery będą się stawać coraz bardziej przezroczyste.", }, "visuals.fov": { "title": "Kąt widzenia", "description": "Kąt widzenia w stopniach. Większe wartości poszerzają pole widzenia, mniejsze je zmniejszają.", }, "visuals.game_brightness": { "title": "Jasność", "description": "Zmienia jasność gry.", }, "visuals.gamma": { "title": "Gamma", "description": "Reguluje krzywą gamma. Wyższe wartości oznaczają jaśniejsze oświetlenie. Wartość 2,5 wyłącza modulację.", }, "visuals.lara_outfit": { "title": "Strój Lary", "description": "Zmienia strój Lary. Opcja domyślna zachowuje standardowe zmiany między poziomami; w przeciwnym razie wybrany strój pozostaje aktywny, dopóki nie zostanie zmieniony ręcznie.", }, "visuals.shadow_type": { "title": "Kształt cieni", "description": "Wybiera sposób rysowania cieni obiektów.\n\n- Ośmiokąt: stare cienie z TR1 i TR2\n- Elipsa: zaokrąglone cienie\n- Sprite: cienie oparte na teksturach z TR3", }, "visuals.sunglasses_mode": { "title": "Okulary przeciwsłoneczne", "description": "\\{review}Zmienia styl okularów przeciwsłonecznych Lary. Uwaga: soczewki będą refleksyjne, jeśli odpowiednia opcja jest włączona.\n\n- Wyłączone: Lara nie będzie nosić okularów przeciwsłonecznych.\n- Nieprzezroczyste: Okulary przeciwsłoneczne Lary będą miały nieprzezroczyste soczewki.\n- Przezroczyste: Okulary przeciwsłoneczne Lary będą miały półprzezroczyste soczewki.", }, "visuals.ui_brightness": { "title": "Jasność interfejsu", "description": "Zmienia jasność interfejsu.", }, "visuals.water_color": { "title": "Kolor wody", "description": "Kolor wody.", } }, "objects": { "alarm_sound": { "name": "Alarm", }, "alligator": { "name": "Aligator", }, "alphabet": { "name": "Domyślna czcionka", }, "alphabet_small": { "name": "Mała czcionka", }, "amber_light": { "name": "Bursztynowe światło", }, "animating_1": { "name": "Animowany Obiekt 1", }, "animating_10": { "name": "\\{review}Animowany Obiekt 10", }, "animating_2": { "name": "Animowany Obiekt 2", }, "animating_3": { "name": "Animowany Obiekt 3", }, "animating_4": { "name": "Animowany Obiekt 4", }, "animating_5": { "name": "Animowany Obiekt 5", }, "animating_6": { "name": "Animowany Obiekt 6", }, "animating_7": { "name": "\\{review}Animowany Obiekt 7", }, "animating_8": { "name": "\\{review}Animowany Obiekt 8", }, "animating_9": { "name": "\\{review}Animowany Obiekt 9", }, "ape": { "name": [ "Małpa", "Goryl", ] }, "area_51_rocket": { "name": "\\{review}Rakieta Strefa 51", }, "area_51_rocket_blast": { "name": "\\{review}Wystrzał Rakiety Strefa 51", }, "area_51_rocket_support": { "name": "\\{review}Wsparcie Rakiety Strefa 51", }, "assault_digits": { "name": "Cyferki", }, "assault_target": { "name": "Tarcza", }, "atlantean_ground": { "name": "\\{review}Atlantydczyk Naziemny", }, "atlantean_shooter": { "name": "\\{review}Atlantydczyk (Strzelający)", }, "atlantean_winged": { "name": "\\{review}Skrzydlaty Atlantydczyk", }, "autos": { "name": "Pistolety automatyczne", }, "autos_ammo": { "name": "Amunicja do pistoletów automatycznych", }, "bacon_lara": { "name": "Klon Lary", }, "baldy": { "name": "Łysy", }, "bandit_1": { "name": [ "Najemnik 1", "Zamaskowany oprych 1", ] }, "bandit_2": { "name": [ "Najemnik 2", "Zamaskowany oprych 2", ] }, "bandit_2b": { "name": [ "Najemnik 3", "Zamaskowany oprych 3", ] }, "barracuda": { "name": "Barakuda", }, "bartoli": { "name": "Marco Bartoli", }, "bat": { "name": "Nietoperz", }, "bat_emitter": { "name": "Emiter nietoperzy", }, "beacon_light": { "name": "Światło sygnalizacyjne", }, "bear": { "name": "Niedźwiedź", }, "bell": { "name": "Dzwon", }, "big_bowl": { "name": "Miska z lawą", }, "big_eel": { "name": "Duży węgorz", }, "big_pod": { "name": "Duży kokon", }, "big_spider": { "name": "Gigantyczny pająk", }, "bird_guardian": { "name": "Ptak-potwór", }, "bird_tweeter_1": { "name": "Kapiąca woda", }, "bird_tweeter_2": { "name": "Śpiewające ptaki", }, "blade": { "name": "Ostrze na ścianie", }, "blood": { "name": "\\{review}Krew", }, "blood_pink": { "name": "\\{review}Krew (ocenzurowana)", }, "blue_light": { "name": "Niebieskie światło", }, "boat": { "name": "Łódź", }, "boat_bits": { "name": "Elementy łodzi", }, "body_part": { "name": "Część ciała", }, "bridge_flat": { "name": "Most płaski", }, "bridge_tilt_1": { "name": "Most pochylony 1", }, "bridge_tilt_2": { "name": "Most pochylony 2", }, "bubble_1": { "name": "Bańka 1", }, "bubble_2": { "name": "Bąbelki 2", }, "bubble_emitter": { "name": "Emiter baniek", }, "camera_target": { "name": "Obiekt kamery", }, "carcass": { "name": "Padlina", }, "ceiling_spikes": { "name": "Sufit z kolcami", }, "centaur": { "name": "Centaur", }, "centaur_statue": { "name": "Posąg", }, "civilian": { "name": "\\{review}Cywil", }, "claw_mutant": { "name": "\\{review}Szponiasty Mutant", }, "clock_chimes": { "name": "Zegar kryjówki Bartoliego", }, "cog_1": { "name": "Zębatka 1", }, "cog_2": { "name": "Zębatka 2", }, "cog_3": { "name": "Zębatka 3", }, "combat_end": { "name": "Koniec walki", }, "compass": { "name": "Rozgrywka", }, "compy": { "name": "Mały dinozaur", }, "controls": { "name": "Sterowanie", }, "copter": { "name": "Helikopter", }, "cowboy": { "name": "Kowboj", }, "crawler_mutant": { "name": "\\{review}Pełzający Mutant", }, "crocodile": { "name": "Krokodyl", }, "crow": { "name": "Kruk", }, "cult_1": { "name": "Zamaskowany zbir 1", }, "cult_1a": { "name": "Zamaskowany zbir 2", }, "cult_1b": { "name": "Zamaskowany zbir 3", }, "cult_2": { "name": "Nożownik", }, "cult_3": { "name": "Zbir z strzelbą", }, "cut_shotgun": { "name": "Animacja końcowa", }, "damocles_sword": { "name": "Miecz Damoklesa", }, "dart": { "name": "Strzała", }, "dart_effect": { "name": "Efekt strzały", }, "dart_emitter": { "name": "Wyzwalacz strzał", }, "desert_eagle": { "name": "Desert Eagle", }, "desert_eagle_ammo": { "name": "Amunicja do Desert Eagle", }, "detonator_box": { "name": "Skrzynka detonatora", }, "ding_dong": { "name": "Dzwonek do drzwi", }, "dino_mutant": { "name": [ "Dino mutant", "Zmutowany dinozaur", ] }, "disc": { "name": "Dysk", }, "disc_emitter": { "name": "Emiter dysków", }, "disposable_animating_1": { "name": "\\{review}Jednorazowa Animacja 1", }, "disposable_animating_10": { "name": "\\{review}Jednorazowa Animacja 10", }, "disposable_animating_2": { "name": "\\{review}Jednorazowa Animacja 2", }, "disposable_animating_3": { "name": "\\{review}Jednorazowa Animacja 3", }, "disposable_animating_4": { "name": "\\{review}Jednorazowa Animacja 4", }, "disposable_animating_5": { "name": "\\{review}Jednorazowa Animacja 5", }, "disposable_animating_6": { "name": "\\{review}Jednorazowa Animacja 6", }, "disposable_animating_7": { "name": "\\{review}Jednorazowa Animacja 7", }, "disposable_animating_8": { "name": "\\{review}Jednorazowa Animacja 8", }, "disposable_animating_9": { "name": "\\{review}Jednorazowa Animacja 9", }, "diver": { "name": "Nurek", }, "dog": { "name": [ "Pies", "Doberman", ] }, "door_1": { "name": "Drzwi 1", }, "door_2": { "name": "Drzwi 2", }, "door_3": { "name": "Drzwi 3", }, "door_4": { "name": "Drzwi 4", }, "door_5": { "name": "Drzwi 5", }, "door_6": { "name": "Drzwi 6", }, "door_7": { "name": "Drzwi 7", }, "door_8": { "name": "Drzwi 8", }, "dragon_back": { "name": "Tył smoka", }, "dragon_bones_1": { "name": "Brak opisu", }, "dragon_bones_2": { "name": "Przednie kości smoka", }, "dragon_bones_3": { "name": "Tylne kości smoka", }, "dragon_front": { "name": "Przód smoka", }, "drawbridge": { "name": "Most zwodzony", }, "dust": { "name": "Kurz", }, "dying_monk": { "name": "Umierający mnich", }, "dying_mutant": { "name": "\\{review}Umierający Mutant", }, "eagle": { "name": "Orzeł", }, "earthquake": { "name": "Trzęsienie ziemi", }, "eel": { "name": "Węgorz", }, "electric_cleaner": { "name": "\\{review}Elektryczny czyściciel", }, "electric_fence": { "name": "Ogrodzenie elektryczne", }, "electrical_light": { "name": "Światło elektryczne", }, "ember": { "name": "Żar", }, "ember_emitter": { "name": "Emiter żaru", }, "explosion_1": { "name": "Eksplozja 1", }, "explosion_2": { "name": "Eksplozja 2", }, "falling_block_1": { "name": "Spadający blok 1", }, "falling_block_2": { "name": "Spadający blok 2", }, "falling_block_3": { "name": "Spadający blok 3", }, "falling_ceiling_1": { "name": "Spadający sufit 1", }, "falling_ceiling_2": { "name": "Spadający sufit 2", }, "fire_head": { "name": "\\{review}Ognista Głowa", }, "fish_mutant": { "name": [ "Ryba mutant", "Zmutowana ryba", ] }, "flame": { "name": [ "Płomień", "Ogień", ] }, "flame_emitter": { "name": [ "Emiter płomieni", "Emiter ognia", ] }, "flame_emitter_big": { "name": "Emiter płomieni (duży)", }, "flame_emitter_jet": { "name": "Emiter płomieni (odrzut)", }, "flame_emitter_side": { "name": "Emiter płomieni (boczny)", }, "flame_emitter_small": { "name": "Emiter płomieni (mały)", }, "flare": { "name": "Flara", }, "flare_fire": { "name": "Iskry flary", }, "flares_box": { "name": "Paczka flar", }, "flickering_light": { "name": "\\{review}Migoczące Światło", }, "fuse_box": { "name": "\\{review}Skrzynka bezpieczników", }, "fx_reserved": { "name": "Szary dysk", }, "gamma": { "name": "Gamma", }, "gas_emitter_green": { "name": "\\{review}Emiter Gazu (Zielony)", }, "general": { "name": "Miniłódź podwodna", }, "globe": { "name": "Globus", }, "glow": { "name": "Poświata", }, "glow_reserved": { "name": "Poświata mapy", }, "gondola": { "name": "Gondola", }, "gong": { "name": "Gong", }, "gong_bonger": { "name": "Pałka do gongu", }, "graphics": { "name": "Grafika", }, "green_light": { "name": "Zielone światło", }, "grenade": { "name": "Granat", }, "grenade_launcher": { "name": "Wyrzutnia granatów", }, "grenade_launcher_ammo": { "name": "Granaty", }, "gun_flash": { "name": "Rozbłysk broni", }, "gun_shell": { "name": "Łuska do pistoletu", }, "harpoon_bolt": { "name": "Harpun", }, "harpoon_gun": { "name": "Wyrzutnia harpunów", }, "harpoon_gun_ammo": { "name": "Harpuny", }, "hook": { "name": "Hak", }, "hot_liquid": { "name": "Gorąca ciecz", }, "huskie": { "name": [ "Pies", "Huskie", ] }, "hybrid_mutant": { "name": "\\{review}Hybrydowy Mutant", }, "icicle": { "name": "Sopel lodu", }, "inv_background": { "name": "Tło menu", }, "jelly": { "name": "Meduza", }, "kayak": { "name": "Kajak", }, "key_1": { "name": "Klucz 1", }, "key_2": { "name": "Klucz 2", }, "key_3": { "name": "Klucz 3", }, "key_4": { "name": "Klucz 4", }, "key_hole_1": { "name": "Zamek 1", }, "key_hole_2": { "name": "Zamek 2", }, "key_hole_3": { "name": "Zamek 3", }, "key_hole_4": { "name": "Zamek 4", }, "kill_all_triggered": { "name": "Eliminacja wszystkich wyzwolonych obiektów", }, "killer_statue": { "name": "Posąg z mieczem", }, "lara": { "name": "Lara", }, "lara_alarm": { "name": "Alarm", }, "lara_autos": { "name": "Animacja pistoletów automatycznych", }, "lara_boat": { "name": "Animacja łodzi", }, "lara_desert_eagle": { "name": "Animacja Desert Eagle", }, "lara_extra": { "name": "Dodatkowa animacja Lary", }, "lara_flare": { "name": "Animacja flary", }, "lara_grenade": { "name": "Animacja wyrzutni granatów", }, "lara_hair": { "name": "Warkocz Lary", }, "lara_harpoon": { "name": "Animacja wyrzutni harpunów", }, "lara_m16": { "name": "Animacja M16", }, "lara_magnums": { "name": "Animacja magnumów", }, "lara_mp5": { "name": "Animacja MP5", }, "lara_pistols": { "name": "Animacja pistoletów", }, "lara_rocket": { "name": "Animacja wyrzutni rakiet", }, "lara_shotgun": { "name": "Animacja strzelby", }, "lara_skidoo": { "name": "Animacja skutera śnieżnego", }, "lara_uzis": { "name": "Animacja uzi", }, "larson": { "name": "Larson", }, "lava_wedge": { "name": "Lawa", }, "lead_bar": { "name": "Sztaba ołowiu", }, "lift": { "name": "Winda", }, "lightning_emitter": { "name": "Wyzwalacz piorunów", }, "lion": { "name": "Lew", }, "lioness": { "name": [ "Lwica", "Lew", ] }, "lizard": { "name": "Jaszczur", }, "m16": { "name": "M16", }, "m16_ammo": { "name": "Magazynki do M16", }, "m16_flash": { "name": "Rozbłysk M16", }, "magnums": { "name": "Magnumy", }, "magnums_ammo": { "name": "Amunicja do magnumów", }, "mesh_swap_1": { "name": "Zamiana obiektu 1", }, "mesh_swap_2": { "name": "Zamiana obiektu 2", }, "mesh_swap_3": { "name": "Zamiana obiektu 3", }, "midas_touch": { "name": "Ręka Midasa", }, "mine": { "name": "Mina wodna", }, "mine_cart": { "name": "\\{review}Wózek Kopalniany", }, "mini_copter": { "name": "Helikopter 2", }, "missile_atlantean_bomb": { "name": "Pocisk (Atlantydzka bomba)", }, "missile_atlantean_shard": { "name": "Pocisk (Atlantydzki odłamek)", }, "missile_flame": { "name": "Pocisk (Płomień)", }, "missile_harpoon": { "name": "Pocisk (Harpun)", }, "missile_knife": { "name": "Pocisk (Nóż)", }, "missile_poison": { "name": "Pocisk (Trucizna)", }, "monk_1": { "name": "Mnich 1", }, "monk_2": { "name": "Mnich 2", }, "monkey": { "name": "Małpa", }, "mounted_gun": { "name": "Działo przeciwpancerne", }, "mouse": { "name": "Szczur", }, "movable_block_1": { "name": "Blok do pchania 1", }, "movable_block_2": { "name": "Blok do pchania 2", }, "movable_block_3": { "name": "Blok do pchania 3", }, "movable_block_4": { "name": "Blok do pchania 4", }, "moving_bar": { "name": "Ruchomy pręt", }, "mp5": { "name": "MP5", }, "mp5_ammo": { "name": "Amunicja do MP5", }, "mp_1": { "name": "\\{review}MP 1", }, "mp_2": { "name": "\\{review}MP 2", }, "mummy": { "name": "Mumia", }, "natla": { "name": "Natla", }, "natla_gun": { "name": "Broń Natli", }, "on_off_light": { "name": "Światło włącz/wyłącz", }, "orca": { "name": "\\{review}Orka", }, "passport": { "name": "Gra", }, "patrol_dog": { "name": [ "Pies", "Pies patrolowy", ] }, "pda": { "name": "Rozgrywka", }, "pendulum_1": { "name": "Worek z piaskiem", }, "pendulum_2": { "name": "Huśtająca się skrzynia", }, "photo": { "name": "Dom Lary", }, "pickup_1": { "name": "Znajdźka 1", }, "pickup_2": { "name": "Znajdźka 2", }, "pickup_aid": { "name": "Pomoc do znajdziek", }, "pierre": { "name": "Pierre", }, "pirahnas": { "name": "Piranie", }, "pistols": { "name": "Pistolety", }, "pistols_ammo": { "name": "Magazynki do pistoletu", }, "player_1": { "name": "Aktor 1", }, "player_10": { "name": "Aktor 10", }, "player_2": { "name": "Aktor 2", }, "player_3": { "name": "Aktor 3", }, "player_4": { "name": "Aktor 4", }, "player_5": { "name": "Aktor 5", }, "player_6": { "name": "Aktor 6", }, "player_7": { "name": "Aktor 7", }, "player_8": { "name": "Aktor 8", }, "player_9": { "name": "Aktor 9", }, "pods": { "name": "Kokon", }, "poison_dart": { "name": "Zatruta strzałka", }, "poison_dart_emitter": { "name": "Wyrzutnia zatrutych strzałek", }, "portacabin": { "name": "Kabina", }, "power_saw": { "name": "Piła tarczowa", }, "prisoner": { "name": "\\{review}Więzień", }, "propeller_1": { "name": "Śmigło samolotu", }, "propeller_2": { "name": "Podwodne śmigło", }, "propeller_3": { "name": "Wentylator powietrza", }, "pulse_light": { "name": "Pulsujące światło", }, "puma": { "name": "Puma", }, "punk_1": { "name": "\\{review}Punk 1", }, "punk_2": { "name": "\\{review}Punk 2", }, "puzzle_1": { "name": "Przedmiot 1", }, "puzzle_2": { "name": "Przedmiot 2", }, "puzzle_3": { "name": "Przedmiot 3", }, "puzzle_4": { "name": "Przedmiot 4", }, "puzzle_done_1": { "name": "Miejsce na przedmiot 1 (ukończone)", }, "puzzle_done_2": { "name": "Miejsce na przedmiot 2 (ukończone)", }, "puzzle_done_3": { "name": "Miejsce na przedmiot 3 (ukończone)", }, "puzzle_done_4": { "name": "Miejsce na przedmiot 4 (ukończone)", }, "puzzle_hole_1": { "name": "Miejsce na przedmiot 1 (puste)", }, "puzzle_hole_2": { "name": "Miejsce na przedmiot 2 (puste)", }, "puzzle_hole_3": { "name": "Miejsce na przedmiot 3 (puste)", }, "puzzle_hole_4": { "name": "Miejsce na przedmiot 4 (puste)", }, "quad_bike": { "name": "Czterokołowiec", }, "quest_1": { "name": "Przedmiot Misji 1", }, "quest_2": { "name": "Przedmiot Misji 2", }, "quest_3": { "name": "Przedmiot Misji 3", }, "quest_4": { "name": "Przedmiot Misji 4", }, "raptor": { "name": [ "Raptor", "Welociraptor", ] }, "raptor_emitter": { "name": "Emiter raptorów", }, "rat": { "name": [ "Szczur", "Szczur lądowy", ] }, "red_light": { "name": "Czerwone światło", }, "rib": { "name": "\\{review}RIB", }, "ricochet": { "name": "Odbicie", }, "rocket": { "name": "Rakieta", }, "rocket_launcher": { "name": "Wyrzutnia rakiet", }, "rocket_launcher_ammo": { "name": "Rakiety", }, "rolling_ball_1": { "name": "Głaz 1", }, "rolling_ball_2": { "name": "Głaz 2", }, "rolling_ball_3": { "name": "Głaz 3", }, "rolling_ball_4": { "name": "Głaz 4", }, "rotating_laser": { "name": "\\{review}Obracające się lasery", }, "rx_worker_1": { "name": "\\{review}RX Pracownik 1", }, "rx_worker_2": { "name": "\\{review}RX Pracownik 2", }, "rx_worker_3": { "name": "\\{review}RX Pracownik 3", }, "save_crystal": { "name": "Kryształ do zapisu", }, "scion": { "name": "Scion", }, "scion_holder": { "name": "Piedestał na Scion", }, "secret_1": { "name": "Sekret 1", }, "secret_2": { "name": "Sekret 2", }, "secret_3": { "name": "Sekret 3", }, "security_guard": { "name": "\\{review}Ochroniarz", }, "security_laser_alarm": { "name": "\\{review}Laser zabezpieczający (Alarm)", }, "security_laser_deadly": { "name": "\\{review}Laser zabezpieczający (Śmiertelny)", }, "security_laser_killer": { "name": "\\{review}Laser zabezpieczający (Zabójczy)", }, "sentry_gun": { "name": "Działko automatyczne", }, "shadow": { "name": "Cień", }, "shark": { "name": "Rekin", }, "shiva": { "name": "Shiva", }, "shotgun": { "name": "Strzelba", }, "shotgun_ammo": { "name": "Naboje do strzelby", }, "shotgun_shell": { "name": "Łuska do strzelby", }, "skate_kid": { "name": [ "Kid", "Dzieciak na deskorolce", ] }, "skateboard": { "name": "Deskorolka", }, "skidoo_armed": { "name": "Czarny skuter śnieżny", }, "skidoo_driver": { "name": "Kierowca czarnego skutera śnieżnego", }, "skidoo_fast": { "name": "Czerwony skuter śnieżny", }, "skidoo_track": { "name": "Tor skutera śnieżnego", }, "skybox": { "name": "Niebo", }, "sliding_pillar": { "name": "Przesuwalny filar", }, "smashable_1": { "name": "Okno 1", }, "smashable_2": { "name": "Okno 2", }, "smashable_3": { "name": "Okno 3", }, "smashable_4": { "name": "Okno 4", }, "smoke_emitter_black": { "name": "Emiter dymu (czarny)", }, "smoke_emitter_white": { "name": "Emiter dymu (biały)", }, "snake": { "name": "Wąż", }, "snow_sprite": { "name": "Ślad skutera śnieżnego", }, "sophia": { "name": "\\{review}Sophia", }, "sound": { "name": "Dźwięk", }, "sphere_of_doom_1": { "name": "Eksplozja smoka 1", }, "sphere_of_doom_2": { "name": "Eksplozja smoka 2", }, "sphere_of_doom_3": { "name": "Eksplozja smoka 3", }, "spider": { "name": "Pająk", }, "spike_wall": { "name": "Ściana z kolcami", }, "spikes": { "name": "Kolce", }, "spinning_blade": { "name": "Toczące się ostrze", }, "splash_1": { "name": "Fale wodne 1", }, "splash_2": { "name": "Fale wody 2", }, "springboard": { "name": "Trampolina", }, "steam_emitter": { "name": "Emiter pary", }, "sthpac_mercenary": { "name": "Najemnik", }, "stopwatch": { "name": "Statystyki", }, "strobe_light": { "name": "Światło stroboskopowe", }, "swat_1": { "name": "\\{review}SWAT 1", }, "swat_2": { "name": "\\{review}SWAT 2", }, "swat_3": { "name": "\\{review}SWAT 3", }, "swinging_axe": { "name": "Siekiera", }, "switch_type_airlock": { "name": "Pokretło", }, "switch_type_button": { "name": [ "Przycisk", "Przełącznik", ] }, "switch_type_normal": { "name": [ "Dźwignia", "Wajcha", "Przełącznik", ] }, "switch_type_small": { "name": "Mały przełącznik", }, "switch_type_uw": { "name": [ "Podwodna dźwignia", "Podwodna wajcha", "Podwodny przełącznik", ] }, "switch_type_wheel": { "name": "Koło do obracania", }, "teeth_trap": { "name": "Zatrzaskujące się drzwi", }, "text_box": { "name": "Ramka interfejsu", }, "thors_handle": { "name": "Uchwyt Młota Thora", }, "thors_head": { "name": "Młot Thora", }, "tiger": { "name": "Tygrys", }, "tony": { "name": "Tony", }, "torso": { "name": "Gigantyczny mutant", }, "train": { "name": "\\{review}Pociąg", }, "trapdoor_1": { "name": "Zapadnia 1", }, "trapdoor_2": { "name": "Zapadnia 2", }, "trapdoor_3": { "name": "Zapadnia 3", }, "trex": { "name": [ "T-Rex", "Tyranozaur", ] }, "trex_alpha": { "name": "T-Rex alfa", }, "tribe_axeman": { "name": "Tubylec (topornik)", }, "tribe_boss": { "name": "Puna", }, "tribe_pipeman": { "name": "Tubylec (dmuchawka)", }, "tropical_fish": { "name": "Rybki", }, "twinkle": { "name": "Iskierki", }, "upv": { "name": "\\{review}Miniłódź podwodna", }, "uzis": { "name": "Uzi", }, "uzis_ammo": { "name": "Magazynki do uzi", }, "vole": { "name": [ "Nornik", "Szczur wodny", ] }, "vulture": { "name": "Sęp", }, "wasp_mutant": { "name": "\\{review}Osi Mutant", }, "wasp_mutant_emitter": { "name": "\\{review}Emiter Osiego Mutanta", }, "water_sprite": { "name": "Ślad łodzi", }, "waterfall": { "name": "Mgiełka wodospadu", }, "white_light": { "name": "Białe światło", }, "willard": { "name": "\\{review}Willard", }, "winston": { "name": "Winston", }, "winston_army": { "name": "Winston (umundurowany)", }, "wolf": { "name": "Wilk", }, "worker_1": { "name": "Oprych z bronią 1", }, "worker_2": { "name": "Oprych z bronią 2", }, "worker_3": { "name": "Oprych z kijem 1", }, "worker_4": { "name": "Oprych z kijem 2", }, "worker_5": { "name": "Oprych z miotaczem ognia", }, "xian_knight": { "name": "Rycerz Xian", }, "xian_knight_statue": { "name": "Posąg rycerza Xian", }, "xian_spearman": { "name": "Włócznik Xian", }, "xian_spearman_statue": { "name": "Posąg włócznika Xian", }, "yeti": { "name": "Yeti", }, "zipline_handle": { "name": "Uchwyt do zjazdu na linie", } } } ================================================ FILE: data/trx/ship/cfg/base_strings-ru.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "language_name": "Русский", "general": { "actions": { "examine_item": "Осмотреть", "hide_dialog": "Скрыть диалог", "reset_defaults": "Сброс", "rotate": "Повернуть", "unbind": "Очистить", "use_item": "Использовать", }, "config_presets": { "applied": "\\{review}Предустановка применена.", "confirm_description": "\\{review}Следующие настройки будут изменены:", "confirm_restart_note": "\\{review}Примечание: для вступления изменений в силу может потребоваться перезапуск игры.", "empty": "\\{review}Пресеты не найдены.", "no_changes": "\\{review}Нет изменений для применения.", "title_fmt": "\\{review}Применить предустановку %s?", }, "globe_select": { "area_1": "\\{review}Зона 1", "area_2": "\\{review}Зона 2", "area_3": "\\{review}Зона 3", "area_4": "\\{review}Зона 4", "area_5": "\\{review}Зона 5", "area_6": "\\{review}Зона 6", }, "inventory_ring": { "heading_adventure": "\\{review}Приключение", "heading_fmt": "%s", "heading_game_over": "Конец игры", "heading_inventory": "Инвентарь", "heading_items": "Предметы", "heading_option": "Настройки", "item_count_fmt": "\\{small}%s", "object_name_fmt": "%s", }, "misc": { "demo_mode": "Демо режим", "direction_keys_controller": "Крестовина", "direction_keys_keyboard": "Стрелки", "empty_slot_fmt": "- ПУСТОЙ СЛОТ %d -", "exit": "Выход", "hold_fmt": "удерж. %s", "off": "Выкл", "on": "Вкл", "pagination_nav": "%d / %d", }, "osd": { "ambiguous_input_2": "Неоднозначный ввод: %s и %s", "ambiguous_input_3": "Неоднозначный ввод: %s, %s, ...", "bilinear_filter_off": "\\{review}Билинейная фильтрация: выкл", "bilinear_filter_on": "\\{review}Билинейная фильтрация: вкл", "command_bad_invocation": "Недопустимый вызов: %s", "command_bool": "вкл, выкл", "command_decimal": "[десятичн.]", "command_integer": "[цел.]", "command_percent": "[цел.]", "command_unavailable": "Команда недоступна", "command_valid_values": "Корректные значения: %s", "complete_level": "Уровень завершён!", "config_option_get": "%s установлен на %s", "config_option_set": "%s изменён на %s", "config_option_unknown_option": "Неизвестная опция: %s", "current_health_get": "Текущее здоровье Лары: %d", "current_health_set": "Здоровье Лары установлено на %d", "door_close": "Сезам, закройся!", "door_open": "Сезам, откройся!", "door_open_fail": "Рядом с Ларой нет дверей", "flipmap_fail_already_off": "Перевёрнутая карта уже выключена", "flipmap_fail_already_on": "Перевёрнутая карта уже включена", "flipmap_off": "Перевёрнутая карта выключена", "flipmap_on": "Перевёрнутая карта включена", "fly_mode_off": "Режим полёта выключен", "fly_mode_on": "Режим полёта включен", "fps_counter_off": "Счётчик FPS выключен", "fps_counter_on": "Счётчик FPS включен", "give_item": "%s добавлен в инвентарь Лары", "give_item_all_guns": "Оружие к бою - Лара вооружена до зубов!", "give_item_all_keys": "Сюрприз! Все необходимые Ларе предметы теперь в её рюкзаке.", "give_item_cheat": "Рюкзак Лары только что стал намного тяжелее!", "heal_already_full_hp": "Лара уже полностью здорова", "heal_success": "Лара исцелена!", "invalid_cutscene": "Невалидная заставка", "invalid_demo": "С этим демо что-то не так...", "invalid_item": "Неизвестный предмет: %s", "invalid_level": "Невалидный уровень", "invalid_object": "Невалидный объект", "invalid_room": "Невалидная комната: %d. Валидные комнаты 0-%d", "invalid_sample": "Невалидный звук: %d", "kill": "Пока-пока!", "kill_all": "Бах! Все враги убиты: %d!", "kill_all_fail": "Ого! Врагов больше не осталось...", "kill_fail": "Поблизости нет врагов...", "lighting_contrast_fmt": "\\{review}Контраст освещения: %s", "load_game": "Игра загружена из слота %d", "load_game_fail_invalid_slot": "Неверный слот сохранения %d", "load_game_fail_unavailable_slot": "Слот сохранения %d недоступен", "object_not_found": "Объект не найден", "play_cutscene": "Загрузка заставки %d", "play_demo": "Загрузка демо %d", "play_level": "Загрузка %s", "pos_lara_missing": "Лара отсутствует", "pos_lara_pos_fmt": "Комната: %d\nПозиция: %.3f, %.3f, %.3f\nВращение: %.3f,%.3f,%.3f", "pos_level_fmt": "Уровень %d", "pos_level_fmt_cutscene": "Заставка %d", "pos_level_fmt_demo": "Демо %d", "quick_load": "\\{review}Быстро загруженный слот %d", "quick_load_fail_no_bound_slot": "\\{review}В данный момент не привязан ни один слот сохранения", "quick_load_fail_unavailable_bound_slot": "\\{review}Привязанный слот сохранения недоступен", "quick_save": "\\{review}Быстро сохранено", "quick_save_fail_no_slots": "\\{review}Слоты быстрого сохранения не настроены", "save_game": "Игра сохранена в слот %d", "save_game_fail_invalid_slot": "Неверный слот сохранения %d", "sound_available_samples": "Доступные звуки : %s", "sound_playing_sample": "Воспроизведение звука %d", "speed_get": "Текущая скорость: %d", "speed_set": "Скорость установлена на %d", "strings_failed": "Ошибка перезагрузки языковых файлов", "strings_reloaded": "Языковые файлы перезагружены", "textures_off": "\\{review}Текстуры: выключены", "textures_on": "\\{review}Текстуры: включены", "trapezoid_filter_off": "Четырёхугольная интерполяция выключена", "trapezoid_filter_on": "Четырёхугольная интерполяция включена", "ui_off": "Интерфейс выключен", "ui_on": "Интерфейс включен", "unknown_command": "Неизвестная команда: %s", "upscaling_factor": "Коэффициент масштабирования: x%d", "wireframe_mode_off": "\\{review}Режим каркаса: выкл", "wireframe_mode_on": "\\{review}Режим каркаса: вкл", }, "overlay": { "debug_animation": "Анимация: ", "debug_animation_state": "\\{review}Состояние: ", "debug_camera_pos": "\\{review}Положение камеры: ", "debug_camera_target": "\\{review}Цель камеры: ", "debug_immune": "Неуязвимость вкл.", "debug_position": "Позиция: ", "debug_rotation": "Поворот: ", "debug_speed": "Скорость: ", "item_count_fmt_pc": "\\{small}%s", "item_count_fmt_ps1": "\\{small}%s", }, "passport": { "delete_save": "\\{review}Удалить", "delete_save_confirm": "\\{review}Удалить это сохранение?", "delete_save_failed": "\\{review}Не удалось удалить выбранное сохранение.", "delete_save_no": "\\{review}Нет", "delete_save_yes": "\\{review}Да", "exit_game": "Выход", "exit_to_title": "Выйти в меню", "load_game": "Загрузить игру", "mode_new_game": "Новая игра", "mode_new_game_jp": "Японская новая игра", "mode_new_game_jp_plus": "Японская новая игра+", "mode_new_game_plus": "Новая игра+", "new_game": "Новая игра", "play_previous_levels": "\\{review}Играть в предыдущие уровни", "restart_level": "Перезапустить уровень", "save_game": "Сохранить игру", "save_slot_unsupported": "\\{review}Это сохранение не поддерживает эту функцию.", "select_level": "Выбор уровня", "select_mod": "\\{review}Выбрать игру", "select_mode": "Выбор режима", "select_save": "\\{review}Выбрать сохранение", "story_so_far": "История до сих пор...", "switch_mod": "\\{review}Сменить игру", }, "pause": { "are_you_sure": "Вы уверены?", "continue": "Остаться", "exit_to_title": "Выйти в меню?", "no": "Нет", "paused": "Пауза", "quit": "Выйти в меню", "yes": "Да", }, "photo_mode": { "advance_frame": "Дополнительный кадр", "camera_move_prompt": "Двигать камеру", "camera_reset_prompt": "Сбросить камеру", "camera_roll_prompt": "Вращать камеру", "camera_rotate_90_prompt": "Поворот на 90°", "camera_rotate_prompt": "Повернуть камеру", "change_lara_pose": "Изменить позу Лары", "fov_prompt": "Изменить угол обзора", "lara_move_prompt": "Двигать Лару", "lara_reset_prompt": "Сбросить Лару", "lara_roll_prompt": "Вращать Лару", "lara_rotate_90_prompt": "Поворот Лары на 90°", "lara_rotate_prompt": "Повернуть Лару", "snap_prompt": "Сделать скриншот", "title_camera_pos": "Фоторежим", "title_lara_pos": "Положение Лары", "toggle_help": "Вкл/выкл помощь", }, "settings": { "common": { "all_hidden_disclaimer": "\\{review}Настройки отключены для этого набора уровней.", "chroma": "Chroma", "edit_value": "Изменить значение", "frozen_option_disclaimer": "Этот параметр принудительно установлен редактором уровней и не может быть изменён.", "hue": "Оттенок", "lightness": "Светлота", "restore_default": "По умолчанию", "toggle_help": "Показать помощь", }, "controls": { "backend": { "controller": "Контроллер", "keyboard": "Клавиатура", }, "customize": "Настроить управление", "layout": { "custom_1": "Пользовательская схема 1", "custom_2": "Пользовательская схема 2", "custom_3": "Пользовательская схема 3", "default": "По умолчанию", }, "tabs": { "basics": "Движение", "items": "Предметы", "misc": "Разное", "system": "Система", } }, "gameplay": { "tabs": { "controls": "Управление", "fixes": "Исправления", "general": "Основные", "mods": "Модификации", "presets": "\\{review}Пресеты", }, "title": "Игровой процесс", }, "graphic_settings": { "tabs": { "bars": "\\{review}Брусья", "rendering": "Графика", "stats": "\\{review}Статистика", "ui": "Интерфейс", "visuals": "Визуальные эффекты", }, "title": "Настройки изображения", }, "sound": { "tabs": { "misc": "\\{review}Разное", "volume": "\\{review}Громкость", }, "title": "Настройки звука", } }, "stats": { "ammo": "Попадания/выстрелы", "ammo_hits": "\\{review}Попадания", "ammo_used": "\\{review}Использовано боеприпасов", "assault_best_time_fmt": "\\{review}%s", "assault_finish": "\\{review}Завершить", "assault_no_times_set": "\\{review}Время не установлено", "assault_other_times_fmt": "\\{review}%s", "assault_title": "\\{review}ЛУЧШИЕ ВРЕМЕНА", "basic_fmt": "%d", "bonus_statistics": "Бонусная статистика", "crystals": "\\{review}Кристаллы", "deaths": "Смертей", "detail_fmt": "%d из %d", "distance_travelled": "Пройденное расстояние", "final_statistics": "Финальная статистика", "gym_assault_course": "\\{review}Полоса препятствий", "gym_racetrack_course": "\\{review}Гоночная трасса", "kills": "Убито", "level": "Уровень", "medipacks_used": "Использовано аптечек", "none": "\\{review}Нет", "pickups": "Предметы", "secrets": "Секреты", "time_taken": "Время", } }, "console": { "cmd": { "braid": { "help": "Переключает косу Лары.", }, "cheats": { "help": "Вкл/выкл внутриигровые читы.", }, "clear": { "help": "\\{review}Очищает видимые журналы консоли.", }, "debug": { "help": "Переключает визуальную отладочную информацию.", }, "drain": { "help": "Осушает текущую комнату, удаляет воду.", }, "end_level": { "help": "Завершает текущий уровень.", }, "exit": { "help": "Выходит из игры.", }, "flipmap": { "help": "Переключает переворачивание карты.", }, "flood": { "help": "Затапливает текущую комнату водой.", }, "fly": { "help": "Переключает режим полета.", }, "fps": { "help": "Изменяет значение FPS.", }, "give": { "help": "Добавляет указанный предмет в инвентарь Лары.", "invalid_secret": "Невалидный секрет: %s (валидные секреты: %s)", "secret_given": "Добавлен секрет %s", "secret_list": "Собрано секретов: %d из %d (%s)", "secret_none": "Собрано секретов: %d из %d", "secret_taken": "Удалён секрет %s", }, "give_secret": { "help": "Перечисляет секреты Лары, или добавляет/удаляет секрет по номеру.", }, "heal": { "help": "Восстанавливает здоровье Лары.", }, "help": { "help": "Показывает справку по всем командам или подробную справку по одной.", "list": "Доступные команды:", }, "hp": { "help": "Устанавливает здоровье Лары на указанное значение.", }, "immune": { "help": "Переключает неуязвимость (в некоторых ситуациях Лара всё ещё может умереть).", "off": "Лара теперь уязвима", "on": "Теперь Лара неуязвима для повреждений", }, "inf_sprint": { "help": "\\{review}Переключает бесконечный бег.", "off": "\\{review}Лара больше не может бегать вечно", "on": "\\{review}Лара теперь может бегать вечно", }, "kill": { "help": "Убивает ближайших врагов.", }, "lighting": { "help": "Переключает освещение.", }, "load": { "help": "\\{review}Загружает игру из указанного слота сохранения или из быстрого сохранения.", }, "lua": { "help": "Выполняет заданную строку кода Lua.", "runtime_error": "Ошибка выполнения Lua: %s", "syntax_error": "Ошибка синтаксиса Lua: %s", }, "mod": { "help": "\\{review}Переключается на указанный мод и перезапускает игру.", }, "music": { "help": "Воспроизводит музыкальную дорожку с указанным идентификатором.", }, "play_cutscene": { "help": "Воспроизводит кат-сцену с указанным номером.", }, "play_demo": { "help": "Воспроизводит демо с указанным номером.", }, "play_gym": { "help": "Воспроизводит уровень Тренажёрный зал", }, "play_level": { "help": "Воспроизводит уровень с указанным именем или номером", }, "play_music": { "invalid_track": "Невалидная музыка", "stopped": "\\{review}Музыка остановлена", "track": "Воспроизведение музыки %d", }, "pos": { "help": "Показывает позицию Лары", }, "save": { "help": "\\{review}Сохраняет игру в указанный слот сохранения или в следующий слот быстрого сохранения.", }, "screenshot": { "help": "\\{review}Сохраняет скриншот на диск с возможным указанием пути.", }, "set": { "help": "Отображает или обновляет указанный параметр конфигурации", }, "sfx": { "help": "Воспроизводит звуковой эффект с указанным идентификатором", }, "spawn": { "fail": "\\{review}Не удалось создать запрошенный объект", "success": "\\{review}Запрошенный объект появился рядом с Ларой", }, "speed": { "help": "Изменяет скорость игры", }, "strings": { "help": "Перезагружает текущие языковые файлы с диска.", }, "teleport": { "item": "Телепортировано к объекту: %d", "item_fail": "Невозможно телепортировать к объекту: %d", "object": "Телепортировано к объекту: %s", "object_fail": "Невозможно телепортировать к объекту: %s", "pos": "Телепортировано в позицию: %.3f %.3f %.3f", "pos_fail": "Невозможно телепортировать в позицию: %.3f %.3f %.3f", "room": "Телепортировано в комнату: %d", "room_fail": "Невозможно телепортировать в комнату: %d", }, "textures": { "help": "\\{review}Переключает отображение текстур.", }, "title": { "help": "Возвращает на титульный экран", }, "tp": { "help": "Телепортирует Лару в указанную позицию или номер комнаты.", }, "trigger": { "help": "\\{review}Активирует или деактивирует предмет по идентификатору, имени предмета или имени объекта.", "invalid_item": "\\{review}Недопустимый предмет: %s", "no_match": "\\{review}Неизвестная цель: %s", "not_found": "\\{review}Совпадающие предметы не найдены для: %s", "triggered": "\\{review}Активированный предмет(ы): %s", "untriggered": "\\{review}Неактивированный предмет(ы): %s", }, "vsync": { "help": "Переключает вертикальную синхронизацию.", }, "weather": { "help": "\\{review}Изменяет текущий тип погоды.", "invalid": "\\{review}Неверная погода: %s (допустимо: %s)", "set": "\\{review}Погода установлена на %s", }, "winston": { "dead": "\\{review}Ваш дворецкий мёртв. Ты монстр!", "spawn_failed": "\\{review}Не удалось вызвать Уинстона", "spawned": "\\{review}Вызван Уинстон рядом с Ларой", "teleported": "\\{review}Вызван Уинстон рядом с Ларой", }, "wireframe": { "help": "Переключает рендеринг каркаса.", } } }, "dynamic": { "config_presets": { "tr1_pc": "\\{review}TR1 PC", "tr1_ps1": "\\{review}TR1 PS1", "tr2_pc": "\\{review}TR2 PC", "tr2_ps1": "\\{review}TR2 PS1", "tr3_pc": "\\{review}TR3 PC", "tr3_ps1": "\\{review}TR3 PS1", }, "enums": { "bar_look": { "tr1_pc": "TR1 PC", "tr2_pc": "TR2 PC", "tr2_ps1": "\\{review}TR2 PS1", "tr3_pc": "TR3 PC", "tr3_ps1": "TR3 PS1", }, "lara_outfit": { "default": "\\{review}По умолчанию", "golden_sophia": "\\{review}Золотая София", "sophia": "\\{review}София", "tr1_bacon_lara": "\\{review}Лара Бекон", "tr1_classic": "\\{review}TR1 Классический", "tr1_combo": "\\{review}TR1 Комбинированный", "tr1_golden_bacon_lara": "\\{review}Золотая Лара Бекон", "tr1_golden_lara": "\\{review}TR1 Золотая Лара", "tr1_gym": "\\{review}TR1 Тренировочный", "tr1_mauled": "\\{review}TR1 Искалеченная", "tr1_ngage": "\\{review}ТР1 Эн-Гейдж", "tr23_golden_lara": "\\{review}TR2/3 Золотая Лара", "tr2_bomber_jacket": "\\{review}Куртка-бомбер", "tr2_classic": "\\{review}TR2 Классический", "tr2_diving_suit": "\\{review}Водолазный костюм 1", "tr2_diving_suit_alpha": "\\{review}Водолазный костюм 2", "tr2_gym": "\\{review}TR2 Тренировочный", "tr2_robe": "\\{review}Платье", "tr2_vegas": "\\{review}Лас-Вегас", "tr3_antarctica": "\\{review}Антарктида", "tr3_catsuit": "\\{review}Кэтсьют", "tr3_classic": "\\{review}TR3 Классический", "tr3_gym": "\\{review}TR3 Тренировочный", "tr3_nevada": "\\{review}Невада", "tr3_south_pacific": "\\{review}Южный Тихий океан", } }, "mods": { "tr1": { "title": "\\{review}Tomb Raider I", }, "tr1-demo-pc": { "title": "\\{review}Демо Tomb Raider I", }, "tr1-ub": { "title": "\\{review}Незавершённое дело", }, "tr2": { "title": "\\{review}Tomb Raider II", }, "tr2-gm": { "title": "\\{review}Золотая маска", }, "tr3": { "title": "\\{review}Tomb Raider III", }, "tr3-la": { "title": "\\{review}Потерянный артефакт", } } }, "enums": { "ALLY_HOSTILITY_POLICY": { "ALLY_HOSTILITY_POLICY_INDIVIDUAL": "\\{review}Индивидуально", "ALLY_HOSTILITY_POLICY_SHARED": "\\{review}Общее", }, "ASPECT_MODE": { "ASPECT_MODE_16_10": "16:10", "ASPECT_MODE_16_9": "16:9", "ASPECT_MODE_4_3": "4:3", "ASPECT_MODE_ANY": "АВТО", }, "BACKGROUND_TYPE": { "BK_BLACK": "\\{review}Черный", "BK_IMAGE": "\\{review}Изображение", "BK_MONOCHROME": "\\{review}Монохром", "BK_MONOCHROME_COOL": "\\{review}Монохромный (холодный)", "BK_MONOCHROME_WARM": "\\{review}Монохромный (теплый)", "BK_NONE": "\\{review}Прозрачный", "BK_PATTERN_STATIC": "\\{review}Статический", "BK_PATTERN_WAVE": "\\{review}Волна", "BK_TRANSPARENT_DARK": "\\{review}Очень темно", "BK_TRANSPARENT_MEDIUM": "\\{review}Темно", }, "BAR_SHOW_MODE": { "BAR_SHOW_MODE_ALWAYS": "Всегда", "BAR_SHOW_MODE_BOSS_ONLY": "Только боссы", "BAR_SHOW_MODE_NEVER": "Никогда", }, "BILLBOARD_LOCK_MODE": { "BILLBOARD_LOCK_NONE": "\\{review}Нет", "BILLBOARD_LOCK_PERSPECTIVE": "\\{review}Перспектива", "BILLBOARD_LOCK_ROLL": "\\{review}Крен", "BILLBOARD_LOCK_ROLL_PITCH": "\\{review}Крен и тангаж", }, "BLOOD_EFFECTS": { "BLOOD_EFFECTS_DISABLED": "\\{review}Отключено", "BLOOD_EFFECTS_PINK": "\\{review}Розовый", "BLOOD_EFFECTS_RED": "\\{review}Красный", }, "CAMERA_MODE": { "CAMERA_MODE_TR1": "TR1", "CAMERA_MODE_TR2": "TR2", "CAMERA_MODE_TR3": "\\{review}TR3", }, "CREATURE_DROWN_POLICY": { "CREATURE_DROWN_POLICY_DEFAULT": "\\{review}По умолчанию", "CREATURE_DROWN_POLICY_NEVER": "\\{review}Никогда", "CREATURE_DROWN_POLICY_SUBMERGED": "\\{review}Погружены", }, "INPUT_BACKEND": { "INPUT_BACKEND_CONTROLLER": "\\{review}Контроллер", "INPUT_BACKEND_KEYBOARD": "\\{review}Клавиатура", }, "INPUT_ROLE": { "INPUT_ROLE_ACTION": "Действие", "INPUT_ROLE_CAMERA_BACK": "Камера назад", "INPUT_ROLE_CAMERA_DOWN": "Камера вниз", "INPUT_ROLE_CAMERA_FORWARD": "Камера вперёд", "INPUT_ROLE_CAMERA_LEFT": "Камера влево", "INPUT_ROLE_CAMERA_RESET": "\\{review}Сброс камеры", "INPUT_ROLE_CAMERA_RIGHT": "Камера вправо", "INPUT_ROLE_CAMERA_UP": "Камера вверх", "INPUT_ROLE_CHANGE_OUTFIT": "\\{review}Сменить костюм", "INPUT_ROLE_CHANGE_TARGET": "Изменить цель", "INPUT_ROLE_CROUCH": "\\{review}Присесть", "INPUT_ROLE_CYCLE_LIGHTING_CONTRAST": "\\{review}Цикл контраста освещения", "INPUT_ROLE_DOWN": "Назад", "INPUT_ROLE_DRAW_WEAPON": "Оружие", "INPUT_ROLE_ENTER_CONSOLE": "Консоль разработчика", "INPUT_ROLE_EQUIP_AUTOS": "Оснастить автоматические пистолеты", "INPUT_ROLE_EQUIP_DESERT_EAGLE": "\\{review}Оснастить Дезерт Игл", "INPUT_ROLE_EQUIP_GRENADE_LAUNCHER": "\\{review}Оснастить гранатомет", "INPUT_ROLE_EQUIP_HARPOON": "\\{review}Оснастить гарпуном", "INPUT_ROLE_EQUIP_M16": "\\{review}Оснастить M16", "INPUT_ROLE_EQUIP_MAGNUMS": "Магнумы", "INPUT_ROLE_EQUIP_MP5": "\\{review}Оснастить MP5", "INPUT_ROLE_EQUIP_PISTOLS": "Пистолеты", "INPUT_ROLE_EQUIP_ROCKET_LAUNCHER": "\\{review}Экипировать ракетную установку", "INPUT_ROLE_EQUIP_SHOTGUN": "Дробовик", "INPUT_ROLE_EQUIP_UZIS": "Узи", "INPUT_ROLE_FLY_CHEAT": "Чит Полёт", "INPUT_ROLE_FPS": "Показать FPS", "INPUT_ROLE_INVENTORY": "Инвентарь", "INPUT_ROLE_ITEM_CHEAT": "Чит Предметы", "INPUT_ROLE_JUMP": "Прыжок", "INPUT_ROLE_LEFT": "Влево", "INPUT_ROLE_LEVEL_SKIP_CHEAT": "Чит Пропуск уровня", "INPUT_ROLE_LOAD": "\\{review}Загрузить", "INPUT_ROLE_LOOK": "Обзор", "INPUT_ROLE_PAUSE": "Пауза", "INPUT_ROLE_QUICK_LOAD": "\\{review}Быстрая загрузка", "INPUT_ROLE_QUICK_SAVE": "\\{review}Быстрое сохранение", "INPUT_ROLE_RIGHT": "Вправо", "INPUT_ROLE_ROLL": "Кувырок", "INPUT_ROLE_SAVE": "\\{review}Сохранить", "INPUT_ROLE_SCREENSHOT": "\\{review}Скриншот", "INPUT_ROLE_SLOW": "Ходьба", "INPUT_ROLE_SPRINT": "\\{review}Бег", "INPUT_ROLE_STEP_LEFT": "Шаг влево", "INPUT_ROLE_STEP_RIGHT": "Шаг вправо", "INPUT_ROLE_SWITCH_BORDERS": "Изменить размер рамки", "INPUT_ROLE_SWITCH_UPSCALING": "Изменить коэффициент масштабирования", "INPUT_ROLE_TOGGLE_BILINEAR_FILTER": "\\{review}Переключить билинейную фильтрацию", "INPUT_ROLE_TOGGLE_FULLSCREEN": "\\{review}Переключить полноэкранный режим", "INPUT_ROLE_TOGGLE_PHOTO_MODE": "Фоторежим", "INPUT_ROLE_TOGGLE_TEXTURES": "\\{review}Переключить текстуры", "INPUT_ROLE_TOGGLE_TRAPEZOID_FILTER": "Вкл/выкл четырёхугольную интерполяцию", "INPUT_ROLE_TOGGLE_UI": "Вкл/выкл интерфейс", "INPUT_ROLE_TOGGLE_WIREFRAME": "\\{review}Переключить каркасный режим", "INPUT_ROLE_TURBO_CHEAT": "Турбо скорость", "INPUT_ROLE_UP": "Вперёд", "INPUT_ROLE_USE_BIG_MEDI": "Большая аптечка", "INPUT_ROLE_USE_FLARE": "\\{review}Вспышка", "INPUT_ROLE_USE_SMALL_MEDI": "Малая аптечка", }, "JUMP_LOCK_MODE": { "JUMP_LOCK_DISABLED": "Отключена", "JUMP_LOCK_LEGACY": "Оригинал", "JUMP_LOCK_TUNED": "Оптимально", }, "LIGHTING_CONTRAST": { "LIGHTING_CONTRAST_HIGH": "Высокая", "LIGHTING_CONTRAST_LOW": "Низкая", "LIGHTING_CONTRAST_MEDIUM": "Средняя", }, "LOADING_SCREENS_MODE": { "LOADING_SCREENS_ALWAYS": "\\{review}Всегда", "LOADING_SCREENS_DISABLED": "\\{review}Отключено", "LOADING_SCREENS_NEW_GAMES": "\\{review}Новые игры", }, "LOOK_MODE": { "LOOK_MODE_ENHANCED": "Расширенный", "LOOK_MODE_RESTRICTED": "Ограниченный", "LOOK_MODE_UNRESTRICTED": "Неограниченный", }, "MUSIC_LOAD_CONDITION": { "MUSIC_LOAD_CONDITION_ALWAYS": "Всегда", "MUSIC_LOAD_CONDITION_NEVER": "Никогда", "MUSIC_LOAD_CONDITION_NON_AMBIENT": "Кроме фона", }, "PROJECTILE_AREA_DAMAGE": { "PROJECTILE_AREA_DAMAGE_MULTI_SWEEP": "\\{review}Множественное сканирование", "PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP": "\\{review}Однократное сканирование", }, "QUICK_GUNS_MODE": { "QUICK_GUNS_MODE_DRAW_AND_HOLSTER": "Достать или убрать", "QUICK_GUNS_MODE_DRAW_ONLY": "Только достать", }, "SCREENSHOT_FORMAT": { "SCREENSHOT_FORMAT_JPEG": "JPG", "SCREENSHOT_FORMAT_PNG": "PNG", }, "SHADOW_TYPE": { "SHADOW_TYPE_CIRCLE": "\\{review}Круг", "SHADOW_TYPE_OCTAGON": "\\{review}Восьмиугольник", "SHADOW_TYPE_SPRITE": "\\{review}Спрайт", }, "STATS_STYLE": { "STATS_STYLE_BARE": "\\{review}Простой", "STATS_STYLE_BORDERED": "\\{review}С рамкой", }, "SUNGLASSES_MODE": { "SUNGLASSES_MODE_OFF": "\\{review}Выкл", "SUNGLASSES_MODE_OPAQUE": "\\{review}Непрозрачный", "SUNGLASSES_MODE_TRANSPARENT": "\\{review}Прозрачный", }, "TARGET_LOCK_MODE": { "TARGET_LOCK_MODE_FULL": "Полный захват", "TARGET_LOCK_MODE_NONE": "Без захвата", "TARGET_LOCK_MODE_SEMI": "Полузахват", }, "TEXTURE_FILTER": { "TEXTURE_FILTER_BILINEAR": "Билинейная", "TEXTURE_FILTER_POINT": "Точечная выборка", }, "UI_ELEMENT_LOCATION": { "UI_ELEMENT_LOCATION_BOTTOM_CENTER": "Внизу по центру", "UI_ELEMENT_LOCATION_BOTTOM_LEFT": "Внизу слева", "UI_ELEMENT_LOCATION_BOTTOM_RIGHT": "Внизу справа", "UI_ELEMENT_LOCATION_TOP_CENTER": "Вверху по центру", "UI_ELEMENT_LOCATION_TOP_LEFT": "Вверху слева", "UI_ELEMENT_LOCATION_TOP_RIGHT": "Вверху справа", }, "UI_STYLE": { "UI_STYLE_PC": "PC", "UI_STYLE_PS1": "PS1", }, "WALL_GLITCH_MODE": { "WALL_GLITCH_FIXED": "Исправлен", "WALL_GLITCH_TR1": "TR1", "WALL_GLITCH_TR2": "TR2", } }, "settings": { "audio.ambient_volume": { "title": "\\{review}Громкость окружения", "description": "\\{review}Регулирует громкость окружения.", }, "audio.cutscene_volume": { "title": "\\{review}Громкость катсцен", "description": "\\{review}Регулирует громкость катсцен в игре.", }, "audio.enable_lara_mic": { "title": "Микрофон рядом с Ларой", "description": "Устанавливает микрофон в положение Лары. Если эта функция отключена, микрофон будет в положении камеры.", }, "audio.enable_music_in_inventory": { "title": "\\{review}Воспроизвести музыку в инвентаре", "description": "\\{review}Позволяет звукам игры, атмосфере и музыке продолжать играть на экране инвентаря.", }, "audio.enable_music_in_menu": { "title": "Музыка в главном меню", "description": "Воспроизводить музыку в главном меню.", }, "audio.enable_pitched_sounds": { "title": "Разнообразные звуки", "description": "Случайным образом слегка изменять тон звуковых эффектов, чтобы разнообразить звучание игры.", }, "audio.enable_ps1_sfx": { "title": "\\{review}Замены звуковых эффектов PS1", "description": "\\{review}Включает определённые замены звуковых эффектов, используя эквиваленты с PS1.\n\n- Выстрелы из Узи (только TR1)\n- Звуки босых ног Лары (только TR2)", }, "audio.enable_underwater_anim_sfx": { "title": "Подводные звуковые эффекты", "description": "Позволяет управлять воспроизведением определенных анимационных звуковых эффектов (для таких объектов, как двери и люки), когда камера находится под водой.", }, "audio.fix_chainblock_secret_sound": { "title": "Исправить звук золотого ключа", "description": "Предотвращает неправильное воспроизведение секретного звука при использовании золотого ключа в Гробнице Тихокана.", }, "audio.fix_secrets_killing_music": { "title": "Исправить звук секрета", "description": "Исправляет ситуацию, когда звук нахождения секрета прерывает активную музыкальную дорожку.", }, "audio.fix_speeches_killing_music": { "title": "Исправить звук разговора", "description": "Исправляет ситуацию, когда звук разговора врагов прерывает активную музыкальную дорожку.", }, "audio.fmv_volume": { "title": "\\{review}Громкость FMV", "description": "\\{review}Регулирует громкость видеороликов.", }, "audio.inventory_ambient_volume": { "title": "\\{review}Громкость окружения (инвентарь)", "description": "\\{review}Регулирует громкость окружения в инвентаре.", }, "audio.inventory_music_volume": { "title": "\\{review}Громкость музыки (инвентарь)", "description": "\\{review}Регулирует громкость музыки в инвентаре.", }, "audio.load_music_triggers": { "title": "Исправить музыкальные триггеры", "description": "Загружает ранее запущенную одноразовую музыку, поэтому дорожки одноразовой музыки не воспроизводятся повторно.", }, "audio.master_volume": { "title": "\\{review}\\{icon music} Главная громкость", "description": "\\{review}Регулирует общую громкость в игре. Остальные настройки зависят от этой громкости.", }, "audio.music_load_condition": { "title": "Восстанавливать музыку", "description": "Загружать музыкальную дорожку, которая воспроизводилась во время сохранения игры.\n\n- Никогда: не восстанавливать музыкальные дорожки при загрузке.\n- Кроме фона: восстанавливать только дорожки, не связанные с фоновой музыкой.\n- Всегда: восстанавливать любые музыкальные дорожки.", }, "audio.music_volume": { "title": "\\{review}Громкость музыки", "description": "\\{review}Регулирует громкость музыки.", }, "audio.mute_out_of_focus": { "title": "Отключить звук при потере фокуса", "description": "Отключать всю музыку и звуковые эффекты, когда окно игры не активно.", }, "audio.sound_volume": { "title": "\\{icon sound} Громкость звуков", "description": "Громкость звуковых эффектов.", }, "audio.underwater_ambient_volume": { "title": "\\{review}Громкость окружения (под водой)", "description": "\\{review}Регулирует громкость окружения под водой.", }, "audio.underwater_music_volume": { "title": "\\{review}Громкость музыки (под водой)", "description": "\\{review}Регулирует громкость музыки под водой.", }, "debug.enable_endless_flare_time": { "title": "\\{review}Бесконечное время горения факелов", "description": "\\{review}Предотвращает затухание ручных факелов. Брошенные факелы по-прежнему будут гаснуть как обычно.", }, "debug.enable_endless_sprint": { "title": "\\{review}Бесконечный спринт", "description": "\\{review}Предотвращает усталость Лары при беге. Препятствия по-прежнему остановят её.", }, "gameplay.ally_hostility_policy": { "title": "\\{review}Политика враждебности союзников", "description": "\\{review}Управляет реакцией дружественных юнитов при получении урона.\n\n- Индивидуально: каждый союзник меняет враждебность самостоятельно (стиль TR3).\n- Общее: все союзники становятся враждебными вместе (стиль монаха из TR2).", }, "gameplay.camera_speed": { "title": "Скорость камеры", "description": "Изменяет скорость камеры в ручном режиме.", }, "gameplay.change_pierre_spawn": { "title": "Изменить режим возрождения Пьера", "description": "Заставляет только что возродившегося (убегающего) Пьера заменить уже существующего (убегающего) Пьера.", }, "gameplay.creature_drown_policy": { "title": "\\{review}Поведение утопления существ", "description": "\\{review}Определяет поведение наземных существ в водных помещениях.\n\n- Никогда: наземные существа никогда не тонут (стиль TR1).\n- По умолчанию: наземные существа тонут в воде глубиной 2 клика и более (стиль TR2/3).\n- Погружены: наземные существа тонут только при полном погружении.", }, "gameplay.disable_extra_guns": { "title": "\\{review}Удалить лишнее оружие", "description": "\\{review}Удаляет все оружие и боеприпасы из игры, кроме пистолетов (для испытаний только с пистолетами).", }, "gameplay.disable_healing_between_levels": { "title": "Постоянный урон", "description": "Не исцелять Лару в начале нового уровня (для прохождений No Heal).", }, "gameplay.disable_medpacks": { "title": "Убрать аптечки", "description": "Убрать из игры все аптечки (для прохождений No Meds).", }, "gameplay.disable_trex_collision": { "title": "Убрать коллизии с тираннозавром", "description": "Устраняет все столкновения с тираннозавром после его смерти. Это помогает, когда тело тираннозавра блокирует проход.", }, "gameplay.enable_ally_targeting": { "title": "Разрешить нацеливание на союзников", "description": "Позволяет Ларе нацеливаться на союзников, таких как монахи. Если этот параметр отключен, союзники будут неуязвимы для боеприпасов Лары.", }, "gameplay.enable_auto_item_selection": { "title": "Автоматический выбор предмета", "description": "Когда Лара нажимает кнопку действия возле замочной скважины или головоломки, и у неё в инвентаре есть соответствующий предмет, этот предмет будет предварительно выбран.", }, "gameplay.enable_back_slope_stumble": { "title": "\\{review}Спотыкание на склоне сзади", "description": "\\{review}Заставляет Лару споткнуться, если она отпрыгивает назад и позади находится склон (TR3). Если отключено, Лара резко останавливается, упираясь в склон (TR1/2).", }, "gameplay.enable_body_bags": { "title": "\\{review}Триггеры мешков для тел", "description": "\\{review}Включает удаление убитых врагов, когда Лара пересекает определённые триггеры в некоторых уровнях. Если отключено, мёртвые враги всегда будут отображаться.", }, "gameplay.enable_boulder_shake": { "title": "\\{review}Включить тряску при движении валуна", "description": "\\{review}Если включено, камера будет трястись, когда валун находится в движении.", }, "gameplay.enable_bouncy_grenades": { "title": "\\{review}Прыгающие гранаты", "description": "\\{review}Включает поведение гранат в стиле TR3: они отскакивают от стен и склонов и создают большую зону взрыва, но за счет уменьшенной скорости.", }, "gameplay.enable_cheats": { "title": "Читы", "description": "Включает различные читы:\n\n- L: немедленно завершить уровень.\n- I: дать Ларе всё оружие, запас боеприпасов и аптечек, а также все сюжетные предметы для текущего уровня.\n- O: включить режим полёта (плавание в воздухе).\n - Клавиша ХОДЬБА: выйти из режима полёта.\n - Клавиша ОРУЖИЕ: открыть ближайшую дверь (не работает в некоторых местах).", }, "gameplay.enable_cinematics": { "title": "\\{review}Скриптовые сцены", "description": "\\{review}Включает скриптовые сцены в начале некоторых уровней, где они предусмотрены.", }, "gameplay.enable_compass_stats": { "title": "Статистика уровня в компасе", "description": "Отображать статистику уровня при выборе компаса.", }, "gameplay.enable_console": { "title": "Консоль", "description": "Включает консоль разработчика.", }, "gameplay.enable_controlled_drops": { "title": "Контролируемое падение", "description": "Позволяет Ларе развернуться в воздухе и ухватиться за выступ, с которого она только что сошла, если удерживать клавишу действия во время падения.", }, "gameplay.enable_crawl_jump": { "title": "\\{review}Прыжок при выходе из лазов", "description": "\\{review}Позволяет Ларе выпрыгивать из лазов.", }, "gameplay.enable_crawl_tilt": { "title": "\\{review}Наклон при ползании", "description": "\\{review}Выравнивает поворот Лары по геометрии пола при ползании.", }, "gameplay.enable_crawling": { "title": "\\{review}Ползание", "description": "\\{review}Позволяет Ларе приседать и ползать.", }, "gameplay.enable_credits": { "title": "Включить титры", "description": "Отображать экраны с титрами после завершения игры. Не влияет на финальный экран статистики.", }, "gameplay.enable_crouch_roll": { "title": "\\{review}Перекат из приседа", "description": "\\{review}Позволяет Ларе делать перекат вперед из приседа по нажатию спринта.", }, "gameplay.enable_cutscenes": { "title": "Заставки", "description": "Воспроизводить внутриигровые заставки.", }, "gameplay.enable_demo": { "title": "Демо режим", "description": "Показывать демонстрации в главном меню.", }, "gameplay.enable_enemy_rotation": { "title": "Разнообразить угол наклона врагов", "description": "Применяет дополнительный случайный угол наклона к некоторым врагам при их инициализации.", }, "gameplay.enable_enhanced_saves": { "title": "Сохранить эффекты", "description": "Улучшает сохранения игры, благодаря чему графические эффекты, туман от водопада, излучатели пламени и многое другое сохраняется, а не исчезает при загрузке.", }, "gameplay.enable_fmv": { "title": "Видеоролики", "description": "Воспроизводить видеоролики.", }, "gameplay.enable_game_modes": { "title": "Выбор режима игры", "description": "Позволяет выбирать дополнительные режимы игры в меню паспорта новой игры.\n\n- Новая игра+: разблокирует всё оружие с бесконечным боезапасом; у врагов вдвое больше здоровья.\n- Японская новая игра: оружие наносит двойной урон и сигнальных ракет 8, а не 6.\n- Японская новая игра+: комбинация Новой игры+ и Японской новой игры.", }, "gameplay.enable_idle_pose_camera": { "title": "\\{review}Камера позы", "description": "\\{review}Настраивает камеру так, чтобы она была направлена на Лару во время анимации позы. Нажмите кнопку «осмотреться», чтобы сбросить камеру.", }, "gameplay.enable_inverted_look": { "title": "Инвертировать обзор", "description": "Инвертировать вертикальную ось во время обзора.", }, "gameplay.enable_item_examining": { "title": "Описание предметов", "description": "Для пользовательских уровней - позволяет отображать описания предметов в инвентаре, если автор уровня предоставил эти данные.", }, "gameplay.enable_jump_twists": { "title": "Акробатика", "description": "Позволяет делать прыжки с поворотами и сальто (нажмите кнопку КУВЫРОК во время прыжка и ныряния).", }, "gameplay.enable_killer_pushblocks": { "title": "\\{review}Включить смертельные толкаемые блоки", "description": "\\{review}Если включено, когда толкаемый блок падает с воздуха и приземляется на Лару, он убьет ее мгновенно. В противном случае Лара зацепится за верх блока и выживет.", }, "gameplay.enable_lean_jumping": { "title": "Компактные прыжки", "description": "Позволяет Ларе продвигаться вперед или назад дальше при выполнении нейтральных прыжков с нажатой соответствующей клавишей ввода.", }, "gameplay.enable_ledge_jumps": { "title": "\\{review}Прыжки с выступа", "description": "\\{review}Позволяет Ларе прыгнуть вверх или назад, когда она висит на уступе, если перед ней есть твёрдая поверхность, от которой можно оттолкнуться.", }, "gameplay.enable_legal": { "title": "Ролики правообладателей", "description": "Показывать логотипы и ролики правообладателей при запуске игры.", }, "gameplay.enable_manual_camera": { "title": "\\{review}Ручное управление камерой", "description": "\\{review}Включает клавиши управления камерой (\\{input camera_forward}\\{input camera_back}\\{input camera_left}\\{input camera_right}), используемые для управления камерой в режиме фотографии, для также вращения игровой камеры.", }, "gameplay.enable_neutral_twists": { "title": "Нейтральные перевороты", "description": "Позволяет Ларе переворачиваться в воздухе, выполняя нейтральный прыжок. Нажмите клавиши ПРЫЖОК и КУВЫРОК одновременно, не двигаясь с места.", }, "gameplay.enable_pickup_aids": { "title": "Помощь при подборе", "description": "Включить эффект прерывистого мерцания возле подбираемых предметов, чтобы выделить их присутствие.", }, "gameplay.enable_play_previous_levels": { "title": "\\{review}Играть в предыдущие уровни", "description": "\\{review}Включает функции \"Играть предыдущие уровни\" и \"История на данный момент...\" в меню выбора Новая игра.", }, "gameplay.enable_responsive_crawl": { "title": "\\{review}Отзывчивое ползание", "description": "\\{review}Включает улучшения по сравнению с оригинальной механикой ползания.\n\n- Позволяет быстрее возобновить ползание после остановки.\n- Позволяет переходить из бега/спринта в ползание без предварительной остановки.\n- Позволяет переходить из ползания в перекат в приседе (если включён) без необходимости сначала вручную приседать.\n- Позволяет поворачиваться в положении приседа.\n- Восстанавливает анимацию подбора предметов Ларой в положении ползания (кроме фальшфейеров).", }, "gameplay.enable_responsive_sprint": { "title": "\\{review}Реактивный бег", "description": "\\{review}Включает более отзывчивый режим бега для Лары.\n\n- позволяет начинать бег, когда у Лары есть энергия, а не только при полном запасе выносливости.\n- позволяет бежать по лестнице без прерывания обычной анимацией бега.", }, "gameplay.enable_save_crystals": { "title": "Кристаллы сохранения", "description": "Ограничить сохранения только началом уровней и кристаллами сохранения. Количество кристаллов сохранения на уровнях ограничено, они одноразовые, как и в версии для PS1. Изменение этой настройки потребует перезапуска уровня.", }, "gameplay.enable_slide_to_run": { "title": "Бег после скольжения", "description": "Позволяет Ларе начать бежать сразу после того, как она достигнет земли после скольжения вперёд по склону. Удерживайте кнопку ВПЕРЁД, чтобы активировать.", }, "gameplay.enable_slow_ledge_swing": { "title": "\\{review}Медленное раскачивание на уступе", "description": "\\{review}Позволяет Ларе медленно раскачиваться, ухватившись за очень тонкий уступ (стиль TR3). Если отключено, Лара слегка качнётся и затем перейдёт в неподвижное висячее положение (стиль TR1/2).", }, "gameplay.enable_smooth_wall_deflect": { "title": "Мягкое столкновение со стеной", "description": "Позволяет Ларе быстрее восстанавливаться после удара о стену, и клавиша направления удерживается вместе с клавишей Вперёд.", }, "gameplay.enable_soft_statics": { "title": "\\{review}Мягкое столкновение с сеткой", "description": "\\{review}Позволяет Ларе плавно двигаться вдоль статических мешей – как в TR4+ – вместо того чтобы резко останавливаться.", }, "gameplay.enable_sprint": { "title": "\\{review}Бег", "description": "\\{review}Позволяет Ларе бегать на спринте, как в TR3 и новее.", }, "gameplay.enable_step_roll_boost": { "title": "Ускорение подъёма", "description": "Позволяет Ларе одним щелчком подняться выше, если перекат нажат у края.", }, "gameplay.enable_swing_cancel": { "title": "Отмена раскачивания", "description": "Позволяет отменить анимацию раскачивания Лары на выступе, если отпустить и быстро схватиться за него снова.", }, "gameplay.enable_target_change": { "title": "Изменить цель", "description": "Позволяет менять цель в стиле TR4+. Нажмите кнопку «Изменить цель» во время прицеливания, чтобы сменить цель.", }, "gameplay.enable_timer_in_inventory": { "title": "Таймер в инвентаре", "description": "Заставляет внутриигровой таймер работать даже во время отображения инвентаря в игре.", }, "gameplay.enable_toggle_crouch": { "title": "\\{review}Переключение приседания", "description": "\\{review}Позволяет Ларе оставаться присевшей после однократного нажатия кнопки приседания. Нажмите кнопку приседания снова, чтобы встать.", }, "gameplay.enable_toggle_sprint": { "title": "\\{review}Переключение бега", "description": "\\{review}Позволяет Ларе продолжать бегать после однократного нажатия кнопки бега. Нажмите кнопку бега снова, чтобы остановиться.", }, "gameplay.enable_total_stats": { "title": "Экран финальной статистики", "description": "Включает экран общей статистики игры, который воспроизводится после титров.", }, "gameplay.enable_tr2_jumping": { "title": "Отзывчивые прыжки", "description": "\\{review}Позволяет Ларе прыгать в любой точке во время бега.", }, "gameplay.enable_tr2_swim_cancel": { "title": "Отзывчивая отмена плавания", "description": "Позволяет Lara быстрее останавливаться под водой при отпускании кнопки плавания.", }, "gameplay.enable_tr2_swimming": { "title": "Плавное плавание", "description": "Добавляет кривую ускорения скорости поворота Лары под водой для более плавного движения, как в оригинальной версии TR2+. Отключение этой опции увеличит скорость поворота Лары под водой, как в оригинальной версии TR1.", }, "gameplay.enable_uw_roll": { "title": "Переворот под водой", "description": "Позволяет Ларе переворачиваться под водой.", }, "gameplay.enable_wading": { "title": "Идти вброд", "description": "\\{review}Позволяет Ларе преодолевать неглубокую воду, а не застревать на поверхности воды.", }, "gameplay.enable_walk_to_items": { "title": "Анимированные взаимодействия", "description": "Лара подходит к предметам и переключателям, когда они находятся поблизости, вместо телепортации к ним.", }, "gameplay.fix_alligator_ai": { "title": "Исправить поведение аллигаторов", "description": "Исправляет ошибку, из-за которой аллигаторы не наносили урона, если Лара оставалась неподвижной в воде.", }, "gameplay.fix_bear_ai": { "title": "Исправить атаку медведя", "description": "Исправляет медвежью атаку, чтобы она не промахивалась по Ларе.", }, "gameplay.fix_bridge_collision": { "title": "Исправить коллизии мостов", "description": "Исправляет ошибки, из-за которых Лара не могла ухватиться за части некоторых мостов и невидимые стены на краю. Также исправляет проблемы столкновений с разводными мостами, люками и мостами, расположенными друг над другом, на склонах и у земли.", }, "gameplay.fix_descending_glitch": { "title": "Исправить хрупкий пол", "description": "Исправляет ошибку, когда при шагании в стороны или назад по разрушаемым плиткам, Лара немедленно спускалась на плитку под ней.", }, "gameplay.fix_flare_throw_priority": { "title": "Исправить приоритет сигнальной ракеты", "description": "Исправляет ошибку, из-за которой Лара отдавала приоритет броску использованной сигнальной ракеты, находясь в воздухе, из-за чего она не могла хвататься за уступы.", }, "gameplay.fix_floor_data_issues": { "title": "Исправить данные пола", "description": "Исправляет проблемы с данными/триггерами пола.", }, "gameplay.fix_free_flare_glitch": { "title": "Исправить бесплатную ракету", "description": "Исправляет возможность создания бесплатной сигнальной ракеты при нажатии на кнопку запуска ракеты во время подбора любого предмета.", }, "gameplay.fix_item_duplication_glitch": { "title": "Исправить дублирование предметов", "description": "Исправляет возможность дублирования использования ключевых предметов в инвентаре.", }, "gameplay.fix_lara_pickup_embed": { "title": "\\{review}Исправлен баг подбора", "description": "\\{review}Исправляет ошибку, из-за которой Лара иногда проскальзывала в стены при сборе предметов под водой, а также при сборе предметов над водой под крутыми наклонными потолками.", }, "gameplay.fix_m16_accuracy": { "title": "\\{review}Исправить точность M16/MP5", "description": "\\{review}Исправлять точность стрельбы из М16/MP5 во время бега Лары.", }, "gameplay.fix_monkey_pickup_priority": { "title": "\\{review}Исправить приоритет подбора обезьян", "description": "\\{review}Атакованные обезьяны будут отдавать приоритет ответной атаке, а не сбору аптечек и ключей.", }, "gameplay.fix_pipeman_aim": { "title": "\\{review}Прицел духовой трубки", "description": "\\{review}Исправляет проблему, из-за которой стрелок с духовой трубкой иногда не может правильно прицелиться в Лару.", }, "gameplay.fix_qwop_glitch": { "title": "Исправить QWOP-состояние", "description": "Исправляет странную анимацию бега, известную как QWOP-состояние, которое иногда возникает, когда Лара приземляется на пол во время прыжка.", }, "gameplay.fix_step_glitch": { "title": "Исправить глюк ступеней", "description": "Исправляет ошибку, из-за которой Лара иногда вдавливалась в стены рядом со ступенями, если она бежала по ним определенным образом.", }, "gameplay.fix_wade_wall_hit": { "title": "Исправить переход вброд", "description": "Исправлять ошибку, из-за которой Лара не реагировала на удары о стену во время перехода вброд.", }, "gameplay.fix_walk_run_jump": { "title": "Исправить прыжок после ходьбы-бега", "description": "Исправлять ошибку, из-за которой Лара иногда не могла прыгнуть сразу после перехода от анимации ходьбы к анимации бега.", }, "gameplay.fix_wall_geometry": { "title": "\\{review}Исправление геометрии стен", "description": "\\{review}Исправляет случаи в геометрии уровней OG, где наклоны внутри стен могут приводить к неточным расчётам высоты.", }, "gameplay.fix_water_exit": { "title": "Исправить выход из воды", "description": "Исправлять ошибку, из-за которой Лара могла напрямую перейти из водной комнаты в соседнюю сухую комнату или в сухую комнату ниже. Кроме того, это не позволит Ларе выбраться из воды на неустойчивые склоны.", }, "gameplay.harpoon_recoil": { "title": "Частота перезарядки гарпуна", "description": "Устанавливает частоту перезарядки гарпуна Ларой, исходя из текущего количества патронов. Например, если установлено значение 3, Ларе придётся перезаряжать гарпунное ружьё после каждого третьего выстрела. Установите значение 0, чтобы полностью отключить перезарядку.", }, "gameplay.idle_pose_timeout": { "title": "\\{review}Таймер ожидания", "description": "\\{review}Позволяет Ларе перейти в анимацию позы после заданного количества секунд бездействия. Установите 0, чтобы отключить.", }, "gameplay.jump_lock_mode": { "title": "Блокировка прыжков", "description": "Для отзывчивых прыжков: позволяет контролировать, через какое время после начала бега Лара может прыгать.\n\n- Оригинал: соответствует оригинальному времени TR2.\n- Оптимально: прыжок возможен на 2 кадра раньше.\n- Отключена: прыжок возможен сразу после анимации начала бега.", }, "gameplay.loading_screens": { "title": "Загрузочные экраны", "description": "\\{review}Управляет экранами загрузки перед загрузкой уровней.\n\n- Отключено: никогда не показывать экраны загрузки.\n- Всегда: показывать экраны загрузки.\n- Новые игры: пропускать экраны загрузки при загрузке сохранения.", }, "gameplay.look_mode": { "title": "Режим обзора", "description": "Позволяет контролировать, когда Лара может использовать обзор.\n\n- Ограниченный: обзор разрешён только когда Лара неподвижна и никогда под водой.\n- Расширенный: обзор разрешён во время большинства анимаций, за исключением таких, как толкание блока.\n- Неограниченный: взгляд разрешён в любое время во время обычного управления Ларой.", }, "gameplay.maximum_quick_save_slots": { "title": "\\{review}Количество слотов быстрого сохранения", "description": "\\{review}Изменяет количество доступных слотов быстрого сохранения.", }, "gameplay.maximum_save_slots": { "title": "Количество слотов сохранения", "description": "Изменяет количество доступных слотов сохранения.", }, "gameplay.pause_on_focus_lost": { "title": "\\{review}Пауза при потере фокуса", "description": "\\{review}Останавливает игровой процесс, когда окно игры теряет фокус.", }, "gameplay.projectile_area_damage": { "title": "\\{review}Урон по области от снарядов", "description": "\\{review}Управляет тем, как распространяется область действия для Ракетной установки и Гранатомёта.\n\n- Однократное сканирование: поведение TR1 и TR2.\n- Множественное сканирование: поведение TR3.\n\nОпция множественного сканирования часто приводит к двойному урону по отдельным врагам.", }, "gameplay.remember_gun_status": { "title": "Помнить оружие между уровнями", "description": "\\{review}При переходе на новый уровень Лара запоминает, каким оружием она пользовалась в последний раз на предыдущем уровне. Если отключить, Лара вернётся к пистолетам в кобуре.", }, "gameplay.restore_ps1_enemies": { "title": "Восстановить врагов PS1", "description": "\\{review}Добавляет мумию, которая появляется в версии для PlayStation в городе Хамун, комната 25.\nДля применения изменений потребуется перезапустить игру.", }, "gameplay.start_lara_hitpoints": { "title": "Начальное здоровье Лары", "description": "Устанавливает значение здоровья Лары в начале каждого уровня.", }, "gameplay.target_mode": { "title": "Режим захвата цели", "description": "Изменяет поведение захвата цели оружием.\n\n- Полный захват: всегда удерживать захват цели, даже если враг уходит из поля зрения или умирает (TR1-3).\n- Полузахват: удерживать захват цели, если враг уходит из поля зрения, но терять захват, если враг умирает.\n- Без захвата: терять захват цели, если враг уходит из поля зрения или умирает (TR4+).", }, "gameplay.wall_glitch_mode": { "title": "Режим глюка стены", "description": "Позволяет использовать поведение глюка стены TR1 в TR2 и наоборот; Исправлен: исправлять все типы глюков стены.", }, "input.enable_buffering_func_keys": { "title": "\\{review}Буферизация (F-клавиши)", "description": "\\{review}Включает буферизацию клавиши F (1 кадр) для точного управления движением Лары. Эта функция изначально присутствует только в порте TombATI (TR1).", }, "input.enable_buffering_inventory": { "title": "\\{review}Буферизация (инвентарь)", "description": "\\{review}Включает буферизацию инвентаря (2 кадра) для точного управления движением Лары.", }, "input.enable_responsive_passport": { "title": "Отзывчивый паспорт", "description": "Отключает блокировку ввода данных пользователем при перелистывании страниц паспорта, вместо этого планирует перелистывание страниц.", }, "input.enable_tr3_sidesteps": { "title": "Улучшенные шаги", "description": "Позволяет делать шаги в сторону в стиле TR3+, например, Shift+стрелки. Отдельные кнопки для шагов в сторону по-прежнему будут работать.", }, "input.quick_guns_mode": { "title": "Быстрые клавиши оружия", "description": "Управляет поведением клавиш быстрого выбора оружия.\n\n- Только достать: нажатие клавиши заставит Лару вытащить назначенное оружие.\n- Достать или убрать: Лара достаёт назначенное оружие, либо, если оно уже есть у неё в руках, убирает его.", }, "language": { "title": "Язык", "description": "Изменить язык интерфейса.", }, "rendering.anisotropy_filter": { "title": "Анизотропный фильтр", "description": "Улучшает фильтрацию текстур на расстоянии.", }, "rendering.aspect_mode": { "title": "Соотношение сторон", "description": "Принудительно устанавливает пропорции игры с помощью Letterbox.", }, "rendering.borders": { "title": "Рамка", "description": "Добавляет черные рамки вокруг игрового окна.", }, "rendering.enable_trapezoid_filter": { "title": "Четырёхугольная интерполяция", "description": "Исправляет отображение четырёхугольников.", }, "rendering.enable_vsync": { "title": "Вертикальная синхронизация", "description": "Включает или выключает вертикальную синхронизацию.", }, "rendering.fps": { "title": "FPS", "description": "Устанавливает количество кадров в секунду в игре.", }, "rendering.lighting_contrast": { "title": "Контрастность динамического освещения", "description": "Увеличивает контрастность динамических источников света, таких как вспышки и выстрелы.", }, "rendering.screenshot_format": { "title": "Формат снимка экрана", "description": "Формат снимка экрана.", }, "rendering.sprite_lock_mode": { "title": "\\{review}Режим блокировки спрайтов", "description": "\\{review}Управляет осями, которые блокируются при отображении спрайтов на экране.\n\n- Нет: отображать спрайты в обычном виде.\n- Крен: блокировать ось крена – полезно только в режиме фото.\n- Крен и тангаж: обеспечивать, чтобы спрайты стояли вертикально и не лежали на земле при взгляде сверху.\n- Перспектива: блокировать оси крена и тангажа, а также слегка поворачивать спрайты к центру экрана.", }, "rendering.texture_filter": { "title": "Фильтрация текстур", "description": "Переключает между сглаженными и пикселизировнными игровыми текстурами.", }, "rendering.ui_filter": { "title": "Фильтрация интерфейса", "description": "Переключает между сглаженными и пикселизированными текстурами элементов интерфейса.", }, "rendering.upscaling_factor": { "title": "Коэффициент масштабирования", "description": "Увеличивает масштаб игры на заданный коэффициент, сохраняя пикселизированный вид.", }, "rendering.upscaling_filter": { "title": "Фильтр масштабирования", "description": "Переключает сглаженный или пиксельный вид для всего экрана.", }, "ui.airbar_color": { "title": "Цвет шкалы воздуха", "description": "Цвет шкалы воздуха.", }, "ui.airbar_color_ps1": { "title": "Цвет шкалы воздуха", "description": "Цвет шкалы воздуха.", }, "ui.airbar_location": { "title": "Положение шкалы воздуха", "description": "Место, где отображается шкала воздуха.", }, "ui.ammo_counter_location": { "title": "\\{review}Расположение счетчика боеприпасов", "description": "\\{review}Место, где отображается счетчик боеприпасов.", }, "ui.bar_look": { "title": "\\{review}Внешний вид панелей", "description": "\\{review}Управляет визуальным отображением панелей интерфейса.", }, "ui.bar_scale": { "title": "Размер шкалы", "description": "Изменяет размер шкал здоровья, воздуха и врагов.", }, "ui.enable_bar_flashing": { "title": "\\{review}Мигающие полосы", "description": "\\{review}Заставляет полосы здоровья и кислорода Лары мигать, когда один из ресурсов на исходе.", }, "ui.enable_smooth_bars": { "title": "Сглаженные шкалы", "description": "Использовать плавные цветовые переходы на шкалах здоровья, воздуха и врагов.", }, "ui.enable_wraparound": { "title": "Закольцевать список", "description": "Переходить к началу списка при достижении последнего пункта", }, "ui.enemy_healthbar_color": { "title": "Цвет шкалы здоровья врагов", "description": "Цвет шкалы здоровья противников.", }, "ui.enemy_healthbar_color_allies": { "title": "Цвет шкалы здоровья союзников", "description": "Цвет шкалы здоровья союзников. Отображается на месте шкалы здоровья врагов.", }, "ui.enemy_healthbar_color_allies_ps1": { "title": "Цвет шкалы здоровья союзников", "description": "Цвет шкалы здоровья союзников. Отображается на месте шкалы здоровья врагов.", }, "ui.enemy_healthbar_color_ps1": { "title": "Цвет шкалы здоровья врагов", "description": "Цвет шкалы здоровья противников.", }, "ui.enemy_healthbar_location": { "title": "Положение шкалы здоровья врагов", "description": "Место, где отображается шкала здоровья противника.", }, "ui.enemy_healthbar_show_mode": { "title": "Шкала здоровья врагов", "description": "Отображать индикатор здоровья для активного противника.", }, "ui.exposurebar_color": { "title": "\\{review}Цвет шкалы воздействия", "description": "\\{review}Цвет шкалы воздействия холодной воды.", }, "ui.exposurebar_color_ps1": { "title": "\\{review}Цвет шкалы воздействия", "description": "\\{review}Цвет шкалы воздействия холодной воды.", }, "ui.exposurebar_location": { "title": "\\{review}Расположение шкалы воздействия", "description": "\\{review}Место, где отображается шкала воздействия холодной воды.", }, "ui.healthbar_color": { "title": "Цвет шкалы здоровья", "description": "Цвет шкалы здоровья.", }, "ui.healthbar_color_ps1": { "title": "Цвет шкалы здоровья", "description": "Цвет шкалы здоровья.", }, "ui.healthbar_location": { "title": "Положение шкалы здоровья", "description": "Место, где отображается шкала здоровья Лары.", }, "ui.healthbar_poison_color": { "title": "\\{review}Цвет полосы здоровья при отравлении", "description": "\\{review}Цвет полосы здоровья, когда Лара отравлена.", }, "ui.healthbar_poison_color_ps1": { "title": "\\{review}Цвет полосы здоровья при отравлении", "description": "\\{review}Цвет полосы здоровья, когда Лара отравлена.", }, "ui.inventory_background_style": { "title": "\\{review}Фон инвентаря", "description": "\\{review}Изменяет способ отображения фона для кольца инвентаря.\n\n- Темный: TR1 (PC).\n- Очень темный: TR1 (PS1).\n- Статичный: TR2 (PC).\n- Волна: TR2 (PS1).\n- Монохромный: TR3.", }, "ui.inventory_fade_effects": { "title": "\\{review}Эффекты затухания инвентаря", "description": "\\{review}Тонкая настройка включения или отключения эффектов затухания в кольце инвентаря в игре. Для работы требуется включенная опция Эффекты затухания.", }, "ui.menu_style": { "title": "Стиль меню", "description": "Изменияет стиль меню.\n\n - PC: стиль меню соответствует версии для ПК.\n - PS1: стиль меню соответствует версии для PlayStation.", }, "ui.pause_background_style": { "title": "\\{review}Пауза фона", "description": "\\{review}Изменяет способ отображения фона для экрана паузы.\n\n- Темный: TR1 (PC).\n- Очень темный: TR1 (PS1).\n- Статичный: TR2 (PC).\n- Волна: TR2 (PS1).\n- Монохромный: TR3.", }, "ui.pause_fade_effects": { "title": "\\{review}Эффекты затухания паузы", "description": "\\{review}Тонкая настройка включения или отключения эффектов затухания на экране паузы. Для работы требуется включенная опция Эффекты затухания.", }, "ui.pickup_scale": { "title": "\\{review}Масштаб подбора", "description": "\\{review}Изменяет размер предметов, анимированных в интерфейсе, когда Лара что-то подбирает.", }, "ui.show_bars": { "title": "\\{review}Показать полосы", "description": "\\{review}Отключает все игровые полосы, скрывая информацию о здоровье Лары и других ресурсах (для сложных прохождений).", }, "ui.show_pickups_overlay": { "title": "Отображение подбора", "description": "Показывает предметы в правом нижнем углу, когда Лара что-то подбирает.", }, "ui.show_title_version": { "title": "\\{review}Текст версии заголовка", "description": "\\{review}Отображает строку версии TRX в кольце инвентаря заголовка.", }, "ui.sprintbar_color": { "title": "\\{review}Цвет индикатора бега", "description": "\\{review}Цвет полоски спринта.", }, "ui.sprintbar_color_ps1": { "title": "\\{review}Цвет индикатора бега", "description": "\\{review}Цвет полоски спринта.", }, "ui.sprintbar_location": { "title": "\\{review}Расположение индикатора бега", "description": "\\{review}Расположение, где отображается полоска спринта.", }, "ui.stats.show_ammo": { "title": "\\{review}Попадания/использовано боеприпасов", "description": "\\{review}Показывает строку боеприпасов в статистике уровня.", }, "ui.stats.show_crystals": { "title": "\\{review}Кристаллы", "description": "\\{review}Показывает строку с кристаллами в статистике уровня.", }, "ui.stats.show_deaths": { "title": "\\{review}Смерти", "description": "\\{review}Показывает количество смертей Лары в статистике компаса и в статистике уровня. Количество смертей обновляется в текущем сохранении сразу после смерти Лары.", }, "ui.stats.show_distance_travelled": { "title": "\\{review}Пройденное расстояние", "description": "\\{review}Показывает строку пройденного расстояния в статистике уровня.", }, "ui.stats.show_kills": { "title": "\\{review}Убийства", "description": "\\{review}Показывает строку убийств в статистике уровня.", }, "ui.stats.show_level_header": { "title": "\\{review}Счетчик уровней", "description": "\\{review}Показывает номер текущего уровня в верхней части статистики уровня.", }, "ui.stats.show_medipacks_used": { "title": "\\{review}Использованные аптечки", "description": "\\{review}Показывает строку использованных аптечек в статистике уровня.", }, "ui.stats.show_pickups": { "title": "\\{review}Подобранные предметы", "description": "\\{review}Показывает строку подобранных предметов в статистике уровня.", }, "ui.stats.show_secrets": { "title": "\\{review}Найденные секреты", "description": "\\{review}Показывает строку найденных секретов в статистике уровня.", }, "ui.stats.show_time_taken": { "title": "\\{review}Затраченное время", "description": "\\{review}Показывает строку затраченного времени в статистике уровня.", }, "ui.stats.show_totals": { "title": "\\{review}Показывать итоги", "description": "\\{review}Показывает итоги рядом со статистикой, когда это применимо. Секреты не затрагиваются этой настройкой.", }, "ui.stats.style": { "title": "\\{review}Стиль статистики", "description": "\\{review}Управляет отображением диалога статистики.\n\n- Простой: показывает упрощенный макет без рамок.\n- С рамкой: показывает макет в рамке.", }, "ui.stats_background_style": { "title": "\\{review}Фон статистики", "description": "\\{review}Изменяет способ отображения фона для статистики в конце уровня.\n\n- Темный: TR1 (PC).\n- Очень темный: TR1 (PS1).\n- Статичный: TR2 (PC).\n- Волна: TR2 (PS1).\n- Монохромный: TR3.", }, "ui.stats_fade_effects": { "title": "\\{review}Эффекты затухания статистики", "description": "\\{review}Тонкая настройка включения или отключения эффектов затухания на экране статистики в конце уровня. Для работы требуется включенная опция Эффекты затухания.", }, "ui.text_scale": { "title": "Размер текста", "description": "Изменяет размер текста интерфейса.", }, "visuals.blood_effects": { "title": "\\{review}Эффекты крови", "description": "\\{review}Управляет цветами искр крови.\n\n- Отключено: искры крови не отображаются.\n- Розовый: стандартный цвет в немецких версиях TR3 для ПК.\n- Красный: стандартный цвет во всех остальных розничных версиях.", }, "visuals.camera_mode": { "title": "Режим камеры", "description": "Регулирует поведение камеры во время таких действий, как нажатие клавиш.", }, "visuals.enable_3d_pickups": { "title": "3D-предметы", "description": "Отображать 3D-модели вместо спрайтов для подбираемых предметов.", }, "visuals.enable_braid": { "title": "Коса Лары", "description": "Показывать косу Лары", }, "visuals.enable_breeze": { "title": "Эффект ветра", "description": "Добавляет эффект ветра на косе Лары в соответствующих комнатах.", }, "visuals.enable_exit_fade_effects": { "title": "Затухание при выходе", "description": "Включает эффекты затухания при выходе из игры на рабочий стол.", }, "visuals.enable_fade_effects": { "title": "Плавные переходы", "description": "Добавляет плавные переходы, например, между экранами инвентаря и паузы, или экранами титров.", }, "visuals.enable_fire_lighting": { "title": "Свет от огня", "description": "Создаёт динамическое освещение рядом с активным пламенем.", }, "visuals.enable_footprints": { "title": "\\{review}Следы", "description": "\\{review}Включает отображение следов Лары на определённых поверхностях в поддерживаемых уровнях.", }, "visuals.enable_glide_cameras": { "title": "\\{review}Плавные камеры", "description": "\\{review}Включает эффект плавного перемещения для фиксированных камер, направленных на Лару, с использованием сглаженной кривой скорости. Если отключено, такие камеры будут мгновенно переключаться на вид, направленный на Лару.", }, "visuals.enable_gun_lighting": { "title": "Свет от выстрелов", "description": "\\{review}Включает динамическое освещение для выстрелов и взрывов.", }, "visuals.enable_ps1_crystals": { "title": "Оттенок кристалла PS1", "description": "Кристаллы сохранения будут отображаться с фиолетовым оттенком, больше похожим на кристаллы PS1.", }, "visuals.enable_reflections": { "title": "Отражения", "description": "Показывать отражения на определенных объектах.", }, "visuals.enable_responsive_mesh_tint": { "title": "\\{review}Адаптивная тонировка мешей", "description": "\\{review}Позволяет отрисовывать отдельные меши Лары с водной тонировкой, если они сами находятся под водой (стиль TR3). В противном случае, если Лара находится в воде, все её меши будут отрисовываться с тонировкой (стиль TR1/2).", }, "visuals.enable_shotgun_flash": { "title": "Вспышки из дробовика", "description": "Показывать вспышки при выстреле из дробовика, как и из другого оружия.", }, "visuals.enable_skybox": { "title": "Скайбоксы", "description": "Показывать текстуры неба и горизонта на поддерживаемых уровнях.", }, "visuals.enable_weather": { "title": "\\{review}Погода", "description": "\\{review}Включает отображение погодных эффектов в поддерживаемых уровнях.", }, "visuals.fix_animated_sprites": { "title": "Исправить анимацию спрайтов", "description": "\\{review}Исправляет оригинальные спрайты подводных растений, чтобы они правильно анимировались в водных зонах.", }, "visuals.fix_item_rots": { "title": "Исправить поворот предметов", "description": "Исправляет проблемы с некоторыми неправильно повёрнутыми предметами при использовании опции 3D-предметов.", }, "visuals.fix_texture_issues": { "title": "Исправить ошибки текстур", "description": "Исправляет проблемы с отсутствующими или неверными текстурами/сетками.", }, "visuals.fog_color": { "title": "\\{review}Цвет тумана", "description": "\\{review}Цвет тумана.", }, "visuals.fog_end": { "title": "Конец тумана", "description": "Расстояние (в тайлах), на котором туман полностью скрывает обзор.", }, "visuals.fog_start": { "title": "Начало тумана", "description": "Расстояние (в тайлах), на котором начинает появляться туман.", }, "visuals.fog_transparency": { "title": "\\{review}Прозрачность тумана", "description": "\\{review}Включить смешивание удаленной геометрии с полностью прозрачными поверхностями.", }, "visuals.fov": { "title": "\\{review}Поле зрения", "description": "\\{review}Угол обзора в градусах. Большие значения расширяют поле зрения, меньшие — сужают его.", }, "visuals.game_brightness": { "title": "Яркость", "description": "Изменяет яркость игры.", }, "visuals.gamma": { "title": "\\{review}Гамма", "description": "\\{review}Регулирует гамма-кривую. Более высокие значения означают более яркое освещение. Значение 2.5 соответствует цветам по умолчанию.", }, "visuals.lara_outfit": { "title": "\\{review}Костюм Лары", "description": "\\{review}Изменяет внешний вид Лары. При выборе По умолчанию сохраняются обычные смены костюма между уровнями; в противном случае выбранный костюм будет использоваться до ручного изменения.", }, "visuals.shadow_type": { "title": "\\{review}Форма теней", "description": "\\{review}Выбирает способ отображения теней объектов.\n\n- Восьмиугольник: старые тени из TR1 и TR2\n- Круг: круглые тени\n- Спрайт: тени на основе текстур из TR3", }, "visuals.sunglasses_mode": { "title": "\\{review}Солнцезащитные очки Лары", "description": "\\{review}Изменяет стиль солнцезащитных очков Лары. Примечание: линзы будут отражающими, если соответствующая опция включена.\n\n- Выкл: Лара не будет носить солнцезащитные очки.\n- Непрозрачный: У солнцезащитных очков Лары будут непрозрачные линзы.\n- Прозрачный: У солнцезащитных очков Лары будут полупрозрачные линзы.", }, "visuals.ui_brightness": { "title": "Яркость интерфейса", "description": "Изменяет яркость интерфейса.", }, "visuals.water_color": { "title": "Цвет воды", "description": "Цвет воды.", } }, "objects": { "alarm_sound": { "name": "\\{review}Тревога", }, "alligator": { "name": "Аллигатор", }, "alphabet": { "name": "\\{review}Шрифт по умолчанию", }, "alphabet_small": { "name": "\\{review}Маленький шрифт", }, "amber_light": { "name": "\\{review}Янтарный свет", }, "animating_1": { "name": "\\{review}Анимация объекта 1", }, "animating_10": { "name": "\\{review}Анимация объекта 10", }, "animating_2": { "name": "\\{review}Анимация объекта 2", }, "animating_3": { "name": "\\{review}Анимация объекта 3", }, "animating_4": { "name": "\\{review}Анимация объекта 4", }, "animating_5": { "name": "\\{review}Анимация объекта 5", }, "animating_6": { "name": "\\{review}Анимация объекта 6", }, "animating_7": { "name": "\\{review}Анимация объекта 7", }, "animating_8": { "name": "\\{review}Анимация объекта 8", }, "animating_9": { "name": "\\{review}Анимация объекта 9", }, "ape": { "name": "Обезьяна", }, "area_51_rocket": { "name": "\\{review}Ракета Зоны 51", }, "area_51_rocket_blast": { "name": "\\{review}Взрыв ракеты Зоны 51", }, "area_51_rocket_support": { "name": "\\{review}Поддержка ракеты Зоны 51", }, "assault_digits": { "name": "\\{review}Цифры штурма", }, "assault_target": { "name": "\\{review}Цель атаки", }, "atlantean_ground": { "name": "\\{review}Наземный Атлант", }, "atlantean_shooter": { "name": "\\{review}Атлант (Стреляющий)", }, "atlantean_winged": { "name": "\\{review}Крылатый Атлант", }, "autos": { "name": "\\{review}Автоматические пистолеты", }, "autos_ammo": { "name": "\\{review}Обоймы для автоматических пистолетов", }, "bacon_lara": { "name": "Двойник Лары", }, "baldy": { "name": "Лысый дядька", }, "bandit_1": { "name": [ "\\{review}Наёмник 1", "\\{review}Маскированный головорез 1", ] }, "bandit_2": { "name": [ "\\{review}Наёмник 2", "\\{review}Маскированный головорез 2", ] }, "bandit_2b": { "name": [ "\\{review}Наёмник 3", "\\{review}Маскированный головорез 3", ] }, "barracuda": { "name": "\\{review}Барракуда", }, "bartoli": { "name": "\\{review}Марко Бартоли", }, "bat": { "name": "Летучая мышь", }, "bat_emitter": { "name": "\\{review}Излучатель летучих мышей", }, "beacon_light": { "name": "\\{review}Маячковый свет", }, "bear": { "name": "Медведь", }, "bell": { "name": "\\{review}Колокол", }, "big_bowl": { "name": "\\{review}Чаша с лавой", }, "big_eel": { "name": "\\{review}Большой угорь", }, "big_pod": { "name": "Большое гнездо", }, "big_spider": { "name": "\\{review}Гигантский паук", }, "bird_guardian": { "name": "\\{review}Птице-монстр", }, "bird_tweeter_1": { "name": "\\{review}Капающая вода", }, "bird_tweeter_2": { "name": "\\{review}Поющие птицы", }, "blade": { "name": "\\{review}Клинок на стене", }, "blood": { "name": "\\{review}Кровь", }, "blood_pink": { "name": "\\{review}Кровь (цензурировано)", }, "blue_light": { "name": "\\{review}Синий свет", }, "boat": { "name": "Лодка", }, "boat_bits": { "name": "\\{review}Обломки лодки", }, "body_part": { "name": "Часть тела", }, "bridge_flat": { "name": "Плоский мост", }, "bridge_tilt_1": { "name": "Наклонный мост 1", }, "bridge_tilt_2": { "name": "Наклонный мост 2", }, "bubble_1": { "name": "Пузыри 1", }, "bubble_2": { "name": "Пузыри 2", }, "bubble_emitter": { "name": "Источник пузырей", }, "camera_target": { "name": "Цель камеры", }, "carcass": { "name": "\\{review}Туша", }, "ceiling_spikes": { "name": "\\{review}Острый потолок", }, "centaur": { "name": "Кентавр", }, "centaur_statue": { "name": "Статуя", }, "civilian": { "name": "\\{review}Гражданский", }, "claw_mutant": { "name": "\\{review}Когтистый Мутант", }, "clock_chimes": { "name": "\\{review}Часы убежища Бартоли", }, "cog_1": { "name": "Шестерёнка 1", }, "cog_2": { "name": "Шестерёнка 2", }, "cog_3": { "name": "Шестерёнка 3", }, "combat_end": { "name": "\\{review}Конец боя", }, "compass": { "name": "Компас", }, "compy": { "name": "\\{review}Компсогнат", }, "controls": { "name": "Управление", }, "copter": { "name": "\\{review}Вертолет", }, "cowboy": { "name": "Ковбой", }, "crawler_mutant": { "name": "\\{review}Ползающий Мутант", }, "crocodile": { "name": "Крокодил", }, "crow": { "name": "\\{review}Ворон", }, "cult_1": { "name": "\\{review}Бандит в маске 1", }, "cult_1a": { "name": "\\{review}Бандит в маске 2", }, "cult_1b": { "name": "\\{review}Бандит в маске 3", }, "cult_2": { "name": "\\{review}Метатель ножей", }, "cult_3": { "name": "\\{review}Бандит с дробовиком", }, "cut_shotgun": { "name": "\\{review}Анимация дробовика", }, "damocles_sword": { "name": "Дамоклов меч", }, "dart": { "name": "Дротик", }, "dart_effect": { "name": "Эффект дротика", }, "dart_emitter": { "name": "Дротикомёт", }, "desert_eagle": { "name": "\\{review}Дезерт Игл", }, "desert_eagle_ammo": { "name": "\\{review}Магазины Дезерт Игл", }, "detonator_box": { "name": "\\{review}Коробка детонатора", }, "ding_dong": { "name": "\\{review}Дверной звонок", }, "dino_mutant": { "name": "Динозавр-мутант", }, "disc": { "name": "\\{review}Диск", }, "disc_emitter": { "name": "\\{review}Излучатель дисков", }, "disposable_animating_1": { "name": "\\{review}Одноразовый Аниматор 1", }, "disposable_animating_10": { "name": "\\{review}Одноразовый Аниматор 10", }, "disposable_animating_2": { "name": "\\{review}Одноразовый Аниматор 2", }, "disposable_animating_3": { "name": "\\{review}Одноразовый Аниматор 3", }, "disposable_animating_4": { "name": "\\{review}Одноразовый Аниматор 4", }, "disposable_animating_5": { "name": "\\{review}Одноразовый Аниматор 5", }, "disposable_animating_6": { "name": "\\{review}Одноразовый Аниматор 6", }, "disposable_animating_7": { "name": "\\{review}Одноразовый Аниматор 7", }, "disposable_animating_8": { "name": "\\{review}Одноразовый Аниматор 8", }, "disposable_animating_9": { "name": "\\{review}Одноразовый Аниматор 9", }, "diver": { "name": "\\{review}Водолаз", }, "dog": { "name": [ "\\{review}Собака", "\\{review}Доберман", ] }, "door_1": { "name": "Дверь 1", }, "door_2": { "name": "Дверь 2", }, "door_3": { "name": "Дверь 3", }, "door_4": { "name": "Дверь 4", }, "door_5": { "name": "Дверь 5", }, "door_6": { "name": "Дверь 6", }, "door_7": { "name": "Дверь 7", }, "door_8": { "name": "Дверь 8", }, "dragon_back": { "name": "\\{review}Дракон сзади", }, "dragon_bones_1": { "name": "\\{review}Заполнитель", }, "dragon_bones_2": { "name": "\\{review}Передние кости дракона", }, "dragon_bones_3": { "name": "\\{review}Задние кости дракона", }, "dragon_front": { "name": "\\{review}Дракон спереди", }, "drawbridge": { "name": "Подъемный мост", }, "dust": { "name": "Пыль", }, "dying_monk": { "name": "\\{review}Умирающий монах", }, "dying_mutant": { "name": "\\{review}Умирающий Мутант", }, "eagle": { "name": "\\{review}Орел", }, "earthquake": { "name": "Землетрясение", }, "eel": { "name": "\\{review}Угорь", }, "electric_cleaner": { "name": "\\{review}Электрический очиститель", }, "electric_fence": { "name": "\\{review}Электрический забор", }, "electrical_light": { "name": "\\{review}Электрический свет", }, "ember": { "name": "Уголь", }, "ember_emitter": { "name": "Источник углей", }, "explosion_1": { "name": "Взрыв 1", }, "explosion_2": { "name": "Взрыв 2", }, "falling_block_1": { "name": [ "Падающий блок", "Разрушающийся пол", "Разрушающиеся плиты", ] }, "falling_block_2": { "name": [ "\\{review}Падающий блок 2", "\\{review}Обрушивающийся пол 2", "\\{review}Обрушивающиеся плитки 2", ] }, "falling_block_3": { "name": [ "\\{review}Падающий блок 3", "\\{review}Обрушивающийся пол 3", "\\{review}Обрушивающиеся плитки 3", ] }, "falling_ceiling_1": { "name": "Падающий потолок 1", }, "falling_ceiling_2": { "name": "Падающий потолок 2", }, "fire_head": { "name": "\\{review}Огненная Голова", }, "fish_mutant": { "name": "Рыба-мутант", }, "flame": { "name": [ "Пламя", "Огонь", ] }, "flame_emitter": { "name": [ "Источник пламени", "Источник огня", ] }, "flame_emitter_big": { "name": "\\{review}Генератор пламени (Большой)", }, "flame_emitter_jet": { "name": "\\{review}Генератор пламени (Струйный)", }, "flame_emitter_side": { "name": "\\{review}Генератор пламени (Боковой)", }, "flame_emitter_small": { "name": "\\{review}Генератор пламени (Малый)", }, "flare": { "name": "\\{review}Вспышка", }, "flare_fire": { "name": "\\{review}Огонь фальшфейера", }, "flares_box": { "name": "\\{review}Коробка вспышки", }, "flickering_light": { "name": "\\{review}Мерцающий Свет", }, "fuse_box": { "name": "\\{review}Коробка предохранителей", }, "fx_reserved": { "name": "\\{review}Серый диск", }, "gamma": { "name": "Гамма", }, "gas_emitter_green": { "name": "\\{review}Излучатель Газа (Зелёный)", }, "general": { "name": "\\{review}Миниподводная лодка", }, "globe": { "name": "\\{review}Глобус", }, "glow": { "name": "Свечение", }, "glow_reserved": { "name": "\\{review}Сияние карты", }, "gondola": { "name": "\\{review}Гондола", }, "gong": { "name": "\\{review}Гонг", }, "gong_bonger": { "name": "\\{review}Палка для гонга", }, "graphics": { "name": "Изображение", }, "green_light": { "name": "\\{review}Зеленый свет", }, "grenade": { "name": "Граната", }, "grenade_launcher": { "name": "\\{review}Гранатомёт", }, "grenade_launcher_ammo": { "name": "\\{review}Гранаты", }, "gun_flash": { "name": "Вспышка выстрела", }, "gun_shell": { "name": "\\{review}Патрон для пистолета", }, "harpoon_bolt": { "name": "\\{review}Гарпунный болт", }, "harpoon_gun": { "name": "\\{review}Гарпунное ружьё", }, "harpoon_gun_ammo": { "name": "\\{review}Гарпуны", }, "hook": { "name": "\\{review}Крюк", }, "hot_liquid": { "name": "\\{review}Дополнительный огонь", }, "huskie": { "name": "\\{review}Собака", }, "hybrid_mutant": { "name": "\\{review}Гибридный Мутант", }, "icicle": { "name": "\\{review}Сосульки", }, "inv_background": { "name": "\\{review}Фон меню", }, "jelly": { "name": "\\{review}Медуза", }, "kayak": { "name": "\\{review}Каяк", }, "key_1": { "name": "Ключ 1", }, "key_2": { "name": "Ключ 2", }, "key_3": { "name": "Ключ 3", }, "key_4": { "name": "Ключ 4", }, "key_hole_1": { "name": "Замочная скважина 1", }, "key_hole_2": { "name": "Замочная скважина 2", }, "key_hole_3": { "name": "Замочная скважина 3", }, "key_hole_4": { "name": "Замочная скважина 4", }, "kill_all_triggered": { "name": "\\{review}Полное уничтожение (активировано)", }, "killer_statue": { "name": "\\{review}Статуя с мечом", }, "lara": { "name": "Лара", }, "lara_alarm": { "name": "\\{review}Звонок тревоги", }, "lara_autos": { "name": "\\{review}Анимация автоматических пистолетов", }, "lara_boat": { "name": "\\{review}Анимация лодки", }, "lara_desert_eagle": { "name": "\\{review}Анимация Дезерт Игл", }, "lara_extra": { "name": "Дополнительная анимация Лары", }, "lara_flare": { "name": "\\{review}Анимация фальшфейера", }, "lara_grenade": { "name": "\\{review}Анимация гранатомёта", }, "lara_hair": { "name": "Коса Лары", }, "lara_harpoon": { "name": "\\{review}Анимация гарпуна", }, "lara_m16": { "name": "\\{review}Анимация M16", }, "lara_magnums": { "name": "Анимация магнумов", }, "lara_mp5": { "name": "\\{review}Анимация MP5", }, "lara_pistols": { "name": "Анимация пистолетов", }, "lara_rocket": { "name": "\\{review}Анимация ракетной установки", }, "lara_shotgun": { "name": "Анимация дробовика", }, "lara_skidoo": { "name": "\\{review}Анимация снегохода", }, "lara_uzis": { "name": "Анимация узи", }, "larson": { "name": "Ларсон", }, "lava_wedge": { "name": "Лава", }, "lead_bar": { "name": "Свинцовый слиток", }, "lift": { "name": "\\{review}Лифт", }, "lightning_emitter": { "name": "Излучатель молний", }, "lion": { "name": "Лев", }, "lioness": { "name": [ "Львица", "Лев", ] }, "lizard": { "name": "\\{review}Ящерица", }, "m16": { "name": "\\{review}M16", }, "m16_ammo": { "name": "\\{review}Магазины для M16", }, "m16_flash": { "name": "\\{review}Вспышка M16", }, "magnums": { "name": "Магнумы", }, "magnums_ammo": { "name": "Обоймы для магнума", }, "mesh_swap_1": { "name": "\\{review}Замена модели 1", }, "mesh_swap_2": { "name": "\\{review}Замена модели 2", }, "mesh_swap_3": { "name": "\\{review}Замена модели 3", }, "midas_touch": { "name": "Рука Мидаса", }, "mine": { "name": "\\{review}Водная мина", }, "mine_cart": { "name": "\\{review}Шахтная Вагонетка", }, "mini_copter": { "name": "\\{review}Вертолет 2", }, "missile_atlantean_bomb": { "name": "\\{review}Снаряд (Атлантическая бомба)", }, "missile_atlantean_shard": { "name": "\\{review}Снаряд (Атлантический осколок)", }, "missile_flame": { "name": "\\{review}Снаряд (Пламя)", }, "missile_harpoon": { "name": "\\{review}Снаряд (Гарпун)", }, "missile_knife": { "name": "\\{review}Снаряд (Нож)", }, "missile_poison": { "name": "\\{review}Снаряд (Яд)", }, "monk_1": { "name": "\\{review}Монах 1", }, "monk_2": { "name": "\\{review}Монах 2", }, "monkey": { "name": "\\{review}Обезьяна", }, "mounted_gun": { "name": "\\{review}Установленный пулемёт", }, "mouse": { "name": "\\{review}Крыса", }, "movable_block_1": { "name": [ "Нажимной блок 1", "Передвижной блок 1", ] }, "movable_block_2": { "name": [ "Нажимной блок 2", "Передвижной блок 2", ] }, "movable_block_3": { "name": [ "Нажимной блок 3", "Передвижной блок 3", ] }, "movable_block_4": { "name": [ "Нажимной блок 4", "Передвижной блок 4", ] }, "moving_bar": { "name": "Движущаяся планка", }, "mp5": { "name": "\\{review}MP5", }, "mp5_ammo": { "name": "\\{review}Магазины для MP5", }, "mp_1": { "name": "\\{review}МП 1", }, "mp_2": { "name": "\\{review}МП 2", }, "mummy": { "name": "Мумия", }, "natla": { "name": "Натла", }, "natla_gun": { "name": "\\{review}Пушка Натлы", }, "on_off_light": { "name": "\\{review}Включение/выключение света", }, "orca": { "name": "\\{review}Косатка", }, "passport": { "name": "Игра", }, "patrol_dog": { "name": "\\{review}Собака", }, "pda": { "name": "Игровой процесс", }, "pendulum_1": { "name": "Маятник", }, "pendulum_2": { "name": [ "\\{review}Маятник", "\\{review}Песчаный ящик", "\\{review}Качающийся топор", "\\{review}Качающийся ящик", ] }, "photo": { "name": "Дом Лары", }, "pickup_1": { "name": "Предмет 1", }, "pickup_2": { "name": "Предмет 2", }, "pickup_aid": { "name": "Первая помощь", }, "pierre": { "name": "Пьер", }, "pirahnas": { "name": "\\{review}Пираньи", }, "pistols": { "name": "Пистолеты", }, "pistols_ammo": { "name": "\\{review}Магазины для пистолета", }, "player_1": { "name": "Актёр заставки 1", }, "player_10": { "name": "\\{review}Актер катсцены 10", }, "player_2": { "name": "Актёр заставки 2", }, "player_3": { "name": "Актёр заставки 3", }, "player_4": { "name": "Актёр заставки 4", }, "player_5": { "name": "\\{review}Актер катсцены 5", }, "player_6": { "name": "\\{review}Актер катсцены 6", }, "player_7": { "name": "\\{review}Актер катсцены 7", }, "player_8": { "name": "\\{review}Актер катсцены 8", }, "player_9": { "name": "\\{review}Актер катсцены 9", }, "pods": { "name": "Гнездо", }, "poison_dart": { "name": "\\{review}Отравленный дротик", }, "poison_dart_emitter": { "name": "\\{review}Метатель отравленных дротиков", }, "portacabin": { "name": "Кабина", }, "power_saw": { "name": "\\{review}Электропила", }, "prisoner": { "name": "\\{review}Заключённый", }, "propeller_1": { "name": "\\{review}Винт самолёта", }, "propeller_2": { "name": "\\{review}Подводный винт", }, "propeller_3": { "name": "\\{review}Воздушный вентилятор", }, "pulse_light": { "name": "\\{review}Импульсный свет", }, "puma": { "name": "Пума", }, "punk_1": { "name": "\\{review}Панк 1", }, "punk_2": { "name": "\\{review}Панк 2", }, "puzzle_1": { "name": "Элемент головоломки 1", }, "puzzle_2": { "name": "Элемент головоломки 2", }, "puzzle_3": { "name": "Элемент головоломки 3", }, "puzzle_4": { "name": "Элемент головоломки 4", }, "puzzle_done_1": { "name": "Головоломка 1 (готово)", }, "puzzle_done_2": { "name": "Головоломка 2 (готово)", }, "puzzle_done_3": { "name": "Головоломка 3 (готово)", }, "puzzle_done_4": { "name": "Головоломка 4 (готово)", }, "puzzle_hole_1": { "name": "Головоломка 1 (пусто)", }, "puzzle_hole_2": { "name": "Головоломка 2 (пусто)", }, "puzzle_hole_3": { "name": "Головоломка 3 (пусто)", }, "puzzle_hole_4": { "name": "Головоломка 4 (пусто)", }, "quad_bike": { "name": "\\{review}Квадроцикл", }, "quest_1": { "name": "\\{review}Предмет задания 1", }, "quest_2": { "name": "\\{review}Предмет задания 2", }, "quest_3": { "name": "\\{review}Предмет задания 3", }, "quest_4": { "name": "\\{review}Предмет задания 4", }, "raptor": { "name": "Раптор", }, "raptor_emitter": { "name": "\\{review}Излучатель Раптора", }, "rat": { "name": [ "Крыса", "Крыса на земле", ] }, "red_light": { "name": "\\{review}Красный свет", }, "rib": { "name": "\\{review}RIB", }, "ricochet": { "name": "Рикошет", }, "rocket": { "name": "\\{review}Ракета", }, "rocket_launcher": { "name": "\\{review}Ракетная установка", }, "rocket_launcher_ammo": { "name": "\\{review}Ракеты", }, "rolling_ball_1": { "name": [ "Валун", "Катящийся камень", ] }, "rolling_ball_2": { "name": [ "\\{review}Камень 2", "\\{review}Катающийся шар 2", ] }, "rolling_ball_3": { "name": [ "\\{review}Камень 3", "\\{review}Катающийся шар 3", ] }, "rolling_ball_4": { "name": [ "\\{review}Камень 4", "\\{review}Катающийся шар 4", ] }, "rotating_laser": { "name": "\\{review}Вращающийся лазер", }, "rx_worker_1": { "name": "\\{review}RX Рабочий 1", }, "rx_worker_2": { "name": "\\{review}RX Рабочий 2", }, "rx_worker_3": { "name": "\\{review}RX Рабочий 3", }, "save_crystal": { "name": "Кристалл сохранения", }, "scion": { "name": "Наследие", }, "scion_holder": { "name": "Постамент Наследия", }, "secret_1": { "name": "\\{review}Секрет 1", }, "secret_2": { "name": "\\{review}Секрет 2", }, "secret_3": { "name": "\\{review}Секрет 3", }, "security_guard": { "name": "\\{review}Охранник", }, "security_laser_alarm": { "name": "\\{review}Охранный лазер (Тревога)", }, "security_laser_deadly": { "name": "\\{review}Охранный лазер (Смертельный)", }, "security_laser_killer": { "name": "\\{review}Охранный лазер (Убийственный)", }, "sentry_gun": { "name": "\\{review}Робот-страж", }, "shadow": { "name": "\\{review}Тень", }, "shark": { "name": "\\{review}Акула", }, "shiva": { "name": "\\{review}Шива", }, "shotgun": { "name": "Дробовик", }, "shotgun_ammo": { "name": "Патроны для дробовика", }, "shotgun_shell": { "name": "\\{review}Патрон для дробовика", }, "skate_kid": { "name": "Скейтер", }, "skateboard": { "name": "Скейтборд", }, "skidoo_armed": { "name": "\\{review}Чёрный снегоход", }, "skidoo_driver": { "name": "\\{review}Водитель чёрного снегохода", }, "skidoo_fast": { "name": "\\{review}Красный снегоход", }, "skidoo_track": { "name": "\\{review}Гусеница снегохода", }, "skybox": { "name": "Скайбокс", }, "sliding_pillar": { "name": "Скользящая колонна", }, "smashable_1": { "name": "\\{review}Разбиваемое окно 1", }, "smashable_2": { "name": "\\{review}Разбиваемое окно 2", }, "smashable_3": { "name": "\\{review}Разбиваемое окно 3", }, "smashable_4": { "name": "\\{review}Разбиваемое окно 4", }, "smoke_emitter_black": { "name": "\\{review}Дымовой излучатель (черный)", }, "smoke_emitter_white": { "name": "\\{review}Дымовой излучатель (белый)", }, "snake": { "name": "\\{review}Змей", }, "snow_sprite": { "name": "\\{review}След снегохода", }, "sophia": { "name": "\\{review}София", }, "sound": { "name": "Звук", }, "sphere_of_doom_1": { "name": "\\{review}Взрыв дракона 1", }, "sphere_of_doom_2": { "name": "\\{review}Взрыв дракона 2", }, "sphere_of_doom_3": { "name": "\\{review}Взрыв дракона 3", }, "spider": { "name": "\\{review}Паук", }, "spike_wall": { "name": "\\{review}Стена с шипами", }, "spikes": { "name": "Шипы", }, "spinning_blade": { "name": "\\{review}Вращающийся клинок", }, "splash_1": { "name": "Всплеск 1", }, "splash_2": { "name": "Всплеск 2", }, "springboard": { "name": "\\{review}Прыжковая доска", }, "steam_emitter": { "name": "\\{review}Паровой излучатель", }, "sthpac_mercenary": { "name": "\\{review}Наёмник из южной части Тихого океана", }, "stopwatch": { "name": "\\{review}Статистика", }, "strobe_light": { "name": "\\{review}Стробоскопический свет", }, "swat_1": { "name": "\\{review}SWAT 1", }, "swat_2": { "name": "\\{review}SWAT 2", }, "swat_3": { "name": "\\{review}SWAT 3", }, "swinging_axe": { "name": "\\{review}Pаскачивающийся топор", }, "switch_type_airlock": { "name": "\\{review}Переключатель воздушного шлюза", }, "switch_type_button": { "name": [ "\\{review}Кнопка", "\\{review}Нажимная кнопка", "\\{review}Выключатель", ] }, "switch_type_normal": { "name": [ "Рычаг", "Переключатель", ] }, "switch_type_small": { "name": "\\{review}Маленький выключатель", }, "switch_type_uw": { "name": [ "Подводный рычаг", "Подводный переключатель", ] }, "switch_type_wheel": { "name": "\\{review}Колёсный переключатель", }, "teeth_trap": { "name": [ "Зубная ловушка", "Клацающая дверь", ] }, "text_box": { "name": "\\{review}Рамка интерфейса", }, "thors_handle": { "name": "Рукоять молота Тора", }, "thors_head": { "name": "Молот Тора", }, "tiger": { "name": "\\{review}Тигр", }, "tony": { "name": "\\{review}Тони", }, "torso": { "name": [ "Торсо", "Адам", "Безногий мутант", ] }, "train": { "name": "\\{review}Поезд", }, "trapdoor_1": { "name": "Люк 1", }, "trapdoor_2": { "name": "Люк 2", }, "trapdoor_3": { "name": "Люк 3", }, "trex": { "name": "Тираннозавр", }, "trex_alpha": { "name": "\\{review}Ти-Рекс Альфа", }, "tribe_axeman": { "name": "\\{review}Топорщик племени", }, "tribe_boss": { "name": "\\{review}Вождь племени", }, "tribe_pipeman": { "name": "\\{review}Пользователь духовой трубки племени", }, "tropical_fish": { "name": "\\{review}Тропическая рыба", }, "twinkle": { "name": "Искры", }, "upv": { "name": "\\{review}Минисуб", }, "uzis": { "name": "Узи", }, "uzis_ammo": { "name": "Обоймы для узи", }, "vole": { "name": [ "Плавающая крыса", "Крыса в воде", ] }, "vulture": { "name": "\\{review}Стервятник", }, "wasp_mutant": { "name": "\\{review}Осинный Мутант", }, "wasp_mutant_emitter": { "name": "\\{review}Излучатель Осиного Мутанта", }, "water_sprite": { "name": "\\{review}След лодки", }, "waterfall": { "name": "Водопадный туман", }, "white_light": { "name": "\\{review}Белый свет", }, "willard": { "name": "\\{review}Уиллард", }, "winston": { "name": "\\{review}Уинстон", }, "winston_army": { "name": "\\{review}Уинстон (армия)", }, "wolf": { "name": "Волк", }, "worker_1": { "name": "\\{review}Бандит с пистолетом 1", }, "worker_2": { "name": "\\{review}Бандит с пистолетом 2", }, "worker_3": { "name": "\\{review}Бандит с палкой 1", }, "worker_4": { "name": "\\{review}Бандит с палкой 2", }, "worker_5": { "name": "\\{review}Бандит с огнемётом", }, "xian_knight": { "name": "\\{review}Ксианский рыцарь", }, "xian_knight_statue": { "name": "\\{review}Статуя ксианского рыцаря", }, "xian_spearman": { "name": "\\{review}Ксианский копейщик", }, "xian_spearman_statue": { "name": "\\{review}Статуя ксианского копейщика", }, "yeti": { "name": "\\{review}Йети", }, "zipline_handle": { "name": "\\{review}Рукоятка зиплайна", } } } ================================================ FILE: data/trx/ship/cfg/base_strings.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "language_name": "English", "general": { "actions": { "examine_item": "Examine", "hide_dialog": "Hide dialog", "reset_defaults": "Reset All", "rotate": "Rotate", "unbind": "Unbind", "use_item": "Use", }, "config_presets": { "applied": "Preset applied.", "confirm_description": "The following settings will be changed:", "confirm_restart_note": "Note: some settings may require a game restart to take effect.", "empty": "No presets found.", "no_changes": "No changes to apply.", "title_fmt": "Apply preset %s?", }, "globe_select": { "area_1": "Area 1", "area_2": "Area 2", "area_3": "Area 3", "area_4": "Area 4", "area_5": "Area 5", "area_6": "Area 6", }, "inventory_ring": { "heading_adventure": "Adventure", "heading_fmt": "%s", "heading_game_over": "GAME OVER", "heading_inventory": "INVENTORY", "heading_items": "ITEMS", "heading_option": "OPTION", "item_count_fmt": "\\{small}%s", "object_name_fmt": "%s", }, "misc": { "demo_mode": "Demo Mode", "direction_keys_controller": "D-Pad", "direction_keys_keyboard": "Arrows", "empty_slot_fmt": "- EMPTY SLOT -", "exit": "Exit", "hold_fmt": "Hold %s", "off": "Off", "on": "On", "pagination_nav": "%d / %d", }, "osd": { "ambiguous_input_2": "Ambiguous input: %s and %s", "ambiguous_input_3": "Ambiguous input: %s, %s, ...", "bilinear_filter_off": "Bilinear filter: off", "bilinear_filter_on": "Bilinear filter: on", "command_bad_invocation": "Invalid invocation: %s", "command_bool": "on, off", "command_decimal": "[decimal]", "command_integer": "[integer]", "command_percent": "[integer]", "command_unavailable": "This command is not currently available", "command_valid_values": "Valid values: %s", "complete_level": "Level complete!", "config_option_get": "%s is currently set to %s", "config_option_set": "%s changed to %s", "config_option_unknown_option": "Unknown option: %s", "current_health_get": "Current Lara's health: %d", "current_health_set": "Lara's health set to %d", "door_close": "Close Sesame!", "door_open": "Open Sesame!", "door_open_fail": "No doors in Lara's proximity", "flipmap_fail_already_off": "Flipmap is already OFF", "flipmap_fail_already_on": "Flipmap is already ON", "flipmap_off": "Flipmap set to OFF", "flipmap_on": "Flipmap set to ON", "fly_mode_off": "Fly mode disabled", "fly_mode_on": "Fly mode enabled", "fps_counter_off": "FPS counter: off", "fps_counter_on": "FPS counter: on", "give_item": "Added %s to Lara's inventory", "give_item_all_guns": "Lock'n'load - Lara's armed to the teeth!", "give_item_all_keys": "Surprise! Every key item Lara needs is now in her backpack.", "give_item_cheat": "Lara's backpack just got way heavier!", "heal_already_full_hp": "Lara's already at full health", "heal_success": "Healed Lara back to full health", "invalid_cutscene": "Invalid cutscene", "invalid_demo": "Invalid demo", "invalid_item": "Unknown item: %s", "invalid_level": "Invalid level", "invalid_object": "Invalid object", "invalid_room": "Invalid room: %d. Valid rooms are 0-%d", "invalid_sample": "Invalid sound: %d", "kill": "Bye-bye!", "kill_all": "Poof! %d enemies gone!", "kill_all_fail": "Uh-oh, there are no enemies left to kill...", "kill_fail": "No enemy nearby...", "lighting_contrast_fmt": "Lighting Contrast: %s", "load_game": "Loaded game from save slot %d", "load_game_fail_invalid_slot": "Invalid save slot %d", "load_game_fail_unavailable_slot": "Save slot %d is not available", "object_not_found": "Object not found", "play_cutscene": "Loading cutscene %d", "play_demo": "Loading demo %d", "play_level": "Loading %s", "pos_lara_missing": "Lara not present", "pos_lara_pos_fmt": "Room: %d\nPosition: %.3f, %.3f, %.3f\nRotation: %.3f, %.3f, %.3f", "pos_level_fmt": "Level %d", "pos_level_fmt_cutscene": "Cutscene %d", "pos_level_fmt_demo": "Demo %d", "quick_load": "Quick-loaded slot %d", "quick_load_fail_no_bound_slot": "No save slot is currently bound", "quick_load_fail_unavailable_bound_slot": "The bound save slot is not available", "quick_save": "Quick-saved", "quick_save_fail_no_slots": "No quick save slots are configured", "save_game": "Saved game to save slot %d", "save_game_fail_invalid_slot": "Invalid save slot %d", "sound_available_samples": "Available sounds: %s", "sound_playing_sample": "Playing sound %d", "speed_get": "Current speed: %d", "speed_set": "Speed set to %d", "strings_failed": "Failed to reload the language files", "strings_reloaded": "Language files reloaded", "textures_off": "Textures: off", "textures_on": "Textures: on", "trapezoid_filter_off": "Trapezoid filter: off", "trapezoid_filter_on": "Trapezoid filter: on", "ui_off": "UI disabled", "ui_on": "UI enabled", "unknown_command": "Unknown command: %s", "upscaling_factor": "Upscaling Factor: x%d", "wireframe_mode_off": "Wireframe mode: off", "wireframe_mode_on": "Wireframe mode: on", }, "overlay": { "debug_animation": "Animation: ", "debug_animation_state": "State: ", "debug_camera_pos": "Camera origin: ", "debug_camera_target": "Camera target: ", "debug_immune": "Invulnerability on", "debug_position": "Position: ", "debug_rotation": "Rotation: ", "debug_speed": "Speed: ", "item_count_fmt_pc": "\\{small}%s", "item_count_fmt_ps1": "\\{small}%s", }, "passport": { "delete_save": "Delete", "delete_save_confirm": "Delete this save?", "delete_save_failed": "Failed to delete the chosen save.", "delete_save_no": "No", "delete_save_yes": "Yes", "exit_game": "Exit Game", "exit_to_title": "Exit to Title", "load_game": "Load Game", "mode_new_game": "New Game", "mode_new_game_jp": "Japanese NG", "mode_new_game_jp_plus": "Japanese NG+", "mode_new_game_plus": "New Game+", "new_game": "New Game", "play_previous_levels": "Play previous levels", "restart_level": "Restart Level", "save_game": "Save Game", "save_slot_unsupported": "This save does not support this feature.", "select_level": "Select Level", "select_mod": "Select Game", "select_mode": "Select Mode", "select_save": "Select Save", "story_so_far": "Story so far...", "switch_mod": "Switch Game", }, "pause": { "are_you_sure": "Are you sure?", "continue": "Continue", "exit_to_title": "Exit to title?", "no": "No", "paused": "Paused", "quit": "Quit", "yes": "Yes", }, "photo_mode": { "advance_frame": "Advance frame", "camera_move_prompt": "Move camera", "camera_reset_prompt": "Reset camera", "camera_roll_prompt": "Roll camera", "camera_rotate_90_prompt": "Rotate camera 90°", "camera_rotate_prompt": "Rotate camera", "change_lara_pose": "Change Lara's pose", "fov_prompt": "Adjust FOV", "lara_move_prompt": "Move Lara", "lara_reset_prompt": "Reset Lara", "lara_roll_prompt": "Roll Lara", "lara_rotate_90_prompt": "Rotate Lara 90°", "lara_rotate_prompt": "Rotate Lara", "snap_prompt": "Take picture", "title_camera_pos": "Photo Mode", "title_lara_pos": "Moving Lara", "toggle_help": "Toggle help", }, "settings": { "common": { "all_hidden_disclaimer": "Settings are disabled for this level set.", "chroma": "Chroma", "edit_value": "Edit value", "frozen_option_disclaimer": "This setting is enforced by the level builder and cannot be changed.", "hue": "Hue", "lightness": "Lightness", "restore_default": "Restore default", "toggle_help": "Toggle help", }, "controls": { "backend": { "controller": "Controller", "keyboard": "Keyboard", }, "customize": "Customize Controls", "layout": { "custom_1": "User Keys 1", "custom_2": "User Keys 2", "custom_3": "User Keys 3", "default": "Default Keys", }, "tabs": { "basics": "Movement", "items": "Items", "misc": "Misc", "system": "System", } }, "gameplay": { "tabs": { "controls": "Controls", "fixes": "Fixes", "general": "General", "mods": "Mods", "presets": "Presets", }, "title": "Gameplay Options", }, "graphic_settings": { "tabs": { "bars": "Bars", "rendering": "Rendering", "stats": "Stats", "ui": "UI", "visuals": "Visuals", }, "title": "Graphic Options", }, "sound": { "tabs": { "misc": "Misc", "volume": "Volume", }, "title": "Sound Options", } }, "stats": { "ammo": "Ammo Hits/Used", "ammo_hits": "Hits", "ammo_used": "Ammo Used", "assault_best_time_fmt": "%s", "assault_finish": "Finish", "assault_no_times_set": "No Times Set", "assault_other_times_fmt": "%s", "assault_title": "BEST TIMES", "basic_fmt": "%d", "bonus_statistics": "Bonus Statistics", "crystals": "Crystals", "deaths": "Deaths", "detail_fmt": "%d of %d", "distance_travelled": "Distance Traveled", "final_statistics": "Final Statistics", "gym_assault_course": "Assault Course", "gym_racetrack_course": "Race Track Course", "kills": "Kills", "level": "Level", "medipacks_used": "Health Packs Used", "none": "None", "pickups": "Pickups", "secrets": "Secrets Found", "time_taken": "Time Taken", } }, "console": { "cmd": { "braid": { "help": "Toggles Lara's braid.", }, "cheats": { "help": "Toggles in-game cheats on or off.", }, "clear": { "help": "Clears visible console logs.", }, "debug": { "help": "Toggles visual debug information.", }, "drain": { "help": "Dries the current room, removing the water.", }, "end_level": { "help": "Ends the current level.", }, "exit": { "help": "Exits the game.", }, "flipmap": { "help": "Toggles the flip map.", }, "flood": { "help": "Submerges the current room with water.", }, "fly": { "help": "Toggles the fly-mode cheat.", }, "fps": { "help": "Changes the FPS value.", }, "give": { "help": "Adds a given item to Lara's inventory.", "invalid_secret": "Invalid secret: %s (valid secrets: %s)", "secret_given": "Added secret %s", "secret_list": "Secrets collected: %d of %d (%s)", "secret_none": "Secrets collected: %d of %d", "secret_taken": "Removed secret %s", }, "give_secret": { "help": "Lists Lara's secrets, or takes/gives a secret by number.", }, "heal": { "help": "Heals Lara back to full health.", }, "help": { "help": "Shows help for all commands or detailed help for one.", "list": "Available commands:", }, "hp": { "help": "Sets Lara's health to the specified value.", }, "immune": { "help": "Toggles invulnerability. (Lara can still be killed in some circumstances.)", "off": "Lara is now vulnerable", "on": "Lara is now impervious to damage", }, "inf_sprint": { "help": "Toggles infinite sprint.", "off": "Lara can no longer sprint forever", "on": "Lara can now sprint forever", }, "kill": { "help": "Kills nearby enemies.", }, "lighting": { "help": "Toggles lighting system.", }, "load": { "help": "Loads game from the given save slot or from a quick save.", }, "lua": { "help": "Executes the given Lua code string.", "runtime_error": "Lua runtime error: %s", "syntax_error": "Lua syntax error: %s", }, "mod": { "help": "Switches to the specified mod and restarts the game.", }, "music": { "help": "Plays a music track with the given id.", }, "play_cutscene": { "help": "Plays a cutscene with the given number.", }, "play_demo": { "help": "Plays a demo with the given number.", }, "play_gym": { "help": "Plays the Gym level.", }, "play_level": { "help": "Plays a level with the given name or number.", }, "play_music": { "invalid_track": "Invalid music track", "stopped": "Music stopped", "track": "Playing music track %d", }, "pos": { "help": "Shows Lara's position.", }, "save": { "help": "Saves game to the given save slot or to the next quick save slot.", }, "screenshot": { "help": "Saves a screenshot to disk at optional location.", }, "set": { "help": "Displays or updates the given configuration setting.", }, "sfx": { "help": "Plays a sound effect with the given id.", }, "spawn": { "fail": "Failed to spawn requested object", "success": "Requested object spawned near Lara", }, "speed": { "help": "Changes the game's speed.", }, "strings": { "help": "Reloads the current language files from disk.", }, "teleport": { "item": "Teleported to item: %d", "item_fail": "Failed to teleport to item: %d", "object": "Teleported to object: %s", "object_fail": "Failed to teleport to object: %s", "pos": "Teleported to position: %.3f %.3f %.3f", "pos_fail": "Failed to teleport to position: %.3f %.3f %.3f", "room": "Teleported to room: %d", "room_fail": "Failed to teleport to room: %d", }, "textures": { "help": "Toggles textures.", }, "title": { "help": "Returns to the title screen.", }, "tp": { "help": "Teleports Lara to a given position or room number.", }, "trigger": { "help": "Triggers or untriggers an item by id, item name, or object name.", "invalid_item": "Invalid item: %s", "no_match": "Unknown target: %s", "not_found": "No matching items found for: %s", "triggered": "Triggered item(s): %s", "untriggered": "Untriggered item(s): %s", }, "vsync": { "help": "Toggles vertical sync.", }, "weather": { "help": "Changes the current weather type.", "invalid": "Invalid weather: %s (valid: %s)", "set": "Weather set to %s", }, "winston": { "dead": "Your butler is dead. You monster!", "spawn_failed": "Failed to summon Winston", "spawned": "Summoned Winston near Lara", "teleported": "Summoned Winston near Lara", }, "wireframe": { "help": "Toggles wireframe rendering.", } } }, "dynamic": { "config_presets": { "tr1_pc": "TR1 PC", "tr1_ps1": "TR1 PS1", "tr2_pc": "TR2 PC", "tr2_ps1": "TR2 PS1", "tr3_pc": "TR3 PC", "tr3_ps1": "TR3 PS1", }, "enums": { "bar_look": { "tr1_pc": "TR1 PC", "tr2_pc": "TR2 PC", "tr2_ps1": "TR2 PS1", "tr3_pc": "TR3 PC", "tr3_ps1": "TR3 PS1", }, "lara_outfit": { "default": "Default", "golden_sophia": "Golden Sophia", "sophia": "Sophia", "tr1_bacon_lara": "Bacon Lara", "tr1_classic": "TR1 Classic", "tr1_combo": "TR1 Combo", "tr1_golden_bacon_lara": "Golden Bacon Lara", "tr1_golden_lara": "TR1 Golden Lara", "tr1_gym": "TR1 Gym", "tr1_mauled": "TR1 Mauled", "tr1_ngage": "TR1 N-Gage", "tr23_golden_lara": "TR2/3 Golden Lara", "tr2_bomber_jacket": "Bomber Jacket", "tr2_classic": "TR2 Classic", "tr2_diving_suit": "Diving Suit 1", "tr2_diving_suit_alpha": "Diving Suit 2", "tr2_gym": "TR2 Gym", "tr2_robe": "Robe", "tr2_vegas": "Vegas", "tr3_antarctica": "Antarctica", "tr3_catsuit": "Catsuit", "tr3_classic": "TR3 Classic", "tr3_gym": "TR3 Gym", "tr3_nevada": "Nevada", "tr3_south_pacific": "South Pacific", } }, "mods": { "tr1": { "title": "Tomb Raider I", }, "tr1-demo-pc": { "title": "Tomb Raider I Demo", }, "tr1-ub": { "title": "Unfinished Business", }, "tr2": { "title": "Tomb Raider II", }, "tr2-gm": { "title": "The Golden Mask", }, "tr3": { "title": "Tomb Raider III", }, "tr3-la": { "title": "The Lost Artifact", } } }, "enums": { "ALLY_HOSTILITY_POLICY": { "ALLY_HOSTILITY_POLICY_INDIVIDUAL": "Individual", "ALLY_HOSTILITY_POLICY_SHARED": "Shared", }, "ASPECT_MODE": { "ASPECT_MODE_16_10": "16:10", "ASPECT_MODE_16_9": "16:9", "ASPECT_MODE_4_3": "4:3", "ASPECT_MODE_ANY": "Any", }, "BACKGROUND_TYPE": { "BK_BLACK": "Black", "BK_IMAGE": "Image", "BK_MONOCHROME": "Monochrome", "BK_MONOCHROME_COOL": "Monochrome (cool)", "BK_MONOCHROME_WARM": "Monochrome (warm)", "BK_NONE": "Transparent", "BK_PATTERN_STATIC": "Static", "BK_PATTERN_WAVE": "Wave", "BK_TRANSPARENT_DARK": "Very dark", "BK_TRANSPARENT_MEDIUM": "Dark", }, "BAR_SHOW_MODE": { "BAR_SHOW_MODE_ALWAYS": "Always", "BAR_SHOW_MODE_BOSS_ONLY": "Boss only", "BAR_SHOW_MODE_NEVER": "Never", }, "BILLBOARD_LOCK_MODE": { "BILLBOARD_LOCK_NONE": "None", "BILLBOARD_LOCK_PERSPECTIVE": "Perspective", "BILLBOARD_LOCK_ROLL": "Roll", "BILLBOARD_LOCK_ROLL_PITCH": "Roll & pitch", }, "BLOOD_EFFECTS": { "BLOOD_EFFECTS_DISABLED": "Disabled", "BLOOD_EFFECTS_PINK": "Pink", "BLOOD_EFFECTS_RED": "Red", }, "CAMERA_MODE": { "CAMERA_MODE_TR1": "TR1", "CAMERA_MODE_TR2": "TR2", "CAMERA_MODE_TR3": "TR3", }, "CREATURE_DROWN_POLICY": { "CREATURE_DROWN_POLICY_DEFAULT": "Default", "CREATURE_DROWN_POLICY_NEVER": "Never", "CREATURE_DROWN_POLICY_SUBMERGED": "Submerged", }, "INPUT_BACKEND": { "INPUT_BACKEND_CONTROLLER": "Controller", "INPUT_BACKEND_KEYBOARD": "Keyboard", }, "INPUT_ROLE": { "INPUT_ROLE_ACTION": "Action", "INPUT_ROLE_CAMERA_BACK": "Camera Back", "INPUT_ROLE_CAMERA_DOWN": "Camera Down", "INPUT_ROLE_CAMERA_FORWARD": "Camera Forward", "INPUT_ROLE_CAMERA_LEFT": "Camera Left", "INPUT_ROLE_CAMERA_RESET": "Camera Reset", "INPUT_ROLE_CAMERA_RIGHT": "Camera Right", "INPUT_ROLE_CAMERA_UP": "Camera Up", "INPUT_ROLE_CHANGE_OUTFIT": "Change Outfit", "INPUT_ROLE_CHANGE_TARGET": "Change Target", "INPUT_ROLE_CROUCH": "Crouch", "INPUT_ROLE_CYCLE_LIGHTING_CONTRAST": "Cycle Lighting Contrast", "INPUT_ROLE_DOWN": "Back", "INPUT_ROLE_DRAW_WEAPON": "Draw Weapon", "INPUT_ROLE_ENTER_CONSOLE": "Dev Console", "INPUT_ROLE_EQUIP_AUTOS": "Equip Automatic Pistols", "INPUT_ROLE_EQUIP_DESERT_EAGLE": "Equip Desert Eagle", "INPUT_ROLE_EQUIP_GRENADE_LAUNCHER": "Equip Grenade Launcher", "INPUT_ROLE_EQUIP_HARPOON": "Equip Harpoon Gun", "INPUT_ROLE_EQUIP_M16": "Equip M16", "INPUT_ROLE_EQUIP_MAGNUMS": "Equip Magnums", "INPUT_ROLE_EQUIP_MP5": "Equip MP5", "INPUT_ROLE_EQUIP_PISTOLS": "Equip Pistols", "INPUT_ROLE_EQUIP_ROCKET_LAUNCHER": "Equip Rocket Launcher", "INPUT_ROLE_EQUIP_SHOTGUN": "Equip Shotgun", "INPUT_ROLE_EQUIP_UZIS": "Equip Uzis", "INPUT_ROLE_FLY_CHEAT": "Fly Cheat", "INPUT_ROLE_FPS": "Show FPS", "INPUT_ROLE_INVENTORY": "Inventory", "INPUT_ROLE_ITEM_CHEAT": "Item Cheat", "INPUT_ROLE_JUMP": "Jump", "INPUT_ROLE_LEFT": "Left", "INPUT_ROLE_LEVEL_SKIP_CHEAT": "Level Skip", "INPUT_ROLE_LOAD": "Load", "INPUT_ROLE_LOOK": "Look", "INPUT_ROLE_PAUSE": "Pause", "INPUT_ROLE_QUICK_LOAD": "Quick Load", "INPUT_ROLE_QUICK_SAVE": "Quick Save", "INPUT_ROLE_RIGHT": "Right", "INPUT_ROLE_ROLL": "Roll", "INPUT_ROLE_SAVE": "Save", "INPUT_ROLE_SCREENSHOT": "Screenshot", "INPUT_ROLE_SLOW": "Walk", "INPUT_ROLE_SPRINT": "Sprint", "INPUT_ROLE_STEP_LEFT": "Step Left", "INPUT_ROLE_STEP_RIGHT": "Step Right", "INPUT_ROLE_SWITCH_BORDERS": "Switch Borders Size", "INPUT_ROLE_SWITCH_UPSCALING": "Switch Upscaling Factor", "INPUT_ROLE_TOGGLE_BILINEAR_FILTER": "Toggle Bilinear Filter", "INPUT_ROLE_TOGGLE_FULLSCREEN": "Toggle Fullscreen", "INPUT_ROLE_TOGGLE_PHOTO_MODE": "Toggle Photo Mode", "INPUT_ROLE_TOGGLE_TEXTURES": "Toggle Textures", "INPUT_ROLE_TOGGLE_TRAPEZOID_FILTER": "Toggle Trapezoid Filter", "INPUT_ROLE_TOGGLE_UI": "Toggle UI", "INPUT_ROLE_TOGGLE_WIREFRAME": "Toggle Wireframe", "INPUT_ROLE_TURBO_CHEAT": "Turbo Speed", "INPUT_ROLE_UP": "Run", "INPUT_ROLE_USE_BIG_MEDI": "Large Medi", "INPUT_ROLE_USE_FLARE": "Flare", "INPUT_ROLE_USE_SMALL_MEDI": "Small Medi", }, "JUMP_LOCK_MODE": { "JUMP_LOCK_DISABLED": "Disabled", "JUMP_LOCK_LEGACY": "Legacy", "JUMP_LOCK_TUNED": "Tuned", }, "LIGHTING_CONTRAST": { "LIGHTING_CONTRAST_HIGH": "High", "LIGHTING_CONTRAST_LOW": "Low", "LIGHTING_CONTRAST_MEDIUM": "Medium", }, "LOADING_SCREENS_MODE": { "LOADING_SCREENS_ALWAYS": "Always", "LOADING_SCREENS_DISABLED": "Disabled", "LOADING_SCREENS_NEW_GAMES": "New games", }, "LOOK_MODE": { "LOOK_MODE_ENHANCED": "Enhanced", "LOOK_MODE_RESTRICTED": "Restricted", "LOOK_MODE_UNRESTRICTED": "Unrestricted", }, "MUSIC_LOAD_CONDITION": { "MUSIC_LOAD_CONDITION_ALWAYS": "Always", "MUSIC_LOAD_CONDITION_NEVER": "Never", "MUSIC_LOAD_CONDITION_NON_AMBIENT": "Non-ambient", }, "PROJECTILE_AREA_DAMAGE": { "PROJECTILE_AREA_DAMAGE_MULTI_SWEEP": "Multi-sweep", "PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP": "Single-sweep", }, "QUICK_GUNS_MODE": { "QUICK_GUNS_MODE_DRAW_AND_HOLSTER": "Draw or holster", "QUICK_GUNS_MODE_DRAW_ONLY": "Draw only", }, "SCREENSHOT_FORMAT": { "SCREENSHOT_FORMAT_JPEG": "JPG", "SCREENSHOT_FORMAT_PNG": "PNG", }, "SHADOW_TYPE": { "SHADOW_TYPE_CIRCLE": "Circle", "SHADOW_TYPE_OCTAGON": "Octagon", "SHADOW_TYPE_SPRITE": "Sprite", }, "STATS_STYLE": { "STATS_STYLE_BARE": "Bare", "STATS_STYLE_BORDERED": "Bordered", }, "SUNGLASSES_MODE": { "SUNGLASSES_MODE_OFF": "Off", "SUNGLASSES_MODE_OPAQUE": "Opaque", "SUNGLASSES_MODE_TRANSPARENT": "Transparent", }, "TARGET_LOCK_MODE": { "TARGET_LOCK_MODE_FULL": "Full lock", "TARGET_LOCK_MODE_NONE": "No lock", "TARGET_LOCK_MODE_SEMI": "Semi lock", }, "TEXTURE_FILTER": { "TEXTURE_FILTER_BILINEAR": "Bilinear", "TEXTURE_FILTER_POINT": "Point", }, "UI_ELEMENT_LOCATION": { "UI_ELEMENT_LOCATION_BOTTOM_CENTER": "Bottom center", "UI_ELEMENT_LOCATION_BOTTOM_LEFT": "Bottom left", "UI_ELEMENT_LOCATION_BOTTOM_RIGHT": "Bottom right", "UI_ELEMENT_LOCATION_TOP_CENTER": "Top center", "UI_ELEMENT_LOCATION_TOP_LEFT": "Top left", "UI_ELEMENT_LOCATION_TOP_RIGHT": "Top right", }, "UI_STYLE": { "UI_STYLE_PC": "PC", "UI_STYLE_PS1": "PS1", }, "WALL_GLITCH_MODE": { "WALL_GLITCH_FIXED": "Fixed", "WALL_GLITCH_TR1": "TR1", "WALL_GLITCH_TR2": "TR2", } }, "settings": { "audio.ambient_volume": { "title": "Ambient volume", "description": "Adjusts ambient volume.", }, "audio.cutscene_volume": { "title": "Cutscene volume", "description": "Adjusts the ingame cutscenes volume.", }, "audio.enable_lara_mic": { "title": "Microphone near Lara", "description": "Set the microphone to be at Lara's position. If disabled, the microphone will be at the camera's position.", }, "audio.enable_music_in_inventory": { "title": "Play music in inventory", "description": "Lets game sounds, ambient and music continue playing in the inventory screen.", }, "audio.enable_music_in_menu": { "title": "Main menu music", "description": "Plays music in the main menu.", }, "audio.enable_pitched_sounds": { "title": "Pitched sounds", "description": "Allows sound effects to be randomly, slightly pitched to vary the game sounds.", }, "audio.enable_ps1_sfx": { "title": "PS1 SFX replacements", "description": "Enables specific sound effect replacements using PS1 equivalents.\n\n- Uzi fire (TR1 only)\n- Lara barefoot sounds (TR2 only)", }, "audio.enable_underwater_anim_sfx": { "title": "Underwater animation SFX", "description": "Allows control over playing specific animation sound effects - for objects such as doors and trapdoors - when the camera is underwater.", }, "audio.fix_chainblock_secret_sound": { "title": "Fix chain block sound", "description": "Prevents the secret sound from incorrectly playing when using the golden key in Tomb of Tihocan.", }, "audio.fix_secrets_killing_music": { "title": "Layered secret music", "description": "Fixes the sound of collecting a secret stopping the active music track.", }, "audio.fix_speeches_killing_music": { "title": "Layered enemy speech", "description": "Fixes enemies stopping the active music track when they speak.", }, "audio.fmv_volume": { "title": "FMV volume", "description": "Adjusts the movies volume.", }, "audio.inventory_ambient_volume": { "title": "Ambient volume (inventory)", "description": "Adjusts ambient volume in inventory screen.", }, "audio.inventory_music_volume": { "title": "Music volume (inventory)", "description": "Adjusts music volume in inventory screen.", }, "audio.load_music_triggers": { "title": "Fix one-shot music triggers", "description": "Loads previously triggered, one shot music so one shot music tracks do not replay.", }, "audio.master_volume": { "title": "\\{icon music} Master volume", "description": "Adjusts all ingame volume. The rest of the settings are relative to this volume.", }, "audio.music_load_condition": { "title": "Restore music on load", "description": "Loads the music track that was playing when the game was saved.\n\n- Never: do not restore music tracks on load.\n- Non-ambient: restore only non-ambient music tracks on load.\n- Always: restore any kind of music track on load.", }, "audio.music_volume": { "title": "Music volume", "description": "Adjusts music volume.", }, "audio.mute_out_of_focus": { "title": "Mute audio when focus lost", "description": "Mutes all music and sound effects when the game window is not focused.", }, "audio.sound_volume": { "title": "\\{icon sound} Sound volume", "description": "Adjusts sound effects volume.", }, "audio.underwater_ambient_volume": { "title": "Ambient volume (underwater)", "description": "Adjusts ambient volume when underwater.", }, "audio.underwater_music_volume": { "title": "Music volume (underwater)", "description": "Adjusts music volume when underwater.", }, "debug.enable_endless_flare_time": { "title": "Endless flare time", "description": "Prevents the handheld flares from ever going out. Thrown flares will still go out as normal.", }, "debug.enable_endless_sprint": { "title": "Endless sprint", "description": "Prevents Lara from ever getting tired when sprinting. Obstacles will still bring her to a stop.", }, "gameplay.ally_hostility_policy": { "title": "Ally hostility policy", "description": "Controls how friendly units react when taking damage.\n\n- Individual: each ally changes hostility on their own (TR3 style).\n- Shared: all allies become hostile together (TR2 monk style).", }, "gameplay.camera_speed": { "title": "Camera speed", "description": "Changes how fast the manual camera moves.", }, "gameplay.change_pierre_spawn": { "title": "Change Pierre spawn mode", "description": "Makes a freshly triggered (runaway) Pierre replace an already existing (runaway) Pierre.", }, "gameplay.creature_drown_policy": { "title": "Creature drown policy", "description": "Controls how land creatures behave in water rooms.\n\n- Never: land creatures will never drown (TR1 style).\n- Default: land creatures will drown in 2-click or deeper water (TR2/3 style).\n- Submerged: land creatures will drown only when fully submerged.", }, "gameplay.disable_extra_guns": { "title": "Remove extra guns", "description": "Removes all weapon and ammo pickups from the game except Pistols (for Pistols Only challenge runs).", }, "gameplay.disable_healing_between_levels": { "title": "Persistent damage", "description": "Stops Lara from healing when starting a new level (for no Heal challenge runs).", }, "gameplay.disable_medpacks": { "title": "Remove medipacks", "description": "Removes all medipacks from the game (for No Meds challenge runs).", }, "gameplay.disable_trex_collision": { "title": "Remove dead T-Rex collision", "description": "Removes all collision with T-Rex upon death. This helps when the T-Rex's body blocks the passage out.", }, "gameplay.enable_ally_targeting": { "title": "Allow targeting allies", "description": "Allows Lara to target allies, such as monks. If disabled, allies will be immune to Lara's ammunition.", }, "gameplay.enable_auto_item_selection": { "title": "Key item pre-selection", "description": "When Lara presses action against a keyhole or puzzle slot, and she has the corresponding item in the inventory, that item will be pre-selected.", }, "gameplay.enable_back_slope_stumble": { "title": "Backwards slope stumble", "description": "Makes Lara perform a stumble if she hops back and there is a slope behind her (TR3). If disabled, Lara will come to a hard stop against the slope (TR1/2).", }, "gameplay.enable_body_bags": { "title": "Body bag triggers", "description": "Enables removal of killed enemies when Lara crosses specific triggers in certain levels. If disabled, dead enemies will always be drawn.", }, "gameplay.enable_boulder_shake": { "title": "Enable boulder shake", "description": "If enabled, the camera will shake when a boulder is in motion.", }, "gameplay.enable_bouncy_grenades": { "title": "Bouncy grenades", "description": "Enables TR3-style grenade behavior: they ricochet off walls and slopes and produce a larger blast radius, but at the expense of reduced velocity.", }, "gameplay.enable_cheats": { "title": "Cheats", "description": "Enables various cheats:\n\n- L: immediately end the level.\n- I: give Lara all weapons; a boost of ammo and medipacks; and all plot items for the current level.\n- O: enable fly cheat (swimming midair).\n - WALK key: exit fly mode.\n - GUN key: open the closest door (doesn't work in some places).", }, "gameplay.enable_cinematics": { "title": "Cinematics", "description": "Enables cinematics at the beginning of certain levels that have them defined.", }, "gameplay.enable_compass_stats": { "title": "Level statistics in compass", "description": "Enables showing level statistics when the compass is selected.", }, "gameplay.enable_console": { "title": "Console", "description": "Enables the developer console.", }, "gameplay.enable_controlled_drops": { "title": "Controlled drops", "description": "Allows Lara to turn mid-air and grab the ledge she just stepped off, if the action input is held while falling.", }, "gameplay.enable_crawl_jump": { "title": "Crawl exit jump", "description": "Allows Lara to jump out of crawlspaces.", }, "gameplay.enable_crawl_tilt": { "title": "Crawl tilt", "description": "Aligns Lara's rotation to the floor geometry when crawling.", }, "gameplay.enable_crawling": { "title": "Crawling", "description": "Allows Lara to crouch and crawl.", }, "gameplay.enable_credits": { "title": "Credit screens", "description": "Enables credits screens shown after completing the game. Does not influence the final statistics screen.", }, "gameplay.enable_crouch_roll": { "title": "Crouch roll", "description": "Allows Lara to do a forward roll while crouched by pressing sprint.", }, "gameplay.enable_cutscenes": { "title": "Cutscenes", "description": "Enables cutscenes playing.", }, "gameplay.enable_demo": { "title": "Demo mode", "description": "Enables demos showing in the main menu.", }, "gameplay.enable_enemy_rotation": { "title": "Randomize enemy start angle", "description": "Applies an additional random angle to some enemies when they are initialised.", }, "gameplay.enable_enhanced_saves": { "title": "Save effects", "description": "Enhances savegames so that graphic effects, waterfall mist, flame emitters, and more are saved instead of disappearing on load.", }, "gameplay.enable_fmv": { "title": "FMVs", "description": "Enables FMVs playing.", }, "gameplay.enable_game_modes": { "title": "Game mode selection", "description": "Allows new game plus options to be selected from the new game passport menu.\n\n- New Game+: unlocks all weapons with infinite ammo; enemies have double the HP.\n- Japanese NG: weapons do double damage and flare pickups contain 8 rather than 6.\n- Japanese NG+: combination of New Game+ and Japanese NG.", }, "gameplay.enable_idle_pose_camera": { "title": "Idle pose camera", "description": "Adjusts the camera to face Lara during her idle pose animation. Pressing look will reset the camera.", }, "gameplay.enable_inverted_look": { "title": "Inverted look", "description": "Inverts the Y axis controls when Lara looks.", }, "gameplay.enable_item_examining": { "title": "Item examination", "description": "For custom levels - allows item descriptions to be displayed in the inventory where the level author has provided suitable data.", }, "gameplay.enable_jump_twists": { "title": "Jump twists", "description": "Enables jump twists and somersaults i.e. press roll during jump and swan dive animations.", }, "gameplay.enable_killer_pushblocks": { "title": "Enable killer pushblocks", "description": "If enabled, when a pushblock falls from the air and lands on Lara, it will kill her outright. Otherwise, Lara will clip on top of the block and survive.", }, "gameplay.enable_lean_jumping": { "title": "Lean jumping", "description": "Allows Lara to creep forwards or backwards further when performing neutral jumps with the relevant input key pressed.", }, "gameplay.enable_ledge_jumps": { "title": "Ledge jumps", "description": "Allows Lara to jump upwards or backwards when hanging from a ledge, provided she has a solid surface in front of her to push against.", }, "gameplay.enable_legal": { "title": "Legal screens", "description": "Enables legal screen and Core Design FMV at the game start.", }, "gameplay.enable_manual_camera": { "title": "Manual camera", "description": "Enables the camera keys (\\{input camera_forward}\\{input camera_back}\\{input camera_left}\\{input camera_right}) used to control Photo Mode camera, to also rotate the ingame camera.", }, "gameplay.enable_neutral_twists": { "title": "Neutral twists", "description": "Allows Lara to twist in the air while performing a neutral jump. Press jump and roll inputs together while stationary.", }, "gameplay.enable_pickup_aids": { "title": "Pickup aids", "description": "Enables an intermittent twinkling effect near pickup items to highlight their presence.", }, "gameplay.enable_play_previous_levels": { "title": "Play previous levels", "description": "Enables the \"Play previous levels\" and \"Story so far...\" features in the New Game selection screen.", }, "gameplay.enable_responsive_crawl": { "title": "Responsive crawling", "description": "Enables enhancements over original crawling mechanics.\n\n- Allows resuming crawling more quickly after coming to a stop.\n- Allows transitioning from run/sprint to crawl without first coming to a stop.\n- Allows transitioning from crawl to crouch-roll (if enabled) without manually crouching first.\n- Allows turning while crouched.\n- Restores Lara's crawl pickup animation (excluding flares).", }, "gameplay.enable_responsive_sprint": { "title": "Responsive sprinting", "description": "Enables a more responsive sprinting state for Lara.\n\n- allows sprinting whenever Lara has energy, rather than only when her stamina is full.\n- allows sprinting up stairs rather than being interrupted by Lara's regular run animation.", }, "gameplay.enable_save_crystals": { "title": "Save crystals", "description": "Limits saving to the beginning of levels and save crystals. Levels have limited, single use save crystals like the PS1 version. Changing this option will require restarting the level.", }, "gameplay.enable_slide_to_run": { "title": "Slide-to-run", "description": "Allows Lara to start running immediately when she reaches ground after sliding forwards on a slope. Hold the forward input to activate.", }, "gameplay.enable_slow_ledge_swing": { "title": "Slow ledge swing", "description": "Allows Lara to swing slowly when she has grabbed a very thin ledge (TR3 style). If disabled, Lara will swing briefly before coming to a resting hanging position (TR1/2 style).", }, "gameplay.enable_smooth_wall_deflect": { "title": "Smooth wall deflection", "description": "Allows Lara to recover more quickly after hitting a wall and a direction key is held together with forward.", }, "gameplay.enable_soft_statics": { "title": "Soft static collision", "description": "Allows Lara to move smoothly against static meshes - similar to TR4+ - rather than coming to a hard stop.", }, "gameplay.enable_sprint": { "title": "Sprinting", "description": "Allows Lara to sprint, similar to TR3+.", }, "gameplay.enable_step_roll_boost": { "title": "Step roll boost", "description": "Allows Lara to be boosted off a one-click high step if roll is pressed near the edge.", }, "gameplay.enable_swing_cancel": { "title": "Swing cancels", "description": "Allows Lara's ledge-swinging animation to be canceled by letting go and quickly grabbing again.", }, "gameplay.enable_target_change": { "title": "Target change", "description": "Enables TR4+ target changing while aiming weapons. Press the Change Target button while aiming to change targets.", }, "gameplay.enable_timer_in_inventory": { "title": "Timer counts in inventory", "description": "Makes the in-game timer work even while the game is showing the inventory.", }, "gameplay.enable_toggle_crouch": { "title": "Toggle crouch", "description": "Allows Lara to stay crouched after pressing the crouch input once. Press crouch again to stand up.", }, "gameplay.enable_toggle_sprint": { "title": "Toggle sprint", "description": "Allows Lara to keep sprinting after pressing the sprint input once. Press sprint again to stop sprinting.", }, "gameplay.enable_total_stats": { "title": "Final statistics screen", "description": "Enables a total game statistics screen that plays after the credits.", }, "gameplay.enable_tr2_jumping": { "title": "Responsive jumping", "description": "Allows Lara to jump at any point while running.", }, "gameplay.enable_tr2_swim_cancel": { "title": "Responsive swim cancel", "description": "Allows Lara to stop more responsively underwater when the swim key is released.", }, "gameplay.enable_tr2_swimming": { "title": "Smooth swimming", "description": "Gives Lara's underwater turn rate an acceleration curve for smoother movement, as per TR2+ originally. Disabling this option will give Lara a snappier turn rate, per original TR1.", }, "gameplay.enable_uw_roll": { "title": "Underwater roll", "description": "Allows Lara to roll while underwater.", }, "gameplay.enable_wading": { "title": "Wading", "description": "Allows Lara to wade through shallow water, rather than becoming stuck on the water surface.", }, "gameplay.enable_walk_to_items": { "title": "Animated interactions", "description": "Makes Lara walk to pickups and switches when nearby instead of teleporting to them.", }, "gameplay.fix_alligator_ai": { "title": "Fix alligator AI", "description": "Fixes alligators dealing no damage if Lara remains still in the water.", }, "gameplay.fix_bear_ai": { "title": "Fix bear AI", "description": "Fixes the bear pat attack so it does not miss Lara.", }, "gameplay.fix_bridge_collision": { "title": "Fix bridge collision", "description": "Fixes Lara not being able to grab parts of some bridges and invisible walls at the edge. Also fixes collision issues with drawbridges, trapdoors, and bridges when stacked over each other, over slopes, and near the ground.", }, "gameplay.fix_descending_glitch": { "title": "Fix breakable floor falls", "description": "Fixes sidestepping and walking backwards on breakable tiles causing Lara to immediately descend to the tile underneath.", }, "gameplay.fix_flare_throw_priority": { "title": "Fix flare throw priority", "description": "Fixes Lara prioritising throwing a spent flare while in mid-air, which can lead to being unable to grab ledges.", }, "gameplay.fix_floor_data_issues": { "title": "Fix floor data issues", "description": "Fixes original issues with floor data/triggers.", }, "gameplay.fix_free_flare_glitch": { "title": "Fix free flare glitch", "description": "Fixes the ability to spawn a free flare when pressing the flare input while picking up any item.", }, "gameplay.fix_item_duplication_glitch": { "title": "Fix item duplication glitch", "description": "Fixes the ability to duplicate usage of key items in the inventory.", }, "gameplay.fix_lara_pickup_embed": { "title": "Fix pickup embed glitch", "description": "Fixes Lara sometimes drifting into walls when collecting underwater items, and fixes Lara embedding under steeply sloped ceilings when picking up an item above water.", }, "gameplay.fix_m16_accuracy": { "title": "Fix M16/MP5 accuracy", "description": "Fixes the accuracy of the M16/MP5 while Lara is running.", }, "gameplay.fix_monkey_pickup_priority": { "title": "Fix monkey pickup priority", "description": "Attacked monkeys will prioritize retaliating over collecting Medi packs and Keys.", }, "gameplay.fix_pipeman_aim": { "title": "Fix pipeman aim", "description": "Fixes the pipeman sometimes not being able to aim darts at Lara correctly.", }, "gameplay.fix_qwop_glitch": { "title": "Fix QWOP glitch", "description": "Fixes Lara jumping on small steps sometimes resulting in a weird running animation, known as a QWOP state.", }, "gameplay.fix_step_glitch": { "title": "Fix step glitch", "description": "Fixes Lara sometimes being pushed into walls adjacent to steps when running up them in a specific way.", }, "gameplay.fix_wade_wall_hit": { "title": "Fix wading wall hit", "description": "Fixes Lara not responding to hitting a wall while wading.", }, "gameplay.fix_walk_run_jump": { "title": "Fix walk run jump", "description": "Fixes Lara at times not being able to jump immediately after going from her walking to running animation.", }, "gameplay.fix_wall_geometry": { "title": "Fix wall geometry", "description": "Fixes cases in OG level geometry where tilts inside walls can lead to inaccurate height calculations.", }, "gameplay.fix_water_exit": { "title": "Fix water exit", "description": "Fixes Lara being able to go directly from a water room to an adjacent dry room, or to a dry room below. Additionally, this will prevent Lara from being able to climb out of water onto non-standable slopes.", }, "gameplay.harpoon_recoil": { "title": "Harpoon recoil", "description": "Sets how often Lara must reload the harpoon gun, based on her current ammo count. For example, if set to 3, she'll need to reload after every third shot. Set to 0 to disable reloading entirely.", }, "gameplay.idle_pose_timeout": { "title": "Idle pose timeout", "description": "Allows Lara to enter a pose animation when she has been idle for the specified number of seconds. Set to 0 to disable.", }, "gameplay.jump_lock_mode": { "title": "Jump lock mode", "description": "For responsive jumping, allows controlling how soon after starting to run that Lara is permitted to jump.\n\n- Legacy: matches original TR2 timing.\n- Tuned: jumping is possible 2 frames earlier.\n- Disabled: jumping is possible immediately after the start-to-run animation.", }, "gameplay.loading_screens": { "title": "Loading screens", "description": "Controls loading screens before level loads.\n\n- Disabled: never show loading screens.\n- Always: show loading screens.\n- New games: skip showing loading screens when loading a save.", }, "gameplay.look_mode": { "title": "Look mode", "description": "Allows controlling when Lara is able to use look.\n\n- Restricted: look is only permitted when Lara is stationary, and never when underwater.\n- Enhanced: look is permitted during most animations, aside from ones such as pushing a block.\n- Unrestricted: look is permitted at any time during normal Lara control.", }, "gameplay.maximum_quick_save_slots": { "title": "Number of quick save slots", "description": "Changes the number of available quick save slots.", }, "gameplay.maximum_save_slots": { "title": "Number of save slots", "description": "Changes the number of available save slots.", }, "gameplay.pause_on_focus_lost": { "title": "Pause when focus lost", "description": "Stops gameplay from advancing when the game window loses focus.", }, "gameplay.projectile_area_damage": { "title": "Projectile area damage", "description": "Controls how the area-of-effect for Rocket Launcher and Grenade Launcher propagates.\n\n- Single-sweep: TR1 & TR2 behavior.\n- Multi-sweep: TR3 behavior.\n\nThe multi-sweep option often ends up doing double damage to individual enemies.", }, "gameplay.remember_gun_status": { "title": "Remember guns between levels", "description": "Makes Lara remember which gun she was using last in the previous level when starting a new level. If disabled, Lara will revert to holstered pistols.", }, "gameplay.restore_ps1_enemies": { "title": "Restore PS1 enemies", "description": "Adds the mummy that appears in the PlayStation version of City of Khamoon, room 25.\nChanging this option will require restarting the game.", }, "gameplay.start_lara_hitpoints": { "title": "Lara's starting health", "description": "Sets Lara's health value for the beginning of each level.", }, "gameplay.target_mode": { "title": "Weapon lock mode", "description": "Changes the behavior of how weapons lock onto targets.\n\n- Full lock: always keep target lock even if the enemy moves out of sight or dies (OG TR1-3).\n- Semi lock: keep target lock if the enemy moves out of sight but lose target lock if the enemy dies.\n- No lock: lose target lock if the enemy goes out of sight or dies (TR4+).", }, "gameplay.wall_glitch_mode": { "title": "Wall glitch mode", "description": "Allows using TR1 wall glitch behavior in TR2 and vice-versa; equally allows fixing all types of wall glitch.", }, "input.enable_buffering_func_keys": { "title": "Buffering (F-keys)", "description": "Enables F-key (1-frame) buffering to achieve precise control of Lara's movement. This function originally only exists in the TombATI port (TR1).", }, "input.enable_buffering_inventory": { "title": "Buffering (inventory)", "description": "Enables inventory (2-frame) buffering to achieve precise control of Lara's movement.", }, "input.enable_responsive_passport": { "title": "Responsive passport", "description": "Disables blocking user input when passport flips pages, scheduling the page flips instead.", }, "input.enable_tr3_sidesteps": { "title": "Enhanced sidesteps", "description": "Enables TR3+ style sidesteps, e.g. shift+directional arrows. Dedicated sidestep buttons will still work.", }, "input.quick_guns_mode": { "title": "Quick gun keys", "description": "Controls the behavior of the quick gun equip keys.\n\n- Draw only: pressing a key will cause Lara to equip the assigned gun.\n- Draw or holster: same as above, plus Lara will undraw the assigned gun if she's currently carrying it.", }, "language": { "title": "Language", "description": "Changes the language of the UI text.", }, "rendering.anisotropy_filter": { "title": "Anisotropy filter", "description": "Enhances texture filtering at distances.", }, "rendering.aspect_mode": { "title": "Aspect mode", "description": "Forces certain game aspect ratios with letterbox.", }, "rendering.borders": { "title": "Borders", "description": "Adds black borders around the game window.", }, "rendering.enable_trapezoid_filter": { "title": "Trapezoid filter", "description": "Corrects rendering of quadrilaterals.", }, "rendering.enable_vsync": { "title": "VSync", "description": "Turns V-Sync on or off.", }, "rendering.fps": { "title": "FPS", "description": "Sets game frames per second.", }, "rendering.lighting_contrast": { "title": "Lighting contrast", "description": "Boosts contrast for dynamic light sources such as flares and gun flashes.", }, "rendering.screenshot_format": { "title": "Screenshot format", "description": "Screenshot file format.", }, "rendering.sprite_lock_mode": { "title": "Sprite lock mode", "description": "Controls which axes to lock when showing sprites on the screen.\n\n- None: show the sprites normally.\n- Roll: lock the roll axis – useful only in photo mode.\n- Roll & pitch: ensure the sprites stand upright and do not lie on the ground when looking at them from above.\n- Perspective: lock roll and pitch axes and addititonally, rotate the sprites slightly towards the center of the screen.", }, "rendering.texture_filter": { "title": "Texture filter", "description": "Switches between smooth and pixel ingame textures.", }, "rendering.ui_filter": { "title": "UI filter", "description": "Switches between smooth and pixel UI textures.", }, "rendering.upscaling_factor": { "title": "Upscaling factor", "description": "Upscales game by a set factor, maintaining pixellated look.", }, "rendering.upscaling_filter": { "title": "Upscaling filter", "description": "Switches smooth or pixel look for the whole screen.", }, "ui.airbar_color": { "title": "Airbar color", "description": "Color of the airbar.", }, "ui.airbar_color_ps1": { "title": "Airbar color", "description": "Color of the airbar.", }, "ui.airbar_location": { "title": "Airbar location", "description": "Location where the airbar is displayed.", }, "ui.ammo_counter_location": { "title": "Ammo counter location", "description": "Location where the ammo counter is displayed.", }, "ui.bar_look": { "title": "Bars appearance", "description": "Controls the visual appearance of the UI bars.", }, "ui.bar_scale": { "title": "Bars scale", "description": "Changes size of UI bars.", }, "ui.enable_bar_flashing": { "title": "Flash bars", "description": "Makes Lara's health and oxygen bars blink when she's running low on either resource.", }, "ui.enable_smooth_bars": { "title": "Smooth bars", "description": "Makes the UI bars use smooth color transitions.", }, "ui.enable_wraparound": { "title": "Scroll wrap", "description": "Lets directional navigation in menus loop around.", }, "ui.enemy_healthbar_color": { "title": "Enemy bar color", "description": "Color of the enemy healthbar.", }, "ui.enemy_healthbar_color_allies": { "title": "Ally bar color", "description": "Color of the allies healthbar. Shown in the location of the enemy healthbars.", }, "ui.enemy_healthbar_color_allies_ps1": { "title": "Ally bar color", "description": "Color of the allies healthbar. Shown in the location of the enemy healthbars.", }, "ui.enemy_healthbar_color_ps1": { "title": "Enemy bar color", "description": "Color of the enemy healthbar.", }, "ui.enemy_healthbar_location": { "title": "Enemy bar location", "description": "Location where the enemy healthbar is displayed.", }, "ui.enemy_healthbar_show_mode": { "title": "Enemy bar mode", "description": "Enables showing a healthbar for the active enemy.", }, "ui.exposurebar_color": { "title": "Exposure bar color", "description": "Color of the cold water exposure bar.", }, "ui.exposurebar_color_ps1": { "title": "Exposure bar color", "description": "Color of the cold water exposure bar.", }, "ui.exposurebar_location": { "title": "Exposure bar location", "description": "Location where the cold water exposure bar is displayed.", }, "ui.healthbar_color": { "title": "Healthbar color", "description": "Color of the healthbar.", }, "ui.healthbar_color_ps1": { "title": "Healthbar color", "description": "Color of the healthbar.", }, "ui.healthbar_location": { "title": "Healthbar location", "description": "Location where the healthbar is displayed.", }, "ui.healthbar_poison_color": { "title": "Poison healthbar color", "description": "Color of the healthbar when Lara is poisoned.", }, "ui.healthbar_poison_color_ps1": { "title": "Poison healthbar color", "description": "Color of the healthbar when Lara is poisoned.", }, "ui.inventory_background_style": { "title": "Inventory background", "description": "Changes the way the background for the inventory ring is displayed.\n\n- Dark: TR1 (PC).\n- Very dark: TR1 (PS1).\n- Static: TR2 (PC).\n- Wave: TR2 (PS1).\n- Monochrome: TR3.", }, "ui.inventory_fade_effects": { "title": "Inventory fade effects", "description": "Fine-tunes the fade effects to be enabled or disabled in the in-game inventory ring. Needs the Fade Effects option to be enabled to work.", }, "ui.menu_style": { "title": "Menu style", "description": "Changes how menus are displayed.\n\n - PC: UI style matches the PC version.\n - PS1: UI style matches the PS1 version.", }, "ui.pause_background_style": { "title": "Pause background", "description": "Changes the way the background for the pause screen is displayed.\n\n- Dark: TR1 (PC).\n- Very dark: TR1 (PS1).\n- Static: TR2 (PC).\n- Wave: TR2 (PS1).\n- Monochrome: TR3.", }, "ui.pause_fade_effects": { "title": "Pause fade effects", "description": "Fine-tunes the fade effects to be enabled or disabled in the pause screen. Needs the Fade Effects option to be enabled to work.", }, "ui.pickup_scale": { "title": "Pickup scale", "description": "Changes size of items animated in the UI when Lara picks something up.", }, "ui.show_bars": { "title": "Show bars", "description": "Disables all ingame bars, obscuring information on Lara's health and other resources (for challenge runs).", }, "ui.show_pickups_overlay": { "title": "Pickups overlay", "description": "Shows items in the bottom-right corner when Lara picks something up.", }, "ui.show_title_version": { "title": "Title version text", "description": "Shows the TRX version string in the title inventory ring.", }, "ui.sprintbar_color": { "title": "Sprintbar color", "description": "Color of the sprintbar.", }, "ui.sprintbar_color_ps1": { "title": "Sprintbar color", "description": "Color of the sprintbar.", }, "ui.sprintbar_location": { "title": "Sprintbar location", "description": "Location where the sprintbar is displayed.", }, "ui.stats.show_ammo": { "title": "Ammo hits/used", "description": "Shows the ammo row in the level statistics.", }, "ui.stats.show_crystals": { "title": "Crystals", "description": "Shows the crystals row in the level statistics.", }, "ui.stats.show_deaths": { "title": "Deaths", "description": "Shows Lara's deaths in the compass statistics and in the level statistics. Death count is updated in the currently loaded save as soon as Lara dies.", }, "ui.stats.show_distance_travelled": { "title": "Distance traveled", "description": "Shows the distance traveled row in the level statistics.", }, "ui.stats.show_kills": { "title": "Kills", "description": "Shows the kills row in the level statistics.", }, "ui.stats.show_level_header": { "title": "Level counter", "description": "Shows the current level number at the top of the level statistics.", }, "ui.stats.show_medipacks_used": { "title": "Health packs used", "description": "Shows the health packs used row in the level statistics.", }, "ui.stats.show_pickups": { "title": "Pickups", "description": "Shows the pickups row in the level statistics.", }, "ui.stats.show_secrets": { "title": "Secrets found", "description": "Shows the secrets found row in the level statistics.", }, "ui.stats.show_time_taken": { "title": "Time taken", "description": "Shows the time taken row in the level statistics.", }, "ui.stats.show_totals": { "title": "Show totals", "description": "Shows totals next to stats when applicable. Secrets remain unaffected by this setting.", }, "ui.stats.style": { "title": "Statistics style", "description": "Controls how the statistics dialog is displayed.\n\n- Bare: shows the simpler unframed layout.\n- Bordered: shows the boxed layout.", }, "ui.stats_background_style": { "title": "Stats background", "description": "Changes the way the background for the end of level stats is displayed.\n\n- Dark: TR1 (PC).\n- Very dark: TR1 (PS1).\n- Static: TR2 (PC).\n- Wave: TR2 (PS1).\n- Monochrome: TR3.", }, "ui.stats_fade_effects": { "title": "Stats fade effects", "description": "Fine-tunes the fade effects to be enabled or disabled in the end of the level statistics screen. Needs the Fade Effects option to be enabled to work.", }, "ui.text_scale": { "title": "Text scale", "description": "Changes the size of UI text.", }, "visuals.blood_effects": { "title": "Blood effects", "description": "Controls blood spark colors.\n\n- Disabled: no blood sparks are shown.\n- Pink: the default in German PC releases of TR3.\n- Red: the default in all other retail releases.", }, "visuals.camera_mode": { "title": "Camera mode", "description": "Adjusts how camera behaves during actions like using keys.", }, "visuals.enable_3d_pickups": { "title": "3D pickups", "description": "Enables 3D models to be rendered in place of the sprites for pickup items.", }, "visuals.enable_braid": { "title": "Lara's braid", "description": "Enables Lara's braid.", }, "visuals.enable_breeze": { "title": "Breeze", "description": "Enables the breeze effect on Lara's braid in appropriate rooms.", }, "visuals.enable_exit_fade_effects": { "title": "Fade on game exit", "description": "Enables the fade effects when exiting the game to desktop.", }, "visuals.enable_fade_effects": { "title": "Fade effects", "description": "Enable fade transitions, for example between credit graphics or for inventory and pause screen transitions.", }, "visuals.enable_fire_lighting": { "title": "Fire lighting", "description": "Enables dynamic lighting to be generated beside active flames.", }, "visuals.enable_footprints": { "title": "Footprints", "description": "Enables rendering of Lara's footprints on certain surfaces in supported levels.", }, "visuals.enable_glide_cameras": { "title": "Glide cameras", "description": "Enables a glide action on fixed cameras that look at Lara by adopting a smooth speed curve. If disabled, such cameras will change the view to look at Lara immediately.", }, "visuals.enable_gun_lighting": { "title": "Gun lighting", "description": "Enables dynamic lighting to be generated for gunshots and explosions.", }, "visuals.enable_ps1_crystals": { "title": "PS1 crystal tint", "description": "Save crystals will be drawn with a purple tint, more similar to the PS1 type.", }, "visuals.enable_reflections": { "title": "Reflections", "description": "Enables reflections on certain objects.", }, "visuals.enable_responsive_mesh_tint": { "title": "Responsive mesh tint", "description": "Enables Lara's individual meshes to be drawn with a water tint if they are themselves located underwater (TR3 style). Otherwise, if Lara is in water, each of her meshes will be drawn with the tint (TR1/2 style).", }, "visuals.enable_shotgun_flash": { "title": "Shotgun flash", "description": "Draws flames when firing the shotgun, like for other guns.", }, "visuals.enable_skybox": { "title": "Skyboxes", "description": "Enables the skybox in supported levels.", }, "visuals.enable_weather": { "title": "Weather", "description": "Enables rendering of weather effects in supported levels.", }, "visuals.fix_animated_sprites": { "title": "Fix sprite animations", "description": "Fixes original underwater plant sprites so they animate properly in water areas.", }, "visuals.fix_item_rots": { "title": "Fix item rotation issues", "description": "Fixes original issues with some incorrectly rotated pickups when using the 3D pickups option.", }, "visuals.fix_texture_issues": { "title": "Fix texture issues", "description": "Fixes original issues with missing or incorrect textures/meshes.", }, "visuals.fog_color": { "title": "Fog color", "description": "Color of the fog.", }, "visuals.fog_end": { "title": "Fog end", "description": "Sets distance in tiles where fog makes everything fully obscured.", }, "visuals.fog_start": { "title": "Fog start", "description": "Sets distance in tiles where fog begins to appear.", }, "visuals.fog_transparency": { "title": "Fog transparency", "description": "Whether to enable blending distant geometry into 100% transparent faces.", }, "visuals.fov": { "title": "Field of view", "description": "Viewing angle in degrees. Larger values widen the field of view, smaller ones narrow it.", }, "visuals.game_brightness": { "title": "Game brightness", "description": "Changes game brightness.", }, "visuals.gamma": { "title": "Gamma", "description": "Adjusts the gamma curve. Higher values mean brighter lighting. The value of 2.5 means default colors.", }, "visuals.lara_outfit": { "title": "Lara's outfit", "description": "Changes Lara's appearance. Choosing Default will respect any regular outfit changes between levels, otherwise the chosen value will persist until changed manually.", }, "visuals.shadow_type": { "title": "Shadows shape", "description": "Selects how entity shadows are rendered.\n\n- Octagon: old TR1 and TR2 shadows\n- Circle: round shadows\n- Sprite: TR3 texture-based shadows", }, "visuals.sunglasses_mode": { "title": "Lara's sunglasses", "description": "Changes the style of Lara's sunglasses. Note that lenses will be reflective if the relevant option is enabled.\n\n- Off: Lara will not wear sunglasses.\n- Opaque: Lara's sunglasses will have opaque lenses.\n- Transparent: Lara's sunglasses will have semi-transparent lenses.", }, "visuals.ui_brightness": { "title": "UI brightness", "description": "Changes UI brightness.", }, "visuals.water_color": { "title": "Water color", "description": "Color of the water.", } }, "objects": { "alarm_sound": { "name": "Alarm", }, "alligator": { "name": "Alligator", }, "alphabet": { "name": "Default font", }, "alphabet_small": { "name": "Small font", }, "amber_light": { "name": "Amber Light", }, "animating_1": { "name": "Animating Object 1", }, "animating_10": { "name": "Animating Object 10", }, "animating_2": { "name": "Animating Object 2", }, "animating_3": { "name": "Animating Object 3", }, "animating_4": { "name": "Animating Object 4", }, "animating_5": { "name": "Animating Object 5", }, "animating_6": { "name": "Animating Object 6", }, "animating_7": { "name": "Animating Object 7", }, "animating_8": { "name": "Animating Object 8", }, "animating_9": { "name": "Animating Object 9", }, "ape": { "name": "Ape", }, "area_51_rocket": { "name": "Area 51 Rocket", }, "area_51_rocket_blast": { "name": "Area 51 Rocket Blast", }, "area_51_rocket_support": { "name": "Area 51 Rocket Support", }, "assault_digits": { "name": "Assault Digits", }, "assault_target": { "name": "Assault Target", }, "atlantean_ground": { "name": "Ground Atlantean", }, "atlantean_shooter": { "name": "Shooting Atlantean", }, "atlantean_winged": { "name": "Winged Atlantean", }, "autos": { "name": "Automatic Pistols", }, "autos_ammo": { "name": "Automatic Pistol Clips", }, "bacon_lara": { "name": "Bacon Lara", }, "baldy": { "name": "Baldy", }, "bandit_1": { "name": [ "Mercenary 1", "Masked Goon 1", ] }, "bandit_2": { "name": [ "Mercenary 2", "Masked Goon 2", ] }, "bandit_2b": { "name": [ "Mercenary 3", "Masked Goon 3", ] }, "barracuda": { "name": "Barracuda", }, "bartoli": { "name": "Marco Bartoli", }, "bat": { "name": "Bat", }, "bat_emitter": { "name": "Bat Emitter", }, "beacon_light": { "name": "Beacon Light", }, "bear": { "name": "Bear", }, "bell": { "name": "Bell", }, "big_bowl": { "name": "Lava Bowl", }, "big_eel": { "name": "Big Eel", }, "big_pod": { "name": "Big Pod", }, "big_spider": { "name": "Giant Spider", }, "bird_guardian": { "name": "Bird Monster", }, "bird_tweeter_1": { "name": "Dripping Water", }, "bird_tweeter_2": { "name": "Singing Birds", }, "blade": { "name": "Wall-mounted Blade", }, "blood": { "name": "Blood", }, "blood_pink": { "name": "Blood (censored)", }, "blue_light": { "name": "Blue Light", }, "boat": { "name": "Boat", }, "boat_bits": { "name": "Boat Bits", }, "body_part": { "name": "Body Part", }, "bridge_flat": { "name": "Bridge Flat", }, "bridge_tilt_1": { "name": "Bridge Tilt 1", }, "bridge_tilt_2": { "name": "Bridge Tilt 2", }, "bubble_1": { "name": "Bubble 1", }, "bubble_2": { "name": "Bubble 2", }, "bubble_emitter": { "name": "Bubble Emitter", }, "camera_target": { "name": "Camera Target", }, "carcass": { "name": "Carcass", }, "ceiling_spikes": { "name": "Spiky Ceiling", }, "centaur": { "name": "Centaur", }, "centaur_statue": { "name": "Centaur Statue", }, "civilian": { "name": "Civilian", }, "claw_mutant": { "name": "Claw Mutant", }, "clock_chimes": { "name": "Bartoli Hideout clock", }, "cog_1": { "name": "Cog 1", }, "cog_2": { "name": "Cog 2", }, "cog_3": { "name": "Cog 3", }, "combat_end": { "name": "Combat End", }, "compass": { "name": "Compass", }, "compy": { "name": "Compsognathus", }, "controls": { "name": "Controls", }, "copter": { "name": "Helicopter", }, "cowboy": { "name": "Cowboy", }, "crawler_mutant": { "name": "Crawler Mutant", }, "crocodile": { "name": "Crocodile", }, "crow": { "name": "Crow", }, "cult_1": { "name": "Masked Goon 1", }, "cult_1a": { "name": "Masked Goon 2", }, "cult_1b": { "name": "Masked Goon 3", }, "cult_2": { "name": "Knife Thrower", }, "cult_3": { "name": "Shotgun Goon", }, "cut_shotgun": { "name": "Shotgun Shower Animation", }, "damocles_sword": { "name": "Damocles Sword", }, "dart": { "name": "Dart", }, "dart_effect": { "name": "Dart Effect", }, "dart_emitter": { "name": "Dart Emitter", }, "desert_eagle": { "name": "Desert Eagle", }, "desert_eagle_ammo": { "name": "Desert Eagle Clips", }, "detonator_box": { "name": "Detonator Box", }, "ding_dong": { "name": "Doorbell", }, "dino_mutant": { "name": "Dino Mutant", }, "disc": { "name": "Disc", }, "disc_emitter": { "name": "Disc Emitter", }, "disposable_animating_1": { "name": "Disposable Animating Object 1", }, "disposable_animating_10": { "name": "Disposable Animating Object 10", }, "disposable_animating_2": { "name": "Disposable Animating Object 2", }, "disposable_animating_3": { "name": "Disposable Animating Object 3", }, "disposable_animating_4": { "name": "Disposable Animating Object 4", }, "disposable_animating_5": { "name": "Disposable Animating Object 5", }, "disposable_animating_6": { "name": "Disposable Animating Object 6", }, "disposable_animating_7": { "name": "Disposable Animating Object 7", }, "disposable_animating_8": { "name": "Disposable Animating Object 8", }, "disposable_animating_9": { "name": "Disposable Animating Object 9", }, "diver": { "name": "Scuba Diver", }, "dog": { "name": [ "Dog", "Doberman", ] }, "door_1": { "name": "Door 1", }, "door_2": { "name": "Door 2", }, "door_3": { "name": "Door 3", }, "door_4": { "name": "Door 4", }, "door_5": { "name": "Door 5", }, "door_6": { "name": "Door 6", }, "door_7": { "name": "Door 7", }, "door_8": { "name": "Door 8", }, "dragon_back": { "name": "Dragon Back", }, "dragon_bones_1": { "name": "Placeholder", }, "dragon_bones_2": { "name": "Dragon Bones Front", }, "dragon_bones_3": { "name": "Dragon Bones Back", }, "dragon_front": { "name": "Dragon Front", }, "drawbridge": { "name": "Drawbridge", }, "dust": { "name": "Dust", }, "dying_monk": { "name": "Dying monk", }, "dying_mutant": { "name": "Dying Mutant", }, "eagle": { "name": "Eagle", }, "earthquake": { "name": "Earthquake", }, "eel": { "name": "Eel", }, "electric_cleaner": { "name": "Electric Cleaner", }, "electric_fence": { "name": "Electric Fence", }, "electrical_light": { "name": "Electrical Light", }, "ember": { "name": "Ember", }, "ember_emitter": { "name": "Ember Emitter", }, "explosion_1": { "name": "Explosion 1", }, "explosion_2": { "name": "Explosion 2", }, "falling_block_1": { "name": [ "Falling Block 1", "Collapsible Floor 1", "Collapsible Tiles 1", ] }, "falling_block_2": { "name": [ "Falling Block 2", "Collapsible Floor 2", "Collapsible Tiles 2", ] }, "falling_block_3": { "name": [ "Falling Block 3", "Collapsible Floor 3", "Collapsible Tiles 3", ] }, "falling_ceiling_1": { "name": "Falling Ceiling 1", }, "falling_ceiling_2": { "name": "Falling Ceiling 2", }, "fire_head": { "name": "Fire Head", }, "fish_mutant": { "name": "Mutant Fish", }, "flame": { "name": [ "Flame", "Fire", ] }, "flame_emitter": { "name": [ "Flame Emitter", "Fire Emitter", ] }, "flame_emitter_big": { "name": [ "Flame Emitter (Big)", "Fire Emitter (Big)", ] }, "flame_emitter_jet": { "name": [ "Flame Emitter (Jet)", "Fire Emitter (Jet)", ] }, "flame_emitter_side": { "name": [ "Flame Emitter (Side)", "Fire Emitter (Side)", ] }, "flame_emitter_small": { "name": [ "Flame Emitter (Small)", "Fire Emitter (Small)", ] }, "flare": { "name": "Flare", }, "flare_fire": { "name": "Flare sparks", }, "flares_box": { "name": "Flares Box", }, "flickering_light": { "name": "Flickering Light", }, "fuse_box": { "name": "Fuse Box", }, "fx_reserved": { "name": "Gray disk", }, "gamma": { "name": "Gamma", }, "gas_emitter_green": { "name": "Gas Emitter (Green)", }, "general": { "name": "Minisub", }, "globe": { "name": "Globe", }, "glow": { "name": "Glow", }, "glow_reserved": { "name": "Map Glow", }, "gondola": { "name": "Gondola", }, "gong": { "name": "Gong", }, "gong_bonger": { "name": "Gong Stick", }, "graphics": { "name": "Graphics", }, "green_light": { "name": "Green Light", }, "grenade": { "name": "Grenade", }, "grenade_launcher": { "name": "Grenade Launcher", }, "grenade_launcher_ammo": { "name": "Grenades", }, "gun_flash": { "name": "Gun Flash", }, "gun_shell": { "name": "Gun Shell", }, "harpoon_bolt": { "name": "Harpoon Bolt", }, "harpoon_gun": { "name": "Harpoon Gun", }, "harpoon_gun_ammo": { "name": "Harpoons", }, "hook": { "name": "Hook", }, "hot_liquid": { "name": "Extra Fire", }, "huskie": { "name": [ "Dog", "Patrol Dog", "Huskie", ] }, "hybrid_mutant": { "name": "Hybrid Mutant", }, "icicle": { "name": "Icicles", }, "inv_background": { "name": "Menu Background", }, "jelly": { "name": "Jellyfish", }, "kayak": { "name": "Kayak", }, "key_1": { "name": "Key 1", }, "key_2": { "name": "Key 2", }, "key_3": { "name": "Key 3", }, "key_4": { "name": "Key 4", }, "key_hole_1": { "name": "Keyhole 1", }, "key_hole_2": { "name": "Keyhole 2", }, "key_hole_3": { "name": "Keyhole 3", }, "key_hole_4": { "name": "Keyhole 4", }, "kill_all_triggered": { "name": "Kill All Triggered", }, "killer_statue": { "name": "Statue with Sword", }, "lara": { "name": "Lara", }, "lara_alarm": { "name": "Alarm Bell", }, "lara_autos": { "name": "Automatic Pistols Animation", }, "lara_boat": { "name": "Boat Animation", }, "lara_desert_eagle": { "name": "Desert Eagle Animation", }, "lara_extra": { "name": "Lara's Extra Animation", }, "lara_flare": { "name": "Flare Animation", }, "lara_grenade": { "name": "Grenade Launcher Animation", }, "lara_hair": { "name": "Lara's Braid", }, "lara_harpoon": { "name": "Harpoon Animation", }, "lara_m16": { "name": "M16 Animation", }, "lara_magnums": { "name": "Magnums Animation", }, "lara_mp5": { "name": "MP5 Animation", }, "lara_pistols": { "name": "Pistols Animation", }, "lara_rocket": { "name": "Rocket Launcher Animation", }, "lara_shotgun": { "name": "Shotgun Animation", }, "lara_skidoo": { "name": "Snowmobile Animation", }, "lara_uzis": { "name": "Uzis Animation", }, "large_medipack": { "name": "Large Medipack", }, "larson": { "name": "Larson", }, "lava_wedge": { "name": "Lava Wedge", }, "lead_bar": { "name": "Lead Bar", }, "lift": { "name": "Lift", }, "lightning_emitter": { "name": "Lightning Emitter", }, "lion": { "name": "Lion", }, "lioness": { "name": [ "Lioness", "Lion", ] }, "lizard": { "name": "Lizard", }, "m16": { "name": "M16", }, "m16_ammo": { "name": "M16 Clips", }, "m16_flash": { "name": "M16 Flash", }, "magnums": { "name": "Magnums", }, "magnums_ammo": { "name": "Magnum Clips", }, "mesh_swap_1": { "name": "Mesh Swap 1", }, "mesh_swap_2": { "name": "Mesh Swap 2", }, "mesh_swap_3": { "name": "Mesh Swap 3", }, "midas_touch": { "name": "Midas Hand", }, "mine": { "name": "Aquatic Mine", }, "mine_cart": { "name": "Mine Cart", }, "mini_copter": { "name": "Helicopter 2", }, "missile_atlantean_bomb": { "name": "Missile (Atlantean Bomb)", }, "missile_atlantean_shard": { "name": "Missile (Atlantean Shard)", }, "missile_flame": { "name": "Missile (Flame)", }, "missile_harpoon": { "name": "Missile (Harpoon)", }, "missile_knife": { "name": "Missile (Knife)", }, "missile_poison": { "name": "Missile (Poison)", }, "monk_1": { "name": "Monk 1", }, "monk_2": { "name": "Monk 2", }, "monkey": { "name": "Monkey", }, "mounted_gun": { "name": "Mounted Gun", }, "mouse": { "name": "Rat", }, "movable_block_1": { "name": [ "Push Block 1", "Movable Block 1", ] }, "movable_block_2": { "name": [ "Push Block 2", "Movable Block 2", ] }, "movable_block_3": { "name": [ "Push Block 3", "Movable Block 3", ] }, "movable_block_4": { "name": [ "Push Block 4", "Movable Block 4", ] }, "moving_bar": { "name": "Moving Bar", }, "mp5": { "name": "MP5", }, "mp5_ammo": { "name": "MP5 Clips", }, "mp_1": { "name": "MP 1", }, "mp_2": { "name": "MP 2", }, "mummy": { "name": "Mummy", }, "natla": { "name": "Natla", }, "natla_gun": { "name": "Natla's Gun", }, "on_off_light": { "name": "On/Off Light", }, "orca": { "name": "Orca", }, "passport": { "name": "Game", }, "patrol_dog": { "name": [ "Dog", "Patrol Dog", ] }, "pda": { "name": "Gameplay", }, "pendulum_1": { "name": [ "Pendulum", "Sandbag", "Swinging box", ] }, "pendulum_2": { "name": [ "Pendulum", "Sandbag", "Swinging box", ] }, "photo": { "name": "Lara's Home", }, "pickup_1": { "name": "Pickup Item 1", }, "pickup_2": { "name": "Pickup Item 2", }, "pickup_aid": { "name": "Pickup Aid", }, "pierre": { "name": "Pierre", }, "pirahnas": { "name": "Pirahnas", }, "pistols": { "name": "Pistols", }, "pistols_ammo": { "name": "Pistol Clips", }, "player_1": { "name": "Cutscene Actor 1", }, "player_10": { "name": "Cutscene Actor 10", }, "player_2": { "name": "Cutscene Actor 2", }, "player_3": { "name": "Cutscene Actor 3", }, "player_4": { "name": "Cutscene Actor 4", }, "player_5": { "name": "Cutscene Actor 5", }, "player_6": { "name": "Cutscene Actor 6", }, "player_7": { "name": "Cutscene Actor 7", }, "player_8": { "name": "Cutscene Actor 8", }, "player_9": { "name": "Cutscene Actor 9", }, "pods": { "name": "Pod", }, "poison_dart": { "name": "Poison Dart", }, "poison_dart_emitter": { "name": "Poison Dart Emitter", }, "portacabin": { "name": "Portable Cabin", }, "power_saw": { "name": "Power Saw", }, "prisoner": { "name": "Prisoner", }, "propeller_1": { "name": "Airplane Propeller", }, "propeller_2": { "name": "Underwater Propeller", }, "propeller_3": { "name": "Air Fan", }, "pulse_light": { "name": "Pulse Light", }, "puma": { "name": "Puma", }, "punk_1": { "name": "Punk 1", }, "punk_2": { "name": "Punk 2", }, "puzzle_1": { "name": "Puzzle Item 1", }, "puzzle_2": { "name": "Puzzle Item 2", }, "puzzle_3": { "name": "Puzzle Item 3", }, "puzzle_4": { "name": "Puzzle Item 4", }, "puzzle_done_1": { "name": "Puzzle Hole 1 (Done)", }, "puzzle_done_2": { "name": "Puzzle Hole 2 (Done)", }, "puzzle_done_3": { "name": "Puzzle Hole 3 (Done)", }, "puzzle_done_4": { "name": "Puzzle Hole 4 (Done)", }, "puzzle_hole_1": { "name": "Puzzle Hole 1 (Empty)", }, "puzzle_hole_2": { "name": "Puzzle Hole 2 (Empty)", }, "puzzle_hole_3": { "name": "Puzzle Hole 3 (Empty)", }, "puzzle_hole_4": { "name": "Puzzle Hole 4 (Empty)", }, "quad_bike": { "name": "Quad Bike", }, "quest_1": { "name": "Quest Item 1", }, "quest_2": { "name": "Quest Item 2", }, "quest_3": { "name": "Quest Item 3", }, "quest_4": { "name": "Quest Item 4", }, "raptor": { "name": "Raptor", }, "raptor_emitter": { "name": "Raptor Emitter", }, "rat": { "name": [ "Rat", "Land Rat", ] }, "red_light": { "name": "Red Light", }, "rib": { "name": [ "Rigid Inflatable Boat", "RIB", ] }, "ricochet": { "name": "Ricochet", }, "rocket": { "name": "Rocket", }, "rocket_launcher": { "name": "Rocket Launcher", }, "rocket_launcher_ammo": { "name": "Rockets", }, "rolling_ball_1": { "name": [ "Boulder 1", "Rolling Ball 1", ] }, "rolling_ball_2": { "name": [ "Boulder 2", "Rolling Ball 2", ] }, "rolling_ball_3": { "name": [ "Boulder 3", "Rolling Ball 3", ] }, "rolling_ball_4": { "name": [ "Boulder 4", "Rolling Ball 4", ] }, "rotating_laser": { "name": "Rotating Laser", }, "rx_worker_1": { "name": "RX Worker 1", }, "rx_worker_2": { "name": "RX Worker 2", }, "rx_worker_3": { "name": [ "RX Worker 3", "Flamethrower", ] }, "save_crystal": { "name": "Savegame Crystal", }, "scion": { "name": "Scion", }, "scion_holder": { "name": "Scion Holder", }, "secret_1": { "name": "Secret 1", }, "secret_2": { "name": "Secret 2", }, "secret_3": { "name": "Secret 3", }, "security_guard": { "name": "Security Guard", }, "security_laser_alarm": { "name": "Security Laser (Alarm)", }, "security_laser_deadly": { "name": "Security Laser (Deadly)", }, "security_laser_killer": { "name": "Security Laser (Killer)", }, "sentry_gun": { "name": "Sentry Gun", }, "shadow": { "name": "Shadow", }, "shark": { "name": "Shark", }, "shiva": { "name": "Shiva", }, "shotgun": { "name": "Shotgun", }, "shotgun_ammo": { "name": "Shotgun Shells", }, "shotgun_shell": { "name": "Shotgun Shell", }, "skate_kid": { "name": "Skate Kid", }, "skateboard": { "name": "Skateboard", }, "skidoo_armed": { "name": "Black Snowmobile", }, "skidoo_driver": { "name": "Black Snowmobile Driver", }, "skidoo_fast": { "name": "Red Snowmobile", }, "skidoo_track": { "name": "Snowmobile Track", }, "skybox": { "name": "Skybox", }, "sliding_pillar": { "name": "Sliding Pillar", }, "small_medipack": { "name": "Small Medipack", }, "smashable_1": { "name": [ "Smashable 1", "Breakable Window 1", ] }, "smashable_2": { "name": [ "Smashable 2", "Breakable Window 2", ] }, "smashable_3": { "name": [ "Smashable 3", "Breakable Window 3", ] }, "smashable_4": { "name": [ "Smashable 4", "Breakable Window 4", ] }, "smoke_emitter_black": { "name": "Smoke Emitter (Black)", }, "smoke_emitter_white": { "name": "Smoke Emitter (White)", }, "snake": { "name": [ "Snake", "Cobra", ] }, "snow_sprite": { "name": "Snowmobile Wake", }, "sophia": { "name": "Sophia", }, "sound": { "name": "Sound", }, "sphere_of_doom_1": { "name": "Dragon Explosion 1", }, "sphere_of_doom_2": { "name": "Dragon Explosion 2", }, "sphere_of_doom_3": { "name": "Dragon Explosion 3", }, "spider": { "name": "Spider", }, "spike_wall": { "name": "Spike Wall", }, "spikes": { "name": "Spikes", }, "spinning_blade": { "name": "Spinning Blade", }, "splash_1": { "name": "Water Ripples 1", }, "splash_2": { "name": "Water Ripples 2", }, "springboard": { "name": "Springboard", }, "steam_emitter": { "name": "Steam Emitter", }, "sthpac_mercenary": { "name": "South Pacific Mercenary", }, "stopwatch": { "name": "Statistics", }, "strobe_light": { "name": "Strobe Light", }, "swat_1": { "name": "SWAT 1", }, "swat_2": { "name": "SWAT 2", }, "swat_3": { "name": "SWAT 3", }, "swinging_axe": { "name": "Swinging Axe", }, "switch_type_airlock": { "name": "Airlock Switch", }, "switch_type_button": { "name": [ "Button", "Push Button", "Switch", ] }, "switch_type_normal": { "name": [ "Lever", "Switch", ] }, "switch_type_small": { "name": "Small Switch", }, "switch_type_uw": { "name": [ "Underwater Lever", "Underwater Switch", ] }, "switch_type_wheel": { "name": [ "Wheel Switch", "Pulley Switch", "Valve Switch", ] }, "teeth_trap": { "name": [ "Teeth Trap", "Clang-clang Door", ] }, "text_box": { "name": "UI Frame", }, "thors_handle": { "name": "Thor's Hammer Handle", }, "thors_head": { "name": "Thor's Hammer", }, "tiger": { "name": "Tiger", }, "tony": { "name": "Tony", }, "torso": { "name": [ "Torso", "Adam", "Giant Mutant", ] }, "train": { "name": "Train", }, "trapdoor_1": { "name": "Trapdoor 1", }, "trapdoor_2": { "name": "Trapdoor 2", }, "trapdoor_3": { "name": "Trapdoor 3", }, "trex": { "name": "T-Rex", }, "trex_alpha": { "name": "T-Rex Alpha", }, "tribe_axeman": { "name": "Tribe Axeman", }, "tribe_boss": { "name": "Tribe Boss", }, "tribe_pipeman": { "name": "Tribe Blowpipe User", }, "tropical_fish": { "name": "Tropical Fish", }, "twinkle": { "name": "Sparkles", }, "upv": { "name": [ "UPV", "Minisub", ] }, "uzis": { "name": "Uzis", }, "uzis_ammo": { "name": "Uzi Clips", }, "vole": { "name": [ "Vole", "Water Rat", ] }, "vulture": { "name": "Vulture", }, "wasp_mutant": { "name": "Wasp Mutant", }, "wasp_mutant_emitter": { "name": "Wasp Mutant Emitter", }, "water_sprite": { "name": "Boat Wake", }, "waterfall": { "name": "Waterfall Mist", }, "white_light": { "name": "White Light", }, "willard": { "name": "Willard", }, "winston": { "name": "Winston", }, "winston_army": { "name": "Winston (army)", }, "wolf": { "name": "Wolf", }, "worker_1": { "name": "Gunman Goon 1", }, "worker_2": { "name": "Gunman Goon 2", }, "worker_3": { "name": "Stick Wielding Goon 1", }, "worker_4": { "name": "Stick Wielding Goon 2", }, "worker_5": { "name": "Flamethrower Goon", }, "xian_knight": { "name": "Xian Knight", }, "xian_knight_statue": { "name": "Xian Knight Statue", }, "xian_spearman": { "name": "Xian Spearman", }, "xian_spearman_statue": { "name": "Xian Spearman Statue", }, "yeti": { "name": "Yeti", }, "zipline_handle": { "name": "Zipline Handle", } } } ================================================ FILE: data/trx/ship/cfg/outfits.json5 ================================================ { "outfits": { // TR1 Gym "tr1_gym" : { "name_gs": "dynamic/enums/lara_outfit/tr1_gym", "mesh_object": "O_LARA_SKIN_SWAP_1", "gun_map": 0, "footstep_sample_id": "SFX_LARA_BAREFOOT", "combat_face_offset": 1, "braid": { "mode": "BRAID_MODE_TR1_HEAD_ONLY", "mesh_offset": 10, "gold_offset": 16, "hair_pos": { "x": 0, "y": 20, "z": -45, }, }, "extra_outfits": { "LS_EXTRA_MIDAS_KILL": "tr1_golden_lara", }, }, // TR1 Classic "tr1_classic": { "name_gs": "dynamic/enums/lara_outfit/tr1_classic", "mesh_object": "O_LARA_SKIN_SWAP_2", "gun_map": 0, "combat_face_offset": 1, "braid": { "mode": "BRAID_MODE_TR1_FULL", "mesh_offset": 10, "gold_offset": 16, "hair_pos": { "x": 0, "y": 20, "z": -45, }, }, "no_holster_offsets": { "thigh_r": 1, "thigh_l": 2, }, "extra_outfits": { "LS_EXTRA_TREX_KILL": "tr1_mauled", "LS_EXTRA_MIDAS_KILL": "tr1_golden_lara", }, }, // TR1 Mauled "tr1_mauled": { "name_gs": "dynamic/enums/lara_outfit/tr1_mauled", "mesh_object": "O_LARA_SKIN_SWAP_3", "gun_map": 0, "combat_face_offset": 1, "braid": { "mode": "BRAID_MODE_TR1_MAULED", "mesh_offset": 10, "gold_offset": 16, "hair_pos": { "x": 0, "y": 20, "z": -45, }, }, "no_holster_offsets": { "thigh_r": 3, "thigh_l": 4, }, "extra_outfits": { "LS_EXTRA_MIDAS_KILL": "tr1_golden_lara", }, }, // TR1 Combo "tr1_combo": { "name_gs": "dynamic/enums/lara_outfit/tr1_combo", "mesh_object": "O_LARA_SKIN_SWAP_4", "gun_map": 0, "combat_face_offset": 1, "braid": { "mode": "BRAID_MODE_TR1_HEAD_ONLY", "mesh_offset": 10, "gold_offset": 16, "hair_pos": { "x": 0, "y": 20, "z": -45, }, }, "no_holster_offsets": { "thigh_r": 1, "thigh_l": 2, }, "extra_outfits": { "LS_EXTRA_MIDAS_KILL": "tr1_golden_lara", }, }, // TR1 N-Gage "tr1_ngage": { "name_gs": "dynamic/enums/lara_outfit/tr1_ngage", "mesh_object": "O_LARA_SKIN_SWAP_24", "gun_map": 0, "combat_face_offset": 1, "braid": { "mode": "BRAID_MODE_TR1_HEAD_ONLY", "mesh_offset": 10, "gold_offset": 16, "hair_pos": { "x": 0, "y": 20, "z": -45, }, }, "no_holster_offsets": { "thigh_r": 25, "thigh_l": 26, }, "extra_outfits": { "LS_EXTRA_MIDAS_KILL": "tr1_golden_lara", }, }, // TR1 Golden Lara "tr1_golden_lara": { "name_gs": "dynamic/enums/lara_outfit/tr1_golden_lara", "mesh_object": "O_LARA_SKIN_SWAP_5", "gun_map": 1, "is_reflective": true, "combat_face_offset": -1, "braid": { "mode": "BRAID_MODE_TR1_GOLD", "mesh_offset": 10, "gold_offset": 16, "hair_pos": { "x": 0, "y": 20, "z": -45, }, }, }, // Bacon Lara "tr1_bacon_lara": { "name_gs": "dynamic/enums/lara_outfit/tr1_bacon_lara", "mesh_object": "O_LARA_SKIN_SWAP_6", "gun_map": 0, "footstep_sample_id": "SFX_LARA_BAREFOOT", "combat_face_offset": -1, "supports_sunglasses": false, "extra_outfits": { "LS_EXTRA_MIDAS_KILL": "tr1_golden_bacon_lara", }, }, // Golden Bacon Lara "tr1_golden_bacon_lara": { "name_gs": "dynamic/enums/lara_outfit/tr1_golden_bacon_lara", "mesh_object": "O_LARA_SKIN_SWAP_7", "gun_map": 1, "is_reflective": true, "supports_sunglasses": false, "combat_face_offset": -1, }, // TR2 Gym "tr2_gym": { "name_gs": "dynamic/enums/lara_outfit/tr2_gym", "mesh_object": "O_LARA_SKIN_SWAP_8", "gun_map": 2, "combat_face_offset": 2, "braid": { "mesh_offset": 22, "gold_offset": 28, "hair_pos": { "x": 0, "y": -23, "z": -55, }, }, "no_holster_offsets": { "thigh_r": 5, "thigh_l": 6, }, "extra_outfits": { "LS_EXTRA_MIDAS_KILL": "tr23_golden_lara", }, }, // TR2 Classic "tr2_classic": { "name_gs": "dynamic/enums/lara_outfit/tr2_classic", "mesh_object": "O_LARA_SKIN_SWAP_9", "gun_map": 2, "combat_face_offset": 2, "braid": { "mesh_offset": 22, "gold_offset": 28, "hair_pos": { "x": 0, "y": -23, "z": -55, }, }, "no_holster_offsets": { "thigh_r": 7, "thigh_l": 8, }, "extra_outfits": { "LS_EXTRA_MIDAS_KILL": "tr23_golden_lara", }, }, // Diving Suit "tr2_diving_suit": { "name_gs": "dynamic/enums/lara_outfit/tr2_diving_suit", "mesh_object": "O_LARA_SKIN_SWAP_10", "gun_map": 2, "footstep_sample_id": "SFX_LARA_BAREFOOT", "combat_face_offset": 2, "braid": { "mesh_offset": 22, "gold_offset": 28, "hair_pos": { "x": 0, "y": -23, "z": -55, }, }, "extra_outfits": { "LS_EXTRA_MIDAS_KILL": "tr23_golden_lara", }, }, // Diving Suit Alpha "tr2_diving_suit_alpha": { "name_gs": "dynamic/enums/lara_outfit/tr2_diving_suit_alpha", "mesh_object": "O_LARA_SKIN_SWAP_23", "gun_map": 2, "footstep_sample_id": "SFX_LARA_BAREFOOT", "combat_face_offset": 2, "braid": { "mesh_offset": 22, "gold_offset": 28, "hair_pos": { "x": 0, "y": -23, "z": -55, }, }, "extra_outfits": { "LS_EXTRA_MIDAS_KILL": "tr23_golden_lara", }, }, // Bomber Jacket "tr2_bomber_jacket": { "name_gs": "dynamic/enums/lara_outfit/tr2_bomber_jacket", "mesh_object": "O_LARA_SKIN_SWAP_11", "gun_map": 2, "combat_face_offset": 2, "braid": { "mesh_offset": 22, "gold_offset": 28, "hair_pos": { "x": 0, "y": -23, "z": -55, }, }, "no_holster_offsets": { "thigh_r": 7, "thigh_l": 8, }, "extra_outfits": { "LS_EXTRA_MIDAS_KILL": "tr23_golden_lara", }, }, // Robe "tr2_robe": { "name_gs": "dynamic/enums/lara_outfit/tr2_robe", "mesh_object": "O_LARA_SKIN_SWAP_12", "gun_map": 2, "footstep_sample_id": "SFX_LARA_BAREFOOT", "combat_face_offset": 2, "braid": { "mesh_offset": 22, "gold_offset": 28, "hair_pos": { "x": 0, "y": -23, "z": -55, }, }, "no_holster_offsets": { "thigh_r": 9, "thigh_l": 10, }, "extra_outfits": { "LS_EXTRA_MIDAS_KILL": "tr23_golden_lara", }, }, // Vegas "tr2_vegas": { "name_gs": "dynamic/enums/lara_outfit/tr2_vegas", "mesh_object": "O_LARA_SKIN_SWAP_13", "gun_map": 2, "combat_face_offset": 2, "braid": { "mesh_offset": 22, "gold_offset": 28, "hair_pos": { "x": 0, "y": -23, "z": -55, }, }, "no_holster_offsets": { "thigh_r": 11, "thigh_l": 12, }, "extra_outfits": { "LS_EXTRA_MIDAS_KILL": "tr23_golden_lara", }, }, // TR2/3 Golden Lara "tr23_golden_lara": { "name_gs": "dynamic/enums/lara_outfit/tr23_golden_lara", "mesh_object": "O_LARA_SKIN_SWAP_14", "gun_map": 1, "is_reflective": true, "combat_face_offset": -1, "braid": { "mesh_offset": 22, "gold_offset": 28, "hair_pos": { "x": 0, "y": -23, "z": -55, }, }, }, // TR3 Gym "tr3_gym": { "name_gs": "dynamic/enums/lara_outfit/tr3_gym", "mesh_object": "O_LARA_SKIN_SWAP_15", "gun_map": 4, "combat_face_offset": 3, "braid": { "mesh_offset": 22, "gold_offset": 28, "hair_pos": { "x": 0, "y": -23, "z": -55, }, }, "no_holster_offsets": { "thigh_r": 13, "thigh_l": 14, }, "extra_outfits": { "LS_EXTRA_MIDAS_KILL": "tr23_golden_lara", }, }, // TR3 Classic "tr3_classic": { "name_gs": "dynamic/enums/lara_outfit/tr3_classic", "mesh_object": "O_LARA_SKIN_SWAP_16", "gun_map": 3, "combat_face_offset": 3, "braid": { "mesh_offset": 22, "gold_offset": 28, "hair_pos": { "x": 0, "y": -23, "z": -55, }, }, "no_holster_offsets": { "thigh_r": 15, "thigh_l": 16, }, "extra_outfits": { "LS_EXTRA_MIDAS_KILL": "tr23_golden_lara", }, }, // South Pacific "tr3_south_pacific": { "name_gs": "dynamic/enums/lara_outfit/tr3_south_pacific", "mesh_object": "O_LARA_SKIN_SWAP_17", "gun_map": 3, "combat_face_offset": 3, "braid": { "mesh_offset": 22, "gold_offset": 28, "hair_pos": { "x": 0, "y": -23, "z": -55, }, }, "no_holster_offsets": { "thigh_r": 17, "thigh_l": 18, }, "extra_outfits": { "LS_EXTRA_MIDAS_KILL": "tr23_golden_lara", }, }, // Catsuit "tr3_catsuit": { "name_gs": "dynamic/enums/lara_outfit/tr3_catsuit", "mesh_object": "O_LARA_SKIN_SWAP_18", "gun_map": 3, "combat_face_offset": 3, "braid": { "mesh_offset": 22, "gold_offset": 28, "hair_pos": { "x": 0, "y": -23, "z": -55, }, }, "no_holster_offsets": { "thigh_r": 19, "thigh_l": 20, }, "extra_outfits": { "LS_EXTRA_MIDAS_KILL": "tr23_golden_lara", }, }, // Nevada "tr3_nevada": { "name_gs": "dynamic/enums/lara_outfit/tr3_nevada", "mesh_object": "O_LARA_SKIN_SWAP_19", "gun_map": 4, "combat_face_offset": 3, "braid": { "mesh_offset": 22, "gold_offset": 28, "hair_pos": { "x": 0, "y": -23, "z": -55, }, }, "no_holster_offsets": { "thigh_r": 21, "thigh_l": 22, }, "extra_outfits": { "LS_EXTRA_MIDAS_KILL": "tr23_golden_lara", }, }, // Antarctica "tr3_antarctica": { "name_gs": "dynamic/enums/lara_outfit/tr3_antarctica", "mesh_object": "O_LARA_SKIN_SWAP_20", "gun_map": 4, "combat_face_offset": 3, "braid": { "mesh_offset": 22, "gold_offset": 28, "hair_pos": { "x": 0, "y": -23, "z": -55, }, }, "no_holster_offsets": { "thigh_r": 23, "thigh_l": 24, }, "extra_outfits": { "LS_EXTRA_MIDAS_KILL": "tr23_golden_lara", }, }, // Sophia Leigh "sophia": { "name_gs": "dynamic/enums/lara_outfit/sophia", "mesh_object": "O_LARA_SKIN_SWAP_21", "gun_map": 3, "combat_face_offset": -1, "extra_outfits": { "LS_EXTRA_MIDAS_KILL": "golden_sophia", }, }, // Golden Sophia Leigh "golden_sophia": { "name_gs": "dynamic/enums/lara_outfit/golden_sophia", "mesh_object": "O_LARA_SKIN_SWAP_22", "gun_map": 1, "is_selectable": false, "is_reflective": true, "combat_face_offset": -1, }, }, "extra_meshes": { "EXTRA_MESH_TR1_BRAID_DEFAULT_HEAD": 7, "EXTRA_MESH_TR1_BRAID_COMBAT_HEAD": 9, "EXTRA_MESH_TR1_BRAID_DEFAULT_TORSO": 4, "EXTRA_MESH_TR1_BRAID_MAULED_TORSO": 5, "EXTRA_MESH_TR1_BRAID_GOLD_HEAD": 8, "EXTRA_MESH_TR1_BRAID_GOLD_TORSO": 6, "EXTRA_MESH_DAGGER_HAND": 35, "EXTRA_MESH_DAGGER_HIPS": 34, "EXTRA_MESH_OAR": 36, "EXTRA_MESH_SPANNER": 37, "EXTRA_MESH_DRINK_CAN": 38, "EXTRA_MESH_GLASSES_OPAQUE": 39, "EXTRA_MESH_GLASSES_TRANSPARENT": 40, }, "gun_maps": [ // 0. TR1 style { "LGT_UNARMED": { "thigh_r": 1, "thigh_l": 2, }, "LGT_PISTOLS": { "hand_r": 56, "hand_l": 57, "thigh_r": 3, "thigh_l": 4, }, "LGT_MAGNUMS": { "hand_r": 58, "hand_l": 59, "thigh_r": 5, "thigh_l": 6, }, "LGT_AUTOS": { "hand_r": 60, "hand_l": 61, "thigh_r": 7, "thigh_l": 8, }, "LGT_DESERT_EAGLE": { "hand_r": 62, "thigh_r": 9, "thigh_l": 2, }, "LGT_UZIS": { "hand_r": 63, "hand_l": 64, "thigh_r": 10, "thigh_l": 11, }, "LGT_SHOTGUN": { "hand_r": 65, "torso": 72, }, "LGT_FLARE": { "hand_l": 66, }, "LGT_M16": { "hand_r": 67, "torso": 73, }, "LGT_MP5": { "hand_r": 68, "torso": 74, }, "LGT_GRENADE": { "hand_r": 69, "torso": 75, }, "LGT_HARPOON": { "hand_r": 70, "torso": 76, }, "LGT_ROCKET": { "hand_r": 71, "torso": 77, }, }, // 1. Gold style { "LGT_UNARMED": { "thigh_r": 12, "thigh_l": 13, }, "LGT_PISTOLS": { "hand_r": 56, "hand_l": 57, "thigh_r": 14, "thigh_l": 15, }, "LGT_MAGNUMS": { "hand_r": 58, "hand_l": 59, "thigh_r": 16, "thigh_l": 17, }, "LGT_AUTOS": { "hand_r": 60, "hand_l": 61, "thigh_r": 18, "thigh_l": 19, }, "LGT_DESERT_EAGLE": { "hand_r": 62, "thigh_r": 20, "thigh_l": 13, }, "LGT_UZIS": { "hand_r": 63, "hand_l": 64, "thigh_r": 21, "thigh_l": 22, }, "LGT_SHOTGUN": { "hand_r": 65, "torso": 72, }, "LGT_FLARE": { "hand_l": 66, }, "LGT_M16": { "hand_r": 67, "torso": 73, }, "LGT_MP5": { "hand_r": 68, "torso": 74, }, "LGT_GRENADE": { "hand_r": 69, "torso": 75, }, "LGT_HARPOON": { "hand_r": 70, "torso": 76, }, "LGT_ROCKET": { "hand_r": 71, "torso": 77, }, }, // 2. TR2 style { "LGT_UNARMED": { "thigh_r": 23, "thigh_l": 24, }, "LGT_PISTOLS": { "hand_r": 56, "hand_l": 57, "thigh_r": 25, "thigh_l": 26, }, "LGT_MAGNUMS": { "hand_r": 58, "hand_l": 59, "thigh_r": 27, "thigh_l": 28, }, "LGT_AUTOS": { "hand_r": 60, "hand_l": 61, "thigh_r": 29, "thigh_l": 30, }, "LGT_DESERT_EAGLE": { "hand_r": 62, "thigh_r": 31, "thigh_l": 24, }, "LGT_UZIS": { "hand_r": 63, "hand_l": 64, "thigh_r": 32, "thigh_l": 33, }, "LGT_SHOTGUN": { "hand_r": 65, "torso": 72, }, "LGT_FLARE": { "hand_l": 66, }, "LGT_M16": { "hand_r": 67, "torso": 73, }, "LGT_MP5": { "hand_r": 68, "torso": 74, }, "LGT_GRENADE": { "hand_r": 69, "torso": 75, }, "LGT_HARPOON": { "hand_r": 70, "torso": 76, }, "LGT_ROCKET": { "hand_r": 71, "torso": 77, }, }, // 3. TR3 style { "LGT_UNARMED": { "thigh_r": 34, "thigh_l": 35, }, "LGT_PISTOLS": { "hand_r": 56, "hand_l": 57, "thigh_r": 36, "thigh_l": 37, }, "LGT_MAGNUMS": { "hand_r": 58, "hand_l": 59, "thigh_r": 38, "thigh_l": 39, }, "LGT_AUTOS": { "hand_r": 60, "hand_l": 61, "thigh_r": 40, "thigh_l": 41, }, "LGT_DESERT_EAGLE": { "hand_r": 62, "thigh_r": 42, "thigh_l": 35, }, "LGT_UZIS": { "hand_r": 63, "hand_l": 64, "thigh_r": 43, "thigh_l": 44, }, "LGT_SHOTGUN": { "hand_r": 65, "torso": 72, }, "LGT_FLARE": { "hand_l": 66, }, "LGT_M16": { "hand_r": 67, "torso": 73, }, "LGT_MP5": { "hand_r": 68, "torso": 74, }, "LGT_GRENADE": { "hand_r": 69, "torso": 75, }, "LGT_HARPOON": { "hand_r": 70, "torso": 76, }, "LGT_ROCKET": { "hand_r": 71, "torso": 77, }, }, // 4. TR3 alt style { "LGT_UNARMED": { "thigh_r": 45, "thigh_l": 46, }, "LGT_PISTOLS": { "hand_r": 56, "hand_l": 57, "thigh_r": 47, "thigh_l": 48, }, "LGT_MAGNUMS": { "hand_r": 58, "hand_l": 59, "thigh_r": 49, "thigh_l": 50, }, "LGT_AUTOS": { "hand_r": 60, "hand_l": 61, "thigh_r": 51, "thigh_l": 52, }, "LGT_DESERT_EAGLE": { "hand_r": 62, "thigh_r": 53, "thigh_l": 46, }, "LGT_UZIS": { "hand_r": 63, "hand_l": 64, "thigh_r": 54, "thigh_l": 55, }, "LGT_SHOTGUN": { "hand_r": 65, "torso": 72, }, "LGT_FLARE": { "hand_l": 66, }, "LGT_M16": { "hand_r": 67, "torso": 73, }, "LGT_MP5": { "hand_r": 68, "torso": 74, }, "LGT_GRENADE": { "hand_r": 69, "torso": 75, }, "LGT_HARPOON": { "hand_r": 70, "torso": 76, }, "LGT_ROCKET": { "hand_r": 71, "torso": 77, }, }, ], } ================================================ FILE: data/trx/ship/cfg/poses.json5 ================================================ [ {"offset": [0, -442, 0], "rots": [[-2624, 4288, 9984], [7808, 1088, 0], [-15168, 0, 0], [-2688, 1984, -3648], [-960, 2048, -960], [-3328, 0, 0], [-3072, 0, 2240], [1856, 3648, -1920], [6528, -14080, -24640], [3328, 0, 0], [-1856, 6144, 512], [1664, -8640, 6080], [6464, 0, 640], [576, -2112, 2688], [5056, 5440, -1664]]}, {"offset": [0, -565, -200], "rots": [[1920, 1024, -512], [15040, 0, -1536], [-15488, 0, 1216], [-1856, 384, 256], [14272, -32768, -29312], [-14016, -3200, 6976], [-1152, 0, 0], [-2688, -832, 1408], [1024, -16192, -8512], [15872, -1024, -704], [7680, 1664, 768], [3264, 12032, 6336], [15360, -17472, -23616], [5248, 4032, 1664], [1472, 192, 2112]]}, {"offset": [-8, -439, -7], "rots": [[-640, 64, 192], [2112, -2304, 1920], [-3136, -64, 0], [1728, 448, -2240], [2688, 2048, -2688], [-3456, 0, 0], [1664, -64, 2368], [0, -64, -256], [15616, 32704, 32704], [0, 0, -320], [-1472, 0, -384], [15616, 32704, 32704], [0, 0, 448], [-1600, -64, 64], [64, 0, 0]]}, {"offset": [-8, -448, -7], "rots": [[768, -320, -256], [-2752, -2176, 1600], [0, 0, 0], [1728, 0, -1152], [1024, 1216, -2112], [-2112, 0, 0], [0, 0, 1856], [-1088, 3456, 576], [7104, 11520, -2560], [11712, -32768, -32768], [2112, 960, -1792], [-1536, -8320, 3776], [4352, 0, 0], [448, 2432, 2560], [-1536, 1728, 640]]}, {"offset": [1, -442, -4], "rots": [[-3008, -1792, -17408], [-2048, -384, -768], [-4224, 0, 0], [-9216, 0, 0], [2752, 0, 2176], [-6592, 0, 0], [-8128, 0, 0], [-1984, 2496, 3264], [15936, 0, 0], [0, 3904, 0], [0, 0, 0], [15232, 0, 0], [0, 6528, 0], [0, 0, 0], [-3008, -1280, 5760]]}, {"offset": [-8, -450, -7], "rots": [[640, 832, 1216], [-2048, -576, -448], [448, 0, 0], [960, -1472, -512], [1664, -448, -2624], [-2752, 0, 0], [704, 0, 1024], [-64, -384, -896], [-9536, -10432, 1664], [15168, -32768, -32768], [832, -5120, -10496], [-8960, 10624, -1664], [15680, -32768, -32448], [2944, 5184, 12032], [-2688, 1024, 1216]]}, {"offset": [0, -422, 0], "rots": [[-640, 384, -64], [5056, -4352, 3392], [-2368, 0, 0], [-2176, 768, -2624], [512, 128, -3648], [-5440, 0, 0], [4928, -448, 4352], [-3008, -6080, -2112], [-64, 2560, -7808], [6784, -256, -448], [-2112, 2752, -128], [1152, -1600, 2816], [15808, 18816, 18816], [-1088, 768, 4096], [-6016, 7936, 2944]]}, {"offset": [0, -435, 50], "rots": [[1152, 0, 1472], [1920, 384, -1856], [-4800, 384, -256], [1792, 704, 384], [4736, 3328, -1984], [-11136, -32704, 32064], [-192, 192, 512], [2944, -1920, -2176], [12160, -4224, -1600], [-64, 1088, -64], [-640, -192, -1088], [1536, 12992, 10880], [-256, 1664, 768], [-960, -13760, 1792], [-5120, -1152, -704]]}, {"offset": [0, -140, 0], "rots": [[576, 0, -1984], [9984, 2496, 5888], [-6400, 32704, 32704], [-11776, 0, 0], [11072, -26304, 31488], [-7104, -32768, 32704], [2496, 0, 0], [-8064, -3264, 7680], [1792, -3456, -10304], [11776, 256, -512], [32000, 30976, 31744], [0, 8960, 2624], [10944, 896, 3392], [3200, 0, 3264], [9344, 4416, -192]]}, {"offset": [0, -90, 0], "rots": [[-2368, 1536, 18496], [8000, -7616, -7744], [-14016, -32768, 32704], [-6080, 2304, -2688], [7424, -5248, -4864], [-14912, 0, 0], [1664, -64, 2368], [-6080, 2432, -4416], [5824, -3712, -320], [15936, -1536, -1856], [0, 0, -1024], [7104, 1344, 0], [10624, 0, -7936], [0, 0, 10496], [-1408, 0, 4864]]}, {"offset": [0, -275, -100], "rots": [[-2816, 0, 320], [10048, 576, 0], [-12992, 32704, 32704], [12480, -1472, -2176], [15168, -128, -2944], [-11840, 32704, -32768], [8512, -192, -1536], [-3776, 0, -64], [11648, -12224, -27456], [12992, -960, -1024], [448, -768, 7488], [12800, 28608, -30272], [15040, 28672, 29632], [-1728, 8256, -13248], [5632, 2048, 832]]}, {"offset": [0, -401, 0], "rots": [[-4224, 2176, 64], [1152, 384, -512], [-14272, -32768, 32704], [-3648, 832, 1344], [12352, 512, -128], [-9728, 0, 0], [3200, 512, 960], [-2560, 5120, -448], [3648, 14272, -5184], [-320, -1152, 2368], [-6976, -14208, -1728], [2304, 7552, 12096], [14656, 29632, 29568], [5568, 1088, -2112], [2176, -4096, 1024]]}, {"offset": [0, -394, 0], "rots": [[-3456, 0, -704], [6656, 3968, 8640], [-8704, 64, -256], [-1088, 320, -2816], [9216, -896, -1344], [-11456, 0, 0], [4800, 2752, 2368], [-4096, 512, -128], [192, 15424, 23296], [-320, -1152, 896], [-3008, -16064, 384], [9088, 28928, 30848], [896, 1600, 192], [-512, -2944, -256], [4096, -896, 0]]}, {"offset": [0, -51, 0], "rots": [[4992, 960, 192], [11712, 0, 0], [-1088, 0, 0], [-1472, 0, 0], [13952, 28608, 27328], [-13632, 1280, 576], [-1152, 0, 0], [-5248, -3456, -1536], [5952, 448, -3264], [7808, -3776, -3328], [320, -4480, 1152], [384, -4224, 4480], [0, 0, 0], [1216, -1408, 12480], [-832, -6208, -1536]]}, {"offset": [0, -435, 0], "rots": [[-3968, 17216, 1152], [9920, -1792, -448], [-7424, 0, 0], [-448, 384, -1152], [2176, 3712, -2176], [-2176, 0, 0], [2944, 0, 2688], [-704, -4096, -2048], [6656, -5888, -5760], [8768, -32768, 29248], [2304, -2944, -1344], [-64, -10688, 3712], [7616, 0, 0], [768, 3328, -1408], [-1280, -9024, -3456]]}, {"offset": [0, -460, 0], "rots": [[-1344, -2624, -1792], [1408, -640, 1088], [0, 0, 0], [0, 0, 0], [6848, -2176, 1344], [-9920, 1280, -1024], [-4160, 0, 1664], [2496, 256, 1856], [1984, 6272, -7104], [2816, 0, -320], [0, 0, 768], [-1152, -2368, 5184], [3648, 0, 0], [192, -3520, -320], [-4672, 1664, 640]]}, {"offset": [0, -456, 0], "rots": [[448, -3008, -576], [3072, -1408, 448], [-8000, 0, 0], [0, 0, 0], [-1024, 2624, 640], [0, 0, 0], [0, 0, 0], [0, 0, 1408], [512, 7168, -4608], [3328, 0, 0], [0, 0, 0], [-960, -1728, 3136], [4288, 0, 0], [0, 0, 0], [-3584, 2688, -1152]]}, {"offset": [0, -450, 0], "rots": [[-128, -1408, -576], [-1024, -64, 768], [-1408, 0, 0], [1984, 0, 0], [2496, 3328, 2176], [0, 0, 0], [-4480, 1920, -832], [512, 2432, 1600], [8704, 5248, -5696], [11776, -32768, 31744], [1088, -64, -384], [-2432, -6080, 3584], [5888, 0, 0], [-1664, 320, -1984], [-2752, 4992, 896]]}, {"offset": [0, -460, 0], "rots": [[1600, -3584, 1600], [7296, -1024, -768], [-16128, 0, 0], [-8768, 3136, -1536], [-1472, 0, 128], [0, 0, 0], [-576, 2048, -1280], [128, 1600, -192], [2688, -14400, -5696], [14400, -32768, 30912], [6272, 448, -4928], [-5376, -4672, 1600], [10880, 7744, 4992], [5440, 3520, 1728], [-2880, 896, -2624]]}, {"offset": [0, -430, 0], "rots": [[-2688, 1088, 768], [7616, 0, -704], [-6272, 0, 0], [3008, 0, 0], [-1600, 2176, -320], [-5248, 0, 0], [-3264, -960, 1920], [-2048, -2048, -320], [4608, 2944, -2240], [11648, -32768, -32768], [-2944, 2048, -4160], [-5056, 0, 0], [13312, -32768, -32768], [2560, 0, 0], [2560, 4096, 1408]]}, {"offset": [0, -565, 0], "rots": [[3328, 1984, -6400], [10176, -13376, -7168], [-10368, -32768, -28608], [-3520, 4032, -4288], [1280, -320, -5696], [0, 0, 0], [-64, 0, 2240], [-7488, -1536, 2880], [7872, -1920, -9088], [704, 0, 0], [0, 0, 0], [-3008, 9664, 7680], [15936, 0, 0], [0, 0, 0], [-6528, 5184, 6912]]}, {"offset": [0, -449, 0], "rots": [[1792, 2304, -512], [0, -2816, 2944], [-1152, 0, 0], [64, 640, -1472], [-4096, 192, -2176], [-384, 0, 0], [1408, 0, 1600], [-576, 1536, 1216], [-5376, 6528, -2304], [4928, 5248, 2560], [-896, 3008, -1600], [14976, 0, 2624], [1088, 0, 0], [-2048, 0, 0], [-3520, -2560, 1536]]}, {"offset": [0, -453, 0], "rots": [[-896, 28672, 832], [-768, -3136, 1152], [-768, 0, 0], [448, 0, -1408], [1088, -320, -3200], [-960, 0, 0], [0, 0, 2560], [1216, -3840, -2112], [-1856, -4992, -320], [7168, 0, 0], [-2304, -4160, -7616], [5568, -6976, 4928], [1728, -22208, -24128], [1408, 9024, 256], [-3072, -11520, -704]]}, {"offset": [0, -454, 0], "rots": [[448, -17024, -320], [4288, -1536, -320], [-13056, 3200, -3072], [-2560, 1280, 0], [-1152, 0, 960], [0, 0, 0], [0, 0, 0], [1024, 832, 1536], [3264, -1344, -704], [8832, -32768, -32768], [1856, 0, 0], [448, 12224, 4736], [15872, 0, -3328], [512, 0, -7936], [-2944, 10624, -384]]}, {"offset": [0, -452, 0], "rots": [[-3008, -768, -192], [1984, 832, -256], [320, 0, 0], [640, -1152, -64], [5056, 3008, 2752], [0, 0, 0], [-3392, 0, -960], [-1088, 896, 1024], [-4544, -8000, 4032], [7168, -3328, -2176], [3520, -192, 6016], [9600, 3328, 3584], [6464, -16448, -22464], [4608, -5504, 11328], [1984, -128, -1408]]}, {"offset": [0, -51, 0], "rots": [[6080, -512, -640], [14848, -32768, 30848], [-12608, 0, 0], [-2112, 0, 0], [12032, 30400, 29312], [-14144, -32768, -31552], [-512, -3328, 192], [-7872, 1344, 576], [8064, -9600, -8384], [8256, -3712, -2688], [576, -896, 3456], [5312, 15104, 12864], [11136, 3008, 1920], [960, 1984, -4736], [2560, -1024, -2368]]}, {"offset": [0, -565, 0], "rots": [[-2176, -640, -8512], [-2560, -4544, 1856], [-5760, 0, 384], [-3136, 0, 0], [4160, 1216, -640], [-12032, 0, 0], [-4288, 0, 0], [-2496, 1280, 3392], [-1344, 3520, -8192], [11008, 0, 0], [6528, 0, 0], [10560, 12032, 30464], [9408, 0, 0], [4032, 0, -4160], [4480, -4736, 768]]}, {"offset": [0, -51, 0], "rots": [[-14016, 0, 0], [-1216, -3392, 2688], [-15680, -2624, 896], [-1984, 0, 0], [-1792, 4544, -2496], [-12544, 0, 0], [0, 0, 0], [1792, -1856, -768], [16256, 0, 0], [9216, -32768, -28800], [0, 0, 12224], [13440, 1472, 1408], [6080, 16640, 14656], [0, 4928, 2944], [7744, -2624, -3136]]}, {"offset": [0, -232, 0], "rots": [[192, 1280, 1600], [14784, -32768, -32768], [-14016, -32768, -32768], [-64, -576, -1728], [768, 2048, -3584], [-13632, -32768, -32768], [-3328, -128, 2112], [-3072, 2240, 448], [1664, 5568, -4160], [8192, 0, 0], [0, 0, 0], [16320, -32768, -25344], [4224, 0, 0], [-2240, 3008, -384], [192, -4992, -2880]]}, {"offset": [0, -385, 0], "rots": [[3968, -32768, -32768], [11136, 0, 0], [1728, 0, 0], [-11008, 0, 0], [-6528, 0, 0], [256, 0, 0], [-9920, 0, 0], [6912, 0, 0], [1664, -28480, -31872], [3264, -2432, -2432], [960, -8128, -10688], [1472, 22528, 30976], [0, 2432, 0], [0, 3648, 17280], [9920, 0, 0]]}, {"offset": [0, -385, 0], "rots": [[-4288, 29056, -28544], [7872, 5312, 1536], [0, 0, 0], [-9216, 0, 0], [-7488, 6208, -3200], [0, 0, 0], [-9344, 0, 0], [3520, 2496, -256], [-2560, 1088, -4224], [1600, 6272, 0], [-6720, 6976, -4608], [5952, -30080, -27200], [0, 9920, 0], [0, 10368, 16000], [1664, -13760, -11648]]}, {"offset": [0, -452, 0], "rots": [[-1472, 0, 0], [3328, 0, -704], [0, 0, 0], [-1728, -1600, 1024], [0, 0, 1024], [-3136, 0, 0], [-128, 2944, 448], [0, 0, 0], [1984, -3840, -3776], [6976, -32768, -32768], [-3008, 1472, 10752], [-832, 2880, 768], [7936, 5120, 2240], [0, 8448, -2304], [1728, -320, 768]]}, {"offset": [0, -193, 0], "rots": [[3456, 0, 0], [12480, 0, 8128], [-6272, -32768, -32768], [7872, 576, 832], [12288, 0, -8000], [-5760, -32768, -32768], [8192, -4736, -3456], [-9088, -1792, 2304], [-3136, -6976, -9856], [9728, -4416, -3584], [640, -6528, -5952], [-2304, 8512, 7424], [8704, 4736, 3200], [2112, 5824, 10368], [4160, 4160, 960]]}, {"offset": [0, -450, 0], "rots": [[-2176, -896, -256], [-192, -1920, -1152], [-64, 0, 0], [1408, -640, 1280], [3904, -832, 640], [384, 0, 0], [-1728, 3264, -576], [192, 0, 0], [-3648, -9856, 4608], [6016, -2688, -192], [1728, -2944, 5184], [-3904, 6976, -3968], [5952, 6848, 2624], [0, 3328, -2432], [4928, 2624, 1536]]}, {"offset": [0, -450, 0], "rots": [[704, 0, 0], [-832, 1088, 1536], [-1152, 0, 128], [576, -2496, -1856], [-1088, -1216, -1472], [-1216, 0, -64], [1024, 3264, 1792], [-448, 0, 0], [14016, -768, -14784], [-320, 0, 64], [256, -1088, -448], [1792, 17408, 12608], [13248, 1920, 2432], [1216, -9984, -704], [-4096, 9152, -2112]]}, {"offset": [0, -420, 0], "rots": [[2560, 9920, 448], [768, -1536, 3520], [-5312, 64, 64], [768, -2240, -3584], [-192, 960, -4032], [-6592, -64, 64], [2624, 1856, 4736], [-2240, -1216, 128], [-512, -5440, -1408], [24320, 3392, 1536], [768, 6016, 4992], [256, -1536, 2368], [22720, -1984, -1280], [2560, -4160, -5632], [-1472, -8896, 512]]}, {"offset": [0, -479, 0], "rots": [[1088, -13696, -9728], [3072, -5312, 8832], [64, 0, 192], [704, 1216, 320], [4160, -1536, -9408], [128, 0, -128], [-7296, 3328, 512], [-1152, 1152, 832], [-1856, 2304, -5440], [20224, 5888, 4032], [768, 6016, 4992], [-704, 12608, 8384], [14592, 30784, 31488], [2560, -4160, -5632], [-4736, 6400, -1536]]}, {"offset": [0, -420, 0], "rots": [[1920, 11136, -832], [1728, -1472, 5312], [-5440, 64, 64], [320, -2944, -4032], [1408, 448, -2112], [-7424, -64, 64], [2752, 1216, 3968], [-2240, -1024, 64], [6848, -8832, -6336], [22336, 3968, 2240], [768, 6016, 4992], [9856, 704, 8512], [23616, -1856, -1152], [2560, -4160, -5632], [-704, -7680, 512]]}, {"offset": [0, -420, 0], "rots": [[-192, -8576, -960], [-128, 3072, 2944], [-6592, 64, 0], [6528, -1088, -3328], [5696, 384, -2560], [-5312, 0, 0], [-2368, 128, 1600], [-704, 320, 384], [-640, 960, -4736], [5120, -192, -768], [-2880, 0, 576], [-2752, -384, 5440], [5888, 192, 448], [-512, -1664, -256], [-192, 6848, 832]]}, {"offset": [0, -450, 0], "rots": [[448, 0, 0], [-320, 1088, 1408], [-1536, 0, 128], [704, -320, -1856], [-384, -192, -1728], [-1472, 0, -64], [1024, 2944, 2496], [-448, 0, 0], [5952, -15616, -10368], [20928, 4672, 3072], [-3264, -4480, -5376], [7424, 5312, 5952], [23168, -1920, -960], [2432, -2240, 3392], [-832, 8192, -192]]}, {"offset": [0, -70, 0], "rots": [[-5440, -10432, 16000], [3520, -4608, -2240], [-16832, -5568, 5696], [-3200, 704, -4608], [5952, -4480, -64], [-23744, 512, -640], [4864, 320, -1856], [-2304, 256, -2880], [1024, -1280, -26496], [18176, 10240, 7040], [2688, -1728, -960], [12928, 11136, 6144], [10944, 832, 1920], [-384, 7808, 6400], [10944, 384, -10560]]}, {"offset": [0, -150, 0], "rots": [[2560, 448, 1088], [15360, -1280, 1792], [-28224, -192, 512], [9856, 1536, 960], [7232, -4736, -3584], [-28928, 256, -576], [-960, -3456, 1920], [-8576, -1216, 704], [0, -5248, -4608], [11008, -1536, -2496], [-1344, -10944, -896], [320, 5568, 10944], [18496, -4352, -3648], [-1856, 5568, -1536], [1408, 128, -1088]]}, {"offset": [0, -170, 0], "rots": [[768, 192, 256], [7936, 3584, 2944], [-26816, -256, 576], [-3072, 1216, -1344], [6144, -5632, -4288], [-26240, 384, -576], [-1600, 320, 1856], [-1600, 0, 0], [12480, -13376, -21120], [25664, 3200, 1152], [-2880, 0, -3456], [19136, -16512, -10496], [24832, -1600, -704], [64, 64, 2560], [-5248, 0, -128]]}, {"offset": [0, -332, 0], "rots": [[256, 0, 256], [5760, 384, 8768], [-8704, 128, -64], [320, 640, -6784], [7680, -7808, -3712], [-16128, -12544, 12480], [7552, 576, 2560], [-3392, -1600, -1344], [-3648, -4160, -9920], [21824, 1664, 768], [5120, -448, 3712], [-2176, -512, 8704], [-1728, -128, -64], [1216, -4544, -3904], [384, 10560, 1792]]}, {"offset": [0, -460, 0], "rots": [[576, 0, 0], [64, 832, 1280], [-1600, 0, 128], [-192, -1024, -1344], [512, -1280, -768], [-4032, 0, -64], [2880, 768, 1024], [-512, -2048, 192], [-1408, -3392, -2688], [4416, -128, -640], [-256, -64, 2816], [-3264, -4672, 3648], [5376, 192, 384], [-1792, 4992, -2176], [-1088, -5760, 576]]}, {"offset": [0, -442, 0], "rots": [[6400, -32768, -32768], [-4800, -704, 384], [-12416, 448, -384], [-9472, -1536, 1792], [-4672, 960, -832], [-12608, -448, 512], [-8000, 2112, -2944], [2112, 0, 64], [-31872, 12544, -2688], [-320, 0, 64], [-1664, -5056, -5184], [0, 17728, -29824], [-320, 0, -64], [-64, -64, 4672], [10752, 128, 192]]}, {"offset": [0, -442, 0], "rots": [[960, 0, 0], [1536, 384, 832], [-14656, 1280, -1216], [-3520, 1472, -1984], [1216, 256, 768], [-5696, 0, 0], [3520, -128, -832], [-2112, 0, -64], [-640, -16896, -3840], [18816, -20096, -22400], [-320, 6976, 2368], [704, 15040, 1984], [3712, -7936, 256], [2688, 0, 0], [-5440, 320, -128]]}, {"offset": [62, -434, -7], "rots": [[-640, 64, -896], [1472, -2752, 4864], [0, 0, 0], [-192, 896, -4224], [3136, 2944, 384], [-5824, 0, 0], [3328, -320, 320], [960, -832, 1664], [-256, -14912, -11712], [12928, -31872, -31808], [0, 0, 9536], [1664, 14336, 8128], [13056, 31168, 31104], [0, 0, 4096], [-2688, 0, -1664]]}, {"offset": [-8, -431, -44], "rots": [[320, 0, -1216], [6848, 1088, 3392], [-7936, 0, 0], [-2304, 1280, -2304], [3520, -2944, 256], [-7168, 0, 0], [2560, 64, 1792], [4480, -128, 1920], [4032, -8384, -7168], [2432, -128, -384], [0, 0, 2176], [6592, 9664, 7936], [2816, 128, 512], [0, 0, -1792], [-6720, 0, 0]]}, {"offset": [57, -388, -7], "rots": [[-640, 64, 192], [4608, 256, 4736], [-8704, 0, 0], [4224, -384, -4544], [6656, 384, -1408], [-12352, 0, 0], [6464, 1024, 1664], [896, -128, -3200], [-2816, 3904, -8000], [-1216, 0, -384], [0, 0, -3008], [-8384, -1216, 3968], [2496, 64, 512], [0, 0, 4096], [-1408, 0, 3968]]}, {"offset": [2, -442, -7], "rots": [[-704, 3584, -64], [3776, -2048, -512], [-448, 0, 0], [-4608, -1664, 384], [128, 768, 256], [-2048, 0, 0], [1984, -2880, -832], [704, -4480, -384], [-1472, 704, -16320], [2112, 0, 0], [0, 0, 896], [-2496, -704, 16384], [1472, 0, 0], [0, 0, -1088], [-3648, 832, 64]]}, {"offset": [-11, -377, -83], "rots": [[2368, -192, -1152], [3520, 704, 3648], [-15872, -32768, 32704], [3712, 64, -2752], [6592, -320, 640], [-4992, 0, 0], [-6080, -960, 320], [1792, -576, 1920], [4800, -1920, -9216], [8640, -384, -512], [0, 0, 64], [6400, 4864, 10112], [8832, 512, 704], [0, 0, 576], [-5888, 0, -192]]}, ] ================================================ FILE: data/trx/ship/cfg/presets/tr1-pc.json5 ================================================ { "name_gs": "dynamic/config_presets/tr1_pc", "config": { "ui.menu_style": "pc", "ui.enable_smooth_bars": true, "ui.bar_look": "tr1_pc", "ui.ammo_counter_location": "top-right", "audio.enable_ps1_sfx": false, "gameplay.restore_ps1_enemies": false, }, } ================================================ FILE: data/trx/ship/cfg/presets/tr1-ps1.json5 ================================================ { "name_gs": "dynamic/config_presets/tr1_ps1", "config": { "ui.menu_style": "ps1", "ui.enable_smooth_bars": false, "ui.bar_look": "tr2_ps1", "ui.ammo_counter_location": "top-right", "audio.enable_ps1_sfx": true, "gameplay.restore_ps1_enemies": true, }, } ================================================ FILE: data/trx/ship/cfg/presets/tr2-pc.json5 ================================================ { "name_gs": "dynamic/config_presets/tr2_pc", "config": { "ui.menu_style": "pc", "ui.enable_smooth_bars": false, "ui.bar_look": "tr2_pc", "ui.ammo_counter_location": "top-right", "audio.enable_ps1_sfx": false, "gameplay.restore_ps1_enemies": false, }, } ================================================ FILE: data/trx/ship/cfg/presets/tr2-ps1.json5 ================================================ { "name_gs": "dynamic/config_presets/tr2_ps1", "config": { "ui.menu_style": "ps1", "ui.enable_smooth_bars": false, "ui.bar_look": "tr2_ps1", "ui.ammo_counter_location": "top-right", "audio.enable_ps1_sfx": true, "gameplay.restore_ps1_enemies": true, }, } ================================================ FILE: data/trx/ship/cfg/presets/tr3-pc.json5 ================================================ { "name_gs": "dynamic/config_presets/tr3_pc", "config": { "ui.menu_style": "pc", "ui.enable_smooth_bars": false, "ui.bar_look": "tr3_pc", "ui.ammo_counter_location": "top-right", "audio.enable_ps1_sfx": false, "gameplay.restore_ps1_enemies": false, "gameplay.enable_save_crystals": false, }, } ================================================ FILE: data/trx/ship/cfg/presets/tr3-ps1.json5 ================================================ { "name_gs": "dynamic/config_presets/tr3_ps1", "config": { "ui.menu_style": "ps1", "ui.enable_smooth_bars": false, "ui.bar_look": "tr3_ps1", "ui.ammo_counter_location": "bottom-right", "audio.enable_ps1_sfx": true, "gameplay.restore_ps1_enemies": true, "gameplay.enable_save_crystals": true, }, } ================================================ FILE: data/trx/ship/cfg/shaders/2d.glsl ================================================ #include "common.glsl" #define EFFECT_NONE 0 #define EFFECT_VIGNETTE 1 #define EFFECT_WAVE 2 #define WAVE_SPEED_SHORT -3.92 #define WAVE_SPEED_LONG -2.81 #define WAVE_TILE_PHASE_SHORT vec2(67.5, 73.0) #define WAVE_TILE_PHASE_LONG vec2(33.94, 28.31) #define WAVE_LIGHT_DELTA 0.125 #define WAVE_Y_TILES 6 #define WAVE_ORBIT_RADIUS 0.2 #define WAVE_FPS_DRIFT 25 / 30 uniform int uEffect; uniform float uOpacity; uniform float uBrightnessScale; uniform int uFitMode; // 0=stretch,1=letterbox,2=crop,3=smart uniform float uSrcAspect; // src_w/src_h #ifdef VERTEX layout(location = 0) in vec2 inPosition; layout(location = 1) in vec2 inTexCoords; out vec2 vertCoords; out float vertLight; out vec2 vertMappedUv; out vec4 vertContentRect; // x0,y0,x1,y1 in normalized screen coords void main() { if ((uEffect & EFFECT_WAVE) != 0) { float edgeOffset = (1.0 / WAVE_Y_TILES) * 2.0; vec2 baseNDC = ((inPosition.xy * (2.0 + 2.0 * edgeOffset)) - (1.0 + edgeOffset)) * vec2(1.0, -1.0); vec2 aspectCorrection = vec2(uViewportSize.y / uViewportSize.x, 1); vec2 repeat = float(WAVE_Y_TILES) / aspectCorrection; float shortPhase = dot(inPosition, repeat * WAVE_TILE_PHASE_SHORT); float longPhase = dot(inPosition, repeat * WAVE_TILE_PHASE_LONG); float shortAng = radians((uTime * WAVE_FPS_DRIFT) * WAVE_SPEED_SHORT + shortPhase); float longAng = radians((uTime * WAVE_FPS_DRIFT) * WAVE_SPEED_LONG + longPhase); float viewportSizeNDC = (1 + edgeOffset * 2); vec2 tileSize = viewportSizeNDC / repeat; vec2 vertexOffset = vec2(cos(shortAng), sin(shortAng)) * tileSize * WAVE_ORBIT_RADIUS; vertLight = 0.5 + (sin(shortAng) + sin(longAng)) * WAVE_LIGHT_DELTA; gl_Position = vec4(baseNDC + vertexOffset, 0.0, 1.0); } else { vec2 baseNDC = inPosition * vec2(2.0, -2.0) + vec2(-1.0, 1.0); gl_Position = vec4(baseNDC, 0.0, 1.0); } vertCoords = inPosition; int mode = uFitMode; float dstAspect = uViewportSize.x / uViewportSize.y; float srcAspect = uSrcAspect; if (mode == 3) { float arDiff = (srcAspect > dstAspect ? srcAspect / dstAspect : dstAspect / srcAspect) - 1.0; if (arDiff <= 0.1) { mode = 0; } else if (srcAspect <= dstAspect) { mode = 1; } else { mode = 2; } } float x0 = 0.0; float y0 = 0.0; float x1 = 1.0; float y1 = 1.0; vec2 uv = inTexCoords; if (mode == 1) { // Letterbox: compute content rect and map UVs within it. if (srcAspect > dstAspect) { float h = dstAspect / srcAspect; y0 = (1.0 - h) * 0.5; y1 = y0 + h; } else { float w = srcAspect / dstAspect; x0 = (1.0 - w) * 0.5; x1 = x0 + w; } uv = (vertCoords - vec2(x0, y0)) / vec2(x1 - x0, y1 - y0); } else if (mode == 2) { // Crop: keep full screen coverage, but zoom the UVs. if (srcAspect < dstAspect) { float h = dstAspect / srcAspect; float visible = 1.0 / h; float v0 = (1.0 - visible) * 0.5; uv.y = v0 + uv.y * visible; } else { float w = srcAspect / dstAspect; float visible = 1.0 / w; float u0 = (1.0 - visible) * 0.5; uv.x = u0 + uv.x * visible; } } vertMappedUv = uv; vertContentRect = vec4(x0, y0, x1, y1); } #elif defined(FRAGMENT) uniform sampler2D uTexMain; uniform vec4 uTexSize; in vec2 vertCoords; in float vertLight; in vec2 vertMappedUv; in vec4 vertContentRect; out vec4 outColor; void main(void) { if (vertCoords.x < vertContentRect.x || vertCoords.x > vertContentRect.z || vertCoords.y < vertContentRect.y || vertCoords.y > vertContentRect.w) { // Outside the content rect: force opaque black so nothing bleeds. outColor = vec4(0.0, 0.0, 0.0, 1.0); return; } vec2 uv = clampTexAtlas(vertMappedUv, uTexSize); outColor = texture(uTexMain, uv); if ((uEffect & EFFECT_WAVE) != 0) { outColor.rgb *= vertLight; } else if ((uEffect & EFFECT_VIGNETTE) != 0) { float x_dist = vertCoords.x - 0.5; float y_dist = vertCoords.y - 0.5; float lightV = 256.0 - sqrt(x_dist * x_dist + y_dist * y_dist) * 300.0; lightV = clamp(lightV, 0.0, 255.0) / 255.0; outColor *= vec4(lightV, lightV, lightV, 1.0); } if (uDesaturation > 0.0) { float luma = dot(outColor.rgb, vec3(0.299, 0.587, 0.114)); outColor.rgb = mix(outColor.rgb, vec3(luma), clamp(uDesaturation, 0.0, 1.0)); } outColor.rgb *= uUIBrightnessMultiplier * uBrightnessScale; outColor.a *= clamp(uOpacity, 0.0, 1.0); // Output premultiplied alpha so callers can use (ONE, ONE_MINUS_SRC_ALPHA). outColor.rgb *= outColor.a; } #endif ================================================ FILE: data/trx/ship/cfg/shaders/billboard.glsl ================================================ #define BILLBOARD_LOCK_NONE 0 #define BILLBOARD_LOCK_ROLL 1 #define BILLBOARD_LOCK_ROLL_PITCH 2 #define BILLBOARD_LOCK_PERSPECTIVE 3 vec4 offsetBillboard(vec3 pos, vec2 disp, mat4 view, mat4 model, mat4 proj, int mode) { vec3 right, up; if (mode == BILLBOARD_LOCK_NONE) { right = normalize(vec3(view[0][0], view[1][0], view[2][0])); up = normalize(vec3(view[0][1], view[1][1], view[2][1])); } else { // Base forward for all locked modes vec3 forward = -normalize(vec3(view[0][2], view[1][2], view[2][2])); const vec3 worldUp = vec3(0,1,0); if (mode != BILLBOARD_LOCK_ROLL) { // Kill pitch if requested by any cylindrical/perspective mode forward = normalize(vec3(forward.x, 0.0, forward.z)); } if (mode == BILLBOARD_LOCK_PERSPECTIVE) { vec4 clip = proj * view * model * vec4(pos,1); float ndcX = clip.x / clip.w; vec3 yawRight = normalize(cross(forward, worldUp)); float inv = inversesqrt(1.0 + ndcX * ndcX); forward = normalize(inv * forward - (ndcX * inv) * yawRight); } right = normalize(cross(forward, worldUp)); up = normalize(cross(right, forward)); } vec4 wp = model * vec4(pos,1); wp.xyz += disp.x * right + disp.y * up; return view * wp; } ================================================ FILE: data/trx/ship/cfg/shaders/common.glsl ================================================ #define PI 3.1415926538 #define WALL_L 1024 #define WIBBLE_SIZE 32 #define MAX_WIBBLE 2 #define SHADE_NEUTRAL 0x1000 #define SHADE_MAX 0x1FFF #define SHADE_CAUSTICS 0x300 #define VERT_NO_WIBBLE 0x0001u #define VERT_FLAT_SHADED 0x0002u #define VERT_REFLECTIVE 0x0004u #define VERT_NO_LIGHTING 0x0008u #define VERT_BILLBOARD 0x0010u #define VERT_ABS_SPRITE 0x0020u #define VERT_NO_ALPHA_DISCARD 0x0040u #define VERT_USE_DYNAMIC_LIGHT 0x0080u #define VERT_USE_OBJECT_LIGHT 0x0100u #define VERT_USE_OWN_LIGHT 0x0200u #define VERT_MOVE 0x0400u #define VERT_GLOW 0x0800u #define LIGHTING_CONTRAST_LOW 0 #define LIGHTING_CONTRAST_MEDIUM 1 #define LIGHTING_CONTRAST_HIGH 2 layout(std140) uniform Globals { vec4 uGlobalTint; vec4 uFogColor; vec2 uFogDistance; // x = fog start, y = fog end vec2 uViewportSize; float uTime; float uTimeInGame; float uBrightnessMultiplier; float uUIBrightnessMultiplier; float uGamma; float uDesaturation; float uSunsetDuration; float uMinShade; int uBillboardLockMode; int uLightingEnabled; // bool int uTrapezoidFilterEnabled; // bool int uReflectionsEnabled; // bool int uTexturesEnabled; // bool int uTRVersion; }; layout(std140) uniform Matrices { mat4 uMatProj; mat4 uMatView; }; vec2 clampTexAtlas(vec2 uv, vec4 atlasSize) { float epsilon = 0.5 / 256.0; return clamp(uv, atlasSize.xy + epsilon, atlasSize.zw - epsilon); } ================================================ FILE: data/trx/ship/cfg/shaders/fbo.glsl ================================================ #ifdef VERTEX layout(location = 0) in vec2 inPosition; out vec2 vertTexCoords; void main(void) { vertTexCoords = inPosition; gl_Position = vec4(vertTexCoords * vec2(2.0, 2.0) + vec2(-1.0, -1.0), 0.0, 1.0); } #elif defined(FRAGMENT) uniform sampler2D uTex0; in vec2 vertTexCoords; out vec4 outColor; void main(void) { outColor = texture(uTex0, vertTexCoords); } #endif ================================================ FILE: data/trx/ship/cfg/shaders/lights.glsl ================================================ #define MAX_LIGHTS 32 #define RLM_NORMAL 0 #define RLM_FLICKER 1 #define RLM_GLOW 2 #define RLM_SUNSET 3 uniform int uWaterEffect; uniform vec3 uWaterEffectParams; // x=choppy amp, y=shimmer amp, z=abs intensity struct Light { vec4 pos; vec4 color; float shade; float falloff; float kind; float _pad0; }; layout(std140) uniform Lights { int uNumLights; int uRoomLightMode; Light uLights[MAX_LIGHTS]; }; layout(std140) uniform LightSource { float uLightAdder; float uLightDivider; vec4 uLightVectorSource; vec4 uTR3Ambient; vec4 uTR3LightDirView[3]; vec4 uTR3LightColor[3]; }; float ogPhaseTurns(vec3 worldPos, int scheme) { // bucket like OG: xyz * (1/64, 1/64, 1/128) ivec3 q = ivec3(floor(vec3(worldPos.x / 64.0, worldPos.y / 64.0, worldPos.z / 128.0))); // cheap hash -> 0..1 float n = fract(sin( dot(vec3(q), vec3(12.9898, 78.233, 37.719)) + float(scheme) * 19.19 ) * 43758.5453); // OG-ish: random is multiples of 4, then &63 => 16 lanes float lane = floor(n * 16.0); // 0..15 float offTurns = lane / 16.0; // 0,1/16,...15/16 // time base is uTimeInGame with period 64 float tTurns = fract(uTimeInGame / 64.0); return tTurns + offTurns; } float effectChoppy(vec3 worldPos) { int scheme = clamp(uWaterEffect - 2, 0, 21); float angle = fract(ogPhaseTurns(worldPos, scheme)) * 2 * PI; return -sin(angle) * uWaterEffectParams.x / 2.0; } float effectShimmer(vec3 worldPos) { int scheme = clamp(uWaterEffect - 2, 0, 21); float angle = fract(ogPhaseTurns(worldPos, scheme)) * 2 * PI; return sin(angle) * uWaterEffectParams.y * 8.0; } float effectAbs() { return uWaterEffectParams.z * 8.0; } int lightFlicker(float t) { float h = fract(sin(t * 593.123) * 43758.5453); return int(h * 32.0); } int lightGlow(float time) { float phase = mod(time, 32.0) / 32.0; float s = sin(phase * 2 * PI); float normalized = (s + 1.0) * 0.5; return int(normalized * 31.0); } int lightSunset(float time) { float sunsetProgress = clamp(time / max(1, uSunsetDuration), 0.0, 1.0); return int(sunsetProgress * 31.0); } int calcRoomShadeIndex(int mode, float time) { if (mode == RLM_FLICKER) { return lightFlicker(time); } if (mode == RLM_GLOW) { return lightGlow(time); } if (mode == RLM_SUNSET) { return lightSunset(time); } return 0; } float lightRoom( int lightMode, float time, float vertexPhase) { int i = calcRoomShadeIndex(lightMode, time); float j = float(int(vertexPhase) & 31); const float MAX_UNIT = 512.0; return (j - 16.0) * float(i) * MAX_UNIT / 31.0; } float lightWaterCaustics(float shade, vec3 vtxPos) { float time = mod(float(uTimeInGame), float(WIBBLE_SIZE)); // just a random offset based on the source vertex float caustic = fract(sin(dot(vtxPos.xyz, vec3(12.9898, 78.233, 37.719))) * 43758.5453); caustic = (caustic * 1023.0) - 511.0; float angle = radians(360.0 * mod((time + caustic) / float(WIBBLE_SIZE), 1.0)); return clamp(shade + sin(angle) * float(SHADE_CAUSTICS), 0.0, float(SHADE_MAX)); } vec3 safeNormalize(vec3 v) { float len2 = dot(v, v); if (len2 <= 0.0) { return vec3(0.0); } return v * inversesqrt(len2); } float lightObjects(vec3 rawNormal, vec4 vertexPos) { float lightAdder = uLightAdder; if (uLightDivider != 0) { vec3 L = mat3(transpose(uMatView * uMatModel)) * uLightVectorSource.xyz / uLightDivider; lightAdder += dot(L, rawNormal.xyz / (1 << 14)) / 4; lightAdder = clamp(lightAdder, 0, SHADE_MAX); } return lightAdder; } vec3 lightObjectsTR3(vec3 rawNormal) { vec3 N = safeNormalize(mat3(uMatView * uMatModel) * (rawNormal.xyz / float(1 << 14))); vec3 L0 = uTR3LightDirView[0].xyz; vec3 L1 = uTR3LightDirView[1].xyz; vec3 L2 = uTR3LightDirView[2].xyz; float d0 = max(dot(N, L0), 0.0); float d1 = max(dot(N, L1), 0.0); float d2 = max(dot(N, L2), 0.0); vec3 rgb = uTR3Ambient.rgb + uTR3LightColor[0].rgb * d0 + uTR3LightColor[1].rgb * d1 + uTR3LightColor[2].rgb * d2; return clamp(rgb, 0.0, 1.0); } vec3 lightOwnTR3(float shade) { float shade8 = floor((SHADE_MAX - shade) / 32.0); // (0x1FFF - shade) >> 5 shade8 = (shade8 <= 0.0) ? 255.0 : shade8; return clamp(uTR3Ambient.rgb * (shade8 / 255.0), 0.0, 1.0); } float lightDynamicTR12Lum(float baseLight, vec4 vertexPos) { float lightAdder = baseLight; for (int i = 0; i < uNumLights; i++) { if (uLights[i].kind != 0.0) { continue; } vec3 dist = uLights[i].pos.xyz - vertexPos.xyz; float radius = exp2(uLights[i].falloff); float distSq = dot(dist, dist); if (distSq > radius * radius) { continue; } float maxShade = exp2(uLights[i].shade); float distTerm = distSq / exp2(2 * uLights[i].falloff - uLights[i].shade); float shade = maxShade - distTerm; lightAdder -= shade; } return max(lightAdder, 0); } vec3 lightDynamicTR12RGB(vec4 vertexPos) { vec3 add = vec3(0.0); for (int i = 0; i < uNumLights; i++) { if (uLights[i].kind == 0.0) { continue; } float radius = uLights[i].falloff * 0.5; vec3 dist = uLights[i].pos.xyz - vertexPos.xyz; float distSq = dot(dist, dist); float radiusSq = radius * radius; if (distSq > radiusSq) { continue; } float d = sqrt(distSq); float factor = (radius - d) / max(radius, 1.0); add += factor * uLights[i].color.rgb; } return add; } vec3 lightDynamicTR3(vec4 vertexPos) { vec3 add = vec3(0.0); for (int i = 0; i < uNumLights; i++) { float radius = uLights[i].falloff * 0.5; // falloff_raw >> 1 vec3 dist = uLights[i].pos.xyz - vertexPos.xyz; float distSq = dot(dist, dist); float radiusSq = radius * radius; if (distSq > radiusSq) { continue; } float d = sqrt(distSq); float factor = (radius - d) / max(radius, 1.0); add += factor * uLights[i].color.rgb; } return add; } float getDynamicLightContrastMul() { // `uMinShade` is configured via the "lighting contrast" option. // For TR1/TR2 it clamps the minimum shade; in TR3 the lighting is additive, // so we remap it to a multiplier: // LOW: uMinShade = SHADE_NEUTRAL -> 1.0 // MED: uMinShade = SHADE_HIGH -> 1.5 // HIGH:uMinShade = 0 -> 2.0 return clamp(2.0 - (uMinShade / float(SHADE_NEUTRAL)), 1.0, 2.0); } float lightLumTR12(float shade, uint flags, vec3 normal, vec4 pos, float phase) { if ((flags & VERT_USE_OWN_LIGHT) != 0u) { shade = uLightAdder + shade; } else if ((flags & VERT_USE_OBJECT_LIGHT) != 0u) { shade = lightObjects(normal, pos); } else { if ((flags & VERT_USE_DYNAMIC_LIGHT) != 0u) { shade = lightDynamicTR12Lum(shade, pos); shade += lightRoom(uRoomLightMode, uTimeInGame, phase); } shade = clamp(shade, 0, SHADE_MAX); } if (uWaterEffect == 1) { shade = lightWaterCaustics(shade, pos.xyz); } return shade; } struct LightingResult { float shade; // used only for TR1-2 vec3 add; // TR3: additive light (dynamic + post effects) vec3 mul; // TR3: multiplicative light (object/own) }; LightingResult light( float shade, uint flags, vec3 normal, vec4 pos, float vertexPhase) { LightingResult result; result.shade = SHADE_NEUTRAL; result.add = vec3(0.0); result.mul = vec3(1.0); if (uLightingEnabled == 0) { return result; } if ((flags & VERT_NO_LIGHTING) != 0u) { return result; } #if TR_VERSION >= 3 if ((flags & VERT_USE_DYNAMIC_LIGHT) != 0u) { result.add += lightDynamicTR3(pos) * getDynamicLightContrastMul(); } if ((flags & VERT_USE_OBJECT_LIGHT) != 0u) { result.mul *= lightObjectsTR3(normal); } else if ((flags & VERT_USE_OWN_LIGHT) != 0u) { result.mul *= lightOwnTR3(shade); } float add = 0.0; if ((flags & VERT_MOVE) != 0u) { add += effectChoppy(pos.xyz) / 256.0; } if ((flags & VERT_GLOW) != 0u) { add += effectShimmer(pos.xyz) / 256.0; add += effectAbs() / 256.0; } result.add += vec3(add); result.shade = SHADE_NEUTRAL; #else result.shade = lightLumTR12(shade, flags, normal, pos, vertexPhase); if ((flags & VERT_USE_DYNAMIC_LIGHT) != 0u) { result.add += lightDynamicTR12RGB(pos) * getDynamicLightContrastMul(); } #endif return result; } ================================================ FILE: data/trx/ship/cfg/shaders/meshes.glsl ================================================ #include "common.glsl" #ifdef VERTEX uniform mat4 uMatModel; uniform bool uWibbleEffect; #include "billboard.glsl" #include "lights.glsl" layout(location = 0) in vec4 inPosition; layout(location = 1) in vec4 inNormal; layout(location = 2) in vec3 inUVW; layout(location = 3) in vec4 inTextureSize; layout(location = 4) in vec2 inTrapezoidRatios; layout(location = 5) in uint inFlags; layout(location = 6) in vec4 inColor; layout(location = 7) in float inShade; out vec4 gEyePos; out vec3 gNormal; flat out uint gFlags; flat out int gTexLayer; out vec2 gTexUV; flat out vec4 gAtlasSize; out vec2 gTrapezoidRatios; out float gShade; out vec4 gColor; vec3 gammaCurve(vec3 rgb, float gamma_exp) { return pow(clamp(rgb, 0.0, 1.0), vec3(gamma_exp)); } vec3 waterWibble(vec4 worldPosition, vec4 screenPosition) { vec3 ndc = screenPosition.xyz / screenPosition.w; vec2 pixelPos = (ndc.xy * 0.5 + 0.5) * uViewportSize; #if TR_VERSION == 3 float phases = (uTimeInGame * 0.5 + length(worldPosition.xyz)) * (2.0 * PI / WIBBLE_SIZE); float scale = length(uViewportSize) / length(vec2(640.0, 480.0)); float adjustedWibble = scale; pixelPos.y += sin(phases) * adjustedWibble; #else float phases = (uTimeInGame + length(worldPosition.xyz)) * (2.0 * PI / WIBBLE_SIZE); pixelPos.x += sin(phases) * MAX_WIBBLE; pixelPos.y += cos(phases) * MAX_WIBBLE; #endif // reverse transform ndc.xy = (pixelPos / uViewportSize - 0.5) * 2.0; return ndc * screenPosition.w; } void main(void) { vec4 worldPos = uMatModel * vec4(inPosition.xyz, 1.0); if ((inFlags & VERT_MOVE) != 0u) { float waterMul = (uWaterEffect != 0) ? 1.0 : 0.0; worldPos.y += effectChoppy(worldPos.xyz) * waterMul; } if ((inFlags & (VERT_ABS_SPRITE | VERT_BILLBOARD)) != 0u) { int lockMode = (inFlags & VERT_ABS_SPRITE) != 0u ? BILLBOARD_LOCK_NONE : uBillboardLockMode; gEyePos = offsetBillboard(inPosition.xyz, inNormal.xy, uMatView, uMatModel, uMatProj, lockMode); } else { gEyePos = uMatView * worldPos; } gNormal = inNormal.xyz; gl_Position = uMatProj * gEyePos; gl_Position.z += inPosition.w; // Apply water wibble effect only to non-sprite vertices if (uWibbleEffect && (inFlags & (VERT_NO_WIBBLE | VERT_BILLBOARD)) == 0u) { gl_Position.xyz = waterWibble(worldPos, gl_Position); } gFlags = inFlags; gAtlasSize = inTextureSize; gTexLayer = (uTexturesEnabled != 0) && (gFlags & VERT_FLAT_SHADED) == 0u ? int(inUVW.z) : -1; gTrapezoidRatios = inTrapezoidRatios; gTexUV = inUVW.xy; if (uTrapezoidFilterEnabled != 0) { gTexUV *= inTrapezoidRatios; } // The vertex diffuse is lit first and then modulated by the texture (or by // the flat polygon's palette color). Keep the lighting component separate // from the base color so gamma is applied in the right place. LightingResult lr = light(inShade, gFlags, inNormal.xyz, worldPos, inNormal.w); gShade = lr.shade; float gamma_exp = 1.0 / ((uGamma / 10.0) * 4.0); #if TR_VERSION >= 3 vec3 lightIn; vec3 modulate; if ((gFlags & VERT_FLAT_SHADED) == 0u) { if (uLightingEnabled == 0) { lightIn = vec3(1); } else { lightIn = inColor.rgb; } modulate = vec3(1); } else { lightIn = vec3(1); modulate = inColor.rgb; } // Combine lighting in linear-ish space first: (base + add) * mul vec3 lit = clamp(lightIn + lr.add, 0.0, 1.0); lit *= lr.mul; lit = gammaCurve(lit, gamma_exp); // Apply flat shading AFTER modulation gColor = vec4(lit * modulate, inColor.a); #else float shade_mul = 1.0; if ((gFlags & VERT_NO_LIGHTING) == 0u) { shade_mul = (2.0 - (max(gShade, uMinShade) / SHADE_NEUTRAL)); } // `shade_mul` is roughly in [0..2]. Remap to [0..1], apply the gamma // curve, and restore the range. Use sqrt() to limit the effect scope, // since we're applying it to the shade (TR1-2) rather than RGB (TR3). vec3 mul = gammaCurve(vec3(shade_mul * 0.5), sqrt(gamma_exp)) * 2.0; gColor = inColor; if ((gFlags & VERT_FLAT_SHADED) == 0u) { gColor.rgb = gammaCurve(gColor.rgb, gamma_exp); } gColor.rgb *= mul; // Preserve the >1.0 lighting range until after texturing so TR1/TR2 // high contrast can still brighten textured geometry. gColor.rgb += lr.add; #endif } #elif defined(FRAGMENT) uniform sampler2DArray uTexAtlas; uniform sampler2D uTexEnvMap; uniform vec3 uTint; uniform bool uDiscardAlpha; in vec4 gEyePos; in vec3 gNormal; flat in uint gFlags; flat in int gTexLayer; in vec2 gTexUV; flat in vec4 gAtlasSize; in float gShade; in vec4 gColor; in vec2 gTrapezoidRatios; out vec4 outColor; vec4 applyFog(vec4 color, float dist) { float fogFactor = clamp( (dist - uFogDistance.x) / (uFogDistance.y - uFogDistance.x), 0.0, 1.0); return mix(color, uFogColor, fogFactor); } void main(void) { vec4 texColor = gColor; // Texturing and base color if (gTexLayer >= 0) { vec3 texCoords = vec3(gTexUV.x, gTexUV.y, gTexLayer); if (uTrapezoidFilterEnabled != 0) { texCoords.xy /= gTrapezoidRatios; } texCoords.xy = clampTexAtlas(texCoords.xy, gAtlasSize); texColor *= texture(uTexAtlas, texCoords); } else { texColor.rgb *= texColor.a; } // Alpha discard - chroma keying || transparent pixels in the opaque pass if (texColor.a <= 0.0 || (uDiscardAlpha && texColor.a < 0.99 && (gFlags & VERT_NO_ALPHA_DISCARD) == 0u)) { discard; } // Reflections if ((gFlags & VERT_REFLECTIVE) != 0u && uReflectionsEnabled != 0) { vec2 env_uv = (normalize(gNormal) * 0.5 + 0.5).xy; env_uv.y = 1.0 - env_uv.y; texColor *= texture(uTexEnvMap, env_uv) * 2; } // Fog if ((gFlags & VERT_NO_LIGHTING) == 0u && uLightingEnabled != 0) { texColor = applyFog(texColor, length(gEyePos.xyz)); } texColor.rgb *= uBrightnessMultiplier; texColor.rgb *= uTint; // Optional desaturation (0 = original, 1 = monochrome). if (uDesaturation > 0.0) { const vec3 luma = vec3(0.2126, 0.7152, 0.0722); float y = dot(texColor.rgb, luma) * 0.5; texColor.rgb = mix(texColor.rgb, vec3(y), clamp(uDesaturation, 0.0, 1.0)); } texColor *= uGlobalTint; outColor = texColor; } #endif ================================================ FILE: data/trx/ship/cfg/shaders/meshes_tr12.glsl ================================================ #define TR_VERSION 2 #include "meshes.glsl" ================================================ FILE: data/trx/ship/cfg/shaders/meshes_tr3.glsl ================================================ #define TR_VERSION 3 #include "meshes.glsl" ================================================ FILE: data/trx/ship/cfg/shaders/ui.glsl ================================================ #include "common.glsl" #ifdef VERTEX layout(location = 0) in vec4 inPosition; layout(location = 1) in vec3 inUVW; layout(location = 2) in vec4 inTextureSize; layout(location = 3) in uint inFlags; layout(location = 4) in vec4 inColor; out vec3 gNormal; flat out uint gFlags; flat out int gTexLayer; out vec2 gTexUV; flat out vec4 gAtlasSize; out vec4 gColor; void main(void) { gl_Position = uMatProj * uMatView * vec4(inPosition.xyz, 1.0); gFlags = inFlags; gAtlasSize = inTextureSize; gTexUV = inUVW.xy; gTexLayer = int(inUVW.z); gColor = inColor; } #elif defined(FRAGMENT) uniform sampler2DArray uTexAtlas; flat in uint gFlags; flat in int gTexLayer; in vec2 gTexUV; flat in vec4 gAtlasSize; in vec4 gColor; out vec4 outColor; void main(void) { vec4 texColor = gColor; if ((gFlags & VERT_FLAT_SHADED) == 0u && gTexLayer >= 0) { vec3 texCoords = vec3(gTexUV.x, gTexUV.y, gTexLayer); texCoords.xy = clampTexAtlas(texCoords.xy, gAtlasSize); texColor *= texture(uTexAtlas, texCoords); if (texColor.a <= 0.0) { discard; } } else { texColor.rgb *= texColor.a; } texColor.rgb *= uUIBrightnessMultiplier; outColor = texColor; } #endif ================================================ FILE: data/trx/ship/cfg/ui.json5 ================================================ { "bars": { "tr1_pc": { "name_gs": "dynamic/enums/bar_look/tr1_pc", "scale": 1, "style": "pc", "border_light": "#353535", "border_dark": "#353535", "colors": { "red": ["#A0281C", "#B82C20", "#A0281C", "#7C2020", "#541420"], "blue": ["#3d717b", "#65929a", "#3d717b", "#1f5d6b", "#004a5b"], "grey": ["#586458", "#748474", "#586458", "#4C504C", "#303030"], "brown": ["#7c5e25", "#a1823c", "#7c5e25", "#644613", "#4c2e02"], "silver": ["#969696", "#E6E6E6", "#C8C8C8", "#8C8C8C", "#646464"], "teal": ["#14be6e", "#1ee682", "#14be6e", "#0f964b", "#0a6e28"], "yellow": ["#b9b723", "#d6d629", "#b9b723", "#9b981e", "#7e7218"], "cyan": ["#20b3bc", "#25d1da", "#20b3bc", "#1b949e", "#166f80"], "pink": ["#DC8CAA", "#FF96C8", "#D282A0", "#A56478", "#783C46"], "purple": ["#562484", "#682d9f", "#562484", "#492070", "#3c195c"], "green": ["#239117", "#33b020", "#239117", "#217210", "#23540b"], "iron": ["#475e76", "#5a748a", "#475e76", "#374e60", "#29414b"], "orange": ["#a86015", "#c66b1e", "#a86015", "#88440f", "#6a260a"] } }, "tr2_pc": { "name_gs": "dynamic/enums/bar_look/tr2_pc", "scale": 0.75, "style": "pc", "border_light": "#FFFFFF", "border_dark": "#404040", "colors": { "red": ["#e40c10", "#e86c04", "#e40c10", "#e40c10", "#e40c10"], "blue": ["#1c10f4", "#fcfcfc", "#1c10f4", "#1c10f4", "#1c10f4"], "grey": ["#4C504C", "#A0A0A0", "#4C504C", "#4C504C", "#4C504C"], "brown": ["#80481c", "#a88044", "#80481c", "#80481c", "#80481c"], "silver": ["#969696", "#E6E6E6", "#969696", "#969696", "#969696"], "teal": ["#00a05d", "#1ee62f", "#00a05d", "#00a05d", "#00a05d"], "yellow": ["#b5a000", "#ffff1f", "#b5a000", "#b5a000", "#b5a000"], "cyan": ["#00d6dc", "#00fbff", "#00b1b9", "#00b1b9", "#00b1b9"], "pink": ["#db649c", "#ecaab0", "#db649c", "#db649c", "#db649c"], "purple": ["#461E6B", "#8040ff", "#461E6B", "#461E6B", "#461E6B"], "green": ["#37aa0b", "#08e713", "#37aa0b", "#37aa0b", "#37aa0b"], "iron": ["#607088", "#fcfcfc", "#607088", "#607088", "#607088"], "orange": ["#cc580b", "#cf8f04", "#cc580b", "#cc580b", "#cc580b"] } }, "tr3_pc": { "name_gs": "dynamic/enums/bar_look/tr3_pc", "scale": 0.75, "style": "pc", "border_light": "#FFFFFF", "border_dark": "#404040", "colors": { "red": ["#4F0000", "#FF0000", "#7F0000", "#7F0000", "#4F0000"], "blue": ["#00004F", "#0000FF", "#00007F", "#00007F", "#00004F"], "grey": ["#4F4F4F", "#7F7F7F", "#5F5F5F", "#5F5F5F", "#4F4F4F"], "brown": ["#3f2710", "#cb7d34", "#653e1a", "#653e1a", "#3f2710"], "silver": ["#4F4F4F", "#BFBFBF", "#7F7F7F", "#7F7F7F", "#4F4F4F"], "teal": ["#004F2F", "#00FF7F", "#007F4F", "#007F4F", "#004F2F"], "yellow": ["#4F4F00", "#FFFF00", "#7F7F00", "#7F7F00", "#4F4F00"], "cyan": ["#004F4F", "#00FFFF", "#007F7F", "#007F7F", "#004F4F"], "pink": ["#4F003F", "#FF7FBF", "#7F3F5F", "#7F3F5F", "#4F003F"], "purple": ["#2F004F", "#7F00FF", "#4F007F", "#4F007F", "#2F004F"], "green": ["#004F00", "#00FF00", "#007F00", "#007F00", "#004F00"], "iron": ["#3d5164", "#93a9bd", "#4f6981", "#4f6981", "#3d5164"], "orange": ["#783600", "#ff8929", "#a84c00", "#a84c00", "#783600"] } }, "tr2_ps1": { "name_gs": "dynamic/enums/bar_look/tr2_ps1", "scale": 1, "style": "ps1", "border_tl": "#508282", "border_tr": "#9F9F9F", "border_bl": "#294141", "border_br": "#4F4F4F", "colors": { "red-green": [["#B60100", "#EA0100", "#B60100", "#870000", "#6D0000"], ["#01B900", "#03FA00", "#01B900", "#018800", "#015A00"]], "dark-red-purple": [["#400000", "#4C0000", "#580000", "#640000", "#7C0000"], ["#400080", "#4C0099", "#5800B2", "#6400CB", "#7C00FD"]], "teal-green": [["#00717A", "#009298", "#00717A", "#005D6A", "#004A5A"], ["#007101", "#009201", "#007101", "#005D01", "#004A00"]], "red-yellow": [["#C00100", "#F00100", "#C00100", "#900000", "#600000"], ["#C0B900", "#F0E800", "#C0B900", "#908A00", "#605A00"]], "dark-blue-red": [["#000170", "#090091", "#000170", "#000053", "#00003E"], ["#C00100", "#F00100", "#C00100", "#900000", "#600000"]], "orange-red": [["#B64C00", "#ED6400", "#B64C00", "#843700", "#6D2D00"], ["#C00100", "#F00100", "#C00100", "#900000", "#600000"]], "yellow-green": [["#B78900", "#EEB300", "#B78900", "#846300", "#6D5200"], ["#01B900", "#03FA00", "#01B900", "#018800", "#015A00"]], "purple-teal": [["#2F0030", "#5F0060", "#7E007F", "#5F0060", "#2F0030"], ["#002E30", "#015D60", "#017C7F", "#015D60", "#002E30"]] } }, "tr3_ps1": { "name_gs": "dynamic/enums/bar_look/tr3_ps1", "scale": 1, "style": "ps1", "border_tl": "#508282", "border_tr": "#9F9F9F", "border_bl": "#294141", "border_br": "#4F4F4F", "colors": { "red-green": [["#3f0000", "#5f0000", "#7f0000", "#5f0000", "#3f0000"], ["#007e00", "#00bc00", "#00fb00", "#00bc00", "#007e00"]], "dark-red-purple": [["#400000", "#4C0000", "#580000", "#640000", "#7C0000"], ["#400080", "#4C0099", "#5800B2", "#6400CB", "#7C00FD"]], "teal-green": [["#00717a", "#009299", "#00717a", "#005d6a", "#004a5a"], ["#007100", "#009202", "#007100", "#005d00", "#004a00"]], "red-yellow": [["#900000", "#c00000", "#f00000", "#c00000", "#900000"], ["#908a00", "#c0b900", "#f0e800", "#c0b900", "#908a00"]], "dark-blue-red": [["#00005f", "#00007f", "#00009f", "#00007f", "#00005f"], ["#5c0002", "#7c0002", "#9a0004", "#7c0002", "#5c0002"]], "orange-red": [["#B64C00", "#ED6400", "#B64C00", "#843700", "#6D2D00"], ["#C00100", "#F00100", "#C00100", "#900000", "#600000"]], "yellow-green": [["#B78900", "#EEB300", "#B78900", "#846300", "#6D5200"], ["#01B900", "#03FA00", "#01B900", "#018800", "#015A00"]], "purple-teal": [["#2f0030", "#5f0060", "#7e007f", "#4f0050", "#1f0020"], ["#002e30", "#005e60", "#007d7f", "#004e50", "#001e20"]] } } }, "ui": { // Menu appearance colors per TR version × UI style (pc/ps1). // Selected at runtime by g_TRVersion and g_Config.ui.menu_style. "tr1": { "pc": { // TS_BACKGROUND gradient: top color, bottom color "background": ["#00000080", "#00000080"], // TS_BACKGROUND_HEAVY gradient: top color, bottom color "background_heavy": ["#000000E0", "#000000E0"], "outline_light": "#E8C070FF", "outline_dark": "#8C7038FF" }, "ps1": { "background_edge": "#00000080", "background_center": "#00004080", "background_heavy_edge": "#000000E0", "background_heavy_center": "#000000E0", "heading_edge": "#00000080", "heading_center": "#80381080", "requested_edge": "#00000080", "requested_center": "#8038DC80", "requested_outline_ch": "#C8C8C8FF", "requested_outline_cv": "#C8C8C8FF", "requested_outline_edge": "#282828FF", "outline_tl": "#606060FF", "outline_tr": "#202020FF", "outline_bl": "#404040FF", "outline_br": "#000000FF", "heading_outline": "#000000FF", } }, "tr2": { "pc": { // Note: TR2 uses O_TEXT_BOX for the outlines. "background": ["#00000080", "#00000080"], "background_heavy": ["#000000E0", "#000000E0"], "outline_light": "#FFFFFFFF", "outline_dark": "#404040FF" }, "ps1": { "background_edge": "#00200080", "background_center": "#00600080", "background_heavy_edge": "#000000E0", "background_heavy_center": "#002000E0", "heading_edge": "#00000080", "heading_center": "#10803880", "requested_edge": "#00000080", "requested_center": "#38F08080", "requested_outline_ch": "#FFFFFFFF", "requested_outline_cv": "#38F080FF", "requested_outline_edge": "#000000FF", "outline_tl": "#606060FF", "outline_tr": "#202020FF", "outline_bl": "#404040FF", "outline_br": "#000000FF", "heading_outline": "#000000FF", } }, "tr3": { "pc": { "background": ["#003FFF50", "#003F1F50"], "background_heavy": ["#001020E0", "#000000E0"], "outline_light": "#4080C0FF", "outline_dark": "#001040FF" }, "ps1": { "background_edge": "#30200080", "background_center": "#00600080", "background_heavy_edge": "#000000E0", "background_heavy_center": "#002000E0", "heading_edge": "#00000080", "heading_center": "#10803880", "requested_edge": "#00000080", "requested_center": "#38F08080", "requested_outline_ch": "#FFFFFFFF", "requested_outline_cv": "#38F080FF", "requested_outline_edge": "#000000FF", "outline_tl": "#606060FF", "outline_tr": "#202020FF", "outline_bl": "#404040FF", "outline_br": "#000000FF", "heading_outline": "#000000FF", } } } } ================================================ FILE: data/trx/ship/games/tr1/catalog_item_actions.csv ================================================ 0, ITEM_ACTION_TURN_180 1, ITEM_ACTION_FLOOR_SHAKE 2, ITEM_ACTION_LARA_NORMAL 3, ITEM_ACTION_BUBBLES 4, ITEM_ACTION_FINISH_LEVEL 5, ITEM_ACTION_EARTHQUAKE 6, ITEM_ACTION_FLOOD 7, ITEM_ACTION_RAISING_BLOCK 8, ITEM_ACTION_STAIRS_TO_SLOPE 9, ITEM_ACTION_DROP_SAND 10, ITEM_ACTION_POWER_UP 11, ITEM_ACTION_EXPLOSION 12, ITEM_ACTION_LARA_HANDS_FREE 13, ITEM_ACTION_FLIP_MAP 14, ITEM_ACTION_LARA_DRAW_RIGHT_GUN 15, ITEM_ACTION_CHAIN_BLOCK 16, ITEM_ACTION_FLICKER 17, ITEM_ACTION_INVISIBILITY_ON 18, ITEM_ACTION_SHADOW_ON 19, ITEM_ACTION_SHADOW_OFF 62, ITEM_ACTION_TURN_90 ================================================ FILE: data/trx/ship/games/tr1/catalog_lara_anims.csv ================================================ 0, LA_RUN 1, LA_WALK_FORWARD 2, LA_WALK_STOP_RIGHT 3, LA_WALK_STOP_LEFT 4, LA_WALK_TO_RUN_RIGHT 5, LA_WALK_TO_RUN_LEFT 6, LA_RUN_START 7, LA_RUN_TO_WALK_RIGHT 8, LA_RUN_TO_STAND_LEFT 9, LA_RUN_TO_WALK_LEFT 10, LA_RUN_TO_STAND_RIGHT 11, LA_STAND_STILL 12, LA_TURN_RIGHT_SLOW 13, LA_TURN_LEFT_SLOW 14, LA_JUMP_FORWARD_LAND_START_UNUSED 15, LA_JUMP_FORWARD_LAND_END_UNUSED 16, LA_RUN_JUMP_RIGHT_START 17, LA_RUN_JUMP_RIGHT_CONTINUE 18, LA_RUN_JUMP_LEFT_START 19, LA_RUN_JUMP_LEFT_CONTINUE 20, LA_WALK_FORWARD_START 21, LA_WALK_FORWARD_START_CONTINUE 22, LA_JUMP_FORWARD_TO_FREEFALL 23, LA_FREEFALL 24, LA_FREEFALL_LAND 25, LA_FREEFALL_LAND_DEATH 26, LA_STAND_TO_JUMP_UP 27, LA_STAND_TO_JUMP_UP_CONTINUE 28, LA_JUMP_UP 29, LA_JUMP_UP_TO_HANG_UNUSED 30, LA_JUMP_UP_TO_FREEFALL 31, LA_JUMP_UP_LAND 32, LA_SMASH_JUMP 33, LA_SMASH_JUMP_CONTINUE 34, LA_FALL_START 35, LA_FALL 36, LA_FALL_TO_FREEFALL 37, LA_HANG_TO_FREEFALL 38, LA_WALK_BACK_END_RIGHT 39, LA_WALK_BACK_END_LEFT 40, LA_WALK_BACK 41, LA_WALK_BACK_START 42, LA_CLIMB_3CLICK 43, LA_CLIMB_3CLICK_END_TO_RUN 44, LA_TURN_RIGHT 45, LA_JUMP_FORWARD_TO_FREEFALL_2 46, LA_REACH_TO_FREEFALL 47, LA_ROLL_ALTERNATE 48, LA_ROLL_END_ALTERNATE 49, LA_JUMP_FORWARD_END_TO_FREEFALL 50, LA_CLIMB_2CLICK 51, LA_CLIMB_2CLICK_END 52, LA_CLIMB_2CLICK_END_TO_RUN 53, LA_WALL_SMASH_LEFT 54, LA_WALL_SMASH_RIGHT 55, LA_RUN_UP_STEP_RIGHT 56, LA_RUN_UP_STEP_LEFT 57, LA_WALK_UP_STEP_RIGHT 58, LA_WALK_UP_STEP_LEFT 59, LA_WALK_DOWN_LEFT 60, LA_WALK_DOWN_RIGHT 61, LA_WALK_DOWN_BACK_LEFT 62, LA_WALK_DOWN_BACK_RIGHT 63, LA_WALL_SWITCH_DOWN 64, LA_WALL_SWITCH_UP 65, LA_SIDE_STEP_LEFT 66, LA_SIDE_STEP_LEFT_END 67, LA_SIDE_STEP_RIGHT 68, LA_SIDE_STEP_RIGHT_END 69, LA_ROTATE_LEFT 70, LA_SLIDE_FORWARD 71, LA_SLIDE_FORWARD_END 72, LA_SLIDE_FORWARD_STOP 73, LA_STAND_TO_JUMP 74, LA_JUMP_BACK_START 75, LA_JUMP_BACK 76, LA_JUMP_FORWARD_START 77, LA_JUMP_FORWARD 78, LA_JUMP_LEFT_START 79, LA_JUMP_LEFT 80, LA_JUMP_RIGHT_START 81, LA_JUMP_RIGHT 82, LA_LAND 83, LA_JUMP_BACK_TO_FREEFALL 84, LA_JUMP_LEFT_TO_FREEFALL 85, LA_JUMP_RIGHT_TO_FREEFALL 86, LA_UNDERWATER_SWIM_FORWARD 87, LA_UNDERWATER_SWIM_FORWARD_DRIFT 88, LA_SMALL_JUMP_BACK_START 89, LA_SMALL_JUMP_BACK 90, LA_SMALL_JUMP_BACK_END 91, LA_JUMP_UP_START 92, LA_LAND_TO_RUN 93, LA_FALL_BACK 94, LA_JUMP_FORWARD_TO_REACH 95, LA_REACH 96, LA_REACH_TO_HANG 97, LA_CLIMB_ON 98, LA_REACH_TO_FREEFALL_2 99, LA_FALL_CROUCHING_LANDING 100, LA_JUMP_FORWARD_TO_REACH_LATE 101, LA_JUMP_FORWARD_START_TO_REACH_ALTERNATE 102, LA_CLIMB_ON_END 103, LA_STAND_IDLE 104, LA_SLIDE_BACKWARD_START 105, LA_SLIDE_BACKWARD 106, LA_SLIDE_BACKWARD_END 107, LA_UNDERWATER_SWIM_TO_IDLE 108, LA_UNDERWATER_IDLE 109, LA_UNDERWARER_IDLE_TO_SWIM 110, LA_ONWATER_IDLE 111, LA_ONWATER_TO_STAND_HIGH 112, LA_FREEFALL_TO_UNDERWATER 113, LA_ONWATER_DIVE_ALTERNATE 114, LA_UNDERWATER_TO_ONWATER 115, LA_ONWATER_SWIM_FORWARD_DIVE 116, LA_ONWATER_SWIM_FORWARD 117, LA_ONWATER_SWIM_FORWARD_TO_IDLE 118, LA_ONWATER_IDLE_TO_SWIM_FORWARD 119, LA_ONWATER_DIVE 120, LA_PUSHABLE_GRAB 121, LA_PUSHABLE_RELEASE 122, LA_PUSHABLE_PULL 123, LA_PUSHABLE_PUSH 124, LA_UNDERWATER_DEATH 125, LA_HIT_FRONT 126, LA_HIT_BACK 127, LA_HIT_LEFT 128, LA_HIT_RIGHT 129, LA_UNDERWATER_SWITCH 130, LA_UNDERWATER_PICKUP 131, LA_USE_KEY 132, LA_ONWATER_DEATH 133, LA_RUN_DEATH 134, LA_USE_PUZZLE 135, LA_PICKUP 136, LA_SHIMMY_LEFT 137, LA_SHIMMY_RIGHT 138, LA_STAND_DEATH 139, LA_BOULDER_DEATH 140, LA_ONWATER_IDLE_TO_SWIM_BACK 141, LA_ONWATER_SWIM_BACK 142, LA_ONWATER_SWIM_BACK_TO_IDLE 143, LA_ONWATER_SWIM_LEFT 144, LA_ONWATER_SWIM_RIGHT 145, LA_DEATH_JUMP 146, LA_ROLL_START 147, LA_ROLL_CONTINUE 148, LA_ROLL_END 149, LA_SPIKE_DEATH 150, LA_SWING_IN_FAST 151, LA_SWANDIVE_ROLL 152, LA_SWANDIVE_TO_UNDERWATER 153, LA_FREEFALL_SWANDIVE 154, LA_FREEFALL_SWANDIVE_TO_UNDERWATER 155, LA_SWANDIVE_DEATH 156, LA_SWANDIVE_LEFT 157, LA_SWANDIVE_RIGHT 158, LA_SWANDIVE_START 159, LA_CLIMB_ON_HANDSTAND 160, LA_RUN_JUMP_ROLL_START 161, LA_SOMERSAULT 162, LA_RUN_JUMP_ROLL_END 163, LA_JUMP_FORWARD_ROLL_START 164, LA_JUMP_FORWARD_ROLL_END 165, LA_JUMP_BACK_ROLL_START 166, LA_JUMP_BACK_ROLL_END 167, LA_UNDERWATER_ROLL_START 168, LA_UNDERWATER_ROLL_END 169, LA_ONWATER_TO_STAND_MEDIUM 170, LA_WADE 171, LA_RUN_TO_WADE_LEFT 172, LA_RUN_TO_WADE_RIGHT 173, LA_WADE_TO_RUN_LEFT 174, LA_WADE_TO_RUN_RIGHT 175, LA_WADE_TO_STAND_RIGHT 176, LA_WADE_TO_STAND_LEFT 177, LA_STAND_TO_WADE 178, LA_ONWATER_TO_WADE 179, LA_ONWATER_TO_WADE_LOW 180, LA_UNDERWATER_TO_STAND 181, LA_UNDERWATER_SWIM_TO_STILL_HUDDLE 182, LA_UNDERWATER_SWIM_TO_STILL_SPRAWL 183, LA_UNDERWATER_SWIM_TO_STILL_MEDIUM 184, LA_SLIDE_FORWARD_TO_RUN 185, LA_JUMP_NEUTRAL_ROLL 186, LA_CONTROLLED_DROP 187, LA_CONTROLLED_DROP_CONTINUE 188, LA_HANG_TO_JUMP_UP 189, LA_HANG_TO_JUMP_UP_CONTINUE 190, LA_HANG_TO_JUMP_BACK 191, LA_HANG_TO_JUMP_BACK_CONTINUE 192, LA_SPRINT 193, LA_RUN_TO_SPRINT_LEFT 194, LA_RUN_TO_SPRINT_RIGHT 195, LA_SPRINT_SLIDE_STAND_LEFT 196, LA_SPRINT_SLIDE_STAND_RIGHT 197, LA_SPRINT_TO_ROLL_LEFT 198, LA_SPRINT_ROLL_LEFT_TO_RUN 199, LA_SPRINT_TO_ROLL_RIGHT 200, LA_SPRINT_ROLL_RIGHT_TO_RUN 201, LA_SPRINT_TO_RUN_LEFT 202, LA_SPRINT_TO_RUN_RIGHT 203, LA_POSE_RIGHT_START 204, LA_POSE_RIGHT_CONTINUE 205, LA_POSE_RIGHT_END 206, LA_POSE_LEFT_START 207, LA_POSE_LEFT_CONTINUE 208, LA_POSE_LEFT_END 209, LA_STAND_TO_LADDER 210, LA_LADDER_UP 211, LA_LADDER_UP_STOP_RIGHT 212, LA_LADDER_UP_STOP_LEFT 213, LA_LADDER_IDLE 214, LA_LADDER_UP_START 215, LA_LADDER_DOWN_STOP_LEFT 216, LA_LADDER_DOWN_STOP_RIGHT 217, LA_LADDER_DOWN 218, LA_LADDER_DOWN_START 219, LA_LADDER_RIGHT 220, LA_LADDER_LEFT 221, LA_LADDER_HANG 222, LA_LADDER_HANG_TO_IDLE 223, LA_LADDER_CLIMB_ON 224, LA_LADDER_BACKFLIP_START 225, LA_LADDER_BACKFLIP_CONTINUE 226, LA_LADDER_UP_HANGING 227, LA_LADDER_DOWN_HANGING 228, LA_LADDER_TO_HANG_DOWN 229, LA_LADDER_TO_HANG_RIGHT 230, LA_LADDER_TO_HANG_LEFT 231, LA_UNKNOWN 232, LA_ONWATER_TO_WADE_SHALLOW_UNUSED 233, LA_FLARE_THROW 234, LA_SWITCH_SMALL_DOWN 235, LA_SWITCH_SMALL_UP 236, LA_BUTTON_PUSH 237, LA_FLARE_PICKUP 238, LA_UNDERWATER_FLARE_PICKUP 239, LA_KICK 240, LA_ZIPLINE_GRAB 241, LA_ZIPLINE_RIDE 242, LA_ZIPLINE_FALL 243, LA_STAND_TO_CROUCH 244, LA_STAND_TO_CROUCH_END 245, LA_STAND_TO_CROUCH_ABORT_UNUSED 246, LA_RUN_TO_CROUCH_LEFT_START 247, LA_RUN_TO_CROUCH_LEFT_END 248, LA_RUN_TO_CROUCH_RIGHT_START 249, LA_RUN_TO_CROUCH_RIGHT_END 250, LA_SPRINT_TO_CROUCH_LEFT 251, LA_SPRINT_TO_CROUCH_RIGHT 252, LA_HANG_TO_CROUCH_START 253, LA_HANG_TO_CROUCH_END 254, LA_CROUCH_IDLE 255, LA_CROUCH_TO_STAND 256, LA_CROUCH_PICKUP 257, LA_CROUCH_PICKUP_FLARE 258, LA_CROUCH_HIT_FRONT 259, LA_CROUCH_HIT_BACK 260, LA_CROUCH_HIT_RIGHT 261, LA_CROUCH_HIT_LEFT 262, LA_CROUCH_ROLL_FORWARD_START 263, LA_CROUCH_ROLL_FORWARD_CONTINUE 264, LA_CROUCH_ROLL_FORWARD_END 265, LA_CROUCH_ROLL_FORWARD_START_ALTERNATE_UNUSED 266, LA_CROUCH_TO_CRAWL_START 267, LA_CROUCH_TO_CRAWL_CONTINUE 268, LA_CROUCH_TO_CRAWL_END 269, LA_CRAWL_IDLE 270, LA_CRAWL_TO_CROUCH_START 271, LA_CRAWL_TO_CROUCH_CONTINUE 272, LA_CRAWL_TO_CROUCH_END_UNUSED 273, LA_CRAWL_IDLE_TO_FORWARD 274, LA_CRAWL_FORWARD 275, LA_CRAWL_FORWARD_TO_IDLE_START_RIGHT 276, LA_CRAWL_FORWARD_TO_IDLE_END_RIGHT 277, LA_CRAWL_FORWARD_TO_IDLE_START_LEFT 278, LA_CRAWL_FORWARD_TO_IDLE_END_LEFT 279, LA_CRAWL_TURN_LEFT 280, LA_CRAWL_TURN_RIGHT 281, LA_CRAWL_IDLE_TO_BACKWARD 282, LA_CRAWL_BACKWARD 283, LA_CRAWL_BACKWARD_TO_IDLE_RIGHT_START 284, LA_CRAWL_BACKWARD_TO_IDLE_RIGHT_END 285, LA_CRAWL_BACKWARD_TO_IDLE_LEFT_START 286, LA_CRAWL_BACKWARD_TO_IDLE_LEFT_END 287, LA_CRAWL_TURN_LEFT_EARLY_END 288, LA_CRAWL_TURN_RIGHT_EARLY_END 289, LA_CRAWL_TO_HANG_START 290, LA_CRAWL_TO_HANG_CONTINUE 291, LA_CRAWL_TO_HANG_END 292, LA_CRAWL_PICKUP 293, LA_CRAWL_HIT_FRONT_UNUSED 294, LA_CRAWL_HIT_BACK_UNUSED 295, LA_CRAWL_HIT_RIGHT_UNUSED 296, LA_CRAWL_HIT_LEFT_UNUSED 297, LA_CRAWL_DEATH 298, LA_CRAWL_JUMP_DOWN 299, LA_CROUCH_TURN_LEFT 300, LA_CROUCH_TURN_RIGHT 301, LA_JUMP_FORWARD_START_TO_GRAB_EARLY 302, LA_JUMP_FORWARD_START_TO_GRAB_LATE 303, LA_RUN_TO_GRAB_RIGHT 304, LA_RUN_TO_GRAB_LEFT 305, LA_SWING_IN_SLOW 306, LA_MONKEY_IDLE 307, LA_MONKEY_FALL 308, LA_MONKEY_GRAB 309, LA_MONKEY_FORWARD 310, LA_MONKEY_STOP_LEFT 311, LA_MONKEY_STOP_RIGHT 312, LA_MONKEY_IDLE_TO_FORWARD_LEFT 313, LA_MONKEY_IDLE_TO_FORWARD_RIGHT 314, LA_MONKEY_SHIMMY_LEFT 315, LA_MONKEY_SHIMMY_LEFT_END 316, LA_MONKEY_SHIMMY_RIGHT 317, LA_MONKEY_SHIMMY_RIGHT_END 318, LA_MONKEY_TURN_AROUND 319, LA_MONKEY_TURN_LEFT 320, LA_MONKEY_TURN_RIGHT 321, LA_MONKEY_TURN_LEFT_EARLY_END 322, LA_MONKEY_TURN_LEFT_LATE_END 323, LA_MONKEY_TURN_RIGHT_EARLY_END 324, LA_MONKEY_TURN_RIGHT_LATE_END 325, LA_SPRINT_SLIDE_STAND_RIGHT_END_ALTERNATE_UNUSED 326, LA_SPRINT_SLIDE_STAND_LEFT_END_ALTERNATE_UNUSED 327, LA_SPRINT_TO_ROLL_LEFT_BETA_UNUSED 328, LA_SPRINT_TO_ROLL_ALTERNATE_START_UNUSED 329, LA_SPRINT_TO_ROLL_ALTERNATE_CONTINUE_UNUSED 330, LA_SPRINT_TO_ROLL_ALTERNATE_END_UNUSED 331, LA_LADDER_TO_CROUCH_START 332, LA_LADDER_TO_CROUCH_END ================================================ FILE: data/trx/ship/games/tr1/catalog_lara_states.csv ================================================ 0, LS_WALK 1, LS_RUN 2, LS_STOP 3, LS_JUMP_FORWARD 4, LS_POSE 5, LS_FAST_BACK 6, LS_TURN_RIGHT 7, LS_TURN_LEFT 8, LS_DEATH 9, LS_FAST_FALL 10, LS_HANG 11, LS_REACH 12, LS_SPLAT 13, LS_TREAD 14, LS_LAND 15, LS_COMPRESS 16, LS_WALK_BACK 17, LS_SWIM 18, LS_GLIDE 19, LS_PULL_UP 20, LS_FAST_TURN 21, LS_STEP_RIGHT 22, LS_STEP_LEFT 23, LS_ROLL_CONT 24, LS_SLIDE 25, LS_JUMP_BACK 26, LS_JUMP_RIGHT 27, LS_JUMP_LEFT 28, LS_JUMP_UP 29, LS_FALL_BACK 30, LS_SHIMMY_LEFT 31, LS_SHIMMY_RIGHT 32, LS_SLIDE_BACK 33, LS_SURF_TREAD 34, LS_SURF_SWIM 35, LS_DIVE 36, LS_PUSH_BLOCK 37, LS_PULL_BLOCK 38, LS_PP_READY 39, LS_PICKUP 40, LS_SWITCH_ON 41, LS_SWITCH_OFF 42, LS_USE_KEY 43, LS_USE_PUZZLE 44, LS_UW_DEATH 45, LS_ROLL 46, LS_SPECIAL 47, LS_SURF_BACK 48, LS_SURF_LEFT 49, LS_SURF_RIGHT 50, LS_USE_MIDAS 51, LS_DIE_MIDAS 52, LS_SWAN_DIVE 53, LS_FAST_DIVE 54, LS_GYMNAST 55, LS_WATER_OUT 56, LS_CONTROLLED 57, LS_TWIST 58, LS_WATER_ROLL 59, LS_WADE 60, LS_RESPONSIVE 61, LS_NEUTRAL_ROLL 62, LS_SPRINT 63, LS_SPRINT_ROLL 64, LS_POSE_START 65, LS_POSE_END 66, LS_POSE_LEFT 67, LS_POSE_RIGHT 68, LS_CLIMB_STANCE 69, LS_CLIMBING 70, LS_CLIMB_LEFT 71, LS_CLIMB_END 72, LS_CLIMB_RIGHT 73, LS_CLIMB_DOWN 74, LS_LARA_TEST1 75, LS_LARA_TEST2 76, LS_LARA_TEST3 77, LS_FLARE_PICKUP 78, LS_KICK 79, LS_ZIPLINE 80, LS_CROUCH_IDLE 81, LS_CROUCH_ROLL 82, LS_CRAWL_IDLE 83, LS_CRAWL_FORWARD 84, LS_CRAWL_TURN_LEFT 85, LS_CRAWL_TURN_RIGHT 86, LS_CRAWL_BACK 87, LS_CLIMB_TO_CRAWL 88, LS_CRAWL_TO_CLIMB 89, LS_CRAWL_JUMP_DOWN 90, LS_CROUCH_TURN_LEFT 91, LS_CROUCH_TURN_RIGHT 92, LS_MONKEY_IDLE 93, LS_MONKEY_FORWARD 94, LS_MONKEY_LEFT 95, LS_MONKEY_RIGHT 96, LS_MONKEY_ROLL 97, LS_MONKEY_TURN_LEFT 98, LS_MONKEY_TURN_RIGHT ================================================ FILE: data/trx/ship/games/tr1/catalog_music.csv ================================================ 0, MX_UNUSED_0 1, MX_UNUSED_1 5, MX_UNUSED_2 13, MX_SECRET 28, MX_TR1_GYM_HINT_03 29, MX_TR1_GYM_HINT_04 37, MX_TR1_GYM_HINT_12 39, MX_TR1_GYM_HINT_14 40, MX_TR1_GYM_HINT_15 41, MX_TR1_GYM_HINT_16 42, MX_TR1_GYM_HINT_17 43, MX_TR1_GYM_HINT_18 49, MX_TR1_GYM_HINT_24 50, MX_TR1_GYM_HINT_25 51, MX_BALDY_SPEECH 52, MX_COWBOY_SPEECH 53, MX_LARSON_SPEECH 54, MX_NATLA_SPEECH 55, MX_PIERRE_SPEECH 56, MX_SKATEKID_SPEECH ================================================ FILE: data/trx/ship/games/tr1/catalog_objects.csv ================================================ 0, O_LARA 1, O_LARA_PISTOLS 2, O_LARA_SHOTGUN 3, O_LARA_MAGNUMS 4, O_LARA_UZIS 5, O_LARA_EXTRA 6, O_BACON_LARA 7, O_WOLF 8, O_BEAR 9, O_BAT 10, O_CROCODILE 11, O_ALLIGATOR 12, O_LION 13, O_LIONESS 14, O_PUMA 15, O_APE 16, O_RAT 17, O_VOLE 18, O_TREX 19, O_RAPTOR 20, O_ATLANTEAN_WINGED 21, O_ATLANTEAN_SHOOTER 22, O_ATLANTEAN_GROUND 23, O_CENTAUR 24, O_MUMMY 25, O_DINO_WARRIOR 26, O_FISH 27, O_LARSON 28, O_PIERRE 29, O_SKATEBOARD 30, O_SKATEKID 31, O_COWBOY 32, O_BALDY 33, O_NATLA 34, O_TORSO 35, O_FALLING_BLOCK_1 36, O_SWINGING_AXE 37, O_SPIKES 38, O_ROLLING_BALL_1 39, O_DART 40, O_DART_EMITTER 41, O_DRAWBRIDGE 42, O_TEETH_TRAP 43, O_DAMOCLES_SWORD 44, O_THORS_HANDLE 45, O_THORS_HEAD 46, O_LIGHTNING_EMITTER 47, O_MOVING_BAR 48, O_MOVABLE_BLOCK_1 49, O_MOVABLE_BLOCK_2 50, O_MOVABLE_BLOCK_3 51, O_MOVABLE_BLOCK_4 52, O_SLIDING_PILLAR 53, O_FALLING_CEILING_1 54, O_FALLING_CEILING_2 55, O_SWITCH_TYPE_NORMAL 56, O_SWITCH_TYPE_UW 57, O_DOOR_TYPE_1 58, O_DOOR_TYPE_2 59, O_DOOR_TYPE_3 60, O_DOOR_TYPE_4 61, O_DOOR_TYPE_5 62, O_DOOR_TYPE_6 63, O_DOOR_TYPE_7 64, O_DOOR_TYPE_8 65, O_TRAPDOOR_TYPE_1 66, O_TRAPDOOR_TYPE_2 67, O_TRAPDOOR_TYPE_3 68, O_BRIDGE_FLAT 69, O_BRIDGE_TILT_1 70, O_BRIDGE_TILT_2 71, O_PASSPORT_OPTION 72, O_COMPASS_OPTION 73, O_PHOTO_OPTION 74, O_COG_1 75, O_COG_2 76, O_COG_3 77, O_PLAYER_1 78, O_PLAYER_2 79, O_PLAYER_3 80, O_PLAYER_4 81, O_PASSPORT_CLOSED 82, O_PDA_OPTION 83, O_SAVE_CRYSTAL_ITEM 84, O_PISTOL_ITEM 85, O_SHOTGUN_ITEM 86, O_MAGNUM_ITEM 87, O_UZI_ITEM 88, O_PISTOL_AMMO_ITEM 89, O_SHOTGUN_AMMO_ITEM 90, O_MAGNUM_AMMO_ITEM 91, O_UZI_AMMO_ITEM 92, O_EXPLOSIVE_ITEM 93, O_SMALL_MEDIPACK_ITEM 94, O_LARGE_MEDIPACK_ITEM 95, O_DETAIL_OPTION 96, O_SOUND_OPTION 97, O_CONTROL_OPTION 98, O_GAMMA_OPTION 99, O_PISTOL_OPTION 100, O_SHOTGUN_OPTION 101, O_MAGNUM_OPTION 102, O_UZI_OPTION 103, O_PISTOL_AMMO_OPTION 104, O_SHOTGUN_AMMO_OPTION 105, O_MAGNUM_AMMO_OPTION 106, O_UZI_AMMO_OPTION 107, O_EXPLOSIVE_OPTION 108, O_SMALL_MEDIPACK_OPTION 109, O_LARGE_MEDIPACK_OPTION 110, O_PUZZLE_ITEM_1 111, O_PUZZLE_ITEM_2 112, O_PUZZLE_ITEM_3 113, O_PUZZLE_ITEM_4 114, O_PUZZLE_OPTION_1 115, O_PUZZLE_OPTION_2 116, O_PUZZLE_OPTION_3 117, O_PUZZLE_OPTION_4 118, O_PUZZLE_HOLE_1 119, O_PUZZLE_HOLE_2 120, O_PUZZLE_HOLE_3 121, O_PUZZLE_HOLE_4 122, O_PUZZLE_DONE_1 123, O_PUZZLE_DONE_2 124, O_PUZZLE_DONE_3 125, O_PUZZLE_DONE_4 126, O_LEADBAR_ITEM 127, O_LEADBAR_OPTION 128, O_MIDAS_TOUCH 129, O_KEY_ITEM_1 130, O_KEY_ITEM_2 131, O_KEY_ITEM_3 132, O_KEY_ITEM_4 133, O_KEY_OPTION_1 134, O_KEY_OPTION_2 135, O_KEY_OPTION_3 136, O_KEY_OPTION_4 137, O_KEY_HOLE_1 138, O_KEY_HOLE_2 139, O_KEY_HOLE_3 140, O_KEY_HOLE_4 141, O_PICKUP_ITEM_1 142, O_PICKUP_ITEM_2 143, O_SCION_ITEM_1 144, O_SCION_ITEM_2 145, O_SCION_ITEM_3 146, O_SCION_ITEM_4 147, O_SCION_HOLDER 148, O_PICKUP_OPTION_1 149, O_PICKUP_OPTION_2 150, O_SCION_OPTION 151, O_EXPLOSION_1 152, O_EXPLOSION_2 153, O_SPLASH_1 154, O_SPLASH_2 155, O_BUBBLE_1 156, O_BUBBLE_2 157, O_BUBBLE_EMITTER 158, O_BLOOD 159, O_BLOOD_PINK 160, O_DART_EFFECT 161, O_CENTAUR_STATUE 162, O_PORTACABIN 163, O_PODS 164, O_RICOCHET 165, O_TWINKLE 166, O_GUN_FLASH 167, O_DUST 168, O_BODY_PART 169, O_CAMERA_TARGET 170, O_WATERFALL 171, O_NATLA_GUN 172, O_MISSILE_ATLANTEAN_SHARD 173, O_MISSILE_ATLANTEAN_BOMB 176, O_EMBER 177, O_EMBER_EMITTER 178, O_FLAME 179, O_FLAME_EMITTER 180, O_LAVA_WEDGE 181, O_BIG_POD 182, O_MOTOR_BOAT 183, O_EARTHQUAKE 184, O_SKYBOX 185, O_PICKUP_AID 186, O_GLOW 187, O_FLAREBOX_ITEM 188, O_FLAREBOX_OPTION 189, O_LARA_HAIR 190, O_ALPHABET 191, O_WINSTON 192, O_LARA_FLARE 193, O_FLARE_ITEM 194, O_FLARE_FIRE # Slots 195-200 moved for Lara skins: available for re-use 201, O_LARA_M16 202, O_LARA_GRENADE_GUN 203, O_LARA_HARPOON_GUN 204, O_M16_OPTION 205, O_GRENADE_GUN_OPTION 206, O_HARPOON_OPTION 207, O_M16_AMMO_OPTION 208, O_GRENADE_AMMO_OPTION 209, O_HARPOON_AMMO_OPTION 210, O_M16_FLASH 211, O_GRENADE 212, O_HARPOON_BOLT 213, O_LARA_AUTOS 214, O_AUTOS_OPTION 215, O_AUTOS_AMMO_OPTION 216, O_LARA_DESERT_EAGLE 217, O_DESERT_EAGLE_OPTION 218, O_DESERT_EAGLE_AMMO_OPTION 219, O_LARA_MP5, 220, O_MP5_OPTION 221, O_MP5_AMMO_OPTION 222, O_LARA_ROCKET_GUN 223, O_ROCKET_GUN_OPTION 224, O_ROCKET_AMMO_OPTION 225, O_ROCKET 241, O_M16_ITEM 242, O_GRENADE_GUN_ITEM 243, O_HARPOON_ITEM 244, O_M16_AMMO_ITEM 245, O_GRENADE_AMMO_ITEM 246, O_HARPOON_AMMO_ITEM 247, O_ALPHABET_SMALL 248, O_AUTOS_ITEM 249, O_AUTOS_AMMO_ITEM 250, O_SNOWFLAKE 251, O_DESERT_EAGLE_ITEM 252, O_DESERT_EAGLE_AMMO_ITEM 253, O_MP5_ITEM 254, O_MP5_AMMO_ITEM 255, O_ROCKET_GUN_ITEM 256, O_ROCKET_AMMO_ITEM 257, O_SHADOW 258, O_LARA_SKIN_SWAP_1 259, O_LARA_SKIN_SWAP_2 260, O_LARA_SKIN_SWAP_3 261, O_LARA_SKIN_SWAP_4 262, O_LARA_SKIN_SWAP_5 263, O_LARA_SKIN_SWAP_6 264, O_LARA_SKIN_SWAP_7 265, O_LARA_SKIN_SWAP_8 266, O_LARA_SKIN_SWAP_9 267, O_LARA_SKIN_SWAP_10 268, O_LARA_SKIN_SWAP_11 269, O_LARA_SKIN_SWAP_12 270, O_LARA_SKIN_SWAP_13 271, O_LARA_SKIN_SWAP_14 272, O_LARA_SKIN_SWAP_15 273, O_LARA_SKIN_SWAP_16 274, O_LARA_SKIN_SWAP_17 275, O_LARA_SKIN_SWAP_18 276, O_LARA_SKIN_SWAP_19 277, O_LARA_SKIN_SWAP_20 278, O_LARA_SKIN_SWAP_21 279, O_LARA_SKIN_SWAP_22 280, O_LARA_SKIN_SWAP_23 281, O_LARA_SKIN_SWAP_24 282, O_LARA_SKIN_SWAP_25 283, O_LARA_SKIN_SWAP_26 284, O_LARA_SKIN_SWAP_27 285, O_LARA_SKIN_SWAP_28 286, O_LARA_SKIN_SWAP_29 287, O_LARA_SKIN_SWAP_30 288, O_LARA_SKIN_SWAP_31 289, O_LARA_SKIN_SWAP_32 290, O_LARA_SKIN_SWAP_EXTRA 291, O_LARA_SKIN_SWAP_GUNS 292, O_LARA_SKIN_SWAP_LEGS ================================================ FILE: data/trx/ship/games/tr1/catalog_samples.csv ================================================ 0, SFX_LARA_FOOTSTEP 2, SFX_LARA_NO 6, SFX_LARA_DRAW 7, SFX_LARA_HOLSTER 8, SFX_LARA_PISTOLS 9, SFX_LARA_RELOAD 10, SFX_LARA_RICOCHET 12, SFX_BEAR_FEET 14, SFX_BEAR_SNARL 16, SFX_BEAR_HURT 20, SFX_WOLF_HURT 26, SFX_LARA_CLIMB_3 27, SFX_LARA_BODYSL 30, SFX_LARA_FALL 31, SFX_LARA_INJURY 33, SFX_LARA_SPLASH 36, SFX_LARA_BREATH 37, SFX_LARA_BUBBLES 39, SFX_LARA_KEY 41, SFX_LARA_GENERAL_DEATH 43, SFX_LARA_UZI_FIRE 44, SFX_LARA_MAGNUMS 45, SFX_LARA_SHOTGUN 48, SFX_LARA_EMPTY 50, SFX_LARA_BULLETHIT 53, SFX_LARA_FALL_DEATH 60, SFX_UNDERWATER 70, SFX_PUSHBLOCK_LAND 70, SFX_EARTHQUAKE_2 79, SFX_WATERFALL_LOOP 81, SFX_WATERFALL_BIG 81, SFX_FLOOD 85, SFX_LION_HURT 95, SFX_RAT_CHIRP 98, SFX_THUNDER 99, SFX_EXPLOSION_2 103, SFX_DAMOCLES_SWORD 104, SFX_EXPLOSION_1 108, SFX_MENU_ROTATE 109, SFX_MENU_LARA_HOME 110, SFX_MENU_GAMEBOY 111, SFX_MENU_CHOOSE 111, SFX_MENU_SPININ 112, SFX_MENU_SPINOUT 113, SFX_MENU_COMPASS 114, SFX_MENU_GUNS 115, SFX_MENU_PASSPORT 116, SFX_MENU_MEDI 117, SFX_RAISINGBLOCK_FX 118, SFX_SAND_FX 119, SFX_STAIRS_2_SLOPE_FX 123, SFX_ATLANTEAN_NEEDLE 132, SFX_SKATEBOARD_HIT 142, SFX_TORSO_HIT 147, SFX_ROLLING_BALL_1_ROLL 147, SFX_EARTHQUAKE_1 149, SFX_LAVA_FOUNTAIN 150, SFX_LOOP_FOR_SMALL_FIRES 151, SFX_DART 155, SFX_POWERUP_FX 161, SFX_TRAPDOOR_OPEN 170, SFX_EXPLOSION_FX 171, SFX_ATLANTEAN_DEATH 172, SFX_CHAINBLOCK_FX 173, SFX_SECRET 199, SFX_BALDY_SPEECH 200, SFX_COWBOY_SPEECH 201, SFX_LARSON_SPEECH 202, SFX_NATLA_SPEECH 203, SFX_PIERRE_SPEECH 204, SFX_SKATEKID_SPEECH 257, SFX_LARA_FLARE_IGNITE 258, SFX_LARA_FLARE_BURN 259, SFX_M16_FIRE 260, SFX_M16_STOP 268, SFX_LARA_AUTOS 269, SFX_LARA_DESERT_EAGLE 270, SFX_MP5_FIRE 271, SFX_ROCKET_FIRE 272, SFX_EXPLOSION_3 273, SFX_LARA_BAREFOOT # 274 used in animations for Lara knee shuffle 344, SFX_WINSTON_GRUNT_1 345, SFX_WINSTON_GRUNT_2 346, SFX_WINSTON_GRUNT_3 347, SFX_WINSTON_CUPS ================================================ FILE: data/trx/ship/games/tr1/gameflow.json5 ================================================ { // NOTE: bad changes to this file may result in crashes. // Lines starting with double slashes are comments and are ignored. "engine": 1, "name": "Tomb Raider I", "main_menu_picture": "title.webp", "savegame_file_fmt": "save_tr1_%02d.dat", "injections": [ "braid.bin", "bubbles.bin", "gun_glow.bin", "lara_animations.bin", "lara_guns.bin", "crystal.bin", "uzi_sfx.bin", "explosion.bin", "font.bin", "pickup_aid.bin", "sprite_alignment.bin", "pda_model.bin", "winston_model.bin", "lara_extra.bin", "misc_sprites.bin", "lara_outfits.bin", ], "enable_tr2_item_drops": false, "convert_dropped_guns": false, "title": { "path": "title.phd", "music_track": 2, "inherit_injections": false, "sequence": [ {"type": "display_picture", "path": "eidos.webp", "legal": true, "display_time": 1, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "play_fmv", "fmv_id": 0}, {"type": "play_fmv", "fmv_id": 1}, {"type": "play_fmv", "fmv_id": 2}, {"type": "exit_to_title"}, ], "injections": [ "pda_model.bin", "font.bin", "title_textures.bin", "misc_sprites.bin", ], }, "levels": [ // Level 0: Lara's Home { "path": "gym.phd", "script": "gym.lua", "type": "gym", "music_track": 0, "inherit_injections": false, "lara_outfit": "tr1_gym", "sequence": [ {"type": "play_fmv", "fmv_id": 3}, {"type": "loading_screen", "path": "gym.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "level_stats"}, {"type": "exit_to_title"}, ], "injections": [ "lara_gym_guns.bin", "braid.bin", "bubbles.bin", "gun_glow.bin", "gym_textures.bin", "lara_animations.bin", "uzi_sfx.bin", "explosion.bin", "pda_model.bin", "font.bin", "winston_model.bin", "misc_sprites.bin", "lara_feet_sfx.bin", "lara_outfits.bin", ], }, // Level 1: Caves { "path": "level1.phd", "music_track": 57, "lara_outfit": "tr1_classic", "sequence": [ {"type": "play_fmv", "fmv_id": 4}, {"type": "loading_screen", "path": "peru.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "caves_fd.bin", "caves_itemrots.bin", "caves_textures.bin", ], }, // Level 2: City of Vilcabamba { "path": "level2.phd", "music_track": 57, "lara_outfit": "tr1_classic", "sequence": [ {"type": "loading_screen", "path": "peru.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "vilcabamba_door_sfx.bin", "vilcabamba_itemrots.bin", "vilcabamba_textures.bin", ], }, // Level 3: Lost Valley { "path": "level3a.phd", "music_track": 57, "lara_outfit": "tr1_classic", "sequence": [ {"type": "loading_screen", "path": "peru.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "valley_fd.bin", "valley_itemrots.bin", "valley_skybox.bin", "valley_textures.bin", ], }, // Level 4: Tomb of Qualopec { "path": "level3b.phd", "music_track": 57, "lara_outfit": "tr1_classic", "sequence": [ {"type": "loading_screen", "path": "peru.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "play_cutscene", "cutscene_id": 0}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "qualopec_door_sfx.bin", "qualopec_fd.bin", "qualopec_itemrots.bin", "qualopec_textures.bin", ], }, // Level 5: St. Francis' Folly { "path": "level4.phd", "music_track": 59, "lara_outfit": "tr1_classic", "sequence": [ {"type": "play_fmv", "fmv_id": 5}, {"type": "loading_screen", "path": "greece.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "door59_frames.bin", "folly_fd.bin", "folly_itemrots.bin", "folly_pickup_meshes.bin", "folly_textures.bin", ], }, // Level 6: Colosseum { "path": "level5.phd", "music_track": 59, "lara_outfit": "tr1_classic", "sequence": [ {"type": "loading_screen", "path": "greece.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "colosseum_fd.bin", "colosseum_itemrots.bin", "colosseum_skybox.bin", "colosseum_textures.bin", "door58_frames.bin", ], }, // Level 7: Palace Midas { "path": "level6.phd", "music_track": 59, "lara_outfit": "tr1_classic", "sequence": [ {"type": "loading_screen", "path": "greece.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "midas_itemrots.bin", "midas_textures.bin", ], "unobtainable_pickups": 1, }, // Level 8: The Cistern { "path": "level7a.phd", "music_track": 58, "lara_outfit": "tr1_classic", "sequence": [ {"type": "loading_screen", "path": "greece.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "cistern_fd.bin", "cistern_itemrots.bin", "cistern_plants.bin", "cistern_skybox.bin", "cistern_textures.bin", ], }, // Level 9: Tomb of Tihocan { "path": "level7b.phd", "music_track": 58, "lara_outfit": "tr1_classic", "sequence": [ {"type": "loading_screen", "path": "greece.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "play_cutscene", "cutscene_id": 1}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "cistern_plants.bin", "tihocan_skybox.bin", "door60_frames.bin", "tihocan_fd.bin", "tihocan_itemrots.bin", "tihocan_textures.bin", ], "item_drops": [ {"enemy_num": 82, "object_ids": [86, 144, 129]}, ], }, // Level 10: City of Khamoon { "path": "level8a.phd", "music_track": 59, "lara_outfit": "tr1_classic", "sequence": [ {"type": "play_fmv", "fmv_id": 6}, {"type": "loading_screen", "path": "egypt.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "khamoon_fd.bin", "khamoon_itemrots.bin", "khamoon_meshfixes.bin", "khamoon_mummy.bin", "khamoon_textures.bin", "panther_sfx.bin", ], }, // Level 11: Obelisk of Khamoon { "path": "level8b.phd", "music_track": 59, "lara_outfit": "tr1_classic", "sequence": [ {"type": "loading_screen", "path": "egypt.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "obelisk_fd.bin", "obelisk_itemrots.bin", "obelisk_meshfixes.bin", "obelisk_skybox.bin", "obelisk_textures.bin", "panther_sfx.bin", ], }, // Level 12: Sanctuary of the Scion { "path": "level8c.phd", "music_track": 59, "lara_outfit": "tr1_classic", "sequence": [ {"type": "loading_screen", "path": "egypt.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "door59_frames.bin", "door59_sfx.bin", "sanctuary_fd.bin", "sanctuary_itemrots.bin", "sanctuary_scion.bin", "sanctuary_textures.bin", ], }, // Level 13: Natla's Mines { "path": "level10a.phd", "music_track": 58, "lara_outfit": "tr1_classic", "sequence": [ {"type": "play_fmv", "fmv_id": 7}, {"type": "loading_screen", "path": "atlantis.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "remove_weapons"}, {"type": "remove_scions"}, {"type": "loop_game"}, {"type": "level_stats"}, {"type": "play_cutscene", "cutscene_id": 2}, {"type": "level_complete"}, ], "injections": [ "mines_cameras.bin", "mines_door_sfx.bin", "mines_fd.bin", "mines_itemrots.bin", "mines_meshfixes.bin", "mines_pushblocks.bin", "mines_textures.bin", "skate_kid_sfx.bin", ], "item_drops": [ {"enemy_num": 17, "object_ids": [86]}, {"enemy_num": 50, "object_ids": [87]}, {"enemy_num": 75, "object_ids": [85]}, ], }, // Level 14: Atlantis { "path": "level10b.phd", "music_track": 60, "lara_outfit": "tr1_classic", "sequence": [ {"type": "play_fmv", "fmv_id": 8}, {"type": "loading_screen", "path": "atlantis.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "give_item", "object_id": "pistols", "quantity": 1}, {"type": "setup_bacon_lara", "anchor_room": 10}, {"type": "loop_game"}, {"type": "play_fmv", "fmv_id": 9}, {"type": "play_cutscene", "cutscene_id": 3}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "atlantis_door_sfx.bin", "atlantis_fd.bin", "atlantis_itemrots.bin", "atlantis_textures.bin", ], "unobtainable_pickups": 3, }, // Level 15: The Great Pyramid { "path": "level10c.phd", "music_track": 60, "lara_outfit": "tr1_classic", "sequence": [ {"type": "loading_screen", "path": "atlantis.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "level_stats"}, {"type": "play_fmv", "fmv_id": 10}, {"type": "play_music", "music_track": 19}, {"type": "level_complete"}, {"type": "display_picture", "credit": true, "path": "end.webp", "display_time": 7.5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credits_1.webp", "display_time": 7.5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credits_2.webp", "display_time": 7.5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credits_3.webp", "display_time": 7.5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "total_stats", "background_path": "install.webp"}, ], "injections": [ "atlantis_door_sfx.bin", "pyramid_fd.bin", "pyramid_itemrots.bin", "pyramid_textures.bin", "scion_collision.bin", ], }, // Level 16-20: Legacy savegame placeholders {"type": "dummy"}, {"type": "dummy"}, {"type": "dummy"}, {"type": "dummy"}, {"type": "dummy"}, // Level 21: Current Position // This level is necessary to read TombATI's save files! // OG has a special level called LV_CURRENT to handle save/load logic. // TRX does away without this hack. However, the existing save games // expect the level count to match, otherwise the game will crash. // Hence this dummy level. { "path": "current.phd", "type": "current", "music_track": 0, "inherit_injections": false, "sequence": [ {"type": "exit_to_title"}, ], }, ], "demos": [ // Demo 1: City of Vilcabamba { "path": "level2.phd", "music_track": 57, "lara_outfit": "tr1_classic", "injections": [ "vilcabamba_door_sfx.bin", "vilcabamba_itemrots.bin", "vilcabamba_textures.bin", ], "sequence": [ {"type": "loading_screen", "path": "peru.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "level_complete"}, ], }, // Demo 2: Lost Valley { "path": "level3a.phd", "music_track": 57, "lara_outfit": "tr1_classic", "injections": [ "valley_itemrots.bin", "valley_skybox.bin", "valley_textures.bin", ], "sequence": [ {"type": "loading_screen", "path": "peru.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "level_complete"}, ], }, ], "cutscenes": [ // Cutscene 1 { "path": "cut1.phd", "music_track": 23, "lara_outfit": "tr1_classic", "inherit_injections": false, "injections": [ "cut1_setup.bin", "braid.bin", "photo.bin", "pda_model.bin", "font.bin", "lara_outfits.bin", "misc_sprites.bin", ], "sequence": [ {"type": "loop_game"}, ], }, // Cutscene 2 { "path": "cut2.phd", "music_track": 25, "lara_outfit": "tr1_classic", "inherit_injections": false, "injections": [ "cut2_setup.bin", "braid.bin", "photo.bin", "pda_model.bin", "font.bin", "lara_outfits.bin", "misc_sprites.bin", ], "sequence": [ {"type": "loop_game"}, ], }, // Cutscene 3 { "path": "cut3.phd", "music_track": 24, "lara_outfit": "tr1_classic", "inherit_injections": false, "injections": [ "cut3_setup.bin", "cut3_textures.bin", "photo.bin", "pda_model.bin", "font.bin", "lara_outfits.bin", "misc_sprites.bin", ], "sequence": [ {"type": "loop_game"}, ], }, // Cutscene 4 { "path": "cut4.phd", "music_track": 22, "lara_outfit": "tr1_classic", "fog_start": 12.0, "fog_end": 18.0, "inherit_injections": false, "injections": [ "cut4_setup.bin", "braid.bin", "cut4_textures.bin", "photo.bin", "pda_model.bin", "font.bin", "lara_outfits.bin", "misc_sprites.bin", ], "sequence": [ {"type": "loop_game"}, ], }, ], // FMVs "fmvs": [ {"path": "core.avi", "legal": true}, {"path": "escape.avi", "legal": true}, {"path": "cafe.avi"}, {"path": "mansion.avi"}, {"path": "snow.avi"}, {"path": "lift.avi"}, {"path": "vision.avi"}, {"path": "canyon.avi"}, {"path": "pyramid.avi"}, {"path": "prison.avi"}, {"path": "end.avi"}, ], "hidden_config": [ "enable_item_examining", // TR1 has no special item descriptions "healthbar_poison_color", "healthbar_poison_color_ps1", "enemy_healthbar_color_allies", "enemy_healthbar_color_allies_ps1", "exposurebar_color", "exposurebar_color_ps1", "exposurebar_location", "exposurebar_show_mode", "enable_ally_targeting", "enable_weather", "enable_footprints", "ally_hostility_policy", "fix_monkey_pickup_priority", "fix_pipeman_aim", "enable_cinematics", "enable_body_bags", ], } ================================================ FILE: data/trx/ship/games/tr1/inv_ring.json5 ================================================ [ { "object_id": "O_SMALL_MEDIPACK_OPTION", "frames_total": 26, "open_frame": 25, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 4032, "x_rot_sel": -7296, "y_rot_sel": -4096, "z_trans_sel": 216, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 14, }, { "object_id": "O_LARGE_MEDIPACK_OPTION", "frames_total": 20, "open_frame": 19, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3616, "x_rot_sel": -8160, "y_rot_sel": -4096, "z_trans_sel": 352, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 13, }, { "object_id": "O_FLAREBOX_OPTION", "frames_total": 31, "open_frame": 30, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "y_rot_sel": -8192, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 12, }, { "object_id": "O_PISTOL_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 1, }, { "object_id": "O_SHOTGUN_OPTION", "frames_total": 13, "open_frame": 12, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "y_rot_sel": -8192, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 2, }, { "object_id": "O_MAGNUM_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 3, }, { "object_id": "O_AUTOS_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 4, }, { "object_id": "O_DESERT_EAGLE_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": 3360, "y_rot_sel": -32768, "z_trans_sel": 362, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 5, }, { "object_id": "O_UZI_OPTION", "frames_total": 13, "open_frame": 12, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 6, }, { "object_id": "O_HARPOON_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -736, "y_rot_sel": -19456, "y_trans_sel": 58, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 11, }, { "object_id": "O_M16_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -224, "y_rot_sel": -18432, "y_trans_sel": 84, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 7, }, { "object_id": "O_MP5_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -224, "y_rot_sel": -18432, "y_trans_sel": 84, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 8, }, { "object_id": "O_ROCKET_GUN_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -224, "y_rot_sel": 14336, "y_trans_sel": 56, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 9, }, { "object_id": "O_GRENADE_GUN_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -224, "y_rot_sel": 14336, "y_trans_sel": 56, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 10, }, { "object_id": "O_PISTOL_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 1, }, { "object_id": "O_SHOTGUN_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 2, }, { "object_id": "O_MAGNUM_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 3, }, { "object_id": "O_AUTOS_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 4, }, { "object_id": "O_DESERT_EAGLE_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 5, }, { "object_id": "O_UZI_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 6, }, { "object_id": "O_HARPOON_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 11, }, { "object_id": "O_M16_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 7, }, { "object_id": "O_MP5_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 8, }, { "object_id": "O_ROCKET_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 9, }, { "object_id": "O_GRENADE_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 10, }, { "object_id": "O_SCION_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 109, }, { "object_id": "O_LEADBAR_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3616, "x_rot_sel": -8160, "y_rot_sel": -4096, "z_trans_sel": 352, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 100, }, { "object_id": "O_PICKUP_OPTION_1", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 111, }, { "object_id": "O_PICKUP_OPTION_2", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 110, }, { "object_id": "O_PUZZLE_OPTION_1", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 108, }, { "object_id": "O_PUZZLE_OPTION_2", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 107, }, { "object_id": "O_PUZZLE_OPTION_3", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 106, }, { "object_id": "O_PUZZLE_OPTION_4", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 105, }, { "object_id": "O_KEY_OPTION_1", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 101, }, { "object_id": "O_KEY_OPTION_2", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 102, }, { "object_id": "O_KEY_OPTION_3", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 103, }, { "object_id": "O_KEY_OPTION_4", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 104, }, { "object_id": "O_STOPWATCH_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 4352, "x_rot_sel": -1536, "y_trans_sel": -170, "z_trans_sel": 320, "meshes_sel": -1, "meshes_drawn": -1, }, { "object_id": "O_COMPASS_OPTION", "frames_total": 25, "open_frame": 10, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 4352, "x_rot_sel": -8192, "z_trans_sel": 456, "meshes_sel": 0b00000101, "meshes_drawn": 0b00000101, }, { "object_id": "O_PASSPORT_OPTION", "frames_total": 30, "open_frame": 14, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 4640, "x_rot_sel": -4320, "z_trans_sel": 384, "meshes_sel": 0b00010011, "meshes_drawn": 0b00010011, "inv_pos": 200, }, { "object_id": "O_DETAIL_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 4224, "x_rot_sel": -6720, "z_trans_sel": 424, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 201, }, { "object_id": "O_SOUND_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 4832, "x_rot_sel": -2336, "z_trans_sel": 368, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 202, }, { "object_id": "O_CONTROL_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 5504, "x_rot_sel": 1536, "z_trans_sel": 352, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 203, }, { "object_id": "O_PHOTO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 4640, "x_rot_sel": -4320, "z_trans_sel": 384, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 205, }, { "object_id": "O_PDA_OPTION", "frames_total": 39, "open_frame": 19, "anim_direction": 1, "x_rot_pt_sel": 4640, "z_trans_sel": 384, "meshes_sel": 0b00000011, "meshes_drawn": 0b00000011, "inv_pos": 204, }, ] ================================================ FILE: data/trx/ship/games/tr1/scripts/gym.lua ================================================ trx.events.on_game_start(function(level, is_save) trx.lara.holsters_visible = trx.lara.has_pistol_weapon -- TODO: remove in TRX 1.5. end) ================================================ FILE: data/trx/ship/games/tr1/strings-de.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "general": { "misc": { "empty_slot_fmt": "- Leerer SLOT %d -", }, "stats": { "secrets": "\\{review}Geheimnisse", } }, "settings": { "visuals.enable_gun_lighting": { "description": "Aktiviert die Verwendung von dynamischer Beleuchtung für Schüsse und Explosionen., ähnlich wie in TR2+.", }, "visuals.fix_animated_sprites": { "description": "Fixt originale Fehler in Die Zisterne und Grab des Tihocan, bei denen Pflanzen-Sprites in Wassergebieten nicht animiert sind.", } }, "objects": { "large_medipack": { "name": "Großes Medi-Pack", }, "small_medipack": { "name": "Kleines Medi-Pack", } }, "cutscenes": [ { "title": "Zwischensequenz 1", }, { "title": "Zwischensequenz 2", }, { "title": "Zwischensequenz 3", }, { "title": "Zwischensequenz 4", } ], "levels": [ { "title": "Laras Haus", }, { "title": "Die Kavernen", }, { "title": " Die Stadt Vilcabamba", "objects": { "key_1": { "name": "Silberner Schlüssel", }, "puzzle_1": { "name": "Goldener Götze", } } }, { "title": "Das verlorene Tal", "objects": { "puzzle_1": { "name": "Zahnrad", } } }, { "title": "Das Grab des Qualopec", }, { "title": "St. Francis' Folly", "objects": { "key_1": { "name": "Schlüssel des Neptun", }, "key_2": { "name": "Schlüssel des Atlas", }, "key_3": { "name": "Schlüssel des Damokles", }, "key_4": { "name": "Schlüssel des Thor", } } }, { "title": "Kolosseum", "objects": { "key_1": { "name": "Rostiger Schlüssel", } } }, { "title": "Der Palast des Midas", "objects": { "puzzle_1": { "name": "Goldbarren", } } }, { "title": "Die Zisterne", "objects": { "key_1": { "name": "Goldener Schlüssel", }, "key_2": { "name": "Silberner Schlüssel", }, "key_3": { "name": "Rostiger Schlüssel", } } }, { "title": "Das Grab des Tihocan", "objects": { "key_1": { "name": "Goldener Schlüssel", }, "key_2": { "name": "Rostiger Schlüssel", }, "key_3": { "name": "Rostiger Schlüssel", } } }, { "title": "Die Stadt Khamoon", "objects": { "key_1": { "name": "Saphir Schlüssel", } } }, { "title": "Der Obelisk von Khamoon", "objects": { "key_1": { "name": "Saphir Schlüssel", }, "puzzle_1": { "name": "Auge des Horus", }, "puzzle_2": { "name": "Skarabäus", }, "puzzle_3": { "name": "Siegel des Anubis", }, "puzzle_4": { "name": "Ankh", } } }, { "title": "Das Heiligtum des Scion", "objects": { "key_1": { "name": "Goldener Schlüssel", }, "puzzle_1": { "name": "Ankh", }, "puzzle_2": { "name": "Skarabäus", } } }, { "title": "Natlas Katakomben", "objects": { "puzzle_1": { "name": "Sicherung", }, "puzzle_2": { "name": "Schlüssel der Pyramide", } } }, { "title": "Atlantis", }, { "title": "Die Große Pyramide", }, { "title": "Dummy", }, { "title": "Dummy", }, { "title": "Dummy", }, { "title": "Dummy", }, { "title": "Titel", }, { "title": "Aktuelle Position", } ], "demos": [ { "title": "Die Stadt Vilcabamba", "objects": { "key_1": { "name": "Silberner Schlüssel", }, "puzzle_1": { "name": "Goldener Götze", } } }, { "title": "Das verlorene Tal", "objects": { "puzzle_1": { "name": "Zahnrad", } } } ] } ================================================ FILE: data/trx/ship/games/tr1/strings-en-gb.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "extends": "en", } ================================================ FILE: data/trx/ship/games/tr1/strings-fr.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "general": { "misc": { "empty_slot_fmt": "- EMPLACEMENT VIDE -", }, "stats": { "secrets": "\\{review}Secrets", } }, "settings": { "visuals.enable_gun_lighting": { "description": "Génère un éclairage dynamique pour les coups de feu et les explosions, comme apparu depuis TR2.", }, "visuals.fix_animated_sprites": { "description": "Corrige le fait que les sprites des végétations ne s'animent pas dans les zones aquatiques de La Citerne et de la Tombe de Tihocan.", } }, "objects": { "large_medipack": { "name": "Grande trousse de soins", }, "small_medipack": { "name": "Petite trousse de soins", } }, "cutscenes": [ { "title": "Cinématique 1", }, { "title": "Cinématique 2", }, { "title": "Cinématique 3", }, { "title": "Cinématique 4", } ], "levels": [ { "title": "Demeure de Lara", }, { "title": "Cavernes", }, { "title": "Cité de Vilcabamba", "objects": { "key_1": { "name": "Clé en Argent", }, "puzzle_1": { "name": "Idole d'Or", } } }, { "title": "La Vallée Perdue", "objects": { "puzzle_1": { "name": "Roue dentée", } } }, { "title": "Tombe de Qualopec", }, { "title": "Monument St Francis", "objects": { "key_1": { "name": "Clé de Neptune", }, "key_2": { "name": "Clé d'Atlas", }, "key_3": { "name": "Clé de Damoclès", }, "key_4": { "name": "Clé de Thor", } } }, { "title": "Colosseum", "objects": { "key_1": { "name": "Clé Rouillée", } } }, { "title": "Palais de Midas", "objects": { "puzzle_1": { "name": "Lingot d'Or", } } }, { "title": "La Citerne", "objects": { "key_1": { "name": "Clé en Or", }, "key_2": { "name": "Clé en Argent", }, "key_3": { "name": "Clé Rouillée", } } }, { "title": "Tombe de Tihocan", "objects": { "key_1": { "name": "Clé en Or", }, "key_2": { "name": "Clé Rouillée", }, "key_3": { "name": "Clé Rouillée", } } }, { "title": "Cité de Khamoon", "objects": { "key_1": { "name": "Clé de Saphir", } } }, { "title": "Obélisque de Khamoon", "objects": { "key_1": { "name": "Clé de Saphir", }, "puzzle_1": { "name": "Œil d'Horus", }, "puzzle_2": { "name": "Scarabée", }, "puzzle_3": { "name": "Sceau d'Anubis", }, "puzzle_4": { "name": "Ankh", } } }, { "title": "Sanctuaire du Scion", "objects": { "key_1": { "name": "Clé en Or", }, "puzzle_1": { "name": "Ankh", }, "puzzle_2": { "name": "Scarabée", } } }, { "title": "Mines de Natla", "objects": { "puzzle_1": { "name": "Fusible", }, "puzzle_2": { "name": "Clé de la Pyramide", } } }, { "title": "Atlantide", }, { "title": "La Grande Pyramide", }, { "title": "Réservé", }, { "title": "Réservé", }, { "title": "Réservé", }, { "title": "Réservé", }, { "title": "Titre", }, { "title": "Position Actuelle", } ], "demos": [ { "title": "Cité de Vilcabamba", "objects": { "key_1": { "name": "Clé en Argent", }, "puzzle_1": { "name": "Idole d'Or", } } }, { "title": "Vallée perdue", "objects": { "puzzle_1": { "name": "Roue dentée", } } } ] } ================================================ FILE: data/trx/ship/games/tr1/strings-gd.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "general": { "misc": { "empty_slot_fmt": "- SLOT FALAMH %d -", }, "stats": { "secrets": "\\{review}Dùnadh", } }, "settings": { "visuals.enable_gun_lighting": { "description": "Cuiridh seo solais fiùghantach ri losgadh armachd is spreadhaidhean, coltach ri TR2+.", }, "visuals.fix_animated_sprites": { "description": "Càraichidh bugan san t-Siasarn is Uaigh Tihocan far nach gluais planntrais anns an uisge.", } }, "objects": { "large_medipack": { "name": "Pasgan Mòr Slàinte", }, "small_medipack": { "name": "Pasgan Beag Slàinte", } }, "cutscenes": [ { "title": "Sealladh-film 1", }, { "title": "Sealladh-film 2", }, { "title": "Sealladh-film 3", }, { "title": "Sealladh-film 4", } ], "levels": [ { "title": "Dachaigh Lara", }, { "title": "Uaimhean", }, { "title": "Baile Vilcabamba", "objects": { "key_1": { "name": "Iuchair Airgid", }, "puzzle_1": { "name": "Ìomhaigh Òir", } } }, { "title": "Gleann Caillte", "objects": { "puzzle_1": { "name": "Gèar Inneil", } } }, { "title": "Uaigh Qualopec", }, { "title": "Gòraiche Naoimh Frang", "objects": { "key_1": { "name": "Iuchair Neaptain", }, "key_2": { "name": "Iuchair Atlas", }, "key_3": { "name": "Iuchair Damocles", }, "key_4": { "name": "Iuchair Thor", } } }, { "title": "Colasaem", "objects": { "key_1": { "name": "Iuchair Meirgeach", } } }, { "title": "Lùchairt Mìdas", "objects": { "puzzle_1": { "name": "Bàr Òir", } } }, { "title": "An t-Siasarn", "objects": { "key_1": { "name": "Iuchair Òir", }, "key_2": { "name": "Iuchair Airgid", }, "key_3": { "name": "Iuchair Meirgeach", } } }, { "title": "Uaigh Tihocan", "objects": { "key_1": { "name": "Iuchair Òir", }, "key_2": { "name": "Iuchair Meirgeach", }, "key_3": { "name": "Iuchair Meirgeach", } } }, { "title": "Baile Khamoon", "objects": { "key_1": { "name": "Iuchair Saoirbheir", } } }, { "title": "Obelisk Khamoon", "objects": { "key_1": { "name": "Iuchair Saoirbheir", }, "puzzle_1": { "name": "Sùil Horus", }, "puzzle_2": { "name": "Sgarab", }, "puzzle_3": { "name": "Ròn Anubis", }, "puzzle_4": { "name": "Ankh", } } }, { "title": "Naomh-chomhnaidh an Scion", "objects": { "key_1": { "name": "Iuchair Òir", }, "puzzle_1": { "name": "Ankh", }, "puzzle_2": { "name": "Sgarab", } } }, { "title": "Mèinnean Natla", "objects": { "puzzle_1": { "name": "Tadhail", }, "puzzle_2": { "name": "Iuchair Pioramaid", } } }, { "title": "Atlantais", }, { "title": "Am Pioramaid Mhòr", }, { "title": "Neach-àite", }, { "title": "Neach-àite", }, { "title": "Neach-àite", }, { "title": "Neach-àite", }, { "title": "Tiotal", }, { "title": "Ionad Làithreach", } ], "demos": [ { "title": "Baile Vilcabamba", "objects": { "key_1": { "name": "Iuchair Airgid", }, "puzzle_1": { "name": "Ìomhaigh Òir", } } }, { "title": "Gleann Caillte", "objects": { "puzzle_1": { "name": "Fiaclan Inneil", } } } ] } ================================================ FILE: data/trx/ship/games/tr1/strings-it.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "general": { "misc": { "empty_slot_fmt": "- SLOT VUOTO %d -", }, "stats": { "secrets": "Segreti", } }, "settings": { "visuals.enable_gun_lighting": { "description": "Abilita l'illuminazione dinamica per spari ed esplosioni, simile a TR2+.", }, "visuals.fix_animated_sprites": { "description": "Risolve i problemi preesistenti nei livelli \"Cisterna\" e \"Tomba di Tihocan\" per cui le animazioni degli sprite delle piante nelle aree acquatiche non funzionano.", } }, "objects": { "large_medipack": { "name": "Kit Medico Grande", }, "small_medipack": { "name": "Kit Medico Piccolo", } }, "cutscenes": [ { "title": "Intermezzo 1", }, { "title": "Intermezzo 2", }, { "title": "Intermezzo 3", }, { "title": "Intermezzo 4", } ], "levels": [ { "title": "Casa di Lara", }, { "title": "Caverne", }, { "title": "Città di Vilcabamba", "objects": { "key_1": { "name": "Chiave d'Argento", }, "puzzle_1": { "name": "Idolo d'Oro", } } }, { "title": "Valle Perduta", "objects": { "puzzle_1": { "name": "Ruota Dentata", } } }, { "title": "Tomba di Qualopec", }, { "title": "Rovine di St. Francis", "objects": { "key_1": { "name": "Chiave di Nettuno", }, "key_2": { "name": "Chiave di Atlante", }, "key_3": { "name": "Chiave di Damocle", }, "key_4": { "name": "Chiave di Thor", } } }, { "title": "Colosseo", "objects": { "key_1": { "name": "Chiave Arrugginita", } } }, { "title": "Palazzo di Mida", "objects": { "puzzle_1": { "name": "Lingotto d'Oro", } } }, { "title": "Cisterna", "objects": { "key_1": { "name": "Chiave d'Oro", }, "key_2": { "name": "Chiave d'Argento", }, "key_3": { "name": "Chiave Arrugginita", } } }, { "title": "Tomba di Tihocan", "objects": { "key_1": { "name": "Chiave d'Oro", }, "key_2": { "name": "Chiave Arrugginita", }, "key_3": { "name": "Chiave Arrugginita", } } }, { "title": "Città di Khamoon", "objects": { "key_1": { "name": "Chiave di Zaffiro", } } }, { "title": "Obelisco di Khamoon", "objects": { "key_1": { "name": "Chiave di Zaffiro", }, "puzzle_1": { "name": "Occhio di Horus", }, "puzzle_2": { "name": "Scarabeo", }, "puzzle_3": { "name": "Sigillo di Anubi", }, "puzzle_4": { "name": "Ankh", } } }, { "title": "Santuario dello Scion", "objects": { "key_1": { "name": "Chiave d'Oro", }, "puzzle_1": { "name": "Ankh", }, "puzzle_2": { "name": "Scarabeo", } } }, { "title": "Miniere di Natla", "objects": { "puzzle_1": { "name": "Fusibile", }, "puzzle_2": { "name": "Chiave della Piramide", } } }, { "title": "Atlantide", }, { "title": "La Grande Piramide", }, { "title": "Vuoto", }, { "title": "Vuoto", }, { "title": "Vuoto", }, { "title": "Vuoto", }, { "title": "Titoli", }, { "title": "Posizione Attuale", } ], "demos": [ { "title": "Città di Vilcabamba", "objects": { "key_1": { "name": "Chiave d'Argento", }, "puzzle_1": { "name": "Idolo d'Oro", } } }, { "title": "Valle Perduta", "objects": { "puzzle_1": { "name": "Ruota Dentata", } } } ] } ================================================ FILE: data/trx/ship/games/tr1/strings-pl.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "general": { "misc": { "empty_slot_fmt": "- PUSTY SLOT %d -", }, "stats": { "secrets": "\\{review}Sekrety", } }, "settings": { "visuals.enable_gun_lighting": { "description": "Włącza dynamiczne oświetlenie przy strzelaniu z broni i eksplozjach, podobnie jak w TR2+.", }, "visuals.fix_animated_sprites": { "description": "Naprawia błędy oryginalnej gry w Rezerwuarze i Grobowcu Tihocana, gdzie glony w wodnych obszarach nie są animowane.", } }, "objects": { "large_medipack": { "name": "Duża apteczka", }, "small_medipack": { "name": "Mała apteczka", } }, "cutscenes": [ { "title": "Przerywnik 1", }, { "title": "Przerywnik 2", }, { "title": "Przerywnik 3", }, { "title": "Przerywnik 4", } ], "levels": [ { "title": "Dom Lary", }, { "title": "Jaskinie", }, { "title": "Miasto Vilcabamba", "objects": { "key_1": { "name": "Srebrny klucz", }, "puzzle_1": { "name": "Złoty posążek", } } }, { "title": "Zaginiona Dolina", "objects": { "puzzle_1": { "name": "Zębatka", } } }, { "title": "Grobowiec Qualopeca", }, { "title": "Kompleks św. Franciszka", "objects": { "key_1": { "name": "Klucz Neptuna", }, "key_2": { "name": "Klucz Atlasa", }, "key_3": { "name": "Klucz Damoklesa", }, "key_4": { "name": "Klucz Thora", } } }, { "title": "Koloseum", "objects": { "key_1": { "name": "Zardzewiały klucz", } } }, { "title": "Pałac Midasa", "objects": { "puzzle_1": { "name": "Sztabka złota", } } }, { "title": "Rezerwuar", "objects": { "key_1": { "name": "Złoty klucz", }, "key_2": { "name": "Srebrny klucz", }, "key_3": { "name": "Zardzewiały klucz", } } }, { "title": "Grobowiec Tihocana", "objects": { "key_1": { "name": "Złoty klucz", }, "key_2": { "name": "Zardzewiały klucz", }, "key_3": { "name": "Zardzewiały klucz", } } }, { "title": "Miasto Khamoona", "objects": { "key_1": { "name": "Szafirowy klucz", } } }, { "title": "Obelisk Khamoona", "objects": { "key_1": { "name": "Szafirowy klucz", }, "puzzle_1": { "name": "Oko Horusa", }, "puzzle_2": { "name": "Skarabeusz", }, "puzzle_3": { "name": "Pieczęć Anubisa", }, "puzzle_4": { "name": "Ankh", } } }, { "title": "Sanktuarium Scion", "objects": { "key_1": { "name": "Złoty klucz", }, "puzzle_1": { "name": "Ankh", }, "puzzle_2": { "name": "Skarabeusz", } } }, { "title": "Kopalnie Natli", "objects": { "puzzle_1": { "name": "Bezpiecznik", }, "puzzle_2": { "name": "Klucz do piramidy", } } }, { "title": "Atlantis", }, { "title": "Wielka Piramida", }, { "title": "-", }, { "title": "-", }, { "title": "-", }, { "title": "-", }, { "title": "Menu główne", }, { "title": "Aktualna pozycja", } ], "demos": [ { "title": "Miasto Vilcabamba", "objects": { "key_1": { "name": "Srebrny klucz", }, "puzzle_1": { "name": "Złoty posążek", } } }, { "title": "Zaginiona Dolina", "objects": { "puzzle_1": { "name": "Zębatka", } } } ] } ================================================ FILE: data/trx/ship/games/tr1/strings-ru.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "general": { "misc": { "empty_slot_fmt": "- ПУСТОЙ СЛОТ %d -", }, "stats": { "secrets": "\\{review}Секреты", } }, "settings": { "visuals.enable_gun_lighting": { "description": "Позволяет генерировать динамическое освещение для выстрелов и взрывов, аналогично TR2+.", }, "visuals.fix_animated_sprites": { "description": "Исправляет изначальные проблемы в Цистерне и Гробнице Тихокана, из-за которых спрайты растений в водных зонах не анимировались.", } }, "objects": { "large_medipack": { "name": "Большая аптечка", }, "small_medipack": { "name": "Маленькая аптечка", } }, "cutscenes": [ { "title": "Cutscene 1", }, { "title": "Cutscene 2", }, { "title": "Cutscene 3", }, { "title": "Cutscene 4", } ], "levels": [ { "title": "Дом Лары", }, { "title": "Пещеры", }, { "title": "Город Вилкабамба", "objects": { "key_1": { "name": "Серебряный ключ", }, "puzzle_1": { "name": "Золотой идол", } } }, { "title": "Затерянная долина", "objects": { "puzzle_1": { "name": "Шестерёнка", } } }, { "title": "Гробница Куалопека", }, { "title": "Монастырь св. Франциска", "objects": { "key_1": { "name": "Ключ Нептуна", }, "key_2": { "name": "Ключ Атласа", }, "key_3": { "name": "Ключ Дамокла", }, "key_4": { "name": "Ключ Тора", } } }, { "title": "Колизей", "objects": { "key_1": { "name": "Ржавый ключ", } } }, { "title": "Дворец Мидаса", "objects": { "puzzle_1": { "name": "Золотой слиток", } } }, { "title": "Цистерна", "objects": { "key_1": { "name": "Золотой ключ", }, "key_2": { "name": "Серебряный ключ", }, "key_3": { "name": "Ржавый ключ", } } }, { "title": "Гробница Тихокана", "objects": { "key_1": { "name": "Золотой ключ", }, "key_2": { "name": "Ржавый ключ", }, "key_3": { "name": "Ржавый ключ", } } }, { "title": "Город Хамун", "objects": { "key_1": { "name": "Сапфировый ключ", } } }, { "title": "Обелиск Хамун", "objects": { "key_1": { "name": "Сапфировый ключ", }, "puzzle_1": { "name": "Глаз Гора", }, "puzzle_2": { "name": "Скарабей", }, "puzzle_3": { "name": "Печать Анубиса", }, "puzzle_4": { "name": "Анк", } } }, { "title": "Святилище Наследия", "objects": { "key_1": { "name": "Золотой ключ", }, "puzzle_1": { "name": "Анк", }, "puzzle_2": { "name": "Скарабей", } } }, { "title": "Раскопки Натлы", "objects": { "puzzle_1": { "name": "Предохранитель", }, "puzzle_2": { "name": "Ключ к пирамиде", } } }, { "title": "Атлантида", }, { "title": "Великая пирамида", }, { "title": "Dummy", }, { "title": "Dummy", }, { "title": "Dummy", }, { "title": "Dummy", }, { "title": "Title", }, { "title": "Current Position", } ], "demos": [ { "title": "Город Вилкабамба", "objects": { "key_1": { "name": "Серебряный ключ", }, "puzzle_1": { "name": "Золотой идол", } } }, { "title": "Затерянная долина", "objects": { "puzzle_1": { "name": "Шестерёнка", } } } ] } ================================================ FILE: data/trx/ship/games/tr1/strings.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "general": { "misc": { "empty_slot_fmt": "- EMPTY SLOT %d -", }, "stats": { "secrets": "Secrets", } }, "settings": { "visuals.enable_gun_lighting": { "description": "Enables dynamic lighting to be generated for gunshots and explosions, similar to TR2+.", }, "visuals.fix_animated_sprites": { "description": "Fixes original issues in The Cistern and Tomb of Tihocan where plant sprites in water areas do not animate.", } }, "objects": { "large_medipack": { "name": "Large Medi Pack", }, "small_medipack": { "name": "Small Medi Pack", } }, "cutscenes": [ { "title": "Cutscene 1", }, { "title": "Cutscene 2", }, { "title": "Cutscene 3", }, { "title": "Cutscene 4", } ], "levels": [ { "title": "Lara's Home", }, { "title": "Caves", }, { "title": "City of Vilcabamba", "objects": { "key_1": { "name": "Silver Key", }, "puzzle_1": { "name": "Gold Idol", } } }, { "title": "Lost Valley", "objects": { "puzzle_1": { "name": "Machine Cog", } } }, { "title": "Tomb of Qualopec", }, { "title": "St. Francis' Folly", "objects": { "key_1": { "name": "Neptune Key", }, "key_2": { "name": "Atlas Key", }, "key_3": { "name": "Damocles Key", }, "key_4": { "name": "Thor Key", } } }, { "title": "Colosseum", "objects": { "key_1": { "name": "Rusty Key", } } }, { "title": "Palace Midas", "objects": { "puzzle_1": { "name": "Gold Bar", } } }, { "title": "The Cistern", "objects": { "key_1": { "name": "Gold Key", }, "key_2": { "name": "Silver Key", }, "key_3": { "name": "Rusty Key", } } }, { "title": "Tomb of Tihocan", "objects": { "key_1": { "name": "Gold Key", }, "key_2": { "name": "Rusty Key", }, "key_3": { "name": "Rusty Key", } } }, { "title": "City of Khamoon", "objects": { "key_1": { "name": "Sapphire Key", } } }, { "title": "Obelisk of Khamoon", "objects": { "key_1": { "name": "Sapphire Key", }, "puzzle_1": { "name": "Eye of Horus", }, "puzzle_2": { "name": "Scarab", }, "puzzle_3": { "name": "Seal of Anubis", }, "puzzle_4": { "name": "Ankh", } } }, { "title": "Sanctuary of the Scion", "objects": { "key_1": { "name": "Gold Key", }, "puzzle_1": { "name": "Ankh", }, "puzzle_2": { "name": "Scarab", } } }, { "title": "Natla's Mines", "objects": { "puzzle_1": { "name": "Fuse", }, "puzzle_2": { "name": "Pyramid Key", } } }, { "title": "Atlantis", }, { "title": "The Great Pyramid", }, { "title": "Dummy", }, { "title": "Dummy", }, { "title": "Dummy", }, { "title": "Dummy", }, { "title": "Title", }, { "title": "Current Position", } ], "demos": [ { "title": "City of Vilcabamba", "objects": { "key_1": { "name": "Silver Key", }, "puzzle_1": { "name": "Gold Idol", } } }, { "title": "Lost Valley", "objects": { "puzzle_1": { "name": "Machine Cog", } } } ] } ================================================ FILE: data/trx/ship/games/tr1/weapons.json5 ================================================ { "LGT_UNARMED": { "sample_num": "SFX_LARA_NO", }, "LGT_FLARE": { "ammo": { "initial_qty": 6, "pickup_qty": 6, "pickup_qty_alt": 8, }, "flash_shade": 2048, "flash_pos": { "x": 11, "y": 32, "z": 80, }, "flash_pos_alt": { "x": -6, "y": 6, "z": 80, }, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {}, "glow_color": [1, 0.75, 0.125], "is_available": false, }, "LGT_PISTOLS": { "type": "WEAPON_TYPE_DUAL_PISTOLS", "lock_angles": [-60, +60, -60, +60], "left_angles": [-170, +60, -80, +80], "right_angles": [-60, +170, -80, +80], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 650, "damage": 1, "ammo": { "initial_qty": 32, "pickup_qty": 32, }, "target_dist": 8.0, "recoil_frame": 9, "flash_time": 3, "flash_shade": 5120, "flash_pos": { "y": 155, "z": 50, }, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {}, "glow_color": [1, 0.75, 0.125], "sample_num": "SFX_LARA_PISTOLS", }, "LGT_MAGNUMS": { "type": "WEAPON_TYPE_DUAL_PISTOLS", "lock_angles": [-60, +60, -60, +60], "left_angles": [-170, +60, -80, +80], "right_angles": [-60, +170, -80, +80], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 650, "damage": 2, "ammo": { "initial_qty": 50, "pickup_qty": 50, }, "target_dist": 8.0, "recoil_frame": 9, "flash_time": 3, "flash_shade": 4096, "flash_pos": { "y": 155, "z": 55, }, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {}, "glow_color": [1, 0.75, 0.125], "sample_num": "SFX_LARA_MAGNUMS", }, "LGT_AUTOS": { "type": "WEAPON_TYPE_DUAL_PISTOLS", "lock_angles": [-60, +60, -60, +60], "left_angles": [-170, +60, -80, +80], "right_angles": [-60, +170, -80, +80], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 650, "damage": 2, "ammo": { "initial_qty": 40, "pickup_qty": 40, }, "target_dist": 8.0, "recoil_frame": 9, "flash_time": 3, "flash_shade": 4096, "flash_pos": { "y": 215, "z": 65, }, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {}, "glow_color": [1, 0.75, 0.125], "sample_num": "SFX_LARA_AUTOS", "is_available": false, }, "LGT_DESERT_EAGLE": { "type": "WEAPON_TYPE_SINGLE_PISTOL", "lock_angles": [-60, +60, -60, +60], "left_angles": [-10, +10, -80, +80], "right_angles": [0, 0, 0, 0], "aim_speed": 10, "shot_accuracy": 4, "gun_height": 650, "damage": 21, "ammo": { "initial_qty": 10, "pickup_qty": 10, }, "target_dist": 8.0, "recoil_frame": 16, "flash_time": 3, "flash_shade": 4096, "flash_pos": { "y": 215, "z": 65, }, "sample_num": "SFX_LARA_DESERT_EAGLE", "is_available": false, }, "LGT_UZIS": { "type": "WEAPON_TYPE_DUAL_PISTOLS", "lock_angles": [-60, +60, -60, +60], "left_angles": [-170, +60, -80, +80], "right_angles": [-60, +170, -80, +80], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 650, "damage": 1, "ammo": { "initial_qty": 100, "pickup_qty": 100, }, "target_dist": 8.0, "recoil_frame": 3, "flash_time": 2, "flash_shade": 2560, "flash_pos": { "y": 180, "z": 55, }, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {}, "glow_color": [1, 0.75, 0.125], "sample_num": "SFX_LARA_UZI_FIRE", }, "LGT_SHOTGUN": { "type": "WEAPON_TYPE_RIFLE", "lock_angles": [-60, +60, -55, +55], "left_angles": [-80, +80, -65, +65], "right_angles": [-80, +80, -65, +65], "aim_speed": 10, "shot_accuracy": 0, "gun_height": 500, "damage": 4, "ammo": { "initial_qty": 2, "pickup_qty": 2, }, "target_dist": 8.0, "equip_anim_idx": 1, "draw_frame": 10, "undraw_frame": 21, "recoil_frame": 9, "flash_time": 3, "flash_shade": 2560, "flash_pos": { "y": 285, }, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {}, "glow_color": [1, 0.75, 0.125], "sample_num": "SFX_LARA_SHOTGUN", }, "LGT_M16": { "type": "WEAPON_TYPE_RIFLE", "lock_angles": [-60, +60, -55, +55], "left_angles": [-80, +80, -65, +65], "right_angles": [-80, +80, -65, +65], "aim_speed": 10, "shot_accuracy": 4, "gun_height": 500, "damage": 3, "ammo": { "initial_qty": 40, "pickup_qty": 40, }, "target_dist": 12.0, "equip_anim_idx": 1, "draw_frame": 10, "undraw_frame": 21, "recoil_frame": 0, "flash_time": 3, "flash_shade": 2560, "sample_num": "", "flash_pos": { "y": 400, "z": 99, }, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {"z": -65}, "glow_color": [1, 0.75, 0.125], "is_available": false, }, "LGT_MP5": { "type": "WEAPON_TYPE_RIFLE", "lock_angles": [-60, +60, -55, +55], "left_angles": [-80, +80, -65, +65], "right_angles": [-80, +80, -65, +65], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 500, "damage": 3, "ammo": { "initial_qty": 60, "pickup_qty": 60, }, "target_dist": 12.0, "equip_anim_idx": 1, "draw_frame": 16, "undraw_frame": 21, "recoil_frame": 0, "flash_time": 3, "flash_shade": 2560, "flash_pos": { "y": 332, "z": 96, }, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {"z": -65}, "glow_color": [1, 0.75, 0.125], "sample_num": "", "is_available": false, }, "LGT_ROCKET": { "type": "WEAPON_TYPE_RIFLE", "lock_angles": [-60, +60, -55, +55], "left_angles": [-80, +80, -65, +65], "right_angles": [-80, +80, -65, +65], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 500, "damage": 30, "ammo": { "initial_qty": 1, "pickup_qty": 1, }, "target_dist": 8.0, "equip_anim_idx": 1, "draw_frame": 12, "undraw_frame": 21, "recoil_frame": 0, "flash_time": 2, "sample_num": "", "is_available": false, }, "LGT_GRENADE": { "type": "WEAPON_TYPE_RIFLE", "lock_angles": [-60, +60, -55, +55], "left_angles": [-80, +80, -65, +65], "right_angles": [-80, +80, -65, +65], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 500, "damage": 30, "ammo": { "initial_qty": 2, "pickup_qty": 2, }, "target_dist": 8.0, "equip_anim_idx": 0, "draw_frame": 13, "undraw_frame": 14, "recoil_frame": 0, "flash_time": 2, "sample_num": "", "is_available": false, }, "LGT_HARPOON": { "type": "WEAPON_TYPE_RIFLE", "lock_angles": [-60, +60, -65, +65], "left_angles": [-80, +80, -75, +75], "right_angles": [-80, +80, -75, +75], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 500, "damage": 4, "ammo": { "initial_qty": 3, "pickup_qty": 3, }, "target_dist": 8.0, "equip_anim_idx": 1, "draw_frame": 10, "undraw_frame": 21, "recoil_frame": 0, "flash_time": 2, "sample_num": "", "is_available": false, }, } ================================================ FILE: data/trx/ship/games/tr1-demo-pc/gameflow.json5 ================================================ { // NOTE: bad changes to this file may result in crashes. // Lines starting with double slashes are comments and are ignored. "engine": 1, "extends": "tr1", "name": "TR1 PC Demo", // path to the main menu background image "main_menu_picture": "title.webp", // path to the savegame file "savegame_file_fmt": "save_demo_pc_%02d.dat", "injections": [ "braid.bin", "bubbles.bin", "gun_glow.bin", "lara_animations.bin", "lara_guns.bin", "uzi_sfx.bin", "explosion.bin", "pickup_aid.bin", "sprite_alignment.bin", "pda_model.bin", "font.bin", "winston_model.bin", "misc_sprites.bin", "lara_outfits.bin", ], "enable_tr2_item_drops": false, "convert_dropped_guns": false, "title": { "path": [ "data_demo_pc/title.phd", "title.phd", ], "music_track": 0, "inherit_injections": false, "sequence": [ {"type": "exit_to_title"}, ], "injections": [ "pda_model.bin", "font.bin", "title_textures.bin", "misc_sprites.bin", ], }, "levels": [ // Level 2: City of Vilcabamba { "path": [ "data_demo_pc/level2.phd", "level2.phd", ], "music_track": 0, "lara_outfit": "tr1_classic", "sequence": [ {"type": "loading_screen", "path": "peru.webp"}, {"type": "loop_game"}, {"type": "level_stats"}, {"type": "level_complete"}, ], }, ], "hidden_config": [ "enable_item_examining", "healthbar_poison_color", "healthbar_poison_color_ps1", "enemy_healthbar_color_allies", "enemy_healthbar_color_allies_ps1", "exposurebar_color", "exposurebar_color_ps1", "exposurebar_location", "exposurebar_show_mode", "enable_ally_targeting", "enable_weather", "enable_footprints", "ally_hostility_policy", "fix_monkey_pickup_priority", "fix_pipeman_aim", ], } ================================================ FILE: data/trx/ship/games/tr1-demo-pc/strings-de.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Die Stadt Vilcabamba", "objects": { "key_1": { "name": "Silberner Schlüssel", }, "puzzle_1": { "name": "Goldener Götze", } } } ] } ================================================ FILE: data/trx/ship/games/tr1-demo-pc/strings-fr.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "\\{review}Ville de Vilcabamba", "objects": { "key_1": { "name": "\\{review}Clé en argent", }, "puzzle_1": { "name": "\\{review}Idole en or", } } } ] } ================================================ FILE: data/trx/ship/games/tr1-demo-pc/strings-gd.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Baile Vilcabamba", "objects": { "key_1": { "name": "Iuchair Airgid", }, "puzzle_1": { "name": "Ìomhaigh Òir", } } } ] } ================================================ FILE: data/trx/ship/games/tr1-demo-pc/strings-it.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Città di Vilcabamba", "objects": { "key_1": { "name": "Chiave d'Argento", }, "puzzle_1": { "name": "Idolo d'Oro", } } } ] } ================================================ FILE: data/trx/ship/games/tr1-demo-pc/strings-pl.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Miasto Vilcabamba", "objects": { "key_1": { "name": "Srebrny klucz", }, "puzzle_1": { "name": "Złoty posążek", } } } ] } ================================================ FILE: data/trx/ship/games/tr1-demo-pc/strings-ru.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Город Вилкабамба", "objects": { "key_1": { "name": "Серебряный ключ", }, "puzzle_1": { "name": "Золотой идол", } } } ] } ================================================ FILE: data/trx/ship/games/tr1-demo-pc/strings.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "City of Vilcabamba", "objects": { "key_1": { "name": "Silver Key", }, "puzzle_1": { "name": "Gold Idol", } } } ] } ================================================ FILE: data/trx/ship/games/tr1-level/gameflow.json5 ================================================ { // This file is used to enable the -l argument support. "engine": 1, "extends": "tr1", "name": "TR1 Direct Level", "main_menu_picture": "title.webp", "savegame_file_fmt": "save_tmp_%02d.dat", "injections": [ "braid.bin", "bubbles.bin", "gun_glow.bin", "lara_animations.bin", "lara_extra.bin", "lara_guns.bin", "uzi_sfx.bin", "explosion.bin", "pickup_aid.bin", "sprite_alignment.bin", "pda_model.bin", "font.bin", "winston_model.bin", "misc_sprites.bin", "lara_outfits.bin", ], "enable_tr2_item_drops": false, "convert_dropped_guns": false, "levels": [ { "path": "%direct_level%", "music_track": 0, "lara_outfit": "tr1_classic", "sequence": [ {"type": "loop_game"}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [], }, ], } ================================================ FILE: data/trx/ship/games/tr1-level/strings-de.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Testlevel", } ] } ================================================ FILE: data/trx/ship/games/tr1-level/strings-fr.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "\\{review}Niveau de test", } ] } ================================================ FILE: data/trx/ship/games/tr1-level/strings-gd.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Ìre Phròbhail", } ] } ================================================ FILE: data/trx/ship/games/tr1-level/strings-it.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Livello di Prova", } ] } ================================================ FILE: data/trx/ship/games/tr1-level/strings-pl.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Poziom testowy", } ] } ================================================ FILE: data/trx/ship/games/tr1-level/strings-ru.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Тестовый уровень", } ] } ================================================ FILE: data/trx/ship/games/tr1-level/strings.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Test Level", } ] } ================================================ FILE: data/trx/ship/games/tr1-ub/gameflow.json5 ================================================ { // NOTE: bad changes to this file may result in crashes. // Lines starting with double slashes are comments and are ignored. "engine": 1, "extends": "tr1", "name": "Unfinished Business", "main_menu_picture": "title_ub.webp", "savegame_file_fmt": "save_trub_%02d.dat", "injections": [ "braid.bin", "bubbles.bin", "gun_glow.bin", "lara_animations.bin", "lara_guns.bin", "uzi_sfx.bin", "explosion.bin", "pickup_aid.bin", "sprite_alignment.bin", "pda_model.bin", "font.bin", "winston_model.bin", "misc_sprites.bin", "lara_outfits.bin", "crystal.bin", ], "enable_tr2_item_drops": false, "convert_dropped_guns": false, "enforced_config": { "fix_water_exit": false, }, "title": { "path": "title.phd", "music_track": 2, "inherit_injections": false, "injections": [ "pda_model.bin", "font.bin", "title_textures.bin", "misc_sprites.bin", ], "sequence": [ {"type": "display_picture", "path": "eidos.webp", "legal": true, "display_time": 1, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "play_fmv", "fmv_id": 0}, {"type": "play_fmv", "fmv_id": 1}, {"type": "exit_to_title"}, ], }, "levels": [ // Level 1: Return to Egypt { "path": "egypt.phd", "music_track": 59, "lara_outfit": "tr1_classic", "sequence": [ {"type": "loading_screen", "path": "ub_loading1.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "egypt_cameras.bin", "egypt_fd.bin", "egypt_itemrots.bin", "egypt_meshfixes.bin", "egypt_textures.bin", "panther_sfx.bin", "egypt_crystals.bin", ], "unobtainable_kills": 1, }, // Level 2: Temple of the Cat { "path": "cat.phd", "music_track": 59, "lara_outfit": "tr1_classic", "sequence": [ {"type": "loading_screen", "path": "ub_loading1.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "cat_cameras.bin", "cat_fd.bin", "cat_itemrots.bin", "cat_meshfixes.bin", "cat_textures.bin", "panther_sfx.bin", "cat_crystals.bin", ], "unobtainable_pickups": 1, }, // Level 3: Atlantean Stronghold { "path": "end.phd", "music_track": 60, "lara_outfit": "tr1_classic", "sequence": [ {"type": "loading_screen", "path": "ub_loading2.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "door61_sfx.bin", "stronghold_fd.bin", "stronghold_itemrots.bin", "stronghold_textures.bin", "stronghold_crystals.bin", ], "unobtainable_kills": 1, }, // Level 4: The Hive { "path": "end2.phd", "music_track": 60, "lara_outfit": "tr1_classic", "sequence": [ {"type": "loading_screen", "path": "ub_loading2.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "level_stats"}, {"type": "play_music", "music_track": 19}, {"type": "display_picture", "credit": true, "path": "end.webp", "display_time": 7.5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credits_ub.webp", "display_time": 7.5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credits_1.webp", "display_time": 7.5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credits_2.webp", "display_time": 7.5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credits_3.webp", "display_time": 7.5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "total_stats", "background_path": "install.webp"}, {"type": "exit_to_title"}, ], "injections": [ "hive_fd.bin", "hive_itemrots.bin", "hive_textures.bin", "hive_crystals.bin", ], }, {"type": "dummy"}, // Level 6: Current Position // This level is necessary to read TombATI's save files. { "path": "current.phd", "type": "current", "music_track": 0, "inherit_injections": false, "sequence": [{"type": "exit_to_title"}], }, ], "fmvs": [ {"path": "core.avi", "legal": true}, {"path": "escape.avi", "legal": true}, ], "hidden_config": [ "enable_cutscenes", // UB has no cutscenes "enable_demo", // UB has no demos "enable_item_examining", // UB has no special item descriptions "healthbar_poison_color", "healthbar_poison_color_ps1", "enemy_healthbar_color_allies", "enemy_healthbar_color_allies_ps1", "exposurebar_color", "exposurebar_color_ps1", "exposurebar_location", "exposurebar_show_mode", "fix_water_exit", "change_pierre_spawn", "fix_bear_ai", "fix_monkey_pickup_priority", "restore_ps1_enemies", "disable_trex_collision", "fix_tihocan_secret_sound", "fix_animated_sprites", "enable_ally_targeting", "enable_weather", "enable_footprints", "ally_hostility_policy", "fix_pipeman_aim", "enable_cinematics", "enable_body_bags", ], } ================================================ FILE: data/trx/ship/games/tr1-ub/strings-de.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Rückkehr nach Ägypten", "objects": { "key_1": { "name": "Goldener Schlüssel", } } }, { "title": "Der Tempel der Katze", "objects": { "key_1": { "name": "Antiker Schlüssel", } } }, { "title": "Die Festung von Atlantis", }, { "title": "Das Nest", }, { "title": "Titel", }, { "title": "Aktuelle Position", } ] } ================================================ FILE: data/trx/ship/games/tr1-ub/strings-fr.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Retour en Égypte", "objects": { "key_1": { "name": "Clé en or", } } }, { "title": "Le Temple du Chat", "objects": { "key_1": { "name": "Clé ornée", } } }, { "title": "La Forteresse Atlante", }, { "title": "La Ruche", }, { "title": "Titre", }, { "title": "Position actuelle", } ] } ================================================ FILE: data/trx/ship/games/tr1-ub/strings-gd.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "A' tilleadh dhan Eiphit", "objects": { "key_1": { "name": "Iuchair Òir", } } }, { "title": "Teampall a' Chait", "objects": { "key_1": { "name": "Iuchair Grinn", } } }, { "title": "An Daingneach Atlantais", }, { "title": "An Nead", }, { "title": "Tiotal", }, { "title": "Suidheachadh Làithreach", } ] } ================================================ FILE: data/trx/ship/games/tr1-ub/strings-it.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Ritorno in Egitto", "objects": { "key_1": { "name": "Chiave d'Oro", } } }, { "title": "Tempio del Gatto", "objects": { "key_1": { "name": "Chiave Decorata", } } }, { "title": "Fortezza Atlantidea", }, { "title": "L'Alveare", }, { "title": "Titoli", }, { "title": "Posizione Attuale", } ] } ================================================ FILE: data/trx/ship/games/tr1-ub/strings-pl.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Powrót do Egiptu", "objects": { "key_1": { "name": "Złoty klucz", } } }, { "title": "Świątynia Kota", "objects": { "key_1": { "name": "Ozdobny klucz", } } }, { "title": "Twierdza Atlantydów", }, { "title": "Rój", }, { "title": "Menu główne", }, { "title": "Aktualna pozycja", } ] } ================================================ FILE: data/trx/ship/games/tr1-ub/strings-ru.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Возвращение в Египет", "objects": { "key_1": { "name": "Золотой ключ", } } }, { "title": "Храм Кошки", "objects": { "key_1": { "name": "Богато украшенный ключ", } } }, { "title": "Крепость атлантов", }, { "title": "Улей", }, { "title": "Оглавление", }, { "title": "Текущая позиция", } ] } ================================================ FILE: data/trx/ship/games/tr1-ub/strings.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Return to Egypt", "objects": { "key_1": { "name": "Gold Key", } } }, { "title": "Temple of the Cat", "objects": { "key_1": { "name": "Ornate Key", } } }, { "title": "Atlantean Stronghold", }, { "title": "The Hive", }, { "title": "Title", }, { "title": "Current Position", } ] } ================================================ FILE: data/trx/ship/games/tr2/catalog_item_actions.csv ================================================ 0, ITEM_ACTION_TURN_180 1, ITEM_ACTION_FLOOR_SHAKE 2, ITEM_ACTION_LARA_NORMAL 3, ITEM_ACTION_BUBBLES 4, ITEM_ACTION_FINISH_LEVEL 5, ITEM_ACTION_FLOOD 6, ITEM_ACTION_CHANDELIER 7, ITEM_ACTION_RUBBLE 8, ITEM_ACTION_PISTON 9, ITEM_ACTION_CURTAIN 10, ITEM_ACTION_SET_CHANGE 11, ITEM_ACTION_EXPLOSION 12, ITEM_ACTION_LARA_HANDS_FREE 13, ITEM_ACTION_FLIP_MAP 14, ITEM_ACTION_LARA_DRAW_RIGHT_GUN 15, ITEM_ACTION_LARA_DRAW_LEFT_GUN 18, ITEM_ACTION_SWAP_MESHES_WITH_MESH_SWAP_1 19, ITEM_ACTION_SWAP_MESHES_WITH_MESH_SWAP_2 20, ITEM_ACTION_SWAP_MESHES_WITH_MESH_SWAP_3 21, ITEM_ACTION_INVISIBILITY_ON 22, ITEM_ACTION_INVISIBILITY_OFF 23, ITEM_ACTION_DYNAMIC_LIGHT_ON 24, ITEM_ACTION_DYNAMIC_LIGHT_OFF 25, ITEM_ACTION_STATUE 26, ITEM_ACTION_RESET_HAIR 27, ITEM_ACTION_BOILER 28, ITEM_ACTION_ASSAULT_RESET 29, ITEM_ACTION_ASSAULT_STOP 30, ITEM_ACTION_ASSAULT_START 31, ITEM_ACTION_ASSAULT_FINISHED 32, ITEM_ACTION_SHADOW_ON 33, ITEM_ACTION_SHADOW_OFF 62, ITEM_ACTION_TURN_90 ================================================ FILE: data/trx/ship/games/tr2/catalog_lara_anims.csv ================================================ 0, LA_RUN 1, LA_WALK_FORWARD 2, LA_WALK_STOP_RIGHT 3, LA_WALK_STOP_LEFT 4, LA_WALK_TO_RUN_RIGHT 5, LA_WALK_TO_RUN_LEFT 6, LA_RUN_START 7, LA_RUN_TO_WALK_RIGHT 8, LA_RUN_TO_STAND_LEFT 9, LA_RUN_TO_WALK_LEFT 10, LA_RUN_TO_STAND_RIGHT 11, LA_STAND_STILL 12, LA_TURN_RIGHT_SLOW 13, LA_TURN_LEFT_SLOW 14, LA_JUMP_FORWARD_LAND_START_UNUSED 15, LA_JUMP_FORWARD_LAND_END_UNUSED 16, LA_RUN_JUMP_RIGHT_START 17, LA_RUN_JUMP_RIGHT_CONTINUE 18, LA_RUN_JUMP_LEFT_START 19, LA_RUN_JUMP_LEFT_CONTINUE 20, LA_WALK_FORWARD_START 21, LA_WALK_FORWARD_START_CONTINUE 22, LA_JUMP_FORWARD_TO_FREEFALL 23, LA_FREEFALL 24, LA_FREEFALL_LAND 25, LA_FREEFALL_LAND_DEATH 26, LA_STAND_TO_JUMP_UP 27, LA_STAND_TO_JUMP_UP_CONTINUE 28, LA_JUMP_UP 29, LA_JUMP_UP_TO_HANG_UNUSED 30, LA_JUMP_UP_TO_FREEFALL 31, LA_JUMP_UP_LAND 32, LA_SMASH_JUMP 33, LA_SMASH_JUMP_CONTINUE 34, LA_FALL_START 35, LA_FALL 36, LA_FALL_TO_FREEFALL 37, LA_HANG_TO_FREEFALL 38, LA_WALK_BACK_END_RIGHT 39, LA_WALK_BACK_END_LEFT 40, LA_WALK_BACK 41, LA_WALK_BACK_START 42, LA_CLIMB_3CLICK 43, LA_CLIMB_3CLICK_END_TO_RUN 44, LA_TURN_RIGHT 45, LA_JUMP_FORWARD_TO_FREEFALL_2 46, LA_REACH_TO_FREEFALL 47, LA_ROLL_ALTERNATE 48, LA_ROLL_END_ALTERNATE 49, LA_JUMP_FORWARD_END_TO_FREEFALL 50, LA_CLIMB_2CLICK 51, LA_CLIMB_2CLICK_END 52, LA_CLIMB_2CLICK_END_TO_RUN 53, LA_WALL_SMASH_LEFT 54, LA_WALL_SMASH_RIGHT 55, LA_RUN_UP_STEP_RIGHT 56, LA_RUN_UP_STEP_LEFT 57, LA_WALK_UP_STEP_RIGHT 58, LA_WALK_UP_STEP_LEFT 59, LA_WALK_DOWN_LEFT 60, LA_WALK_DOWN_RIGHT 61, LA_WALK_DOWN_BACK_LEFT 62, LA_WALK_DOWN_BACK_RIGHT 63, LA_WALL_SWITCH_DOWN 64, LA_WALL_SWITCH_UP 65, LA_SIDE_STEP_LEFT 66, LA_SIDE_STEP_LEFT_END 67, LA_SIDE_STEP_RIGHT 68, LA_SIDE_STEP_RIGHT_END 69, LA_ROTATE_LEFT 70, LA_SLIDE_FORWARD 71, LA_SLIDE_FORWARD_END 72, LA_SLIDE_FORWARD_STOP 73, LA_STAND_TO_JUMP 74, LA_JUMP_BACK_START 75, LA_JUMP_BACK 76, LA_JUMP_FORWARD_START 77, LA_JUMP_FORWARD 78, LA_JUMP_LEFT_START 79, LA_JUMP_LEFT 80, LA_JUMP_RIGHT_START 81, LA_JUMP_RIGHT 82, LA_LAND 83, LA_JUMP_BACK_TO_FREEFALL 84, LA_JUMP_LEFT_TO_FREEFALL 85, LA_JUMP_RIGHT_TO_FREEFALL 86, LA_UNDERWATER_SWIM_FORWARD 87, LA_UNDERWATER_SWIM_FORWARD_DRIFT 88, LA_SMALL_JUMP_BACK_START 89, LA_SMALL_JUMP_BACK 90, LA_SMALL_JUMP_BACK_END 91, LA_JUMP_UP_START 92, LA_LAND_TO_RUN 93, LA_FALL_BACK 94, LA_JUMP_FORWARD_TO_REACH 95, LA_REACH 96, LA_REACH_TO_HANG 97, LA_CLIMB_ON 98, LA_REACH_TO_FREEFALL_2 99, LA_FALL_CROUCHING_LANDING 100, LA_JUMP_FORWARD_TO_REACH_LATE 101, LA_JUMP_FORWARD_START_TO_REACH_ALTERNATE 102, LA_CLIMB_ON_END 103, LA_STAND_IDLE 104, LA_SLIDE_BACKWARD_START 105, LA_SLIDE_BACKWARD 106, LA_SLIDE_BACKWARD_END 107, LA_UNDERWATER_SWIM_TO_IDLE 108, LA_UNDERWATER_IDLE 109, LA_UNDERWARER_IDLE_TO_SWIM 110, LA_ONWATER_IDLE 111, LA_ONWATER_TO_STAND_HIGH 112, LA_FREEFALL_TO_UNDERWATER 113, LA_ONWATER_DIVE_ALTERNATE 114, LA_UNDERWATER_TO_ONWATER 115, LA_ONWATER_SWIM_FORWARD_DIVE 116, LA_ONWATER_SWIM_FORWARD 117, LA_ONWATER_SWIM_FORWARD_TO_IDLE 118, LA_ONWATER_IDLE_TO_SWIM_FORWARD 119, LA_ONWATER_DIVE 120, LA_PUSHABLE_GRAB 121, LA_PUSHABLE_RELEASE 122, LA_PUSHABLE_PULL 123, LA_PUSHABLE_PUSH 124, LA_UNDERWATER_DEATH 125, LA_HIT_FRONT 126, LA_HIT_BACK 127, LA_HIT_LEFT 128, LA_HIT_RIGHT 129, LA_UNDERWATER_SWITCH 130, LA_UNDERWATER_PICKUP 131, LA_USE_KEY 132, LA_ONWATER_DEATH 133, LA_RUN_DEATH 134, LA_USE_PUZZLE 135, LA_PICKUP 136, LA_SHIMMY_LEFT 137, LA_SHIMMY_RIGHT 138, LA_STAND_DEATH 139, LA_BOULDER_DEATH 140, LA_ONWATER_IDLE_TO_SWIM_BACK 141, LA_ONWATER_SWIM_BACK 142, LA_ONWATER_SWIM_BACK_TO_IDLE 143, LA_ONWATER_SWIM_LEFT 144, LA_ONWATER_SWIM_RIGHT 145, LA_DEATH_JUMP 146, LA_ROLL_START 147, LA_ROLL_CONTINUE 148, LA_ROLL_END 149, LA_SPIKE_DEATH 150, LA_SWING_IN_FAST 151, LA_SWANDIVE_ROLL 152, LA_SWANDIVE_TO_UNDERWATER 153, LA_FREEFALL_SWANDIVE 154, LA_FREEFALL_SWANDIVE_TO_UNDERWATER 155, LA_SWANDIVE_DEATH 156, LA_SWANDIVE_LEFT 157, LA_SWANDIVE_RIGHT 158, LA_SWANDIVE_START 159, LA_CLIMB_ON_HANDSTAND 207, LA_RUN_JUMP_ROLL_START 208, LA_SOMERSAULT 209, LA_RUN_JUMP_ROLL_END 210, LA_JUMP_FORWARD_ROLL_START 211, LA_JUMP_FORWARD_ROLL_END 212, LA_JUMP_BACK_ROLL_START 213, LA_JUMP_BACK_ROLL_END 203, LA_UNDERWATER_ROLL_START 205, LA_UNDERWATER_ROLL_END 191, LA_ONWATER_TO_STAND_MEDIUM 177, LA_WADE 178, LA_RUN_TO_WADE_LEFT 179, LA_RUN_TO_WADE_RIGHT 180, LA_WADE_TO_RUN_LEFT 181, LA_WADE_TO_RUN_RIGHT 184, LA_WADE_TO_STAND_RIGHT 185, LA_WADE_TO_STAND_LEFT 186, LA_STAND_TO_WADE 190, LA_ONWATER_TO_WADE 193, LA_ONWATER_TO_WADE_LOW 192, LA_UNDERWATER_TO_STAND 198, LA_UNDERWATER_SWIM_TO_STILL_HUDDLE 199, LA_UNDERWATER_SWIM_TO_STILL_SPRAWL 200, LA_UNDERWATER_SWIM_TO_STILL_MEDIUM 218, LA_SLIDE_FORWARD_TO_RUN 219, LA_JUMP_NEUTRAL_ROLL 220, LA_CONTROLLED_DROP 221, LA_CONTROLLED_DROP_CONTINUE 222, LA_HANG_TO_JUMP_UP 223, LA_HANG_TO_JUMP_UP_CONTINUE 224, LA_HANG_TO_JUMP_BACK 225, LA_HANG_TO_JUMP_BACK_CONTINUE 226, LA_SPRINT 227, LA_RUN_TO_SPRINT_LEFT 228, LA_RUN_TO_SPRINT_RIGHT 229, LA_SPRINT_SLIDE_STAND_LEFT 230, LA_SPRINT_SLIDE_STAND_RIGHT 231, LA_SPRINT_TO_ROLL_LEFT 232, LA_SPRINT_ROLL_LEFT_TO_RUN 233, LA_SPRINT_TO_ROLL_RIGHT 234, LA_SPRINT_ROLL_RIGHT_TO_RUN 235, LA_SPRINT_TO_RUN_LEFT 236, LA_SPRINT_TO_RUN_RIGHT 237, LA_POSE_RIGHT_START 238, LA_POSE_RIGHT_CONTINUE 239, LA_POSE_RIGHT_END 240, LA_POSE_LEFT_START 241, LA_POSE_LEFT_CONTINUE 242, LA_POSE_LEFT_END 160, LA_STAND_TO_LADDER 161, LA_LADDER_UP 162, LA_LADDER_UP_STOP_RIGHT 163, LA_LADDER_UP_STOP_LEFT 164, LA_LADDER_IDLE 165, LA_LADDER_UP_START 166, LA_LADDER_DOWN_STOP_LEFT 167, LA_LADDER_DOWN_STOP_RIGHT 168, LA_LADDER_DOWN 169, LA_LADDER_DOWN_START 170, LA_LADDER_RIGHT 171, LA_LADDER_LEFT 172, LA_LADDER_HANG 173, LA_LADDER_HANG_TO_IDLE 174, LA_LADDER_CLIMB_ON 182, LA_LADDER_BACKFLIP_START 183, LA_LADDER_BACKFLIP_CONTINUE 187, LA_LADDER_UP_HANGING 188, LA_LADDER_DOWN_HANGING 194, LA_LADDER_TO_HANG_DOWN 201, LA_LADDER_TO_HANG_RIGHT 202, LA_LADDER_TO_HANG_LEFT 175, LA_UNKNOWN 176, LA_ONWATER_TO_WADE_SHALLOW_UNUSED 189, LA_FLARE_THROW 195, LA_SWITCH_SMALL_DOWN 196, LA_SWITCH_SMALL_UP 197, LA_BUTTON_PUSH 204, LA_FLARE_PICKUP 206, LA_UNDERWATER_FLARE_PICKUP 214, LA_KICK 215, LA_ZIPLINE_GRAB 216, LA_ZIPLINE_RIDE 217, LA_ZIPLINE_FALL 243, LA_STAND_TO_CROUCH 244, LA_STAND_TO_CROUCH_END 245, LA_STAND_TO_CROUCH_ABORT_UNUSED 246, LA_RUN_TO_CROUCH_LEFT_START 247, LA_RUN_TO_CROUCH_LEFT_END 248, LA_RUN_TO_CROUCH_RIGHT_START 249, LA_RUN_TO_CROUCH_RIGHT_END 250, LA_SPRINT_TO_CROUCH_LEFT 251, LA_SPRINT_TO_CROUCH_RIGHT 252, LA_HANG_TO_CROUCH_START 253, LA_HANG_TO_CROUCH_END 254, LA_CROUCH_IDLE 255, LA_CROUCH_TO_STAND 256, LA_CROUCH_PICKUP 257, LA_CROUCH_PICKUP_FLARE 258, LA_CROUCH_HIT_FRONT 259, LA_CROUCH_HIT_BACK 260, LA_CROUCH_HIT_RIGHT 261, LA_CROUCH_HIT_LEFT 262, LA_CROUCH_ROLL_FORWARD_START 263, LA_CROUCH_ROLL_FORWARD_CONTINUE 264, LA_CROUCH_ROLL_FORWARD_END 265, LA_CROUCH_ROLL_FORWARD_START_ALTERNATE_UNUSED 266, LA_CROUCH_TO_CRAWL_START 267, LA_CROUCH_TO_CRAWL_CONTINUE 268, LA_CROUCH_TO_CRAWL_END 269, LA_CRAWL_IDLE 270, LA_CRAWL_TO_CROUCH_START 271, LA_CRAWL_TO_CROUCH_CONTINUE 272, LA_CRAWL_TO_CROUCH_END_UNUSED 273, LA_CRAWL_IDLE_TO_FORWARD 274, LA_CRAWL_FORWARD 275, LA_CRAWL_FORWARD_TO_IDLE_START_RIGHT 276, LA_CRAWL_FORWARD_TO_IDLE_END_RIGHT 277, LA_CRAWL_FORWARD_TO_IDLE_START_LEFT 278, LA_CRAWL_FORWARD_TO_IDLE_END_LEFT 279, LA_CRAWL_TURN_LEFT 280, LA_CRAWL_TURN_RIGHT 281, LA_CRAWL_IDLE_TO_BACKWARD 282, LA_CRAWL_BACKWARD 283, LA_CRAWL_BACKWARD_TO_IDLE_RIGHT_START 284, LA_CRAWL_BACKWARD_TO_IDLE_RIGHT_END 285, LA_CRAWL_BACKWARD_TO_IDLE_LEFT_START 286, LA_CRAWL_BACKWARD_TO_IDLE_LEFT_END 287, LA_CRAWL_TURN_LEFT_EARLY_END 288, LA_CRAWL_TURN_RIGHT_EARLY_END 289, LA_CRAWL_TO_HANG_START 290, LA_CRAWL_TO_HANG_CONTINUE 291, LA_CRAWL_TO_HANG_END 292, LA_CRAWL_PICKUP 293, LA_CRAWL_HIT_FRONT_UNUSED 294, LA_CRAWL_HIT_BACK_UNUSED 295, LA_CRAWL_HIT_RIGHT_UNUSED 296, LA_CRAWL_HIT_LEFT_UNUSED 297, LA_CRAWL_DEATH 298, LA_CRAWL_JUMP_DOWN 299, LA_CROUCH_TURN_LEFT 300, LA_CROUCH_TURN_RIGHT 301, LA_JUMP_FORWARD_START_TO_GRAB_EARLY 302, LA_JUMP_FORWARD_START_TO_GRAB_LATE 303, LA_RUN_TO_GRAB_RIGHT 304, LA_RUN_TO_GRAB_LEFT 305, LA_SWING_IN_SLOW 306, LA_MONKEY_IDLE 307, LA_MONKEY_FALL 308, LA_MONKEY_GRAB 309, LA_MONKEY_FORWARD 310, LA_MONKEY_STOP_LEFT 311, LA_MONKEY_STOP_RIGHT 312, LA_MONKEY_IDLE_TO_FORWARD_LEFT 313, LA_MONKEY_IDLE_TO_FORWARD_RIGHT 314, LA_MONKEY_SHIMMY_LEFT 315, LA_MONKEY_SHIMMY_LEFT_END 316, LA_MONKEY_SHIMMY_RIGHT 317, LA_MONKEY_SHIMMY_RIGHT_END 318, LA_MONKEY_TURN_AROUND 319, LA_MONKEY_TURN_LEFT 320, LA_MONKEY_TURN_RIGHT 321, LA_MONKEY_TURN_LEFT_EARLY_END 322, LA_MONKEY_TURN_LEFT_LATE_END 323, LA_MONKEY_TURN_RIGHT_EARLY_END 324, LA_MONKEY_TURN_RIGHT_LATE_END 325, LA_SPRINT_SLIDE_STAND_RIGHT_END_ALTERNATE_UNUSED 326, LA_SPRINT_SLIDE_STAND_LEFT_END_ALTERNATE_UNUSED 327, LA_SPRINT_TO_ROLL_LEFT_BETA_UNUSED 328, LA_SPRINT_TO_ROLL_ALTERNATE_START_UNUSED 329, LA_SPRINT_TO_ROLL_ALTERNATE_CONTINUE_UNUSED 330, LA_SPRINT_TO_ROLL_ALTERNATE_END_UNUSED 331, LA_LADDER_TO_CROUCH_START 332, LA_LADDER_TO_CROUCH_END ================================================ FILE: data/trx/ship/games/tr2/catalog_lara_states.csv ================================================ 0, LS_WALK 1, LS_RUN 2, LS_STOP 3, LS_JUMP_FORWARD 4, LS_POSE 5, LS_FAST_BACK 6, LS_TURN_RIGHT 7, LS_TURN_LEFT 8, LS_DEATH 9, LS_FAST_FALL 10, LS_HANG 11, LS_REACH 12, LS_SPLAT 13, LS_TREAD 14, LS_LAND 15, LS_COMPRESS 16, LS_WALK_BACK 17, LS_SWIM 18, LS_GLIDE 19, LS_PULL_UP 20, LS_FAST_TURN 21, LS_STEP_RIGHT 22, LS_STEP_LEFT 23, LS_ROLL_CONT 24, LS_SLIDE 25, LS_JUMP_BACK 26, LS_JUMP_RIGHT 27, LS_JUMP_LEFT 28, LS_JUMP_UP 29, LS_FALL_BACK 30, LS_SHIMMY_LEFT 31, LS_SHIMMY_RIGHT 32, LS_SLIDE_BACK 33, LS_SURF_TREAD 34, LS_SURF_SWIM 35, LS_DIVE 36, LS_PUSH_BLOCK 37, LS_PULL_BLOCK 38, LS_PP_READY 39, LS_PICKUP 40, LS_SWITCH_ON 41, LS_SWITCH_OFF 42, LS_USE_KEY 43, LS_USE_PUZZLE 44, LS_UW_DEATH 45, LS_ROLL 46, LS_SPECIAL 47, LS_SURF_BACK 48, LS_SURF_LEFT 49, LS_SURF_RIGHT 50, LS_USE_MIDAS 51, LS_DIE_MIDAS 52, LS_SWAN_DIVE 53, LS_FAST_DIVE 54, LS_GYMNAST 55, LS_WATER_OUT 79, LS_CONTROLLED 68, LS_TWIST 66, LS_WATER_ROLL 65, LS_WADE 71, LS_RESPONSIVE 72, LS_NEUTRAL_ROLL 73, LS_SPRINT 74, LS_SPRINT_ROLL 75, LS_POSE_START 76, LS_POSE_END 77, LS_POSE_LEFT 78, LS_POSE_RIGHT 56, LS_CLIMB_STANCE 57, LS_CLIMBING 58, LS_CLIMB_LEFT 59, LS_CLIMB_END 60, LS_CLIMB_RIGHT 61, LS_CLIMB_DOWN 62, LS_LARA_TEST1 63, LS_LARA_TEST2 64, LS_LARA_TEST3 67, LS_FLARE_PICKUP 69, LS_KICK 70, LS_ZIPLINE 80, LS_CROUCH_IDLE 81, LS_CROUCH_ROLL 82, LS_CRAWL_IDLE 83, LS_CRAWL_FORWARD 84, LS_CRAWL_TURN_LEFT 85, LS_CRAWL_TURN_RIGHT 86, LS_CRAWL_BACK 87, LS_CLIMB_TO_CRAWL 88, LS_CRAWL_TO_CLIMB 89, LS_CRAWL_JUMP_DOWN 90, LS_CROUCH_TURN_LEFT 91, LS_CROUCH_TURN_RIGHT 92, LS_MONKEY_IDLE 93, LS_MONKEY_FORWARD 94, LS_MONKEY_LEFT 95, LS_MONKEY_RIGHT 96, LS_MONKEY_ROLL 97, LS_MONKEY_TURN_LEFT 98, LS_MONKEY_TURN_RIGHT ================================================ FILE: data/trx/ship/games/tr2/catalog_music.csv ================================================ 43, MX_SECRET 18, MX_TR2_GYM_HINT_14 19, MX_TR2_GYM_HINT_15 20, MX_TR2_GYM_HINT_16 21, MX_TR2_GYM_HINT_17 22, MX_TR2_GYM_HINT_18 24, MX_DAGGER_PULL 23, MX_CUTSCENE_BATH 57, MX_REVEAL_1 59, MX_REVEAL_2 48, MX_SKIDOO_THEME 49, MX_BATTLE_THEME ================================================ FILE: data/trx/ship/games/tr2/catalog_objects.csv ================================================ 0, O_LARA 1, O_LARA_PISTOLS 2, O_LARA_HAIR 3, O_LARA_SHOTGUN 4, O_LARA_AUTOS 5, O_LARA_UZIS 6, O_LARA_M16 7, O_LARA_GRENADE_GUN 8, O_LARA_HARPOON_GUN 9, O_LARA_FLARE 10, O_LARA_SKIDOO 11, O_LARA_BOAT 12, O_LARA_EXTRA 13, O_SKIDOO_FAST 14, O_BOAT 15, O_DOG 16, O_CULT_1 17, O_CULT_1A 18, O_CULT_1B 19, O_CULT_2 20, O_CULT_3 21, O_MOUSE 22, O_DRAGON_FRONT 23, O_DRAGON_BACK 24, O_GONDOLA 25, O_SHARK 26, O_EEL 27, O_BIG_EEL 28, O_BARRACUDA 29, O_DIVER 30, O_WORKER_1 31, O_WORKER_2 32, O_WORKER_3 33, O_WORKER_4 34, O_WORKER_5 35, O_JELLY 36, O_SPIDER 37, O_BIG_SPIDER 38, O_CROW 39, O_TIGER 40, O_BARTOLI 41, O_XIAN_SPEARMAN 42, O_XIAN_SPEARMAN_STATUE 43, O_XIAN_KNIGHT 44, O_XIAN_KNIGHT_STATUE 45, O_YETI 46, O_BIRD_GUARDIAN 47, O_EAGLE 48, O_BANDIT_1 49, O_BANDIT_2 50, O_BANDIT_2B 51, O_SKIDOO_ARMED 52, O_SKIDOO_DRIVER 53, O_MONK_1 54, O_MONK_2 55, O_FALLING_BLOCK_1 56, O_FALLING_BLOCK_2 57, O_FALLING_BLOCK_3 58, O_PENDULUM_1 59, O_SPIKES 60, O_ROLLING_BALL_1 61, O_DISC 62, O_DISC_EMITTER 63, O_DRAWBRIDGE 64, O_TEETH_TRAP 65, O_LIFT 66, O_GENERAL 67, O_MOVABLE_BLOCK_1 68, O_MOVABLE_BLOCK_2 69, O_MOVABLE_BLOCK_3 70, O_MOVABLE_BLOCK_4 71, O_BIG_BOWL 72, O_SMASH_OBJECT_1 73, O_SMASH_OBJECT_2 74, O_SMASH_OBJECT_3 75, O_SMASH_OBJECT_4 76, O_PROPELLER_1 77, O_POWER_SAW 78, O_HOOK 79, O_FALLING_CEILING_1 80, O_SPINNING_BLADE 81, O_BLADE 82, O_KILLER_STATUE 83, O_ROLLING_BALL_2 84, O_ICICLE 85, O_SPIKE_WALL 86, O_SPRINGBOARD 87, O_CEILING_SPIKES 88, O_BELL 89, O_WATER_SPRITE 90, O_SNOW_SPRITE 91, O_SKIDOO_TRACK 92, O_SWITCH_TYPE_AIRLOCK 93, O_SWITCH_TYPE_SMALL 94, O_PROPELLER_2 95, O_PROPELLER_3 96, O_PENDULUM_2 97, O_MESH_SWAP_1 98, O_MESH_SWAP_2 99, O_MESH_SWAP_3 100, O_TEXT_BOX 101, O_ROLLING_BALL_3 102, O_ZIPLINE_HANDLE 103, O_SWITCH_TYPE_BUTTON 104, O_SWITCH_TYPE_NORMAL 105, O_SWITCH_TYPE_UW 106, O_DOOR_TYPE_1 107, O_DOOR_TYPE_2 108, O_DOOR_TYPE_3 109, O_DOOR_TYPE_4 110, O_DOOR_TYPE_5 111, O_DOOR_TYPE_6 112, O_DOOR_TYPE_7 113, O_DOOR_TYPE_8 114, O_TRAPDOOR_TYPE_1 115, O_TRAPDOOR_TYPE_2 116, O_TRAPDOOR_TYPE_3 117, O_BRIDGE_FLAT 118, O_BRIDGE_TILT_1 119, O_BRIDGE_TILT_2 120, O_PASSPORT_OPTION 121, O_STOPWATCH_OPTION 122, O_PHOTO_OPTION 123, O_PLAYER_1 124, O_PLAYER_2 125, O_PLAYER_3 126, O_PLAYER_4 127, O_PLAYER_5 128, O_PLAYER_6 129, O_PLAYER_7 130, O_PLAYER_8 131, O_PLAYER_9 132, O_PLAYER_10 133, O_PASSPORT_CLOSED 134, O_PDA_OPTION 135, O_PISTOL_ITEM 136, O_SHOTGUN_ITEM 137, O_AUTOS_ITEM 138, O_UZI_ITEM 139, O_HARPOON_ITEM 140, O_M16_ITEM 141, O_GRENADE_GUN_ITEM 142, O_PISTOL_AMMO_ITEM 143, O_SHOTGUN_AMMO_ITEM 144, O_AUTOS_AMMO_ITEM 145, O_UZI_AMMO_ITEM 146, O_HARPOON_AMMO_ITEM 147, O_M16_AMMO_ITEM 148, O_GRENADE_AMMO_ITEM 149, O_SMALL_MEDIPACK_ITEM 150, O_LARGE_MEDIPACK_ITEM 151, O_FLAREBOX_ITEM 152, O_FLARE_ITEM 153, O_DETAIL_OPTION 154, O_SOUND_OPTION 155, O_CONTROL_OPTION 156, O_GAMMA_OPTION 157, O_PISTOL_OPTION 158, O_SHOTGUN_OPTION 159, O_AUTOS_OPTION 160, O_UZI_OPTION 161, O_HARPOON_OPTION 162, O_M16_OPTION 163, O_GRENADE_GUN_OPTION 164, O_PISTOL_AMMO_OPTION 165, O_SHOTGUN_AMMO_OPTION 166, O_AUTOS_AMMO_OPTION 167, O_UZI_AMMO_OPTION 168, O_HARPOON_AMMO_OPTION 169, O_M16_AMMO_OPTION 170, O_GRENADE_AMMO_OPTION 171, O_SMALL_MEDIPACK_OPTION 172, O_LARGE_MEDIPACK_OPTION 173, O_FLAREBOX_OPTION 174, O_PUZZLE_ITEM_1 175, O_PUZZLE_ITEM_2 176, O_PUZZLE_ITEM_3 177, O_PUZZLE_ITEM_4 178, O_PUZZLE_OPTION_1 179, O_PUZZLE_OPTION_2 180, O_PUZZLE_OPTION_3 181, O_PUZZLE_OPTION_4 182, O_PUZZLE_HOLE_1 183, O_PUZZLE_HOLE_2 184, O_PUZZLE_HOLE_3 185, O_PUZZLE_HOLE_4 186, O_PUZZLE_DONE_1 187, O_PUZZLE_DONE_2 188, O_PUZZLE_DONE_3 189, O_PUZZLE_DONE_4 190, O_SECRET_1 191, O_SECRET_2 192, O_SECRET_3 193, O_KEY_ITEM_1 194, O_KEY_ITEM_2 195, O_KEY_ITEM_3 196, O_KEY_ITEM_4 197, O_KEY_OPTION_1 198, O_KEY_OPTION_2 199, O_KEY_OPTION_3 200, O_KEY_OPTION_4 201, O_KEY_HOLE_1 202, O_KEY_HOLE_2 203, O_KEY_HOLE_3 204, O_KEY_HOLE_4 205, O_PICKUP_ITEM_1 206, O_PICKUP_ITEM_2 207, O_PICKUP_OPTION_1 208, O_PICKUP_OPTION_2 209, O_SPHERE_OF_DOOM_1 210, O_SPHERE_OF_DOOM_2 211, O_SPHERE_OF_DOOM_3 212, O_ALARM_SOUND 213, O_BIRD_TWEETER_1 214, O_TREX 215, O_BIRD_TWEETER_2 216, O_CLOCK_CHIMES 217, O_DRAGON_BONES_1 218, O_DRAGON_BONES_2 219, O_DRAGON_BONES_3 220, O_HOT_LIQUID 221, O_BOAT_BITS 222, O_MINE 223, O_INV_BACKGROUND 224, O_FX_RESERVED 225, O_GONG_BONGER 226, O_GONG 227, O_DETONATOR_BOX 228, O_COPTER 229, O_EXPLOSION_1 230, O_SPLASH_1 231, O_BUBBLE_1 232, O_BUBBLE_EMITTER 233, O_BLOOD 234, O_DART_EFFECT 235, O_FLARE_FIRE 236, O_GLOW 237, O_GLOW_RESERVED 238, O_RICOCHET 239, O_TWINKLE 240, O_GUN_FLASH 241, O_M16_FLASH 242, O_BODY_PART 243, O_CAMERA_TARGET 244, O_WATERFALL 245, O_MISSILE_HARPOON 246, O_MISSILE_FLAME 247, O_MISSILE_KNIFE 248, O_GRENADE 249, O_HARPOON_BOLT 250, O_EMBER 251, O_EMBER_EMITTER 252, O_FLAME 253, O_FLAME_EMITTER 254, O_SKYBOX 255, O_ALPHABET 256, O_DYING_MONK 257, O_DING_DONG 258, O_LARA_ALARM 259, O_MINI_COPTER 260, O_WINSTON 261, O_ASSAULT_DIGITS 262, O_COMBAT_END 263, O_CUT_SHOTGUN 264, O_EARTHQUAKE 265, O_BEAR 266, O_WOLF 267, O_MONK_3 268, O_PICKUP_AID 269, O_SAVE_CRYSTAL_ITEM # Slots 270-274 moved for Lara skins: available for re-use 275, O_SECRET_1_OPTION 276, O_SECRET_2_OPTION 277, O_SECRET_3_OPTION 278, O_ALPHABET_SMALL 279, O_LARA_MAGNUMS 280, O_MAGNUM_OPTION 281, O_MAGNUM_AMMO_OPTION 282, O_MAGNUM_ITEM 283, O_MAGNUM_AMMO_ITEM 284, O_SNOWFLAKE 285, O_LARA_DESERT_EAGLE 286, O_DESERT_EAGLE_OPTION 287, O_DESERT_EAGLE_AMMO_OPTION 288, O_DESERT_EAGLE_ITEM 289, O_DESERT_EAGLE_AMMO_ITEM 290, O_LARA_MP5, 291, O_MP5_OPTION 292, O_MP5_AMMO_OPTION 293, O_MP5_ITEM 294, O_MP5_AMMO_ITEM 295, O_LARA_ROCKET_GUN 296, O_ROCKET_GUN_OPTION 297, O_ROCKET_AMMO_OPTION 298, O_ROCKET 299, O_ROCKET_GUN_ITEM 300, O_ROCKET_AMMO_ITEM 301, O_SHADOW 302, O_LARA_SKIN_SWAP_1 303, O_LARA_SKIN_SWAP_2 304, O_LARA_SKIN_SWAP_3 305, O_LARA_SKIN_SWAP_4 306, O_LARA_SKIN_SWAP_5 307, O_LARA_SKIN_SWAP_6 308, O_LARA_SKIN_SWAP_7 309, O_LARA_SKIN_SWAP_8 310, O_LARA_SKIN_SWAP_9 311, O_LARA_SKIN_SWAP_10 312, O_LARA_SKIN_SWAP_11 313, O_LARA_SKIN_SWAP_12 314, O_LARA_SKIN_SWAP_13 315, O_LARA_SKIN_SWAP_14 316, O_LARA_SKIN_SWAP_15 317, O_LARA_SKIN_SWAP_16 318, O_LARA_SKIN_SWAP_17 319, O_LARA_SKIN_SWAP_18 320, O_LARA_SKIN_SWAP_19 321, O_LARA_SKIN_SWAP_20 322, O_LARA_SKIN_SWAP_21 323, O_LARA_SKIN_SWAP_22 324, O_LARA_SKIN_SWAP_23 325, O_LARA_SKIN_SWAP_24 326, O_LARA_SKIN_SWAP_25 327, O_LARA_SKIN_SWAP_26 328, O_LARA_SKIN_SWAP_27 329, O_LARA_SKIN_SWAP_28 330, O_LARA_SKIN_SWAP_29 331, O_LARA_SKIN_SWAP_30 332, O_LARA_SKIN_SWAP_31 333, O_LARA_SKIN_SWAP_32 334, O_LARA_SKIN_SWAP_EXTRA 335, O_LARA_SKIN_SWAP_GUNS 336, O_LARA_SKIN_SWAP_LEGS 337, O_BLOOD_PINK ================================================ FILE: data/trx/ship/games/tr2/catalog_samples.csv ================================================ 0, SFX_LARA_FOOTSTEP 2, SFX_LARA_NO 6, SFX_LARA_DRAW 7, SFX_LARA_HOLSTER 8, SFX_LARA_PISTOLS 9, SFX_LARA_RELOAD 10, SFX_LARA_RICOCHET 11, SFX_LARA_FLARE_IGNITE 12, SFX_LARA_FLARE_BURN 21, SFX_LARA_AUTOS 24, SFX_MASSIVE_CRASH 27, SFX_LARA_BODYSL 30, SFX_LARA_FALL 31, SFX_LARA_INJURY 36, SFX_LARA_BREATH 37, SFX_LARA_BUBBLES 39, SFX_LARA_KEY 41, SFX_LARA_GENERAL_DEATH 43, SFX_LARA_UZI_FIRE 45, SFX_LARA_SHOTGUN 48, SFX_CLICK 50, SFX_LARA_BULLETHIT 53, SFX_LARA_FALL_DEATH 58, SFX_GLASS_BREAK 60, SFX_UNDERWATER 71, SFX_ENEMY_HIT_1 72, SFX_ENEMY_HIT_2 78, SFX_M16_FIRE 79, SFX_WATERFALL_LOOP 79, SFX_FLOOD 104, SFX_M16_STOP 105, SFX_EXPLOSION_1 105, SFX_EXPLOSION_3 108, SFX_MENU_ROTATE 109, SFX_MENU_LARA_HOME 111, SFX_MENU_CHOOSE 111, SFX_MENU_SPININ 112, SFX_MENU_SPINOUT 113, SFX_MENU_STOPWATCH 114, SFX_MENU_GUNS 115, SFX_MENU_PASSPORT 116, SFX_MENU_MEDI 147, SFX_ROLLING_BALL_1_ROLL 150, SFX_LOOP_FOR_SMALL_FIRES 153, SFX_SKIDOO_IDLE 155, SFX_SKIDOO_MOVING 190, SFX_PULLEY_CRANE 191, SFX_CURTAIN 195, SFX_BOAT_IDLE 197, SFX_BOAT_MOVING 201, SFX_CLATTER_1 202, SFX_CLATTER_2 203, SFX_CLATTER_3 204, SFX_SPIKE_WALL 205, SFX_LARA_FLESH_WOUND 206, SFX_SAW_REVVING 207, SFX_SAW_STOP 208, SFX_DOOR_CHIME 213, SFX_AIRPLANE_IDLE 215, SFX_UNDERWATER_FAN_ON 217, SFX_SMALL_FAN_ON 222, SFX_ROLLING_BALL_2_ROLL 223, SFX_ROLLING_BALL_2_STOP 227, SFX_ROLLING_BALL_3_ROLL 228, SFX_ROLLING_BALL_3_STOP 231, SFX_ROLLING_BLADE 245, SFX_MONK_CRUNCH 254, SFX_DISC 258, SFX_PROJECTILE_HIT 278, SFX_CHAIN_PULLEY 280, SFX_ZIPLINE_GO 281, SFX_ZIPLINE_STOP 284, SFX_BOWL_POUR 285, SFX_WATERFALL_2 297, SFX_HELICOPTER_LOOP 298, SFX_DRAGON_FEET 298, SFX_EARTHQUAKE_1 305, SFX_DRAGON_FIRE 312, SFX_WARRIOR_HOVER 316, SFX_BIRDS_CHIRP 317, SFX_CRUNCH_1 318, SFX_CRUNCH_2 325, SFX_PUSHBLOCK_LAND 325, SFX_EARTHQUAKE_2 329, SFX_DRIPS_REVERB 330, SFX_STAGE_BACKDROP 331, SFX_STONE_DOOR_SLIDE 332, SFX_PLATFORM_ALARM 334, SFX_DOORBELL 335, SFX_BURGLAR_ALARM 336, SFX_BOAT_ENGINE 337, SFX_BOAT_INTO_WATER 338, SFX_BOILER 341, SFX_MARCO_BARTOLLI_TRANSFORM 344, SFX_WINSTON_GRUNT_1 345, SFX_WINSTON_GRUNT_2 346, SFX_WINSTON_GRUNT_3 347, SFX_WINSTON_CUPS 348, SFX_BRITTLE_GROUND_BREAK 349, SFX_SPIDER_EXPLODE 370, SFX_LARA_MAGNUMS 371, SFX_LARA_DESERT_EAGLE 372, SFX_MP5_FIRE 373, SFX_EXPLOSION_2 374, SFX_ROCKET_FIRE 375, SFX_LARA_BAREFOOT # 376 used in animations for Lara knee shuffle ================================================ FILE: data/trx/ship/games/tr2/gameflow.json5 ================================================ { // NOTE: bad changes to this file may result in crashes. // Lines starting with double slashes are comments and are ignored. "engine": 2, "name": "Tomb Raider II", "main_menu_picture": "title_eu.webp", "savegame_file_fmt": "save_tr2_%02d.dat", "demo_version": false, "enable_tr2_item_drops": true, "convert_dropped_guns": true, "title": { "path": "title.tr2", "music_track": 60, "sequence": [ {"type": "display_picture", "path": "legal_eu.webp", "legal": true}, {"type": "play_fmv", "fmv_id": 0}, {"type": "play_fmv", "fmv_id": 1}, {"type": "exit_to_title"}, ], "inherit_injections": false, "injections": [ "font.bin", "inv_background.bin", "pda_model.bin", "title_textures.bin", "misc_sprites.bin", ] }, "sfx_path": "main.sfx", "injections": [ "font.bin", "lara_animations.bin", "pda_model.bin", "pickup_aid.bin", "crystal.bin", "winston_model.bin", "lara_extra.bin", "lara_rifle_sfx.bin", "secret_models_og.bin", "misc_sprites.bin", "lara_outfits.bin", ], "levels": [ // 0. Lara's Home { "type": "gym", "path": "assault.tr2", "script": "assault.lua", "music_track": -1, "lara_outfit": "tr2_gym", "sequence": [ {"type": "loading_screen", "path": "mansion.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "level_stats"}, ], "inherit_injections": false, "injections": [ "font.bin", "gym_fd.bin", "gym_sfx.bin", "gym_music_tracks.bin", "gym_textures.bin", "lara_gym_guns.bin", "lara_animations.bin", "pda_model.bin", "pickup_aid.bin", "misc_sprites.bin", "lara_outfits.bin", ], }, // 1. The Great Wall { "path": "wall.tr2", "music_track": 29, "lara_outfit": "tr2_classic", "sequence": [ {"type": "play_fmv", "fmv_id": 2}, {"type": "loading_screen", "path": "china.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "give_item", "object_id": "shotgun"}, {"type": "give_item", "object_id": "small_medipack"}, {"type": "give_item", "object_id": "large_medipack"}, {"type": "give_item", "object_id": "flare", "quantity": 2}, {"type": "add_secret_reward", "object_id": "grenade_launcher"}, {"type": "add_secret_reward", "object_id": "grenade_launcher_ammo", "quantity": 2}, {"type": "add_secret_reward", "object_id": "small_medipack"}, {"type": "loop_game"}, {"type": "play_cutscene", "cutscene_id": 0}, {"type": "play_music", "music_track": 37}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "common_pickup_meshes.bin", "door106_sfx.bin", "lara_guns.bin", "wall_cameras.bin", "wall_itemrots.bin", "wall_music_tracks.bin", "wall_textures.bin", "wall_crystals.bin", ], }, // 2. Venice { "path": "boat.tr2", "music_track": -1, "lara_outfit": "tr2_classic", "sequence": [ {"type": "loading_screen", "path": "venice.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "add_secret_reward", "object_id": "autos_ammo", "quantity": 4}, {"type": "loop_game"}, {"type": "play_music", "music_track": 37}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "boat_bits.bin", "common_pickup_meshes.bin", "lara_guns.bin", "venice_fd.bin", "venice_itemrots.bin", "venice_music_tracks.bin", "venice_textures.bin", "venice_crystals.bin", ], }, // 3. Bartoli's Hideout { "path": "venice.tr2", "music_track": -1, "lara_outfit": "tr2_classic", "sequence": [ {"type": "loading_screen", "path": "venice.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "enable_sunset"}, {"type": "add_secret_reward", "object_id": "shotgun_ammo", "quantity": 4}, {"type": "loop_game"}, {"type": "play_music", "music_track": 37}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "bartoli_music_tracks.bin", "bartoli_secret_fd.bin", "bartoli_textures.bin", "common_pickup_meshes.bin", "door108_sfx.bin", "lara_guns.bin", "detonator_lights.bin", "bartoli_crystals.bin", ], }, // 4. Opera House { "path": "opera.tr2", "music_track": 27, "lara_outfit": "tr2_classic", "sequence": [ {"type": "loading_screen", "path": "venice.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "add_secret_reward", "object_id": "uzis"}, {"type": "add_secret_reward", "object_id": "uzis_ammo", "quantity": 4}, {"type": "loop_game"}, {"type": "play_cutscene", "cutscene_id": 1}, {"type": "play_music", "music_track": 37}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "common_pickup_meshes.bin", "door108_sfx.bin", "door111_sfx.bin", "lara_guns.bin", "loose_boards_sfx.bin", "opera_fd.bin", "opera_textures.bin", "opera_itemrots.bin", "opera_music_tracks.bin", "opera_sfx.bin", "opera_crystals.bin", ], "unobtainable_kills": 1, }, // 5. Offshore Rig { "path": "rig.tr2", "music_track": 54, "lara_outfit": "tr2_classic", "sequence": [ {"type": "play_fmv", "fmv_id": 3}, {"type": "loading_screen", "path": "rig.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "set_lara_start_anim", "anim": 8}, {"type": "remove_weapons"}, {"type": "add_secret_reward", "object_id": "uzis"}, {"type": "add_secret_reward", "object_id": "uzis_ammo", "quantity": 2}, {"type": "loop_game"}, {"type": "play_music", "music_track": 37}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "door108_sfx.bin", "lara_guns.bin", "rig_itemrots.bin", "rig_music_tracks.bin", "rig_pickup_meshes.bin", "rig_textures.bin", "scuba_sfx.bin", "rig_crystals.bin", ], }, // 6. Diving Area { "path": "platform.tr2", "music_track": 54, "lara_outfit": "tr2_classic", "sequence": [ {"type": "loading_screen", "path": "rig.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "add_secret_reward", "object_id": "uzis_ammo", "quantity": 4}, {"type": "give_item", "object_id": "pistols", "quantity": 1}, {"type": "loop_game"}, {"type": "play_cutscene", "cutscene_id": 2}, {"type": "play_music", "music_track": 37}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "diving_cameras.bin", "diving_itemrots.bin", "diving_music_tracks.bin", "diving_pickup_meshes.bin", "diving_sfx.bin", "diving_textures.bin", "door108_sfx.bin", "lara_guns.bin", "scuba_sfx.bin", "diving_crystals.bin", ], }, // 7. 40 Fathoms { "path": "unwater.tr2", "music_track": 30, "lara_outfit": "tr2_diving_suit", "sequence": [ {"type": "play_fmv", "fmv_id": 4}, {"type": "loading_screen", "path": "titan.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "add_secret_reward", "object_id": "harpoon_gun_ammo", "quantity": 4}, {"type": "loop_game"}, {"type": "play_music", "music_track": 37}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "common_pickup_meshes.bin", "fathoms_goon_sfx.bin", "fathoms_itemrots.bin", "fathoms_music_tracks.bin", "fathoms_secret_fd.bin", "fathoms_plants.bin", "fathoms_textures.bin", "lara_guns.bin", "scuba_sfx.bin", "fathoms_crystals.bin", ], }, // 8. Wreck of the Maria Doria { "path": "keel.tr2", "music_track": 27, "lara_outfit": "tr2_diving_suit", "sequence": [ {"type": "loading_screen", "path": "titan.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "add_secret_reward", "object_id": "grenade_launcher"}, {"type": "add_secret_reward", "object_id": "grenade_launcher_ammo", "quantity": 2}, {"type": "loop_game"}, {"type": "play_music", "music_track": 37}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "lara_guns.bin", "scuba_sfx.bin", "wreck_cameras.bin", "wreck_fd.bin", "wreck_goon_sfx.bin", "wreck_itemrots.bin", "wreck_music_tracks.bin", "wreck_pickup_meshes.bin", "wreck_plants.bin", "wreck_secret_fd.bin", "wreck_textures.bin", "wreck_crystals.bin", ], "unobtainable_kills": 1, }, // 9. Living Quarters { "path": "living.tr2", "music_track": 30, "lara_outfit": "tr2_diving_suit", "sequence": [ {"type": "loading_screen", "path": "titan.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "add_secret_reward", "object_id": "m16_ammo", "quantity": 4}, {"type": "loop_game"}, {"type": "play_music", "music_track": 37}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "lara_guns.bin", "living_deck_goon_sfx.bin", "living_fd.bin", "living_itemrots.bin", "living_music_tracks.bin", "living_pickup_meshes.bin", "living_secret_fd.bin", "living_sfx.bin", "living_textures.bin", "seaweed_collision.bin", "scuba_sfx.bin", "living_crystals.bin", ], }, // 10. The Deck { "path": "deck.tr2", "music_track": 27, "lara_outfit": "tr2_diving_suit", "sequence": [ {"type": "loading_screen", "path": "titan.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "add_secret_reward", "object_id": "grenade_launcher_ammo", "quantity": 4}, {"type": "loop_game"}, {"type": "play_music", "music_track": 37}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "breakable_tile_sfx.bin", "deck_cameras.bin", "deck_fd.bin", "deck_itemrots.bin", "deck_music_tracks.bin", "deck_pickup_meshes.bin", "deck_plants.bin", "deck_secret_fd.bin", "deck_textures.bin", "door110_sfx.bin", "lara_guns.bin", "living_deck_goon_sfx.bin", "scuba_sfx.bin", "deck_crystals.bin", ], }, // 11. Tibetan Foothills { "path": "skidoo.tr2", "music_track": 29, "lara_outfit": "tr2_bomber_jacket", "weather_type": "snow", "sequence": [ {"type": "play_fmv", "fmv_id": 5}, {"type": "loading_screen", "path": "tibet.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "give_item", "object_id": "puzzle_4"}, {"type": "add_secret_reward", "object_id": "uzis_ammo", "quantity": 4}, {"type": "loop_game"}, {"type": "play_music", "music_track": 37}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "common_pickup_meshes.bin", "lara_guns.bin", "tibet_fd.bin", "tibet_itemrots.bin", "tibet_music_tracks.bin", "tibet_textures.bin", "tibet_crystals.bin", ], }, // 12. Barkhang Monastery { "path": "monastry.tr2", "script": "monastry.lua", "music_track": -1, "lara_outfit": "tr2_bomber_jacket", "sequence": [ {"type": "loading_screen", "path": "tibet.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "give_item", "object_id": "puzzle_4"}, {"type": "add_secret_reward", "object_id": "m16_ammo", "quantity": 4}, {"type": "loop_game"}, {"type": "play_music", "music_track": 37}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "barkhang_cameras.bin", "barkhang_fd.bin", "barkhang_itemrots.bin", "barkhang_music_tracks.bin", "barkhang_pickup_meshes.bin", "barkhang_textures.bin", "lara_guns.bin", "barkhang_crystals.bin", ], "unobtainable_pickups": 2, "unobtainable_kills": 1, }, // 13. Catacombs of the Talion { "path": "catacomb.tr2", "music_track": 27, "lara_outfit": "tr2_bomber_jacket", "sequence": [ {"type": "loading_screen", "path": "tibet.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "add_secret_reward", "object_id": "grenade_launcher_ammo", "quantity": 2}, {"type": "add_secret_reward", "object_id": "m16_ammo", "quantity": 2}, {"type": "loop_game"}, {"type": "play_music", "music_track": 37}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "breakable_tile_sfx.bin", "catacombs_fd.bin", "catacombs_itemrots.bin", "catacombs_music_tracks.bin", "catacombs_textures.bin", "common_pickup_meshes.bin", "door108_sfx.bin", "lara_guns.bin", "catacombs_crystals.bin", ], "unobtainable_pickups": 1, }, // 14. Ice Palace { "path": "icecave.tr2", "music_track": 27, "lara_outfit": "tr2_bomber_jacket", "sequence": [ {"type": "loading_screen", "path": "tibet.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "add_secret_reward", "object_id": "grenade_launcher_ammo", "quantity": 4}, {"type": "loop_game"}, {"type": "play_music", "music_track": 37}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "common_pickup_meshes.bin", "door108_sfx.bin", "guardian_death_commands.bin", "lara_guns.bin", "palace_fd.bin", "palace_itemrots.bin", "palace_music_tracks.bin", "palace_secret_fd.bin", "palace_textures.bin", "portcullis_sfx.bin", "palace_crystals.bin", ], }, // 15. Temple of Xian { "path": "emprtomb.tr2", "music_track": 55, "lara_outfit": "tr2_classic", "sequence": [ {"type": "play_fmv", "fmv_id": 6}, {"type": "loading_screen", "path": "china.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "add_secret_reward", "object_id": "uzis_ammo", "quantity": 8}, {"type": "loop_game"}, {"type": "play_cutscene", "cutscene_id": 3}, {"type": "play_music", "music_track": 37}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "door108_sfx.bin", "lara_guns.bin", "portcullis_sfx.bin", "xian_fd.bin", "xian_itemrots.bin", "xian_music_tracks.bin", "xian_pickup_meshes.bin", "xian_sfx.bin", "xian_textures.bin", "xian_crystals.bin", ], "unobtainable_pickups": 1, }, // 16. Floating Islands { "path": "floating.tr2", "script": "floating.lua", "music_track": 55, "lara_outfit": "tr2_classic", "sequence": [ {"type": "loading_screen", "path": "china.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "disable_floor", "height": 9728}, {"type": "add_secret_reward", "object_id": "grenade_launcher_ammo", "quantity": 8}, {"type": "loop_game"}, {"type": "play_music", "music_track": 37}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "lara_guns.bin", "floating_fd.bin", "floating_itemrots.bin", "floating_music_tracks.bin", "floating_pickup_meshes.bin", "floating_textures.bin", "floating_crystals.bin", ], }, // 17. The Dragon's Lair { "path": "xian.tr2", "music_track": 55, "lara_outfit": "tr2_classic", "sequence": [ {"type": "loading_screen", "path": "china.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "play_music", "music_track": 37}, {"type": "level_stats"}, {"type": "play_fmv", "fmv_id": 7}, {"type": "level_complete"}, ], "injections": [ "common_pickup_meshes.bin", "dagger_sprite.bin", "lair_bartolipos.bin", "lair_music_tracks.bin", "lair_textures.bin", "lara_guns.bin", "portcullis_sfx.bin", "lair_crystals.bin", ], }, // 18. Home Sweet Home { "path": "house.tr2", "script": "house.lua", "music_track": -1, "lara_outfit": "tr2_robe", "sequence": [ {"type": "loading_screen", "path": "mansion.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "give_item", "object_id": "key_1"}, {"type": "set_lara_start_anim", "anim": 15}, {"type": "remove_weapons"}, {"type": "remove_ammo"}, {"type": "remove_flares"}, {"type": "remove_medipacks"}, {"type": "loop_game"}, {"type": "play_music", "music_track": 48}, {"type": "level_stats"}, {"type": "level_complete"}, {"type": "display_picture", "credit": true, "path": "credit01.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, {"type": "display_picture", "credit": true, "path": "credit02.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, {"type": "display_picture", "credit": true, "path": "credit03.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, {"type": "display_picture", "credit": true, "path": "credit04.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, {"type": "display_picture", "credit": true, "path": "credit05.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, {"type": "display_picture", "credit": true, "path": "credit06.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, {"type": "display_picture", "credit": true, "path": "credit07.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, {"type": "display_picture", "credit": true, "path": "credit08.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, {"type": "total_stats", "background_path": "end.webp"}, ], "injections": [ "common_pickup_meshes.bin", "explosion.bin", "house_sfx.bin", "house_itemrots.bin", "house_music_tracks.bin", "house_shower_frames.bin", "house_textures.bin", "lara_house_guns.bin", ], }, ], "demos": [ // Demo 1: Venice { "path": "boat.tr2", "music_track": -1, "lara_outfit": "tr2_classic", "sequence": [ {"type": "loading_screen", "path": "venice.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "add_secret_reward", "object_id": "autos_ammo", "quantity": 4}, {"type": "loop_game"}, {"type": "level_complete"}, ], "injections": [ "common_pickup_meshes.bin", "venice_fd.bin", "venice_itemrots.bin", "venice_music_tracks.bin", "venice_textures.bin", "venice_crystals.bin", ], }, // Demo 2: Wreck of the Maria Doria { "path": "keel.tr2", "music_track": 27, "lara_outfit": "tr2_diving_suit", "sequence": [ {"type": "loading_screen", "path": "titan.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "add_secret_reward", "object_id": "grenade_launcher"}, {"type": "add_secret_reward", "object_id": "grenade_launcher_ammo", "quantity": 2}, {"type": "loop_game"}, {"type": "level_complete"}, ], "injections": [ "scuba_sfx.bin", "wreck_fd.bin", "wreck_goon_sfx.bin", "wreck_itemrots.bin", "wreck_music_tracks.bin", "wreck_pickup_meshes.bin", "wreck_plants.bin", "wreck_textures.bin", "wreck_crystals.bin", ], }, // Demo 3: Tibetan Foothills { "path": "skidoo.tr2", "music_track": 29, "weather_type": "snow", "lara_outfit": "tr2_bomber_jacket", "sequence": [ {"type": "loading_screen", "path": "tibet.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "give_item", "object_id": "puzzle_4"}, {"type": "add_secret_reward", "object_id": "uzis_ammo", "quantity": 4}, {"type": "loop_game"}, {"type": "level_complete"}, ], "injections": [ "common_pickup_meshes.bin", "tibet_fd.bin", "tibet_itemrots.bin", "tibet_music_tracks.bin", "tibet_textures.bin", "tibet_crystals.bin", ], }, ], "cutscenes": [ { "path": "cut1.tr2", "music_track": 2, "lara_outfit": "tr2_classic", "sequence": [ {"type": "loop_game"}, ], "inherit_injections": false, "injections": [ "font.bin", "photo.bin", "misc_sprites.bin", "lara_outfits.bin", ], }, { "path": "cut2.tr2", "music_track": 3, "lara_outfit": "tr2_classic", "sequence": [{"type": "loop_game"}], "inherit_injections": false, "injections": [ "cut2_setup.bin", "cut2_textures.bin", "font.bin", "photo.bin", "misc_sprites.bin", "lara_outfits.bin", ], }, { "path": "cut3.tr2", "script": "cut3.lua", "music_track": 4, "lara_outfit": "tr2_classic", "sequence": [{"type": "loop_game"}], "inherit_injections": false, "injections": [ "cut3_setup.bin", "cut3_textures.bin", "font.bin", "photo.bin", "misc_sprites.bin", "lara_outfits.bin", ], }, { "path": "cut4.tr2", "music_track": 26, "lara_outfit": "tr2_classic", "sequence": [ {"type": "loop_game"}, ], "inherit_injections": false, "injections": [ "cut4_setup.bin", "cut4_textures.bin", "font.bin", "photo.bin", "misc_sprites.bin", "lara_outfits.bin", ], }, ], "fmvs": [ {"path": "LOGO.RPL", "legal": true}, {"path": "ANCIENT.RPL"}, {"path": "MODERN.RPL"}, {"path": "LANDING.RPL"}, {"path": "MS.RPL"}, {"path": "CRASH.RPL"}, {"path": "JEEP.RPL"}, {"path": "END.RPL"}, ], "hidden_config": [ "enable_item_examining", // TR2 has no special item descriptions "disable_trex_collision", // TR2 always disables corpse collision "fix_alligator_ai", // TR2 has no alligators "fix_bear_ai", // TR2 has no bears "fix_monkey_pickup_priority", "healthbar_poison_color", "healthbar_poison_color_ps1", "exposurebar_color", "exposurebar_color_ps1", "exposurebar_location", "exposurebar_show_mode", "change_pierre_spawn", "fix_chainblock_secret_sound", "enable_compass_stats", "enable_wading", "restore_ps1_enemies", "fix_speeches_killing_music", "enable_footprints", "fix_pipeman_aim", ], } ================================================ FILE: data/trx/ship/games/tr2/inv_ring.json5 ================================================ [ { "object_id": "O_SMALL_MEDIPACK_OPTION", "frames_total": 26, "open_frame": 25, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 4032, "x_rot_sel": -7296, "y_rot_sel": -4096, "z_trans_sel": 216, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 14, }, { "object_id": "O_LARGE_MEDIPACK_OPTION", "frames_total": 20, "open_frame": 19, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3616, "x_rot_sel": -8160, "y_rot_sel": -4096, "z_trans_sel": 352, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 13, }, { "object_id": "O_FLAREBOX_OPTION", "frames_total": 31, "open_frame": 30, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "y_rot_sel": -8192, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 12, }, { "object_id": "O_PISTOL_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": 2848, "y_rot_sel": -32768, "y_trans_sel": 38, "z_trans_sel": 352, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 1, }, { "object_id": "O_SHOTGUN_OPTION", "frames_total": 13, "open_frame": 12, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": 5120, "y_rot_sel": 30720, "z_trans_sel": 228, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 2, }, { "object_id": "O_MAGNUM_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": 3360, "y_rot_sel": -32768, "z_trans_sel": 362, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 3, }, { "object_id": "O_AUTOS_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": 3360, "y_rot_sel": -32768, "z_trans_sel": 362, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 4, }, { "object_id": "O_DESERT_EAGLE_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": 3360, "y_rot_sel": -32768, "z_trans_sel": 362, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 5, }, { "object_id": "O_UZI_OPTION", "frames_total": 13, "open_frame": 12, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": 2336, "y_rot_sel": -32768, "y_trans_sel": 56, "z_trans_sel": 322, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 6, }, { "object_id": "O_HARPOON_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -736, "y_rot_sel": -19456, "y_trans_sel": 58, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 11, }, { "object_id": "O_M16_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -224, "y_rot_sel": -18432, "y_trans_sel": 84, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 7, }, { "object_id": "O_MP5_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -224, "y_rot_sel": -18432, "y_trans_sel": 84, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 8, }, { "object_id": "O_ROCKET_GUN_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -224, "y_rot_sel": 14336, "y_trans_sel": 56, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 9, }, { "object_id": "O_GRENADE_GUN_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -224, "y_rot_sel": 14336, "y_trans_sel": 56, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 10, }, { "object_id": "O_PISTOL_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 1, }, { "object_id": "O_SHOTGUN_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 2, }, { "object_id": "O_MAGNUM_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 3, }, { "object_id": "O_AUTOS_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 4, }, { "object_id": "O_DESERT_EAGLE_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 5, }, { "object_id": "O_UZI_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 6, }, { "object_id": "O_HARPOON_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 10, }, { "object_id": "O_M16_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 7, }, { "object_id": "O_MP5_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 8, }, { "object_id": "O_ROCKET_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 9, }, { "object_id": "O_GRENADE_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 10, }, { "object_id": "O_SCION_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 109, }, { "object_id": "O_LEADBAR_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3616, "x_rot_sel": -8160, "y_rot_sel": -4096, "z_trans_sel": 352, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 100, }, { "object_id": "O_PICKUP_OPTION_1", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 111, }, { "object_id": "O_PICKUP_OPTION_2", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 110, }, { "object_id": "O_PUZZLE_OPTION_1", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 108, }, { "object_id": "O_PUZZLE_OPTION_2", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 107, }, { "object_id": "O_PUZZLE_OPTION_3", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 106, }, { "object_id": "O_PUZZLE_OPTION_4", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 105, }, { "object_id": "O_KEY_OPTION_1", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 101, }, { "object_id": "O_KEY_OPTION_2", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 102, }, { "object_id": "O_KEY_OPTION_3", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 103, }, { "object_id": "O_KEY_OPTION_4", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 104, }, { "object_id": "O_STOPWATCH_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "y_trans_sel": -135, "z_trans_sel": 320, "meshes_sel": -1, "meshes_drawn": -1, }, { "object_id": "O_COMPASS_OPTION", "frames_total": 25, "open_frame": 10, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 4352, "x_rot_sel": -8192, "z_trans_sel": 456, "meshes_sel": 0b00000101, "meshes_drawn": 0b00000101, }, { "object_id": "O_PASSPORT_OPTION", "frames_total": 30, "open_frame": 14, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 4640, "x_rot_sel": -4320, "z_trans_sel": 384, "meshes_sel": 0b00010011, "meshes_drawn": 0b00010011, "inv_pos": 200, }, { "object_id": "O_DETAIL_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 4224, "x_rot_sel": -7232, "y_trans_sel": 16, "z_trans_sel": 444, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 201, }, { "object_id": "O_SOUND_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 4832, "x_rot_sel": -5408, "y_rot_sel": -3072, "y_trans_sel": -2, "z_trans_sel": 350, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 202, }, { "object_id": "O_CONTROL_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 5504, "x_rot_sel": -2560, "x_rot_nosel": 5632, "y_rot_sel": 13312, "y_trans_sel": 46, "z_trans_sel": 508, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 203, }, { "object_id": "O_PHOTO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 4640, "x_rot_sel": -4320, "z_trans_sel": 384, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 205, }, { "object_id": "O_PDA_OPTION", "frames_total": 39, "open_frame": 19, "anim_direction": 1, "x_rot_pt_sel": 4640, "z_trans_sel": 384, "meshes_sel": 0b00000011, "meshes_drawn": 0b00000011, "inv_pos": 204, }, ] ================================================ FILE: data/trx/ship/games/tr2/scripts/assault.lua ================================================ trx.events.on_game_start(function(level, is_save) trx.lara.holsters_visible = trx.lara.has_pistol_weapon -- TODO: remove in TRX 1.5. if is_save then return end local records = trx.assault_stats.list_records() if #records > 1 then trx.music.play(22) else trx.music.play(5) end end) ================================================ FILE: data/trx/ship/games/tr2/scripts/cut3.lua ================================================ local suit_change_anim = 7 local outfit_changed = false trx.events.after_control(function() local lara_item = trx.lara.item if lara_item.anim >= suit_change_anim and not outfit_changed then trx.lara.outfit = "tr2_diving_suit" outfit_changed = true end end) ================================================ FILE: data/trx/ship/games/tr2/scripts/floating.lua ================================================ trx.events.after_level_file(function(level) trx.objects.swap_mesh(trx.catalog.objects.secret_2_option, trx.catalog.objects.secret_3_option, 0, 0) end) ================================================ FILE: data/trx/ship/games/tr2/scripts/house.lua ================================================ trx.events.on_game_start(function(level, is_save) trx.lara.holsters_visible = trx.lara.has_pistol_weapon if trx.lara.extra_anim == -1 then -- TODO: remove in TRX 1.5. trx.lara.set_extra_equipment(trx.lara.mesh.hips, trx.lara.extra_mesh.dagger_hips) end end) ================================================ FILE: data/trx/ship/games/tr2/scripts/level1.lua ================================================ trx.events.before_level_file(function(level) trx.creatures.add_ally(trx.catalog.objects.monk_1) trx.creatures.add_ally_target(trx.catalog.objects.bandit_2) end) ================================================ FILE: data/trx/ship/games/tr2/scripts/level3.lua ================================================ trx.events.before_level_file(function(level) trx.creatures.add_ally(trx.catalog.objects.monk_2) trx.creatures.add_ally_target(trx.catalog.objects.bandit_1) trx.creatures.add_ally_target(trx.catalog.objects.bandit_2) end) ================================================ FILE: data/trx/ship/games/tr2/scripts/level4.lua ================================================ trx.events.before_level_file(function(level) trx.creatures.add_ally(trx.catalog.objects.monk_2) trx.creatures.add_ally_target(trx.catalog.objects.bandit_2) end) ================================================ FILE: data/trx/ship/games/tr2/scripts/monastry.lua ================================================ trx.events.before_level_file(function(level) trx.creatures.add_ally(trx.catalog.objects.monk_1) trx.creatures.add_ally(trx.catalog.objects.monk_2) trx.creatures.add_ally_target(trx.catalog.objects.bandit_1) trx.creatures.add_ally_target(trx.catalog.objects.bandit_2) end) ================================================ FILE: data/trx/ship/games/tr2/strings-de.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "settings": { "gameplay.fix_bear_ai": { "description": "Fixt, dass die aufrecht stehend ausgeführten Prankenhiebe von Bären Lara, im Addon 'Die Goldene Maske', nicht mehr verfehlen.", }, "visuals.fix_animated_sprites": { "description": "\\{review}Fixt originale Fehler in 40 Faden, Das Wrack der Maria Doria und An Deck, bei denen Pflanzen-Sprites in Wassergebieten nicht animiert sind.", } }, "objects": { "large_medipack": { "name": "Großes Medi-Pack", }, "small_medipack": { "name": "Kleines Medi-Pack", } }, "cutscenes": [ { "title": "Cutscene 1", }, { "title": "Cutscene 2", }, { "title": "Cutscene 3", }, { "title": "Cutscene 4", } ], "levels": [ { "title": "Laras Haus", }, { "title": "Die Große Mauer", "objects": { "key_1": { "name": "Wachstuben-Schlüssel", }, "key_2": { "name": "Rostiger Schlüssel", } } }, { "title": "Venedig", "objects": { "key_1": { "name": "Bootshaus-Schlüssel", }, "key_2": { "name": "Stählernder Schlüssel", }, "key_3": { "name": "Eiserner Schlüssel", } } }, { "title": "Bartolis Versteck", "objects": { "key_1": { "name": "Bibliothek-Schlüssel", }, "key_2": { "name": "Sprengschlüssel", } } }, { "title": "Das Opernhaus", "objects": { "key_1": { "name": "Ornament-Schlüssel", }, "puzzle_1": { "name": "Relais", }, "puzzle_2": { "name": "Schaltkteis", } } }, { "title": "Der Bohrturm", "objects": { "key_1": { "name": "Rote Karte", }, "key_2": { "name": "Gelbe Karte", }, "key_3": { "name": "Grüne Karte", } } }, { "title": "Die Tiefe", "objects": { "key_1": { "name": "Rote Karte", }, "key_4": { "name": "Blaue Karte", }, "puzzle_1": { "name": "Chip", } } }, { "title": "40 Faden", }, { "title": "Das Wrack der Maria Doria", "objects": { "key_1": { "name": "Waschraumschlüssel", }, "key_2": { "name": "Rostiger Schlüssel", }, "key_3": { "name": "Kabinen-Schlüssel", }, "puzzle_1": { "name": "Unterbrecher", } } }, { "title": "Die Quatiere", "objects": { "key_1": { "name": "Theater-Schlüssel", }, "key_2": { "name": "Rostiger Schlüssel", } } }, { "title": "An Deck", "objects": { "key_2": { "name": "Stern-Schlüssel", }, "key_3": { "name": "Lager-Schlüssel", }, "key_4": { "name": "Kabinen-Schlüssel", }, "puzzle_4": { "name": "Seraph", } } }, { "title": "Das tibetanische Hochland", "objects": { "tiger": { "name": "Schneeleopard", }, "key_1": { "name": "Zugbrücken-Schlüssel", }, "key_2": { "name": "Hüttenschlüssel", }, "puzzle_4": { "name": "Seraph", } } }, { "title": "Das Kloster von Barkhang", "objects": { "key_1": { "name": "Kammer-Schlüssel", }, "key_2": { "name": "Falltür-Schlüssel", }, "key_3": { "name": "Dachboden-Schlüssel", }, "key_4": { "name": "Hallen-Schlüssel", }, "puzzle_1": { "name": "Gebets-Rolle", }, "puzzle_2": { "name": "Edelstein", }, "puzzle_4": { "name": "Seraph", } } }, { "title": "Die Katakomben des Talion", "objects": { "tiger": { "name": "Schneeleopard", }, "pickup_1": { "name": "Gong-Hammer", }, "puzzle_1": { "name": "Tibetanische Maske", } } }, { "title": "Eispalast", "objects": { "tiger": { "name": "Weißer Tiger", }, "key_2": { "name": "Gong-Hammer", }, "pickup_2": { "name": "Talion", }, "puzzle_1": { "name": "Tibetanische Maske", } } }, { "title": "Der Tempel von Xian", "objects": { "key_2": { "name": "Goldener Schlüssel", }, "key_3": { "name": "Silberner Schlüssel", }, "key_4": { "name": "Kammer-Schlüssel", }, "puzzle_1": { "name": "Siegel des Drachen", } } }, { "title": "Die schwimmenden Inseln", "objects": { "puzzle_1": { "name": "Mystische Plakette", }, "puzzle_2": { "name": "Mystische Plakette", } } }, { "title": "Der Hort des Drachen", "objects": { "puzzle_1": { "name": "Mystische Plakette", }, "puzzle_2": { "name": "Dolch von Xian", } } }, { "title": "Zuhause", "objects": { "key_1": { "name": "Waffenschrank-Schlüssel", }, "puzzle_1": { "name": "Dolch von Xian", } } } ], "demos": [ { "title": "Venedig", "objects": { "key_1": { "name": "Bootshaus-Schlüssel", }, "key_2": { "name": "Stählerner Schlüssel", }, "key_3": { "name": "Eiserner Schlüssel", } } }, { "title": "Das Wrack der Maria Doria", "objects": { "key_1": { "name": "Waschraum-Schlüssel", }, "key_2": { "name": "Rostiger Schlüssel", }, "key_3": { "name": "Kabinen-Schlüssel", }, "puzzle_1": { "name": "Unterbrecher", } } }, { "title": "Das tibetanische Hochland", "objects": { "tiger": { "name": "Schneeleopard", }, "key_1": { "name": "Zugbrücken-Schlüssel", }, "key_2": { "name": "Hüttenschlüssel", }, "puzzle_4": { "name": "Seraph", } } } ] } ================================================ FILE: data/trx/ship/games/tr2/strings-en-gb.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "extends": "en", "levels": [ {}, {}, {}, {}, {}, {}, {}, {}, {}, { "objects": { "key_1": { "name": "Theatre Key", } } }, {}, {}, {}, {}, {}, {}, {}, {}, {} ] } ================================================ FILE: data/trx/ship/games/tr2/strings-fr.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "settings": { "gameplay.fix_bear_ai": { "description": "\\{review}Corrige l'attaque de patte d'ours dans The Golden Mask pour qu'elle ne manque pas Lara.", }, "visuals.fix_animated_sprites": { "description": "\\{review}Corrige le fait que les sprites des végétations ne s'animent pas dans les zones aquatiques du 40 brasses, Épave du Maria Doria et Le Pont.", } }, "objects": { "large_medipack": { "name": "\\{review}Grand medipack", }, "small_medipack": { "name": "\\{review}Petit medipack", } }, "cutscenes": [ { "title": "\\{review}Scène cinématique 1", }, { "title": "\\{review}Scène cinématique 2", }, { "title": "\\{review}Scène cinématique 3", }, { "title": "\\{review}Scène cinématique 4", } ], "levels": [ { "title": "\\{review}Demeure de Lara", }, { "title": "\\{review}La Grande Muraille", "objects": { "key_1": { "name": "\\{review}Clé de la Garde", }, "key_2": { "name": "\\{review}Clé Rouillée", } } }, { "title": "\\{review}Venise", "objects": { "key_1": { "name": "\\{review}Clé du Hangar à Bateaux", }, "key_2": { "name": "\\{review}Clé en Acier", }, "key_3": { "name": "\\{review}Clé en Fer", } } }, { "title": "\\{review}La cache de Bartoli", "objects": { "key_1": { "name": "\\{review}Clé de la Bibliothèque", }, "key_2": { "name": "\\{review}Clé de Détonateur", } } }, { "title": "\\{review}L’Opéra", "objects": { "key_1": { "name": "\\{review}Clé Ornate", }, "puzzle_1": { "name": "\\{review}Boîte de Relais", }, "puzzle_2": { "name": "\\{review}Carte de Circuit", } } }, { "title": "\\{review}La plate-forme offshore", "objects": { "key_1": { "name": "\\{review}Carte d'Accès Rouge", }, "key_2": { "name": "\\{review}Carte d'Accès Jaune", }, "key_3": { "name": "\\{review}Carte d'Accès Verte", } } }, { "title": "\\{review}Zone de plongée", "objects": { "key_1": { "name": "\\{review}Carte d'Accès Rouge", }, "key_4": { "name": "\\{review}Carte d'Accès Bleue", }, "puzzle_1": { "name": "\\{review}Puce de Machine", } } }, { "title": "\\{review}40 brasses", }, { "title": "\\{review}Épave du Maria Doria", "objects": { "key_1": { "name": "\\{review}Clé de la Salle de Repos", }, "key_2": { "name": "\\{review}Clé Rouillée", }, "key_3": { "name": "\\{review}Clé de Cabine", }, "puzzle_1": { "name": "\\{review}Disjoncteur", } } }, { "title": "\\{review}Quartiers d'habitation", "objects": { "key_1": { "name": "\\{review}Clé de Théâtre", }, "key_2": { "name": "\\{review}Clé Rouillée", } } }, { "title": "\\{review}Le Pont", "objects": { "key_2": { "name": "\\{review}Clé de Poupe", }, "key_3": { "name": "\\{review}Clé de Stockage", }, "key_4": { "name": "\\{review}Clé de Cabine", }, "puzzle_4": { "name": "\\{review}Le Séraphin", } } }, { "title": "\\{review}Les Contreforts tibétains", "objects": { "tiger": { "name": "\\{review}Léopard des Neiges", }, "key_1": { "name": "\\{review}Clé du Pont-Levis", }, "key_2": { "name": "\\{review}Clé de la Hutte", }, "puzzle_4": { "name": "\\{review}Le Séraphin", } } }, { "title": "\\{review}Le Monastère de Barkhang", "objects": { "key_1": { "name": "\\{review}Clé de Chambre Forte", }, "key_2": { "name": "\\{review}Clé de Trappe", }, "key_3": { "name": "\\{review}Clé des Toits", }, "key_4": { "name": "\\{review}Clé du Hall Principal", }, "puzzle_1": { "name": "\\{review}Moulins à Prière", }, "puzzle_2": { "name": "\\{review}Gemmes", }, "puzzle_4": { "name": "\\{review}Le Séraphin", } } }, { "title": "\\{review}Les Catacombes du Talion", "objects": { "tiger": { "name": "\\{review}Léopard des neiges", }, "pickup_1": { "name": "\\{review}Marteau de gong", }, "puzzle_1": { "name": "\\{review}Masque tibétain", } } }, { "title": "\\{review}Le Palais de glace", "objects": { "tiger": { "name": "\\{review}Tigre blanc", }, "key_2": { "name": "\\{review}Marteau de gong", }, "pickup_2": { "name": "\\{review}Talion", }, "puzzle_1": { "name": "\\{review}Masque tibétain", } } }, { "title": "\\{review}Le Temple de Xian", "objects": { "key_2": { "name": "\\{review}Clé en or", }, "key_3": { "name": "\\{review}Clé en argent", }, "key_4": { "name": "\\{review}Clé de la chambre principale", }, "puzzle_1": { "name": "\\{review}Le sceau du dragon", } } }, { "title": "\\{review}Les Îles flottantes", "objects": { "puzzle_1": { "name": "\\{review}Plaque mystique", }, "puzzle_2": { "name": "\\{review}Plaque mystique", } } }, { "title": "\\{review}L’Antre du dragon", "objects": { "puzzle_1": { "name": "\\{review}Plaque mystique", }, "puzzle_2": { "name": "\\{review}Dague de Xian", } } }, { "title": "\\{review}Home Sweet Home", "objects": { "key_1": { "name": "\\{review}Clé du placard à armes", }, "puzzle_1": { "name": "\\{review}Dague de Xian", } } } ], "demos": [ { "title": "\\{review}Venise", "objects": { "key_1": { "name": "\\{review}Clé du bateau", }, "key_2": { "name": "\\{review}Clé en acier", }, "key_3": { "name": "\\{review}Clé en fer", } } }, { "title": "\\{review}É épave de la Maria Doria", "objects": { "key_1": { "name": "\\{review}Clé de la salle de repos", }, "key_2": { "name": "\\{review}Clé rouillée", }, "key_3": { "name": "\\{review}Clé de la cabane", }, "puzzle_1": { "name": "\\{review}Disjoncteur", } } }, { "title": "\\{review}Contreforts tibétains", "objects": { "tiger": { "name": "\\{review}Léopard des neiges", }, "key_1": { "name": "\\{review}Clé du pont-levis", }, "key_2": { "name": "\\{review}Clé de la hutte", }, "puzzle_4": { "name": "\\{review}Le séraphin", } } } ] } ================================================ FILE: data/trx/ship/games/tr2/strings-gd.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "settings": { "gameplay.fix_bear_ai": { "description": "Càraichidh ionnsaigh a' mhathain anns a' Mhasg Òir gus nach caill e Lara.", }, "visuals.fix_animated_sprites": { "description": "Càraichidh bugan ann an 40 Famhair, Long-bhriste na Maria Doria agus An Deic far nach gluais planntrais anns an uisge.", } }, "objects": { "large_medipack": { "name": "Pasgan Mòr Slàinte", }, "small_medipack": { "name": "Pasgan Beag Slàinte", } }, "cutscenes": [ { "title": "Sealladh-film 1", }, { "title": "Sealladh-film 2", }, { "title": "Sealladh-film 3", }, { "title": "Sealladh-film 4", } ], "levels": [ { "title": "Dachaigh Lara", }, { "title": "Am Balla Mòr", "objects": { "key_1": { "name": "Iuchair an Gheàrd-taighe", }, "key_2": { "name": "Iuchair Meirgeach", } } }, { "title": "Venise", "objects": { "key_1": { "name": "Iuchair an Taigh-bàta", }, "key_2": { "name": "Iuchair Stàilinn", }, "key_3": { "name": "Iuchair Iarainn", } } }, { "title": "Taigh-falaich Bartoli", "objects": { "key_1": { "name": "Iuchair an Leabharlainn", }, "key_2": { "name": "Iuchair an Spreadhaichear", } } }, { "title": "Taigh na h-Opera", "objects": { "key_1": { "name": "Iuchair Grinn", }, "puzzle_1": { "name": "Bogsa-iomlaid", }, "puzzle_2": { "name": "Bòrd-chuairt", } } }, { "title": "Inneal-ola Farraige", "objects": { "key_1": { "name": "Cairt-pasa Dearg", }, "key_2": { "name": "Cairt-pasa Buidhe", }, "key_3": { "name": "Cairt-pasa Uaine", } } }, { "title": "Roinn Dàibheadh", "objects": { "key_1": { "name": "Cairt-pasa Dearg", }, "key_4": { "name": "Cairt-pasa Gorm", }, "puzzle_1": { "name": "Sgealb", } } }, { "title": "40 Famhair", }, { "title": "Long-bhriste na Maria Doria", "objects": { "key_1": { "name": "Iuchair an t-Seòmair Fois", }, "key_2": { "name": "Iuchair Meirgeach", }, "key_3": { "name": "Iuchair a' Chaibine", }, "puzzle_1": { "name": "Briseadair-cuairt", } } }, { "title": "Na Còmhnaidhean", "objects": { "key_1": { "name": "Iuchair an Taigh-cluiche", }, "key_2": { "name": "Iuchair Meirgeach", } } }, { "title": "An Deic", "objects": { "key_2": { "name": "Iuchair an Stìùir", }, "key_3": { "name": "Iuchair an Stòrais", }, "key_4": { "name": "Iuchair a' Chaibine", }, "puzzle_4": { "name": "An t-Seraph", } } }, { "title": "Bràighean Tìbeata", "objects": { "tiger": { "name": "Lìopaird-shneachda", }, "key_1": { "name": "Iuchair an Drochaid-tarraing", }, "key_2": { "name": "Iuchair na Bothain", }, "puzzle_4": { "name": "An t-Seraph", } } }, { "title": "Manachainn Barkhang", "objects": { "key_1": { "name": "Iuchair an t-Seòmair Làidir", }, "key_2": { "name": "Iuchair an Làr-lùbte", }, "key_3": { "name": "Iuchair nam Mullaichean", }, "key_4": { "name": "Iuchair na Prìomh Thalla", }, "puzzle_1": { "name": "Cuibhlichean-ùrnaigh", }, "puzzle_2": { "name": "Clachan-luaidh", }, "puzzle_4": { "name": "An t-Seraph", } } }, { "title": "Uaimhean an Talion", "objects": { "tiger": { "name": "Lìopaird-shneachda", }, "pickup_1": { "name": "Òrd a' Ghong", }, "puzzle_1": { "name": "Masg Tìbeata", } } }, { "title": "Lùchairt Deighe", "objects": { "tiger": { "name": "Tìgear Geal", }, "key_2": { "name": "Òrd a' Ghong", }, "pickup_2": { "name": "An Talion", }, "puzzle_1": { "name": "Masg Tìbeata", } } }, { "title": "Teampall Xian", "objects": { "key_2": { "name": "Iuchair Òir", }, "key_3": { "name": "Iuchair Airgid", }, "key_4": { "name": "Iuchair na Prìomh Sheòmair", }, "puzzle_1": { "name": "Ròn an Dràgon", } } }, { "title": "Eileanan Fleòdraidh", "objects": { "puzzle_1": { "name": "Plaic Dhìomhair", }, "puzzle_2": { "name": "Plaic Dhìomhair", } } }, { "title": "Uaimh an Dràgon", "objects": { "puzzle_1": { "name": "Plaic Dhìomhair", }, "puzzle_2": { "name": "Biodag Xian", } } }, { "title": "Dachaigh, mo Ghràdh", "objects": { "key_1": { "name": "Iuchair an Clòsaid-armachd", }, "puzzle_1": { "name": "Biodag Xian", } } } ], "demos": [ { "title": "Venise", "objects": { "key_1": { "name": "Iuchair an Taigh-bàta", }, "key_2": { "name": "Iuchair Stàilinn", }, "key_3": { "name": "Iuchair Iarainn", } } }, { "title": "Long-bhriste na Maria Doria", "objects": { "key_1": { "name": "Iuchair an t-Seòmair Fois", }, "key_2": { "name": "Iuchair Meirgeach", }, "key_3": { "name": "Iuchair a' Chaibine", }, "puzzle_1": { "name": "Briseadair-cuairt", } } }, { "title": "Bràighean Tìbeata", "objects": { "tiger": { "name": "Lìopaird-shneachda", }, "key_1": { "name": "Iuchair an Drochaid-tarraing", }, "key_2": { "name": "Iuchair na Bothain", }, "puzzle_4": { "name": "An t-Seraph", } } } ] } ================================================ FILE: data/trx/ship/games/tr2/strings-it.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "settings": { "gameplay.fix_bear_ai": { "description": "Risolve il problema nel pacchetto di espansione La Maschera Dorata per cui gli attacchi con la zampa degli orsi non colpiscono Lara.", }, "visuals.fix_animated_sprites": { "description": "Risolve i problemi preesistenti nei livelli \"In Profondità\", \"Relitto della Maria Doria\" e \"Sul Ponte\" per cui le animazioni degli sprite delle piante nelle aree acquatiche non funzionano.", } }, "objects": { "large_medipack": { "name": "Kit Medico Grande", }, "small_medipack": { "name": "Kit Medico Piccolo", } }, "cutscenes": [ { "title": "Intermezzo 1", }, { "title": "Intermezzo 2", }, { "title": "Intermezzo 3", }, { "title": "Intermezzo 4", } ], "levels": [ { "title": "Casa di Lara", }, { "title": "La Grande Muraglia", "objects": { "key_1": { "name": "Chiave della Guardiola", }, "key_2": { "name": "Chiave Arrugginita", } } }, { "title": "Venezia", "objects": { "key_1": { "name": "Chiave della Rimessa", }, "key_2": { "name": "Chiave d'Acciaio", }, "key_3": { "name": "Chiave di Ferro", } } }, { "title": "Covo di Bartoli", "objects": { "key_1": { "name": "Chiave della Biblioteca", }, "key_2": { "name": "Chiave del Detonatore", } } }, { "title": "Teatro dell'Opera", "objects": { "key_1": { "name": "Chiave Cesellata", }, "puzzle_1": { "name": "Scatola Fusibili", }, "puzzle_2": { "name": "Circuito d'Avvio", } } }, { "title": "Piattaforma Offshore", "objects": { "key_1": { "name": "Tessera Rossa", }, "key_2": { "name": "Tessera Gialla", }, "key_3": { "name": "Tessera Verde", } } }, { "title": "Area di immersione", "objects": { "key_1": { "name": "Tessera Rossa", }, "key_4": { "name": "Tessera Blu", }, "puzzle_1": { "name": "Chip", } } }, { "title": "In Profondità", }, { "title": "Relitto della Maria Doria", "objects": { "key_1": { "name": "Chiave del Guardaroba", }, "key_2": { "name": "Chiave Arrugginita", }, "key_3": { "name": "Chiave della Cabina", }, "puzzle_1": { "name": "Interruttore di Circuito", } } }, { "title": "Alloggi", "objects": { "key_1": { "name": "Chiave del Teatro", }, "key_2": { "name": "Chiave Arrugginita", } } }, { "title": "Sul Ponte", "objects": { "key_2": { "name": "Chiave di Poppa", }, "key_3": { "name": "Chiave del Ripostiglio", }, "key_4": { "name": "Chiave della Cabina", }, "puzzle_4": { "name": "Il Serafo", } } }, { "title": "Pendici Tibetane", "objects": { "tiger": { "name": "Leopardo delle Nevi", }, "key_1": { "name": "Chiave del Ponte", }, "key_2": { "name": "Chiave della Capanna", }, "puzzle_4": { "name": "Il Serafo", } } }, { "title": "Monastero di Barkhang", "objects": { "key_1": { "name": "Chiave del Caveau", }, "key_2": { "name": "Chiave della Botola", }, "key_3": { "name": "Chiave del Solaio", }, "key_4": { "name": "Chiave dell’Atrio", }, "puzzle_1": { "name": "Ruote della Preghiera", }, "puzzle_2": { "name": "Gemme", }, "puzzle_4": { "name": "Il Serafo", } } }, { "title": "Catacombe del Talion", "objects": { "tiger": { "name": "Leopardo delle Nevi", }, "pickup_1": { "name": "Martello del Gong", }, "puzzle_1": { "name": "Maschera Tibetana", } } }, { "title": "Palazzo di Ghiaccio", "objects": { "tiger": { "name": "Tigre Bianca", }, "key_2": { "name": "Martello del Gong", }, "pickup_2": { "name": "Talion", }, "puzzle_1": { "name": "Maschera Tibetana", } } }, { "title": "Tempio dello Xian", "objects": { "key_2": { "name": "Chiave d'Oro", }, "key_3": { "name": "Chiave d'Argento", }, "key_4": { "name": "Chiave Camera Principale", }, "puzzle_1": { "name": "Sigillo del Drago", } } }, { "title": "Isole Galleggianti", "objects": { "puzzle_1": { "name": "Piastra Mistica", }, "puzzle_2": { "name": "Piastra Mistica", } } }, { "title": "La Tana del Drago", "objects": { "puzzle_1": { "name": "Piastra Mistica", }, "puzzle_2": { "name": "Pugnale di Xian", } } }, { "title": "Casa Dolce Casa", "objects": { "key_1": { "name": "Chiave Arsenale", }, "puzzle_1": { "name": "Pugnale di Xian", } } } ], "demos": [ { "title": "Venezia", "objects": { "key_1": { "name": "Chiave della Rimessa", }, "key_2": { "name": "Chiave d'Acciaio", }, "key_3": { "name": "Chiave di Ferro", } } }, { "title": "Relitto della Maria Doria", "objects": { "key_1": { "name": "Chiave del Guardaroba", }, "key_2": { "name": "Chiave Arrugginita", }, "key_3": { "name": "Chiave della Cabina", }, "puzzle_1": { "name": "Interruttore di Circuito", } } }, { "title": "Pendici Tibetane", "objects": { "tiger": { "name": "Leopardo delle Nevi", }, "key_1": { "name": "Chiave del Ponte", }, "key_2": { "name": "Chiave della Capanna", }, "puzzle_4": { "name": "Il Serafo", } } } ] } ================================================ FILE: data/trx/ship/games/tr2/strings-pl.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "settings": { "gameplay.fix_bear_ai": { "description": "Naprawia atak niedźwiedzia w dodatku Złota Maska, aby nie chybiał Lary.", }, "visuals.fix_animated_sprites": { "description": "Przywraca brakującą animację glonów w podwodnych poziomach.", } }, "objects": { "large_medipack": { "name": "Duża apteczka", }, "small_medipack": { "name": "Mała apteczka", } }, "cutscenes": [ { "title": "Przerywnik 1", }, { "title": "Przerywnik 2", }, { "title": "Przerywnik 3", }, { "title": "Przerywnik 4", } ], "levels": [ { "title": "Dom Lary", }, { "title": "Wielki Mur", "objects": { "key_1": { "name": "Klucz do strażnicy", }, "key_2": { "name": "Zardzewiały klucz", } } }, { "title": "Wenecja", "objects": { "key_1": { "name": "Klucz do hangaru łodzi", }, "key_2": { "name": "Stalowy klucz", }, "key_3": { "name": "Żelazny klucz", } } }, { "title": "Kryjówka Bartoliego", "objects": { "key_1": { "name": "Klucz do biblioteki", }, "key_2": { "name": "Klucz do detonatora", } } }, { "title": "Opera", "objects": { "key_1": { "name": "Ozdobny klucz", }, "puzzle_1": { "name": "Skrzynka przekaźnikowa", }, "puzzle_2": { "name": "Układ elektroniczny", } } }, { "title": "Platforma wiertnicza", "objects": { "key_1": { "name": "Czerwona karta dostępu", }, "key_2": { "name": "Żółta karta dostępu", }, "key_3": { "name": "Zielona karta dostępu", } } }, { "title": "Strefa nurkowania", "objects": { "key_1": { "name": "Czerwona karta dostępu", }, "key_4": { "name": "Niebieska karta dostępu", }, "puzzle_1": { "name": "Chip maszynowy", } } }, { "title": "40 sążni", }, { "title": "Wrak statku Maria Doria", "objects": { "key_1": { "name": "Klucz do łazienki", }, "key_2": { "name": "Zardzewiały klucz", }, "key_3": { "name": "Klucz do kajuty", }, "puzzle_1": { "name": "Wyłącznik obwodu", } } }, { "title": "Strefa pasażerska", "objects": { "key_1": { "name": "Klucz do teatru", }, "key_2": { "name": "Zardzewiały klucz", } } }, { "title": "Pokład", "objects": { "key_2": { "name": "Klucz do zamka rufowego", }, "key_3": { "name": "Klucz do magazynu", }, "key_4": { "name": "Klucz do kajuty", }, "puzzle_4": { "name": "Seraf", } } }, { "title": "Pogórze Tybetu", "objects": { "tiger": { "name": "Lampart śnieżny", }, "key_1": { "name": "Klucz do zwodzonego mostu", }, "key_2": { "name": "Klucz do chaty", }, "puzzle_4": { "name": "Seraf", } } }, { "title": "Klasztor Barkhang", "objects": { "key_1": { "name": "Klucz do skarbca", }, "key_2": { "name": "Klucz do zapadni", }, "key_3": { "name": "Klucz do wyjścia na dach", }, "key_4": { "name": "Klucz do głównej sali", }, "puzzle_1": { "name": "Młynek modlitewny", }, "puzzle_2": { "name": "Kamień szlachetny", }, "puzzle_4": { "name": "Seraf", } } }, { "title": "Katakumby Talionu", "objects": { "tiger": { "name": "Śnieżny lampart", }, "pickup_1": { "name": "Pałka do gongu", }, "puzzle_1": { "name": "Tybetańska maska", } } }, { "title": "Pałac lodowy", "objects": { "tiger": { "name": "Biały tygrys", }, "key_2": { "name": "Pałka do gongu", }, "pickup_2": { "name": "Talion", }, "puzzle_1": { "name": "Tybetańska maska", } } }, { "title": "Świątynia Xian", "objects": { "key_2": { "name": "Złoty klucz", }, "key_3": { "name": "Srebrny klucz", }, "key_4": { "name": "Klucz do głównej komnaty", }, "puzzle_1": { "name": "Smocza pieczęć", } } }, { "title": "Podniebne wyspy", "objects": { "puzzle_1": { "name": "Mistyczna tabliczka", }, "puzzle_2": { "name": "Mistyczna tabliczka", } } }, { "title": "Legowisko smoka", "objects": { "puzzle_1": { "name": "Mistyczna tabliczka", }, "puzzle_2": { "name": "Sztylet Xian", } } }, { "title": "Dom, słodki dom", "objects": { "key_1": { "name": "Klucz do szafki na broń", }, "puzzle_1": { "name": "Sztylet Xian", } } } ], "demos": [ { "title": "Wenecja", "objects": { "key_1": { "name": "Klucz do hangaru łodzi", }, "key_2": { "name": "Stalowy klucz", }, "key_3": { "name": "Żelazny klucz", } } }, { "title": "Wrak statku Maria Doria", "objects": { "key_1": { "name": "Klucz do łazienki", }, "key_2": { "name": "Zardzewiały klucz", }, "key_3": { "name": "Klucz do kajuty", }, "puzzle_1": { "name": "Wyłącznik obwodu", } } }, { "title": "Pogórze Tybetu", "objects": { "tiger": { "name": "Lampart śnieżny", }, "key_1": { "name": "Klucz do zwodzonego mostu", }, "key_2": { "name": "Klucz do chaty", }, "puzzle_4": { "name": "Seraf", } } } ] } ================================================ FILE: data/trx/ship/games/tr2/strings.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "settings": { "gameplay.fix_bear_ai": { "description": "Fixes the bear pat attack in The Golden Mask so it does not miss Lara.", }, "visuals.fix_animated_sprites": { "description": "Fixes original issues in 40 Fathoms, Wreck of the Maria Doria and The Deck where plant sprites in water areas do not animate.", } }, "objects": { "large_medipack": { "name": "Large Medipack", }, "small_medipack": { "name": "Small Medipack", } }, "cutscenes": [ { "title": "Cutscene 1", }, { "title": "Cutscene 2", }, { "title": "Cutscene 3", }, { "title": "Cutscene 4", } ], "levels": [ { "title": "Lara's Home", }, { "title": "The Great Wall", "objects": { "key_1": { "name": "Guardhouse Key", }, "key_2": { "name": "Rusty Key", } } }, { "title": "Venice", "objects": { "key_1": { "name": "Boathouse Key", }, "key_2": { "name": "Steel Key", }, "key_3": { "name": "Iron Key", } } }, { "title": "Bartoli's Hideout", "objects": { "key_1": { "name": "Library Key", }, "key_2": { "name": "Detonator Key", } } }, { "title": "Opera House", "objects": { "key_1": { "name": "Ornate Key", }, "puzzle_1": { "name": "Relay Box", }, "puzzle_2": { "name": "Circuit Board", } } }, { "title": "Offshore Rig", "objects": { "key_1": { "name": "Red Pass Card", }, "key_2": { "name": "Yellow Pass Card", }, "key_3": { "name": "Green Pass Card", } } }, { "title": "Diving Area", "objects": { "key_1": { "name": "Red Pass Card", }, "key_4": { "name": "Blue Pass Card", }, "puzzle_1": { "name": "Machine Chip", } } }, { "title": "40 Fathoms", }, { "title": "Wreck of the Maria Doria", "objects": { "key_1": { "name": "Rest Room Key", }, "key_2": { "name": "Rusty Key", }, "key_3": { "name": "Cabin Key", }, "puzzle_1": { "name": "Circuit Breaker", } } }, { "title": "Living Quarters", "objects": { "key_1": { "name": "Theater Key", }, "key_2": { "name": "Rusty Key", } } }, { "title": "The Deck", "objects": { "key_2": { "name": "Stern Key", }, "key_3": { "name": "Storage Key", }, "key_4": { "name": "Cabin Key", }, "puzzle_4": { "name": "The Seraph", } } }, { "title": "Tibetan Foothills", "objects": { "tiger": { "name": "Snow Leopard", }, "key_1": { "name": "Drawbridge Key", }, "key_2": { "name": "Hut Key", }, "puzzle_4": { "name": "The Seraph", } } }, { "title": "Barkhang Monastery", "objects": { "key_1": { "name": "Strongroom Key", }, "key_2": { "name": "Trapdoor Key", }, "key_3": { "name": "Rooftops Key", }, "key_4": { "name": "Main Hall Key", }, "puzzle_1": { "name": "Prayer Wheels", }, "puzzle_2": { "name": "Gemstones", }, "puzzle_4": { "name": "The Seraph", } } }, { "title": "Catacombs of the Talion", "objects": { "tiger": { "name": "Snow Leopard", }, "pickup_1": { "name": "Gong Hammer", }, "puzzle_1": { "name": "Tibetan Mask", } } }, { "title": "Ice Palace", "objects": { "tiger": { "name": "White Tiger", }, "key_2": { "name": "Gong Hammer", }, "pickup_2": { "name": "Talion", }, "puzzle_1": { "name": "Tibetan Mask", } } }, { "title": "Temple of Xian", "objects": { "key_2": { "name": "Gold Key", }, "key_3": { "name": "Silver Key", }, "key_4": { "name": "Main Chamber Key", }, "puzzle_1": { "name": "The Dragon Seal", } } }, { "title": "Floating Islands", "objects": { "puzzle_1": { "name": "Mystic Plaque", }, "puzzle_2": { "name": "Mystic Plaque", } } }, { "title": "The Dragon's Lair", "objects": { "puzzle_1": { "name": "Mystic Plaque", }, "puzzle_2": { "name": "Dagger of Xian", } } }, { "title": "Home Sweet Home", "objects": { "key_1": { "name": "Gun Cupboard Key", }, "puzzle_1": { "name": "Dagger of Xian", } } } ], "demos": [ { "title": "Venice", "objects": { "key_1": { "name": "Boathouse Key", }, "key_2": { "name": "Steel Key", }, "key_3": { "name": "Iron Key", } } }, { "title": "Wreck of the Maria Doria", "objects": { "key_1": { "name": "Rest Room Key", }, "key_2": { "name": "Rusty Key", }, "key_3": { "name": "Cabin Key", }, "puzzle_1": { "name": "Circuit Breaker", } } }, { "title": "Tibetan Foothills", "objects": { "tiger": { "name": "Snow Leopard", }, "key_1": { "name": "Drawbridge Key", }, "key_2": { "name": "Hut Key", }, "puzzle_4": { "name": "The Seraph", } } } ] } ================================================ FILE: data/trx/ship/games/tr2/weapons.json5 ================================================ { "LGT_UNARMED": { "sample_num": "SFX_LARA_NO", }, "LGT_FLARE": { "ammo": { "initial_qty": 6, "pickup_qty": 6, "pickup_qty_alt": 8, }, "flash_shade": 2048, "flash_pos": { "x": 11, "y": 32, "z": 80, }, "flash_pos_alt": { "x": -6, "y": 6, "z": 80, }, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {}, "glow_color": [1, 0.75, 0.125], }, "LGT_PISTOLS": { "type": "WEAPON_TYPE_DUAL_PISTOLS", "lock_angles": [-60, +60, -60, +60], "left_angles": [-170, +60, -80, +80], "right_angles": [-60, +170, -80, +80], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 650, "damage": 1, "ammo": { "initial_qty": 32, "pickup_qty": 32, }, "target_dist": 8.0, "recoil_frame": 9, "flash_time": 3, "flash_shade": 5120, "flash_pos": { "y": 185, "z": 40, }, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {}, "glow_color": [1, 0.75, 0.125], "sample_num": "SFX_LARA_PISTOLS", }, "LGT_MAGNUMS": { "type": "WEAPON_TYPE_DUAL_PISTOLS", "lock_angles": [-60, +60, -60, +60], "left_angles": [-170, +60, -80, +80], "right_angles": [-60, +170, -80, +80], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 650, "damage": 2, "ammo": { "initial_qty": 50, "pickup_qty": 50, }, "target_dist": 8.0, "recoil_frame": 9, "flash_time": 3, "flash_shade": 4096, "flash_pos": { "y": 155, "z": 55, }, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {}, "glow_color": [1, 0.75, 0.125], "sample_num": "SFX_LARA_MAGNUMS", "is_available": false, }, "LGT_AUTOS": { "type": "WEAPON_TYPE_DUAL_PISTOLS", "lock_angles": [-60, +60, -60, +60], "left_angles": [-170, +60, -80, +80], "right_angles": [-60, +170, -80, +80], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 650, "damage": 2, "ammo": { "initial_qty": 40, "pickup_qty": 40, }, "target_dist": 8.0, "recoil_frame": 9, "flash_time": 3, "flash_shade": 4096, "flash_pos": { "y": 215, "z": 65, }, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {}, "glow_color": [1, 0.75, 0.125], "sample_num": "SFX_LARA_AUTOS", }, "LGT_DESERT_EAGLE": { "type": "WEAPON_TYPE_SINGLE_PISTOL", "lock_angles": [-60, +60, -60, +60], "left_angles": [-10, +10, -80, +80], "right_angles": [0, 0, 0, 0], "aim_speed": 10, "shot_accuracy": 4, "gun_height": 650, "damage": 21, "ammo": { "initial_qty": 10, "pickup_qty": 10, }, "target_dist": 8.0, "recoil_frame": 16, "flash_time": 3, "flash_shade": 4096, "flash_pos": { "y": 215, "z": 65, }, "sample_num": "SFX_LARA_DESERT_EAGLE", "is_available": false, }, "LGT_UZIS": { "type": "WEAPON_TYPE_DUAL_PISTOLS", "lock_angles": [-60, +60, -60, +60], "left_angles": [-170, +60, -80, +80], "right_angles": [-60, +170, -80, +80], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 650, "damage": 1, "ammo": { "initial_qty": 80, "pickup_qty": 80, }, "target_dist": 8.0, "recoil_frame": 3, "flash_time": 3, "flash_shade": 2560, "flash_pos": { "y": 200, "z": 50, }, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {}, "glow_color": [1, 0.75, 0.125], "sample_num": "SFX_LARA_UZI_FIRE", }, "LGT_SHOTGUN": { "type": "WEAPON_TYPE_RIFLE", "lock_angles": [-60, +60, -55, +55], "left_angles": [-80, +80, -65, +65], "right_angles": [-80, +80, -65, +65], "aim_speed": 10, "shot_accuracy": 0, "gun_height": 500, "damage": 3, "ammo": { "initial_qty": 2, "pickup_qty": 2, }, "target_dist": 8.0, "equip_anim_idx": 1, "draw_frame": 10, "undraw_frame": 21, "recoil_frame": 9, "flash_time": 3, "flash_shade": 2560, "flash_pos": { "y": 285, }, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {}, "glow_color": [1, 0.75, 0.125], "sample_num": "SFX_LARA_SHOTGUN", }, "LGT_M16": { "type": "WEAPON_TYPE_RIFLE", "lock_angles": [-60, +60, -55, +55], "left_angles": [-80, +80, -65, +65], "right_angles": [-80, +80, -65, +65], "aim_speed": 10, "shot_accuracy": 4, "gun_height": 500, "damage": 3, "ammo": { "initial_qty": 40, "pickup_qty": 40, }, "target_dist": 12.0, "equip_anim_idx": 1, "draw_frame": 10, "undraw_frame": 21, "recoil_frame": 0, "flash_time": 3, "flash_shade": 2560, "flash_pos": { "y": 400, "z": 99, }, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {"z": -65}, "glow_color": [1, 0.75, 0.125], "sample_num": "", }, "LGT_MP5": { "type": "WEAPON_TYPE_RIFLE", "lock_angles": [-60, +60, -55, +55], "left_angles": [-80, +80, -65, +65], "right_angles": [-80, +80, -65, +65], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 500, "damage": 3, "ammo": { "initial_qty": 60, "pickup_qty": 60, }, "target_dist": 12.0, "equip_anim_idx": 1, "draw_frame": 16, "undraw_frame": 21, "recoil_frame": 0, "flash_time": 3, "flash_shade": 2560, "flash_pos": { "y": 332, "z": 96, }, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {"z": -65}, "glow_color": [1, 0.75, 0.125], "sample_num": "", "is_available": false, }, "LGT_ROCKET": { "type": "WEAPON_TYPE_RIFLE", "lock_angles": [-60, +60, -55, +55], "left_angles": [-80, +80, -65, +65], "right_angles": [-80, +80, -65, +65], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 500, "damage": 30, "ammo": { "initial_qty": 1, "pickup_qty": 1, }, "target_dist": 8.0, "equip_anim_idx": 1, "draw_frame": 12, "undraw_frame": 21, "recoil_frame": 0, "flash_time": 2, "sample_num": "", "is_available": false, }, "LGT_GRENADE": { "type": "WEAPON_TYPE_RIFLE", "lock_angles": [-60, +60, -55, +55], "left_angles": [-80, +80, -65, +65], "right_angles": [-80, +80, -65, +65], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 500, "damage": 30, "ammo": { "initial_qty": 2, "pickup_qty": 2, }, "target_dist": 8.0, "equip_anim_idx": 0, "draw_frame": 13, "undraw_frame": 14, "recoil_frame": 0, "flash_time": 2, "sample_num": "", }, "LGT_HARPOON": { "type": "WEAPON_TYPE_RIFLE", "lock_angles": [-60, +60, -65, +65], "left_angles": [-80, +80, -75, +75], "right_angles": [-80, +80, -75, +75], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 500, "damage": 4, "ammo": { "initial_qty": 3, "pickup_qty": 3, }, "target_dist": 8.0, "equip_anim_idx": 1, "draw_frame": 10, "undraw_frame": 21, "recoil_frame": 0, "flash_time": 2, "sample_num": "", }, "LGT_SKIDOO": { "type": "WEAPON_TYPE_MOUNTED", "lock_angles": [-30, 30, -55, 55], "left_angles": [-30, 30, -55, 55], "right_angles": [-30, 30, -55, 55], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 400, "damage": 3, "target_dist": 8.0, "recoil_frame": 0, "flash_time": 2, "flash_shade": 5120, "flash_pos": { "y": 185, "z": 40, }, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {}, "glow_color": [1, 0.75, 0.125], "sample_num": "SFX_LARA_UZI_FIRE", }, } ================================================ FILE: data/trx/ship/games/tr2-gm/gameflow.json5 ================================================ { // NOTE: bad changes to this file may result in crashes. // Lines starting with double slashes are comments and are ignored. "engine": 2, "extends": "tr2", "name": "The Golden Mask", "main_menu_picture": "title_eu_gm.webp", "savegame_file_fmt": "save_trgm_%02d.dat", "demo_version": false, "enable_tr2_item_drops": true, "convert_dropped_guns": true, "title": { "path": ["title_gm.tr2", "title.tr2"], "music_track": 60, "sequence": [ {"type": "display_picture", "path": "legal_eu_gm.webp", "legal": true}, {"type": "exit_to_title"}, ], "inherit_injections": false, "injections": [ "font.bin", "inv_background.bin", "pda_model.bin", "title_textures.bin", "misc_sprites.bin", ] }, "sfx_path": ["main_gm.sfx", "main.sfx"], "injections": [ "font.bin", "lara_animations.bin", "pda_model.bin", "pickup_aid.bin", "lara_extra.bin", "lara_rifle_sfx.bin", "secret_models_gm.bin", "misc_sprites.bin", "lara_outfits.bin", "crystal.bin", ], "levels": [ // 0. Legacy savegame placeholder {"type": "dummy"}, // 1. The Cold War { "path": "level1.tr2", "script": "level1.lua", "music_track": 29, "lara_outfit": "tr2_bomber_jacket", "sequence": [ {"type": "loading_screen", "path": "gm_level1.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "give_item", "object_id": "shotgun"}, {"type": "give_item", "object_id": "small_medipack"}, {"type": "give_item", "object_id": "large_medipack"}, {"type": "give_item", "object_id": "flare", "quantity": 2}, {"type": "loop_game"}, {"type": "play_music", "music_track": 37}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "coldwar_fd.bin", "coldwar_itemrots.bin", "coldwar_music_tracks.bin", "coldwar_objects.bin", "coldwar_textures.bin", "common_pickup_meshes_gm.bin", "lara_guns.bin", "shark_sfx.bin", "winston_model.bin", "coldwar_crystals.bin", ], }, // 2. Fool's Gold { "path": "level2.tr2", "music_track": 54, "lara_outfit": "tr2_bomber_jacket", "sequence": [ {"type": "loading_screen", "path": "gm_level2.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "play_music", "music_track": 37}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "door107_sfx.bin", "door108_sfx.bin", "fools_itemrots.bin", "fools_music_tracks.bin", "fools_pickup_meshes.bin", "fools_textures.bin", "lara_guns.bin", "winston_model.bin", "fools_crystals.bin", ], }, // 3. Furnace of the Gods { "path": "level3.tr2", "script": "level3.lua", "music_track": 55, "lara_outfit": "tr2_bomber_jacket", "sequence": [ {"type": "loading_screen", "path": "gm_level3.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "play_music", "music_track": 37}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "furnace_itemrots.bin", "lara_guns.bin", "furnace_music_tracks.bin", "furnace_objects.bin", "furnace_pickup_meshes.bin", "furnace_textures.bin", "winston_model.bin", "furnace_crystals.bin", ], "unobtainable_ally_kills": 2, }, // 4. Kingdom { "path": "level4.tr2", "script": "level4.lua", "music_track": 27, "lara_outfit": "tr2_bomber_jacket", "sequence": [ {"type": "loading_screen", "path": "gm_level4.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "give_item", "object_id": "puzzle_1"}, {"type": "loop_game"}, {"type": "play_music", "music_track": 48}, {"type": "level_stats"}, {"type": "display_picture", "credit": true, "path": "credit00_gm.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, {"type": "display_picture", "credit": true, "path": "credit01.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, {"type": "display_picture", "credit": true, "path": "credit02.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, {"type": "display_picture", "credit": true, "path": "credit03.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, {"type": "display_picture", "credit": true, "path": "credit04.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, {"type": "display_picture", "credit": true, "path": "credit05.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, {"type": "display_picture", "credit": true, "path": "credit06.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, {"type": "display_picture", "credit": true, "path": "credit07_gm.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, {"type": "display_picture", "credit": true, "path": "credit08.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, {"type": "total_stats", "background_path": "end.webp"}, {"type": "level_complete"}, ], "injections": [ "common_pickup_meshes_gm.bin", "door108_sfx.bin", "guardian_death_commands.bin", "lara_guns.bin", "kingdom_cameras.bin", "kingdom_itemrots.bin", "kingdom_music_tracks.bin", "kingdom_textures.bin", "winston_model.bin", "kingdom_crystals.bin", ], }, // 5. Nightmare in Vegas { "path": "level5.tr2", "type": "bonus", "music_track": 30, "lara_outfit": "tr2_vegas", "sequence": [ {"type": "loading_screen", "path": "gm_level5.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "remove_weapons"}, {"type": "remove_ammo"}, {"type": "remove_flares"}, {"type": "remove_medipacks"}, {"type": "give_item", "object_id": "pistols"}, {"type": "loop_game"}, {"type": "play_music", "music_track": 37}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "common_pickup_meshes_gm.bin", "door108_sfx.bin", "guardian_death_commands.bin", "lara_vegas_guns.bin", "vegas_fd.bin", "vegas_itemrots.bin", "vegas_music_tracks.bin", "vegas_textures.bin", "vegas_crystals.bin", ], }, ], "demos": [ ], "cutscenes": [ ], "fmvs": [ ], "hidden_config": [ "enable_cutscenes", // TR2G has no cutscenes "enable_demo", // TR2G has no demos "enable_fmv", // TR2G has no FMVs "enable_item_examining", // TR2G has no special item descriptions "fix_alligator_ai", // TR2G has no alligators "fix_animated_sprites", "healthbar_poison_color", "healthbar_poison_color_ps1", "exposurebar_color", "exposurebar_color_ps1", "exposurebar_location", "exposurebar_show_mode", "change_pierre_spawn", "fix_chainblock_secret_sound", "enable_compass_stats", "enable_wading", "restore_ps1_enemies", "fix_speeches_killing_music", "enable_weather", "enable_footprints", "fix_monkey_pickup_priority", "fix_pipeman_aim", "enable_cinematics", ], } ================================================ FILE: data/trx/ship/games/tr2-gm/strings-de.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Laras Haus", }, { "title": "Der kalte Krieg", "objects": { "tiger": { "name": "Schneeleopard", }, "key_1": { "name": "Wachraum-Schlüssel", }, "key_2": { "name": "Schacht'B' Schlüssel", } } }, { "title": "Das Gold des Narren", "objects": { "key_1": { "name": "Schlüsselkarte 1", }, "key_4": { "name": "Schlüsselkarte 2", }, "puzzle_1": { "name": "Platine", } } }, { "title": "Hochofen der Götter", "objects": { "big_spider": { "name": "Eisbär", }, "spider": { "name": "Wolf", }, "puzzle_1": { "name": "Maske des Tornarsuk", }, "puzzle_2": { "name": "Goldklumpen", } } }, { "title": "Königreich", "objects": { "tiger": { "name": "Schneeleopard", }, "puzzle_1": { "name": "Maske des Tornarsuk", } } }, { "title": "Alptraum in Vegas", "objects": { "key_1": { "name": "Hotelschlüssel", }, "puzzle_1": { "name": "Fahrstulteil", }, "puzzle_2": { "name": "Türstromkreis", } } } ] } ================================================ FILE: data/trx/ship/games/tr2-gm/strings-fr.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "\\{review}Demeure de Lara", }, { "title": "\\{review}Guerre froide", "objects": { "tiger": { "name": "\\{review}Léopard des Neiges", }, "key_1": { "name": "\\{review}Clé de la Salle des Gardiens", }, "key_2": { "name": "\\{review}Clé de l'Ascenseur 'B'", } } }, { "title": "\\{review}Désillusion", "objects": { "key_1": { "name": "\\{review}Clé de Carte 1", }, "key_4": { "name": "\\{review}Clé de Carte 2", }, "puzzle_1": { "name": "\\{review}Carte de Circuit", } } }, { "title": "\\{review}Le Fournaise des dieux", "objects": { "big_spider": { "name": "\\{review}Ours Polaire", }, "spider": { "name": "\\{review}Loup", }, "puzzle_1": { "name": "\\{review}Masque de Tornarsuk", }, "puzzle_2": { "name": "\\{review}Pépite d'Or", } } }, { "title": "\\{review}Royaume", "objects": { "tiger": { "name": "\\{review}Léopard des Neiges", }, "puzzle_1": { "name": "\\{review}Masque de Tornarsuk", } } }, { "title": "\\{review}Cauchemar à Las Vegas", "objects": { "key_1": { "name": "\\{review}Clé d'Hôtel", }, "puzzle_1": { "name": "\\{review}Jonction de l'Ascenseur", }, "puzzle_2": { "name": "\\{review}Circuit de Porte", } } } ] } ================================================ FILE: data/trx/ship/games/tr2-gm/strings-gd.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Dachaigh Lara", }, { "title": "An Cogadh Fuar", "objects": { "tiger": { "name": "Lìopaird-shneachda", }, "key_1": { "name": "Iuchair an t-Seòmair-ghàrda", }, "key_2": { "name": "Iuchair Toll 'B'", } } }, { "title": "Òr nan Amadan", "objects": { "key_1": { "name": "Cairt-iuchrach 1", }, "key_4": { "name": "Cairt-iuchrach 2", }, "puzzle_1": { "name": "Bòrd-chuairt", } } }, { "title": "Àmhainn nan Diathan", "objects": { "big_spider": { "name": "Mathan-pòla", }, "spider": { "name": "Madadh-allaidh", }, "puzzle_1": { "name": "Masg Thornarsuk", }, "puzzle_2": { "name": "Bleideag Òir", } } }, { "title": "Rìoghachd", "objects": { "tiger": { "name": "Lìopaird-shneachda", }, "puzzle_1": { "name": "Masg Thornarsuk", } } }, { "title": "Cuthag ann am Vegas", "objects": { "key_1": { "name": "Iuchair an Taigh-òsta", }, "puzzle_1": { "name": "Ceangal an Lioft", }, "puzzle_2": { "name": "Cuairt na Dorsan", } } } ] } ================================================ FILE: data/trx/ship/games/tr2-gm/strings-it.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Casa di Lara", }, { "title": "La Guerra Fredda", "objects": { "tiger": { "name": "Leopardo delle Nevi", }, "key_1": { "name": "Chiave della Guardiola", }, "key_2": { "name": "Chiave del Tunnel \"B\"", } } }, { "title": "L'Oro degli Sciocchi", "objects": { "key_1": { "name": "Chiave Magnetica 1", }, "key_4": { "name": "Chiave Magnetica 2", }, "puzzle_1": { "name": "Circuito d'Avvio", } } }, { "title": "Crogiolo degli Dèi", "objects": { "big_spider": { "name": "Orso Polare", }, "spider": { "name": "Lupo", }, "puzzle_1": { "name": "Maschera di Tornarsuk", }, "puzzle_2": { "name": "Pepita d'Oro", } } }, { "title": "Il Regno", "objects": { "tiger": { "name": "Leopardo delle Nevi", }, "puzzle_1": { "name": "Maschera di Tornarsuk", } } }, { "title": "Incubo a Las Vegas", "objects": { "key_1": { "name": "Chiave dell'Hotel", }, "puzzle_1": { "name": "Giunto per Ascensore", }, "puzzle_2": { "name": "Circuito della Porta", } } } ] } ================================================ FILE: data/trx/ship/games/tr2-gm/strings-pl.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Dom Lary", }, { "title": "Zimna Wojna", "objects": { "tiger": { "name": "Lampart śnieżny", }, "key_1": { "name": "Klucz do strażnicy", }, "key_2": { "name": "Klucz do szybu 'B'", } } }, { "title": "Złoto głupców", "objects": { "key_1": { "name": "Karta dostępu 1", }, "key_4": { "name": "Karta dostępu 2", }, "puzzle_1": { "name": "Układ elektroniczny", } } }, { "title": "Piec bogów", "objects": { "big_spider": { "name": "Niedźwiedź polarny", }, "spider": { "name": "Wilk", }, "puzzle_1": { "name": "Maska Tornarsuka", }, "puzzle_2": { "name": "Złoty samorodek", } } }, { "title": "Królestwo", "objects": { "tiger": { "name": "Lampart śnieżny", }, "puzzle_1": { "name": "Maska Tornarsuka", } } }, { "title": "Koszmar w Vegas", "objects": { "key_1": { "name": "Klucz do hotelu", }, "puzzle_1": { "name": "Węzeł windy", }, "puzzle_2": { "name": "Obwód drzwi", } } } ] } ================================================ FILE: data/trx/ship/games/tr2-gm/strings.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Lara's Home", }, { "title": "The Cold War", "objects": { "tiger": { "name": "Snow Leopard", }, "key_1": { "name": "Guardroom Key", }, "key_2": { "name": "Shaft 'B' Key", } } }, { "title": "Fool's Gold", "objects": { "key_1": { "name": "CardKey 1", }, "key_4": { "name": "CardKey 2", }, "puzzle_1": { "name": "Circuit Board", } } }, { "title": "Furnace of the Gods", "objects": { "big_spider": { "name": "Polar Bear", }, "spider": { "name": "Wolf", }, "puzzle_1": { "name": "Mask of Tornarsuk", }, "puzzle_2": { "name": "Gold Nugget", } } }, { "title": "Kingdom", "objects": { "tiger": { "name": "Snow Leopard", }, "puzzle_1": { "name": "Mask of Tornarsuk", } } }, { "title": "Nightmare in Vegas", "objects": { "key_1": { "name": "Hotel Key", }, "puzzle_1": { "name": "Elevator Junction", }, "puzzle_2": { "name": "Door Circuit", } } } ] } ================================================ FILE: data/trx/ship/games/tr2-level/gameflow.json5 ================================================ { // This file is used to enable the -l argument support. "engine": 2, "extends": "tr2", "name": "TR2 Direct Level", "main_menu_picture": "title_eu.webp", "savegame_file_fmt": "save_tr2_custom_%02d.dat", "demo_version": false, "enable_tr2_item_drops": true, "convert_dropped_guns": true, "sfx_path": "main.sfx", "injections": [ "font.bin", "lara_animations.bin", "lara_guns.bin", "pda_model.bin", "pickup_aid.bin", "winston_model.bin", "crystal.bin", "lara_extra.bin", "lara_rifle_sfx.bin", "secret_models_og.bin", "misc_sprites.bin", "lara_outfits.bin", ], "levels": [ { "path": "%direct_level%", "music_track": -1, "lara_outfit": "tr2_classic", "sequence": [ {"type": "loop_game"}, {"type": "level_stats"}, ], }, ], } ================================================ FILE: data/trx/ship/games/tr2-level/strings-de.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Testlevel", } ] } ================================================ FILE: data/trx/ship/games/tr2-level/strings-fr.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "\\{review}Niveau de test", } ] } ================================================ FILE: data/trx/ship/games/tr2-level/strings-gd.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Ìre Phròbhail", } ] } ================================================ FILE: data/trx/ship/games/tr2-level/strings-it.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Livello di Prova", } ] } ================================================ FILE: data/trx/ship/games/tr2-level/strings-pl.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Poziom testowy", } ] } ================================================ FILE: data/trx/ship/games/tr2-level/strings.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Test Level", } ] } ================================================ FILE: data/trx/ship/games/tr3/catalog_item_actions.csv ================================================ 0, ITEM_ACTION_TURN_180 1, ITEM_ACTION_FLOOR_SHAKE 2, ITEM_ACTION_LARA_NORMAL 3, ITEM_ACTION_BUBBLES 4, ITEM_ACTION_FINISH_LEVEL 5, ITEM_ACTION_FLOOD 6, ITEM_ACTION_CHANDELIER 7, ITEM_ACTION_RUBBLE 8, ITEM_ACTION_PISTON 9, ITEM_ACTION_CURTAIN 10, ITEM_ACTION_SET_CHANGE 11, ITEM_ACTION_EXPLOSION 12, ITEM_ACTION_LARA_HANDS_FREE 13, ITEM_ACTION_FLIP_MAP 14, ITEM_ACTION_LARA_DRAW_RIGHT_GUN 15, ITEM_ACTION_LARA_DRAW_LEFT_GUN 16, ITEM_ACTION_LARA_SHOOT_RIGHT_GUN 17, ITEM_ACTION_LARA_SHOOT_LEFT_GUN 18, ITEM_ACTION_SWAP_MESHES_WITH_MESH_SWAP_1 19, ITEM_ACTION_SWAP_MESHES_WITH_MESH_SWAP_2 20, ITEM_ACTION_SWAP_MESHES_WITH_MESH_SWAP_3 21, ITEM_ACTION_INVISIBILITY_ON 22, ITEM_ACTION_INVISIBILITY_OFF 23, ITEM_ACTION_DYNAMIC_LIGHT_ON 24, ITEM_ACTION_DYNAMIC_LIGHT_OFF 25, ITEM_ACTION_STATUE 26, ITEM_ACTION_RESET_HAIR 27, ITEM_ACTION_BOILER 28, ITEM_ACTION_ASSAULT_RESET 29, ITEM_ACTION_ASSAULT_STOP 30, ITEM_ACTION_ASSAULT_START 31, ITEM_ACTION_ASSAULT_FINISHED 32, ITEM_ACTION_FOOTPRINT 33, ITEM_ACTION_ASSAULT_PENALTY_8 34, ITEM_ACTION_RACETRACK_START 35, ITEM_ACTION_RACETRACK_RESET 36, ITEM_ACTION_RACETRACK_FINISHED 37, ITEM_ACTION_ASSAULT_PENALTY_30 38, ITEM_ACTION_GYM_HINT_1 39, ITEM_ACTION_GYM_HINT_2 40, ITEM_ACTION_GYM_HINT_3 41, ITEM_ACTION_GYM_HINT_4 42, ITEM_ACTION_GYM_HINT_5 43, ITEM_ACTION_GYM_HINT_6 44, ITEM_ACTION_GYM_HINT_7 45, ITEM_ACTION_GYM_HINT_8 46, ITEM_ACTION_GYM_HINT_9 47, ITEM_ACTION_GYM_HINT_10 48, ITEM_ACTION_GYM_HINT_11 49, ITEM_ACTION_GYM_HINT_12 50, ITEM_ACTION_GYM_HINT_13 51, ITEM_ACTION_GYM_HINT_14 52, ITEM_ACTION_GYM_HINT_15 53, ITEM_ACTION_GYM_HINT_16 54, ITEM_ACTION_GYM_HINT_17 55, ITEM_ACTION_GYM_HINT_18 56, ITEM_ACTION_GYM_HINT_19 57, ITEM_ACTION_GYM_HINT_RESET 58, ITEM_ACTION_CAMERA_SHAKE 59, ITEM_ACTION_LOWERING_BLOCK 60, ITEM_ACTION_SHADOW_ON 61, ITEM_ACTION_SHADOW_OFF 62, ITEM_ACTION_TURN_90 ================================================ FILE: data/trx/ship/games/tr3/catalog_lara_anims.csv ================================================ 0, LA_RUN 1, LA_WALK_FORWARD 2, LA_WALK_STOP_RIGHT 3, LA_WALK_STOP_LEFT 4, LA_WALK_TO_RUN_RIGHT 5, LA_WALK_TO_RUN_LEFT 6, LA_RUN_START 7, LA_RUN_TO_WALK_RIGHT 8, LA_RUN_TO_STAND_LEFT 9, LA_RUN_TO_WALK_LEFT 10, LA_RUN_TO_STAND_RIGHT 11, LA_STAND_STILL 12, LA_TURN_RIGHT_SLOW 13, LA_TURN_LEFT_SLOW 14, LA_JUMP_FORWARD_LAND_START_UNUSED 15, LA_JUMP_FORWARD_LAND_END_UNUSED 16, LA_RUN_JUMP_RIGHT_START 17, LA_RUN_JUMP_RIGHT_CONTINUE 18, LA_RUN_JUMP_LEFT_START 19, LA_RUN_JUMP_LEFT_CONTINUE 20, LA_WALK_FORWARD_START 21, LA_WALK_FORWARD_START_CONTINUE 22, LA_JUMP_FORWARD_TO_FREEFALL 23, LA_FREEFALL 24, LA_FREEFALL_LAND 25, LA_FREEFALL_LAND_DEATH 26, LA_STAND_TO_JUMP_UP 27, LA_STAND_TO_JUMP_UP_CONTINUE 28, LA_JUMP_UP 29, LA_JUMP_UP_TO_HANG_UNUSED 30, LA_JUMP_UP_TO_FREEFALL 31, LA_JUMP_UP_LAND 32, LA_SMASH_JUMP 33, LA_SMASH_JUMP_CONTINUE 34, LA_FALL_START 35, LA_FALL 36, LA_FALL_TO_FREEFALL 37, LA_HANG_TO_FREEFALL 38, LA_WALK_BACK_END_RIGHT 39, LA_WALK_BACK_END_LEFT 40, LA_WALK_BACK 41, LA_WALK_BACK_START 42, LA_CLIMB_3CLICK 43, LA_CLIMB_3CLICK_END_TO_RUN 44, LA_TURN_RIGHT 45, LA_JUMP_FORWARD_TO_FREEFALL_2 46, LA_REACH_TO_FREEFALL 47, LA_ROLL_ALTERNATE 48, LA_ROLL_END_ALTERNATE 49, LA_JUMP_FORWARD_END_TO_FREEFALL 50, LA_CLIMB_2CLICK 51, LA_CLIMB_2CLICK_END 52, LA_CLIMB_2CLICK_END_TO_RUN 53, LA_WALL_SMASH_LEFT 54, LA_WALL_SMASH_RIGHT 55, LA_RUN_UP_STEP_RIGHT 56, LA_RUN_UP_STEP_LEFT 57, LA_WALK_UP_STEP_RIGHT 58, LA_WALK_UP_STEP_LEFT 59, LA_WALK_DOWN_LEFT 60, LA_WALK_DOWN_RIGHT 61, LA_WALK_DOWN_BACK_LEFT 62, LA_WALK_DOWN_BACK_RIGHT 63, LA_WALL_SWITCH_DOWN 64, LA_WALL_SWITCH_UP 65, LA_SIDE_STEP_LEFT 66, LA_SIDE_STEP_LEFT_END 67, LA_SIDE_STEP_RIGHT 68, LA_SIDE_STEP_RIGHT_END 69, LA_ROTATE_LEFT 70, LA_SLIDE_FORWARD 71, LA_SLIDE_FORWARD_END 72, LA_SLIDE_FORWARD_STOP 73, LA_STAND_TO_JUMP 74, LA_JUMP_BACK_START 75, LA_JUMP_BACK 76, LA_JUMP_FORWARD_START 77, LA_JUMP_FORWARD 78, LA_JUMP_LEFT_START 79, LA_JUMP_LEFT 80, LA_JUMP_RIGHT_START 81, LA_JUMP_RIGHT 82, LA_LAND 83, LA_JUMP_BACK_TO_FREEFALL 84, LA_JUMP_LEFT_TO_FREEFALL 85, LA_JUMP_RIGHT_TO_FREEFALL 86, LA_UNDERWATER_SWIM_FORWARD 87, LA_UNDERWATER_SWIM_FORWARD_DRIFT 88, LA_SMALL_JUMP_BACK_START 89, LA_SMALL_JUMP_BACK 90, LA_SMALL_JUMP_BACK_END 91, LA_JUMP_UP_START 92, LA_LAND_TO_RUN 93, LA_FALL_BACK 94, LA_JUMP_FORWARD_TO_REACH 95, LA_REACH 96, LA_REACH_TO_HANG 97, LA_CLIMB_ON 98, LA_REACH_TO_FREEFALL_2 99, LA_FALL_CROUCHING_LANDING 100, LA_JUMP_FORWARD_TO_REACH_LATE 101, LA_JUMP_FORWARD_START_TO_REACH_ALTERNATE 102, LA_CLIMB_ON_END 103, LA_STAND_IDLE 104, LA_SLIDE_BACKWARD_START 105, LA_SLIDE_BACKWARD 106, LA_SLIDE_BACKWARD_END 107, LA_UNDERWATER_SWIM_TO_IDLE 108, LA_UNDERWATER_IDLE 109, LA_UNDERWARER_IDLE_TO_SWIM 110, LA_ONWATER_IDLE 111, LA_ONWATER_TO_STAND_HIGH 112, LA_FREEFALL_TO_UNDERWATER 113, LA_ONWATER_DIVE_ALTERNATE 114, LA_UNDERWATER_TO_ONWATER 115, LA_ONWATER_SWIM_FORWARD_DIVE 116, LA_ONWATER_SWIM_FORWARD 117, LA_ONWATER_SWIM_FORWARD_TO_IDLE 118, LA_ONWATER_IDLE_TO_SWIM_FORWARD 119, LA_ONWATER_DIVE 120, LA_PUSHABLE_GRAB 121, LA_PUSHABLE_RELEASE 122, LA_PUSHABLE_PULL 123, LA_PUSHABLE_PUSH 124, LA_UNDERWATER_DEATH 125, LA_HIT_FRONT 126, LA_HIT_BACK 127, LA_HIT_LEFT 128, LA_HIT_RIGHT 129, LA_UNDERWATER_SWITCH 130, LA_UNDERWATER_PICKUP 131, LA_USE_KEY 132, LA_ONWATER_DEATH 133, LA_RUN_DEATH 134, LA_USE_PUZZLE 135, LA_PICKUP 136, LA_SHIMMY_LEFT 137, LA_SHIMMY_RIGHT 138, LA_STAND_DEATH 139, LA_BOULDER_DEATH 140, LA_ONWATER_IDLE_TO_SWIM_BACK 141, LA_ONWATER_SWIM_BACK 142, LA_ONWATER_SWIM_BACK_TO_IDLE 143, LA_ONWATER_SWIM_LEFT 144, LA_ONWATER_SWIM_RIGHT 145, LA_DEATH_JUMP 146, LA_ROLL_START 147, LA_ROLL_CONTINUE 148, LA_ROLL_END 149, LA_SPIKE_DEATH 150, LA_SWING_IN_SLOW 151, LA_SWANDIVE_ROLL 152, LA_SWANDIVE_TO_UNDERWATER 153, LA_FREEFALL_SWANDIVE 154, LA_FREEFALL_SWANDIVE_TO_UNDERWATER 155, LA_SWANDIVE_DEATH 156, LA_SWANDIVE_LEFT 157, LA_SWANDIVE_RIGHT 158, LA_SWANDIVE_START 159, LA_CLIMB_ON_HANDSTAND 160, LA_STAND_TO_LADDER 161, LA_LADDER_UP 162, LA_LADDER_UP_STOP_RIGHT 163, LA_LADDER_UP_STOP_LEFT 164, LA_LADDER_IDLE 165, LA_LADDER_UP_START 166, LA_LADDER_DOWN_STOP_LEFT 167, LA_LADDER_DOWN_STOP_RIGHT 168, LA_LADDER_DOWN 169, LA_LADDER_DOWN_START 170, LA_LADDER_RIGHT 171, LA_LADDER_LEFT 172, LA_LADDER_HANG 173, LA_LADDER_HANG_TO_IDLE 174, LA_LADDER_CLIMB_ON 175, LA_UNKNOWN 176, LA_ONWATER_TO_WADE_SHALLOW_UNUSED 177, LA_WADE 178, LA_RUN_TO_WADE_LEFT 179, LA_RUN_TO_WADE_RIGHT 180, LA_WADE_TO_RUN_LEFT 181, LA_WADE_TO_RUN_RIGHT 182, LA_LADDER_BACKFLIP_START 183, LA_LADDER_BACKFLIP_CONTINUE 184, LA_WADE_TO_STAND_RIGHT 185, LA_WADE_TO_STAND_LEFT 186, LA_STAND_TO_WADE 187, LA_LADDER_UP_HANGING 188, LA_LADDER_DOWN_HANGING 189, LA_FLARE_THROW 190, LA_ONWATER_TO_WADE 191, LA_ONWATER_TO_STAND_MEDIUM 192, LA_UNDERWATER_TO_STAND 193, LA_ONWATER_TO_WADE_LOW 194, LA_LADDER_TO_HANG_DOWN 195, LA_SWITCH_SMALL_DOWN 196, LA_SWITCH_SMALL_UP 197, LA_BUTTON_PUSH 198, LA_UNDERWATER_SWIM_TO_STILL_HUDDLE 199, LA_UNDERWATER_SWIM_TO_STILL_SPRAWL 200, LA_UNDERWATER_SWIM_TO_STILL_MEDIUM 201, LA_LADDER_TO_HANG_RIGHT 202, LA_LADDER_TO_HANG_LEFT 203, LA_UNDERWATER_ROLL_START 204, LA_FLARE_PICKUP 205, LA_UNDERWATER_ROLL_END 206, LA_UNDERWATER_FLARE_PICKUP 207, LA_RUN_JUMP_ROLL_START 208, LA_SOMERSAULT 209, LA_RUN_JUMP_ROLL_END 210, LA_JUMP_FORWARD_ROLL_START 211, LA_JUMP_FORWARD_ROLL_END 212, LA_JUMP_BACK_ROLL_START 213, LA_JUMP_BACK_ROLL_END 214, LA_ZIPLINE_GRAB 215, LA_ZIPLINE_RIDE 216, LA_ZIPLINE_FALL 217, LA_STAND_TO_CROUCH 218, LA_CROUCH_ROLL_FORWARD_START 219, LA_CROUCH_ROLL_FORWARD_CONTINUE 220, LA_CROUCH_ROLL_FORWARD_END 221, LA_CROUCH_TO_STAND 222, LA_CROUCH_IDLE 223, LA_SPRINT 224, LA_RUN_TO_SPRINT_LEFT 225, LA_RUN_TO_SPRINT_RIGHT 226, LA_SPRINT_SLIDE_STAND_RIGHT 227, LA_SPRINT_SLIDE_STAND_RIGHT_END_ALTERNATE_UNUSED 228, LA_SPRINT_SLIDE_STAND_LEFT 229, LA_SPRINT_SLIDE_STAND_LEFT_END_ALTERNATE_UNUSED 230, LA_SPRINT_TO_ROLL_LEFT 231, LA_SPRINT_TO_ROLL_LEFT_BETA_UNUSED 232, LA_SPRINT_ROLL_LEFT_TO_RUN 233, LA_MONKEY_GRAB 234, LA_MONKEY_IDLE 235, LA_MONKEY_FALL 236, LA_MONKEY_FORWARD 237, LA_MONKEY_STOP_LEFT 238, LA_MONKEY_STOP_RIGHT 239, LA_MONKEY_IDLE_TO_FORWARD_LEFT 240, LA_SPRINT_TO_ROLL_ALTERNATE_START_UNUSED 241, LA_SPRINT_TO_ROLL_ALTERNATE_CONTINUE_UNUSED 242, LA_SPRINT_TO_ROLL_ALTERNATE_END_UNUSED 243, LA_SPRINT_TO_RUN_LEFT 244, LA_SPRINT_TO_RUN_RIGHT 245, LA_STAND_TO_CROUCH_END 246, LA_SLIDE_FORWARD_TO_RUN 247, LA_CROUCH_ROLL_FORWARD_START_ALTERNATE_UNUSED 248, LA_JUMP_FORWARD_START_TO_GRAB_EARLY 249, LA_JUMP_FORWARD_START_TO_GRAB_LATE 250, LA_RUN_TO_GRAB_RIGHT 251, LA_RUN_TO_GRAB_LEFT 252, LA_MONKEY_IDLE_TO_FORWARD_RIGHT 253, LA_MONKEY_SHIMMY_LEFT 254, LA_MONKEY_SHIMMY_LEFT_END 255, LA_MONKEY_SHIMMY_RIGHT 256, LA_MONKEY_SHIMMY_RIGHT_END 257, LA_MONKEY_TURN_AROUND 258, LA_CROUCH_TO_CRAWL_START 259, LA_CRAWL_TO_CROUCH_START 260, LA_CRAWL_FORWARD 261, LA_CRAWL_IDLE_TO_FORWARD 262, LA_CRAWL_FORWARD_TO_IDLE_START_RIGHT 263, LA_CRAWL_IDLE 264, LA_CROUCH_TO_CRAWL_END 265, LA_CRAWL_TO_CROUCH_END_UNUSED 266, LA_CRAWL_FORWARD_TO_IDLE_END_RIGHT 267, LA_CRAWL_FORWARD_TO_IDLE_START_LEFT 268, LA_CRAWL_FORWARD_TO_IDLE_END_LEFT 269, LA_CRAWL_TURN_LEFT 270, LA_CRAWL_TURN_RIGHT 271, LA_MONKEY_TURN_LEFT 272, LA_MONKEY_TURN_RIGHT 273, LA_CROUCH_TO_CRAWL_CONTINUE 274, LA_CRAWL_TO_CROUCH_CONTINUE 275, LA_CRAWL_IDLE_TO_BACKWARD 276, LA_CRAWL_BACKWARD 277, LA_CRAWL_BACKWARD_TO_IDLE_RIGHT_START 278, LA_CRAWL_BACKWARD_TO_IDLE_RIGHT_END 279, LA_CRAWL_BACKWARD_TO_IDLE_LEFT_START 280, LA_CRAWL_BACKWARD_TO_IDLE_LEFT_END 281, LA_CRAWL_TURN_LEFT_EARLY_END 282, LA_CRAWL_TURN_RIGHT_EARLY_END 283, LA_MONKEY_TURN_LEFT_EARLY_END 284, LA_MONKEY_TURN_LEFT_LATE_END 285, LA_MONKEY_TURN_RIGHT_EARLY_END 286, LA_MONKEY_TURN_RIGHT_LATE_END 287, LA_HANG_TO_CROUCH_START 288, LA_HANG_TO_CROUCH_END 289, LA_CRAWL_TO_HANG_START 290, LA_CRAWL_TO_HANG_CONTINUE 291, LA_CROUCH_PICKUP 292, LA_CRAWL_PICKUP 293, LA_CROUCH_HIT_FRONT 294, LA_CROUCH_HIT_BACK 295, LA_CROUCH_HIT_RIGHT 296, LA_CROUCH_HIT_LEFT 297, LA_CRAWL_HIT_FRONT_UNUSED 298, LA_CRAWL_HIT_BACK_UNUSED 299, LA_CRAWL_HIT_RIGHT_UNUSED 300, LA_CRAWL_HIT_LEFT_UNUSED 301, LA_CRAWL_DEATH 302, LA_CRAWL_TO_HANG_END 303, LA_STAND_TO_CROUCH_ABORT_UNUSED 304, LA_RUN_TO_CROUCH_LEFT_START 305, LA_RUN_TO_CROUCH_RIGHT_START 306, LA_RUN_TO_CROUCH_LEFT_END 307, LA_RUN_TO_CROUCH_RIGHT_END 308, LA_SPRINT_TO_ROLL_RIGHT 309, LA_SPRINT_ROLL_RIGHT_TO_RUN 310, LA_SPRINT_TO_CROUCH_LEFT 311, LA_SPRINT_TO_CROUCH_RIGHT 312, LA_CROUCH_PICKUP_FLARE 313, LA_KICK 314, LA_JUMP_NEUTRAL_ROLL 315, LA_CONTROLLED_DROP 316, LA_CONTROLLED_DROP_CONTINUE 317, LA_HANG_TO_JUMP_UP 318, LA_HANG_TO_JUMP_UP_CONTINUE 319, LA_HANG_TO_JUMP_BACK 320, LA_HANG_TO_JUMP_BACK_CONTINUE 321, LA_POSE_RIGHT_START 322, LA_POSE_RIGHT_CONTINUE 323, LA_POSE_RIGHT_END 324, LA_POSE_LEFT_START 325, LA_POSE_LEFT_CONTINUE 326, LA_POSE_LEFT_END 327, LA_CRAWL_JUMP_DOWN 328, LA_CROUCH_TURN_LEFT 329, LA_CROUCH_TURN_RIGHT 330, LA_SWING_IN_FAST 331, LA_LADDER_TO_CROUCH_START 332, LA_LADDER_TO_CROUCH_END ================================================ FILE: data/trx/ship/games/tr3/catalog_lara_states.csv ================================================ 0, LS_WALK 1, LS_RUN 2, LS_STOP 3, LS_JUMP_FORWARD 4, LS_POSE 5, LS_FAST_BACK 6, LS_TURN_RIGHT 7, LS_TURN_LEFT 8, LS_DEATH 9, LS_FAST_FALL 10, LS_HANG 11, LS_REACH 12, LS_SPLAT 13, LS_TREAD 14, LS_LAND 15, LS_COMPRESS 16, LS_WALK_BACK 17, LS_SWIM 18, LS_GLIDE 19, LS_PULL_UP 20, LS_FAST_TURN 21, LS_STEP_RIGHT 22, LS_STEP_LEFT 23, LS_ROLL_CONT 24, LS_SLIDE 25, LS_JUMP_BACK 26, LS_JUMP_RIGHT 27, LS_JUMP_LEFT 28, LS_JUMP_UP 29, LS_FALL_BACK 30, LS_SHIMMY_LEFT 31, LS_SHIMMY_RIGHT 32, LS_SLIDE_BACK 33, LS_SURF_TREAD 34, LS_SURF_SWIM 35, LS_DIVE 36, LS_PUSH_BLOCK 37, LS_PULL_BLOCK 38, LS_PP_READY 39, LS_PICKUP 40, LS_SWITCH_ON 41, LS_SWITCH_OFF 42, LS_USE_KEY 43, LS_USE_PUZZLE 44, LS_UW_DEATH 45, LS_ROLL 46, LS_SPECIAL 47, LS_SURF_BACK 48, LS_SURF_LEFT 49, LS_SURF_RIGHT 50, LS_USE_MIDAS 51, LS_DIE_MIDAS 52, LS_SWAN_DIVE 53, LS_FAST_DIVE 54, LS_GYMNAST 55, LS_WATER_OUT 56, LS_CLIMB_STANCE 57, LS_CLIMBING 58, LS_CLIMB_LEFT 59, LS_CLIMB_END 60, LS_CLIMB_RIGHT 61, LS_CLIMB_DOWN 62, LS_LARA_TEST1 63, LS_LARA_TEST2 64, LS_LARA_TEST3 65, LS_WADE 66, LS_WATER_ROLL 67, LS_FLARE_PICKUP 68, LS_TWIST 69, LS_KICK 70, LS_ZIPLINE 71, LS_CROUCH_IDLE 72, LS_CROUCH_ROLL 73, LS_SPRINT 74, LS_SPRINT_ROLL 75, LS_MONKEY_IDLE 76, LS_MONKEY_FORWARD 77, LS_MONKEY_LEFT 78, LS_MONKEY_RIGHT 79, LS_MONKEY_ROLL 80, LS_CRAWL_IDLE 81, LS_CRAWL_FORWARD 82, LS_MONKEY_TURN_LEFT 83, LS_MONKEY_TURN_RIGHT 84, LS_CRAWL_TURN_LEFT 85, LS_CRAWL_TURN_RIGHT 86, LS_CRAWL_BACK 87, LS_CLIMB_TO_CRAWL 88, LS_CRAWL_TO_CLIMB 89, LS_CONTROLLED 90, LS_RESPONSIVE 91, LS_NEUTRAL_ROLL 92, LS_POSE_START 93, LS_POSE_END 94, LS_POSE_LEFT 95, LS_POSE_RIGHT 96, LS_CRAWL_JUMP_DOWN 97, LS_CROUCH_TURN_LEFT 98, LS_CROUCH_TURN_RIGHT ================================================ FILE: data/trx/ship/games/tr3/catalog_music.csv ================================================ 12, MX_RIB_THEME 12, MX_MINE_CART_THEME 82, MX_TR3_GYM_EXERCISE_07 83, MX_TR3_GYM_EXERCISE_14 86, MX_TR3_GYM_EXERCISE_04 89, MX_TR3_GYM_EXERCISE_16 90, MX_TR3_GYM_EXERCISE_19 95, MX_TR3_GYM_HINT_FAST_TIME 96, MX_TR3_GYM_EXERCISE_17 98, MX_TR3_GYM_EXERCISE_11 107, MX_TR3_GYM_EXERCISE_02 108, MX_TR3_GYM_EXERCISE_01 109, MX_TR3_GYM_EXERCISE_15 110, MX_TR3_GYM_EXERCISE_06 112, MX_TR3_GYM_EXERCISE_18 113, MX_TR3_GYM_EXERCISE_08 114, MX_TR3_GYM_EXERCISE_09 115, MX_TR3_GYM_EXERCISE_03 116, MX_TR3_GYM_EXERCISE_12 117, MX_TR3_GYM_EXERCISE_13 118, MX_TR3_GYM_EXERCISE_05 119, MX_TR3_GYM_EXERCISE_10 122, MX_SECRET ================================================ FILE: data/trx/ship/games/tr3/catalog_objects.csv ================================================ 0, O_LARA 1, O_LARA_PISTOLS 2, O_LARA_HAIR 3, O_LARA_SHOTGUN 4, O_LARA_DESERT_EAGLE 5, O_LARA_UZIS 6, O_LARA_MP5 7, O_LARA_ROCKET_GUN 8, O_LARA_GRENADE_GUN 9, O_LARA_HARPOON_GUN 10, O_LARA_FLARE 11, O_LARA_VEHICLE_ANIM 12, O_LARA_VEHICLE_EXTRA 13, O_LARA_EXTRA 14, O_KAYAK 15, O_RIB 16, O_QUAD_BIKE 17, O_MINE_CART 18, O_MOUNTED_GUN 19, O_UPV 20, O_TRIBE_AXEMAN 21, O_TRIBE_PIPEMAN 22, O_PATROL_DOG 23, O_MOUSE 24, O_KILL_ALL_TRIGGERED 25, O_ORCA 26, O_DIVER 27, O_CROW 28, O_TIGER 29, O_VULTURE 30, O_ASSAULT_TARGET 31, O_DYING_MUTANT 32, O_ALLIGATOR 34, O_COMPY 35, O_LIZARD 36, O_TRIBE_BOSS 37, O_STHPAC_MERCENARY 38, O_CARCASS 39, O_RX_WORKER_1 40, O_RX_WORKER_2 41, O_HUSKIE 42, O_CRAWLER_MUTANT 44, O_WASP_MUTANT 45, O_CLAW_MUTANT 46, O_HYBRID_MUTANT 47, O_WASP_MUTANT_EMITTER 48, O_RAPTOR_EMITTER 49, O_WILLARD 50, O_RX_WORKER_3 51, O_SWAT_1 52, O_SWAT_2 53, O_PUNK_1 54, O_PUNK_2 55, O_WATER_BLOKE 56, O_SECURITY_GUARD 57, O_SOPHIA 58, O_ELECTRIC_CLEANER 59, O_FLOATING_CORPSE 60, O_MP_1 61, O_MP_2 62, O_PRISONER 63, O_SWAT_3 64, O_SENTRY_GUN 65, O_CIVILIAN 66, O_SECURITY_LASER_ALARM 67, O_SECURITY_LASER_DEADLY 68, O_SECURITY_LASER_KILLER 69, O_COBRA 70, O_SHIVA 71, O_MONKEY 72, O_BEAR_TRAP 73, O_TONY 74, O_AI_GUARD 75, O_AI_AMBUSH 76, O_AI_PATROL_1 77, O_AI_MODIFY 78, O_AI_FOLLOW 79, O_AI_PATROL_2 80, O_AI_X1 81, O_AI_X2 82, O_AI_X3 83, O_FALLING_BLOCK_1 84, O_FALLING_BLOCK_2 85, O_FALLING_BLOCK_3 86, O_PENDULUM_1 87, O_SPIKES 88, O_ROLLING_BALL_1 89, O_ROLLING_BALL_4 90, O_POISON_DART 91, O_POISON_DART_EMITTER 92, O_HOMING_DART_EMITTER 93, O_DRAWBRIDGE 94, O_TEETH_TRAP 95, O_LIFT 96, O_MOVING_BAR 97, O_MOVABLE_BLOCK_1 98, O_MOVABLE_BLOCK_2 99, O_MOVABLE_BLOCK_3 100, O_MOVABLE_BLOCK_4 101, O_SMASH_OBJECT_4 102, O_SMASH_OBJECT_3 103, O_SMASH_OBJECT_2 104, O_SMASH_OBJECT_1 105, O_POWER_SAW 106, O_HOOK 107, O_FALLING_CEILING_1 108, O_SPINNING_BLADE 109, O_CIRCULAR_BLADE 110, O_TRAIN 111, O_BLADE 112, O_ROLLING_BALL_2 113, O_ICICLE 114, O_SPIKE_WALL 115, O_SPRINGBOARD 116, O_CEILING_SPIKES 117, O_SWITCH_TYPE_WHEEL 118, O_SWITCH_TYPE_SMALL 119, O_PROPELLER_2 120, O_PROPELLER_3 121, O_PENDULUM_2 122, O_MESH_SWAP_1 123, O_MESH_SWAP_2 124, O_MESH_SWAP_3 125, O_TEXT_BOX 126, O_ROLLING_BALL_3 127, O_ZIPLINE_HANDLE 128, O_SWITCH_TYPE_BUTTON 129, O_SWITCH_TYPE_NORMAL 130, O_SWITCH_TYPE_UW 131, O_DOOR_TYPE_1 132, O_DOOR_TYPE_2 133, O_DOOR_TYPE_3 134, O_DOOR_TYPE_4 135, O_DOOR_TYPE_5 136, O_DOOR_TYPE_6 137, O_DOOR_TYPE_7 138, O_DOOR_TYPE_8 139, O_TRAPDOOR_TYPE_1 140, O_TRAPDOOR_TYPE_2 141, O_TRAPDOOR_TYPE_3 142, O_BRIDGE_FLAT 143, O_BRIDGE_TILT_1 144, O_BRIDGE_TILT_2 145, O_PASSPORT_OPTION 146, O_STOPWATCH_OPTION 147, O_PHOTO_OPTION 148, O_PLAYER_1 149, O_PLAYER_2 150, O_PLAYER_3 151, O_PLAYER_4 152, O_PLAYER_5 153, O_PLAYER_6 154, O_PLAYER_7 155, O_PLAYER_8 156, O_PLAYER_9 157, O_PLAYER_10 158, O_PASSPORT_CLOSED 159, O_PDA_OPTION 160, O_PISTOL_ITEM 161, O_SHOTGUN_ITEM 162, O_DESERT_EAGLE_ITEM 163, O_UZI_ITEM 164, O_HARPOON_ITEM 165, O_MP5_ITEM 166, O_ROCKET_GUN_ITEM 167, O_GRENADE_GUN_ITEM 168, O_PISTOL_AMMO_ITEM 169, O_SHOTGUN_AMMO_ITEM 170, O_DESERT_EAGLE_AMMO_ITEM 171, O_UZI_AMMO_ITEM 172, O_HARPOON_AMMO_ITEM 173, O_MP5_AMMO_ITEM 174, O_ROCKET_AMMO_ITEM 175, O_GRENADE_AMMO_ITEM 176, O_SMALL_MEDIPACK_ITEM 177, O_LARGE_MEDIPACK_ITEM 178, O_FLAREBOX_ITEM 179, O_FLARE_ITEM 180, O_SAVE_CRYSTAL_ITEM 181, O_DETAIL_OPTION 182, O_SOUND_OPTION 183, O_CONTROL_OPTION 184, O_GLOBE_SELECT_OPTION 185, O_PISTOL_OPTION 186, O_SHOTGUN_OPTION 187, O_DESERT_EAGLE_OPTION 188, O_UZI_OPTION 189, O_HARPOON_OPTION 190, O_MP5_OPTION 191, O_ROCKET_GUN_OPTION 192, O_GRENADE_GUN_OPTION 193, O_PISTOL_AMMO_OPTION 194, O_SHOTGUN_AMMO_OPTION 195, O_DESERT_EAGLE_AMMO_OPTION 196, O_UZI_AMMO_OPTION 197, O_HARPOON_AMMO_OPTION 198, O_MP5_AMMO_OPTION 199, O_ROCKET_AMMO_OPTION 200, O_GRENADE_AMMO_OPTION 201, O_SMALL_MEDIPACK_OPTION 202, O_LARGE_MEDIPACK_OPTION 203, O_FLAREBOX_OPTION 204, O_SAVE_CRYSTAL_OPTION 205, O_PUZZLE_ITEM_1 206, O_PUZZLE_ITEM_2 207, O_PUZZLE_ITEM_3 208, O_PUZZLE_ITEM_4 209, O_PUZZLE_OPTION_1 210, O_PUZZLE_OPTION_2 211, O_PUZZLE_OPTION_3 212, O_PUZZLE_OPTION_4 213, O_PUZZLE_HOLE_1 214, O_PUZZLE_HOLE_2 215, O_PUZZLE_HOLE_3 216, O_PUZZLE_HOLE_4 217, O_PUZZLE_DONE_1 218, O_PUZZLE_DONE_2 219, O_PUZZLE_DONE_3 220, O_PUZZLE_DONE_4 221, O_SECRET_1 222, O_SECRET_2 223, O_SECRET_3 224, O_KEY_ITEM_1 225, O_KEY_ITEM_2 226, O_KEY_ITEM_3 227, O_KEY_ITEM_4 228, O_KEY_OPTION_1 229, O_KEY_OPTION_2 230, O_KEY_OPTION_3 231, O_KEY_OPTION_4 232, O_KEY_HOLE_1 233, O_KEY_HOLE_2 234, O_KEY_HOLE_3 235, O_KEY_HOLE_4 236, O_PICKUP_ITEM_1 237, O_PICKUP_ITEM_2 238, O_PICKUP_OPTION_1 239, O_PICKUP_OPTION_2 240, O_QUEST_ITEM_1 241, O_QUEST_ITEM_2 242, O_QUEST_ITEM_3 243, O_QUEST_ITEM_4 244, O_QUEST_OPTION_1 245, O_QUEST_OPTION_2 246, O_QUEST_OPTION_3 247, O_QUEST_OPTION_4 248, O_PICKUP_DISPLAY_PISTOLS 249, O_PICKUP_DISPLAY_SHOTGUN 250, O_PICKUP_DISPLAY_DESERTEAGLE 251, O_PICKUP_DISPLAY_UZIS 252, O_PICKUP_DISPLAY_HARPOON 253, O_PICKUP_DISPLAY_HANDK 254, O_PICKUP_DISPLAY_ROCKET_LAUNCHER 255, O_PICKUP_DISPLAY_GRENADE_LAUNCHER 256, O_PICKUP_DISPLAY_PISTOLS_AMMO 257, O_PICKUP_DISPLAY_SHOTGUN_AMMO 258, O_PICKUP_DISPLAY_DESERTEAGLE_AMMO 259, O_PICKUP_DISPLAY_UZIS_AMMO 260, O_PICKUP_DISPLAY_HARPOON_AMMO 261, O_PICKUP_DISPLAY_HANDK_AMMO 262, O_PICKUP_DISPLAY_ROCKET_LAUNCHER_AMMO 263, O_PICKUP_DISPLAY_GRENADE_LAUNCHER_AMMO 264, O_PICKUP_DISPLAY_SMALL_MEDIPACK 265, O_PICKUP_DISPLAY_LARGE_MEDIPACK 266, O_PICKUP_DISPLAY_FLAREBOX 267, O_PICKUP_DISPLAY_SAVEGAME_CCRYSTAL 268, O_PICKUP_DISPLAY_PUZZLE_1 269, O_PICKUP_DISPLAY_PUZZLE_2 270, O_PICKUP_DISPLAY_PUZZLE_3 271, O_PICKUP_DISPLAY_PUZZLE_4 272, O_PICKUP_DISPLAY_KEY_1 273, O_PICKUP_DISPLAY_KEY_2 274, O_PICKUP_DISPLAY_KEY_3 275, O_PICKUP_DISPLAY_KEY_4 276, O_PICKUP_DISPLAY_ICON_1 277, O_PICKUP_DISPLAY_ICON_2 278, O_PICKUP_DISPLAY_ICON_3 279, O_PICKUP_DISPLAY_ICON_4 280, O_PICKUP_DISPLAY_PICKUP_1 281, O_PICKUP_DISPLAY_PICKUP_2 282, O_FIRE_HEAD 283, O_TONY_FIRE_BALL 284, O_SPHERE_OF_DOOM_3 285, O_ALARM_SOUND 286, O_WATER_DRIP 287, O_TREX_ALPHA 288, O_RAPTOR 289, O_BIRD_TWEETER 290, O_CLOCK_CHIMES 291, O_ROTATING_LASER 292, O_ELECTRIC_FENCE 293, O_HOT_LIQUID 294, O_SHADOW 295, O_DETONATOR_BOX 296, O_EXPLOSION_1 297, O_BUBBLE_1 298, O_BUBBLE_2 299, O_GLOW 300, O_GUN_FLASH 301, O_M16_FLASH 302, O_DESERT_EAGLE_FLASH 303, O_BODY_PART 304, O_CAMERA_TARGET 305, O_WATERFALL 306, O_MISSILE_HARPOON 307, O_DRAGON_FIRE 308, O_KNIFE 309, O_ROCKET 310, O_HARPOON_BOLT 311, O_GRENADE 312, O_AREA_51_ROCKET 313, O_AREA_51_ROCKET_BLAST 314, O_AREA_51_ROCKET_SUPPORT 315, O_LARA_SKIN 316, O_LAVA 317, O_LAVA_EMITTER 318, O_STROBE_LIGHT 319, O_ELECTRICAL_LIGHT 320, O_ON_OFF_LIGHT 321, O_PULSE_LIGHT 322, O_BEACON_LIGHT 323, O_EXTRA_LIGHT_UNUSED 324, O_RED_LIGHT 325, O_GREEN_LIGHT 326, O_BLUE_LIGHT 327, O_AMBER_LIGHT 328, O_WHITE_LIGHT 329, O_FLAME 330, O_FLAME_EMITTER_BIG 331, O_FLAME_EMITTER_SMALL 332, O_FLAME_EMITTER_JET 333, O_FLAME_EMITTER_SIDE 334, O_SMOKE_EMITTER_WHITE 335, O_SMOKE_EMITTER_BLACK 336, O_STEAM_EMITTER 337, O_GAS_EMITTER_GREEN 338, O_PIRAHNAS 339, O_TROPICAL_FISH 340, O_PIRAHNA_GFX 341, O_TROPICAL_FISH_GFX 342, O_BAT_GFX 343, O_TRIBEBOSS_GFX 344, O_SPIDER_GFX 345, O_TUMBLEWEED 346, O_LEAVES 347, O_BAT_EMITTER 348, O_BIRD_EMITTER 349, O_ANIMATING_1 350, O_ANIMATING_2 351, O_ANIMATING_3 352, O_ANIMATING_4 353, O_ANIMATING_5 354, O_ANIMATING_6 355, O_SKYBOX 356, O_ALPHABET 357, O_DING_DONG 358, O_LARA_ALARM 359, O_MINI_COPTER 360, O_WINSTON 361, O_WINSTON_ARMY 362, O_ASSAULT_DIGITS 363, O_FINAL_LEVEL 364, O_CUT_SHOTGUN 365, O_EARTHQUAKE 366, O_GUN_SHELL 367, O_SHOTGUN_SHELL 368, O_CLAW_MUTANT_PLASMA_BALL 369, O_EXTRA_FX_2 370, O_DISPOSABLE_ANIMATING_1 371, O_SOPHIA_LASER_BOLT 372, O_SOPHIA_PLASMA_BALL 373, O_FUSE_BOX 374, O_EXTRA_FX_7 375, O_EXTRA_FX_8 376, O_ALPHABET_SMALL 377, O_SNOWFLAKE 378, O_LARA_MAGNUMS 379, O_MAGNUM_OPTION 380, O_MAGNUM_AMMO_OPTION 381, O_MAGNUM_ITEM 382, O_MAGNUM_AMMO_ITEM 383, O_LARA_AUTOS 384, O_AUTOS_OPTION 385, O_AUTOS_AMMO_OPTION 386, O_AUTOS_ITEM 387, O_AUTOS_AMMO_ITEM 388, O_LARA_M16 389, O_M16_OPTION 390, O_M16_AMMO_OPTION 391, O_M16_ITEM 392, O_M16_AMMO_ITEM 393, O_LARA_SKIN_SWAP_1 394, O_LARA_SKIN_SWAP_2 395, O_LARA_SKIN_SWAP_3 396, O_LARA_SKIN_SWAP_4 397, O_LARA_SKIN_SWAP_5 398, O_LARA_SKIN_SWAP_6 399, O_LARA_SKIN_SWAP_7 400, O_LARA_SKIN_SWAP_8 401, O_LARA_SKIN_SWAP_9 402, O_LARA_SKIN_SWAP_10 403, O_LARA_SKIN_SWAP_11 404, O_LARA_SKIN_SWAP_12 405, O_LARA_SKIN_SWAP_13 406, O_LARA_SKIN_SWAP_14 407, O_LARA_SKIN_SWAP_15 408, O_LARA_SKIN_SWAP_16 409, O_LARA_SKIN_SWAP_17 410, O_LARA_SKIN_SWAP_18 411, O_LARA_SKIN_SWAP_19 412, O_LARA_SKIN_SWAP_20 413, O_LARA_SKIN_SWAP_21 414, O_LARA_SKIN_SWAP_22 415, O_LARA_SKIN_SWAP_23 416, O_LARA_SKIN_SWAP_24 417, O_LARA_SKIN_SWAP_25 418, O_LARA_SKIN_SWAP_26 419, O_LARA_SKIN_SWAP_27 420, O_LARA_SKIN_SWAP_28 421, O_LARA_SKIN_SWAP_29 422, O_LARA_SKIN_SWAP_30 423, O_LARA_SKIN_SWAP_31 424, O_LARA_SKIN_SWAP_32 425, O_LARA_SKIN_SWAP_EXTRA 426, O_LARA_SKIN_SWAP_GUNS 427, O_LARA_SKIN_SWAP_LEGS 428, O_PICKUP_AID ================================================ FILE: data/trx/ship/games/tr3/catalog_samples.csv ================================================ 0, SFX_LARA_FOOTSTEP 2, SFX_LARA_NO 6, SFX_LARA_DRAW 7, SFX_LARA_HOLSTER 8, SFX_LARA_PISTOLS 9, SFX_LARA_RELOAD 10, SFX_LARA_RICOCHET 11, SFX_LARA_FLARE_IGNITE 12, SFX_LARA_FLARE_BURN 23, SFX_UPV_HARPOON 27, SFX_LARA_BODYSL 30, SFX_LARA_FALL 31, SFX_LARA_INJURY 33, SFX_LARA_SPLASH 34, SFX_LARA_GET_OUT 36, SFX_LARA_BREATH 37, SFX_LARA_BUBBLES 39, SFX_LARA_KEY 41, SFX_LARA_GENERAL_DEATH 43, SFX_LARA_UZI_FIRE 44, SFX_LARA_UZI_STOP 45, SFX_LARA_SHOTGUN 48, SFX_CLICK 49, SFX_LARA_SHOTGUN_SHELL 50, SFX_LARA_BULLETHIT 53, SFX_LARA_FALL_DEATH 56, SFX_LARA_FLESH_WOUND 60, SFX_UNDERWATER 69, SFX_ICICLE 70, SFX_LARA_THUD 70, SFX_PUSHBLOCK_LAND 72, SFX_LONDON_SWAT_FIRE 76, SFX_BLAST_CIRCLE 77, SFX_ROCKET_FIRE 78, SFX_MP5_FIRE 79, SFX_WATERFALL_LOOP 105, SFX_EXPLOSION_1 106, SFX_EXPLOSION_2 107, SFX_EARTHQUAKE_LOOP 108, SFX_MENU_ROTATE 109, SFX_MENU_CHOOSE 109, SFX_MENU_LARA_HOME 111, SFX_MENU_SPININ 112, SFX_MENU_SPINOUT 113, SFX_MENU_STOPWATCH 114, SFX_MENU_GUNS 115, SFX_MENU_PASSPORT 116, SFX_MENU_MEDI 119, SFX_TARGET_HITS 120, SFX_TARGET_SMASH 121, SFX_LARA_DESERT_EAGLE 131, SFX_CLEANER_FUSEBOX 137, SFX_AMERICAN_SWAT_FIRE 147, SFX_SPIKE_WALL 147, SFX_ROLLING_BALL_1_ROLL 147, SFX_ROLLING_BALL_4_ROLL 148, SFX_TRAIN_LOOP 149, SFX_LOWERING_BLOCK 150, SFX_LOOP_FOR_SMALL_FIRES 153, SFX_QUAD_IDLE 155, SFX_QUAD_MOVE 157, SFX_BATS_1 163, SFX_FLOOD 191, SFX_CLEANER_LOOP 195, SFX_RIB_IDLE 197, SFX_RIB_MOVING 202, SFX_QUAD_FRONT_IMPACT 204, SFX_FLAME_THROWER_LOOP 208, SFX_ALARM_1 209, SFX_MINE_CART_TRACK_LOOP 210, SFX_MINE_CART_PULLY_LOOP 211, SFX_MINE_CART_CLUNK_START 212, SFX_SAVE_CRYSTAL 214, SFX_SHUTTERS_BREAK 215, SFX_UNDERWATER_FAN_ON 216, SFX_UNDERWATER_FAN_OFF 219, SFX_MINE_CART_SREECH_BRAKE 220, SFX_SPANNER_CLUNK 247, SFX_BLOWPIPE_BLOW 257, SFX_HUGE_ROCKET_LOOP 258, SFX_SHIVA_SWORD_1 259, SFX_SHIVA_SWORD_2 280, SFX_ZIPLINE_GO 281, SFX_ZIPLINE_STOP 288, SFX_FOOTSTEPS_MUD 289, SFX_FOOTSTEPS_ICE 290, SFX_FOOTSTEPS_GRAVEL 291, SFX_FOOTSTEPS_SAND_OR_GRASS 292, SFX_FOOTSTEPS_WOOD 293, SFX_FOOTSTEPS_SNOW 294, SFX_FOOTSTEPS_METAL 299, SFX_ENGLISH_HOY 300, SFX_AMERICAN_HOY 305, SFX_SECURITY_GUARD_FIRE 318, SFX_MACAQUE_ROLL 334, SFX_DOORBELL 335, SFX_BURGLAR_ALARM 336, SFX_BOAT_ENGINE 346, SFX_UPV_LOOP 347, SFX_UPV_START 348, SFX_UPV_STOP 352, SFX_SOPHIA_SUMMON 353, SFX_SOPHIA_TAKE_HIT 354, SFX_SOPHIA_SUMMON_NOT 361, SFX_TRIBOSS_TAKE_HIT 362, SFX_TRIBOSS_TURN_CHAIR 370, SFX_LARA_MAGNUMS 371, SFX_LARA_AUTOS 372, SFX_M16_FIRE 373, SFX_M16_STOP 374, SFX_LARA_BAREFOOT ================================================ FILE: data/trx/ship/games/tr3/gameflow.json5 ================================================ { // NOTE: bad changes to this file may result in crashes. // Lines starting with double slashes are comments and are ignored. "engine": 3, "name": "Tomb Raider III", "main_menu_picture": "title_eu.webp", "savegame_file_fmt": "save_tr3_%02d.dat", "enable_tr2_item_drops": true, "convert_dropped_guns": true, "title": { "path": "title.tr2", "music_track": 5, "sequence": [ {"type": "display_picture", "path": "legal_eu.webp", "legal": true}, {"type": "play_fmv", "fmv_id": 0}, {"type": "play_fmv", "fmv_id": 1}, {"type": "exit_to_title"}, ], }, "ambient_tracks": [ 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 73, 74, 75, 76, 77, 78, ], "sfx_path": "main.sfx", "injections": [ "font.bin", "lara_animations.bin", "pda_model.bin", "lara_extra.bin", "misc_sprites.bin", "lara_outfits.bin", "pickup_aid.bin", ], "globe_select_entries": [ {"rot": [-1536, -7936, 1536], "start_level_ordinal": 1, "completion_level_ordinal": 4, "prereq_zones": [], "mesh_idx": 2}, {"rot": [1024, -512, -256], "start_level_ordinal": 5, "completion_level_ordinal": 8, "prereq_zones": [0], "mesh_idx": 5}, {"rot": [2560, 21248, -4096], "start_level_ordinal": 13, "completion_level_ordinal": 15, "prereq_zones": [0], "mesh_idx": 4}, {"rot": [-3328, 29440, 1024], "start_level_ordinal": -1, "completion_level_ordinal": -1, "prereq_zones": [], "mesh_idx": 3}, {"rot": [3072, -20992, 6400], "start_level_ordinal": 9, "completion_level_ordinal": 12, "prereq_zones": [0], "mesh_idx": 1}, {"rot": [-5120, -15360, -18688], "start_level_ordinal": 16, "completion_level_ordinal": 19, "prereq_zones": [0, 1, 2, 4], "mesh_idx": 6}, ], "levels": [ // 0. Lara's Home { "type": "gym", "path": "house.tr2", "music_track": -1, "lara_outfit": "tr3_gym", "sequence": [ {"type": "loading_screen", "path": "house.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "level_stats"}, ], "injections": [ "gym_sky.bin", "lara_gym_guns.bin", ], }, // 1. Jungle { "path": "jungle.tr2", "script": "jungle.lua", "music_track": 34, "lara_outfit": "tr3_classic", "weather_type": "rain", "sequence": [ {"type": "loading_screen", "path": "india.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "give_item", "object_id": "small_medipack"}, {"type": "give_item", "object_id": "large_medipack"}, {"type": "give_item", "object_id": "flare", "quantity": 2}, {"type": "loop_game"}, {"type": "play_cutscene", "cutscene_id": 0}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "india_sky.bin", "lara_guns.bin", ], }, // 2. Temple Ruins { "path": "temple.tr2", "music_track": 34, "lara_outfit": "tr3_classic", "sequence": [ {"type": "loading_screen", "path": "india.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "play_cutscene", "cutscene_id": 1}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "india_sky.bin", "lara_guns.bin", ], }, // 3. The River Ganges { "path": "quadchas.tr2", "music_track": 34, "lara_outfit": "tr3_classic", "weather_type": "rain", "sequence": [ {"type": "loading_screen", "path": "india.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "india_sky.bin", "lara_guns.bin", "ganges_door131_frames.bin", ], }, // 4. Caves of Kaliya { "path": "tonyboss.tr2", "music_track": 30, "lara_outfit": "tr3_classic", "sequence": [ {"type": "loading_screen", "path": "india.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "play_fmv", "fmv_id": 2}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "globe_select", "image": "india.webp"}, ], "injections": [ "lara_guns.bin", "globe_model.bin", ], }, // 5. Coastal Village { "path": "shore.tr2", "music_track": 32, "lara_outfit": "tr3_south_pacific", "sequence": [ {"type": "loading_screen", "path": "southpac.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "play_cutscene", "cutscene_id": 2}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "coastal_airlock.bin", "coastal_sky.bin", "coastal_animating_bounds.bin", "lara_guns.bin", ], "unobtainable_pickups": 1, }, // 6. Crash Site { "path": "crash.tr2", "script": "crash.lua", "music_track": 33, "lara_outfit": "tr3_south_pacific", "sequence": [ {"type": "loading_screen", "path": "southpac.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "give_item", "object_id": "pickup_1", "quantity": 1}, {"type": "loop_game"}, {"type": "play_cutscene", "cutscene_id": 3}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "crash_sky.bin", "crash_pickup_meshes.bin", "lara_guns.bin", ], }, // 7. Madubu Gorge { "path": "rapids.tr2", "music_track": 36, "death_tile": "rapids", "water_particles": true, "lara_outfit": "tr3_south_pacific", "sequence": [ {"type": "loading_screen", "path": "southpac.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "lara_guns.bin", "rapids_sky.bin", ], }, // 8. Temple of Puna { "path": "triboss.tr2", "music_track": 30, "lara_outfit": "tr3_south_pacific", "sequence": [ {"type": "loading_screen", "path": "southpac.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "globe_select", "image": "southpac.webp"}, ], "injections": [ "lara_guns.bin", "puna_pickup_meshes.bin", "globe_model.bin", ], }, // 9. Thames Wharf { "path": "roofs.tr2", "music_track": 73, "lara_outfit": "tr3_catsuit", "weather_type": "rain", "sequence": [ {"type": "loading_screen", "path": "london.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "disable_floor", "height": 1792}, {"type": "loop_game"}, {"type": "play_cutscene", "cutscene_id": 4}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "lara_guns.bin", "menu_artefacts.bin", ], }, // 10. Aldwych { "path": "sewer.tr2", "music_track": 74, "water_particles": true, "lara_outfit": "tr3_catsuit", "sequence": [ {"type": "loading_screen", "path": "london.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "play_cutscene", "cutscene_id": 5}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "lara_guns.bin", "aldwych_fd.bin", "aldwych_pickup_meshes.bin", "aldwych_textures.bin", "menu_artefacts.bin", ], }, // 11. Lud's Gate { "path": "tower.tr2", "script": "tower.lua", "music_track": 31, "water_particles": true, "lara_outfit": "tr3_catsuit", "sequence": [ {"type": "loading_screen", "path": "london.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "play_cutscene", "cutscene_id": 6}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "lara_guns.bin", "luds_diver_animation.bin", "luds_textures.bin", "menu_artefacts.bin", ], }, // 12. City { "path": "office.tr2", "music_track": 78, "lara_outfit": "tr3_catsuit", "weather_type": "rain", "death_tile": "electric", "sequence": [ {"type": "loading_screen", "path": "london.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "disable_floor", "height": 5120}, {"type": "loop_game"}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "globe_select", "image": "london.webp"}, ], "injections": [ "lara_guns.bin", "globe_model.bin", "city_textures.bin", "menu_artefacts.bin", ], }, // 13. Nevada Desert { "path": "nevada.tr2", "music_track": 33, "death_tile": "electric", "lara_outfit": "tr3_nevada", "sequence": [ {"type": "loading_screen", "path": "nevada.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "play_cutscene", "cutscene_id": 7}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "lara_guns.bin", "nevada_sky.bin", "nevada_door132_frames.bin", ], "unobtainable_pickups": 1, }, // 14. High Security Compound { "path": "compound.tr2", "script": "compound.lua", "music_track": 27, "lara_outfit": "tr3_nevada", "sequence": [ {"type": "loading_screen", "path": "nevada.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "set_lara_start_anim", "anim": 20}, {"type": "remove_weapons"}, {"type": "remove_ammo"}, {"type": "remove_medipacks"}, {"type": "remove_flares"}, {"type": "give_item", "object_id": "small_medipack", "quantity": 1}, {"type": "loop_game"}, {"type": "play_cutscene", "cutscene_id": 8}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "area51_sky.bin", "lara_guns.bin", "compound_cine.bin", "compound_animating_bounds.bin", "compound_textures.bin", ], "unobtainable_kills": 9, "unobtainable_ally_kills": 8, }, // 15. Area 51 { "path": "area51.tr2", "script": "area51.lua", "music_track": 27, "death_tile": "electric", "lara_outfit": "tr3_nevada", "sequence": [ {"type": "loading_screen", "path": "nevada.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "give_item", "object_id": "pistols", "quantity": 1}, {"type": "loop_game"}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "globe_select", "image": "nevada.webp"}, ], "injections": [ "area51_sky.bin", "lara_guns.bin", "globe_model.bin", "area51_textures.bin", ], }, // 16. Antarctica { "path": "antarc.tr2", "music_track": 28, "lara_outfit": "tr3_antarctica", "cold_water": true, "weather_type": "snow", "sequence": [ {"type": "play_fmv", "fmv_id": 3}, {"type": "loading_screen", "path": "antarc.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "play_cutscene", "cutscene_id": 9}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "antarc_airlock.bin", "antarc_sky.bin", "lara_guns.bin", "antarc_door134_frames.bin", "menu_artefacts.bin", ], }, // 17. RX-Tech Mines { "path": "mines.tr2", "script": "mines.lua", "music_track": 30, "lara_outfit": "tr3_antarctica", "cold_water": true, "sequence": [ {"type": "loading_screen", "path": "antarc.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "remove_scions"}, {"type": "loop_game"}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "lara_guns.bin", "drill_collision.bin", "mines_textures.bin", "flamethrower_sfx.bin", ], }, // 18. Lost City of Tinnos { "path": "city.tr2", "music_track": 26, "lara_outfit": "tr3_antarctica", "sequence": [ {"type": "loading_screen", "path": "antarc.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "play_cutscene", "cutscene_id": 10}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "lara_guns.bin", "tinnos_cameras.bin", "tinnos_flames.bin", ], "unobtainable_pickups": 1, }, // 19. Meteorite Cavern { "path": "chamber.tr2", "music_track": 26, "lara_outfit": "tr3_antarctica", "weather_type": "snow", "sequence": [ {"type": "loading_screen", "path": "antarc.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "play_fmv", "fmv_id": 4}, {"type": "play_music", "music_track": 121}, {"type": "display_picture", "credit": true, "path": "theend2.webp", "display_time": 5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credit01.webp", "display_time": 5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credit02.webp", "display_time": 5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credit03.webp", "display_time": 5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credit04.webp", "display_time": 5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credit05.webp", "display_time": 5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credit06.webp", "display_time": 5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credit07.webp", "display_time": 5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credit08.webp", "display_time": 5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credit09.webp", "display_time": 5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "total_stats", "background_path": "theend2.webp"}, {"type": "level_complete"}, ], "injections": [ "cavern_sky.bin", "lara_guns.bin", "cavern_pickup_meshes.bin", "cavern_door131_frames.bin", "flamethrower_sfx.bin", "menu_artefacts.bin", ], }, // 20. All Hallows { "type": "bonus", "path": "stpaul.tr2", "music_track": 30, "lara_outfit": "tr3_catsuit", "weather_type": "rain", "sequence": [ {"type": "loading_screen", "path": "london.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "disable_floor", "height": 10000}, {"type": "loop_game"}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "lara_guns.bin", "stpaul_animating_bounds.bin", "stpaul_textures.bin", "menu_artefacts.bin", ], "unobtainable_pickups": 9, }, ], "cutscenes": [ // Cutscene 1 { "path": "cut6.tr2", "music_track": 64, "lara_outfit": "tr3_classic", "sequence": [ {"type": "loop_game"}, ], "inherit_injections": false, "injections": [ "cut6_setup.bin", "misc_sprites.bin", "font.bin", "lara_outfits.bin", ], }, // Cutscene 2 { "path": "cut9.tr2", "music_track": 69, "lara_outfit": "tr3_classic", "sequence": [ {"type": "loop_game"}, ], "inherit_injections": false, "injections": [ "cut9_setup.bin", "misc_sprites.bin", "font.bin", "india_sky.bin", "lara_outfits.bin", ], }, // Cutscene 3 { "path": "cut1.tr2", "music_track": 68, "lara_outfit": "tr3_south_pacific", "sequence": [ {"type": "loop_game"}, ], "inherit_injections": false, "injections": [ "cut1_setup.bin", "misc_sprites.bin", "font.bin", "crash_sky.bin", "lara_outfits.bin", ], }, // Cutscene 4 { "path": "cut4.tr2", "music_track": 65, "lara_outfit": "tr3_south_pacific", "sequence": [ {"type": "loop_game"}, ], "inherit_injections": false, "injections": [ "cut4_setup.bin", "misc_sprites.bin", "font.bin", "lara_outfits.bin", ], }, // Cutscene 5 { "path": "cut2.tr2", "music_track": 67, "lara_outfit": "tr3_catsuit", "weather_type": "rain", "sequence": [ {"type": "loop_game"}, ], "inherit_injections": false, "injections": [ "cut2_setup.bin", "misc_sprites.bin", "font.bin", "london_sky.bin", "lara_outfits.bin", ], }, // Cutscene 6 { "path": "cut5.tr2", "music_track": 63, "lara_outfit": "tr3_catsuit", "sequence": [ {"type": "loop_game"}, ], "inherit_injections": false, "injections": [ "cut5_setup.bin", "cut5_textures.bin", "misc_sprites.bin", "font.bin", "lara_outfits.bin", ], }, // Cutscene 7 { "path": "cut11.tr2", "music_track": 71, "lara_outfit": "tr3_catsuit", "sequence": [ {"type": "loop_game"}, ], "inherit_injections": false, "injections": [ "cut11_setup.bin", "misc_sprites.bin", "font.bin", "london_sky.bin", "lara_outfits.bin", ], }, // Cutscene 8 { "path": "cut7.tr2", "music_track": 72, "lara_outfit": "tr3_nevada", "sequence": [ {"type": "loop_game"}, ], "inherit_injections": false, "injections": [ "cut7_setup.bin", "misc_sprites.bin", "font.bin", "nevada_sky.bin", "lara_outfits.bin", ], }, // Cutscene 9 { "path": "cut8.tr2", "script": "cut8.lua", "music_track": 70, "lara_outfit": "tr3_nevada", "sequence": [ {"type": "loop_game"}, ], "inherit_injections": false, "injections": [ "cut8_setup.bin", "misc_sprites.bin", "font.bin", "lara_outfits.bin", ], }, // Cutscene 10 { "path": "cut3.tr2", "music_track": 62, "lara_outfit": "tr3_antarctica", "sequence": [ {"type": "loop_game"}, ], "inherit_injections": false, "injections": [ "antarc_sky.bin", "cut3_setup.bin", "cut3_shell.bin", "misc_sprites.bin", "font.bin", "lara_outfits.bin", ], }, // Cutscene 11 { "path": "cut12.tr2", "music_track": 66, "lara_outfit": "tr3_antarctica", "sequence": [ {"type": "loop_game"}, ], "inherit_injections": false, "injections": [ "cut12_setup.bin", "misc_sprites.bin", "font.bin", "lara_outfits.bin", ], }, ], "fmvs": [ {"path": "logo.rpl", "legal": true}, {"path": "intr_eng.rpl"}, {"path": "sail_eng.rpl"}, {"path": "crsh_eng.rpl"}, {"path": "endgame.rpl"}, ], "hidden_config": [ "enable_3d_pickups", // TR3 has no sprite pickups "enable_item_examining", "disable_trex_collision", "fix_bear_ai", "enable_save_crystals", "enable_ps1_crystals", "fix_chainblock_secret_sound", "enable_compass_stats", "enable_crawling", "enable_wading", "restore_ps1_enemies", "change_pierre_spawn", "fix_speeches_killing_music", ], } ================================================ FILE: data/trx/ship/games/tr3/inv_ring.json5 ================================================ [ { "object_id": "O_SMALL_MEDIPACK_OPTION", "frames_total": 26, "open_frame": 25, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 4032, "x_rot_sel": -7296, "y_rot_sel": -4096, "z_trans_sel": 216, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 14, }, { "object_id": "O_LARGE_MEDIPACK_OPTION", "frames_total": 20, "open_frame": 19, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3616, "x_rot_sel": -8160, "y_rot_sel": -4096, "z_trans_sel": 352, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 13, }, { "object_id": "O_FLAREBOX_OPTION", "frames_total": 31, "open_frame": 30, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "y_rot_sel": -8192, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 12, }, { "object_id": "O_PISTOL_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": 2848, "y_rot_sel": -32768, "y_trans_sel": 38, "z_trans_sel": 352, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 1, }, { "object_id": "O_SHOTGUN_OPTION", "frames_total": 13, "open_frame": 12, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": 5120, "y_rot_sel": 30720, "z_trans_sel": 228, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 2, }, { "object_id": "O_MAGNUM_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": 3360, "y_rot_sel": -32768, "z_trans_sel": 362, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 3, }, { "object_id": "O_AUTOS_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": 3360, "y_rot_sel": -32768, "z_trans_sel": 362, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 4, }, { "object_id": "O_DESERT_EAGLE_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": 3360, "y_rot_sel": -32768, "z_trans_sel": 362, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 5, }, { "object_id": "O_UZI_OPTION", "frames_total": 13, "open_frame": 12, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": 2336, "y_rot_sel": -32768, "y_trans_sel": 56, "z_trans_sel": 322, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 6, }, { "object_id": "O_HARPOON_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -736, "y_rot_sel": -19456, "y_trans_sel": 58, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 11, }, { "object_id": "O_M16_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -224, "y_rot_sel": -18432, "y_trans_sel": 84, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 7, }, { "object_id": "O_MP5_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -224, "y_rot_sel": -18432, "y_trans_sel": 84, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 8, }, { "object_id": "O_ROCKET_GUN_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -224, "y_rot_sel": 14336, "y_trans_sel": 56, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 9, }, { "object_id": "O_GRENADE_GUN_OPTION", "frames_total": 12, "open_frame": 11, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -224, "y_rot_sel": 14336, "y_trans_sel": 56, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 10, }, { "object_id": "O_PISTOL_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 1, }, { "object_id": "O_SHOTGUN_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 2, }, { "object_id": "O_MAGNUM_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 3, }, { "object_id": "O_AUTOS_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 4, }, { "object_id": "O_DESERT_EAGLE_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 5, }, { "object_id": "O_UZI_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 6, }, { "object_id": "O_HARPOON_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 11, }, { "object_id": "O_M16_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 7, }, { "object_id": "O_MP5_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 8, }, { "object_id": "O_ROCKET_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 9, }, { "object_id": "O_GRENADE_AMMO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "x_rot_sel": -3808, "z_trans_sel": 296, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 10, }, { "object_id": "O_SCION_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 109, }, { "object_id": "O_LEADBAR_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3616, "x_rot_sel": -8160, "y_rot_sel": -4096, "z_trans_sel": 352, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 100, }, { "object_id": "O_PICKUP_OPTION_1", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 111, }, { "object_id": "O_PICKUP_OPTION_2", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 110, }, { "object_id": "O_PUZZLE_OPTION_1", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 108, }, { "object_id": "O_PUZZLE_OPTION_2", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 107, }, { "object_id": "O_PUZZLE_OPTION_3", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 106, }, { "object_id": "O_PUZZLE_OPTION_4", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 105, }, { "object_id": "O_KEY_OPTION_1", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 101, }, { "object_id": "O_KEY_OPTION_2", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 102, }, { "object_id": "O_KEY_OPTION_3", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 103, }, { "object_id": "O_KEY_OPTION_4", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 104, }, { "object_id": "O_STOPWATCH_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 3200, "y_trans_sel": -135, "z_trans_sel": 320, "meshes_sel": -1, "meshes_drawn": -1, }, { "object_id": "O_COMPASS_OPTION", "frames_total": 25, "open_frame": 10, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 4352, "x_rot_sel": -8192, "z_trans_sel": 456, "meshes_sel": 0b00000101, "meshes_drawn": 0b00000101, }, { "object_id": "O_GLOBE_SELECT_OPTION", "frames_total": 160, "anim_direction": 1, "anim_speed": 1, "meshes_sel": -1, "meshes_drawn": -1, "y_trans_sel": -176, "z_trans_sel": 256, "which_meshes": 0xFFFFFFF7, "drawn_meshes": 0xFFFFFFF7, "inv_pos": 300, }, { "object_id": "O_PASSPORT_OPTION", "frames_total": 30, "open_frame": 14, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 4640, "x_rot_sel": -4320, "z_trans_sel": 384, "meshes_sel": 0b00010011, "meshes_drawn": 0b00010011, "inv_pos": 200, }, { "object_id": "O_DETAIL_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 4224, "x_rot_sel": -7232, "y_trans_sel": 16, "z_trans_sel": 444, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 201, }, { "object_id": "O_SOUND_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 4832, "x_rot_sel": -5408, "y_rot_sel": -3072, "y_trans_sel": -2, "z_trans_sel": 350, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 202, }, { "object_id": "O_CONTROL_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 5504, "x_rot_sel": -2560, "x_rot_nosel": 5632, "y_rot_sel": 13312, "y_trans_sel": 46, "z_trans_sel": 508, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 203, }, { "object_id": "O_PHOTO_OPTION", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 4640, "x_rot_sel": -4320, "z_trans_sel": 384, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 205, }, { "object_id": "O_PDA_OPTION", "frames_total": 39, "open_frame": 19, "anim_direction": 1, "x_rot_pt_sel": 4640, "z_trans_sel": 384, "meshes_sel": 0b00000011, "meshes_drawn": 0b00000011, "inv_pos": 204, }, { "object_id": "O_QUEST_OPTION_1", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 112, }, { "object_id": "O_QUEST_OPTION_2", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 113, }, { "object_id": "O_QUEST_OPTION_3", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 114, }, { "object_id": "O_QUEST_OPTION_4", "frames_total": 1, "anim_direction": 1, "anim_speed": 1, "x_rot_pt_sel": 7200, "x_rot_sel": -4352, "z_trans_sel": 256, "meshes_sel": -1, "meshes_drawn": -1, "inv_pos": 115, }, ] ================================================ FILE: data/trx/ship/games/tr3/scripts/area51.lua ================================================ trx.events.before_level_file(function(level) trx.creatures.add_ally(trx.catalog.objects.prisoner) end) ================================================ FILE: data/trx/ship/games/tr3/scripts/compound.lua ================================================ trx.events.before_level_file(function(level) trx.creatures.add_ally(trx.catalog.objects.prisoner) end) ================================================ FILE: data/trx/ship/games/tr3/scripts/crash.lua ================================================ trx.events.before_level_file(function(level) trx.creatures.add_ally(trx.catalog.objects.sthpac_mercenary) end) ================================================ FILE: data/trx/ship/games/tr3/scripts/cut8.lua ================================================ local drink_frame_num = 737 local equipment_set = false trx.events.after_control(function() local lara_item = trx.lara.item if lara_item.frame >= drink_frame_num and not equipment_set then trx.lara.set_extra_equipment(trx.lara.mesh.hand_r, trx.lara.extra_mesh.drink_can) equipment_set = true end end) ================================================ FILE: data/trx/ship/games/tr3/scripts/jungle.lua ================================================ trx.events.before_level_file(function(level) trx.creatures.add_ally(trx.catalog.objects.monkey) end) ================================================ FILE: data/trx/ship/games/tr3/scripts/mines.lua ================================================ trx.events.before_level_file(function(level) trx.creatures.add_ally(trx.catalog.objects.rx_worker_3) end) ================================================ FILE: data/trx/ship/games/tr3/scripts/tower.lua ================================================ trx.events.before_level_file(function(level) trx.creatures.add_ally(trx.catalog.objects.punk_1) end) ================================================ FILE: data/trx/ship/games/tr3/scripts/zoo.lua ================================================ trx.events.before_level_file(function(level) trx.creatures.add_ally(trx.catalog.objects.monkey) end) ================================================ FILE: data/trx/ship/games/tr3/strings-de.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "general": { "globe_select": { "area_1": "\\{review}Indien", "area_2": "\\{review}Südseeinseln", "area_3": "\\{review}Nevada-Wüste", "area_4": "\\{review}", "area_5": "\\{review}London", "area_6": "\\{review}Antarktis", }, "inventory_ring": { "heading_fmt": "\\{color 5}%s", "item_count_fmt": "\\{color 3}%s", "object_name_fmt": "\\{color 5}%s", }, "misc": { "empty_slot_fmt": "- Leerer Slot -", }, "overlay": { "item_count_fmt_pc": "%s", "item_count_fmt_ps1": "\\{color 3}%s", }, "pause": { "paused": "\\{color 5}Pause", }, "stats": { "assault_other_times_fmt": "\\{review}\\{color 2}%s", "gym_assault_course": "\\{review}\\{color 5}Angriffskurs", "gym_racetrack_course": "\\{review}\\{color 5}Rennstrecke", } }, "objects": { "large_medipack": { "name": "Großes Medi-Pack", }, "quest_1": { "description": "", "name": "\\{review}Infada-Stein", }, "quest_2": { "description": "", "name": "\\{review}Element 115", }, "quest_3": { "description": "", "name": "\\{review}Auge des Isis", }, "quest_4": { "description": "", "name": "\\{review}Ora-Dolch", }, "small_medipack": { "name": "Kleines Medi-Pack", } }, "cutscenes": [ { "title": "Zwischensequenz 1", }, { "title": "Zwischensequenz 2", }, { "title": "Zwischensequenz 3", }, { "title": "Zwischensequenz 4", }, { "title": "Zwischensequenz 5", }, { "title": "Zwischensequenz 6", }, { "title": "Zwischensequenz 7", }, { "title": "Zwischensequenz 8", }, { "title": "Zwischensequenz 9", }, { "title": "Zwischensequenz 10", }, { "title": "Zwischensequenz 11", } ], "levels": [ { "title": "Laras Anwesen", "objects": { "key_1": { "name": "Schlüssel zur Rennstrecke", } } }, { "title": "Dschungel", "objects": { "key_4": { "name": "Indra-Schlüssel", } } }, { "title": "Templeruine", "objects": { "key_1": { "name": "Schlüssel des Ganesha", }, "puzzle_1": { "name": "Säbel", }, "puzzle_2": { "name": "Säbel", } } }, { "title": "Der Ganges", "objects": { "key_1": { "name": "Schlüssel zum Tor", } } }, { "title": "Kaliya Höhlen", }, { "title": "Küstendorf", "objects": { "key_1": { "name": "Schlüssel des Schmugglers", }, "puzzle_1": { "name": "Schlangenstein", } } }, { "title": "Absturzstelle", "objects": { "key_1": { "name": "Commander Bishop's Schlüssel", }, "key_2": { "name": "Lt. Tuckerman's Schlüssel", }, "pickup_1": { "name": "Sumpfkarte", "description": "", } } }, { "title": "Madubu Schlucht", }, { "title": "Punatempel", }, { "title": "Kai an der Themse", "objects": { "key_1": { "name": "Schlüssel zum Belüftungsraum", }, "key_2": { "name": "Schlüssel zur Kathedrale", } } }, { "title": "Aldwych", "objects": { "key_1": { "name": "Wartungsraumschlüssel", }, "key_2": { "name": "Solomon's Schlüssel", }, "key_3": { "name": "Solomon's Schlüssel", }, "puzzle_1": { "name": "Alter Penny", }, "puzzle_2": { "name": "Ticket", }, "puzzle_3": { "name": "Steinmetzhammer", }, "puzzle_4": { "name": "Verzierter Stern", } } }, { "title": "Lud's Gate", "objects": { "key_1": { "name": "Schlüssel zum Heizraum", }, "puzzle_1": { "name": "Balsamierungs Öl", } } }, { "title": "Innenstadt", }, { "title": "Wüste von Nevada", "objects": { "key_1": { "name": "Zugangskarte zum Generator", }, "key_2": { "name": "Zündschlüssel", } } }, { "title": "Hochsicherheitstrakt", "objects": { "key_1": { "name": "Codekarte Typ A", }, "key_2": { "name": "Codekarte Typ B", }, "puzzle_1": { "name": "Blauer Sicherheitsstecker", }, "puzzle_2": { "name": "Gelber Sicherheitsstecker", } } }, { "title": "Area 51", "objects": { "key_1": { "name": "Abschusscode", }, "puzzle_1": { "name": "Zugangsstecker Turm", }, "puzzle_2": { "name": "CD zur Codefreigabe", }, "puzzle_3": { "name": "CD zur Codefreigabe", }, "puzzle_4": { "name": "Zugangsstecker Hangar", } } }, { "title": "Antarktis", "objects": { "key_1": { "name": "Schlüssel zur Hütte", }, "puzzle_1": { "name": "Brechstange", }, "puzzle_2": { "name": "Torschlüssel", } } }, { "title": "RX-Techs Bergwerk", "objects": { "puzzle_1": { "name": "Brechstange", }, "puzzle_2": { "name": "Bleisäurebattarie", }, "puzzle_3": { "name": "Anlasser für die Winch", } } }, { "title": "Die vergessene Stadt Tinnos", "objects": { "key_1": { "name": "Uli-Schlüssel", }, "puzzle_1": { "name": "Ozeanische Maske", } } }, { "title": "Höhle des Meteoriten", }, { "title": "All Hallows", "objects": { "key_1": { "name": "Schlüssel zum Gewölbe", } } } ] } ================================================ FILE: data/trx/ship/games/tr3/strings-it.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "general": { "globe_select": { "area_1": "India", "area_2": "Isole del Sud Pacifico", "area_3": "Nevada", "area_4": " ", "area_5": "Londra", "area_6": "Antartide", }, "inventory_ring": { "heading_fmt": "\\{color 5}%s", "item_count_fmt": "\\{color 3}%s", "object_name_fmt": "\\{color 5}%s", }, "misc": { "empty_slot_fmt": "- Slot Vuoto -", }, "overlay": { "item_count_fmt_pc": "%s", "item_count_fmt_ps1": "\\{color 3}%s", }, "pause": { "paused": "\\{color 5}Pausa", }, "stats": { "assault_other_times_fmt": "\\{color 2}%s", "gym_assault_course": "\\{color 5}Corso d'Addestramento", "gym_racetrack_course": "\\{color 5}Corso di Guida", } }, "objects": { "large_medipack": { "name": "Kit Medico Grande", }, "quest_1": { "description": "", "name": "Pietra Infada", }, "quest_2": { "description": "", "name": "Elemento 115", }, "quest_3": { "description": "", "name": "Occhio di Iside", }, "quest_4": { "description": "", "name": "Pugnale Ora", }, "small_medipack": { "name": "Kit Medico Piccolo", } }, "cutscenes": [ { "title": "Intermezzo 1", }, { "title": "Intermezzo 2", }, { "title": "Intermezzo 3", }, { "title": "Intermezzo 4", }, { "title": "Intermezzo 5", }, { "title": "Intermezzo 6", }, { "title": "Intermezzo 7", }, { "title": "Intermezzo 8", }, { "title": "Intermezzo 9", }, { "title": "Intermezzo 10", }, { "title": "Intermezzo 11", } ], "levels": [ { "title": "Casa di Lara", "objects": { "key_1": { "name": "Chiave del Circuito", } } }, { "title": "Giungla", "objects": { "key_4": { "name": "Chiave di Indra", } } }, { "title": "Rovine del Tempio", "objects": { "key_1": { "name": "Chiave di Ganesha", }, "puzzle_1": { "name": "Scimitarra", }, "puzzle_2": { "name": "Scimitarra", } } }, { "title": "Il Fiume Gange", "objects": { "key_1": { "name": "Chiave del Cancello", } } }, { "title": "Caverne di Kaliya", }, { "title": "Villaggio Costiero", "objects": { "key_1": { "name": "Chiave del Contrabbandiere", }, "puzzle_1": { "name": "Pietra del Serpente", } } }, { "title": "Luogo dello Schianto", "objects": { "key_1": { "name": "Chiave del Comandante Bishop", }, "key_2": { "name": "Chiave del Tenente Tuckerman", }, "pickup_1": { "name": "Mappa della Palude", "description": "", } } }, { "title": "Gola di Madubu", }, { "title": "Tempio di Puna", }, { "title": "Molo sul Tamigi", "objects": { "key_1": { "name": "Chiave della Canna Fumaria", }, "key_2": { "name": "Chiave della Cattedrale", } } }, { "title": "Aldwych", "objects": { "key_1": { "name": "Chiave di Manutenzione", }, "key_2": { "name": "Chiave di Salomone", }, "key_3": { "name": "Chiave di Salomone", }, "puzzle_1": { "name": "Vecchio Penny", }, "puzzle_2": { "name": "Biglietto", }, "puzzle_3": { "name": "Martello Massonico", }, "puzzle_4": { "name": "Stella Ornata", } } }, { "title": "Porta di Lud", "objects": { "key_1": { "name": "Chiave della Caldaia", }, "puzzle_1": { "name": "Fluido di Imbalsamazione", } } }, { "title": "Città di Londra", }, { "title": "Deserto del Nevada", "objects": { "key_1": { "name": "Chiave d'Accesso al Generatore", }, "key_2": { "name": "Chiave del Detonatore", } } }, { "title": "Reparto di Massima Sicurezza", "objects": { "key_1": { "name": "Scheda Elettronica di Tipo A", }, "key_2": { "name": "Scheda Elettronica di Tipo B", }, "puzzle_1": { "name": "Chiave di Sicurezza Elettronica Blu", }, "puzzle_2": { "name": "Chiave di Sicurezza Elettronica Gialla", } } }, { "title": "Area 51", "objects": { "key_1": { "name": "Scheda Codice di Lancio", }, "puzzle_1": { "name": "Chiave di Accesso alla Torre", }, "puzzle_2": { "name": "Disco del Codice d'Accesso", }, "puzzle_3": { "name": "Disco del Codice d'Accesso", }, "puzzle_4": { "name": "Chiave di Accesso all'Hangar", } } }, { "title": "Antartide", "objects": { "key_1": { "name": "Chiave della Capanna", }, "puzzle_1": { "name": "Piede di Porco", }, "puzzle_2": { "name": "Chiave di Controllo del Cancello", } } }, { "title": "Miniere della RX-Tech", "objects": { "puzzle_1": { "name": "Piede di Porco", }, "puzzle_2": { "name": "Batteria al Piombo", }, "puzzle_3": { "name": "Avviamento dell'Argano", } } }, { "title": "Città Perduta di Tinnos", "objects": { "key_1": { "name": "Chiave Uli", }, "puzzle_1": { "name": "Maschera Oceaniana", } } }, { "title": "Caverna del Meteorite", }, { "title": "All Hallows", "objects": { "key_1": { "name": "Chiave della Cripta", } } } ] } ================================================ FILE: data/trx/ship/games/tr3/strings-pl.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "general": { "globe_select": { "area_1": "Indie", "area_2": "Wyspy Południowego Pacyfiku", "area_3": "Pustynia Nevada", "area_4": " ", "area_5": "Londyn", "area_6": "\\{Antarktyda", }, "inventory_ring": { "heading_fmt": "\\{color 5}%s", "item_count_fmt": "\\{color 3}%s", "object_name_fmt": "\\{color 5}%s", }, "misc": { "empty_slot_fmt": "- Pusty Slot -", }, "overlay": { "item_count_fmt_pc": "%s", "item_count_fmt_ps1": "\\{color 3}%s", }, "pause": { "paused": "\\{color 5}Pauza", }, "stats": { "assault_other_times_fmt": "\\{color 2}%s", "gym_assault_course": "\\{color 5}Tor Przeszkód", "gym_racetrack_course": "\\{color 5}Tor Wyścigowy", } }, "cutscenes": [ { "title": "Scena Przerywnikowa 1", }, { "title": "Scena Przerywnikowa 2", }, { "title": "Scena Przerywnikowa 3", }, { "title": "Scena Przerywnikowa 4", }, { "title": "Scena Przerywnikowa 5", }, { "title": "Scena Przerywnikowa 6", }, { "title": "Scena Przerywnikowa 7", }, { "title": "Scena Przerywnikowa 8", }, { "title": "Scena Przerywnikowa 9", }, { "title": "Scena Przerywnikowa 10", }, { "title": "Scena Przerywnikowa 11", } ], "levels": [ { "title": "Dom Lary", "objects": { "key_1": { "name": "Klucz do toru wyścigowego", } } }, { "title": "Dżungla", "objects": { "key_4": { "name": "Klucz Indry", } } }, { "title": "Ruiny Świątyni", "objects": { "key_1": { "name": "Klucz Ganeszy", }, "puzzle_1": { "name": "Szabla", }, "puzzle_2": { "name": "Szabla", } } }, { "title": "Rzeka Ganges", "objects": { "key_1": { "name": "Klucz do bramy", } } }, { "title": "Jaskinie Kaliyi", }, { "title": "Wioska Nadbrzeżna", "objects": { "key_1": { "name": "Klucz przemytnika", }, "puzzle_1": { "name": "Kamień węża", } } }, { "title": "Miejsce Katastrofy", "objects": { "key_1": { "name": "Klucz komandora Bishopa", }, "key_2": { "name": "Klucz porucznika Tuckermana", }, "pickup_1": { "name": "Mapa bagna", "description": "", } } }, { "title": "Wąwóz Madubu", }, { "title": "Świątynia Puny", }, { "title": "Nabrzeże Tamizy", "objects": { "key_1": { "name": "Klucz do kotłowni", }, "key_2": { "name": "Klucz do katedry", } } }, { "title": "Aldwych", "objects": { "key_1": { "name": "Klucz konserwacyjny", }, "key_2": { "name": "Klucz Salomona", }, "key_3": { "name": "Klucz Salomona", }, "puzzle_1": { "name": "Stara moneta", }, "puzzle_2": { "name": "Bilet", }, "puzzle_3": { "name": "Masoński młotek", }, "puzzle_4": { "name": "Ozdobna gwiazda", } } }, { "title": "Brama Lud'a", "objects": { "key_1": { "name": "Klucz do kotłowni", }, "puzzle_1": { "name": "Płyn balsamujący", } } }, { "title": "Miasto", }, { "title": "Pustynia Nevady", "objects": { "key_1": { "name": "Dostęp do generatora", }, "key_2": { "name": "Przełącznik detonatora", } } }, { "title": "Ściśle Strzeżona Baza", "objects": { "key_1": { "name": "Karta dostępu typu A", }, "key_2": { "name": "Karta dostępu typu B", }, "puzzle_1": { "name": "Niebieska przepustka bezpieczeństwa", }, "puzzle_2": { "name": "Żółta przepustka bezpieczeństwa", } } }, { "title": "Strefa 51", "objects": { "key_1": { "name": "Przepustka z kodem startowym", }, "puzzle_1": { "name": "Klucz dostępu do wieży", }, "puzzle_2": { "name": "Dysk z kodem dostępu", }, "puzzle_3": { "name": "Dysk z kodem dostępu", }, "puzzle_4": { "name": "Klucz dostępu do hangaru", } } }, { "title": "Antarktyda", "objects": { "key_1": { "name": "Klucz do chaty", }, "puzzle_1": { "name": "Łom", }, "puzzle_2": { "name": "Klucz do bramy", } } }, { "title": "Kopalnie RX-Tech", "objects": { "puzzle_1": { "name": "Łom", }, "puzzle_2": { "name": "Akumulator", }, "puzzle_3": { "name": "Rozrusznik wyciągarki", } } }, { "title": "Zaginione Miasto Tinnos", "objects": { "key_1": { "name": "Klucz Uli", }, "puzzle_1": { "name": "Maska", } } }, { "title": "Jaskinia Meteorytowa", }, { "title": "Wszystkich Świętych", "objects": { "quest_1": { "name": "Kamień Infada", "description": "", }, "quest_2": { "name": "Pierwiastek 115", "description": "", }, "quest_3": { "name": "Oko Izydy", "description": "", }, "quest_4": { "name": "Sztylet Ora", "description": "", }, "key_1": { "name": "Klucz do skarbca", } } } ] } ================================================ FILE: data/trx/ship/games/tr3/strings.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "general": { "globe_select": { "area_1": "India", "area_2": "South Pacific Islands", "area_3": "Nevada", "area_4": "", "area_5": "London", "area_6": "Antarctica", }, "inventory_ring": { "heading_fmt": "\\{color 5}%s", "item_count_fmt": "\\{color 3}%s", "object_name_fmt": "\\{color 5}%s", }, "misc": { "empty_slot_fmt": "- Empty Slot -", }, "overlay": { "item_count_fmt_pc": "%s", "item_count_fmt_ps1": "\\{color 3}%s", }, "pause": { "paused": "\\{color 5}Paused", }, "stats": { "assault_other_times_fmt": "\\{color 2}%s", "gym_assault_course": "\\{color 5}Assault Course", "gym_racetrack_course": "\\{color 5}Race Track Course", } }, "objects": { "large_medipack": { "name": "Large Medi Pack", }, "quest_1": { "description": "", "name": "Infada Stone", }, "quest_2": { "description": "", "name": "Element 115", }, "quest_3": { "description": "", "name": "Eye of Isis", }, "quest_4": { "description": "", "name": "Ora Dagger", }, "small_medipack": { "name": "Small Medi Pack", } }, "cutscenes": [ { "title": "Cutscene 1", }, { "title": "Cutscene 2", }, { "title": "Cutscene 3", }, { "title": "Cutscene 4", }, { "title": "Cutscene 5", }, { "title": "Cutscene 6", }, { "title": "Cutscene 7", }, { "title": "Cutscene 8", }, { "title": "Cutscene 9", }, { "title": "Cutscene 10", }, { "title": "Cutscene 11", } ], "levels": [ { "title": "Lara's Home", "objects": { "key_1": { "name": "Racetrack Key", } } }, { "title": "Jungle", "objects": { "key_4": { "name": "Indra Key", } } }, { "title": "Temple Ruins", "objects": { "key_1": { "name": "Key of Ganesha", }, "puzzle_1": { "name": "Scimitar", }, "puzzle_2": { "name": "Scimitar", } } }, { "title": "The River Ganges", "objects": { "key_1": { "name": "Gate Key", } } }, { "title": "Caves of Kaliya", }, { "title": "Coastal Village", "objects": { "key_1": { "name": "Smuggler's Key", }, "puzzle_1": { "name": "Serpent Stone", } } }, { "title": "Crash Site", "objects": { "key_1": { "name": "Commander Bishop's Key", }, "key_2": { "name": "Lt. Tuckerman's Key", }, "pickup_1": { "name": "Swamp Map", "description": "", } } }, { "title": "Madubu Gorge", }, { "title": "Temple of Puna", }, { "title": "Thames Wharf", "objects": { "key_1": { "name": "Flue Room Key", }, "key_2": { "name": "Cathedral Key", } } }, { "title": "Aldwych", "objects": { "key_1": { "name": "Maintenance Key", }, "key_2": { "name": "Solomon's Key", }, "key_3": { "name": "Solomon's Key", }, "puzzle_1": { "name": "Old Penny", }, "puzzle_2": { "name": "Ticket", }, "puzzle_3": { "name": "Masonic Mallet", }, "puzzle_4": { "name": "Ornate Star", } } }, { "title": "Lud's Gate", "objects": { "key_1": { "name": "Boiler Room Key", }, "puzzle_1": { "name": "Embalming Fluid", } } }, { "title": "City", }, { "title": "Nevada Desert", "objects": { "key_1": { "name": "Generator Access", }, "key_2": { "name": "Detonator Switch", } } }, { "title": "High Security Compound", "objects": { "key_1": { "name": "Keycard Type A", }, "key_2": { "name": "Keycard Type B", }, "puzzle_1": { "name": "Blue Security Pass", }, "puzzle_2": { "name": "Yellow Security Pass", } } }, { "title": "Area 51", "objects": { "key_1": { "name": "Launch Code Pass", }, "puzzle_1": { "name": "Tower Access Key", }, "puzzle_2": { "name": "Code Clearance Disk", }, "puzzle_3": { "name": "Code Clearance Disk", }, "puzzle_4": { "name": "Hanger Access Key", } } }, { "title": "Antarctica", "objects": { "key_1": { "name": "Hut Key", }, "puzzle_1": { "name": "Crowbar", }, "puzzle_2": { "name": "Gate Control Key", } } }, { "title": "RX-Tech Mines", "objects": { "puzzle_1": { "name": "Crowbar", }, "puzzle_2": { "name": "Lead Acid Battery", }, "puzzle_3": { "name": "Winch Starter", } } }, { "title": "Lost City of Tinnos", "objects": { "key_1": { "name": "Uli Key", }, "puzzle_1": { "name": "Oceanic Mask", } } }, { "title": "Meteorite Cavern", }, { "title": "All Hallows", "objects": { "key_1": { "name": "Vault Key", } } } ] } ================================================ FILE: data/trx/ship/games/tr3/weapons.json5 ================================================ { "LGT_UNARMED": { "sample_num": "SFX_LARA_NO", }, "LGT_FLARE": { "ammo": { "initial_qty": 6, "pickup_qty": 6, "pickup_qty_alt": 8, }, "flash_shade": 2048, "flash_pos": {"x": 11, "y": 32, "z": 80}, "flash_pos_alt": {"x": -6, "y": 6, "z": 80}, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {}, "glow_color": [1, 0.75, 0.125], }, "LGT_PISTOLS": { "type": "WEAPON_TYPE_DUAL_PISTOLS", "lock_angles": [-60, +60, -60, +60], "left_angles": [-170, +60, -80, +80], "right_angles": [-60, +170, -80, +80], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 650, "damage": 1, "ammo": { "initial_qty": 32, "pickup_qty": 32, }, "target_dist": 8.0, "recoil_frame": 9, "flash_time": 3, "flash_shade": 5120, "flash_pos": {"y": 185, "z": 40}, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {}, "glow_color": [1, 0.75, 0.125], "muzzle_pos": {"x": -16, "y": 128, "z": 40}, "muzzle_pos_alt": {"x": 16, "y": 128, "z": 40}, "shell_pos": {"x": 8, "y": 48, "z": 40}, "shell_pos_alt": {"x": -12, "y": 48, "z": 40}, "smoke_count": 28, "sample_num": "SFX_LARA_PISTOLS", }, "LGT_MAGNUMS": { "type": "WEAPON_TYPE_DUAL_PISTOLS", "lock_angles": [-60, +60, -60, +60], "left_angles": [-170, +60, -80, +80], "right_angles": [-60, +170, -80, +80], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 650, "damage": 2, "ammo": { "initial_qty": 50, "pickup_qty": 50, }, "target_dist": 8.0, "recoil_frame": 9, "flash_time": 3, "flash_shade": 4096, "flash_pos": {"y": 155, "z": 55}, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {}, "glow_color": [1, 0.75, 0.125], "muzzle_pos": {"x": -16, "y": 128, "z": 40}, "muzzle_pos_alt": {"x": 16, "y": 128, "z": 40}, "shell_pos": {"x": 8, "y": 48, "z": 40}, "shell_pos_alt": {"x": -12, "y": 48, "z": 40}, "smoke_count": 28, "sample_num": "SFX_LARA_MAGNUMS", "is_available": false, }, "LGT_AUTOS": { "type": "WEAPON_TYPE_DUAL_PISTOLS", "lock_angles": [-60, +60, -60, +60], "left_angles": [-170, +60, -80, +80], "right_angles": [-60, +170, -80, +80], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 650, "damage": 2, "ammo": { "initial_qty": 40, "pickup_qty": 40, }, "target_dist": 8.0, "recoil_frame": 9, "flash_time": 3, "flash_shade": 4096, "flash_pos": {"y": 215, "z": 65}, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {}, "glow_color": [1, 0.75, 0.125], "muzzle_pos": {"x": -16, "y": 128, "z": 40}, "muzzle_pos_alt": {"x": 16, "y": 128, "z": 40}, "shell_pos": {"x": 8, "y": 48, "z": 40}, "shell_pos_alt": {"x": -12, "y": 48, "z": 40}, "smoke_count": 28, "sample_num": "SFX_LARA_AUTOS", "is_available": false, }, "LGT_DESERT_EAGLE": { "type": "WEAPON_TYPE_SINGLE_PISTOL", "lock_angles": [-60, +60, -60, +60], "left_angles": [-10, +10, -80, +80], "right_angles": [0, 0, 0, 0], "aim_speed": 10, "shot_accuracy": 4, "gun_height": 650, "damage": 21, "ammo": { "initial_qty": 10, "pickup_qty": 10, }, "target_dist": 8.0, "recoil_frame": 16, "flash_time": 3, "flash_shade": 4096, "flash_pos": {"y": 215, "z": 65}, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {}, "glow_color": [1, 0.75, 0.125], "muzzle_pos": {"x": -32, "y": 160, "z": 56}, "muzzle_pos_alt": {"x": 16, "y": 160, "z": 56}, "shell_pos": {"x": 16, "y": 40, "z": 56}, "shell_pos_alt": {"x": -16, "y": 40, "z": 56}, "smoke_count": 28, "sample_num": "SFX_LARA_DESERT_EAGLE", }, "LGT_UZIS": { "type": "WEAPON_TYPE_DUAL_PISTOLS", "lock_angles": [-60, +60, -60, +60], "left_angles": [-170, +60, -80, +80], "right_angles": [-60, +170, -80, +80], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 650, "damage": 1, "ammo": { "initial_qty": 40, "pickup_qty": 40, }, "target_dist": 8.0, "recoil_frame": 3, "flash_time": 3, "flash_shade": 2560, "flash_pos": {"y": 200, "z": 50}, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {}, "glow_color": [1, 0.75, 0.125], "muzzle_pos": {"x": -16, "y": 140, "z": 48}, "muzzle_pos_alt": {"x": 8, "y": 140, "z": 48}, "shell_pos": {"x": 8, "y": 35, "z": 48}, "shell_pos_alt": {"x": -16, "y": 35, "z": 48}, "smoke_count": 28, "sample_num": "SFX_LARA_UZI_FIRE", }, "LGT_SHOTGUN": { "type": "WEAPON_TYPE_RIFLE", "lock_angles": [-60, +60, -55, +55], "left_angles": [-80, +80, -65, +65], "right_angles": [-80, +80, -65, +65], "aim_speed": 10, "shot_accuracy": 0, "gun_height": 500, "damage": 3, "ammo": { "initial_qty": 2, "pickup_qty": 2, }, "target_dist": 8.0, "equip_anim_idx": 1, "draw_frame": 10, "undraw_frame": 21, "recoil_frame": 9, "flash_time": 3, "flash_shade": 2560, "flash_pos": {"y": 285}, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {}, "glow_color": [1, 0.75, 0.125], "muzzle_pos": {"x": -16, "y": 228, "z": 96}, "shell_pos": {"x": 16, "y": 114, "z": 32}, "smoke_count": 32, "sample_num": "SFX_LARA_SHOTGUN", }, "LGT_M16": { "type": "WEAPON_TYPE_RIFLE", "lock_angles": [-60, +60, -55, +55], "left_angles": [-80, +80, -65, +65], "right_angles": [-80, +80, -65, +65], "aim_speed": 10, "shot_accuracy": 4, "gun_height": 500, "damage": 3, "ammo": { "initial_qty": 40, "pickup_qty": 40, }, "target_dist": 12.0, "equip_anim_idx": 1, "draw_frame": 10, "undraw_frame": 21, "recoil_frame": 0, "flash_time": 3, "flash_shade": 2560, "sample_num": "", "flash_pos": {"y": 400, "z": 99}, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {"z": -65}, "glow_color": [1, 0.75, 0.125], "muzzle_pos": {"x": 0, "y": 228, "z": 96}, "shell_pos": {"x": 16, "y": 2, "z": 64}, "smoke_count": 24, "is_available": false, }, "LGT_MP5": { "type": "WEAPON_TYPE_RIFLE", "lock_angles": [-60, +60, -55, +55], "left_angles": [-80, +80, -65, +65], "right_angles": [-80, +80, -65, +65], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 500, "damage": 3, "ammo": { "initial_qty": 60, "pickup_qty": 60, }, "target_dist": 12.0, "equip_anim_idx": 1, "draw_frame": 16, "undraw_frame": 21, "recoil_frame": 0, "flash_time": 3, "flash_shade": 2560, "flash_pos": {"y": 332, "z": 96}, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {"z": -65}, "glow_color": [1, 0.75, 0.125], "muzzle_pos": {"x": 0, "y": 228, "z": 96}, "shell_pos": {"x": 16, "y": 2, "z": 64}, "smoke_count": 24, "sample_num": "", }, "LGT_GRENADE": { "type": "WEAPON_TYPE_RIFLE", "lock_angles": [-60, +60, -55, +55], "left_angles": [-80, +80, -65, +65], "right_angles": [-80, +80, -65, +65], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 500, "damage": 20, "ammo": { "initial_qty": 2, "pickup_qty": 2, }, "target_dist": 8.0, "equip_anim_idx": 0, "draw_frame": 13, "undraw_frame": 14, "recoil_frame": 0, "flash_time": 2, "muzzle_pos": {"x": 0, "y": 180, "z": 80}, "smoke_count": 32, "sample_num": "", }, "LGT_ROCKET": { "type": "WEAPON_TYPE_RIFLE", "lock_angles": [-60, +60, -55, +55], "left_angles": [-80, +80, -65, +65], "right_angles": [-80, +80, -65, +65], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 500, "damage": 30, "ammo": { "initial_qty": 1, "pickup_qty": 1, }, "target_dist": 8.0, "equip_anim_idx": 1, "draw_frame": 12, "undraw_frame": 21, "recoil_frame": 0, "flash_time": 2, "muzzle_pos": {"x": 0, "y": 84, "z": 72}, "smoke_count": 32, "sample_num": "", }, "LGT_HARPOON": { "type": "WEAPON_TYPE_RIFLE", "lock_angles": [-60, +60, -65, +65], "left_angles": [-80, +80, -75, +75], "right_angles": [-80, +80, -75, +75], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 500, "damage": 6, "ammo": { "initial_qty": 3, "pickup_qty": 3, }, "target_dist": 8.0, "equip_anim_idx": 1, "draw_frame": 10, "undraw_frame": 21, "recoil_frame": 0, "flash_time": 2, "sample_num": "", }, "LGT_SKIDOO": { "type": "WEAPON_TYPE_MOUNTED", "lock_angles": [-30, 30, -55, 55], "left_angles": [-30, 30, -55, 55], "right_angles": [-30, 30, -55, 55], "aim_speed": 10, "shot_accuracy": 8, "gun_height": 400, "damage": 3, "target_dist": 8.0, "recoil_frame": 0, "flash_time": 2, "flash_shade": 5120, "flash_pos": {"y": 185, "z": 40}, "flash_color": [0.75, 0.56, 0.0], "glow_pos": {}, "glow_color": [1, 0.75, 0.125], "sample_num": "SFX_LARA_UZI_FIRE", }, } ================================================ FILE: data/trx/ship/games/tr3-la/gameflow.json5 ================================================ { // NOTE: bad changes to this file may result in crashes. // Lines starting with double slashes are comments and are ignored. "engine": 3, "extends": "tr3", "name": "The Lost Artifact", "main_menu_picture": "title_eu_la.webp", "savegame_file_fmt": "save_trla_%02d.dat", "enable_tr2_item_drops": true, "convert_dropped_guns": true, "title": { "path": ["title_la.tr2", "title.tr2"], "music_track": 5, "sequence": [ {"type": "display_picture", "path": "legal_eu_la.webp", "legal": true}, {"type": "play_fmv", "fmv_id": 0}, {"type": "play_fmv", "fmv_id": 1}, {"type": "exit_to_title"}, ], }, "ambient_tracks": [ 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 73, 74, 75, 76, 77, 78, ], "sfx_path": ["main_la.sfx", "main.sfx"], "injections": [ "font.bin", "lara_animations.bin", "pda_model.bin", "lara_extra.bin", "misc_sprites.bin", "lara_outfits.bin", "pickup_aid.bin", ], "levels": [ // 0. Legacy savegame placeholder {"type": "dummy"}, // 1. Highland Fling { "path": "scotland.tr2", "music_track": 36, "lara_outfit": "tr3_classic", "weather_type": "rain", "sequence": [ {"type": "loading_screen", "path": "highland.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "give_item", "object_id": "small_medipack"}, {"type": "give_item", "object_id": "large_medipack"}, {"type": "give_item", "object_id": "flare", "quantity": 2}, {"type": "loop_game"}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "lara_guns.bin", "scotland_sky.bin", ], }, // 2. Willard's Lair { "path": "willsden.tr2", "music_track": 30, "lara_outfit": "tr3_classic", "sequence": [ {"type": "loading_screen", "path": "willard.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "lara_guns.bin", "scotland_sky.bin", "willsden_heli.bin", ], "unobtainable_kills": 1, }, // 3. Shakespeare Cliff { "path": "chunnel.tr2", "music_track": 74, "lara_outfit": "tr3_nevada", "sequence": [ {"type": "loading_screen", "path": "chunnel.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "lara_guns.bin", "cliff_door132_frames.bin", ], }, // 4. Sleeping with the Fishes { "path": "undersea.tr2", "music_track": 27, "lara_outfit": "tr3_catsuit", "sequence": [ {"type": "loading_screen", "path": "undersea.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "antarc_airlock.bin", "lara_guns.bin", "undersea_animating_bounds.bin", "undersea_train.bin", ], }, // 5. It's a Madhouse! { "path": "zoo.tr2", "script": "zoo.lua", "music_track": 34, "lara_outfit": "tr3_catsuit", "sequence": [ {"type": "loading_screen", "path": "zoo.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "remove_scions"}, {"type": "loop_game"}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "level_complete"}, ], "injections": [ "lara_guns.bin", "zoo_train.bin", ], }, // 6. Reunion { "path": "slinc.tr2", "music_track": 26, "lara_outfit": "tr3_catsuit", "sequence": [ {"type": "loading_screen", "path": "slinc.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "loop_game"}, {"type": "play_music", "music_track": 14}, {"type": "level_stats"}, {"type": "play_music", "music_track": 121}, {"type": "display_picture", "credit": true, "path": "theend2_la.webp", "display_time": 5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credit01_eu_la.webp", "display_time": 5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credit02_la.webp", "display_time": 5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credit03_la.webp", "display_time": 5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credit04_la.webp", "display_time": 5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credit05_la.webp", "display_time": 5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credit06_la.webp", "display_time": 5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credit07_la.webp", "display_time": 5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credit08_la.webp", "display_time": 5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "display_picture", "credit": true, "path": "credit09_la.webp", "display_time": 5, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "total_stats", "background_path": "theend2_la.webp"}, {"type": "level_complete"}, ], "injections": [ "lara_guns.bin", "reunion_flames.bin", ], "unobtainable_pickups": 1, "unobtainable_secrets": 1, }, ], "fmvs": [ {"path": "logo.rpl", "legal": true}, ], "hidden_config": [ "enable_3d_pickups", // TR3 has no sprite pickups "enable_item_examining", "disable_trex_collision", "fix_bear_ai", "enable_save_crystals", "enable_ps1_crystals", "fix_chainblock_secret_sound", "enable_compass_stats", "enable_crawling", "enable_wading", "restore_ps1_enemies", "change_pierre_spawn", "fix_speeches_killing_music", "fix_pipeman_aim", "enable_cinematics", ], } ================================================ FILE: data/trx/ship/games/tr3-la/strings-de.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Laras Anwesen", "objects": { "key_1": { "name": "Schlüssel zur Rennstrecke", } } }, { "title": "Das Hochland", "objects": { "puzzle_1": { "name": "Brechstange", }, "puzzle_2": { "name": "Distelstein", } } }, { "title": "Willards Unterschlupf", "objects": { "key_1": { "name": "Steinhaufen-Schlüssel", }, "puzzle_1": { "name": "Brechstange", } } }, { "title": "Shakespeare-Klippe", "objects": { "key_1": { "name": "Bohrer-Aktivierungs-Karte", }, "puzzle_1": { "name": "Pumoen-Zugangs-Disk", } } }, { "title": "Mit den Fischen schlafen", "objects": { "puzzle_1": { "name": "Blaue Stromkreis-Glühbirne", }, "puzzle_2": { "name": "Mutant-Probe", }, "puzzle_3": { "name": "Mutant-Probe", }, "puzzle_4": { "name": "Gelbe Stromkreis-Glühbirne", } } }, { "title": "Es ist ein Irrenhaus!", "objects": { "key_1": { "name": "Zoo-Schlüssel", }, "key_4": { "name": "Vogelhaus-Schlüssel", }, "puzzle_1": { "name": "Hand von Rathmore", } } }, { "title": "Wiedervereinigung", "objects": { "puzzle_1": { "name": "Hand von Rathmore", } } } ] } ================================================ FILE: data/trx/ship/games/tr3-la/strings-it.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Casa di Lara", "objects": { "key_1": { "name": "Chiave del Circuito", } } }, { "title": "Balzo nell'Altopiano", "objects": { "puzzle_1": { "name": "Piede di Porco", }, "puzzle_2": { "name": "Gemma del Cardo", } } }, { "title": "Nascondiglio di Willard", "objects": { "key_1": { "name": "Chiave del Tumulo", }, "puzzle_1": { "name": "Piede di Porco", } } }, { "title": "Scogliera di Shakespeare", "objects": { "key_1": { "name": "Scheda di Attivazione della Trivella", }, "puzzle_1": { "name": "Disco di Accesso alla Pompa", } } }, { "title": "Dormire con i Pesci", "objects": { "puzzle_1": { "name": "Lampadina", }, "puzzle_2": { "name": "Campione Mutante", }, "puzzle_3": { "name": "Campione Mutante", }, "puzzle_4": { "name": "Lampadina", } } }, { "title": "È un Manicomio!", "objects": { "key_1": { "name": "Chiave dello Zoo", }, "key_4": { "name": "Chiave della Voliera", }, "puzzle_1": { "name": "Mano di Rathmore", } } }, { "title": "Riunione", "objects": { "puzzle_1": { "name": "Mano di Rathmore", } } } ] } ================================================ FILE: data/trx/ship/games/tr3-la/strings-pl.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "objects": { "quest_1": { "description": "", "name": "Dłoń Rathmore'a", } }, "levels": [ { "title": "Dom Lary", "objects": { "key_1": { "name": "Klucz do toru wyścigowego", } } }, { "title": "Wypad na wzgórza", "objects": { "puzzle_1": { "name": "Łom", }, "puzzle_2": { "name": "Kamień Ostu", } } }, { "title": "Kryjówka Willarda", "objects": { "key_1": { "name": "Klucz do kurhanu", }, "puzzle_1": { "name": "Łom", } } }, { "title": "Klif Szekspira", "objects": { "key_1": { "name": "Karta uruchamiająca wiertło", }, "puzzle_1": { "name": "Dysk dostępu do pompy", } } }, { "title": "Na dnie z rybami", "objects": { "puzzle_1": { "name": "Żarówka układu", }, "puzzle_2": { "name": "Próbka mutanta", }, "puzzle_3": { "name": "Próbka mutanta", }, "puzzle_4": { "name": "Żarówka układu", } } }, { "title": "Dom wariatów!", "objects": { "key_1": { "name": "Klucz do zoo", }, "key_4": { "name": "Klucz do woliery", }, "puzzle_1": { "name": "Dłoń Rathmore'a", } } }, { "title": "Spotkanie po latach", "objects": { "puzzle_1": { "name": "Dłoń Rathmore'a", } } } ] } ================================================ FILE: data/trx/ship/games/tr3-la/strings.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "objects": { "orca": { "name": "Dolphin", }, "quest_1": { "description": "", "name": "Hand of Rathmore", } }, "levels": [ { "title": "Lara's Home", "objects": { "key_1": { "name": "Racetrack Key", } } }, { "title": "Highland Fling", "objects": { "puzzle_1": { "name": "Crowbar", }, "puzzle_2": { "name": "Thistle Stone", } } }, { "title": "Willard's Lair", "objects": { "key_1": { "name": "Cairn Key", }, "puzzle_1": { "name": "Crowbar", } } }, { "title": "Shakespeare Cliff", "objects": { "key_1": { "name": "Drill Activator Card", }, "puzzle_1": { "name": "Pump Access Disk", } } }, { "title": "Sleeping with the Fishes", "objects": { "puzzle_1": { "name": "Circuit Bulb", }, "puzzle_2": { "name": "Mutant Sample", }, "puzzle_3": { "name": "Mutant Sample", }, "puzzle_4": { "name": "Circuit Bulb", } } }, { "title": "It's a Madhouse!", "objects": { "key_1": { "name": "Zoo Key", }, "key_4": { "name": "Aviary Key", }, "puzzle_1": { "name": "Hand of Rathmore", } } }, { "title": "Reunion", "objects": { "puzzle_1": { "name": "Hand of Rathmore", } } } ] } ================================================ FILE: data/trx/ship/games/tr3-level/gameflow.json5 ================================================ { // This file is used to enable the -l argument support. "engine": 3, "extends": "tr3", "name": "TR3 Direct Level", "main_menu_picture": "title_eu.webp", "savegame_file_fmt": "save_tr3_custom_%02d.dat", "demo_version": false, "enable_tr2_item_drops": true, "convert_dropped_guns": true, "sfx_path": "main.sfx", "injections": [ "font.bin", "lara_animations.bin", "pda_model.bin", "lara_extra.bin", "misc_sprites.bin", "lara_outfits.bin", "pickup_aid.bin", ], "levels": [ { "path": "%direct_level%", "music_track": -1, "lara_outfit": "tr3_classic", "sequence": [ {"type": "loop_game"}, {"type": "level_stats"}, ], }, ], } ================================================ FILE: data/trx/ship/games/tr3-level/strings-it.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Livello di Prova", } ] } ================================================ FILE: data/trx/ship/games/tr3-level/strings-pl.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Poziom testowy", } ] } ================================================ FILE: data/trx/ship/games/tr3-level/strings.json5 ================================================ { // For usage, refer to the documentation here: // https://lostartefacts.dev/trx/docs/stable/game_strings "levels": [ { "title": "Test Level", } ] } ================================================ FILE: data/trx/version.rc ================================================ 1 VERSIONINFO FILEVERSION 0,0,0,0 PRODUCTVERSION 0,0,0,0 BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "080904E4" BEGIN VALUE "CompanyName", "LostArtefacts" VALUE "FileDescription", "Tomb Raider X: Community Edition" VALUE "FileVersion", "TRX {version}" VALUE "InternalName", "TRX" VALUE "OriginalFilename", "TRX.exe" VALUE "ProductName", "Tomb Raider X: Community Edition" VALUE "ProductVersion", "TRX {version}" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x809, 1252 END END ================================================ FILE: docs/BUILDING.md ================================================ # Building TRX ## Build workflow Initial build: - Compile the project. - Copy all executable files from `build/` to your game directory. - Copy the contents of `data/*/ship/` to your game directory. Subsequent builds: - Compile the project. - Copy all executable files from `build/` to your game directory. We recommend making a script file to do this. ## Compiling ### Compiling on Linux Follow [BUILDING_ON_LINUX.md](BUILDING_ON_LINUX.md). ### Compiling on Windows Follow [BUILDING_ON_WINDOWS.md](BUILDING_ON_WINDOWS.md). ### Compiling on MacOS Follow [BUILDING_ON_MACOS.md](BUILDING_ON_MACOS.md). ### Supported compilers Please be advised that any build systems that are not the one we use for automating releases (= mingw-w64) come at user's own risk. They might crash or even refuse to compile. ================================================ FILE: docs/BUILDING_ON_LINUX.md ================================================ ## Building on Linux This guide describes the officially supported Linux build workflow using Docker and [just](https://github.com/casey/just). ## Installing dependencies Install the following dependencies using your distribution's package manager: - `docker` - `ffmpeg` - `glew` - `just` - `meson` - `pkgconfig` - `python3` - `sdl2` - `uthash` Depending on your system, the Docker package may be named `docker`, `docker.io`, or similar. ## Building TRX 1. Download the shipped game assets for the game you want to build: ```bash ./tools/download_assets X ``` Replace `X` with the TR version. 2. Build the Linux target: ```bash just trx-build-linux target='debug' ``` The built files will be placed in the `build/` directory. ## Other build methods The Docker workflow above is the recommended way to build TRX on Linux. If you prefer a manual, non-Docker setup, you are welcome to try it, but it is not part of the project's official build workflow. The best starting point is to inspect the files in `tools/*/docker/` for the external dependencies and `meson.build` for the local files, then tailor your system to match the release build environment as closely as possible. ## Running the game To prepare the game directory: 1. Copy the built files from `build/`. 2. Copy the contents of `data/X/ship/`. 3. Copy the original game files from your game installation. Replace `X` with the TR version you built. Once the files are in place, run: ```bash ./TRX ``` ================================================ FILE: docs/BUILDING_ON_MACOS.md ================================================ ## Building on macOS This guide describes the native macOS build workflow using Meson. ## Installing dependencies Install either Homebrew or MacPorts, then install the required dependencies. Homebrew: ```bash brew install sdl2 glew ffmpeg@6 uthash pkgconfig meson python@3.14 ``` MacPorts: ```bash sudo port install sdl2 ffmpeg uthash pkgconfig glew meson python@3.14 ``` ## Building TRX 1. Download the shipped game assets for the game you want to build: ```bash ./tools/download_assets X ``` Replace `X` with the TR version. 2. Configure the build: Intel Macs: ```bash meson setup build src --prefix=/tmp/TRX.app --bindir=Contents/MacOS --buildtype release --cross-file tools/shared/mac/x86-64_cross_file.txt ``` Apple Silicon Macs: ```bash meson setup build src --prefix=/tmp/TRX.app --bindir=Contents/MacOS --buildtype release ``` 3. Build the project: ```bash meson compile -C build ``` ## Other build methods The native Meson workflow above is the recommended way to build TRX on macOS. If you want to experiment with other approaches, such as Docker or a different native setup, you are welcome to do so, but they are not part of the project's official build workflow. ## Running the game To prepare the game directory: 1. Copy the built files from `build/`. 2. Copy the contents of `data/X/ship/`. 3. Copy the original game files from your game installation. Replace `X` with the TR version you built. Once the files are in place, run: ```bash ./TRX ``` ================================================ FILE: docs/BUILDING_ON_WINDOWS.md ================================================ ## Building on Windows This guide describes the officially supported Windows build workflow using WSL. ## Installing dependencies Install WSL and Ubuntu first. 1. Run PowerShell as Administrator. 2. Enable the Windows Subsystem for Linux feature: ```powershell Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux ``` 3. Restart the computer. 4. Open the Microsoft Store. 5. Install Ubuntu. Once WSL is installed, continue by following the Linux build guide from within your Ubuntu environment. ## Building TRX After opening Ubuntu in WSL, follow the steps in [BUILDING_ON_LINUX.md](BUILDING_ON_LINUX.md). ## Other build methods The WSL workflow above is the recommended way to build TRX on Windows. If you want to experiment with Visual Studio or other native Windows build methods, you are welcome to do so, but they are not part of the project's official build workflow. ## Running the game Once the game directory is prepared, run TRX from the copied build output and game files as described in [BUILDING_ON_LINUX.md](BUILDING_ON_LINUX.md). ================================================ FILE: docs/CHANGELOG.md ================================================ ## [Unreleased](https://github.com/LostArtefacts/TRX/compare/trx-1.5...develop) - ××××-××-×× - added the ability to do a forward roll without releasing sprint first (#5270) - added the ability for Lara to align herself with floor tilts when crawling (Gameplay Options → Controls → Crawl tilt) (#4945) - added the ability to turn off or censor blood effects (Graphic Options → Visuals → Blood effects) - added the ability to delete saves directly from the passport save and load screens (#5309) - added the ability to pause FMVs with the Pause input (#1754) - added an option to stop the game from advancing when the window loses focus (Gameplay Options → General → Pause when focus lost) (#3978) - added `O_DISPOSABLE_ANIMATING_1`...`O_DISPOSABLE_ANIMATING_10`, which will behave like regular animating objects but are removed from being drawn when deactivated - added an option to fix inaccurate wall geometry in original levels (Gameplay Options → Fixes → Fix wall geometry) - added an option to control how land creatures behave in water (Gameplay Options → General → Creature drown policy) (#5387) - changed the PS1 crystal tint option to take effect without having to reload the level - changed the `ITEM_ACTION_FLOOD` sound effect to play when underwater rather than only when above water - improved weapon setup so picking up a weapon can now give a different amount of ammo than picking up its matching ammo item (#5352) - improved `--level PATH` so it accepts relative paths and reports clearer startup errors when the level cannot be launched - improved savegame loading if item counts have changed between making the save and loading it - fixed max stats not refreshing after changing unobtainable pickups, kills, or secrets in the gameflow - fixed Lara rapidly switching animations when shimmying across the top of a ladder (#5295) - fixed Lara being able to crawl and crouch-roll too far into water from land - fixed incorrect transparent and yellow pixels on TR2 and TR3 outfit heads when bilinear filtering is enabled (#5300) - fixed rotating 3D pickup notifications following the UI filter setting instead of the in-game texture filter - fixed climbing issues on ladders that are against walls that (incorrectly) contain tilt data within them (#5304, regression from 1.1) - fixed Lara not transitioning immediately to run after vaulting two clicks when forward is held (#5305, regression from 1.3) - fixed settings list auto-scroll sometimes stopping after switching to a different mod (regression from 1.4) - fixed first-time keyboard keybindings conflicting after switching to a different mod (regression from 1.4) - fixed boulders stopping too soon on some slopes with low ceilings (#5337, regression from 1.2) - fixed persistent damage restoring Lara to full health after inter-level cutscenes (#5364, regression from 1.2) - fixed missing default object names for `O_ANIMATING_7`...`O_ANIMATING_10` and `O_FLICKERING_LIGHT` (regression from 1.4) - fixed empty centaur statues incorrectly referencing other level items when the centaur object is not loaded - fixed TR3 camera mode potentially behaving erratically when loading a level and the look input is held (regression from 1.1) **TR1**: - added savegame crystals to Unfinished Business (#1525) - fixed not being able to hear the flood/drain sound effect when using the lever in Tomb of Tihocan room 23 **TR2**: - added savegame crystals to base levels and The Golden Mask - added an option to disable body bag triggers, so that killed enemies will always be visible **TR3**: - added Boat (RIB) control - added Mine Cart control - added Willard control - added RX-Tech Worker 1 control - added RX-Tech Worker 2 control - added RX-Tech Worker 3 control - added Crawler Mutant control - added Dying Mutant control - added Hybrid Mutant control - added Wasp Mutant control - added Wasp Mutant Emitter control - added Claw Mutant control - added Fire Head control - added Disposable Animating control (Tinnos light shaft) - added an option to disable body bag triggers, so that killed enemies will always be visible - changed Sophia's final height to follow the level setup instead of using a fixed value - restored the animated mine cart tracks in RX-Tech Mines - restored the missing flamethrower blast sound effect in RX-Tech Mines and Meteorite Cavern - removed Lara's Home from TR3:LA to stay compatible with the OG and other expansion packs - fixed letterboxing of images on 16:10 resolution - fixed Fire Lighting option having no effect - fixed missing conveyor belt animations in High Security Compound - fixed delayed lighting updates on Lara during movement, particularly noticeable on ladders (regression from 1.1) - fixed z-fighting in High Security Compound rooms 135/179 - fixed transparent and magenta pixels on grating textures in High Security Compound and Area 51 - fixed an incorrect window texture in High Security Compound room 105 - fixed the satellite dish in High Security Compound room 44 being clipped out of view too soon (missing animation bounds) (#5297, regression from 1.1) - fixed It's a Madhouse! street lamps being too bright - fixed menu artefacts not appearing in consistent positions between levels - fixed TR3:LA playing TR3 intro FMV - fixed Coastal Village and Lost City of Tinnos having the wrong pickup count - fixed Willard's Lair having wrong kill count - fixed Reunion having wrong pickup and secret count - fixed being able to re-use switches that are intended to only be used once (#5328, regression from 1.1) - fixed capitalization of the "Empty Slot" text in passport - fixed the second boulder at the beginning of Reunion stopping too early (regression from 1.2) - fixed Willard increasing the kill count each time he collapses (OG bug) - fixed Wasp Emitters generating too many spawns if activated from non one-shot triggers and the player stands for too long on the trigger - fixed Lara being unable to pull up on specific ledges near walls that have invalid triangles within them (regression from 1.1) - fixed potential crashes when using grenades on enemies in levels that use the body bag feature (#5378, regression from 1.1) - fixed activated one-shot antitriggers not being remembered when loading a save (regression from 1.2) - fixed the Ora Dagger appearing too low in the inventory in Meteorite Cavern (regression from 1.2) - fixed quest item pickup counts incrementing if the item cheat is used and then save/load is repeatedly used (regression from 1.2) ## [1.5](https://github.com/LostArtefacts/TRX/compare/trx-1.4.2...trx-1.5) - 2026-04-04 Showcase: https://youtu.be/TTlajgcM9-8 - added multi-key combo shortcuts (up to 3 keys) with two binding slots per action for both keyboard and controller - added remembering of the last played mod - added a new console command, `/tp enemy`, to cycle Lara through hostile creatures in the current level - added a new animation command, `ITEM_ACTION_TURN_90`, which will rotate the affected item 90° - added dynamic mod discovery from the games/ directory using new `extends` and `name` fields in gameflow.json5 - added expanded statistics screen customization, including per-row toggles and a choice between bare and bordered layouts (Graphic Options → Stats) - added an option to show or hide the version text in the title inventory ring (#5235) - added optional save/heal crystal counts to level and final statistics (#5180) - added the ability for security lasers to activate heavy triggers when tripped by Lara (#5225) - added `trx.camera.reset()` to Lua, which will reposition the camera based on Lara's position - added an option to disable cinematics at the start of levels (Offshore Rig, Home Sweet Home and High Security Compound) (Gameplay → General → Cinematics) (#5284) - changed `--level` to no longer require `-e/--engine` to work - changed background images on title, inventory, and statistics screens to always use smooth bilinear filtering instead of the pixel-sharp look - improved rendering line segments (poison darts, rain drops, SWAT laser sights) - fixed recordings keeping unbound hotkeys active during playback - fixed being unable to change FOV after using photo mode without restarting the level (#5246, regression from TR1X 4.15) - fixed Lara getting stuck if using crouch-roll near very low ceilings (#5248) - fixed Bell in room 48 being shootable from room 55 again (#4949, regression from TRX 1.4 Sophia Reunion targeting fix) - fixed certain TR1 1.1 savegames refusing to load (#5252, regression from 1.2) - fixed the total kill count including allies if hostility policy is set to individual and Lara shoots a hostile enemy (#5255, regression from 1.2) - fixed quick-load remaining unavailable while Lara is in her death animation (#5264, regression from 1.3) - fixed destroying the Fuse Box to defeat Sophia in City and Reunion not counting as a kill in the level statistics - fixed potential freezing issues after moving an item to a different room via Lua - fixed crash when issuing `/mod tr1-ub` when playing late TR1 levels - fixed Lara being unable to draw weapons if animated interactions are enabled and she is hit by an enemy while moving towards a pickup (#5288, regression from TR1X 4.13) - fixed interaction issues with pushblocks when in shallow water (regression from 1.0) **TR2**: - changed the Detonator Box to no longer hard-code dynamic light output; refer to the migration guide for custom levels - fixed the total possible kill count in Furnace of the Gods being inaccurate if the monks are attacked (#5229) **TR3**: - added Security Laser (Alarm) control - added Security Laser (Damage) control - added Security Laser (Kill) control - added Rotating Laser control - added Sentry Gun control - added Civilian control - added Detonator Box control - added Prisoner control - added MP 1 control - added MP 2 control - added Orca control - added Area 51 Rocket control - added Hook control - added pickup aids (Graphic Options → Visuals → Pickup aids) (#5239) - added high-resolution 16:9 and 4:3 images for TR3:LA To download the new images ahead of a stable release, please see the [TRX data](https://github.com/LostArtefacts/TRX-data) repository. - restored the cutscene at the beginning of High Security Compound - changed Area 51 Rocket to no longer hardcode room 52 as the fire blast room; instead it checks for presence of an upwards portal pointing to the rocket room - changed enemies who are killed by lasers to be included in the stats - fixed Sophia's staff having a shadow in the City cutscene - fixed some doors having a bad rotation when closing, mostly visible when using the door cheat - fixed the Area 51 sliding doors being offset too far from the floor - fixed bad vertices in staircase static meshes in Aldwych and Lud's Gate, allowing for visible gaps in geometry (#5182) - fixed bad positioning of light static meshes in Aldwych that could result in Lara not grabbing certain ledges (#5181) - fixed several missing textures in Lud's Gate room 77 - fixed Tony's fireballs flying the wrong way and piling up after loading a save (regression from 1.1) - fixed rockets exploding underwater being able to create a water splash in the wrong place (regression from 1.1) ## [1.4.2](https://github.com/LostArtefacts/TRX/compare/trx-1.4.1...trx-1.4.2) - 2026-03-24 - fixed 3D pickups and inventory ring view still affected by fog (regression from 1.4) **TR2**: - fixed the Detonator Box being difficult to activate when selecting the key manually from the inventory (#5215, regression from TR2X 1.3) - fixed the Detonator Box rotating if Lara interacts with it but then doesn't pick the key from the inventory (#5215, regression from TR2X 1.3) ## [1.4.1](https://github.com/LostArtefacts/TRX/compare/trx-1.4...trx-1.4.1) - 2026-03-23 - fixed Lara using the ladder-to-crouch animation in some rare cases despite there being headroom in front of her (regression from 1.2) - fixed toggle-sprint key failing to cancel sprint mid-run (#5174, regression from 1.4) - fixed toggle-duck key failing to keep Lara ducked in run-to-duck and sprint-to-duck paths (#5177, regression from 1.4) - fixed flipped state of "Pause music in inventory", changed to "Enable music in inventory" (regression from 1.4) - fixed final statistics in City of Khamoon counting the optional PS1 mummy when Restore PS1 enemies is disabled (#5188, regression from 1.1) - fixed bouncy grenades getting stuck in certain geometry (#5202, regression from 1.2) - fixed thrown flares getting stuck in certain geometry (#5202, regression from 1.4) **TR3**: - fixed door 34 in Thames Wharf closing permanently during the flipmap puzzle (#5170, regression from 1.1) - fixed Lara being unable to move after grabbing ladders in specific geometry (#5169, regression from 1.1) - fixed a missing camera shake effect in room 135 in Aldwych (#5183) - fixed a missing sound effect during the flip map in the Egyptian room in Lud's Gate (#5183) - fixed potential framerate drops during audio playback when no active sound effects are playing (regression from 1.3) - fixed some enemies automatically being hostile when triggered if other enemies have been killed (#5203, regression from 1.1) ## [1.4](https://github.com/LostArtefacts/TRX/compare/trx-1.3.1...trx-1.4) - 2026-03-21 Showcase: https://youtu.be/8SavYv2SawI - added an option to let Lara stay crouched without holding the button (Gameplay → Controls → Toggle crouch) (#5006) - added an option to let Lara keep sprinting without holding the button (Gameplay → Controls → Toggle sprint) (#5006) - added three additional outfits for Lara - added a new console command, `/mod {name}`, to switch between installed game/mod packs without relaunching - added a new option in the New Game dialog, "Switch Game", to switch between installed game/mod packs without relaunching - added experimental support for config presets (Gameplay Options → Presets) Currently very basic presets available only – looking for help with improving them :) - added new backgrounds to Inventory Ring / Pause screen / Stats screen styles: - Transparent: like TR2 PS1 Pause Screen - Black: like the Remasters - Monochrome (cool): like TR3 PS1 Inventory Screen - Monochrome (warm): like TR3 PS1 Pause Screen - added support for TR4-style trigger-triggerers (named `O_TRIGGER_GATE` in TRX) for custom levels - added support for `.wma` music files for broader custom level compatibility - added support for `.ogv` and `.fmv` FMV extensions, with `.ogv` preferred over `.fmv` for remaster compatibility - added audio fallback for FMV files that lack an audio stream (e.g. remastered `.ogv`), probing alternative extensions for a companion audio track - added an option to move Ammo counter location (Graphic Options → UI → Ammo counter location) (#5076) - added simple level-load caching by introducing a `cache/` folder - added an option for Lara to wear semi-transparent sunglasses (Graphic Options → Lara's sunglasses) - added four additional general animating object slots, `O_ANIMATING_7` to `O_ANIMATING_10` - added `O_FLICKERING_LIGHT`, which is similar to `O_ELECTRICAL_LIGHT` but is permanently flickering - changed the reflections option to be available in all game modes (Graphic Options → Enable reflections) - changed the delay in performing a running jump by one frame less, when jump lock mode is set to disabled (Gameplay → Controls → Jump lock mode) (#3841) - improved level loading times by 15% - improved FMV audio to play through the game's existing audio mixer instead of opening a separate audio device - fixed High lighting contrast not attenuating brightness properly in TR1 and TR2 (regression from 1.1) - fixed the photo mode red frame not covering the full screen when using integer upscaling - fixed boulders that have moved vertically reactivating for a frame after loading a save (regression from 1.2) - fixed low fog distances affecting 3D pickups and inventory ring view - fixed cheat and weapon hotkeys affecting gameplay during demos (#5163, regression from 1.0) **TR1**: - added the ability to use flames on Pendulums, similar to TR3 - changed skyboxes in TR1 to be drawn only if the appropriate room flag is set - changed the scion pickup in Sanctuary of the Scion to not be displayed on-screen briefly before the level ends (#3682) - fixed Scion taking damage before activation (regression from 1.0) **TR2**: - added the ability to use flames on Pendulums, similar to TR3 - changed demos to show accurate gun meshes before Lara draws her pre-selected weapon (#3585) - fixed Bartoli appearing frozen towards the end of the Opera House cutscene - fixed pulling the Dagger of Xian from dragon's corpse not counting as a pickup (regression from 1.0) - fixed thrown flares falling through trapdoors and becoming stuck in the void if thrown underwater near the floor (#3708) - fixed flamethrowers and Dragon's breath doing weird animation when hitting floor (#5104, regression from 1.3) - fixed yetis dealing no damage during their charge attack (#5126, regression from TR2X 0.8) **TR3**: - added UPV control - added Train control - added Patrol Dog control - added Crow control - added Sophia control - added Fuse Box control - added Electric Cleaner control - added Punk control, including `O_PUNK_2`, which was unused in OG - added Security Guard control - added Propeller control - added SWAT control - added Diver control - added Pendulum control - added 60 FPS interpolation to: - sparks - weather effects - water effects - wake effects - explosion rings - bat emitters - changed Ammo Counter to appear in red when the Menu Style is set to PS1 - changed Punks to have friendliness assignable through Lua, so removing the hard-coded behaviour in the Lud's Gate level sequence - changed Trains to no longer hard-code speed based on the level number and instead take it from their default animation - changed Pendulums that have flames to be setup by placing a flame emitter at the same position, rather than setting the item's timer via its trigger - changed Meteorite Artefacts to be exempted from drop tile centering - fixed a soft lock preventing Lara from picking up the artefact, when saving/loading during boss explosion sequence (regression from 1.2) - fixed the helicopter in Highland Fling briefly disappearing when crossing room portals (regression from 1.1) - fixed Lara dying from touching Trains that haven't yet been activated - fixed harpoons from Divers not spawning blood when they hit Lara - fixed `O_KILL_ALL_TRIGGERED` removing unused Save Crystals (#5035) - fixed TR1/TR2-only options showing up in TR3 gameplay settings (#5055) - fixed Lara stopping against one-click raised slopes when running instead of beginning to slide (#5038) - fixed rain not spawning in outside rooms in the Thames Wharf cutscene - added PSX-style underwater water particles to Madubu Gorge, Aldwych, and Lud's Gate - fixed the punk in the cutscene before Lud's Gate walking through a wall - fixed Lara appearing frozen at the beginning of the cutscene before City - fixed incorrect texturing on the fish in City - fixed the Eye of Isis not showing in the inventory in All Hallows - fixed too low volume in all FMVs (except logo which used a different codec) - fixed Lara by default being unable to climb out of water onto steep slopes (change manually in Gameplay → Fixes → Fix water exit) - fixed thrown flares falling through trapdoors (regression from 1.1) - fixed some level textures appearing slightly misaligned on room geometry - fixed potential AI behavioural differences in the South Pacific Mercenary (regression from 1.2) - fixed smoke from Lara's guns persisting between levels (regression from 1.1) - fixed sound effects potentially playing after completing a level and entering into the globe select screen (regression from 1.2) - fixed audio lag and framerate drops near the end of an audio track that's playing when no active sound effects are playing (#5167, regression from 1.3) ## [1.3.1](https://github.com/LostArtefacts/TRX/compare/trx-1.3...trx-1.3.1) - 2026-03-11 - fixed main.sfx resolution being enforced (regression from 1.3) - fixed the microphone entering underwater mode too eagerly when `Microphone near Lara` is enabled (#5057, #4888) - fixed save counters sometimes drifting after dying and reloading (#5054, regression from TR1X 4.9 / TRX 1.0) - fixed flare and gun flash being drawn with a water tint when in shallow water regardless of responsive tint option (#5072, regression from 1.2) - fixed fade transitions using the wrong picture size when upscaling or borders are enabled (#5081, regression) **TR2**: - fixed guns as secret rewards not being converted to the equivalent ammo if Lara already has the gun **TR3**: - fixed reverb affecting inventory ring sounds (#5056) - fixed Pause text color - fixed the secret sound not playing in some installations, whereby `cdaudio.wad` contains invalid track sizes (#5049) - fixed mounting a UPV causing Lara's braid to stand upright (OG) ## [1.3](https://github.com/LostArtefacts/TRX/compare/trx-1.2.2...trx-1.3) - 2026-03-06 Showcase: https://youtu.be/FgB9JgDM65E - added the ability to freely rotate examinable items - added a color editor dialog for fog and water colors in Graphic Options → Visuals - added an option for Lara to wear sunglasses (Graphic Options → Visuals → Sunglasses) (#4869) - added `O_SWITCH_TYPE_WHEEL`, which is similar to `O_SWITCH_TYPE_AIRLOCK` but can be used more than once - added `O_SMASH_OBJECT_3`, which can only be broken with triggers or the Crash Site gun - added `O_SMASH_OBJECT_4`, which behaves like `O_SMASH_OBJECT_1` but uses `SFX_SHUTTERS_BREAK` - added `O_TREX_ALPHA`, which can target raptors and be distracted by flares - added the ability to trigger dragons independently of Bartoli in custom levels (#5011) - place an `O_DRAGON_BACK` item in the editor and trigger it normally - the dragon will spawn immediately when triggered and will be one-phase, so no dagger needs to be pulled - added a new Lua item query helpers, `trx.items.find(query)` and `trx.items.first(query)`, with support for `object_id` and `room_num` filters - added a new Lua catalog, `trx.catalog.weapons`, for weapon identifiers - added a new Lua property, `trx.lara.equipped_gun`, to read Lara's currently equipped gun type - added a new Lua property, `trx.lara.target`, to read Lara's current locked target item - added a new Lua property, `trx.Item.flags`, to read current item flags (related to triggers) - added a new Lua property, `trx.Item.timer`, to read current item timer value (related to triggers) - added support for using more sound slots than originally possible in custom levels (#3898) - added support for actual quick saves with round-robin quicksave slot cycling. (#1897) Note: This feature is disabled by default and needs the player to manually bind new inputs. - added quick-save/load command aliases: - `/save quick`, `/quicksave`, `/qs` - `/load quick [slot]`, `/load q[slot]`, `/quickload [slot]`, `/ql [slot]` - added blood effects when enemies shoot any other creature (not just Lara) - added support for the globe-style level selection mechanic in the new game for level builders (#4920) - added an option to control how Lara swings on thin ledges (Gameplay → Controls → Slow ledge swing) (#3341) - added `/tp precise {x} {y} {z}` to teleport using raw world-space coordinates (no `/1024` scaling – matches TRView) - added the ability to use glide cameras when using TR3 camera mode - added an option to toggle glide cameras (Graphic Options → Visuals → Glide cameras) - changed PC and PS1 UI colors to no longer be hardcoded by moving it to `ui.json5` (#5003) - changed Fog start and Fog end to change by 10 by default, with Slow allowing 1-step precision (#5015) - changed `O_WINDOW_1` and `O_WINDOW_2` to `O_SMASH_OBJECT_1` and `O_SMASH_OBJECT_2` respectively - changed `O_MINI_COPTER` to no longer hardcode direction - changed Earthquake to support being reset - changed loading screens setting to use modes (`disabled`, `always`, `new-games`). Previously, they were hardcoded to not show for saves (#1290) - changed logs to no longer emit ANSI color characters when the game's output is piped to a file / process - changed the degenerate static mesh collision check to only apply when all axes have an empty size - improved error reporting for gameflow issues to now display full key paths for faulty nodes - fixed Lara teleporting after vaulting 2 or 3 clicks when there is a room below the target position that has no immediately adjoining portal (#4530) - fixed Lara attempting to jump up (using action) despite the ceiling above her making it impossible to grab any ledge (#3558) - fixed Lara not being able to grab ledges when under low ceilings (#4093) - fixed Lara sometimes falling when vaulting 2 or 3 clicks onto a ledge that has triangulation - fixed NG+ always forcing Lara's default equipped gun at level start even when "remember guns between levels" is enabled (#4711) - fixed not restoring Lara's back weapon mesh between levels when "remember guns" is enabled and a rifle-type weapon is equipped at level end - fixed a missing footstep sound when Lara starts to sprint - fixed Lara's flare undraw animation being skippable on specific late draw frames (#1593) - fixed UI bar scale option not updating the padding and borders (regression from 1.2) - fixed Blade stopping in the wrong position when anti-triggered (#4894) - fixed very distant Boulders causing camera shake (similar to the Tihocan crocodile targeting bug) - fixed drawing debug triggers using wrong orientation in some triangular geometry - fixed heavy triggers with no `TO_TARGET` / `TO_CAMERA` resetting cameras - fixed Lua `trx.catalog` only exposing `objects` and `flip_effects`; it now also exposes `lara_states`, `lara_anims`, `music`, and `samples` - fixed a freeze if firing a grenade very close to room portals (#4938, regression from 1.2) - fixed non-deterministic Inventory Ring control (transition speeds depended on v-sync / wall clock timing) - fixed game logic speeding up while the game was fading out after quitting - fixed Lara being able to shoot smashable objects located in unreachable overlapping rooms (#4949, regression from TR1X 4.14 / TR2X 1.4) - fixed touching Lava Wedges causing endless Flame effect spawns when the immunity cheat is on - fixed touching Lava tiles causing reduced Flame effect when the immunity cheat is on - fixed collision issues on bridges, trapdoors, breakable tiles and pushblocks if positioned over a triangle portal (regression from 1.0) - fixed Lara being able to sprint through swamps when responsive sprinting is enabled - fixed bar borders scaling poorly (off-by-1px errors, regression from 1.2) - fixed death counter not being preserved in saves after changing levels, causing stopwatch and final statistics to sometimes show 0 deaths **TR1**: - added an option to allow Lara to crouch and crawl (Gameplay → Controls → Crawling) - added support for monkey bars - changed Lara to be able to grab ealier when performing forward jumps, like TR3 - fixed a very rare case of raptors using an incorrect death animation - fixed Lara unable to run around in random spots at the bottom of The Great Pyramid's starting pit **TR2**: - added an option to allow Lara to crouch and crawl (Gameplay → Controls → Crawling) - added support for monkey bars - changed Lara to be able to grab ealier when performing forward jumps, like TR3 - fixed secret reward in Venice giving Magnums ammo instead of Automatic Pistol Clips (#4951, regression from 1.1) - fixed flickering switches and spike ceilings in Temple of Xian and Floating Islands (#4874) - fixed Airlock door handles not getting drawn from certain angles (#4886, regression from 1.0) - fixed loading screens showing before playing FMVs on most levels - fixed Lara not being able to move after exiting water, having used an underwater lever with the animated interactions setting enabled (#4912, regression from 1.0) - fixed Bell in room 48 being shootable from room 55 (#4949, regression from TR2X 1.4) - fixed "Disable T-Rex Collision" option missing from The Golden Mask (there are T-Rex enemies in Nightmare in Vegas) - removed the requirement to use `main.sfx` in custom levels (#3898) **TR3**: - added reverb support - added Kayak control - added Compsognathus control - added Mounted Gun control - added Tribe Axeman control - added Tribe Pipeman control - added Tribe Boss control - added Lizard control - added Crocodile control - added Carcass control (hanging Raptor) - added T-Rex control - added Raptor control - added Raptor Emitter control - added Bat Emitter control with save/load support - added South Pacific Mercenary control - added Smashable Wall control - added Smashable Shutters control - added a slide-to-sprint animation state change for Lara, similar to TR1 and TR2 - added a new gameplay option to toggle Lara's crouch roll (Gameplay → Controls → Crouch roll) - added an option to allow Lara to jump out of crawlspaces (Gameplay → Controls → Crawl exit jump) - added crouching/crawling enhancements (Gameplay → Controls → Responsive crawling) - added the ability to resume crawling more quickly after coming to a stop - added transitions from run/sprint to crawl without first coming to a stop - added a transition from crawl to crouch-roll without having to manually crouch first - added the ability to turn while in the crouch idle state - restored an unused pickup animation when in the crawling state, bypassing the crouch transition - added a transition from ladder to crawlspaces instead of first having to drop and re-grab the ladder (#4954) - fixed Uzis having wrong clips capacity (was 80, is now 40 – sorry!) - fixed Lara briefly switching from run back to wade when crossing from 2-click to 1-click water depth - fixed Lara unable to climb small ledges with low crawlspaces - fixed Lara using the thin-ledge swing hang animation instead of the normal hang in some 1-click ledge cases - fixed Lara being unable to transition from slow swing at the base of a ladder to being able to climb the ladder - fixed Lara's cutscene gun shots not rendering muzzle flashes, gun smoke and shell ejections (e.g., Tony cutscene) - fixed water ripples triggering z-fighting with 0-click ground surfaces - fixed footprints rendering with an excessive Y offset - fixed wheel switches only being usable once - fixed wheel switch triggers activating too early - fixed Kayak voiding and teleporting on large slopes - fixed Kayak wake effects sometimes clipping through complex geometry - fixed loading screens showing before playing FMVs in Antarctica - fixed end credits referencing non-existing image file - fixed Puna to no longer hardcode Lizard locations, and instead use relative offsets - fixed Puna's summoned Lizards counting towards total level kill count - fixed Tony briefly appearing for a single frame when loading a save after his death - fixed Lara sometimes getting stuck when crawling backwards off a tilted ledge (#4956) - fixed the Tribe Pipeman sometimes not being able to aim darts at Lara correctly (Gameplay → Fixes → Fix Pipeman aim) - fixed Lara's footprints sometimes spawning when standing on a bridge, trapdoor or pushblock - fixed Lara being unable to walk or sidestep at times when standing on a bridge that sits over a steep slope (regression from 1.1) - fixed Lara's left arm elevating when holding a flare and performing a crouch pickup (regression from 1.1) - removed the limitation of one Carcass instance per level working with Piranhas - removed the limitation of Piranhas only attacking Carcass instances if the level sequence matches Crash Site's - restored the ability for Lara to perform grab cancels, like TR1 and TR2 - restored glide camera functionality ## [1.2.2](https://github.com/LostArtefacts/TRX/compare/trx-1.2.1...trx-1.2.2) - 2026-02-13 - fixed a potential `GL_OUT_OF_MEMORY` error that could occur after reloading levels many times (regression from <1.0) ## [1.2.1](https://github.com/LostArtefacts/TRX/compare/trx-1.2...trx-1.2.1) - 2026-02-11 - fixed title ring music inheriting the wrong audio volume (regression from 1.2) - fixed settings dialog changing size when cycling through non-scrollable tabs (regression from 1.2) - fixed Play Previous Level feature not restoring Lara's equipment correctly for pre-1.2 saves (regression from 1.2) - fixed Play Previous Level feature causing Lara to instantly die for pre-1.2 saves, when the Persistent Damage option is on (regression from 1.2) Note: for those 1.0/1.1 saves, this feature will restore her health to full, as it was not stored correctly. 1.2 will continue to restore the correct HP value. - fixed TR2 delayed music triggers not working (regression from 1.1) - fixed TR3 using delayed music triggers (TR2-only feature) ## [1.2](https://github.com/LostArtefacts/TRX/compare/trx-1.1...trx-1.2) - 2026-02-11 Showcase: https://www.youtube.com/watch?v=jeq8rQONaic - added globe level selection mechanic - added Bubble Emitter control (#4629) - added dynamic light objects: - added Red Light control - added Green Light control - added Blue Light control - added Amber Light control - added White Light control - added Strobe Light control - added Pulse Light control - added Beacon Light control - added On/Off Light control - added the ability in Lua to hook into control loop events during cutscenes - added an option to change Lara's outfit, with 20 variants included by default; custom levels can provide up to 32 outfits (Visuals → General → Lara's outfit) (#4383) - added an option to control UI brightness (Graphic Options → Rendering → UI brightness); renamed "Brightness" to "Game brightness" - added an option to allow Lara's underwater mesh tint to be more responsive based on position, as per TR3 (Visuals → General → Responsive mesh tint) - added the ability for custom levels to define Lara's braid position relative to her head (#110) - added the ability to disable manual camera (Gameplay → Controls → Manual camera) - added the ability to enable bouncy grenades (Gameplay → General → Enable bouncy grenades) - added the ability to toggle TR1/2 and TR3 projectile area damage – TR3 often deals double damage (Gameplay → Mods → Projectile Area Damage) - added the ability to hide pickup notifications in the bottom-right corner (Graphic Options → UI → Pickups overlay) - added a new Lua event, `trx.events.on_game_start`, which fires when the level finishes loading and the game is about to start - added a new Lua function, `trx.rooms.flip()`, to toggle the flip map (#4704) - added a new Lua function, `trx.rooms.flip_effect()`, to set the active flip effect with an optional timer (#4704) - added a new Lua catalog, `trx.catalog.flip_effects` for name-based flip effect catalog IDs - added a new Lua music play mode, `trx.music.PlayMode.OVERLAY` for playing on top of currently played track - added new Lua catalogs for Lara states, Lara anims, music, and samples - added a new Lua module, `trx.camera`, with camera getters and `trx.camera.shake()` - added a new Lua property, `trx.rooms.Room.num` - added support for cross-fades to the title screen - added visual previews of bar colors (Graphic Options → Bars) - added the ability to change PS1 bar colors - added shadow rendering to all cutscene actors - added endless sprint (available previously via the `/restless` command) to the UI settings (Gameplay options → Mods → Endless sprint) - added endless flare time cheat (Gameplay options → Mods → Endless flare time) - added `O_VULTURE` for custom levels - added `O_ROLLING_BALL_4` (giant Temple of Puna boulder) for custom levels - added an option to control whether or not moving boulders should shake the camera (Gameplay options → General → Enable boulder shake) - added an option to make Lara stumble if she hops backwards and there is a slope behind her (Gameplay options → Controls → Backwards slope stumble) - added `/trigger` and `/untrigger` console commands, with support for targeting by item ID, item name, or object name - added the ability to seek backwards through cutscenes with left button - added the ability to trigger collapsible tiles from heavy triggers, regardless of Lara's position (#4807) - added floor height change detection for boulders when stopped, so they will drop if the floor below them drops (#4808) - added splash effects to neutral twists and rolls (#4793) - improved rendering performance - improved the ability to seek through cutscenes to support even faster seeks (Slow = ±1 s, default = ±5 s, new: Draw = ±15 s) - improved `/tp` to accept `room`/`item` prefixes and `rN`/`iN` shortcuts - improved inventory ring active item highlight for smoother appearance - improved savegame file size by reducing it about 20–30%. - improved indentation for nested bullets in the UIs - changed `debug.debug_cuboids` option name from "debug cuboids" to "debug bounding boxes" (`/debug bounding-boxes` or `/set debug-bounding-boxes 1`) - changed `debug.enable_debug_pos` option to split into `enable_debug_pos` and `enable_debug_anim` - changed `debug.enable_invulnerability` option to only show the marker if the setting `enable_debug_status` is on (off by default) (#4631) - changed `audio.load_music_triggers` (Gameplay → Fixes → Fix one-shot music triggers) to be enabled by default - changed photo mode to no longer show "Entering photo mode" in the console - changed photo mode to always display a red frame around the game view when active (not visible in screenshots) - changed stats dialog to include allies in kill count if they turn hostile. This applies to all levels that follow, and the final stats screen. - changed rooms-to-draw tracking to no longer stop at the 100-room limit - changed boulders to stop if the ceiling height is lower than their height - changed all UI bar colors from hardcoded to configurable via `cfg/ui.json5`, enabling some customization for PS1 bars - changed `/debug [0|1]` command to no longer spam about settings that aren't changed - changed `/set` command to always use hyphens for enum option values, and accept both underscores and hyphens - changed Lua catalog keys to strip `O_` prefixes and use lowercase - changed Lua event callback names to be more consistent: - `on_level_init` → `before_level_file` - `on_level_start` → `after_level_file` - `on_level_load` → `after_level_state` - `on_control` → `before_control` - `on_control_post` → `after_control` - changed turbo cheat to auto‑reset to normal speed if pushed past limit, making it easier for new players to recover from accidental changes - changed Blades to support being reset - changed the barefoot SFX option toggle in TR2 to no longer require reloading the level for changes to take effect - changed triggers that target pickup items to support antitriggers, switches and bitmasks - removed support for legacy (TombATI / TR2 GOG/Steam) and pre-1.0 (TR1X/TR2X) savegame files - fixed random face dropouts on levels with more than 32k textures - fixed a small hiccup when launching the game on certain GPUs - fixed inconsistent music volume in the statistics screens (#4499) - fixed shadows to support 60 FPS interpolation - fixed soft static mesh collision not working right with statics that appear in overlapping rooms - fixed drawing debug triggers using random tint near water sources - fixed drawing debug triggers glitching through triangular portals - fixed Lara being force-resurfaced near split-triangle water portals in certain spots - fixed custom levels that contain invalid room static mesh references not being able to load (#4770) - fixed the tip of Lara's braid using an invalid offset position on the first frame of a level (#4821) - fixed drawing shadows twice when item intersects a portal (#4640, regression from 1.0) - fixed drawing circle/octagon shadows in TR2/TR3 cutscenes using wrong positions - fixed being unable to use the manual camera in TR3 camera mode when Lara is idle (#4670, regression from 1.1) - fixed grenades not killing more than a single enemy - fixed running `/title` and similar commands leaving the "Examine" button briefly visible in the key items ring (old regression) - fixed running `/title` and similar commands when examining an item causing incorrect item rotation next time the ring opens (old regression) - fixed endless sprint cheat setting not retained between game relaunches - fixed Cobras not being counted in level kill count - fixed stats dialog retaining friendly status for allies that become enemy types in later levels, causing them to get excluded from kill count - fixed targeting hostile ex-allies not working if "Enable ally targeting" option is off - fixed `/play` and similar commands fading out instead of running instantly on stats/title screens - fixed `/play` and similar commands sometimes preserving cutscene camera tilt if invoked while a cutscene was paused - fixed Cheats description showing arrows in the indented bullets (#4753, regression from TRX 1.1) - fixed game freezing on exit on certain platforms when there are no active sound devices (SDL bug) - fixed Lara twitching when trying to step back onto death tiles - fixed Lara's look head rotation/tilt limits being hardcoded to the engine version rather than camera mode - fixed Lara rotating around an incorrect origin in photo mode during cutscenes - fixed pushblocks being able to fall into rooms below despite no portals being present (#4788, regression from TR1X 4.15/TR2X 1.5) - fixed one-shot triggers for hidden pickup items making the items permanently invisible (#4784) - fixed secret tracks played at low quality when "fix secrets killing music" option is on - fixed secret tracks not restored from the savegame when "fix secrets killing music" option is on - fixed slow-forward seeking through cutscenes (right+slow) not working (regression from 1.0) - fixed statics marked collidable but with zero‑size hitboxes causing phantom collisions - fixed Lara being displaced during the sprint-slide animation if she tried to pick up an item at the same time (#4843, regression from TR1X 4.14, TR2X 1.4) **TR1**: - added Unfinished Business loading screens (#1310, thanks to rockahub) - fixed save crystal reflections rendering upside down (regression from 4.14) - fixed underwater wobble effect acting twitchy with camera movement - fixed several texture issues on each of Lara's outfits and guns - fixed gun injections overwriting Lara's footstep SFX in all levels (#4733, regression from 1.1) - fixed pushblocks in Natla's Mines becoming unusable after loading a save made in earlier versions (#4735, regression from 1.1) - fixed low-quality texture palette on injected TR2/3 weapons and flares - fixed baddie speeches played at low quality when "fix speeches killing music" option is on - fixed baddie speeches not restored from the savegame when "fix speeches killing music" option is on **TR2**: - added "Sound Options → Misc → Layered secret music" option - added "Gameplay → Fixes → Fix one-shot music triggers" option - changed Assault Course stats to show scroll indicators (#3510) - changed statistics screen rows to be more compact - fixed wrong line played when finishing the Assault Course for the first time (#4667, regression from 1.1) - fixed underwater wobble effect acting twitchy with camera movement - fixed several texture issues on each of Lara's outfits and guns - fixed a deviation in water current behaviour that could result in Lara stopping too early (#4706, regression from TR2X 1.1) - fixed gun injections overwriting Lara's footstep SFX in underwater levels (#4733, regression from 1.1) - fixed exploding Armed Snowmobile not disappearing the vehicle (#4762) - fixed the polar bear in Furnace of the Gods twitching if killed when in its reared state (#4624) - fixed incorrect textures on the MP5 when equipped or on Lara's back **TR3**: - added "Sound Options → Misc → Layered secret music" option - added "Gameplay → Fixes → Fix one-shot music triggers" option - added new UI bar appearances, "TR3 PC" and "TR3 PS1" (Graphic Options → Bars → Bars appearance) - added new water currents - added new blood effects - added underwater blood spills - added poison mechanic - added heal crystals - added animated puzzle holes support - added new creature explosions effects - added meteorite artifacts support - added examine item feature for certain items - added Monkey control - added Shiva control - added Tony control - added Spikes animation in Coastal Village and Madubu Gorge - added Electric Fence control - added Aldwych Drill control (Spike Ceiling with timer=1 to descend faster) - added TR3 behavior patterns to Tiger control - added Kill All Triggered control - added Vulture control - added Boulder control - added Poison Dart control - added Earthquake control - added dynamic light objects: - added Red Light control - added Green Light control - added Blue Light control - added Amber Light control - added White Light control - added Strobe Light control - added Pulse Light control - added Beacon Light control - added On/Off Light control - added Lara's backwards-hop stumble if there is a slope behind her - added "Sound Options → Misc → Layered secret music" option - improved look camera stability to reduce idle-breathing camera bobbing/roll - improved Monkeys to no longer hardcode hostility status based on Tiger presence - changed Assault Course stats to show scroll indicators (#3510) - changed statistics screen rows to be more compact - changed hostile Monkeys to share hostility status, like TR2 Barkhang monks (the original TR3 behavior can be restored in Gameplay → General → Ally hostility policy) - changed enemy drops to appear at the tile center, to conform with the OG - fixed several texture issues on each of Lara's outfits and guns - fixed actors jumping to their start frame at the end of cutscenes - fixed Flame in Cutscene 4 and 6 appearing static - fixed Swamp Map rotation - fixed seaweed disappearing too quickly in certain levels - fixed Hand of Rathmore not rotating in Sleeping with the Fishes - fixed Icicles not having sound - fixed Spike Walls not having sound - fixed colored exhaust smokes on Quad Bike for 1 frame - fixed Cobras and Rattlesnakes being immune to explosives in their sleeping state - fixed Quad Bikes not restoring their state from savegames properly - fixed exploding Assault Targets in Lara's Home counting as penalties - fixed surface and underwater effects simulation speed - fixed underwater wobble effect amplitude - fixed animated textures speed - fixed inconsistent Meteor Artifacts names - fixed wrong item selection sound in the inventory ring - fixed flame emitters not getting restored when loading from a save - fixed Lara holding onto ledges after dying if the Action key wasn't released - fixed Shiva death smoke effects getting misplaced if the player saves and reloads mid-battle - fixed Grenade, Rocket Launcher, and Harpoons damage - fixed being unable to antitrigger Poison Dart Emitters - fixed ally Lua API not working with most of the TR3 enemies supported so far - fixed one-shot antitriggers / antipads behavior - fixed Blades in Coastal Village not respecting antitrigger - fixed some Poison Darts disappearing 1 frame early - fixed running down an enemy with a Quad not counting as a kill - fixed killing Cobras with a manually-aimed projectile not counting as a kill - fixed smoke and spark rotation snapping at 180° instead of rotating smoothly - fixed Lara burning instead of getting electrocuted when touching the top of the electric fence - fixed driving over Winston with a Quad Bike or shooting him with the Harpoon Gun causing him to bleed - fixed driving over Assault Target with a Quad Bike or shooting it with the Harpoon Gun causing it to spawn blood - fixed skybox data in Scotland TR3:LA levels to show correct top and bottom colors ## [1.1](https://github.com/LostArtefacts/TRX/compare/trx-1.0.3...trx-1.1) - 2026-01-17 Showcase: https://www.youtube.com/watch?v=veVYyr--H1A - added a fade-in and fade-out effect to patterned inventory backgrounds - added the ability to use monochrome image for inventory and statistic screens backgrounds - added the ability to use very dark image for inventory and statistic screens backgrounds (#4469) - added the ability to change pause screen background - added the ability to control whether or not allies are hostile towards Lara via Lua (#3873) - added the ability to control via Lua which enemies are allies and which are ones that will fight with allies (#3873) - added the ability to control Lara's air timer via Lua (#4592) - added the ability to fine-tune the fade effects between the inventory ring, the pause screen, and the stats screen (Graphic Options → UI → Inventory/Pause/Stats fade effects) - added gamma control (TR3-style) to all games (Graphic Options → Rendering → Gamma) - added support for TR3 weather effects to all games (#3881) - added support for 3D secret objects, and provided defaults for OG levels in TR2 (#4380) - added catalog object IDs to Lua - added the ability to swap meshes in Lua - added support for locked cameras, similar to TR4+ (#2040) - added support to use `O_DINO_WARRIOR` and `O_FISH` as aliases for `O_TREX` and `O_BARRACUDA` respectively - added the ability to define gun types, flash shade and offset positions in `cfg/weapons.json5` - added the ability to define ammo pickup quantities per weapon in `cfg/weapons.json5` (#4518) - added a new input, that lets the player toggle in-game textures on/off, available by default under F8 - added a new console command, `/textures`, that lets the player toggle in-game textures on/off - added a new console command, `/weather`, that lets the player control the weather - added a new console command, `/spawn`, that lets the builder spawn an entity of their choice to test things around - added Animating Item 1-6 control - added the option to use TR3 sprite-based shadows (Visuals → Shadows shape) - added an option for soft static mesh collision; this also allows for arbitrary mesh rotation in custom levels and retaining accurate collision (Gameplay → Controls → Soft mesh collision) (#3654) - added an option to use the TR3 camera (Visuals → Camera Mode) - improved a fade-in and fade-out effect on loading screens – they now smoothly transition to the game screen - improved fog behavior to be less dependent on camera rotation - changed the 3D pickups option to try the simplified 3D meshes first, if available, before falling back to inventory items - changed the 2D and 3D statics limit from 256 to unlimited - changed the lighting contrast key binding to F9 - changed underwater statics to be affected by caustics, even if they don't get merged into level geometry (#4430) - changed Magnums and Automatic Pistols to be separate objects, so both can appear in the same level (#4475) - changed the M16 and MP5 to be separate objects, so both can appear in the same level - changed the swinging axe to be defined separately from other pendulums (use object `O_SWINGING_AXE` in catalogs) - changed the following trap types to support being reset (#3993) - collapsible tiles - Damocles swords - ember emitters - falling ceiling - hooks - icicles - lava wedge - pendulums - pushblocks (via timed triggers only) - spike ceilings - changed the fonts to no longer use hardcoded character widths - changed the fonts to use dedicated sprites for accented characters instead of composing them at runtime - changed the fonts to use dedicated sprites for similar-looking characters instead of using aliases - changed the reset keybindings bars appearance to be more visible - changed the default exposure bar PC color to blue 2 - changed lua music `PlayMode` constant names - removed the `scripting/trx` directory – internal TRX LUA scripts now get embedded in the exe - fixed broken final statistic counters (#4432, regression from 1.0) - fixed undefined behavior (crashes and/or texture glitches) in levels with a lot of textures - fixed a crash if a pickup aid spawns against an item whose 3D model isn't present - fixed Bacon Lara not always being drawn perfectly in sync with Lara's animation (#4210) - fixed gondolas not being drawn with an underwater tint when they have sunk (#4428) - fixed the teleport-to-item command not succeeding if used in succession with the same type and an out of bounds item is encountered (#4468) - fixed skybox faces with transparent pixels always rendering in front of all other faces (#4351, regression from 1.0) - fixed unbound inputs not being saved between game launches (#4360, regression from TR1X 4.14/TR2X 1.4) - fixed Lara drawing a flare when the draw weapons input is pressed, and she already has an active flare but no weapons (#4361, regression from TR2X 1.4) - fixed wading splashes spawning when using the fly cheat (#4400, regression from 1.0) - fixed grenades not exploding floating water creatures (#4399, regression from TR2X 1.3) - fixed water enemies not getting tinted when dead and floating (#4407, regression from 1.0) - fixed Lara not colliding with mines/gondolas when underwater (#4424, regression from TR2X 1.3) - fixed flare box pickups containing only one flare if Lara has none in her inventory at that time (#4423, regression from 1.0) - fixed water enemies appearing untinted for a frame after dying and moving to the water surface (#4420, regression from TR2X 0.1) - fixed the interactive fly cheat breaking with animated interactions enabled (#4444, regression from TR1X 4.14) - fixed switch triggers using an incorrect state check, which could result in fixed camera behavior that deviated from OG (#4456, regression from 1.0) - fixed ambient music triggers to no longer kill active normal music tracks (#4463) - fixed game crashing when Lara passes through light sources in certain levels - fixed waterfall mist not brightening when holding a flare (#4486) - fixed resetting camera in the photo mode not clearing the underwater tint - fixed developer console text editing (backspace, moving the caret) doing weird things with Unicode characters - fixed Lara jumping if player holds the swim button when exiting the fly cheat (#4470) - fixed game refusing to load savegames made with the JP mode (#4558) **TR1**: - added the ability to change inventory and statistics background styles (pattern + wave are not implemented in TR1) - added Automatic Pistols, the Desert Eagle, the MP5, and the Rocket Launcher to the `/moreguns` console command - fixed Lara standing two clicks below `O_FALLING_BLOCK_3` items rather than directly on top (#4374) - fixed missing menu guns SFX in Lara's Home - fixed several OG texture issues in Caves (rooms 0, 1, 2, 6, 24, 30 and 32) - fixed Lara automatically being given TR2 weapons in NG+ when playing the OG levels (#4365, regression from 1.0) - fixed Lara's pistol holster meshes appearing in NG+ in place of her Uzi holster meshes (#4368, regression from 1.0) - fixed Lara's footstep sounds being very quiet when weapons are equipped (#4451, regression from 1.0) - fixed the grenade blast SFX not always playing in succession (#4628, regression from 1.0) **TR2**: - added unused gym voice line at level start if Lara has any logged assault course attempts (#2822) - added high-resolution 16:9 and 4:3 loading screens - added high-resolution 16:9 and 4:3 game end screen To download the new images ahead of a stable release, please see the [TRX data](https://github.com/LostArtefacts/TRX-data) repository. - added Magnums, the Desert Eagle, the MP5, and the Rocket Launcher to the `/moreguns` console command - changed Tibetan Foothills to have snow (you can disable this via Graphic Options → Visuals → Weather) - changed ember emitters to use the `SFX_LAVA_FOUNTAIN` sample (#4376) - fixed the scuba diver's death SFX not playing (#4386) - fixed a missing trigger for tiger 6 in Ice Palace (#4390) - fixed missing music triggers in Venice room 11 and Floating Islands room 80 - fixed a missing death tile in Floating Islands room 91 - fixed vertex lighting and stretched textures in Lara's Home room 28 and Home Sweet Home room 27 - fixed z-fighting on fences in Barkhang Monastery and gondola poles in Venice - fixed missing oxygen tanks in Offshore Rig room 82 - fixed the monk in the Diving Area cutscene not having a complete death animation - fixed demos not using loading screens - fixed reading room lights for custom TR2 levels (regression from 1.0) - fixed the switch in room 46 of Opera House randomly disappearing - fixed game crashing when Lara passes through light sources in levels compiled with dxtre3D - fixed Snowmobile music not getting resumed (#4519) - fixed Stopwatch position in the inventory ring (#2014) - fixed static lighting on broken ice/windows (#4506, regression from 1.0) **TR3**: A lot of our TR3 work builds on *TOMB3*, which Troye and ChocolateFan kindly let us dive into and expand on. Their hard work gave us the perfect base to push TRX further, and made the climb a lot less vertical! - added support for monkey bar mechanics - added support for crawlspace mechanics - added RGB lighting system support - added flame effects - added swamp and water surfaces wave effect - added underwater caustics - added proper bubbles - added water splash and ripple effects - added waterfall mist effect - added per-mesh underwater tinting (Lara only) - added `cdaudio.wad` music playback support - added weather effects - added sprite-based shadows - added footprints - added surface-based step sounds - added cold breath effects - added gun shells - added gun projectiles - added gun smoke effects - added new ricochets - added flare lighting and sparks - added monochrome inventory backgrounds - added TR3 inventory ring lighting - added high-resolution 16:9 and 4:3 loading screens - added high-resolution 16:9 and 4:3 title and game end screens - added high-resolution 16:9 and 4:3 credit images To download the new images ahead of a stable release, please see the [TRX data](https://github.com/LostArtefacts/TRX-data) repository. - added support for the serif font - added support for colored text - added Assault Course and Race Track course mechanics - added Quad Bike control - added Animating Item 1-6 control - added Electrical Light control - added Smoke Emitters control - added Steam Emitter control - added Flame Emitter 1-3 and Side Flame Emitter control - added Piranhas and Tropical Fish control - added Desert Eagle control - added MP5 control - added Rocket Launcher control - added Magnums, the Automatic Pistols, and the M16 to the `/moreguns` console command - added all weapons to Lara's Home (accessible with cheats or via the console only) - added Assault Course target control - added Assault Course penalty system - added an option to fix the MP5 accuracy while running - added TR3 camera control and look functionality - improved run-to-crawl transition - improved text colors of the Assault Course statistics and timers - improved Assault Course targets to spawn ricochets - changed The River Ganges, City and All Hallows to have rain - fixed sample reading to support correct pitch and volume - fixed pool edges shifting along with the water effect - fixed Lara's thigh being drawn when a flare is in Lara's hand or has been discarded - fixed gun flashes being drawn in white - fixed disabling lighting system not working - fixed skybox data to show correct top and bottom colors - fixed Assault Course timer remaining indefinitely on screen - fixed Quad Bike low visibility of exhaust smokes at high speeds - fixed Quad Bike wheels appearing to spin backwards at high speeds - fixed the skybox's blue lid for the Thames Wharf and City cutscenes - fixed fish schools to no longer swim at supersonic speeds if their triggers do not have timers set, or reuse the same timer - fixed Lara letting go of some ledges - fixed shadow sizes dependent on Lara's placement instead of their owner's ## [1.0.3](https://github.com/LostArtefacts/TRX/compare/trx-1.0.2...trx-1.0.3) - 2025-11-27 - fixed the conveyor belt fuse in Natla's Mines not appearing after using the nearby switch (#4349, regression from 1.0) ## [1.0.2](https://github.com/LostArtefacts/TRX/compare/trx-1.0.1...trx-1.0.2) - 2025-11-26 - fixed Lara being unable to interact with keyholes after picking up an item if animated interactions are enabled (#4342, regression from 1.0) ## [1.0.1](https://github.com/LostArtefacts/TRX/compare/trx-1.0...trx-1.0.1) - 2025-11-25 - changed default master volume to 80% in TR2 to match TR1 (#4337) - fixed 2D sprites not appearing in the UI (#4338, regression since 1.0) ## [1.0](https://github.com/LostArtefacts/TRX/compare/76109a8855da99f3304ca4d9a3f5882dada2dd40...trx-1.0) - 2025-11-23 Showcase: https://youtu.be/vVU9vbUXTXc **Common**: - added LUA scripting engine Supports basic events, item interactions, teleporting and much more. See [the documentation](../trx/lua) for details. - added a game flow option for cold water in custom levels, similar to TR3 (#4021) - added a splash effect when Lara jumps in wading depth water, similar to TR3+ (#3975) - added bounding box debugging (`/debug 1` or `/set debug-cuboids 1`) - added support for object, music, sound, flip effects, Lara state, and Lara animation slots overrides through CSV catalogs Lets builders link hardcoded logic to slots of their choice, allowing object sharing between games (for example, use TR1 bats in TR2). This feature is experimental — some objects may not behave correctly. Please report any bugs encountered! 🩷 See [the documentation](../CATALOGS.md) for details. - added `enable_debug_camera` setting that shows camera position in realtime (reachable via `/debug` and `/set`) - added the ability to fast-forward through cutscenes with the right button (+5 s) or with slow+right (+1 s) - added support for dark theme on Windows - added support for triangular geometry - added support for additive blending in textures - added support for quicksand rooms - improved bilinear filtering for smoother edge blending when multiple objects overlap in depth - improved rendering of statics and items in overlapping rooms (#2005) - improved ricochets placement - fixed dart and disc ricochets being placed mid-air (#4063) - fixed ricochets not showing on slopes - improved bar setting UIs in various ways - added two new options: "Show bars" on/off, and "Flash bars" on/off - changed the bars options to be placed in its own tab - changed the appearance labels to better align with expectations (#4025) - removed the look modes for every bar (except enemy bars that retain the "boss only" setting) - fixed health bar flicker on medi packs when cycling the inventory ring (#4211, regression from TR1X 4.14 / TR2X 1.4) - changed the `/debug` command to accept optional option name argument (for example: `/debug pos 1`) - changed dart emitters and disc emitters to have separate slots (so with catalogs, both can be used in the same level simultaneously) - changed the debug position UI to no longer be hidden in photo mode - changed the unrestricted look mode option to include Lara being able to look freely while shooting an enemy (#4090) - changed the `ambient_tracks` property to be only available on the root level - changed music triggers that match the level's default ambient track to automatically be treated as ambient if omitted from `ambient_tracks` (#4181) - changed the `-q`/`--quiet` argument to no longer silence warnings - changed the `Remember Guns between Levels` option to also apply to whether or not Lara starts with those guns equipped - changed the FOV formula to be consistent between games - changed the FOV default increment from 10 to 5 (#4026) - removed "Vertical FOV" option - removed "Use PS1 FOV" option - fixed missing footstep sound effects when Lara climbs off a ladder and when she finishes a handstand (#4030) - fixed a crash in custom levels if a flip effect that expects to act on an item is used in a regular trigger (#4085) - fixed a crash if trying to kill an enemy by name but there is no naming definition for that object - fixed photo mode camera clipping through overlapping rooms (#1674) - fixed bogus warnings about resume info in logs when playing cutscenes and in the title level - fixed title bar size being too small on HiDPI screens on Windows platform (#2837) - fixed statics and items not getting rendered when all portals leading to them are offscreen (#2005) - fixed Lara's arms getting stuck in the M16 gun firing animation while she dies (#4130) - fixed Lara jittering in the QWOP state - fixed doors and trapdoors not interpolating when using the door cheat - fixed credit images and loading images showing black screen if the file is missing (#4325) - fixed caustics not affecting underwater plant sprites (#4317) **TR1**: - added a new easter egg command - added support for flares (for OG levels, use `/give flare`) (#4121) - added support for TR2 weapons (for OG levels, use `/give moreguns`) - added support for custom levels to use Lara's extra animations from TR2 - added new hidden settings (available via LUA and the `/set` command): - `flow.lockout_option_ring` - `flow.load_save_disabled` - `flow.play_any_level` - `flow.cheat_keys` - added the ability for the sound system to use Lara's position instead of camera's position (#1438) (Sound Options → Misc → Microphone near Lara) - added an option to use TR2-style inventory ring backgrounds in custom levels (Graphic Options → UI → Inventory background) (#4264) - added an option to use TR2-style statistics dialog backgrounds in custom levels (Graphic Options → UI → Stats background) (#4264) - improved the positions of some 3D pickup items, such as the scion that Pierre drops - improved the quality of the PS1 Uzi SFX (#4024) - changed the following game flow options to become hidden settings (available via LUA and the `/set` command): - `flow.demo_delay` - `gameplay.enable_killer_pushblocks` - changed Select Level and Story So Far features placement to the New Game menu - changed the input buffering option to separately tackle F-keys and Inventory (Gameplay → Input → Buffering (F-keys), Gameplay → Input → Buffering (Inventory)) - changed exploded meshes to trigger a splash effect when they hit water, similar to TR2 - changed LOS algorithm to TR2+ implementation - changed save crystal collision to make them easier to activate - changed cutscene data (e.g. `cut1.phd`, as opposed to in-game cinematics) to match TR2 format, where Lara (as `O_LARA`) must be defined as an item in the level file - changed the Remove shotguns, Remove Uzis and Remove Magnums into a single "Remove extra guns" option - changed toggling Lara's braid in-game to swap out her head and torso meshes appropriately without the need to reload the level (#2399) - removed the `Enhanced shotgun targeting` option in favour of using the common weapon lock mode (Gameplay → Controls → Weapon lock mode) - fixed Lara being able to push blocks through toggle opacity 1 portals (#4129) - fixed Lara drifting during the Atlantis cutscene while the camera focuses in on Natla (#4153) - fixed Lara retaining her hit animation if nudged by an enemy at the same time as starting a special animation such as picking up a scion (#4212) - fixed Lara being drawn if the explosion cheat has been used and Bacon Lara is active (#4148) - fixed ambient music not playing in demo levels (#4046, regression from 4.13) - fixed caustics stopping after spending roughly 12 minutes in a level (#4109, regression from 4.10) - fixed legacy UB crashing the game (#4113, regression from 4.12) - fixed select level feature to also be available to games started with `/play` - fixed select level feature slot status not updated on save - fixed "Story So Far..." showing loading screens - fixed matrix stack overflow crash when moving through overlapping or dome portals (#2685) - fixed the gun-draw SFX playing when holstering the shotgun (#3755) - fixed Lara's braid remaining reflective if the fly cheat is used to resurrect her on the Midas hand - fixed invulnerability cheat not getting disabled during the demos (regression from 4.13) - fixed crash when loading OG saves made in City of Khamoon, while the "Restore PS1 enemies" option is on (#4217, regression from 2.16) - fixed the jump lock mode UI option remaining visible when responsive jumping is disabled (#4027, regression from 4.13) - fixed a slight misalignment in the PDA in its open state (#4247) - fixed Lara being unable to exit the water in (for example) room 41 of Return to Egypt (#4315, regression from 4.12) **TR2**: - added loading screens (Gameplay Options → General → Loading screens) (#1620) Tomb Raider II 3×2 upscales done by Arsunt. Tomb Raider II: The Golden Mask images done by Lito Perezito. - added Restart Level option when Lara dies (#1555) - added Play Previous Levels feature (available in the New Game screen) - added Story So Far… feature (available in the New Game screen) - added game mode selection to the Play Any Level feature - added support for custom levels to use Lara's extra animations from TR1 - added an option to use TR1-style inventory ring backgrounds (Graphic Options → UI → Inventory background) (#3923) - added an option to use TR1-style statistics dialog backgrounds (Graphic Options → UI → Stats background) (#3923) - added extended statistics support (#2578) - added pickup count and death count support in the stats screen (Graphic Options → UI → Statistics details) - added max pickup, secret and kills support (Graphic Options → UI → Statistics details) - added deaths counter support (Gameplay Options → General → Count Lara's death) - added unobtainable secrets, pickups and kills stats support in the gameflow - added an option to disable final statistics (Gameplay options → General → Final statistics screen) - added an option to disable all medipacks (Gameplay options → Mods → Remove medipacks) - added an option to disable all guns except Pistols (Gameplay options → Mods → Remove extra guns) - added an option for pickup aids, which will show an intermittent twinkle when Lara is nearby pickup items (Graphic Options → Visuals → Pickup aids) (#4057) - added an option for animated interactions with pickups and switches (Gameplay → Controls → Animated interactions) (#4067) - added an option to change max savegame slot count (Gameplay → General → Number of save slots) - added an option to turn off Inventory input buffering (Gameplay → Input → Buffering (Inventory)) - added an option to turn on TR1-style F-keys input buffering (Gameplay → Input → Buffering (F-keys)) - added an option to draw Shotgun flashes (Graphic Options → Visuals → Shotgun flash) - added support for TR1-like secret triggers (#2047) - added support to disable wading, like TR1 (Gameplay → Controls → Wading) (hidden by default) - added support to disable responsive running jumps, like TR1 (Gameplay → Controls → Responsive jumping) - added support to disable responsive swim cancel, like TR1 (Gameplay → Controls → Responsive swim cancel) - added support for game-flow defined enemy item drops, similar to OG TR1 levels; regular level-defined drops will continue to work normally - improved the quality of the PS1 barefoot SFX (#4024) - changed the following game flow options to become hidden settings (available via LUA and the `/set` command): - `flow.lockout_option_ring` - `flow.load_save_disabled` - `flow.play_any_level` - `flow.demo_delay` - `flow.cheat_keys` - `gameplay.enable_killer_pushblocks` - changed the Pause key to no longer work when Lara's dead (similar to TR1) - changed sprites to respect the water tint if placed underwater - removed the following game flow options: - `cmd_init` - `cmd_title` - `cmd_death_in_demo` - `cmd_death_in_game` - `cmd_demo_end` - `cmd_demo_interrupt` - `single_level` - `is_demo_version` - fixed the new game modes dialog requiring the Action key to show up (TR2 dialogs don't need this.) - fixed Lara's pistols not being removed from holsters when she equips during a cutscene (#4136) - fixed potential softlocks in custom levels with enemies who have end-level flip effects but the player uses the grenade launcher to kill them (#4261) - fixed Lara not having holstered pistols after she changes costumes in the Diving Area cutscene (#4142) - fixed ambient music not playing in demo levels (#4046, regression from 1.3) - fixed twists not adhering to original game movement (#4078, regression from 1.4) - fixed legacy saves in Opera House and Vegas crashing the game (#4103, regression from 1.5) - fixed caustics stopping after spending roughly 12 minutes in a level (#4109, regression from 1.4) - fixed Lara being able to push blocks through toggle opacity 1 portals (#4129, regression from 1.5) - fixed pistols disappearing from Lara's holsters in the cutscene following The Great Wall (#4145, regression from 0.9) - fixed Lara's thigh meshes defaulting if entering the fly cheat while holding a flare and she doesn't currently have holstered weapons (#4143, regression from 1.3) - fixed wrong lighting of exploded body parts - fixed weird clipping when moving through overlapping or dome portals (#2685) - fixed Lara reloading the harpoon gun if she draws the weapon and does not have any harpoons (#4259) - fixed Lara holding on to the grenade launcher for too long when undrawing it (#3474) - fixed the holster SFX playing when drawing rifle type weapons (#3755) - fixed missing SFX in the harpoon drawing and undrawing animations (#3755) - fixed invulnerability cheat not getting disabled during the demos (regression from 1.3) - fixed disable targeting allies option not working (#4184, regression from 1.5) - fixed Lara losing forward momentum on springboards when the wall glitch mode option is set to `Fixed` (#4187, regression from 1.2) - fixed the M16 accuracy option not taking effect until restarting the game (#4227, regression from 0.3) - fixed incorrect keys object orientation in the inventory ring (#4239, regression from 0.3) - fixed underwater hum when Microphone near Lara option is on (#2188) - fixed 3D pickups not rendering if the associated sprite is not present in the level file (#4275, regression from 0.6) **TR3**: - added basic TR3 level loader (nothing is working yet!) ================================================ FILE: docs/CHANGE_SUBMISSION.md ================================================ # Submitting changes ## Pull requests We commit via pull requests rather than directly to the protected `develop` branch. Each pull request undergoes a peer review and requires at least one approval from the development team before merging. We ensure that all discussions are resolved and aim to test changes prior to merging. When a code review comment is minor and the author has addressed it, they should mark it as resolved. Otherwise, we leave discussions open to allow reviewers to respond. After addressing all change requests, it's considerate to re-request a review from the relevant parties. ## Changelog We maintain a changelog for each project in the `CHANGELOG.md` files, recording any changes except internal modifications or refactors. New features and original bug fixes should also be documented in the `README.md`. If a change affects game flow behavior, be sure to update the `GAME_FLOW/` accordingly. Likewise, changes to the console commands should update `COMMANDS.md`. ## Commit scope When merging, we use rebasing for a clean commit history. For that reason, each significant change should have an isolated commit. It's okay to force-push pull requests. ## Commit messages **Bug fixes and feature implementations should include the phrase `Resolves #123`.** For player-facing changes without an existing ticket, a ticket needs to be created first. Anything else is just for consistency and general neatness. Our commit messages aim to respect the 50/72 rule and have the following form: module-prefix: description in an imperative mood (max 50 characters) Longer description of what happens that can span multiple lines. Each line should be maximally 72 characters long, with the exceptions of code/log dumps. The prefix should describe the module that the pull request touches the most. In general this is the name of the `.c` or `.h` file with the most changes. Note that this includes the folder names which are separated with `/`. Avoid underscores (`_`) in favor of dashes (`-`). The description should be as concise as possible; any details should be given in the commit message body. Use simple, to the point words like `add`, `fix`, `remove`, `improve`. Good: ```text ui: improve resolution changing Added the ability for the player to switch resolutions directly from the game ui. Resolves #123. ``` Great: ```text log: fix varargs for Log_Message() On Linux, the engine crashes when printing the log messages. This happens because the current code re-uses the same va_list variable on two calls to vprintf() and vfprintf(). Actually, this is not allowed. For using the same information on multiple formatting functions, it is needed to create a copy of the primary va_list to a second one, by using va_copy(). After rewriting properly the Log_Message() function, the segmentation fault is gone. Tested on both Linux and Windows builds. ``` > [!NOTE] > This has no ticket number, but it was an internal change improving support > for a platform unsupported at that time, which made it acceptable. Bad: ```text ui: implemented the ability to switch resolutions from the ui ``` - the subject doesn't use imperative mood - the subject is too long - it's missing a ticket number Bad: ```text dart: added dart emitters to the savegame (#779) dart: added dart emitters to the savegame Add function for checking legacy savegame save flags Resolves #774. ``` - it duplicates the subject in the message body - the subject doesn't use imperative mood When using squash to merge, it is acceptable for GitHub to append the pull request number, but it's important to carefully review the body field, as it often includes unwanted content. ================================================ FILE: docs/CODING_GUIDELINES.md ================================================ # Coding guidelines ## Top values - Compatibility with the original game's look and feel - Player choice whether to enable any impactful changes - Maintainability - Automation where possible - Documentation (git history and GitHub issues are great for this purpose) ## Automatic code formatting This project uses [pre-commit](https://pre-commit.com/) to make sure the code is formatted the right way. This tool has additional external dependencies: `clang-format` for automatic code formatting. To install pre-commit: ```console python3 -m pip install --user pre-commit pre-commit install ``` To install required external dependencies on Ubuntu: ```console apt-get install -y clang-format-18 ``` After this, each commit should trigger a hook to automatically format changes. To manually initiate this process, run `just lint-format`. This excludes the slower checks that could affect productivity. For the full process, run `just lint`. If installing the above software isn't possible, the CI pipeline will indicate necessary changes in case of mistakes. ## Coding conventions - Variables are `lower_snake_case` - Global variables are `g_PascalCase` - Module variables are `m_PascalCase` and static - Global function names are `Module_PascalCase` - Module functions are `M_PascalCase` and static - Macros are `UPPER_SNAKE_CASE` - Struct names are `UPPER_SNAKE_CASE` - Struct members are `lower_snake_case` - Enum names are `UPPER_SNAKE_CASE` - Enum members are `UPPER_SNAKE_CASE` It's recommended to minimize the use of global variables. Instead, consider declaring them as `static` within the module they're used. Other things: - We use clang-format to automatically format the code. - We do not omit `{` and `}`. - We use K&R brace style. - We condense consecutive `if` expressions into one. Recommended: ```c if (a && b) { } ``` Not recommended: ```c if (a) { if (b) { } } ``` When expressions become extraordinarily complex, consider refactoring them into smaller conditions or functions. ## Tooling Internal tools are typically coded in a reasonably recent version of Python, while avoiding the use of bash, shell, and similar languages. ================================================ FILE: docs/CONTRIBUTING.md ================================================ # Development guidelines This section collects the main documents for contributing to TRX. ## Build and setup - Follow [BUILDING.md](BUILDING.md) for the build workflow and supported compiler notes. ## Working with the project - Follow [CODING_GUIDELINES.md](CODING_GUIDELINES.md) for project values, automatic formatting, coding conventions, and tooling notes. - Follow [CHANGE_SUBMISSION.md](CHANGE_SUBMISSION.md) for pull requests, changelog updates, commit scope, and commit messages. ## Release process - Follow [RELEASING.md](RELEASING.md) for branching, releases, hotfixes, and versioning. ## Glossary - See [GLOSSARY.md](GLOSSARY.md) for common project terms and nicknames. ================================================ FILE: docs/GLOSSARY.md ================================================ # Glossary - OG: original game (for TR1 this is most often TombATI) - PS: the PlayStation version of the game - UK Box: a variant of Tomb Raider II released on physical discs in the UK - Multipatch: a variant of Tomb Raider II released on Steam - Vole: a rat that swims - Pod: a mutant egg (including the big egg) - Cabin: the room with the pistols from Natla's Mines - Statue: centaur statues from the entrance of Tihocan's Tomb - Bacon Lara: the doppelgänger Lara in the Atlantis level - Torso/Adam: the big boss mutant from The Great Pyramid level - Tomb1Main: the previous name of the TR1X project - TR1X: the previous name of this TRX project targeting TR1 - TR2X: the previous name of this TRX project targeting TR2 - T1M: short hand of Tomb1Main ================================================ FILE: docs/RELEASING.md ================================================ # Releasing TRX ## Branching model We have two branches: `develop` and `stable`. `develop` is where all changes about to be published in the next release land. `stable` is the latest release. ## Releasing a new version New version releases are published automatically whenever a new tag is pushed to the `stable` branch with the help of GitHub actions. The general workflow is this: ```console RELEASE_VERSION=... # Switch to the stable branch. git checkout stable # Merge `develop` into it. git merge develop # Create a special commit `docs: release X.Y.Z` marking the release in the # relevant changelog file. Then tag it with `trx-X.Y.Z`.Y.Z`. # You can do that by hand, or run the command below: tools/release commit ${RELEASE_VERSION} tools/release tag ${RELEASE_VERSION} # Review the changelog content. # Switch back to develop. git checkout develop # Merge stable using fast-forward. git merge --ff stable # Review both branches and changes. If everything is okay, push to GitHub. # You can do this by hand: git push origin develop stable trx-X.Y.Z, or: # tools/release push ${RELEASE_VERSION} ``` ## Hotfixes Hotfix releases are a bit different as we try to not include non-bugfix changes in them. Here instead of merging `develop` to `stable` we cherry-pick relevant changes, resolving conflicts along the way. ## Versioning We increase the major version for significant releases based on judgment, typically defaulting to increasing the minor version. Hotfixes increase the patch version. ================================================ FILE: docs/SECRETS.md ================================================ # GitHub repository secrets In the unfortunate event that the lead developers become unavailable for any reason, here is documentation detailing the third-party integrations we're using, including some paid services. All integrations are automated through CI, and the access information is securely stored in GitHub secrets. ### DockerHub We utilize DockerHub for our Docker builds, maintaining distinct images for Windows and Linux platforms. (Mac builds are an exception and do not employ Docker.) Each image is equipped with the necessary tools to compile the game executable, thus eliminating the need for developers or CI environments to install additional tools on their machines. While these images can be built locally, we host the pre-built Docker images on DockerHub for the convenience of new developers and CI processes that otherwise couldn't cache the images. **Variables**: - `DOCKERHUB_USERNAME`: The username to log to the DockerHub and under which the images are hosted. - `DOCKERHUB_PASSWORD`: The password to the account. Whenever we change the images for any reason (such as by introducing a new hard dependency), we have to run the Build Docker toolchain GitHub action by hand. ### MacOS builds MacOS builds require a paid Apple Developer account. **Variables**: - `MACOS_APPLEID`: Apple developer account id / email address - `MACOS_APP_PWD`: Apple developer account password. It is recommended to use an app password, that can be individually revoked. - `MACOS_TEAMID`: Every Apple developer account has an associated team ID. To see one: 1. Navigate to https://developer.apple.com/account. 2. Go to Membership details. 3. Examine the `Team ID` value. - `MACOS_KEYCHAIN_PWD`: This is used for a temporary keychain file made by the GitHub workflow - as such, the exact value doesn't really matter. - `MACOS_CERTIFICATE`: A codesigning certificate generated from the Apple developer account. To generate it: 1. Navigate to https://developer.apple.com/account/resources/certificates/list. 2. Create a new Certificate: 1. Select Developer ID Application; continue to the next page. 2. Create a Certificate Signing Request and Private Key pair: > openssl req -new -newkey rsa:2048 -nodes -keyout TR1X.key -out TR1X.csr -subj "/emailAddress=your-mail@example.com, CN=TR1X" 3. Upload the newly generated `TR1X.csr` file; continue to the next page. 3. Download the certificate and save it as `TR1X.cer`. 4. Convert the certificate to the PKCS12 format - run: > openssl pkcs12 -export -out TR1X.pem -inkey TR1X.key -in TR1X.cer -name TR1X -legacy This command will ask you for a password. It should be noted down. 5. Serialize the key in base-64 without spaces - run: > base64 TR1X.pem|tr -d '\n' > base64 -i BUILD_CERTIFICATE.p12 | pbcopy (macos) The result is to be put as the value of the `MACOS_CERTIFICATE` secret. - `MACOS_CERTIFICATE_PWD`: The password to the `MACOS_CERTIFICATE`. ================================================ FILE: docs/gameflow.schema.json ================================================ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "TRX gameflow schema", "$defs": { "path": { "oneOf": [ { "type": "string" }, { "type": "array", "items": { "type": "string" }, "minItems": 1 } ] } }, "type": "object", "properties": { "name": { "type": "string", "description": "Human-readable display name for this mod, shown in the Switch Game menu." }, "engine": { "type": "integer", "enum": [1, 2, 3], "description": "Engine version this mod runs on (1 = TR1, 2 = TR2, 3 = TR3)." }, "extends": { "type": "string", "description": "Directory name of the base mod this mod extends. Used for asset fallback." }, "main_menu_picture": { "type": "string", "description": "Path to the main menu background image." }, "main_script": { "type": "string", "description": "Path to a global Lua script to execute after game initialization, before the first level loads." }, "savegame_file_fmt": { "type": "string", "description": "Path pattern to look for the savegame files." }, "demo_version": { "type": "boolean", "description": "Legacy demo version flag (scheduled for removal)." }, "title": { "$ref": "#/definitions/title", "description": "Configuration for the title screen." }, "sfx_path": { "$ref": "#/$defs/path", "description": "Path to the sound effects (.sfx) file to use in the game." }, "injections": { "type": "array", "description": "Global data injection file paths applied to all levels unless overridden.", "items": { "type": "string" } }, "globe_select_entries": { "type": "array", "description": "Array of globe entries to define how level area selection works.", "items": { "$ref": "#/definitions/globe_select" } }, "levels": { "type": "array", "description": "Array of regular level definitions.", "items": { "$ref": "#/definitions/level" } }, "cutscenes": { "type": "array", "description": "Array of cutscene level definitions.", "items": { "$ref": "#/definitions/level" } }, "demos": { "type": "array", "description": "Array of demo level definitions.", "items": { "$ref": "#/definitions/level" } }, "fmvs": { "type": "array", "description": "Array of FMV entries.", "items": { "$ref": "#/definitions/fmv" } }, "convert_dropped_guns": { "type": "boolean", "description": "Forces guns dropped by enemies to be converted to the equivalent ammo if Lara already has the gun." }, "enable_tr2_item_drops": { "type": "boolean", "description": "Forces enemies who are placed in the same position as pickup items to carry those items and drop them when killed." }, "enforced_config": { "type": "object", "description": "Overrides for any regular game config settings.", "additionalProperties": true }, "hidden_config": { "type": "array", "description": "List of config settings to hide from the in-game settings dialogs.", "items": { "type": "string" } }, "fog_start": { "type": "number", "description": "The distance (in tiles) at which objects and the world start to fade into blackness." }, "fog_end": { "type": "number", "description": "The distance (in tiles) at which objects and the world are clipped away." }, "water_color": { "description": "Water color (R, G, B) or `#RRGGBB`. 1.0 or `FF` means pass-through, 0.0 or `00` means completely black color.", "oneOf": [ { "type": "array", "items": { "type": "number" }, "minItems": 3, "maxItems": 3 }, { "type": "string" } ] }, "ambient_tracks": { "type": "array", "description": "List of music track IDs treated as ambient music (persists on save/load).", "items": { "type": "integer" } } }, "required": [ "engine", "main_menu_picture", "savegame_file_fmt", "levels" ], "additionalProperties": false, "definitions": { "command": { "type": "object", "properties": { "action": { "type": "string" }, "param": { "type": [ "integer", "string" ] } }, "required": [ "action" ], "additionalProperties": false }, "title": { "type": "object", "properties": { "path": { "$ref": "#/$defs/path" }, "music_track": { "type": "integer" }, "inherit_injections": { "type": "boolean" }, "injections": { "type": "array", "items": { "type": "string" } }, "sequence": { "type": "array", "items": { "$ref": "#/definitions/sequence" } } }, "required": [ "path", "sequence" ], "additionalProperties": false }, "level": { "type": "object", "properties": { "type": { "type": "string" }, "script": { "type": "string", "description": "Path to a Lua script executed after loading this level." }, "path": { "$ref": "#/$defs/path" }, "music_track": { "type": "integer" }, "lara_outfit": { "type": "string" }, "weather_type": { "type": "string", "enum": [ "rain", "snow" ], "description": "Enables a per-level weather effect." }, "water_particles": { "type": "boolean", "description": "TR3 only. Enables PSX-style underwater water particles in this level." }, "cold_water": { "type": "boolean" }, "death_tile": { "type": "string", "description": "Defines death-tile behavior (for TR3 only).", "enum": [ "lava", "rapids", "electric" ] }, "sequence": { "type": "array", "items": { "$ref": "#/definitions/sequence" } }, "inherit_injections": { "type": "boolean" }, "injections": { "type": "array", "items": { "type": "string" } }, "item_drops": { "type": "array", "items": { "$ref": "#/definitions/item_drop" } }, "water_color": { "type": "array", "items": { "type": "number" }, "minItems": 3, "maxItems": 3 }, "fog_start": { "type": "number" }, "fog_end": { "type": "number" }, "unobtainable_pickups": { "type": "integer" }, "unobtainable_kills": { "type": "integer" }, "unobtainable_ally_kills": { "type": "integer" }, "unobtainable_secrets": { "type": "integer" } }, "if": { "properties": { "type": { "enum": [ "current", "dummy" ] } }, "required": [ "type" ] }, "else": { "required": [ "path", "sequence", "lara_outfit" ] }, "additionalProperties": false }, "item_drop": { "type": "object", "properties": { "enemy_num": { "type": "integer" }, "object_ids": { "type": "array", "items": { "type": "integer" } } }, "required": [ "enemy_num", "object_ids" ], "additionalProperties": false }, "globe_select": { "type": "object", "properties": { "rot": { "type": "array", "items": { "type": "integer" }, "minItems": 3, "maxItems": 3 }, "start_level_ordinal": { "type": "integer" }, "completion_level_ordinal": { "type": "integer" }, "prereq_zones": { "type": "array", "items": { "type": "integer" } }, "mesh_idx": { "type": "integer" } }, "required": [ "rot", "start_level_ordinal", "completion_level_ordinal", "prereq_zones", "mesh_idx" ], "additionalProperties": false }, "fmv": { "type": "object", "properties": { "path": { "$ref": "#/$defs/path" }, "legal": { "type": "boolean" } }, "required": [ "path" ], "additionalProperties": false }, "sequence": { "oneOf": [ { "$ref": "#/definitions/seq_loop_game" }, { "$ref": "#/definitions/seq_level_complete" }, { "$ref": "#/definitions/seq_exit_to_title" }, { "$ref": "#/definitions/seq_level_stats" }, { "$ref": "#/definitions/seq_total_stats" }, { "$ref": "#/definitions/seq_display_picture" }, { "$ref": "#/definitions/seq_loading_screen" }, { "$ref": "#/definitions/seq_play_fmv" }, { "$ref": "#/definitions/seq_give_item" }, { "$ref": "#/definitions/seq_add_secret_reward" }, { "$ref": "#/definitions/seq_play_music" }, { "$ref": "#/definitions/seq_remove_ammo" }, { "$ref": "#/definitions/seq_remove_weapons" }, { "$ref": "#/definitions/seq_remove_medipacks" }, { "$ref": "#/definitions/seq_remove_scions" }, { "$ref": "#/definitions/seq_remove_flares" }, { "$ref": "#/definitions/seq_play_cutscene" }, { "$ref": "#/definitions/seq_enable_sunset" }, { "$ref": "#/definitions/seq_set_lara_start_anim" }, { "$ref": "#/definitions/seq_setup_bacon_lara" }, { "$ref": "#/definitions/seq_disable_floor" }, { "$ref": "#/definitions/seq_globe_select" } ] }, "seq_loop_game": { "type": "object", "description": "Plays the main game loop.", "properties": { "type": { "const": "loop_game" } }, "required": [ "type" ], "additionalProperties": false }, "seq_level_complete": { "type": "object", "description": "Ends the current level and plays the next one, if available.", "properties": { "type": { "const": "level_complete" } }, "required": [ "type" ], "additionalProperties": false }, "seq_exit_to_title": { "type": "object", "description": "Returns to the title level.", "properties": { "type": { "const": "exit_to_title" } }, "required": [ "type" ], "additionalProperties": false }, "seq_level_stats": { "type": "object", "description": "Displays the end of level statistics for the current level. In a Gym level, this fades the screen to black.", "properties": { "type": { "const": "level_stats" } }, "required": [ "type" ], "additionalProperties": false }, "seq_total_stats": { "type": "object", "description": "Displays the end of game statistics with the given picture file shown as a background.", "properties": { "type": { "const": "total_stats" }, "background_path": { "type": "string" } }, "required": [ "type", "background_path" ], "additionalProperties": false }, "seq_display_picture": { "type": "object", "description": "Displays the specified picture for a fixed time. Files that are needed to function only with a specific aspect ratio can be placed in a directory adjacent to the main image, named according to the aspect ratio – for example, 4x3/title.png or 16x10/title.png. The game won't attempt to match these precisely; instead, it will select the file with the aspect ratio closest to the game's viewport. The main image designated by path is presumed to have a 16:9 aspect ratio for this purpose, and as such there's no need for 16x9-specific directory. This logic applies to all images.", "properties": { "type": { "const": "display_picture" }, "path": { "$ref": "#/$defs/path" }, "display_time": { "type": "number" }, "fade_in_time": { "type": "number" }, "fade_out_time": { "type": "number" }, "credit": { "type": "boolean" }, "legal": { "type": "boolean" } }, "required": [ "type", "path" ], "additionalProperties": false }, "seq_loading_screen": { "type": "object", "description": "Shows a picture prior to loading a level. Functions identically to display_picture, except these pictures can be enabled/disabled by the user with the loading screen option in the config tool.", "properties": { "type": { "const": "loading_screen" }, "path": { "type": "string" }, "display_time": { "type": "number" }, "fade_in_time": { "type": "number" }, "fade_out_time": { "type": "number" } }, "required": [ "type", "path" ], "additionalProperties": false }, "seq_play_cutscene": { "type": "object", "description": "Plays the specified cinematic level (from the cutscenes).", "properties": { "type": { "const": "play_cutscene" }, "cutscene_id": { "type": "integer" } }, "required": [ "type", "cutscene_id" ], "additionalProperties": false }, "seq_play_fmv": { "type": "object", "description": "Plays the specified FMV. fmv_id must be a valid index into the fmvs root key.", "properties": { "type": { "const": "play_fmv" }, "fmv_id": { "type": [ "integer", "string" ] } }, "required": [ "type", "fmv_id" ], "additionalProperties": false }, "seq_give_item": { "type": "object", "description": "Adds the specified item and quantity to Lara's inventory.", "properties": { "type": { "const": "give_item" }, "object_id": { "type": [ "integer", "string" ] }, "quantity": { "type": "integer" } }, "required": [ "type", "object_id" ], "additionalProperties": false }, "seq_add_secret_reward": { "type": "object", "description": "Adds the specified item to the current level's list of rewards for collecting all secrets.", "properties": { "type": { "const": "add_secret_reward" }, "object_id": { "type": [ "integer", "string" ] }, "quantity": { "type": "integer" } }, "required": [ "type", "object_id" ], "additionalProperties": false }, "seq_play_music": { "type": "object", "description": "Plays the given audio track.", "properties": { "type": { "const": "play_music" }, "music_track": { "type": "integer" } }, "required": [ "type", "music_track" ], "additionalProperties": false }, "seq_remove_ammo": { "type": "object", "description": "Any combination of these sequences can be used to modify Lara's inventory at the start of a level. `remove_weapons` does not remove the ammo for those guns, and `remove_ammo` does not remove the guns. Item removal is performed first, followed by addition.", "properties": { "type": { "const": "remove_ammo" } }, "required": [ "type" ], "additionalProperties": false }, "seq_remove_weapons": { "type": "object", "description": "Any combination of these sequences can be used to modify Lara's inventory at the start of a level. `remove_weapons` does not remove the ammo for those guns, and `remove_ammo` does not remove the guns. Item removal is performed first, followed by addition.", "properties": { "type": { "const": "remove_weapons" } }, "required": [ "type" ], "additionalProperties": false }, "seq_remove_medipacks": { "type": "object", "description": "Any combination of these sequences can be used to modify Lara's inventory at the start of a level. `remove_weapons` does not remove the ammo for those guns, and `remove_ammo` does not remove the guns. Item removal is performed first, followed by addition.", "properties": { "type": { "const": "remove_medipacks" } }, "required": [ "type" ], "additionalProperties": false }, "seq_remove_scions": { "type": "object", "description": "Any combination of these sequences can be used to modify Lara's inventory at the start of a level. `remove_weapons` does not remove the ammo for those guns, and `remove_ammo` does not remove the guns. Item removal is performed first, followed by addition.", "properties": { "type": { "const": "remove_scions" } }, "required": [ "type" ], "additionalProperties": false }, "seq_remove_flares": { "type": "object", "description": "Any combination of these sequences can be used to modify Lara's inventory at the start of a level. `remove_weapons` does not remove the ammo for those guns, and `remove_ammo` does not remove the guns. Item removal is performed first, followed by addition.", "properties": { "type": { "const": "remove_flares" } }, "required": [ "type" ], "additionalProperties": false }, "seq_enable_sunset": { "type": "object", "description": "Enables the sunset effect, like in Bartoli's Hideout. At present, this feature is hardcoded to gradually darken the game 40 minutes into playing a level.", "properties": { "type": { "const": "enable_sunset" } }, "required": [ "type" ], "additionalProperties": false }, "seq_set_lara_start_anim": { "type": "object", "description": "Applies the selected animation to Lara when the level begins. This is used, for example, in the Offshore Rig of Tomb Raider II.", "properties": { "type": { "const": "set_lara_start_anim" }, "anim": { "type": "integer" } }, "required": [ "type", "anim" ], "additionalProperties": false }, "seq_setup_bacon_lara": { "type": "object", "description": "Sets the room number in which Bacon Lara will be anchored to enable correct mirroring behaviour with Lara.", "properties": { "type": { "const": "setup_bacon_lara" }, "anchor_room": { "type": "integer" } }, "required": [ "type", "anchor_room" ], "additionalProperties": false }, "seq_disable_floor": { "type": "object", "description": "Configures a specific height (with 256 representing 1 click and 1024 representing 1 sector) to define an abyss that will invariably lead to Lara's death if she falls into it. Additionally, it employs special rendering to ensure it isn't treated as solid ground.", "properties": { "type": { "const": "disable_floor" }, "height": { "type": "integer" } }, "required": [ "type", "height" ], "additionalProperties": false }, "seq_globe_select": { "type": "object", "description": "Displays the area selection globe using the specified background image.", "properties": { "type": { "const": "globe_select" }, "image": { "type": "string" } }, "required": [ "type", "image" ], "additionalProperties": false } } } ================================================ FILE: docs/tr1/CHANGELOG.md ================================================ ## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr1-4.15.1...develop) - ××××-××-×× See [/docs/CHANGELOG.md]. ## [4.15.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.15...tr1-4.15.1) - 2025-10-10 - changed the examine dialog to be usable with non-puzzle items (#4009) - fixed a crash on game exit if specifying "ambient_tracks" in the game flow root (regression from 4.11) - fixed alternate ambient tracks being lost on reload in custom levels (#3997, regression from 4.14) - fixed Lara at times not being able to grab pushblocks despite being in the correct position to do so (#4005, regression from 0.9.1) - fixed Lara appearing flat for a frame during the neutral twist, controlled drop and ledge jump back animations (#4012, regression from 4.14) - fixed the pickup embed glitch when Lara is below a steeply sloped ceiling not being optional (Gameplay → Fixes → Fix pickup embed glitch) (#4020, regression from 4.10) ## [4.15](https://github.com/LostArtefacts/TRX/compare/tr1-4.14.2...tr1-4.15) - 2025-10-04 Showcase: https://youtu.be/BwZXWL0WULg - added an option to use TR2-style UI bars (Graphics → UI → Bars look) - added an option to use PS1-style UI bars (Graphics → UI → Bars look) (#1637) - added a new `/cls` / `/clear` console command to quickly clear console logs - added support for ladders (#3124) - improved PS1-style UI faithfulness - improved sound settings: - added tabs (Volume and Misc) - added a dedicated option to control master volume (Sound options → Volume → Master volume) - added a dedicated option to control cutscenes volume (Sound options → Volume → Cutscenes volume) (#3490) - added a dedicated option to control FMV volume (Sound options → Volume → FMV volume) (#3490) - added a dedicated option to control general ambient volume (Sound options → Volume → Ambient volume) (#3707) - improved volume settings to accept slow input for finer adjustments - fixed changing sound volume not updating certain ambient sound sources while in the inventory ring (#3970) - changed OG glitch-related config options to be on/fixed by default (#3929) - changed the UI style to use the PS1 look by default (Graphics → UI → Menu style) - changed pickup aids to be enabled by default (Graphics → Visuals → Pickup aids) - changed responsive jumping to be enabled by default (Gameplay → Controls → Responsive jumping) - changed lean jumping to be enabled by default (Gameplay → Controls → Lean jumping) - changed smooth swimming to be enabled by default (Gameplay → Controls → Smooth swimming) - changed responsive swim cancel to be enabled by default (Gameplay → Controls → Responsive swim cancel) - changed idle pose timeout from 15 to 60 seconds by default (Gameplay → Controls → Idle pose timeout) - changed idle pose camera to be disabled by default (Gameplay → Controls → Idle pose camera) - changed PS1 uzi sound to be enabled by default (Sound → Misc → PS1 uzi sound) - changed max pickup scale to 200% (#3952) - fixed pickup scale being greyed out if the 3D pickups option is enabled (#3952) - fixed certain ambient sounds volume scaling wrong on non-100% volumes - fixed trapdoor type 3 (object #67) not functioning (#3895) - fixed gameplay settings UI displaying eagerly after the first use (#3583, regression from 4.13) - fixed changing FPS after advancing frames in photo mode causing the game to speed up (#3605, regression from 4.13) - fixed CPU spike during playing FMVs (#3908, regression from 4.6) - fixed `/play` command likely to skip opening FMVs when inventory buffering is enabled (#3910, regression from 3.0) - fixed `/pos` command crashing in cutscenes (#3944, regression from 4.10) - fixed loading a save made in the gym with the item cheat resulting in Lara's meshes appearing broken (#3917, regression from 4.7) - fixed resumed music tracks playing briefly track start upon savegame load (#3916) - fixed loading TombATI saves with shotgun equipped causing weird Lara's animation (#3920, regression from 4.12) - fixed numerous interactions with movable blocks, trapdoors, drawbridges, bridges, sliding pillars, and falling blocks for custom levels (#2758): - added the ability for movable blocks to move on trapdoors, drawbridges, bridges, sliding pillars, and falling blocks - added the ability for stacks of movable blocks to fall and land on trapdoors, drawbridges, bridges, sliding pillars, and falling blocks - added the ability for stacks of movable blocks to fall when on opened trapdoors and drawbridges - fixed various bugs with falling movable blocks - fixed pushblocks becoming unusable when on the same sector as a door that does not sit on a room portal (#3814) - fixed pushblocks that fall from a great height potentially causing a crash (#3969) - fixed recordings replaying commands twice (regression from 4.14) - fixed the fix for the sticky corner glitch not being optional - now linked to Gameplay → Fixes → Wall glitch mode (#3957, regression from 4.14) - fixed Lara retaining guns if drawn during wade to float transition (#3979, regression from 4.13) - fixed -s/--save argument no longer working with -l/--level (#3990, regression from 4.14) ## [4.14.2](https://github.com/LostArtefacts/TRX/compare/tr1-4.14.1...tr1-4.14.2) - 2025-09-07 - fixed broken rendering in MacOS releases (#3880, regression from 4.14) - fixed images from MacOS releases (#3892, regression from 4.14) ## [4.14.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.14...tr1-4.14.1) - 2025-08-30 - fixed missing shader and configuration files from MacOS releases (#3870, regression from 4.14) - fixed zero byte at the end of config files (#3875, regression from 4.14) - fixed stacked sprites flickering (#3872, regression from 4.14) ## [4.14](https://github.com/LostArtefacts/TRX/compare/tr1-4.13.2...tr1-4.14) - 2025-08-23 Showcase: https://www.youtube.com/watch?v=iV8G9lhxVQ8 >[!WARNING] >Attention level builders: this version introduces backwards incompatible changes to the file structure. >Please refer to the [migration guide](../trx/MIGRATING.md) to see how to update your levels. - added lighting contrast option (Graphic options → Rendering → Lighting contrast) - added new command switches: - `--test-record` and `--test-replay` for automated playthroughs with (internal tool – the recording file format may be subject to changes) - `--headless`: runs the game offscreen with no audio and at unlocked simulation speed - -q`, `--quiet`: outputs only error messages to the terminal, with log files being written to normally - added new hotkeys: F7 for toggling the wireframe mode, F8 for cycling the lighting contrast - added ability to move Lara around in photo mode - added additional poses for photo mode - added an option to allow Lara to sprint (Gameplay → Controls → Sprinting) (#3711) - added an option to use Lara's slide-to-run animation from TR3+ (Gameplay → Controls → Slide-to-run) (#1089) - added an option to use Lara's neutral jump-twist from early TR1 betas (Gameplay → Controls → Neutral twists) (#1392) - added an option to allow Lara to turn around and grab a ledge she has just stepped off (Gameplay → Controls → Controlled drops) (#3621) - added an option to allow Lara to jump up or back when hanging from a ledge (Gameplay → Controls → Ledge jumps) (#3683) - added an option to have Lara pose after standing idle for a certain time (Gameplay → Controls → Idle pose timeout) (#3727) - added an option to keep sprites upright (Graphic options → Rendering → Sprites lock mode) - added an option to scale the 3D pickups in the UI (Graphic options → UI → Pickup scale) - added an option to control fog color (Graphic options → Visuals → Fog transparency and Fog color) (#712, #3618) - added Russian translation - added German translation - added skyboxes to The Cistern and Tomb of Tihocan (#2143) - added a new `/lua` console command (for now, [it cannot do much](../trx/lua/)) - added a new `/restless` console command, which enables or disables infinite sprint - added debug room clip feature (`/debug 1`) - improved object loading error messages when an invalid object ID is detected - improved frames in Lara's jump-twist animations - improved lighting, projection and sizing of 3D pickups in the UI - improved PS1 menu style border offsets and frames to match PC style - improved drawing shadows in no-clip camera mode (they're no longer double-sided) - improved wireframe mode to show text and UI normally - improved bilinear filter edge blending (#587) - improved window resize performance in the title inventory ring - changed the brightness filter to also work on title inventory ring background - changed the brightness filter option to work in smaller increments (10% reduced to 5%); added support for slow increments by 1% (hold Walk key) - changed the text and bar scale option to work in smaller increments (10% reduced to 5%); added support for slow increments by 1% (hold Walk key) - changed the game flow and game strings file placement - changed the skybox option to allow toggling in-game without the need to reload the level - changed the texture page limit from 128 to unlimited (#3517) - changed the `/set` console command to report boolean values as `0` or `1`, language-agnostic - changed waterfall objects to always be drawn when active rather than only when Lara is within a 10 sector range (#3598) - changed `-l`/`--level` switch to accept the level number on top of the level path - changed settings dialogs to show a suitable message if a level builder has hidden all options within that dialog (#3637) - changed the fly cheat to allow Lara to interact with switches and pickups (#3665) - removed the option in Unfinished Business to fix animated sprites as it is irrelevant there - fixed glide camera behaviour and position in room 101 in Temple of the Cat (#3533) - fixed French translations containing Italian text in some cases (#3567) - fixed the camera remaining locked on moving lava if it touches Lara when she is immune (#3578) - fixed several issues with door data - fixed missing door/trapdoor sound effects (#3408, #3374, #3617, #3619) - fixed animation frames in doors in St. Francis' Folly, Tomb of Tihocan and Sanctuary of the Scion (#3661) - fixed the cameras for doors 81 in Tomb of Tihocan and 1 in Sanctuary of the Scion only showing once (#3661) - fixed the passport having an invisible back page, noticeable when opening/closing it (#2051) - fixed z-fighting on the front of the passport (#3584) - fixed setting description dialog missing borders with PS1 UI style (#3714, regression from 4.12) - fixed being unable to activate waterfall objects with code bits (#3589) - fixed skippable triggers for waterfall objects in Lost Valley (#3593) - fixed incorrectly rotated 3D pickup items in several levels (#2147) - fixed incorrect room mesh structure in Vilcabamba room 41, causing disappearing polygons (#3613) - fixed missing textures on the statues in Vilcabamba and Tomb of Qualopec (#3629) - fixed missing textures in Atlantis rooms 7, 9, 13, 14, 95, 96 (#3657) - fixed missing double-sided textures in The Cistern rooms 9 and 12 - fixed texture clipping in Atlantean Stronghold when looking into room 18, and missing textures in rooms 5, 6, 18 and 74 (#3668) - fixed the collision box on the tall statues in Tomb of Qualopec e.g. room 20 (#3629) - fixed the mesh structure on the cat statue in Egyptian levels to standardize its position (#3634) - fixed the collision box on some static meshes in Egypt to prevent the camera shaking when Lara walks by them (#762) - fixed incorrect transparent pixels on room textures in St. Francis' Folly and Temple of the Cat (#3659) - fixed the positions of big pods in Atlantean levels and cutscenes (#3670) - fixed several texture issues in Lara's Home, Vilcabamba, Lost Valley, St. Francis' Folly and Natla's Mines (#3860) - fixed a missing transition animation between Lara jumping forward and entering freefall (#3815) - fixed incorrect wet footstep sounds in some of Lara's climb-up animations (#3607, regression from 4.6) - fixed the `/kill` command potentially causing a crash if used in a level with pods that don't hatch creatures (#3628, regression from 4.12) - fixed Lara's animation not being restored correctly on load if a save was made during a special animation, such as using the Midas Hand (#3625, regression from 4.9) - fixed emitted darts moving in the wrong direction when reloading a save (#3677, regression from 2.11) - fixed backslash/grave key/less-than character on some keyboards shown as ???? – now it's shown as backslash (#3713) - fixed wireframe mode rendering as mostly white (#3649, regression from 4.13.2) - fixed wireframe mode not working in the inventory ring (#3651, regression from 4.10) - fixed the boulder in room 78 getting drawn in the overlapping room 74 in Tomb of Tihocan (#3761, regression from 4.10) - fixed shadow rendering - fixed Y component not interpolated in 60 FPS (#1314) - fixed shadows being rendered partially opaque near room portals (#879) - fixed Bacon Lara shadow rendered transparent when she's standing on a trapdoor (#3666) - fixed potentially being able to reactivate an already used puzzle slot's trigger (#3849, regression from 4.13) - fixed being unable to cycle poses in photo mode if cheats were disabled (#3726, regression from 4.13) - fixed Lara exiting the fly cheat if the walk key is used during photo mode (#3753, regression from 4.13) - fixed being able to issue certain console commands that target Lara during loading screens (#3662, regression from 4.13) - fixed flame SFX being audible underwater (#3830, regression from 4.13) - fixed z-fighting of doors near walls - fixed Lara walking backwards off ledges into lava (#3745) - fixed room scheduling algorithm sometimes drawing overlapping rooms (#3774, regression from 4.1) - fixed exiting photo mode on a controller conflicting with the roll input (#3842, regression from 4.8) - fixed resuming non-ambient music tracks when loading a savegame (#3845, regression from 4.13) - fixed quick draw button not working until after Lara equipped guns by other methods with certain saves (#3844, regression from 4.13) ## [4.13.2](https://github.com/LostArtefacts/TRX/compare/tr1-4.13.1...tr1-4.13.2) - 2025-07-20 - fixed savegame scanner only seeing all-lowercase file names (#3518, regression from 4.9) - fixed drawing UI text with bilinear filter and PS1 UI (#3548, regression from 4.13) - fixed dynamic fire light being generated despite the flame object not being present in the level (#3539, regression from 4.13) - fixed the first camera frame when starting or loading a level being inaccurate (#3537, regression from 4.12.3) ## [4.13.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.13...tr1-4.13.1) - 2025-07-18 - fixed Lara's first pose in photo mode at times being skipped (#3522, regression from 4.13) - fixed Lara's arms being drawn inaccurately when posing in photo mode with dual weapons equipped (#3520, regression from 4.13) - fixed Lara being unable to use key items at times with animated interactions enabled (#3524, regression from 4.13) ## [4.13](https://github.com/LostArtefacts/TRX/compare/tr1-4.12.3...tr1-4.13) - 2025-07-14 Showcase: https://youtu.be/YKI7u2QOolU - reworked screen resolutions - removed "screen resolution" option - removed "window size" rendering mode, enforcing the FBO rendering method (#3332) - added aspect ratio mode (Graphic options → Rendering → Aspect mode) - added window border option (Graphic options → Rendering → Borders) - added integer upscaling option (Graphic options → Rendering → Upscaling factor) - renamed "FBO filter" option to "Upscaling filter" (Graphic options → Rendering → Upscaling filter) - greatly improved text and other UI rendering with upscaling turned on (#1944) - changed screenshots to always produce images at desktop resolution - added French translation - added Gaelic translation - added Italian translation to the installer - added dedicated British English translation (#3212) - added the ability to advance individual frames to the photo mode - added the ability to skip end game credits (#3266) - added the ability to hide specific game settings (#3242) - added the ability to cycle UI tabs with sidestep keys (#3272) - added the ability to skip consecutive credit images by holding the action / escape keys - added the ability to cycle between a list of predefined Lara poses in the photo mode - added a `/lighting` console command to let the player turn lighting system on/off - added an `/immune` console command to make Lara impervious to damage - added support for underwater save crystals in custom levels (#3356) - added an option to have dynamic lights generated by flames (Graphic options → Visuals → Fire lighting) (#3336) - added an option to control responsive jumping lock behaviour (Gameplay settings → Controls → Jump lock mode) (#3389) - added an option to display level counter in the statistics dialog (Graphic options → UI → Level counter) (#1087) - added an option to control playing of certain animation sound effects such as doors when underwater (Sound options → Underwater animation SFX) (#3385) - added an option to allow the audio to play when the game is out of focus (Sound options → Mute audio when focus lost, #3333) - added an option to make the quick gun equip keys also holster the active gun (Gameplay settings → UI → Quick gun keys) (#828) - added an option to control texture filter for UI alone (Graphic options → Rendering → UI filter) - added the ability to use the dev console during FMVs - improved the `/tp` command to orient Lara towards keyholes and doors - improved handling of animation sound effects when in shallow water (#3385) - improved performance when resizing the window - improved error messages for game flow and string edit mistakes to include path of the problematic file - changed statistics details mode to be placed in the UI section - changed controls dialog to remember the player's preferred input method - changed UI to show icons relevant to the chosen input method - changed death timer skip to only trigger with Action and Inventory keys - changed the examine dialog to be close-able with Look button (#3225) - changed some settings to be hidden when they're only applicable to specific games or custom levels (#3242) - changed some settings to be dimmed when they're not taking effect due to other settings (#3166) - changed photo mode help dialog to show icons for inputs - changed settings to retain their active position until exiting to title or starting a new level (#3271) - changed the dev console to accept compound characters (#2938) - changed save crystal collision to be more lenient for custom levels (#3343) - changed the walk-run-jump bug fix for responsive jumping to be optional (Gameplay settings → Fixes → Fix walk run jump) (#3389) - changed the enhanced look option to allow choosing between original TR1, original TR2 or unrestricted modes (Gameplay settings → Controls → Look mode) (#3403) - changed `/secret give` and `/secret take` to give or take all valid secrets when no index is specified - removed config tool (we have ingame setting dialogs now) - removed the "Enable numeric keys" option (it was added when these keys were not changeable) - fixed several more OG texture issues (refer to README for details) (#3352) - fixed Lara not saying 'no' near receptacles if she doesn't carry any items (#3337, regression from 4.0) - fixed Lara not saying 'no' near complete receptacles (#3337, regression from 4.0) - fixed key items getting consumed at the start of the interaction with receptacles (#3399) - fixed certain commands (such as `/load` or `/play`) not working as expected while in the key use inventory screen (#3338) - fixed the camera resetting if Lara is looking and then draws her guns (OG behaviour retained when using restricted look mode) (#3406) - fixed game window getting misplaced in windowed mode between game relaunches on certain systems (#3418) - fixed Lara using the wrong hit animation under certain scenarios based on her hit angle (#3424) - fixed already playing samples not getting muted when the game window goes out of focus - fixed the `/play` command starting the level with wrong items sometimes (#3147, regression from 4.11) - fixed the `/tp` command breaking the photo mode - fixed the `/tp` command misbehaving when giving fractional coordinates - fixed Compass label in Gym not appearing when holding arrows during inventory spin-out (#3460) ## [4.12.3](https://github.com/LostArtefacts/TRX/compare/tr1-4.12.2...tr1-4.12.3) - 2025-06-24 - fixed game crashing when the expected resources are missing (#3310, regression from 4.12.2) - fixed restore default pop-up requiring all 3 water color options to be adjusted instead of just one (#3314, regression from 4.12) - fixed pause screen rendered without background overlay if fade effects are disabled (#3316, regression from 4.11) - fixed `/pos` command crashing when the level title is not set (regression from 4.12) ## [4.12.2](https://github.com/LostArtefacts/TRX/compare/tr1-4.12.1...tr1-4.12.2) - 2025-06-22 - fixed depth buffer problems when closing the inventory ring with fade effects disabled (#3267, regression from 4.8) - fixed Lara's braid not being reflective (on Midas' hand) (#3257, regression from 4.9) - fixed turbo cheat causing audio desync in cutscenes (#3263) - fixed support for non-linear secret flags in custom levels (#3262, regression from 4.12) - fixed movable blocks getting stuck in midair if the game is saved and loaded while they are falling (#3274) - fixed PS touchpad input missing an icon (#3288, regression from 4.12) - fixed inability to use unbind key / reset layout buttons with controllers (#3290, regression from 4.12) - fixed inventory ring consuming too many items under severe frame drop conditions (#3295, regression from 4.8) - fixed screenshots stripping accented characters (#3238) - fixed accented lowercase `i` characters retaining the superscript dot (#3298) - reverted the partial fix for wrong audio device reinitialization (#3251, regression from 4.12) ## [4.12.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.12...tr1-4.12.1) - 2025-06-18 - fixed certain secrets not registering (#3252, regression from 4.12) ## [4.12](https://github.com/LostArtefacts/TRX/compare/tr1-4.11.2...tr1-4.12) - 2025-06-17 Showcase: https://www.youtube.com/watch?v=IqjVuXTVI4A - added builtin support for ingame string translations - changed duplicate game strings between TR1 and TR2 to be placed in a single file TRX_common_strings.json5 - added a new setting, `enable_review_markers`, which display which text requires review (only available via `/set`) - added Italian translation - added Polish translation - added support for non-breaking spaces - fixed game crashing when trying to word-wrap unknown characters - added UI for all config tool settings - added ingame help for all settings - added support for object name aliases; added aliases for dev commands - added an optional breeze effect for Lara's braid in appropriate outside rooms (#3090) - added keyboard and controller input icons to the controls settings dialog - added an option to adjust music and ambient volume while in inventory - added a `/secret` console command for easier debugging of secrets - added `enable_debug_pos` setting that shows Lara's position in realtime (reachable via `/debug`, fine-tuned `/set`) - added an option to control whether or not Lara responds to hitting a wall while wading (#3138) - added an option for smooth wall deflection when Lara comes to a stop at a wall, similar to TR2 (#3148) - added an option to fix the step glitch where Lara can be pushed into walls (#3148) - added an option to have Lara always roll off one-click steps rather than boosting forward (#3149) - added an option to toggle allowing Lara to exit from water horizontally, below, or climbing out onto non-standable slopes (#3154) - added an option to toggle random enemy initial angle adjustment (#3129) - improved the teleport cheat if used when Lara is in a special animation, such as grabbing the Scion - improved the dev console commands documentation - changed the maximum number of 2D static mesh slots (room sprites) from 50 to 256 (#3200) - changed the wall glitch config option to a selection of being fixed, using TR1 behaviour or TR2 behaviour (#3153) - changed sound and music volumes to be displayed as percentage instead of 0-10 - changed the graphic settings dialog to use tabs - changed the setting dialogs to respect the UI wraparound setting - changed the `/tp` command to align Lara to switches and pickups - changed the `/set` command to accept `-`, which will restore the given setting to its default state - changed the music track slot limit from 64 to 1024 (#3101) - changed text kerning to a smaller value - changed the underwater music volume setting to separate ambient and music volume sliders - changed logs format to include timestamps - fixed a game crash in custom levels if centaur statues exploded without having centaur objects in the level file (#3155) - fixed being unable to re-purpose some gym music tracks in custom levels (#3164) - fixed Lara not catching fire after reloading a save made when she was on fire and enhanced saves are disabled (applies to new saves only) (#3157) - fixed 3D pickups misplacing or hiding UI elements with render mode set to window size and the game windowed (#3067, regression from 4.10) - fixed the `/play` command crashing when the game has only ATI saves (#3137, regression from 4.10) - fixed the `/play` command taking resume information from the highlighted slot (#3137, regression from 4.10) - fixed text glyphs having cut off right and bottom borders (regression from 4.7) - fixed unbind key option being available when it shouldn't (#3111, regression from 4.11) - fixed not saving screen resolution (regression from 4.11) - fixed vertical FOV option not working properly (#3120, regression from 4.10) - fixed Lara's position on a ledge after grabbing it extremely late (#3132, regression from 2.2.1) - fixed a rare crash when editing certain dev console history entries (#2913, regression from 4.10) - fixed a desync in the Vilcabamba demo if the wall glitch fix option was enabled (#3172, regression from 1.3) - fixed demos being affected if Lara's starting HP has been altered (#3180, regression from 2.6) - fixed Lara's health bar showing at the start of cutscenes (#3182, regression from 4.11) - fixed broken playback of mono music tracks (regression from 2.0) - fixed hot-plugging certain audio devices causing glitchy playback (partial fix; regression from 2.0) - fixed being unable to toggle fullscreen mode during FMV sequences (#3188, regression from 4.6) - fixed examine hint text lingering on screen when moving to a different item in the inventory (#3228, regression from 4.8) - fixed controls settings dialog missing trapezoid filter option (#3246, regression from 4.9) - fixed logging not outputting anything on Windows terminals ## [4.11.2](https://github.com/LostArtefacts/TRX/compare/tr1-4.11.1...tr1-4.11.2) - 2025-05-24 - improved word wrapping algorithm in the dev console - changed examine item descriptions to remove extra blank lines - fixed examine item overlapping with other UI elements at large text scales - fixed a crash related to carried items if using saves made prior to 4.11 (#3052, regression from 4.11) ## [4.11.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.11...tr1-4.11.1) - 2025-05-23 - fixed "Load Game" bottom text arrows jumping when entering the load game dialog (regression from 4.11) - fixed missing arrows around focused navigation elements in the controls dialog (#3042, regression from 4.11) - fixed text outline being a bit laggy when rebinding the inputs in the controls dialog - fixed crashes in the save dialog on Linux (#3046, regression from 4.11) ## [4.11](https://github.com/LostArtefacts/TRX/compare/tr1-4.10.2...tr1-4.11) - 2025-05-21 Showcase: https://www.youtube.com/watch?v=JVtcZoNoeRM - added the ability to trigger a flip effect without having to also trigger the flip map, in line with TR2 (#2921) - added a /help command (#2917) - added an option to toggle between TR1 and TR2 camera modes (#2990) - added the ability to trigger different ambient tracks in custom levels, which will loop and be remembered between saves (#811) - changed the all items cheat to include the lead bar if present in the level (#3008) - changed the design of the controls dialog to use pages, making it match the new TR2X controls dialog - changed the pause screen to have a darker black overlay transparency (#2252) - fixed Lara's braid pointing straight down when swimming below sloped ceilings (#1600) - fixed enemy hitpoints being doubled in demo mode as a result of NG+ (#2904) - fixed an illegal reachable slope in Lost Valley room 58, which could lead to Lara becoming softlocked (#2900) - fixed some pickup sprites being too far embedded into the floor (#2903) - fixed vase room sprites in Return to Egypt and Temple of the Cat being embedded in the floor (#2095) - fixed the camera behaving erratically in rooms/sectors that have no pathfinding data (#2946) - fixed the game crashing when editing long dev console history entries (#2913, regression from 4.10) - fixed FPS counter turning off after a game relaunch (#2911) - fixed falling ceiling and Damocles Sword traps not falling through stacked rooms (#2924) - fixed health bar in top center position covering inventory text - fixed the save crystal animation skipping a frame in 60 FPS (#1528) - fixed Lara unable to equip pistols after getting the Shotgun wet while wading (#2994) - fixed select level dialog not reacting to the menu back key (#2918, regression from 4.9) - fixed carried items falling from flying enemies not animating in 60 FPS (#2954, regression from 4.0) - fixed items carried by the Qualopec mummy spawning early after save/load (#2956, regression from 4.6) - fixed potential memory corruption if `/kill all` is used with a Qualopec mummy that is carrying items (#2957, regression from 4.6) - fixed a crash when portal debugging is enabled in rooms that have no portals (#2968, regression from 4.8) - fixed rats/voles and crocodiles/alligators at times not assuming the correct death pose after reloading a save (#2960, regression from 0.12) - fixed incorrect camera shifts when some fixed cameras return to normal view (#2971, regression from 4.9) - fixed Lara not having weapons when playing a level with -l/--level (#2995, regression from 4.9) - fixed inventory ring items not being animated when the ring is rotating (#2964, regression from 4.9) - fixed a hole appearing in the floor in Natla's Mines room 84 after exploding the TNT box (#3007, regression from 4.9) - fixed button mashing causing quick save/load to misbehave on a specific passport animation frame (#3021, regression from 4.10) - fixed save level numbers being replaced incorrectly if Lara dies in a level and the last save was in the previous level (#3026, regression from 4.9) - fixed ambient music not looping correctly after reloading a save with the option to reload ambient timestamps enabled (#3032, regression from 4.6) ## [4.10.2](https://github.com/LostArtefacts/TRX/compare/tr1-4.10.1...tr1-4.10.2) - 2025-05-15 - fixed animated textures not working the right way in flipped rooms (#2966, regression from 4.10) - fixed the final statistics always showing zero deaths regardless of the actual total (#2965, regression from 4.10) ## [4.10.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.10...tr1-4.10.1) - 2025-04-30 - fixed water caustics appearance (#2896, regression from 4.10) ## [4.10](https://github.com/LostArtefacts/TRX/compare/tr1-4.9...tr1-4.10) - 2025-04-30 Showcase: https://www.youtube.com/watch?v=qJPq9obD6Cc - added an ability to customize the fog distances (#634) - added an ability to customize the water color [see the reference](../WATER_COLORS.md) (#1532) - added support for a hex water color notation (eg. `#80FFFF`) in the game flow file - added support for antitriggers, like TR2+ (#2580) - added support for aspect ratio-specific images (#1840) - added an option to wraparound when scrolling UI dialogs, such as save/load (#2834) - added aliases to CLI options (`-gold` becomes `-g/--gold`, `-demo_pc` becomes `--demo-pc`) - improved bubble appearance (#2672) - improved rendering performance - improved pause exit dialog - it can now be canceled with escape - improved the `/set` console command to display available options if given an unknown argument - added a `--help` CLI option (may not output anything on Windows machines – OS bug) - changed the `draw_distance_fade` and `draw_distance_max` to `fog_start` and `fog_end` - changed `Select Detail` dialog title to `Graphic Options` - changed the number of static mesh slots from 50 to 256 (#2734) - changed the "enable EIDOS logo" option to disable the Core Design and Bink Video Codec FMVs as well; renamed to "enable legal" (#2741) - changed sprite pickups to respect the water tint if placed underwater (#2673) - changed save to take priority over load when both inputs are held on the same frame, in line with OG (#2833) - changed the sound dialog appearance (repositioned and added text labels) - changed The Unfinished Business strings to default to the OG strings file for the main tables (#2847) - changed the dev console to no longer add duplicate entries to the history - removed the pretty pixels options (it's now always enabled, #2258) - fixed the bilinear filter to not readjust the UVs (#2258) - fixed disabling the cutscenes causing the game to exit (#2743, regression from 4.8) - fixed anisotropy filter causing black lines on certain GPUs (#902) - fixed mesh faces not being drawn under some circumstances (#2452, #2438) - fixed objects disappearing too early around screen edges (#2005) - fixed the trapezoid filter being toggled if Alt-F4 (either left or right) is used to close the game (#2690) - fixed enemies in one-click high water appearing with a water tint, and not making any animation sounds (#2753) - fixed the scale of the four keys in St. Francis' Folly (#2652) - fixed the panther at times not making a sound when it dies, and restored Skate Kid's death SFX (#2647) - fixed pushblocks being rotated when Lara grabs them, most noticeable if asymmetric textures have been used (#2776) - fixed Lara becoming clamped if she picks up an item under a steeply sloped ceiling (#2879) - fixed a crash when 3D pickups are disabled and Lara crosses a trigger to look at a pickup item (#2711, regression from 4.8) - fixed trapezoid filter warping on faces close to the camera (#2629, regression from 4.9) - fixed Mac builds crashing upon start (regression from 4.9) - fixed sprites rendering black if no shade value is assigned in the level (#2701, regression from 4.9) - fixed being stuck on the Restart Level page if using save crystals and F5 is pressed when no saves are present (#2700, regression from 4.8.2) - fixed being stuck on the Exit to Title page if using save crystals and a new save is made when there were previously none, and then F5 is pressed (#2700, regression from 4.9) - fixed the sprite UVs to restore the right and bottom edge pixels (#2672, regression from 4.8) - fixed sprites missing the fog effect (regression from 4.9) - fixed the camera going out of bounds in 60fps near specific invalid floor data (known as no-space) (#2764, regression from 4.9) - fixed wrong PS1-style title bar color for the end of the level stats dialog (regression from 4.9) - fixed Story So Far showing up even when there's nothing to play (#2611, regression from 2.10) - fixed Story So Far not playing the opening FMV, `cafe.rpl` (#2779, regression from 2.10) - fixed Lara at times ending up in incorrect rooms when using the teleport cheat (#2486, regression from 3.0) - fixed the `/pos` console command reporting the base room number when Lara is actually in a flipped room (#2487, regression from 3.0) - fixed clicks in audio sounds (#2846, regression from 2.0) - fixed Lara being killed if she enters the void in a level that uses the `disable_floor` sequence in the game flow (#2874, regression from 4.9) - fixed game crashing if the music folder was not present (#2887, regression from 4.9) - fixed the camera jumping if going from a look at trigger to a fixed camera (#3033, regression from 4.8) - fixed game crashing on unknown sequencer events ## [4.9](https://github.com/LostArtefacts/TRX/compare/tr1-4.8.3...tr1-4.9) - 2025-03-31 Showcase: https://www.youtube.com/watch?v=AYVpnsYQNno - added quadrilateral interpolation (#354) - added `/flood` and `/drain` console commands - added support for `-l`/`--level` argument to play a single level - added support for `-s`/`--save` argument to immediately start a saved game - added support for custom levels to use `disable_floor` in the gameflow, similar to TR2's Floating Islands (#2541) - added drawing of object mesh spheres to the `/debug` console command - added TR2+ stats if the full stat detail mode option is enabled (#2561): - ammo hits / used - health packs used - distance travelled - added a TR2+ style bordered stat box to the end of level stats if the full stat detail mode option is enabled (#2658) - changed the Controls screen to hide the reset and unbind texts when changing a key (#2103) - changed injections to a new file format with a smaller footprint and improved applicability tests (#1967) - changed the `/pos` command to show `Demo` and `Cutscene` instead of `Level` when relevant - changed the `/pos` command to show demo and cutscene numbers starting at 1, in line with `/play` - changed the `/play` and `/pos` commands to always treat the gym level as the level 0 – even if it's not included - changed sprites to respect the water tint if placed underwater (#2093) - changed the optional `Deaths` stat to be placed last in the stats menu - fixed delays when scanning available save games (#2610, #1335, regression from <3.0) - fixed several instances of the camera going out of bounds (#1034) - fixed issues with stacked, floating and flipmap pushblocks in custom levels - fixed issues with fixed cameras in 60 FPS shifting before settling on their target (#1186) - fixed missiles from mutants/centaurs/Natla jittering in 60 FPS (#1314) - fixed the bear AI fix option being applied in the Vilcabamba demo (#2559, regression from 4.8) - fixed extremely large item quantities crashing the game (#2497, regression from 0.3) - fixed Lara's meshes not resetting after using the fly cheat (#2565, #2572, regressions from 4.8) - fixed the select level feature not giving Lara her items (#2617, regression from 4.8) - fixed guns appearing in Lara's hands if the draw input is held when unarmed and while picking up a gun item (#2577, regressions from 0.8/4.3) - fixed being able to play with Lara invisible after using the explosion cheat then the fly cheat (#2584, regression from 4.8) - fixed the `/pos` command not working in cutscenes - fixed the `/pos` command not showing demo and cutscene titles - fixed the embedded bats fix causing problems inside rooms with trapdoors (regression from 4.6) - fixed cutscene music looping (#2591, regression from 4.8) - fixed saves created before version 2.15 causing a crash on load (#2654, regression from 4.8) - fixed the console opening when remapping its key (#2641) - removed perspective filter toggle (it had no effect; repurposed to trapezoid interpolation toggle) - improved camera mode navigation: - improved support for pivoting - improved roll support - expanded world bounding box by 5 tiles in each direction - added support for 60 FPS ## [4.8.3](https://github.com/LostArtefacts/TRX/compare/tr1-4.8.2...tr1-4.8.3) - 2025-02-17 - fixed some of Lara's speech in the gym not playing in response to player action (#2514, regression from 4.8) - fixed passport text disappearing too quickly (#2512, regression from 4.8.2) - fixed inability to navigate to Select Level menu (#2518, regression from 4.8.2) - fixed NG+ flag causing problems with loading non-NG+ savegames (#2515, regression from 2.8) ## [4.8.2](https://github.com/LostArtefacts/TRX/compare/tr1-4.8.1...tr1-4.8.2) - 2025-02-15 - improved memory usage by shedding ca. 100-110 MB on average - changed default FPS value to 60 (#2501) - changed passport to be more responsive to player inputs (#1328) - fixed Story So Far not skipping over levels (#2506, regression from 4.8) - fixed resolving paths (especially to music files) on case-sensitive filesystems (#1934, #2504) ## [4.8.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.8...tr1-4.8.1) - 2025-02-14 - fixed loading non-Caves saves triggering a new save prompt when save crystals are enabled (#2498, regression from 4.8) ## [4.8](https://github.com/LostArtefacts/TRX/compare/tr1-4.7.1...tr1-4.8) - 2025-02-14 Showcase: https://www.youtube.com/watch?v=td2Qz3nbRVo >[!WARNING] >Attention level builders: this version introduces backwards incompatible changes to the game flow file. >Please refer to the following documents to see how to update your levels: >- [Migration guide](../trx/MIGRATING.md) >- [Game flow documentation](../trx/game_flow/) >- [Game strings documentation](../trx/GAME_STRINGS.md) - added the ability to hold left/right to move through menus more quickly (#2298) - added an option for pickup aids, which will show an intermittent twinkle when Lara is nearby pickup items (#2076) - added an optional demo number argument to the `/demo` command - added pause screen support to demos - added a fade-out effect when exiting the pause screen to the inventory - added exit fade-out effects (#2348) - added optional dynamic lighting for gun flashes and explosions, similar to TR2+ (#2357) - added a `/cut` (alias: `/cutscene`) console command for playing cutscenes - added a `/gym` (alias: `/home`) console command for playing Lara's Home - added a `/music` console command that plays a specific music track - added a `/debug` console command that shows all triggers and portals - added a console log when using the `/demo` command - improved pause screen compatibility with PS1 (#2248) - improved level loading times with respect to injection processing - improved wireframe mode appearance around screen edges - ⚠️ changed the game data to use a separate strings file for text information, removing it from the game flow file - ⚠️ changed the game flow file internal structure - changed the object texture limit from 2048 to unlimited (within game's overall memory cap) - changed the sprite texture limit from 512 to unlimited (within game's overall memory cap) - changed demo to be interrupted only by esc or action keys - changed the turbo cheat to also affect ingame timer (#2167) - changed the pause screen to wait before yielding control during fade out effect - changed the compass and final stats to use two columns, similar to TR2 (doesn't apply to end-of-level "bare" stats) - changed the fix for transparent eyes on wolves to use black instead of off-white (#2252) - changed the `/kill` command with no arguments to look for enemies within 5 tiles (#2297) - fixed blood spawning on Lara from gunshots using incorrect positioning data (#2253) - fixed ghost meshes appearing near statics in custom levels (#2310) - fixed photo mode switching to the wrong flipmap rooms at times (#2362) - fixed the teleporting command sometimes putting Lara in invalid flipmap rooms (#2370) - fixed teleporting to an item on a ledge sometimes pushing Lara to the room below (#2372) - fixed secret and enemy speech not playing if the sound effects are missing from the level file (#2458) - fixed being unable to load a level that contains no sound effect data (#2460) - fixed input controller remaps not being saved across game relaunches (#2422, regression from 4.6) - fixed the upside-down camera fix to no longer limit Lara's vision (#2276, regression from 4.2) - fixed being unable to load some old custom levels that contain certain (invalid) floor data (#2114, regression from 4.3) - fixed a desync in the Lost Valley demo if responsive swim cancellation was enabled (#2113, regression from 4.6) - fixed the game hanging when Lara is on fire and enters the fly cheat on the same frame as reaching water (#2116, regression from 0.8) - fixed Lara activating triggers one frame too early (#2208, regression from 4.3) - fixed wrong underwater caustics speed with the turbo cheat (#2231) - fixed 1-frame UI flicker on pause screen exit confirmation - fixed the game crashing if a cinematic is triggered but the level contains no cinematic frames (#2413) - fixed missing ricochet sprites in the gym (#2462) - fixed being able to use keys and puzzle items in keyholes/slots that have already been used (#2256, regression from 4.0) - fixed textures animating during demo fade-outs (#2217, regression from 4.0) - fixed waterfall mist not animating during demo (#2218, regression from 3.0) - fixed sound option arrows disappearing with specific volumes chosen (#2295, regression from 2.7) - fixed wireframe mode discarding transparent pixels (#2315, regression from 4.2) - fixed sprite pickup not being paused in the pause/inventory screen (#2319, regression from 4.1) - fixed 3D pickups not being paused in the pause/inventory screen (#2319, regression from 2.16) - fixed incorrect sprite sequences potentially animating after visiting a level with valid animating sprites (#2309, regression from 4.0) - fixed `/kill all` command destroying Scion and causing a soft lock in The Great Pyramid (#2329, regression from 4.4) - fixed health bar continuing to show when the inventory ring rotates (#1991, regression from 4.0) - fixed header and arrows disappearing when the inventory ring rotates (#2352, regression from 4.4) - fixed Story So Far feature not playing opening FMVs from the current level (#2360, regression from 4.2) - fixed `/play` command crashing the game when used after loading a level (#2411, regression) - fixed various death counter problems (existing saves will have the count reset) (#2264/#2412, regression from 2.6) - fixed `/demo` command crashing the game if no demos are present (regression from 4.1) - fixed Lara not being able to jump or stop swimming if the related responsive config options are enabled, but enhanced animations are not present (#2397, regression from 4.6) - fixed Lara not being able to jump if responsive jumping is disabled via the console in-level in certain scenarios (#2444, regression from 4.6) - fixed Lara being unable to climb or use guns after using an underwater lever and then entering the wading state (#2416, regression from 4.6) - fixed Eidos logo briefly flashing prior to the initial fade-in effect (#1388, regression from 4.1) - fixed Lara's meshes being incorrectly swapped in various scenarios using the fly cheat (#2461, regression from 4.7) ## [4.7.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.7...tr1-4.7.1) - 2024-12-21 - changed the inventory examine UI to auto-hide if the item description is empty (#2097) - fixed falling pickup items not being drawn when they land in rare cases (#2088) - fixed unbinding keys not working for controllers (#2090, regression from 4.6) - fixed hiding game UI causing the reset progressbar UI element to not show (regression from 4.7) ## [4.7](https://github.com/LostArtefacts/TRX/compare/tr1-4.6.1...tr1-4.7) - 2024-12-20 Showcase: https://www.youtube.com/watch?v=ThXt0I2j_QI - added support for Wayland in binary Linux builds (#1927) - added support for Unicode in gameflow JSON (#386, #636, #1919 and #1928) Expanding on the 4.6's added support for named sequences, we now support most of the characters the following Unicode planes: - Basic Latin - Cyrillic - Greek and Coptic - Latin-1 Supplement - Latin Extended A The sprites were created by Arsunt originally posted in the TRF topic here: https://www.tombraiderforums.com/showthread.php?p=8396039 This should be enough to let gameflow editors provide full localisation for the following languages: Basque, Belarusian, Bosnian, Bulgarian, Catalan, Croatian, Czech, Danish, Dutch, English, Estonian, Faroese, Finnish, French, Galician, German, Greek, Hungarian, Icelandic, Indonesian, Irish, Italian, Latvian, Lithuanian, Macedonian, Malay, Maltese, Northern Sami, Norwegian, Polish, Portuguese, Romanian, Russian, Serbian, Slovak, Slovenian, Spanish, Swedish, Turkish and possibly more. Importantly, Asian and Arabic languages remain unsupported at the moment. - added the ability for falling pushblocks to kill Lara outright if one lands directly on her (#2035) - fixed clock drift accumulating with time (#1935, regression from 4.0) - fixed a potential invisible wall issue in custom levels with non-portal doors and certain geometry (#1958, regression from 4.3) - fixed transparent eyes on the wolf and bat models in Peru (#1945) - fixed incorrect transparent pixels on some Egypt textures (#1975) - fixed arrows overlapping with passport text if strings longer than the defaults are used (#1971) - fixed objects close to the camera being clipped (#819, regression from TombATI) - fixed the drawbridge in Obelisk of Khamoon not being angled correctly when open, which was resulting in embedded artefacts (#2006) - fixed incorrect positions on static meshes in Obelisk of Khamoon, Return to Egypt and Temple of the Cat (#2006) - fixed incorrect picture strides on certain hardware (#1979) - fixed doors at times disappearing if Lara is close to portals and the door's room is no longer visible (#2005) - fixed camera positions in Return to Egypt and Temple of the Cat (#1317, regression from 4.1) - fixed being able to see the flipmap in Natla's Mines when moving the boat (#2019) - fixed an invisible wall in Temple of the Cat, due to a wrongly positioned door (#2021) - fixed the `enable_console` config option not being adhered to (#2063, regression from 4.5) - fixed random pixels in the injected explosion sprites (#1985, regression from 4.6) ## [4.6.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.6...tr1-4.6.1) - 2024-11-25 - added ability to disable saves completely by setting the save slot to 0 (#1954) - fixed invisible walls being present in front of some doors (#1948, regression from 4.6) - fixed big font scale causing text overlaps in the graphics options (#1946) - fixed missing FMVs causing the game to go silent (#1931, regression from 4.6) - fixed game crashing when toggling the bilinear filter in passport (#1942, regression from 4.5) - fixed game crashing when changing the save slot with `/set` when in passport (#1954, regression from 4.2) ## [4.6](https://github.com/LostArtefacts/TRX/compare/tr1-4.5.1...tr1-4.6) - 2024-11-18 Showcase: https://www.youtube.com/watch?v=raSzSAu7kLI - added support for wading, similar to TR2+ (#1537) - added the ability to pause during cutscenes (#1673) - added an option to enable responsive swim cancellation, similar to TR2+ (#1004) - added a special target, "pickup", to item-based console commands - added support for custom levels to enforce values for any config setting (#1846) - added support for key/puzzle/pickup descriptions, allowing players to examine said items in the inventory (#1821) - added an option to fix inventory item usage duplication (#1586) - added optional automatic key/puzzle inventory item pre-selection (#1884) - added a search feature to the config tool (#1889) - improved enemy item drops by supporting the TR2+ approach of having drops defined in level data (#1713) - improved Italian localization for the Config Tool - improved the injection approach for Lara's responsive jumping (#1823) - improved the exploding Lara input cheat to always use explosion sprites - changed OpenGL backend to use version 3.3, with fallback to 2.1 if initialization fails (#1738) - changed text backend to accept named sequences. Currently supported sequences (limited by the sprites available in OG): - `\{umlaut}` - `\{hat}` - `\{acute accent}` - `\{grave accent}` - `\{arrow up}` - `\{arrow down}` - `\{small digit 0}` - `\{small digit 1}` - `\{small digit 2}` - `\{small digit 3}` - `\{small digit 4}` - `\{small digit 5}` - `\{small digit 6}` - `\{small digit 7}` - `\{small digit 8}` - `\{small digit 9}` - `\{button empty}` - `\{button triangle}` - `\{button circle}` - `\{button x}` - `\{button square}` - `\{button l1}` - `\{button r1}` - `\{button l2}` - `\{button r2}` - `\{button down}` - `\{button up}` - `\{button left}` - `\{button right}` - `\{icon sound}` - `\{icon music}` - `\{ammo shotgun}` - `\{ammo magnums}` - `\{ammo uzis}` - changed the `/pos` command to include the level number and title - changed the `/tp` command to teleport to items in a round-robin fashion The first call will teleport Lara to the object that's the closest to her; repeated calls will cycle through all matching objects in the object placement order. - changed the music timestamp loading behaviour and config option to support ambient tracks (#1769) - removed health cheat (we now have the `/hp` command) - removed background for the "Reset" and "Unbind" labels in the controls dialog - removed `force_game_modes` and `force_save_crystals` from the gameflow - see GAMEFLOW.md for details on how to enforce these settings (#1857) - fixed a crash relating to audio decoding (#1895) - fixed missing pushblock SFX in Natla's Mines (#1714) - fixed crash reports not working in certain circumstances (#1738) - fixed missing trapdoor triggers in City of Khamoon (#1744) - fixed being unable to rename the lead bar (#1774, regression from 4.5) - fixed the controls menu extending to the bottom of the screen with certain text scaling values (#1783, regression from 2.12) - fixed game stuck at remapping controller key if no controllers connected (#1788) - fixed being able to shoot the scion multiple times if save/load is used while it blows up (#1819) - fixed certain erroneous `/play` invocations resulting in duplicated error messages - fixed the `/play` console command resulting in Lara starting the target level without pistols (#1861, regression from 4.5) - fixed the demo mode text overlapping with the enemy health bar if the health bar is located in the bottom centered (#1446) - fixed mutant explosions sometimes heavily damaging Lara even if they missed (#1758, regression since 4.5) - fixed wrongly calculated trapdoor size that could affect custom levels (#1904) ## [4.5.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.5...tr1-4.5.1) - 2024-10-14 - fixed mac builds missing embedded resources (#1710, regression from 4.5) ## [4.5](https://github.com/LostArtefacts/TRX/compare/tr1-4.4...tr1-4.5) - 2024-10-08 Showcase: https://www.youtube.com/watch?v=eMnVYbB4QBc - added a photo mode feature (#1669) - added `/sfx` command - added `/nextlevel` alias to `/endlevel` console command - added `/quit` alias to `/exit` console command - added an option to toggle the in-game UI, such as healthbars and ammo text (#1656) - added the ability to cycle through console prompt history (#1571) - added Lara's exit-water-to-medium-height animation from TR2+ (#1538) - improved object name matching in console commands to work like TR2X - improved vertex movement when looking through water portals even more (#1493) - improved console commands targeting creatures and pickups (#1667) - changed the easter egg console command to pack more punch - changed `/set` console command to do fuzzy matching (LostArtefacts/libtrx#38) - removed dedicated camera reset button in favor of pressing the look button (#1658) - fixed console caret position off by a couple of pixels (regression from 3.0) - fixed holding a key when closing the console registering as a game input (regression from 3.0) - fixed ability to crash the game with extreme FOV values (regression from 0.9) - fixed double "Fly mode enabled" message when using `/fly` console command (regression from 4.0) - fixed crash in the `/set` console command (regression from 4.4) - fixed toggling fullscreen not always saving (regression from 4.4) - fixed altering fov with `/set` not being immediately respected (#1547) - fixed main menu music volume when exiting while underwater with certain music settings (#1540, regression from 4.4) - fixed `/kill` command unable to target a special object - fixed really fast typing in console sometimes losing the first input (regression from 4.4) - fixed Lara's head not matching the braid if in use when she is killed by the T-rex (#1549) - fixed `/endlevel` displaying a success message in the title screen - fixed Story So Far feature looping cutscenes forever (#1551, regression from 4.4) - fixed a rare crash related to the camera that could affect custom levels (#1671) - fixed a bug when saving and loading when picking up an item or using a switch with animated interactions enabled (#1546) - fixed a bug where Lara was stuck for a long time in an animated interactions if pushed (#1687) ## [4.4](https://github.com/LostArtefacts/TRX/compare/tr1-4.3...tr1-4.4) - 2024-09-20 Showcase: https://www.youtube.com/watch?v=3XOSl9WqH3A - added `/exit` command (#1462) - added reflections to Midas Hand death animation and savegame crystals (#154) - added an option to use PS1 tinted savegame crystals (#1506) - improved appearance of textures around edges when bilinear filter is off (#1483) Since this removes the seams on pushblocks, this was made optional. - improved level load times (#1456, #1457) - improved logs module names readability - improved crash debug information on Windows - improved vertex movement when looking through water portals (#1493) - improved anisotropic filter rendering (#902, #1507) - improved skybox appearance (#1520) - fixed `/play`, `/load`, `/demo` and similar commands not working in stats, credits, cinematics and fmvs (#1477) - fixed console commands being able to interfere with demos, cutscenes and the title screen (#1489, regression from 3.0) - fixed reopening the compass not resetting its needle (#1472, regression from 4.0) - fixed holstering pistols hiding the gun meshes 1 frame too early (#1449, regression from 0.6) - fixed Lara's sliding animation sometimes being interrupted by a stumble (#1452, regression from 4.3) - fixed cameras with glide values sometimes moving in the wrong direction (#1451, regression from 4.3) - fixed `/give` console command giving duplicate items under some circumstances (#1463, regression from 3.0) - fixed `/give` console command confusing logging around mismatched items (#1463, regression from 3.0) - fixed `/give` console command unable to give Scion in Tomb of Qualopec and Sanctuary (regression from 3.0) - fixed `/flip` console command misreporting an already enabled flipmap as off (regression from 4.0) - fixed `/kill` console command not fully killing enemies (#1482, regression from 3.0) - fixed `/tp` console command not always picking the closest item (#1486, regression from 4.1) - fixed `/tp` console command reporting teleport fails as success (#1484, regression from 4.1) - fixed `/tp` console command allowing teleporting to consumed savegame crystals (#1518) - fixed `/hp` console command taking arbitrary integers - fixed `/set` console command crashing with unknown targets (regression from 4.2) - fixed `/set` console command not sanitizing numeric values (#1515) - fixed console commands causing improper ring shutdown with selected inventory item (#1460, regression from 3.0) - fixed console input immediately ending demo (#1480, regression from 4.1) - fixed a potential softlock when killing the Torso boss in Great Pyramid (#1236) - fixed Bacon Lara re-spawning after saving and loading (#1500, regression from 0.7) - fixed config JSON not sanitizing some numeric values (#1515) - fixed potential crashes in custom levels if hybrid creature objects are not present in the level (#1444) - changed the target change functionality from the look key to a new, standalone key (default `z` on keyboard, `left analog click` on controller) (#1503) - changed `/heal` console command to also extinguish Lara - changed `/tp` console command to look for the closest place to teleport to when targeting items (#1484) - changed `/set` console command output to always use fully-qualified option names - changed `/fps`, `/vsync`, `/wireframe`, `/braid` and `/cheats` console commands output to be in line with `/set` console command output - changed the door cheat to also target drawbridges ## [4.3](https://github.com/LostArtefacts/TRX/compare/tr1-4.2...tr1-4.3) - 2024-08-15 Showcase: https://www.youtube.com/watch?v=kc2oo-pSMh0 - added deadly water feature from TR2+ for custom levels (#1404) - added skybox support, with a default option provided for Lost Valley, Colosseum and Obelisk of Khamoon (#94) - added an option for Lara to use her underwater swimming physics from TR2+ (#1003) - added weapons to Lara's empty holsters on pickup (#1291) - added options to quiet or mute music while underwater (#528) - improved initial level load time by lazy-loading audio samples (LostArtefacts/TR2X#114) - changed the turbo cheat to no longer affect the gameplay time (#1420) - changed weapon pickup behavior when unarmed to set any weapon as the default weapon, not just pistols (#1443) - fixed adjacent Midas Touch objects potentially allowing gold bar duplication in custom levels (#1415) - fixed the excessive pitch and playback speed correction for music files with sampling rate other than 44100 Hz (#1417, regression from 2.0) - fixed the ingame timer being skewed upon inventory open (#1420, regression from 4.1) - fixed Lara able to reach triggers through closed doors (#1419, regression from 1.1.4) - fixed Lara voiding when loading the game on a closed door (#1419) - fixed underwater caustics not resumed smoothly when unpausing (#1423, regression from 3.2) - fixed collision issues with drawbridges, trapdoors, and bridges when stacked over each other, over slopes, and near the ground (#606) - fixed an issue with a missing Spanish config tool translation for the target mode (#1439) - fixed carrying over unexpected guns in holsters to the next level under rare scenarios (#1437, regression from 2.4) - fixed item cheats not updating Lara holster and backpack meshes (#1437) ## [4.2](https://github.com/LostArtefacts/TRX/compare/tr1-4.1.2...tr1-4.2) - 2024-07-14 Showcase: https://www.youtube.com/watch?v=gV7oz0wEzWk - added creating minidump files on crashes - added new console commands: - `/hp` - `/hp [num]` - `/heal` - `/wireframe` - `/set` - added unobtainable secrets stat support in the gameflow (#1379) - added a wireframe mode - changed console caret blinking rate (#1377) - changed the TR1X install source in the installer to suggest using the existing installation directory (#1350) - fixed config tool and installer missing icons (#1358, regression from 4.0) - fixed looking forward too far causing an upside down camera frame (#1338) - fixed the enemy bear behavior in demo mode (#1370, regression from 2.16) - fixed the FPS counter overlapping the healthbar in demo mode (#1369) - fixed the Scion being extremely difficult to shoot with the shotgun (#1381) - fixed lightning rendering z-buffer issues (#1385, regression from 1.4) - fixed possible game crashes if more than 16 savegame slots are set (#1374) - fixed savegame slots higher than 64 not working (#1395) - fixed a crash in custom levels if a room had more than 1500 vertices (#1398) - fixed a potential crash or silence with certain music files (#794, regression from 2.0) - fixed the console command to change FPS also starting demo mode (#1368) - fixed text blinking rate being different in 30 and 60 FPS (#1377) - fixed slow sound volume adjustment at 60 FPS when holding arrow keys (#1407) ## [4.1.2](https://github.com/LostArtefacts/TRX/compare/tr1-4.1.1...tr1-4.1.2) - 2024-04-28 - fixed pictures display time (#1349, regression from 4.1) ## [4.1.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.1...tr1-4.1.1) - 2024-04-27 - fixed reading animated texture data in levels (#1346, regression from 4.1) ## [4.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.0.3...tr1-4.1) - 2024-04-26 Showcase: https://www.youtube.com/watch?v=ioo2P0FuFWU - added ability to show enemy healthbars only for bosses (#1300) - added ability to kill specific enemy types (#1313) - added ability to teleport to nearest specific object (#1312) - added `/load` and `/save` commands for even quicker savegame operations - added `/demo` command to quickly play the demo - added `/title` command to quickly exit to title - added `/vsync on` and `/vsync off` commands to toggle the VSync setting - added `/give all` variant of the item cheat - changed injection files to be placed in its own directory (#1306) - changed item cheat sound effects - changed the `/play` command to work immediately in the title screen - fixed turbo cheat speed setting not saved across game relaunches (#1320) - fixed turbo cheat behavior with the following game elements (#1341): - animated textures animation rate (regression from 4.0.3) - 3D pickups animation rate (regression from 4.0.3) - healthbar flashing rate - UI text flashing rate - inventory stats timer - underwater wibble effect rate - loading screen and credit images display time - title screen demo delay - fade times - fixed camera vibrations when using the teleport command in 60 FPS (#1274) - fixed the camera being thrown through doors for one frame when looked at from fixed camera positions (#954) - fixed console not retaining changed user settings across game relaunches (#1318) - fixed passport inventory item not being animated in 60 FPS (#1314) - fixed object explosions not being animated in 60 FPS (#1314) - fixed lava emitters not being animated in 60 FPS (#1314) - fixed underwater bubbles not being animated in 60 FPS (#1314) - fixed compass needle being too fast in 60 FPS (#1316, regression from 4.0) - fixed black screen flickers that can occur in 60 FPS (#1295) - fixed a slight delay with the passport menu selector (#1334) - decreased initial flicker upon game launch (#1322) ## [4.0.3](https://github.com/LostArtefacts/TRX/compare/tr1-4.0.2...tr1-4.0.3) - 2024-04-14 - fixed flickering sprite pickups (#1298) ## [4.0.2](https://github.com/LostArtefacts/TRX/compare/tr1-4.0.1...tr1-4.0.2) - 2024-04-11 - fixed Mac binaries not working on x86-64 (eg not Apple Silicon) - fixed building on Linux outside of the Docker toolchain (#1296, regression from 4.0) ## [4.0.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.0...tr1-4.0.1) - 2024-04-10 - fixed trying to pick up a lead bar crashing the game (#1293, regression from 4.0) ## [4.0](https://github.com/LostArtefacts/TRX/compare/tr1-3.1.1...tr1-4.0) - 2024-04-09 Showcase: https://www.youtube.com/watch?v=-ED8HSHdHHQ&t=63s - added experimental support for 60 FPS, available from the in-game graphics menu - added ability to slow the game down using the turbo cheat (#1215) - added /speed command to control the turbo cheat (#1215) - added the option to change weapon targets by tapping the look key like in TR4+ (#1145) - added three targeting options: full lock always keeps target lock (OG), semi lock loses target lock if the enemy dies, and no lock loses target lock if the enemy goes out of sight or dies (TR4+) (#1146) - added an option to the installer to install from a CD drive (#1144) - added stack traces to logs for better crash debugging (#1165) - added an option to use PS1 loading screens (#358) - added high quality images for the Eidos, Unfinished Business title, Unfinished Business credit, and final statistics screens - added support for macOS builds (for both Apple Silicon and Intel) - added optional support for OpenGL 3.3 Core Profile - added Italian localization to the config tool - added the ability to move the look camera while targeting an enemy in combat (#1187) - added the ability to skip fade-out in stats screens - added support for animated room sprites in custom levels and an option to animate plant sprites in The Cistern and Tomb of Tihocan (#449) - added on-screen messages for certain actions (#1220) - changed stats no longer disappear during fade-out (#1211) - changed the way music timestamps are internally handled – resets music position in existing saves - changed vertex and fragment shaders into unified files that are runtime pre-processed for OpenGL versions 2.1 or 3.3 - changed the `/kill` command to use Lara as a reference point, and kill all creatures that are within a single tile first (#1256) - changed the config not to save key mappings if they do not deviate from the current version's defaults (#1218) - changed the item cheat keybind to also work in Gym - changed the item cheat command to display a relevant message if Lara object is not loaded - fixed a missing translation for the Spanish config tool for the Eidos logo skip option (#1151) - fixed a flipmap issue in Natla's Mines that could make the cabin appear stacked and prevent normal gameplay (#1052) - fixed several texture issues across the majority of levels (#1231) - fixed broken gorilla animations (#1244, regression from 2.15.3) - fixed saving and loading the music timestamp when the load current music option is enabled and game sounds in inventory are disabled (#1237) - fixed the remember played music option always being enabled (#1249, regression from 2.16) - fixed the underwater SFX playing for one frame at the start of Palace Midas (#1251) - fixed an incorrect frame in Lara's underwater twist animation (OG bug in TR2 onwards) (#1242) - fixed Lara saying "no" when taking valid actions in front of a key item receptacle (#1268) - fixed Lara not saying "no" when using the Scion incorrectly (#1278) - fixed flickering in bats' death animations and rapid shooting if Lara continues to fire when they are killed (#992) - fixed an incorrect animation in the door used at the beginning of Colosseum (#1287) ## [3.1.1](https://github.com/LostArtefacts/TRX/compare/tr1-3.1...tr1-3.1.1) - 2024-01-19 - changed quick load to show empty passport instead of opening the save game menu when there are no saves (#1141) - fixed a game crash when the quick load passport is deselected (#1136, regression from 3.1) - fixed not being able to save in an empty slot using quick save if the load game menu was opened before (#1140, regression from 3.1) - fixed the passport briefly flashing inaccessible page text (#1137, regression from 3.1) ## [3.1](https://github.com/LostArtefacts/TRX/compare/tr1-3.0.5...tr1-3.1) - 2024-01-14 - added the option to use "shell(s)" to give shotgun ammo in the developer console (#1096) - added the restart level option to the passport in save crystal mode (#1099) - added the ability to back out of menus with the circle and triangle buttons when using a gamepad (cross acts as confirm) (#1104) - changed `force_enable_save_crystals` to `force_save_crystals` for custom level authors to force enable or disable the save crystals setting (#1102) - changed `force_disable_game_modes` to `force_game_modes` for custom level authors to force enable or disable the game modes setting (#1102) - changed the Scion in The Great Pyramid from spawning blood when hit to a ricochet effect if texture fixes enabled (#1121) - changed the gamepad control menu's 'reset all buttons' bind to held R1 (was held triangle) (#1104) - changed the number of visible enemies from 8 to 32 (#1122) - fixed FMVs always playing at 100% volume – now they'll play at the game sound volume (#1110) - fixed bugs when trying to stack multiple movable blocks (#1079) - fixed Lara's meshes being swapped in the gym level when using the console to give guns (#1092) - fixed Midas's touch having unrestricted vertical range (#1094) - fixed flames not being drawn when Lara is on fire but leaves the room where she caught fire (#1106) - fixed being able to deselect the passport in quick save, quick load, save crystal, and death modes (#1108) - fixed inability to save in Unfinished Business in crystals mode as UB doesn't have crystals (#1102) - fixed items not being added to inventory if the sprite is missing from the level file (#1130) - fixed differences when looking at items from triggers that do not use fixed cameras when the enhanced look option is enabled (#1026) ## [3.0.5](https://github.com/LostArtefacts/TRX/compare/tr1-3.0.4...tr1-3.0.5) - 2023-12-13 - fixed crash when pressing certain keys and the console is disabled (#1116, regression from 3.0) - fixed lightning bolts wrongly drawn (#1113, regression from 0.9) ## [3.0.4](https://github.com/LostArtefacts/TRX/compare/tr1-3.0.3...tr1-3.0.4) - 2023-12-08 - fixed missiles damaging Lara when she is far beyond their damage range (#1090) - fixed pushblocks moving freely if Lara releases but tries to regrab during the release animation (#1101, regression from 3.0) ## [3.0.3](https://github.com/LostArtefacts/TRX/compare/tr1-3.0.2...tr1-3.0.3) - 2023-11-27 - fixed underwater shadow effects rendering always in the same way rather than at random (#1081) ## [3.0.2](https://github.com/LostArtefacts/TRX/compare/tr1-3.0.1...tr1-3.0.2) - 2023-11-11 - fixed incorrect usage reference URLs in the gameflow files (#1073) - fixed random number generation becoming stuck after entering and leaving the inventory, which affected effects and SFX (#1070, #1074) ## [3.0.1](https://github.com/LostArtefacts/TRX/compare/tr1-3.0...tr1-3.0.1) - 2023-11-10 - fixed installer not detecting old Tomb1Main installations (#1071) ## [3.0](https://github.com/LostArtefacts/TRX/compare/tr1-2.16...tr1-3.0) - 2023-11-09 Showcase: https://www.youtube.com/watch?v=vqvOkZzHx6M - renamed the project from Tomb1Main to TR1X in an effort to establish our own unique identity, while respectfully disassociating from TR2Main. - added developer console (accessible with `/`, see [2-COMMANDS.md] for details) - added Linux builds and toolchain - added an option to allow Lara to roll while underwater, similar to TR2+ (#993) - added an option to turn off Eidos logo entirely through config (#1044) - added the bonus level type for custom levels that unlocks if all main game secrets are found (#645) - added detection for animation commands to play SFX on land, water or both (#999) - added support for customizable enemy item drops via the gameflow (#967) - added an option to enable F-key and inventory frame buffering (#591) - added a pickup overlay for the Midas gold bar when it changes from lead (#1010) - added an option to allow Lara to creep forwards or backwards further when performing neutral jumps, in line with TR2+ (#998) - added an option to the installer to choose between the original and fan-made Unfinished Business level sets (#1019) - fixed baddies dropping duplicate guns (only affects mods) (#1000) - fixed Lara never using the step back down right animation (#1014) - fixed dead crocodiles floating in drained rooms (#1031) - fixed 3d pickups sometimes triggering z-buffer issues (#1015) - fixed oversized passport in cinematic camera mode (eg when Lara steps on the Midas Hand) (#1009) - fixed braid being disabled by default unless the player runs the config tool first (#1043) - fixed various bugs with falling movable blocks (#723) - fixed the incorrect positioning of door 12 in Tomb of Tihocan (#1063) - fixed a potential softlock in The Cistern by restoring a missing trigger in room 56 (#1066) - improved frame scheduling to use less CPU (#985) - improved and expanded gameflow documentation (#1018) - rotated the Scion in Tomb of Qualopec to face the the main gate and Qualopec (#1007) ## [2.16](https://github.com/LostArtefacts/TRX/compare/tr1-2.15.3...tr1-2.16) - 2023-09-20 - added a new rendering mode called "framebuffer" that lets the game to run at lower resolutions (#114) (forces players to reset their bilinear filter setting) - added the current music track and timestamp to the savegame so they now persist on load (#419) - added the triggered music tracks to the savegame so one shot tracks don't replay on load (#371) - added forward/backward input detection in line with TR2+ for jump-twists (#931) - added an option to restore the mummy in City of Khamoon room 25, similar to the PS1 version (#886) - added a flag indicating if new game plus is unlocked to the player config which allows the player to select new game plus or not when making a new game (#966) - improved Spanish localization for the config tool - improved support for windowed mode (#896) - changed sprite-based pickups to 3D pickups when the 3D pickups option is enabled (#257) - changed the installer to always overwrite all essential files such as the gameflow and injections (#904) - changed the data injection system to warn when it detects invalid or missing files, rather than preventing levels from loading (#918) - changed the gameflow to detect and skip over legacy sequence types, rather than preventing the game from starting (#882) - moved the enable_game_modes option from the gameflow to the config tool and added a gameflow option to override (#962) - moved the enable_save_crystals option from the gameflow to the config tool (#962) - fixed Natla's gun moving while she is in her semi death state (#878) - fixed an error message from showing on exiting the game when the gym level is not present in the gameflow (#899) - fixed the bear pat attack so it does not miss Lara (#450) - fixed some incorrectly rotated pickups when using the 3D pickups option (#253) - fixed dead centaurs exploding again after saving and reloading (#924) - fixed the incorrect starting animation on centaurs that spawn from statues (#926, regression from 2.15) - fixed jump-twist animations at times being interrupted (#932, regression from 2.15.1) - fixed walk-run-jump at times breaking when TR2 jumping is enabled (OG bug in TR2+) (#934) - fixed Lara jumping late with TR2 jumping enabled, as compared to normal TR1 jumping when entering the run animation initially (#975) - fixed the reset and unbind progress bars in the controls menu for non-default bar scaling (#930) - fixed original data issues where music triggers are not set as one shot (#939) - fixed a missing enemy trigger in Tomb of Tihocan (#751) - fixed incorrect trapdoor triggers in City of Khamoon and a switch trigger in Obelisk of Khamoon (#942) - fixed the setup of two music triggers in St. Francis' Folly (#865) - fixed data portal issues in Atlantean Stronghold that could result in a crash (#227) - fixed the camera in Natla's Mines when pulling the lever in room 67 (#352) - fixed flame emitter saving and loading which caused rare crashing (#947) - fixed new game plus not working if enable_game_modes was set to false (#960, regression from 2.8) - fixed Alt-Enter triggering game actions (#979, regression from 2.15) - fixed Natla spinning in her semi-death and second phases when more than one is active in the level (#906) - fixed FPS counter, perspective filter and texture filter not always saved when changed from keyboard (#988) ## [2.15.3](https://github.com/LostArtefacts/TRX/compare/tr1-2.15.2...tr1-2.15.3) - 2023-08-15 - fixed Lara stuttering when performing certain animations (#901, regression from 2.14) - fixed Lara not grabbing certain edges when the swing-cancel option is enabled (#911) ## [2.15.2](https://github.com/LostArtefacts/TRX/compare/tr1-2.15.1...tr1-2.15.2) - 2023-07-17 - fixed Natla not leaving her semi-death state after Lara takes her down for the first time (#892, regression from 2.15.1) ## [2.15.1](https://github.com/LostArtefacts/TRX/compare/tr1-2.15...tr1-2.15.1) - 2023-07-14 - fixed the ape not performing the vault animation when climbing (#880) - fixed holding down up or down to scroll the passport faster (#883, regression from 2.14) - fixed Lara becoming stuck in a T-pose on rare occasions after performing a jump tiwst (#889) ## [2.15](https://github.com/LostArtefacts/TRX/compare/tr1-2.14...tr1-2.15) - 2023-06-08 - added an option to enable TR2+ jump-twist and somersault animations (#88) - added the ability to unbind the sidestep left and sidestep right keys (#766) - added a cheat to explode Lara like in TR2 and TR3 (#793) - added an inverted look camera option (#700) - added a camera speed option for the manual camera (#815) - added an option to fix original texture issues (#826) - added menu specific controls meaning arrow keys, return, and escape now always function in menus (#814, regression from 2.12) - added forward/backward jumps while looking and looking up/down while hanging if enhanced look is enabled (#848) - added case insensitive directory and file detection (#845) - added controller detection during runtime (#850) - added an option to allow cancelling Lara's ledge-swinging animation (#856) - added an option to allow Lara to jump at any point while running, similar to TR2+ (#157) - added the ability to define the anchor room for Bacon Lara in the gameflow (#868) - changed screen resolution option to apply immediately (#114) - changed shaders to use GLSL 1.20 which should fix most issues with OpenGL 2.1 (#327, #685) - changed Bacon Lara to prevent movement after her death (#875) - fixed sounds stopping instead of pausing if game sounds in inventory are disabled (#717) - fixed skipping Eidos logo and end credits (#541) - fixed ceiling heights at times being miscalculated, resulting in camera issues and Lara being able to jump into the ceiling (#323) - fixed Lara not being able to jump off trapdoors or crumbling floors if the sidestep descent fix is enabled (#830) - fixed walk to pickups feature (#834, regression from 2.8) - fixed .mpeg FMVs not working (#844) - fixed the restart level passport text incorrectly showing new game in Lara's Home (#851) - fixed quick load creating an invalid save if used when no saves are present (#853) - fixed Lara entering body hit animations when not appropriate to do so (#857) - fixed SkateKid causing a game crash when too many enemies are active (#866) - fixed missiles damaging Lara when she is far beyond their damage range (#871) ## [2.14](https://github.com/LostArtefacts/TRX/compare/tr1-2.13.2...tr1-2.14) - 2023-04-05 - added Spanish localization to the config tool - added an option to launch Unfinished Business from the config tool (#739) - added dart emitters to the savegame (#774) - added the ability for level builders to stop all music via triggers (#785) - added an option to prevent enemy speeches stopping the current music track (#762) - improved the control of Lara's braid to result in smoother animation and to detect floor collision (#761) - increased the number of effects from 100 to 1000 (#623) - changed the health, air, and enemy bars to better match the PS1 version (#698) - removed the fix_pyramid_secret gameflow sequence (now handled by data injection) (#788) - fixed Larson's gun textures in Tomb of Qualopec to match the cutscene and Sanctuary of the Scion (#737) - fixed texture issues in the Cowboy, Kold and Skateboard Kid models (#744) - fixed the savegame requestor arrow's position with a large number of savegames and long level titles (#756) - fixed empty holsters when starting a level with the shotgun equipped (#749) - fixed a crash when taking a screenshot of an opening FMV (#445) - fixed the animation of Lara's left arm when the shotgun is equipped (#771) - fixed Lara's braid not turning to gold during the Midas touch animation (#769) - fixed the equipped weapon's ammo showing on the inventory screen (#777) - fixed the health, air, and enemy bars from being affected by the text scaling option (#698) - fixed music triggers with partial masks killing the ambient track (#763) - fixed the text and bar scaling from being able to be set below the max and min (#698) - fixed a data issue in Colosseum, which prevented a bat from triggering (#750) - fixed lightning and gun flash continuing to animate in the inventory, pause and statistics screens (#767) - fixed the FPS, healthbar, and arrows from overlapping on the inventory screen (#787) ## [2.13.2](https://github.com/LostArtefacts/TRX/compare/tr1-2.13.1...tr1-2.13.2) - 2023-03-10 - fixed depth buffer size causing rendering issues on some hardware (#748, regression from 2.13) - fixed a game crash when loading a save in which Lara had been struck by an exploding missile (#746) ## [2.13.1](https://github.com/LostArtefacts/TRX/compare/tr1-2.13...tr1-2.13.1) - 2023-03-03 - added an option to use the PlayStation Uzi sound effects (#152) - fixed a few flip effect sounds not playing (#743, regression from 2.12.1) - fixed a game crash when exiting the game with a controller connected (#663) ## [2.13](https://github.com/LostArtefacts/TRX/compare/tr1-2.12.1...tr1-2.13) - 2023-02-19 - added the ability to inject data into levels, with Lara's braid being the initial focus (#27) - added support for .ogg, .mp3 and .wav formats for audio tracks (#688) - added the mummy to the level kill stats if Lara touches it and it falls (#701) - fixed save crystal collision pushing Lara through walls (#682) - fixed passport animation when deselecting the passport (#703) - fixed inconsistent wording in config tool health and air color options (#705) - fixed Scion 1 respawning on load (#707) - fixed dead water rats looking alive when a room's water is drained (#687, regression from 0.12.0) - fixed triggered flip effects not working if there are no sound devices (#583) - fixed the incorrect ceiling textures in Colosseum (#131) ## [2.12.1](https://github.com/LostArtefacts/TRX/compare/tr1-2.12...tr1-2.12.1) - 2023-01-16 - fixed crash when using enhanced saves in levels with flame emitters (#693) - fixed the death counter from breaking old saves if enhanced saves are turned on (#699) ## [2.12](https://github.com/LostArtefacts/TRX/compare/tr1-2.11...tr1-2.12) - 2022-12-23 - added collision to save crystals (#654) - added additional custom control schemes (#636) - added the ability to unbind unessential keys (#657) - added the ability to reset control schemes to default (#657) - added customizable controller support (#659) - added French localization to the config tool (#664) - fixed small cracks in the UI borders for PS1-style menus (#643) - fixed Lara loading inside a movable block if she's on a stack near a room portal (#619) - fixed a game crash on shutdown if the action button is held down (#646) - fixed the compass and new game menus at high text scaling (#648) - fixed save crystals so they are single use (#654) - fixed demo mode if the do not heal on level finish option is used (#660) - removed the puzzle key sound effect when using save crystals (#654) - stopped the default controls from functioning when the user unbound them (#564) ## [2.11](https://github.com/LostArtefacts/TRX/compare/tr1-2.10.3...tr1-2.11) - 2022-10-19 - added a .NET-based configuration tool (#633) - added graphics effects, lava emitters, flame emitters, and waterfalls to the savegame so they now persist on load (#418) - added an option to turn off sound effect pitching (#625) - changed passport to highlight latest save at game start (#618) - fixed some sound effects playing in the inventory when disable_music_in_inventory is true (#486) - fixed underwater currents breaking in rare cases (#127) - fixed gameflow option remove_guns preventing weapon pickups in rare situations (#611) - fixed gameflow option remove_scions causing Lara to equip weapons even if she has none (#605) - added gameflow option remove_ammo to remove all shotgun, magnum and uzi ammo from the inventory on level start (#599) - added gameflow option remove_medipacks to remove all medi packs from the inventory on level start (#599) - improved the UI frame drawing, it will now look consistent across all resolutions and no longer have gaps between the lines - fixed bridge item in City of Khamoon being incorrectly raised (#627) - fixed Lara firing blanks indefinitely when she doesn't have pistols and is out of ammo on non-pistol weapons (#629) ## [2.10.3](https://github.com/LostArtefacts/TRX/compare/tr1-2.10.2...tr1-2.10.3) - 2022-09-15 - fixed save crystal mode always saving in the first slot (#607, regression from 2.8) ## [2.10.2](https://github.com/LostArtefacts/TRX/compare/tr1-2.10.1...tr1-2.10.2) - 2022-08-03 - fixed revert_to_pistols ignoring gameflow's remove_guns (#603) ## [2.10.1](https://github.com/LostArtefacts/TRX/compare/tr1-2.10...tr1-2.10.1) - 2022-07-27 - fixed Lara being able to equip pistols in the gym level (#594) ## [2.10](https://github.com/LostArtefacts/TRX/compare/tr1-2.9.1...tr1-2.10) - 2022-07-26 - added a .NET-based installer - added the option to make Lara revert to pistols on new level start (#557) - added the PS1 style UI (#517) - added the "Story So far..." option in the select level menu to view cutscenes and FMVs (#201) ## [2.9.1](https://github.com/LostArtefacts/TRX/compare/tr1-2.9...tr1-2.9.1) - 2022-06-03 - fixed crash on centaur hatch (#579, regression from 2.9) ## [2.9](https://github.com/LostArtefacts/TRX/compare/tr1-2.8.2...tr1-2.9) - 2022-06-01 - added generic SDL-based controller support (#278) - added the ability to make freshly triggered (runaway) Pierre replace an already existing (runaway) Pierre (#532) - added a fade out when completing Lara's Home (#383) - added the config option to change the number of save slots (#170) - changed default save slot count to 25 (#170) - removed DInput-based XBox controller support - fixed Tihocan chain block sound (#433) - fixed passport menu with high UI scaling (#546, regression from 2.7) - fixed passport menu border being off by one pixel (#547) - fixed the new game and save game passport options using the wrong closing animation (#542, regression from 2.7) - fixed bridges at floor level appearing under the floor (#523) - fixed Lara's outfit in Lara's Home when replaying the level (#571, regression from 2.7) - fixed crash when dying in the gym level with no saves (#576, regression from 2.8) - fixed exiting select level menu causing deaths in a new game incremented in that slot (#575, regression from 2.8) ## [2.8.2](https://github.com/LostArtefacts/TRX/compare/tr1-2.8.1...tr1-2.8.2) - 2022-05-20 - fixed Lara not picking up items near the edges of room portals (#563, regression from 2.8) ## [2.8.1](https://github.com/LostArtefacts/TRX/compare/tr1-2.8...tr1-2.8.1) - 2022-05-05 - fixed Pierre not resetting across levels (#538, regression from 2.7) - fixed pushables breaking with flipped rooms when loading a save (#536, regression from 2.8) ## [2.8](https://github.com/LostArtefacts/TRX/compare/tr1-2.7...tr1-2.8) - 2022-05-04 - added the option to pause sound in the inventory screen (#309) - added level selection to the load game menu (#197) - added the ability to pick up multiple items at once with walk to items enabled (#505) - added the ability to skip pictures during fade animation (#510) - added a cheat to increase the game speed (#135) - added a matrix stack overflow error check and message if GetRoomBounds runs infinitely (#506) - added ability to turn off trex collision (#437) - changed the savegame dialog to remember the user's requested slot number (#514) - changed the new game dialog to always fall back to new game - fixed ghost margins during fade animation on HiDPI screens (#438) - fixed music rolling over to the main menu if main menu music disabled (#490) - fixed Unfinished Business gameflow not using basic / detailed stats strings (#497, regression from 2.7) - fixed picking up multiple underwater pickups with walk to items enabled (#500) - fixed incorrect Lara health when restarting a level - fixed pushables breaking with flipped rooms when loading a save (#496, regression from 2.6) - fixed pictures displayed before starting a level causing a black screen (custom levels only) - fixed underwater caustics animating at 2x speed (#109) - fixed new game plus infinite ammo carrying over to a loaded game (#535, regression from 2.6) ## [2.7](https://github.com/LostArtefacts/TRX/compare/tr1-2.6.4...tr1-2.7) - 2022-03-16 - added ability to automatically walk to pickups when nearby (#18) - added ability to automatically walk to switches when nearby (#222) - added ability to turn off detailed end of the level stats (#447) - added contextual arrows to passport navigation (#420) - added contextual arrows to sound option navigation (#459) - added contextual arrows to controls option navigation (#461) - added contextual arrows to graphics option navigation (#462) - added a final statistics screen (#385) - added music during the credits (#356) - added fade effects to displayed images (#476) - added unobtainable pickups and kills stats support in the gameflow (#470) - fixed exploded mutant pods sometimes appearing unhatched on reload (#423) - fixed sound effects playing rapidly in sound menu if input held down (#467) ## [2.6.4](https://github.com/LostArtefacts/TRX/compare/tr1-2.6.3...tr1-2.6.4) - 2022-02-20 - fixed crash when loading a legacy save and saving on a new slot (#442, regression from 2.6) ## [2.6.3](https://github.com/LostArtefacts/TRX/compare/tr1-2.6.2...tr1-2.6.3) - 2022-02-18 - fixed croc and rats breaking saves after a flipmap (#441, regression from 2.6) ## [2.6.2](https://github.com/LostArtefacts/TRX/compare/tr1-2.6.1...tr1-2.6.2) - 2022-02-17 - fixed equipping gun after starting a demo (#440, regression from 2.6) ## [2.6.1](https://github.com/LostArtefacts/TRX/compare/tr1-2.6...tr1-2.6.1) - 2022-02-16 - fixed equipping gun after starting the game (#439, regression from 2.6) ## [2.6](https://github.com/LostArtefacts/TRX/compare/tr1-2.5...tr1-2.6) - 2022-02-16 - added deaths counter (#388, requires new saves) - added total pickups and kills per level to the compass and end level stats screens (#362) - added new, more resilient savegame format (#277) - added ability to give Lara various items in the gameflow file - added restart level to passport menu on death (#48) - changed Lara's starting health to be configurable; useful for no damage runs (#365) - changed saves to be put in the saves/ directory (#87) - changed fade animations to block the main menu inventory ring like in PS1 (#379) - changed fade animations to be FPS-independent - changed fade animations to run faster in the main menu - changed compass text order to be consistent with level stats (#415) - fixed detail levels text flashing with any option change (#380) - fixed main menu demo playing even when the passport is open (#410, regression from 2.1) - fixed broken poses at the end of cinematics (#390) - fixed libavcodec-related memory leaks (#389) - fixed crash in custom levels that call `level_stats` after playing an FMV (#393, regression from 2.5) - fixed calling `level_stats` for different levels (#336, requires new saves) - fixed sounds playing after demo mode ends when game is minimized (#399) - fixed glitched floor in the Natla cutscene (#405) - fixed gun pickups disappearing in rare circumstances on save load (#406) - fixed equipping gun after loading a legacy save (#427, regression from 2.4) - fixed empty mutant shells in Unfinished Business spawning Lara's hips (#250) - fixed rare audio distance glitch (#421) - fixed Lara not getting her pistols in Atlantis if the player finishes Natla's Mines without picking up any gun (#424) - fixed broken dart ricochet effect (#429) ## [2.5](https://github.com/LostArtefacts/TRX/compare/tr1-2.4...tr1-2.5) - 2022-01-31 - added CHANGELOG.md - added ability to skip cinematics with the Action key - added fade animations (#363) - added a vsync option (#364) - fixed certain inputs skipping too many things (#359) - fixed a memory leak in the audio sampler (#369) ## [2.4](https://github.com/LostArtefacts/TRX/compare/tr1-2.3...tr1-2.4) - 2022-01-19 - added ability to skip FMVs with the action key (#334) - changed shaders to use GLSL version 1.30 (#327) - changed savegames to consume less space - fixed ingame overlay (bars and ammo) being sometimes shown in the menus - fixed menu backgrounds not being shown on certain platforms (#324) - fixed Lara reverting back to pistols when finishing a level with another gun (#338) - fixed lava wedge not setting Lara on fire (#353, regression from 2.2) - fixed fallback game strings not working (#335, regression from 2.3) - fixed high DPI window scaling on Windows (#280) - fixed not all sounds being muted when minimizing the game (#349) - fixed ability to push movable blocks through doors (#46) - fixed showing inventory ring up/down arrows when uncalled for (#337) - fixed Tomb1Main.log to be placed in the game directory rather than the current working directory - fixed a crash when exiting the game (regression from 2.3) - fixed a crash when shader compilation fails ## [2.3](https://github.com/LostArtefacts/TRX/compare/tr1-2.2.1...tr1-2.3) - 2022-01-12 - added ability to hold down forward/back to move through saves faster (#171) - changed screenshots to be saved in its own folder and with more meaningful names (#255) - fixed audible clicks near the end of samples (#281) - fixed secret chime not playing if the secret sound fix is disabled, and nothing plays between consecutive secret pickups (#310) - fixed ambient noises not pausing on pause screen (#316) - fixed underwater sound effect playing only once (#305) - fixed UZI sound stopping near big mutant explosions - fixed switching inventory rings briefly displaying black frames (#75) - fixed top offscreen load game selection (#273, #304) - fixed Lara voiding through static objects (#299) - fixed step left controller input not working (#302, regression from 2.0) - fixed memory leaks ## [2.2.1](https://github.com/LostArtefacts/TRX/compare/tr1-2.2...tr1-2.2.1) - 2022-01-05 - fixed listing available resolutions (a regression from 2.2) - fixed Lara's airbar showing up when Lara's dead (a regression from 2.1) ## [2.2](https://github.com/LostArtefacts/TRX/compare/tr1-2.1...tr1-2.2) - 2022-01-05 - added ability to control anisotropy filter strength - changed the engine look for HD FMVs by default for Unfinished Business - removed tiny screen resolutions (might require setting the resolution again) - fixed Lara getting set on fire on trapdoors over lava - fixed letterbox in main menu showing garbage data on certain machines - fixed save crystals saving before gym level - fixed black lines appearing on walls and floors - fixed hang bug for stacked rooms ## [2.1](https://github.com/LostArtefacts/TRX/compare/tr1-...tr1-2.1) - 2021-12-21 - added ability to disable healthbar and airbar flashing - changed the engine look for HD FMVs by default - increased max active samples to 20 (should fix rare mute sounds issues) - fixed loading TombATI Atlantis saves - fixed shotgun shooting when target out of sight - fixed save selection being offscreen if the first savegame starts with high enough number - fixed alligators dealing no damage under certain circumstances - fixed grabbing bridges under certain circumstances - fixed crash if user presses a key during ring close animation ## [2.0.1](https://github.com/LostArtefacts/TRX/compare/tr1-2.0...tr1-2.0.1) - 2021-12-13 Added an icon to the .exe (thanks TRFan94!) ## [2.0](https://github.com/LostArtefacts/TRX/compare/tr1-1.4.0...tr1-2.0) - 2021-12-07 Shipped our own .exe! Tomb1Main is now fully open source and no longer needs injecting itself to the game. It also no longer depends on any of the TombATI .dll files. You can have both versions installed in the same folder. - added support for HD FMVs - added support for .png and .jpg pictures - added support for .png and .jpg screenshots - added fanmade 16:9 menu backgrounds - added wine support - added ability to run the game from any directory (its CWD no longer needs to point to the game's directory) - changed music player to SDL - changed sample player to SDL - changed FMV player to libavcodec and SDL - changed Eidos logo and initial FMVs to be stored in the gameflow file - changed Unfinished Business to no longer play cafe.rpl - changed the game no longer switches resolution back and forth in windowed mode - changed T1M no longer reads atiset.dat - improved shaders readability (chroma key is now stored in the texture alpha channel) - improved shader performance a bit when the bilinear filter is off - improved 3D rendering performance a bit (no more C++ exception handling) - fixed brightness not being saved - fixed game exiting with "Fatal DirectInput error" when losing focus early ## [1.4.0](https://github.com/LostArtefacts/TRX/compare/tr1-1.3.0...tr1-1.4.0) - 2021-11-16 - added adjustable ingame brightness - added per-level fog settings - added control over fog density (in terms of tiles) - improved TR3 sidesteps - improved wording in readme - fixed lighting for 3D pickups - fixed a crash when drawing lightnings - fixed a crash when compiling the game on MSVC ## [1.3.0](https://github.com/LostArtefacts/TRX/compare/tr1-1.2.2...tr1-1.3.0) - 2021-11-06 - added version in the bottom right corner - added movable camera on W,A,S,D - added Xbox One Controller support - added rounded shadows (instead of the default octagon) - added per-level customizable water color (with customizable blue component) - added rendering of pickups on the ground as 3D meshes - added the ability to change resolution in-game - added optional fixes for the following original game glitches: - slope/wall bug ("bonk to ascend" bug) - breakable tiles bug ("sidestep to descend" bug) - qwop - changed maximum textures from 2048 to 8192 - changed maximum texture pages from 32 to 128 - changed default level skip cheat key from X to L - removed hard limit of 1024 rooms - fixed level skip working in inventory (it would apply only after closing the inventory) - fixed bats being positioned too high - fixed flashing conflicts when cheat buttons are disabled - fixed ability to rebind the pause button ## [1.2.2](https://github.com/LostArtefacts/TRX/compare/tr1-1.2.1...tr1-1.2.2) - 2021-10-17 - added ability to mute music in main menu - added pausing the music while in pause - added more screen resolutions - fixed demos playing oddly when the enhanced look option is enabled - fixed shadows rendering - fixed too big healthbar margins on low resolutions - fixed bilinear filter not working - fixed resolution width/height being ignored ## [1.2.1](https://github.com/LostArtefacts/TRX/compare/tr1-1.2.0...tr1-1.2.1) - 2021-10-17 - added resolution_width and resolution_height to the default settings - fixed screen resolution regression from 1.2.0 ## [1.2.0](https://github.com/LostArtefacts/TRX/compare/tr1-1.1.5...tr1-1.2.0) - 2021-10-15 - fixed a common crash on many machines ## [1.1.5](https://github.com/LostArtefacts/TRX/compare/tr1-1.1.4...tr1-1.1.5) - 2021-10-13 - fixed a regression resulting in crashes from 1.1.4 ## [1.1.4](https://github.com/LostArtefacts/TRX/compare/tr1-1.1.3...tr1-1.1.4) - 2021-10-13 - fixed problem with the alt key on certain machines - fixed a rare crash on certain machines ## [1.1.3](https://github.com/LostArtefacts/TRX/compare/tr1-1.1.2...tr1-1.1.3) - 2021-03-30 - changed smooth bars to be enabled by default - changed end of level freeze fix can no longer be disabled - changed creature distance fix can no longer be disabled - changed pistols + key triggers fix can no longer be disabled - changed illegal gun equip fix can no longer be disabled - changed FMV escape key fix can no longer be disabled - changed input to DirectInput - fixed switchin Control keys when shimmying causing Lara to drop - fixed some anomalies around FPS counter within ingame menus - fixed controls UI missing its borders ## [1.1.2](https://github.com/LostArtefacts/TRX/compare/tr1-1.1.1...tr1-1.1.2) - 2021-03-30 - fixed main menu demo mode not playing correctly (regression from 1.1.1) - fixed game speeding up on certain machines (regression from 1.1.1) ## [1.1.1](https://github.com/LostArtefacts/TRX/compare/tr1-1.1...tr1-1.1.1) - 2021-03-29 - added deactivating game when Alt-Tabbing - improved pink bar color - fixed sounds volume slider not working for ingame sounds ## [1.1](https://github.com/LostArtefacts/TRX/compare/tr1-1.0...tr1-1.1) - 2021-03-28 - added an alert messagebox whenever something bad (within the code's expectations) happens - added smooth bars (needs to be explicitly enabled in the settings) - finished porting the input and sound routines - fixed custom bar colors not working in certain levels - fixed RNG not being seeded (no practical consequences on the gameplay) ## [1.0](https://github.com/LostArtefacts/TRX/compare/tr1-0.13.3...tr1-1.0) - 2021-03-21 - added pause screen - added -gold command line switch to run Unfinished Business ## [0.13.3](https://github.com/LostArtefacts/TRX/compare/tr1-0.13.2...tr1-0.13.3) - 2021-03-21 - added crystals mode (can be enabled in the gameflow) - improved navigation through keyboard controls UI - fixed Unfinished Business gameflow not loading - fixed OG conflicting controls not flashing after relaunching the game - fixed drawing Lara's hair when she carries shotgun on her back - fixed loading custom layouts that conflict with default controls ## [0.13.2](https://github.com/LostArtefacts/TRX/compare/tr1-0.13.1...tr1-0.13.2) - 2021-03-19 - fixed lighting issues (Lara being sometimes very brightly lighted) ## [0.13.1](https://github.com/LostArtefacts/TRX/compare/tr1-0.13.0...tr1-0.13.1) - 2021-03-19 - changed demo_delay constant to be stored in the gameflow file - fixed regression in LoadSamples ## [0.13.0](https://github.com/LostArtefacts/TRX/compare/tr1-0.12.7...tr1-0.13.0) - 2021-03-19 - added display_time parameter to display_picture (requires overwriting your gameflow file) - added user controllable UI and bar scaling - changed limit of max items (moveables in TRLE lingo) from 256 to 10240 - fixed whacky navigation in controls dialog if cheats are enabled - fixed regression in LoadItems that crashes Atlantis - fixed skipping pictures displayed before starting the level with the escape key causing inventory to open ## [0.12.7](https://github.com/LostArtefacts/TRX/compare/tr1-0.12.6...tr1-0.12.7) - 2021-03-19 - added ability to remap cheat keys (except obscure f11 debug key) - changed f10 level skip cheat key to 'x' (can be now changed); had to be done because the game does not let mapping to function keys - changed lots of variables to stay in T1M memory (may cause regressions) - changed runtime game config to be read and written to a new JSON configuration rather than atiset.cfg - changed files directory placement to a new directory, cfg/ ## [0.12.6](https://github.com/LostArtefacts/TRX/compare/tr1-0.12.5...tr1-0.12.6) - 2021-03-18 - fixed loading game in Natla's Mines causing Lara to lose her guns ## [0.12.5](https://github.com/LostArtefacts/TRX/compare/tr1-0.12.4...tr1-0.12.5) - 2021-03-17 - fixed collected secrets resetting after using compass ## [0.12.4](https://github.com/LostArtefacts/TRX/compare/tr1-0.12.3...tr1-0.12.4) - 2021-03-17 - added showing level stats in compass (can be disabled) - added ability to disable game mode selection in gameflow - added fallback gameflow strings (in case someone installs new T1M but forgets to't override the gameflow file) - added ability to exit level stats with escape - changed ingame timer to tick also in the inventory (can be disabled) - changed bar sizes and location to match TR2Main - fixed reading key configuration for keys that override defaults - fixed calculating creature distances (fixes Tihocan croc bug) ## [0.12.3](https://github.com/LostArtefacts/TRX/compare/tr1-0.12.2...tr1-0.12.3) - 2021-03-17 - add Japanese mode (enemies are 2 times weaker) - improve skipping cutscenes - fix crash when FMVs are missing (this doesn't add support for HQ FMVs though) ## [0.12.2](https://github.com/LostArtefacts/TRX/compare/tr1-0.12.1...tr1-0.12.2) - 2021-03-14 - changed settings to save after each change - fixed OG music stopping when playing the secrets chime (can be disabled) - fixed OG game not saving key layout choice (default vs. user keys) - fixed OG volume slider not working when starting muted - fixed OG holding action to skip credit pictures skipping them all at once - fixed OG holding escape to skip FMVs opening inventory ## [0.12.1](https://github.com/LostArtefacts/TRX/compare/tr1-0.12.0...tr1-0.12.1) - 2021-03-14 - huge internal refactors - improved door open cheat - changed 4k scaling path to be always enabled (previously known as enable_enhanced_ui) - fixed killing music underwater - fixed main menu background for UB ## [0.12.0](https://github.com/LostArtefacts/TRX/compare/tr1-0.11.1...tr1-0.12.0) - 2021-03-12 - introduced gameflow sequencer (moves FMVs, cutscenes, level stats etc. logic to the gameflow JSON file); add ability to control number of levels - refactored gameflow - added ability to disable cinematic scenes - changed automatic calculation of secret count to be always enabled - fixed starting NG+ from gym not working - fixed cinematics resetting FOV ## [0.11.1](https://github.com/LostArtefacts/TRX/compare/tr1-0.11...tr1-0.11.1) - 2021-03-11 - added ability to turn off main menu demos - added ability to turn off FMVs - added reporting JSON parsing errors in the logs - fixed reading config sometimes not working - fixed killing music in the inventory - fixed missing Demo Mode text - fixed showing Eidos logo for too short - fixed Lara wearing normal clothes in Gym ## [0.11](https://github.com/LostArtefacts/TRX/compare/tr1-0.10.5...tr1-0.11) - 2021-03-11 - introduced gameflow file (moves all game strings to a gameflow JSON file, including level paths and names); level number, FMVs etc. are still hardcoded ## [0.10.5](https://github.com/LostArtefacts/TRX/compare/tr1-0.10.4...tr1-0.10.5) - 2021-03-10 - added arrows to save/load dialogs - improved user keys settings dialog - you don't have to hold the key for exactly 1 frame anymore - made new game dialog smaller - fixed passport closing when exiting new game mode selection dialog ## [0.10.4](https://github.com/LostArtefacts/TRX/compare/tr1-0.10.3...tr1-0.10.4) - 2021-03-08 - fixed load game screen ## [0.10.3](https://github.com/LostArtefacts/TRX/compare/tr1-0.10.2...tr1-0.10.3) - 2021-03-08 - added NG/NG+ mode selection ## [0.10.2](https://github.com/LostArtefacts/TRX/compare/tr1-0.10.1...tr1-0.10.2) - 2021-03-07 - fixed fly cheat resurrection with lava wedges ## [0.10.1](https://github.com/LostArtefacts/TRX/compare/tr1-0.10...tr1-0.10.1) - 2021-03-07 - improved dealing with missing config - renamed config to .json5 - fixed sound going off after playing a cinematic ## [0.10](https://github.com/LostArtefacts/TRX/compare/tr1-0.9.2...tr1-0.10) - 2021-03-06 - added support for opening closest doors ## [0.9.2](https://github.com/LostArtefacts/TRX/compare/tr1-0.9.1...tr1-0.9.2) - 2021-03-05 - fixed messged up FMV sequence IDs - fixed crash when drawing lightnings near Scion ## [0.9.1](https://github.com/LostArtefacts/TRX/compare/tr1-0.9...tr1-0.9.1) - 2021-03-04 - fixed bats flying near floor - fixed typo in Tomb1Main.json causing everything to be disabled ## [0.9](https://github.com/LostArtefacts/TRX/compare/tr1-0.8.3...tr1-0.9) - 2021-03-03 - added FOV support (overrides GLrage completely, but should be compatible with it) - added support for more than 3 pickups at once (for TRLE builders) - fixed smaller pickup sprites - fixed showing FPS in the main menu doing weird stuff to the inventory text after starting the game ## [0.8.3](https://github.com/LostArtefacts/TRX/compare/tr1-0.8.2...tr1-0.8.3) - 2021-02-28 - improved TR3-like sidesteps - improved bar flashing modes - fixed Lara targeting enemies even after death - fixed version information missing from releases ## [0.8.2](https://github.com/LostArtefacts/TRX/compare/tr1-0.8.1...tr1-0.8.2) - 2021-02-28 - fixed Lara drawing guns when loading OG saves ## [0.8.1](https://github.com/LostArtefacts/TRX/compare/tr1-0.8...tr1-0.8.1) - 2021-02-27 - fixed AI sometimes having problems to find Lara - fixed shotgun firing sound after running out of ammo - fixed OG being able to get pistols by running out of ammo in other weapons, even without having them in the inventory ## [0.8](https://github.com/LostArtefacts/TRX/compare/tr1-0.7.6...tr1-0.8) - 2021-02-27 - added optional TR3-like sidesteps - added "never" to healthbar display modes (so that you can run without ever knowing your health!) - added airbar display modes (so that you can swim without ever knowing your remaining oxygen) - added experimental braid, off by default (works only in Lost Valley due to other levels having no braid meshes) - added version information (it's in file properties) - changed turning fly cheat on above water no longer causes Lara to create bubbles for 1 frame - changed turning fly cheat on after stepping on Midas hand and getting eaten by T-Rex now resets Lara's appearance back to normal - change turning fly cheat on while burning now extinguishes Lara - increase the chance for the player to resurrect Lara with fly cheat after dying (up to 10 s, but it has to be the first keystroke they press) - fixed T1M bug - holding fly cheat and WALK resulting in hoisting Lara up - fixed T1M bug - added ability to draw last selected weapon with numkeys - fixed OG bug - keys and puzzles not triggering after drawing guns - fixed OG bug - having to draw guns via inventory after picking them up in Natla's Mines - fixed OG crash when Lara is on fire and walks too far away from where she caught fire ## [0.7.6](https://github.com/LostArtefacts/TRX/compare/tr1-0.7.5...tr1-0.7.6) - 2021-02-23 - fixed Atlanteans behavior ## [0.7.5](https://github.com/LostArtefacts/TRX/compare/tr1-0.7.4...tr1-0.7.5) - 2021-02-22 - fixed ammo text placement - fixed healthbar placement in the inventory ## [0.7.4](https://github.com/LostArtefacts/TRX/compare/tr1-0.7.3...tr1-0.7.4) - 2021-02-22 - added support for user-configured bar colors - switched configuration format to use JSON5 - moved comments to Tomb5Main.json - fixed bar placement ## [0.7.3](https://github.com/LostArtefacts/TRX/compare/tr1-0.7.2...tr1-0.7.3) - 2021-02-22 - added support for user-configured bar locations - fixed pickups scaling ## [0.7.2](https://github.com/LostArtefacts/TRX/compare/tr1-0.7.1...tr1-0.7.2) - 2021-02-22 - fixed ability to look around while Lara's dead - fixed UI scaling in controls dialog - fixed crash for some creatures ## [0.7.1](https://github.com/LostArtefacts/TRX/compare/tr1-0.7...tr1-0.7.1) - 2021-02-22 - added inventory cheat - made fly cheat faster ## [0.7](https://github.com/LostArtefacts/TRX/compare/tr1-0.6...tr1-0.7) - 2021-02-21 - added fly cheat - fixed a crash when hit by a lightning (T1M regression) - fixed missing "Demo Mode" text (T1M regression) ## [0.6](https://github.com/LostArtefacts/TRX/compare/tr1-0.5.1...tr1-0.6) - 2021-02-20 - changed the code to count secrets automatically (useful for custom level builders) - fixed secret trigger in The Great Pyramid - fixed a crash when loading levels with more than 1024 textures - fixed drawing Lara (T1M regression) ## [0.5.1](https://github.com/LostArtefacts/TRX/compare/tr1-0.5...tr1-0.5.1) - 2021-02-20 - added fire sprite to shotgun ## [0.5](https://github.com/LostArtefacts/TRX/compare/tr1-0.4.1...tr1-0.5) - 2021-02-18 - renamed the project from TR1Main to Tomb1Main on the request of Arsunt - improved documentation ## [0.4.1](https://github.com/LostArtefacts/TRX/compare/tr1-...tr1-0.4.1) - 2021-02-15 - added an option to always show the healthbar - fixed enemy healthbars in NG+ - fixed no heal mode ## [0.4](https://github.com/LostArtefacts/TRX/compare/tr1-0.3.1...tr1-0.4) - 2021-02-14 - added UI scaling - added ability to look around underwater ## [0.3.1](https://github.com/LostArtefacts/TRX/compare/tr1-...tr1-0.3.1) - 2021-02-13 - improved the ability to look around while running ## [0.3](https://github.com/LostArtefacts/TRX/compare/tr1-0.2.1...tr1-0.3) - 2021-02-13 - added an option disable magnums - added an option disable uzis - added an option disable shotgun - added ability to look around while running - added support for using items with numeric keys - fixed an OG bug with the secret sound in Tomb of Tihocan ## [0.2.1](https://github.com/LostArtefacts/TRX/compare/tr1-0.2...tr1-0.2.1) - 2021-02-11 - changed the default configuration to enable enemy healthbars, red healthbar and end of the level freeze fix ## [0.2](https://github.com/LostArtefacts/TRX/compare/tr1-0.1...tr1-0.2) - 2021-02-11 - added enemy healthbars - added a red healthbar ## [0.1](https://github.com/LostArtefacts/TRX/compare/...tr1-0.1) - 2021-02-10 Initial version. ================================================ FILE: docs/tr1/INSTALLING.md ================================================ # Windows (installer) ## Installing (simplified) 1. Download the latest TRX installer for TR1 (e.g. `TRX-1.0-Windows_Installer-tr1.exe`). 2. Mark the installer EXE as safe to run: - Right-click on the `.exe`. - Go to properties. - Click "Unblock". 3. Run the installer and proceed with the steps. > [!NOTE] > When downloading TRX, you might see a warning from Windows Defender, your browser, or another security tool. Modern antivirus systems use AI‑based heuristics – they flag anything uncommon or unsigned as suspicious, even if it's perfectly safe. TRX can trigger these alerts because: > > - It isn't signed with a costly commercial certificate. > - It's a niche, community‑built project, so not widely recognized. > - It's a custom build, not from the Microsoft Store. > > Don't worry: TRX is open‑source, and you can inspect the code yourself on [GitHub](https://github.com/LostArtefacts/TRX/). # Windows / Linux ## Installing (manual) 1. Download the TRX zip file. 2. Extract the zip file into a directory of your choice. Make sure you choose to overwrite existing directories and files. 3. If installing for the first time – put your original game files into the target directory. **Steam / GOG users** 1. Extract the original `GAME.BIN` / `GAME.GOG` file to your target directory. On Windows, this can be done with tools like UltraISO or UniExtract. On Linux, this can be done with `innoextract`, `bin2iso` and `7z`. 2. Get the music files – unfortunately, neither GOG nor Steam ship these assets. - You can download the music files from the link below. https://lostartefacts.dev/aux/tr1x/music.zip (The legality of this approach is disputable.) - Rip the assets yourself from a physical PlayStation/SegaSaturn disk. 3. Optionally, install the Unfinished Business expansion pack. - Pre-packaged level files containing fan-made patch to include music triggers: https://lostartefacts.dev/aux/tr1x/trub-music.zip - Pre-packaged original level files, which do not include music triggers: https://lostartefacts.dev/aux/tr1x/trub-vanilla.zip - Original ISO - requires more involved manual extraction: https://archive.org/details/tomb-raider-i-unfinished-business-pc-eng-full-version_20201225 **TombATI users** 1. Copy the `data`, `fmv` and `music` directories. ## Verifying the installation If you install everything correctly, your game directory should look more or less like this (click to expand):
.
├── cfg
│   ├── presets
│   │   ├── tr1-pc.json5
│   │   ├── tr1-ps1.json5
│   │   ├── tr2-pc.json5
│   │   ├── tr2-ps1.json5
│   │   ├── tr3-pc.json5
│   │   └── tr3-ps1.json5
│   ├── tr1
│   │   ├── gameflow.json5
│   │   ├── strings-de.json5
│   │   ├── strings-en-gb.json5
│   │   ├── strings-fr.json5
│   │   ├── strings-gd.json5
│   │   ├── strings-it.json5
│   │   ├── strings-pl.json5
│   │   ├── strings-ru.json5
│   │   └── strings.json5
│   ├── tr1-demo-pc
│   │   ├── gameflow.json5
│   │   ├── strings-de.json5
│   │   ├── strings-fr.json5
│   │   ├── strings-gd.json5
│   │   ├── strings-it.json5
│   │   ├── strings-pl.json5
│   │   ├── strings-ru.json5
│   │   └── strings.json5
│   ├── tr1-level
│   │   ├── gameflow.json5
│   │   ├── strings-de.json5
│   │   ├── strings-fr.json5
│   │   ├── strings-gd.json5
│   │   ├── strings-it.json5
│   │   ├── strings-pl.json5
│   │   ├── strings-ru.json5
│   │   └── strings.json5
│   ├── tr1-ub
│   │   ├── gameflow.json5
│   │   ├── strings-de.json5
│   │   ├── strings-fr.json5
│   │   ├── strings-gd.json5
│   │   ├── strings-it.json5
│   │   ├── strings-pl.json5
│   │   ├── strings-ru.json5
│   │   └── strings.json5
│   ├── base_strings-de.json5
│   ├── base_strings-en-gb.json5
│   ├── base_strings-fr.json5
│   ├── base_strings-gd.json5
│   ├── base_strings-it.json5
│   ├── base_strings-pl.json5
│   ├── base_strings-ru.json5
│   ├── base_strings.json5
│   ├── catalog_item_actions.csv
│   ├── catalog_lara_anims.csv
│   ├── catalog_lara_states.csv
│   ├── catalog_music.csv
│   ├── catalog_objects.csv
│   ├── catalog_samples.csv
│   ├── inv_ring.json5
│   ├── outfits.json5
│   ├── poses.json5
│   ├── TR1X.json5*
│   ├── ui.json5
│   └── weapons.json5
├── data
│   ├── images
│   │   ├── atlantis.webp
│   │   ├── credits_1.webp
│   │   ├── credits_2.webp
│   │   ├── credits_3.webp
│   │   ├── credits_3_alt.webp
│   │   ├── credits_ps1.webp
│   │   ├── credits_ub.webp
│   │   ├── egypt.webp
│   │   ├── eidos.webp
│   │   ├── end.webp
│   │   ├── greece.webp
│   │   ├── greece_saturn.webp
│   │   ├── gym.webp
│   │   ├── install.webp
│   │   ├── peru.webp
│   │   ├── title.webp
│   │   ├── title_og_alt.webp
│   │   ├── title_ub.webp
│   │   ├── ub_loading1.webp
│   │   └── ub_loading2.webp
│   ├── injections
│   │   ├── atlantis_door_sfx.bin
│   │   ├── atlantis_fd.bin
│   │   ├── atlantis_itemrots.bin
│   │   ├── atlantis_textures.bin
│   │   ├── braid.bin
│   │   ├── bubbles.bin
│   │   ├── cat_cameras.bin
│   │   ├── cat_crystals.bin
│   │   ├── cat_fd.bin
│   │   ├── cat_itemrots.bin
│   │   ├── cat_meshfixes.bin
│   │   ├── cat_textures.bin
│   │   ├── caves_fd.bin
│   │   ├── caves_itemrots.bin
│   │   ├── caves_textures.bin
│   │   ├── cistern_fd.bin
│   │   ├── cistern_itemrots.bin
│   │   ├── cistern_plants.bin
│   │   ├── cistern_skybox.bin
│   │   ├── cistern_textures.bin
│   │   ├── colosseum_fd.bin
│   │   ├── colosseum_itemrots.bin
│   │   ├── colosseum_skybox.bin
│   │   ├── colosseum_textures.bin
│   │   ├── crystal.bin
│   │   ├── cut1_setup.bin
│   │   ├── cut2_setup.bin
│   │   ├── cut3_setup.bin
│   │   ├── cut3_textures.bin
│   │   ├── cut4_setup.bin
│   │   ├── cut4_textures.bin
│   │   ├── door58_frames.bin
│   │   ├── door59_frames.bin
│   │   ├── door59_sfx.bin
│   │   ├── door60_frames.bin
│   │   ├── door61_sfx.bin
│   │   ├── egypt_cameras.bin
│   │   ├── egypt_crystals.bin
│   │   ├── egypt_fd.bin
│   │   ├── egypt_itemrots.bin
│   │   ├── egypt_meshfixes.bin
│   │   ├── egypt_textures.bin
│   │   ├── explosion.bin
│   │   ├── folly_fd.bin
│   │   ├── folly_itemrots.bin
│   │   ├── folly_pickup_meshes.bin
│   │   ├── folly_textures.bin
│   │   ├── font.bin
│   │   ├── gun_glow.bin
│   │   ├── gym_textures.bin
│   │   ├── hive_crystals.bin
│   │   ├── hive_fd.bin
│   │   ├── hive_itemrots.bin
│   │   ├── hive_textures.bin
│   │   ├── khamoon_fd.bin
│   │   ├── khamoon_itemrots.bin
│   │   ├── khamoon_meshfixes.bin
│   │   ├── khamoon_mummy.bin
│   │   ├── khamoon_textures.bin
│   │   ├── lara_animations.bin
│   │   ├── lara_extra.bin
│   │   ├── lara_feet_sfx.bin
│   │   ├── lara_flares.bin
│   │   ├── lara_guns.bin
│   │   ├── lara_gym_flares.bin
│   │   ├── lara_gym_guns.bin
│   │   ├── lara_outfits.bin
│   │   ├── midas_itemrots.bin
│   │   ├── midas_textures.bin
│   │   ├── mines_cameras.bin
│   │   ├── mines_door_sfx.bin
│   │   ├── mines_fd.bin
│   │   ├── mines_itemrots.bin
│   │   ├── mines_meshfixes.bin
│   │   ├── mines_pushblocks.bin
│   │   ├── mines_textures.bin
│   │   ├── misc_sprites.bin
│   │   ├── obelisk_fd.bin
│   │   ├── obelisk_itemrots.bin
│   │   ├── obelisk_meshfixes.bin
│   │   ├── obelisk_skybox.bin
│   │   ├── obelisk_textures.bin
│   │   ├── panther_sfx.bin
│   │   ├── pda_model.bin
│   │   ├── photo.bin
│   │   ├── pickup_aid.bin
│   │   ├── pyramid_fd.bin
│   │   ├── pyramid_itemrots.bin
│   │   ├── pyramid_textures.bin
│   │   ├── qualopec_door_sfx.bin
│   │   ├── qualopec_fd.bin
│   │   ├── qualopec_itemrots.bin
│   │   ├── qualopec_textures.bin
│   │   ├── sanctuary_fd.bin
│   │   ├── sanctuary_itemrots.bin
│   │   ├── sanctuary_scion.bin
│   │   ├── sanctuary_textures.bin
│   │   ├── scion_collision.bin
│   │   ├── skate_kid_sfx.bin
│   │   ├── sprite_alignment.bin
│   │   ├── stronghold_crystals.bin
│   │   ├── stronghold_fd.bin
│   │   ├── stronghold_itemrots.bin
│   │   ├── stronghold_textures.bin
│   │   ├── tihocan_fd.bin
│   │   ├── tihocan_itemrots.bin
│   │   ├── tihocan_skybox.bin
│   │   ├── tihocan_textures.bin
│   │   ├── title_textures.bin
│   │   ├── uzi_sfx.bin
│   │   ├── valley_fd.bin
│   │   ├── valley_itemrots.bin
│   │   ├── valley_skybox.bin
│   │   ├── valley_textures.bin
│   │   ├── vilcabamba_door_sfx.bin
│   │   ├── vilcabamba_itemrots.bin
│   │   ├── vilcabamba_textures.bin
│   │   └── winston_model.bin
│   ├── scripts
│   │   └── gym.lua
│   ├── cat.phd
│   ├── cut1.phd
│   ├── cut2.phd
│   ├── cut3.phd
│   ├── cut4.phd
│   ├── egypt.phd
│   ├── end2.phd
│   ├── end.phd
│   ├── gym.phd
│   ├── level1.phd
│   ├── level2.phd
│   ├── level3a.phd
│   ├── level3b.phd
│   ├── level4.phd
│   ├── level5.phd
│   ├── level6.phd
│   ├── level7a.phd
│   ├── level7b.phd
│   ├── level8a.phd
│   ├── level8b.phd
│   ├── level8c.phd
│   ├── level10a.phd
│   ├── level10b.phd
│   ├── level10c.phd
│   └── title.phd
├── fmv
│   ├── cafe.rpl
│   ├── canyon.rpl
│   ├── core.avi
│   ├── end.rpl
│   ├── escape.rpl
│   ├── lift.rpl
│   ├── mansion.rpl
│   ├── prison.rpl
│   ├── pyramid.rpl
│   ├── snow.rpl
│   └── vision.rpl
├── music
│   ├── track02.flac
│   ├── track03.flac
│   ├── track04.flac
│   ├── track05.flac
│   ├── track06.flac
│   ├── track07.flac
│   ├── track08.flac
│   ├── track09.flac
│   ├── track10.flac
│   ├── track11.flac
│   ├── track12.flac
│   ├── track13.flac
│   ├── track14.flac
│   ├── track15.flac
│   ├── track16.flac
│   ├── track17.flac
│   ├── track18.flac
│   ├── track19.flac
│   ├── track20.flac
│   ├── track21.flac
│   ├── track22.flac
│   ├── track23.flac
│   ├── track24.flac
│   ├── track25.flac
│   ├── track26.flac
│   ├── track27.flac
│   ├── track28.flac
│   ├── track29.flac
│   ├── track30.flac
│   ├── track31.flac
│   ├── track32.flac
│   ├── track33.flac
│   ├── track34.flac
│   ├── track35.flac
│   ├── track36.flac
│   ├── track37.flac
│   ├── track38.flac
│   ├── track39.flac
│   ├── track40.flac
│   ├── track41.flac
│   ├── track42.flac
│   ├── track43.flac
│   ├── track44.flac
│   ├── track45.flac
│   ├── track46.flac
│   ├── track47.flac
│   ├── track48.flac
│   ├── track49.flac
│   ├── track50.flac
│   ├── track51.flac
│   ├── track52.flac
│   ├── track53.flac
│   ├── track54.flac
│   ├── track55.flac
│   ├── track56.flac
│   ├── track57.flac
│   ├── track58.flac
│   ├── track59.flac
│   └── track60.flac
├── shaders
│   ├── 2d.glsl
│   ├── billboard.glsl
│   ├── common.glsl
│   ├── fbo.glsl
│   ├── lights.glsl
│   ├── meshes.glsl
│   ├── meshes_tr3.glsl
│   ├── meshes_tr12.glsl
│   └── ui.glsl
└── TRX.exe
*\* Will not be present until the game has been launched.* ## Playing the game - To play the game, run `TRX.exe`. - To play the Unfinished Business expansion pack, run `TRX.exe --gold`. # macOS ## Installing 1. Download the latest TRX for TR1 installer image (e.g `TRX-0.1-Mac-tr1.dmg`). Mount the image and drag TR1X to the Applications folder. 2. Run TR1X from the Applications folder. This will show you an error dialog about missing game data files. This is expected at this point, as you have not copied them in yet. However, it's important to run the app first to allow macOS to verify the app bundle's signature. 3. Find TR1X in your Applications folder. Right-click it and click "Show Package Contents". 4. Copy your Tomb Raider 1 game data files into `Contents/Resources`. (See the Windows / Linux instructions for retrieving game data from e.g. GOG.) In case you see a popup "TR1X is damaged" when you run the game, run `xattr -cr /Applications/TR1X.app`. ## Verifying the installation If you install everything correctly, your game directory should look more or less like this (click to expand):
.
└── Contents
    ├── Resources
    │   ├── cfg
    │   │   ├── presets
    │   │   │   ├── tr1-pc.json5
    │   │   │   ├── tr1-ps1.json5
    │   │   │   ├── tr2-pc.json5
    │   │   │   ├── tr2-ps1.json5
    │   │   │   ├── tr3-pc.json5
    │   │   │   └── tr3-ps1.json5
    │   │   ├── tr1
    │   │   │   ├── gameflow.json5
    │   │   │   ├── strings-de.json5
    │   │   │   ├── strings-en-gb.json5
    │   │   │   ├── strings-fr.json5
    │   │   │   ├── strings-gd.json5
    │   │   │   ├── strings-it.json5
    │   │   │   ├── strings-pl.json5
    │   │   │   ├── strings-ru.json5
    │   │   │   └── strings.json5
    │   │   ├── tr1-demo-pc
    │   │   │   ├── gameflow.json5
    │   │   │   ├── strings-de.json5
    │   │   │   ├── strings-fr.json5
    │   │   │   ├── strings-gd.json5
    │   │   │   ├── strings-it.json5
    │   │   │   ├── strings-pl.json5
    │   │   │   ├── strings-ru.json5
    │   │   │   └── strings.json5
    │   │   ├── tr1-level
    │   │   │   ├── gameflow.json5
    │   │   │   ├── strings-de.json5
    │   │   │   ├── strings-fr.json5
    │   │   │   ├── strings-gd.json5
    │   │   │   ├── strings-it.json5
    │   │   │   ├── strings-pl.json5
    │   │   │   ├── strings-ru.json5
    │   │   │   └── strings.json5
    │   │   ├── tr1-ub
    │   │   │   ├── gameflow.json5
    │   │   │   ├── strings-de.json5
    │   │   │   ├── strings-fr.json5
    │   │   │   ├── strings-gd.json5
    │   │   │   ├── strings-it.json5
    │   │   │   ├── strings-pl.json5
    │   │   │   ├── strings-ru.json5
    │   │   │   └── strings.json5
    │   │   ├── base_strings-de.json5
    │   │   ├── base_strings-en-gb.json5
    │   │   ├── base_strings-fr.json5
    │   │   ├── base_strings-gd.json5
    │   │   ├── base_strings-it.json5
    │   │   ├── base_strings-pl.json5
    │   │   ├── base_strings-ru.json5
    │   │   ├── base_strings.json5
    │   │   ├── catalog_item_actions.csv
    │   │   ├── catalog_lara_anims.csv
    │   │   ├── catalog_lara_states.csv
    │   │   ├── catalog_music.csv
    │   │   ├── catalog_objects.csv
    │   │   ├── catalog_samples.csv
    │   │   ├── inv_ring.json5
    │   │   ├── outfits.json5
    │   │   ├── poses.json5
    │   │   ├── ui.json5
    │   │   └── weapons.json5
    │   ├── data
    │   │   ├── images
    │   │   │   ├── atlantis.webp
    │   │   │   ├── credits_1.webp
    │   │   │   ├── credits_2.webp
    │   │   │   ├── credits_3.webp
    │   │   │   ├── credits_3_alt.webp
    │   │   │   ├── credits_ps1.webp
    │   │   │   ├── credits_ub.webp
    │   │   │   ├── egypt.webp
    │   │   │   ├── eidos.webp
    │   │   │   ├── end.webp
    │   │   │   ├── greece.webp
    │   │   │   ├── greece_saturn.webp
    │   │   │   ├── gym.webp
    │   │   │   ├── install.webp
    │   │   │   ├── peru.webp
    │   │   │   ├── title.webp
    │   │   │   ├── title_og_alt.webp
    │   │   │   ├── title_ub.webp
    │   │   │   ├── ub_loading1.webp
    │   │   │   └── ub_loading2.webp
    │   │   ├── injections
    │   │   │   ├── atlantis_door_sfx.bin
    │   │   │   ├── atlantis_fd.bin
    │   │   │   ├── atlantis_itemrots.bin
    │   │   │   ├── atlantis_textures.bin
    │   │   │   ├── braid.bin
    │   │   │   ├── bubbles.bin
    │   │   │   ├── cat_cameras.bin
    │   │   │   ├── cat_crystals.bin
    │   │   │   ├── cat_fd.bin
    │   │   │   ├── cat_itemrots.bin
    │   │   │   ├── cat_meshfixes.bin
    │   │   │   ├── cat_textures.bin
    │   │   │   ├── caves_fd.bin
    │   │   │   ├── caves_itemrots.bin
    │   │   │   ├── caves_textures.bin
    │   │   │   ├── cistern_fd.bin
    │   │   │   ├── cistern_itemrots.bin
    │   │   │   ├── cistern_plants.bin
    │   │   │   ├── cistern_skybox.bin
    │   │   │   ├── cistern_textures.bin
    │   │   │   ├── colosseum_fd.bin
    │   │   │   ├── colosseum_itemrots.bin
    │   │   │   ├── colosseum_skybox.bin
    │   │   │   ├── colosseum_textures.bin
    │   │   │   ├── crystal.bin
    │   │   │   ├── cut1_setup.bin
    │   │   │   ├── cut2_setup.bin
    │   │   │   ├── cut3_setup.bin
    │   │   │   ├── cut3_textures.bin
    │   │   │   ├── cut4_setup.bin
    │   │   │   ├── cut4_textures.bin
    │   │   │   ├── door58_frames.bin
    │   │   │   ├── door59_frames.bin
    │   │   │   ├── door59_sfx.bin
    │   │   │   ├── door60_frames.bin
    │   │   │   ├── door61_sfx.bin
    │   │   │   ├── egypt_cameras.bin
    │   │   │   ├── egypt_crystals.bin
    │   │   │   ├── egypt_fd.bin
    │   │   │   ├── egypt_itemrots.bin
    │   │   │   ├── egypt_meshfixes.bin
    │   │   │   ├── egypt_textures.bin
    │   │   │   ├── explosion.bin
    │   │   │   ├── folly_fd.bin
    │   │   │   ├── folly_itemrots.bin
    │   │   │   ├── folly_pickup_meshes.bin
    │   │   │   ├── folly_textures.bin
    │   │   │   ├── font.bin
    │   │   │   ├── gun_glow.bin
    │   │   │   ├── gym_textures.bin
    │   │   │   ├── hive_crystals.bin
    │   │   │   ├── hive_fd.bin
    │   │   │   ├── hive_itemrots.bin
    │   │   │   ├── hive_textures.bin
    │   │   │   ├── khamoon_fd.bin
    │   │   │   ├── khamoon_itemrots.bin
    │   │   │   ├── khamoon_meshfixes.bin
    │   │   │   ├── khamoon_mummy.bin
    │   │   │   ├── khamoon_textures.bin
    │   │   │   ├── lara_animations.bin
    │   │   │   ├── lara_extra.bin
    │   │   │   ├── lara_feet_sfx.bin
    │   │   │   ├── lara_flares.bin
    │   │   │   ├── lara_guns.bin
    │   │   │   ├── lara_gym_flares.bin
    │   │   │   ├── lara_gym_guns.bin
    │   │   │   ├── lara_outfits.bin
    │   │   │   ├── midas_itemrots.bin
    │   │   │   ├── midas_textures.bin
    │   │   │   ├── mines_cameras.bin
    │   │   │   ├── mines_door_sfx.bin
    │   │   │   ├── mines_fd.bin
    │   │   │   ├── mines_itemrots.bin
    │   │   │   ├── mines_meshfixes.bin
    │   │   │   ├── mines_pushblocks.bin
    │   │   │   ├── mines_textures.bin
    │   │   │   ├── misc_sprites.bin
    │   │   │   ├── obelisk_fd.bin
    │   │   │   ├── obelisk_itemrots.bin
    │   │   │   ├── obelisk_meshfixes.bin
    │   │   │   ├── obelisk_skybox.bin
    │   │   │   ├── obelisk_textures.bin
    │   │   │   ├── panther_sfx.bin
    │   │   │   ├── pda_model.bin
    │   │   │   ├── photo.bin
    │   │   │   ├── pickup_aid.bin
    │   │   │   ├── pyramid_fd.bin
    │   │   │   ├── pyramid_itemrots.bin
    │   │   │   ├── pyramid_textures.bin
    │   │   │   ├── qualopec_door_sfx.bin
    │   │   │   ├── qualopec_fd.bin
    │   │   │   ├── qualopec_itemrots.bin
    │   │   │   ├── qualopec_textures.bin
    │   │   │   ├── sanctuary_fd.bin
    │   │   │   ├── sanctuary_itemrots.bin
    │   │   │   ├── sanctuary_scion.bin
    │   │   │   ├── sanctuary_textures.bin
    │   │   │   ├── scion_collision.bin
    │   │   │   ├── skate_kid_sfx.bin
    │   │   │   ├── sprite_alignment.bin
    │   │   │   ├── stronghold_crystals.bin
    │   │   │   ├── stronghold_fd.bin
    │   │   │   ├── stronghold_itemrots.bin
    │   │   │   ├── stronghold_textures.bin
    │   │   │   ├── tihocan_fd.bin
    │   │   │   ├── tihocan_itemrots.bin
    │   │   │   ├── tihocan_skybox.bin
    │   │   │   ├── tihocan_textures.bin
    │   │   │   ├── title_textures.bin
    │   │   │   ├── uzi_sfx.bin
    │   │   │   ├── valley_fd.bin
    │   │   │   ├── valley_itemrots.bin
    │   │   │   ├── valley_skybox.bin
    │   │   │   ├── valley_textures.bin
    │   │   │   ├── vilcabamba_door_sfx.bin
    │   │   │   ├── vilcabamba_itemrots.bin
    │   │   │   ├── vilcabamba_textures.bin
    │   │   │   └── winston_model.bin
    │   │   ├── scripts
    │   │   │   └── gym.lua
    │   │   ├── cat.phd
    │   │   ├── cut1.phd
    │   │   ├── cut2.phd
    │   │   ├── cut3.phd
    │   │   ├── cut4.phd
    │   │   ├── egypt.phd
    │   │   ├── end2.phd
    │   │   ├── end.phd
    │   │   ├── gym.phd
    │   │   ├── level1.phd
    │   │   ├── level2.phd
    │   │   ├── level3a.phd
    │   │   ├── level3b.phd
    │   │   ├── level4.phd
    │   │   ├── level5.phd
    │   │   ├── level6.phd
    │   │   ├── level7a.phd
    │   │   ├── level7b.phd
    │   │   ├── level8a.phd
    │   │   ├── level8b.phd
    │   │   ├── level8c.phd
    │   │   ├── level10a.phd
    │   │   ├── level10b.phd
    │   │   ├── level10c.phd
    │   │   └── title.phd
    │   ├── fmv
    │   │   ├── cafe.rpl
    │   │   ├── canyon.rpl
    │   │   ├── core.avi
    │   │   ├── end.rpl
    │   │   ├── escape.rpl
    │   │   ├── lift.rpl
    │   │   ├── mansion.rpl
    │   │   ├── prison.rpl
    │   │   ├── pyramid.rpl
    │   │   ├── snow.rpl
    │   │   └── vision.rpl
    │   ├── music
    │   │   ├── track02.flac
    │   │   ├── track03.flac
    │   │   ├── track04.flac
    │   │   ├── track05.flac
    │   │   ├── track06.flac
    │   │   ├── track07.flac
    │   │   ├── track08.flac
    │   │   ├── track09.flac
    │   │   ├── track10.flac
    │   │   ├── track11.flac
    │   │   ├── track12.flac
    │   │   ├── track13.flac
    │   │   ├── track14.flac
    │   │   ├── track15.flac
    │   │   ├── track16.flac
    │   │   ├── track17.flac
    │   │   ├── track18.flac
    │   │   ├── track19.flac
    │   │   ├── track20.flac
    │   │   ├── track21.flac
    │   │   ├── track22.flac
    │   │   ├── track23.flac
    │   │   ├── track24.flac
    │   │   ├── track25.flac
    │   │   ├── track26.flac
    │   │   ├── track27.flac
    │   │   ├── track28.flac
    │   │   ├── track29.flac
    │   │   ├── track30.flac
    │   │   ├── track31.flac
    │   │   ├── track32.flac
    │   │   ├── track33.flac
    │   │   ├── track34.flac
    │   │   ├── track35.flac
    │   │   ├── track36.flac
    │   │   ├── track37.flac
    │   │   ├── track38.flac
    │   │   ├── track39.flac
    │   │   ├── track40.flac
    │   │   ├── track41.flac
    │   │   ├── track42.flac
    │   │   ├── track43.flac
    │   │   ├── track44.flac
    │   │   ├── track45.flac
    │   │   ├── track46.flac
    │   │   ├── track47.flac
    │   │   ├── track48.flac
    │   │   ├── track49.flac
    │   │   ├── track50.flac
    │   │   ├── track51.flac
    │   │   ├── track52.flac
    │   │   ├── track53.flac
    │   │   ├── track54.flac
    │   │   ├── track55.flac
    │   │   ├── track56.flac
    │   │   ├── track57.flac
    │   │   ├── track58.flac
    │   │   ├── track59.flac
    │   │   └── track60.flac
    │   ├── shaders
    │   │   ├── 2d.glsl
    │   │   ├── billboard.glsl
    │   │   ├── common.glsl
    │   │   ├── fbo.glsl
    │   │   ├── lights.glsl
    │   │   ├── meshes.glsl
    │   │   ├── meshes_tr3.glsl
    │   │   ├── meshes_tr12.glsl
    │   │   └── ui.glsl
    │   └── icon.icns
    ├── _CodeSignature
    ├── Frameworks
    ├── info.plist
    └── MacOS
*\* Will not be present until the game has been launched.* ================================================ FILE: docs/tr2/CHANGELOG.md ================================================ ## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-1.5.1...develop) - ××××-××-×× See [/docs/CHANGELOG.md]. ## [1.5.1](https://github.com/LostArtefacts/TRX/compare/tr2-1.5...tr2-1.5.1) - 2025-10-10 - changed the examine dialog to be usable with non-puzzle items (#4009) - fixed discs that spawn from emitters facing East starting too far from the emitters themselves (#4007) - fixed a crash on game exit if specifying "ambient_tracks" in the game flow root (regression from 1.1) - fixed alternate ambient tracks being lost on reload in custom levels (#3997, regression from 1.4) - fixed a crash if the game had certain objects (id=17, 18, 41, 43, 50) but was missing their reference objects (id=16, 16, 42, 44, 49, respectively) - fixed Lara at times not being able to grab pushblocks despite being in the correct position to do so (#4005, regression from 1.5) - fixed enemies being unable to smash windows (#4011, regression from 1.4) - fixed Lara appearing flat for a frame during the neutral twist, controlled drop and ledge jump back animations (#4012, regression from 1.4) ## [1.5](https://github.com/LostArtefacts/TRX/compare/tr2-1.4.2...tr2-1.5) - 2025-10-04 Showcase: https://youtu.be/ClkbvsENSvc - added an option to use Lara's barefoot sound effects in appropriate levels (Sound options → Barefoot SFX) (#2643) - added dev console gradient backdrop, similar to TR1X (#2150) - added an option to use smooth bars (Graphics → UI → Smooth bars, default off) - added an option to use TR1-style UI bars (Graphics → UI → Bars look) - added an option to use PS1-style UI bars (Graphics → UI → Bars look) (#1637) - added an option to use PS1-style UI backgrounds and frames (Graphics → UI → Menu style) (#1635) - added an option to use PS1-style carpet texture animation (Graphics → UI → Background style) (#1630) - added an option to change target lock modes (Gameplay → Controls → Weapon lock mode) (#3950) - added an option to cycle targets (Gameplay → Controls → Target change; Controls → Misc → Change Target) (#3951) - added a new `/cls` / `/clear` console command to quickly clear console logs - added an option to turn off ingame timer in the inventory ring (Gameplay → General → Timer counts in inventory) (#3931) - added an option to disable demos (Gameplay → General → Demo mode) - added an option to disable music in the title screen (Sound → Misc → Main menu music) - improved sound settings: - added tabs (Volume and Misc) - added a dedicated option to control master volume (Sound options → Volume → Master volume) - added a dedicated option to control cutscenes volume (Sound options → Volume → Cutscenes volume) (#3490) - added a dedicated option to control FMV volume (Sound options → Volume → FMV volume) (#3490) - added a dedicated option to control general ambient volume (Sound options → Volume → Ambient volume) (#3707) - added an option to turn off sound effect pitching (#625) - improved volume settings to accept slow input for finer adjustments - fixed changing sound volume not updating certain ambient sound sources while in the inventory ring (#3970) - changed OG glitch-related config options to be on/fixed by default (#3929) - changed the Use PSX FOV option name to Use PS1 FOV (Graphics → Visuals → Use PS1 FOV) - changed the UI style to use the PS1 look by default (Graphics → UI → Menu style) - changed the bar appearance to use the PS1 look by default (Graphics → UI → Bars look) - changed the inventory and stats screen to use the PS1 wave animation by default (Graphics → UI → Background style) - changed idle pose timeout from 15 to 60 seconds by default (Gameplay → Controls → Idle pose timeout) - changed idle pose camera to be disabled by default (Gameplay → Controls → Idle pose camera) - changed the game to launch in fullscreen mode by default (Alt-Enter to toggle) - changed max pickup scale to 200% (#3952) - fixed pickup scale being greyed out if the 3D pickups option is enabled (#3952) - fixed trapdoor type 3 (object #116) not functioning (#3895) - fixed camera stutter when shimmying on ladders to the left (#3904, regression from 1.3) - fixed gameplay settings UI displaying eagerly after the first use (#3583, regression from 1.3) - fixed changing FPS after advancing frames in photo mode causing the game to speed up (#3605, regression from 1.3) - fixed CPU spike during playing FMVs (regression from 0.6) - fixed `/play` command likely to skip opening FMVs (#3910, regression from 0.8) - fixed resumed music tracks playing briefly track start upon savegame load (#3916) - fixed highlight size in health and air bars - fixed a potential crash when loading a save where Lara is holding a flare (#3924, regression from 1.0) - fixed unrestricted look mode allowing cinematic cameras to be broken out of (#3926, regression from 1.4) - added the ability for falling movable blocks to kill Lara outright if one lands directly on her (#3784) - fixed numerous interactions with movable blocks, trapdoors, drawbridges, bridges, lifts, and falling blocks for custom levels (#2758): - added the ability for movable blocks to move on trapdoors, drawbridges, bridges, lifts, and falling blocks - added the ability for stacks of movable blocks to fall and land on trapdoors, drawbridges, bridges, lifts, and falling blocks - added the ability for stacks of movable blocks to fall when on opened trapdoors and drawbridges - added the ability for movable blocks to travel up and down lifts - fixed various bugs with falling movable blocks - fixed Lara hang climbing up a movable block used as a ladder piece (#3828) - fixed pushblocks becoming unusable when on the same sector as a door that does not sit on a room portal (#3814) - fixed pushblocks that fall from a great height potentially causing a crash (#3969) - fixed a rare crash if the t-rex is killed with a grenade and many other enemies are active (#3938) - fixed dead skidoo drivers not registering with combat end after loading a save (#3966) - fixed recordings replaying commands twice (regression from 1.4) - fixed the fix for the sticky corner glitch not being optional - now linked to Gameplay → Fixes → Wall glitch mode (#3957, regression from 1.4) - fixed Lara shooting rifle-type weapons drawn during wade to float transition (#3986) - fixed Lara retaining guns if drawn during wade to float transition (#3979, regression from 1.3) - fixed Lara instantly holstering harpoon when drawing in 2+ click water (#3980, regression from 1.3) - fixed -s/--save argument no longer working with -l/--level (#3990, regression from 1.4) ## [1.4.2](https://github.com/LostArtefacts/TRX/compare/tr2-1.4.1...tr2-1.4.2) - 2025-09-07 - fixed broken rendering in MacOS releases (#3880, regression from 1.4) - fixed images from MacOS releases (#3892, regression from 1.4) - fixed the combat end logic not completing properly if Lara is on a vehicle (#3885) - fixed dead water creatures not registering with combat end (#3887, regression from 1.3) ## [1.4.1](https://github.com/LostArtefacts/TRX/compare/tr2-1.4...tr2-1.4.1) - 2025-08-30 - fixed missing shader and configuration files from MacOS releases (#3870, regression from 1.4) - fixed zero byte at the end of config files (#3875, regression from 1.4) - fixed stacked sprites flickering (#3872, regression from 1.4) ## [1.4](https://github.com/LostArtefacts/TRX/compare/tr2-1.3.2...tr2-1.4) - 2025-08-23 Showcase: https://www.youtube.com/watch?v=AAOP1VFX9Lw >[!WARNING] >Attention level builders: this version introduces backwards incompatible changes to the file structure. >Please refer to the [migration guide](../3-MIGRATING.md) to see how to update your levels. - reworked TR2 rendering - added round shadows option (Graphic options → Visuals → Round shadows) - added option to disable skyboxes (Graphic options → Visuals → Skyboxes) - added brightness option (Graphic settings → Rendering → Brightness) - added anisotropy option (Graphic settings → Rendering → Anisotropy filter) - added vertical sync option (Graphic settings → Rendering → VSync) - added an option to keep sprites upright (Graphic options → Rendering → Sprites lock mode) - added debug portals feature (`/debug 1`) - added debug room clip feature (`/debug 1`) - added debug spheres feature (`/debug 1`) - added debug triggers feature (`/debug 1`) - added support for 60 fps in 3D UI pickups - improved bilinear filter appearance - no more dark edges around objects - improved bilinear filter texture adjustment - no more texture "expansion" (#2258) - changed the F7 hotkey to be used as a wireframe toggle (previously available as Shift+F7) - removed software rendering mode - removed the z-buffer option, which is now always enabled - removed undocumented linear and nearest texel adjustment options - fixed trapezoid textures warping at the edge of the screen (#2629) - fixed certain polygons disappearing in some objects (#3699) - fixed z-fighting of doors near walls - added new command switches: - `--test-record` and `--test-replay` for automated playthroughs with (internal tool – the recording file format may be subject to changes) - `--headless`: runs the game offscreen with no audio and at unlocked simulation speed - -q`, `--quiet`: outputs only error messages to the terminal, with log files being written to normally - added ability to move Lara around in photo mode (use sidestep keys to switch modes) - added additional poses for photo mode - added an option to allow Lara to sprint (Gameplay → Controls → Sprinting) (#3711) - added an option to use Lara's slide-to-run animation from TR3+ (Gameplay → Controls → Slide-to-run) (#1089) - added an option to use Lara's neutral jump-twist from early TR1 betas (Gameplay → Controls → Neutral twists) (#1392) - added an option to allow Lara to turn around and grab a ledge she has just stepped off (Gameplay → Controls → Controlled drops) (#3621) - added an option to allow Lara to jump up or back when hanging from a ledge (Gameplay → Controls → Ledge jumps) (#3683) - added an option to have Lara pose after standing idle for a certain time (Gameplay → Controls → Idle pose timeout) (#3727) - added an option to animate the algae in 40 Fathoms, Wreck of the Maria Doria and The Deck (Gameplay settings → Fixes → Fix sprite animations) (#3141) - added an option to scale the 3D pickups in the UI (Graphic options → UI → Pickup scale) - added an option to control fog color (Graphic options → Visuals → Fog transparency and Fog color) (#712, #3618) - added German translation - added a PS1 fade-out to final cutscene (#3521) - added a new `/vsync` console command to toggle the vsync option, like in TR1 - added a new `/lua` console command (for now, [it cannot do much](../8-LUA.md)) - added a new `/restless` console command, which enables or disables infinite sprint - improved frames in Lara's jump-twist animations - improved object loading error messages when an invalid object ID is detected - improved window resize performance in the title inventory ring - improved projectiles - changed conventional weapons to smash all shatterable objects simultaneously instead of 1 for rifles and 2 for pistols (#3378, #3551) - fixed collision detection on windows - fixed harpoons/grenades having no effect on bells (#3379) - fixed conventional weapons not spawning ricochets on bells (#3379) - changed the game flow and game strings file placement - changed the texture page limit from 128 to unlimited (#3517) - changed the `/set` console command to report boolean values as `0` or `1`, language-agnostic - changed waterfall objects to always be drawn when active rather than only when Lara is within a 10 sector range (#3598) - changed `-l`/`--level` switch to accept the level number on top of the level path - changed settings dialogs to show a suitable message if a level builder has hidden all options within that dialog (#3637) - changed the text and bar scale option to work in smaller increments (10% reduced to 5%); added support for slow increments by 1% (hold Walk key) - changed the fly cheat to allow Lara to interact with switches and pickups (#3665) - fixed audio in the shower cutscene in Home Sweet Home not being sync with the turbo cheat (#3541) - fixed projectiles sometimes not shattering breakable windows (#3378, #3551) - fixed flat/opaque window shards in Lara's Home and Home Sweet Home (#3512) - fixed several OG texture issues (#1834, #2082, #3140, #3187, #3372, #3516, #3629, #3634, #3657, #3659, #3791, #3829, #3860) - fixed the passport having an invisible back page, noticeable when opening/closing it (#2051) - fixed z-fighting on the front of the passport (#3584) - fixed window 23 in Venice potentially appearing broken after loading a savegame, despite being intact before saving (#3559) - fixed French translations containing Italian text in some cases (#3567) - fixed several missing, delayed and duplicated door sound effects (#3363, #3614. #3615, #3616, #3663) - fixed being unable to antitrigger waterfall objects (#3589) - fixed incorrect frames in Lara's underwater roll animation (#1589) - fixed mismatched animation frames between the airlock wheel and its corresponding door in offshore levels (#3644) - fixed incorrect airlock and sliding door object positions in offshore levels (#3644) - fixed incorrect door positions in Nightmare in Vegas, causing some to be visible through walls (#3836) - fixed incorrect push button object positions in all levels where it appears (#3596) - fixed incorrect portals in Catacombs of the Talion room 41 (#3664) - fixed being unable to hang off bridges in Barkhang Monastery and Temple of Xian (#3691) - fixed missing zipline reset triggers in Lara's Home (#3698) - fixed shadows Y component not interpolated in 60 FPS (#1314) - fixed a crash when the level file was missing - fixed Lara walking backwards off ledges into lava (#3745) - fixed backslash/grave key/less-than character on some keyboards shown as ???? – now it's shown as backslash (#3713) - fixed Lara being able to get on a skidoo while underwater and consequently dying (#3810) - fixed a missing transition animation between Lara jumping forward and entering freefall (#3815) - fixed potentially being able to reactivate an already used puzzle slot's trigger (#3849, regression from 1.3) - fixed persistent damage resetting Lara's HP after cutscenes (#3595, regression from 1.2) - fixed Lara not being able to look when look mode is set to unrestricted and she is using an airlock door (#3645, regression from 1.3) - fixed wireframe mode rendering as mostly white (#3649, regression from 1.3.2) - fixed being unable to cycle poses in photo mode if cheats were disabled (#3726, regression from 1.3) - fixed Lara exiting the fly cheat if the walk key is used during photo mode (#3753, regression from 1.3) - fixed triggered pickup items flickering in custom levels (#3623, regression from 0.10) - fixed Lara not throwing away a spent flare when swimming of flying (#3816, regression from 0.8) - fixed flame SFX being audible underwater (#3830, regression from 0.3) - fixed harpoon gun not working correctly in NG+ (#3837, regression from 1.3) - fixed exiting photo mode on a controller conflicting with the roll input (#3842, regression from 0.9) - fixed Lara being able to move away from a keyhole/puzzle slot after selecting the key item from the inventory (#3866, regression from 1.3) ## [1.3.2](https://github.com/LostArtefacts/TRX/compare/tr2-1.3.1...tr2-1.3.2) - 2025-07-20 - fixed audio playback with CDAudio backend in cutscenes (#2593) - fixed sprites having thick borders depending on viewing angle (#3549, regression from 1.3) - fixed savegame scanner only seeing all-lowercase file names (#3518, regression from 1.0) - fixed dynamic fire light being generated despite the flame object not being present in the level (#3539, regression from 1.3) - fixed harpoons disappearing if used near inactive/invisible enemies (#3546, regression from 1.3) - fixed the first camera frame when starting or loading a level being inaccurate (#3537, regression from 1.2.2) ## [1.3.1](https://github.com/LostArtefacts/TRX/compare/tr2-1.3...tr2-1.3.1) - 2025-07-18 - fixed Lara's first pose in photo mode at times being skipped (#3522, regression from 1.3) - fixed Lara's arms being drawn inaccurately when posing in photo mode with dual weapons equipped (#3520, regression from 1.3) - fixed Lara's hair at times becoming detached when posing in photo mode with the M16 equipped (#3525, regression from 1.3) ## [1.3](https://github.com/LostArtefacts/TRX/compare/tr2-1.2.2...tr2-1.3) - 2025-07-14 Showcase: https://youtu.be/C9Nf4j05u_w - reworked scaler/sizer options - added an option to set the upscaling filter (Graphic settings → Rendering → Upscaling filter) - changed the "Sizer" option name to "Upscaling factor" (Graphic settings → Rendering → Upscaling factor) - changed the maximum upscaling factor from 4 to 8 - changed the "Scaler" option name to "Borders" (Graphic settings → Rendering → Borders) - changed the border option to use nice square borders if the aspect mode is set to Any - greatly improved text and other UI rendering with upscaling turned on (#1944) - removed default bindings for the "Sizer" and the "Scaler" options (#2853) - changed screenshots to always produce images at desktop resolution - added French translation - added Gaelic translation - added Italian translation to the installer - added dedicated British English translation (#3212) - added the ability to advance individual frames to the photo mode - added the ability to skip end game credits (#3266) - added the ability to hide specific game settings (#3242) - added the ability to cycle UI tabs with sidestep keys (#3272) - added the ability to change the health bar color for allies, defaulting to green (#3005) - added the ability to skip consecutive credit images by holding the action / escape keys - added the ability to cycle between a list of predefined Lara poses in the photo mode - added the ability to use the dev console during FMVs - added a new easter egg command - added a `/lighting` console command to let the player turn lighting system on/off - added an `/immune` console command to make Lara impervious to damage - added an option to have dynamic lights generated by flames (Graphic options → Visuals → Fire lighting) (#3336) - added missing weapons to Lara's Home, Home Sweet Home and Nightmare in Vegas (for the weapons cheat) (#3360) - added the ability in custom levels to use the bear, wolf and ice warrior monk from The Golden Mask in the same level as spiders and other monks - added an option to use TR1 snappy swim turn behaviour (Gameplay settings → Controls → Smooth swimming) (#3387) - added an option to disable underwater twist (Gameplay settings → Controls → Underwater roll) (#3388) - added an option to disable jump twist and swan-dive roll (Gameplay settings → Controls → Jump twists) (#3388) - added an option to control responsive jumping lock behaviour (Gameplay settings → Controls → Jump lock mode) (#3389) - added an option to display level counter in the statistics dialog (Graphic options → UI → Level counter) (#1087) - added an option to control playing of certain animation sound effects such as doors when underwater (Sound options → Underwater animation SFX) (#3385) - added an option to choose between original TR1, original TR2 or unrestricted look modes (Gameplay settings → Controls → Look mode) (#3403) - added an option to make the quick gun equip keys also holster the active gun (Gameplay settings → UI → Quick gun keys) (#828) - added an option to allow the audio to mute when the game is out of focus (Sound options → Mute audio when focus lost, #3333) - added an option to control texture filter for UI alone (Graphic options → Rendering → UI filter) - added a 16:10 aspect ratio to the Aspect mode option (Graphic options → Rendering → Aspect mode) - added an inverted look camera option (Gameplay settings → Controls → Inverted look) (#3403) - added missing end of level statistic screens to Home Sweet Home and Kingdom (#2682) - added an option to control whether or not Lara reverts to pistols when going from one level to another (Gameplay settings → General → Remember guns between levels) (#3455) - improved performance when resizing the window - improved support for >3 secret dragons in custom levels up to 16 dragons - improved the `/tp` command to orient Lara towards keyholes and doors - improved handling of animation sound effects when in shallow water (#3385) - improved error messages for game flow and string edit mistakes to include path of the problematic file - ⚠️ changed game flow logic for a level that follows one that removed Lara's guns e.g. Diving Area: re-adding pistols now needs to be done in the game flow file, similar to Atlantis in TR1 - changed statistics details mode to be placed in the UI section - changed controls dialog to remember the player's preferred input method - changed UI to show icons relevant to the chosen input method - changed death timer skip to only trigger with Action and Inventory keys - changed the examine dialog to be close-able with Look button (#3225) - changed some settings to be hidden when they're only applicable to specific games or custom levels (#3242) - changed some settings to be dimmed when they're not taking effect due to other settings (#3166) - changed photo mode help dialog to show icons for inputs - changed settings to retain their active position until exiting to title or starting a new level (#3271) - changed the dev console to accept compound characters (#2938) - changed the item duplication glitch fix to be on by default - changed the Bartoli's Hideout sunset effect to also apply to skybox lighting (#1617) - changed `/secret give` and `/secret take` to give or take all valid secrets when no index is specified - removed config tool (we have ingame setting dialogs now) - removed the limit of 10 dynamic lights per frame (#3384) - removed the `gym_enabled` game flow property - fixed inventory screen carpet background texture stretched on non-4:3 aspect ratios (#2022) - fixed picked up guns not appearing in holsters / on Lara's back (#1588) - fixed room 134 in Opera House having wrong textures (#3142) - fixed room 136 in Opera House not having water (#3214) - fixed Lara not saying 'aha' when picking up the secret in Lara's Home (#3103) - fixed Lara not drawing weapons with quick draw hotkeys if that was her last equipped weapon (#828) - fixed Lara not drawing weapons other than pistols and Shotgun with draw key if she didn't have any weapons (#828) - fixed Lara using flares only once when holding the flare key (#2062, regression from 0.3) - fixed Lara defaulting to pistols when starting Diving Area, if the player has not collected them in Offshore Rig (#828) - fixed missing zipline sound in Home Sweet Home (#3102) - fixed flare count getting corrupt on save/load if Lara had more than 255 flares (#1592) - fixed title screen background not updating aspect ratio when moving fullscreen window between monitors (#2842) - fixed title screen background and credit images stretching when using very wide resolutions (#2001) - fixed certain commands (such as `/load` or `/play`) not working as expected while in the key use inventory screen (#3338) - fixed Lara able to schedule an interaction with a detonator when it's in use (#3349) - fixed Lara not saying 'no' near gong or detonator when applicable (#3337) - fixed Lara saying 'no' near receptacles after loading a game (#1603) - fixed Lara saying 'no' near receptacles when using guns, medikits or flares (#1601) - fixed Lara being able to permanently discard a key item if she gets pushed on the exact frame she interact with a receptacle (#3398) - fixed key items getting consumed at the start of the interaction with receptacles (#3399) - fixed the Bartoli's Hideout sunset effect being reset after reloading a save (#1617) - fixed the shotgun sound at the end of the shower cutscene in Home Sweet Home being cut off when the credits start (#1579) - fixed the camera being partially inside the wall at the end of the Home Sweet Home shower cutscene (#3370) - fixed the boat veering if Lara looks left or right when driving (#3409) - fixed Lara not equipping a weapon chosen from inventory if it is the last weapon used (#3457) - fixed Stopwatch label in Gym not appearing when holding arrows during inventory spin-out (#3460) - fixed incorrectly shaded sprites (#3476, regression from 1.0) - fixed being able to deselect the passport in the game over screen (#3381, regression from 1.0) - fixed Lara getting stuck in the fly cheat in rare circumstances (#3392, regression from 0.3) - fixed hostile snowmobiles only shooting one gun (#3478, regression from 0.8) - fixed support for >3 secret dragons in custom levels (#3415, regression from 1.2) - fixed level select picking one level ahead of the one chosen if the gym is disabled (#3446, regression from 1.0) - fixed Lara's holsters resetting at times to incorrect meshes when using the fly cheat (#3451, regression from 0.3) - fixed a possible soft lock when saving the game after killing the last boss in Home Sweet Home (#3470, regression from 1.2) - fixed the `/play` command starting the level with wrong items sometimes (#3147, regression from 1.1) - fixed the `/play` command starting Gym in The Golden Mask (this level is not working correctly with TR2G's main.sfx) - fixed the `/tp` command breaking the photo mode - fixed the `/tp` command misbehaving when giving fractional coordinates - fixed the `/play` command not stopping active music when used to play Venice (#3469, regression from 0.8) - fixed Lara being affected by the `/kill` command if monks have been angered (#3492, regression from 1.0) ## [1.2.2](https://github.com/LostArtefacts/TRX/compare/tr2-1.2.1...tr2-1.2.2) - 2025-06-24 - fixed underwater hum not playing properly (#3305, regression from 0.10) - fixed game crashing when the expected resources are missing (#3310, regression from 1.2.1) - fixed restore default pop-up requiring all 3 water color options to be adjusted instead of just one (#3314, regression from 1.2) - fixed pause screen rendered without background overlay if fade effects are disabled (#3316, regression from 1.1) - fixed `/pos` command crashing when the level title is not set (regression from 1.2) ## [1.2.1](https://github.com/LostArtefacts/TRX/compare/tr2-1.2...tr2-1.2.1) - 2025-06-22 - fixed some secrets in some levels incorrectly registering by standing on specific tiles (#3280, regression from 1.2) - fixed movable blocks getting stuck in midair if the game is saved and loaded while they are falling (#3274) - fixed PS touchpad input missing an icon (#3288, regression from 4.12) - fixed inability to use unbind key / reset layout buttons with controllers (#3290, regression from 1.2) - fixed inventory ring consuming too many items under severe frame drop conditions (#3295, regression from 1.0) - fixed screenshots stripping accented characters (#3238) - fixed accented lowercase `i` characters retaining the superscript dot (#3298) - reverted the partial fix for wrong audio device reinitialization (#3251, regression from 1.2) ## [1.2](https://github.com/LostArtefacts/TRX/compare/tr2-1.1...tr2-1.2) - 2025-06-17 Showcase: https://www.youtube.com/watch?v=yG82_Lt6v9M - added builtin support for ingame string translations - changed duplicate game strings between TR1 and TR2 to be placed in a single file TRX_common_strings.json5 - added a new setting, `enable_review_markers`, which display which text requires review (only available via `/set`) - added Italian translation - added Polish translation - added support for non-breaking spaces - fixed game crashing when trying to word-wrap unknown characters - added UI for all config tool settings - added ingame help for all settings - added the ability to use `.avi`, `.mkv`, `.mp4`, `.mpeg`, and `.webm` files for FMVs, as well as the default `.rpl` (#3190) - added support for showing key/puzzle/pickup item descriptions (examining) in the inventory (#1875) - added support for object name aliases; added aliases for dev commands - added a pickup overlay display when Lara pulls the dagger from the dragon (#1830) - added an option to disable Lara's braid (#3089) - added an option to disable the breeze effect on Lara's braid (#3090) - added keyboard and controller input icons to the controls settings dialog - added an option to continue playing music while in the inventory (#1702) - added an option to adjust music and ambient volume while in the inventory (#2870) - added a `/debug` console command - added a `/secret` console command for easier debugging of secrets - added `enable_debug_pos` setting that shows Lara's position in realtime (reachable via `/debug`) - added graphics effects to the savegame so they now persist on load (#2736) - added an option to control whether or not Lara responds to hitting a wall while wading (#3138) - added an option to fix the breakable floor descending glitch (#3152) - added an option to fix wall glitches, or to use TR1 wall glitch behaviour (#3153) - added an option to disable swing cancelling (#3150) - added an option to disable lean jumping (#3151) - added an option to disable smooth wall deflection when Lara comes to a stop at a wall, similar to TR1 (#3148) - added an option to have Lara boost forward when rolling off one-click steps, similar to TR1 (#3149) - added an option to toggle allowing Lara to exit from water horizontally, below, or climbing out onto non-standable slopes (#3154) - added an option to toggle random enemy initial angle adjustment (#3129) - added an option to prevent Lara targeting allies, either with weapons or the skidoo (#3012) - added an option to alter Lara's HP for the beginning of each level (#3179) - added an option to not restore Lara's HP at the beginning of each level (#3179) - added an option to configure how many shots Lara can take with the harpoon gun before reloading, including disabling reloading altogether (#3057) - improved word wrapping algorithm in the dev console - improved the dev console commands documentation - changed logs format to include timestamps - changed the music track slot limit from 64 to 1024 (#3101) - ⚠️ changed the music track behaviour to no longer shift track numbers (#3100) - if playing original levels, make sure to update the game flow and injection files from this release - if building levels, use track numbers that correspond to the file names; previously built levels will need to be manually adjusted - changed the maximum number of 2D static mesh slots (room sprites) from 50 to 256 (#3200) - changed sound and music volumes to be displayed as percentage instead of 0-10 - changed the `/tp` command to align Lara to switches and pickups - changed the `/set` command to accept `-`, which will restore the given setting to its default state - changed the graphic settings dialog to use tabs - changed the setting dialogs to respect the UI wraparound setting - changed the combat end logic (used in Home Sweet Home) to allow using any regular enemy type aside from the boss - changed the rotation of some pickups in The Golden Mask to better suit the 3D pickups option (#1973) - changed text kerning to a smaller value - fixed a missing collapsible tile trigger in The Cold War room 82 (#3058) - fixed missing sound effects for collapsible tiles in Opera House, The Deck and Catacombs of the Talion (#2262, #2872, #3087) - fixed texture and visibility issues with the skyboxes in The Cold War and Kingdom (#3056) - fixed the same boss item always being selected in Home Sweet Home, regardless of Lara's proximity (#3062) - fixed transparent eyes on Lara's model in the gym and Home Sweet Home levels (#3072) - fixed transparent eyes on the wolf model in Furnace of the Gods (#3073) - fixed Lara getting stuck in her hit animation if she is hit while using an airlock door, the detonator or the gong (#3092) - fixed Lara behaving erratically if she is killed while hanging from a ledge (#3134) - fixed Lara's health bar showing in the Home Sweet Home shower cutscene (#1564) - fixed Lara dropping flares after certain special animations, such as pulling the dagger from the dragon (#3084, regression from 1.1) - fixed unbind key option being available when it shouldn't (#3111, regression from 1.1) - fixed the sizer option accepting values above 1 which made no sense (#3123, regression from 1.0) - fixed a rare crash when editing certain dev console history entries (#2913, regression from 1.0) - fixed Lara's health bar showing at the start of cutscenes (#3182, regression from 1.1) - fixed scaler/sizer options not working under some circumstances (#3240, regression from 0.7) - fixed broken playback of mono music tracks (regression from 0.2) - fixed hot-plugging certain audio devices causing glitchy playback (partial fix; regression from 0.2) - fixed stats dialog reserving too much space for extra secrets (#3237, regression from 1.0) - fixed logging not outputting anything on Windows terminals - fixed `/kill all` command softlocking the game in Home Sweet Home ## [1.1](https://github.com/LostArtefacts/TRX/compare/tr2-1.0.2...tr2-1.1) - 2025-05-23 Showcase: https://www.youtube.com/watch?v=g5lrrDXDYKo - added a /help command (#2917) - added a flashing Demo Mode caption to demos (#1556) - added arrows to the passport text like in TR1X (#2926) - added aliases to CLI options (`-gold` becomes `-g/--gold`) - added a `--help` CLI option (may not output anything on Windows machines – OS bug) - added explosion sprites to Home Sweet Home (#1569) - added ability to reposition the health bar and the air bar (#1611) - added enemy health bars (#2909) - added an FPS counter (#2910) - added the ability to move the camera around with W,A,S,D (rebindable) (#2978) - added an option to toggle between TR1 and TR2 camera modes (#2990) - added the ability to reset active inputs layout - added the ability to unbind non-essential keys - added the ability to rebind more keys - added the ability to trigger different ambient tracks in custom levels, which will loop and be remembered between saves - improved word wrapping algorithm in the dev console - improved the `/set` console command to display available options if given an unknown argument - improved handling of items that are dropped by enemies (#2952) - added the ability for any enemy type to drop items, excluding eels - fixed items dropped by flying creatures not falling to the ground - changed the design of the controls dialog to use pages, making it better suited for small screens, larger text sizes, and more key bindings - changed on-screen messages (such as `Z-Buffer on` to use the dev console, like in TR1X) - changed the sound dialog appearance (repositioned, added text labels and arrows) - changed the installer to always allow downloading music files (#2891) - changed the dev console to no longer add duplicate entries to the history - changed the health bar and the air bar sizes to be slightly bigger - changed the pause screen to have a darker black overlay transparency (#2252) - removed the hard-coded inventory allocation on the first level by default, moving it instead to the game flow (#1867) - removed the hard-coded repositioning of Bartoli (pre-dragon) on initialise (#2950) - fixed Lara's braid pointing straight down when swimming below sloped ceilings (#1600) - fixed glide cameras using a default speed rather than maintaining the values set in the level file (#2962) - fixed Lara being killed if she enters the void in a level that uses the `disable_floor` sequence in the game flow (#2874, regression from 0.10) - fixed Lara unable to equip pistols after getting a rifle-type weapon wet while wading (#2994) - fixed flame emitter 23 in room 6 not being deactivated when the lever in room 1 is used (#2851) - fixed Lara snapping to face forwards if she has a slight angle and action is pressed after using an airlock door (#2215) - fixed Lara being able to equip guns and flares during in-game cutscenes (#2895) - fixed an illegal reachable slope in Barkhang Monastery room 96, which could lead to Lara becoming softlocked (#2900) - fixed the camera behaving erratically in rooms/sectors that have no pathfinding data (#2946) - fixed wall light mesh positions in Venice, Bartoli's Hideout and Barkhang Monastery (#2944) - fixed faulty zoning data in Ice Palace rooms 48/110 that could result in the yetis becoming stuck (#3000) - fixed a misplaced springboard trigger in Ice Palace room 104 (#3003) - fixed the game crashing on unknown sequencer events - fixed the game crashing when editing long dev console history entries (#2913, regression from 1.0) - fixed harpoon's ammo counter overlapping with the air bar (#2871) - fixed flames showing briefly when Lara enters water and a death tile is present - fixed being unable to load a save made in the first level if that level removes Lara's weapons but also has a shotgun pickup (#2934, regression from 0.9) - fixed misplaced effects such as bubbles and dragon fire in 60 FPS (#2873, #2881, regression from 0.10) - fixed incorrect camera shifts when some fixed cameras return to normal view (#2971, regression from 0.10) - fixed blood not spawning when Lara is run down by boulders/barrels (#2982, regression from 0.7) - fixed floors being lowered too much under pushable blocks that are killed in the same trigger that flips the map (#3007, regression from 0.9) - fixed inventory ring items not being animated when the ring is rotating (#2964, regression from 0.9) - fixed the camera jumping if going from a look at trigger to a fixed camera, such as in The Cold War room 36 (#3033, regression from 0.9) - fixed a crash in The Golden Mask if the bear is killed with the grenade launcher (#3037, regression from 1.0) - fixed passport faces partially invisible ## [1.0.2](https://github.com/LostArtefacts/TRX/compare/tr2-1.0.1...tr2-1.0.2) - 2025-04-26 - changed The Golden Mask strings to default to the OG strings file for the main tables (#2847) - fixed Lara voiding if she stops on a tile with a closing door, and the door isn't on a portal (#2848) - fixed guns carried by enemies not being converted to ammo if Lara has picked up the same gun elsewhere in the same level (#2856) - fixed button mashing causing quick save/load to misbehave on a specific passport animation frame (#2863, regression from 1.0) - fixed guns carried by enemies not being converted to ammo if Lara starts the level with the gun and the game has later been reloaded (#2850, regression from 1.0) - fixed 1920x1080 screenshots in 16:9 aspect mode being saved as 1919x1080 (#2845, regression from 0.8) - fixed clicks in audio sounds (#2846, regression from 0.2) ## [1.0.1](https://github.com/LostArtefacts/TRX/compare/tr2-1.0...tr2-1.0.1) - 2025-04-24 - added an option to wraparound when scrolling UI dialogs, such as save/load (#2834) - improved graphic settings dialog sizing (#2841) - changed save to take priority over load when both inputs are held on the same frame, in line with OG (#2833) - fixed the selected keyboard/controller layout not being saved (#2830, regression from 1.0) - fixed toggling the PSX FOV option not having an immediate effect (#2831, regression from 1.0) - fixed changing the aspect ratio not updating the current background image (#2832, regression from 1.0) ## [1.0](https://github.com/LostArtefacts/TRX/compare/tr2-0.10...tr2-1.0) - 2025-04-23 Showcase: https://www.youtube.com/watch?v=iUNUJda6QCU - added support for The Golden Mask (#1621) - added ability to turn off legal screen and FMVs (#2740) - added ability to turn off ingame cutscenes (#2127) - added HD images from TR2Main (with Arsunt's consent) - added sunglasses for graphic options (#1615) - added control over the fog distances for players and level builders (#1622) - added control over the water color for players and level builders [see the reference](/docs/WATER_COLORS.md) (#1619) - added an installer for Windows (#2681) - added the bonus level game flow type, which allows for levels to be unlocked if all main game secrets are found (#2668) - added the ability for custom levels to have up to two of each secret type per level (#2674) - added BSON savegame support, removing the limits imposed by the OG 8KB file size, so allowing for storing more data and offering improved feature support (legacy save files can still be read, similar to TR1) (#2662) - added NG+, Japanese, and Japanese NG+ game mode options to the New Game page in the passport (#2731) - added the ability for spike walls to be reset (antitriggered) - added the current music track and timestamp to the savegame so they now persist on load (#2579) - added waterfalls to the savegame so that they now persist on load (#2686) - added support for aspect ratio-specific images (#1840) - added a guard to ensure the game always starts on a visible screen even after unplugging displays (#2819) - improved performance when moving the window around - improved pause exit dialog - it can now be canceled with escape - changed savegame files to be stored in the `saves` directory (#2087) - changed the default fog distance to 22 tiles cutting off at 30 tiles to match TR1X (#1622) - changed the number of static mesh slots from 50 to 256 (#2734) - changed the maximum number of items (moveables) per level from 256 to 10240 (1024 remains the limit for triggered items) (#1794) - changed the maximum number of visible enemies from 5 to 32 (#1624) - changed the maximum number of effects (flames, embers, exploding parts etc) from 100 to 1000 (#1581) - changed default pitch of the save/load dialog ingame - it's now higher. - removed the need to specify in the game flow levels that have no secrets (secrets will be automatically counted) (#1582) - removed the hard-coded end-level behaviour of the bird guardian for custom levels (#1583) - removed the FPS and aspect mode options from the config tool (now available in-game in the graphics options) - fixed the inability to completely mute the sounds, even at sound volume 0 (#2722) - fixed the final two levels not allowing for secrets to be counted in the statistics (#1582) - fixed assault course best times not being retained between game relaunches (#1578) - fixed flares disappearing on the ground when the z buffer is enabled (#1595) - fixed Lara's holsters being empty if a game flow level removes all weapons but also re-adds the pistols (#2677) - fixed the console opening when remapping its key (#2641) - fixed the boat when it explodes after crossing mines, where Lara's hips would appear rather than exploded boat parts (#1605) - fixed Lara's hips appearing on Bartoli in the Temple of Xian cutscene (#2558) - fixed collision issues with drawbridges, trapdoors, and bridges when stacked over each other, over slopes, and near the ground (#2752) - fixed the lift to work in any cardinal direction in custom levels, not just South (#2100) - fixed the springboard not responding correctly when Lara drives across one on a skidoo (#1903) - fixed the drawbridge producing dynamic light when open (#2294) - fixed the scale of several pickup models in The Golden Mask (#2652) - fixed the shark in The Cold War not making any sounds when biting Lara (#2678) - fixed the bird monster not having a shadow (#2060) - fixed the in-game cinematic camera at times yielding invalid positions (and hence views) in custom levels (#2754) - fixed a softlock in Temple of Xian if the main chamber key is missed (#2042) - fixed a potential softlock in Floating Islands if returning towards the level start from the gold secret (#2590) - fixed a potential softlock in Nightmare in Vegas where the bird monster could remain inactive, or the flip map not set (#1851) - fixed invalid portals in The Deck between rooms 17 and 104, which could result in Lara seeing enemies in disconnected rooms (#2393) - fixed pushblocks being rotated when Lara grabs them, most noticeable if asymmetric textures have been used (#2776) - fixed the boat briefly having an underwater hue when Lara first climbs on (#2787) - fixed destroyed gondolas appearing embedded in the ground after loading a save (#1612) - fixed a crash in custom levels with large rooms (#2749) - fixed the viewport not always in sync with the window (#2820) - fixed inability to move the window to another screen (#2820) - fixed flares flipped to the right when thrown (regression from 0.10) - fixed the camera going out of bounds in 60fps near specific invalid floor data (known as no-space) (#2764, regression from 0.10) - fixed sprites rendering black if no shade value is assigned in the level (#2701, regression from 0.8) - fixed some 3D pickup items rendering black in software mode (#2792, regression from 0.10) - fixed Lara at times ending up in incorrect rooms when using the teleport cheat (#2486, regression from 0.3) - fixed the `/pos` console command reporting the base room number when Lara is actually in a flipped room (#2487, regression from 0.3) - fixed a crash if an image was missing - fixed a crash on level load if an animation has no frames (#2746, regression from 0.8) - fixed flares missing the flicker effect in 60 FPS (#2806, regression from 0.10) ## [0.10](https://github.com/LostArtefacts/TRX/compare/tr2-0.9.2...tr2-0.10) - 2025-03-18 Showcase: https://www.youtube.com/watch?v=s41hznpTJkY - added support for 60 FPS rendering - added support for more accented characters (#2356) - added quadrilateral interpolation (#354) - added a `/cheats` console command - added a `/wireframe` console command (#2500) - added a `/fps` console command - added `/flood` and `/drain` console commands - added support for `-l`/`--level` argument to play a single level - added support for `-s`/`--save` argument to immediately start a saved game - added the ability to specify per-level SFX files rather than enforcing the default (main.sfx) on all levels (#2615) - added the camera shutter sound to cutscenes for photo mode (#2280) - added Italian localization to the config tool - improved camera mode navigation: - improved support for pivoting - improved roll support - expanded world bounding box by 5 tiles in each direction - added support for 60 FPS - changed injections to a new file format with a smaller footprint, improved applicability tests and similar feature support as TR1 (#1967) - changed the `/pos` command to show `Demo` and `Cutscene` instead of `Level` when relevant - changed the `/pos` command to show demo and cutscene numbers starting at 1, in line with `/play` - changed the `/play` and `/pos` commands to always treat the gym level as the level 0 – even if it's not included - removed the hardcoded title screen image path, replacing it with a game flow file property instead - fixed smashed windows blocking enemy pathing after loading a save (#2535) - fixed several instances of the camera going out of bounds (#1034) - fixed Lara getting stuck in a T-pose after jumping/falling and then dying before reaching fast fall speed (#2575) - fixed missing enemy sound effects in the underwater levels (#2293) - fixed seaweed collision in Living Quarters preventing Lara from climbing out of the water in room 15 (#2197) - fixed the scale and rotation of several pickup models, such as the offshore key cards and Barkhang prayer wheels (#1832, #1894) - fixed a rare issue whereby Lara would be unable to move after disposing a flare (#2545, regression from 0.9) - fixed flare pickups only adding one flare to Lara's inventory rather than six (#2551, regression from 0.9) - fixed several issues with pushblocks (#2036/#2193) - fixed an invisible wall above stacked pushblocks if near a ceiling portal - fixed floor height issues with pushblocks poised to fall in various scenarios - fixed being unable to stack multiple pushblocks over multiple rooms - fixed falling pushblocks using the enemy grunt sound effect - fixed play any level causing the game to hang when no gym level is present (#2560, regression from 0.9) - fixed extremely large item quantities crashing the game (#2497, regression from 0.3) - fixed missing new game text in the passport when play any level is enabled (#2563, regression from 0.9) - fixed the play any level dialog not showing in the gym passport (#2564, regression from 0.9) - fixed losing the NG+ flag when loading a save that has it set (#2566, regression from 0.9.2) - fixed the ammo counter not showing in demos if NG+ is set (#2574, regression from 0.9) - fixed being able to play with Lara invisible after using the explosion cheat then the fly cheat (#2584, regression from 0.9) - fixed the `/pos` command not showing demo and cutscene titles - fixed the distance travelled stat displaying the wrong value when over 1000m (#2659) ## [0.9.2](https://github.com/LostArtefacts/TRX/compare/tr2-0.9.1...tr2-0.9.2) - 2025-02-19 - fixed secret rewards not handed out after loading a save (#2528, regression from 0.8) - fixed music not working on certain Linux setups (#2504, regression from 0.2) ## [0.9.1](https://github.com/LostArtefacts/TRX/compare/tr2-0.9...tr2-0.9.1) - 2025-02-15 - improved memory usage by shedding ca. 100-110 MB on average - changed passport to be more responsive to player inputs (#1328) - fixed resolving paths (especially to music files) on case-sensitive filesystems (#1934, #2504) - fixed loading a game crashing on Linux (#2508, regression from 0.9) ## [0.9](https://github.com/LostArtefacts/TRX/compare/tr2-0.8...tr2-0.9) - 2025-02-14 Showcase: https://www.youtube.com/watch?v=FrBSW35ZPKY - added Linux builds and toolchain (#1598) - added macOS builds (for both Apple Silicon and Intel) (#2226) - added pause dialog (#1638) - added a photo mode feature (#2277) - added fade-out effect to the demos - added the ability to hold left/right to move through menus more quickly (#2298) - added the ability to disable exit fade effects alone (#2348) - added a fade-out effect when completing Lara's Home - added support for animated sprites (#2401) - added a `/cut` (alias: `/cutscene`) console command for playing cutscenes - added a `/gym` (alias: `/home`) console command for playing Lara's Home - added a `/music` console command that plays a specific music track - added a console log when using the `/demo` command - improved rendering to achieve a slight performance boost in big rooms (#2325) - improved wireframe mode appearance around screen edges - changed the object texture limit from 2048 to unlimited (within game's overall memory cap) (#1795) - changed the sprite texture limit from 512 to unlimited (within game's overall memory cap) (#1795) - changed the texture page limit from 32 to 128 (#1796) - changed default input bindings to let the photo mode binding be compatible with TR1X: | Key | Old binding | New binding | | ----------------------------- | ----------- | ------------ | | Decrease resolution | Shift+F1 | Shift+F11 | | Increase resolution | F1 | F11 | | Decrease internal screen size | Shift+F2 | Shift+F10 | | Increase internal screen size | F2 | F10 | | Toggle photo mode | --- | F1 | | Toggle photo mode UI | --- | H | - changed the `/kill` command with no arguments to look for enemies within 5 tiles (#2297) - changed the game data to use a separate strings file for text information, removing it from the game flow file - changed dynamic lighting for gun flashes and explosions to be optional (#2357) - fixed scale of secret icons on level complete summary (#1631) - fixed showing inventory ring up/down arrows when uncalled for (#2225) - fixed Lara never stepping backwards off a step using her right foot (#1602) - fixed flawed frame number checks which prevented Lara's wall hit animation while wading - fixed blood spawning on Lara from gunshots using incorrect positioning data (#2253) - fixed ghost meshes appearing near statics in custom levels (#2310) - fixed potential memory corruption when reading a custom level with more than 512 sprite textures (#2338) - fixed the teleporting command sometimes putting Lara in invalid flipmap rooms (#2370) - fixed teleporting to an item on a ledge sometimes pushing Lara to the room below (#2372) - fixed the game crashing if a cinematic is triggered but the level contains no cinematic frames (#2413) - fixed being unable to load a level that contains no sound effect data (#2460) - fixed issues with sound effects not playing or looping forever in some cases when many other effects are playing (#2494) - fixed Lara activating triggers one frame too early (#2205, regression from 0.7) - fixed savegame incompatibility with OG (#2271, regression from 0.8) - fixed stopwatch showing wrong UI in some circumstances (#2221, regression from 0.8) - fixed excessive braid movement when dead in windy rooms (#2265, regression from 0.8) - fixed item counter shown even for a single medipack (#2222, regression from 0.3) - fixed item counter always hidden in NG+, even for keys (#2223, regression from 0.3) - fixed the passport object not being selected when exiting to title (#2192, regression from 0.8) - fixed the upside-down camera fix to no longer limit Lara's vision (#2276, regression from 0.8) - fixed /kill command freezing the game under rare circumstances (#2297, regression from 0.3) - fixed wireframe mode discarding transparent pixels (#2315, regression from 0.7) - fixed sprite pickups not being paused in the pause/inventory screen (#2319, regression from 0.6) - fixed Skidoo snow wake effects at slow speeds (#2324, regression from 0.6) - fixed software renderer skybox occlusion issues (#2343, regression from 0.7) - fixed gunflare from bandits in Tibetan levels spawning too far from their guns (#2365, regression from 0.8) - fixed guns sometimes appearing in Lara's hands when entering the fly cheat while undrawing weapons (#2376, regression from 0.3) - fixed the `/play` console command not resetting Lara's inventory (#2267, regression from 0.3) - fixed flashing text when trying to exit passport while Lara is dead and an action is required (#2263) ## [0.8](https://github.com/LostArtefacts/TRX/compare/tr2-0.8...tr2-0.8) - 2025-01-01 - completed decompilation efforts – TR2X.dll is gone, Tomb2.exe no longer needed (#1694) - added the ability to set user-defined FOV (no UI for it yet) (#2177) - added the ability to turn FMVs off (#2110) - added an option to use PS1 contrast levels, available under F8 (#1646) - added an option to use TR3+ side steps (#2111) - added an option to allow disabling the developer console (#2063) - added an optional fix for the QWOP glitch (#2122) - added an optional fix for the step glitch, where Lara can be pushed into walls (#2124) - added an optional fix for drawing a free flare during the underwater pickup animation (#2123) - added an optional fix for Lara drifting into walls when collecting underwater items (#2096) - added an option to control how music is played while underwater (#1937) - added an optional demo number argument to the `/demo` command - added an option to set the bar scaling (no UI for it yet) (#1636) - added an option to set the text scaling (no UI for it yet) (#1636) - improved the animation of Lara's braid (#2094) - changed demo to be interrupted only by esc or action keys - changed the turbo cheat to also affect ingame timer (#2167) - fixed health bar and air bar scaling (#2149) - fixed text being stretched on non-4:3 aspect ratios (#2012) - fixed Lara prioritising throwing a spent flare while mid-air, so to avoid missing ledge grabs (#1989) - fixed Lara at times not being able to jump immediately after going from her walking to running animation (#1587) - fixed bubbles spawning from flares if Lara is in shallow water (#1590) - fixed flare sound effects not always playing when Lara is in shallow water (#1590) - fixed looking forward too far causing an upside down camera frame (#1594) - fixed music not playing if triggered while the game is muted, but the volume is then increased (#2170) - fixed game FOV being interpreted as horizontal (#2002) - fixed the inventory up arrow at times overlapping the health bar (#2180) - fixed software renderer not applying underwater tint (#2066, regression from 0.7) - fixed some enemies not looking at Lara (#2080, regression from 0.6) - fixed the camera getting stuck at the start of Home Sweet Home (#2129, regression from 0.7) - fixed assault course timer not paused in the inventory (#2153, regression from 0.6) - fixed Lara spawning air bubbles above water surfaces during the fly cheat (#2115, regression from 0.3) - fixed demos playing too eagerly (#2068, regression from 0.3) - fixed Lara sometimes being unable to use switches (#2184, regression from 0.6) - fixed Lara interacting with airlock switches in unexpected ways (#2186, regression from 0.6) - fixed input controller remaps not being saved across game relaunches (#2422, regression from 0.6) ## [0.7.1](https://github.com/LostArtefacts/TRX/compare/tr2-0.7...tr2-0.7.1) - 2024-12-17 - fixed a crash when selecting the sound option (#2057, regression from 0.6) ## [0.7](https://github.com/LostArtefacts/TRX/compare/tr2-0.6...tr2-0.7) - 2024-12-16 - switched to OpenGL rendering (#1844) - improved support for non-4:3 aspect ratios (#1647) - changed fullscreen behavior to use windowed desktop mode (#1643) - added an option for 1-2-3-4× pixel upscaling (available under the F1/Shift-F1 key) - added the ability to use the window border option at all times (available under the F2/Shift-F2 key) - added the ability to toggle between the software/hardware renderer at runtime (available under the F12 key) - added fade effects to the hardware renderer (#1623) - added an informative text when toggling various rendering options at runtime (#1873) - added a wireframe mode (available with `/set` console command and with Shift+F7) - changed the software renderer to use the picture's palette for the background pictures - changed the hardware renderer to always use 16-bit textures (#1558) - fixed texture corruption after FMVs play (#1562) - fixed black borders in windowed mode (#1645) - fixed "Failed to create device" when toggling fullscreen (#1842) - fixed distant rooms sometimes not appearing, causing the skybox to be visible when it shouldn't (#2000) - fixed rendering problems on certain Intel GPUs (#1574) - replaced the Windows Registry configuration with .json files - removed setup dialog support (using `Tomb2.exe -setup` will have no effect on TR2X) - removed unused detail level option - removed triple buffering option - removed dither option - added support for custom levels to enforce values for any config setting (#1846) - added an option to fix inventory item usage duplication (#1586) - added optional automatic key/puzzle inventory item pre-selection (#1884) - added a search feature to the config tool (#1889) - added an option to fix rotation on some pickup items to better suit 3D pickup mode (#1613) - added background for the final game stats (#1584) - added the ability to turn fade effects on/off (#1623) - removed unused detail level option - fixed a crash when trying to draw too many rooms at once (#1998) - fixed Lara getting stuck in her hit animation if she is hit while mounting the boat or skidoo (#1606) - fixed pistols appearing in Lara's hands when entering the fly cheat during certain animations (#1874) - fixed wrongly calculated trapdoor size that could affect custom levels (#1904) - fixed one of the collapsible tiles in Opera House room 184 not triggering (#1902) - fixed being unable to use the drawbridge key in Tibetan Foothills after the flipmap (#1744) - fixed missing triggers and ladder in Catacombs of the Talion after the flipmap (#1960) - fixed incorrect music trigger types at the beginning of Catacombs of the Talion (#1962) - fixed missing death tiles in Temple of Xian room 91 (#1920) - fixed the detonator key and gong hammer not activating their target items when manually selected from the inventory (#1887) - fixed wrongly positioned doors in Ice Palace and Floating Islands, which caused invisible walls (#1963) - fixed picking up the Gong Hammer in Ice Palace sometimes not opening the nearby door (#1716) - fixed room 98 in Wreck of the Maria Doria not having water (#1939) - fixed a potential crash if Lara is on the skidoo in a room with many other adjoining rooms (#1987) - fixed a softlock in Home Sweet Home if the final cutscene is triggered while Lara is on water surface (#1701) - fixed Lara's left arm becoming stuck if a flare is drawn just before the final cutscene in Home Sweet Home (#1992) - fixed resizing game window on the stats dialog cloning the UI elements, eventually crashing the game (#1999) - fixed exiting the game with Alt+F4 not immediately working in cutscenes - fixed game freezing when starting demo/credits/inventory offscreen - fixed problems when trying to launch the game with High DPI mode enabled (#1845) - fixed clock drift accumulating with time, causing audio desync in cutscenes (#1935, regression from 0.6) - fixed controllers dialog missing background in the software renderer mode (#1978, regression from 0.6) - fixed a crash relating to audio decoding (#1895, regression from 0.2) - fixed depth problems when drawing certain rooms (#1853, regression from 0.6) - fixed being unable to go from surface swimming to underwater swimming without first stopping (#1863, regression from 0.6) - fixed Lara continuing to walk after being killed if in that animation (#1880, regression from 0.1) - fixed some music tracks looping while Lara remained on the same trigger tile (#1899, regression from 0.2) - fixed some music tracks not playing if they were the last played track and the level had no ambience (#1899, regression from 0.2) - fixed broken final stats screen in software rendering mode (#1915, regression from 0.6) - fixed screenshots not capturing level stats (#1925, regression from 0.6) - fixed screenshots sometimes crashing in the windowed mode (regression from 0.6) - fixed creatures being able to swim/fly above the ceiling up to one tile (#1936, regression from 0.1) - fixed the `/kill all` command reporting an incorrect count in some levels (#1995, regression from 0.3) ## [0.6](https://github.com/LostArtefacts/TRX/compare/tr2-0.5...tr2-0.6) - 2024-11-06 - added a fly cheat key (#1642) - added an items cheat key (#1641) - added a level skip cheat key (#1640) - added a turbo cheat (#1639) - added the ability to skip end credits with the action and escape keys (#1800) - added the ability to skip FMVs with the action key (#1650) - added the ability to hold forward/back to move through menus more quickly (#1644) - added optional rendering of pickups in the UI as 3D meshes (#1633) - added optional rendering of pickups on the ground as 3D meshes (#1634) - added a special target, "pickup", to item-based console commands - changed the inputs backend from DirectX to SDL (#1695) - improved controller support to match TR1X - changed the number of custom layouts to 3 - changed default key bindings according to the following table: | Key | Old binding | New binding | Reason | ----------------------------- | ----------- | ------------ | ----- | Flare | Comma (,) | Period (.) | To maintain forward compatibility with TR3 | Screenshot | S | Print Screen | To maintain compatibility with TR1X | Toggle bilinear filter | F8 | F3 | To maintain compatibility with TR1X | Toggle perspective filter | Shift+F8 | F4 | To maintain compatibility with TR1X | Toggle z-buffer | F7 | F7 | Likely to be permanently enabled in the future | Toggle triple buffering | Shift+F7 | **Removed** | Obscure setting, will be either removed or available via the ingame UI at some point | Toggle dither | F11 | **Removed** | Obscure setting, will be either removed or available via the ingame UI at some point | Toggle fullscreen | F12 | Alt-Enter | To maintain compatibility with TR1X | Toggle rendering mode | Shift+F12 | F12 | No more conflict to require Shift | Decrease resolution | F1 | Shift+F1 | F3 and F4 are already taken | Increase resolution | F2 | F1 | F3 and F4 are already taken | Decrease internal screen size | F3 | Shift+F2 | F3 and F4 are already taken | Increase internal screen size | F4 | F2 | F3 and F4 are already taken - removed "falling through" to the default layout, with the exception of keyboard arrows (matching TR1X behavior) - removed hardcoded Shift+F7 key binding for toggling triple buffering - removed hardcoded `0` key binding for flares - removed hardcoded cooldown of 15 frames for medipacks - changed text backend to accept named sequences (eg. "\{arrow up}" and similar) - changed inventory to pause the music rather than muting it (#1707) - changed the `/pos` command to include the level number and title - changed the `/tp` command to teleport to items in a round-robin fashion The first call will teleport Lara to the object that's the closest to her; repeated calls will cycle through all matching objects in the object placement order. - improved FMV mode appearance - removed black scanlines (#1729) - improved FMV mode behavior - stopped switching screen resolutions (#1729) - improved screenshots: now saved in the screenshots/ directory with level titles and timestamps as JPG or PNG, similar to TR1X (#1773) - improved switch object names - Switch Type 1 renamed to "Airlock Switch" - Switch Type 2 renamed to "Small Switch" - Switch Type 3 renamed to "Switch Button" - Switch Type 4 renamed to "Lever/Switch" - Switch Type 5 renamed to "Underwater Lever/Switch" - fixed screenshots not working in windowed mode (#1766) - fixed screenshots key not getting debounced (#1773) - fixed `/give` not working with weapons (regression from 0.5) - fixed the camera being cut off after using the gong hammer in Ice Palace (#1580) - fixed the audio not being in sync when Lara strikes the gong in Ice Palace (#1725) - fixed door cheat not working with drawbridges (#1748) - fixed certain audio samples continuing to play after finishing the level (#1770, regression from 0.2) - fixed Lara's underwater hue being retained when re-entering a boat (#1596) - fixed Lara reloading the harpoon gun after every shot in NG+ (#1575) - fixed the dragon reviving itself after Lara removes the dagger in rare circumstances (#1572) - fixed grenades counting as double kills in the game statistics (#1560) - fixed the ammo counter being hidden while a demo plays in NG+ (#1559) - fixed the game crashing in large rooms with z-buffer disabled (#1761, regression from 0.2) - fixed the game hanging if exited during the level stats, credits, or final stats (#1585) - fixed the console not being drawn during credits (#1802) - fixed grenades launched at too slow speeds (#1760, regression from 0.3) - fixed the dragon counting as more than one kill if allowed to revive (#1771) - fixed a crash when firing grenades at Xian guards in statue form (#1561) - fixed harpoon bolts damaging inactive enemies (#1804) - fixed enemies that are run over by the skidoo not being counted in the statistics (#1772) - fixed sound settings resuming the music (#1707) - fixed being able to use hotkeys in the end-level statistics screen - fixed the inventory ring spinout animation sometimes running too fast (#1704, regression from 0.3) - fixed new saves not displaying the save count in the passport (#1591) - fixed certain erroneous `/play` invocations resulting in duplicated error messages ## [0.5](https://github.com/LostArtefacts/TRX/compare/afaf12a...tr2-0.5) - 2024-10-08 - added `/sfx` command - added `/nextlevel` alias to `/endlevel` console command - added `/quit` alias to `/exit` console command - added the ability to cycle through console prompt history (#1571) - improved vertex movement when looking through water portals (#1493) - improved console commands targeting creatures and pickups (#1667) - changed `/set` console command to do fuzzy matching (LostArtefacts/libtrx#38) - fixed crash in the `/set` console command (regression from 0.3) - fixed using console in cutscenes immediately exiting the game (regression from 0.3) - fixed Lara remaining tilted when teleporting off a vehicle while on a slope (LostArtefacts/TR2X#275, regression from 0.3) - fixed `/endlevel` displaying a success message in the title screen - fixed very loud music volume set by default (#1614) ## [0.4] Version 0.4 was skipped because of a major repository merge with TR1X into TRX. ## [0.3](https://github.com/LostArtefacts/TR2X/compare/0.2...0.3) - 2024-09-20 - added new console commands: - `/endlevel` - `/demo` - `/title` - `/play [level]` - `/load [slot]` - `/save [slot]` - `/exit` - `/fly` - `/give` - `/kill` - `/flip` - `/set` - added the ability to remap the console key (LostArtefacts/TR2X#163) - added `/tp` console command's ability to teleport to specific items - added `/fly` console command's ability to open nearest doors - added an option to fix M16 accuracy while running (LostArtefacts/TR2X#45) - added a .NET-based configuration tool (LostArtefacts/TR2X#197) - improved initial level load time by lazy-loading audio samples (LostArtefacts/TR2X#114) - improved crash debug information (LostArtefacts/TR2X#137) - improved the console caret sprite (LostArtefacts/TR2X#91) - changed the default flare key from `/` to `.` to avoid conflicts with the console (LostArtefacts/TR2X#163) - fixed numeric keys interfering with the demos (LostArtefacts/TR2X#172) - fixed explosions sometimes being drawn too dark (LostArtefacts/TR2X#187) - fixed killing the T-Rex with a grenade launcher crashing the game (LostArtefacts/TR2X#168) - fixed secret rewards not displaying shotgun ammo (LostArtefacts/TR2X#159) - fixed controls dialog remapping being too sensitive (LostArtefacts/TR2X#5) - fixed `/tp` console command during special animations in HSH and Offshore Rig (LostArtefacts/TR2X#178, regression from 0.2) - fixed `/hp` console command taking arbitrary integers - fixed console commands being able to interfere with demos, cutscenes and the title screen (LostArtefacts/TR2X#182, #179, regression from 0.2) - fixed console registering key inputs too eagerly (regression from 0.2) - fixed console not being drawn in cutscenes (LostArtefacts/TR2X#180, regression from 0.2) - fixed sounds not playing under certain circumstances (LostArtefacts/TR2X#113, regression from 0.2) - fixed the excessive pitch and playback speed correction for music files with sampling rate other than 44100 Hz (LostArtefacts/TR1X#1417, regression from 0.2) - fixed a crash potential with certain music files (regression from 0.2) - fixed enemy movement patterns in demo 1 and demo 3 (LostArtefacts/TR2X#98, regression from 0.1) - fixed underwater creatures dying (LostArtefacts/TR2X#98, regression from 0.1) - fixed a crash when spawning enemy drops (LostArtefacts/TR2X#125, regression from 0.1) - fixed how sprites are shaded (LostArtefacts/TR2X#134, regression from 0.1.1) - fixed enemies unable to climb (LostArtefacts/TR2X#138, regression from 0.1) - fixed items not being reset between level loads (LostArtefacts/TR2X#142, regression from 0.1) - fixed pulling the dagger from the dragon not activating triggers (LostArtefacts/TR2X#148, regression from 0.1) - fixed the music at the beginning of Offshore Rig not playing (LostArtefacts/TR2X#150, regression from 0.1) - fixed wade animation when moving from deep to shallow water (LostArtefacts/TR2X#231, regression from 0.1) - fixed the distorted skybox in room 5 of Barkhang Monastery (LostArtefacts/TR2X#196) ## [0.2](https://github.com/LostArtefacts/TR2X/compare/0.1.1...0.2) - 2024-05-07 - added dev console with the following commands: - `/pos` - `/tp [room_num]` - `/tp [x] [y] [z]` - `/hp` - `/hp [num]` - `/heal` - changed the music backend from WinMM to libtrx (SDL + libav) - changed the sound backend from DirectX to libtrx (SDL + libav) - fixed seams around underwater portals (LostArtefacts/TR2X#76, regression from 0.1) - fixed Lara's climb down camera angle (LostArtefacts/TR2X#78, regression from 0.1) - fixed healthbar and airbar flashing the wrong way when at low values (LostArtefacts/TR2X#82, regression from 0.1) ## [0.1.1](https://github.com/LostArtefacts/TR2X/compare/0.1...0.1.1) - 2024-04-27 - fixed Lara's shadow with z-buffer option on (LostArtefacts/TR2X#64, regression from 0.1) - fixed rare camera issues (LostArtefacts/TR2X#65, regression from 0.1) - fixed flat rectangle colors (LostArtefacts/TR2X#70, regression from 0.1) - fixed medipacks staying open after use in Lara's inventory (LostArtefacts/TR2X#69, regression from 0.1) - fixed pickup sprites UI drawn forever in Lara's Home (LostArtefacts/TR2X#68, regression from 0.1) ## [0.1](https://github.com/rr-/TR2X/compare/...0.1) - 2024-04-26 - added version string to the inventory - fixed CDAudio not playing on certain versions (uses PaulD patch) - fixed TGA screenshots crashing the game ================================================ FILE: docs/tr2/INSTALLING.md ================================================ # Windows (installer) ## Installing (simplified) 1. Download the latest TRX installer for TR2 (e.g. `TRX-1.0-Windows_Installer-tr2.exe`). 2. Mark the installer EXE as safe to run: - Right-click on the `.exe`. - Go to properties. - Click "Unblock". 3. Run the installer and proceed with the steps. > [!NOTE] > When downloading TRX, you might see a warning from Windows Defender, your browser, or another security tool. Modern antivirus systems use AI‑based heuristics – they flag anything uncommon or unsigned as suspicious, even if it's perfectly safe. TRX can trigger these alerts because: > > - It isn't signed with a costly commercial certificate. > - It's a niche, community‑built project, so not widely recognized. > - It's a custom build, not from the Microsoft Store. > > Don't worry: TRX is open‑source, and you can inspect the code yourself on [GitHub](https://github.com/LostArtefacts/TRX/). # Windows / Linux ## Installing (manual) 1. Download the TRX zip file. 2. Extract the zip file into a directory of your choice. Make sure you choose to overwrite existing directories and files. 3. If installing for the first time – put your original game files into the target directory. Optionally, you can also install the Golden Mask expansion pack files. Extract the contents of the following zip into the target directory: https://lostartefacts.dev/aux/tr2x/trgm.zip ## Verifying the installation If you install everything correctly, your game directory should look more or less like this (click to expand):
.
├── cfg
│   ├── presets
│   │   ├── tr1-pc.json5
│   │   ├── tr1-ps1.json5
│   │   ├── tr2-pc.json5
│   │   ├── tr2-ps1.json5
│   │   ├── tr3-pc.json5
│   │   └── tr3-ps1.json5
│   ├── tr2
│   │   ├── gameflow.json5
│   │   ├── strings-de.json5
│   │   ├── strings-en-gb.json5
│   │   ├── strings-fr.json5
│   │   ├── strings-gd.json5
│   │   ├── strings-it.json5
│   │   ├── strings-pl.json5
│   │   └── strings.json5
│   ├── tr2-gm
│   │   ├── gameflow.json5
│   │   ├── strings-de.json5
│   │   ├── strings-fr.json5
│   │   ├── strings-gd.json5
│   │   ├── strings-it.json5
│   │   ├── strings-pl.json5
│   │   └── strings.json5
│   ├── tr2-level
│   │   ├── gameflow.json5
│   │   ├── strings-de.json5
│   │   ├── strings-fr.json5
│   │   ├── strings-gd.json5
│   │   ├── strings-it.json5
│   │   ├── strings-pl.json5
│   │   └── strings.json5
│   ├── base_strings-de.json5
│   ├── base_strings-en-gb.json5
│   ├── base_strings-fr.json5
│   ├── base_strings-gd.json5
│   ├── base_strings-it.json5
│   ├── base_strings-pl.json5
│   ├── base_strings-ru.json5
│   ├── base_strings.json5
│   ├── catalog_item_actions.csv
│   ├── catalog_lara_anims.csv
│   ├── catalog_lara_states.csv
│   ├── catalog_music.csv
│   ├── catalog_objects.csv
│   ├── catalog_samples.csv
│   ├── inv_ring.json5
│   ├── outfits.json5
│   ├── poses.json5
│   ├── TR2X.json5*
│   ├── ui.json5
│   └── weapons.json5
├── data
│   ├── images
│   │   ├── 3x2
│   │   │   ├── china.webp
│   │   │   ├── credit00_gm.webp
│   │   │   ├── credit01.webp
│   │   │   ├── credit02.webp
│   │   │   ├── credit03.webp
│   │   │   ├── credit04.webp
│   │   │   ├── credit05.webp
│   │   │   ├── credit06.webp
│   │   │   ├── credit07.webp
│   │   │   ├── credit07_gm.webp
│   │   │   ├── credit08.webp
│   │   │   ├── end.webp
│   │   │   ├── gm_level1.webp
│   │   │   ├── gm_level2.webp
│   │   │   ├── gm_level3.webp
│   │   │   ├── gm_level4.webp
│   │   │   ├── gm_level5.webp
│   │   │   ├── legal_eu.webp
│   │   │   ├── legal_eu_gm.webp
│   │   │   ├── legal_us.webp
│   │   │   ├── legal_us_gm.webp
│   │   │   ├── mansion.webp
│   │   │   ├── rig.webp
│   │   │   ├── tibet.webp
│   │   │   ├── titan.webp
│   │   │   ├── title_eu.webp
│   │   │   ├── title_eu_gm.webp
│   │   │   ├── title_us.webp
│   │   │   ├── title_us_gm.webp
│   │   │   └── venice.webp
│   │   ├── 4x3
│   │   │   ├── china.webp
│   │   │   ├── credit00_gm.webp
│   │   │   ├── credit01.webp
│   │   │   ├── credit02.webp
│   │   │   ├── credit03.webp
│   │   │   ├── credit04.webp
│   │   │   ├── credit05.webp
│   │   │   ├── credit06.webp
│   │   │   ├── credit07.webp
│   │   │   ├── credit07_gm.webp
│   │   │   ├── credit08.webp
│   │   │   ├── end.webp
│   │   │   ├── gm_level1.webp
│   │   │   ├── gm_level2.webp
│   │   │   ├── gm_level3.webp
│   │   │   ├── gm_level4.webp
│   │   │   ├── gm_level5.webp
│   │   │   ├── legal_eu.webp
│   │   │   ├── legal_eu_gm.webp
│   │   │   ├── legal_us.webp
│   │   │   ├── legal_us_gm.webp
│   │   │   ├── mansion.webp
│   │   │   ├── rig.webp
│   │   │   ├── tibet.webp
│   │   │   ├── titan.webp
│   │   │   ├── title_eu.webp
│   │   │   ├── title_eu_gm.webp
│   │   │   ├── title_us.webp
│   │   │   ├── title_us_gm.webp
│   │   │   └── venice.webp
│   │   ├── og
│   │   │   ├── china.webp
│   │   │   ├── credit00_gm.webp
│   │   │   ├── credit01.webp
│   │   │   ├── credit02.webp
│   │   │   ├── credit03.webp
│   │   │   ├── credit04.webp
│   │   │   ├── credit05.webp
│   │   │   ├── credit06.webp
│   │   │   ├── credit07.webp
│   │   │   ├── credit07_gm.webp
│   │   │   ├── credit08.webp
│   │   │   ├── end.webp
│   │   │   ├── legal.webp
│   │   │   ├── mansion.webp
│   │   │   ├── rig.webp
│   │   │   ├── tibet.webp
│   │   │   ├── titan.webp
│   │   │   ├── title_eu.webp
│   │   │   ├── title_eu_gm.webp
│   │   │   ├── title_us.webp
│   │   │   ├── title_us_gm.webp
│   │   │   └── venice.webp
│   │   ├── china.webp
│   │   ├── credit00_gm.webp
│   │   ├── credit01.webp
│   │   ├── credit02.webp
│   │   ├── credit03.webp
│   │   ├── credit04.webp
│   │   ├── credit05.webp
│   │   ├── credit06.webp
│   │   ├── credit07.webp
│   │   ├── credit07_gm.webp
│   │   ├── credit08.webp
│   │   ├── end.webp
│   │   ├── gm_level1.webp
│   │   ├── gm_level2.webp
│   │   ├── gm_level3.webp
│   │   ├── gm_level4.webp
│   │   ├── gm_level5.webp
│   │   ├── legal_eu.webp
│   │   ├── legal_eu_gm.webp
│   │   ├── legal_us.webp
│   │   ├── legal_us_gm.webp
│   │   ├── mansion.webp
│   │   ├── rig.webp
│   │   ├── tibet.webp
│   │   ├── titan.webp
│   │   ├── title_eu.webp
│   │   ├── title_eu_gm.webp
│   │   ├── title_us.webp
│   │   ├── title_us_gm.webp
│   │   └── venice.webp
│   ├── injections
│   │   ├── barkhang_cameras.bin
│   │   ├── barkhang_crystals.bin
│   │   ├── barkhang_fd.bin
│   │   ├── barkhang_itemrots.bin
│   │   ├── barkhang_music_tracks.bin
│   │   ├── barkhang_pickup_meshes.bin
│   │   ├── barkhang_textures.bin
│   │   ├── bartoli_crystals.bin
│   │   ├── bartoli_music_tracks.bin
│   │   ├── bartoli_secret_fd.bin
│   │   ├── bartoli_textures.bin
│   │   ├── boat_bits.bin
│   │   ├── breakable_tile_sfx.bin
│   │   ├── catacombs_crystals.bin
│   │   ├── catacombs_fd.bin
│   │   ├── catacombs_itemrots.bin
│   │   ├── catacombs_music_tracks.bin
│   │   ├── catacombs_textures.bin
│   │   ├── coldwar_crystals.bin
│   │   ├── coldwar_fd.bin
│   │   ├── coldwar_itemrots.bin
│   │   ├── coldwar_music_tracks.bin
│   │   ├── coldwar_objects.bin
│   │   ├── coldwar_textures.bin
│   │   ├── common_pickup_meshes.bin
│   │   ├── common_pickup_meshes_gm.bin
│   │   ├── crystal.bin
│   │   ├── cut2_setup.bin
│   │   ├── cut2_textures.bin
│   │   ├── cut3_setup.bin
│   │   ├── cut3_textures.bin
│   │   ├── cut4_setup.bin
│   │   ├── cut4_textures.bin
│   │   ├── dagger_sprite.bin
│   │   ├── deck_cameras.bin
│   │   ├── deck_crystals.bin
│   │   ├── deck_fd.bin
│   │   ├── deck_itemrots.bin
│   │   ├── deck_music_tracks.bin
│   │   ├── deck_pickup_meshes.bin
│   │   ├── deck_plants.bin
│   │   ├── deck_secret_fd.bin
│   │   ├── deck_textures.bin
│   │   ├── detonator_lights.bin
│   │   ├── diving_cameras.bin
│   │   ├── diving_crystals.bin
│   │   ├── diving_itemrots.bin
│   │   ├── diving_music_tracks.bin
│   │   ├── diving_pickup_meshes.bin
│   │   ├── diving_sfx.bin
│   │   ├── diving_textures.bin
│   │   ├── door106_sfx.bin
│   │   ├── door107_sfx.bin
│   │   ├── door108_sfx.bin
│   │   ├── door110_sfx.bin
│   │   ├── door111_sfx.bin
│   │   ├── explosion.bin
│   │   ├── fathoms_crystals.bin
│   │   ├── fathoms_goon_sfx.bin
│   │   ├── fathoms_itemrots.bin
│   │   ├── fathoms_music_tracks.bin
│   │   ├── fathoms_plants.bin
│   │   ├── fathoms_secret_fd.bin
│   │   ├── fathoms_textures.bin
│   │   ├── floating_crystals.bin
│   │   ├── floating_fd.bin
│   │   ├── floating_itemrots.bin
│   │   ├── floating_music_tracks.bin
│   │   ├── floating_pickup_meshes.bin
│   │   ├── floating_textures.bin
│   │   ├── font.bin
│   │   ├── fools_crystals.bin
│   │   ├── fools_itemrots.bin
│   │   ├── fools_music_tracks.bin
│   │   ├── fools_pickup_meshes.bin
│   │   ├── fools_textures.bin
│   │   ├── furnace_crystals.bin
│   │   ├── furnace_itemrots.bin
│   │   ├── furnace_music_tracks.bin
│   │   ├── furnace_objects.bin
│   │   ├── furnace_pickup_meshes.bin
│   │   ├── furnace_textures.bin
│   │   ├── guardian_death_commands.bin
│   │   ├── gym_fd.bin
│   │   ├── gym_music_tracks.bin
│   │   ├── gym_sfx.bin
│   │   ├── gym_textures.bin
│   │   ├── house_itemrots.bin
│   │   ├── house_music_tracks.bin
│   │   ├── house_sfx.bin
│   │   ├── house_shower_frames.bin
│   │   ├── house_textures.bin
│   │   ├── inv_background.bin
│   │   ├── kingdom_cameras.bin
│   │   ├── kingdom_crystals.bin
│   │   ├── kingdom_itemrots.bin
│   │   ├── kingdom_music_tracks.bin
│   │   ├── kingdom_textures.bin
│   │   ├── lair_bartolipos.bin
│   │   ├── lair_crystals.bin
│   │   ├── lair_music_tracks.bin
│   │   ├── lair_textures.bin
│   │   ├── lara_animations.bin
│   │   ├── lara_extra.bin
│   │   ├── lara_guns.bin
│   │   ├── lara_gym_guns.bin
│   │   ├── lara_house_guns.bin
│   │   ├── lara_outfits.bin
│   │   ├── lara_rifle_sfx.bin
│   │   ├── lara_vegas_guns.bin
│   │   ├── living_crystals.bin
│   │   ├── living_deck_goon_sfx.bin
│   │   ├── living_fd.bin
│   │   ├── living_itemrots.bin
│   │   ├── living_music_tracks.bin
│   │   ├── living_pickup_meshes.bin
│   │   ├── living_secret_fd.bin
│   │   ├── living_sfx.bin
│   │   ├── living_textures.bin
│   │   ├── loose_boards_sfx.bin
│   │   ├── misc_sprites.bin
│   │   ├── opera_crystals.bin
│   │   ├── opera_fd.bin
│   │   ├── opera_itemrots.bin
│   │   ├── opera_music_tracks.bin
│   │   ├── opera_sfx.bin
│   │   ├── opera_textures.bin
│   │   ├── palace_crystals.bin
│   │   ├── palace_fd.bin
│   │   ├── palace_itemrots.bin
│   │   ├── palace_music_tracks.bin
│   │   ├── palace_secret_fd.bin
│   │   ├── palace_textures.bin
│   │   ├── pda_model.bin
│   │   ├── photo.bin
│   │   ├── pickup_aid.bin
│   │   ├── portcullis_sfx.bin
│   │   ├── rig_crystals.bin
│   │   ├── rig_itemrots.bin
│   │   ├── rig_music_tracks.bin
│   │   ├── rig_pickup_meshes.bin
│   │   ├── rig_textures.bin
│   │   ├── scuba_sfx.bin
│   │   ├── seaweed_collision.bin
│   │   ├── secret_models_gm.bin
│   │   ├── secret_models_og.bin
│   │   ├── shark_sfx.bin
│   │   ├── tibet_crystals.bin
│   │   ├── tibet_fd.bin
│   │   ├── tibet_itemrots.bin
│   │   ├── tibet_music_tracks.bin
│   │   ├── tibet_textures.bin
│   │   ├── title_textures.bin
│   │   ├── vegas_crystals.bin
│   │   ├── vegas_fd.bin
│   │   ├── vegas_itemrots.bin
│   │   ├── vegas_music_tracks.bin
│   │   ├── vegas_textures.bin
│   │   ├── venice_crystals.bin
│   │   ├── venice_fd.bin
│   │   ├── venice_itemrots.bin
│   │   ├── venice_music_tracks.bin
│   │   ├── venice_textures.bin
│   │   ├── wall_cameras.bin
│   │   ├── wall_crystals.bin
│   │   ├── wall_itemrots.bin
│   │   ├── wall_music_tracks.bin
│   │   ├── wall_textures.bin
│   │   ├── winston_model.bin
│   │   ├── wreck_cameras.bin
│   │   ├── wreck_crystals.bin
│   │   ├── wreck_fd.bin
│   │   ├── wreck_goon_sfx.bin
│   │   ├── wreck_itemrots.bin
│   │   ├── wreck_music_tracks.bin
│   │   ├── wreck_pickup_meshes.bin
│   │   ├── wreck_plants.bin
│   │   ├── wreck_secret_fd.bin
│   │   ├── wreck_textures.bin
│   │   ├── xian_crystals.bin
│   │   ├── xian_fd.bin
│   │   ├── xian_itemrots.bin
│   │   ├── xian_music_tracks.bin
│   │   ├── xian_pickup_meshes.bin
│   │   ├── xian_sfx.bin
│   │   └── xian_textures.bin
│   ├── scripts
│   │   ├── assault.lua
│   │   ├── cut3.lua
│   │   ├── floating.lua
│   │   ├── house.lua
│   │   ├── level1.lua
│   │   ├── level3.lua
│   │   ├── level4.lua
│   │   └── monastry.lua
│   ├── assault.tr2
│   ├── boat.tr2
│   ├── catacomb.tr2
│   ├── cut1.tr2
│   ├── cut2.tr2
│   ├── cut3.tr2
│   ├── cut4.tr2
│   ├── deck.tr2
│   ├── emprtomb.tr2
│   ├── floating.tr2
│   ├── house.tr2
│   ├── icecave.tr2
│   ├── keel.tr2
│   ├── level1.tr2
│   ├── level2.tr2
│   ├── level3.tr2
│   ├── level4.tr2
│   ├── level5.tr2
│   ├── living.tr2
│   ├── main.sfx
│   ├── main_gm.sfx
│   ├── monastry.tr2
│   ├── opera.tr2
│   ├── platform.tr2
│   ├── rig.tr2
│   ├── skidoo.tr2
│   ├── title.tr2
│   ├── title_gm.tr2
│   ├── unwater.tr2
│   ├── venice.tr2
│   ├── wall.tr2
│   └── xian.tr2
├── fmv
│   ├── ancient.rpl
│   ├── crash.rpl
│   ├── end.rpl
│   ├── jeep.rpl
│   ├── landing.rpl
│   ├── logo.rpl
│   ├── modern.rpl
│   └── ms.rpl
├── music
│   ├── 2.mp3
│   ├── 3.mp3
│   ├── 4.mp3
│   ├── 5.mp3
│   ├── 6.mp3
│   ├── 7.mp3
│   ├── 8.mp3
│   ├── 9.mp3
│   ├── 10.mp3
│   ├── 11.mp3
│   ├── 12.mp3
│   ├── 13.mp3
│   ├── 14.mp3
│   ├── 15.mp3
│   ├── 16.mp3
│   ├── 17.mp3
│   ├── 18.mp3
│   ├── 19.mp3
│   ├── 20.mp3
│   ├── 21.mp3
│   ├── 22.mp3
│   ├── 23.mp3
│   ├── 24.mp3
│   ├── 25.mp3
│   ├── 26.mp3
│   ├── 27.mp3
│   ├── 28.mp3
│   ├── 29.mp3
│   ├── 30.mp3
│   ├── 31.mp3
│   ├── 32.mp3
│   ├── 33.mp3
│   ├── 34.mp3
│   ├── 35.mp3
│   ├── 36.mp3
│   ├── 37.mp3
│   ├── 38.mp3
│   ├── 39.mp3
│   ├── 40.mp3
│   ├── 41.mp3
│   ├── 42.mp3
│   ├── 43.mp3
│   ├── 44.mp3
│   ├── 45.mp3
│   ├── 46.mp3
│   ├── 47.mp3
│   ├── 48.mp3
│   ├── 49.mp3
│   ├── 50.mp3
│   ├── 51.mp3
│   ├── 52.mp3
│   ├── 53.mp3
│   ├── 54.mp3
│   ├── 55.mp3
│   ├── 56.mp3
│   ├── 57.mp3
│   ├── 58.mp3
│   ├── 59.mp3
│   ├── 60.mp3
│   └── 61.mp3
├── shaders
│   ├── 2d.glsl
│   ├── billboard.glsl
│   ├── common.glsl
│   ├── fbo.glsl
│   ├── lights.glsl
│   ├── meshes.glsl
│   ├── meshes_tr3.glsl
│   ├── meshes_tr12.glsl
│   └── ui.glsl
└── TRX.exe
*\* Will not be present until the game has been launched.* ## Playing the game - To play the game, run `TRX.exe`. - To play the Golden Mask expansion pack, run `TRX.exe --gold`. # macOS ## Installing 1. Download the latest TRX for TR2 installer image (e.g `TRX-0.1-Mac-tr2.dmg`). Mount the image and drag TR2X to the Applications folder. 2. Run TR2X from the Applications folder. This will show you an error dialog about missing game data files. This is expected at this point, as you have not copied them in yet. However, it's important to run the app first to allow macOS to verify the app bundle's signature. 3. Find TR2X in your Applications folder. Right-click it and click "Show Package Contents". 4. Copy your Tomb Raider 2 game data files into `Contents/Resources`. (See the Windows / Linux instructions for retrieving game data from e.g. GOG.) In case you see a popup "TR2X is damaged" when you run the game, run `xattr -cr /Applications/TR2X.app`. ## Verifying the installation If you install everything correctly, your game directory should look more or less like this (click to expand):
.
└── Contents
    ├── Resources
    │   ├── cfg
    │   │   ├── presets
    │   │   │   ├── tr1-pc.json5
    │   │   │   ├── tr1-ps1.json5
    │   │   │   ├── tr2-pc.json5
    │   │   │   ├── tr2-ps1.json5
    │   │   │   ├── tr3-pc.json5
    │   │   │   └── tr3-ps1.json5
    │   │   ├── tr2
    │   │   │   ├── gameflow.json5
    │   │   │   ├── strings-de.json5
    │   │   │   ├── strings-en-gb.json5
    │   │   │   ├── strings-fr.json5
    │   │   │   ├── strings-gd.json5
    │   │   │   ├── strings-it.json5
    │   │   │   ├── strings-pl.json5
    │   │   │   └── strings.json5
    │   │   ├── tr2-gm
    │   │   │   ├── gameflow.json5
    │   │   │   ├── strings-de.json5
    │   │   │   ├── strings-fr.json5
    │   │   │   ├── strings-gd.json5
    │   │   │   ├── strings-it.json5
    │   │   │   ├── strings-pl.json5
    │   │   │   └── strings.json5
    │   │   ├── tr2-level
    │   │   │   ├── gameflow.json5
    │   │   │   ├── strings-de.json5
    │   │   │   ├── strings-fr.json5
    │   │   │   ├── strings-gd.json5
    │   │   │   ├── strings-it.json5
    │   │   │   ├── strings-pl.json5
    │   │   │   └── strings.json5
    │   │   ├── base_strings-de.json5
    │   │   ├── base_strings-en-gb.json5
    │   │   ├── base_strings-fr.json5
    │   │   ├── base_strings-gd.json5
    │   │   ├── base_strings-it.json5
    │   │   ├── base_strings-pl.json5
    │   │   ├── base_strings-ru.json5
    │   │   ├── base_strings.json5
    │   │   ├── catalog_item_actions.csv
    │   │   ├── catalog_lara_anims.csv
    │   │   ├── catalog_lara_states.csv
    │   │   ├── catalog_music.csv
    │   │   ├── catalog_objects.csv
    │   │   ├── catalog_samples.csv
    │   │   ├── inv_ring.json5
    │   │   ├── outfits.json5
    │   │   ├── poses.json5
    │   │   ├── ui.json5
    │   │   └── weapons.json5
    │   ├── data
    │   │   ├── images
    │   │   │   ├── 3x2
    │   │   │   │   ├── china.webp
    │   │   │   │   ├── credit00_gm.webp
    │   │   │   │   ├── credit01.webp
    │   │   │   │   ├── credit02.webp
    │   │   │   │   ├── credit03.webp
    │   │   │   │   ├── credit04.webp
    │   │   │   │   ├── credit05.webp
    │   │   │   │   ├── credit06.webp
    │   │   │   │   ├── credit07.webp
    │   │   │   │   ├── credit07_gm.webp
    │   │   │   │   ├── credit08.webp
    │   │   │   │   ├── end.webp
    │   │   │   │   ├── gm_level1.webp
    │   │   │   │   ├── gm_level2.webp
    │   │   │   │   ├── gm_level3.webp
    │   │   │   │   ├── gm_level4.webp
    │   │   │   │   ├── gm_level5.webp
    │   │   │   │   ├── legal_eu.webp
    │   │   │   │   ├── legal_eu_gm.webp
    │   │   │   │   ├── legal_us.webp
    │   │   │   │   ├── legal_us_gm.webp
    │   │   │   │   ├── mansion.webp
    │   │   │   │   ├── rig.webp
    │   │   │   │   ├── tibet.webp
    │   │   │   │   ├── titan.webp
    │   │   │   │   ├── title_eu.webp
    │   │   │   │   ├── title_eu_gm.webp
    │   │   │   │   ├── title_us.webp
    │   │   │   │   ├── title_us_gm.webp
    │   │   │   │   └── venice.webp
    │   │   │   ├── 4x3
    │   │   │   │   ├── china.webp
    │   │   │   │   ├── credit00_gm.webp
    │   │   │   │   ├── credit01.webp
    │   │   │   │   ├── credit02.webp
    │   │   │   │   ├── credit03.webp
    │   │   │   │   ├── credit04.webp
    │   │   │   │   ├── credit05.webp
    │   │   │   │   ├── credit06.webp
    │   │   │   │   ├── credit07.webp
    │   │   │   │   ├── credit07_gm.webp
    │   │   │   │   ├── credit08.webp
    │   │   │   │   ├── end.webp
    │   │   │   │   ├── gm_level1.webp
    │   │   │   │   ├── gm_level2.webp
    │   │   │   │   ├── gm_level3.webp
    │   │   │   │   ├── gm_level4.webp
    │   │   │   │   ├── gm_level5.webp
    │   │   │   │   ├── legal_eu.webp
    │   │   │   │   ├── legal_eu_gm.webp
    │   │   │   │   ├── legal_us.webp
    │   │   │   │   ├── legal_us_gm.webp
    │   │   │   │   ├── mansion.webp
    │   │   │   │   ├── rig.webp
    │   │   │   │   ├── tibet.webp
    │   │   │   │   ├── titan.webp
    │   │   │   │   ├── title_eu.webp
    │   │   │   │   ├── title_eu_gm.webp
    │   │   │   │   ├── title_us.webp
    │   │   │   │   ├── title_us_gm.webp
    │   │   │   │   └── venice.webp
    │   │   │   ├── og
    │   │   │   │   ├── china.webp
    │   │   │   │   ├── credit00_gm.webp
    │   │   │   │   ├── credit01.webp
    │   │   │   │   ├── credit02.webp
    │   │   │   │   ├── credit03.webp
    │   │   │   │   ├── credit04.webp
    │   │   │   │   ├── credit05.webp
    │   │   │   │   ├── credit06.webp
    │   │   │   │   ├── credit07.webp
    │   │   │   │   ├── credit07_gm.webp
    │   │   │   │   ├── credit08.webp
    │   │   │   │   ├── end.webp
    │   │   │   │   ├── legal.webp
    │   │   │   │   ├── mansion.webp
    │   │   │   │   ├── rig.webp
    │   │   │   │   ├── tibet.webp
    │   │   │   │   ├── titan.webp
    │   │   │   │   ├── title_eu.webp
    │   │   │   │   ├── title_eu_gm.webp
    │   │   │   │   ├── title_us.webp
    │   │   │   │   ├── title_us_gm.webp
    │   │   │   │   └── venice.webp
    │   │   │   ├── china.webp
    │   │   │   ├── credit00_gm.webp
    │   │   │   ├── credit01.webp
    │   │   │   ├── credit02.webp
    │   │   │   ├── credit03.webp
    │   │   │   ├── credit04.webp
    │   │   │   ├── credit05.webp
    │   │   │   ├── credit06.webp
    │   │   │   ├── credit07.webp
    │   │   │   ├── credit07_gm.webp
    │   │   │   ├── credit08.webp
    │   │   │   ├── end.webp
    │   │   │   ├── gm_level1.webp
    │   │   │   ├── gm_level2.webp
    │   │   │   ├── gm_level3.webp
    │   │   │   ├── gm_level4.webp
    │   │   │   ├── gm_level5.webp
    │   │   │   ├── legal_eu.webp
    │   │   │   ├── legal_eu_gm.webp
    │   │   │   ├── legal_us.webp
    │   │   │   ├── legal_us_gm.webp
    │   │   │   ├── mansion.webp
    │   │   │   ├── rig.webp
    │   │   │   ├── tibet.webp
    │   │   │   ├── titan.webp
    │   │   │   ├── title_eu.webp
    │   │   │   ├── title_eu_gm.webp
    │   │   │   ├── title_us.webp
    │   │   │   ├── title_us_gm.webp
    │   │   │   └── venice.webp
    │   │   ├── injections
    │   │   │   ├── barkhang_cameras.bin
    │   │   │   ├── barkhang_crystals.bin
    │   │   │   ├── barkhang_fd.bin
    │   │   │   ├── barkhang_itemrots.bin
    │   │   │   ├── barkhang_music_tracks.bin
    │   │   │   ├── barkhang_pickup_meshes.bin
    │   │   │   ├── barkhang_textures.bin
    │   │   │   ├── bartoli_crystals.bin
    │   │   │   ├── bartoli_music_tracks.bin
    │   │   │   ├── bartoli_secret_fd.bin
    │   │   │   ├── bartoli_textures.bin
    │   │   │   ├── boat_bits.bin
    │   │   │   ├── breakable_tile_sfx.bin
    │   │   │   ├── catacombs_crystals.bin
    │   │   │   ├── catacombs_fd.bin
    │   │   │   ├── catacombs_itemrots.bin
    │   │   │   ├── catacombs_music_tracks.bin
    │   │   │   ├── catacombs_textures.bin
    │   │   │   ├── coldwar_crystals.bin
    │   │   │   ├── coldwar_fd.bin
    │   │   │   ├── coldwar_itemrots.bin
    │   │   │   ├── coldwar_music_tracks.bin
    │   │   │   ├── coldwar_objects.bin
    │   │   │   ├── coldwar_textures.bin
    │   │   │   ├── common_pickup_meshes.bin
    │   │   │   ├── common_pickup_meshes_gm.bin
    │   │   │   ├── crystal.bin
    │   │   │   ├── cut2_setup.bin
    │   │   │   ├── cut2_textures.bin
    │   │   │   ├── cut3_setup.bin
    │   │   │   ├── cut3_textures.bin
    │   │   │   ├── cut4_setup.bin
    │   │   │   ├── cut4_textures.bin
    │   │   │   ├── dagger_sprite.bin
    │   │   │   ├── deck_cameras.bin
    │   │   │   ├── deck_crystals.bin
    │   │   │   ├── deck_fd.bin
    │   │   │   ├── deck_itemrots.bin
    │   │   │   ├── deck_music_tracks.bin
    │   │   │   ├── deck_pickup_meshes.bin
    │   │   │   ├── deck_plants.bin
    │   │   │   ├── deck_secret_fd.bin
    │   │   │   ├── deck_textures.bin
    │   │   │   ├── detonator_lights.bin
    │   │   │   ├── diving_cameras.bin
    │   │   │   ├── diving_crystals.bin
    │   │   │   ├── diving_itemrots.bin
    │   │   │   ├── diving_music_tracks.bin
    │   │   │   ├── diving_pickup_meshes.bin
    │   │   │   ├── diving_sfx.bin
    │   │   │   ├── diving_textures.bin
    │   │   │   ├── door106_sfx.bin
    │   │   │   ├── door107_sfx.bin
    │   │   │   ├── door108_sfx.bin
    │   │   │   ├── door110_sfx.bin
    │   │   │   ├── door111_sfx.bin
    │   │   │   ├── explosion.bin
    │   │   │   ├── fathoms_crystals.bin
    │   │   │   ├── fathoms_goon_sfx.bin
    │   │   │   ├── fathoms_itemrots.bin
    │   │   │   ├── fathoms_music_tracks.bin
    │   │   │   ├── fathoms_plants.bin
    │   │   │   ├── fathoms_secret_fd.bin
    │   │   │   ├── fathoms_textures.bin
    │   │   │   ├── floating_crystals.bin
    │   │   │   ├── floating_fd.bin
    │   │   │   ├── floating_itemrots.bin
    │   │   │   ├── floating_music_tracks.bin
    │   │   │   ├── floating_pickup_meshes.bin
    │   │   │   ├── floating_textures.bin
    │   │   │   ├── font.bin
    │   │   │   ├── fools_crystals.bin
    │   │   │   ├── fools_itemrots.bin
    │   │   │   ├── fools_music_tracks.bin
    │   │   │   ├── fools_pickup_meshes.bin
    │   │   │   ├── fools_textures.bin
    │   │   │   ├── furnace_crystals.bin
    │   │   │   ├── furnace_itemrots.bin
    │   │   │   ├── furnace_music_tracks.bin
    │   │   │   ├── furnace_objects.bin
    │   │   │   ├── furnace_pickup_meshes.bin
    │   │   │   ├── furnace_textures.bin
    │   │   │   ├── guardian_death_commands.bin
    │   │   │   ├── gym_fd.bin
    │   │   │   ├── gym_music_tracks.bin
    │   │   │   ├── gym_sfx.bin
    │   │   │   ├── gym_textures.bin
    │   │   │   ├── house_itemrots.bin
    │   │   │   ├── house_music_tracks.bin
    │   │   │   ├── house_sfx.bin
    │   │   │   ├── house_shower_frames.bin
    │   │   │   ├── house_textures.bin
    │   │   │   ├── inv_background.bin
    │   │   │   ├── kingdom_cameras.bin
    │   │   │   ├── kingdom_crystals.bin
    │   │   │   ├── kingdom_itemrots.bin
    │   │   │   ├── kingdom_music_tracks.bin
    │   │   │   ├── kingdom_textures.bin
    │   │   │   ├── lair_bartolipos.bin
    │   │   │   ├── lair_crystals.bin
    │   │   │   ├── lair_music_tracks.bin
    │   │   │   ├── lair_textures.bin
    │   │   │   ├── lara_animations.bin
    │   │   │   ├── lara_extra.bin
    │   │   │   ├── lara_guns.bin
    │   │   │   ├── lara_gym_guns.bin
    │   │   │   ├── lara_house_guns.bin
    │   │   │   ├── lara_outfits.bin
    │   │   │   ├── lara_rifle_sfx.bin
    │   │   │   ├── lara_vegas_guns.bin
    │   │   │   ├── living_crystals.bin
    │   │   │   ├── living_deck_goon_sfx.bin
    │   │   │   ├── living_fd.bin
    │   │   │   ├── living_itemrots.bin
    │   │   │   ├── living_music_tracks.bin
    │   │   │   ├── living_pickup_meshes.bin
    │   │   │   ├── living_secret_fd.bin
    │   │   │   ├── living_sfx.bin
    │   │   │   ├── living_textures.bin
    │   │   │   ├── loose_boards_sfx.bin
    │   │   │   ├── misc_sprites.bin
    │   │   │   ├── opera_crystals.bin
    │   │   │   ├── opera_fd.bin
    │   │   │   ├── opera_itemrots.bin
    │   │   │   ├── opera_music_tracks.bin
    │   │   │   ├── opera_sfx.bin
    │   │   │   ├── opera_textures.bin
    │   │   │   ├── palace_crystals.bin
    │   │   │   ├── palace_fd.bin
    │   │   │   ├── palace_itemrots.bin
    │   │   │   ├── palace_music_tracks.bin
    │   │   │   ├── palace_secret_fd.bin
    │   │   │   ├── palace_textures.bin
    │   │   │   ├── pda_model.bin
    │   │   │   ├── photo.bin
    │   │   │   ├── pickup_aid.bin
    │   │   │   ├── portcullis_sfx.bin
    │   │   │   ├── rig_crystals.bin
    │   │   │   ├── rig_itemrots.bin
    │   │   │   ├── rig_music_tracks.bin
    │   │   │   ├── rig_pickup_meshes.bin
    │   │   │   ├── rig_textures.bin
    │   │   │   ├── scuba_sfx.bin
    │   │   │   ├── seaweed_collision.bin
    │   │   │   ├── secret_models_gm.bin
    │   │   │   ├── secret_models_og.bin
    │   │   │   ├── shark_sfx.bin
    │   │   │   ├── tibet_crystals.bin
    │   │   │   ├── tibet_fd.bin
    │   │   │   ├── tibet_itemrots.bin
    │   │   │   ├── tibet_music_tracks.bin
    │   │   │   ├── tibet_textures.bin
    │   │   │   ├── title_textures.bin
    │   │   │   ├── vegas_crystals.bin
    │   │   │   ├── vegas_fd.bin
    │   │   │   ├── vegas_itemrots.bin
    │   │   │   ├── vegas_music_tracks.bin
    │   │   │   ├── vegas_textures.bin
    │   │   │   ├── venice_crystals.bin
    │   │   │   ├── venice_fd.bin
    │   │   │   ├── venice_itemrots.bin
    │   │   │   ├── venice_music_tracks.bin
    │   │   │   ├── venice_textures.bin
    │   │   │   ├── wall_cameras.bin
    │   │   │   ├── wall_crystals.bin
    │   │   │   ├── wall_itemrots.bin
    │   │   │   ├── wall_music_tracks.bin
    │   │   │   ├── wall_textures.bin
    │   │   │   ├── winston_model.bin
    │   │   │   ├── wreck_cameras.bin
    │   │   │   ├── wreck_crystals.bin
    │   │   │   ├── wreck_fd.bin
    │   │   │   ├── wreck_goon_sfx.bin
    │   │   │   ├── wreck_itemrots.bin
    │   │   │   ├── wreck_music_tracks.bin
    │   │   │   ├── wreck_pickup_meshes.bin
    │   │   │   ├── wreck_plants.bin
    │   │   │   ├── wreck_secret_fd.bin
    │   │   │   ├── wreck_textures.bin
    │   │   │   ├── xian_crystals.bin
    │   │   │   ├── xian_fd.bin
    │   │   │   ├── xian_itemrots.bin
    │   │   │   ├── xian_music_tracks.bin
    │   │   │   ├── xian_pickup_meshes.bin
    │   │   │   ├── xian_sfx.bin
    │   │   │   └── xian_textures.bin
    │   │   ├── scripts
    │   │   │   ├── assault.lua
    │   │   │   ├── cut3.lua
    │   │   │   ├── floating.lua
    │   │   │   ├── house.lua
    │   │   │   ├── level1.lua
    │   │   │   ├── level3.lua
    │   │   │   ├── level4.lua
    │   │   │   └── monastry.lua
    │   │   ├── assault.tr2
    │   │   ├── boat.tr2
    │   │   ├── catacomb.tr2
    │   │   ├── cut1.tr2
    │   │   ├── cut2.tr2
    │   │   ├── cut3.tr2
    │   │   ├── cut4.tr2
    │   │   ├── deck.tr2
    │   │   ├── emprtomb.tr2
    │   │   ├── floating.tr2
    │   │   ├── house.tr2
    │   │   ├── icecave.tr2
    │   │   ├── keel.tr2
    │   │   ├── level1.tr2
    │   │   ├── level2.tr2
    │   │   ├── level3.tr2
    │   │   ├── level4.tr2
    │   │   ├── level5.tr2
    │   │   ├── living.tr2
    │   │   ├── main.sfx
    │   │   ├── main_gm.sfx
    │   │   ├── monastry.tr2
    │   │   ├── opera.tr2
    │   │   ├── platform.tr2
    │   │   ├── rig.tr2
    │   │   ├── skidoo.tr2
    │   │   ├── title.tr2
    │   │   ├── title_gm.tr2
    │   │   ├── unwater.tr2
    │   │   ├── venice.tr2
    │   │   ├── wall.tr2
    │   │   └── xian.tr2
    │   ├── fmv
    │   │   ├── ancient.rpl
    │   │   ├── crash.rpl
    │   │   ├── end.rpl
    │   │   ├── jeep.rpl
    │   │   ├── landing.rpl
    │   │   ├── logo.rpl
    │   │   ├── modern.rpl
    │   │   └── ms.rpl
    │   ├── music
    │   │   ├── 2.mp3
    │   │   ├── 3.mp3
    │   │   ├── 4.mp3
    │   │   ├── 5.mp3
    │   │   ├── 6.mp3
    │   │   ├── 7.mp3
    │   │   ├── 8.mp3
    │   │   ├── 9.mp3
    │   │   ├── 10.mp3
    │   │   ├── 11.mp3
    │   │   ├── 12.mp3
    │   │   ├── 13.mp3
    │   │   ├── 14.mp3
    │   │   ├── 15.mp3
    │   │   ├── 16.mp3
    │   │   ├── 17.mp3
    │   │   ├── 18.mp3
    │   │   ├── 19.mp3
    │   │   ├── 20.mp3
    │   │   ├── 21.mp3
    │   │   ├── 22.mp3
    │   │   ├── 23.mp3
    │   │   ├── 24.mp3
    │   │   ├── 25.mp3
    │   │   ├── 26.mp3
    │   │   ├── 27.mp3
    │   │   ├── 28.mp3
    │   │   ├── 29.mp3
    │   │   ├── 30.mp3
    │   │   ├── 31.mp3
    │   │   ├── 32.mp3
    │   │   ├── 33.mp3
    │   │   ├── 34.mp3
    │   │   ├── 35.mp3
    │   │   ├── 36.mp3
    │   │   ├── 37.mp3
    │   │   ├── 38.mp3
    │   │   ├── 39.mp3
    │   │   ├── 40.mp3
    │   │   ├── 41.mp3
    │   │   ├── 42.mp3
    │   │   ├── 43.mp3
    │   │   ├── 44.mp3
    │   │   ├── 45.mp3
    │   │   ├── 46.mp3
    │   │   ├── 47.mp3
    │   │   ├── 48.mp3
    │   │   ├── 49.mp3
    │   │   ├── 50.mp3
    │   │   ├── 51.mp3
    │   │   ├── 52.mp3
    │   │   ├── 53.mp3
    │   │   ├── 54.mp3
    │   │   ├── 55.mp3
    │   │   ├── 56.mp3
    │   │   ├── 57.mp3
    │   │   ├── 58.mp3
    │   │   ├── 59.mp3
    │   │   ├── 60.mp3
    │   │   └── 61.mp3
    │   ├── shaders
    │   │   ├── 2d.glsl
    │   │   ├── billboard.glsl
    │   │   ├── common.glsl
    │   │   ├── fbo.glsl
    │   │   ├── lights.glsl
    │   │   ├── meshes.glsl
    │   │   ├── meshes_tr3.glsl
    │   │   ├── meshes_tr12.glsl
    │   │   └── ui.glsl
    │   └── icon.icns
    ├── _CodeSignature
    ├── Frameworks
    ├── info.plist
    └── MacOS
*\* Will not be present until the game has been launched.* ================================================ FILE: docs/tr2/symbols.txt ================================================ # TYPES typedef IDirect3DDevice2 *LPDIRECT3DDEVICE2; typedef IDirect3DTexture2 *LPDIRECT3DTEXTURE2; typedef IDirect3DViewport2 *LPDIRECT3DVIEWPORT2; typedef IDirect3DMaterial2 *LPDIRECT3DMATERIAL2; typedef DDSURFACEDESC DDSDESC, *LPDDSDESC; typedef LPDIRECTDRAWSURFACE3 LPDDS; typedef LPDIRECTDRAW3 LPDD; typedef D3DTEXTUREHANDLE HWR_TEXTURE_HANDLE; typedef struct __unaligned { int32_t x; int32_t y; int32_t z; } XYZ_32; typedef struct __unaligned { int16_t x; int16_t y; int16_t z; } XYZ_16; typedef struct __unaligned { int32_t _00; int32_t _01; int32_t _02; int32_t _03; int32_t _10; int32_t _11; int32_t _12; int32_t _13; int32_t _20; int32_t _21; int32_t _22; int32_t _23; } MATRIX; typedef enum { VGA_NO_VGA = 0, VGA_256_COLOR = 1, VGA_MODEX = 2, VGA_STANDARD = 3, } VGA_MODE; typedef struct __unaligned { LPBITMAPINFO bmp_info; void *bmp_data; HPALETTE hPalette; DWORD flags; } BITMAP_RESOURCE; typedef struct __unaligned { int32_t width; int32_t height; int32_t bpp; VGA_MODE vga; } DISPLAY_MODE; typedef struct __unaligned DISPLAY_MODE_NODE { struct DISPLAY_MODE_NODE *next; struct DISPLAY_MODE_NODE *previous; DISPLAY_MODE body; } DISPLAY_MODE_NODE; typedef struct __unaligned { DISPLAY_MODE_NODE *head; DISPLAY_MODE_NODE *tail; DWORD count; } DISPLAY_MODE_LIST; typedef struct __unaligned { char *content; bool is_valid; } STRING_FLAGGED; typedef struct __unaligned { LPGUID adapter_guid_ptr; GUID adapter_guid; STRING_FLAGGED driver_desc; STRING_FLAGGED driver_name; DDCAPS_DX5 driver_caps; DDCAPS_DX5 hel_caps; GUID device_guid; D3DDEVICEDESC_V2 hw_device_desc; DISPLAY_MODE_LIST hw_disp_mode_list; DISPLAY_MODE_LIST sw_disp_mode_list; DISPLAY_MODE vga_mode1; DISPLAY_MODE vga_mode2; uint32_t screen_width; bool hw_render_supported; bool sw_windowed_supported; bool hw_windowed_supported; bool is_vga_mode1_presented; bool is_vga_mode2_presented; bool perspective_correct_supported; bool dither_supported; bool zbuffer_supported; bool linear_filter_supported; bool shade_restricted; } DISPLAY_ADAPTER; typedef struct __unaligned DISPLAY_ADAPTER_NODE { struct DISPLAY_ADAPTER_NODE *next; struct DISPLAY_ADAPTER_NODE *previous; DISPLAY_ADAPTER body; } DISPLAY_ADAPTER_NODE; typedef struct __unaligned { DISPLAY_ADAPTER_NODE *head; DISPLAY_ADAPTER_NODE *tail; DWORD count; } DISPLAY_ADAPTER_LIST; typedef struct __unaligned { GUID *adapter_guid_ptr; GUID adapter_guid; STRING_FLAGGED description; STRING_FLAGGED module; } SOUND_ADAPTER; typedef struct __unaligned SOUND_ADAPTER_NODE { struct SOUND_ADAPTER_NODE *next; struct SOUND_ADAPTER_NODE *previous; SOUND_ADAPTER body; } SOUND_ADAPTER_NODE; typedef struct __unaligned { SOUND_ADAPTER_NODE *head; SOUND_ADAPTER_NODE *tail; DWORD count; } SOUND_ADAPTER_LIST; typedef struct __unaligned { GUID *lpJoystickGuid; GUID joystickGuid; STRING_FLAGGED productName; STRING_FLAGGED instanceName; } JOYSTICK; typedef struct __unaligned JOYSTICK_NODE { struct JOYSTICK_NODE *next; struct JOYSTICK_NODE *previous; JOYSTICK body; } JOYSTICK_NODE; typedef struct __unaligned JOYSTICK_LIST { struct JOYSTICK_LIST *head; struct JOYSTICK_LIST *tail; DWORD count; } JOYSTICK_LIST; typedef enum { RM_UNKNOWN = 0, RM_SOFTWARE = 1, RM_HARDWARE = 2, } RENDER_MODE; typedef enum { AM_4_3 = 0, AM_16_9 = 1, AM_ANY = 2, } ASPECT_MODE; typedef enum { TAM_DISABLED = 0, TAM_BILINEAR_ONLY = 1, TAM_ALWAYS = 2, } TEXEL_ADJUST_MODE; typedef struct __unaligned { DISPLAY_ADAPTER_NODE *preferred_display_adapter; SOUND_ADAPTER_NODE *preferred_sound_adapter; JOYSTICK_NODE *preferred_joystick; const DISPLAY_MODE_NODE *video_mode; RENDER_MODE render_mode; int32_t window_width; int32_t window_height; ASPECT_MODE aspect_mode; bool perspective_correct; bool dither; bool zbuffer; bool bilinear_filtering; bool triple_buffering; // TODO: remove this option bool fullscreen; bool sound_enabled; bool lara_mic; // TODO: remove this option bool joystick_enabled; bool disable_16bit_textures; bool dont_sort_primitives; bool flip_broken; TEXEL_ADJUST_MODE texel_adjust_mode; int32_t nearest_adjustment; int32_t linear_adjustment; } APP_SETTINGS; typedef struct __unaligned { LPDDS sys_mem_surface; LPDDS vid_mem_surface; LPDIRECTDRAWPALETTE palette; LPDIRECT3DTEXTURE2 texture_3d; HWR_TEXTURE_HANDLE tex_handle; int32_t width; int32_t height; int32_t status; } TEXPAGE_DESC; typedef struct __unaligned { union { uint8_t red; uint8_t r; }; union { uint8_t green; uint8_t g; }; union { uint8_t blue; uint8_t b; }; } RGB_888; typedef struct __unaligned { union { uint8_t red; uint8_t r; }; union { uint8_t green; uint8_t g; }; union { uint8_t blue; uint8_t b; }; union { uint8_t alpha; uint8_t a; }; } RGBA_8888; typedef struct { struct { uint32_t r; uint32_t g; uint32_t b; uint32_t a; } mask, depth, offset; } COLOR_BIT_MASKS; typedef struct __unaligned { D3DCOLOR clr[4][4]; } GOURAUD_FILL; typedef struct __unaligned { D3DCOLOR clr[9]; } GOURAUD_OUTLINE; typedef struct __unaligned { uint8_t index[256]; } DEPTHQ_ENTRY; typedef struct __unaligned { uint8_t index[32]; } GOURAUD_ENTRY; typedef struct __unaligned { XYZ_32 pos; XYZ_16 rot; } PHD_3DPOS; typedef struct __unaligned { int32_t x; int32_t y; int32_t z; int32_t r; } SPHERE; typedef struct __unaligned { union { uint32_t all; struct { uint32_t active: 1; uint32_t flash: 1; uint32_t rotate_h: 1; uint32_t rotate_v: 1; uint32_t centre_h: 1; uint32_t centre_v: 1; uint32_t hide: 1; uint32_t right: 1; uint32_t bottom: 1; uint32_t background: 1; uint32_t outline: 1; uint32_t multiline: 1; uint32_t manual_draw: 1; // not present in the OG }; } flags; uint16_t text_flags; uint16_t bgnd_flags; uint16_t outl_flags; XYZ_16 pos; int16_t letter_spacing; int16_t word_spacing; struct { int16_t rate; int16_t count; } flash; int16_t bgnd_color; const uint16_t *bgnd_gour; int16_t outl_color; const uint16_t *outl_gour; struct { int16_t x; int16_t y; } bgnd_size; XYZ_16 bgnd_off; struct { int32_t h; int32_t v; } scale; char *content; } TEXTSTRING; typedef struct __unaligned { float xv; float yv; float zv; float rhw; float xs; float ys; int16_t clip; int16_t g; int16_t u; int16_t v; } PHD_VBUF; typedef struct __unaligned { uint16_t u; uint16_t v; } PHD_UV; typedef struct __unaligned { uint16_t draw_type; uint16_t tex_page; PHD_UV uv[4]; } PHD_TEXTURE; typedef struct __unaligned { uint16_t tex_page; uint16_t offset; uint16_t width; uint16_t height; int16_t x0; int16_t y0; int16_t x1; int16_t y1; } PHD_SPRITE; typedef enum { SHAPE_SPRITE = 1, SHAPE_LINE = 2, SHAPE_BOX = 3, SHAPE_FBOX = 4, } SHAPE; typedef enum { SPRF_RGB = 0x00FFFFFF, SPRF_ABS = 0x01000000, SPRF_SEMITRANS = 0x02000000, SPRF_SCALE = 0x04000000, SPRF_SHADE = 0x08000000, } SPRITE_FLAG; typedef struct __unaligned { float xv; float yv; float zv; float rhw; float xs; float ys; float u; float v; float g; } POINT_INFO; typedef struct __unaligned { float x; float y; float rhw; float u; float v; float g; } VERTEX_INFO; typedef enum { INPUT_ROLE_FORWARD = 0, INPUT_ROLE_BACK = 1, INPUT_ROLE_LEFT = 2, INPUT_ROLE_RIGHT = 3, INPUT_ROLE_STEP_L = 4, INPUT_ROLE_STEP_R = 5, INPUT_ROLE_SLOW = 6, INPUT_ROLE_JUMP = 7, INPUT_ROLE_ACTION = 8, INPUT_ROLE_DRAW = 9, INPUT_ROLE_USE_FLARE = 10, INPUT_ROLE_LOOK = 11, INPUT_ROLE_ROLL = 12, INPUT_ROLE_OPTION = 13, } INPUT_ROLE; typedef struct __unaligned { uint16_t no_selector : 1; uint16_t ready : 1; // not present in the OG uint16_t pad : 14; uint16_t items_count; uint16_t selected; uint16_t visible_count; uint16_t line_offset; uint16_t line_old_offset; uint16_t pix_width; uint16_t line_height; int16_t x_pos; int16_t y_pos; int16_t z_pos; uint16_t item_string_len; char *pitem_strings1; char *pitem_strings2; uint32_t *pitem_flags1; uint32_t *pitem_flags2; uint32_t heading_flags1; uint32_t heading_flags2; uint32_t background_flags; uint32_t moreup_flags; uint32_t moredown_flags; uint32_t item_flags1[24]; // MAX_REQUESTER_ITEMS uint32_t item_flags2[24]; // MAX_REQUESTER_ITEMS TEXTSTRING *heading_text1; TEXTSTRING *heading_text2; TEXTSTRING *background_text; TEXTSTRING *moreup_text; TEXTSTRING *moredown_text; TEXTSTRING *item_texts1[24]; // MAX_REQUESTER_ITEMS TEXTSTRING *item_texts2[24]; // MAX_REQUESTER_ITEMS char heading_string1[32]; char heading_string2[32]; uint32_t render_width; uint32_t render_height; } REQUEST_INFO; typedef enum { POLY_GTMAP = 0, POLY_WGTMAP = 1, POLY_GTMAP_PERSP = 2, POLY_WGTMAP_PERSP = 3, POLY_LINE = 4, POLY_FLAT = 5, POLY_GOURAUD = 6, POLY_TRANS = 7, POLY_SPRITE = 8, POLY_HWR_GTMAP = 9, POLY_HWR_WGTMAP = 10, POLY_HWR_GOURAUD = 11, POLY_HWR_LINE = 12, POLY_HWR_TRANS = 13, } POLY_TYPE; typedef struct __unaligned { uint32_t best_time[10]; uint32_t best_finish[10]; uint32_t finish_count; } ASSAULT_STATS; typedef struct __unaligned { int32_t _0; int32_t _1; } SORT_ITEM; typedef enum { ST_AVG_Z = 0, ST_MAX_Z = 1, ST_FAR_Z = 2, } SORT_TYPE; typedef enum { DRAW_OPAQUE = 0, DRAW_COLOR_KEY = 1, } DRAW_TYPE; typedef struct __unaligned { int32_t floor; int32_t ceiling; int32_t type; } COLL_SIDE; typedef struct __unaligned { COLL_SIDE side_mid; COLL_SIDE side_front; COLL_SIDE side_left; COLL_SIDE side_right; int32_t radius; int32_t bad_pos; int32_t bad_neg; int32_t bad_ceiling; XYZ_32 shift; XYZ_32 old; int16_t old_anim_state; int16_t old_anim_num; int16_t old_frame_num; int16_t facing; int16_t quadrant; int16_t coll_type; int16_t *trigger; int8_t x_tilt; int8_t z_tilt; int8_t hit_by_baddie; int8_t hit_static; uint16_t slopes_are_walls: 1; // 0x01 1 uint16_t slopes_are_pits: 1; // 0x02 2 uint16_t lava_is_pit: 1; // 0x04 4 uint16_t enable_baddie_push: 1; // 0x08 8 uint16_t enable_spaz: 1; // 0x10 16 uint16_t hit_ceiling: 1; // 0x20 32 uint16_t pad: 10; } COLL_INFO; typedef struct __unaligned { int16_t min_x; int16_t max_x; int16_t min_y; int16_t max_y; int16_t min_z; int16_t max_z; } BOUNDS_16; typedef struct __unaligned { int16_t mesh_idx; uint16_t flags; BOUNDS_16 draw_bounds; BOUNDS_16 collision_bounds; } STATIC_INFO; typedef struct __unaligned { int32_t floor; uint32_t touch_bits; uint32_t mesh_bits; int16_t object_id; int16_t current_anim_state; int16_t goal_anim_state; int16_t required_anim_state; int16_t anim_num; int16_t frame_num; int16_t room_num; int16_t next_item; int16_t next_active; int16_t speed; int16_t fall_speed; int16_t hit_points; int16_t box_num; int16_t timer; uint16_t flags; int16_t shade_1; int16_t shade_2; int16_t carried_item; void *data; union { struct { XYZ_32 pos; XYZ_16 rot; }; PHD_3DPOS pos_full; // TODO: stick to pos and rot }; uint16_t active: 1; // 0x0001 uint16_t status: 2; // 0x0002…0x0004 uint16_t gravity: 1; // 0x0008 uint16_t hit_status: 1; // 0x0010 uint16_t collidable: 1; // 0x0020 uint16_t looked_at: 1; // 0x0040 uint16_t dynamic_light: 1; // 0x0080 uint16_t killed: 1; // 0x0100 uint16_t pad: 7; // 0x0200…0x8000 } ITEM; typedef struct __unaligned { uint32_t timer; uint32_t shots; uint32_t hits; uint32_t distance; uint16_t kills; uint8_t secrets_flags; uint8_t medipacks; } STATISTICS_INFO; typedef struct __unaligned { uint16_t pistol_ammo; uint16_t magnum_ammo; uint16_t uzi_ammo; uint16_t shotgun_ammo; uint16_t m16_ammo; uint16_t grenade_ammo; uint16_t harpoon_ammo; uint8_t small_medipacks; uint8_t large_medipacks; uint8_t reserved1; uint8_t flares; uint8_t gun_status; uint8_t gun_type; uint16_t available: 1; // 0x01 1 uint16_t has_pistols: 1; // 0x02 2 uint16_t has_magnums: 1; // 0x04 4 uint16_t has_uzis: 1; // 0x08 8 uint16_t has_shotgun: 1; // 0x10 16 uint16_t has_m16: 1; // 0x20 32 uint16_t has_grenade: 1; // 0x40 64 uint16_t has_harpoon: 1; // 0x80 128 uint16_t pad : 8; uint16_t reserved2; STATISTICS_INFO statistics; } START_INFO; typedef struct __unaligned { START_INFO start[24]; STATISTICS_INFO statistics; int16_t current_level; bool bonus_flag; uint8_t num_pickup[2]; uint8_t num_puzzle[4]; uint8_t num_key[4]; uint16_t reserved; char buffer[6272]; // MAX_SG_BUFFER_SIZE } SAVEGAME_INFO; typedef struct __unaligned { uint16_t idx; int16_t box; uint8_t pit_room; int8_t floor; uint8_t sky_room; int8_t ceiling; } SECTOR; typedef struct __unaligned { int16_t lock_angles[4]; int16_t left_angles[4]; int16_t right_angles[4]; int16_t aim_speed; int16_t shot_accuracy; int32_t gun_height; int32_t damage; int32_t target_dist; int16_t recoil_frame; int16_t flash_time; int16_t sample_num; } WEAPON_INFO; typedef struct __unaligned { XYZ_32 pos; XYZ_16 rot; int16_t room_num; int16_t object_id; int16_t next_free; int16_t next_active; int16_t speed; int16_t fall_speed; int16_t frame_num; int16_t counter; int16_t shade; } EFFECT; typedef struct __unaligned { int16_t zone_num; int16_t enemy_zone_num; int32_t distance; int32_t ahead; int32_t bite; int16_t angle; int16_t enemy_facing; } AI_INFO; typedef struct __unaligned { int16_t exit_box; uint16_t search_num; int16_t next_expansion; int16_t box_num; } BOX_NODE; typedef struct __unaligned { BOX_NODE *node; int16_t head; int16_t tail; uint16_t search_num; uint16_t block_mask; int16_t step; int16_t drop; int16_t fly; int16_t zone_count; int16_t target_box; int16_t required_box; XYZ_32 target; } LOT_INFO; typedef enum { GFL_NO_LEVEL = -1, GFL_TITLE = 0, GFL_NORMAL = 1, GFL_SAVED = 2, GFL_DEMO = 3, GFL_CUTSCENE = 4, GFL_STORY = 5, GFL_QUIET = 6, GFL_MID_STORY = 7, } GAME_FLOW_LEVEL_TYPE; typedef struct __unaligned { int16_t timer; int16_t sprite; } PICKUP_INFO; typedef struct __unaligned { int16_t shape; XYZ_16 pos; int32_t param1; int32_t param2; void *grdptr; int16_t sprite_num; } INVENTORY_SPRITE; typedef struct __unaligned { char *string; int16_t object_id; int16_t frames_total; int16_t current_frame; int16_t goal_frame; int16_t open_frame; int16_t anim_direction; int16_t anim_speed; int16_t anim_count; int16_t x_rot_pt_sel; int16_t x_rot_pt; int16_t x_rot_sel; int16_t x_rot_nosel; int16_t x_rot; int16_t y_rot_sel; int16_t y_rot; int32_t y_trans_sel; int32_t y_trans; int32_t z_trans_sel; int32_t z_trans; uint32_t meshes_sel; uint32_t meshes_drawn; int16_t inv_pos; INVENTORY_SPRITE **sprite_list; int32_t reserved[4]; } INVENTORY_ITEM; typedef enum { RNG_OPENING = 0, RNG_OPEN = 1, RNG_CLOSING = 2, RNG_MAIN2OPTION = 3, RNG_MAIN2KEYS = 4, RNG_KEYS2MAIN = 5, RNG_OPTION2MAIN = 6, RNG_SELECTING = 7, RNG_SELECTED = 8, RNG_DESELECTING = 9, RNG_DESELECT = 10, RNG_CLOSING_ITEM = 11, RNG_EXITING_INVENTORY = 12, RNG_DONE = 13, } RING_STATUS; typedef struct __unaligned { int16_t count; int16_t status; int16_t status_target; int16_t radius_target; int16_t radius_rate; int16_t camera_y_target; int16_t camera_y_rate; int16_t camera_pitch_target; int16_t camera_pitch_rate; int16_t rotate_target; int16_t rotate_rate; int16_t item_pt_x_rot_target; int16_t item_pt_x_rot_rate; int16_t item_x_rot_target; int16_t item_x_rot_rate; int32_t item_y_trans_target; int32_t item_y_trans_rate; int32_t item_z_trans_target; int32_t item_z_trans_rate; int32_t misc; } IMOTION_INFO; typedef enum { PM_SPINE = 1, PM_FRONT = 2, PM_IN_FRONT = 4, PM_PAGE_2 = 8, PM_BACK = 16, PM_IN_BACK = 32, PM_PAGE_1 = 64, PM_COMMON = PM_SPINE | PM_BACK | PM_FRONT, } PASS_MESH; typedef struct __unaligned { INVENTORY_ITEM **list; int16_t type; int16_t radius; int16_t camera_pitch; int16_t rotating; int16_t rot_count; int16_t current_object; int16_t target_object; int16_t number_of_objects; int16_t angle_adder; int16_t rot_adder; int16_t rot_adder_l; int16_t rot_adder_r; PHD_3DPOS ring_pos; PHD_3DPOS camera; XYZ_32 light; IMOTION_INFO *imo; } RING_INFO; typedef enum { GFE_PICTURE = 0, GFE_LIST_START = 1, GFE_LIST_END = 2, GFE_PLAY_FMV = 3, GFE_START_LEVEL = 4, GFE_CUTSCENE = 5, GFE_LEVEL_COMPLETE = 6, GFE_DEMO_PLAY = 7, GFE_JUMP_TO_SEQ = 8, GFE_END_SEQ = 9, GFE_SET_TRACK = 10, GFE_SUNSET = 11, GFE_LOADING_PIC = 12, GFE_DEADLY_WATER = 13, GFE_REMOVE_WEAPONS = 14, GFE_GAME_COMPLETE = 15, GFE_CUT_ANGLE = 16, GFE_NO_FLOOR = 17, GFE_ADD_TO_INV = 18, GFE_START_ANIM = 19, GFE_NUM_SECRETS = 20, GFE_KILL_TO_COMPLETE = 21, GFE_REMOVE_AMMO = 22, } GF_EVENTS; typedef enum { MOOD_BORED = 0, MOOD_ATTACK = 1, MOOD_ESCAPE = 2, MOOD_STALK = 3, } MOOD_TYPE; typedef enum { TARGET_NONE = 0, TARGET_PRIMARY = 1, TARGET_SECONDARY = 2, } TARGET_TYPE; typedef struct __unaligned { XYZ_32 pos; int32_t mesh_num; } BITE; typedef struct __unaligned { int16_t *frame_ptr; int16_t interpolation; int16_t current_anim_state; int32_t velocity; int32_t acceleration; int16_t frame_base; int16_t frame_end; int16_t jump_anim_num; int16_t jump_frame_num; int16_t num_changes; int16_t change_idx; int16_t num_commands; int16_t command_idx; } ANIM; typedef struct { int16_t goal_anim_state; int16_t num_ranges; int16_t range_idx; } ANIM_CHANGE; typedef struct { int16_t start_frame; int16_t end_frame; int16_t link_anim_num; int16_t link_frame_num; } ANIM_RANGE; typedef struct __unaligned { int16_t room; XYZ_16 normal; XYZ_16 vertex[4]; } PORTAL; typedef struct __unaligned { int16_t count; PORTAL portal[]; } PORTALS; typedef struct __unaligned { int32_t x; int32_t y; int32_t z; int16_t intensity_1; int16_t intensity_2; int32_t falloff_1; int32_t falloff_2; } LIGHT; typedef struct __unaligned { XYZ_16 pos; struct __unaligned { int16_t y; } rot; int16_t shade_1; int16_t shade_2; int16_t static_num; } MESH; typedef enum { RF_UNDERWATER = 0x01, RF_OUTSIDE = 0x08, RF_DYNAMIC_LIT = 0x10, RF_NOT_INSIDE = 0x20, RF_INSIDE = 0x40, } ROOM_FLAG; typedef struct __unaligned { SECTOR *sector; SECTOR old_sector; int16_t block; } DOORPOS_DATA; typedef struct __unaligned { DOORPOS_DATA d1; DOORPOS_DATA d1flip; DOORPOS_DATA d2; DOORPOS_DATA d2flip; } DOOR_DATA; typedef struct __unaligned { int16_t *data; PORTALS *portals; SECTOR *sectors; LIGHT *lights; MESH *meshes; XYZ_32 pos; int32_t min_floor; int32_t max_ceiling; struct __unaligned { int16_t z; int16_t x; } size; int16_t ambient_1; int16_t ambient_2; int16_t light_mode; int16_t num_lights; int16_t num_meshes; int16_t bound_left; int16_t bound_right; int16_t bound_top; int16_t bound_bottom; uint16_t bound_active; int16_t test_left; int16_t test_right; int16_t test_top; int16_t test_bottom; int16_t item_num; int16_t effect_num; int16_t flipped_room; uint16_t flags; } ROOM; typedef struct __unaligned { int16_t head_rotation; int16_t neck_rotation; int16_t maximum_turn; int16_t flags; int16_t item_num; MOOD_TYPE mood; LOT_INFO lot; XYZ_32 target; ITEM *enemy; } CREATURE; typedef enum { CAM_CHASE = 0, CAM_FIXED = 1, CAM_LOOK = 2, CAM_COMBAT = 3, CAM_CINEMATIC = 4, CAM_HEAVY = 5, } CAMERA_TYPE; typedef struct __unaligned { union { XYZ_32 pos; struct { int32_t x; int32_t y; int32_t z; }; }; int16_t room_num; int16_t box_num; } GAME_VECTOR; typedef struct __unaligned { union { struct __unaligned { int32_t x; int32_t y; int32_t z; }; XYZ_32 pos; }; int16_t data; int16_t flags; } OBJECT_VECTOR; typedef struct __unaligned { uint8_t left; uint8_t right; uint8_t top; uint8_t bottom; int16_t height; int16_t overlap_index; } BOX_INFO; typedef enum { LV_GYM = 0, LV_FIRST = 1, } LEVEL_TYPE; typedef enum { RT_MAIN = 0, RT_OPTION = 1, RT_KEYS = 2, } RING_TYPE; typedef enum { INV_COLOR_BLACK = 0, INV_COLOR_GRAY = 1, INV_COLOR_WHITE = 2, INV_COLOR_RED = 3, INV_COLOR_ORANGE = 4, INV_COLOR_YELLOW = 5, INV_COLOR_DARK_GREEN = 12, INV_COLOR_GREEN = 13, INV_COLOR_CYAN = 14, INV_COLOR_BLUE = 15, INV_COLOR_MAGENTA = 16, INV_COLOR_NUMBER_OF = 17, } INV_COLOR; typedef enum { INV_GAME_MODE = 0, INV_TITLE_MODE = 1, INV_KEYS_MODE = 2, INV_SAVE_MODE = 3, INV_LOAD_MODE = 4, INV_DEATH_MODE = 5, } INVENTORY_MODE; typedef enum { TRAP_SET = 0, TRAP_ACTIVATE = 1, TRAP_WORKING = 2, TRAP_FINISHED = 3, } TRAP_ANIM; typedef enum { DOOR_STATE_CLOSED = 0, DOOR_STATE_OPEN = 1, } DOOR_STATE; typedef enum { GFD_START_GAME = 0x0000, GFD_START_SAVED_GAME = 0x0100, GFD_START_CINE = 0x0200, GFD_START_FMV = 0x0300, GFD_START_DEMO = 0x0400, GFD_EXIT_TO_TITLE = 0x0500, GFD_LEVEL_COMPLETE = 0x0600, GFD_EXIT_GAME = 0x0700, GFD_EXIT_TO_OPTION = 0x0800, GFD_TITLE_DESELECT = 0x0900, } GAME_FLOW_DIR; typedef struct __unaligned { int32_t first_option; int32_t title_replace; int32_t on_death_demo_mode; int32_t on_death_in_game; int32_t no_input_time; int32_t on_demo_interrupt; int32_t on_demo_end; uint16_t reserved1[18]; uint16_t num_levels; uint16_t num_pictures; uint16_t num_titles; uint16_t num_fmvs; uint16_t num_cutscenes; uint16_t num_demos; uint16_t title_track; int16_t single_level; uint16_t reserved2[16]; uint16_t demo_version: 1; // 0x0001 uint16_t title_disabled: 1; // 0x0002 uint16_t cheat_mode_check_disabled: 1; // 0x0004 uint16_t no_input_timeout: 1; // 0x0008 uint16_t load_save_disabled: 1; // 0x0010 uint16_t screen_sizing_disabled: 1; // 0x0020 uint16_t lockout_option_ring: 1; // 0x0040 uint16_t dozy_cheat_enabled: 1; // 0x0080 uint16_t cyphered_strings: 1; // 0x0100 uint16_t gym_enabled: 1; // 0x0200 uint16_t play_any_level: 1; // 0x0400 uint16_t cheat_enable: 1; // 0x0800 uint16_t reserved3[3]; uint8_t cypher_code; uint8_t language; uint8_t secret_track; uint8_t level_complete_track; uint16_t reserved4[2]; } GAME_FLOW; typedef struct __unaligned { int16_t mesh_count; int16_t mesh_idx; int32_t bone_idx; int16_t *frame_base; // TODO: make me FRAME_INFO void (*initialise_func)(int16_t item_num); void (*control_func)(int16_t item_num); void (*floor_height_func)( const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height); void (*ceiling_height_func)( const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height); void (*draw_func)(const ITEM *item); void (*collision_func)(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); int16_t anim_idx; int16_t hit_points; int16_t pivot_length; int16_t radius; int16_t shadow_size; union { uint16_t flags; struct { uint16_t loaded: 1; // 0x01 1 uint16_t intelligent: 1; // 0x02 2 uint16_t save_position: 1; // 0x04 4 uint16_t save_hitpoints: 1; // 0x08 8 uint16_t save_flags: 1; // 0x10 16 uint16_t save_anim: 1; // 0x20 32 uint16_t semi_transparent: 1; // 0x40 64 uint16_t water_creature: 1; // 0x80 128 uint16_t pad : 8; }; }; } OBJECT; typedef struct __unaligned { GAME_VECTOR pos; GAME_VECTOR target; CAMERA_TYPE type; int32_t shift; uint32_t flags; int32_t fixed_camera; int32_t num_frames; int32_t bounce; int32_t underwater; int32_t target_distance; int32_t target_square; int16_t target_angle; int16_t actual_angle; int16_t target_elevation; int16_t box; int16_t num; int16_t last; int16_t timer; int16_t speed; ITEM *item; ITEM *last_item; OBJECT_VECTOR *fixed; int32_t is_lara_mic; // TODO: remove this - now stored in g_Config XYZ_32 mic_pos; } CAMERA_INFO; typedef struct __unaligned { int16_t *frame_base; int16_t frame_num; int16_t anim_num; int16_t lock; struct __unaligned { int16_t y; int16_t x; int16_t z; } rot; // TODO: XYZ_16 int16_t flash_gun; } LARA_ARM; typedef struct __unaligned { int32_t ammo; } AMMO_INFO; typedef enum { LWS_ABOVE_WATER = 0, LWS_UNDERWATER = 1, LWS_SURFACE = 2, LWS_CHEAT = 3, LWS_WADE = 4, } LARA_WATER_STATE; typedef struct __unaligned { int16_t item_num; int16_t gun_status; int16_t gun_type; int16_t request_gun_type; int16_t last_gun_type; int16_t calc_fall_speed; int16_t water_status; int16_t climb_status; int16_t pose_count; int16_t hit_frame; int16_t hit_direction; int16_t air; int16_t dive_count; int16_t death_timer; int16_t current_active; int16_t spaz_effect_count; int16_t flare_age; int16_t skidoo; int16_t weapon_item; int16_t back_gun; int16_t flare_frame; union { uint16_t flags; struct ___unaligned { uint16_t flare_control_left: 1; // 0x01 1 uint16_t flare_control_right: 1; // 0x02 2 uint16_t extra_anim: 1; // 0x04 4 uint16_t look: 1; // 0x08 8 uint16_t burn: 1; // 0x10 16 uint16_t pad: 11; }; }; int32_t water_surface_dist; XYZ_32 last_pos; EFFECT *spaz_effect; uint32_t mesh_effects; int16_t *mesh_ptrs[15]; ITEM *target; int16_t target_angles[2]; int16_t turn_rate; int16_t move_angle; int16_t head_y_rot; int16_t head_x_rot; int16_t head_z_rot; int16_t torso_y_rot; int16_t torso_x_rot; int16_t torso_z_rot; LARA_ARM left_arm; LARA_ARM right_arm; AMMO_INFO pistol_ammo; AMMO_INFO magnum_ammo; AMMO_INFO uzi_ammo; AMMO_INFO shotgun_ammo; AMMO_INFO harpoon_ammo; AMMO_INFO grenade_ammo; AMMO_INFO m16_ammo; CREATURE *creature; } LARA_INFO; typedef enum { SFX_LARA_FEET = 0, SFX_LARA_CLIMB_2 = 1, SFX_LARA_NO = 2, SFX_LARA_SLIPPING = 3, SFX_LARA_LAND = 4, SFX_LARA_CLIMB_1 = 5, SFX_LARA_DRAW = 6, SFX_LARA_HOLSTER = 7, SFX_LARA_FIRE = 8, SFX_LARA_RELOAD = 9, SFX_LARA_RICOCHET = 10, SFX_LARA_FLARE_IGNITE = 11, SFX_LARA_FLARE_BURN = 12, SFX_LARA_HARPOON_FIRE = 15, SFX_LARA_HARPOON_LOAD = 16, SFX_LARA_WET_FEET = 17, SFX_LARA_WADE = 18, SFX_LARA_TREAD = 20, SFX_LARA_FIRE_MAGNUMS = 21, SFX_LARA_HARPOON_LOAD_WATER = 22, SFX_LARA_HARPOON_FIRE_WATER = 23, SFX_MASSIVE_CRASH = 24, SFX_PUSH_SWITCH = 25, SFX_LARA_CLIMB_3 = 26, SFX_LARA_BODYSL = 27, SFX_LARA_SHIMMY = 28, SFX_LARA_JUMP = 29, SFX_LARA_FALL = 30, SFX_LARA_INJURY = 31, SFX_LARA_ROLL = 32, SFX_LARA_SPLASH = 33, SFX_LARA_GETOUT = 34, SFX_LARA_SWIM = 35, SFX_LARA_BREATH = 36, SFX_LARA_BUBBLES = 37, SFX_LARA_SWITCH = 38, SFX_LARA_KEY = 39, SFX_LARA_OBJECT = 40, SFX_LARA_GENERAL_DEATH = 41, SFX_LARA_KNEES_DEATH = 42, SFX_LARA_UZI_FIRE = 43, SFX_LARA_UZI_STOP = 44, SFX_LARA_SHOTGUN = 45, SFX_LARA_BLOCK_PUSH_1 = 46, SFX_LARA_BLOCK_PUSH_2 = 47, SFX_CLICK = 48, SFX_LARA_HIT = 49, SFX_LARA_BULLETHIT = 50, SFX_LARA_BLKPULL = 51, SFX_LARA_FLOATING = 52, SFX_LARA_FALLDETH = 53, SFX_LARA_GRABHAND = 54, SFX_LARA_GRABBODY = 55, SFX_LARA_GRABFEET = 56, SFX_LARA_SWITCHUP = 57, SFX_GLASS_BREAK = 58, SFX_WATER_LOOP = 59, SFX_UNDERWATER = 60, SFX_UNDERWATER_SWITCH = 61, SFX_LARA_PICKUP = 62, SFX_BLOCK_SOUND = 63, SFX_DOOR = 64, SFX_SWING = 65, SFX_ROCK_FALL_CRUMBLE = 66, SFX_ROCK_FALL_LAND = 67, SFX_ROCK_FALL_SOLID = 68, SFX_ENEMY_FEET = 69, SFX_ENEMY_GRUNT = 70, SFX_ENEMY_HIT_1 = 71, SFX_ENEMY_HIT_2 = 72, SFX_ENEMY_DEATH_1 = 73, SFX_ENEMY_JUMP = 74, SFX_ENEMY_CLIMBUP = 75, SFX_ENEMY_CLIMBDOWN = 76, SFX_WEAPON_CLATTER = 77, SFX_M16_FIRE = 78, SFX_WATERFALL_LOOP = 79, SFX_SWORD_STATUE_DROP = 80, SFX_SWORD_STATUE_LIFT = 81, SFX_PORTCULLIS_UP = 82, SFX_PORTCULLIS_DOWN = 83, SFX_DOG_FEET_1 = 84, SFX_BODY_SLAM = 85, SFX_DOG_BARK_1 = 86, SFX_DOG_FEET_2 = 87, SFX_DOG_BARK_2 = 88, SFX_DOG_DEATH = 89, SFX_DOG_PANT = 90, SFX_LEOPARD_FEET = 91, SFX_LEOPARD_ROAR = 92, SFX_LEOPARD_BITE = 93, SFX_LEOPARD_STRIKE = 94, SFX_LEOPARD_DEATH = 95, SFX_LEOPARD_GROWL = 96, SFX_RAT_ATTACK = 97, SFX_RAT_DEATH = 98, SFX_TIGER_ROAR = 99, SFX_TIGER_BITE = 100, SFX_TIGER_STRIKE = 101, SFX_TIGER_DEATH = 102, SFX_TIGER_GROWL = 103, SFX_M16_STOP = 104, SFX_EXPLOSION_1 = 105, SFX_GROWL = 106, SFX_SPIDER_JUMP = 107, SFX_MENU_ROTATE = 108, SFX_MENU_LARA_HOME = 109, SFX_MENU_SPININ = 111, SFX_MENU_SPINOUT = 112, SFX_MENU_STOPWATCH = 113, SFX_MENU_GUNS = 114, SFX_MENU_PASSPORT = 115, SFX_MENU_MEDI = 116, SFX_ENEMY_HEELS = 117, SFX_ENEMY_FIRE_SILENCER = 118, SFX_ENEMY_AH_DYING = 119, SFX_ENEMY_OOH_DYING = 120, SFX_ENEMY_THUMP = 121, SFX_SPIDER_MOVING = 122, SFX_LARA_MINI_LOAD = 123, SFX_LARA_MINI_LOCK = 124, SFX_LARA_MINI_FIRE = 125, SFX_SPIDER_BITE = 126, SFX_SLAM_DOOR_SLIDE = 127, SFX_SLAM_DOOR_CLOSE = 128, SFX_EAGLE_SQUAWK = 129, SFX_EAGLE_WING_FLAP = 130, SFX_EAGLE_DEATH = 131, SFX_CROW_CAW = 132, SFX_CROW_WING_FLAP = 133, SFX_CROW_DEATH = 134, SFX_CROW_ATTACK = 135, SFX_ENEMY_GUN_COCKING = 136, SFX_ENEMY_FIRE_1 = 137, SFX_ENEMY_FIRE_TWIRL = 138, SFX_ENEMY_HOLSTER = 139, SFX_ENEMY_BREATH_1 = 140, SFX_ENEMY_CHUCKLE = 141, SFX_MONK_POY = 142, SFX_MONK_DEATH = 143, SFX_LARA_SPIKE_DEATH = 145, SFX_LARA_DEATH_3 = 146, SFX_ROLLING_BALL = 147, SFX_SANDBAG_SNAP = 148, SFX_SANDBAG_HIT = 149, SFX_LOOP_FOR_SMALL_FIRES = 150, SFX_SKIDOO_START = 152, SFX_SKIDOO_IDLE = 153, SFX_SKIDOO_ACCELERATE = 154, SFX_SKIDOO_MOVING = 155, SFX_SKIDOO_STOP = 156, SFX_ENEMY_FIRE_2 = 157, SFX_ENEMY_DEATH_2 = 158, SFX_ENEMY_BREATH_2 = 159, SFX_STICK_TAP = 160, SFX_TRAPDOOR_OPEN = 161, SFX_TRAPDOOR_CLOSE = 162, SFX_YETI_GROWL = 163, SFX_YETI_CHEST_BEAT = 164, SFX_YETI_THUMP = 165, SFX_YETI_GRUNT_1 = 166, SFX_YETI_SCREAM = 167, SFX_YETI_DEATH = 168, SFX_YETI_GROWL_1 = 169, SFX_YETI_GROWL_2 = 170, SFX_YETI_GRUNT_2 = 171, SFX_YETI_GROWL_3 = 172, SFX_YETI_FEET = 173, SFX_ENEMY_HEAVY_BREATH = 174, SFX_ENEMY_FLAMETHROWER_FIRE = 175, SFX_ENEMY_FLAMETHROWER_SCRAPE = 176, SFX_ENEMY_FLAMETHROWER_CLICK = 177, SFX_ENEMY_FLAMETHROWER_DEATH = 178, SFX_ENEMY_FLAMETHROWER_FALL = 179, SFX_ENEMY_BELT_JINGLE = 180, SFX_ENEMY_WRENCH = 181, SFX_FOOTSTEP = 182, SFX_FOOTSTEP_HIT = 183, SFX_ENEMY_COCKING_SHOTGUN = 184, SFX_SCUBA_DIVER_FLIPPER = 186, SFX_SCUBA_DIVER_BREATH = 188, SFX_PULLEY_CRANE = 190, SFX_CURTAIN = 191, SFX_SCUBA_DIVER_DEATH = 192, SFX_SCUBA_DIVER_DIVING = 193, SFX_BOAT_START = 194, SFX_BOAT_IDLE = 195, SFX_BOAT_ACCELERATE = 196, SFX_BOAT_MOVING = 197, SFX_BOAT_STOP = 198, SFX_BOAT_SLOW_DOWN = 199, SFX_BOAT_HIT = 200, SFX_CLATTER_1 = 201, SFX_CLATTER_2 = 202, SFX_CLATTER_3 = 203, SFX_DOOR_SLIDE = 204, SFX_LARA_FLESH_WOUND = 205, SFX_SAW_REVVING = 206, SFX_SAW_STOP = 207, SFX_DOOR_CHIME = 208, SFX_CHAIN_CREAK_SNAP = 209, SFX_SWINGING = 210, SFX_BREAKING_1 = 211, SFX_PULLEY_MOVE = 212, SFX_AIRPLANE_IDLE = 213, SFX_UNDERWATER_FAN_ON = 215, SFX_SMALL_FAN_ON = 217, SFX_SWINGING_BOX_BAG = 218, SFX_JUMP_PAD_UP = 219, SFX_JUMP_PAD_DOWN = 220, SFX_BREAKING_2 = 221, SFX_SNOWBALL_ROLL = 222, SFX_SNOWBALL_STOP = 223, SFX_ROLLING = 224, SFX_ROLLING_STOP_1 = 225, SFX_ROLLING_STOP_2 = 226, SFX_ROLLING_2 = 227, SFX_ROLLING_2_HIT = 228, SFX_SIDE_BLADE_SWING = 229, SFX_SIDE_BLADE_BACK = 230, SFX_ROLLING_BLADE = 231, SFX_ICILE_DETACH = 232, SFX_ICICLE_HIT = 233, SFX_ROTATING_HANDLE_LOOSE = 234, SFX_ROTATING_HANDLE_TURN = 235, SFX_ROTATING_HANDLE_OPEN = 236, SFX_ROTATING_HANDLE_CREAK = 237, SFX_MONK_FEET = 238, SFX_MONK_SWORD_SWING_1 = 239, SFX_MONK_SWORD_SWING_2 = 240, SFX_MONK_SHOUT_1 = 241, SFX_MONK_SHOUT_2 = 242, SFX_MONK_SHOUT_3 = 243, SFX_MONK_SHOUT_4 = 244, SFX_MONK_CRUNCH = 245, SFX_MONK_BREATH = 246, SFX_SPLASH_SURFACE = 247, SFX_WATERFALL_1 = 248, SFX_ENEMY_FEET_SNOW = 249, SFX_ENEMY_FIRE_3 = 250, SFX_ENEMY_FIRE_SEMIAUTO = 251, SFX_ENEMY_DEATH_3 = 252, SFX_ENEMY_DEATH_4 = 253, SFX_CIRCLE_BLADE = 254, SFX_KNIFETHROWER_FEET = 255, SFX_MONK_OYE = 256, SFX_MONK_AWEH = 257, SFX_CIRCLE_BLADE_HIT = 258, SFX_KNIFETHROWER_WARRIOR_FEET = 259, SFX_WARRIOR_BLADE_SWING_1 = 260, SFX_WARRIOR_BLADE_SWING_2 = 261, SFX_WARRIOR_GROWL = 262, SFX_KNIFETHROWER_HICCUP = 263, SFX_WARROPR_BURP = 264, SFX_WARRIOR_GROWL_1 = 265, SFX_WARRIOR_WAKE = 267, SFX_WARRIOR_GROWL_2 = 268, SFX_SMALL_SWITCH = 269, SFX_CHAIN_PULLEY = 278, SFX_ZIPLINE_GRAB = 279, SFX_ZIPLINE_GO = 280, SFX_ZIPLINE_STOP = 281, SFX_BODY_SLUMP = 282, SFX_BOWL_TIPPING = 283, SFX_BOWL_POUR = 284, SFX_WATERFALL_2 = 285, SFX_ELEVATOR_OPEN = 286, SFX_ELEVATOR_CLOSE = 287, SFX_MINISUB_CLATTER_1 = 288, SFX_MINISUB_CLATTER_2 = 289, SFX_MINISUB_CLATTER_3 = 290, SFX_BIRD_MONSTER_SCREAM = 291, SFX_BIRD_MONSTER_GASP = 292, SFX_BIRD_MONSTER_BREATH = 293, SFX_BIRD_MONSTER_FEET = 294, SFX_BIRD_MONSTER_DEATH = 295, SFX_BIRD_MONSTER_SCRAPE = 296, SFX_HELICOPTER_LOOP = 297, SFX_DRAGON_FEET = 298, SFX_DRAGON_GROWL_1 = 299, SFX_DRAGON_GROWL_2 = 300, SFX_DRAGON_FALL = 301, SFX_DRAGON_BREATH = 302, SFX_DRAGON_GROWL_3 = 303, SFX_DRAGON_GRUNT = 304, SFX_DRAGON_FIRE = 305, SFX_DRAGON_LEG_LIFT = 306, SFX_DRAGON_LEG_HIT = 307, SFX_WARRIOR_BLADE_SWING_3 = 308, SFX_WARRIOR_BLADE_SWING_FAST = 309, SFX_WARRIOR_BREATH_ACTIVE = 311, SFX_WARRIOR_HOVER = 312, SFX_WARRIOR_LANDING = 313, SFX_WARRIOR_SWORD_CLANK = 314, SFX_WARRIOR_SWORD_SLICE = 315, SFX_BIRDS_CHIRP = 316, SFX_CRUNCH_1 = 317, SFX_CRUNCH_2 = 318, SFX_DOOR_CREAK = 319, SFX_BREAKING_3 = 320, SFX_BIG_SPIDER_SNARL = 321, SFX_BIG_SPIDER_FEET = 322, SFX_BIG_SPIDER_DEATH = 323, SFX_T_REX_ROAR = 324, SFX_T_REX_FEET = 325, SFX_T_REX_GROWL_1 = 326, SFX_T_REX_DEATH = 327, SFX_DRIPS_REVERB = 329, SFX_STAGE_BACKDROP = 330, SFX_STONE_DOOR_SLIDE = 331, SFX_PLATFORM_ALARM = 332, SFX_TICK_TOCK = 333, SFX_DOORBELL = 334, SFX_BURGLAR_ALARM = 335, SFX_BOAT_ENGINE = 336, SFX_BOAT_INTO_WATER = 337, SFX_UNKNOWN_1 = 338, SFX_UNKNOWN_2 = 339, SFX_UNKNOWN_3 = 340, SFX_MARCO_BARTOLLI_TRANSFORM = 341, SFX_WINSTON_SHUFFLE = 342, SFX_WINSTON_FEET = 343, SFX_WINSTON_GRUNT_1 = 344, SFX_WINSTON_GRUNT_2 = 345, SFX_WINSTON_GRUNT_3 = 346, SFX_WINSTON_CUPS = 347, SFX_BRITTLE_GROUND_BREAK = 348, SFX_SPIDER_EXPLODE = 349, SFX_SHARK_BITE = 350, SFX_LAVA_BUBBLES = 351, SFX_EXPLOSION_2 = 352, SFX_BURGLARS = 353, SFX_ZIPPER = 354, SFX_NUMBER_OF = 370, } SOUND_EFFECT_ID; typedef enum { SPM_NORMAL = 0, SPM_UNDERWATER = 1, SPM_ALWAYS = 2, SPM_PITCH = 4, } SOUND_PLAY_MODE; typedef enum { CF_NORMAL = 0, CF_FOLLOW_CENTRE = 1, CF_NO_CHUNKY = 2, CF_CHASE_OBJECT = 3, } CAMERA_FLAGS; typedef enum { FBBOX_MIN_X = 0, FBBOX_MAX_X = 1, FBBOX_MIN_Y = 2, FBBOX_MAX_Y = 3, FBBOX_MIN_Z = 4, FBBOX_MAX_Z = 5, FBBOX_X = 6, FBBOX_Y = 7, FBBOX_Z = 8, FBBOX_ROT = 9, } FRAME_BBOX_INFO; typedef struct __unaligned { union { int32_t flags; struct { uint32_t matrix_pop: 1; uint32_t matrix_push: 1; uint32_t rot_x: 1; uint32_t rot_y: 1; uint32_t rot_z: 1; uint32_t pad: 11; }; }; XYZ_32 pos; } BONE; typedef enum { BF_MATRIX_POP = 1, BF_MATRIX_PUSH = 2, BF_ROT_X = 4, BF_ROT_Y = 8, BF_ROT_Z = 16, } BONE_FLAGS; typedef struct __unaligned { int16_t tx; int16_t ty; int16_t tz; int16_t cx; int16_t cy; int16_t cz; int16_t fov; int16_t roll; } CINE_FRAME; typedef enum { IF_ONE_SHOT = 0x0100, IF_CODE_BITS = 0x3E00, IF_REVERSE = 0x4000, IF_INVISIBLE = 0x0100, IF_KILLED = 0x8000, } ITEM_FLAG; typedef enum { IS_INACTIVE = 0, IS_ACTIVE = 1, IS_DEACTIVATED = 2, IS_INVISIBLE = 3, } ITEM_STATUS; typedef struct __unaligned { uint16_t key[14]; // INPUT_ROLE_NUMBER_OF } CONTROL_LAYOUT; typedef enum { IN_FORWARD = 0x00000001, IN_BACK = 0x00000002, IN_LEFT = 0x00000004, IN_RIGHT = 0x00000008, IN_JUMP = 0x00000010, IN_DRAW = 0x00000020, IN_ACTION = 0x00000040, IN_SLOW = 0x00000080, IN_OPTION = 0x00000100, IN_LOOK = 0x00000200, IN_STEP_LEFT = 0x00000400, IN_STEP_RIGHT = 0x00000800, IN_ROLL = 0x00001000, IN_PAUSE = 0x00002000, IN_RESERVED1 = 0x00004000, IN_RESERVED2 = 0x00008000, IN_DOZY_CHEAT = 0x00010000, IN_STUFF_CHEAT = 0x00020000, IN_DEBUG_INFO = 0x00040000, IN_FLARE = 0x00080000, IN_SELECT = 0x00100000, IN_DESELECT = 0x00200000, IN_SAVE = 0x00400000, IN_LOAD = 0x00800000, } INPUT_STATE; typedef enum { LA_RUN = 0, LA_WALK_FORWARD = 1, LA_WALK_STOP_RIGHT = 2, LA_WALK_STOP_LEFT = 3, LA_WALK_TO_RUN_RIGHT = 4, LA_WALK_TO_RUN_LEFT = 5, LA_RUN_START = 6, LA_RUN_TO_WALK_RIGHT = 7, LA_RUN_TO_STAND_LEFT = 8, LA_RUN_TO_WALK_LEFT = 9, LA_RUN_TO_STAND_RIGHT = 10, LA_STAND_STILL = 11, LA_TURN_RIGHT_SLOW = 12, LA_TURN_LEFT_SLOW = 13, LA_JUMP_FORWARD_LAND_START = 14, LA_JUMP_FORWARD_LAND_END_UNUSED = 15, LA_RUN_JUMP_RIGHT_START = 16, LA_RUN_JUMP_RIGHT_CONTINUE = 17, LA_RUN_JUMP_LEFT_START = 18, LA_RUN_JUMP_LEFT_CONTINUE = 19, LA_WALK_FORWARD_START = 20, LA_WALK_FORWARD_START_CONTINUE = 21, LA_JUMP_FORWARD_TO_FREEFALL = 22, LA_FREEFALL = 23, LA_FREEFALL_LAND = 24, LA_FREEFALL_LAND_DEATH = 25, LA_STAND_TO_JUMP_UP = 26, LA_STAND_TO_JUMP_UP_CONTINUE = 27, LA_JUMP_UP = 28, LA_JUMP_UP_TO_HANG = 29, LA_JUMP_UP_TO_FREEFALL = 30, LA_JUMP_UP_LAND = 31, LA_SMASH_JUMP = 32, LA_SMASH_JUMP_CONTINUE = 33, LA_FALL_START = 34, LA_FALL = 35, LA_FALL_TO_FREEFALL = 36, LA_HANG_TO_FREEFALL = 37, LA_WALK_BACK_END_RIGHT = 38, LA_WALK_BACK_END_LEFT = 39, LA_WALK_BACK = 40, LA_WALK_BACK_START = 41, LA_CLIMB_3CLICK = 42, LA_CLIMB_3CLICK_END_TO_RUN = 43, LA_TURN_RIGHT = 44, LA_JUMP_FORWARD_TO_FREEFALL_2 = 45, LA_REACH_TO_FREEFALL = 46, LA_ROLL_ALTERNATE = 47, LA_ROLL_END_ALTERNATE = 48, LA_JUMP_FORWARD_END_TO_FREEFALL = 49, LA_CLIMB_2CLICK = 50, LA_CLIMB_2CLICK_END = 51, LA_CLIMB_2CLICK_END_TO_RUN = 52, LA_WALL_SMASH_LEFT = 53, LA_WALL_SMASH_RIGHT = 54, LA_RUN_UP_STEP_RIGHT = 55, LA_RUN_UP_STEP_LEFT = 56, LA_WALK_UP_STEP_RIGHT = 57, LA_WALK_UP_STEP_LEFT = 58, LA_WALK_DOWN_LEFT = 59, LA_WALK_DOWN_RIGHT = 60, LA_WALK_DOWN_BACK_LEFT = 61, LA_WALK_DOWN_BACK_RIGHT = 62, LA_WALL_SWITCH_DOWN = 63, LA_WALL_SWITCH_UP = 64, LA_SIDESTEP_LEFT = 65, LA_SIDESTEP_LEFT_END = 66, LA_SIDESTEP_RIGHT = 67, LA_SIDESTEP_RIGHT_END = 68, LA_ROTATE_LEFT = 69, LA_SLIDE_FORWARD = 70, LA_SLIDE_FORWARD_END = 71, LA_SLIDE_FORWARD_STOP = 72, LA_STAND_TO_JUMP = 73, LA_JUMP_BACK_START = 74, LA_JUMP_BACK = 75, LA_JUMP_FORWARD_START = 76, LA_JUMP_FORWARD = 77, LA_JUMP_LEFT_START = 78, LA_JUMP_LEFT = 79, LA_JUMP_RIGHT_START = 80, LA_JUMP_RIGHT = 81, LA_LAND = 82, LA_JUMP_BACK_TO_FREEFALL = 83, LA_JUMP_LEFT_TO_FREEFALL = 84, LA_JUMP_RIGHT_TO_FREEFALL = 85, LA_UNDERWATER_SWIM_FORWARD = 86, LA_UNDERWATER_SWIM_FORWARD_DRIFT = 87, LA_SMALL_JUMP_BACK_START = 88, LA_SMALL_JUMP_BACK = 89, LA_SMALL_JUMP_BACK_END = 90, LA_JUMP_UP_START = 91, LA_LAND_TO_RUN = 92, LA_FALL_BACK = 93, LA_JUMP_FORWARD_TO_REACH = 94, LA_REACH = 95, LA_REACH_TO_HANG = 96, LA_CLIMB_ON = 97, LA_REACH_TO_FREEFALL_2 = 98, LA_FALL_CROUCHING_LANDING = 99, LA_JUMP_FORWARD_TO_REACH_LATE = 100, LA_JUMP_FORWARD_START_TO_REACH_ALTERNATE = 101, LA_CLIMB_ON_END = 102, LA_STAND_IDLE = 103, LA_SLIDE_BACKWARD_START = 104, LA_SLIDE_BACKWARD = 105, LA_SLIDE_BACKWARD_END = 106, LA_UNDERWATER_SWIM_TO_IDLE = 107, LA_UNDERWATER_IDLE = 108, LA_UNDERWARER_IDLE_TO_SWIM = 109, LA_ONWATER_IDLE = 110, LA_ONWATER_TO_STAND_HIGH = 111, LA_FREEFALL_TO_UNDERWATER = 112, LA_ONWATER_DIVE_ALTERNATE = 113, LA_UNDERWATER_TO_ONWATER = 114, LA_ONWATER_SWIM_FORWARD_DIVE = 115, LA_ONWATER_SWIM_FORWARD = 116, LA_ONWATER_SWIM_FORWARD_TO_IDLE = 117, LA_ONWATER_IDLE_TO_SWIM_FORWARD = 118, LA_ONWATER_DIVE = 119, LA_PUSHABLE_GRAB = 120, LA_PUSHABLE_RELEASE = 121, LA_PUSHABLE_PULL = 122, LA_PUSHABLE_PUSH = 123, LA_UNDERWATER_DEATH = 124, LA_HIT_FRONT = 125, LA_HIT_BACK = 126, LA_HIT_LEFT = 127, LA_HIT_RIGHT = 128, LA_UNDERWATER_SWITCH = 129, LA_UNDERWATER_PICKUP = 130, LA_USE_KEY = 131, LA_ONWATER_DEATH = 132, LA_RUN_DEATH = 133, LA_USE_PUZZLE = 134, LA_PICKUP = 135, LA_SHIMMY_LEFT = 136, LA_SHIMMY_RIGHT = 137, LA_STAND_DEATH = 138, LA_BOULDER_DEATH = 139, LA_ONWATER_IDLE_TO_SWIM_BACK = 140, LA_ONWATER_SWIM_BACK = 141, LA_ONWATER_SWIM_BACK_TO_IDLE = 142, LA_ONWATER_SWIM_LEFT = 143, LA_ONWATER_SWIM_RIGHT = 144, LA_DEATH_JUMP = 145, LA_ROLL_START = 146, LA_ROLL_CONTINUE = 147, LA_ROLL_END = 148, LA_SPIKE_DEATH = 149, LA_REACH_TO_THIN_LEDGE = 150, LA_SWANDIVE_ROLL = 151, LA_SWANDIVE_TO_UNDERWATER = 152, LA_FREEFALL_SWANDIVE = 153, LA_FREEFALL_SWANDIVE_TO_UNDERWATER = 154, LA_SWANDIVE_DEATH = 155, LA_SWANDIVE_LEFT = 156, LA_SWANDIVE_RIGHT = 157, LA_SWANDIVE_START = 158, LA_CLIMB_ON_HANDSTAND = 159, LA_STAND_TO_LADDER = 160, LA_LADDER_UP = 161, LA_LADDER_UP_STOP_RIGHT = 162, LA_LADDER_UP_STOP_LEFT = 163, LA_LADDER_IDLE = 164, LA_LADDER_UP_START = 165, LA_LADDER_DOWN_STOP_LEFT = 166, LA_LADDER_DOWN_STOP_RIGHT = 167, LA_LADDER_DOWN = 168, LA_LADDER_DOWN_START = 169, LA_LADDER_RIGHT = 170, LA_LADDER_LEFT = 171, LA_LADDER_HANG = 172, LA_LADDER_HANG_TO_IDLE = 173, LA_LADDER_CLIMB_ON = 174, LA_UNKNOWN = 175, LA_ONWATER_TO_WADE_SHALLOW = 176, LA_WADE = 177, LA_RUN_TO_WADE_LEFT = 178, LA_RUN_TO_WADE_RIGHT = 179, LA_WADE_TO_RUN_LEFT = 180, LA_WADE_TO_RUN_RIGHT = 181, LA_LADDER_BACKFLIP_START = 182, LA_LADDER_BACKFLIP_CONTINUE = 183, LA_WADE_TO_STAND_RIGHT = 184, LA_WADE_TO_STAND_LEFT = 185, LA_STAND_TO_WADE = 186, LA_LADDER_UP_HANGING = 187, LA_LADDER_DOWN_HANGING = 188, LA_FLARE_THROW = 189, LA_ONWATER_TO_WADE = 190, LA_ONWATER_TO_STAND_MEDIUM = 191, LA_UNDERWATER_TO_STAND = 192, LA_ONWATER_TO_WADE_LOW = 193, LA_LADDER_TO_HANG_DOWN = 194, LA_SWITCH_SMALL_DOWN = 195, LA_SWITCH_SMALL_UP = 196, LA_BUTTON_PUSH = 197, LA_UNDERWATER_SWIM_TO_STILL_HUDDLE = 198, LA_UNDERWATER_SWIM_TO_STILL_SPRAWL = 199, LA_UNDERWATER_SWIM_TO_STILL_MEDIUM = 200, LA_LADDER_TO_HANG_RIGHT = 201, LA_LADDER_TO_HANG_LEFT = 202, LA_UNDERWATER_ROLL_START = 203, LA_FLARE_PICKUP = 204, LA_UNDERWATER_ROLL_END = 205, LA_UNDERWATER_FLARE_PICKUP = 206, LA_RUN_JUMP_ROLL_START = 207, LA_SOMERSAULT = 208, LA_RUN_JUMP_ROLL_END = 209, LA_JUMP_FORWARD_ROLL_START = 210, LA_JUMP_FORWARD_ROLL_END = 211, LA_JUMP_BACK_ROLL_START = 212, LA_JUMP_BACK_ROLL_END = 213, LA_KICK = 214, LA_ZIPLINE_GRAB = 215, LA_ZIPLINE_RIDE = 216, LA_ZIPLINE_FALL = 217, } LARA_ANIMATION; typedef enum { LA_EXTRA_BREATH = 0, LA_EXTRA_PLUNGER = 1, LA_EXTRA_YETI_KILL = 2, LA_EXTRA_SHARK_KILL = 3, LA_EXTRA_AIRLOCK = 4, LA_EXTRA_GONG_BONG = 5, LA_EXTRA_TREX_KILL = 6, LA_EXTRA_PULL_DAGGER = 7, LA_EXTRA_START_ANIM = 8, LA_EXTRA_START_HOUSE = 9, LA_EXTRA_FINAL_ANIM = 10, } LARA_EXTRA_ANIMATION; typedef enum { LS_WALK = 0, LS_RUN = 1, LS_STOP = 2, LS_JUMP_FORWARD = 3, LS_POSE = 4, LS_FAST_BACK = 5, LS_TURN_RIGHT = 6, LS_TURN_LEFT = 7, LS_DEATH = 8, LS_FAST_FALL = 9, LS_HANG = 10, LS_REACH = 11, LS_SPLAT = 12, LS_TREAD = 13, LS_LAND = 14, LS_COMPRESS = 15, LS_BACK = 16, LS_SWIM = 17, LS_GLIDE = 18, LS_NULL = 19, LS_FAST_TURN = 20, LS_STEP_RIGHT = 21, LS_STEP_LEFT = 22, LS_HIT = 23, LS_SLIDE = 24, LS_JUMP_BACK = 25, LS_JUMP_RIGHT = 26, LS_JUMP_LEFT = 27, LS_JUMP_UP = 28, LS_FALL_BACK = 29, LS_HANG_LEFT = 30, LS_HANG_RIGHT = 31, LS_SLIDE_BACK = 32, LS_SURF_TREAD = 33, LS_SURF_SWIM = 34, LS_DIVE = 35, LS_PUSH_BLOCK = 36, LS_PULL_BLOCK = 37, LS_PP_READY = 38, LS_PICKUP = 39, LS_SWITCH_ON = 40, LS_SWITCH_OFF = 41, LS_USE_KEY = 42, LS_USE_PUZZLE = 43, LS_UW_DEATH = 44, LS_ROLL = 45, LS_SPECIAL = 46, LS_SURF_BACK = 47, LS_SURF_LEFT = 48, LS_SURF_RIGHT = 49, LS_USE_MIDAS = 50, LS_DIE_MIDAS = 51, LS_SWAN_DIVE = 52, LS_FAST_DIVE = 53, LS_GYMNAST = 54, LS_WATER_OUT = 55, LS_CLIMB_STANCE = 56, LS_CLIMBING = 57, LS_CLIMB_LEFT = 58, LS_CLIMB_END = 59, LS_CLIMB_RIGHT = 60, LS_CLIMB_DOWN = 61, LS_LARA_TEST1 = 62, LS_LARA_TEST2 = 63, LS_LARA_TEST3 = 64, LS_WADE = 65, LS_WATER_ROLL = 66, LS_FLARE_PICKUP = 67, LS_TWIST = 68, LS_KICK = 69, LS_ZIPLINE = 70, } LARA_STATE; typedef enum { LGS_ARMLESS = 0, LGS_HANDS_BUSY = 1, LGS_DRAW = 2, LGS_UNDRAW = 3, LGS_READY = 4, LGS_SPECIAL = 5, } LARA_GUN_STATE; typedef enum { LGT_UNARMED = 0, LGT_PISTOLS = 1, LGT_MAGNUMS = 2, LGT_UZIS = 3, LGT_SHOTGUN = 4, LGT_M16 = 5, LGT_GRENADE = 6, LGT_HARPOON = 7, LGT_FLARE = 8, LGT_SKIDOO = 9, NUM_WEAPONS = 10, } LARA_GUN_TYPE; typedef enum { LM_HIPS = 0, LM_THIGH_L = 1, LM_CALF_L = 2, LM_FOOT_L = 3, LM_THIGH_R = 4, LM_CALF_R = 5, LM_FOOT_R = 6, LM_TORSO = 7, LM_UARM_R = 8, LM_LARM_R = 9, LM_HAND_R = 10, LM_UARM_L = 11, LM_LARM_L = 12, LM_HAND_L = 13, LM_HEAD = 14, LM_NUMBER_OF = 15, } LARA_MESH; typedef enum { NO_OBJECT = -1, O_LARA = 0, O_LARA_PISTOLS = 1, O_LARA_HAIR = 2, O_LARA_SHOTGUN = 3, O_LARA_MAGNUMS = 4, O_LARA_UZIS = 5, O_LARA_M16 = 6, O_LARA_GRENADE = 7, O_LARA_HARPOON = 8, O_LARA_FLARE = 9, O_LARA_SKIDOO = 10, O_LARA_BOAT = 11, O_LARA_EXTRA = 12, O_SKIDOO_FAST = 13, O_BOAT = 14, O_DOG = 15, O_CULT_1 = 16, O_CULT_1A = 17, O_CULT_1B = 18, O_CULT_2 = 19, O_CULT_3 = 20, O_MOUSE = 21, O_DRAGON_FRONT = 22, O_DRAGON_BACK = 23, O_GONDOLA = 24, O_SHARK = 25, O_EEL = 26, O_BIG_EEL = 27, O_BARRACUDA = 28, O_DIVER = 29, O_WORKER_1 = 30, O_WORKER_2 = 31, O_WORKER_3 = 32, O_WORKER_4 = 33, O_WORKER_5 = 34, O_JELLY = 35, O_SPIDER = 36, O_BIG_SPIDER = 37, O_CROW = 38, O_TIGER = 39, O_BARTOLI = 40, O_XIAN_SPEARMAN = 41, O_XIAN_SPEARMAN_STATUE = 42, O_XIAN_KNIGHT = 43, O_XIAN_KNIGHT_STATUE = 44, O_YETI = 45, O_BIRD_GUARDIAN = 46, O_EAGLE = 47, O_BANDIT_1 = 48, O_BANDIT_2 = 49, O_BANDIT_2B = 50, O_SKIDOO_ARMED = 51, O_SKIDOO_DRIVER = 52, O_MONK_1 = 53, O_MONK_2 = 54, O_FALLING_BLOCK_1 = 55, O_FALLING_BLOCK_2 = 56, O_FALLING_BLOCK_3 = 57, O_PENDULUM_1 = 58, O_SPIKES = 59, O_ROLLING_BALL_1 = 60, O_DART = 61, O_DART_EMITTER = 62, O_DRAWBRIDGE = 63, O_TEETH_TRAP = 64, O_LIFT = 65, O_GENERAL = 66, O_MOVABLE_BLOCK_1 = 67, O_MOVABLE_BLOCK_2 = 68, O_MOVABLE_BLOCK_3 = 69, O_MOVABLE_BLOCK_4 = 70, O_BIG_BOWL = 71, O_WINDOW_1 = 72, O_WINDOW_2 = 73, O_WINDOW_3 = 74, O_WINDOW_4 = 75, O_PROPELLER_1 = 76, O_POWER_SAW = 77, O_HOOK = 78, O_FALLING_CEILING = 79, O_SPINNING_BLADE = 80, O_BLADE = 81, O_KILLER_STATUE = 82, O_ROLLING_BALL_2 = 83, O_ICICLE = 84, O_SPIKE_WALL = 85, O_SPRINGBOARD = 86, O_CEILING_SPIKES = 87, O_BELL = 88, O_WATER_SPRITE = 89, O_SNOW_SPRITE = 90, O_SKIDOO_TRACK = 91, O_SWITCH_TYPE_AIRLOCK = 92, O_SWITCH_TYPE_SMALL = 93, O_PROPELLER_2 = 94, O_PROPELLER_3 = 95, O_PENDULUM_2 = 96, O_MESH_SWAP_1 = 97, O_MESH_SWAP_2 = 98, O_LARA_SWAP = 99, O_TEXT_BOX = 100, O_ROLLING_BALL_3 = 101, O_ZIPLINE_HANDLE = 102, O_SWITCH_TYPE_BUTTON = 103, O_SWITCH_TYPE_NORMAL = 104, O_SWITCH_TYPE_UW = 105, O_DOOR_TYPE_1 = 106, O_DOOR_TYPE_2 = 107, O_DOOR_TYPE_3 = 108, O_DOOR_TYPE_4 = 109, O_DOOR_TYPE_5 = 110, O_DOOR_TYPE_6 = 111, O_DOOR_TYPE_7 = 112, O_DOOR_TYPE_8 = 113, O_TRAPDOOR_TYPE_1 = 114, O_TRAPDOOR_TYPE_2 = 115, O_TRAPDOOR_TYPE_3 = 116, O_BRIDGE_FLAT = 117, O_BRIDGE_TILT_1 = 118, O_BRIDGE_TILT_2 = 119, O_PASSPORT_OPTION = 120, O_COMPASS_OPTION = 121, O_PHOTO_OPTION = 122, O_PLAYER_1 = 123, O_PLAYER_2 = 124, O_PLAYER_3 = 125, O_PLAYER_4 = 126, O_PLAYER_5 = 127, O_PLAYER_6 = 128, O_PLAYER_7 = 129, O_PLAYER_8 = 130, O_PLAYER_9 = 131, O_PLAYER_10 = 132, O_PASSPORT_CLOSED = 133, O_COMPASS_ITEM = 134, O_PISTOL_ITEM = 135, O_SHOTGUN_ITEM = 136, O_MAGNUM_ITEM = 137, O_UZI_ITEM = 138, O_HARPOON_ITEM = 139, O_M16_ITEM = 140, O_GRENADE_ITEM = 141, O_PISTOL_AMMO_ITEM = 142, O_SHOTGUN_AMMO_ITEM = 143, O_MAGNUM_AMMO_ITEM = 144, O_UZI_AMMO_ITEM = 145, O_HARPOON_AMMO_ITEM = 146, O_M16_AMMO_ITEM = 147, O_GRENADE_AMMO_ITEM = 148, O_SMALL_MEDIPACK_ITEM = 149, O_LARGE_MEDIPACK_ITEM = 150, O_FLARES_ITEM = 151, O_FLARE_ITEM = 152, O_DETAIL_OPTION = 153, O_SOUND_OPTION = 154, O_CONTROL_OPTION = 155, O_GAMMA_OPTION = 156, O_PISTOL_OPTION = 157, O_SHOTGUN_OPTION = 158, O_MAGNUM_OPTION = 159, O_UZI_OPTION = 160, O_HARPOON_OPTION = 161, O_M16_OPTION = 162, O_GRENADE_OPTION = 163, O_PISTOL_AMMO_OPTION = 164, O_SHOTGUN_AMMO_OPTION = 165, O_MAGNUM_AMMO_OPTION = 166, O_UZI_AMMO_OPTION = 167, O_HARPOON_AMMO_OPTION = 168, O_M16_AMMO_OPTION = 169, O_GRENADE_AMMO_OPTION = 170, O_SMALL_MEDIPACK_OPTION = 171, O_LARGE_MEDIPACK_OPTION = 172, O_FLARES_OPTION = 173, O_PUZZLE_ITEM_1 = 174, O_PUZZLE_ITEM_2 = 175, O_PUZZLE_ITEM_3 = 176, O_PUZZLE_ITEM_4 = 177, O_PUZZLE_OPTION_1 = 178, O_PUZZLE_OPTION_2 = 179, O_PUZZLE_OPTION_3 = 180, O_PUZZLE_OPTION_4 = 181, O_PUZZLE_HOLE_1 = 182, O_PUZZLE_HOLE_2 = 183, O_PUZZLE_HOLE_3 = 184, O_PUZZLE_HOLE_4 = 185, O_PUZZLE_DONE_1 = 186, O_PUZZLE_DONE_2 = 187, O_PUZZLE_DONE_3 = 188, O_PUZZLE_DONE_4 = 189, O_SECRET_1 = 190, O_SECRET_2 = 191, O_SECRET_3 = 192, O_KEY_ITEM_1 = 193, O_KEY_ITEM_2 = 194, O_KEY_ITEM_3 = 195, O_KEY_ITEM_4 = 196, O_KEY_OPTION_1 = 197, O_KEY_OPTION_2 = 198, O_KEY_OPTION_3 = 199, O_KEY_OPTION_4 = 200, O_KEY_HOLE_1 = 201, O_KEY_HOLE_2 = 202, O_KEY_HOLE_3 = 203, O_KEY_HOLE_4 = 204, O_PICKUP_ITEM_1 = 205, O_PICKUP_ITEM_2 = 206, O_PICKUP_OPTION_1 = 207, O_PICKUP_OPTION_2 = 208, O_SPHERE_OF_DOOM_1 = 209, O_SPHERE_OF_DOOM_2 = 210, O_SPHERE_OF_DOOM_3 = 211, O_ALARM_SOUND = 212, O_BIRD_TWEETER_1 = 213, O_DINO = 214, O_BIRD_TWEETER_2 = 215, O_CLOCK_CHIMES = 216, O_DRAGON_BONES_1 = 217, O_DRAGON_BONES_2 = 218, O_DRAGON_BONES_3 = 219, O_HOT_LIQUID = 220, O_BOAT_BITS = 221, O_MINE = 222, O_INV_BACKGROUND = 223, O_FX_RESERVED = 224, O_GONG_BONGER = 225, O_DETONATOR_1 = 226, O_DETONATOR_2 = 227, O_COPTER = 228, O_EXPLOSION = 229, O_SPLASH = 230, O_BUBBLE = 231, O_BUBBLE_EMITTER = 232, O_BLOOD = 233, O_DART_EFFECT = 234, O_FLARE_FIRE = 235, O_GLOW = 236, O_GLOW_RESERVED = 237, O_RICOCHET = 238, O_TWINKLE = 239, O_GUN_FLASH = 240, O_M16_FLASH = 241, O_BODY_PART = 242, O_CAMERA_TARGET = 243, O_WATERFALL = 244, O_MISSILE_HARPOON = 245, O_MISSILE_FLAME = 246, O_MISSILE_KNIFE = 247, O_GRENADE = 248, O_HARPOON_BOLT = 249, O_EMBER = 250, O_EMBER_EMITTER = 251, O_FLAME = 252, O_FLAME_EMITTER = 253, O_SKYBOX = 254, O_ALPHABET = 255, O_DYING_MONK = 256, O_DING_DONG = 257, O_LARA_ALARM = 258, O_MINI_COPTER = 259, O_WINSTON = 260, O_ASSAULT_DIGITS = 261, O_FINAL_LEVEL_COUNTER = 262, O_CUT_SHOTGUN = 263, O_EARTHQUAKE = 264, O_NUMBER_OF = 265, } GAME_OBJECT_ID; typedef enum { MX_INACTIVE = -1, MX_UNUSED_0 = 0, // 2.mp3 MX_UNUSED_1 = 1, // 2.mp3 MX_CUTSCENE_THE_GREAT_WALL = 2, // 2.mp3 MX_UNUSED_2 = 3, // 2.mp3 MX_CUTSCENE_OPERA_HOUSE = 4, // 3.mp3 MX_CUTSCENE_BROTHER_CHAN = 5, // 4.mp3 MX_GYM_HINT_1 = 6, // 5.mp3 MX_GYM_HINT_2 = 7, // 6.mp3 MX_GYM_HINT_3 = 8, // 7.mp3 MX_GYM_HINT_4 = 9, // 8.mp3 MX_GYM_HINT_5 = 10, // 9.mp3 MX_GYM_HINT_6 = 11, // 10.mp3 MX_GYM_HINT_7 = 12, // 11.mp3 MX_GYM_HINT_8 = 13, // 12.mp3 MX_GYM_HINT_9 = 14, // 13.mp3 MX_GYM_HINT_10 = 15, // 14.mp3 MX_GYM_HINT_11 = 16, // 15.mp3 MX_GYM_HINT_12 = 17, // 16.mp3 MX_GYM_HINT_13 = 18, // 17.mp3 MX_GYM_HINT_14 = 19, // 18.mp3 MX_UNUSED_3 = 20, // 18.mp3 MX_UNUSED_4 = 21, // 18.mp3 MX_GYM_HINT_15 = 22, // 19.mp3 MX_GYM_HINT_16 = 23, // 20.mp3 MX_GYM_HINT_17 = 24, // 21.mp3 MX_GYM_HINT_18 = 25, // 22.mp3 MX_UNUSED_5 = 26, // 23.mp3 MX_CUTSCENE_BATH = 27, // 23.mp3 MX_DAGGER_PULL = 28, // 24.mp3 MX_GYM_HINT_20 = 29, // 25.mp3 MX_CUTSCENE_XIAN = 30, // 26.mp3 MX_CAVES_AMBIENCE = 31, // 27.mp3 MX_SEWERS_AMBIENCE = 32, // 28.mp3 MX_WINDY_AMBIENCE = 33, // 29.mp3 MX_HEARTBEAT_AMBIENCE = 34, // 30.mp3 MX_SURPRISE_1 = 35, // 31.mp3 MX_SURPRISE_2 = 36, // 32.mp3 MX_SURPRISE_3 = 37, // 33.mp3 MX_OOH_AAH_1 = 38, // 34.mp3 MX_OOH_AAH_2 = 39, // 35.mp3 MX_VENICE_VIOLINS = 40, // 36.mp3 MX_END_OF_LEVEL = 41, // 37.mp3 MX_SPOOKY_1 = 42, // 38.mp3 MX_SPOOKY_2 = 43, // 39.mp3 MX_SPOOKY_3 = 44, // 40.mp3 MX_HARP_THEME = 45, // 41.mp3 MX_MYSTERY_1 = 46, // 42.mp3 MX_SECRET = 47, // 43.mp3 MX_AMBUSH_1 = 48, // 44.mp3 MX_AMBUSH_2 = 49, // 45.mp3 MX_AMBUSH_3 = 50, // 46.mp3 MX_AMBUSH_4 = 51, // 47.mp3 MX_SKIDOO_THEME = 52, // 48.mp3 MX_BATTLE_THEME = 53, // 49.mp3 MX_MYSTERY_2 = 54, // 50.mp3 MX_MYSTERY_3 = 55, // 51.mp3 MX_MYSTERY_4 = 56, // 52.mp3 MX_MYSTERY_5 = 57, // 53.mp3 MX_RIG_AMBIENCE = 58, // 54.mp3 MX_TOMB_AMBIENCE = 59, // 55.mp3 MX_OOH_AAH_3 = 60, // 56.mp3 MX_REVEAL_1 = 61, // 57.mp3 MX_CUTSCENE_RIG = 62, // 58.mp3 MX_REVEAL_2 = 63, // 59.mp3 MX_TITLE_THEME = 64, // 60.mp3 MX_UNUSED_6 = 65, // 61.mp3 } MUSIC_TRACK_ID; typedef enum { COLL_NONE = 0x00, COLL_FRONT = 0x01, COLL_LEFT = 0x02, COLL_RIGHT = 0x04, COLL_TOP = 0x08, COLL_TOP_FRONT = 0x10, COLL_CLAMP = 0x20, } COLL_TYPE; typedef enum { FT_FLOOR = 0, FT_DOOR = 1, FT_TILT = 2, FT_ROOF = 3, FT_TRIGGER = 4, FT_LAVA = 5, FT_CLIMB = 6, } FLOOR_TYPE; typedef enum { HT_WALL = 0, HT_SMALL_SLOPE = 1, HT_BIG_SLOPE = 2, } HEIGHT_TYPE; typedef enum { DIR_UNKNOWN = -1, DIR_NORTH = 0, DIR_EAST = 1, DIR_SOUTH = 2, DIR_WEST = 3, } DIRECTION; typedef struct __unaligned { uint16_t x; uint16_t y; } XGEN_X; typedef struct __unaligned { int32_t x1; int32_t x2; } XBUF_X; typedef struct __unaligned { int16_t x; int16_t y; int16_t g; } XGEN_XG; typedef struct __unaligned { int32_t x1; int32_t g1; int32_t x2; int32_t g2; } XBUF_XG; typedef struct __unaligned { uint16_t x; uint16_t y; uint16_t g; uint16_t u; uint16_t v; } XGEN_XGUV; typedef struct __unaligned { int32_t x1; int32_t g1; int32_t u1; int32_t v1; int32_t x2; int32_t g2; int32_t u2; int32_t v2; } XBUF_XGUV; typedef struct __unaligned { uint16_t x; uint16_t y; uint16_t g; float rhw; float u; float v; } XGEN_XGUVP; typedef struct __unaligned { int32_t x1; int32_t g1; float u1; float v1; float rhw1; int32_t x2; int32_t g2; float u2; float v2; float rhw2; } XBUF_XGUVP; typedef struct __unaligned { uint8_t manufacturer; uint8_t version; uint8_t rle; uint8_t bpp; uint16_t x_min; uint16_t y_min; uint16_t x_max; uint16_t y_max; uint16_t h_dpi; uint16_t v_dpi; RGB_888 palette[16]; uint8_t reserved; uint8_t planes; uint16_t bytes_per_line; uint16_t pal_pnterpret; uint16_t h_res; uint16_t v_res; uint8_t reserved_data[54]; } PCX_HEADER; typedef struct __unaligned { uint8_t id_length; uint8_t color_map_type; uint8_t data_type_code; uint16_t color_map_origin; uint16_t color_map_length; uint8_t color_map_depth; uint16_t x_origin; uint16_t y_origin; uint16_t width; uint16_t height; uint8_t bpp; uint8_t image_descriptor; } TGA_HEADER; typedef struct __unaligned { int16_t number; int16_t volume; int16_t randomness; int16_t flags; } SAMPLE_INFO; /* typedef struct __unaligned { int32_t volume; int32_t pan; int32_t sample_num; int32_t pitch; } SOUND_SLOT; */ typedef enum { SF_FLIP = 0x40, SF_UNFLIP = 0x80, } SOUND_FLAG; typedef enum { GBUF_TEMP_ALLOC = 0, GBUF_TEXTURE_PAGES = 1, GBUF_MESH_POINTERS = 2, GBUF_MESHES = 3, GBUF_ANIMS = 4, GBUF_STRUCTS = 5, GBUF_ANIM_RANGES = 6, GBUF_ANIM_COMMANDS = 7, GBUF_ANIM_BONES = 8, GBUF_ANIM_FRAMES = 9, GBUF_ROOM_TEXTURES = 10, GBUF_ROOMS = 11, GBUF_ROOM_MESH = 12, GBUF_ROOM_PORTALS = 13, GBUF_ROOM_FLOOR = 14, GBUF_ROOM_LIGHTS = 15, GBUF_ROOM_STATIC_MESHES = 16, GBUF_FLOOR_DATA = 17, GBUF_ITEMS = 18, GBUF_CAMERAS = 19, GBUF_SOUND_FX = 20, GBUF_BOXES = 21, GBUF_OVERLAPS = 22, GBUF_GROUND_ZONE = 23, GBUF_FLY_ZONE = 24, GBUF_ANIMATING_TEXTURE_RANGES = 25, GBUF_CINEMATIC_FRAMES = 26, GBUF_LOAD_DEMO_BUFFER = 27, GBUF_SAVE_DEMO_BUFFER = 28, GBUF_CINEMATIC_EFFECTS = 29, GBUF_MUMMY_HEAD_TURN = 30, GBUF_EXTRA_DOOR_STUFF = 31, GBUF_EFFECTS_ARRAY = 32, GBUF_CREATURE_DATA = 33, GBUF_CREATURE_LOT = 34, GBUF_SAMPLE_INFOS = 35, GBUF_SAMPLES = 36, GBUF_SAMPLE_OFFSETS = 37, GBUF_ROLLING_BALL_STUFF = 38, GBUF_SKIDOO_STUFF = 39, GBUF_LOAD_PICTURE_BUFFER = 40, GBUF_FMV_BUFFERS = 41, GBUF_POLYGON_BUFFERS = 42, GBUF_ORDER_TABLES = 43, GBUF_CLUTS = 44, GBUF_TEXTURE_INFOS = 45, GBUF_SPRITE_INFOS = 46, GBUF_NUM_MALLOC_TYPES = 47, } GAME_BUFFER; typedef enum { CLRB_PRIMARY_BUFFER = 0x0001, CLRB_BACK_BUFFER = 0x0002, CLRB_THIRD_BUFFER = 0x0004, CLRB_Z_BUFFER = 0x0008, CLRB_RENDER_BUFFER = 0x0010, CLRB_PICTURE_BUFFER = 0x0020, CLRB_WINDOWED_PRIMARY_BUFFER = 0x0040, CLRB_RESERVED = 0x0080, CLRB_PHDWINSIZE = 0x0100, } CLEAR_BUFFER_FLAGS; typedef enum { AC_NULL = 0, AC_MOVE_ORIGIN = 1, AC_JUMP_VELOCITY = 2, AC_ATTACK_READY = 3, AC_DEACTIVATE = 4, AC_SOUND_FX = 5, AC_EFFECT = 6, } ANIM_COMMAND; typedef enum { ACE_ALL = 0, ACE_LAND = 1, ACE_WATER = 2, } ANIM_COMMAND_ENVIRONMENT; typedef struct __unaligned { DDPIXELFORMAT pixel_fmt; COLOR_BIT_MASKS color_bit_masks; DWORD bpp; } TEXTURE_FORMAT; typedef struct __unaligned { int32_t boat_turn; int32_t left_fallspeed; int32_t right_fallspeed; int16_t tilt_angle; int16_t extra_rotation; int32_t water; int32_t pitch; } BOAT_INFO; typedef struct __unaligned { int16_t track_mesh; int32_t skidoo_turn; int32_t left_fallspeed; int32_t right_fallspeed; int16_t momentum_angle; int16_t extra_rotation; int32_t pitch; } SKIDOO_INFO; typedef struct __unaligned { int32_t start_height; int32_t wait_time; } LIFT_INFO; typedef struct __unaligned { struct { XYZ_16 min; XYZ_16 max; } shift, rot; } OBJECT_BOUNDS; typedef struct __unaligned { int32_t xv; int32_t yv; int32_t zv; } PORTAL_VBUF; typedef struct __unaligned { BOUNDS_16 bounds; XYZ_16 offset; int16_t mesh_rots[]; } FRAME_INFO; typedef struct __unaligned { XYZ_16 pos; int16_t radius; int16_t poly_count; int16_t vertex_count; XYZ_16 vertex[32]; } SHADOW_INFO; typedef struct __unaligned { int32_t table[32]; // WIBBLE_SIZE } ROOM_LIGHT_TABLE; typedef struct __unaligned { XYZ_16 pos; int16_t light_base; uint8_t light_table_value; uint8_t flags; int16_t light_adder; } ROOM_VERTEX; typedef struct __unaligned { XYZ_32 pos; XYZ_16 rot; } HAIR_SEGMENT; typedef enum { TO_OBJECT = 0, TO_CAMERA = 1, TO_SINK = 2, TO_FLIP_MAP = 3, TO_FLIP_ON = 4, TO_FLIP_OFF = 5, TO_TARGET = 6, TO_FINISH = 7, TO_CD = 8, TO_FLIP_EFFECT = 9, TO_SECRET = 10, TO_BODY_BAG = 11, } TRIGGER_OBJECT; typedef enum { TT_TRIGGER = 0, TT_PAD = 1, TT_SWITCH = 2, TT_KEY = 3, TT_PICKUP = 4, TT_HEAVY = 5, TT_ANTIPAD = 6, TT_COMBAT = 7, TT_DUMMY = 8, TT_ANTITRIGGER = 9, } TRIGGER_TYPE; typedef enum { GF_S_PC_DETAIL_LEVELS = 0, GF_S_PC_DEMO_MODE = 1, GF_S_PC_SOUND = 2, GF_S_PC_CONTROLS = 3, GF_S_PC_GAMMA = 4, GF_S_PC_SET_VOLUMES = 5, GF_S_PC_USER_KEYS = 6, GF_S_PC_SAVE_FILE_WARNING = 7, GF_S_PC_TRY_AGAIN_QUESTION = 8, GF_S_PC_YES = 9, GF_S_PC_NO = 10, GF_S_PC_SAVE_COMPLETE = 11, GF_S_PC_NO_SAVE_GAMES = 12, GF_S_PC_NONE_VALID = 13, GF_S_PC_SAVE_GAME_QUESTION = 14, GF_S_PC_EMPTY_SLOT = 15, GF_S_PC_OFF = 16, GF_S_PC_ON = 17, GF_S_PC_SETUP_SOUND_CARD = 18, GF_S_PC_DEFAULT_KEYS = 19, GF_S_PC_DOZY = 20, GF_S_PC_NUMBER_OF = 41, } GF_PC_STRING; typedef enum { GF_S_GAME_HEADING_INVENTORY = 0, GF_S_GAME_HEADING_OPTION = 1, GF_S_GAME_HEADING_ITEMS = 2, GF_S_GAME_HEADING_GAME_OVER = 3, GF_S_GAME_PASSPORT_LOAD_GAME = 4, GF_S_GAME_PASSPORT_SAVE_GAME = 5, GF_S_GAME_PASSPORT_NEW_GAME = 6, GF_S_GAME_PASSPORT_RESTART_LEVEL = 7, GF_S_GAME_PASSPORT_EXIT_TO_TITLE = 8, GF_S_GAME_PASSPORT_EXIT_DEMO = 9, GF_S_GAME_PASSPORT_EXIT_GAME = 10, GF_S_GAME_PASSPORT_SELECT_LEVEL = 11, GF_S_GAME_PASSPORT_SAVE_POSITION = 12, GF_S_GAME_DETAIL_SELECT_DETAIL = 13, GF_S_GAME_DETAIL_HIGH = 14, GF_S_GAME_DETAIL_MEDIUM = 15, GF_S_GAME_DETAIL_LOW = 16, GF_S_GAME_KEYMAP_WALK = 17, GF_S_GAME_KEYMAP_ROLL = 18, GF_S_GAME_KEYMAP_RUN = 19, GF_S_GAME_KEYMAP_LEFT = 20, GF_S_GAME_KEYMAP_RIGHT = 21, GF_S_GAME_KEYMAP_BACK = 22, GF_S_GAME_KEYMAP_STEP_LEFT = 23, GF_S_GAME_KEYMAP_RESERVED_1 = 24, GF_S_GAME_KEYMAP_STEP_RIGHT = 25, GF_S_GAME_KEYMAP_RESERVED_2 = 26, GF_S_GAME_KEYMAP_LOOK = 27, GF_S_GAME_KEYMAP_JUMP = 28, GF_S_GAME_KEYMAP_ACTION = 29, GF_S_GAME_KEYMAP_DRAW_WEAPON = 30, GF_S_GAME_KEYMAP_RESERVED_3 = 31, GF_S_GAME_KEYMAP_INVENTORY = 32, GF_S_GAME_KEYMAP_FLARE = 33, GF_S_GAME_KEYMAP_STEP = 34, GF_S_GAME_INV_ITEM_STATISTICS = 35, GF_S_GAME_INV_ITEM_PISTOLS = 36, GF_S_GAME_INV_ITEM_SHOTGUN = 37, GF_S_GAME_INV_ITEM_MAGNUMS = 38, GF_S_GAME_INV_ITEM_UZIS = 39, GF_S_GAME_INV_ITEM_HARPOON = 40, GF_S_GAME_INV_ITEM_M16 = 41, GF_S_GAME_INV_ITEM_GRENADE = 42, GF_S_GAME_INV_ITEM_FLARE = 43, GF_S_GAME_INV_ITEM_PISTOL_AMMO = 44, GF_S_GAME_INV_ITEM_SHOTGUN_AMMO = 45, GF_S_GAME_INV_ITEM_MAGNUM_AMMO = 46, GF_S_GAME_INV_ITEM_UZI_AMMO = 47, GF_S_GAME_INV_ITEM_HARPOON_AMMO = 48, GF_S_GAME_INV_ITEM_M16_AMMO = 49, GF_S_GAME_INV_ITEM_GRENADE_AMMO = 50, GF_S_GAME_INV_ITEM_SMALL_MEDIPACK = 51, GF_S_GAME_INV_ITEM_LARGE_MEDIPACK = 52, GF_S_GAME_INV_ITEM_PICKUP = 53, GF_S_GAME_INV_ITEM_PUZZLE = 54, GF_S_GAME_INV_ITEM_KEY = 55, GF_S_GAME_INV_ITEM_GAME = 56, GF_S_GAME_INV_ITEM_LARA_HOME = 57, GF_S_GAME_MISC_LOADING = 58, GF_S_GAME_MISC_TIME_TAKEN = 59, GF_S_GAME_MISC_SECRETS_FOUND = 60, GF_S_GAME_MISC_LOCATION = 61, GF_S_GAME_MISC_KILLS = 62, GF_S_GAME_MISC_AMMO_USED = 63, GF_S_GAME_MISC_HITS = 64, GF_S_GAME_MISC_SAVES_PERFORMED = 65, GF_S_GAME_MISC_DISTANCE_TRAVELLED = 66, GF_S_GAME_MISC_HEALTH_PACKS_USED = 67, GF_S_GAME_MISC_RELEASE_VERSION = 68, GF_S_GAME_MISC_NONE = 69, GF_S_GAME_MISC_FINISH = 70, GF_S_GAME_MISC_BEST_TIMES = 71, GF_S_GAME_MISC_NO_TIMES_SET = 72, GF_S_GAME_MISC_NA = 73, GF_S_GAME_MISC_CURRENT_POSITION = 74, GF_S_GAME_MISC_FINAL_STATISTICS = 75, GF_S_GAME_MISC_OF = 76, GF_S_GAME_MISC_STORY_SO_FAR = 77, GF_S_GAME_NUMBER_OF = 89, } GF_GAME_STRING; typedef enum { GF_ADD_INV_PISTOLS = 0, GF_ADD_INV_SHOTGUN = 1, GF_ADD_INV_MAGNUMS = 2, GF_ADD_INV_UZIS = 3, GF_ADD_INV_HARPOON = 4, GF_ADD_INV_M16 = 5, GF_ADD_INV_GRENADE = 6, GF_ADD_INV_PISTOL_AMMO = 7, GF_ADD_INV_SHOTGUN_AMMO = 8, GF_ADD_INV_MAGNUM_AMMO = 9, GF_ADD_INV_UZI_AMMO = 10, GF_ADD_INV_HARPOON_AMMO = 11, GF_ADD_INV_M16_AMMO = 12, GF_ADD_INV_GRENADE_AMMO = 13, GF_ADD_INV_FLARES = 14, GF_ADD_INV_SMALL_MEDI = 15, GF_ADD_INV_LARGE_MEDI = 16, GF_ADD_INV_PICKUP_1 = 17, GF_ADD_INV_PICKUP_2 = 18, GF_ADD_INV_PUZZLE_1 = 19, GF_ADD_INV_PUZZLE_2 = 20, GF_ADD_INV_PUZZLE_3 = 21, GF_ADD_INV_PUZZLE_4 = 22, GF_ADD_INV_KEY_1 = 23, GF_ADD_INV_KEY_2 = 24, GF_ADD_INV_KEY_3 = 25, GF_ADD_INV_KEY_4 = 26, GF_ADD_INV_NUMBER_OF = 27, } GF_ADD_INV; typedef enum { IT_NAME = 0, IT_QTY = 1, IT_NUMBER_OF = 2, } INV_TEXT; typedef enum { REQ_CENTER = 0x00, REQ_USE = 0x01, REQ_ALIGN_LEFT = 0x02, REQ_ALIGN_RIGHT = 0x04, REQ_HEADING = 0x08, REQ_BEST_TIME = 0x10, REQ_NORMAL_TIME = 0x20, REQ_NO_TIME = 0x40, } REQUESTER_FLAGS; # FUNCTIONS # Offset Size Declaration # 3dsystem/3d_gen.c 0x00401000 0x01D0 void __cdecl Matrix_GenerateW2V(PHD_3DPOS *viewpos); 0x004011D0 0x0072 void __cdecl Matrix_LookAt(int32_t xsrc, int32_t ysrc, int32_t zsrc, int32_t xtar, int32_t ytar, int32_t ztar, int16_t roll); 0x00401250 0x0078 void __cdecl Math_GetVectorAngles(int32_t x, int32_t y, int32_t z, int16_t *dest); 0x004012D0 0x00AA void __cdecl Matrix_RotX(int16_t rx); 0x00401380 0x00A8 void __cdecl Matrix_RotY(int16_t ry); 0x00401430 0x00A8 void __cdecl Matrix_RotZ(int16_t rz); 0x004014E0 0x01DC void __cdecl Matrix_RotYXZ(int16_t ry, int16_t rx, int16_t rz); 0x004016C0 0x01E7 void __cdecl Matrix_RotYXZpack(uint32_t rpack); 0x004018B0 0x00AB bool __cdecl Matrix_TranslateRel(int32_t x, int32_t y, int32_t z); 0x00401960 0x007A void __cdecl Matrix_TranslateAbs(int32_t x, int32_t y, int32_t z); 0x004019E0 0x00F3 void __cdecl Output_InsertPolygons(const int16_t *obj_ptr, int32_t clip); 0x00401AE0 0x00EA void __cdecl Output_InsertRoom(const int16_t *obj_ptr, int32_t is_outside); 0x00401BD0 0x0032 const int16_t *__cdecl Output_CalcSkyboxLight(const int16_t *obj_ptr); 0x00401C10 0x0134 void __cdecl Output_InsertSkybox(const int16_t *obj_ptr); 0x00401D50 0x0001 void __cdecl Output_InsertInventoryBackground(const int16_t *obj_ptr); 0x00401D60 0x01D5 const int16_t *__cdecl Output_CalcObjectVertices(const int16_t *obj_ptr); 0x00401F40 0x016D const int16_t *__cdecl Output_CalcVerticeLight(const int16_t *obj_ptr); 0x004020B0 0x027D const int16_t *__cdecl Output_CalcRoomVertices(const int16_t *obj_ptr, int32_t far_clip); 0x00402330 0x00C7 void __cdecl Output_RotateLight(int16_t pitch, int16_t yaw); 0x00402400 0x0039 void __cdecl Output_InitPolyList(void); 0x00402430 0x0033 void __cdecl Output_SortPolyList(void); 0x00402470 0x00C5 void __cdecl Output_QuickSort(int32_t left, int32_t right); 0x00402540 0x0036 void __cdecl Output_PrintPolyList(uint8_t *surface_ptr); 0x00402580 0x00A1 void __cdecl Output_AlterFOV(int16_t fov); 0x00402690 0x0095 void __cdecl Output_SetNearZ(int32_t near_z); 0x004026E0 0x006B void __cdecl Output_SetFarZ(int32_t far_z); 0x00402700 0x0266 void __cdecl Output_Init(int16_t x, int16_t y, int32_t width, int32_t height, int32_t near_z, int32_t far_z, int16_t view_angle, int32_t screen_width, int32_t screen_height); # 3dsystem/3d_out.c 0x00402970 0x019F void __cdecl Output_DrawPolyLine(const int16_t *obj_ptr); 0x00402B10 0x0035 void __cdecl Output_DrawPolyFlat(const int16_t *obj_ptr); 0x00402B50 0x0035 void __cdecl Output_DrawPolyTrans(const int16_t *obj_ptr); 0x00402B90 0x0035 void __cdecl Output_DrawPolyGouraud(const int16_t *obj_ptr); 0x00402BD0 0x003C void __cdecl Output_DrawPolyGTMap(const int16_t *obj_ptr); 0x00402C10 0x003C void __cdecl Output_DrawPolyWGTMap(const int16_t *obj_ptr); 0x00402C50 0x00D2 int32_t __cdecl Output_XGenX(const int16_t *obj_ptr); 0x00402D30 0x0146 int32_t __cdecl Output_XGenXG(const int16_t *obj_ptr); 0x00402E80 0x0219 int32_t __cdecl Output_XGenXGUV(const int16_t *obj_ptr); 0x004030A0 0x0284 int32_t __cdecl Output_XGenXGUVPerspFP(const int16_t *obj_ptr); 0x00403330 0x0FC6 void __cdecl Output_GTMapPersp32FP(int32_t y1, int32_t y2, uint8_t *tex_page); 0x00404300 0x14C4 void __cdecl Output_WGTMapPersp32FP(int32_t y1, int32_t y2, uint8_t *tex_page); 0x004057D0 0x0037 void __cdecl Output_DrawPolyGTMapPersp(const int16_t *obj_ptr); 0x00405810 0x0037 void __cdecl Output_DrawPolyWGTMapPersp(const int16_t *obj_ptr); # 3dsystem/3dinsert. 0x00405850 0x006C int32_t __cdecl Output_VisibleZClip(const PHD_VBUF *vtx0, const PHD_VBUF *vtx1, const PHD_VBUF *vtx2); 0x004058C0 0x0140 int32_t __cdecl Output_ZedClipper(int32_t vtx_count, POINT_INFO *pts, VERTEX_INFO *vtx); 0x00405A00 0x0511 int32_t __cdecl Output_XYGUVClipper(int32_t vtx_count, VERTEX_INFO *vtx); 0x00405F20 0x0A5C const int16_t *__cdecl Output_InsertObjectGT4(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type); 0x00406980 0x0872 const int16_t *__cdecl Output_InsertObjectGT3(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type); 0x00407200 0x0422 int32_t __cdecl Output_XYGClipper(int32_t vtx_count, VERTEX_INFO *vtx); 0x00407630 0x03D1 const int16_t *__cdecl Output_InsertObjectG4(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type); 0x00407A10 0x031B const int16_t *__cdecl Output_InsertObjectG3(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type); 0x00407D30 0x02D0 int32_t __cdecl Output_XYClipper(int32_t vtx_count, VERTEX_INFO *vtx); 0x00408000 0x04A4 void __cdecl Output_InsertTrans8(const PHD_VBUF *vbuf, int16_t shade); 0x004084B0 0x00D3 void __cdecl Output_InsertTransQuad(int32_t x, int32_t y, int32_t width, int32_t height, int32_t z); 0x00408590 0x00CB void __cdecl Output_InsertFlatRect(int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t z, uint8_t color_idx); 0x00408660 0x00B5 void __cdecl Output_InsertLine(int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t z, uint8_t color_idx); 0x00408720 0x0642 void __cdecl Output_InsertGT3_ZBuffered(const PHD_VBUF *vtx0, const PHD_VBUF *vtx1, const PHD_VBUF *vtx2, const PHD_TEXTURE *texture, const PHD_UV *uv0, const PHD_UV *uv1, const PHD_UV *uv2); 0x00408D70 0x0140 void __cdecl Output_DrawClippedPoly_Textured(int32_t vtx_count); 0x00408EB0 0x0444 void __cdecl Output_InsertGT4_ZBuffered(const PHD_VBUF *vtx0, const PHD_VBUF *vtx1, const PHD_VBUF *vtx2, const PHD_VBUF *vtx3, const PHD_TEXTURE *texture); 0x00409300 0x0091 const int16_t *__cdecl Output_InsertObjectGT4_ZBuffered(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type); 0x004093A0 0x00AA const int16_t *__cdecl Output_InsertObjectGT3_ZBuffered(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type); 0x00409450 0x039C const int16_t *__cdecl Output_InsertObjectG4_ZBuffered(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type); 0x004097F0 0x00F7 void __cdecl Output_DrawPoly_Gouraud(int32_t vtx_count, int32_t red, int32_t green, int32_t blue); 0x004098F0 0x02D3 const int16_t *__cdecl Output_InsertObjectG3_ZBuffered(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type); 0x00409BD0 0x01C9 void __cdecl Output_InsertFlatRect_ZBuffered(int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t z, uint8_t color_idx); 0x00409DA0 0x0133 void __cdecl Output_InsertLine_ZBuffered(int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t z, uint8_t color_idx); 0x00409EE0 0x0706 void __cdecl Output_InsertGT3_Sorted(const PHD_VBUF *vtx0, const PHD_VBUF *vtx1, const PHD_VBUF *vtx2, const PHD_TEXTURE *texture, const PHD_UV *uv0, const PHD_UV *uv1, const PHD_UV *uv2, SORT_TYPE sort_type); 0x0040A5F0 0x01AC void __cdecl Output_InsertClippedPoly_Textured(int32_t vtx_count, float z, int16_t poly_type, int16_t tex_page); 0x0040A7A0 0x04D7 void __cdecl Output_InsertGT4_Sorted(const PHD_VBUF *vtx0, const PHD_VBUF *vtx1, const PHD_VBUF *vtx2, const PHD_VBUF *vtx3, const PHD_TEXTURE *texture, SORT_TYPE sort_type); 0x0040AC80 0x008C const int16_t *__cdecl Output_InsertObjectGT4_Sorted(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type); 0x0040AD10 0x009F const int16_t *__cdecl Output_InsertObjectGT3_Sorted(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type); 0x0040ADB0 0x043B const int16_t *__cdecl Output_InsertObjectG4_Sorted(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type); 0x0040B1F0 0x0175 void __cdecl Output_InsertPoly_Gouraud(int32_t vtx_count, float z, int32_t red, int32_t green, int32_t blue, int16_t poly_type); 0x0040B370 0x0343 const int16_t *__cdecl Output_InsertObjectG3_Sorted(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type); 0x0040B6C0 0x0347 void __cdecl Output_InsertSprite_Sorted(int32_t z, int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t sprite_idx, int16_t shade); 0x0040BA10 0x017F void __cdecl Output_InsertFlatRect_Sorted(int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t z, uint8_t color_idx); 0x0040BB90 0x012B void __cdecl Output_InsertLine_Sorted(int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t z, uint8_t color_idx); 0x0040BCC0 0x0195 void __cdecl Output_InsertTrans8_Sorted(const PHD_VBUF *vbuf, int16_t shade); 0x0040BE60 0x013D void __cdecl Output_InsertTransQuad_Sorted(int32_t x, int32_t y, int32_t width, int32_t height, int32_t z); 0x0040BFA0 0x00A7 void __cdecl Output_InsertSprite(int32_t z, int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t sprite_idx, int16_t shade); # 3dsystem/scalespr. 0x0040C050 0x02C7 void __cdecl Output_DrawSprite(uint32_t flags, int32_t x, int32_t y, int32_t z, int16_t sprite_idx, int16_t shade, int16_t scale); 0x0040C320 0x0085 void __cdecl Output_DrawPickup(int32_t sx, int32_t sy, int32_t scale, int16_t sprite_idx, int16_t shade); 0x0040C3B0 0x0152 const int16_t *__cdecl Output_InsertRoomSprite(const int16_t *obj_ptr, int32_t vtx_count); 0x0040C510 0x0096 void __cdecl Output_DrawScreenSprite2D(int32_t sx, int32_t sy, int32_t sz, int32_t scale_h, int32_t scale_v, int16_t sprite_idx, int16_t shade, uint16_t flags); 0x0040C5B0 0x009D void __cdecl Output_DrawScreenSprite(int32_t sx, int32_t sy, int32_t sz, int32_t scale_h, int32_t scale_v, int16_t sprite_idx, int16_t shade, uint16_t flags); 0x0040C650 0x0223 void __cdecl Output_DrawScaledSpriteC(const int16_t *obj_ptr); # game/bird.c 0x0040C880 0x0089 void __cdecl Bird_Initialise(int16_t item_num); 0x0040C910 0x0200 void __cdecl Bird_Control(int16_t item_num); # game/boat.c 0x0040CB30 0x003C void __cdecl Boat_Initialise(int16_t item_num); 0x0040CB70 0x0170 int32_t __cdecl Boat_CheckGetOn(int16_t item_num, COLL_INFO *coll); 0x0040CCE0 0x015E void __cdecl Boat_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); 0x0040CE40 0x00F8 int32_t __cdecl Boat_TestWaterHeight(ITEM *item, int32_t z_off, int32_t x_off, XYZ_32 *pos); 0x0040CF40 0x01C1 void __cdecl Boat_DoShift(int32_t boat_num); 0x0040D110 0x0174 void __cdecl Boat_DoWakeEffect(ITEM *boat); 0x0040D290 0x004B int32_t __cdecl Boat_DoDynamics(int32_t height, int32_t fall_speed, int32_t *y); 0x0040D2E0 0x04DD int32_t __cdecl Boat_Dynamics(int16_t boat_num); 0x0040D7C0 0x0187 int32_t __cdecl Boat_UserControl(ITEM *boat); 0x0040D950 0x0169 void __cdecl Boat_Animation(ITEM *boat, int32_t collide); 0x0040DAC0 0x062A void __cdecl Boat_Control(int16_t item_num); 0x0040E0F0 0x00B3 void __cdecl Gondola_Control(int16_t item_num); # game/box.c 0x0040E1B0 0x002F void __cdecl Creature_Initialise(int16_t item_num); 0x0040E1E0 0x0047 int32_t __cdecl Creature_Activate(int16_t item_num); 0x0040E230 0x0242 void __cdecl Creature_AIInfo(ITEM *item, AI_INFO *info); 0x0040E490 0x01F3 int32_t __cdecl Box_SearchLOT(LOT_INFO *lot, int32_t expansion); 0x0040E690 0x006F int32_t __cdecl Box_UpdateLOT(LOT_INFO *lot, int32_t expansion); 0x0040E700 0x0095 void __cdecl Box_TargetBox(LOT_INFO *lot, int16_t box_num); 0x0040E7A0 0x00F2 int32_t __cdecl Box_StalkBox(const ITEM *item, const ITEM *enemy, int16_t box_num); 0x0040E8A0 0x00A4 int32_t __cdecl Box_EscapeBox(const ITEM *item, const ITEM *enemy, int16_t box_num); 0x0040E950 0x00A7 int32_t __cdecl Box_ValidBox(const ITEM *item, int16_t zone_num, int16_t box_num); 0x0040EA00 0x043F void __cdecl Creature_Mood(ITEM *item, AI_INFO *info, int32_t violent); 0x0040EE70 0x0459 TARGET_TYPE __cdecl Box_CalculateTarget(XYZ_32 *target, ITEM *item, LOT_INFO *lot); 0x0040F2D0 0x00F8 int32_t __cdecl Creature_CheckBaddieOverlap(int16_t item_num); 0x0040F3D0 0x008B int32_t __cdecl Box_BadFloor(int32_t x, int32_t y, int32_t z, int32_t box_height, int32_t next_height, int16_t room_num, LOT_INFO *lot); 0x0040F460 0x00B8 void __cdecl Creature_Die(int16_t item_num, int32_t explode); 0x0040F520 0x08CC int32_t __cdecl Creature_Animate(int16_t item_num, int16_t angle, int16_t tilt); 0x0040FDF0 0x00D5 int16_t __cdecl Creature_Turn(ITEM *item, int16_t maximum_turn); 0x0040FED0 0x0035 void __cdecl Creature_Tilt(ITEM *item, int16_t angle); 0x0040FF10 0x0049 void __cdecl Creature_Head(ITEM *item, int16_t required); 0x0040FF60 0x004E void __cdecl Creature_Neck(ITEM *item, int16_t required); 0x0040FFB0 0x00A8 void __cdecl Creature_Float(int16_t item_num); 0x00410060 0x0050 void __cdecl Creature_Underwater(ITEM *item, int32_t depth); 0x004100B0 0x005C int16_t __cdecl Creature_Effect(ITEM *item, BITE *bite, int16_t (*__cdecl spawn)(int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num)); 0x00410110 0x0131 int32_t __cdecl Creature_Vault(int16_t item_num, int16_t angle, int32_t vault, int32_t shift); 0x00410250 0x016F void __cdecl Creature_Kill(ITEM *item, int32_t kill_anim, int32_t kill_state, int32_t lara_kill_state); 0x004103C0 0x01DB void __cdecl Creature_GetBaddieTarget(int16_t item_num, int32_t goody); # game/camera.c 0x004105A0 0x00B0 void __cdecl Camera_Initialise(void); 0x00410650 0x0372 void __cdecl Camera_Move(const GAME_VECTOR *target, int32_t speed); 0x004109D0 0x00D7 void __cdecl Camera_Clip(int32_t *x, int32_t *y, int32_t *h, int32_t target_x, int32_t target_y, int32_t target_h, int32_t left, int32_t top, int32_t right, int32_t bottom); 0x00410AB0 0x0154 void __cdecl Camera_Shift(int32_t *x, int32_t *y, int32_t *h, int32_t target_x, int32_t target_y, int32_t target_h, int32_t left, int32_t top, int32_t right, int32_t bottom); 0x00410C10 0x0050 const SECTOR *__cdecl Camera_GoodPosition(int32_t x, int32_t y, int32_t z, int16_t room_num); 0x00410C60 0x0781 void __cdecl Camera_SmartShift(GAME_VECTOR *target, void (*__cdecl shift)(int32_t *x, int32_t *y, int32_t *h, int32_t target_x, int32_t target_y, int32_t target_h, int32_t left, int32_t top, int32_t right, int32_t bottom)); 0x004113F0 0x00ED void __cdecl Camera_Chase(const ITEM *item); 0x004114E0 0x019E int32_t __cdecl Camera_ShiftClamp(GAME_VECTOR *pos, int32_t clamp); 0x00411680 0x018E void __cdecl Camera_Combat(const ITEM *item); 0x00411810 0x01E2 void __cdecl Camera_Look(const ITEM *item); 0x00411A00 0x0099 void __cdecl Camera_Fixed(void); 0x00411AA0 0x04A9 void __cdecl Camera_Update(void); # game/cinema.c 0x00411F50 0x000A void __cdecl Game_SetCutsceneTrack(int32_t track); 0x00411F60 0x0112 int32_t __cdecl Game_Cutscene_Start(int32_t level_num); 0x00412080 0x0093 void __cdecl Room_InitCinematic(void); 0x00412120 0x016F int32_t __cdecl Game_Cutscene_Control(int32_t nframes); 0x00412290 0x0138 void __cdecl Camera_UpdateCutscene(void); 0x004123D0 0x007F int32_t __cdecl Room_FindByPos(int32_t x, int32_t y, int32_t z); 0x00412450 0x00DC void __cdecl CutscenePlayer_Control(int16_t item_num); 0x00412530 0x0096 void __cdecl Lara_Control_Cutscene(int16_t item_num); 0x004125D0 0x008F void __cdecl CutscenePlayer1_Initialise(int16_t item_num); 0x00412660 0x0033 void __cdecl CutscenePlayerGen_Initialise(int16_t item_num); 0x004126A0 0x0245 void __cdecl Camera_LoadCutsceneFrame(void); # game/collide.c 0x004128F0 0x067C void __cdecl Collide_GetCollisionInfo(COLL_INFO *coll, int32_t xpos, int32_t ypos, int32_t zpos, int16_t room_num, int32_t obj_height); 0x00412FB0 0x002F int32_t __cdecl Room_FindGridShift(int32_t src, int32_t dst); 0x00412FE0 0x03D2 int32_t __cdecl Collide_CollideStaticObjects(COLL_INFO *coll, int32_t x, int32_t y, int32_t z, int16_t room_num, int32_t height); 0x004133D0 0x00C8 void __cdecl Room_GetNearbyRooms(int32_t x, int32_t y, int32_t z, int32_t r, int32_t h, int16_t room_num); 0x004134A0 0x0055 void __cdecl Room_GetNewRoom(int32_t x, int32_t y, int32_t z, int16_t room_num); 0x00413500 0x0037 void __cdecl Item_ShiftCol(ITEM *item, COLL_INFO *coll); 0x00413540 0x005D void __cdecl Item_UpdateRoom(ITEM *item, int32_t height); 0x004135A0 0x0099 int16_t __cdecl Room_GetTiltType(const SECTOR *sector, int32_t x, int32_t y, int32_t z); 0x00413640 0x0195 void __cdecl Lara_BaddieCollision(ITEM *lara_item, COLL_INFO *coll); 0x004137E0 0x0079 void __cdecl Lara_TakeHit(ITEM *lara_item, COLL_INFO *coll); 0x00413860 0x0078 void __cdecl Creature_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); 0x004138E0 0x0055 void __cdecl Object_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); 0x00413940 0x0077 void __cdecl Door_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); 0x004139C0 0x0067 void __cdecl Object_Collision_Trap(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); 0x00413A30 0x0306 void __cdecl Lara_Push(ITEM *item, ITEM *lara_item, COLL_INFO *coll, int32_t spaz_on, int32_t big_push); 0x00413D40 0x00CB int32_t __cdecl Item_TestBoundsCollide(const ITEM *src_item, const ITEM *dst_item, int32_t radius); 0x00413E10 0x0137 int32_t __cdecl Item_TestPosition(int16_t *bounds, ITEM *src_item, ITEM *dst_item); 0x00413F50 0x013B void __cdecl Item_AlignPosition(XYZ_32 *vec, ITEM *src_item, ITEM *dst_item); 0x00414090 0x0187 int32_t __cdecl Lara_MovePosition(XYZ_32 *vec, ITEM *item, ITEM *lara_item); 0x00414220 0x016E int32_t __cdecl Misc_Move3DPosTo3DPos(PHD_3DPOS *src_pos, const PHD_3DPOS *dest_pos, int32_t velocity, int16_t ang_add); # game/control.c 0x00414390 0x0356 int32_t __cdecl Game_Control(int32_t nframes, int32_t demo_mode); 0x004146F0 0x0338 void __cdecl Item_Animate(ITEM *item); 0x00414A60 0x00AB int32_t __cdecl Item_GetAnimChange(ITEM *item, const ANIM *anim); 0x00414B10 0x005F void __cdecl Item_Translate(ITEM *item, int32_t x, int32_t y, int32_t z); 0x00414B70 0x0198 SECTOR *__cdecl Room_GetSector(int32_t x, int32_t y, int32_t z, int16_t *room_num); 0x00414D10 0x0168 int32_t __cdecl Room_GetWaterHeight(int32_t x, int32_t y, int32_t z, int16_t room_num); 0x00414E80 0x0265 int32_t __cdecl Room_GetHeight(const SECTOR *sector, int32_t x, int32_t y, int32_t z); 0x00415100 0x00E7 void __cdecl Camera_RefreshFromTrigger(int16_t type, const int16_t *data); 0x004151F0 0x0690 void __cdecl Room_TestTriggers(int16_t *data, int32_t heavy); 0x004158D0 0x0055 int32_t __cdecl Item_IsTriggerActive(ITEM *item); 0x00415930 0x023D int32_t __cdecl Room_GetCeiling(const SECTOR *sector, int32_t x, int32_t y, int32_t z); 0x00415B90 0x004E int16_t __cdecl Room_GetDoor(const SECTOR *sector); 0x00415BE0 0x00A0 int32_t __cdecl LOS_Check(const GAME_VECTOR *start, GAME_VECTOR *target); 0x00415C80 0x02EB int32_t __cdecl LOS_CheckZ(const GAME_VECTOR *start, GAME_VECTOR *target); 0x00415F70 0x02EC int32_t __cdecl LOS_CheckX(const GAME_VECTOR *start, GAME_VECTOR *target); 0x00416260 0x00DA int32_t __cdecl LOS_ClipTarget(const GAME_VECTOR *start, GAME_VECTOR *target, const SECTOR *sector); 0x00416340 0x02FE int32_t __cdecl LOS_CheckSmashable(const GAME_VECTOR *start, GAME_VECTOR *target); 0x00416640 0x00B3 void __cdecl Room_FlipMap(void); 0x00416700 0x0096 void __cdecl Room_RemoveFlipItems(ROOM *r); 0x004167A0 0x005C void __cdecl Room_AddFlipItems(ROOM *r); 0x00416800 0x0024 void __cdecl Room_TriggerMusicTrack(int16_t value, int16_t flags, int16_t type); 0x00416830 0x00DA void __cdecl Room_TriggerMusicTrackImpl(int16_t value, int16_t flags, int16_t type); # game/demo.c 0x00416910 0x0059 int32_t __cdecl Demo_Control(int32_t level_num); 0x00416970 0x01B0 int32_t __cdecl Demo_Start(int32_t level_num); 0x00416B20 0x00CD void __cdecl Demo_LoadLaraPos(void); 0x00416BF0 0x002D void __cdecl Demo_GetInput(void); # game/diver.c 0x00416C20 0x007A int16_t __cdecl Diver_Harpoon(int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num); 0x00416CA0 0x0106 int32_t __cdecl Diver_GetWaterSurface(int32_t x, int32_t y, int32_t z, int16_t room_num); 0x00416DB0 0x0389 void __cdecl Diver_Control(int16_t item_num); # game/dog.c 0x00417160 0x0387 void __cdecl Dog_Control(int16_t item_num); 0x00417510 0x027E void __cdecl Tiger_Control(int16_t item_num); # game/dragon.c 0x004177B0 0x017F void __cdecl Twinkle_Control(int16_t effect_num); 0x00417930 0x00D9 void __cdecl Effect_CreateBartoliLight(int16_t item_num); 0x00417A10 0x00AB int16_t __cdecl Effect_MissileFlame(int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num); 0x00417AC0 0x02ED void __cdecl Dragon_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); 0x00417DB0 0x00D9 void __cdecl Dragon_Bones(int16_t item_num); 0x00417E90 0x0519 void __cdecl Dragon_Control(int16_t back_num); 0x004183E0 0x0114 void __cdecl Bartoli_Initialise(int16_t item_num); 0x00418500 0x0193 void __cdecl Bartoli_Control(int16_t item_num); 0x004186A0 0x0287 void __cdecl TRex_Control(int16_t item_num); # game/draw.c 0x00418950 0x0037 int32_t __cdecl Game_DrawCinematic(void); 0x00418990 0x0037 int32_t __cdecl Game_Draw(void); 0x004189D0 0x02B0 void __cdecl Room_DrawAllRooms(int16_t current_room); 0x00418C80 0x01C6 void __cdecl Room_GetBounds(void); 0x00418E50 0x037F void __cdecl Room_SetBounds(const int16_t *objptr, int32_t room_num, ROOM *parent); 0x004191D0 0x03D2 void __cdecl Room_Clip(ROOM *r); 0x004195B0 0x00B4 void __cdecl Room_DrawSingleRoomGeometry(int16_t room_num); 0x00419670 0x0218 void __cdecl Room_DrawSingleRoomObjects(int16_t room_num); 0x00419890 0x0147 void __cdecl Effect_Draw(int16_t effect_num); 0x004199E0 0x0083 void __cdecl Object_DrawSpriteItem(const ITEM *item); 0x00419A70 0x0378 void __cdecl Object_DrawAnimatingItem(const ITEM *item); 0x00419DF0 0x0D02 void __cdecl Lara_Draw(const ITEM *item); 0x0041AB20 0x0BC6 void __cdecl Lara_Draw_I(const ITEM *item, const FRAME_INFO *frame1, const FRAME_INFO *frame2, int32_t frac, int32_t rate); 0x0041B710 0x0034 void __cdecl Matrix_InitInterpolate(int32_t frac, int32_t rate); 0x0041B750 0x0022 void __cdecl Matrix_Pop_I(void); 0x0041B780 0x0027 void __cdecl Matrix_Push_I(void); 0x0041B7B0 0x0031 void __cdecl Matrix_RotY_I(int16_t ang); 0x0041B7F0 0x0031 void __cdecl Matrix_RotX_I(int16_t ang); 0x0041B830 0x0031 void __cdecl Matrix_RotZ_I(int16_t ang); 0x0041B870 0x0041 void __cdecl Matrix_TranslateRel_I(int32_t x, int32_t y, int32_t z); 0x0041B8C0 0x0047 void __cdecl Matrix_TranslateRel_ID(int32_t x, int32_t y, int32_t z, int32_t x2, int32_t y2, int32_t z2); 0x0041B910 0x0041 void __cdecl Matrix_RotYXZ_I(int16_t y, int16_t x, int16_t z); 0x0041B960 0x003D void __cdecl Matrix_RotYXZsuperpack_I(const int16_t **pprot1, const int16_t **pprot2, int32_t skip); 0x0041B9A0 0x00A1 void __cdecl Matrix_RotYXZsuperpack(const int16_t **pprot, int32_t skip); 0x0041BA50 0x002A void __cdecl Output_InsertPolygons_I(int16_t *ptr, int32_t clip); 0x0041BA80 0x01A5 void __cdecl Matrix_Interpolate(void); 0x0041BC30 0x00FC void __cdecl Matrix_InterpolateArm(void); 0x0041BD30 0x014B void __cdecl Gun_DrawFlash(LARA_GUN_TYPE weapon_type, int32_t clip); 0x0041BEA0 0x00E8 void __cdecl Output_CalculateObjectLighting(const ITEM *item, const BOUNDS_16 *bounds); 0x0041BF90 0x0092 int32_t __cdecl Item_GetFrames(const ITEM *item, FRAME_INFO *frmptr[], int32_t *rate); 0x0041C030 0x007C BOUNDS_16 *__cdecl Item_GetBoundsAccurate(const ITEM *item); 0x0041C0B0 0x0035 FRAME_INFO *__cdecl Item_GetBestFrame(const ITEM *item); 0x0041C0F0 0x0048 void __cdecl Output_AddDynamicLight(int32_t x, int32_t y, int32_t z, int32_t intensity, int32_t falloff); # game/eel.c 0x0041C140 0x019D void __cdecl BigEel_Control(int16_t item_num); 0x0041C2E0 0x01E1 void __cdecl Eel_Control(int16_t item_num); # game/effects.c 0x0041C4D0 0x008C int32_t __cdecl Lara_IsNearItem(PHD_3DPOS *pos, int32_t distance); 0x0041C560 0x0068 void __cdecl Sound_UpdateEffects(void); 0x0041C5D0 0x0059 int16_t __cdecl DoBloodSplat(int32_t x, int32_t y, int32_t z, int16_t speed, int16_t direction, int16_t room_num); 0x0041C630 0x00A4 void __cdecl DoLotsOfBlood(int32_t x, int32_t y, int32_t z, int16_t speed, int16_t direction, int16_t room_num, int32_t num); 0x0041C6E0 0x0082 void __cdecl Blood_Control(int16_t effect_num); 0x0041C770 0x007F void __cdecl Explosion_Control(int16_t effect_num); 0x0041C7F0 0x0072 void __cdecl Ricochet(GAME_VECTOR *pos); 0x0041C870 0x0030 void __cdecl Ricochet_Control(int16_t effect_num); 0x0041C8A0 0x0064 void __cdecl CreateBubble(XYZ_32 *pos, int16_t room_num); 0x0041C910 0x0078 void __cdecl FX_Bubbles(ITEM *item); 0x0041C990 0x00F3 void __cdecl Bubble_Control(int16_t effect_num); 0x0041CA90 0x00C2 void __cdecl Splash(ITEM *item); 0x0041CB60 0x0071 void __cdecl Splash_Control(int16_t effect_num); 0x0041CBE0 0x00AE void __cdecl WaterSprite_Control(int16_t effect_num); 0x0041CC90 0x008C void __cdecl SnowSprite_Control(int16_t effect_num); 0x0041CD20 0x00DE void __cdecl HotLiquid_Control(int16_t effect_num); 0x0041CE00 0x013D void __cdecl Waterfall_Control(int16_t item_num); 0x0041CF40 0x000B void __cdecl FX_FinishLevel(ITEM *item); 0x0041CF50 0x0016 void __cdecl FX_Turn180(ITEM *item); 0x0041CF70 0x0096 void __cdecl FX_FloorShake(ITEM *item); 0x0041D010 0x0040 void __cdecl FX_LaraNormal(ITEM *item); 0x0041D050 0x001C void __cdecl FX_Boiler(ITEM *item); 0x0041D070 0x008F void __cdecl FX_Flood(ITEM *item); 0x0041D100 0x0023 void __cdecl FX_Rubble(ITEM *item); 0x0041D130 0x002C void __cdecl FX_Chandelier(ITEM *item); 0x0041D160 0x0023 void __cdecl FX_Explosion(ITEM *item); 0x0041D190 0x001C void __cdecl FX_Piston(ITEM *item); 0x0041D1B0 0x001C void __cdecl FX_Curtain(ITEM *item); 0x0041D1D0 0x001C void __cdecl FX_Statue(ITEM *item); 0x0041D1F0 0x001C void __cdecl FX_SetChange(ITEM *item); 0x0041D210 0x003F void __cdecl DingDong_Control(int16_t item_num); 0x0041D250 0x0037 void __cdecl LaraAlarm_Control(int16_t item_num); 0x0041D290 0x0067 void __cdecl AlarmSound_Control(int16_t item_num); 0x0041D300 0x005D void __cdecl BirdTweeter_Control(int16_t item_num); 0x0041D360 0x0059 void __cdecl DoChimeSound(const ITEM *item); 0x0041D3C0 0x0068 void __cdecl ClockChimes_Control(int16_t item_num); 0x0041D430 0x0128 void __cdecl SphereOfDoom_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); 0x0041D560 0x00F0 void __cdecl SphereOfDoom_Control(int16_t item_num); 0x0041D650 0x012D void __cdecl SphereOfDoom_Draw(const ITEM *item); 0x0041D780 0x000A void __cdecl FX_LaraHandsFree(ITEM *item); 0x0041D790 0x0005 void __cdecl FX_FlipMap(ITEM *item); 0x0041D7A0 0x0043 void __cdecl FX_LaraDrawRightGun(ITEM *item); 0x0041D7F0 0x0043 void __cdecl FX_LaraDrawLeftGun(ITEM *item); 0x0041D840 0x0063 void __cdecl FX_SwapMeshesWithMeshSwap1(ITEM *item); 0x0041D8B0 0x0063 void __cdecl FX_SwapMeshesWithMeshSwap2(ITEM *item); 0x0041D920 0x009A void __cdecl FX_SwapMeshesWithMeshSwap3(ITEM *item); 0x0041D9C0 0x0009 void __cdecl FX_InvisibilityOn(ITEM *item); 0x0041D9D0 0x0016 void __cdecl FX_InvisibilityOff(ITEM *item); 0x0041D9F0 0x0009 void __cdecl FX_DynamicLightOn(ITEM *item); 0x0041DA00 0x000B void __cdecl FX_DynamicLightOff(ITEM *item); 0x0041DA10 0x0005 void __cdecl FX_ResetHair(ITEM *item); 0x0041DA20 0x0024 void __cdecl FX_AssaultStart(ITEM *item); 0x0041DA50 0x001F void __cdecl FX_AssaultStop(ITEM *item); 0x0041DA70 0x0017 void __cdecl FX_AssaultReset(ITEM *item); 0x0041DA90 0x00B2 void __cdecl FX_AssaultFinished(ITEM *item); # game/enemies.c 0x0041DB50 0x0076 int16_t __cdecl Knife(int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num); 0x0041DBD0 0x040B void __cdecl Cultist2_Control(int16_t item_num); 0x0041E000 0x04A1 void __cdecl Monk_Control(int16_t item_num); 0x0041E4D0 0x05BD void __cdecl Worker3_Control(int16_t item_num); 0x0041EAE0 0x03F7 void __cdecl XianWarrior_Draw(const ITEM *item); 0x0041EEE0 0x00A8 void __cdecl XianSpearman_DoDamage(ITEM *item, CREATURE *xian, int32_t damage); 0x0041EF90 0x0058 void __cdecl XianWarrior_Initialise(int16_t item_num); 0x0041EFF0 0x0590 void __cdecl XianSpearman_Control(int16_t item_num); 0x0041F5D0 0x0098 void __cdecl XianKnight_SparkleTrail(ITEM *item); 0x0041F670 0x03BA void __cdecl XianKnight_Control(int16_t item_num); # game/gameflow.c 0x0041FA60 0x01E9 int32_t __cdecl GF_LoadScriptFile(const char *fname); 0x0041FC50 0x001F int32_t __cdecl GF_DoFrontendSequence(void); 0x0041FC70 0x0066 int32_t __cdecl GF_DoLevelSequence(int32_t level, GAME_FLOW_LEVEL_TYPE type); 0x0041FCE0 0x047C int32_t __cdecl GF_InterpretSequence(int16_t *ptr, GAME_FLOW_LEVEL_TYPE type, int32_t seq_type); 0x004201C0 0x0CD3 void __cdecl GF_ModifyInventory(int32_t level, int32_t type); # game/hair.c 0x00420EA0 0x0074 void __cdecl Lara_Hair_Initialise(void); 0x00420F20 0x09E5 void __cdecl Lara_Hair_Control(bool in_cutscene); 0x00421920 0x0076 void __cdecl Lara_Hair_Draw(void); # game/health.c 0x004219A0 0x002D BOOL __cdecl Overlay_FlashCounter(void); 0x004219D0 0x0145 void __cdecl Overlay_DrawAssaultTimer(void); 0x00421B20 0x0045 void __cdecl Overlay_DrawGameInfo(bool pickup_state); 0x00421B70 0x00AB void __cdecl Overlay_DrawHealthBar(bool flash_state); 0x00421C20 0x0097 void __cdecl Overlay_DrawAirBar(bool flash_state); 0x00421CC0 0x0028 void __cdecl Overlay_MakeAmmoString(char *string); 0x00421CF0 0x0132 void __cdecl Overlay_DrawAmmoInfo(void); 0x00421E40 0x0015 void __cdecl Overlay_InitialisePickUpDisplay(void); 0x00421E60 0x00FD void __cdecl Overlay_DrawPickups(bool pickup_state); 0x00421F60 0x006C void __cdecl Overlay_AddDisplayPickup(GAME_OBJECT_ID object_id); 0x00421FD0 0x007A void __cdecl Overlay_DisplayModeInfo(char* string); 0x00422050 0x002C void __cdecl Overlay_DrawModeInfo(void); # game/inventory.c 0x00422080 0x119E int32_t __cdecl Inv_Display(int32_t inventory_mode); 0x00423310 0x0156 void __cdecl Inv_Construct(void); 0x00423470 0x0089 void __cdecl Inv_SelectMeshes(INVENTORY_ITEM *inv_item); 0x00423500 0x0081 int32_t __cdecl Inv_AnimateInventoryItem(INVENTORY_ITEM *inv_item); 0x00423590 0x041D void __cdecl Inv_DrawInventoryItem(INVENTORY_ITEM *inv_item); 0x004239C0 0x0019 int32_t __cdecl Input_GetDebounced(int32_t input); 0x004239E0 0x0005 void __cdecl Inv_DoInventoryPicture(void); 0x004239F0 0x0132 void __cdecl Inv_DoInventoryBackground(void); # game/invfunc.c 0x00423B30 0x010A void __cdecl Inv_InitColors(void); 0x00423C40 0x0167 void __cdecl Inv_RingIsOpen(RING_INFO *ring); 0x00423DB0 0x0081 void __cdecl Inv_RingIsNotOpen(RING_INFO *ring); 0x00423E40 0x0369 void __cdecl Inv_RingNotActive(INVENTORY_ITEM *inv_item); 0x004242B0 0x0032 void __cdecl Inv_RingActive(void); 0x004242F0 0x06BE int32_t __cdecl Inv_AddItem(GAME_OBJECT_ID object_id); 0x00424B00 0x0129 void __cdecl Inv_InsertItem(INVENTORY_ITEM *inv_item); 0x00424C30 0x0077 int32_t __cdecl Inv_RequestItem(GAME_OBJECT_ID object_id); 0x00424CB0 0x001B void __cdecl Inv_RemoveAllItems(void); 0x00424CD0 0x0110 int32_t __cdecl Inv_RemoveItem(GAME_OBJECT_ID object_id); 0x00424DE0 0x00C1 int32_t __cdecl Inv_GetItemOption(GAME_OBJECT_ID object_id); 0x00424FD0 0x0024 void __cdecl Inv_RemoveInventoryText(void); 0x00425000 0x010F void __cdecl Inv_Ring_Init(RING_INFO *ring, int16_t type, INVENTORY_ITEM **list, int16_t qty, int16_t current, IMOTION_INFO *imo); 0x00425110 0x0060 void __cdecl Inv_Ring_GetView(RING_INFO *ring, PHD_3DPOS *viewer); 0x00425170 0x0040 void __cdecl Inv_Ring_Light(RING_INFO *ring); 0x004251B0 0x002C void __cdecl Inv_Ring_CalcAdders(RING_INFO *ring, int16_t rotation_duration); 0x004251E0 0x013E void __cdecl Inv_Ring_DoMotions(RING_INFO *ring); 0x00425320 0x002F void __cdecl Inv_Ring_RotateLeft(RING_INFO *ring); 0x00425350 0x002F void __cdecl Inv_Ring_RotateRight(RING_INFO *ring); 0x00425380 0x0063 void __cdecl Inv_Ring_MotionInit(RING_INFO *ring, int16_t frames, int16_t status, int16_t status_target); 0x004253F0 0x002C void __cdecl Inv_Ring_MotionSetup(RING_INFO *ring, int16_t status, int16_t status_target, int16_t frames); 0x00425420 0x0026 void __cdecl Inv_Ring_MotionRadius(RING_INFO *ring, int16_t target); 0x00425450 0x0022 void __cdecl Inv_Ring_MotionRotation(RING_INFO *ring, int16_t rotation, int16_t target); 0x00425480 0x0025 void __cdecl Inv_Ring_MotionCameraPos(RING_INFO *ring, int16_t target); 0x004254B0 0x0020 void __cdecl Inv_Ring_MotionCameraPitch(RING_INFO *ring, int16_t target); 0x004254D0 0x005D void __cdecl Inv_Ring_MotionItemSelect(RING_INFO *ring, INVENTORY_ITEM *inv_item); 0x00425530 0x0063 void __cdecl Inv_Ring_MotionItemDeselect(RING_INFO *ring, INVENTORY_ITEM *inv_item); # game/invtext.c 0x004255A0 0x0082 void __cdecl Requester_Init(REQUEST_INFO *req); 0x00425630 0x00A3 void __cdecl Requester_Shutdown(REQUEST_INFO *req); 0x004256E0 0x001B void __cdecl Requester_Item_CenterAlign(REQUEST_INFO *req, TEXTSTRING *txt); 0x00425700 0x0054 void __cdecl Requester_Item_LeftAlign(REQUEST_INFO *req, TEXTSTRING *txt); 0x00425760 0x0056 void __cdecl Requester_Item_RightAlign(REQUEST_INFO *req, TEXTSTRING *txt); 0x004257C0 0x0866 int32_t __cdecl Requester_Display(REQUEST_INFO *req, int32_t des, int32_t backgrounds); 0x00426030 0x00AA void __cdecl Requester_SetHeading(REQUEST_INFO *req, char *text1, uint32_t flags1, char *text2, uint32_t flags2); 0x004260E0 0x0013 void __cdecl Requester_RemoveAllItems(REQUEST_INFO *req); 0x00426100 0x00C0 void __cdecl Requester_ChangeItem(REQUEST_INFO *req, int32_t item, const char *text1, uint32_t flags1, const char *text2, uint32_t flags2); 0x004261C0 0x00AC void __cdecl Requester_AddItem(REQUEST_INFO *req, const char *text1, uint32_t flags1, const char *text2, uint32_t flags2); 0x00426270 0x0039 void __cdecl Requester_SetSize(REQUEST_INFO *req, int32_t maxlines, int32_t ypos); 0x004262B0 0x0081 int32_t __cdecl AddAssaultTime(uint32_t time); 0x00426340 0x01D6 void __cdecl ShowGymStatsText(char *time_str, int32_t type); 0x00426520 0x0397 void __cdecl ShowStatsText(char *time_str, int32_t type); 0x004268C0 0x0425 void __cdecl ShowEndStatsText(void); # game/items.c 0x00426CF0 0x0052 void __cdecl Item_InitialiseArray(int32_t num_items); 0x00426D50 0x011E void __cdecl Item_Kill(int16_t item_num); 0x00426E70 0x0039 int16_t __cdecl Item_Create(void); 0x00426EB0 0x01B3 void __cdecl Item_Initialise(int16_t item_num); 0x00427070 0x008A void __cdecl Item_RemoveActive(int16_t item_num); 0x00427100 0x006F void __cdecl Item_RemoveDrawn(int16_t item_num); 0x00427170 0x005A void __cdecl Item_AddActive(int16_t item_num); 0x004271D0 0x009C void __cdecl Item_NewRoom(int16_t item_num, int16_t room_num); 0x00427270 0x007C int32_t __cdecl Item_GlobalReplace(GAME_OBJECT_ID src_object_id, GAME_OBJECT_ID dst_object_id); 0x004272F0 0x0030 void __cdecl Effect_InitialiseArray(void); 0x00427320 0x006C int16_t __cdecl Effect_Create(int16_t room_num); 0x00427390 0x00E3 void __cdecl Effect_Kill(int16_t effect_num); 0x00427480 0x0093 void __cdecl Effect_NewRoom(int16_t effect_num, int16_t room_num); 0x00427520 0x0058 void __cdecl Item_ClearKilled(void); # game/lara.c 0x00427580 0x0195 void __cdecl Lara_HandleAboveWater(ITEM *item, COLL_INFO *coll); 0x00427720 0x0066 void __cdecl Lara_LookUpDown(void); 0x00427790 0x0072 void __cdecl Lara_LookLeftRight(void); 0x00427810 0x0089 void __cdecl Lara_ResetLook(void); 0x004278A0 0x008B void __cdecl Lara_State_Walk(ITEM *item, COLL_INFO *coll); 0x00427930 0x0143 void __cdecl Lara_State_Run(ITEM *item, COLL_INFO *coll); 0x00427A80 0x0148 void __cdecl Lara_State_Stop(ITEM *item, COLL_INFO *coll); 0x00427BD0 0x00D3 void __cdecl Lara_State_ForwardJump(ITEM *item, COLL_INFO *coll); 0x00427CB0 0x0057 void __cdecl Lara_State_FastBack(ITEM *item, COLL_INFO *coll); 0x00427D10 0x008A void __cdecl Lara_State_TurnRight(ITEM *item, COLL_INFO *coll); 0x00427DA0 0x0089 void __cdecl Lara_State_TurnLeft(ITEM *item, COLL_INFO *coll); 0x00427E30 0x0014 void __cdecl Lara_State_Death(ITEM *item, COLL_INFO *coll); 0x00427E50 0x0040 void __cdecl Lara_State_FastFall(ITEM *item, COLL_INFO *coll); 0x00427E90 0x0058 void __cdecl Lara_State_Hang(ITEM *item, COLL_INFO *coll); 0x00427EF0 0x001C void __cdecl Lara_State_Reach(ITEM *item, COLL_INFO *coll); 0x00427F10 0x000A void __cdecl Lara_State_Splat(ITEM *item, COLL_INFO *coll); 0x00427F20 0x010C void __cdecl Lara_State_Compress(ITEM *item, COLL_INFO *coll); 0x00428030 0x0084 void __cdecl Lara_State_Back(ITEM *item, COLL_INFO *coll); 0x004280C0 0x000B void __cdecl Lara_State_Null(ITEM *item, COLL_INFO *coll); 0x004280D0 0x004B void __cdecl Lara_State_FastTurn(ITEM *item, COLL_INFO *coll); 0x00428120 0x007C void __cdecl Lara_State_StepRight(ITEM *item, COLL_INFO *coll); 0x004281A0 0x007C void __cdecl Lara_State_StepLeft(ITEM *item, COLL_INFO *coll); 0x00428220 0x002B void __cdecl Lara_State_Slide(ITEM *item, COLL_INFO *coll); 0x00428250 0x004A void __cdecl Lara_State_BackJump(ITEM *item, COLL_INFO *coll); 0x004282A0 0x0033 void __cdecl Lara_State_RightJump(ITEM *item, COLL_INFO *coll); 0x004282E0 0x0033 void __cdecl Lara_State_LeftJump(ITEM *item, COLL_INFO *coll); 0x00428320 0x0013 void __cdecl Lara_State_UpJump(ITEM *item, COLL_INFO *coll); 0x00428340 0x002C void __cdecl Lara_State_Fallback(ITEM *item, COLL_INFO *coll); 0x00428370 0x0035 void __cdecl Lara_State_HangLeft(ITEM *item, COLL_INFO *coll); 0x004283B0 0x0035 void __cdecl Lara_State_HangRight(ITEM *item, COLL_INFO *coll); 0x004283F0 0x0018 void __cdecl Lara_State_SlideBack(ITEM *item, COLL_INFO *coll); 0x00428410 0x0030 void __cdecl Lara_State_PushBlock(ITEM *item, COLL_INFO *coll); 0x00428440 0x0027 void __cdecl Lara_State_PPReady(ITEM *item, COLL_INFO *coll); 0x00428470 0x0030 void __cdecl Lara_State_Pickup(ITEM *item, COLL_INFO *coll); 0x004284A0 0x0058 void __cdecl Lara_State_PickupFlare(ITEM *item, COLL_INFO *coll); 0x00428500 0x0039 void __cdecl Lara_State_SwitchOn(ITEM *item, COLL_INFO *coll); 0x00428540 0x0030 void __cdecl Lara_State_UseKey(ITEM *item, COLL_INFO *coll); 0x00428570 0x001D void __cdecl Lara_State_Special(ITEM *item, COLL_INFO *coll); 0x00428590 0x002F void __cdecl Lara_State_SwanDive(ITEM *item, COLL_INFO *coll); 0x004285C0 0x0054 void __cdecl Lara_State_FastDive(ITEM *item, COLL_INFO *coll); 0x00428620 0x0015 void __cdecl Lara_State_WaterOut(ITEM *item, COLL_INFO *coll); 0x00428640 0x00CA void __cdecl Lara_State_Wade(ITEM *item, COLL_INFO *coll); 0x00428710 0x0096 void __cdecl Lara_State_Zipline(ITEM *item, COLL_INFO *coll); 0x004287B0 0x004C void __cdecl Lara_State_Extra_Breath(ITEM *item, COLL_INFO *coll); 0x00428800 0x0047 void __cdecl Lara_State_Extra_YetiKill(ITEM *item, COLL_INFO *coll); 0x00428850 0x0091 void __cdecl Lara_State_Extra_SharkKill(ITEM *item, COLL_INFO *coll); 0x004288F0 0x0013 void __cdecl Lara_State_Extra_Airlock(ITEM *item, COLL_INFO *coll); 0x00428910 0x001D void __cdecl Lara_State_Extra_GongBong(ITEM *item, COLL_INFO *coll); 0x00428930 0x0051 void __cdecl Lara_State_Extra_DinoKill(ITEM *item, COLL_INFO *coll); 0x00428990 0x00BC void __cdecl Lara_State_Extra_PullDagger(ITEM *item, COLL_INFO *coll); 0x00428A50 0x004D void __cdecl Lara_State_Extra_StartAnim(ITEM *item, COLL_INFO *coll); 0x00428AA0 0x00A5 void __cdecl Lara_State_Extra_StartHouse(ITEM *item, COLL_INFO *coll); 0x00428B50 0x00A3 void __cdecl Lara_State_Extra_FinalAnim(ITEM *item, COLL_INFO *coll); 0x00428C00 0x0051 int32_t __cdecl Lara_Fallen(ITEM *item, COLL_INFO *coll); 0x00428C60 0x009B void __cdecl Lara_CollideStop(ITEM *item, COLL_INFO *coll); 0x00428D20 0x0191 void __cdecl Lara_Col_Walk(ITEM *item, COLL_INFO *coll); 0x00428EC0 0x0176 void __cdecl Lara_Col_Run(ITEM *item, COLL_INFO *coll); 0x00429040 0x0081 void __cdecl Lara_Col_Stop(ITEM *item, COLL_INFO *coll); 0x004290D0 0x00D7 void __cdecl Lara_Col_ForwardJump(ITEM *item, COLL_INFO *coll); 0x004291B0 0x00B3 void __cdecl Lara_Col_FastBack(ITEM *item, COLL_INFO *coll); 0x00429270 0x0095 void __cdecl Lara_Col_TurnRight(ITEM *item, COLL_INFO *coll); 0x00429310 0x0013 void __cdecl Lara_Col_TurnLeft(ITEM *item, COLL_INFO *coll); 0x00429330 0x0068 void __cdecl Lara_Col_Death(ITEM *item, COLL_INFO *coll); 0x004293A0 0x0099 void __cdecl Lara_Col_FastFall(ITEM *item, COLL_INFO *coll); 0x00429440 0x0127 void __cdecl Lara_Col_Hang(ITEM *item, COLL_INFO *coll); 0x00429570 0x0090 void __cdecl Lara_Col_Reach(ITEM *item, COLL_INFO *coll); 0x00429600 0x0059 void __cdecl Lara_Col_Splat(ITEM *item, COLL_INFO *coll); 0x00429660 0x0013 void __cdecl Lara_Col_Land(ITEM *item, COLL_INFO *coll); 0x00429680 0x0096 void __cdecl Lara_Col_Compress( ITEM *item, COLL_INFO *coll ); 0x00429720 0x00FB void __cdecl Lara_Col_Back(ITEM *item, COLL_INFO *coll); 0x00429820 0x00BE void __cdecl Lara_Col_StepRight(ITEM *item, COLL_INFO *coll); 0x004298E0 0x0013 void __cdecl Lara_Col_StepLeft(ITEM *item, COLL_INFO *coll); 0x00429900 0x001E void __cdecl Lara_Col_Slide(ITEM *item, COLL_INFO *coll); 0x00429920 0x0023 void __cdecl Lara_Col_BackJump(ITEM *item, COLL_INFO *coll); 0x00429950 0x0023 void __cdecl Lara_Col_RightJump(ITEM *item, COLL_INFO *coll); 0x00429980 0x0023 void __cdecl Lara_Col_LeftJump(ITEM *item, COLL_INFO *coll); 0x004299B0 0x011B void __cdecl Lara_Col_UpJump(ITEM *item, COLL_INFO *coll); 0x00429AD0 0x0083 void __cdecl Lara_Col_Fallback(ITEM *item, COLL_INFO *coll); 0x00429B60 0x0033 void __cdecl Lara_Col_HangLeft(ITEM *item, COLL_INFO *coll); 0x00429BA0 0x0033 void __cdecl Lara_Col_HangRight(ITEM *item, COLL_INFO *coll); 0x00429BE0 0x0023 void __cdecl Lara_Col_SlideBack(ITEM *item, COLL_INFO *coll); 0x00429C10 0x0013 void __cdecl Lara_Col_Null(ITEM *item, COLL_INFO *coll); 0x00429C30 0x0081 void __cdecl Lara_Col_Roll(ITEM *item, COLL_INFO *coll); 0x00429CC0 0x00B3 void __cdecl Lara_Col_Roll2(ITEM *item, COLL_INFO *coll); 0x00429D80 0x0069 void __cdecl Lara_Col_SwanDive(ITEM *item, COLL_INFO *coll); 0x00429DF0 0x0079 void __cdecl Lara_Col_FastDive(ITEM *item, COLL_INFO *coll); 0x00429E70 0x0162 void __cdecl Lara_Col_Wade(ITEM *item, COLL_INFO *coll); 0x00429FE0 0x0036 void __cdecl Lara_Col_Default(ITEM *item, COLL_INFO *coll); 0x0042A020 0x0074 void __cdecl Lara_Col_Jumper(ITEM *item, COLL_INFO *coll); 0x0042A0A0 0x0032 void __cdecl Lara_GetCollisionInfo(ITEM *item, COLL_INFO *coll); 0x0042A0E0 0x00E2 void __cdecl Lara_SlideSlope(ITEM *item, COLL_INFO *coll); 0x0042A1D0 0x0067 int32_t __cdecl Lara_HitCeiling(ITEM *item, COLL_INFO *coll); 0x0042A240 0x007F int32_t __cdecl Lara_DeflectEdge(ITEM *item, COLL_INFO *coll); 0x0042A2C0 0x0136 void __cdecl Lara_DeflectEdgeJump(ITEM *item, COLL_INFO *coll); 0x0042A440 0x00AB void __cdecl Lara_SlideEdgeJump(ITEM *item, COLL_INFO *coll); 0x0042A530 0x00E1 int32_t __cdecl Lara_TestWall(ITEM *item, int32_t front, int32_t right, int32_t down); 0x0042A640 0x00F5 int32_t __cdecl Lara_TestHangOnClimbWall(ITEM *item, COLL_INFO *coll); 0x0042A750 0x00BE int32_t __cdecl Lara_TestClimbStance(ITEM *item, COLL_INFO *coll); 0x0042A810 0x033E void __cdecl Lara_HangTest(ITEM *item, COLL_INFO *coll); 0x0042AB70 0x00AD int32_t __cdecl Lara_TestEdgeCatch(ITEM *item, COLL_INFO *coll, int32_t *edge); 0x0042AC20 0x016D int32_t __cdecl Lara_TestHangJumpUp(ITEM *item, COLL_INFO *coll); 0x0042AD90 0x019E int32_t __cdecl Lara_TestHangJump(ITEM *item, COLL_INFO *coll); 0x0042AF30 0x00B1 int32_t __cdecl Lara_TestHangSwingIn(ITEM *item, int16_t angle); 0x0042AFF0 0x02E7 int32_t __cdecl Lara_TestVault(ITEM *item, COLL_INFO *coll); 0x0042B2E0 0x0130 int32_t __cdecl Lara_TestSlide(ITEM *item, COLL_INFO *coll); 0x0042B410 0x0075 int16_t __cdecl Lara_FloorFront(ITEM *item, int16_t ang, int32_t dist); 0x0042B490 0x00BB int32_t __cdecl Lara_LandedBad(ITEM *item, COLL_INFO *coll); 0x0042B550 0x038F void __cdecl Lara_GetJointAbsPosition(XYZ_32 *vec, int32_t joint); 0x0042B8E0 0x031A void __cdecl Lara_GetJointAbsPosition_I(ITEM *item, XYZ_32 *vec, int16_t *frame1, int16_t *frame2, int32_t frac, int32_t rate); # game/lara1gun.c 0x0042BC00 0x0033 void __cdecl Gun_Rifle_DrawMeshes(LARA_GUN_TYPE weapon_type); 0x0042BC40 0x002B void __cdecl Gun_Rifle_UndrawMeshes(LARA_GUN_TYPE weapon_type); 0x0042BC70 0x0070 void __cdecl Gun_Rifle_Ready(LARA_GUN_TYPE weapon_type); 0x0042BCE0 0x00F5 void __cdecl Gun_Rifle_Control(LARA_GUN_TYPE weapon_type); 0x0042BDE0 0x00F2 void __cdecl Gun_Rifle_FireShotgun(void); 0x0042BEE0 0x007B void __cdecl Gun_Rifle_FireM16(bool running); 0x0042BF60 0x0187 void __cdecl Gun_Rifle_FireHarpoon(void); 0x0042C0F0 0x0344 void __cdecl HarpoonBolt_Control(int16_t item_num); 0x0042C440 0x00F0 void __cdecl Gun_Rifle_FireGrenade(void); 0x0042C530 0x03FD void __cdecl Grenade_Control(int16_t item_num); 0x0042C930 0x0166 void __cdecl Gun_Rifle_Draw(LARA_GUN_TYPE weapon_type); 0x0042CAA0 0x0104 void __cdecl Gun_Rifle_Undraw(LARA_GUN_TYPE weapon_type); 0x0042CBB0 0x037E void __cdecl Gun_Rifle_Animate(LARA_GUN_TYPE weapon_type); # game/lara2gun.c 0x0042CF60 0x004F void __cdecl Gun_Pistols_SetArmInfo(LARA_ARM *arm, int32_t frame); 0x0042CFB0 0x007C void __cdecl Gun_Pistols_Draw(LARA_GUN_TYPE weapon_type); 0x0042D030 0x0225 void __cdecl Gun_Pistols_Undraw(LARA_GUN_TYPE weapon_type); 0x0042D260 0x005C void __cdecl Gun_Pistols_Ready(LARA_GUN_TYPE weapon_type); 0x0042D2C0 0x004E void __cdecl Gun_Pistols_DrawMeshes(LARA_GUN_TYPE weapon_type); 0x0042D310 0x003A void __cdecl Gun_Pistols_UndrawMeshLeft(LARA_GUN_TYPE weapon_type); 0x0042D350 0x003A void __cdecl Gun_Pistols_UndrawMeshRight(LARA_GUN_TYPE weapon_type); 0x0042D390 0x018C void __cdecl Gun_Pistols_Control(LARA_GUN_TYPE weapon_type); 0x0042D520 0x0330 void __cdecl Gun_Pistols_Animate(LARA_GUN_TYPE weapon_type); # game/laraclimb.c 0x0042D850 0x0035 void __cdecl Lara_State_ClimbLeft(ITEM *item, COLL_INFO *coll); 0x0042D890 0x0035 void __cdecl Lara_State_ClimbRight(ITEM *item, COLL_INFO *coll); 0x0042D8D0 0x0075 void __cdecl Lara_State_ClimbStance(ITEM *item, COLL_INFO *coll); 0x0042D950 0x0014 void __cdecl Lara_State_Climbing(ITEM *item, COLL_INFO *coll); 0x0042D970 0x001E void __cdecl Lara_State_ClimbEnd(ITEM *item, COLL_INFO *coll); 0x0042D990 0x0014 void __cdecl Lara_State_ClimbDown(ITEM *item, COLL_INFO *coll); 0x0042D9B0 0x005D void __cdecl Lara_Col_ClimbLeft(ITEM *item, COLL_INFO *coll); 0x0042DA10 0x0059 void __cdecl Lara_Col_ClimbRight(ITEM *item, COLL_INFO *coll); 0x0042DA70 0x020D void __cdecl Lara_Col_ClimbStance(ITEM *item, COLL_INFO *coll); 0x0042DC80 0x014D void __cdecl Lara_Col_Climbing(ITEM *item, COLL_INFO *coll); 0x0042DDD0 0x019C void __cdecl Lara_Col_ClimbDown(ITEM *item, COLL_INFO *coll); 0x0042DF70 0x00AA int32_t __cdecl Lara_CheckForLetGo(ITEM *item, COLL_INFO *coll); 0x0042E020 0x0263 int32_t __cdecl Lara_TestClimb(int32_t x, int32_t y, int32_t z, int32_t xfront, int32_t zfront, int32_t item_height, int16_t item_room, int32_t *shift); 0x0042E290 0x00BC int32_t __cdecl Lara_TestClimbPos(ITEM *item, int32_t front, int32_t right, int32_t origin, int32_t height, int32_t *shift); 0x0042E360 0x00EF void __cdecl Lara_DoClimbLeftRight(ITEM *item, COLL_INFO *coll, int32_t result, int32_t shift); 0x0042E450 0x0235 int32_t __cdecl Lara_TestClimbUpPos(ITEM *item, int32_t front, int32_t right, int32_t *shift, int32_t *ledge); # game/larafire.c 0x0042E6A0 0x04E8 void __cdecl Gun_Control(void); 0x0042EC10 0x003B int32_t __cdecl Gun_CheckForHoldingState(int32_t state); 0x0042EC50 0x011C void __cdecl Gun_InitialiseNewWeapon(void); 0x0042ED90 0x0194 void __cdecl Gun_TargetInfo(const WEAPON_INFO *winfo); 0x0042EF30 0x021C void __cdecl Gun_GetNewTarget(WEAPON_INFO *winfo); 0x0042F150 0x00AA void __cdecl Gun_FindTargetPoint(const ITEM *item, GAME_VECTOR *target); 0x0042F200 0x00C1 void __cdecl Gun_AimWeapon(WEAPON_INFO *winfo, LARA_ARM *arm); 0x0042F2D0 0x0360 int32_t __cdecl Gun_FireWeapon(LARA_GUN_TYPE weapon_type, ITEM *target, const ITEM *src, const int16_t *angles); 0x0042F640 0x0096 void __cdecl Gun_HitTarget(ITEM *item, GAME_VECTOR *hitpos, int32_t damage); 0x0042F6E0 0x0051 void __cdecl Gun_SmashItem(int16_t item_num, LARA_GUN_TYPE weapon_type); 0x0042F740 0x003B GAME_OBJECT_ID Gun_GetWeaponAnim(const LARA_GUN_TYPE gun_type); # game/laraflare.c 0x0042F7A0 0x009D int32_t __cdecl Flare_DoLight(XYZ_32 *pos, int32_t flare_age); 0x0042F840 0x00D3 void __cdecl Flare_DoInHand(int32_t flare_age); 0x0042F920 0x00F8 void __cdecl Flare_DrawInAir(const ITEM *item); 0x0042FA20 0x01D7 void __cdecl Flare_Create(int32_t thrown); 0x0042FC00 0x004B void __cdecl Flare_SetArm(int32_t frame); 0x0042FC50 0x0169 void __cdecl Flare_Draw(void); 0x0042FDC0 0x0221 void __cdecl Flare_Undraw(void); 0x0042FFF0 0x0018 void __cdecl Flare_DrawMeshes(void); 0x00430010 0x0018 void __cdecl Flare_UndrawMeshes(void); 0x00430030 0x003E void __cdecl Flare_Ready(void); 0x00430070 0x026E void __cdecl Flare_Control(int16_t item_num); # game/laramisc.c 0x004302E0 0x0668 void __cdecl Lara_Control(int16_t item_num); 0x00430970 0x02CD void __cdecl Lara_Animate(ITEM *item); 0x00430C70 0x013F void __cdecl Lara_UseItem(GAME_OBJECT_ID object_id); 0x00430E30 0x00BA void __cdecl Lara_CheatGetStuff(void); 0x00430EF0 0x001B void __cdecl Lara_ControlExtra(int16_t item_num); 0x00430F10 0x0021 void __cdecl Lara_InitialiseLoad(int16_t item_num); 0x00430F40 0x02BD void __cdecl Lara_Initialise(int32_t type); 0x00431200 0x036C void __cdecl Lara_InitialiseInventory(int32_t level_num); 0x00431570 0x00FA void __cdecl Lara_InitialiseMeshes(int32_t level_num); # game/larasurf.c 0x00431670 0x0158 void __cdecl Lara_HandleSurface(ITEM *item, COLL_INFO *coll); 0x004317D0 0x0070 void __cdecl Lara_State_SurfSwim(ITEM *item, COLL_INFO *coll); 0x00431840 0x005E void __cdecl Lara_State_SurfBack(ITEM *item, COLL_INFO *coll); 0x004318A0 0x0060 void __cdecl Lara_State_SurfLeft(ITEM *item, COLL_INFO *coll); 0x00431900 0x0060 void __cdecl Lara_State_SurfRight(ITEM *item, COLL_INFO *coll); 0x00431960 0x00EB void __cdecl Lara_State_SurfTread(ITEM *item, COLL_INFO *coll); 0x00431A50 0x0032 void __cdecl Lara_Col_SurfSwim(ITEM *item, COLL_INFO *coll); 0x00431A90 0x0023 void __cdecl Lara_Col_SurfBack(ITEM *item, COLL_INFO *coll); 0x00431AC0 0x0023 void __cdecl Lara_Col_SurfLeft(ITEM *item, COLL_INFO *coll); 0x00431AF0 0x0023 void __cdecl Lara_Col_SurfRight(ITEM *item, COLL_INFO *coll); 0x00431B20 0x001E void __cdecl Lara_Col_SurfTread(ITEM *item, COLL_INFO *coll); 0x00431B40 0x00F3 void __cdecl Lara_SurfaceCollision(ITEM *item, COLL_INFO *coll); 0x00431C40 0x00E7 int32_t __cdecl Lara_TestWaterStepOut(ITEM *item, COLL_INFO *coll); 0x00431D30 0x021C int32_t __cdecl Lara_TestWaterClimbOut(ITEM *item, COLL_INFO *coll); # game/laraswim.c 0x00431F50 0x0223 void __cdecl Lara_HandleUnderwater(ITEM *item, COLL_INFO *coll); 0x00432180 0x0086 void __cdecl Lara_SwimTurn(ITEM *item); 0x00432210 0x006B void __cdecl Lara_State_Swim(ITEM *item, COLL_INFO *coll); 0x00432280 0x0076 void __cdecl Lara_State_Glide(ITEM *item, COLL_INFO *coll); 0x00432300 0x0085 void __cdecl Lara_State_Tread(ITEM *item, COLL_INFO *coll); 0x00432390 0x0014 void __cdecl Lara_State_Dive(ITEM *item, COLL_INFO *coll); 0x004323B0 0x0053 void __cdecl Lara_State_UWDeath(ITEM *item, COLL_INFO *coll); 0x00432410 0x000B void __cdecl Lara_State_UWTwist(ITEM *item, COLL_INFO *coll); 0x00432420 0x0013 void __cdecl Lara_Col_Swim(ITEM *item, COLL_INFO *coll); 0x00432440 0x005B void __cdecl Lara_Col_UWDeath(ITEM *item, COLL_INFO *coll); 0x004324A0 0x0192 int32_t __cdecl Lara_GetWaterDepth(int32_t x, int32_t y, int32_t z, int16_t room_num); 0x00432640 0x00CE void __cdecl Lara_TestWaterDepth(ITEM *item, COLL_INFO *coll); 0x00432710 0x015C void __cdecl Lara_SwimCollision(ITEM *item, COLL_INFO *coll); 0x00432870 0x01EC void __cdecl Lara_WaterCurrent(COLL_INFO *coll); # game/lot.c 0x00432A60 0x0053 void __cdecl LOT_InitialiseArray(void); 0x00432AC0 0x004F void __cdecl LOT_DisableBaddieAI(int16_t item_num); 0x00432B10 0x01B0 bool __cdecl LOT_EnableBaddieAI(int16_t item_num, bool always); 0x00432CC0 0x0106 void __cdecl LOT_InitialiseSlot(int16_t item_num, int32_t slot); 0x00432ED0 0x00B8 void __cdecl LOT_CreateZone(ITEM *item); 0x00432F90 0x0049 void __cdecl LOT_ClearLOT(LOT_INFO *lot); # game/missile.c 0x00432FE0 0x02D0 void __cdecl Missile_Control(int16_t effect_num); 0x004332B0 0x00A7 void __cdecl Missile_ShootAtLara(EFFECT *effect); 0x00433360 0x0386 int32_t __cdecl Item_Explode(int16_t item_num, int32_t mesh_bits, int16_t damage); 0x004336F0 0x0200 void __cdecl BodyPart_Control(int16_t effect_num); # game/moveblock.c 0x004338F0 0x002C void __cdecl MovableBlock_Initialise(int16_t item_num); 0x00433920 0x0148 void __cdecl MovableBlock_Control(int16_t item_num); 0x00433A70 0x0239 void __cdecl MovableBlock_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); 0x00433CD0 0x004E int32_t __cdecl MovableBlock_TestDestination(ITEM *item, int32_t block_height); 0x00433D20 0x0137 int32_t __cdecl MovableBlock_TestPush(ITEM *item, int32_t block_height, uint16_t quadrant); 0x00433E70 0x0225 int32_t __cdecl MovableBlock_TestPull(ITEM *item, int32_t block_height, uint16_t quadrant); 0x004340B0 0x00BB void __cdecl Room_AlterFloorHeight(ITEM *item, int32_t height); 0x00434170 0x0022 void __cdecl MovableBlock_Draw(const ITEM *item); 0x004341A0 0x006B void __cdecl Object_DrawUnclippedItem(const ITEM *item); # game/objects.c 0x00434210 0x00DB void __cdecl Earthquake_Control(int16_t item_num); 0x004342F0 0x003C void __cdecl FinalCutscene_Control(int16_t item_num); 0x00434330 0x009D void __cdecl InitialiseFinalLevel(void); 0x00434400 0x020F void __cdecl FinalLevelCounter_Control(int16_t item_num); 0x00434610 0x00D9 void __cdecl MiniCopter_Control(int16_t item_num); 0x004346F0 0x007C void __cdecl DyingMonk_Initialise(int16_t item_num); 0x00434770 0x0087 void __cdecl DyingMonk_Control(int16_t item_num); 0x00434800 0x00BD void __cdecl GongBonger_Control(int16_t item_num); 0x004348C0 0x00BF void __cdecl Zipline_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); 0x00434980 0x028F void __cdecl Zipline_Control(int16_t item_num); 0x00434C10 0x00E3 void __cdecl BigBowl_Control(int16_t item_num); 0x00434D00 0x007E void __cdecl Bell_Control(int16_t item_num); 0x00434D80 0x0075 void __cdecl Window_Initialise(int16_t item_num); 0x00434E00 0x00C4 void __cdecl Window_Smash(int16_t item_num); 0x00434ED0 0x0096 void __cdecl Window_1_Control(int16_t item_num); 0x00434F70 0x00DC void __cdecl Window_2_Control(int16_t item_num); 0x00435050 0x0042 void __cdecl Door_Shut(DOORPOS_DATA *d); 0x004350A0 0x0032 void __cdecl Door_Open(DOORPOS_DATA *d); 0x004350E0 0x03DC void __cdecl Door_Initialise(int16_t item_num); 0x004354C0 0x00C8 void __cdecl Door_Control(int16_t item_num); 0x00435590 0x00B1 int32_t __cdecl Drawbridge_IsItemOnTop(const ITEM *item, int32_t x, int32_t y); 0x00435650 0x0036 void __cdecl Drawbridge_Floor(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height); 0x00435690 0x003B void __cdecl Drawbridge_Ceiling(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height); 0x004356D0 0x002C void __cdecl Drawbridge_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); 0x00435700 0x0035 void __cdecl Lift_Initialise(int16_t item_num); 0x00435740 0x00D4 void __cdecl Lift_Control(int16_t item_num); 0x00435820 0x0179 void __cdecl Lift_FloorCeiling(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *floor, int32_t *ceiling); 0x004359A0 0x0035 void __cdecl Lift_Floor(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height); 0x004359E0 0x0035 void __cdecl Lift_Ceiling(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height); 0x00435A20 0x0016 void __cdecl BridgeFlat_Floor(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height); 0x00435A40 0x001B void __cdecl BridgeFlat_Ceiling(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height); 0x00435A60 0x003B int32_t __cdecl Bridge_GetOffset(const ITEM *item, int32_t x, int32_t z); 0x00435AA0 0x0030 void __cdecl BridgeTilt1_Floor(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height); 0x00435AD0 0x0035 void __cdecl BridgeTilt1_Ceiling(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height); 0x00435B10 0x002F void __cdecl BridgeTilt2_Floor(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height); 0x00435B40 0x0034 void __cdecl BridgeTilt2_Ceiling(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height); 0x00435B80 0x010C void __cdecl Copter_Control(int16_t item_num); 0x00435C90 0x00D2 void __cdecl General_Control(int16_t item_num); 0x00435D70 0x008D void __cdecl Detonator_Control(int16_t item_num); # game/people.c 0x00435E00 0x0085 bool __cdecl Creature_CanTargetEnemy(const ITEM *item, const AI_INFO *info); 0x00435E90 0x003B void __cdecl Glow_Control(int16_t effect_num); 0x00435ED0 0x004E void __cdecl GunFlash_Control(int16_t effect_num); 0x00435F20 0x0066 int16_t __cdecl Effect_GunShot(int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num); 0x00435F90 0x00B9 int16_t __cdecl Effect_GunHit(int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num); 0x00436050 0x00A7 int16_t __cdecl Effect_GunMiss(int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num); 0x00436100 0x01C4 int32_t __cdecl Creature_ShootAtLara(ITEM *item, AI_INFO *info, BITE *gun, int16_t extra_rotation, int32_t damage); 0x004362D0 0x0043 void __cdecl Cultist1_Initialise(int16_t item_num); 0x00436320 0x0401 void __cdecl Cultist1_Control(int16_t item_num); 0x00436750 0x0050 void __cdecl Cultist3_Initialise(int16_t item_num); 0x004367A0 0x053C void __cdecl Cultist3_Control(int16_t item_num); 0x00436D10 0x03CA void __cdecl Worker1_Control(int16_t item_num); 0x00437110 0x042A void __cdecl Worker2_Control(int16_t item_num); 0x00437570 0x030B void __cdecl Bandit1_Control(int16_t item_num); 0x004378B0 0x0408 void __cdecl Bandit2_Control(int16_t item_num); 0x00437CF0 0x0172 void __cdecl Winston_Control(int16_t item_num); # game/pickup.c 0x00437E70 0x0480 void __cdecl Pickup_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); 0x004382F0 0x020A void __cdecl Switch_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); 0x00438500 0x00FC void __cdecl Switch_CollisionUW(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); 0x00438600 0x023B void __cdecl Detonator_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); 0x00438840 0x0223 void __cdecl Keyhole_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); 0x00438A80 0x0294 void __cdecl PuzzleHole_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); 0x00438D40 0x0039 void __cdecl Switch_Control(int16_t item_num); 0x00438D80 0x00BD int32_t __cdecl Switch_Trigger(int16_t item_num, int16_t timer); 0x00438E40 0x003D int32_t __cdecl Keyhole_Trigger(int16_t item_num); 0x00438E80 0x0033 int32_t __cdecl Pickup_Trigger(int16_t item_num); 0x00438EC0 0x0023 void __cdecl Secret2_Control(int16_t item_num); # game/rat.c 0x00438EF0 0x01DC void __cdecl Mouse_Control(int16_t item_num); # game/savegame.c 0x004390E0 0x0062 void __cdecl InitialiseStartInfo(void); 0x00439150 0x00DB void __cdecl ModifyStartInfo(int32_t level_num); 0x00439230 0x0201 void __cdecl CreateStartInfo(int32_t level_num); 0x00439440 0x052B void __cdecl CreateSaveGameInfo(void); 0x00439970 0x085C void __cdecl ExtractSaveGameInfo(void); 0x0043A1D0 0x0015 void __cdecl ResetSG(void); 0x0043A1F0 0x004C void __cdecl WriteSG(const void *pointer, int32_t size); 0x0043A240 0x0035 void __cdecl ReadSG(void *pointer, int32_t size); # game/setup.c 0x0043A280 0x015F int32_t __cdecl Level_Initialise(int32_t level_num, int32_t level_type); 0x0043A3E0 0x0061 void __cdecl InitialiseGameFlags(void); 0x0043A450 0x0027 void __cdecl InitialiseLevelFlags(void); 0x0043A480 0x103B void __cdecl Object_SetupBaddyObjects(void); 0x0043B4C0 0x05FD void __cdecl Object_SetupTrapObjects(void); 0x0043BAC0 0x0C4C void __cdecl Object_SetupGeneralObjects(void); 0x0043C710 0x0068 void __cdecl Object_SetupAllObjects(void); 0x0043C780 0x00CE void __cdecl GetCarriedItems(void); # game/shark.c 0x0043C850 0x0116 void __cdecl Jelly_Control(int16_t item_num); 0x0043C970 0x021B void __cdecl Barracuda_Control(int16_t item_num); 0x0043CBA0 0x027C void __cdecl Shark_Control(int16_t item_num); # game/skidoo.c 0x0043CE30 0x0040 void __cdecl Skidoo_Initialise(int16_t item_num); 0x0043CE70 0x00E1 int32_t __cdecl Skidoo_CheckGetOn(int16_t item_num, COLL_INFO *coll); 0x0043CF60 0x00F8 void __cdecl Skidoo_Collision(int16_t item_num, ITEM *litem, COLL_INFO *coll); 0x0043D060 0x01F9 void __cdecl Skidoo_BaddieCollision(const ITEM *skidoo); 0x0043D260 0x00B2 int32_t __cdecl Skidoo_TestHeight(const ITEM *item, int32_t z_off, int32_t x_off, XYZ_32 *pos); 0x0043D320 0x027C int32_t __cdecl DoShift(ITEM *skidoo, XYZ_32 *pos, XYZ_32 *old); 0x0043D5A0 0x0054 int32_t __cdecl DoDynamics(int32_t height, int32_t fall_speed, int32_t *y); 0x0043D600 0x0090 int32_t __cdecl GetCollisionAnim(ITEM *skidoo, XYZ_32 *moved); 0x0043D690 0x0140 void __cdecl Skidoo_DoSnowEffect(ITEM *skidoo); 0x0043D7D0 0x049E int32_t __cdecl Skidoo_Dynamics(ITEM *skidoo); 0x0043DC70 0x01B6 int32_t __cdecl Skidoo_UserControl(ITEM *skidoo, int32_t height, int32_t *pitch); 0x0043DE30 0x0106 int32_t __cdecl Skidoo_CheckGetOffOK(int32_t direction); 0x0043DF40 0x02B9 void __cdecl Skidoo_Animation(ITEM *skidoo, int32_t collide, int32_t dead); 0x0043E220 0x007C void __cdecl Skidoo_Explode(const ITEM *skidoo); 0x0043E2A0 0x0233 int32_t __cdecl Skidoo_CheckGetOff(void); 0x0043E4E0 0x011B void __cdecl Skidoo_Guns(void); 0x0043E600 0x0440 int32_t __cdecl Skidoo_Control(void); 0x0043EA60 0x02D5 void __cdecl Skidoo_Draw(const ITEM *item); 0x0043ED40 0x007F void __cdecl SkidooDriver_Initialise(int16_t item_num); 0x0043EDD0 0x03E2 void __cdecl SkidooDriver_Control(int16_t rider_num); 0x0043F1D0 0x0119 void __cdecl SkidooArmed_Push(const ITEM *item, ITEM *lara_item, int32_t radius); 0x0043F2F0 0x0081 void __cdecl SkidooArmed_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); # game/sound.c 0x0043F380 0x0031 int32_t __cdecl Music_GetRealTrack(int32_t track); 0x0043F3C0 0x0484 void __cdecl Sound_Effect(int32_t sample_id, const XYZ_32 *pos, uint32_t flags); 0x0043F860 0x005E void __cdecl Sound_StopEffect(int32_t sample_id); 0x0043F8C0 0x0086 void __cdecl Sound_EndScene(void); 0x0043F950 0x0024 void __cdecl Sound_Shutdown(void); 0x0043F980 0x002A void __cdecl Sound_Init(void); # game/sphere.c 0x0043F9B0 0x0128 int32_t __cdecl Collide_TestCollision(ITEM *item, const ITEM *lara_item); 0x0043FAE0 0x02D8 int32_t __cdecl Collide_GetSpheres(const ITEM *item, SPHERE *spheres, bool world_space); 0x0043FDC0 0x019A void __cdecl Collide_GetJointAbsPosition(const ITEM *item, XYZ_32 *out_vec, int32_t joint); 0x0043FF60 0x005D void __cdecl TeethTrap_Bite(ITEM *item, const BITE *bite); # game/spider.c 0x0043FFC0 0x00AC void __cdecl Spider_Leap(int16_t item_num, int16_t angle); 0x00440070 0x0206 void __cdecl Spider_Control(int16_t item_num); 0x00440290 0x01A5 void __cdecl BigSpider_Control(int16_t item_num); # game/text.c 0x00440450 0x002C void __cdecl Text_Init(void); 0x00440480 0x0105 TEXTSTRING *__cdecl Text_Create(int32_t x, int32_t y, int32_t z, const char *text); 0x00440590 0x0037 void __cdecl Text_ChangeText(TEXTSTRING *string, const char *text); 0x004405D0 0x0017 void __cdecl Text_SetScale(TEXTSTRING *string, int32_t scale_h, int32_t scale_v); 0x004405F0 0x002B void __cdecl Text_Flash(TEXTSTRING *string, int16_t enable, int16_t rate); 0x00440620 0x008C void __cdecl Text_AddBackground(TEXTSTRING *string, int16_t x_size, int16_t y_size, int16_t x_off, int16_t y_off, int16_t z_off, int16_t color, uint16_t *gour_ptr, uint16_t flags); 0x004406B0 0x0010 void __cdecl Text_RemoveBackground(TEXTSTRING *string); 0x004406C0 0x0029 void __cdecl Text_AddOutline(TEXTSTRING *string, int16_t enable, int16_t color, uint16_t *gour_ptr, uint16_t flags); 0x004406F0 0x0010 void __cdecl Text_RemoveOutline(TEXTSTRING *string); 0x00440700 0x001E void __cdecl Text_CentreH(TEXTSTRING *string, int16_t enable); 0x00440720 0x001E void __cdecl Text_CentreV(TEXTSTRING *string, int16_t enable); 0x00440740 0x001E void __cdecl Text_AlignRight(TEXTSTRING *string, int16_t enable); 0x00440760 0x001E void __cdecl Text_AlignBottom(TEXTSTRING *string, int16_t enable); 0x00440780 0x0107 int32_t __cdecl Text_GetWidth(TEXTSTRING *string); 0x00440890 0x0025 int32_t __cdecl Text_Remove(TEXTSTRING *string); 0x004408C0 0x0024 int16_t __cdecl Text_GetTextLength(const char *text); 0x004408F0 0x0027 void __cdecl Text_Draw(void); 0x00440920 0x0189 void __cdecl Text_DrawBorder(int32_t x, int32_t y, int32_t z, int32_t width, int32_t height); 0x00440AB0 0x03D2 void __cdecl Text_DrawText(const TEXTSTRING *string); 0x00440E90 0x0037 uint32_t __cdecl Text_GetScaleH(uint32_t value); 0x00440ED0 0x0039 uint32_t __cdecl Text_GetScaleV(uint32_t value); # game/traps.c 0x00440F10 0x01F4 void __cdecl Mine_Control(int16_t mine_num); 0x00441110 0x0138 void __cdecl SpikeWall_Control(int16_t item_num); 0x00441250 0x0115 void __cdecl SpikeCeiling_Control(int16_t item_num); 0x00441370 0x0086 void __cdecl Hook_Control(int16_t item_num); 0x00441400 0x0190 void __cdecl Propeller_Control(int16_t item_num); 0x00441590 0x017B void __cdecl SpinningBlade_Control(int16_t item_num); 0x00441710 0x00FE void __cdecl Icicle_Control(int16_t item_num); 0x00441810 0x003C void __cdecl Blade_Initialise(int16_t item_num); 0x00441850 0x0091 void __cdecl Blade_Control(int16_t item_num); 0x004418F0 0x0046 void __cdecl KillerStatue_Initialise(int16_t item_num); 0x00441940 0x0109 void __cdecl KillerStatue_Control(int16_t item_num); 0x00441A50 0x00DB void __cdecl Springboard_Control(int16_t item_num); 0x00441B30 0x003C void __cdecl RollingBall_Initialise(int16_t item_num); 0x00441B70 0x0347 void __cdecl RollingBall_Control(int16_t item_num); 0x00441EC0 0x024A void __cdecl RollingBall_Collision(int16_t item_num, ITEM *litem, COLL_INFO *coll); 0x00442110 0x0155 void __cdecl Spikes_Collision(int16_t item_num, ITEM *litem, COLL_INFO *coll); 0x00442270 0x004F void __cdecl Trapdoor_Control(int16_t item_num); 0x004422C0 0x003A void __cdecl Trapdoor_Floor(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height); 0x00442300 0x003F void __cdecl Trapdoor_Ceiling(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height); 0x00442340 0x00A3 int32_t __cdecl Trapdoor_IsItemOnTop(const ITEM *item, int32_t x, int32_t z); 0x004423F0 0x010A void __cdecl Pendulum_Control(int16_t item_num); 0x00442500 0x0105 void __cdecl FallingBlock_Control(int16_t item_num); 0x00442610 0x003E void __cdecl FallingBlock_Floor(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height); 0x00442650 0x0044 void __cdecl FallingBlock_Ceiling(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height); 0x004426A0 0x00BD void __cdecl TeethTrap_Control(int16_t item_num); 0x00442760 0x00E0 void __cdecl FallingCeiling_Control(int16_t item_num); 0x00442840 0x013E void __cdecl DartEmitter_Control(int16_t item_num); 0x00442980 0x0155 void __cdecl Dart_Control(int16_t item_num); 0x00442AE0 0x004B void __cdecl DartEffect_Control(int16_t effect_num); 0x00442B30 0x0090 void __cdecl FlameEmitter_Control(int16_t item_num); 0x00442BC0 0x0164 void __cdecl Flame_Control(int16_t effect_num); 0x00442D30 0x0049 void __cdecl Lara_CatchFire(void); 0x00442D80 0x00E6 void __cdecl Lara_TouchLava(ITEM *item); 0x00442E70 0x00C5 void __cdecl EmberEmitter_Control(int16_t item_num); 0x00442F40 0x010B void __cdecl Ember_Control(int16_t effect_num); # game/yeti.c 0x00443050 0x02CA void __cdecl BirdGuardian_Control(int16_t item_num); 0x00443350 0x05ED void __cdecl Yeti_Control(int16_t item_num); 0x00443990 0x01B8 void __cdecl BGND_Make640x480(uint8_t *bitmap, RGB_888 *palette); 0x00443B50 0x00B9 int32_t __cdecl BGND_AddTexture(int32_t tile_idx, BYTE *bitmap, int32_t pal_index, RGB_888 *bmp_pal); 0x00443C10 0x0032 void __cdecl BGND_GetPageHandles(void); 0x00443C50 0x005F void __cdecl BGND_DrawInGameBlack(void); 0x00443CB0 0x00DC void __cdecl BGND_DrawQuad(float sx, float sy, float width, float height, D3DCOLOR color); 0x00443D90 0x0220 void __cdecl BGND_DrawInGameBackground(void); 0x00443FB0 0x0251 void __cdecl BGND_DrawTextureTile(int32_t sx, int32_t sy, int32_t width, int32_t height, HWR_TEXTURE_HANDLE tex_source, int32_t tu, int32_t tv, int32_t t_width, int32_t t_height, D3DCOLOR color0, D3DCOLOR color1, D3DCOLOR color2, D3DCOLOR color3); 0x00444210 0x008B D3DCOLOR __cdecl BGND_CenterLighting(int32_t x, int32_t y, int32_t width, int32_t height); 0x004444C0 0x004D void __cdecl BGND_Free(void); 0x00444510 0x0030 bool __cdecl BGND_Init(void); 0x00444540 0x003E void __cdecl Enumerate3DDevices(DISPLAY_ADAPTER *adapter); 0x00444570 0x001F bool __cdecl D3DCreate(void); 0x004445B0 0x00BD HRESULT __stdcall Enum3DDevicesCallback(GUID *lpGuid, LPTSTR lpDeviceDescription, LPTSTR lpDeviceName, LPD3DDEVICEDESC lpD3DHWDeviceDesc, LPD3DDEVICEDESC lpD3DHELDeviceDesc, LPVOID lpContext); 0x00444670 0x0037 bool __cdecl D3DIsSupported(LPD3DDEVICEDESC desc); 0x004446B0 0x00B9 bool __cdecl D3DSetViewport(void); 0x00444770 0x01B8 void __cdecl D3DDeviceCreate(LPDDS lpBackBuffer); 0x00444930 0x006A void __cdecl Direct3DRelease(void); 0x00444980 0x0006 bool __cdecl Direct3DInit(void); 0x00444990 0x0018 sub_444990 0x004449A0 0x0012 sub_4449A0 0x004449D0 0x00C6 sub_4449D0 0x00444AA0 0x0018 sub_444AA0 0x00444AB0 0x005F sub_444AB0 0x00444B20 0x008C sub_444B20 0x00444BB0 0x0005 sub_444BB0 0x00444BC0 0x0001 sub_444BC0 0x00444BD0 0x0054 bool __cdecl DDrawCreate(LPGUID lpGUID); 0x00444C30 0x0033 void __cdecl DDrawRelease(void); 0x00444C70 0x0073 void __cdecl GameWindowCalculateSizeFromClient(int32_t *width, int32_t *height); 0x00444CF0 0x006A void __cdecl GameWindowCalculateSizeFromClientByZero(int32_t *width, int32_t *height); 0x00444D60 0x0041 void __cdecl WinVidSetMinWindowSize(int32_t width, int32_t height); 0x00444DB0 0x0008 void __cdecl WinVidClearMinWindowSize(void); 0x00444DC0 0x0041 void __cdecl WinVidSetMaxWindowSize(int32_t width, int32_t height); 0x00444E10 0x0008 void __cdecl WinVidClearMaxWindowSize(void); 0x00444E20 0x0048 int32_t __cdecl CalculateWindowWidth(int32_t width, int32_t height); 0x00444E70 0x0028 int32_t __cdecl CalculateWindowHeight(int32_t width, int32_t height); 0x00444EA0 0x0104 bool __cdecl WinVidGetMinMaxInfo(LPMINMAXINFO info); 0x00444FB0 0x0011 HWND __cdecl WinVidFindGameWindow(void); 0x00444FD0 0x00E2 bool __cdecl WinVidSpinMessageLoop(bool needWait); 0x004450C0 0x0043 void __cdecl WinVidShowGameWindow(int32_t nCmdShow); 0x00445110 0x003A void __cdecl WinVidHideGameWindow(void); 0x00445150 0x0035 void __cdecl WinVidSetGameWindowSize(int32_t width, int32_t height); 0x00445190 0x00A7 bool __cdecl ShowDDrawGameWindow(bool active); 0x00445240 0x0087 bool __cdecl HideDDrawGameWindow(void); 0x004452D0 0x0044 HRESULT __cdecl DDrawSurfaceCreate(LPDDSDESC dsp, LPDDS *surface); 0x00445320 0x0046 HRESULT __cdecl DDrawSurfaceRestoreLost(LPDDS surface1, LPDDS surface2, bool blank); 0x00445370 0x004D bool __cdecl WinVidClearBuffer(LPDDS surface, LPRECT rect, DWORD fill_color); 0x004453C0 0x003D HRESULT __cdecl WinVidBufferLock(LPDDS surface, LPDDSDESC desc, DWORD flags); 0x00445400 0x0025 HRESULT __cdecl WinVidBufferUnlock(LPDDS surface, LPDDSDESC desc); 0x00445430 0x0090 bool __cdecl WinVidCopyBitmapToBuffer(LPDDS surface, const BYTE *bitmap); 0x004454C0 0x0046 DWORD __cdecl GetRenderBitDepth(DWORD dwRGBBitCount); 0x00445550 0x0071 void __thiscall WinVidGetColorBitMasks(COLOR_BIT_MASKS *bm, LPDDPIXELFORMAT pixel_format); 0x004455D0 0x0044 void __cdecl BitMaskGetNumberOfBits(uint32_t bit_mask, uint32_t *bit_depth, uint32_t *bit_offset); 0x00445620 0x0061 DWORD __cdecl CalculateCompatibleColor(COLOR_BIT_MASKS *mask, int32_t red, int32_t green, int32_t blue, int32_t alpha); 0x00445690 0x008C bool __cdecl WinVidGetDisplayMode(DISPLAY_MODE *disp_mode); 0x00445720 0x0088 bool __cdecl WinVidGoFullScreen(DISPLAY_MODE *disp_mode); 0x004457B0 0x010B bool __cdecl WinVidGoWindowed(int32_t width, int32_t height, DISPLAY_MODE *dispMode); 0x004458C0 0x00D5 void __cdecl WinVidSetDisplayAdapter(DISPLAY_ADAPTER *disp_adapter); 0x004459A0 0x0045 bool __thiscall CompareVideoModes(const DISPLAY_MODE *mode1, const DISPLAY_MODE *mode2); 0x004459F0 0x0053 bool __cdecl WinVidGetDisplayModes(void); 0x00445A50 0x03B1 HRESULT __stdcall EnumDisplayModesCallback(LPDDSDESC lpDDSurfaceDesc, LPVOID lpContext); 0x00445E10 0x0040 bool __cdecl WinVidInit(void); 0x00445E50 0x00AF bool __cdecl WinVidGetDisplayAdapters(void); 0x00445F00 0x0013 void __thiscall S_FlaggedString_Delete(STRING_FLAGGED *string); 0x00445F20 0x001A bool __cdecl EnumerateDisplayAdapters(DISPLAY_ADAPTER_LIST *displayAdapterList); 0x00445F40 0x01BE BOOL __stdcall EnumDisplayAdaptersCallback(GUID *lpGUID, LPTSTR lpDriverDescription, LPTSTR lpDriverName, LPVOID lpContext); 0x00446100 0x0035 void __thiscall S_FlaggedString_InitAdapter(DISPLAY_ADAPTER *adapter); 0x00446140 0x006A bool __cdecl WinVidRegisterGameWindowClass(void); 0x004461B0 0x049F LRESULT __stdcall WinVidGameWindowProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam); 0x004467C0 0x01C0 void __cdecl WinVidResizeGameWindow(HWND hWnd, int32_t edge, LPRECT rect); 0x004469A0 0x00BC bool __cdecl WinVidCheckGameWindowPalette(HWND hWnd); 0x00446A60 0x00C6 bool __cdecl WinVidCreateGameWindow(void); 0x00446B30 0x0022 void __cdecl WinVidFreeWindow(void); 0x00446B60 0x004D void __cdecl WinVidExitMessage(void); 0x00446BB0 0x0048 DISPLAY_ADAPTER_NODE *__cdecl WinVidGetDisplayAdapter(GUID *lpGuid); 0x00446C00 0x0374 void __cdecl WinVidStart(void); 0x00446F80 0x0013 void __cdecl WinVidFinish(void); 0x00446FA0 0x000D void __thiscall DisplayModeListInit(DISPLAY_MODE_LIST *pList); 0x00446FB0 0x0032 void __thiscall DisplayModeListDelete(DISPLAY_MODE_LIST *pList); 0x00446FF0 0x0012 DISPLAY_MODE *__thiscall InsertDisplayMode(DISPLAY_MODE_LIST *modeList, DISPLAY_MODE_NODE *before); 0x00447010 0x0048 DISPLAY_MODE *__thiscall InsertDisplayModeInListHead(DISPLAY_MODE_LIST *modeList); 0x00447060 0x004A DISPLAY_MODE *__thiscall InsertDisplayModeInListTail(DISPLAY_MODE_LIST *modeList); 0x004470B0 0x0018 sub_4470B0 0x004470C0 0x0012 sub_4470C0 0x004470F0 0x0068 sub_4470F0 0x00447160 0x0018 sub_447160 0x00447170 0x0039 sub_447170 0x004471C0 0x002F sub_4471C0 0x004471F0 0x0022 bool __cdecl DInputCreate(void); 0x00447220 0x001A void __cdecl DInputRelease(void); 0x00447240 0x005A void __cdecl WinInReadKeyboard(LPVOID lpInputData); 0x004472A0 0x00F3 DWORD __cdecl WinInReadJoystick(int32_t *x, int32_t *y); 0x004473A0 0x0005 sub_4473A0 0x004473B0 0x007F bool __cdecl WinInputInit(void); 0x00447430 0x0024 bool __cdecl DInputEnumDevices(JOYSTICK_LIST *joystickList); 0x00447460 0x00E8 BOOL __stdcall DInputEnumDevicesCallback(LPCDIDEVICEINSTANCE lpddi, LPVOID pvRef); 0x00447550 0x001F void __thiscall S_FlaggedString_Create(STRING_FLAGGED *string, int32_t size); 0x00447570 0x004E JOYSTICK_NODE *__cdecl GetJoystick(GUID *lpGuid); 0x004475C0 0x00C9 void __cdecl DInputKeyboardCreate(void); 0x00447690 0x0029 void __cdecl DInputKeyboardRelease(void); 0x004476C0 0x00E4 bool __cdecl DInputJoystickCreate(void); 0x004477B0 0x002D void __cdecl WinInStart(void); 0x004477E0 0x000F void __cdecl WinInFinish(void); 0x004477F0 0x0017 void __cdecl WinInRunControlPanel(HWND hWnd); 0x00447810 0x0062 void __cdecl IncreaseScreenSize(void); 0x00447880 0x0062 void __cdecl DecreaseScreenSize(void); 0x004478F0 0x009F void __cdecl setup_screen_size(void); 0x00447990 0x0034 void __cdecl TempVideoAdjust(int32_t hires, double sizer); 0x004479D0 0x0039 void __cdecl TempVideoRemove(void); 0x00447A10 0x0035 void __cdecl S_FadeInInventory(BOOL isFade); 0x00447A50 0x0027 void __cdecl S_FadeOutInventory(BOOL isFade); 0x00447A80 0x0018 sub_447A80 0x00447A90 0x0012 sub_447A90 0x00447AC0 0x0068 sub_447AC0 0x00447B30 0x0018 sub_447B30 0x00447B40 0x0039 sub_447B40 0x00447B90 0x002F sub_447B90 0x00447BC0 0x0048 const SOUND_ADAPTER_NODE *__cdecl S_Audio_Sample_GetAdapter(GUID *guid); 0x00447C10 0x002E void __cdecl S_Audio_Sample_CloseAllTracks(void); 0x00447C40 0x010E bool __cdecl S_Audio_Sample_Load(int32_t sample_id, LPWAVEFORMATEX format, const void *data, int32_t data_size); 0x00447D50 0x0045 bool __cdecl S_Audio_Sample_IsTrackPlaying(int32_t track_id); 0x00447DA0 0x00E7 int32_t __cdecl S_Audio_Sample_Play(int32_t sample_id, int32_t volume, int32_t pitch, int32_t pan, int32_t flags); 0x00447E90 0x0039 int32_t __cdecl S_Audio_Sample_GetFreeTrackIndex(void); 0x00447ED0 0x002C void __cdecl S_Audio_Sample_AdjustTrackVolumeAndPan(int32_t track_id, int32_t volume, int32_t pan); 0x00447F00 0x0031 void __cdecl S_Audio_Sample_AdjustTrackPitch(int32_t track_id, int32_t pitch); 0x00447F40 0x002F void __cdecl S_Audio_Sample_CloseTrack(int32_t track_id); 0x00447FA0 0x0005 sub_447FA0 0x00447FB0 0x009C bool __cdecl S_Audio_Sample_Init(void); 0x00448050 0x001A bool __cdecl S_Audio_Sample_DSoundEnumerate(SOUND_ADAPTER_LIST *adapter_list); 0x00448070 0x00E2 BOOL __stdcall S_Audio_Sample_DSoundEnumCallback(LPGUID guid, LPCTSTR description, LPCTSTR module, LPVOID context); 0x00448160 0x017C void __cdecl S_Audio_Sample_Init2(HWND hwnd); 0x004482E0 0x001C bool __cdecl S_Audio_Sample_DSoundCreate(GUID *guid); 0x00448300 0x00C4 bool __cdecl S_Audio_Sample_DSoundBufferTest(void); 0x004483D0 0x002A void __cdecl S_Audio_Sample_Shutdown(void); 0x00448400 0x0006 bool __cdecl S_Audio_Sample_IsEnabled(void); 0x00448410 0x0005 sub_448410 0x00448420 0x0001 sub_448420 0x00448430 0x013B void __cdecl CreateScreenBuffers(void); 0x00448570 0x0094 void __cdecl CreatePrimarySurface(void); 0x00448610 0x0098 void __cdecl CreateBackBuffer(void); 0x004486B0 0x009D void __cdecl CreateClipper(void); 0x00448750 0x00D3 void __cdecl CreateWindowPalette(void); 0x00448830 0x00BC void __cdecl CreateZBuffer(void); 0x004488F0 0x002B DWORD __cdecl GetZBufferDepth(void); 0x00448920 0x00A1 void __cdecl CreateRenderBuffer(void); 0x004489D0 0x0070 void __cdecl CreatePictureBuffer(void); 0x00448A40 0x01A4 void __cdecl ClearBuffers(DWORD flags, DWORD fill_color); 0x00448BF0 0x013C void __cdecl RestoreLostBuffers(void); 0x00448D30 0x00CF void __cdecl UpdateFrame(bool need_run_message_loop, LPRECT rect); 0x00448E00 0x003B void __cdecl WaitPrimaryBufferFlip(void); 0x00448E40 0x0003 bool __cdecl RenderInit(void); 0x00448E50 0x03A5 void __cdecl RenderStart(bool is_reset); 0x00449200 0x00E6 void __cdecl RenderFinish(bool need_to_clear_textures); 0x004492F0 0x0204 bool __cdecl ApplySettings(APP_SETTINGS *new_settings); 0x00449500 0x0105 void __cdecl FmvBackToGame(void); 0x00449610 0x023A void __cdecl GameApplySettings(APP_SETTINGS *new_settings); 0x00449850 0x0067 void __cdecl UpdateGameResolution(void); 0x004498C0 0x000C LPCTSTR __cdecl DecodeErrorMessage(DWORD error_code); 0x004498D0 0x0049 BOOL __cdecl ReadFileSync(HANDLE handle, LPVOID lpBuffer, DWORD nBytesToRead, LPDWORD lpnBytesRead, LPOVERLAPPED lpOverlapped); 0x00449920 0x0188 BOOL __cdecl Level_LoadTexturePages(HANDLE handle); 0x00449AB0 0x03A0 BOOL __cdecl Level_LoadRooms(HANDLE handle); 0x00449E50 0x0097 void __cdecl AdjustTextureUVs(bool reset_uv_add); 0x00449EF0 0x057E BOOL __cdecl Level_LoadObjects(HANDLE handle); 0x0044A470 0x0135 BOOL __cdecl Level_LoadSprites(HANDLE handle); 0x0044A5B0 0x01D6 BOOL __cdecl Level_LoadItems(HANDLE handle); 0x0044A790 0x0188 BOOL __cdecl Level_LoadDepthQ(HANDLE handle); 0x0044A920 0x0071 BOOL __cdecl Level_LoadPalettes(HANDLE handle); 0x0044A9A0 0x0060 BOOL __cdecl Level_LoadCameras(HANDLE handle); 0x0044AA00 0x0060 BOOL __cdecl Level_LoadSoundEffects(HANDLE handle); 0x0044AA60 0x0221 BOOL __cdecl Level_LoadBoxes(HANDLE handle); 0x0044AC90 0x0055 BOOL __cdecl Level_LoadAnimatedTextures(HANDLE handle); 0x0044ACF0 0x0079 BOOL __cdecl Level_LoadCinematic(HANDLE handle); 0x0044AD70 0x008A BOOL __cdecl Level_LoadDemo(HANDLE handle); 0x0044AE00 0x009A void __cdecl Level_LoadDemoExternal(LPCTSTR level_name); 0x0044AEA0 0x0265 BOOL __cdecl Level_LoadSamples(HANDLE handle); 0x0044B110 0x0036 void __cdecl ChangeFileNameExtension(char *file_name, const char *file_ext); 0x0044B150 0x0026 LPCTSTR __cdecl GetFullPath(LPCTSTR file_name); 0x0044B180 0x00E0 BOOL __cdecl SelectDrive(void); 0x0044B260 0x024A bool __cdecl Level_Load(const char *file_name, int32_t level_num); 0x0044B4B0 0x0018 BOOL __cdecl S_LoadLevelFile(LPCTSTR file_name, int32_t level_num, GAME_FLOW_LEVEL_TYPE level_type); 0x0044B4D0 0x002A void __cdecl S_UnloadLevelFile(void); 0x0044B500 0x0014 void __cdecl S_AdjustTexelCoordinates(void); 0x0044B520 0x00C4 BOOL __cdecl S_ReloadLevelGraphics(BOOL reload_palettes, BOOL reload_tex_pages); 0x0044B5F0 0x00C6 BOOL __cdecl GF_ReadStringTable(DWORD count, char **string_table, char **string_buf, LPDWORD buf_size, HANDLE handle); 0x0044B6C0 0x06D1 BOOL __cdecl GF_LoadFromFile(const char *file_name); 0x0044BDA0 0x006B bool __cdecl PlayFMV(const char *file_name); 0x0044BE10 0x02E0 void __cdecl WinPlayFMV(const char *file_name, bool is_playback); 0x0044C0F0 0x0048 void __cdecl WinStopFMV(bool is_playback); 0x0044C140 0x0088 bool __cdecl IntroFMV(const char *file_name1, const char *file_name2); 0x0044C1D0 0x0023 uint16_t __cdecl S_FindColor(int32_t red, int32_t green, int32_t blue); 0x0044C200 0x0035 void __cdecl S_DrawScreenLine(int32_t x, int32_t y, int32_t z, int32_t x_len, int32_t y_len, BYTE color_idx, D3DCOLOR *gour, uint16_t flags); 0x0044C240 0x0116 void __cdecl S_DrawScreenBox(int32_t sx, int32_t sy, int32_t z, int32_t width, int32_t height, BYTE color_idx, const GOURAUD_OUTLINE *gour, uint16_t flags); 0x0044C360 0x002E void __cdecl S_DrawScreenFBox(int32_t sx, int32_t sy, int32_t z, int32_t width, int32_t height, BYTE color_idx, const GOURAUD_FILL *gour, uint16_t flags); 0x0044C390 0x000F void __cdecl S_FinishInventory(void); 0x0044C3A0 0x0043 void __cdecl S_FadeToBlack(void); 0x0044C3F0 0x0057 void __cdecl S_Wait(int32_t timeout, BOOL input_check); 0x0044C450 0x000E bool __cdecl S_PlayFMV(const char *file_name); 0x0044C460 0x0013 bool __cdecl S_IntroFMV(const char *file_name1, const char *file_name2); 0x0044C480 0x0144 int16_t __cdecl Game_Start(int32_t level_num, GAME_FLOW_LEVEL_TYPE level_type); 0x0044C5D0 0x009A int32_t __cdecl Game_Loop(bool demo_mode); 0x0044C670 0x0006 int32_t __cdecl LevelCompleteSequence(void); 0x0044C680 0x01C2 int32_t __cdecl LevelStats(int32_t level_num); 0x0044C850 0x0113 int32_t __cdecl GameStats(int32_t level_num); 0x0044C970 0x001E int32_t __cdecl Random_GetControl(void); 0x0044C990 0x000A void __cdecl Random_SeedControl(int32_t seed); 0x0044C9A0 0x001E int32_t __cdecl Random_GetDraw(void); 0x0044C9C0 0x000A void __cdecl Random_SeedDraw(int32_t seed); 0x0044C9D0 0x0044 void __cdecl GetValidLevelsList(REQUEST_INFO *req); 0x0044CA20 0x004C void __cdecl GetSavedGamesList(REQUEST_INFO *req); 0x0044CA70 0x0233 void __cdecl DisplayCredits(void); 0x0044CCB0 0x0165 BOOL __cdecl S_FrontEndCheck(void); 0x0044CE20 0x0114 int32_t __cdecl S_SaveGame(const void *save_data, uint32_t save_size, int32_t slot_num); 0x0044CF40 0x0096 int32_t __cdecl S_LoadGame(void *save_data, uint32_t save_size, int32_t slot_num); 0x0044CFE0 0x0128 void __cdecl HWR_InitState(void); 0x0044D110 0x0029 void __cdecl HWR_ResetTexSource(void); 0x0044D140 0x002B void __cdecl HWR_ResetColorKey(void); 0x0044D170 0x0059 void __cdecl HWR_ResetZBuffer(void); 0x0044D1D0 0x0024 void __cdecl HWR_TexSource(HWR_TEXTURE_HANDLE tex_source); 0x0044D200 0x004A void __cdecl HWR_EnableColorKey(bool state); 0x0044D250 0x0082 void __cdecl HWR_EnableZBuffer(bool z_write_enable, bool z_enable); 0x0044D2E0 0x0016 void __cdecl HWR_BeginScene(void); 0x0044D310 0x016C void __cdecl HWR_DrawPolyList(void); 0x0044D490 0x008E void __cdecl HWR_LoadTexturePages(int32_t pages_count, void *pages_buf, RGB_888 *palette); 0x0044D520 0x004A void __cdecl HWR_FreeTexturePages(void); 0x0044D570 0x0035 void __cdecl HWR_GetPageHandles(void); 0x0044D5B0 0x0019 bool __cdecl HWR_VertexBufferFull(void); 0x0044D5E0 0x0022 bool __cdecl HWR_Init(void); 0x0044D610 0x005C BOOL __cdecl S_InitialiseSystem(void); 0x0044D670 0x0011 void __cdecl GameBuf_Shutdown(void); 0x0044D690 0x0021 void __cdecl GameBuf_Reset(void); 0x0044D6C0 0x006C void *__cdecl GameBuf_Alloc(size_t alloc_size, GAME_BUFFER buf_index); 0x0044D740 0x0034 void __cdecl GameBuf_Free(size_t free_size); 0x0044D780 0x00E8 void __cdecl Output_CalculateWibbleTable(void); 0x0044D870 0x007F void __cdecl Random_Seed(void); 0x0044D8F0 0x0120 BOOL __cdecl S_Input_Key(KEYMAP keymap); 0x0044DA10 0x0AC4 bool __cdecl Input_Update(void); 0x0044E4E0 0x003C int32_t __cdecl RenderErrorBox(int32_t error_code); 0x0044E520 0x01D6 int32_t __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int32_t nShowCmd); 0x0044E6F0 0x0001 sub_44E6F0 0x0044E700 0x0064 int32_t __cdecl GameInit(bool skip_cd_init); 0x0044E770 0x0055 void __cdecl Shell_Cleanup(void); 0x0044E7A0 0x007C int32_t __cdecl WinGameStart(void); 0x0044E820 0x0039 void __cdecl Shell_Shutdown(void); 0x0044E860 0x0017 sub_44E860 0x0044E880 0x0010 sub_44E880 0x0044E890 0x003C void __cdecl Shell_ExitSystem(const char *message); 0x0044E8E0 0x010F void __cdecl ScreenshotPCX(void); 0x0044E9F0 0x00AE DWORD __cdecl CompPCX(BYTE *bitmap, DWORD width, DWORD height, RGB_888 *palette, BYTE **pcx_data); 0x0044EAA0 0x00D2 DWORD __cdecl EncodeLinePCX(BYTE *src, DWORD width, BYTE *dst); 0x0044EB80 0x003E DWORD __cdecl EncodePutPCX(BYTE value, BYTE num, BYTE *buffer); 0x0044EBC0 0x01F5 void __cdecl Screenshot(LPDDS screen); 0x0044EDC0 0x007F void __cdecl Option_DoInventory(INVENTORY_ITEM *item); 0x0044EED0 0x0648 void __cdecl Option_Passport(INVENTORY_ITEM *item); 0x0044F520 0x02DA void __cdecl Option_Detail(INVENTORY_ITEM *item); 0x0044F800 0x049D void __cdecl Option_Sound(INVENTORY_ITEM *item); 0x0044FCA0 0x00C0 void __cdecl Option_Compass(INVENTORY_ITEM *item); 0x0044FD60 0x007E void __cdecl Option_Controls_FlashConflicts(void); 0x0044FDE0 0x0040 void __cdecl Option_Controls_DefaultConflict(void); 0x0044FE20 0x06F4 void __cdecl Option_Controls(INVENTORY_ITEM *item); 0x00450530 0x04D0 void __cdecl Option_Controls_ShowControls(void); 0x00450A00 0x0096 void __cdecl Option_Controls_UpdateText(void); 0x00450AA0 0x003B void __cdecl S_RemoveCtrlText(void); 0x00450AE0 0x0006 int32_t __cdecl GetRenderHeight(void); 0x00450AF0 0x0006 int32_t __cdecl GetRenderWidth(void); 0x00450B00 0x00E4 void __cdecl S_InitialisePolyList(BOOL clear_back_buffer); 0x00450BF0 0x0036 DWORD __cdecl S_DumpScreen(void); 0x00450C30 0x000B void __cdecl S_ClearScreen(void); 0x00450C40 0x0037 void __cdecl S_InitialiseScreen(GAME_FLOW_LEVEL_TYPE level_type); 0x00450C80 0x0089 void __cdecl S_OutputPolyList(void); 0x00450CC0 0x0270 int32_t __cdecl Output_GetObjectBounds(const BOUNDS_16 *bounds); 0x00450F30 0x0046 void __cdecl S_InsertBackPolygon(int32_t x0, int32_t y0, int32_t x1, int32_t y1); 0x00450F80 0x01F1 void __cdecl Output_InsertShadow(int16_t radius, const BOUNDS_16 *bounds, const ITEM *item); 0x00451180 0x02F6 void __cdecl Output_CalculateLight(int32_t x, int32_t y, int32_t z, int16_t room_num); 0x00451480 0x0031 void __cdecl Output_CalculateStaticLight(int16_t adder); 0x004514C0 0x0124 void __cdecl Output_CalculateStaticMeshLight(int32_t x, int32_t y, int32_t z, int32_t shade_1, int32_t shade_2, ROOM *room); 0x004515F0 0x0206 void __cdecl Output_LightRoom(ROOM *room); 0x00451800 0x01CC void __cdecl Output_DrawHealthBar(int32_t percent); 0x004519D0 0x01F6 void __cdecl Output_DrawAirBar(int32_t percent); 0x00451BD0 0x00C0 void __cdecl Output_DoAnimateTextures(int32_t ticks); 0x00451C90 0x0051 void __cdecl Output_SetupBelowWater(bool underwater); 0x00451CF0 0x0021 void __cdecl Output_SetupAboveWater(bool underwater); 0x00451D20 0x00B1 void __cdecl Output_AnimateTextures(int32_t ticks); 0x00451DE0 0x0105 void __cdecl S_DisplayPicture(const char *file_name, BOOL is_title); 0x00451EF0 0x007E void __cdecl S_SyncPictureBufferPalette(void); 0x00451F70 0x001C void __cdecl S_DontDisplayPicture(void); 0x00451F80 0x000D void __cdecl ScreenDump(void); 0x00451F90 0x0010 void __cdecl ScreenPartialDump(void); 0x00451FA0 0x01C9 void __cdecl FadeToPal(int32_t fade_value, RGB_888 *palette); 0x00452170 0x0026 void __cdecl ScreenClear(bool is_phd_win_size); 0x004521A0 0x00AB void __cdecl S_CopyScreenToBuffer(void); 0x00452250 0x0254 void __cdecl S_CopyBufferToScreen(void); 0x004522A0 0x00FA BOOL __cdecl DecompPCX(const uint8_t *pcx, size_t pcx_size, LPBYTE pic, RGB_888 *pal); 0x004523A0 0x0005 sub_4523A0 0x004523B0 0x0001 sub_4523B0 0x004523C0 0x004E bool __cdecl OpenGameRegistryKey(LPCTSTR key); 0x00452410 0x0005 LONG __cdecl CloseGameRegistryKey(void); 0x00452420 0x0262 bool __cdecl SE_WriteAppSettings(APP_SETTINGS *settings); 0x00452690 0x0348 int32_t __cdecl SE_ReadAppSettings(APP_SETTINGS *settings); 0x004529E0 0x00D7 bool __cdecl SE_GraphicsTestStart(void); 0x00452AB0 0x0014 void __cdecl SE_GraphicsTestFinish(void); 0x00452AD0 0x0003 int32_t __cdecl SE_GraphicsTestExecute(void); 0x00452AE0 0x0057 int32_t __cdecl SE_GraphicsTest(void); 0x00452B40 0x00C7 bool __cdecl SE_SoundTestStart(void); 0x00452C00 0x0005 void __cdecl SE_SoundTestFinish(void); 0x00452C10 0x003D int32_t __cdecl SE_SoundTestExecute(void); 0x00452C50 0x0057 int32_t __cdecl SE_SoundTest(void); 0x00452CB0 0x003E int32_t __stdcall SE_PropSheetCallback(HWND hwndDlg, UINT uMsg, LPARAM lParam); 0x00452CF0 0x005D LRESULT __stdcall SE_NewPropSheetWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); 0x00452D50 0x02DE bool __cdecl SE_ShowSetupDialog(HWND hParent, bool isDefault); 0x00453030 0x0351 INT_PTR __stdcall SE_GraphicsDlgProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam); 0x004533F0 0x01DC void __cdecl SE_GraphicsDlgFullScreenModesUpdate(HWND hwndDlg); 0x004535E0 0x0017 void __cdecl SE_GraphicsAdapterSet(HWND hwndDlg, DISPLAY_ADAPTER_NODE *adapter); 0x00453600 0x0735 void __cdecl SE_GraphicsDlgUpdate(HWND hwndDlg); 0x00453D40 0x017C void __cdecl SE_GraphicsDlgInit(HWND hwndDlg); 0x00453EC0 0x0149 INT_PTR __stdcall SE_SoundDlgProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam); 0x00454050 0x000A void __cdecl SE_SoundAdapterSet(HWND hwndDlg, SOUND_ADAPTER_NODE *adapter); 0x00454060 0x011B void __cdecl SE_SoundDlgUpdate(HWND hwndDlg); 0x00454180 0x00BE void __cdecl SE_SoundDlgInit(HWND hwndDlg); 0x00454240 0x0106 INT_PTR __stdcall SE_ControlsDlgProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam); 0x00454350 0x000A void __cdecl SE_ControlsJoystickSet(HWND hwndDlg, JOYSTICK_NODE *joystick); 0x00454360 0x0068 void __cdecl SE_ControlsDlgUpdate(HWND hwndDlg); 0x004543D0 0x00BD void __cdecl SE_ControlsDlgInit(HWND hwndDlg); 0x00454490 0x008A INT_PTR __stdcall SE_OptionsDlgProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam); 0x00454520 0x0234 void __cdecl SE_OptionsDlgUpdate(HWND hwndDlg); 0x00454760 0x004B void __cdecl SE_OptionsStrCat(LPTSTR *dstString, bool isEnabled, bool *isNext, LPCTSTR srcString); 0x004547B0 0x00DC INT_PTR __stdcall SE_AdvancedDlgProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam); 0x004548B0 0x0093 void __cdecl SE_AdvancedDlgUpdate(HWND hwndDlg); 0x00454950 0x000E void __cdecl SE_AdvancedDlgInit(HWND hwndDlg); 0x00454960 0x0011 HWND __cdecl SE_FindSetupDialog(void); 0x00454980 0x02D0 BOOL __cdecl Shell_Main(void); 0x00454C50 0x0110 int16_t __cdecl TitleSequence(void); 0x00454D60 0x032D void __cdecl Lara_Cheat_CheckKeys(void); 0x004550C0 0x007D void __cdecl S_SaveSettings(void); 0x00455140 0x00DB void __cdecl S_LoadSettings(void); 0x00455220 0x0046 int32_t __cdecl S_Audio_Sample_OutPlay(int32_t sample_id, uint16_t volume, int32_t pitch, int32_t pan); 0x00455270 0x002A int32_t __cdecl S_Audio_Sample_CalculateSampleVolume(int32_t volume); 0x004552A0 0x0026 int32_t __cdecl S_Audio_Sample_CalculateSamplePan(int16_t pan); 0x004552D0 0x0046 int32_t __cdecl S_Audio_Sample_OutPlayLooped(int32_t track_id, uint16_t volume, int32_t pitch, int32_t pan); 0x00455320 0x0039 void __cdecl S_Audio_Sample_OutSetPanAndVolume(int32_t track_id, int32_t pan, uint16_t volume); 0x00455360 0x001C void __cdecl S_Audio_Sample_OutSetPitch(int32_t track_id, int32_t pitch); 0x00455380 0x000A void __cdecl Sound_SetMasterVolume(int32_t volume); 0x00455390 0x0017 void __cdecl S_Audio_Sample_OutCloseTrack(int32_t track_id); 0x004553B0 0x003C void __cdecl S_Audio_Sample_OutCloseAllTracks(void); 0x004553C0 0x001F BOOL __cdecl S_Audio_Sample_OutIsTrackPlaying(int32_t track_id); 0x004553E0 0x0077 bool __cdecl Music_Init(void); 0x00455460 0x0051 void __cdecl Music_Shutdown(void); 0x00455500 0x006F void __cdecl Music_Play(int16_t track_id, bool is_looped); 0x00455570 0x0039 void __cdecl Music_Stop(void); 0x004555B0 0x0084 bool __cdecl Music_PlaySynced(int32_t track_id); 0x00455640 0x0061 int32_t __cdecl Music_GetFrames(void); 0x004556B0 0x0092 void __cdecl Music_SetVolume(int32_t volume); 0x004557A0 0x0137 void __cdecl CopyBitmapPalette(RGB_888 *src_pal, BYTE *src_bitmap, int32_t bitmap_size, RGB_888 *dest_pal); 0x004558E0 0x00C8 BYTE __cdecl FindNearestPaletteEntry(RGB_888 *palette, int32_t red, int32_t green, int32_t blue, bool ignore_sys_palette); 0x004559B0 0x00AE void __cdecl SyncSurfacePalettes(void *src_data, int32_t width, int32_t height, int32_t src_pitch, RGB_888 *src_palette, void *dst_data, int32_t dst_pitch, RGB_888 *dst_palette, bool preserve_sys_palette); 0x00455A60 0x0087 int32_t __cdecl CreateTexturePalette(const RGB_888 *pal); 0x00455AF0 0x001C int32_t __cdecl GetFreePaletteIndex(void); 0x00455B10 0x0023 void __cdecl FreePalette(int32_t palette_idx); 0x00455B40 0x0012 void __cdecl SafeFreePalette(int32_t palette_idx); 0x00455B90 0x006A int32_t __cdecl CreateTexturePage(int32_t width, int32_t height, LPDIRECTDRAWPALETTE palette); 0x00455C00 0x001C int32_t __cdecl GetFreeTexturePageIndex(void); 0x00455C20 0x0098 bool __cdecl CreateTexturePageSurface(TEXPAGE_DESC *desc); 0x00455CC0 0x0174 bool __cdecl TexturePageInit(TEXPAGE_DESC *page); 0x00455E40 0x0025 LPDIRECT3DTEXTURE2 __cdecl Create3DTexture(LPDDS surface); 0x00455E70 0x0020 void __cdecl SafeFreeTexturePage(int32_t page_idx); 0x00455E90 0x0032 void __cdecl FreeTexturePage(int32_t page_idx); 0x00455ED0 0x003B void __cdecl TexturePageReleaseVidMemSurface(TEXPAGE_DESC *page); 0x00455F10 0x0026 void __cdecl FreeTexturePages(void); 0x00455F40 0x00A2 bool __cdecl LoadTexturePage(int32_t page_idx, bool reset); 0x00455FF0 0x0035 bool __cdecl ReloadTextures(bool reset); 0x00456030 0x003E HWR_TEXTURE_HANDLE __cdecl GetTexturePageHandle(int32_t page_idx); 0x00456070 0x00F5 int32_t __cdecl AddTexturePage8(int32_t width, int32_t height, const uint8_t *page_buf, int32_t pal_idx); 0x00456170 0x0196 int32_t __cdecl AddTexturePage16(int32_t width, int32_t height, const uint8_t *page_buf); 0x00456310 0x011A HRESULT __stdcall EnumTextureFormatsCallback(LPDDSDESC lpDdsd, LPVOID lpContext); 0x00456430 0x0025 HRESULT __cdecl EnumerateTextureFormats(void); 0x00456460 0x0030 void __cdecl CleanupTextures(void); 0x00456470 0x001F bool __cdecl InitTextures(void); 0x00456490 0x0040 void __cdecl UpdateTicks(void); 0x004564D0 0x0051 bool __cdecl TIME_Init(void); 0x00456530 0x0058 DWORD __cdecl Sync(void); 0x00456590 0x0036 LPVOID __cdecl UT_LoadResource(LPCTSTR lpName, LPCTSTR lpType); 0x004565D0 0x0060 void __cdecl UT_InitAccurateTimer(void); 0x00456630 0x004E double __cdecl UT_Microseconds(void); 0x00456680 0x006F BOOL __cdecl UT_CenterWindow(HWND hWnd); 0x004566F0 0x002C LPTSTR __cdecl UT_FindArg(LPCTSTR str); 0x00456720 0x0018 int32_t __cdecl UT_MessageBox(LPCTSTR lpText, HWND hWnd); 0x00456740 0x0042 int32_t __cdecl UT_ErrorBox(UINT uID, HWND hWnd); 0x00456790 0x0051 LPCTSTR __cdecl GuidBinaryToString(GUID *guid); 0x004567F0 0x00AA bool __cdecl GuidStringToBinary(LPCTSTR lpString, GUID *guid); 0x004568A0 0x0030 BOOL __cdecl OpenRegistryKey(LPCTSTR lpSubKey); 0x004568D0 0x000F bool __cdecl IsNewRegistryKeyCreated(void); 0x004568E0 0x000D LONG __cdecl CloseRegistryKey(void); 0x004568F0 0x001E LONG __cdecl SetRegistryDwordValue(LPCTSTR lpValueName, DWORD value); 0x00456910 0x002A LONG __cdecl SetRegistryBoolValue(LPCTSTR lpValueName, bool value); 0x00456940 0x0036 LONG __cdecl SetRegistryFloatValue(LPCTSTR lpValueName, double value); 0x00456980 0x0037 LONG __cdecl SetRegistryBinaryValue(LPCTSTR lpValueName, LPBYTE value, DWORD valueSize); 0x004569C0 0x004A LONG __cdecl SetRegistryStringValue(LPCTSTR lpValueName, LPCTSTR value, int32_t length); 0x00456A10 0x0013 LONG __cdecl DeleteRegistryValue(LPCTSTR lpValueName); 0x00456A30 0x005E bool __cdecl GetRegistryDwordValue(LPCTSTR lpValueName, DWORD *pValue, DWORD defaultValue); 0x00456A90 0x0076 bool __cdecl GetRegistryBoolValue(LPCTSTR lpValueName, bool *pValue, bool defaultValue); 0x00456B10 0x005C bool __cdecl GetRegistryFloatValue(LPCTSTR lpValueName, double *value, double defaultValue); 0x00456B70 0x0071 bool __cdecl GetRegistryBinaryValue(LPCTSTR lpValueName, LPBYTE value, DWORD valueSize, LPBYTE defaultValue); 0x00456BF0 0x0095 bool __cdecl GetRegistryStringValue(LPCTSTR lpValueName, LPTSTR value, DWORD maxSize, LPCTSTR defaultValue); 0x00456C90 0x0091 bool __cdecl GetRegistryGuidValue(LPCTSTR lpValueName, GUID *value, GUID *defaultValue); 0x00456D30 0x0037 void __thiscall SE_ReleaseBitmapResource(BITMAP_RESOURCE *bmpRsrc); 0x00456D70 0x00C4 void __thiscall SE_LoadBitmapResource(BITMAP_RESOURCE *bmpRsrc, LPCTSTR lpName); 0x00456E40 0x0064 void __thiscall SE_DrawBitmap(BITMAP_RESOURCE *bmpRsrc, HDC hdc, int32_t x, int32_t y); 0x00456EB0 0x001C void __thiscall SE_UpdateBitmapPalette(BITMAP_RESOURCE *bmpRsrc, HWND hWnd, HWND hSender); 0x00456ED0 0x0057 void __thiscall SE_ChangeBitmapPalette(BITMAP_RESOURCE *bmpRsrc, HWND hWnd); 0x00456F30 0x0061 bool __cdecl SE_RegisterSetupWindowClass(void); 0x00456FA0 0x023A LRESULT __stdcall SE_SetupWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); 0x004571E0 0x0026 void __cdecl SE_PassMessageToImage(HWND hWnd, UINT uMsg, WPARAM wParam); 0x00457210 0x006E void __cdecl UT_MemBlt(BYTE *dstBuf, DWORD dstX, DWORD dstY, DWORD width, DWORD height, DWORD dstPitch, BYTE *srcBuf, DWORD srcX, DWORD srcY, DWORD srcPitch); 0x00457280 0x001E void __cdecl Matrix_Push(void); 0x0045729E 0x0033 void __cdecl Matrix_PushUnit(void); 0x004572D4 0x0061 void __fastcall Output_FlatA(int32_t y0, int32_t y1, uint8_t color_idx); // actually, __watcall, which is esoteric and rarely supported 0x00457335 0x013A void __fastcall Output_TransA(int32_t y0, int32_t y1, uint8_t depth_q); // actually, __watcall, which is esoteric and rarely supported 0x0045746F 0x0160 void __fastcall Output_GourA(int32_t y0, int32_t y1, uint8_t color_idx); // actually, __watcall, which is esoteric and rarely supported 0x004575CF 0x02FD void __fastcall Output_GTMapA(int32_t y0, int32_t y1, uint8_t *tex_page); // actually, __watcall, which is esoteric and rarely supported 0x004578CC 0x0341 void __fastcall Output_WGTMapA(int32_t y0, int32_t y1, uint8_t *tex_page); // actually, __watcall, which is esoteric and rarely supported 0x00457C10 0x0048 int32_t __fastcall Math_Atan(int32_t x, int32_t y); 0x00457C58 0x0006 int32_t __fastcall Math_Cos(int16_t angle); 0x00457C5E 0x001B int32_t __fastcall Math_Sin(int16_t angle); 0x00457C79 0x001A int32_t __fastcall Math_SinImpl(int16_t angle); 0x00457C93 0x002C uint32_t __fastcall Math_Sqrt(uint32_t n); 0x00458D00 0x0006 int __cdecl Player_PlayFrame(LPVOID, LPVOID, LPVOID, DWORD, LPCRECT, DWORD, DWORD, DWORD); 0x00458D06 0x0006 int __cdecl Movie_GetTotalFrames(LPVOID); 0x00458D0C 0x0006 int __cdecl Movie_GetCurrentFrame(LPVOID); 0x00458D12 0x0006 int __cdecl Player_StartTimer(LPVOID); 0x00458D18 0x0006 int __cdecl Player_InitMoviePlayback(LPVOID, LPVOID, LPVOID); 0x00458D1E 0x0006 int __cdecl Movie_SetSyncAdjust(LPVOID, LPVOID, DWORD); 0x00458D24 0x0006 int __cdecl Player_InitSound(LPVOID, DWORD, DWORD, BOOL, DWORD, DWORD, DWORD, DWORD, DWORD); 0x00458D2A 0x0006 int __cdecl Movie_GetSoundChannels(LPVOID); 0x00458D30 0x0006 int __cdecl Movie_GetSoundRate(LPVOID); 0x00458D36 0x0006 int __cdecl Movie_GetSoundPrecision(LPVOID); 0x00458D3C 0x0006 int __cdecl Player_GetDSErrorCode(void); 0x00458D42 0x0006 int __cdecl Player_InitSoundSystem(HWND); 0x00458D48 0x0006 int __cdecl Player_BlankScreen(DWORD, DWORD, DWORD, DWORD); 0x00458D4E 0x0006 int __cdecl Player_InitPlaybackMode(HWND, LPVOID, DWORD, DWORD); 0x00458D54 0x0006 int __cdecl Player_InitVideo(LPVOID, LPVOID, DWORD, DWORD, DWORD, DWORD, DWORD, DWORD, DWORD, DWORD, DWORD, DWORD, DWORD); 0x00458D5A 0x0006 int __cdecl Movie_GetXSize(LPVOID); 0x00458D60 0x0006 int __cdecl Movie_GetYSize(LPVOID); 0x00458D66 0x0006 int __cdecl Movie_GetFormat(LPVOID); 0x00458D6C 0x0006 int __cdecl Player_InitMovie(LPVOID, DWORD, DWORD, LPCTSTR, DWORD); 0x00458D72 0x0006 int __cdecl Player_PassInDirectDrawObject(LPDIRECTDRAW3); 0x00458D78 0x0006 int __cdecl Player_ReturnPlaybackMode(BOOL); 0x00458D7E 0x0006 int __cdecl Player_ShutDownSoundSystem(void); 0x00458D84 0x0006 int __cdecl Player_ShutDownMovie(LPVOID); 0x00458D8A 0x0006 int __cdecl Player_ShutDownVideo(LPVOID); 0x00458D90 0x0006 int __cdecl Player_ShutDownSound(LPVOID); 0x00458D96 0x0006 int __cdecl Player_StopTimer(LPVOID); # VARIABLES # Offset Declaration 0x00464060 uint32_t g_PerspectiveDistance = 0x3000000; 0x00464068 void (*__cdecl g_PolyDrawRoutines[9])(const int16_t *obj_ptr); 0x0046408C float g_RhwFactor = 335544320.0f; // 10*2**25 0x004640B0 int32_t g_CineTrackID = 1; 0x004640B8 int32_t g_CineTickRate = 0x8000; // 0x8000 = PHD_ONE/TICKS_PER_FRAME 0x004640BC int16_t g_CD_TrackID = -1; 0x004640C4 int32_t g_FlipEffect = -1; 0x004641F0 uint32_t g_AssaultBestTime = -1; 0x004641F8 void (*__cdecl g_EffectRoutines[32])(ITEM *item); 0x00464310 int16_t g_CineTargetAngle = 0x4000; // PHD_90 0x004644E0 int32_t g_OverlayStatus = 1; 0x004654E0 int16_t g_Inv_MainObjectsCount = 8; 0x00465604 int16_t g_Inv_OptionObjectsCount = 4; 0x00465618 BOOL g_GymInvOpenEnabled = TRUE; 0x00465A50 int16_t g_Inv_Chosen = -1; 0x00465A54 INVENTORY_MODE g_Inv_Mode = INV_TITLE_MODE; 0x00465A5C int16_t g_OptionSoundVolume = 165; // NOTE: value should be 10 0x00465A60 int16_t g_OptionMusicVolume = 255; // NOTE: should be 10 0x00465AD4 int32_t g_JumpPermitted = 1; 0x00465AD8 int16_t g_LaraOldSlideAngle = 1; 0x00465CD0 void (*__cdecl g_LaraControlRoutines[71])(ITEM *item, COLL_INFO *coll); 0x00465DF0 void (*__cdecl g_ExtraControlRoutines[11])(ITEM *item, COLL_INFO *coll); 0x00465E20 void (*__cdecl g_LaraCollisionRoutines[71])(ITEM *item, COLL_INFO *coll); 0x00466290 int8_t g_TextSpacing[80]; 0x004662E0 int8_t g_TextASCIIMap[]; 0x00466400 int32_t g_BGND_PaletteIndex = -1; 0x00466480 double g_GameSizer = 1.0; 0x00466488 double g_GameSizerCopy = 1.0; 0x00466490 int32_t g_FadeValue = 0x100000; 0x00466494 int32_t g_FadeLimit = 0x100000; 0x00466498 int32_t g_FadeAdder = 0x8000; 0x004664E8 const char *g_ErrorMessages[43]; 0x00466BB0 int32_t g_RandControl; 0x00466BB4 int32_t g_RandDraw; 0x00466F70 CONTROL_LAYOUT g_Layout[2]; 0x00467DD0 const int32_t g_AtanBaseTable[8]; 0x00467DF0 const int16_t g_AtanAngleTable[0x800]; 0x00468DF4 const int16_t g_SinTable[0x402]; 0x0046C300 int32_t g_MidSort = 0; 0x0046C304 float g_ViewportAspectRatio = 0.0f; 0x0046C308 int32_t g_XGenY1; 0x0046C30C int32_t g_XGenY2; 0x0046C310 GOURAUD_ENTRY g_GouraudTable[256]; 0x0046E310 int32_t g_PhdWinTop; 0x0046E318 PHD_SPRITE g_PhdSprites[512]; 0x00470318 int32_t g_LsAdder; 0x0047031C float g_FltWinBottom; 0x00470320 float g_FltResZBuf; 0x00470324 float g_FltResZ; 0x00470328 void (*__cdecl g_Output_InsertTransQuad)(int32_t x, int32_t y, int32_t width, int32_t height, int32_t z); 0x0047032C int32_t g_PhdWinHeight; 0x00470330 int32_t g_PhdWinCenterX; 0x00470334 int32_t g_PhdWinCenterY; 0x00470338 int16_t g_LsYaw; 0x0047033C void (*__cdecl g_Output_InsertTrans8)(const PHD_VBUF *vbuf, int16_t shade); 0x00470340 float g_FltWinTop; 0x00470348 SORT_ITEM g_SortBuffer[4000]; 0x00478048 float g_FltWinLeft; 0x0047804C int16_t g_PhdWinMinY; 0x00478058 int32_t g_PhdFarZ; 0x0047805C float g_FltRhwOPersp; 0x00478060 int32_t g_PhdWinBottom; 0x00478064 int32_t g_PhdPersp; 0x00478068 int32_t g_PhdWinLeft; 0x0047806C void (*__cdecl g_Output_InsertFlatRect)(int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t z, uint8_t color_idx); 0x00478070 int16_t g_Info3DBuffer[120000]; 0x004B29F0 int32_t g_PhdWinMaxX; 0x004B29F4 int32_t g_PhdNearZ; 0x004B29F8 float g_FltResZORhw; 0x004B29FC float g_FltFarZ; 0x004B2A00 float g_FltWinCenterX; 0x004B2A04 float g_FltWinCenterY; 0x004B2A08 int32_t g_PhdScreenHeight; 0x004B2A0C uint8_t *g_PrintSurfacePtr; 0x004B2A10 int16_t g_PhdWinMinX; 0x004B2A14 float g_FltPerspONearZ; 0x004B2A18 float g_FltRhwONearZ; 0x004B2A1C int32_t g_PhdWinMaxY; 0x004B2A20 void (*__cdecl g_Output_InsertSprite)(int32_t z, int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t sprite_idx, int16_t shade); 0x004B2A24 float g_FltNearZ; 0x004B2A28 MATRIX *g_MatrixPtr; 0x004B2A2C const int16_t *(*__cdecl g_Output_DrawObjectGT3)(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type); 0x004B2A30 const int16_t *(*__cdecl g_Output_DrawObjectGT4)(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type); 0x004B2A38 int32_t g_RandomTable[32]; 0x004B2AB8 float g_FltPersp; 0x004B2AC0 MATRIX g_W2VMatrix; 0x004B2AF0 int16_t *g_Info3DPtr; 0x004B2AF4 int32_t g_PhdWinWidth; 0x004B2AF8 void (*__cdecl g_Output_InsertLine)(int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t z, uint8_t color_idx); 0x004B2B00 PHD_TEXTURE g_TextureInfo[0x800]; // MAX_TEXTURES 0x004BCB00 int32_t g_PhdViewDistance; 0x004BCB04 int16_t g_LsPitch; 0x004BCB08 const int16_t *(*__cdecl g_Output_DrawObjectG4)(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type); 0x004BCB10 int16_t g_ShadesTable[32]; 0x004BCB50 const int16_t *(*__cdecl g_Output_DrawObjectG3)(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type); 0x004BCB58 MATRIX g_MatrixStack[]; 0x004BD2D8 DEPTHQ_ENTRY g_DepthQTable[32]; 0x004BF3D8 int32_t g_PhdScreenWidth; 0x004BF3DC int32_t g_LsDivider; 0x004BF3E0 PHD_VBUF g_PhdVBuf[1500]; 0x004CAF60 void *g_XBuffer; 0x004D6AE0 uint8_t *g_TexturePageBuffer8[32]; // MAX_TEXTURE_PAGES 0x004D6B60 float g_FltWinRight; 0x004D6B68 XYZ_32 g_LsVectorView; 0x004D6B78 float g_WibbleTable[32]; 0x004D6BF8 int32_t g_PhdWinRight; 0x004D6BFC int32_t g_SurfaceCount; 0x004D6C00 SORT_ITEM *g_Sort3DPtr; 0x004D6C0C int32_t g_WibbleOffset; 0x004D6C10 int32_t g_IsWibbleEffect; 0x004D6C14 int32_t g_IsWaterEffect; 0x004D6CD8 VERTEX_INFO g_VBuffer[20]; 0x004D6F78 int8_t g_IsShadeEffect; 0x004D6F80 D3DTLVERTEX g_VBufferD3D[32]; 0x004D7380 PALETTEENTRY g_GamePalette16[256]; 0x004D7780 int32_t g_CineFrameCurrent; 0x004D778C int32_t g_IsChunkyCamera; 0x004D7794 int32_t g_NoInputCounter; 0x004D7798 BOOL g_IsResetFlag; 0x004D779C int32_t g_FlipTimer; 0x004D77A0 int32_t g_LOSNumRooms = 0; 0x004D77A4 BOOL g_StopInventory; 0x004D77AC BOOL g_IsDemoLevelType; 0x004D77B0 BOOL g_IsDemoLoaded; 0x004D77C0 int32_t g_BoundStart; 0x004D77C4 int32_t g_BoundEnd; 0x004D77E0 int32_t g_IsAssaultTimerDisplay; 0x004D77E4 BOOL g_IsAssaultTimerActive; 0x004D77E8 BOOL g_IsMonkAngry; 0x004D791C int32_t g_OldGameTimer; 0x004D7920 BOOL g_FlashState; 0x004D7924 int32_t g_FlashCounter; 0x004D7928 int32_t g_OldHitPoints; 0x004D792C TEXTSTRING *g_AmmoTextInfo; 0x004D7930 TEXTSTRING *g_DisplayModeTextInfo; 0x004D7934 DWORD g_DisplayModeInfoTimer; 0x004D7938 UINT16 g_Inv_MainCurrent; 0x004D793C UINT16 g_Inv_KeyObjectsCount; 0x004D7940 UINT16 g_Inv_KeysCurrent; 0x004D7944 UINT16 g_Inv_OptionCurrent; 0x004D7954 TEXTSTRING* g_Inv_RingText; 0x004D795C TEXTSTRING* g_Inv_UpArrow1; 0x004D7960 TEXTSTRING* g_Inv_UpArrow2; 0x004D7964 TEXTSTRING* g_Inv_DownArrow1; 0x004D7968 TEXTSTRING* g_Inv_DownArrow2; 0x004D796C uint32_t g_InputDB; 0x004D7978 uint16_t g_Inv_IsActive; 0x004D79A0 BOOL g_Inv_DemoMode; 0x004D79B4 BOOL g_Inv_IsOptionsDelay; 0x004D79B8 int32_t g_Inv_OptionsDelayCounter; 0x004D79BC uint16_t g_SoundOptionLine; 0x004D79C0 REQUEST_INFO g_StatsRequester; 0x004D7BD8 ASSAULT_STATS g_Assault; 0x004D7C38 int32_t g_LevelItemCount; 0x004D7C3C int32_t g_HealthBarTimer; 0x004D7C80 int32_t g_SoundTrackIds[128]; 0x004D7EBC LPDIRECT3DDEVICE2 g_D3DDev; 0x004D7EE4 bool g_IsGameWindowCreated; 0x004D7EE8 bool g_IsGameWindowUpdating; 0x004D7EEC bool g_IsDDrawGameWindowShow; 0x004D7EF0 int32_t g_MinWindowClientWidth; 0x004D7ED0 int32_t g_MinWindowClientHeight; 0x004D8388 int32_t g_MinWindowWidth; 0x004D838C int32_t g_MinWindowHeight; 0x004D7EF4 bool g_IsGameWindowShow; 0x004D7EF8 bool g_IsMinWindowSizeSet; 0x004D7EFC int32_t g_MaxWindowClientWidth; 0x004D7F00 int32_t g_GameWindowWidth; 0x004D7F04 bool g_IsMinMaxInfoSpecial; 0x004D7F08 bool g_IsGameFullScreen; 0x004D7F0C bool g_IsGameWindowMaximized; 0x004D7F10 HWND g_GameWindowHandle; 0x004D7F14 int32_t g_GameWindowHeight; 0x004D7F18 DISPLAY_ADAPTER_NODE* g_PrimaryDisplayAdapter; 0x004D7F20 DISPLAY_ADAPTER g_CurrentDisplayAdapter; 0x004D8338 uint32_t g_LockedBufferCount; 0x004D833C int32_t g_GameWindowPositionX; 0x004D8340 int32_t g_GameWindowPositionY; 0x004D8348 DISPLAY_ADAPTER_LIST g_DisplayAdapterList; 0x004D8354 int32_t g_MaxWindowClientHeight; 0x004D8358 bool g_IsMessageLoopClosed; 0x004D835C int32_t g_MaxWindowWidth; 0x004D7EDC int32_t g_MaxWindowHeight; 0x004D8360 bool g_IsMaxWindowSizeSet; 0x004D8364 uint32_t g_AppResultCode; 0x004D8368 int32_t g_FullScreenWidth; 0x004D836C int32_t g_FullScreenHeight; 0x004D8370 int32_t g_FullScreenBPP; 0x004D8374 int32_t g_FullScreenVGA; 0x004D8378 uint8_t g_IsGameToExit; 0x004D8568 int32_t g_ScreenSizer; 0x004D856C int32_t g_IsVidSizeLock; 0x004D8570 DWORD g_SampleFreqs[256]; 0x004D8970 SOUND_ADAPTER_LIST g_SoundAdapterList; 0x004D8980 LPDIRECTSOUNDBUFFER g_SampleBuffers[256]; 0x004D8D80 uint8_t g_IsSoundEnabled; 0x004D8D84 LPDIRECTSOUND g_DSound; 0x004D8D88 int32_t g_ChannelSamples[32]; 0x004D8E08 LPDIRECTSOUNDBUFFER g_ChannelBuffers[32]; 0x004D8E8C SOUND_ADAPTER g_CurrentSoundAdapter; 0x004D8EAC SOUND_ADAPTER_NODE *g_PrimarySoundAdapter; 0x004D8EB0 LPDDS g_RenderBufferSurface; 0x004D92B8 LPDDS g_ThirdBufferSurface; 0x004D92BC LPDDS g_PictureBufferSurface; 0x004D92C0 LPDDS g_ZBufferSurface; 0x004D92C8 LPDDS g_PrimaryBufferSurface; 0x004D9338 int32_t g_GameVid_Width; 0x004D933C int32_t g_GameVid_Height; 0x004D9340 int32_t g_GameVid_BPP; 0x004D934C int32_t g_UVAdd; 0x004D9351 int8_t g_GameVid_IsWindowedVGA; 0x004D9EAC int32_t g_IsFMVPlaying; 0x004D9EC0 int32_t g_CurrentLevel; 0x004D9EC4 int32_t g_LevelComplete; 0x004D9ED8 D3DTLVERTEX g_HWR_VertexBuffer[0x2000]; // MAX_VERTICES 0x00519EE0 HWR_TEXTURE_HANDLE g_HWR_PageHandles[32]; 0x00519F60 D3DTLVERTEX *g_HWR_VertexPtr; 0x0051A0CC char *g_GameBuf_MemBase; 0x0051A0D0 BOOL g_ConflictLayout[14]; // INPUT_ROLE_NUMBER_OF 0x0051A108 uint8_t g_DIKeys[256]; 0x0051A208 int32_t g_Input; 0x0051A20C int8_t g_IsVidModeLock; 0x0051A210 int32_t g_JoyKeys; 0x0051A214 int32_t g_JoyXPos; 0x0051A218 int32_t g_JoyYPos; 0x0051A220 int32_t g_MediPackCooldown; 0x0051A224 int8_t g_IsF3Pressed; 0x0051A228 int8_t g_IsF4Pressed; 0x0051A22C int8_t g_IsF7Pressed; 0x0051A230 int8_t g_IsF8Pressed; 0x0051A234 int8_t g_IsF11Pressed; 0x0051A238 HINSTANCE g_GameModule; 0x0051A23C char *g_CmdLine; 0x0051A240 int32_t g_ScreenshotCounter; 0x0051B918 RECT g_PhdWinRect; 0x0051B928 int32_t g_HiRes; 0x0051B930 RGB_888 g_GamePalette8[256]; 0x0051BCC0 APP_SETTINGS g_SavedAppSettings; 0x0051BD20 char g_ErrorMessage[128]; 0x0051BDA8 int32_t g_MasterVolume; 0x0051BDAC MCIDEVICEID g_MciDeviceID; 0x0051BDB0 int32_t g_CD_LoopTrack; 0x0051C820 TEXTSTRING g_TextstringTable[64]; // MAX_TEXTSTRINGS 0x0051D6A0 int16_t g_TextstringCount = 0; 0x0051D6C0 char g_TextstringBuffers[64][64]; 0x0051E6C4 int32_t g_SoundIsActive; 0x0051E9E0 SAVEGAME_INFO g_SaveGame; 0x005206E0 LARA_INFO g_Lara; 0x005207BC ITEM *g_LaraItem; 0x005207C0 EFFECT *g_Effects; 0x005207C4 int16_t g_NextEffectFree; 0x005207C6 int16_t g_NextItemFree; 0x005207C8 int16_t g_NextItemActive; 0x005207CA int16_t g_NextEffectActive; 0x005207CC int16_t g_PrevItemActive; 0x00521CA0 PICKUP_INFO g_Pickups[12]; 0x00521DE0 GAME_FLOW g_GameFlow; 0x00521FDC int32_t g_SoundEffectCount; 0x00522000 OBJECT g_Objects[265]; 0x005252B0 int16_t **g_Meshes; 0x005252C0 MATRIX g_IMMatrixStack[256]; 0x005258F0 int32_t g_IMFrac; 0x005258F4 ANIM *g_Anims; 0x00525BE8 int32_t *g_AnimBones; 0x00526180 int32_t g_RoomCount; 0x00526184 int32_t g_IMRate; 0x00526188 MATRIX *g_IMMatrixPtr; 0x0052618C ROOM *g_Rooms; 0x00526240 int32_t g_FlipStatus; 0x00526288 int16_t *g_TriggerIndex; 0x005262A0 int32_t g_LOSRooms[20]; 0x005262F0 ITEM *g_Items; 0x005262F6 int16_t g_NumCineFrames; 0x005262F8 CINE_FRAME *g_CineData = NULL; 0x00526300 PHD_3DPOS g_CinePos; 0x00526314 int16_t g_CineFrameIdx; 0x00526320 CAMERA_INFO g_Camera; 0x005263CC BOX_INFO *g_Boxes; 0x004D855C LPDIRECTINPUT g_DInput; 0x004D8560 LPDIRECTINPUTDEVICE IDID_SysKeyboard; 0x0051BDA0 BOOL g_IsTitleLoaded; 0x004D7980 int32_t g_Inv_ExtraData[8]; 0x004D8394 int32_t g_MessageLoopCounter; 0x004D8384 bool g_IsGameWindowMinimized; 0x004D8390 bool g_IsGameWindowActive; 0x004D837C int32_t g_GameWindowY; 0x004D7EE0 LPDIRECTDRAW3 g_DDraw; 0x004D8380 int32_t g_GameWindowX; 0x00463150 GUID g_IID_IDirectDrawSurface3; 0x00463170 GUID g_IID_IDirect3DTexture2; 0x004640A0 BITE g_CrowBite; 0x00464090 BITE g_BirdBite; 0x005263C0 int16_t *g_FlyZone[2]; 0x005263A0 int16_t *g_GroundZone[][2]; 0x005263C8 uint16_t *g_Overlap; 0x005206C0 CREATURE *g_BaddieSlots; 0x00526312 int16_t g_CineLevelID; 0x005252B8 int32_t g_DrawRoomsCount; 0x00525B20 int16_t g_DrawRoomsArray[100]; 0x00525BEC int32_t g_DynamicLightCount; 0x004D7784 int32_t g_CineTickCount; 0x004D7788 int32_t g_OriginalRoom; 0x00465518 INVENTORY_ITEM *g_Inv_MainList[]; 0x00465608 INVENTORY_ITEM *g_Inv_OptionList[]; 0x004655A8 INVENTORY_ITEM *g_Inv_KeysList[]; 0x004644F8 int32_t g_Inv_NFrames; 0x00525C00 STATIC_INFO g_StaticObjects[50]; // MAX_STATIC_OBJECTS 0x00521FE0 OBJECT_VECTOR *g_SoundEffects; 0x0051E6E0 int16_t g_SampleLUT[]; 0x0051E9C4 SAMPLE_INFO *g_SampleInfos; 0x004D7C78 SOUND_SLOT g_SoundSlots[32]; 0x004D9328 RECT g_GameVid_Rect; 0x004D9358 LPDDS g_BackBufferSurface; 0x004D9350 bool g_GameVid_IsVga; 0x004D9344 int32_t g_GameVid_BufWidth; 0x004D9348 int32_t g_GameVid_BufHeight; 0x004D8EB4 LPDIRECTDRAWCLIPPER g_DDrawClipper; 0x004D8EB8 PALETTEENTRY g_WinVid_Palette[256]; 0x004D92C4 LPDIRECTDRAWPALETTE g_DDrawPalette; 0x004D7EC4 LPDIRECT3DVIEWPORT2 g_D3DView; 0x004D9355 bool g_NeedToReloadTextures; 0x004D9352 bool g_GameVid_IsFullscreenVGA; 0x004D9353 bool g_IsWindowedVGA; 0x004D9354 bool g_Is16bitTextures; 0x004D9318 RECT g_GameVid_BufRect; 0x00466BE4 int16_t g_DumpX; 0x00466BE6 int16_t g_DumpY; 0x00466BE8 int16_t g_DumpWidth; 0x00466BEA int16_t g_DumpHeight; 0x0051C1B8 TEXTURE_FORMAT g_TextureFormat; 0x004D92E8 COLOR_BIT_MASKS g_ColorBitMasks; 0x0051BC30 bool g_WinVidNeedToResetBuffers; 0x004D7E88 bool g_BGND_PictureIsReady; 0x004D7E90 int32_t g_BGND_TexturePageIndexes[5]; 0x004D7EA8 HWR_TEXTURE_HANDLE g_BGND_PageHandles[5]; 0x004D7EC0 LPDIRECT3D2 g_D3D; 0x004D7EC8 LPDIRECT3DMATERIAL2 g_D3DMaterial; 0x004D7ED4 LPDIRECTDRAW g_DDrawInterface; 0x00466448 const char g_GameClassName[]; 0x00466468 const char g_GameWindowName[]; 0x004D7ED8 bool g_IsGameWindowChanging; 0x00519F68 D3DRENDERSTATETYPE g_AlphaBlendEnabler; 0x00519ED8 D3DTEXTUREHANDLE g_CurrentTexSource; 0x00519F6C bool g_ColorKeyState; 0x0051C20C bool g_TexturesAlphaChannel; 0x00519F64 bool g_ZEnableState; 0x00519F70 bool g_ZWriteEnableState; 0x00466BDC int32_t g_PaletteIndex; 0x00519F78 int32_t g_HWR_TexturePageIndexes[32]; // MAX_TEXTURE_PAGES 0x004D7790 int32_t g_HeightType; 0x004D9D94 int16_t *g_FloorData; 0x00525B08 int16_t *g_AnimCommands; 0x0052617C ANIM_CHANGE *g_AnimChanges; 0x00525B04 ANIM_RANGE *g_AnimRanges; 0x00526260 int32_t g_FlipMaps[10]; // MAX_FLIP_MAPS 0x005252B4 int32_t g_Outside; 0x00526198 int32_t g_OutsideRight; 0x00526178 int32_t g_OutsideLeft; 0x005261AC int32_t g_OutsideTop; 0x00525B00 int32_t g_OutsideBottom; 0x00525900 int32_t g_BoundRooms[128]; // MAX_BOUND_ROOMS 0x005258C0 PORTAL_VBUF g_DoorVBuf[4]; 0x00464180 int32_t g_BoxLines[12][2]; 0x00526190 BOOL g_CameraUnderwater; 0x005263D0 int32_t g_BoxCount; 0x004D7C50 int32_t g_SlotsUsed; 0x004D9360 int32_t g_TexturePageCount; 0x004D9D90 int16_t *g_MeshBase; 0x004D9E98 int32_t g_TextureInfoCount; 0x004D93F0 uint8_t g_LabTextureUVFlag[2048]; // MAX_TEXTURES 0x005251B0 FRAME_INFO *g_AnimFrames; 0x0051BC38 int32_t g_IsWet; 0x0051B308 RGB_888 g_WaterPalette[256]; 0x004BF2D8 uint8_t g_DepthQIndex[256]; 0x004D7C74 int32_t g_NumCameras; 0x0051B92C int16_t *g_AnimTextureRanges; 0x005262F4 int16_t g_CineLoaded; 0x005261B0 uint32_t *g_DemoPtr; 0x005261B4 int32_t g_DemoCount; 0x0051E6C0 int32_t g_NumSampleInfos; 0x004D9BF4 int32_t g_LevelFilePalettesOffset; 0x004D9BF8 int32_t g_LevelFileTexPagesOffset; 0x004D9E9C int32_t g_LevelFileDepthQOffset; 0x004D9D98 char g_LevelFileName[256]; 0x005261C0 uint16_t g_MusicTrackFlags[64]; 0x00465AE0 WEAPON_INFO g_Weapons[]; 0x005206A8 int16_t g_FinalBossActive; 0x005206BA int16_t g_FinalLevelCount; 0x005206BC int16_t g_FinalBossCount; 0x005206B0 int16_t g_FinalBossItem[5]; 0x004D77B4 int32_t g_DemoLevel; 0x004D77B8 int32_t g_DemoLevel2; 0x00464A90 INVENTORY_ITEM g_Inv_Item_Stopwatch; 0x00464AE0 INVENTORY_ITEM g_Inv_Item_Pistols; 0x00464B30 INVENTORY_ITEM g_Inv_Item_Flare; 0x00464B80 INVENTORY_ITEM g_Inv_Item_Shotgun; 0x00464BD0 INVENTORY_ITEM g_Inv_Item_Magnums; 0x00464C20 INVENTORY_ITEM g_Inv_Item_Uzis; 0x00464C70 INVENTORY_ITEM g_Inv_Item_Harpoon; 0x00464CC0 INVENTORY_ITEM g_Inv_Item_M16; 0x00464D10 INVENTORY_ITEM g_Inv_Item_Grenade; 0x00464D60 INVENTORY_ITEM g_Inv_Item_PistolAmmo; 0x00464DB0 INVENTORY_ITEM g_Inv_Item_ShotgunAmmo; 0x00464E00 INVENTORY_ITEM g_Inv_Item_MagnumAmmo; 0x00464E50 INVENTORY_ITEM g_Inv_Item_UziAmmo; 0x00464EA0 INVENTORY_ITEM g_Inv_Item_HarpoonAmmo; 0x00464EF0 INVENTORY_ITEM g_Inv_Item_M16Ammo; 0x00464F40 INVENTORY_ITEM g_Inv_Item_GrenadeAmmo; 0x00464F90 INVENTORY_ITEM g_Inv_Item_SmallMedi; 0x00464FE0 INVENTORY_ITEM g_Inv_Item_LargeMedi; 0x00465030 INVENTORY_ITEM g_Inv_Item_Pickup1; 0x00465080 INVENTORY_ITEM g_Inv_Item_Pickup2; 0x004650D0 INVENTORY_ITEM g_Inv_Item_Puzzle1; 0x00465120 INVENTORY_ITEM g_Inv_Item_Puzzle2; 0x00465170 INVENTORY_ITEM g_Inv_Item_Puzzle3; 0x004651C0 INVENTORY_ITEM g_Inv_Item_Puzzle4; 0x00465210 INVENTORY_ITEM g_Inv_Item_Key1; 0x00465260 INVENTORY_ITEM g_Inv_Item_Key2; 0x004652B0 INVENTORY_ITEM g_Inv_Item_Key3; 0x00465300 INVENTORY_ITEM g_Inv_Item_Key4; 0x00465350 INVENTORY_ITEM g_Inv_Item_Passport; 0x004653A0 INVENTORY_ITEM g_Inv_Item_Graphics; 0x004653F0 INVENTORY_ITEM g_Inv_Item_Sound; 0x00465440 INVENTORY_ITEM g_Inv_Item_Controls; 0x00465490 INVENTORY_ITEM g_Inv_Item_Photo; 0x00465620 REQUEST_INFO g_LoadGameRequester; 0x00465838 REQUEST_INFO g_SaveGameRequester; 0x004642E8 int16_t g_GF_NumSecrets = 3; 0x004642F0 int16_t g_GF_MusicTracks[16]; 0x004D77EC int32_t g_GF_ScriptVersion; 0x004D77F0 int32_t g_GF_LaraStartAnim; 0x004D77F4 int16_t g_GF_SunsetEnabled; 0x004D77F8 int16_t g_GF_DeadlyWater; 0x004D77FC int16_t g_GF_NoFloor; 0x004D7800 int16_t g_GF_RemoveWeapons; 0x004D7804 int16_t g_GF_RemoveAmmo; 0x004D7808 char g_GF_Kill2Complete; 0x004D780C int8_t g_GF_StartGame; 0x004D7818 char g_GF_Description[256]; 0x004D9C00 int16_t g_GF_LevelOffsets[200]; 0x00521DC4 char **g_GF_CutsceneFileNames; 0x00521E68 char *g_GF_FMVFilenamesBuf; 0x00521E6C char *g_GF_Key1StringsBuf; 0x00521E70 int16_t *g_GF_FrontendSequence; 0x00521E74 char **g_GF_Key2Strings; 0x00521E78 char *g_GF_CutsceneFileNamesBuf; 0x00521E7C char *g_GF_Key4StringsBuf; 0x00521E80 int16_t *g_GF_SequenceBuf; 0x00521E84 char *g_GF_Key2StringsBuf; 0x00521E8C char *g_GF_PicFilenamesBuf; 0x00521E90 char **g_GF_Key4Strings; 0x00521DC0 char **g_GF_Puzzle1Strings; 0x00521E98 char **g_GF_Puzzle2Strings; 0x00521EC0 char **g_GF_Puzzle3Strings; 0x00521E60 char **g_GF_Puzzle4Strings; 0x00521E94 char **g_GF_Pickup1Strings; 0x00521F44 char **g_GF_Pickup2Strings; 0x00521EA8 char *g_GF_Puzzle1StringsBuf; 0x00521F40 char *g_GF_Puzzle2StringsBuf; 0x00521F98 char *g_GF_Puzzle3StringsBuf; 0x00521F90 char *g_GF_Puzzle4StringsBuf; 0x00521E64 char *g_GF_Pickup1StringsBuf; 0x00521E88 char *g_GF_Pickup2StringsBuf; 0x00521E9C char *g_GF_LevelFileNamesBuf; 0x00521EA0 char **g_GF_PicFilenames; 0x00521EA4 char **g_GF_Key1Strings; 0x00521EAC char *g_GF_LevelNamesBuf; 0x00521EB0 char **g_GF_GameStrings; 0x00521EB4 char *g_GF_PCStringsBuf; 0x00521EB8 char *g_GF_GameStringsBuf; 0x00521EBC char **g_GF_Key3Strings; 0x00521EC4 char **g_GF_LevelNames; 0x00521EE0 int16_t *g_GF_ScriptTable[24]; // MAX_LEVELS 0x00521F48 char **g_GF_TitleFileNames; 0x00521F4C char *g_GF_TitleFileNamesBuf; 0x00521F50 char **g_GF_PCStrings; 0x00521F54 char **g_GF_LevelFileNames; 0x00521F60 int16_t g_GF_ValidDemos[24]; // MAX_DEMO_FILES 0x00521F94 char **g_GF_FMVFilenames; 0x00521F9C char *g_GF_Key3StringsBuf; 0x00521FA0 char g_GF_SecretInvItems[27]; // GF_ADD_INV_NUMBER_OF 0x00521FC0 char g_GF_Add2InvItems[27]; // GF_ADD_INV_NUMBER_OF 0x004D9ECC int32_t g_GameMode; // GAMEMODE 0x004D7970 int32_t g_OldInputDB; 0x004D7948 TEXTSTRING *g_Inv_ItemText[3]; // IT_NUMBER_OF 0x004D7950 TEXTSTRING *g_Inv_LevelText; 0x004D7958 TEXTSTRING *g_Inv_TagText; 0x004D9EBC int32_t g_SavedGames; 0x0051A2CC TEXTSTRING *g_PasswordText1; 0x0051A2D0 int32_t g_PassportMode; 0x0051A2D8 TEXTSTRING *g_DetailText[5]; 0x0051A2F0 TEXTSTRING *g_SoundText[4]; 0x0051A290 TEXTSTRING *m_ControlsTextA[14]; // INPUT_ROLE_NUMBER_OF 0x0051A258 TEXTSTRING *g_ControlsTextB[14]; // INPUT_ROLE_NUMBER_OF 0x0051A300 TEXTSTRING *g_ControlsText[2]; 0x004D7C30 int32_t m_ShowStatsTextMode; 0x005207E0 char g_ValidLevelStrings1[]; 0x00521720 char g_ValidLevelStrings2[]; 0x004D7C34 int32_t m_ShowEndStatsTextMode; 0x004D7C2C int32_t m_ShowGymStatsTextMode; 0x00520D00 uint32_t g_RequesterFlags1[24]; // MAX_REQUESTER_ITEMS 0x00520CA0 uint32_t g_RequesterFlags2[24]; // MAX_REQUESTER_ITEMS 0x00521C40 uint32_t g_SaveGameReqFlags1[24]; // MAX_REQUESTER_ITEMS 0x00521BE0 uint32_t g_SaveGameReqFlags2[24]; // MAX_REQUESTER_ITEMS 0x004D9EC8 int32_t g_SaveCounter; 0x00466B80 int16_t g_SavedLevels[24]; // MAX_LEVELS 0x004654E8 int16_t g_Inv_MainQtys[]; 0x00465578 int16_t g_Inv_KeysQtys[]; 0x0046773C int32_t g_DetailLevel; 0x0051A250 int32_t g_LayoutPage; 0x0051A24C int32_t g_KeySelector; 0x0051A248 int32_t g_KeyCursor; 0x00466FA8 const char *g_KeyNames[]; 0x00464500 const uint16_t g_Requester_BackgroundGour1[]; 0x00464520 const uint16_t g_Requester_BackgroundGour2[]; 0x00464538 const uint16_t g_Requester_MainGour1[]; 0x00464558 const uint16_t g_Requester_MainGour2[]; 0x00464590 const uint16_t g_Requester_SelectionGour2[]; 0x004645A8 const uint16_t g_Requester_UnselectionGour1[]; 0x005216E0 uint16_t g_InvColors[17]; // INV_COLOR_NUMBER_OF 0x00464150 BITE g_DragonMouth; 0x00466230 BITE g_SkidooLeftGun; 0x00466240 BITE g_SkidooRightGun; 0x00464130 BITE g_DogBite; 0x00464140 BITE g_TigerBite; 0x00465F40 int16_t g_MovableBlockBounds[]; 0x00465F58 int16_t g_ZiplineHandleBounds[]; 0x00465FF0 int16_t g_PickupBounds[]; 0x00466018 int16_t g_GongBounds[]; 0x00466030 int16_t g_PickupBoundsUW[]; 0x00466058 int16_t g_SwitchBounds[]; 0x004660A0 int16_t g_SwitchBoundsUW[]; 0x004660C8 int16_t g_KeyholeBounds[]; 0x004660F0 int16_t g_PuzzleHoleBounds[]; 0x00465F70 XYZ_32 g_ZiplineHandlePosition; 0x00466008 XYZ_32 g_PickupPosition; 0x00466048 XYZ_32 g_PickupPositionUW; 0x00466070 XYZ_32 g_SmallSwitchPosition; 0x00466080 XYZ_32 g_PushSwitchPosition; 0x00466090 XYZ_32 g_AirlockPosition; 0x004660B8 XYZ_32 g_SwitchUWPosition; 0x004660E0 XYZ_32 g_KeyholePosition; 0x00466108 XYZ_32 g_PuzzleHolePosition; 0x004D7C58 XYZ_32 g_InteractPosition; 0x004D7C68 XYZ_32 g_DetonatorPosition; 0x004D9EB0 void *g_MovieContext; 0x004D9EB4 void *g_FmvContext; 0x004D9EB8 void *g_FmvSoundContext; 0x0051A000 size_t g_GameBuf_MemCap; 0x0051A004 char *g_GameBuf_MemPtr; 0x0051A008 size_t g_GameBuf_MemUsed; 0x0051A00C size_t g_GameBuf_MemFree; 0x0051B608 RGB_888 g_PicturePalette[256]; 0x004D7E7C int32_t g_DetonateAllMines; 0x005206A4 int32_t g_SavegameBufPos; 0x0051E9C8 char *g_SavegameBufPtr; 0x0051C210 LPDIRECTDRAWPALETTE g_TexturePalettes[16]; // MAX_PALETTES 0x0051BDB8 TEXPAGE_DESC g_TexturePages[32]; // MAX_TEXTURE_PAGES 0x00466280 BITE g_BigSpiderBite; 0x0051C20D uint8_t g_TexturesHaveCompatibleMasks; 0x00467768 SHADOW_INFO g_ShadowInfo; 0x004663C0 BITE g_YetiLBite; 0x004663D0 BITE g_YetiRBite; 0x004663E0 BITE g_BirdGuardianBite; 0x0051BDA4 int32_t g_CheatMode; 0x0051BD1C bool g_CheatFlare; 0x0051BD18 int16_t g_CheatAngle; 0x0051BD10 int32_t g_CheatTurn; 0x0051A308 ROOM_LIGHT_TABLE g_RoomLightTables[32]; // WIBBLE_SIZE 0x005251C0 LIGHT g_DynamicLights[10]; // MAX_DYNAMIC_LIGHTS 0x0051B908 int32_t g_RoomLightShades[4]; 0x00526194 int32_t g_SunsetTimer; 0x00521CD0 int32_t g_IsFirstHair; 0x00521CE0 XYZ_32 g_HairVelocity[7]; // HAIR_SEGMENTS + 1 0x00521D40 HAIR_SEGMENT g_HairSegments[7]; // HAIR_SEGMENTS + 1 0x004D7918 int32_t g_HairWind; 0x004641E0 BITE g_BigEelBite; 0x00466210 BITE g_BarracudaBite; 0x00466220 BITE g_SharkBite; 0x00466118 BITE g_MouseBite; 0x00465F80 BITE g_Cultist1Gun; 0x004642C8 BITE g_Cultist2LeftHand; 0x004642D8 BITE g_Cultist2RightHand; 0x00465F90 BITE g_Cultist3LeftGun; 0x00465FA0 BITE g_Cultist3RightGun; 0x00465FD0 BITE g_Bandit1Gun; 0x00465FE0 BITE g_Bandit2Gun; 0x00465FB0 BITE g_Worker1Gun; 0x00465FC0 BITE g_Worker2Gun; 0x00464288 BITE g_Worker3Hit; 0x00464278 BITE g_MonkHit; 0x004642A8 BITE g_XianSpearmanRightSpear; 0x00464298 BITE g_XianSpearmanLeftSpear; 0x004642B8 BITE g_XianKnightSword; 0x00466360 BITE g_TeethTrapTeeth1A; 0x00466370 BITE g_TeethTrapTeeth1B; 0x00466380 BITE g_TeethTrapTeeth2A; 0x00466390 BITE g_TeethTrapTeeth2B; 0x004663A0 BITE g_TeethTrapTeeth3A; 0x004663B0 BITE g_TeethTrapTeeth3B; ================================================ FILE: docs/tr3/INSTALLING.md ================================================ # Windows (installer) ## Installing (simplified) **The TR3 installer is not yet ready, but we'll eventually provide it.** > [!NOTE] > When downloading TRX, you might see a warning from Windows Defender, your browser, or another security tool. Modern antivirus systems use AI‑based heuristics – they flag anything uncommon or unsigned as suspicious, even if it's perfectly safe. TRX can trigger these alerts because: > > - It isn't signed with a costly commercial certificate. > - It's a niche, community‑built project, so not widely recognized. > - It's a custom build, not from the Microsoft Store. > > Don't worry: TRX is open‑source, and you can inspect the code yourself on [GitHub](https://github.com/LostArtefacts/TRX/). # Windows / Linux ## Installing (manual) 1. Download the TRX zip file. 2. Extract the zip file into a directory of your choice. Make sure you choose to overwrite existing directories and files. 3. If installing for the first time – put your original game files into the target directory. Unfortunately, due to legal reasons, we cannot offer an easy packaging of The Lost Artifact expansion pack. ## Verifying the installation If you install everything correctly, your game directory should look more or less like this (click to expand):
.
├── audio
│   └── cdaudio.wad
├── cfg
│   ├── presets
│   │   ├── tr1-pc.json5
│   │   ├── tr1-ps1.json5
│   │   ├── tr2-pc.json5
│   │   ├── tr2-ps1.json5
│   │   ├── tr3-pc.json5
│   │   └── tr3-ps1.json5
│   ├── tr3
│   │   ├── gameflow.json5
│   │   ├── strings-de.json5
│   │   ├── strings-it.json5
│   │   ├── strings-pl.json5
│   │   └── strings.json5
│   ├── tr3-la
│   │   ├── gameflow.json5
│   │   ├── strings-de.json5
│   │   ├── strings-it.json5
│   │   ├── strings-pl.json5
│   │   └── strings.json5
│   ├── tr3-level
│   │   ├── gameflow.json5
│   │   ├── strings-it.json5
│   │   ├── strings-pl.json5
│   │   └── strings.json5
│   ├── base_strings-de.json5
│   ├── base_strings-en-gb.json5
│   ├── base_strings-fr.json5
│   ├── base_strings-gd.json5
│   ├── base_strings-it.json5
│   ├── base_strings-pl.json5
│   ├── base_strings-ru.json5
│   ├── base_strings.json5
│   ├── catalog_item_actions.csv
│   ├── catalog_lara_anims.csv
│   ├── catalog_lara_states.csv
│   ├── catalog_music.csv
│   ├── catalog_objects.csv
│   ├── catalog_samples.csv
│   ├── inv_ring.json5
│   ├── outfits.json5
│   ├── poses.json5
│   ├── TR3X.json5*
│   ├── ui.json5
│   └── weapons.json5
├── cuts
│   ├── cut1.tr2
│   ├── cut2.tr2
│   ├── cut3.tr2
│   ├── cut4.tr2
│   ├── cut5.tr2
│   ├── cut6.tr2
│   ├── cut7.tr2
│   ├── cut8.tr2
│   ├── cut9.tr2
│   ├── cut11.tr2
│   └── cut12.tr2
├── data
│   ├── images
│   │   ├── 3x2
│   │   │   ├── antarc.webp
│   │   │   ├── credit01.webp
│   │   │   ├── credit02.webp
│   │   │   ├── credit03.webp
│   │   │   ├── credit04.webp
│   │   │   ├── credit05.webp
│   │   │   ├── credit06.webp
│   │   │   ├── credit07.webp
│   │   │   ├── credit08.webp
│   │   │   ├── credit09.webp
│   │   │   ├── house.webp
│   │   │   ├── india.webp
│   │   │   ├── legal_eu.webp
│   │   │   ├── legal_us.webp
│   │   │   ├── london.webp
│   │   │   ├── nevada.webp
│   │   │   ├── southpac.webp
│   │   │   ├── theend2.webp
│   │   │   ├── title_eu.webp
│   │   │   └── title_us.webp
│   │   ├── 4x3
│   │   │   ├── antarc.webp
│   │   │   ├── credit01.webp
│   │   │   ├── credit02.webp
│   │   │   ├── credit03.webp
│   │   │   ├── credit04.webp
│   │   │   ├── credit05.webp
│   │   │   ├── credit06.webp
│   │   │   ├── credit07.webp
│   │   │   ├── credit08.webp
│   │   │   ├── credit09.webp
│   │   │   ├── house.webp
│   │   │   ├── india.webp
│   │   │   ├── legal_eu.webp
│   │   │   ├── legal_us.webp
│   │   │   ├── london.webp
│   │   │   ├── nevada.webp
│   │   │   ├── southpac.webp
│   │   │   ├── theend2.webp
│   │   │   ├── title_eu.webp
│   │   │   └── title_us.webp
│   │   ├── og
│   │   │   ├── antarc.webp
│   │   │   ├── credit01.webp
│   │   │   ├── credit02.webp
│   │   │   ├── credit03.webp
│   │   │   ├── credit04.webp
│   │   │   ├── credit05.webp
│   │   │   ├── credit06.webp
│   │   │   ├── credit07.webp
│   │   │   ├── credit08.webp
│   │   │   ├── credit09.webp
│   │   │   ├── house.webp
│   │   │   ├── india.webp
│   │   │   ├── legal_eu.webp
│   │   │   ├── legal_us.webp
│   │   │   ├── london.webp
│   │   │   ├── nevada.webp
│   │   │   ├── nevadafff.webp
│   │   │   ├── southpac.webp
│   │   │   ├── theend2.webp
│   │   │   ├── theend.webp
│   │   │   ├── title_eu.webp
│   │   │   └── title_us.webp
│   │   ├── antarc.webp
│   │   ├── credit01.webp
│   │   ├── credit02.webp
│   │   ├── credit03.webp
│   │   ├── credit04.webp
│   │   ├── credit05.webp
│   │   ├── credit06.webp
│   │   ├── credit07.webp
│   │   ├── credit08.webp
│   │   ├── credit09.webp
│   │   ├── house.webp
│   │   ├── india.webp
│   │   ├── legal_eu.webp
│   │   ├── legal_us.webp
│   │   ├── london.webp
│   │   ├── nevada.webp
│   │   ├── southpac.webp
│   │   ├── theend2.webp
│   │   ├── title_eu.webp
│   │   └── title_us.webp
│   ├── injections
│   │   ├── aldwych_fd.bin
│   │   ├── aldwych_pickup_meshes.bin
│   │   ├── aldwych_textures.bin
│   │   ├── antarc_airlock.bin
│   │   ├── antarc_door134_frames.bin
│   │   ├── antarc_sky.bin
│   │   ├── area51_sky.bin
│   │   ├── area51_textures.bin
│   │   ├── cavern_door131_frames.bin
│   │   ├── cavern_pickup_meshes.bin
│   │   ├── cavern_sky.bin
│   │   ├── city_textures.bin
│   │   ├── cliff_door132_frames.bin
│   │   ├── coastal_airlock.bin
│   │   ├── coastal_animating_bounds.bin
│   │   ├── coastal_sky.bin
│   │   ├── compound_animating_bounds.bin
│   │   ├── compound_cine.bin
│   │   ├── compound_textures.bin
│   │   ├── crash_pickup_meshes.bin
│   │   ├── crash_sky.bin
│   │   ├── cut1_setup.bin
│   │   ├── cut2_setup.bin
│   │   ├── cut3_setup.bin
│   │   ├── cut3_shell.bin
│   │   ├── cut4_setup.bin
│   │   ├── cut5_setup.bin
│   │   ├── cut5_textures.bin
│   │   ├── cut6_setup.bin
│   │   ├── cut7_setup.bin
│   │   ├── cut8_setup.bin
│   │   ├── cut9_setup.bin
│   │   ├── cut11_setup.bin
│   │   ├── cut12_setup.bin
│   │   ├── drill_collision.bin
│   │   ├── flamethrower_sfx.bin
│   │   ├── font.bin
│   │   ├── ganges_door131_frames.bin
│   │   ├── globe_model.bin
│   │   ├── gym_sky.bin
│   │   ├── india_sky.bin
│   │   ├── lara_animations.bin
│   │   ├── lara_extra.bin
│   │   ├── lara_guns.bin
│   │   ├── lara_gym_guns.bin
│   │   ├── lara_outfits.bin
│   │   ├── london_sky.bin
│   │   ├── luds_diver_animation.bin
│   │   ├── luds_textures.bin
│   │   ├── menu_artefacts.bin
│   │   ├── mines_textures.bin
│   │   ├── misc_sprites.bin
│   │   ├── nevada_door132_frames.bin
│   │   ├── nevada_sky.bin
│   │   ├── pda_model.bin
│   │   ├── pickup_aid.bin
│   │   ├── puna_pickup_meshes.bin
│   │   ├── rapids_sky.bin
│   │   ├── reunion_flames.bin
│   │   ├── scotland_sky.bin
│   │   ├── stpaul_animating_bounds.bin
│   │   ├── stpaul_textures.bin
│   │   ├── tinnos_cameras.bin
│   │   ├── tinnos_flames.bin
│   │   ├── undersea_animating_bounds.bin
│   │   ├── undersea_train.bin
│   │   ├── willsden_heli.bin
│   │   └── zoo_train.bin
│   ├── scripts
│   │   ├── area51.lua
│   │   ├── compound.lua
│   │   ├── crash.lua
│   │   ├── cut8.lua
│   │   ├── jungle.lua
│   │   ├── mines.lua
│   │   ├── tower.lua
│   │   └── zoo.lua
│   ├── antarc.tr2
│   ├── area51.tr2
│   ├── chamber.tr2
│   ├── chunnel.tr2
│   ├── city.tr2
│   ├── compound.tr2
│   ├── crash.tr2
│   ├── house.tr2
│   ├── jungle.tr2
│   ├── main.sfx
│   ├── main_la.sfx
│   ├── mines.tr2
│   ├── nevada.tr2
│   ├── office.tr2
│   ├── quadchas.tr2
│   ├── rapids.tr2
│   ├── roofs.tr2
│   ├── scotland.tr2
│   ├── sewer.tr2
│   ├── shore.tr2
│   ├── slinc.tr2
│   ├── stpaul.tr2
│   ├── temple.tr2
│   ├── title.tr2
│   ├── title_la.tr2
│   ├── tombpc.dat
│   ├── tonyboss.tr2
│   ├── tower.tr2
│   ├── triboss.tr2
│   ├── undersea.tr2
│   ├── willsden.tr2
│   └── zoo.tr2
├── fmv
│   ├── crsh_eng.rpl
│   ├── endgame.rpl
│   ├── intr_eng.rpl
│   ├── logo.rpl
│   └── sail_eng.rpl
├── shaders
│   ├── 2d.glsl
│   ├── billboard.glsl
│   ├── common.glsl
│   ├── fbo.glsl
│   ├── lights.glsl
│   ├── meshes.glsl
│   ├── meshes_tr3.glsl
│   ├── meshes_tr12.glsl
│   └── ui.glsl
└── TRX.exe
*\* Will not be present until the game has been launched.* ## Playing the game - To play the game, run `TRX.exe`. - To play the Lost Artifact expansion pack, run `TRX.exe --gold`. # macOS ## Installing 1. Download the latest TRX for TR3 installer image (e.g `TRX-0.1-Mac-tr3.dmg`). Mount the image and drag TR3X to the Applications folder. 2. Run TR3X from the Applications folder. This will show you an error dialog about missing game data files. This is expected at this point, as you have not copied them in yet. However, it's important to run the app first to allow macOS to verify the app bundle's signature. 3. Find TR3X in your Applications folder. Right-click it and click "Show Package Contents". 4. Copy your Tomb Raider 3 game data files into `Contents/Resources`. (See the Windows / Linux instructions for retrieving game data from e.g. GOG.) In case you see a popup "TR3X is damaged" when you run the game, run `xattr -cr /Applications/TR3X.app`. ## Verifying the installation If you install everything correctly, your game directory should look more or less like this (click to expand):
.
└── Contents
    ├── Resources
    │   ├── audio
    │   │   └── cdaudio.wad
    │   ├── cfg
    │   │   ├── presets
    │   │   │   ├── tr1-pc.json5
    │   │   │   ├── tr1-ps1.json5
    │   │   │   ├── tr2-pc.json5
    │   │   │   ├── tr2-ps1.json5
    │   │   │   ├── tr3-pc.json5
    │   │   │   └── tr3-ps1.json5
    │   │   ├── tr3
    │   │   │   ├── gameflow.json5
    │   │   │   ├── strings-de.json5
    │   │   │   ├── strings-it.json5
    │   │   │   ├── strings-pl.json5
    │   │   │   └── strings.json5
    │   │   ├── tr3-la
    │   │   │   ├── gameflow.json5
    │   │   │   ├── strings-de.json5
    │   │   │   ├── strings-it.json5
    │   │   │   ├── strings-pl.json5
    │   │   │   └── strings.json5
    │   │   ├── tr3-level
    │   │   │   ├── gameflow.json5
    │   │   │   ├── strings-it.json5
    │   │   │   ├── strings-pl.json5
    │   │   │   └── strings.json5
    │   │   ├── base_strings-de.json5
    │   │   ├── base_strings-en-gb.json5
    │   │   ├── base_strings-fr.json5
    │   │   ├── base_strings-gd.json5
    │   │   ├── base_strings-it.json5
    │   │   ├── base_strings-pl.json5
    │   │   ├── base_strings-ru.json5
    │   │   ├── base_strings.json5
    │   │   ├── catalog_item_actions.csv
    │   │   ├── catalog_lara_anims.csv
    │   │   ├── catalog_lara_states.csv
    │   │   ├── catalog_music.csv
    │   │   ├── catalog_objects.csv
    │   │   ├── catalog_samples.csv
    │   │   ├── inv_ring.json5
    │   │   ├── outfits.json5
    │   │   ├── poses.json5
    │   │   ├── ui.json5
    │   │   └── weapons.json5
    │   ├── cuts
    │   │   ├── cut1.tr2
    │   │   ├── cut2.tr2
    │   │   ├── cut3.tr2
    │   │   ├── cut4.tr2
    │   │   ├── cut5.tr2
    │   │   ├── cut6.tr2
    │   │   ├── cut7.tr2
    │   │   ├── cut8.tr2
    │   │   ├── cut9.tr2
    │   │   ├── cut11.tr2
    │   │   └── cut12.tr2
    │   ├── data
    │   │   ├── images
    │   │   │   ├── 3x2
    │   │   │   │   ├── antarc.webp
    │   │   │   │   ├── credit01.webp
    │   │   │   │   ├── credit02.webp
    │   │   │   │   ├── credit03.webp
    │   │   │   │   ├── credit04.webp
    │   │   │   │   ├── credit05.webp
    │   │   │   │   ├── credit06.webp
    │   │   │   │   ├── credit07.webp
    │   │   │   │   ├── credit08.webp
    │   │   │   │   ├── credit09.webp
    │   │   │   │   ├── house.webp
    │   │   │   │   ├── india.webp
    │   │   │   │   ├── legal_eu.webp
    │   │   │   │   ├── legal_us.webp
    │   │   │   │   ├── london.webp
    │   │   │   │   ├── nevada.webp
    │   │   │   │   ├── southpac.webp
    │   │   │   │   ├── theend2.webp
    │   │   │   │   ├── title_eu.webp
    │   │   │   │   └── title_us.webp
    │   │   │   ├── 4x3
    │   │   │   │   ├── antarc.webp
    │   │   │   │   ├── credit01.webp
    │   │   │   │   ├── credit02.webp
    │   │   │   │   ├── credit03.webp
    │   │   │   │   ├── credit04.webp
    │   │   │   │   ├── credit05.webp
    │   │   │   │   ├── credit06.webp
    │   │   │   │   ├── credit07.webp
    │   │   │   │   ├── credit08.webp
    │   │   │   │   ├── credit09.webp
    │   │   │   │   ├── house.webp
    │   │   │   │   ├── india.webp
    │   │   │   │   ├── legal_eu.webp
    │   │   │   │   ├── legal_us.webp
    │   │   │   │   ├── london.webp
    │   │   │   │   ├── nevada.webp
    │   │   │   │   ├── southpac.webp
    │   │   │   │   ├── theend2.webp
    │   │   │   │   ├── title_eu.webp
    │   │   │   │   └── title_us.webp
    │   │   │   ├── og
    │   │   │   │   ├── antarc.webp
    │   │   │   │   ├── credit01.webp
    │   │   │   │   ├── credit02.webp
    │   │   │   │   ├── credit03.webp
    │   │   │   │   ├── credit04.webp
    │   │   │   │   ├── credit05.webp
    │   │   │   │   ├── credit06.webp
    │   │   │   │   ├── credit07.webp
    │   │   │   │   ├── credit08.webp
    │   │   │   │   ├── credit09.webp
    │   │   │   │   ├── house.webp
    │   │   │   │   ├── india.webp
    │   │   │   │   ├── legal_eu.webp
    │   │   │   │   ├── legal_us.webp
    │   │   │   │   ├── london.webp
    │   │   │   │   ├── nevada.webp
    │   │   │   │   ├── nevadafff.webp
    │   │   │   │   ├── southpac.webp
    │   │   │   │   ├── theend2.webp
    │   │   │   │   ├── theend.webp
    │   │   │   │   ├── title_eu.webp
    │   │   │   │   └── title_us.webp
    │   │   │   ├── antarc.webp
    │   │   │   ├── credit01.webp
    │   │   │   ├── credit02.webp
    │   │   │   ├── credit03.webp
    │   │   │   ├── credit04.webp
    │   │   │   ├── credit05.webp
    │   │   │   ├── credit06.webp
    │   │   │   ├── credit07.webp
    │   │   │   ├── credit08.webp
    │   │   │   ├── credit09.webp
    │   │   │   ├── house.webp
    │   │   │   ├── india.webp
    │   │   │   ├── legal_eu.webp
    │   │   │   ├── legal_us.webp
    │   │   │   ├── london.webp
    │   │   │   ├── nevada.webp
    │   │   │   ├── southpac.webp
    │   │   │   ├── theend2.webp
    │   │   │   ├── title_eu.webp
    │   │   │   └── title_us.webp
    │   │   ├── injections
    │   │   │   ├── aldwych_fd.bin
    │   │   │   ├── aldwych_pickup_meshes.bin
    │   │   │   ├── aldwych_textures.bin
    │   │   │   ├── antarc_airlock.bin
    │   │   │   ├── antarc_door134_frames.bin
    │   │   │   ├── antarc_sky.bin
    │   │   │   ├── area51_sky.bin
    │   │   │   ├── area51_textures.bin
    │   │   │   ├── cavern_door131_frames.bin
    │   │   │   ├── cavern_pickup_meshes.bin
    │   │   │   ├── cavern_sky.bin
    │   │   │   ├── city_textures.bin
    │   │   │   ├── cliff_door132_frames.bin
    │   │   │   ├── coastal_airlock.bin
    │   │   │   ├── coastal_animating_bounds.bin
    │   │   │   ├── coastal_sky.bin
    │   │   │   ├── compound_animating_bounds.bin
    │   │   │   ├── compound_cine.bin
    │   │   │   ├── compound_textures.bin
    │   │   │   ├── crash_pickup_meshes.bin
    │   │   │   ├── crash_sky.bin
    │   │   │   ├── cut1_setup.bin
    │   │   │   ├── cut2_setup.bin
    │   │   │   ├── cut3_setup.bin
    │   │   │   ├── cut3_shell.bin
    │   │   │   ├── cut4_setup.bin
    │   │   │   ├── cut5_setup.bin
    │   │   │   ├── cut5_textures.bin
    │   │   │   ├── cut6_setup.bin
    │   │   │   ├── cut7_setup.bin
    │   │   │   ├── cut8_setup.bin
    │   │   │   ├── cut9_setup.bin
    │   │   │   ├── cut11_setup.bin
    │   │   │   ├── cut12_setup.bin
    │   │   │   ├── drill_collision.bin
    │   │   │   ├── flamethrower_sfx.bin
    │   │   │   ├── font.bin
    │   │   │   ├── ganges_door131_frames.bin
    │   │   │   ├── globe_model.bin
    │   │   │   ├── gym_sky.bin
    │   │   │   ├── india_sky.bin
    │   │   │   ├── lara_animations.bin
    │   │   │   ├── lara_extra.bin
    │   │   │   ├── lara_guns.bin
    │   │   │   ├── lara_gym_guns.bin
    │   │   │   ├── lara_outfits.bin
    │   │   │   ├── london_sky.bin
    │   │   │   ├── luds_diver_animation.bin
    │   │   │   ├── luds_textures.bin
    │   │   │   ├── menu_artefacts.bin
    │   │   │   ├── mines_textures.bin
    │   │   │   ├── misc_sprites.bin
    │   │   │   ├── nevada_door132_frames.bin
    │   │   │   ├── nevada_sky.bin
    │   │   │   ├── pda_model.bin
    │   │   │   ├── pickup_aid.bin
    │   │   │   ├── puna_pickup_meshes.bin
    │   │   │   ├── rapids_sky.bin
    │   │   │   ├── reunion_flames.bin
    │   │   │   ├── scotland_sky.bin
    │   │   │   ├── stpaul_animating_bounds.bin
    │   │   │   ├── stpaul_textures.bin
    │   │   │   ├── tinnos_cameras.bin
    │   │   │   ├── tinnos_flames.bin
    │   │   │   ├── undersea_animating_bounds.bin
    │   │   │   ├── undersea_train.bin
    │   │   │   ├── willsden_heli.bin
    │   │   │   └── zoo_train.bin
    │   │   ├── scripts
    │   │   │   ├── area51.lua
    │   │   │   ├── compound.lua
    │   │   │   ├── crash.lua
    │   │   │   ├── cut8.lua
    │   │   │   ├── jungle.lua
    │   │   │   ├── mines.lua
    │   │   │   ├── tower.lua
    │   │   │   └── zoo.lua
    │   │   ├── antarc.tr2
    │   │   ├── area51.tr2
    │   │   ├── chamber.tr2
    │   │   ├── chunnel.tr2
    │   │   ├── city.tr2
    │   │   ├── compound.tr2
    │   │   ├── crash.tr2
    │   │   ├── house.tr2
    │   │   ├── jungle.tr2
    │   │   ├── main.sfx
    │   │   ├── main_la.sfx
    │   │   ├── mines.tr2
    │   │   ├── nevada.tr2
    │   │   ├── office.tr2
    │   │   ├── quadchas.tr2
    │   │   ├── rapids.tr2
    │   │   ├── roofs.tr2
    │   │   ├── scotland.tr2
    │   │   ├── sewer.tr2
    │   │   ├── shore.tr2
    │   │   ├── slinc.tr2
    │   │   ├── stpaul.tr2
    │   │   ├── temple.tr2
    │   │   ├── title.tr2
    │   │   ├── title_la.tr2
    │   │   ├── tombpc.dat
    │   │   ├── tonyboss.tr2
    │   │   ├── tower.tr2
    │   │   ├── triboss.tr2
    │   │   ├── undersea.tr2
    │   │   ├── willsden.tr2
    │   │   └── zoo.tr2
    │   ├── fmv
    │   │   ├── crsh_eng.rpl
    │   │   ├── endgame.rpl
    │   │   ├── intr_eng.rpl
    │   │   ├── logo.rpl
    │   │   └── sail_eng.rpl
    │   ├── shaders
    │   │   ├── 2d.glsl
    │   │   ├── billboard.glsl
    │   │   ├── common.glsl
    │   │   ├── fbo.glsl
    │   │   ├── lights.glsl
    │   │   ├── meshes.glsl
    │   │   ├── meshes_tr3.glsl
    │   │   ├── meshes_tr12.glsl
    │   │   └── ui.glsl
    │   └── icon.icns
    ├── _CodeSignature
    ├── Frameworks
    ├── info.plist
    └── MacOS
*\* Will not be present until the game has been launched.* ================================================ FILE: docs/trx/CATALOGS.md ================================================ --- title: Catalogs order: 7 --- # Catalogs TombEditor normally lets builders manage WADs and object slots through its catalogs. The snag is that the game engine itself still references certain slots directly in code. That means if a builder repurposes one of those hardcoded slots for an animation command, they might get an ugly surprise when another in‑game object tries to use that same slot behind the scenes. That's where TRX catalogs come in. Before TRX 1.0, all object IDs and music tracks were hardcoded for TR1 and TR2. Now, builders can freely re‑assign those IDs however they want. In the future, the original lists will allow extensions by including objects from other games! Under the hood, each entity is identified by its stable name string. The catalog maps numeric slots to these name keys, so when the engine references an entity, it can grab the correct sample or resource tied to that slot. Catalogs are just comma‑separated value (CSV) files you can edit with any text editor, including Notepad or Excel. They live in the `cfg/` folder and must be present for the game to function properly. TRX catalogs only include data that's directly referenced by the game's code. Entries used *only* in animation commands or other editor‑controlled behaviors aren't included, since those can already be managed freely within the level editor. ## Working example Let's say, as a builder, you wish to use the bird monster from TR2 in a TR1 level. Here are the necessary steps to set this up. 1. Make a note of the OG TR2 slot for the bird monster e.g. from WadTool or trview. In this case, it's `46`. 2. Look-up the object name for TR2. To do this, download a copy of TRX with TR2 assets and open the `cfg/catalog_objects.csv` file. For slot 46, the name is `O_BIRD_GUARDIAN`. 3. Choose a slot in your TR1 level that you wish to use for this object. You can pick a new slot, or replace an existing one. Add the object to the slot in WadTool and keep a note of the number you chose. 4. In your TR1 level folder, open `cfg/catalog_objects.csv`. Add an entry on a new line with the slot number and the object name, ensuring you have a comma in between. For example, if you chose slot `247`, the line would be:

`247, O_BIRD_GUARDIAN`

If you chose to replace an existing object, instead of adding a new line, locate that slot in the CSV file and just replace the object name. That's it! You can now place a bird monster in your level in TombEditor. You can also proceed to check which sounds the bird monster uses in its animations in WadTool and set up appropriate ones for TR1. ================================================ FILE: docs/trx/COMMANDS.md ================================================ --- title: Commands order: 2 --- # Commands TRX introduces a developer console, by default accessible with the / key. This key can be rebound in the controls dialog to anything of your choice. Note that where / is used in command documentation, you should interpret that as whichever key you have bound, and not include it as part of the command itself. ## Gameplay commands - `/help` `/help {command}` Shows a list of the available commands or a detailed help message for the chosen one. Even Lara needs a lifeline! - `/pos` Reveals Lara's exact coordinates in the universe. Knowledge is power! - `/tp {room_number}` `/tp room {room_number}` `/tp r{room_number}` `/tp item {item_number}` `/tp i{item_number}` `/tp precise {x} {y} {z}` `/tp {x} {y} {z}` `/tp {object}` `/tp enemy` `/tp pickup` Instant travel! Teleports Lara to: - a random spot within the specified room; - an item's position by item number; - the specified X,Y,Z coordinates in grid units; - the specified world-space coordinates with `precise` (no `1024` scaling); - the next pickup in round-robin order with `pickup`; - the next hostile creature in round-robin order with `enemy`; - the nearest object of a specific type. - legacy: `/tp {room_number}` is still accepted. - `/hp` `/hp {health}` Displays or sets Lara's health. Tougher trials await! - `/heal` Tough day, Lara? Heals our girl back to full health. - `/give {item_name}` `/give {num} {item_name}` `/give all` `/give guns` or `/guns` `/give moreguns` or `/moreguns` `/give keys` or `/keys` Gives Lara an item. Try `/give guns` to arm her to the teeth, and `/give keys` to get her all important puzzle items. Ain't nobody got time for searching! - `/secret` `/secret take` `/secret take {num}` `/secret give` `/secret give {num}` Uncovers Lara's secret stash: list discovered secrets, pilfer one or all with `take`, or gift one or all back with `give`. - `/kill` `/kill all` `/kill {enemy_type}` Tired of all of those pesky creatures and goons trying to spoil your adventure? Instantly dispose of the nearest one, or kill them all at once. - `/fly` `/fly on` `/fly off` Turns on the fly cheat. Why even walk? Levitate like a legend. - `/immune` `/immune on` `/immune off` Turns on immunity, making Lara impervious to harm. Perfect for when you'd rather explore every nook than tiptoe past traps. - `/restless` `/restless on` `/restless off` Turns on infinite sprint. Lara's always been a speedster, but with this, even cheetahs are asking her for running tips! - `/teatime` Calls your loyal butler to whatever ends of the world you're exploring right now. Effective immediately. - `/spawn {object}` Spawn an object of your choice. Not guaranteed to behave, but good for testing and oddly therapeutic for goofing off. - `/trigger {item_num}` `/trigger {item_name}` `/trigger {object}` Force-triggers one or more items by their item ID, item name, or object name. Great for testing switches, traps, and scripted events. - `/untrigger {item_num}` `/untrigger {item_name}` `/untrigger {object}` Reverses `/trigger` for one or more items. ## Configuration commands - `/set {option}` `/set {option} {value}` `/set {option} -` Retrieve or change specific configuration options, like a tech-savvy wizard. - use `-` as `{value}` to restore the option to default. - some options need a game or level re-launch to apply. - option names use `-`, not `_`, because reasons. - `/cheats on` `/cheats off` Enables or disables the cheater's toolkit. But let's face it – you're reading _this_, so that ship has sailed. - `/braid on` `/braid off` Toggle Lara's braid like it's a fashion accessory. Hair today, gone tomorrow. - `/wireframe on` `/wireframe off` Enables or disables the wireframe mode. Enter the debugging realm! - `/lighting on` `/lighting off` Enables or disables the lighting system. Bask in dynamic shadows or embrace bright clarity! - `/textures on` `/textures off` Enables or disables texture rendering. Peek the exact polygons that power these pretty visuals! - `/debug on` `/debug off` `/debug {option} on` `/debug {option} off` Toggles debug mode, turning your screen into a glorious display of dev scribbles. - floor triggers - enemy skips incoming! - room portals - wait, there are _how_ many rooms?! - room clip rectangles – the source of developers nightmares. - object mesh spheres - see hitboxes in their natural habitat. - Lara's position and animation details - nerdy stats, you've gotta love them. - bounding boxes – to marvel at the collision code. - `/speed` `/speed {num}` Displays or sets the speed of the game. Because sometimes you want to moonwalk through mayhem. - `/vsync on` `/vsync off` Turns vertical sync on or off. For the smooth freaks among us. - `/fps` `/fps {num}` Displays or sets the game's frames per second. Higher FPS = smoother Lara. - `/weather off` `/weather snow` `/weather rain` Changes the current level weather. Your game, your forecast. ## Environmental commands - `/flip` `/flip off` `/flip on` Switches the global flipmap on or off. Turn the reality around you on its head. - `/flood` `/drain` `/flood {room_num}` `/drain {room_num}` Floods or drains rooms at will. Act like you're Poseidon with a plumbing license, for when drowning is preferable to puzzles! - `/music {track_id}` Plays a music track by its ID. Perfect for setting the mood at will. - `/sfx` `/sfx {sound}` Plays a sound effect on demand. Because sometimes you just need Lara to grunt on cue. ## Game flow commands - `/endlevel` `/nextlevel` Too cool to finish puzzles? Smash-cut to the ending! Lara doesn't have time for this nonsense. - `/level {num}` `/level {name}` `/play {num}` `/play {name}` Launches any level you like. Start with `/play 0` to warm up in the gym – or skip straight to the danger zone with `/play 1` onwards. - `/cut {num}` `/cutscene {num}` Plays a dramatic cinematic. Follow the lore! - `/gym` `/home` Sends Lara to her humble abode. Even tomb raiders can't skip leg day. - `/save {slot_num}` `/save quick` `/quicksave` `/qs` Save your progress to a normal slot, or do a rotating quick-save. - `/load {slot_num}` `/load quick [slot_num]` `/load q[slot_num]` `/quickload [slot_num]` `/ql [slot_num]` Time-travel to a previous normal save, or load a quick-save by sorted quick-save position (most recent is `1`). - `/demo` `/demo {num}` Starts a demo. No number? They'll just cycle. - `/title` Had enough? Let's return to the main menu. - `/mod {name}` Switches to another installed game/mod pack and reloads the game flow. Great for hopping between adventures without relaunching TRX. - `/exit` Closes the game. Ends the adventure. We're done here. ## Miscellaneous commands - `/cls` `/clear` Wipes the console logs, quickly erasing all traces of your cheat spree (or that ugly pile of debug misery). - `/strings` Reloads the current language files on the fly. Très utile for translators. - `/screenshot [path]` Commemorates Lara's antics by taking a picture and saving it to the optional path (relative to the game root directory). - `/lua {string}` Type any LUA code to run it on the spot. Proceed with caution, or at least a sense of adventure! ================================================ FILE: docs/trx/COMMAND_LINE.md ================================================ --- title: CLI options order: 1 --- # Command line options Currently the following command line interface options are available: - `--mod ` Runs a specific game or mod directly. Available mods: - `tr1` (Tomb Raider I) - `tr1-ub` (Tomb Raider I: Unfinished Business) - `tr1-demo-pc` (Tomb Raider I: PC Demo) - `tr2` (Tomb Raider II) - `tr2-gm` (Tomb Raider II: The Golden Mask) - `tr3` (Tomb Raider III) - `tr3-la` (Tomb Raider III: The Lost Artifact) TRX remembers the last selected game or expansion when no explicit startup option is given. Because of that, `TRX.exe` starts whichever game or expansion was selected most recently. Use `TRX.exe --mod` to force the matching expansion pack from a shortcut. - `-l `, `--level ` Runs the game immediately launching it into the specified level. If `` is provided, runs the custom level located in the specified location. If the path is relative, it is resolved from the current working directory first, then from the game directory. Internally, this option uses `tr*-level/gameflow.json5` as a template instructing it how to run the game. If `` is an integer, plays the level with the given number within the main game flow (1-based). - `-s `, `--save `: Runs the game immediately loading a specific save slot. The first save starts at `num=1`. This option can be combined with `-l`/`--level`. - `--test-record `: Records gameplay events to an external text file. - `--test-replay `, `--test-play `: Replays gameplay events from an external text file. - `--headless`: Runs the game in command line only. Only available with `--test-replay`. - `--headless-fps `: In headless mode, forces the simulation to run at a constant FPS. If omitted or zero, uses the FPS from the configuration. - `--debug-render-performance`: Outputs diagnostic information related to GPU usage and throughput. - `-q`, `--quiet` Suppresses most of output to the standard output, keeping only errors. The log file is written to normally. > [!TIP] > If you want `TRX.exe` to start the main game again after using an expansion > shortcut, switch back to the main game in the passport first, or launch it > with `--mod`/`--engine` from the shortcut as needed. > [!NOTE] > Gameplay capture is considered an internal testing tool, and may be > subject to breaking changes without warnings. # Legacy command line options - `-g`, `--gold` (legacy: `-gold`) Runs the Unfinished Business or the Golden Mask expansion pack, depending on the last launched game. Please use `--mod` option instead. - `--demo-pc` (legacy: `-demo_pc`) (TR1 only) Runs the PC demo level. Please use `--mod` option instead. ================================================ FILE: docs/trx/ENEMY_DEFAULTS.md ================================================ --- title: Enemy defaults order: 14 --- # Enemy defaults ### Tomb Raider 1 | Game ID | TRX ID | Hit points | | ------- | ------------------- | ---------- | | `7` | O_WOLF | `6` | | `8` | O_BEAR | `20` | | `9` | O_BAT | `1` | | `10` | O_CROCODILE | `20` | | `11` | O_ALLIGATOR | `20` | | `12` | O_LION | `30` | | `13` | O_LIONESS | `25` | | `14` | O_PUMA | `45` | | `15` | O_APE | `22` | | `16` | O_RAT | `5` | | `17` | O_VOLE | `5` | | `18` | O_TREX | `100` | | `19` | O_RAPTOR | `20` | | `20` | O_ATLANTEAN_WINGED | `50` | | `21` | O_ATLANTEAN_SHOOTER | `50` | | `22` | O_ATLANTEAN_GROUND | `50` | | `23` | O_CENTAUR | `120` | | `24` | O_MUMMY | `18` | | `25` | O_DINO_WARRIOR | `100` | | `26` | O_FISH | `12` | | `27` | O_LARSON | `50` | | `28` | O_PIERRE | `70` | | `30` | O_SKATEKID | `125` | | `31` | O_COWBOY | `150` | | `32` | O_BALDY | `200` | | `33` | O_NATLA | `400` | | `34` | O_TORSO | `500` | | `145` | O_SCION_ITEM_3 | `5` | ### Tomb Raider 2 | Game ID | TRX ID | Hit points | | ------- | --------------- | ---------- | | `15` | O_DOG | `10` | | `16` | O_CULT_1 | `25` | | `17` | O_CULT_1A | `25` | | `18` | O_CULT_1B | `25` | | `19` | O_CULT_2 | `60` | | `20` | O_CULT_3 | `150` | | `21` | O_MOUSE | `4` | | `22` | O_DRAGON_FRONT | `300` | | `25` | O_SHARK | `30` | | `26` | O_EEL | `5` | | `27` | O_BIG_EEL | `20` | | `28` | O_BARRACUDA | `12` | | `29` | O_DIVER | `20` | | `30` | O_WORKER_1 | `25` | | `31` | O_WORKER_2 | `20` | | `32` | O_WORKER_3 | `27` | | `33` | O_WORKER_4 | `27` | | `34` | O_WORKER_5 | `20` | | `35` | O_JELLY | `10` | | `36` | O_SPIDER | `5` | | `37` | O_BIG_SPIDER | `40` | | `38` | O_CROW | `15` | | `39` | O_TIGER | `20` | | `41` | O_XIAN_SPEARMAN | `100` | | `43` | O_XIAN_KNIGHT | `80` | | `45` | O_YETI | `30` | | `46` | O_BIRD_GUARDIAN | `200` | | `47` | O_EAGLE | `20` | | `48` | O_BANDIT_1 | `45` | | `49` | O_BANDIT_2 | `50` | | `50` | O_BANDIT_2B | `50` | | `53` | O_MONK_1 | `30` | | `54` | O_MONK_2 | `30` | | `214` | O_TREX | `100` | | `265` | O_BEAR | `30` | | `266` | O_WOLF | `10` | | `267` | O_MONK_3 | `30` | ================================================ FILE: docs/trx/GAME_STRINGS.md ================================================ --- title: Game strings order: 4 --- # Game Strings ## Overview This document describes how to use and translate the TRX game strings. Game strings let level builders customize built-in object and level names in custom levels, and translators translate the entire game (including UI) into other languages. The configuration is structured in JSON5 format, which permits comments and supports a layering mechanism for overrides. ## Audiences This document serves two main audiences: - **Level builders**: Customize object names, level titles, and UI strings for custom levels and mod packs. - **Translators**: Translate the full game text (gameplay, UI, and menus) into different languages. ## Quick‑start example ```json5 { // Override only the key_1 pickup in Level 0 "levels": [ { "title": "City of Vilcabamba", "objects": { "key_1": { "name": "Gold Key" } } } ] } ``` ## Layering and Override Mechanism TRX supports multiple layers of strings files that are loaded in a specific order. Later layers override earlier ones, allowing expansion packs and custom mods to selectively override base text. The default load order is: 1. `base_strings.json5` — Common defaults for all engines and languages. 2. `tr1/strings.json5` — Base strings for the main game. 3. `tr1-ub/strings.json5` — Overrides for the Unfinished Business expansion pack (or other expansion packs/mods). Depending on which mod or pack you run, the third layer may vary: - `tr1-ub/strings.json5` for the Unfinished Business expansion pack - `tr2-gm/strings.json5` for the Golden Mask TR2 expansion pack - `tr1-demo-pc/strings.json5` for the PC TR1 demo - `tr*-level/strings.json5` for the -l/--level command line switch For example, if the same key exists in both `base_strings.json5` and `tr1/strings.json5`, the value `tr1/strings.json5` will take precedence. Each layer can also have a variant translated to other languages - see [this section](./4-GAME_STRINGS.md#translating-each-layer). ## General structure The document is organized as follows: ```json5 { "language_name": "English", "levels": [ { "title": "City of Vilcabamba", "objects": { "key_1": { "name": "Silver Key", "description": "This shows when the player examines key1 in the inventory.", }, "puzzle_1": { "name": "Gold Idol", "description": "You can use \n to make new lines and \f to make new pages.", }, "key_2": { "name": "Rusty Key", }, // etc }, "general": { "stats": { "time_taken": "Time Taken", // etc }, }, }, // etc ], "general": { "stats": { "time_taken": "Time Taken", // etc }, }, "console": { "cmd": { "help": { "help": "Shows help for all commands or detailed help for one.", }, }, }, "dynamic": { "enums": { "lara_outfit": { "default": "Default", "tr1_classic": "TR1 Classic", // etc }, }, }, "enums": { "ASPECT_MODE": { "ASPECT_MODE_ANY": "Any", // etc }, // etc }, "settings": { "visuals.lara_outfit": { "title": "Lara's outfit", "description": "Changes Lara's appearance.", }, // etc }, "objects": { "lara": {"name": "Lara"}, "dog": {"names": ["Dog", "Doberman"]}, // etc } } ```
Property Type Required Scope / Description
extends String No Fallback to another language code for missing entries. For dialects (e.g., "fr-ca"), specify "extends": "fr" to inherit missing layers from the parent language.
language_name String No (only in common file) The display name of the language (e.g., "English", "Français") shown in the language selection UI. Should only be defined in the base_strings.json5 file.
levels Object array No This is where overrides for individual level details are defined. If a level doesn't override a string through its objects or nested string sections such as general or console, it'll be looked up in the global scope next. If the global scope doesn't define it either, it'll default to an internal default value shipped with the engine.
title String Yes Level entry field (levels[].title).
objects Object array No Object-related strings.
general Nested object map No General gameplay, UI, and menu strings in the form general/<group>/<key>.
console Nested object map No Developer-console labels, help text, and command messages in the form console/<group>/<key>.
settings Nested object map No Localized setting labels and descriptions, keyed by option path (<option>/title and <option>/description).
enums Nested object map No Static enum labels in the form enums/<ENUM_TYPE>/<ENUM_VALUE>.
dynamic Nested object map No Runtime dynamic-enum labels in the form dynamic/enums/<domain>/<value>.
### Object entry fields
Property Type Required Scope / Description
name String / String array No Object entry field (objects.<id>.name). Allows renaming any object, including key items and pickups. Can be a list of strings: inventory objects use the first name; additional names can be used with commands like /tp and /give.
description String No Object entry field (objects.<id>.description). Defines longer text for key and puzzle items. Use \n for new lines and \f for page breaks. Empty strings suppress the examine text UI.
> [!NOTE] > The `extends` property now refers to another language code, not a file path. > When present in the common strings layer, the manager will first load the > parent language (and any further `extends` chains), then apply the current > file's overrides. Cyclic `extends` chains are detected and will emit a > warning rather than infinite recursion. ## Common Object IDs and names | JSON key | Object ID (TR1) | Object ID (TR2) | Object ID (TR3) | |------------|-----------------|-----------------|-----------------| | `key_1` | 129 and 133 | 193 and 197 | 224 and 228 | | `key_2` | 130 and 134 | 194 and 198 | 225 and 229 | | `key_3` | 131 and 135 | 195 and 199 | 226 and 230 | | `key_4` | 132 and 136 | 196 and 200 | 227 and 231 | | `pickup_1` | 141 and 148 | 205 and 207 | 236 and 238 | | `pickup_2` | 142 and 149 | 206 and 208 | 237 and 239 | | `puzzle_1` | 110 and 114 | 174 and 178 | 205 and 209 | | `puzzle_2` | 111 and 115 | 175 and 179 | 206 and 210 | | `puzzle_3` | 112 and 116 | 176 and 180 | 207 and 211 | | `puzzle_4` | 113 and 117 | 177 and 181 | 208 and 212 | | `quest_1` | - | - | 240 and 244 | | `quest_2` | - | - | 241 and 245 | | `quest_3` | - | - | 242 and 246 | | `quest_4` | - | - | 243 and 247 | | `secret_1` | - | 190 | - | | `secret_2` | - | 191 | - | | `secret_3` | - | 192 | - | > [!NOTE] > Nearly all pickup items exist in two forms, as early games differentiate > between a sprite displayed on the ground and a 3D object depicted in the > inventory ring. Secrets are a notable exception, as they never appear in the > inventory ring in the original game. For convenience, both forms are defined > using a single key. ## Translation Workflow ### Translating each layer To provide localized translations, place language-specific overrides alongside each base file using the naming pattern `-.json5`. Translation files must live in the same directory as their base (e.g. `cfg/`). For example, to add French translations: ```text cfg/base_strings-fr.json5 # common strings cfg/tr1/strings-fr.json5 # base game strings cfg/tr1-demo-pc/strings-fr.json5 # TR1 demo overrides cfg/tr1-level/strings-fr.json5 # custom-level pack overrides cfg/tr1-ub/strings-fr.json5 # Unfinished Business overrides cfg/tr2/strings-fr.json5 # base game strings cfg/tr2-gm/strings-fr.json5 # Golden Mask TR2 overrides cfg/tr2-level/strings-fr.json5 # custom-level pack overrides ``` When the game starts, TRX will detect these files and load them in place of the default layer for that language code (`fr` in this example). Omit any translation file for a layer you do not need; the game will fall back to the English base for that layer by default. ### Live reloading (`/strings`) To apply changes to string files without restarting the game, use the `/strings` console command. It reloads all string layers for the current language (common, base, and mod-specific), making it easy to test translation or custom overrides on the fly. In addition, languages can be switched at runtime without the need to use the UI with the `/set language ` console command (e.g. `/set language fr`). New language files are only detected at the game launch. If you create a new layer file, you'll need to relaunch the game to see the effects. ### Review system The development team uses AI-assisted tools to create initial translations. Automated translations are tagged with a special marker `\{review}` indicating that the text needs human review. By default, review markers are hidden in-game. To enable review mode and display markers, run: ``` /set review 1 ``` Translators should remove the `\{review}` tags once the translation has been reviewed and finalized. ### `language_name` Only supported in the common strings file (`base_strings.json5`), the `language_name` property sets the display name of the language in the options menu. For example: ```json5 { "language_name": "Français" } ``` ## Custom levels ### General tips - **Zero-indexed levels**: Levels are zero-indexed and match the order in the game flow file. - **Minimal overrides**: Only define the strings you need; the game will fall back to built-in defaults for any missing entries. For example: ```json5 { "levels": [ { "title": "City of Vilcabamba", "objects": { "key_1": {"name": "Gold Key"} } } ] } ``` - **Renaming any object**: You can rename puzzle items, keys, enemies, or any other in-game object. For example, TR2 object #⁠39 (tiger) can be renamed to "Snow Leopard" in a winter-themed level. ### Console and object IDs All objects have names, even if they are not shown in the UI. Use these names with console commands such as `/tp` or `/give`. For example, `/tp tiger` teleports Lara to object #⁠39. Console commands accept partial, case-insensitive names, and will match unique substrings to objects (powered by fuzzy matching). In case the player uses languages other than English, the commands also accept builtin English names (so even though wolf is called a wilk in Polish, on top of `/tp wilk` the players can still `/tp wolf`). For a complete list of object IDs for a specific engine, refer to the game strings files shipped with the relevant TRX builds. ================================================ FILE: docs/trx/INJECTIONS.md ================================================ --- title: Injections (.bin files) order: 15 --- # Injections (`.bin` files) In TRX, an *injection* is a binary patch file (`.bin`) that the engine applies to level data at load time. Injections are used to fix or extend base game data in a way that stays compatible with custom levels (unless you intentionally replace the same data in your own WAD). Most builders only need injections for the "default TRX assets" (extra Lara animations, extended fonts, PDA model, etc). ## How injections are configured (gameflow) Gameflow JSON supports injections in two places: - **Global injections**: applied to all levels. - **Per-level injections**: applied only for that level, optionally inheriting global injections. By default, injections defined in the global gameflow are applied to every level. If a level defines its own injections, those are merged with the global set when the level loads. Individual levels can set `inherit_injections` to `false`. In that case, global injection files are not used. If such a level defines its own `injections`, only those are applied; if it defines none, nothing is injected. Relevant keys (names may differ slightly per gameflow version): ```json5 { // global "injections": [ "data/injections/lara_extra.bin", "data/injections/font.bin" ], "levels": [ { "path": "data/levels/MY_LEVEL.TR2", "inherit_injections": true, "injections": [ "data/injections/pda_model.bin" ] } ] } ``` > [!WARNING] > If you **import** the assets into your level WAD (see below), you should then > **remove** the corresponding `.bin` from gameflow to avoid > double-applying/replacing data! > [!NOTE] > If a level should **not** receive the global injections, set > `"inherit_injections": false` (or omit inheritance, depending on the > schema/version you're targeting). > [!NOTE] > The gameflow ignores referenced injection files that do not exist, but it's > best practice to remove references to keep gameflow clean. ## Builder workflow: keep the `.bin`, or bake into your WAD You can handle TRX default assets in two ways: 1. **Keep using injections** (recommended for most cases): ship the `.bin` files and reference them in gameflow. 2. **Bake assets into your WAD**: import the provided assets into your level's WAD, then remove the related `.bin` references from gameflow. ### Common steps for importing TRX assets into your WAD (WadTool) 1. Open your level's WAD in **WadTool**. 2. Open the extracted `.wad2` file for the applicable game as the **source level** in WadTool. 3. Move the required assets from the source to the destination, replacing the existing ones. 4. Follow any asset-specific notes in the table below. 5. Update your gameflow to remove references to the asset's `.bin` file. TRX provides asset packs intended for WadTool import: - `https://lostartefacts.dev/pub/tr1-assets.zip` - `https://lostartefacts.dev/pub/tr2-assets.zip` The zips also include Tomb Editor catalogs (`Moveables.xml` / `SpriteSequences.xml`) so TRX object names show up (and to enable cross-game placements like TR2 guns in TR1 levels). See the README inside the zip for details. ## Builder note on custom levels Custom levels should generally not rely on injections for correctness; instead, provide data that is already correct and consistent. Note however that the injections that relate to Lara can work in custom levels that do not modify Lara's default mesh structure or animations. These injection files are based on the original Lara model. ## Default injection files (reference) The rule of thumb for custom levels is that if an injection file name starts with a level name, it is meant for specific original levels and should generally be removed from your custom gameflow unless you know what you're doing. ### Core files useful for most builders | Injection file | Usage | Purpose | | --- | --- | --- | | `lara_animations.bin` | TR1, TR2 | Lara animations/state/commands (jump-twist, somersault, underwater roll, wading, etc). If Lara's appearance is customised, move the source object to another slot and replace meshes manually. **TR1 only:** add `wet-feet.xml` to the sound catalogue (adds sound IDs 15 & 17) and provide the referenced wet-feet `.wav` samples (or your own). | | `lara_guns.bin` / `lara_gym_guns.bin` | TR1, TR2 | In TR1, replaces Lara's fixed shotgun-torso mesh with the TR2+ approach of an independent resting gun mesh. These files also contain Lara's guns from the other games, including flares. The gym file injects all of Lara's weapons and weapon animations in the gym level (for cheats only). | | `lara_extra.bin` | TR1, TR2 | Combined object containing extra animations shared between TR1, TR2 and TR3. | | `lara_outfits.bin` | TR1, TR2 | Contains each of Lara's outfits to offer live skin swaps in-game. | | `pda_model.bin` | TR1, TR2 | The original PDA model with an opening animation. Used by the Gameplay options UI. | | `font.bin` | TR1, TR2 | Replacement font sprites to support more characters than OG. | | `secret_models_*.bin` | TR2 | 3D models for secret pickups in OG and Golden Mask. | | `braid.bin` | TR1 | Braid option: injects braid plus mesh swaps for Lara's head/backpack (incl. Midas variant). | | `bubbles.bin` | TR1 | Replacement sprites for Lara's underwater bubbles (OG sprites are cut off). | | `pickup_aid.bin` | TR1, TR2 | Sprite sequence for pickup aids option; custom levels should define a suitable sprite sequence in slot 185. | | `photo.bin` | TR1, TR2 | Camera shutter SFX for photo mode (needed only for cutscene levels). | | `crystal.bin` | TR1, TR2 | Replacement savegame crystal model (PS1 style). | | `scion_collision.bin` | TR1 | Increases collision radius on the targetable Scion so it can be shot with the shotgun. | | `guardian_death_commands.bin` | TR2 | Bird guardian death anim command to end the level on the final frame (TRX removes the hard-coded behavior). | | `mines_pushblocks.bin` | TR1 | Restores missing scraping SFX for pushblock types 2/3/4 by injecting anim command data. | | `boat_bits.bin` | TR2 | Model for `O_BOAT_BITS` (221) used to show the boat exploding when it crosses mines. | | `explosion.bin` | TR1 | Explosion sprites for certain console commands. | | `misc_sprites.bin` | TR1, TR2 | Various special-effect sprites such as snowflakes, shadow sprites, and the pink blood sequence used by `O_BLOOD_2`. | ### Level-specific files | Injection file | Usage | Purpose | | --- | --- | --- | | `*_cameras.bin` | TR1, TR2 | Positional adjustments for cameras that can otherwise cause visual issues. | | `*_fd.bin` | TR1, TR2 | Fixes for floor data issues in original levels. | | `*_itemrots.bin` | TR1, TR2 | Pickup item rotations for better visuals with 3D pickups. | | `*_meshfixes.bin` | TR1 | Miscellaneous mesh adjustments for objects (e.g., to avoid z-fighting). | | `*_music_tracks.bin` | TR2 | Trigger adjustments to convert music track numbers to match file names (OG levels only). | | `*_pickup_meshes.bin` | TR1, TR2 | Pickup mesh edits (e.g., rescaling keys / specific pickups). | | `*_sfx.bin` | TR1, TR2 | Various SFX fixes/additions. | | `*_skybox.bin` | TR1 | Predefined skybox injected into specific levels, and specific rooms marked to use it. | | `*_textures.bin` | TR1, TR2 | Texture fixes in original levels (e.g., gaps, wrong colors). | | `cistern_plants.bin` | TR1 | Disables animation on sprite ID 193 in The Cistern and Tomb of Tihocan. | | `detonator_lights.bin` | TR2 | Adds animation commands to the Bartoli's Hideout detonator to control the dynamic lighting. | | `khamoon_mummy.bin` | TR1 | Mummy in City of Khamoon room 25 (present on PS1, missing on PC). | | `seaweed_collision.bin` | TR2 | Fixes seaweed in Living Quarters blocking Lara from exiting the water. | | `breakable_tile_sfx.bin` | TR2 | Adds missing breakable tiles (collapsing floor) sounds that are otherwise silent in the OG. | | `loose_boards_sfx.bin` | TR2 | Adds missing breakable tiles (collapsing floor) sounds that are otherwise silent in the OG. | | `dagger_sprite.bin` | TR2 | Adds a UI sprite for the Dagger of Xian when 3D pickups are disabled. | | `lara_feet_sfx.bin` | TR1 | Resets Lara's footstep sound effects in the gym level to allow for contextual outfit SFX. | ================================================ FILE: docs/trx/INSTALLING.md ================================================ # Windows (installer) ## Installing (simplified) 1. Download the latest combined TRX installer. 2. Choose a destination directory for TRX itself. 3. Select whichever game packs you want to add: - `TR1`, `TR2`, and `TR3` from your original Steam, GOG, or disc installs. - `TR1:UB`, `TR1 PC Demo`, and `TR2:GM` as direct downloads from Lost Artefacts. - `TR3:LA` from your original disc install. 4. Let the installer remap the original files into the combined `games//` hierarchy. > [!NOTE] > When downloading TRX, you might see a warning from Windows Defender, your browser, or another security tool. Modern antivirus systems use AI-based heuristics - they flag anything uncommon or unsigned as suspicious, even if it's perfectly safe. TRX can trigger these alerts because: > > - It isn't signed with an expensive commercial certificate. > - It’s a small, community-made project, not a big corporate release, meaning tools err on the side of caution. > - It comes straight from us, not the Microsoft Store. > > TRX is open-source, so if you're curious, you can peek at everything on [GitHub](https://github.com/LostArtefacts/TRX/). # Windows / Linux ## Installing (manual) 1. Download the combined TRX zip file (without the `tr1-3` suffix). 2. Extract the zip file into a directory of your choice. Make sure you choose to overwrite existing directories and files. 3. Copy your original game files into the new combined hierarchy. The new zip separates shared files from game data: - Shared TRX files stay in `cfg/`. - Game-specific files now live in `games//`. When using the combined zip, please do not copy your original files into top-level `data/`, `fmv/`, `music/`, `audio/`, or `cuts/` directories. Instead, place them in these target directories: - **Tomb Raider 1** - `data/*.phd` → `games/tr1/levels/*.phd` - `fmv/*` → `games/tr1/fmv/*` - `music/*` → `games/tr1/music/*` - **Tomb Raider 1: Unfinished Business** - `data/cat.phd` → `games/tr1-ub/levels/cat.phd` - `data/egypt.phd` → `games/tr1-ub/levels/egypt.phd` - `data/end.phd` → `games/tr1-ub/levels/end.phd` - `data/end2.phd` → `games/tr1-ub/levels/end2.phd` - **Tomb Raider 2** - `data/*.tr2` → `games/tr2/levels/*.tr2` - `data/main.sfx` → `games/tr2/main.sfx` - `fmv/*` → `games/tr2/fmv/*` - `music/*.mp3` → `games/tr2/music/*.mp3` - **Tomb Raider 2: Golden Mask** - `data/level1.tr2` → `games/tr2-gm/levels/level1.tr2` - `data/level2.tr2` → `games/tr2-gm/levels/level2.tr2` - `data/level3.tr2` → `games/tr2-gm/levels/level3.tr2` - `data/level4.tr2` → `games/tr2-gm/levels/level4.tr2` - `data/level5.tr2` → `games/tr2-gm/levels/level5.tr2` - `data/title.tr2` → `games/tr2-gm/levels/title.tr2` - `data/main.sfx` → `games/tr2-gm/main.sfx` - **Tomb Raider 3** - `data/*.tr2` → `games/tr3/levels/*.tr2` - `data/main.sfx` → `games/tr3/main.sfx` - `audio/cdaudio.wad` → `games/tr3/audio/cdaudio.wad` - `cuts/*.tr2` → `games/tr3/cuts/*.tr2` - `fmv/*` → `games/tr3/fmv/*` - **Tomb Raider 3: The Lost Artifact** - `data/chunnel.tr2` → `games/tr3-la/levels/chunnel.tr2` - `data/scotland.tr2` → `games/tr3-la/levels/scotland.tr2` - `data/slinc.tr2` → `games/tr3-la/levels/slinc.tr2` - `data/undersea.tr2` → `games/tr3-la/levels/undersea.tr2` - `data/willsden.tr2` → `games/tr3-la/levels/willsden.tr2` - `data/zoo.tr2` → `games/tr3-la/levels/zoo.tr2` - `data/title.tr2` → `games/tr3-la/levels/title.tr2` - `data/main.sfx` → `games/tr3-la/main.sfx` ## Verifying the installation If you install everything correctly, your game directory should look more or less like this (click to expand):
.
├── cfg
│   ├── presets
│   │   ├── tr1-pc.json5
│   │   ├── tr1-ps1.json5
│   │   ├── tr2-pc.json5
│   │   ├── tr2-ps1.json5
│   │   ├── tr3-pc.json5
│   │   └── tr3-ps1.json5
│   ├── shaders
│   │   ├── 2d.glsl
│   │   ├── billboard.glsl
│   │   ├── common.glsl
│   │   ├── fbo.glsl
│   │   ├── lights.glsl
│   │   ├── meshes.glsl
│   │   ├── meshes_tr3.glsl
│   │   ├── meshes_tr12.glsl
│   │   └── ui.glsl
│   ├── base_strings-de.json5
│   ├── base_strings-en-gb.json5
│   ├── base_strings-fr.json5
│   ├── base_strings-gd.json5
│   ├── base_strings-it.json5
│   ├── base_strings-pl.json5
│   ├── base_strings-ru.json5
│   ├── base_strings.json5
│   ├── outfits.json5
│   ├── poses.json5
│   ├── shell.json5*
│   ├── TR1X.json5*
│   ├── TR2X.json5*
│   ├── TR3X.json5*
│   └── ui.json5
├── games
│   ├── tr1
│   │   ├── fmv
│   │   │   ├── cafe.rpl
│   │   │   ├── canyon.rpl
│   │   │   ├── core.avi
│   │   │   ├── end.rpl
│   │   │   ├── escape.rpl
│   │   │   ├── lift.rpl
│   │   │   ├── mansion.rpl
│   │   │   ├── prison.rpl
│   │   │   ├── pyramid.rpl
│   │   │   ├── snow.rpl
│   │   │   └── vision.rpl
│   │   ├── images
│   │   │   ├── atlantis.webp
│   │   │   ├── credits_1.webp
│   │   │   ├── credits_2.webp
│   │   │   ├── credits_3.webp
│   │   │   ├── credits_3_alt.webp
│   │   │   ├── credits_ps1.webp
│   │   │   ├── egypt.webp
│   │   │   ├── eidos.webp
│   │   │   ├── end.webp
│   │   │   ├── greece.webp
│   │   │   ├── greece_saturn.webp
│   │   │   ├── gym.webp
│   │   │   ├── install.webp
│   │   │   ├── peru.webp
│   │   │   ├── title.webp
│   │   │   └── title_og_alt.webp
│   │   ├── injections
│   │   │   ├── atlantis_door_sfx.bin
│   │   │   ├── atlantis_fd.bin
│   │   │   ├── atlantis_itemrots.bin
│   │   │   ├── atlantis_textures.bin
│   │   │   ├── braid.bin
│   │   │   ├── bubbles.bin
│   │   │   ├── cat_cameras.bin
│   │   │   ├── cat_crystals.bin
│   │   │   ├── cat_fd.bin
│   │   │   ├── cat_itemrots.bin
│   │   │   ├── cat_meshfixes.bin
│   │   │   ├── cat_textures.bin
│   │   │   ├── caves_fd.bin
│   │   │   ├── caves_itemrots.bin
│   │   │   ├── caves_textures.bin
│   │   │   ├── cistern_fd.bin
│   │   │   ├── cistern_itemrots.bin
│   │   │   ├── cistern_plants.bin
│   │   │   ├── cistern_skybox.bin
│   │   │   ├── cistern_textures.bin
│   │   │   ├── colosseum_fd.bin
│   │   │   ├── colosseum_itemrots.bin
│   │   │   ├── colosseum_skybox.bin
│   │   │   ├── colosseum_textures.bin
│   │   │   ├── crystal.bin
│   │   │   ├── cut1_setup.bin
│   │   │   ├── cut2_setup.bin
│   │   │   ├── cut3_setup.bin
│   │   │   ├── cut3_textures.bin
│   │   │   ├── cut4_setup.bin
│   │   │   ├── cut4_textures.bin
│   │   │   ├── door58_frames.bin
│   │   │   ├── door59_frames.bin
│   │   │   ├── door59_sfx.bin
│   │   │   ├── door60_frames.bin
│   │   │   ├── door61_sfx.bin
│   │   │   ├── egypt_cameras.bin
│   │   │   ├── egypt_crystals.bin
│   │   │   ├── egypt_fd.bin
│   │   │   ├── egypt_itemrots.bin
│   │   │   ├── egypt_meshfixes.bin
│   │   │   ├── egypt_textures.bin
│   │   │   ├── explosion.bin
│   │   │   ├── folly_fd.bin
│   │   │   ├── folly_itemrots.bin
│   │   │   ├── folly_pickup_meshes.bin
│   │   │   ├── folly_textures.bin
│   │   │   ├── font.bin
│   │   │   ├── gun_glow.bin
│   │   │   ├── gym_textures.bin
│   │   │   ├── hive_crystals.bin
│   │   │   ├── hive_fd.bin
│   │   │   ├── hive_itemrots.bin
│   │   │   ├── hive_textures.bin
│   │   │   ├── khamoon_fd.bin
│   │   │   ├── khamoon_itemrots.bin
│   │   │   ├── khamoon_meshfixes.bin
│   │   │   ├── khamoon_mummy.bin
│   │   │   ├── khamoon_textures.bin
│   │   │   ├── lara_animations.bin
│   │   │   ├── lara_extra.bin
│   │   │   ├── lara_feet_sfx.bin
│   │   │   ├── lara_flares.bin
│   │   │   ├── lara_guns.bin
│   │   │   ├── lara_gym_flares.bin
│   │   │   ├── lara_gym_guns.bin
│   │   │   ├── lara_outfits.bin
│   │   │   ├── midas_itemrots.bin
│   │   │   ├── midas_textures.bin
│   │   │   ├── mines_cameras.bin
│   │   │   ├── mines_door_sfx.bin
│   │   │   ├── mines_fd.bin
│   │   │   ├── mines_itemrots.bin
│   │   │   ├── mines_meshfixes.bin
│   │   │   ├── mines_pushblocks.bin
│   │   │   ├── mines_textures.bin
│   │   │   ├── misc_sprites.bin
│   │   │   ├── obelisk_fd.bin
│   │   │   ├── obelisk_itemrots.bin
│   │   │   ├── obelisk_meshfixes.bin
│   │   │   ├── obelisk_skybox.bin
│   │   │   ├── obelisk_textures.bin
│   │   │   ├── panther_sfx.bin
│   │   │   ├── pda_model.bin
│   │   │   ├── photo.bin
│   │   │   ├── pickup_aid.bin
│   │   │   ├── pyramid_fd.bin
│   │   │   ├── pyramid_itemrots.bin
│   │   │   ├── pyramid_textures.bin
│   │   │   ├── qualopec_door_sfx.bin
│   │   │   ├── qualopec_fd.bin
│   │   │   ├── qualopec_itemrots.bin
│   │   │   ├── qualopec_textures.bin
│   │   │   ├── sanctuary_fd.bin
│   │   │   ├── sanctuary_itemrots.bin
│   │   │   ├── sanctuary_scion.bin
│   │   │   ├── sanctuary_textures.bin
│   │   │   ├── scion_collision.bin
│   │   │   ├── skate_kid_sfx.bin
│   │   │   ├── sprite_alignment.bin
│   │   │   ├── stronghold_crystals.bin
│   │   │   ├── stronghold_fd.bin
│   │   │   ├── stronghold_itemrots.bin
│   │   │   ├── stronghold_textures.bin
│   │   │   ├── tihocan_fd.bin
│   │   │   ├── tihocan_itemrots.bin
│   │   │   ├── tihocan_skybox.bin
│   │   │   ├── tihocan_textures.bin
│   │   │   ├── title_textures.bin
│   │   │   ├── uzi_sfx.bin
│   │   │   ├── valley_fd.bin
│   │   │   ├── valley_itemrots.bin
│   │   │   ├── valley_skybox.bin
│   │   │   ├── valley_textures.bin
│   │   │   ├── vilcabamba_door_sfx.bin
│   │   │   ├── vilcabamba_itemrots.bin
│   │   │   ├── vilcabamba_textures.bin
│   │   │   └── winston_model.bin
│   │   ├── levels
│   │   │   ├── cut1.phd
│   │   │   ├── cut2.phd
│   │   │   ├── cut3.phd
│   │   │   ├── cut4.phd
│   │   │   ├── gym.phd
│   │   │   ├── level1.phd
│   │   │   ├── level2.phd
│   │   │   ├── level3a.phd
│   │   │   ├── level3b.phd
│   │   │   ├── level4.phd
│   │   │   ├── level5.phd
│   │   │   ├── level6.phd
│   │   │   ├── level7a.phd
│   │   │   ├── level7b.phd
│   │   │   ├── level8a.phd
│   │   │   ├── level8b.phd
│   │   │   ├── level8c.phd
│   │   │   ├── level10a.phd
│   │   │   ├── level10b.phd
│   │   │   ├── level10c.phd
│   │   │   └── title.phd
│   │   ├── music
│   │   │   ├── track02.flac
│   │   │   ├── track03.flac
│   │   │   ├── track04.flac
│   │   │   ├── track05.flac
│   │   │   ├── track06.flac
│   │   │   ├── track07.flac
│   │   │   ├── track08.flac
│   │   │   ├── track09.flac
│   │   │   ├── track10.flac
│   │   │   ├── track11.flac
│   │   │   ├── track12.flac
│   │   │   ├── track13.flac
│   │   │   ├── track14.flac
│   │   │   ├── track15.flac
│   │   │   ├── track16.flac
│   │   │   ├── track17.flac
│   │   │   ├── track18.flac
│   │   │   ├── track19.flac
│   │   │   ├── track20.flac
│   │   │   ├── track21.flac
│   │   │   ├── track22.flac
│   │   │   ├── track23.flac
│   │   │   ├── track24.flac
│   │   │   ├── track25.flac
│   │   │   ├── track26.flac
│   │   │   ├── track27.flac
│   │   │   ├── track28.flac
│   │   │   ├── track29.flac
│   │   │   ├── track30.flac
│   │   │   ├── track31.flac
│   │   │   ├── track32.flac
│   │   │   ├── track33.flac
│   │   │   ├── track34.flac
│   │   │   ├── track35.flac
│   │   │   ├── track36.flac
│   │   │   ├── track37.flac
│   │   │   ├── track38.flac
│   │   │   ├── track39.flac
│   │   │   ├── track40.flac
│   │   │   ├── track41.flac
│   │   │   ├── track42.flac
│   │   │   ├── track43.flac
│   │   │   ├── track44.flac
│   │   │   ├── track45.flac
│   │   │   ├── track46.flac
│   │   │   ├── track47.flac
│   │   │   ├── track48.flac
│   │   │   ├── track49.flac
│   │   │   ├── track50.flac
│   │   │   ├── track51.flac
│   │   │   ├── track52.flac
│   │   │   ├── track53.flac
│   │   │   ├── track54.flac
│   │   │   ├── track55.flac
│   │   │   ├── track56.flac
│   │   │   ├── track57.flac
│   │   │   ├── track58.flac
│   │   │   ├── track59.flac
│   │   │   └── track60.flac
│   │   ├── scripts
│   │   │   └── gym.lua
│   │   ├── catalog_item_actions.csv
│   │   ├── catalog_lara_anims.csv
│   │   ├── catalog_lara_states.csv
│   │   ├── catalog_music.csv
│   │   ├── catalog_objects.csv
│   │   ├── catalog_samples.csv
│   │   ├── gameflow.json5
│   │   ├── inv_ring.json5
│   │   ├── strings-de.json5
│   │   ├── strings-en-gb.json5
│   │   ├── strings-fr.json5
│   │   ├── strings-gd.json5
│   │   ├── strings-it.json5
│   │   ├── strings-pl.json5
│   │   ├── strings-ru.json5
│   │   ├── strings.json5
│   │   └── weapons.json5
│   ├── tr1-demo-pc
│   │   ├── gameflow.json5
│   │   ├── strings-de.json5
│   │   ├── strings-fr.json5
│   │   ├── strings-gd.json5
│   │   ├── strings-it.json5
│   │   ├── strings-pl.json5
│   │   ├── strings-ru.json5
│   │   └── strings.json5
│   ├── tr1-level
│   │   ├── gameflow.json5
│   │   ├── strings-de.json5
│   │   ├── strings-fr.json5
│   │   ├── strings-gd.json5
│   │   ├── strings-it.json5
│   │   ├── strings-pl.json5
│   │   ├── strings-ru.json5
│   │   └── strings.json5
│   ├── tr1-ub
│   │   ├── images
│   │   │   ├── credits_ub.webp
│   │   │   ├── title_ub.webp
│   │   │   ├── ub_loading1.webp
│   │   │   └── ub_loading2.webp
│   │   ├── levels
│   │   │   ├── cat.phd
│   │   │   ├── egypt.phd
│   │   │   ├── end2.phd
│   │   │   └── end.phd
│   │   ├── gameflow.json5
│   │   ├── strings-de.json5
│   │   ├── strings-fr.json5
│   │   ├── strings-gd.json5
│   │   ├── strings-it.json5
│   │   ├── strings-pl.json5
│   │   ├── strings-ru.json5
│   │   └── strings.json5
│   ├── tr2
│   │   ├── fmv
│   │   │   ├── ancient.rpl
│   │   │   ├── crash.rpl
│   │   │   ├── end.rpl
│   │   │   ├── jeep.rpl
│   │   │   ├── landing.rpl
│   │   │   ├── logo.rpl
│   │   │   ├── modern.rpl
│   │   │   └── ms.rpl
│   │   ├── images
│   │   │   ├── 3x2
│   │   │   │   ├── china.webp
│   │   │   │   ├── credit01.webp
│   │   │   │   ├── credit02.webp
│   │   │   │   ├── credit03.webp
│   │   │   │   ├── credit04.webp
│   │   │   │   ├── credit05.webp
│   │   │   │   ├── credit06.webp
│   │   │   │   ├── credit07.webp
│   │   │   │   ├── credit08.webp
│   │   │   │   ├── end.webp
│   │   │   │   ├── legal_eu.webp
│   │   │   │   ├── legal_us.webp
│   │   │   │   ├── mansion.webp
│   │   │   │   ├── rig.webp
│   │   │   │   ├── tibet.webp
│   │   │   │   ├── titan.webp
│   │   │   │   ├── title_eu.webp
│   │   │   │   ├── title_us.webp
│   │   │   │   └── venice.webp
│   │   │   ├── 4x3
│   │   │   │   ├── china.webp
│   │   │   │   ├── credit01.webp
│   │   │   │   ├── credit02.webp
│   │   │   │   ├── credit03.webp
│   │   │   │   ├── credit04.webp
│   │   │   │   ├── credit05.webp
│   │   │   │   ├── credit06.webp
│   │   │   │   ├── credit07.webp
│   │   │   │   ├── credit08.webp
│   │   │   │   ├── end.webp
│   │   │   │   ├── legal_eu.webp
│   │   │   │   ├── legal_us.webp
│   │   │   │   ├── mansion.webp
│   │   │   │   ├── rig.webp
│   │   │   │   ├── tibet.webp
│   │   │   │   ├── titan.webp
│   │   │   │   ├── title_eu.webp
│   │   │   │   ├── title_us.webp
│   │   │   │   └── venice.webp
│   │   │   ├── og
│   │   │   │   ├── china.webp
│   │   │   │   ├── credit01.webp
│   │   │   │   ├── credit02.webp
│   │   │   │   ├── credit03.webp
│   │   │   │   ├── credit04.webp
│   │   │   │   ├── credit05.webp
│   │   │   │   ├── credit06.webp
│   │   │   │   ├── credit07.webp
│   │   │   │   ├── credit08.webp
│   │   │   │   ├── end.webp
│   │   │   │   ├── legal.webp
│   │   │   │   ├── mansion.webp
│   │   │   │   ├── rig.webp
│   │   │   │   ├── tibet.webp
│   │   │   │   ├── titan.webp
│   │   │   │   ├── title_eu.webp
│   │   │   │   ├── title_us.webp
│   │   │   │   └── venice.webp
│   │   │   ├── china.webp
│   │   │   ├── credit01.webp
│   │   │   ├── credit02.webp
│   │   │   ├── credit03.webp
│   │   │   ├── credit04.webp
│   │   │   ├── credit05.webp
│   │   │   ├── credit06.webp
│   │   │   ├── credit07.webp
│   │   │   ├── credit08.webp
│   │   │   ├── end.webp
│   │   │   ├── legal_eu.webp
│   │   │   ├── legal_us.webp
│   │   │   ├── mansion.webp
│   │   │   ├── rig.webp
│   │   │   ├── tibet.webp
│   │   │   ├── titan.webp
│   │   │   ├── title_eu.webp
│   │   │   ├── title_us.webp
│   │   │   └── venice.webp
│   │   ├── injections
│   │   │   ├── barkhang_cameras.bin
│   │   │   ├── barkhang_crystals.bin
│   │   │   ├── barkhang_fd.bin
│   │   │   ├── barkhang_itemrots.bin
│   │   │   ├── barkhang_music_tracks.bin
│   │   │   ├── barkhang_pickup_meshes.bin
│   │   │   ├── barkhang_textures.bin
│   │   │   ├── bartoli_crystals.bin
│   │   │   ├── bartoli_music_tracks.bin
│   │   │   ├── bartoli_secret_fd.bin
│   │   │   ├── bartoli_textures.bin
│   │   │   ├── boat_bits.bin
│   │   │   ├── breakable_tile_sfx.bin
│   │   │   ├── catacombs_crystals.bin
│   │   │   ├── catacombs_fd.bin
│   │   │   ├── catacombs_itemrots.bin
│   │   │   ├── catacombs_music_tracks.bin
│   │   │   ├── catacombs_textures.bin
│   │   │   ├── coldwar_crystals.bin
│   │   │   ├── coldwar_fd.bin
│   │   │   ├── coldwar_itemrots.bin
│   │   │   ├── coldwar_music_tracks.bin
│   │   │   ├── coldwar_objects.bin
│   │   │   ├── coldwar_textures.bin
│   │   │   ├── common_pickup_meshes.bin
│   │   │   ├── common_pickup_meshes_gm.bin
│   │   │   ├── crystal.bin
│   │   │   ├── cut2_setup.bin
│   │   │   ├── cut2_textures.bin
│   │   │   ├── cut3_setup.bin
│   │   │   ├── cut3_textures.bin
│   │   │   ├── cut4_setup.bin
│   │   │   ├── cut4_textures.bin
│   │   │   ├── dagger_sprite.bin
│   │   │   ├── deck_cameras.bin
│   │   │   ├── deck_crystals.bin
│   │   │   ├── deck_fd.bin
│   │   │   ├── deck_itemrots.bin
│   │   │   ├── deck_music_tracks.bin
│   │   │   ├── deck_pickup_meshes.bin
│   │   │   ├── deck_plants.bin
│   │   │   ├── deck_secret_fd.bin
│   │   │   ├── deck_textures.bin
│   │   │   ├── detonator_lights.bin
│   │   │   ├── diving_cameras.bin
│   │   │   ├── diving_crystals.bin
│   │   │   ├── diving_itemrots.bin
│   │   │   ├── diving_music_tracks.bin
│   │   │   ├── diving_pickup_meshes.bin
│   │   │   ├── diving_sfx.bin
│   │   │   ├── diving_textures.bin
│   │   │   ├── door106_sfx.bin
│   │   │   ├── door107_sfx.bin
│   │   │   ├── door108_sfx.bin
│   │   │   ├── door110_sfx.bin
│   │   │   ├── door111_sfx.bin
│   │   │   ├── explosion.bin
│   │   │   ├── fathoms_crystals.bin
│   │   │   ├── fathoms_goon_sfx.bin
│   │   │   ├── fathoms_itemrots.bin
│   │   │   ├── fathoms_music_tracks.bin
│   │   │   ├── fathoms_plants.bin
│   │   │   ├── fathoms_secret_fd.bin
│   │   │   ├── fathoms_textures.bin
│   │   │   ├── floating_crystals.bin
│   │   │   ├── floating_fd.bin
│   │   │   ├── floating_itemrots.bin
│   │   │   ├── floating_music_tracks.bin
│   │   │   ├── floating_pickup_meshes.bin
│   │   │   ├── floating_textures.bin
│   │   │   ├── font.bin
│   │   │   ├── fools_crystals.bin
│   │   │   ├── fools_itemrots.bin
│   │   │   ├── fools_music_tracks.bin
│   │   │   ├── fools_pickup_meshes.bin
│   │   │   ├── fools_textures.bin
│   │   │   ├── furnace_crystals.bin
│   │   │   ├── furnace_itemrots.bin
│   │   │   ├── furnace_music_tracks.bin
│   │   │   ├── furnace_objects.bin
│   │   │   ├── furnace_pickup_meshes.bin
│   │   │   ├── furnace_textures.bin
│   │   │   ├── guardian_death_commands.bin
│   │   │   ├── gym_fd.bin
│   │   │   ├── gym_music_tracks.bin
│   │   │   ├── gym_sfx.bin
│   │   │   ├── gym_textures.bin
│   │   │   ├── house_itemrots.bin
│   │   │   ├── house_music_tracks.bin
│   │   │   ├── house_sfx.bin
│   │   │   ├── house_shower_frames.bin
│   │   │   ├── house_textures.bin
│   │   │   ├── inv_background.bin
│   │   │   ├── kingdom_cameras.bin
│   │   │   ├── kingdom_crystals.bin
│   │   │   ├── kingdom_itemrots.bin
│   │   │   ├── kingdom_music_tracks.bin
│   │   │   ├── kingdom_textures.bin
│   │   │   ├── lair_bartolipos.bin
│   │   │   ├── lair_crystals.bin
│   │   │   ├── lair_music_tracks.bin
│   │   │   ├── lair_textures.bin
│   │   │   ├── lara_animations.bin
│   │   │   ├── lara_extra.bin
│   │   │   ├── lara_guns.bin
│   │   │   ├── lara_gym_guns.bin
│   │   │   ├── lara_house_guns.bin
│   │   │   ├── lara_outfits.bin
│   │   │   ├── lara_rifle_sfx.bin
│   │   │   ├── lara_vegas_guns.bin
│   │   │   ├── living_crystals.bin
│   │   │   ├── living_deck_goon_sfx.bin
│   │   │   ├── living_fd.bin
│   │   │   ├── living_itemrots.bin
│   │   │   ├── living_music_tracks.bin
│   │   │   ├── living_pickup_meshes.bin
│   │   │   ├── living_secret_fd.bin
│   │   │   ├── living_sfx.bin
│   │   │   ├── living_textures.bin
│   │   │   ├── loose_boards_sfx.bin
│   │   │   ├── misc_sprites.bin
│   │   │   ├── opera_crystals.bin
│   │   │   ├── opera_fd.bin
│   │   │   ├── opera_itemrots.bin
│   │   │   ├── opera_music_tracks.bin
│   │   │   ├── opera_sfx.bin
│   │   │   ├── opera_textures.bin
│   │   │   ├── palace_crystals.bin
│   │   │   ├── palace_fd.bin
│   │   │   ├── palace_itemrots.bin
│   │   │   ├── palace_music_tracks.bin
│   │   │   ├── palace_secret_fd.bin
│   │   │   ├── palace_textures.bin
│   │   │   ├── pda_model.bin
│   │   │   ├── photo.bin
│   │   │   ├── pickup_aid.bin
│   │   │   ├── portcullis_sfx.bin
│   │   │   ├── rig_crystals.bin
│   │   │   ├── rig_itemrots.bin
│   │   │   ├── rig_music_tracks.bin
│   │   │   ├── rig_pickup_meshes.bin
│   │   │   ├── rig_textures.bin
│   │   │   ├── scuba_sfx.bin
│   │   │   ├── seaweed_collision.bin
│   │   │   ├── secret_models_gm.bin
│   │   │   ├── secret_models_og.bin
│   │   │   ├── shark_sfx.bin
│   │   │   ├── tibet_crystals.bin
│   │   │   ├── tibet_fd.bin
│   │   │   ├── tibet_itemrots.bin
│   │   │   ├── tibet_music_tracks.bin
│   │   │   ├── tibet_textures.bin
│   │   │   ├── title_textures.bin
│   │   │   ├── vegas_crystals.bin
│   │   │   ├── vegas_fd.bin
│   │   │   ├── vegas_itemrots.bin
│   │   │   ├── vegas_music_tracks.bin
│   │   │   ├── vegas_textures.bin
│   │   │   ├── venice_crystals.bin
│   │   │   ├── venice_fd.bin
│   │   │   ├── venice_itemrots.bin
│   │   │   ├── venice_music_tracks.bin
│   │   │   ├── venice_textures.bin
│   │   │   ├── wall_cameras.bin
│   │   │   ├── wall_crystals.bin
│   │   │   ├── wall_itemrots.bin
│   │   │   ├── wall_music_tracks.bin
│   │   │   ├── wall_textures.bin
│   │   │   ├── winston_model.bin
│   │   │   ├── wreck_cameras.bin
│   │   │   ├── wreck_crystals.bin
│   │   │   ├── wreck_fd.bin
│   │   │   ├── wreck_goon_sfx.bin
│   │   │   ├── wreck_itemrots.bin
│   │   │   ├── wreck_music_tracks.bin
│   │   │   ├── wreck_pickup_meshes.bin
│   │   │   ├── wreck_plants.bin
│   │   │   ├── wreck_secret_fd.bin
│   │   │   ├── wreck_textures.bin
│   │   │   ├── xian_crystals.bin
│   │   │   ├── xian_fd.bin
│   │   │   ├── xian_itemrots.bin
│   │   │   ├── xian_music_tracks.bin
│   │   │   ├── xian_pickup_meshes.bin
│   │   │   ├── xian_sfx.bin
│   │   │   └── xian_textures.bin
│   │   ├── levels
│   │   │   ├── assault.tr2
│   │   │   ├── boat.tr2
│   │   │   ├── catacomb.tr2
│   │   │   ├── cut1.tr2
│   │   │   ├── cut2.tr2
│   │   │   ├── cut3.tr2
│   │   │   ├── cut4.tr2
│   │   │   ├── deck.tr2
│   │   │   ├── emprtomb.tr2
│   │   │   ├── floating.tr2
│   │   │   ├── house.tr2
│   │   │   ├── icecave.tr2
│   │   │   ├── keel.tr2
│   │   │   ├── living.tr2
│   │   │   ├── monastry.tr2
│   │   │   ├── opera.tr2
│   │   │   ├── platform.tr2
│   │   │   ├── rig.tr2
│   │   │   ├── skidoo.tr2
│   │   │   ├── title.tr2
│   │   │   ├── unwater.tr2
│   │   │   ├── venice.tr2
│   │   │   ├── wall.tr2
│   │   │   └── xian.tr2
│   │   ├── music
│   │   │   ├── 2.mp3
│   │   │   ├── 3.mp3
│   │   │   ├── 4.mp3
│   │   │   ├── 5.mp3
│   │   │   ├── 6.mp3
│   │   │   ├── 7.mp3
│   │   │   ├── 8.mp3
│   │   │   ├── 9.mp3
│   │   │   ├── 10.mp3
│   │   │   ├── 11.mp3
│   │   │   ├── 12.mp3
│   │   │   ├── 13.mp3
│   │   │   ├── 14.mp3
│   │   │   ├── 15.mp3
│   │   │   ├── 16.mp3
│   │   │   ├── 17.mp3
│   │   │   ├── 18.mp3
│   │   │   ├── 19.mp3
│   │   │   ├── 20.mp3
│   │   │   ├── 21.mp3
│   │   │   ├── 22.mp3
│   │   │   ├── 23.mp3
│   │   │   ├── 24.mp3
│   │   │   ├── 25.mp3
│   │   │   ├── 26.mp3
│   │   │   ├── 27.mp3
│   │   │   ├── 28.mp3
│   │   │   ├── 29.mp3
│   │   │   ├── 30.mp3
│   │   │   ├── 31.mp3
│   │   │   ├── 32.mp3
│   │   │   ├── 33.mp3
│   │   │   ├── 34.mp3
│   │   │   ├── 35.mp3
│   │   │   ├── 36.mp3
│   │   │   ├── 37.mp3
│   │   │   ├── 38.mp3
│   │   │   ├── 39.mp3
│   │   │   ├── 40.mp3
│   │   │   ├── 41.mp3
│   │   │   ├── 42.mp3
│   │   │   ├── 43.mp3
│   │   │   ├── 44.mp3
│   │   │   ├── 45.mp3
│   │   │   ├── 46.mp3
│   │   │   ├── 47.mp3
│   │   │   ├── 48.mp3
│   │   │   ├── 49.mp3
│   │   │   ├── 50.mp3
│   │   │   ├── 51.mp3
│   │   │   ├── 52.mp3
│   │   │   ├── 53.mp3
│   │   │   ├── 54.mp3
│   │   │   ├── 55.mp3
│   │   │   ├── 56.mp3
│   │   │   ├── 57.mp3
│   │   │   ├── 58.mp3
│   │   │   ├── 59.mp3
│   │   │   ├── 60.mp3
│   │   │   └── 61.mp3
│   │   ├── scripts
│   │   │   ├── assault.lua
│   │   │   ├── cut3.lua
│   │   │   ├── floating.lua
│   │   │   ├── house.lua
│   │   │   ├── level1.lua
│   │   │   ├── level3.lua
│   │   │   ├── level4.lua
│   │   │   └── monastry.lua
│   │   ├── catalog_item_actions.csv
│   │   ├── catalog_lara_anims.csv
│   │   ├── catalog_lara_states.csv
│   │   ├── catalog_music.csv
│   │   ├── catalog_objects.csv
│   │   ├── catalog_samples.csv
│   │   ├── gameflow.json5
│   │   ├── inv_ring.json5
│   │   ├── main.sfx
│   │   ├── strings-de.json5
│   │   ├── strings-en-gb.json5
│   │   ├── strings-fr.json5
│   │   ├── strings-gd.json5
│   │   ├── strings-it.json5
│   │   ├── strings-pl.json5
│   │   ├── strings.json5
│   │   └── weapons.json5
│   ├── tr2-gm
│   │   ├── images
│   │   │   ├── 3x2
│   │   │   │   ├── credit00_gm.webp
│   │   │   │   ├── credit07_gm.webp
│   │   │   │   ├── gm_level1.webp
│   │   │   │   ├── gm_level2.webp
│   │   │   │   ├── gm_level3.webp
│   │   │   │   ├── gm_level4.webp
│   │   │   │   ├── gm_level5.webp
│   │   │   │   ├── legal_eu_gm.webp
│   │   │   │   ├── legal_us_gm.webp
│   │   │   │   ├── title_eu_gm.webp
│   │   │   │   └── title_us_gm.webp
│   │   │   ├── 4x3
│   │   │   │   ├── credit00_gm.webp
│   │   │   │   ├── credit07_gm.webp
│   │   │   │   ├── gm_level1.webp
│   │   │   │   ├── gm_level2.webp
│   │   │   │   ├── gm_level3.webp
│   │   │   │   ├── gm_level4.webp
│   │   │   │   ├── gm_level5.webp
│   │   │   │   ├── legal_eu_gm.webp
│   │   │   │   ├── legal_us_gm.webp
│   │   │   │   ├── title_eu_gm.webp
│   │   │   │   └── title_us_gm.webp
│   │   │   ├── og
│   │   │   │   ├── credit00_gm.webp
│   │   │   │   ├── credit07_gm.webp
│   │   │   │   ├── title_eu_gm.webp
│   │   │   │   └── title_us_gm.webp
│   │   │   ├── credit00_gm.webp
│   │   │   ├── credit07_gm.webp
│   │   │   ├── gm_level1.webp
│   │   │   ├── gm_level2.webp
│   │   │   ├── gm_level3.webp
│   │   │   ├── gm_level4.webp
│   │   │   ├── gm_level5.webp
│   │   │   ├── legal_eu_gm.webp
│   │   │   ├── legal_us_gm.webp
│   │   │   ├── title_eu_gm.webp
│   │   │   └── title_us_gm.webp
│   │   ├── levels
│   │   │   ├── level1.tr2
│   │   │   ├── level2.tr2
│   │   │   ├── level3.tr2
│   │   │   ├── level4.tr2
│   │   │   ├── level5.tr2
│   │   │   └── title.tr2
│   │   ├── gameflow.json5
│   │   ├── main.sfx
│   │   ├── strings-de.json5
│   │   ├── strings-fr.json5
│   │   ├── strings-gd.json5
│   │   ├── strings-it.json5
│   │   ├── strings-pl.json5
│   │   └── strings.json5
│   ├── tr2-level
│   │   ├── gameflow.json5
│   │   ├── strings-de.json5
│   │   ├── strings-fr.json5
│   │   ├── strings-gd.json5
│   │   ├── strings-it.json5
│   │   ├── strings-pl.json5
│   │   └── strings.json5
│   ├── tr3
│   │   ├── audio
│   │   │   └── cdaudio.wad
│   │   ├── cuts
│   │   │   ├── cut1.tr2
│   │   │   ├── cut2.tr2
│   │   │   ├── cut3.tr2
│   │   │   ├── cut4.tr2
│   │   │   ├── cut5.tr2
│   │   │   ├── cut6.tr2
│   │   │   ├── cut7.tr2
│   │   │   ├── cut8.tr2
│   │   │   ├── cut9.tr2
│   │   │   ├── cut11.tr2
│   │   │   └── cut12.tr2
│   │   ├── fmv
│   │   │   ├── crsh_eng.rpl
│   │   │   ├── endgame.rpl
│   │   │   ├── intr_eng.rpl
│   │   │   ├── logo.rpl
│   │   │   └── sail_eng.rpl
│   │   ├── images
│   │   │   ├── 3x2
│   │   │   │   ├── antarc.webp
│   │   │   │   ├── credit01.webp
│   │   │   │   ├── credit02.webp
│   │   │   │   ├── credit03.webp
│   │   │   │   ├── credit04.webp
│   │   │   │   ├── credit05.webp
│   │   │   │   ├── credit06.webp
│   │   │   │   ├── credit07.webp
│   │   │   │   ├── credit08.webp
│   │   │   │   ├── credit09.webp
│   │   │   │   ├── house.webp
│   │   │   │   ├── india.webp
│   │   │   │   ├── legal_eu.webp
│   │   │   │   ├── legal_us.webp
│   │   │   │   ├── london.webp
│   │   │   │   ├── nevada.webp
│   │   │   │   ├── southpac.webp
│   │   │   │   ├── theend2.webp
│   │   │   │   ├── title_eu.webp
│   │   │   │   └── title_us.webp
│   │   │   ├── 4x3
│   │   │   │   ├── antarc.webp
│   │   │   │   ├── credit01.webp
│   │   │   │   ├── credit02.webp
│   │   │   │   ├── credit03.webp
│   │   │   │   ├── credit04.webp
│   │   │   │   ├── credit05.webp
│   │   │   │   ├── credit06.webp
│   │   │   │   ├── credit07.webp
│   │   │   │   ├── credit08.webp
│   │   │   │   ├── credit09.webp
│   │   │   │   ├── house.webp
│   │   │   │   ├── india.webp
│   │   │   │   ├── legal_eu.webp
│   │   │   │   ├── legal_us.webp
│   │   │   │   ├── london.webp
│   │   │   │   ├── nevada.webp
│   │   │   │   ├── southpac.webp
│   │   │   │   ├── theend2.webp
│   │   │   │   ├── title_eu.webp
│   │   │   │   └── title_us.webp
│   │   │   ├── og
│   │   │   │   ├── antarc.webp
│   │   │   │   ├── credit01.webp
│   │   │   │   ├── credit02.webp
│   │   │   │   ├── credit03.webp
│   │   │   │   ├── credit04.webp
│   │   │   │   ├── credit05.webp
│   │   │   │   ├── credit06.webp
│   │   │   │   ├── credit07.webp
│   │   │   │   ├── credit08.webp
│   │   │   │   ├── credit09.webp
│   │   │   │   ├── house.webp
│   │   │   │   ├── india.webp
│   │   │   │   ├── legal_eu.webp
│   │   │   │   ├── legal_us.webp
│   │   │   │   ├── london.webp
│   │   │   │   ├── nevada.webp
│   │   │   │   ├── nevadafff.webp
│   │   │   │   ├── southpac.webp
│   │   │   │   ├── theend2.webp
│   │   │   │   ├── theend.webp
│   │   │   │   ├── title_eu.webp
│   │   │   │   └── title_us.webp
│   │   │   ├── antarc.webp
│   │   │   ├── credit01.webp
│   │   │   ├── credit02.webp
│   │   │   ├── credit03.webp
│   │   │   ├── credit04.webp
│   │   │   ├── credit05.webp
│   │   │   ├── credit06.webp
│   │   │   ├── credit07.webp
│   │   │   ├── credit08.webp
│   │   │   ├── credit09.webp
│   │   │   ├── house.webp
│   │   │   ├── india.webp
│   │   │   ├── legal_eu.webp
│   │   │   ├── legal_us.webp
│   │   │   ├── london.webp
│   │   │   ├── nevada.webp
│   │   │   ├── southpac.webp
│   │   │   ├── theend2.webp
│   │   │   ├── title_eu.webp
│   │   │   └── title_us.webp
│   │   ├── injections
│   │   │   ├── aldwych_fd.bin
│   │   │   ├── aldwych_pickup_meshes.bin
│   │   │   ├── aldwych_textures.bin
│   │   │   ├── antarc_airlock.bin
│   │   │   ├── antarc_door134_frames.bin
│   │   │   ├── antarc_sky.bin
│   │   │   ├── area51_sky.bin
│   │   │   ├── area51_textures.bin
│   │   │   ├── cavern_door131_frames.bin
│   │   │   ├── cavern_pickup_meshes.bin
│   │   │   ├── cavern_sky.bin
│   │   │   ├── city_textures.bin
│   │   │   ├── cliff_door132_frames.bin
│   │   │   ├── coastal_airlock.bin
│   │   │   ├── coastal_animating_bounds.bin
│   │   │   ├── coastal_sky.bin
│   │   │   ├── compound_animating_bounds.bin
│   │   │   ├── compound_cine.bin
│   │   │   ├── compound_textures.bin
│   │   │   ├── crash_pickup_meshes.bin
│   │   │   ├── crash_sky.bin
│   │   │   ├── cut1_setup.bin
│   │   │   ├── cut2_setup.bin
│   │   │   ├── cut3_setup.bin
│   │   │   ├── cut3_shell.bin
│   │   │   ├── cut4_setup.bin
│   │   │   ├── cut5_setup.bin
│   │   │   ├── cut5_textures.bin
│   │   │   ├── cut6_setup.bin
│   │   │   ├── cut7_setup.bin
│   │   │   ├── cut8_setup.bin
│   │   │   ├── cut9_setup.bin
│   │   │   ├── cut11_setup.bin
│   │   │   ├── cut12_setup.bin
│   │   │   ├── drill_collision.bin
│   │   │   ├── flamethrower_sfx.bin
│   │   │   ├── font.bin
│   │   │   ├── ganges_door131_frames.bin
│   │   │   ├── globe_model.bin
│   │   │   ├── gym_sky.bin
│   │   │   ├── india_sky.bin
│   │   │   ├── lara_animations.bin
│   │   │   ├── lara_extra.bin
│   │   │   ├── lara_guns.bin
│   │   │   ├── lara_gym_guns.bin
│   │   │   ├── lara_outfits.bin
│   │   │   ├── london_sky.bin
│   │   │   ├── luds_diver_animation.bin
│   │   │   ├── luds_textures.bin
│   │   │   ├── menu_artefacts.bin
│   │   │   ├── mines_textures.bin
│   │   │   ├── misc_sprites.bin
│   │   │   ├── nevada_door132_frames.bin
│   │   │   ├── nevada_sky.bin
│   │   │   ├── pda_model.bin
│   │   │   ├── pickup_aid.bin
│   │   │   ├── puna_pickup_meshes.bin
│   │   │   ├── rapids_sky.bin
│   │   │   ├── reunion_flames.bin
│   │   │   ├── scotland_sky.bin
│   │   │   ├── stpaul_animating_bounds.bin
│   │   │   ├── stpaul_textures.bin
│   │   │   ├── tinnos_cameras.bin
│   │   │   ├── tinnos_flames.bin
│   │   │   ├── undersea_animating_bounds.bin
│   │   │   ├── undersea_train.bin
│   │   │   ├── willsden_heli.bin
│   │   │   └── zoo_train.bin
│   │   ├── levels
│   │   │   ├── antarc.tr2
│   │   │   ├── area51.tr2
│   │   │   ├── chamber.tr2
│   │   │   ├── city.tr2
│   │   │   ├── compound.tr2
│   │   │   ├── crash.tr2
│   │   │   ├── house.tr2
│   │   │   ├── jungle.tr2
│   │   │   ├── mines.tr2
│   │   │   ├── nevada.tr2
│   │   │   ├── office.tr2
│   │   │   ├── quadchas.tr2
│   │   │   ├── rapids.tr2
│   │   │   ├── roofs.tr2
│   │   │   ├── sewer.tr2
│   │   │   ├── shore.tr2
│   │   │   ├── stpaul.tr2
│   │   │   ├── temple.tr2
│   │   │   ├── title.tr2
│   │   │   ├── tonyboss.tr2
│   │   │   ├── tower.tr2
│   │   │   └── triboss.tr2
│   │   ├── scripts
│   │   │   ├── area51.lua
│   │   │   ├── compound.lua
│   │   │   ├── crash.lua
│   │   │   ├── cut8.lua
│   │   │   ├── jungle.lua
│   │   │   ├── mines.lua
│   │   │   ├── tower.lua
│   │   │   └── zoo.lua
│   │   ├── catalog_item_actions.csv
│   │   ├── catalog_lara_anims.csv
│   │   ├── catalog_lara_states.csv
│   │   ├── catalog_music.csv
│   │   ├── catalog_objects.csv
│   │   ├── catalog_samples.csv
│   │   ├── gameflow.json5
│   │   ├── inv_ring.json5
│   │   ├── main.sfx
│   │   ├── strings-de.json5
│   │   ├── strings-it.json5
│   │   ├── strings-pl.json5
│   │   ├── strings.json5
│   │   ├── tombpc.dat
│   │   └── weapons.json5
│   ├── tr3-la
│   │   ├── levels
│   │   │   ├── chunnel.tr2
│   │   │   ├── scotland.tr2
│   │   │   ├── slinc.tr2
│   │   │   ├── title.tr2
│   │   │   ├── undersea.tr2
│   │   │   ├── willsden.tr2
│   │   │   └── zoo.tr2
│   │   ├── gameflow.json5
│   │   ├── main.sfx
│   │   ├── strings-de.json5
│   │   ├── strings-it.json5
│   │   ├── strings-pl.json5
│   │   └── strings.json5
│   └── tr3-level
│       ├── gameflow.json5
│       ├── strings-it.json5
│       ├── strings-pl.json5
│       └── strings.json5
└── TRX.exe
*\* Will not be present until the game has been launched.* ## Playing the game - To play the last selected game or expansion, run `TRX.exe`. - To launch a specific base game from a shortcut, use one of the following commands: - `TRX.exe --mod tr1` - `TRX.exe --mod tr2` - `TRX.exe --mod tr3` - To launch a specific expansion pack from a shortcut, use one of the following commands: - `TRX.exe --mod tr1-ub` - `TRX.exe --mod tr2-gm` - `TRX.exe --mod tr3-la` ## Migrating from previous TRX installations If TRX shows an error like `Mixed mod layout detected: found legacy mod data`, your install contains a mix of the old layout and the new one. Older combined builds stored mod definitions under `cfg//`, with the matching game files in top-level folders such as `data/`, `fmv/`, `music/`, `audio/`, and `cuts/`. Current combined builds keep each game together under `games//`. That means each game now has its own directory, such as: - `games/tr1/` - `games/tr2/` - `games/tr3/` Inside each of those folders you will find the game's own files, for example `levels/`, `fmv/`, `music/`, `audio/`, `cuts/`, and the mod's `gameflow.json5`. To fix the error: 1. Keep the shared `cfg/` directory from the current combined TRX build. 2. Remove any legacy per-game folders from `cfg/`, such as: - `cfg/tr1/`, - `cfg/tr1-demo-pc`, - `cfg/tr1-level/`, - `cfg/tr1-ub/`, - `cfg/tr2/`, - `cfg/tr2-gm/`, - `cfg/tr2-level/`, - `cfg/tr3/`, - `cfg/tr3-la/`, - `cfg/tr3-level/`. 3. Move or re-copy your original game files into the matching `games//` folders listed above. 4. Do not keep the same game in both places at once. For example, if you use `games/tr1/`, do not also keep `cfg/tr1/` and `data/`. If you are updating from an older combined install, the simplest fix is usually to start from a fresh extract of the latest TRX zip and then copy your original game files into `games//` again. # macOS ## Installing macOS packages still use the per-game hierarchy. The combined zip layout documented above does not apply to macOS yet. Use the existing guides instead: - [TR1 installing guide](../tr1/INSTALLING.md) - [TR2 installing guide](../tr2/INSTALLING.md) - [TR3 installing guide](../tr3/INSTALLING.md) ================================================ FILE: docs/trx/LEVELS.md ================================================ --- title: Level reference order: 9 --- # Level reference ## Tomb Raider I | # | Title | File | Pickups | Kills | Secrets | Save Crystals | | -- | ---------------------- | -------------- | --------------------- | --------------------- | --------------- | ------------- | | 0 | Lara's Home | `gym.phd` | - | - | - | - | | 1 | Caves | `level1.phd` | 7 | 14 | 3 | 1 | | 2 | City of Vilcabamba | `level2.phd` | 13 | 29 | 3 | 2 | | 3 | Lost Valley | `level3a.phd` | 16 | 13 | 5 | 3 | | 4 | Tomb of Qualopec | `level3b.phd` | 8 | 7 / 8[1] | 3 | 3 | | 5 | St. Francis' Folly | `level4.phd` | 19 | 23 | 4 | 5 | | 6 | Colosseum | `level5.phd` | 14 | 26 / 27[2] | 3 | 4 | | 7 | Palace Midas | `level6.phd` | 22 / 23[3] | 43 | 3 | 8 | | 8 | The Cistern | `level7a.phd` | 28 | 34 | 3 | 5 | | 9 | Tomb of Tihocan | `level7b.phd` | 26 | 17 | 2 | 5 | | 10 | City of Khamoon | `level8a.phd` | 24 | 14 / 15[4] | 3 | 4 | | 11 | Obelisk of Khamoon | `level8b.phd` | 38 | 16 | 3 | 7 | | 12 | Sanctuary of the Scion | `level8c.phd` | 29 | 15 | 1 | 7 | | 13 | Natla's Mines | `level10a.phd` | 30 | 3 | 3 | 7 | | 14 | Atlantis | `level10b.phd` | 50 | 32 | 3 | 7 | | 15 | The Great Pyramid | `level10c.phd` | 31 | 6 | 3[5] | 4 | | - | Total | - | 355 | 294 / 295 | 45 | 72 | - [1]: extra mummy kill in the Tomb of Qualopec. - [2]: bat with a missing trigger in the OG. - [3]: unobtainable medipack in the Midas hub. - [4]: additional mummy in the PS1 version. - [5]: broken secret trigger in some versions of the game. ## Tomb Raider I: Unfinished Business | # | Title | File | Pickups | Kills | Secrets | | -- | -------------------- | ----------- | ------- | ----- | ------- | | 1 | Return to Egypt | `egypt.phd` | 53 | 41 | 3 | | 2 | Temple of the Cat | `cat.phd` | 63 | 44 | 4 | | 3 | Atlantean Stronghold | `end.phd` | 63 | 31 | 2 | | 4 | The Hive | `end2.phd` | 60 | 41 | 1 | | - | Total | - | 239 | 157 | 10 | ## Tomb Raider II | # | Title | File | Pickups | Kills | Secrets | | -- | ------------------------ | -------------- | --------------------- | --------------------- | ------- | | 0 | Lara's Home | `assault.tr2` | 1 | - | - | | 1 | The Great Wall | `wall.tr2` | 14 | 23 | 3 | | 2 | Venice | `boat.tr2` | 30 | 24 | 3 | | 3 | Bartoli's Hideout | `venice.tr2` | 28 | 37 | 3 | | 4 | Opera House | `opera.tr2` | 37 | 46 | 3 | | 5 | Offshore Rig | `rig.tr2` | 31 | 20 | 3 | | 6 | Diving Area | `platform.tr2` | 39 | 34 | 3 | | 7 | 40 Fathoms | `unwater.tr2` | 14 | 16 | 3 | | 8 | Wreck of the Maria Doria | `keel.tr2` | 41 | 35 / 36[1] | 3 | | 9 | Living Quarters | `living.tr2` | 16 | 21 | 3 | | 10 | The Deck | `deck.tr2` | 35 | 30 | 3 | | 11 | Tibetan Foothills | `skidoo.tr2` | 31 | 33 | 3 | | 12 | Barkhang Monastery | `monastry.tr2` | 44 | 30 / 52[2] | 3 | | 13 | Catacombs of the Talion | `catacomb.tr2` | 39 | 33 | 3 | | 14 | Ice Palace | `icecave.tr2` | 33 | 22 | 3 | | 15 | Temple of Xian | `emprtomb.tr2` | 39 / 40[3] | 37 | 3 | | 16 | Floating Islands | `floating.tr2` | 39 | 26 | 3 | | 17 | The Dragon's Lair | `xian.tr2` | 25 | 11 | - | | 18 | Home Sweet Home | `house.tr2` | 45 | 16 | - | | - | Total | - | 580 | 494 / 516 | 48 | - [1]: shark in an unreachable room (possible to kill with extreme patience) - [2]: hostiles vs. hostiles and allies - [3]: unobtainable medipack above the waterfall lake ## Tomb Raider II: The Golden Mask | # | Title | File | Pickups | Kills | Secrets | | -- | ------------------- | ------------ | --------------------- | --------------------- | --------- | | 1 | The Cold War | `level1.tr2` | 71 | 39 / 44[1] | 3 | | 2 | Fool's Gold | `level2.tr2` | 69 | 62 | 3 | | 3 | Furnace of the Gods | `level3.tr2` | 64 | 38 / 41[1] | 3 | | 4 | Kingdom | `level4.tr2` | 50 / 52[2] | 25 / 29[1] | 3 | | 5 | Nightmare in Vegas | `level5.tr2` | 75 | 23 | 3 | | - | Total | - | 329 / 331 | 187 / 199 | 15 | - [1]: hostiles vs. hostiles and allies - [2]: additional drops from allies ================================================ FILE: docs/trx/MIGRATING.md ================================================ --- title: Migrating levels order: 3 --- # Migration guide for level builders ## TRX ### Version 1.5 to 1.6 1. **TR1 and TR2 blood catalog names were renamed**: In `cfg/catalog_objects.csv`, update old symbols to the new names: - `O_BLOOD_1` → `O_BLOOD` This also affects catalog-derived Lua names (`trx.catalog.objects`): - `blood_1` → `blood` 2. **Update weapon ammo quantities**: In `weapons.json5`, the old `pickup_qty` and `pickup_qty_alt` fields have been reorganized under a new nested `ammo` object. This lets weapon pickups grant a different amount of ammo than their matching ammo pickups. To match the previous setup: 1. Open `weapons.json5`. 2. For each weapon entry: - Create a nested `ammo` object if it doesn't already exist. - Move the value from `pickup_qty` into both `ammo.initial_qty` and `ammo.pickup_qty` fields. 3. If the weapon had a `pickup_qty_alt` field (e.g. flares): - Move that value into `ammo.pickup_qty_alt`. 4. Remove the old `pickup_qty` and `pickup_qty_alt` fields. 3. **TR1 Atlantean catalog names were changed**: In `cfg/catalog_objects.csv`, update old symbols to the new names: - `O_WARRIOR_1` → `O_ATLANTEAN_WINGED` - `O_WARRIOR_2` → `O_ATLANTEAN_SHOOTER` - `O_WARRIOR_3` → `O_ATLANTEAN_GROUND` This also affects catalog-derived Lua names (`trx.catalog.objects`): - `warrior_1` → `atlantean_winged` - `warrior_2` → `atlantean_shooter` - `warrior_3` → `atlantean_ground` ### Version 1.4 to 1.5 1. **Update TR2 detonator box** Dynamic light output when using the detonator is no longer hard-coded and now uses animation commands. The updated OG asset is available to download [here](INJECTIONS.md#builder-workflow-keep-the-codebincode-or-bake-into-your-wad). ### Version 1.3 to 1.4 1. **Update strings file structure** The flat string section has been replaced with nested root sections. Please see shipped string files or documentation for details. ### Version 1.2 to 1.3 1. **TR1 missile catalog names were renamed**: In `cfg/catalog_objects.csv`, update old missile symbols to the new names: - `O_MISSILE_1` → `O_NATLA_GUN` - `O_MISSILE_2` → `O_MISSILE_ATLANTEAN_SHARD` - `O_MISSILE_3` → `O_MISSILE_ATLANTEAN_BOMB` - `O_MISSILE_4` and `O_MISSILE_5` are no longer used and should be removed. This also affects catalog-derived Lua names (`trx.catalog.objects`): - `missile_1` → `natla_gun` - `missile_2` → `missile_atlantean_shard` - `missile_3` → `missile_atlantean_bomb` 2. **TR2 breakable window catalog names were renamed**: In `cfg/catalog_objects.csv`, update old breakable windows to the new names: - `O_WINDOW_1` → `O_SMASH_OBJECT_1` - `O_WINDOW_2` → `O_SMASH_OBJECT_2` This also affects catalog-derived Lua names (`trx.catalog.objects`): - `window_1` → `smash_object_1` - `window_2` → `smash_object_2` 3. **Flooding flip effect sound ID was changed**: In `cfg/catalog_samples.csv`, add an alias for `SFX_FLOOD`: - TR1: `81, SFX_FLOOD` - TR2: `79, SFX_FLOOD` ### Version 1.1 to 1.2 1. **Lara skin system**: Lara's outfit must now be defined using additional skin objects, along with game-flow and JSON setup. Refer to [outfits documentation](OUTFITS.md). 2. **Lua event name cleanup**: The following events got new names: - `on_level_init` → `before_level_file` - `on_level_start` → `after_level_file` - `on_level_load` → `after_level_state` - `on_control` → `before_control` - `on_control_post` → `after_control` 3. **Lua objects catalog name cleanup**: All keys in `trx.catalog.objects` had their `O_` prefix removed and were converted to lowercase. Before: `trx.catalog.objects.O_BANDIT_1` After: `trx.catalog.objects.bandit_1` 4. **Savegame file pattern rename**: Replace `savegame_fmt_bson` with `savegame_file_fmt` in game flow files. The old `savegame_fmt_bson` key is still accepted but logs a warning and is scheduled for removal in TRX 1.5. 5. **Legacy savegame pattern removed**: Remove the `savegame_fmt_legacy` key from game flow files. ### Version 1.0 to 1.1 1. **Ally and ally target behavior moved to Lua**: Monks being allies and bandits being enemies who will target allies is no longer hardcoded and instead must be defined in Lua. Refer to the game flow and linked script files of the original levels for reference. ## TR1X ### Version 4.15 to TRX 1.0 1. **Game flow options moved to the config module**: Certain settings are no longer part of the game flow spec and instead became hidden player settings. To change them, put them in the `enforced_config` section. List of the affected settings: - `demo_delay` - `enable_killer_pushblocks` 2. **Lara shotgun animation**: Lara now uses the TR2+ approach of a separate shotgun mesh on her back. You must use the `lara_guns.bin` injection or otherwise refer to https://github.com/LostArtefacts/TRXInjectionTool/blob/main/docs/ASSETS.md 3. **Lara extra animations**: Lara now uses the TR2+ approach of having defined state changes for extra animations (scion pickups, Midas touch etc). You must use the `lara_extra.bin` injection or otherwise refer to https://github.com/LostArtefacts/TRXInjectionTool/blob/main/docs/ASSETS.md ### Version 4.13 to 4.14 1. **Update file paths** - Move and rename the `cfg/TR1X_gameflow.json5` file to `cfg/tr1/gameflow.json5`. - Move and rename the `cfg/TR1X_strings*.json5` files to `cfg/tr1/strings*.json5`. - Move and rename the `cfg/TRX_common_strings*.json5` files to `cfg/base_strings*.json5`. - Remove leftover `TR1X_strings_ub.json5`. This is how the directory should look: ``` . └── cfg    ├── base_strings.json5    ├── base_strings-pl.json5 (in case you want to provide translation files)    ├── base_strings-….json5 (in case you want to provide translation files)    ├── tr1    │   ├── gameflow.json5    │   ├── strings.json5    │   ├── strings-pl.json5 (in case you want to provide translation files)    │   └── strings-….json5 (in case you want to provide translation files)    └── poses.json5 ``` ### Version 4.9 to 4.10 1. **Update fog configuration** If you wish to force your fog settings on player: - Rename `draw_distance_fade` to `fog_start` - Rename `draw_distance_max` to `fog_end` If you wish to give the player agency to change the fog: - Remove `draw_distance_fade` and `draw_distance_max` ### Version 4.7 to 4.8 1. **Rename basic keys** - Replace `file` key with `path` for every level. - Replace `music` key with `music_track` for every level. 2. **Update level enumeration structure**: - The `"type": "title"` property is no longer supported. Instead, the title level needs to be placed in the top-level `"title"` key. - The `"type": "cutscene"` property is no longer supported. Instead, the cutscenes need to be placed in the top-level `"cutscenes"` array. - All FMVs need to be placed in its own top-level `"fmvs"` array. 3. **Update individual level sequences** - `start_game` should be removed. - `exit_to_cine` should be removed. - `exit_to_level` should be replaced with `level_complete`. No parameter needed. - `display_picture` no longer takes a `picture_path` argument and instead just takes a `path`. - `loading_screen` no longer takes a `picture_path` argument and instead just takes a `path`. - `level_stats` no longer takes a `level_id` argument. - `total_stats` no longer takes a `picture_path` argument and instead takes a `background_path`. - `play_fmv` no longer takes a `fmv_path` argument and instead takes a `fmv_id`. - `play_synced_audio` is renamed to `play_music` and takes a `music_track` argument rather than `audio_id`. 4. **Update strings** The game strings are now placed in a separate file, `TR1X_strings.json5` in preparation to eventually support internationalization. Elements such as item titles or item names need to be configured entirely in the new file, so all `"strings"` keys can be safely removed from the game flow. Refer to [game strings documentation](4-GAME_STRINGS.md) for more details. ## TR2X ### Version 1.5 to TRX 1.0 1. **Game flow options moved to the config module**: Certain settings are no longer part of the game flow spec and instead became hidden player settings. To change them, put them in the `enforced_config` section. List of the affected settings: - `lockout_option_ring` - `load_save_disabled` - `play_any_level` - `demo_delay` - `cheat_keys` - `enable_killer_pushblocks` 2. **Removed game flow settings** The following game flow features were removed and are no longer available: - `cmd_init` - `cmd_title` - `cmd_death_in_demo` - `cmd_death_in_game` - `cmd_demo_end` - `cmd_demo_interrupt` - `single_level` - `is_demo_version` 3. **Lara extra animations**: Lara's extra animations have been combined with TR1. You must use the `lara_extra.bin` injection or otherwise refer to https://github.com/LostArtefacts/TRXInjectionTool/blob/main/docs/ASSETS.md 4. **Secret track**: The setting `secret_track` is no longer present – the engine will always play `MX_SECRET` track. To change its slot, please refer to the `catalog_music.csv` file. ### Version 1.3 to 1.4 1. **Update file paths** - Move and rename the `cfg/TR2X_gameflow.json5` file to `cfg/tr2/gameflow.json5`. - Move and rename the `cfg/TR2X_strings*.json5` files to `cfg/tr2/strings*.json5`. - Move and rename the `cfg/TRX_common_strings*.json5` files to `cfg/base_strings*.json5`. - Remove leftover `TR2X_strings_ub.json5`. This is how the directory should look: ``` . └── cfg    ├── base_strings.json5    ├── base_strings-pl.json5 (in case you want to provide translation files)    ├── base_strings-….json5 (in case you want to provide translation files)    ├── tr2    │   ├── gameflow.json5    │   ├── strings.json5    │   ├── strings-pl.json5 (in case you want to provide translation files)    │   └── strings-….json5 (in case you want to provide translation files)    └── poses.json5 ``` ### Version 1.2 to 1.3 1. **Rename objects** - Replace `"detonator_1"` with `"gong"`. - Replace `"detonator_2"` with `"detonator_box"`. 2. **Re-add pistols** Pistols are no longer added automatically to a level that follows one in which Lara previously lost her weapons. A game flow entry to re-add pistols will be required - refer to the Diving Area level in the default game flow. 3. **Bears, wolves and ice warriors** If you wish to use the bear, wolf or ice warrior (monk with no shadow) from The Golden Mask while still being able to use big spiders, small spiders and other monks, use the following object slots. - Bear: slot 265 - Wolf: slot 266 - Ice warrior: slot 267 4. **Disabling gym** The option `gym_enabled` is no longer available. If you need to remove the access to Lara's Home, please either remove the relevant level from the game flow (this may break existing saves), or change its type to `"dummy"` to get it ignored (this will work with existing saves). ### Version 1.0.2 to 1.1 1. **Update first level inventory allocation** The first level no longer hard-codes the shotgun, flare and small/large medi pack allocations. To continue to have Lara start with these items, refer to the shipped game flow file's `Great Wall` sequences, specifically the `give_item` entries. ================================================ FILE: docs/trx/MUSIC.md ================================================ --- title: Music track IDs order: 10 --- # Music track IDs ### Tomb Raider 1 | ID | Description | | --- | --- | | `0` | Null (antipads and antitriggers with this track ID will stop current track) | | `1` | Unused | | `2` | _Tomb Raider Theme_ | | `3` | _Where the Depths Unfold_ (part 1) | | `4` | _Tomb Raider Theme_ (alternative mix) | | `5` | Unused | | `6` | _Time to Run_ | | `7` | _Friend Since Gone_ | | `8` | _The T-Rex_ (part 1) | | `9` | _A Long Way Down_ | | `10` | _Longing for Home_ | | `11` | _Spooky_ (part 1) | | `12` | _Keep your balance_ | | `13` | Secret sound | | `14` | _Spooky_ (part 3) | | `15` | _Where the Depths Unfold_ (part 2) | | `16` | _The T-Rex_ (part 2) | | `17` | _Where the Depths Unfold_ (part 3) | | `18` | _Where the Depths Unfold_ (part 4) | | `19` | _Tomb Raider Theme_ (alternative mix 2) | | `20` | _Time to Run_ (part 2) | | `21` | _Longing for Home_ (alternative mix) | | `22` | Natla's fall cutscene | | `23` | Larson cutscene | | `24` | Natla placing Scion cutscene | | `25` | Lara in Tomb of Tihocan cutscene | | `26` | Gym hint "Welcome to my home. I'll take you on a guided tour." | | `27` | Gym hint "Use the D-Pad to go to the music room." | | `28` | Gym hint "OK. Let's do some tumbling. Press the Jump button." | | `29` | Gym hint "Now press it again and quickly press one of the directions, and I'll jump that way." | | `30` | Gym hint "Ah, the main hall. Sorry about the crates. I'm having some things put into storage, and the delivery people haven't been yet." | | `31` | Gym hint "Run up to a crate, and while still pressing Forward, press Action, and I'll vault up onto it." | | `32` | Gym hint "This used to be the ball room, but I've converted it into my own personal gym. What do you think? Well, let's do some exercises!" | | `33` | Gym hint "I don't actually run everywhere. When I want to be careful, I walk. Hold down the Walk button, and walk to the white line." | | `34` | Gym hint "With the Walk button down, I won't fall off even if you try to make me. Go on, try it." | | `35` | Gym hint "If you want to look around, press and hold the Look button. Then press in the direction where you want to look." | | `36` | Gym hint "If a jump is too far for me, I can grab the ledge and save myself from a nasty fall. Walk to the edge with the white line until I won't go any further. Then press Jump immediately followed by Forward, and while I'm in the air, press and hold the Action button." | | `37` | Gym hint "Press Forward, and I'll climb up." | | `38` | Gym hint "If I do a running jump, I can make a jump like that, no problem." | | `39` | Gym hint "Walk to the edge with a white line until I stop. Then let go of Walk, and tap Backwards to give me a run-up. Press Forward, and almost immediately press and hold the Jump button. I won't actually jump until the last minute." | | `40` | Gym hint "Right, this is a really big one. So do a running jump exactly as before, except while I'm in the air, press and hold the Action button to make me grab the edge." | | `41` | Gym hint "Nice!" | | `42` | Gym hint "Try to vault up here. Press Forward, and hold Action." | | `43` | Gym hint "I can't climb up because the gap is too small. But press Right, and I'll shimmy sideways until there is room. Then press Forward." | | `44` | Gym hint "Great! If there is a long jump and I don't want to hurt myself jumping off, I can let myself down carefully." | | `45` | Gym hint "Tap Backwards, and I'll jump off Backwards. Immediately press and hold the Action button, and I'll grab the ledge on the way down." | | `46` | Gym hint "Then let go." | | `47` | Gym hint "Let's go for a swim!" | | `48` | Gym hint "The Jump button and the directions move me around underwater." | | `49` | Gym hint "Oh, air! Just use Forward, and Left and Right, to manoeuvre around on the surface. Press Jump to dive down for another swim-about, or go to the edge and press Action to climb out." | | `50` | Gym hint "Right. Now I'd better take off these wet clothes." | | `51` | Baldy's speech "Say cheese!" | | `52` | Cowboy's speech "Ain't nothing personal." | | `53` | Larson's speech "I still got a pain in my brain from ya – and it's telling me funny ideas now, like to shoot you to hell!" | | `54` | Natla's speech "You can't bump off me and my brood so easily, Lara!" | | `55` | Pierre's speech "A little late for the prize giving, no? Still, it is the taking part which counts." | | `56` | Skate kid's speech "You firing at me? You firing at me, huh? Ain't nobody else here, so you must be firing at me!" | | `57` | Caves ambience | | `58` | Cistern ambience | | `59` | Windy ambience | | `60` | Atlantis ambience | ### Tomb Raider 2 | ID | Description | | --- | --- | | `2` | Cutscene (The Great Wall) | | `3` | Cutscene (Opera House) | | `4` | Cutscene (Brother Chan) | | `5` | Gym hint "Welcome back. After that grueling business last year I decided to build this assault course to hone my skills… and learn some new ones." | | `6` | Gym hint "No, that's not right. You need to press Jump and Forward together for me to clear the gap. Run back to the start and try again." | | `7` | Gym hint "For bigger gaps I need to do a running jump. Back to the start." | | `8` | Gym hint "To climb up, press Forward and hold down the Action button." | | `9` | Gym hint "To avoid falling off the ledges, you can use the Walk button. That way, I'm more careful and won't step over the edge." | | `10` | Gym hint "To make that jump successfully, I need to walk back as far as possible from the edge, then take a running jump as before. Or you could walk to the edge, jump forward, and press Action to make me grab the far edge." | | `11` | Gym hint "Okay, that was quite a tough one. You need to make a running jump, and then press Action for me to grab the edge. To get the run-up exactly right, walk to the edge, then tap Backwards to jump back. If you run forwards from there and press Jump, I'll make it just right." | | `12` | Gym hint "I can climb up this wall. Just walk up to it and hold down the Action button. You can then use the direction buttons to climb up, down, and side-to-side." | | `13` | Gym hint "This set of obstacles is easiest to traverse by using my sideways jumps. Just press Left or Right at the same time as Jump. I can jump backwards, too." | | `14` | Gym hint "This is another climbing wall. You can tell by the footholds." | | `15` | Gym hint "Oh, dear. Back to the start if I want to beat my best time." | | `16` | Gym hint "To swim underwater, press the Jump button and the direction buttons to guide me." | | `17` | Gym hint "On the surface, the direction buttons move me about, and the Jump button makes me dive underwater. At the edge of the pool, I can climb out by using the Action button." | | `18` | Gym hint "Great, but nowhere near my best time." | | `19` | Gym hint "Gosh! That was my best time yet!" | | `20` | Gym hint "Almost. Perhaps another try, and I might beat it." | | `21` | Gym hint "Congratulations! You did it! But perhaps I could've been faster." | | `22` | Gym hint "Hi there. Let's see if I can beat my best time." | | `23` | Cutscene (bathroom) | | `24` | Dagger pull | | `25` | Gym hint "Feel free to explore the rest of the house and gardens." | | `26` | Cutscene (Temple of Xian) | | `27` | Caves ambience | | `28` | Sewers ambience | | `29` | Windy ambience | | `30` | Heartbeat ambience | | `31` | Surprise 1 | | `32` | Surprise 2 | | `33` | Surprise 3 | | `34` | Ooh-aah 1 | | `35` | Ooh-aah 2 | | `36` | Venice Violins | | `37` | End of level | | `41` | Harp theme | | `42` | Mystery 1 | | `44` | Ambush 1 | | `45` | Ambush 2 | | `46` | Ambush 3 | | `47` | Ambush 4 | | `48` | Skidoo theme | | `49` | Battle theme | | `50` | Mystery 2 | | `51` | Mystery 3 | | `52` | Mystery 4 | | `53` | Mystery 5 | | `54` | Rig ambience | | `55` | Tomb ambience | | `56` | Ooh-aah 3 | | `57` | Reveal 1 | | `58` | Cutscene (Offshore Rig) | | `59` | Reveal 2 | | `60` | Title theme | | `61` | Unused | ================================================ FILE: docs/trx/OUTFITS.md ================================================ --- title: Lara's outfits order: 16 --- # Outfits In TR1 and TR2 originally, Lara's meshes were taken from the `O_LARA` object, with mesh swaps being performed as required at runtime using additional objects, such as `O_LARA_PISTOL`, `O_LARA_SHOTGUN`, `O_LARA_EXTRA` etc. TR3 moved to a dedicated `O_LARA_SKIN` object, but still depended on the additional gun and extra mesh swaps, and these remained tightly coupled with the level's outfit. For example, when putting a shotgun in Lara's hand, the relevant mesh would include an entire copy of her hand, when in reality only the shotgun was required. This meant if customizing Lara's gloves, the builder would need to do so on several different objects. TRX uses a different skin system, both to allow outfit swaps in-game for players and to remove unnecessary mesh faces where applicable for a more streamlined data setup. Custom level builders can define up to 32 outfits; following is a guide to the data and JSON configuration, and some scenario/workflow examples. ## Data setup The skin system uses the following objects. These are provided in the `lara_outfits.bin` injection, and are available to download as a separate WAD (see [injections](INJECTIONS.md)). #### `O_LARA_SKIN_SWAP_1`...`O_LARA_SKIN_SWAP_32` Each of these should contain a distinct Lara model, with the mesh count and bone order conforming to the standard for Lara. Bone offsets are used (e.g. consider Bacon Lara's different structure); animations are not used. #### `O_LARA_SKIN_SWAP_EXTRA` This object contains various additional meshes for Lara, such as altered torsos when the TR1 braid is in use, Lara's combat face, and meshes used in extra animations, such as pulling the dagger in Dragon's Lair. It also contains both the TR1 and TR2/3 braid. #### `O_LARA_SKIN_SWAP_GUNS` This object contains holsters - both empty and equipped with the various guns - as well as the guns themselves when they are in Lara's hands or on her back. #### `O_LARA_SKIN_SWAP_LEGS` This object contains copies of Lara's legs for each outfit, with holster strap textures removed. This allows levels such as Home Sweet Home to swap out Lara's legs when she has no holsters. It is not an essential object to include. ## JSON setup The file `cfg/outfits.json5` sets up the available outfits, and how they should behave. The structure of this file is described below. #### Top-level overview
Property Type Description
outfits Object map The keys in this map define the available outfits, and these are the keys that should be used in the game-flow. The outfit object is described separately below.
extra_meshes Integer map This map defines mesh offsets in O_LARA_SKIN_SWAP_EXTRA, which are required for various events/paths in the engine.
gun_maps Object array These maps dictate which meshes to use in O_LARA_SKIN_SWAP_GUNS for a given outfit and gun combination.
### Outfits
Show snippet ```json "tr1_classic": { "name_gs": "dynamic/enums/lara_outfit/tr1_classic", "mesh_object": "O_LARA_SKIN_SWAP_2", "is_reflective": false, "gun_map": 0, "combat_face_offset": 1, "supports_sunglasses": true, "braid": { "mode": "BRAID_MODE_TR1_FULL", "mesh_offset": 10, "gold_offset": 16, "hair_pos": { "x": 0, "y": 20, "z": -45, }, }, "no_holster_offsets": { "thigh_r": 1, "thigh_l": 2, }, "extra_outfits": { "LS_EXTRA_TREX_KILL": "tr1_mauled", "LS_EXTRA_MIDAS_KILL": "tr1_golden_lara", }, }, ```
Property Type Description
name_gs String The game string key used for localized UI labels for this outfit (for example, dynamic/enums/lara_outfit/tr1_classic).
mesh_object String Indicates which object contains the outfit's meshes and bones.
is_reflective Boolean Indicates whether or not the outfit is reflective.
gun_map Integer The index into the gun_maps array to use for this outfit.
braid Object The braid setup specific to this outfit. If omitted, no braid will be shown. See the braids section below.
combat_face_offset Integer The mesh offset in O_LARA_SKIN_SWAP_EXTRA for Lara's combat face. -1 implies no combat face swap. This mesh is used when Lara is firing a weapon (traditionally, the O_LARA_UZI head mesh was used).
supports_sunglasses Boolean Defines whether or not sunglasses can be used with this outfit.
no_holster_offsets Integer map The mesh offsets in O_LARA_SKIN_SWAP_LEGS to use when Lara's holsters aren't visible. Omitting this property infers no mesh swaps.
extra_outfits String map Pointers to alternative outfits to use for specific game events. The two supported events are as follows. If these are omitted, no swaps will occur for the events.
LS_EXTRA_TREX_KILL When Lara is killed by the T-rex - instant full outfit swap.
LS_EXTRA_MIDAS_KILL When Lara steps on the Midas hand - progressive outfit swap.
### Braids
Property Type Description
mode String Indicates special handling when the braid is active.
BRAID_MODE_NONE No special treatment (this mode is implied if mode is not specified).
BRAID_MODE_TR1_HEAD_ONLY Replaces Lara's head with EXTRA_MESH_TR1_BRAID_DEFAULT_HEAD defined in the O_LARA_SKIN_SWAP_EXTRA object.
BRAID_MODE_TR1_FULL As per BRAID_MODE_TR1_HEAD_ONLY, plus Lara's torso will be replaced with EXTRA_MESH_TR1_BRAID_DEFAULT_TORSO.
BRAID_MODE_TR1_MAULED As per BRAID_MODE_TR1_FULL, but the torso swap mesh used here is EXTRA_MESH_TR1_BRAID_MAULED_TORSO.
BRAID_MODE_TR1_GOLD As per BRAID_MODE_TR1_FULL, but the swap meshes used here are EXTRA_MESH_TR1_BRAID_GOLD_HEAD and EXTRA_MESH_TR1_BRAID_GOLD_TORSO.
mesh_offset Integer The starting offset in O_LARA_SKIN_SWAP_EXTRA for the regular braid meshes and bones.
gold_offset Integer The starting offset in O_LARA_SKIN_SWAP_EXTRA for the golden braid meshes and bones.
hair_pos XYZ The position relative to Lara's head where the braid will be drawn.
### Guns
Show snippet ```json { "LGT_DESERT_EAGLE": { "hand_r": 62, "thigh_r": 9, }, "LGT_UZIS": { "hand_r": 63, "hand_l": 64, "thigh_r": 10, "thigh_l": 11, }, "LGT_SHOTGUN": { "hand_r": 65, "torso": 72, }, } ```
The map keys must match known engine weapons. See [weapons](WEAPONS.md) and `cfg/weapons.json5` for reference. Any entry may omit hand, thigh or torso, and note that specific gun types will only look for particular entries. For example, defining a `thigh_l` property for `LGT_SHOTGUN` is meaningless and will be ignored. Any missing fields imply that no mesh is drawn for that slot.
Property Type Description
hand_r
hand_l
Integer The mesh offset in O_LARA_SKIN_SWAP_GUNS for the gun to draw in Lara's hands.
thigh_r
thigh_l
Integer The mesh offset in O_LARA_SKIN_SWAP_GUNS for the gun to draw against Lara's thighs. This expects the holster to be part of the mesh.
torso Integer The mesh offset in O_LARA_SKIN_SWAP_GUNS for the gun to draw on Lara's back.
## Custom level use cases > I don't need to customize Lara's outfit, and I don't mind players freely switching outfits. In this scenario, simply ship your level with `lara_outfits.bin` and the default `cfg/outfits.json5`. *** > I don't need to customize Lara's outfit, nor do I want the player to change it. In this case, you can simply enforce the outfit option, and continue to ship the standard files as above. Provided your game-flow level has an accurate `lara_outfit` entry, you can enforce as follows. ```json "enforced_config": { "lara_outfit": null, }, ``` Using null here means that if your second level uses a different outfit, that will still be honoured. *** > I don't need to customize Lara's outfits, but I want to restrict the ones that can be selected by the player. Ship the default injection file, but edit `cfg/outfits.json5` by removing the entries from the `outfits` section that you do not need. *** > I want to customize Lara's outfits. Download the provided TRX WAD to access the data included in the shipped injection. Move each of the relevant objects to your WAD. You can then proceed to edit the meshes as required. Ensure that you remove the `lara_outfits.bin` from your game-flow, otherwise these will override the data in your level. > I want to customize Lara's braid. Braid meshes and bones are taken from the `O_LARA_SKIN_SWAP_EXTRA` object, so to customize it you will need to follow the same steps as for customizing outfits i.e. import the TRX WAD, edit the data and remove the injection. ================================================ FILE: docs/trx/SAMPLES.md ================================================ --- title: Sample IDs order: 11 --- # Sample IDs ### Tomb Raider 1 | ID | Description | | --- | --- | | `0` | SFX_LARA_FEET | | `1` | SFX_LARA_CLIMB_2 | | `2` | SFX_LARA_NO | | `3` | SFX_LARA_SLIPPING | | `4` | SFX_LARA_LAND | | `5` | SFX_LARA_CLIMB_1 | | `6` | SFX_LARA_DRAW | | `7` | SFX_LARA_HOLSTER | | `8` | SFX_LARA_PISTOLS | | `9` | SFX_LARA_RELOAD | | `10` | SFX_LARA_RICOCHET | | `11` | SFX_BEAR_GROWL | | `12` | SFX_BEAR_FEET | | `13` | SFX_BEAR_ATTACK | | `14` | SFX_BEAR_SNARL | | `15` | SFX_LARA_WET_FEET | | `16` | SFX_BEAR_HURT | | `17` | SFX_LARA_WADE | | `18` | SFX_BEAR_DEATH | | `19` | SFX_WOLF_JUMP | | `20` | SFX_WOLF_HURT | | `22` | SFX_WOLF_DEATH | | `24` | SFX_WOLF_HOWL | | `25` | SFX_WOLF_ATTACK | | `26` | SFX_LARA_CLIMB_3 | | `27` | SFX_LARA_BODYSL | | `28` | SFX_LARA_SHIMMY_2 | | `29` | SFX_LARA_JUMP | | `30` | SFX_LARA_FALL | | `31` | SFX_LARA_INJURY | | `32` | SFX_LARA_ROLL | | `33` | SFX_LARA_SPLASH | | `34` | SFX_LARA_GETOUT | | `35` | SFX_LARA_SWIM | | `36` | SFX_LARA_BREATH | | `37` | SFX_LARA_BUBBLES | | `38` | SFX_LARA_SWITCH | | `39` | SFX_LARA_KEY | | `40` | SFX_LARA_OBJECT | | `41` | SFX_LARA_GENERAL_DEATH | | `42` | SFX_LARA_KNEES_DEATH | | `43` | SFX_LARA_UZI_FIRE | | `44` | SFX_LARA_MAGNUMS | | `45` | SFX_LARA_SHOTGUN | | `46` | SFX_LARA_BLOCK_PUSH_1 | | `47` | SFX_LARA_BLOCK_PUSH_2 | | `48` | SFX_LARA_EMPTY | | `50` | SFX_LARA_BULLETHIT | | `51` | SFX_LARA_BLKPULL | | `52` | SFX_LARA_FLOATING | | `53` | SFX_LARA_FALLDETH | | `54` | SFX_LARA_GRABHAND | | `55` | SFX_LARA_GRABBODY | | `56` | SFX_LARA_GRABFEET | | `57` | SFX_LARA_SWITCHUP | | `58` | SFX_BAT_SQK | | `59` | SFX_BAT_FLAP | | `60` | SFX_UNDERWATER | | `61` | SFX_UNDERWATER_SWITCH | | `63` | SFX_BLOCK_SOUND | | `64` | SFX_DOOR | | `65` | SFX_PENDULUM_BLADES | | `66` | SFX_ROCK_FALL_CRUMBLE | | `67` | SFX_ROCK_FALL_FALL | | `68` | SFX_ROCK_FALL_LAND | | `69` | SFX_T_REX_DEATH | | `70` | SFX_T_REX_STOMP | | `70` | SFX_PUSHBLOCK_LAND | | `70` | SFX_EARTHQUAKE_2 | | `71` | SFX_T_REX_ROAR | | `72` | SFX_T_REX_ATTACK | | `73` | SFX_RAPTOR_ROAR | | `74` | SFX_RAPTOR_ATTACK | | `75` | SFX_RAPTOR_FEET | | `76` | SFX_MUMMY_GROWL | | `77` | SFX_LARSON_FIRE | | `78` | SFX_LARSON_RICOCHET | | `79` | SFX_WATERFALL_LOOP | | `80` | SFX_WATER_LOOP | | `81` | SFX_WATERFALL_BIG | | `82` | SFX_CHAINDOOR_UP | | `83` | SFX_CHAINDOOR_DOWN | | `84` | SFX_COGS | | `85` | SFX_LION_HURT | | `86` | SFX_LION_ATTACK | | `87` | SFX_LION_ROAR | | `88` | SFX_LION_DEATH | | `89` | SFX_GORILLA_FEET | | `90` | SFX_GORILLA_PANT | | `91` | SFX_GORILLA_DEATH | | `92` | SFX_CROC_FEET | | `93` | SFX_CROC_ATTACK | | `94` | SFX_RAT_FEET | | `95` | SFX_RAT_CHIRP | | `96` | SFX_RAT_ATTACK | | `97` | SFX_RAT_DEATH | | `98` | SFX_THUNDER | | `99` | SFX_EXPLOSION_2 | | `100` | SFX_GORILLA_GRUNT | | `101` | SFX_GORILLA_GRUNTS | | `102` | SFX_CROC_DEATH | | `103` | SFX_DAMOCLES_SWORD | | `104` | SFX_EXPLOSION_1 | | `108` | SFX_MENU_ROTATE | | `109` | SFX_MENU_LARA_HOME | | `110` | SFX_MENU_GAMEBOY | | `111` | SFX_MENU_SPININ | | `112` | SFX_MENU_SPINOUT | | `113` | SFX_MENU_COMPASS | | `114` | SFX_MENU_GUNS | | `115` | SFX_MENU_PASSPORT | | `116` | SFX_MENU_MEDI | | `117` | SFX_RAISINGBLOCK_FX | | `118` | SFX_SAND_FX | | `119` | SFX_STAIRS_2_SLOPE_FX | | `120` | SFX_ATLANTEAN_WALK | | `121` | SFX_ATLANTEAN_ATTACK | | `122` | SFX_ATLANTEAN_JUMP_ATTACK | | `123` | SFX_ATLANTEAN_NEEDLE | | `124` | SFX_ATLANTEAN_BALL | | `125` | SFX_ATLANTEAN_WINGS | | `126` | SFX_ATLANTEAN_RUN | | `127` | SFX_SLAMDOOR_CLOSE | | `128` | SFX_SLAMDOOR_OPEN | | `129` | SFX_SKATEBOARD_MOVE | | `130` | SFX_SKATEBOARD_STOP | | `131` | SFX_SKATEBOARD_SHOOT | | `132` | SFX_SKATEBOARD_HIT | | `133` | SFX_SKATEBOARD_START | | `134` | SFX_SKATEBOARD_DEATH | | `135` | SFX_SKATEBOARD_HIT_GROUND | | `136` | SFX_TORSO_HIT_GROUND | | `137` | SFX_TORSO_ATTACK_1 | | `138` | SFX_TORSO_ATTACK_2 | | `139` | SFX_TORSO_DEATH | | `140` | SFX_TORSO_ARM_SWING | | `141` | SFX_TORSO_MOVE | | `142` | SFX_TORSO_HIT | | `143` | SFX_CENTAUR_FEET | | `144` | SFX_CENTAUR_ROAR | | `145` | SFX_LARA_SPIKE_DEATH | | `146` | SFX_LARA_DEATH_3 | | `147` | SFX_ROLLING_BALL_1_ROLL | | `147` | SFX_EARTHQUAKE_1 | | `148` | SFX_LAVA_LOOP | | `149` | SFX_LAVA_FOUNTAIN | | `150` | SFX_LOOP_FOR_SMALL_FIRES | | `151` | SFX_DART | | `152` | SFX_METAL_DOOR_CLOSE | | `153` | SFX_METAL_DOOR_OPEN | | `154` | SFX_ALTAR_LOOP | | `155` | SFX_POWERUP_FX | | `156` | SFX_COWBOY_DEATH | | `157` | SFX_BLACK_GOON_DEATH | | `158` | SFX_LARSON_DEATH | | `159` | SFX_PIERRE_DEATH | | `160` | SFX_NATLA_DEATH | | `161` | SFX_TRAPDOOR_OPEN | | `162` | SFX_TRAPDOOR_CLOSE | | `163` | SFX_ATLANTEAN_EGG_LOOP | | `164` | SFX_ATLANTEAN_EGG_HATCH | | `165` | SFX_DRILL_ENGINE_START | | `166` | SFX_DRILL_ENGINE_LOOP | | `167` | SFX_CONVEYOR_BELT | | `168` | SFX_HUT_LOWERED | | `169` | SFX_HUT_HIT_GROUND | | `170` | SFX_EXPLOSION_FX | | `171` | SFX_ATLANTEAN_DEATH | | `172` | SFX_CHAINBLOCK_FX | | `173` | SFX_SECRET | | `174` | SFX_GYM_HINT_01 | | `175` | SFX_GYM_HINT_02 | | `176` | SFX_GYM_HINT_03 | | `177` | SFX_GYM_HINT_04 | | `178` | SFX_GYM_HINT_05 | | `179` | SFX_GYM_HINT_06 | | `180` | SFX_GYM_HINT_07 | | `181` | SFX_GYM_HINT_08 | | `182` | SFX_GYM_HINT_09 | | `183` | SFX_GYM_HINT_10 | | `184` | SFX_GYM_HINT_11 | | `185` | SFX_GYM_HINT_12 | | `186` | SFX_GYM_HINT_13 | | `187` | SFX_GYM_HINT_14 | | `188` | SFX_GYM_HINT_15 | | `189` | SFX_GYM_HINT_16 | | `190` | SFX_GYM_HINT_17 | | `191` | SFX_GYM_HINT_18 | | `192` | SFX_GYM_HINT_19 | | `193` | SFX_GYM_HINT_20 | | `194` | SFX_GYM_HINT_21 | | `195` | SFX_GYM_HINT_22 | | `196` | SFX_GYM_HINT_23 | | `197` | SFX_GYM_HINT_24 | | `198` | SFX_GYM_HINT_25 | | `199` | SFX_BALDY_SPEECH | | `200` | SFX_COWBOY_SPEECH | | `201` | SFX_LARSON_SPEECH | | `202` | SFX_NATLA_SPEECH | | `203` | SFX_PIERRE_SPEECH | | `204` | SFX_SKATEKID_SPEECH | | `205` | SFX_LARA_SETUP | ### Tomb Raider 2 | ID | Description | | --- | --- | | `0` | SFX_LARA_FEET | | `1` | SFX_LARA_CLIMB_2 | | `2` | SFX_LARA_NO | | `3` | SFX_LARA_SLIPPING | | `4` | SFX_LARA_LAND | | `5` | SFX_LARA_CLIMB_1 | | `6` | SFX_LARA_DRAW | | `7` | SFX_LARA_HOLSTER | | `8` | SFX_LARA_PISTOLS | | `9` | SFX_LARA_RELOAD | | `10` | SFX_LARA_RICOCHET | | `11` | SFX_LARA_FLARE_IGNITE | | `12` | SFX_LARA_FLARE_BURN | | `15` | SFX_LARA_HARPOON_FIRE | | `16` | SFX_LARA_HARPOON_LOAD | | `17` | SFX_LARA_WET_FEET | | `18` | SFX_LARA_WADE | | `20` | SFX_LARA_TREAD | | `21` | SFX_LARA_AUTOS | | `22` | SFX_LARA_HARPOON_LOAD_WATER | | `23` | SFX_LARA_HARPOON_FIRE_WATER | | `24` | SFX_MASSIVE_CRASH | | `25` | SFX_PUSH_SWITCH | | `26` | SFX_LARA_CLIMB_3 | | `27` | SFX_LARA_BODYSL | | `28` | SFX_LARA_SHIMMY | | `29` | SFX_LARA_JUMP | | `30` | SFX_LARA_FALL | | `31` | SFX_LARA_INJURY | | `32` | SFX_LARA_ROLL | | `33` | SFX_LARA_SPLASH | | `34` | SFX_LARA_GETOUT | | `35` | SFX_LARA_SWIM | | `36` | SFX_LARA_BREATH | | `37` | SFX_LARA_BUBBLES | | `38` | SFX_LARA_SWITCH | | `39` | SFX_LARA_KEY | | `40` | SFX_LARA_OBJECT | | `41` | SFX_LARA_GENERAL_DEATH | | `42` | SFX_LARA_KNEES_DEATH | | `43` | SFX_LARA_UZI_FIRE | | `44` | SFX_LARA_UZI_STOP | | `45` | SFX_LARA_SHOTGUN | | `46` | SFX_LARA_BLOCK_PUSH_1 | | `47` | SFX_LARA_BLOCK_PUSH_2 | | `48` | SFX_CLICK | | `49` | SFX_LARA_HIT | | `50` | SFX_LARA_BULLETHIT | | `51` | SFX_LARA_BLKPULL | | `52` | SFX_LARA_FLOATING | | `53` | SFX_LARA_FALLDETH | | `54` | SFX_LARA_GRABHAND | | `55` | SFX_LARA_GRABBODY | | `56` | SFX_LARA_GRABFEET | | `57` | SFX_LARA_SWITCHUP | | `58` | SFX_GLASS_BREAK | | `59` | SFX_WATER_LOOP | | `60` | SFX_UNDERWATER | | `61` | SFX_UNDERWATER_SWITCH | | `62` | SFX_LARA_PICKUP | | `63` | SFX_BLOCK_SOUND | | `64` | SFX_DOOR | | `65` | SFX_SWING | | `66` | SFX_ROCK_FALL_CRUMBLE | | `67` | SFX_ROCK_FALL_LAND | | `68` | SFX_ROCK_FALL_SOLID | | `69` | SFX_ENEMY_FEET | | `70` | SFX_ENEMY_GRUNT | | `71` | SFX_ENEMY_HIT_1 | | `72` | SFX_ENEMY_HIT_2 | | `73` | SFX_ENEMY_DEATH_1 | | `74` | SFX_ENEMY_JUMP | | `75` | SFX_ENEMY_CLIMBUP | | `76` | SFX_ENEMY_CLIMBDOWN | | `77` | SFX_WEAPON_CLATTER | | `78` | SFX_M16_FIRE | | `79` | SFX_WATERFALL_LOOP | | `80` | SFX_SWORD_STATUE_DROP | | `81` | SFX_SWORD_STATUE_LIFT | | `82` | SFX_PORTCULLIS_UP | | `83` | SFX_PORTCULLIS_DOWN | | `84` | SFX_DOG_FEET_1 | | `85` | SFX_BODY_SLAM | | `86` | SFX_DOG_BARK_1 | | `87` | SFX_DOG_FEET_2 | | `88` | SFX_DOG_BARK_2 | | `89` | SFX_DOG_DEATH | | `90` | SFX_DOG_PANT | | `91` | SFX_LEOPARD_FEET | | `92` | SFX_LEOPARD_ROAR | | `93` | SFX_LEOPARD_BITE | | `94` | SFX_LEOPARD_STRIKE | | `95` | SFX_LEOPARD_DEATH | | `96` | SFX_LEOPARD_GROWL | | `97` | SFX_RAT_ATTACK | | `98` | SFX_RAT_DEATH | | `99` | SFX_TIGER_ROAR | | `100` | SFX_TIGER_BITE | | `101` | SFX_TIGER_STRIKE | | `102` | SFX_TIGER_DEATH | | `103` | SFX_TIGER_GROWL | | `104` | SFX_M16_STOP | | `105` | SFX_EXPLOSION_1 | | `106` | SFX_GROWL | | `107` | SFX_SPIDER_JUMP | | `108` | SFX_MENU_ROTATE | | `109` | SFX_MENU_LARA_HOME | | `111` | SFX_MENU_SPININ | | `112` | SFX_MENU_SPINOUT | | `113` | SFX_MENU_STOPWATCH | | `114` | SFX_MENU_GUNS | | `115` | SFX_MENU_PASSPORT | | `116` | SFX_MENU_MEDI | | `117` | SFX_ENEMY_HEELS | | `118` | SFX_ENEMY_FIRE_SILENCER | | `119` | SFX_ENEMY_AH_DYING | | `120` | SFX_ENEMY_OOH_DYING | | `121` | SFX_ENEMY_THUMP | | `122` | SFX_SPIDER_MOVING | | `123` | SFX_LARA_MINI_LOAD | | `124` | SFX_LARA_MINI_LOCK | | `125` | SFX_LARA_MINI_FIRE | | `126` | SFX_SPIDER_BITE | | `127` | SFX_SLAM_DOOR_SLIDE | | `128` | SFX_SLAM_DOOR_CLOSE | | `129` | SFX_EAGLE_SQUAWK | | `130` | SFX_EAGLE_WING_FLAP | | `131` | SFX_EAGLE_DEATH | | `132` | SFX_CROW_CAW | | `133` | SFX_CROW_WING_FLAP | | `134` | SFX_CROW_DEATH | | `135` | SFX_CROW_ATTACK | | `136` | SFX_ENEMY_GUN_COCKING | | `137` | SFX_ENEMY_FIRE_1 | | `138` | SFX_ENEMY_FIRE_TWIRL | | `139` | SFX_ENEMY_HOLSTER | | `140` | SFX_ENEMY_BREATH_1 | | `141` | SFX_ENEMY_CHUCKLE | | `142` | SFX_MONK_POY | | `143` | SFX_MONK_DEATH | | `145` | SFX_LARA_SPIKE_DEATH | | `146` | SFX_LARA_DEATH_3 | | `147` | SFX_ROLLING_BALL | | `148` | SFX_SANDBAG_SNAP | | `149` | SFX_SANDBAG_HIT | | `150` | SFX_LOOP_FOR_SMALL_FIRES | | `152` | SFX_SKIDOO_START | | `153` | SFX_SKIDOO_IDLE | | `154` | SFX_SKIDOO_ACCELERATE | | `155` | SFX_SKIDOO_MOVING | | `156` | SFX_SKIDOO_STOP | | `157` | SFX_ENEMY_FIRE_2 | | `158` | SFX_ENEMY_DEATH_2 | | `159` | SFX_ENEMY_BREATH_2 | | `160` | SFX_STICK_TAP | | `161` | SFX_TRAPDOOR_OPEN | | `162` | SFX_TRAPDOOR_CLOSE | | `163` | SFX_YETI_GROWL | | `164` | SFX_YETI_CHEST_BEAT | | `165` | SFX_YETI_THUMP | | `166` | SFX_YETI_GRUNT_1 | | `167` | SFX_YETI_SCREAM | | `168` | SFX_YETI_DEATH | | `169` | SFX_YETI_GROWL_1 | | `170` | SFX_YETI_GROWL_2 | | `171` | SFX_YETI_GRUNT_2 | | `172` | SFX_YETI_GROWL_3 | | `173` | SFX_YETI_FEET | | `174` | SFX_ENEMY_HEAVY_BREATH | | `175` | SFX_ENEMY_FLAMETHROWER_FIRE | | `176` | SFX_ENEMY_FLAMETHROWER_SCRAPE | | `177` | SFX_ENEMY_FLAMETHROWER_CLICK | | `178` | SFX_ENEMY_FLAMETHROWER_DEATH | | `179` | SFX_ENEMY_FLAMETHROWER_FALL | | `180` | SFX_ENEMY_BELT_JINGLE | | `181` | SFX_ENEMY_WRENCH | | `182` | SFX_FOOTSTEP | | `183` | SFX_FOOTSTEP_HIT | | `184` | SFX_ENEMY_COCKING_SHOTGUN | | `186` | SFX_SCUBA_DIVER_FLIPPER | | `188` | SFX_SCUBA_DIVER_BREATH | | `190` | SFX_PULLEY_CRANE | | `191` | SFX_CURTAIN | | `192` | SFX_SCUBA_DIVER_DEATH | | `193` | SFX_SCUBA_DIVER_DIVING | | `194` | SFX_BOAT_START | | `195` | SFX_BOAT_IDLE | | `196` | SFX_BOAT_ACCELERATE | | `197` | SFX_BOAT_MOVING | | `198` | SFX_BOAT_STOP | | `199` | SFX_BOAT_SLOW_DOWN | | `200` | SFX_BOAT_HIT | | `201` | SFX_CLATTER_1 | | `202` | SFX_CLATTER_2 | | `203` | SFX_CLATTER_3 | | `204` | SFX_SPIKE_WALL | | `205` | SFX_LARA_FLESH_WOUND | | `206` | SFX_SAW_REVVING | | `207` | SFX_SAW_STOP | | `208` | SFX_DOOR_CHIME | | `209` | SFX_CHAIN_CREAK_SNAP | | `210` | SFX_SWINGING | | `211` | SFX_BREAKING_1 | | `212` | SFX_PULLEY_MOVE | | `213` | SFX_AIRPLANE_IDLE | | `215` | SFX_UNDERWATER_FAN_ON | | `217` | SFX_SMALL_FAN_ON | | `218` | SFX_SWINGING_BOX_BAG | | `219` | SFX_JUMP_PAD_UP | | `220` | SFX_JUMP_PAD_DOWN | | `221` | SFX_BREAKING_2 | | `222` | SFX_ROLLING_BALL_2_ROLL | | `223` | SFX_ROLLING_BALL_2_STOP | | `224` | SFX_ROLLING | | `225` | SFX_ROLLING_STOP_1 | | `226` | SFX_ROLLING_STOP_2 | | `227` | SFX_ROLLING_BALL_3_ROLL | | `228` | SFX_ROLLING_BALL_3_STOP | `229` | SFX_SIDE_BLADE_SWING | | `230` | SFX_SIDE_BLADE_BACK | | `231` | SFX_ROLLING_BLADE | | `232` | SFX_ICILE_DETACH | | `233` | SFX_ICICLE_HIT | | `234` | SFX_ROTATING_HANDLE_LOOSE | | `235` | SFX_ROTATING_HANDLE_TURN | | `236` | SFX_ROTATING_HANDLE_OPEN | | `237` | SFX_ROTATING_HANDLE_CREAK | | `238` | SFX_MONK_FEET | | `239` | SFX_MONK_SWORD_SWING_1 | | `240` | SFX_MONK_SWORD_SWING_2 | | `241` | SFX_MONK_SHOUT_1 | | `242` | SFX_MONK_SHOUT_2 | | `243` | SFX_MONK_SHOUT_3 | | `244` | SFX_MONK_SHOUT_4 | | `245` | SFX_MONK_CRUNCH | | `246` | SFX_MONK_BREATH | | `247` | SFX_SPLASH_SURFACE | | `248` | SFX_WATERFALL_1 | | `249` | SFX_ENEMY_FEET_SNOW | | `250` | SFX_ENEMY_FIRE_3 | | `251` | SFX_ENEMY_FIRE_SEMIAUTO | | `252` | SFX_ENEMY_DEATH_3 | | `253` | SFX_ENEMY_DEATH_4 | | `254` | SFX_DISC | | `255` | SFX_KNIFETHROWER_FEET | | `256` | SFX_MONK_OYE | | `257` | SFX_MONK_AWEH | | `258` | SFX_PROJECTILE_HIT | | `259` | SFX_KNIFETHROWER_WARRIOR_FEET | | `260` | SFX_WARRIOR_BLADE_SWING_1 | | `261` | SFX_WARRIOR_BLADE_SWING_2 | | `262` | SFX_WARRIOR_GROWL | | `263` | SFX_KNIFETHROWER_HICCUP | | `264` | SFX_WARROPR_BURP | | `265` | SFX_WARRIOR_GROWL_1 | | `267` | SFX_WARRIOR_WAKE | | `268` | SFX_WARRIOR_GROWL_2 | | `269` | SFX_SMALL_SWITCH | | `278` | SFX_CHAIN_PULLEY | | `279` | SFX_ZIPLINE_GRAB | | `280` | SFX_ZIPLINE_GO | | `281` | SFX_ZIPLINE_STOP | | `282` | SFX_BODY_SLUMP | | `283` | SFX_BOWL_TIPPING | | `284` | SFX_BOWL_POUR | | `285` | SFX_WATERFALL_2 | | `286` | SFX_ELEVATOR_OPEN | | `287` | SFX_ELEVATOR_CLOSE | | `288` | SFX_MINISUB_CLATTER_1 | | `289` | SFX_MINISUB_CLATTER_2 | | `290` | SFX_MINISUB_CLATTER_3 | | `291` | SFX_BIRD_MONSTER_SCREAM | | `292` | SFX_BIRD_MONSTER_GASP | | `293` | SFX_BIRD_MONSTER_BREATH | | `294` | SFX_BIRD_MONSTER_FEET | | `295` | SFX_BIRD_MONSTER_DEATH | | `296` | SFX_BIRD_MONSTER_SCRAPE | | `297` | SFX_HELICOPTER_LOOP | | `298` | SFX_DRAGON_FEET | | `298` | SFX_EARTHQUAKE_1 | | `299` | SFX_DRAGON_GROWL_1 | | `300` | SFX_DRAGON_GROWL_2 | | `301` | SFX_DRAGON_FALL | | `302` | SFX_DRAGON_BREATH | | `303` | SFX_DRAGON_GROWL_3 | | `304` | SFX_DRAGON_GRUNT | | `305` | SFX_DRAGON_FIRE | | `306` | SFX_DRAGON_LEG_LIFT | | `307` | SFX_DRAGON_LEG_HIT | | `308` | SFX_WARRIOR_BLADE_SWING_3 | | `309` | SFX_WARRIOR_BLADE_SWING_FAST | | `311` | SFX_WARRIOR_BREATH_ACTIVE | | `312` | SFX_WARRIOR_HOVER | | `313` | SFX_WARRIOR_LANDING | | `314` | SFX_WARRIOR_SWORD_CLANK | | `315` | SFX_WARRIOR_SWORD_SLICE | | `316` | SFX_BIRDS_CHIRP | | `317` | SFX_CRUNCH_1 | | `318` | SFX_CRUNCH_2 | | `319` | SFX_DOOR_CREAK | | `320` | SFX_BREAKING_3 | | `321` | SFX_BIG_SPIDER_SNARL | | `322` | SFX_BIG_SPIDER_FEET | | `323` | SFX_BIG_SPIDER_DEATH | | `324` | SFX_T_REX_ROAR | | `325` | SFX_T_REX_STOMP | | `325` | SFX_PUSHBLOCK_LAND | | `325` | SFX_EARTHQUAKE_2 | | `326` | SFX_T_REX_GROWL_1 | | `327` | SFX_T_REX_DEATH | | `329` | SFX_DRIPS_REVERB | | `330` | SFX_STAGE_BACKDROP | | `331` | SFX_STONE_DOOR_SLIDE | | `332` | SFX_PLATFORM_ALARM | | `333` | SFX_TICK_TOCK | | `334` | SFX_DOORBELL | | `335` | SFX_BURGLAR_ALARM | | `336` | SFX_BOAT_ENGINE | | `337` | SFX_BOAT_INTO_WATER | | `338` | SFX_UNKNOWN_1 | | `339` | SFX_UNKNOWN_2 | | `340` | SFX_UNKNOWN_3 | | `341` | SFX_MARCO_BARTOLLI_TRANSFORM | | `342` | SFX_WINSTON_SHUFFLE | | `343` | SFX_WINSTON_FEET | | `344` | SFX_WINSTON_GRUNT_1 | | `345` | SFX_WINSTON_GRUNT_2 | | `346` | SFX_WINSTON_GRUNT_3 | | `347` | SFX_WINSTON_CUPS | | `348` | SFX_BRITTLE_GROUND_BREAK | | `349` | SFX_SPIDER_EXPLODE | | `350` | SFX_SHARK_BITE | | `351` | SFX_LAVA_BUBBLES | | `352` | SFX_EXPLOSION_2 | | `353` | SFX_BURGLARS | | `354` | SFX_ZIPPER | ================================================ FILE: docs/trx/SUPPORT.md ================================================ --- title: Getting support order: 12 --- # Getting support For bugs, crashes, and feature requests, please use [GitHub issues](https://github.com/LostArtefacts/TRX/issues). Before opening a bug report, read the [bug reporting guide](https://github.com/LostArtefacts/TRX/blob/develop/BUG_REPORTING.md). ================================================ FILE: docs/trx/WATER_COLORS.md ================================================ --- title: Water colors order: 8 --- # Water colors

TR1

Platform Color Color (array) Usage
DOS #99B2FF #99B2FF [0.6, 0.698, 1] original DOS version color
PC #72FFFF #72FFFF [0.447, 1, 1] default TombATI color

TR2

Platform Color Color (array) Usage
PC #80E0FF #80E0FF [0.502, 0.878, 1] default PC hardware renderer color
#AAAAFF #AAAAFF [0.667, 0.667, 1] default PC software renderer color
PS1 #80FFFF #80FFFF [0.502, 1, 1] Lara's Home
#B2E5E5 #B2E5E5 [0.698, 0.898, 0.898] The Great Wall
#CCFF80 #CCFF80 [0.8, 1, 0.502] Venice
#CCFF80 #CCFF80 [0.8, 1, 0.502] Bartoli's Hideout
#CCFF80 #CCFF80 [0.8, 1, 0.502] Opera House
#80FFFF #80FFFF [0.502, 1, 1] Offshore Rig
#80FFFF #80FFFF [0.502, 1, 1] Diving Area
#80FFFF #80FFFF [0.502, 1, 1] 40 Fathoms
#80FFFF #80FFFF [0.502, 1, 1] Wreck of the Maria Doria
#80FFFF #80FFFF [0.502, 1, 1] Living Quarters
#80FFFF #80FFFF [0.502, 1, 1] The Deck
#B2E5E5 #B2E5E5 [0.698, 0.898, 0.898] Tibetan Foothills
#80FFFF #80FFFF [0.502, 1, 1] Barkhang Monastery
#80FFFF #80FFFF [0.502, 1, 1] Catacombs of the Talion
#80FFFF #80FFFF [0.502, 1, 1] Ice Palace
#CCFF99 #CCFF99 [0.8, 1, 0.6] Temple of Xian
#CCFFCC #CCFFCC [0.8, 1, 0.8] Floating Islands
#CCFFCC #CCFFCC [0.8, 1, 0.8] Dragon's Lair
#80FFFF #80FFFF [0.502, 1, 1] Home Sweet Home

TR3

Platform Color Color (array) Usage
PC #80E0FF #80E0FF [0.502, 0.878, 1] default PC renderer color
PS1 #CCFF80 #CCFF80 [0.8, 1, 0.502] Lara's Home
#CCFF80 #CCFF80 [0.8, 1, 0.502] Jungle
#CCFF80 #CCFF80 [0.8, 1, 0.502] Temple Ruins
#CCFF80 #CCFF80 [0.8, 1, 0.502] The River Ganges
#CCFF80 #CCFF80 [0.8, 1, 0.502] Caves of Kaliya
#80FFFF #80FFFF [0.502, 1, 1] Coastal Village
#FFFFFF #FFFFFF [1, 1, 1] Crash Site
#FFFFFF #FFFFFF [1, 1, 1] Madubu Gorge
#80E0FF #80E0FF [0.502, 0.878, 1] Temple of Puna (no water)
#FFFFFF #FFFFFF [1, 1, 1] Thames Wharf
#CCFF80 #CCFF80 [0.8, 1, 0.502] Aldwych
#CCFF80 #CCFF80 [0.8, 1, 0.502] Lud's Gate
#CCFF80 #CCFF80 [0.8, 1, 0.502] City
#FFFFFF #FFFFFF [1, 1, 1] Nevada Desert
#FFFFFF #FFFFFF [1, 1, 1] High Security Compound
#FFFFFF #FFFFFF [1, 1, 1] Area 51
#80FFFF #80FFFF [0.502, 1, 1] Antarctica
#CCFFCC #CCFFCC [0.8, 1, 0.8] RX-Tech Mines
#80E0FF #80E0FF [0.502, 0.878, 1] Lost City of Tinnos
#80E0FF #80E0FF [0.502, 0.878, 1] Meteorite Cavern (no water)
#B2E6E6 #B2E6E6 [0.698, 0.902, 0.902] All Hallows
================================================ FILE: docs/trx/WEAPONS.md ================================================ --- title: Weapons order: 13 --- # Weapons Lara has a fixed number of weapons as follows. - Pistols - Magnums / Automatic Pistols - Uzis - Shotgun - M16 - Grenade Launcher - Harpoon Gun - Flare (not strictly a weapon, but treated similarly by the engine) - Black Skidoo The file `cfg/weapons.json5` contains properties for these weapon types, each described in the table below.
Property Type Description
aim_speed Integer Determines how quickly Lara's arms rotate into position when aiming at a target.
damage Integer The HP damage value to subtract from targets when struck by this weapon type. This value is doubled when playing either Japanese or Japanese NG+ modes.
draw_frame Integer For rifle type weapons, the relative frame number of the equip animation where the object mesh swap is performed e.g. removing the shotgun from Lara's back and putting it in her hand.
equip_anim_idx Integer For rifle type weapons, the relative equip animation index of the associated object e.g. O_LARA_SHOTGUN.
flash_pos / flash_pos_alt XYZ Specifies the offset position where the weapon flash object (O_GUN_FLASH / O_M16_FLASH / O_FLARE_FIRE) will be drawn. flash_pos_alt is used only for discarded flares.
flash_shade Integer Specifies the shade applied when drawing the weapon flash object (O_GUN_FLASH / O_M16_FLASH / O_FLARE_FIRE).
flash_color Float array (length 3) Specifies the color applied when drawing the weapon flash object (O_GUN_FLASH / O_M16_FLASH / O_FLARE_FIRE), used in TR3 lighting system.
glow_color Float array (length 3) Specifies the color applied when drawing the weapon glow object (O_GLOW), used in TR3 lighting system.
glow_pos XYZ Specifies the additional offset to apply to the glow sprite position.
flash_time Integer Determines the number of frames to show the weapon flash object (O_GUN_FLASH / O_M16_FLASH) after firing a weapon.
muzzle_pos XYZ Specifies the additional offset to apply to the muzzle for smoke effects (right hand).
muzzle_pos_alt XYZ Specifies the additional offset to apply to the muzzle for smoke effects (left hand for dual pistols).
smoke_count Integer How many smoke effect instances to spawn upon shooting.
shell_pos XYZ Specifies the additional offset to apply to the gun for shells (right hand).
shell_pos_alt XYZ Specifies the additional offset to apply to the gun for shells (left hand for dual pistols).
gun_height Integer Used to determine the start Y position when firing a weapon, and to determine if Lara is too far submerged in water to be able to use a weapon (other than the harpoon).
is_available Boolean Determines if a weapon can be given to Lara when using item cheats. Pickups for unavailable weapons/flares will still work normally.
left_angles Integer array (length 4) These values determine if Lara has lost target on her left arm.
lock_angles Integer array (length 4) These values are used to test if Lara is able to lock on to a target.
ammo Object Configures how much ammo a weapon gives when acquired and when its matching ammo pickup is collected.
ammo.initial_qty Integer The amount of ammo given when the weapon itself is collected.
ammo.pickup_qty Integer The amount of ammo given when the equivalent ammo object is picked up.
ammo.pickup_qty_alt Integer As per ammo.pickup_qty, but this applies exclusively to flares when playing Japanese NG.
recoil_frame Integer For pistol type weapons, this value determines when Lara should snap back to the aiming frame after the weapon is fired i.e. Uzis have a lower value than Pistols for faster fire rate.
right_angles Integer array (length 4) These values determine if Lara has lost target on her right arm.
sample_num String The sound effect to play when the weapon is fired (see ./SAMPLES.md).
shot_accuracy Integer Adds a random factor to angles used when firing a weapon. Higher values mean less accuracy.
target_dist Float The maximum distance (in world sectors) that a target can be from Lara in order for her to lock on.
type String The category that determines how the gun is handled. Accepted values are as follows.
  • WEAPON_TYPE_DUAL_PISTOLS
  • WEAPON_TYPE_SINGLE_PISTOL
  • WEAPON_TYPE_RIFLE
  • WEAPON_TYPE_MOUNTED
undraw_frame Integer For rifle type weapons, the relative frame number of the unequip animation where the object mesh swap is performed e.g. removing the shotgun from Lara's hand and putting it on her back.
================================================ FILE: docs/trx/game_flow/COMMANDS.md ================================================ --- title: Commands order: 3 --- ## Game flow commands The command allows you to modify the original game flow, but please note that deviations from the original script may result in unexpected behavior. If you encounter any bugs, we encourage you to report your experience by opening an issue on GitHub. The overall structure is as follows: ```json5 { "command": "play_level", "param": 5, } ``` Currently the following commands are available.
Command Description Parameter
noop Continue the flow as normal. N/A
play_level Play a specific level. Level to play.
load_saved_game Load a specific savegame. Save slot number to use
play_cutscene Play a specific cutscene. Cutscene number to play
play_demo Play a specific demo. Demo number to play.
play_fmv Play a specific movie. Movie number to play.
exit_to_title Return the game to the title screen. N/A
level_complete End the current sequence inside level sequences, do nothing otherwise. N/A
exit_game Exit the game to desktop. N/A
select_level Play a specific level (and reset inventory). Level number to play.
restart_level¹ Restart the currently played level. N/A
story_so_far¹ Play the movies and cutscenes up until the currently played level. Save slot number to use
**¹** Tomb Raider 1 only. Additional notes: - All numbers (levels, cutscenes, ...) start with 0. ================================================ FILE: docs/trx/game_flow/GLOBAL_PROPERTIES.md ================================================ --- title: Global properties order: 0 --- ## Global properties The following properties are in the root of the game flow document and control various pieces of global behaviour. Currently, the majority of this section remains distinct for each game. ### TR1 #### Example structure
Show snippet ```json5 { "main_menu_picture": "data/titleh.png", "savegame_file_fmt": "save_tr1_%02d.dat", "water_color": [0.45, 1.0, 1.0], "fog_transparency": false, "fog_color": [0.0, 0.0, 0.0], "fog_start": 22.0, "fog_end": 30.0, "ambient_tracks": [57, 58, 59, 60], "injections": [ "data/global_injection1.bin", "data/global_injection2.bin", // etc ], "convert_dropped_guns": false, "enforced_config": { "enable_save_crystals": false, }, "hidden_config": [ "enable_legal", ], // Optional global Lua script file "main_script": "data/scripts/global.lua", "levels": [ { "path": "data/gym.phd", // etc }, ], "cutscenes": [ { "path": "data/cut1.phd", // etc }, ], "demos": [ { "path": "data/gym.phd", // etc }, ], "fmvs": [ {"path": "data/snow.rpl"}, // etc }, } ```
#### Reference
Property Type Description
cold_water Boolean Enables an exposure meter for Lara when she is in cold water.
convert_dropped_guns Boolean Forces guns dropped by enemies to be converted to the equivalent ammo if Lara already has the gun. See Item drops for full details.
extends String Directory name of the base mod this mod extends. Used for asset fallback and engine version resolution. Required for custom mods to appear in the Switch Game menu.
fog_transparency Boolean Enables blending distant geometry into skybox rather than a solid color.
fog_color Float array or hex string Fog color (R, G, B) or `#RRGGBB`. OG uses `#000000`. Will have no effect if `fog_transparency` is set to true.
fog_start Double The distance (in tiles) at which objects and the world start to fade into blackness.
  • The default value in OG TR1 is hardcoded to 12.
  • The default (disabled) value in TombATI is 72.
fog_end Double The distance (in tiles) at which objects and the world are clipped away.
  • The default value in OG TR1 is hardcoded to 20.
  • The default (disabled) value in TombATI is 80.
enable_tr2_item_drops Boolean Forces enemies who are placed in the same position as pickup items to carry those items and drop them when killed (OG TR2+ behavior). See Item drops for full details.
enforced_config String-to-object map This allows any regular game config setting to be overriden. See User configuration for full details.
hidden_config String array This allows any regular game config setting to be hidden from the ingame settings dialogs. See User configuration for full details.
injections String array Global data injection file paths. Individual levels will inherit these unless inherit_injections is set to false on those levels. See Injections for full details.
globe_select_entries Object array Defines up to 6 selectable destinations for the globe_select sequence. Each entry is an object with the following keys:
  • rot (integer array, length 3): target rotation ([x, y, z]) in engine angle units.
  • start_level_ordinal (integer): ordinal number of the first level for this destination within the main level table.
  • completion_level_ordinal (integer): ordinal number of a level that, once completed, marks this destination as completed.
  • prereq_zones (integer array): a list of required destination indices to unlock this area (e.g. [0, 1, 2, 4]).
  • mesh_idx (integer): globe mesh index used to represent the destination (for rotation/selection and hiding unavailable meshes).
levels Object array* This is where the individual level details are defined - see Level properties for full details.
main_script String Path to a global Lua script to execute after game initialization, before the first level loads.
name String Human-readable display name for this mod, shown in the Switch Game menu. If not set, the directory name is used as a fallback.
main_menu_picture String* Path to the main menu background image.
savegame_file_fmt String* Path pattern to look for the savegame files.
water_color Float array or hex string Water color (R, G, B) or `#RRGGBB`. 1.0 or `FF` means pass-through, 0.0 or `00` means completely black color. See this table for reference values.
ambient_tracks Integer array A list of music track IDs, which will be treated as ambient music. If Lara crosses a trigger for any of these, it will become the current looped track, and will persist on save/load.
**\*** Required property. ### TR2 #### Example structure
Show snippet ```json5 { // NOTE: bad changes to this file may result in crashes. // Lines starting with double slashes are comments and are ignored. "main_menu_picture": "data/images/title_eu.webp", "savegame_file_fmt": "save_tr2_%02d.dat", "demo_version": false, "title": { "path": "data/title.tr2", "music_track": 60, "sequence": [ {"type": "display_picture", "path": "data/images/legal_eu.webp", "legal": true}, {"type": "play_fmv", "fmv_id": 0}, {"type": "play_fmv", "fmv_id": 1}, {"type": "exit_to_title"}, ], }, "sfx_path": "main.sfx", "injections": [ "data/injections/pda_model.bin", "data/injections/winston_model.bin", "data/injections/font.bin", ], "levels": [ { "path": "data/gym.phd", // etc }, ], "cutscenes": [ { "path": "data/cut1.phd", // etc }, ], "demos": [ { "path": "data/gym.phd", // etc }, ], "fmvs": [ {"path": "data/snow.rpl"}, // etc ], "enforced_config": { enable_3d_pickups": false, }, "hidden_config": [ "enable_save_crystals", ], } ```
#### Reference
Property Type Description
demo_version Boolean Legacy setting scheduled for removal at a later time.
main_menu_picture String* Path to the main menu background image.
savegame_file_fmt String* Path pattern to look for the savegame files.
sfx_path String The path to the sound effects (.sfx) file to use in the game.
globe_select_entries Object array Defines up to 6 selectable destinations for the globe_select sequence. See TR1 section for the full schema.
fog_transparency Boolean Enables blending distant geometry into skybox rather than a solid color.
fog_color Float array or hex string Fog color (R, G, B) or `#RRGGBB`. OG uses `#000000`. Will have no effect if `fog_transparency` is set to true.
fog_start Double The distance (in tiles) at which objects and the world start to fade into blackness. The default value in OG TR2 is hardcoded to 12.
fog_end Double The distance (in tiles) at which objects and the world are clipped away. The default value in OG TR2 is hardcoded to 20.
water_color Float array or hex string Water color (R, G, B) or `#RRGGBB`. 1.0 or `FF` means pass-through, 0.0 or `00` means completely black color. See this table for reference values.
ambient_tracks Integer array A list of music track IDs, which will be treated as ambient music. If Lara crosses a trigger for any of these, it will become the current looped track, and will persist on save/load.
================================================ FILE: docs/trx/game_flow/README.md ================================================ --- title: Game flow order: 5 --- # Game flow specification In the original Tomb Raider 1, the game flow was completely hard-coded, including the levels, limiting the builders' flexibility. Tomb Raider 2 improved upon this by introducing a tombpc.dat binary file for game configuration, although it remained cryptic and required builders to use dedicated tooling. TRX has transitioned away from these earlier methods, choosing to manage game flow with a JSON file that provides a unified structure for both games. This document details the elements that can be modified using this updated format. ================================================ FILE: docs/trx/game_flow/SEQUENCES.md ================================================ --- title: Sequences order: 2 --- ## Sequences The following describes each available game flow sequence type and the required parameters for each. Note that while this table is displayed in alphabetical order, care must be taken to define sequences in the correct order. Refer to the default game flow for examples.
Sequence Parameter Type Description
loop_game N/A Plays the main game loop.
level_complete N/A Ends the current level and plays the next one, if available.
exit_to_title N/A Returns to the title level.
level_stats N/A Displays the end of level statistics for the current level. In a Gym level, this fades the screen to black.
total_stats path String Displays the end of game statistics with the given picture file shown as a background.
globe_select globe_select_entries Object Ends the current level and opens the globe destination selector. Available destinations are configured by the global globe_select_entries. You can make the globe selectable at game start. To do this, let the first level contain only the `globe_select` directive, and have its first area link to level 2.
image String Optional path to the background image. If omitted, a plain black background is used.
display_picture path String Displays the specified picture for a fixed time. Files that are needed to function only with a specific aspect ratio can be placed in a directory adjacent to the main image, named according to the aspect ratio – for example, 4x3/title.png or 16x10/title.png. The game won't attempt to match these precisely; instead, it will select the file with the aspect ratio closest to the game's viewport. The main image designated by path is presumed to have a 16:9 aspect ratio for this purpose, and as such there's no need for 16x9-specific directory.
This logic applies to all images.
display_time Double Number of seconds to display the picture for (default: 5).
fade_in_time Double Number of seconds to do the fade-in animation, if enabled (default: 1).
fade_out_time Double Number of seconds to do the fade-out animation, if enabled (default: 0.33).
loading_screen path String Shows a picture prior to loading a level. Functions identically to display_picture, except these pictures can be enabled/disabled by the user with the loading screen option in the config tool.
display_time Double
fade_in_time Double
fade_out_time Double
play_cutscene cutscene_id Integer Plays the specified cinematic level (from the cutscenes).
play_fmv fmv_id String Plays the specified FMV. fmv_id must be a valid index into the fmvs root key.
give_item object_id Integer / String Adds the specified item and quantity to Lara's inventory.
quantity Integer
add_secret_reward object_id Integer / String Adds the specified item to the current level's list of rewards for collecting all secrets. This applies when using the TR2 style of specific secret item pickups as opposed to floor-data defined triggers only.
quantity Integer
play_music music_track Integer Plays the given audio track.
remove_ammo N/A Any combination of these sequences can be used to modify Lara's inventory at the start of a level. There are a few simple points to note:
  • remove_weapons does not remove the ammo for those guns, and equally remove_ammo does not remove the guns. Each works independently of the other.
  • These sequences can also work together with give_item - so, item removal is performed first, followed by addition.
remove_weapons N/A
remove_medipacks N/A
remove_flares N/A
remove_scions N/A
setup_bacon_lara anchor_room Integer Sets the room number in which Bacon Lara will be anchored to enable correct mirroring behaviour with Lara.
enable_sunset² N/A Enables the sunset effect, like in Bartoli's Hideout. At present, this feature is hardcoded to gradually darken the game 40 minutes into playing a level.
set_lara_start_anim value Integer Applies the selected animation to Lara when the level begins. This is used, for example, in the Offshore Rig of Tomb Raider II.
disable_floor value Integer Configures a specific height (with 256 representing 1 click and 1024 representing 1 sector) to define an abyss that will invariably lead to Lara's death if she falls into it. Additionally, it employs special rendering to ensure it isn't treated as solid ground. This is used, for example, in the Floating Islands of Tomb Raider II.
**²** Tomb Raider 2 only. ================================================ FILE: docs/trx/game_flow/USER_CONFIGURATION.md ================================================ --- title: User configuration order: 4 --- ## User Configuration TRX allows the players to configure the game to their taste. The ingame setting dialogs write to `cfg/TR1X.json5` and `cfg/TR2X.json5`. As a level builder, you may however wish to enforce some settings to match how your level is designed. As an example, let's say you do not wish to add save crystals to your level, and as a result you wish to prevent the player from enabling that option in the config tool. To achieve this, open `cfg/tr1/gameflow.json5` in a suitable text editor and add the following. ```json5 { // … "enforced_config": { "enable_save_crystals": false, } } ``` This means that the game will enforce your chosen value for this particular config setting. If the player tries to edit the settings, the option to toggle save crystals will be disabled. You can add as many settings within the `enforced_config` section as needed. Refer to the key names within `cfg/TR1X.json5` and `cfg/TR2X.json5` for reference. Note that you do not need to ship a full configuration with your level, and indeed it is not recommended to do so if you have, for example, your own custom keyboard or controller layouts defined. If you do not have any requirement to enforce settings, you can omit the `enforced_config` section from your game flow altogether. ### Hiding settings from the in-game UI If you prefer to hide certain configuration options entirely (rather than merely enforce their values), you can add a `hidden_config` section in your game flow JSON. Any setting listed here will be omitted from the settings dialogs. For example: ```json5 { // … "hidden_config": [ "enable_legal", "enable_save_crystals", ] } ``` When all settings in a given tab are hidden, that tab will also be removed from the UI. ================================================ FILE: docs/trx/game_flow/levels/BONUS_LEVELS.md ================================================ --- title: Bonus levels order: 5 --- ## Bonus levels The game flow supports bonus levels, which are unlocked only when the player collects all secrets in the game's normal levels. These bonus levels behave just like normal levels, so you can include FMVs, cutscenes in-between and so on. Statistics are maintained separately, so normal end-game statistics are shown once, and then separate bonus level statistics are shown on completion of those levels. Following is a sample level configuration with three normal levels and two bonus levels. After the end-game credits are played following level 3, if the player has collected all secrets, they will then be taken to level 4. Otherwise, the game will exit to title.
Show example setup ```json5 { "levels": [ { // gym level definition }, { "path": "data/level1.phd", "music_track": 57, "sequence": [ {"type": "loop_game"}, {"type": "level_stats"}, {"type": "level_complete"}, ], }, { "path": "data/level2.phd", "music_track": 57, "sequence": [ {"type": "loop_game"}, {"type": "level_stats"}, {"type": "level_complete"}, ], }, { "path": "data/level3.phd", "music_track": 57, "sequence": [ {"type": "loop_game"}, {"type": "level_stats"}, {"type": "play_music", "music_track": 19}, {"type": "display_picture", "path": "data/end.pcx", "display_time": 7.5}, {"type": "display_picture", "path": "data/cred1.pcx", "display_time": 7.5}, {"type": "display_picture", "path": "data/cred2.pcx", "display_time": 7.5}, {"type": "display_picture", "path": "data/cred3.pcx", "display_time": 7.5}, {"type": "total_stats", "background_path": "data/install.pcx"}, {"type": "level_complete"}, ], }, { "path": "data/bonus1.phd", "type": "bonus", "music_track": 57, "sequence": [ {"type": "play_fmv", "fmv_path": "snow.avi"}, {"type": "loop_game"}, {"type": "play_cutscene", "cutscene_id": 0}, {"type": "level_stats"}, {"type": "level_complete"}, ], }, { "path": "data/bonus2.phd", "type": "bonus", "music_track": 57, "sequence": [ {"type": "loop_game"}, {"type": "level_stats"}, {"type": "play_music", "music_track": 14}, {"type": "total_stats", "background_path": "data/install.pcx"}, {"type": "exit_to_title"}, ], }, ], "cutscenes": [ { "path": "data/bonuscut1.phd", "music_track": 23, "sequence": [ {"type": "loop_game"}, ], }, ], } ```
================================================ FILE: docs/trx/game_flow/levels/CUTSCENE_PROPERTIES.md ================================================ --- title: Cutscenes order: 1 --- ## Cutscenes The `cutscenes` section contains all the cinematic levels, used with the `play_cutscene` sequence. Its structure is identical to the regular levels. ================================================ FILE: docs/trx/game_flow/levels/DEMO_PROPERTIES.md ================================================ --- title: Demos order: 4 --- ## Demos The `demos` section contains all the levels that can play a demo when the player leaves the main inventory screen idle for a while or by using the `/demo` command. For the demos to work, these levels need to have demo data built-in. Aside from this requirement, this section works just like the regular levels. ================================================ FILE: docs/trx/game_flow/levels/FMV_PROPERTIES.md ================================================ --- title: FMVs order: 2 --- ## FMVs The FMVs section of the document defines how to play video content. This is an array of objects and can be defined in any order. The flow is controlled using the correct sequencing within each level itself. Following are each of the properties available within an FMV. ```json5 { "path": "data/example.avi", } ```
Property Type Description
path String* The path to the FMV's video file.
**\*** Required property. ================================================ FILE: docs/trx/game_flow/levels/ITEM_DROPS.md ================================================ --- title: Item drops order: 4 --- ## Item drops In the original Tomb Raider I, items dropped by enemies were hardcoded such that only specific enemies could drop, and the items and quantities that they dropped were immutable. This is no longer the case, with the game flow providing a mechanism to allow the _majority_ of enemy types to carry and drop items. Note that this also means by default that the original enemies who did drop items will not do so unless the game flow has been configured as such. Item drops can be defined in two ways. If `enable_tr2_item_drops` is `true`, then custom level builders can add items directly to the level file, setting their position to be the same as the enemies who should drop them. For the original TR1 levels, `enable_tr2_item_drops` is `false`. Item drops are instead defined in the `item_drops` section of a level's definition by creating objects with the following parameter structure. You can define at most one entry per enemy, but that definition can have as many drop items as necessary (within the engine's overall item limit).
Show example setup ```json5 { "path": "data/example.phd", "music_track": 57, "item_drops": [ {"enemy_num": 17, "object_ids": [86]}, {"enemy_num": 50, "object_ids": [87]}, {"enemy_num": 12, "object_ids": [93, 93]}, {"enemy_num": 47, "object_ids": [111]}, ], "sequence": [ {"type": "loop_game"}, {"type": "level_stats"}, {"type": "level_complete"}, ], }, ``` This translates as follows. - Enemy #17 will drop the magnums - Enemy #50 will drop the uzis - Enemy #12 will drop two small medipacks - Enemy #47 will drop puzzle 2
Parameter Type Description
enemy_num Integer The index of the enemy in the level's item list.
object_ids Integer / string array A list of item types to drop. These items will spawn dynamically and do not need to be added to the level file. Duplicate IDs are permitted in the same array.
You can also toggle `convert_dropped_guns` in [global properties](../GLOBAL_PROPERTIES.md#convert-dropped-guns). When `true`, if an enemy drops a gun that Lara already has, it will be converted to the equivalent ammo. When `false`, the gun will always be dropped. ### Enemy validity All enemy types are permitted to carry and drop items. This includes regular enemies as well as TR1 Atlantean pods (objects 163, 181), TR1 centaur statues (object 161), and TR2 statues (objects 42, 44). For pods, the items will be allocated to the creature within (obviously empty pods are excluded). Items dropped by flying or swimming creatures will fall to the ground (TR1 only). For clarity, following is a list of all enemy type IDs which you can reference when building your game flow. The game flow will ignore drops for non-enemy type objects, and a suitable warning message will be produced in the log file.
TR1TR2
Object ID NameObject ID Name
7Wolf15Dog
8Bear16Masked Goon 1
9Bat17Masked Goon 2
10Crocodile18Masked Goon 3
11Alligator19Knife Thrower
12Lion20Shotgun Goon
13Lioness21Rat
14Puma22Dragon Front
15Ape25Shark
16Rat26Eel
17Vole27Big Eel
18T-rex28Barracuda
19Raptor29Scuba Diver
20Flying mutant30Gunman Goon 1
21Grounded mutant (shooter)31Gunman Goon 2
22Grounded mutant (non-shooter)32Stick Wielding Goon 1
23Centaur33Stick Wielding Goon 2
24Mummy (Tomb of Qualopec)34Flamethrower Goon
27Larson35Jellyfish
28Pierre (not runaway)36Spider
30Skate kid37Giant Spider
31Cowboy38Crow
32Kold39Tiger
33Natla (items drop after second phase)40Marco Bartoli
34Torso41Xian Spearman
42Xian Spearman Statue
43Xian Knight
44Xian Knight
45Yeti
46Bird Monster
47Eagle
48Mercenary 1
49Mercenary 2
50Mercenary 3
52Black Snowmobile Driver
214T-Rex
### Item validity The following object types are capable of being carried and dropped. The game flow will ignore anything that is not in this list, and a suitable warning message will be produced in the log file.
TR1TR2
Object IDNameObject IDName
84Pistols135Pistols
85Shotgun136Shotgun
86Magnums137Automatic Pistols
87Uzis138Uzis
89Shotgun ammo139Harpoon Gun
90Magnum ammo140M16
91Uzi ammo141Grenade Launcher
93Small medipack142Pistol Clips
94Large medipack143Shotgun Shells
110Puzzle1144Automatic Pistol Clips
111Puzzle2145Uzi Clips
112Puzzle3146Harpoons
113Puzzle4147M16 Clips
126Lead bar148Grenades
129Key1149Small Medipack
130Key2150Large Medipack
131Key3152Flare
132Key4151Flares Box
141Pickup1174Puzzle Item 1
142Pickup2175Puzzle Item 2
144Scion (à la Pierre)176Puzzle Item 3
177Puzzle Item 4
193Key 1
194Key 2
195Key 3
196Key 4
205Pickup Item 1
206Pickup Item 2
190Secret 1
191Secret 2
192Secret 3
================================================ FILE: docs/trx/game_flow/levels/README.md ================================================ --- title: Levels order: 1 --- ================================================ FILE: docs/trx/game_flow/levels/REGULAR_LEVELS.md ================================================ --- title: Regular levels order: 0 --- ## Regular levels The `levels` section of the document defines how the game plays out. This is an array of objects and can be defined in any order. The flow is controlled using the correct [sequencing](../SEQUENCES.md) within each level itself. Following are each of the properties available within a level.
Show snippet ```json5 { "path": "data/example.phd", // Optional level Lua script file "script": "data/scripts/level1.lua", "music_track": 57, "lara_outfit": "tr2_classic", "weather_type": "rain", "water_particles": true, "death_tile": "rapids", "water_color": [0.7, 0.5, 0.85], "cold_water": true, "fog_transparency": false, "fog_color": [0, 0, 0], "fog_start": 34.0, "fog_end": 50.0, "unobtainable_pickups": 1, "unobtainable_kills": 1, "inherit_injections": false, "injections": [ "data/level_injection1.bin", "data/level_injection2.bin", ], "item_drops": [ {"enemy_num": 17, "object_ids": [86]}, {"enemy_num": 50, "object_ids": [87]}, // etc ], "sequence": [ {"type": "play_fmv", "fmv_id": 0}, // etc ], }, ```
Property Type Description
path String* The path to the level's data file.
script String Path to a Lua script executed after loading this level.
type String The level type, which must be one of the following values. Defaults to normal level.
Type Description
normal A standard level.
gym At most one of these can be defined. Accessed from the photo option (object ID 73) on the title screen. If omitted, the photo option is not displayed.
bonus Only playable when all secrets are collected. See Bonus levels for full details.
current One level of this type is necessary to read TombATI's save files. OG has a special level called LV_CURRENT to handle save/load logic. TRX does away with this hack. However, the existing save games expect the level count to match, otherwise the game will crash.
dummy A placeholder level necessary to read TombATI's save files.
sequence Object array* Instructions to define how a level plays out. See Sequences for full details.
music_track Integer* The ambient music track ID.
lara_outfit string* Defines the outfit to use for Lara, unless overridden by player choice. See Outfits for full details.
weather_type String TR3 only. Enables per-level weather. Valid values: rain, snow. Omit for none.
water_particles Boolean TR3 only. Enables PSX-style underwater water particles for this level. These follow the weather effects toggle.
death_tile String TR3 only. Controls the per-level death tile behavior. Valid values: lava, rapids, electric. Omit for lava.
cold_water Boolean Can be customized per level. See the global property for details.
fog_transparency Boolean Can be customized per level. See the global property for details.
fog_color Float array or hex string Can be customized per level. See the global property for details.
fog_start Double Can be customized per level. See the global property for details.
fog_end Double Can be customized per level. See the global property for details.
injections String array Injection file paths. See Injections for full details.
inherit_injections Boolean A flag to indicate whether or not the level should use the globally defined injections. See Injections for full details.
item_drops Object array Instructions to allocate items to enemies who will drop those items when killed. See Item drops for full details.
sfx_path² String The path to the sound effects (.sfx) file to use in this level. If this property is not defined, the default global file will be used.
unobtainable_kills Integer A count of enemies that will be excluded from kill statistics.
unobtainable_pickups Integer A count of items that will be excluded from pickup statistics.
unobtainable_secrets Integer A count of secrets that will be excluded from secret statistics. Useful for level demos.
water_color Float array or hex string Can be customized per level. See the global property for details.
**\*** Required property. **²** Tomb Raider 2 only. ================================================ FILE: docs/trx/lua/GETTING_STARTED.md ================================================ --- title: Getting started order: 0 --- ## Getting started You'll need: - A TR1 or TR2 TRX build from the latest develop branch that adds Lua scripting. - Familiarity with the game flow JSON format. ### Quick steps Add per-level scripts in a level object: ```json5 { "levels": [ // …, { "script": "data/scripts/level1.lua", // …, }, // … ], // … } ``` Create a file `data/scripts/level1.lua` folder in your project and put the following content: ```lua trx.events.after_level_state(function(level) trx.log.info("hello from level 1!") end) ``` Start the game. In the logs, you should see the following: ``` INF | 2025-10-04 12:12:23.155 [data/scripts/level1.lua:2:?] hello from level 1! ``` --- Optionally, you can also load a global script by adding a global property to your game flow configuration: ```json5 { // Optional global Lua script file "main_script": "data/scripts/global.lua", } ``` ### Interactive commands You can also try out short lua commands in-game with the `/lua` console command: ```text /lua ``` ================================================ FILE: docs/trx/lua/README.md ================================================ --- title: LUA scripting order: 6 --- # Introduction This module adds **Lua scripting support** for creating advanced in-level behaviors – dynamic triggers, event systems, and experimental gameplay logic. Its goal is to give builders a powerful toolkit for designing interactions that go beyond what the editor alone can express. The Lua system is still under active development. It works today, but its utility is currently limited, and some APIs may behave unexpectedly in certain edge cases. These quirks are part of the framework's growing pains, and we're steadily working to refine and stabilize it. Because the system is evolving quickly, the exposed scripting APIs are not yet final. Function names, parameters, and available hooks may change as we continue to expand and improve the framework. All updates and breaking changes will be documented in the changelog so you can keep your projects up to date. Also, we'd *love* to hear what you're building! Tell us about the systems, interactions, or bold ideas you'd like to explore with Lua scripting – your experiments and feedback will directly shape the future of this module. ================================================ FILE: docs/trx/lua/examples/README.md ================================================ --- title: Examples order: 1 --- ## LUA script examples ### Adjusting enemy HP This example sets all bats to start with 20 hitpoints, and all wolves to start with 30 hitpoints. Simply adjust the lookup table to fit your needs. Refer to [this section](../../ENEMY_DEFAULTS.md) as a reference for original values. ```lua -- Lookup table mapping object IDs to HP local hp_lut = { [trx.catalog.objects.wolf] = 30, [trx.catalog.objects.bat] = 20, } -- Adjust HP of enemies when the level loads trx.events.after_level_state(function(level) for i = 1, #trx.items do local item = trx.items[i] local hp = hp_lut[item.object_id] if hp ~= nil then item.hit_points = hp item.max_hit_points = hp end end end) ``` ### Teleporting Lara upon picking up a medipack This will teleport Lara back to the starting point in TR1 Caves. Resetting the camera may be required in some cases. ```lua trx.events.on_pickup(function(pickup_item) local lara = trx.lara.item lara.pos = { x = 73.5 * 1024, y = 3 * 1024, z = 3.5 * 1024, } trx.camera.reset() end) ``` ### Running code every control loop This will run the provided function once every logical frame, meaning the function will always run at 30 FPS regardless of the player's FPS settings. ```lua trx.events.before_control(function() -- handle per-frame control logic end) ``` ### Changing water color in concrete rooms This will change the color to crimson red if Lara is in room 15, and demonstrates how to throttle updates to only happen if Lara goes from one room to another. ```lua local last_room = 0 trx.events.before_control(function() local lara = trx.lara.item if lara.room_num ~= last_room then last_room = lara.room_num if lara.room_num == 15 then trx.config.set("visuals.water_color", "ff0000") else trx.config.set("visuals.water_color", "0000ff") end end end) ``` ================================================ FILE: docs/trx/lua/reference/ASSAULT_STATS.md ================================================ --- title: Assault course stats order: 15 --- ## Assault course module Module for controlling the gym assault course statistics. ### Functions - [lua]`trx.assault_stats.add_record(time)` Adds a new record with the given time (in seconds). Increments internal attempt number. - [lua]`trx.assault_stats.remove_record(record_id)` Removes a record at the given position, with ids starting from 1. - [lua]`trx.assault_stats.list_records()` Returns a list of record times. Structure: - `time`: time in seconds. - `attempt_num`: which attempt this was. ================================================ FILE: docs/trx/lua/reference/CAMERA.md ================================================ --- title: Camera order: 16 --- ## Camera module Module for inspecting the active camera state. ### Properties - **`pos`**: current camera position as `{ x, y, z }`. - **`room_num`**: 1-based room number of the camera position, or `nil` if unknown. - **`room`**: [lua]`trx.rooms.Room` for the camera position, or `nil` if unknown. - **`target_pos`**: current camera target position as `{ x, y, z }`. - **`target_room_num`**: 1-based room number of the camera target, or `nil` if unknown. ### Functions - [lua]`trx.camera.shake(intensity)` Sets camera shake intensity by updating the camera bounce value. Positive values shake the camera upward; negative values shake it downward. Example: ```lua trx.camera.shake(200) ``` - [lua]`trx.camera.reset()` Resets the camera based on Lara's current position. ================================================ FILE: docs/trx/lua/reference/CATALOG.md ================================================ --- title: Catalog order: 13 --- ## Catalog module A convenience module for accessing TRX catalog IDs. ### Structures - [lua]`trx.catalog.objects` This table contains each TRX object ID. Names match those defined in `catalog_objects.csv`, with the `O_` prefix stripped and lowercased. Examples: - [lua]`if item.object_id == trx.catalog.objects.wolf then ...` - [lua]`trx.catalog.flip_effects` This table contains each TRX flip effect action ID. Names match those defined in `catalog_item_actions.csv`, with the `ITEM_ACTION_` prefix stripped and lowercased. Examples: - [lua]`trx.rooms.flip_effect(trx.catalog.flip_effects.floor_shake, 10)` - [lua]`trx.catalog.lara_states` This table contains each TRX Lara state ID. Names match those defined in `catalog_lara_states.csv`, with the `LS_` prefix stripped and lowercased. Examples: - [lua]`if lara.state == trx.catalog.lara_states.run then ...` - [lua]`trx.catalog.weapons` This table contains each TRX Lara gun type ID. Names match the keys from `weapons.json5`, with the `LGT_` prefix stripped and lowercased. Examples: - [lua]`if trx.lara.equipped_gun == trx.catalog.weapons.desert_eagle then ...` - [lua]`trx.catalog.lara_anims` This table contains each TRX Lara animation ID. Names match those defined in `catalog_lara_anims.csv`, with the `LA_` prefix stripped and lowercased. Examples: - [lua]`if anim == trx.catalog.lara_anims.jump_forward then ...` - [lua]`trx.catalog.music` This table contains each TRX music track ID. Names match those defined in `catalog_music.csv`, with the `MX_` prefix stripped and lowercased. Examples: - [lua]`trx.music.play(trx.catalog.music.secret)` - [lua]`trx.catalog.samples` This table contains each TRX sample ID. Names match those defined in `catalog_samples.csv`, with the `SFX_` prefix stripped and lowercased. Examples: - [lua]`trx.sound.play(trx.catalog.samples.lara_no)` ================================================ FILE: docs/trx/lua/reference/CONFIG.md ================================================ --- title: Config order: 8 --- ## Config module Module for querying and modifying engine configuration settings. ### Functions - [lua]`trx.config.get(key)` Retrieves the current value of the config option specified by `key`. - [lua]`trx.config.set(key, value)` Sets the configuration option identified by `key` to `value`. Returns an error if the option is unknown or the value has an invalid format. All values are currently passed as strings, even for numeric or boolean options. Color settings expect a 6-digit hexadecimal string. Note that using this API overrides the player's settings. The old value isn't stored anywhere – the new value immediately becomes the active setting, as if changed directly by the player, and will be remembered across game saves and relaunches globally. Because of this, it's best to mark any options you plan to modify (like water or fog color, view distances, etc.) as frozen or hidden in the gameflow settings. - [lua]`trx.config.list()` Returns a Lua table mapping each config key to its current value. Example - to list all settings and their values in the log file: ```lua for opt, val in pairs(trx.config.list()) do trx.log.info(opt .. " = " .. val) end ``` ================================================ FILE: docs/trx/lua/reference/CONSOLE.md ================================================ --- title: Console order: 5 --- ## Console module Module for interacting with the developer console. ### Functions - [lua]`trx.console.log("string1", "string2", ...)` [lua]`trx.console.log.generic(level, ...)` [lua]`trx.console.log.info(...)` [lua]`trx.console.log.error(...)` [lua]`trx.console.log.warn(...)` [lua]`trx.console.log.warning(...)` [lua]`trx.console.log.debug(...)` Logs a line to the developer console with a specific level. - [lua]`trx.console.eval("string"[, opts])` Evaluates a given string as a developer console command. By default, output from commands is silenced and only appears in the terminal and the log file. To see output from the commands normally, pass `{ verbose = true }` in `opts`. Example: > ```lua > trx.console.eval("play 1", { verbose = true }) > ``` will play the first level and show an according message in the console log. - [lua]`trx.console.clear()` Clears the console. ================================================ FILE: docs/trx/lua/reference/CREATURE.md ================================================ --- title: Creatures order: 12 --- ## Creatures module Module for controlling certain creature behavior. ### Functions - [lua]`trx.creatures.hostile_allies` Reads/writes the global flag to indicate if allies are hostile towards Lara. Examples: - [lua]`trx.creatures.hostile_allies = true` All allies are now hostile - [lua]`trx.creatures.add_ally(obj_id)` Marks the given object as being an ally of Lara. Examples: - [lua]`trx.creatures.add_ally(trx.catalog.objects.monk_1)` All items of type `O_MONK_1` are now an ally of Lara - [lua]`trx.creatures.add_ally_target(obj_id)` Marks the given object as being one who will fight with any allies of Lara. Examples: - [lua]`trx.creatures.add_ally_target(trx.catalog.objects.bandit_1)` All items of type `O_BANDIT_1` will now target any allies of Lara. ================================================ FILE: docs/trx/lua/reference/EVENTS.md ================================================ --- title: Events order: 2 --- ## Events module Lua scripts can listen for game events using the global `events` API. ### API - [lua]`trx.events.before_level_file(callback)` - [lua]`trx.events.after_level_file(callback)` - [lua]`trx.events.after_level_state(callback)` - [lua]`trx.events.on_game_start(callback)` - [lua]`trx.events.on_pickup(callback)` - [lua]`trx.events.before_control(callback)` - [lua]`trx.events.after_control(callback)` Register a handler for a game event. Returns `listener_id`. - [lua]`trx.events.detach(listener_id)` Remove a previously registered event handler. ### Events #### `before_level_file` Happens prior to loading the level file. Arguments: - `level_num` #### `after_level_file` Happens after the level finishes loading, prior to loading information from a savegame. Arguments: - `level_num` #### `after_level_state` Happens after the level finishes loading, after loading information from a savegame. If the game is started normally, this duplicates `after_level_file`. Arguments: - `level_num` #### `on_game_start` Happens after the level finishes loading and the game is about to start. The difference from `after_level_file` and `after_level_state` is that this waits for the fade-to-black / cross-fade effects to finish, and is suitable to play sound effects and run game logic. Arguments: - `level_num` - `is_save` #### `on_pickup` Happens just after Lara picks up an item. Arguments: - `item_num` #### `before_control` Happens on every logical game frame, before executing main game logic. Arguments: none #### `after_control` Happens on every logical game frame, after executing main game logic. Arguments: none ### Examples ```lua trx.events.before_level_file(function(level_num) -- handle pre-file-load setup end) trx.events.after_level_state(function(level_num) -- handle post-savegame state restore end) trx.events.on_pickup(function(item_num) trx.console.log(trx.items[item_num].object_id) end) local control_handler = trx.events.before_control(function() -- handle control loop event end) -- detach a handler trx.events.detach(control_handler) ``` ================================================ FILE: docs/trx/lua/reference/GAME.md ================================================ --- title: Game order: 11 --- ## Game module Module for retrieving game version and level tables. ### Structures - [lua]`trx.game.Level` Represents a level entry. Properties: - **`num`**: Integer ordinal number of the level. - **`name`**: String title of the level. - **`path`**: String file path of the level data. - **`type`**: Integer type identifier of the level. - [lua]`trx.game.Settings` Represents global engine settings. Properties: - **`lockout_option_ring`**: Whether to disallow the player from using the option ring in-game. - **`load_save_disabled`**: Whether to disable saving and loading the game. - **`play_any_level`**: Whether to show a full list of all levels in place of the New Game passport page. - **`demo_delay`**: The number of seconds to pass in the main menu before playing the demo. - **`cheat_keys`**: Whether to enable original game cheats (the ones where Lara turns around three times). Writable properties: - `lockout_option_ring` - `load_save_disabled` - `play_any_level` - `demo_delay` - `cheat_keys` ### Functions - [lua]`trx.game.version` Returns the current game version integer. This is guessed from the level data. - [lua]`trx.game.trx_version` Returns the current TRX version string. - [lua]`trx.game.current_level` Retrieves the [lua]`trx.game.Level` that's currently loaded or `nil`. - [lua]`#trx.game.settings` Accesses the global engine settings. - [lua]`#trx.game.levels` [lua]`#trx.game.demos` [lua]`#trx.game.cutscenes` Returns the number of levels of the specific type. - [lua]`trx.game.levels[num]` [lua]`trx.game.demos[num]` [lua]`trx.game.cutscenes[num]` Retrieves the [lua]`trx.game.Level` of the specific type at the given index, or `nil` if out of range. - [lua]`trx.game.play_level(num)` Plays the specified level via game flow override or errors if invalid. If the Gym level is available, it's the level 1. - [lua]`trx.game.play_cutscene(num)` Plays the specified cutscene via game flow override or errors if invalid. - [lua]`trx.game.play_demo(num)` Plays the specified demo via game flow override or errors if invalid. ================================================ FILE: docs/trx/lua/reference/ITEMS.md ================================================ --- title: Items order: 4 --- ## Items module Module for controlling all moveables behavior. ### Structures - [lua]`trx.items.Item` Represents an item, also known as a moveable. Properties: - **`pos`**: A table with fields `x`, `y`, `z` representing position. - **`rot`**: A table with fields `x`, `y`, `z` representing rotation. - **`anim`**: Current animation number (0-indexed). - **`frame`**: Current frame number (0-indexed). - **`room_num`**: room number. - **`room`**: [`trx.rooms.Room`] object for the room containing this item. - **`status`**: Integer representing the item's status. - **`flags`**: Integer representing the item's trigger-related flags. - **`timer`**: Integer representing the item's trigger-related timer value. - **`hit_points`**: Integer representing the item's hit points. - **`max_hit_points`**: Integer representing the item's hit points. - **`object_id`**: Integer ID of the item's object type. - **`name`**: String name of the item, or `nil` if none. Writable properties: - `pos` (updating this also updates `room` and `room_num`) - `rot` - `anim` - `frame` - `hit_points` (updating this also may increase `max_hit_points`) - `max_hit_points` - `name` (string identifier; setting duplicates raises an error) ### Functions - [lua]`#trx.items` Returns the total number of allocated items. - [lua]`trx.items[num]` [lua]`trx.items["name"]` Retrieves the [lua]`trx.items.Item` at the given 1-based index or with the given `name`, or `nil` if out of range/not found. Example: ```lua local item = trx.items[1] item.name = "lara" local lara = trx.items["lara"] ``` - [lua]`trx.items.fn.get(arg)` Alias of `trx.items[arg]`. Example: ```lua local item_hp = trx.items.fn.get(17).hit_points local lara_hp = trx.items.fn.get("lara").hit_points ``` - [lua]`trx.items.find(query)` Finds all items matching the query and returns a list of [lua]`trx.items.Item`. Supported query fields: - `object_id` - `room_num` Unknown query fields are ignored and logged as warnings. Example: ```lua local wolves = trx.items.find({ object_id = trx.catalog.objects.wolf }) local wolves_in_room_7 = trx.items.find({ object_id = trx.catalog.objects.wolf, room_num = 7, }) ``` - [lua]`trx.items.first(query)` Finds the first item matching the query and returns a [lua]`trx.items.Item`, or `nil` if none match. Supported query fields: - `object_id` - `room_num` Unknown query fields are ignored and logged as warnings. Example: ```lua local first_natla = trx.items.first({ object_id = trx.catalog.objects.natla, }) ``` ================================================ FILE: docs/trx/lua/reference/LARA.md ================================================ --- title: Lara order: 3 --- ## Lara module Module for interacting with the Lara's object. ### Functions - [lua]`trx.lara.item` Returns [lua]`trx.items.Item` associated with Lara, or [lua]`nil` if the Lara object is not available. - [lua]`trx.lara.target` Read-only - returns Lara's current gun target as [lua]`trx.items.Item`, or [lua]`nil` if no target is locked. - [lua]`trx.lara.exposure_bar` Reads/writes exposure timer (cold water bar). The maximum value is 600. If the cold bar setting is disabled on the game flow level, the health must be managed manually from LUA. - [lua]`trx.lara.air_bar` Reads/writes Lara's air timer. The maximum value is 1800. Example: ```lua -- Infinite oxygen trx.events.after_control(function() trx.lara.air_bar = 1800 end) ``` - [lua]`trx.lara.outfit` Reads/writes Lara's outfit name string (for example [lua]`"tr2_diving_suit"`). Outfit names are the keys defined in [md]`cfg/outfits.json5`. Outfits are stored in saves, but writing this value does not change the global config setting, so subsequent levels will adhere to regular outfit changes. - [lua]`trx.lara.set_extra_equipment(lara_mesh_id, extra_mesh_id)` Defines a specific extra mesh to be drawn at the same position as the given Lara mesh. The extra mesh must be present in the `O_LARA_SKIN_SWAP_EXTRA` object and should be setup properly in the [outfits JSON](../../OUTFITS.md). Refer to the constants further below. Example: ```lua -- Put an oar in Lara's right hand trx.lara.set_extra_equipment(trx.lara.mesh.hand_r, trx.lara.extra_mesh.oar) ``` - [lua]`trx.lara.clear_equipment(lara_mesh_id)` Removes any equipment on the given Lara mesh. This applies to guns and extra meshes. - [lua]`trx.lara.holsters_visible` Hides or shows Lara's holsters. This is used in OG TR1/2 gym and Home Sweet Home levels to maintain original outfit appearance. If Lara picks up a holster type weapon, or the weapon cheat is used, or a gun is given via the console, the holsters will automatically be made visible. - [lua]`trx.lara.has_pistol_weapon` Read-only - returns true if Lara has any pistol-type weapon currently in her inventory. - [lua]`trx.lara.extra_anim` Read-only - if Lara is currently in an extra anim state, returns the relative animation number of the `O_LARA_EXTRA` object. Otherwise, returns -1. - [lua]`trx.lara.equipped_gun` Read-only - returns Lara's currently equipped gun ID. Compare values using [lua]`trx.catalog.weapons`. ## Mesh constants - `trx.lara.mesh.hips` An index to Lara's hips mesh. - `trx.lara.mesh.thigh_l` An index to Lara's left thigh mesh. - `trx.lara.mesh.calf_l` An index to Lara's left calf mesh. - `trx.lara.mesh.foot_l` An index to Lara's left foot mesh. - `trx.lara.mesh.thigh_r` An index to Lara's right thigh mesh. - `trx.lara.mesh.calf_r` An index to Lara's right calf mesh. - `trx.lara.mesh.foot_r` An index to Lara's right foot mesh. - `trx.lara.mesh.torso` An index to Lara's torso mesh. - `trx.lara.mesh.uarm_r` An index to Lara's upper right arm mesh. - `trx.lara.mesh.larm_r` An index to Lara's lower right arm mesh. - `trx.lara.mesh.hand_r` An index to Lara's right hand mesh. - `trx.lara.mesh.uarm_l` An index to Lara's upper left arm mesh. - `trx.lara.mesh.larm_l` An index to Lara's lower left arm mesh. - `trx.lara.mesh.hand_l` An index to Lara's left hand mesh. - `trx.lara.mesh.head` An index to Lara's head mesh. ## Extra mesh constants - `trx.lara.extra_mesh.dagger_hand` An index to the dagger mesh used when Lara retrieves it from the dragon and when inspecting it at home. - `trx.lara.extra_mesh.dagger_hips` An index to the dagger mesh that sits on Lara's hips during Home Sweet Home. - `trx.lara.extra_mesh.oar` An index to the oar mesh used when Lara is in the kayak. - `trx.lara.extra_mesh.spanner` An index to the spanner mesh used when Lara is in the minecart. - `trx.lara.extra_mesh.drink_can` An index to the drink can mesh used in the High Security Compound cutscene. - `trx.lara.extra_mesh.glasses_opaque` An index to Lara's opaque sunglasses mesh. - `trx.lara.extra_mesh.glasses_transparent` An index to Lara's transparent sunglasses mesh. ================================================ FILE: docs/trx/lua/reference/LOGGING.md ================================================ --- title: Logging order: 1 --- ## Logging module Lua scripts can log with contextual source info via the global `Log` module. Each call records the Lua script filename, function name, and line number. The results are logged to the standard output in the console as well as the `TRX.log` file in the installation directory. ### Functions - [lua]`trx.log.info(message)` Logs an information to the terminal output and the log file. - [lua]`trx.log.warn(message)` Logs a warning to the terminal output and the log file. - [lua]`trx.log.error(message)` Logs an error to the terminal output and the log file. - [lua]`trx.log.debug(message)` Logs a debug message to the terminal output and the log file. ### Examples ```lua trx.log.info("hello from lua") ``` ================================================ FILE: docs/trx/lua/reference/MISC.md ================================================ --- title: Miscellaneous order: 9 --- ## Miscellaneous functions - [lua]`assert(condition)` Logs a detailed error message to the console if something is not true. - [lua]`print(message)` Logs a basic string to the console without extra decorations like timestamp or the module name. ================================================ FILE: docs/trx/lua/reference/MUSIC.md ================================================ --- title: Music order: 6 --- ## Music module ## Functions - [lua]`trx.music.get_track()` Returns current playing track ID, or `nil` if none. - [lua]`trx.music.play(id[, opts])` Plays specified track. `opts.mode` selects a play mode constant. Errors if the track ID or mode is invalid. Examples: - [lua]`trx.music.play(1)` Plays track 1 once. - [lua]`trx.music.play(2, { mode = trx.music.PlayMode.LOOP })` Plays track 2 as a looped track. - [lua]`trx.music.pause()` Pauses the music. - [lua]`trx.music.unpause()` Resumes paused music. - [lua]`trx.music.stop()` Stops all music. ## Play mode constants - `trx.music.PlayMode.ONCE` Plays the track once; after it finishes, any active looped track resumes. - `trx.music.PlayMode.LOOP` Plays the track in looped mode continuously. This track becomes the ambient track. - `trx.music.PlayMode.NO_REPEAT` Plays the track once but prevents retriggering if it's already playing. - `trx.music.PlayMode.DELAY` Schedules the track for later playback without starting it immediately. - `trx.music.PlayMode.OVERLAY` Schedules the track on top of current music track. ================================================ FILE: docs/trx/lua/reference/OBJECTS.md ================================================ --- title: Object order: 14 --- ## Object module Module for controlling game objects. ### Functions - [lua]`trx.objects.swap_mesh(obj1_id, obj2_id, mesh1_num, mesh2_num)` Swaps the given meshes of the given objects. Examples: - [lua]`trx.objects.swap_mesh(trx.catalog.objects.pierre, trx.catalog.objects.larson, 8, 8)` Pierre now has Larson's head, and vice-versa - [lua]`trx.objects.swap_mesh(obj1_id, obj2_id)` Similar to above, but this will swap out all meshes rather than specific ones. This works best when both objects have the same mesh count; if one object has fewer meshes than the other, the minimum count will be used. Examples: - [lua]`trx.objects.swap_mesh(trx.catalog.objects.pierre, trx.catalog.objects.larson)` Pierre and Larson's meshes are fully swapped - [lua]`trx.objects.swap_mesh(trx.catalog.objects.pierre, trx.catalog.objects.warrior_1)` Pierre's 15 meshes are now of mutant type; the mutant's first 15 meshes are Pierre's, the rest are default. ================================================ FILE: docs/trx/lua/reference/README.md ================================================ --- title: Modules order: 2 --- ================================================ FILE: docs/trx/lua/reference/ROOMS.md ================================================ --- title: Rooms order: 10 --- ## Rooms module Module for inspecting all rooms in the current level. ### Structures - [lua]`trx.rooms.fn.FlipStatus`: - `trx.rooms.fn.FlipStatus.NONE` This is a normal room. - `trx.rooms.fn.FlipStatus.UNFLIPPED` This room is currently reachable by Lara. - `trx.rooms.fn.FlipStatus.FLIPPED` This room is currently inactive and unreachable by Lara. - [lua]`trx.rooms.fn.Room` Represents a room. Properties: - **`num`**: 1-based room number. - **`underwater`**: Whether the room is underwater or not. - **`wind`**: Whether the room has breeze enabled or not. (Requires the player to have breeze enabled in the game settings). - **`bounds`**: a table with world-coordinate bounds of the room. The table contains: - **`min_x`**: minimum x coordinate. - **`min_y`**: minimum y coordinate. - **`min_z`**: minimum z coordinate. - **`max_x`**: maximum x coordinate. - **`max_y`**: maximum y coordinate. - **`max_z`**: maximum z coordinate. - **`internal_bounds`**: similar to `bounds`, but excludes the outer sector. - **`flip_status`**: current room flip status (see `trx.rooms.fn.FlipStatus`). - **`flipped_room`**: linked flip room of this room. Writable properties: - `underwater` - `wind` ### Functions -- Uses Lua length operator on the rooms table: - [lua]`#trx.rooms` Returns the total number of rooms. - [lua]`trx.rooms[num]` Retrieves the [lua]`trx.rooms.fn.Room` at the given 1-based index, or `nil` if out of range. - [lua]`trx.rooms.fn.get(arg)` Alias of `trx.rooms[arg]`. - [lua]`trx.rooms.flip()` Flips the current room map. - [lua]`trx.rooms.flip_effect(effect_id, [timer])` Sets the flip effect id (0-based), and optionally the flip timer. Use `effect_id=-1` to disable the current effect. ================================================ FILE: docs/trx/lua/reference/SOUND.md ================================================ --- title: Sound order: 7 --- ## Sound module ## Functions - [lua]`trx.sound.is_available(id)` Returns `true` if the specified sound sample is available. - [lua]`trx.sound.stop(id)` Stops the specified sound effect. - [lua]`trx.sound.play(id[, opts])` Plays specified sound effect. `opts.pos` may be a `{ x=, y=, z= }` table for position. Examples: - [lua]`trx.sound.play(99)` Plays the sound 99 (in TR1, this is an explosion, in TR2 this is a tiger's roar) at full volume. - [lua]`trx.sound.play(99, { pos = { x = 100, y = 200, z = 50 } })` Plays the same sound at world position (100,200,50), applying pan and volume accordingly. - [lua]`trx.sound.stop_all()` Stops all currently playing sound effects. ================================================ FILE: docs/trx/water_colors.yml ================================================ name: Water colors order: 8 TR1 DOS: - name: original DOS version color color: "#99B2FF" TR1 PC: - name: default TombATI color color: "#72FFFF" TR2 PC: - name: default PC hardware renderer color color: "#80E0FF" - name: default PC software renderer color color: "#AAAAFF" TR2 PS1: - name: Lara's Home color: "#80FFFF" - name: The Great Wall color: "#B2E5E5" - name: Venice color: "#CCFF80" - name: Bartoli's Hideout color: "#CCFF80" - name: Opera House color: "#CCFF80" - name: Offshore Rig color: "#80FFFF" - name: Diving Area color: "#80FFFF" - name: 40 Fathoms color: "#80FFFF" - name: Wreck of the Maria Doria color: "#80FFFF" - name: Living Quarters color: "#80FFFF" - name: The Deck color: "#80FFFF" - name: Tibetan Foothills color: "#B2E5E5" - name: Barkhang Monastery color: "#80FFFF" - name: Catacombs of the Talion color: "#80FFFF" - name: Ice Palace color: "#80FFFF" - name: Temple of Xian color: "#CCFF99" - name: Floating Islands color: "#CCFFCC" - name: Dragon's Lair color: "#CCFFCC" - name: Home Sweet Home color: "#80FFFF" TR3 PC: - name: default PC renderer color color: "#80E0FF" TR3 PS1: - name: Lara's Home color: "#CCFF80" - name: Jungle color: "#CCFF80" - name: Temple Ruins color: "#CCFF80" - name: The River Ganges color: "#CCFF80" - name: Caves of Kaliya color: "#CCFF80" - name: Coastal Village color: "#80FFFF" - name: Crash Site color: "#FFFFFF" - name: Madubu Gorge color: "#FFFFFF" - name: Temple of Puna (no water) color: "#80E0FF" - name: Thames Wharf color: "#FFFFFF" - name: Aldwych color: "#CCFF80" - name: Lud's Gate color: "#CCFF80" - name: City color: "#CCFF80" - name: Nevada Desert color: "#FFFFFF" - name: High Security Compound color: "#FFFFFF" - name: Area 51 color: "#FFFFFF" - name: Antarctica color: "#80FFFF" - name: RX-Tech Mines color: "#CCFFCC" - name: Lost City of Tinnos color: "#80E0FF" - name: Meteorite Cavern (no water) color: "#80E0FF" - name: All Hallows color: "#B2E6E6" ================================================ FILE: justfile ================================================ CWD := `pwd` HOST_USER_UID := `id -u` HOST_USER_GID := `id -g` DOCKER_IMAGE_VERSION := "20260318.rev1" default: (trx-build-win "debug") _docker_push tag: docker push {{tag}}:{{DOCKER_IMAGE_VERSION}} _docker_build dockerfile tag force="0": #!/usr/bin/env sh full_tag="{{tag}}:{{DOCKER_IMAGE_VERSION}}" if [ "{{force}}" = "0" ]; then docker images --format '{''{.Repository}}:{''{.Tag}}' | grep '^'"$full_tag"'$' >/dev/null if [ $? -eq 0 ]; then echo "Docker image $full_tag found" exit 0 fi echo "Docker image $full_tag not found, trying to download from DockerHub" if docker pull $full_tag; then echo "Docker image $full_tag downloaded from DockerHub" exit 0 fi echo "Docker image $full_tag not found, trying to build" fi echo "Building Docker image: {{dockerfile}} → $full_tag" docker build \ . \ -f {{dockerfile}} \ -t $full_tag _docker_run tag *args: #!/usr/bin/env sh full_tag="{{tag}}:{{DOCKER_IMAGE_VERSION}}" echo "Running docker image: $full_tag {{args}}" docker run \ --rm \ --user \ {{HOST_USER_UID}}:{{HOST_USER_GID}} \ -e CCACHE_DIR \ -e CCACHE_BASEDIR \ -e CCACHE_COMPILERCHECK \ -e CCACHE_MAXSIZE \ -v {{CWD}}:/app/ \ $full_tag \ {{args}} image-win force="1": (_docker_build "tools/shared/docker/game-win/Dockerfile" "rrdash/trx-win" force) image-linux force="1": (_docker_build "tools/shared/docker/game-linux/Dockerfile" "rrdash/trx-linux" force) image-win-installer force="1": (_docker_build "tools/shared/docker/installer/Dockerfile" "rrdash/trx-installer" force) push-image-linux: (image-linux "0") (_docker_push "rrdash/trx-linux") push-image-win: (image-win "0") (_docker_push "rrdash/trx-win") import "justfile.tr1" import "justfile.tr2" import "justfile.tr3" download-assets tr_version='all': tools/download_assets {{tr_version}} output-release-name: tools/output_release_name output-current-version *args: tools/get_version {{args}} output-current-changelog *args: tools/output_current_changelog {{args}} output-package-name *args: tools/output_package_name {{args}} clean: -find build/ -type f -delete -find tools/ -type f \( -ipath '*/out/*' -or -ipath '*/bin/*' -or -ipath '*/obj/*' \) -delete -find . -mindepth 1 -empty -type d -delete [group('lint')] lint-imports: tools/sort_imports [group('lint')] lint-format: prek -a [group('lint')] lint: (lint-imports) (lint-format) trx-build-linux target='debug': (image-linux "0") (_docker_run "rrdash/trx-linux" "build" "--target" target) trx-build-win target='debug': (image-win "0") (_docker_run "rrdash/trx-win" "build" "--target" target) trx-build-win-installer target='release' *args: \ (trx-build-win target) \ (_docker_run "rrdash/trx-win" "package" "-o" "tools/installer/TRX_Installer/Resources/release.zip") \ (image-win-installer "0") \ (_docker_run "rrdash/trx-installer" "trx") trx-package-linux target='debug' *args: (trx-build-linux target) (_docker_run "rrdash/trx-linux" "package-all" args) trx-package-win target='debug' *args: (trx-build-win target) (_docker_run "rrdash/trx-win" "package-all" args) trx-package-win-installer target='release' *args: \ (trx-build-win-installer target args) \ (_docker_run "rrdash/trx-win" "package" "--platform" "win-installer" args) ================================================ FILE: justfile.tr1 ================================================ [group('tr1')] tr1-build-win-installer: (image-win-installer "0") (_docker_run "rrdash/trx-installer" "1") [group('tr1')] tr1-package-linux target='release' *args: (trx-build-linux target) (_docker_run "rrdash/trx-linux" "package" "--tr-version" "1" args) [group('tr1')] tr1-package-win target='release' *args: (trx-build-win target) (_docker_run "rrdash/trx-win" "package" "--tr-version" "1" args) [group('tr1')] tr1-package-win-installer target='release' *args: \ (trx-build-win target) \ (_docker_run "rrdash/trx-win" "package" "--tr-version" "1" "-o" "tools/installer/TR1X_Installer/Resources/release.zip") \ (tr1-build-win-installer) \ (_docker_run "rrdash/trx-win" "package" "--platform" "win-installer" "--tr-version" "1" args) ================================================ FILE: justfile.tr2 ================================================ [group('tr2')] tr2-build-win-installer: (image-win-installer "0") (_docker_run "rrdash/trx-installer" "2") [group('tr2')] tr2-package-linux target='release' *args: (trx-build-linux target) (_docker_run "rrdash/trx-linux" "package" "--tr-version" "2" args) [group('tr2')] tr2-package-win target='release' *args: (trx-build-win target) (_docker_run "rrdash/trx-win" "package" "--tr-version" "2" args) [group('tr2')] tr2-package-win-installer target='release' *args: \ (trx-build-win target) \ (_docker_run "rrdash/trx-win" "package" "--tr-version" "2" "-o" "tools/installer/TR2X_Installer/Resources/release.zip") \ (tr2-build-win-installer) \ (_docker_run "rrdash/trx-win" "package" "--platform" "win-installer" "--tr-version" "2" args) ================================================ FILE: justfile.tr3 ================================================ [group('tr3')] tr3-build-win-installer: (image-win-installer "0") (_docker_run "rrdash/trx-installer" "3") [group('tr3')] tr3-package-linux target='release' *args: (trx-build-linux target) (_docker_run "rrdash/trx-linux" "package" "--tr-version" "3" args) [group('tr3')] tr3-package-win target='release' *args: (trx-build-win target) (_docker_run "rrdash/trx-win" "package" "--tr-version" "3" args) [group('tr3')] tr3-package-win-installer target='release' *args: \ (trx-build-win target) \ (_docker_run "rrdash/trx-win" "package" "--tr-version" "3" "-o" "tools/installer/TR3X_Installer/Resources/release.zip") \ (tr3-build-win-installer) \ (_docker_run "rrdash/trx-win" "package" "--platform" "win-installer" "--tr-version" "3" args) ================================================ FILE: src/meson.build ================================================ project( 'TRX', 'c', default_options: [ 'c_std=c2x', 'warning_level=3', ], meson_version: '>=1.3.0', ) fs = import('fs') staticdeps = get_option('staticdeps') # Always dynamically link on macOS if host_machine.system() == 'darwin' staticdeps = false endif c_compiler = meson.get_compiler('c') git = find_program('git', required: true) python3 = find_program('python3', required: true) trx_lua_embed = custom_target( 'trx_lua_embed', input: [ '../data/scripting/assault_stats.lua', '../data/scripting/camera.lua', '../data/scripting/catalog.lua', '../data/scripting/config.lua', '../data/scripting/console.lua', '../data/scripting/creatures.lua', '../data/scripting/events.lua', '../data/scripting/game.lua', '../data/scripting/items.lua', '../data/scripting/lara.lua', '../data/scripting/log.lua', '../data/scripting/music.lua', '../data/scripting/objects.lua', '../data/scripting/rooms.lua', '../data/scripting/sound.lua', ], output: ['trx_embedded_lua.c'], command: [ python3, meson.project_source_root() + '/../tools/embed_trx_lua.py', '--output', '@OUTPUT0@', '@INPUT@', ], ) prefix_map = fs.relative_to(meson.current_source_dir() / 'trx', meson.global_build_root()) common_build_opts = [ '-Wshadow', '-Wno-unused', '-Wno-unused-parameter', '-DMESON_BUILD', '-DGLEW_NO_GLU', '-DSDL_MAIN_HANDLED', '-fms-extensions', '-fno-omit-frame-pointer', '-fmacro-prefix-map=@0@/='.format(prefix_map), '-fdebug-prefix-map=@0@/=src/'.format(prefix_map), '-DDWST_STATIC', '-DPCRE2_STATIC', '-DPCRE2_CODE_UNIT_WIDTH=8', '-DDEBUG=' + (get_option('buildtype') == 'debug' ? '1' : '0'), ] # Some warning names are compiler-specific (e.g. Clang-only) and will spam notes # on GCC. Only add the ones the current compiler actually supports. compiler_specific_warning_opts = [ '-Wno-microsoft-anon-tag', '-Wno-gnu-binary-literal', '-Wno-gnu-empty-initializer', '-Wno-gnu-zero-variadic-macro-arguments', ] build_opts = ( common_build_opts + c_compiler.get_supported_arguments(compiler_specific_warning_opts) ) add_project_arguments(build_opts, language: 'c') null_dep = dependency('', required: false) dep_avcodec = dependency('libavcodec', static: staticdeps) dep_avformat = dependency('libavformat', static: staticdeps) dep_avutil = dependency('libavutil', static: staticdeps) dep_sdl2 = dependency('SDL2', static: staticdeps) dep_glew = dependency('glew', static: staticdeps) dep_pcre2 = dependency('libpcre2-8', static: staticdeps) dep_backtrace = c_compiler.find_library('backtrace', static: true, required: false) dep_swscale = dependency('libswscale', static: staticdeps) dep_swresample = dependency('libswresample', static: staticdeps) dep_lua = dependency('lua', static: staticdeps) c_compiler.check_header('uthash.h', required: true) dep_mathlibrary = c_compiler.find_library('m', static: staticdeps, required: false) dep_zlib = null_dep if not staticdeps dep_zlib = dependency('zlib', static: staticdeps) endif if host_machine.system() == 'windows' dep_opengl = c_compiler.find_library('opengl32') else dep_opengl = dependency('GL') endif dependencies = [ dep_avcodec, dep_avformat, dep_avutil, dep_backtrace, dep_glew, dep_lua, dep_mathlibrary, dep_opengl, dep_pcre2, dep_sdl2, dep_swresample, dep_swscale, dep_zlib, ] common_sources = [ 'trx/av/audio.c', 'trx/av/audio_reverb.c', 'trx/av/audio_sample.c', 'trx/av/audio_stream.c', 'trx/av/image.c', 'trx/av/video.c', 'trx/config/common.c', 'trx/config/dynamic_enum.c', 'trx/config/enum.c', 'trx/config/file.c', 'trx/config/map.c', 'trx/config/presets.c', 'trx/config/priv.c', 'trx/config/vars.c', 'trx/core/benchmark.c', 'trx/core/bson/parse.c', 'trx/core/bson/write.c', 'trx/core/colors.c', 'trx/core/enum_map.c', 'trx/core/event_manager.c', 'trx/core/filesystem.c', 'trx/core/hash.c', 'trx/core/json/base.c', 'trx/core/json/parse.c', 'trx/core/json/util/file.c', 'trx/core/json/util/read_io.c', 'trx/core/json/util/write_io.c', 'trx/core/json/write.c', 'trx/core/log.c', 'trx/core/math/func.c', 'trx/core/math/geom.c', 'trx/core/math/trig.c', 'trx/core/math/util.c', 'trx/core/memory.c', 'trx/core/strings/case_funcs.c', 'trx/core/strings/common.c', 'trx/core/strings/fuzzy_match.c', 'trx/core/thread_pool.c', 'trx/core/vector.c', 'trx/core/virtual_file.c', 'trx/game/anims/commands.c', 'trx/game/anims/common.c', 'trx/game/anims/frames.c', 'trx/game/camera/box_camera.c', 'trx/game/camera/cinematic.c', 'trx/game/camera/common.c', 'trx/game/camera/environment.c', 'trx/game/camera/fixed.c', 'trx/game/camera/los_camera.c', 'trx/game/camera/photo_mode.c', 'trx/game/camera/vars.c', 'trx/game/catalog/manager.c', 'trx/game/clock/common.c', 'trx/game/clock/timer.c', 'trx/game/clock/turbo.c', 'trx/game/collision/common.c', 'trx/game/collision/los.c', 'trx/game/console/cmd/clear.c', 'trx/game/console/cmd/config.c', 'trx/game/console/cmd/debug.c', 'trx/game/console/cmd/die.c', 'trx/game/console/cmd/easy_config.c', 'trx/game/console/cmd/end_level.c', 'trx/game/console/cmd/exit_game.c', 'trx/game/console/cmd/exit_to_title.c', 'trx/game/console/cmd/flipmap.c', 'trx/game/console/cmd/flood.c', 'trx/game/console/cmd/fly.c', 'trx/game/console/cmd/give_item.c', 'trx/game/console/cmd/give_secret.c', 'trx/game/console/cmd/heal.c', 'trx/game/console/cmd/help.c', 'trx/game/console/cmd/immune.c', 'trx/game/console/cmd/inf_sprint.c', 'trx/game/console/cmd/kill.c', 'trx/game/console/cmd/load_game.c', 'trx/game/console/cmd/lua.c', 'trx/game/console/cmd/mod.c', 'trx/game/console/cmd/music.c', 'trx/game/console/cmd/play_cutscene.c', 'trx/game/console/cmd/play_demo.c', 'trx/game/console/cmd/play_gym.c', 'trx/game/console/cmd/play_level.c', 'trx/game/console/cmd/pos.c', 'trx/game/console/cmd/save_game.c', 'trx/game/console/cmd/screenshot.c', 'trx/game/console/cmd/set_health.c', 'trx/game/console/cmd/sfx.c', 'trx/game/console/cmd/spawn.c', 'trx/game/console/cmd/speed.c', 'trx/game/console/cmd/strings.c', 'trx/game/console/cmd/teleport.c', 'trx/game/console/cmd/test_text.c', 'trx/game/console/cmd/trigger.c', 'trx/game/console/cmd/weather.c', 'trx/game/console/cmd/winston.c', 'trx/game/console/common.c', 'trx/game/console/history.c', 'trx/game/console/registry.c', 'trx/game/creature/alert.c', 'trx/game/creature/behavior.c', 'trx/game/creature/common.c', 'trx/game/creature/shooting.c', 'trx/game/cutscene.c', 'trx/game/demo.c', 'trx/game/effects/draw.c', 'trx/game/effects/manager.c', 'trx/game/enum.c', 'trx/game/events.c', 'trx/game/fader.c', 'trx/game/fmv.c', 'trx/game/fx/common.c', 'trx/game/fx/explosion_ring.c', 'trx/game/fx/footprint.c', 'trx/game/fx/gun_flash.c', 'trx/game/fx/laser.c', 'trx/game/fx/wake.c', 'trx/game/fx/water.c', 'trx/game/fx/water_particles.c', 'trx/game/fx/weather.c', 'trx/game/game/control.c', 'trx/game/game/draw.c', 'trx/game/game/state.c', 'trx/game/game_buf.c', 'trx/game/game_flow/common.c', 'trx/game/game_flow/inventory.c', 'trx/game/game_flow/reader.c', 'trx/game/game_flow/sequencer.c', 'trx/game/game_flow/sequencer_events.c', 'trx/game/game_flow/sequencer_misc.c', 'trx/game/game_flow/util.c', 'trx/game/game_flow/vars.c', 'trx/game/game_strings/entries.c', 'trx/game/game_strings/manager.c', 'trx/game/game_strings/table/common.c', 'trx/game/game_strings/table/priv.c', 'trx/game/game_strings/table/reader.c', 'trx/game/gun/common.c', 'trx/game/gun/control.c', 'trx/game/gun/misc.c', 'trx/game/gun/pistols.c', 'trx/game/gun/rifle.c', 'trx/game/gun/smashing.c', 'trx/game/gun/smoke.c', 'trx/game/gun/vars.c', 'trx/game/gym.c', 'trx/game/inject/common.c', 'trx/game/inject/data/anims.c', 'trx/game/inject/data/camera.c', 'trx/game/inject/data/meshes.c', 'trx/game/inject/data/objects.c', 'trx/game/inject/data/sound.c', 'trx/game/inject/data/textures.c', 'trx/game/inject/editor.c', 'trx/game/inject/editors/anims.c', 'trx/game/inject/editors/floor_data.c', 'trx/game/inject/editors/items.c', 'trx/game/inject/editors/meshes.c', 'trx/game/inject/editors/objects.c', 'trx/game/inject/editors/rooms.c', 'trx/game/inject/editors/textures.c', 'trx/game/inject/testers/items.c', 'trx/game/inject/testers/rooms.c', 'trx/game/inject/utils.c', 'trx/game/input/backends/controller.c', 'trx/game/input/backends/internal.c', 'trx/game/input/backends/keyboard.c', 'trx/game/input/combo.c', 'trx/game/input/common.c', 'trx/game/input/update.c', 'trx/game/interpolation.c', 'trx/game/inventory.c', 'trx/game/inventory_ring/control.c', 'trx/game/inventory_ring/draw.c', 'trx/game/inventory_ring/priv.c', 'trx/game/inventory_ring/vars.c', 'trx/game/items/actions/common.c', 'trx/game/items/actions/effects.c', 'trx/game/items/actions/footprint.c', 'trx/game/items/actions/general.c', 'trx/game/items/actions/gym_tr3.c', 'trx/game/items/actions/ids.c', 'trx/game/items/actions/items.c', 'trx/game/items/actions/lara.c', 'trx/game/items/anim.c', 'trx/game/items/carrier.c', 'trx/game/items/col.c', 'trx/game/items/draw.c', 'trx/game/items/manager.c', 'trx/game/items/utils.c', 'trx/game/items/walkable.c', 'trx/game/lara/breath.c', 'trx/game/lara/cheat.c', 'trx/game/lara/cheat_keys.c', 'trx/game/lara/col.c', 'trx/game/lara/col/climb.c', 'trx/game/lara/col/crouch.c', 'trx/game/lara/col/jump.c', 'trx/game/lara/col/land.c', 'trx/game/lara/col/monkey.c', 'trx/game/lara/col/swim.c', 'trx/game/lara/common.c', 'trx/game/lara/control.c', 'trx/game/lara/draw.c', 'trx/game/lara/electric.c', 'trx/game/lara/flare.c', 'trx/game/lara/hair.c', 'trx/game/lara/look.c', 'trx/game/lara/mesh.c', 'trx/game/lara/misc.c', 'trx/game/lara/pose.c', 'trx/game/lara/skin/common.c', 'trx/game/lara/skin/storage.c', 'trx/game/lara/state.c', 'trx/game/lara/state/climb.c', 'trx/game/lara/state/crouch.c', 'trx/game/lara/state/extra.c', 'trx/game/lara/state/jump.c', 'trx/game/lara/state/land.c', 'trx/game/lara/state/monkey.c', 'trx/game/lara/state/swim.c', 'trx/game/lara/vehicle.c', 'trx/game/level/cache.c', 'trx/game/level/common.c', 'trx/game/level/context.c', 'trx/game/level/finalize/animations.c', 'trx/game/level/finalize/gameplay_objects.c', 'trx/game/level/finalize/render_assets.c', 'trx/game/level/finalize/rooms.c', 'trx/game/level/format/format_tr1.c', 'trx/game/level/format/format_tr2.c', 'trx/game/level/format/format_tr3.c', 'trx/game/level/format/pipeline.c', 'trx/game/level/pipeline.c', 'trx/game/level/sections/anims.c', 'trx/game/level/sections/audio.c', 'trx/game/level/sections/cinematics.c', 'trx/game/level/sections/meshes.c', 'trx/game/level/sections/objects.c', 'trx/game/level/sections/pathing.c', 'trx/game/level/sections/rooms.c', 'trx/game/level/sections/textures.c', 'trx/game/level/settings.c', 'trx/game/lua/assault_stats.c', 'trx/game/lua/camera.c', 'trx/game/lua/catalog.c', 'trx/game/lua/common.c', 'trx/game/lua/config.c', 'trx/game/lua/console.c', 'trx/game/lua/creatures.c', 'trx/game/lua/events.c', 'trx/game/lua/game.c', 'trx/game/lua/items.c', 'trx/game/lua/lara.c', 'trx/game/lua/log.c', 'trx/game/lua/music.c', 'trx/game/lua/objects.c', 'trx/game/lua/rooms.c', 'trx/game/lua/sound.c', 'trx/game/matrix.c', 'trx/game/music/backend_cdaudio.c', 'trx/game/music/backend_cdaudio_wad.c', 'trx/game/music/backend_files.c', 'trx/game/music/common.c', 'trx/game/music/ids.c', 'trx/game/objects/col.c', 'trx/game/objects/common.c', 'trx/game/objects/creatures/ape.c', 'trx/game/objects/creatures/atlantean.c', 'trx/game/objects/creatures/bacon_lara.c', 'trx/game/objects/creatures/baldy.c', 'trx/game/objects/creatures/bandit_1.c', 'trx/game/objects/creatures/bandit_2.c', 'trx/game/objects/creatures/barracuda.c', 'trx/game/objects/creatures/bartoli.c', 'trx/game/objects/creatures/bat.c', 'trx/game/objects/creatures/bear.c', 'trx/game/objects/creatures/big_eel.c', 'trx/game/objects/creatures/big_spider.c', 'trx/game/objects/creatures/bird.c', 'trx/game/objects/creatures/bird_guardian.c', 'trx/game/objects/creatures/centaur.c', 'trx/game/objects/creatures/centaur_statue.c', 'trx/game/objects/creatures/civilian.c', 'trx/game/objects/creatures/claw_mutant.c', 'trx/game/objects/creatures/claw_mutant_plasma_ball.c', 'trx/game/objects/creatures/cobra.c', 'trx/game/objects/creatures/compy.c', 'trx/game/objects/creatures/cowboy.c', 'trx/game/objects/creatures/crawler_mutant.c', 'trx/game/objects/creatures/crocodile.c', 'trx/game/objects/creatures/cultist_1.c', 'trx/game/objects/creatures/cultist_2.c', 'trx/game/objects/creatures/cultist_3.c', 'trx/game/objects/creatures/diver.c', 'trx/game/objects/creatures/dog.c', 'trx/game/objects/creatures/dragon.c', 'trx/game/objects/creatures/eel.c', 'trx/game/objects/creatures/hybrid_mutant.c', 'trx/game/objects/creatures/jelly.c', 'trx/game/objects/creatures/larson.c', 'trx/game/objects/creatures/lion.c', 'trx/game/objects/creatures/lizard.c', 'trx/game/objects/creatures/mercenary.c', 'trx/game/objects/creatures/monk.c', 'trx/game/objects/creatures/monkey.c', 'trx/game/objects/creatures/mouse.c', 'trx/game/objects/creatures/mp_1.c', 'trx/game/objects/creatures/mp_2.c', 'trx/game/objects/creatures/mummy.c', 'trx/game/objects/creatures/natla.c', 'trx/game/objects/creatures/natla_gun.c', 'trx/game/objects/creatures/orca.c', 'trx/game/objects/creatures/patrol_dog.c', 'trx/game/objects/creatures/pierre.c', 'trx/game/objects/creatures/pod.c', 'trx/game/objects/creatures/prisoner.c', 'trx/game/objects/creatures/punk.c', 'trx/game/objects/creatures/raptor.c', 'trx/game/objects/creatures/rat.c', 'trx/game/objects/creatures/rx_worker_1.c', 'trx/game/objects/creatures/rx_worker_2.c', 'trx/game/objects/creatures/rx_worker_3.c', 'trx/game/objects/creatures/security_guard.c', 'trx/game/objects/creatures/shark.c', 'trx/game/objects/creatures/shiva.c', 'trx/game/objects/creatures/skate_kid.c', 'trx/game/objects/creatures/skidoo_driver.c', 'trx/game/objects/creatures/sophia.c', 'trx/game/objects/creatures/sophia_laser_bolt.c', 'trx/game/objects/creatures/sophia_plasma_ball.c', 'trx/game/objects/creatures/spider.c', 'trx/game/objects/creatures/swat.c', 'trx/game/objects/creatures/tiger.c', 'trx/game/objects/creatures/tony.c', 'trx/game/objects/creatures/tony_fire_ball.c', 'trx/game/objects/creatures/torso.c', 'trx/game/objects/creatures/trex.c', 'trx/game/objects/creatures/trex_alpha.c', 'trx/game/objects/creatures/tribe_axeman.c', 'trx/game/objects/creatures/tribe_boss.c', 'trx/game/objects/creatures/tribe_pipeman.c', 'trx/game/objects/creatures/wasp_mutant.c', 'trx/game/objects/creatures/willard.c', 'trx/game/objects/creatures/willard_plasma_ball.c', 'trx/game/objects/creatures/winston.c', 'trx/game/objects/creatures/winston_army.c', 'trx/game/objects/creatures/wolf.c', 'trx/game/objects/creatures/worker_1.c', 'trx/game/objects/creatures/worker_2.c', 'trx/game/objects/creatures/worker_3.c', 'trx/game/objects/creatures/xian_common.c', 'trx/game/objects/creatures/xian_knight.c', 'trx/game/objects/creatures/xian_spearman.c', 'trx/game/objects/creatures/yeti.c', 'trx/game/objects/draw.c', 'trx/game/objects/effects/blood.c', 'trx/game/objects/effects/body_part.c', 'trx/game/objects/effects/bubble.c', 'trx/game/objects/effects/dart_effect.c', 'trx/game/objects/effects/ember.c', 'trx/game/objects/effects/explosion.c', 'trx/game/objects/effects/flame.c', 'trx/game/objects/effects/glow.c', 'trx/game/objects/effects/gun_flash.c', 'trx/game/objects/effects/gun_shell.c', 'trx/game/objects/effects/hot_liquid.c', 'trx/game/objects/effects/missile.c', 'trx/game/objects/effects/pickup_aid.c', 'trx/game/objects/effects/ricochet.c', 'trx/game/objects/effects/snow_sprite.c', 'trx/game/objects/effects/splash.c', 'trx/game/objects/effects/twinkle.c', 'trx/game/objects/effects/water_sprite.c', 'trx/game/objects/general/ai_node.c', 'trx/game/objects/general/alarm_sound.c', 'trx/game/objects/general/animating.c', 'trx/game/objects/general/area_51_rocket.c', 'trx/game/objects/general/assault_target.c', 'trx/game/objects/general/bat_emitter.c', 'trx/game/objects/general/bell.c', 'trx/game/objects/general/big_bowl.c', 'trx/game/objects/general/bird_tweeter.c', 'trx/game/objects/general/boat.c', 'trx/game/objects/general/bridge_common.c', 'trx/game/objects/general/bridge_flat.c', 'trx/game/objects/general/bridge_tilt1.c', 'trx/game/objects/general/bridge_tilt2.c', 'trx/game/objects/general/cabin.c', 'trx/game/objects/general/camera_target.c', 'trx/game/objects/general/carcass.c', 'trx/game/objects/general/clock_chimes.c', 'trx/game/objects/general/cog.c', 'trx/game/objects/general/combat_end.c', 'trx/game/objects/general/copter.c', 'trx/game/objects/general/cutscene_player.c', 'trx/game/objects/general/detonator_box.c', 'trx/game/objects/general/ding_dong.c', 'trx/game/objects/general/disposable_animating.c', 'trx/game/objects/general/door.c', 'trx/game/objects/general/drawbridge.c', 'trx/game/objects/general/dummy.c', 'trx/game/objects/general/earthquake.c', 'trx/game/objects/general/final_cutscene.c', 'trx/game/objects/general/flare_item.c', 'trx/game/objects/general/fuse_box.c', 'trx/game/objects/general/gas_emitter.c', 'trx/game/objects/general/general.c', 'trx/game/objects/general/gong.c', 'trx/game/objects/general/gong_bonger.c', 'trx/game/objects/general/grenade.c', 'trx/game/objects/general/harpoon_bolt.c', 'trx/game/objects/general/keyhole.c', 'trx/game/objects/general/kill_all_triggered.c', 'trx/game/objects/general/lara_alarm.c', 'trx/game/objects/general/lift.c', 'trx/game/objects/general/lights/beacon_light.c', 'trx/game/objects/general/lights/colored_light.c', 'trx/game/objects/general/lights/electrical_light.c', 'trx/game/objects/general/lights/on_off_light.c', 'trx/game/objects/general/lights/pulse_light.c', 'trx/game/objects/general/lights/strobe_light.c', 'trx/game/objects/general/mini_copter.c', 'trx/game/objects/general/moving_bar.c', 'trx/game/objects/general/pickup.c', 'trx/game/objects/general/puzzle_hole.c', 'trx/game/objects/general/rocket.c', 'trx/game/objects/general/save_crystal.c', 'trx/game/objects/general/scion1.c', 'trx/game/objects/general/scion3.c', 'trx/game/objects/general/scion4.c', 'trx/game/objects/general/scion_holder.c', 'trx/game/objects/general/shoal.c', 'trx/game/objects/general/smashable.c', 'trx/game/objects/general/smoke_emitter.c', 'trx/game/objects/general/sphere_of_doom.c', 'trx/game/objects/general/switch.c', 'trx/game/objects/general/trapdoor.c', 'trx/game/objects/general/trigger_gate.c', 'trx/game/objects/general/waterfall.c', 'trx/game/objects/general/zipline.c', 'trx/game/objects/names.c', 'trx/game/objects/setup.c', 'trx/game/objects/traps/blade.c', 'trx/game/objects/traps/bubble_emitter.c', 'trx/game/objects/traps/cleaner.c', 'trx/game/objects/traps/common.c', 'trx/game/objects/traps/damocles_sword.c', 'trx/game/objects/traps/dart.c', 'trx/game/objects/traps/dart_emitter.c', 'trx/game/objects/traps/dying_monk.c', 'trx/game/objects/traps/electric_fence.c', 'trx/game/objects/traps/ember_emitter.c', 'trx/game/objects/traps/falling_block.c', 'trx/game/objects/traps/falling_ceiling.c', 'trx/game/objects/traps/fire_head.c', 'trx/game/objects/traps/flame_emitter.c', 'trx/game/objects/traps/gondola.c', 'trx/game/objects/traps/hook.c', 'trx/game/objects/traps/icicle.c', 'trx/game/objects/traps/killer_statue.c', 'trx/game/objects/traps/lava_wedge.c', 'trx/game/objects/traps/lightning_emitter.c', 'trx/game/objects/traps/midas_touch.c', 'trx/game/objects/traps/mine.c', 'trx/game/objects/traps/movable_block.c', 'trx/game/objects/traps/pendulum.c', 'trx/game/objects/traps/power_saw.c', 'trx/game/objects/traps/propeller.c', 'trx/game/objects/traps/raptor_emitter.c', 'trx/game/objects/traps/rolling_ball.c', 'trx/game/objects/traps/rotating_laser.c', 'trx/game/objects/traps/security_laser.c', 'trx/game/objects/traps/sentry_gun.c', 'trx/game/objects/traps/sliding_pillar.c', 'trx/game/objects/traps/spike_ceiling.c', 'trx/game/objects/traps/spike_wall.c', 'trx/game/objects/traps/spikes.c', 'trx/game/objects/traps/spinning_blade.c', 'trx/game/objects/traps/springboard.c', 'trx/game/objects/traps/teeth_trap.c', 'trx/game/objects/traps/thors_hammer.c', 'trx/game/objects/traps/train.c', 'trx/game/objects/traps/wasp_emitter.c', 'trx/game/objects/vars.c', 'trx/game/objects/vehicles/boat.c', 'trx/game/objects/vehicles/common.c', 'trx/game/objects/vehicles/kayak.c', 'trx/game/objects/vehicles/mine_cart.c', 'trx/game/objects/vehicles/mounted_gun.c', 'trx/game/objects/vehicles/quad_bike.c', 'trx/game/objects/vehicles/rib.c', 'trx/game/objects/vehicles/skidoo_armed.c', 'trx/game/objects/vehicles/skidoo_common.c', 'trx/game/objects/vehicles/skidoo_fast.c', 'trx/game/objects/vehicles/upv.c', 'trx/game/option/common.c', 'trx/game/option/controls.c', 'trx/game/option/examine.c', 'trx/game/option/gameplay.c', 'trx/game/option/globe_select.c', 'trx/game/option/graphics.c', 'trx/game/option/passport.c', 'trx/game/option/sound.c', 'trx/game/option/stats.c', 'trx/game/output/bind.c', 'trx/game/output/common.c', 'trx/game/output/draw.c', 'trx/game/output/func.c', 'trx/game/output/lights.c', 'trx/game/output/mesh_batcher/batcher.c', 'trx/game/output/mesh_batcher/mesh.c', 'trx/game/output/mesh_batcher/mesh_builder.c', 'trx/game/output/quad.c', 'trx/game/output/scene_compositor.c', 'trx/game/output/shaders/generic.c', 'trx/game/output/shaders/mesh.c', 'trx/game/output/shaders/ui.c', 'trx/game/output/sources/lightnings.c', 'trx/game/output/sources/misc.c', 'trx/game/output/sources/objects.c', 'trx/game/output/sources/overlay.c', 'trx/game/output/sources/poly_fx.c', 'trx/game/output/sources/rooms.c', 'trx/game/output/sources/rooms_debug.c', 'trx/game/output/sources/shadows.c', 'trx/game/output/sources/sprites.c', 'trx/game/output/sources/ui.c', 'trx/game/output/state.c', 'trx/game/output/textures.c', 'trx/game/output/uniforms.c', 'trx/game/output/utils.c', 'trx/game/output/vars.c', 'trx/game/output/vertex_range.c', 'trx/game/overlay.c', 'trx/game/pathing/box.c', 'trx/game/pathing/lot.c', 'trx/game/phase/executor.c', 'trx/game/phase/phase_cutscene.c', 'trx/game/phase/phase_demo.c', 'trx/game/phase/phase_game.c', 'trx/game/phase/phase_globe_select.c', 'trx/game/phase/phase_inventory.c', 'trx/game/phase/phase_pause.c', 'trx/game/phase/phase_photo_mode.c', 'trx/game/phase/phase_picture.c', 'trx/game/phase/phase_stats.c', 'trx/game/photo_mode.c', 'trx/game/random.c', 'trx/game/replay/test_recorder.c', 'trx/game/replay/test_replay.c', 'trx/game/rooms/common.c', 'trx/game/rooms/draw.c', 'trx/game/rooms/floor_data.c', 'trx/game/rooms/geometry.c', 'trx/game/savegame/common.c', 'trx/game/savegame/file.c', 'trx/game/savegame/file_read.c', 'trx/game/savegame/file_write.c', 'trx/game/screenshot.c', 'trx/game/shell/args.c', 'trx/game/shell/common.c', 'trx/game/shell/config.c', 'trx/game/shell/events.c', 'trx/game/shell/flow.c', 'trx/game/shell/input.c', 'trx/game/shell/main.c', 'trx/game/shell/mod.c', 'trx/game/shell/paths.c', 'trx/game/shell/platform.c', 'trx/game/shell/session.c', 'trx/game/shell/state.c', 'trx/game/sound/common.c', 'trx/game/sound/ids.c', 'trx/game/sparks/manager.c', 'trx/game/sparks/spawners.c', 'trx/game/spawn.c', 'trx/game/stats/common.c', 'trx/game/stats/init.c', 'trx/game/stats/scan.c', 'trx/game/ui/common.c', 'trx/game/ui/dialogs/base_passport.c', 'trx/game/ui/dialogs/color_editor.c', 'trx/game/ui/dialogs/config_presets.c', 'trx/game/ui/dialogs/controls.c', 'trx/game/ui/dialogs/controls_backend.c', 'trx/game/ui/dialogs/controls_editor.c', 'trx/game/ui/dialogs/gameplay_settings.c', 'trx/game/ui/dialogs/graphic_settings.c', 'trx/game/ui/dialogs/new_game.c', 'trx/game/ui/dialogs/pause.c', 'trx/game/ui/dialogs/photo_mode.c', 'trx/game/ui/dialogs/play_any_level.c', 'trx/game/ui/dialogs/save_slot.c', 'trx/game/ui/dialogs/select_level.c', 'trx/game/ui/dialogs/setting_helpers/enums.c', 'trx/game/ui/dialogs/setting_helpers/handlers.c', 'trx/game/ui/dialogs/setting_helpers/handlers_language.c', 'trx/game/ui/dialogs/settings.c', 'trx/game/ui/dialogs/settings_editor.c', 'trx/game/ui/dialogs/settings_tabs.c', 'trx/game/ui/dialogs/sound_settings.c', 'trx/game/ui/dialogs/stats.c', 'trx/game/ui/dialogs/switch_mod.c', 'trx/game/ui/dialogs/text.c', 'trx/game/ui/draw.c', 'trx/game/ui/elements/ammo_label.c', 'trx/game/ui/elements/anchor.c', 'trx/game/ui/elements/bar.c', 'trx/game/ui/elements/bar_enemy_hp.c', 'trx/game/ui/elements/bar_lara_air.c', 'trx/game/ui/elements/bar_lara_exposure.c', 'trx/game/ui/elements/bar_lara_hp.c', 'trx/game/ui/elements/bar_lara_sprint.c', 'trx/game/ui/elements/button_label.c', 'trx/game/ui/elements/color_swatch.c', 'trx/game/ui/elements/flash.c', 'trx/game/ui/elements/fps_counter.c', 'trx/game/ui/elements/frame.c', 'trx/game/ui/elements/gradient_slider.c', 'trx/game/ui/elements/hide.c', 'trx/game/ui/elements/horizontal_line.c', 'trx/game/ui/elements/label.c', 'trx/game/ui/elements/modal.c', 'trx/game/ui/elements/offset.c', 'trx/game/ui/elements/pad.c', 'trx/game/ui/elements/progress_button.c', 'trx/game/ui/elements/prompt.c', 'trx/game/ui/elements/requester.c', 'trx/game/ui/elements/resize.c', 'trx/game/ui/elements/row_arrows.c', 'trx/game/ui/elements/scrollable_stack.c', 'trx/game/ui/elements/sleek_bar.c', 'trx/game/ui/elements/spacer.c', 'trx/game/ui/elements/span.c', 'trx/game/ui/elements/stack.c', 'trx/game/ui/elements/tab_switch.c', 'trx/game/ui/elements/window.c', 'trx/game/ui/events.c', 'trx/game/ui/helpers.c', 'trx/game/ui/hud/console.c', 'trx/game/ui/hud/console_logs.c', 'trx/game/ui/hud/overlay.c', 'trx/game/ui/scaler.c', 'trx/game/ui/scrollable.c', 'trx/game/ui/settings.c', 'trx/game/ui/text.c', 'trx/game/viewport.c', 'trx/gl/buffer.c', 'trx/gl/context.c', 'trx/gl/enum.c', 'trx/gl/fbo.c', 'trx/gl/program.c', 'trx/gl/renderer.c', 'trx/gl/sampler.c', 'trx/gl/screenshot.c', 'trx/gl/texture.c', 'trx/gl/track.c', 'trx/gl/utils.c', 'trx/gl/vertex_array.c', 'trx/version.c', ] if dep_backtrace.found() and host_machine.system() == 'linux' common_sources += ['trx/core/log_linux.c'] elif host_machine.system() == 'windows' common_sources += ['trx/core/log_windows.c'] dwarfstack = subproject('dwarfstack', default_options: ['warning_level=0']) dep_dwarfstack = dwarfstack.get_variable('dep_dwarfstack') dep_dbghelp = c_compiler.find_library('dbghelp') dependencies += [dep_dbghelp, dep_dwarfstack] else common_sources += ['trx/core/log_unknown.c'] endif link_args = [] if host_machine.system() == 'windows' link_args += ['-static'] endif ################################################################################# # TRX ################################################################################# # autogenerated files trx_init = custom_target( 'trx_fake_init', output: ['trx_init.c'], command: [python3, meson.project_source_root() + '/../tools/generate_init', '-o', meson.current_build_dir() / '@OUTPUT0@'], build_always_stale: true, ) trx_version_rc = custom_target( 'trx_fake_version', output: ['trx_version.rc'], command: [python3, meson.project_source_root() + '/../tools/generate_rcfile', '-o', '@OUTPUT0@'], build_always_stale: true, ) trx_icon_rc = custom_target( 'trx_fake_icon', output: ['trx_icon.rc'], command: [python3, meson.project_source_root() + '/../tools/generate_rcfile', '-o', '@OUTPUT0@'], ) trx_resources = [] if host_machine.system() == 'windows' windows = import('windows') trx_resources = [ windows.compile_resources(trx_version_rc), windows.compile_resources(trx_icon_rc), ] endif trx_sources = common_sources + [trx_init, trx_resources, trx_lua_embed] executable( 'TRX', trx_sources, dependencies: dependencies, link_args: link_args, win_subsystem: 'windows', install: true, install_tag: 'common', c_args: ['-I.'], ) if host_machine.system() == 'darwin' mac_install_tree = files('../tools/shared/mac/install_tree') foreach game_id : ['tr1', 'tr2', 'tr3'] meson.add_install_script(mac_install_tree, '--source', '../data/@0@/ship/cfg'.format(game_id), '--dest', 'Contents/Resources/cfg', install_tag: game_id) meson.add_install_script(mac_install_tree, '--source', '../data/@0@/ship/data'.format(game_id), '--dest', 'Contents/Resources/data', install_tag: game_id) meson.add_install_script(mac_install_tree, '--source', '../data/common/ship/cfg', '--dest', 'Contents/Resources/cfg', install_tag: game_id) meson.add_install_script(mac_install_tree, '--source', '../data/common/ship/shaders', '--dest', 'Contents/Resources/shaders', install_tag: game_id) install_data('../data/@0@/mac/icon.icns'.format(game_id), install_dir: 'Contents/Resources', install_tag: game_id) install_data('../data/@0@/mac/Info.plist'.format(game_id), install_dir: 'Contents', install_tag: game_id) endforeach meson.add_install_script('../tools/shared/mac/bundle_dylibs', '-a', 'TR1X', install_tag: 'tr1') meson.add_install_script('../tools/shared/mac/bundle_dylibs', '-a', 'TR2X', install_tag: 'tr2') meson.add_install_script('../tools/shared/mac/bundle_dylibs', '-a', 'TR3X', install_tag: 'tr3') endif ================================================ FILE: src/meson.options ================================================ option('staticdeps', type: 'boolean', value: true, description: 'Try to build against static dependencies. default: true') ================================================ FILE: src/subprojects/dwarfstack.wrap ================================================ [wrap-file] directory = dwarfstack-2.2 source_url = https://github.com/ssbssa/dwarfstack/archive/refs/tags/2.2.tar.gz source_filename = dwarfstack-2.2.tar.gz source_hash = 1fca1d12756941c4c932b50f9abe56a3756f012a1e93deef553e141a78cc2709 patch_directory = dwarfstack [provide] dwarfstack = dwarfstack_dep ================================================ FILE: src/subprojects/packagefiles/dwarfstack/meson.build ================================================ project( 'dwarfstack', 'c', default_options: [ 'c_std=c2x', 'warning_level=2', ], ) c_compiler = meson.get_compiler('c') build_opts = [ '-Wno-unused', '-DDWST_STATIC', '-DNO_DBGHELP', '-DLIBDWARF_STATIC', ] add_project_arguments(build_opts, language: 'c') dep_zlib = dependency('zlib') sources = [ 'mgwhelp/dwarf_pe.c', 'src/dwst-exception-dialog.c', 'src/dwst-exception.c', 'src/dwst-process.c', 'src/dwst-location.c', 'src/dwst-file.c', 'libdwarf/dwarf_debugnames.c', 'libdwarf/dwarf_dsc.c', 'libdwarf/dwarf_alloc.c', 'libdwarf/dwarf_loclists.c', 'libdwarf/dwarf_macro5.c', 'libdwarf/dwarf_harmless.c', 'libdwarf/dwarf_locationop_read.c', 'libdwarf/dwarf_gnu_index.c', 'libdwarf/dwarf_rnglists.c', 'libdwarf/dwarf_error.c', 'libdwarf/dwarf_init_finish.c', 'libdwarf/dwarf_abbrev.c', 'libdwarf/dwarf_xu_index.c', 'libdwarf/dwarf_names.c', 'libdwarf/dwarf_str_offsets.c', 'libdwarf/dwarf_tsearchhash.c', 'libdwarf/dwarf_die_deliv.c', 'libdwarf/dwarf_frame.c', 'libdwarf/dwarf_query.c', 'libdwarf/dwarf_global.c', 'libdwarf/dwarf_loc.c', 'libdwarf/dwarf_tied.c', 'libdwarf/dwarf_util.c', 'libdwarf/dwarf_form.c', 'libdwarf/dwarf_groups.c', 'libdwarf/dwarf_frame2.c', 'libdwarf/dwarf_memcpy_swap.c', 'libdwarf/dwarf_leb.c', 'libdwarf/dwarf_debuglink.c', 'libdwarf/dwarf_string.c', 'libdwarf/dwarf_line.c', 'libdwarf/dwarf_fission_to_cu.c', 'libdwarf/dwarf_find_sigref.c', 'libdwarf/dwarf_ranges.c', ] dependencies = [ dep_zlib, ] libdwarfstack = static_library( 'libdwarfstack', sources, dependencies: dependencies, include_directories: [ 'include/', 'mgwhelp/', 'src/', 'libdwarf/', ] ) dep_dwarfstack = declare_dependency( link_whole: libdwarfstack, include_directories: [ include_directories('include', is_system: true) ] ) ================================================ FILE: src/trx/av/audio.c ================================================ #include #include #include #include #include #include #include #include SDL_AudioDeviceID g_AudioDeviceID = 0; static int32_t m_RefCount = 0; static size_t m_MixBufferCapacity = 0; static float *m_MixBuffer = nullptr; static Uint8 m_Silence = 0; static bool m_Muted = false; static bool m_CallbackSeen = false; static bool m_ShouldSkipSDLQuitAudio = false; static void M_MixerCallback(void *userdata, Uint8 *stream_data, int32_t len) { m_CallbackSeen = true; memset(m_MixBuffer, m_Silence, len); Audio_Sample_Mix(m_MixBuffer, len); Audio_Reverb_Process(m_MixBuffer, len); Audio_Stream_Mix(m_MixBuffer, len); if (m_Muted) { memset(m_MixBuffer, m_Silence, len); } memcpy(stream_data, m_MixBuffer, len); } bool Audio_Init(void) { m_RefCount++; if (g_AudioDeviceID) { // already initialized return true; } m_CallbackSeen = false; m_ShouldSkipSDLQuitAudio = false; int32_t result = SDL_InitSubSystem(SDL_INIT_AUDIO); if (result < 0) { LOG_ERROR("Error while calling SDL_Init: 0x%lx", result); return false; } SDL_AudioSpec desired; SDL_memset(&desired, 0, sizeof(desired)); desired.freq = AUDIO_WORKING_RATE; desired.format = AUDIO_WORKING_FORMAT; desired.channels = AUDIO_WORKING_CHANNELS; desired.samples = AUDIO_SAMPLES; desired.callback = M_MixerCallback; desired.userdata = nullptr; SDL_AudioSpec delivered; g_AudioDeviceID = SDL_OpenAudioDevice(nullptr, 0, &desired, &delivered, 0); if (!g_AudioDeviceID) { LOG_ERROR("Failed to open audio device: %s", SDL_GetError()); return false; } m_Silence = desired.silence; m_MixBufferCapacity = desired.samples * desired.channels * SDL_AUDIO_BITSIZE(desired.format) / 8; m_MixBuffer = Memory_Alloc(m_MixBufferCapacity); SDL_PauseAudioDevice(g_AudioDeviceID, 0); Audio_Sample_Init(); Audio_Stream_Init(); Audio_Reverb_Init(AUDIO_WORKING_RATE, AUDIO_WORKING_CHANNELS); return true; } bool Audio_Shutdown(void) { m_RefCount--; if (m_RefCount > 0) { return false; } if (g_AudioDeviceID) { SDL_PauseAudioDevice(g_AudioDeviceID, 1); if (!m_CallbackSeen) { m_ShouldSkipSDLQuitAudio = true; } else { SDL_CloseAudioDevice(g_AudioDeviceID); } g_AudioDeviceID = 0; } Memory_FreePointer(&m_MixBuffer); Audio_Sample_Shutdown(); Audio_Stream_Shutdown(); Audio_Reverb_Shutdown(); if (!m_ShouldSkipSDLQuitAudio) { SDL_QuitSubSystem(SDL_INIT_AUDIO); } return true; } bool Audio_ShouldSkipSDLQuitAudio(void) { return m_ShouldSkipSDLQuitAudio; } void Audio_Mute(void) { m_Muted = true; } void Audio_Unmute(void) { m_Muted = false; } bool Audio_IsMuted(void) { return m_Muted; } void Audio_LockDevice(void) { if (g_AudioDeviceID == 0) { return; } SDL_LockAudioDevice(g_AudioDeviceID); } void Audio_UnlockDevice(void) { if (g_AudioDeviceID == 0) { return; } SDL_UnlockAudioDevice(g_AudioDeviceID); } void Audio_SetReverbType(const uint8_t reverb_type) { if (g_AudioDeviceID) { SDL_LockAudioDevice(g_AudioDeviceID); } Audio_Reverb_SetType(reverb_type); if (g_AudioDeviceID) { SDL_UnlockAudioDevice(g_AudioDeviceID); } } uint8_t Audio_GetReverbType(void) { uint8_t reverb_type = 0; if (g_AudioDeviceID) { SDL_LockAudioDevice(g_AudioDeviceID); } reverb_type = Audio_Reverb_GetType(); if (g_AudioDeviceID) { SDL_UnlockAudioDevice(g_AudioDeviceID); } return reverb_type; } int32_t Audio_GetAVChannelLayout(const int32_t channels) { switch (channels) { // clang-format off case 1: return AV_CH_LAYOUT_MONO; case 2: return AV_CH_LAYOUT_STEREO; default: return AV_CH_LAYOUT_MONO; // clang-format on } } int32_t Audio_GetAVAudioFormat(const int32_t sample_fmt) { switch (sample_fmt) { // clang-format off case AUDIO_U8: return AV_SAMPLE_FMT_U8; case AUDIO_S16: return AV_SAMPLE_FMT_S16; case AUDIO_S32: return AV_SAMPLE_FMT_S32; case AUDIO_F32: return AV_SAMPLE_FMT_FLT; default: return -1; // clang-format on } } int32_t Audio_GetSDLAudioFormat(const enum AVSampleFormat sample_fmt) { // clang-format off switch (sample_fmt) { case AV_SAMPLE_FMT_U8: return AUDIO_U8; case AV_SAMPLE_FMT_S16: return AUDIO_S16; case AV_SAMPLE_FMT_S32: return AUDIO_S32; case AV_SAMPLE_FMT_FLT: return AUDIO_F32; default: return -1; } // clang-format on } ================================================ FILE: src/trx/av/audio.h ================================================ #pragma once #include #include #include #include #define AUDIO_MAX_SAMPLES 1000 #define AUDIO_MAX_ACTIVE_SAMPLES 50 #define AUDIO_MAX_ACTIVE_STREAMS 10 #define AUDIO_DRIFT_THRESHOLD 0.2 #define AUDIO_NO_SOUND (-1) bool Audio_Init(void); bool Audio_Shutdown(void); bool Audio_ShouldSkipSDLQuitAudio(void); void Audio_Mute(void); void Audio_Unmute(void); bool Audio_IsMuted(void); bool Audio_Stream_Pause(int32_t sound_id); bool Audio_Stream_Unpause(int32_t sound_id); bool Audio_Stream_SetPaused(int32_t sound_id, bool is_paused); int32_t Audio_Stream_CreateFromFile(const char *path); int32_t Audio_Stream_CreateFromMemory(uint8_t *data, size_t size); bool Audio_Stream_Close(int32_t sound_id); bool Audio_Stream_IsLooped(int32_t sound_id); bool Audio_Stream_SetVolume(int32_t sound_id, float volume); bool Audio_Stream_SetIsLooped(int32_t sound_id, bool is_looped); // Sync the audio against specific timestamp (seek if the drift is too large). bool Audio_Stream_SyncTimestamp(int32_t sound_id, double timestamp); bool Audio_Stream_SetFinishCallback( int32_t sound_id, void (*callback)(int32_t sound_id, void *user_data), void *user_data); double Audio_Stream_GetTimestamp(int32_t sound_id); double Audio_Stream_GetDuration(int32_t sound_id); bool Audio_Stream_SeekTimestamp(int32_t sound_id, double timestamp); bool Audio_Stream_SetStartTimestamp(int32_t sound_id, double timestamp); bool Audio_Stream_SetStopTimestamp(int32_t sound_id, double timestamp); bool Audio_Sample_Load(int32_t sample_num, const char *content, size_t size); bool Audio_Sample_Unload(int32_t sample_id); bool Audio_Sample_UnloadAll(void); int32_t Audio_Sample_Play( int32_t sample_id, int32_t volume, float pitch, int32_t pan, bool is_looped); bool Audio_Sample_IsPlaying(int32_t sound_id); bool Audio_Sample_Pause(int32_t sound_id); bool Audio_Sample_PauseAll(void); bool Audio_Sample_Unpause(int32_t sound_id); bool Audio_Sample_UnpauseAll(void); bool Audio_Sample_Close(int32_t sound_id); bool Audio_Sample_CloseAll(void); bool Audio_Sample_SetPan(int32_t sound_id, int32_t pan); bool Audio_Sample_SetVolume(int32_t sound_id, int32_t volume); bool Audio_Sample_SetPitch(int32_t sound_id, float pan); void Audio_SetReverbType(uint8_t reverb_type); uint8_t Audio_GetReverbType(void); ================================================ FILE: src/trx/av/audio_internal.h ================================================ #pragma once #include #include #include #define AUDIO_WORKING_RATE 44100 #define AUDIO_WORKING_FORMAT AUDIO_F32 #define AUDIO_SAMPLES 500 #define AUDIO_WORKING_CHANNELS 2 extern SDL_AudioDeviceID g_AudioDeviceID; void Audio_LockDevice(void); void Audio_UnlockDevice(void); int32_t Audio_GetAVChannelLayout(int32_t sample_fmt); int32_t Audio_GetAVAudioFormat(int32_t sample_fmt); int32_t Audio_GetSDLAudioFormat(enum AVSampleFormat sample_fmt); void Audio_Sample_Init(void); void Audio_Sample_Shutdown(void); void Audio_Sample_Mix(float *dst_buffer, size_t len); void Audio_Stream_Init(void); void Audio_Stream_Shutdown(void); void Audio_Stream_Mix(float *dst_buffer, size_t len); void Audio_Reverb_Init(int32_t sample_rate, int32_t channels); void Audio_Reverb_Shutdown(void); void Audio_Reverb_Process(float *dst_buffer, size_t len); void Audio_Reverb_SetType(uint8_t reverb_type); uint8_t Audio_Reverb_GetType(void); ================================================ FILE: src/trx/av/audio_reverb.c ================================================ // Based on Wine/FAudio reverb DSP (FAudioFX_reverb.c). Adapted for TRX. // Original authors: Ethan Lee, Luigi Auriemma, and the MonoGame Team. // License: zlib (see wine/libs/faudio/src/FAudioFX_reverb.c). #include #include #include #include #include #include #include #define M_PRESET_COUNT 3 #define M_REVERB_DEFAULT_REAR_DELAY 5 #define M_REVERB_DEFAULT_POSITION 6 #define M_REVERB_DEFAULT_POSITION_MATRIX 27 #define M_REVERB_DEFAULT_ROOM_SIZE 100.0f #define M_REVERB_DEFAULT_WET_DRY_MIX 100.0f #define M_REVERB_DEFAULT_ROOM_FILTER_FREQ 5000.0f #define M_REVERB_DEFAULT_ROOM_FILTER_MAIN 0.0f #define M_REVERB_DEFAULT_ROOM_FILTER_HF 0.0f #define M_REVERB_DEFAULT_REFLECTIONS_GAIN 0.0f #define M_REVERB_DEFAULT_REVERB_GAIN 0.0f #define M_REVERB_DEFAULT_DECAY_TIME 1.0f #define M_REVERB_DEFAULT_DENSITY 100.0f #define M_REVERB_MAX_REFLECTIONS_DELAY 300 #define M_REVERB_MAX_REVERB_DELAY 85 #define M_REVERB_MIN_DECAY_TIME 0.1f #define M_REVERB_DENORMAL_EPSILON 1e-15f #define M_REVERB_WET_GAIN 1.20f // TRX addition #define M_DELAY_MAX_MS 300 #define M_REVERB_COUNT_COMB 8 #define M_REVERB_COUNT_APF_IN 1 #define M_REVERB_COUNT_APF_OUT 4 typedef struct { float wet_dry_mix; int32_t room; int32_t room_hf; float room_rolloff_factor; float decay_time; float decay_hf_ratio; int32_t reflections; float reflections_delay; int32_t reverb; float reverb_delay; float diffusion; float density; float hf_reference; } M_I3DL2_PARAMETERS; typedef struct { float wet_dry_mix; uint32_t reflections_delay; uint8_t reverb_delay; uint8_t rear_delay; uint8_t position_left; uint8_t position_right; uint8_t position_matrix_left; uint8_t position_matrix_right; uint8_t early_diffusion; uint8_t late_diffusion; uint8_t low_eq_gain; uint8_t low_eq_cutoff; uint8_t high_eq_gain; uint8_t high_eq_cutoff; float room_filter_freq; float room_filter_main; float room_filter_hf; float reflections_gain; float reverb_gain; float decay_time; float density; float room_size; } M_PARAMETERS; typedef struct { int32_t sample_rate; uint32_t capacity; uint32_t delay; uint32_t read_idx; uint32_t write_idx; float *buffer; } M_DSP_DELAY; typedef enum { M_DSP_BI_QUAD_LOW_SHELVING, M_DSP_BI_QUAD_HIGH_SHELVING, } M_DSP_BI_QUAD_TYPE; typedef struct { int32_t sample_rate; float a0; float a1; float a2; float b1; float b2; float c0; float d0; float delay0; float delay1; } M_DSP_BI_QUAD; typedef struct { M_DSP_DELAY comb_delay; M_DSP_BI_QUAD low_shelving; M_DSP_BI_QUAD high_shelving; float comb_feedback_gain; } M_DSP_COMB_SHELVING; typedef struct { M_DSP_DELAY apf_delay; float apf_feedback_gain; } M_DSP_ALL_PASS; typedef enum { M_POSITION_LEFT = 1, M_POSITION_RIGHT = 2, M_POSITION_CENTER = 4, M_POSITION_REAR = 8 } M_CHANNEL_POSITION_FLAGS; typedef struct { M_DSP_DELAY reverb_delay; M_DSP_COMB_SHELVING lpf_comb[M_REVERB_COUNT_COMB]; M_DSP_ALL_PASS apf_out[M_REVERB_COUNT_APF_OUT]; M_DSP_BI_QUAD room_high_shelf; float early_gain; float gain; } M_DSP_REVERB_CHANNEL; typedef struct { M_DSP_DELAY early_delay; M_DSP_ALL_PASS apf_in[M_REVERB_COUNT_APF_IN]; int32_t in_channels; int32_t out_channels; int32_t reverb_channels; M_DSP_REVERB_CHANNEL channel[2]; float early_gain; float reverb_gain; float room_gain; float wet_ratio; float dry_ratio; } M_DSP_REVERB; static const float m_CombDelays[M_REVERB_COUNT_COMB] = { 25.31f, 26.94f, 28.96f, 30.75f, 32.24f, 33.80f, 35.31f, 36.67f, }; static const float m_ApfInDelays[M_REVERB_COUNT_APF_IN] = { 13.28f }; static const float m_ApfOutDelays[M_REVERB_COUNT_APF_OUT] = { 5.10f, 12.61f, 10.0f, 7.73f, }; static const M_I3DL2_PARAMETERS m_ReverbPresets[M_PRESET_COUNT] = { // clang-format off { 50.0f, -1000, -500, 0.0f, 2.31f, 0.64f, -711, 0.012f, -800, 0.017f, 100.0f, 100.0f, 5000.0f, }, // Small Room { 50.0f, -1000, -500, 0.0f, 2.31f, 0.64f, -711, 0.012f, -300, 0.017f, 100.0f, 100.0f, 5000.0f, }, // Medium Room { 50.0f, -1000, -500, 0.0f, 2.31f, 0.64f, -711, 0.012f, 200, 0.017f, 100.0f, 100.0f, 5000.0f, } // Large Room // clang-format on }; static bool m_IsInitialised = false; static uint8_t m_ReverbType = 0; static M_PARAMETERS m_ReverbTypes[M_PRESET_COUNT]; static M_DSP_REVERB m_Reverb; static inline float M_DbGainToFactor(const float gain) { return powf(10.0f, gain / 20.0f); } static inline uint32_t M_MsToSamples( const float msec, const int32_t sample_rate) { return (uint32_t)((sample_rate * msec) / 1000.0f); } static inline float M_Undenormalize(const float sample_in) { return fabsf(sample_in) < M_REVERB_DENORMAL_EPSILON ? 0.0f : sample_in; } static inline void M_DspDelay_Initialize( M_DSP_DELAY *const filter, const int32_t sample_rate, const float delay_ms) { ASSERT(delay_ms >= 0.0f && delay_ms <= M_DELAY_MAX_MS); filter->sample_rate = sample_rate; filter->capacity = M_MsToSamples(M_DELAY_MAX_MS, sample_rate); filter->delay = M_MsToSamples(delay_ms, sample_rate); filter->read_idx = 0; filter->write_idx = filter->delay; filter->buffer = Memory_Alloc(filter->capacity * sizeof(float)); } static inline void M_DspDelay_Change( M_DSP_DELAY *const filter, const float delay_ms) { ASSERT(delay_ms >= 0.0f && delay_ms <= M_DELAY_MAX_MS); filter->delay = M_MsToSamples(delay_ms, filter->sample_rate); filter->read_idx = (filter->write_idx - filter->delay + filter->capacity) % filter->capacity; } static inline float M_DspDelay_Read(M_DSP_DELAY *const filter) { ASSERT(filter->read_idx < filter->capacity); const float delay_out = filter->buffer[filter->read_idx]; filter->read_idx = (filter->read_idx + 1) % filter->capacity; return delay_out; } static inline void M_DspDelay_Write( M_DSP_DELAY *const filter, const float sample) { ASSERT(filter->write_idx < filter->capacity); filter->buffer[filter->write_idx] = M_Undenormalize(sample); filter->write_idx = (filter->write_idx + 1) % filter->capacity; } static inline float M_DspDelay_Process( M_DSP_DELAY *const filter, const float sample) { const float delay_out = M_DspDelay_Read(filter); M_DspDelay_Write(filter, sample); return delay_out; } static inline void M_DspDelay_Reset(M_DSP_DELAY *const filter) { filter->read_idx = 0; filter->write_idx = filter->delay; memset(filter->buffer, 0, filter->capacity * sizeof(float)); } static inline void M_DspDelay_Destroy(M_DSP_DELAY *const filter) { Memory_Free(filter->buffer); filter->buffer = nullptr; } static inline float M_DspComb_FeedbackFromRT60( M_DSP_DELAY *const delay, const float rt60_ms) { const float exponent = (-3.0f * (float)delay->delay * 1000.0f) / ((float)delay->sample_rate * rt60_ms); return powf(10.0f, exponent); } static inline void M_DspBiQuad_Change( M_DSP_BI_QUAD *const filter, const M_DSP_BI_QUAD_TYPE type, const float frequency, const float q, const float gain) { const float theta_c = (2 * M_PI * frequency) / (float)filter->sample_rate; const float mu = M_DbGainToFactor(gain); const float beta = type == M_DSP_BI_QUAD_LOW_SHELVING ? 4.0f / (1.0f + mu) : (1.0f + mu) / 4.0f; const float delta = beta * tanf(theta_c * 0.5f); const float gamma = (1.0f - delta) / (1.0f + delta); if (type == M_DSP_BI_QUAD_LOW_SHELVING) { filter->a0 = (1.0f - gamma) * 0.5f; filter->a1 = filter->a0; } else { filter->a0 = (1.0f + gamma) * 0.5f; filter->a1 = -filter->a0; } filter->a2 = 0.0f; filter->b1 = -gamma; filter->b2 = 0.0f; filter->c0 = mu - 1.0f; filter->d0 = 1.0f; } static inline void M_DspBiQuad_Initialize( M_DSP_BI_QUAD *const filter, const int32_t sample_rate, const M_DSP_BI_QUAD_TYPE type, const float frequency, const float q, const float gain) { filter->sample_rate = sample_rate; filter->delay0 = 0.0f; filter->delay1 = 0.0f; M_DspBiQuad_Change(filter, type, frequency, q, gain); } static inline float M_DspBiQuad_Process( M_DSP_BI_QUAD *const filter, const float sample_in) { const float result = (filter->a0 * sample_in) + filter->delay0; const float delay0 = (filter->a1 * sample_in) - (filter->b1 * result) + filter->delay1; const float delay1 = (filter->a2 * sample_in) - (filter->b2 * result); filter->delay0 = M_Undenormalize(delay0); filter->delay1 = M_Undenormalize(delay1); return M_Undenormalize((result * filter->c0) + (sample_in * filter->d0)); } static inline void M_DspBiQuad_Reset(M_DSP_BI_QUAD *const filter) { filter->delay0 = 0.0f; filter->delay1 = 0.0f; } static inline void M_DspBiQuad_Destroy(M_DSP_BI_QUAD *const filter) { (void)filter; } static inline void M_DspCombShelving_Initialize( M_DSP_COMB_SHELVING *const filter, const int32_t sample_rate, const float delay_ms, const float rt60_ms, const float low_frequency, const float low_q, const float low_gain, const float high_frequency, const float high_q, const float high_gain) { M_DspDelay_Initialize(&filter->comb_delay, sample_rate, delay_ms); filter->comb_feedback_gain = M_DspComb_FeedbackFromRT60(&filter->comb_delay, rt60_ms); M_DspBiQuad_Initialize( &filter->low_shelving, sample_rate, M_DSP_BI_QUAD_LOW_SHELVING, low_frequency, low_q, low_gain); M_DspBiQuad_Initialize( &filter->high_shelving, sample_rate, M_DSP_BI_QUAD_HIGH_SHELVING, high_frequency, high_q, high_gain); } static inline float M_DspCombShelving_Process( M_DSP_COMB_SHELVING *const filter, const float sample_in) { const float delay_out = M_DspDelay_Read(&filter->comb_delay); float feedback = M_DspBiQuad_Process(&filter->high_shelving, delay_out); feedback = M_DspBiQuad_Process(&filter->low_shelving, feedback); const float to_buf = M_Undenormalize(sample_in + (filter->comb_feedback_gain * feedback)); M_DspDelay_Write(&filter->comb_delay, to_buf); return delay_out; } static inline void M_DspCombShelving_Reset(M_DSP_COMB_SHELVING *const filter) { M_DspDelay_Reset(&filter->comb_delay); M_DspBiQuad_Reset(&filter->low_shelving); M_DspBiQuad_Reset(&filter->high_shelving); } static inline void M_DspCombShelving_Destroy(M_DSP_COMB_SHELVING *const filter) { M_DspDelay_Destroy(&filter->comb_delay); M_DspBiQuad_Destroy(&filter->low_shelving); M_DspBiQuad_Destroy(&filter->high_shelving); } static inline void M_DspAllPass_Initialize( M_DSP_ALL_PASS *const filter, const int32_t sample_rate, const float delay_ms, const float gain) { M_DspDelay_Initialize(&filter->apf_delay, sample_rate, delay_ms); filter->apf_feedback_gain = gain; } static inline void M_DspAllPass_Change( M_DSP_ALL_PASS *const filter, const float delay_ms, const float gain) { M_DspDelay_Change(&filter->apf_delay, delay_ms); filter->apf_feedback_gain = gain; } static inline float M_DspAllPass_Process( M_DSP_ALL_PASS *const filter, const float sample_in) { const float delay_out = M_DspDelay_Read(&filter->apf_delay); const float to_buf = M_Undenormalize(sample_in + (filter->apf_feedback_gain * delay_out)); M_DspDelay_Write(&filter->apf_delay, to_buf); return M_Undenormalize(delay_out - (filter->apf_feedback_gain * to_buf)); } static inline void M_DspAllPass_Reset(M_DSP_ALL_PASS *const filter) { M_DspDelay_Reset(&filter->apf_delay); } static inline void M_DspAllPass_Destroy(M_DSP_ALL_PASS *const filter) { M_DspDelay_Destroy(&filter->apf_delay); } static inline M_CHANNEL_POSITION_FLAGS M_GetChannelPositionFlags( const int32_t total_channels, const int32_t channel) { switch (total_channels) { case 1: return M_POSITION_CENTER; case 2: return channel == 0 ? M_POSITION_LEFT : M_POSITION_RIGHT; default: break; } ASSERT(0 && "Unsupported channel count"); return M_POSITION_LEFT; } static inline float M_GetStereoSpreadDelayMS( const int32_t total_channels, const int32_t channel) { const M_CHANNEL_POSITION_FLAGS flags = M_GetChannelPositionFlags(total_channels, channel); return (flags & M_POSITION_RIGHT) != 0 ? 0.5216f : 0.0f; } static inline void M_DspReverb_Create( M_DSP_REVERB *const reverb, const int32_t sample_rate, const int32_t in_channels, const int32_t out_channels) { ASSERT(in_channels == 1 || in_channels == 2); ASSERT(out_channels == 1 || out_channels == 2); memset(reverb, 0, sizeof(*reverb)); M_DspDelay_Initialize(&reverb->early_delay, sample_rate, 10.0f); for (int32_t i = 0; i < M_REVERB_COUNT_APF_IN; i++) { M_DspAllPass_Initialize( &reverb->apf_in[i], sample_rate, m_ApfInDelays[i], 0.5f); } reverb->reverb_channels = out_channels; for (int32_t c = 0; c < reverb->reverb_channels; c++) { M_DspDelay_Initialize( &reverb->channel[c].reverb_delay, sample_rate, 10.0f); for (int32_t i = 0; i < M_REVERB_COUNT_COMB; i++) { M_DspCombShelving_Initialize( &reverb->channel[c].lpf_comb[i], sample_rate, m_CombDelays[i] + M_GetStereoSpreadDelayMS(reverb->reverb_channels, c), 500.0f, 500.0f, 0.0f, -6.0f, 5000.0f, 0.0f, -6.0f); } for (int32_t i = 0; i < M_REVERB_COUNT_APF_OUT; i++) { M_DspAllPass_Initialize( &reverb->channel[c].apf_out[i], sample_rate, m_ApfOutDelays[i] + M_GetStereoSpreadDelayMS(reverb->reverb_channels, c), 0.5f); } M_DspBiQuad_Initialize( &reverb->channel[c].room_high_shelf, sample_rate, M_DSP_BI_QUAD_HIGH_SHELVING, 5000.0f, 0.0f, -10.0f); reverb->channel[c].gain = 1.0f; } reverb->early_gain = 1.0f; reverb->reverb_gain = 1.0f; reverb->dry_ratio = 0.0f; reverb->wet_ratio = 1.0f; reverb->in_channels = in_channels; reverb->out_channels = out_channels; } static inline void M_DspReverb_Destroy(M_DSP_REVERB *const reverb) { M_DspDelay_Destroy(&reverb->early_delay); for (int32_t i = 0; i < M_REVERB_COUNT_APF_IN; i++) { M_DspAllPass_Destroy(&reverb->apf_in[i]); } for (int32_t c = 0; c < reverb->reverb_channels; c++) { M_DspDelay_Destroy(&reverb->channel[c].reverb_delay); for (int32_t i = 0; i < M_REVERB_COUNT_COMB; i++) { M_DspCombShelving_Destroy(&reverb->channel[c].lpf_comb[i]); } M_DspBiQuad_Destroy(&reverb->channel[c].room_high_shelf); for (int32_t i = 0; i < M_REVERB_COUNT_APF_OUT; i++) { M_DspAllPass_Destroy(&reverb->channel[c].apf_out[i]); } } } static inline void M_DspReverb_SetParameters( M_DSP_REVERB *const reverb, const M_PARAMETERS *const params) { const float early_diffusion = 0.6f - ((params->early_diffusion / 15.0f) * 0.2f); M_DspDelay_Change(&reverb->early_delay, (float)params->reflections_delay); for (int32_t i = 0; i < M_REVERB_COUNT_APF_IN; i++) { M_DspAllPass_Change( &reverb->apf_in[i], m_ApfInDelays[i], early_diffusion); } for (int32_t c = 0; c < reverb->reverb_channels; c++) { const M_CHANNEL_POSITION_FLAGS position = M_GetChannelPositionFlags(reverb->reverb_channels, c); const float channel_delay = (position & M_POSITION_REAR) != 0 ? params->rear_delay : 0.0f; M_DspDelay_Change( &reverb->channel[c].reverb_delay, (float)params->reverb_delay + channel_delay); for (int32_t i = 0; i < M_REVERB_COUNT_COMB; i++) { M_DSP_COMB_SHELVING *const comb = &reverb->channel[c].lpf_comb[i]; M_DspDelay_Change( &comb->comb_delay, m_CombDelays[i] + M_GetStereoSpreadDelayMS(reverb->reverb_channels, c)); comb->comb_feedback_gain = M_DspComb_FeedbackFromRT60( &comb->comb_delay, MAX(params->decay_time, M_REVERB_MIN_DECAY_TIME) * 1000.0f); M_DspBiQuad_Change( &comb->low_shelving, M_DSP_BI_QUAD_LOW_SHELVING, 50.0f + params->low_eq_cutoff * 50.0f, 0.0f, params->low_eq_gain - 8.0f); M_DspBiQuad_Change( &comb->high_shelving, M_DSP_BI_QUAD_HIGH_SHELVING, 1000.0f + params->high_eq_cutoff * 500.0f, 0.0f, params->high_eq_gain - 8.0f); } } reverb->early_gain = M_DbGainToFactor(params->reflections_gain); reverb->reverb_gain = M_DbGainToFactor(params->reverb_gain); reverb->room_gain = M_DbGainToFactor(params->room_filter_main); const float late_diffusion = 0.6f - ((params->late_diffusion / 15.0f) * 0.2f); for (int32_t c = 0; c < reverb->reverb_channels; c++) { const M_CHANNEL_POSITION_FLAGS position = M_GetChannelPositionFlags(reverb->reverb_channels, c); for (int32_t i = 0; i < M_REVERB_COUNT_APF_OUT; i++) { M_DspAllPass_Change( &reverb->channel[c].apf_out[i], m_ApfOutDelays[i] + M_GetStereoSpreadDelayMS(reverb->reverb_channels, c), late_diffusion); } M_DspBiQuad_Change( &reverb->channel[c].room_high_shelf, M_DSP_BI_QUAD_HIGH_SHELVING, params->room_filter_freq, 0.0f, params->room_filter_main + params->room_filter_hf); float gain = 0.0f; if ((position & M_POSITION_LEFT) != 0) { gain = params->position_matrix_left; } else if ((position & M_POSITION_RIGHT) != 0) { gain = params->position_matrix_right; } else { gain = (params->position_matrix_left + params->position_matrix_right) / 2.0f; } reverb->channel[c].gain = 1.5f - (gain / 27.0f) * 0.5f; if ((position & M_POSITION_REAR) != 0) { reverb->channel[c].gain *= 0.75f; } if ((position & M_POSITION_LEFT) != 0) { gain = params->position_left; } else if ((position & M_POSITION_RIGHT) != 0) { gain = params->position_right; } else { gain = (params->position_left + params->position_right) / 2.0f; } reverb->channel[c].early_gain = (1.2f - (gain / 6.0f) * 0.2f) * reverb->early_gain; } reverb->wet_ratio = params->wet_dry_mix / 100.0f; reverb->dry_ratio = 1.0f - reverb->wet_ratio; } static inline void M_DspReverb_Reset(M_DSP_REVERB *const reverb) { M_DspDelay_Reset(&reverb->early_delay); for (int32_t i = 0; i < M_REVERB_COUNT_APF_IN; i++) { M_DspAllPass_Reset(&reverb->apf_in[i]); } for (int32_t c = 0; c < reverb->reverb_channels; c++) { M_DspDelay_Reset(&reverb->channel[c].reverb_delay); for (int32_t i = 0; i < M_REVERB_COUNT_COMB; i++) { M_DspCombShelving_Reset(&reverb->channel[c].lpf_comb[i]); } for (int32_t i = 0; i < M_REVERB_COUNT_APF_OUT; i++) { M_DspAllPass_Reset(&reverb->channel[c].apf_out[i]); } M_DspBiQuad_Reset(&reverb->channel[c].room_high_shelf); } } static inline float M_DspReverb_ProcessEarly( M_DSP_REVERB *const reverb, const float sample_in) { float delay_out = M_DspDelay_Process(&reverb->early_delay, sample_in); for (int32_t i = 0; i < M_REVERB_COUNT_APF_IN; i++) { delay_out = M_DspAllPass_Process(&reverb->apf_in[i], delay_out); } return delay_out; } static inline float M_DspReverb_ProcessChannel( M_DSP_REVERB *const reverb, M_DSP_REVERB_CHANNEL *const channel, const float sample_in) { float sample_out = 0.0f; float early_late = 0.0f; const float revdelay = M_DspDelay_Process(&channel->reverb_delay, sample_in); for (int32_t i = 0; i < M_REVERB_COUNT_COMB; i++) { sample_out += M_DspCombShelving_Process(&channel->lpf_comb[i], revdelay); } sample_out /= (float)M_REVERB_COUNT_COMB; for (int32_t i = 0; i < M_REVERB_COUNT_APF_OUT; i++) { sample_out = M_DspAllPass_Process(&channel->apf_out[i], sample_out); } early_late = (sample_in * channel->early_gain) + (sample_out * reverb->reverb_gain); sample_out = M_DspBiQuad_Process( &channel->room_high_shelf, early_late * reverb->room_gain); return sample_out * channel->gain; } static inline void M_DspReverb_Process_2_to_2( M_DSP_REVERB *const reverb, float *samples, const size_t sample_count) { float *const samples_end = samples + (sample_count * 2); while (samples < samples_end) { const float in = (samples[0] + samples[1]) * 0.5f; const float early = M_DspReverb_ProcessEarly(reverb, in); const float left = (M_DspReverb_ProcessChannel(reverb, &reverb->channel[0], early) * reverb->wet_ratio * M_REVERB_WET_GAIN) + samples[0] * reverb->dry_ratio; const float right = (M_DspReverb_ProcessChannel(reverb, &reverb->channel[1], early) * reverb->wet_ratio * M_REVERB_WET_GAIN) + samples[1] * reverb->dry_ratio; samples[0] = M_Undenormalize(left); samples[1] = M_Undenormalize(right); samples += 2; } } static inline void M_DspReverb_Process_1_to_1( M_DSP_REVERB *const reverb, float *samples, const size_t sample_count) { float *const samples_end = samples + sample_count; while (samples < samples_end) { const float in = *samples; const float early = M_DspReverb_ProcessEarly(reverb, in); const float late = M_DspReverb_ProcessChannel(reverb, &reverb->channel[0], early); *samples = M_Undenormalize( (late * reverb->wet_ratio) + (in * reverb->dry_ratio)); samples++; } } static inline void M_ConvertI3DL2ToNative( const M_I3DL2_PARAMETERS *const i3dl2, M_PARAMETERS *const native) { native->rear_delay = M_REVERB_DEFAULT_REAR_DELAY; native->position_left = M_REVERB_DEFAULT_POSITION; native->position_right = M_REVERB_DEFAULT_POSITION; native->position_matrix_left = M_REVERB_DEFAULT_POSITION_MATRIX; native->position_matrix_right = M_REVERB_DEFAULT_POSITION_MATRIX; native->room_size = M_REVERB_DEFAULT_ROOM_SIZE; native->low_eq_cutoff = 4; native->high_eq_cutoff = 6; native->room_filter_main = (float)i3dl2->room / 100.0f; native->room_filter_hf = (float)i3dl2->room_hf / 100.0f; if (i3dl2->decay_hf_ratio >= 1.0f) { int32_t index = (int32_t)(-4.0f * log10f(i3dl2->decay_hf_ratio)); if (index < -8) { index = -8; } native->low_eq_gain = (uint8_t)(index < 0 ? index + 8 : 8); native->high_eq_gain = 8; native->decay_time = i3dl2->decay_time * i3dl2->decay_hf_ratio; } else { int32_t index = (int32_t)(4.0f * log10f(i3dl2->decay_hf_ratio)); if (index < -8) { index = -8; } native->low_eq_gain = 8; native->high_eq_gain = (uint8_t)(index < 0 ? index + 8 : 8); native->decay_time = i3dl2->decay_time; } float reflections_delay = i3dl2->reflections_delay * 1000.0f; if (reflections_delay >= M_REVERB_MAX_REFLECTIONS_DELAY) { reflections_delay = (float)(M_REVERB_MAX_REFLECTIONS_DELAY - 1); } else if (reflections_delay <= 1.0f) { reflections_delay = 1.0f; } native->reflections_delay = (uint32_t)reflections_delay; float reverb_delay = i3dl2->reverb_delay * 1000.0f; if (reverb_delay >= M_REVERB_MAX_REVERB_DELAY) { reverb_delay = (float)(M_REVERB_MAX_REVERB_DELAY - 1); } native->reverb_delay = (uint8_t)reverb_delay; native->reflections_gain = i3dl2->reflections / 100.0f; native->reverb_gain = i3dl2->reverb / 100.0f; native->early_diffusion = (uint8_t)(15.0f * i3dl2->diffusion / 100.0f); native->late_diffusion = native->early_diffusion; native->density = i3dl2->density; native->room_filter_freq = i3dl2->hf_reference; native->wet_dry_mix = i3dl2->wet_dry_mix; } void Audio_Reverb_Init(const int32_t sample_rate, const int32_t channels) { if (m_IsInitialised) { return; } M_DspReverb_Create(&m_Reverb, sample_rate, channels, channels); for (int32_t i = 0; i < M_PRESET_COUNT; i++) { M_ConvertI3DL2ToNative(&m_ReverbPresets[i], &m_ReverbTypes[i]); } m_IsInitialised = true; } void Audio_Reverb_Shutdown(void) { if (!m_IsInitialised) { return; } M_DspReverb_Destroy(&m_Reverb); m_IsInitialised = false; } void Audio_Reverb_SetType(uint8_t reverb_type) { CLAMPG(reverb_type, M_PRESET_COUNT); const bool type_changed = m_ReverbType != reverb_type; m_ReverbType = reverb_type; if (!m_IsInitialised) { return; } if (type_changed) { M_DspReverb_Reset(&m_Reverb); } if (m_ReverbType != 0) { M_DspReverb_SetParameters(&m_Reverb, &m_ReverbTypes[m_ReverbType - 1]); } } uint8_t Audio_Reverb_GetType(void) { return m_ReverbType; } void Audio_Reverb_Process(float *const dst_buffer, const size_t len) { if (!m_IsInitialised) { return; } if (m_ReverbType == 0) { return; } const size_t samples = len / sizeof(float) / (size_t)AUDIO_WORKING_CHANNELS; if (samples == 0) { return; } if (AUDIO_WORKING_CHANNELS == 1) { M_DspReverb_Process_1_to_1(&m_Reverb, dst_buffer, samples); } else if (AUDIO_WORKING_CHANNELS == 2) { M_DspReverb_Process_2_to_2(&m_Reverb, dst_buffer, samples); } } ================================================ FILE: src/trx/av/audio_sample.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef struct { struct { int32_t format; AVChannelLayout ch_layout; int32_t sample_rate; } src, dst; SwrContext *ctx; size_t working_buffer_size; uint8_t *working_buffer; } M_SWR_CONTEXT; typedef struct { char *original_data; size_t original_size; float *sample_data; int32_t channels; int32_t num_samples; } AUDIO_SAMPLE; typedef struct { bool is_used; bool is_looped; bool is_playing; float volume_l; // sample gain multiplier float volume_r; // sample gain multiplier float pitch; // `volume`/`pan` come from the game layer (src/trx/game/sound/common.c). // Despite the historic "decibel" naming, these values are not base-10 // centi-dB. They are a log2-based gain domain that the OG engine used to // feed directly to DirectSound (-10000..0 style range). // // In TRX we keep the game-side math pristine and interpret these as: // TR1/2: log2(gain) * 1000 // TR3: DirectSound-style centi-dB in [-10000..0] // // `M_DecibelToMultiplier()` is the corresponding inverse transform: // TR1/2: gain = 2^(value/1000) // TR3: gain = 10^(value/2000) // // This makes combining contributions (volume + pan) an additive operation // in the game's log domain, while still producing a linear multiplier for // the mixer. int32_t volume; int32_t pan; // pitch shift means the same samples can be reused twice, hence float float current_sample; AUDIO_SAMPLE *sample; } AUDIO_SAMPLE_SOUND; typedef struct { const uint8_t *data; const uint8_t *ptr; int32_t size; int32_t remaining; } AUDIO_AV_BUFFER; static int32_t m_LoadedSamplesCount = 0; static AUDIO_SAMPLE m_LoadedSamples[AUDIO_MAX_SAMPLES] = {}; static AUDIO_SAMPLE_SOUND m_Samples[AUDIO_MAX_ACTIVE_SAMPLES] = {}; static double M_DecibelToMultiplier(double db_gain) { if (g_TRVersion < 3) { // Legacy scale return pow(2.0, db_gain / 600.0); } else { // DirectSound-style centi-dB domain: gain = 10^(centi_dB/2000). return pow(10.0, db_gain / 2000.0); } } static bool M_RecalculateChannelVolumes(int32_t sound_id) { if (!g_AudioDeviceID || sound_id < 0 || sound_id >= AUDIO_MAX_ACTIVE_SAMPLES) { return false; } AUDIO_SAMPLE_SOUND *sound = &m_Samples[sound_id]; sound->volume_l = M_DecibelToMultiplier( sound->volume - (sound->pan > 0 ? sound->pan : 0)); sound->volume_r = M_DecibelToMultiplier( sound->volume + (sound->pan < 0 ? sound->pan : 0)); return true; } static int32_t M_ReadAVBuffer(void *opaque, uint8_t *dst, int32_t dst_size) { ASSERT(opaque != nullptr); ASSERT(dst != nullptr); AUDIO_AV_BUFFER *src = opaque; int32_t read = dst_size >= src->remaining ? src->remaining : dst_size; if (!read) { return AVERROR_EOF; } memcpy(dst, src->ptr, read); src->ptr += read; src->remaining -= read; return read; } static int64_t M_SeekAVBuffer(void *opaque, int64_t offset, int32_t whence) { ASSERT(opaque != nullptr); AUDIO_AV_BUFFER *src = opaque; if (whence & AVSEEK_SIZE) { return src->size; } switch (whence) { case SEEK_SET: if (src->size - offset < 0) { return AVERROR_EOF; } src->ptr = src->data + offset; src->remaining = src->size - offset; break; case SEEK_CUR: if (src->remaining - offset < 0) { return AVERROR_EOF; } src->ptr += offset; src->remaining -= offset; break; case SEEK_END: if (src->size + offset < 0) { return AVERROR_EOF; } src->ptr = src->data - offset; src->remaining = src->size + offset; break; } return src->ptr - src->data; } static int32_t M_OutputAudioFrame( M_SWR_CONTEXT *const swr, AVFrame *const frame) { // Determine the maximum number of output samples this call can produce, // based on the current delay already inside the resampler plus the new // input. Using av_rescale_rnd() keeps everything in integer domain and // avoids cumulative rounding errors. const int64_t delay = swr_get_delay(swr->ctx, swr->src.sample_rate); const int32_t out_samples = (int32_t)av_rescale_rnd( delay + frame->nb_samples, swr->dst.sample_rate, swr->src.sample_rate, AV_ROUND_UP); if (out_samples <= 0) { return 0; // nothing to do } uint8_t *out_buffer = nullptr; if (av_samples_alloc( &out_buffer, nullptr, swr->dst.ch_layout.nb_channels, out_samples, swr->dst.format, 1) < 0) { return AVERROR(ENOMEM); } // Convert – we do *not* drain the resampler here. const int32_t converted = swr_convert( swr->ctx, &out_buffer, out_samples, (const uint8_t **)frame->data, frame->nb_samples); if (converted < 0) { av_freep(&out_buffer); return converted; // propagate error } if (converted > 0) { const int32_t out_buffer_size = av_samples_get_buffer_size( nullptr, swr->dst.ch_layout.nb_channels, converted, swr->dst.format, 1); if (out_buffer_size > 0) { swr->working_buffer = Memory_Realloc( swr->working_buffer, swr->working_buffer_size + out_buffer_size); memcpy( swr->working_buffer + swr->working_buffer_size, out_buffer, out_buffer_size); swr->working_buffer_size += out_buffer_size; } } av_freep(&out_buffer); return 0; } static int32_t M_DecodePacket( AVCodecContext *const dec, const AVPacket *const pkt, AVFrame *frame, M_SWR_CONTEXT *const swr) { // Submit the packet to the decoder int32_t ret = avcodec_send_packet(dec, pkt); if (ret < 0) { LOG_ERROR( "Error submitting a packet for decoding (%s)\n", av_err2str(ret)); return ret; } // Get all the available frames from the decoder while (ret >= 0) { ret = avcodec_receive_frame(dec, frame); if (ret < 0) { // those two return values are special and mean there is no output // frame available, but there were no errors during decoding if (ret == AVERROR_EOF || ret == AVERROR(EAGAIN)) { return 0; } LOG_ERROR( "Error receiving a frame for decoding (%s)\n", av_err2str(ret)); return ret; } ret = M_OutputAudioFrame(swr, frame); av_frame_unref(frame); } return ret; } static bool M_ConvertRawData( const uint8_t *const original_data, const int32_t original_size, const int32_t dst_sample_rate, const int32_t dst_format, const int32_t dst_channel_count, uint8_t **const out_sample_data, size_t *const out_size, size_t *const out_sample_count) { bool result = false; struct { size_t read_buffer_size; AVIOContext *avio_context; AVStream *stream; AVFormatContext *format_ctx; const AVCodec *codec; AVCodecContext *codec_ctx; AVPacket *packet; AVFrame *frame; } av = { .read_buffer_size = 8192, .avio_context = nullptr, .stream = nullptr, .format_ctx = nullptr, .codec = nullptr, .codec_ctx = nullptr, .packet = nullptr, .frame = nullptr, }; M_SWR_CONTEXT swr = {}; int32_t error_code; uint8_t *const read_buffer = av_malloc(av.read_buffer_size); if (read_buffer == nullptr) { error_code = AVERROR(ENOMEM); goto cleanup; } AUDIO_AV_BUFFER av_buf = { .data = original_data, .ptr = original_data, .size = original_size, .remaining = original_size, }; av.avio_context = avio_alloc_context( read_buffer, av.read_buffer_size, 0, &av_buf, M_ReadAVBuffer, nullptr, M_SeekAVBuffer); av.format_ctx = avformat_alloc_context(); av.format_ctx->pb = av.avio_context; error_code = avformat_open_input(&av.format_ctx, "mem:", nullptr, nullptr); if (error_code != 0) { goto cleanup; } error_code = avformat_find_stream_info(av.format_ctx, nullptr); if (error_code < 0) { goto cleanup; } av.stream = nullptr; for (uint32_t i = 0; i < av.format_ctx->nb_streams; i++) { AVStream *current_stream = av.format_ctx->streams[i]; if (current_stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { av.stream = current_stream; break; } } if (av.stream == nullptr) { error_code = AVERROR_STREAM_NOT_FOUND; goto cleanup; } av.codec = avcodec_find_decoder(av.stream->codecpar->codec_id); if (av.codec == nullptr) { error_code = AVERROR_DEMUXER_NOT_FOUND; goto cleanup; } av.codec_ctx = avcodec_alloc_context3(av.codec); if (av.codec_ctx == nullptr) { error_code = AVERROR(ENOMEM); goto cleanup; } error_code = avcodec_parameters_to_context(av.codec_ctx, av.stream->codecpar); if (error_code) { goto cleanup; } error_code = avcodec_open2(av.codec_ctx, av.codec, nullptr); if (error_code < 0) { goto cleanup; } av.packet = av_packet_alloc(); if (av.packet == nullptr) { error_code = AVERROR(ENOMEM); goto cleanup; } av.frame = av_frame_alloc(); if (av.frame == nullptr) { error_code = AVERROR(ENOMEM); goto cleanup; } swr.src.sample_rate = av.codec_ctx->sample_rate; swr.src.ch_layout = av.codec_ctx->ch_layout; swr.src.format = av.codec_ctx->sample_fmt; swr.dst.sample_rate = AUDIO_WORKING_RATE; av_channel_layout_default(&swr.dst.ch_layout, dst_channel_count); swr.dst.format = Audio_GetAVAudioFormat(AUDIO_WORKING_FORMAT); swr_alloc_set_opts2( &swr.ctx, &swr.dst.ch_layout, swr.dst.format, swr.dst.sample_rate, &swr.src.ch_layout, swr.src.format, swr.src.sample_rate, 0, 0); if (swr.ctx == nullptr) { av_packet_unref(av.packet); error_code = AVERROR(ENOMEM); goto cleanup; } error_code = swr_init(swr.ctx); if (error_code != 0) { av_packet_unref(av.packet); goto cleanup; } while ((error_code = av_read_frame(av.format_ctx, av.packet)) >= 0) { M_DecodePacket(av.codec_ctx, av.packet, av.frame, &swr); av_packet_unref(av.packet); if (error_code < 0) { break; } } if (av.codec_ctx != nullptr) { M_DecodePacket(av.codec_ctx, nullptr, av.frame, &swr); } if (error_code == AVERROR_EOF) { error_code = 0; } else if (error_code < 0) { goto cleanup; } if (out_size != nullptr) { *out_size = swr.working_buffer_size; } if (out_sample_count != nullptr) { *out_sample_count = (int32_t)swr.working_buffer_size / av_get_bytes_per_sample(swr.dst.format) / swr.dst.ch_layout.nb_channels; } if (out_sample_data != nullptr) { *out_sample_data = swr.working_buffer; } else { Memory_FreePointer(&swr.working_buffer); } result = true; cleanup: if (error_code != 0) { LOG_ERROR("Error while decoding sample: %s", av_err2str(error_code)); } if (!result) { if (out_size != nullptr) { *out_size = 0; } if (out_sample_count != nullptr) { *out_sample_count = 0; } if (out_sample_data != nullptr) { *out_sample_data = nullptr; } Memory_FreePointer(&swr.working_buffer); } if (swr.ctx) { swr_free(&swr.ctx); } if (av.frame) { av_frame_free(&av.frame); } if (av.packet) { av_packet_free(&av.packet); } av.codec = nullptr; if (av.codec_ctx) { avcodec_free_context(&av.codec_ctx); } if (av.format_ctx) { avformat_close_input(&av.format_ctx); } if (av.avio_context) { av_freep(&av.avio_context->buffer); avio_context_free(&av.avio_context); } return result; } static bool M_ConvertSample(const int32_t sample_id) { ASSERT(sample_id >= 0 && sample_id < m_LoadedSamplesCount); AUDIO_SAMPLE *const sample = &m_LoadedSamples[sample_id]; if (sample->sample_data != nullptr) { return true; } size_t num_samples; BENCHMARK benchmark = Benchmark_Start(); const bool result = M_ConvertRawData( (uint8_t *)sample->original_data, sample->original_size, AUDIO_WORKING_RATE, Audio_GetAVAudioFormat(AUDIO_WORKING_FORMAT), 1, (uint8_t **)&sample->sample_data, nullptr, &num_samples); char buffer[80]; sprintf(buffer, "sample %d decoded", sample_id); Benchmark_End(&benchmark, buffer); sample->channels = 1; sample->num_samples = num_samples; return result; } static bool M_IsOriginalDataDefined(const int32_t sample_id) { ASSERT(sample_id >= 0 && sample_id < m_LoadedSamplesCount); const AUDIO_SAMPLE *const sample = &m_LoadedSamples[sample_id]; return sample->original_data != nullptr; } void Audio_Sample_Init(void) { for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_SAMPLES; sound_id++) { AUDIO_SAMPLE_SOUND *sound = &m_Samples[sound_id]; sound->is_used = false; sound->is_playing = false; sound->volume = 0.0f; sound->pitch = 1.0f; sound->pan = 0.0f; sound->current_sample = 0.0f; sound->sample = nullptr; } } void Audio_Sample_Shutdown(void) { Audio_Sample_CloseAll(); Audio_Sample_UnloadAll(); } bool Audio_Sample_Unload(const int32_t sample_id) { if (sample_id < 0 || sample_id >= AUDIO_MAX_SAMPLES) { LOG_ERROR("Maximum allowed samples: %d", AUDIO_MAX_SAMPLES); return false; } bool result = false; AUDIO_SAMPLE *const sample = &m_LoadedSamples[sample_id]; if (sample->sample_data == nullptr) { LOG_ERROR("Sample %d is already unloaded", sample_id); return false; } Memory_FreePointer(&sample->sample_data); Memory_FreePointer(&sample->original_data); m_LoadedSamplesCount--; return true; } bool Audio_Sample_UnloadAll(void) { m_LoadedSamplesCount = 0; for (int32_t i = 0; i < AUDIO_MAX_SAMPLES; i++) { AUDIO_SAMPLE *const sample = &m_LoadedSamples[i]; Memory_FreePointer(&sample->sample_data); Memory_FreePointer(&sample->original_data); } return true; } bool Audio_Sample_Load( const int32_t sample_id, const char *const data, const size_t size) { if (data == nullptr || size == 0) { LOG_ERROR("Missing sample data %d", sample_id); return false; } if (!g_AudioDeviceID) { LOG_ERROR("Unitialized audio device"); return false; } if (sample_id < 0 || sample_id >= AUDIO_MAX_SAMPLES) { LOG_ERROR("Maximum allowed samples: %d", AUDIO_MAX_SAMPLES); return false; } AUDIO_SAMPLE *const sample = &m_LoadedSamples[sample_id]; if (sample->original_data != nullptr) { LOG_ERROR( "Sample %d is already loaded (trying to overwrite with %d bytes)", sample_id, size); return false; } sample->original_data = Memory_Alloc(size); sample->original_size = size; memcpy(sample->original_data, data, size); m_LoadedSamplesCount++; return true; } int32_t Audio_Sample_Play( int32_t sample_id, int32_t volume, float pitch, int32_t pan, bool is_looped) { if (!g_AudioDeviceID) { LOG_ERROR("audio device is unavailable"); return false; } if (sample_id < 0 || sample_id >= m_LoadedSamplesCount) { LOG_DEBUG("Invalid sample id: %d", sample_id); return AUDIO_NO_SOUND; } if (!M_IsOriginalDataDefined(sample_id)) { return AUDIO_NO_SOUND; } int32_t result = AUDIO_NO_SOUND; Audio_LockDevice(); for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_SAMPLES; sound_id++) { AUDIO_SAMPLE_SOUND *sound = &m_Samples[sound_id]; if (sound->is_used) { continue; } M_ConvertSample(sample_id); sound->is_used = true; sound->is_playing = true; sound->volume = volume; sound->pitch = pitch; sound->pan = pan; sound->is_looped = is_looped; sound->current_sample = 0.0f; sound->sample = &m_LoadedSamples[sample_id]; M_RecalculateChannelVolumes(sound_id); result = sound_id; break; } Audio_UnlockDevice(); if (result == AUDIO_NO_SOUND) { LOG_ERROR("All sample buffers are used!"); } return result; } bool Audio_Sample_IsPlaying(int32_t sound_id) { if (!g_AudioDeviceID || sound_id < 0 || sound_id >= AUDIO_MAX_ACTIVE_SAMPLES) { return false; } return m_Samples[sound_id].is_playing; } bool Audio_Sample_Pause(int32_t sound_id) { if (!g_AudioDeviceID) { return false; } if (m_Samples[sound_id].is_playing) { Audio_LockDevice(); m_Samples[sound_id].is_playing = false; Audio_UnlockDevice(); } return true; } bool Audio_Sample_PauseAll(void) { if (!g_AudioDeviceID) { return false; } for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_SAMPLES; sound_id++) { if (m_Samples[sound_id].is_used) { Audio_Sample_Pause(sound_id); } } return true; } bool Audio_Sample_Unpause(int32_t sound_id) { if (!g_AudioDeviceID) { return false; } if (!m_Samples[sound_id].is_playing) { Audio_LockDevice(); m_Samples[sound_id].is_playing = true; Audio_UnlockDevice(); } return true; } bool Audio_Sample_UnpauseAll(void) { if (!g_AudioDeviceID) { return false; } for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_SAMPLES; sound_id++) { if (m_Samples[sound_id].is_used) { Audio_Sample_Unpause(sound_id); } } return true; } bool Audio_Sample_Close(int32_t sound_id) { if (!g_AudioDeviceID || sound_id < 0 || sound_id >= AUDIO_MAX_ACTIVE_SAMPLES) { return false; } Audio_LockDevice(); m_Samples[sound_id].is_used = false; m_Samples[sound_id].is_playing = false; Audio_UnlockDevice(); return true; } bool Audio_Sample_CloseAll(void) { if (!g_AudioDeviceID) { return false; } for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_SAMPLES; sound_id++) { if (m_Samples[sound_id].is_used) { Audio_Sample_Close(sound_id); } } return true; } bool Audio_Sample_SetPan(int32_t sound_id, int32_t pan) { if (!g_AudioDeviceID || sound_id < 0 || sound_id >= AUDIO_MAX_ACTIVE_SAMPLES) { return false; } Audio_LockDevice(); m_Samples[sound_id].pan = pan; M_RecalculateChannelVolumes(sound_id); Audio_UnlockDevice(); return true; } bool Audio_Sample_SetVolume(int32_t sound_id, int32_t volume) { if (!g_AudioDeviceID || sound_id < 0 || sound_id >= AUDIO_MAX_ACTIVE_SAMPLES) { return false; } Audio_LockDevice(); m_Samples[sound_id].volume = volume; M_RecalculateChannelVolumes(sound_id); Audio_UnlockDevice(); return true; } bool Audio_Sample_SetPitch(int32_t sound_id, float pitch) { if (!g_AudioDeviceID || sound_id < 0 || sound_id >= AUDIO_MAX_ACTIVE_SAMPLES) { return false; } Audio_LockDevice(); m_Samples[sound_id].pitch = pitch; M_RecalculateChannelVolumes(sound_id); Audio_UnlockDevice(); return true; } void Audio_Sample_Mix(float *dst_buffer, size_t len) { for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_SAMPLES; sound_id++) { AUDIO_SAMPLE_SOUND *sound = &m_Samples[sound_id]; if (!sound->is_playing) { continue; } int32_t samples_requested = len / sizeof(AUDIO_WORKING_FORMAT) / AUDIO_WORKING_CHANNELS; float src_sample_idx = sound->current_sample; const float *src_buffer = sound->sample->sample_data; float *dst_ptr = dst_buffer; while ((dst_ptr - dst_buffer) / AUDIO_WORKING_CHANNELS < samples_requested) { // because we handle 3d sound ourselves, downmix to mono float src_sample = 0.0f; for (int32_t i = 0; i < sound->sample->channels; i++) { src_sample += src_buffer [(int32_t)src_sample_idx * sound->sample->channels + i]; } src_sample /= (float)sound->sample->channels; *dst_ptr++ += src_sample * sound->volume_l; *dst_ptr++ += src_sample * sound->volume_r; src_sample_idx += sound->pitch; if ((int32_t)src_sample_idx >= sound->sample->num_samples) { if (sound->is_looped) { src_sample_idx = 0.0f; } else { break; } } } sound->current_sample = src_sample_idx; if (sound->current_sample >= sound->sample->num_samples && !sound->is_looped) { Audio_Sample_Close(sound_id); } } } ================================================ FILE: src/trx/av/audio_stream.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define READ_BUFFER_SIZE \ (AUDIO_SAMPLES * AUDIO_WORKING_CHANNELS * sizeof(AUDIO_WORKING_FORMAT)) typedef enum { M_STREAM_SRC_NONE, M_STREAM_SRC_MEMORY, } M_STREAM_SOURCE_TYPE; typedef struct { uint8_t *data; size_t size; size_t pos; } M_MEM_SOURCE; typedef struct { bool is_used; bool is_playing; bool is_read_done; bool is_looped; float volume; double duration; double decode_timestamp; int64_t played_samples; double start_at; double stop_at; void (*finish_callback)(int32_t sound_id, void *user_data); void *finish_callback_user_data; M_STREAM_SOURCE_TYPE src_type; void *src; uint8_t *avio_ctx_buffer; AVIOContext *avio_ctx; struct { AVStream *stream; AVFormatContext *format_ctx; const AVCodec *codec; AVCodecContext *codec_ctx; AVPacket *packet; AVFrame *frame; } av; struct { struct { int32_t format; AVChannelLayout ch_layout; int32_t sample_rate; } src, dst; SwrContext *ctx; } swr; struct { SDL_AudioStream *stream; } sdl; } AUDIO_STREAM_SOUND; extern SDL_AudioDeviceID g_AudioDeviceID; static AUDIO_STREAM_SOUND m_Streams[AUDIO_MAX_ACTIVE_STREAMS] = {}; static float m_MixBuffer[AUDIO_SAMPLES * AUDIO_WORKING_CHANNELS] = {}; static size_t m_DecodeBufferCapacity = 0; static float *m_DecodeBuffer = nullptr; static int32_t M_MemoryRead( void *const opaque, uint8_t *const buf, const int32_t buf_size) { ASSERT(opaque != nullptr); ASSERT(buf != nullptr); if (buf_size <= 0) { return 0; } M_MEM_SOURCE *const s = opaque; if (s->pos >= s->size) { return AVERROR_EOF; } size_t to_copy = s->size - s->pos; if (to_copy > (size_t)buf_size) { to_copy = (size_t)buf_size; } memcpy(buf, s->data + s->pos, to_copy); s->pos += to_copy; return (int32_t)to_copy; } static int64_t M_MemorySeek( void *const opaque, const int64_t offset, const int32_t whence) { ASSERT(opaque != nullptr); M_MEM_SOURCE *const s = opaque; if ((whence & AVSEEK_SIZE) != 0) { return (int64_t)s->size; } const int32_t base_whence = whence & ~AVSEEK_FORCE; int64_t base; if (base_whence == SEEK_SET) { base = 0; } else if (base_whence == SEEK_CUR) { base = (int64_t)s->pos; } else if (base_whence == SEEK_END) { base = (int64_t)s->size; } else { return AVERROR(EINVAL); } int64_t new_pos = base + offset; if (new_pos < 0) { new_pos = 0; } if (new_pos > (int64_t)s->size) { new_pos = (int64_t)s->size; } s->pos = (size_t)new_pos; return new_pos; } static void M_ResetPlaybackState( AUDIO_STREAM_SOUND *const stream, const double relative_timestamp) { ASSERT(stream != nullptr); const double clamped = MAX(0.0, relative_timestamp); stream->played_samples = (int64_t)(clamped * (double)AUDIO_WORKING_RATE); } static void M_DiscardSDLStreamData(AUDIO_STREAM_SOUND *const stream) { ASSERT(stream != nullptr); if (stream->sdl.stream != nullptr) { while (SDL_AudioStreamAvailable(stream->sdl.stream) > 0) { const int32_t bytes_gotten = SDL_AudioStreamGet( stream->sdl.stream, m_MixBuffer, READ_BUFFER_SIZE); if (bytes_gotten <= 0) { break; } } } } static void M_SeekToStart(AUDIO_STREAM_SOUND *stream) { ASSERT(stream != nullptr); stream->decode_timestamp = stream->start_at; M_ResetPlaybackState(stream, 0.0); int32_t error_code; if (stream->start_at <= 0.0) { // reset to start of file avio_seek(stream->av.format_ctx->pb, 0, SEEK_SET); error_code = avformat_seek_file( stream->av.format_ctx, -1, 0, 0, 0, AVSEEK_FLAG_FRAME); } else { // seek to specific timestamp AVFormatContext *const fmt = stream->av.format_ctx; if (fmt->pb != nullptr && (fmt->pb->seekable & AVIO_SEEKABLE_NORMAL)) { const int64_t ts = (int64_t)(stream->start_at * AV_TIME_BASE); error_code = avformat_seek_file( fmt, stream->av.stream->index, INT64_MIN, ts, INT64_MAX, AVSEEK_FLAG_BACKWARD); } else { // fallback to stream-based seek const double time_base_sec = av_q2d(stream->av.stream->time_base); error_code = av_seek_frame( fmt, stream->av.stream->index, (int64_t)(stream->start_at / time_base_sec), AVSEEK_FLAG_ANY); } } if (error_code < 0) { LOG_ERROR( "seek failed for timestamp %f: %s", stream->decode_timestamp, av_err2str(error_code)); } else { avcodec_flush_buffers(stream->av.codec_ctx); M_DiscardSDLStreamData(stream); stream->is_read_done = false; } } static bool M_DecodeFrame(AUDIO_STREAM_SOUND *stream) { ASSERT(stream != nullptr); if (stream->stop_at > 0.0 && stream->decode_timestamp >= stream->stop_at) { if (stream->is_looped) { M_SeekToStart(stream); return M_DecodeFrame(stream); } else { return false; } } // av_read_frame() overwrites the packet; always unref any previous content. av_packet_unref(stream->av.packet); int32_t error_code = av_read_frame(stream->av.format_ctx, stream->av.packet); if (error_code == AVERROR_EOF && stream->is_looped) { M_SeekToStart(stream); return M_DecodeFrame(stream); } if (error_code == AVERROR_EOF) { return false; } if (error_code < 0) { LOG_ERROR( "error while decoding audio stream: %d (%s)", error_code, av_err2str(error_code)); return false; } if (stream->av.packet->stream_index != stream->av.stream->index) { return true; } error_code = avcodec_send_packet(stream->av.codec_ctx, stream->av.packet); if (error_code < 0) { av_packet_unref(stream->av.packet); LOG_ERROR( "Got an error when decoding frame: %s", av_err2str(error_code)); return false; } return true; } static bool M_InitialiseFromFormatContext( int32_t sound_id, AVFormatContext *const fmt_ctx) { if (!g_AudioDeviceID || sound_id < 0 || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) { return false; } bool ret = false; Audio_LockDevice(); AUDIO_STREAM_SOUND *stream = &m_Streams[sound_id]; int32_t error_code = 0; stream->av.format_ctx = fmt_ctx; error_code = avformat_find_stream_info(stream->av.format_ctx, nullptr); if (error_code < 0) { goto cleanup; } stream->av.stream = nullptr; for (uint32_t i = 0; i < stream->av.format_ctx->nb_streams; i++) { AVStream *current_stream = stream->av.format_ctx->streams[i]; if (current_stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { stream->av.stream = current_stream; break; } } if (!stream->av.stream) { error_code = AVERROR_STREAM_NOT_FOUND; goto cleanup; } stream->av.codec = avcodec_find_decoder(stream->av.stream->codecpar->codec_id); if (!stream->av.codec) { error_code = AVERROR_DEMUXER_NOT_FOUND; goto cleanup; } stream->av.codec_ctx = avcodec_alloc_context3(stream->av.codec); if (!stream->av.codec_ctx) { error_code = AVERROR(ENOMEM); goto cleanup; } error_code = avcodec_parameters_to_context( stream->av.codec_ctx, stream->av.stream->codecpar); if (error_code != 0) { goto cleanup; } error_code = avcodec_open2(stream->av.codec_ctx, stream->av.codec, nullptr); if (error_code < 0) { goto cleanup; } stream->av.packet = av_packet_alloc(); if (!stream->av.packet) { error_code = AVERROR(ENOMEM); goto cleanup; } stream->av.frame = av_frame_alloc(); if (!stream->av.frame) { error_code = AVERROR(ENOMEM); goto cleanup; } M_DecodeFrame(stream); const int32_t sdl_channels = stream->av.codec_ctx->ch_layout.nb_channels; stream->is_read_done = false; stream->is_used = true; stream->is_playing = true; stream->is_looped = false; stream->volume = 1.0f; stream->decode_timestamp = 0.0; stream->played_samples = 0; stream->finish_callback = nullptr; stream->finish_callback_user_data = nullptr; stream->duration = (double)stream->av.format_ctx->duration / (double)AV_TIME_BASE; stream->start_at = -1.0; // negative value means unset stream->stop_at = -1.0; // negative value means unset stream->sdl.stream = SDL_NewAudioStream( AUDIO_WORKING_FORMAT, sdl_channels, AUDIO_WORKING_RATE, AUDIO_WORKING_FORMAT, sdl_channels, AUDIO_WORKING_RATE); if (!stream->sdl.stream) { LOG_ERROR("Failed to create SDL stream: %s", SDL_GetError()); goto cleanup; } ret = true; cleanup: if (error_code != 0) { LOG_ERROR( "Error while opening audio stream: %s", av_err2str(error_code)); } if (!ret) { Audio_Stream_Close(sound_id); } Audio_UnlockDevice(); return ret; } static bool M_EnqueueFrame(AUDIO_STREAM_SOUND *stream) { ASSERT(stream != nullptr); int32_t error_code; if (!stream->swr.ctx) { stream->swr.src.sample_rate = stream->av.codec_ctx->sample_rate; stream->swr.src.ch_layout = stream->av.codec_ctx->ch_layout; stream->swr.src.format = stream->av.codec_ctx->sample_fmt; stream->swr.dst.sample_rate = AUDIO_WORKING_RATE; av_channel_layout_default( &stream->swr.dst.ch_layout, AUDIO_WORKING_CHANNELS); stream->swr.dst.format = Audio_GetAVAudioFormat(AUDIO_WORKING_FORMAT); swr_alloc_set_opts2( &stream->swr.ctx, &stream->swr.dst.ch_layout, stream->swr.dst.format, stream->swr.dst.sample_rate, &stream->swr.src.ch_layout, stream->swr.src.format, stream->swr.src.sample_rate, 0, 0); if (!stream->swr.ctx) { av_packet_unref(stream->av.packet); error_code = AVERROR(ENOMEM); goto cleanup; } error_code = swr_init(stream->swr.ctx); if (error_code != 0) { av_packet_unref(stream->av.packet); goto cleanup; } } while (1) { error_code = avcodec_receive_frame(stream->av.codec_ctx, stream->av.frame); if (error_code == AVERROR(EAGAIN)) { av_frame_unref(stream->av.frame); error_code = 0; break; } if (error_code < 0) { av_frame_unref(stream->av.frame); break; } uint8_t *out_buffer = nullptr; const int32_t out_samples = swr_get_out_samples(stream->swr.ctx, stream->av.frame->nb_samples); av_samples_alloc( &out_buffer, nullptr, stream->swr.dst.ch_layout.nb_channels, out_samples, stream->swr.dst.format, 1); int32_t resampled_size = swr_convert( stream->swr.ctx, &out_buffer, out_samples, (const uint8_t **)stream->av.frame->data, stream->av.frame->nb_samples); size_t out_pos = 0; while (resampled_size > 0) { const size_t out_buffer_size = av_samples_get_buffer_size( nullptr, stream->swr.dst.ch_layout.nb_channels, resampled_size, stream->swr.dst.format, 1); if (out_pos + out_buffer_size > m_DecodeBufferCapacity) { m_DecodeBufferCapacity = out_pos + out_buffer_size; m_DecodeBuffer = Memory_Realloc(m_DecodeBuffer, m_DecodeBufferCapacity); } if (m_DecodeBuffer != nullptr && out_buffer != nullptr) { memcpy( (uint8_t *)m_DecodeBuffer + out_pos, out_buffer, out_buffer_size); } out_pos += out_buffer_size; resampled_size = swr_convert( stream->swr.ctx, &out_buffer, out_samples, nullptr, 0); } if (SDL_AudioStreamPut(stream->sdl.stream, m_DecodeBuffer, out_pos)) { LOG_ERROR("Got an error when decoding frame: %s", SDL_GetError()); av_frame_unref(stream->av.frame); break; } ASSERT(stream->av.format_ctx != nullptr); ASSERT(stream->av.codec_ctx != nullptr); ASSERT(stream->av.stream != nullptr); double time_base_sec = av_q2d(stream->av.stream->time_base); stream->decode_timestamp = stream->av.frame->best_effort_timestamp * time_base_sec; av_freep(&out_buffer); av_frame_unref(stream->av.frame); } av_packet_unref(stream->av.packet); cleanup: if (error_code > 0) { LOG_ERROR( "Got an error when decoding frame: %d, %s", error_code, av_err2str(error_code)); } return true; } static bool M_InitialiseFromPath(int32_t sound_id, const char *file_path) { ASSERT(file_path != nullptr); if (!g_AudioDeviceID || sound_id < 0 || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) { return false; } int32_t error_code = 0; AVFormatContext *fmt_ctx = nullptr; error_code = avformat_open_input(&fmt_ctx, file_path, nullptr, nullptr); if (error_code != 0) { LOG_ERROR( "Error while opening audio %s: %s", file_path, av_err2str(error_code)); return false; } return M_InitialiseFromFormatContext(sound_id, fmt_ctx); } static void M_Clear(AUDIO_STREAM_SOUND *stream) { ASSERT(stream != nullptr); stream->is_used = false; stream->is_playing = false; stream->is_read_done = true; stream->is_looped = false; stream->volume = 0.0f; stream->duration = 0.0; stream->decode_timestamp = 0.0; stream->played_samples = 0; stream->sdl.stream = nullptr; stream->finish_callback = nullptr; stream->finish_callback_user_data = nullptr; stream->src_type = M_STREAM_SRC_NONE; stream->src = nullptr; stream->avio_ctx_buffer = nullptr; stream->avio_ctx = nullptr; } void Audio_Stream_Init(void) { for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_STREAMS; sound_id++) { M_Clear(&m_Streams[sound_id]); } } void Audio_Stream_Shutdown(void) { Memory_FreePointer(&m_DecodeBuffer); m_DecodeBufferCapacity = 0; if (!g_AudioDeviceID) { return; } for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_STREAMS; sound_id++) { if (m_Streams[sound_id].is_used) { Audio_Stream_Close(sound_id); } } } bool Audio_Stream_SyncTimestamp(const int32_t sound_id, const double timestamp) { if (!g_AudioDeviceID || sound_id < 0 || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) { return false; } AUDIO_STREAM_SOUND *const stream = &m_Streams[sound_id]; double drift = Audio_Stream_GetTimestamp(sound_id) - timestamp; if (drift < 0) { drift = -drift; } if (drift >= AUDIO_DRIFT_THRESHOLD) { LOG_DEBUG("Detected audio drift: %f s", drift); Audio_Stream_SeekTimestamp(sound_id, timestamp); return true; } return false; } bool Audio_Stream_Pause(int32_t sound_id) { if (!g_AudioDeviceID || sound_id < 0 || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) { return false; } AUDIO_STREAM_SOUND *const stream = &m_Streams[sound_id]; if (stream->is_playing) { Audio_LockDevice(); stream->is_playing = false; Audio_UnlockDevice(); } return true; } bool Audio_Stream_Unpause(int32_t sound_id) { if (!g_AudioDeviceID || sound_id < 0 || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) { return false; } AUDIO_STREAM_SOUND *const stream = &m_Streams[sound_id]; if (!stream->is_playing) { Audio_LockDevice(); stream->is_playing = true; Audio_UnlockDevice(); } return true; } bool Audio_Stream_SetPaused(const int32_t sound_id, const bool is_paused) { return is_paused ? Audio_Stream_Pause(sound_id) : Audio_Stream_Unpause(sound_id); } int32_t Audio_Stream_CreateFromFile(const char *file_path) { if (!g_AudioDeviceID) { return AUDIO_NO_SOUND; } ASSERT(file_path != nullptr); for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_STREAMS; sound_id++) { AUDIO_STREAM_SOUND *stream = &m_Streams[sound_id]; if (stream->is_used) { continue; } if (!M_InitialiseFromPath(sound_id, file_path)) { return AUDIO_NO_SOUND; } return sound_id; } return AUDIO_NO_SOUND; } int32_t Audio_Stream_CreateFromMemory(uint8_t *const data, const size_t size) { if (!g_AudioDeviceID) { return AUDIO_NO_SOUND; } ASSERT(data != nullptr); ASSERT(size != 0); for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_STREAMS; sound_id++) { AUDIO_STREAM_SOUND *stream = &m_Streams[sound_id]; if (stream->is_used) { continue; } M_MEM_SOURCE *const src = Memory_Alloc(sizeof(M_MEM_SOURCE)); *src = (M_MEM_SOURCE) { .data = data, .size = size, .pos = 0, }; stream->src_type = M_STREAM_SRC_MEMORY; stream->src = src; stream->avio_ctx_buffer = av_malloc(4096); if (stream->avio_ctx_buffer == nullptr) { Audio_Stream_Close(sound_id); return AUDIO_NO_SOUND; } stream->avio_ctx = avio_alloc_context( stream->avio_ctx_buffer, 4096, 0, src, M_MemoryRead, nullptr, M_MemorySeek); if (stream->avio_ctx == nullptr) { Audio_Stream_Close(sound_id); return AUDIO_NO_SOUND; } stream->av.format_ctx = avformat_alloc_context(); if (stream->av.format_ctx == nullptr) { Audio_Stream_Close(sound_id); return AUDIO_NO_SOUND; } stream->av.format_ctx->pb = stream->avio_ctx; stream->av.format_ctx->flags |= AVFMT_FLAG_CUSTOM_IO; int32_t error_code = avformat_open_input( &stream->av.format_ctx, nullptr, nullptr, nullptr); if (error_code != 0) { LOG_ERROR( "Error while opening audio memory stream: %s", av_err2str(error_code)); Audio_Stream_Close(sound_id); return AUDIO_NO_SOUND; } if (!M_InitialiseFromFormatContext(sound_id, stream->av.format_ctx)) { Audio_Stream_Close(sound_id); return AUDIO_NO_SOUND; } return sound_id; } return AUDIO_NO_SOUND; } bool Audio_Stream_Close(int32_t sound_id) { if (!g_AudioDeviceID || sound_id < 0 || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) { return false; } Audio_LockDevice(); AUDIO_STREAM_SOUND *stream = &m_Streams[sound_id]; if (stream->av.codec_ctx) { // XXX: potential libav bug - avcodec_close should free this info if (stream->av.codec_ctx->extradata != nullptr) { av_freep(&stream->av.codec_ctx->extradata); } avcodec_free_context(&stream->av.codec_ctx); stream->av.codec_ctx = nullptr; } if (stream->av.format_ctx) { avformat_close_input(&stream->av.format_ctx); stream->av.format_ctx = nullptr; } if (stream->avio_ctx != nullptr) { av_freep(&stream->avio_ctx->buffer); avio_context_free(&stream->avio_ctx); stream->avio_ctx = nullptr; } else if (stream->avio_ctx_buffer != nullptr) { av_freep(&stream->avio_ctx_buffer); } stream->avio_ctx_buffer = nullptr; if (stream->src_type == M_STREAM_SRC_MEMORY && stream->src != nullptr) { M_MEM_SOURCE *const src = stream->src; Memory_FreePointer(&src->data); Memory_FreePointer(&stream->src); } if (stream->swr.ctx) { swr_free(&stream->swr.ctx); } if (stream->av.frame) { av_frame_free(&stream->av.frame); stream->av.frame = nullptr; } if (stream->av.packet) { av_packet_free(&stream->av.packet); stream->av.packet = nullptr; } stream->av.stream = nullptr; stream->av.codec = nullptr; if (stream->sdl.stream) { SDL_FreeAudioStream(stream->sdl.stream); stream->sdl.stream = nullptr; } void (*finish_callback)(int32_t, void *) = stream->finish_callback; void *finish_callback_user_data = stream->finish_callback_user_data; M_Clear(stream); Audio_UnlockDevice(); if (finish_callback) { finish_callback(sound_id, finish_callback_user_data); } return true; } bool Audio_Stream_SetVolume(int32_t sound_id, float volume) { if (!g_AudioDeviceID || sound_id < 0 || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) { return false; } m_Streams[sound_id].volume = volume; return true; } bool Audio_Stream_IsLooped(int32_t sound_id) { if (!g_AudioDeviceID || sound_id < 0 || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) { return false; } return m_Streams[sound_id].is_looped; } bool Audio_Stream_SetIsLooped(int32_t sound_id, bool is_looped) { if (!g_AudioDeviceID || sound_id < 0 || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) { return false; } m_Streams[sound_id].is_looped = is_looped; return true; } bool Audio_Stream_SetFinishCallback( int32_t sound_id, void (*callback)(int32_t sound_id, void *user_data), void *user_data) { if (!g_AudioDeviceID || sound_id < 0 || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) { return false; } m_Streams[sound_id].finish_callback = callback; m_Streams[sound_id].finish_callback_user_data = user_data; return true; } void Audio_Stream_Mix(float *dst_buffer, size_t len) { for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_STREAMS; sound_id++) { AUDIO_STREAM_SOUND *const stream = &m_Streams[sound_id]; if (!stream->is_playing) { continue; } while ((SDL_AudioStreamAvailable(stream->sdl.stream) < (int32_t)len) && !stream->is_read_done) { if (M_DecodeFrame(stream)) { M_EnqueueFrame(stream); } else { stream->is_read_done = true; } } memset(m_MixBuffer, 0, READ_BUFFER_SIZE); int32_t bytes_gotten = SDL_AudioStreamGet( stream->sdl.stream, m_MixBuffer, READ_BUFFER_SIZE); if (bytes_gotten < 0) { LOG_ERROR("Error reading from sdl.stream: %s", SDL_GetError()); stream->is_playing = false; stream->is_used = false; stream->is_read_done = true; } else if (bytes_gotten == 0) { // legit end of stream. looping is handled in // M_DecodeFrame stream->is_playing = false; stream->is_used = false; stream->is_read_done = true; } else { int32_t samples_gotten = bytes_gotten / (AUDIO_WORKING_CHANNELS * sizeof(AUDIO_WORKING_FORMAT)); stream->played_samples += (int64_t)samples_gotten; const float *src_ptr = &m_MixBuffer[0]; float *dst_ptr = dst_buffer; for (int32_t s = 0; s < samples_gotten; s++) { for (int32_t c = 0; c < AUDIO_WORKING_CHANNELS; c++) { *dst_ptr++ += *src_ptr++ * stream->volume; } } } if (!stream->is_used) { Audio_Stream_Close(sound_id); } } } double Audio_Stream_GetTimestamp(int32_t sound_id) { if (!g_AudioDeviceID || sound_id < 0 || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) { return -1.0; } double timestamp = -1.0; AUDIO_STREAM_SOUND *stream = &m_Streams[sound_id]; if (stream->duration > 0.0) { Audio_LockDevice(); timestamp = (double)stream->played_samples / (double)AUDIO_WORKING_RATE; Audio_UnlockDevice(); } return timestamp; } double Audio_Stream_GetDuration(int32_t sound_id) { if (!g_AudioDeviceID || sound_id < 0 || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) { return -1.0; } Audio_LockDevice(); AUDIO_STREAM_SOUND *stream = &m_Streams[sound_id]; double duration = stream->duration; Audio_UnlockDevice(); return duration; } bool Audio_Stream_SeekTimestamp(const int32_t sound_id, const double timestamp) { if (!g_AudioDeviceID || sound_id < 0 || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) { return false; } AUDIO_STREAM_SOUND *const stream = &m_Streams[sound_id]; if (!stream->is_used) { return false; } Audio_LockDevice(); const double time_base_sec = av_q2d(stream->av.stream->time_base); if (time_base_sec <= 0.0) { LOG_ERROR( "Audio_Stream_SeekTimestamp: invalid time_base %f", time_base_sec); Audio_UnlockDevice(); return false; } const int32_t stream_index = stream->av.stream->index; const int64_t seek_target = (int64_t)((MAX(0.0f, stream->start_at) + timestamp) / time_base_sec); const int32_t error_code = av_seek_frame( stream->av.format_ctx, stream_index, seek_target, AVSEEK_FLAG_ANY); if (error_code < 0) { LOG_ERROR( "seek failed for timestamp %f: %s", timestamp, av_err2str(error_code)); Audio_UnlockDevice(); return false; } avcodec_flush_buffers(stream->av.codec_ctx); if (stream->sdl.stream != nullptr) { M_DiscardSDLStreamData(stream); } stream->decode_timestamp = timestamp + MAX(stream->start_at, 0.0f); M_ResetPlaybackState(stream, timestamp); stream->is_read_done = false; Audio_UnlockDevice(); return true; } bool Audio_Stream_SetStartTimestamp(int32_t sound_id, double timestamp) { if (!g_AudioDeviceID || sound_id < 0 || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) { return false; } m_Streams[sound_id].start_at = timestamp; return true; } bool Audio_Stream_SetStopTimestamp(int32_t sound_id, double timestamp) { if (!g_AudioDeviceID || sound_id < 0 || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) { return false; } m_Streams[sound_id].stop_at = timestamp; return true; } ================================================ FILE: src/trx/av/image.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef struct { struct { int32_t x; int32_t y; int32_t width; int32_t height; } src, dst; } IMAGE_BLIT; typedef struct { int error_code; AVFormatContext *format_ctx; AVCodecContext *codec_ctx; const AVCodec *codec; AVFrame *frame; AVPacket *packet; } IMAGE_READER_CONTEXT; static void M_Free(IMAGE_READER_CONTEXT *const ctx) { if (ctx->packet != nullptr) { av_packet_free(&ctx->packet); } if (ctx->frame != nullptr) { av_frame_free(&ctx->frame); } if (ctx->codec_ctx != nullptr) { avcodec_free_context(&ctx->codec_ctx); } if (ctx->format_ctx != nullptr) { avformat_close_input(&ctx->format_ctx); } } static bool M_Init(const char *const path, IMAGE_READER_CONTEXT *const ctx) { ASSERT(ctx != nullptr); ctx->format_ctx = nullptr; ctx->codec = nullptr; ctx->codec_ctx = nullptr; ctx->frame = nullptr; ctx->packet = nullptr; int32_t error_code = 0; error_code = avformat_open_input(&ctx->format_ctx, path, nullptr, nullptr); if (error_code != 0) { goto finish; } #if 0 error_code = avformat_find_stream_info(format_ctx, nullptr); if (error_code < 0) { goto finish; } #endif AVStream *video_stream = nullptr; for (unsigned int i = 0; i < ctx->format_ctx->nb_streams; i++) { AVStream *current_stream = ctx->format_ctx->streams[i]; if (current_stream->codecpar == nullptr) { continue; } if (current_stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { video_stream = current_stream; break; } } if (video_stream == nullptr) { error_code = AVERROR_STREAM_NOT_FOUND; goto finish; } ctx->codec = avcodec_find_decoder(video_stream->codecpar->codec_id); if (ctx->codec == nullptr) { error_code = AVERROR_DEMUXER_NOT_FOUND; goto finish; } ctx->codec_ctx = avcodec_alloc_context3(ctx->codec); if (ctx->codec_ctx == nullptr) { error_code = AVERROR(ENOMEM); goto finish; } error_code = avcodec_parameters_to_context(ctx->codec_ctx, video_stream->codecpar); if (error_code) { goto finish; } #if 0 ctx->codec_ctx->thread_count = 0; if (ctx->codec->capabilities & AV_CODEC_CAP_FRAME_THREADS) ctx->codec_ctx->thread_type = FF_THREAD_FRAME; else if (ctx->codec->capabilities & AV_CODEC_CAP_SLICE_THREADS) ctx->codec_ctx->thread_type = FF_THREAD_SLICE; else ctx->codec_ctx->thread_count = 1; //don't use multithreading #endif error_code = avcodec_open2(ctx->codec_ctx, ctx->codec, nullptr); if (error_code < 0) { goto finish; } ctx->packet = av_packet_alloc(); error_code = av_read_frame(ctx->format_ctx, ctx->packet); if (error_code < 0) { goto finish; } error_code = avcodec_send_packet(ctx->codec_ctx, ctx->packet); if (error_code < 0) { goto finish; } ctx->frame = av_frame_alloc(); if (ctx->frame == nullptr) { error_code = AVERROR(ENOMEM); goto finish; } error_code = avcodec_receive_frame(ctx->codec_ctx, ctx->frame); if (error_code < 0) { goto finish; } error_code = 0; finish: if (error_code != 0) { LOG_ERROR( "Error while opening image %s: %s", path, av_err2str(error_code)); M_Free(ctx); return false; } return true; } static IMAGE_BLIT M_GetBlit( const int32_t source_width, const int32_t source_height, const int32_t target_width, const int32_t target_height, IMAGE_FIT_MODE fit_mode) { const float source_ratio = source_width / (float)source_height; const float target_ratio = target_width / (float)target_height; if (fit_mode == IMAGE_FIT_SMART) { const float ar_diff = (source_ratio > target_ratio ? source_ratio / target_ratio : target_ratio / source_ratio) - 1.0f; if (ar_diff <= 0.1f) { // if the difference between aspect ratios is under 10%, just // stretch it fit_mode = IMAGE_FIT_STRETCH; } else if (source_ratio <= target_ratio) { // if the viewport is too wide, center the image fit_mode = IMAGE_FIT_LETTERBOX; } else { // if the image is too wide, crop the image fit_mode = IMAGE_FIT_CROP; } } IMAGE_BLIT blit; switch (fit_mode) { case IMAGE_FIT_STRETCH: blit.src.width = source_width; blit.src.height = source_height; blit.src.x = 0; blit.src.y = 0; blit.dst.width = target_width; blit.dst.height = target_height; blit.dst.x = 0; blit.dst.y = 0; break; case IMAGE_FIT_CROP: blit.src.width = source_ratio < target_ratio ? source_width : source_height * target_ratio; blit.src.height = source_ratio < target_ratio ? source_width / target_ratio : source_height; blit.src.x = (source_width - blit.src.width) / 2; blit.src.y = (source_height - blit.src.height) / 2; blit.dst.width = target_width; blit.dst.height = target_height; blit.dst.x = 0; blit.dst.y = 0; break; case IMAGE_FIT_LETTERBOX: blit.src.width = source_width; blit.src.height = source_height; blit.src.x = 0; blit.src.y = 0; blit.dst.width = (source_ratio > target_ratio) ? target_width : target_height * source_ratio; blit.dst.height = (source_ratio > target_ratio) ? target_width / source_ratio : target_height; blit.dst.x = (target_width - blit.dst.width) / 2; blit.dst.y = (target_height - blit.dst.height) / 2; break; default: ASSERT_FAIL(); break; } return blit; } static IMAGE *M_ConstructImage( IMAGE_READER_CONTEXT *const ctx, const int32_t target_width, const int32_t target_height, IMAGE_FIT_MODE fit_mode) { ASSERT(ctx != nullptr); ASSERT(target_width > 0); ASSERT(target_height > 0); IMAGE_BLIT blit = M_GetBlit( ctx->frame->width, ctx->frame->height, target_width, target_height, fit_mode); if (blit.src.y != 0 || blit.src.x != 0) { ctx->frame->crop_top = blit.src.y; ctx->frame->crop_left = blit.src.x; av_frame_apply_cropping(ctx->frame, AV_FRAME_CROP_UNALIGNED); } struct SwsContext *const sws_ctx = sws_getContext( blit.src.width, blit.src.height, ctx->frame->format, blit.dst.width, blit.dst.height, AV_PIX_FMT_RGB24, SWS_BILINEAR, nullptr, nullptr, nullptr); if (sws_ctx == nullptr) { LOG_ERROR("Failed to get SWS context"); return nullptr; } IMAGE *const target_image = Image_Create(target_width, target_height); uint8_t *dst_planes[4] = { (uint8_t *)target_image->data + (blit.dst.y * target_image->width + blit.dst.x) * sizeof(IMAGE_PIXEL), nullptr, nullptr, nullptr }; int dst_linesize[4] = { target_image->width * sizeof(IMAGE_PIXEL), 0, 0, 0 }; sws_scale( sws_ctx, (const uint8_t *const *)ctx->frame->data, ctx->frame->linesize, 0, blit.src.height, dst_planes, dst_linesize); sws_freeContext(sws_ctx); return target_image; } IMAGE *Image_Create(const int width, const int height) { IMAGE *image = Memory_Alloc(sizeof(IMAGE)); image->width = width; image->height = height; image->data = Memory_Alloc(width * height * sizeof(IMAGE_PIXEL)); return image; } IMAGE *Image_CreateFromFile(const char *const path) { ASSERT(path != nullptr); IMAGE_READER_CONTEXT ctx; if (!M_Init(path, &ctx)) { return nullptr; } IMAGE *target_image = M_ConstructImage( &ctx, ctx.frame->width, ctx.frame->height, IMAGE_FIT_STRETCH); M_Free(&ctx); return target_image; } IMAGE *Image_CreateFromFileInto( const char *const path, const int32_t target_width, const int32_t target_height, const IMAGE_FIT_MODE fit_mode) { ASSERT(path != nullptr); IMAGE_READER_CONTEXT ctx; if (!M_Init(path, &ctx)) { return nullptr; } IMAGE *target_image = M_ConstructImage(&ctx, target_width, target_height, fit_mode); M_Free(&ctx); return target_image; } bool Image_SaveToFile(const IMAGE *const image, const char *const path) { ASSERT(image != nullptr); ASSERT(path != nullptr); bool result = false; int error_code = 0; const AVCodec *codec = nullptr; AVCodecContext *codec_ctx = nullptr; AVFrame *frame = nullptr; AVPacket *packet = nullptr; struct SwsContext *sws_ctx = nullptr; MYFILE *fp = nullptr; enum AVPixelFormat src_pix_fmt = AV_PIX_FMT_RGB24; enum AVPixelFormat dst_pix_fmt; enum AVCodecID codec_id; if (strstr(path, ".jpg")) { dst_pix_fmt = AV_PIX_FMT_YUVJ420P; codec_id = AV_CODEC_ID_MJPEG; } else if (strstr(path, ".png")) { dst_pix_fmt = AV_PIX_FMT_RGB24; codec_id = AV_CODEC_ID_PNG; } else { LOG_ERROR("Cannot determine image format based on path '%s'", path); goto cleanup; } File_EnsureParentDirectories(path); fp = File_Open(path, FILE_OPEN_WRITE); if (fp == nullptr) { LOG_ERROR("Cannot create image file: %s", path); goto cleanup; } codec = avcodec_find_encoder(codec_id); if (codec == nullptr) { error_code = AVERROR_MUXER_NOT_FOUND; goto cleanup; } codec_ctx = avcodec_alloc_context3(codec); if (codec_ctx == nullptr) { error_code = AVERROR(ENOMEM); goto cleanup; } codec_ctx->bit_rate = 400000; codec_ctx->width = image->width; codec_ctx->height = image->height; codec_ctx->time_base = (AVRational) { 1, 25 }; codec_ctx->pix_fmt = dst_pix_fmt; if (codec_id == AV_CODEC_ID_MJPEG) { // 9 JPEG quality codec_ctx->flags |= AV_CODEC_FLAG_QSCALE; codec_ctx->global_quality = FF_QP2LAMBDA * 9; } error_code = avcodec_open2(codec_ctx, codec, nullptr); if (error_code < 0) { goto cleanup; } frame = av_frame_alloc(); if (frame == nullptr) { error_code = AVERROR(ENOMEM); goto cleanup; } frame->format = codec_ctx->pix_fmt; frame->width = codec_ctx->width; frame->height = codec_ctx->height; frame->pts = 0; error_code = av_image_alloc( frame->data, frame->linesize, codec_ctx->width, codec_ctx->height, codec_ctx->pix_fmt, 32); if (error_code < 0) { goto cleanup; } packet = av_packet_alloc(); sws_ctx = sws_getContext( image->width, image->height, src_pix_fmt, frame->width, frame->height, dst_pix_fmt, SWS_BILINEAR, nullptr, nullptr, nullptr); if (sws_ctx == nullptr) { LOG_ERROR("Failed to get SWS context"); error_code = AVERROR_EXTERNAL; goto cleanup; } uint8_t *src_planes[4]; int src_linesize[4]; av_image_fill_arrays( src_planes, src_linesize, (const uint8_t *)image->data, src_pix_fmt, image->width, image->height, 1); sws_scale( sws_ctx, (const uint8_t *const *)src_planes, src_linesize, 0, image->height, frame->data, frame->linesize); error_code = avcodec_send_frame(codec_ctx, frame); if (error_code < 0) { goto cleanup; } while (error_code >= 0) { error_code = avcodec_receive_packet(codec_ctx, packet); if (error_code == AVERROR(EAGAIN) || error_code == AVERROR_EOF) { error_code = 0; break; } if (error_code < 0) { goto cleanup; } File_WriteData(fp, packet->data, packet->size); av_packet_unref(packet); } cleanup: if (error_code) { LOG_ERROR( "Error while saving image %s: %s", path, av_err2str(error_code)); } else { result = true; } if (fp) { File_Close(fp); fp = nullptr; } if (sws_ctx) { sws_freeContext(sws_ctx); } if (packet) { av_packet_free(&packet); } if (codec) { avcodec_free_context(&codec_ctx); } if (frame) { av_freep(&frame->data[0]); av_frame_free(&frame); } return result; } IMAGE *Image_Scale( const IMAGE *const source_image, size_t target_width, size_t target_height, IMAGE_FIT_MODE fit_mode) { ASSERT(source_image != nullptr); ASSERT(source_image->data != nullptr); ASSERT(target_width > 0); ASSERT(target_height > 0); IMAGE_BLIT blit = M_GetBlit( source_image->width, source_image->height, target_width, target_height, fit_mode); struct SwsContext *const sws_ctx = sws_getContext( blit.src.width, blit.src.height, AV_PIX_FMT_RGB24, blit.dst.width, blit.dst.height, AV_PIX_FMT_RGB24, SWS_BILINEAR, nullptr, nullptr, nullptr); if (sws_ctx == nullptr) { LOG_ERROR("Failed to get SWS context"); return nullptr; } IMAGE *const target_image = Image_Create(target_width, target_height); uint8_t *src_planes[4] = { (uint8_t *)source_image->data + (blit.src.y * source_image->width + blit.src.x) * sizeof(IMAGE_PIXEL), nullptr, nullptr, nullptr }; int src_linesize[4] = { source_image->width * sizeof(IMAGE_PIXEL), 0, 0, 0 }; uint8_t *dst_planes[4] = { (uint8_t *)target_image->data + (blit.dst.y * target_image->width + blit.dst.x) * sizeof(IMAGE_PIXEL), nullptr, nullptr, nullptr }; int dst_linesize[4] = { target_image->width * sizeof(IMAGE_PIXEL), 0, 0, 0 }; sws_scale( sws_ctx, (const uint8_t *const *)src_planes, src_linesize, 0, blit.src.height, (uint8_t *const *)dst_planes, dst_linesize); sws_freeContext(sws_ctx); return target_image; } void Image_Free(IMAGE *image) { if (image) { Memory_FreePointer(&image->data); } Memory_FreePointer(&image); } ================================================ FILE: src/trx/av/image.h ================================================ #pragma once #include #include typedef struct { uint8_t r; uint8_t g; uint8_t b; } IMAGE_PIXEL; typedef struct { int32_t width; int32_t height; IMAGE_PIXEL *data; } IMAGE; typedef enum { IMAGE_FIT_STRETCH, IMAGE_FIT_CROP, IMAGE_FIT_LETTERBOX, IMAGE_FIT_SMART, } IMAGE_FIT_MODE; IMAGE *Image_Create(int width, int height); IMAGE *Image_CreateFromFile(const char *path); IMAGE *Image_CreateFromFileInto( const char *path, int32_t target_width, int32_t target_height, IMAGE_FIT_MODE fit_mode); void Image_Free(IMAGE *image); bool Image_GetFileInfo(const char *path, int32_t *width, int32_t *height); bool Image_SaveToFile(const IMAGE *image, const char *path); IMAGE *Image_Scale( const IMAGE *source_image, size_t target_width, size_t target_height, IMAGE_FIT_MODE fit_mode); ================================================ FILE: src/trx/av/video.c ================================================ /* * Copyright (c) 2003 Fabrice Bellard * * This file is part of FFmpeg. * * FFmpeg is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * FFmpeg is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with FFmpeg; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define MAX_QUEUE_SIZE (15 * 1024 * 1024) #define MIN_FRAMES 25 #define SDL_AUDIO_MIN_BUFFER_SIZE 512 #define SDL_AUDIO_MAX_CALLBACKS_PER_SEC 30 #define AV_SYNC_THRESHOLD_MIN 0.04 #define AV_SYNC_THRESHOLD_MAX 0.1 #define AV_SYNC_FRAMEDUP_THRESHOLD 0.1 #define AV_NOSYNC_THRESHOLD 10.0 #define SAMPLE_CORRECTION_PERCENT_MAX 10 #define AUDIO_DIFF_AVG_NB 20 #define REFRESH_RATE 0.01 #define SAMPLE_ARRAY_SIZE (8 * 65536) #define VIDEO_PICTURE_QUEUE_SIZE 3 #define SAMPLE_QUEUE_SIZE 9 #define FRAME_QUEUE_SIZE FFMAX(SAMPLE_QUEUE_SIZE, VIDEO_PICTURE_QUEUE_SIZE) #define FF_QUIT_EVENT (SDL_USEREVENT + 2) typedef struct { AVPacket *pkt; int serial; } M_PACKET_LIST; typedef struct { AVFifo *pkt_list; int nb_packets; int size; int64_t duration; bool abort_request; int serial; SDL_mutex *mutex; SDL_cond *cond; } M_PACKET_QUEUE; typedef struct { int freq; int channels; AVChannelLayout ch_layout; enum AVSampleFormat fmt; int frame_size; int bytes_per_sec; } M_AUDIO_PARAMS; typedef struct { double pts; double pts_drift; double last_updated; double speed; int serial; bool paused; int *queue_serial; } M_CLOCK; typedef struct { AVFrame *frame; int serial; double pts; double duration; int64_t pos; int width; int height; int format; AVRational sar; } M_FRAME; typedef struct { int64_t pkt_pos; } M_FRAME_DATA; typedef struct { M_FRAME queue[FRAME_QUEUE_SIZE]; int rindex; int windex; int size; int max_size; int keep_last; int rindex_shown; SDL_mutex *mutex; SDL_cond *cond; M_PACKET_QUEUE *pktq; } M_FRAME_QUEUE; typedef enum { AV_SYNC_AUDIO_MASTER, AV_SYNC_VIDEO_MASTER, AV_SYNC_EXTERNAL_CLOCK, } M_CLOCK_TYPE; typedef struct { AVPacket *pkt; M_PACKET_QUEUE *queue; AVCodecContext *avctx; int pkt_serial; int finished; bool packet_pending; SDL_cond *empty_queue_cond; int64_t start_pts; AVRational start_pts_tb; int64_t next_pts; AVRational next_pts_tb; SDL_Thread *decoder_tid; } M_DECODER; typedef struct { SDL_Thread *read_tid; AVInputFormat *iformat; bool abort_request; bool playback_finished; bool force_refresh; bool paused; bool last_paused; bool queue_attachments_req; int read_pause_return; AVFormatContext *ic; double remaining_time; M_CLOCK audclk; M_CLOCK vidclk; M_CLOCK extclk; M_FRAME_QUEUE pictq; M_FRAME_QUEUE sampq; M_DECODER auddec; M_DECODER viddec; int audio_stream; int av_sync_type; double audio_clock; int audio_clock_serial; double audio_diff_cum; double audio_diff_avg_coef; double audio_diff_threshold; int audio_diff_avg_count; AVStream *audio_st; M_PACKET_QUEUE audioq; int audio_hw_buf_size; uint8_t *audio_buf; uint8_t *audio_buf1; unsigned int audio_buf_size; unsigned int audio_buf1_size; int audio_buf_index; int audio_write_buf_size; int audio_volume; M_AUDIO_PARAMS audio_src; M_AUDIO_PARAMS audio_tgt; struct SwrContext *swr_ctx; int frame_drops_early; int frame_drops_late; // surface size at the size of the display buffer int surface_width; int surface_height; // target surface coordinates, keeping the video A:R int target_surface_x; int target_surface_y; int target_surface_width; int target_surface_height; double frame_timer; double frame_last_returned_time; int video_stream; AVStream *video_st; M_PACKET_QUEUE videoq; double max_frame_duration; // maximum duration of a frame - above this, we // consider the jump a timestamp discontinuity struct SwsContext *img_convert_ctx; bool eof; char *filename; int display_width; int display_height; SDL_cond *continue_read_thread; void *primary_surface; enum AVPixelFormat primary_surface_pixel_format; int32_t primary_surface_stride; VIDEO_SURFACE_ALLOCATOR_FUNC surface_allocator_func; void *surface_allocator_func_user_data; void (*surface_deallocator_func)(void *surface, void *user_data); void *surface_deallocator_func_user_data; void (*surface_clear_func)(void *surface, void *user_data); void *surface_clear_func_user_data; void (*render_begin_func)(void *surface, void *user_data); void *render_begin_func_user_data; void (*render_end_func)(void *surface, void *user_data); void *render_end_func_user_data; void *(*surface_lock_func)(void *surface, void *user_data); void *surface_lock_func_user_data; void (*surface_unlock_func)(void *surface, void *user_data); void *surface_unlock_func_user_data; void (*surface_upload_func)(void *surface, void *user_data); void *surface_upload_func_user_data; bool audio_enabled; } M_STATE; static int64_t m_AudioCallbackTime; static SDL_AudioDeviceID m_AudioDevice = 0; static int M_PacketQueuePutPrivate(M_PACKET_QUEUE *q, AVPacket *pkt) { M_PACKET_LIST pkt1; if (q->abort_request) { return -1; } pkt1.pkt = pkt; pkt1.serial = q->serial; int32_t ret = av_fifo_write(q->pkt_list, &pkt1, 1); if (ret < 0) { return ret; } q->nb_packets++; q->size += pkt1.pkt->size + sizeof(pkt1); q->duration += pkt1.pkt->duration; SDL_CondSignal(q->cond); return 0; } static int M_PacketQueuePut(M_PACKET_QUEUE *q, AVPacket *pkt) { AVPacket *pkt1; int ret; pkt1 = av_packet_alloc(); if (!pkt1) { av_packet_unref(pkt); return -1; } av_packet_move_ref(pkt1, pkt); SDL_LockMutex(q->mutex); ret = M_PacketQueuePutPrivate(q, pkt1); SDL_UnlockMutex(q->mutex); if (ret < 0) { av_packet_free(&pkt1); } return ret; } static int M_PacketQueuePutNullPacket( M_PACKET_QUEUE *q, AVPacket *pkt, int stream_index) { pkt->stream_index = stream_index; return M_PacketQueuePut(q, pkt); } static int M_PacketQueueInit(M_PACKET_QUEUE *q) { memset(q, 0, sizeof(M_PACKET_QUEUE)); q->pkt_list = av_fifo_alloc2(1, sizeof(M_PACKET_LIST), AV_FIFO_FLAG_AUTO_GROW); if (q->pkt_list == nullptr) { return AVERROR(ENOMEM); } q->mutex = SDL_CreateMutex(); if (!q->mutex) { LOG_ERROR("SDL_CreateMutex(): %s", SDL_GetError()); return AVERROR(ENOMEM); } q->cond = SDL_CreateCond(); if (!q->cond) { LOG_ERROR("SDL_CreateCond(): %s", SDL_GetError()); return AVERROR(ENOMEM); } q->abort_request = true; return 0; } static void M_PacketQueueFlush(M_PACKET_QUEUE *q) { if (q == nullptr || q->mutex == nullptr || q->pkt_list == nullptr) { return; } M_PACKET_LIST pkt1; SDL_LockMutex(q->mutex); while (av_fifo_read(q->pkt_list, &pkt1, 1) >= 0) { av_packet_free(&pkt1.pkt); } q->nb_packets = 0; q->size = 0; q->duration = 0; q->serial++; SDL_UnlockMutex(q->mutex); } static void M_PacketQueueDestroy(M_PACKET_QUEUE *q) { if (q == nullptr) { return; } M_PacketQueueFlush(q); av_fifo_freep2(&q->pkt_list); if (q->mutex != nullptr) { SDL_DestroyMutex(q->mutex); q->mutex = nullptr; } if (q->cond != nullptr) { SDL_DestroyCond(q->cond); q->cond = nullptr; } } static void M_PacketQueueAbort(M_PACKET_QUEUE *q) { SDL_LockMutex(q->mutex); q->abort_request = true; SDL_CondSignal(q->cond); SDL_UnlockMutex(q->mutex); } static void M_PacketQueueStart(M_PACKET_QUEUE *q) { SDL_LockMutex(q->mutex); q->abort_request = false; q->serial++; SDL_UnlockMutex(q->mutex); } static int M_PacketQueueGet( M_PACKET_QUEUE *q, AVPacket *pkt, int block, int *serial) { M_PACKET_LIST pkt1; int ret; SDL_LockMutex(q->mutex); while (1) { if (q->abort_request) { ret = -1; break; } if (av_fifo_read(q->pkt_list, &pkt1, 1) >= 0) { q->nb_packets--; q->size -= pkt1.pkt->size + sizeof(pkt1); q->duration -= pkt1.pkt->duration; av_packet_move_ref(pkt, pkt1.pkt); if (serial) { *serial = pkt1.serial; } av_packet_free(&pkt1.pkt); ret = 1; break; } else if (!block) { ret = 0; break; } else { SDL_CondWait(q->cond, q->mutex); } } SDL_UnlockMutex(q->mutex); return ret; } static int M_DecoderInit( M_DECODER *d, AVCodecContext *avctx, M_PACKET_QUEUE *queue, SDL_cond *empty_queue_cond) { memset(d, 0, sizeof(M_DECODER)); d->pkt = av_packet_alloc(); if (!d->pkt) { return AVERROR(ENOMEM); } d->avctx = avctx; d->queue = queue; d->empty_queue_cond = empty_queue_cond; d->start_pts = AV_NOPTS_VALUE; d->pkt_serial = -1; return 0; } static int M_DecoderDecodeFrame(M_DECODER *d, AVFrame *frame) { int ret = AVERROR(EAGAIN); while (1) { if (d->queue->serial == d->pkt_serial) { do { if (d->queue->abort_request) { return -1; } switch (d->avctx->codec_type) { case AVMEDIA_TYPE_VIDEO: ret = avcodec_receive_frame(d->avctx, frame); if (ret >= 0) { frame->pts = frame->best_effort_timestamp; } break; case AVMEDIA_TYPE_AUDIO: ret = avcodec_receive_frame(d->avctx, frame); if (ret >= 0) { AVRational tb = (AVRational) { 1, frame->sample_rate }; if (frame->pts != AV_NOPTS_VALUE) { frame->pts = av_rescale_q( frame->pts, d->avctx->pkt_timebase, tb); } else if (d->next_pts != AV_NOPTS_VALUE) { frame->pts = av_rescale_q(d->next_pts, d->next_pts_tb, tb); } if (frame->pts != AV_NOPTS_VALUE) { d->next_pts = frame->pts + frame->nb_samples; d->next_pts_tb = tb; } } break; default: break; } if (ret == AVERROR_EOF) { d->finished = d->pkt_serial; avcodec_flush_buffers(d->avctx); return 0; } if (ret >= 0) { return 1; } } while (ret != AVERROR(EAGAIN)); } while (1) { if (d->queue->nb_packets == 0) { SDL_CondSignal(d->empty_queue_cond); } if (d->packet_pending) { d->packet_pending = false; } else { int old_serial = d->pkt_serial; if (M_PacketQueueGet(d->queue, d->pkt, 1, &d->pkt_serial) < 0) { return -1; } if (old_serial != d->pkt_serial) { avcodec_flush_buffers(d->avctx); d->finished = 0; d->next_pts = d->start_pts; d->next_pts_tb = d->start_pts_tb; } } if (d->queue->serial == d->pkt_serial) { break; } av_packet_unref(d->pkt); } if (d->pkt->buf && !d->pkt->opaque_ref) { d->pkt->opaque_ref = av_buffer_allocz(sizeof(M_FRAME_DATA)); if (!d->pkt->opaque_ref) { return AVERROR(ENOMEM); } M_FRAME_DATA *fd = (M_FRAME_DATA *)d->pkt->opaque_ref->data; fd->pkt_pos = d->pkt->pos; } if (avcodec_send_packet(d->avctx, d->pkt) == AVERROR(EAGAIN)) { LOG_ERROR( "Receive_frame and send_packet both returned EAGAIN, " "which is an API violation."); d->packet_pending = true; } else { av_packet_unref(d->pkt); } } } static void M_DecoderShutdown(M_DECODER *d) { av_packet_free(&d->pkt); avcodec_free_context(&d->avctx); } static void M_FrameQueueUnrefItem(M_FRAME *vp) { av_frame_unref(vp->frame); } static int M_FrameQueueInit( M_FRAME_QUEUE *f, M_PACKET_QUEUE *pktq, int max_size, int keep_last) { memset(f, 0, sizeof(M_FRAME_QUEUE)); if (!(f->mutex = SDL_CreateMutex())) { LOG_ERROR("SDL_CreateMutex(): %s", SDL_GetError()); return AVERROR(ENOMEM); } if (!(f->cond = SDL_CreateCond())) { LOG_ERROR("SDL_CreateCond(): %s", SDL_GetError()); return AVERROR(ENOMEM); } f->pktq = pktq; f->max_size = FFMIN(max_size, FRAME_QUEUE_SIZE); f->keep_last = !!keep_last; for (int i = 0; i < f->max_size; i++) { if (!(f->queue[i].frame = av_frame_alloc())) { return AVERROR(ENOMEM); } } return 0; } static void M_FrameQueueShutdown(M_FRAME_QUEUE *f) { for (int i = 0; i < f->max_size; i++) { M_FRAME *vp = &f->queue[i]; if (vp->frame != nullptr) { M_FrameQueueUnrefItem(vp); av_frame_free(&vp->frame); } } if (f->mutex != nullptr) { SDL_DestroyMutex(f->mutex); f->mutex = nullptr; } if (f->cond != nullptr) { SDL_DestroyCond(f->cond); f->cond = nullptr; } } static void M_FrameQueueSignal(M_FRAME_QUEUE *f) { SDL_LockMutex(f->mutex); SDL_CondSignal(f->cond); SDL_UnlockMutex(f->mutex); } static M_FRAME *M_FrameQueuePeek(M_FRAME_QUEUE *f) { return &f->queue[(f->rindex + f->rindex_shown) % f->max_size]; } static M_FRAME *M_FrameQueuePeekNext(M_FRAME_QUEUE *f) { return &f->queue[(f->rindex + f->rindex_shown + 1) % f->max_size]; } static M_FRAME *M_FrameQueuePeekLast(M_FRAME_QUEUE *f) { return &f->queue[f->rindex]; } static M_FRAME *M_FrameQueuePeekWritable(M_FRAME_QUEUE *f) { SDL_LockMutex(f->mutex); while (f->size >= f->max_size && !f->pktq->abort_request) { SDL_CondWait(f->cond, f->mutex); } SDL_UnlockMutex(f->mutex); if (f->pktq->abort_request) { return nullptr; } return &f->queue[f->windex]; } static M_FRAME *M_FrameQueuePeekReadable(M_FRAME_QUEUE *f) { SDL_LockMutex(f->mutex); while (f->size - f->rindex_shown <= 0 && !f->pktq->abort_request) { SDL_CondWait(f->cond, f->mutex); } SDL_UnlockMutex(f->mutex); if (f->pktq->abort_request) { return nullptr; } return &f->queue[(f->rindex + f->rindex_shown) % f->max_size]; } static void M_FrameQueuePush(M_FRAME_QUEUE *f) { if (++f->windex == f->max_size) { f->windex = 0; } SDL_LockMutex(f->mutex); f->size++; SDL_CondSignal(f->cond); SDL_UnlockMutex(f->mutex); } static void M_FrameQueueNext(M_FRAME_QUEUE *f) { if (f->keep_last && !f->rindex_shown) { f->rindex_shown = 1; return; } M_FrameQueueUnrefItem(&f->queue[f->rindex]); if (++f->rindex == f->max_size) { f->rindex = 0; } SDL_LockMutex(f->mutex); f->size--; SDL_CondSignal(f->cond); SDL_UnlockMutex(f->mutex); } static int M_FrameQueueNBRemaining(M_FRAME_QUEUE *f) { return f->size - f->rindex_shown; } static void M_DecoderAbort(M_DECODER *d, M_FRAME_QUEUE *fq) { M_PacketQueueAbort(d->queue); M_FrameQueueSignal(fq); SDL_WaitThread(d->decoder_tid, nullptr); d->decoder_tid = nullptr; M_PacketQueueFlush(d->queue); } static void M_ReallocPrimarySurface( M_STATE *is, int surface_width, int surface_height, bool clear) { is->surface_width = surface_width; is->surface_height = surface_height; if (is->primary_surface != nullptr) { is->surface_deallocator_func( is->primary_surface, is->surface_deallocator_func_user_data); is->primary_surface = nullptr; } { is->primary_surface = is->surface_allocator_func( is->surface_width, is->surface_height, is->surface_allocator_func_user_data); } if (clear) { is->surface_clear_func( is->primary_surface, is->surface_clear_func_user_data); } } static void M_RecalcSurfaceTargetRect( M_STATE *is, int32_t frame_width, int32_t frame_height) { const float source_ratio = frame_width / (float)frame_height; const float target_ratio = is->surface_width / (float)is->surface_height; is->target_surface_width = source_ratio < target_ratio ? is->surface_height * source_ratio : is->surface_width; is->target_surface_height = source_ratio < target_ratio ? is->surface_height : is->surface_width / source_ratio; is->target_surface_x = (is->surface_width - is->target_surface_width) / 2; is->target_surface_y = (is->surface_height - is->target_surface_height) / 2; } static int M_UploadTexture(M_STATE *is, AVFrame *frame) { int ret = 0; is->img_convert_ctx = sws_getCachedContext( is->img_convert_ctx, frame->width, frame->height, frame->format, is->target_surface_width, is->target_surface_height, is->primary_surface_pixel_format, SWS_BILINEAR, nullptr, nullptr, nullptr); if (is->img_convert_ctx) { is->render_begin_func( is->primary_surface, is->render_begin_func_user_data); void *pixels = is->surface_lock_func( is->primary_surface, is->surface_lock_func_user_data); if (pixels != nullptr) { uint8_t *surf_planes[4] = { pixels, nullptr, nullptr, nullptr }; int surf_linesize[4] = {}; if (is->primary_surface_stride > 0) { surf_linesize[0] = is->primary_surface_stride; } else { surf_linesize[0] = av_image_get_linesize( is->primary_surface_pixel_format, is->surface_width, 0); } surf_planes[0] += is->target_surface_y * surf_linesize[0]; surf_planes[0] += av_image_get_linesize( is->primary_surface_pixel_format, is->target_surface_x, 0); sws_scale( is->img_convert_ctx, (const uint8_t *const *)frame->data, frame->linesize, 0, frame->height, surf_planes, surf_linesize); is->surface_unlock_func( is->primary_surface, is->surface_unlock_func_user_data); is->surface_upload_func( is->primary_surface, is->surface_upload_func_user_data); } is->render_end_func(is->primary_surface, is->render_end_func_user_data); } else { LOG_ERROR("Cannot initialize the conversion context"); ret = -1; } return ret; } static void M_VideoImageDisplay(M_STATE *is) { M_FRAME *vp = M_FrameQueuePeekLast(&is->pictq); M_RecalcSurfaceTargetRect(is, vp->frame->width, vp->frame->height); M_UploadTexture(is, vp->frame); } static void M_StreamComponentClose(M_STATE *is, int stream_index) { AVFormatContext *ic = is->ic; AVCodecParameters *codecpar; if (stream_index < 0 || stream_index >= (signed)ic->nb_streams) { return; } codecpar = ic->streams[stream_index]->codecpar; switch (codecpar->codec_type) { case AVMEDIA_TYPE_AUDIO: M_DecoderAbort(&is->auddec, &is->sampq); M_DecoderShutdown(&is->auddec); swr_free(&is->swr_ctx); av_freep(&is->audio_buf1); if (m_AudioDevice > 0) { SDL_CloseAudioDevice(m_AudioDevice); m_AudioDevice = 0; } is->audio_buf1_size = 0; is->audio_buf = nullptr; break; case AVMEDIA_TYPE_VIDEO: M_DecoderAbort(&is->viddec, &is->pictq); M_DecoderShutdown(&is->viddec); break; default: break; } ic->streams[stream_index]->discard = AVDISCARD_ALL; switch (codecpar->codec_type) { case AVMEDIA_TYPE_AUDIO: is->audio_st = nullptr; is->audio_stream = -1; break; case AVMEDIA_TYPE_VIDEO: is->video_st = nullptr; is->video_stream = -1; break; default: break; } } static void M_StreamClose(M_STATE *is) { if (is == nullptr) { return; } if (is->read_tid != nullptr) { SDL_WaitThread(is->read_tid, nullptr); is->read_tid = nullptr; } if (is->ic != nullptr && is->audio_stream >= 0) { M_StreamComponentClose(is, is->audio_stream); } if (is->ic != nullptr && is->video_stream >= 0) { M_StreamComponentClose(is, is->video_stream); } if (is->ic != nullptr) { avformat_close_input(&is->ic); } M_PacketQueueDestroy(&is->videoq); M_PacketQueueDestroy(&is->audioq); if (is->pictq.mutex != nullptr || is->pictq.cond != nullptr) { M_FrameQueueShutdown(&is->pictq); } if (is->sampq.mutex != nullptr || is->sampq.cond != nullptr) { M_FrameQueueShutdown(&is->sampq); } if (is->continue_read_thread != nullptr) { SDL_DestroyCond(is->continue_read_thread); is->continue_read_thread = nullptr; } sws_freeContext(is->img_convert_ctx); av_free(is->filename); if (is->primary_surface) { is->surface_deallocator_func( is->primary_surface, is->surface_deallocator_func_user_data); } av_free(is); } static void M_VideoDisplay(M_STATE *is) { if (is->video_st) { M_VideoImageDisplay(is); } } static double M_GetClock(M_CLOCK *c) { if (*c->queue_serial != c->serial) { return NAN; } if (c->paused) { return c->pts; } else { double time = av_gettime_relative() / 1000000.0; return c->pts_drift + time - (time - c->last_updated) * (1.0 - c->speed); } } static void M_SetClockAt(M_CLOCK *c, double pts, int serial, double time) { c->pts = pts; c->last_updated = time; c->pts_drift = c->pts - time; c->serial = serial; } static void M_SetClock(M_CLOCK *c, double pts, int serial) { double time = av_gettime_relative() / 1000000.0; M_SetClockAt(c, pts, serial, time); } static void M_InitClock(M_CLOCK *c, int *queue_serial) { c->speed = 1.0; c->paused = false; c->queue_serial = queue_serial; M_SetClock(c, NAN, -1); } static void M_SyncClockToSlave(M_CLOCK *const c, M_CLOCK *const slave) { double clock = M_GetClock(c); double slave_clock = M_GetClock(slave); if (!isnan(slave_clock) && (isnan(clock) || fabs(clock - slave_clock) > AV_NOSYNC_THRESHOLD)) { M_SetClock(c, slave_clock, slave->serial); } } static int M_GetMasterSyncType(M_STATE *is) { if (is->av_sync_type == AV_SYNC_VIDEO_MASTER) { if (is->video_st) { return AV_SYNC_VIDEO_MASTER; } else { return AV_SYNC_AUDIO_MASTER; } } else if (is->av_sync_type == AV_SYNC_AUDIO_MASTER) { if (is->audio_st) { return AV_SYNC_AUDIO_MASTER; } else { return AV_SYNC_EXTERNAL_CLOCK; } } else { return AV_SYNC_EXTERNAL_CLOCK; } } static double M_GetMasterClock(M_STATE *is) { switch (M_GetMasterSyncType(is)) { case AV_SYNC_VIDEO_MASTER: return M_GetClock(&is->vidclk); case AV_SYNC_AUDIO_MASTER: return M_GetClock(&is->audclk); default: return M_GetClock(&is->extclk); } } static double M_ComputeTargetDelay(double delay, M_STATE *is) { double sync_threshold, diff = 0; if (M_GetMasterSyncType(is) != AV_SYNC_VIDEO_MASTER) { diff = M_GetClock(&is->vidclk) - M_GetMasterClock(is); sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay)); if (!isnan(diff) && fabs(diff) < is->max_frame_duration) { if (diff <= -sync_threshold) { delay = FFMAX(0, delay + diff); } else if ( diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD) { delay = delay + diff; } else if (diff >= sync_threshold) { delay = 2 * delay; } } } return delay; } static double M_VPDuration(M_STATE *is, M_FRAME *vp, M_FRAME *nextvp) { if (vp->serial == nextvp->serial) { double duration = nextvp->pts - vp->pts; if (isnan(duration) || duration <= 0 || duration > is->max_frame_duration) { return vp->duration; } else { return duration; } } else { return 0.0; } } static void M_UpdateVideoPTS(M_STATE *is, double pts, int64_t pos, int serial) { M_SetClock(&is->vidclk, pts, serial); M_SyncClockToSlave(&is->extclk, &is->vidclk); } static void M_VideoRefresh(void *opaque, double *remaining_time) { M_STATE *is = opaque; double time; if (is->video_st) { retry: if (M_FrameQueueNBRemaining(&is->pictq) != 0) { double last_duration, duration, delay; M_FRAME *vp, *lastvp; lastvp = M_FrameQueuePeekLast(&is->pictq); vp = M_FrameQueuePeek(&is->pictq); if (vp->serial != is->videoq.serial) { M_FrameQueueNext(&is->pictq); goto retry; } if (lastvp->serial != vp->serial) { is->frame_timer = av_gettime_relative() / 1000000.0; } if (is->paused) { goto display; } last_duration = M_VPDuration(is, lastvp, vp); delay = M_ComputeTargetDelay(last_duration, is); time = av_gettime_relative() / 1000000.0; if (time < is->frame_timer + delay) { *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time); goto display; } is->frame_timer += delay; if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX) { is->frame_timer = time; } SDL_LockMutex(is->pictq.mutex); if (!isnan(vp->pts)) { M_UpdateVideoPTS(is, vp->pts, vp->pos, vp->serial); } SDL_UnlockMutex(is->pictq.mutex); if (M_FrameQueueNBRemaining(&is->pictq) > 1) { M_FRAME *nextvp = M_FrameQueuePeekNext(&is->pictq); duration = M_VPDuration(is, vp, nextvp); if (M_GetMasterSyncType(is) != AV_SYNC_VIDEO_MASTER && time > is->frame_timer + duration) { is->frame_drops_late++; M_FrameQueueNext(&is->pictq); goto retry; } } M_FrameQueueNext(&is->pictq); is->force_refresh = true; } display: if (is->force_refresh && is->pictq.rindex_shown) { M_VideoDisplay(is); } } is->force_refresh = false; } static int M_QueuePicture( M_STATE *is, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial) { M_FRAME *vp; if (!(vp = M_FrameQueuePeekWritable(&is->pictq))) { return -1; } vp->sar = src_frame->sample_aspect_ratio; vp->width = src_frame->width; vp->height = src_frame->height; vp->format = src_frame->format; vp->pts = pts; vp->duration = duration; vp->pos = pos; vp->serial = serial; av_frame_move_ref(vp->frame, src_frame); M_FrameQueuePush(&is->pictq); return 0; } static int M_GetVideoFrame(M_STATE *is, AVFrame *frame) { int got_picture; if ((got_picture = M_DecoderDecodeFrame(&is->viddec, frame)) < 0) { return -1; } if (got_picture) { double dpts = NAN; if (frame->pts != AV_NOPTS_VALUE) { dpts = av_q2d(is->video_st->time_base) * frame->pts; } frame->sample_aspect_ratio = av_guess_sample_aspect_ratio(is->ic, is->video_st, frame); if (M_GetMasterSyncType(is) != AV_SYNC_VIDEO_MASTER) { if (frame->pts != AV_NOPTS_VALUE) { double diff = dpts - M_GetMasterClock(is); if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD && diff < 0 && is->viddec.pkt_serial == is->vidclk.serial && is->videoq.nb_packets) { is->frame_drops_early++; av_frame_unref(frame); got_picture = 0; } } } } return got_picture; } static int M_AudioThread(void *arg) { M_STATE *is = arg; AVFrame *frame = av_frame_alloc(); M_FRAME *af; int last_serial = -1; int got_frame = 0; AVRational tb; int ret = 0; if (frame == nullptr) { return AVERROR(ENOMEM); } do { got_frame = M_DecoderDecodeFrame(&is->auddec, frame); if (got_frame < 0) { goto the_end; } if (got_frame) { tb = (AVRational) { 1, frame->sample_rate }; M_FRAME_DATA *fd = frame->opaque_ref ? (M_FRAME_DATA *)frame->opaque_ref->data : nullptr; af = M_FrameQueuePeekWritable(&is->sampq); if (af == nullptr) { goto the_end; } af->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb); af->pos = fd ? fd->pkt_pos : -1; af->serial = is->auddec.pkt_serial; af->duration = av_q2d((AVRational) { frame->nb_samples, frame->sample_rate }); av_frame_move_ref(af->frame, frame); M_FrameQueuePush(&is->sampq); if (is->audioq.serial != is->auddec.pkt_serial) { break; } if (ret == AVERROR_EOF) { is->auddec.finished = is->auddec.pkt_serial; } } } while (ret >= 0 || ret == AVERROR(EAGAIN) || ret == AVERROR_EOF); the_end: av_frame_free(&frame); return ret; } static int M_DecoderStart( M_DECODER *d, int (*fn)(void *), const char *thread_name, void *arg) { M_PacketQueueStart(d->queue); d->decoder_tid = SDL_CreateThread(fn, thread_name, arg); if (!d->decoder_tid) { LOG_ERROR("SDL_CreateThread(): %s", SDL_GetError()); return AVERROR(ENOMEM); } return 0; } static int M_VideoThread(void *arg) { M_STATE *is = arg; AVFrame *frame = av_frame_alloc(); double pts; double duration; int ret; AVRational tb = is->video_st->time_base; AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, nullptr); if (!frame) { return AVERROR(ENOMEM); } while (1) { ret = M_GetVideoFrame(is, frame); if (ret < 0) { goto the_end; } if (!ret) { continue; } is->frame_last_returned_time = av_gettime_relative() / 1000000.0; M_FRAME_DATA *fd = frame->opaque_ref ? (M_FRAME_DATA *)frame->opaque_ref->data : nullptr; duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational) { frame_rate.den, frame_rate.num }) : 0); pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb); ret = M_QueuePicture( is, frame, pts, duration, fd ? fd->pkt_pos : -1, is->viddec.pkt_serial); av_frame_unref(frame); if (is->videoq.serial != is->viddec.pkt_serial) { break; } } the_end: av_frame_free(&frame); return 0; } static int M_SynchronizeAudio(M_STATE *is, int nb_samples) { int wanted_nb_samples = nb_samples; if (M_GetMasterSyncType(is) != AV_SYNC_AUDIO_MASTER) { double diff, avg_diff; int min_nb_samples, max_nb_samples; diff = M_GetClock(&is->audclk) - M_GetMasterClock(is); if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD) { is->audio_diff_cum = diff + is->audio_diff_avg_coef * is->audio_diff_cum; if (is->audio_diff_avg_count < AUDIO_DIFF_AVG_NB) { is->audio_diff_avg_count++; } else { avg_diff = is->audio_diff_cum * (1.0 - is->audio_diff_avg_coef); if (fabs(avg_diff) >= is->audio_diff_threshold) { wanted_nb_samples = nb_samples + (int)(diff * is->audio_src.freq); min_nb_samples = ((nb_samples * (100 - SAMPLE_CORRECTION_PERCENT_MAX) / 100)); max_nb_samples = ((nb_samples * (100 + SAMPLE_CORRECTION_PERCENT_MAX) / 100)); wanted_nb_samples = av_clip( wanted_nb_samples, min_nb_samples, max_nb_samples); } } } else { is->audio_diff_avg_count = 0; is->audio_diff_cum = 0; } } return wanted_nb_samples; } static int M_AudioDecodeFrame(M_STATE *is) { int data_size, resampled_data_size; av_unused double audio_clock0; int wanted_nb_samples; M_FRAME *af; if (is->paused) { return -1; } do { #if defined(_WIN32) while (M_FrameQueueNBRemaining(&is->sampq) == 0) { if ((av_gettime_relative() - m_AudioCallbackTime) > 1000000LL * is->audio_hw_buf_size / is->audio_tgt.bytes_per_sec / 2) { return -1; } av_usleep(1000); } #endif if (!(af = M_FrameQueuePeekReadable(&is->sampq))) { return -1; } M_FrameQueueNext(&is->sampq); } while (af->serial != is->audioq.serial); data_size = av_samples_get_buffer_size( nullptr, af->frame->ch_layout.nb_channels, af->frame->nb_samples, af->frame->format, 1); wanted_nb_samples = M_SynchronizeAudio(is, af->frame->nb_samples); if (af->frame->format != is->audio_src.fmt || av_channel_layout_compare( &af->frame->ch_layout, &is->audio_src.ch_layout) || af->frame->sample_rate != is->audio_src.freq || (wanted_nb_samples != af->frame->nb_samples && !is->swr_ctx)) { int ret; swr_free(&is->swr_ctx); ret = swr_alloc_set_opts2( &is->swr_ctx, &is->audio_tgt.ch_layout, is->audio_tgt.fmt, is->audio_tgt.freq, &af->frame->ch_layout, af->frame->format, af->frame->sample_rate, 0, nullptr); if (ret < 0 || swr_init(is->swr_ctx) < 0) { LOG_ERROR( "Cannot create sample rate converter for conversion of %d Hz " "%s %d channels to %d Hz %s %d channels!", af->frame->sample_rate, av_get_sample_fmt_name(af->frame->format), af->frame->ch_layout.nb_channels, is->audio_tgt.freq, av_get_sample_fmt_name(is->audio_tgt.fmt), is->audio_tgt.ch_layout.nb_channels); swr_free(&is->swr_ctx); return -1; } if (av_channel_layout_copy( &is->audio_src.ch_layout, &af->frame->ch_layout) < 0) { return -1; } is->audio_src.freq = af->frame->sample_rate; is->audio_src.fmt = af->frame->format; } if (is->swr_ctx) { const uint8_t **in = (const uint8_t **)af->frame->extended_data; uint8_t **out = &is->audio_buf1; int out_count = (int64_t)wanted_nb_samples * is->audio_tgt.freq / af->frame->sample_rate + 256; int out_size = av_samples_get_buffer_size( nullptr, is->audio_tgt.ch_layout.nb_channels, out_count, is->audio_tgt.fmt, 0); int len2; if (out_size < 0) { LOG_ERROR("av_samples_get_buffer_size() failed"); return -1; } if (wanted_nb_samples != af->frame->nb_samples) { if (swr_set_compensation( is->swr_ctx, (wanted_nb_samples - af->frame->nb_samples) * is->audio_tgt.freq / af->frame->sample_rate, wanted_nb_samples * is->audio_tgt.freq / af->frame->sample_rate) < 0) { LOG_ERROR("swr_set_compensation() failed"); return -1; } } av_fast_malloc(&is->audio_buf1, &is->audio_buf1_size, out_size); if (!is->audio_buf1) { return AVERROR(ENOMEM); } len2 = swr_convert(is->swr_ctx, out, out_count, in, af->frame->nb_samples); if (len2 < 0) { LOG_ERROR("swr_convert() failed"); return -1; } if (len2 == out_count) { LOG_ERROR("audio buffer is probably too small"); if (swr_init(is->swr_ctx) < 0) { swr_free(&is->swr_ctx); } } is->audio_buf = is->audio_buf1; resampled_data_size = len2 * is->audio_tgt.ch_layout.nb_channels * av_get_bytes_per_sample(is->audio_tgt.fmt); } else { is->audio_buf = af->frame->data[0]; resampled_data_size = data_size; } audio_clock0 = is->audio_clock; if (!isnan(af->pts)) { is->audio_clock = af->pts + (double)af->frame->nb_samples / af->frame->sample_rate; } else { is->audio_clock = NAN; } is->audio_clock_serial = af->serial; return resampled_data_size; } static void M_SDLAudioCallback(void *opaque, Uint8 *stream, int len) { M_STATE *is = opaque; int audio_size, len1; m_AudioCallbackTime = av_gettime_relative(); while (len > 0) { if (is->audio_buf_index >= (signed)is->audio_buf_size) { audio_size = M_AudioDecodeFrame(is); if (audio_size < 0) { is->audio_buf = nullptr; is->audio_buf_size = SDL_AUDIO_MIN_BUFFER_SIZE / is->audio_tgt.frame_size * is->audio_tgt.frame_size; } else { is->audio_buf_size = audio_size; } is->audio_buf_index = 0; } len1 = is->audio_buf_size - is->audio_buf_index; if (len1 > len) { len1 = len; } if (is->audio_buf && is->audio_volume == SDL_MIX_MAXVOLUME) { memcpy( stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1); } else { memset(stream, 0, len1); if (is->audio_buf) { SDL_MixAudioFormat( stream, (uint8_t *)is->audio_buf + is->audio_buf_index, AUDIO_S16SYS, len1, is->audio_volume); } } len -= len1; stream += len1; is->audio_buf_index += len1; } is->audio_write_buf_size = is->audio_buf_size - is->audio_buf_index; if (!isnan(is->audio_clock)) { M_SetClockAt( &is->audclk, is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec, is->audio_clock_serial, m_AudioCallbackTime / 1000000.0); M_SyncClockToSlave(&is->extclk, &is->audclk); } } static int M_AudioOpen( M_STATE *is, AVChannelLayout *wanted_channel_layout, int wanted_sample_rate, M_AUDIO_PARAMS *audio_hw_params) { SDL_AudioSpec wanted_spec, spec; const char *env; static const int next_nb_channels[] = { 0, 0, 1, 6, 2, 6, 4, 6 }; static const int next_sample_rates[] = { 0, 44100, 48000, 96000, 192000 }; int next_sample_rate_idx = FF_ARRAY_ELEMS(next_sample_rates) - 1; int wanted_nb_channels = wanted_channel_layout->nb_channels; env = SDL_getenv("SDL_AUDIO_CHANNELS"); if (env) { wanted_nb_channels = atoi(env); av_channel_layout_uninit(wanted_channel_layout); av_channel_layout_default(wanted_channel_layout, wanted_nb_channels); } if (wanted_channel_layout->order != AV_CHANNEL_ORDER_NATIVE) { av_channel_layout_uninit(wanted_channel_layout); av_channel_layout_default(wanted_channel_layout, wanted_nb_channels); } wanted_nb_channels = wanted_channel_layout->nb_channels; wanted_spec.channels = wanted_nb_channels; wanted_spec.freq = wanted_sample_rate; if (wanted_spec.freq <= 0 || wanted_spec.channels <= 0) { LOG_ERROR("Invalid sample rate or channel count!"); return -1; } while (next_sample_rate_idx && next_sample_rates[next_sample_rate_idx] >= wanted_spec.freq) { next_sample_rate_idx--; } wanted_spec.format = AUDIO_S16SYS; wanted_spec.silence = 0; wanted_spec.samples = FFMAX( SDL_AUDIO_MIN_BUFFER_SIZE, 2 << av_log2(wanted_spec.freq / SDL_AUDIO_MAX_CALLBACKS_PER_SEC)); wanted_spec.callback = M_SDLAudioCallback; wanted_spec.userdata = is; while ( !(m_AudioDevice = SDL_OpenAudioDevice( nullptr, 0, &wanted_spec, &spec, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE | SDL_AUDIO_ALLOW_CHANNELS_CHANGE))) { LOG_WARNING( "SDL_OpenAudio (%d channels, %d Hz): %s", wanted_spec.channels, wanted_spec.freq, SDL_GetError()); wanted_spec.channels = next_nb_channels[FFMIN(7, wanted_spec.channels)]; if (!wanted_spec.channels) { wanted_spec.freq = next_sample_rates[next_sample_rate_idx--]; wanted_spec.channels = wanted_nb_channels; if (!wanted_spec.freq) { LOG_ERROR("No more combinations to try, audio open failed"); return -1; } } av_channel_layout_default(wanted_channel_layout, wanted_spec.channels); } if (spec.format != AUDIO_S16SYS) { LOG_ERROR("SDL advised audio format %d is not supported!", spec.format); return -1; } if (spec.channels != wanted_spec.channels) { av_channel_layout_uninit(wanted_channel_layout); av_channel_layout_default(wanted_channel_layout, spec.channels); if (wanted_channel_layout->order != AV_CHANNEL_ORDER_NATIVE) { LOG_ERROR( "SDL advised channel count %d is not supported!", spec.channels); return -1; } } audio_hw_params->fmt = AV_SAMPLE_FMT_S16; audio_hw_params->freq = spec.freq; if (av_channel_layout_copy( &audio_hw_params->ch_layout, wanted_channel_layout) < 0) { return -1; } audio_hw_params->frame_size = av_samples_get_buffer_size( nullptr, audio_hw_params->ch_layout.nb_channels, 1, audio_hw_params->fmt, 1); audio_hw_params->bytes_per_sec = av_samples_get_buffer_size( nullptr, audio_hw_params->ch_layout.nb_channels, audio_hw_params->freq, audio_hw_params->fmt, 1); if (audio_hw_params->bytes_per_sec <= 0 || audio_hw_params->frame_size <= 0) { LOG_ERROR("av_samples_get_buffer_size failed"); return -1; } return spec.size; } static int M_StreamComponentOpen(M_STATE *is, int stream_index) { AVFormatContext *ic = is->ic; AVCodecContext *avctx = nullptr; const AVCodec *codec = nullptr; const char *forced_codec_name = nullptr; AVDictionary *opts = nullptr; const AVDictionaryEntry *t = nullptr; int sample_rate; int nb_channels; AVChannelLayout ch_layout = {}; bool has_ch_layout = false; int ret = 0; if (stream_index < 0 || stream_index >= (signed)ic->nb_streams) { return -1; } avctx = avcodec_alloc_context3(nullptr); if (!avctx) { return AVERROR(ENOMEM); } ret = avcodec_parameters_to_context( avctx, ic->streams[stream_index]->codecpar); if (ret < 0) { goto fail; } avctx->pkt_timebase = ic->streams[stream_index]->time_base; codec = avcodec_find_decoder(avctx->codec_id); if (!codec) { LOG_ERROR( "No decoder could be found for codec %s", avcodec_get_name(avctx->codec_id)); ret = AVERROR(EINVAL); goto fail; } avctx->codec_id = codec->id; avctx->lowres = 0; if ((ret = avcodec_open2(avctx, codec, nullptr)) < 0) { goto fail; } is->eof = false; ic->streams[stream_index]->discard = AVDISCARD_DEFAULT; switch (avctx->codec_type) { case AVMEDIA_TYPE_AUDIO: sample_rate = avctx->sample_rate; ret = av_channel_layout_copy(&ch_layout, &avctx->ch_layout); if (ret < 0) { goto fail; } has_ch_layout = true; if ((ret = M_AudioOpen(is, &ch_layout, sample_rate, &is->audio_tgt)) < 0) { goto fail; } is->audio_hw_buf_size = ret; is->audio_src = is->audio_tgt; is->audio_buf_size = 0; is->audio_buf_index = 0; is->audio_diff_avg_coef = exp(log(0.01) / AUDIO_DIFF_AVG_NB); is->audio_diff_avg_count = 0; is->audio_diff_threshold = (double)(is->audio_hw_buf_size) / is->audio_tgt.bytes_per_sec; is->audio_stream = stream_index; is->audio_st = ic->streams[stream_index]; if ((ret = M_DecoderInit( &is->auddec, avctx, &is->audioq, is->continue_read_thread)) < 0) { goto fail; } if (is->ic->iformat->flags & AVFMT_NOTIMESTAMPS) { is->auddec.start_pts = is->audio_st->start_time; is->auddec.start_pts_tb = is->audio_st->time_base; } if (M_DecoderStart(&is->auddec, M_AudioThread, "audio_decoder", is) < 0) { LOG_ERROR("Error starting audio decoder"); goto fail; } SDL_PauseAudioDevice(m_AudioDevice, 0); break; case AVMEDIA_TYPE_VIDEO: is->video_stream = stream_index; is->video_st = ic->streams[stream_index]; if ((ret = M_DecoderInit( &is->viddec, avctx, &is->videoq, is->continue_read_thread)) < 0) { goto fail; } is->queue_attachments_req = true; if ((M_DecoderStart(&is->viddec, M_VideoThread, "video_decoder", is)) < 0) { LOG_ERROR("Error starting video decoder"); goto fail; } break; default: break; } goto out; fail: avcodec_free_context(&avctx); out: if (has_ch_layout) { av_channel_layout_uninit(&ch_layout); } return ret; } static int M_DecodeInterruptCB(void *ctx) { M_STATE *is = ctx; return is->abort_request; } static int M_StreamHasEnoughPackets( AVStream *st, int stream_id, M_PACKET_QUEUE *queue) { return stream_id < 0 || queue->abort_request || (st->disposition & AV_DISPOSITION_ATTACHED_PIC) || (queue->nb_packets > MIN_FRAMES && (!queue->duration || av_q2d(st->time_base) * queue->duration > 1.0)); } static int M_ReadThread(void *arg) { M_STATE *is = arg; AVFormatContext *ic = nullptr; int err; int ret; int st_index[AVMEDIA_TYPE_NB]; AVPacket *pkt = nullptr; SDL_mutex *wait_mutex = SDL_CreateMutex(); int64_t pkt_ts; if (!wait_mutex) { LOG_ERROR("SDL_CreateMutex(): %s", SDL_GetError()); ret = AVERROR(ENOMEM); goto fail; } memset(st_index, -1, sizeof(st_index)); is->eof = false; pkt = av_packet_alloc(); if (!pkt) { LOG_ERROR("Could not allocate packet."); ret = AVERROR(ENOMEM); goto fail; } ic = avformat_alloc_context(); if (!ic) { LOG_ERROR("Could not allocate context."); ret = AVERROR(ENOMEM); goto fail; } ic->interrupt_callback.callback = M_DecodeInterruptCB; ic->interrupt_callback.opaque = is; err = avformat_open_input(&ic, is->filename, nullptr, nullptr); if (err < 0) { LOG_ERROR( "Error while opening file %s: %s", is->filename, av_err2str(err)); ret = -1; goto fail; } is->ic = ic; avformat_find_stream_info(ic, nullptr); #if LIBAVFORMAT_VERSION_MAJOR < 59 av_format_inject_global_side_data(ic); #endif if (ic->pb) { ic->pb->eof_reached = 0; } is->max_frame_duration = (ic->iformat->flags & AVFMT_TS_DISCONT) ? 10.0 : 3600.0; for (int i = 0; i < (signed)ic->nb_streams; i++) { AVStream *st = ic->streams[i]; enum AVMediaType type = st->codecpar->codec_type; st->discard = AVDISCARD_ALL; } st_index[AVMEDIA_TYPE_VIDEO] = av_find_best_stream( ic, AVMEDIA_TYPE_VIDEO, st_index[AVMEDIA_TYPE_VIDEO], -1, nullptr, 0); st_index[AVMEDIA_TYPE_AUDIO] = av_find_best_stream( ic, AVMEDIA_TYPE_AUDIO, st_index[AVMEDIA_TYPE_AUDIO], st_index[AVMEDIA_TYPE_VIDEO], nullptr, 0); if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) { AVStream *st = ic->streams[st_index[AVMEDIA_TYPE_VIDEO]]; AVCodecParameters *codecpar = st->codecpar; AVRational sar = av_guess_sample_aspect_ratio(ic, st, nullptr); } if (is->audio_enabled && st_index[AVMEDIA_TYPE_AUDIO] >= 0) { M_StreamComponentOpen(is, st_index[AVMEDIA_TYPE_AUDIO]); } ret = -1; if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) { ret = M_StreamComponentOpen(is, st_index[AVMEDIA_TYPE_VIDEO]); } if (is->video_stream < 0 && is->audio_stream < 0) { LOG_ERROR("Failed to decode file"); ret = -1; goto fail; } while (1) { if (is->abort_request) { break; } if (is->paused != is->last_paused) { is->last_paused = is->paused; if (is->paused) { is->read_pause_return = av_read_pause(ic); } else { av_read_play(ic); } } if (is->queue_attachments_req) { if (is->video_st && is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC) { if ((ret = av_packet_ref(pkt, &is->video_st->attached_pic)) < 0) { goto fail; } M_PacketQueuePut(&is->videoq, pkt); M_PacketQueuePutNullPacket(&is->videoq, pkt, is->video_stream); } is->queue_attachments_req = false; } if (is->audioq.size + is->videoq.size > MAX_QUEUE_SIZE || (M_StreamHasEnoughPackets( is->audio_st, is->audio_stream, &is->audioq) && M_StreamHasEnoughPackets( is->video_st, is->video_stream, &is->videoq))) { SDL_LockMutex(wait_mutex); SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10); SDL_UnlockMutex(wait_mutex); continue; } if (!is->paused && (!is->audio_st || (is->auddec.finished == is->audioq.serial && M_FrameQueueNBRemaining(&is->sampq) == 0)) && (!is->video_st || (is->viddec.finished == is->videoq.serial && M_FrameQueueNBRemaining(&is->pictq) == 0))) { ret = AVERROR_EOF; goto fail; } ret = av_read_frame(ic, pkt); if (ret < 0) { if ((ret == AVERROR_EOF || avio_feof(ic->pb)) && !is->eof) { if (is->video_stream >= 0) { M_PacketQueuePutNullPacket( &is->videoq, pkt, is->video_stream); } if (is->audio_stream >= 0) { M_PacketQueuePutNullPacket( &is->audioq, pkt, is->audio_stream); } is->eof = true; } if (ic->pb && ic->pb->error) { goto fail; } SDL_LockMutex(wait_mutex); SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10); SDL_UnlockMutex(wait_mutex); continue; } else { is->eof = false; } pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts; if (pkt->stream_index == is->audio_stream) { M_PacketQueuePut(&is->audioq, pkt); } else if ( pkt->stream_index == is->video_stream && !(is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC)) { M_PacketQueuePut(&is->videoq, pkt); } else { av_packet_unref(pkt); } } ret = 0; fail: if (ic && !is->ic) { avformat_close_input(&ic); } av_packet_free(&pkt); is->playback_finished = true; SDL_DestroyMutex(wait_mutex); return 0; } static M_STATE *M_StreamOpen(const char *filename) { M_STATE *const is = av_mallocz(sizeof(M_STATE)); if (is == nullptr) { return nullptr; } is->video_stream = -1; is->audio_stream = -1; is->filename = av_strdup(filename); if (is->filename == nullptr) { goto fail; } is->iformat = nullptr; if (M_FrameQueueInit(&is->pictq, &is->videoq, VIDEO_PICTURE_QUEUE_SIZE, 1) < 0) { goto fail; } if (M_FrameQueueInit(&is->sampq, &is->audioq, SAMPLE_QUEUE_SIZE, 1) < 0) { goto fail; } if (M_PacketQueueInit(&is->videoq) < 0) { goto fail; } if (M_PacketQueueInit(&is->audioq) < 0) { goto fail; } if (!(is->continue_read_thread = SDL_CreateCond())) { LOG_ERROR("SDL_CreateCond(): %s", SDL_GetError()); goto fail; } M_InitClock(&is->vidclk, &is->videoq.serial); M_InitClock(&is->audclk, &is->audioq.serial); M_InitClock(&is->extclk, &is->extclk.serial); is->audio_clock_serial = -1; is->audio_volume = SDL_MIX_MAXVOLUME; is->av_sync_type = AV_SYNC_AUDIO_MASTER; return is; fail: M_StreamClose(is); return nullptr; } VIDEO *Video_Open(const char *const file_path) { if (file_path == nullptr || String_IsEmpty(file_path)) { LOG_ERROR("Cannot open video: empty file path"); return nullptr; } LOG_DEBUG("Playing video: %s", file_path); int flags = SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER; if (SDL_Init(flags)) { LOG_ERROR("Could not initialize SDL - %s", SDL_GetError()); return nullptr; } SDL_EventState(SDL_SYSWMEVENT, SDL_IGNORE); SDL_EventState(SDL_USEREVENT, SDL_IGNORE); VIDEO *const result = Memory_Alloc(sizeof(VIDEO)); result->priv = M_StreamOpen(file_path); if (result->priv == nullptr) { Memory_Free(result); LOG_ERROR("Failed to initialize video!"); return nullptr; } result->path = Memory_DupStr(file_path); result->is_playing = true; return result; } void Video_PumpEvents(VIDEO *video) { M_STATE *const is = video->priv; if (is->remaining_time > 0.0) { av_usleep((int64_t)(is->remaining_time * 1000000.0)); } is->remaining_time = REFRESH_RATE; if (!is->paused || is->force_refresh) { M_VideoRefresh(is, &is->remaining_time); } video->is_playing = !is->abort_request && !is->playback_finished; } void Video_SetAudioEnabled(VIDEO *const video, const bool enabled) { M_STATE *const is = video->priv; is->audio_enabled = enabled; } void Video_SetVolume(VIDEO *const video, const double volume) { M_STATE *const is = video->priv; is->audio_volume = volume * SDL_MIX_MAXVOLUME; } void Video_Start(VIDEO *const video) { M_STATE *const is = video->priv; is->remaining_time = 0.0; is->read_tid = SDL_CreateThread(M_ReadThread, "read_thread", is); if (is->read_tid == nullptr) { LOG_ERROR("Error starting read thread: %s", SDL_GetError()); } } void Video_Stop(VIDEO *const video) { M_STATE *const is = video->priv; is->abort_request = true; } void Video_Close(VIDEO *const video) { M_STATE *const is = video->priv; if (is) { M_StreamClose(is); } LOG_DEBUG("Finished playing video: %s", video->path); Memory_Free((char *)video->path); Memory_Free(video); } void Video_SetSurfaceSize( VIDEO *const video, const int32_t surface_width, const int32_t surface_height) { M_STATE *const is = video->priv; if (is->surface_width == surface_width && is->surface_height == surface_height) { return; } M_ReallocPrimarySurface(is, surface_width, surface_height, false); } void Video_SetSurfacePixelFormat(VIDEO *video, enum AVPixelFormat pixel_format) { M_STATE *const is = video->priv; if (is->primary_surface_pixel_format == pixel_format) { return; } is->primary_surface_pixel_format = pixel_format; M_ReallocPrimarySurface(is, is->surface_width, is->surface_height, false); } void Video_SetSurfaceStride(VIDEO *video, const int32_t stride) { M_STATE *const is = video->priv; if (is->primary_surface_stride == stride) { return; } is->primary_surface_stride = stride; M_ReallocPrimarySurface(is, is->surface_width, is->surface_height, false); } void Video_SetSurfaceAllocatorFunc( VIDEO *const video, const VIDEO_SURFACE_ALLOCATOR_FUNC func, void *const user_data) { M_STATE *const is = video->priv; is->surface_allocator_func = func; is->surface_allocator_func_user_data = user_data; } void Video_SetSurfaceDeallocatorFunc( VIDEO *const video, void (*func)(void *surface, void *user_data), void *const user_data) { M_STATE *const is = video->priv; is->surface_deallocator_func = func; is->surface_deallocator_func_user_data = user_data; } void Video_SetSurfaceClearFunc( VIDEO *const video, void (*func)(void *surface, void *user_data), void *const user_data) { M_STATE *const is = video->priv; is->surface_clear_func = func; is->surface_clear_func_user_data = user_data; } void Video_SetSurfaceLockFunc( VIDEO *const video, void *(*func)(void *surface, void *user_data), void *const user_data) { M_STATE *const is = video->priv; is->surface_lock_func = func; is->surface_lock_func_user_data = user_data; } void Video_SetSurfaceUnlockFunc( VIDEO *const video, void (*func)(void *surface, void *user_data), void *const user_data) { M_STATE *const is = video->priv; is->surface_unlock_func = func; is->surface_unlock_func_user_data = user_data; } void Video_SetSurfaceUploadFunc( VIDEO *const video, void (*func)(void *surface, void *user_data), void *const user_data) { M_STATE *const is = video->priv; is->surface_upload_func = func; is->surface_upload_func_user_data = user_data; } void Video_SetRenderBeginFunc( VIDEO *const video, void (*func)(void *surface, void *user_data), void *const user_data) { M_STATE *const is = video->priv; is->render_begin_func = func; is->render_begin_func_user_data = user_data; } void Video_SetRenderEndFunc( VIDEO *const video, void (*func)(void *surface, void *user_data), void *const user_data) { M_STATE *const is = video->priv; is->render_end_func = func; is->render_end_func_user_data = user_data; } void Video_SetExternalAudioClock(VIDEO *const video, const double timestamp) { M_STATE *const is = video->priv; M_SetClock(&is->extclk, timestamp, is->extclk.serial); } void Video_SetPaused(VIDEO *const video, const bool paused) { M_STATE *const is = video->priv; is->paused = paused; if (!paused) { is->force_refresh = true; } } ================================================ FILE: src/trx/av/video.h ================================================ #pragma once #include #include typedef struct { const char *path; bool is_playing; void *priv; } VIDEO; typedef void *(*VIDEO_SURFACE_ALLOCATOR_FUNC)( int32_t width, int32_t height, void *user_data); VIDEO *Video_Open(const char *path); void Video_SetAudioEnabled(VIDEO *video, bool enabled); void Video_SetVolume(VIDEO *video, double volume); void Video_SetSurfaceSize(VIDEO *video, int32_t width, int32_t height); void Video_SetSurfacePixelFormat(VIDEO *video, enum AVPixelFormat pixel_format); void Video_SetSurfaceStride(VIDEO *video, int32_t stride); void Video_SetSurfaceAllocatorFunc( VIDEO *video, VIDEO_SURFACE_ALLOCATOR_FUNC func, void *user_data); void Video_SetSurfaceDeallocatorFunc( VIDEO *video, void (*func)(void *surface, void *user_data), void *user_data); void Video_SetSurfaceClearFunc( VIDEO *video, void (*func)(void *surface, void *user_data), void *user_data); void Video_SetSurfaceLockFunc( VIDEO *video, void *(*func)(void *surface, void *user_data), void *user_data); void Video_SetSurfaceUnlockFunc( VIDEO *video, void (*func)(void *surface, void *user_data), void *user_data); void Video_SetSurfaceUploadFunc( VIDEO *video, void (*func)(void *surface, void *user_data), void *user_data); void Video_SetRenderBeginFunc( VIDEO *video, void (*func)(void *surface, void *user_data), void *user_data); void Video_SetRenderEndFunc( VIDEO *video, void (*func)(void *surface, void *user_data), void *user_data); void Video_SetExternalAudioClock(VIDEO *video, double timestamp); void Video_SetPaused(VIDEO *video, bool paused); void Video_Start(VIDEO *video); void Video_Stop(VIDEO *video); void Video_PumpEvents(VIDEO *video); void Video_Close(VIDEO *video); ================================================ FILE: src/trx/config/common.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include // In-memory list of pointers to config options enforced by the game flow. static VECTOR *m_EnforcedOptions = nullptr; // In-memory list of pointers to config options hidden by the game flow. static VECTOR *m_HiddenOptions = nullptr; static EVENT_MANAGER *m_EventManager = nullptr; static void M_FreeStringOptionValues(void) { const CONFIG_OPTION *option = Config_GetOptionMap(); while (option != nullptr && option->target != nullptr) { if (option->type == COT_STRING || option->type == COT_DYNAMIC_ENUM) { Memory_Free(*(char **)option->target); } option++; } } __attribute__((constructor)) static void M_Init(void) { m_EventManager = EventManager_Create(); } __attribute__((destructor)) static void M_Shutdown(void) { EventManager_Free(m_EventManager); m_EventManager = nullptr; M_FreeStringOptionValues(); Memory_FreePointer(&g_Config.default_path); Memory_FreePointer(&g_Config.enforced_path); if (m_EnforcedOptions != nullptr) { Vector_Free(m_EnforcedOptions); m_EnforcedOptions = nullptr; } if (m_HiddenOptions != nullptr) { Vector_Free(m_HiddenOptions); m_HiddenOptions = nullptr; } } void Config_ApplyDefaultSettings(void) { const CONFIG_OPTION *option = Config_GetOptionMap(); while (option->target != nullptr) { Config_RestoreOptionDefault(option->target); option++; } } bool Config_Read( const char *const default_path, const char *const enforced_path) { // Always initialize the config, even if the file is missing, so that // the game can interact with these properties. Memory_FreePointer(&g_Config.default_path); Memory_FreePointer(&g_Config.enforced_path); g_Config.default_path = Memory_DupStr(default_path); g_Config.enforced_path = Memory_DupStr(enforced_path); g_Config.loaded = true; LOG_DEBUG("Reading config"); LOG_DEBUG(" default_path=%s", g_Config.default_path); LOG_DEBUG(" enforced_path=%s", g_Config.enforced_path); if (m_EnforcedOptions == nullptr) { m_EnforcedOptions = Vector_Create(sizeof(void *)); } else { Vector_ClearRealloc(m_EnforcedOptions); } if (m_HiddenOptions == nullptr) { m_HiddenOptions = Vector_Create(sizeof(void *)); } else { Vector_ClearRealloc(m_HiddenOptions); } const CONFIG_IO_ARGS args = { .default_path = g_Config.default_path, .enforced_path = g_Config.enforced_path, .action = &Config_LoadFromJSON, .enforced_targets = m_EnforcedOptions, .hidden_targets = m_HiddenOptions, }; const bool result = ConfigFile_Read(&args); if (result) { LOG_DEBUG("Config loaded"); } else { LOG_WARNING("Errors while loading config"); } Config_Sanitize(); g_SavedConfig = g_Config; return result; } bool Config_Update(void) { Config_Sanitize(); if (memcmp(&g_Config, &g_SavedConfig, sizeof(CONFIG)) == 0) { return false; } if (m_EventManager != nullptr) { const EVENT event = { .name = "change", .sender = nullptr, .data = nullptr, }; EventManager_Fire(m_EventManager, &event); } g_Config.dirty = false; g_SavedConfig = g_Config; return true; } bool Config_Write(void) { ASSERT(g_Config.default_path != nullptr); const CONFIG_IO_ARGS args = { .default_path = g_Config.default_path, .enforced_path = g_Config.enforced_path, .action = &Config_DumpToJSON, }; return ConfigFile_Write(&args); } int32_t Config_SubscribeChanges( const EVENT_LISTENER listener, void *const user_data) { ASSERT(m_EventManager != nullptr); return EventManager_Subscribe( m_EventManager, "change", nullptr, listener, user_data); } void Config_UnsubscribeChanges(const int32_t listener_id) { ASSERT(m_EventManager != nullptr); EventManager_Unsubscribe(m_EventManager, listener_id); } const CONFIG_OPTION *Config_GetOption(const void *const target) { const CONFIG_OPTION *option = Config_GetOptionMap(); if (option == nullptr) { return nullptr; } while (option->target != nullptr) { if (option->target == target) { return option; } option++; } return nullptr; } bool Config_IsOptionEnforced(const void *const target) { return m_EnforcedOptions != nullptr && Vector_Contains(m_EnforcedOptions, &target); } bool Config_IsOptionHidden(const void *const target) { return m_HiddenOptions != nullptr && Vector_Contains(m_HiddenOptions, &target); } bool Config_IsOptionAtDefault(const void *const target) { const CONFIG_OPTION *option = Config_GetOption(target); if (target == nullptr) { return true; } switch (option->type) { case COT_BOOL: return *(bool *)option->target == *(bool *)option->default_value; case COT_INT32: return *(int32_t *)option->target == *(int32_t *)option->default_value; case COT_FLOAT: case COT_FLOAT_PERCENT: return *(float *)option->target == *(float *)option->default_value; case COT_DOUBLE: return *(double *)option->target == *(double *)option->default_value; case COT_RGB888: { const RGB_888 cur = *(RGB_888 *)option->target; const RGB_888 def = *(RGB_888 *)option->default_value; return cur.r == def.r && cur.g == def.g && cur.b == def.b; } case COT_ENUM: return *(int32_t *)option->target == *(int32_t *)option->default_value; break; case COT_STRING: case COT_DYNAMIC_ENUM: { const char *const cur = *(char **)option->target; const char *const def = (const char *)option->default_value; if (cur == nullptr && def == nullptr) { return true; } if (cur == nullptr || def == nullptr) { return false; } return strcmp(cur, def) == 0; } } return true; } bool Config_RestoreOptionDefault(const void *const target) { const CONFIG_OPTION *option = Config_GetOption(target); if (target == nullptr) { return false; } switch (option->type) { case COT_BOOL: *(bool *)option->target = *(bool *)option->default_value; return true; case COT_INT32: *(int32_t *)option->target = *(int32_t *)option->default_value; return true; case COT_FLOAT: case COT_FLOAT_PERCENT: *(float *)option->target = *(float *)option->default_value; return true; case COT_DOUBLE: *(double *)option->target = *(double *)option->default_value; return true; case COT_RGB888: *(RGB_888 *)option->target = *(RGB_888 *)option->default_value; return true; case COT_ENUM: *(int32_t *)option->target = *(int32_t *)option->default_value; return true; case COT_STRING: case COT_DYNAMIC_ENUM: { char **const p = (char **)option->target; const char *const def = (const char *)option->default_value; char *const old = *p; *p = def != nullptr ? Memory_DupStr(def) : nullptr; // VERY IMPORTANT: free the memory AFTER we allocate, so that we force // the pointer to get a different macro, so that change subscribers // can see the string has changed by comparing just the pointers. Memory_Free(old); return true; } } return false; } static bool M_ParseBool(const char *const value, bool *const result) { if (String_Match(value, "^(on|true|1)$")) { *result = true; return true; } if (String_Match(value, "^(off|false|0)$")) { *result = false; return true; } return false; } static bool M_ParseInt32(const char *const value, int32_t *const result) { return sscanf(value, "%d", result) == 1; } static bool M_ParseFloat(const char *const value, float *const result) { return sscanf(value, "%f", result) == 1; } static bool M_ParseDouble(const char *const value, double *const result) { return sscanf(value, "%lf", result) == 1; } static bool M_ParseEnum( const CONFIG_OPTION *const option, const char *const value, const bool allow_numeric, int32_t *const result) { const int32_t mapped = EnumMap_Get(option->param, value, -1); if (mapped != -1) { *result = mapped; return true; } if (allow_numeric) { return M_ParseInt32(value, result); } return false; } static bool M_ParseRGB888(const char *const value, RGB_888 *const result) { return String_ParseRGB888(value, result); } static const char *M_FormatBool(const bool value) { return String_FormatStatic("%d", value); } static const char *M_FormatBoolHuman(const bool value) { return value ? GS("general/misc/on") : GS("general/misc/off"); } static const char *M_FormatInt32(const int32_t value) { return String_FormatStatic("%d", value); } static const char *M_FormatFloat(const float value) { return String_FormatStatic("%.2f", value); } static const char *M_FormatFloatPercent(const float value) { return String_FormatStatic("%.0f%%", value); } static const char *M_FormatDouble(const double value) { return String_FormatStatic("%.2f", value); } static const char *M_FormatEnumMachine( const CONFIG_OPTION *const option, const int32_t value) { return String_FormatStatic("%s", EnumMap_ToString(option->param, value)); } static const char *M_FormatEnumHuman( const CONFIG_OPTION *const option, const int32_t value) { const char *const localized = EnumMap_GetLabel(option->param, value); ASSERT(localized != nullptr); return localized; } static const char *M_FormatRGB888(const RGB_888 *const value) { return String_FormatStatic( "%02hhx%02hhx%02hhx", value->r, value->g, value->b); } static const char *M_FormatString(const char *const value) { return String_FormatStatic("%s", value != nullptr ? value : ""); } const char *Config_GetOptionValueAsString( const CONFIG_OPTION *const option, const bool human_readable) { if (option == nullptr) { return nullptr; } switch (option->type) { case COT_BOOL: return human_readable ? M_FormatBoolHuman(*(bool *)option->target) : M_FormatBool(*(bool *)option->target); case COT_INT32: return M_FormatInt32(*(int32_t *)option->target); case COT_FLOAT: return M_FormatFloat(*(float *)option->target); case COT_FLOAT_PERCENT: return M_FormatFloatPercent((*(float *)option->target) * 100.0f); case COT_DOUBLE: return M_FormatDouble(*(double *)option->target); case COT_ENUM: return human_readable ? M_FormatEnumHuman(option, *(int32_t *)option->target) : M_FormatEnumMachine(option, *(int32_t *)option->target); case COT_RGB888: return M_FormatRGB888(option->target); case COT_STRING: return M_FormatString(*(char **)option->target); case COT_DYNAMIC_ENUM: { if (human_readable) { const char *const value = *(char **)option->target; const char *const label = Config_DynamicEnum_GetLabelForValue(option, value); if (label != nullptr) { return label; } } return M_FormatString(*(char **)option->target); } default: return nullptr; } } const char *Config_GetOptionTitle(const CONFIG_OPTION *const opt) { if (opt == nullptr || opt->name == nullptr) { return nullptr; } return GameString_Get(String_FormatStatic("settings/%s/title", opt->name)); } const char *Config_GetOptionDescription(const CONFIG_OPTION *const opt) { if (opt == nullptr || opt->name == nullptr) { return nullptr; } return GameString_Get( String_FormatStatic("settings/%s/description", opt->name)); } char *Config_NormalizeOptionValueString( const CONFIG_OPTION *const option, const char *const value, const bool human_readable) { if (option == nullptr) { return Memory_DupStr(value != nullptr ? value : ""); } const char *const input = value != nullptr ? value : ""; #define L_NORMALIZE_TYPED(type_, parse_expr_, format_expr_) \ do { \ type_ parsed; \ if (!(parse_expr_)) { \ return Memory_DupStr(input); \ } \ return Memory_DupStr(format_expr_); \ } while (false) switch (option->type) { case COT_BOOL: L_NORMALIZE_TYPED( bool, M_ParseBool(input, &parsed), human_readable ? M_FormatBoolHuman(parsed) : M_FormatBool(parsed)); case COT_INT32: L_NORMALIZE_TYPED( int32_t, M_ParseInt32(input, &parsed), M_FormatInt32(parsed)); case COT_FLOAT: L_NORMALIZE_TYPED( float, M_ParseFloat(input, &parsed), M_FormatFloat(parsed)); case COT_FLOAT_PERCENT: L_NORMALIZE_TYPED( float, M_ParseFloat(input, &parsed), M_FormatFloatPercent(parsed)); case COT_DOUBLE: L_NORMALIZE_TYPED( double, M_ParseDouble(input, &parsed), M_FormatDouble(parsed)); case COT_ENUM: L_NORMALIZE_TYPED( int32_t, M_ParseEnum(option, input, true, &parsed), human_readable ? M_FormatEnumHuman(option, parsed) : M_FormatEnumMachine(option, parsed)); case COT_RGB888: L_NORMALIZE_TYPED( RGB_888, M_ParseRGB888(input, &parsed), M_FormatRGB888(&parsed)); case COT_STRING: return Memory_DupStr(M_FormatString(input)); case COT_DYNAMIC_ENUM: if (!Config_DynamicEnum_IsValidValue(option, input)) { return Memory_DupStr(input); } if (human_readable) { const char *const label = Config_DynamicEnum_GetLabelForValue(option, input); if (label != nullptr) { return Memory_DupStr(label); } } return Memory_DupStr(M_FormatString(input)); } #undef L_NORMALIZE_TYPED return Memory_DupStr(input); } bool Config_SetOptionValueFromString( const CONFIG_OPTION *const option, const char *const new_value) { ASSERT(option != nullptr); ASSERT(option->target != nullptr); switch (option->type) { case COT_BOOL: { bool parsed; if (M_ParseBool(new_value, &parsed)) { *(bool *)option->target = parsed; return true; } break; } case COT_INT32: { int32_t parsed; if (M_ParseInt32(new_value, &parsed)) { *(int32_t *)option->target = parsed; return true; } break; } case COT_FLOAT: { float parsed; if (M_ParseFloat(new_value, &parsed)) { *(float *)option->target = parsed; return true; } break; } case COT_FLOAT_PERCENT: { float parsed; if (M_ParseFloat(new_value, &parsed)) { *(float *)option->target = parsed / 100.0f; return true; } break; } case COT_DOUBLE: { double parsed; if (M_ParseDouble(new_value, &parsed)) { *(double *)option->target = parsed; return true; } break; } case COT_ENUM: { int32_t parsed; if (M_ParseEnum(option, new_value, false, &parsed)) { *(int32_t *)option->target = parsed; return true; } break; } case COT_RGB888: { RGB_888 parsed; if (M_ParseRGB888(new_value, &parsed)) { *(RGB_888 *)option->target = parsed; return true; } break; } case COT_STRING: case COT_DYNAMIC_ENUM: { if (option->type == COT_DYNAMIC_ENUM && !Config_DynamicEnum_IsValidValue(option, new_value)) { return false; } char **const p = (char **)option->target; char *const old = *p; *p = new_value != nullptr ? Memory_DupStr(new_value) : nullptr; // VERY IMPORTANT: free the memory AFTER we allocate, so that we force // the pointer to get a different macro, so that change subscribers // can see the string has changed by comparing just the pointers. Memory_Free(old); return true; } } return false; } ================================================ FILE: src/trx/config/common.h ================================================ #pragma once #include #include #include #include void Config_ApplyDefaultSettings(void); bool Config_Read(const char *default_path, const char *enforced_path); bool Config_Write(void); bool Config_Update(void); const CONFIG_OPTION *Config_GetOptionMap(void); int32_t Config_SubscribeChanges(EVENT_LISTENER listener, void *user_data); void Config_UnsubscribeChanges(int32_t listener_id); // Retrieves CONFIG_OPTION related to the target setting (a pointer into a // g_Config property). const CONFIG_OPTION *Config_GetOption(const void *target); // Returns true if a given setting was enforced by the game flow file. bool Config_IsOptionEnforced(const void *target); // Returns true if a given setting should be hidden in settings dialogs. bool Config_IsOptionHidden(const void *target); // Returns whether the given setting's current value is the same as its default. bool Config_IsOptionAtDefault(const void *target); // Restores the given setting's default value. bool Config_RestoreOptionDefault(const void *target); // Get a flat string name of an option. const char *Config_ResolveOptionName(const char *option_name); // Retrieve an option given a string path. const CONFIG_OPTION *Config_GetOptionByPath(const char *path); // Returns translated title for a config option. const char *Config_GetOptionTitle(const CONFIG_OPTION *opt); // Returns translated description for a config option. const char *Config_GetOptionDescription(const CONFIG_OPTION *opt); // Formats the current value of a config option as a static string. // The string must not be freed and is short lived. const char *Config_GetOptionValueAsString( const CONFIG_OPTION *option, bool human_readable); // Normalizes an option value string using the same parser/formatter as config // runtime values. // Returns an allocated string that must be freed by the caller. char *Config_NormalizeOptionValueString( const CONFIG_OPTION *option, const char *value, bool human_readable); // Updates the given setting's value from string. bool Config_SetOptionValueFromString( const CONFIG_OPTION *option, const char *new_value); ================================================ FILE: src/trx/config/const.h ================================================ #pragma once #define MAX_ASSAULT_TIMES 10 #define CONFIG_MIN_BRIGHTNESS 0.1f #define CONFIG_MAX_BRIGHTNESS 2.0f #define CONFIG_MIN_GAMMA 1.0f #define CONFIG_MAX_GAMMA 10.0f ================================================ FILE: src/trx/config/dynamic_enum.c ================================================ #include #include #include #include #include #include #include typedef struct { char *value; char *label; } M_DYNAMIC_ENUM_VALUE; typedef struct M_DYNAMIC_ENUM_REGISTRY_ENTRY { const void *target; VECTOR *values; struct M_DYNAMIC_ENUM_REGISTRY_ENTRY *next; } M_DYNAMIC_ENUM_REGISTRY_ENTRY; static M_DYNAMIC_ENUM_REGISTRY_ENTRY *m_Registry = nullptr; static bool M_IsDynamicEnum(const CONFIG_OPTION *const option) { return option != nullptr && option->type == COT_DYNAMIC_ENUM; } static bool M_IsSameValue(const char *const left, const char *const right) { if (left == nullptr && right == nullptr) { return true; } if (left == nullptr || right == nullptr) { return false; } return strcmp(left, right) == 0; } static M_DYNAMIC_ENUM_REGISTRY_ENTRY *M_GetRegistryEntry( const CONFIG_OPTION *const option, const bool create) { if (option == nullptr) { return nullptr; } for (M_DYNAMIC_ENUM_REGISTRY_ENTRY *entry = m_Registry; entry != nullptr; entry = entry->next) { if (entry->target == option->target) { return entry; } } if (!create) { return nullptr; } M_DYNAMIC_ENUM_REGISTRY_ENTRY *const entry = Memory_Alloc(sizeof(*entry)); entry->target = option->target; entry->values = Vector_Create(sizeof(M_DYNAMIC_ENUM_VALUE)); entry->next = m_Registry; m_Registry = entry; return entry; } static void M_FreeValues(VECTOR **const values_ptr) { if (values_ptr == nullptr || *values_ptr == nullptr) { return; } VECTOR *const values = *values_ptr; for (int32_t i = 0; i < values->count; i++) { M_DYNAMIC_ENUM_VALUE *const dyn_value = Vector_Get(values, i); Memory_FreePointer(&dyn_value->value); Memory_FreePointer(&dyn_value->label); } Vector_Free(values); *values_ptr = nullptr; } static int32_t M_FindValueIndex( const CONFIG_OPTION *const option, const char *const value) { const M_DYNAMIC_ENUM_REGISTRY_ENTRY *const entry = M_GetRegistryEntry(option, false); if (entry == nullptr || entry->values == nullptr) { return -1; } for (int32_t i = 0; i < entry->values->count; i++) { const M_DYNAMIC_ENUM_VALUE *const dyn_value = Vector_Get(entry->values, i); if (M_IsSameValue(dyn_value->value, value)) { return i; } } return -1; } static const M_DYNAMIC_ENUM_VALUE *M_GetValueEntry( const CONFIG_OPTION *const option, const int32_t index) { const M_DYNAMIC_ENUM_REGISTRY_ENTRY *const entry = M_GetRegistryEntry(option, false); if (entry == nullptr || entry->values == nullptr) { return nullptr; } if (index < 0 || index >= entry->values->count) { return nullptr; } return Vector_Get(entry->values, index); } static const char *M_GetDisplayLabel( const M_DYNAMIC_ENUM_VALUE *const dyn_value) { if (dyn_value == nullptr) { return "(null)"; } if (!String_IsEmpty(dyn_value->label)) { const char *const resolved = GameString_Get(dyn_value->label); if (!String_IsEmpty(resolved)) { return resolved; } } if (dyn_value->value != nullptr) { return dyn_value->value; } return "(null)"; } static void M_Shutdown(void) { M_DYNAMIC_ENUM_REGISTRY_ENTRY *entry = m_Registry; while (entry != nullptr) { M_DYNAMIC_ENUM_REGISTRY_ENTRY *const next = entry->next; M_FreeValues(&entry->values); Memory_FreePointer(&entry); entry = next; } m_Registry = nullptr; } __attribute__((destructor)) static void M_AtShutdown(void) { M_Shutdown(); } void Config_DynamicEnum_ResetValues(const CONFIG_OPTION *const option) { if (!M_IsDynamicEnum(option)) { return; } M_DYNAMIC_ENUM_REGISTRY_ENTRY *const entry = M_GetRegistryEntry(option, true); ASSERT(entry != nullptr); M_FreeValues(&entry->values); entry->values = Vector_Create(sizeof(M_DYNAMIC_ENUM_VALUE)); } bool Config_DynamicEnum_AddValue( const CONFIG_OPTION *const option, const char *const value, const char *const label) { if (!M_IsDynamicEnum(option)) { return false; } M_DYNAMIC_ENUM_REGISTRY_ENTRY *const entry = M_GetRegistryEntry(option, true); ASSERT(entry != nullptr); if (entry->values == nullptr) { entry->values = Vector_Create(sizeof(M_DYNAMIC_ENUM_VALUE)); } M_DYNAMIC_ENUM_VALUE dyn_value = { .value = value != nullptr ? Memory_DupStr(value) : nullptr, .label = label != nullptr ? Memory_DupStr(label) : nullptr, }; Vector_Add(entry->values, &dyn_value); return true; } bool Config_DynamicEnum_IsValidValue( const CONFIG_OPTION *const option, const char *const value) { return M_FindValueIndex(option, value) >= 0; } int32_t Config_DynamicEnum_GetValueCount(const CONFIG_OPTION *const option) { const M_DYNAMIC_ENUM_REGISTRY_ENTRY *const entry = M_GetRegistryEntry(option, false); if (entry == nullptr || entry->values == nullptr) { return 0; } return entry->values->count; } const char *Config_DynamicEnum_GetValueAt( const CONFIG_OPTION *const option, const int32_t index) { const M_DYNAMIC_ENUM_VALUE *const dyn_value = M_GetValueEntry(option, index); if (dyn_value == nullptr) { return nullptr; } return dyn_value->value; } const char *Config_DynamicEnum_GetLabelAt( const CONFIG_OPTION *const option, const int32_t index) { const M_DYNAMIC_ENUM_VALUE *const dyn_value = M_GetValueEntry(option, index); return M_GetDisplayLabel(dyn_value); } const char *Config_DynamicEnum_GetLabelForValue( const CONFIG_OPTION *const option, const char *const value) { const int32_t idx = M_FindValueIndex(option, value); if (idx < 0) { return value != nullptr ? value : "(null)"; } return Config_DynamicEnum_GetLabelAt(option, idx); } bool Config_DynamicEnum_CanCycle( const CONFIG_OPTION *const option, const char *const current, const int32_t dir) { if (!M_IsDynamicEnum(option) || dir == 0) { return false; } const int32_t value_count = Config_DynamicEnum_GetValueCount(option); if (value_count <= 0) { return false; } const int32_t cur_idx = M_FindValueIndex(option, current); if (cur_idx < 0) { return true; } const int32_t step = dir < 0 ? -1 : 1; const int32_t next_idx = cur_idx + step; return next_idx >= 0 && next_idx < value_count; } const char *Config_DynamicEnum_GetNext( const CONFIG_OPTION *const option, const char *const current, const int32_t dir) { if (!M_IsDynamicEnum(option) || dir == 0) { return nullptr; } const int32_t value_count = Config_DynamicEnum_GetValueCount(option); if (value_count <= 0) { return nullptr; } const int32_t cur_idx = M_FindValueIndex(option, current); if (cur_idx < 0) { return Config_DynamicEnum_GetValueAt(option, 0); } const int32_t step = dir < 0 ? -1 : 1; const int32_t next_idx = cur_idx + step; if (next_idx < 0 || next_idx >= value_count) { return nullptr; } return Config_DynamicEnum_GetValueAt(option, next_idx); } ================================================ FILE: src/trx/config/dynamic_enum.h ================================================ #pragma once #include #include void Config_DynamicEnum_ResetValues(const CONFIG_OPTION *option); bool Config_DynamicEnum_AddValue( const CONFIG_OPTION *option, const char *value, const char *label); bool Config_DynamicEnum_IsValidValue( const CONFIG_OPTION *option, const char *value); int32_t Config_DynamicEnum_GetValueCount(const CONFIG_OPTION *option); const char *Config_DynamicEnum_GetValueAt( const CONFIG_OPTION *option, int32_t index); const char *Config_DynamicEnum_GetLabelAt( const CONFIG_OPTION *option, int32_t index); const char *Config_DynamicEnum_GetLabelForValue( const CONFIG_OPTION *option, const char *value); bool Config_DynamicEnum_CanCycle( const CONFIG_OPTION *option, const char *current, int32_t dir); const char *Config_DynamicEnum_GetNext( const CONFIG_OPTION *option, const char *current, int32_t dir); ================================================ FILE: src/trx/config/enum.c ================================================ #include #include static __attribute__((constructor)) void M_Init(void) { ENUM_MAP(ASPECT_MODE, ASPECT_MODE_ANY, "any"); ENUM_MAP(ASPECT_MODE, ASPECT_MODE_4_3, "4:3"); ENUM_MAP(ASPECT_MODE, ASPECT_MODE_16_9, "16:9"); ENUM_MAP(ASPECT_MODE, ASPECT_MODE_16_10, "16:10"); ENUM_MAP(INPUT_BACKEND, INPUT_BACKEND_KEYBOARD, "keyboard"); ENUM_MAP(INPUT_BACKEND, INPUT_BACKEND_CONTROLLER, "controller"); ENUM_MAP(SCREENSHOT_FORMAT, SCREENSHOT_FORMAT_JPEG, "jpg"); ENUM_MAP(SCREENSHOT_FORMAT, SCREENSHOT_FORMAT_JPEG, "jpeg"); ENUM_MAP(SCREENSHOT_FORMAT, SCREENSHOT_FORMAT_PNG, "png"); ENUM_MAP(MUSIC_LOAD_CONDITION, MUSIC_LOAD_CONDITION_NEVER, "never"); ENUM_MAP( MUSIC_LOAD_CONDITION, MUSIC_LOAD_CONDITION_NON_AMBIENT, "non-ambient"); ENUM_MAP(MUSIC_LOAD_CONDITION, MUSIC_LOAD_CONDITION_ALWAYS, "always"); ENUM_MAP(LOADING_SCREENS_MODE, LOADING_SCREENS_DISABLED, "disabled"); ENUM_MAP(LOADING_SCREENS_MODE, LOADING_SCREENS_ALWAYS, "always"); ENUM_MAP(LOADING_SCREENS_MODE, LOADING_SCREENS_NEW_GAMES, "new-games"); ENUM_MAP(BAR_SHOW_MODE, BAR_SHOW_MODE_NEVER, "never"); ENUM_MAP(BAR_SHOW_MODE, BAR_SHOW_MODE_ALWAYS, "always"); ENUM_MAP(BAR_SHOW_MODE, BAR_SHOW_MODE_BOSS_ONLY, "boss-only"); ENUM_MAP(UI_ELEMENT_LOCATION, UI_ELEMENT_LOCATION_TOP_LEFT, "top-left"); ENUM_MAP(UI_ELEMENT_LOCATION, UI_ELEMENT_LOCATION_TOP_CENTER, "top-center"); ENUM_MAP(UI_ELEMENT_LOCATION, UI_ELEMENT_LOCATION_TOP_RIGHT, "top-right"); ENUM_MAP( UI_ELEMENT_LOCATION, UI_ELEMENT_LOCATION_BOTTOM_LEFT, "bottom-left"); ENUM_MAP( UI_ELEMENT_LOCATION, UI_ELEMENT_LOCATION_BOTTOM_CENTER, "bottom-center"); ENUM_MAP( UI_ELEMENT_LOCATION, UI_ELEMENT_LOCATION_BOTTOM_RIGHT, "bottom-right"); ENUM_MAP(UI_STYLE, UI_STYLE_PS1, "ps1"); ENUM_MAP(UI_STYLE, UI_STYLE_PC, "pc"); ENUM_MAP(BACKGROUND_TYPE, BK_NONE, "none"); ENUM_MAP(BACKGROUND_TYPE, BK_TRANSPARENT_MEDIUM, "transparent"); ENUM_MAP(BACKGROUND_TYPE, BK_TRANSPARENT_DARK, "transparent-dark"); ENUM_MAP(BACKGROUND_TYPE, BK_BLACK, "black"); ENUM_MAP(BACKGROUND_TYPE, BK_PATTERN_STATIC, "pattern-static"); ENUM_MAP(BACKGROUND_TYPE, BK_PATTERN_WAVE, "pattern-wave"); ENUM_MAP(BACKGROUND_TYPE, BK_IMAGE, "image"); ENUM_MAP(BACKGROUND_TYPE, BK_MONOCHROME, "monochrome"); ENUM_MAP(BACKGROUND_TYPE, BK_MONOCHROME_WARM, "monochrome-warm"); ENUM_MAP(BACKGROUND_TYPE, BK_MONOCHROME_COOL, "monochrome-cool"); ENUM_MAP(CAMERA_MODE, CAMERA_MODE_TR1, "tr1"); ENUM_MAP(CAMERA_MODE, CAMERA_MODE_TR2, "tr2"); ENUM_MAP(CAMERA_MODE, CAMERA_MODE_TR3, "tr3"); ENUM_MAP(WALL_GLITCH_MODE, WALL_GLITCH_FIXED, "fixed"); ENUM_MAP(WALL_GLITCH_MODE, WALL_GLITCH_TR1, "tr1"); ENUM_MAP(WALL_GLITCH_MODE, WALL_GLITCH_TR2, "tr2"); ENUM_MAP(JUMP_LOCK_MODE, JUMP_LOCK_LEGACY, "legacy"); ENUM_MAP(JUMP_LOCK_MODE, JUMP_LOCK_TUNED, "tuned"); ENUM_MAP(JUMP_LOCK_MODE, JUMP_LOCK_DISABLED, "disabled"); ENUM_MAP(LOOK_MODE, LOOK_MODE_RESTRICTED, "restricted"); ENUM_MAP(LOOK_MODE, LOOK_MODE_ENHANCED, "enhanced"); ENUM_MAP(LOOK_MODE, LOOK_MODE_UNRESTRICTED, "unrestricted"); ENUM_MAP(QUICK_GUNS_MODE, QUICK_GUNS_MODE_DRAW_ONLY, "draw-only"); ENUM_MAP( QUICK_GUNS_MODE, QUICK_GUNS_MODE_DRAW_AND_HOLSTER, "draw-and-holster"); ENUM_MAP(LIGHTING_CONTRAST, LIGHTING_CONTRAST_LOW, "low"); ENUM_MAP(LIGHTING_CONTRAST, LIGHTING_CONTRAST_MEDIUM, "medium"); ENUM_MAP(LIGHTING_CONTRAST, LIGHTING_CONTRAST_HIGH, "high"); ENUM_MAP(BILLBOARD_LOCK_MODE, BILLBOARD_LOCK_NONE, "none"); ENUM_MAP(BILLBOARD_LOCK_MODE, BILLBOARD_LOCK_ROLL, "roll"); ENUM_MAP(BILLBOARD_LOCK_MODE, BILLBOARD_LOCK_ROLL_PITCH, "roll-pitch"); ENUM_MAP(BILLBOARD_LOCK_MODE, BILLBOARD_LOCK_PERSPECTIVE, "perspective"); ENUM_MAP(TARGET_LOCK_MODE, TARGET_LOCK_MODE_FULL, "full-lock"); ENUM_MAP(TARGET_LOCK_MODE, TARGET_LOCK_MODE_SEMI, "semi-lock"); ENUM_MAP(TARGET_LOCK_MODE, TARGET_LOCK_MODE_NONE, "no-lock"); ENUM_MAP(STATS_STYLE, STATS_STYLE_BARE, "bare"); ENUM_MAP(STATS_STYLE, STATS_STYLE_BORDERED, "bordered"); ENUM_MAP(SHADOW_TYPE, SHADOW_TYPE_OCTAGON, "octagon"); ENUM_MAP(SHADOW_TYPE, SHADOW_TYPE_CIRCLE, "circle"); ENUM_MAP(SHADOW_TYPE, SHADOW_TYPE_SPRITE, "sprite"); ENUM_MAP(BLOOD_EFFECTS, BLOOD_EFFECTS_DISABLED, "disabled"); ENUM_MAP(BLOOD_EFFECTS, BLOOD_EFFECTS_PINK, "pink"); ENUM_MAP(BLOOD_EFFECTS, BLOOD_EFFECTS_RED, "red"); ENUM_MAP(SUNGLASSES_MODE, SUNGLASSES_MODE_OFF, "off"); ENUM_MAP(SUNGLASSES_MODE, SUNGLASSES_MODE_OPAQUE, "opaque"); ENUM_MAP(SUNGLASSES_MODE, SUNGLASSES_MODE_TRANSPARENT, "transparent"); ENUM_MAP(ALLY_HOSTILITY_POLICY, ALLY_HOSTILITY_POLICY_SHARED, "shared"); ENUM_MAP( ALLY_HOSTILITY_POLICY, ALLY_HOSTILITY_POLICY_INDIVIDUAL, "individual"); ENUM_MAP(CREATURE_DROWN_POLICY, CREATURE_DROWN_POLICY_NEVER, "never"); ENUM_MAP(CREATURE_DROWN_POLICY, CREATURE_DROWN_POLICY_DEFAULT, "default"); ENUM_MAP( CREATURE_DROWN_POLICY, CREATURE_DROWN_POLICY_SUBMERGED, "accurate"); ENUM_MAP( PROJECTILE_AREA_DAMAGE, PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP, "single-sweep"); ENUM_MAP( PROJECTILE_AREA_DAMAGE, PROJECTILE_AREA_DAMAGE_MULTI_SWEEP, "multi-sweep"); } ================================================ FILE: src/trx/config/enum.h ================================================ #pragma once typedef enum { ASPECT_MODE_4_3, ASPECT_MODE_16_9, ASPECT_MODE_16_10, ASPECT_MODE_ANY, } ASPECT_MODE; typedef enum { INPUT_BACKEND_KEYBOARD, INPUT_BACKEND_CONTROLLER, INPUT_BACKEND_NUMBER_OF, } INPUT_BACKEND; typedef enum { INPUT_LAYOUT_DEFAULT, INPUT_LAYOUT_CUSTOM_1, INPUT_LAYOUT_CUSTOM_2, INPUT_LAYOUT_CUSTOM_3, INPUT_LAYOUT_NUMBER_OF, } INPUT_LAYOUT; typedef enum { SCREENSHOT_FORMAT_JPEG, SCREENSHOT_FORMAT_PNG, } SCREENSHOT_FORMAT; typedef enum { BK_NONE, BK_TRANSPARENT_MEDIUM, BK_TRANSPARENT_DARK, BK_BLACK, BK_PATTERN_STATIC, BK_PATTERN_WAVE, BK_IMAGE, BK_MONOCHROME, BK_MONOCHROME_COOL, BK_MONOCHROME_WARM, } BACKGROUND_TYPE; typedef enum { UI_ELEMENT_LOCATION_TOP_LEFT, UI_ELEMENT_LOCATION_TOP_CENTER, UI_ELEMENT_LOCATION_TOP_RIGHT, UI_ELEMENT_LOCATION_BOTTOM_LEFT, UI_ELEMENT_LOCATION_BOTTOM_CENTER, UI_ELEMENT_LOCATION_BOTTOM_RIGHT, UI_ELEMENT_LOCATION_CUSTOM, } UI_ELEMENT_LOCATION; typedef enum { BAR_SHOW_MODE_NEVER, BAR_SHOW_MODE_ALWAYS, BAR_SHOW_MODE_BOSS_ONLY, } BAR_SHOW_MODE; typedef enum { MUSIC_LOAD_CONDITION_NEVER, MUSIC_LOAD_CONDITION_NON_AMBIENT, MUSIC_LOAD_CONDITION_ALWAYS, } MUSIC_LOAD_CONDITION; typedef enum { LOADING_SCREENS_DISABLED, LOADING_SCREENS_ALWAYS, LOADING_SCREENS_NEW_GAMES, } LOADING_SCREENS_MODE; typedef enum { UI_STYLE_PS1, UI_STYLE_PC, } UI_STYLE; typedef enum { CAMERA_MODE_TR1, CAMERA_MODE_TR2, CAMERA_MODE_TR3, CAMERA_MODE_NUMBER_OF, } CAMERA_MODE; typedef enum { WALL_GLITCH_FIXED, WALL_GLITCH_TR1, WALL_GLITCH_TR2, } WALL_GLITCH_MODE; typedef enum { JUMP_LOCK_LEGACY, JUMP_LOCK_TUNED, JUMP_LOCK_DISABLED, JUMP_LOCK_NUMBER_OF, } JUMP_LOCK_MODE; typedef enum { LOOK_MODE_RESTRICTED, LOOK_MODE_ENHANCED, LOOK_MODE_UNRESTRICTED, } LOOK_MODE; typedef enum { QUICK_GUNS_MODE_DRAW_ONLY, QUICK_GUNS_MODE_DRAW_AND_HOLSTER, } QUICK_GUNS_MODE; typedef enum { LIGHTING_CONTRAST_LOW, LIGHTING_CONTRAST_MEDIUM, LIGHTING_CONTRAST_HIGH, LIGHTING_CONTRAST_NUMBER_OF, } LIGHTING_CONTRAST; typedef enum { BILLBOARD_LOCK_NONE, BILLBOARD_LOCK_ROLL, BILLBOARD_LOCK_ROLL_PITCH, BILLBOARD_LOCK_PERSPECTIVE, BILLBOARD_LOCK_NUMBER_OF, } BILLBOARD_LOCK_MODE; typedef enum { TARGET_LOCK_MODE_FULL, TARGET_LOCK_MODE_SEMI, TARGET_LOCK_MODE_NONE, } TARGET_LOCK_MODE; typedef enum { STATS_STYLE_BARE, STATS_STYLE_BORDERED, } STATS_STYLE; typedef enum { SHADOW_TYPE_OCTAGON, SHADOW_TYPE_CIRCLE, SHADOW_TYPE_SPRITE, SHADOW_TYPE_NUMBER_OF, } SHADOW_TYPE; typedef enum { BLOOD_EFFECTS_DISABLED, BLOOD_EFFECTS_PINK, BLOOD_EFFECTS_RED, BLOOD_EFFECTS_NUMBER_OF, } BLOOD_EFFECTS; typedef enum { ALLY_HOSTILITY_POLICY_INDIVIDUAL, ALLY_HOSTILITY_POLICY_SHARED, } ALLY_HOSTILITY_POLICY; typedef enum { CREATURE_DROWN_POLICY_NEVER, CREATURE_DROWN_POLICY_DEFAULT, CREATURE_DROWN_POLICY_SUBMERGED, } CREATURE_DROWN_POLICY; typedef enum { PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP, PROJECTILE_AREA_DAMAGE_MULTI_SWEEP, } PROJECTILE_AREA_DAMAGE; typedef enum { SUNGLASSES_MODE_OFF, SUNGLASSES_MODE_OPAQUE, SUNGLASSES_MODE_TRANSPARENT, } SUNGLASSES_MODE; ================================================ FILE: src/trx/config/file.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_EMPTY_ROOT "{}" #define M_ENFORCED_KEY "enforced_config" #define M_HIDDEN_KEY "hidden_config" static bool M_ReadFromJSON( const char *const default_path, const char *const enforced_path, void (*load)(JSON_OBJECT *root_obj), VECTOR *const enforced_targets, VECTOR *const hidden_targets) { bool result = false; JSON_VALUE *cfg_root = JSONFile_Read(default_path); if (cfg_root != nullptr) { result = true; } else { JSON_OBJECT *const cfg_root_obj = JSON_ObjectNew(); JSON_ObjectAppendInt(cfg_root_obj, "config_version", -1); cfg_root = JSON_ValueFromObject(cfg_root_obj); } JSON_VALUE *const enf_root = enforced_path != nullptr ? JSONFile_Read(enforced_path) : nullptr; JSON_OBJECT *cfg_root_obj = JSON_ValueAsObject(cfg_root); JSON_OBJECT *enf_root_obj = JSON_ValueAsObject(enf_root); // Merge settings from the game flow file. JSON_OBJECT *const enforced_config = JSON_ObjectGetObject(enf_root_obj, M_ENFORCED_KEY); if (enforced_config != nullptr && enforced_targets != nullptr) { Vector_ClearRealloc(enforced_targets); const JSON_OBJECT_ELEMENT *elem = enforced_config->start; while (elem != nullptr) { const char *const name = elem->name->string; const CONFIG_OPTION *const opt = Config_GetOptionByPath(name); if (opt != nullptr) { Vector_Add(enforced_targets, &opt->target); } elem = elem->next; } } if (enforced_config != nullptr) { JSON_ObjectMerge(cfg_root_obj, enforced_config); } // Record hidden settings from the game flow file. JSON_ARRAY *const hidden_config_arr = JSON_ObjectGetArray(enf_root_obj, M_HIDDEN_KEY); if (hidden_config_arr != nullptr && hidden_targets != nullptr) { Vector_ClearRealloc(hidden_targets); for (size_t i = 0; i < hidden_config_arr->length; i++) { const char *const name = JSON_ArrayGetString(hidden_config_arr, i, nullptr); if (name == nullptr) { LOG_WARNING( "Expected element %d in \"%s\" to be a string", i, M_HIDDEN_KEY); continue; } const CONFIG_OPTION *const opt = Config_GetOptionByPath(name); if (opt != nullptr) { Vector_Add(hidden_targets, &opt->target); } } } load(cfg_root_obj); if (cfg_root) { JSON_ValueFree(cfg_root); } if (enf_root) { JSON_ValueFree(enf_root); } return result; } static void M_PreserveEnforcedState( JSON_OBJECT *const root_obj, JSON_VALUE *const old_root, JSON_VALUE *const enf_root) { if (old_root == nullptr || enf_root == nullptr) { return; } JSON_OBJECT *old_root_obj = JSON_ValueAsObject(old_root); JSON_OBJECT *enf_root_obj = JSON_ValueAsObject(enf_root); JSON_OBJECT *enforced_obj = JSON_ObjectGetObject(enf_root_obj, M_ENFORCED_KEY); if (enforced_obj == nullptr) { return; } // Restore the original values for any enforced settings, provided they were // defined. JSON_OBJECT_ELEMENT *elem = enforced_obj->start; while (elem != nullptr) { const char *const name = elem->name->string; elem = elem->next; JSON_ObjectEvictKey(root_obj, name); if (!JSON_ObjectContainsKey(old_root_obj, name)) { continue; } JSON_VALUE *const old_value = JSON_ObjectGetValue(old_root_obj, name); JSON_ObjectAppend(root_obj, name, old_value); } } bool ConfigFile_Read(const CONFIG_IO_ARGS *const args) { ASSERT(args->default_path != nullptr); return M_ReadFromJSON( args->default_path, args->enforced_path, args->action, args->enforced_targets, args->hidden_targets); } bool ConfigFile_Write(const CONFIG_IO_ARGS *const args) { ASSERT(args->default_path != nullptr); JSON_VALUE *const old_root = JSONFile_Read(args->default_path); JSON_VALUE *const enf_root = args->enforced_path != nullptr ? JSONFile_Read(args->enforced_path) : nullptr; JSON_OBJECT *const root_obj = JSON_ObjectNew(); args->action(root_obj); M_PreserveEnforcedState(root_obj, old_root, enf_root); JSON_VALUE *const new_root = JSON_ValueFromObject(root_obj); const bool updated = JSONFile_Write(args->default_path, new_root); JSON_ValueFree(new_root); JSON_ValueFree(old_root); JSON_ValueFree(enf_root); return updated; } void ConfigFile_LoadOptions(JSON_OBJECT *root_obj, const CONFIG_OPTION *options) { const CONFIG_OPTION *opt = options; while (opt->target != nullptr) { switch (opt->type) { case COT_BOOL: *(bool *)opt->target = JSON_ObjectGetBool( root_obj, Config_ResolveOptionName(opt->name), *(bool *)opt->default_value); break; case COT_INT32: { JSON_VALUE *const value = JSON_ObjectGetValue( root_obj, Config_ResolveOptionName(opt->name)); bool success = false; if (value != nullptr && value->type == JSON_TYPE_NUMBER) { *(int32_t *)opt->target = JSON_ValueGetInt(value, *(int32_t *)opt->default_value); success = true; } else if (value != nullptr && value->type == JSON_TYPE_STRING) { success = String_ParseInteger( JSON_ValueGetString(value, ""), (int32_t *)opt->target); } if (!success) { *(int32_t *)opt->target = *(int32_t *)opt->default_value; } break; } case COT_FLOAT: case COT_FLOAT_PERCENT: *(float *)opt->target = JSON_ObjectGetDouble( root_obj, Config_ResolveOptionName(opt->name), *(float *)opt->default_value); break; case COT_DOUBLE: *(double *)opt->target = JSON_ObjectGetDouble( root_obj, Config_ResolveOptionName(opt->name), *(double *)opt->default_value); break; case COT_ENUM: *(int *)opt->target = ConfigFile_ReadEnum( root_obj, Config_ResolveOptionName(opt->name), *(int *)opt->default_value, opt->param); break; case COT_STRING: case COT_DYNAMIC_ENUM: { const char *const val = JSON_ObjectGetString( root_obj, Config_ResolveOptionName(opt->name), (const char *)opt->default_value); char **const p = (char **)opt->target; Memory_FreePointer(p); if (val != nullptr) { *p = Memory_DupStr(val); } else if (opt->default_value != nullptr) { *p = Memory_DupStr(opt->default_value); } else { *p = nullptr; } break; } case COT_RGB888: { RGB_888 *const target = (RGB_888 *)opt->target; JSON_VALUE *const value = JSON_ObjectGetValue( root_obj, Config_ResolveOptionName(opt->name)); bool success = false; if (value != nullptr && value->type == JSON_TYPE_NUMBER) { const uint32_t rgb_value = JSON_ValueGetInt(value, JSON_INVALID_NUMBER); ASSERT(rgb_value != JSON_INVALID_NUMBER); target->r = (rgb_value >> 0) & 0xFF; target->g = (rgb_value >> 8) & 0xFF; target->b = (rgb_value >> 16) & 0xFF; success = true; } else if (value != nullptr && value->type == JSON_TYPE_STRING) { const char *str_value = JSON_ValueGetString(value, JSON_INVALID_STRING); ASSERT(str_value != JSON_INVALID_STRING); success = String_ParseRGB888(str_value, target); } if (!success) { *(RGB_888 *)opt->target = *(RGB_888 *)opt->default_value; } break; } } opt++; } } void ConfigFile_DumpOptions(JSON_OBJECT *root_obj, const CONFIG_OPTION *options) { const CONFIG_OPTION *opt = options; while (opt->target != nullptr) { switch (opt->type) { case COT_BOOL: JSON_ObjectAppendBool( root_obj, Config_ResolveOptionName(opt->name), *(bool *)opt->target); break; case COT_INT32: JSON_ObjectAppendInt( root_obj, Config_ResolveOptionName(opt->name), *(int32_t *)opt->target); break; case COT_FLOAT: case COT_FLOAT_PERCENT: JSON_ObjectAppendDouble( root_obj, Config_ResolveOptionName(opt->name), *(float *)opt->target); break; case COT_DOUBLE: JSON_ObjectAppendDouble( root_obj, Config_ResolveOptionName(opt->name), *(double *)opt->target); break; case COT_ENUM: ConfigFile_WriteEnum( root_obj, Config_ResolveOptionName(opt->name), *(int *)opt->target, (const char *)opt->param); break; case COT_STRING: case COT_DYNAMIC_ENUM: if (*(char **)opt->target != nullptr) { JSON_ObjectAppendString( root_obj, Config_ResolveOptionName(opt->name), *(char **)opt->target); } break; case COT_RGB888: { const RGB_888 *const color = (RGB_888 *)opt->target; char tmp[10]; sprintf(tmp, "#%02X%02X%02X", color->r, color->g, color->b); JSON_ObjectAppendString( root_obj, Config_ResolveOptionName(opt->name), tmp); break; } } opt++; } } int ConfigFile_ReadEnum( JSON_OBJECT *const obj, const char *const name, const int default_value, const char *const enum_name) { const char *value_str = JSON_ObjectGetString(obj, name, nullptr); if (value_str != nullptr) { return EnumMap_Get(enum_name, value_str, default_value); } return default_value; } void ConfigFile_WriteEnum( JSON_OBJECT *obj, const char *name, int value, const char *enum_name) { JSON_ObjectAppendString(obj, name, EnumMap_ToString(enum_name, value)); } bool ConfigFile_LoadGymTrackStats( JSON_OBJECT *const root_obj, const char *const key_name, GYM_TRACK_STATS *const stats) { JSON_OBJECT *const stats_obj = JSON_ObjectGetObject(root_obj, key_name); if (stats_obj == nullptr) { return false; } JSON_ARRAY *const entries_arr = JSON_ObjectGetArray(stats_obj, "entries"); if (entries_arr != nullptr) { for (size_t i = 0; i < entries_arr->length && i < MAX_ASSAULT_TIMES; i++) { JSON_OBJECT *const entry_obj = JSON_ArrayGetObject(entries_arr, i); if (entry_obj != nullptr) { stats->entries[i].time = JSON_ObjectGetInt( entry_obj, "time", stats->entries[i].time); stats->entries[i].attempt_num = JSON_ObjectGetInt( entry_obj, "attempt_num", stats->entries[i].attempt_num); } } } stats->total_attempts = JSON_ObjectGetInt(stats_obj, "total_attempts", stats->total_attempts); return true; } bool ConfigFile_DumpGymTrackStats( JSON_OBJECT *const root_obj, const char *const key_name, const GYM_TRACK_STATS *const stats) { JSON_OBJECT *const stats_obj = JSON_ObjectNew(); JSON_ARRAY *const entries_arr = JSON_ArrayNew(); for (int32_t i = 0; i < MAX_ASSAULT_TIMES; i++) { if (stats->entries[i].time == 0) { break; } JSON_OBJECT *const entry_obj = JSON_ObjectNew(); JSON_ObjectAppendInt(entry_obj, "time", stats->entries[i].time); JSON_ObjectAppendInt( entry_obj, "attempt_num", stats->entries[i].attempt_num); JSON_ArrayAppendObject(entries_arr, entry_obj); } JSON_ObjectAppendArray(stats_obj, "entries", entries_arr); JSON_ObjectAppendInt(stats_obj, "total_attempts", stats->total_attempts); JSON_ObjectAppendObject(root_obj, key_name, stats_obj); return true; } ================================================ FILE: src/trx/config/file.h ================================================ #pragma once #include #include #include #include #include #include typedef struct { const char *default_path; const char *enforced_path; void (*action)(JSON_OBJECT *root_obj); VECTOR *enforced_targets; VECTOR *hidden_targets; } CONFIG_IO_ARGS; bool ConfigFile_Read(const CONFIG_IO_ARGS *control); bool ConfigFile_Write(const CONFIG_IO_ARGS *control); void ConfigFile_LoadOptions( JSON_OBJECT *root_obj, const CONFIG_OPTION *options); void ConfigFile_DumpOptions( JSON_OBJECT *root_obj, const CONFIG_OPTION *options); int ConfigFile_ReadEnum( JSON_OBJECT *obj, const char *name, int default_value, const char *enum_name); void ConfigFile_WriteEnum( JSON_OBJECT *obj, const char *name, int value, const char *enum_name); bool ConfigFile_LoadGymTrackStats( JSON_OBJECT *root_obj, const char *key_name, GYM_TRACK_STATS *assault_stats); bool ConfigFile_DumpGymTrackStats( JSON_OBJECT *root_obj, const char *key_name, const GYM_TRACK_STATS *assault_stats); ================================================ FILE: src/trx/config/map.c ================================================ #include #include #include #include #include #include #include #include #include #include #define X_CFG_BOOL(target_, default_value_) \ { .name = QUOTE(target_), \ .type = COT_BOOL, \ .target = &g_Config.target_, \ .default_value = &(bool) { default_value_ }, \ .param = nullptr }, #define X_CFG_INT32(target_, default_value_) \ { .name = QUOTE(target_), \ .type = COT_INT32, \ .target = &g_Config.target_, \ .default_value = &(int32_t) { default_value_ }, \ .param = nullptr }, #define X_CFG_FLOAT(target_, default_value_) \ { .name = QUOTE(target_), \ .type = COT_FLOAT, \ .target = &g_Config.target_, \ .default_value = &(float) { default_value_ }, \ .param = nullptr }, #define X_CFG_FLOAT_PERCENT(target_, default_value_) \ { .name = QUOTE(target_), \ .type = COT_FLOAT_PERCENT, \ .target = &g_Config.target_, \ .default_value = &(float) { default_value_ }, \ .param = nullptr }, #define X_CFG_DOUBLE(target_, default_value_) \ { .name = QUOTE(target_), \ .type = COT_DOUBLE, \ .target = &g_Config.target_, \ .default_value = &(double) { default_value_ }, \ .param = nullptr }, #define X_CFG_ENUM(target_, default_value_, enum_map) \ X_CFG_ENUM_EX(QUOTE(target_), target_, default_value_, enum_map) #define X_CFG_ENUM_EX(name_, target_, default_value_, enum_map) \ { .name = name_, \ .type = COT_ENUM, \ .target = &g_Config.target_, \ .default_value = &(int32_t) { default_value_ }, \ .param = ENUM_MAP_NAME(enum_map) }, #define X_CFG_RGB888(target_, default_r, default_g, default_b) \ { .name = QUOTE(target_), \ .type = COT_RGB888, \ .target = &g_Config.target_, \ .default_value = &(RGB_888) { default_r, default_g, default_b }, \ .param = nullptr }, #define X_CFG_STRING(target_, default_value_) \ { .name = QUOTE(target_), \ .type = COT_STRING, \ .target = &g_Config.target_, \ .default_value = default_value_, \ .param = nullptr }, #define X_CFG_STRING_EX(name_, target_, default_value_) \ { .name = name_, \ .type = COT_STRING, \ .target = &g_Config.target_, \ .default_value = default_value_, \ .param = nullptr }, #define X_CFG_DYNAMIC_ENUM(target_, default_value_) \ { .name = QUOTE(target_), \ .type = COT_DYNAMIC_ENUM, \ .target = &g_Config.target_, \ .default_value = default_value_, \ .param = nullptr }, #define X_CFG_DYNAMIC_ENUM_EX(name_, target_, default_value_) \ { .name = name_, \ .type = COT_DYNAMIC_ENUM, \ .target = &g_Config.target_, \ .default_value = default_value_, \ .param = nullptr }, static const CONFIG_OPTION *m_ConfigOptionMap[TR_VERSION_COUNT] = { [0] = (CONFIG_OPTION[]) { #include {}, // sentinel }, [1] = (CONFIG_OPTION[]) { #include {}, // sentinel }, [2] = (CONFIG_OPTION[]) { #include {}, // sentinel }, }; #undef X_CFG_BOOL #undef X_CFG_INT32 #undef X_CFG_FLOAT #undef X_CFG_FLOAT_PERCENT #undef X_CFG_DOUBLE #undef X_CFG_ENUM #undef X_CFG_ENUM_EX #undef X_CFG_RGB888 #undef X_CFG_STRING #undef X_CFG_STRING_EX #undef X_CFG_DYNAMIC_ENUM #undef X_CFG_DYNAMIC_ENUM_EX const CONFIG_OPTION *Config_GetOptionMap(void) { if (g_TRVersion < 1 || g_TRVersion > 3) { return nullptr; } return m_ConfigOptionMap[g_TRVersion - 1]; } const char *Config_ResolveOptionName(const char *option_name) { const char *dot = strrchr(option_name, '.'); if (dot) { return dot + 1; } return option_name; } const CONFIG_OPTION *Config_GetOptionByPath(const char *const path) { if (path == nullptr) { return nullptr; } for (const CONFIG_OPTION *opt = Config_GetOptionMap(); opt->name != nullptr; opt++) { if (strcmp(opt->name, path) == 0 || strcmp(Config_ResolveOptionName(opt->name), path) == 0) { return opt; } } return nullptr; } ================================================ FILE: src/trx/config/map.def ================================================ X_CFG_BOOL(audio.enable_lara_mic, false) X_CFG_BOOL(audio.enable_music_in_inventory, true) X_CFG_BOOL(audio.enable_music_in_menu, true) X_CFG_BOOL(audio.enable_pitched_sounds, true) X_CFG_BOOL(audio.enable_ps1_sfx, true) X_CFG_BOOL(audio.enable_underwater_anim_sfx, true) X_CFG_BOOL(audio.fix_chainblock_secret_sound, true) X_CFG_BOOL(audio.fix_secrets_killing_music, true) X_CFG_BOOL(audio.load_music_triggers, true) X_CFG_BOOL(audio.mute_out_of_focus, true) X_CFG_BOOL(debug.enable_debug_anim, false) X_CFG_BOOL(debug.enable_debug_bounding_boxes, false) X_CFG_BOOL(debug.enable_debug_camera, false) X_CFG_BOOL(debug.enable_debug_portals, false) X_CFG_BOOL(debug.enable_debug_pos, false) X_CFG_BOOL(debug.enable_debug_room_clip, false) X_CFG_BOOL(debug.enable_debug_spheres, false) X_CFG_BOOL(debug.enable_debug_status, false) X_CFG_BOOL(debug.enable_debug_triggers, false) X_CFG_BOOL(debug.enable_endless_flare_time, false) X_CFG_BOOL(debug.enable_endless_sprint, false) X_CFG_BOOL(debug.enable_invulnerability, false) X_CFG_BOOL(debug.enable_review_markers, false) X_CFG_BOOL(flow.cheat_keys, true) X_CFG_BOOL(flow.load_save_disabled, false) X_CFG_BOOL(flow.lockout_option_ring, false) X_CFG_BOOL(flow.play_any_level, false) X_CFG_BOOL(gameplay.change_pierre_spawn, true) X_CFG_BOOL(gameplay.disable_extra_guns, false) X_CFG_BOOL(gameplay.disable_healing_between_levels, false) X_CFG_BOOL(gameplay.disable_medpacks, false) X_CFG_BOOL(gameplay.disable_trex_collision, false) X_CFG_BOOL(gameplay.enable_ally_targeting, true) X_CFG_BOOL(gameplay.enable_auto_item_selection, true) X_CFG_BOOL(gameplay.enable_body_bags, false) X_CFG_BOOL(gameplay.enable_cheats, false) X_CFG_BOOL(gameplay.enable_cinematics, true) X_CFG_BOOL(gameplay.enable_compass_stats, true) X_CFG_BOOL(gameplay.enable_console, true) X_CFG_BOOL(gameplay.enable_controlled_drops, true) X_CFG_BOOL(gameplay.enable_crawl_jump, true) X_CFG_BOOL(gameplay.enable_crawl_tilt, true) X_CFG_BOOL(gameplay.enable_crawling, true) X_CFG_BOOL(gameplay.enable_credits, true) X_CFG_BOOL(gameplay.enable_crouch_roll, true) X_CFG_BOOL(gameplay.enable_cutscenes, true) X_CFG_BOOL(gameplay.enable_demo, true) X_CFG_BOOL(gameplay.enable_enemy_rotation, true) X_CFG_BOOL(gameplay.enable_enhanced_saves, true) X_CFG_BOOL(gameplay.enable_fmv, true) X_CFG_BOOL(gameplay.enable_game_modes, true) X_CFG_BOOL(gameplay.enable_idle_pose_camera, false) X_CFG_BOOL(gameplay.enable_inverted_look, false) X_CFG_BOOL(gameplay.enable_item_examining, true) X_CFG_BOOL(gameplay.enable_jump_twists, true) X_CFG_BOOL(gameplay.enable_killer_pushblocks, true) X_CFG_BOOL(gameplay.enable_lean_jumping, true) X_CFG_BOOL(gameplay.enable_ledge_jumps, true) X_CFG_BOOL(gameplay.enable_legal, true) X_CFG_BOOL(gameplay.enable_manual_camera, false) X_CFG_BOOL(gameplay.enable_neutral_twists, true) X_CFG_BOOL(gameplay.enable_pickup_aids, true) X_CFG_BOOL(gameplay.enable_play_previous_levels, true) X_CFG_BOOL(gameplay.enable_responsive_crawl, true) X_CFG_BOOL(gameplay.enable_responsive_sprint, true) X_CFG_BOOL(gameplay.enable_save_crystals, false) X_CFG_BOOL(gameplay.enable_slide_to_run, true) X_CFG_BOOL(gameplay.enable_smooth_wall_deflect, true) X_CFG_BOOL(gameplay.enable_soft_statics, false) X_CFG_BOOL(gameplay.enable_sprint, true) X_CFG_BOOL(gameplay.enable_swing_cancel, true) X_CFG_BOOL(gameplay.enable_target_change, true) X_CFG_BOOL(gameplay.enable_toggle_crouch, false) X_CFG_BOOL(gameplay.enable_toggle_sprint, false) X_CFG_BOOL(gameplay.enable_total_stats, true) X_CFG_BOOL(gameplay.enable_tr2_jumping, true) X_CFG_BOOL(gameplay.enable_tr2_swim_cancel, true) X_CFG_BOOL(gameplay.enable_tr2_swimming, true) X_CFG_BOOL(gameplay.enable_uw_roll, true) X_CFG_BOOL(gameplay.enable_wading, true) X_CFG_BOOL(gameplay.enable_walk_to_items, false) X_CFG_BOOL(gameplay.fix_alligator_ai, true) X_CFG_BOOL(gameplay.fix_bear_ai, true) X_CFG_BOOL(gameplay.fix_bridge_collision, true) X_CFG_BOOL(gameplay.fix_descending_glitch, true) X_CFG_BOOL(gameplay.fix_flare_throw_priority, true) X_CFG_BOOL(gameplay.fix_floor_data_issues, true) X_CFG_BOOL(gameplay.fix_free_flare_glitch, true) X_CFG_BOOL(gameplay.fix_item_duplication_glitch, true) X_CFG_BOOL(gameplay.fix_lara_pickup_embed, true) X_CFG_BOOL(gameplay.fix_m16_accuracy, true) X_CFG_BOOL(gameplay.fix_monkey_pickup_priority, true) X_CFG_BOOL(gameplay.fix_pipeman_aim, true) X_CFG_BOOL(gameplay.fix_qwop_glitch, true) X_CFG_BOOL(gameplay.fix_step_glitch, true) X_CFG_BOOL(gameplay.fix_wade_wall_hit, true) X_CFG_BOOL(gameplay.fix_walk_run_jump, true) X_CFG_BOOL(gameplay.fix_wall_geometry, true) X_CFG_BOOL(gameplay.pause_on_focus_lost, false) X_CFG_BOOL(gameplay.remember_gun_status, true) X_CFG_BOOL(gameplay.restore_ps1_enemies, false) X_CFG_BOOL(input.enable_buffering_func_keys, false) X_CFG_BOOL(input.enable_buffering_inventory, true) X_CFG_BOOL(input.enable_responsive_passport, true) X_CFG_BOOL(input.enable_tr3_sidesteps, true) X_CFG_BOOL(profile.new_game_plus_unlock, false) X_CFG_BOOL(rendering.enable_lighting, true) X_CFG_BOOL(rendering.enable_textures, true) X_CFG_BOOL(rendering.enable_trapezoid_filter, true) X_CFG_BOOL(rendering.enable_vsync, true) X_CFG_BOOL(rendering.enable_wireframe, false) X_CFG_BOOL(ui.enable_bar_flashing, true) X_CFG_BOOL(ui.enable_fps_counter, false) X_CFG_BOOL(ui.enable_game_ui, true) X_CFG_BOOL(ui.enable_photo_mode_ui, true) X_CFG_BOOL(ui.enable_wraparound, true) X_CFG_BOOL(ui.inventory_fade_effects, true) X_CFG_BOOL(ui.pause_fade_effects, true) X_CFG_BOOL(ui.show_bars, true) X_CFG_BOOL(ui.show_pickups_overlay, true) X_CFG_BOOL(ui.show_title_version, true) X_CFG_BOOL(ui.stats.show_ammo, true) X_CFG_BOOL(ui.stats.show_crystals, false) X_CFG_BOOL(ui.stats.show_deaths, true) X_CFG_BOOL(ui.stats.show_distance_travelled, true) X_CFG_BOOL(ui.stats.show_kills, true) X_CFG_BOOL(ui.stats.show_level_header, false) X_CFG_BOOL(ui.stats.show_medipacks_used, true) X_CFG_BOOL(ui.stats.show_pickups, true) X_CFG_BOOL(ui.stats.show_secrets, true) X_CFG_BOOL(ui.stats.show_time_taken, true) X_CFG_BOOL(ui.stats.show_totals, true) X_CFG_BOOL(ui.stats_fade_effects, true) X_CFG_BOOL(visuals.enable_3d_pickups, true) X_CFG_BOOL(visuals.enable_braid, true) X_CFG_BOOL(visuals.enable_breeze, true) X_CFG_BOOL(visuals.enable_exit_fade_effects, true) X_CFG_BOOL(visuals.enable_fade_effects, true) X_CFG_BOOL(visuals.enable_fire_lighting, true) X_CFG_BOOL(visuals.enable_footprints, true) X_CFG_BOOL(visuals.enable_glide_cameras, true) X_CFG_BOOL(visuals.enable_gun_lighting, true) X_CFG_BOOL(visuals.enable_ps1_crystals, true) X_CFG_BOOL(visuals.enable_reflections, true) X_CFG_BOOL(visuals.enable_responsive_mesh_tint, true) X_CFG_BOOL(visuals.enable_shotgun_flash, true) X_CFG_BOOL(visuals.enable_skybox, true) X_CFG_BOOL(visuals.enable_weather, true) X_CFG_BOOL(visuals.fix_animated_sprites, true) X_CFG_BOOL(visuals.fix_item_rots, true) X_CFG_BOOL(visuals.fix_texture_issues, true) X_CFG_BOOL(visuals.fog_transparency, false) X_CFG_BOOL(window.is_fullscreen, true) X_CFG_BOOL(window.is_maximized, true) X_CFG_DYNAMIC_ENUM(visuals.lara_outfit, nullptr) X_CFG_DYNAMIC_ENUM_EX("ui.airbar_color", ui.lara_air_bar.color, "blue") X_CFG_DYNAMIC_ENUM_EX("ui.airbar_color_ps1", ui.lara_air_bar.color_ps1, "teal-green") X_CFG_DYNAMIC_ENUM_EX("ui.enemy_healthbar_color", ui.enemy_health_bar.color, "grey") X_CFG_DYNAMIC_ENUM_EX("ui.enemy_healthbar_color_allies", ui.enemy_health_bar.color_allies, "teal") X_CFG_DYNAMIC_ENUM_EX("ui.enemy_healthbar_color_allies_ps1", ui.enemy_health_bar.color_allies_ps1, "yellow-green") X_CFG_DYNAMIC_ENUM_EX("ui.enemy_healthbar_color_ps1", ui.enemy_health_bar.color_ps1, "orange-red") X_CFG_DYNAMIC_ENUM_EX("ui.exposurebar_color", ui.lara_exposure_bar.color, "cyan") X_CFG_DYNAMIC_ENUM_EX("ui.exposurebar_color_ps1", ui.lara_exposure_bar.color_ps1, "dark-blue-red") X_CFG_DYNAMIC_ENUM_EX("ui.healthbar_color", ui.lara_health_bar.color, "red") X_CFG_DYNAMIC_ENUM_EX("ui.healthbar_color_ps1", ui.lara_health_bar.color_ps1, "red-green") X_CFG_DYNAMIC_ENUM_EX("ui.healthbar_poison_color", ui.lara_health_bar.poison_color, "yellow") X_CFG_DYNAMIC_ENUM_EX("ui.healthbar_poison_color_ps1", ui.lara_health_bar.poison_color_ps1, "dark-red-purple") X_CFG_DYNAMIC_ENUM_EX("ui.sprintbar_color", ui.lara_sprint_bar.color, "green") X_CFG_DYNAMIC_ENUM_EX("ui.sprintbar_color_ps1", ui.lara_sprint_bar.color_ps1, "red-yellow") X_CFG_ENUM(audio.music_load_condition, MUSIC_LOAD_CONDITION_NON_AMBIENT, MUSIC_LOAD_CONDITION) X_CFG_ENUM(gameplay.ally_hostility_policy, ALLY_HOSTILITY_POLICY_SHARED, ALLY_HOSTILITY_POLICY) X_CFG_ENUM(gameplay.creature_drown_policy, CREATURE_DROWN_POLICY_SUBMERGED, CREATURE_DROWN_POLICY) X_CFG_ENUM(gameplay.loading_screens, LOADING_SCREENS_DISABLED, LOADING_SCREENS_MODE) X_CFG_ENUM(gameplay.look_mode, LOOK_MODE_ENHANCED, LOOK_MODE) X_CFG_ENUM(gameplay.target_mode, TARGET_LOCK_MODE_FULL, TARGET_LOCK_MODE) X_CFG_ENUM(gameplay.wall_glitch_mode, WALL_GLITCH_FIXED, WALL_GLITCH_MODE) X_CFG_ENUM(input.backend, INPUT_BACKEND_KEYBOARD, INPUT_BACKEND) X_CFG_ENUM(input.quick_guns_mode, QUICK_GUNS_MODE_DRAW_AND_HOLSTER, QUICK_GUNS_MODE) X_CFG_ENUM(rendering.aspect_mode, ASPECT_MODE_ANY, ASPECT_MODE) X_CFG_ENUM(rendering.lighting_contrast, LIGHTING_CONTRAST_MEDIUM, LIGHTING_CONTRAST) X_CFG_ENUM(rendering.screenshot_format, SCREENSHOT_FORMAT_JPEG, SCREENSHOT_FORMAT) X_CFG_ENUM(rendering.sprite_lock_mode, BILLBOARD_LOCK_PERSPECTIVE, BILLBOARD_LOCK_MODE) X_CFG_ENUM(rendering.texture_filter, TEXTURE_FILTER_POINT, TEXTURE_FILTER) X_CFG_ENUM(rendering.ui_filter, TEXTURE_FILTER_POINT, TEXTURE_FILTER) X_CFG_ENUM(rendering.upscaling_filter, TEXTURE_FILTER_POINT, TEXTURE_FILTER) X_CFG_ENUM(ui.menu_style, UI_STYLE_PS1, UI_STYLE) X_CFG_ENUM(ui.stats.style, STATS_STYLE_BORDERED, STATS_STYLE) X_CFG_ENUM(visuals.blood_effects, BLOOD_EFFECTS_RED, BLOOD_EFFECTS) X_CFG_ENUM(visuals.sunglasses_mode, SUNGLASSES_MODE_OFF, SUNGLASSES_MODE) X_CFG_ENUM_EX("ui.airbar_location", ui.lara_air_bar.location, UI_ELEMENT_LOCATION_TOP_RIGHT, UI_ELEMENT_LOCATION) X_CFG_ENUM_EX("ui.ammo_counter_location", ui.ammo_counter.location, UI_ELEMENT_LOCATION_TOP_RIGHT, UI_ELEMENT_LOCATION) X_CFG_ENUM_EX("ui.enemy_healthbar_location", ui.enemy_health_bar.location, UI_ELEMENT_LOCATION_BOTTOM_LEFT, UI_ELEMENT_LOCATION) X_CFG_ENUM_EX("ui.enemy_healthbar_show_mode", ui.enemy_health_bar.show_mode, BAR_SHOW_MODE_ALWAYS, BAR_SHOW_MODE) X_CFG_ENUM_EX("ui.exposurebar_location", ui.lara_exposure_bar.location, UI_ELEMENT_LOCATION_TOP_RIGHT, UI_ELEMENT_LOCATION) X_CFG_ENUM_EX("ui.healthbar_location", ui.lara_health_bar.location, UI_ELEMENT_LOCATION_TOP_LEFT, UI_ELEMENT_LOCATION) X_CFG_ENUM_EX("ui.sprintbar_location", ui.lara_sprint_bar.location, UI_ELEMENT_LOCATION_TOP_RIGHT, UI_ELEMENT_LOCATION) X_CFG_FLOAT(rendering.anisotropy_filter, 16.0f) X_CFG_FLOAT(rendering.wireframe_width, 2.5) X_CFG_FLOAT(visuals.game_brightness, 1.0f) X_CFG_FLOAT(visuals.gamma, 2.5f) X_CFG_FLOAT(visuals.ui_brightness, 1.0f) X_CFG_FLOAT_PERCENT(audio.ambient_volume, 1.0f) X_CFG_FLOAT_PERCENT(audio.cutscene_volume, 1.0f) X_CFG_FLOAT_PERCENT(audio.fmv_volume, 1.0f) X_CFG_FLOAT_PERCENT(audio.inventory_ambient_volume, 1.0f) X_CFG_FLOAT_PERCENT(audio.inventory_music_volume, 1.0f) X_CFG_FLOAT_PERCENT(audio.master_volume, 0.8f) X_CFG_FLOAT_PERCENT(audio.music_volume, 1.0f) X_CFG_FLOAT_PERCENT(audio.sound_volume, 1.0f) X_CFG_FLOAT_PERCENT(audio.underwater_ambient_volume, 1.0f) X_CFG_FLOAT_PERCENT(audio.underwater_music_volume, 1.0f) X_CFG_FLOAT_PERCENT(rendering.borders, 0.0f) X_CFG_FLOAT_PERCENT(ui.bar_scale, 1.0f) X_CFG_FLOAT_PERCENT(ui.pickup_scale, 1.0f) X_CFG_FLOAT_PERCENT(ui.text_scale, 1.0f) X_CFG_INT32(config_version, 0) X_CFG_INT32(gameplay.camera_speed, 5) X_CFG_INT32(gameplay.harpoon_recoil, 4) X_CFG_INT32(gameplay.idle_pose_timeout, 60) X_CFG_INT32(gameplay.start_lara_hitpoints, LARA_MAX_HITPOINTS) X_CFG_INT32(gameplay.turbo_speed, 0) X_CFG_INT32(input.controller_layout, INPUT_LAYOUT_DEFAULT) X_CFG_INT32(input.keyboard_layout, INPUT_LAYOUT_DEFAULT) X_CFG_INT32(rendering.fps, 60) X_CFG_INT32(rendering.upscaling_factor, 1) X_CFG_INT32(visuals.fog_end, 30) X_CFG_INT32(visuals.fog_start, 22) X_CFG_INT32(visuals.fov, 80) X_CFG_INT32(window.fs_height, -1) X_CFG_INT32(window.fs_width, -1) X_CFG_INT32(window.height, 720) X_CFG_INT32(window.width, 1280) X_CFG_INT32(window.x, -1) X_CFG_INT32(window.y, -1) X_CFG_RGB888(visuals.fog_color, 0, 0, 0) X_CFG_STRING(language, "en") ================================================ FILE: src/trx/config/map_tr1.def ================================================ #include X_CFG_BOOL(audio.fix_speeches_killing_music, true) X_CFG_BOOL(gameplay.enable_slow_ledge_swing, false) X_CFG_ENUM(gameplay.jump_lock_mode, JUMP_LOCK_TUNED, JUMP_LOCK_MODE) X_CFG_ENUM(visuals.camera_mode, CAMERA_MODE_TR1, CAMERA_MODE) X_CFG_RGB888(visuals.water_color, 0x72, 0xFF, 0xFF) X_CFG_ENUM(ui.inventory_background_style, BK_TRANSPARENT_MEDIUM, BACKGROUND_TYPE) X_CFG_ENUM(ui.stats_background_style, BK_TRANSPARENT_MEDIUM, BACKGROUND_TYPE) X_CFG_ENUM(ui.pause_background_style, BK_TRANSPARENT_DARK, BACKGROUND_TYPE) X_CFG_BOOL(gameplay.enable_boulder_shake, false) X_CFG_BOOL(gameplay.enable_back_slope_stumble, false) X_CFG_BOOL(gameplay.enable_step_roll_boost, true) X_CFG_BOOL(gameplay.enable_timer_in_inventory, false) X_CFG_BOOL(gameplay.fix_water_exit, false) X_CFG_BOOL(ui.enable_smooth_bars, true) X_CFG_DYNAMIC_ENUM(ui.bar_look, "tr1_pc") X_CFG_FLOAT(flow.demo_delay, 16.0f) X_CFG_INT32(gameplay.maximum_save_slots, 25) X_CFG_INT32(gameplay.maximum_quick_save_slots, 1) X_CFG_ENUM(visuals.shadow_type, SHADOW_TYPE_CIRCLE, SHADOW_TYPE) X_CFG_BOOL(gameplay.enable_bouncy_grenades, false) X_CFG_ENUM(gameplay.projectile_area_damage, PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP, PROJECTILE_AREA_DAMAGE) ================================================ FILE: src/trx/config/map_tr2.def ================================================ #include X_CFG_BOOL(audio.fix_speeches_killing_music, false) X_CFG_BOOL(gameplay.enable_slow_ledge_swing, false) X_CFG_ENUM(gameplay.jump_lock_mode, JUMP_LOCK_LEGACY, JUMP_LOCK_MODE) X_CFG_ENUM(visuals.camera_mode, CAMERA_MODE_TR2, CAMERA_MODE) X_CFG_RGB888(visuals.water_color, 0x80, 0xE0, 0xFF) X_CFG_ENUM(ui.inventory_background_style, BK_PATTERN_WAVE, BACKGROUND_TYPE) X_CFG_ENUM(ui.stats_background_style, BK_PATTERN_WAVE, BACKGROUND_TYPE) X_CFG_ENUM(ui.pause_background_style, BK_TRANSPARENT_DARK, BACKGROUND_TYPE) X_CFG_BOOL(gameplay.enable_boulder_shake, true) X_CFG_BOOL(gameplay.enable_back_slope_stumble, false) X_CFG_BOOL(gameplay.enable_step_roll_boost, false) X_CFG_BOOL(gameplay.enable_timer_in_inventory, true) X_CFG_BOOL(gameplay.fix_water_exit, true) X_CFG_BOOL(ui.enable_smooth_bars, false) X_CFG_DYNAMIC_ENUM(ui.bar_look, "tr2_ps1") X_CFG_FLOAT(flow.demo_delay, 30.0f) X_CFG_INT32(gameplay.maximum_save_slots, 24) X_CFG_INT32(gameplay.maximum_quick_save_slots, 1) X_CFG_ENUM(visuals.shadow_type, SHADOW_TYPE_CIRCLE, SHADOW_TYPE) X_CFG_BOOL(gameplay.enable_bouncy_grenades, false) X_CFG_ENUM(gameplay.projectile_area_damage, PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP, PROJECTILE_AREA_DAMAGE) ================================================ FILE: src/trx/config/map_tr3.def ================================================ #include X_CFG_BOOL(audio.fix_speeches_killing_music, false) X_CFG_BOOL(gameplay.enable_slow_ledge_swing, true) X_CFG_ENUM(gameplay.jump_lock_mode, JUMP_LOCK_LEGACY, JUMP_LOCK_MODE) X_CFG_ENUM(visuals.camera_mode, CAMERA_MODE_TR3, CAMERA_MODE) X_CFG_RGB888(visuals.water_color, 0x80, 0xE0, 0xFF) X_CFG_ENUM(ui.inventory_background_style, BK_MONOCHROME, BACKGROUND_TYPE) X_CFG_ENUM(ui.stats_background_style, BK_MONOCHROME, BACKGROUND_TYPE) X_CFG_ENUM(ui.pause_background_style, BK_TRANSPARENT_DARK, BACKGROUND_TYPE) X_CFG_BOOL(gameplay.enable_boulder_shake, true) X_CFG_BOOL(gameplay.enable_back_slope_stumble, true) X_CFG_BOOL(gameplay.enable_step_roll_boost, false) X_CFG_BOOL(gameplay.enable_timer_in_inventory, true) X_CFG_BOOL(gameplay.fix_water_exit, false) X_CFG_BOOL(ui.enable_smooth_bars, false) X_CFG_DYNAMIC_ENUM(ui.bar_look, "tr3_ps1") X_CFG_FLOAT(flow.demo_delay, 30.0f) X_CFG_INT32(gameplay.maximum_save_slots, 24) X_CFG_INT32(gameplay.maximum_quick_save_slots, 1) X_CFG_ENUM(visuals.shadow_type, SHADOW_TYPE_SPRITE, SHADOW_TYPE) X_CFG_BOOL(gameplay.enable_bouncy_grenades, true) X_CFG_ENUM(gameplay.projectile_area_damage, PROJECTILE_AREA_DAMAGE_MULTI_SWEEP, PROJECTILE_AREA_DAMAGE) ================================================ FILE: src/trx/config/option.h ================================================ #pragma once typedef enum { COT_BOOL, COT_INT32, COT_FLOAT, COT_FLOAT_PERCENT, COT_DOUBLE, COT_ENUM, COT_RGB888, COT_STRING, COT_DYNAMIC_ENUM, } CONFIG_OPTION_TYPE; typedef struct { const char *name; CONFIG_OPTION_TYPE type; const void *target; const void *default_value; const void *param; } CONFIG_OPTION; ================================================ FILE: src/trx/config/presets.c ================================================ #include #include #include #include #include #include #include #include #include #include static VECTOR *m_Presets = nullptr; // CONFIG_PRESET static void M_FreePreset(CONFIG_PRESET *const preset) { Memory_FreePointer(&preset->name_gs); for (int32_t i = 0; i < preset->setting_count; i++) { Memory_FreePointer(&preset->keys[i]); Memory_FreePointer(&preset->values[i]); } Memory_FreePointer(&preset->keys); Memory_FreePointer(&preset->values); } static void M_FreeAllPresets(void) { if (m_Presets != nullptr) { for (int32_t i = 0; i < m_Presets->count; i++) { M_FreePreset(Vector_Get(m_Presets, i)); } Vector_Free(m_Presets); m_Presets = nullptr; } } static char *M_SerializeJSONValue(const JSON_VALUE *const val) { if (val == nullptr) { return Memory_DupStr(""); } const char *const str = JSON_ValueGetString(val, nullptr); if (str != nullptr) { return Memory_DupStr(str); } const int bool_val = JSON_ValueGetBool(val, JSON_INVALID_BOOL); if (bool_val != JSON_INVALID_BOOL) { return Memory_DupStr(bool_val ? "true" : "false"); } const JSON_NUMBER *const num = JSON_ValueGetNumber(val); if (num != nullptr && num->number != nullptr) { return Memory_DupStr(num->number); } return Memory_DupStr(""); } static bool M_LoadPreset(const char *const path) { JSON_VALUE *const root = JSONFile_ReadEx(path, false); if (root == nullptr) { LOG_WARNING("Failed to parse preset: %s", path); return false; } const JSON_OBJECT *const root_obj = JSON_ValueAsObject(root); if (root_obj == nullptr) { LOG_WARNING("Preset root is not an object: %s", path); JSON_ValueFree(root); return false; } const char *const name_gs = JSON_ObjectGetString(root_obj, "name_gs", nullptr); if (name_gs == nullptr) { LOG_WARNING("Preset missing name_gs: %s", path); JSON_ValueFree(root); return false; } const JSON_OBJECT *const config_obj = JSON_ObjectGetObject(root_obj, "config"); if (config_obj == nullptr) { LOG_WARNING("Preset missing config object: %s", path); JSON_ValueFree(root); return false; } // Count entries first int32_t count = 0; for (const JSON_OBJECT_ELEMENT *elem = config_obj->start; elem != nullptr; elem = elem->next) { count++; } CONFIG_PRESET preset = { .name_gs = Memory_DupStr(name_gs), .setting_count = count, .keys = count > 0 ? Memory_Alloc(sizeof(char *) * count) : nullptr, .values = count > 0 ? Memory_Alloc(sizeof(char *) * count) : nullptr, }; int32_t i = 0; for (const JSON_OBJECT_ELEMENT *elem = config_obj->start; elem != nullptr; elem = elem->next, i++) { preset.keys[i] = Memory_DupStr(elem->name->string); char *const serialized = M_SerializeJSONValue(elem->value); const CONFIG_OPTION *const opt = Config_GetOptionByPath(preset.keys[i]); preset.values[i] = Config_NormalizeOptionValueString(opt, serialized, false); Memory_Free(serialized); } Vector_Add(m_Presets, &preset); JSON_ValueFree(root); return true; } static void __attribute__((destructor)) M_Shutdown(void) { M_FreeAllPresets(); } void Config_Presets_ScanFiles(void) { M_FreeAllPresets(); m_Presets = Vector_Create(sizeof(CONFIG_PRESET)); char *presets_dir = TRXPath_ExpandVars("%mod_dir%/presets"); if (presets_dir == nullptr || !File_DirExists(presets_dir)) { Memory_FreePointer(&presets_dir); presets_dir = TRXPath_ExpandVars("%base_mod_dir%/presets"); } if (presets_dir == nullptr || !File_DirExists(presets_dir)) { Memory_FreePointer(&presets_dir); presets_dir = TRXPath_ExpandVars("%config_dir%/presets"); } if (presets_dir == nullptr || !File_DirExists(presets_dir)) { Memory_FreePointer(&presets_dir); return; } void *const dir = File_OpenDirectory(presets_dir); if (dir == nullptr) { Memory_FreePointer(&presets_dir); return; } const char *entry_name; while ((entry_name = File_ReadDirectory(dir)) != nullptr) { if (!String_EndsWith(entry_name, ".json5")) { continue; } const char *const full_path = String_FormatStatic("%s/%s", presets_dir, entry_name); M_LoadPreset(full_path); } File_CloseDirectory(dir); Memory_FreePointer(&presets_dir); LOG_INFO("Loaded %d config preset(s)", m_Presets->count); } int32_t Config_Presets_GetCount(void) { return m_Presets != nullptr ? m_Presets->count : 0; } const CONFIG_PRESET *Config_Presets_Get(const int32_t idx) { if (m_Presets == nullptr || idx < 0 || idx >= m_Presets->count) { return nullptr; } return Vector_Get(m_Presets, idx); } void Config_Presets_Apply(const int32_t idx) { const CONFIG_PRESET *const preset = Config_Presets_Get(idx); if (preset == nullptr) { return; } for (int32_t i = 0; i < preset->setting_count; i++) { const CONFIG_OPTION *const opt = Config_GetOptionByPath(preset->keys[i]); if (opt == nullptr) { LOG_WARNING("Preset: unknown config key '%s'", preset->keys[i]); continue; } Config_SetOptionValueFromString(opt, preset->values[i]); } Config_Update(); Config_Write(); } ================================================ FILE: src/trx/config/presets.h ================================================ #pragma once #include // A config preset represents a named set of settings to apply all at once. // Presets are loaded from cfg/presets/*.json5 at startup. typedef struct { char *name_gs; // Flat arrays of config key paths and their string values. char **keys; char **values; int32_t setting_count; } CONFIG_PRESET; // Load all presets from the game's cfg/presets/ directory. void Config_Presets_ScanFiles(void); // Number of loaded presets. int32_t Config_Presets_GetCount(void); // Returns the preset at the given index, or nullptr if out of range. const CONFIG_PRESET *Config_Presets_Get(int32_t idx); // Apply all settings from preset[idx] and write config to disk. void Config_Presets_Apply(int32_t idx); ================================================ FILE: src/trx/config/priv.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_CONFIG_VERSION_CURRENT 1 static void M_LoadKeyboardLayout( JSON_OBJECT *const parent_obj, const INPUT_LAYOUT layout) { char layout_name[20]; sprintf(layout_name, "layout_%d", layout); JSON_ARRAY *const arr = JSON_ObjectGetArray(parent_obj, layout_name); if (arr == nullptr) { return; } for (size_t i = 0; i < arr->length; i++) { JSON_OBJECT *const bind_obj = JSON_ArrayGetObject(arr, i); if (bind_obj == nullptr) { // this can happen on TR1X <= 3.1.1, which is no longer supported LOG_WARNING("unsupported keyboard layout config"); continue; } Input_AssignFromJSONObject(INPUT_BACKEND_KEYBOARD, layout, bind_obj); } } static void M_LoadControllerLayout( JSON_OBJECT *const parent_obj, const INPUT_LAYOUT layout) { char layout_name[20]; sprintf(layout_name, "cntlr_layout_%d", layout); JSON_ARRAY *const arr = JSON_ObjectGetArray(parent_obj, layout_name); if (arr == nullptr) { return; } for (size_t i = 0; i < arr->length; i++) { JSON_OBJECT *const bind_obj = JSON_ArrayGetObject(arr, i); if (bind_obj == nullptr) { // this can happen on TR1X <= 3.1.1, which is no longer supported LOG_WARNING("unsupported controller layout config"); continue; } Input_AssignFromJSONObject(INPUT_BACKEND_CONTROLLER, layout, bind_obj); } } static void M_LoadLegacyInputConfig(JSON_OBJECT *const root_obj) { for (INPUT_LAYOUT layout = INPUT_LAYOUT_CUSTOM_1; layout < INPUT_LAYOUT_NUMBER_OF; layout++) { M_LoadKeyboardLayout(root_obj, layout); M_LoadControllerLayout(root_obj, layout); } } static void M_LoadInputLayout( JSON_OBJECT *const parent_obj, const INPUT_BACKEND backend, const INPUT_LAYOUT layout) { char layout_name[20]; sprintf(layout_name, "layout_%d", layout); JSON_ARRAY *const arr = JSON_ObjectGetArray(parent_obj, layout_name); if (arr == nullptr) { return; } for (size_t i = 0; i < arr->length; i++) { JSON_OBJECT *const bind_obj = JSON_ArrayGetObject(arr, i); ASSERT(bind_obj != nullptr); Input_AssignFromJSONObject(backend, layout, bind_obj); } } static void M_LoadInputConfig(JSON_OBJECT *const root_obj) { JSON_OBJECT *const input_obj = JSON_ObjectGetObject(root_obj, "input"); if (input_obj == nullptr) { return; } JSON_OBJECT *const keyboard_obj = JSON_ObjectGetObject(input_obj, "keyboard"); JSON_OBJECT *const controller_obj = JSON_ObjectGetObject(input_obj, "controller"); for (INPUT_LAYOUT layout = INPUT_LAYOUT_CUSTOM_1; layout < INPUT_LAYOUT_NUMBER_OF; layout++) { if (keyboard_obj != nullptr) { M_LoadInputLayout(keyboard_obj, INPUT_BACKEND_KEYBOARD, layout); } if (controller_obj != nullptr) { M_LoadInputLayout(controller_obj, INPUT_BACKEND_CONTROLLER, layout); } } } static void M_DumpInputLayout( JSON_OBJECT *const parent_obj, const INPUT_BACKEND backend, const INPUT_LAYOUT layout) { JSON_ARRAY *const arr = JSON_ArrayNew(); bool has_elements = false; for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) { for (int32_t slot = 0; slot < INPUT_BINDING_SLOTS; slot++) { JSON_OBJECT *const bind_obj = JSON_ObjectNew(); if (Input_AssignToJSONObject( backend, layout, bind_obj, role, slot)) { has_elements = true; JSON_ArrayAppendObject(arr, bind_obj); } else { JSON_ObjectFree(bind_obj); } } } if (has_elements) { char layout_name[20]; sprintf(layout_name, "layout_%d", layout); JSON_ObjectAppendArray(parent_obj, layout_name, arr); } else { JSON_ArrayFree(arr); } } static void M_DumpInputConfig(JSON_OBJECT *const root_obj) { JSON_OBJECT *const input_obj = JSON_ObjectNew(); JSON_OBJECT *const keyboard_obj = JSON_ObjectNew(); JSON_OBJECT *const controller_obj = JSON_ObjectNew(); JSON_ObjectAppendObject(root_obj, "input", input_obj); JSON_ObjectAppendObject(input_obj, "keyboard", keyboard_obj); JSON_ObjectAppendObject(input_obj, "controller", controller_obj); for (INPUT_LAYOUT layout = INPUT_LAYOUT_CUSTOM_1; layout < INPUT_LAYOUT_NUMBER_OF; layout++) { M_DumpInputLayout(keyboard_obj, INPUT_BACKEND_KEYBOARD, layout); M_DumpInputLayout(controller_obj, INPUT_BACKEND_CONTROLLER, layout); } } static void M_MigrateBarColorName(char **const value_ptr) { const char *value = *value_ptr; const char *new_value = nullptr; if (value == nullptr) { return; } if (String_Equivalent(value, "gold")) { new_value = "brown"; } else if (String_Equivalent(value, "green")) { new_value = "teal"; } else if (String_Equivalent(value, "gold2")) { new_value = "yellow"; } else if (String_Equivalent(value, "blue2")) { new_value = "cyan"; } else if (String_Equivalent(value, "green2")) { new_value = "green"; } else if (String_Equivalent(value, "gold-green")) { new_value = "yellow-green"; } if (new_value != nullptr) { Memory_FreePointer(value_ptr); *value_ptr = Memory_DupStr(new_value); } } typedef enum { M_LEGACY_STATS_DETAILS_MINIMAL, M_LEGACY_STATS_DETAILS_DETAILED, M_LEGACY_STATS_DETAILS_FULL, } M_LEGACY_STATS_DETAILS; static bool M_TryGetLegacyStatsDetails( JSON_OBJECT *const parent_obj, M_LEGACY_STATS_DETAILS *const out_value) { const JSON_VALUE *const value = JSON_ObjectGetValue(parent_obj, "stat_detail_mode"); if (value == nullptr) { return false; } if (value->type == JSON_TYPE_STRING) { const char *const value_str = JSON_ValueGetString(value, nullptr); if (String_Equivalent(value_str, "minimal")) { *out_value = M_LEGACY_STATS_DETAILS_MINIMAL; return true; } if (String_Equivalent(value_str, "detailed")) { *out_value = M_LEGACY_STATS_DETAILS_DETAILED; return true; } if (String_Equivalent(value_str, "full")) { *out_value = M_LEGACY_STATS_DETAILS_FULL; return true; } return false; } if (value->type == JSON_TYPE_NUMBER) { switch (JSON_ValueGetInt(value, M_LEGACY_STATS_DETAILS_FULL)) { case M_LEGACY_STATS_DETAILS_MINIMAL: *out_value = M_LEGACY_STATS_DETAILS_MINIMAL; return true; case M_LEGACY_STATS_DETAILS_DETAILED: *out_value = M_LEGACY_STATS_DETAILS_DETAILED; return true; case M_LEGACY_STATS_DETAILS_FULL: *out_value = M_LEGACY_STATS_DETAILS_FULL; return true; default: break; } } return false; } static void M_MigrateLegacyStatsOptions(JSON_OBJECT *const parent_obj) { M_LEGACY_STATS_DETAILS legacy_details = M_LEGACY_STATS_DETAILS_FULL; const bool has_legacy_details = M_TryGetLegacyStatsDetails(parent_obj, &legacy_details); if (!has_legacy_details) { return; } if (JSON_ObjectGetValue(parent_obj, "style") == nullptr) { g_Config.ui.stats.style = g_TRVersion == 1 && legacy_details != M_LEGACY_STATS_DETAILS_FULL ? STATS_STYLE_BARE : STATS_STYLE_BORDERED; } if (JSON_ObjectGetValue(parent_obj, "show_totals") == nullptr) { g_Config.ui.stats.show_totals = legacy_details != M_LEGACY_STATS_DETAILS_MINIMAL; } if (legacy_details == M_LEGACY_STATS_DETAILS_FULL) { return; } if (JSON_ObjectGetValue(parent_obj, "show_time_taken") == nullptr) { g_Config.ui.stats.show_time_taken = true; } if (JSON_ObjectGetValue(parent_obj, "show_secrets") == nullptr) { g_Config.ui.stats.show_secrets = true; } if (JSON_ObjectGetValue(parent_obj, "show_crystals") == nullptr) { g_Config.ui.stats.show_crystals = false; } if (JSON_ObjectGetValue(parent_obj, "show_pickups") == nullptr) { g_Config.ui.stats.show_pickups = g_TRVersion == 1; } if (JSON_ObjectGetValue(parent_obj, "show_kills") == nullptr) { g_Config.ui.stats.show_kills = true; } if (JSON_ObjectGetValue(parent_obj, "show_ammo") == nullptr) { g_Config.ui.stats.show_ammo = g_TRVersion != 1; } if (JSON_ObjectGetValue(parent_obj, "show_medipacks_used") == nullptr) { g_Config.ui.stats.show_medipacks_used = g_TRVersion != 1; } if (JSON_ObjectGetValue(parent_obj, "show_distance_travelled") == nullptr) { g_Config.ui.stats.show_distance_travelled = g_TRVersion != 1; } } static void M_LoadLegacyOptions(JSON_OBJECT *const parent_obj) { #define L_READ_BOOL(target, key) \ target = JSON_ObjectGetBool(parent_obj, key, target) #define L_READ_INT(target, key) \ target = JSON_ObjectGetInt(parent_obj, key, target) // TR1X 2.16..4.5.1 load_current_music { const JSON_VALUE *const value = JSON_ObjectGetValue(parent_obj, "load_current_music"); if (JSON_ValueIsTrue(value)) { g_Config.audio.music_load_condition = MUSIC_LOAD_CONDITION_NON_AMBIENT; } else if (JSON_ValueIsFalse(value)) { g_Config.audio.music_load_condition = MUSIC_LOAD_CONDITION_NEVER; } } // Legacy bool loading screens option. { const JSON_VALUE *const value = JSON_ObjectGetValue(parent_obj, "enable_loading_screens"); if (JSON_ValueIsTrue(value)) { g_Config.gameplay.loading_screens = LOADING_SCREENS_ALWAYS; } else if (JSON_ValueIsFalse(value)) { g_Config.gameplay.loading_screens = LOADING_SCREENS_DISABLED; } } // TR1X ..4.7 L_READ_BOOL(g_Config.window.is_fullscreen, "enable_fullscreen"); L_READ_BOOL(g_Config.window.is_maximized, "enable_maximized"); L_READ_BOOL(g_Config.gameplay.enable_walk_to_items, "walk_to_items"); L_READ_BOOL( g_Config.gameplay.enable_inverted_look, "enabled_inverted_look"); L_READ_INT(g_Config.window.x, "window_x"); L_READ_INT(g_Config.window.y, "window_y"); L_READ_INT(g_Config.window.width, "window_width"); L_READ_INT(g_Config.window.height, "window_height"); L_READ_INT(g_Config.input.keyboard_layout, "layout"); L_READ_INT(g_Config.input.controller_layout, "cntlr_layout"); // TR1X ..4.9 L_READ_BOOL(g_Config.gameplay.enable_cutscenes, "enable_cine"); L_READ_BOOL(g_Config.gameplay.enable_legal, "enable_eidos_logo"); // TR1X ..4.11, TR2X ..1.1: 0…10 scale volumes to 0.0f…1.0f { const JSON_VALUE *const value = JSON_ObjectGetValue(parent_obj, "sound_volume"); const JSON_NUMBER *const num = value != nullptr ? JSON_ValueGetNumber(value) : nullptr; if (num != nullptr && strchr(num->number, '.') == nullptr) { g_Config.audio.sound_volume = JSON_ValueGetInt(value, 0) / 10.0f; } } { const JSON_VALUE *const value = JSON_ObjectGetValue(parent_obj, "music_volume"); const JSON_NUMBER *const num = value != nullptr ? JSON_ValueGetNumber(value) : nullptr; if (value != nullptr && value->type == JSON_TYPE_NUMBER && strchr(num->number, '.') == nullptr) { g_Config.audio.music_volume = JSON_ValueGetInt(value, 0) / 10.0f; } } // TR1X ..4.11: convert wall bug fix { const JSON_VALUE *const value = JSON_ObjectGetValue(parent_obj, "fix_wall_jump_glitch"); if (JSON_ValueIsTrue(value)) { g_Config.gameplay.wall_glitch_mode = WALL_GLITCH_FIXED; } } // TR1X ..4.13: convert enhanced look to enum type { const JSON_VALUE *const value = JSON_ObjectGetValue(parent_obj, "enable_enhanced_look"); if (JSON_ValueIsTrue(value)) { g_Config.gameplay.look_mode = LOOK_MODE_UNRESTRICTED; } else if (JSON_ValueIsFalse(value)) { g_Config.gameplay.look_mode = LOOK_MODE_RESTRICTED; } } // TR1X ..4.15, TR2X 1.5 if (JSON_ObjectGetValue(parent_obj, "ambient_volume") == nullptr) { g_Config.audio.ambient_volume = g_Config.audio.music_volume; } if (JSON_ObjectGetValue(parent_obj, "cutscene_volume") == nullptr) { g_Config.audio.cutscene_volume = g_Config.audio.music_volume; } if (JSON_ObjectGetValue(parent_obj, "fmv_volume") == nullptr) { g_Config.audio.fmv_volume = g_Config.audio.music_volume; } // TR2X 1.6 L_READ_BOOL(g_Config.audio.enable_ps1_sfx, "enable_barefoot_sfx"); // TR1X ..4.16 L_READ_BOOL(g_Config.audio.enable_ps1_sfx, "enable_ps_uzi_sfx"); L_READ_BOOL( g_Config.audio.fix_chainblock_secret_sound, "fix_tihocan_secret_sound"); L_READ_BOOL(g_Config.input.enable_buffering_inventory, "enable_buffering"); L_READ_BOOL(g_Config.input.enable_buffering_func_keys, "enable_buffering"); // TR1X ..4.16, TR2X ..1.6 { const JSON_VALUE *const value = JSON_ObjectGetValue(parent_obj, "revert_to_pistols"); if (JSON_ValueIsTrue(value)) { g_Config.gameplay.remember_gun_status = false; } else if (JSON_ValueIsFalse(value)) { g_Config.gameplay.remember_gun_status = true; } } // TR2X ..1.5.1 { const JSON_VALUE *const value = JSON_ObjectGetValue(parent_obj, "fix_pickup_drift_glitch"); if (JSON_ValueIsFalse(value)) { g_Config.gameplay.fix_lara_pickup_embed = false; } } // TRX ..1.2: split brightness into game and UI fields. { const JSON_VALUE *const old_value = JSON_ObjectGetValue(parent_obj, "brightness"); if (old_value != nullptr) { const float old_brightness = JSON_ValueGetDouble( old_value, g_Config.visuals.game_brightness); if (JSON_ObjectGetValue(parent_obj, "game_brightness") == nullptr) { g_Config.visuals.game_brightness = old_brightness; } if (JSON_ObjectGetValue(parent_obj, "ui_brightness") == nullptr) { g_Config.visuals.ui_brightness = old_brightness; } } } // TRX legacy: "round shadows" boolean to enum shadow type. if (JSON_ObjectGetValue(parent_obj, "shadow_type") == nullptr) { const JSON_VALUE *const value = JSON_ObjectGetValue(parent_obj, "enable_round_shadow"); if (JSON_ValueIsTrue(value)) { g_Config.visuals.shadow_type = SHADOW_TYPE_CIRCLE; } else if (JSON_ValueIsFalse(value)) { g_Config.visuals.shadow_type = SHADOW_TYPE_OCTAGON; } } // Pre-1.2 UI bar look and color migration; one-shot via config version. if (g_Config.config_version >= 0 && g_Config.config_version < 1) { { const char *const value = JSON_ObjectGetString(parent_obj, "bar_look", ""); const char *new_value = nullptr; if (String_Equivalent(value, "tr1")) { new_value = "tr1_pc"; } else if (String_Equivalent(value, "tr2")) { new_value = "tr2_pc"; } else if (String_Equivalent(value, "tr3")) { new_value = "tr3_pc"; } else if (String_Equivalent(value, "ps1")) { new_value = "tr2_ps1"; } if (new_value != nullptr) { Memory_FreePointer(&g_Config.ui.bar_look); g_Config.ui.bar_look = Memory_DupStr(new_value); } } M_MigrateBarColorName(&g_Config.ui.lara_health_bar.color); M_MigrateBarColorName(&g_Config.ui.lara_health_bar.color_ps1); M_MigrateBarColorName(&g_Config.ui.lara_health_bar.poison_color); M_MigrateBarColorName(&g_Config.ui.lara_health_bar.poison_color_ps1); M_MigrateBarColorName(&g_Config.ui.lara_air_bar.color); M_MigrateBarColorName(&g_Config.ui.lara_air_bar.color_ps1); M_MigrateBarColorName(&g_Config.ui.lara_sprint_bar.color); M_MigrateBarColorName(&g_Config.ui.lara_sprint_bar.color_ps1); M_MigrateBarColorName(&g_Config.ui.lara_exposure_bar.color); M_MigrateBarColorName(&g_Config.ui.lara_exposure_bar.color_ps1); M_MigrateBarColorName(&g_Config.ui.enemy_health_bar.color); M_MigrateBarColorName(&g_Config.ui.enemy_health_bar.color_ps1); M_MigrateBarColorName(&g_Config.ui.enemy_health_bar.color_allies); M_MigrateBarColorName(&g_Config.ui.enemy_health_bar.color_allies_ps1); } // Pre 1.3 Fixed glide camera option was only visible to TR2 and defaulted // to false in TR1. Now a regular toggle for all camera modes. if (g_TRVersion == 2) { L_READ_BOOL(g_Config.visuals.enable_glide_cameras, "fix_glide_cameras"); } // TRX ..1.4: sunglasses on/off changed to mode. if (JSON_ObjectGetValue(parent_obj, "sunglasses_mode") == nullptr) { const JSON_VALUE *const value = JSON_ObjectGetValue(parent_obj, "enable_sunglasses"); g_Config.visuals.sunglasses_mode = JSON_ValueIsTrue(value) ? SUNGLASSES_MODE_OPAQUE : SUNGLASSES_MODE_OFF; } if (JSON_ObjectGetValue(parent_obj, "show_level_header") == nullptr) { L_READ_BOOL( g_Config.ui.stats.show_level_header, "enable_stats_level_header"); } if (JSON_ObjectGetValue(parent_obj, "show_deaths") == nullptr) { L_READ_BOOL(g_Config.ui.stats.show_deaths, "enable_deaths_counter"); } M_MigrateLegacyStatsOptions(parent_obj); if (g_Config.config_version >= 0 && g_Config.config_version < M_CONFIG_VERSION_CURRENT) { g_Config.config_version = M_CONFIG_VERSION_CURRENT; } #undef L_READ_BOOL #undef L_READ_INT } void Config_LoadFromJSON(JSON_OBJECT *root_obj) { ConfigFile_LoadOptions(root_obj, Config_GetOptionMap()); if (Gym_TrackManager_HasStats(GYM_TRACK_ASSAULT)) { ConfigFile_LoadGymTrackStats( root_obj, "assault_stats", &g_Config.profile.assault_stats); } if (Gym_TrackManager_HasStats(GYM_TRACK_QUAD)) { ConfigFile_LoadGymTrackStats( root_obj, "racetrack_stats", &g_Config.profile.racetrack_stats); } M_LoadLegacyInputConfig(root_obj); M_LoadInputConfig(root_obj); M_LoadLegacyOptions(root_obj); } void Config_DumpToJSON(JSON_OBJECT *root_obj) { ConfigFile_DumpOptions(root_obj, Config_GetOptionMap()); if (Gym_TrackManager_HasStats(GYM_TRACK_ASSAULT)) { ConfigFile_DumpGymTrackStats( root_obj, "assault_stats", &g_Config.profile.assault_stats); } if (Gym_TrackManager_HasStats(GYM_TRACK_QUAD)) { ConfigFile_DumpGymTrackStats( root_obj, "racetrack_stats", &g_Config.profile.racetrack_stats); } M_DumpInputConfig(root_obj); } void Config_Sanitize(void) { if (g_Config.rendering.aspect_mode != ASPECT_MODE_ANY && g_Config.rendering.aspect_mode != ASPECT_MODE_16_9 && g_Config.rendering.aspect_mode != ASPECT_MODE_16_10) { g_Config.rendering.aspect_mode = ASPECT_MODE_4_3; } CLAMP(g_Config.audio.master_volume, 0.0f, 1.0f); CLAMP(g_Config.audio.sound_volume, 0.0f, 1.0f); CLAMP(g_Config.audio.music_volume, 0.0f, 1.0f); CLAMP(g_Config.audio.inventory_music_volume, 0.0f, 1.0f); CLAMP(g_Config.audio.underwater_music_volume, 0.0f, 1.0f); CLAMP(g_Config.audio.ambient_volume, 0.0f, 1.0f); CLAMP(g_Config.audio.inventory_ambient_volume, 0.0f, 1.0f); CLAMP(g_Config.audio.underwater_ambient_volume, 0.0f, 1.0f); CLAMP(g_Config.audio.cutscene_volume, 0.0f, 1.0f); CLAMP(g_Config.audio.fmv_volume, 0.0f, 1.0f); CLAMP(g_Config.input.keyboard_layout, 0, INPUT_LAYOUT_NUMBER_OF - 1); CLAMP(g_Config.input.controller_layout, 0, INPUT_LAYOUT_NUMBER_OF - 1); CLAMP( g_Config.gameplay.turbo_speed, CLOCK_TURBO_SPEED_MIN, CLOCK_TURBO_SPEED_MAX); CLAMP(g_Config.gameplay.start_lara_hitpoints, 1, LARA_MAX_HITPOINTS); CLAMP(g_Config.gameplay.camera_speed, 1, 10); CLAMP(g_Config.gameplay.idle_pose_timeout, 0, 1200); CLAMP(g_Config.rendering.wireframe_width, 1.0, 100.0); CLAMP(g_Config.rendering.upscaling_factor, 1, 8); CLAMP(g_Config.rendering.borders, 0.0, 0.45); CLAMP(g_Config.ui.bar_scale, 0.5, 2.0); CLAMP(g_Config.ui.text_scale, 0.5, 2.0); CLAMP(g_Config.ui.pickup_scale, 0.5, 2.0); CLAMP(g_Config.visuals.fog_start, 1, 100); CLAMP(g_Config.visuals.fog_end, 1, 100); CLAMP(g_Config.visuals.fov, 30, 150); CLAMPL(g_Config.gameplay.maximum_save_slots, 0); CLAMPL(g_Config.gameplay.maximum_quick_save_slots, 0); CLAMP(g_Config.visuals.shadow_type, 0, SHADOW_TYPE_NUMBER_OF - 1); CLAMP(g_Config.visuals.blood_effects, 0, BLOOD_EFFECTS_NUMBER_OF - 1); CLAMP(g_Config.gameplay.loading_screens, 0, LOADING_SCREENS_NEW_GAMES); if (g_Config.rendering.fps != 30 && g_Config.rendering.fps != 60) { g_Config.rendering.fps = 30; } CLAMP( g_Config.visuals.game_brightness, CONFIG_MIN_BRIGHTNESS, CONFIG_MAX_BRIGHTNESS); CLAMP( g_Config.visuals.ui_brightness, CONFIG_MIN_BRIGHTNESS, CONFIG_MAX_BRIGHTNESS); CLAMP(g_Config.visuals.gamma, CONFIG_MIN_GAMMA, CONFIG_MAX_GAMMA); CLAMPL(g_Config.rendering.anisotropy_filter, 1.0); } ================================================ FILE: src/trx/config/priv.h ================================================ #pragma once #include void Config_LoadFromJSON(JSON_OBJECT *root_obj); void Config_DumpToJSON(JSON_OBJECT *root_obj); void Config_Sanitize(void); ================================================ FILE: src/trx/config/types.h ================================================ #pragma once #include #include #include #include typedef struct { uint32_t time; uint32_t attempt_num; } GYM_TRACK_ENTRY; typedef struct { GYM_TRACK_ENTRY entries[MAX_ASSAULT_TIMES]; uint32_t total_attempts; } GYM_TRACK_STATS; typedef struct { // This signifies whether the config was already read from disk. bool loaded; // This holds paths passed to Config_Read(), so that Config_Write() knows // where to save the updates. char *default_path; char *enforced_path; // This field is used to force trigger a change event for fields that are // not stored in the CONFIG struct. bool dirty; // Start of user fields int32_t config_version; char *language; struct { bool new_game_plus_unlock; GYM_TRACK_STATS assault_stats; GYM_TRACK_STATS racetrack_stats; } profile; struct { INPUT_BACKEND backend; // Not decisive - mostly for UI visuals union { struct { int32_t keyboard_layout; int32_t controller_layout; }; int32_t layout[INPUT_BACKEND_NUMBER_OF]; }; bool enable_tr3_sidesteps; bool enable_responsive_passport; bool enable_buffering_func_keys; bool enable_buffering_inventory; QUICK_GUNS_MODE quick_guns_mode; } input; struct { bool is_fullscreen; bool is_maximized; int32_t x; int32_t y; int32_t width; int32_t height; int32_t fs_width; int32_t fs_height; } window; struct { bool enable_fade_effects; bool enable_exit_fade_effects; int32_t fov; CAMERA_MODE camera_mode; bool enable_glide_cameras; float game_brightness; float ui_brightness; float gamma; bool enable_reflections; bool enable_3d_pickups; bool enable_braid; bool enable_breeze; bool enable_gun_lighting; bool enable_fire_lighting; bool enable_shotgun_flash; bool enable_responsive_mesh_tint; char *lara_outfit; SUNGLASSES_MODE sunglasses_mode; SHADOW_TYPE shadow_type; BLOOD_EFFECTS blood_effects; bool enable_skybox; bool enable_weather; bool enable_footprints; bool enable_ps1_crystals; bool fix_item_rots; bool fix_animated_sprites; bool fix_texture_issues; RGB_888 water_color; bool fog_transparency; RGB_888 fog_color; int32_t fog_start; int32_t fog_end; } visuals; struct { bool enable_game_ui; bool enable_photo_mode_ui; bool enable_wraparound; bool enable_fps_counter; bool show_pickups_overlay; bool show_title_version; float text_scale; float bar_scale; float pickup_scale; UI_STYLE menu_style; struct { STATS_STYLE style; bool show_totals; bool show_level_header; bool show_time_taken; bool show_secrets; bool show_crystals; bool show_pickups; bool show_kills; bool show_ammo; bool show_medipacks_used; bool show_distance_travelled; bool show_deaths; } stats; BACKGROUND_TYPE inventory_background_style; BACKGROUND_TYPE stats_background_style; BACKGROUND_TYPE pause_background_style; bool inventory_fade_effects; bool stats_fade_effects; bool pause_fade_effects; bool enable_smooth_bars; char *bar_look; bool show_bars; bool enable_bar_flashing; struct { UI_ELEMENT_LOCATION location; char *color; char *color_ps1; char *poison_color; char *poison_color_ps1; } lara_health_bar; struct { UI_ELEMENT_LOCATION location; char *color; char *color_ps1; } lara_air_bar, lara_sprint_bar, lara_exposure_bar; struct { BAR_SHOW_MODE show_mode; UI_ELEMENT_LOCATION location; char *color; char *color_ps1; char *color_allies; char *color_allies_ps1; } enemy_health_bar; struct { UI_ELEMENT_LOCATION location; } ammo_counter; } ui; struct { float master_volume; float sound_volume; float music_volume; float inventory_music_volume; float underwater_music_volume; float ambient_volume; float inventory_ambient_volume; float underwater_ambient_volume; float cutscene_volume; float fmv_volume; bool fix_chainblock_secret_sound; bool fix_secrets_killing_music; bool fix_speeches_killing_music; bool enable_lara_mic; bool enable_music_in_menu; bool enable_music_in_inventory; bool enable_ps1_sfx; bool enable_pitched_sounds; bool load_music_triggers; bool enable_underwater_anim_sfx; bool mute_out_of_focus; MUSIC_LOAD_CONDITION music_load_condition; } audio; struct { bool disable_healing_between_levels; bool disable_medpacks; bool disable_extra_guns; bool enable_pickup_aids; bool enable_save_crystals; bool enable_enhanced_saves; bool enable_cheats; bool enable_console; bool enable_game_modes; bool enable_play_previous_levels; bool enable_fmv; bool enable_legal; bool enable_credits; bool enable_cinematics; bool enable_cutscenes; bool enable_demo; LOADING_SCREENS_MODE loading_screens; bool enable_compass_stats; bool enable_total_stats; bool pause_on_focus_lost; bool enable_jump_twists; bool enable_uw_roll; bool enable_crouch_roll; bool enable_tr2_swimming; bool enable_wading; bool enable_tr2_swim_cancel; bool enable_tr2_jumping; bool enable_swing_cancel; bool enable_smooth_wall_deflect; bool enable_lean_jumping; bool enable_step_roll_boost; bool enable_slide_to_run; bool enable_back_slope_stumble; bool enable_neutral_twists; bool enable_controlled_drops; bool enable_ledge_jumps; bool enable_crawling; bool enable_responsive_crawl; bool enable_crawl_jump; bool enable_crawl_tilt; bool enable_sprint; bool enable_responsive_sprint; bool enable_toggle_crouch; bool enable_toggle_sprint; bool enable_slow_ledge_swing; int32_t idle_pose_timeout; bool enable_idle_pose_camera; bool enable_soft_statics; bool enable_bouncy_grenades; bool enable_auto_item_selection; bool enable_manual_camera; bool enable_item_examining; bool enable_target_change; bool enable_walk_to_items; bool restore_ps1_enemies; bool enable_ally_targeting; bool enable_enemy_rotation; bool enable_killer_pushblocks; bool enable_boulder_shake; bool enable_body_bags; ALLY_HOSTILITY_POLICY ally_hostility_policy; CREATURE_DROWN_POLICY creature_drown_policy; bool enable_timer_in_inventory; LOOK_MODE look_mode; bool enable_inverted_look; bool remember_gun_status; int32_t turbo_speed; int32_t camera_speed; int32_t start_lara_hitpoints; int32_t maximum_save_slots; int32_t maximum_quick_save_slots; int32_t harpoon_recoil; JUMP_LOCK_MODE jump_lock_mode; TARGET_LOCK_MODE target_mode; bool fix_qwop_glitch; bool fix_step_glitch; bool fix_item_duplication_glitch; bool fix_descending_glitch; bool fix_lara_pickup_embed; bool fix_water_exit; WALL_GLITCH_MODE wall_glitch_mode; bool fix_wall_geometry; bool fix_alligator_ai; bool disable_trex_collision; bool change_pierre_spawn; bool fix_m16_accuracy; bool fix_flare_throw_priority; bool fix_free_flare_glitch; bool fix_walk_run_jump; bool fix_wade_wall_hit; bool fix_floor_data_issues; bool fix_bridge_collision; bool fix_bear_ai; bool fix_monkey_pickup_priority; bool fix_pipeman_aim; PROJECTILE_AREA_DAMAGE projectile_area_damage; } gameplay; struct { ASPECT_MODE aspect_mode; int32_t fps; bool enable_trapezoid_filter; bool enable_lighting; bool enable_textures; TEXTURE_FILTER ui_filter; TEXTURE_FILTER texture_filter; TEXTURE_FILTER upscaling_filter; bool enable_wireframe; float wireframe_width; bool enable_vsync; float anisotropy_filter; SCREENSHOT_FORMAT screenshot_format; LIGHTING_CONTRAST lighting_contrast; BILLBOARD_LOCK_MODE sprite_lock_mode; int32_t upscaling_factor; float borders; } rendering; struct { bool enable_debug_triggers; bool enable_debug_portals; bool enable_debug_room_clip; bool enable_debug_spheres; bool enable_debug_bounding_boxes; bool enable_debug_pos; bool enable_debug_anim; bool enable_debug_camera; bool enable_debug_status; bool enable_review_markers; bool enable_invulnerability; bool enable_endless_sprint; bool enable_endless_flare_time; } debug; struct { bool lockout_option_ring; bool load_save_disabled; bool play_any_level; float demo_delay; bool cheat_keys; } flow; } CONFIG; ================================================ FILE: src/trx/config/vars.c ================================================ #include CONFIG g_Config = {}; CONFIG g_SavedConfig = {}; ================================================ FILE: src/trx/config/vars.h ================================================ #pragma once #include extern CONFIG g_Config; extern CONFIG g_SavedConfig; ================================================ FILE: src/trx/config.h ================================================ #pragma once #include #include #include ================================================ FILE: src/trx/core/benchmark.c ================================================ #include #include #include static void M_Log( BENCHMARK *const b, const char *file, int32_t line, const char *func, Uint64 current, const char *message, const bool closing) { const Uint64 freq = SDL_GetPerformanceFrequency(); const double elapsed_start = (double)(current - b->start) * 1000.0 / (double)freq; const double elapsed_last = (double)(current - b->last) * 1000.0 / (double)freq; if (closing) { if (message == nullptr) { Log_Message( LOG_LEVEL_DEBUG, file, line, func, "took %.02f ms", elapsed_start); } else { Log_Message( LOG_LEVEL_DEBUG, file, line, func, "%s: took %.02f ms", message, elapsed_start); } } else { if (message == nullptr) { Log_Message( LOG_LEVEL_DEBUG, file, line, func, "took %.02f ms (%.02f ms)", elapsed_start, elapsed_last); } else { Log_Message( LOG_LEVEL_DEBUG, file, line, func, "%s: took %.02f ms (%.02f ms)", message, elapsed_start, elapsed_last); } } } BENCHMARK Benchmark_Start(void) { const Uint64 perf = SDL_GetPerformanceCounter(); return (BENCHMARK) { .start = perf, .last = perf, }; } void Benchmark_Tick_Impl( BENCHMARK *const b, const char *const file, const int32_t line, const char *const func, const char *const message) { const Uint64 current = SDL_GetPerformanceCounter(); M_Log(b, file, line, func, current, message, false); b->last = current; } void Benchmark_End_Impl( BENCHMARK *b, const char *const file, const int32_t line, const char *const func, const char *const message) { const Uint64 current = SDL_GetPerformanceCounter(); M_Log(b, file, line, func, current, message, true); } ================================================ FILE: src/trx/core/benchmark.h ================================================ #pragma once #include typedef struct { Uint64 start; Uint64 last; } BENCHMARK; BENCHMARK Benchmark_Start(void); #define Benchmark_End(b, ...) \ Benchmark_End_Impl(b, __FILE__, __LINE__, __func__, __VA_ARGS__) #define Benchmark_Tick(b, ...) \ Benchmark_Tick_Impl(b, __FILE__, __LINE__, __func__, __VA_ARGS__) void Benchmark_End_Impl( BENCHMARK *b, const char *file, int32_t line, const char *func, const char *message); void Benchmark_Tick_Impl( BENCHMARK *b, const char *file, int32_t line, const char *func, const char *message); ================================================ FILE: src/trx/core/bson/enum.h ================================================ #pragma once typedef enum { BSON_PARSE_ERROR_NONE = 0, BSON_PARSE_ERROR_INVALID_VALUE, BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER, BSON_PARSE_ERROR_UNEXPECTED_TRAILING_BYTES, BSON_PARSE_ERROR_UNKNOWN, } BSON_PARSE_ERROR; ================================================ FILE: src/trx/core/bson/parse.c ================================================ #include #include #include #include #include #include #include typedef struct { const char *src; size_t size; size_t offset; char *data; char *dom; size_t dom_size; size_t data_size; size_t error; } M_STATE; static bool M_GetValueSize(M_STATE *state, uint8_t marker); static void M_HandleValue(M_STATE *state, JSON_VALUE *value, uint8_t marker); static int32_t M_ReadI32(const char *const src) { int32_t value = 0; memcpy(&value, src, sizeof(value)); return value; } static double M_ReadDouble(const char *const src) { double value = 0.0; memcpy(&value, src, sizeof(value)); return value; } static bool M_GetObjectKeySize(M_STATE *state) { ASSERT(state != nullptr); while (state->src[state->offset]) { state->data_size++; state->offset++; } state->data_size++; state->offset++; return true; } static bool M_GetNullValueSize(M_STATE *state) { ASSERT(state != nullptr); return true; } static bool M_GetBoolValueSize(M_STATE *state) { ASSERT(state != nullptr); if (state->offset + sizeof(uint8_t) > state->size) { state->error = BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return false; } switch (state->src[state->offset]) { case 0x00: break; case 0x01: break; default: state->error = BSON_PARSE_ERROR_INVALID_VALUE; return false; } state->offset++; return true; } static bool M_GetInt32ValueSize(M_STATE *state) { ASSERT(state != nullptr); if (state->offset + sizeof(int32_t) > state->size) { state->error = BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return false; } int32_t num = M_ReadI32(&state->src[state->offset]); state->offset += sizeof(int32_t); state->dom_size += sizeof(JSON_NUMBER); state->data_size += snprintf(nullptr, 0, "%d", num) + 1; return true; } static bool M_GetDoubleValueSize(M_STATE *state) { ASSERT(state != nullptr); if (state->offset + sizeof(double) > state->size) { state->error = BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return false; } double num = M_ReadDouble(&state->src[state->offset]); state->offset += sizeof(double); state->dom_size += sizeof(JSON_NUMBER); state->data_size += snprintf(nullptr, 0, "%f", num) + 1; return true; } static bool M_GetStringValueSize(M_STATE *state) { ASSERT(state != nullptr); if (state->offset + sizeof(int32_t) > state->size) { state->error = BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return false; } int32_t size = M_ReadI32(&state->src[state->offset]); state->offset += sizeof(int32_t); if (state->offset + size > state->size) { state->error = BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return false; } if (state->src[state->offset + size - 1] != '\0') { state->error = BSON_PARSE_ERROR_INVALID_VALUE; return false; } state->offset += size; state->dom_size += sizeof(JSON_STRING); state->data_size += size; return true; } static bool M_GetArrayElementWrappedSize(M_STATE *state) { ASSERT(state != nullptr); if (state->offset + sizeof(uint8_t) > state->size) { state->error = BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return false; } uint8_t marker = state->src[state->offset]; state->offset++; // BSON arrays always use keys state->dom_size += sizeof(JSON_STRING); if (!M_GetObjectKeySize(state)) { return false; } state->dom_size += sizeof(JSON_VALUE); return M_GetValueSize(state, marker); } static bool M_GetArraySize(M_STATE *state) { ASSERT(state != nullptr); const size_t start_offset = state->offset; if (state->offset + sizeof(int32_t) > state->size) { state->error = BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return false; } const int size = M_ReadI32(&state->src[state->offset]); state->offset += sizeof(int32_t); while (state->offset < start_offset + size - 1) { state->dom_size += sizeof(JSON_ARRAY_ELEMENT); if (!M_GetArrayElementWrappedSize(state)) { return false; } } if (state->offset + sizeof(char) > state->size) { state->error = BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return false; } if (state->src[state->offset] != '\0') { state->error = BSON_PARSE_ERROR_INVALID_VALUE; return false; } state->offset++; return true; } static bool M_GetArrayValueSize(M_STATE *state) { ASSERT(state != nullptr); state->dom_size += sizeof(JSON_ARRAY); return M_GetArraySize(state); } static bool M_GetObjectElementWrappedSize(M_STATE *state) { ASSERT(state != nullptr); if (state->offset + sizeof(uint8_t) > state->size) { state->error = BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return false; } uint8_t marker = state->src[state->offset]; state->offset++; state->dom_size += sizeof(JSON_STRING); if (!M_GetObjectKeySize(state)) { return false; } state->dom_size += sizeof(JSON_VALUE); return M_GetValueSize(state, marker); } static bool M_GetObjectSize(M_STATE *state) { ASSERT(state != nullptr); const size_t start_offset = state->offset; if (state->offset + sizeof(int32_t) > state->size) { state->error = BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return false; } const int size = M_ReadI32(&state->src[state->offset]); state->offset += sizeof(int32_t); while (state->offset < start_offset + size - 1) { state->dom_size += sizeof(JSON_OBJECT_ELEMENT); if (!M_GetObjectElementWrappedSize(state)) { return false; } } if (state->offset + sizeof(char) > state->size) { state->error = BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return false; } if (state->src[state->offset] != '\0') { state->error = BSON_PARSE_ERROR_INVALID_VALUE; return false; } state->offset++; return true; } static bool M_GetObjectValueSize(M_STATE *state) { ASSERT(state != nullptr); state->dom_size += sizeof(JSON_OBJECT); return M_GetObjectSize(state); } static bool M_GetValueSize(M_STATE *state, uint8_t marker) { ASSERT(state != nullptr); switch (marker) { case 0x01: return M_GetDoubleValueSize(state); case 0x02: return M_GetStringValueSize(state); case 0x03: return M_GetObjectValueSize(state); case 0x04: return M_GetArrayValueSize(state); case 0x0A: return M_GetNullValueSize(state); case 0x08: return M_GetBoolValueSize(state); case 0x10: return M_GetInt32ValueSize(state); default: state->error = BSON_PARSE_ERROR_INVALID_VALUE; return false; } } static bool M_GetRootSize(M_STATE *state) { // assume the root element to be an object state->dom_size += sizeof(JSON_VALUE); return M_GetObjectValueSize(state); } static void M_HandleObjectKey(M_STATE *state, JSON_STRING *string) { ASSERT(state != nullptr); ASSERT(string != nullptr); size_t size = 0; string->ref_count = 1; string->string = state->data; while (state->src[state->offset]) { state->data[size++] = state->src[state->offset++]; } string->string_size = size; state->data[size++] = state->src[state->offset++]; state->data += size; } static void M_HandleNullValue(M_STATE *state, JSON_VALUE *value) { ASSERT(state != nullptr); ASSERT(value != nullptr); value->type = JSON_TYPE_NULL; value->payload = nullptr; } static void M_HandleBoolValue(M_STATE *state, JSON_VALUE *value) { ASSERT(state != nullptr); ASSERT(value != nullptr); ASSERT(state->offset + sizeof(char) <= state->size); switch (state->src[state->offset]) { case 0x00: value->type = JSON_TYPE_FALSE; value->payload = nullptr; break; case 0x01: value->type = JSON_TYPE_TRUE; value->payload = nullptr; break; default: ASSERT_FAIL(); } state->offset++; } static void M_HandleInt32Value(M_STATE *state, JSON_VALUE *value) { ASSERT(state != nullptr); ASSERT(value != nullptr); ASSERT(state->offset + sizeof(int32_t) <= state->size); int32_t num = M_ReadI32(&state->src[state->offset]); state->offset += sizeof(int32_t); JSON_NUMBER *number = (JSON_NUMBER *)state->dom; number->ref_count = 1; state->dom += sizeof(JSON_NUMBER); number->number = state->data; sprintf(state->data, "%d", num); number->number_size = strlen(number->number); state->data += number->number_size + 1; value->type = JSON_TYPE_NUMBER; value->payload = number; } static void M_HandleDoubleValue(M_STATE *state, JSON_VALUE *value) { ASSERT(state != nullptr); ASSERT(value != nullptr); ASSERT(state->offset + sizeof(double) <= state->size); double num = M_ReadDouble(&state->src[state->offset]); state->offset += sizeof(double); JSON_NUMBER *number = (JSON_NUMBER *)state->dom; number->ref_count = 1; state->dom += sizeof(JSON_NUMBER); number->number = state->data; sprintf(state->data, "%f", num); number->number_size = strlen(number->number); state->data += number->number_size + 1; // strip trailing zeroes after decimal point if (strchr(number->number, '.')) { while (number->number[number->number_size - 1] == '0' && number->number_size > 1) { number->number_size--; } number->number[number->number_size] = '\0'; } value->type = JSON_TYPE_NUMBER; value->payload = number; } static void M_HandleStringValue(M_STATE *state, JSON_VALUE *value) { ASSERT(state != nullptr); ASSERT(value != nullptr); ASSERT(state->offset + sizeof(int32_t) <= state->size); int32_t size = M_ReadI32(&state->src[state->offset]); state->offset += sizeof(int32_t); JSON_STRING *string = (JSON_STRING *)state->dom; string->ref_count = 1; state->dom += sizeof(JSON_STRING); memcpy(state->data, state->src + state->offset, size); state->offset += size; string->string = state->data; string->string_size = size; state->data += size; value->type = JSON_TYPE_STRING; value->payload = string; } static void M_HandleArrayElementWrapped( M_STATE *state, JSON_ARRAY_ELEMENT *element) { ASSERT(state != nullptr); ASSERT(element != nullptr); ASSERT(state->offset + sizeof(uint8_t) <= state->size); uint8_t marker = state->src[state->offset]; state->offset++; // BSON arrays always use keys JSON_STRING *key = (JSON_STRING *)state->dom; key->ref_count = 1; state->dom += sizeof(JSON_STRING); M_HandleObjectKey(state, key); JSON_VALUE *value = (JSON_VALUE *)state->dom; value->ref_count = 1; state->dom += sizeof(JSON_VALUE); element->value = value; M_HandleValue(state, value, marker); } static void M_HandleArray(M_STATE *state, JSON_ARRAY *array) { ASSERT(state != nullptr); ASSERT(array != nullptr); const size_t start_offset = state->offset; ASSERT(state->offset + sizeof(int32_t) <= state->size); const int size = M_ReadI32(&state->src[state->offset]); state->offset += sizeof(int32_t); JSON_ARRAY_ELEMENT *previous = nullptr; int count = 0; while (state->offset < start_offset + size - 1) { JSON_ARRAY_ELEMENT *element = (JSON_ARRAY_ELEMENT *)state->dom; element->ref_count = 1; state->dom += sizeof(JSON_ARRAY_ELEMENT); if (!previous) { array->start = element; } else { previous->next = element; } previous = element; M_HandleArrayElementWrapped(state, element); count++; } if (previous) { previous->next = nullptr; } if (!count) { array->start = nullptr; } array->ref_count = 1; array->length = count; ASSERT(state->offset + sizeof(char) <= state->size); ASSERT(state->src[state->offset] == '\0'); state->offset++; } static void M_HandleArrayValue(M_STATE *state, JSON_VALUE *value) { ASSERT(state != nullptr); ASSERT(value != nullptr); JSON_ARRAY *array = (JSON_ARRAY *)state->dom; array->ref_count = 1; state->dom += sizeof(JSON_ARRAY); M_HandleArray(state, array); value->type = JSON_TYPE_ARRAY; value->payload = array; } static void M_HandleObjectElementWrapped( M_STATE *state, JSON_OBJECT_ELEMENT *element) { ASSERT(state != nullptr); ASSERT(element != nullptr); ASSERT(state->offset + sizeof(uint8_t) <= state->size); uint8_t marker = state->src[state->offset]; state->offset++; JSON_STRING *key = (JSON_STRING *)state->dom; key->ref_count = 1; state->dom += sizeof(JSON_STRING); M_HandleObjectKey(state, key); JSON_VALUE *value = (JSON_VALUE *)state->dom; value->ref_count = 1; state->dom += sizeof(JSON_VALUE); element->name = key; element->value = value; M_HandleValue(state, value, marker); } static void M_HandleObject(M_STATE *state, JSON_OBJECT *object) { ASSERT(state != nullptr); ASSERT(object != nullptr); const size_t start_offset = state->offset; ASSERT(state->offset + sizeof(int32_t) <= state->size); const int size = M_ReadI32(&state->src[state->offset]); state->offset += sizeof(int32_t); JSON_OBJECT_ELEMENT *previous = nullptr; int count = 0; while (state->offset < start_offset + size - 1) { JSON_OBJECT_ELEMENT *element = (JSON_OBJECT_ELEMENT *)state->dom; element->ref_count = 1; state->dom += sizeof(JSON_OBJECT_ELEMENT); if (!previous) { object->start = element; } else { previous->next = element; } previous = element; M_HandleObjectElementWrapped(state, element); count++; } if (previous) { previous->next = nullptr; } if (!count) { object->start = nullptr; } object->ref_count = 1; object->length = count; ASSERT(state->offset + sizeof(char) <= state->size); ASSERT(state->src[state->offset] == '\0'); state->offset++; } static void M_HandleObjectValue(M_STATE *state, JSON_VALUE *value) { ASSERT(state != nullptr); ASSERT(value != nullptr); JSON_OBJECT *object = (JSON_OBJECT *)state->dom; object->ref_count = 1; state->dom += sizeof(JSON_OBJECT); M_HandleObject(state, object); value->type = JSON_TYPE_OBJECT; value->payload = object; } static void M_HandleValue(M_STATE *state, JSON_VALUE *value, uint8_t marker) { ASSERT(state != nullptr); ASSERT(value != nullptr); switch (marker) { case 0x01: M_HandleDoubleValue(state, value); break; case 0x02: M_HandleStringValue(state, value); break; case 0x03: M_HandleObjectValue(state, value); break; case 0x04: M_HandleArrayValue(state, value); break; case 0x0A: M_HandleNullValue(state, value); break; case 0x08: M_HandleBoolValue(state, value); break; case 0x10: M_HandleInt32Value(state, value); break; default: ASSERT_FAIL(); } } JSON_VALUE *BSON_Parse(const char *src, size_t src_size) { return BSON_ParseEx(src, src_size, nullptr); } JSON_VALUE *BSON_ParseEx( const char *src, size_t src_size, BSON_PARSE_RESULT *result) { M_STATE state; void *allocation; JSON_VALUE *value; size_t total_size; if (result) { result->error = BSON_PARSE_ERROR_NONE; result->error_offset = 0; } if (!src) { return nullptr; } state.src = src; state.size = src_size; state.offset = 0; state.error = BSON_PARSE_ERROR_NONE; state.dom_size = 0; state.data_size = 0; if (M_GetRootSize(&state)) { if (state.offset != state.size) { state.error = BSON_PARSE_ERROR_UNEXPECTED_TRAILING_BYTES; } } if (state.error != BSON_PARSE_ERROR_NONE) { if (result) { result->error = state.error; result->error_offset = state.offset; } LOG_ERROR( "Error while reading BSON near offset %d: %s", state.offset, BSON_GetErrorDescription(state.error)); return nullptr; } total_size = state.dom_size + state.data_size; allocation = Memory_Alloc(total_size); state.offset = 0; state.dom = (char *)allocation; state.data = state.dom + state.dom_size; // assume the root element to be an object value = (JSON_VALUE *)state.dom; value->ref_count = 0; state.dom += sizeof(JSON_VALUE); M_HandleObjectValue(&state, value); ASSERT(state.dom == (char *)allocation + state.dom_size); ASSERT(state.data == (char *)allocation + state.dom_size + state.data_size); return value; } const char *BSON_GetErrorDescription(BSON_PARSE_ERROR error) { switch (error) { case BSON_PARSE_ERROR_NONE: return "no error"; case BSON_PARSE_ERROR_INVALID_VALUE: return "invalid value"; case BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER: return "premature end of buffer"; case BSON_PARSE_ERROR_UNEXPECTED_TRAILING_BYTES: return "unexpected trailing bytes"; case BSON_PARSE_ERROR_UNKNOWN: default: return "unknown"; } } ================================================ FILE: src/trx/core/bson/parse.h ================================================ #pragma once #include // Parse a BSON file, returning a pointer to the root of the JSON structure. // Returns nullptr if an error occurred (malformed BSON input, or malloc // failed). JSON_VALUE *BSON_Parse(const char *src, size_t src_size); JSON_VALUE *BSON_ParseEx( const char *src, size_t src_size, BSON_PARSE_RESULT *result); const char *BSON_GetErrorDescription(BSON_PARSE_ERROR error); ================================================ FILE: src/trx/core/bson/types.h ================================================ #pragma once #include #include typedef struct { BSON_PARSE_ERROR error; size_t error_offset; } BSON_PARSE_RESULT; ================================================ FILE: src/trx/core/bson/write.c ================================================ #include #include #include #include #include #include #include #include #include static bool M_GetValueWrappedSize( size_t *size, const char *key, const JSON_VALUE *value); static char *M_WriteValueWrapped( char *data, const char *key, const JSON_VALUE *value); static bool M_GetMarkerSize(size_t *size, const char *key) { ASSERT(size != nullptr); ASSERT(key != nullptr); *size += 1; // marker *size += strlen(key); // key *size += 1; // nullptr terminator return true; } static bool M_GetNullWrappedSize(size_t *size, const char *key) { ASSERT(size != nullptr); ASSERT(key != nullptr); return M_GetMarkerSize(size, key); } static bool M_GetBoolWrappedSize(size_t *size, const char *key) { ASSERT(size != nullptr); ASSERT(key != nullptr); if (!M_GetMarkerSize(size, key)) { return false; } *size += 1; return true; } static bool M_GetInt32Size(size_t *size) { ASSERT(size != nullptr); *size += sizeof(int32_t); return true; } static bool M_GetInt32WrappedSize(size_t *size, const char *key) { ASSERT(size != nullptr); ASSERT(key != nullptr); if (!M_GetMarkerSize(size, key)) { return false; } if (!M_GetInt32Size(size)) { return false; } return true; } static bool M_GetDoubleSize(size_t *size) { ASSERT(size != nullptr); *size += sizeof(double); return true; } static bool M_GetDoubleWrappedSize(size_t *size, const char *key) { ASSERT(size != nullptr); ASSERT(key != nullptr); if (!M_GetMarkerSize(size, key)) { return false; } if (!M_GetDoubleSize(size)) { return false; } return true; } static bool M_GetNumberWrappedSize( size_t *size, const char *key, const JSON_NUMBER *number) { ASSERT(size != nullptr); ASSERT(key != nullptr); char *str = number->number; ASSERT(str != nullptr); // hexadecimal numbers if (number->number_size >= 2 && (str[1] == 'x' || str[1] == 'X')) { return M_GetInt32WrappedSize(size, key); } // skip leading sign if (str[0] == '+' || str[0] == '-') { str += 1; } ASSERT(str[0] != '\0'); if (!strcmp(str, "Infinity")) { // BSON does not support Infinity. return M_GetDoubleWrappedSize(size, key); } else if (!strcmp(str, "NaN")) { // BSON does not support NaN. return M_GetInt32WrappedSize(size, key); } else if (strchr(str, '.')) { return M_GetDoubleWrappedSize(size, key); } else { return M_GetInt32WrappedSize(size, key); } return false; } static bool M_GetStringSize(size_t *size, const JSON_STRING *string) { ASSERT(size != nullptr); ASSERT(string != nullptr); *size += sizeof(uint32_t); // size *size += string->string_size; // string *size += 1; // nullptr terminator return true; } static bool M_GetStringWrappedSize( size_t *size, const char *key, const JSON_STRING *string) { ASSERT(size != nullptr); ASSERT(key != nullptr); ASSERT(string != nullptr); if (!M_GetMarkerSize(size, key)) { return false; } if (!M_GetStringSize(size, string)) { return false; } return true; } static bool M_GetArraySize(size_t *size, const JSON_ARRAY *array) { ASSERT(size != nullptr); ASSERT(array != nullptr); char key[12]; int idx = 0; *size += sizeof(int32_t); // object size for (JSON_ARRAY_ELEMENT *element = array->start; element != nullptr; element = element->next) { sprintf(key, "%d", idx); idx++; if (!M_GetValueWrappedSize(size, key, element->value)) { return false; } } *size += 1; // nullptr terminator return true; } static bool M_GetArrayWrappedSize( size_t *size, const char *key, const JSON_ARRAY *array) { ASSERT(size != nullptr); ASSERT(key != nullptr); ASSERT(array != nullptr); if (!M_GetMarkerSize(size, key)) { return false; } if (!M_GetArraySize(size, array)) { return false; } return true; } static bool M_GetObjectSize(size_t *size, const JSON_OBJECT *object) { ASSERT(size != nullptr); ASSERT(object != nullptr); *size += sizeof(int32_t); // object size for (JSON_OBJECT_ELEMENT *element = object->start; element != nullptr; element = element->next) { if (!M_GetValueWrappedSize( size, element->name->string, element->value)) { return false; } } *size += 1; // nullptr terminator return true; } static bool M_GetObjectWrappedSize( size_t *size, const char *key, const JSON_OBJECT *object) { ASSERT(size != nullptr); ASSERT(key != nullptr); ASSERT(object != nullptr); if (!M_GetMarkerSize(size, key)) { return false; } if (!M_GetObjectSize(size, object)) { return false; } return true; } static bool M_GetValueSize(size_t *size, const JSON_VALUE *value) { ASSERT(size != nullptr); ASSERT(value != nullptr); switch (value->type) { case JSON_TYPE_ARRAY: return M_GetArraySize(size, (JSON_ARRAY *)value->payload); case JSON_TYPE_OBJECT: return M_GetObjectSize(size, (JSON_OBJECT *)value->payload); default: LOG_ERROR("Bad BSON root element: %d", value->type); } return false; } static bool M_GetValueWrappedSize( size_t *size, const char *key, const JSON_VALUE *value) { ASSERT(size != nullptr); ASSERT(key != nullptr); ASSERT(value != nullptr); switch (value->type) { case JSON_TYPE_NULL: return M_GetNullWrappedSize(size, key); case JSON_TYPE_TRUE: return M_GetBoolWrappedSize(size, key); case JSON_TYPE_FALSE: return M_GetBoolWrappedSize(size, key); case JSON_TYPE_NUMBER: return M_GetNumberWrappedSize(size, key, (JSON_NUMBER *)value->payload); case JSON_TYPE_STRING: return M_GetStringWrappedSize(size, key, (JSON_STRING *)value->payload); case JSON_TYPE_ARRAY: return M_GetArrayWrappedSize(size, key, (JSON_ARRAY *)value->payload); case JSON_TYPE_OBJECT: return M_GetObjectWrappedSize(size, key, (JSON_OBJECT *)value->payload); default: LOG_ERROR("Unknown JSON element: %d", value->type); return false; } } static char *M_WriteMarker(char *data, const char *key, const uint8_t marker) { ASSERT(data != nullptr); ASSERT(key != nullptr); *data++ = marker; strcpy(data, key); data += strlen(key); *data++ = '\0'; return data; } static char *M_WriteNullWrapped(char *data, const char *key) { ASSERT(data != nullptr); ASSERT(key != nullptr); return M_WriteMarker(data, key, '\x0A'); } static char *M_WriteBoolWrapped(char *data, const char *key, bool value) { ASSERT(data != nullptr); ASSERT(key != nullptr); data = M_WriteMarker(data, key, '\x08'); *(int8_t *)data++ = (int8_t)value; return data; } static char *M_WriteInt32(char *data, const int32_t value) { ASSERT(data != nullptr); memcpy(data, &value, sizeof(value)); data += sizeof(int32_t); return data; } static char *M_WriteInt32Wrapped( char *data, const char *key, const int32_t value) { ASSERT(data != nullptr); ASSERT(key != nullptr); data = M_WriteMarker(data, key, '\x10'); return M_WriteInt32(data, value); } static char *M_WriteDouble(char *data, const double value) { ASSERT(data != nullptr); memcpy(data, &value, sizeof(value)); data += sizeof(double); return data; } static char *M_WriteDoubleWrapped( char *data, const char *key, const double value) { ASSERT(data != nullptr); ASSERT(key != nullptr); data = M_WriteMarker(data, key, '\x01'); return M_WriteDouble(data, value); } static char *M_WriteNumberWrapped( char *data, const char *key, const JSON_NUMBER *number) { ASSERT(data != nullptr); ASSERT(key != nullptr); ASSERT(number != nullptr); char *str = number->number; // hexadecimal numbers if (number->number_size >= 2 && (str[1] == 'x' || str[1] == 'X')) { return M_WriteInt32Wrapped( data, key, json_strtoumax(number->number, nullptr, 0)); } // skip leading sign if (str[0] == '+' || str[0] == '-') { str++; } ASSERT(str[0] != '\0'); if (!strcmp(str, "Infinity")) { // BSON does not support Infinity. return M_WriteDoubleWrapped(data, key, DBL_MAX); } else if (!strcmp(str, "NaN")) { // BSON does not support NaN. return M_WriteInt32Wrapped(data, key, 0); } else if (strchr(str, '.')) { return M_WriteDoubleWrapped(data, key, atof(number->number)); } else { return M_WriteInt32Wrapped(data, key, atoi(number->number)); } return data; } static char *M_WriteString(char *data, const JSON_STRING *string) { ASSERT(data != nullptr); ASSERT(string != nullptr); const uint32_t bson_string_size = string->string_size + 1; memcpy(data, &bson_string_size, sizeof(bson_string_size)); data += sizeof(uint32_t); memcpy(data, string->string, string->string_size); data += string->string_size; *data++ = '\0'; return data; } static char *M_WriteStringWrapped( char *data, const char *key, const JSON_STRING *string) { ASSERT(data != nullptr); ASSERT(key != nullptr); ASSERT(string != nullptr); data = M_WriteMarker(data, key, '\x02'); data = M_WriteString(data, string); return data; } static char *M_WriteArray(char *data, const JSON_ARRAY *array) { ASSERT(data != nullptr); ASSERT(array != nullptr); char key[12]; int idx = 0; char *old = data; data += sizeof(int32_t); for (JSON_ARRAY_ELEMENT *element = array->start; element != nullptr; element = element->next) { sprintf(key, "%d", idx); idx++; data = M_WriteValueWrapped(data, key, element->value); } *data++ = '\0'; const int32_t object_size = data - old; memcpy(old, &object_size, sizeof(object_size)); return data; } static char *M_WriteArrayWrapped( char *data, const char *key, const JSON_ARRAY *array) { ASSERT(data != nullptr); ASSERT(key != nullptr); ASSERT(array != nullptr); data = M_WriteMarker(data, key, '\x04'); data = M_WriteArray(data, array); return data; } static char *M_WriteObject(char *data, const JSON_OBJECT *object) { ASSERT(data != nullptr); ASSERT(object != nullptr); char *old = data; data += sizeof(int32_t); for (JSON_OBJECT_ELEMENT *element = object->start; element != nullptr; element = element->next) { data = M_WriteValueWrapped(data, element->name->string, element->value); } *data++ = '\0'; const int32_t object_size = data - old; memcpy(old, &object_size, sizeof(object_size)); return data; } static char *M_WriteObjectWrapped( char *data, const char *key, const JSON_OBJECT *object) { ASSERT(data != nullptr); ASSERT(key != nullptr); ASSERT(object != nullptr); data = M_WriteMarker(data, key, '\x03'); data = M_WriteObject(data, object); return data; } static char *M_WriteValue(char *data, const JSON_VALUE *value) { ASSERT(data != nullptr); ASSERT(value != nullptr); switch (value->type) { case JSON_TYPE_ARRAY: data = M_WriteArray(data, (JSON_ARRAY *)value->payload); break; case JSON_TYPE_OBJECT: data = M_WriteObject(data, (JSON_OBJECT *)value->payload); break; default: ASSERT_FAIL(); } return data; } static char *M_WriteValueWrapped( char *data, const char *key, const JSON_VALUE *value) { ASSERT(data != nullptr); ASSERT(key != nullptr); ASSERT(value != nullptr); switch (value->type) { case JSON_TYPE_NULL: return M_WriteNullWrapped(data, key); case JSON_TYPE_TRUE: return M_WriteBoolWrapped(data, key, true); case JSON_TYPE_FALSE: return M_WriteBoolWrapped(data, key, false); case JSON_TYPE_NUMBER: return M_WriteNumberWrapped(data, key, (JSON_NUMBER *)value->payload); case JSON_TYPE_STRING: return M_WriteStringWrapped(data, key, (JSON_STRING *)value->payload); case JSON_TYPE_ARRAY: return M_WriteArrayWrapped(data, key, (JSON_ARRAY *)value->payload); case JSON_TYPE_OBJECT: return M_WriteObjectWrapped(data, key, (JSON_OBJECT *)value->payload); default: return nullptr; } } void *BSON_Write(const JSON_VALUE *value, size_t *out_size) { ASSERT(value != nullptr); *out_size = -1; if (value == nullptr) { return nullptr; } size_t size = 0; if (!M_GetValueSize(&size, value)) { return nullptr; } char *data = Memory_Alloc(size); char *data_end = M_WriteValue(data, value); ASSERT((size_t)(data_end - data) == size); if (out_size != nullptr) { *out_size = size; } return data; } ================================================ FILE: src/trx/core/bson/write.h ================================================ #pragma once #include /* Write out a BSON binary string. Return 0 if an error occurred (malformed * JSON input, or malloc failed). The out_size parameter is optional. */ void *BSON_Write(const JSON_VALUE *value, size_t *out_size); ================================================ FILE: src/trx/core/bson.h ================================================ #pragma once #include #include #include #include ================================================ FILE: src/trx/core/colors.c ================================================ #include #include #include #ifndef M_PI #define M_PI 3.14159265358979323846 #endif RGBA_8888 Color_RGB888ToRGBA8888_Impl(const RGB_888 color) { return Color_RGB888ToRGBA8888Ex_Impl(color, 255); } RGBA_8888 Color_RGB888ToRGBA8888Ex_Impl( const RGB_888 color, const uint8_t alpha) { return (RGBA_8888) { color.r, color.g, color.b, alpha }; } RGBA_F Color_RGBFToRGBAF_Impl(const RGB_F color) { return Color_RGBFToRGBAFEx_Impl(color, 1.0f); } RGBA_F Color_RGBFToRGBAFEx_Impl(const RGB_F color, const float alpha) { return (RGBA_F) { color.r, color.g, color.b, alpha }; } RGBA_8888 Color_ARGB1555ToRGBA8888(const uint16_t argb1555) { // Extract 5-bit values for each ARGB component. uint8_t a1 = (argb1555 >> 15) & 0x01; uint8_t r5 = (argb1555 >> 10) & 0x1F; uint8_t g5 = (argb1555 >> 5) & 0x1F; uint8_t b5 = argb1555 & 0x1F; // Expand 5-bit color components to 8-bit. uint8_t a8 = a1 * 255; // 1-bit alpha (either 0 or 255) uint8_t r8 = (r5 << 3) | (r5 >> 2); uint8_t g8 = (g5 << 3) | (g5 >> 2); uint8_t b8 = (b5 << 3) | (b5 >> 2); return (RGBA_8888) { .r = r8, .g = g8, .b = b8, .a = a8, }; } RGB_F Color_MixRGBF_Impl( const RGB_F color_1, const RGB_F color_2, const float ratio) { return (RGB_F) { .r = color_1.r + (color_2.r - color_1.r) * ratio, .g = color_1.g + (color_2.g - color_1.g) * ratio, .b = color_1.b + (color_2.b - color_1.b) * ratio, }; } RGBA_F Color_MixRGBAF_Impl( const RGBA_F color_1, const RGBA_F color_2, const float ratio) { return (RGBA_F) { .r = color_1.r + (color_2.r - color_1.r) * ratio, .g = color_1.g + (color_2.g - color_1.g) * ratio, .b = color_1.b + (color_2.b - color_1.b) * ratio, .a = color_1.a + (color_2.a - color_1.a) * ratio, }; } RGB_888 Color_MixRGB888_Impl( const RGB_888 color_1, const RGB_888 color_2, const float ratio) { return (RGB_888) { .r = (uint8_t)(color_1.r + (color_2.r - color_1.r) * ratio), .g = (uint8_t)(color_1.g + (color_2.g - color_1.g) * ratio), .b = (uint8_t)(color_1.b + (color_2.b - color_1.b) * ratio), }; } RGBA_8888 Color_MixRGBA8888_Impl( const RGBA_8888 color_1, const RGBA_8888 color_2, const float ratio) { return (RGBA_8888) { .r = (uint8_t)(color_1.r + (color_2.r - color_1.r) * ratio), .g = (uint8_t)(color_1.g + (color_2.g - color_1.g) * ratio), .b = (uint8_t)(color_1.b + (color_2.b - color_1.b) * ratio), .a = (uint8_t)(color_1.a + (color_2.a - color_1.a) * ratio), }; } RGB_888 Color_HSLToRGB(const float h, const float s, const float l) { float hue = h < 0.0f ? 0.0f : fmodf(h, 360.0f); float sat = s; float light = l; CLAMP(hue, 0.0f, 360.0f); CLAMP(sat, 0.0f, 1.0f); CLAMP(light, 0.0f, 1.0f); const float c = (1.0f - fabsf(2.0f * light - 1.0f)) * sat; const float x = c * (1.0f - fabsf(fmodf(hue / 60.0f, 2.0f) - 1.0f)); const float m = light - c / 2.0f; float rp = 0.0f; float gp = 0.0f; float bp = 0.0f; if (hue < 60.0f) { rp = c; gp = x; } else if (hue < 120.0f) { rp = x; gp = c; } else if (hue < 180.0f) { gp = c; bp = x; } else if (hue < 240.0f) { gp = x; bp = c; } else if (hue < 300.0f) { rp = x; bp = c; } else { rp = c; bp = x; } return (RGB_888) { .r = (uint8_t)roundf((rp + m) * 255.0f), .g = (uint8_t)roundf((gp + m) * 255.0f), .b = (uint8_t)roundf((bp + m) * 255.0f), }; } void Color_RGBToHSL( const RGB_888 color, float *const out_h, float *const out_s, float *const out_l) { const float r = color.r / 255.0f; const float g = color.g / 255.0f; const float b = color.b / 255.0f; const float max_c = MAX(r, MAX(g, b)); const float min_c = MIN(r, MIN(g, b)); const float delta = max_c - min_c; float light = (max_c + min_c) / 2.0f; float hue = 0.0f; float sat = 0.0f; if (delta > 0.0f) { if (max_c == r) { hue = 60.0f * fmodf((g - b) / delta, 6.0f); } else if (max_c == g) { hue = 60.0f * (((b - r) / delta) + 2.0f); } else { hue = 60.0f * (((r - g) / delta) + 4.0f); } if (hue < 0.0f) { hue += 360.0f; } sat = delta / (1.0f - fabsf(2.0f * light - 1.0f)); } CLAMP(hue, 0.0f, 360.0f); CLAMP(sat, 0.0f, 1.0f); CLAMP(light, 0.0f, 1.0f); *out_h = hue; *out_s = sat; *out_l = light; } static float M_SRGBToLinear(const float c) { if (c <= 0.04045f) { return c / 12.92f; } return powf((c + 0.055f) / 1.055f, 2.4f); } static float M_LinearToSRGB(const float c) { if (c <= 0.0031308f) { return c * 12.92f; } return 1.055f * powf(c, 1.0f / 2.4f) - 0.055f; } RGB_888 Color_OKLCHToRGB(const float l, const float c, const float h) { float lightness = l; float chroma = c; float hue = h < 0.0f ? 0.0f : fmodf(h, 360.0f); CLAMP(lightness, 0.0f, 1.0f); CLAMP(chroma, 0.0f, 1.0f); CLAMP(hue, 0.0f, 360.0f); const float hue_rad = hue * M_PI / 180.0f; const float a = chroma * cosf(hue_rad); const float b = chroma * sinf(hue_rad); const float l_ = lightness + 0.3963377774f * a + 0.2158037573f * b; const float m_ = lightness - 0.1055613458f * a - 0.0638541728f * b; const float s_ = lightness - 0.0894841775f * a - 1.2914855480f * b; const float l_3 = l_ * l_ * l_; const float m_3 = m_ * m_ * m_; const float s_3 = s_ * s_ * s_; float r_linear = +4.0767416621f * l_3 - 3.3077115913f * m_3 + 0.2309699292f * s_3; float g_linear = -1.2684380046f * l_3 + 2.6097574011f * m_3 - 0.3413193965f * s_3; float b_linear = -0.0041960863f * l_3 - 0.7034186147f * m_3 + 1.7076147010f * s_3; CLAMP(r_linear, 0.0f, 1.0f); CLAMP(g_linear, 0.0f, 1.0f); CLAMP(b_linear, 0.0f, 1.0f); const float r_srgb = M_LinearToSRGB(r_linear); const float g_srgb = M_LinearToSRGB(g_linear); const float b_srgb = M_LinearToSRGB(b_linear); return (RGB_888) { .r = (uint8_t)roundf(r_srgb * 255.0f), .g = (uint8_t)roundf(g_srgb * 255.0f), .b = (uint8_t)roundf(b_srgb * 255.0f), }; } void Color_RGBToOKLCH( const RGB_888 color, float *const out_l, float *const out_c, float *const out_h) { const float r = M_SRGBToLinear(color.r / 255.0f); const float g = M_SRGBToLinear(color.g / 255.0f); const float b = M_SRGBToLinear(color.b / 255.0f); const float l = cbrtf(0.4122214708f * r + 0.5363325363f * g + 0.0514459929f * b); const float m = cbrtf(0.2119034982f * r + 0.6806995451f * g + 0.1073969566f * b); const float s = cbrtf(0.0883024619f * r + 0.2817188376f * g + 0.6299787005f * b); float lightness = 0.2104542553f * l + 0.7936177850f * m - 0.0040720468f * s; const float a = 1.9779984951f * l - 2.4285922050f * m + 0.4505937099f * s; const float b_comp = 0.0259040371f * l + 0.7827717662f * m - 0.8086757660f * s; const float chroma = sqrtf(a * a + b_comp * b_comp); float hue = atan2f(b_comp, a) * 180.0f / M_PI; if (hue < 0.0f) { hue += 360.0f; } CLAMP(lightness, 0.0f, 1.0f); *out_l = lightness; *out_c = chroma; *out_h = hue; } ================================================ FILE: src/trx/core/colors.h ================================================ #pragma once #include typedef struct { float r, g, b; } RGB_F; typedef struct { float r, g, b, a; } RGBA_F; typedef struct { uint8_t r, g, b; } RGB_888; typedef struct { uint8_t r, g, b, a; } RGBA_8888; #define COLOR_RGBA_8888_BLACK ((RGBA_8888) { 0x00, 0x00, 0x00, 0xFF }) #define COLOR_RGBA_8888_WHITE ((RGBA_8888) { 0xFF, 0xFF, 0xFF, 0xFF }) #define COLOR_RGB_888_BLACK ((RGB_888) { 0x00, 0x00, 0x00 }) #define COLOR_RGB_888_WHITE ((RGB_888) { 0xFF, 0xFF, 0xFF }) #define COLOR_RGB_F_WHITE ((RGB_F) { 1.0f, 1.0f, 1.0f }) RGBA_8888 Color_RGB888ToRGBA8888_Impl(RGB_888 color); RGBA_8888 Color_RGB888ToRGBA8888Ex_Impl(RGB_888 color, uint8_t alpha); RGBA_F Color_RGBFToRGBAF_Impl(RGB_F color); RGBA_F Color_RGBFToRGBAFEx_Impl(RGB_F color, float alpha); RGBA_8888 Color_ARGB1555ToRGBA8888(uint16_t argb1555); RGB_F Color_MixRGBF_Impl(RGB_F color_1, RGB_F color_2, float ratio); RGBA_F Color_MixRGBAF_Impl(RGBA_F color_1, RGBA_F color_2, float ratio); RGB_888 Color_MixRGB888_Impl(RGB_888 color_1, RGB_888 color_2, float ratio); RGBA_8888 Color_MixRGBA8888_Impl( RGBA_8888 color_1, RGBA_8888 color_2, float ratio); #define Color_RGBToRGBA(color) \ _Generic( \ (color), \ RGB_888: Color_RGB888ToRGBA8888_Impl, \ RGB_F: Color_RGBFToRGBAF_Impl)(color) #define Color_RGBToRGBAEx(color, alpha) \ _Generic( \ (color), \ RGB_888: Color_RGB888ToRGBA8888Ex_Impl, \ RGB_F: Color_RGBFToRGBAFEx_Impl)(color, alpha) #define Color_Mix(color_1, color_2, ratio) \ _Generic( \ (color_1), \ RGB_F: Color_MixRGBF_Impl, \ RGBA_F: Color_MixRGBAF_Impl, \ RGB_888: Color_MixRGB888_Impl, \ RGBA_8888: Color_MixRGBA8888_Impl)(color_1, color_2, ratio) RGB_888 Color_HSLToRGB(float h, float s, float l); void Color_RGBToHSL(RGB_888 color, float *out_h, float *out_s, float *out_l); RGB_888 Color_OKLCHToRGB(float l, float c, float h); void Color_RGBToOKLCH(RGB_888 color, float *out_l, float *out_c, float *out_h); ================================================ FILE: src/trx/core/enum_map.c ================================================ #include #include #include #include #include typedef struct { char *key; int32_t value; UT_hash_handle hh; } M_STR_TO_ID_ENTRY; typedef struct { char *key; char *str_value; UT_hash_handle hh; } M_ID_TO_STR_ENTRY; static M_STR_TO_ID_ENTRY *m_Str2IdMap = nullptr; static M_ID_TO_STR_ENTRY *m_Id2StrMap = nullptr; static M_ID_TO_STR_ENTRY *m_Id2NameMap = nullptr; static M_ID_TO_STR_ENTRY *m_Id2LabelKeyMap = nullptr; static void M_ClearStr2IdMap(M_STR_TO_ID_ENTRY **map) { M_STR_TO_ID_ENTRY *current, *tmp; HASH_ITER(hh, *map, current, tmp) { HASH_DEL(*map, current); Memory_Free(current->key); Memory_Free(current); } } static void M_ClearId2StrMap(M_ID_TO_STR_ENTRY **map) { M_ID_TO_STR_ENTRY *current, *tmp; HASH_ITER(hh, *map, current, tmp) { HASH_DEL(*map, current); Memory_Free(current->str_value); Memory_Free(current->key); Memory_Free(current); } } static __attribute__((destructor)) void M_Shutdown(void) { M_ClearStr2IdMap(&m_Str2IdMap); M_ClearId2StrMap(&m_Id2StrMap); M_ClearId2StrMap(&m_Id2NameMap); M_ClearId2StrMap(&m_Id2LabelKeyMap); } static void M_DefineStr2Id( M_STR_TO_ID_ENTRY **map, const char *const enum_type_name, const int32_t enum_value, const char *const str_value) { const char *const key = String_FormatStatic("%s|%s", enum_type_name, str_value); M_STR_TO_ID_ENTRY *const entry = Memory_Alloc(sizeof(M_STR_TO_ID_ENTRY)); entry->key = Memory_DupStr(key); entry->value = enum_value; HASH_ADD_KEYPTR(hh, *map, entry->key, strlen(entry->key), entry); } static void M_DefineId2Str( M_ID_TO_STR_ENTRY **map, const char *const enum_type_name, const int32_t enum_value, const char *const str_value) { const char *const key = String_FormatStatic("%s|%d", enum_type_name, enum_value); M_ID_TO_STR_ENTRY *entry; HASH_FIND_STR(*map, key, entry); if (entry != nullptr) { // The inverse lookup is already defined - do not override it. // (This means that the first call to ENUM_MAP for a given enum value // also determines what serializing it back to string will pick // in the event there are multiple aliases). return; } entry = Memory_Alloc(sizeof(M_ID_TO_STR_ENTRY)); entry->key = Memory_DupStr(key); entry->str_value = Memory_DupStr(str_value); HASH_ADD_KEYPTR(hh, *map, entry->key, strlen(entry->key), entry); } static int32_t M_Str2Id( M_STR_TO_ID_ENTRY *const *map, const char *const enum_type_name, const char *const str_value, int32_t default_value) { const char *const key = String_FormatStatic("%s|%s", enum_type_name, str_value); M_STR_TO_ID_ENTRY *entry; HASH_FIND_STR(*map, key, entry); return entry != nullptr ? entry->value : default_value; } static const char *M_Id2Str( M_ID_TO_STR_ENTRY *const *map, const char *const enum_type_name, const int32_t enum_value) { const char *const key = String_FormatStatic("%s|%d", enum_type_name, enum_value); M_ID_TO_STR_ENTRY *entry; HASH_FIND_STR(*map, key, entry); return entry != nullptr ? entry->str_value : nullptr; } void EnumMap_Define( const char *const enum_type_name, const char *const enum_name, const char *const label_key, const int32_t enum_value, const char *const str_value) { M_DefineStr2Id(&m_Str2IdMap, enum_type_name, enum_value, str_value); M_DefineId2Str(&m_Id2StrMap, enum_type_name, enum_value, str_value); M_DefineId2Str(&m_Id2NameMap, enum_type_name, enum_value, enum_name); M_DefineId2Str(&m_Id2LabelKeyMap, enum_type_name, enum_value, label_key); } int32_t EnumMap_Get( const char *const enum_type_name, const char *const str_value, int32_t default_value) { return M_Str2Id(&m_Str2IdMap, enum_type_name, str_value, default_value); } const char *EnumMap_ToString( const char *const enum_type_name, const int32_t enum_value) { return M_Id2Str(&m_Id2StrMap, enum_type_name, enum_value); } const char *EnumMap_GetName( const char *const enum_type_name, const int32_t enum_value) { return M_Id2Str(&m_Id2NameMap, enum_type_name, enum_value); } const char *EnumMap_GetLabel( const char *const enum_type_name, const int32_t enum_value) { const char *const key = M_Id2Str(&m_Id2LabelKeyMap, enum_type_name, enum_value); if (key == nullptr) { return nullptr; } return GameString_Get(key); } VECTOR *EnumMap_ListValues(const char *const enum_type_name) { if (enum_type_name == nullptr) { return nullptr; } // Compare the prefix to find the matching enum values. const size_t prefix_len = strlen(enum_type_name) + 1; VECTOR *const results = Vector_Create(sizeof(char *)); M_ID_TO_STR_ENTRY *entry; M_ID_TO_STR_ENTRY *tmp; HASH_ITER(hh, m_Id2StrMap, entry, tmp) { if (strncmp(entry->key, enum_type_name, prefix_len - 1) == 0 && entry->key[prefix_len - 1] == '|') { Vector_Add(results, &entry->str_value); } } return results; } ================================================ FILE: src/trx/core/enum_map.h ================================================ #include #define ENUM_MAP(enum_type_name, enum_value, str_value) \ EnumMap_Define( \ ENUM_MAP_NAME(enum_type_name), #enum_value, \ ENUM_MAP_LABEL_KEY(enum_type_name, enum_value), enum_value, str_value) #define ENUM_MAP_SELF(enum_type_name, enum_value) \ ENUM_MAP(enum_type_name, enum_value, #enum_value) #define ENUM_MAP_GET(enum_type_name, str_value, default_value) \ EnumMap_Get(ENUM_MAP_NAME(enum_type_name), str_value, default_value) #define ENUM_MAP_TO_STRING(enum_type_name, enum_value) \ EnumMap_ToString(ENUM_MAP_NAME(enum_type_name), enum_value) #define ENUM_MAP_NAME(enum_type_name) #enum_type_name #define ENUM_MAP_LABEL_KEY(enum_type_name, enum_value) \ "enums/" #enum_type_name "/" #enum_value // Associate an integer enum value, such as WEATHER_SNOW, with a string // representation such as "snow". // @param enum_type_name Name of the enum type, such as "WEATHER". // @param enum_name Name of the enum, such as "WEATHER_SNOW". // @param enum_value Value of the enum, such as 1. // @param str_value String representation of the enum, such as "snow". void EnumMap_Define( const char *enum_type_name, const char *enum_name, const char *label_key, int32_t enum_value, const char *str_value); // Retrieve an integer enum value from a string representation. // @param enum_type_name Name of the enum type, such as "WEATHER". // @param str_value String representation of the enum, such as "snow". // @param default_value Value to return in case the mapping fails. // @return Value of the enum, such as 1. int32_t EnumMap_Get( const char *enum_type_name, const char *str_value, int32_t default_value); // Retrieve an enum integer canonical name as a string. // @param enum_type_name Name of the enum type, such as "WEATHER". // @param enum_value Value of the enum, such as 1. // @return Name of the enum, such as "WEATHER_SNOW". const char *EnumMap_GetName(const char *enum_type_name, int32_t enum_value); // Retrieve a string representation, such as "snow", based on an integer enum // value such as WEATHER_SNOW. // @param enum_type_name Name of the enum type, such as "WEATHER". // @param enum_value Value of the enum, such as 1. // @return String representation of the enum, such as "snow". const char *EnumMap_ToString(const char *enum_type_name, int32_t enum_value); // Retrieve a localized label for the given enum value. // @param enum_type_name Name of the enum type, such as "WEATHER". // @param enum_value Value of the enum, such as 1. // @return Localized label or nullptr if missing. const char *EnumMap_GetLabel(const char *enum_type_name, int32_t enum_value); // Returns a vector of valid string values for the given enum_type_name. // // The returned vector must be freed via Vector_Free(). The string pointers // within the vector are owned by the enum map and should not be freed by the // caller. Returns nullptr if the enum_type_name is not valid. VECTOR *EnumMap_ListValues(const char *enum_type_name); ================================================ FILE: src/trx/core/event_manager.c ================================================ #include #include #include #include #include typedef struct { int32_t listener_id; const char *event_name; const void *sender; EVENT_LISTENER listener; void *user_data; } M_LISTENER; typedef struct EVENT_MANAGER { VECTOR *listeners; int32_t listener_id; } EVENT_MANAGER; EVENT_MANAGER *EventManager_Create(void) { EVENT_MANAGER *manager = Memory_Alloc(sizeof(EVENT_MANAGER)); manager->listeners = Vector_Create(sizeof(M_LISTENER)); manager->listener_id = 0; return manager; } void EventManager_Free(EVENT_MANAGER *const manager) { if (manager == nullptr) { return; } Vector_Free(manager->listeners); Memory_Free(manager); } int32_t EventManager_Subscribe( EVENT_MANAGER *const manager, const char *const event_name, const void *const sender, const EVENT_LISTENER listener, void *const user_data) { M_LISTENER entry = { .listener_id = manager->listener_id++, .event_name = event_name, .sender = sender, .listener = listener, .user_data = user_data, }; Vector_Add(manager->listeners, &entry); return entry.listener_id; } void EventManager_Unsubscribe( EVENT_MANAGER *const manager, const int32_t listener_id) { for (int32_t i = 0; i < manager->listeners->count; i++) { M_LISTENER entry = *(M_LISTENER *)Vector_Get(manager->listeners, i); if (entry.listener_id == listener_id) { Vector_RemoveAt(manager->listeners, i); return; } } } void EventManager_Fire(EVENT_MANAGER *const manager, const EVENT *const event) { for (int32_t i = 0; i < manager->listeners->count; i++) { M_LISTENER entry = *(M_LISTENER *)Vector_Get(manager->listeners, i); if (strcmp(entry.event_name, event->name) == 0 && entry.sender == event->sender) { entry.listener(event, entry.user_data); } } } ================================================ FILE: src/trx/core/event_manager.h ================================================ #pragma once #include typedef struct { const char *name; const void *sender; const void *data; } EVENT; typedef void (*EVENT_LISTENER)(const EVENT *, void *user_data); typedef struct EVENT_MANAGER EVENT_MANAGER; EVENT_MANAGER *EventManager_Create(void); void EventManager_Free(EVENT_MANAGER *manager); int32_t EventManager_Subscribe( EVENT_MANAGER *manager, const char *event_name, const void *sender, EVENT_LISTENER listener, void *user_data); void EventManager_Unsubscribe(EVENT_MANAGER *manager, int32_t listener_id); void EventManager_Fire(EVENT_MANAGER *manager, const EVENT *event); ================================================ FILE: src/trx/core/filesystem.c ================================================ #include #include #include #include #include #include #include #include #include #if defined(_WIN32) #include #include #include #define PATH_SEPARATOR "\\" #else #include #include #define PATH_SEPARATOR "/" #endif struct MYFILE { FILE *fp; const char *path; }; #if defined(_WIN32) #include #include #include #include static wchar_t *M_UTF8ToWide(const char *const utf8_str) { if (utf8_str == nullptr) { return nullptr; } const size_t len = strlen(utf8_str); const size_t wide_len = MultiByteToWideChar(CP_UTF8, 0, utf8_str, len, nullptr, 0); wchar_t *wide_str = Memory_Alloc((wide_len + 1) * sizeof(wchar_t)); MultiByteToWideChar(CP_UTF8, 0, utf8_str, len, wide_str, wide_len); wide_str[wide_len] = L'\0'; return wide_str; } static FILE *M_UTF8Fopen(const char *path, const char *mode) { if (path == nullptr || mode == nullptr) { return nullptr; } wchar_t *const wide_path = M_UTF8ToWide(path); wchar_t *const wide_mode = M_UTF8ToWide(mode); FILE *const file = _wfopen(wide_path, wide_mode); Memory_Free(wide_path); Memory_Free(wide_mode); return file; } #else static FILE *M_UTF8Fopen(const char *path, const char *mode) { if (path == nullptr || mode == nullptr) { return nullptr; } return fopen(path, mode); } #endif static bool M_ExistsRaw(const char *path) { if (path == nullptr) { return false; } FILE *fp = M_UTF8Fopen(path, "rb"); if (fp) { fclose(fp); return true; } return false; } bool File_IsAbsolute(const char *path) { return path && (path[0] == '/' || strstr(path, ":\\")); } bool File_IsRelative(const char *path) { return path && !File_IsAbsolute(path); } bool File_DirExists(const char *path) { if (path == nullptr) { return false; } DIR *dir = opendir(path); if (dir != nullptr) { closedir(dir); return true; } return false; } bool File_Exists(const char *path) { if (path == nullptr) { return false; } return M_ExistsRaw(path); } char *File_GetParentDirectory(const char *path) { if (path == nullptr) { return nullptr; } char *last_delim = MAX(strrchr(path, '/'), strrchr(path, '\\')); if (last_delim != nullptr) { return String_Format("%.*s", last_delim - path, path); } return nullptr; } MYFILE *File_Open(const char *path, FILE_OPEN_MODE mode) { MYFILE *file = Memory_Alloc(sizeof(MYFILE)); file->path = Memory_DupStr(path); switch (mode) { case FILE_OPEN_WRITE: file->fp = M_UTF8Fopen(path, "wb"); break; case FILE_OPEN_READ: file->fp = M_UTF8Fopen(path, "rb"); break; case FILE_OPEN_READ_WRITE: file->fp = M_UTF8Fopen(path, "r+b"); break; default: file->fp = nullptr; break; } if (file->fp == nullptr) { Memory_FreePointer(&file->path); Memory_FreePointer(&file); } return file; } bool File_ReadData(MYFILE *const file, void *const data, const size_t size) { return fread(data, size, 1, file->fp) == 1; } bool File_ReadItems( MYFILE *const file, void *data, const size_t count, const size_t item_size) { return fread(data, item_size, count, file->fp) == count; } int8_t File_ReadS8(MYFILE *const file) { int8_t result; File_ReadData(file, &result, sizeof(result)); return result; } int16_t File_ReadS16(MYFILE *const file) { int16_t result; File_ReadData(file, &result, sizeof(result)); return result; } int32_t File_ReadS32(MYFILE *const file) { int32_t result; File_ReadData(file, &result, sizeof(result)); return result; } uint8_t File_ReadU8(MYFILE *const file) { uint8_t result; File_ReadData(file, &result, sizeof(result)); return result; } uint16_t File_ReadU16(MYFILE *const file) { uint16_t result; File_ReadData(file, &result, sizeof(result)); return result; } uint32_t File_ReadU32(MYFILE *const file) { uint32_t result; File_ReadData(file, &result, sizeof(result)); return result; } void File_WriteData( MYFILE *const file, const void *const data, const size_t size) { fwrite(data, size, 1, file->fp); } void File_WriteItems( MYFILE *const file, const void *const data, const size_t count, const size_t item_size) { fwrite(data, item_size, count, file->fp); } void File_WriteS8(MYFILE *const file, const int8_t value) { fwrite(&value, sizeof(value), 1, file->fp); } void File_WriteS16(MYFILE *const file, const int16_t value) { fwrite(&value, sizeof(value), 1, file->fp); } void File_WriteS32(MYFILE *const file, const int32_t value) { fwrite(&value, sizeof(value), 1, file->fp); } void File_WriteU8(MYFILE *const file, const uint8_t value) { fwrite(&value, sizeof(value), 1, file->fp); } void File_WriteU16(MYFILE *const file, const uint16_t value) { fwrite(&value, sizeof(value), 1, file->fp); } void File_WriteU32(MYFILE *const file, const uint32_t value) { fwrite(&value, sizeof(value), 1, file->fp); } void File_WriteString(MYFILE *file, const char *fmt, ...) { if (file == nullptr || file->fp == nullptr) { return; } va_list args; va_start(args, fmt); const char *s = String_FormatStaticV(fmt, args); va_end(args); fputs(s, file->fp); } void File_Skip(MYFILE *file, size_t bytes) { File_Seek(file, bytes, FILE_SEEK_CUR); } void File_Seek(MYFILE *file, size_t pos, FILE_SEEK_MODE mode) { switch (mode) { case FILE_SEEK_SET: fseek(file->fp, pos, SEEK_SET); break; case FILE_SEEK_CUR: fseek(file->fp, pos, SEEK_CUR); break; case FILE_SEEK_END: fseek(file->fp, pos, SEEK_END); break; } } size_t File_Pos(MYFILE *file) { return ftell(file->fp); } size_t File_Size(MYFILE *file) { size_t old = ftell(file->fp); fseek(file->fp, 0, SEEK_END); size_t size = ftell(file->fp); fseek(file->fp, old, SEEK_SET); return size; } const char *File_GetPath(MYFILE *file) { return file->path; } bool File_GetMeta( const char *const path, uint64_t *const out_size, uint64_t *const out_mtime) { MYFILE *const file = File_Open(path, FILE_OPEN_READ); if (file == nullptr) { return false; } if (out_size != nullptr) { *out_size = (uint64_t)File_Size(file); } if (out_mtime != nullptr) { uint64_t mtime = 0; #if defined(_WIN32) struct _stat64 st; if (_fstat64(_fileno(file->fp), &st) == 0) { mtime = (uint64_t)st.st_mtime; } #else struct stat st; if (fstat(fileno(file->fp), &st) == 0) { mtime = (uint64_t)st.st_mtime; } #endif *out_mtime = mtime; } File_Close(file); return true; } void File_Close(MYFILE *file) { fclose(file->fp); Memory_FreePointer(&file->path); // free per-file line buffer Memory_FreePointer(&file); } bool File_Load(const char *path, char **output_data, size_t *output_size) { ASSERT(output_data != nullptr); MYFILE *fp = File_Open(path, FILE_OPEN_READ); if (!fp) { LOG_ERROR("Can't open file %s", path); *output_data = nullptr; return false; } size_t data_size = File_Size(fp); char *data = Memory_Alloc(data_size + 1); File_ReadData(fp, data, data_size); if (File_Pos(fp) != data_size) { *output_data = nullptr; LOG_ERROR("Can't read file %s", path); Memory_FreePointer(&data); File_Close(fp); return false; } File_Close(fp); data[data_size] = '\0'; *output_data = data; if (output_size != nullptr) { *output_size = data_size; } return true; } void File_CreateDirectory(const char *path) { if (path == nullptr) { return; } #if defined(_WIN32) _mkdir(path); #else mkdir(path, 0775); #endif } void File_EnsureParentDirectories(const char *path) { ASSERT(path != nullptr); char *parent = File_GetParentDirectory(path); if (parent != nullptr) { /* Only recurse/create if there is a distinct, non-empty parent */ if (parent[0] != '\0' && strcmp(parent, path) != 0) { if (!File_DirExists(parent)) { File_EnsureParentDirectories(parent); File_CreateDirectory(parent); } } Memory_FreePointer(&parent); } } void *File_OpenDirectory(const char *const path) { ASSERT(path != nullptr); return opendir(path); } const char *File_ReadDirectory(void *const dir) { DIR *path_dir = (DIR *)dir; struct dirent *cur_file = readdir(dir); if (cur_file == nullptr) { return nullptr; } return cur_file->d_name; } void File_CloseDirectory(void *const dir) { ASSERT(dir != nullptr); closedir(dir); } ================================================ FILE: src/trx/core/filesystem.h ================================================ #pragma once #include #include // Low-level filesystem module. // Intentionally dumb wrappers over file/dir primitives. No path policy, // no token expansion, and no case-normalization logic belongs here. typedef enum { FILE_SEEK_SET, FILE_SEEK_CUR, FILE_SEEK_END, } FILE_SEEK_MODE; typedef enum { FILE_OPEN_READ, FILE_OPEN_READ_WRITE, FILE_OPEN_WRITE, } FILE_OPEN_MODE; typedef struct MYFILE MYFILE; // ============================================================================ // Path functions // Return true when path points to an existing directory. bool File_DirExists(const char *path); // Return true if path is absolute for current platform conventions. bool File_IsAbsolute(const char *path); // Return true if path is not absolute. bool File_IsRelative(const char *path); // Return true when path points to an existing file. bool File_Exists(const char *path); // Return parent directory component of path (owning string), or nullptr. char *File_GetParentDirectory(const char *path); // ============================================================================ // File handle functions // Open path with requested mode and wrap as MYFILE. // Returns nullptr on failure. MYFILE *File_Open(const char *path, FILE_OPEN_MODE mode); // Current byte position in file stream. size_t File_Pos(MYFILE *file); // Total file size in bytes. size_t File_Size(MYFILE *file); // Original path passed to File_Open. const char *File_GetPath(MYFILE *file); // Get file size and modification time (seconds since epoch). // Returns false if the file cannot be opened/resolved. bool File_GetMeta(const char *path, uint64_t *out_size, uint64_t *out_mtime); // Skip forward by `bytes`. void File_Skip(MYFILE *file, size_t bytes); // Seek to position according to FILE_SEEK_MODE. void File_Seek(MYFILE *file, size_t pos, FILE_SEEK_MODE mode); // Close and free MYFILE. void File_Close(MYFILE *file); // Load entire file into memory as a null-terminated buffer. // Caller owns `output_data` and must free it. bool File_Load(const char *path, char **output_data, size_t *output_size); // ============================================================================ // Read helpers // Read exact byte count into `data`. Returns false on short read. bool File_ReadData(MYFILE *file, void *data, size_t size); // Read `count` items of `item_size` into `data`. Returns false on short read. bool File_ReadItems(MYFILE *file, void *data, size_t count, size_t item_size); // Typed scalar read helpers. int8_t File_ReadS8(MYFILE *file); int16_t File_ReadS16(MYFILE *file); int32_t File_ReadS32(MYFILE *file); uint8_t File_ReadU8(MYFILE *file); uint16_t File_ReadU16(MYFILE *file); uint32_t File_ReadU32(MYFILE *file); // ============================================================================ // Write helpers // Raw/typed write helpers. void File_WriteData(MYFILE *file, const void *data, size_t size); void File_WriteItems( MYFILE *file, const void *data, size_t count, size_t item_size); void File_WriteS8(MYFILE *file, int8_t value); void File_WriteS16(MYFILE *file, int16_t value); void File_WriteS32(MYFILE *file, int32_t value); void File_WriteU8(MYFILE *file, uint8_t value); void File_WriteU16(MYFILE *file, uint16_t value); void File_WriteU32(MYFILE *file, uint32_t value); // Write formatted string to file using a static-format buffer. // The formatted text is written via fputs; no trailing newline is added. void File_WriteString(MYFILE *file, const char *fmt, ...); // ============================================================================ // Directory functions // Create one directory path component. void File_CreateDirectory(const char *path); // Recursively ensure all parent directories for `path` exist. void File_EnsureParentDirectories(const char *path); // Directory iteration API. void *File_OpenDirectory(const char *path); const char *File_ReadDirectory(void *dir); void File_CloseDirectory(void *dir); ================================================ FILE: src/trx/core/hash.c ================================================ #include #include #define M_FNV_1A_PRIME 1099511628211ULL uint64_t Hash_FNV1a64_Init(void) { return HASH_FNV1A64_BASE; } uint64_t Hash_FNV1a64_Update( uint64_t hash, const void *const data, const size_t size) { const uint8_t *cur = data; for (size_t i = 0; i < size; i++) { hash ^= cur[i]; hash *= M_FNV_1A_PRIME; } return hash; } uint64_t Hash_FNV1a64_UpdateU32(const uint64_t hash, const uint32_t value) { return Hash_FNV1a64_Update(hash, &value, sizeof(value)); } uint64_t Hash_FNV1a64_UpdateU64(const uint64_t hash, const uint64_t value) { return Hash_FNV1a64_Update(hash, &value, sizeof(value)); } uint64_t Hash_FNV1a64_UpdateString(uint64_t hash, const char *const value) { if (value == nullptr) { return Hash_FNV1a64_UpdateU32(hash, 0); } const uint32_t len = (uint32_t)strlen(value); hash = Hash_FNV1a64_UpdateU32(hash, len); return Hash_FNV1a64_Update(hash, value, len); } ================================================ FILE: src/trx/core/hash.h ================================================ #pragma once #include #include #define HASH_FNV1A64_BASE 14695981039346656037ULL uint64_t Hash_FNV1a64_Init(void); uint64_t Hash_FNV1a64_Update(uint64_t hash, const void *data, size_t size); uint64_t Hash_FNV1a64_UpdateU32(uint64_t hash, uint32_t value); uint64_t Hash_FNV1a64_UpdateU64(uint64_t hash, uint64_t value); uint64_t Hash_FNV1a64_UpdateString(uint64_t hash, const char *value); ================================================ FILE: src/trx/core/json/base.c ================================================ #include #include #include #include #include #include static JSON_NUMBER *M_NumberNewInt(const int number) { const size_t size = snprintf(nullptr, 0, "%d", number) + 1; char *const buf = Memory_Alloc(size); sprintf(buf, "%d", number); JSON_NUMBER *const elem = Memory_Alloc(sizeof(JSON_NUMBER)); elem->number = buf; elem->number_size = strlen(buf); return elem; } static JSON_NUMBER *M_NumberNewInt64(const int64_t number) { const size_t size = snprintf(nullptr, 0, "%" PRId64, number) + 1; char *const buf = Memory_Alloc(size); sprintf(buf, "%" PRId64, number); JSON_NUMBER *const elem = Memory_Alloc(sizeof(JSON_NUMBER)); elem->number = buf; elem->number_size = strlen(buf); return elem; } static JSON_NUMBER *M_NumberNewDouble(const double number) { const size_t size = snprintf(nullptr, 0, "%f", number) + 3; char *const buf = Memory_Alloc(size); sprintf(buf, "%f", number); // Remove trailing zeros, keeping at least one digit after the decimal point char *const dot = strchr(buf, '.'); if (dot == nullptr) { strcat(buf, ".0"); } else { char *end = buf + strlen(buf) - 1; while (end > dot && *end == '0') { end--; } if (*end == '.') { // All fractional digits removed => append a single 0 to get "1.0". end[1] = '0'; end[2] = '\0'; } else { // Terminate string after the last non-zero digit to get "1.123". end[1] = '\0'; } } JSON_NUMBER *const elem = Memory_Alloc(sizeof(JSON_NUMBER)); elem->number = buf; elem->number_size = strlen(buf); return elem; } static void M_NumberFree(JSON_NUMBER *const num) { if (num->ref_count == 0) { Memory_Free(num->number); Memory_Free(num); } } static JSON_STRING *M_StringNew(const char *const string) { JSON_STRING *const str = Memory_Alloc(sizeof(JSON_STRING)); str->string = Memory_DupStr(string); str->string_size = strlen(string); return str; } static void M_StringFree(JSON_STRING *const str) { if (str->ref_count == 0) { Memory_Free(str->string); Memory_Free(str); } } static JSON_VALUE *M_ValueFromNumber(JSON_NUMBER *const num) { JSON_VALUE *const value = Memory_Alloc(sizeof(JSON_VALUE)); value->type = JSON_TYPE_NUMBER; value->payload = num; return value; } static void M_ArrayElementFree(JSON_ARRAY_ELEMENT *const element) { if (element->ref_count == 0) { Memory_Free(element); } } static void M_ObjectElementFree(JSON_OBJECT_ELEMENT *element) { if (element->ref_count == 0) { Memory_FreePointer(&element); } } JSON_VALUE *JSON_ValueFromBool(const int b) { JSON_VALUE *const value = Memory_Alloc(sizeof(JSON_VALUE)); value->type = b ? JSON_TYPE_TRUE : JSON_TYPE_FALSE; value->payload = nullptr; return value; } JSON_VALUE *JSON_ValueFromInt(const int number) { return M_ValueFromNumber(M_NumberNewInt(number)); } JSON_VALUE *JSON_ValueFromInt64(const int64_t number) { return M_ValueFromNumber(M_NumberNewInt64(number)); } JSON_VALUE *JSON_ValueFromDouble(const double number) { return M_ValueFromNumber(M_NumberNewDouble(number)); } JSON_VALUE *JSON_ValueFromString(const char *const string) { JSON_VALUE *const value = Memory_Alloc(sizeof(JSON_VALUE)); value->type = JSON_TYPE_STRING; value->payload = M_StringNew(string); return value; } JSON_VALUE *JSON_ValueFromArray(JSON_ARRAY *const arr) { JSON_VALUE *const value = Memory_Alloc(sizeof(JSON_VALUE)); value->type = JSON_TYPE_ARRAY; value->payload = arr; return value; } JSON_VALUE *JSON_ValueFromObject(JSON_OBJECT *const obj) { JSON_VALUE *const value = Memory_Alloc(sizeof(JSON_VALUE)); value->type = JSON_TYPE_OBJECT; value->payload = obj; return value; } void JSON_ValueFree(JSON_VALUE *const value) { if (value == nullptr) { return; } switch (value->type) { case JSON_TYPE_NUMBER: M_NumberFree((JSON_NUMBER *)value->payload); break; case JSON_TYPE_STRING: M_StringFree((JSON_STRING *)value->payload); break; case JSON_TYPE_ARRAY: JSON_ArrayFree((JSON_ARRAY *)value->payload); break; case JSON_TYPE_OBJECT: JSON_ObjectFree((JSON_OBJECT *)value->payload); break; case JSON_TYPE_TRUE: case JSON_TYPE_NULL: case JSON_TYPE_FALSE: break; } if (value->ref_count == 0) { Memory_Free(value); } } bool JSON_ValueIsNull(const JSON_VALUE *const value) { return value != nullptr && value->type == JSON_TYPE_NULL; } bool JSON_ValueIsTrue(const JSON_VALUE *const value) { return value != nullptr && value->type == JSON_TYPE_TRUE; } bool JSON_ValueIsFalse(const JSON_VALUE *const value) { return value != nullptr && value->type == JSON_TYPE_FALSE; } int JSON_ValueGetBool(const JSON_VALUE *const value, const int d) { if (JSON_ValueIsTrue(value)) { return 1; } else if (JSON_ValueIsFalse(value)) { return 0; } return d; } const JSON_NUMBER *JSON_ValueGetNumber(const JSON_VALUE *const value) { if (value == nullptr || value->type != JSON_TYPE_NUMBER) { return nullptr; } return (const JSON_NUMBER *)value->payload; } int JSON_ValueGetInt(const JSON_VALUE *const value, const int d) { const JSON_NUMBER *const num = JSON_ValueGetNumber(value); if (num == nullptr) { return d; } const char *const s = num->number; if (strncmp(s, "0x", 2) == 0 || strncmp(s, "0X", 2) == 0) { return strtol(s, nullptr, 16); } if (strncmp(s, "0b", 2) == 0 || strncmp(s, "0B", 2) == 0) { return strtol(s + 2, nullptr, 2); } return atoi(s); } int64_t JSON_ValueGetInt64(const JSON_VALUE *const value, const int64_t d) { const JSON_NUMBER *const num = JSON_ValueGetNumber(value); return num != nullptr ? strtoll(num->number, nullptr, 10) : d; } double JSON_ValueGetDouble(const JSON_VALUE *const value, const double d) { const JSON_NUMBER *const num = JSON_ValueGetNumber(value); return num != nullptr ? atof(num->number) : d; } const char *JSON_ValueGetString( const JSON_VALUE *const value, const char *const d) { if (value == nullptr || value->type != JSON_TYPE_STRING) { return nullptr; } const JSON_STRING *const string = value->payload; return string != nullptr ? string->string : d; } JSON_ARRAY *JSON_ValueAsArray_Impl(const JSON_VALUE *const value) { if (value == nullptr || value->type != JSON_TYPE_ARRAY) { return nullptr; } return (JSON_ARRAY *)value->payload; } JSON_OBJECT *JSON_ValueAsObject_Impl(const JSON_VALUE *const value) { if (value == nullptr || value->type != JSON_TYPE_OBJECT) { return nullptr; } return (JSON_OBJECT *)value->payload; } JSON_ARRAY *JSON_ArrayNew(void) { JSON_ARRAY *const arr = Memory_Alloc(sizeof(JSON_ARRAY)); arr->start = nullptr; arr->length = 0; return arr; } void JSON_ArrayFree(JSON_ARRAY *const arr) { JSON_ARRAY_ELEMENT *elem = arr->start; while (elem) { JSON_ARRAY_ELEMENT *const next = elem->next; JSON_ValueFree(elem->value); M_ArrayElementFree(elem); elem = next; } if (arr->ref_count == 0) { Memory_Free(arr); } } void JSON_ArrayAppend(JSON_ARRAY *const arr, JSON_VALUE *const value) { JSON_ARRAY_ELEMENT *elem = Memory_Alloc(sizeof(JSON_ARRAY_ELEMENT)); elem->value = value; elem->next = nullptr; if (arr->start) { JSON_ARRAY_ELEMENT *target = arr->start; while (target->next) { target = target->next; } target->next = elem; } else { arr->start = elem; } arr->length++; } void JSON_ArrayAppendBool(JSON_ARRAY *const arr, const int b) { JSON_ArrayAppend(arr, JSON_ValueFromBool(b)); } void JSON_ArrayAppendInt(JSON_ARRAY *const arr, const int number) { JSON_ArrayAppend(arr, JSON_ValueFromInt(number)); } void JSON_ArrayAppendDouble(JSON_ARRAY *const arr, const double number) { JSON_ArrayAppend(arr, JSON_ValueFromDouble(number)); } void JSON_ArrayAppendString(JSON_ARRAY *const arr, const char *string) { JSON_ArrayAppend(arr, JSON_ValueFromString(string)); } void JSON_ArrayAppendArray(JSON_ARRAY *const arr, JSON_ARRAY *const arr2) { JSON_ArrayAppend(arr, JSON_ValueFromArray(arr2)); } void JSON_ArrayAppendObject(JSON_ARRAY *const arr, JSON_OBJECT *const obj) { JSON_ArrayAppend(arr, JSON_ValueFromObject(obj)); } JSON_VALUE *JSON_ArrayGetValue(const JSON_ARRAY *const arr, const size_t idx) { if (arr == nullptr || idx >= arr->length) { return nullptr; } JSON_ARRAY_ELEMENT *elem = arr->start; for (size_t i = 0; i < idx; i++) { elem = elem->next; } return elem->value; } int JSON_ArrayGetBool( const JSON_ARRAY *const arr, const size_t idx, const int d) { const JSON_VALUE *const value = JSON_ArrayGetValue(arr, idx); return JSON_ValueGetBool(value, d); } int JSON_ArrayGetInt(const JSON_ARRAY *const arr, const size_t idx, const int d) { const JSON_VALUE *const value = JSON_ArrayGetValue(arr, idx); return JSON_ValueGetInt(value, d); } double JSON_ArrayGetDouble( const JSON_ARRAY *const arr, const size_t idx, const double d) { const JSON_VALUE *const value = JSON_ArrayGetValue(arr, idx); return JSON_ValueGetDouble(value, d); } const char *JSON_ArrayGetString( const JSON_ARRAY *const arr, const size_t idx, const char *const d) { const JSON_VALUE *const value = JSON_ArrayGetValue(arr, idx); return JSON_ValueGetString(value, d); } JSON_ARRAY *JSON_ArrayGetArray_Impl( const JSON_ARRAY *const arr, const size_t idx) { JSON_VALUE *const value = JSON_ArrayGetValue(arr, idx); return JSON_ValueAsArray(value); } JSON_OBJECT *JSON_ArrayGetObject_Impl( const JSON_ARRAY *const arr, const size_t idx) { JSON_VALUE *const value = JSON_ArrayGetValue(arr, idx); return JSON_ValueAsObject(value); } JSON_OBJECT *JSON_ObjectNew(void) { JSON_OBJECT *const obj = Memory_Alloc(sizeof(JSON_OBJECT)); obj->start = nullptr; obj->length = 0; return obj; } void JSON_ObjectFree(JSON_OBJECT *const obj) { JSON_OBJECT_ELEMENT *elem = obj->start; while (elem) { JSON_OBJECT_ELEMENT *next = elem->next; M_StringFree(elem->name); JSON_ValueFree(elem->value); M_ObjectElementFree(elem); elem = next; } if (obj->ref_count == 0) { Memory_Free(obj); } } void JSON_ObjectAppend( JSON_OBJECT *const obj, const char *const key, JSON_VALUE *const value) { JSON_OBJECT_ELEMENT *elem = Memory_Alloc(sizeof(JSON_OBJECT_ELEMENT)); elem->name = M_StringNew(key); elem->value = value; elem->next = nullptr; if (obj->start) { JSON_OBJECT_ELEMENT *target = obj->start; while (target->next) { target = target->next; } target->next = elem; } else { obj->start = elem; } obj->length++; } void JSON_ObjectAppendBool( JSON_OBJECT *const obj, const char *const key, const int b) { JSON_ObjectAppend(obj, key, JSON_ValueFromBool(b)); } void JSON_ObjectAppendInt( JSON_OBJECT *const obj, const char *const key, const int number) { JSON_ObjectAppend(obj, key, JSON_ValueFromInt(number)); } void JSON_ObjectAppendInt64( JSON_OBJECT *const obj, const char *const key, const int64_t number) { JSON_ObjectAppend(obj, key, JSON_ValueFromInt64(number)); } void JSON_ObjectAppendDouble( JSON_OBJECT *const obj, const char *const key, const double number) { JSON_ObjectAppend(obj, key, JSON_ValueFromDouble(number)); } void JSON_ObjectAppendString( JSON_OBJECT *const obj, const char *const key, const char *string) { JSON_ObjectAppend(obj, key, JSON_ValueFromString(string)); } void JSON_ObjectAppendArray( JSON_OBJECT *const obj, const char *const key, JSON_ARRAY *const arr) { JSON_ObjectAppend(obj, key, JSON_ValueFromArray(arr)); } void JSON_ObjectAppendObject( JSON_OBJECT *const obj, const char *const key, JSON_OBJECT *const obj2) { JSON_ObjectAppend(obj, key, JSON_ValueFromObject(obj2)); } bool JSON_ObjectContainsKey(JSON_OBJECT *const obj, const char *const key) { JSON_OBJECT_ELEMENT *elem = obj->start; while (elem != nullptr) { if (!strcmp(elem->name->string, key)) { return true; } elem = elem->next; } return false; } void JSON_ObjectEvictKey(JSON_OBJECT *const obj, const char *const key) { if (obj == nullptr) { return; } JSON_OBJECT_ELEMENT *elem = obj->start; JSON_OBJECT_ELEMENT *prev = nullptr; while (elem) { if (!strcmp(elem->name->string, key)) { if (prev == nullptr) { obj->start = elem->next; } else { prev->next = elem->next; } M_StringFree(elem->name); JSON_ValueFree(elem->value); M_ObjectElementFree(elem); obj->length--; return; } prev = elem; elem = elem->next; } } void JSON_ObjectMerge(JSON_OBJECT *const root, const JSON_OBJECT *const obj) { JSON_OBJECT_ELEMENT *elem = obj->start; while (elem != nullptr) { JSON_ObjectEvictKey(root, elem->name->string); JSON_ObjectAppend(root, elem->name->string, elem->value); elem = elem->next; } } JSON_VALUE *JSON_ObjectGetValue( const JSON_OBJECT *const obj, const char *const key) { if (obj == nullptr) { return nullptr; } JSON_OBJECT_ELEMENT *elem = obj->start; while (elem) { if (!strcmp(elem->name->string, key)) { return elem->value; } elem = elem->next; } return nullptr; } int JSON_ObjectGetBool( const JSON_OBJECT *const obj, const char *const key, const int d) { const JSON_VALUE *const value = JSON_ObjectGetValue(obj, key); return JSON_ValueGetBool(value, d); } int JSON_ObjectGetInt( const JSON_OBJECT *const obj, const char *const key, const int d) { const JSON_VALUE *const value = JSON_ObjectGetValue(obj, key); return JSON_ValueGetInt(value, d); } int64_t JSON_ObjectGetInt64( const JSON_OBJECT *const obj, const char *const key, const int64_t d) { const JSON_VALUE *const value = JSON_ObjectGetValue(obj, key); return JSON_ValueGetInt64(value, d); } double JSON_ObjectGetDouble( const JSON_OBJECT *const obj, const char *const key, const double d) { const JSON_VALUE *const value = JSON_ObjectGetValue(obj, key); return JSON_ValueGetDouble(value, d); } const char *JSON_ObjectGetString( const JSON_OBJECT *const obj, const char *const key, const char *const d) { const JSON_VALUE *const value = JSON_ObjectGetValue(obj, key); return JSON_ValueGetString(value, d); } JSON_ARRAY *JSON_ObjectGetArray_Impl( const JSON_OBJECT *const obj, const char *const key) { JSON_VALUE *const value = JSON_ObjectGetValue(obj, key); return JSON_ValueAsArray(value); } JSON_OBJECT *JSON_ObjectGetObject_Impl( const JSON_OBJECT *const obj, const char *const key) { JSON_VALUE *const value = JSON_ObjectGetValue(obj, key); return JSON_ValueAsObject(value); } ================================================ FILE: src/trx/core/json/base.h ================================================ #pragma once #include #include // values JSON_VALUE *JSON_ValueFromBool(int b); JSON_VALUE *JSON_ValueFromInt(int number); JSON_VALUE *JSON_ValueFromInt64(int64_t number); JSON_VALUE *JSON_ValueFromDouble(double number); JSON_VALUE *JSON_ValueFromString(const char *string); JSON_VALUE *JSON_ValueFromArray(JSON_ARRAY *arr); JSON_VALUE *JSON_ValueFromObject(JSON_OBJECT *obj); void JSON_ValueFree(JSON_VALUE *value); bool JSON_ValueIsNull(const JSON_VALUE *value); bool JSON_ValueIsTrue(const JSON_VALUE *value); bool JSON_ValueIsFalse(const JSON_VALUE *value); int JSON_ValueGetBool(const JSON_VALUE *value, int d); int JSON_ValueGetInt(const JSON_VALUE *value, int d); int64_t JSON_ValueGetInt64(const JSON_VALUE *value, int64_t d); double JSON_ValueGetDouble(const JSON_VALUE *value, double d); const JSON_NUMBER *JSON_ValueGetNumber(const JSON_VALUE *value); const char *JSON_ValueGetString(const JSON_VALUE *value, const char *d); JSON_ARRAY *JSON_ValueAsArray_Impl(const JSON_VALUE *value); #define JSON_ValueAsArray(value) \ JSON_CONST_DISPATCH( \ value, const JSON_ARRAY *, JSON_ValueAsArray_Impl(value)) JSON_OBJECT *JSON_ValueAsObject_Impl(const JSON_VALUE *value); #define JSON_ValueAsObject(value) \ JSON_CONST_DISPATCH( \ value, const JSON_OBJECT *, JSON_ValueAsObject_Impl(value)) // arrays JSON_ARRAY *JSON_ArrayNew(void); void JSON_ArrayFree(JSON_ARRAY *arr); void JSON_ArrayAppend(JSON_ARRAY *arr, JSON_VALUE *value); void JSON_ArrayAppendBool(JSON_ARRAY *arr, int b); void JSON_ArrayAppendInt(JSON_ARRAY *arr, int number); void JSON_ArrayAppendDouble(JSON_ARRAY *arr, double number); void JSON_ArrayAppendString(JSON_ARRAY *arr, const char *string); void JSON_ArrayAppendArray(JSON_ARRAY *arr, JSON_ARRAY *arr2); void JSON_ArrayAppendObject(JSON_ARRAY *arr, JSON_OBJECT *obj); JSON_VALUE *JSON_ArrayGetValue(const JSON_ARRAY *arr, size_t idx); int JSON_ArrayGetBool(const JSON_ARRAY *arr, size_t idx, int d); int JSON_ArrayGetInt(const JSON_ARRAY *arr, size_t idx, int d); double JSON_ArrayGetDouble(const JSON_ARRAY *arr, size_t idx, double d); const char *JSON_ArrayGetString( const JSON_ARRAY *arr, size_t idx, const char *d); JSON_ARRAY *JSON_ArrayGetArray_Impl(const JSON_ARRAY *arr, size_t idx); #define JSON_ArrayGetArray(value, ...) \ JSON_CONST_DISPATCH( \ value, const JSON_ARRAY *, \ JSON_ArrayGetArray_Impl(value, __VA_ARGS__)) JSON_OBJECT *JSON_ArrayGetObject_Impl(const JSON_ARRAY *arr, size_t idx); #define JSON_ArrayGetObject(value, ...) \ JSON_CONST_DISPATCH( \ value, const JSON_ARRAY *, \ JSON_ArrayGetObject_Impl(value, __VA_ARGS__)) // objects JSON_OBJECT *JSON_ObjectNew(void); void JSON_ObjectFree(JSON_OBJECT *obj); void JSON_ObjectAppend(JSON_OBJECT *obj, const char *key, JSON_VALUE *value); void JSON_ObjectAppendBool(JSON_OBJECT *obj, const char *key, int b); void JSON_ObjectAppendInt(JSON_OBJECT *obj, const char *key, int number); void JSON_ObjectAppendInt64(JSON_OBJECT *obj, const char *key, int64_t number); void JSON_ObjectAppendDouble(JSON_OBJECT *obj, const char *key, double number); void JSON_ObjectAppendString( JSON_OBJECT *obj, const char *key, const char *string); void JSON_ObjectAppendArray(JSON_OBJECT *obj, const char *key, JSON_ARRAY *arr); void JSON_ObjectAppendObject( JSON_OBJECT *obj, const char *key, JSON_OBJECT *obj2); bool JSON_ObjectContainsKey(JSON_OBJECT *obj, const char *key); void JSON_ObjectEvictKey(JSON_OBJECT *obj, const char *key); void JSON_ObjectMerge(JSON_OBJECT *root, const JSON_OBJECT *obj); JSON_VALUE *JSON_ObjectGetValue(const JSON_OBJECT *obj, const char *key); int JSON_ObjectGetBool(const JSON_OBJECT *obj, const char *key, int d); int JSON_ObjectGetInt(const JSON_OBJECT *obj, const char *key, int d); int64_t JSON_ObjectGetInt64(const JSON_OBJECT *obj, const char *key, int64_t d); double JSON_ObjectGetDouble(const JSON_OBJECT *obj, const char *key, double d); const char *JSON_ObjectGetString( const JSON_OBJECT *obj, const char *key, const char *d); JSON_ARRAY *JSON_ObjectGetArray_Impl(const JSON_OBJECT *obj, const char *key); #define JSON_ObjectGetArray(value, ...) \ JSON_CONST_DISPATCH( \ value, const JSON_ARRAY *, \ JSON_ObjectGetArray_Impl(value, __VA_ARGS__)) JSON_OBJECT *JSON_ObjectGetObject_Impl(const JSON_OBJECT *obj, const char *key); #define JSON_ObjectGetObject(value, ...) \ JSON_CONST_DISPATCH( \ value, const JSON_OBJECT *, \ JSON_ObjectGetObject_Impl(value, __VA_ARGS__)) ================================================ FILE: src/trx/core/json/enum.h ================================================ #pragma once typedef enum { JSON_TYPE_STRING, JSON_TYPE_NUMBER, JSON_TYPE_OBJECT, JSON_TYPE_ARRAY, JSON_TYPE_TRUE, JSON_TYPE_FALSE, JSON_TYPE_NULL } JSON_TYPE; ================================================ FILE: src/trx/core/json/parse.c ================================================ #include #include typedef struct { const char *src; size_t size; size_t offset; size_t flags_bitset; char *data; char *dom; size_t dom_size; size_t data_size; size_t line_no; size_t line_offset; size_t error; } M_STATE; static int M_GetValueSize(M_STATE *state, int is_global_object); static void M_HandleValue( M_STATE *state, int is_global_object, JSON_VALUE *value); static int M_HexDigit(const char c) { if ('0' <= c && c <= '9') { return c - '0'; } if ('a' <= c && c <= 'f') { return c - 'a' + 10; } if ('A' <= c && c <= 'F') { return c - 'A' + 10; } return -1; } static int M_HexValue( const char *c, const unsigned long size, unsigned long *result) { const char *p; int digit; if (size > sizeof(unsigned long) * 2) { return 0; } *result = 0; for (p = c; (unsigned long)(p - c) < size; ++p) { *result <<= 4; digit = M_HexDigit(*p); if (digit < 0 || digit > 15) { return 0; } *result |= (unsigned char)digit; } return 1; } static int M_SkipWhitespace(M_STATE *state) { size_t offset = state->offset; const size_t size = state->size; const char *const src = state->src; /* the only valid whitespace according to ECMA-404 is ' ', '\n', '\r' and * '\t'. */ switch (src[offset]) { default: return 0; case ' ': case '\r': case '\t': case '\n': break; } do { switch (src[offset]) { default: /* Update offset. */ state->offset = offset; return 1; case ' ': case '\r': case '\t': break; case '\n': state->line_no++; state->line_offset = offset; break; } offset++; } while (offset < size); /* Update offset. */ state->offset = offset; return 1; } static int M_SkipCStyleComments(M_STATE *state) { /* do we have a comment?. */ if ('/' == state->src[state->offset]) { /* skip '/'. */ state->offset++; if ('/' == state->src[state->offset]) { /* we had a comment of the form //. */ /* skip second '/'. */ state->offset++; while (state->offset < state->size) { switch (state->src[state->offset]) { default: /* skip the character in the comment. */ state->offset++; break; case '\n': /* if we have a newline, our comment has ended! Skip the * newline. */ state->offset++; /* we entered a newline, so move our line info forward. */ state->line_no++; state->line_offset = state->offset; return 1; } } /* we reached the end of the JSON file! */ return 1; } else if ('*' == state->src[state->offset]) { /* we had a comment in the C-style long form. */ /* skip '*'. */ state->offset++; while (state->offset + 1 < state->size) { if (('*' == state->src[state->offset]) && ('/' == state->src[state->offset + 1])) { /* we reached the end of our comment! */ state->offset += 2; return 1; } else if ('\n' == state->src[state->offset]) { /* we entered a newline, so move our line info forward. */ state->line_no++; state->line_offset = state->offset; } /* skip character within comment. */ state->offset++; } /* Comment wasn't ended correctly which is a failure. */ return 1; } } /* we didn't have any comment, which is ok too! */ return 0; } static int M_SkipAllSkippables(M_STATE *state) { /* skip all whitespace and other skippables until there are none left. note * that the previous version suffered from read past errors should. the * stream end on M_SkipCStyleComments eg. '{"a" ' with comments flag. */ int did_consume = 0; const size_t size = state->size; if (JSON_PARSE_FLAGS_ALLOW_C_STYLE_COMMENTS & state->flags_bitset) { do { if (state->offset == size) { state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return 1; } did_consume = M_SkipWhitespace(state); /* This should really be checked on access, not in front of every * call. */ if (state->offset == size) { state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return 1; } did_consume |= M_SkipCStyleComments(state); } while (0 != did_consume); } else { do { if (state->offset == size) { state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return 1; } did_consume = M_SkipWhitespace(state); } while (0 != did_consume); } if (state->offset == size) { state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return 1; } return 0; } static int M_GetStringSize(M_STATE *state, size_t is_key) { size_t offset = state->offset; const size_t size = state->size; size_t data_size = 0; const char *const src = state->src; const int is_single_quote = '\'' == src[offset]; const char quote_to_use = is_single_quote ? '\'' : '"'; const size_t flags_bitset = state->flags_bitset; unsigned long codepoint; unsigned long high_surrogate = 0; if ((JSON_PARSE_FLAGS_ALLOW_LOCATION_INFORMATION & flags_bitset) != 0 && is_key != 0) { state->dom_size += sizeof(JSON_STRING_EX); } else { state->dom_size += sizeof(JSON_STRING); } if ('"' != src[offset]) { /* if we are allowed single quoted strings check for that too. */ if (!((JSON_PARSE_FLAGS_ALLOW_SINGLE_QUOTED_STRINGS & flags_bitset) && is_single_quote)) { state->error = JSON_PARSE_ERROR_EXPECTED_OPENING_QUOTE; state->offset = offset; return 1; } } /* skip leading '"' or '\''. */ offset++; while ((offset < size) && (quote_to_use != src[offset])) { /* add space for the character. */ data_size++; switch (src[offset]) { case '\0': case '\t': state->error = JSON_PARSE_ERROR_INVALID_STRING; state->offset = offset; return 1; } if ('\\' == src[offset]) { /* skip reverse solidus character. */ offset++; if (offset == size) { state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; state->offset = offset; return 1; } switch (src[offset]) { default: state->error = JSON_PARSE_ERROR_INVALID_STRING_ESCAPE_SEQUENCE; state->offset = offset; return 1; case '"': case '\\': case '/': case 'b': case 'f': case 'n': case 'r': case 't': /* all valid characters! */ offset++; break; case 'u': if (!(offset + 5 < size)) { /* invalid escaped unicode sequence! */ state->error = JSON_PARSE_ERROR_INVALID_STRING_ESCAPE_SEQUENCE; state->offset = offset; return 1; } codepoint = 0; if (!M_HexValue(&src[offset + 1], 4, &codepoint)) { /* escaped unicode sequences must contain 4 hexadecimal * digits! */ state->error = JSON_PARSE_ERROR_INVALID_STRING_ESCAPE_SEQUENCE; state->offset = offset; return 1; } /* Valid sequence! * see: https://en.wikipedia.org/wiki/UTF-8#Invalid_code_points. * 1 7 U + 0000 U + 007F 0xxxxxxx. 2 11 * U + 0080 U + 07FF 110xxxxx 10xxxxxx. 3 16 * U + 0800 U + FFFF 1110xxxx 10xxxxxx 10xxxxxx. * 4 21 U + 10000 U + 10FFFF 11110xxx * 10xxxxxx 10xxxxxx 10xxxxxx. * Note: the high and low surrogate halves used by UTF-16 * (U+D800 through U+DFFF) and code points not encodable by * UTF-16 (those after U+10FFFF) are not legal Unicode values, * and their UTF-8 encoding must be treated as an invalid byte * sequence. */ if (high_surrogate != 0) { /* we previously read the high half of the \uxxxx\uxxxx * pair, so now we expect the low half. */ if (codepoint >= 0xdc00 && codepoint <= 0xdfff) { /* low surrogate range. */ data_size += 3; high_surrogate = 0; } else { state->error = JSON_PARSE_ERROR_INVALID_STRING_ESCAPE_SEQUENCE; state->offset = offset; return 1; } } else if (codepoint <= 0x7f) { data_size += 0; } else if (codepoint <= 0x7ff) { data_size += 1; } else if ( codepoint >= 0xd800 && codepoint <= 0xdbff) { /* high surrogate range. */ /* The codepoint is the first half of a "utf-16 surrogate * pair". so we need the other half for it to be valid: * \uHHHH\uLLLL. */ if (offset + 11 > size || '\\' != src[offset + 5] || 'u' != src[offset + 6]) { state->error = JSON_PARSE_ERROR_INVALID_STRING_ESCAPE_SEQUENCE; state->offset = offset; return 1; } high_surrogate = codepoint; } else if ( codepoint >= 0xd800 && codepoint <= 0xdfff) { /* low surrogate range. */ /* we did not read the other half before. */ state->error = JSON_PARSE_ERROR_INVALID_STRING_ESCAPE_SEQUENCE; state->offset = offset; return 1; } else { data_size += 2; } /* escaped codepoints after 0xffff are supported in json through * utf-16 surrogate pairs: \uD83D\uDD25 for U+1F525. */ offset += 5; break; } } else if (('\r' == src[offset]) || ('\n' == src[offset])) { if (!(JSON_PARSE_FLAGS_ALLOW_MULTI_LINE_STRINGS & flags_bitset)) { /* invalid escaped unicode sequence! */ state->error = JSON_PARSE_ERROR_INVALID_STRING_ESCAPE_SEQUENCE; state->offset = offset; return 1; } offset++; } else { /* skip character (valid part of sequence). */ offset++; } } /* If the offset is equal to the size, we had a non-terminated string! */ if (offset == size) { state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; state->offset = offset - 1; return 1; } /* skip trailing '"' or '\''. */ offset++; /* add enough space to store the string. */ state->data_size += data_size; /* one more byte for null terminator ending the string! */ state->data_size++; /* update offset. */ state->offset = offset; return 0; } static int M_IsValidUnquotedKeyChar(const char c) { return ( ('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('_' == c)); } static int M_GetKeySize(M_STATE *state) { const size_t flags_bitset = state->flags_bitset; if (JSON_PARSE_FLAGS_ALLOW_UNQUOTED_KEYS & flags_bitset) { size_t offset = state->offset; const size_t size = state->size; const char *const src = state->src; size_t data_size = state->data_size; /* if we are allowing unquoted keys, first grok for a quote... */ if ('"' == src[offset]) { /* ... if we got a comma, just parse the key as a string as normal. */ return M_GetStringSize(state, 1); } else if ( (JSON_PARSE_FLAGS_ALLOW_SINGLE_QUOTED_STRINGS & flags_bitset) && ('\'' == src[offset])) { /* ... if we got a comma, just parse the key as a string as normal. */ return M_GetStringSize(state, 1); } else { while ((offset < size) && M_IsValidUnquotedKeyChar(src[offset])) { offset++; data_size++; } /* one more byte for null terminator ending the string! */ data_size++; if (JSON_PARSE_FLAGS_ALLOW_LOCATION_INFORMATION & flags_bitset) { state->dom_size += sizeof(JSON_STRING_EX); } else { state->dom_size += sizeof(JSON_STRING); } /* update offset. */ state->offset = offset; /* update data_size. */ state->data_size = data_size; return 0; } } else { /* we are only allowed to have quoted keys, so just parse a string! */ return M_GetStringSize(state, 1); } } static int M_GetObjectSize(M_STATE *state, int is_global_object) { const size_t flags_bitset = state->flags_bitset; const char *const src = state->src; const size_t size = state->size; size_t elements = 0; int allow_comma = 0; int found_closing_brace = 0; if (is_global_object) { /* if we found an opening '{' of an object, we actually have a normal * JSON object at the root of the DOM... */ if (!M_SkipAllSkippables(state) && '{' == state->src[state->offset]) { /* . and we don't actually have a global object after all! */ is_global_object = 0; } } if (!is_global_object) { if ('{' != src[state->offset]) { state->error = JSON_PARSE_ERROR_UNKNOWN; return 1; } /* skip leading '{'. */ state->offset++; } state->dom_size += sizeof(JSON_OBJECT); if ((state->offset == size) && !is_global_object) { state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return 1; } do { if (!is_global_object) { if (M_SkipAllSkippables(state)) { state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return 1; } if ('}' == src[state->offset]) { /* skip trailing '}'. */ state->offset++; found_closing_brace = 1; /* finished the object! */ break; } } else { /* we don't require brackets, so that means the object ends when the * input stream ends! */ if (M_SkipAllSkippables(state)) { break; } } /* if we parsed at least once element previously, grok for a comma. */ if (allow_comma) { if (',' == src[state->offset]) { /* skip comma. */ state->offset++; allow_comma = 0; } else if (JSON_PARSE_FLAGS_ALLOW_NO_COMMAS & flags_bitset) { /* we don't require a comma, and we didn't find one, which is * ok! */ allow_comma = 0; } else { /* otherwise we are required to have a comma, and we found none. */ state->error = JSON_PARSE_ERROR_EXPECTED_COMMA_OR_CLOSING_BRACKET; return 1; } if (JSON_PARSE_FLAGS_ALLOW_TRAILING_COMMA & flags_bitset) { continue; } else { if (M_SkipAllSkippables(state)) { state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return 1; } } } if (M_GetKeySize(state)) { /* key parsing failed! */ state->error = JSON_PARSE_ERROR_INVALID_STRING; return 1; } if (M_SkipAllSkippables(state)) { state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return 1; } if (JSON_PARSE_FLAGS_ALLOW_EQUALS_IN_OBJECT & flags_bitset) { const char current = src[state->offset]; if ((':' != current) && ('=' != current)) { state->error = JSON_PARSE_ERROR_EXPECTED_COLON; return 1; } } else { if (':' != src[state->offset]) { state->error = JSON_PARSE_ERROR_EXPECTED_COLON; return 1; } } /* skip colon. */ state->offset++; if (M_SkipAllSkippables(state)) { state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return 1; } if (M_GetValueSize(state, /* is_global_object = */ 0)) { /* value parsing failed! */ return 1; } /* successfully parsed a name/value pair! */ elements++; allow_comma = 1; } while (state->offset < size); if ((state->offset == size) && !is_global_object && !found_closing_brace) { state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return 1; } state->dom_size += sizeof(JSON_OBJECT_ELEMENT) * elements; return 0; } static int M_GetArraySize(M_STATE *state) { const size_t flags_bitset = state->flags_bitset; size_t elements = 0; int allow_comma = 0; const char *const src = state->src; const size_t size = state->size; if ('[' != src[state->offset]) { /* expected array to begin with leading '['. */ state->error = JSON_PARSE_ERROR_UNKNOWN; return 1; } /* skip leading '['. */ state->offset++; state->dom_size += sizeof(JSON_ARRAY); while (state->offset < size) { if (M_SkipAllSkippables(state)) { state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return 1; } if (']' == src[state->offset]) { /* skip trailing ']'. */ state->offset++; state->dom_size += sizeof(JSON_ARRAY_ELEMENT) * elements; /* finished the object! */ return 0; } /* if we parsed at least once element previously, grok for a comma. */ if (allow_comma) { if (',' == src[state->offset]) { /* skip comma. */ state->offset++; allow_comma = 0; } else if (!(JSON_PARSE_FLAGS_ALLOW_NO_COMMAS & flags_bitset)) { state->error = JSON_PARSE_ERROR_EXPECTED_COMMA_OR_CLOSING_BRACKET; return 1; } if (JSON_PARSE_FLAGS_ALLOW_TRAILING_COMMA & flags_bitset) { allow_comma = 0; continue; } else { if (M_SkipAllSkippables(state)) { state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return 1; } } } if (M_GetValueSize(state, /* is_global_object = */ 0)) { /* value parsing failed! */ return 1; } /* successfully parsed an array element! */ elements++; allow_comma = 1; } /* we consumed the entire input before finding the closing ']' of the array! */ state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return 1; } static int M_GetNumberSize(M_STATE *state) { const size_t flags_bitset = state->flags_bitset; size_t offset = state->offset; const size_t size = state->size; int had_leading_digits = 0; const char *const src = state->src; state->dom_size += sizeof(JSON_NUMBER); if ((JSON_PARSE_FLAGS_ALLOW_HEXADECIMAL_NUMBERS & flags_bitset) && (offset + 1 < size) && ('0' == src[offset]) && (('x' == src[offset + 1]) || ('X' == src[offset + 1]))) { /* skip the leading 0x that identifies a hexadecimal number. */ offset += 2; /* consume hexadecimal digits. */ while ((offset < size) && (('0' <= src[offset] && src[offset] <= '9') || ('a' <= src[offset] && src[offset] <= 'f') || ('A' <= src[offset] && src[offset] <= 'F'))) { offset++; } } else if ( (JSON_PARSE_FLAGS_ALLOW_BINARY_NUMBERS & flags_bitset) && (offset + 1 < size) && ('0' == src[offset]) && (('b' == src[offset + 1]) || ('B' == src[offset + 1]))) { /* skip the leading 0b that identifies a binary number. */ offset += 2; /* consume binary digits. */ while ((offset < size) && ('0' <= src[offset] && src[offset] <= '1')) { offset++; } } else { int found_sign = 0; int inf_or_nan = 0; if ((offset < size) && (('-' == src[offset]) || ((JSON_PARSE_FLAGS_ALLOW_LEADING_PLUS_SIGN & flags_bitset) && ('+' == src[offset])))) { /* skip valid leading '-' or '+'. */ offset++; found_sign = 1; } if (JSON_PARSE_FLAGS_ALLOW_INF_AND_NAN & flags_bitset) { const char inf[9] = "Infinity"; const size_t inf_strlen = sizeof(inf) - 1; const char nan[4] = "NaN"; const size_t nan_strlen = sizeof(nan) - 1; if (offset + inf_strlen < size) { int found = 1; size_t i; for (i = 0; i < inf_strlen; i++) { if (inf[i] != src[offset + i]) { found = 0; break; } } if (found) { /* We found our special 'Infinity' keyword! */ offset += inf_strlen; inf_or_nan = 1; } } if (offset + nan_strlen < size) { int found = 1; size_t i; for (i = 0; i < nan_strlen; i++) { if (nan[i] != src[offset + i]) { found = 0; break; } } if (found) { /* We found our special 'NaN' keyword! */ offset += nan_strlen; inf_or_nan = 1; } } } if (found_sign && !inf_or_nan && (offset < size) && !('0' <= src[offset] && src[offset] <= '9')) { /* check if we are allowing leading '.'. */ if (!(JSON_PARSE_FLAGS_ALLOW_LEADING_OR_TRAILING_DECIMAL_POINT & flags_bitset) || ('.' != src[offset])) { /* a leading '-' must be immediately followed by any digit! */ state->error = JSON_PARSE_ERROR_INVALID_NUMBER_FORMAT; state->offset = offset; return 1; } } if ((offset < size) && ('0' == src[offset])) { /* skip valid '0'. */ offset++; /* we need to record whether we had any leading digits for checks * later. */ had_leading_digits = 1; if ((offset < size) && ('0' <= src[offset] && src[offset] <= '9')) { /* a leading '0' must not be immediately followed by any digit! */ state->error = JSON_PARSE_ERROR_INVALID_NUMBER_FORMAT; state->offset = offset; return 1; } } /* the main digits of our number next. */ while ((offset < size) && ('0' <= src[offset] && src[offset] <= '9')) { offset++; /* we need to record whether we had any leading digits for checks * later. */ had_leading_digits = 1; } if ((offset < size) && ('.' == src[offset])) { offset++; if (!('0' <= src[offset] && src[offset] <= '9')) { if (!(JSON_PARSE_FLAGS_ALLOW_LEADING_OR_TRAILING_DECIMAL_POINT & flags_bitset) || !had_leading_digits) { /* a decimal point must be followed by at least one digit. */ state->error = JSON_PARSE_ERROR_INVALID_NUMBER_FORMAT; state->offset = offset; return 1; } } /* a decimal point can be followed by more digits of course! */ while ((offset < size) && ('0' <= src[offset] && src[offset] <= '9')) { offset++; } } if ((offset < size) && ('e' == src[offset] || 'E' == src[offset])) { /* our number has an exponent! Skip 'e' or 'E'. */ offset++; if ((offset < size) && ('-' == src[offset] || '+' == src[offset])) { /* skip optional '-' or '+'. */ offset++; } if ((offset < size) && !('0' <= src[offset] && src[offset] <= '9')) { /* an exponent must have at least one digit! */ state->error = JSON_PARSE_ERROR_INVALID_NUMBER_FORMAT; state->offset = offset; return 1; } /* consume exponent digits. */ do { offset++; } while ((offset < size) && ('0' <= src[offset] && src[offset] <= '9')); } } if (offset < size) { switch (src[offset]) { case ' ': case '\t': case '\r': case '\n': case '}': case ',': case ']': /* all of the above are ok. */ break; case '=': if (JSON_PARSE_FLAGS_ALLOW_EQUALS_IN_OBJECT & flags_bitset) { break; } state->error = JSON_PARSE_ERROR_INVALID_NUMBER_FORMAT; state->offset = offset; return 1; default: state->error = JSON_PARSE_ERROR_INVALID_NUMBER_FORMAT; state->offset = offset; return 1; } } state->data_size += offset - state->offset; /* one more byte for null terminator ending the number string! */ state->data_size++; /* update offset. */ state->offset = offset; return 0; } static int M_GetValueSize(M_STATE *state, int is_global_object) { const size_t flags_bitset = state->flags_bitset; const char *const src = state->src; size_t offset; const size_t size = state->size; if (JSON_PARSE_FLAGS_ALLOW_LOCATION_INFORMATION & flags_bitset) { state->dom_size += sizeof(JSON_VALUE_EX); } else { state->dom_size += sizeof(JSON_VALUE); } if (is_global_object) { return M_GetObjectSize(state, /* is_global_object = */ 1); } else { if (M_SkipAllSkippables(state)) { state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER; return 1; } /* can cache offset now. */ offset = state->offset; switch (src[offset]) { case '"': return M_GetStringSize(state, 0); case '\'': if (JSON_PARSE_FLAGS_ALLOW_SINGLE_QUOTED_STRINGS & flags_bitset) { return M_GetStringSize(state, 0); } else { /* invalid value! */ state->error = JSON_PARSE_ERROR_INVALID_VALUE; return 1; } case '{': return M_GetObjectSize(state, /* is_global_object = */ 0); case '[': return M_GetArraySize(state); case '-': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': return M_GetNumberSize(state); case '+': if (JSON_PARSE_FLAGS_ALLOW_LEADING_PLUS_SIGN & flags_bitset) { return M_GetNumberSize(state); } else { /* invalid value! */ state->error = JSON_PARSE_ERROR_INVALID_NUMBER_FORMAT; return 1; } case '.': if (JSON_PARSE_FLAGS_ALLOW_LEADING_OR_TRAILING_DECIMAL_POINT & flags_bitset) { return M_GetNumberSize(state); } else { /* invalid value! */ state->error = JSON_PARSE_ERROR_INVALID_NUMBER_FORMAT; return 1; } default: if ((offset + 4) <= size && 't' == src[offset + 0] && 'r' == src[offset + 1] && 'u' == src[offset + 2] && 'e' == src[offset + 3]) { state->offset += 4; return 0; } else if ( (offset + 5) <= size && 'f' == src[offset + 0] && 'a' == src[offset + 1] && 'l' == src[offset + 2] && 's' == src[offset + 3] && 'e' == src[offset + 4]) { state->offset += 5; return 0; } else if ( (offset + 4) <= size && 'n' == state->src[offset + 0] && 'u' == state->src[offset + 1] && 'l' == state->src[offset + 2] && 'l' == state->src[offset + 3]) { state->offset += 4; return 0; } else if ( (JSON_PARSE_FLAGS_ALLOW_INF_AND_NAN & flags_bitset) && (offset + 3) <= size && 'N' == src[offset + 0] && 'a' == src[offset + 1] && 'N' == src[offset + 2]) { return M_GetNumberSize(state); } else if ( (JSON_PARSE_FLAGS_ALLOW_INF_AND_NAN & flags_bitset) && (offset + 8) <= size && 'I' == src[offset + 0] && 'n' == src[offset + 1] && 'f' == src[offset + 2] && 'i' == src[offset + 3] && 'n' == src[offset + 4] && 'i' == src[offset + 5] && 't' == src[offset + 6] && 'y' == src[offset + 7]) { return M_GetNumberSize(state); } /* invalid value! */ state->error = JSON_PARSE_ERROR_INVALID_VALUE; return 1; } } } static void M_HandleString(M_STATE *state, JSON_STRING *string) { size_t offset = state->offset; size_t bytes_written = 0; const char *const src = state->src; const char quote_to_use = '\'' == src[offset] ? '\'' : '"'; char *data = state->data; unsigned long high_surrogate = 0; unsigned long codepoint; string->ref_count = 1; string->string = data; /* skip leading '"' or '\''. */ offset++; while (quote_to_use != src[offset]) { if ('\\' == src[offset]) { /* skip the reverse solidus. */ offset++; switch (src[offset++]) { default: return; /* we cannot ever reach here. */ case 'u': { codepoint = 0; if (!M_HexValue(&src[offset], 4, &codepoint)) { return; /* this shouldn't happen as the value was already * validated. */ } offset += 4; if (codepoint <= 0x7fu) { data[bytes_written++] = (char)codepoint; /* 0xxxxxxx. */ } else if (codepoint <= 0x7ffu) { data[bytes_written++] = (char)(0xc0u | (codepoint >> 6)); /* 110xxxxx. */ data[bytes_written++] = (char)(0x80u | (codepoint & 0x3fu)); /* 10xxxxxx. */ } else if ( codepoint >= 0xd800 && codepoint <= 0xdbff) { /* high surrogate. */ high_surrogate = codepoint; continue; /* we need the low half to form a complete codepoint. */ } else if ( codepoint >= 0xdc00 && codepoint <= 0xdfff) { /* low surrogate. */ /* combine with the previously read half to obtain the * complete codepoint. */ const unsigned long surrogate_offset = 0x10000u - (0xD800u << 10) - 0xDC00u; codepoint = (high_surrogate << 10) + codepoint + surrogate_offset; high_surrogate = 0; data[bytes_written++] = (char)(0xF0u | (codepoint >> 18)); /* 11110xxx. */ data[bytes_written++] = (char)(0x80u | ((codepoint >> 12) & 0x3fu)); /* 10xxxxxx. */ data[bytes_written++] = (char)(0x80u | ((codepoint >> 6) & 0x3fu)); /* 10xxxxxx. */ data[bytes_written++] = (char)(0x80u | (codepoint & 0x3fu)); /* 10xxxxxx. */ } else { /* we assume the value was validated and thus is within the * valid range. */ data[bytes_written++] = (char)(0xe0u | (codepoint >> 12)); /* 1110xxxx. */ data[bytes_written++] = (char)(0x80u | ((codepoint >> 6) & 0x3fu)); /* 10xxxxxx. */ data[bytes_written++] = (char)(0x80u | (codepoint & 0x3fu)); /* 10xxxxxx. */ } } break; case '"': data[bytes_written++] = '"'; break; case '\\': data[bytes_written++] = '\\'; break; case '/': data[bytes_written++] = '/'; break; case 'b': data[bytes_written++] = '\b'; break; case 'f': data[bytes_written++] = '\f'; break; case 'n': data[bytes_written++] = '\n'; break; case 'r': data[bytes_written++] = '\r'; break; case 't': data[bytes_written++] = '\t'; break; case '\r': data[bytes_written++] = '\r'; /* check if we have a "\r\n" sequence. */ if ('\n' == src[offset]) { data[bytes_written++] = '\n'; offset++; } break; case '\n': data[bytes_written++] = '\n'; break; } } else { /* copy the character. */ data[bytes_written++] = src[offset++]; } } /* skip trailing '"' or '\''. */ offset++; /* record the size of the string. */ string->string_size = bytes_written; /* add null terminator to string. */ data[bytes_written++] = '\0'; /* move data along. */ state->data += bytes_written; /* update offset. */ state->offset = offset; } static void M_HandleKey(M_STATE *state, JSON_STRING *string) { if (JSON_PARSE_FLAGS_ALLOW_UNQUOTED_KEYS & state->flags_bitset) { const char *const src = state->src; char *const data = state->data; size_t offset = state->offset; /* if we are allowing unquoted keys, check for quoted anyway... */ if (('"' == src[offset]) || ('\'' == src[offset])) { /* ... if we got a quote, just parse the key as a string as normal. */ M_HandleString(state, string); } else { size_t size = 0; string->ref_count = 1; string->string = state->data; while (M_IsValidUnquotedKeyChar(src[offset])) { data[size++] = src[offset++]; } /* add null terminator to string. */ data[size] = '\0'; /* record the size of the string. */ string->string_size = size++; /* move data along. */ state->data += size; /* update offset. */ state->offset = offset; } } else { /* we are only allowed to have quoted keys, so just parse a string! */ M_HandleString(state, string); } } static void M_HandleObject( M_STATE *state, int is_global_object, JSON_OBJECT *object) { const size_t flags_bitset = state->flags_bitset; const size_t size = state->size; const char *const src = state->src; size_t elements = 0; int allow_comma = 0; JSON_OBJECT_ELEMENT *previous = nullptr; if (is_global_object) { /* if we skipped some whitespace, and then found an opening '{' of an. */ /* object, we actually have a normal JSON object at the root of the * DOM... */ if ('{' == src[state->offset]) { /* . and we don't actually have a global object after all! */ is_global_object = 0; } } if (!is_global_object) { /* skip leading '{'. */ state->offset++; } M_SkipAllSkippables(state); /* reset elements. */ elements = 0; while (state->offset < size) { JSON_OBJECT_ELEMENT *element = nullptr; JSON_STRING *string = nullptr; JSON_VALUE *value = nullptr; if (!is_global_object) { M_SkipAllSkippables(state); if ('}' == src[state->offset]) { /* skip trailing '}'. */ state->offset++; /* finished the object! */ break; } } else { if (M_SkipAllSkippables(state)) { /* global object ends when the file ends! */ break; } } /* if we parsed at least one element previously, grok for a comma. */ if (allow_comma) { if (',' == src[state->offset]) { /* skip comma. */ state->offset++; allow_comma = 0; continue; } } element = (JSON_OBJECT_ELEMENT *)state->dom; element->ref_count = 1; state->dom += sizeof(JSON_OBJECT_ELEMENT); if (nullptr == previous) { /* this is our first element, so record it in our object. */ object->start = element; } else { previous->next = element; } previous = element; if (JSON_PARSE_FLAGS_ALLOW_LOCATION_INFORMATION & flags_bitset) { JSON_STRING_EX *string_ex = (JSON_STRING_EX *)state->dom; state->dom += sizeof(JSON_STRING_EX); string_ex->offset = state->offset; string_ex->line_no = state->line_no; string_ex->row_no = state->offset - state->line_offset; string = &(string_ex->string); } else { string = (JSON_STRING *)state->dom; state->dom += sizeof(JSON_STRING); } element->name = string; M_HandleKey(state, string); M_SkipAllSkippables(state); /* skip colon or equals. */ state->offset++; M_SkipAllSkippables(state); if (JSON_PARSE_FLAGS_ALLOW_LOCATION_INFORMATION & flags_bitset) { JSON_VALUE_EX *value_ex = (JSON_VALUE_EX *)state->dom; state->dom += sizeof(JSON_VALUE_EX); value_ex->offset = state->offset; value_ex->line_no = state->line_no; value_ex->row_no = state->offset - state->line_offset; value = &(value_ex->value); } else { value = (JSON_VALUE *)state->dom; state->dom += sizeof(JSON_VALUE); } element->value = value; M_HandleValue(state, /* is_global_object = */ 0, value); /* successfully parsed a name/value pair! */ elements++; allow_comma = 1; } /* if we had at least one element, end the linked list. */ if (previous) { previous->next = nullptr; } if (elements == 0) { object->start = nullptr; } object->ref_count = 1; object->length = elements; } static void M_HandleArray(M_STATE *state, JSON_ARRAY *array) { const char *const src = state->src; const size_t size = state->size; size_t elements = 0; int allow_comma = 0; JSON_ARRAY_ELEMENT *previous = nullptr; /* skip leading '['. */ state->offset++; M_SkipAllSkippables(state); /* reset elements. */ elements = 0; do { JSON_ARRAY_ELEMENT *element = nullptr; JSON_VALUE *value = nullptr; M_SkipAllSkippables(state); if (']' == src[state->offset]) { /* skip trailing ']'. */ state->offset++; /* finished the array! */ break; } /* if we parsed at least one element previously, grok for a comma. */ if (allow_comma) { if (',' == src[state->offset]) { /* skip comma. */ state->offset++; allow_comma = 0; continue; } } element = (JSON_ARRAY_ELEMENT *)state->dom; element->ref_count = 1; state->dom += sizeof(JSON_ARRAY_ELEMENT); if (nullptr == previous) { /* this is our first element, so record it in our array. */ array->start = element; } else { previous->next = element; } previous = element; if (JSON_PARSE_FLAGS_ALLOW_LOCATION_INFORMATION & state->flags_bitset) { JSON_VALUE_EX *value_ex = (JSON_VALUE_EX *)state->dom; state->dom += sizeof(JSON_VALUE_EX); value_ex->offset = state->offset; value_ex->line_no = state->line_no; value_ex->row_no = state->offset - state->line_offset; value = &(value_ex->value); } else { value = (JSON_VALUE *)state->dom; state->dom += sizeof(JSON_VALUE); } element->value = value; M_HandleValue(state, /* is_global_object = */ 0, value); /* successfully parsed an array element! */ elements++; allow_comma = 1; } while (state->offset < size); /* end the linked list. */ if (previous) { previous->next = nullptr; } if (elements == 0) { array->start = nullptr; } array->ref_count = 1; array->length = elements; } static void M_HandleNumber(M_STATE *state, JSON_NUMBER *number) { const size_t flags_bitset = state->flags_bitset; size_t offset = state->offset; const size_t size = state->size; size_t bytes_written = 0; const char *const src = state->src; char *data = state->data; number->ref_count = 1; number->number = data; if (JSON_PARSE_FLAGS_ALLOW_HEXADECIMAL_NUMBERS & flags_bitset) { if (('0' == src[offset]) && (('x' == src[offset + 1]) || ('X' == src[offset + 1]))) { /* consume hexadecimal digits. */ while ((offset < size) && (('0' <= src[offset] && src[offset] <= '9') || ('a' <= src[offset] && src[offset] <= 'f') || ('A' <= src[offset] && src[offset] <= 'F') || ('x' == src[offset]) || ('X' == src[offset]))) { data[bytes_written++] = src[offset++]; } } } if (JSON_PARSE_FLAGS_ALLOW_BINARY_NUMBERS & flags_bitset) { if (('0' == src[offset]) && (('b' == src[offset + 1]) || ('b' == src[offset + 1]))) { /* consume binary digits. */ while ((offset < size) && (('0' <= src[offset] && src[offset] <= '1') || ('b' == src[offset]) || ('B' == src[offset]))) { data[bytes_written++] = src[offset++]; } } } while (offset < size) { int end = 0; switch (src[offset]) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.': case 'e': case 'E': case '+': case '-': data[bytes_written++] = src[offset++]; break; default: end = 1; break; } if (0 != end) { break; } } if (JSON_PARSE_FLAGS_ALLOW_INF_AND_NAN & flags_bitset) { const size_t inf_strlen = 8; /* = strlen("Infinity");. */ const size_t nan_strlen = 3; /* = strlen("NaN");. */ if (offset + inf_strlen < size) { if ('I' == src[offset]) { size_t i; /* We found our special 'Infinity' keyword! */ for (i = 0; i < inf_strlen; i++) { data[bytes_written++] = src[offset++]; } } } if (offset + nan_strlen < size) { if ('N' == src[offset]) { size_t i; /* We found our special 'NaN' keyword! */ for (i = 0; i < nan_strlen; i++) { data[bytes_written++] = src[offset++]; } } } } /* record the size of the number. */ number->number_size = bytes_written; /* add null terminator to number string. */ data[bytes_written++] = '\0'; /* move data along. */ state->data += bytes_written; /* update offset. */ state->offset = offset; } static void M_HandleValue( M_STATE *state, int is_global_object, JSON_VALUE *value) { const size_t flags_bitset = state->flags_bitset; const char *const src = state->src; const size_t size = state->size; size_t offset; M_SkipAllSkippables(state); /* cache offset now. */ offset = state->offset; if (is_global_object) { value->type = JSON_TYPE_OBJECT; value->payload = state->dom; state->dom += sizeof(JSON_OBJECT); M_HandleObject( state, /* is_global_object = */ 1, (JSON_OBJECT *)value->payload); } else { value->ref_count = 1; switch (src[offset]) { case '"': case '\'': value->type = JSON_TYPE_STRING; value->payload = state->dom; state->dom += sizeof(JSON_STRING); M_HandleString(state, (JSON_STRING *)value->payload); break; case '{': value->type = JSON_TYPE_OBJECT; value->payload = state->dom; state->dom += sizeof(JSON_OBJECT); M_HandleObject( state, /* is_global_object = */ 0, (JSON_OBJECT *)value->payload); break; case '[': value->type = JSON_TYPE_ARRAY; value->payload = state->dom; state->dom += sizeof(JSON_ARRAY); M_HandleArray(state, (JSON_ARRAY *)value->payload); break; case '-': case '+': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.': value->type = JSON_TYPE_NUMBER; value->payload = state->dom; state->dom += sizeof(JSON_NUMBER); M_HandleNumber(state, (JSON_NUMBER *)value->payload); break; default: if ((offset + 4) <= size && 't' == src[offset + 0] && 'r' == src[offset + 1] && 'u' == src[offset + 2] && 'e' == src[offset + 3]) { value->type = JSON_TYPE_TRUE; value->payload = nullptr; state->offset += 4; } else if ( (offset + 5) <= size && 'f' == src[offset + 0] && 'a' == src[offset + 1] && 'l' == src[offset + 2] && 's' == src[offset + 3] && 'e' == src[offset + 4]) { value->type = JSON_TYPE_FALSE; value->payload = nullptr; state->offset += 5; } else if ( (offset + 4) <= size && 'n' == src[offset + 0] && 'u' == src[offset + 1] && 'l' == src[offset + 2] && 'l' == src[offset + 3]) { value->type = JSON_TYPE_NULL; value->payload = nullptr; state->offset += 4; } else if ( (JSON_PARSE_FLAGS_ALLOW_INF_AND_NAN & flags_bitset) && (offset + 3) <= size && 'N' == src[offset + 0] && 'a' == src[offset + 1] && 'N' == src[offset + 2]) { value->type = JSON_TYPE_NUMBER; value->payload = state->dom; state->dom += sizeof(JSON_NUMBER); M_HandleNumber(state, (JSON_NUMBER *)value->payload); } else if ( (JSON_PARSE_FLAGS_ALLOW_INF_AND_NAN & flags_bitset) && (offset + 8) <= size && 'I' == src[offset + 0] && 'n' == src[offset + 1] && 'f' == src[offset + 2] && 'i' == src[offset + 3] && 'n' == src[offset + 4] && 'i' == src[offset + 5] && 't' == src[offset + 6] && 'y' == src[offset + 7]) { value->type = JSON_TYPE_NUMBER; value->payload = state->dom; state->dom += sizeof(JSON_NUMBER); M_HandleNumber(state, (JSON_NUMBER *)value->payload); } break; } } } JSON_VALUE *JSON_Parse(const void *src, size_t src_size) { return JSON_ParseEx( src, src_size, JSON_PARSE_FLAGS_DEFAULT, nullptr, nullptr, nullptr); } JSON_VALUE *JSON_ParseEx( const void *src, size_t src_size, size_t flags_bitset, void *(*alloc_func_ptr)(void *user_data, size_t size), void *user_data, JSON_PARSE_RESULT *result) { M_STATE state; void *allocation; JSON_VALUE *value; size_t total_size; int input_error; if (result) { result->error = JSON_PARSE_ERROR_NONE; result->error_offset = 0; result->error_line_no = 0; result->error_row_no = 0; } if (nullptr == src) { /* invalid src pointer was null! */ return nullptr; } state.src = (const char *)src; state.size = src_size; state.offset = 0; state.line_no = 1; state.line_offset = 0; state.error = JSON_PARSE_ERROR_NONE; state.dom_size = 0; state.data_size = 0; state.flags_bitset = flags_bitset; input_error = M_GetValueSize( &state, (int)(JSON_PARSE_FLAGS_ALLOW_GLOBAL_OBJECT & state.flags_bitset)); if (input_error == 0) { M_SkipAllSkippables(&state); if (state.offset != state.size) { /* our parsing didn't have an error, but there are characters * remaining in the input that weren't part of the JSON! */ state.error = JSON_PARSE_ERROR_UNEXPECTED_TRAILING_CHARACTERS; input_error = 1; } } if (input_error) { /* parsing value's size failed (most likely an invalid JSON DOM!). */ if (result) { result->error = state.error; result->error_offset = state.offset; result->error_line_no = state.line_no; result->error_row_no = state.offset - state.line_offset; } return nullptr; } /* our total allocation is the combination of the dom and data sizes (we. */ /* first encode the structure of the JSON, and then the data referenced by. */ /* the JSON values). */ total_size = state.dom_size + state.data_size; if (nullptr == alloc_func_ptr) { allocation = Memory_Alloc(total_size); } else { allocation = alloc_func_ptr(user_data, total_size); } if (nullptr == allocation) { /* malloc failed! */ if (result) { result->error = JSON_PARSE_ERROR_ALLOCATOR_FAILED; result->error_offset = 0; result->error_line_no = 0; result->error_row_no = 0; } return nullptr; } /* reset offset so we can reuse it. */ state.offset = 0; /* reset the line information so we can reuse it. */ state.line_no = 1; state.line_offset = 0; state.dom = (char *)allocation; state.data = state.dom + state.dom_size; if (JSON_PARSE_FLAGS_ALLOW_LOCATION_INFORMATION & state.flags_bitset) { JSON_VALUE_EX *value_ex = (JSON_VALUE_EX *)state.dom; state.dom += sizeof(JSON_VALUE_EX); value_ex->offset = state.offset; value_ex->line_no = state.line_no; value_ex->row_no = state.offset - state.line_offset; value = &(value_ex->value); } else { value = (JSON_VALUE *)state.dom; state.dom += sizeof(JSON_VALUE); } M_HandleValue( &state, (int)(JSON_PARSE_FLAGS_ALLOW_GLOBAL_OBJECT & state.flags_bitset), value); ((JSON_VALUE *)allocation)->ref_count = 0; return (JSON_VALUE *)allocation; } const char *JSON_GetErrorDescription(JSON_PARSE_ERROR error) { switch (error) { case JSON_PARSE_ERROR_NONE: return "no error"; case JSON_PARSE_ERROR_EXPECTED_COMMA_OR_CLOSING_BRACKET: return "expected comma or closing bracket"; case JSON_PARSE_ERROR_EXPECTED_COLON: return "expected colon"; case JSON_PARSE_ERROR_EXPECTED_OPENING_QUOTE: return "expected opening quote"; case JSON_PARSE_ERROR_INVALID_STRING_ESCAPE_SEQUENCE: return "invalid string escape sequence"; case JSON_PARSE_ERROR_INVALID_NUMBER_FORMAT: return "invalid number format"; case JSON_PARSE_ERROR_INVALID_VALUE: return "invalid value"; case JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER: return "premature end of buffer"; case JSON_PARSE_ERROR_INVALID_STRING: return "allocator failed"; case JSON_PARSE_ERROR_ALLOCATOR_FAILED: return "allocator failed"; case JSON_PARSE_ERROR_UNEXPECTED_TRAILING_CHARACTERS: return "unexpected trailing characters"; case JSON_PARSE_ERROR_UNKNOWN: default: return "unknown"; } } ================================================ FILE: src/trx/core/json/parse.h ================================================ #pragma once #include typedef enum { JSON_PARSE_ERROR_NONE = 0, JSON_PARSE_ERROR_EXPECTED_COMMA_OR_CLOSING_BRACKET, JSON_PARSE_ERROR_EXPECTED_COLON, JSON_PARSE_ERROR_EXPECTED_OPENING_QUOTE, JSON_PARSE_ERROR_INVALID_STRING_ESCAPE_SEQUENCE, JSON_PARSE_ERROR_INVALID_NUMBER_FORMAT, JSON_PARSE_ERROR_INVALID_VALUE, JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER, JSON_PARSE_ERROR_INVALID_STRING, JSON_PARSE_ERROR_ALLOCATOR_FAILED, JSON_PARSE_ERROR_UNEXPECTED_TRAILING_CHARACTERS, JSON_PARSE_ERROR_UNKNOWN } JSON_PARSE_ERROR; typedef enum { JSON_PARSE_FLAGS_DEFAULT = 0, /* allow trailing commas in objects and arrays. For example, both [true,] and {"a" : null,} would be allowed with this option on. */ JSON_PARSE_FLAGS_ALLOW_TRAILING_COMMA = 0x1, /* allow unquoted keys for objects. For example, {a : null} would be allowed with this option on. */ JSON_PARSE_FLAGS_ALLOW_UNQUOTED_KEYS = 0x2, /* allow a global unbracketed object. For example, a : null, b : true, c : {} would be allowed with this option on. */ JSON_PARSE_FLAGS_ALLOW_GLOBAL_OBJECT = 0x4, /* allow objects to use '=' instead of ':' between key/value pairs. For example, a = null, b : true would be allowed with this option on. */ JSON_PARSE_FLAGS_ALLOW_EQUALS_IN_OBJECT = 0x8, /* allow that objects don't have to have comma separators between key/value pairs. */ JSON_PARSE_FLAGS_ALLOW_NO_COMMAS = 0x10, /* allow c-style comments (either variants) to be ignored in the input JSON file. */ JSON_PARSE_FLAGS_ALLOW_C_STYLE_COMMENTS = 0x20, /* deprecated flag, unused. */ JSON_PARSE_FLAGS_DEPRECATED = 0x40, /* record location information for each value. */ JSON_PARSE_FLAGS_ALLOW_LOCATION_INFORMATION = 0x80, /* allow strings to be 'single quoted'. */ JSON_PARSE_FLAGS_ALLOW_SINGLE_QUOTED_STRINGS = 0x100, /* allow numbers to be binary. */ JSON_PARSE_FLAGS_ALLOW_BINARY_NUMBERS = 0x4000, /* allow numbers to be hexadecimal. */ JSON_PARSE_FLAGS_ALLOW_HEXADECIMAL_NUMBERS = 0x200, /* allow numbers like +123 to be parsed. */ JSON_PARSE_FLAGS_ALLOW_LEADING_PLUS_SIGN = 0x400, /* allow numbers like .0123 or 123. to be parsed. */ JSON_PARSE_FLAGS_ALLOW_LEADING_OR_TRAILING_DECIMAL_POINT = 0x800, /* allow Infinity, -Infinity, NaN, -NaN. */ JSON_PARSE_FLAGS_ALLOW_INF_AND_NAN = 0x1000, /* allow multi line string values. */ JSON_PARSE_FLAGS_ALLOW_MULTI_LINE_STRINGS = 0x2000, /* allow simplified JSON to be parsed. Simplified JSON is an enabling of a set of other parsing options. */ JSON_PARSE_FLAGS_ALLOW_SIMPLIFIED_JSON = (JSON_PARSE_FLAGS_ALLOW_TRAILING_COMMA | JSON_PARSE_FLAGS_ALLOW_UNQUOTED_KEYS | JSON_PARSE_FLAGS_ALLOW_GLOBAL_OBJECT | JSON_PARSE_FLAGS_ALLOW_EQUALS_IN_OBJECT | JSON_PARSE_FLAGS_ALLOW_NO_COMMAS), /* allow JSON5 to be parsed. JSON5 is an enabling of a set of other parsing options. */ JSON_PARSE_FLAGS_ALLOW_JSON5 = (JSON_PARSE_FLAGS_ALLOW_TRAILING_COMMA | JSON_PARSE_FLAGS_ALLOW_UNQUOTED_KEYS | JSON_PARSE_FLAGS_ALLOW_C_STYLE_COMMENTS | JSON_PARSE_FLAGS_ALLOW_SINGLE_QUOTED_STRINGS | JSON_PARSE_FLAGS_ALLOW_HEXADECIMAL_NUMBERS | JSON_PARSE_FLAGS_ALLOW_BINARY_NUMBERS | JSON_PARSE_FLAGS_ALLOW_LEADING_PLUS_SIGN | JSON_PARSE_FLAGS_ALLOW_LEADING_OR_TRAILING_DECIMAL_POINT | JSON_PARSE_FLAGS_ALLOW_INF_AND_NAN | JSON_PARSE_FLAGS_ALLOW_MULTI_LINE_STRINGS) } JSON_PARSE_FLAGS; /* Parse a JSON text file, returning a pointer to the root of the JSON * structure. JSON_Parse performs 1 call to malloc for the entire encoding. * Returns 0 if an error occurred (malformed JSON input, or malloc failed). */ JSON_VALUE *JSON_Parse(const void *src, size_t src_size); /* Parse a JSON text file, returning a pointer to the root of the JSON * structure. JSON_Parse performs 1 call to alloc_func_ptr for the entire * encoding. Returns 0 if an error occurred (malformed JSON input, or malloc * failed). If an error occurred, the result struct (if not nullptr) will * explain the type of error, and the location in the input it occurred. If * alloc_func_ptr is null then malloc is used. */ JSON_VALUE *JSON_ParseEx( const void *src, size_t src_size, size_t flags_bitset, void *(*alloc_func_ptr)(void *, size_t), void *user_data, JSON_PARSE_RESULT *result); const char *JSON_GetErrorDescription(JSON_PARSE_ERROR error); ================================================ FILE: src/trx/core/json/types.h ================================================ #pragma once #define JSON_INVALID_BOOL -1 #define JSON_INVALID_STRING nullptr #define JSON_INVALID_NUMBER 0x7FFFFFFF #include #include #include #define json_uintmax_t uintmax_t #define json_strtoumax strtoumax #define JSON_CONST_DISPATCH(arg, ctype, call) \ _Generic(0 ? (arg) : (void *)1, const void *: (ctype)call, default: call) typedef struct { void *payload; size_t type; size_t ref_count; } JSON_VALUE; typedef struct { char *string; size_t string_size; size_t ref_count; } JSON_STRING; typedef struct { JSON_STRING string; size_t offset; size_t line_no; size_t row_no; } JSON_STRING_EX; typedef struct { char *number; size_t number_size; size_t ref_count; } JSON_NUMBER; typedef struct JSON_OBJECT_ELEMENT { JSON_STRING *name; JSON_VALUE *value; struct JSON_OBJECT_ELEMENT *next; size_t ref_count; } JSON_OBJECT_ELEMENT; typedef struct { JSON_OBJECT_ELEMENT *start; size_t length; size_t ref_count; } JSON_OBJECT; typedef struct JSON_ARRAY_ELEMENT { JSON_VALUE *value; struct JSON_ARRAY_ELEMENT *next; size_t ref_count; } JSON_ARRAY_ELEMENT; typedef struct { JSON_ARRAY_ELEMENT *start; size_t length; size_t ref_count; } JSON_ARRAY; typedef struct { JSON_VALUE value; size_t offset; size_t line_no; size_t row_no; } JSON_VALUE_EX; typedef struct { size_t error; size_t error_offset; size_t error_line_no; size_t error_row_no; } JSON_PARSE_RESULT; ================================================ FILE: src/trx/core/json/util/file.c ================================================ #include #include #include #include #include #include #include #include #define M_PARSE_FLAGS \ (JSON_PARSE_FLAGS_ALLOW_JSON5 | JSON_PARSE_FLAGS_ALLOW_LOCATION_INFORMATION) void JSONFile_ExitWithReadIOError( const JSON_READ_IO *const io, const char *const fallback_message) { const char *const error = JSON_ReadIO_GetError(io); if (error != nullptr && error[0] != '\0') { char log_message[1024]; char dialog_message[1024]; JSON_ReadIO_FormatError(io, false, log_message, sizeof(log_message)); JSON_ReadIO_FormatError( io, true, dialog_message, sizeof(dialog_message)); Shell_ExitSystemEx(log_message, dialog_message); } Shell_ExitSystem(fallback_message); } JSON_VALUE *JSONFile_Read(const char *path) { return JSONFile_ReadEx(path, false); } JSON_VALUE *JSONFile_ReadEx(const char *path, const bool exit_on_error) { char *file_data = nullptr; if (!File_Load(path, &file_data, nullptr)) { return nullptr; } JSON_PARSE_RESULT pr; JSON_VALUE *const value = JSON_ParseEx( file_data, strlen(file_data), M_PARSE_FLAGS, nullptr, nullptr, &pr); if (value == nullptr) { JSON_READ_IO *const io = JSON_ReadIO_Create(nullptr, 0, path); JSON_ReadIO_SetErrorAt( io, pr.error_line_no, pr.error_row_no, "%s", JSON_GetErrorDescription(pr.error)); if (exit_on_error) { JSONFile_ExitWithReadIOError(io, "JSON parse error"); } else { char log_message[1024]; JSON_ReadIO_FormatError( io, false, log_message, sizeof(log_message)); LOG_ERROR("%s", log_message); } JSON_ReadIO_Destroy(io); } Memory_FreePointer(&file_data); return value; } bool JSONFile_Write(const char *path, JSON_VALUE *const value) { char *old_data = nullptr; File_Load(path, &old_data, nullptr); size_t out_len; char *out_data = JSON_WritePretty(value, " ", "\n", &out_len); bool updated = false; if (old_data == nullptr || strcmp(old_data, out_data) != 0) { MYFILE *const fp = File_Open(path, FILE_OPEN_WRITE); if (fp == nullptr) { LOG_ERROR("unable to open '%s' for writing", path); } else { LOG_DEBUG("saving JSON to %s", path); File_WriteData(fp, out_data, out_len - 1); // w/o \0 File_Close(fp); updated = true; } } Memory_FreePointer(&old_data); Memory_FreePointer(&out_data); return updated; } ================================================ FILE: src/trx/core/json/util/file.h ================================================ #pragma once #include typedef struct JSON_READ_IO JSON_READ_IO; // Read and parse a JSON5 file. Missing files will return nullptr. // @param path Path to read. // @return The root JSON_VALUE, or nullptr on I/O/parse failure. Caller // must free the result with JSON_ValueFree(). JSON_VALUE *JSONFile_Read(const char *path); // Like JSONFile_Read(), except optionally exits on parse error. JSON_VALUE *JSONFile_ReadEx(const char *path, bool exit_on_error); // Format and hard-exit with the JSON read error details. void JSONFile_ExitWithReadIOError( const JSON_READ_IO *io, const char *fallback_message); // Write a JSON_VALUE to disk (pretty-printed), overwriting only if changed. // @param path Path to read. // @param root Value to write to the file. // @return Returns true if the file was written; false on error or no-op. bool JSONFile_Write(const char *path, JSON_VALUE *root); ================================================ FILE: src/trx/core/json/util/read_io.c ================================================ #include #include #include #include #include #include #include #include #define M_MAX_STACK_SIZE 10 typedef struct JSON_READ_IO { char source_path[256]; char path[256]; int32_t path_index_stack[M_MAX_STACK_SIZE]; int32_t path_top; char error_msg[256]; char error_path[256]; char error_body[256]; int32_t error_line; int32_t error_col; JSON_VALUE *stack[M_MAX_STACK_SIZE]; JSON_VALUE *current; size_t current_pos; uint16_t version; } JSON_READ_IO; static void M_SetErrorV( JSON_READ_IO *const io, const int32_t line, const int32_t col, const bool has_explicit_location, const char *const fmt, va_list ap) { char body[256]; vsnprintf(body, sizeof(body), fmt, ap); int32_t final_line = line; int32_t final_col = col; if (!has_explicit_location) { final_line = -1; final_col = -1; if (io != nullptr && io->source_path[0] != '\0' && io->current != nullptr) { // File-backed JSON parses in this codebase are location-enabled. // Avoid probing non-file trees (e.g. BSON) where value_ex layout // is not guaranteed. const JSON_VALUE_EX *const value_ex = (const JSON_VALUE_EX *)io->current; const size_t offset = value_ex->offset; const size_t line_val = value_ex->line_no; const size_t col_val = value_ex->row_no; if (offset <= 16 * 1024 * 1024 && line_val > 0 && line_val <= 1024 * 1024 && col_val <= 1024 * 1024) { final_line = (int32_t)line_val; final_col = (int32_t)col_val; } } } if (io != nullptr) { io->error_line = final_line; io->error_col = final_col; snprintf(io->error_body, sizeof(io->error_body), "%s", body); if (io->path[0] != '\0') { snprintf(io->error_path, sizeof(io->error_path), "%s", io->path); } else { io->error_path[0] = '\0'; } } if (io == nullptr) { return; } if (io->source_path[0] != '\0') { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-truncation" if (final_line >= 0 && final_col >= 0) { if (io->path[0] != '\0') { snprintf( io->error_msg, sizeof(io->error_msg), "Error parsing '%s' (line %d, col %d): %s - %s", io->source_path, final_line, final_col, io->path, body); } else { snprintf( io->error_msg, sizeof(io->error_msg), "Error parsing '%s' (line %d, col %d): %s", io->source_path, final_line, final_col, body); } } else { if (io->path[0] != '\0') { snprintf( io->error_msg, sizeof(io->error_msg), "Error parsing '%s': %s - %s", io->source_path, io->path, body); } else { snprintf( io->error_msg, sizeof(io->error_msg), "Error parsing '%s': %s", io->source_path, body); } } #pragma GCC diagnostic pop } else { if (final_line >= 0 && final_col >= 0) { snprintf( io->error_msg, sizeof(io->error_msg), "(line %d, col %d): %.200s", final_line, final_col, body); } else { snprintf(io->error_msg, sizeof(io->error_msg), "%.200s", body); } } } static void M_SetError(JSON_READ_IO *const io, const char *fmt, ...) { va_list ap; va_start(ap, fmt); M_SetErrorV(io, -1, -1, false, fmt, ap); va_end(ap); } static void M_SetErrorAt( JSON_READ_IO *const io, const int32_t line, const int32_t col, const char *fmt, ...) { va_list ap; va_start(ap, fmt); M_SetErrorV(io, line, col, true, fmt, ap); va_end(ap); } static bool M_PushPathKey(JSON_READ_IO *const io, const char *const key) { if (io->path_top + 1 >= M_MAX_STACK_SIZE) { return false; } const size_t pos = strlen(io->path); io->path_index_stack[io->path_top++] = pos; if (pos != 0) { strncat(io->path, ".", sizeof(io->path) - strlen(io->path) - 1); } strncat(io->path, key, sizeof(io->path) - strlen(io->path) - 1); return true; } static bool M_PushPathIndex(JSON_READ_IO *const io, const size_t idx) { if (io->path_top + 1 >= M_MAX_STACK_SIZE) { return false; } io->path_index_stack[io->path_top++] = strlen(io->path); char tmp[32]; snprintf(tmp, sizeof(tmp), "[%zu]", idx); strncat(io->path, tmp, sizeof(io->path) - strlen(io->path) - 1); return true; } static void M_PopPath(JSON_READ_IO *const io) { if (io->path_top <= 0) { io->path[0] = '\0'; io->path_top = 0; return; } int pos = io->path_index_stack[--io->path_top]; io->path[pos] = '\0'; } static bool M_PushValue(JSON_READ_IO *const io, JSON_VALUE *const value) { if (value == nullptr) { M_SetError(io, "pushing null value"); return false; } if (io->current_pos + 1 >= M_MAX_STACK_SIZE) { M_SetError(io, "stack overflow"); return false; } io->current_pos++; io->stack[io->current_pos] = value; io->current = io->stack[io->current_pos]; return true; } static bool M_ReadBoolCurrent(JSON_READ_IO *const io, bool *const target) { if (JSON_ValueIsTrue(io->current)) { *target = true; return true; } else if (JSON_ValueIsFalse(io->current)) { *target = false; return true; } else { M_SetError(io, "not a bool"); return false; } } #define L_DEFINE_M_READ_NUM_CURRENT(type_, name, minv, maxv) \ static bool M_ReadNumCurrent_##name( \ JSON_READ_IO *const io, void *const target) \ { \ if (io->current->type != JSON_TYPE_NUMBER) { \ M_SetError(io, "not a number"); \ return false; \ } \ const long long val = JSON_ValueGetInt(io->current, 0); \ if (val < (long long)(minv) || val > (long long)(maxv)) { \ M_SetError(io, "value out of range: %lld", val); \ return false; \ } \ const type_ parsed = (type_)val; \ memcpy(target, &parsed, sizeof(parsed)); \ return true; \ } L_DEFINE_M_READ_NUM_CURRENT(int8_t, S8, INT8_MIN, INT8_MAX) L_DEFINE_M_READ_NUM_CURRENT(int16_t, S16, INT16_MIN, INT16_MAX) L_DEFINE_M_READ_NUM_CURRENT(int32_t, S32, INT32_MIN, INT32_MAX) L_DEFINE_M_READ_NUM_CURRENT(uint8_t, U8, 0, UINT8_MAX) L_DEFINE_M_READ_NUM_CURRENT(uint16_t, U16, 0, UINT16_MAX) L_DEFINE_M_READ_NUM_CURRENT(uint32_t, U32, 0, UINT32_MAX) #undef L_DEFINE_M_READ_NUM_CURRENT static bool M_ReadNumCurrent_Double( JSON_READ_IO *const io, double *const target) { if (io->current->type != JSON_TYPE_NUMBER) { M_SetError(io, "not a number"); return false; } const double val = JSON_ValueGetDouble(io->current, -1.0); memcpy(target, &val, sizeof(val)); return true; } static bool M_ReadNumCurrent_Float(JSON_READ_IO *const io, float *const target) { if (io->current->type != JSON_TYPE_NUMBER) { M_SetError(io, "not a number"); return false; } const double val = JSON_ValueGetDouble(io->current, -1.0); const float parsed = (float)val; memcpy(target, &parsed, sizeof(parsed)); return true; } static bool M_ReadStringCurrent( JSON_READ_IO *const io, const char **const target) { if (io->current->type != JSON_TYPE_STRING) { M_SetError(io, "not a string"); return false; } *target = JSON_ValueGetString(io->current, nullptr); return *target != nullptr; } bool JSON_ReadIO_ReadXYZ32Current( JSON_READ_IO *const io, void *const target_void) { XYZ_32 *const target = target_void; JSON_ARRAY *const tuple = JSON_ValueAsArray(io->current); if (tuple != nullptr) { const int32_t tuple_len = tuple->length; if (tuple_len != 3) { M_SetError(io, "XYZ tuple must have exactly 3 values"); JSON_FAIL(); } JSON_MUST(JSON_READ_A(io, 0, &target->x)); JSON_MUST(JSON_READ_A(io, 1, &target->y)); JSON_MUST(JSON_READ_A(io, 2, &target->z)); } else { JSON_MUST(JSON_READ(io, "x", &target->x)); JSON_MUST(JSON_READ(io, "y", &target->y)); JSON_MUST(JSON_READ(io, "z", &target->z)); } JSON_FINISH(); } bool JSON_ReadIO_ReadXYZ16Current( JSON_READ_IO *const io, void *const target_void) { XYZ_32 tmp; JSON_MUST(JSON_ReadIO_ReadXYZ32Current(io, &tmp)); if (tmp.x < INT16_MIN || tmp.x > INT16_MAX || tmp.y < INT16_MIN || tmp.y > INT16_MAX || tmp.z < INT16_MIN || tmp.z > INT16_MAX) { M_SetError(io, "XYZ16 value out of range"); JSON_FAIL(); } XYZ_16 *const target = target_void; target->x = tmp.x; target->y = tmp.y; target->z = tmp.z; JSON_FINISH(); } static bool M_ReadRGB888Current(JSON_READ_IO *const io, RGB_888 *const target) { JSON_ARRAY *const tuple = JSON_ValueAsArray(io->current); if (tuple != nullptr) { const int32_t tuple_len = tuple->length; if (tuple_len != 3) { M_SetError(io, "RGB array must have exactly 3 values"); JSON_FAIL(); } RGB_F color = { -1.0f, -1.0f, -1.0f }; JSON_MUST(JSON_READ_A(io, 0, &color.r)); JSON_MUST(JSON_READ_A(io, 1, &color.g)); JSON_MUST(JSON_READ_A(io, 2, &color.b)); if (color.r < 0.0f || color.g < 0.0f || color.b < 0.0f || color.r > 1.0f || color.g > 1.0f || color.b > 1.0f) { M_SetError(io, "RGB array values must be in range 0.0..1.0"); JSON_FAIL(); } *target = (RGB_888) { (uint8_t)(color.r * 255.0f), (uint8_t)(color.g * 255.0f), (uint8_t)(color.b * 255.0f), }; } else { const char *str = nullptr; JSON_MUST(JSON_READ_CURRENT(io, &str)); if (!String_ParseRGB888(str, target)) { M_SetError(io, "invalid RGB color string"); JSON_FAIL(); } } JSON_FINISH(); } static bool M_ReadRGBA8888Current( JSON_READ_IO *const io, RGBA_8888 *const target) { const char *str = nullptr; JSON_MUST(JSON_READ_CURRENT(io, &str)); if (!String_ParseRGBA8888(str, target)) { M_SetError(io, "invalid RGBA color string"); JSON_FAIL(); } JSON_FINISH(); } #define L_DEFINE_JSON_READ_IO_TYPE(name, ctype, impl_func) \ bool JSON_ReadIO_Read##name##Current( \ JSON_READ_IO *const io, void *const target) \ { \ return impl_func(io, (ctype *)target); \ } L_DEFINE_JSON_READ_IO_TYPE(Bool, bool, M_ReadBoolCurrent) L_DEFINE_JSON_READ_IO_TYPE(S8, int8_t, M_ReadNumCurrent_S8) L_DEFINE_JSON_READ_IO_TYPE(U8, uint8_t, M_ReadNumCurrent_U8) L_DEFINE_JSON_READ_IO_TYPE(S16, int16_t, M_ReadNumCurrent_S16) L_DEFINE_JSON_READ_IO_TYPE(U16, uint16_t, M_ReadNumCurrent_U16) L_DEFINE_JSON_READ_IO_TYPE(S32, int32_t, M_ReadNumCurrent_S32) L_DEFINE_JSON_READ_IO_TYPE(U32, uint32_t, M_ReadNumCurrent_U32) L_DEFINE_JSON_READ_IO_TYPE(Float, float, M_ReadNumCurrent_Float) L_DEFINE_JSON_READ_IO_TYPE(Double, double, M_ReadNumCurrent_Double) L_DEFINE_JSON_READ_IO_TYPE(String, const char *, M_ReadStringCurrent) L_DEFINE_JSON_READ_IO_TYPE(RGB888, RGB_888, M_ReadRGB888Current) L_DEFINE_JSON_READ_IO_TYPE(RGBA8888, RGBA_8888, M_ReadRGBA8888Current) #undef L_DEFINE_JSON_READ_IO_TYPE const char *JSON_ReadIO_GetError(const JSON_READ_IO *const io) { return io->error_msg; } const char *JSON_ReadIO_GetErrorPath(const JSON_READ_IO *const io) { return io->error_path; } const char *JSON_ReadIO_GetErrorBody(const JSON_READ_IO *const io) { return io->error_body; } int32_t JSON_ReadIO_GetErrorLine(const JSON_READ_IO *const io) { return io->error_line; } int32_t JSON_ReadIO_GetErrorCol(const JSON_READ_IO *const io) { return io->error_col; } uint16_t JSON_ReadIO_GetVersion(const JSON_READ_IO *const io) { return io->version; } void JSON_ReadIO_FormatError( const JSON_READ_IO *const io, const bool multiline, char *const buffer, const size_t buffer_size) { const char *const body = JSON_ReadIO_GetErrorBody(io); const char *const path = JSON_ReadIO_GetErrorPath(io); const char *const source_path = io->source_path; const int32_t line = JSON_ReadIO_GetErrorLine(io); const int32_t col = JSON_ReadIO_GetErrorCol(io); const char *const separator = multiline ? "\n" : " "; if (buffer_size == 0) { return; } if (source_path == nullptr || source_path[0] == '\0') { snprintf(buffer, buffer_size, "%s", JSON_ReadIO_GetError(io)); return; } if (line >= 0 && col >= 0) { if (path != nullptr && path[0] != '\0') { snprintf( buffer, buffer_size, "Error parsing '%s' (line %d, col %d):%s%s - %s", source_path, line, col, separator, path, body); } else { snprintf( buffer, buffer_size, "Error parsing '%s' (line %d, col %d):%s%s", source_path, line, col, separator, body); } } else { if (path != nullptr && path[0] != '\0') { snprintf( buffer, buffer_size, "Error parsing '%s':%s%s - %s", source_path, separator, path, body); } else { snprintf( buffer, buffer_size, "Error parsing '%s':%s%s", source_path, separator, body); } } } void JSON_ReadIO_SetError(JSON_READ_IO *const io, const char *fmt, ...) { va_list ap; va_start(ap, fmt); char body[256]; vsnprintf(body, sizeof(body), fmt, ap); va_end(ap); M_SetError(io, "%s", body); } void JSON_ReadIO_SetErrorAt( JSON_READ_IO *const io, const int32_t line, const int32_t col, const char *fmt, ...) { va_list ap; va_start(ap, fmt); char body[256]; vsnprintf(body, sizeof(body), fmt, ap); va_end(ap); M_SetErrorAt(io, line, col, "%s", body); } bool JSON_ReadIO_PushObject(JSON_READ_IO *const io, const char *const key) { JSON_OBJECT *const current_obj = JSON_ValueAsObject(io->current); if (current_obj == nullptr) { M_SetError(io, "not an object"); return false; } JSON_VALUE *const child = JSON_ObjectGetValue(current_obj, key); if (child == nullptr) { M_SetError(io, "key does not exist: %s", key); return false; } if (!M_PushPathKey(io, key)) { M_SetError(io, "path depth overflow"); return false; } return M_PushValue(io, child); } bool JSON_ReadIO_PushArrayElem(JSON_READ_IO *const io, const size_t index) { JSON_ARRAY *const current_arr = JSON_ValueAsArray(io->current); if (current_arr == nullptr) { M_SetError(io, "not an array"); return false; } JSON_VALUE *const child = JSON_ArrayGetValue(current_arr, index); if (child == nullptr) { M_SetError(io, "invalid array index"); return false; } if (!M_PushPathIndex(io, index)) { M_SetError(io, "path depth overflow"); return false; } return M_PushValue(io, child); } bool JSON_ReadIO_Pop(JSON_READ_IO *const io) { if (io->current_pos == 0) { M_SetError(io, "pop from empty stack"); return false; } io->current_pos--; io->current = io->stack[io->current_pos]; M_PopPath(io); return true; } int32_t JSON_ReadIO_GetArrayLength(JSON_READ_IO *const io) { JSON_ARRAY *const arr = JSON_ValueAsArray(io->current); if (arr == nullptr) { M_SetError(io, "not an array"); return -1; } return arr->length; } bool JSON_ReadIO_HasKey(JSON_READ_IO *const io, const char *const key) { JSON_OBJECT *const obj = JSON_ValueAsObject(io->current); if (obj == nullptr) { return false; } return JSON_ObjectContainsKey(obj, key); } JSON_OBJECT *JSON_ReadIO_GetCurrentObject(JSON_READ_IO *const io) { return JSON_ValueAsObject(io->current); } JSON_VALUE *JSON_ReadIO_GetCurrentValue(JSON_READ_IO *const io) { return io->current; } JSON_READ_IO *JSON_ReadIO_Create( JSON_VALUE *const root, const uint16_t version, const char *const source_path) { JSON_READ_IO *const io = Memory_Alloc(sizeof(*io)); if (source_path != nullptr) { snprintf(io->source_path, sizeof(io->source_path), "%s", source_path); } else { io->source_path[0] = '\0'; } io->stack[0] = root; io->current_pos = 0; io->current = io->stack[0]; io->version = version; return io; } void JSON_ReadIO_Destroy(JSON_READ_IO *const io) { Memory_Free(io); } ================================================ FILE: src/trx/core/json/util/read_io.h ================================================ #pragma once #include #include #include #include #include #include typedef struct JSON_READ_IO JSON_READ_IO; typedef struct { void *tmp; } JSON_READ_IO_DUMMY; #define JSON_READ_IO_TYPE_LIST_BASE(X) \ X(Bool, bool) \ X(S8, int8_t) \ X(U8, uint8_t) \ X(S16, int16_t) \ X(U16, uint16_t) \ X(S32, int32_t) \ X(U32, uint32_t) \ X(Float, float) \ X(Double, double) \ X(XYZ16, XYZ_16) \ X(XYZ32, XYZ_32) \ X(RGB888, RGB_888) \ X(RGBA8888, RGBA_8888) \ X(String, const char *) #define JSON_READ_IO_TYPE_LIST JSON_READ_IO_TYPE_LIST_BASE #define JSON_READ_IO_TYPE_TO_CURRENT_FN(name, ctype) \ ctype: \ JSON_ReadIO_Read##name##Current, // ============================================================================ // Public APIs JSON_READ_IO *JSON_ReadIO_Create( JSON_VALUE *root, uint16_t version, const char *source_path); void JSON_ReadIO_Destroy(JSON_READ_IO *io); const char *JSON_ReadIO_GetError(const JSON_READ_IO *io); const char *JSON_ReadIO_GetErrorPath(const JSON_READ_IO *io); const char *JSON_ReadIO_GetErrorBody(const JSON_READ_IO *io); int32_t JSON_ReadIO_GetErrorLine(const JSON_READ_IO *io); int32_t JSON_ReadIO_GetErrorCol(const JSON_READ_IO *io); uint16_t JSON_ReadIO_GetVersion(const JSON_READ_IO *io); void JSON_ReadIO_FormatError( const JSON_READ_IO *io, bool multiline, char *buffer, size_t buffer_size); void JSON_ReadIO_SetError(JSON_READ_IO *io, const char *fmt, ...); void JSON_ReadIO_SetErrorAt( JSON_READ_IO *io, int32_t line, int32_t col, const char *fmt, ...); bool JSON_ReadIO_PushObject(JSON_READ_IO *io, const char *key); bool JSON_ReadIO_PushArrayElem(JSON_READ_IO *io, size_t index); bool JSON_ReadIO_Pop(JSON_READ_IO *io); int32_t JSON_ReadIO_GetArrayLength(JSON_READ_IO *io); bool JSON_ReadIO_HasKey(JSON_READ_IO *io, const char *key); JSON_OBJECT *JSON_ReadIO_GetCurrentObject(JSON_READ_IO *io); JSON_VALUE *JSON_ReadIO_GetCurrentValue(JSON_READ_IO *io); #define L_DECLARE_JSON_READ_IO_TYPE(name, ctype) \ bool JSON_ReadIO_Read##name##Current(JSON_READ_IO *io, void *target); JSON_READ_IO_TYPE_LIST(L_DECLARE_JSON_READ_IO_TYPE) #undef L_DECLARE_JSON_READ_IO_TYPE #define JSON_PUSH(io, key) JSON_ReadIO_PushObject((io), (key)) #define JSON_PUSH_INDEX(io, idx) JSON_ReadIO_PushArrayElem((io), (idx)) #define JSON_POP(io) JSON_ReadIO_Pop((io)) #define JSON_ARRAY_LEN(io) JSON_ReadIO_GetArrayLength((io)) // Read the value into target_ptr from the current stack value. #define JSON_READ_CURRENT(io, target_ptr) \ _Generic( \ *(target_ptr), \ JSON_READ_IO_TYPE_LIST(JSON_READ_IO_TYPE_TO_CURRENT_FN) \ JSON_READ_IO_DUMMY: JSON_ReadIO_ReadS32Current)( \ (io), (target_ptr)) // Push a key onto stack, read the value into target_ptr, and pop the value. // Fails if the key is missing, or reading failed. #define JSON_READ(io, key, target_ptr) \ (JSON_PUSH((io), (key)) \ ? (JSON_READ_CURRENT((io), (target_ptr)) ? JSON_POP((io)) \ : (JSON_POP((io)), false)) \ : false) // Like JSON_READ(), except also supports default value to fall back to. #define JSON_READ_D(io, key, target_ptr, default_value) \ ((*(target_ptr) = (default_value)), JSON_READ((io), (key), (target_ptr))) // Like JSON_READ(), except push an array index instead of an object key. #define JSON_READ_A(io, idx, target_ptr) \ (JSON_PUSH_INDEX((io), (idx)) \ ? (JSON_READ_CURRENT((io), (target_ptr)) ? JSON_POP((io)) \ : (JSON_POP((io)), false)) \ : false) // ============================================================================ // Control flow macros. // Users of this API are expected to consume JSON_ReadIO_GetError() in the // outermost scope. Example usage: // // static bool M_CustomSectionReader(JSON_READ_IO *io) // { // int32_t foo_value; // JSON_MUST(JSON_READ(io, "foo", &foo_value); // JSON_FINISH(); // } // // static void M_OuterReader(void) // { // JSON_READ_IO *io = JSON_ReadIO_Create(…); // bool success = M_CustomSectionReader(io); // JSON_ReadIO_Destroy(io); // } // If the expr fails, log an error, and carry on. #define JSON_SHOULD(expr) \ ((expr) ? 1 : (LOG_WARNING("%s", JSON_ReadIO_GetError(io)), 0)) // Do nothing. #define JSON_OPTIONAL(expr) (expr) // If the expr fails, immediately go to the failure route. #define JSON_MUST(expr) \ if (!(expr)) { \ goto fail; \ } // Immediately go to the failure route (with `goto fail`). #define JSON_FAIL() goto fail; // Declare the failure route: by default, it just does return a bool. // To be used at the end of the function. #define JSON_FINISH() \ do { \ success: \ return true; \ fail: \ return false; \ } while (0); ================================================ FILE: src/trx/core/json/util/write_io.c ================================================ #include #include #include #include #define M_MAX_STACK_SIZE 10 typedef struct JSON_WRITE_IO { JSON_VALUE *stack[M_MAX_STACK_SIZE]; JSON_VALUE *current; size_t current_pos; } JSON_WRITE_IO; static void M_PushValue(JSON_WRITE_IO *const io, JSON_VALUE *const value) { io->current_pos++; io->stack[io->current_pos] = value; io->current = value; } static void M_Pop(JSON_WRITE_IO *const io) { io->current_pos--; io->current = io->stack[io->current_pos]; } void JSON_WriteIO_PushObject(JSON_WRITE_IO *const io) { JSON_OBJECT *const child = JSON_ObjectNew(); M_PushValue(io, JSON_ValueFromObject(child)); } void JSON_WriteIO_PushArray(JSON_WRITE_IO *const io) { JSON_ARRAY *const child = JSON_ArrayNew(); M_PushValue(io, JSON_ValueFromArray(child)); } void JSON_WriteIO_PopAndSet(JSON_WRITE_IO *const io, const char *const key) { JSON_OBJECT *const parent = JSON_ValueAsObject(io->stack[io->current_pos - 1]); ASSERT(parent != nullptr); JSON_ObjectAppend(parent, key, io->current); M_Pop(io); } void JSON_WriteIO_DiscardCurrent(JSON_WRITE_IO *const io) { JSON_ValueFree(io->current); M_Pop(io); } void JSON_WriteIO_PopAndSetNZ(JSON_WRITE_IO *const io, const char *const key) { const JSON_OBJECT *const obj = JSON_ValueAsObject(io->current); if (obj != nullptr && obj->length == 0) { JSON_WriteIO_DiscardCurrent(io); return; } const JSON_ARRAY *const arr = JSON_ValueAsArray(io->current); if (arr != nullptr && arr->length == 0) { JSON_WriteIO_DiscardCurrent(io); return; } JSON_WriteIO_PopAndSet(io, key); } void JSON_WriteIO_PopAndAppend(JSON_WRITE_IO *const io) { JSON_ARRAY *const parent = JSON_ValueAsArray(io->stack[io->current_pos - 1]); ASSERT(parent != nullptr); JSON_ArrayAppend(parent, io->current); M_Pop(io); } JSON_OBJECT *JSON_WriteIO_GetCurrentObject(JSON_WRITE_IO *const io) { return JSON_ValueAsObject(io->current); } void JSON_WriteIO_PushString(JSON_WRITE_IO *const io, const char *const value) { M_PushValue(io, JSON_ValueFromString(value)); } void JSON_WriteIO_PushBool(JSON_WRITE_IO *const io, const bool value) { M_PushValue(io, JSON_ValueFromBool(value)); } void JSON_WriteIO_PushInt(JSON_WRITE_IO *const io, const int32_t value) { M_PushValue(io, JSON_ValueFromInt(value)); } void JSON_WriteIO_PushDouble(JSON_WRITE_IO *const io, const double value) { M_PushValue(io, JSON_ValueFromDouble(value)); } void JSON_WriteIO_PushRGB888(JSON_WRITE_IO *const io, const RGB_888 value) { JSON_WriteIO_PushArray(io); JSON_WriteIO_PushDouble(io, (double)value.r / 255.0); JSON_WriteIO_PopAndAppend(io); JSON_WriteIO_PushDouble(io, (double)value.g / 255.0); JSON_WriteIO_PopAndAppend(io); JSON_WriteIO_PushDouble(io, (double)value.b / 255.0); JSON_WriteIO_PopAndAppend(io); } void JSON_WriteIO_PushXYZ16(JSON_WRITE_IO *const io, const XYZ_16 value) { JSON_WriteIO_PushObject(io); JSON_WriteIO_PushInt(io, value.x); JSON_WriteIO_PopAndSet(io, "x"); JSON_WriteIO_PushInt(io, value.y); JSON_WriteIO_PopAndSet(io, "y"); JSON_WriteIO_PushInt(io, value.z); JSON_WriteIO_PopAndSet(io, "z"); } void JSON_WriteIO_PushXYZ32(JSON_WRITE_IO *const io, const XYZ_32 value) { JSON_WriteIO_PushObject(io); JSON_WriteIO_PushInt(io, value.x); JSON_WriteIO_PopAndSet(io, "x"); JSON_WriteIO_PushInt(io, value.y); JSON_WriteIO_PopAndSet(io, "y"); JSON_WriteIO_PushInt(io, value.z); JSON_WriteIO_PopAndSet(io, "z"); } JSON_WRITE_IO *JSON_WriteIO_Create(void) { JSON_WRITE_IO *const io = Memory_Alloc(sizeof(*io)); JSON_OBJECT *const root_obj = JSON_ObjectNew(); io->stack[0] = JSON_ValueFromObject(root_obj); io->current = io->stack[0]; return io; } void JSON_WriteIO_Destroy(JSON_WRITE_IO *const io) { JSON_ValueFree(io->stack[0]); Memory_Free(io); } JSON_VALUE *JSON_WriteIO_GetRoot(JSON_WRITE_IO *const io) { ASSERT(io->stack[0] != nullptr); return io->stack[0]; } ================================================ FILE: src/trx/core/json/util/write_io.h ================================================ #pragma once #include #include #include #include #include #include typedef struct JSON_WRITE_IO JSON_WRITE_IO; typedef enum JSON_WRITE_TYPE { JSON_WRITE_TYPE_BOOL = 0, JSON_WRITE_TYPE_INT, JSON_WRITE_TYPE_DOUBLE, JSON_WRITE_TYPE_STRING, } JSON_WRITE_TYPE; JSON_WRITE_IO *JSON_WriteIO_Create(void); void JSON_WriteIO_Destroy(JSON_WRITE_IO *io); JSON_VALUE *JSON_WriteIO_GetRoot(JSON_WRITE_IO *io); void JSON_WriteIO_PushObject(JSON_WRITE_IO *io); void JSON_WriteIO_PushArray(JSON_WRITE_IO *io); void JSON_WriteIO_PopAndSet(JSON_WRITE_IO *io, const char *key); void JSON_WriteIO_PopAndSetNZ(JSON_WRITE_IO *io, const char *key); void JSON_WriteIO_PopAndAppend(JSON_WRITE_IO *io); void JSON_WriteIO_DiscardCurrent(JSON_WRITE_IO *io); JSON_OBJECT *JSON_WriteIO_GetCurrentObject(JSON_WRITE_IO *io); void JSON_WriteIO_PushBool(JSON_WRITE_IO *io, bool value); void JSON_WriteIO_PushInt(JSON_WRITE_IO *io, int32_t value); void JSON_WriteIO_PushDouble(JSON_WRITE_IO *io, double value); void JSON_WriteIO_PushRGB888(JSON_WRITE_IO *io, RGB_888 value); void JSON_WriteIO_PushXYZ16(JSON_WRITE_IO *io, XYZ_16 value); void JSON_WriteIO_PushXYZ32(JSON_WRITE_IO *io, XYZ_32 value); void JSON_WriteIO_PushString(JSON_WRITE_IO *io, const char *value); #define JSONW_PUSH_OBJECT(io) JSON_WriteIO_PushObject((io)) #define JSONW_PUSH_ARRAY(io) JSON_WriteIO_PushArray((io)) #define JSONW_POP_AND_SET(io, key) JSON_WriteIO_PopAndSet((io), (key)) #define JSONW_POP_AND_SET_NZ(io, key) JSON_WriteIO_PopAndSetNZ((io), (key)) #define JSONW_POP_AND_APPEND(io) JSON_WriteIO_PopAndAppend((io)) #define JSONW_PUSH_VALUE(io, value) \ _Generic( \ (value), \ bool: JSON_WriteIO_PushBool, \ int8_t: JSON_WriteIO_PushInt, \ uint8_t: JSON_WriteIO_PushInt, \ int16_t: JSON_WriteIO_PushInt, \ uint16_t: JSON_WriteIO_PushInt, \ int32_t: JSON_WriteIO_PushInt, \ uint32_t: JSON_WriteIO_PushInt, \ float: JSON_WriteIO_PushDouble, \ double: JSON_WriteIO_PushDouble, \ RGB_888: JSON_WriteIO_PushRGB888, \ XYZ_16: JSON_WriteIO_PushXYZ16, \ XYZ_32: JSON_WriteIO_PushXYZ32, \ const char *: JSON_WriteIO_PushString, \ char *: JSON_WriteIO_PushString)(io, value) #define JSONW_WRITE(io, key, value) \ do { \ JSONW_PUSH_VALUE((io), (value)); \ JSONW_POP_AND_SET((io), (key)); \ } while (0) #define JSONW_WRITE_NZ(io, key, value) \ do { \ typeof(value) _tmp = (value); \ unsigned char _zero[sizeof(_tmp)] = { 0 }; \ if (memcmp(&_tmp, _zero, sizeof(_tmp)) != 0) { \ JSONW_WRITE(io, key, _tmp); \ } \ } while (0) ================================================ FILE: src/trx/core/json/write.c ================================================ #include #include static int M_GetValueSize_Minified(const JSON_VALUE *value, size_t *size); static char *M_WriteValue_Minified(const JSON_VALUE *value, char *data); static int M_GetValueSize_Pretty( const JSON_VALUE *value, size_t depth, size_t indent_size, size_t newline_size, size_t *size); static char *M_WriteValue_Pretty( const JSON_VALUE *value, size_t depth, const char *indent, const char *newline, char *data); static int M_GetNumberSize(const JSON_NUMBER *number, size_t *size) { json_uintmax_t parsed_number; size_t i; if (number->number_size >= 2) { switch (number->number[1]) { case 'x': case 'X': /* the number is a JSON_PARSE_FLAGS_ALLOW_HEXADECIMAL_NUMBERS * hexadecimal so we have to do extra work to convert it to a * non-hexadecimal for JSON output. */ parsed_number = json_strtoumax(number->number, nullptr, 0); i = 0; while (0 != parsed_number) { parsed_number /= 10; i++; } *size += i; return 0; } } /* check to see if the number has leading/trailing decimal point. */ i = 0; /* skip any leading '+' or '-'. */ if ((i < number->number_size) && (('+' == number->number[i]) || ('-' == number->number[i]))) { i++; } /* check if we have infinity. */ if ((i < number->number_size) && ('I' == number->number[i])) { const char *inf = "Infinity"; size_t k; for (k = i; k < number->number_size; k++) { const char c = *inf++; /* Check if we found the Infinity string! */ if ('\0' == c) { break; } else if (c != number->number[k]) { break; } } if ('\0' == *inf) { /* Inf becomes 1.7976931348623158e308 because JSON can't support it. */ *size += 22; /* if we had a leading '-' we need to record it in the JSON output. */ if ('-' == number->number[0]) { *size += 1; } } return 0; } /* check if we have nan. */ if ((i < number->number_size) && ('N' == number->number[i])) { const char *nan = "NaN"; size_t k; for (k = i; k < number->number_size; k++) { const char c = *nan++; /* Check if we found the NaN string! */ if ('\0' == c) { break; } else if (c != number->number[k]) { break; } } if ('\0' == *nan) { /* NaN becomes 1 because JSON can't support it. */ *size += 1; return 0; } } /* if we had a leading decimal point. */ if ((i < number->number_size) && ('.' == number->number[i])) { /* 1 + because we had a leading decimal point. */ *size += 1; goto cleanup; } for (; i < number->number_size; i++) { const char c = number->number[i]; if (!('0' <= c && c <= '9')) { break; } } /* if we had a trailing decimal point. */ if ((i + 1 == number->number_size) && ('.' == number->number[i])) { /* 1 + because we had a trailing decimal point. */ *size += 1; goto cleanup; } cleanup: *size += number->number_size; /* the actual string of the number. */ /* if we had a leading '+' we don't record it in the JSON output. */ if ('+' == number->number[0]) { *size -= 1; } return 0; } static int M_GetStringSize(const JSON_STRING *string, size_t *size) { size_t i; for (i = 0; i < string->string_size; i++) { switch (string->string[i]) { case '"': case '\\': case '\b': case '\f': case '\n': case '\r': case '\t': *size += 2; break; default: *size += 1; break; } } *size += 2; /* need to encode the surrounding '"' characters. */ return 0; } static int M_GetArraySize_Minified(const JSON_ARRAY *array, size_t *size) { JSON_ARRAY_ELEMENT *element; *size += 2; /* '[' and ']'. */ if (1 < array->length) { *size += array->length - 1; /* ','s seperate each element. */ } for (element = array->start; nullptr != element; element = element->next) { if (M_GetValueSize_Minified(element->value, size)) { /* value was malformed! */ return 1; } } return 0; } static int M_GetObjectSize_Minified(const JSON_OBJECT *object, size_t *size) { JSON_OBJECT_ELEMENT *element; *size += 2; /* '{' and '}'. */ *size += object->length; /* ':'s seperate each name/value pair. */ if (1 < object->length) { *size += object->length - 1; /* ','s seperate each element. */ } for (element = object->start; nullptr != element; element = element->next) { if (M_GetStringSize(element->name, size)) { /* string was malformed! */ return 1; } if (M_GetValueSize_Minified(element->value, size)) { /* value was malformed! */ return 1; } } return 0; } static int M_GetValueSize_Minified(const JSON_VALUE *value, size_t *size) { switch (value->type) { default: /* unknown value type found! */ return 1; case JSON_TYPE_NUMBER: return M_GetNumberSize((JSON_NUMBER *)value->payload, size); case JSON_TYPE_STRING: return M_GetStringSize((JSON_STRING *)value->payload, size); case JSON_TYPE_ARRAY: return M_GetArraySize_Minified((JSON_ARRAY *)value->payload, size); case JSON_TYPE_OBJECT: return M_GetObjectSize_Minified((JSON_OBJECT *)value->payload, size); case JSON_TYPE_TRUE: *size += 4; /* the string "true". */ return 0; case JSON_TYPE_FALSE: *size += 5; /* the string "false". */ return 0; case JSON_TYPE_NULL: *size += 4; /* the string "null". */ return 0; } } static char *M_WriteNumber(const JSON_NUMBER *number, char *data) { json_uintmax_t parsed_number, backup; size_t i; if (number->number_size >= 2) { switch (number->number[1]) { case 'x': case 'X': /* The number is a JSON_PARSE_FLAGS_ALLOW_HEXADECIMAL_NUMBERS * hexadecimal so we have to do extra work to convert it to a * non-hexadecimal for JSON output. */ parsed_number = json_strtoumax(number->number, nullptr, 0); /* We need a copy of parsed number twice, so take a backup of it. */ backup = parsed_number; i = 0; while (0 != parsed_number) { parsed_number /= 10; i++; } /* Restore parsed_number to its original value stored in the backup. */ parsed_number = backup; /* Now use backup to take a copy of i, or the length of the string. */ backup = i; do { *(data + i - 1) = '0' + (char)(parsed_number % 10); parsed_number /= 10; i--; } while (0 != parsed_number); data += backup; return data; } } /* check to see if the number has leading/trailing decimal point. */ i = 0; /* skip any leading '-'. */ if ((i < number->number_size) && (('+' == number->number[i]) || ('-' == number->number[i]))) { i++; } /* check if we have infinity. */ if ((i < number->number_size) && ('I' == number->number[i])) { const char *inf = "Infinity"; size_t k; for (k = i; k < number->number_size; k++) { const char c = *inf++; /* Check if we found the Infinity string! */ if ('\0' == c) { break; } else if (c != number->number[k]) { break; } } if ('\0' == *inf++) { const char *dbl_max; /* if we had a leading '-' we need to record it in the JSON output. */ if ('-' == number->number[0]) { *data++ = '-'; } /* Inf becomes 1.7976931348623158e308 because JSON can't support it. */ for (dbl_max = "1.7976931348623158e308"; '\0' != *dbl_max; dbl_max++) { *data++ = *dbl_max; } return data; } } /* check if we have nan. */ if ((i < number->number_size) && ('N' == number->number[i])) { const char *nan = "NaN"; size_t k; for (k = i; k < number->number_size; k++) { const char c = *nan++; /* Check if we found the NaN string! */ if ('\0' == c) { break; } else if (c != number->number[k]) { break; } } if ('\0' == *nan++) { /* NaN becomes 0 because JSON can't support it. */ *data++ = '0'; return data; } } /* if we had a leading decimal point. */ if ((i < number->number_size) && ('.' == number->number[i])) { i = 0; /* skip any leading '+'. */ if ('+' == number->number[i]) { i++; } /* output the leading '-' if we had one. */ if ('-' == number->number[i]) { *data++ = '-'; i++; } /* insert a '0' to fix the leading decimal point for JSON output. */ *data++ = '0'; /* and output the rest of the number as normal. */ for (; i < number->number_size; i++) { *data++ = number->number[i]; } return data; } for (; i < number->number_size; i++) { const char c = number->number[i]; if (!('0' <= c && c <= '9')) { break; } } /* if we had a trailing decimal point. */ if ((i + 1 == number->number_size) && ('.' == number->number[i])) { i = 0; /* skip any leading '+'. */ if ('+' == number->number[i]) { i++; } /* output the leading '-' if we had one. */ if ('-' == number->number[i]) { *data++ = '-'; i++; } /* and output the rest of the number as normal. */ for (; i < number->number_size; i++) { *data++ = number->number[i]; } /* insert a '0' to fix the trailing decimal point for JSON output. */ *data++ = '0'; return data; } i = 0; /* skip any leading '+'. */ if ('+' == number->number[i]) { i++; } for (; i < number->number_size; i++) { *data++ = number->number[i]; } return data; } static char *M_WriteString(const JSON_STRING *string, char *data) { size_t i; *data++ = '"'; /* open the string. */ for (i = 0; i < string->string_size; i++) { switch (string->string[i]) { case '"': *data++ = '\\'; /* escape the control character. */ *data++ = '"'; break; case '\\': *data++ = '\\'; /* escape the control character. */ *data++ = '\\'; break; case '\b': *data++ = '\\'; /* escape the control character. */ *data++ = 'b'; break; case '\f': *data++ = '\\'; /* escape the control character. */ *data++ = 'f'; break; case '\n': *data++ = '\\'; /* escape the control character. */ *data++ = 'n'; break; case '\r': *data++ = '\\'; /* escape the control character. */ *data++ = 'r'; break; case '\t': *data++ = '\\'; /* escape the control character. */ *data++ = 't'; break; default: *data++ = string->string[i]; break; } } *data++ = '"'; /* close the string. */ return data; } static char *M_WriteArray_Minified(const JSON_ARRAY *array, char *data) { JSON_ARRAY_ELEMENT *element = nullptr; *data++ = '['; /* open the array. */ for (element = array->start; nullptr != element; element = element->next) { if (element != array->start) { *data++ = ','; /* ','s seperate each element. */ } data = M_WriteValue_Minified(element->value, data); if (nullptr == data) { /* value was malformed! */ return nullptr; } } *data++ = ']'; /* close the array. */ return data; } static char *M_WriteObject_Minified(const JSON_OBJECT *object, char *data) { JSON_OBJECT_ELEMENT *element = nullptr; *data++ = '{'; /* open the object. */ for (element = object->start; nullptr != element; element = element->next) { if (element != object->start) { *data++ = ','; /* ','s seperate each element. */ } data = M_WriteString(element->name, data); if (nullptr == data) { /* string was malformed! */ return nullptr; } *data++ = ':'; /* ':'s seperate each name/value pair. */ data = M_WriteValue_Minified(element->value, data); if (nullptr == data) { /* value was malformed! */ return nullptr; } } *data++ = '}'; /* close the object. */ return data; } static char *M_WriteValue_Minified(const JSON_VALUE *value, char *data) { switch (value->type) { default: /* unknown value type found! */ return nullptr; case JSON_TYPE_NUMBER: return M_WriteNumber((JSON_NUMBER *)value->payload, data); case JSON_TYPE_STRING: return M_WriteString((JSON_STRING *)value->payload, data); case JSON_TYPE_ARRAY: return M_WriteArray_Minified((JSON_ARRAY *)value->payload, data); case JSON_TYPE_OBJECT: return M_WriteObject_Minified((JSON_OBJECT *)value->payload, data); case JSON_TYPE_TRUE: data[0] = 't'; data[1] = 'r'; data[2] = 'u'; data[3] = 'e'; return data + 4; case JSON_TYPE_FALSE: data[0] = 'f'; data[1] = 'a'; data[2] = 'l'; data[3] = 's'; data[4] = 'e'; return data + 5; case JSON_TYPE_NULL: data[0] = 'n'; data[1] = 'u'; data[2] = 'l'; data[3] = 'l'; return data + 4; } } void *JSON_WriteMinified(const JSON_VALUE *value, size_t *out_size) { size_t size = 0; char *data = nullptr; char *data_end = nullptr; if (nullptr == value) { return nullptr; } if (M_GetValueSize_Minified(value, &size)) { /* value was malformed! */ return nullptr; } size += 1; /* for the '\0' null terminating character. */ data = (char *)Memory_Alloc(size); if (nullptr == data) { /* malloc failed! */ return nullptr; } data_end = M_WriteValue_Minified(value, data); if (nullptr == data_end) { /* bad chi occurred! */ Memory_Free(data); return nullptr; } /* null terminated the string. */ *data_end = '\0'; if (nullptr != out_size) { *out_size = size; } return data; } static int M_GetArraySize_Pretty( const JSON_ARRAY *array, size_t depth, size_t indent_size, size_t newline_size, size_t *size) { JSON_ARRAY_ELEMENT *element; *size += 1; /* '['. */ if (0 < array->length) { /* if we have any elements we need to add a newline after our '['. */ *size += newline_size; *size += array->length - 1; /* ','s seperate each element. */ for (element = array->start; nullptr != element; element = element->next) { /* each element gets an indent. */ *size += (depth + 1) * indent_size; if (M_GetValueSize_Pretty( element->value, depth + 1, indent_size, newline_size, size)) { /* value was malformed! */ return 1; } /* each element gets a newline too. */ *size += newline_size; } /* since we wrote out some elements, need to add a newline and * indentation. */ /* to the trailing ']'. */ *size += depth * indent_size; } *size += 1; /* ']'. */ return 0; } static int M_GetObjectSize_Pretty( const JSON_OBJECT *object, size_t depth, size_t indent_size, size_t newline_size, size_t *size) { JSON_OBJECT_ELEMENT *element; *size += 1; /* '{'. */ if (0 < object->length) { *size += newline_size; /* need a newline next. */ *size += object->length - 1; /* ','s seperate each element. */ for (element = object->start; nullptr != element; element = element->next) { /* each element gets an indent and newline. */ *size += (depth + 1) * indent_size; *size += newline_size; if (M_GetStringSize(element->name, size)) { /* string was malformed! */ return 1; } *size += 2; /* seperate each name/value pair with ": ". */ if (M_GetValueSize_Pretty( element->value, depth + 1, indent_size, newline_size, size)) { /* value was malformed! */ return 1; } } *size += depth * indent_size; } *size += 1; /* '}'. */ return 0; } static int M_GetValueSize_Pretty( const JSON_VALUE *value, size_t depth, size_t indent_size, size_t newline_size, size_t *size) { switch (value->type) { default: /* unknown value type found! */ return 1; case JSON_TYPE_NUMBER: return M_GetNumberSize((JSON_NUMBER *)value->payload, size); case JSON_TYPE_STRING: return M_GetStringSize((JSON_STRING *)value->payload, size); case JSON_TYPE_ARRAY: return M_GetArraySize_Pretty( (JSON_ARRAY *)value->payload, depth, indent_size, newline_size, size); case JSON_TYPE_OBJECT: return M_GetObjectSize_Pretty( (JSON_OBJECT *)value->payload, depth, indent_size, newline_size, size); case JSON_TYPE_TRUE: *size += 4; /* the string "true". */ return 0; case JSON_TYPE_FALSE: *size += 5; /* the string "false". */ return 0; case JSON_TYPE_NULL: *size += 4; /* the string "null". */ return 0; } } static char *M_WriteArray_Pretty( const JSON_ARRAY *array, size_t depth, const char *indent, const char *newline, char *data) { size_t k, m; JSON_ARRAY_ELEMENT *element; *data++ = '['; /* open the array. */ if (0 < array->length) { for (k = 0; '\0' != newline[k]; k++) { *data++ = newline[k]; } for (element = array->start; nullptr != element; element = element->next) { if (element != array->start) { *data++ = ','; /* ','s seperate each element. */ for (k = 0; '\0' != newline[k]; k++) { *data++ = newline[k]; } } for (k = 0; k < depth + 1; k++) { for (m = 0; '\0' != indent[m]; m++) { *data++ = indent[m]; } } data = M_WriteValue_Pretty( element->value, depth + 1, indent, newline, data); if (nullptr == data) { /* value was malformed! */ return nullptr; } } for (k = 0; '\0' != newline[k]; k++) { *data++ = newline[k]; } for (k = 0; k < depth; k++) { for (m = 0; '\0' != indent[m]; m++) { *data++ = indent[m]; } } } *data++ = ']'; /* close the array. */ return data; } static char *M_WriteObject_Pretty( const JSON_OBJECT *object, size_t depth, const char *indent, const char *newline, char *data) { size_t k, m; JSON_OBJECT_ELEMENT *element; *data++ = '{'; /* open the object. */ if (0 < object->length) { for (k = 0; '\0' != newline[k]; k++) { *data++ = newline[k]; } for (element = object->start; nullptr != element; element = element->next) { if (element != object->start) { *data++ = ','; /* ','s seperate each element. */ for (k = 0; '\0' != newline[k]; k++) { *data++ = newline[k]; } } for (k = 0; k < depth + 1; k++) { for (m = 0; '\0' != indent[m]; m++) { *data++ = indent[m]; } } data = M_WriteString(element->name, data); if (nullptr == data) { /* string was malformed! */ return nullptr; } /* ": "s seperate each name/value pair. */ *data++ = ':'; *data++ = ' '; data = M_WriteValue_Pretty( element->value, depth + 1, indent, newline, data); if (nullptr == data) { /* value was malformed! */ return nullptr; } } for (k = 0; '\0' != newline[k]; k++) { *data++ = newline[k]; } for (k = 0; k < depth; k++) { for (m = 0; '\0' != indent[m]; m++) { *data++ = indent[m]; } } } *data++ = '}'; /* close the object. */ return data; } static char *M_WriteValue_Pretty( const JSON_VALUE *value, size_t depth, const char *indent, const char *newline, char *data) { switch (value->type) { default: /* unknown value type found! */ return nullptr; case JSON_TYPE_NUMBER: return M_WriteNumber((JSON_NUMBER *)value->payload, data); case JSON_TYPE_STRING: return M_WriteString((JSON_STRING *)value->payload, data); case JSON_TYPE_ARRAY: return M_WriteArray_Pretty( (JSON_ARRAY *)value->payload, depth, indent, newline, data); case JSON_TYPE_OBJECT: return M_WriteObject_Pretty( (JSON_OBJECT *)value->payload, depth, indent, newline, data); case JSON_TYPE_TRUE: data[0] = 't'; data[1] = 'r'; data[2] = 'u'; data[3] = 'e'; return data + 4; case JSON_TYPE_FALSE: data[0] = 'f'; data[1] = 'a'; data[2] = 'l'; data[3] = 's'; data[4] = 'e'; return data + 5; case JSON_TYPE_NULL: data[0] = 'n'; data[1] = 'u'; data[2] = 'l'; data[3] = 'l'; return data + 4; } } void *JSON_WritePretty( const JSON_VALUE *value, const char *indent, const char *newline, size_t *out_size) { size_t size = 0; size_t indent_size = 0; size_t newline_size = 0; char *data = nullptr; char *data_end = nullptr; if (nullptr == value) { return nullptr; } if (nullptr == indent) { indent = " "; /* default to two spaces. */ } if (nullptr == newline) { newline = "\n"; /* default to linux newlines. */ } while ('\0' != indent[indent_size]) { ++indent_size; /* skip non-null terminating characters. */ } while ('\0' != newline[newline_size]) { ++newline_size; /* skip non-null terminating characters. */ } if (M_GetValueSize_Pretty(value, 0, indent_size, newline_size, &size)) { /* value was malformed! */ return nullptr; } size += 1; /* for the '\0' null terminating character. */ data = (char *)Memory_Alloc(size); if (nullptr == data) { /* malloc failed! */ return nullptr; } data_end = M_WriteValue_Pretty(value, 0, indent, newline, data); if (nullptr == data_end) { /* bad chi occurred! */ Memory_Free(data); return nullptr; } /* null terminated the string. */ *data_end = '\0'; if (nullptr != out_size) { *out_size = size; } return data; } ================================================ FILE: src/trx/core/json/write.h ================================================ #pragma once #include /* Write out a minified JSON utf-8 string. This string is an encoding of the * minimal string characters required to still encode the same data. * json_write_minified performs 1 call to malloc for the entire encoding. Return * 0 if an error occurred (malformed JSON input, or malloc failed). The out_size * parameter is optional as the utf-8 string is null terminated. */ void *JSON_WriteMinified(const JSON_VALUE *value, size_t *out_size); /* Write out a pretty JSON utf-8 string. This string is encoded such that the * resultant JSON is pretty in that it is easily human readable. The indent and * newline parameters allow a user to specify what kind of indentation and * newline they want (two spaces / three spaces / tabs? \r, \n, \r\n ?). Both * indent and newline can be nullptr, indent defaults to two spaces (" "), and * newline defaults to linux newlines ('\n' as the newline character). * json_write_pretty performs 1 call to malloc for the entire encoding. Return 0 * if an error occurred (malformed JSON input, or malloc failed). The out_size * parameter is optional as the utf-8 string is null terminated. */ void *JSON_WritePretty( const JSON_VALUE *value, const char *indent, const char *newline, size_t *out_size); ================================================ FILE: src/trx/core/json.h ================================================ #pragma once #include #include #include #include #include ================================================ FILE: src/trx/core/log.c ================================================ #include #include #include #include #include #define M_FORMAT "%s | %s [%s:%d:%s] " static LOG_LEVEL m_LogLevel = LOG_LEVEL_MAX; static FILE *m_LogHandle = nullptr; static bool m_UseAnsiColors = true; static const char *const m_LogLevelColors[] = { [LOG_LEVEL_INFO] = LOG_ANSI_COLOR_RESET, [LOG_LEVEL_WARNING] = LOG_ANSI_COLOR_YELLOW, [LOG_LEVEL_ERROR] = LOG_ANSI_COLOR_RED, [LOG_LEVEL_DEBUG] = LOG_ANSI_COLOR_CYAN, }; static const char *const m_LogLevelStrings[] = { [LOG_LEVEL_INFO] = "INF", [LOG_LEVEL_WARNING] = "WRN", [LOG_LEVEL_ERROR] = "ERR", [LOG_LEVEL_DEBUG] = "DBG", }; void Log_Init(const char *path, const LOG_LEVEL min_level) { if (m_LogHandle != nullptr) { fclose(m_LogHandle); m_LogHandle = nullptr; } m_LogLevel = min_level; m_UseAnsiColors = Log_ShouldUseAnsiColors(); if (path != nullptr) { m_LogHandle = fopen(path, "w"); } Log_Init_Extra(path); } LOG_LEVEL Log_GetMinLevel(void) { return m_LogLevel; } void Log_SetMinLevel(const LOG_LEVEL min_level) { m_LogLevel = min_level; } void Log_Message( const LOG_LEVEL level, const char *const file, const int line, const char *const func, const char *const fmt, ...) { va_list va; va_start(va, fmt); char timestamp_str[32]; struct timeval tv; gettimeofday(&tv, nullptr); struct tm *const tm_info = localtime(&tv.tv_sec); const size_t timestamp_len = strftime( timestamp_str, sizeof(timestamp_str), "%Y-%m-%d %H:%M:%S", tm_info); snprintf( timestamp_str + timestamp_len, sizeof(timestamp_str) - timestamp_len, ".%03d", (int)(tv.tv_usec / 1000)); const char *const log_str = m_LogLevelStrings[level]; const char *const log_color = m_LogLevelColors[level]; // print to log file if (m_LogHandle != nullptr) { va_list vb; va_copy(vb, va); fprintf( m_LogHandle, M_FORMAT, log_str, timestamp_str, file, line, func); vfprintf(m_LogHandle, fmt, vb); fprintf(m_LogHandle, "\n"); fflush(m_LogHandle); va_end(vb); } // print to stdout if (level >= m_LogLevel) { if (m_UseAnsiColors) { printf("%s", log_color); } printf(M_FORMAT, log_str, timestamp_str, file, line, func); vprintf(fmt, va); if (m_UseAnsiColors) { printf("%s", LOG_ANSI_COLOR_RESET); } printf("\n"); fflush(stdout); } va_end(va); } void Log_Shutdown(void) { Log_Shutdown_Extra(); if (m_LogHandle != nullptr) { fclose(m_LogHandle); m_LogHandle = nullptr; } } ================================================ FILE: src/trx/core/log.h ================================================ #pragma once typedef enum { // from least important to most important LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_WARNING, LOG_LEVEL_ERROR, LOG_LEVEL_MAX = -1, } LOG_LEVEL; #define LOG_ANSI_COLOR_RED "\x1b[31m" #define LOG_ANSI_COLOR_GREEN "\x1b[32m" #define LOG_ANSI_COLOR_YELLOW "\x1b[33m" #define LOG_ANSI_COLOR_CYAN "\x1b[36m" #define LOG_ANSI_COLOR_RESET "\x1b[0m" #define LOG_GENERIC(level, ...) \ Log_Message(level, __FILE__, __LINE__, __func__, __VA_ARGS__) #define LOG_INFO(...) LOG_GENERIC(LOG_LEVEL_INFO, __VA_ARGS__) #define LOG_WARNING(...) LOG_GENERIC(LOG_LEVEL_WARNING, __VA_ARGS__) #define LOG_ERROR(...) LOG_GENERIC(LOG_LEVEL_ERROR, __VA_ARGS__) #define LOG_DEBUG(...) LOG_GENERIC(LOG_LEVEL_DEBUG, __VA_ARGS__) #define LOG_TRACE(...) // disable by default #define LOG_VAR(var) \ _Generic( \ (var), \ int: LOG_DEBUG(#var ": %d", var), \ bool: LOG_DEBUG(#var ": %d", var), \ int8_t: LOG_DEBUG(#var ": %d", var), \ int16_t: LOG_DEBUG(#var ": %d", var), \ uint8_t: LOG_DEBUG(#var ": %d", var), \ uint16_t: LOG_DEBUG(#var ": %d", var), \ uint32_t: LOG_DEBUG(#var ": %d", var), \ float: LOG_DEBUG(#var ": %f", var), \ double: LOG_DEBUG(#var ": %f", var), \ char *: LOG_DEBUG(#var ": %s", var), \ const char *: LOG_DEBUG(#var ": %s", var), \ default: LOG_DEBUG(#var ": %p", var)) void Log_Init(const char *path, LOG_LEVEL min_level); LOG_LEVEL Log_GetMinLevel(void); void Log_SetMinLevel(LOG_LEVEL min_level); void Log_Shutdown(void); void Log_Message( LOG_LEVEL level, const char *file, int line, const char *func, const char *fmt, ...); // platform-specific implementations bool Log_ShouldUseAnsiColors(void); void Log_Init_Extra(const char *path); void Log_Shutdown_Extra(void); ================================================ FILE: src/trx/core/log_linux.c ================================================ #include #include #include #include #include #include #include static void M_ErrorCallback(void *data, const char *msg, int errnum) { LOG_ERROR("%s", msg); } static int M_StackTrace( void *data, uintptr_t pc, const char *filename, int lineno, const char *func_name) { if (filename) { LOG_ERROR( "0x%08X: %s (%s:%d)", pc, func_name ? func_name : "???", filename ? filename : "???", lineno); } else { LOG_ERROR("0x%08X: %s", pc, func_name ? func_name : "???"); } return 0; } static void M_SignalHandler(int sig) { LOG_ERROR("== CRASH REPORT =="); LOG_ERROR("SIGNAL: %d", sig); LOG_ERROR("STACK TRACE:"); struct backtrace_state *state = backtrace_create_state( nullptr, BACKTRACE_SUPPORTS_THREADS, M_ErrorCallback, nullptr); backtrace_full(state, 0, M_StackTrace, M_ErrorCallback, nullptr); exit(EXIT_FAILURE); } void Log_Init_Extra(const char *path) { signal(SIGSEGV, M_SignalHandler); signal(SIGFPE, M_SignalHandler); signal(SIGILL, M_SignalHandler); } void Log_Shutdown_Extra(void) { } bool Log_ShouldUseAnsiColors(void) { return isatty(fileno(stdout)); } ================================================ FILE: src/trx/core/log_unknown.c ================================================ #include bool Log_ShouldUseAnsiColors(void) { return true; } void Log_Init_Extra(const char *path) { } void Log_Shutdown_Extra(void) { } ================================================ FILE: src/trx/core/log_windows.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING #define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004 #endif static char *m_MiniDumpPath = nullptr; static char *M_GetMiniDumpPath(const char *const log_path) { char *dot = strrchr(log_path, '.'); if (dot == nullptr) { return nullptr; } const size_t index = dot - log_path; const char *new_extension = ".dmp"; const size_t new_len = index + strlen(new_extension) + 1; char *minidump_path = Memory_Alloc(new_len); strncpy(minidump_path, log_path, index); strcat(minidump_path, new_extension); return minidump_path; } static void M_StackTrace( const uint64_t addr, const char *filename, const int line_no, const char *const func_name, void *const context, const int column_no) { int32_t *count = context; void *ptr = (void *)(uintptr_t)addr; switch (line_no) { case DWST_BASE_ADDR: LOG_INFO("--- 0x%p: %s", ptr, filename); break; case DWST_NOT_FOUND: case DWST_NO_DBG_SYM: case DWST_NO_SRC_FILE: LOG_INFO("%02d. 0x%p: %s", *count, ptr, filename); (*count)++; break; default: if (ptr != nullptr) { LOG_INFO( "%02d. 0x%p: (%s:%d:%d) %s", *count, ptr, filename, line_no, column_no, func_name); } else { LOG_INFO( "%02d. %*s (%s:%d:%d) %s", *count, (int32_t)sizeof(void *) * 2, "", filename, line_no, column_no, func_name); } (*count)++; break; } } static void M_CreateMiniDump( EXCEPTION_POINTERS *const ex, const char *const path) { HANDLE handle = CreateFile( path, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); MINIDUMP_EXCEPTION_INFORMATION dump_info; dump_info.ExceptionPointers = ex; dump_info.ThreadId = GetCurrentThreadId(); dump_info.ClientPointers = TRUE; MiniDumpWriteDump( GetCurrentProcess(), GetCurrentProcessId(), handle, MiniDumpNormal, &dump_info, nullptr, nullptr); CloseHandle(handle); LOG_INFO("Crash dump info put in %s", path); } LONG WINAPI Log_CrashHandler(EXCEPTION_POINTERS *ex) { LOG_ERROR("== CRASH REPORT =="); LOG_INFO("EXCEPTION CODE: %x", ex->ExceptionRecord->ExceptionCode); LOG_INFO("EXCEPTION ADDRESS: %x", ex->ExceptionRecord->ExceptionAddress); LOG_INFO("STACK TRACE:"); int32_t count = 0; dwstOfException(ex->ContextRecord, &M_StackTrace, &count); M_CreateMiniDump(ex, m_MiniDumpPath); return EXCEPTION_EXECUTE_HANDLER; } void Log_Init_Extra(const char *log_path) { // let the game (a gui app) log output to a terminal if (AttachConsole(ATTACH_PARENT_PROCESS)) { FILE *fp; freopen_s(&fp, "CONOUT$", "w", stdout); freopen_s(&fp, "CONOUT$", "w", stderr); freopen_s(&fp, "CONIN$", "r", stdin); } // enable ANSI escape codes processing HANDLE h_out = GetStdHandle(STD_OUTPUT_HANDLE); if (h_out != INVALID_HANDLE_VALUE) { DWORD mode = 0; if (GetConsoleMode(h_out, &mode)) { SetConsoleMode(h_out, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING); } } if (log_path != nullptr) { m_MiniDumpPath = M_GetMiniDumpPath(log_path); SetUnhandledExceptionFilter(Log_CrashHandler); } } void Log_Shutdown_Extra(void) { Memory_FreePointer(&m_MiniDumpPath); } bool Log_ShouldUseAnsiColors(void) { return _isatty(_fileno(stdout)); } ================================================ FILE: src/trx/core/math/const.h ================================================ #pragma once #ifndef M_PI #define M_PI 3.14159265358979323846 #endif // clang-format off #define DEG_360 0x10000 #define DEG_315 (DEG_180 + DEG_135) // = 0xE000 #define DEG_270 (DEG_180 + DEG_90) // = 0xC000 #define DEG_225 (DEG_180 + DEG_45) // = 0xA000 #define DEG_180 ((DEG_360) / 2) // = 0x8000 #define DEG_135 ((DEG_45) * 3) // = 0x6000 #define DEG_90 ((DEG_360) / 4) // = 0x4000 #define DEG_45 ((DEG_360) / 8) // = 0x2000 #define DEG_1 ((DEG_360) / 360) // = 182 // clang-format on #define W2V_SHIFT 14 ================================================ FILE: src/trx/core/math/func.c ================================================ #include #include #include #include uint32_t Math_Sqrt(uint32_t n) { uint32_t result = 0; uint32_t base = 0x40000000; do { do { uint32_t based_result = base + result; result >>= 1; if (based_result > n) { break; } n -= based_result; result |= base; base >>= 2; } while (base); base >>= 2; } while (base); return result; } uint64_t Math_Sqrt64(uint64_t n) { uint64_t result = 0; uint64_t bit = 1ULL << 62; while (bit > n) { bit >>= 2; } while (bit != 0) { if (n >= result + bit) { n -= result + bit; result = (result >> 1) + bit; } else { result >>= 1; } bit >>= 2; } return result; } void Math_GetVectorAngles( const int32_t x, const int32_t y, const int32_t z, int16_t *const dest) { dest[0] = XYZ_32_GetYaw((XYZ_32) { x, y, z }); dest[1] = XYZ_32_GetPitch((XYZ_32) { x, y, z }); } int32_t Math_AngleInCone(int32_t angle1, int32_t angle2, int32_t cone) { const int32_t diff = ((int)(angle1 - angle2 + DEG_180)) % DEG_360 - DEG_180; return ABS(diff) < cone; } DIRECTION Math_GetDirection(const int16_t angle) { return (uint16_t)(angle + DEG_45) / DEG_90; } DIRECTION Math_GetDirectionCone(const int16_t angle, const int16_t cone) { if (angle >= -cone && angle <= cone) { return DIR_NORTH; } else if (angle >= DEG_90 - cone && angle <= DEG_90 + cone) { return DIR_EAST; } else if (angle >= DEG_180 - cone || angle <= -DEG_180 + cone) { return DIR_SOUTH; } else if (angle >= -DEG_90 - cone && angle <= -DEG_90 + cone) { return DIR_WEST; } return DIR_UNKNOWN; } int16_t Math_DirectionToAngle(const DIRECTION dir) { switch (dir) { case DIR_NORTH: return 0; case DIR_EAST: return DEG_90; case DIR_SOUTH: return -DEG_180; case DIR_WEST: return -DEG_90; default: return 0; } } int32_t Math_AngleMean(int32_t angle1, int32_t angle2, double ratio) { int32_t diff = angle2 - angle1; if (diff > DEG_180) { diff -= DEG_360; } else if (diff < -DEG_180) { diff += DEG_360; } int32_t result = angle1 + diff * ratio; result %= DEG_360; if (result < 0) { result += DEG_360; } return result; } int32_t Math_FloorDiv(const int32_t x, const int32_t divisor) { return (x >= 0) ? x / divisor : -((-x + divisor - 1) / divisor); } int32_t Math_GCD(int32_t a, int32_t b) { while (b != 0) { int32_t t = b; b = a % b; a = t; } return a; } ================================================ FILE: src/trx/core/math/func.h ================================================ #pragma once #include uint32_t Math_Sqrt(uint32_t n); uint64_t Math_Sqrt64(uint64_t n); void Math_GetVectorAngles(int32_t x, int32_t y, int32_t z, int16_t *dest); int32_t Math_AngleInCone(int32_t angle1, int32_t angle2, int32_t cone); DIRECTION Math_GetDirection(int16_t angle); DIRECTION Math_GetDirectionCone(int16_t angle, int16_t cone); int16_t Math_DirectionToAngle(DIRECTION dir); int32_t Math_AngleMean(int32_t angle1, int32_t angle2, double ratio); int32_t Math_FloorDiv(int32_t x, int32_t divisor); int32_t Math_GCD(int32_t a, int32_t b); ================================================ FILE: src/trx/core/math/geom.c ================================================ #include #include #include #include #include #include int16_t XYZ_32_GetYaw(const XYZ_32 pos) { return Math_Atan(pos.z, pos.x); } int16_t XYZ_32_GetYawDiff(const XYZ_32 pos1, const XYZ_32 pos2) { return Math_Atan(pos2.z - pos1.z, pos2.x - pos1.x); } int16_t XYZ_32_GetPitch(XYZ_32 pos) { // make sure SQUARE() doesn't get out of bounds while ((int16_t)pos.x != pos.x || (int16_t)pos.y != pos.y || (int16_t)pos.z != pos.z) { pos.x >>= 1; pos.y >>= 1; pos.z >>= 1; } return Math_Atan(Math_Sqrt(SQUARE(pos.x) + SQUARE(pos.z)), -pos.y); } int32_t XYZ_32_GetDistance(const XYZ_32 pos1, const XYZ_32 pos2) { int64_t x = (int64_t)pos1.x - pos2.x; int64_t y = (int64_t)pos1.y - pos2.y; int64_t z = (int64_t)pos1.z - pos2.z; int32_t scale = 0; while ((int32_t)x != x || (int32_t)y != y || (int32_t)z != z) { scale++; x >>= 1; y >>= 1; z >>= 1; } const uint64_t dist = Math_Sqrt64( SQUARE((uint64_t)ABS(x)) + SQUARE((uint64_t)ABS(y)) + SQUARE((uint64_t)ABS(z))); if (dist > ((uint64_t)INT32_MAX >> scale)) { return INT32_MAX; } return (int32_t)(dist << scale); } bool XYZ_32_IsNearby( const XYZ_32 pos1, const XYZ_32 pos2, const int32_t distance) { const XYZ_32 delta = { .x = pos1.x - pos2.x, .y = pos1.y - pos2.y, .z = pos1.z - pos2.z, }; return delta.x > -distance && delta.x < distance && delta.y > -distance && delta.y < distance && delta.z > -distance && delta.z < distance; } int32_t XYZ_32_GetLength(const XYZ_32 pos) { return (int32_t)Math_Sqrt64( SQUARE((uint64_t)ABS((int64_t)pos.x)) + SQUARE((uint64_t)ABS((int64_t)pos.y)) + SQUARE((uint64_t)ABS((int64_t)pos.z))); } int32_t XYZ_32_GetLength2(const XYZ_32 pos) { const int64_t dist_64 = XYZ_32_GetLength2_64(pos); return dist_64 > INT32_MAX ? INT32_MAX : (int32_t)dist_64; } int64_t XYZ_32_GetLength2_64(const XYZ_32 pos) { return SQUARE((int64_t)pos.x) + SQUARE((int64_t)pos.y) + SQUARE((int64_t)pos.z); } bool XYZ_32_AreEquivalent(const XYZ_32 pos1, const XYZ_32 pos2) { return pos1.x == pos2.x && pos1.y == pos2.y && pos1.z == pos2.z; } bool XYZ_16_AreEquivalent(const XYZ_16 rot1, const XYZ_16 rot2) { return rot1.x == rot2.x && rot1.y == rot2.y && rot1.z == rot2.z; } XYZ_32 XYZ_32_From16(const XYZ_16 src) { return (XYZ_32) { src.x, src.y, src.z }; } XYZ_32 XYZ_32_OffsetYaw( const XYZ_32 src, const int16_t yaw, const int32_t distance) { return (XYZ_32) { .x = src.x + ((distance * Math_Sin(yaw)) >> W2V_SHIFT), .y = src.y, .z = src.z + ((distance * Math_Cos(yaw)) >> W2V_SHIFT), }; } XYZ_32 XYZ_32_FromYawPitch( const int16_t yaw, const int16_t pitch, const int32_t distance) { const int32_t cx = Math_Cos(pitch); const int32_t sx = Math_Sin(pitch); const int32_t cy = Math_Cos(yaw); const int32_t sy = Math_Sin(yaw); const int32_t horz = (distance * cx) >> W2V_SHIFT; return (XYZ_32) { .x = (int32_t)(((int64_t)horz * sy) >> W2V_SHIFT), .y = -(int32_t)(((int64_t)distance * sx) >> W2V_SHIFT), .z = (int32_t)(((int64_t)horz * cy) >> W2V_SHIFT), }; } int64_t XYZ_32_DotProduct_64(const XYZ_32 a, const XYZ_32 b) { return (int64_t)a.x * b.x + (int64_t)a.y * b.y + (int64_t)a.z * b.z; } bool XYZ_32_ProjectPointOntoAxis( const XYZ_32 origin, const XYZ_32 axis, const int64_t axis_len2, XYZ_32 *const pos) { // Finds the value `t` such that the point // origin + t * axis // is the closest point on the line to the original *pos, and then writes // that point back into *pos. // // Example: // - origin = (0, 0, 0) // - axis = (1, 0, 0) // line is the X axis // - *pos = (5, 2, -3) // The closest point on the X axis is (5, 0, 0), so after the call: // - *pos = (5, 0, 0) if (axis_len2 == 0) { return false; } const XYZ_32 offset = { .x = pos->x - origin.x, .y = pos->y - origin.y, .z = pos->z - origin.z, }; const int64_t t_num = XYZ_32_DotProduct_64(offset, axis); *pos = (XYZ_32) { .x = origin.x + (int32_t)((t_num * axis.x) / axis_len2), .y = origin.y + (int32_t)((t_num * axis.y) / axis_len2), .z = origin.z + (int32_t)((t_num * axis.z) / axis_len2), }; return true; } float XYZ_F_DotProduct(const XYZ_F a, const XYZ_F b) { return a.x * b.x + a.y * b.y + a.z * b.z; } float XYZ_F_Length2(const XYZ_F pos) { return XYZ_F_DotProduct(pos, pos); } float XYZ_F_Length(const XYZ_F pos) { return sqrtf(XYZ_F_Length2(pos)); } XYZ_F XYZ_F_Subtract(const XYZ_F a, const XYZ_F b) { return (XYZ_F) { .x = a.x - b.x, .y = a.y - b.y, .z = a.z - b.z, }; } ================================================ FILE: src/trx/core/math/geom.h ================================================ #pragma once #include XYZ_32 XYZ_32_From16(XYZ_16 src); int16_t XYZ_32_GetYaw(XYZ_32 pos); int16_t XYZ_32_GetYawDiff(XYZ_32 pos1, const XYZ_32 pos2); int16_t XYZ_32_GetPitch(XYZ_32 pos); int32_t XYZ_32_GetDistance(XYZ_32 pos1, XYZ_32 pos2); // Take length of a vector int32_t XYZ_32_GetLength(XYZ_32 pos); // Take squared length of a vector int32_t XYZ_32_GetLength2(XYZ_32 pos); int64_t XYZ_32_GetLength2_64(XYZ_32 pos); int64_t XYZ_32_DotProduct_64(XYZ_32 a, XYZ_32 b); bool XYZ_32_AreEquivalent(XYZ_32 pos1, XYZ_32 pos2); bool XYZ_32_IsNearby(XYZ_32 pos1, XYZ_32 pos2, int32_t distance); XYZ_32 XYZ_32_FromYawPitch(int16_t yaw, int16_t pitch, int32_t distance); XYZ_32 XYZ_32_OffsetYaw(XYZ_32 src, int16_t yaw, int32_t distance); bool XYZ_32_ProjectPointOntoAxis( XYZ_32 origin, XYZ_32 axis, int64_t axis_len2, XYZ_32 *pos); bool XYZ_16_AreEquivalent(XYZ_16 rot1, XYZ_16 rot2); float XYZ_F_DotProduct(XYZ_F a, XYZ_F b); float XYZ_F_Length2(XYZ_F pos); float XYZ_F_Length(XYZ_F pos); XYZ_F XYZ_F_Subtract(XYZ_F a, XYZ_F b); ================================================ FILE: src/trx/core/math/trig.c ================================================ #include #include static const int16_t m_SinTable[0x402] = { 0x0000, 0x0019, 0x0032, 0x004B, 0x0065, 0x007E, 0x0097, 0x00B0, 0x00C9, 0x00E2, 0x00FB, 0x0114, 0x012E, 0x0147, 0x0160, 0x0179, 0x0192, 0x01AB, 0x01C4, 0x01DD, 0x01F7, 0x0210, 0x0229, 0x0242, 0x025B, 0x0274, 0x028D, 0x02A6, 0x02C0, 0x02D9, 0x02F2, 0x030B, 0x0324, 0x033D, 0x0356, 0x036F, 0x0388, 0x03A1, 0x03BB, 0x03D4, 0x03ED, 0x0406, 0x041F, 0x0438, 0x0451, 0x046A, 0x0483, 0x049C, 0x04B5, 0x04CE, 0x04E7, 0x0500, 0x051A, 0x0533, 0x054C, 0x0565, 0x057E, 0x0597, 0x05B0, 0x05C9, 0x05E2, 0x05FB, 0x0614, 0x062D, 0x0646, 0x065F, 0x0678, 0x0691, 0x06AA, 0x06C3, 0x06DC, 0x06F5, 0x070E, 0x0727, 0x0740, 0x0759, 0x0772, 0x078B, 0x07A4, 0x07BD, 0x07D6, 0x07EF, 0x0807, 0x0820, 0x0839, 0x0852, 0x086B, 0x0884, 0x089D, 0x08B6, 0x08CF, 0x08E8, 0x0901, 0x0919, 0x0932, 0x094B, 0x0964, 0x097D, 0x0996, 0x09AF, 0x09C7, 0x09E0, 0x09F9, 0x0A12, 0x0A2B, 0x0A44, 0x0A5C, 0x0A75, 0x0A8E, 0x0AA7, 0x0AC0, 0x0AD8, 0x0AF1, 0x0B0A, 0x0B23, 0x0B3B, 0x0B54, 0x0B6D, 0x0B85, 0x0B9E, 0x0BB7, 0x0BD0, 0x0BE8, 0x0C01, 0x0C1A, 0x0C32, 0x0C4B, 0x0C64, 0x0C7C, 0x0C95, 0x0CAE, 0x0CC6, 0x0CDF, 0x0CF8, 0x0D10, 0x0D29, 0x0D41, 0x0D5A, 0x0D72, 0x0D8B, 0x0DA4, 0x0DBC, 0x0DD5, 0x0DED, 0x0E06, 0x0E1E, 0x0E37, 0x0E4F, 0x0E68, 0x0E80, 0x0E99, 0x0EB1, 0x0ECA, 0x0EE2, 0x0EFB, 0x0F13, 0x0F2B, 0x0F44, 0x0F5C, 0x0F75, 0x0F8D, 0x0FA5, 0x0FBE, 0x0FD6, 0x0FEE, 0x1007, 0x101F, 0x1037, 0x1050, 0x1068, 0x1080, 0x1099, 0x10B1, 0x10C9, 0x10E1, 0x10FA, 0x1112, 0x112A, 0x1142, 0x115A, 0x1173, 0x118B, 0x11A3, 0x11BB, 0x11D3, 0x11EB, 0x1204, 0x121C, 0x1234, 0x124C, 0x1264, 0x127C, 0x1294, 0x12AC, 0x12C4, 0x12DC, 0x12F4, 0x130C, 0x1324, 0x133C, 0x1354, 0x136C, 0x1384, 0x139C, 0x13B4, 0x13CC, 0x13E4, 0x13FB, 0x1413, 0x142B, 0x1443, 0x145B, 0x1473, 0x148B, 0x14A2, 0x14BA, 0x14D2, 0x14EA, 0x1501, 0x1519, 0x1531, 0x1549, 0x1560, 0x1578, 0x1590, 0x15A7, 0x15BF, 0x15D7, 0x15EE, 0x1606, 0x161D, 0x1635, 0x164C, 0x1664, 0x167C, 0x1693, 0x16AB, 0x16C2, 0x16DA, 0x16F1, 0x1709, 0x1720, 0x1737, 0x174F, 0x1766, 0x177E, 0x1795, 0x17AC, 0x17C4, 0x17DB, 0x17F2, 0x180A, 0x1821, 0x1838, 0x184F, 0x1867, 0x187E, 0x1895, 0x18AC, 0x18C3, 0x18DB, 0x18F2, 0x1909, 0x1920, 0x1937, 0x194E, 0x1965, 0x197C, 0x1993, 0x19AA, 0x19C1, 0x19D8, 0x19EF, 0x1A06, 0x1A1D, 0x1A34, 0x1A4B, 0x1A62, 0x1A79, 0x1A90, 0x1AA7, 0x1ABE, 0x1AD4, 0x1AEB, 0x1B02, 0x1B19, 0x1B30, 0x1B46, 0x1B5D, 0x1B74, 0x1B8A, 0x1BA1, 0x1BB8, 0x1BCE, 0x1BE5, 0x1BFC, 0x1C12, 0x1C29, 0x1C3F, 0x1C56, 0x1C6C, 0x1C83, 0x1C99, 0x1CB0, 0x1CC6, 0x1CDD, 0x1CF3, 0x1D0A, 0x1D20, 0x1D36, 0x1D4D, 0x1D63, 0x1D79, 0x1D90, 0x1DA6, 0x1DBC, 0x1DD3, 0x1DE9, 0x1DFF, 0x1E15, 0x1E2B, 0x1E42, 0x1E58, 0x1E6E, 0x1E84, 0x1E9A, 0x1EB0, 0x1EC6, 0x1EDC, 0x1EF2, 0x1F08, 0x1F1E, 0x1F34, 0x1F4A, 0x1F60, 0x1F76, 0x1F8C, 0x1FA2, 0x1FB7, 0x1FCD, 0x1FE3, 0x1FF9, 0x200F, 0x2024, 0x203A, 0x2050, 0x2065, 0x207B, 0x2091, 0x20A6, 0x20BC, 0x20D1, 0x20E7, 0x20FD, 0x2112, 0x2128, 0x213D, 0x2153, 0x2168, 0x217D, 0x2193, 0x21A8, 0x21BE, 0x21D3, 0x21E8, 0x21FE, 0x2213, 0x2228, 0x223D, 0x2253, 0x2268, 0x227D, 0x2292, 0x22A7, 0x22BC, 0x22D2, 0x22E7, 0x22FC, 0x2311, 0x2326, 0x233B, 0x2350, 0x2365, 0x237A, 0x238E, 0x23A3, 0x23B8, 0x23CD, 0x23E2, 0x23F7, 0x240B, 0x2420, 0x2435, 0x244A, 0x245E, 0x2473, 0x2488, 0x249C, 0x24B1, 0x24C5, 0x24DA, 0x24EF, 0x2503, 0x2518, 0x252C, 0x2541, 0x2555, 0x2569, 0x257E, 0x2592, 0x25A6, 0x25BB, 0x25CF, 0x25E3, 0x25F8, 0x260C, 0x2620, 0x2634, 0x2648, 0x265C, 0x2671, 0x2685, 0x2699, 0x26AD, 0x26C1, 0x26D5, 0x26E9, 0x26FD, 0x2711, 0x2724, 0x2738, 0x274C, 0x2760, 0x2774, 0x2788, 0x279B, 0x27AF, 0x27C3, 0x27D6, 0x27EA, 0x27FE, 0x2811, 0x2825, 0x2838, 0x284C, 0x2860, 0x2873, 0x2886, 0x289A, 0x28AD, 0x28C1, 0x28D4, 0x28E7, 0x28FB, 0x290E, 0x2921, 0x2935, 0x2948, 0x295B, 0x296E, 0x2981, 0x2994, 0x29A7, 0x29BB, 0x29CE, 0x29E1, 0x29F4, 0x2A07, 0x2A1A, 0x2A2C, 0x2A3F, 0x2A52, 0x2A65, 0x2A78, 0x2A8B, 0x2A9D, 0x2AB0, 0x2AC3, 0x2AD6, 0x2AE8, 0x2AFB, 0x2B0D, 0x2B20, 0x2B33, 0x2B45, 0x2B58, 0x2B6A, 0x2B7D, 0x2B8F, 0x2BA1, 0x2BB4, 0x2BC6, 0x2BD8, 0x2BEB, 0x2BFD, 0x2C0F, 0x2C21, 0x2C34, 0x2C46, 0x2C58, 0x2C6A, 0x2C7C, 0x2C8E, 0x2CA0, 0x2CB2, 0x2CC4, 0x2CD6, 0x2CE8, 0x2CFA, 0x2D0C, 0x2D1E, 0x2D2F, 0x2D41, 0x2D53, 0x2D65, 0x2D76, 0x2D88, 0x2D9A, 0x2DAB, 0x2DBD, 0x2DCF, 0x2DE0, 0x2DF2, 0x2E03, 0x2E15, 0x2E26, 0x2E37, 0x2E49, 0x2E5A, 0x2E6B, 0x2E7D, 0x2E8E, 0x2E9F, 0x2EB0, 0x2EC2, 0x2ED3, 0x2EE4, 0x2EF5, 0x2F06, 0x2F17, 0x2F28, 0x2F39, 0x2F4A, 0x2F5B, 0x2F6C, 0x2F7D, 0x2F8D, 0x2F9E, 0x2FAF, 0x2FC0, 0x2FD0, 0x2FE1, 0x2FF2, 0x3002, 0x3013, 0x3024, 0x3034, 0x3045, 0x3055, 0x3066, 0x3076, 0x3087, 0x3097, 0x30A7, 0x30B8, 0x30C8, 0x30D8, 0x30E8, 0x30F9, 0x3109, 0x3119, 0x3129, 0x3139, 0x3149, 0x3159, 0x3169, 0x3179, 0x3189, 0x3199, 0x31A9, 0x31B9, 0x31C8, 0x31D8, 0x31E8, 0x31F8, 0x3207, 0x3217, 0x3227, 0x3236, 0x3246, 0x3255, 0x3265, 0x3274, 0x3284, 0x3293, 0x32A3, 0x32B2, 0x32C1, 0x32D0, 0x32E0, 0x32EF, 0x32FE, 0x330D, 0x331D, 0x332C, 0x333B, 0x334A, 0x3359, 0x3368, 0x3377, 0x3386, 0x3395, 0x33A3, 0x33B2, 0x33C1, 0x33D0, 0x33DF, 0x33ED, 0x33FC, 0x340B, 0x3419, 0x3428, 0x3436, 0x3445, 0x3453, 0x3462, 0x3470, 0x347F, 0x348D, 0x349B, 0x34AA, 0x34B8, 0x34C6, 0x34D4, 0x34E2, 0x34F1, 0x34FF, 0x350D, 0x351B, 0x3529, 0x3537, 0x3545, 0x3553, 0x3561, 0x356E, 0x357C, 0x358A, 0x3598, 0x35A5, 0x35B3, 0x35C1, 0x35CE, 0x35DC, 0x35EA, 0x35F7, 0x3605, 0x3612, 0x3620, 0x362D, 0x363A, 0x3648, 0x3655, 0x3662, 0x366F, 0x367D, 0x368A, 0x3697, 0x36A4, 0x36B1, 0x36BE, 0x36CB, 0x36D8, 0x36E5, 0x36F2, 0x36FF, 0x370C, 0x3718, 0x3725, 0x3732, 0x373F, 0x374B, 0x3758, 0x3765, 0x3771, 0x377E, 0x378A, 0x3797, 0x37A3, 0x37B0, 0x37BC, 0x37C8, 0x37D5, 0x37E1, 0x37ED, 0x37F9, 0x3805, 0x3812, 0x381E, 0x382A, 0x3836, 0x3842, 0x384E, 0x385A, 0x3866, 0x3871, 0x387D, 0x3889, 0x3895, 0x38A1, 0x38AC, 0x38B8, 0x38C3, 0x38CF, 0x38DB, 0x38E6, 0x38F2, 0x38FD, 0x3909, 0x3914, 0x391F, 0x392B, 0x3936, 0x3941, 0x394C, 0x3958, 0x3963, 0x396E, 0x3979, 0x3984, 0x398F, 0x399A, 0x39A5, 0x39B0, 0x39BB, 0x39C5, 0x39D0, 0x39DB, 0x39E6, 0x39F0, 0x39FB, 0x3A06, 0x3A10, 0x3A1B, 0x3A25, 0x3A30, 0x3A3A, 0x3A45, 0x3A4F, 0x3A59, 0x3A64, 0x3A6E, 0x3A78, 0x3A82, 0x3A8D, 0x3A97, 0x3AA1, 0x3AAB, 0x3AB5, 0x3ABF, 0x3AC9, 0x3AD3, 0x3ADD, 0x3AE6, 0x3AF0, 0x3AFA, 0x3B04, 0x3B0E, 0x3B17, 0x3B21, 0x3B2A, 0x3B34, 0x3B3E, 0x3B47, 0x3B50, 0x3B5A, 0x3B63, 0x3B6D, 0x3B76, 0x3B7F, 0x3B88, 0x3B92, 0x3B9B, 0x3BA4, 0x3BAD, 0x3BB6, 0x3BBF, 0x3BC8, 0x3BD1, 0x3BDA, 0x3BE3, 0x3BEC, 0x3BF5, 0x3BFD, 0x3C06, 0x3C0F, 0x3C17, 0x3C20, 0x3C29, 0x3C31, 0x3C3A, 0x3C42, 0x3C4B, 0x3C53, 0x3C5B, 0x3C64, 0x3C6C, 0x3C74, 0x3C7D, 0x3C85, 0x3C8D, 0x3C95, 0x3C9D, 0x3CA5, 0x3CAD, 0x3CB5, 0x3CBD, 0x3CC5, 0x3CCD, 0x3CD5, 0x3CDD, 0x3CE4, 0x3CEC, 0x3CF4, 0x3CFB, 0x3D03, 0x3D0B, 0x3D12, 0x3D1A, 0x3D21, 0x3D28, 0x3D30, 0x3D37, 0x3D3F, 0x3D46, 0x3D4D, 0x3D54, 0x3D5B, 0x3D63, 0x3D6A, 0x3D71, 0x3D78, 0x3D7F, 0x3D86, 0x3D8D, 0x3D93, 0x3D9A, 0x3DA1, 0x3DA8, 0x3DAF, 0x3DB5, 0x3DBC, 0x3DC2, 0x3DC9, 0x3DD0, 0x3DD6, 0x3DDD, 0x3DE3, 0x3DE9, 0x3DF0, 0x3DF6, 0x3DFC, 0x3E03, 0x3E09, 0x3E0F, 0x3E15, 0x3E1B, 0x3E21, 0x3E27, 0x3E2D, 0x3E33, 0x3E39, 0x3E3F, 0x3E45, 0x3E4A, 0x3E50, 0x3E56, 0x3E5C, 0x3E61, 0x3E67, 0x3E6C, 0x3E72, 0x3E77, 0x3E7D, 0x3E82, 0x3E88, 0x3E8D, 0x3E92, 0x3E98, 0x3E9D, 0x3EA2, 0x3EA7, 0x3EAC, 0x3EB1, 0x3EB6, 0x3EBB, 0x3EC0, 0x3EC5, 0x3ECA, 0x3ECF, 0x3ED4, 0x3ED8, 0x3EDD, 0x3EE2, 0x3EE7, 0x3EEB, 0x3EF0, 0x3EF4, 0x3EF9, 0x3EFD, 0x3F02, 0x3F06, 0x3F0A, 0x3F0F, 0x3F13, 0x3F17, 0x3F1C, 0x3F20, 0x3F24, 0x3F28, 0x3F2C, 0x3F30, 0x3F34, 0x3F38, 0x3F3C, 0x3F40, 0x3F43, 0x3F47, 0x3F4B, 0x3F4F, 0x3F52, 0x3F56, 0x3F5A, 0x3F5D, 0x3F61, 0x3F64, 0x3F68, 0x3F6B, 0x3F6E, 0x3F72, 0x3F75, 0x3F78, 0x3F7B, 0x3F7F, 0x3F82, 0x3F85, 0x3F88, 0x3F8B, 0x3F8E, 0x3F91, 0x3F94, 0x3F97, 0x3F99, 0x3F9C, 0x3F9F, 0x3FA2, 0x3FA4, 0x3FA7, 0x3FAA, 0x3FAC, 0x3FAF, 0x3FB1, 0x3FB4, 0x3FB6, 0x3FB8, 0x3FBB, 0x3FBD, 0x3FBF, 0x3FC1, 0x3FC4, 0x3FC6, 0x3FC8, 0x3FCA, 0x3FCC, 0x3FCE, 0x3FD0, 0x3FD2, 0x3FD4, 0x3FD5, 0x3FD7, 0x3FD9, 0x3FDB, 0x3FDC, 0x3FDE, 0x3FE0, 0x3FE1, 0x3FE3, 0x3FE4, 0x3FE6, 0x3FE7, 0x3FE8, 0x3FEA, 0x3FEB, 0x3FEC, 0x3FED, 0x3FEF, 0x3FF0, 0x3FF1, 0x3FF2, 0x3FF3, 0x3FF4, 0x3FF5, 0x3FF6, 0x3FF7, 0x3FF7, 0x3FF8, 0x3FF9, 0x3FFA, 0x3FFA, 0x3FFB, 0x3FFC, 0x3FFC, 0x3FFD, 0x3FFD, 0x3FFE, 0x3FFE, 0x3FFE, 0x3FFF, 0x3FFF, 0x3FFF, 0x4000, 0x4000, 0x4000, 0x4000, 0x4000, 0x4000, 0x4000, }; static const int32_t m_AtanBaseTable[8] = { 0x0000, -0x4000, -0xFFFF, 0xC000, -0x8000, 0x4000, 0x8000, -0xC000, }; static const int16_t m_AtanAngleTable[0x802] = { 0x0000, 0x0005, 0x000A, 0x000F, 0x0014, 0x0019, 0x001F, 0x0024, 0x0029, 0x002E, 0x0033, 0x0038, 0x003D, 0x0042, 0x0047, 0x004C, 0x0051, 0x0057, 0x005C, 0x0061, 0x0066, 0x006B, 0x0070, 0x0075, 0x007A, 0x007F, 0x0084, 0x008A, 0x008F, 0x0094, 0x0099, 0x009E, 0x00A3, 0x00A8, 0x00AD, 0x00B2, 0x00B7, 0x00BC, 0x00C2, 0x00C7, 0x00CC, 0x00D1, 0x00D6, 0x00DB, 0x00E0, 0x00E5, 0x00EA, 0x00EF, 0x00F4, 0x00FA, 0x00FF, 0x0104, 0x0109, 0x010E, 0x0113, 0x0118, 0x011D, 0x0122, 0x0127, 0x012C, 0x0131, 0x0137, 0x013C, 0x0141, 0x0146, 0x014B, 0x0150, 0x0155, 0x015A, 0x015F, 0x0164, 0x0169, 0x016F, 0x0174, 0x0179, 0x017E, 0x0183, 0x0188, 0x018D, 0x0192, 0x0197, 0x019C, 0x01A1, 0x01A6, 0x01AC, 0x01B1, 0x01B6, 0x01BB, 0x01C0, 0x01C5, 0x01CA, 0x01CF, 0x01D4, 0x01D9, 0x01DE, 0x01E3, 0x01E9, 0x01EE, 0x01F3, 0x01F8, 0x01FD, 0x0202, 0x0207, 0x020C, 0x0211, 0x0216, 0x021B, 0x0220, 0x0226, 0x022B, 0x0230, 0x0235, 0x023A, 0x023F, 0x0244, 0x0249, 0x024E, 0x0253, 0x0258, 0x025D, 0x0262, 0x0268, 0x026D, 0x0272, 0x0277, 0x027C, 0x0281, 0x0286, 0x028B, 0x0290, 0x0295, 0x029A, 0x029F, 0x02A4, 0x02A9, 0x02AF, 0x02B4, 0x02B9, 0x02BE, 0x02C3, 0x02C8, 0x02CD, 0x02D2, 0x02D7, 0x02DC, 0x02E1, 0x02E6, 0x02EB, 0x02F0, 0x02F6, 0x02FB, 0x0300, 0x0305, 0x030A, 0x030F, 0x0314, 0x0319, 0x031E, 0x0323, 0x0328, 0x032D, 0x0332, 0x0337, 0x033C, 0x0341, 0x0347, 0x034C, 0x0351, 0x0356, 0x035B, 0x0360, 0x0365, 0x036A, 0x036F, 0x0374, 0x0379, 0x037E, 0x0383, 0x0388, 0x038D, 0x0392, 0x0397, 0x039C, 0x03A2, 0x03A7, 0x03AC, 0x03B1, 0x03B6, 0x03BB, 0x03C0, 0x03C5, 0x03CA, 0x03CF, 0x03D4, 0x03D9, 0x03DE, 0x03E3, 0x03E8, 0x03ED, 0x03F2, 0x03F7, 0x03FC, 0x0401, 0x0407, 0x040C, 0x0411, 0x0416, 0x041B, 0x0420, 0x0425, 0x042A, 0x042F, 0x0434, 0x0439, 0x043E, 0x0443, 0x0448, 0x044D, 0x0452, 0x0457, 0x045C, 0x0461, 0x0466, 0x046B, 0x0470, 0x0475, 0x047A, 0x047F, 0x0484, 0x0489, 0x048E, 0x0494, 0x0499, 0x049E, 0x04A3, 0x04A8, 0x04AD, 0x04B2, 0x04B7, 0x04BC, 0x04C1, 0x04C6, 0x04CB, 0x04D0, 0x04D5, 0x04DA, 0x04DF, 0x04E4, 0x04E9, 0x04EE, 0x04F3, 0x04F8, 0x04FD, 0x0502, 0x0507, 0x050C, 0x0511, 0x0516, 0x051B, 0x0520, 0x0525, 0x052A, 0x052F, 0x0534, 0x0539, 0x053E, 0x0543, 0x0548, 0x054D, 0x0552, 0x0557, 0x055C, 0x0561, 0x0566, 0x056B, 0x0570, 0x0575, 0x057A, 0x057F, 0x0584, 0x0589, 0x058E, 0x0593, 0x0598, 0x059D, 0x05A2, 0x05A7, 0x05AC, 0x05B1, 0x05B6, 0x05BB, 0x05C0, 0x05C5, 0x05CA, 0x05CF, 0x05D4, 0x05D9, 0x05DE, 0x05E3, 0x05E8, 0x05ED, 0x05F2, 0x05F7, 0x05FC, 0x0601, 0x0606, 0x060B, 0x0610, 0x0615, 0x061A, 0x061F, 0x0624, 0x0629, 0x062E, 0x0633, 0x0638, 0x063D, 0x0642, 0x0647, 0x064C, 0x0651, 0x0656, 0x065B, 0x0660, 0x0665, 0x066A, 0x066E, 0x0673, 0x0678, 0x067D, 0x0682, 0x0687, 0x068C, 0x0691, 0x0696, 0x069B, 0x06A0, 0x06A5, 0x06AA, 0x06AF, 0x06B4, 0x06B9, 0x06BE, 0x06C3, 0x06C8, 0x06CD, 0x06D2, 0x06D7, 0x06DC, 0x06E1, 0x06E5, 0x06EA, 0x06EF, 0x06F4, 0x06F9, 0x06FE, 0x0703, 0x0708, 0x070D, 0x0712, 0x0717, 0x071C, 0x0721, 0x0726, 0x072B, 0x0730, 0x0735, 0x0739, 0x073E, 0x0743, 0x0748, 0x074D, 0x0752, 0x0757, 0x075C, 0x0761, 0x0766, 0x076B, 0x0770, 0x0775, 0x077A, 0x077E, 0x0783, 0x0788, 0x078D, 0x0792, 0x0797, 0x079C, 0x07A1, 0x07A6, 0x07AB, 0x07B0, 0x07B5, 0x07B9, 0x07BE, 0x07C3, 0x07C8, 0x07CD, 0x07D2, 0x07D7, 0x07DC, 0x07E1, 0x07E6, 0x07EB, 0x07EF, 0x07F4, 0x07F9, 0x07FE, 0x0803, 0x0808, 0x080D, 0x0812, 0x0817, 0x081C, 0x0820, 0x0825, 0x082A, 0x082F, 0x0834, 0x0839, 0x083E, 0x0843, 0x0848, 0x084C, 0x0851, 0x0856, 0x085B, 0x0860, 0x0865, 0x086A, 0x086F, 0x0873, 0x0878, 0x087D, 0x0882, 0x0887, 0x088C, 0x0891, 0x0896, 0x089A, 0x089F, 0x08A4, 0x08A9, 0x08AE, 0x08B3, 0x08B8, 0x08BD, 0x08C1, 0x08C6, 0x08CB, 0x08D0, 0x08D5, 0x08DA, 0x08DF, 0x08E3, 0x08E8, 0x08ED, 0x08F2, 0x08F7, 0x08FC, 0x0901, 0x0905, 0x090A, 0x090F, 0x0914, 0x0919, 0x091E, 0x0922, 0x0927, 0x092C, 0x0931, 0x0936, 0x093B, 0x093F, 0x0944, 0x0949, 0x094E, 0x0953, 0x0958, 0x095C, 0x0961, 0x0966, 0x096B, 0x0970, 0x0975, 0x0979, 0x097E, 0x0983, 0x0988, 0x098D, 0x0992, 0x0996, 0x099B, 0x09A0, 0x09A5, 0x09AA, 0x09AE, 0x09B3, 0x09B8, 0x09BD, 0x09C2, 0x09C6, 0x09CB, 0x09D0, 0x09D5, 0x09DA, 0x09DE, 0x09E3, 0x09E8, 0x09ED, 0x09F2, 0x09F6, 0x09FB, 0x0A00, 0x0A05, 0x0A0A, 0x0A0E, 0x0A13, 0x0A18, 0x0A1D, 0x0A22, 0x0A26, 0x0A2B, 0x0A30, 0x0A35, 0x0A39, 0x0A3E, 0x0A43, 0x0A48, 0x0A4D, 0x0A51, 0x0A56, 0x0A5B, 0x0A60, 0x0A64, 0x0A69, 0x0A6E, 0x0A73, 0x0A77, 0x0A7C, 0x0A81, 0x0A86, 0x0A8B, 0x0A8F, 0x0A94, 0x0A99, 0x0A9E, 0x0AA2, 0x0AA7, 0x0AAC, 0x0AB1, 0x0AB5, 0x0ABA, 0x0ABF, 0x0AC4, 0x0AC8, 0x0ACD, 0x0AD2, 0x0AD7, 0x0ADB, 0x0AE0, 0x0AE5, 0x0AE9, 0x0AEE, 0x0AF3, 0x0AF8, 0x0AFC, 0x0B01, 0x0B06, 0x0B0B, 0x0B0F, 0x0B14, 0x0B19, 0x0B1E, 0x0B22, 0x0B27, 0x0B2C, 0x0B30, 0x0B35, 0x0B3A, 0x0B3F, 0x0B43, 0x0B48, 0x0B4D, 0x0B51, 0x0B56, 0x0B5B, 0x0B60, 0x0B64, 0x0B69, 0x0B6E, 0x0B72, 0x0B77, 0x0B7C, 0x0B80, 0x0B85, 0x0B8A, 0x0B8F, 0x0B93, 0x0B98, 0x0B9D, 0x0BA1, 0x0BA6, 0x0BAB, 0x0BAF, 0x0BB4, 0x0BB9, 0x0BBD, 0x0BC2, 0x0BC7, 0x0BCB, 0x0BD0, 0x0BD5, 0x0BD9, 0x0BDE, 0x0BE3, 0x0BE7, 0x0BEC, 0x0BF1, 0x0BF5, 0x0BFA, 0x0BFF, 0x0C03, 0x0C08, 0x0C0D, 0x0C11, 0x0C16, 0x0C1B, 0x0C1F, 0x0C24, 0x0C29, 0x0C2D, 0x0C32, 0x0C37, 0x0C3B, 0x0C40, 0x0C45, 0x0C49, 0x0C4E, 0x0C53, 0x0C57, 0x0C5C, 0x0C60, 0x0C65, 0x0C6A, 0x0C6E, 0x0C73, 0x0C78, 0x0C7C, 0x0C81, 0x0C86, 0x0C8A, 0x0C8F, 0x0C93, 0x0C98, 0x0C9D, 0x0CA1, 0x0CA6, 0x0CAB, 0x0CAF, 0x0CB4, 0x0CB8, 0x0CBD, 0x0CC2, 0x0CC6, 0x0CCB, 0x0CCF, 0x0CD4, 0x0CD9, 0x0CDD, 0x0CE2, 0x0CE6, 0x0CEB, 0x0CF0, 0x0CF4, 0x0CF9, 0x0CFD, 0x0D02, 0x0D07, 0x0D0B, 0x0D10, 0x0D14, 0x0D19, 0x0D1E, 0x0D22, 0x0D27, 0x0D2B, 0x0D30, 0x0D34, 0x0D39, 0x0D3E, 0x0D42, 0x0D47, 0x0D4B, 0x0D50, 0x0D54, 0x0D59, 0x0D5E, 0x0D62, 0x0D67, 0x0D6B, 0x0D70, 0x0D74, 0x0D79, 0x0D7D, 0x0D82, 0x0D87, 0x0D8B, 0x0D90, 0x0D94, 0x0D99, 0x0D9D, 0x0DA2, 0x0DA6, 0x0DAB, 0x0DAF, 0x0DB4, 0x0DB9, 0x0DBD, 0x0DC2, 0x0DC6, 0x0DCB, 0x0DCF, 0x0DD4, 0x0DD8, 0x0DDD, 0x0DE1, 0x0DE6, 0x0DEA, 0x0DEF, 0x0DF3, 0x0DF8, 0x0DFC, 0x0E01, 0x0E05, 0x0E0A, 0x0E0F, 0x0E13, 0x0E18, 0x0E1C, 0x0E21, 0x0E25, 0x0E2A, 0x0E2E, 0x0E33, 0x0E37, 0x0E3C, 0x0E40, 0x0E45, 0x0E49, 0x0E4E, 0x0E52, 0x0E56, 0x0E5B, 0x0E5F, 0x0E64, 0x0E68, 0x0E6D, 0x0E71, 0x0E76, 0x0E7A, 0x0E7F, 0x0E83, 0x0E88, 0x0E8C, 0x0E91, 0x0E95, 0x0E9A, 0x0E9E, 0x0EA3, 0x0EA7, 0x0EAC, 0x0EB0, 0x0EB4, 0x0EB9, 0x0EBD, 0x0EC2, 0x0EC6, 0x0ECB, 0x0ECF, 0x0ED4, 0x0ED8, 0x0EDC, 0x0EE1, 0x0EE5, 0x0EEA, 0x0EEE, 0x0EF3, 0x0EF7, 0x0EFC, 0x0F00, 0x0F04, 0x0F09, 0x0F0D, 0x0F12, 0x0F16, 0x0F1B, 0x0F1F, 0x0F23, 0x0F28, 0x0F2C, 0x0F31, 0x0F35, 0x0F3A, 0x0F3E, 0x0F42, 0x0F47, 0x0F4B, 0x0F50, 0x0F54, 0x0F58, 0x0F5D, 0x0F61, 0x0F66, 0x0F6A, 0x0F6E, 0x0F73, 0x0F77, 0x0F7C, 0x0F80, 0x0F84, 0x0F89, 0x0F8D, 0x0F91, 0x0F96, 0x0F9A, 0x0F9F, 0x0FA3, 0x0FA7, 0x0FAC, 0x0FB0, 0x0FB5, 0x0FB9, 0x0FBD, 0x0FC2, 0x0FC6, 0x0FCA, 0x0FCF, 0x0FD3, 0x0FD7, 0x0FDC, 0x0FE0, 0x0FE5, 0x0FE9, 0x0FED, 0x0FF2, 0x0FF6, 0x0FFA, 0x0FFF, 0x1003, 0x1007, 0x100C, 0x1010, 0x1014, 0x1019, 0x101D, 0x1021, 0x1026, 0x102A, 0x102E, 0x1033, 0x1037, 0x103B, 0x1040, 0x1044, 0x1048, 0x104D, 0x1051, 0x1055, 0x105A, 0x105E, 0x1062, 0x1067, 0x106B, 0x106F, 0x1073, 0x1078, 0x107C, 0x1080, 0x1085, 0x1089, 0x108D, 0x1092, 0x1096, 0x109A, 0x109E, 0x10A3, 0x10A7, 0x10AB, 0x10B0, 0x10B4, 0x10B8, 0x10BC, 0x10C1, 0x10C5, 0x10C9, 0x10CE, 0x10D2, 0x10D6, 0x10DA, 0x10DF, 0x10E3, 0x10E7, 0x10EB, 0x10F0, 0x10F4, 0x10F8, 0x10FD, 0x1101, 0x1105, 0x1109, 0x110E, 0x1112, 0x1116, 0x111A, 0x111F, 0x1123, 0x1127, 0x112B, 0x1130, 0x1134, 0x1138, 0x113C, 0x1140, 0x1145, 0x1149, 0x114D, 0x1151, 0x1156, 0x115A, 0x115E, 0x1162, 0x1166, 0x116B, 0x116F, 0x1173, 0x1177, 0x117C, 0x1180, 0x1184, 0x1188, 0x118C, 0x1191, 0x1195, 0x1199, 0x119D, 0x11A1, 0x11A6, 0x11AA, 0x11AE, 0x11B2, 0x11B6, 0x11BB, 0x11BF, 0x11C3, 0x11C7, 0x11CB, 0x11CF, 0x11D4, 0x11D8, 0x11DC, 0x11E0, 0x11E4, 0x11E9, 0x11ED, 0x11F1, 0x11F5, 0x11F9, 0x11FD, 0x1202, 0x1206, 0x120A, 0x120E, 0x1212, 0x1216, 0x121A, 0x121F, 0x1223, 0x1227, 0x122B, 0x122F, 0x1233, 0x1237, 0x123C, 0x1240, 0x1244, 0x1248, 0x124C, 0x1250, 0x1254, 0x1259, 0x125D, 0x1261, 0x1265, 0x1269, 0x126D, 0x1271, 0x1275, 0x127A, 0x127E, 0x1282, 0x1286, 0x128A, 0x128E, 0x1292, 0x1296, 0x129A, 0x129F, 0x12A3, 0x12A7, 0x12AB, 0x12AF, 0x12B3, 0x12B7, 0x12BB, 0x12BF, 0x12C3, 0x12C7, 0x12CC, 0x12D0, 0x12D4, 0x12D8, 0x12DC, 0x12E0, 0x12E4, 0x12E8, 0x12EC, 0x12F0, 0x12F4, 0x12F8, 0x12FC, 0x1301, 0x1305, 0x1309, 0x130D, 0x1311, 0x1315, 0x1319, 0x131D, 0x1321, 0x1325, 0x1329, 0x132D, 0x1331, 0x1335, 0x1339, 0x133D, 0x1341, 0x1345, 0x1349, 0x134D, 0x1351, 0x1355, 0x135A, 0x135E, 0x1362, 0x1366, 0x136A, 0x136E, 0x1372, 0x1376, 0x137A, 0x137E, 0x1382, 0x1386, 0x138A, 0x138E, 0x1392, 0x1396, 0x139A, 0x139E, 0x13A2, 0x13A6, 0x13AA, 0x13AE, 0x13B2, 0x13B6, 0x13BA, 0x13BE, 0x13C2, 0x13C6, 0x13CA, 0x13CE, 0x13D2, 0x13D6, 0x13DA, 0x13DE, 0x13E2, 0x13E6, 0x13E9, 0x13ED, 0x13F1, 0x13F5, 0x13F9, 0x13FD, 0x1401, 0x1405, 0x1409, 0x140D, 0x1411, 0x1415, 0x1419, 0x141D, 0x1421, 0x1425, 0x1429, 0x142D, 0x1431, 0x1435, 0x1439, 0x143D, 0x1440, 0x1444, 0x1448, 0x144C, 0x1450, 0x1454, 0x1458, 0x145C, 0x1460, 0x1464, 0x1468, 0x146C, 0x1470, 0x1473, 0x1477, 0x147B, 0x147F, 0x1483, 0x1487, 0x148B, 0x148F, 0x1493, 0x1497, 0x149B, 0x149E, 0x14A2, 0x14A6, 0x14AA, 0x14AE, 0x14B2, 0x14B6, 0x14BA, 0x14BE, 0x14C1, 0x14C5, 0x14C9, 0x14CD, 0x14D1, 0x14D5, 0x14D9, 0x14DD, 0x14E0, 0x14E4, 0x14E8, 0x14EC, 0x14F0, 0x14F4, 0x14F8, 0x14FB, 0x14FF, 0x1503, 0x1507, 0x150B, 0x150F, 0x1513, 0x1516, 0x151A, 0x151E, 0x1522, 0x1526, 0x152A, 0x152D, 0x1531, 0x1535, 0x1539, 0x153D, 0x1541, 0x1544, 0x1548, 0x154C, 0x1550, 0x1554, 0x1558, 0x155B, 0x155F, 0x1563, 0x1567, 0x156B, 0x156E, 0x1572, 0x1576, 0x157A, 0x157E, 0x1581, 0x1585, 0x1589, 0x158D, 0x1591, 0x1594, 0x1598, 0x159C, 0x15A0, 0x15A4, 0x15A7, 0x15AB, 0x15AF, 0x15B3, 0x15B7, 0x15BA, 0x15BE, 0x15C2, 0x15C6, 0x15C9, 0x15CD, 0x15D1, 0x15D5, 0x15D8, 0x15DC, 0x15E0, 0x15E4, 0x15E8, 0x15EB, 0x15EF, 0x15F3, 0x15F7, 0x15FA, 0x15FE, 0x1602, 0x1606, 0x1609, 0x160D, 0x1611, 0x1614, 0x1618, 0x161C, 0x1620, 0x1623, 0x1627, 0x162B, 0x162F, 0x1632, 0x1636, 0x163A, 0x163E, 0x1641, 0x1645, 0x1649, 0x164C, 0x1650, 0x1654, 0x1658, 0x165B, 0x165F, 0x1663, 0x1666, 0x166A, 0x166E, 0x1671, 0x1675, 0x1679, 0x167D, 0x1680, 0x1684, 0x1688, 0x168B, 0x168F, 0x1693, 0x1696, 0x169A, 0x169E, 0x16A1, 0x16A5, 0x16A9, 0x16AC, 0x16B0, 0x16B4, 0x16B7, 0x16BB, 0x16BF, 0x16C2, 0x16C6, 0x16CA, 0x16CD, 0x16D1, 0x16D5, 0x16D8, 0x16DC, 0x16E0, 0x16E3, 0x16E7, 0x16EB, 0x16EE, 0x16F2, 0x16F6, 0x16F9, 0x16FD, 0x1700, 0x1704, 0x1708, 0x170B, 0x170F, 0x1713, 0x1716, 0x171A, 0x171D, 0x1721, 0x1725, 0x1728, 0x172C, 0x1730, 0x1733, 0x1737, 0x173A, 0x173E, 0x1742, 0x1745, 0x1749, 0x174C, 0x1750, 0x1754, 0x1757, 0x175B, 0x175E, 0x1762, 0x1766, 0x1769, 0x176D, 0x1770, 0x1774, 0x1778, 0x177B, 0x177F, 0x1782, 0x1786, 0x1789, 0x178D, 0x1791, 0x1794, 0x1798, 0x179B, 0x179F, 0x17A2, 0x17A6, 0x17AA, 0x17AD, 0x17B1, 0x17B4, 0x17B8, 0x17BB, 0x17BF, 0x17C2, 0x17C6, 0x17C9, 0x17CD, 0x17D1, 0x17D4, 0x17D8, 0x17DB, 0x17DF, 0x17E2, 0x17E6, 0x17E9, 0x17ED, 0x17F0, 0x17F4, 0x17F7, 0x17FB, 0x17FE, 0x1802, 0x1806, 0x1809, 0x180D, 0x1810, 0x1814, 0x1817, 0x181B, 0x181E, 0x1822, 0x1825, 0x1829, 0x182C, 0x1830, 0x1833, 0x1837, 0x183A, 0x183E, 0x1841, 0x1845, 0x1848, 0x184C, 0x184F, 0x1853, 0x1856, 0x185A, 0x185D, 0x1860, 0x1864, 0x1867, 0x186B, 0x186E, 0x1872, 0x1875, 0x1879, 0x187C, 0x1880, 0x1883, 0x1887, 0x188A, 0x188E, 0x1891, 0x1894, 0x1898, 0x189B, 0x189F, 0x18A2, 0x18A6, 0x18A9, 0x18AD, 0x18B0, 0x18B3, 0x18B7, 0x18BA, 0x18BE, 0x18C1, 0x18C5, 0x18C8, 0x18CC, 0x18CF, 0x18D2, 0x18D6, 0x18D9, 0x18DD, 0x18E0, 0x18E3, 0x18E7, 0x18EA, 0x18EE, 0x18F1, 0x18F5, 0x18F8, 0x18FB, 0x18FF, 0x1902, 0x1906, 0x1909, 0x190C, 0x1910, 0x1913, 0x1917, 0x191A, 0x191D, 0x1921, 0x1924, 0x1928, 0x192B, 0x192E, 0x1932, 0x1935, 0x1938, 0x193C, 0x193F, 0x1943, 0x1946, 0x1949, 0x194D, 0x1950, 0x1953, 0x1957, 0x195A, 0x195D, 0x1961, 0x1964, 0x1968, 0x196B, 0x196E, 0x1972, 0x1975, 0x1978, 0x197C, 0x197F, 0x1982, 0x1986, 0x1989, 0x198C, 0x1990, 0x1993, 0x1996, 0x199A, 0x199D, 0x19A0, 0x19A4, 0x19A7, 0x19AA, 0x19AE, 0x19B1, 0x19B4, 0x19B8, 0x19BB, 0x19BE, 0x19C2, 0x19C5, 0x19C8, 0x19CC, 0x19CF, 0x19D2, 0x19D5, 0x19D9, 0x19DC, 0x19DF, 0x19E3, 0x19E6, 0x19E9, 0x19ED, 0x19F0, 0x19F3, 0x19F6, 0x19FA, 0x19FD, 0x1A00, 0x1A04, 0x1A07, 0x1A0A, 0x1A0D, 0x1A11, 0x1A14, 0x1A17, 0x1A1B, 0x1A1E, 0x1A21, 0x1A24, 0x1A28, 0x1A2B, 0x1A2E, 0x1A31, 0x1A35, 0x1A38, 0x1A3B, 0x1A3E, 0x1A42, 0x1A45, 0x1A48, 0x1A4B, 0x1A4F, 0x1A52, 0x1A55, 0x1A58, 0x1A5C, 0x1A5F, 0x1A62, 0x1A65, 0x1A69, 0x1A6C, 0x1A6F, 0x1A72, 0x1A76, 0x1A79, 0x1A7C, 0x1A7F, 0x1A83, 0x1A86, 0x1A89, 0x1A8C, 0x1A8F, 0x1A93, 0x1A96, 0x1A99, 0x1A9C, 0x1A9F, 0x1AA3, 0x1AA6, 0x1AA9, 0x1AAC, 0x1AB0, 0x1AB3, 0x1AB6, 0x1AB9, 0x1ABC, 0x1AC0, 0x1AC3, 0x1AC6, 0x1AC9, 0x1ACC, 0x1ACF, 0x1AD3, 0x1AD6, 0x1AD9, 0x1ADC, 0x1ADF, 0x1AE3, 0x1AE6, 0x1AE9, 0x1AEC, 0x1AEF, 0x1AF2, 0x1AF6, 0x1AF9, 0x1AFC, 0x1AFF, 0x1B02, 0x1B05, 0x1B09, 0x1B0C, 0x1B0F, 0x1B12, 0x1B15, 0x1B18, 0x1B1C, 0x1B1F, 0x1B22, 0x1B25, 0x1B28, 0x1B2B, 0x1B2E, 0x1B32, 0x1B35, 0x1B38, 0x1B3B, 0x1B3E, 0x1B41, 0x1B44, 0x1B48, 0x1B4B, 0x1B4E, 0x1B51, 0x1B54, 0x1B57, 0x1B5A, 0x1B5D, 0x1B61, 0x1B64, 0x1B67, 0x1B6A, 0x1B6D, 0x1B70, 0x1B73, 0x1B76, 0x1B79, 0x1B7D, 0x1B80, 0x1B83, 0x1B86, 0x1B89, 0x1B8C, 0x1B8F, 0x1B92, 0x1B95, 0x1B98, 0x1B9C, 0x1B9F, 0x1BA2, 0x1BA5, 0x1BA8, 0x1BAB, 0x1BAE, 0x1BB1, 0x1BB4, 0x1BB7, 0x1BBA, 0x1BBD, 0x1BC1, 0x1BC4, 0x1BC7, 0x1BCA, 0x1BCD, 0x1BD0, 0x1BD3, 0x1BD6, 0x1BD9, 0x1BDC, 0x1BDF, 0x1BE2, 0x1BE5, 0x1BE8, 0x1BEB, 0x1BEE, 0x1BF2, 0x1BF5, 0x1BF8, 0x1BFB, 0x1BFE, 0x1C01, 0x1C04, 0x1C07, 0x1C0A, 0x1C0D, 0x1C10, 0x1C13, 0x1C16, 0x1C19, 0x1C1C, 0x1C1F, 0x1C22, 0x1C25, 0x1C28, 0x1C2B, 0x1C2E, 0x1C31, 0x1C34, 0x1C37, 0x1C3A, 0x1C3D, 0x1C40, 0x1C43, 0x1C46, 0x1C49, 0x1C4C, 0x1C4F, 0x1C52, 0x1C55, 0x1C58, 0x1C5B, 0x1C5E, 0x1C61, 0x1C64, 0x1C67, 0x1C6A, 0x1C6D, 0x1C70, 0x1C73, 0x1C76, 0x1C79, 0x1C7C, 0x1C7F, 0x1C82, 0x1C85, 0x1C88, 0x1C8B, 0x1C8E, 0x1C91, 0x1C94, 0x1C97, 0x1C9A, 0x1C9D, 0x1CA0, 0x1CA3, 0x1CA6, 0x1CA9, 0x1CAC, 0x1CAF, 0x1CB2, 0x1CB5, 0x1CB8, 0x1CBB, 0x1CBE, 0x1CC1, 0x1CC3, 0x1CC6, 0x1CC9, 0x1CCC, 0x1CCF, 0x1CD2, 0x1CD5, 0x1CD8, 0x1CDB, 0x1CDE, 0x1CE1, 0x1CE4, 0x1CE7, 0x1CEA, 0x1CED, 0x1CF0, 0x1CF3, 0x1CF5, 0x1CF8, 0x1CFB, 0x1CFE, 0x1D01, 0x1D04, 0x1D07, 0x1D0A, 0x1D0D, 0x1D10, 0x1D13, 0x1D16, 0x1D18, 0x1D1B, 0x1D1E, 0x1D21, 0x1D24, 0x1D27, 0x1D2A, 0x1D2D, 0x1D30, 0x1D33, 0x1D35, 0x1D38, 0x1D3B, 0x1D3E, 0x1D41, 0x1D44, 0x1D47, 0x1D4A, 0x1D4D, 0x1D4F, 0x1D52, 0x1D55, 0x1D58, 0x1D5B, 0x1D5E, 0x1D61, 0x1D64, 0x1D66, 0x1D69, 0x1D6C, 0x1D6F, 0x1D72, 0x1D75, 0x1D78, 0x1D7B, 0x1D7D, 0x1D80, 0x1D83, 0x1D86, 0x1D89, 0x1D8C, 0x1D8E, 0x1D91, 0x1D94, 0x1D97, 0x1D9A, 0x1D9D, 0x1DA0, 0x1DA2, 0x1DA5, 0x1DA8, 0x1DAB, 0x1DAE, 0x1DB1, 0x1DB3, 0x1DB6, 0x1DB9, 0x1DBC, 0x1DBF, 0x1DC2, 0x1DC4, 0x1DC7, 0x1DCA, 0x1DCD, 0x1DD0, 0x1DD3, 0x1DD5, 0x1DD8, 0x1DDB, 0x1DDE, 0x1DE1, 0x1DE3, 0x1DE6, 0x1DE9, 0x1DEC, 0x1DEF, 0x1DF1, 0x1DF4, 0x1DF7, 0x1DFA, 0x1DFD, 0x1DFF, 0x1E02, 0x1E05, 0x1E08, 0x1E0B, 0x1E0D, 0x1E10, 0x1E13, 0x1E16, 0x1E19, 0x1E1B, 0x1E1E, 0x1E21, 0x1E24, 0x1E26, 0x1E29, 0x1E2C, 0x1E2F, 0x1E32, 0x1E34, 0x1E37, 0x1E3A, 0x1E3D, 0x1E3F, 0x1E42, 0x1E45, 0x1E48, 0x1E4A, 0x1E4D, 0x1E50, 0x1E53, 0x1E55, 0x1E58, 0x1E5B, 0x1E5E, 0x1E60, 0x1E63, 0x1E66, 0x1E69, 0x1E6B, 0x1E6E, 0x1E71, 0x1E74, 0x1E76, 0x1E79, 0x1E7C, 0x1E7F, 0x1E81, 0x1E84, 0x1E87, 0x1E8A, 0x1E8C, 0x1E8F, 0x1E92, 0x1E94, 0x1E97, 0x1E9A, 0x1E9D, 0x1E9F, 0x1EA2, 0x1EA5, 0x1EA8, 0x1EAA, 0x1EAD, 0x1EB0, 0x1EB2, 0x1EB5, 0x1EB8, 0x1EBA, 0x1EBD, 0x1EC0, 0x1EC3, 0x1EC5, 0x1EC8, 0x1ECB, 0x1ECD, 0x1ED0, 0x1ED3, 0x1ED5, 0x1ED8, 0x1EDB, 0x1EDE, 0x1EE0, 0x1EE3, 0x1EE6, 0x1EE8, 0x1EEB, 0x1EEE, 0x1EF0, 0x1EF3, 0x1EF6, 0x1EF8, 0x1EFB, 0x1EFE, 0x1F00, 0x1F03, 0x1F06, 0x1F08, 0x1F0B, 0x1F0E, 0x1F10, 0x1F13, 0x1F16, 0x1F18, 0x1F1B, 0x1F1E, 0x1F20, 0x1F23, 0x1F26, 0x1F28, 0x1F2B, 0x1F2E, 0x1F30, 0x1F33, 0x1F36, 0x1F38, 0x1F3B, 0x1F3D, 0x1F40, 0x1F43, 0x1F45, 0x1F48, 0x1F4B, 0x1F4D, 0x1F50, 0x1F53, 0x1F55, 0x1F58, 0x1F5A, 0x1F5D, 0x1F60, 0x1F62, 0x1F65, 0x1F68, 0x1F6A, 0x1F6D, 0x1F6F, 0x1F72, 0x1F75, 0x1F77, 0x1F7A, 0x1F7C, 0x1F7F, 0x1F82, 0x1F84, 0x1F87, 0x1F8A, 0x1F8C, 0x1F8F, 0x1F91, 0x1F94, 0x1F97, 0x1F99, 0x1F9C, 0x1F9E, 0x1FA1, 0x1FA4, 0x1FA6, 0x1FA9, 0x1FAB, 0x1FAE, 0x1FB0, 0x1FB3, 0x1FB6, 0x1FB8, 0x1FBB, 0x1FBD, 0x1FC0, 0x1FC3, 0x1FC5, 0x1FC8, 0x1FCA, 0x1FCD, 0x1FCF, 0x1FD2, 0x1FD5, 0x1FD7, 0x1FDA, 0x1FDC, 0x1FDF, 0x1FE1, 0x1FE4, 0x1FE6, 0x1FE9, 0x1FEC, 0x1FEE, 0x1FF1, 0x1FF3, 0x1FF6, 0x1FF8, 0x1FFB, 0x1FFD, 0x2000, 0x2000, }; int32_t Math_Cos(int32_t angle) { return Math_Sin(angle + DEG_90); } int32_t Math_Sin(int32_t angle) { uint16_t sector = (uint16_t)angle & (DEG_180 - 1); if (sector > DEG_90) { sector = DEG_180 - sector; } int16_t result = m_SinTable[sector >> 4]; if ((uint16_t)angle >= DEG_180) { result = -result; } return result; } int32_t Math_Atan(int32_t x, int32_t y) { if (x == 0 && y == 0) { return 0; } int8_t base = 0; if (x < 0) { base |= 4; x = -x; } if (y < 0) { base |= 2; y = -y; } if (y > x) { base |= 1; int32_t tmp = x; x = y; y = tmp; } int32_t result = m_AtanBaseTable[base] + m_AtanAngleTable[0x800 * y / x]; if (result < 0) { result = -result; } return result; } ================================================ FILE: src/trx/core/math/trig.h ================================================ #pragma once #include int32_t Math_Cos(int32_t angle); int32_t Math_Sin(int32_t angle); int32_t Math_Atan(int32_t x, int32_t y); ================================================ FILE: src/trx/core/math/types.h ================================================ #pragma once #include #pragma pack(push, 1) typedef struct { int32_t x; int32_t z; } XZ_32; typedef struct { int16_t x; int16_t z; } XZ_16; typedef struct { int32_t x; int32_t y; int32_t z; } XYZ_32; typedef struct { int16_t x; int16_t y; int16_t z; } XYZ_16; typedef struct { bool x; bool y; bool z; } XYZ_BOOL; typedef struct { float x, y, z; } XYZ_F; typedef struct { float x, y, z, w; } XYZW_F; typedef enum { DIR_UNKNOWN = -1, DIR_NORTH = 0, DIR_EAST = 1, DIR_SOUTH = 2, DIR_WEST = 3, } DIRECTION; typedef struct { XYZ_16 min; XYZ_16 max; } BOUNDS_16; typedef struct { XYZ_32 min; XYZ_32 max; } BOUNDS_32; #pragma pack(pop) ================================================ FILE: src/trx/core/math/util.c ================================================ #include bool Bounds32_Intersect(const BOUNDS_32 *const a, const BOUNDS_32 *const b) { return !( a->min.x > b->max.x || a->max.x < b->min.x || a->min.y > b->max.y || a->max.y < b->min.y || a->min.z > b->max.z || a->max.z < b->min.z); } ================================================ FILE: src/trx/core/math/util.h ================================================ #pragma once #include bool Bounds32_Intersect(const BOUNDS_32 *a, const BOUNDS_32 *b); ================================================ FILE: src/trx/core/math.h ================================================ #pragma once #include #include #include #include #include #include ================================================ FILE: src/trx/core/memory.c ================================================ #include #include #include #include #include static MEMORY_ARENA_CHUNK *M_ArenaAllocChunk( MEMORY_ARENA_ALLOCATOR *const allocator, const size_t size) { const size_t new_chunk_size = MAX(allocator->default_chunk_size, size); MEMORY_ARENA_CHUNK *const new_chunk = Memory_Alloc(sizeof(MEMORY_ARENA_CHUNK) + new_chunk_size); new_chunk->memory = (char *)new_chunk + sizeof(MEMORY_ARENA_CHUNK); new_chunk->size = new_chunk_size; new_chunk->offset = 0; new_chunk->next = nullptr; return new_chunk; } size_t Memory_Align(const size_t size) { return (size + 7) & ~7; } void *Memory_Alloc(const size_t size) { void *result = malloc(size); ASSERT(result != nullptr); memset(result, 0, size); return result; } void *Memory_Realloc(void *const memory, const size_t size) { void *result = realloc(memory, size); ASSERT(result != nullptr); return result; } void Memory_Free(void *const memory) { if (memory != nullptr) { free(memory); } } void Memory_FreePointer(void *arg) { ASSERT(arg != nullptr); void *memory; memcpy(&memory, arg, sizeof(void *)); memcpy(arg, &(void *) { nullptr }, sizeof(void *)); Memory_Free(memory); } void *Memory_Dup(const void *const buffer, const size_t size) { ASSERT(buffer != nullptr); char *memory = Memory_Alloc(size); memcpy(memory, buffer, size); return memory; } char *Memory_DupStr(const char *const string) { if (string == nullptr) { return nullptr; } char *memory = Memory_Alloc(strlen(string) + 1); strcpy(memory, string); return memory; } void *Memory_ArenaAlloc( MEMORY_ARENA_ALLOCATOR *const allocator, const size_t size) { // Ensure a default chunk size is set. if (allocator->default_chunk_size == 0) { allocator->default_chunk_size = 1024 * 4; // default to 4K } // Find first chunk that has enough space. MEMORY_ARENA_CHUNK *chunk = allocator->current_chunk; while (chunk != nullptr && chunk->offset + size > chunk->size) { chunk = chunk->next; } // If no chunk satisfies this criteria, append a new chunk. if (chunk == nullptr) { chunk = M_ArenaAllocChunk(allocator, size); if (allocator->current_chunk != nullptr) { chunk->next = allocator->current_chunk->next; allocator->current_chunk->next = chunk; } allocator->current_chunk = chunk; if (allocator->first_chunk == nullptr) { allocator->first_chunk = chunk; } } ASSERT(chunk != nullptr); // Allocate from the current chunk. void *const result = (char *)chunk->memory + chunk->offset; chunk->offset += size; return result; } void Memory_ArenaReset(MEMORY_ARENA_ALLOCATOR *const allocator) { MEMORY_ARENA_CHUNK *chunk = allocator->first_chunk; while (chunk != nullptr) { memset(chunk->memory, 0, chunk->size); chunk->offset = 0; chunk = chunk->next; } allocator->current_chunk = allocator->first_chunk; } void Memory_ArenaFree(MEMORY_ARENA_ALLOCATOR *const allocator) { MEMORY_ARENA_CHUNK *chunk = allocator->first_chunk; while (chunk != nullptr) { MEMORY_ARENA_CHUNK *const next = chunk->next; Memory_Free(chunk); chunk = next; } allocator->first_chunk = nullptr; allocator->current_chunk = nullptr; } ================================================ FILE: src/trx/core/memory.h ================================================ #pragma once #include // Basic memory utilities that exit the game in case the system runs out of // memory. // Arena allocator - a buffer that only grows, until it's reset. Doesn't // support freeing while in-use. typedef struct MEMORY_ARENA_CHUNK { void *memory; size_t size; size_t offset; struct MEMORY_ARENA_CHUNK *next; } MEMORY_ARENA_CHUNK; typedef struct { MEMORY_ARENA_CHUNK *first_chunk; MEMORY_ARENA_CHUNK *current_chunk; size_t default_chunk_size; } MEMORY_ARENA_ALLOCATOR; // Align byte count to the platform-specific pointer size. size_t Memory_Align(size_t size); // Allocate n bytes. In case the memory allocation fails, shows an error to the // user and exits the application. The allocated memory is filled with zeros. void *Memory_Alloc(size_t size); // Reallocate existing memory to n bytes, returning an address to the // reallocated memory. In case the memory allocation fails, shows an error to // the user and exits the application. All pointers to the old memory address // become invalid. Preserves the previous memory contents. If the memory is // nullptr, the function acts like Memory_Alloc. void *Memory_Realloc(void *memory, size_t size); // Frees the memory associated with a given address. If the memory is nullptr, // the function is a no-op. void Memory_Free(void *memory); // Frees the memory associated with a given pointer and sets it to nullptr. The // user is expected to pass a pointer of their variable like so: // // char *mem = Memory_Alloc(10); // Memory_FreePointer(&mem); // (mem is now nullptr) // // Giving a nullptr to this function is a fatal error. Passing mem directly is // also an error. void Memory_FreePointer(void *memory); // Duplicates a buffer. In case the memory allocation fails, shows an error to // the user and exits the application. // Giving a nullptr to this function is a fatal error. void *Memory_Dup(const void *buffer, size_t size); // Duplicates a string. In case the memory allocation fails, shows an error to // the user and exits the application. The string must be nullptr-terminated. // Giving a nullptr to this function returns nullptr. char *Memory_DupStr(const char *string); // Allocate n bytes using the arena allocator. If there's insufficient memory, // grow the buffer using internal growth function. The allocated memory is // filled with zeros. void *Memory_ArenaAlloc(MEMORY_ARENA_ALLOCATOR *allocator, size_t size); // Resets the buffer used by the arena allocator, but does not free the memory. // allocator must not be a nullptr. Used to reset the buffer, but not suffer // from performance penalty associated with reallocating the actual memory. void Memory_ArenaReset(MEMORY_ARENA_ALLOCATOR *allocator); // Frees the entire buffer owned by the arena allocator. allocator must not be // nullptr. void Memory_ArenaFree(MEMORY_ARENA_ALLOCATOR *allocator); #define AUTO_FREE __attribute__((cleanup(Memory_FreePointer))) ================================================ FILE: src/trx/core/shell.h ================================================ #pragma once void Shell_ExitSystem(const char *message); void Shell_ExitSystemEx(const char *log_message, const char *dialog_message); void Shell_ExitSystemFmt(const char *fmt, ...); ================================================ FILE: src/trx/core/strings/case_funcs.c ================================================ #include #include #include #include // Mapping of lowercase to uppercase characters beyond ASCII. static struct { const char *lower; const char *upper; } m_CaseMap[] = { #define X_CASE_MAP(low, up) { low, up }, #include #undef X_CASE_MAP { .lower = nullptr, .upper = nullptr }, }; size_t String_GetCharByteSize(const char *const ptr) { // Check for named escape sequence. if (*ptr == '\\' && *(ptr + 1) == '{') { const char *end = strchr(ptr + 2, '}'); if (end != nullptr) { return end + 1 - ptr; } return 1; } // clang-format off // UTF-8 sequence lengths if ((*ptr & 0x80) == 0x00) { return 1; } // 1-byte sequence if ((*ptr & 0xE0) == 0xC0) { return 2; } // 2-byte sequence if ((*ptr & 0xF0) == 0xE0) { return 3; } // 3-byte sequence if ((*ptr & 0xF8) == 0xF0) { return 4; } // 4-byte sequence // clang-format on // Fallback to 1 return 1; } char *String_ToUpper(const char *const text) { if (text == nullptr) { return nullptr; } const size_t text_len = strlen(text); char *const upper_text = Memory_Alloc(text_len + 1); const char *src = text; char *dest = upper_text; while (*src != '\0') { bool mapped = false; for (size_t i = 0; m_CaseMap[i].lower != nullptr; i++) { const char *const lower = m_CaseMap[i].lower; const char *const upper = m_CaseMap[i].upper; const size_t lower_len = strlen(lower); if (strncmp(src, lower, lower_len) == 0) { const size_t upper_len = strlen(upper); memcpy(dest, upper, upper_len); dest += upper_len; src += lower_len; mapped = true; break; } } if (mapped) { continue; } const size_t char_len = String_GetCharByteSize(src); memcpy(dest, src, char_len); dest += char_len; src += char_len; } *dest = '\0'; return upper_text; } char *String_ToUpperPattern(const char *const pattern) { char *const upper_pattern = Memory_DupStr(pattern); char *q = upper_pattern; while (*q != '\0') { if (*q == '%') { q++; while (*q != '\0' && !strchr("diouxXfFeEgGaAcspn%", *q)) { q++; } if (*q != '\0') { q++; } } else { *q = (char)toupper((unsigned char)*q); q++; } } return upper_pattern; } ================================================ FILE: src/trx/core/strings/case_map.def ================================================ // This file is autogenerated - do not edit. // See tools/glyphs/generate_case_map for details. X_CASE_MAP("a", "A") X_CASE_MAP("b", "B") X_CASE_MAP("c", "C") X_CASE_MAP("d", "D") X_CASE_MAP("e", "E") X_CASE_MAP("f", "F") X_CASE_MAP("g", "G") X_CASE_MAP("h", "H") X_CASE_MAP("i", "I") X_CASE_MAP("j", "J") X_CASE_MAP("k", "K") X_CASE_MAP("l", "L") X_CASE_MAP("m", "M") X_CASE_MAP("n", "N") X_CASE_MAP("o", "O") X_CASE_MAP("p", "P") X_CASE_MAP("q", "Q") X_CASE_MAP("r", "R") X_CASE_MAP("s", "S") X_CASE_MAP("t", "T") X_CASE_MAP("u", "U") X_CASE_MAP("v", "V") X_CASE_MAP("w", "W") X_CASE_MAP("x", "X") X_CASE_MAP("y", "Y") X_CASE_MAP("z", "Z") X_CASE_MAP("ª", "ª") X_CASE_MAP("µ", "Μ") X_CASE_MAP("º", "º") X_CASE_MAP("à", "À") X_CASE_MAP("á", "Á") X_CASE_MAP("â", "Â") X_CASE_MAP("ã", "Ã") X_CASE_MAP("ä", "Ä") X_CASE_MAP("å", "Å") X_CASE_MAP("æ", "Æ") X_CASE_MAP("ç", "Ç") X_CASE_MAP("è", "È") X_CASE_MAP("é", "É") X_CASE_MAP("ê", "Ê") X_CASE_MAP("ë", "Ë") X_CASE_MAP("ì", "Ì") X_CASE_MAP("í", "Í") X_CASE_MAP("î", "Î") X_CASE_MAP("ï", "Ï") X_CASE_MAP("ð", "Ð") X_CASE_MAP("ñ", "Ñ") X_CASE_MAP("ò", "Ò") X_CASE_MAP("ó", "Ó") X_CASE_MAP("ô", "Ô") X_CASE_MAP("õ", "Õ") X_CASE_MAP("ö", "Ö") X_CASE_MAP("ø", "Ø") X_CASE_MAP("ù", "Ù") X_CASE_MAP("ú", "Ú") X_CASE_MAP("û", "Û") X_CASE_MAP("ü", "Ü") X_CASE_MAP("ý", "Ý") X_CASE_MAP("þ", "Þ") X_CASE_MAP("ÿ", "Ÿ") X_CASE_MAP("ā", "Ā") X_CASE_MAP("ă", "Ă") X_CASE_MAP("ą", "Ą") X_CASE_MAP("ć", "Ć") X_CASE_MAP("ĉ", "Ĉ") X_CASE_MAP("ċ", "Ċ") X_CASE_MAP("č", "Č") X_CASE_MAP("ď", "Ď") X_CASE_MAP("đ", "Đ") X_CASE_MAP("ē", "Ē") X_CASE_MAP("ĕ", "Ĕ") X_CASE_MAP("ė", "Ė") X_CASE_MAP("ę", "Ę") X_CASE_MAP("ě", "Ě") X_CASE_MAP("ĝ", "Ĝ") X_CASE_MAP("ğ", "Ğ") X_CASE_MAP("ġ", "Ġ") X_CASE_MAP("ģ", "Ģ") X_CASE_MAP("ĥ", "Ĥ") X_CASE_MAP("ħ", "Ħ") X_CASE_MAP("ĩ", "Ĩ") X_CASE_MAP("ī", "Ī") X_CASE_MAP("ĭ", "Ĭ") X_CASE_MAP("į", "Į") X_CASE_MAP("ı", "I") X_CASE_MAP("ĵ", "Ĵ") X_CASE_MAP("ķ", "Ķ") X_CASE_MAP("ĸ", "ĸ") X_CASE_MAP("ĺ", "Ĺ") X_CASE_MAP("ļ", "Ļ") X_CASE_MAP("ľ", "Ľ") X_CASE_MAP("ŀ", "Ŀ") X_CASE_MAP("ł", "Ł") X_CASE_MAP("ń", "Ń") X_CASE_MAP("ņ", "Ņ") X_CASE_MAP("ň", "Ň") X_CASE_MAP("ŋ", "Ŋ") X_CASE_MAP("ō", "Ō") X_CASE_MAP("ŏ", "Ŏ") X_CASE_MAP("ő", "Ő") X_CASE_MAP("œ", "Œ") X_CASE_MAP("ŕ", "Ŕ") X_CASE_MAP("ŗ", "Ŗ") X_CASE_MAP("ř", "Ř") X_CASE_MAP("ś", "Ś") X_CASE_MAP("ŝ", "Ŝ") X_CASE_MAP("ş", "Ş") X_CASE_MAP("š", "Š") X_CASE_MAP("ţ", "Ţ") X_CASE_MAP("ť", "Ť") X_CASE_MAP("ŧ", "Ŧ") X_CASE_MAP("ũ", "Ũ") X_CASE_MAP("ū", "Ū") X_CASE_MAP("ŭ", "Ŭ") X_CASE_MAP("ů", "Ů") X_CASE_MAP("ű", "Ű") X_CASE_MAP("ų", "Ų") X_CASE_MAP("ŵ", "Ŵ") X_CASE_MAP("ŷ", "Ŷ") X_CASE_MAP("ź", "Ź") X_CASE_MAP("ż", "Ż") X_CASE_MAP("ž", "Ž") X_CASE_MAP("ǎ", "Ǎ") X_CASE_MAP("ǐ", "Ǐ") X_CASE_MAP("ǒ", "Ǒ") X_CASE_MAP("ǔ", "Ǔ") X_CASE_MAP("ǧ", "Ǧ") X_CASE_MAP("ǩ", "Ǩ") X_CASE_MAP("ǵ", "Ǵ") X_CASE_MAP("ǹ", "Ǹ") X_CASE_MAP("ș", "Ș") X_CASE_MAP("ț", "Ț") X_CASE_MAP("ȟ", "Ȟ") X_CASE_MAP("ȧ", "Ȧ") X_CASE_MAP("ȯ", "Ȯ") X_CASE_MAP("ȳ", "Ȳ") X_CASE_MAP("ά", "Ά") X_CASE_MAP("έ", "Έ") X_CASE_MAP("ή", "Ή") X_CASE_MAP("ί", "Ί") X_CASE_MAP("α", "Α") X_CASE_MAP("β", "Β") X_CASE_MAP("γ", "Γ") X_CASE_MAP("δ", "Δ") X_CASE_MAP("ε", "Ε") X_CASE_MAP("ζ", "Ζ") X_CASE_MAP("η", "Η") X_CASE_MAP("θ", "Θ") X_CASE_MAP("ι", "Ι") X_CASE_MAP("κ", "Κ") X_CASE_MAP("λ", "Λ") X_CASE_MAP("μ", "Μ") X_CASE_MAP("ν", "Ν") X_CASE_MAP("ξ", "Ξ") X_CASE_MAP("ο", "Ο") X_CASE_MAP("π", "Π") X_CASE_MAP("ρ", "Ρ") X_CASE_MAP("ς", "Σ") X_CASE_MAP("σ", "Σ") X_CASE_MAP("τ", "Τ") X_CASE_MAP("υ", "Υ") X_CASE_MAP("φ", "Φ") X_CASE_MAP("χ", "Χ") X_CASE_MAP("ψ", "Ψ") X_CASE_MAP("ω", "Ω") X_CASE_MAP("ϊ", "Ϊ") X_CASE_MAP("ϋ", "Ϋ") X_CASE_MAP("ό", "Ό") X_CASE_MAP("ύ", "Ύ") X_CASE_MAP("ώ", "Ώ") X_CASE_MAP("а", "А") X_CASE_MAP("б", "Б") X_CASE_MAP("в", "В") X_CASE_MAP("г", "Г") X_CASE_MAP("д", "Д") X_CASE_MAP("е", "Е") X_CASE_MAP("ж", "Ж") X_CASE_MAP("з", "З") X_CASE_MAP("и", "И") X_CASE_MAP("й", "Й") X_CASE_MAP("к", "К") X_CASE_MAP("л", "Л") X_CASE_MAP("м", "М") X_CASE_MAP("н", "Н") X_CASE_MAP("о", "О") X_CASE_MAP("п", "П") X_CASE_MAP("р", "Р") X_CASE_MAP("с", "С") X_CASE_MAP("т", "Т") X_CASE_MAP("у", "У") X_CASE_MAP("ф", "Ф") X_CASE_MAP("х", "Х") X_CASE_MAP("ц", "Ц") X_CASE_MAP("ч", "Ч") X_CASE_MAP("ш", "Ш") X_CASE_MAP("щ", "Щ") X_CASE_MAP("ъ", "Ъ") X_CASE_MAP("ы", "Ы") X_CASE_MAP("ь", "Ь") X_CASE_MAP("э", "Э") X_CASE_MAP("ю", "Ю") X_CASE_MAP("я", "Я") X_CASE_MAP("ѐ", "Ѐ") X_CASE_MAP("ё", "Ё") X_CASE_MAP("ђ", "Ђ") X_CASE_MAP("ѓ", "Ѓ") X_CASE_MAP("є", "Є") X_CASE_MAP("ѕ", "Ѕ") X_CASE_MAP("і", "І") X_CASE_MAP("ї", "Ї") X_CASE_MAP("ј", "Ј") X_CASE_MAP("љ", "Љ") X_CASE_MAP("њ", "Њ") X_CASE_MAP("ћ", "Ћ") X_CASE_MAP("ќ", "Ќ") X_CASE_MAP("ѝ", "Ѝ") X_CASE_MAP("ў", "Ў") X_CASE_MAP("џ", "Џ") X_CASE_MAP("ґ", "Ґ") X_CASE_MAP("ḃ", "Ḃ") X_CASE_MAP("ḋ", "Ḋ") X_CASE_MAP("ḟ", "Ḟ") X_CASE_MAP("ḡ", "Ḡ") X_CASE_MAP("ḣ", "Ḣ") X_CASE_MAP("ḧ", "Ḧ") X_CASE_MAP("ḱ", "Ḱ") X_CASE_MAP("ḿ", "Ḿ") X_CASE_MAP("ṁ", "Ṁ") X_CASE_MAP("ṅ", "Ṅ") X_CASE_MAP("ṕ", "Ṕ") X_CASE_MAP("ṗ", "Ṗ") X_CASE_MAP("ṙ", "Ṙ") X_CASE_MAP("ṡ", "Ṡ") X_CASE_MAP("ṫ", "Ṫ") X_CASE_MAP("ṽ", "Ṽ") X_CASE_MAP("ẁ", "Ẁ") X_CASE_MAP("ẃ", "Ẃ") X_CASE_MAP("ẅ", "Ẅ") X_CASE_MAP("ẇ", "Ẇ") X_CASE_MAP("ẋ", "Ẋ") X_CASE_MAP("ẍ", "Ẍ") X_CASE_MAP("ẏ", "Ẏ") X_CASE_MAP("ẑ", "Ẑ") X_CASE_MAP("ẽ", "Ẽ") X_CASE_MAP("ỳ", "Ỳ") X_CASE_MAP("ỹ", "Ỹ") ================================================ FILE: src/trx/core/strings/common.c ================================================ #include #include #include #include #include #include #include #include #include #include #include // Number of static buffers in a rotating ring for String_FormatStatic, etc. #define M_MAX_STATIC_BUFFERS 8 typedef struct { char *buf; size_t capacity; } M_STATIC_BUFFER; static M_STATIC_BUFFER m_StaticBufferRing[M_MAX_STATIC_BUFFERS] = {}; static int m_StaticBufNext = 0; __attribute__((destructor)) static void M_Shutdown(void) { for (int32_t i = 0; i < M_MAX_STATIC_BUFFERS; i++) { Memory_FreePointer(&m_StaticBufferRing[i].buf); m_StaticBufferRing[i].capacity = 0; } } static M_STATIC_BUFFER *M_CycleStaticBuffer(void) { const int32_t idx = m_StaticBufNext; m_StaticBufNext = (m_StaticBufNext + 1) % M_MAX_STATIC_BUFFERS; return &m_StaticBufferRing[idx]; } static void M_AddPage( const char *text, int32_t start_pos, int32_t length, VECTOR *const pages) { char substr[length + 1]; while (text[start_pos] == '\n' && text[start_pos] != '\0') { start_pos++; length--; } strncpy(substr, text + start_pos, length); substr[length] = '\0'; const char *page = Memory_DupStr(substr); Vector_Add(pages, &page); } bool String_EndsWith(const char *str, const char *suffix) { int str_len = strlen(str); int suffix_len = strlen(suffix); if (suffix_len > str_len) { return 0; } return strcmp(str + str_len - suffix_len, suffix) == 0; } bool String_Equivalent(const char *a, const char *b) { if (a == nullptr || b == nullptr) { return false; } size_t a_size = strlen(a); size_t b_size = strlen(b); if (a_size != b_size) { return false; } for (size_t i = 0; i < a_size; i++) { if (tolower(a[i]) != tolower(b[i])) { return false; } } return true; } const char *String_CaseSubstring(const char *subject, const char *pattern) { if (subject == nullptr || pattern == nullptr) { return nullptr; } size_t str_size = strlen(subject); size_t substr_size = strlen(pattern); if (substr_size > str_size) { return nullptr; } if (substr_size == 0) { return subject; } for (size_t i = 0; i < str_size + 1 - substr_size; i++) { bool equivalent = true; for (size_t j = 0; j < substr_size; j++) { if (tolower(subject[i + j]) != tolower(pattern[j])) { equivalent = false; break; } } if (equivalent) { return subject + i; } } return nullptr; } bool String_Match(const char *const subject, const char *const pattern) { if (subject == nullptr || pattern == nullptr) { return 0; } const unsigned char *const usubject = (const unsigned char *)subject; const unsigned char *const upattern = (const unsigned char *)pattern; const uint32_t options = PCRE2_CASELESS; const uint32_t ovec_size = 128; int err_code; PCRE2_SIZE err_offset; pcre2_code *const re = pcre2_compile( upattern, PCRE2_ZERO_TERMINATED, options, &err_code, &err_offset, nullptr); if (re == nullptr) { PCRE2_UCHAR8 buffer[128]; pcre2_get_error_message(err_code, buffer, 120); LOG_ERROR("%d\t%s", err_code, buffer); return false; } pcre2_match_data *const match_data = pcre2_match_data_create(ovec_size, nullptr); const int flags = 0; const int rc = pcre2_match( re, usubject, PCRE2_ZERO_TERMINATED, 0, flags, match_data, nullptr); pcre2_match_data_free(match_data); pcre2_code_free(re); return rc > 0; } bool String_IsEmpty(const char *const value) { return String_Match(value, "^\\s*$"); } bool String_ParseBool(const char *const value, bool *const target) { if (String_Match(value, "^(0|false|off)$")) { if (target != nullptr) { *target = false; } return true; } if (String_Match(value, "^(1|true|on)$")) { if (target != nullptr) { *target = true; } return true; } return false; } bool String_ParseInteger(const char *const value, int32_t *const target) { return sscanf(value, "%d", target) == 1; } bool String_ParseDecimal(const char *const value, float *const target) { bool has_dot = false; for (size_t i = 0; i < strlen(value); i++) { if (i == 0 && value[i] == '-') { continue; } if (!isdigit(value[i])) { if (value[i] == '.') { if (has_dot) { return false; } has_dot = true; } else { return false; } } } if (target != nullptr) { *target = atof(value); } return true; } bool String_ParseRGB888(const char *value, RGB_888 *const target) { if (value[0] == '#') { value++; } return sscanf( value, "%02hhX%02hhX%02hhX", &target->r, &target->g, &target->b) == 3; } bool String_ParseRGBA8888(const char *value, RGBA_8888 *const target) { if (value[0] == '#') { value++; } return sscanf( value, "%02hhX%02hhX%02hhX%02hhX", &target->r, &target->g, &target->b, &target->a) == 4; } VECTOR *String_Paginate(const char *const text, const int32_t max_lines) { VECTOR *const pages = Vector_Create(sizeof(char *)); int32_t line_count = 0; int32_t start_pos = 0; int32_t current_length = 0; const char *iter_text = text; while (*iter_text != '\0') { current_length++; if (*iter_text == '\n') { line_count++; } if (line_count == max_lines || *iter_text == '\f') { M_AddPage(text, start_pos, current_length, pages); start_pos += current_length; current_length = 0; line_count = 0; } *iter_text++; } // Anything that is left becomes its own page. if (pages->count == 0 || current_length != 0) { M_AddPage(text, start_pos, current_length, pages); } return pages; } char *String_Format(const char *const fmt, ...) { va_list args; va_start(args, fmt); int len = vsnprintf(nullptr, 0, fmt, args); if (len < 0) { va_end(args); return nullptr; } char *const result = Memory_Alloc(len + 1); va_end(args); va_start(args, fmt); vsnprintf(result, len + 1, fmt, args); va_end(args); return result; } void String_FormatInto( char **target_buf, size_t *target_cap, const char *const fmt, ...) { va_list args; va_start(args, fmt); String_FormatIntoV(target_buf, target_cap, fmt, args); va_end(args); } void String_FormatIntoV( char **target_buf, size_t *target_cap, const char *const fmt, va_list args) { va_list args_copy; va_copy(args_copy, args); const int32_t len = vsnprintf(nullptr, 0, fmt, args_copy); va_end(args_copy); if (len < 0) { return; } const size_t needed = (size_t)len + 1; if (*target_cap < needed) { *target_buf = Memory_Realloc(*target_buf, needed); *target_cap = needed; } vsnprintf(*target_buf, *target_cap, fmt, args); } const char *String_FormatStatic(const char *const fmt, ...) { va_list args; va_start(args, fmt); const char *const result = String_FormatStaticV(fmt, args); va_end(args); return result; } const char *String_FormatStaticV(const char *const fmt, va_list args) { M_STATIC_BUFFER *const buffer = M_CycleStaticBuffer(); String_FormatIntoV(&buffer->buf, &buffer->capacity, fmt, args); return buffer->buf; } ================================================ FILE: src/trx/core/strings/common.h ================================================ #pragma once #include #include #include #include bool String_EndsWith(const char *str, const char *suffix); bool String_Equivalent(const char *a, const char *b); const char *String_CaseSubstring(const char *subject, const char *pattern); bool String_Match(const char *subject, const char *pattern); bool String_IsEmpty(const char *value); bool String_ParseBool(const char *value, bool *target); bool String_ParseInteger(const char *value, int32_t *target); bool String_ParseDecimal(const char *value, float *target); bool String_ParseRGB888(const char *value, RGB_888 *target); bool String_ParseRGBA8888(const char *value, RGBA_8888 *target); size_t String_GetCharByteSize(const char *ptr); char *String_ToUpper(const char *text); char *String_ToUpperPattern(const char *text); VECTOR *String_Paginate(const char *text, int32_t max_lines); // ============================================================================ char *String_Format(const char *fmt, ...); // Like String_Format, but prints into a specified string buffer. // If the buffer is too small, reallocates it to fit the string. void String_FormatInto( char **target_buf, size_t *target_cap, const char *fmt, ...); // Like String_FormatInto, but accepts a va_list of arguments. void String_FormatIntoV( char **target_buf, size_t *target_cap, const char *fmt, va_list args); // Like String_Format, but writes into a static buffer that grows as needed. // The caller must not free() the result; it will be freed on program exit. const char *String_FormatStatic(const char *fmt, ...); // Like String_FormatStatic, but accepts a va_list of arguments. const char *String_FormatStaticV(const char *fmt, va_list args); ================================================ FILE: src/trx/core/strings/fuzzy_match.c ================================================ #include #include #include #include #include #define FULL_MATCH_SCORE_BONUS 100 #define WORD_MATCH_SCORE_BONUS 50 #define PERCENT_MATCH_SCORE 50 #define LETTER_MATCH_SCORE_BONUS 1 static STRING_FUZZY_SCORE M_GetScore( const char *const user_input, const char *const reference, const int32_t weight) { const int32_t percent_score = PERCENT_MATCH_SCORE * strlen(user_input) / strlen(reference); const int32_t letter_score = LETTER_MATCH_SCORE_BONUS * strlen(user_input); char *word_regex = Memory_Alloc(strlen(user_input) + 20); char *full_regex = Memory_Alloc(strlen(user_input) + 20); sprintf(word_regex, "\\b%s\\b", user_input); sprintf(full_regex, "^\\s*%s\\s*$", user_input); // Assume a partial match bool is_full = false; bool is_word = false; int32_t score = letter_score + percent_score; if (String_Match(reference, full_regex)) { // Got a full match is_full = true; score += FULL_MATCH_SCORE_BONUS; } else if (String_Match(reference, word_regex)) { // Got a word match is_word = true; score += WORD_MATCH_SCORE_BONUS; } else if (String_CaseSubstring(reference, user_input) == nullptr) { // No match. score = 0; } Memory_FreePointer(&word_regex); Memory_FreePointer(&full_regex); return (STRING_FUZZY_SCORE) { .is_full = is_full, .is_word = is_word, .score = score * weight, }; } static void M_DiscardNonFullMatches(VECTOR *const matches) { bool has_full_match = false; for (int32_t i = 0; i < matches->count; i++) { const STRING_FUZZY_MATCH *const match = Vector_Get(matches, i); if (match->score.is_full) { has_full_match = true; } } if (has_full_match) { for (int32_t i = matches->count - 1; i >= 0; i--) { const STRING_FUZZY_MATCH *const match = Vector_Get(matches, i); if (!match->score.is_full) { Vector_RemoveAt(matches, i); } } } } static void M_DiscardNonWordMatches(VECTOR *const matches) { bool has_word_match = false; for (int32_t i = 0; i < matches->count; i++) { const STRING_FUZZY_MATCH *const match = Vector_Get(matches, i); if (match->score.is_word) { has_word_match = true; } } if (has_word_match) { for (int32_t i = matches->count - 1; i >= 0; i--) { const STRING_FUZZY_MATCH *const match = Vector_Get(matches, i); if (!match->score.is_word) { Vector_RemoveAt(matches, i); } } } } static void M_SortMatches(VECTOR *const matches) { // sort by match length so that best-matching results appear first for (int32_t i = 0; i < matches->count; i++) { const STRING_FUZZY_MATCH *const match_1 = Vector_Get(matches, i); for (int32_t j = i + 1; j < matches->count; j++) { const STRING_FUZZY_MATCH *const match_2 = Vector_Get(matches, j); if (match_1->score.score < match_2->score.score) { Vector_Swap(matches, i, j); } } } } static void M_DiscardDuplicateMatches(VECTOR *const matches) { for (int32_t i = matches->count - 1; i >= 0; i--) { const STRING_FUZZY_MATCH *const match = Vector_Get(matches, i); bool is_unique = true; for (int32_t j = 0; j < matches->count; j++) { const STRING_FUZZY_MATCH *const other_match = Vector_Get(matches, j); if (j != i && match->value == other_match->value) { is_unique = false; break; } } if (!is_unique) { Vector_RemoveAt(matches, i); } } } VECTOR *String_FuzzyMatch(const char *user_input, const VECTOR *const source) { VECTOR *matches = Vector_Create(sizeof(STRING_FUZZY_MATCH)); for (int32_t i = 0; i < source->count; i++) { const STRING_FUZZY_SOURCE *const source_item = Vector_Get((VECTOR *)source, i); const STRING_FUZZY_SCORE score = M_GetScore(user_input, source_item->key, source_item->weight); if (score.score <= 0) { continue; } STRING_FUZZY_MATCH match = { .key = source_item->key, .value = source_item->value, .score = score, }; Vector_Add(matches, &match); } M_DiscardNonFullMatches(matches); M_DiscardNonWordMatches(matches); M_DiscardDuplicateMatches(matches); M_SortMatches(matches); return matches; } ================================================ FILE: src/trx/core/strings/fuzzy_match.h ================================================ #pragma once #include #include typedef struct { const char *key; void *value; int32_t weight; } STRING_FUZZY_SOURCE; typedef struct { bool is_full; bool is_word; int32_t score; } STRING_FUZZY_SCORE; typedef struct { const char *key; void *value; STRING_FUZZY_SCORE score; } STRING_FUZZY_MATCH; // Takes a vector of STRING_FUZZY_SOURCE. // Returns a vector of STRING_FUZZY_MATCH. VECTOR *String_FuzzyMatch(const char *user_input, const VECTOR *source); ================================================ FILE: src/trx/core/strings.h ================================================ #pragma once #include #include ================================================ FILE: src/trx/core/thread_pool.c ================================================ #include #include #include #include typedef struct JOB { THREAD_FUNC func; void *user_data; struct JOB *next; } JOB; struct THREAD_POOL { SDL_Thread **threads; size_t num_threads; SDL_mutex *queue_mutex; SDL_cond *queue_cond; JOB *queue_head; JOB *queue_tail; bool stop; size_t working_count; SDL_cond *working_cond; }; static int32_t M_WorkerThread(void *const arg) { THREAD_POOL *const pool = arg; while (true) { SDL_LockMutex(pool->queue_mutex); while (!pool->stop && pool->queue_head == nullptr) { SDL_CondWait(pool->queue_cond, pool->queue_mutex); } if (pool->stop && pool->queue_head == nullptr) { SDL_UnlockMutex(pool->queue_mutex); break; } JOB *const job = pool->queue_head; pool->queue_head = job->next; if (pool->queue_head == nullptr) { pool->queue_tail = nullptr; } pool->working_count++; SDL_UnlockMutex(pool->queue_mutex); job->func(job->user_data); Memory_Free(job); SDL_LockMutex(pool->queue_mutex); pool->working_count--; if (pool->working_count == 0 && pool->queue_head == nullptr) { SDL_CondSignal(pool->working_cond); } SDL_UnlockMutex(pool->queue_mutex); } return 0; } THREAD_POOL *ThreadPool_Create(int32_t num_threads) { if (num_threads <= 0) { num_threads = SDL_GetCPUCount(); if (num_threads <= 0) { num_threads = 1; } } THREAD_POOL *const pool = Memory_Alloc(sizeof(*pool)); pool->threads = Memory_Alloc(sizeof(SDL_Thread *) * num_threads); pool->num_threads = num_threads; pool->queue_mutex = SDL_CreateMutex(); pool->queue_cond = SDL_CreateCond(); pool->working_cond = SDL_CreateCond(); pool->queue_head = pool->queue_tail = nullptr; pool->stop = false; pool->working_count = 0; for (int32_t i = 0; i < num_threads; i++) { pool->threads[i] = SDL_CreateThread(M_WorkerThread, "worker", pool); } return pool; } void ThreadPool_Destroy(THREAD_POOL *const pool) { if (pool == nullptr) { return; } SDL_LockMutex(pool->queue_mutex); pool->stop = true; SDL_CondBroadcast(pool->queue_cond); SDL_UnlockMutex(pool->queue_mutex); for (size_t i = 0; i < pool->num_threads; i++) { SDL_WaitThread(pool->threads[i], nullptr); } SDL_DestroyCond(pool->working_cond); SDL_DestroyCond(pool->queue_cond); SDL_DestroyMutex(pool->queue_mutex); Memory_Free(pool->threads); Memory_Free(pool); } bool ThreadPool_AddJob( THREAD_POOL *const pool, THREAD_FUNC func, void *const user_data) { JOB *const job = Memory_Alloc(sizeof(JOB)); job->func = func; job->user_data = user_data; job->next = nullptr; SDL_LockMutex(pool->queue_mutex); if (pool->queue_tail) { pool->queue_tail->next = job; pool->queue_tail = job; } else { pool->queue_head = pool->queue_tail = job; } SDL_CondSignal(pool->queue_cond); SDL_UnlockMutex(pool->queue_mutex); return true; } void ThreadPool_Wait(THREAD_POOL *pool) { SDL_LockMutex(pool->queue_mutex); while (pool->queue_head != nullptr || pool->working_count != 0) { SDL_CondWait(pool->working_cond, pool->queue_mutex); } SDL_UnlockMutex(pool->queue_mutex); } ================================================ FILE: src/trx/core/thread_pool.h ================================================ #pragma once #include typedef struct THREAD_POOL THREAD_POOL; typedef void (*THREAD_FUNC)(void *userdata); // Create a thread pool with the given number of worker threads. // Returns nullptr on failure. THREAD_POOL *ThreadPool_Create(int32_t num_threads); // Destroy a thread pool, freeing all resources. // Blocks until all worker threads have exited. void ThreadPool_Destroy(THREAD_POOL *pool); // Submit a job to the thread pool. Returns true on success. bool ThreadPool_AddJob(THREAD_POOL *pool, THREAD_FUNC func, void *user_data); // Wait for all submitted jobs to complete. void ThreadPool_Wait(THREAD_POOL *pool); ================================================ FILE: src/trx/core/utils.h ================================================ #pragma once #define Q(x) #x #define QUOTE(x) Q(x) #define CONCAT_(a, b) a##b #define CONCAT(a, b) CONCAT_(a, b) #define ARRAY_SIZE(x) (sizeof((x)) / sizeof((x)[0])) #define SQUARE(A) ((A) * (A)) #ifndef ABS #define ABS(x) (((x) < 0) ? (-(x)) : (x)) #define MIN(x, y) ((x) <= (y) ? (x) : (y)) #define MAX(x, y) ((x) >= (y) ? (x) : (y)) #endif #define MIN3(x, y, z) MIN(MIN((x), (y)), (z)) #define MAX3(x, y, z) MAX(MAX((x), (y)), (z)) #define CLAMPL(a, b) \ do { \ if ((a) < (b)) \ (a) = (b); \ } while (0) #define CLAMPG(a, b) \ do { \ if ((a) > (b)) \ (a) = (b); \ } while (0) #define CLAMP(a, b, c) \ do { \ if ((a) < (b)) \ (a) = (b); \ else if ((a) > (c)) \ (a) = (c); \ } while (0) #define MINMAX(target, low, high) MAX(MIN((target), (low)), (target)) #define SWAP(a, b) \ do { \ typeof(a) SWAP_tmp_ = (a); \ (a) = (b); \ (b) = SWAP_tmp_; \ } while (0) #define SWAP2(a, b, tmp) \ do { \ (tmp) = (a); \ (a) = (b); \ (b) = (tmp); \ } while (0) #define TOGGLE(target) \ do { \ (target) = !(target); \ } while (0); #define CYCLE(target, rate, number_of) \ do { \ (target) += (rate); \ (target) += (number_of); \ (target) %= (number_of); \ } while (0); #define ALIGN(a, bytes) ((a + (bytes) - 1) & (~(bytes - 1))) #define TOGGLE_BIT(target_var, target_bit, condition) \ do { \ if (condition) { \ (target_var) |= (target_bit); \ } else { \ (target_var) &= ~(target_bit); \ } \ } while (0) #define MKTAG(a, b, c, d) \ ((a) | ((b) << 8) | ((c) << 16) | ((unsigned)(d) << 24)) #define LERP(a, b, ratio) ((a) + ((b) - (a)) * (ratio)) ================================================ FILE: src/trx/core/vector.c ================================================ #include #include #include #include #include #define VECTOR_DEFAULT_CAPACITY 4 #define VECTOR_GROWTH_RATE 2 #define P(obj) ((*obj->priv)) struct VECTOR_PRIV { char *items; }; static void M_EnsureCapacity(VECTOR *const vector, const int32_t n) { while (vector->count + n > vector->capacity) { vector->capacity *= VECTOR_GROWTH_RATE; P(vector).items = Memory_Realloc( P(vector).items, vector->item_size * vector->capacity); } } VECTOR *Vector_Create(const size_t item_size) { return Vector_CreateAtCapacity(item_size, VECTOR_DEFAULT_CAPACITY); } VECTOR *Vector_CreateAtCapacity(const size_t item_size, const int32_t capacity) { VECTOR *const vector = Memory_Alloc(sizeof(VECTOR)); vector->count = 0; vector->capacity = capacity; vector->item_size = item_size; vector->priv = Memory_Alloc(sizeof(struct VECTOR_PRIV)); P(vector).items = Memory_Alloc(item_size * capacity); return vector; } void Vector_EnsureCapacity(VECTOR *const vector, const int32_t capacity) { if (vector->capacity >= capacity) { return; } vector->capacity = capacity; P(vector).items = Memory_Realloc(P(vector).items, vector->item_size * capacity); } void Vector_Free(VECTOR *vector) { if (vector == nullptr) { return; } Memory_FreePointer(&P(vector).items); Memory_FreePointer(&vector->priv); Memory_FreePointer(&vector); } int32_t Vector_IndexOf(const VECTOR *const vector, const void *const item) { for (int32_t i = 0; i < vector->count; i++) { if (memcmp( P(vector).items + i * vector->item_size, item, vector->item_size) == 0) { return i; } } return -1; } int32_t Vector_LastIndexOf(const VECTOR *const vector, const void *const item) { const char *const items = P(vector).items; for (int32_t i = vector->count - 1; i >= 0; i--) { if (memcmp(items + i * vector->item_size, item, vector->item_size) == 0) { return i; } } return -1; } bool Vector_Contains(const VECTOR *const vector, const void *const item) { return Vector_IndexOf(vector, item) != -1; } void *Vector_Get(const VECTOR *const vector, const int32_t index) { ASSERT(index >= 0 && index < vector->count); char *const items = P(vector).items; return (void *)(items + index * vector->item_size); } void *Vector_GetData(const VECTOR *const vector) { return P(vector).items; } void *Vector_Expand(VECTOR *const vector, const int32_t count) { ASSERT(count >= 0); M_EnsureCapacity(vector, count); char *const items = P(vector).items; void *const result = items + vector->count * vector->item_size; vector->count += count; return result; } void Vector_Add(VECTOR *const vector, const void *const item) { memcpy(Vector_Expand(vector, 1), item, vector->item_size); } void Vector_Insert( VECTOR *const vector, const int32_t index, const void *const item) { ASSERT(index >= 0 && index <= vector->count); M_EnsureCapacity(vector, 1); char *const items = P(vector).items; if (index < vector->count) { memmove( items + (index + 1) * vector->item_size, items + index * vector->item_size, (vector->count - index) * vector->item_size); } memcpy(items + index * vector->item_size, item, vector->item_size); vector->count++; } void Vector_Swap( VECTOR *const vector, const int32_t index1, const int32_t index2) { ASSERT(index1 >= 0 && index1 < vector->count); ASSERT(index2 >= 0 && index2 < vector->count); if (index1 == index2) { return; } char *const items = P(vector).items; void *tmp = Memory_Alloc(vector->item_size); memcpy(tmp, items + index1 * vector->item_size, vector->item_size); memcpy( items + index1 * vector->item_size, items + index2 * vector->item_size, vector->item_size); memcpy(items + index2 * vector->item_size, tmp, vector->item_size); Memory_FreePointer(&tmp); } bool Vector_Remove(VECTOR *const vector, const void *item) { const int32_t index = Vector_IndexOf(vector, item); if (index == -1) { return false; } Vector_RemoveAt(vector, index); return true; } void Vector_RemoveAt(VECTOR *const vector, const int32_t index) { ASSERT(index >= 0 && index < vector->count); char *const items = P(vector).items; memset(items + index * vector->item_size, 0, vector->item_size); if (index + 1 < vector->count) { memmove( items + index * vector->item_size, items + (index + 1) * vector->item_size, (vector->count - (index + 1)) * vector->item_size); } vector->count--; } void Vector_Reverse(VECTOR *const vector) { int32_t i = 0; int32_t j = vector->count - 1; void *tmp = Memory_Alloc(vector->item_size); char *const items = P(vector).items; for (; i < j; i++, j--) { memcpy(tmp, items + i * vector->item_size, vector->item_size); memcpy( items + i * vector->item_size, items + j * vector->item_size, vector->item_size); memcpy(items + j * vector->item_size, tmp, vector->item_size); } Memory_FreePointer(&tmp); } void Vector_Clear(VECTOR *const vector) { vector->count = 0; } void Vector_ClearRealloc(VECTOR *const vector) { vector->count = 0; vector->capacity = VECTOR_DEFAULT_CAPACITY; P(vector).items = Memory_Realloc(P(vector).items, vector->item_size * vector->capacity); memset(P(vector).items, 0, vector->item_size * vector->count); } ================================================ FILE: src/trx/core/vector.h ================================================ #pragma once #include #include struct VECTOR_PRIV; typedef struct { int32_t count; int32_t capacity; size_t item_size; struct VECTOR_PRIV *priv; } VECTOR; VECTOR *Vector_Create(size_t item_size); VECTOR *Vector_CreateAtCapacity(size_t item_size, int32_t capacity); void Vector_EnsureCapacity(VECTOR *vector, int32_t capacity); void Vector_Free(VECTOR *vector); int32_t Vector_IndexOf(const VECTOR *vector, const void *item); int32_t Vector_LastIndexOf(const VECTOR *vector, const void *item); bool Vector_Contains(const VECTOR *vector, const void *item); void *Vector_Get(const VECTOR *vector, int32_t index); void *Vector_GetData(const VECTOR *vector); void *Vector_Expand(VECTOR *vector, int32_t count); void Vector_Add(VECTOR *vector, const void *item); void Vector_Insert(VECTOR *vector, int32_t index, const void *item); void Vector_Swap(VECTOR *vector, int32_t index1, int32_t index2); bool Vector_Remove(VECTOR *vector, const void *item); void Vector_RemoveAt(VECTOR *vector, int32_t index); void Vector_Reverse(VECTOR *vector); void Vector_Clear(VECTOR *vector); void Vector_ClearRealloc(VECTOR *vector); ================================================ FILE: src/trx/core/virtual_file.c ================================================ #include #include #include #include #include #include VFILE *VFile_CreateFromPath(const char *const path) { MYFILE *fp = File_Open(path, FILE_OPEN_READ); if (!fp) { LOG_ERROR("Can't open file %s", path); return nullptr; } const size_t data_size = File_Size(fp); char *data = Memory_Alloc(data_size); File_ReadData(fp, data, data_size); if (File_Pos(fp) != data_size) { LOG_ERROR("Can't read file %s", path); Memory_FreePointer(&data); File_Close(fp); return nullptr; } File_Close(fp); VFILE *const file = Memory_Alloc(sizeof(VFILE)); file->content = data; file->size = data_size; file->cur_ptr = file->content; return file; } VFILE *VFile_CreateFromBuffer(const char *data, size_t size) { VFILE *const file = Memory_Alloc(sizeof(VFILE)); file->content = Memory_Dup(data, size); file->size = size; file->cur_ptr = file->content; return file; } void VFile_Close(VFILE *file) { ASSERT(file != nullptr); Memory_FreePointer(&file->content); Memory_FreePointer(&file); } size_t VFile_GetPos(const VFILE *file) { return file->cur_ptr - file->content; } void VFile_SetPos(VFILE *const file, const size_t pos) { ASSERT(pos <= file->size); file->cur_ptr = file->content + pos; } void VFile_Skip(VFILE *const file, const int32_t offset) { ASSERT(VFile_TrySkip(file, offset)); } bool VFile_TrySkip(VFILE *const file, const int32_t offset) { const size_t cur_pos = VFile_GetPos(file); if (cur_pos + offset > file->size) { return false; } file->cur_ptr += offset; return true; } void VFile_Read(VFILE *const file, void *const target, const size_t size) { ASSERT(VFile_TryRead(file, target, size)); } bool VFile_TryRead(VFILE *const file, void *const target, const size_t size) { ASSERT(file != nullptr); if (size == 0) { return true; } ASSERT(target != nullptr); const size_t cur_pos = VFile_GetPos(file); if (cur_pos + size > file->size) { return false; } memcpy(target, file->cur_ptr, size); file->cur_ptr += size; return true; } int8_t VFile_ReadS8(VFILE *file) { int8_t result; VFile_Read(file, &result, sizeof(result)); return result; } int16_t VFile_ReadS16(VFILE *file) { int16_t result; VFile_Read(file, &result, sizeof(result)); return result; } int32_t VFile_ReadS32(VFILE *file) { int32_t result; VFile_Read(file, &result, sizeof(result)); return result; } uint8_t VFile_ReadU8(VFILE *file) { uint8_t result; VFile_Read(file, &result, sizeof(result)); return result; } uint16_t VFile_ReadU16(VFILE *file) { uint16_t result; VFile_Read(file, &result, sizeof(result)); return result; } uint32_t VFile_ReadU32(VFILE *file) { uint32_t result; VFile_Read(file, &result, sizeof(result)); return result; } #define DEFINE_TRY_READ(name, type) \ bool name(VFILE *const file, type *const dst) \ { \ return VFile_TryRead(file, dst, sizeof(type)); \ } DEFINE_TRY_READ(VFile_TryReadS8, int8_t) DEFINE_TRY_READ(VFile_TryReadS16, int16_t) DEFINE_TRY_READ(VFile_TryReadS32, int32_t) DEFINE_TRY_READ(VFile_TryReadU8, uint8_t) DEFINE_TRY_READ(VFile_TryReadU16, uint16_t) DEFINE_TRY_READ(VFile_TryReadU32, uint32_t) ================================================ FILE: src/trx/core/virtual_file.h ================================================ #pragma once #include #include typedef struct { char *content; size_t size; char *cur_ptr; } VFILE; VFILE *VFile_CreateFromPath(const char *path); VFILE *VFile_CreateFromBuffer(const char *data, size_t size); void VFile_Close(VFILE *file); size_t VFile_GetPos(const VFILE *file); void VFile_SetPos(VFILE *file, size_t pos); void VFile_Skip(VFILE *file, int32_t offset); void VFile_Read(VFILE *file, void *target, size_t size); int8_t VFile_ReadS8(VFILE *file); int16_t VFile_ReadS16(VFILE *file); int32_t VFile_ReadS32(VFILE *file); uint8_t VFile_ReadU8(VFILE *file); uint16_t VFile_ReadU16(VFILE *file); uint32_t VFile_ReadU32(VFILE *file); bool VFile_TrySkip(VFILE *file, int32_t offset); bool VFile_TryRead(VFILE *file, void *target, size_t size); bool VFile_TryReadS8(VFILE *file, int8_t *dst); bool VFile_TryReadS16(VFILE *file, int16_t *dst); bool VFile_TryReadS32(VFILE *file, int32_t *dst); bool VFile_TryReadU8(VFILE *file, uint8_t *dst); bool VFile_TryReadU16(VFILE *file, uint16_t *dst); bool VFile_TryReadU32(VFILE *file, uint32_t *dst); ================================================ FILE: src/trx/debug.h ================================================ #pragma once #include #define ASSERT(x) \ do { \ if (!(x)) { \ LOG_ERROR("Assertion failed: %s", #x); \ __builtin_trap(); \ } \ } while (0) #define ASSERT_FAIL() \ do { \ LOG_ERROR("Assertion failed"); \ __builtin_trap(); \ } while (0) #define ASSERT_FAIL_FMT(fmt, ...) \ do { \ LOG_ERROR("Assertion failed: " fmt __VA_OPT__(, ) __VA_ARGS__); \ __builtin_trap(); \ } while (0) #define SOFT_ASSERT(x, msg) \ do { \ if (!(x)) { \ LOG_ERROR("Warning: %s (%s)", msg, #x); \ } \ } while (0) ================================================ FILE: src/trx/game/anims/commands.c ================================================ #include #include #include #include static void M_ParseCommand(ANIM_COMMAND *const command, const int16_t **data) { const int16_t *data_ptr = *data; command->type = *data_ptr++; switch (command->type) { case AC_MOVE_ORIGIN: { XYZ_16 *const pos = GameBuf_Alloc(sizeof(XYZ_16), GBUF_ANIM_COMMANDS); pos->x = *data_ptr++; pos->y = *data_ptr++; pos->z = *data_ptr++; command->data = (void *)pos; break; } case AC_JUMP_VELOCITY: { ANIM_COMMAND_VELOCITY_DATA *const cmd_data = GameBuf_Alloc( sizeof(ANIM_COMMAND_VELOCITY_DATA), GBUF_ANIM_COMMANDS); cmd_data->fall_speed = *data_ptr++; cmd_data->speed = *data_ptr++; command->data = (void *)cmd_data; break; } case AC_SOUND_FX: case AC_EFFECT: { ANIM_COMMAND_EFFECT_DATA *const cmd_data = GameBuf_Alloc(sizeof(ANIM_COMMAND_EFFECT_DATA), GBUF_ANIM_COMMANDS); cmd_data->frame_num = *data_ptr++; const int16_t effect_data = *data_ptr++; cmd_data->effect_num = effect_data & 0x3FFF; cmd_data->fx_type = 0; if (command->type == AC_EFFECT && g_TRVersion == 3) { cmd_data->fx_type = effect_data & 0xC000; cmd_data->environment = ACE_ALL; } else { cmd_data->environment = (effect_data & 0xC000) >> 14; } command->data = (void *)cmd_data; break; } default: command->data = nullptr; break; } *data = data_ptr; } void Anim_LoadCommands(const int16_t *data) { BENCHMARK benchmark = Benchmark_Start(); const int32_t anim_count = Anim_GetTotalCount(); for (int32_t i = 0; i < anim_count; i++) { ANIM *const anim = Anim_GetAnim(i); if (anim->num_commands == 0) { continue; } anim->commands = GameBuf_Alloc( sizeof(ANIM_COMMAND) * anim->num_commands, GBUF_ANIM_COMMANDS); const int16_t *data_ptr = &data[anim->command_idx]; for (int32_t j = 0; j < anim->num_commands; j++) { ANIM_COMMAND *const command = &anim->commands[j]; M_ParseCommand(command, &data_ptr); } } Benchmark_End(&benchmark, nullptr); } ================================================ FILE: src/trx/game/anims/common.c ================================================ #include #include #include static int32_t m_AnimCount = 0; static int32_t m_ChangeCount = 0; static int32_t m_RangeCount = 0; static int32_t m_BoneCount = 0; static ANIM *m_Anims = nullptr; static ANIM_CHANGE *m_Changes = nullptr; static ANIM_RANGE *m_Ranges = nullptr; static ANIM_BONE *m_Bones = nullptr; static ANIM m_NullAnim = {}; void Anim_InitialiseAnims(const int32_t num_anims) { m_AnimCount = num_anims; m_Anims = GameBuf_Alloc(sizeof(ANIM) * num_anims, GBUF_ANIMS); } void Anim_InitialiseChanges(const int32_t num_changes) { m_ChangeCount = num_changes; m_Changes = GameBuf_Alloc(sizeof(ANIM_CHANGE) * num_changes, GBUF_ANIM_CHANGES); } void Anim_InitialiseRanges(const int32_t num_ranges) { m_RangeCount = num_ranges; m_Ranges = GameBuf_Alloc(sizeof(ANIM_RANGE) * num_ranges, GBUF_ANIM_RANGES); } void Anim_InitialiseBones(const int32_t num_bones) { m_BoneCount = num_bones; m_Bones = GameBuf_Alloc(sizeof(ANIM_BONE) * num_bones, GBUF_ANIM_BONES); } int32_t Anim_GetTotalCount(void) { return m_AnimCount; } ANIM *Anim_GetAnim(const int32_t anim_idx) { if (anim_idx < 0 || anim_idx >= m_AnimCount) { return &m_NullAnim; } return &m_Anims[anim_idx]; } ANIM_CHANGE *Anim_GetChange(const int32_t change_idx) { ASSERT(change_idx >= 0 && change_idx < m_ChangeCount); return &m_Changes[change_idx]; } ANIM_RANGE *Anim_GetRange(const int32_t range_idx) { ASSERT(range_idx >= 0 && range_idx < m_RangeCount); return &m_Ranges[range_idx]; } ANIM_BONE *Anim_GetBone(const int32_t bone_idx) { ANIM_BONE *const result = Anim_TryGetBone(bone_idx); ASSERT(result != nullptr); return result; } ANIM_BONE *Anim_TryGetBone(const int32_t bone_idx) { if (bone_idx >= 0 && bone_idx < m_BoneCount) { return &m_Bones[bone_idx]; } return nullptr; } bool Anim_TestAbsFrameEqual(const int16_t abs_frame, const int16_t frame) { return abs_frame == frame; } bool Anim_TestAbsFrameRange( const int16_t abs_frame, const int16_t start, const int16_t end) { return abs_frame >= start && abs_frame <= end; } bool Anim_HasChange(const ANIM *const anim, const int16_t goal_state_id) { for (int32_t i = 0; i < anim->num_changes; i++) { const ANIM_CHANGE *const change = Anim_GetChange(anim->change_idx + i); if (change->goal_anim_state == goal_state_id) { return true; } } return false; } bool Anim_HasFXCommand(const ANIM *const anim, const int16_t fx_num) { return Anim_HasFXCommandBetween( anim, fx_num, 0, anim->frame_end - anim->frame_base); } bool Anim_HasFXCommandBetween( const ANIM *const anim, const int16_t fx_num, const int32_t frame_a, const int32_t frame_b) { for (int32_t i = 0; i < anim->num_commands; i++) { const ANIM_COMMAND *const cmd = &anim->commands[i]; if (cmd->type != AC_EFFECT) { continue; } const ANIM_COMMAND_EFFECT_DATA *const data = (ANIM_COMMAND_EFFECT_DATA *)cmd->data; if (data->effect_num != fx_num) { continue; } const int32_t frame_num = data->frame_num - anim->frame_base; if (frame_num >= frame_a && frame_num <= frame_b) { return true; } } return false; } ================================================ FILE: src/trx/game/anims/common.h ================================================ #pragma once #include #include #define NO_ANIM (-1) void Anim_InitialiseAnims(int32_t num_anims); void Anim_InitialiseChanges(int32_t num_changes); void Anim_InitialiseRanges(int32_t num_ranges); void Anim_InitialiseBones(int32_t num_bones); void Anim_LoadCommands(const int16_t *data); void Anim_InitialiseFrames(int32_t num_frames); int32_t Anim_GetTotalFrameCount( const LEVEL_FORMAT_LOADER *loader, int32_t frame_data_length); void Anim_LoadFrames( const LEVEL_FORMAT_LOADER *loader, const int16_t *data, int32_t data_length); int32_t Anim_GetTotalCount(void); ANIM *Anim_GetAnim(int32_t anim_idx); ANIM_CHANGE *Anim_GetChange(int32_t change_idx); ANIM_RANGE *Anim_GetRange(int32_t range_idx); ANIM_BONE *Anim_GetBone(int32_t bone_idx); ANIM_BONE *Anim_TryGetBone(int32_t bone_idx); bool Anim_TestAbsFrameEqual(int16_t abs_frame, int16_t frame); bool Anim_TestAbsFrameRange(int16_t abs_frame, int16_t start, int16_t end); bool Anim_HasChange(const ANIM *anim, int16_t goal_state_id); bool Anim_HasFXCommand(const ANIM *anim, int16_t fx_num); bool Anim_HasFXCommandBetween( const ANIM *anim, int16_t fx_num, int32_t frame_a, int32_t frame_b); ================================================ FILE: src/trx/game/anims/enum.h ================================================ #pragma once // clang-format off typedef enum { AC_NULL = 0, AC_MOVE_ORIGIN = 1, AC_JUMP_VELOCITY = 2, AC_ATTACK_READY = 3, AC_DEACTIVATE = 4, AC_SOUND_FX = 5, AC_EFFECT = 6, } ANIM_COMMAND_TYPE; typedef enum { ACE_ALL = 0, ACE_LAND = 1, ACE_WATER = 2, } ANIM_COMMAND_ENVIRONMENT; // clang-format on ================================================ FILE: src/trx/game/anims/frames.c ================================================ #include #include #include #include #include #include #include #include #include typedef enum { RPM_ALL = 0, RPM_X = 1, RPM_Y = 2, RPM_Z = 3, } ROT_PACK_MODE; static ANIM_FRAME *m_Frames = nullptr; static int32_t M_GetAnimFrameCount( const LEVEL_FORMAT_LOADER *const loader, const int32_t anim_idx, const int32_t frame_data_length) { const ANIM *const anim = Anim_GetAnim(anim_idx); if (loader->game_version == 1) { return (int32_t)ceil( ((anim->frame_end - anim->frame_base) / (float)anim->interpolation) + 1); } else { uint32_t next_ofs = anim_idx == Anim_GetTotalCount() - 1 ? (unsigned)(sizeof(int16_t) * frame_data_length) : Anim_GetAnim(anim_idx + 1)->frame_ofs; if (anim->frame_size == 0) { ASSERT(next_ofs - anim->frame_ofs == 0); return 0; } return (next_ofs - anim->frame_ofs) / (int32_t)(sizeof(int16_t) * anim->frame_size); } } static OBJECT *M_GetAnimObject(const int32_t anim_idx) { for (int32_t i = O_FIRST; i < O_NUMBER_OF; i++) { OBJECT *const obj = Object_Get(i); if (obj->loaded && obj->mesh_count >= 0 && obj->anim_idx == anim_idx) { return obj; } } return nullptr; } static ANIM_FRAME *M_FindFrameBase(const uint32_t frame_ofs) { const int32_t anim_count = Anim_GetTotalCount(); for (int32_t i = 0; i < anim_count; i++) { const ANIM *const anim = Anim_GetAnim(i); if (anim->frame_ofs == frame_ofs) { return anim->frame_ptr; } } return nullptr; } static void M_ExtractRotation( XYZ_16 *const rot, const int16_t rot_val_1, const int16_t rot_val_2) { rot->x = (rot_val_1 & 0x3FF0) << 2; rot->y = (((rot_val_1 & 0xF) << 6) | ((rot_val_2 & 0xFC00) >> 10)) << 6; rot->z = (rot_val_2 & 0x3FF) << 6; } static void M_ParseMeshRotation( const LEVEL_FORMAT_LOADER *const loader, XYZ_16 *const rot, const int16_t **data) { const int16_t *data_ptr = *data; if (loader->game_version == 1) { const int16_t rot_val_1 = *data_ptr++; const int16_t rot_val_2 = *data_ptr++; M_ExtractRotation(rot, rot_val_2, rot_val_1); } else { rot->x = 0; rot->y = 0; rot->z = 0; const int16_t rot_val_1 = *data_ptr++; const ROT_PACK_MODE mode = (ROT_PACK_MODE)((rot_val_1 & 0xC000) >> 14); switch (mode) { case RPM_X: rot->x = (rot_val_1 & 0x3FF) << 6; break; case RPM_Y: rot->y = (rot_val_1 & 0x3FF) << 6; break; case RPM_Z: rot->z = (rot_val_1 & 0x3FF) << 6; break; default: const int16_t rot_val_2 = *data_ptr++; M_ExtractRotation(rot, rot_val_1, rot_val_2); break; } } *data = data_ptr; } static int32_t M_ParseFrame( const LEVEL_FORMAT_LOADER *const loader, ANIM_FRAME *const frame, const int16_t *data_ptr, int16_t mesh_count, const uint8_t frame_size) { const int16_t *const frame_start = data_ptr; frame->bounds.min.x = *data_ptr++; frame->bounds.max.x = *data_ptr++; frame->bounds.min.y = *data_ptr++; frame->bounds.max.y = *data_ptr++; frame->bounds.min.z = *data_ptr++; frame->bounds.max.z = *data_ptr++; frame->offset.x = *data_ptr++; frame->offset.y = *data_ptr++; frame->offset.z = *data_ptr++; if (loader->game_version == 1) { mesh_count = *data_ptr++; } frame->mesh_rots = GameBuf_Alloc(sizeof(XYZ_16) * mesh_count, GBUF_ANIM_FRAMES); for (int32_t i = 0; i < mesh_count; i++) { XYZ_16 *const rot = &frame->mesh_rots[i]; M_ParseMeshRotation(loader, rot, &data_ptr); } if (loader->game_version > 1) { data_ptr += MAX(0, frame_size - (data_ptr - frame_start)); } return data_ptr - frame_start; } int32_t Anim_GetTotalFrameCount( const LEVEL_FORMAT_LOADER *const loader, const int32_t frame_data_length) { const int32_t anim_count = Anim_GetTotalCount(); int32_t total_frame_count = 0; for (int32_t i = 0; i < anim_count; i++) { total_frame_count += M_GetAnimFrameCount(loader, i, frame_data_length); } return total_frame_count; } void Anim_InitialiseFrames(const int32_t num_frames) { LOG_INFO("%d anim frames", num_frames); m_Frames = GameBuf_Alloc(sizeof(ANIM_FRAME) * num_frames, GBUF_ANIM_FRAMES); } void Anim_LoadFrames( const LEVEL_FORMAT_LOADER *const loader, const int16_t *data, const int32_t data_length) { BENCHMARK benchmark = Benchmark_Start(); const int32_t anim_count = Anim_GetTotalCount(); OBJECT *cur_obj = nullptr; int32_t frame_idx = 0; for (int32_t i = 0; i < anim_count; i++) { OBJECT *const next_obj = M_GetAnimObject(i); const bool obj_changed = next_obj != nullptr; if (obj_changed) { cur_obj = next_obj; cur_obj->anim_count = 0; } if (cur_obj == nullptr) { continue; } ANIM *const anim = Anim_GetAnim(i); cur_obj->anim_count++; const int32_t frame_count = M_GetAnimFrameCount(loader, i, data_length); const int16_t *data_ptr = &data[anim->frame_ofs / sizeof(int16_t)]; for (int32_t j = 0; j < frame_count; j++) { ANIM_FRAME *const frame = &m_Frames[frame_idx++]; if (j == 0) { anim->frame_ptr = frame; if (obj_changed) { cur_obj->frame_base = frame; } } data_ptr += M_ParseFrame( loader, frame, data_ptr, cur_obj->mesh_count, anim->frame_size); } } // Some OG data contains objects that point to the previous object's frames, // so ensure everything that's loaded is configured as such. for (int32_t i = O_FIRST; i < O_NUMBER_OF; i++) { OBJECT *const obj = Object_Get(i); if (obj->loaded && obj->mesh_count >= 0 && obj->anim_idx == NO_ANIM && obj->frame_base == nullptr) { obj->frame_base = M_FindFrameBase(obj->frame_ofs); } } Benchmark_End(&benchmark, nullptr); } ================================================ FILE: src/trx/game/anims/types.h ================================================ #pragma once #include #include typedef struct { int16_t goal_anim_state; int16_t num_ranges; int16_t range_idx; } ANIM_CHANGE; typedef struct { int16_t start_frame; int16_t end_frame; int16_t link_anim_num; int16_t link_frame_num; } ANIM_RANGE; typedef struct { ANIM_COMMAND_TYPE type; void *data; } ANIM_COMMAND; typedef struct { int16_t fall_speed; int16_t speed; } ANIM_COMMAND_VELOCITY_DATA; typedef struct { int16_t frame_num; int16_t effect_num; ANIM_COMMAND_ENVIRONMENT environment; int16_t fx_type; } ANIM_COMMAND_EFFECT_DATA; typedef struct { bool matrix_pop; bool matrix_push; XYZ_BOOL rot; XYZ_32 pos; } ANIM_BONE; typedef struct { BOUNDS_16 bounds; XYZ_16 offset; XYZ_16 *mesh_rots; } ANIM_FRAME; typedef struct { ANIM_FRAME *frame_ptr; uint32_t frame_ofs; uint8_t interpolation; uint8_t frame_size; int16_t current_anim_state; int32_t velocity; int32_t acceleration; int16_t frame_base; int16_t frame_end; int16_t jump_anim_num; int16_t jump_frame_num; int16_t num_changes; int16_t change_idx; int16_t num_commands; int16_t command_idx; ANIM_COMMAND *commands; } ANIM; ================================================ FILE: src/trx/game/anims.h ================================================ #pragma once #include #include #include ================================================ FILE: src/trx/game/camera/box_camera.c ================================================ #include #include #include #include #include #include #include // clang-format off #define M_MAX_ELEVATION (85 * DEG_1) // = 15470 #define M_COMBAT_DISTANCE (WALL_L * 5 / 2) // = 2560 #define M_LOOK_DISTANCE (WALL_L * 3 / 2) // = 1536 #define M_LOOK_CLAMP (STEP_L + 50) // = 296 // clang-format on #define M_SHIFT_ARGS \ int32_t *x, int32_t *y, int32_t *h, int32_t target_x, int32_t target_y, \ int32_t target_h, int32_t left, int32_t top, int32_t right, \ int32_t bottom typedef struct { int16_t chase_speed; int32_t min_square; int32_t shift_scale; bool test_shift_pair; bool clip_shift_height; bool test_early_lb_shift; bool use_fixed_los_check; bool override_chase_speed; CAMERA_LOOK_SETTINGS look_settings; CAMERA_LOOK_SETTINGS look_settings_surf; } M_SETTINGS; static const M_SETTINGS m_CameraSettings[CAMERA_MODE_NUMBER_OF] = { [CAMERA_MODE_TR1] = { .chase_speed = 12, .min_square = SQUARE(WALL_L / 4), .shift_scale = 1, .test_shift_pair = false, .clip_shift_height = false, .test_early_lb_shift = true, .use_fixed_los_check = false, .override_chase_speed = false, .look_settings = { // clang-format off .max_head_rotation = +50 * DEG_1, .min_head_rotation = -50 * DEG_1, .head_turn = +2 * DEG_1, .max_head_tilt = +22 * DEG_1, .min_head_tilt = -42 * DEG_1, .torso_head_rot_y = 1.0f, .torso_head_rot_x = 1.0f, // clang-format on }, .look_settings_surf = { // clang-format off .head_turn = +3 * DEG_1, .max_head_rotation = +50 * DEG_1, .min_head_rotation = -50 * DEG_1, .max_head_tilt = +40 * DEG_1, .min_head_tilt = -40 * DEG_1, .torso_head_rot_y = 0.5f, .torso_head_rot_x = 0.0f, // clang-format on }, }, [CAMERA_MODE_TR2] = { .chase_speed = 10, .min_square = SQUARE(WALL_L / 3), .shift_scale = 2, .test_shift_pair = true, .clip_shift_height = true, .test_early_lb_shift = false, .use_fixed_los_check = true, .override_chase_speed = true, .look_settings = { // clang-format off .head_turn = +2 * DEG_1, .max_head_rotation = +44 * DEG_1, .min_head_rotation = -44 * DEG_1, .max_head_tilt = +22 * DEG_1, .min_head_tilt = -42 * DEG_1, .torso_head_rot_y = 1.0f, .torso_head_rot_x = 1.0f, // clang-format on }, .look_settings_surf = { // clang-format off .head_turn = +3 * DEG_1, .max_head_rotation = +50 * DEG_1, .min_head_rotation = -50 * DEG_1, .max_head_tilt = +40 * DEG_1, .min_head_tilt = -40 * DEG_1, .torso_head_rot_y = 0.5f, .torso_head_rot_x = 0.0f, // clang-format on }, }, }; static BOX_INFO m_FixedBox = {}; static const M_SETTINGS *M_GetSettings(void) { return &m_CameraSettings[g_Config.visuals.camera_mode]; } static int16_t M_GetChaseSpeed(void) { return M_GetSettings()->chase_speed; } static const CAMERA_LOOK_SETTINGS *M_GetLookSettingsFunc(const bool on_surface) { return on_surface ? &M_GetSettings()->look_settings_surf : &M_GetSettings()->look_settings; } static const BOX_INFO *M_GetBox( const SECTOR *const sector, const int32_t x, const int32_t z, const bool generate_box) { if (sector->box != NO_BOX) { return Box_GetBox(sector->box); } if (!generate_box) { return nullptr; } // A level may have blocked specific sector or room pathfinding, so create a // dummy one-sector box to prevent erratic camera positioning. m_FixedBox.left = ROUND_TO_SECTOR(z); m_FixedBox.top = ROUND_TO_SECTOR(x); m_FixedBox.right = ROUND_TO_SECTOR_END(z); m_FixedBox.bottom = ROUND_TO_SECTOR_END(x); return &m_FixedBox; } static const SECTOR *M_GetSector( const int32_t x, const int32_t y, const int32_t z, int16_t room_num) { const XYZ_32 pos = { x, y, z }; const SECTOR *const sector = Room_GetSector(pos, &room_num); const int32_t height = Room_GetHeight(sector, pos); const int32_t ceiling = Room_GetCeiling(sector, pos); if (y > height || y < ceiling) { return nullptr; } return sector; } static bool M_IsGoodPosition( const int32_t x, const int32_t y, const int32_t z, int16_t room_num) { return M_GetSector(x, y, z, room_num) != nullptr; } static int32_t M_ShiftClamp(GAME_VECTOR *const pos, const int32_t clamp) { const SECTOR *const sector = Room_GetSector(pos->pos, &pos->room_num); const BOX_INFO *const box = M_GetBox(sector, pos->x, pos->z, true); const XYZ_32 old_pos = pos->pos; const int32_t left = box->left + clamp; const int32_t right = box->right - clamp; if (pos->z < left && !M_IsGoodPosition(pos->x, pos->y, pos->z - clamp, pos->room_num)) { pos->z = left; } else if ( pos->z > right && !M_IsGoodPosition(pos->x, pos->y, pos->z + clamp, pos->room_num)) { pos->z = right; } const int32_t top = box->top + clamp; const int32_t bottom = box->bottom - clamp; if (pos->x < top && !M_IsGoodPosition(pos->x - clamp, pos->y, pos->z, pos->room_num)) { pos->x = top; } else if ( pos->x > bottom && !M_IsGoodPosition(pos->x + clamp, pos->y, pos->z, pos->room_num)) { pos->x = bottom; } int32_t height = Room_GetHeight(sector, old_pos) - clamp; int32_t ceiling = Room_GetCeiling(sector, old_pos) + clamp; if (height < ceiling) { ceiling = (height + ceiling) >> 1; height = ceiling; } if (old_pos.y > height) { return height - old_pos.y; } else if (old_pos.y < ceiling) { return ceiling - old_pos.y; } return 0; } static void M_SmartShift(GAME_VECTOR *const target, void (*shift)(M_SHIFT_ARGS)) { LOS_Check(&g_Camera.target, target, false); const ROOM *room = Room_Get(g_Camera.target.room_num); const SECTOR *sector = Room_GetWorldSector(room, g_Camera.target.x, g_Camera.target.z); const BOX_INFO *box = M_GetBox(sector, g_Camera.target.x, g_Camera.target.z, true); room = Room_Get(target->room_num); sector = Room_GetWorldSector(room, target->x, target->z); if (room->flags.swamp) { target->y = room->max_ceiling - STEP_L; sector = Room_GetSector(target->pos, &target->room_num); } if (target->z < box->left || target->z > box->right || target->x < box->top || target->x > box->bottom) { box = M_GetBox(sector, target->x, target->z, true); } int32_t left = box->left; int32_t right = box->right; int32_t top = box->top; int32_t bottom = box->bottom; const M_SETTINGS *const settings = M_GetSettings(); int32_t test = ROUND_TO_SECTOR_END(target->z - WALL_L); const SECTOR *const good_left = M_GetSector(target->x, target->y, test, target->room_num); if (good_left != nullptr) { box = M_GetBox(good_left, target->x, test, false); if (box != nullptr && box->left < left) { left = box->left; } } else if (settings->test_shift_pair) { left = test; } test = ROUND_TO_SECTOR(target->z + WALL_L); const SECTOR *const good_right = M_GetSector(target->x, target->y, test, target->room_num); if (good_right != nullptr) { box = M_GetBox(good_right, target->x, test, false); if (box != nullptr && box->right > right) { right = box->right; } } else if (settings->test_shift_pair) { right = test; } test = ROUND_TO_SECTOR_END(target->x - WALL_L); const SECTOR *const good_top = M_GetSector(test, target->y, target->z, target->room_num); if (good_top != nullptr) { box = M_GetBox(good_top, test, target->z, false); if (box != nullptr && box->top < top) { top = box->top; } } else if (settings->test_shift_pair) { top = test; } test = ROUND_TO_SECTOR(target->x + WALL_L); const SECTOR *const good_bottom = M_GetSector(test, target->y, target->z, target->room_num); if (good_bottom != nullptr) { box = M_GetBox(good_bottom, test, target->z, false); if (box != nullptr && box->bottom > bottom) { bottom = box->bottom; } } else if (settings->test_shift_pair) { bottom = test; } left += STEP_L; right -= STEP_L; top += STEP_L; bottom -= STEP_L; GAME_VECTOR target_a = { .x = target->x, .y = target->y, .z = target->z, .room_num = target->room_num, }; GAME_VECTOR target_b = { .x = target->x, .y = target->y, .z = target->z, .room_num = target->room_num, }; bool clip = false; bool prefer_a = !settings->test_shift_pair; #define L_SHIFT(axis1, axis2, l1, l2, r1, r2) \ shift( \ &target_a.axis1, &target_a.axis2, &target_a.y, g_Camera.target.axis1, \ g_Camera.target.axis2, g_Camera.target.y, l1, l2, r1, r2); \ shift( \ &target_b.axis1, &target_b.axis2, &target_b.y, g_Camera.target.axis1, \ g_Camera.target.axis2, g_Camera.target.y, l1, r2, r1, l2) if (!settings->test_shift_pair) { if (target->z < left && good_left == nullptr) { clip = true; if (target->x < g_Camera.target.x) { L_SHIFT(z, x, left, top, right, bottom); } else { L_SHIFT(z, x, left, bottom, right, top); } } else if (target->z > right && good_right == nullptr) { clip = true; if (target->x < g_Camera.target.x) { L_SHIFT(z, x, right, top, left, bottom); } else { L_SHIFT(z, x, right, bottom, left, top); } } else if (target->x < top && good_top == nullptr) { clip = true; if (target->z < g_Camera.target.z) { L_SHIFT(x, z, top, left, bottom, right); } else { L_SHIFT(x, z, top, right, bottom, left); } } else if (target->x > bottom && good_bottom == nullptr) { clip = true; if (target->z < g_Camera.target.z) { L_SHIFT(x, z, bottom, left, top, right); } else { L_SHIFT(x, z, bottom, right, top, left); } } } else { if (ABS(target->z - g_Camera.target.z) > ABS(target->x - g_Camera.target.x)) { if (target->z < left && good_left == nullptr) { clip = true; prefer_a = g_Camera.pos.x < g_Camera.target.x; L_SHIFT(z, x, left, top, right, bottom); } else if (target->z > right && good_right == nullptr) { clip = true; prefer_a = g_Camera.pos.x < g_Camera.target.x; L_SHIFT(z, x, right, top, left, bottom); } else if (target->x < top && good_top == nullptr) { clip = true; prefer_a = target->z < g_Camera.target.z; L_SHIFT(x, z, top, left, bottom, right); } else if (target->x > bottom && good_bottom == nullptr) { clip = true; prefer_a = target->z < g_Camera.target.z; L_SHIFT(x, z, bottom, left, top, right); } } else { if (target->x < top && good_top == nullptr) { clip = true; prefer_a = g_Camera.pos.z < g_Camera.target.z; L_SHIFT(x, z, top, left, bottom, right); } else if (target->x > bottom && good_bottom == nullptr) { clip = true; prefer_a = g_Camera.pos.z < g_Camera.target.z; L_SHIFT(x, z, bottom, left, top, right); } else if (target->z < left && good_left == nullptr) { clip = true; prefer_a = target->x < g_Camera.target.x; L_SHIFT(z, x, left, top, right, bottom); } else if (target->z > right && good_right == nullptr) { clip = true; prefer_a = target->x < g_Camera.target.x; L_SHIFT(z, x, right, top, left, bottom); } } } #undef L_SHIFT if (!clip) { return; } if (settings->test_shift_pair) { if (prefer_a) { prefer_a = LOS_Check(&g_Camera.target, &target_a, false); } else { prefer_a = !LOS_Check(&g_Camera.target, &target_b, false); } } if (prefer_a) { target->pos = target_a.pos; } else { target->pos = target_b.pos; } Room_GetSector(target->pos, &target->room_num); } static void M_Clip(M_SHIFT_ARGS) { const int32_t x_diff = *x - target_x; const int32_t y_diff = *y - target_y; const int32_t h_diff = *h - target_h; int32_t height = *h; if ((right > left) != (target_x < left)) { if (x_diff != 0) { *y = target_y + (left - target_x) * y_diff / x_diff; height = target_h + (left - target_x) * h_diff / x_diff; } *x = left; } if ((bottom > top && target_y > top && (*y) < top) || (bottom < top && target_y < top && (*y) > top)) { if (y_diff != 0) { *x = target_x + (top - target_y) * x_diff / y_diff; height = target_h + (top - target_y) * h_diff / y_diff; } *y = top; } const M_SETTINGS *const settings = M_GetSettings(); if (settings->clip_shift_height) { *h = height; } } static void M_Shift(M_SHIFT_ARGS) { const int32_t l_square = SQUARE(target_x - left); const int32_t r_square = SQUARE(target_x - right); const int32_t t_square = SQUARE(target_y - top); const int32_t b_square = SQUARE(target_y - bottom); const int32_t tl_square = t_square + l_square; const int32_t tr_square = t_square + r_square; const int32_t bl_square = b_square + l_square; const M_SETTINGS *const settings = M_GetSettings(); const int32_t scaled_target = g_Camera.target_square * settings->shift_scale; int32_t shift; if (g_Camera.target_square < tl_square) { *x = left; shift = g_Camera.target_square - l_square; if (shift >= 0) { shift = Math_Sqrt(shift); *y = target_y + (top >= bottom ? shift : -shift); } } else if (tl_square > settings->min_square) { *x = left; *y = top; } else if (g_Camera.target_square < bl_square) { *x = left; shift = g_Camera.target_square - l_square; if (shift >= 0) { shift = Math_Sqrt(shift); *y = target_y + (top < bottom ? shift : -shift); } } else if ( settings->test_early_lb_shift && bl_square > settings->min_square) { *x = left; *y = bottom; } else if (scaled_target < tr_square) { shift = scaled_target - t_square; if (shift >= 0) { shift = Math_Sqrt(shift); *x = target_x + (left < right ? shift : -shift); *y = top; } } else if (settings->test_early_lb_shift || bl_square <= tr_square) { *x = right; *y = top; } else { *x = left; *y = bottom; } } static void M_Move(const GAME_VECTOR *const target, const int32_t speed) { const GAME_VECTOR old_pos = g_Camera.pos; GAME_VECTOR pos = g_Camera.pos; pos.x += (target->x - pos.x) / speed; pos.z += (target->z - pos.z) / speed; pos.y += (target->y - pos.y) / speed; pos.room_num = target->room_num; Camera_SetChunky(false); const SECTOR *sector = Room_GetSector(pos.pos, &pos.room_num); int32_t height = Room_GetHeight(sector, pos.pos); if (height == NO_HEIGHT) { // Attempt to clamp within the previous sector's height bounds. Only if // that fails continue to revert fully to the last good Y position. pos.room_num = old_pos.room_num; sector = Room_GetSector(old_pos.pos, &pos.room_num); height = Room_GetHeight(sector, old_pos.pos); const int32_t old_ceiling = Room_GetCeiling(sector, old_pos.pos); CLAMP(pos.y, old_ceiling + STEP_L, height - STEP_L); sector = Room_GetSector(pos.pos, &pos.room_num); height = Room_GetHeight(sector, pos.pos); if (height == NO_HEIGHT) { pos.y = old_pos.y; pos.room_num = old_pos.room_num; sector = Room_GetSector(pos.pos, &pos.room_num); height = Room_GetHeight(sector, pos.pos); } } height -= STEP_L; if (pos.y >= height && target->y >= height) { LOS_Check(&g_Camera.target, &pos, false); sector = Room_GetSector(pos.pos, &pos.room_num); height = Room_GetHeight(sector, pos.pos) - STEP_L; } g_Camera.pos = pos; int32_t ceiling = Room_GetCeiling(sector, pos.pos) + STEP_L; if (height < ceiling) { ceiling = (height + ceiling) >> 1; height = ceiling; } Camera_ApplyBounce(); if (g_Camera.pos.y > height) { g_Camera.shift = height - g_Camera.pos.y; } else if (g_Camera.pos.y < ceiling) { g_Camera.shift = ceiling - g_Camera.pos.y; } else { g_Camera.shift = 0; } Camera_UpdateMicPosition(); } static void M_Chase(const ITEM *const item) { g_Camera.target_elevation += item->rot.x; g_Camera.target_elevation = MIN(g_Camera.target_elevation, M_MAX_ELEVATION); g_Camera.target_elevation = MAX(g_Camera.target_elevation, -M_MAX_ELEVATION); const int32_t distance = (g_Camera.target_distance * Math_Cos(g_Camera.target_elevation)) >> W2V_SHIFT; const int16_t angle = g_Camera.target_angle + item->rot.y; g_Camera.target_square = SQUARE(distance); const XYZ_32 offset = { .y = (g_Camera.target_distance * Math_Sin(g_Camera.target_elevation)) >> W2V_SHIFT, .x = -((distance * Math_Sin(angle)) >> W2V_SHIFT), .z = -((distance * Math_Cos(angle)) >> W2V_SHIFT), }; GAME_VECTOR target = { .x = g_Camera.target.x + offset.x, .y = g_Camera.target.y + offset.y, .z = g_Camera.target.z + offset.z, .room_num = g_Camera.pos.room_num, }; const M_SETTINGS *const settings = M_GetSettings(); const int16_t speed = settings->override_chase_speed || g_Camera.fixed_camera ? g_Camera.speed : settings->chase_speed; M_SmartShift(&target, M_Shift); M_Move(&target, speed); } static void M_Combat(const ITEM *const item) { g_Camera.target.z = item->pos.z; g_Camera.target.x = item->pos.x; g_Camera.target_distance = M_COMBAT_DISTANCE; const LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (lara_info->target != nullptr) { g_Camera.target_angle = lara_info->target_angles[0] + item->rot.y; g_Camera.target_elevation = lara_info->target_angles[1] + item->rot.x; } else { g_Camera.target_angle = lara_info->torso_rot.y + lara_info->head_rot.y + item->rot.y; g_Camera.target_elevation = lara_info->torso_rot.x + lara_info->head_rot.x + item->rot.x; } const int32_t distance = (M_COMBAT_DISTANCE * Math_Cos(g_Camera.target_elevation)) >> W2V_SHIFT; const XYZ_32 offset = { .y = +((g_Camera.target_distance * Math_Sin(g_Camera.target_elevation)) >> W2V_SHIFT), .x = -((distance * Math_Sin(g_Camera.target_angle)) >> W2V_SHIFT), .z = -((distance * Math_Cos(g_Camera.target_angle)) >> W2V_SHIFT), }; GAME_VECTOR target = { .x = g_Camera.target.x + offset.x, .y = g_Camera.target.y + offset.y, .z = g_Camera.target.z + offset.z, .room_num = g_Camera.pos.room_num, }; if (lara_info->water_status == LWS_UNDERWATER) { const ITEM *const lara_item = Lara_GetItem(); const int32_t water_height = lara_info->water_surface_dist + lara_item->pos.y; if (g_Camera.target.y > water_height && water_height > target.y) { target.y = lara_info->water_surface_dist + lara_item->pos.y; target.z = g_Camera.target.z + (water_height - g_Camera.target.y) * (target.z - g_Camera.target.z) / (target.y - g_Camera.target.y); target.x = g_Camera.target.x + (water_height - g_Camera.target.y) * (target.x - g_Camera.target.x) / (target.y - g_Camera.target.y); } } M_SmartShift(&target, M_Shift); M_Move(&target, g_Camera.speed); } static void M_Fixed(void) { const OBJECT_VECTOR *const fixed = Camera_GetFixedObject(g_Camera.num); GAME_VECTOR target = { .x = fixed->x, .y = fixed->y, .z = fixed->z, .room_num = fixed->data, }; const M_SETTINGS *const settings = M_GetSettings(); if (settings->use_fixed_los_check && !LOS_Check(&g_Camera.target, &target, false)) { M_ShiftClamp(&target, STEP_L); } g_Camera.fixed_camera = true; M_Move(&target, g_Camera.speed); if (g_Camera.timer != 0) { g_Camera.timer--; if (g_Camera.timer == 0) { g_Camera.timer = -1; } } } static void M_Look(const ITEM *const item) { const XYZ_32 old = { .x = g_Camera.target.x, .y = g_Camera.target.y, .z = g_Camera.target.z, }; g_Camera.target.z = item->pos.z; g_Camera.target.x = item->pos.x; g_Camera.target_distance = M_LOOK_DISTANCE; const LARA_INFO *const lara_info = Lara_GetLaraInfo(); g_Camera.target_angle = item->rot.y + lara_info->torso_rot.y + lara_info->head_rot.y; g_Camera.target_elevation = item->rot.x + lara_info->torso_rot.x + lara_info->head_rot.x; const int32_t distance = (M_LOOK_DISTANCE * Math_Cos(g_Camera.target_elevation)) >> W2V_SHIFT; g_Camera.shift = (-STEP_L * 2 * Math_Sin(g_Camera.target_elevation)) >> W2V_SHIFT; g_Camera.target.z += (g_Camera.shift * Math_Cos(item->rot.y)) >> W2V_SHIFT; g_Camera.target.x += (g_Camera.shift * Math_Sin(item->rot.y)) >> W2V_SHIFT; if (!M_IsGoodPosition( g_Camera.target.x, g_Camera.target.y, g_Camera.target.z, g_Camera.target.room_num)) { g_Camera.target.x = item->pos.x; g_Camera.target.z = item->pos.z; } g_Camera.target.y += M_ShiftClamp(&g_Camera.target, M_LOOK_CLAMP); const XYZ_32 offset = { .y = +((g_Camera.target_distance * Math_Sin(g_Camera.target_elevation)) >> W2V_SHIFT), .x = -((distance * Math_Sin(g_Camera.target_angle)) >> W2V_SHIFT), .z = -((distance * Math_Cos(g_Camera.target_angle)) >> W2V_SHIFT), }; GAME_VECTOR target = { .x = g_Camera.target.x + offset.x, .y = g_Camera.target.y + offset.y, .z = g_Camera.target.z + offset.z, .room_num = g_Camera.pos.room_num, }; M_SmartShift(&target, M_Clip); g_Camera.target.z = old.z + (g_Camera.target.z - old.z) / g_Camera.speed; g_Camera.target.x = old.x + (g_Camera.target.x - old.x) / g_Camera.speed; M_Move(&target, g_Camera.speed); g_Camera.debuff = 5; } static void M_ClampResult(void) { XYZ_32 *const pos = &g_Camera.interp.result.pos; const int32_t shift = g_Camera.interp.result.shift; const ROOM *const room = Room_Get(g_Camera.interp.room_num); const SECTOR *sector = Room_GetWorldSector(room, pos->x, pos->z); if (sector->box != NO_BOX) { goto finish; } sector = Room_GetWorldSector(room, g_Camera.pos.x, g_Camera.pos.z); if (sector->box == NO_BOX) { goto finish; } const BOX_INFO *const box = Box_GetBox(sector->box); CLAMP(pos->x, box->top, box->bottom); CLAMP(pos->z, box->left, box->right); finish: const int32_t floor = Room_GetHeightEx(sector, *pos, true, NO_ITEM); const int32_t ceiling = Room_GetCeilingEx(sector, *pos, true); if (floor != NO_HEIGHT && ceiling != NO_HEIGHT) { CLAMP(pos->y, ceiling - shift, floor - shift); } Room_GetSector( (XYZ_32) { pos->x, pos->y + shift, pos->z }, &g_Camera.interp.room_num); } static void M_Reset(void) { const ITEM *const lara_item = Lara_GetItem(); ASSERT(lara_item != nullptr); g_Camera.shift = lara_item->pos.y - WALL_L; g_Camera.target.x = lara_item->pos.x; g_Camera.target.y = g_Camera.shift; g_Camera.target.z = lara_item->pos.z; g_Camera.target.room_num = lara_item->room_num; g_Camera.pos.x = g_Camera.target.x; g_Camera.pos.y = g_Camera.target.y; g_Camera.pos.z = g_Camera.target.z - 100; g_Camera.pos.room_num = g_Camera.target.room_num; } static void M_Update( const ITEM *const item, const bool fixed_camera, int32_t target_y) { const BOUNDS_16 *bounds = Item_GetBoundsAccurate(item); if (g_Camera.item != nullptr && !fixed_camera) { bounds = Item_GetBoundsAccurate(g_Camera.item); const int32_t dx = g_Camera.item->pos.x - item->pos.x; const int32_t dz = g_Camera.item->pos.z - item->pos.z; const int32_t shift = Math_Sqrt(SQUARE(dx) + SQUARE(dz)); int16_t angle = Math_Atan(dz, dx) - item->rot.y; int16_t tilt = Math_Atan( shift, target_y - (bounds->min.y + bounds->max.y) / 2 - g_Camera.item->pos.y); angle >>= 1; tilt >>= 1; if (angle > CAMERA_MIN_HEAD_ROTATION && angle < CAMERA_MAX_HEAD_ROTATION && tilt > CAMERA_MIN_HEAD_TILT && tilt < CAMERA_MAX_HEAD_TILT) { LARA_INFO *const lara_info = Lara_GetLaraInfo(); int16_t change = angle - lara_info->head_rot.y; if (change > CAMERA_HEAD_TURN) { lara_info->head_rot.y += CAMERA_HEAD_TURN; } else if (change < -CAMERA_HEAD_TURN) { lara_info->head_rot.y -= CAMERA_HEAD_TURN; } else { lara_info->head_rot.y = angle; } change = tilt - lara_info->head_rot.x; if (change > CAMERA_HEAD_TURN) { lara_info->head_rot.x += CAMERA_HEAD_TURN; } else if (change < -CAMERA_HEAD_TURN) { lara_info->head_rot.x -= CAMERA_HEAD_TURN; } else { lara_info->head_rot.x += change; } lara_info->torso_rot.x = lara_info->head_rot.x; lara_info->torso_rot.y = lara_info->head_rot.y; g_Camera.type = CAM_LOOK; g_Camera.item->looked_at = true; } } if (g_Camera.type == CAM_LOOK || g_Camera.type == CAM_COMBAT) { target_y -= STEP_L; g_Camera.target.room_num = item->room_num; if (g_Camera.fixed_camera) { g_Camera.target.y = target_y; g_Camera.speed = 1; } else { g_Camera.target.y += (target_y - g_Camera.target.y) >> 2; g_Camera.speed = g_Camera.type == CAM_LOOK ? CAMERA_LOOK_SPEED : CAMERA_COMBAT_SPEED; } g_Camera.fixed_camera = false; if (g_Camera.type == CAM_LOOK) { M_Look(item); } else { M_Combat(item); } } else { if (fixed_camera) { g_Camera.debuff = 0; } if (g_Camera.debuff > 0) { const XYZ_32 old = g_Camera.target.pos; g_Camera.target.x = (item->pos.x + old.x) / 2; g_Camera.target.z = (item->pos.z + old.z) / 2; g_Camera.debuff--; } else { g_Camera.target.x = item->pos.x; g_Camera.target.z = item->pos.z; } if (g_Camera.flags == CF_FOLLOW_CENTRE) { const int32_t shift = (bounds->min.z + bounds->max.z) / 2; g_Camera.target.z += (shift * Math_Cos(item->rot.y)) >> W2V_SHIFT; g_Camera.target.x += (shift * Math_Sin(item->rot.y)) >> W2V_SHIFT; } g_Camera.target.room_num = item->room_num; if (g_Camera.fixed_camera != fixed_camera) { g_Camera.target.y = target_y; g_Camera.fixed_camera = true; g_Camera.speed = 1; } else { g_Camera.fixed_camera = false; g_Camera.target.y += (target_y - g_Camera.target.y) / 4; } const SECTOR *const sector = Room_GetSector( (XYZ_32) { g_Camera.target.x, target_y, g_Camera.target.z }, &g_Camera.target.room_num); const int32_t height = Room_GetHeight(sector, g_Camera.target.pos); if (g_Camera.target.y > height) { Camera_SetChunky(false); } if (g_Camera.type == CAM_CHASE || g_Camera.flags == CF_CHASE_OBJECT) { M_Chase(item); } else { M_Fixed(); } } } static const CAMERA_STRATEGY m_Strategy = { .get_chase_speed_func = M_GetChaseSpeed, .get_look_settings_func = M_GetLookSettingsFunc, .clamp_result_func = M_ClampResult, .reset_func = M_Reset, .update_func = M_Update, }; REGISTER_CAMERA(CAMERA_MODE_TR1, m_Strategy) REGISTER_CAMERA(CAMERA_MODE_TR2, m_Strategy) ================================================ FILE: src/trx/game/camera/cinematic.c ================================================ #include #include #include #include #include #include #include static CINE_FRAME *m_CineFrames = nullptr; static CINE_DATA m_CineData = {}; static void M_UpdateCutscene(const XYZ_32 base_pos, const int16_t angle) { const CINE_FRAME *const frame = Camera_GetCurrentCineFrame(); const int32_t c = Math_Cos(angle); const int32_t s = Math_Sin(angle); #define SHIFT(prop, axis1, op, axis2) \ (((frame->prop.shift.axis1 * c) op(frame->prop.shift.axis2 * s)) \ >> W2V_SHIFT) const XYZ_32 camera_target = { .x = base_pos.x + SHIFT(target, x, +, z), .y = base_pos.y + frame->target.shift.y, .z = base_pos.z + SHIFT(target, z, -, x), }; const XYZ_32 camera_pos = { .x = base_pos.x + SHIFT(camera, x, +, z), .y = base_pos.y + frame->camera.shift.y, .z = base_pos.z + SHIFT(camera, z, -, x), }; #undef SHIFT const int16_t room_num = Room_GetIndexFromPos(camera_pos); if (room_num != NO_ROOM) { g_Camera.pos.room_num = room_num; } g_Camera.target.pos = camera_target; g_Camera.pos.pos = camera_pos; g_Camera.roll = frame->roll; g_Camera.shift = 0; Viewport_AlterFOV(frame->fov, FOV_MODE_CUTSCENE); } void Camera_InitialiseCineFrames(const int32_t num_frames) { m_CineData.frame_count = num_frames; m_CineData.frame_idx = 0; m_CineFrames = num_frames == 0 ? nullptr : GameBuf_Alloc(num_frames * sizeof(CINE_FRAME), GBUF_CINEMATIC_FRAMES); } CINE_FRAME *Camera_GetCineFrame(const int32_t frame_idx) { if (m_CineFrames == nullptr) { return nullptr; } return &m_CineFrames[frame_idx]; } CINE_FRAME *Camera_GetCurrentCineFrame(void) { return Camera_GetCineFrame(m_CineData.frame_idx); } CINE_DATA *Camera_GetCineData(void) { return &m_CineData; } void Camera_InvokeCinematic( const ITEM *const item, const int32_t frame_idx, const int16_t extra_y_rot) { g_Camera.type = CAM_CINEMATIC; m_CineData.frame_idx = frame_idx; m_CineData.position.pos = item->pos; m_CineData.position.rot = item->rot; m_CineData.position.rot.y += extra_y_rot; } void Camera_LoadCutsceneFrame(void) { CINE_DATA *const cine_data = Camera_GetCineData(); if (cine_data->frame_count == 0) { return; } cine_data->frame_idx++; if (cine_data->frame_idx >= cine_data->frame_count) { cine_data->frame_idx = cine_data->frame_count - 1; } M_UpdateCutscene(cine_data->position.pos, cine_data->position.rot.y); Camera_UpdateMicPosition(); } void Camera_UpdateCutscene(void) { const CINE_DATA *const cine_data = Camera_GetCineData(); if (cine_data->frame_count == 0) { return; } const ITEM *const lara_item = Lara_GetItem(); M_UpdateCutscene(lara_item->pos, g_Camera.target_angle); Camera_EnsureEnvironment(); } ================================================ FILE: src/trx/game/camera/cinematic.h ================================================ #pragma once #include #include void Camera_InitialiseCineFrames(int32_t num_frames); CINE_FRAME *Camera_GetCineFrame(int32_t frame_idx); CINE_FRAME *Camera_GetCurrentCineFrame(void); CINE_DATA *Camera_GetCineData(void); void Camera_InvokeCinematic( const ITEM *item, int32_t frame_idx, int16_t extra_y_rot); void Camera_LoadCutsceneFrame(void); void Camera_UpdateCutscene(void); ================================================ FILE: src/trx/game/camera/common.c ================================================ #include #include #include #include #include #include #include #include #define M_CHASE_ELEVATION (WALL_L * 3 / 2) // = 1536 static CAMERA_STRATEGY m_Strategies[CAMERA_MODE_NUMBER_OF] = {}; // Camera speed option ranges from 1-10, so index 0 is unused. static const double m_ManualCameraMultiplier[11] = { 1.0, .5, .625, .75, .875, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0, }; static bool m_IsChunky = false; static bool m_IsInitialised = false; static void M_OffsetAdditionalAngle(const int16_t delta) { g_Camera.additional_angle += delta; } static void M_OffsetAdditionalElevation(const int16_t delta) { // Do not allow elevation to overflow. int32_t new_elevation = g_Camera.additional_elevation + delta; CLAMP(new_elevation, INT16_MIN, INT16_MAX); g_Camera.additional_elevation = new_elevation; } static void M_OffsetReset(void) { g_Camera.additional_angle = 0; g_Camera.additional_elevation = 0; } static const CAMERA_STRATEGY *M_GetStrategy(void) { return &m_Strategies[g_Config.visuals.camera_mode]; } const CAMERA_LOOK_SETTINGS *Camera_GetLookSettings(const bool on_surface) { return M_GetStrategy()->get_look_settings_func(on_surface); } void Camera_RegisterStrategy( const CAMERA_MODE mode, const CAMERA_STRATEGY strategy) { m_Strategies[mode] = strategy; } bool Camera_IsChunky(void) { return m_IsChunky; } void Camera_SetChunky(const bool is_chunky) { m_IsChunky = is_chunky; } void Camera_Initialise(void) { m_IsInitialised = false; Matrix_ResetStack(); g_Camera.last = NO_CAMERA; g_Camera.underwater = false; Camera_ResetPosition(); Camera_Update(); m_IsInitialised = true; } void Camera_ResetPosition(void) { const CAMERA_STRATEGY *const strategy = M_GetStrategy(); strategy->reset_func(); g_Camera.roll = 0; g_Camera.target_distance = CAMERA_DEFAULT_DISTANCE; g_Camera.item = nullptr; g_Camera.speed = 1; g_Camera.flags = CF_NORMAL; g_Camera.bounce = 0; g_Camera.num = NO_CAMERA; g_Camera.fixed_camera = false; g_Camera.additional_angle = 0; g_Camera.additional_elevation = 0; const LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (!lara_info->extra_anim) { g_Camera.type = CAM_CHASE; } } void Camera_Reset(void) { g_Camera.mic_pos.room_num = NO_ROOM; g_Camera.pos.room_num = NO_ROOM; } void Camera_ApplyBounce(void) { if (g_Camera.bounce > 0) { g_Camera.pos.y += g_Camera.bounce; g_Camera.target.y += g_Camera.bounce; g_Camera.bounce = 0; } else if (g_Camera.bounce < 0) { const XYZ_32 shake = { .x = g_Camera.bounce * (Random_GetControl() - 0x4000) / 0x7FFF, .y = g_Camera.bounce * (Random_GetControl() - 0x4000) / 0x7FFF, .z = g_Camera.bounce * (Random_GetControl() - 0x4000) / 0x7FFF, }; g_Camera.pos.x += shake.x; g_Camera.pos.y += shake.y; g_Camera.pos.z += shake.z; g_Camera.target.y += shake.x; g_Camera.target.y += shake.y; g_Camera.target.z += shake.z; g_Camera.bounce += 5; } } void Camera_ClampInterpResult(void) { if (g_Camera.type == CAM_PHOTO_MODE) { Room_GetSector( (XYZ_32) { g_Camera.interp.result.pos.x, g_Camera.interp.result.pos.y + g_Camera.interp.result.shift, g_Camera.interp.result.pos.z, }, &g_Camera.interp.room_num); return; } const CAMERA_STRATEGY *const strategy = M_GetStrategy(); strategy->clamp_result_func(); } void Camera_Update(void) { if (g_Camera.type == CAM_PHOTO_MODE) { Camera_PhotoMode_Update(); Camera_EnsureEnvironment(); return; } if (g_Camera.type == CAM_CINEMATIC) { Camera_LoadCutsceneFrame(); Camera_EnsureEnvironment(); return; } if (g_Camera.flags != CF_NO_CHUNKY) { Camera_SetChunky(true); } const bool fixed_camera = g_Camera.item != nullptr && (g_Camera.type == CAM_FIXED || g_Camera.type == CAM_HEAVY); const ITEM *const item = fixed_camera ? g_Camera.item : Lara_GetItem(); const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item); int32_t y = item->pos.y; if (fixed_camera) { y += (bounds->min.y + bounds->max.y) / 2; } else { y += bounds->max.y + (((int32_t)(bounds->min.y - bounds->max.y)) * 3 >> 2); } const CAMERA_STRATEGY *const strategy = M_GetStrategy(); strategy->update_func(item, fixed_camera, y); g_Camera.last = g_Camera.num; g_Camera.fixed_camera = fixed_camera; switch (g_Camera.type) { case CAM_LOOK: case CAM_CINEMATIC: case CAM_COMBAT: case CAM_FIXED: g_Camera.additional_angle = 0; g_Camera.additional_elevation = 0; break; default: break; } if (g_Camera.type != CAM_HEAVY || g_Camera.timer == -1) { g_Camera.type = CAM_CHASE; g_Camera.num = NO_CAMERA; g_Camera.last_item = g_Camera.item; g_Camera.item = nullptr; g_Camera.target_angle = g_Camera.additional_angle; g_Camera.target_elevation = g_Camera.additional_elevation; g_Camera.target_distance = M_CHASE_ELEVATION; g_Camera.flags = CF_NORMAL; if (g_Config.visuals.camera_mode != CAMERA_MODE_TR1) { g_Camera.speed = strategy->get_chase_speed_func(); } } Camera_SetChunky(false); if (m_IsInitialised) { Camera_EnsureEnvironment(); } } void Camera_MoveManual(void) { if (g_Input.camera_reset) { M_OffsetReset(); } if (!g_Config.gameplay.enable_manual_camera) { return; } const int16_t camera_delta = (const int32_t)(DEG_90 / LOGIC_FPS) * (double)m_ManualCameraMultiplier[g_Config.gameplay.camera_speed]; if (g_Input.camera_left) { M_OffsetAdditionalAngle(camera_delta); } else if (g_Input.camera_right) { M_OffsetAdditionalAngle(-camera_delta); } if (g_Input.camera_forward) { M_OffsetAdditionalElevation(-camera_delta); } else if (g_Input.camera_back) { M_OffsetAdditionalElevation(camera_delta); } } void Camera_Apply(void) { Matrix_LookAt( g_Camera.interp.result.pos.x, g_Camera.interp.result.pos.y + g_Camera.interp.result.shift, g_Camera.interp.result.pos.z, g_Camera.interp.result.target.x, g_Camera.interp.result.target.y, g_Camera.interp.result.target.z, g_Camera.roll); } ================================================ FILE: src/trx/game/camera/common.h ================================================ #pragma once #include #include void Camera_Update(void); void Camera_MoveManual(void); void Camera_Apply(void); bool Camera_IsChunky(void); void Camera_SetChunky(bool is_chunky); void Camera_Initialise(void); void Camera_ResetPosition(void); void Camera_Reset(void); void Camera_ApplyBounce(void); void Camera_ClampInterpResult(void); const CAMERA_LOOK_SETTINGS *Camera_GetLookSettings(bool on_surface); bool Camera_LOSCheck(GAME_VECTOR *start, GAME_VECTOR *target, int32_t shift); bool Camera_Collide(GAME_VECTOR *ideal, int32_t shift, bool y_first); void Camera_RegisterStrategy(CAMERA_MODE mode, CAMERA_STRATEGY strategy); #define REGISTER_CAMERA(mode, strategy) \ __attribute__((constructor)) static void M_RegisterCamera##mode(void) \ { \ Camera_RegisterStrategy(mode, strategy); \ } ================================================ FILE: src/trx/game/camera/const.h ================================================ #pragma once #include // clang-format off #define NO_CAMERA (-1) #define CAMERA_DEFAULT_DISTANCE (WALL_L * 3 / 2) // = 1536 #define CAMERA_MAX_HEAD_ROTATION (50 * DEG_1) // = 9100 #define CAMERA_MIN_HEAD_ROTATION (-CAMERA_MAX_HEAD_ROTATION) // = -9100 #define CAMERA_MAX_HEAD_TILT (85 * DEG_1) // = 15470 #define CAMERA_MIN_HEAD_TILT (-CAMERA_MAX_HEAD_TILT) // = -15470 #define CAMERA_HEAD_TURN (4 * DEG_1) // = 728 #define CAMERA_COMBAT_SPEED 8 #define CAMERA_LOOK_SPEED 4 // clang-format on ================================================ FILE: src/trx/game/camera/enum.h ================================================ #pragma once typedef enum { CAM_CHASE = 0, CAM_FIXED = 1, CAM_LOOK = 2, CAM_COMBAT = 3, CAM_CINEMATIC = 4, CAM_HEAVY = 5, CAM_PHOTO_MODE = 6, } CAMERA_TYPE; typedef enum { CF_NORMAL = 0, CF_FOLLOW_CENTRE = 1, CF_NO_CHUNKY = 2, CF_CHASE_OBJECT = 3, } CAMERA_FLAGS; ================================================ FILE: src/trx/game/camera/environment.c ================================================ #include #include #include #include #include #include #include #include #include typedef enum { TARGET_UNKNOWN, TARGET_INVALID, TARGET_VALID, } M_TARGET_STATUS; static void M_AdjustMusicVolume(const bool is_underwater) { if (!Game_IsPlaying()) { return; } const bool is_ambient = Music_GetCurrentPlayingTrack() == Music_GetCurrentLoopedTrack(); const bool is_cutscene = GF_GetCurrentLevel()->type == GFL_CUTSCENE; const double base_volume = is_cutscene ? g_Config.audio.cutscene_volume : is_ambient ? g_Config.audio.ambient_volume : g_Config.audio.music_volume; const double multiplier = !is_underwater || is_cutscene ? 1.0 : is_ambient ? g_Config.audio.underwater_ambient_volume : g_Config.audio.underwater_music_volume; Music_SetVolume(base_volume * multiplier); } static inline M_TARGET_STATUS M_HandleCameraTrigger( const TRIGGER_CMD *const cmd) { const TRIGGER_CAMERA_DATA *const cam_data = (TRIGGER_CAMERA_DATA *)cmd->parameter; if (cam_data->camera_num != g_Camera.last) { return TARGET_INVALID; } g_Camera.num = cam_data->camera_num; const bool is_look_or_combat = g_Camera.type == CAM_LOOK || g_Camera.type == CAM_COMBAT; const bool is_locked = Camera_IsLocked(g_Camera.num); if (g_Camera.timer < 0 || (is_look_or_combat && !is_locked)) { g_Camera.timer = -1; return TARGET_INVALID; } g_Camera.type = CAM_FIXED; if (g_Config.visuals.enable_glide_cameras && cam_data->glide != 0) { g_Camera.speed = cam_data->glide + 1; } return TARGET_VALID; } static inline void M_HandleTargetTrigger(const TRIGGER_CMD *const cmd) { const bool is_look_or_combat = g_Camera.type == CAM_LOOK || g_Camera.type == CAM_COMBAT; const bool is_locked = Camera_IsLocked(g_Camera.num); if (!is_look_or_combat || is_locked) { g_Camera.item = Item_Get((int16_t)(intptr_t)cmd->parameter); } } static inline void M_ValidateTriggerTarget(const M_TARGET_STATUS status) { if (g_Camera.item == nullptr) { return; } const bool is_new_item = g_Camera.item != g_Camera.last_item; const bool item_was_looked_at = g_Camera.item->looked_at; const bool should_clear = (status == TARGET_INVALID) || (status == TARGET_UNKNOWN && item_was_looked_at && is_new_item); if (should_clear) { g_Camera.item = nullptr; } } void Camera_RefreshFromTrigger(const TRIGGER *const trigger) { M_TARGET_STATUS status = TARGET_UNKNOWN; for (const TRIGGER_CMD *cmd = trigger->command; cmd != nullptr; cmd = cmd->next_cmd) { if (cmd->type == TO_CAMERA) { status = M_HandleCameraTrigger(cmd); } else if (cmd->type == TO_TARGET) { M_HandleTargetTrigger(cmd); } } M_ValidateTriggerTarget(status); if (g_Config.visuals.camera_mode != CAMERA_MODE_TR1 && status != TARGET_UNKNOWN && g_Camera.num == -1 && g_Camera.timer > 0) { g_Camera.timer = -1; } } void Camera_EnsureEnvironment(void) { if (g_Camera.mic_pos.room_num != NO_ROOM) { const ROOM *const room = Room_Get(g_Camera.mic_pos.room_num); if (room->flags.underwater) { M_AdjustMusicVolume(true); Sound_Effect(SFX_UNDERWATER, nullptr, SPM_ALWAYS); } else { M_AdjustMusicVolume(false); Sound_StopEffect(SFX_UNDERWATER); } } if (g_Camera.pos.room_num != NO_ROOM) { const ROOM *const room = Room_Get(g_Camera.pos.room_num); g_Camera.underwater = room->flags.underwater; } uint8_t reverb_type = 0; if (g_Camera.mic_pos.room_num != NO_ROOM) { const ROOM *const room = Room_Get(g_Camera.mic_pos.room_num); reverb_type = room->reverb_info; } Sound_SetReverbType(reverb_type); } void Camera_UpdateMicPosition(void) { if (g_Config.audio.enable_lara_mic) { const LARA_INFO *const lara_info = Lara_GetLaraInfo(); const ITEM *const lara_item = Lara_GetItem(); g_Camera.actual_angle = lara_info->torso_rot.y + lara_info->head_rot.y + lara_item->rot.y; g_Camera.mic_pos.room_num = lara_item->room_num; XYZ_32 pos = { 0, 16, 0 }; if (lara_info->water_status == LWS_SURFACE) { pos.y = -36; } if (lara_info->water_surface_dist != -NO_HEIGHT && Lara_GetMeshPos(LM_HEAD, &pos)) { g_Camera.mic_pos.pos = pos; Room_GetSector(pos, &g_Camera.mic_pos.room_num); } else { g_Camera.mic_pos.pos = lara_item->pos; } } else { g_Camera.actual_angle = Math_Atan( g_Camera.target.z - g_Camera.pos.z, g_Camera.target.x - g_Camera.pos.x); g_Camera.mic_pos.pos.x = g_Camera.pos.x + ((g_PhdPersp * Math_Sin(g_Camera.actual_angle)) >> W2V_SHIFT); g_Camera.mic_pos.pos.z = g_Camera.pos.z + ((g_PhdPersp * Math_Cos(g_Camera.actual_angle)) >> W2V_SHIFT); g_Camera.mic_pos.pos.y = g_Camera.pos.y; g_Camera.mic_pos.room_num = g_Camera.pos.room_num; } } ================================================ FILE: src/trx/game/camera/environment.h ================================================ #pragma once #include void Camera_RefreshFromTrigger(const TRIGGER *trigger); void Camera_EnsureEnvironment(void); void Camera_UpdateMicPosition(void); ================================================ FILE: src/trx/game/camera/fixed.c ================================================ #include #include #include #define M_LOCKED_CAMERA 1 static int32_t m_FixedObjectCount = 0; static OBJECT_VECTOR *m_FixedObjects = nullptr; void Camera_InitialiseFixedObjects(const int32_t num_objects) { m_FixedObjectCount = num_objects + 1; m_FixedObjects = GameBuf_Alloc(m_FixedObjectCount * sizeof(OBJECT_VECTOR), GBUF_CAMERAS); } int32_t Camera_GetFixedObjectCount(void) { return m_FixedObjectCount - 1; } int32_t Camera_GetDynamicFixedObjectIdx(void) { return m_FixedObjectCount - 1; } void Camera_UpdateDynamicFixedObject(const XYZ_32 pos, const int16_t room_num) { const int32_t idx = Camera_GetDynamicFixedObjectIdx(); OBJECT_VECTOR *const camera = Camera_GetFixedObject(idx); camera->pos = pos; camera->data = room_num; } OBJECT_VECTOR *Camera_GetFixedObject(const int32_t object_idx) { if (m_FixedObjects == nullptr) { return nullptr; } return &m_FixedObjects[object_idx]; } bool Camera_IsLocked(const int32_t camera_num) { if (camera_num == NO_CAMERA) { return false; } const OBJECT_VECTOR *const fixed_camera = Camera_GetFixedObject(camera_num); return (fixed_camera->flags & M_LOCKED_CAMERA) != 0; } ================================================ FILE: src/trx/game/camera/fixed.h ================================================ #pragma once #include void Camera_InitialiseFixedObjects(int32_t num_objects); int32_t Camera_GetFixedObjectCount(void); int32_t Camera_GetDynamicFixedObjectIdx(void); void Camera_UpdateDynamicFixedObject(XYZ_32 pos, int16_t room_num); OBJECT_VECTOR *Camera_GetFixedObject(int32_t object_idx); bool Camera_IsLocked(int32_t camera_num); ================================================ FILE: src/trx/game/camera/los_camera.c ================================================ #include #include #include #include #include // clang-format off #define M_LOS_STEPS 8 #define M_MAX_SNAPS 8 #define M_SNAP_DELTA (STEP_L * 3) // = 768 #define M_DEFAULT_ELEVATION (-10 * DEG_1) // = -1820 #define M_MAX_ELEVATION (85 * DEG_1) // = 15470 #define M_CHASE_SHIFT (STEP_L * 3 / 2) // = 384 #define M_LOOK_SHIFT (STEP_L * 7 / 8) // = 224 #define M_CHASE_SPEED 10 #define M_MAX_LOOK_TILT (55 * DEG_1) // = 10010 #define M_MIN_LOOK_TILT (-75 * DEG_1) // = -13650 #define M_MAX_LOOK_ROTATION (80 * DEG_1) // = 14560 #define M_MIN_LOOK_ROTATION -M_MAX_LOOK_ROTATION // = -14560 // clang-format on typedef struct { struct { int16_t current_anim_state; int16_t goal_anim_state; XYZ_32 pos; XYZ_16 rot; XYZ_16 head_rot; XYZ_16 torso_rot; } lara; CAMERA_TYPE cam_type; int16_t additional_angle; int16_t additional_elevation; } M_STATE; typedef struct { GAME_VECTOR pos; GAME_VECTOR target; } M_IDEAL; static M_STATE m_LastState = {}; static M_IDEAL m_LastIdeal = {}; static M_IDEAL m_LastLookIdeal = {}; static int32_t m_Snaps = 0; static CAMERA_LOOK_SETTINGS m_LookSettings = { // clang-format off .head_turn = +2 * DEG_1, .max_head_rotation = +44 * DEG_1, .min_head_rotation = -44 * DEG_1, .max_head_tilt = +30 * DEG_1, .min_head_tilt = -35 * DEG_1, .torso_head_rot_y = 1.0f, .torso_head_rot_x = 1.0f, // clang-format on }; static int16_t M_GetChaseSpeed(void) { return M_CHASE_SPEED; } static const CAMERA_LOOK_SETTINGS *M_GetLookSettingsFunc(const bool on_surface) { return &m_LookSettings; } static bool M_LOS( GAME_VECTOR *const start, GAME_VECTOR *const target, const int32_t shift) { const XYZ_32 delta = { .x = (target->x - start->x) >> 3, .y = (target->y - start->y) >> 3, .z = (target->z - start->z) >> 3, }; XYZ_32 pos = start->pos; int16_t room_num = start->room_num; bool valid_space = false; bool clipped = false; int32_t i = 0; for (; i < M_LOS_STEPS; i++) { int16_t next_room_num = room_num; const SECTOR *const sector = Room_GetSector(pos, &next_room_num); if (Room_Get(next_room_num)->flags.swamp) { clipped = true; break; } const int32_t height = Room_GetHeightEx(sector, pos, true, NO_ITEM); const int32_t ceiling = Room_GetCeilingEx(sector, pos, true); if (height == NO_HEIGHT || ceiling == NO_HEIGHT || ceiling >= height) { if (!valid_space) { pos.x += delta.x; pos.y += delta.y; pos.z += delta.z; continue; } clipped = true; break; } if (pos.y > height) { const int32_t height_diff = pos.y - height; if (height_diff < shift) { pos.y = height; } else { clipped = true; break; } } if (pos.y < ceiling) { const int32_t ceiling_diff = ceiling - pos.y; if (ceiling_diff < shift) { pos.y = ceiling; } else { clipped = true; break; } } valid_space = true; room_num = next_room_num; pos.x += delta.x; pos.y += delta.y; pos.z += delta.z; } if (i != 0) { pos.x -= delta.x; pos.y -= delta.y; pos.z -= delta.z; } Room_GetSector(pos, &room_num); target->pos = pos; target->room_num = room_num; return !clipped; } static inline void M_ClampY(int16_t room_num, XYZ_32 *const pos) { const SECTOR *const sector = Room_GetSector(*pos, &room_num); const int32_t height = Room_GetHeightEx(sector, *pos, true, NO_ITEM); const int32_t ceiling = Room_GetCeilingEx(sector, *pos, true); if (ceiling < height && ceiling != NO_HEIGHT && height != NO_HEIGHT) { if (ceiling > pos->y - 255 && height < pos->y + 255) { pos->y = (ceiling + height) >> 1; } else if (height < pos->y + 255) { pos->y = height - 255; } else if (ceiling > pos->y - 255) { pos->y = ceiling + 255; } } } static bool M_Collide( GAME_VECTOR *const ideal, const int32_t shift, const bool y_first) { XYZ_32 pos = ideal->pos; if (y_first) { M_ClampY(ideal->room_num, &pos); } #define L_OUT_OF_BOUNDS \ (height < pos.y || height == NO_HEIGHT || ceiling == NO_HEIGHT \ || ceiling >= height || pos.y < ceiling) // -X clamp int16_t room_num = ideal->room_num; XYZ_32 sample_pos = { .x = pos.x - shift, .y = pos.y, .z = pos.z }; const SECTOR *sector = Room_GetSector(sample_pos, &room_num); int32_t height = Room_GetHeightEx(sector, sample_pos, true, NO_ITEM); int32_t ceiling = Room_GetCeilingEx(sector, sample_pos, true); if (L_OUT_OF_BOUNDS) { pos.x = ROUND_TO_SECTOR(pos.x) + shift; } // -Z clamp room_num = ideal->room_num; sample_pos = (XYZ_32) { .x = pos.x, .y = pos.y, .z = pos.z - shift }; sector = Room_GetSector(sample_pos, &room_num); height = Room_GetHeightEx(sector, sample_pos, true, NO_ITEM); ceiling = Room_GetCeilingEx(sector, sample_pos, true); if (L_OUT_OF_BOUNDS) { pos.z = ROUND_TO_SECTOR(pos.z) + shift; } // +X clamp room_num = ideal->room_num; sample_pos = (XYZ_32) { .x = pos.x + shift, .y = pos.y, .z = pos.z }; sector = Room_GetSector(sample_pos, &room_num); height = Room_GetHeightEx(sector, sample_pos, true, NO_ITEM); ceiling = Room_GetCeilingEx(sector, sample_pos, true); if (L_OUT_OF_BOUNDS) { pos.x = ROUND_TO_SECTOR_END(pos.x) - shift; } // +Z clamp room_num = ideal->room_num; sample_pos = (XYZ_32) { .x = pos.x, .y = pos.y, .z = pos.z + shift }; sector = Room_GetSector(sample_pos, &room_num); height = Room_GetHeightEx(sector, sample_pos, true, NO_ITEM); ceiling = Room_GetCeilingEx(sector, sample_pos, true); if (L_OUT_OF_BOUNDS) { pos.z = ROUND_TO_SECTOR_END(pos.z) - shift; } if (!y_first) { M_ClampY(ideal->room_num, &pos); } room_num = ideal->room_num; sector = Room_GetSector(pos, &room_num); height = Room_GetHeightEx(sector, pos, true, NO_ITEM); ceiling = Room_GetCeilingEx(sector, pos, true); if (L_OUT_OF_BOUNDS) { return true; } #undef L_OUT_OF_BOUNDS Room_GetSector(pos, &ideal->room_num); ideal->pos = pos; return false; } static bool M_UpdateLaraState(void) { const LARA_INFO *const lara = Lara_GetLaraInfo(); const ITEM *const lara_item = Lara_GetItem(); bool same_lara_state = m_LastState.lara.current_anim_state == lara_item->current_anim_state && m_LastState.lara.goal_anim_state == lara_item->goal_anim_state && XYZ_16_AreEquivalent(m_LastState.lara.head_rot, lara->head_rot) && XYZ_32_AreEquivalent(m_LastState.lara.pos, lara_item->pos); bool same_camera_state = m_LastState.cam_type == g_Camera.type; if (g_Camera.type != CAM_LOOK) { same_lara_state &= XYZ_16_AreEquivalent(m_LastState.lara.rot, lara_item->rot) && XYZ_16_AreEquivalent( m_LastState.lara.torso_rot, lara->torso_rot); same_camera_state &= m_LastState.additional_angle == g_Camera.additional_angle && m_LastState.additional_elevation == g_Camera.additional_elevation; } if (same_lara_state && same_camera_state) { return false; } m_LastState.lara.current_anim_state = lara_item->current_anim_state; m_LastState.lara.goal_anim_state = lara_item->goal_anim_state; m_LastState.lara.head_rot = lara->head_rot; m_LastState.lara.pos = lara_item->pos; if (g_Camera.type != CAM_LOOK) { m_LastState.lara.rot = lara_item->rot; m_LastState.lara.torso_rot = lara->torso_rot; m_LastState.additional_angle = g_Camera.additional_angle; m_LastState.additional_elevation = g_Camera.additional_elevation; } return true; } static void M_Move(GAME_VECTOR *const ideal, const int32_t speed) { if (M_UpdateLaraState()) { m_LastIdeal.pos = *ideal; } else { *ideal = m_LastIdeal.pos; } g_Camera.pos.x += (ideal->x - g_Camera.pos.x) / speed; g_Camera.pos.y += (ideal->y - g_Camera.pos.y) / speed; g_Camera.pos.z += (ideal->z - g_Camera.pos.z) / speed; g_Camera.pos.room_num = ideal->room_num; Camera_ApplyBounce(); XYZ_32 pos = g_Camera.pos.pos; int16_t room_num = g_Camera.pos.room_num; const XYZ_32 sample_pos = { pos.x, pos.y + STEP_L, pos.z }; const SECTOR *sector = Room_GetSector(sample_pos, &room_num); const ROOM *const room = Room_Get(room_num); if (room->flags.swamp) { pos.y = room->max_ceiling - STEP_L; Room_GetSector(pos, &g_Camera.pos.room_num); } sector = Room_GetSector(pos, &room_num); int32_t height = Room_GetHeightEx(sector, pos, true, NO_ITEM); int32_t ceiling = Room_GetCeilingEx(sector, pos, true); if (pos.y < ceiling || pos.y > height) { M_LOS(&g_Camera.target, &g_Camera.pos, 0); const XYZ_32 delta = { .x = ABS(g_Camera.pos.x - ideal->x), .y = ABS(g_Camera.pos.y - ideal->y), .z = ABS(g_Camera.pos.z - ideal->z), }; if (delta.x < M_SNAP_DELTA && delta.y < M_SNAP_DELTA && delta.z < M_SNAP_DELTA) { GAME_VECTOR start = *ideal; GAME_VECTOR target = g_Camera.pos; if (!M_LOS(&start, &target, 0)) { m_Snaps++; if (m_Snaps >= M_MAX_SNAPS) { g_Camera.pos = *ideal; m_Snaps = 0; } } } } pos = g_Camera.pos.pos; room_num = g_Camera.pos.room_num; sector = Room_GetSector(pos, &room_num); height = Room_GetHeightEx(sector, pos, true, NO_ITEM); ceiling = Room_GetCeilingEx(sector, pos, true); if (pos.y - 255 < ceiling && pos.y + 255 > height && ceiling < height && ceiling != NO_HEIGHT && height != NO_HEIGHT) { g_Camera.pos.y = (ceiling + height) >> 1; } else if ( pos.y + 255 > height && ceiling < height && ceiling != NO_HEIGHT && height != NO_HEIGHT) { g_Camera.pos.y = height - 255; } else if ( pos.y - 255 < ceiling && ceiling < height && ceiling != NO_HEIGHT && height != NO_HEIGHT) { g_Camera.pos.y = ceiling + 255; } else if ( ceiling >= height || height == NO_HEIGHT || ceiling == NO_HEIGHT) { g_Camera.pos = *ideal; } Room_GetSector(g_Camera.pos.pos, &g_Camera.pos.room_num); m_LastState.cam_type = g_Camera.type; Camera_UpdateMicPosition(); } static GAME_VECTOR M_GetIdeal( const int32_t distance, const int16_t target_rot_y) { int32_t farthest = 0x7FFFFFFF; int32_t farthest_num = 0; GAME_VECTOR temp[2] = {}; GAME_VECTOR ideals[5] = {}; for (int32_t i = 0; i < 5; i++) { ideals[i].y = ((Math_Sin(g_Camera.target_elevation) * g_Camera.target_distance) >> W2V_SHIFT) + g_Camera.target.y; } for (int32_t i = 0; i < 5; i++) { const int16_t angle = i > 0 ? ((i - 1) << W2V_SHIFT) : (g_Camera.target_angle + target_rot_y); ideals[i].x = g_Camera.target.x - ((distance * Math_Sin(angle)) >> W2V_SHIFT); ideals[i].z = g_Camera.target.z - ((distance * Math_Cos(angle)) >> W2V_SHIFT); ideals[i].room_num = g_Camera.target.room_num; if (M_LOS(&g_Camera.target, &ideals[i], 200)) { temp[0] = ideals[i]; temp[1] = g_Camera.pos; if (i == 0 || M_LOS(&temp[0], &temp[1], 0)) { if (i == 0) { farthest_num = 0; break; } const int32_t dx = SQUARE(g_Camera.pos.x - ideals[i].x); const int32_t dz = SQUARE(g_Camera.pos.z - ideals[i].z) + dx; if (dz < farthest) { farthest = dz; farthest_num = i; } } } else if (i == 0) { temp[0] = ideals[i]; temp[1] = g_Camera.pos; const int32_t dx = SQUARE(g_Camera.target.x - ideals[i].x); const int32_t dz = SQUARE(g_Camera.target.z - ideals[i].z) + dx; if (dz > 0x90000) { farthest_num = 0; break; } } } return ideals[farthest_num]; } static void M_Chase(const ITEM *const item) { if (g_Camera.target_elevation == 0) { g_Camera.target_elevation = M_DEFAULT_ELEVATION; } g_Camera.target_elevation += item->rot.x; CLAMP(g_Camera.target_elevation, -M_MAX_ELEVATION, M_MAX_ELEVATION); const int32_t distance = (g_Camera.target_distance * Math_Cos(g_Camera.target_elevation)) >> W2V_SHIFT; int16_t room_num = g_Camera.target.room_num; const SECTOR *sector = Room_GetSector( (XYZ_32) { g_Camera.target.x, g_Camera.target.y + STEP_L, g_Camera.target.z, }, &room_num); const ROOM *const room = Room_Get(room_num); if (room->flags.swamp) { g_Camera.target.y = room->max_ceiling - STEP_L; } XYZ_32 pos = g_Camera.target.pos; sector = Room_GetSector(pos, &g_Camera.target.room_num); int32_t height = Room_GetHeightEx(sector, pos, true, NO_ITEM); int32_t ceiling = Room_GetCeilingEx(sector, pos, true); if (ceiling + 16 > height - 16 && height != NO_HEIGHT && ceiling != NO_HEIGHT) { g_Camera.target.y = (height + ceiling) >> 1; g_Camera.target_elevation = 0; } else if (pos.y > height - 16 && height != NO_HEIGHT) { g_Camera.target.y = height - 16; g_Camera.target_elevation = 0; } else if (pos.y < ceiling + 16 && ceiling != NO_HEIGHT) { g_Camera.target.y = ceiling + 16; g_Camera.target_elevation = 0; } sector = Room_GetSector(g_Camera.target.pos, &g_Camera.target.room_num); pos = g_Camera.target.pos; room_num = g_Camera.target.room_num; sector = Room_GetSector(g_Camera.target.pos, &room_num); height = Room_GetHeightEx(sector, pos, true, NO_ITEM); ceiling = Room_GetCeilingEx(sector, pos, true); if (pos.y < ceiling || pos.y > height || ceiling >= height || height == NO_HEIGHT || ceiling == NO_HEIGHT) { g_Camera.target = m_LastIdeal.target; } GAME_VECTOR ideal = M_GetIdeal(distance, item->rot.y); M_Collide(&ideal, M_CHASE_SHIFT, true); if (m_LastState.cam_type == CAM_FIXED) { g_Camera.speed = 1; } M_Move(&ideal, g_Camera.speed); } static void M_Combat(const ITEM *const item) { g_Camera.target.x = item->pos.x; g_Camera.target.z = item->pos.z; const LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->target != nullptr) { g_Camera.target_angle = lara->target_angles[0] + item->rot.y; g_Camera.target_elevation = lara->target_angles[1] + item->rot.x; } else { g_Camera.target_angle = lara->torso_rot.y + lara->head_rot.y + item->rot.y; g_Camera.target_elevation = lara->head_rot.x + item->rot.x + lara->torso_rot.x - 2730; } int16_t room_num = g_Camera.target.room_num; const SECTOR *sector = Room_GetSector( (XYZ_32) { g_Camera.target.x, g_Camera.target.y + STEP_L, g_Camera.target.z, }, &room_num); const ROOM *const room = Room_Get(room_num); if (room->flags.swamp) { g_Camera.target.y = room->max_ceiling - STEP_L; } XYZ_32 pos = g_Camera.target.pos; sector = Room_GetSector(pos, &g_Camera.target.room_num); int32_t height = Room_GetHeightEx(sector, pos, true, NO_ITEM); int32_t ceiling = Room_GetCeilingEx(sector, pos, true); if (ceiling + 64 > height - 64 && height != NO_HEIGHT && ceiling != NO_HEIGHT) { g_Camera.target.y = (ceiling + height) >> 1; g_Camera.target_elevation = 0; } else if (pos.y > height - 64 && height != NO_HEIGHT) { g_Camera.target.y = height - 64; g_Camera.target_elevation = 0; } else if (pos.y < ceiling + 64 && ceiling != NO_HEIGHT) { g_Camera.target.y = ceiling + 64; g_Camera.target_elevation = 0; } pos = g_Camera.target.pos; Room_GetSector(pos, &g_Camera.target.room_num); room_num = g_Camera.target.room_num; sector = Room_GetSector(pos, &room_num); height = Room_GetHeightEx(sector, pos, true, NO_ITEM); ceiling = Room_GetCeilingEx(sector, pos, true); if (pos.y < ceiling || pos.y > height || ceiling >= height || height == NO_HEIGHT || ceiling == NO_HEIGHT) { g_Camera.target = m_LastIdeal.target; } g_Camera.target_distance = CAMERA_DEFAULT_DISTANCE; const int32_t distance = g_Camera.target_distance * Math_Cos(g_Camera.target_elevation) >> W2V_SHIFT; GAME_VECTOR ideal = M_GetIdeal(distance, 0); M_Collide(&ideal, M_CHASE_SHIFT, true); if (m_LastState.cam_type == CAM_FIXED) { g_Camera.speed = 1; } M_Move(&ideal, g_Camera.speed); } static void M_Fixed(void) { const OBJECT_VECTOR *const fixed = Camera_GetFixedObject(g_Camera.num); GAME_VECTOR target = { .x = fixed->x, .y = fixed->y, .z = fixed->z, .room_num = fixed->data, }; g_Camera.fixed_camera = true; M_Move(&target, g_Camera.speed); if (g_Camera.timer != 0) { g_Camera.timer--; if (g_Camera.timer == 0) { g_Camera.timer = -1; } } } static XYZ_32 M_GetHeadPos(const int32_t x, const int32_t y, const int32_t z) { XYZ_32 pos = { .x = x, .y = y, .z = z, }; if (!Lara_GetMeshPos(LM_HEAD, &pos)) { // If look is held while loading a level, Lara won't have been drawn yet // so ensure a valid fallback position is used. Collide_GetJointAbsPosition(Lara_GetItem(), &pos, LM_HEAD); } return pos; } static void M_Look(const ITEM *const item) { LARA_INFO *const lara = Lara_GetLaraInfo(); const ITEM *const lara_item = Lara_GetItem(); XYZ_16 head_rot = lara->head_rot; XYZ_16 torso_rot = lara->torso_rot; lara->torso_rot.x = 0; lara->torso_rot.y = 0; lara->head_rot.x <<= 1; lara->head_rot.y <<= 1; CLAMP(lara->head_rot.x, M_MIN_LOOK_TILT, M_MAX_LOOK_TILT); CLAMP(lara->head_rot.y, M_MIN_LOOK_ROTATION, M_MAX_LOOK_ROTATION); // Get head-relative points in mesh space (faithful), then project the // camera's ray onto the head->forward axis to remove breathing-induced // roll/tilt wobble without moving the ray off Lara's head. const XYZ_32 head_pos = M_GetHeadPos(0, 0, 0); XYZ_32 pos_1 = M_GetHeadPos(0, 16, 64); int16_t room_num = lara_item->room_num; const XYZ_32 sample_pos = { pos_1.x, pos_1.y + STEP_L, pos_1.z }; const SECTOR *sector = Room_GetSector(sample_pos, &room_num); const ROOM *room = Room_Get(room_num); if (room->flags.swamp) { pos_1.y = room->max_ceiling - STEP_L; } sector = Room_GetSector(pos_1, &room_num); int32_t height = Room_GetHeight(sector, pos_1); int32_t ceiling = Room_GetCeiling(sector, pos_1); if (height == NO_HEIGHT || ceiling == NO_HEIGHT || ceiling >= height || pos_1.y > height || pos_1.y < ceiling) { pos_1 = M_GetHeadPos(0, 16, 0); room_num = lara_item->room_num; sector = Room_GetSector( (XYZ_32) { pos_1.x, pos_1.y + STEP_L, pos_1.z }, &room_num); room = Room_Get(room_num); if (room->flags.swamp) { pos_1.y = room->max_ceiling - STEP_L; } sector = Room_GetSector(pos_1, &room_num); height = Room_GetHeight(sector, pos_1); ceiling = Room_GetCeiling(sector, pos_1); if (height == NO_HEIGHT || ceiling == NO_HEIGHT || ceiling >= height || pos_1.y > height || pos_1.y < ceiling) { pos_1 = M_GetHeadPos(0, 16, -64); } } XYZ_32 pos_2 = M_GetHeadPos(0, 0, -1024); XYZ_32 pos_3 = M_GetHeadPos(0, 0, 2048); // Constrain the camera ray to pass through Lara's head (OG behavior), but // remove idle-breathing wobble by projecting onto a stable forward axis // derived from yaw + pitch (no roll/tilt from torso animation). const int16_t yaw = lara_item->rot.y + lara->head_rot.y; const int16_t pitch = lara_item->rot.x + lara->head_rot.x; const XYZ_32 axis = XYZ_32_FromYawPitch(yaw, pitch, 1 << W2V_SHIFT); const int64_t axis_len2 = XYZ_32_GetLength2_64(axis); if (axis_len2 != 0) { XYZ_32_ProjectPointOntoAxis(head_pos, axis, axis_len2, &pos_1); XYZ_32_ProjectPointOntoAxis(head_pos, axis, axis_len2, &pos_2); XYZ_32_ProjectPointOntoAxis(head_pos, axis, axis_len2, &pos_3); } const XYZ_32 delta = { .x = (pos_2.x - pos_1.x) >> 3, .y = (pos_2.y - pos_1.y) >> 3, .z = (pos_2.z - pos_1.z) >> 3, }; XYZ_32 ideal_pos = pos_1; room_num = lara_item->room_num; int32_t i = 0; for (; i < M_LOS_STEPS; i++) { int16_t next_room_num = room_num; sector = Room_GetSector( (XYZ_32) { ideal_pos.x, ideal_pos.y + STEP_L, ideal_pos.z }, &next_room_num); if (Room_Get(next_room_num)->flags.swamp) { ideal_pos.y = Room_Get(next_room_num)->max_ceiling - STEP_L; break; } sector = Room_GetSector(ideal_pos, &next_room_num); height = Room_GetHeight(sector, ideal_pos); ceiling = Room_GetCeiling(sector, ideal_pos); if (height == NO_HEIGHT || ceiling == NO_HEIGHT || ceiling >= height || ideal_pos.y > height || ideal_pos.y < ceiling) { break; } room_num = next_room_num; ideal_pos.x += delta.x; ideal_pos.y += delta.y; ideal_pos.z += delta.z; } if (i != 0) { ideal_pos.x -= delta.x; ideal_pos.y -= delta.y; ideal_pos.z -= delta.z; } GAME_VECTOR ideal = { .pos = ideal_pos, .room_num = room_num, }; if (M_UpdateLaraState()) { m_LastLookIdeal.pos = ideal; m_LastLookIdeal.target.pos = pos_3; } else { ideal = m_LastLookIdeal.pos; pos_3 = m_LastLookIdeal.target.pos; } M_Collide(&ideal, M_LOOK_SHIFT, true); if (m_LastState.cam_type == CAM_FIXED) { g_Camera.pos.pos = ideal.pos; g_Camera.target.pos = pos_3; } else { g_Camera.pos.x += (ideal.x - g_Camera.pos.x) >> 2; g_Camera.pos.y += (ideal.y - g_Camera.pos.y) >> 2; g_Camera.pos.z += (ideal.z - g_Camera.pos.z) >> 2; g_Camera.target.x += (pos_3.x - g_Camera.target.x) >> 2; g_Camera.target.y += (pos_3.y - g_Camera.target.y) >> 2; g_Camera.target.z += (pos_3.z - g_Camera.target.z) >> 2; } g_Camera.target.room_num = lara_item->room_num; if (g_Camera.type == m_LastState.cam_type) { Camera_ApplyBounce(); } Room_GetSector(g_Camera.pos.pos, &g_Camera.pos.room_num); ideal_pos = g_Camera.pos.pos; room_num = g_Camera.pos.room_num; sector = Room_GetSector(ideal_pos, &room_num); height = Room_GetHeight(sector, ideal_pos); ceiling = Room_GetCeiling(sector, ideal_pos); if (ceiling != NO_HEIGHT && height != NO_HEIGHT) { if (ceiling > ideal_pos.y - 255 && height < ideal_pos.y + 255 && height > ceiling) { g_Camera.pos.y = (ceiling + height) >> 1; } else if (height < ideal_pos.y + 255 && height > ceiling) { g_Camera.pos.y = height - 255; } else if (ceiling > ideal_pos.y - 255 && height > ceiling) { g_Camera.pos.y = ceiling + 255; } } ideal_pos = g_Camera.pos.pos; room_num = g_Camera.pos.room_num; sector = Room_GetSector(ideal_pos, &room_num); height = Room_GetHeight(sector, ideal_pos); ceiling = Room_GetCeiling(sector, ideal_pos); if (Room_Get(room_num)->flags.swamp) { g_Camera.pos.y = Room_Get(room_num)->max_ceiling - STEP_L; } else if ( ideal_pos.y < ceiling || ideal_pos.y > height || ceiling >= height || height == NO_HEIGHT || ceiling == NO_HEIGHT) { M_LOS(&g_Camera.target, &g_Camera.pos, 0); } ideal_pos = g_Camera.pos.pos; room_num = g_Camera.pos.room_num; sector = Room_GetSector(ideal_pos, &room_num); height = Room_GetHeight(sector, ideal_pos); ceiling = Room_GetCeiling(sector, ideal_pos); if (ideal_pos.y < ceiling || ideal_pos.y > height || ceiling >= height || height == NO_HEIGHT || ceiling == NO_HEIGHT || Room_Get(room_num)->flags.swamp) { g_Camera.pos.pos = pos_1; g_Camera.pos.room_num = lara_item->room_num; } Room_GetSector(g_Camera.pos.pos, &g_Camera.pos.room_num); m_LastState.cam_type = g_Camera.type; Camera_UpdateMicPosition(); lara->head_rot = head_rot; lara->torso_rot = torso_rot; } static void M_ClampResult(void) { XYZ_32 pos = g_Camera.interp.result.pos; int16_t room_num = g_Camera.interp.room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); const int32_t height = Room_GetHeightEx(sector, pos, true, NO_ITEM); const int32_t ceiling = Room_GetCeilingEx(sector, pos, true); if (height == NO_HEIGHT || ceiling == NO_HEIGHT || height < g_Camera.pos.y || ceiling > g_Camera.pos.y) { pos = g_Camera.pos.pos; room_num = g_Camera.pos.room_num; } Room_GetSector(pos, &room_num); g_Camera.interp.result.pos = pos; g_Camera.interp.room_num = room_num; } static void M_Reset(void) { const ITEM *const lara_item = Lara_GetItem(); ASSERT(lara_item != nullptr); g_Camera.shift = 0; m_LastIdeal.target.pos = lara_item->pos; m_LastIdeal.target.pos.y -= WALL_L; m_LastIdeal.target.room_num = lara_item->room_num; g_Camera.target = m_LastIdeal.target; g_Camera.pos = m_LastIdeal.target; g_Camera.pos.z -= 100; } static void M_Update( const ITEM *const item, const bool fixed_camera, int32_t target_y) { Camera_SetChunky(false); if (g_Camera.type != CAM_LOOK) { m_LastIdeal.target = g_Camera.target; } const BOUNDS_16 *bounds = Item_GetBoundsAccurate(item); if (g_Camera.item != nullptr && !fixed_camera) { bounds = Item_GetBoundsAccurate(g_Camera.item); const int32_t dx = g_Camera.item->pos.x - item->pos.x; const int32_t dz = g_Camera.item->pos.z - item->pos.z; const int32_t shift = Math_Sqrt(SQUARE(dx) + SQUARE(dz)); int16_t angle = Math_Atan(dz, dx) - item->rot.y; int16_t tilt = Math_Atan( shift, target_y - (bounds->min.y + bounds->max.y) / 2 - g_Camera.item->pos.y); angle >>= 1; tilt >>= 1; if (angle > CAMERA_MIN_HEAD_ROTATION && angle < CAMERA_MAX_HEAD_ROTATION && tilt > CAMERA_MIN_HEAD_TILT && tilt < CAMERA_MAX_HEAD_TILT) { LARA_INFO *const lara_info = Lara_GetLaraInfo(); int16_t change = angle - lara_info->head_rot.y; if (change > CAMERA_HEAD_TURN) { lara_info->head_rot.y += CAMERA_HEAD_TURN; } else if (change < -CAMERA_HEAD_TURN) { lara_info->head_rot.y -= CAMERA_HEAD_TURN; } else { lara_info->head_rot.y = angle; } change = tilt - lara_info->head_rot.x; if (change > CAMERA_HEAD_TURN) { lara_info->head_rot.x += CAMERA_HEAD_TURN; } else if (change < -CAMERA_HEAD_TURN) { lara_info->head_rot.x -= CAMERA_HEAD_TURN; } else { lara_info->head_rot.x = tilt; } lara_info->torso_rot.x = lara_info->head_rot.x; lara_info->torso_rot.y = lara_info->head_rot.y; g_Camera.type = CAM_LOOK; g_Camera.item->looked_at = true; } } if (g_Camera.type == CAM_LOOK || g_Camera.type == CAM_COMBAT) { target_y -= STEP_L; g_Camera.target.room_num = item->room_num; if (g_Camera.fixed_camera) { g_Camera.target.y = target_y; g_Camera.speed = 1; } else { g_Camera.target.y += (target_y - g_Camera.target.y) >> 2; g_Camera.speed = g_Camera.type == CAM_LOOK ? CAMERA_LOOK_SPEED : CAMERA_COMBAT_SPEED; } g_Camera.fixed_camera = false; if (g_Camera.type == CAM_LOOK) { M_Look(item); } else { M_Combat(item); } } else { g_Camera.target.x = item->pos.x; g_Camera.target.z = item->pos.z; if (g_Camera.flags == CF_FOLLOW_CENTRE) { const int32_t shift = (bounds->min.z + bounds->max.z) / 2; g_Camera.target.z += (shift * Math_Cos(item->rot.y)) >> W2V_SHIFT; g_Camera.target.x += (shift * Math_Sin(item->rot.y)) >> W2V_SHIFT; } g_Camera.target.room_num = item->room_num; g_Camera.target.y = target_y; if (g_Camera.fixed_camera != fixed_camera) { g_Camera.fixed_camera = true; g_Camera.speed = 1; } else { g_Camera.fixed_camera = false; } if (g_Camera.speed != 1 && m_LastState.cam_type != CAM_LOOK) { g_Camera.target.x = ((g_Camera.target.x - m_LastIdeal.target.x) >> 2) + m_LastIdeal.target.x; g_Camera.target.y = ((g_Camera.target.y - m_LastIdeal.target.y) >> 2) + m_LastIdeal.target.y; g_Camera.target.z = ((g_Camera.target.z - m_LastIdeal.target.z) >> 2) + m_LastIdeal.target.z; } Room_GetSector(g_Camera.target.pos, &g_Camera.target.room_num); if (g_Camera.type == CAM_CHASE || g_Camera.flags == CF_CHASE_OBJECT) { M_Chase(item); } else { M_Fixed(); } } } bool Camera_LOSCheck( GAME_VECTOR *const start, GAME_VECTOR *const target, const int32_t shift) { return M_LOS(start, target, shift); } bool Camera_Collide( GAME_VECTOR *const ideal, const int32_t shift, const bool y_first) { return M_Collide(ideal, shift, y_first); } static const CAMERA_STRATEGY m_Strategy = { .get_chase_speed_func = M_GetChaseSpeed, .get_look_settings_func = M_GetLookSettingsFunc, .clamp_result_func = M_ClampResult, .reset_func = M_Reset, .update_func = M_Update, }; REGISTER_CAMERA(CAMERA_MODE_TR3, m_Strategy) ================================================ FILE: src/trx/game/camera/photo_mode.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #define MIN_PHOTO_FOV 10 #define MAX_PHOTO_FOV 150 #define PHOTO_ROT_SHIFT (DEG_1 * 4) #define PHOTO_MAX_PITCH_ROLL (DEG_90 - DEG_1) #define PHOTO_MAX_SPEED 100 #define M_CosMul(a, b) TRIGMULT2(Math_Cos((a)), (b)) #define M_SinMul(a, b) TRIGMULT2(Math_Sin((a)), (b)) static int32_t m_PhotoSpeed = 0; static int32_t m_OriginalFOV; static FOV_MODE m_OriginalFOVMode; static CAMERA_INFO m_OriginalCamera = {}; static int32_t m_CurrentFOV; static FOV_MODE m_CurrentFOVMode; static CAMERA_INFO m_StartingCamera = {}; static struct { bool is_chunky; int32_t fov; FOV_MODE fov_mode; CAMERA_INFO camera; } m_PreviousState; static BOUNDS_32 m_WorldBounds = {}; static void M_ResetCamera(const bool exiting) { CAMERA_INFO camera = g_Camera; g_Camera = exiting ? m_OriginalCamera : m_StartingCamera; // ensure Camera_EnsureEnvironment() picks up the flag change g_Camera.underwater = camera.underwater; Viewport_AlterFOV(exiting ? -1 : m_OriginalFOV, m_OriginalFOVMode); m_CurrentFOV = m_OriginalFOV / DEG_1; } static int32_t M_GetShiftSpeed(const int32_t val) { return val * m_PhotoSpeed / (float)PHOTO_MAX_SPEED; } static int32_t M_GetRotSpeed(void) { return MAX(DEG_1, M_GetShiftSpeed(PHOTO_ROT_SHIFT)); } static XYZ_32 M_GetShift(const int32_t dx, const int32_t dy, const int32_t dz) { const int16_t yaw = g_Camera.target_angle; const int16_t pitch = g_Camera.target_elevation; const int16_t roll = g_Camera.roll; const int32_t dx_r = M_CosMul(roll, dx) - M_SinMul(roll, dy); const int32_t dy_r = M_SinMul(roll, dx) + M_CosMul(roll, dy); const int32_t dz_r = dz; // unchanged if roll is around Z const int32_t dy_p = M_CosMul(pitch, dy_r) - M_SinMul(pitch, dz_r); const int32_t dz_p = M_SinMul(pitch, dy_r) + M_CosMul(pitch, dz_r); const int32_t dx_p = dx_r; // unchanged if pitch is around X const int32_t dx_y = M_CosMul(yaw, dx_p) + M_SinMul(yaw, dz_p); const int32_t dz_y = -M_SinMul(yaw, dx_p) + M_CosMul(yaw, dz_p); const int32_t dy_y = dy_p; // unchanged if yaw is around Y return (XYZ_32) { dx_y, dy_y, dz_y }; } static void M_ShiftCamera(int32_t dx, int32_t dy, int32_t dz) { const XYZ_32 shift = M_GetShift(dx, dy, dz); g_Camera.pos.x += shift.x; g_Camera.pos.y += shift.y; g_Camera.pos.z += shift.z; g_Camera.target.x += shift.x; g_Camera.target.y += shift.y; g_Camera.target.z += shift.z; } static void M_ApplyRotation( const int32_t d_yaw, const int32_t d_pitch, const int32_t d_roll, const bool respect_roll) { int32_t yaw = g_Camera.target_angle; int32_t pitch = g_Camera.target_elevation; int32_t roll = g_Camera.roll; // rotate with respect to current upright axis if (respect_roll) { yaw += M_CosMul(roll, d_yaw) + M_SinMul(roll, d_pitch); pitch += M_CosMul(roll, d_pitch) - M_SinMul(roll, d_yaw); } else { yaw += d_yaw; pitch += d_pitch; } roll += d_roll; // handle pivoting if (pitch >= DEG_90 || pitch <= -DEG_90) { roll += DEG_180; yaw += DEG_180; pitch = g_Camera.target_elevation; } g_Camera.target_angle = yaw; g_Camera.target_elevation = pitch; g_Camera.roll = roll; } static void M_RotateCamera( const int32_t d_yaw, const int32_t d_pitch, const int32_t d_roll) { M_ApplyRotation(d_yaw, d_pitch, d_roll, true); const XYZ_32 shift = M_GetShift(0, 0, g_Camera.target_distance); g_Camera.target.x = g_Camera.pos.x + shift.x; g_Camera.target.y = g_Camera.pos.y + shift.y; g_Camera.target.z = g_Camera.pos.z + shift.z; } static void M_RotateTarget( const int32_t d_yaw, const int32_t d_pitch, const int32_t d_roll) { M_ApplyRotation(d_yaw, d_pitch, d_roll, false); const XYZ_32 shift = M_GetShift(0, 0, g_Camera.target_distance); g_Camera.pos.x = g_Camera.target.x - shift.x; g_Camera.pos.y = g_Camera.target.y - shift.y; g_Camera.pos.z = g_Camera.target.z - shift.z; } static void M_ClampCameraPos(void) { // While the camera is free, we want to clamp to within overall world bounds // to help counteract getting lost in the void. const GAME_VECTOR prev_cam_pos = g_Camera.pos; CLAMP(g_Camera.pos.x, m_WorldBounds.min.x, m_WorldBounds.max.x); CLAMP(g_Camera.pos.y, m_WorldBounds.min.y, m_WorldBounds.max.y); CLAMP(g_Camera.pos.z, m_WorldBounds.min.z, m_WorldBounds.max.z); g_Camera.target.x += (g_Camera.pos.x - prev_cam_pos.x); g_Camera.target.y += (g_Camera.pos.y - prev_cam_pos.y); g_Camera.target.z += (g_Camera.pos.z - prev_cam_pos.z); } static bool M_CameraInsideRoom(const XYZ_32 pos, const int16_t room_num) { return Room_PointInside(Room_Get(room_num), pos); } static void M_UpdateCameraRooms(void) { Room_GetSector(g_Camera.pos.pos, &g_Camera.pos.room_num); Room_GetSector(g_Camera.target.pos, &g_Camera.target.room_num); if (!M_CameraInsideRoom(g_Camera.pos.pos, g_Camera.pos.room_num)) { const int16_t pos_room_num = Room_GetIndexFromPos(g_Camera.pos.pos); const int16_t tar_room_num = Room_GetIndexFromPos(g_Camera.target.pos); if (pos_room_num != NO_ROOM) { g_Camera.pos.room_num = pos_room_num; if (tar_room_num == NO_ROOM) { g_Camera.target.room_num = pos_room_num; } } if (tar_room_num != NO_ROOM) { g_Camera.target.room_num = tar_room_num; if (pos_room_num == NO_ROOM) { g_Camera.pos.room_num = tar_room_num; } } } Camera_EnsureEnvironment(); } static bool M_HandleShiftInputs(void) { bool result = false; const int32_t distance = M_GetShiftSpeed((WALL_L * 5.0) / LOGIC_FPS); if (g_Input.camera_left) { M_ShiftCamera(-distance, 0, 0); result = true; } else if (g_Input.camera_right) { M_ShiftCamera(distance, 0, 0); result = true; } if (g_Input.camera_forward) { M_ShiftCamera(0, 0, distance); result = true; } else if (g_Input.camera_back) { M_ShiftCamera(0, 0, -distance); result = true; } if (!g_Input.slow && g_Input.camera_up) { M_ShiftCamera(0, -distance, 0); result = true; } else if (!g_Input.slow && g_Input.camera_down) { M_ShiftCamera(0, distance, 0); result = true; } return result; } static bool M_HandleRotationInputs(void) { bool result = false; if (g_Input.forward) { M_RotateCamera(0, -M_GetRotSpeed(), 0); result = true; } else if (g_Input.back) { M_RotateCamera(0, M_GetRotSpeed(), 0); result = true; } if (g_Input.left) { M_RotateCamera(-M_GetRotSpeed(), 0, 0); result = true; } else if (g_Input.right) { M_RotateCamera(M_GetRotSpeed(), 0, 0); result = true; } if (g_Input.slow && g_Input.camera_up) { M_RotateCamera(0, 0, -M_GetRotSpeed()); result = true; } else if (g_Input.slow && g_Input.camera_down) { M_RotateCamera(0, 0, M_GetRotSpeed()); result = true; } return result; } static bool M_HandleTargetRotationInputs(void) { bool result = false; if (g_InputDB.roll) { M_RotateTarget(-DEG_90, 0, 0); result = true; } return result; } static bool M_HandleFOVInputs(void) { if (!g_Input.draw) { return false; } if (g_Input.slow) { m_CurrentFOV--; } else { m_CurrentFOV++; } CLAMP(m_CurrentFOV, MIN_PHOTO_FOV, MAX_PHOTO_FOV); Viewport_AlterFOV(m_CurrentFOV * DEG_1, m_CurrentFOVMode); return true; } void Camera_PhotoMode_Enter(void) { m_OriginalCamera = g_Camera; int16_t angles[2]; Math_GetVectorAngles( g_Camera.target.x - g_Camera.pos.x, g_Camera.target.y - g_Camera.pos.y, g_Camera.target.z - g_Camera.pos.z, angles); g_Camera.target_angle = angles[0]; g_Camera.target_elevation = angles[1]; g_Camera.target_distance = CAMERA_DEFAULT_DISTANCE; g_Camera.target_square = SQUARE(g_Camera.target_distance); g_Camera.target.room_num = g_Camera.pos.room_num; Room_GetSector(g_Camera.target.pos, &g_Camera.target.room_num); m_StartingCamera = g_Camera; m_OriginalFOV = Viewport_GetEffectiveFOV(); m_OriginalFOVMode = Viewport_GetFOVMode(); m_CurrentFOV = m_OriginalFOV / DEG_1; m_CurrentFOVMode = m_OriginalFOVMode; g_Camera.type = CAM_PHOTO_MODE; const int32_t border = WALL_L * 5; m_WorldBounds = Room_GetWorldBounds(); m_WorldBounds.min.x -= border; m_WorldBounds.min.y -= border; m_WorldBounds.min.z -= border; m_WorldBounds.max.x += border; m_WorldBounds.max.y += border; m_WorldBounds.max.z += border; M_UpdateCameraRooms(); } void Camera_PhotoMode_Exit(void) { Lara_Pose_Clear(); Viewport_AlterFOV(m_OriginalFOV, m_OriginalFOVMode); M_ResetCamera(true); } void Camera_PhotoMode_Update(void) { M_HandleFOVInputs(); bool changed = false; if (g_InputDB.camera_reset) { M_ResetCamera(false); g_Camera.type = CAM_PHOTO_MODE; changed = true; } changed |= M_HandleShiftInputs(); changed |= M_HandleRotationInputs(); if (changed) { m_PhotoSpeed++; CLAMPG(m_PhotoSpeed, PHOTO_MAX_SPEED); } else { m_PhotoSpeed = 0; } changed |= M_HandleTargetRotationInputs(); if (changed) { g_Camera.mic_pos = g_Camera.pos; M_ClampCameraPos(); M_UpdateCameraRooms(); } } void Camera_PhotoMode_UpdateFOV(void) { M_HandleFOVInputs(); } void Camera_PhotoMode_Pause(void) { m_PreviousState.camera = g_Camera; m_PreviousState.is_chunky = Camera_IsChunky(); m_PreviousState.fov = Viewport_GetSystemFOV(); m_PreviousState.fov_mode = Viewport_GetFOVMode(); g_Camera = m_OriginalCamera; } void Camera_PhotoMode_Resume(void) { g_Camera = m_PreviousState.camera; Camera_SetChunky(m_PreviousState.is_chunky); Viewport_AlterFOV(m_PreviousState.fov, m_PreviousState.fov_mode); } ================================================ FILE: src/trx/game/camera/photo_mode.h ================================================ #pragma once void Camera_PhotoMode_Enter(void); void Camera_PhotoMode_Exit(void); void Camera_PhotoMode_Update(void); void Camera_PhotoMode_UpdateFOV(void); void Camera_PhotoMode_Pause(void); void Camera_PhotoMode_Resume(void); ================================================ FILE: src/trx/game/camera/types.h ================================================ #pragma once #include #include typedef struct { int16_t head_turn; int16_t max_head_rotation; int16_t min_head_rotation; int16_t max_head_tilt; int16_t min_head_tilt; float torso_head_rot_y; float torso_head_rot_x; } CAMERA_LOOK_SETTINGS; typedef struct { int16_t (*get_chase_speed_func)(void); const CAMERA_LOOK_SETTINGS *(*get_look_settings_func)(bool on_surface); void (*clamp_result_func)(void); void (*reset_func)(void); void (*update_func)(const ITEM *item, bool fixed_camera, int32_t target_y); } CAMERA_STRATEGY; typedef struct { GAME_VECTOR pos; GAME_VECTOR target; CAMERA_TYPE type; int32_t shift; CAMERA_FLAGS flags; bool fixed_camera; int32_t bounce; bool underwater; int32_t target_distance; int32_t target_square; int16_t target_angle; int16_t actual_angle; int16_t target_elevation; int16_t num; int16_t last; int16_t timer; int16_t speed; int16_t roll; ITEM *item; ITEM *last_item; int32_t debuff; // used for the manual camera control int16_t additional_angle; int16_t additional_elevation; GAME_VECTOR mic_pos; struct { struct { XYZ_32 target; XYZ_32 pos; int32_t shift; } result, prev; int16_t room_num; } interp; } CAMERA_INFO; typedef struct { struct { XYZ_16 shift; } target, camera; int16_t fov; int16_t roll; } CINE_FRAME; typedef struct { int16_t frame_idx; int16_t frame_count; struct { XYZ_32 pos; XYZ_16 rot; int16_t target_angle; } position; } CINE_DATA; ================================================ FILE: src/trx/game/camera/vars.c ================================================ #include CAMERA_INFO g_Camera = {}; ================================================ FILE: src/trx/game/camera/vars.h ================================================ #pragma once #include extern CAMERA_INFO g_Camera; ================================================ FILE: src/trx/game/camera.h ================================================ #pragma once #include #include #include #include #include #include #include #include #include ================================================ FILE: src/trx/game/catalog/item_actions.def ================================================ X_CATALOG_ID(ITEM_ACTION_TURN_180) X_CATALOG_ID(ITEM_ACTION_LARA_NORMAL) X_CATALOG_ID(ITEM_ACTION_LARA_HANDS_FREE) X_CATALOG_ID(ITEM_ACTION_LARA_DRAW_RIGHT_GUN) X_CATALOG_ID(ITEM_ACTION_LARA_DRAW_LEFT_GUN) X_CATALOG_ID(ITEM_ACTION_LARA_SHOOT_RIGHT_GUN) X_CATALOG_ID(ITEM_ACTION_LARA_SHOOT_LEFT_GUN) X_CATALOG_ID(ITEM_ACTION_RESET_HAIR) X_CATALOG_ID(ITEM_ACTION_FINISH_LEVEL) X_CATALOG_ID(ITEM_ACTION_FLIP_MAP) X_CATALOG_ID(ITEM_ACTION_FLOOR_SHAKE) X_CATALOG_ID(ITEM_ACTION_BUBBLES) X_CATALOG_ID(ITEM_ACTION_EARTHQUAKE) X_CATALOG_ID(ITEM_ACTION_FLOOD) X_CATALOG_ID(ITEM_ACTION_RAISING_BLOCK) X_CATALOG_ID(ITEM_ACTION_STAIRS_TO_SLOPE) X_CATALOG_ID(ITEM_ACTION_DROP_SAND) X_CATALOG_ID(ITEM_ACTION_POWER_UP) X_CATALOG_ID(ITEM_ACTION_EXPLOSION) X_CATALOG_ID(ITEM_ACTION_CHAIN_BLOCK) X_CATALOG_ID(ITEM_ACTION_FLICKER) X_CATALOG_ID(ITEM_ACTION_CHANDELIER) X_CATALOG_ID(ITEM_ACTION_RUBBLE) X_CATALOG_ID(ITEM_ACTION_PISTON) X_CATALOG_ID(ITEM_ACTION_CURTAIN) X_CATALOG_ID(ITEM_ACTION_SET_CHANGE) X_CATALOG_ID(ITEM_ACTION_STATUE) X_CATALOG_ID(ITEM_ACTION_BOILER) X_CATALOG_ID(ITEM_ACTION_SWAP_MESHES_WITH_MESH_SWAP_1) X_CATALOG_ID(ITEM_ACTION_SWAP_MESHES_WITH_MESH_SWAP_2) X_CATALOG_ID(ITEM_ACTION_SWAP_MESHES_WITH_MESH_SWAP_3) X_CATALOG_ID(ITEM_ACTION_INVISIBILITY_ON) X_CATALOG_ID(ITEM_ACTION_INVISIBILITY_OFF) X_CATALOG_ID(ITEM_ACTION_DYNAMIC_LIGHT_ON) X_CATALOG_ID(ITEM_ACTION_DYNAMIC_LIGHT_OFF) X_CATALOG_ID(ITEM_ACTION_ASSAULT_RESET) X_CATALOG_ID(ITEM_ACTION_ASSAULT_STOP) X_CATALOG_ID(ITEM_ACTION_ASSAULT_START) X_CATALOG_ID(ITEM_ACTION_ASSAULT_FINISHED) X_CATALOG_ID(ITEM_ACTION_SHADOW_ON) X_CATALOG_ID(ITEM_ACTION_SHADOW_OFF) X_CATALOG_ID(ITEM_ACTION_FOOTPRINT) X_CATALOG_ID(ITEM_ACTION_ASSAULT_PENALTY_8) X_CATALOG_ID(ITEM_ACTION_ASSAULT_PENALTY_30) X_CATALOG_ID(ITEM_ACTION_RACETRACK_START) X_CATALOG_ID(ITEM_ACTION_RACETRACK_RESET) X_CATALOG_ID(ITEM_ACTION_RACETRACK_FINISHED) X_CATALOG_ID(ITEM_ACTION_GYM_HINT_1) X_CATALOG_ID(ITEM_ACTION_GYM_HINT_2) X_CATALOG_ID(ITEM_ACTION_GYM_HINT_3) X_CATALOG_ID(ITEM_ACTION_GYM_HINT_4) X_CATALOG_ID(ITEM_ACTION_GYM_HINT_5) X_CATALOG_ID(ITEM_ACTION_GYM_HINT_6) X_CATALOG_ID(ITEM_ACTION_GYM_HINT_7) X_CATALOG_ID(ITEM_ACTION_GYM_HINT_8) X_CATALOG_ID(ITEM_ACTION_GYM_HINT_9) X_CATALOG_ID(ITEM_ACTION_GYM_HINT_10) X_CATALOG_ID(ITEM_ACTION_GYM_HINT_11) X_CATALOG_ID(ITEM_ACTION_GYM_HINT_12) X_CATALOG_ID(ITEM_ACTION_GYM_HINT_13) X_CATALOG_ID(ITEM_ACTION_GYM_HINT_14) X_CATALOG_ID(ITEM_ACTION_GYM_HINT_15) X_CATALOG_ID(ITEM_ACTION_GYM_HINT_16) X_CATALOG_ID(ITEM_ACTION_GYM_HINT_17) X_CATALOG_ID(ITEM_ACTION_GYM_HINT_18) X_CATALOG_ID(ITEM_ACTION_GYM_HINT_19) X_CATALOG_ID(ITEM_ACTION_GYM_HINT_RESET) X_CATALOG_ID(ITEM_ACTION_CAMERA_SHAKE) X_CATALOG_ID(ITEM_ACTION_LOWERING_BLOCK) X_CATALOG_ID(ITEM_ACTION_TURN_90) ================================================ FILE: src/trx/game/catalog/lara_anims.def ================================================ X_CATALOG_ID(LA_RUN) X_CATALOG_ID(LA_WALK_FORWARD) X_CATALOG_ID(LA_WALK_STOP_RIGHT) X_CATALOG_ID(LA_WALK_STOP_LEFT) X_CATALOG_ID(LA_WALK_TO_RUN_RIGHT) X_CATALOG_ID(LA_WALK_TO_RUN_LEFT) X_CATALOG_ID(LA_RUN_START) X_CATALOG_ID(LA_RUN_TO_WALK_RIGHT) X_CATALOG_ID(LA_RUN_TO_STAND_LEFT) X_CATALOG_ID(LA_RUN_TO_WALK_LEFT) X_CATALOG_ID(LA_RUN_TO_STAND_RIGHT) X_CATALOG_ID(LA_STAND_STILL) X_CATALOG_ID(LA_TURN_RIGHT_SLOW) X_CATALOG_ID(LA_TURN_LEFT_SLOW) X_CATALOG_ID(LA_JUMP_FORWARD_LAND_START_UNUSED) X_CATALOG_ID(LA_JUMP_FORWARD_LAND_END_UNUSED) X_CATALOG_ID(LA_RUN_JUMP_RIGHT_START) X_CATALOG_ID(LA_RUN_JUMP_RIGHT_CONTINUE) X_CATALOG_ID(LA_RUN_JUMP_LEFT_START) X_CATALOG_ID(LA_RUN_JUMP_LEFT_CONTINUE) X_CATALOG_ID(LA_WALK_FORWARD_START) X_CATALOG_ID(LA_WALK_FORWARD_START_CONTINUE) X_CATALOG_ID(LA_JUMP_FORWARD_TO_FREEFALL) X_CATALOG_ID(LA_FREEFALL) X_CATALOG_ID(LA_FREEFALL_LAND) X_CATALOG_ID(LA_FREEFALL_LAND_DEATH) X_CATALOG_ID(LA_STAND_TO_JUMP_UP) X_CATALOG_ID(LA_STAND_TO_JUMP_UP_CONTINUE) X_CATALOG_ID(LA_JUMP_UP) X_CATALOG_ID(LA_JUMP_UP_TO_HANG_UNUSED) X_CATALOG_ID(LA_JUMP_UP_TO_FREEFALL) X_CATALOG_ID(LA_JUMP_UP_LAND) X_CATALOG_ID(LA_SMASH_JUMP) X_CATALOG_ID(LA_SMASH_JUMP_CONTINUE) X_CATALOG_ID(LA_FALL_START) X_CATALOG_ID(LA_FALL) X_CATALOG_ID(LA_FALL_TO_FREEFALL) X_CATALOG_ID(LA_HANG_TO_FREEFALL) X_CATALOG_ID(LA_WALK_BACK_END_RIGHT) X_CATALOG_ID(LA_WALK_BACK_END_LEFT) X_CATALOG_ID(LA_WALK_BACK) X_CATALOG_ID(LA_WALK_BACK_START) X_CATALOG_ID(LA_CLIMB_3CLICK) X_CATALOG_ID(LA_CLIMB_3CLICK_END_TO_RUN) X_CATALOG_ID(LA_TURN_RIGHT) X_CATALOG_ID(LA_JUMP_FORWARD_TO_FREEFALL_2) X_CATALOG_ID(LA_REACH_TO_FREEFALL) X_CATALOG_ID(LA_ROLL_ALTERNATE) X_CATALOG_ID(LA_ROLL_END_ALTERNATE) X_CATALOG_ID(LA_JUMP_FORWARD_END_TO_FREEFALL) X_CATALOG_ID(LA_CLIMB_2CLICK) X_CATALOG_ID(LA_CLIMB_2CLICK_END) X_CATALOG_ID(LA_CLIMB_2CLICK_END_TO_RUN) X_CATALOG_ID(LA_WALL_SMASH_LEFT) X_CATALOG_ID(LA_WALL_SMASH_RIGHT) X_CATALOG_ID(LA_RUN_UP_STEP_RIGHT) X_CATALOG_ID(LA_RUN_UP_STEP_LEFT) X_CATALOG_ID(LA_WALK_UP_STEP_RIGHT) X_CATALOG_ID(LA_WALK_UP_STEP_LEFT) X_CATALOG_ID(LA_WALK_DOWN_LEFT) X_CATALOG_ID(LA_WALK_DOWN_RIGHT) X_CATALOG_ID(LA_WALK_DOWN_BACK_LEFT) X_CATALOG_ID(LA_WALK_DOWN_BACK_RIGHT) X_CATALOG_ID(LA_WALL_SWITCH_DOWN) X_CATALOG_ID(LA_WALL_SWITCH_UP) X_CATALOG_ID(LA_SIDE_STEP_LEFT) X_CATALOG_ID(LA_SIDE_STEP_LEFT_END) X_CATALOG_ID(LA_SIDE_STEP_RIGHT) X_CATALOG_ID(LA_SIDE_STEP_RIGHT_END) X_CATALOG_ID(LA_ROTATE_LEFT) X_CATALOG_ID(LA_SLIDE_FORWARD) X_CATALOG_ID(LA_SLIDE_FORWARD_END) X_CATALOG_ID(LA_SLIDE_FORWARD_STOP) X_CATALOG_ID(LA_STAND_TO_JUMP) X_CATALOG_ID(LA_JUMP_BACK_START) X_CATALOG_ID(LA_JUMP_BACK) X_CATALOG_ID(LA_JUMP_FORWARD_START) X_CATALOG_ID(LA_JUMP_FORWARD) X_CATALOG_ID(LA_JUMP_LEFT_START) X_CATALOG_ID(LA_JUMP_LEFT) X_CATALOG_ID(LA_JUMP_RIGHT_START) X_CATALOG_ID(LA_JUMP_RIGHT) X_CATALOG_ID(LA_LAND) X_CATALOG_ID(LA_JUMP_BACK_TO_FREEFALL) X_CATALOG_ID(LA_JUMP_LEFT_TO_FREEFALL) X_CATALOG_ID(LA_JUMP_RIGHT_TO_FREEFALL) X_CATALOG_ID(LA_UNDERWATER_SWIM_FORWARD) X_CATALOG_ID(LA_UNDERWATER_SWIM_FORWARD_DRIFT) X_CATALOG_ID(LA_SMALL_JUMP_BACK_START) X_CATALOG_ID(LA_SMALL_JUMP_BACK) X_CATALOG_ID(LA_SMALL_JUMP_BACK_END) X_CATALOG_ID(LA_JUMP_UP_START) X_CATALOG_ID(LA_LAND_TO_RUN) X_CATALOG_ID(LA_FALL_BACK) X_CATALOG_ID(LA_JUMP_FORWARD_TO_REACH) X_CATALOG_ID(LA_REACH) X_CATALOG_ID(LA_REACH_TO_HANG) X_CATALOG_ID(LA_CLIMB_ON) X_CATALOG_ID(LA_REACH_TO_FREEFALL_2) X_CATALOG_ID(LA_FALL_CROUCHING_LANDING) X_CATALOG_ID(LA_JUMP_FORWARD_TO_REACH_LATE) X_CATALOG_ID(LA_JUMP_FORWARD_START_TO_REACH_ALTERNATE) X_CATALOG_ID(LA_CLIMB_ON_END) X_CATALOG_ID(LA_STAND_IDLE) X_CATALOG_ID(LA_SLIDE_BACKWARD_START) X_CATALOG_ID(LA_SLIDE_BACKWARD) X_CATALOG_ID(LA_SLIDE_BACKWARD_END) X_CATALOG_ID(LA_UNDERWATER_SWIM_TO_IDLE) X_CATALOG_ID(LA_UNDERWATER_IDLE) X_CATALOG_ID(LA_UNDERWARER_IDLE_TO_SWIM) X_CATALOG_ID(LA_ONWATER_IDLE) X_CATALOG_ID(LA_ONWATER_TO_STAND_HIGH) X_CATALOG_ID(LA_FREEFALL_TO_UNDERWATER) X_CATALOG_ID(LA_ONWATER_DIVE_ALTERNATE) X_CATALOG_ID(LA_UNDERWATER_TO_ONWATER) X_CATALOG_ID(LA_ONWATER_SWIM_FORWARD_DIVE) X_CATALOG_ID(LA_ONWATER_SWIM_FORWARD) X_CATALOG_ID(LA_ONWATER_SWIM_FORWARD_TO_IDLE) X_CATALOG_ID(LA_ONWATER_IDLE_TO_SWIM_FORWARD) X_CATALOG_ID(LA_ONWATER_DIVE) X_CATALOG_ID(LA_PUSHABLE_GRAB) X_CATALOG_ID(LA_PUSHABLE_RELEASE) X_CATALOG_ID(LA_PUSHABLE_PULL) X_CATALOG_ID(LA_PUSHABLE_PUSH) X_CATALOG_ID(LA_UNDERWATER_DEATH) X_CATALOG_ID(LA_HIT_FRONT) X_CATALOG_ID(LA_HIT_BACK) X_CATALOG_ID(LA_HIT_LEFT) X_CATALOG_ID(LA_HIT_RIGHT) X_CATALOG_ID(LA_UNDERWATER_SWITCH) X_CATALOG_ID(LA_UNDERWATER_PICKUP) X_CATALOG_ID(LA_USE_KEY) X_CATALOG_ID(LA_ONWATER_DEATH) X_CATALOG_ID(LA_RUN_DEATH) X_CATALOG_ID(LA_USE_PUZZLE) X_CATALOG_ID(LA_PICKUP) X_CATALOG_ID(LA_SHIMMY_LEFT) X_CATALOG_ID(LA_SHIMMY_RIGHT) X_CATALOG_ID(LA_STAND_DEATH) X_CATALOG_ID(LA_BOULDER_DEATH) X_CATALOG_ID(LA_ONWATER_IDLE_TO_SWIM_BACK) X_CATALOG_ID(LA_ONWATER_SWIM_BACK) X_CATALOG_ID(LA_ONWATER_SWIM_BACK_TO_IDLE) X_CATALOG_ID(LA_ONWATER_SWIM_LEFT) X_CATALOG_ID(LA_ONWATER_SWIM_RIGHT) X_CATALOG_ID(LA_DEATH_JUMP) X_CATALOG_ID(LA_ROLL_START) X_CATALOG_ID(LA_ROLL_CONTINUE) X_CATALOG_ID(LA_ROLL_END) X_CATALOG_ID(LA_SPIKE_DEATH) X_CATALOG_ID(LA_SWING_IN_SLOW) X_CATALOG_ID(LA_SWING_IN_FAST) X_CATALOG_ID(LA_SWANDIVE_ROLL) X_CATALOG_ID(LA_SWANDIVE_TO_UNDERWATER) X_CATALOG_ID(LA_FREEFALL_SWANDIVE) X_CATALOG_ID(LA_FREEFALL_SWANDIVE_TO_UNDERWATER) X_CATALOG_ID(LA_SWANDIVE_DEATH) X_CATALOG_ID(LA_SWANDIVE_LEFT) X_CATALOG_ID(LA_SWANDIVE_RIGHT) X_CATALOG_ID(LA_SWANDIVE_START) X_CATALOG_ID(LA_CLIMB_ON_HANDSTAND) X_CATALOG_ID(LA_RUN_JUMP_ROLL_START) X_CATALOG_ID(LA_SOMERSAULT) X_CATALOG_ID(LA_RUN_JUMP_ROLL_END) X_CATALOG_ID(LA_JUMP_FORWARD_ROLL_START) X_CATALOG_ID(LA_JUMP_FORWARD_ROLL_END) X_CATALOG_ID(LA_JUMP_BACK_ROLL_START) X_CATALOG_ID(LA_JUMP_BACK_ROLL_END) X_CATALOG_ID(LA_UNDERWATER_ROLL_START) X_CATALOG_ID(LA_UNDERWATER_ROLL_END) X_CATALOG_ID(LA_ONWATER_TO_STAND_MEDIUM) X_CATALOG_ID(LA_WADE) X_CATALOG_ID(LA_RUN_TO_WADE_LEFT) X_CATALOG_ID(LA_RUN_TO_WADE_RIGHT) X_CATALOG_ID(LA_WADE_TO_RUN_LEFT) X_CATALOG_ID(LA_WADE_TO_RUN_RIGHT) X_CATALOG_ID(LA_WADE_TO_STAND_RIGHT) X_CATALOG_ID(LA_WADE_TO_STAND_LEFT) X_CATALOG_ID(LA_STAND_TO_WADE) X_CATALOG_ID(LA_ONWATER_TO_WADE) X_CATALOG_ID(LA_ONWATER_TO_WADE_LOW) X_CATALOG_ID(LA_UNDERWATER_TO_STAND) X_CATALOG_ID(LA_UNDERWATER_SWIM_TO_STILL_HUDDLE) X_CATALOG_ID(LA_UNDERWATER_SWIM_TO_STILL_SPRAWL) X_CATALOG_ID(LA_UNDERWATER_SWIM_TO_STILL_MEDIUM) X_CATALOG_ID(LA_SLIDE_FORWARD_TO_RUN) X_CATALOG_ID(LA_JUMP_NEUTRAL_ROLL) X_CATALOG_ID(LA_CONTROLLED_DROP) X_CATALOG_ID(LA_CONTROLLED_DROP_CONTINUE) X_CATALOG_ID(LA_HANG_TO_JUMP_UP) X_CATALOG_ID(LA_HANG_TO_JUMP_UP_CONTINUE) X_CATALOG_ID(LA_HANG_TO_JUMP_BACK) X_CATALOG_ID(LA_HANG_TO_JUMP_BACK_CONTINUE) X_CATALOG_ID(LA_SPRINT) X_CATALOG_ID(LA_RUN_TO_SPRINT_LEFT) X_CATALOG_ID(LA_RUN_TO_SPRINT_RIGHT) X_CATALOG_ID(LA_SPRINT_SLIDE_STAND_LEFT) X_CATALOG_ID(LA_SPRINT_SLIDE_STAND_RIGHT) X_CATALOG_ID(LA_SPRINT_TO_ROLL_LEFT) X_CATALOG_ID(LA_SPRINT_ROLL_LEFT_TO_RUN) X_CATALOG_ID(LA_SPRINT_TO_ROLL_RIGHT) X_CATALOG_ID(LA_SPRINT_ROLL_RIGHT_TO_RUN) X_CATALOG_ID(LA_SPRINT_TO_RUN_LEFT) X_CATALOG_ID(LA_SPRINT_TO_RUN_RIGHT) X_CATALOG_ID(LA_POSE_RIGHT_START) X_CATALOG_ID(LA_POSE_RIGHT_CONTINUE) X_CATALOG_ID(LA_POSE_RIGHT_END) X_CATALOG_ID(LA_POSE_LEFT_START) X_CATALOG_ID(LA_POSE_LEFT_CONTINUE) X_CATALOG_ID(LA_POSE_LEFT_END) X_CATALOG_ID(LA_STAND_TO_LADDER) X_CATALOG_ID(LA_LADDER_UP) X_CATALOG_ID(LA_LADDER_UP_STOP_RIGHT) X_CATALOG_ID(LA_LADDER_UP_STOP_LEFT) X_CATALOG_ID(LA_LADDER_IDLE) X_CATALOG_ID(LA_LADDER_UP_START) X_CATALOG_ID(LA_LADDER_DOWN_STOP_LEFT) X_CATALOG_ID(LA_LADDER_DOWN_STOP_RIGHT) X_CATALOG_ID(LA_LADDER_DOWN) X_CATALOG_ID(LA_LADDER_DOWN_START) X_CATALOG_ID(LA_LADDER_RIGHT) X_CATALOG_ID(LA_LADDER_LEFT) X_CATALOG_ID(LA_LADDER_HANG) X_CATALOG_ID(LA_LADDER_HANG_TO_IDLE) X_CATALOG_ID(LA_LADDER_CLIMB_ON) X_CATALOG_ID(LA_LADDER_BACKFLIP_START) X_CATALOG_ID(LA_LADDER_BACKFLIP_CONTINUE) X_CATALOG_ID(LA_LADDER_UP_HANGING) X_CATALOG_ID(LA_LADDER_DOWN_HANGING) X_CATALOG_ID(LA_LADDER_TO_HANG_DOWN) X_CATALOG_ID(LA_LADDER_TO_HANG_RIGHT) X_CATALOG_ID(LA_LADDER_TO_HANG_LEFT) X_CATALOG_ID(LA_UNKNOWN) X_CATALOG_ID(LA_ONWATER_TO_WADE_SHALLOW_UNUSED) X_CATALOG_ID(LA_FLARE_THROW) X_CATALOG_ID(LA_SWITCH_SMALL_DOWN) X_CATALOG_ID(LA_SWITCH_SMALL_UP) X_CATALOG_ID(LA_BUTTON_PUSH) X_CATALOG_ID(LA_FLARE_PICKUP) X_CATALOG_ID(LA_UNDERWATER_FLARE_PICKUP) X_CATALOG_ID(LA_KICK) X_CATALOG_ID(LA_ZIPLINE_GRAB) X_CATALOG_ID(LA_ZIPLINE_RIDE) X_CATALOG_ID(LA_ZIPLINE_FALL) X_CATALOG_ID(LA_STAND_TO_CROUCH) X_CATALOG_ID(LA_CROUCH_ROLL_FORWARD_START) X_CATALOG_ID(LA_CROUCH_ROLL_FORWARD_CONTINUE) X_CATALOG_ID(LA_CROUCH_ROLL_FORWARD_END) X_CATALOG_ID(LA_CROUCH_TO_STAND) X_CATALOG_ID(LA_CROUCH_IDLE) X_CATALOG_ID(LA_SPRINT_SLIDE_STAND_RIGHT_ALTERNATE) X_CATALOG_ID(LA_SPRINT_SLIDE_STAND_LEFT_ALTERNATE) X_CATALOG_ID(LA_SPRINT_TO_ROLL_LEFT_ALTERNATE) X_CATALOG_ID(LA_MONKEY_GRAB) X_CATALOG_ID(LA_MONKEY_IDLE) X_CATALOG_ID(LA_MONKEY_FALL) X_CATALOG_ID(LA_MONKEY_FORWARD) X_CATALOG_ID(LA_MONKEY_STOP_LEFT) X_CATALOG_ID(LA_MONKEY_STOP_RIGHT) X_CATALOG_ID(LA_MONKEY_IDLE_TO_FORWARD_LEFT) X_CATALOG_ID(LA_SPRINT_TO_ROLL_START_ALTERNATE) X_CATALOG_ID(LA_SPRINT_TO_ROLL_CONTINUE_ALTERNATE) X_CATALOG_ID(LA_SPRINT_TO_ROLL_END_ALTERNATE) X_CATALOG_ID(LA_STAND_TO_CROUCH_END) X_CATALOG_ID(LA_CROUCH_ROLL_FORWARD_ALTERNATE) X_CATALOG_ID(LA_JUMP_FORWARD_START_TO_GRAB_EARLY) X_CATALOG_ID(LA_JUMP_FORWARD_START_TO_GRAB_LATE) X_CATALOG_ID(LA_RUN_TO_GRAB_RIGHT) X_CATALOG_ID(LA_RUN_TO_GRAB_LEFT) X_CATALOG_ID(LA_MONKEY_IDLE_TO_FORWARD_RIGHT) X_CATALOG_ID(LA_MONKEY_SHIMMY_LEFT) X_CATALOG_ID(LA_MONKEY_SHIMMY_LEFT_END) X_CATALOG_ID(LA_MONKEY_SHIMMY_RIGHT) X_CATALOG_ID(LA_MONKEY_SHIMMY_RIGHT_END) X_CATALOG_ID(LA_MONKEY_TURN_AROUND) X_CATALOG_ID(LA_CROUCH_TO_CRAWL_START) X_CATALOG_ID(LA_CRAWL_TO_CROUCH_START) X_CATALOG_ID(LA_CRAWL_FORWARD) X_CATALOG_ID(LA_CRAWL_IDLE_TO_FORWARD) X_CATALOG_ID(LA_CRAWL_FORWARD_TO_IDLE_START_RIGHT) X_CATALOG_ID(LA_CRAWL_IDLE) X_CATALOG_ID(LA_CROUCH_TO_CRAWL_END) X_CATALOG_ID(LA_CRAWL_TO_CROUCH_END_UNUSED) X_CATALOG_ID(LA_CRAWL_FORWARD_TO_IDLE_END_RIGHT) X_CATALOG_ID(LA_CRAWL_FORWARD_TO_IDLE_START_LEFT) X_CATALOG_ID(LA_CRAWL_FORWARD_TO_IDLE_END_LEFT) X_CATALOG_ID(LA_CRAWL_TURN_LEFT) X_CATALOG_ID(LA_CRAWL_TURN_RIGHT) X_CATALOG_ID(LA_MONKEY_TURN_LEFT) X_CATALOG_ID(LA_MONKEY_TURN_RIGHT) X_CATALOG_ID(LA_CROUCH_TO_CRAWL_CONTINUE) X_CATALOG_ID(LA_CRAWL_TO_CROUCH_CONTINUE) X_CATALOG_ID(LA_CRAWL_IDLE_TO_BACKWARD) X_CATALOG_ID(LA_CRAWL_BACKWARD) X_CATALOG_ID(LA_CRAWL_BACKWARD_TO_IDLE_RIGHT_START) X_CATALOG_ID(LA_CRAWL_BACKWARD_TO_IDLE_RIGHT_END) X_CATALOG_ID(LA_CRAWL_BACKWARD_TO_IDLE_LEFT_START) X_CATALOG_ID(LA_CRAWL_BACKWARD_TO_IDLE_LEFT_END) X_CATALOG_ID(LA_CRAWL_TURN_LEFT_EARLY_END) X_CATALOG_ID(LA_CRAWL_TURN_RIGHT_EARLY_END) X_CATALOG_ID(LA_CRAWL_MONKEY_TURN_LEFT_EARLY_END) X_CATALOG_ID(LA_CRAWL_MONKEY_TURN_LEFT_LATE_END) X_CATALOG_ID(LA_CRAWL_MONKEY_TURN_RIGHT_EARLY_END) X_CATALOG_ID(LA_CRAWL_MONKEY_TURN_RIGHT_LATE_END) X_CATALOG_ID(LA_HANG_TO_CROUCH_START) X_CATALOG_ID(LA_HANG_TO_CROUCH_END) X_CATALOG_ID(LA_CRAWL_TO_HANG_START) X_CATALOG_ID(LA_CRAWL_TO_HANG_CONTINUE) X_CATALOG_ID(LA_CROUCH_PICKUP) X_CATALOG_ID(LA_CRAWL_PICKUP) X_CATALOG_ID(LA_CROUCH_HIT_FRONT) X_CATALOG_ID(LA_CROUCH_HIT_BACK) X_CATALOG_ID(LA_CROUCH_HIT_RIGHT) X_CATALOG_ID(LA_CROUCH_HIT_LEFT) X_CATALOG_ID(LA_CRAWL_HIT_FRONT) X_CATALOG_ID(LA_CRAWL_HIT_BACK) X_CATALOG_ID(LA_CRAWL_HIT_RIGHT) X_CATALOG_ID(LA_CRAWL_HIT_LEFT) X_CATALOG_ID(LA_CRAWL_DEATH) X_CATALOG_ID(LA_CRAWL_TO_HANG_END) X_CATALOG_ID(LA_STAND_TO_CROUCH_ABORT) X_CATALOG_ID(LA_RUN_TO_CROUCH_LEFT_START) X_CATALOG_ID(LA_RUN_TO_CROUCH_RIGHT_START) X_CATALOG_ID(LA_RUN_TO_CROUCH_LEFT_END) X_CATALOG_ID(LA_RUN_TO_CROUCH_RIGHT_END) X_CATALOG_ID(LA_SPRINT_TO_CROUCH_LEFT) X_CATALOG_ID(LA_SPRINT_TO_CROUCH_RIGHT) X_CATALOG_ID(LA_CROUCH_PICKUP_FLARE) X_CATALOG_ID(LA_CRAWL_JUMP_DOWN) X_CATALOG_ID(LA_CROUCH_TURN_LEFT) X_CATALOG_ID(LA_CROUCH_TURN_RIGHT) X_CATALOG_ID(LA_LADDER_TO_CROUCH_START) X_CATALOG_ID(LA_LADDER_TO_CROUCH_END) ================================================ FILE: src/trx/game/catalog/lara_states.def ================================================ X_CATALOG_ID(LS_WALK) X_CATALOG_ID(LS_RUN) X_CATALOG_ID(LS_STOP) X_CATALOG_ID(LS_JUMP_FORWARD) X_CATALOG_ID(LS_POSE) X_CATALOG_ID(LS_FAST_BACK) X_CATALOG_ID(LS_TURN_RIGHT) X_CATALOG_ID(LS_TURN_LEFT) X_CATALOG_ID(LS_DEATH) X_CATALOG_ID(LS_FAST_FALL) X_CATALOG_ID(LS_HANG) X_CATALOG_ID(LS_REACH) X_CATALOG_ID(LS_SPLAT) X_CATALOG_ID(LS_TREAD) X_CATALOG_ID(LS_LAND) X_CATALOG_ID(LS_COMPRESS) X_CATALOG_ID(LS_WALK_BACK) X_CATALOG_ID(LS_SWIM) X_CATALOG_ID(LS_GLIDE) X_CATALOG_ID(LS_PULL_UP) X_CATALOG_ID(LS_FAST_TURN) X_CATALOG_ID(LS_STEP_RIGHT) X_CATALOG_ID(LS_STEP_LEFT) X_CATALOG_ID(LS_ROLL_CONT) X_CATALOG_ID(LS_SLIDE) X_CATALOG_ID(LS_JUMP_BACK) X_CATALOG_ID(LS_JUMP_RIGHT) X_CATALOG_ID(LS_JUMP_LEFT) X_CATALOG_ID(LS_JUMP_UP) X_CATALOG_ID(LS_FALL_BACK) X_CATALOG_ID(LS_SHIMMY_LEFT) X_CATALOG_ID(LS_SHIMMY_RIGHT) X_CATALOG_ID(LS_SLIDE_BACK) X_CATALOG_ID(LS_SURF_TREAD) X_CATALOG_ID(LS_SURF_SWIM) X_CATALOG_ID(LS_DIVE) X_CATALOG_ID(LS_PUSH_BLOCK) X_CATALOG_ID(LS_PULL_BLOCK) X_CATALOG_ID(LS_PP_READY) X_CATALOG_ID(LS_PICKUP) X_CATALOG_ID(LS_SWITCH_ON) X_CATALOG_ID(LS_SWITCH_OFF) X_CATALOG_ID(LS_USE_KEY) X_CATALOG_ID(LS_USE_PUZZLE) X_CATALOG_ID(LS_UW_DEATH) X_CATALOG_ID(LS_ROLL) X_CATALOG_ID(LS_SPECIAL) X_CATALOG_ID(LS_SURF_BACK) X_CATALOG_ID(LS_SURF_LEFT) X_CATALOG_ID(LS_SURF_RIGHT) X_CATALOG_ID(LS_USE_MIDAS) X_CATALOG_ID(LS_DIE_MIDAS) X_CATALOG_ID(LS_SWAN_DIVE) X_CATALOG_ID(LS_FAST_DIVE) X_CATALOG_ID(LS_GYMNAST) X_CATALOG_ID(LS_WATER_OUT) X_CATALOG_ID(LS_CONTROLLED) X_CATALOG_ID(LS_TWIST) X_CATALOG_ID(LS_WATER_ROLL) X_CATALOG_ID(LS_WADE) X_CATALOG_ID(LS_RESPONSIVE) X_CATALOG_ID(LS_NEUTRAL_ROLL) X_CATALOG_ID(LS_SPRINT) X_CATALOG_ID(LS_SPRINT_ROLL) X_CATALOG_ID(LS_POSE_START) X_CATALOG_ID(LS_POSE_END) X_CATALOG_ID(LS_POSE_LEFT) X_CATALOG_ID(LS_POSE_RIGHT) X_CATALOG_ID(LS_CLIMB_STANCE) X_CATALOG_ID(LS_CLIMBING) X_CATALOG_ID(LS_CLIMB_LEFT) X_CATALOG_ID(LS_CLIMB_END) X_CATALOG_ID(LS_CLIMB_RIGHT) X_CATALOG_ID(LS_CLIMB_DOWN) X_CATALOG_ID(LS_LARA_TEST1) X_CATALOG_ID(LS_LARA_TEST2) X_CATALOG_ID(LS_LARA_TEST3) X_CATALOG_ID(LS_FLARE_PICKUP) X_CATALOG_ID(LS_KICK) X_CATALOG_ID(LS_ZIPLINE) X_CATALOG_ID(LS_CROUCH_IDLE) X_CATALOG_ID(LS_CROUCH_ROLL) X_CATALOG_ID(LS_MONKEY_IDLE) X_CATALOG_ID(LS_MONKEY_FORWARD) X_CATALOG_ID(LS_MONKEY_LEFT) X_CATALOG_ID(LS_MONKEY_RIGHT) X_CATALOG_ID(LS_MONKEY_ROLL) X_CATALOG_ID(LS_CRAWL_IDLE) X_CATALOG_ID(LS_CRAWL_FORWARD) X_CATALOG_ID(LS_MONKEY_TURN_LEFT) X_CATALOG_ID(LS_MONKEY_TURN_RIGHT) X_CATALOG_ID(LS_CRAWL_TURN_LEFT) X_CATALOG_ID(LS_CRAWL_TURN_RIGHT) X_CATALOG_ID(LS_CRAWL_BACK) X_CATALOG_ID(LS_CLIMB_TO_CRAWL) X_CATALOG_ID(LS_CRAWL_TO_CLIMB) X_CATALOG_ID(LS_CRAWL_JUMP_DOWN) X_CATALOG_ID(LS_CROUCH_TURN_LEFT) X_CATALOG_ID(LS_CROUCH_TURN_RIGHT) ================================================ FILE: src/trx/game/catalog/manager.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include // Compile-time table of catalog IDs and their name strings typedef struct { CATALOG_CONTEXT context; CATALOG_ID id; const char *name_str; } M_ENTRY; static const M_ENTRY m_CatalogEntryDefs[] = { #define X_CATALOG_ID(enum_value) { CATALOG_MUSIC, enum_value, #enum_value }, #include #undef X_CATALOG_ID #define X_CATALOG_ID(enum_value) { CATALOG_OBJECTS, enum_value, #enum_value }, #include #undef X_CATALOG_ID #define X_CATALOG_ID(enum_value) { CATALOG_SAMPLES, enum_value, #enum_value }, #include #undef X_CATALOG_ID #define X_CATALOG_ID(enum_value) \ { CATALOG_LARA_STATES, enum_value, #enum_value }, #include #undef X_CATALOG_ID #define X_CATALOG_ID(enum_value) \ { CATALOG_LARA_ANIMS, enum_value, #enum_value }, #include #undef X_CATALOG_ID #define X_CATALOG_ID(enum_value) \ { CATALOG_ITEM_ACTIONS, enum_value, #enum_value }, #include #undef X_CATALOG_ID }; // Number of catalog entries static const size_t m_CatalogEntryCount = ARRAY_SIZE(m_CatalogEntryDefs); // Internal map from name to CATALOG_ID typedef struct { const char *name_str; int32_t enum_value; UT_hash_handle hh; } M_NAME_ENTRY; static M_NAME_ENTRY *m_Name2EnumMap[CATALOG_CONTEXT_MAX] = { nullptr }; // Internal map from game ID to CATALOG_ID typedef struct { int32_t game_id; int32_t enum_value; UT_hash_handle hh; } M_GAME_ID_ENTRY; static M_GAME_ID_ENTRY *m_GameID2EnumMap[CATALOG_CONTEXT_MAX] = { nullptr }; // Parsed game IDs arrays (dynamically sized) static int32_t **m_CatalogGameIDs = nullptr; // State flag static bool m_Initialized = false; // Helper: clear game_id->enum map static void M_ClearGameIDMap(M_GAME_ID_ENTRY **const map) { M_GAME_ID_ENTRY *cur, *tmp; HASH_ITER(hh, *map, cur, tmp) { HASH_DEL(*map, cur); Memory_Free(cur); } } // Helper: clear name->enum map static void M_ClearNameMap(M_NAME_ENTRY **const map) { M_NAME_ENTRY *cur, *tmp; HASH_ITER(hh, *map, cur, tmp) { HASH_DEL(*map, cur); Memory_Free(cur); } } // Trim leading/trailing whitespace in-place static char *M_StrTrim(char *s) { while (*s && isspace(*s)) { s++; } if (*s == '\0') { return s; } char *end = s + strlen(s) - 1; while (end > s && isspace(*end)) { *end-- = '\0'; } return s; } // Parse one CSV field into out buffer; advance *p past field static void M_ParseCSVField( const char **const p, char *const out, const size_t out_sz) { const char *src = *p; char *dst = out; const char *const end = out + out_sz - 1; if (*src == '"') { src++; while (*src && (*src != '"' || (src[1] == '"'))) { if (*src == '"' && src[1] == '"') { src++; } if (dst < end) { *dst++ = *src; } src++; } if (*src == '"') { src++; } } else { while (*src && *src != ',') { if (dst < end) { *dst++ = *src; } src++; } } *dst = '\0'; if (*src == ',') { src++; } *p = src; } // Build initial maps on first load static void M_Initialize(void) { const size_t count = m_CatalogEntryCount; m_CatalogGameIDs = Memory_Alloc(sizeof(*m_CatalogGameIDs) * CATALOG_CONTEXT_MAX); for (size_t ctx = 0; ctx < CATALOG_CONTEXT_MAX; ctx++) { m_CatalogGameIDs[ctx] = Memory_Alloc(sizeof(*m_CatalogGameIDs[ctx]) * count); } for (size_t idx = 0; idx < count; idx++) { const CATALOG_CONTEXT ctx = m_CatalogEntryDefs[idx].context; const CATALOG_ID id = m_CatalogEntryDefs[idx].id; m_CatalogGameIDs[ctx][id] = -1; M_NAME_ENTRY *const entry = Memory_Alloc(sizeof(*entry)); entry->name_str = m_CatalogEntryDefs[idx].name_str; entry->enum_value = id; HASH_ADD_KEYPTR( hh, m_Name2EnumMap[ctx], entry->name_str, (uint32_t)strlen(entry->name_str), entry); } m_Initialized = true; } bool Catalog_Load( const CATALOG_CONTEXT context, const char *const csv_path, const bool allow_duplicates) { if (!m_Initialized) { M_Initialize(); } char *file_data; size_t file_size; if (!File_Load(csv_path, &file_data, &file_size)) { return false; } const char *pos = file_data; const char *end = file_data + file_size; char line[512]; while (pos < end) { size_t len = 0; while (pos < end && *pos != '\n' && len + 1 < sizeof(line)) { line[len++] = *pos++; } if (pos < end && *pos == '\n') { pos++; } line[len] = '\0'; if (line[0] == '\0' || line[0] == '#') { continue; } const char *p = line; char id_buf[32]; char name_buf[64]; M_ParseCSVField(&p, id_buf, sizeof(id_buf)); M_ParseCSVField(&p, name_buf, sizeof(name_buf)); char *const id_str = M_StrTrim(id_buf); char *const name_str = M_StrTrim(name_buf); const int32_t game_id = (int32_t)strtol(id_str, nullptr, 10); CATALOG_ID id; if (!Catalog_NameToEnum(context, name_str, &id)) { continue; } m_CatalogGameIDs[context][id] = game_id; if (game_id >= 0) { M_GAME_ID_ENTRY *existing = nullptr; HASH_FIND_INT(m_GameID2EnumMap[context], &game_id, existing); if (existing == nullptr) { M_GAME_ID_ENTRY *gentry = Memory_Alloc(sizeof(*gentry)); gentry->game_id = game_id; gentry->enum_value = id; HASH_ADD_INT(m_GameID2EnumMap[context], game_id, gentry); } else if (!allow_duplicates) { LOG_ERROR( "Duplicate game ID %d for context %d", game_id, context); } } } Memory_FreePointer(&file_data); return true; } bool Catalog_NameToEnum( const CATALOG_CONTEXT context, const char *name, CATALOG_ID *const out_id) { M_NAME_ENTRY *entry = nullptr; HASH_FIND_STR(m_Name2EnumMap[context], name, entry); if (entry != nullptr) { *out_id = (CATALOG_ID)entry->enum_value; return true; } return false; } bool Catalog_EnumToGameID( const CATALOG_CONTEXT context, const CATALOG_ID id, int32_t *const out_game_id) { if (id < 0 || (size_t)id >= m_CatalogEntryCount) { return false; } const int32_t gid = m_CatalogGameIDs[context][id]; if (gid < 0) { return false; } *out_game_id = gid; return true; } bool Catalog_GameIDToEnum( const CATALOG_CONTEXT context, const int32_t game_id, CATALOG_ID *const out_id) { M_GAME_ID_ENTRY *entry = nullptr; HASH_FIND_INT(m_GameID2EnumMap[context], &game_id, entry); if (entry != nullptr) { *out_id = (CATALOG_ID)entry->enum_value; return true; } return false; } void Catalog_Shutdown(void) { if (!m_Initialized) { return; } for (size_t ctx = 0; ctx < CATALOG_CONTEXT_MAX; ctx++) { M_ClearGameIDMap(&m_GameID2EnumMap[ctx]); M_ClearNameMap(&m_Name2EnumMap[ctx]); Memory_Free(m_CatalogGameIDs[ctx]); } Memory_Free(m_CatalogGameIDs); m_CatalogGameIDs = nullptr; m_Initialized = false; } ================================================ FILE: src/trx/game/catalog/manager.h ================================================ #pragma once #include // Context discriminator for separate catalog namespaces typedef enum CATALOG_CONTEXT { CATALOG_OBJECTS, CATALOG_MUSIC, CATALOG_SAMPLES, CATALOG_LARA_STATES, CATALOG_LARA_ANIMS, CATALOG_ITEM_ACTIONS, CATALOG_CONTEXT_MAX, } CATALOG_CONTEXT; typedef int32_t CATALOG_ID; // Load mappings for a specific context from a CSV file of the form: // game_id,name[,comment] // A game_id of -1 indicates no mapping for that entry. // Returns true on success. bool Catalog_Load( CATALOG_CONTEXT context, const char *csv_path, bool allow_duplicates); // Convert an item name to its CATALOG_ID within a context. // Returns false if not found. bool Catalog_NameToEnum( CATALOG_CONTEXT context, const char *name, CATALOG_ID *out_id); // Convert a CATALOG_ID to its game-specific ID within a context. // Returns false if unmapped. bool Catalog_EnumToGameID( CATALOG_CONTEXT context, CATALOG_ID id, int32_t *out_game_id); // Convert a game-specific ID to its CATALOG_ID within a context. // Returns false if not found. bool Catalog_GameIDToEnum( CATALOG_CONTEXT context, int32_t game_id, CATALOG_ID *out_id); // Free internal resources. void Catalog_Shutdown(void); ================================================ FILE: src/trx/game/catalog/music.def ================================================ // Common X_CATALOG_ID(MX_SECRET) // TR1 X_CATALOG_ID(MX_TR1_GYM_HINT_03) X_CATALOG_ID(MX_TR1_GYM_HINT_04) X_CATALOG_ID(MX_TR1_GYM_HINT_12) X_CATALOG_ID(MX_TR1_GYM_HINT_14) X_CATALOG_ID(MX_TR1_GYM_HINT_15) X_CATALOG_ID(MX_TR1_GYM_HINT_16) X_CATALOG_ID(MX_TR1_GYM_HINT_17) X_CATALOG_ID(MX_TR1_GYM_HINT_18) X_CATALOG_ID(MX_TR1_GYM_HINT_24) X_CATALOG_ID(MX_TR1_GYM_HINT_25) X_CATALOG_ID(MX_BALDY_SPEECH) X_CATALOG_ID(MX_COWBOY_SPEECH) X_CATALOG_ID(MX_LARSON_SPEECH) X_CATALOG_ID(MX_NATLA_SPEECH) X_CATALOG_ID(MX_PIERRE_SPEECH) X_CATALOG_ID(MX_SKATEKID_SPEECH) X_CATALOG_ID(MX_UNUSED_0) X_CATALOG_ID(MX_UNUSED_1) X_CATALOG_ID(MX_UNUSED_2) // TR2 X_CATALOG_ID(MX_TR2_GYM_HINT_14) X_CATALOG_ID(MX_TR2_GYM_HINT_15) X_CATALOG_ID(MX_TR2_GYM_HINT_16) X_CATALOG_ID(MX_TR2_GYM_HINT_17) X_CATALOG_ID(MX_DAGGER_PULL) X_CATALOG_ID(MX_CUTSCENE_BATH) X_CATALOG_ID(MX_REVEAL_1) X_CATALOG_ID(MX_REVEAL_2) X_CATALOG_ID(MX_SKIDOO_THEME) X_CATALOG_ID(MX_BATTLE_THEME) // TR3 X_CATALOG_ID(MX_TR3_GYM_HINT_FAST_TIME) X_CATALOG_ID(MX_TR3_GYM_EXERCISE_01) X_CATALOG_ID(MX_TR3_GYM_EXERCISE_02) X_CATALOG_ID(MX_TR3_GYM_EXERCISE_03) X_CATALOG_ID(MX_TR3_GYM_EXERCISE_04) X_CATALOG_ID(MX_TR3_GYM_EXERCISE_05) X_CATALOG_ID(MX_TR3_GYM_EXERCISE_06) X_CATALOG_ID(MX_TR3_GYM_EXERCISE_07) X_CATALOG_ID(MX_TR3_GYM_EXERCISE_08) X_CATALOG_ID(MX_TR3_GYM_EXERCISE_09) X_CATALOG_ID(MX_TR3_GYM_EXERCISE_10) X_CATALOG_ID(MX_TR3_GYM_EXERCISE_11) X_CATALOG_ID(MX_TR3_GYM_EXERCISE_12) X_CATALOG_ID(MX_TR3_GYM_EXERCISE_13) X_CATALOG_ID(MX_TR3_GYM_EXERCISE_14) X_CATALOG_ID(MX_TR3_GYM_EXERCISE_15) X_CATALOG_ID(MX_TR3_GYM_EXERCISE_16) X_CATALOG_ID(MX_TR3_GYM_EXERCISE_17) X_CATALOG_ID(MX_TR3_GYM_EXERCISE_18) X_CATALOG_ID(MX_TR3_GYM_EXERCISE_19) X_CATALOG_ID(MX_MINE_CART_THEME) X_CATALOG_ID(MX_RIB_THEME) ================================================ FILE: src/trx/game/catalog/objects.def ================================================ X_CATALOG_ID(O_LARA) X_CATALOG_ID(O_LARA_PISTOLS) X_CATALOG_ID(O_LARA_SHOTGUN) X_CATALOG_ID(O_LARA_MAGNUMS) X_CATALOG_ID(O_LARA_UZIS) X_CATALOG_ID(O_LARA_EXTRA) X_CATALOG_ID(O_BACON_LARA) X_CATALOG_ID(O_WOLF) X_CATALOG_ID(O_BEAR) X_CATALOG_ID(O_BAT) X_CATALOG_ID(O_CROCODILE) X_CATALOG_ID(O_ALLIGATOR) X_CATALOG_ID(O_LION) X_CATALOG_ID(O_LIONESS) X_CATALOG_ID(O_PUMA) X_CATALOG_ID(O_APE) X_CATALOG_ID(O_RAT) X_CATALOG_ID(O_VOLE) X_CATALOG_ID(O_TREX) X_CATALOG_ID(O_RAPTOR) X_CATALOG_ID(O_COMPY) X_CATALOG_ID(O_ATLANTEAN_WINGED) X_CATALOG_ID(O_ATLANTEAN_SHOOTER) X_CATALOG_ID(O_ATLANTEAN_GROUND) X_CATALOG_ID(O_CENTAUR) X_CATALOG_ID(O_MUMMY) X_CATALOG_ID(O_DINO_WARRIOR) X_CATALOG_ID(O_FISH) X_CATALOG_ID(O_LARSON) X_CATALOG_ID(O_PIERRE) X_CATALOG_ID(O_SKATEBOARD) X_CATALOG_ID(O_SKATEKID) X_CATALOG_ID(O_COWBOY) X_CATALOG_ID(O_BALDY) X_CATALOG_ID(O_NATLA) X_CATALOG_ID(O_TORSO) X_CATALOG_ID(O_FALLING_BLOCK_1) X_CATALOG_ID(O_PENDULUM_1) X_CATALOG_ID(O_SPIKES) X_CATALOG_ID(O_ROLLING_BALL_1) X_CATALOG_ID(O_DART) X_CATALOG_ID(O_DART_EMITTER) X_CATALOG_ID(O_DISC) X_CATALOG_ID(O_DISC_EMITTER) X_CATALOG_ID(O_DRAWBRIDGE) X_CATALOG_ID(O_TEETH_TRAP) X_CATALOG_ID(O_DAMOCLES_SWORD) X_CATALOG_ID(O_THORS_HANDLE) X_CATALOG_ID(O_THORS_HEAD) X_CATALOG_ID(O_LIGHTNING_EMITTER) X_CATALOG_ID(O_MOVING_BAR) X_CATALOG_ID(O_MOVABLE_BLOCK_1) X_CATALOG_ID(O_MOVABLE_BLOCK_2) X_CATALOG_ID(O_MOVABLE_BLOCK_3) X_CATALOG_ID(O_MOVABLE_BLOCK_4) X_CATALOG_ID(O_SLIDING_PILLAR) X_CATALOG_ID(O_FALLING_CEILING_1) X_CATALOG_ID(O_FALLING_CEILING_2) X_CATALOG_ID(O_SWITCH_TYPE_NORMAL) X_CATALOG_ID(O_SWITCH_TYPE_UW) X_CATALOG_ID(O_DOOR_TYPE_1) X_CATALOG_ID(O_DOOR_TYPE_2) X_CATALOG_ID(O_DOOR_TYPE_3) X_CATALOG_ID(O_DOOR_TYPE_4) X_CATALOG_ID(O_DOOR_TYPE_5) X_CATALOG_ID(O_DOOR_TYPE_6) X_CATALOG_ID(O_DOOR_TYPE_7) X_CATALOG_ID(O_DOOR_TYPE_8) X_CATALOG_ID(O_TRAPDOOR_TYPE_1) X_CATALOG_ID(O_TRAPDOOR_TYPE_2) X_CATALOG_ID(O_TRAPDOOR_TYPE_3) X_CATALOG_ID(O_BRIDGE_FLAT) X_CATALOG_ID(O_BRIDGE_TILT_1) X_CATALOG_ID(O_BRIDGE_TILT_2) X_CATALOG_ID(O_PASSPORT_OPTION) X_CATALOG_ID(O_COMPASS_OPTION) X_CATALOG_ID(O_STOPWATCH_OPTION) X_CATALOG_ID(O_PHOTO_OPTION) X_CATALOG_ID(O_COG_1) X_CATALOG_ID(O_COG_2) X_CATALOG_ID(O_COG_3) X_CATALOG_ID(O_PLAYER_1) X_CATALOG_ID(O_PLAYER_2) X_CATALOG_ID(O_PLAYER_3) X_CATALOG_ID(O_PLAYER_4) X_CATALOG_ID(O_PASSPORT_CLOSED) X_CATALOG_ID(O_SAVE_CRYSTAL_ITEM) X_CATALOG_ID(O_PISTOL_ITEM) X_CATALOG_ID(O_SHOTGUN_ITEM) X_CATALOG_ID(O_MAGNUM_ITEM) X_CATALOG_ID(O_UZI_ITEM) X_CATALOG_ID(O_PISTOL_AMMO_ITEM) X_CATALOG_ID(O_SHOTGUN_AMMO_ITEM) X_CATALOG_ID(O_MAGNUM_AMMO_ITEM) X_CATALOG_ID(O_UZI_AMMO_ITEM) X_CATALOG_ID(O_EXPLOSIVE_ITEM) X_CATALOG_ID(O_SMALL_MEDIPACK_ITEM) X_CATALOG_ID(O_LARGE_MEDIPACK_ITEM) X_CATALOG_ID(O_DETAIL_OPTION) X_CATALOG_ID(O_SOUND_OPTION) X_CATALOG_ID(O_CONTROL_OPTION) X_CATALOG_ID(O_GAMMA_OPTION) X_CATALOG_ID(O_PISTOL_OPTION) X_CATALOG_ID(O_SHOTGUN_OPTION) X_CATALOG_ID(O_MAGNUM_OPTION) X_CATALOG_ID(O_UZI_OPTION) X_CATALOG_ID(O_PISTOL_AMMO_OPTION) X_CATALOG_ID(O_SHOTGUN_AMMO_OPTION) X_CATALOG_ID(O_MAGNUM_AMMO_OPTION) X_CATALOG_ID(O_UZI_AMMO_OPTION) X_CATALOG_ID(O_EXPLOSIVE_OPTION) X_CATALOG_ID(O_SMALL_MEDIPACK_OPTION) X_CATALOG_ID(O_LARGE_MEDIPACK_OPTION) X_CATALOG_ID(O_PUZZLE_ITEM_1) X_CATALOG_ID(O_PUZZLE_ITEM_2) X_CATALOG_ID(O_PUZZLE_ITEM_3) X_CATALOG_ID(O_PUZZLE_ITEM_4) X_CATALOG_ID(O_PUZZLE_OPTION_1) X_CATALOG_ID(O_PUZZLE_OPTION_2) X_CATALOG_ID(O_PUZZLE_OPTION_3) X_CATALOG_ID(O_PUZZLE_OPTION_4) X_CATALOG_ID(O_PUZZLE_HOLE_1) X_CATALOG_ID(O_PUZZLE_HOLE_2) X_CATALOG_ID(O_PUZZLE_HOLE_3) X_CATALOG_ID(O_PUZZLE_HOLE_4) X_CATALOG_ID(O_PUZZLE_DONE_1) X_CATALOG_ID(O_PUZZLE_DONE_2) X_CATALOG_ID(O_PUZZLE_DONE_3) X_CATALOG_ID(O_PUZZLE_DONE_4) X_CATALOG_ID(O_LEADBAR_ITEM) X_CATALOG_ID(O_LEADBAR_OPTION) X_CATALOG_ID(O_MIDAS_TOUCH) X_CATALOG_ID(O_KEY_ITEM_1) X_CATALOG_ID(O_KEY_ITEM_2) X_CATALOG_ID(O_KEY_ITEM_3) X_CATALOG_ID(O_KEY_ITEM_4) X_CATALOG_ID(O_KEY_OPTION_1) X_CATALOG_ID(O_KEY_OPTION_2) X_CATALOG_ID(O_KEY_OPTION_3) X_CATALOG_ID(O_KEY_OPTION_4) X_CATALOG_ID(O_KEY_HOLE_1) X_CATALOG_ID(O_KEY_HOLE_2) X_CATALOG_ID(O_KEY_HOLE_3) X_CATALOG_ID(O_KEY_HOLE_4) X_CATALOG_ID(O_PICKUP_ITEM_1) X_CATALOG_ID(O_PICKUP_ITEM_2) X_CATALOG_ID(O_SCION_ITEM_1) X_CATALOG_ID(O_SCION_ITEM_2) X_CATALOG_ID(O_SCION_ITEM_3) X_CATALOG_ID(O_SCION_ITEM_4) X_CATALOG_ID(O_SCION_HOLDER) X_CATALOG_ID(O_PICKUP_OPTION_1) X_CATALOG_ID(O_PICKUP_OPTION_2) X_CATALOG_ID(O_SCION_OPTION) X_CATALOG_ID(O_EXPLOSION_1) X_CATALOG_ID(O_EXPLOSION_2) X_CATALOG_ID(O_SPLASH_1) X_CATALOG_ID(O_SPLASH_2) X_CATALOG_ID(O_BUBBLE_1) X_CATALOG_ID(O_BUBBLE_2) X_CATALOG_ID(O_BUBBLE_EMITTER) X_CATALOG_ID(O_BLOOD) X_CATALOG_ID(O_BLOOD_PINK) X_CATALOG_ID(O_DART_EFFECT) X_CATALOG_ID(O_CENTAUR_STATUE) X_CATALOG_ID(O_PORTACABIN) X_CATALOG_ID(O_PODS) X_CATALOG_ID(O_RICOCHET) X_CATALOG_ID(O_TWINKLE) X_CATALOG_ID(O_GUN_FLASH) X_CATALOG_ID(O_DUST) X_CATALOG_ID(O_BODY_PART) X_CATALOG_ID(O_CAMERA_TARGET) X_CATALOG_ID(O_WATERFALL) X_CATALOG_ID(O_NATLA_GUN) X_CATALOG_ID(O_MISSILE_ATLANTEAN_SHARD) X_CATALOG_ID(O_MISSILE_ATLANTEAN_BOMB) X_CATALOG_ID(O_EMBER) X_CATALOG_ID(O_EMBER_EMITTER) X_CATALOG_ID(O_FLAME) X_CATALOG_ID(O_FLAME_EMITTER) X_CATALOG_ID(O_FLAME_EMITTER_BIG) X_CATALOG_ID(O_FLAME_EMITTER_SMALL) X_CATALOG_ID(O_FLAME_EMITTER_JET) X_CATALOG_ID(O_FLAME_EMITTER_SIDE) X_CATALOG_ID(O_LAVA_WEDGE) X_CATALOG_ID(O_BIG_POD) X_CATALOG_ID(O_MOTOR_BOAT) X_CATALOG_ID(O_EARTHQUAKE) X_CATALOG_ID(O_SKYBOX) X_CATALOG_ID(O_PICKUP_AID) X_CATALOG_ID(O_GLOW) X_CATALOG_ID(O_LARA_HAIR) X_CATALOG_ID(O_ALPHABET) X_CATALOG_ID(O_ALPHABET_SMALL) X_CATALOG_ID(O_LARA_M16) X_CATALOG_ID(O_LARA_GRENADE_GUN) X_CATALOG_ID(O_LARA_HARPOON_GUN) X_CATALOG_ID(O_LARA_FLARE) X_CATALOG_ID(O_LARA_SKIDOO) X_CATALOG_ID(O_LARA_BOAT) X_CATALOG_ID(O_SKIDOO_FAST) X_CATALOG_ID(O_BOAT) X_CATALOG_ID(O_DOG) X_CATALOG_ID(O_CULT_1) X_CATALOG_ID(O_CULT_1A) X_CATALOG_ID(O_CULT_1B) X_CATALOG_ID(O_CULT_2) X_CATALOG_ID(O_CULT_3) X_CATALOG_ID(O_MOUSE) X_CATALOG_ID(O_DRAGON_FRONT) X_CATALOG_ID(O_DRAGON_BACK) X_CATALOG_ID(O_GONDOLA) X_CATALOG_ID(O_SHARK) X_CATALOG_ID(O_EEL) X_CATALOG_ID(O_BIG_EEL) X_CATALOG_ID(O_BARRACUDA) X_CATALOG_ID(O_DIVER) X_CATALOG_ID(O_WORKER_1) X_CATALOG_ID(O_WORKER_2) X_CATALOG_ID(O_WORKER_3) X_CATALOG_ID(O_WORKER_4) X_CATALOG_ID(O_WORKER_5) X_CATALOG_ID(O_JELLY) X_CATALOG_ID(O_SPIDER) X_CATALOG_ID(O_BIG_SPIDER) X_CATALOG_ID(O_CROW) X_CATALOG_ID(O_TIGER) X_CATALOG_ID(O_BARTOLI) X_CATALOG_ID(O_XIAN_SPEARMAN) X_CATALOG_ID(O_XIAN_SPEARMAN_STATUE) X_CATALOG_ID(O_XIAN_KNIGHT) X_CATALOG_ID(O_XIAN_KNIGHT_STATUE) X_CATALOG_ID(O_YETI) X_CATALOG_ID(O_BIRD_GUARDIAN) X_CATALOG_ID(O_EAGLE) X_CATALOG_ID(O_BANDIT_1) X_CATALOG_ID(O_BANDIT_2) X_CATALOG_ID(O_BANDIT_2B) X_CATALOG_ID(O_SKIDOO_ARMED) X_CATALOG_ID(O_SKIDOO_DRIVER) X_CATALOG_ID(O_MONK_1) X_CATALOG_ID(O_MONK_2) X_CATALOG_ID(O_FALLING_BLOCK_2) X_CATALOG_ID(O_FALLING_BLOCK_3) X_CATALOG_ID(O_LIFT) X_CATALOG_ID(O_GENERAL) X_CATALOG_ID(O_BIG_BOWL) X_CATALOG_ID(O_SMASH_OBJECT_1) X_CATALOG_ID(O_SMASH_OBJECT_2) X_CATALOG_ID(O_SMASH_OBJECT_3) X_CATALOG_ID(O_SMASH_OBJECT_4) X_CATALOG_ID(O_PROPELLER_1) X_CATALOG_ID(O_POWER_SAW) X_CATALOG_ID(O_HOOK) X_CATALOG_ID(O_SPINNING_BLADE) X_CATALOG_ID(O_BLADE) X_CATALOG_ID(O_KILLER_STATUE) X_CATALOG_ID(O_ROLLING_BALL_2) X_CATALOG_ID(O_ICICLE) X_CATALOG_ID(O_SPIKE_WALL) X_CATALOG_ID(O_SPRINGBOARD) X_CATALOG_ID(O_CEILING_SPIKES) X_CATALOG_ID(O_BELL) X_CATALOG_ID(O_WATER_SPRITE) X_CATALOG_ID(O_SNOW_SPRITE) X_CATALOG_ID(O_SKIDOO_TRACK) X_CATALOG_ID(O_SWITCH_TYPE_AIRLOCK) X_CATALOG_ID(O_SWITCH_TYPE_SMALL) X_CATALOG_ID(O_PROPELLER_2) X_CATALOG_ID(O_PROPELLER_3) X_CATALOG_ID(O_PENDULUM_2) X_CATALOG_ID(O_MESH_SWAP_1) X_CATALOG_ID(O_MESH_SWAP_2) X_CATALOG_ID(O_TEXT_BOX) X_CATALOG_ID(O_ROLLING_BALL_3) X_CATALOG_ID(O_ZIPLINE_HANDLE) X_CATALOG_ID(O_SWITCH_TYPE_BUTTON) X_CATALOG_ID(O_PLAYER_5) X_CATALOG_ID(O_PLAYER_6) X_CATALOG_ID(O_PLAYER_7) X_CATALOG_ID(O_PLAYER_8) X_CATALOG_ID(O_PLAYER_9) X_CATALOG_ID(O_PLAYER_10) X_CATALOG_ID(O_PDA_OPTION) X_CATALOG_ID(O_HARPOON_ITEM) X_CATALOG_ID(O_M16_ITEM) X_CATALOG_ID(O_GRENADE_GUN_ITEM) X_CATALOG_ID(O_HARPOON_AMMO_ITEM) X_CATALOG_ID(O_M16_AMMO_ITEM) X_CATALOG_ID(O_GRENADE_AMMO_ITEM) X_CATALOG_ID(O_FLAREBOX_ITEM) X_CATALOG_ID(O_FLARE_ITEM) X_CATALOG_ID(O_HARPOON_OPTION) X_CATALOG_ID(O_M16_OPTION) X_CATALOG_ID(O_GRENADE_GUN_OPTION) X_CATALOG_ID(O_HARPOON_AMMO_OPTION) X_CATALOG_ID(O_M16_AMMO_OPTION) X_CATALOG_ID(O_GRENADE_AMMO_OPTION) X_CATALOG_ID(O_FLAREBOX_OPTION) X_CATALOG_ID(O_SECRET_1) X_CATALOG_ID(O_SECRET_2) X_CATALOG_ID(O_SECRET_3) X_CATALOG_ID(O_SPHERE_OF_DOOM_1) X_CATALOG_ID(O_SPHERE_OF_DOOM_2) X_CATALOG_ID(O_SPHERE_OF_DOOM_3) X_CATALOG_ID(O_ALARM_SOUND) X_CATALOG_ID(O_BIRD_TWEETER_1) X_CATALOG_ID(O_BIRD_TWEETER_2) X_CATALOG_ID(O_CLOCK_CHIMES) X_CATALOG_ID(O_DRAGON_BONES_1) X_CATALOG_ID(O_DRAGON_BONES_2) X_CATALOG_ID(O_DRAGON_BONES_3) X_CATALOG_ID(O_HOT_LIQUID) X_CATALOG_ID(O_SHADOW) X_CATALOG_ID(O_BOAT_BITS) X_CATALOG_ID(O_MINE) X_CATALOG_ID(O_INV_BACKGROUND) X_CATALOG_ID(O_FX_RESERVED) X_CATALOG_ID(O_GONG_BONGER) X_CATALOG_ID(O_GONG) X_CATALOG_ID(O_DETONATOR_BOX) X_CATALOG_ID(O_COPTER) X_CATALOG_ID(O_FLARE_FIRE) X_CATALOG_ID(O_GLOW_RESERVED) X_CATALOG_ID(O_M16_FLASH) X_CATALOG_ID(O_GUN_SHELL) X_CATALOG_ID(O_SHOTGUN_SHELL) X_CATALOG_ID(O_MISSILE_HARPOON) X_CATALOG_ID(O_MISSILE_POISON) X_CATALOG_ID(O_MISSILE_FLAME) X_CATALOG_ID(O_MISSILE_KNIFE) X_CATALOG_ID(O_GRENADE) X_CATALOG_ID(O_HARPOON_BOLT) X_CATALOG_ID(O_DYING_MONK) X_CATALOG_ID(O_DING_DONG) X_CATALOG_ID(O_LARA_ALARM) X_CATALOG_ID(O_MINI_COPTER) X_CATALOG_ID(O_WINSTON) X_CATALOG_ID(O_WINSTON_ARMY) X_CATALOG_ID(O_ASSAULT_DIGITS) X_CATALOG_ID(O_COMBAT_END) X_CATALOG_ID(O_CUT_SHOTGUN) X_CATALOG_ID(O_MONK_3) X_CATALOG_ID(O_LARA_SKIN_SWAP_1) X_CATALOG_ID(O_LARA_SKIN_SWAP_2) X_CATALOG_ID(O_LARA_SKIN_SWAP_3) X_CATALOG_ID(O_LARA_SKIN_SWAP_4) X_CATALOG_ID(O_LARA_SKIN_SWAP_5) X_CATALOG_ID(O_LARA_SKIN_SWAP_6) X_CATALOG_ID(O_LARA_SKIN_SWAP_7) X_CATALOG_ID(O_LARA_SKIN_SWAP_8) X_CATALOG_ID(O_LARA_SKIN_SWAP_9) X_CATALOG_ID(O_LARA_SKIN_SWAP_10) X_CATALOG_ID(O_LARA_SKIN_SWAP_11) X_CATALOG_ID(O_LARA_SKIN_SWAP_12) X_CATALOG_ID(O_LARA_SKIN_SWAP_13) X_CATALOG_ID(O_LARA_SKIN_SWAP_14) X_CATALOG_ID(O_LARA_SKIN_SWAP_15) X_CATALOG_ID(O_LARA_SKIN_SWAP_16) X_CATALOG_ID(O_LARA_SKIN_SWAP_17) X_CATALOG_ID(O_LARA_SKIN_SWAP_18) X_CATALOG_ID(O_LARA_SKIN_SWAP_19) X_CATALOG_ID(O_LARA_SKIN_SWAP_20) X_CATALOG_ID(O_LARA_SKIN_SWAP_21) X_CATALOG_ID(O_LARA_SKIN_SWAP_22) X_CATALOG_ID(O_LARA_SKIN_SWAP_23) X_CATALOG_ID(O_LARA_SKIN_SWAP_24) X_CATALOG_ID(O_LARA_SKIN_SWAP_25) X_CATALOG_ID(O_LARA_SKIN_SWAP_26) X_CATALOG_ID(O_LARA_SKIN_SWAP_27) X_CATALOG_ID(O_LARA_SKIN_SWAP_28) X_CATALOG_ID(O_LARA_SKIN_SWAP_29) X_CATALOG_ID(O_LARA_SKIN_SWAP_30) X_CATALOG_ID(O_LARA_SKIN_SWAP_31) X_CATALOG_ID(O_LARA_SKIN_SWAP_32) X_CATALOG_ID(O_LARA_SKIN_SWAP_EXTRA) X_CATALOG_ID(O_LARA_SKIN_SWAP_GUNS) X_CATALOG_ID(O_LARA_SKIN_SWAP_LEGS) X_CATALOG_ID(O_LARA_SKIN) X_CATALOG_ID(O_COBRA) X_CATALOG_ID(O_SWINGING_AXE) X_CATALOG_ID(O_SECRET_1_OPTION) X_CATALOG_ID(O_SECRET_2_OPTION) X_CATALOG_ID(O_SECRET_3_OPTION) X_CATALOG_ID(O_LARA_AUTOS) X_CATALOG_ID(O_AUTOS_ITEM) X_CATALOG_ID(O_AUTOS_AMMO_ITEM) X_CATALOG_ID(O_AUTOS_OPTION) X_CATALOG_ID(O_AUTOS_AMMO_OPTION) X_CATALOG_ID(O_LARA_DESERT_EAGLE) X_CATALOG_ID(O_DESERT_EAGLE_ITEM) X_CATALOG_ID(O_DESERT_EAGLE_AMMO_ITEM) X_CATALOG_ID(O_DESERT_EAGLE_OPTION) X_CATALOG_ID(O_DESERT_EAGLE_AMMO_OPTION) X_CATALOG_ID(O_LARA_MP5) X_CATALOG_ID(O_MP5_ITEM) X_CATALOG_ID(O_MP5_AMMO_ITEM) X_CATALOG_ID(O_MP5_OPTION) X_CATALOG_ID(O_MP5_AMMO_OPTION) X_CATALOG_ID(O_LARA_ROCKET_GUN) X_CATALOG_ID(O_ROCKET_GUN_ITEM) X_CATALOG_ID(O_ROCKET_AMMO_ITEM) X_CATALOG_ID(O_ROCKET_GUN_OPTION) X_CATALOG_ID(O_ROCKET_AMMO_OPTION) X_CATALOG_ID(O_ROCKET) X_CATALOG_ID(O_HEAVY_ROCKET) X_CATALOG_ID(O_DUMMY) X_CATALOG_ID(O_SNOWFLAKE) X_CATALOG_ID(O_ANIMATING_1) X_CATALOG_ID(O_ANIMATING_2) X_CATALOG_ID(O_ANIMATING_3) X_CATALOG_ID(O_ANIMATING_4) X_CATALOG_ID(O_ANIMATING_5) X_CATALOG_ID(O_ANIMATING_6) X_CATALOG_ID(O_ANIMATING_7) X_CATALOG_ID(O_ANIMATING_8) X_CATALOG_ID(O_ANIMATING_9) X_CATALOG_ID(O_ANIMATING_10) X_CATALOG_ID(O_SMOKE_EMITTER_WHITE) X_CATALOG_ID(O_SMOKE_EMITTER_BLACK) X_CATALOG_ID(O_STEAM_EMITTER) X_CATALOG_ID(O_ASSAULT_TARGET) X_CATALOG_ID(O_ELECTRICAL_LIGHT) X_CATALOG_ID(O_FLICKERING_LIGHT) X_CATALOG_ID(O_STROBE_LIGHT) X_CATALOG_ID(O_ON_OFF_LIGHT) X_CATALOG_ID(O_PULSE_LIGHT) X_CATALOG_ID(O_BEACON_LIGHT) X_CATALOG_ID(O_RED_LIGHT) X_CATALOG_ID(O_GREEN_LIGHT) X_CATALOG_ID(O_BLUE_LIGHT) X_CATALOG_ID(O_AMBER_LIGHT) X_CATALOG_ID(O_WHITE_LIGHT) X_CATALOG_ID(O_QUAD_BIKE) X_CATALOG_ID(O_KAYAK) X_CATALOG_ID(O_LARA_VEHICLE_ANIM) X_CATALOG_ID(O_MOUNTED_GUN) X_CATALOG_ID(O_TROPICAL_FISH) X_CATALOG_ID(O_PIRAHNAS) X_CATALOG_ID(O_QUEST_ITEM_1) X_CATALOG_ID(O_QUEST_ITEM_2) X_CATALOG_ID(O_QUEST_ITEM_3) X_CATALOG_ID(O_QUEST_ITEM_4) X_CATALOG_ID(O_QUEST_OPTION_1) X_CATALOG_ID(O_QUEST_OPTION_2) X_CATALOG_ID(O_QUEST_OPTION_3) X_CATALOG_ID(O_QUEST_OPTION_4) X_CATALOG_ID(O_AI_AMBUSH) X_CATALOG_ID(O_AI_GUARD) X_CATALOG_ID(O_AI_FOLLOW) X_CATALOG_ID(O_AI_PATROL_1) X_CATALOG_ID(O_AI_PATROL_2) X_CATALOG_ID(O_AI_MODIFY) X_CATALOG_ID(O_AI_X1) X_CATALOG_ID(O_AI_X2) X_CATALOG_ID(O_AI_X3) X_CATALOG_ID(O_TONY) X_CATALOG_ID(O_TONY_FIRE_BALL) X_CATALOG_ID(O_WILLARD) X_CATALOG_ID(O_WILLARD_PLASMA_BALL) X_CATALOG_ID(O_GLOBE_SELECT_OPTION) X_CATALOG_ID(O_KILL_ALL_TRIGGERED) X_CATALOG_ID(O_MONKEY) X_CATALOG_ID(O_MESH_SWAP_3) X_CATALOG_ID(O_VULTURE) X_CATALOG_ID(O_ROLLING_BALL_4) X_CATALOG_ID(O_POISON_DART_EMITTER) X_CATALOG_ID(O_POISON_DART) X_CATALOG_ID(O_SHIVA) X_CATALOG_ID(O_ELECTRIC_FENCE) X_CATALOG_ID(O_SWITCH_TYPE_WHEEL) X_CATALOG_ID(O_TRIBE_AXEMAN) X_CATALOG_ID(O_TRIBE_BOSS) X_CATALOG_ID(O_TRIBE_PIPEMAN) X_CATALOG_ID(O_LIZARD) X_CATALOG_ID(O_CARCASS) X_CATALOG_ID(O_BAT_EMITTER) X_CATALOG_ID(O_TREX_ALPHA) X_CATALOG_ID(O_RAPTOR_EMITTER) X_CATALOG_ID(O_STHPAC_MERCENARY) X_CATALOG_ID(O_TRAIN) X_CATALOG_ID(O_PATROL_DOG) X_CATALOG_ID(O_HUSKIE) X_CATALOG_ID(O_ELECTRIC_CLEANER) X_CATALOG_ID(O_PUNK_1) X_CATALOG_ID(O_PUNK_2) X_CATALOG_ID(O_UPV) X_CATALOG_ID(O_SECURITY_GUARD) X_CATALOG_ID(O_GAS_EMITTER_GREEN) X_CATALOG_ID(O_SWAT_1) X_CATALOG_ID(O_SWAT_2) X_CATALOG_ID(O_SWAT_3) X_CATALOG_ID(O_FUSE_BOX) X_CATALOG_ID(O_SOPHIA) X_CATALOG_ID(O_SOPHIA_LASER_BOLT) X_CATALOG_ID(O_SOPHIA_PLASMA_BALL) X_CATALOG_ID(O_TRIGGER_GATE) X_CATALOG_ID(O_ROTATING_LASER) X_CATALOG_ID(O_SECURITY_LASER_ALARM) X_CATALOG_ID(O_SECURITY_LASER_DEADLY) X_CATALOG_ID(O_SECURITY_LASER_KILLER) X_CATALOG_ID(O_SENTRY_GUN) X_CATALOG_ID(O_CIVILIAN) X_CATALOG_ID(O_PRISONER) X_CATALOG_ID(O_MP_1) X_CATALOG_ID(O_MP_2) X_CATALOG_ID(O_ORCA) X_CATALOG_ID(O_AREA_51_ROCKET) X_CATALOG_ID(O_AREA_51_ROCKET_BLAST) X_CATALOG_ID(O_AREA_51_ROCKET_SUPPORT) X_CATALOG_ID(O_MINE_CART) X_CATALOG_ID(O_RX_WORKER_1) X_CATALOG_ID(O_RX_WORKER_2) X_CATALOG_ID(O_RX_WORKER_3) X_CATALOG_ID(O_CRAWLER_MUTANT) X_CATALOG_ID(O_DYING_MUTANT) X_CATALOG_ID(O_HYBRID_MUTANT) X_CATALOG_ID(O_WASP_MUTANT) X_CATALOG_ID(O_WASP_MUTANT_EMITTER) X_CATALOG_ID(O_CLAW_MUTANT) X_CATALOG_ID(O_CLAW_MUTANT_PLASMA_BALL) X_CATALOG_ID(O_DISPOSABLE_ANIMATING_1) X_CATALOG_ID(O_DISPOSABLE_ANIMATING_2) X_CATALOG_ID(O_DISPOSABLE_ANIMATING_3) X_CATALOG_ID(O_DISPOSABLE_ANIMATING_4) X_CATALOG_ID(O_DISPOSABLE_ANIMATING_5) X_CATALOG_ID(O_DISPOSABLE_ANIMATING_6) X_CATALOG_ID(O_DISPOSABLE_ANIMATING_7) X_CATALOG_ID(O_DISPOSABLE_ANIMATING_8) X_CATALOG_ID(O_DISPOSABLE_ANIMATING_9) X_CATALOG_ID(O_DISPOSABLE_ANIMATING_10) X_CATALOG_ID(O_FIRE_HEAD) X_CATALOG_ID(O_RIB) ================================================ FILE: src/trx/game/catalog/samples.def ================================================ X_CATALOG_ID(SFX_LARA_NO) X_CATALOG_ID(SFX_LARA_DRAW) X_CATALOG_ID(SFX_LARA_HOLSTER) X_CATALOG_ID(SFX_LARA_PISTOLS) X_CATALOG_ID(SFX_LARA_RELOAD) X_CATALOG_ID(SFX_LARA_RICOCHET) X_CATALOG_ID(SFX_BEAR_FEET) X_CATALOG_ID(SFX_BEAR_SNARL) X_CATALOG_ID(SFX_BEAR_HURT) X_CATALOG_ID(SFX_WOLF_HURT) X_CATALOG_ID(SFX_LARA_CLIMB_3) X_CATALOG_ID(SFX_LARA_BODYSL) X_CATALOG_ID(SFX_LARA_FALL) X_CATALOG_ID(SFX_LARA_INJURY) X_CATALOG_ID(SFX_LARA_SPLASH) X_CATALOG_ID(SFX_LARA_BREATH) X_CATALOG_ID(SFX_LARA_BUBBLES) X_CATALOG_ID(SFX_LARA_KEY) X_CATALOG_ID(SFX_LARA_UZI_FIRE) X_CATALOG_ID(SFX_LARA_UZI_STOP) X_CATALOG_ID(SFX_LARA_GENERAL_DEATH) X_CATALOG_ID(SFX_LARA_FALL_DEATH) X_CATALOG_ID(SFX_LARA_MAGNUMS) X_CATALOG_ID(SFX_LARA_AUTOS) X_CATALOG_ID(SFX_LARA_DESERT_EAGLE) X_CATALOG_ID(SFX_LARA_SHOTGUN) X_CATALOG_ID(SFX_LARA_EMPTY) X_CATALOG_ID(SFX_LARA_SHOTGUN_SHELL) X_CATALOG_ID(SFX_LARA_BULLETHIT) X_CATALOG_ID(SFX_UNDERWATER) X_CATALOG_ID(SFX_PUSHBLOCK_LAND) X_CATALOG_ID(SFX_EARTHQUAKE_1) X_CATALOG_ID(SFX_EARTHQUAKE_2) X_CATALOG_ID(SFX_EARTHQUAKE_LOOP) X_CATALOG_ID(SFX_WATERFALL_LOOP) X_CATALOG_ID(SFX_WATERFALL_BIG) X_CATALOG_ID(SFX_FLOOD) X_CATALOG_ID(SFX_LION_HURT) X_CATALOG_ID(SFX_RAT_CHIRP) X_CATALOG_ID(SFX_THUNDER) X_CATALOG_ID(SFX_EXPLOSION_2) X_CATALOG_ID(SFX_DAMOCLES_SWORD) X_CATALOG_ID(SFX_EXPLOSION_1) X_CATALOG_ID(SFX_MENU_ROTATE) X_CATALOG_ID(SFX_MENU_LARA_HOME) X_CATALOG_ID(SFX_MENU_GAMEBOY) X_CATALOG_ID(SFX_MENU_SPININ) X_CATALOG_ID(SFX_MENU_SPINOUT) X_CATALOG_ID(SFX_MENU_COMPASS) X_CATALOG_ID(SFX_MENU_GUNS) X_CATALOG_ID(SFX_MENU_PASSPORT) X_CATALOG_ID(SFX_MENU_MEDI) X_CATALOG_ID(SFX_MENU_CHOOSE) X_CATALOG_ID(SFX_RAISINGBLOCK_FX) X_CATALOG_ID(SFX_SAND_FX) X_CATALOG_ID(SFX_STAIRS_2_SLOPE_FX) X_CATALOG_ID(SFX_ATLANTEAN_NEEDLE) X_CATALOG_ID(SFX_SKATEBOARD_HIT) X_CATALOG_ID(SFX_TORSO_HIT) X_CATALOG_ID(SFX_ROLLING_BALL_1_ROLL) X_CATALOG_ID(SFX_ROLLING_BALL_1_STOP) X_CATALOG_ID(SFX_LAVA_FOUNTAIN) X_CATALOG_ID(SFX_LOOP_FOR_SMALL_FIRES) X_CATALOG_ID(SFX_DART) X_CATALOG_ID(SFX_DISC) X_CATALOG_ID(SFX_POWERUP_FX) X_CATALOG_ID(SFX_TRAPDOOR_OPEN) X_CATALOG_ID(SFX_EXPLOSION_FX) X_CATALOG_ID(SFX_ATLANTEAN_DEATH) X_CATALOG_ID(SFX_CHAINBLOCK_FX) X_CATALOG_ID(SFX_SECRET) X_CATALOG_ID(SFX_BALDY_SPEECH) X_CATALOG_ID(SFX_COWBOY_SPEECH) X_CATALOG_ID(SFX_LARSON_SPEECH) X_CATALOG_ID(SFX_NATLA_SPEECH) X_CATALOG_ID(SFX_PIERRE_SPEECH) X_CATALOG_ID(SFX_SKATEKID_SPEECH) X_CATALOG_ID(SFX_LARA_FLARE_IGNITE) X_CATALOG_ID(SFX_LARA_FLARE_BURN) X_CATALOG_ID(SFX_MASSIVE_CRASH) X_CATALOG_ID(SFX_CLICK) X_CATALOG_ID(SFX_GLASS_BREAK) X_CATALOG_ID(SFX_ENEMY_HIT_1) X_CATALOG_ID(SFX_ENEMY_HIT_2) X_CATALOG_ID(SFX_M16_FIRE) X_CATALOG_ID(SFX_M16_STOP) X_CATALOG_ID(SFX_MP5_FIRE) X_CATALOG_ID(SFX_ROCKET_FIRE) X_CATALOG_ID(SFX_MENU_STOPWATCH) X_CATALOG_ID(SFX_SANDBAG_HIT) X_CATALOG_ID(SFX_SKIDOO_IDLE) X_CATALOG_ID(SFX_SKIDOO_MOVING) X_CATALOG_ID(SFX_PULLEY_CRANE) X_CATALOG_ID(SFX_CURTAIN) X_CATALOG_ID(SFX_BOAT_IDLE) X_CATALOG_ID(SFX_BOAT_MOVING) X_CATALOG_ID(SFX_CLATTER_1) X_CATALOG_ID(SFX_CLATTER_2) X_CATALOG_ID(SFX_CLATTER_3) X_CATALOG_ID(SFX_SPIKE_WALL) X_CATALOG_ID(SFX_LARA_FLESH_WOUND) X_CATALOG_ID(SFX_SAW_REVVING) X_CATALOG_ID(SFX_SAW_STOP) X_CATALOG_ID(SFX_DOOR_CHIME) X_CATALOG_ID(SFX_AIRPLANE_IDLE) X_CATALOG_ID(SFX_UNDERWATER_FAN_ON) X_CATALOG_ID(SFX_UNDERWATER_FAN_OFF) X_CATALOG_ID(SFX_SMALL_FAN_ON) X_CATALOG_ID(SFX_ROLLING_BALL_2_ROLL) X_CATALOG_ID(SFX_ROLLING_BALL_2_STOP) X_CATALOG_ID(SFX_ROLLING_BALL_3_ROLL) X_CATALOG_ID(SFX_ROLLING_BALL_3_STOP) X_CATALOG_ID(SFX_ROLLING_BLADE) X_CATALOG_ID(SFX_MONK_CRUNCH) X_CATALOG_ID(SFX_PROJECTILE_HIT) X_CATALOG_ID(SFX_CHAIN_PULLEY) X_CATALOG_ID(SFX_ZIPLINE_GO) X_CATALOG_ID(SFX_ZIPLINE_STOP) X_CATALOG_ID(SFX_BOWL_POUR) X_CATALOG_ID(SFX_WATERFALL_2) X_CATALOG_ID(SFX_HELICOPTER_LOOP) X_CATALOG_ID(SFX_DRAGON_FEET) X_CATALOG_ID(SFX_DRAGON_FIRE) X_CATALOG_ID(SFX_WARRIOR_HOVER) X_CATALOG_ID(SFX_BIRDS_CHIRP) X_CATALOG_ID(SFX_CRUNCH_1) X_CATALOG_ID(SFX_CRUNCH_2) X_CATALOG_ID(SFX_DRIPS_REVERB) X_CATALOG_ID(SFX_STAGE_BACKDROP) X_CATALOG_ID(SFX_STONE_DOOR_SLIDE) X_CATALOG_ID(SFX_PLATFORM_ALARM) X_CATALOG_ID(SFX_DOORBELL) X_CATALOG_ID(SFX_BURGLAR_ALARM) X_CATALOG_ID(SFX_BOAT_ENGINE) X_CATALOG_ID(SFX_BOAT_INTO_WATER) X_CATALOG_ID(SFX_BOILER) X_CATALOG_ID(SFX_MARCO_BARTOLLI_TRANSFORM) X_CATALOG_ID(SFX_WINSTON_GRUNT_1) X_CATALOG_ID(SFX_WINSTON_GRUNT_2) X_CATALOG_ID(SFX_WINSTON_GRUNT_3) X_CATALOG_ID(SFX_WINSTON_CUPS) X_CATALOG_ID(SFX_BRITTLE_GROUND_BREAK) X_CATALOG_ID(SFX_SPIDER_EXPLODE) X_CATALOG_ID(SFX_FOOTSTEPS_MUD) X_CATALOG_ID(SFX_FOOTSTEPS_ICE) X_CATALOG_ID(SFX_FOOTSTEPS_GRAVEL) X_CATALOG_ID(SFX_FOOTSTEPS_SAND_OR_GRASS) X_CATALOG_ID(SFX_FOOTSTEPS_WOOD) X_CATALOG_ID(SFX_FOOTSTEPS_SNOW) X_CATALOG_ID(SFX_FOOTSTEPS_METAL) X_CATALOG_ID(SFX_TARGET_HITS) X_CATALOG_ID(SFX_TARGET_SMASH) X_CATALOG_ID(SFX_QUAD_FRONT_IMPACT) X_CATALOG_ID(SFX_QUAD_MOVE) X_CATALOG_ID(SFX_QUAD_IDLE) X_CATALOG_ID(SFX_EXPLOSION_3) X_CATALOG_ID(SFX_SAVE_CRYSTAL) X_CATALOG_ID(SFX_ICICLE) X_CATALOG_ID(SFX_BLAST_CIRCLE) X_CATALOG_ID(SFX_LARA_GET_OUT) X_CATALOG_ID(SFX_SHIVA_SWORD_1) X_CATALOG_ID(SFX_SHIVA_SWORD_2) X_CATALOG_ID(SFX_ROLLING_BALL_4_ROLL) X_CATALOG_ID(SFX_ROLLING_BALL_4_STOP) X_CATALOG_ID(SFX_BLOWPIPE_BLOW) X_CATALOG_ID(SFX_ALARM_1) X_CATALOG_ID(SFX_MACAQUE_ROLL) X_CATALOG_ID(SFX_LARA_FOOTSTEP) X_CATALOG_ID(SFX_LARA_BAREFOOT) X_CATALOG_ID(SFX_LARA_THUD) X_CATALOG_ID(SFX_BATS_1) X_CATALOG_ID(SFX_SHUTTERS_BREAK) X_CATALOG_ID(SFX_TRIBOSS_TAKE_HIT) X_CATALOG_ID(SFX_TRIBOSS_TURN_CHAIR) X_CATALOG_ID(SFX_AMERICAN_HOY) X_CATALOG_ID(SFX_ENGLISH_HOY) X_CATALOG_ID(SFX_TRAIN_LOOP) X_CATALOG_ID(SFX_CLEANER_FUSEBOX) X_CATALOG_ID(SFX_CLEANER_LOOP) X_CATALOG_ID(SFX_UPV_LOOP) X_CATALOG_ID(SFX_UPV_START) X_CATALOG_ID(SFX_UPV_STOP) X_CATALOG_ID(SFX_UPV_HARPOON) X_CATALOG_ID(SFX_SECURITY_GUARD_FIRE) X_CATALOG_ID(SFX_LONDON_SWAT_FIRE) X_CATALOG_ID(SFX_AMERICAN_SWAT_FIRE) X_CATALOG_ID(SFX_SOPHIA_SUMMON) X_CATALOG_ID(SFX_SOPHIA_TAKE_HIT) X_CATALOG_ID(SFX_SOPHIA_SUMMON_NOT) X_CATALOG_ID(SFX_LOWERING_BLOCK) X_CATALOG_ID(SFX_HUGE_ROCKET_LOOP) X_CATALOG_ID(SFX_SPANNER_CLUNK) X_CATALOG_ID(SFX_MINE_CART_CLUNK_START) X_CATALOG_ID(SFX_MINE_CART_SREECH_BRAKE) X_CATALOG_ID(SFX_MINE_CART_TRACK_LOOP) X_CATALOG_ID(SFX_MINE_CART_PULLY_LOOP) X_CATALOG_ID(SFX_FLAME_THROWER_LOOP) X_CATALOG_ID(SFX_RIB_MOVING) X_CATALOG_ID(SFX_RIB_IDLE) ================================================ FILE: src/trx/game/clock/common.c ================================================ #include #include #include #include #include #include #include #include #include #include static bool m_Disabled = false; static Uint64 m_LastCounter = 0; static Uint64 m_InitCounter = 0; static Uint64 m_Frequency = 0; static double m_Accumulator = 0.0; static struct { double real_time_at_last_change; double sim_time_at_last_change; double sim_speed; } m_Priv; // Fixed‐FPS simulation in headless mode static bool m_HeadlessFixedFPS = false; static double m_HeadlessFPS_DT = 0.0; static double m_HeadlessOffset = 0.0; static double m_HeadlessAnchor = 0.0; static double M_GetHighPrecisionCounter(void) { if (m_HeadlessFixedFPS) { // Return virtual time = anchor + offset return m_HeadlessAnchor + m_HeadlessOffset; } return (SDL_GetPerformanceCounter() - m_InitCounter) / (double)m_Frequency; } void Clock_Init(void) { m_Frequency = SDL_GetPerformanceFrequency(); m_InitCounter = SDL_GetPerformanceCounter(); } void Clock_DisableWait(void) { m_Disabled = true; } void Clock_EnableHeadlessFixedFPS(int32_t fps) { if (fps <= 0) { m_HeadlessFixedFPS = false; return; } // Anchor to current real time, reset offset m_HeadlessAnchor = M_GetHighPrecisionCounter(); m_HeadlessOffset = 0.0; m_HeadlessFPS_DT = 1.0 / (double)fps; m_HeadlessFixedFPS = true; } size_t Clock_GetDateTime(char *const buffer, const size_t size) { time_t lt = time(0); struct tm *tptr = localtime(<); return snprintf( buffer, size, "%04d%02d%02d_%02d%02d%02d", tptr->tm_year + 1900, tptr->tm_mon + 1, tptr->tm_mday, tptr->tm_hour, tptr->tm_min, tptr->tm_sec); } int32_t Clock_GetCurrentFPS(void) { return g_Config.rendering.fps; } int32_t Clock_GetFrameAdvance(void) { return Clock_GetCurrentFPS() == 30 ? 2 : 1; } void Clock_SyncTick(void) { m_LastCounter = SDL_GetPerformanceCounter(); m_Accumulator = 0.0; } int32_t Clock_WaitTick(void) { if (m_Disabled && m_HeadlessFixedFPS) { // Advance virtual time by one fixed frame m_HeadlessOffset += m_HeadlessFPS_DT; return 1; } if (m_Disabled) { return 1; } const Uint64 current_counter = SDL_GetPerformanceCounter(); // If this is the first call, just initialize and return a frame. if (m_LastCounter == 0) { m_LastCounter = current_counter; return 1; } const int32_t fps = Clock_GetCurrentFPS(); const double speed_multiplier = Clock_GetSpeedMultiplier(); // The duration of one frame in performance counter units const double frame_ticks = m_Frequency / (fps * speed_multiplier); // Calculate elapsed ticks since last call const double elapsed_ticks = (double)(current_counter - m_LastCounter); // Add the elapsed ticks to the accumulator m_Accumulator += elapsed_ticks; // Determine how many frames we can "release" from the accumulator int32_t frames = (int32_t)(m_Accumulator / frame_ticks); if (frames < 1) { // Not enough accumulated time for even one frame // Calculate how long we should wait (in ms) to hit the frame boundary double needed = frame_ticks - m_Accumulator; double delay_ms = (needed / m_Frequency) * 1000.0; if (delay_ms > 0) { SDL_Delay((Uint32)delay_ms); } // After waiting, measure again to be accurate const Uint64 after_delay_counter = SDL_GetPerformanceCounter(); const double after_delay_elapsed = (double)(after_delay_counter - current_counter); m_Accumulator += after_delay_elapsed; // Now, we should have at least one frame available frames = (int32_t)(m_Accumulator / frame_ticks); if (frames < 1) { // To avoid a possible floating-point corner case, ensure at least // one frame frames = 1; } } // Consume the frames from the m_Accumulator m_Accumulator -= frames * frame_ticks; // Update the last counter to the current performance counter m_LastCounter = SDL_GetPerformanceCounter(); return frames; } double Clock_GetRealTime(void) { return M_GetHighPrecisionCounter(); } double Clock_GetSimTime(void) { const double real_now = M_GetHighPrecisionCounter(); const double real_delta = real_now - m_Priv.real_time_at_last_change; return m_Priv.sim_time_at_last_change + real_delta * m_Priv.sim_speed; } void Clock_SetSimSpeed(const double new_speed) { // First, figure out how much sim time has passed so far const double prev_sim_time = Clock_GetSimTime(); // Then re-anchor the reference point m_Priv.real_time_at_last_change = M_GetHighPrecisionCounter(); m_Priv.sim_time_at_last_change = prev_sim_time; m_Priv.sim_speed = new_speed; } ================================================ FILE: src/trx/game/clock/common.h ================================================ #pragma once #include #include void Clock_Init(void); // Disables any kind of waiting in Clock_WaitTick void Clock_DisableWait(void); // In headless mode, simulate a fixed FPS (seconds per frame = 1/fps) void Clock_EnableHeadlessFixedFPS(int32_t fps); void Clock_SyncTick(void); int32_t Clock_WaitTick(void); size_t Clock_GetDateTime(char *buffer, size_t size); int32_t Clock_GetFrameAdvance(void); int32_t Clock_GetCurrentFPS(void); void Clock_SetSimSpeed(double new_speed); double Clock_GetRealTime(void); double Clock_GetSimTime(void); ================================================ FILE: src/trx/game/clock/const.h ================================================ #pragma once #define LOGIC_FPS 30 ================================================ FILE: src/trx/game/clock/timer.c ================================================ #include #include static double M_GetTime(const CLOCK_TIMER *const timer) { return timer->type == CLOCK_TIMER_REAL ? Clock_GetRealTime() : Clock_GetSimTime(); } void ClockTimer_Sync(CLOCK_TIMER *const timer) { timer->ref = M_GetTime(timer); } double ClockTimer_PeekElapsed(const CLOCK_TIMER *const timer) { return M_GetTime(timer) - timer->ref; } double ClockTimer_TakeElapsed(CLOCK_TIMER *const timer) { const double prev_time_sec = timer->ref; const double current_time_sec = M_GetTime(timer); timer->ref = current_time_sec; return current_time_sec - prev_time_sec; } bool ClockTimer_CheckElapsed(const CLOCK_TIMER *const timer, double sec) { return (M_GetTime(timer) - timer->ref) >= sec; } bool ClockTimer_CheckElapsedAndTake(CLOCK_TIMER *const timer, double sec) { const double current_time_sec = M_GetTime(timer); if ((current_time_sec - timer->ref) >= sec) { timer->ref = current_time_sec; return true; } return false; } ================================================ FILE: src/trx/game/clock/timer.h ================================================ #pragma once typedef enum { CLOCK_TIMER_SIM, CLOCK_TIMER_REAL, } CLOCK_TIMER_TYPE; typedef struct { double ref; CLOCK_TIMER_TYPE type; } CLOCK_TIMER; void ClockTimer_Sync(CLOCK_TIMER *timer); double ClockTimer_PeekElapsed(const CLOCK_TIMER *timer); double ClockTimer_TakeElapsed(CLOCK_TIMER *timer); bool ClockTimer_CheckElapsed(const CLOCK_TIMER *timer, double sec); bool ClockTimer_CheckElapsedAndTake(CLOCK_TIMER *timer, double sec); ================================================ FILE: src/trx/game/clock/turbo.c ================================================ #include #include #include #include #include #include #include #include void Clock_CycleTurboSpeed(const bool forward) { int32_t new_speed = Clock_GetTurboSpeed() + (forward ? 1 : -1); if (new_speed < CLOCK_TURBO_SPEED_MIN || new_speed > CLOCK_TURBO_SPEED_MAX) { new_speed = 0; } Clock_SetTurboSpeed(new_speed); } int32_t Clock_GetTurboSpeed(void) { return g_Config.gameplay.turbo_speed; } void Clock_SetTurboSpeed(int32_t value) { CLAMP(value, CLOCK_TURBO_SPEED_MIN, CLOCK_TURBO_SPEED_MAX); if (value == g_Config.gameplay.turbo_speed) { return; } g_Config.gameplay.turbo_speed = value; Config_Update(); Console_Log(GS("general/osd/speed_set"), value); Clock_SetSimSpeed(Clock_GetSpeedMultiplier()); } double Clock_GetSpeedMultiplier(void) { if (Clock_GetTurboSpeed() > 0) { return 1.0 + Clock_GetTurboSpeed(); } else if (Clock_GetTurboSpeed() < 0) { return pow(2.0, Clock_GetTurboSpeed()); } else { return 1.0; } } ================================================ FILE: src/trx/game/clock/turbo.h ================================================ #pragma once #include #define CLOCK_TURBO_SPEED_MIN -2 #define CLOCK_TURBO_SPEED_MAX 2 void Clock_CycleTurboSpeed(bool forward); int32_t Clock_GetTurboSpeed(void); void Clock_SetTurboSpeed(int32_t value); double Clock_GetSpeedMultiplier(void); ================================================ FILE: src/trx/game/clock.h ================================================ #pragma once #include #include #include #include ================================================ FILE: src/trx/game/collision/common.c ================================================ #include #include #include #include #include #include #include #include #include #include #define M_HEADROOM 160 // Additional collision space above Lara's head. static bool M_IsOnWalkable( const SECTOR *const sector, const XYZ_32 pos, const int32_t room_height) { return g_Config.gameplay.fix_bridge_collision && Room_IsOnWalkable(sector, pos, room_height, NO_ITEM); } // Probes the front, left, and right of Lara and fills in the collision info for // each side. The collision info depends on Lara's state. Her state determines // how big slope and lava pit sectors are treated. For example, in the walk // state, Lara won't walk up big slopes or walk down into lava pits. static void M_FillSide( const COLL_INFO *const coll, COLL_SIDE *const side, const XYZ_32 pos, const XZ_32 probe, const int32_t obj_height, int16_t *const room_num) { const int32_t y = pos.y - obj_height; const int32_t y_top = y - M_HEADROOM; int16_t local_room_num = *room_num; int16_t *const test_room_num = g_Config.gameplay.wall_glitch_mode == WALL_GLITCH_FIXED ? &local_room_num : room_num; const XYZ_32 sample_pos = { .x = pos.x + probe.x, .y = y_top, .z = pos.z + probe.z, }; const SECTOR *sector = Room_GetSector(sample_pos, test_room_num); int32_t height = Room_GetHeight(sector, sample_pos); int32_t ceiling = Room_GetCeiling(sector, sample_pos); const int32_t room_height = height; const int32_t room_ceiling = ceiling; const bool sim_wall = room_height == ceiling && room_height != NO_HEIGHT && !sector->ceiling.is_split && !sector->floor.is_split && sector->ceiling.tilt.x == 0 && sector->ceiling.tilt.z == 0 && sector->floor.tilt.x == 0 && sector->floor.tilt.z == 0; if (height != NO_HEIGHT) { height -= pos.y; } if (ceiling != NO_HEIGHT) { ceiling -= y; } side->floor = height; side->ceiling = ceiling; side->type = Room_GetHeightType(); const bool is_on_walkable = M_IsOnWalkable(sector, sample_pos, room_height); const bool is_front = side == &coll->side_front; if (is_front) { XYZ_32 front_probe_pos = sample_pos; front_probe_pos.x += probe.x; front_probe_pos.z += probe.z; sector = Room_GetSector(front_probe_pos, room_num); height = Room_GetHeight(sector, front_probe_pos); if (height != NO_HEIGHT) { height -= pos.y; } } if (!is_on_walkable) { if (coll->slopes_are_walls && (side->type == HT_BIG_SLOPE || side->type == HT_DIAGONAL) && side->floor < 0 && (!is_front || (side->floor < coll->side_mid.floor && height < side->floor))) { side->floor = -32767; } else if ( coll->slopes_are_pits && (side->type == HT_BIG_SLOPE || side->type == HT_DIAGONAL) && side->floor > (is_front ? coll->side_mid.floor : 0)) { side->floor = STEP_L * 2; } else if ( coll->lava_is_pit && side->floor > 0 && Room_GetPitSector(sector, pos.x, pos.z)->is_death_sector) { side->floor = STEP_L * 2; } } else if (sim_wall) { side->floor = NO_HEIGHT; side->ceiling = NO_HEIGHT; } } int32_t Collide_GetSpheres( const ITEM *const item, SPHERE *const spheres, const bool world_space) { if (item == nullptr) { return 0; } XYZ_32 pos; if (world_space) { pos = item->pos; Matrix_PushUnit(); } else { pos.x = 0; pos.y = 0; pos.z = 0; Matrix_Push(); Matrix_TranslateAbs32(item->pos); } Matrix_Rot16(item->rot); const ANIM_FRAME *const frame = Item_GetBestFrame(item); Matrix_TranslateRel16(frame->offset); Matrix_Rot16(frame->mesh_rots[0]); const OBJECT *const obj = Object_Get(item->object_id); const OBJECT_MESH *mesh = Object_GetMesh(obj->mesh_idx); Matrix_Push(); Matrix_TranslateRel16(mesh->center); spheres[0].pos.x = pos.x + (g_MatrixPtr->_03 >> W2V_SHIFT); spheres[0].pos.y = pos.y + (g_MatrixPtr->_13 >> W2V_SHIFT); spheres[0].pos.z = pos.z + (g_MatrixPtr->_23 >> W2V_SHIFT); spheres[0].r = mesh->radius; Matrix_Pop(); const int16_t *extra_rotation = item->extra_rotations; for (int32_t i = 1; i < obj->mesh_count; i++) { const ANIM_BONE *const bone = Object_GetBone(obj, i - 1); if (bone->matrix_pop) { Matrix_Pop(); } if (bone->matrix_push) { Matrix_Push(); } Matrix_TranslateRel32(bone->pos); Matrix_Rot16(frame->mesh_rots[i]); Object_ApplyExtraRotation(&extra_rotation, bone->rot, false); mesh = Object_GetMesh(obj->mesh_idx + i); Matrix_Push(); Matrix_TranslateRel16(mesh->center); SPHERE *const sphere = &spheres[i]; sphere->pos.x = pos.x + (g_MatrixPtr->_03 >> W2V_SHIFT); sphere->pos.y = pos.y + (g_MatrixPtr->_13 >> W2V_SHIFT); sphere->pos.z = pos.z + (g_MatrixPtr->_23 >> W2V_SHIFT); sphere->r = mesh->radius; Matrix_Pop(); } Matrix_Pop(); return obj->mesh_count; } int32_t Collide_TestCollision(ITEM *const item, const ITEM *const lara_item) { SPHERE slist_baddie[34]; SPHERE slist_lara[34]; uint32_t touch_bits = 0; int32_t num1 = Collide_GetSpheres(item, slist_baddie, true); int32_t num2 = Collide_GetSpheres(lara_item, slist_lara, true); for (int32_t i = 0; i < num1; i++) { const SPHERE *const ptr1 = &slist_baddie[i]; if (ptr1->r <= 0) { continue; } for (int32_t j = 0; j < num2; j++) { const SPHERE *const ptr2 = &slist_lara[j]; if (ptr2->r <= 0) { continue; } const int32_t dx = ptr2->pos.x - ptr1->pos.x; const int32_t dy = ptr2->pos.y - ptr1->pos.y; const int32_t dz = ptr2->pos.z - ptr1->pos.z; const int32_t d1 = SQUARE(dx) + SQUARE(dy) + SQUARE(dz); const int32_t d2 = SQUARE(ptr1->r + ptr2->r); if (d1 < d2) { touch_bits |= 1 << i; break; } } } item->touch_bits = touch_bits; return touch_bits; } void Collide_GetJointAbsPosition( const ITEM *const item, XYZ_32 *const out_vec, const int32_t joint) { const OBJECT *const obj = Object_Get(item->object_id); ANIM_FRAME *frames[2] = { nullptr, nullptr }; int32_t rate = 0; const int32_t frac = Item_GetFrames(item, frames, &rate); const bool use_item_interp = Interpolation_IsActive() && item->enable_interpolation; const XYZ_32 item_pos = use_item_interp ? item->interp.result.pos : item->pos; const XYZ_16 item_rot = use_item_interp ? item->interp.result.rot : item->rot; if (frames[0] == nullptr) { Matrix_PushUnit(); Matrix_Rot16(item_rot); Matrix_TranslateRel32(*out_vec); out_vec->x = item_pos.x + (g_MatrixPtr->_03 >> W2V_SHIFT); out_vec->y = item_pos.y + (g_MatrixPtr->_13 >> W2V_SHIFT); out_vec->z = item_pos.z + (g_MatrixPtr->_23 >> W2V_SHIFT); Matrix_Pop(); return; } const ANIM_FRAME *const frame_a = frames[0]; const ANIM_FRAME *const frame_b = frames[1]; const bool do_interp = frame_b != nullptr && frac != 0 && rate != 0; int32_t stack = 1; Matrix_PushUnit(); Matrix_Rot16(item_rot); if (do_interp) { Matrix_InitInterpolate(frac, rate); Matrix_TranslateRel16_ID(frame_a->offset, frame_b->offset); Matrix_Rot16_ID(frame_a->mesh_rots[0], frame_b->mesh_rots[0]); } else { Matrix_TranslateRel16(frame_a->offset); Matrix_Rot16(frame_a->mesh_rots[0]); } const int16_t *extra_rotation = item->extra_rotations; const int32_t max_joint = obj->mesh_count > 0 ? obj->mesh_count - 1 : 0; const int32_t abs_joint = MIN(max_joint, joint); for (int32_t i = 0; i < abs_joint; i++) { const ANIM_BONE *const bone = Object_GetBone(obj, i); if (bone->matrix_pop) { stack--; if (do_interp) { Matrix_Pop_I(); } else { Matrix_Pop(); } } if (bone->matrix_push) { stack++; if (do_interp) { Matrix_Push_I(); } else { Matrix_Push(); } } if (do_interp) { Matrix_TranslateRel32_I(bone->pos); Matrix_Rot16_ID( frame_a->mesh_rots[i + 1], frame_b->mesh_rots[i + 1]); Object_ApplyExtraRotation(&extra_rotation, bone->rot, true); } else { Matrix_TranslateRel32(bone->pos); Matrix_Rot16(frame_a->mesh_rots[i + 1]); Object_ApplyExtraRotation(&extra_rotation, bone->rot, false); } } if (do_interp) { Matrix_TranslateRel32_I(*out_vec); Matrix_Interpolate(); } else { Matrix_TranslateRel32(*out_vec); } out_vec->x = item_pos.x + (g_MatrixPtr->_03 >> W2V_SHIFT); out_vec->y = item_pos.y + (g_MatrixPtr->_13 >> W2V_SHIFT); out_vec->z = item_pos.z + (g_MatrixPtr->_23 >> W2V_SHIFT); while (stack--) { if (do_interp) { Matrix_Pop_I(); } else { Matrix_Pop(); } } } void Collide_GetCollisionInfo( COLL_INFO *const coll, const int32_t x_pos, const int32_t y_pos, const int32_t z_pos, int16_t room_num, int32_t obj_height) { coll->coll_type = COLL_NONE; coll->shift.x = 0; coll->shift.y = 0; coll->shift.z = 0; coll->quadrant = Math_GetDirection(coll->facing); bool reset_room = false; int16_t prev_room_num = room_num; if (obj_height < 0) { reset_room = true; obj_height = -obj_height; } int32_t x = x_pos; int32_t z = z_pos; const int32_t y = y_pos - obj_height; const int32_t y_top = y - M_HEADROOM; const XYZ_32 sample_pos = { .x = x, .y = y_top, .z = z }; const SECTOR *sector = Room_GetSector(sample_pos, &room_num); int32_t height = Room_GetHeight(sector, sample_pos); int32_t room_height = height; if (height != NO_HEIGHT) { height -= y_pos; } int32_t ceiling = Room_GetCeiling(sector, sample_pos); if (ceiling != NO_HEIGHT) { ceiling -= y; } coll->side_mid.floor = height; coll->side_mid.ceiling = ceiling; coll->side_mid.type = Room_GetHeightType(); bool is_on_walkable = M_IsOnWalkable(sector, sample_pos, room_height); if (is_on_walkable) { coll->tilt = (XZ_16) {}; } else { const ITEM *const lara_item = Lara_GetItem(); coll->tilt = Room_GetTiltType(sector, (XYZ_32) { x, lara_item->pos.y, z }); } XZ_32 probe_left = {}; XZ_32 probe_right = {}; XZ_32 probe_front = {}; switch (coll->quadrant) { case DIR_NORTH: probe_front.x = (coll->radius * Math_Sin(coll->facing)) >> W2V_SHIFT; probe_front.z = coll->radius; probe_left.x = -coll->radius; probe_left.z = coll->radius; probe_right.x = coll->radius; probe_right.z = coll->radius; break; case DIR_EAST: probe_front.x = coll->radius; probe_front.z = (coll->radius * Math_Cos(coll->facing)) >> W2V_SHIFT; probe_left.x = coll->radius; probe_left.z = coll->radius; probe_right.x = coll->radius; probe_right.z = -coll->radius; break; case DIR_SOUTH: probe_front.x = (coll->radius * Math_Sin(coll->facing)) >> W2V_SHIFT; probe_front.z = -coll->radius; probe_left.x = coll->radius; probe_left.z = -coll->radius; probe_right.x = -coll->radius; probe_right.z = -coll->radius; break; case DIR_WEST: probe_front.x = -coll->radius; probe_front.z = (coll->radius * Math_Cos(coll->facing)) >> W2V_SHIFT; probe_left.x = -coll->radius; probe_left.z = -coll->radius; probe_right.x = -coll->radius; probe_right.z = coll->radius; break; default: break; } if (reset_room) { room_num = prev_room_num; } const XYZ_32 probe_base = { x_pos, y_pos, z_pos }; M_FillSide( coll, &coll->side_front, probe_base, probe_front, obj_height, &room_num); int16_t room_num2; room_num2 = prev_room_num; M_FillSide( coll, &coll->side_left, probe_base, probe_left, obj_height, &room_num2); room_num2 = prev_room_num; M_FillSide( coll, &coll->side_right, probe_base, probe_right, obj_height, &room_num2); M_FillSide( coll, &coll->side_left2, probe_base, probe_left, obj_height, &room_num); M_FillSide( coll, &coll->side_right2, probe_base, probe_right, obj_height, &room_num); const int16_t static_room_num = g_TRVersion >= 3 ? prev_room_num : room_num; if (Collide_CollideStaticObjects( coll, x_pos, y_pos, z_pos, static_room_num, obj_height)) { const XYZ_32 test_pos = { .x = x_pos + coll->shift.x, .y = y_pos, .z = z_pos + coll->shift.z, }; sector = Room_GetSector(test_pos, &room_num); if (Room_GetHeight(sector, test_pos) < test_pos.y - WALL_L / 2 || Room_GetCeiling(sector, test_pos) > y) { coll->shift.x = -coll->shift.x; coll->shift.z = -coll->shift.z; } } if (coll->side_mid.floor == NO_HEIGHT) { coll->shift.x = coll->old.x - x_pos; coll->shift.y = coll->old.y - y_pos; coll->shift.z = coll->old.z - z_pos; coll->coll_type = COLL_FRONT; return; } if (coll->side_mid.floor - coll->side_mid.ceiling <= 0) { coll->shift.x = coll->old.x - x_pos; coll->shift.y = coll->old.y - y_pos; coll->shift.z = coll->old.z - z_pos; coll->coll_type = COLL_CLAMP; return; } if (coll->side_mid.ceiling >= 0) { coll->shift.y = coll->side_mid.ceiling; coll->coll_type = COLL_TOP; } if (coll->side_front.floor > coll->bad_pos || coll->side_front.floor < coll->bad_neg || coll->side_front.ceiling > coll->bad_ceiling) { if (coll->side_front.type == HT_DIAGONAL || coll->side_front.type == HT_SPLIT_TRI) { coll->shift.x = coll->old.x - x; coll->shift.z = coll->old.z - z; } else { switch (coll->quadrant) { case DIR_NORTH: case DIR_SOUTH: coll->shift.x = coll->old.x - x_pos; coll->shift.z = Room_FindGridShift(z_pos + probe_front.z, z_pos); break; case DIR_EAST: case DIR_WEST: coll->shift.x = Room_FindGridShift(x_pos + probe_front.x, x_pos); coll->shift.z = coll->old.z - z_pos; break; default: break; } } coll->coll_type = COLL_FRONT; return; } if (coll->side_front.ceiling >= coll->bad_ceiling) { coll->shift.x = coll->old.x - x_pos; coll->shift.y = coll->old.y - y_pos; coll->shift.z = coll->old.z - z_pos; coll->coll_type = COLL_TOP_FRONT; return; } if (coll->side_left.floor > coll->bad_pos || coll->side_left.floor < coll->bad_neg) { if (coll->side_left.type == HT_SPLIT_TRI) { coll->shift.x = coll->old.x - x; coll->shift.z = coll->old.z - z; } else { switch (coll->quadrant) { case DIR_NORTH: case DIR_SOUTH: coll->shift.x = Room_FindGridShift( x_pos + probe_left.x, x_pos + probe_front.x); break; case DIR_EAST: case DIR_WEST: coll->shift.z = Room_FindGridShift( z_pos + probe_left.z, z_pos + probe_front.z); break; default: break; } } coll->coll_type = COLL_LEFT; return; } if (coll->side_right.floor > coll->bad_pos || coll->side_right.floor < coll->bad_neg) { if (coll->side_right.type == HT_SPLIT_TRI) { coll->shift.x = coll->old.x - x; coll->shift.z = coll->old.z - z; } else { switch (coll->quadrant) { case DIR_NORTH: case DIR_SOUTH: coll->shift.x = Room_FindGridShift( x_pos + probe_right.x, x_pos + probe_front.x); break; case DIR_EAST: case DIR_WEST: coll->shift.z = Room_FindGridShift( z_pos + probe_right.z, z_pos + probe_front.z); break; default: break; } } coll->coll_type = COLL_RIGHT; return; } } bool Collide_CollideStaticObjects( COLL_INFO *const coll, const int32_t x, const int32_t y, const int32_t z, const int16_t room_num, const int32_t height) { coll->hit_static = 0; const int32_t in_x_min = x - coll->radius; const int32_t in_x_max = x + coll->radius; const int32_t in_y_min = y - height; const int32_t in_y_max = y; const int32_t in_z_min = z - coll->radius; const int32_t in_z_max = z + coll->radius; XYZ_32 shifter = { .x = 0, .z = 0 }; Room_GetNearbyRooms( (XYZ_32) { x, y, z }, coll->radius + 50, height + 50, room_num); for (int32_t i = 0; i < Room_DrawGetCount(); i++) { const ROOM *const room = Room_Get(Room_DrawGetRoom(i)); for (int32_t j = 0; j < room->num_static_meshes; j++) { const STATIC_MESH *const mesh = &room->static_meshes[j]; const STATIC_OBJECT_3D *const obj = Object_Get3DStatic(mesh->static_num); if (!obj->collidable) { continue; } int32_t x_min; int32_t x_max; int32_t z_min; int32_t z_max; const int32_t y_min = mesh->pos.y + obj->collision_bounds.min.y; const int32_t y_max = mesh->pos.y + obj->collision_bounds.max.y; switch (mesh->rot.y) { case DEG_90: x_min = mesh->pos.x + obj->collision_bounds.min.z; x_max = mesh->pos.x + obj->collision_bounds.max.z; z_min = mesh->pos.z - obj->collision_bounds.max.x; z_max = mesh->pos.z - obj->collision_bounds.min.x; break; case -DEG_180: x_min = mesh->pos.x - obj->collision_bounds.max.x; x_max = mesh->pos.x - obj->collision_bounds.min.x; z_min = mesh->pos.z - obj->collision_bounds.max.z; z_max = mesh->pos.z - obj->collision_bounds.min.z; break; case -DEG_90: x_min = mesh->pos.x - obj->collision_bounds.max.z; x_max = mesh->pos.x - obj->collision_bounds.min.z; z_min = mesh->pos.z + obj->collision_bounds.min.x; z_max = mesh->pos.z + obj->collision_bounds.max.x; break; default: x_min = mesh->pos.x + obj->collision_bounds.min.x; x_max = mesh->pos.x + obj->collision_bounds.max.x; z_min = mesh->pos.z + obj->collision_bounds.min.z; z_max = mesh->pos.z + obj->collision_bounds.max.z; break; } if (in_x_max <= x_min || in_x_min >= x_max || in_y_max <= y_min || in_y_min >= y_max || in_z_max <= z_min || in_z_min >= z_max) { continue; } coll->hit_static = 1; if (g_Config.gameplay.enable_soft_statics) { return true; } int32_t shl = in_x_max - x_min; int32_t shr = x_max - in_x_min; if (shl < shr) { shifter.x = -shl; } else { shifter.x = shr; } shl = in_z_max - z_min; shr = z_max - in_z_min; if (shl < shr) { shifter.z = -shl; } else { shifter.z = shr; } switch (coll->quadrant) { case DIR_NORTH: if (shifter.x > coll->radius || shifter.x < -coll->radius) { coll->coll_type = COLL_FRONT; coll->shift.x = coll->old.x - x; coll->shift.z = shifter.z; } else if (shifter.x > 0) { coll->coll_type = COLL_LEFT; coll->shift.x = shifter.x; coll->shift.z = 0; } else if (shifter.x < 0) { coll->coll_type = COLL_RIGHT; coll->shift.x = shifter.x; coll->shift.z = 0; } break; case DIR_EAST: if (shifter.z > coll->radius || shifter.z < -coll->radius) { coll->coll_type = COLL_FRONT; coll->shift.x = shifter.x; coll->shift.z = coll->old.z - z; } else if (shifter.z > 0) { coll->coll_type = COLL_RIGHT; coll->shift.x = 0; coll->shift.z = shifter.z; } else if (shifter.z < 0) { coll->coll_type = COLL_LEFT; coll->shift.x = 0; coll->shift.z = shifter.z; } break; case DIR_SOUTH: if (shifter.x > coll->radius || shifter.x < -coll->radius) { coll->coll_type = COLL_FRONT; coll->shift.x = coll->old.x - x; coll->shift.z = shifter.z; } else if (shifter.x > 0) { coll->coll_type = COLL_RIGHT; coll->shift.x = shifter.x; coll->shift.z = 0; } else if (shifter.x < 0) { coll->coll_type = COLL_LEFT; coll->shift.x = shifter.x; coll->shift.z = 0; } break; case DIR_WEST: if (shifter.z > coll->radius || shifter.z < -coll->radius) { coll->coll_type = COLL_FRONT; coll->shift.x = shifter.x; coll->shift.z = coll->old.z - z; } else if (shifter.z > 0) { coll->coll_type = COLL_LEFT; coll->shift.x = 0; coll->shift.z = shifter.z; } else if (shifter.z < 0) { coll->coll_type = COLL_RIGHT; coll->shift.x = 0; coll->shift.z = shifter.z; } break; default: break; } return true; } } return false; } bool Collide_TestBoundsCollide( const COLL_ITEM *const src_item, const COLL_ITEM *const dst_item, const int32_t radius) { const BOUNDS_16 *const src_bounds = &src_item->bounds; const BOUNDS_16 *const dst_bounds = &dst_item->bounds; if (src_item->pos.y + src_bounds->min.y >= dst_item->pos.y + dst_bounds->max.y || src_item->pos.y + src_bounds->max.y <= dst_item->pos.y + dst_bounds->min.y) { return false; } const int32_t c = Math_Cos(src_item->rot.y); const int32_t s = Math_Sin(src_item->rot.y); const int32_t dx = dst_item->pos.x - src_item->pos.x; const int32_t dz = dst_item->pos.z - src_item->pos.z; const int32_t rx = (c * dx - s * dz) >> W2V_SHIFT; const int32_t rz = (c * dz + s * dx) >> W2V_SHIFT; // clang-format off return ( rx >= src_bounds->min.x - radius && rx <= src_bounds->max.x + radius && rz >= src_bounds->min.z - radius && rz <= src_bounds->max.z + radius); // clang-format on } void Collide_DoProperDetection(ITEM *const item, const XYZ_32 old_pos) { int32_t ceiling; int32_t height; int32_t oldonobj; int32_t bs; int32_t yang; int16_t room_num = item->room_num; const SECTOR *sector = Room_GetSector(old_pos, &room_num); int32_t oldheight = Room_GetHeight(sector, old_pos); int32_t oldtype = Room_GetHeightType(); room_num = item->room_num; sector = Room_GetSector(item->pos, &room_num); height = Room_GetHeight(sector, item->pos); if (item->pos.y >= height) { bs = 0; if ((oldtype == HT_BIG_SLOPE || oldtype == HT_DIAGONAL) && oldheight < height) { yang = (uint16_t)item->rot.y; const XZ_16 tilt = Room_GetTiltType(sector, item->pos); if (tilt.x < 0) { if (yang >= DEG_180) { bs = 1; } } else if (tilt.x > 0) { if (yang <= DEG_180) { bs = 1; } } if (tilt.z < 0) { if (yang >= DEG_90 && yang <= DEG_270) { bs = 1; } } else if (tilt.z > 0) { if (yang <= DEG_90 || yang >= DEG_270) { bs = 1; } } } const bool x_cross = ROUND_TO_SECTOR(item->pos.x ^ old_pos.x) != 0; const bool z_cross = ROUND_TO_SECTOR(item->pos.z ^ old_pos.z) != 0; if (old_pos.y > height + 32 && !bs && (x_cross || z_cross)) { const bool xs = x_cross && z_cross ? ABS(old_pos.x - item->pos.x) < ABS(old_pos.z - item->pos.z) : true; item->rot.y = x_cross && xs ? -item->rot.y : -DEG_180 - item->rot.y; item->pos = old_pos; item->speed >>= 1; } else if (oldtype != HT_BIG_SLOPE && oldtype != HT_DIAGONAL) { if (item->fall_speed > 0) { if (item->fall_speed > 16) { if (item->object_id == O_GRENADE) { item->fall_speed = (item->fall_speed >> 1) - item->fall_speed; } else { item->fall_speed = -(item->fall_speed >> 2); if (item->fall_speed < -100) { item->fall_speed = -100; } } } else { item->fall_speed = 0; if (item->object_id == O_GRENADE) { item->speed--; item->required_anim_state = 1; item->rot.x = 0; } else { item->speed -= 3; } if (item->speed < 0) { item->speed = 0; } } } item->pos.y = height; } else { item->speed -= item->speed >> 2; const XZ_16 tilt = Room_GetTiltType(sector, item->pos); if (tilt.x < 0 && ABS(tilt.x) - ABS(tilt.z) >= MAX_SLOPE) { if ((uint16_t)item->rot.y > DEG_180) { item->rot.y = -1 - item->rot.y; if (item->fall_speed > 0) { item->fall_speed = -(item->fall_speed >> 1); } } else { if (item->speed < 32) { item->speed -= tilt.x << 1; if ((uint16_t)item->rot.y > DEG_90 && (uint16_t)item->rot.y < DEG_270) { item->rot.y -= 0x1000; if ((uint16_t)item->rot.y < DEG_90) { item->rot.y = DEG_90; } } else if ((uint16_t)item->rot.y < DEG_90) { item->rot.y += 0x1000; if ((uint16_t)item->rot.y > DEG_90) { item->rot.y = DEG_90; } } } item->fall_speed = item->fall_speed > 0 ? -(item->fall_speed >> 1) : 0; } } else if (tilt.x > 0 && ABS(tilt.x) - ABS(tilt.z) >= MAX_SLOPE) { if ((uint16_t)item->rot.y < DEG_180) { item->rot.y = -1 - item->rot.y; if (item->fall_speed > 0) { item->fall_speed = -(item->fall_speed >> 1); } } else { if (item->speed < 32) { item->speed += tilt.x << 1; if ((uint16_t)item->rot.y > DEG_270 || (uint16_t)item->rot.y < DEG_90) { item->rot.y -= 0x1000; if ((uint16_t)item->rot.y < DEG_270) { item->rot.y = -DEG_90; } } else if ((uint16_t)item->rot.y < DEG_270) { item->rot.y += 0x1000; if ((uint16_t)item->rot.y > DEG_270) { item->rot.y = -DEG_90; } } } item->fall_speed = item->fall_speed > 0 ? -(item->fall_speed >> 1) : 0; } } else if (tilt.z < 0 && ABS(tilt.z) - ABS(tilt.x) >= MAX_SLOPE) { if ((uint16_t)item->rot.y > DEG_90 && (uint16_t)item->rot.y < DEG_270) { item->rot.y = 0x7FFF - item->rot.y; if (item->fall_speed > 0) { item->fall_speed = -(item->fall_speed >> 1); } } else { if (item->speed < 32) { item->speed -= tilt.z << 1; if ((uint16_t)item->rot.y < DEG_180) { item->rot.y -= DEG_90; if ((uint16_t)item->rot.y > 61440) { item->rot.y = 0; } } else { item->rot.y += DEG_90; if ((uint16_t)item->rot.y < DEG_90) { item->rot.y = 0; } } } item->fall_speed = item->fall_speed > 0 ? -(item->fall_speed >> 1) : 0; } } else if (tilt.z > 0 && ABS(tilt.z) - ABS(tilt.x) >= MAX_SLOPE) { if ((uint16_t)item->rot.y > DEG_270 || (uint16_t)item->rot.y < DEG_90) { item->rot.y = 0x7FFF - item->rot.y; if (item->fall_speed > 0) { item->fall_speed = -(item->fall_speed >> 1); } } else { if (item->speed < 32) { item->speed += tilt.z << 1; if ((uint16_t)item->rot.y > DEG_180) { item->rot.y -= 0x1000; if ((uint16_t)item->rot.y < DEG_180) { item->rot.y = -DEG_180; } } else if ((uint16_t)item->rot.y < DEG_180) { item->rot.y += 0x1000; if ((uint16_t)item->rot.y > DEG_180) { item->rot.y = -DEG_180; } } } item->fall_speed = item->fall_speed > 0 ? -(item->fall_speed >> 1) : 0; } } else if (tilt.x < 0 && tilt.z < 0) { if ((uint16_t)item->rot.y > DEG_135 && (uint16_t)item->rot.y < DEG_315) { item->rot.y = -(DEG_90 + 1) - item->rot.y; if (item->fall_speed > 0) { item->fall_speed = -(item->fall_speed >> 1); } } else { if (item->speed < 32) { item->speed -= tilt.z + tilt.x; if ((uint16_t)item->rot.y > DEG_45 && (uint16_t)item->rot.y < DEG_225) { item->rot.y -= 0x1000; if ((uint16_t)item->rot.y < DEG_45) { item->rot.y = DEG_45; } } else if (item->rot.y != DEG_45) { item->rot.y += 0x1000; if ((uint16_t)item->rot.y > DEG_45) { item->rot.y = DEG_45; } } } item->fall_speed = item->fall_speed > 0 ? -(item->fall_speed >> 1) : 0; } } else if (tilt.x < 0 && tilt.z > 0) { if ((uint16_t)item->rot.y > DEG_225 || (uint16_t)item->rot.y < DEG_45) { item->rot.y = DEG_90 - 1 - item->rot.y; if (item->fall_speed > 0) { item->fall_speed = -(item->fall_speed >> 1); } } else { if (item->speed < 32) { item->speed += tilt.z - tilt.x; if ((uint16_t)item->rot.y < DEG_315 && (uint16_t)item->rot.y > DEG_135) { item->rot.y -= 0x1000; if ((uint16_t)item->rot.y < DEG_135) { item->rot.y = DEG_135; } } else if (item->rot.y != DEG_135) { item->rot.y += 0x1000; if ((uint16_t)item->rot.y > DEG_135) { item->rot.y = DEG_135; } } } item->fall_speed = item->fall_speed > 0 ? -(item->fall_speed >> 1) : 0; } } else if (tilt.x > 0 && tilt.z > 0) { if ((uint16_t)item->rot.y > DEG_315 || (uint16_t)item->rot.y < DEG_135) { item->rot.y = -(DEG_90 + 1) - item->rot.y; if (item->fall_speed > 0) { item->fall_speed = -(item->fall_speed >> 1); } } else { if (item->speed < 32) { item->speed += tilt.z + tilt.x; if ((uint16_t)item->rot.y < DEG_45 || (uint16_t)item->rot.y > DEG_225) { item->rot.y -= 0x1000; if ((uint16_t)item->rot.y < DEG_225) { item->rot.y = -DEG_135; } } else if ((uint16_t)item->rot.y != DEG_225) { item->rot.y += 0x1000; if ((uint16_t)item->rot.y > DEG_225) { item->rot.y = -DEG_135; } } } item->fall_speed = item->fall_speed > 0 ? -(item->fall_speed >> 1) : 0; } } else if (tilt.x > 0 && tilt.z < 0) { if ((uint16_t)item->rot.y > DEG_45 && (uint16_t)item->rot.y < DEG_225) { item->rot.y = DEG_90 - 1 - item->rot.y; if (item->fall_speed > 0) { item->fall_speed = -(item->fall_speed >> 1); } } else { if (item->speed < 32) { item->speed += tilt.x - tilt.z; if ((uint16_t)item->rot.y < DEG_135 || (uint16_t)item->rot.y > DEG_315) { item->rot.y -= 0x1000; if ((uint16_t)item->rot.y < DEG_315) { item->rot.y = -DEG_45; } } else if ((uint16_t)item->rot.y != DEG_315) { item->rot.y += 0x1000; if ((uint16_t)item->rot.y > DEG_315) { item->rot.y = -DEG_45; } } } item->fall_speed = item->fall_speed > 0 ? -(item->fall_speed >> 1) : 0; } } item->pos = old_pos; } } else { if (item->fall_speed >= 0) { const XYZ_32 test_pos = { item->pos.x, old_pos.y, item->pos.z, }; room_num = item->room_num; sector = Room_GetSector(test_pos, &room_num); height = Room_GetHeight(sector, test_pos); const bool on_walkable = Room_IsOnWalkable(sector, test_pos, height, NO_ITEM); if (item->pos.y >= height && on_walkable) { if (item->fall_speed > 16) { if (item->object_id == O_GRENADE) { item->fall_speed = -(item->fall_speed / 2); } else { item->fall_speed = -(item->fall_speed / 4); CLAMPL(item->fall_speed, -100); } } else if (item->fall_speed > 0) { item->fall_speed = 0; if (item->object_id == O_GRENADE) { item->speed--; item->required_anim_state = 1; item->rot.x = 0; } else { item->speed -= 3; } CLAMPL(item->speed, 0); } item->pos.y = height; } } room_num = item->room_num; sector = Room_GetSector(item->pos, &room_num); ceiling = Room_GetCeiling(sector, item->pos); if (item->pos.y < ceiling) { const bool x_cross = ROUND_TO_SECTOR(item->pos.x ^ old_pos.x) != 0; const bool z_cross = ROUND_TO_SECTOR(item->pos.z ^ old_pos.z) != 0; if (old_pos.y < ceiling && (x_cross || z_cross)) { item->rot.y = x_cross ? -item->rot.y : -DEG_180 - item->rot.y; if (item->object_id == O_GRENADE) { item->speed -= item->speed >> 3; } else { item->speed >>= 1; } item->pos = old_pos; } else { item->pos.y = ceiling; } if (item->fall_speed < 0) { item->fall_speed = -item->fall_speed; } } } room_num = item->room_num; Room_GetSector(item->pos, &room_num); if (item->room_num != room_num) { Item_UpdateRoom(Item_GetIndex(item), room_num); } } void Collide_ShiftItem(ITEM *const item, COLL_INFO *const coll) { item->pos.x += coll->shift.x; item->pos.y += coll->shift.y; item->pos.z += coll->shift.z; coll->shift.x = 0; coll->shift.y = 0; coll->shift.z = 0; } ================================================ FILE: src/trx/game/collision/common.h ================================================ #pragma once #include int32_t Collide_GetSpheres(const ITEM *item, SPHERE *spheres, bool world_space); int32_t Collide_TestCollision(ITEM *item, const ITEM *lara_item); void Collide_GetJointAbsPosition( const ITEM *item, XYZ_32 *out_vec, int32_t joint); void Collide_GetCollisionInfo( COLL_INFO *coll, int32_t x, int32_t y, int32_t z, int16_t room_num, int32_t obj_height); bool Collide_CollideStaticObjects( COLL_INFO *coll, int32_t x, int32_t y, int32_t z, int16_t room_num, int32_t height); bool Collide_TestBoundsCollide( const COLL_ITEM *src_item, const COLL_ITEM *dst_item, int32_t radius); void Collide_DoProperDetection(ITEM *item, XYZ_32 old_pos); void Collide_ShiftItem(ITEM *item, COLL_INFO *coll); ================================================ FILE: src/trx/game/collision/los.c ================================================ #include #include #include #include #include #include #define M_CLIP_1 8 #define M_CLIP_2 8 #define M_MAX_LOS_ROOMS 200 static int32_t m_LOSRooms[M_MAX_LOS_ROOMS] = {}; static int32_t m_LOSNumRooms = 0; static inline bool M_TryPushLOSRoom(const int16_t room_num) { if (m_LOSNumRooms < M_MAX_LOS_ROOMS) { m_LOSRooms[m_LOSNumRooms++] = room_num; return true; } return false; } static inline bool M_ResetLOSRooms(const int16_t room_num) { m_LOSNumRooms = 0; return M_TryPushLOSRoom(room_num); } static inline bool M_RoomInLOSRooms(const int16_t room_num) { for (int32_t i = 0; i < m_LOSNumRooms; i++) { if (m_LOSRooms[i] == room_num) { return true; } } return false; } // This routine transforms the world-space LOS segment [start,target] into the // object's local coordinates (undoing its translation and Y-rotation), then // performs a slab intersection test against that local AABB. The first // smashable item hit is returned, or NO_ITEM if none. // // (AABB = Axis-Aligned Bounding Box. It's the rectangular box defined by // bounds->min/max along X,Y,Z in the object's local space (no rotation).) // // @param start World-space ray origin // @param target World-space ray end // @param item Item to check // @return Whether the item collides with the segment bool LOS_CheckItemIntersectSegment( const GAME_VECTOR *const start, GAME_VECTOR *const target, const ITEM *const item) { const double dx = target->x - start->x; const double dy = target->y - start->y; const double dz = target->z - start->z; // Translate into object-local space const double ox = start->x - item->pos.x; const double oy = start->y - item->pos.y; const double oz = start->z - item->pos.z; // Unrotate by -rot.y around Y axis const float c = cosf(-item->rot.y * M_PI / DEG_180); const float s = sinf(-item->rot.y * M_PI / DEG_180); const double lx = ox * c + oz * s; const double ly = oy; const double lz = oz * c - ox * s; const double ldx = dx * c + dz * s; const double ldy = dy; const double ldz = dz * c - dx * s; // Local AABB extents from item's bounds const BOUNDS_16 *const orig_bounds = Item_GetBoundsAccurate(item); BOUNDS_16 patched_bounds = *orig_bounds; const BOUNDS_16 *const bounds = &patched_bounds; // Parametric interval [t0..t1] in Q14 fixed-point double t0 = 0.0; double t1 = 1.0; // X slab if (ldx != 0) { double t_near = (double)(bounds->min.x - lx) / ldx; double t_far = (double)(bounds->max.x - lx) / ldx; if (t_near > t_far) { SWAP(t_near, t_far); } if (t_near > t1 || t_far < t0) { return false; } CLAMPL(t0, t_near); CLAMPG(t1, t_far); } else if (lx < bounds->min.x || lx > bounds->max.x) { return false; } // Y slab if (ldy != 0) { double t_near = (double)(bounds->min.y - ly) / ldy; double t_far = (double)(bounds->max.y - ly) / ldy; if (t_near > t_far) { SWAP(t_near, t_far); } if (t_near > t1 || t_far < t0) { return false; } CLAMPL(t0, t_near); CLAMPG(t1, t_far); } else if (ly < bounds->min.y || ly > bounds->max.y) { return false; } // Z slab if (ldz != 0) { double t_near = (double)(bounds->min.z - lz) / ldz; double t_far = (double)(bounds->max.z - lz) / ldz; if (t_near > t_far) { SWAP(t_near, t_far); } if (t_near > t1 || t_far < t0) { return false; } CLAMPL(t0, t_near); CLAMPG(t1, t_far); } else if (lz < bounds->min.z || lz > bounds->max.z) { return false; } // world-space hit position = start + t0 * (target-start) target->x = start->x + t0 * dx; target->y = start->y + t0 * dy; target->z = start->z + t0 * dz; return true; } // Smashes all objects along the ray path. // // @param start World-space ray origin // @param target World-space ray end // @return First smashable item's index, or NO_ITEM if none hit int32_t LOS_CheckSmashable( const GAME_VECTOR start, const GAME_VECTOR target, XYZ_32 *const out_hit_pos) { int32_t best_dist = INT32_MAX; int16_t best_item_num = NO_ITEM; for (int16_t item_num = 0; item_num < Item_GetTotalCount(); item_num++) { const ITEM *const item = Item_Get(item_num); if (item->status == IS_DEACTIVATED) { continue; } if (!Object_IsType(item->object_id, g_SmashableObjects) && !Object_IsType(item->object_id, g_ShatterableObjects) && !Object_IsType(item->object_id, g_HeavyShatterableObjects)) { continue; } GAME_VECTOR hit_pos = target; if (!LOS_CheckItemIntersectSegment(&start, &hit_pos, item)) { continue; } // Confirm item is reachable via visible rooms { GAME_VECTOR start_tmp = start; GAME_VECTOR target_tmp = { .pos = item->pos, }; if (!LOS_Check(&start_tmp, &target_tmp, false)) { continue; } } // Ray segment intersects the object's local AABB const int32_t dist = Item_GetDistance(item, start.pos); if (dist < best_dist) { best_dist = dist; best_item_num = item_num; if (out_hit_pos != nullptr) { *out_hit_pos = hit_pos.pos; } } } return best_item_num; } static int32_t M_CheckX( const GAME_VECTOR *const start, GAME_VECTOR *const target) { const int32_t dx = target->x - start->x; if (dx == 0) { return 1; } const int32_t dy = ((target->y - start->y) * WALL_L) / dx; const int32_t dz = ((target->z - start->z) * WALL_L) / dx; int16_t room_num = start->room_num; int16_t last_room_num = start->room_num; M_ResetLOSRooms(room_num); if (dx < 0) { XYZ_32 cur_pos; cur_pos.x = ROUND_TO_SECTOR(start->x); cur_pos.y = start->y + ((dy * (cur_pos.x - start->x)) >> WALL_SHIFT); cur_pos.z = start->z + ((dz * (cur_pos.x - start->x)) >> WALL_SHIFT); while (cur_pos.x > target->x) { XYZ_32 sample_pos = cur_pos; { const SECTOR *const sector = Room_GetSector(sample_pos, &room_num); const int32_t height = Room_GetHeight(sector, sample_pos); const int32_t ceiling = Room_GetCeiling(sector, sample_pos); if (cur_pos.y > height || cur_pos.y < ceiling) { target->pos = cur_pos; target->room_num = room_num; return -1; } } if (room_num != last_room_num) { last_room_num = room_num; M_TryPushLOSRoom(room_num); } sample_pos.x -= 1; { const SECTOR *const sector = Room_GetSector(sample_pos, &room_num); const int32_t height = Room_GetHeight(sector, sample_pos); const int32_t ceiling = Room_GetCeiling(sector, sample_pos); if (cur_pos.y > height || cur_pos.y < ceiling) { target->pos = cur_pos; target->room_num = last_room_num; return 0; } } cur_pos.x -= WALL_L; cur_pos.y -= dy; cur_pos.z -= dz; } } else { XYZ_32 cur_pos; cur_pos.x = ROUND_TO_SECTOR_END(start->x); cur_pos.y = start->y + (((cur_pos.x - start->x) * dy) >> WALL_SHIFT); cur_pos.z = start->z + (((cur_pos.x - start->x) * dz) >> WALL_SHIFT); while (cur_pos.x < target->x) { XYZ_32 sample_pos = cur_pos; { const SECTOR *const sector = Room_GetSector(sample_pos, &room_num); const int32_t height = Room_GetHeight(sector, sample_pos); const int32_t ceiling = Room_GetCeiling(sector, sample_pos); if (cur_pos.y > height || cur_pos.y < ceiling) { target->pos = cur_pos; target->room_num = room_num; return -1; } } if (room_num != last_room_num) { last_room_num = room_num; M_TryPushLOSRoom(room_num); } sample_pos.x += 1; { const SECTOR *const sector = Room_GetSector(sample_pos, &room_num); const int32_t height = Room_GetHeight(sector, sample_pos); const int32_t ceiling = Room_GetCeiling(sector, sample_pos); if (cur_pos.y > height || cur_pos.y < ceiling) { target->pos = cur_pos; target->room_num = last_room_num; return 0; } } cur_pos.x += WALL_L; cur_pos.y += dy; cur_pos.z += dz; } } target->room_num = room_num; return 1; } static int32_t M_CheckZ( const GAME_VECTOR *const start, GAME_VECTOR *const target) { const int32_t dz = target->z - start->z; if (dz == 0) { return 1; } const int32_t dx = ((target->x - start->x) * WALL_L) / dz; const int32_t dy = ((target->y - start->y) * WALL_L) / dz; int16_t room_num = start->room_num; int16_t last_room_num = start->room_num; M_ResetLOSRooms(room_num); if (dz < 0) { XYZ_32 cur_pos; cur_pos.z = ROUND_TO_SECTOR(start->z); cur_pos.x = start->x + ((dx * (cur_pos.z - start->z)) >> WALL_SHIFT); cur_pos.y = start->y + ((dy * (cur_pos.z - start->z)) >> WALL_SHIFT); while (cur_pos.z > target->z) { XYZ_32 sample_pos = cur_pos; { const SECTOR *const sector = Room_GetSector(sample_pos, &room_num); const int32_t height = Room_GetHeight(sector, sample_pos); const int32_t ceiling = Room_GetCeiling(sector, sample_pos); if (cur_pos.y > height || cur_pos.y < ceiling) { target->pos = cur_pos; target->room_num = room_num; return -1; } } if (room_num != last_room_num) { last_room_num = room_num; M_TryPushLOSRoom(room_num); } sample_pos.z -= 1; { const SECTOR *const sector = Room_GetSector(sample_pos, &room_num); const int32_t height = Room_GetHeight(sector, sample_pos); const int32_t ceiling = Room_GetCeiling(sector, sample_pos); if (cur_pos.y > height || cur_pos.y < ceiling) { target->pos = cur_pos; target->room_num = last_room_num; return 0; } } cur_pos.z -= WALL_L; cur_pos.x -= dx; cur_pos.y -= dy; } } else { XYZ_32 cur_pos; cur_pos.z = ROUND_TO_SECTOR_END(start->z); cur_pos.x = start->x + ((dx * (cur_pos.z - start->z)) >> WALL_SHIFT); cur_pos.y = start->y + ((dy * (cur_pos.z - start->z)) >> WALL_SHIFT); while (cur_pos.z < target->z) { XYZ_32 sample_pos = cur_pos; { const SECTOR *const sector = Room_GetSector(sample_pos, &room_num); const int32_t height = Room_GetHeight(sector, sample_pos); const int32_t ceiling = Room_GetCeiling(sector, sample_pos); if (cur_pos.y > height || cur_pos.y < ceiling) { target->pos = cur_pos; target->room_num = room_num; return -1; } } if (room_num != last_room_num) { last_room_num = room_num; M_TryPushLOSRoom(room_num); } sample_pos.z += 1; { const SECTOR *const sector = Room_GetSector(sample_pos, &room_num); const int32_t height = Room_GetHeight(sector, sample_pos); const int32_t ceiling = Room_GetCeiling(sector, sample_pos); if (cur_pos.y > height || cur_pos.y < ceiling) { target->pos = cur_pos; target->room_num = last_room_num; return 0; } } cur_pos.z += WALL_L; cur_pos.x += dx; cur_pos.y += dy; } } target->room_num = room_num; return 1; } static int32_t M_ClipTargetSimple( const GAME_VECTOR *const start, GAME_VECTOR *const target) { const SECTOR *sector = Room_GetSector(target->pos, &target->room_num); // This function exists because of issue #4070. int32_t dx = target->x - start->x; int32_t dy = target->y - start->y; int32_t dz = target->z - start->z; const int32_t height = Room_GetHeight(sector, target->pos); if (target->y > height && start->y < height) { target->y = height; target->x = start->x + dx * (height - start->y) / dy; target->z = start->z + dz * (height - start->y) / dy; return false; } const int32_t ceiling = Room_GetCeiling(sector, target->pos); if (target->y < ceiling && start->y > ceiling) { target->y = ceiling; target->x = start->x + dx * (ceiling - start->y) / dy; target->z = start->z + dz * (ceiling - start->y) / dy; return false; } return true; } static int32_t M_ClipTargetWithSlopes( const GAME_VECTOR *const start, GAME_VECTOR *const target) { int16_t room_num = target->room_num; const SECTOR *sector = Room_GetSector(target->pos, &room_num); if (target->y > Room_GetHeight(sector, target->pos)) { const XYZ_32 origin = { start->x + ((M_CLIP_1 - 1) * (target->x - start->x) / M_CLIP_1), start->y + ((M_CLIP_1 - 1) * (target->y - start->y) / M_CLIP_1), start->z + ((M_CLIP_1 - 1) * (target->z - start->z) / M_CLIP_1), }; XYZ_32 delta; for (int32_t i = M_CLIP_2 - 1; i > 0; i--) { delta.x = origin.x + (i * (target->x - origin.x) / M_CLIP_2); delta.y = origin.y + (i * (target->y - origin.y) / M_CLIP_2); delta.z = origin.z + (i * (target->z - origin.z) / M_CLIP_2); sector = Room_GetSector(delta, &room_num); if (delta.y < Room_GetHeight(sector, delta)) { break; } } target->pos = delta; target->room_num = room_num; return 0; } if (target->y < Room_GetCeiling(sector, target->pos)) { const XYZ_32 origin = { start->x + ((M_CLIP_1 - 1) * (target->x - start->x) / M_CLIP_1), start->y + ((M_CLIP_1 - 1) * (target->y - start->y) / M_CLIP_1), start->z + ((M_CLIP_1 - 1) * (target->z - start->z) / M_CLIP_1), }; XYZ_32 delta; for (int32_t i = M_CLIP_2 - 1; i > 0; i--) { delta.x = origin.x + (i * (target->x - origin.x) / M_CLIP_2); delta.y = origin.y + (i * (target->y - origin.y) / M_CLIP_2); delta.z = origin.z + (i * (target->z - origin.z) / M_CLIP_2); sector = Room_GetSector(delta, &room_num); if (delta.y > Room_GetCeiling(sector, delta)) { break; } } target->pos = delta; target->room_num = room_num; return 0; } return 1; } bool LOS_Check( const GAME_VECTOR *const start, GAME_VECTOR *const target, const bool use_slope_clipping) { const int32_t dx = ABS(target->x - start->x); const int32_t dz = ABS(target->z - start->z); int32_t los1; int32_t los2; if (dz > dx) { los1 = M_CheckX(start, target); los2 = M_CheckZ(start, target); } else { los1 = M_CheckZ(start, target); los2 = M_CheckX(start, target); } if (!los2) { return false; } if (dx == 0 && dz == 0) { target->room_num = start->room_num; } const bool clip_result = (use_slope_clipping ? M_ClipTargetWithSlopes(start, target) : M_ClipTargetSimple(start, target)); return clip_result && los1 == 1 && los2 == 1; } ================================================ FILE: src/trx/game/collision/los.h ================================================ #pragma once #include bool LOS_Check( const GAME_VECTOR *start, GAME_VECTOR *target, bool use_detailed_clipping); bool LOS_CheckItemIntersectSegment( const GAME_VECTOR *start, GAME_VECTOR *target, const ITEM *item); int32_t LOS_CheckSmashable( GAME_VECTOR start, GAME_VECTOR target, XYZ_32 *out_hit_pos); ================================================ FILE: src/trx/game/collision/types.h ================================================ #pragma once #include #include typedef struct { int32_t floor; int32_t ceiling; int32_t type; } COLL_SIDE; typedef enum { // clang-format off COLL_NONE = 0x00, COLL_FRONT = 0x01, COLL_LEFT = 0x02, COLL_RIGHT = 0x04, COLL_TOP = 0x08, COLL_TOP_FRONT = 0x10, COLL_CLAMP = 0x20, // clang-format on } COLL_TYPE; typedef struct { COLL_SIDE side_mid; COLL_SIDE side_front; COLL_SIDE side_left; COLL_SIDE side_right; COLL_SIDE side_left2; COLL_SIDE side_right2; int32_t radius; int32_t bad_pos; int32_t bad_neg; int32_t bad_ceiling; XYZ_32 shift; XYZ_32 old; int16_t old_anim_state; int16_t old_anim_num; int16_t old_frame_num; int16_t facing; DIRECTION quadrant; int16_t coll_type; XZ_16 tilt; int8_t hit_by_baddie; int8_t hit_static; // clang-format off uint16_t slopes_are_walls: 1; // 0x01 1 uint16_t slopes_are_pits: 1; // 0x02 2 uint16_t lava_is_pit: 1; // 0x04 4 uint16_t enable_baddie_push: 1; // 0x08 8 uint16_t enable_hit: 1; // 0x10 16 uint16_t pad: 11; // clang-format on } COLL_INFO; typedef struct { XYZ_32 pos; int32_t r; } SPHERE; typedef struct { BOUNDS_16 bounds; XYZ_32 pos; XYZ_16 rot; } COLL_ITEM; ================================================ FILE: src/trx/game/collision.h ================================================ #pragma once #include #include ================================================ FILE: src/trx/game/console/cmd/clear.c ================================================ #include #include #include static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { if (!String_IsEmpty(ctx->args)) { return CR_BAD_INVOCATION; } Console_Clear(); return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND( "cls|clear", M_Entrypoint, GS_ID("console/cmd/clear/help")) ================================================ FILE: src/trx/game/console/cmd/config.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include static char *M_NormalizeValue(const char *const value) { if (value == nullptr) { return nullptr; } char *const result = Memory_DupStr(value); for (uint32_t i = 0; i < strlen(result); i++) { if (result[i] == '_') { result[i] = '-'; } } return result; } static const char *M_Resolve(const char *const option_name) { const char *dot = strrchr(option_name, '.'); if (dot) { return dot + 1; } return option_name; } static const CONFIG_OPTION *M_GetOptionFromKey(const char *const key) { VECTOR *source = Vector_Create(sizeof(STRING_FUZZY_SOURCE)); for (const CONFIG_OPTION *option = Config_GetOptionMap(); option->name != nullptr; option++) { STRING_FUZZY_SOURCE source_item = { .key = (const char *)Console_Cmd_Config_NormalizeKey(option->name), .value = (void *)option, .weight = 1, }; Vector_Add(source, &source_item); } VECTOR *matches = String_FuzzyMatch(key, source); const CONFIG_OPTION *result = nullptr; if (matches->count == 0) { Console_LogError(GS("general/osd/config_option_unknown_option"), key); } else if (matches->count == 1) { const STRING_FUZZY_MATCH *const match = Vector_Get(matches, 0); result = match->value; } else if (matches->count == 2) { const STRING_FUZZY_MATCH *const match1 = Vector_Get(matches, 0); const STRING_FUZZY_MATCH *const match2 = Vector_Get(matches, 1); Console_LogError( GS("general/osd/ambiguous_input_2"), match1->key, match2->key); } else if (matches->count >= 3) { const STRING_FUZZY_MATCH *const match1 = Vector_Get(matches, 0); const STRING_FUZZY_MATCH *const match2 = Vector_Get(matches, 1); Console_LogError( GS("general/osd/ambiguous_input_3"), match1->key, match2->key); } for (int32_t i = 0; i < source->count; i++) { const STRING_FUZZY_SOURCE *const source_item = Vector_Get(source, i); Memory_Free((char *)source_item->key); } Vector_Free(matches); Vector_Free(source); return result; } static char *M_FormatValuesList(const VECTOR *const values) { if (values == nullptr || values->count == 0) { return nullptr; } char *result = nullptr; for (int32_t i = 0; i < values->count; i++) { const char *const value = *(char **)Vector_Get(values, i); if (value == nullptr) { continue; } char *normalized = M_NormalizeValue(value); if (normalized == nullptr) { continue; } if (result == nullptr) { result = normalized; } else { char *const joined = String_Format("%s, %s", result, normalized); Memory_FreePointer(&result); Memory_FreePointer(&normalized); result = joined; } } return result; } static char *M_FormatDynamicEnumDefaults(const CONFIG_OPTION *const option) { VECTOR *const values = Vector_Create(sizeof(char *)); const char *const default_value = "-"; Vector_Add(values, &default_value); const int32_t value_count = Config_DynamicEnum_GetValueCount(option); for (int32_t i = 0; i < value_count; i++) { const char *const value = Config_DynamicEnum_GetValueAt(option, i); if (value != nullptr) { Vector_Add(values, &value); } } char *const result = M_FormatValuesList(values); Vector_Free(values); return result; } static char *M_GetValueForConsole(const CONFIG_OPTION *const option) { if ((option->type == COT_STRING || option->type == COT_DYNAMIC_ENUM) && *(char **)option->target == nullptr) { return Memory_DupStr("(null)"); } const char *const value = Config_GetOptionValueAsString(option, false); if (value == nullptr) { return nullptr; } if (option->type == COT_ENUM || option->type == COT_DYNAMIC_ENUM) { return M_NormalizeValue(value); } return Memory_DupStr(value); } static bool M_TryApplyOptionValue( const CONFIG_OPTION *const option, const char *const new_value) { if (strcmp(new_value, "-") == 0) { return Config_RestoreOptionDefault(option->target); } if (Config_SetOptionValueFromString(option, new_value)) { return true; } if (option->type != COT_ENUM && option->type != COT_DYNAMIC_ENUM) { return false; } char *normalized = M_NormalizeValue(new_value); if (normalized != nullptr) { const bool different = strcmp(normalized, new_value) != 0; if (different && Config_SetOptionValueFromString(option, normalized)) { Memory_FreePointer(&normalized); return true; } Memory_FreePointer(&normalized); } char *underscore = Memory_DupStr(new_value); bool different = false; for (uint32_t i = 0; i < strlen(underscore); i++) { if (underscore[i] == '-') { underscore[i] = '_'; different = true; } } if (different && Config_SetOptionValueFromString(option, underscore)) { Memory_FreePointer(&underscore); return true; } Memory_FreePointer(&underscore); return false; } // Builds a source list of all options and returns fuzzy-match results. // Caller must free the result vector with Vector_Free(). static VECTOR *M_GetOptionsFuzzy(const char *const key) { VECTOR *source = Vector_Create(sizeof(STRING_FUZZY_SOURCE)); for (const CONFIG_OPTION *option = Config_GetOptionMap(); option->name != nullptr; option++) { STRING_FUZZY_SOURCE source_item = { .key = (const char *)Console_Cmd_Config_NormalizeKey(option->name), .value = (void *)option, .weight = 1, }; Vector_Add(source, &source_item); } VECTOR *matches = String_FuzzyMatch(key, source); for (int32_t i = 0; i < source->count; i++) { const STRING_FUZZY_SOURCE *const source_item = Vector_Get(source, i); Memory_Free((char *)source_item->key); } Vector_Free(source); return matches; } static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { COMMAND_RESULT result = CR_BAD_INVOCATION; char *key = Memory_DupStr(ctx->args); char *const space = strchr(key, ' '); const char *new_value = nullptr; if (space != nullptr) { new_value = space + 1; space[0] = '\0'; // nullptr-terminate the key } const CONFIG_OPTION *const option = M_GetOptionFromKey(key); if (option == nullptr) { result = CR_FAILURE; } else { result = Console_Cmd_Config_Helper(option, new_value); } cleanup: Memory_FreePointer(&key); return result; } // Return a comma-delimited list of valid values for the option. // Caller must free the result with Memory_Free*(). static char *M_GetAvailableOptions(const CONFIG_OPTION *const option) { if (option == nullptr) { return nullptr; } switch (option->type) { case COT_BOOL: return Memory_DupStr(GS("general/osd/command_bool")); case COT_INT32: return Memory_DupStr(GS("general/osd/command_integer")); case COT_DOUBLE: case COT_FLOAT: return Memory_DupStr(GS("general/osd/command_decimal")); case COT_FLOAT_PERCENT: return Memory_DupStr(GS("general/osd/command_percent")); case COT_ENUM: { const char *enum_name = (const char *)option->param; VECTOR *const values = EnumMap_ListValues(enum_name); if (values == nullptr) { return nullptr; } char *const result = M_FormatValuesList(values); Vector_Free(values); return result; } case COT_DYNAMIC_ENUM: return M_FormatDynamicEnumDefaults(option); case COT_STRING: return nullptr; default: return nullptr; } } char *Console_Cmd_Config_NormalizeKey(const char *key) { // TODO: Once we support arbitrary glyphs, this conversion should // no longer be necessary. char *result = Memory_DupStr(key); for (uint32_t i = 0; i < strlen(result); i++) { if (result[i] == '_') { result[i] = '-'; } } return result; } const CONFIG_OPTION *Console_Cmd_Config_GetOptionFromTarget( const void *const target) { for (const CONFIG_OPTION *option = Config_GetOptionMap(); option->name != nullptr; option++) { if (option->target == target) { return option; } } return nullptr; } COMMAND_RESULT Console_Cmd_Config_Helper( const CONFIG_OPTION *const option, const char *const new_value) { ASSERT(option != nullptr); char *normalized_name = Console_Cmd_Config_NormalizeKey(option->name); COMMAND_RESULT result = CR_FAILURE; if (new_value == nullptr || String_IsEmpty(new_value)) { char *value_str = M_GetValueForConsole(option); if (value_str == nullptr) { result = CR_FAILURE; goto cleanup; } Console_Log( GS("general/osd/config_option_get"), normalized_name, value_str); Memory_FreePointer(&value_str); result = CR_SUCCESS; goto cleanup; } if (M_TryApplyOptionValue(option, new_value)) { Config_Update(); char *value_str = M_GetValueForConsole(option); ASSERT(value_str != nullptr); Console_Log( GS("general/osd/config_option_set"), normalized_name, value_str); Memory_FreePointer(&value_str); result = CR_SUCCESS; } else { // Report bad invocation on the provided new value Console_LogError(GS("general/osd/command_bad_invocation"), new_value); char *available_options = M_GetAvailableOptions(option); if (available_options != nullptr) { Console_Log( GS("general/osd/command_valid_values"), available_options); Memory_FreePointer(&available_options); } result = CR_FAILURE; } cleanup: Memory_FreePointer(&normalized_name); return result; } VECTOR *Console_Cmd_Config_GetOptionsFromKey(const char *const key) { return M_GetOptionsFuzzy(key); } REGISTER_CONSOLE_COMMAND("set", M_Entrypoint, GS_ID("console/cmd/set/help")) ================================================ FILE: src/trx/game/console/cmd/config.h ================================================ #pragma once #include #include #include #include char *Console_Cmd_Config_NormalizeKey(const char *key); bool Console_Cmd_Config_SetCurrentValue( const CONFIG_OPTION *option, const char *new_value); // Returns a vector of STRING_FUZZY_MATCH entries for the given key. // Caller must free the result with Vector_Free(). VECTOR *Console_Cmd_Config_GetOptionsFromKey(const char *key); // Returns a pointer to a CONFIG_OPTION from a pointer into g_Config. const CONFIG_OPTION *Console_Cmd_Config_GetOptionFromTarget(const void *target); COMMAND_RESULT Console_Cmd_Config_Helper( const CONFIG_OPTION *option, const char *new_value); ================================================ FILE: src/trx/game/console/cmd/debug.c ================================================ #include #include #include #include #include #include #include #include typedef struct { bool *target; const CONFIG_OPTION *option; } DEBUG_OPTION_ENTRY; static DEBUG_OPTION_ENTRY m_AllOptions[] = { { &g_Config.debug.enable_debug_portals, nullptr }, { &g_Config.debug.enable_debug_room_clip, nullptr }, { &g_Config.debug.enable_debug_triggers, nullptr }, { &g_Config.debug.enable_debug_spheres, nullptr }, { &g_Config.debug.enable_debug_bounding_boxes, nullptr }, { &g_Config.debug.enable_debug_pos, nullptr }, { &g_Config.debug.enable_debug_anim, nullptr }, { &g_Config.debug.enable_debug_camera, nullptr }, { &g_Config.debug.enable_debug_status, nullptr }, { nullptr, nullptr } }; static void M_InitOptions(void) { for (int32_t i = 0; m_AllOptions[i].target; i++) { m_AllOptions[i].option = Console_Cmd_Config_GetOptionFromTarget(m_AllOptions[i].target); } } static VECTOR *M_BuildMatches(const char *const key) { VECTOR *const matches = Console_Cmd_Config_GetOptionsFromKey(key); VECTOR *const filtered = Vector_Create(sizeof(STRING_FUZZY_MATCH)); for (int32_t i = 0; i < matches->count; i++) { const STRING_FUZZY_MATCH *const match = Vector_Get(matches, i); const CONFIG_OPTION *const option = match->value; for (int32_t j = 0; m_AllOptions[j].target; j++) { if (option == m_AllOptions[j].option) { Vector_Add(filtered, match); break; } } } Vector_Free(matches); return filtered; } static void M_LogOption(const CONFIG_OPTION *const option) { char *const name = Console_Cmd_Config_NormalizeKey(option->name); Console_Log( GS("general/osd/config_option_set"), name, Config_GetOptionValueAsString(option, false)); Memory_Free(name); } static void M_ShowOption(const CONFIG_OPTION *const option) { char *const name = Console_Cmd_Config_NormalizeKey(option->name); Console_Log( GS("general/osd/config_option_get"), name, Config_GetOptionValueAsString(option, false)); Memory_Free(name); } static bool M_UpdateOption(const CONFIG_OPTION *const option, const bool enable) { bool *const target = (bool *)option->target; if (*target == enable) { return false; } *target = enable; M_LogOption(option); return true; } static bool M_UpdateAll(const bool enable) { bool changed = false; for (int32_t i = 0; m_AllOptions[i].target != nullptr; i++) { if (M_UpdateOption(m_AllOptions[i].option, enable)) { changed = true; } } return changed; } static void M_ShowStatus(void) { for (int32_t i = 0; m_AllOptions[i].target != nullptr; i++) { M_ShowOption(m_AllOptions[i].option); } } static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx) { if (m_AllOptions[0].option == nullptr) { M_InitOptions(); } if (String_IsEmpty(ctx->args)) { M_ShowStatus(); return CR_SUCCESS; } char *args = Memory_DupStr(ctx->args); char *space = strchr(args, ' '); char *key = args; char *val = nullptr; if (space) { *space = '\0'; val = space + 1; } if (val != nullptr && strchr(val, ' ') != nullptr) { Memory_Free(args); return CR_BAD_INVOCATION; } bool use_set = false; bool explicit_enable = false; if (val == nullptr && String_ParseBool(key, &explicit_enable)) { if (M_UpdateAll(explicit_enable)) { Config_Update(); } Memory_Free(args); return CR_SUCCESS; } else if (val != nullptr && String_ParseBool(val, &explicit_enable)) { use_set = true; } else if (val != nullptr) { Memory_Free(args); return CR_BAD_INVOCATION; } VECTOR *const matches = M_BuildMatches(key); if (matches->count == 0) { Console_LogError(GS("general/osd/config_option_unknown_option"), key); Vector_Free(matches); Memory_Free(args); return CR_FAILURE; } bool changed = false; for (int32_t i = 0; i < matches->count; i++) { const STRING_FUZZY_MATCH *const match = Vector_Get(matches, i); const CONFIG_OPTION *const option = match->value; const bool enable = use_set ? explicit_enable : !*(bool *)option->target; if (M_UpdateOption(option, enable)) { changed = true; } } if (changed) { Config_Update(); } Vector_Free(matches); Memory_Free(args); return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND("debug", M_Entrypoint, GS_ID("console/cmd/debug/help")) ================================================ FILE: src/trx/game/console/cmd/die.c ================================================ #include #include #include #include #include #include #include #include static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { if (!String_IsEmpty(ctx->args)) { return CR_BAD_INVOCATION; } if (!Game_IsPlayable()) { return CR_UNAVAILABLE; } LARA_INFO *const lara = Lara_GetLaraInfo(); ITEM *const lara_item = Lara_GetItem(); if (lara_item->hit_points <= 0) { return CR_UNAVAILABLE; } Sound_Effect(SFX_LARA_FALL, &lara_item->pos, SPM_NORMAL); Sound_Effect(SFX_EXPLOSION_1, &lara_item->pos, SPM_NORMAL); Item_Explode(lara->item_num, -1, 1); lara_item->hit_points = 0; lara_item->flags |= IF_ONE_SHOT; return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND("abortion", M_Entrypoint, nullptr) REGISTER_CONSOLE_COMMAND("natla-?s(uc|tin)ks", M_Entrypoint, nullptr) ================================================ FILE: src/trx/game/console/cmd/easy_config.c ================================================ #include #include #include #include typedef struct { const char *prefix; void *target; } COMMAND_TO_OPTION_MAP; static COMMAND_TO_OPTION_MAP m_CommandToOptionMap[] = { { "braid", &g_Config.visuals.enable_braid }, { "cheats", &g_Config.gameplay.enable_cheats }, { "vsync", &g_Config.rendering.enable_vsync }, { "wireframe", &g_Config.rendering.enable_wireframe }, { "fps", &g_Config.rendering.fps }, { "lighting", &g_Config.rendering.enable_lighting }, { "textures", &g_Config.rendering.enable_textures }, { nullptr, nullptr }, }; static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { COMMAND_TO_OPTION_MAP *match = m_CommandToOptionMap; while (match->target != nullptr) { if (String_Equivalent(match->prefix, ctx->prefix)) { return Console_Cmd_Config_Helper( Console_Cmd_Config_GetOptionFromTarget(match->target), ctx->args); } match++; } return CR_FAILURE; } REGISTER_CONSOLE_COMMAND("braid", M_Entrypoint, GS_ID("console/cmd/braid/help")) REGISTER_CONSOLE_COMMAND( "cheats", M_Entrypoint, GS_ID("console/cmd/cheats/help")) REGISTER_CONSOLE_COMMAND("vsync", M_Entrypoint, GS_ID("console/cmd/vsync/help")) REGISTER_CONSOLE_COMMAND( "wireframe", M_Entrypoint, GS_ID("console/cmd/wireframe/help")) REGISTER_CONSOLE_COMMAND("fps", M_Entrypoint, GS_ID("console/cmd/fps/help")) REGISTER_CONSOLE_COMMAND( "lighting", M_Entrypoint, GS_ID("console/cmd/lighting/help")) REGISTER_CONSOLE_COMMAND( "textures", M_Entrypoint, GS_ID("console/cmd/textures/help")) ================================================ FILE: src/trx/game/console/cmd/end_level.c ================================================ #include #include #include #include static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { if (!String_IsEmpty(ctx->args)) { return CR_BAD_INVOCATION; } if (GF_GetCurrentLevel() == nullptr || GF_GetCurrentLevel()->type == GFL_TITLE) { return CR_UNAVAILABLE; } Lara_Cheat_EndLevel(); return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND( "endlevel", M_Entrypoint, GS_ID("console/cmd/end_level/help")) REGISTER_CONSOLE_COMMAND( "nextlevel", M_Entrypoint, GS_ID("console/cmd/end_level/help")) REGISTER_CONSOLE_COMMAND("end-level", M_Entrypoint, nullptr) REGISTER_CONSOLE_COMMAND("next-level", M_Entrypoint, nullptr) ================================================ FILE: src/trx/game/console/cmd/exit_game.c ================================================ #include #include #include static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { if (!String_IsEmpty(ctx->args)) { return CR_BAD_INVOCATION; } GF_OverrideCommand((GF_COMMAND) { .action = GF_EXIT_GAME }); return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND("exit", M_Entrypoint, GS_ID("console/cmd/exit/help")) REGISTER_CONSOLE_COMMAND("quit", M_Entrypoint, GS_ID("console/cmd/exit/help")) ================================================ FILE: src/trx/game/console/cmd/exit_to_title.c ================================================ #include #include #include static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { if (!String_IsEmpty(ctx->args)) { return CR_BAD_INVOCATION; } GF_OverrideCommand((GF_COMMAND) { .action = GF_EXIT_TO_TITLE }); return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND("title", M_Entrypoint, GS_ID("console/cmd/title/help")) ================================================ FILE: src/trx/game/console/cmd/flipmap.c ================================================ #include #include #include #include #include #include #include static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { if (GF_GetCurrentLevel() == nullptr || GF_GetCurrentLevel()->type == GFL_TITLE) { return CR_UNAVAILABLE; } if (!Game_IsLoaded()) { return CR_UNAVAILABLE; } bool new_state = Room_GetFlipStatus(); if (String_IsEmpty(ctx->args)) { new_state = !new_state; } else if (!String_ParseBool(ctx->args, &new_state)) { return CR_BAD_INVOCATION; } if (Room_GetFlipStatus() == new_state) { Console_LogWarning( new_state ? GS("general/osd/flipmap_fail_already_on") : GS("general/osd/flipmap_fail_already_off")); return CR_SUCCESS; } Room_FlipMap(); Console_Log( new_state ? GS("general/osd/flipmap_on") : GS("general/osd/flipmap_off")); return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND( "flip", M_Entrypoint, GS_ID("console/cmd/flipmap/help")) REGISTER_CONSOLE_COMMAND( "flipmap", M_Entrypoint, GS_ID("console/cmd/flipmap/help")) ================================================ FILE: src/trx/game/console/cmd/flood.c ================================================ #include #include #include #include static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { int32_t room_num; if (!String_ParseInteger(ctx->args, &room_num)) { if (!String_IsEmpty(ctx->args)) { return CR_BAD_INVOCATION; } ITEM *const lara_item = Lara_GetItem(); if (lara_item == nullptr) { return CR_UNAVAILABLE; } room_num = lara_item->room_num; } if (String_Equivalent(ctx->prefix, "flood")) { Room_Get(room_num)->flags.underwater = true; } else if (String_Equivalent(ctx->prefix, "drain")) { Room_Get(room_num)->flags.underwater = false; } else { return CR_UNAVAILABLE; } return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND("flood", M_Entrypoint, GS_ID("console/cmd/flood/help")) REGISTER_CONSOLE_COMMAND("drain", M_Entrypoint, GS_ID("console/cmd/drain/help")) ================================================ FILE: src/trx/game/console/cmd/fly.c ================================================ #include #include #include #include #include #include static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { if (!Game_IsPlayable()) { return CR_UNAVAILABLE; } bool enable; if (String_ParseBool(ctx->args, &enable)) { if (enable) { Lara_Cheat_EnterFlyMode(); } else { Lara_Cheat_ExitFlyMode(); } return CR_SUCCESS; } if (!String_IsEmpty(ctx->args)) { return CR_BAD_INVOCATION; } const LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->water_status == LWS_CHEAT) { Lara_Cheat_ExitFlyMode(); } else { Lara_Cheat_EnterFlyMode(); } return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND("fly", M_Entrypoint, GS_ID("console/cmd/fly/help")) ================================================ FILE: src/trx/game/console/cmd/give_item.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include static bool M_CanTargetObjectPickup(const OBJECT_ID obj_id) { return Object_IsType(obj_id, g_InvObjects) && Object_Get(obj_id)->loaded && Object_IsType( Object_GetCognateInverse(obj_id, g_ItemToInvObjectMap), g_PickupObjects); } static bool M_Match(const COMMAND_CONTEXT *const ctx, const char *const cmd) { return String_Equivalent(ctx->prefix, cmd) || String_Equivalent(ctx->args, cmd); } static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { if (!Game_IsPlayable()) { return CR_UNAVAILABLE; } if (M_Match(ctx, "keys")) { return Lara_Cheat_GiveAllKeys() ? CR_SUCCESS : CR_FAILURE; } if (M_Match(ctx, "guns")) { return Lara_Cheat_GiveAllGuns(false) ? CR_SUCCESS : CR_FAILURE; } if (M_Match(ctx, "moreguns")) { return Lara_Cheat_GiveAllGuns(true) ? CR_SUCCESS : CR_FAILURE; } if (String_Equivalent(ctx->args, "all")) { return Lara_Cheat_GiveAllItems() ? CR_SUCCESS : CR_FAILURE; } int32_t num = 1; const char *args = ctx->args; if (sscanf(ctx->args, "%d ", &num) == 1) { args = strstr(args, " "); if (args == nullptr) { return CR_BAD_INVOCATION; } CLAMPG(num, MAX_QTY); args++; } if (String_IsEmpty(ctx->args)) { return CR_BAD_INVOCATION; } bool found = false; int32_t match_count = 0; OBJECT_NAME_MATCH *matches = Object_IdsFromName(args, &match_count, M_CanTargetObjectPickup); for (int32_t i = 0; i < match_count; i++) { const OBJECT_ID obj_id = matches[i].object_id; const char *const obj_name = matches[i].matched_name != nullptr ? matches[i].matched_name : args; Inv_AddItemNTimes(obj_id, num); Console_Log(GS("general/osd/give_item"), obj_name); found = true; } Memory_FreePointer(&matches); if (!found) { Console_LogError(GS("general/osd/invalid_item"), args); return CR_FAILURE; } return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND( "give|keys|(?:more)?guns", M_Entrypoint, GS_ID("console/cmd/give/help")) ================================================ FILE: src/trx/game/console/cmd/give_secret.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #define M_FMT_NUM "#%d" static const char *M_FormatAvailable(void) { LEVEL_MAX_STATS *const max_stats = Stats_GetLevelMaxStats(Game_GetCurrentLevel()); ASSERT(max_stats != nullptr); char buf[128] = {}; char *ptr = buf; bool first = true; for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) { if ((1 << i) & max_stats->all_secrets_mask) { if (!first) { ptr += sprintf(ptr, ", "); } first = false; ptr += sprintf(ptr, M_FMT_NUM, i + 1); } } return String_FormatStatic("%s", buf); } static const char *M_FormatPresent(void) { char buf[128] = {}; char *ptr = buf; bool first = true; for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) { if (Stats_HasSecret(i)) { if (!first) { ptr += sprintf(ptr, ", "); } first = false; ptr += sprintf(ptr, M_FMT_NUM, i + 1); } } return String_FormatStatic("%s", buf); } static void M_LogInvalid(const int32_t idx) { Console_LogError( GS("console/cmd/give/invalid_secret"), String_FormatStatic(M_FMT_NUM, idx + 1), M_FormatAvailable()); } static COMMAND_RESULT M_TakeSecret(const int32_t idx) { if (Stats_RemoveSecret(idx)) { Console_Log( GS("console/cmd/give/secret_taken"), String_FormatStatic(M_FMT_NUM, idx + 1)); return CR_SUCCESS; } M_LogInvalid(idx); return CR_FAILURE; } static COMMAND_RESULT M_GiveSecret(const int32_t idx) { if (Stats_AddSecret(idx)) { Console_Log( GS("console/cmd/give/secret_given"), String_FormatStatic(M_FMT_NUM, idx + 1)); return CR_SUCCESS; } M_LogInvalid(idx); return CR_FAILURE; } static COMMAND_RESULT M_ListSecrets(void) { const LEVEL_MAX_STATS *const max_stats = Stats_GetLevelMaxStats(Game_GetCurrentLevel()); ASSERT(max_stats != nullptr); RESUME_INFO *const info = Savegame_GetCurrentInfo(Game_GetCurrentLevel()); ASSERT(info != nullptr); const char *const buf = M_FormatPresent(); Console_Log( strcmp(buf, "") == 0 ? GS("console/cmd/give/secret_none") : GS("console/cmd/give/secret_list"), info->stats.secret_count, max_stats->max_secret_count, buf); return CR_SUCCESS; } static COMMAND_RESULT M_GiveAllSecrets(void) { for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) { Stats_AddSecret(i); // Not all `i` are valid, but it's handled inside } return M_ListSecrets(); } static COMMAND_RESULT M_TakeAllSecrets(void) { for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) { Stats_RemoveSecret(i); // Not all `i` are valid, but it's handled inside } return M_ListSecrets(); } static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { if (!Game_IsPlayable()) { return CR_UNAVAILABLE; } if (String_IsEmpty(ctx->args)) { return M_ListSecrets(); } if (String_Equivalent(ctx->args, "give")) { return M_GiveAllSecrets(); } if (String_Equivalent(ctx->args, "take")) { return M_TakeAllSecrets(); } char *args = Memory_DupStr(ctx->args); char *subcmd = args; char *param = nullptr; if (!String_IsEmpty(ctx->args)) { param = strchr(args, ' '); if (param != nullptr) { *param = '\0'; param++; } } if (param == nullptr || String_IsEmpty(param)) { Memory_FreePointer(&args); return CR_BAD_INVOCATION; } int32_t num; if (!String_ParseInteger(param, &num)) { Memory_FreePointer(&args); return CR_BAD_INVOCATION; } COMMAND_RESULT result = CR_FAILURE; if (String_Equivalent(subcmd, "take")) { result = M_TakeSecret(num - 1); } else if (String_Equivalent(subcmd, "give")) { result = M_GiveSecret(num - 1); } else { result = CR_BAD_INVOCATION; } Memory_FreePointer(&args); return result; } REGISTER_CONSOLE_COMMAND( "secret", M_Entrypoint, GS_ID("console/cmd/give_secret/help")) ================================================ FILE: src/trx/game/console/cmd/heal.c ================================================ #include #include #include #include #include #include #include #include static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { if (!String_IsEmpty(ctx->args)) { return CR_BAD_INVOCATION; } if (!Game_IsPlayable()) { return CR_UNAVAILABLE; } ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara_item->hit_points == LARA_MAX_HITPOINTS) { Console_LogWarning(GS("general/osd/heal_already_full_hp")); } else { Console_Log(GS("general/osd/heal_success")); } lara_item->hit_points = LARA_MAX_HITPOINTS; lara->poison_timer = 0; Lara_Extinguish(); return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND("heal", M_Entrypoint, GS_ID("console/cmd/heal/help")) ================================================ FILE: src/trx/game/console/cmd/help.c ================================================ #include #include #include #include #include static COMMAND_RESULT M_ListAllCommands(void) { VECTOR *const vec = Console_Registry_GetAll(); const CONSOLE_COMMAND **list = (const CONSOLE_COMMAND **)Vector_GetData(vec); // compute buffer size int32_t total = 0; for (int32_t i = 0; i < vec->count; i++) { if (list[i]->help_id == nullptr) { continue; } total += strlen(list[i]->prefix) + (i + 1 < vec->count ? 2 : 0); } char *buf = Memory_Alloc(total + 1); for (int32_t i = 0; i < vec->count; i++) { if (list[i]->help_id == nullptr) { continue; } strcat(buf, list[i]->prefix); if (i + 1 < vec->count) { strcat(buf, ", "); } } Console_Log("%s", GS("console/cmd/help/list")); Console_Log("%s", buf); Memory_Free(buf); Vector_Free(vec); return CR_SUCCESS; } static COMMAND_RESULT M_ShowSpecificCommand(const char *const cmd_name) { const CONSOLE_COMMAND *cmd = Console_Registry_Get(cmd_name); if (cmd == nullptr || cmd->help_id == nullptr) { Console_LogError(GS("general/osd/unknown_command"), cmd_name); return CR_FAILURE; } const char *const help = GameString_Get(cmd->help_id); Console_Log("%s", help); return CR_SUCCESS; } static COMMAND_RESULT M_Help(const COMMAND_CONTEXT *const ctx) { if (ctx->args != nullptr && *ctx->args != '\0') { return M_ShowSpecificCommand(ctx->args); } return M_ListAllCommands(); } REGISTER_CONSOLE_COMMAND("help", M_Help, GS_ID("console/cmd/help/help")) ================================================ FILE: src/trx/game/console/cmd/immune.c ================================================ #include #include #include #include #include #include static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { bool enable; if (String_ParseBool(ctx->args, &enable)) { g_Config.debug.enable_invulnerability = enable; Config_Update(); } else if (!String_IsEmpty(ctx->args)) { return CR_BAD_INVOCATION; } else { g_Config.debug.enable_invulnerability = !g_Config.debug.enable_invulnerability; Config_Update(); } Console_Log( g_Config.debug.enable_invulnerability ? GS("console/cmd/immune/on") : GS("console/cmd/immune/off")); return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND( "immune", M_Entrypoint, GS_ID("console/cmd/immune/help")) REGISTER_CONSOLE_COMMAND( "immunity", M_Entrypoint, GS_ID("console/cmd/immune/help")) ================================================ FILE: src/trx/game/console/cmd/inf_sprint.c ================================================ #include #include #include #include #include #include static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { bool enable; if (String_ParseBool(ctx->args, &enable)) { g_Config.debug.enable_endless_sprint = enable; Config_Update(); } else if (!String_IsEmpty(ctx->args)) { return CR_BAD_INVOCATION; } else { g_Config.debug.enable_endless_sprint = !g_Config.debug.enable_endless_sprint; Config_Update(); } Console_Log( g_Config.debug.enable_endless_sprint ? GS("console/cmd/inf_sprint/on") : GS("console/cmd/inf_sprint/off")); return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND( "restless", M_Entrypoint, GS_ID("console/cmd/inf_sprint/help")) ================================================ FILE: src/trx/game/console/cmd/kill.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include extern bool CombatEnd_IsWaitingForBoss(void); extern OBJECT_ID CombatEnd_GetBossType(void); static bool M_CanTargetObjectCreature(const OBJECT_ID obj_id) { return (Object_IsType(obj_id, g_CreatureObjects) || Object_IsType(obj_id, g_LoyalObjects)) && Object_Get(obj_id)->loaded; } static bool M_KillSingleEnemyInRange(const int32_t max_dist) { const ITEM *const lara_item = Lara_GetItem(); int32_t best_dist = -1; int16_t best_item_num = NO_ITEM; for (int16_t item_num = 0; item_num < Item_GetTotalCount(); item_num++) { const ITEM *const item = Item_Get(item_num); if (Creature_IsHostile(item)) { const int32_t dist = Item_GetDistance(item, lara_item->pos); if (dist <= max_dist) { if (best_item_num == NO_ITEM || dist < best_dist) { best_dist = dist; best_item_num = item_num; } } } } if (best_item_num != NO_ITEM) { if (Lara_Cheat_KillEnemy(best_item_num)) { return true; } } return false; } static int32_t M_KillAllEnemiesInRange(const int32_t max_dist) { int32_t kill_count = 0; const ITEM *const lara_item = Lara_GetItem(); for (int16_t item_num = 0; item_num < Item_GetTotalCount(); item_num++) { const ITEM *const item = Item_Get(item_num); if (Creature_IsHostile(item)) { const int32_t dist = Item_GetDistance(item, lara_item->pos); if (dist <= max_dist) { // Kill this enemy if (Lara_Cheat_KillEnemy(item_num)) { kill_count++; } } } } return kill_count; } static COMMAND_RESULT M_KillAllEnemies(void) { int32_t num_killed = 0; for (int16_t item_num = 0; item_num < Item_GetTotalCount(); item_num++) { const ITEM *const item = Item_Get(item_num); if (!Creature_IsHostile(item)) { continue; } if (item->object_id == CombatEnd_GetBossType() && CombatEnd_IsWaitingForBoss()) { continue; } if (Lara_Cheat_KillEnemy(item_num)) { num_killed++; } } if (num_killed == 0) { Console_LogError(GS("general/osd/kill_all_fail")); return CR_FAILURE; } Console_Log(GS("general/osd/kill_all"), num_killed); return CR_SUCCESS; } static COMMAND_RESULT M_KillNearestEnemies(void) { const ITEM *const lara_item = Lara_GetItem(); int32_t kill_count = M_KillAllEnemiesInRange(WALL_L); if (kill_count == 0) { kill_count = M_KillSingleEnemyInRange(5 * WALL_L); } if (kill_count == 0) { // No enemies killed Console_LogError(GS("general/osd/kill_fail")); return CR_FAILURE; } else { // At least one enemy was killed. Console_Log(GS("general/osd/kill")); return CR_SUCCESS; } } static COMMAND_RESULT M_KillEnemyType(const char *const enemy_name) { bool matches_found = false; int32_t num_killed = 0; int32_t match_count = 0; OBJECT_NAME_MATCH *matches = Object_IdsFromName(enemy_name, &match_count, M_CanTargetObjectCreature); for (int16_t item_num = 0; item_num < Item_GetTotalCount(); item_num++) { const ITEM *const item = Item_Get(item_num); bool is_matched = false; for (int32_t i = 0; i < match_count; i++) { if (matches[i].object_id == item->object_id) { is_matched = true; break; } } if (!is_matched) { continue; } matches_found = true; if (Lara_Cheat_KillEnemy(item_num)) { num_killed++; } } Memory_FreePointer(&matches); if (!matches_found) { Console_LogError(GS("general/osd/invalid_object"), enemy_name); return CR_FAILURE; } if (num_killed == 0) { Console_LogError(GS("general/osd/object_not_found"), enemy_name); return CR_FAILURE; } Console_Log(GS("general/osd/kill_all"), num_killed); return CR_SUCCESS; } static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { if (!Game_IsLoaded()) { return CR_UNAVAILABLE; } if (String_Equivalent(ctx->args, "all")) { return M_KillAllEnemies(); } if (String_IsEmpty(ctx->args)) { return M_KillNearestEnemies(); } return M_KillEnemyType(ctx->args); } REGISTER_CONSOLE_COMMAND("kill", M_Entrypoint, GS_ID("console/cmd/kill/help")) ================================================ FILE: src/trx/game/console/cmd/load_game.c ================================================ #include #include #include #include #include #include #include #include typedef struct { SAVEGAME_SLOT_POOL pool; int32_t slot_num; bool has_slot_num; } M_SLOT_REQUEST; static bool M_TryParseQuickArg( const char *const args, int32_t *const slot_num, bool *const has_slot_num) { *has_slot_num = false; if (String_IsEmpty(args)) { return true; } char tail = '\0'; if (String_ParseInteger(args, slot_num) || sscanf(args, " quick %d %c", slot_num, &tail) == 1 || sscanf(args, " q%d %c", slot_num, &tail) == 1) { *has_slot_num = true; return true; } return false; } static bool M_TryParseLoadArg( const char *const args, M_SLOT_REQUEST *const request) { int32_t slot_num = -1; if (String_ParseInteger(args, &slot_num)) { request->pool = SAVEGAME_SLOT_POOL_NORMAL; request->slot_num = slot_num; request->has_slot_num = true; return true; } char tail = '\0'; if (sscanf(args, " quick %d %c", &slot_num, &tail) == 1 || sscanf(args, " q%d %c", &slot_num, &tail) == 1) { request->pool = SAVEGAME_SLOT_POOL_QUICK; request->slot_num = slot_num; request->has_slot_num = true; return true; } if (sscanf(args, " quick %c", &tail) == 0 || sscanf(args, " q %c", &tail) == 0 || sscanf(args, " q%c", &tail) == 0) { request->pool = SAVEGAME_SLOT_POOL_QUICK; request->slot_num = -1; request->has_slot_num = false; return true; } return false; } static COMMAND_RESULT M_ExecuteLoad(const M_SLOT_REQUEST request) { SAVEGAME_SLOT_REF slot = Savegame_InvalidSlot(); int32_t shown_slot_num = request.slot_num; if (request.pool == SAVEGAME_SLOT_POOL_QUICK) { const int32_t visual_count = Savegame_GetQuickVisualCount(); const int32_t visual_slot = request.has_slot_num ? request.slot_num : 1; if (visual_slot < 1 || visual_slot > visual_count) { Console_LogError( GS("general/osd/load_game_fail_invalid_slot"), visual_slot); return CR_FAILURE; } slot = Savegame_QuickFromVisualIndex(visual_slot - 1); shown_slot_num = visual_slot; } else { const int32_t slot_idx = request.slot_num - 1; // convert 1-indexing to 0-indexing if (slot_idx < 0 || slot_idx >= Savegame_GetSlotCount(SAVEGAME_SLOT_POOL_NORMAL)) { Console_LogError( GS("general/osd/load_game_fail_invalid_slot"), request.slot_num); return CR_FAILURE; } slot = Savegame_NormalSlot(slot_idx); } if (Savegame_IsSlotFree(slot)) { Console_LogError( GS("general/osd/load_game_fail_unavailable_slot"), shown_slot_num); return CR_FAILURE; } GF_OverrideCommand((GF_COMMAND) { .action = GF_START_SAVED_GAME, .param = Savegame_SlotToParam(slot), }); if (request.pool == SAVEGAME_SLOT_POOL_QUICK) { Console_Log(GS("general/osd/quick_load"), shown_slot_num); } else { Console_Log(GS("general/osd/load_game"), shown_slot_num); } return CR_SUCCESS; } static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { M_SLOT_REQUEST request; if (!M_TryParseLoadArg(ctx->args, &request)) { return CR_BAD_INVOCATION; } return M_ExecuteLoad(request); } static COMMAND_RESULT M_EntrypointQL(const COMMAND_CONTEXT *const ctx) { M_SLOT_REQUEST request = { .pool = SAVEGAME_SLOT_POOL_QUICK, .slot_num = -1, .has_slot_num = false, }; if (!M_TryParseQuickArg( ctx->args, &request.slot_num, &request.has_slot_num)) { return CR_BAD_INVOCATION; } return M_ExecuteLoad(request); } REGISTER_CONSOLE_COMMAND("load", M_Entrypoint, GS_ID("console/cmd/load/help")) REGISTER_CONSOLE_COMMAND( "quickload", M_EntrypointQL, GS_ID("console/cmd/load/help")) REGISTER_CONSOLE_COMMAND("ql", M_EntrypointQL, GS_ID("console/cmd/load/help")) ================================================ FILE: src/trx/game/console/cmd/lua.c ================================================ #include #include #include #include #include static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx) { if (String_IsEmpty(ctx->args)) { return CR_BAD_INVOCATION; } COMMAND_RESULT cmd_result; LUA_RESULT eval_result = Lua_Eval(ctx->args); if (eval_result.code == LUA_ERRSYNTAX) { Console_LogError( GS("console/cmd/lua/syntax_error"), eval_result.message); cmd_result = CR_FAILURE; } else if (eval_result.code != LUA_OK) { Console_LogError( GS("console/cmd/lua/runtime_error"), eval_result.message); cmd_result = CR_FAILURE; } else { cmd_result = CR_SUCCESS; } Lua_FreeResult(&eval_result); return cmd_result; } REGISTER_CONSOLE_COMMAND("lua", M_Entrypoint, GS_ID("console/cmd/lua/help")) ================================================ FILE: src/trx/game/console/cmd/mod.c ================================================ #include #include #include #include #include #include static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { if (String_IsEmpty(ctx->args)) { const SHELL_ARGS *const args = Shell_GetArgs(); Console_Log("Currently loaded mod: %s", args->mod->name); return CR_SUCCESS; } const SHELL_MOD *const mod = Shell_GetModByName(ctx->args); if (!Shell_CanSwitchToMod(mod)) { Console_LogError("Invalid mod: %s", ctx->args); return CR_FAILURE; } Shell_RequestModSwitch(mod->name); GF_OverrideCommand((GF_COMMAND) { .action = GF_SWITCH_MOD }); return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND("mod", M_Entrypoint, GS_ID("console/cmd/mod/help")) ================================================ FILE: src/trx/game/console/cmd/music.c ================================================ #include #include #include #include #include static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { int32_t track_to_play = -1; if (String_IsEmpty(ctx->args)) { return CR_BAD_INVOCATION; } else if (String_ParseInteger(ctx->args, &track_to_play)) { if (track_to_play == 0 || track_to_play == -1) { Music_Stop(); Console_Log(GS("console/cmd/play_music/stopped")); } else if (Music_Play_Direct(track_to_play, MPM_ONCE)) { Console_Log(GS("console/cmd/play_music/track"), track_to_play); } else { Console_LogError(GS("console/cmd/play_music/invalid_track")); } return CR_SUCCESS; } else { return CR_BAD_INVOCATION; } } REGISTER_CONSOLE_COMMAND("music", M_Entrypoint, GS_ID("console/cmd/music/help")) ================================================ FILE: src/trx/game/console/cmd/play_cutscene.c ================================================ #include #include #include #include #include static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { int32_t cutscene_to_load = -1; if (String_IsEmpty(ctx->args)) { return CR_BAD_INVOCATION; } else if (String_ParseInteger(ctx->args, &cutscene_to_load)) { cutscene_to_load--; const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_CUTSCENES); if (cutscene_to_load < 0 || cutscene_to_load >= level_table->count) { Console_LogError(GS("general/osd/invalid_cutscene")); return CR_FAILURE; } const GF_LEVEL *const level = &level_table->levels[cutscene_to_load]; GF_OverrideCommand((GF_COMMAND) { .action = GF_START_CINE, .param = cutscene_to_load, }); Console_Log(GS("general/osd/play_cutscene"), level->num + 1); return CR_SUCCESS; } else { return CR_BAD_INVOCATION; } } REGISTER_CONSOLE_COMMAND( "cut", M_Entrypoint, GS_ID("console/cmd/play_cutscene/help")) REGISTER_CONSOLE_COMMAND( "cutscene", M_Entrypoint, GS_ID("console/cmd/play_cutscene/help")) ================================================ FILE: src/trx/game/console/cmd/play_demo.c ================================================ #include #include #include #include #include #include static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { int32_t demo_to_load = -1; const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_DEMOS); if (String_IsEmpty(ctx->args)) { demo_to_load = Demo_ChooseLevel(-1); } else if (String_ParseInteger(ctx->args, &demo_to_load)) { demo_to_load--; } else { return CR_BAD_INVOCATION; } if (demo_to_load < 0 || demo_to_load >= level_table->count) { Console_LogError(GS("general/osd/invalid_demo")); return CR_FAILURE; } const GF_LEVEL *const level = &level_table->levels[demo_to_load]; GF_OverrideCommand((GF_COMMAND) { .action = GF_START_DEMO, .param = demo_to_load, }); Console_Log(GS("general/osd/play_demo"), level->num + 1); return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND( "demo", M_Entrypoint, GS_ID("console/cmd/play_demo/help")) ================================================ FILE: src/trx/game/console/cmd/play_gym.c ================================================ #include #include #include #include #include static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { if (String_IsEmpty(ctx->args)) { const GF_LEVEL *const level = GF_GetGymLevel(); if (level == nullptr) { Console_LogError(GS("general/osd/invalid_level")); return CR_FAILURE; } GF_OverrideCommand((GF_COMMAND) { .action = GF_SELECT_GAME, .param = level->num, }); Console_Log(GS("general/osd/play_level"), level->title); return CR_SUCCESS; } else { return CR_BAD_INVOCATION; } } REGISTER_CONSOLE_COMMAND( "gym", M_Entrypoint, GS_ID("console/cmd/play_gym/help")) REGISTER_CONSOLE_COMMAND( "home", M_Entrypoint, GS_ID("console/cmd/play_gym/help")) ================================================ FILE: src/trx/game/console/cmd/play_level.c ================================================ #include #include #include #include #include static const GF_LEVEL *M_FindLevel(const char *const user_input) { int32_t level_num = -1; if (String_ParseInteger(user_input, &level_num)) { return GF_GetLevelByOrdinalNumber(GFLT_MAIN, level_num); } VECTOR *source = Vector_Create(sizeof(STRING_FUZZY_SOURCE)); for (int32_t i = 0; i < GF_GetLevelTable(GFLT_MAIN)->count; i++) { const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i); STRING_FUZZY_SOURCE source_item = { .key = level->title, .value = (void *)level, .weight = 1, }; if (source_item.key != nullptr) { Vector_Add(source, &source_item); } } const GF_LEVEL *const gym_level = GF_GetGymLevel(); if (gym_level != nullptr) { STRING_FUZZY_SOURCE source_item = { .key = "gym", .value = (void *)gym_level, .weight = 1, }; Vector_Add(source, &source_item); } const GF_LEVEL *result = nullptr; VECTOR *matches = String_FuzzyMatch(user_input, source); if (matches->count >= 1) { const STRING_FUZZY_MATCH *const match = Vector_Get(matches, 0); result = (const GF_LEVEL *)match->value; } if (matches != nullptr) { Vector_Free(matches); matches = nullptr; } if (source != nullptr) { Vector_Free(source); source = nullptr; } return result; } static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { if (String_IsEmpty(ctx->args)) { return CR_BAD_INVOCATION; } VECTOR *matches = nullptr; const GF_LEVEL *const level = M_FindLevel(ctx->args); if (level == nullptr || level->type == GFL_DUMMY || level->type == GFL_CURRENT) { Console_LogError(GS("general/osd/invalid_level")); return CR_FAILURE; } GF_OverrideCommand((GF_COMMAND) { .action = GF_SELECT_GAME, .param = level->num, }); Console_Log(GS("general/osd/play_level"), level->title); return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND( "play", M_Entrypoint, GS_ID("console/cmd/play_level/help")) REGISTER_CONSOLE_COMMAND( "level", M_Entrypoint, GS_ID("console/cmd/play_level/help")) ================================================ FILE: src/trx/game/console/cmd/pos.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { if (!Game_IsLoaded()) { return CR_UNAVAILABLE; } if (!String_IsEmpty(ctx->args)) { return CR_BAD_INVOCATION; } const GF_LEVEL *const current_level = GF_GetCurrentLevel(); if (current_level->type == GFL_TITLE) { return CR_UNAVAILABLE; } const char *level_type_fmt = nullptr; int32_t reindex = 0; switch (current_level->type) { case GFL_CUTSCENE: level_type_fmt = GS("general/osd/pos_level_fmt_cutscene"); reindex = 1; break; case GFL_DEMO: level_type_fmt = GS("general/osd/pos_level_fmt_demo"); reindex = 1; break; default: level_type_fmt = GS("general/osd/pos_level_fmt"); reindex = GF_GetGymLevel() == nullptr ? 1 : 0; break; } const char *const level_type = String_FormatStatic(level_type_fmt, current_level->num + reindex); const ITEM *const lara_item = Lara_GetItem(); const char *details; if (lara_item == nullptr) { details = String_FormatStatic("%s", GS("general/osd/pos_lara_missing")); } else { int16_t room_num = lara_item->room_num; const ROOM *const room = Room_Get(room_num); if (Room_GetFlipStatus() && room->flipped_room != NO_ROOM) { room_num = room->flipped_room; } details = String_FormatStatic( GS("general/osd/pos_lara_pos_fmt"), room_num, lara_item->pos.x / (float)WALL_L, lara_item->pos.y / (float)WALL_L, lara_item->pos.z / (float)WALL_L, lara_item->rot.x * 360.0f / (float)DEG_360, lara_item->rot.y * 360.0f / (float)DEG_360, lara_item->rot.z * 360.0f / (float)DEG_360); } const char *const glue = lara_item == nullptr ? "\n" : " "; const char *const message = current_level->title != nullptr && strcmp(level_type, current_level->title) == 0 ? String_FormatStatic("%s%s%s", level_type, glue, details) : String_FormatStatic( "%s (%s)%s%s", level_type, current_level->title, glue, details); Console_Log("%s", message); return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND("pos", M_Entrypoint, GS_ID("console/cmd/pos/help")) ================================================ FILE: src/trx/game/console/cmd/save_game.c ================================================ #include #include #include #include #include #include #include #include #include static bool M_TryParseQuickKeyword(const char *const args) { char tail = '\0'; return sscanf(args, " quick %c", &tail) == 0 || sscanf(args, " q %c", &tail) == 0; } static COMMAND_RESULT M_HandleQuickSave(void) { const SAVEGAME_SLOT_REF slot = Savegame_GetNextQuickSlot(); if (!Savegame_IsValidSlotRef(slot)) { Console_LogError(GS("general/osd/quick_save_fail_no_slots")); return CR_FAILURE; } Savegame_Save(slot); Console_Log(GS("general/osd/quick_save")); return CR_SUCCESS; } static COMMAND_RESULT M_EntrypointQS(const COMMAND_CONTEXT *const ctx) { if (!Game_IsPlayable()) { return CR_UNAVAILABLE; } if (!String_IsEmpty(ctx->args)) { return CR_BAD_INVOCATION; } return M_HandleQuickSave(); } static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { if (!Game_IsPlayable()) { return CR_UNAVAILABLE; } int32_t slot_num; if (String_ParseInteger(ctx->args, &slot_num)) { const int32_t slot_idx = slot_num - 1; // convert 1-indexing to 0-indexing if (slot_idx < 0 || slot_idx >= Savegame_GetSlotCount(SAVEGAME_SLOT_POOL_NORMAL)) { Console_LogError( GS("general/osd/save_game_fail_invalid_slot"), slot_num); return CR_BAD_INVOCATION; } Savegame_Save(Savegame_NormalSlot(slot_idx)); Console_Log(GS("general/osd/save_game"), slot_num); return CR_SUCCESS; } if (M_TryParseQuickKeyword(ctx->args)) { return M_HandleQuickSave(); } return CR_BAD_INVOCATION; } REGISTER_CONSOLE_COMMAND("save", M_Entrypoint, GS_ID("console/cmd/save/help")) REGISTER_CONSOLE_COMMAND( "quicksave", M_EntrypointQS, GS_ID("console/cmd/save/help")) REGISTER_CONSOLE_COMMAND("qs", M_EntrypointQS, GS_ID("console/cmd/save/help")) ================================================ FILE: src/trx/game/console/cmd/screenshot.c ================================================ #include #include #include #include #include #include #include static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx) { const char *const arg = ctx->args; if (arg == nullptr || String_IsEmpty(arg)) { Screenshot_Make(g_Config.rendering.screenshot_format); } else { Screenshot_MakeToPath(arg); } return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND( "screenshot", M_Entrypoint, GS_ID("console/cmd/screenshot/help")) ================================================ FILE: src/trx/game/console/cmd/set_health.c ================================================ #include #include #include #include #include #include #include #include #include static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { if (!Game_IsPlayable()) { return CR_UNAVAILABLE; } ITEM *const lara_item = Lara_GetItem(); if (String_IsEmpty(ctx->args)) { Console_Log( GS("general/osd/current_health_get"), lara_item->hit_points); return CR_SUCCESS; } int32_t hp; if (!String_ParseInteger(ctx->args, &hp)) { return CR_BAD_INVOCATION; } CLAMP(hp, 0, LARA_MAX_HITPOINTS); lara_item->hit_points = hp; Console_Log(GS("general/osd/current_health_set"), hp); return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND("hp", M_Entrypoint, GS_ID("console/cmd/hp/help")) ================================================ FILE: src/trx/game/console/cmd/sfx.c ================================================ #include #include #include #include #include #include #include #include static char *M_CreateRangeString(void) { size_t buffer_size = 64; char *result = Memory_Alloc(buffer_size); int32_t prev = -1; int32_t start = -1; const SAMPLE_ID max_id = Sound_GetMaxDirectSampleID(); for (SAMPLE_ID i = 0; i <= max_id; i++) { const bool valid = Sound_IsAvailable_Direct(i); if (valid && start == -1) { start = i; } if (!valid && start != -1) { char temp[32]; if (start == prev) { sprintf(temp, "%d, ", prev); } else { sprintf(temp, "%d-%d, ", start, prev); } const int32_t len = strlen(temp); if (strlen(result) + len >= buffer_size) { buffer_size *= 2; result = Memory_Realloc(result, buffer_size); } strcat(result, temp); start = -1; } if (valid) { prev = i; } } // Remove the trailing comma and space result[strlen(result) - 2] = '\0'; return result; } static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { if (String_IsEmpty(ctx->args)) { char *ranges = M_CreateRangeString(); Console_Log(GS("general/osd/sound_available_samples"), ranges); Memory_FreePointer(&ranges); return CR_SUCCESS; } SAMPLE_ID sfx_id; if (!String_ParseInteger(ctx->args, &sfx_id)) { return CR_BAD_INVOCATION; } if (!Sound_IsAvailable_Direct(sfx_id)) { Console_LogError(GS("general/osd/invalid_sample"), sfx_id); return CR_FAILURE; } Console_Log(GS("general/osd/sound_playing_sample"), sfx_id); Sound_Effect_Direct(sfx_id, nullptr, SPM_ALWAYS); return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND("sfx", M_Entrypoint, GS_ID("console/cmd/sfx/help")) ================================================ FILE: src/trx/game/console/cmd/spawn.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static GAME_VECTOR M_GetTargetPos( const ITEM *const lara_item, const int16_t angle_add) { const int16_t angle = lara_item->rot.y + angle_add; const int32_t s = Math_Sin(angle); const int32_t c = Math_Cos(angle); const int32_t dist = WALL_L; return (GAME_VECTOR) { .x = lara_item->pos.x + ((s * dist) >> W2V_SHIFT), .y = lara_item->pos.y, .z = lara_item->pos.z + ((c * dist) >> W2V_SHIFT), .room_num = lara_item->room_num, }; } static bool M_FindValidTargetPos( const ITEM *const lara_item, GAME_VECTOR *const out_pos) { for (int32_t angle = -DEG_45; angle <= DEG_45; angle += DEG_45) { GAME_VECTOR pos = M_GetTargetPos(lara_item, angle); if (Room_FindValidPos(&pos.pos, &pos.room_num)) { *out_pos = pos; return true; } } return false; } static bool M_CanSpawnObject(const OBJECT_ID object_id) { return !Object_IsType(object_id, g_NullObjects) && !Object_IsType(object_id, g_AnimObjects) && !Object_IsType(object_id, g_InvObjects) && Object_Get(object_id)->loaded; } static bool M_ShouldEnableBaddieAI(const OBJECT_ID object_id) { if (Object_IsType(object_id, g_CreatureObjects)) { return true; } if (Object_IsType(object_id, g_LoyalObjects)) { return true; } return Object_Get(object_id)->intelligent; } static bool M_SpawnItem( const OBJECT_ID object_id, const GAME_VECTOR target_pos, const XYZ_16 target_rot) { const int16_t item_num = Item_Create(); if (item_num == NO_ITEM) { return false; } ITEM *const new_item = Item_Get(item_num); new_item->object_id = object_id; new_item->room_num = target_pos.room_num; new_item->pos = target_pos.pos; new_item->rot = target_rot; new_item->shade.value_1 = -1; Item_Initialise(item_num); Item_AddActive(item_num); if (M_ShouldEnableBaddieAI(object_id)) { new_item->status = IS_ACTIVE; LOT_EnableBaddieAI(item_num, true); } return true; } static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx) { if (!Game_IsPlayable()) { return CR_UNAVAILABLE; } const ITEM *const lara_item = Lara_GetItem(); if (lara_item->hit_points <= 0) { return CR_UNAVAILABLE; } GAME_VECTOR target_pos; if (!M_FindValidTargetPos(lara_item, &target_pos)) { Console_LogError(GS("console/cmd/spawn/fail")); return CR_FAILURE; } const char *const args = ctx->args; int32_t match_count = 0; OBJECT_NAME_MATCH *matches = nullptr; if (String_IsEmpty(args)) { return CR_BAD_INVOCATION; } else { matches = Object_IdsFromName(args, &match_count, M_CanSpawnObject); } if (match_count <= 0) { Console_LogError(GS("general/osd/invalid_item"), args); Memory_FreePointer(&matches); return CR_FAILURE; } const int32_t dx = lara_item->pos.x - target_pos.x; const int32_t dz = lara_item->pos.z - target_pos.z; const XYZ_16 target_rot = { .y = Math_Atan(dz, dx) }; bool spawned = false; for (int32_t i = 0; i < match_count; i++) { if (M_SpawnItem(matches[i].object_id, target_pos, target_rot)) { spawned = true; break; } } Memory_FreePointer(&matches); if (!spawned) { return CR_FAILURE; } Console_Log(GS("console/cmd/spawn/success")); return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND("spawn", M_Entrypoint, nullptr) ================================================ FILE: src/trx/game/console/cmd/speed.c ================================================ #include #include #include #include #include static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { if (String_Equivalent(ctx->args, "")) { Console_Log(GS("general/osd/speed_get"), Clock_GetTurboSpeed()); return CR_SUCCESS; } int32_t num = -1; if (String_ParseInteger(ctx->args, &num)) { Clock_SetTurboSpeed(num); return CR_SUCCESS; } return CR_BAD_INVOCATION; } REGISTER_CONSOLE_COMMAND("speed", M_Entrypoint, GS_ID("console/cmd/speed/help")) ================================================ FILE: src/trx/game/console/cmd/strings.c ================================================ #include #include #include #include #include static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { const bool success = GameStringManager_ReloadLanguage(g_Config.language); if (success) { Console_Log("%s", GS("general/osd/strings_reloaded")); } else { Console_LogError("%s", GS("general/osd/strings_failed")); } return success ? CR_SUCCESS : CR_FAILURE; } REGISTER_CONSOLE_COMMAND( "strings", M_Entrypoint, GS_ID("console/cmd/strings/help")) ================================================ FILE: src/trx/game/console/cmd/teleport.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static int16_t m_LastTeleportedItemNum = NO_ITEM; static bool M_ObjectCanBePickedUp(const OBJECT_ID obj_id) { if (!Object_IsType(obj_id, g_PickupObjects)) { return true; } for (int32_t item_num = 0; item_num < Item_GetTotalCount(); item_num++) { const ITEM *const item = Item_Get(item_num); if (item->object_id == obj_id && item->status != IS_INVISIBLE) { return true; } } return false; } static bool M_CanTargetObject(const OBJECT_ID obj_id) { return !Object_IsType(obj_id, g_NullObjects) && !Object_IsType(obj_id, g_AnimObjects) && !Object_IsType(obj_id, g_InvObjects) && Object_Get(obj_id)->loaded && M_ObjectCanBePickedUp(obj_id); } static bool M_CanTargetItem( const ITEM *const item, const OBJECT_NAME_MATCH *const matches, const int32_t match_count) { // Collected pickups if (Object_IsType(item->object_id, g_PickupObjects) && (item->status == IS_INVISIBLE || item->status == IS_DEACTIVATED || item->room_num == NO_ROOM)) { return false; } // Killed enemies and removed items if (item->flags & IF_KILLED) { return false; } // Out of bounds items int16_t room_num = item->room_num; if (room_num != NO_ROOM) { const SECTOR *const sector = Room_GetSector(item->pos, &room_num); if (Room_GetHeight(sector, item->pos) == NO_HEIGHT) { return false; } } // Non-matches to user input bool is_matched = false; for (int32_t j = 0; j < match_count; j++) { if (matches[j].object_id == item->object_id) { is_matched = true; break; } } if (!is_matched) { return false; } return true; } static bool M_CanTargetEnemyItem(const ITEM *const item) { if (!Creature_IsHostile(item) || item->room_num == NO_ROOM || item->hit_points <= 0 || (item->flags & IF_KILLED) != 0) { return false; } int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); return Room_GetHeight(sector, item->pos) != NO_HEIGHT; } static const ITEM *M_GetItemToTeleporTo(const char *const user_input) { int32_t match_count = 0; OBJECT_NAME_MATCH *matches = Object_IdsFromName(user_input, &match_count, M_CanTargetObject); const ITEM *const lara_item = Lara_GetItem(); int16_t best_item_num = NO_ITEM; // Choose the matching item closest to Lara. const int32_t near_distance = WALL_L; int16_t closest_item_num = NO_ITEM; int32_t closest_distance = INT32_MAX; for (int32_t item_num = 0; item_num < Item_GetTotalCount(); item_num++) { const ITEM *const item = Item_Get(item_num); if (!M_CanTargetItem(item, matches, match_count)) { continue; } const int32_t distance = Item_GetDistance(item, lara_item->pos); if (distance < closest_distance) { closest_distance = distance; closest_item_num = item_num; } } if (closest_distance > near_distance) { best_item_num = closest_item_num; } else { // If Lara's already very close to a matching item, choose the next // matching item in a round-robin fashion. const int16_t start_idx = (closest_item_num + 1) % Item_GetTotalCount(); for (int32_t i = 0; i < Item_GetTotalCount(); i++) { int16_t item_num = (start_idx + i) % Item_GetTotalCount(); if (item_num == closest_item_num) { continue; } const ITEM *const item = Item_Get(item_num); if (!M_CanTargetItem(item, matches, match_count)) { continue; } const int32_t distance = Item_GetDistance(item, lara_item->pos); if (distance < near_distance) { continue; } best_item_num = item_num; break; } } Memory_FreePointer(&matches); return Item_Get(best_item_num); } static const ITEM *M_GetHostileEnemyToTeleportTo(void) { const int32_t item_count = Item_GetTotalCount(); if (item_count <= 0) { return nullptr; } int16_t start_idx = 0; if (m_LastTeleportedItemNum >= 0 && m_LastTeleportedItemNum < item_count) { start_idx = (m_LastTeleportedItemNum + 1) % item_count; } for (int32_t i = 0; i < item_count; i++) { const int16_t item_num = (start_idx + i) % item_count; const ITEM *const item = Item_Get(item_num); if (!M_CanTargetEnemyItem(item)) { continue; } m_LastTeleportedItemNum = item_num; return item; } return nullptr; } static inline bool M_IsFloatRound(const float num) { return fabsf(num - roundf(num)) < 0.0001f; } static void M_AlignLaraToItem(const ITEM *const item) { typedef enum { L_DIR_NONE = -1, L_DIR_SAME = 0, L_DIR_OPPOSITE = 1, } L_DIR; L_DIR dir = L_DIR_NONE; if (Object_IsType(item->object_id, g_PickupObjects)) { dir = L_DIR_OPPOSITE; } else if (Object_IsType(item->object_id, g_SwitchObjects)) { dir = L_DIR_SAME; } else if (Object_IsType(item->object_id, g_ReceptacleObjects)) { dir = L_DIR_SAME; } else if (Object_IsType(item->object_id, g_DoorObjects)) { dir = L_DIR_OPPOSITE; } else if (item->object_id == O_ZIPLINE_HANDLE) { dir = L_DIR_SAME; } if (dir != L_DIR_NONE) { ITEM *const lara_item = Lara_GetItem(); lara_item->rot.x = 0; lara_item->rot.y = item->rot.y; lara_item->rot.z = 0; if (dir == L_DIR_OPPOSITE) { lara_item->rot.y += DEG_180; } } } static COMMAND_RESULT M_TeleportToXYZ( float x, const float y, float z, const bool precise_coords) { if (!precise_coords) { if (M_IsFloatRound(x)) { x += 0.5f; } if (M_IsFloatRound(z)) { z += 0.5f; } } const XYZ_32 pos = { .x = precise_coords ? x : x * WALL_L, .y = precise_coords ? y : y * WALL_L, .z = precise_coords ? z : z * WALL_L, }; if (!Lara_Cheat_Teleport(pos, NO_ROOM)) { Console_LogError(GS("console/cmd/teleport/pos_fail"), x, y, z); return CR_FAILURE; } Console_Log(GS("console/cmd/teleport/pos"), x, y, z); return CR_SUCCESS; } static COMMAND_RESULT M_TeleportToItemNum(const int16_t item_num) { if (item_num < 0 || item_num >= Item_GetTotalCount()) { Console_LogError(GS("console/cmd/teleport/item_fail"), item_num); return CR_FAILURE; } const ITEM *const item = Item_Get(item_num); if (item == nullptr || item->room_num == NO_ROOM) { Console_LogError(GS("console/cmd/teleport/item_fail"), item_num); return CR_FAILURE; } if ((item->flags & IF_KILLED) != 0) { Console_LogError(GS("console/cmd/teleport/item_fail"), item_num); return CR_FAILURE; } const XYZ_32 pos = { .x = item->pos.x, .y = item->pos.y - STEP_L / 4, .z = item->pos.z, }; if (Lara_Cheat_Teleport(pos, item->room_num)) { M_AlignLaraToItem(item); Console_Log(GS("console/cmd/teleport/item"), item_num); return CR_SUCCESS; } Console_LogError(GS("console/cmd/teleport/item_fail"), item_num); return CR_FAILURE; } static COMMAND_RESULT M_TeleportToRoom(const int16_t room_num) { if (room_num < 0 || room_num >= Room_GetCount()) { Console_LogWarning( GS("general/osd/invalid_room"), room_num, Room_GetCount() - 1); return CR_FAILURE; } const ROOM *const room = Room_Get(room_num); const int32_t x1 = room->pos.x + WALL_L; const int32_t x2 = room->pos.x + (room->size.x << WALL_SHIFT) - WALL_L; const int32_t y1 = room->min_floor; const int32_t y2 = room->max_ceiling; const int32_t z1 = room->pos.z + WALL_L; const int32_t z2 = room->pos.z + (room->size.z << WALL_SHIFT) - WALL_L; bool success = false; for (int32_t i = 0; i < 100; i++) { const XYZ_32 pos = { .x = x1 + Random_GetControl() * (x2 - x1) / 0x7FFF, .y = y1, .z = z1 + Random_GetControl() * (z2 - z1) / 0x7FFF, }; if (Lara_Cheat_Teleport(pos, room_num)) { success = true; break; } } if (!success) { Console_LogError(GS("console/cmd/teleport/room_fail"), room_num); return CR_FAILURE; } Console_Log(GS("console/cmd/teleport/room"), room_num); return CR_SUCCESS; } static COMMAND_RESULT M_TeleportToObject(const char *const user_input) { // Nearest item of this name if (String_Equivalent(user_input, "")) { return CR_BAD_INVOCATION; } const ITEM *const best_item = M_GetItemToTeleporTo(user_input); if (best_item == nullptr) { Console_LogError(GS("console/cmd/teleport/object_fail"), user_input); return CR_FAILURE; } // Determine which alias matched via Object_IdsFromName int32_t match_count = 0; OBJECT_NAME_MATCH *matches = Object_IdsFromName(user_input, &match_count, M_CanTargetObject); const char *reported_name = user_input; for (int32_t i = 0; i < match_count; i++) { if (matches[i].object_id == best_item->object_id) { if (matches[i].matched_name != nullptr) { reported_name = matches[i].matched_name; } break; } } Memory_FreePointer(&matches); const XYZ_32 pos = { .x = best_item->pos.x, .y = best_item->pos.y - STEP_L / 4, .z = best_item->pos.z, }; if (Lara_Cheat_Teleport(pos, best_item->room_num)) { M_AlignLaraToItem(best_item); Console_Log(GS("console/cmd/teleport/object"), reported_name); } else { Console_LogError(GS("console/cmd/teleport/object_fail"), reported_name); } return CR_SUCCESS; } static COMMAND_RESULT M_TeleportToEnemy(void) { ITEM *const enemy_item = (ITEM *)M_GetHostileEnemyToTeleportTo(); if (enemy_item == nullptr) { Console_LogError(GS("console/cmd/teleport/object_fail"), "enemy"); return CR_FAILURE; } const XYZ_32 pos = { .x = enemy_item->pos.x, .y = enemy_item->pos.y - STEP_L / 4, .z = enemy_item->pos.z, }; if (!Lara_Cheat_Teleport(pos, enemy_item->room_num)) { Console_LogError(GS("console/cmd/teleport/object_fail"), "enemy"); return CR_FAILURE; } M_AlignLaraToItem(enemy_item); Console_Log(GS("console/cmd/teleport/object"), "enemy"); return CR_SUCCESS; } static bool M_TryParseTagNumber( const char *const args, const char tag, int16_t *const out_num) { if (args == nullptr) { return false; } if (args[0] != tag) { return false; } int32_t num32 = 0; if (!String_ParseInteger(args + 1, &num32)) { return false; } if (num32 < INT16_MIN || num32 > INT16_MAX) { return false; } *out_num = (int16_t)num32; return true; } static bool M_TryParseKeywordNumber( const char *const args, const char *const keyword, int16_t *const out_num) { if (args == nullptr || keyword == nullptr) { return false; } const size_t keyword_len = strlen(keyword); if (strncmp(args, keyword, keyword_len) != 0) { return false; } int32_t num32 = 0; if (!String_ParseInteger(args + keyword_len, &num32)) { return false; } if (num32 < INT16_MIN || num32 > INT16_MAX) { return false; } *out_num = (int16_t)num32; return true; } static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { if (!Game_IsPlayable()) { return CR_UNAVAILABLE; } const ITEM *const lara_item = Lara_GetItem(); if (!lara_item->hit_points) { return CR_UNAVAILABLE; } float x, y, z; if (sscanf(ctx->args, "precise %f %f %f", &x, &y, &z) == 3) { return M_TeleportToXYZ(x, y, z, true); } if (sscanf(ctx->args, "%f %f %f", &x, &y, &z) == 3) { return M_TeleportToXYZ(x, y, z, false); } int16_t num = 0; if (M_TryParseKeywordNumber(ctx->args, "item ", &num)) { return M_TeleportToItemNum(num); } if (M_TryParseTagNumber(ctx->args, 'i', &num)) { return M_TeleportToItemNum(num); } if (M_TryParseKeywordNumber(ctx->args, "room ", &num)) { return M_TeleportToRoom(num); } if (M_TryParseTagNumber(ctx->args, 'r', &num)) { return M_TeleportToRoom(num); } int16_t room_num = -1; if (sscanf(ctx->args, "%hd", &room_num) == 1) { // legacy return M_TeleportToRoom(room_num); } if (String_Equivalent(ctx->args, "enemy")) { return M_TeleportToEnemy(); } return M_TeleportToObject(ctx->args); } REGISTER_CONSOLE_COMMAND("tp", M_Entrypoint, GS_ID("console/cmd/tp/help")) ================================================ FILE: src/trx/game/console/cmd/test_text.c ================================================ #include #include #include static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { char buf[500] = {}; char *ptr = buf; for (int32_t y = 0; y < 3; y++) { for (int32_t x = 0; x < 4; x++) { const int32_t i = y * 4 + x; ptr += sprintf(ptr, "\\{color %d}Color %d\\{/color} ", i, i); } ptr += sprintf(ptr, "\n"); } ptr += sprintf(ptr, "\\{dim}Dim\\{/dim}\n"); ptr += sprintf(ptr, "Secrets: \\{secret 1}\\{secret 2}\\{secret 3}"); Console_Log("%s", buf); return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND("test-text", M_Entrypoint, nullptr) ================================================ FILE: src/trx/game/console/cmd/trigger.c ================================================ #include #include #include #include #include #include #include #include #include #include #include typedef enum { M_TARGET_OK = 0, M_TARGET_INVALID_ITEM, M_TARGET_NO_MATCH, M_TARGET_NOT_FOUND, } M_TARGET_RESULT; static bool M_CanTargetObject(const OBJECT_ID object_id) { return Object_Get(object_id)->loaded; } static void M_SortItemNums(int16_t *const item_nums, const int32_t count) { for (int32_t i = 0; i < count - 1; i++) { for (int32_t j = i + 1; j < count; j++) { if (item_nums[i] > item_nums[j]) { SWAP(item_nums[i], item_nums[j]); } } } } static char *M_BuildCollapsedItemNums( const int16_t *const item_nums, const int32_t count) { if (count <= 0) { return Memory_DupStr(""); } char *result = nullptr; for (int32_t i = 0; i < count; i++) { const int16_t start = item_nums[i]; int16_t end = start; while (i + 1 < count && item_nums[i + 1] == end + 1) { i++; end = item_nums[i]; } char *segment = nullptr; if (end == start) { segment = String_Format("%d", start); } else { segment = String_Format("%d-%d", start, end); } if (result == nullptr) { result = segment; } else { char *const joined = String_Format("%s, %s", result, segment); Memory_FreePointer(&result); Memory_FreePointer(&segment); result = joined; } } if (result == nullptr) { return Memory_DupStr(""); } return result; } static void M_ApplyTrigger(const int16_t item_num, const bool enable) { ITEM *const item = Item_Get(item_num); if (item == nullptr) { return; } const OBJECT *const obj = Object_Get(item->object_id); if (obj->trigger_func != nullptr) { const bool use_default_handling = obj->trigger_func(item, nullptr); if (!use_default_handling) { return; } } if (enable) { item->flags |= IF_CODE_BITS; item->timer = 0; Item_AddActive(item_num); if (obj->intelligent) { item->status = IS_ACTIVE; LOT_EnableBaddieAI(item_num, true); } } else { item->flags &= ~IF_CODE_BITS; Item_RemoveActive(item_num); if (obj->intelligent) { item->status = IS_ACTIVE; LOT_DisableBaddieAI(item_num); } } } static M_TARGET_RESULT M_TargetItemsFromItemNum( VECTOR *const target_item_nums, const int32_t item_num_arg) { if (item_num_arg < 0 || item_num_arg >= Item_GetTotalCount()) { return M_TARGET_INVALID_ITEM; } const int16_t item_num = (int16_t)item_num_arg; const ITEM *const item = Item_Get(item_num); if (item == nullptr || item->object_id == O_LARA) { return M_TARGET_INVALID_ITEM; } Vector_Add(target_item_nums, &item_num); return M_TARGET_OK; } static M_TARGET_RESULT M_TargetItemsFromItemName( VECTOR *const target_item_nums, const char *const item_name) { const ITEM *const item = Item_GetByName(item_name); if (item == nullptr) { return M_TARGET_NO_MATCH; } const int16_t item_num = Item_GetIndex(item); Vector_Add(target_item_nums, &item_num); return M_TARGET_OK; } static M_TARGET_RESULT M_TargetItemsFromObjectName( VECTOR *const target_item_nums, const char *const object_name) { int32_t match_count = 0; OBJECT_NAME_MATCH *matches = Object_IdsFromName(object_name, &match_count, M_CanTargetObject); if (match_count <= 0) { Memory_FreePointer(&matches); return M_TARGET_NO_MATCH; } const int32_t total_count = Item_GetTotalCount(); for (int16_t item_num = 0; item_num < total_count; item_num++) { const ITEM *const item = Item_Get(item_num); if (item == nullptr || item->object_id == O_LARA) { continue; } if ((item->flags & IF_KILLED) != 0) { continue; } bool is_matched = false; for (int32_t i = 0; i < match_count; i++) { if (matches[i].object_id == item->object_id) { is_matched = true; break; } } if (!is_matched) { continue; } Vector_Add(target_item_nums, &item_num); } Memory_FreePointer(&matches); if (target_item_nums->count <= 0) { return M_TARGET_NOT_FOUND; } return M_TARGET_OK; } static M_TARGET_RESULT M_GetTargetItems( VECTOR *const target_item_nums, const char *const user_input) { int32_t item_num_arg = 0; if (String_ParseInteger(user_input, &item_num_arg)) { return M_TargetItemsFromItemNum(target_item_nums, item_num_arg); } const M_TARGET_RESULT named_result = M_TargetItemsFromItemName(target_item_nums, user_input); if (named_result == M_TARGET_OK) { return M_TARGET_OK; } return M_TargetItemsFromObjectName(target_item_nums, user_input); } static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { if (!Game_IsLoaded()) { return CR_UNAVAILABLE; } const bool is_trigger = String_Equivalent(ctx->prefix, "trigger"); const bool is_untrigger = String_Equivalent(ctx->prefix, "untrigger"); if (!is_trigger && !is_untrigger) { return CR_BAD_INVOCATION; } const bool enable = is_trigger; if (String_IsEmpty(ctx->args)) { return CR_BAD_INVOCATION; } VECTOR *const target_item_nums = Vector_Create(sizeof(int16_t)); const M_TARGET_RESULT target_result = M_GetTargetItems(target_item_nums, ctx->args); if (target_result != M_TARGET_OK) { switch (target_result) { case M_TARGET_INVALID_ITEM: Console_LogError(GS("console/cmd/trigger/invalid_item"), ctx->args); break; case M_TARGET_NO_MATCH: Console_LogError(GS("console/cmd/trigger/no_match"), ctx->args); break; case M_TARGET_NOT_FOUND: Console_LogError(GS("console/cmd/trigger/not_found"), ctx->args); break; case M_TARGET_OK: break; } Vector_Free(target_item_nums); return CR_FAILURE; } int16_t *const item_nums = Vector_GetData(target_item_nums); M_SortItemNums(item_nums, target_item_nums->count); for (int32_t i = 0; i < target_item_nums->count; i++) { M_ApplyTrigger(item_nums[i], enable); } char *collapsed = M_BuildCollapsedItemNums(item_nums, target_item_nums->count); Console_Log( enable ? GS("console/cmd/trigger/triggered") : GS("console/cmd/trigger/untriggered"), collapsed); Memory_FreePointer(&collapsed); Vector_Free(target_item_nums); return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND( "trigger", M_Entrypoint, GS_ID("console/cmd/trigger/help")) REGISTER_CONSOLE_COMMAND( "untrigger", M_Entrypoint, GS_ID("console/cmd/trigger/help")) ================================================ FILE: src/trx/game/console/cmd/weather.c ================================================ #include #include #include #include #include #include #include #include #include #include #include static char *M_GetAvailableWeatherTypes(void) { VECTOR *const values = EnumMap_ListValues("WEATHER_TYPE"); if (values == nullptr) { return nullptr; } size_t total_len = 1; const char *const sep = ", "; for (int32_t i = 0; i < values->count; i++) { const char *const s = *(char **)Vector_Get(values, i); total_len += strlen(s) + (i + 1 < values->count ? strlen(sep) : 0); } char *const result = Memory_Alloc(total_len); for (int32_t i = 0; i < values->count; i++) { const char *const s = *(char **)Vector_Get(values, i); strcat(result, s); if (i + 1 < values->count) { strcat(result, sep); } } Vector_Free(values); return result; } static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) { if (GF_GetCurrentLevel() == nullptr || GF_GetCurrentLevel()->type == GFL_TITLE) { return CR_UNAVAILABLE; } if (!Game_IsLoaded()) { return CR_UNAVAILABLE; } if (String_IsEmpty(ctx->args)) { return CR_BAD_INVOCATION; } const int32_t weather_type_raw = ENUM_MAP_GET(WEATHER_TYPE, ctx->args, -1); if (weather_type_raw == -1) { char *available = M_GetAvailableWeatherTypes(); if (available != nullptr) { Console_LogError( GS("console/cmd/weather/invalid"), ctx->args, available); } Memory_FreePointer(&available); return CR_FAILURE; } FX_Weather_SetWeather((WEATHER_TYPE)weather_type_raw); Console_Log(GS("console/cmd/weather/set"), ctx->args); return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND( "weather", M_Entrypoint, GS_ID("console/cmd/weather/help")) ================================================ FILE: src/trx/game/console/cmd/winston.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include static bool M_TrySummon(const GAME_VECTOR target_pos, const OBJECT_ID object_id) { const ITEM *const lara_item = Lara_GetItem(); for (int16_t item_num = 0; item_num < Item_GetTotalCount(); item_num++) { ITEM *const item = Item_Get(item_num); if (item->object_id == object_id) { if (item->status == IS_INVISIBLE || item->status == IS_INACTIVE) { item->status = IS_ACTIVE; Item_AddActive(item_num); LOT_EnableBaddieAI(item_num, true); } else if ((item->flags & IF_KILLED) != 0) { Music_Stop(); Console_Log(GS("console/cmd/winston/dead")); return true; } item->pos.x = target_pos.x; item->pos.y = target_pos.y; item->pos.z = target_pos.z; item->rot.y = lara_item->rot.y; Item_UpdateRoom(item_num, target_pos.room_num); return true; } } return false; } static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx) { if (!Game_IsPlayable()) { return CR_UNAVAILABLE; } const ITEM *const lara_item = Lara_GetItem(); if (lara_item->hit_points <= 0) { return CR_UNAVAILABLE; } const OBJECT *const obj = Object_Get(O_WINSTON); if (!obj->loaded) { return CR_UNAVAILABLE; } GAME_VECTOR target_pos = { .x = lara_item->pos.x + STEP_L, .y = lara_item->pos.y - WALL_L, .z = lara_item->pos.z + STEP_L, .room_num = lara_item->room_num, }; if (!Room_FindValidPos(&target_pos.pos, &target_pos.room_num)) { Console_LogError(GS("console/cmd/winston/spawn_failed")); return CR_FAILURE; } const LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (lara_info->killed_loyal_item) { Music_Stop(); Console_Log(GS("console/cmd/winston/dead")); return CR_FAILURE; } if (M_TrySummon(target_pos, O_WINSTON_ARMY) || M_TrySummon(target_pos, O_WINSTON)) { Console_Log(GS("console/cmd/winston/teleported")); return CR_SUCCESS; } const int16_t item_num = Item_Create(); if (item_num == NO_ITEM) { return CR_FAILURE; } ITEM *const new_item = Item_Get(item_num); new_item->object_id = O_WINSTON; new_item->room_num = target_pos.room_num; new_item->pos.x = target_pos.x; new_item->pos.y = target_pos.y; new_item->pos.z = target_pos.z; new_item->rot.y = lara_item->rot.y; new_item->shade.value_1 = -1; Item_Initialise(item_num); Item_AddActive(item_num); new_item->status = IS_ACTIVE; LOT_EnableBaddieAI(item_num, true); Console_Log(GS("console/cmd/winston/spawned")); return CR_SUCCESS; } REGISTER_CONSOLE_COMMAND("teatime", M_Entrypoint, nullptr) ================================================ FILE: src/trx/game/console/common.c ================================================ #include #include #include #include #include #include #include #include #include #include #include static bool m_IsOpened = false; static UI_CONSOLE_STATE m_UIState = {}; // Controls whether console commands emit log events to the UI console static bool m_Verbose = true; void Console_Init(void) { UI_Console_Init(&m_UIState); Console_History_Init(); } void Console_Shutdown(void) { UI_Console_Free(&m_UIState); Console_History_Shutdown(); m_IsOpened = false; } void Console_Open(void) { if (m_IsOpened) { return; } m_IsOpened = true; UI_FireEvent( (EVENT) { .name = "console_open", .sender = nullptr, .data = nullptr }); } void Console_Close(void) { if (!m_IsOpened) { return; } m_IsOpened = false; UI_FireEvent((EVENT) { .name = "console_close", .sender = nullptr, .data = nullptr }); } bool Console_IsOpened(void) { return m_IsOpened; } void Console_LogEx( const LOG_LEVEL level, const char *file, int line, const char *func, const char *const fmt, ...) { ASSERT(fmt != nullptr); va_list va; va_start(va, fmt); va_list va_copy; va_copy(va_copy, va); const size_t text_length = vsnprintf(nullptr, 0, fmt, va); char *text = Memory_Alloc(text_length + 1); va_end(va); vsnprintf(text, text_length + 1, fmt, va_copy); va_end(va_copy); Log_Message(level, file, line, func, "%s", text); if (m_Verbose) { UI_FireEvent((EVENT) { .name = "console_log", .sender = nullptr, .data = text, }); } Memory_FreePointer(&text); } void Console_SetVerbose(const bool verbose) { m_Verbose = verbose; } bool Console_IsVerbose(void) { return m_Verbose; } void Console_Clear(void) { UI_FireEvent((EVENT) { .name = "console_clear", }); } COMMAND_RESULT Console_Eval(const char *const cmdline) { LOG_INFO("executing command: %s", cmdline); const CONSOLE_COMMAND *const matching_cmd = Console_Registry_Get(cmdline); if (matching_cmd == nullptr) { Console_LogError(GS("general/osd/unknown_command"), cmdline); return CR_BAD_INVOCATION; } char *prefix = Memory_DupStr(cmdline); char *args = ""; char *space = strchr(prefix, ' '); if (space != nullptr) { *space = '\0'; args = space + 1; } const COMMAND_CONTEXT ctx = { .cmd = matching_cmd, .prefix = prefix, .args = args, }; ASSERT(matching_cmd->proc != nullptr); const COMMAND_RESULT result = matching_cmd->proc(&ctx); Memory_FreePointer(&prefix); switch (result) { case CR_BAD_INVOCATION: Console_LogError(GS("general/osd/command_bad_invocation"), cmdline); break; case CR_UNAVAILABLE: Console_LogError(GS("general/osd/command_unavailable")); break; case CR_SUCCESS: case CR_FAILURE: // The commands themselves are responsible for handling logging in // these scenarios. break; } return result; } void Console_Control(void) { UI_Console_Control(&m_UIState); } void Console_Draw(void) { UI_Console(&m_UIState); } ================================================ FILE: src/trx/game/console/common.h ================================================ #pragma once #include #include #include #define Console_LogGeneric(level, ...) \ Console_LogEx(level, __FILE__, __LINE__, __func__, __VA_ARGS__) #define Console_Log(...) Console_LogGeneric(LOG_LEVEL_INFO, __VA_ARGS__) #define Console_LogWarning(...) \ Console_LogGeneric(LOG_LEVEL_WARNING, __VA_ARGS__) #define Console_LogError(...) Console_LogGeneric(LOG_LEVEL_ERROR, __VA_ARGS__) void Console_Init(void); void Console_Shutdown(void); void Console_Open(void); void Console_Close(void); bool Console_IsOpened(void); void Console_LogEx( LOG_LEVEL level, const char *file, int line, const char *func, const char *fmt, ...); void Console_Clear(void); COMMAND_RESULT Console_Eval(const char *cmdline); // Controls whether console commands emit log events to the UI console void Console_SetVerbose(bool verbose); bool Console_IsVerbose(void); void Console_Control(void); void Console_Draw(void); ================================================ FILE: src/trx/game/console/enum.h ================================================ #pragma once typedef enum { CR_SUCCESS, CR_FAILURE, CR_UNAVAILABLE, CR_BAD_INVOCATION, } COMMAND_RESULT; ================================================ FILE: src/trx/game/console/history.c ================================================ #include #include #include #include #include #include #include #include #include #define MAX_HISTORY_ENTRIES 30 VECTOR *m_History = nullptr; static const char *M_GetPath(void) { return String_FormatStatic( "%s/TR%dX_console_history.json5", Shell_GetConfigDir(), g_TRVersion); } static void M_LoadFromJSON(JSON_VALUE *const doc) { JSON_OBJECT *const root_obj = JSON_ValueAsObject(doc); JSON_ARRAY *const arr = JSON_ObjectGetArray(root_obj, "entries"); if (arr == nullptr) { return; } Console_History_Clear(); for (size_t i = 0; i < arr->length; i++) { const char *const line = JSON_ArrayGetString(arr, i, nullptr); if (line != nullptr) { Console_History_Append(line); } } } static JSON_VALUE *M_DumpToJSON(void) { JSON_ARRAY *const arr = JSON_ArrayNew(); for (int32_t i = 0; i < Console_History_GetLength(); i++) { JSON_ArrayAppendString(arr, Console_History_Get(i)); } if (arr->length == 0) { JSON_ArrayFree(arr); return nullptr; } JSON_OBJECT *root_obj = JSON_ObjectNew(); JSON_ObjectAppendArray(root_obj, "entries", arr); return JSON_ValueFromObject(root_obj); } void Console_History_Init(void) { m_History = Vector_Create(sizeof(char *)); JSON_VALUE *const doc = JSONFile_Read(M_GetPath()); if (doc != nullptr) { M_LoadFromJSON(doc); JSON_ValueFree(doc); } } void Console_History_Shutdown(void) { if (m_History == nullptr) { return; } JSON_VALUE *const doc = M_DumpToJSON(); if (doc != nullptr) { JSONFile_Write(M_GetPath(), doc); JSON_ValueFree(doc); } for (int32_t i = m_History->count - 1; i >= 0; i--) { char *const prompt = *(char **)Vector_Get(m_History, i); Memory_Free(prompt); } Vector_Free(m_History); m_History = nullptr; } int32_t Console_History_GetLength(void) { return m_History->count; } void Console_History_Clear(void) { for (int32_t i = m_History->count - 1; i >= 0; i--) { char *const prompt = *(char **)Vector_Get(m_History, i); Memory_Free(prompt); } Vector_Clear(m_History); } void Console_History_Append(const char *const prompt) { for (int32_t i = m_History->count - 1; i >= 0; i--) { char *const entry = *(char **)Vector_Get(m_History, i); if (strcmp(entry, prompt) == 0) { Memory_Free(entry); Vector_RemoveAt(m_History, i); } } if (m_History->count == MAX_HISTORY_ENTRIES) { char *const oldest = *(char **)Vector_Get(m_History, 0); Memory_Free(oldest); Vector_RemoveAt(m_History, 0); } char *const prompt_copy = Memory_DupStr(prompt); Vector_Add(m_History, &prompt_copy); } const char *Console_History_Get(const int32_t idx) { if (idx < 0 || idx >= m_History->count) { return nullptr; } const char *const prompt = *(char **)Vector_Get(m_History, idx); return prompt; } ================================================ FILE: src/trx/game/console/history.h ================================================ #pragma once #include int32_t Console_History_GetLength(void); void Console_History_Clear(void); void Console_History_Append(const char *prompt); const char *Console_History_Get(int32_t idx); ================================================ FILE: src/trx/game/console/internal.h ================================================ #pragma once void Console_History_Init(void); void Console_History_Shutdown(void); ================================================ FILE: src/trx/game/console/registry.c ================================================ #include #include #include #include #include typedef struct M_NODE { CONSOLE_COMMAND cmd; struct M_NODE *next; } M_NODE; static M_NODE *m_List = nullptr; __attribute__((destructor)) static void M_Shutdown(void) { M_NODE *current = m_List; while (current != nullptr) { M_NODE *const next = current->next; Memory_Free(current); current = next; } m_List = nullptr; } const CONSOLE_COMMAND *Console_Registry_Get(const char *const cmdline) { const M_NODE *current = m_List; while (current != nullptr) { const M_NODE *const next = current->next; char regex[strlen(current->cmd.prefix) + 13]; sprintf(regex, "^(%s)(\\s+.*)?$", current->cmd.prefix); if (String_Match(cmdline, regex)) { return ¤t->cmd; } current = next; } return nullptr; } void Console_Registry_Add(CONSOLE_COMMAND cmd) { M_NODE *node = Memory_Alloc(sizeof(M_NODE)); node->cmd = cmd; node->next = m_List; m_List = node; } VECTOR *Console_Registry_GetAll(void) { VECTOR *vec = Vector_Create(sizeof(const CONSOLE_COMMAND *)); M_NODE *node = m_List; while (node != nullptr) { const CONSOLE_COMMAND *cmd_ptr = &node->cmd; Vector_Add(vec, &cmd_ptr); node = node->next; } return vec; } ================================================ FILE: src/trx/game/console/registry.h ================================================ #pragma once #include #include #include #include void Console_Registry_Add(CONSOLE_COMMAND cmd); const CONSOLE_COMMAND *Console_Registry_Get(const char *cmdline); // Retrieve a vector containing pointers to all registered console commands. // The returned vector must be freed via Vector_Free(). VECTOR *Console_Registry_GetAll(void); #define REGISTER_CONSOLE_COMMAND(prefix_, proc_, help_) \ __attribute__((__constructor__)) static void CONCAT( \ M_Register_, __LINE__)(void) \ { \ Console_Registry_Add((CONSOLE_COMMAND) { \ .prefix = prefix_, .proc = proc_, .help_id = help_ }); \ } ================================================ FILE: src/trx/game/console/types.h ================================================ #pragma once #include #include typedef struct { const struct CONSOLE_COMMAND *cmd; const char *prefix; const char *args; } COMMAND_CONTEXT; typedef struct CONSOLE_COMMAND { const char *prefix; COMMAND_RESULT (*proc)(const COMMAND_CONTEXT *ctx); GAME_STRING_ID help_id; } CONSOLE_COMMAND; ================================================ FILE: src/trx/game/console.h ================================================ #pragma once #include #include ================================================ FILE: src/trx/game/const.h ================================================ #pragma once #include #define LOGIC_FPS 30 #define STEP_L 256 #define WALL_L 1024 // = 1 << WALL_SHIFT #define WALL_SHIFT 10 #define GRAVITY 6 #define FAST_FALL_SPEED 128 #define FOV_VALUE_PASSPORT 80 #define FOV_MODE_GAME FOV_MODE_PC #define FOV_MODE_PASSPORT FOV_MODE_PS1 #define FOV_MODE_CUTSCENE (g_TRVersion == 1 ? FOV_MODE_VERTICAL : FOV_MODE_PC) ================================================ FILE: src/trx/game/creature/alert.c ================================================ #include #include #include #include void Creature_AlertNearbyGuards(const ITEM *const item) { for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) { CREATURE *const creature = LOT_GetBaddieSlot(i); if (creature->item_num == NO_ITEM) { continue; } const ITEM *const target = Item_Get(creature->item_num); if (target->room_num == item->room_num) { creature->alerted = true; continue; } int32_t dx = (target->pos.x - item->pos.x) >> 6; int32_t dy = (target->pos.y - item->pos.y) >> 6; int32_t dz = (target->pos.z - item->pos.z) >> 6; int32_t dist = SQUARE(dx) + SQUARE(dy) + SQUARE(dz); if (dist < 8000) { creature->alerted = true; } } } void Creature_AlertAllGuards(const int16_t item_num) { const ITEM *const item = Item_Get(item_num); for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) { CREATURE *const creature = LOT_GetBaddieSlot(i); if (creature->item_num == NO_ITEM) { continue; } const ITEM *const target = Item_Get(creature->item_num); if (target->object_id == item->object_id && target->status == IS_ACTIVE) { creature->alerted = true; } } } ================================================ FILE: src/trx/game/creature/alert.h ================================================ #pragma once #include void Creature_AlertNearbyGuards(const ITEM *item); void Creature_AlertAllGuards(int16_t item_num); ================================================ FILE: src/trx/game/creature/behavior.c ================================================ #include #include #include #include #include #define M_ALLY_FRIENDLY_FIRE_THRESHOLD 10 static bool m_AlliesHostile = false; static VECTOR *m_AllyObjects = nullptr; static VECTOR *m_AllyTargetingObjects = nullptr; __attribute__((constructor)) static void M_Init(void) { m_AllyObjects = Vector_Create(sizeof(OBJECT_ID)); m_AllyTargetingObjects = Vector_Create(sizeof(OBJECT_ID)); } __attribute__((destructor)) static void M_Shutdown(void) { #define L_DELETE_VECTOR(vec) \ if (vec != nullptr) { \ Vector_Free(vec); \ vec = nullptr; \ } L_DELETE_VECTOR(m_AllyObjects); L_DELETE_VECTOR(m_AllyTargetingObjects); #undef L_DELETE_VECTOR } void Creature_Reset(void) { Creature_SetAlliesHostile(false); Vector_Clear(m_AllyObjects); Vector_Clear(m_AllyTargetingObjects); } bool Creature_AreAlliesHostile(void) { return m_AlliesHostile; } void Creature_SetAlliesHostile(bool enable) { m_AlliesHostile = enable; if (enable) { Stats_MarkAlliesHostile(); } } void Creature_Hurt(ITEM *const item, const int32_t damage) { if (damage <= 0) { return; } CREATURE *const creature = item->creature_data; if (creature != nullptr) { creature->hurt_by_lara = true; } if (!Creature_IsAlly(item)) { return; } switch (g_Config.gameplay.ally_hostility_policy) { case ALLY_HOSTILITY_POLICY_INDIVIDUAL: Stats_MarkAlliesHostile(); break; case ALLY_HOSTILITY_POLICY_SHARED: if (!m_AlliesHostile) { if (creature != nullptr) { creature->damage_from_lara += damage; } if (item->hit_points <= 0 || (creature != nullptr && (creature->damage_from_lara > M_ALLY_FRIENDLY_FIRE_THRESHOLD || creature->mood == MOOD_BORED))) { m_AlliesHostile = true; Stats_MarkAlliesHostile(); } } break; } } bool Creature_IsHostile(const ITEM *const item) { if (item->object_id != O_SKIDOO_ARMED && !Object_IsType(item->object_id, g_CreatureObjects)) { return false; } if (!Creature_IsAlly(item)) { return true; } switch (g_Config.gameplay.ally_hostility_policy) { case ALLY_HOSTILITY_POLICY_INDIVIDUAL: const CREATURE *const creature = item->creature_data; return creature != nullptr && creature->hurt_by_lara; case ALLY_HOSTILITY_POLICY_SHARED: return m_AlliesHostile; } return false; } bool Creature_IsAlly(const ITEM *const item) { return Vector_Contains(m_AllyObjects, (void *)&item->object_id); } bool Creature_IsAllyTargetingEnemy(const ITEM *const item) { return Vector_Contains(m_AllyTargetingObjects, (void *)&item->object_id); } void Creature_AddAlly(const OBJECT_ID obj_id) { Vector_Add(m_AllyObjects, (void *)&obj_id); } void Creature_AddAllyTargetingEnemy(const OBJECT_ID obj_id) { Vector_Add(m_AllyTargetingObjects, (void *)&obj_id); } ================================================ FILE: src/trx/game/creature/common.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // clang-format off #define M_FLOAT_SPEED 32 #define M_MAX_DISTANCE (g_TRVersion < 3 ? WALL_L * 30 : STEP_L * 125) // = 30720 (TR1/2), 32000 (TR3) #define M_ATTACK_RANGE SQUARE(WALL_L * 3) // = 0x900000 = 9437184 #define M_ESCAPE_CHANCE 2048 #define M_RECOVER_CHANCE 256 #define M_TARGET_TOLERANCE 0x400000 #define M_MAX_TILT (3 * DEG_1) // = 546 #define M_MAX_HEAD_CHANGE (5 * DEG_1) // = 910 #define M_MAX_JOINT_CHANGE (5 * DEG_1) // = 910 #define M_HEAD_ARC (g_TRVersion == 1 ? FRONT_ARC : 0x3000) // = 16384 (TR1), 12288 (TR2) #define M_JOINT_ARC 0x3000 #define M_MAX_X_ROT (20 * DEG_1) // = 3640 #define M_BITE_DISTANCE (g_TRVersion < 3 ? STEP_L : STEP_L * 2) #define M_BOX_DAMAGE 20 // clang-format on static const LARA_TRX_STATE m_CrouchShiftStates[] = { // clang-format off LS_CROUCH_IDLE, LS_CROUCH_ROLL, LS_CROUCH_TURN_LEFT, LS_CROUCH_TURN_RIGHT, LS_CRAWL_IDLE, LS_CRAWL_FORWARD, LS_CRAWL_TURN_LEFT, LS_CRAWL_TURN_RIGHT, LS_TRX_INVALID, // sentinel // clang-format on }; static bool M_TestSwitchOrKill( const int16_t item_num, const OBJECT_ID target_id) { if (Object_Get(target_id)->loaded) { return true; } LOG_WARNING( "Object %d is not loaded; item %d cannot be converted.", target_id, item_num); Item_Kill(item_num); return false; } static void M_GetBaddieTarget(const int16_t item_num, const bool goody) { ITEM *const lara_item = Lara_GetItem(); ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; ITEM *best_item = nullptr; int32_t best_distance = INT32_MAX; for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) { const int16_t target_item_num = LOT_GetBaddieSlot(i)->item_num; if (target_item_num == NO_ITEM || target_item_num == item_num) { continue; } ITEM *const target = Item_Get(target_item_num); const OBJECT_ID obj_id = target->object_id; if (goody && !Creature_IsAllyTargetingEnemy(target)) { continue; } else if (!goody && !Creature_IsAlly(target)) { continue; } const int32_t dx = (target->pos.x - item->pos.x) >> 6; const int32_t dy = (target->pos.y - item->pos.y) >> 6; const int32_t dz = (target->pos.z - item->pos.z) >> 6; const int32_t distance = SQUARE(dx) + SQUARE(dy) + SQUARE(dz); if (distance < best_distance) { best_item = target; best_distance = distance; } } if (best_item == nullptr) { if (!goody || Creature_IsHostile(item)) { creature->enemy = lara_item; } else { creature->enemy = nullptr; } return; } if (!goody || Creature_IsHostile(item)) { const int32_t dx = (lara_item->pos.x - item->pos.x) >> 6; const int32_t dy = (lara_item->pos.y - item->pos.y) >> 6; const int32_t dz = (lara_item->pos.z - item->pos.z) >> 6; const int32_t distance = SQUARE(dx) + SQUARE(dy) + SQUARE(dz); if (distance < best_distance) { best_item = lara_item; best_distance = distance; } } const ITEM *const target = creature->enemy; if (target == nullptr || target->status != IS_ACTIVE) { creature->enemy = best_item; } else { const int32_t dx = (target->pos.x - item->pos.x) >> 6; const int32_t dy = (target->pos.y - item->pos.y) >> 6; const int32_t dz = (target->pos.z - item->pos.z) >> 6; const int32_t distance = SQUARE(dz) + SQUARE(dy) + SQUARE(dx); if (distance < best_distance + M_TARGET_TOLERANCE) { creature->enemy = best_item; } } } static ITEM *M_ChooseEnemy(const ITEM *const item) { CREATURE *const creature = item->creature_data; if (Creature_IsAlly(item)) { M_GetBaddieTarget(creature->item_num, true); } else if (Creature_IsAllyTargetingEnemy(item)) { M_GetBaddieTarget(creature->item_num, false); } else { creature->enemy = Lara_GetItem(); } if (creature->enemy != nullptr) { return creature->enemy; } return Lara_GetItem(); } static bool M_SwitchToWater( const int16_t item_num, const int32_t wh, const HYBRID_INFO *const info) { if (wh == NO_HEIGHT) { return false; } ITEM *const item = Item_Get(item_num); if (item->hit_points <= 0 && item->current_anim_state == info->land.death_state) { // Dead land creatures should remain in their pose permanently. return false; } // Switch to the water creature, but only switch animations if the creature // is alive to avoid savegame reload issues. if (!M_TestSwitchOrKill(item_num, info->water.id)) { return false; } item->object_id = info->water.id; if (item->hit_points > 0) { Item_SwitchToAnim(item, info->water.active_anim, 0); item->current_anim_state = Item_GetAnim(item)->current_anim_state; item->goal_anim_state = item->current_anim_state; } item->pos.y = wh; return true; } static bool M_SwitchToLand( const int16_t item_num, const int32_t wh, const HYBRID_INFO *const info) { if (wh != NO_HEIGHT) { return false; } if (!M_TestSwitchOrKill(item_num, info->land.id)) { return false; } ITEM *const item = Item_Get(item_num); // Switch to the land creature regardless of death state. item->object_id = info->land.id; item->rot.x = 0; if (item->hit_points > 0) { Item_SwitchToAnim(item, info->land.active_anim, 0); item->current_anim_state = Item_GetAnim(item)->current_anim_state; item->goal_anim_state = item->current_anim_state; } else { Item_SwitchToAnim(item, info->land.death_anim, -1); item->current_anim_state = info->land.death_state; item->goal_anim_state = item->current_anim_state; int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); item->floor = Room_GetHeight(sector, item->pos); item->pos.y = item->floor; Item_UpdateRoom(item_num, room_num); } return true; } const ITEM *M_GetBaddieOverlap(const int16_t item_num) { const ITEM *item = Item_Get(item_num); if (item->speed == 0 || item->hit_points <= 0) { return nullptr; } const int32_t x = item->pos.x; const int32_t y = item->pos.y; const int32_t z = item->pos.z; int32_t radius = Object_Get(item->object_id)->radius; if (g_TRVersion < 3) { radius = SQUARE(radius); } int16_t link = Room_Get(item->room_num)->item_num; while (link != NO_ITEM && link != item_num) { item = Item_Get(link); if (item != Lara_GetItem() && item->status == IS_ACTIVE) { if (g_TRVersion >= 3 && item->hit_points > 0) { const int32_t dx = ABS(item->pos.x - x); const int32_t dz = ABS(item->pos.z - z); const int32_t distance = dx > dz ? dx + (dz >> 1) : dz + (dx >> 1); const int32_t item_radius = Object_Get(item->object_id)->radius; if (distance < item_radius + radius) { return item; } } else if (g_TRVersion < 3 && item->speed != 0) { const XYZ_32 delta = { item->pos.x - x, item->pos.y - y, item->pos.z - z, }; const int32_t distance = SQUARE(delta.x) + SQUARE(delta.y) + SQUARE(delta.z); if (distance < radius) { return item; } } } link = item->next_item; } return nullptr; } static bool M_TestDrowned( const ITEM *const item, const BOUNDS_16 *const bounds, const int16_t room_num) { if (item->hit_points <= 0) { return false; } switch (g_Config.gameplay.creature_drown_policy) { case CREATURE_DROWN_POLICY_DEFAULT: return Room_Get(room_num)->flags.underwater; case CREATURE_DROWN_POLICY_SUBMERGED: const int32_t water_height = Room_GetWaterHeight(item->pos, room_num); return water_height != NO_HEIGHT && water_height + STEP_L <= item->pos.y - ABS(bounds->min.y); default: return false; } } void Creature_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); // TODO: remove GF check once demo config reset is run before level load if (g_Config.gameplay.enable_enemy_rotation || GF_GetCurrentLevel()->type == GFL_DEMO) { item->rot.y += (Random_GetControl() - DEG_90) >> 1; } item->collidable = true; item->creature_data = nullptr; item->extra_rotations = nullptr; } bool Creature_Activate(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (item->status != IS_INVISIBLE) { return item->creature_data != nullptr; } if (!LOT_EnableBaddieAI(item_num, false)) { return false; } item->status = IS_ACTIVE; return true; } void Creature_AIInfo(ITEM *const item, AI_INFO *const info) { CREATURE *const creature = item->creature_data; if (creature == nullptr) { return; } ITEM *enemy = g_TRVersion >= 3 ? creature->enemy : M_ChooseEnemy(item); if (enemy == nullptr) { enemy = Lara_GetItem(); creature->enemy = enemy; } const int16_t *const zone = Box_GetLotZone(&creature->lot); const bool use_fixed_fly_zone = g_TRVersion == 3 && creature->lot.setup.fly != 0; { const ROOM *const room = Room_Get(item->room_num); item->box_num = Room_GetWorldSector(room, item->pos.x, item->pos.z)->box; info->zone_num = use_fixed_fly_zone ? BOX_FIXED_FLY_ZONE : zone[item->box_num]; } { const ROOM *const room = Room_Get(enemy->room_num); enemy->box_num = Room_GetWorldSector(room, enemy->pos.x, enemy->pos.z)->box; info->enemy_zone_num = use_fixed_fly_zone ? BOX_FIXED_FLY_ZONE : zone[enemy->box_num]; } const BOX_INFO *const enemy_box = Box_GetBox(enemy->box_num); // TODO: TR3 defines non-LOT creatures, like cobras and handles them // differently here and in LOT initialisation. if (((enemy_box->overlap_index & creature->lot.setup.block_mask) != 0) || (creature->lot.node[item->box_num].search_num == (creature->lot.search_num | BOX_BLOCKED_SEARCH))) { info->enemy_zone_num |= BOX_BLOCKED; } const OBJECT *const obj = Object_Get(item->object_id); const ITEM *const lara_item = Lara_GetItem(); const LARA_INFO *const lara = Lara_GetLaraInfo(); const XZ_32 pivot = { .x = (obj->pivot_length * Math_Sin(item->rot.y)) >> W2V_SHIFT, .z = (obj->pivot_length * Math_Cos(item->rot.y)) >> W2V_SHIFT, }; XZ_32 enemy_pos = { .x = enemy->pos.x, .z = enemy->pos.z, }; if (g_TRVersion >= 3) { const int16_t enemy_angle = enemy == lara_item ? lara->move_angle : enemy->rot.y; enemy_pos.x += (14 * enemy->speed * Math_Sin(enemy_angle)) >> W2V_SHIFT; enemy_pos.z += (14 * enemy->speed * Math_Cos(enemy_angle)) >> W2V_SHIFT; } int32_t x = enemy_pos.x - pivot.x - item->pos.x; int32_t z = enemy_pos.z - pivot.z - item->pos.z; int32_t y = item->pos.y - enemy->pos.y; // sic, reversed if (enemy == lara_item && Lara_HasState(m_CrouchShiftStates)) { y -= STEP_L * 3 / 2; } const bool too_far = ABS(z) > M_MAX_DISTANCE || ABS(x) > M_MAX_DISTANCE; if (creature->enemy == nullptr || (g_TRVersion == 3 && too_far)) { info->distance = INT32_MAX; } else if (g_TRVersion < 3 && too_far) { info->distance = SQUARE(M_MAX_DISTANCE); } else { info->distance = SQUARE(x) + SQUARE(z); } const int16_t angle = Math_Atan(z, x); info->angle = angle - item->rot.y; info->enemy_facing = angle - enemy->rot.y + DEG_180; info->ahead = info->angle > -FRONT_ARC && info->angle < FRONT_ARC; info->bite = info->ahead && ABS(enemy->pos.y - item->pos.y) <= M_BITE_DISTANCE && (g_TRVersion == 1 || enemy->hit_points > 0); x = ABS(x); z = ABS(z); if (x > z) { info->x_angle = Math_Atan(x + (z >> 1), y); } else { info->x_angle = Math_Atan(z + (x >> 1), y); } if (g_TRVersion == 3) { if (!creature->hurt_by_lara && creature->enemy == lara_item && !Creature_IsHostile(item) && Creature_IsAlly(item)) { creature->enemy = nullptr; info->ahead = false; info->bite = false; info->distance = INT32_MAX; } } } bool Creature_EnsureHabitat( const int16_t item_num, int32_t *const wh, const HYBRID_INFO *const info) { // Test the environment for a hybrid creature. Record the water height and // return whether or not a type conversion has taken place. const ITEM *const item = Item_Get(item_num); const int32_t water_height = Room_GetWaterHeight(item->pos, item->room_num); if (wh != nullptr) { *wh = water_height; } if (item->status == IS_INACTIVE) { return false; } return item->object_id == info->land.id ? M_SwitchToWater(item_num, water_height, info) : M_SwitchToLand(item_num, water_height, info); } void Creature_Mood( const ITEM *const item, const AI_INFO *const info, const bool violent) { Creature_UpdateMood(item, info, violent); Creature_ApplyMood(item, info, violent); } void Creature_UpdateMood( const ITEM *const item, const AI_INFO *const info, const bool violent) { CREATURE *const creature = item->creature_data; if (creature == nullptr) { return; } LOT_INFO *const lot = &creature->lot; const ITEM *enemy = creature->enemy; if (lot->node[item->box_num].search_num == (lot->search_num | BOX_BLOCKED_SEARCH)) { lot->required_box = NO_BOX; } if (creature->mood != MOOD_ATTACK && lot->required_box != NO_BOX && !Box_ValidBox(item, info->zone_num, lot->target_box)) { if (info->zone_num == info->enemy_zone_num) { creature->mood = MOOD_BORED; } lot->required_box = NO_BOX; } const MOOD_TYPE mood = creature->mood; if (enemy == nullptr) { creature->mood = MOOD_BORED; enemy = Lara_GetItem(); } else if ( enemy->hit_points <= 0 && (g_TRVersion < 3 || enemy == Lara_GetItem())) { creature->mood = MOOD_BORED; } else if (violent) { switch (mood) { case MOOD_BORED: case MOOD_STALK: if (info->zone_num == info->enemy_zone_num) { creature->mood = MOOD_ATTACK; } else if (item->hit_status) { creature->mood = MOOD_ESCAPE; } break; case MOOD_ATTACK: if (info->zone_num != info->enemy_zone_num) { creature->mood = MOOD_BORED; } break; case MOOD_ESCAPE: if (info->zone_num == info->enemy_zone_num) { creature->mood = MOOD_ATTACK; } break; } } else { switch (mood) { case MOOD_BORED: case MOOD_STALK: if (g_TRVersion >= 3 && creature->alerted && info->zone_num != info->enemy_zone_num) { creature->mood = info->distance > WALL_L * 3 ? MOOD_STALK : MOOD_BORED; } else if ( g_TRVersion < 3 && item->hit_status && (Random_GetControl() < M_ESCAPE_CHANCE || info->zone_num != info->enemy_zone_num)) { creature->mood = MOOD_ESCAPE; } else if (info->zone_num == info->enemy_zone_num) { if (info->distance < M_ATTACK_RANGE || (creature->mood == MOOD_STALK && lot->required_box == NO_BOX)) { creature->mood = MOOD_ATTACK; } else { creature->mood = MOOD_STALK; } } break; case MOOD_ATTACK: if (item->hit_status && (Random_GetControl() < M_ESCAPE_CHANCE || info->zone_num != info->enemy_zone_num)) { creature->mood = g_TRVersion < 3 ? MOOD_ESCAPE : MOOD_STALK; } else if ( info->zone_num != info->enemy_zone_num && (g_TRVersion < 3 || info->distance > 6 * WALL_L)) { creature->mood = MOOD_BORED; } break; case MOOD_ESCAPE: if (info->zone_num == info->enemy_zone_num && Random_GetControl() < M_RECOVER_CHANCE) { creature->mood = MOOD_STALK; } break; } } if (mood != creature->mood) { if (mood == MOOD_ATTACK) { Box_TargetBox(lot, lot->target_box); } lot->required_box = NO_BOX; } } void Creature_ApplyMood( const ITEM *const item, const AI_INFO *const info, const bool violent) { CREATURE *const creature = item->creature_data; if (creature == nullptr) { return; } LOT_INFO *const lot = &creature->lot; const ITEM *enemy = creature->enemy; switch (creature->mood) { case MOOD_BORED: { const int16_t box_num = lot->node[lot->zone_count * Random_GetControl() / 0x7FFF].box_num; if (Box_ValidBox(item, info->zone_num, box_num)) { if (Box_StalkBox(item, enemy, box_num) && creature->enemy != nullptr && enemy->hit_points > 0) { Box_TargetBox(lot, box_num); if (g_TRVersion < 3) { creature->mood = MOOD_STALK; } } else if (lot->required_box == NO_BOX) { Box_TargetBox(lot, box_num); } } break; } case MOOD_ATTACK: { const int32_t smartness = Object_Get(item->object_id)->smartness; if (smartness < 0 || Random_GetControl() < smartness) { lot->target = enemy->pos; lot->required_box = enemy->box_num; if (lot->setup.fly != 0 && Lara_GetLaraInfo()->water_status == LWS_ABOVE_WATER) { lot->target.y += Item_GetBestFrame(enemy)->bounds.min.y; } } break; } case MOOD_ESCAPE: { const int16_t box_num = lot->node[lot->zone_count * Random_GetControl() / 0x7FFF].box_num; if (Box_ValidBox(item, info->zone_num, box_num) && lot->required_box == NO_BOX) { if (Box_EscapeBox(item, enemy, box_num)) { Box_TargetBox(lot, box_num); } else if ( info->zone_num == info->enemy_zone_num && Box_StalkBox(item, enemy, box_num) && (g_TRVersion < 3 || !violent)) { Box_TargetBox(lot, box_num); creature->mood = MOOD_STALK; } } break; } case MOOD_STALK: { if (lot->required_box == NO_BOX || !Box_StalkBox(item, enemy, lot->required_box)) { const int16_t box_num = lot->node[lot->zone_count * Random_GetControl() / 0x7FFF] .box_num; if (Box_ValidBox(item, info->zone_num, box_num)) { if (Box_StalkBox(item, enemy, box_num)) { Box_TargetBox(lot, box_num); } else if (lot->required_box == NO_BOX) { Box_TargetBox(lot, box_num); if (info->zone_num != info->enemy_zone_num) { creature->mood = MOOD_BORED; } } } } break; } } if (lot->target_box == NO_BOX) { Box_TargetBox(lot, item->box_num); } Box_CalculateTarget(&creature->target, item, lot); } int16_t Creature_Turn(ITEM *const item, int16_t max_turn) { const CREATURE *const creature = item->creature_data; if (creature == nullptr || max_turn == 0) { return 0; } if (item->speed == 0 && g_TRVersion < 3) { return 0; } const int32_t dx = creature->target.x - item->pos.x; const int32_t dz = creature->target.z - item->pos.z; int16_t angle = Math_Atan(dz, dx) - item->rot.y; if (angle > FRONT_ARC || angle < -FRONT_ARC) { const int32_t range = (item->speed * (1 << 14)) / max_turn; if (SQUARE(dx) + SQUARE(dz) < SQUARE(range)) { max_turn /= 2; } } CLAMP(angle, -max_turn, max_turn); item->rot.y += angle; return angle; } void Creature_Tilt(ITEM *const item, int16_t angle) { angle = angle * 4 - item->rot.z; CLAMP(angle, -M_MAX_TILT, M_MAX_TILT); item->rot.z += angle; } void Creature_Head(ITEM *const item, const int16_t required) { CREATURE *const creature = item->creature_data; if (creature == nullptr) { return; } int16_t change = required - creature->head_rotation; CLAMP(change, -M_MAX_HEAD_CHANGE, M_MAX_HEAD_CHANGE); creature->head_rotation += change; CLAMP(creature->head_rotation, -M_HEAD_ARC, M_HEAD_ARC); } void Creature_Neck(ITEM *const item, const int16_t required) { CREATURE *const creature = item->creature_data; if (creature == nullptr) { return; } int16_t change = required - creature->neck_rotation; CLAMP(change, -M_MAX_HEAD_CHANGE, M_MAX_HEAD_CHANGE); creature->neck_rotation += change; CLAMP(creature->neck_rotation, -M_HEAD_ARC, M_HEAD_ARC); } void Creature_Joint( ITEM *const item, const int16_t joint, const int16_t required) { CREATURE *const creature = item->creature_data; if (creature == nullptr) { return; } int16_t change = required - creature->joint_rotation[joint]; CLAMP(change, -M_MAX_JOINT_CHANGE, M_MAX_JOINT_CHANGE); creature->joint_rotation[joint] += change; CLAMP(creature->joint_rotation[joint], -M_JOINT_ARC, M_JOINT_ARC); } void Creature_Float(const int16_t item_num) { ITEM *const item = Item_Get(item_num); item->hit_points = 0; item->rot.x = 0; const int32_t wh = Room_GetWaterHeight(item->pos, item->room_num); if (item->pos.y > wh) { item->pos.y -= M_FLOAT_SPEED; } CLAMPL(item->pos.y, wh); Item_Animate(item); int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); item->floor = Room_GetHeight(sector, item->pos); Item_UpdateRoom(item_num, room_num); } void Creature_Underwater(ITEM *const item, const int32_t depth) { const int32_t wh = Room_GetWaterHeight(item->pos, item->room_num); if (item->pos.y >= wh + depth) { return; } item->pos.y = wh + depth; if (item->rot.x > 2 * DEG_1) { item->rot.x -= 2 * DEG_1; } else { CLAMPG(item->rot.x, 0); } } bool Creature_CanSeeEnemy(const ITEM *const item, const AI_INFO *const info) { // XXX(Dash): I don't understand the need for this function, // when there's CanTargetEnemy(). const CREATURE *const creature = item->creature_data; const ITEM *const enemy = creature->enemy; if (enemy == nullptr || enemy->hit_points <= 0 || (enemy != Lara_GetItem() && enemy->creature_data == nullptr) || info->angle - creature->joint_rotation[2] <= -DEG_90 || info->angle - creature->joint_rotation[2] >= DEG_90 || info->distance >= CREATURE_SHOOT_RANGE) { return false; } GAME_VECTOR start = { .x = item->pos.x, .y = item->pos.y - STEP_L * 3, .z = item->pos.z, .room_num = item->room_num, }; const BOUNDS_16 *const bounds = &Item_GetBestFrame(enemy)->bounds; GAME_VECTOR target = { .x = enemy->pos.x, .y = enemy->pos.y + ((3 * bounds->min.y + bounds->max.y) >> 2), .z = enemy->pos.z, .room_num = enemy->room_num, }; return LOS_Check(&start, &target, true); } bool Creature_CanTargetEnemy(const ITEM *const item, const AI_INFO *const info) { const CREATURE *const creature = item->creature_data; if (creature == nullptr) { return false; } const ITEM *const enemy = creature->enemy; if (enemy == nullptr || !info->ahead || info->distance >= CREATURE_SHOOT_RANGE) { return false; } GAME_VECTOR start = { .pos = item->pos, .room_num = item->room_num }; GAME_VECTOR target = { .pos = enemy->pos, .room_num = enemy->room_num }; if (g_TRVersion == 3) { if (enemy->hit_points <= 0 || (enemy != Lara_GetItem() && enemy->creature_data == nullptr)) { return false; } const BOUNDS_16 *const bounds1 = &Item_GetBestFrame(item)->bounds; const BOUNDS_16 *const bounds2 = &Item_GetBestFrame(enemy)->bounds; start.pos.y += (bounds1->max.y + 3 * bounds1->min.y) >> 2; target.pos.y += (bounds2->max.y + 3 * bounds2->min.y) >> 2; } else { start.pos.y -= STEP_L * 3; target.pos.y -= STEP_L * 3; } return LOS_Check(&start, &target, true); } void Creature_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); LARA_INFO *const lara = Lara_GetLaraInfo(); if (!Lara_TestBoundsCollide(item, coll->radius)) { return; } if (!Collide_TestCollision(item, lara_item)) { return; } if (lara->water_status == LWS_UNDERWATER || lara->water_status == LWS_SURFACE) { return; } if (coll->enable_baddie_push) { Lara_Col_ItemPush( item, coll, (g_TRVersion >= 2 || item->hit_points > 0) ? coll->enable_hit : false, false); } else if (coll->enable_hit && g_TRVersion == 3) { const BOUNDS_16 *const bounds = &Item_GetBestFrame(item)->bounds; const int32_t s = Math_Sin(lara_item->rot.y); const int32_t c = Math_Cos(lara_item->rot.y); const int32_t x = (bounds->min.x + bounds->max.x) / 2; const int32_t z = (bounds->max.z - bounds->min.z) / 2; const int32_t rx = (lara_item->pos.x - item->pos.x) - ((c * x + s * z) >> W2V_SHIFT); const int32_t rz = (lara_item->pos.z - item->pos.z) - ((c * z - s * x) >> W2V_SHIFT); if (bounds->max.z - bounds->min.z > STEP_L) { lara->hit_direction = (lara_item->rot.y + DEG_180 - Math_Atan(rz, rx) + DEG_45) >> W2V_SHIFT; lara->hit_frame++; CLAMPG(lara->hit_frame, 30); } } } bool Creature_Animate( const int16_t item_num, const int16_t angle, const int16_t tilt) { ITEM *const item = Item_Get(item_num); const CREATURE *const creature = item->creature_data; const OBJECT *const obj = Object_Get(item->object_id); if (creature == nullptr) { return false; } const LOT_INFO *const lot = &creature->lot; const XYZ_32 old = item->pos; const int16_t *const zone = Box_GetLotZone(lot); if (g_TRVersion >= 2 && !Object_IsType(item->object_id, g_WaterObjects)) { int16_t room_num = item->room_num; Room_GetSector(item->pos, &room_num); Item_UpdateRoom(item_num, room_num); } Item_Animate(item); if (item->status == IS_DEACTIVATED) { Creature_Die(item_num, false); return false; } const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item); int32_t y = item->pos.y + bounds->min.y; int16_t room_num = item->room_num; XYZ_32 sample_pos = { old.x, y, old.z }; Room_GetSector(sample_pos, &room_num); sample_pos.x = item->pos.x; sample_pos.z = item->pos.z; const SECTOR *sector = Room_GetSector(sample_pos, &room_num); int32_t height = Box_GetBox(sector->box)->height; int16_t next_box = lot->node[sector->box].exit_box; int32_t next_height = next_box != NO_BOX ? Box_GetBox(next_box)->height : height; const bool fly_check = g_TRVersion < 3 || lot->setup.fly == 0; const int32_t box_height = Box_GetBox(item->box_num)->height; if (sector->box == NO_BOX || (fly_check && zone[item->box_num] != zone[sector->box]) || box_height - height > lot->setup.step || box_height - height < lot->setup.drop) { const int32_t pos_x = item->pos.x >> WALL_SHIFT; const int32_t pos_z = g_TRVersion < 3 ? pos_x : item->pos.z >> WALL_SHIFT; // TODO: OG bug in TR1/2? const int32_t shift_x = old.x >> WALL_SHIFT; const int32_t shift_z = old.z >> WALL_SHIFT; if (pos_x < shift_x) { item->pos.x = ROUND_TO_SECTOR(old.x); } else if (pos_x > shift_x) { item->pos.x = ROUND_TO_SECTOR_END(old.x); } if (pos_z < shift_z) { item->pos.z = ROUND_TO_SECTOR(old.z); } else if (pos_z > shift_z) { item->pos.z = ROUND_TO_SECTOR_END(old.z); } sample_pos.x = item->pos.x; sample_pos.y = y; sample_pos.z = item->pos.z; sector = Room_GetSector(sample_pos, &room_num); height = Box_GetBox(sector->box)->height; next_box = lot->node[sector->box].exit_box; next_height = next_box != NO_BOX ? Box_GetBox(next_box)->height : height; } const int32_t x = item->pos.x; const int32_t z = item->pos.z; const int32_t pos_x = x & (WALL_L - 1); const int32_t pos_z = z & (WALL_L - 1); int32_t shift_x = 0; int32_t shift_z = 0; const int32_t radius = obj->radius; if (pos_z < radius) { if (Box_BadFloor( x, y, z - radius, height, next_height, room_num, lot)) { shift_z = radius - pos_z; } if (pos_x < radius) { if (Box_BadFloor( x - radius, y, z, height, next_height, room_num, lot)) { shift_x = radius - pos_x; } else if ( shift_z == 0 && Box_BadFloor( x - radius, y, z - radius, height, next_height, room_num, lot)) { if (item->rot.y > -DEG_135 && item->rot.y < DEG_45) { shift_z = radius - pos_z; } else { shift_x = radius - pos_x; } } } else if (pos_x > WALL_L - radius) { if (Box_BadFloor( x + radius, y, z, height, next_height, room_num, lot)) { shift_x = WALL_L - radius - pos_x; } else if ( shift_z == 0 && Box_BadFloor( x + radius, y, z - radius, height, next_height, room_num, lot)) { if (item->rot.y > -DEG_45 && item->rot.y < DEG_135) { shift_z = radius - pos_z; } else { shift_x = WALL_L - radius - pos_x; } } } } else if (pos_z > WALL_L - radius) { if (Box_BadFloor( x, y, z + radius, height, next_height, room_num, lot)) { shift_z = WALL_L - radius - pos_z; } if (pos_x < radius) { if (Box_BadFloor( x - radius, y, z, height, next_height, room_num, lot)) { shift_x = radius - pos_x; } else if ( shift_z == 0 && Box_BadFloor( x - radius, y, z + radius, height, next_height, room_num, lot)) { if (item->rot.y > -DEG_45 && item->rot.y < DEG_135) { shift_x = radius - pos_x; } else { shift_z = WALL_L - radius - pos_z; } } } else if (pos_x > WALL_L - radius) { if (Box_BadFloor( x + radius, y, z, height, next_height, room_num, lot)) { shift_x = WALL_L - radius - pos_x; } else if ( shift_z == 0 && Box_BadFloor( x + radius, y, z + radius, height, next_height, room_num, lot)) { if (item->rot.y > -DEG_135 && item->rot.y < DEG_45) { shift_x = WALL_L - radius - pos_x; } else { shift_z = WALL_L - radius - pos_z; } } } } else if (pos_x < radius) { if (Box_BadFloor( x - radius, y, z, height, next_height, room_num, lot)) { shift_x = radius - pos_x; } } else if (pos_x > WALL_L - radius) { if (Box_BadFloor( x + radius, y, z, height, next_height, room_num, lot)) { shift_x = WALL_L - radius - pos_x; } } item->pos.x += shift_x; item->pos.z += shift_z; if (shift_x != 0 || shift_z != 0) { sample_pos.x = item->pos.x; sample_pos.y = y; sample_pos.z = item->pos.z; sector = Room_GetSector(sample_pos, &room_num); item->rot.y += angle; Creature_Tilt(item, tilt * 2); } if (g_TRVersion < 3 || (item->object_id != O_TREX && item->object_id != O_TREX_ALPHA)) { const ITEM *const hit_item = M_GetBaddieOverlap(item_num); if (g_TRVersion < 3 && hit_item != nullptr) { item->pos = old; return true; } if (g_TRVersion >= 3 && hit_item != nullptr) { const int16_t item_angle = Math_Atan( hit_item->pos.z - item->pos.z, hit_item->pos.x - item->pos.x) - item->rot.y; if (item_angle != 0) { if (ABS(item_angle) < 2048) { item->rot.y -= item_angle; } else if (item_angle > 0) { item->rot.y -= 2048; } else { item->rot.y += 2048; } } return true; } } if (lot->setup.fly != 0) { int32_t dy = creature->target.y - item->pos.y; CLAMP(dy, -lot->setup.fly, lot->setup.fly); height = Room_GetHeight(sector, (XYZ_32) { item->pos.x, y, item->pos.z }); if (item->pos.y + dy > height) { if (item->pos.y <= height) { dy = 0; item->pos.y = height; } else { dy = -lot->setup.fly; item->pos.x = old.x; item->pos.z = old.z; } } else if ( fly_check || Object_IsType(item->object_id, g_WaterObjects)) { const int32_t ceiling = Room_GetCeiling( sector, (XYZ_32) { item->pos.x, y, item->pos.z }); int32_t min_y = bounds->min.y; switch (item->object_id) { case O_ALLIGATOR: min_y = 0; break; case O_SHARK: case O_ORCA: min_y = 128; break; default: break; } if (item->pos.y + min_y + dy < ceiling) { if (item->pos.y + min_y < ceiling) { item->pos.x = old.x; item->pos.z = old.z; dy = lot->setup.fly; } else { dy = 0; } } } else { sample_pos = (XYZ_32) { item->pos.x, y + STEP_L, item->pos.z }; Room_GetSector(sample_pos, &room_num); const ROOM *const room = Room_Get(room_num); if (room->flags.underwater || room->flags.swamp) { dy = -lot->setup.fly; } } item->pos.y += dy; sample_pos = (XYZ_32) { item->pos.x, y, item->pos.z }; sector = Room_GetSector(sample_pos, &room_num); item->floor = Room_GetHeight(sector, sample_pos); int16_t item_angle = item->speed != 0 ? Math_Atan(item->speed, -dy) : 0; if (g_TRVersion >= 2) { CLAMP(item_angle, -M_MAX_X_ROT, M_MAX_X_ROT); } if (item_angle < item->rot.x - DEG_1) { item->rot.x -= DEG_1; } else if (item_angle > item->rot.x + DEG_1) { item->rot.x += DEG_1; } else { item->rot.x = item_angle; } } else { sector = Room_GetSector(item->pos, &room_num); if (g_TRVersion == 3) { const int32_t ceiling = Room_GetCeiling(sector, item->pos); int32_t min_y = bounds->min.y; switch (item->object_id) { case O_TREX: case O_TREX_ALPHA: case O_SHIVA: case O_CLAW_MUTANT: min_y = STEP_L * 3; break; default: break; } if (item->pos.y + min_y < ceiling) { item->pos = old; sector = Room_GetSector(item->pos, &room_num); } } item->floor = Room_GetHeight(sector, item->pos); if (item->pos.y > item->floor) { item->pos.y = item->floor; } else if (item->floor - item->pos.y > STEP_L / 4) { item->pos.y += STEP_L / 4; } else if (item->pos.y < item->floor) { item->pos.y = item->floor; } item->rot.x = 0; } if (!Object_IsType(item->object_id, g_WaterObjects)) { // Get the room just above the enemy so that if it is in one-click high // water, its effects behave still as though in a dry room. Room_GetSector( (XYZ_32) { item->pos.x, item->pos.y - (STEP_L * 2), item->pos.z }, &room_num); if (M_TestDrowned(item, bounds, room_num)) { item->hit_points = 0; } } Item_UpdateRoom(item_num, room_num); return true; } void Creature_SpecialKill( ITEM *const item, const int32_t kill_anim, const int32_t kill_state, const int32_t lara_kill_state) { LARA_INFO *const lara = Lara_GetLaraInfo(); ITEM *const lara_item = Lara_GetItem(); Item_SwitchToAnim(item, kill_anim, 0); item->current_anim_state = kill_state; lara_item->pos = item->pos; lara_item->rot = item->rot; lara_item->fall_speed = 0; lara_item->gravity = false; lara_item->speed = 0; lara->air = -1; lara->gun_type = LGT_UNARMED; int16_t room_num = item->room_num; Item_UpdateRoom(lara->item_num, room_num); Lara_SwitchToExtraState(lara_kill_state); g_Camera.pos.room_num = lara_item->room_num; } void Creature_TestBoxDamage(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (item->box_num == NO_BOX || (Box_GetBox(item->box_num)->overlap_index & BOX_BLOCKED) == 0) { return; } const XYZ_32 pos = { .x = item->pos.x, .y = item->pos.y - (Random_GetControl() & 0xFF) - 32, .z = item->pos.z, }; Spawn_BloodBath( pos.x, pos.y, pos.z, (Random_GetControl() & 0x7F) + STEP_L / 2, Random_GetControl() << 1, item->room_num, 3); if (item->hit_points <= 0) { return; } item->hit_points -= M_BOX_DAMAGE; if (item->hit_points <= 0) { Stats_AddKill(); } } void Creature_Die(const int16_t item_num, const bool explode) { ITEM *const item = Item_Get(item_num); switch (item->object_id) { case O_LIZARD: TribeBoss_SetLizardActive(false); break; case O_DRAGON_FRONT: case O_TORSO: item->hit_points = 0; return; case O_SKIDOO_ARMED: if (explode) { Item_Explode(item_num, -1, 0); ITEM *const vehicle_item = Item_Get(item_num); vehicle_item->hit_points = 0; vehicle_item->status = IS_INVISIBLE; return; } break; case O_SKIDOO_DRIVER: if (explode) { Item_Explode(item_num, -1, 0); } item->hit_points = 0; const int16_t vehicle_item_num = SkidooDriver_GetSkidooItemNum(item); if (vehicle_item_num == NO_ITEM) { return; } ITEM *const vehicle_item = Item_Get(vehicle_item_num); vehicle_item->hit_points = 0; vehicle_item->status = IS_INVISIBLE; return; default: break; } item->collidable = false; item->hit_points = 0; if (explode) { Item_Explode(item_num, -1, 0); Item_Kill(item_num); } else { Item_RemoveActive(item_num); } const OBJECT *const obj = Object_Get(item->object_id); if (obj->intelligent) { LOT_DisableBaddieAI(item_num); } item->flags |= IF_ONE_SHOT; Carrier_TestItemDrops(item_num); } int32_t Creature_Vault( const int16_t item_num, const int16_t angle, int32_t vault, const int32_t shift) { ITEM *const item = Item_Get(item_num); const int16_t room_num = item->room_num; const XYZ_32 old = item->pos; Creature_Animate(item_num, angle, 0); if (item->floor > old.y + STEP_L * 7 / 2) { vault = -4; } else if ( item->floor > old.y + STEP_L * 5 / 2 && item->object_id == O_MONKEY) { vault = -3; } else if ( item->floor > old.y + STEP_L * 3 / 2 && item->object_id == O_MONKEY) { vault = -2; } else if (item->pos.y > old.y - STEP_L * 3 / 2) { return 0; } else if (item->pos.y > old.y - STEP_L * 5 / 2) { vault = 2; } else if (item->pos.y > old.y - STEP_L * 7 / 2) { vault = 3; } else { vault = 4; } const int32_t old_x_sector = old.x >> WALL_SHIFT; const int32_t old_z_sector = old.z >> WALL_SHIFT; const int32_t x_sector = item->pos.x >> WALL_SHIFT; const int32_t z_sector = item->pos.z >> WALL_SHIFT; if (old_z_sector == z_sector) { if (old_x_sector == x_sector) { return 0; } if (old_x_sector >= x_sector) { item->rot.y = -DEG_90; item->pos.x = (old_x_sector * WALL_L) + shift; } else { item->rot.y = DEG_90; item->pos.x = (x_sector * WALL_L) - shift; } } else if (old_x_sector == x_sector) { if (old_z_sector >= z_sector) { item->rot.y = -DEG_180; item->pos.z = (old_z_sector * WALL_L) + shift; } else { item->rot.y = 0; item->pos.z = (z_sector * WALL_L) - shift; } } item->floor = old.y; item->pos.y = old.y; Item_UpdateRoom(item_num, room_num); return vault; } int16_t Creature_Effect( const ITEM *const item, const BITE *const bite, int16_t (*const spawn)( int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num)) { XYZ_32 pos = bite->pos; Collide_GetJointAbsPosition(item, &pos, bite->mesh_num); return spawn(pos.x, pos.y, pos.z, item->speed, item->rot.y, item->room_num); } int16_t Creature_AIGuard(CREATURE *const creature) { if (Item_Get(creature->item_num)->ai_bits & AI_MODIFY) { return 0; } const int32_t rnd = Random_GetControl(); if (rnd < 256) { creature->head_left = true; creature->head_right = true; } else if (rnd < 384) { creature->head_left = true; creature->head_right = false; } else if (rnd < 512) { creature->head_left = false; creature->head_right = true; } if (creature->head_left && creature->head_right) { return 0; } if (creature->head_left) { return -DEG_90; } if (creature->head_right) { return DEG_90; } return 0; } static bool M_SameZone(const CREATURE *const creature, ITEM *const target_item) { if (creature->lot.setup.fly != 0) { return true; } int16_t *const zone = Box_GetGroundZone( Room_GetFlipStatus(), (creature->lot.setup.step >> 8) - 1); ITEM *const item = Item_Get(creature->item_num); const ROOM *room = Room_Get(item->room_num); item->box_num = Room_GetWorldSector(room, item->pos.x, item->pos.z)->box; room = Room_Get(target_item->room_num); target_item->box_num = Room_GetWorldSector(room, target_item->pos.x, target_item->pos.z)->box; return zone[item->box_num] == zone[target_item->box_num]; } void Creature_GetAITarget(CREATURE *const creature) { ITEM *const lara_item = Lara_GetItem(); ITEM *const item = Item_Get(creature->item_num); ITEM *const enemy = creature->enemy; const OBJECT_ID enemy_object_id = enemy != nullptr ? enemy->object_id : NO_OBJECT; uint8_t ai_bits = item->ai_bits; if (ai_bits & AI_GUARD) { creature->enemy = lara_item; if (creature->alerted) { item->ai_bits &= ~AI_GUARD; if (ai_bits & AI_AMBUSH) { item->ai_bits |= AI_MODIFY; } } } else if (ai_bits & AI_PATROL_1) { if (creature->alerted || creature->hurt_by_lara) { item->ai_bits &= ~AI_PATROL_1; if (ai_bits & AI_AMBUSH) { item->ai_bits |= AI_MODIFY; } } else if (!creature->patrol_2 && enemy_object_id != O_AI_PATROL_1) { for (int32_t i = 0; i < Item_GetTotalCount(); i++) { ITEM *const target = Item_Get(i); if (target->object_id == O_AI_PATROL_1 && target->room_num != NO_ROOM && M_SameZone(creature, target) && target->rot.y == item->ai_tag) { creature->enemy = target; return; } } } else if (creature->patrol_2 && enemy_object_id != O_AI_PATROL_2) { for (int32_t i = 0; i < Item_GetTotalCount(); i++) { ITEM *const target = Item_Get(i); if (target->object_id == O_AI_PATROL_2 && target->room_num != NO_ROOM && M_SameZone(creature, target) && target->rot.y == item->ai_tag) { creature->enemy = target; return; } } } else if ( ABS(enemy->pos.x - item->pos.x) < 768 && ABS(enemy->pos.y - item->pos.y) < 768 && ABS(enemy->pos.z - item->pos.z) < 768) { Room_TestTriggers(enemy); creature->patrol_2 = !creature->patrol_2; } } else if (ai_bits & AI_AMBUSH) { if (ai_bits & AI_MODIFY || creature->hurt_by_lara) { if (enemy_object_id != O_AI_AMBUSH) { for (int32_t i = 0; i < Item_GetTotalCount(); i++) { ITEM *const target = Item_Get(i); if (target->object_id == O_AI_AMBUSH && target->room_num != NO_ROOM && M_SameZone(creature, target) && (target->rot.y == item->ai_tag || item->object_id == O_MONKEY)) { creature->enemy = target; return; } } } else if (item->object_id != O_MONKEY) { if (ABS(enemy->pos.x - item->pos.x) < 768 && ABS(enemy->pos.y - item->pos.y) < 768 && ABS(enemy->pos.z - item->pos.z) < 768) { Room_TestTriggers(enemy); creature->reached_goal = 1; creature->enemy = lara_item; item->ai_bits &= ~(AI_AMBUSH | AI_MODIFY); item->ai_bits |= AI_GUARD; creature->alerted = false; } } } else { creature->enemy = lara_item; } } else if (ai_bits & AI_FOLLOW) { if (creature->hurt_by_lara) { creature->enemy = lara_item; creature->alerted = true; item->ai_bits &= ~AI_FOLLOW; } else if (item->hit_status) { item->ai_bits &= ~AI_FOLLOW; } else if (enemy_object_id != O_AI_FOLLOW) { for (int32_t i = 0; i < Item_GetTotalCount(); i++) { ITEM *const target = Item_Get(i); if (target->object_id == O_AI_FOLLOW && target->room_num != NO_ROOM && M_SameZone(creature, target) && target->rot.y == item->ai_tag) { creature->enemy = target; return; } } } else if ( ABS(enemy->pos.x - item->pos.x) < 768 && ABS(enemy->pos.y - item->pos.y) < 768 && ABS(enemy->pos.z - item->pos.z) < 768) { creature->reached_goal = 1; item->ai_bits &= ~AI_FOLLOW; } } else if (item->object_id == O_MONKEY && item->carried_item == nullptr) { if (creature->hurt_by_lara && g_Config.gameplay.fix_monkey_pickup_priority) { creature->enemy = lara_item; return; } if (item->ai_bits == AI_MODIFY) { if (enemy_object_id != O_KEY_ITEM_4) { for (int32_t i = 0; i < Item_GetTotalCount(); i++) { ITEM *const target = Item_Get(i); if (target->object_id == O_KEY_ITEM_4 && target->room_num != NO_ROOM && !target->ai_bits && target->status != IS_INVISIBLE && !target->clear_body && M_SameZone(creature, target)) { creature->enemy = target; return; } } } } else if (enemy_object_id != O_SMALL_MEDIPACK_ITEM) { for (int32_t i = 0; i < Item_GetTotalCount(); i++) { ITEM *const target = Item_Get(i); if (target->object_id == O_SMALL_MEDIPACK_ITEM && target->room_num != NO_ROOM && !target->ai_bits && target->status != IS_INVISIBLE && !target->clear_body && M_SameZone(creature, target)) { creature->enemy = target; return; } } } } } ================================================ FILE: src/trx/game/creature/common.h ================================================ #pragma once #include #include void Creature_Initialise(int16_t item_num); bool Creature_Activate(int16_t item_num); void Creature_AIInfo(ITEM *item, AI_INFO *info); bool Creature_EnsureHabitat( int16_t item_num, int32_t *wh, const HYBRID_INFO *info); void Creature_Mood(const ITEM *item, const AI_INFO *info, bool violent); void Creature_UpdateMood(const ITEM *item, const AI_INFO *info, bool violent); void Creature_ApplyMood(const ITEM *item, const AI_INFO *info, bool violent); int16_t Creature_Turn(ITEM *item, int16_t max_turn); void Creature_Tilt(ITEM *item, int16_t angle); void Creature_Head(ITEM *item, int16_t required); void Creature_Neck(ITEM *item, int16_t required); void Creature_Joint(ITEM *item, int16_t joint, int16_t required); void Creature_Float(int16_t item_num); void Creature_Underwater(ITEM *item, int32_t depth); bool Creature_CanSeeEnemy(const ITEM *item, const AI_INFO *info); bool Creature_CanTargetEnemy(const ITEM *item, const AI_INFO *info); void Creature_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); bool Creature_Animate(int16_t item_num, int16_t angle, int16_t tilt); void Creature_SpecialKill( ITEM *item, int32_t kill_anim, int32_t kill_state, int32_t lara_kill_state); void Creature_TestBoxDamage(int16_t item_num); void Creature_Die(int16_t item_num, bool explode); int32_t Creature_Vault( int16_t item_num, int16_t angle, int32_t vault, int32_t shift); void Creature_Reset(void); bool Creature_AreAlliesHostile(void); void Creature_SetAlliesHostile(bool enable); void Creature_Hurt(ITEM *item, int32_t damage); bool Creature_IsHostile(const ITEM *item); bool Creature_IsAlly(const ITEM *item); bool Creature_IsAllyTargetingEnemy(const ITEM *item); void Creature_AddAlly(OBJECT_ID obj_id); void Creature_AddAllyTargetingEnemy(OBJECT_ID obj_id); int16_t Creature_Effect( const ITEM *item, const BITE *bite, int16_t (*spawn)( int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num)); bool Creature_Shoot( ITEM *item, const AI_INFO *info, const CREATURE_GUN *gun, int16_t extra_rotation, int32_t damage); int16_t Creature_AIGuard(CREATURE *creature); void Creature_GetAITarget(CREATURE *creature); ================================================ FILE: src/trx/game/creature/const.h ================================================ #pragma once #include #include #define FRONT_ARC DEG_90 #define UNIT_SHADOW 256 #define CREATURE_STALK_DIST (3 * WALL_L) // = 3072 #define CREATURE_ESCAPE_DIST (5 * WALL_L) // = 5120 #define CREATURE_TARGET_DIST (4 * WALL_L) // = 4096 #define CREATURE_MISS_CHANCE 0x2000 #define CREATURE_SHOOT_RANGE SQUARE((g_TRVersion == 1 ? 7 : 8) * WALL_L) // = 51380224 (TR1), 67108864 (TR2) ================================================ FILE: src/trx/game/creature/enum.h ================================================ #pragma once typedef enum { MOOD_BORED = 0, MOOD_ATTACK = 1, MOOD_ESCAPE = 2, MOOD_STALK = 3, } MOOD_TYPE; ================================================ FILE: src/trx/game/creature/shooting.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #define M_SHOOT_TARGETING_SPEED 300 #define M_SHOOT_HIT_CHANCE 0x2000 static void M_CalcShootVectors( const ITEM *const item, const ITEM *const target_item, XYZ_32 *const start, XYZ_32 *const target) { start->x = item->pos.x; start->y = item->pos.y - STEP_L * 3; start->z = item->pos.z; target->x = target_item->pos.x; target->y = target_item->pos.y - STEP_L * 3; target->z = target_item->pos.z; const int16_t angle = XYZ_32_GetYaw((XYZ_32) { .x = target->x - start->x, .y = target->y - start->y, .z = target->z - start->z, }); const int32_t dist = WALL_L * 2; target->x += (dist * Math_Sin(angle)) >> W2V_SHIFT; target->z += (dist * Math_Cos(angle)) >> W2V_SHIFT; } static void M_TriggerTR3GunShell( const ITEM *const item, const CREATURE_GUN *const gun) { const int16_t effect_num = Effect_Create(item->room_num); if (effect_num == NO_EFFECT) { return; } XYZ_32 pos = { .x = gun->muzzle.pos.x >> 2, .y = gun->muzzle.pos.y >> 2, .z = gun->muzzle.pos.z >> 2, }; Collide_GetJointAbsPosition(item, &pos, gun->muzzle.mesh_num); EFFECT *const effect = Effect_Get(effect_num); effect->pos = pos; effect->room_num = item->room_num; effect->rot.x = 0; effect->rot.y = 0; effect->rot.z = (int16_t)Random_GetControl(); effect->speed = (int16_t)((Random_GetControl() & 0x1F) + 16); effect->object_id = O_GUN_SHELL; effect->fall_speed = (int16_t)(-48 - (Random_GetControl() & 7)); effect->frame_num = Object_Get(O_GUN_SHELL)->mesh_idx; effect->shade = 0x4210; effect->counter = 1; effect->flag1 = item->rot.y + (Random_GetControl() & 0xFFF) - 0x4800; } static void M_TriggerTR3GunSmoke( const ITEM *const item, const CREATURE_GUN *const gun) { XYZ_32 pos = { .x = gun->muzzle.pos.x - (gun->muzzle.pos.x >> 2), .y = gun->muzzle.pos.y - (gun->muzzle.pos.y >> 2), .z = gun->muzzle.pos.z - (gun->muzzle.pos.z >> 2), }; Collide_GetJointAbsPosition(item, &pos, gun->muzzle.mesh_num); GAME_VECTOR smoke_pos = { .pos = pos, .room_num = item->room_num, }; Room_GetSector(smoke_pos.pos, &smoke_pos.room_num); Sparks_TriggerGunSmoke(smoke_pos, true, LGT_PISTOLS, 32); } bool Creature_Shoot( ITEM *const item, const AI_INFO *const info, const CREATURE_GUN *const gun, const int16_t extra_rotation, const int32_t damage) { const ITEM *const lara_item = Lara_GetItem(); const CREATURE *const creature = item->creature_data; ITEM *const target_item = creature->enemy; if (g_TRVersion == 3) { M_TriggerTR3GunShell(item, gun); M_TriggerTR3GunSmoke(item, gun); } bool is_targetable; bool is_hit; if (g_TRVersion == 1) { // TR1 targeting is a bit dumb - eg with some minimal effort, Pierre // can't reach Lara in Folly. This branch preserves this behavior. if (info->distance > CREATURE_SHOOT_RANGE) { is_targetable = false; is_hit = false; } else { is_hit = Random_GetControl() < ((CREATURE_SHOOT_RANGE - info->distance) / (CREATURE_SHOOT_RANGE / 0x7FFF) - CREATURE_MISS_CHANCE); is_targetable = true; } } else { if (info->distance > CREATURE_SHOOT_RANGE || !Creature_CanTargetEnemy(item, info)) { is_targetable = false; is_hit = false; } else { int32_t distance = (((target_item->speed * Math_Sin(info->enemy_facing)) >> W2V_SHIFT) * CREATURE_SHOOT_RANGE) / M_SHOOT_TARGETING_SPEED; distance = info->distance + SQUARE(distance); if (distance > CREATURE_SHOOT_RANGE) { is_hit = false; } else { const int32_t chance = M_SHOOT_HIT_CHANCE + (CREATURE_SHOOT_RANGE - info->distance) / (CREATURE_SHOOT_RANGE / 0x5000); is_hit = Random_GetControl() < chance; } is_targetable = true; } } int16_t effect_num = NO_EFFECT; if (target_item == lara_item) { if (is_hit) { effect_num = Creature_Effect(item, &gun->muzzle, Spawn_GunHit); Item_TakeDamage(target_item, damage, true); } else if (is_targetable) { effect_num = Creature_Effect(item, &gun->muzzle, Spawn_GunMiss); } } else { effect_num = Creature_Effect(item, &gun->muzzle, Spawn_GunShot); if (is_hit) { Item_TakeDamage(target_item, damage / 10, true); const OBJECT *const target_obj = Object_Get(target_item->object_id); int32_t joint = Random_GetControl() & 0xF; if (joint >= target_obj->mesh_count) { joint = 0; } XYZ_32 pos = {}; Collide_GetJointAbsPosition(target_item, &pos, joint); Spawn_Blood( pos.x, pos.y, pos.z, target_item->speed, target_item->rot.y, target_item->room_num); } } if (FX_GunFlash_Spawn(item, gun) && effect_num != NO_EFFECT) { // Kill the old-style flash effect just spawned from previous chunk Effect_Kill(effect_num); effect_num = NO_EFFECT; } if (effect_num != NO_EFFECT) { Effect_Get(effect_num)->rot.y += extra_rotation; } XYZ_32 start, target; M_CalcShootVectors(item, target_item, &start, &target); Gun_SmashItems( (GAME_VECTOR) { .pos = start, .room_num = item->room_num, }, (GAME_VECTOR) { .pos = target, .room_num = target_item->room_num, }, nullptr, NO_OBJECT); return is_targetable; } ================================================ FILE: src/trx/game/creature/types.h ================================================ #pragma once #include #include #include typedef struct CREATURE { union { // NOTE: creature extra rotations are provided via this array. // Intelligent objects wire item->extra_rotations to joint_rotation. int16_t joint_rotation[4]; struct { // These are old TR1-2 aliases. int16_t head_rotation; int16_t neck_rotation; int16_t _extra_rotation[2]; }; }; int16_t maximum_turn; int16_t flags; bool alerted; bool head_left; bool head_right; bool reached_goal; bool hurt_by_lara; int32_t damage_from_lara; bool patrol_2; int16_t item_num; MOOD_TYPE mood; LOT_INFO lot; XYZ_32 target; ITEM *enemy; } CREATURE; typedef struct { int16_t zone_num; int16_t enemy_zone_num; int32_t distance; bool ahead; bool bite; int16_t angle; int16_t x_angle; int16_t enemy_facing; } AI_INFO; typedef struct { XYZ_32 pos; int32_t mesh_num; } BITE; typedef struct { BITE muzzle; bool tr3_enemy_flash; BITE tr3_flash; int16_t tr3_enemy_weapon_flags; int16_t tr3_flash_shade; int16_t tr3_flash_rot_x; struct { BITE bite; RGBA_8888 color; float width; } tr3_laser; } CREATURE_GUN; typedef struct { struct { OBJECT_ID id; int16_t active_anim; int16_t death_anim; int16_t death_state; } land, water; } HYBRID_INFO; ================================================ FILE: src/trx/game/creature.h ================================================ #pragma once #include #include #include #include #include ================================================ FILE: src/trx/game/cutscene.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static CAMERA_INFO m_LocalCamera = {}; static OBJECT_MESH **m_CapturedObjectMeshes = nullptr; static OBJECT_ID *m_CapturedObjectMeshOwners = nullptr; static int32_t m_CapturedObjectMeshCount = 0; static bool m_DrawLeftGunFlash = false; static bool m_DrawRightGunFlash = false; typedef struct { bool is_valid; LARA_SKIN_TYPE skin_type; LARA_GUN_TYPE hand_l_type; LARA_GUN_TYPE hand_r_type; LARA_GUN_TYPE thigh_l_type; LARA_GUN_TYPE thigh_r_type; bool holsters_visible; } M_LARA_CUTSCENE_STATE; static M_LARA_CUTSCENE_STATE m_LaraCutsceneState = {}; static LARA_GUN_TYPE M_GetGunEquipmentType(const LARA_MESH mesh) { const LARA_SKIN_EQUIPMENT *const equipment = Lara_Skin_GetEquipment(mesh); if (equipment->type == EQUIPMENT_TYPE_WEAPON) { return (LARA_GUN_TYPE)equipment->data; } return LGT_UNARMED; } static void M_CaptureLaraCutsceneState(void) { m_LaraCutsceneState.is_valid = true; m_LaraCutsceneState.skin_type = Lara_Skin_GetType(); m_LaraCutsceneState.hand_l_type = M_GetGunEquipmentType(LM_HAND_L); m_LaraCutsceneState.hand_r_type = M_GetGunEquipmentType(LM_HAND_R); m_LaraCutsceneState.thigh_l_type = M_GetGunEquipmentType(LM_THIGH_L); m_LaraCutsceneState.thigh_r_type = M_GetGunEquipmentType(LM_THIGH_R); m_LaraCutsceneState.holsters_visible = Lara_Skin_AreHolstersVisible(); } static void M_CaptureObjectMeshesState(void) { Memory_FreePointer(&m_CapturedObjectMeshes); Memory_FreePointer(&m_CapturedObjectMeshOwners); m_CapturedObjectMeshCount = Object_GetMeshCount(); if (m_CapturedObjectMeshCount <= 0) { return; } m_CapturedObjectMeshes = Memory_Alloc( m_CapturedObjectMeshCount * sizeof(*m_CapturedObjectMeshes)); m_CapturedObjectMeshOwners = Memory_Alloc( m_CapturedObjectMeshCount * sizeof(*m_CapturedObjectMeshOwners)); for (int32_t i = 0; i < m_CapturedObjectMeshCount; i++) { m_CapturedObjectMeshes[i] = Object_GetMesh(i); m_CapturedObjectMeshOwners[i] = NO_OBJECT; } for (OBJECT_ID obj_id = O_FIRST; obj_id < O_NUMBER_OF; obj_id++) { const OBJECT *const obj = Object_Get(obj_id); if (!obj->loaded || obj->mesh_count <= 0 || obj->mesh_idx < 0) { continue; } for (int32_t mesh_idx = 0; mesh_idx < obj->mesh_count; mesh_idx++) { const int32_t abs_idx = obj->mesh_idx + mesh_idx; if (abs_idx >= 0 && abs_idx < m_CapturedObjectMeshCount) { m_CapturedObjectMeshOwners[abs_idx] = obj_id; } } } } static void M_RestoreObjectMeshesState(void) { if (m_CapturedObjectMeshes == nullptr || m_CapturedObjectMeshCount <= 0) { return; } const int32_t mesh_count = Object_GetMeshCount(); if (mesh_count != m_CapturedObjectMeshCount) { return; } for (int32_t i = 0; i < mesh_count; i++) { if (Object_GetMesh(i) == m_CapturedObjectMeshes[i]) { continue; } int32_t j = -1; for (int32_t k = i + 1; k < mesh_count; k++) { if (Object_GetMesh(k) == m_CapturedObjectMeshes[i]) { j = k; break; } } if (j < 0) { continue; } if (m_CapturedObjectMeshOwners[i] == NO_OBJECT || m_CapturedObjectMeshOwners[j] == NO_OBJECT) { continue; } const OBJECT *const obj_1 = Object_Get(m_CapturedObjectMeshOwners[i]); const OBJECT *const obj_2 = Object_Get(m_CapturedObjectMeshOwners[j]); Object_SwapMeshEx( m_CapturedObjectMeshOwners[i], m_CapturedObjectMeshOwners[j], i - obj_1->mesh_idx, j - obj_2->mesh_idx); } } static void M_RestoreLaraCutsceneState(void) { if (!m_LaraCutsceneState.is_valid) { return; } Lara_Skin_SetType(m_LaraCutsceneState.skin_type); Lara_Skin_SetGunEquipment(LM_HAND_L, m_LaraCutsceneState.hand_l_type); Lara_Skin_SetGunEquipment(LM_HAND_R, m_LaraCutsceneState.hand_r_type); Lara_Skin_SetGunEquipment(LM_THIGH_L, m_LaraCutsceneState.thigh_l_type); Lara_Skin_SetGunEquipment(LM_THIGH_R, m_LaraCutsceneState.thigh_r_type); Lara_Skin_SetHolstersVisible(m_LaraCutsceneState.holsters_visible); } static bool M_IsCutsceneActor(const ITEM *const item) { return (item->object_id >= O_PLAYER_1 && item->object_id <= O_PLAYER_10) || item->object_id == O_LARA; } static void M_ResetActorAnimation(ITEM *const item) { Item_SwitchToAnim(item, 0, 0); item->prev_frame_num = item->frame_num; item->current_anim_state = Item_GetAnim(item)->current_anim_state; item->goal_anim_state = item->current_anim_state; item->required_anim_state = 0; } static void M_ResetActorsToStart(void) { M_RestoreObjectMeshesState(); M_RestoreLaraCutsceneState(); for (int32_t i = 0; i < Item_GetTotalCount(); i++) { ITEM *const item = Item_Get(i); if (M_IsCutsceneActor(item)) { M_ResetActorAnimation(item); } } } static void M_ControlGun(void) { LARA_INFO *const lara = Lara_GetLaraInfo(); const ITEM *const lara_item = Lara_GetItem(); m_DrawLeftGunFlash = false; m_DrawRightGunFlash = false; if (lara->left_arm.flash_gun > 0) { m_DrawLeftGunFlash = true; lara->left_arm.flash_gun--; } if (lara->right_arm.flash_gun > 0) { m_DrawRightGunFlash = true; lara->right_arm.flash_gun--; } Gun_Smoke_Control(); if (g_Config.visuals.enable_gun_lighting && (m_DrawLeftGunFlash || m_DrawRightGunFlash)) { XYZ_32 pos = { .x = -12, .y = 48, .z = 40 }; LARA_MESH mesh = LM_HAND_L; if (m_DrawRightGunFlash) { pos.x = 8; mesh = LM_HAND_R; } Collide_GetJointAbsPosition(lara_item, &pos, mesh); pos.x += (Random_GetControl() & 0xFF) - 128; pos.y -= (Random_GetControl() & 0x7F) - 63; pos.z += (Random_GetControl() & 0xFF) - 128; if (g_TRVersion >= 3) { const RGB_888 color = { .r = 192 + (Random_GetControl() & 0x3F), .g = 144 + (Random_GetControl() & 0x1F), .b = Random_GetControl() & 0x3F, }; Output_AddDynamicLightRGB(pos, 10, color); } else { Output_AddDynamicLight(pos, 10, 11); } } } static void M_DrawGunFlash(const LARA_MESH hand_mesh) { const LARA_INFO *const lara = Lara_GetLaraInfo(); XYZ_32 pos = {}; const bool has_mesh_pos = Lara_GetMeshPos(hand_mesh, &pos); if (!has_mesh_pos) { const ITEM *const lara_item = Lara_GetItem(); Collide_GetJointAbsPosition(lara_item, &pos, hand_mesh); } Matrix_Push(); *g_MatrixPtr = g_ViewMatrix; *g_WMatrixPtr = g_IDMatrix; Matrix_TranslateAbs32(pos); if (has_mesh_pos && lara->mesh_pos_matrices_valid) { MATRIX hand_rot = lara->mesh_pos_matrices[hand_mesh]; hand_rot._03 = 0; hand_rot._13 = 0; hand_rot._23 = 0; Matrix_Mul3x3(&hand_rot); } Gun_DrawFlash(LGT_PISTOLS, CLIP_FULLY_VISIBLE, false); Matrix_Pop(); } static void M_Control(void) { Output_ResetDynamicLights(); Camera_UpdateCutscene(); M_ControlGun(); Item_Control(); Effect_Control(); Sparks_Control(); FX_Control(); Output_AnimateTextures(1); Lara_Hair_Control(true); } static void M_ReplayActors( CINE_DATA *const cine_data, const int32_t start_frame, const int32_t end_frame) { for (int32_t frame_idx = start_frame; frame_idx < end_frame; frame_idx++) { Lua_FireEventInt32(LUA_EVENT_BEFORE_CONTROL, 0); cine_data->frame_idx = frame_idx; M_Control(); Lua_FireEventInt32(LUA_EVENT_AFTER_CONTROL, 0); } } static void M_PlayerControl(const int16_t item_num) { ITEM *const item = Item_Get(item_num); CAMERA_INFO *const camera = Cutscene_GetCamera(); item->rot.y = camera->target_angle; item->pos = camera->pos.pos; XYZ_32 pos = {}; Collide_GetJointAbsPosition(item, &pos, 0); int16_t room_num = Room_GetIndexFromPos(pos); if (room_num != NO_ROOM) { Item_UpdateRoom(item_num, room_num); } int16_t floor_room_num = item->room_num; const SECTOR *const sector = Room_GetSector(pos, &floor_room_num); const int32_t height = Room_GetHeight(sector, pos); item->floor = height == NO_HEIGHT ? pos.y : height; Lara_Animate(item); } static void M_InitialisePlayer(const int16_t item_num) { OBJECT *const obj = Object_Get(O_LARA); obj->draw_func = Lara_Draw; obj->control_func = M_PlayerControl; obj->shadow_size = (UNIT_SHADOW * 10) / 16; Item_AddActive(item_num); ITEM *const item = Item_Get(item_num); CAMERA_INFO *const camera = Cutscene_GetCamera(); Camera_GetCineData()->position.target_angle = item->rot.y; g_Camera.pos.room_num = item->room_num; g_Camera.target_angle = item->rot.y; CINE_DATA *const cine_data = Camera_GetCineData(); cine_data->position.pos = item->pos; camera->pos.pos = item->pos; if (item->room_num != NO_ROOM) { camera->pos.room_num = item->room_num; } camera->target_angle = item->rot.y; item->rot.y = 0; item->dynamic_light = false; Item_SwitchToAnim(item, 0, 0); item->goal_anim_state = 0; item->current_anim_state = 0; LARA_INFO *const lara = Lara_GetLaraInfo(); lara->hit_direction = DIR_UNKNOWN; } static void M_Skip(const int32_t frames) { CINE_DATA *const cine_data = Camera_GetCineData(); const int32_t source_frame = cine_data->frame_idx; int32_t target_frame = source_frame + frames; CLAMP(target_frame, 0, cine_data->frame_count - 1); if (target_frame == source_frame) { return; } if (target_frame > source_frame) { M_ReplayActors(cine_data, source_frame, target_frame); } else { Lua_ReloadLevelScript(); M_ResetActorsToStart(); M_ReplayActors(cine_data, 0, target_frame); } cine_data->frame_idx = target_frame; Camera_UpdateCutscene(); } bool Cutscene_Start(const int32_t level_num) { const GF_LEVEL *const level = GF_GetLevel(GFLT_CUTSCENES, level_num); ASSERT(GF_GetCurrentLevel() == level); m_DrawLeftGunFlash = false; m_DrawRightGunFlash = false; M_InitialisePlayer(Item_GetIndex(Lara_GetItem())); M_CaptureLaraCutsceneState(); M_CaptureObjectMeshesState(); Camera_GetCineData()->frame_idx = 0; if (level->music_track != MX_INACTIVE) { Music_Play_Direct(level->music_track, MPM_ONCE); } return true; } void Cutscene_End(void) { m_DrawLeftGunFlash = false; m_DrawRightGunFlash = false; Memory_FreePointer(&m_CapturedObjectMeshes); Memory_FreePointer(&m_CapturedObjectMeshOwners); m_CapturedObjectMeshCount = 0; Music_Stop(); } GF_COMMAND Cutscene_Control(void) { Interpolation_Remember(); Music_SyncTimestamp(Camera_GetCineData()->frame_idx / (double)LOGIC_FPS); Input_Update(); Shell_ProcessInput(); if (g_InputDB.menu_confirm || g_InputDB.menu_back) { return (GF_COMMAND) { .action = GF_LEVEL_COMPLETE }; } else if (g_InputDB.pause) { const GF_COMMAND gf_cmd = GF_PauseGame(); if (gf_cmd.action != GF_NOOP) { return gf_cmd; } } else if (g_InputDB.toggle_photo_mode) { const GF_COMMAND gf_cmd = GF_EnterPhotoMode(); if (gf_cmd.action != GF_NOOP) { return gf_cmd; } } else if (g_InputDB.menu_right || g_InputDB.menu_left) { const int32_t dir = g_InputDB.menu_right ? 1 : -1; const int32_t speed = g_Input.draw ? 15 : (g_Input.slow ? 1 : 5); M_Skip(dir * LOGIC_FPS * speed); } M_Control(); CINE_DATA *const cine_data = Camera_GetCineData(); cine_data->frame_idx++; if (cine_data->frame_idx >= cine_data->frame_count) { // Remember the scene after the update to prevent the interpolation // from twitching the camera back and forth. Interpolation_Remember(); return (GF_COMMAND) { .action = GF_LEVEL_COMPLETE }; } return (GF_COMMAND) { .action = GF_NOOP }; } void Cutscene_Draw(void) { Interpolation_Interpolate(); Camera_Apply(); Room_DrawAllRooms(g_Camera.interp.room_num, g_Camera.target.room_num); if (m_DrawLeftGunFlash) { M_DrawGunFlash(LM_HAND_L); } if (m_DrawRightGunFlash) { M_DrawGunFlash(LM_HAND_R); } SceneCompositor_Flush(); if (g_Config.visuals.enable_reflections) { Output_Textures_UpdateEnvironmentMap(); } } CAMERA_INFO *Cutscene_GetCamera(void) { return &m_LocalCamera; } ================================================ FILE: src/trx/game/cutscene.h ================================================ #pragma once #include #include bool Cutscene_Start(int32_t level_num); void Cutscene_End(void); GF_COMMAND Cutscene_Control(void); void Cutscene_Draw(void); CAMERA_INFO *Cutscene_GetCamera(void); ================================================ FILE: src/trx/game/demo.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define L_MODIFY_CONFIG() \ X_PROCESS_CONFIG(gameplay.disable_healing_between_levels, false); \ X_PROCESS_CONFIG(gameplay.enable_target_change, false); \ X_PROCESS_CONFIG(gameplay.enable_tr2_jumping, g_TRVersion >= 2); \ X_PROCESS_CONFIG(gameplay.enable_tr2_swim_cancel, g_TRVersion >= 2); \ X_PROCESS_CONFIG(gameplay.enable_tr2_swimming, g_TRVersion >= 2); \ X_PROCESS_CONFIG(gameplay.enable_wading, g_TRVersion >= 2); \ X_PROCESS_CONFIG(gameplay.enable_walk_to_items, false); \ X_PROCESS_CONFIG(gameplay.fix_bear_ai, false); \ X_PROCESS_CONFIG(gameplay.harpoon_recoil, 4); \ X_PROCESS_CONFIG( \ gameplay.look_mode, \ g_TRVersion >= 2 ? LOOK_MODE_ENHANCED : LOOK_MODE_RESTRICTED); \ X_PROCESS_CONFIG(gameplay.start_lara_hitpoints, LARA_MAX_HITPOINTS); \ X_PROCESS_CONFIG(gameplay.target_mode, TARGET_LOCK_MODE_FULL); \ X_PROCESS_CONFIG( \ gameplay.wall_glitch_mode, \ g_TRVersion >= 2 ? WALL_GLITCH_TR2 : WALL_GLITCH_TR1); \ X_PROCESS_CONFIG(input.quick_guns_mode, QUICK_GUNS_MODE_DRAW_ONLY); \ X_PROCESS_CONFIG(visuals.enable_fire_lighting, false); \ X_PROCESS_CONFIG(debug.enable_invulnerability, false); typedef struct { const uint32_t *demo_ptr; const GF_LEVEL *level; struct { CONFIG config; GAME_BONUS_FLAG bonus_flag; } old_config; uint32_t *data; } M_PRIV; static int32_t m_LastDemoNum = 0; static M_PRIV m_Priv; static void M_PrepareConfig(M_PRIV *const p) { // Changing certains settings affects negatively the original game demo // data, so temporarily turn off all relevant enhancements. p->old_config.config = g_Config; p->old_config.bonus_flag = Game_GetBonusFlag(); Game_SetBonusFlag(GBF_NONE); #define X_PROCESS_CONFIG(var, value) g_Config.var = value; L_MODIFY_CONFIG(); #undef X_PROCESS_CONFIG } static void M_RestoreConfig(M_PRIV *const p) { Game_SetBonusFlag(p->old_config.bonus_flag); #define X_PROCESS_CONFIG(var, value) g_Config.var = p->old_config.config.var; L_MODIFY_CONFIG(); #undef X_PROCESS_CONFIG } void Demo_LoadData(VFILE *const file, const size_t size) { M_PRIV *const p = &m_Priv; if (size == 0) { p->data = nullptr; } else { p->data = GameBuf_Alloc((size + 1) * sizeof(uint32_t), GBUF_DEMO_BUFFER); p->data[size] = -1; VFile_Read(file, p->data, size); } } bool Demo_UpdateInput(void) { M_PRIV *const p = &m_Priv; const INPUT_STATE old_demo_input = g_Input; union { uint32_t any; struct { // clang-format off uint32_t forward: 1; uint32_t back: 1; uint32_t left: 1; uint32_t right: 1; uint32_t jump: 1; uint32_t draw: 1; uint32_t action: 1; uint32_t slow: 1; uint32_t option: 1; uint32_t look: 1; uint32_t step_left: 1; uint32_t step_right: 1; uint32_t roll: 1; uint32_t _pad: 6; uint32_t use_flare: 1; uint32_t menu_confirm: 1; uint32_t menu_back: 1; uint32_t save: 1; uint32_t load: 1; // clang-format on }; } demo_input = { .any = *p->demo_ptr }; if ((int32_t)demo_input.any == -1) { return false; } // Translate demo inputs (that use hardcoded OG key layout) to TRX inputs. g_Input = (INPUT_STATE) { // clang-format off .forward = demo_input.forward, .back = demo_input.back, .left = demo_input.left, .right = demo_input.right, .jump = demo_input.jump, .draw = demo_input.draw, .action = demo_input.action, .slow = demo_input.slow, .option = demo_input.option, .look = demo_input.look, .step_left = demo_input.step_left, .step_right = demo_input.step_right, .roll = demo_input.roll, .use_flare = demo_input.use_flare, .menu_confirm = demo_input.menu_confirm, .menu_back = demo_input.menu_back, .save = demo_input.save, .load = demo_input.load, // clang-format on }; g_InputDB = g_Input; for (int32_t i = 0; i < INPUT_STATE_ANY_WORDS; i++) { g_InputDB.any[i] &= ~old_demo_input.any[i]; } p->demo_ptr++; return true; } bool Demo_Start(const int32_t level_num) { M_PRIV *const p = &m_Priv; p->level = GF_GetLevel(GFLT_DEMOS, level_num); ASSERT(p->level != nullptr); ASSERT(GF_GetCurrentLevel() == p->level); M_PrepareConfig(p); Interpolation_Remember(); // Remember old inputs in case the demo was forcefully started with some // keys pressed. In that case, it should only be stopped if the user // presses some other key. Input_Update(); if (p->data == nullptr) { LOG_ERROR("Level '%s' has no demo data", p->level->path); return false; } if (p->level->music_track != MX_INACTIVE) { Music_Play_Direct(p->level->music_track, MPM_LOOP); } p->demo_ptr = p->data; ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara = Lara_GetLaraInfo(); lara_item->pos.x = *p->demo_ptr++; lara_item->pos.y = *p->demo_ptr++; lara_item->pos.z = *p->demo_ptr++; lara_item->rot.x = *p->demo_ptr++; lara_item->rot.y = *p->demo_ptr++; lara_item->rot.z = *p->demo_ptr++; int16_t room_num = *p->demo_ptr++; Item_UpdateRoom(lara->item_num, room_num); const SECTOR *const sector = Room_GetSector(lara_item->pos, &room_num); lara_item->floor = Room_GetHeight(sector, lara_item->pos); if (g_TRVersion >= 2) { lara->last_gun_type = *p->demo_ptr++; Lara_Cheat_GetStuff(); } else { lara->last_gun_type = LGT_PISTOLS; } if (Gun_IsRifleType(lara->last_gun_type)) { Gun_SetLaraBackMesh(lara->last_gun_type); } else if ( lara->last_gun_type != LGT_UNARMED && lara->last_gun_type != LGT_FLARE) { Gun_SetLaraHolsterLMesh(lara->last_gun_type); Gun_SetLaraHolsterRMesh(lara->last_gun_type); } Camera_Initialise(); Random_SeedDraw(0xD371F947); Random_SeedControl(0xD371F947); g_OverlayFlag = 1; Overlay_SetBottomText((OVERLAY_TEXT) { .kind = UI_OVERLAY_TEXT_GS_KEY, .gs_key = GS_ID("general/misc/demo_mode"), .flash_enabled = true, }); return true; } void Demo_End(void) { M_PRIV *const p = &m_Priv; M_RestoreConfig(p); Overlay_SetBottomText((OVERLAY_TEXT) { 0 }); Music_Stop(); } void Demo_Pause(void) { M_PRIV *const p = &m_Priv; M_RestoreConfig(p); Overlay_SetBottomText((OVERLAY_TEXT) { 0 }); } void Demo_Unpause(void) { M_PRIV *const p = &m_Priv; M_PrepareConfig(p); Overlay_SetBottomText((OVERLAY_TEXT) { .kind = UI_OVERLAY_TEXT_GS_KEY, .gs_key = GS_ID("general/misc/demo_mode"), .flash_enabled = true, }); } int32_t Demo_ChooseLevel(const int32_t demo_num) { M_PRIV *const p = &m_Priv; const int32_t demo_count = GF_GetLevelTable(GFLT_DEMOS)->count; if (demo_count <= 0) { return -1; } else if (demo_num < 0 || demo_num >= demo_count) { return (m_LastDemoNum++) % demo_count; } else { return demo_num; } } GF_COMMAND Demo_Control(void) { return Game_Control(true); } void Demo_StopFlashing(void) { Overlay_SetBottomText((OVERLAY_TEXT) { .kind = UI_OVERLAY_TEXT_GS_KEY, .gs_key = GS_ID("general/misc/demo_mode"), .flash_enabled = false, }); } ================================================ FILE: src/trx/game/demo.h ================================================ #pragma once #include #include void Demo_LoadData(VFILE *file, size_t size); uint32_t *Demo_GetData(void); bool Demo_Start(int32_t level_num); void Demo_End(void); void Demo_Pause(void); void Demo_Unpause(void); void Demo_StopFlashing(void); bool Demo_UpdateInput(void); GF_COMMAND Demo_Control(void); int32_t Demo_ChooseLevel(int32_t demo_num); ================================================ FILE: src/trx/game/effects/const.h ================================================ #pragma once #define NO_EFFECT (-1) #define MAX_EFFECTS 1000 ================================================ FILE: src/trx/game/effects/draw.c ================================================ #include #include #include #include #include #include #include void Effect_Draw(const int16_t effect_num) { const EFFECT *const effect = Effect_Get(effect_num); const OBJECT *const obj = Object_Get(effect->object_id); if (!obj->loaded) { return; } // TR3 uses BUBBLES1 as a dummy effect carrier and renders the bubble via // an attached spark; keep TR1/TR2 legacy bubble rendering intact. if (g_TRVersion == 3 && effect->object_id == O_BUBBLE_1) { return; } if (effect->object_id == O_GLOW) { Output_DrawSprite( effect->interp.result.pos.x, effect->interp.result.pos.y, effect->interp.result.pos.z, Object_Get(O_GLOW)->mesh_idx, effect->shade, COLOR_RGB_F_WHITE, DRAW_BLEND); return; } if (obj->effect_draw_func != nullptr) { if (obj->effect_draw_func(effect)) { return; } } if (obj->mesh_count < 0) { const RGB_F tint = Object_IsType(effect->object_id, g_WaterSpriteObjects) ? COLOR_RGB_F_WHITE : Output_GetTint(); int16_t shade = effect->shade; if (shade == -1) { Output_CalculateLight(effect->pos, effect->room_num); shade = Output_GetLightAdder(); } Output_DrawSprite( effect->interp.result.pos.x, effect->interp.result.pos.y, effect->interp.result.pos.z, obj->mesh_idx - effect->frame_num, shade, tint, DRAW_BLEND); } else { Matrix_Push(); Matrix_TranslateAbs32(effect->interp.result.pos); Matrix_Rot16(effect->interp.result.rot); if (obj->mesh_count != 0) { Output_CalculateStaticLight(effect->shade); Object_DrawMesh(obj->mesh_idx, -1, false); } else { Output_CalculateLight(effect->interp.result.pos, effect->room_num); Object_DrawMesh(effect->frame_num, -1, false); } Matrix_Pop(); } } ================================================ FILE: src/trx/game/effects/draw.h ================================================ #pragma once #include void Effect_Draw(int16_t effect_num); ================================================ FILE: src/trx/game/effects/manager.c ================================================ #include #include #include #include #include #include static EFFECT *m_Effects = nullptr; static int16_t m_NextEffectFree = NO_EFFECT; static int16_t m_NextEffectActive = NO_EFFECT; static void M_RemoveActive(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); int16_t link_num = m_NextEffectActive; if (link_num == effect_num) { m_NextEffectActive = effect->next_active; return; } while (link_num != NO_EFFECT) { EFFECT *const fx_link = Effect_Get(link_num); if (fx_link->next_active == effect_num) { fx_link->next_active = effect->next_active; return; } link_num = fx_link->next_active; } } static void M_RemoveDrawn(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); ROOM *const room = Room_Get(effect->room_num); int16_t link_num = room->effect_num; if (link_num == effect_num) { room->effect_num = effect->next_free; return; } while (link_num != NO_EFFECT) { EFFECT *const fx_link = Effect_Get(link_num); if (fx_link->next_free == effect_num) { fx_link->next_free = effect->next_free; return; } link_num = fx_link->next_free; } } void Effect_InitialiseArray(void) { m_Effects = GameBuf_Alloc(MAX_EFFECTS * sizeof(EFFECT), GBUF_EFFECTS); m_NextEffectFree = 0; m_NextEffectActive = NO_EFFECT; for (int32_t i = 0; i < MAX_EFFECTS - 1; i++) { EFFECT *const effect = Effect_Get(i); effect->next_free = i + 1; } m_Effects[MAX_EFFECTS - 1].next_free = NO_EFFECT; } void Effect_Control(void) { int16_t effect_num = m_NextEffectActive; while (effect_num != NO_EFFECT) { const EFFECT *const effect = Effect_Get(effect_num); const OBJECT *const obj = Object_Get(effect->object_id); const int16_t next = effect->next_active; if (obj->control_func != nullptr) { obj->control_func(effect_num); } effect_num = next; } } EFFECT *Effect_Get(const int16_t effect_num) { return &m_Effects[effect_num]; } int16_t Effect_GetIndex(const EFFECT *const effect) { return effect - m_Effects; } int16_t Effect_GetInOrderNum(const int16_t effect_num) { int16_t order_num = 0; for (int16_t link_num = Effect_GetActiveNum(); link_num != NO_EFFECT; link_num = Effect_Get(link_num)->next_active) { if (link_num == effect_num) { return order_num; } order_num++; } return NO_EFFECT; } int16_t Effect_GetActiveNum(void) { return m_NextEffectActive; } int16_t Effect_Create(const int16_t room_num) { const int16_t effect_num = m_NextEffectFree; if (effect_num == NO_EFFECT) { return NO_EFFECT; } EFFECT *const effect = Effect_Get(effect_num); m_NextEffectFree = effect->next_free; ROOM *const room = Room_Get(room_num); effect->room_num = room_num; effect->next_free = room->effect_num; room->effect_num = effect_num; effect->next_active = m_NextEffectActive; m_NextEffectActive = effect_num; effect->shade = SHADE_NEUTRAL; return effect_num; } void Effect_Kill(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); Sparks_DetachEffect(effect_num); M_RemoveActive(effect_num); M_RemoveDrawn(effect_num); effect->next_free = m_NextEffectFree; m_NextEffectFree = effect_num; } void Effect_KillAllActive(void) { int16_t effect_num = Effect_GetActiveNum(); while (effect_num != NO_EFFECT) { EFFECT *const effect = Effect_Get(effect_num); const int16_t next_effect_num = effect->next_active; const OBJECT *const obj = Object_Get(effect->object_id); if (obj->control_func != nullptr && (effect->object_id != O_FLAME || effect->counter >= 0)) { Effect_Kill(effect_num); } effect_num = next_effect_num; } } void Effect_UpdateRoom(const int16_t effect_num, const int16_t room_num) { EFFECT *const effect = Effect_Get(effect_num); ROOM *const old_room = Room_Get(effect->room_num); int16_t link_num = old_room->effect_num; if (link_num == effect_num) { old_room->effect_num = effect->next_free; } else { while (link_num != NO_EFFECT) { if (m_Effects[link_num].next_free == effect_num) { m_Effects[link_num].next_free = effect->next_free; break; } link_num = m_Effects[link_num].next_free; } } ROOM *const new_room = Room_Get(room_num); effect->room_num = room_num; effect->next_free = new_room->effect_num; new_room->effect_num = effect_num; } ================================================ FILE: src/trx/game/effects/manager.h ================================================ #pragma once #include void Effect_InitialiseArray(void); void Effect_Control(void); EFFECT *Effect_Get(int16_t effect_num); int16_t Effect_GetIndex(const EFFECT *effect); int16_t Effect_GetInOrderNum(int16_t effect_num); int16_t Effect_GetActiveNum(void); int16_t Effect_Create(int16_t room_num); void Effect_Kill(int16_t effect_num); void Effect_KillAllActive(void); void Effect_UpdateRoom(int16_t effect_num, int16_t room_num); ================================================ FILE: src/trx/game/effects/types.h ================================================ #pragma once #include #include #include typedef struct { XYZ_32 pos; XYZ_16 rot; int16_t room_num; OBJECT_ID object_id; int16_t next_free; int16_t next_active; int16_t speed; int16_t fall_speed; int16_t frame_num; int16_t counter; int16_t shade; int32_t flag1, flag2; struct { struct { XYZ_32 pos; XYZ_16 rot; } result, prev; } interp; } EFFECT; ================================================ FILE: src/trx/game/effects.h ================================================ #pragma once #include #include #include #include ================================================ FILE: src/trx/game/enum.c ================================================ #include #include #include #include #include #include #include #include #include #include #include static __attribute__((constructor)) void M_Init(void) { #define X_INPUT_ROLE(role_name, state_name) \ ENUM_MAP(INPUT_ROLE, role_name, #state_name); #include #undef X_INPUT_ROLE ENUM_MAP(GAME_BUFFER, GBUF_TEXTURE_PAGES, "Texture pages"); ENUM_MAP(GAME_BUFFER, GBUF_PALETTES, "Color palettes"); ENUM_MAP(GAME_BUFFER, GBUF_OBJECT_TEXTURES, "Object textures"); ENUM_MAP(GAME_BUFFER, GBUF_SPRITE_TEXTURES, "Sprite textures"); ENUM_MAP(GAME_BUFFER, GBUF_STATIC_OBJECTS_3D, "Static objects (3D)"); ENUM_MAP(GAME_BUFFER, GBUF_STATIC_OBJECTS_2D, "Static objects (2D)"); ENUM_MAP(GAME_BUFFER, GBUF_MESH_POINTERS, "Mesh pointers"); ENUM_MAP(GAME_BUFFER, GBUF_MESHES, "Meshes"); ENUM_MAP(GAME_BUFFER, GBUF_ANIMS, "Animations"); ENUM_MAP(GAME_BUFFER, GBUF_ANIM_CHANGES, "Animation changes"); ENUM_MAP(GAME_BUFFER, GBUF_ANIM_RANGES, "Animation ranges"); ENUM_MAP(GAME_BUFFER, GBUF_ANIM_COMMANDS, "Animation commands"); ENUM_MAP(GAME_BUFFER, GBUF_ANIM_BONES, "Animation bones"); ENUM_MAP(GAME_BUFFER, GBUF_ANIM_FRAMES, "Animation frames"); ENUM_MAP(GAME_BUFFER, GBUF_ROOMS, "Rooms"); ENUM_MAP(GAME_BUFFER, GBUF_ROOM_MESH, "Room meshes"); ENUM_MAP(GAME_BUFFER, GBUF_ROOM_PORTALS, "Room portals"); ENUM_MAP(GAME_BUFFER, GBUF_ROOM_SECTORS, "Room sectors"); ENUM_MAP(GAME_BUFFER, GBUF_ROOM_LIGHTS, "Room lights"); ENUM_MAP(GAME_BUFFER, GBUF_ROOM_STATIC_MESHES, "Room static meshes"); ENUM_MAP(GAME_BUFFER, GBUF_FLOOR_DATA, "Floor data"); ENUM_MAP(GAME_BUFFER, GBUF_ITEMS, "Items"); ENUM_MAP(GAME_BUFFER, GBUF_ITEM_DATA, "Item data"); ENUM_MAP(GAME_BUFFER, GBUF_EFFECTS, "Effects"); ENUM_MAP(GAME_BUFFER, GBUF_CAMERAS, "Cameras"); ENUM_MAP(GAME_BUFFER, GBUF_SOUND_SOURCES, "Sound sources"); ENUM_MAP(GAME_BUFFER, GBUF_BOXES, "Boxes"); ENUM_MAP(GAME_BUFFER, GBUF_OVERLAPS, "Overlaps"); ENUM_MAP(GAME_BUFFER, GBUF_GROUND_ZONE, "Ground zones"); ENUM_MAP(GAME_BUFFER, GBUF_FLY_ZONE, "Fly zones"); ENUM_MAP( GAME_BUFFER, GBUF_ANIMATED_TEXTURE_RANGES, "Animated texture ranges"); ENUM_MAP(GAME_BUFFER, GBUF_CINEMATIC_FRAMES, "Cinematic frames"); ENUM_MAP(GAME_BUFFER, GBUF_DEMO_BUFFER, "Demo buffer"); ENUM_MAP(GAME_BUFFER, GBUF_CREATURE_DATA, "Creature data"); ENUM_MAP(GAME_BUFFER, GBUF_CREATURE_LOT, "Creature pathfinding"); ENUM_MAP(GAME_BUFFER, GBUF_SAMPLE_INFOS, "Sample information"); ENUM_MAP(GAME_BUFFER, GBUF_SAMPLES, "Samples"); ENUM_MAP(GAME_BUFFER, GBUF_WALKABLES, "Walkables buffer"); ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_LOOP_GAME, "loop_game"); ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_PLAY_FMV, "play_fmv"); ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_PLAY_CUTSCENE, "play_cutscene"); ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_PLAY_MUSIC, "play_music"); ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_LOADING_SCREEN, "loading_screen"); ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_DISPLAY_PICTURE, "display_picture"); ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_LEVEL_STATS, "level_stats"); ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_TOTAL_STATS, "total_stats"); ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_GLOBE_SELECT, "globe_select"); ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_EXIT_TO_TITLE, "exit_to_title"); ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_LEVEL_COMPLETE, "level_complete"); ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_ADD_ITEM, "give_item"); ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_REMOVE_WEAPONS, "remove_weapons"); ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_REMOVE_AMMO, "remove_ammo"); ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_REMOVE_MEDIPACKS, "remove_medipacks"); ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_REMOVE_FLARES, "remove_flares"); ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_DISABLE_FLOOR, "disable_floor"); ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_SETUP_BACON_LARA, "setup_bacon_lara"); ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_REMOVE_SCIONS, "remove_scions"); ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_ENABLE_SUNSET, "enable_sunset"); ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_SET_START_ANIM, "set_lara_start_anim"); ENUM_MAP( GF_SEQUENCE_EVENT_TYPE, GFS_ADD_SECRET_REWARD, "add_secret_reward"); ENUM_MAP(GF_LEVEL_TYPE, GFL_TITLE, "title"); ENUM_MAP(GF_LEVEL_TYPE, GFL_NORMAL, "normal"); ENUM_MAP(GF_LEVEL_TYPE, GFL_CUTSCENE, "cutscene"); ENUM_MAP(GF_LEVEL_TYPE, GFL_GYM, "gym"); ENUM_MAP(GF_LEVEL_TYPE, GFL_BONUS, "bonus"); ENUM_MAP(GF_LEVEL_TYPE, GFL_DUMMY, "dummy"); ENUM_MAP(GF_LEVEL_TYPE, GFL_CURRENT, "current"); ENUM_MAP(WEATHER_TYPE, WEATHER_NONE, "none"); ENUM_MAP(WEATHER_TYPE, WEATHER_RAIN, "rain"); ENUM_MAP(WEATHER_TYPE, WEATHER_SNOW, "snow"); ENUM_MAP(GF_DEATH_TILE, GF_DEATH_TILE_LAVA, "lava"); ENUM_MAP(GF_DEATH_TILE, GF_DEATH_TILE_RAPIDS, "rapids"); ENUM_MAP(GF_DEATH_TILE, GF_DEATH_TILE_ELECTRIC, "electric"); ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_UNARMED); ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_PISTOLS); ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_MAGNUMS); ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_UZIS); ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_SHOTGUN); ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_M16); ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_MP5); ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_GRENADE); ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_HARPOON); ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_FLARE); ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_SKIDOO); ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_AUTOS); ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_DESERT_EAGLE); ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_ROCKET); ENUM_MAP_SELF(WEAPON_TYPE, WEAPON_TYPE_DUAL_PISTOLS); ENUM_MAP_SELF(WEAPON_TYPE, WEAPON_TYPE_SINGLE_PISTOL); ENUM_MAP_SELF(WEAPON_TYPE, WEAPON_TYPE_RIFLE); ENUM_MAP_SELF(WEAPON_TYPE, WEAPON_TYPE_MOUNTED); ENUM_MAP(UI_BAR_TYPE, UI_BAR_LARA_HP, "lara_hp"); ENUM_MAP(UI_BAR_TYPE, UI_BAR_LARA_HP_POISON, "lara_hp_poison"); ENUM_MAP(UI_BAR_TYPE, UI_BAR_LARA_AIR, "lara_air"); ENUM_MAP(UI_BAR_TYPE, UI_BAR_LARA_STAMINA, "lara_stamina"); ENUM_MAP(UI_BAR_TYPE, UI_BAR_LARA_EXPOSURE, "lara_exposure"); ENUM_MAP(UI_BAR_TYPE, UI_BAR_ENEMY_HP, "enemy_hp"); ENUM_MAP(UI_BAR_TYPE, UI_BAR_ALLY_HP, "ally_hp"); ENUM_MAP(UI_BAR_TYPE, UI_BAR_PROGRESS, "progress"); ENUM_MAP_SELF(LARA_SKIN_BRAID_MODE, BRAID_MODE_NONE); ENUM_MAP_SELF(LARA_SKIN_BRAID_MODE, BRAID_MODE_TR1_HEAD_ONLY); ENUM_MAP_SELF(LARA_SKIN_BRAID_MODE, BRAID_MODE_TR1_FULL); ENUM_MAP_SELF(LARA_SKIN_BRAID_MODE, BRAID_MODE_TR1_MAULED); ENUM_MAP_SELF(LARA_SKIN_BRAID_MODE, BRAID_MODE_TR1_GOLD); ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_TR1_BRAID_DEFAULT_HEAD); ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_TR1_BRAID_COMBAT_HEAD); ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_TR1_BRAID_DEFAULT_TORSO); ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_TR1_BRAID_MAULED_TORSO); ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_TR1_BRAID_GOLD_HEAD); ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_TR1_BRAID_GOLD_TORSO); ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_DAGGER_HAND); ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_DAGGER_HIPS); ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_OAR); ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_SPANNER); ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_DRINK_CAN); ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_GLASSES_OPAQUE); ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_GLASSES_TRANSPARENT); ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_BREATH); ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_TREX_KILL); ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_SCION_PICKUP_1); ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_USE_MIDAS); ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_MIDAS_KILL); ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_SCION_PICKUP_2); ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_TORSO_KILL); ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_PLUNGER); ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_START_ANIM); ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_AIRLOCK); ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_SHARK_KILL); ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_YETI_KILL); ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_GONG_BONG); ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_GUARD_KILL); ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_PULL_DAGGER); ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_START_HOUSE); ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_END_HOUSE); ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_SHIVA_KILL); ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_RAPIDS_DROWN); ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_TRAIN_KILL); ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_JAIL_WAKE_UP); ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_WILLARD_KILL); } ================================================ FILE: src/trx/game/events.c ================================================ #include #include #include static EVENT_MANAGER *m_GameEventManager = nullptr; void GameEvent_Init(void) { m_GameEventManager = EventManager_Create(); } void GameEvent_Shutdown(void) { EventManager_Free(m_GameEventManager); m_GameEventManager = nullptr; } int32_t GameEvent_Subscribe( const char *const event_name, const void *const sender, const GAME_EVENT_LISTENER listener, void *const user_data) { ASSERT(m_GameEventManager != nullptr); return EventManager_Subscribe( m_GameEventManager, event_name, sender, listener, user_data); } void GameEvent_Unsubscribe(const int32_t listener_id) { if (m_GameEventManager != nullptr) { EventManager_Unsubscribe(m_GameEventManager, listener_id); } } void GameEvent_Fire(const EVENT event) { if (m_GameEventManager != nullptr) { EventManager_Fire(m_GameEventManager, &event); } } ================================================ FILE: src/trx/game/events.h ================================================ #pragma once #include // Game-level event names #define GAME_EVENT_SCREENSHOT "screenshot" #define GAME_EVENT_COMMAND "console_command" typedef void (*GAME_EVENT_LISTENER)(const EVENT *event, void *user_data); void GameEvent_Init(void); void GameEvent_Shutdown(void); int32_t GameEvent_Subscribe( const char *event_name, const void *sender, GAME_EVENT_LISTENER listener, void *user_data); void GameEvent_Unsubscribe(int32_t listener_id); void GameEvent_Fire(const EVENT event); ================================================ FILE: src/trx/game/fader.c ================================================ #include #include #include #include #include static void M_Init(FADER *const fader, FADER_ARGS args) { CLAMP(args.initial, 0.0f, 1.0f); CLAMP(args.target, 0.0f, 1.0f); if (args.from_current) { args.initial = Fader_GetCurrentValue(fader); // Reduce duration proportionally to how close the initial value is to // the target. float ratio = ABS(args.target - args.initial); CLAMP(ratio, 0.0f, 1.0f); args.duration *= ratio; if (ratio < 1.0f) { args.debuff = 0.0f; } } fader->args = args; ClockTimer_Sync(&fader->timer); } void Fader_InitTo( FADER *const fader, const float initial, const float target, const float duration) { M_Init( fader, (FADER_ARGS) { .from_current = false, .initial = initial, .target = target, .duration = duration, .debuff = 0.0f, }); } void Fader_InitToHold( FADER *const fader, const float initial, const float target, const float duration, const float debuff) { M_Init( fader, (FADER_ARGS) { .from_current = false, .initial = initial, .target = target, .duration = duration, .debuff = debuff, }); } void Fader_InitFromCurrent( FADER *const fader, const float target, const float duration) { M_Init( fader, (FADER_ARGS) { .from_current = true, .initial = 0.0f, .target = target, .duration = duration, .debuff = 0.0f, }); } void Fader_InitFromCurrentHold( FADER *const fader, const float target, const float duration, const float debuff) { M_Init( fader, (FADER_ARGS) { .from_current = true, .initial = 0.0f, .target = target, .duration = duration, .debuff = debuff, }); } float Fader_GetCurrentValue(const FADER *const fader) { if (!g_Config.visuals.enable_fade_effects || fader->args.duration <= 0.0) { return fader->args.target; } const float elapsed_time = ClockTimer_PeekElapsed(&fader->timer); const float target_time = fader->args.duration; float ratio = elapsed_time / target_time; CLAMP(ratio, 0.0, 1.0); float value = fader->args.initial + (fader->args.target - fader->args.initial) * (float)ratio; CLAMP(value, 0.0f, 1.0f); return value; } bool Fader_IsActive(const FADER *const fader) { if (!g_Config.visuals.enable_fade_effects || fader->args.duration <= 0.0) { return false; } const float elapsed_time = ClockTimer_PeekElapsed(&fader->timer); const float target_time = fader->args.duration + fader->args.debuff; return elapsed_time < target_time; } ================================================ FILE: src/trx/game/fader.h ================================================ #pragma once #include #include typedef struct { bool from_current; float initial; float target; // This value controls how much to keep the last frame after the animation // is done (1.0 = one second). float debuff; float duration; } FADER_ARGS; typedef struct { FADER_ARGS args; CLOCK_TIMER timer; } FADER; void Fader_InitTo(FADER *fader, float initial, float target, float duration); void Fader_InitToHold( FADER *fader, float initial, float target, float duration, float debuff); void Fader_InitFromCurrent(FADER *fader, float target, float duration); void Fader_InitFromCurrentHold( FADER *fader, float target, float duration, float debuff); bool Fader_IsActive(const FADER *fader); float Fader_GetCurrentValue(const FADER *fader); ================================================ FILE: src/trx/game/fmv.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static bool m_IsPlaying = false; static const char *const m_FallbackExts[] = { ".mp4", ".mpeg", ".webm", ".avi", ".fmv", ".rpl", nullptr, }; #define M_FADE_TIME 0.4f #define M_PAUSE_OVERLAY_OPACITY 0.8f typedef struct { OUTPUT_QUAD_SURFACE_DESC desc; uint8_t *buffer; } M_SURFACE; typedef struct { OUTPUT_QUAD *renderer_2d; bool show_pause_overlay; FADER pause_fader; } M_RENDER_CONTEXT; static OUTPUT_QUAD_SURFACE_DESC M_MakeSurfaceDesc( const int32_t width, const int32_t height) { return (OUTPUT_QUAD_SURFACE_DESC) { .width = width, .height = height, .bit_count = 32, .tex_format = GL_BGRA, .tex_type = GL_UNSIGNED_INT_8_8_8_8_REV, .uv = { { .u = 0.0f, .v = 0.0f }, { .u = 1.0f, .v = 0.0f }, { .u = 1.0f, .v = 1.0f }, { .u = 0.0f, .v = 1.0f }, }, .pitch = width * 4, }; } static int32_t M_OpenAudioStream(const char *const file_name) { int32_t audio_id = Audio_Stream_CreateFromFile(file_name); if (audio_id != AUDIO_NO_SOUND) { return audio_id; } // The video file may lack an audio stream (e.g. remastered .ogv). // Try other FMV extensions to find a file that contains audio. const char *const dot = strrchr(file_name, '.'); if (dot == nullptr) { return AUDIO_NO_SOUND; } const size_t base_len = (size_t)(dot - file_name); for (const char *const *ext = m_FallbackExts; *ext != nullptr; ext++) { char *const candidate = String_Format("%.*s%s", (int)base_len, file_name, *ext); if (File_Exists(candidate)) { audio_id = Audio_Stream_CreateFromFile(candidate); Memory_Free(candidate); if (audio_id != AUDIO_NO_SOUND) { return audio_id; } } else { Memory_Free(candidate); } } return AUDIO_NO_SOUND; } static void *M_AllocateSurface( const int32_t width, const int32_t height, void *const user_data) { M_SURFACE *const surface = Memory_Alloc(sizeof(M_SURFACE)); surface->desc = M_MakeSurfaceDesc(width, height); surface->buffer = Memory_Alloc(surface->desc.pitch * surface->desc.height); return surface; } static void M_DeallocateSurface(void *const surface, void *const user_data) { M_SURFACE *const surface_ = surface; Memory_Free(surface_->buffer); Memory_Free(surface_); } static void M_ClearSurface(void *const surface, void *const user_data) { ASSERT(surface != nullptr); M_SURFACE *const surface_ = surface; memset(surface_->buffer, 0, surface_->desc.pitch * surface_->desc.height); } static void M_RenderBegin(void *const surface, void *const user_data) { Output_BeginScene(); } static void M_DrawUI(void) { UI_BeginScene(); Overlay_Draw(); Console_Draw(); Console_Control(); Console_Control(); UI_EndScene(); UI_Draw(); } static float M_GetPauseOverlayOpacity(const M_RENDER_CONTEXT *const ctx) { if (!g_Config.ui.pause_fade_effects) { return ctx->show_pause_overlay ? M_PAUSE_OVERLAY_OPACITY : 0.0f; } return Fader_GetCurrentValue(&ctx->pause_fader) * M_PAUSE_OVERLAY_OPACITY; } static bool M_ShouldShowPauseText(const M_RENDER_CONTEXT *const ctx) { if (!ctx->show_pause_overlay) { return false; } if (!g_Config.ui.pause_fade_effects) { return true; } return !Fader_IsActive(&ctx->pause_fader) && Fader_GetCurrentValue(&ctx->pause_fader) >= 1.0f; } static void M_RenderEnd(void *const surface, void *const user_data) { Output_EndScene(); Output_FlipScreen(); } static void *M_LockSurface(void *const surface, void *const user_data) { ASSERT(surface != nullptr); M_SURFACE *const surface_ = surface; return surface_->buffer; } static void M_UnlockSurface(void *const surface, void *const user_data) { } static void M_UploadSurface(void *const surface, void *const user_data) { M_RENDER_CONTEXT *const ctx = user_data; M_SURFACE *const surface_ = surface; const float overlay_opacity = M_GetPauseOverlayOpacity(ctx); Output_Quad_Upload(ctx->renderer_2d, &surface_->desc, surface_->buffer); Output_SwitchViewport(VIEWPORT_GAME); Output_Quad_Render(ctx->renderer_2d); if (overlay_opacity > 0.0f) { Output_Overlay_DrawBlackRectangle(overlay_opacity, false); } Output_SwitchViewport(VIEWPORT_UI); M_DrawUI(); } static void M_SetPauseText(const bool show) { if (show) { Overlay_SetBottomText((OVERLAY_TEXT) { .kind = UI_OVERLAY_TEXT_GS_KEY, .gs_key = GS_ID("general/pause/paused"), }); } else { Overlay_SetBottomText((OVERLAY_TEXT) {}); } } static void M_RedrawFrame(M_RENDER_CONTEXT *const ctx) { const float overlay_opacity = M_GetPauseOverlayOpacity(ctx); Output_BeginScene(); Output_SwitchViewport(VIEWPORT_GAME); Output_Quad_Render(ctx->renderer_2d); if (overlay_opacity > 0.0f) { Output_Overlay_DrawBlackRectangle(overlay_opacity, false); } Output_SwitchViewport(VIEWPORT_UI); M_DrawUI(); Output_EndScene(); Output_FlipScreen(); } static bool M_Play(const char *const file_name) { if (file_name == nullptr || String_IsEmpty(file_name)) { LOG_ERROR("Cannot play FMV: empty file path"); return false; } VIDEO *const video = Video_Open(file_name); if (video == nullptr) { return false; } M_RENDER_CONTEXT render_ctx = { .renderer_2d = Output_Quad_Create(), }; Video_SetSurfaceAllocatorFunc(video, M_AllocateSurface, nullptr); Video_SetSurfaceDeallocatorFunc(video, M_DeallocateSurface, nullptr); Video_SetSurfaceClearFunc(video, M_ClearSurface, nullptr); Video_SetRenderBeginFunc(video, M_RenderBegin, nullptr); Video_SetRenderEndFunc(video, M_RenderEnd, nullptr); Video_SetSurfaceLockFunc(video, M_LockSurface, nullptr); Video_SetSurfaceUnlockFunc(video, M_UnlockSurface, nullptr); Video_SetSurfaceUploadFunc(video, M_UploadSurface, &render_ctx); Video_SetAudioEnabled(video, false); const int32_t audio_id = M_OpenAudioStream(file_name); bool input_paused = false; bool paused = false; g_OldInputDB = g_Input; Fader_InitTo(&render_ctx.pause_fader, 0.0f, 0.0f, 0.0f); M_SetPauseText(false); Video_Start(video); while (video->is_playing) { Shell_ProcessEvents(); const bool focus_paused = g_Config.gameplay.pause_on_focus_lost && !Shell_IsFocused(); Input_Update(); Shell_ProcessInput(); render_ctx.show_pause_overlay = input_paused; M_SetPauseText(M_ShouldShowPauseText(&render_ctx)); Overlay_Control(); const bool should_pause = focus_paused || input_paused; if (should_pause != paused) { Video_SetPaused(video, should_pause); Audio_Stream_SetPaused(audio_id, should_pause); paused = should_pause; } const float volume = Audio_IsMuted() ? 0.0f : g_Config.audio.master_volume * g_Config.audio.fmv_volume; Audio_Stream_SetVolume(audio_id, volume); const double audio_ts = Audio_Stream_GetTimestamp(audio_id); if (audio_ts >= 0.0) { Video_SetExternalAudioClock(video, audio_ts); } Video_SetSurfaceSize( video, Viewport_GetWidth(VIEWPORT_GAME), Viewport_GetHeight(VIEWPORT_GAME)); Video_SetSurfacePixelFormat(video, AV_PIX_FMT_BGRA); Video_PumpEvents(video); if (paused) { M_RedrawFrame(&render_ctx); } if ((g_InputDB.pause || (input_paused && g_InputDB.menu_back)) && !focus_paused) { input_paused = !input_paused; if (g_Config.ui.pause_fade_effects) { Fader_InitFromCurrent( &render_ctx.pause_fader, input_paused ? 1.0f : 0.0f, M_FADE_TIME); } } else if ( (!paused && (g_InputDB.menu_back || g_InputDB.menu_confirm)) || GF_GetOverrideCommand().action != GF_NOOP || Shell_IsExiting()) { Video_Stop(video); break; } } M_SetPauseText(false); Audio_Stream_Close(audio_id); Video_Close(video); Output_Quad_Destroy(render_ctx.renderer_2d); Output_ApplyRenderSettings(); return true; } bool FMV_Play(const char *const file_path) { Music_Stop(); Sound_StopAll(); if (!g_Config.gameplay.enable_fmv) { return false; } m_IsPlaying = true; const bool result = M_Play(file_path); m_IsPlaying = false; return result; } bool FMV_IsPlaying(void) { return m_IsPlaying; } ================================================ FILE: src/trx/game/fmv.h ================================================ #pragma once bool FMV_Play(const char *file_path); bool FMV_IsPlaying(void); ================================================ FILE: src/trx/game/fx/common.c ================================================ #include #include #include #include #include #include #include #include #include void FX_Control(void) { FX_Ring_Control(); FX_Wake_Control(); FX_Water_Control(); FX_Weather_Control(); FX_WaterParticles_Control(); FX_Footprint_Control(); FX_GunFlash_Control(); FX_Laser_Control(); } void FX_Draw(void) { FX_Ring_Draw(); FX_Water_Draw(); FX_Weather_Draw(); FX_WaterParticles_Draw(); FX_GunFlash_Draw(); FX_Laser_Draw(); FX_Footprint_Draw(); } void FX_Reset(void) { FX_Water_Reset(); FX_Weather_Reset(); FX_WaterParticles_Reset(); FX_Footprint_Reset(); FX_Wake_Reset(); FX_Ring_Reset(); } ================================================ FILE: src/trx/game/fx/common.h ================================================ #pragma once void FX_Reset(void); void FX_Control(void); void FX_Draw(void); ================================================ FILE: src/trx/game/fx/explosion_ring.c ================================================ #include #include #include #include #include #include #include #include #include #include #define M_MAX_RINGS 6 static FX_RING m_Rings[FX_RING_TYPE_NUMBER_OF][M_MAX_RINGS] = {}; static bool m_Active[FX_RING_TYPE_NUMBER_OF] = {}; static void M_RotateZX( XYZ_32 *const out, const XYZ_32 in, const int32_t rot_z, const int32_t rot_x) { const int32_t sz = Math_Sin(rot_z); const int32_t cz = Math_Cos(rot_z); const int32_t sx = Math_Sin(rot_x); const int32_t cx = Math_Cos(rot_x); const int32_t xz = (in.x * cz - in.y * sz) >> W2V_SHIFT; const int32_t yz = (in.x * sz + in.y * cz) >> W2V_SHIFT; const int32_t zz = in.z; out->x = xz; out->y = (yz * cx - zz * sx) >> W2V_SHIFT; out->z = (yz * sx + zz * cx) >> W2V_SHIFT; } static void M_RememberRing(FX_RING *const ring) { ring->prev_radius = ring->radius; ring->prev_rot = ring->rot; ring->prev_pos = ring->pos; } static void M_InterpolateRing(const FX_RING *const ring, FX_RING *const out) { *out = *ring; const double ratio = Interpolation_GetWorldRate(); out->radius = (int16_t)LERP(ring->prev_radius, ring->radius, ratio); out->rot.x = Math_AngleMean(ring->prev_rot.x, ring->rot.x, ratio); out->rot.z = Math_AngleMean(ring->prev_rot.z, ring->rot.z, ratio); out->pos.x = (int32_t)LERP(ring->prev_pos.x, ring->pos.x, ratio); out->pos.y = (int32_t)LERP(ring->prev_pos.y, ring->pos.y, ratio); out->pos.z = (int32_t)LERP(ring->prev_pos.z, ring->pos.z, ratio); } static void M_BuildRingCircle( FX_RING *const ring, const int32_t radius, const int32_t band, const bool clear_inner, const int32_t angle_base) { int32_t angle = angle_base; for (int32_t i = 0; i < 8; i++) { FX_EXPLOSION_VERT *const vtx = &ring->verts[band * 8 + i]; vtx->pos.x = (radius * Math_Sin(angle << 4)) >> W2V_SHIFT; vtx->pos.z = (radius * Math_Cos(angle << 4)) >> W2V_SHIFT; if (clear_inner && band != 0) { vtx->color = COLOR_RGB_888_BLACK; } angle = (angle + 512) & 0xFFF; } } static void M_DrawTexturedRing(const FX_RING *const ring) { const int32_t time4 = Output_GetTimeInGame() * 4; const int32_t sprite_base = Object_Get(O_EXPLOSION_1)->mesh_idx; const int32_t sprite_idx = sprite_base + 4 + ((time4 >> 4) & 3); const int32_t rot_z = ring->rot.z << 4; const int32_t rot_x = ring->rot.x << 4; for (int32_t j = 0; j < 8; j++) { const int32_t j2 = (j == 7) ? 0 : (j + 1); const FX_EXPLOSION_VERT *const o0 = &ring->verts[j]; const FX_EXPLOSION_VERT *const o1 = &ring->verts[j2]; const FX_EXPLOSION_VERT *const i0 = &ring->verts[8 + j]; const FX_EXPLOSION_VERT *const i1 = &ring->verts[8 + j2]; if ((o0->color.r | o0->color.g | o0->color.b | o1->color.r | o1->color.g | o1->color.b | i0->color.r | i0->color.g | i0->color.b | i1->color.r | i1->color.g | i1->color.b) == 0U) { continue; } XYZ_32 p_local[4] = { { o0->pos.x, 0, o0->pos.z }, { o1->pos.x, 0, o1->pos.z }, { i1->pos.x, 0, i1->pos.z }, { i0->pos.x, 0, i0->pos.z }, }; XYZ_32 p_rot[4] = {}; XYZ_32 p_world[4] = {}; for (int32_t c = 0; c < 4; c++) { M_RotateZX(&p_rot[c], p_local[c], rot_z, rot_x); p_world[c].x = ring->pos.x + p_rot[c].x; p_world[c].y = ring->pos.y + p_rot[c].y; p_world[c].z = ring->pos.z + p_rot[c].z; } const RGBA_8888 color[4] = { { o0->color.r, o0->color.g, o0->color.b, 255 }, { o1->color.r, o1->color.g, o1->color.b, 255 }, { i1->color.r, i1->color.g, i1->color.b, 255 }, { i0->color.r, i0->color.g, i0->color.b, 255 }, }; OutputSource_PolyFX_StageSpriteQuadWorld( sprite_idx, p_world, color, DRAW_BLEND_ADD); } } static void M_DrawFlatRing(const FX_RING *const ring) { const int32_t rot_z = ring->rot.z << 4; const int32_t rot_x = ring->rot.x << 4; for (int32_t j = 0; j < 8; j++) { const int32_t j2 = (j == 7) ? 0 : (j + 1); const FX_EXPLOSION_VERT *const o0 = &ring->verts[j]; const FX_EXPLOSION_VERT *const o1 = &ring->verts[j2]; const FX_EXPLOSION_VERT *const i0 = &ring->verts[8 + j]; const FX_EXPLOSION_VERT *const i1 = &ring->verts[8 + j2]; if ((o0->color.r | o0->color.g | o0->color.b | o1->color.r | o1->color.g | o1->color.b | i0->color.r | i0->color.g | i0->color.b | i1->color.r | i1->color.g | i1->color.b) == 0U) { continue; } XYZ_32 p_local[4] = { { o0->pos.x, 0, o0->pos.z }, { o1->pos.x, 0, o1->pos.z }, { i1->pos.x, 0, i1->pos.z }, { i0->pos.x, 0, i0->pos.z }, }; XYZ_32 p_rot[4] = {}; XYZ_32 p_world[4] = {}; for (int32_t c = 0; c < 4; c++) { M_RotateZX(&p_rot[c], p_local[c], rot_z, rot_x); p_world[c].x = ring->pos.x + p_rot[c].x; p_world[c].y = ring->pos.y + p_rot[c].y; p_world[c].z = ring->pos.z + p_rot[c].z; } const XYZ_32 world_pos[4] = { p_world[0], p_world[1], p_world[2], p_world[3], }; const RGBA_8888 color[4] = { { o0->color.r, o0->color.g, o0->color.b, 0xC0 }, { o1->color.r, o1->color.g, o1->color.b, 0xC0 }, { i1->color.r, i1->color.g, i1->color.b, 0xC0 }, { i0->color.r, i0->color.g, i0->color.b, 0xC0 }, }; OutputSource_PolyFX_StageQuadExt( -1, world_pos, nullptr, color, VERT_FLAT_SHADED | VERT_NO_LIGHTING | VERT_NO_WIBBLE, DRAW_BLEND_ADD); } } static void M_ControlExplosionRings(void) { bool any_active = false; for (int32_t i = 0; i < M_MAX_RINGS; i++) { FX_RING *const ring = &m_Rings[FX_RING_TYPE_BLAST][i]; if (ring->on == 0) { continue; } M_RememberRing(ring); ring->life--; if (ring->life == 0) { ring->on = 0; continue; } ring->radius += ring->speed; any_active = true; } if (!any_active) { m_Active[FX_RING_TYPE_BLAST] = false; } } static void M_ControlSummonRings(void) { bool any_active = false; for (int32_t i = 0; i < M_MAX_RINGS; i++) { FX_RING *const ring = &m_Rings[FX_RING_TYPE_SUMMON][i]; if (ring->on == 0) { continue; } M_RememberRing(ring); ring->life--; ring->radius -= ring->speed; if (ring->life == 0 || ring->radius <= 0) { ring->on = 0; continue; } ring->speed += 2; any_active = true; } if (!any_active) { m_Active[FX_RING_TYPE_SUMMON] = false; } } static void M_ControlKnockBackRings(void) { bool any_active = false; for (int32_t i = 0; i < M_MAX_RINGS; i++) { FX_RING *const ring = &m_Rings[FX_RING_TYPE_KNOCKBACK][i]; if (ring->on == 0) { continue; } M_RememberRing(ring); ring->life--; if (ring->life == 0) { ring->on = 0; continue; } ring->radius += ring->speed; if (ring->speed < 0) { ring->speed--; } else { ring->speed += i == 1 ? 3 : 2; } any_active = true; } if (!any_active) { m_Active[FX_RING_TYPE_KNOCKBACK] = false; } } static void M_DrawExplosionRings(const int32_t angle_base) { for (int32_t i = 0; i < M_MAX_RINGS; i++) { FX_RING draw_ring = {}; FX_RING *const ring = &m_Rings[FX_RING_TYPE_BLAST][i]; if (ring->on == 0) { continue; } M_InterpolateRing(ring, &draw_ring); int32_t rad = draw_ring.radius; for (int32_t band = 0; band < 2; band++) { M_BuildRingCircle(&draw_ring, rad, band, false, angle_base); for (int32_t k = 0; k < 8; k++) { FX_EXPLOSION_VERT *const vtx = &draw_ring.verts[band * 8 + k]; int32_t r = 0; int32_t g = 0; int32_t b = 0; if (draw_ring.on == 2) { // Tony r = (Random_GetDraw() & 0x1F) + 224; g = (r >> 2) + (Random_GetDraw() & 0x3F); b = Random_GetDraw() & 0x3F; } else if (draw_ring.on == 3) { // Sophia r = Random_GetDraw() & 0x3F; g = (Random_GetDraw() & 0x1F) + 224; b = (g >> 2) + (Random_GetDraw() & 0x3F); } else if (draw_ring.on == 4) { // Puna r = Random_GetDraw() & 0x1F; b = (Random_GetDraw() & 0x3F) + 224; g = (b >> 2) + (Random_GetDraw() & 0x3F); } else { // Willard r = Random_GetDraw() & 0x3F; g = (Random_GetDraw() & 0x1F) + 224; b = (g >> 1) + (Random_GetDraw() & 0x3F); } vtx->color = (RGB_888) { (r * ring->life) >> 5, (g * ring->life) >> 5, (b * ring->life) >> 5, }; } rad >>= 1; } M_DrawTexturedRing(&draw_ring); } } static void M_DrawSummonRings(const int32_t angle_base) { for (int32_t i = 0; i < M_MAX_RINGS; i++) { FX_RING draw_ring = {}; FX_RING *const ring = &m_Rings[FX_RING_TYPE_SUMMON][i]; if (ring->on == 0) { continue; } M_InterpolateRing(ring, &draw_ring); const int32_t fade = ring->life > 32 ? (64 - ring->life) << 1 : ring->life < 8 ? ring->life << 3 : 64; int32_t rad = draw_ring.radius; for (int32_t band = 0; band < 2; band++) { M_BuildRingCircle(&draw_ring, rad, band, true, angle_base); if (band == 0) { for (int32_t k = 0; k < 8; k++) { FX_EXPLOSION_VERT *const vtx = &draw_ring.verts[k]; const int32_t g = (Random_GetDraw() & 0x1F) + 224; const int32_t b = (g >> 2) + (Random_GetDraw() & 0x3F); const int32_t r = Random_GetDraw() & 0x3F; vtx->color = (RGB_888) { (r * fade) >> 7, (g * fade) >> 7, (b * fade) >> 7, }; } } rad >>= 1; } M_DrawFlatRing(&draw_ring); } } static void M_DrawKnockBackRings(const int32_t angle_base) { for (int32_t i = 0; i < M_MAX_RINGS; i++) { FX_RING draw_ring = {}; FX_RING *const ring = &m_Rings[FX_RING_TYPE_KNOCKBACK][i]; if (ring->on == 0) { continue; } M_InterpolateRing(ring, &draw_ring); int32_t fade = ring->life > 24 ? (32 - ring->life) << 2 : ring->life < 16 ? ring->life << 1 : 32; int32_t rad = draw_ring.radius; for (int32_t band = 0; band < 2; band++) { M_BuildRingCircle(&draw_ring, rad, band, false, angle_base); for (int32_t k = 0; k < 8; k++) { FX_EXPLOSION_VERT *const vtx = &draw_ring.verts[band * 8 + k]; const int32_t g = (Random_GetDraw() & 0x1F) + 224; const int32_t b = (g >> 2) + (Random_GetDraw() & 0x3F); const int32_t r = Random_GetDraw() & 0x3F; vtx->color = (RGB_888) { (r * fade) >> 5, (g * fade) >> 5, (b * fade) >> 5, }; } rad >>= 1; fade >>= 1; } M_DrawTexturedRing(&draw_ring); } } void FX_Ring_Reset(void) { memset(m_Rings, 0, sizeof(m_Rings)); memset(m_Active, 0, sizeof(m_Active)); } void FX_Ring_Sync(FX_RING *const ring) { if (ring == nullptr) { return; } M_RememberRing(ring); } FX_RING *FX_Ring_GetRing(const FX_RING_TYPE type, const int32_t idx) { if (idx < 0 || idx >= M_MAX_RINGS) { return nullptr; } m_Active[type] = true; return &m_Rings[type][idx]; } FX_RING *FX_Ring_PeekRing(const FX_RING_TYPE type, const int32_t idx) { if (idx < 0 || idx >= M_MAX_RINGS) { return nullptr; } return &m_Rings[type][idx]; } void FX_Ring_Control(void) { if (m_Active[FX_RING_TYPE_BLAST]) { M_ControlExplosionRings(); } if (m_Active[FX_RING_TYPE_SUMMON]) { M_ControlSummonRings(); } if (m_Active[FX_RING_TYPE_KNOCKBACK]) { M_ControlKnockBackRings(); } } void FX_Ring_SpawnKnockBack(const XYZ_32 pos) { for (int32_t i = 0; i < M_MAX_RINGS; i++) { FX_RING *const ring = &m_Rings[FX_RING_TYPE_KNOCKBACK][i]; ring->on = 1; ring->life = 32; ring->speed = ((i == 1) + 1) << 4; ring->pos.x = pos.x; ring->pos.y = pos.y - 512 + (i << 7); ring->pos.z = pos.z; ring->rot.x = 0; ring->rot.z = 0; ring->radius = ((i == 1) + 2) << 8; FX_Ring_Sync(ring); } m_Active[FX_RING_TYPE_KNOCKBACK] = true; } void FX_Ring_BounceKnockBack(void) { for (int32_t i = 0; i < M_MAX_RINGS; i++) { FX_RING *const ring = &m_Rings[FX_RING_TYPE_KNOCKBACK][i]; ring->speed = (int16_t)((-ring->speed) >> 2); } } bool FX_Ring_IsRingActive(const FX_RING_TYPE type) { return m_Active[type]; } void FX_Ring_Draw(void) { const int32_t time4 = Output_GetTimeInGame() * 4; const int32_t angle_base = (time4 & 0x3F) << 3; M_DrawExplosionRings(angle_base); M_DrawSummonRings(angle_base); M_DrawKnockBackRings(angle_base); } ================================================ FILE: src/trx/game/fx/explosion_ring.h ================================================ #pragma once #include #include #include typedef struct { XZ_16 pos; RGB_888 color; } FX_EXPLOSION_VERT; typedef struct { int16_t on; int16_t life; int16_t speed; int16_t radius; int16_t prev_radius; XZ_16 rot; XZ_16 prev_rot; XYZ_32 pos; XYZ_32 prev_pos; FX_EXPLOSION_VERT verts[16]; } FX_RING; typedef enum { FX_RING_TYPE_BLAST, FX_RING_TYPE_SUMMON, FX_RING_TYPE_KNOCKBACK, FX_RING_TYPE_NUMBER_OF, } FX_RING_TYPE; void FX_Ring_Reset(void); void FX_Ring_Control(void); void FX_Ring_Draw(void); void FX_Ring_SpawnKnockBack(XYZ_32 pos); void FX_Ring_BounceKnockBack(void); void FX_Ring_Sync(FX_RING *ring); bool FX_Ring_IsRingActive(FX_RING_TYPE type); FX_RING *FX_Ring_GetRing(FX_RING_TYPE type, int32_t idx); FX_RING *FX_Ring_PeekRing(FX_RING_TYPE type, int32_t idx); ================================================ FILE: src/trx/game/fx/footprint.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_MAX_FOOTPRINTS 32 #define M_FOOTPRINT_LIFETIME 512 #define M_FOOTPRINT_Z_DEPTH_ADJUST -0.5f typedef struct { int32_t x; int32_t y; int32_t z; int16_t room_num; int16_t y_rot; int16_t active; } M_FOOTPRINT; typedef struct { M_FOOTPRINT prints[M_MAX_FOOTPRINTS]; int32_t next_idx; } M_PRIV; static M_PRIV m_Priv; static const SAMPLE_TRX_ID m_StepSounds[14] = { SFX_FOOTSTEPS_MUD, SFX_FOOTSTEPS_SNOW, SFX_FOOTSTEPS_SAND_OR_GRASS, SFX_FOOTSTEPS_GRAVEL, SFX_FOOTSTEPS_ICE, SFX_TRX_INVALID, SFX_TRX_INVALID, SFX_FOOTSTEPS_WOOD, SFX_FOOTSTEPS_METAL, SFX_TRX_INVALID, SFX_FOOTSTEPS_SAND_OR_GRASS, SFX_TRX_INVALID, SFX_FOOTSTEPS_WOOD, SFX_FOOTSTEPS_METAL, }; void FX_Footprint_Reset(void) { M_PRIV *const p = &m_Priv; memset(p, 0, sizeof(*p)); } void FX_Footprint_Add(const ITEM *const lara_item, const bool is_left_foot) { M_PRIV *const p = &m_Priv; if (lara_item == nullptr) { return; } XYZ_32 pos = {}; Collide_GetJointAbsPosition( lara_item, &pos, is_left_foot ? LM_FOOT_L : LM_FOOT_R); int16_t room_num = lara_item->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); if (sector == nullptr) { return; } if (sector->fx < ARRAY_SIZE(m_StepSounds) && m_StepSounds[sector->fx] != SFX_TRX_INVALID) { Sound_Effect(m_StepSounds[sector->fx], &lara_item->pos, SPM_NORMAL); } if (sector->fx > 4) { return; } const int32_t y = Room_GetHeight(sector, pos); if (y == NO_HEIGHT || Room_IsOnWalkable(sector, pos, y, NO_ITEM)) { return; } M_FOOTPRINT *const print = &m_Priv.prints[m_Priv.next_idx]; print->x = pos.x; print->y = y; print->z = pos.z; print->room_num = room_num; print->y_rot = lara_item->rot.y; print->active = M_FOOTPRINT_LIFETIME; p->next_idx = (p->next_idx + 1) % M_MAX_FOOTPRINTS; } static void M_GetWorldPoint( const M_FOOTPRINT *const print, const XYZ_32 local, XYZ_32 *const out_world) { const int32_t s = Math_Sin(print->y_rot); const int32_t c = Math_Cos(print->y_rot); const int32_t dx = TRIGMULT2(local.x, c) + TRIGMULT2(local.z, s); const int32_t dz = TRIGMULT2(local.z, c) - TRIGMULT2(local.x, s); out_world->x = print->x + dx; out_world->y = print->y + local.y; out_world->z = print->z + dz; } static int32_t M_GetVertexYOffset( const M_FOOTPRINT *const print, const XYZ_32 world_pos) { int16_t room_num = print->room_num; const XYZ_32 pos = { world_pos.x, print->y, world_pos.z }; const SECTOR *const sector = Room_GetSector(pos, &room_num); if (sector == nullptr) { return 0; } const int32_t height = Room_GetHeight(sector, pos); if (height == NO_HEIGHT) { return 0; } int32_t dy = height - print->y; if (ABS(dy) > 128) { dy = 0; } return dy; } void FX_Footprint_Control(void) { if (!g_Config.visuals.enable_footprints) { return; } M_PRIV *const p = &m_Priv; for (int32_t i = 0; i < M_MAX_FOOTPRINTS; i++) { M_FOOTPRINT *const print = &p->prints[i]; if (print->active != 0) { print->active--; } } } void FX_Footprint_Draw(void) { if (!g_Config.visuals.enable_footprints) { return; } const M_PRIV *const p = &m_Priv; const OBJECT *const obj = Object_Get(O_EXPLOSION_1); if (obj == nullptr || !obj->loaded) { return; } const int32_t sprite_idx = obj->mesh_idx + 17; if (sprite_idx < 0 || sprite_idx >= Output_GetSpriteTextureCount()) { return; } const XYZ_32 corners[3] = { { .x = 0, .y = 0, .z = -64 }, { .x = -128, .y = 0, .z = 64 }, { .x = 128, .y = 0, .z = 64 }, }; for (int32_t i = 0; i < M_MAX_FOOTPRINTS; i++) { const M_FOOTPRINT *const print = &p->prints[i]; if (print->active == 0) { continue; } XYZ_32 world[3] = {}; for (int32_t j = 0; j < 3; j++) { M_GetWorldPoint(print, corners[j], &world[j]); world[j].y += M_GetVertexYOffset(print, world[j]); } int32_t c = print->active < 29 ? (print->active << 2) : 112; CLAMP(c, 0, 255); const RGBA_8888 color = { c, c, c, 255 }; const RGBA_8888 tri_color[3] = { color, color, color }; for (int32_t j = 0; j < 4; j++) { OutputSource_PolyFX_StageSpriteTriWorldDepth( sprite_idx, world, tri_color, M_FOOTPRINT_Z_DEPTH_ADJUST, DRAW_BLEND_SUB); } } } ================================================ FILE: src/trx/game/fx/footprint.h ================================================ #pragma once #include void FX_Footprint_Reset(void); void FX_Footprint_Add(const ITEM *lara_item, bool is_left_foot); void FX_Footprint_Control(void); void FX_Footprint_Draw(void); ================================================ FILE: src/trx/game/fx/gun_flash.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #define M_MAX_FLASHES 32 #define M_LIFETIME 3 #define M_AXIS_UNIT 1024 typedef struct { bool active; int16_t owner_item_num; int16_t room_num; int16_t lifetime; XZ_16 rot; BITE bite; OBJECT_ID flash_object_id; XYZ_32 light_pos; } M_GUN_FLASH; typedef struct { M_GUN_FLASH flashes[M_MAX_FLASHES]; int32_t next_idx; } M_PRIV; static M_PRIV m_Priv = {}; static int16_t M_GetRandomRoll(void) { const int16_t rnd = (int16_t)Random_GetControl(); return (int16_t)((rnd << 14) + (rnd >> 2) - 4096); } static bool M_GetOwnerItem( const M_GUN_FLASH *const flash, ITEM **const out_item) { if (flash->owner_item_num < 0 || flash->owner_item_num >= Item_GetTotalCount()) { return false; } *out_item = Item_Get(flash->owner_item_num); return *out_item != nullptr; } static void M_GetJointPose( const ITEM *const item, const BITE bite, XYZ_32 *const out_pos, MATRIX *const out_rot) { XYZ_32 pos = bite.pos; Collide_GetJointAbsPosition(item, &pos, bite.mesh_num); XYZ_32 axis_x = bite.pos; axis_x.x += M_AXIS_UNIT; Collide_GetJointAbsPosition(item, &axis_x, bite.mesh_num); XYZ_32 axis_y = bite.pos; axis_y.y += M_AXIS_UNIT; Collide_GetJointAbsPosition(item, &axis_y, bite.mesh_num); XYZ_32 axis_z = bite.pos; axis_z.z += M_AXIS_UNIT; Collide_GetJointAbsPosition(item, &axis_z, bite.mesh_num); *out_pos = pos; *out_rot = g_IDMatrix; out_rot->_00 = ((int64_t)(axis_x.x - pos.x) << W2V_SHIFT) / M_AXIS_UNIT; out_rot->_10 = ((int64_t)(axis_x.y - pos.y) << W2V_SHIFT) / M_AXIS_UNIT; out_rot->_20 = ((int64_t)(axis_x.z - pos.z) << W2V_SHIFT) / M_AXIS_UNIT; out_rot->_01 = ((int64_t)(axis_y.x - pos.x) << W2V_SHIFT) / M_AXIS_UNIT; out_rot->_11 = ((int64_t)(axis_y.y - pos.y) << W2V_SHIFT) / M_AXIS_UNIT; out_rot->_21 = ((int64_t)(axis_y.z - pos.z) << W2V_SHIFT) / M_AXIS_UNIT; out_rot->_02 = ((int64_t)(axis_z.x - pos.x) << W2V_SHIFT) / M_AXIS_UNIT; out_rot->_12 = ((int64_t)(axis_z.y - pos.y) << W2V_SHIFT) / M_AXIS_UNIT; out_rot->_22 = ((int64_t)(axis_z.z - pos.z) << W2V_SHIFT) / M_AXIS_UNIT; out_rot->_03 = 0; out_rot->_13 = 0; out_rot->_23 = 0; } bool FX_GunFlash_Spawn( const ITEM *const owner_item, const CREATURE_GUN *const gun) { if (owner_item == nullptr || gun == nullptr || !gun->tr3_enemy_flash) { return false; } M_GUN_FLASH *const flash = &m_Priv.flashes[m_Priv.next_idx]; flash->active = true; flash->owner_item_num = Item_GetIndex(owner_item); flash->room_num = owner_item->room_num; flash->lifetime = M_LIFETIME; flash->rot = (XZ_16) { .x = gun->tr3_flash_rot_x, .z = M_GetRandomRoll() }; flash->bite = gun->tr3_flash; flash->flash_object_id = (gun->tr3_enemy_weapon_flags & 1) != 0 ? O_M16_FLASH : O_GUN_FLASH; flash->light_pos = owner_item->pos; m_Priv.next_idx = (m_Priv.next_idx + 1) % M_MAX_FLASHES; return true; } void FX_GunFlash_Control(void) { for (int32_t i = 0; i < M_MAX_FLASHES; i++) { M_GUN_FLASH *const flash = &m_Priv.flashes[i]; if (!flash->active) { continue; } ITEM *owner_item = nullptr; if (!M_GetOwnerItem(flash, &owner_item)) { flash->active = false; continue; } flash->room_num = owner_item->room_num; flash->rot.z = M_GetRandomRoll(); XYZ_32 light_pos = flash->bite.pos; Collide_GetJointAbsPosition( owner_item, &light_pos, flash->bite.mesh_num); flash->light_pos = light_pos; if (g_Config.visuals.enable_gun_lighting) { const int32_t falloff = (flash->lifetime << 1) + 8; if (g_TRVersion >= 3) { Output_AddDynamicLightRGB( flash->light_pos, falloff, (RGB_888) { 192, 128, 32 }); } else { Output_AddDynamicLight(flash->light_pos, falloff, 11); } } flash->lifetime--; if (flash->lifetime <= 0) { flash->active = false; } } } void FX_GunFlash_Draw(void) { const OBJECT *const glow_obj = Object_Get(O_GLOW); for (int32_t i = 0; i < M_MAX_FLASHES; i++) { const M_GUN_FLASH *const flash = &m_Priv.flashes[i]; if (!flash->active) { continue; } ITEM *owner_item = nullptr; if (!M_GetOwnerItem(flash, &owner_item)) { continue; } const OBJECT *const flash_obj = Object_Get(flash->flash_object_id); if (!flash_obj->loaded) { continue; } XYZ_32 flash_pos = {}; MATRIX flash_rot = {}; M_GetJointPose(owner_item, flash->bite, &flash_pos, &flash_rot); if (glow_obj->loaded) { Output_DrawSprite( flash_pos.x, flash_pos.y, flash_pos.z, glow_obj->mesh_idx, SHADE_NEUTRAL, (RGB_F) { 1.0f, 0.89f, 0.13f }, DRAW_BLEND_ADD); } Matrix_Push(); *g_MatrixPtr = g_ViewMatrix; *g_WMatrixPtr = g_IDMatrix; Matrix_TranslateAbs32(flash_pos); Matrix_Mul3x3(&flash_rot); Matrix_RotX(flash->rot.x); Matrix_RotZ(flash->rot.z); Output_CalculateStaticLightRGB_F((RGB_F) { 1.0f, 0.89f, 0.13f }); Object_DrawMesh(flash_obj->mesh_idx, -1, false); Matrix_Pop(); } } ================================================ FILE: src/trx/game/fx/gun_flash.h ================================================ #pragma once #include #include bool FX_GunFlash_Spawn(const ITEM *owner_item, const CREATURE_GUN *gun); void FX_GunFlash_Control(void); void FX_GunFlash_Draw(void); ================================================ FILE: src/trx/game/fx/laser.c ================================================ #include #include #include #include #include #define M_MAX_LASERS 32 #define M_LIFETIME 2 typedef struct { bool active; int16_t owner_item_num; int16_t lifetime; BITE bite; RGBA_8888 color; float width; } M_LASER; typedef struct { M_LASER lasers[M_MAX_LASERS]; int32_t next_idx; } M_PRIV; static M_PRIV m_Priv = {}; static bool M_GetOwnerItem(const M_LASER *const laser, ITEM **const out_item) { if (laser->owner_item_num < 0 || laser->owner_item_num >= Item_GetTotalCount()) { return false; } *out_item = Item_Get(laser->owner_item_num); return *out_item != nullptr; } bool FX_Laser_Spawn(const ITEM *const owner_item, const CREATURE_GUN *const gun) { if (owner_item == nullptr || gun == nullptr || !gun->tr3_enemy_flash) { return false; } M_LASER *const laser = &m_Priv.lasers[m_Priv.next_idx]; laser->active = true; laser->lifetime = M_LIFETIME; laser->owner_item_num = Item_GetIndex(owner_item); laser->bite = gun->tr3_laser.bite; laser->color = gun->tr3_laser.color; laser->width = gun->tr3_laser.width; m_Priv.next_idx = (m_Priv.next_idx + 1) % M_MAX_LASERS; return true; } void FX_Laser_Control(void) { for (int32_t i = 0; i < M_MAX_LASERS; i++) { M_LASER *const laser = &m_Priv.lasers[i]; if (!laser->active) { continue; } ITEM *owner_item = nullptr; if (!M_GetOwnerItem(laser, &owner_item)) { laser->active = false; continue; } laser->lifetime--; if (laser->lifetime <= 0) { laser->active = false; } } } void FX_Laser_Draw(void) { for (int32_t i = 0; i < M_MAX_LASERS; i++) { const M_LASER *const laser = &m_Priv.lasers[i]; if (!laser->active) { continue; } ITEM *owner_item = nullptr; if (!M_GetOwnerItem(laser, &owner_item)) { continue; } GAME_VECTOR start = { .pos = laser->bite.pos, .room_num = owner_item->room_num, }; GAME_VECTOR target = start; target.y *= 32; Collide_GetJointAbsPosition( owner_item, &start.pos, laser->bite.mesh_num); Collide_GetJointAbsPosition( owner_item, &target.pos, laser->bite.mesh_num); LOS_Check(&start, &target, false); const int32_t dist = XYZ_32_GetDistance(start.pos, target.pos); int32_t segment_count = 2 * dist / WALL_L; CLAMP(segment_count, 8, 32); const XYZ_32 delta = { .x = target.x - start.x, .y = target.y - start.y, .z = target.z - start.z, }; for (int32_t j = 0; j < segment_count; j++) { const float seg_start = (float)j / segment_count; const float seg_end = (float)(j + 1) / segment_count; const XYZ_32 p1 = { .x = start.x + delta.x * seg_start, .y = start.y + delta.y * seg_start, .z = start.z + delta.z * seg_start, }; const XYZ_32 p2 = { .x = start.x + delta.x * seg_end, .y = start.y + delta.y * seg_end, .z = start.z + delta.z * seg_end, }; OutputSource_PolyFX_StageLineSegment( p1, laser->color, p2, laser->color, laser->width, DRAW_BLEND_ADD); } } } ================================================ FILE: src/trx/game/fx/laser.h ================================================ #pragma once #include #include bool FX_Laser_Spawn(const ITEM *owner_item, const CREATURE_GUN *gun); void FX_Laser_Control(void); void FX_Laser_Draw(void); ================================================ FILE: src/trx/game/fx/wake.c ================================================ #include #include #include #include #include #include #define M_MAX_POINTS 32 static const XZ_32 m_Offsets[2] = { { .x = -128, 0 }, { .x = 128, .z = 0 } }; static FX_WAKE_POINT m_Points[M_MAX_POINTS][2] = {}; static uint8_t m_Shade = 0; static uint8_t m_StartIndex = 0; static bool m_Active = false; static RGBA_8888 M_GrayFromWakeLife(const int32_t life, const int32_t shift) { int32_t c = (life >> shift) << 3; CLAMPG(c, 255); return (RGBA_8888) { c, c, c, 255 }; } static XYZ_32 M_GetWakeOrigin(const ITEM *const item, const XZ_32 offset) { XYZ_32 pos = item->interp.result.pos; pos = XYZ_32_OffsetYaw(pos, item->interp.result.rot.y, offset.z); pos = XYZ_32_OffsetYaw(pos, item->interp.result.rot.y + DEG_90, offset.x); return pos; } void FX_Wake_Reset(void) { for (int32_t i = 0; i < M_MAX_POINTS; i++) { m_Points[i][0].life = 0; m_Points[i][1].life = 0; } m_Active = false; } void FX_Wake_Control(void) { if (!m_Active) { return; } bool any_active = false; for (int32_t i = 0; i < 2; i++) { for (int32_t j = 0; j < M_MAX_POINTS; j++) { FX_WAKE_POINT *const pt = &m_Points[j][i]; if (pt->life > 0) { pt->prev_pos[0] = pt->pos[0]; pt->prev_pos[1] = pt->pos[1]; pt->life--; pt->pos[0].x += pt->vel[0].x; pt->pos[0].z += pt->vel[0].z; pt->pos[1].x += pt->vel[1].x; pt->pos[1].z += pt->vel[1].z; if (pt->life > 0) { any_active = true; } } } } if (!any_active) { m_Active = false; } } FX_WAKE_POINT *FX_Wake_GetPoint(const int32_t wake_idx, const int32_t side) { return &m_Points[wake_idx][side]; } uint8_t FX_Wake_GetShade(void) { return m_Shade; } void FX_Wake_SetShade(const uint8_t shade) { m_Shade = shade; } uint8_t FX_Wake_GetStartIndex(void) { return m_StartIndex; } void FX_Wake_AdvanceStartIndex(void) { m_StartIndex = (m_StartIndex + 1) & (M_MAX_POINTS - 1); m_Active = true; } void FX_Wake_Draw(const ITEM *const item) { const int64_t max_origin_dist_sq = XYZ_32_GetLength2_64((XYZ_32) { WALL_L / 2, 0, 0 }); const double ratio = Interpolation_GetWorldRate(); const bool do_interp = Interpolation_IsActive() && ratio > 0.0 && ratio < 1.0; for (int32_t side = 0; side < 2; side++) { const XYZ_32 origin = M_GetWakeOrigin(item, m_Offsets[side]); XYZ_32 prev[2] = { origin, origin }; int32_t c12 = 64; if (m_Shade < 16) { c12 = (c12 * m_Shade) >> 4; } int32_t current = (m_StartIndex - 1) & (M_MAX_POINTS - 1); const FX_WAKE_POINT *const first_pt = &m_Points[current][side]; if (first_pt->life != 0) { const XYZ_32 first_pos0 = do_interp ? (XYZ_32) { .x = (int32_t)LERP( first_pt->prev_pos[0].x, first_pt->pos[0].x, ratio), .y = (int32_t)LERP( first_pt->prev_pos[0].y, first_pt->pos[0].y, ratio), .z = (int32_t)LERP( first_pt->prev_pos[0].z, first_pt->pos[0].z, ratio), } : first_pt->pos[0]; const XYZ_32 first_pos1 = do_interp ? (XYZ_32) { .x = (int32_t)LERP( first_pt->prev_pos[1].x, first_pt->pos[1].x, ratio), .y = (int32_t)LERP( first_pt->prev_pos[1].y, first_pt->pos[1].y, ratio), .z = (int32_t)LERP( first_pt->prev_pos[1].z, first_pt->pos[1].z, ratio), } : first_pt->pos[1]; const XYZ_32 delta0 = { .x = origin.x - first_pos0.x, .y = 0, .z = origin.z - first_pos0.z, }; const XYZ_32 delta1 = { .x = origin.x - first_pos1.x, .y = 0, .z = origin.z - first_pos1.z, }; const int64_t dist0_sq = XYZ_32_GetLength2_64(delta0); const int64_t dist1_sq = XYZ_32_GetLength2_64(delta1); if (dist0_sq > max_origin_dist_sq && dist1_sq > max_origin_dist_sq) { // Prevent a long bridge quad when the hull origin drifts away // from the latest wake segment (e.g. moving over dry slopes). prev[0] = first_pos1; prev[1] = first_pos0; } } for (int32_t nw = 0; nw < M_MAX_POINTS; nw++) { const FX_WAKE_POINT *const pt = &m_Points[current][side]; if (pt->life == 0U) { break; } int32_t c34 = pt->life; if (m_Shade < 16) { c34 = (c34 * m_Shade) >> 4; } const RGBA_8888 quad_color[4] = { M_GrayFromWakeLife(c12, 2), M_GrayFromWakeLife(c12, 1), M_GrayFromWakeLife(c34, 1), M_GrayFromWakeLife(c34, 2), }; XYZ_32 curr[2] = { do_interp ? (XYZ_32) { .x = (int32_t)LERP( pt->prev_pos[0].x, pt->pos[0].x, ratio), .y = (int32_t)LERP( pt->prev_pos[0].y, pt->pos[0].y, ratio), .z = (int32_t)LERP( pt->prev_pos[0].z, pt->pos[0].z, ratio), } : pt->pos[0], do_interp ? (XYZ_32) { .x = (int32_t)LERP( pt->prev_pos[1].x, pt->pos[1].x, ratio), .y = (int32_t)LERP( pt->prev_pos[1].y, pt->pos[1].y, ratio), .z = (int32_t)LERP( pt->prev_pos[1].z, pt->pos[1].z, ratio), } : pt->pos[1], }; const XYZ_32 quad_world[4] = { prev[0], prev[1], curr[0], curr[1], }; OutputSource_PolyFX_StageQuadExt( -1, quad_world, nullptr, quad_color, VERT_FLAT_SHADED | VERT_NO_LIGHTING | VERT_NO_WIBBLE, DRAW_BLEND_ADD); prev[0] = curr[1]; prev[1] = curr[0]; c12 = c34; current = (current - 1) & (M_MAX_POINTS - 1); } } } ================================================ FILE: src/trx/game/fx/wake.h ================================================ #pragma once #include typedef struct { XYZ_32 pos[2]; XYZ_32 prev_pos[2]; XZ_32 vel[2]; uint8_t life; } FX_WAKE_POINT; void FX_Wake_Reset(void); void FX_Wake_Control(void); FX_WAKE_POINT *FX_Wake_GetPoint(int32_t wake_idx, int32_t side); uint8_t FX_Wake_GetShade(void); void FX_Wake_SetShade(uint8_t shade); uint8_t FX_Wake_GetStartIndex(void); void FX_Wake_AdvanceStartIndex(void); void FX_Wake_Draw(const ITEM *item); ================================================ FILE: src/trx/game/fx/water.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #define M_SPLASH_Z_DEPTH_ADJUST -0.005f #define M_RIPPLE_Z_DEPTH_ADJUST -0.005f static const int16_t m_SplashRings[8][2] = { { 0, -24 }, { 17, -17 }, { 24, 0 }, { 17, 17 }, { 0, 24 }, { -17, 17 }, { -24, 0 }, { -17, -17 }, }; static const uint8_t m_SplashQuadLinks[32] = { 8, 9, 0, 1, 9, 10, 1, 2, 10, 11, 2, 3, 11, 12, 3, 4, 12, 13, 4, 5, 13, 14, 5, 6, 14, 15, 6, 7, 15, 8, 7, 0, }; static FX_WATER_SPLASH m_Splashes[4]; static FX_WATER_RIPPLE m_Ripples[16]; static int32_t m_SplashCount = 0; static int32_t m_Wibble = 0; void FX_Water_Reset(void) { memset(m_Splashes, 0, sizeof(m_Splashes)); memset(m_Ripples, 0, sizeof(m_Ripples)); m_SplashCount = 0; m_Wibble = 0; } static bool M_IsRoomUnderwater(const int16_t room_num) { return Room_Get(room_num)->flags.underwater; } static void M_RememberRipple(FX_WATER_RIPPLE *const ripple) { ripple->prev_size = ripple->size; ripple->prev_life = ripple->life; ripple->prev_init = ripple->init; } static void M_RememberSplash(FX_WATER_SPLASH *const splash) { splash->prev_life = splash->life; for (int32_t i = 0; i < 48; i++) { FX_WATER_SPLASH_VERT *const v = &splash->v[i]; v->prev_wx = v->wx; v->prev_wy = v->wy; v->prev_wz = v->wz; } } static bool M_GetUnderwaterBloodColor( RGBA_8888 *const color, const int32_t life) { switch (g_Config.visuals.blood_effects) { case BLOOD_EFFECTS_DISABLED: return false; case BLOOD_EFFECTS_PINK: *color = (RGBA_8888) { life / 2, 0, life, 255 }; return true; case BLOOD_EFFECTS_RED: *color = (RGBA_8888) { life, 0, 0, 255 }; return true; case BLOOD_EFFECTS_NUMBER_OF: break; } return false; } FX_WATER_RIPPLE *FX_Water_SetupRipple( const int32_t x, const int32_t y, const int32_t z, int32_t size, const bool is_still) { int32_t idx = -1; for (int32_t i = 0; i < (int32_t)ARRAY_SIZE(m_Ripples); i++) { if ((m_Ripples[i].flags & 1U) == 0U) { idx = i; break; } } if (idx < 0) { return nullptr; } FX_WATER_RIPPLE *const ripple = &m_Ripples[idx]; if (size < 0) { ripple->flags = is_still ? 19U : 3U; size = -size; } else { ripple->flags = 1U; } ripple->init = 1U; ripple->size = (uint8_t)size; ripple->life = (uint8_t)((Random_GetControl() & 0xF) + 48); ripple->x = (Random_GetControl() & 0x7F) + x - 64; ripple->y = y; ripple->z = (Random_GetControl() & 0x7F) + z - 64; M_RememberRipple(ripple); return ripple; } void FX_Water_SetupSplash(const FX_WATER_SPLASH_SETUP *const setup_) { FX_WATER_SPLASH_SETUP setup = *setup_; int32_t idx = -1; for (int32_t i = 0; i < (int32_t)ARRAY_SIZE(m_Splashes); i++) { if ((m_Splashes[i].flags & 1U) == 0U) { idx = i; break; } } if (idx < 0) { Sound_Effect( SFX_LARA_SPLASH, &(XYZ_32) { setup.x, setup.y, setup.z }, SPM_NORMAL); return; } FX_WATER_SPLASH *const splash = &m_Splashes[idx]; splash->flags = 3U; if (setup.outer_friction == -9) { splash->flags = 67U; setup.outer_friction = 9; } splash->x = setup.x; splash->y = setup.y; splash->z = setup.z; splash->life = 63U; FX_WATER_SPLASH_VERT *v = splash->v; for (int32_t i = 0; i < 8; i++) { v->wx = (setup.inner_xz_off * m_SplashRings[i][0]) * 2; v->wy = 0; v->wz = (setup.inner_xz_off * m_SplashRings[i][1]) * 2; v->xv = (setup.inner_xz_vel * m_SplashRings[i][0]) / 12; v->yv = 0; v->zv = (setup.inner_xz_vel * m_SplashRings[i][1]) / 12; v->oxv = v->xv >> 3; v->ozv = v->zv >> 3; v->gravity = 0; v->friction = (uint8_t)(setup.inner_friction - 2); v->prev_wx = v->wx; v->prev_wy = v->wy; v->prev_wz = v->wz; v++; } for (int32_t i = 0; i < 8; i++) { v->wx = ((setup.inner_xz_off + setup.inner_xz_size) * m_SplashRings[i][0]) * 2; v->wy = setup.inner_y_size; v->wz = ((setup.inner_xz_off + setup.inner_xz_size) * m_SplashRings[i][1]) * 2; v->xv = (setup.inner_xz_vel * m_SplashRings[i][0]) >> 3; v->yv = setup.inner_y_vel; v->zv = (setup.inner_xz_vel * m_SplashRings[i][1]) >> 3; v->oxv = v->xv >> 3; v->ozv = v->zv >> 3; v->gravity = (uint8_t)setup.inner_gravity; v->friction = (uint8_t)setup.inner_friction; v->prev_wx = v->wx; v->prev_wy = v->wy; v->prev_wz = v->wz; v++; } for (int32_t i = 0; i < 8; i++) { v->wx = (setup.middle_xz_off * m_SplashRings[i][0]) * 2; v->wy = 0; v->wz = (setup.middle_xz_off * m_SplashRings[i][1]) * 2; v->xv = (setup.middle_xz_vel * m_SplashRings[i][0]) / 12; v->yv = 0; v->zv = (setup.middle_xz_vel * m_SplashRings[i][1]) / 12; v->oxv = v->xv >> 3; v->ozv = v->zv >> 3; v->gravity = 0; v->friction = (uint8_t)(setup.middle_friction - 2); v->prev_wx = v->wx; v->prev_wy = v->wy; v->prev_wz = v->wz; v++; } for (int32_t i = 0; i < 8; i++) { v->wx = ((setup.middle_xz_off + setup.middle_xz_size) * m_SplashRings[i][0]) * 2; v->wy = setup.middle_y_size; v->wz = ((setup.middle_xz_off + setup.middle_xz_size) * m_SplashRings[i][1]) * 2; v->xv = (setup.middle_xz_vel * m_SplashRings[i][0]) >> 3; v->yv = setup.middle_y_vel; v->zv = (setup.middle_xz_vel * m_SplashRings[i][1]) >> 3; v->oxv = v->xv >> 3; v->ozv = v->zv >> 3; v->gravity = (uint8_t)setup.middle_gravity; v->friction = (uint8_t)setup.middle_friction; v->prev_wx = v->wx; v->prev_wy = v->wy; v->prev_wz = v->wz; v++; } for (int32_t i = 0; i < 8; i++) { v->wx = (setup.outer_xz_off * m_SplashRings[i][0]) * 2; v->wy = 0; v->wz = (setup.outer_xz_off * m_SplashRings[i][1]) * 2; v->xv = (setup.outer_xz_vel * m_SplashRings[i][0]) / 12; v->yv = 0; v->zv = (setup.outer_xz_vel * m_SplashRings[i][1]) / 12; v->oxv = v->xv >> 3; v->ozv = v->zv >> 3; v->gravity = 0; v->friction = (uint8_t)(setup.outer_friction - 2); v->prev_wx = v->wx; v->prev_wy = v->wy; v->prev_wz = v->wz; v++; } for (int32_t i = 0; i < 8; i++) { v->wx = ((setup.outer_xz_off + setup.outer_xz_size) * m_SplashRings[i][0]) * 2; v->wy = 0; v->wz = ((setup.outer_xz_off + setup.outer_xz_size) * m_SplashRings[i][1]) * 2; v->xv = (setup.outer_xz_vel * m_SplashRings[i][0]) >> 3; v->yv = 0; v->zv = (setup.outer_xz_vel * m_SplashRings[i][1]) >> 3; v->oxv = v->xv >> 3; v->ozv = v->zv >> 3; v->gravity = 0; v->friction = (uint8_t)setup.outer_friction; v->prev_wx = v->wx; v->prev_wy = v->wy; v->prev_wz = v->wz; v++; } splash->prev_life = splash->life; Sound_Effect( SFX_LARA_SPLASH, &(XYZ_32) { setup.x, setup.y, setup.z }, SPM_NORMAL); } void FX_Water_Splash(const ITEM *const item) { int16_t room_num = item->room_num; Room_GetSector(item->pos, &room_num); if (!M_IsRoomUnderwater(room_num)) { return; } const int32_t water_height = Room_GetWaterHeight(item->pos, room_num); FX_WATER_SPLASH_SETUP setup = { .x = item->pos.x, .y = water_height, .z = item->pos.z, .inner_xz_off = 32, .inner_xz_size = 8, .inner_y_size = -128, .inner_xz_vel = 320, .inner_y_vel = (int16_t)(-40 * item->fall_speed), .inner_gravity = 160, .inner_friction = 7, .middle_xz_off = 48, .middle_xz_size = 32, .middle_y_size = -64, .middle_xz_vel = 480, .middle_y_vel = (int16_t)(-20 * item->fall_speed), .middle_gravity = 96, .middle_friction = 8, .outer_xz_off = 32, .outer_xz_size = 128, .outer_xz_vel = 544, .outer_friction = 9, }; FX_Water_SetupSplash(&setup); } void FX_Water_WadeSplash( const ITEM *const item, const int32_t water_height, const int32_t depth) { int16_t room_num = item->room_num; Room_GetSector(item->pos, &room_num); if (!M_IsRoomUnderwater(room_num)) { return; } const BOUNDS_16 *const bounds = &Item_GetBestFrame(item)->bounds; if (item->pos.y + bounds->min.y > water_height || item->pos.y + bounds->max.y < water_height) { return; } if (item->fall_speed > 0 && depth < 474 && m_SplashCount == 0) { const FX_WATER_SPLASH_SETUP setup = { .x = item->pos.x, .y = water_height, .z = item->pos.z, .inner_xz_off = 16, .inner_xz_size = 12, .inner_y_size = -96, .inner_xz_vel = 160, .inner_y_vel = (int16_t)(-72 * item->fall_speed), .inner_gravity = 128, .inner_friction = 7, .middle_xz_off = 24, .middle_xz_size = 24, .middle_y_size = -64, .middle_xz_vel = 224, .middle_y_vel = (int16_t)(-36 * item->fall_speed), .middle_gravity = 72, .middle_friction = 8, .outer_xz_off = 32, .outer_xz_size = 32, .outer_xz_vel = 272, .outer_friction = 9, }; FX_Water_SetupSplash(&setup); m_SplashCount = 16; } else if ( (m_Wibble & 0xF) == 0 && (((Random_GetControl() & 0xF) == 0) || item->current_anim_state != LS(LS_STOP))) { FX_Water_SetupRipple( item->pos.x, water_height, item->pos.z, -16 - (Random_GetControl() & 0xF), item->current_anim_state == LS(LS_STOP)); } } void FX_Water_TriggerUnderwaterBlood(const XYZ_32 pos, const int32_t size) { int32_t idx = -1; for (int32_t i = 0; i < (int32_t)ARRAY_SIZE(m_Ripples); i++) { if ((m_Ripples[i].flags & 1U) == 0U) { idx = i; break; } } if (idx < 0) { return; } FX_WATER_RIPPLE *const ripple = &m_Ripples[idx]; ripple->flags = 0x33U; ripple->init = 1U; ripple->life = (Random_GetControl() & 7) - 16; ripple->size = size; ripple->x = pos.x + (Random_GetControl() & 0x3F) - 32; ripple->y = pos.y; ripple->z = pos.z + (Random_GetControl() & 0x3F) - 32; M_RememberRipple(ripple); } void FX_Water_TriggerUnderwaterBloodD(const XYZ_32 pos, const int32_t size) { int32_t idx = -1; for (int32_t i = 0; i < (int32_t)ARRAY_SIZE(m_Ripples); i++) { if ((m_Ripples[i].flags & 1U) == 0U) { idx = i; break; } } if (idx < 0) { return; } FX_WATER_RIPPLE *const ripple = &m_Ripples[idx]; ripple->flags = 0x33U; ripple->init = 1U; ripple->life = (Random_GetDraw() & 7) - 16; ripple->size = size; ripple->x = pos.x + (Random_GetDraw() & 0x3F) - 32; ripple->y = pos.y; ripple->z = pos.z + (Random_GetDraw() & 0x3F) - 32; M_RememberRipple(ripple); } static RGBA_8888 M_Gray(int32_t c) { CLAMP(c, 0, 255); return (RGBA_8888) { c, c, c, 255 }; } static void M_DrawSplash( const int32_t base_sprite_idx, const FX_WATER_SPLASH *s) { const double ratio = Interpolation_GetWorldRate(); const bool do_interp = Interpolation_IsActive() && ratio > 0.0 && ratio < 1.0; XYZ_32 points[48]; for (int32_t i = 0; i < 48; i++) { const FX_WATER_SPLASH_VERT *const v = &s->v[i]; points[i] = (XYZ_32) { .x = s->x + ((do_interp ? (int16_t)LERP(v->prev_wx, v->wx, ratio) : v->wx) >> 4), .y = s->y + (do_interp ? (int16_t)LERP(v->prev_wy, v->wy, ratio) : v->wy), .z = s->z + ((do_interp ? (int16_t)LERP(v->prev_wz, v->wz, ratio) : v->wz) >> 4), }; } for (int32_t ring = 0; ring < 3; ring++) { int32_t sprite_idx = base_sprite_idx + 8; if (ring == 2 || (ring == 0 && (s->flags & 4U) != 0U) || (ring == 1 && (s->flags & 8U) != 0U)) { sprite_idx = base_sprite_idx + 4 + ((m_Wibble >> 4) & 3); } const int32_t life = do_interp ? (int32_t)LERP(s->prev_life, s->life, ratio) : (int32_t)s->life; int32_t c = life << 1; CLAMPG(c, 255); const RGBA_8888 c1 = M_Gray(c); c = (life - (life >> 2)) << 1; CLAMPG(c, 255); const RGBA_8888 c2 = M_Gray(c); const int32_t base = ring * 16; for (int32_t quad = 0; quad < 8; quad++) { const int32_t i0 = m_SplashQuadLinks[quad * 4 + 0] + base; const int32_t i1 = m_SplashQuadLinks[quad * 4 + 1] + base; const int32_t i2 = m_SplashQuadLinks[quad * 4 + 2] + base; const int32_t i3 = m_SplashQuadLinks[quad * 4 + 3] + base; const XYZ_32 quad_pos[4] = { points[i0], points[i1], points[i3], points[i2], }; const RGBA_8888 quad_color[4] = { c1, c1, c2, c2 }; OutputSource_PolyFX_StageSpriteQuadWorldDepth( sprite_idx, quad_pos, quad_color, M_SPLASH_Z_DEPTH_ADJUST, DRAW_BLEND_ADD); } } } static void M_DrawRipple( const int32_t base_sprite_idx, const FX_WATER_RIPPLE *r) { const double ratio = Interpolation_GetWorldRate(); const bool do_interp = Interpolation_IsActive() && ratio > 0.0 && ratio < 1.0; const int32_t size = do_interp ? (int32_t)LERP(r->prev_size, r->size, ratio) : (int32_t)r->size; const int32_t init = do_interp ? (int32_t)LERP(r->prev_init, r->init, ratio) : (int32_t)r->init; const int32_t life = do_interp ? (int32_t)LERP(r->prev_life, r->life, ratio) : (int32_t)r->life; const int32_t n = size << 2; int32_t sprite_idx = base_sprite_idx + 9; RGBA_8888 color; if ((r->flags & 0x10U) != 0U) { if ((r->flags & 0x20U) != 0U) { sprite_idx = base_sprite_idx; if (!M_GetUnderwaterBloodColor(&color, life)) { return; } } else { int32_t c1 = init != 0 ? (init >> 2) : (life >> 2); c1 <<= 3; CLAMPG(c1, 255); color = M_Gray(c1); } } else { int32_t c1 = init != 0 ? (init >> 1) : (life >> 1); c1 <<= 3; CLAMPG(c1, 255); color = M_Gray(c1); } // double-sided const XYZ_32 quad_pos[2][4] = { { { r->x - n, r->y, r->z - n }, { r->x + n, r->y, r->z - n }, { r->x + n, r->y, r->z + n }, { r->x - n, r->y, r->z + n }, }, { { r->x - n, r->y, r->z - n }, { r->x - n, r->y, r->z + n }, { r->x + n, r->y, r->z + n }, { r->x + n, r->y, r->z - n }, }, }; const RGBA_8888 quad_color[4] = { color, color, color, color }; OutputSource_PolyFX_StageSpriteQuadWorldDepth( sprite_idx, quad_pos[0], quad_color, M_RIPPLE_Z_DEPTH_ADJUST, DRAW_BLEND_ADD); OutputSource_PolyFX_StageSpriteQuadWorldDepth( sprite_idx, quad_pos[1], quad_color, M_RIPPLE_Z_DEPTH_ADJUST, DRAW_BLEND_ADD); } void FX_Water_Draw(void) { const OBJECT *const explosion = Object_Get(O_EXPLOSION_1); if (!explosion->loaded) { return; } const int32_t base_sprite_idx = explosion->mesh_idx; for (int32_t i = 0; i < (int32_t)ARRAY_SIZE(m_Splashes); i++) { const FX_WATER_SPLASH *const splash = &m_Splashes[i]; if ((splash->flags & 1U) == 0U) { continue; } M_DrawSplash(base_sprite_idx, splash); } for (int32_t i = 0; i < (int32_t)ARRAY_SIZE(m_Ripples); i++) { const FX_WATER_RIPPLE *const ripple = &m_Ripples[i]; if ((ripple->flags & 1U) == 0U) { continue; } M_DrawRipple(base_sprite_idx, ripple); } } void FX_Water_Control(void) { if (m_SplashCount > 0) { m_SplashCount--; } for (int32_t i = 0; i < (int32_t)ARRAY_SIZE(m_Splashes); i++) { FX_WATER_SPLASH *const splash = &m_Splashes[i]; if ((splash->flags & 1U) == 0U) { continue; } M_RememberSplash(splash); bool set = false; for (int32_t j = 0; j < 48; j++) { FX_WATER_SPLASH_VERT *const v = &splash->v[j]; v->wx += v->xv >> 2; v->wy += (int16_t)(v->yv >> 6); v->wz += v->zv >> 2; v->xv -= v->xv >> v->friction; v->zv -= v->zv >> v->friction; if ((v->oxv < 0 && v->xv > v->oxv) || (v->oxv > 0 && v->xv < v->oxv)) { v->xv = v->oxv; } else if ( (v->ozv < 0 && v->zv > v->ozv) || (v->ozv > 0 && v->zv < v->ozv)) { v->zv = v->ozv; } v->yv += (int32_t)v->gravity << 3; CLAMPG(v->yv, 0x10000); if (v->wy > 0) { if (j < 16) { splash->flags |= 4U; } else if (j < 32) { splash->flags |= 8U; } v->wy = 0; set = true; } } if (set) { splash->life--; if (splash->life == 0U) { splash->flags = 0U; } } } for (int32_t i = 0; i < (int32_t)ARRAY_SIZE(m_Ripples); i++) { FX_WATER_RIPPLE *const ripple = &m_Ripples[i]; if ((ripple->flags & 1U) == 0U) { continue; } M_RememberRipple(ripple); if (ripple->size < 254U) { ripple->size += 2U; } if (ripple->init == 0U) { ripple->life -= 2U; if (ripple->life > 250U) { ripple->flags = 0U; } } else if (ripple->init < ripple->life) { ripple->init += 4U; if (ripple->init >= ripple->life) { ripple->init = 0U; } } } m_Wibble = (m_Wibble + 4) & 0xFC; } ================================================ FILE: src/trx/game/fx/water.h ================================================ #pragma once #include #include #include typedef struct { int32_t x; int32_t y; int32_t z; int32_t prev_size; int32_t prev_life; int32_t prev_init; uint8_t flags; uint8_t life; uint8_t size; uint8_t init; } FX_WATER_RIPPLE; typedef struct { int16_t wx; int16_t wy; int16_t wz; int16_t prev_wx; int16_t prev_wy; int16_t prev_wz; int16_t xv; int32_t yv; int16_t zv; int16_t oxv; int16_t ozv; uint8_t friction; uint8_t gravity; } FX_WATER_SPLASH_VERT; typedef struct { int32_t x; int32_t y; int32_t z; int32_t prev_life; uint8_t flags; uint8_t life; uint8_t pad[2]; FX_WATER_SPLASH_VERT v[48]; } FX_WATER_SPLASH; typedef struct { int32_t x; int32_t y; int32_t z; int16_t inner_xz_off; int16_t inner_xz_size; int16_t inner_y_size; int16_t inner_xz_vel; int16_t inner_y_vel; int16_t inner_gravity; int16_t inner_friction; int16_t middle_xz_off; int16_t middle_xz_size; int16_t middle_y_size; int16_t middle_xz_vel; int16_t middle_y_vel; int16_t middle_gravity; int16_t middle_friction; int16_t outer_xz_off; int16_t outer_xz_size; int16_t outer_xz_vel; int16_t outer_friction; } FX_WATER_SPLASH_SETUP; void FX_Water_Reset(void); void FX_Water_Control(void); void FX_Water_Draw(void); FX_WATER_RIPPLE *FX_Water_SetupRipple( int32_t x, int32_t y, int32_t z, int32_t size, bool is_still); void FX_Water_SetupSplash(const FX_WATER_SPLASH_SETUP *setup); void FX_Water_Splash(const ITEM *item); void FX_Water_WadeSplash(const ITEM *item, int32_t water_height, int32_t depth); void FX_Water_TriggerUnderwaterBlood(XYZ_32 pos, int32_t size); void FX_Water_TriggerUnderwaterBloodD(XYZ_32 pos, int32_t size); ================================================ FILE: src/trx/game/fx/water_particles.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_MAX_WATER_PARTICLES 256 #define M_MAX_WATER_PARTICLES_ALIVE 16 #define M_SPAWN_DIST_MASK 0xFFF #define M_SPAWN_ANGLE_MASK 0x1FFE #define M_SPAWN_Y_MASK 0x7FF #define M_BASE_Y_OFF (-1024) #define M_MIN_SIZE 4.0f #define M_MAX_SIZE 16.0f typedef struct { XYZ_32 pos; XYZ_32 prev_pos; uint8_t life; uint8_t yv; int8_t xv; int8_t zv; } M_WATER_PARTICLE; static M_WATER_PARTICLE m_WaterParticles[M_MAX_WATER_PARTICLES]; static bool M_IsEnabled(void) { if (!g_Config.visuals.enable_weather) { return false; } const GF_LEVEL *const level = GF_GetCurrentLevel(); return level != nullptr && level->water_particles; } static void M_Clear(void) { memset(m_WaterParticles, 0, sizeof(m_WaterParticles)); } static int64_t M_GetViewDepth(const XYZ_32 pos) { // clang-format off return g_ViewMatrix._20 * pos.x + g_ViewMatrix._21 * pos.y + g_ViewMatrix._22 * pos.z + g_ViewMatrix._23; // clang-format on } static float M_GetFixedScale(const float unit) { const float base_w = 640.0f; const float base_h = 480.0f; const float game_w = (float)Viewport_GetWidth(VIEWPORT_GAME); const float game_h = (float)Viewport_GetHeight(VIEWPORT_GAME); const float x = game_w > base_w ? (game_w * unit) / base_w : unit; const float y = game_h > base_h ? (game_h * unit) / base_h : unit; return MIN(x, y); } static void M_Spawn(M_WATER_PARTICLE *const particle) { const ITEM *const lara_item = Lara_GetItem(); if (lara_item == nullptr) { return; } const int32_t dist = Random_GetDraw() & M_SPAWN_DIST_MASK; const int32_t angle = (Random_GetDraw() & M_SPAWN_ANGLE_MASK) * 8; particle->pos = XYZ_32_OffsetYaw(lara_item->pos, angle, dist); particle->pos.y += (Random_GetDraw() & M_SPAWN_Y_MASK) + M_BASE_Y_OFF; int16_t room_num = NO_ROOM; Room_GetOutsideStatus(particle->pos, &room_num); if (room_num == NO_ROOM || !Room_Get(room_num)->flags.underwater) { particle->pos.x = 0; return; } particle->life = (uint8_t)((Random_GetDraw() & 7) + 16); particle->xv = (int8_t)(Random_GetDraw() & 3); if (particle->xv == 2) { particle->xv = -1; } particle->yv = (uint8_t)(((Random_GetDraw() & 7) + 8) << 3); particle->zv = (int8_t)(Random_GetDraw() & 3); if (particle->zv == 2) { particle->zv = -1; } particle->prev_pos = particle->pos; } void FX_WaterParticles_Reset(void) { M_Clear(); } void FX_WaterParticles_Control(void) { if (!M_IsEnabled()) { M_Clear(); return; } int32_t num_alive = 0; for (int32_t i = 0; i < M_MAX_WATER_PARTICLES; i++) { M_WATER_PARTICLE *const particle = &m_WaterParticles[i]; if (particle->pos.x == 0 && num_alive < M_MAX_WATER_PARTICLES_ALIVE) { num_alive++; M_Spawn(particle); } if (particle->pos.x == 0) { continue; } particle->prev_pos = particle->pos; particle->pos.x += particle->xv; particle->pos.y += (particle->yv & 0xF8) >> 6; particle->pos.z += particle->zv; if (particle->life == 0) { particle->pos.x = 0; continue; } particle->life--; if ((particle->yv & 7) != 7) { particle->yv++; } } } void FX_WaterParticles_Draw(void) { if (!M_IsEnabled()) { return; } const OBJECT *const obj = Object_Get(O_EXPLOSION_1); if (obj == nullptr || !obj->loaded) { return; } const int32_t sprite_idx = obj->mesh_idx + 17; if (sprite_idx < 0 || sprite_idx >= Output_GetSpriteTextureCount()) { return; } const int32_t atlas_idx = Output_Textures_GetSpriteUVWIndex(sprite_idx, 0); const OUTPUT_TEXTURE_SIZE atlas_size = Output_Textures_GetAtlasSize(atlas_idx / 4); const OUTPUT_TEXTURE_SIZE tri_size[3] = { atlas_size, atlas_size, atlas_size }; const OUTPUT_UVW tri_uvw[3] = { Output_Textures_GetUVW( Output_Textures_GetSpriteUVWIndex(sprite_idx, 1)), Output_Textures_GetUVW( Output_Textures_GetSpriteUVWIndex(sprite_idx, 2)), Output_Textures_GetUVW( Output_Textures_GetSpriteUVWIndex(sprite_idx, 3)), }; const double ratio = Interpolation_GetWorldRate(); const bool do_interp = Interpolation_IsActive() && ratio > 0.0 && ratio < 1.0; for (int32_t i = 0; i < M_MAX_WATER_PARTICLES; i++) { M_WATER_PARTICLE *const particle = &m_WaterParticles[i]; if (particle->pos.x == 0) { continue; } const XYZ_32 center = do_interp ? (XYZ_32) { .x = (int32_t)LERP( particle->prev_pos.x, particle->pos.x, ratio), .y = (int32_t)LERP( particle->prev_pos.y, particle->pos.y, ratio), .z = (int32_t)LERP( particle->prev_pos.z, particle->pos.z, ratio), } : particle->pos; const int64_t zv = M_GetViewDepth(center); const int64_t near_z = Output_GetNearZ(); const int64_t far_z = Output_GetFarZ(); const int32_t vpos_z = (int32_t)(zv >> W2V_SHIFT); if (vpos_z < 128) { if (particle->life > 16) { particle->life = 16; } continue; } if (zv <= near_z || zv >= far_z || g_PhdPersp <= 0) { continue; } float size = (float)((g_PhdPersp * (particle->yv >> 3)) / vpos_z); CLAMP(size, M_MIN_SIZE, M_MAX_SIZE); size /= 3.0f; size = M_GetFixedScale(size) / 2.0f; uint32_t c; if ((particle->yv & 7) < 7) { c = (uint32_t)(particle->yv & 7); } else if (particle->life > 18) { c = 15; } else { c = particle->life; } c <<= 2; CLAMPG(c, 255); const RGBA_8888 color = { c, c, c, 255 }; const RGBA_8888 colors[3] = { color, color, color }; const XYZ_32 world_pos[3] = { center, center, center }; const float disp[3][2] = { { size, -2.0f * size }, { size, size }, { -2.0f * size, size }, }; OutputSource_PolyFX_StageTriExtUV( world_pos, tri_uvw, tri_size, disp, colors, VERT_NO_LIGHTING | VERT_NO_WIBBLE | VERT_BILLBOARD | VERT_ABS_SPRITE, DRAW_BLEND_ADD); } } ================================================ FILE: src/trx/game/fx/water_particles.h ================================================ #pragma once void FX_WaterParticles_Reset(void); void FX_WaterParticles_Control(void); void FX_WaterParticles_Draw(void); ================================================ FILE: src/trx/game/fx/weather.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_MAX_WEATHER 256 #define M_MAX_WEATHER_ALIVE 16 #define M_RAIN_MAX_DISTANCE 6000 #define M_RAIN_BASE_Y_OFF (-1024) #define M_RAIN_BASE_YV 16 #define M_RAIN_YV_RND_MASK 7 #define M_RAIN_SPAWN_DIST_MASK 0xFFF #define M_RAIN_SPAWN_ANGLE_MASK 0x1FFE #define M_RAIN_Y_RND_MASK 0x7FF #define M_RAIN_LIFE_BASE 88 #define M_SNOW_LIFE_BASE 96 #define M_SNOW_YV_MIN 8 #define M_SNOW_YV_RANGE 24 typedef struct { XYZ_32 pos; XYZ_32 prev_pos; int32_t prev_yv; int8_t xv; uint8_t yv; int8_t zv; uint8_t life; } M_RAINDROP; typedef struct { XYZ_32 pos; XYZ_32 prev_pos; bool stopped; int32_t prev_yv; int32_t prev_life; int8_t xv; uint8_t yv; int8_t zv; uint8_t life; } M_SNOWFLAKE; static M_RAINDROP m_Raindrops[M_MAX_WEATHER]; static M_SNOWFLAKE m_Snowflakes[M_MAX_WEATHER]; static WEATHER_TYPE m_WeatherType = WEATHER_NONE; static void M_ClearWeather(void) { memset(m_Raindrops, 0, sizeof(m_Raindrops)); memset(m_Snowflakes, 0, sizeof(m_Snowflakes)); } static int64_t M_GetViewDepth(const XYZ_32 pos) { // clang-format off return g_ViewMatrix._20 * pos.x + g_ViewMatrix._21 * pos.y + g_ViewMatrix._22 * pos.z + g_ViewMatrix._23; // clang-format on } static bool M_SpawnParticle(XYZ_32 *const pos) { const ITEM *const lara_item = Lara_GetItem(); if (lara_item == nullptr) { return false; } XYZ_32 lara_pos = {}; if (!Lara_GetMeshPos(LM_HIPS, &lara_pos)) { lara_pos = lara_item->pos; } const int32_t dist = Random_GetDraw() & M_RAIN_SPAWN_DIST_MASK; const int32_t angle = (Random_GetDraw() & M_RAIN_SPAWN_ANGLE_MASK) * 8; *pos = XYZ_32_OffsetYaw(lara_pos, angle, dist); pos->y += M_RAIN_BASE_Y_OFF - (Random_GetDraw() & M_RAIN_Y_RND_MASK); int16_t room_num = NO_ROOM; if (Room_GetOutsideStatus(*pos, &room_num) != 1) { pos->x = 0; return false; } return true; } static void M_SpawnRainDrop(M_RAINDROP *const drop) { if (!M_SpawnParticle(&drop->pos)) { return; } drop->yv = (uint8_t)((Random_GetDraw() & M_RAIN_YV_RND_MASK) + M_RAIN_BASE_YV); drop->xv = (int8_t)((Random_GetDraw() & 7) - 4); drop->zv = (int8_t)((Random_GetDraw() & 7) - 4); drop->life = (uint8_t)(M_RAIN_LIFE_BASE - ((int32_t)drop->yv << 1)); drop->prev_pos = drop->pos; drop->prev_yv = drop->yv; } static void M_UpdateRain(void) { const XZ_32 wind = Sparks_GetSmokeWind(); int32_t num_alive = 0; for (int32_t i = 0; i < M_MAX_WEATHER; i++) { M_RAINDROP *const drop = &m_Raindrops[i]; if (drop->pos.x == 0 && num_alive < M_MAX_WEATHER_ALIVE) { num_alive++; M_SpawnRainDrop(drop); } if (drop->pos.x == 0) { continue; } drop->prev_pos = drop->pos; drop->prev_yv = drop->yv; int16_t room_num = NO_ROOM; const int32_t outside = Room_GetOutsideStatus(drop->pos, &room_num); if (outside == -2 || (room_num != NO_ROOM && Room_Get(room_num)->flags.underwater) || drop->life > 240 || ABS(g_Camera.pos.x - drop->pos.x) > M_RAIN_MAX_DISTANCE || ABS(g_Camera.pos.z - drop->pos.z) > M_RAIN_MAX_DISTANCE) { drop->pos.x = 0; continue; } drop->pos.x += (int32_t)drop->xv + 4 * wind.x; drop->pos.y += (int32_t)drop->yv * 8; drop->pos.z += (int32_t)drop->zv + 4 * wind.z; int32_t rnd = Random_GetDraw(); if ((rnd & 3) != 3) { drop->xv += (int8_t)((rnd & 3) - 1); if (drop->xv < -4) { drop->xv = -4; } else if (drop->xv > 4) { drop->xv = 4; } } rnd = (rnd >> 2) & 3; if (rnd != 3) { drop->zv += (int8_t)(rnd - 1); if (drop->zv < -4) { drop->zv = -4; } else if (drop->zv > 4) { drop->zv = 4; } } drop->life -= 2; if (drop->life > 240) { drop->pos.x = 0; } } } static void M_DrawRain(void) { const XZ_32 wind = Sparks_GetSmokeWind(); const double ratio = Interpolation_GetWorldRate(); const bool do_interp = Interpolation_IsActive() && ratio > 0.0 && ratio < 1.0; for (int32_t i = 0; i < M_MAX_WEATHER; i++) { const M_RAINDROP *const drop = &m_Raindrops[i]; if (drop->pos.x == 0) { continue; } const int32_t yv = do_interp ? (int32_t)LERP(drop->prev_yv, drop->yv, ratio) : (int32_t)drop->yv; const XYZ_32 to = do_interp ? (XYZ_32) { .x = (int32_t)LERP(drop->prev_pos.x, drop->pos.x, ratio), .y = (int32_t)LERP(drop->prev_pos.y, drop->pos.y, ratio), .z = (int32_t)LERP(drop->prev_pos.z, drop->pos.z, ratio), } : drop->pos; const XYZ_32 from = { to.x - (wind.x * 4), to.y - (yv * 8), to.z - (wind.z * 4), }; const RGBA_8888 from_color = { 0, 0, 0x20, 0x00 }; const RGBA_8888 to_color = { 0x30, 0x40, 0x60, 0x80 }; OutputSource_PolyFX_StageLineSegment( from, from_color, to, to_color, 1.0f, DRAW_BLEND); } } static void M_SpawnSnowflake(M_SNOWFLAKE *const snow) { if (!M_SpawnParticle(&snow->pos)) { return; } snow->stopped = false; snow->xv = (int8_t)((Random_GetDraw() & 7) - 4); snow->yv = (uint8_t)(((Random_GetDraw() % M_SNOW_YV_RANGE) + M_SNOW_YV_MIN) * 8); snow->zv = (int8_t)((Random_GetDraw() & 7) - 4); snow->life = (uint8_t)(M_SNOW_LIFE_BASE - ((int32_t)snow->yv << 1)); snow->prev_pos = snow->pos; snow->prev_yv = snow->yv; snow->prev_life = snow->life; } static void M_UpdateSnow(void) { int32_t num_alive = 0; for (int32_t i = 0; i < M_MAX_WEATHER; i++) { M_SNOWFLAKE *const snow = &m_Snowflakes[i]; if (snow->pos.x == 0 && num_alive < M_MAX_WEATHER_ALIVE) { num_alive++; M_SpawnSnowflake(snow); } if (snow->pos.x == 0) { continue; } snow->prev_pos = snow->pos; snow->prev_yv = snow->yv; snow->prev_life = snow->life; const XYZ_32 old_pos = snow->pos; int16_t room_num = NO_ROOM; int32_t outside = 1; if (!snow->stopped) { snow->pos.x += snow->xv; snow->pos.y += (snow->yv & 0xF8) >> 2; snow->pos.z += snow->zv; outside = Room_GetOutsideStatus(snow->pos, &room_num); if (outside == -3) { snow->pos.x = 0; continue; } if (outside == -2 || (room_num != NO_ROOM && Room_Get(room_num)->flags.underwater)) { snow->stopped = true; snow->pos = old_pos; if (snow->life > 16) { snow->life = 16; } if (snow->yv > 16) { snow->yv -= 16; } } } if (snow->life == 0) { snow->pos.x = 0; continue; } if ((ABS(g_Camera.pos.x - snow->pos.x) > M_RAIN_MAX_DISTANCE || ABS(g_Camera.pos.z - snow->pos.z) > M_RAIN_MAX_DISTANCE) && snow->life > 16) { snow->life = 16; } const XZ_32 wind = Sparks_GetSmokeWind(); if (snow->xv < (wind.x * 2)) { snow->xv++; } else if (snow->xv > (wind.x * 2)) { snow->xv--; } if (snow->zv < (wind.z * 2)) { snow->zv++; } else if (snow->zv > (wind.z * 2)) { snow->zv--; } snow->life -= 2; if ((snow->yv & 7) != 7) { snow->yv++; } } } static void M_DrawSnow(void) { const OBJECT *const obj = Object_Get(O_SNOWFLAKE); if (!obj->loaded) { return; } const int32_t sprite_idx = obj->mesh_idx; const double ratio = Interpolation_GetWorldRate(); const bool do_interp = Interpolation_IsActive() && ratio > 0.0 && ratio < 1.0; for (int32_t i = 0; i < M_MAX_WEATHER; i++) { M_SNOWFLAKE *const snow = &m_Snowflakes[i]; if (snow->pos.x == 0) { continue; } const XYZ_32 center = do_interp ? (XYZ_32) { .x = (int32_t)LERP(snow->prev_pos.x, snow->pos.x, ratio), .y = (int32_t)LERP(snow->prev_pos.y, snow->pos.y, ratio), .z = (int32_t)LERP(snow->prev_pos.z, snow->pos.z, ratio), } : snow->pos; const int64_t zv = M_GetViewDepth(center); const int64_t near_z = Output_GetNearZ(); const int64_t far_z = Output_GetFarZ(); const int32_t vpos_z = (int32_t)(zv >> W2V_SHIFT); if (vpos_z < 128 && snow->life > 16) { snow->life = 16; } if (zv <= near_z || zv >= far_z) { continue; } if (vpos_z < 128 || g_PhdPersp <= 0) { continue; } const int32_t yv = do_interp ? (int32_t)LERP(snow->prev_yv, snow->yv, ratio) : (int32_t)snow->yv; const int32_t life = do_interp ? (int32_t)LERP(snow->prev_life, snow->life, ratio) : (int32_t)snow->life; const int32_t game_w = Viewport_GetWidth(VIEWPORT_GAME); const int32_t game_h = Viewport_GetHeight(VIEWPORT_GAME); const int32_t ui_w = Viewport_GetWidth(VIEWPORT_UI); const int32_t ui_h = Viewport_GetHeight(VIEWPORT_UI); const XYZ_32 world_pos[4] = { center, center, center, center }; const float s = 8.0f; const float disp[4][2] = { { -s, -s }, { s, -s }, { s, s }, { -s, s }, }; uint32_t c; if ((yv & 7) < 7) { c = (uint32_t)(yv & 7); } else if (life > 18) { c = 15; } else { c = (uint32_t)life; } c <<= 3; CLAMPG(c, 255); { const int32_t fog_start = Output_GetFogStart(); const int32_t fog_end = Output_GetFogEnd(); float fade = 1.0f; if (fog_end > fog_start && vpos_z > fog_start) { float t = (vpos_z - fog_start) / (float)(fog_end - fog_start); CLAMP(t, 0.0f, 1.0f); fade = 1.0f - t; } c = (uint32_t)(c * fade); } const RGBA_8888 color = { c, c, c, 255 }; const RGBA_8888 colors[4] = { color, color, color, color }; OutputSource_PolyFX_StageQuadExt( sprite_idx, world_pos, disp, colors, VERT_NO_LIGHTING | VERT_NO_WIBBLE | VERT_BILLBOARD | VERT_ABS_SPRITE, DRAW_BLEND_ADD); } } void FX_Weather_Reset(void) { M_ClearWeather(); } WEATHER_TYPE FX_Weather_GetWeather(void) { return m_WeatherType; } void FX_Weather_SetWeather(const WEATHER_TYPE weather_type) { m_WeatherType = weather_type; } void FX_Weather_Control(void) { if (!g_Config.visuals.enable_weather) { return; } const WEATHER_TYPE weather_type = m_WeatherType; if (weather_type == WEATHER_RAIN) { M_UpdateRain(); } else if (weather_type == WEATHER_SNOW) { M_UpdateSnow(); } } void FX_Weather_Draw(void) { if (!g_Config.visuals.enable_weather) { return; } switch (m_WeatherType) { case WEATHER_RAIN: M_DrawRain(); break; case WEATHER_SNOW: M_DrawSnow(); break; default: break; } } ================================================ FILE: src/trx/game/fx/weather.h ================================================ #pragma once #include typedef enum { WEATHER_NONE = 0, WEATHER_RAIN, WEATHER_SNOW, } WEATHER_TYPE; void FX_Weather_Reset(void); void FX_Weather_Control(void); void FX_Weather_Draw(void); WEATHER_TYPE FX_Weather_GetWeather(void); void FX_Weather_SetWeather(WEATHER_TYPE weather_type); ================================================ FILE: src/trx/game/fx.h ================================================ #pragma once #include #include #include #include #include #include #include #include #include ================================================ FILE: src/trx/game/game/control.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_FRAME_BUFFER(key) \ do { \ Shell_ProcessEvents(); \ Output_BeginScene(); \ Game_Draw(true); \ Input_Update(); \ Output_EndScene(); \ Output_FlipScreen(); \ Clock_WaitTick(); \ } while (g_Input.key); int32_t g_OverlayFlag = 0; bool Game_Start(const GF_LEVEL *const level, const GF_SEQUENCE_CONTEXT seq_ctx) { Game_SetCurrentLevel(level); g_OverlayFlag = 1; Camera_Initialise(); Interpolation_Remember(); Sound_StopAll(); const bool is_cutscene = level->type == GFL_CUTSCENE; if (level->music_track != MX_INACTIVE && (is_cutscene || Music_GetCurrentLoopedTrack() == MX_INACTIVE)) { Music_Play_Direct( level->music_track, is_cutscene ? MPM_ONCE : MPM_LOOP); } const LUA_EVENT_ARG args[] = { { .type = LUA_EVENT_ARG_INT32, .value = { .i32 = level->num } }, { .type = LUA_EVENT_ARG_BOOL, .value = { .b = seq_ctx == GFSC_SAVED } }, }; Lua_FireEventEx(LUA_EVENT_GAME_START, args, 2); return true; } void Game_End(void) { Savegame_PersistGameToCurrentInfo(Game_GetCurrentLevel()); Music_Stop(); } GF_COMMAND Game_Control(const bool demo_mode) { const LARA_INFO *const lara = Lara_GetLaraInfo(); if (g_Passport.ask_for_save && !lara->extra_anim) { // ask for a save at the start of a level for the save crystals mode const GF_COMMAND gf_cmd = GF_ShowInventory(INV_SAVE_CRYSTAL_MODE); g_Passport.ask_for_save = false; if (gf_cmd.action != GF_NOOP) { return gf_cmd; } } Interpolation_Remember(); if (!Game_IsInGym() || Gym_TrackManager_IsTimerActive(GYM_TRACK_ASSAULT) || Gym_TrackManager_IsTimerActive(GYM_TRACK_QUAD) || !Object_Get(O_ASSAULT_DIGITS)->loaded) { Stats_UpdateTimer(); } if (Game_IsInGym()) { Gym_Control(); } if (g_Config.flow.cheat_keys) { Lara_Cheat_CheckKeys(); } if (Game_IsLevelComplete()) { Sound_StopAll(); Music_Stop(); return (GF_COMMAND) { .action = GF_LEVEL_COMPLETE }; } Input_Update(); Shell_ProcessInput(); if (g_InputDB.toggle_photo_mode) { return GF_EnterPhotoMode(); } else if (g_InputDB.pause && lara->death_timer == 0) { return GF_PauseGame(); } if (demo_mode) { if (g_InputDB.menu_confirm || g_InputDB.menu_back) { return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE }; } if (!Demo_UpdateInput()) { g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE }; } } Game_ProcessInput(); if ((g_InputDB.quick_save || g_InputDB.quick_load) && !demo_mode && !g_Config.flow.load_save_disabled) { bool quick_handled = false; if (g_InputDB.quick_save && !lara->extra_anim && lara->death_timer == 0) { const SAVEGAME_SLOT_REF slot = Savegame_GetNextQuickSlot(); if (!Savegame_IsValidSlotRef(slot)) { Console_LogError( "%s", GS("general/osd/quick_save_fail_no_slots")); } else if (Savegame_Save(slot)) { Console_Log("%s", GS("general/osd/quick_save")); } quick_handled = true; } else if (g_InputDB.quick_load) { const SAVEGAME_SLOT_REF slot = Savegame_GetBoundSlot(); if (!Savegame_IsValidSlotRef(slot)) { Console_LogError( "%s", GS("general/osd/quick_load_fail_no_bound_slot")); } else if (Savegame_IsSlotFree(slot)) { Console_LogError( "%s", GS("general/osd/quick_load_fail_unavailable_bound_slot")); } else { if (slot.pool == SAVEGAME_SLOT_POOL_QUICK) { const int32_t visual_index = Savegame_QuickToVisualIndex(slot); Console_Log(GS("general/osd/quick_load"), visual_index + 1); } else { Console_Log(GS("general/osd/load_game"), slot.index + 1); } return (GF_COMMAND) { .action = GF_START_SAVED_GAME, .param = Savegame_SlotToParam(slot), }; } quick_handled = true; } if (quick_handled) { // Prevent mixed bindings (quick + normal save/load on same key) // from also opening the passport save/load flow. g_Input.save = false; g_Input.load = false; g_InputDB.save = false; g_InputDB.load = false; } } if (lara->death_timer > DEATH_WAIT || (lara->death_timer > DEATH_WAIT_INPUT && (g_InputDB.menu_confirm || g_InputDB.menu_back) && !g_Input.fly_cheat) || g_OverlayFlag == 2) { if (demo_mode || (g_TRVersion >= 2 && Game_IsInGym())) { return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE }; } if (g_OverlayFlag == 2) { g_OverlayFlag = 1; return GF_ShowInventory(INV_DEATH_MODE); } else { g_OverlayFlag = 2; } } if ((g_InputDB.option || g_InputDB.load || g_InputDB.save || g_OverlayFlag <= 0) && lara->death_timer == 0 && !lara->extra_anim) { if (g_TRVersion == 1 && g_Camera.type == CAM_CINEMATIC) { g_OverlayFlag = 0; } else if (g_OverlayFlag > 0) { if (g_Config.flow.lockout_option_ring && g_Config.flow.load_save_disabled) { g_OverlayFlag = 0; } else if (g_Input.save) { g_OverlayFlag = -2; } else if (g_Input.load) { g_OverlayFlag = -1; } else { g_OverlayFlag = 0; } } else { GF_COMMAND gf_cmd; if (g_OverlayFlag == -1) { gf_cmd = GF_ShowInventory(INV_LOAD_MODE); } else if (g_OverlayFlag == -2) { gf_cmd = GF_ShowInventory(INV_SAVE_MODE); } else { gf_cmd = GF_ShowInventory(INV_GAME_MODE); } g_OverlayFlag = 1; if (gf_cmd.action != GF_NOOP) { return gf_cmd; } } } Output_ResetDynamicLights(); Sound_ResetAmbient(); Item_Control(); Effect_Control(); Sparks_Control(); Lara_Control(); FX_Control(); Lara_Hair_Control(false); Camera_Update(); ItemAction_RunActive(); Sound_UpdateEffects(); Overlay_Animate(1); Output_AnimateTextures(1); return (GF_COMMAND) { .action = GF_NOOP }; } void Game_ProcessInput(void) { if (GF_GetCurrentLevel()->type == GFL_DEMO) { return; } if (g_InputDB.use_small_medi && Inv_RequestItem(O_SMALL_MEDIPACK_OPTION)) { Lara_UseItem(O_SMALL_MEDIPACK_OPTION); } if (g_InputDB.use_big_medi && Inv_RequestItem(O_LARGE_MEDIPACK_OPTION)) { Lara_UseItem(O_LARGE_MEDIPACK_OPTION); } if (g_Config.input.enable_buffering_func_keys && Game_IsPlaying()) { if (g_Input.toggle_bilinear_filter) { M_FRAME_BUFFER(toggle_bilinear_filter); } else if (g_Input.toggle_trapezoid_filter) { M_FRAME_BUFFER(toggle_trapezoid_filter); } else if (g_Input.toggle_fps_counter) { M_FRAME_BUFFER(toggle_fps_counter); } } if (g_InputDB.toggle_ui) { UI_ToggleState(&g_Config.ui.enable_game_ui); } } ================================================ FILE: src/trx/game/game/control.h ================================================ #pragma once #include bool Game_Start(const GF_LEVEL *level, GF_SEQUENCE_CONTEXT seq_ctx); void Game_End(void); GF_COMMAND Game_Control(bool demo_mode); void Game_ProcessInput(void); ================================================ FILE: src/trx/game/game/draw.c ================================================ #include #include #include #include #include void Game_Draw(const bool draw_overlay) { Interpolation_Interpolate(); Camera_Apply(); Room_DrawAllRooms(g_Camera.interp.room_num, g_Camera.target.room_num); if (draw_overlay) { Overlay_DrawGameInfo(); } SceneCompositor_Flush(); if (g_Config.visuals.enable_reflections) { Output_Textures_UpdateEnvironmentMap(); } } ================================================ FILE: src/trx/game/game/draw.h ================================================ #pragma once void Game_Draw(bool draw_overlay); ================================================ FILE: src/trx/game/game/enum.h ================================================ #pragma once typedef enum { // clang-format off GBF_NONE = 0, GBF_NGPLUS = 1 << 0, GBF_JAPANESE = 1 << 1, // clang=format on } GAME_BONUS_FLAG; ================================================ FILE: src/trx/game/game/state.c ================================================ #include #include #include #include #include #include #include static bool m_IsPlaying = false; static const GF_LEVEL *m_CurrentLevel = nullptr; static GAME_BONUS_FLAG m_BonusFlag = GBF_NONE; static bool m_IsLevelComplete = false; void Game_SetIsPlaying(const bool is_playing) { m_IsPlaying = is_playing; Random_FreezeDraw(!is_playing); } bool Game_IsPlaying(void) { return m_IsPlaying; } const GF_LEVEL *Game_GetCurrentLevel(void) { return m_CurrentLevel; } void Game_SetCurrentLevel(const GF_LEVEL *const level) { m_CurrentLevel = level; } bool Game_IsInGym(void) { const GF_LEVEL *const current_level = GF_GetCurrentLevel(); return current_level != nullptr && current_level->type == GFL_GYM; } bool Game_IsLoaded(void) { if (FMV_IsPlaying()) { return false; } const GF_LEVEL *const current_level = GF_GetCurrentLevel(); if (current_level == nullptr || current_level->type == GFL_TITLE) { return false; } return true; } bool Game_IsPlayable(void) { if (FMV_IsPlaying()) { return false; } const GF_LEVEL *const current_level = GF_GetCurrentLevel(); if (current_level == nullptr || current_level->type == GFL_TITLE || current_level->type == GFL_DEMO || current_level->type == GFL_CUTSCENE) { return false; } if (!Object_Get(O_LARA)->loaded || Lara_GetItem() == nullptr || !Lara_IsControllable()) { return false; } return true; } GAME_BONUS_FLAG Game_GetBonusFlag(void) { return m_BonusFlag; } void Game_SetBonusFlag(const GAME_BONUS_FLAG flag) { m_BonusFlag = flag; } bool Game_IsBonusFlagSet(const GAME_BONUS_FLAG flag) { return (m_BonusFlag & flag) != 0; } void Game_SetIsLevelComplete(const bool is_complete) { m_IsLevelComplete = is_complete; } bool Game_IsLevelComplete(void) { return m_IsLevelComplete; } ================================================ FILE: src/trx/game/game/state.h ================================================ #pragma once #include #include extern int32_t g_OverlayFlag; // Sets the game's playing state, which in turn toggles random draw lock, and // certain overlay displays/animations, such as bars and pickups. void Game_SetIsPlaying(bool is_playing); // Returns true if the game is in a playing state - i.e. not suspended, such as // during pause, photo mode, or while the inventory is open. bool Game_IsPlaying(void); const GF_LEVEL *Game_GetCurrentLevel(void); void Game_SetCurrentLevel(const GF_LEVEL *level); bool Game_IsInGym(void); // Returns true if an FMV is not playing and the current level is not the title. bool Game_IsLoaded(void); // Returns true if an FMV is not playing, if the level type is neither the // title, a demo or a cutscene, and if Lara is loaded and controllable. bool Game_IsPlayable(void); GAME_BONUS_FLAG Game_GetBonusFlag(void); void Game_SetBonusFlag(GAME_BONUS_FLAG flag); bool Game_IsBonusFlagSet(GAME_BONUS_FLAG flag); void Game_SetIsLevelComplete(bool is_complete); bool Game_IsLevelComplete(void); ================================================ FILE: src/trx/game/game.h ================================================ #pragma once #include #include #include #include ================================================ FILE: src/trx/game/game_buf.c ================================================ #include #include #include static MEMORY_ARENA_ALLOCATOR m_Allocator[GBUF_NUM_MALLOC_TYPES] = {}; void GameBuf_Init(void) { for (int32_t i = 0; i < GBUF_NUM_MALLOC_TYPES; i++) { m_Allocator[i].default_chunk_size = 2048; } } void GameBuf_Reset(void) { for (int32_t i = 0; i < GBUF_NUM_MALLOC_TYPES; i++) { Memory_ArenaReset(&m_Allocator[i]); } } void GameBuf_ResetSingle(const GAME_BUFFER buffer) { Memory_ArenaReset(&m_Allocator[buffer]); } void GameBuf_Shutdown(void) { for (int32_t i = 0; i < GBUF_NUM_MALLOC_TYPES; i++) { Memory_ArenaFree(&m_Allocator[i]); } } void *GameBuf_Alloc(const size_t alloc_size, const GAME_BUFFER buffer) { const size_t aligned_size = Memory_Align(alloc_size); return Memory_ArenaAlloc(&m_Allocator[buffer], aligned_size); } ================================================ FILE: src/trx/game/game_buf.h ================================================ #pragma once #include // Internal game memory manager using an arena allocator. Memory is allocated // in discrete chunks, with each allocation request served via pointer // arithmetic within the active chunk. When a request exceeds the current // chunk's capacity, a new chunk is allocated to continue servicing // allocations. This design offers very fast allocation speeds, but individual // blocks cannot be freed – only the entire arena can be reset when needed. For // more granular memory management, use Memory_Alloc / Memory_Free. typedef enum { // clang-format off GBUF_TEXTURE_PAGES, GBUF_PALETTES, GBUF_OBJECT_TEXTURES, GBUF_SPRITE_TEXTURES, GBUF_STATIC_OBJECTS_3D, GBUF_STATIC_OBJECTS_2D, GBUF_MESH_POINTERS, GBUF_MESHES, GBUF_ANIMS, GBUF_ANIM_CHANGES, GBUF_ANIM_RANGES, GBUF_ANIM_COMMANDS, GBUF_ANIM_BONES, GBUF_ANIM_FRAMES, GBUF_ROOMS, GBUF_ROOM_MESH, GBUF_ROOM_PORTALS, GBUF_ROOM_SECTORS, GBUF_ROOM_LIGHTS, GBUF_ROOM_STATIC_MESHES, GBUF_FLOOR_DATA, GBUF_OUTSIDE_ROOM_TABLE, GBUF_ITEMS, GBUF_ITEM_DATA, GBUF_EFFECTS, GBUF_CAMERAS, GBUF_SOUND_SOURCES, GBUF_BOXES, GBUF_OVERLAPS, GBUF_GROUND_ZONE, GBUF_FLY_ZONE, GBUF_ANIMATED_TEXTURE_RANGES, GBUF_CINEMATIC_FRAMES, GBUF_CREATURE_DATA, GBUF_CREATURE_LOT, GBUF_SAMPLE_INFOS, GBUF_SAMPLES, GBUF_DEMO_BUFFER, GBUF_WALKABLES, GBUF_NUM_MALLOC_TYPES, // clang-format on } GAME_BUFFER; void GameBuf_Init(void); void GameBuf_Shutdown(void); void GameBuf_ResetSingle(GAME_BUFFER buffer); void GameBuf_Reset(void); void *GameBuf_Alloc(size_t alloc_size, GAME_BUFFER buffer); ================================================ FILE: src/trx/game/game_flow/common.c ================================================ #include #include #include #include static const GF_LEVEL *m_CurrentLevel = nullptr; static GF_COMMAND m_OverrideCommand = { .action = GF_NOOP }; static bool M_SkipLevel(const GF_LEVEL *const level) { return level->type == GFL_DUMMY || level->type == GFL_CURRENT; } static void M_FreeSequence(GF_SEQUENCE *const sequence) { Memory_Free(sequence->events); } static void M_FreeInjections(INJECTION_DATA *const injections) { if (injections->data_paths != nullptr) { for (int32_t i = 0; i < injections->count; i++) { Memory_FreePointer(&injections->data_paths[i]); } } Memory_FreePointer(&injections->data_paths); injections->count = 0; } static void M_FreeLevel(GF_LEVEL *const level) { Memory_FreePointer(&level->path); Memory_FreePointer(&level->title); Memory_FreePointer(&level->script_path); Memory_FreePointer(&level->lara_outfit); M_FreeInjections(&level->injections); M_FreeSequence(&level->sequence); if (level->item_drops.count > 0) { for (int32_t i = 0; i < level->item_drops.count; i++) { Memory_FreePointer(&level->item_drops.data[i].object_ids); } Memory_FreePointer(&level->item_drops.data); } Memory_FreePointer(&level->settings.sfx_path); } static void M_FreeLevelTable(GF_LEVEL_TABLE *const level_table) { if (level_table != nullptr) { for (int32_t i = 0; i < level_table->count; i++) { M_FreeLevel(&level_table->levels[i]); } Memory_FreePointer(&level_table->levels); level_table->count = 0; } } static void M_FreeFMVs(GAME_FLOW *const gf) { for (int32_t i = 0; i < gf->fmv_count; i++) { Memory_FreePointer(&gf->fmvs[i].path); } Memory_FreePointer(&gf->fmvs); gf->fmv_count = 0; } void GF_Init(void) { } void GF_Shutdown(void) { m_CurrentLevel = nullptr; m_OverrideCommand = (GF_COMMAND) { .action = GF_NOOP }; GAME_FLOW *const gf = &g_GameFlow; M_FreeInjections(&gf->injections); for (int32_t i = 0; i < GFLT_NUMBER_OF; i++) { M_FreeLevelTable(&gf->level_tables[i]); } M_FreeFMVs(gf); Memory_FreePointer(&gf->globe.entries); gf->globe.count = 0; if (gf->title_level != nullptr) { M_FreeLevel(gf->title_level); Memory_FreePointer(&gf->title_level); } Memory_FreePointer(&gf->main_menu_background_path); Memory_FreePointer(&gf->savegame_file_fmt); Memory_FreePointer(&gf->ambient_tracks.ids); gf->ambient_tracks.count = 0; Memory_FreePointer(&gf->settings.sfx_path); Memory_FreePointer(&gf->main_script_path); Memory_FreePointer(&gf->meta.name); Memory_FreePointer(&gf->meta.extends); Memory_FreePointer(&gf->path); } void GF_OverrideCommand(const GF_COMMAND command) { m_OverrideCommand = command; } GF_COMMAND GF_GetOverrideCommand(void) { return m_OverrideCommand; } GF_LEVEL_TABLE_TYPE GF_GetLevelTableType(const GF_LEVEL_TYPE level_type) { switch (level_type) { case GFL_GYM: case GFL_NORMAL: case GFL_BONUS: case GFL_DUMMY: case GFL_CURRENT: return GFLT_MAIN; case GFL_CUTSCENE: return GFLT_CUTSCENES; case GFL_DEMO: return GFLT_DEMOS; case GFL_TITLE: return GFLT_TITLE; } ASSERT_FAIL(); } const GF_LEVEL_TABLE *GF_GetLevelTable( const GF_LEVEL_TABLE_TYPE level_table_type) { return &g_GameFlow.level_tables[level_table_type]; } int32_t GF_GetLevelCount(const GF_LEVEL_TABLE_TYPE level_table_type) { int32_t count = 0; const GF_LEVEL_TABLE *const tbl = GF_GetLevelTable(level_table_type); for (int32_t i = 0; i < tbl->count; i++) { const GF_LEVEL *const level = &tbl->levels[i]; if (level->type == GFL_GYM || M_SkipLevel(level)) { continue; } count++; } return count; } int32_t GF_GetLevelOrdinalNumber( GF_LEVEL_TABLE_TYPE level_table_type, const GF_LEVEL *const ref_level) { int32_t ordinal = 1; const GF_LEVEL_TABLE *const tbl = GF_GetLevelTable(level_table_type); for (int32_t i = 0; i < tbl->count; i++) { const GF_LEVEL *level = &tbl->levels[i]; if (M_SkipLevel(level)) { continue; } if (level == ref_level) { // Special case: gym levels have no ordinal return (level->type == GFL_GYM) ? 0 : ordinal; } if (level->type != GFL_GYM) { ordinal++; } } return -1; } GF_LEVEL *GF_GetLevelByOrdinalNumber( GF_LEVEL_TABLE_TYPE level_table_type, const int32_t level_num) { const GF_LEVEL_TABLE *const tbl = GF_GetLevelTable(level_table_type); for (int32_t i = 0; i < tbl->count; i++) { GF_LEVEL *const level = &tbl->levels[i]; if (GF_GetLevelOrdinalNumber(level_table_type, level) == level_num) { return level; } } return nullptr; } const GF_LEVEL *GF_GetCurrentLevel(void) { return m_CurrentLevel; } const GF_LEVEL *GF_GetTitleLevel(void) { return g_GameFlow.title_level; } const GF_LEVEL *GF_GetGymLevel(void) { const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); for (int32_t i = 0; i < level_table->count; i++) { const GF_LEVEL *const level = &level_table->levels[i]; if (level->type == GFL_GYM && !M_SkipLevel(level)) { return level; } } return nullptr; } const GF_LEVEL *GF_GetFirstLevel(void) { const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); for (int32_t i = 0; i < level_table->count; i++) { const GF_LEVEL *const level = &level_table->levels[i]; if (level->type == GFL_GYM || M_SkipLevel(level)) { continue; } return level; } return nullptr; } const GF_LEVEL *GF_GetLastLevel(void) { const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); const GF_LEVEL *result = nullptr; for (int32_t i = 0; i < level_table->count; i++) { const GF_LEVEL *const level = &level_table->levels[i]; if (level->type == GFL_GYM || M_SkipLevel(level)) { continue; } result = level; } return result; } const GF_LEVEL *GF_GetLevel( const GF_LEVEL_TABLE_TYPE level_table_type, const int32_t num) { if (level_table_type == GFLT_TITLE) { return GF_GetTitleLevel(); } const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(level_table_type); ASSERT(level_table != nullptr); if (num < 0 || num >= level_table->count) { LOG_ERROR("Invalid cutscene number: %d", num); return nullptr; } return &level_table->levels[num]; } const GF_LEVEL *GF_GetLevelAfter(const GF_LEVEL *const level) { if (level == nullptr) { return nullptr; } const GF_LEVEL_TABLE_TYPE level_table_type = GF_GetLevelTableType(level->type); const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(level_table_type); for (int32_t i = level->num + 1; i < level_table->count; i++) { const GF_LEVEL *const next_level = &level_table->levels[i]; if (!M_SkipLevel(next_level)) { return next_level; } } return nullptr; } const GF_LEVEL *GF_GetLevelBefore(const GF_LEVEL *const level) { if (level == nullptr) { return nullptr; } const GF_LEVEL_TABLE_TYPE level_table_type = GF_GetLevelTableType(level->type); const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(level_table_type); for (int32_t i = level->num - 1; i >= 0; i--) { const GF_LEVEL *const prev_level = &level_table->levels[i]; if (!M_SkipLevel(prev_level)) { return prev_level; } } return nullptr; } void GF_SetCurrentLevel(const GF_LEVEL *const level) { m_CurrentLevel = level; } void GF_SetLevelTitle(GF_LEVEL *const level, const char *const title) { Memory_FreePointer(&level->title); level->title = title != nullptr ? Memory_DupStr(title) : nullptr; } ================================================ FILE: src/trx/game/game_flow/common.h ================================================ #pragma once #include #include void GF_Init(void); void GF_Shutdown(void); void GF_OverrideCommand(GF_COMMAND action); GF_COMMAND GF_GetOverrideCommand(void); GF_LEVEL_TABLE_TYPE GF_GetLevelTableType(const GF_LEVEL_TYPE level_type); const GF_LEVEL_TABLE *GF_GetLevelTable(GF_LEVEL_TABLE_TYPE level_type); int32_t GF_GetLevelCount(GF_LEVEL_TABLE_TYPE level_table_type); const GF_LEVEL *GF_GetCurrentLevel(void); const GF_LEVEL *GF_GetTitleLevel(void); const GF_LEVEL *GF_GetGymLevel(void); const GF_LEVEL *GF_GetFirstLevel(void); const GF_LEVEL *GF_GetLastLevel(void); const GF_LEVEL *GF_GetLevel(GF_LEVEL_TABLE_TYPE level_table_type, int32_t num); const GF_LEVEL *GF_GetLevelAfter(const GF_LEVEL *level); const GF_LEVEL *GF_GetLevelBefore(const GF_LEVEL *level); // Get human-readable number (as opposed to index), starting with 1. // Returns 0 for Gym levels and -1 for unknown levels. int32_t GF_GetLevelOrdinalNumber( GF_LEVEL_TABLE_TYPE level_table_type, const GF_LEVEL *level); // Get the level based on the human-readable number - opposite of // GF_GetLevelOrdinalNumber(). GF_LEVEL *GF_GetLevelByOrdinalNumber( GF_LEVEL_TABLE_TYPE level_table_type, int32_t level_num); void GF_SetCurrentLevel(const GF_LEVEL *level); void GF_SetLevelTitle(GF_LEVEL *level, const char *title); // Returns true if any story cutscenes or FMVs occur before gameplay in any // main level up to the level in the specified save slot. bool GF_HasAvailableStory(SAVEGAME_SLOT_REF slot); ================================================ FILE: src/trx/game/game_flow/enum.h ================================================ #pragma once typedef enum { GFLT_UNKNOWN = -1, GFLT_TITLE, GFLT_MAIN, GFLT_CUTSCENES, GFLT_DEMOS, GFLT_NUMBER_OF, } GF_LEVEL_TABLE_TYPE; typedef enum { // Genuine level types GFL_TITLE, GFL_NORMAL, GFL_CUTSCENE, GFL_DEMO, GFL_GYM, GFL_BONUS, // Legacy level types to maintain savegame backwards compatibility. // TODO: get rid of these. GFL_DUMMY, GFL_CURRENT, } GF_LEVEL_TYPE; typedef enum { GFSC_NORMAL, GFSC_SAVED, GFSC_RESTART, GFSC_SELECT, GFSC_STORY, } GF_SEQUENCE_CONTEXT; typedef enum { GF_NOOP = -1, GF_START_GAME, GF_START_CINE, GF_START_FMV, GF_START_DEMO, GF_EXIT_TO_TITLE, GF_LEVEL_COMPLETE, GF_EXIT_GAME, GF_SWITCH_MOD, GF_START_SAVED_GAME, GF_RESTART_GAME, GF_SELECT_GAME, GF_GLOBE_SELECT, GF_STORY_SO_FAR, } GF_ACTION; typedef enum { GFS_DISPLAY_PICTURE, GFS_LOOP_GAME, GFS_PLAY_CUTSCENE, GFS_PLAY_FMV, GFS_PLAY_MUSIC, GFS_EXIT_TO_TITLE, GFS_LEVEL_STATS, GFS_TOTAL_STATS, GFS_GLOBE_SELECT, GFS_LEVEL_COMPLETE, GFS_LOADING_SCREEN, GFS_ADD_ITEM, GFS_ADD_SECRET_REWARD, GFS_REMOVE_WEAPONS, GFS_REMOVE_AMMO, GFS_REMOVE_MEDIPACKS, GFS_REMOVE_FLARES, GFS_REMOVE_SCIONS, GFS_DISABLE_FLOOR, GFS_SETUP_BACON_LARA, GFS_SET_START_ANIM, GFS_ENABLE_SUNSET, GFS_NUMBER_OF, } GF_SEQUENCE_EVENT_TYPE; typedef enum { GF_DEATH_TILE_LAVA, GF_DEATH_TILE_RAPIDS, GF_DEATH_TILE_ELECTRIC, } GF_DEATH_TILE; ================================================ FILE: src/trx/game/game_flow/inventory.c ================================================ #include #include #include #include #include #include #include #include static int8_t m_SecretInvItems[O_NUMBER_OF] = {}; static int8_t m_Add2InvItems[O_NUMBER_OF] = {}; static bool m_RemoveWeapons = false; static bool m_RemoveAmmo = false; static bool m_RemoveFlares = false; static bool m_RemoveMedipacks = false; static bool m_RemoveScions = false; static bool M_CanHaveItem(const OBJECT_ID object_id) { if (Object_IsType(object_id, g_GunObjects) && object_id != O_PISTOL_ITEM && g_Config.gameplay.disable_extra_guns) { return false; } if ((object_id == O_SMALL_MEDIPACK_ITEM || object_id == O_LARGE_MEDIPACK_ITEM) && g_Config.gameplay.disable_medpacks) { return false; } return true; } static bool M_ResumeInfo_HasWeapon( const RESUME_INFO *const resume, const LARA_GUN_TYPE gun_type) { switch (gun_type) { // clang-format off case LGT_PISTOLS: return resume->flags.has_pistols; case LGT_MAGNUMS: return resume->flags.has_magnums; case LGT_AUTOS: return resume->flags.has_autos; case LGT_DESERT_EAGLE: return resume->flags.has_desert_eagle; case LGT_UZIS: return resume->flags.has_uzis; case LGT_SHOTGUN: return resume->flags.has_shotgun; case LGT_HARPOON: return resume->flags.has_harpoon; case LGT_M16: return resume->flags.has_m16; case LGT_MP5: return resume->flags.has_mp5; case LGT_GRENADE: return resume->flags.has_grenade; case LGT_ROCKET: return resume->flags.has_rocket; default: return false; // clang-format on } } static void M_ResumeInfo_SetWeapon( RESUME_INFO *const resume, const LARA_GUN_TYPE gun_type, const bool has_weapon) { switch (gun_type) { // clang-format off case LGT_PISTOLS: resume->flags.has_pistols = has_weapon; break; case LGT_MAGNUMS: resume->flags.has_magnums = has_weapon; break; case LGT_AUTOS: resume->flags.has_autos = has_weapon; break; case LGT_DESERT_EAGLE: resume->flags.has_desert_eagle = has_weapon; break; case LGT_UZIS: resume->flags.has_uzis = has_weapon; break; case LGT_SHOTGUN: resume->flags.has_shotgun = has_weapon; break; case LGT_HARPOON: resume->flags.has_harpoon = has_weapon; break; case LGT_M16: resume->flags.has_m16 = has_weapon; break; case LGT_MP5: resume->flags.has_mp5 = has_weapon; break; case LGT_GRENADE: resume->flags.has_grenade = has_weapon; break; case LGT_ROCKET: resume->flags.has_rocket = has_weapon; break; default: break; // clang-format on } } static void M_ResumeInfo_AddAmmo( RESUME_INFO *const resume, const LARA_GUN_TYPE gun_type, const int32_t ammo_qty) { switch (gun_type) { // clang-format off case LGT_MAGNUMS: resume->magnum_ammo += ammo_qty; break; case LGT_AUTOS: resume->autos_ammo += ammo_qty; break; case LGT_DESERT_EAGLE: resume->desert_eagle_ammo += ammo_qty; break; case LGT_UZIS: resume->uzi_ammo += ammo_qty; break; case LGT_SHOTGUN: resume->shotgun_ammo += ammo_qty; break; case LGT_HARPOON: resume->harpoon_ammo += ammo_qty; break; case LGT_M16: resume->m16_ammo += ammo_qty; break; case LGT_MP5: resume->mp5_ammo += ammo_qty; break; case LGT_GRENADE: resume->grenade_ammo += ammo_qty; break; case LGT_ROCKET: resume->rocket_ammo += ammo_qty; break; default: break; // clang-format on } } static void M_ResumeInfo_AddItem( RESUME_INFO *const resume, const OBJECT_ID object_id, const int32_t qty) { switch (object_id) { case O_SMALL_MEDIPACK_ITEM: case O_SMALL_MEDIPACK_OPTION: resume->small_medipacks += qty; break; case O_LARGE_MEDIPACK_ITEM: case O_LARGE_MEDIPACK_OPTION: resume->large_medipacks += qty; break; case O_FLAREBOX_ITEM: case O_FLAREBOX_OPTION: case O_FLARE_ITEM: resume->flares += qty; break; default: break; } } static void M_ModifyResumeInfo_GunOrAmmo( RESUME_INFO *const resume, const LARA_GUN_TYPE gun_type) { const OBJECT_ID gun_object_id = Gun_GetGunObject(gun_type); const OBJECT_ID ammo_object_id = Gun_GetAmmoObject(gun_type); const int32_t ammo_pickup_qty = Gun_GetAmmoPickupQuantity(gun_type); const int32_t ammo_initial_qty = Gun_GetAmmoInitialQuantity(gun_type); if (!M_CanHaveItem(gun_object_id) || !M_CanHaveItem(ammo_object_id)) { return; } M_ResumeInfo_AddAmmo( resume, gun_type, ammo_pickup_qty * m_Add2InvItems[ammo_object_id]); if (!M_ResumeInfo_HasWeapon(resume, gun_type) && m_Add2InvItems[gun_object_id] > 0) { M_ResumeInfo_SetWeapon(resume, gun_type, true); M_ResumeInfo_AddAmmo(resume, gun_type, ammo_initial_qty); } } static void M_ModifyResumeInfo_Item( RESUME_INFO *const resume, const OBJECT_ID object_id) { if (!M_CanHaveItem(object_id)) { return; } M_ResumeInfo_AddItem(resume, object_id, m_Add2InvItems[object_id]); } static void M_CollectNewPickup(const OBJECT_ID object_id) { Overlay_AddDisplayPickup(object_id); Stats_AddPickup(); } static void M_ModifyInventory_GunOrAmmo( const GF_INV_TYPE type, const LARA_GUN_TYPE gun_type) { const OBJECT_ID gun_object_id = Gun_GetGunObject(gun_type); const OBJECT_ID ammo_object_id = Gun_GetAmmoObject(gun_type); const int32_t ammo_pickup_qty = Gun_GetAmmoPickupQuantity(gun_type); const int32_t ammo_initial_qty = Gun_GetAmmoInitialQuantity(gun_type); AMMO_INFO *const ammo_info = Gun_GetAmmoInfo(gun_type); if (!M_CanHaveItem(gun_object_id) || !M_CanHaveItem(ammo_object_id)) { return; } if (Inv_RequestItem(gun_object_id)) { if (type == GF_INV_SECRET) { // Convert already collected guns into ammo to maintain stats // accuracy. ammo_info->ammo += ammo_pickup_qty * m_SecretInvItems[ammo_object_id]; ammo_info->ammo += ammo_initial_qty * m_SecretInvItems[gun_object_id]; for (int32_t i = 0; i < m_SecretInvItems[ammo_object_id]; i++) { M_CollectNewPickup(ammo_object_id); } for (int32_t i = 0; i < m_SecretInvItems[gun_object_id]; i++) { M_CollectNewPickup(ammo_object_id); } } else if (type == GF_INV_REGULAR) { ammo_info->ammo += ammo_pickup_qty * m_Add2InvItems[ammo_object_id]; } } else if ( (type == GF_INV_REGULAR && m_Add2InvItems[gun_object_id] > 0) || (type == GF_INV_SECRET && m_SecretInvItems[gun_object_id] > 0)) { Inv_AddItem(gun_object_id); if (type == GF_INV_SECRET) { ammo_info->ammo += ammo_pickup_qty * m_SecretInvItems[ammo_object_id]; M_CollectNewPickup(gun_object_id); for (int32_t i = 0; i < m_SecretInvItems[ammo_object_id]; i++) { M_CollectNewPickup(ammo_object_id); } } else if (type == GF_INV_REGULAR) { ammo_info->ammo += ammo_pickup_qty * m_Add2InvItems[ammo_object_id]; } } else if (type == GF_INV_SECRET) { for (int32_t i = 0; i < m_SecretInvItems[ammo_object_id]; i++) { Inv_AddItem(ammo_object_id); M_CollectNewPickup(ammo_object_id); } } else if (type == GF_INV_REGULAR) { for (int32_t i = 0; i < m_Add2InvItems[ammo_object_id]; i++) { Inv_AddItem(ammo_object_id); } } } static void M_ModifyInventory_Item( const GF_INV_TYPE type, const OBJECT_ID object_id) { int32_t qty = 0; if (type == GF_INV_SECRET) { qty = m_SecretInvItems[object_id]; } else if (type == GF_INV_REGULAR) { qty = m_Add2InvItems[object_id]; } // Check for gameplay mods from secret rewards if (!M_CanHaveItem(object_id)) { qty = 0; } for (int32_t i = 0; i < qty; i++) { if (Inv_AddItem(object_id) && type == GF_INV_SECRET) { M_CollectNewPickup(object_id); } } } void GF_InventoryModifier_Scan(const GF_LEVEL *const level) { for (int32_t i = O_FIRST; i < O_NUMBER_OF; i++) { m_SecretInvItems[i] = 0; m_Add2InvItems[i] = 0; } m_RemoveWeapons = false; m_RemoveAmmo = false; m_RemoveFlares = false; m_RemoveMedipacks = false; m_RemoveScions = false; if (level == nullptr) { return; } for (int32_t i = 0; i < level->sequence.length; i++) { const GF_SEQUENCE_EVENT *const event = &level->sequence.events[i]; if (event->type == GFS_ADD_ITEM || event->type == GFS_ADD_SECRET_REWARD) { const GF_ADD_ITEM_DATA *const data = event->data; if (data->object_id < O_FIRST || data->object_id >= O_NUMBER_OF) { continue; } if (data->inv_type == GF_INV_SECRET) { m_SecretInvItems[data->object_id] += data->quantity; } else if (data->inv_type == GF_INV_REGULAR) { m_Add2InvItems[data->object_id] += data->quantity; } } else if (event->type == GFS_REMOVE_WEAPONS) { m_RemoveWeapons = true; } else if (event->type == GFS_REMOVE_AMMO) { m_RemoveAmmo = true; } else if (event->type == GFS_REMOVE_FLARES) { m_RemoveFlares = true; } else if (event->type == GFS_REMOVE_MEDIPACKS) { m_RemoveMedipacks = true; } else if (event->type == GFS_REMOVE_SCIONS) { m_RemoveScions = true; } } } int32_t GF_GetSecretRewardCount(const GF_LEVEL *const level) { int32_t sum = 0; if (level == nullptr) { return sum; } for (int32_t i = 0; i < level->sequence.length; i++) { const GF_SEQUENCE_EVENT *const event = &level->sequence.events[i]; if (event->type == GFS_ADD_SECRET_REWARD) { const GF_ADD_ITEM_DATA *const data = event->data; if (data->inv_type == GF_INV_SECRET) { sum += data->quantity; } } } return sum; } void GF_InventoryModifier_ApplyToResumeInfo(const GF_LEVEL *const level) { RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); if (m_RemoveWeapons) { resume->flags.has_pistols = false; resume->flags.has_magnums = false; resume->flags.has_autos = false; resume->flags.has_desert_eagle = false; resume->flags.has_uzis = false; resume->flags.has_shotgun = false; resume->flags.has_m16 = false; resume->flags.has_mp5 = false; resume->flags.has_grenade = false; resume->flags.has_rocket = false; resume->flags.has_harpoon = false; resume->holsters_gun_type = LGT_UNARMED; resume->back_gun_type = LGT_UNARMED; resume->equipped_gun_type = LGT_UNARMED; resume->gun_status = LGS_ARMLESS; } if (!resume->flags.has_pistols && m_Add2InvItems[O_PISTOL_ITEM]) { resume->flags.has_pistols = true; if (resume->equipped_gun_type == LGT_UNARMED) { resume->equipped_gun_type = LGT_PISTOLS; } } if (m_RemoveAmmo) { resume->pistol_ammo = 0; resume->magnum_ammo = 0; resume->autos_ammo = 0; resume->desert_eagle_ammo = 0; resume->uzi_ammo = 0; resume->shotgun_ammo = 0; resume->m16_ammo = 0; resume->mp5_ammo = 0; resume->grenade_ammo = 0; resume->rocket_ammo = 0; resume->harpoon_ammo = 0; } if (m_RemoveScions) { resume->num_scions = 0; resume->num_quest_item_1 = 0; resume->num_quest_item_2 = 0; resume->num_quest_item_3 = 0; resume->num_quest_item_4 = 0; m_RemoveScions = false; } if (m_RemoveFlares) { resume->flares = 0; m_RemoveFlares = false; } if (m_RemoveMedipacks) { resume->large_medipacks = 0; resume->small_medipacks = 0; m_RemoveMedipacks = false; } M_ModifyResumeInfo_GunOrAmmo(resume, LGT_PISTOLS); M_ModifyResumeInfo_GunOrAmmo(resume, LGT_MAGNUMS); M_ModifyResumeInfo_GunOrAmmo(resume, LGT_AUTOS); M_ModifyResumeInfo_GunOrAmmo(resume, LGT_DESERT_EAGLE); M_ModifyResumeInfo_GunOrAmmo(resume, LGT_UZIS); M_ModifyResumeInfo_GunOrAmmo(resume, LGT_SHOTGUN); M_ModifyResumeInfo_GunOrAmmo(resume, LGT_HARPOON); M_ModifyResumeInfo_GunOrAmmo(resume, LGT_M16); M_ModifyResumeInfo_GunOrAmmo(resume, LGT_MP5); M_ModifyResumeInfo_GunOrAmmo(resume, LGT_GRENADE); M_ModifyResumeInfo_GunOrAmmo(resume, LGT_ROCKET); M_ModifyResumeInfo_Item(resume, O_SMALL_MEDIPACK_ITEM); M_ModifyResumeInfo_Item(resume, O_LARGE_MEDIPACK_ITEM); M_ModifyResumeInfo_Item(resume, O_FLARE_ITEM); } void GF_InventoryModifier_Apply( const GF_LEVEL *const level, const GF_INV_TYPE type) { RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); // For GF_INV_REGULAR, we must ignore weapons, ammo, medpacks and flares, // as these are handled by RESUME_INFO and // GF_InventoryModifier_ApplyToResumeInfo and Lara_InitialiseInventory. if (type == GF_INV_SECRET) { if (m_Add2InvItems[O_PISTOL_ITEM]) { Inv_AddItem(O_PISTOL_ITEM); if (resume->equipped_gun_type == LGT_UNARMED) { resume->equipped_gun_type = LGT_PISTOLS; } } M_ModifyInventory_GunOrAmmo(type, LGT_MAGNUMS); M_ModifyInventory_GunOrAmmo(type, LGT_AUTOS); M_ModifyInventory_GunOrAmmo(type, LGT_DESERT_EAGLE); M_ModifyInventory_GunOrAmmo(type, LGT_UZIS); M_ModifyInventory_GunOrAmmo(type, LGT_SHOTGUN); M_ModifyInventory_GunOrAmmo(type, LGT_HARPOON); M_ModifyInventory_GunOrAmmo(type, LGT_M16); M_ModifyInventory_GunOrAmmo(type, LGT_MP5); M_ModifyInventory_GunOrAmmo(type, LGT_GRENADE); M_ModifyInventory_GunOrAmmo(type, LGT_ROCKET); } M_ModifyInventory_Item(type, O_PICKUP_ITEM_1); M_ModifyInventory_Item(type, O_PICKUP_ITEM_2); M_ModifyInventory_Item(type, O_PUZZLE_ITEM_1); M_ModifyInventory_Item(type, O_PUZZLE_ITEM_2); M_ModifyInventory_Item(type, O_PUZZLE_ITEM_3); M_ModifyInventory_Item(type, O_PUZZLE_ITEM_4); M_ModifyInventory_Item(type, O_KEY_ITEM_1); M_ModifyInventory_Item(type, O_KEY_ITEM_2); M_ModifyInventory_Item(type, O_KEY_ITEM_3); M_ModifyInventory_Item(type, O_KEY_ITEM_4); M_ModifyInventory_Item(type, O_LEADBAR_ITEM); M_ModifyInventory_Item(type, O_SCION_ITEM_1); M_ModifyInventory_Item(type, O_SCION_ITEM_2); if (type == GF_INV_SECRET) { M_ModifyInventory_Item(type, O_SMALL_MEDIPACK_ITEM); M_ModifyInventory_Item(type, O_LARGE_MEDIPACK_ITEM); M_ModifyInventory_Item(type, O_FLARE_ITEM); } } ================================================ FILE: src/trx/game/game_flow/inventory.h ================================================ #pragma once #include void GF_InventoryModifier_Scan(const GF_LEVEL *level); void GF_InventoryModifier_Apply(const GF_LEVEL *level, GF_INV_TYPE type); void GF_InventoryModifier_ApplyToResumeInfo(const GF_LEVEL *level); int32_t GF_GetSecretRewardCount(const GF_LEVEL *level); ================================================ FILE: src/trx/game/game_flow/reader.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef struct { GAME_FLOW *gf; const char *script_path; JSON_READ_IO *io; bool validation_mode; } M_CONTEXT; #define M_DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(name) \ int32_t name( \ const M_CONTEXT *ctx, GF_SEQUENCE_EVENT *event, void *extra_data, \ void *user_arg) typedef int32_t (*M_SEQUENCE_EVENT_HANDLER_FUNC)( const M_CONTEXT *ctx, GF_SEQUENCE_EVENT *event, void *extra_data, void *user_arg); typedef struct { GF_SEQUENCE_EVENT_TYPE event_type; M_SEQUENCE_EVENT_HANDLER_FUNC handler_func; void *handler_func_arg; } M_SEQUENCE_EVENT_HANDLER; typedef bool (*M_LOAD_ARRAY_FUNC)( const M_CONTEXT *ctx, void *target_elem, size_t target_elem_idx, void *user_arg); static M_DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandleIntEvent); static M_DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandlePictureEvent); static M_DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandleTotalStatsEvent); static M_DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandleAddItemEvent); static M_DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandleGlobeSelectEvent); static M_SEQUENCE_EVENT_HANDLER m_SequenceEventHandlers[] = { // clang-format off // Events without arguments { GFS_ENABLE_SUNSET, nullptr, nullptr }, { GFS_REMOVE_WEAPONS, nullptr, nullptr }, { GFS_REMOVE_SCIONS, nullptr, nullptr }, { GFS_REMOVE_AMMO, nullptr, nullptr }, { GFS_REMOVE_FLARES, nullptr, nullptr }, { GFS_REMOVE_MEDIPACKS, nullptr, nullptr }, { GFS_LEVEL_COMPLETE, nullptr, nullptr }, { GFS_LEVEL_STATS, nullptr, nullptr }, { GFS_EXIT_TO_TITLE, nullptr, nullptr }, { GFS_GLOBE_SELECT, M_HandleGlobeSelectEvent, nullptr }, // Events with integer arguments { GFS_SET_START_ANIM, M_HandleIntEvent, "anim" }, { GFS_LOOP_GAME, M_HandleIntEvent, "level_id" }, { GFS_PLAY_CUTSCENE, M_HandleIntEvent, "cutscene_id" }, { GFS_PLAY_FMV, M_HandleIntEvent, "fmv_id" }, { GFS_PLAY_MUSIC, M_HandleIntEvent, "music_track" }, { GFS_SETUP_BACON_LARA, M_HandleIntEvent, "anchor_room" }, { GFS_DISABLE_FLOOR, M_HandleIntEvent, "height" }, // Special cases with custom handlers { GFS_LOADING_SCREEN, M_HandlePictureEvent, nullptr }, { GFS_DISPLAY_PICTURE, M_HandlePictureEvent, nullptr }, { GFS_TOTAL_STATS, M_HandleTotalStatsEvent, nullptr }, { GFS_ADD_ITEM, M_HandleAddItemEvent, nullptr }, { GFS_ADD_SECRET_REWARD, M_HandleAddItemEvent, nullptr }, // Sentinel to mark the end of the table { (GF_SEQUENCE_EVENT_TYPE)-1, nullptr, nullptr }, // clang-format on }; static void M_ExitWithJSONError(const M_CONTEXT *const ctx) { JSONFile_ExitWithReadIOError( ctx->io, String_FormatStatic("%s: gameflow parse error", ctx->script_path)); } static bool M_ReadObjectID( const M_CONTEXT *const ctx, OBJECT_ID *const object_id_out) { int32_t game_id; if (JSON_OPTIONAL(JSON_READ_CURRENT(ctx->io, &game_id))) { *object_id_out = Object_FromGameID(game_id); } else { const char *object_key; JSON_MUST(JSON_READ_CURRENT(ctx->io, &object_key)); *object_id_out = Object_IdFromKey(object_key); } if (!ctx->validation_mode && *object_id_out == NO_OBJECT) { JSON_ReadIO_SetError(ctx->io, "'object_id' must be a valid object id"); JSON_FAIL(); } JSON_FINISH(); } static M_SEQUENCE_EVENT_HANDLER *M_GetSequenceEventHandlers(void) { return m_SequenceEventHandlers; } // Read a "path" value that may be either a plain string or an array of // candidate strings tried in order. // Pass optional=true to allow the key to be absent (out_path is set to nullptr // in that case). // Pass path_type=(TRX_DYNAMIC_PATH)-1 to skip resolution and return the first // candidate as-is. static bool M_ReadPath( JSON_READ_IO *const io, const char *const key, const bool optional, const TRX_DYNAMIC_PATH path_type, char **const out_path, const bool suppress_errors) { ASSERT(key != nullptr); *out_path = nullptr; const bool resolve = path_type != (TRX_DYNAMIC_PATH)-1; if (!JSON_PUSH(io, key)) { if (suppress_errors) { return true; } if (optional) { return true; } return false; } // All failure paths below must pop before returning to keep the // push/pop depth balanced and avoid corrupting the parser state. bool ok = false; JSON_ARRAY *const path_array = JSON_ValueAsArray(JSON_ReadIO_GetCurrentValue(io)); const int32_t count = path_array != nullptr ? JSON_ARRAY_LEN(io) : 1; if (count <= 0) { if (!suppress_errors) { JSON_ReadIO_SetError( io, "path array must contain at least one entry"); } } else { for (int32_t i = 0; i < count; i++) { const char *path = nullptr; const bool read_ok = path_array != nullptr ? JSON_READ_A(io, i, &path) : JSON_READ_CURRENT(io, &path); if (!read_ok) { break; } if (!resolve) { *out_path = Memory_DupStr(path); ok = true; break; } const char *const resolved = TRXPath_PeekResolve(path_type, path); if (resolved != nullptr) { *out_path = Memory_DupStr(resolved); ok = true; break; } } if (!ok && resolve) { if (optional) { ok = true; // leave *out_path as nullptr } else if (suppress_errors) { ok = true; // sizing pass should not surface path failures } else { JSON_ReadIO_SetError( io, "failed to resolve any path candidate"); } } else if (!ok && suppress_errors) { ok = true; // sizing pass should not surface malformed path values } } if (!JSON_POP(io)) { return false; } return ok; } static bool M_LoadSettings( const M_CONTEXT *const ctx, GF_LEVEL_SETTINGS *const settings) { JSON_READ_IO *const io = ctx->io; if (JSON_OPTIONAL(JSON_PUSH(io, "fog_start"))) { JSON_MUST(JSON_READ_CURRENT(io, &settings->fog_start.value)); settings->fog_start.is_present = true; JSON_MUST(JSON_POP(io)); } if (JSON_OPTIONAL(JSON_PUSH(io, "fog_end"))) { JSON_MUST(JSON_READ_CURRENT(io, &settings->fog_end.value)); settings->fog_end.is_present = true; JSON_MUST(JSON_POP(io)); } if (JSON_OPTIONAL(JSON_PUSH(io, "fog_transparency"))) { JSON_MUST(JSON_READ_CURRENT(io, &settings->fog_transparency.value)); settings->fog_transparency.is_present = true; JSON_MUST(JSON_POP(io)); } if (JSON_OPTIONAL(JSON_PUSH(io, "fog_color"))) { JSON_MUST(JSON_READ_CURRENT(io, &settings->fog_color.value)); settings->fog_color.is_present = true; JSON_MUST(JSON_POP(io)); } if (JSON_OPTIONAL(JSON_PUSH(io, "water_color"))) { JSON_MUST(JSON_READ_CURRENT(io, &settings->water_color.value)); settings->water_color.is_present = true; JSON_MUST(JSON_POP(io)); } if (JSON_OPTIONAL(JSON_PUSH(io, "cold_water"))) { JSON_MUST(JSON_READ_CURRENT(io, &settings->cold_water.value)); settings->cold_water.is_present = true; JSON_MUST(JSON_POP(io)); } if (JSON_OPTIONAL(JSON_PUSH(io, "death_tile"))) { const char *tmp_s = nullptr; JSON_MUST(JSON_READ_CURRENT(io, &tmp_s)); const int32_t value = EnumMap_Get(ENUM_MAP_NAME(GF_DEATH_TILE), tmp_s, -1); if (value < 0) { JSON_ReadIO_SetError(io, "Invalid death_tile value '%s'", tmp_s); JSON_FAIL(); } settings->death_tile.is_present = true; settings->death_tile.value = value; JSON_MUST(JSON_POP(io)); } if (JSON_OPTIONAL(JSON_PUSH(io, "sfx_path"))) { JSON_MUST(JSON_POP(io)); JSON_SHOULD(M_ReadPath( io, "sfx_path", false, TRX_DYNAMIC_PATH_SFX_FILE, &settings->sfx_path, false)); } JSON_FINISH(); } static bool M_LoadLevelItemDrops( const M_CONTEXT *const ctx, GF_LEVEL *const level) { JSON_READ_IO *const io = ctx->io; level->item_drops.count = 0; if (!JSON_OPTIONAL(JSON_PUSH(io, "item_drops"))) { return true; } if (ctx->gf->enable_tr2_item_drops) { LOG_WARNING( "TR2 item drops are enabled: gameflow-defined drops for level " "%d will be ignored", level->num); JSON_MUST(JSON_POP(io)); return true; } level->item_drops.count = JSON_ARRAY_LEN(io); if (level->item_drops.count < 0) { JSON_FAIL(); } level->item_drops.data = Memory_Alloc( sizeof(GF_DROP_ITEM_DATA) * (size_t)level->item_drops.count); for (int32_t i = 0; i < level->item_drops.count; i++) { JSON_MUST(JSON_PUSH_INDEX(io, i)); GF_DROP_ITEM_DATA *data = &level->item_drops.data[i]; JSON_MUST(JSON_READ(io, "enemy_num", &data->enemy_num)); JSON_MUST(JSON_PUSH(io, "object_ids")); const int32_t object_count = JSON_ARRAY_LEN(io); if (object_count < 0) { JSON_FAIL(); } data->count = object_count; data->object_ids = Memory_Alloc(sizeof(int16_t) * data->count); for (int32_t j = 0; j < data->count; j++) { JSON_MUST(JSON_PUSH_INDEX(io, j)); OBJECT_ID id = NO_OBJECT; JSON_MUST(M_ReadObjectID(ctx, &id)); data->object_ids[j] = (int16_t)id; JSON_MUST(JSON_POP(io)); } JSON_MUST(JSON_POP(io)); JSON_MUST(JSON_POP(io)); } JSON_MUST(JSON_POP(io)); JSON_FINISH(); } static void M_CopyRootSettingsIntoLevel( GF_LEVEL_SETTINGS *const dst, const GF_LEVEL_SETTINGS *const src) { *dst = *src; dst->sfx_path = src->sfx_path != nullptr ? Memory_DupStr(src->sfx_path) : nullptr; } static void M_ReadModMeta(JSON_READ_IO *const io, GF_MOD_META *const meta) { meta->name = nullptr; meta->engine = 0; meta->extends = nullptr; const char *tmp_s = nullptr; if (JSON_OPTIONAL(JSON_READ(io, "name", &tmp_s)) && tmp_s != nullptr) { meta->name = Memory_DupStr(tmp_s); } JSON_OPTIONAL(JSON_READ(io, "engine", &meta->engine)); tmp_s = nullptr; if (JSON_OPTIONAL(JSON_READ(io, "extends", &tmp_s)) && tmp_s != nullptr) { meta->extends = Memory_DupStr(tmp_s); } } static bool M_LoadRoot(const M_CONTEXT *const ctx) { JSON_READ_IO *const io = ctx->io; const char *tmp_s = nullptr; M_ReadModMeta(io, &ctx->gf->meta); JSON_MUST(JSON_READ(io, "main_menu_picture", &tmp_s)); ctx->gf->main_menu_background_path = Memory_DupStr(TRXPath_TryResolve(TRX_DYNAMIC_PATH_IMAGE_FILE, tmp_s)); if (!JSON_READ(io, "savegame_file_fmt", &tmp_s) || tmp_s == nullptr) { if (!JSON_READ(io, "savegame_fmt_bson", &tmp_s) || tmp_s == nullptr) { JSON_FAIL(); } // TODO: remove in TRX 1.5. LOG_WARNING( "%s: 'savegame_fmt_bson' is deprecated; use 'savegame_file_fmt'", ctx->script_path); } ctx->gf->savegame_file_fmt = Memory_DupStr(tmp_s); tmp_s = nullptr; if (JSON_OPTIONAL(JSON_READ(io, "main_script", &tmp_s)) && tmp_s != nullptr) { ctx->gf->main_script_path = Memory_DupStr( TRXPath_TryResolve(TRX_DYNAMIC_PATH_SCRIPT_FILE, tmp_s)); } if (JSON_PUSH(io, "ambient_tracks")) { const int32_t count = JSON_ARRAY_LEN(io); if (count < 0) { JSON_FAIL(); } if (count > 0) { ctx->gf->ambient_tracks.is_present = true; ctx->gf->ambient_tracks.count = count; ctx->gf->ambient_tracks.ids = Memory_Alloc(sizeof(MUSIC_ID) * (size_t)count); for (int32_t i = 0; i < count; i++) { int32_t track = MX_INACTIVE; JSON_SHOULD(JSON_READ_A(io, i, &track)); ctx->gf->ambient_tracks.ids[i] = track; } } JSON_MUST(JSON_POP(io)); } ctx->gf->enable_tr2_item_drops = g_TRVersion > 1; JSON_READ_D( io, "enable_tr2_item_drops", &ctx->gf->enable_tr2_item_drops, g_TRVersion > 1); JSON_READ_D( io, "convert_dropped_guns", &ctx->gf->convert_dropped_guns, g_TRVersion > 1); JSON_FINISH(); } static bool M_LoadGlobeEntry( const M_CONTEXT *const ctx, void *const target_elem, size_t idx, void *const user_arg) { GF_GLOBE_ENTRY *const e = target_elem; ASSERT(user_arg == nullptr); JSON_READ_IO *const io = ctx->io; JSON_MUST(JSON_READ(io, "rot", &e->rot)); JSON_MUST(JSON_READ(io, "start_level_ordinal", &e->start_level_ordinal)); JSON_MUST(JSON_READ( io, "completion_level_ordinal", &e->completion_level_ordinal)); JSON_MUST(JSON_PUSH(io, "prereq_zones")); const int32_t prereq_count = JSON_ARRAY_LEN(io); if (prereq_count < 0) { JSON_FAIL(); } uint32_t computed_prereq_mask = 0; for (int32_t i = 0; i < prereq_count; i++) { int32_t zone = JSON_INVALID_NUMBER; JSON_MUST(JSON_READ_A(io, i, &zone)); if (zone < 0 || zone >= MAX_GLOBE_ZONES) { JSON_ReadIO_SetError( io, "'prereq_zones' entries must be in range 0..%d", MAX_GLOBE_ZONES - 1); JSON_FAIL(); } computed_prereq_mask |= 1u << zone; } e->prereq_mask = computed_prereq_mask; JSON_MUST(JSON_POP(io)); int32_t mesh_idx = JSON_INVALID_NUMBER; JSON_MUST(JSON_READ(io, "mesh_idx", &mesh_idx)); e->mesh_idx = (uint8_t)mesh_idx; JSON_FINISH(); } static M_DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandleIntEvent) { JSON_READ_IO *const io = ctx->io; if (event != nullptr) { int32_t value; JSON_READ_D(io, user_arg, &value, -1); event->data = (void *)(intptr_t)value; } return 0; } static M_DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandlePictureEvent) { JSON_READ_IO *const io = ctx->io; char *expanded_path = nullptr; JSON_SHOULD(M_ReadPath( io, "path", false, TRX_DYNAMIC_PATH_IMAGE_FILE, &expanded_path, event == nullptr)); if (event != nullptr) { GF_DISPLAY_PICTURE_DATA *const event_data = extra_data; JSON_READ_D(io, "legal", &event_data->is_legal, false); JSON_READ_D(io, "credit", &event_data->is_credit, false); JSON_READ_D(io, "display_time", &event_data->display_time, 5.0f); JSON_READ_D(io, "fade_in_time", &event_data->fade_in_time, 1.0f); JSON_READ_D( io, "fade_out_time", &event_data->fade_out_time, 1.0f / 3.0f); if (expanded_path != nullptr) { event_data->path = (char *)extra_data + sizeof(GF_DISPLAY_PICTURE_DATA); strcpy(event_data->path, expanded_path); } else { event_data->path = nullptr; } event->data = event_data; } const int32_t out_size = sizeof(GF_DISPLAY_PICTURE_DATA) + (expanded_path == nullptr ? 0 : strlen(expanded_path) + 1); Memory_FreePointer(&expanded_path); return out_size; fail: Memory_FreePointer(&expanded_path); return 0; } static M_DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandleTotalStatsEvent) { JSON_READ_IO *const io = ctx->io; char *expanded_path = nullptr; JSON_SHOULD(M_ReadPath( io, "background_path", false, TRX_DYNAMIC_PATH_IMAGE_FILE, &expanded_path, event == nullptr)); if (expanded_path == nullptr) { if (event != nullptr) { event->data = nullptr; } return 0; } if (event != nullptr) { char *const event_data = extra_data; strcpy(event_data, expanded_path); event->data = event_data; } const int32_t out_size = strlen(expanded_path) + 1; Memory_FreePointer(&expanded_path); return out_size; } static M_DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandleAddItemEvent) { JSON_READ_IO *const io = ctx->io; OBJECT_ID obj_id = NO_OBJECT; JSON_MUST(JSON_PUSH(io, "object_id")); JSON_MUST(M_ReadObjectID(ctx, &obj_id)); JSON_MUST(JSON_POP(io)); if (event != nullptr) { GF_ADD_ITEM_DATA *const event_data = extra_data; event_data->object_id = obj_id; JSON_READ_D(io, "quantity", &event_data->quantity, 1); event_data->inv_type = event->type == GFS_ADD_ITEM ? GF_INV_REGULAR : GF_INV_SECRET; event->data = event_data; } return sizeof(GF_ADD_ITEM_DATA); fail: return -1; } static M_DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandleGlobeSelectEvent) { JSON_READ_IO *const io = ctx->io; const char *image; JSON_READ_D(io, "image", &image, nullptr); char *expanded_image = Memory_DupStr(TRXPath_TryResolve(TRX_DYNAMIC_PATH_IMAGE_FILE, image)); if (expanded_image == nullptr) { if (event != nullptr) { event->data = nullptr; } return 0; } if (event != nullptr) { GF_GLOBE_SELECT_DATA *const event_data = extra_data; event_data->image_path = (char *)extra_data + sizeof(GF_GLOBE_SELECT_DATA); strcpy(event_data->image_path, expanded_image); event->data = event_data; } const int32_t out_size = sizeof(GF_GLOBE_SELECT_DATA) + strlen(expanded_image) + 1; Memory_FreePointer(&expanded_image); return out_size; } static bool M_LoadArray( const M_CONTEXT *const ctx, const char *const key, int32_t *const count, void **const elements, const size_t element_size, const M_LOAD_ARRAY_FUNC load_func, void *const load_func_arg) { if (!JSON_OPTIONAL(JSON_PUSH(ctx->io, key))) { return true; } const int32_t elem_count = JSON_ARRAY_LEN(ctx->io); if (elem_count < 0) { JSON_FAIL(); } *count = elem_count; *elements = Memory_Alloc(element_size * (size_t)(*count)); for (size_t i = 0; i < (size_t)elem_count; i++) { void *const element = (char *)*elements + i * element_size; JSON_MUST(JSON_PUSH_INDEX(ctx->io, i)); JSON_MUST(load_func(ctx, element, i, load_func_arg)); JSON_MUST(JSON_POP(ctx->io)); } JSON_MUST(JSON_POP(ctx->io)); JSON_FINISH(); } static bool M_LoadGlobeSelectEntries(const M_CONTEXT *const ctx) { ctx->gf->globe.count = 0; ctx->gf->globe.entries = nullptr; return M_LoadArray( ctx, "globe_select_entries", &ctx->gf->globe.count, (void **)&ctx->gf->globe.entries, sizeof(GF_GLOBE_ENTRY), M_LoadGlobeEntry, nullptr); } static int32_t M_LoadSequenceEvent( const M_CONTEXT *const ctx, GF_SEQUENCE_EVENT *const event, void *const extra_data) { const char *type_str = nullptr; JSON_MUST(JSON_READ(ctx->io, "type", &type_str)); const GF_SEQUENCE_EVENT_TYPE type = ENUM_MAP_GET(GF_SEQUENCE_EVENT_TYPE, type_str, -1); if (type == (GF_SEQUENCE_EVENT_TYPE)-1) { JSON_ReadIO_SetError( ctx->io, "unknown game flow sequence event type: '%s'", type_str); JSON_FAIL(); } const M_SEQUENCE_EVENT_HANDLER *handler = M_GetSequenceEventHandlers(); while (handler->event_type != (GF_SEQUENCE_EVENT_TYPE)-1 && handler->event_type != type) { handler++; } int32_t extra_data_size = 0; if (handler->handler_func != nullptr) { extra_data_size = handler->handler_func( ctx, nullptr, nullptr, handler->handler_func_arg); } if (extra_data_size >= 0 && event != nullptr) { event->type = handler->event_type; if (handler->handler_func != nullptr) { handler->handler_func( ctx, event, extra_data, handler->handler_func_arg); } else { event->data = nullptr; } } return extra_data_size; fail: return -1; } static bool M_LoadSequence( const M_CONTEXT *const ctx, GF_SEQUENCE *const sequence) { JSON_READ_IO *const io = ctx->io; sequence->length = 0; const int32_t seq_count = JSON_ARRAY_LEN(io); if (seq_count < 0) { JSON_FAIL(); } const size_t event_base_size = sizeof(GF_SEQUENCE_EVENT); size_t total_data_size = 0; for (int32_t i = 0; i < seq_count; i++) { JSON_MUST(JSON_PUSH_INDEX(io, i)); const int32_t event_extra_size = M_LoadSequenceEvent(ctx, nullptr, nullptr); JSON_MUST(JSON_POP(io)); if (event_extra_size < 0) { JSON_FAIL(); } sequence->length++; total_data_size += Memory_Align((size_t)event_extra_size); } const size_t events_block_size = Memory_Align((size_t)sequence->length * event_base_size); total_data_size += events_block_size; char *const data = Memory_Alloc(total_data_size); char *extra_data_ptr = data + events_block_size; sequence->events = (GF_SEQUENCE_EVENT *)data; int32_t j = 0; for (int32_t i = 0; i < seq_count; i++) { JSON_MUST(JSON_PUSH_INDEX(io, i)); const int32_t event_extra_size = M_LoadSequenceEvent(ctx, &sequence->events[j], extra_data_ptr); JSON_MUST(JSON_POP(io)); if (event_extra_size < 0) { // Parsing this event failed - discard it continue; } extra_data_ptr += Memory_Align((size_t)event_extra_size); j++; } JSON_FINISH(); } static bool M_LoadLevelInjections( const M_CONTEXT *const ctx, GF_LEVEL *const level) { JSON_READ_IO *const io = ctx->io; bool inherit; JSON_READ_D(io, "inherit_injections", &inherit, true); int32_t local_count = 0; if (JSON_PUSH(io, "injections")) { local_count = JSON_ARRAY_LEN(io); if (local_count < 0) { JSON_FAIL(); } JSON_MUST(JSON_POP(io)); } level->injections.count = 0; if (local_count == 0 && !inherit) { return true; } if (inherit) { level->injections.count += ctx->gf->injections.count; } level->injections.count += local_count; level->injections.data_paths = Memory_Alloc(sizeof(char *) * level->injections.count); int32_t base_index = 0; if (inherit) { for (int32_t i = 0; i < ctx->gf->injections.count; i++) { level->injections.data_paths[i] = Memory_DupStr(ctx->gf->injections.data_paths[i]); } base_index = ctx->gf->injections.count; } if (local_count == 0) { return true; } JSON_MUST(JSON_PUSH(io, "injections")); for (int32_t i = 0; i < local_count; i++) { const char *str = nullptr; JSON_MUST(JSON_READ_A(io, i, &str)); level->injections.data_paths[base_index + i] = Memory_DupStr( TRXPath_TryResolve(TRX_DYNAMIC_PATH_INJECTION_FILE, str)); } JSON_MUST(JSON_POP(io)); JSON_FINISH(); } static bool M_LoadLevelSequence( const M_CONTEXT *const ctx, GF_LEVEL *const level) { JSON_MUST(JSON_PUSH(ctx->io, "sequence")); JSON_MUST(M_LoadSequence(ctx, &level->sequence)); JSON_MUST(JSON_POP(ctx->io)); for (int32_t i = 0; i < level->sequence.length; i++) { GF_SEQUENCE_EVENT *const event = &level->sequence.events[i]; if (event->type == GFS_LOOP_GAME) { event->data = (void *)(intptr_t)level->num; } } JSON_FINISH(); } static bool M_LoadLevel( const M_CONTEXT *const ctx, void *const target_elem, const size_t idx, void *const user_arg) { GF_LEVEL *const level = target_elem; JSON_READ_IO *const io = ctx->io; level->num = idx; { level->type = (GF_LEVEL_TYPE)(intptr_t)user_arg; if (JSON_OPTIONAL(JSON_PUSH(io, "type"))) { const char *tmp = nullptr; JSON_MUST(JSON_READ_CURRENT(io, &tmp)); JSON_MUST(JSON_POP(io)); const GF_LEVEL_TYPE user_type = ENUM_MAP_GET(GF_LEVEL_TYPE, tmp, -1); if (user_type == (GF_LEVEL_TYPE)-1) { JSON_ReadIO_SetError(io, "unrecognized type '%s'", tmp); JSON_FAIL(); } if (level->type != GFL_NORMAL && GF_GetLevelTableType(user_type) != GFLT_MAIN) { JSON_ReadIO_SetError( io, "cannot override level type=%s to %s", ENUM_MAP_TO_STRING(GF_LEVEL_TYPE, level->type), ENUM_MAP_TO_STRING(GF_LEVEL_TYPE, user_type)); JSON_FAIL(); } level->type = user_type; } } if (level->type == GFL_DUMMY) { return true; } { const TRX_DYNAMIC_PATH path_type = (level->type == GFL_DUMMY || level->type == GFL_CURRENT) ? (TRX_DYNAMIC_PATH)-1 : (level->type == GFL_TITLE || level->type == GFL_GYM) ? TRX_DYNAMIC_PATH_SHARED_LEVEL_FILE : TRX_DYNAMIC_PATH_LEVEL_FILE; JSON_MUST( M_ReadPath(io, "path", false, path_type, &level->path, false)); } { const char *tmp_script = nullptr; if (JSON_OPTIONAL(JSON_READ(io, "script", &tmp_script)) && tmp_script != nullptr) { level->script_path = Memory_DupStr( TRXPath_TryResolve(TRX_DYNAMIC_PATH_SCRIPT_FILE, tmp_script)); } else { level->script_path = nullptr; } } { level->music_track = MX_INACTIVE; if (JSON_OPTIONAL(JSON_PUSH(io, "music_track"))) { JSON_MUST(JSON_READ_CURRENT(io, &level->music_track)); JSON_MUST(JSON_POP(io)); } } { const bool outfit_optional = level->type == GFL_TITLE || level->type == GFL_DUMMY || level->type == GFL_CURRENT; if (!outfit_optional) { const char *tmp = nullptr; JSON_MUST(JSON_READ(io, "lara_outfit", &tmp)); if (!ctx->validation_mode && !Lara_Skin_IsOutfitAvailable( Lara_Skin_FindOutfitByName(tmp))) { JSON_ReadIO_SetError( io, "invalid 'lara_outfit' value (%s)", tmp); JSON_FAIL(); } level->lara_outfit = Memory_DupStr(tmp); } } level->weather_type = WEATHER_NONE; { if (JSON_OPTIONAL(JSON_PUSH(io, "weather_type"))) { const char *tmp = nullptr; JSON_MUST(JSON_READ_CURRENT(io, &tmp)); JSON_MUST(JSON_POP(io)); level->weather_type = ENUM_MAP_GET(WEATHER_TYPE, tmp, WEATHER_NONE); } } JSON_READ_D(io, "water_particles", &level->water_particles, false); JSON_READ_D(io, "unobtainable_pickups", &level->unobtainable.pickups, 0); JSON_READ_D(io, "unobtainable_kills", &level->unobtainable.kills, 0); JSON_READ_D( io, "unobtainable_ally_kills", &level->unobtainable.ally_kills, 0); JSON_READ_D(io, "unobtainable_secrets", &level->unobtainable.secrets, 0); M_CopyRootSettingsIntoLevel(&level->settings, &ctx->gf->settings); JSON_MUST(M_LoadSettings(ctx, &level->settings)); JSON_MUST(M_LoadLevelItemDrops(ctx, level)); JSON_MUST(M_LoadLevelSequence(ctx, level)); JSON_MUST(M_LoadLevelInjections(ctx, level)); JSON_FINISH(); } static bool M_LoadLevelTable( const M_CONTEXT *const ctx, const char *const key, GF_LEVEL_TABLE *const level_table, const GF_LEVEL_TYPE default_level_type) { JSON_MUST(M_LoadArray( ctx, key, &level_table->count, (void **)&level_table->levels, sizeof(GF_LEVEL), M_LoadLevel, (void *)(intptr_t)default_level_type)); JSON_FINISH(); } static bool M_LoadLevels(const M_CONTEXT *const ctx) { JSON_MUST(JSON_PUSH(ctx->io, "levels")); JSON_MUST(JSON_POP(ctx->io)); JSON_MUST(M_LoadLevelTable( ctx, "levels", &ctx->gf->level_tables[GFLT_MAIN], GFL_NORMAL)); JSON_FINISH(); } static bool M_LoadCutscenes(const M_CONTEXT *const ctx) { return M_LoadLevelTable( ctx, "cutscenes", &ctx->gf->level_tables[GFLT_CUTSCENES], GFL_CUTSCENE); } static bool M_LoadDemos(const M_CONTEXT *const ctx) { return M_LoadLevelTable( ctx, "demos", &ctx->gf->level_tables[GFLT_DEMOS], GFL_DEMO); } static bool M_LoadTitleLevel(const M_CONTEXT *const ctx) { if (!JSON_OPTIONAL(JSON_PUSH(ctx->io, "title"))) { return true; } ctx->gf->title_level = Memory_Alloc(sizeof(GF_LEVEL)); JSON_MUST( M_LoadLevel(ctx, ctx->gf->title_level, 0, (void *)(intptr_t)GFL_TITLE)); JSON_MUST(JSON_POP(ctx->io)); JSON_FINISH(); } static bool M_LoadFMV( const M_CONTEXT *const ctx, void *const target_elem, size_t idx, void *const user_arg) { GF_FMV *const fmv = target_elem; ASSERT(user_arg == nullptr); JSON_READ_IO *const io = ctx->io; char *path = nullptr; JSON_SHOULD( M_ReadPath(io, "path", false, TRX_DYNAMIC_PATH_FMV_FILE, &path, false)); fmv->path = path; JSON_READ_D(io, "legal", &fmv->is_legal, false); JSON_READ_D(io, "credit", &fmv->is_credit, false); JSON_FINISH(); } static bool M_LoadFMVs(const M_CONTEXT *const ctx) { JSON_MUST(M_LoadArray( ctx, "fmvs", &ctx->gf->fmv_count, (void **)&ctx->gf->fmvs, sizeof(GF_FMV), M_LoadFMV, nullptr)); JSON_FINISH(); } static bool M_LoadGlobalInjections(const M_CONTEXT *const ctx) { JSON_READ_IO *const io = ctx->io; ctx->gf->injections.count = 0; if (!JSON_PUSH(io, "injections")) { return true; } ctx->gf->injections.count = JSON_ARRAY_LEN(io); if (ctx->gf->injections.count < 0) { JSON_FAIL(); } ctx->gf->injections.data_paths = Memory_Alloc(sizeof(char *) * (size_t)ctx->gf->injections.count); for (int32_t i = 0; i < ctx->gf->injections.count; i++) { const char *str = nullptr; JSON_MUST(JSON_READ_A(io, i, &str)); ctx->gf->injections.data_paths[i] = Memory_DupStr( TRXPath_TryResolve(TRX_DYNAMIC_PATH_INJECTION_FILE, str)); } JSON_MUST(JSON_POP(io)); JSON_FINISH(); } static bool M_LoadGameFlowDoc( JSON_VALUE *const doc, const char *const path, const bool exit_on_error, const bool validation_mode) { GF_Shutdown(); M_CONTEXT ctx = { .gf = &g_GameFlow, .validation_mode = validation_mode }; ctx.gf->main_script_path = nullptr; ctx.gf->path = Memory_DupStr(path); ctx.script_path = g_GameFlow.path; ctx.io = nullptr; ctx.io = JSON_ReadIO_Create(doc, 0, path); JSON_MUST(M_LoadRoot(&ctx)); JSON_MUST(M_LoadSettings(&ctx, &ctx.gf->settings)); JSON_MUST(M_LoadGlobeSelectEntries(&ctx)); JSON_MUST(M_LoadGlobalInjections(&ctx)); JSON_MUST(M_LoadLevels(&ctx)); JSON_MUST(M_LoadCutscenes(&ctx)); JSON_MUST(M_LoadDemos(&ctx)); JSON_MUST(M_LoadFMVs(&ctx)); JSON_MUST(M_LoadTitleLevel(&ctx)); JSON_ReadIO_Destroy(ctx.io); JSON_ValueFree(doc); return true; fail: if (exit_on_error) { M_ExitWithJSONError(&ctx); } else { LOG_WARNING("%s", JSON_ReadIO_GetError(ctx.io)); JSON_ReadIO_Destroy(ctx.io); JSON_ValueFree(doc); GF_Shutdown(); } return false; } static bool M_LoadGameFlowEx( const char *const path, const bool exit_on_error, const bool validation_mode) { JSON_VALUE *const doc = JSONFile_ReadEx(path, exit_on_error); if (doc == nullptr) { if (exit_on_error) { Shell_ExitSystemFmt("Failed to open script file %s", path); } return false; } return M_LoadGameFlowDoc(doc, path, exit_on_error, validation_mode); } void GF_LoadFromFile(const char *const path) { M_LoadGameFlowEx(path, true, false); } bool GF_TryLoadFromFile(const char *const path) { return M_LoadGameFlowEx(path, false, false); } bool GF_ValidateMod(const char *const mod_name, const char *const path) { if (!M_LoadGameFlowEx(path, false, true)) { LOG_WARNING("Mod '%s' has invalid gameflow data", mod_name); return false; } GF_Shutdown(); return true; } bool GF_ReadModMeta(const char *const path, GF_MOD_META *const meta) { ASSERT(meta != nullptr); JSON_VALUE *const doc = JSONFile_Read(path); if (doc == nullptr) { return false; } JSON_READ_IO *const io = JSON_ReadIO_Create(doc, 0, path); M_ReadModMeta(io, meta); JSON_ReadIO_Destroy(io); JSON_ValueFree(doc); return true; } ================================================ FILE: src/trx/game/game_flow/reader.h ================================================ #pragma once #include // Load the game flow from a file. void GF_LoadFromFile(const char *path); // Load the game flow from a file. // Returns false on I/O or parse/validation failure instead of exiting. bool GF_TryLoadFromFile(const char *path); // Load and validate path-backed gameflow references for a mod. // Returns false if parsing fails or any required resolved path is missing. bool GF_ValidateMod(const char *mod_name, const char *path); // Quick-parse a gameflow file to extract only the mod metadata fields // ("name", "engine", and "extends"). Returns true on success. Caller must // free meta->name and meta->extends with Memory_FreePointer(). bool GF_ReadModMeta(const char *path, GF_MOD_META *meta); ================================================ FILE: src/trx/game/game_flow/sequencer.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include static GF_COMMAND M_RunEvent( const GF_LEVEL *const level, const GF_SEQUENCE *const sequence, const int32_t event_idx, const GF_SEQUENCE_CONTEXT seq_ctx, void *const seq_ctx_arg) { GF_COMMAND gf_cmd = { .action = GF_NOOP }; const GF_SEQUENCE_EVENT *const event = &sequence->events[event_idx]; LOG_DEBUG( "event type=%s(%d) data=0x%x", ENUM_MAP_TO_STRING(GF_SEQUENCE_EVENT_TYPE, event->type), event->type, event->data); const GF_SEQUENCE_EVENT_HANDLER event_handler = GF_GetSequenceEventHandler(event->type); if (event_handler == nullptr) { return gf_cmd; } gf_cmd = event_handler(level, sequence, event_idx, seq_ctx, seq_ctx_arg); LOG_DEBUG( "event type=%s(%d) data=0x%x finished, result: action=%s, " "param=%d", ENUM_MAP_TO_STRING(GF_SEQUENCE_EVENT_TYPE, event->type), event->type, event->data, ENUM_MAP_TO_STRING(GF_ACTION, gf_cmd.action), gf_cmd.param); return gf_cmd; } static void M_PreSequenceHook( const GF_SEQUENCE_CONTEXT seq_ctx, void *const seq_ctx_arg) { Room_SetAbyssHeight(0); Output_SetSunsetEnabled(false); Lara_SetControllable(false); Lara_SetStartAnimState(LS_EXTRA_BREATH); if (seq_ctx == GFSC_SAVED) { Game_SetBonusFlag(GBF_NONE); } } static GF_SEQUENCE_CONTEXT M_SwitchSequenceContext( const GF_SEQUENCE_EVENT *const event, const GF_SEQUENCE_CONTEXT seq_ctx) { // Update sequence context if necessary if (event->type != GFS_LOOP_GAME) { return seq_ctx; } switch (seq_ctx) { case GFSC_SAVED: case GFSC_RESTART: case GFSC_SELECT: return GFSC_NORMAL; default: return seq_ctx; } } static const GF_LEVEL *M_GetCanonicalNextLevel(const GF_LEVEL *const level) { // Canonical order is still used for console-driven linear simulation. return GF_GetLevelAfter(level); } static const GF_LEVEL *M_GetLinkedPrevLevel(const GF_LEVEL *const level) { RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); if (resume == nullptr) { return nullptr; } if (resume->prev_level == -1) { return nullptr; } return GF_GetLevel(GFLT_MAIN, resume->prev_level); } static bool M_IsLevelDescendantOf( const GF_LEVEL *const level, const int32_t ancestor_level_num) { RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); if (resume == nullptr) { return false; } const int32_t count = GF_GetLevelTable(GFLT_MAIN)->count; int32_t current_prev = resume->prev_level; for (int32_t i = 0; i < count && current_prev != -1; i++) { if (current_prev == ancestor_level_num) { return true; } const GF_LEVEL *const prev_level = GF_GetLevel(GFLT_MAIN, current_prev); if (prev_level == nullptr) { break; } RESUME_INFO *const prev_resume = Savegame_GetCurrentInfo(prev_level); if (prev_resume == nullptr) { break; } current_prev = prev_resume->prev_level; } return false; } GF_COMMAND GF_InterpretSequence( const GF_LEVEL *const level, GF_SEQUENCE_CONTEXT seq_ctx, void *const seq_ctx_arg) { ASSERT(level != nullptr); LOG_DEBUG( "running sequence for level=%d type=%d seq_ctx=%d", level->num, level->type, seq_ctx); if (level->type == GFL_DUMMY || level->type == GFL_CURRENT) { return (GF_COMMAND) { .action = GF_NOOP }; } M_PreSequenceHook(seq_ctx, seq_ctx_arg); GF_COMMAND gf_cmd = { .action = GF_EXIT_TO_TITLE }; const GF_LEVEL *const prev_level = M_GetLinkedPrevLevel(level); // before load switch (seq_ctx) { case GFSC_STORY: break; case GFSC_SAVED: GF_InventoryModifier_Scan(level); // reset current info to the defaults so that we do not do // Item_GlobalReplace in the inventory initialization routines too early Savegame_InitCurrentInfo(); break; case GFSC_RESTART: if (level == GF_GetGymLevel() || level == GF_GetFirstLevel()) { Savegame_InitCurrentInfo(); } else { const int32_t prev_level_num = Savegame_GetCurrentInfo(level)->prev_level; Savegame_ResetCurrentInfo(level); if (prev_level_num != -1) { const GF_LEVEL *const linked_prev_level = GF_GetLevel(GFLT_MAIN, prev_level_num); if (linked_prev_level != nullptr) { Savegame_CarryCurrentInfoToNextLevel( linked_prev_level, level); } } Savegame_ApplyLogicToCurrentInfo(level); } if (level->type == GFL_NORMAL || level->type == GFL_BONUS) { GF_InventoryModifier_Scan(level); GF_InventoryModifier_ApplyToResumeInfo(level); } break; case GFSC_SELECT: { const SAVEGAME_SLOT_REF slot = Savegame_GetBoundSlot(); if (Savegame_IsValidSlotRef(slot)) { // select level feature Savegame_InitCurrentInfo(); if (level->num > GF_GetFirstLevel()->num) { Savegame_LoadOnlyResumeInfo(slot); const int32_t prev_level_num = Savegame_GetCurrentInfo(level)->prev_level; const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); for (int32_t i = 0; i < level_table->count; i++) { const GF_LEVEL *const tmp_level = &level_table->levels[i]; if (tmp_level->type == GFL_GYM) { continue; } if (tmp_level == level || M_IsLevelDescendantOf(tmp_level, level->num)) { Savegame_ResetCurrentInfo(tmp_level); } } if (prev_level_num != -1) { const GF_LEVEL *const linked_prev_level = GF_GetLevel(GFLT_MAIN, prev_level_num); if (linked_prev_level != nullptr) { Savegame_CarryCurrentInfoToNextLevel( linked_prev_level, level); } } Savegame_ApplyLogicToCurrentInfo(level); GF_InventoryModifier_Scan(level); GF_InventoryModifier_ApplyToResumeInfo(level); } else { Savegame_ApplyLogicToCurrentInfo(level); GF_InventoryModifier_Scan(level); GF_InventoryModifier_ApplyToResumeInfo(level); } } else { // console /play level feature Inv_RemoveAllItems(); if (level == GF_GetGymLevel()) { Savegame_InitCurrentInfo(); GF_InventoryModifier_Scan(level); GF_InventoryModifier_ApplyToResumeInfo(level); } else { const GF_LEVEL *tmp_level = GF_GetFirstLevel(); Savegame_ResetCurrentInfo(tmp_level); while (tmp_level != nullptr && tmp_level <= level) { Savegame_ApplyLogicToCurrentInfo(tmp_level); GF_InventoryModifier_Scan(tmp_level); GF_InventoryModifier_ApplyToResumeInfo(tmp_level); if (tmp_level == level) { break; } const GF_LEVEL *const next_level = M_GetCanonicalNextLevel(tmp_level); if (next_level != nullptr) { Savegame_CarryCurrentInfoToNextLevel( tmp_level, next_level); } tmp_level = next_level; } } } break; } default: if (level->type == GFL_GYM) { Savegame_ResetCurrentInfo(level); Savegame_ApplyLogicToCurrentInfo(level); } else if (level->type == GFL_DEMO) { Savegame_ApplyLogicToCurrentInfo(level); } else if (level->type == GFL_NORMAL || level->type == GFL_BONUS) { Savegame_ApplyLogicToCurrentInfo(level); GF_InventoryModifier_Scan(level); GF_InventoryModifier_ApplyToResumeInfo(level); } } // Run any level Lua script Lua_ClearLevelListeners(); Lua_SetScriptContext(LUA_CONTEXT_LEVEL); if (level->script_path != nullptr) { LUA_RESULT res = Lua_EvalFile(level->script_path); if (res.code != LUA_OK) { LOG_ERROR("Lua level script error: %s", res.message); } Lua_FreeResult(&res); } Lua_SetScriptContext(LUA_CONTEXT_GLOBAL); // load the level if (seq_ctx != GFSC_STORY || level->type == GFL_CUTSCENE) { if (!Level_Initialise(level, seq_ctx)) { Game_SetCurrentLevel(nullptr); GF_SetCurrentLevel(nullptr); if (level->type == GFL_TITLE) { gf_cmd = (GF_COMMAND) { .action = GF_EXIT_GAME }; } else { gf_cmd = (GF_COMMAND) { .action = GF_EXIT_TO_TITLE }; } } } const GF_SEQUENCE *const sequence = &level->sequence; for (int32_t i = 0; i < sequence->length; i++) { const GF_SEQUENCE_EVENT *const event = &sequence->events[i]; gf_cmd = M_RunEvent(level, sequence, i, seq_ctx, seq_ctx_arg); if (gf_cmd.action != GF_NOOP) { return gf_cmd; } // Update sequence context if necessary seq_ctx = M_SwitchSequenceContext(event, seq_ctx); } LOG_DEBUG( "sequence finished: action=%s param=%d", ENUM_MAP_TO_STRING(GF_ACTION, gf_cmd.action), gf_cmd.param); return gf_cmd; } ================================================ FILE: src/trx/game/game_flow/sequencer.h ================================================ #pragma once #include #include #include GF_COMMAND GF_EnterPhotoMode(void); GF_COMMAND GF_PauseGame(void); GF_COMMAND GF_ShowInventory(INVENTORY_MODE inv_mode); bool GF_ShowInventoryKeys(OBJECT_ID receptacle_type_id); GF_COMMAND GF_RunTitle(void); GF_COMMAND GF_RunDemo(int32_t demo_num); GF_COMMAND GF_RunCutscene(int32_t cutscene_num, bool cross_fade_in); GF_COMMAND GF_RunGame(const GF_LEVEL *level, GF_SEQUENCE_CONTEXT seq_ctx); GF_COMMAND GF_RunGlobeSelect(const char *background_path); GF_COMMAND GF_DoFrontendSequence(void); GF_COMMAND GF_DoDemoSequence(int32_t demo_num); GF_COMMAND GF_DoCutsceneSequence(int32_t cutscene_num, bool cross_fade_in); GF_COMMAND GF_InterpretSequence( const GF_LEVEL *level, GF_SEQUENCE_CONTEXT seq_ctx, void *seq_ctx_arg); GF_COMMAND GF_DoLevelSequence( const GF_LEVEL *start_level, GF_SEQUENCE_CONTEXT seq_ctx); GF_COMMAND GF_PlayAvailableStory(SAVEGAME_SLOT_REF slot); ================================================ FILE: src/trx/game/game_flow/sequencer_events.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_GF_HANDLER(name) \ static GF_COMMAND name( \ const GF_LEVEL *const level, const GF_SEQUENCE *const sequence, \ const int32_t event_idx, const GF_SEQUENCE_CONTEXT seq_ctx, \ void *const seq_ctx_arg) // clang-format off #define X_EVENT_HANDLER_LIST \ X(GFS_EXIT_TO_TITLE, M_HandleExitToTitle) \ X(GFS_LEVEL_COMPLETE, M_HandleLevelComplete) \ X(GFS_LOOP_GAME, M_HandlePlayLevel) \ X(GFS_PLAY_CUTSCENE, M_HandlePlayCutscene) \ X(GFS_PLAY_FMV, M_HandlePlayFMV) \ X(GFS_PLAY_MUSIC, M_HandlePlayMusic) \ X(GFS_ADD_ITEM, M_HandleInventoryModifier) \ X(GFS_REMOVE_WEAPONS, M_HandleInventoryModifier) \ X(GFS_REMOVE_AMMO, M_HandleInventoryModifier) \ X(GFS_REMOVE_MEDIPACKS, M_HandleInventoryModifier) \ X(GFS_REMOVE_SCIONS, M_HandleInventoryModifier) \ X(GFS_ADD_SECRET_REWARD, M_HandleInventoryModifier) \ X(GFS_REMOVE_FLARES, M_HandleInventoryModifier) \ X(GFS_LOADING_SCREEN, M_HandlePicture) \ X(GFS_DISPLAY_PICTURE, M_HandlePicture) \ X(GFS_LEVEL_STATS, M_HandleLevelStats) \ X(GFS_TOTAL_STATS, M_HandleTotalStats) \ X(GFS_GLOBE_SELECT, M_HandleGlobeSelect) \ X(GFS_SET_START_ANIM, M_HandleSetStartAnim) \ X(GFS_ENABLE_SUNSET, M_HandleEnableSunset) \ X(GFS_SETUP_BACON_LARA, M_HandleSetupBaconLara) \ X(GFS_DISABLE_FLOOR, M_HandleDisableFloor) // clang-format on #define X(id, name) M_GF_HANDLER(name); X_EVENT_HANDLER_LIST #undef X static GF_SEQUENCE_EVENT_HANDLER m_EventHandlers[GFS_NUMBER_OF] = { #define X(id, name) [id] = name, X_EVENT_HANDLER_LIST #undef X // clang-format on }; static void M_FinishLevelBasic(void) { const GF_LEVEL *const current_level = Game_GetCurrentLevel(); if (current_level == GF_GetLastLevel()) { g_Config.profile.new_game_plus_unlock = true; Config_Update(); } RESUME_INFO *const resume = Savegame_GetCurrentInfo(current_level); if (resume != nullptr) { resume->flags.available = true; resume->level_completed = true; } } static const GF_LEVEL *M_GetCanonicalNextLevel(const GF_LEVEL *const level) { // Canonical order is still used for regular level flow; resume inheritance // is non-linear and tracked via RESUME_INFO.prev_level. return GF_GetLevelAfter(level); } M_GF_HANDLER(M_HandleExitToTitle) { return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE }; } M_GF_HANDLER(M_HandleLevelComplete) { if (seq_ctx != GFSC_NORMAL) { return (GF_COMMAND) { .action = GF_NOOP }; } M_FinishLevelBasic(); const GF_LEVEL *const current_level = Game_GetCurrentLevel(); const GF_LEVEL *const next_level = M_GetCanonicalNextLevel(current_level); if (next_level == nullptr) { return (GF_COMMAND) { .action = GF_NOOP }; } Savegame_PersistGameToCurrentInfo(next_level); RESUME_INFO *const next_resume = Savegame_GetCurrentInfo(next_level); if (next_resume != nullptr) { next_resume->prev_level = current_level->num; } if (next_level->type == GFL_BONUS && !Stats_CheckAllSecretsCollected()) { return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE }; } return (GF_COMMAND) { .action = GF_START_GAME, .param = next_level->num, }; } M_GF_HANDLER(M_HandlePlayLevel) { GF_COMMAND gf_cmd = { .action = GF_NOOP }; if (seq_ctx == GFSC_STORY) { const int32_t savegame_level_num = (int32_t)(intptr_t)seq_ctx_arg; if (savegame_level_num == level->num) { return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE }; } else { return (GF_COMMAND) { .action = GF_NOOP }; } } if (Lara_GetItem() != nullptr) { Lara_Initialise(level); } if (level->music_track != MX_INACTIVE) { Music_Stop(); } Lua_FireEventInt32(LUA_EVENT_AFTER_LEVEL_FILE, level->num); // post load switch (seq_ctx) { case GFSC_SAVED: { const SAVEGAME_SLOT_REF slot = Savegame_GetBoundSlot(); if (!Savegame_Load(slot)) { LOG_ERROR("Failed to load save file!"); Game_SetCurrentLevel(nullptr); GF_SetCurrentLevel(nullptr); return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE }; } break; } default: if (level->type == GFL_NORMAL || level->type == GFL_BONUS) { Savegame_SetInitialVersion(SG_CURRENT_VERSION); GF_InventoryModifier_Scan(Game_GetCurrentLevel()); GF_InventoryModifier_Apply(Game_GetCurrentLevel(), GF_INV_REGULAR); } break; } GF_DisableObjectsIfNeeded(); Lua_FireEventInt32(LUA_EVENT_AFTER_LEVEL_STATE, level->num); g_Passport.ask_for_save = g_Config.gameplay.enable_save_crystals && seq_ctx == GFSC_NORMAL && GF_GetLevelTableType(level->type) == GFLT_MAIN && level != GF_GetFirstLevel() && level != GF_GetGymLevel(); ASSERT(GF_GetCurrentLevel() == level); if (level->type == GFL_DEMO) { gf_cmd = GF_RunDemo(level->num); } else if (level->type == GFL_CUTSCENE) { gf_cmd = GF_RunCutscene(level->num, (bool)(intptr_t)seq_ctx_arg); } else { if (seq_ctx != GFSC_SAVED && level != GF_GetFirstLevel()) { Lara_RevertToPistolsIfNeeded(); } gf_cmd = GF_RunGame(level, seq_ctx); } if (gf_cmd.action == GF_LEVEL_COMPLETE) { gf_cmd.action = GF_NOOP; } return gf_cmd; } M_GF_HANDLER(M_HandlePlayCutscene) { GF_COMMAND gf_cmd = { .action = GF_NOOP }; const GF_SEQUENCE_EVENT *const event = &sequence->events[event_idx]; const GF_SEQUENCE_EVENT *const prev_event = event_idx > 0 ? &sequence->events[event_idx - 1] : nullptr; const int16_t cutscene_num = (int16_t)(intptr_t)event->data; const bool cross_fade_in = prev_event != nullptr && prev_event->type == GFS_LOOP_GAME; if (seq_ctx != GFSC_SAVED && g_Config.gameplay.enable_cutscenes) { gf_cmd = GF_DoCutsceneSequence(cutscene_num, cross_fade_in); if (gf_cmd.action == GF_LEVEL_COMPLETE) { gf_cmd.action = GF_NOOP; } } return gf_cmd; } M_GF_HANDLER(M_HandlePlayFMV) { GF_COMMAND gf_cmd = { .action = GF_NOOP }; const GF_SEQUENCE_EVENT *const event = &sequence->events[event_idx]; const int16_t fmv_id = (int16_t)(intptr_t)event->data; if (seq_ctx == GFSC_SAVED) { return gf_cmd; } if (fmv_id < 0 || fmv_id >= g_GameFlow.fmv_count) { LOG_ERROR("Invalid FMV number: %d", fmv_id); return gf_cmd; } const GF_FMV *const fmv = &g_GameFlow.fmvs[fmv_id]; if (fmv->is_legal && !g_Config.gameplay.enable_legal) { return gf_cmd; } if (fmv->is_credit && !g_Config.gameplay.enable_credits) { return gf_cmd; } FMV_Play(fmv->path); return gf_cmd; } M_GF_HANDLER(M_HandlePlayMusic) { if (seq_ctx != GFSC_STORY) { const GF_SEQUENCE_EVENT *const event = &sequence->events[event_idx]; Music_SetVolume(g_Config.audio.music_volume); Music_Play_Direct((int32_t)(intptr_t)event->data, MPM_ONCE); } return (GF_COMMAND) { .action = GF_NOOP }; } M_GF_HANDLER(M_HandlePicture) { GF_COMMAND gf_cmd = { .action = GF_NOOP }; const GF_SEQUENCE_EVENT *const event = &sequence->events[event_idx]; const GF_SEQUENCE_EVENT *const prev_event = event_idx > 0 ? &sequence->events[event_idx - 1] : nullptr; const bool is_after_fmv = prev_event != nullptr && prev_event->type == GFS_PLAY_FMV; if (event->type == GFS_LOADING_SCREEN) { if (g_Config.gameplay.loading_screens == LOADING_SCREENS_DISABLED) { return gf_cmd; } else if (seq_ctx == GFSC_STORY) { return gf_cmd; } else if ( g_Config.gameplay.loading_screens == LOADING_SCREENS_NEW_GAMES && seq_ctx != GFSC_NORMAL && seq_ctx != GFSC_SELECT) { return gf_cmd; } Music_Stop(); } else if (seq_ctx == GFSC_SAVED) { return gf_cmd; } GF_DISPLAY_PICTURE_DATA *data = event->data; if (data->path == nullptr) { return gf_cmd; } if (data->is_legal && !g_Config.gameplay.enable_legal) { return gf_cmd; } if (data->is_credit && !g_Config.gameplay.enable_credits) { return gf_cmd; } PHASE *const phase = Phase_Picture_Create((PHASE_PICTURE_ARGS) { .file_name = data->path, .display_time = data->display_time, .fade_in_time = data->fade_in_time, .fade_out_time = data->fade_out_time, .display_time_includes_fades = g_TRVersion >= 2, .loading_pic = event->type == GFS_LOADING_SCREEN, .block_cross_fade_in = is_after_fmv, }); gf_cmd = PhaseExecutor_Run(phase); Phase_Picture_Destroy(phase); return gf_cmd; } M_GF_HANDLER(M_HandleInventoryModifier) { // handled in GF_InventoryModifier_Apply return (GF_COMMAND) { .action = GF_NOOP }; } M_GF_HANDLER(M_HandleLevelStats) { GF_COMMAND gf_cmd = { .action = GF_NOOP }; if (seq_ctx != GFSC_NORMAL) { return gf_cmd; } PHASE *const phase = Phase_Stats_Create((PHASE_STATS_ARGS) { .background_type = Game_IsInGym() ? BK_TRANSPARENT_MEDIUM : g_Config.ui.stats_background_style, .level_num = -1, .show_final_stats = false, .use_bare_style = g_Config.ui.stats.style == STATS_STYLE_BARE, }); gf_cmd = PhaseExecutor_Run(phase); Phase_Stats_Destroy(phase); return gf_cmd; } M_GF_HANDLER(M_HandleTotalStats) { GF_COMMAND gf_cmd = { .action = GF_EXIT_TO_TITLE }; if (seq_ctx != GFSC_NORMAL) { return gf_cmd; } if (!g_Config.gameplay.enable_total_stats) { return gf_cmd; } const GF_SEQUENCE_EVENT *const event = &sequence->events[event_idx]; PHASE *const phase = Phase_Stats_Create((PHASE_STATS_ARGS) { .background_type = BK_IMAGE, .background_path = event->data, .show_final_stats = true, .use_bare_style = false, .level_num = -1, }); gf_cmd = PhaseExecutor_Run(phase); Phase_Stats_Destroy(phase); return gf_cmd; } M_GF_HANDLER(M_HandleGlobeSelect) { if (seq_ctx != GFSC_NORMAL) { return (GF_COMMAND) { .action = GF_NOOP }; } M_FinishLevelBasic(); const GF_SEQUENCE_EVENT *const event = &sequence->events[event_idx]; const GF_GLOBE_SELECT_DATA *const data = event->data; const GF_COMMAND gf_cmd = GF_RunGlobeSelect(data != nullptr ? data->image_path : nullptr); if (gf_cmd.action == GF_START_GAME) { const GF_LEVEL *const current_level = Game_GetCurrentLevel(); const GF_LEVEL *const next_level = GF_GetLevel(GFLT_MAIN, gf_cmd.param); if (next_level != nullptr) { Savegame_PersistGameToCurrentInfo(next_level); RESUME_INFO *const next_resume = Savegame_GetCurrentInfo(next_level); if (next_resume != nullptr) { next_resume->prev_level = current_level != nullptr ? current_level->num : -1; } } } return gf_cmd; } M_GF_HANDLER(M_HandleSetStartAnim) { GF_COMMAND gf_cmd = { .action = GF_NOOP }; const GF_SEQUENCE_EVENT *const event = &sequence->events[event_idx]; if (seq_ctx != GFSC_STORY) { Lara_SetStartAnimState((LARA_EXTRA_STATE)(intptr_t)event->data); } return gf_cmd; } M_GF_HANDLER(M_HandleEnableSunset) { GF_COMMAND gf_cmd = { .action = GF_NOOP }; if (seq_ctx != GFSC_STORY) { Output_SetSunsetEnabled(true); } return gf_cmd; } M_GF_HANDLER(M_HandleSetupBaconLara) { // TODO: move me to lua! if (seq_ctx != GFSC_STORY) { const GF_SEQUENCE_EVENT *const event = &sequence->events[event_idx]; const int32_t anchor_room = (int32_t)(intptr_t)event->data; if (!BaconLara_InitialiseAnchor(anchor_room)) { LOG_ERROR("Could not anchor Bacon Lara to room %d", anchor_room); return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE }; } } return (GF_COMMAND) { .action = GF_NOOP }; } M_GF_HANDLER(M_HandleDisableFloor) { GF_COMMAND gf_cmd = { .action = GF_NOOP }; if (seq_ctx != GFSC_STORY) { const GF_SEQUENCE_EVENT *const event = &sequence->events[event_idx]; Room_SetAbyssHeight((int16_t)(intptr_t)event->data); } return gf_cmd; } GF_SEQUENCE_EVENT_HANDLER GF_GetSequenceEventHandler( const GF_SEQUENCE_EVENT_TYPE event_type) { return m_EventHandlers[event_type]; } ================================================ FILE: src/trx/game/game_flow/sequencer_events.h ================================================ #pragma once #include typedef GF_COMMAND (*GF_SEQUENCE_EVENT_HANDLER)( const GF_LEVEL *, const GF_SEQUENCE *, int32_t event_id, GF_SEQUENCE_CONTEXT, void *); GF_SEQUENCE_EVENT_HANDLER GF_GetSequenceEventHandler( GF_SEQUENCE_EVENT_TYPE event_type); ================================================ FILE: src/trx/game/game_flow/sequencer_misc.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include GF_COMMAND GF_RunTitle(void) { Savegame_UnbindSlot(); GameStringTable_Apply(nullptr); const GF_LEVEL *const title_level = GF_GetTitleLevel(); if (!Level_Initialise(title_level, GFSC_NORMAL)) { return (GF_COMMAND) { .action = GF_EXIT_GAME }; } return GF_ShowInventory(INV_TITLE_MODE); } GF_COMMAND GF_EnterPhotoMode(void) { PHASE *const subphase = Phase_PhotoMode_Create(); const GF_COMMAND gf_cmd = PhaseExecutor_Run(subphase); Phase_PhotoMode_Destroy(subphase); return gf_cmd; } GF_COMMAND GF_PauseGame(void) { PHASE *const subphase = Phase_Pause_Create(); const GF_COMMAND gf_cmd = PhaseExecutor_Run(subphase); Phase_Pause_Destroy(subphase); return gf_cmd; } GF_COMMAND GF_ShowInventory(const INVENTORY_MODE mode) { PHASE *const phase = Phase_Inventory_Create(mode); const GF_COMMAND gf_cmd = PhaseExecutor_Run(phase); Phase_Inventory_Destroy(phase); return gf_cmd; } bool GF_ShowInventoryKeys(const OBJECT_ID receptacle_type_id) { if (!InvRing_IsRingAvailable(RT_KEYS)) { return false; } if (g_Config.gameplay.enable_auto_item_selection) { const OBJECT_ID obj_id = Object_GetCognateInverse( receptacle_type_id, g_KeyItemToReceptacleMap); InvRing_SetRequestedObjectID(obj_id); } else { Inv_ClearSelection(); } const GF_COMMAND gf_cmd = GF_ShowInventory(INV_KEYS_MODE); if (gf_cmd.action != GF_NOOP) { GF_OverrideCommand(gf_cmd); } return true; } GF_COMMAND GF_RunDemo(const int32_t demo_num) { PHASE *const demo_phase = Phase_Demo_Create(demo_num); const GF_COMMAND gf_cmd = PhaseExecutor_Run(demo_phase); Phase_Demo_Destroy(demo_phase); return gf_cmd; } GF_COMMAND GF_RunCutscene(const int32_t cutscene_num, const bool cross_fade_in) { PHASE *const cutscene_phase = Phase_Cutscene_Create((PHASE_CUTSCENE_ARGS) { .level_num = cutscene_num, .cross_fade_in = cross_fade_in, }); const GF_COMMAND gf_cmd = PhaseExecutor_Run(cutscene_phase); Phase_Cutscene_Destroy(cutscene_phase); return gf_cmd; } GF_COMMAND GF_RunGame( const GF_LEVEL *const level, const GF_SEQUENCE_CONTEXT seq_ctx) { PHASE *const phase = Phase_Game_Create(level, seq_ctx); const GF_COMMAND gf_cmd = PhaseExecutor_Run(phase); Phase_Game_Destroy(phase); return gf_cmd; } GF_COMMAND GF_RunGlobeSelect(const char *const background_path) { PHASE *const phase = Phase_GlobeSelect_Create((PHASE_GLOBE_SELECT_ARGS) { .background_path = background_path, }); const GF_COMMAND gf_cmd = PhaseExecutor_Run(phase); Phase_GlobeSelect_Destroy(phase); return gf_cmd; } GF_COMMAND GF_DoFrontendSequence(void) { const SHELL_ARGS *const args = Shell_GetArgs(); if (args != nullptr) { if (args->save_to_load >= 0) { return (GF_COMMAND) { .action = GF_START_SAVED_GAME, .param = Savegame_SlotToParam( Savegame_NormalSlot(args->save_to_load)), }; } if (args->level_to_select >= 0) { const GF_LEVEL *const level = GF_GetLevelByOrdinalNumber(GFLT_MAIN, args->level_to_select); if (level == nullptr) { Shell_ExitSystemFmt( "Invalid level number: %d", args->level_to_select); } return (GF_COMMAND) { .action = GF_SELECT_GAME, .param = level->num, }; } if (args->level_to_play != nullptr) { return (GF_COMMAND) { .action = GF_START_GAME, .param = 0, }; } } if (g_GameFlow.title_level == nullptr) { return (GF_COMMAND) { .action = GF_NOOP }; } return GF_InterpretSequence(g_GameFlow.title_level, GFSC_NORMAL, nullptr); } GF_COMMAND GF_DoLevelSequence( const GF_LEVEL *const start_level, const GF_SEQUENCE_CONTEXT seq_ctx) { const GF_LEVEL *current_level = start_level; const GF_LEVEL_TABLE_TYPE level_table_type = GF_GetLevelTableType(current_level->type); const int32_t level_count = GF_GetLevelTable(level_table_type)->count; while (true) { const GF_COMMAND gf_cmd = GF_InterpretSequence(current_level, seq_ctx, nullptr); if (gf_cmd.action != GF_NOOP && gf_cmd.action != GF_LEVEL_COMPLETE) { return gf_cmd; } if (Game_IsInGym()) { return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE }; } if (current_level->num + 1 >= level_count) { return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE }; } current_level++; } } GF_COMMAND GF_DoDemoSequence(int32_t demo_num) { demo_num = Demo_ChooseLevel(demo_num); if (demo_num < 0) { return (GF_COMMAND) { .action = GF_NOOP }; } const GF_LEVEL *const level = GF_GetLevel(GFLT_DEMOS, demo_num); if (level == nullptr) { LOG_ERROR("Missing demo: %d", demo_num); return (GF_COMMAND) { .action = GF_NOOP }; } return GF_InterpretSequence(level, GFSC_NORMAL, nullptr); } GF_COMMAND GF_DoCutsceneSequence( const int32_t cutscene_num, const bool cross_fade_in) { const GF_LEVEL *const level = GF_GetLevel(GFLT_CUTSCENES, cutscene_num); if (level == nullptr) { LOG_ERROR("Missing cutscene: %d", cutscene_num); return (GF_COMMAND) { .action = GF_NOOP }; } return GF_InterpretSequence( level, GFSC_NORMAL, (void *)(intptr_t)cross_fade_in); } GF_COMMAND GF_PlayAvailableStory(const SAVEGAME_SLOT_REF slot) { const int32_t savegame_level = Savegame_GetLevelNumber(slot); const bool prev_enable_legal = g_Config.gameplay.enable_legal; g_Config.gameplay.enable_legal = false; // Play intro FMVs and cutscenes GF_DoFrontendSequence(); const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); for (int32_t i = 0; i <= MIN(savegame_level, level_table->count); i++) { const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i); if (level->type == GFL_GYM) { continue; } const GF_COMMAND gf_cmd = GF_InterpretSequence( level, GFSC_STORY, (void *)(intptr_t)savegame_level); if (gf_cmd.action == GF_EXIT_TO_TITLE || gf_cmd.action == GF_EXIT_GAME) { break; } } g_Config.gameplay.enable_legal = prev_enable_legal; return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE }; } bool GF_HasAvailableStory(const SAVEGAME_SLOT_REF slot) { if (Savegame_IsSlotFree(slot)) { return false; } const int32_t savegame_level = Savegame_GetLevelNumber(slot); // Check intro FMVs and cutscenes in frontend sequence (skip legal FMVs) const GF_LEVEL *const title_level = GF_GetTitleLevel(); if (title_level != nullptr) { const GF_SEQUENCE *const seq = &title_level->sequence; for (int32_t j = 0; j < seq->length; j++) { const GF_SEQUENCE_EVENT *const ev = &seq->events[j]; if (ev->type == GFS_PLAY_CUTSCENE) { return true; } if (ev->type == GFS_PLAY_FMV) { const int32_t fmv_id = (int32_t)(intptr_t)ev->data; const GF_FMV *const fmv = &g_GameFlow.fmvs[fmv_id]; if (!fmv->is_legal && !fmv->is_credit) { return true; } } } } // Check for any cutscenes or FMVs up until the save point const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); const int32_t max_level = MIN(savegame_level, level_table->count); for (int32_t i = 0; i <= max_level; i++) { const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i); if (level->type == GFL_GYM) { continue; } const GF_SEQUENCE *const seq = &level->sequence; for (int32_t j = 0; j < seq->length; j++) { const GF_SEQUENCE_EVENT *const ev = &seq->events[j]; // Stop checking after the saved level if (ev->type == GFS_LOOP_GAME) { break; } if (ev->type == GFS_PLAY_CUTSCENE) { return true; } if (ev->type == GFS_PLAY_FMV) { const int32_t fmv_id = (int32_t)(intptr_t)ev->data; const GF_FMV *const fmv = &g_GameFlow.fmvs[fmv_id]; if (!fmv->is_legal && !fmv->is_credit) { return true; } } } } return false; } ================================================ FILE: src/trx/game/game_flow/types.h ================================================ #pragma once #include #include #include #include #include typedef struct GF_COMMAND { GF_ACTION action; int32_t param; } GF_COMMAND; // ---------------------------------------------------------------------------- // Sequencer structures // ---------------------------------------------------------------------------- typedef struct { GF_SEQUENCE_EVENT_TYPE type; void *data; } GF_SEQUENCE_EVENT; typedef struct { int32_t length; GF_SEQUENCE_EVENT *events; } GF_SEQUENCE; // Concrete events data typedef struct { char *path; bool is_legal; bool is_credit; float display_time; float fade_in_time; float fade_out_time; } GF_DISPLAY_PICTURE_DATA; typedef struct { char *image_path; } GF_GLOBE_SELECT_DATA; typedef enum { GF_INV_REGULAR, GF_INV_SECRET, } GF_INV_TYPE; typedef struct { OBJECT_ID object_id; GF_INV_TYPE inv_type; int32_t quantity; } GF_ADD_ITEM_DATA; // ---------------------------------------------------------------------------- // Game flow level structures // ---------------------------------------------------------------------------- typedef struct { int32_t count; char **data_paths; } INJECTION_DATA; typedef struct { const char *path; bool is_legal; bool is_credit; } GF_FMV; typedef struct { bool is_present; int32_t count; MUSIC_ID *ids; } GF_AMBIENT_DATA; typedef struct { struct { bool is_present; float value; } fog_start, fog_end; struct { bool is_present; bool value; } fog_transparency; struct { bool is_present; RGB_888 value; } fog_color; struct { bool is_present; RGB_888 value; } water_color; char *sfx_path; struct { bool is_present; bool value; } cold_water; struct { bool is_present; int32_t value; } death_tile; } GF_LEVEL_SETTINGS; typedef struct { int32_t enemy_num; int32_t count; int16_t *object_ids; } GF_DROP_ITEM_DATA; typedef struct { int32_t num; GF_LEVEL_TYPE type; char *path; char *title; // Path to a Lua script executed when this level loads char *script_path; MUSIC_ID music_track; char *lara_outfit; GF_SEQUENCE sequence; INJECTION_DATA injections; GF_LEVEL_SETTINGS settings; WEATHER_TYPE weather_type; bool water_particles; struct { uint32_t pickups; uint32_t kills; uint32_t ally_kills; uint32_t secrets; } unobtainable; struct { int32_t count; GF_DROP_ITEM_DATA *data; } item_drops; } GF_LEVEL; typedef struct { int32_t count; GF_LEVEL *levels; } GF_LEVEL_TABLE; typedef struct { XYZ_16 rot; int32_t start_level_ordinal; int32_t completion_level_ordinal; uint32_t prereq_mask; uint8_t mesh_idx; } GF_GLOBE_ENTRY; // ---------------------------------------------------------------------------- // Mod metadata // ---------------------------------------------------------------------------- typedef struct { char *name; int32_t engine; char *extends; } GF_MOD_META; // ---------------------------------------------------------------------------- // Game flow structures // ---------------------------------------------------------------------------- typedef struct { char *path; GF_MOD_META meta; GF_LEVEL *title_level; GF_LEVEL_TABLE level_tables[GFLT_NUMBER_OF]; // FMVs struct { int32_t fmv_count; GF_FMV *fmvs; }; // savegame settings struct { char *savegame_file_fmt; }; // global settings struct { char *main_menu_background_path; bool enable_tr2_item_drops; bool convert_dropped_guns; GF_AMBIENT_DATA ambient_tracks; }; // other data GF_LEVEL_SETTINGS settings; INJECTION_DATA injections; // Globe select entries struct { int32_t count; GF_GLOBE_ENTRY *entries; } globe; // Path to a global Lua script executed after game initialization char *main_script_path; } GAME_FLOW; ================================================ FILE: src/trx/game/game_flow/util.c ================================================ #include #include #include #include #include #include #include static void M_DisableObject(const OBJECT_ID object_id) { OBJECT *const obj = Object_Get(object_id); obj->loaded = false; obj->collision_func = nullptr; obj->control_func = nullptr; obj->draw_func = nullptr; obj->floor_height_func = nullptr; obj->ceiling_height_func = nullptr; } static void M_ReplaceObject( const OBJECT_ID src_object_id, const OBJECT_ID dst_object_id) { for (int32_t i = 0; i < Item_GetTotalCount(); i++) { ITEM *const item = Item_Get(i); if (item->object_id == src_object_id) { item->object_id = dst_object_id; } } } void GF_DisableObjectsIfNeeded(void) { const GF_LEVEL *const level = Game_GetCurrentLevel(); if (level == nullptr || (level->type != GFL_NORMAL && level->type != GFL_BONUS && level->type != GFL_GYM)) { return; } if (g_Config.gameplay.disable_medpacks) { M_DisableObject(O_SMALL_MEDIPACK_ITEM); M_DisableObject(O_LARGE_MEDIPACK_ITEM); } if (g_Config.gameplay.disable_extra_guns) { const RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); ASSERT(resume != nullptr); for (int32_t i = 0; g_GunObjects[i] != NO_OBJECT; i++) { if (resume->flags.has_pistols) { M_DisableObject(g_GunObjects[i]); } else { M_ReplaceObject(g_GunObjects[i], O_PISTOL_ITEM); } M_DisableObject( Object_GetCognate(g_GunObjects[i], g_GunAmmoObjectMap)); } } } int32_t GF_BadGetLevelNum(void) { return GF_GetCurrentLevel()->num; } bool GF_BadIsMod(const char *const mod) { return Shell_IsCurrentMod(mod); } ================================================ FILE: src/trx/game/game_flow/util.h ================================================ #pragma once #include void GF_DisableObjectsIfNeeded(void); // NOTE: This API should be eliminated! int32_t GF_BadGetLevelNum(void); bool GF_BadIsMod(const char *mod); ================================================ FILE: src/trx/game/game_flow/vars.c ================================================ #include GAME_FLOW g_GameFlow = {}; ================================================ FILE: src/trx/game/game_flow/vars.h ================================================ #pragma once #include extern GAME_FLOW g_GameFlow; ================================================ FILE: src/trx/game/game_flow.h ================================================ #pragma once #include #include #include #include #include #include #include #include ================================================ FILE: src/trx/game/game_strings/entries.c ================================================ #include #include #include // One-slot-per-string for stable indirection on reload. typedef struct M_SLOT { const char *value; } M_SLOT; typedef struct { char *key; M_SLOT *slot; UT_hash_handle hh; } M_STRING_ENTRY; static M_STRING_ENTRY *m_StringTable = nullptr; void GameString_Init(void) { #include } void GameString_Shutdown(void) { GameString_Clear(); } void GameString_Define(const char *const key, const char *value) { M_STRING_ENTRY *entry; HASH_FIND_STR(m_StringTable, key, entry); if (entry == nullptr) { entry = Memory_Alloc(sizeof(*entry)); entry->key = Memory_DupStr(key); entry->slot = Memory_Alloc(sizeof(*entry->slot)); entry->slot->value = nullptr; HASH_ADD_KEYPTR( hh, m_StringTable, entry->key, strlen(entry->key), entry); } Memory_Free((void *)entry->slot->value); entry->slot->value = Memory_DupStr(value); } bool GameString_IsKnown(const char *const key) { M_STRING_ENTRY *entry; HASH_FIND_STR(m_StringTable, key, entry); return entry != nullptr; } const char *GameString_Get(const char *const key) { M_STRING_ENTRY *entry; HASH_FIND_STR(m_StringTable, key, entry); return entry ? entry->slot->value : nullptr; } const char *const *GameString_GetPtr(const char *const key) { M_STRING_ENTRY *entry; HASH_FIND_STR(m_StringTable, key, entry); return entry ? &entry->slot->value : nullptr; } void GameString_Clear(void) { M_STRING_ENTRY *entry, *tmp; HASH_ITER(hh, m_StringTable, entry, tmp) { HASH_DEL(m_StringTable, entry); Memory_Free(entry->key); Memory_Free((void *)entry->slot->value); Memory_Free(entry->slot); Memory_Free(entry); } } ================================================ FILE: src/trx/game/game_strings/entries.def ================================================ // Enum values GS_DEFINE(dynamic/enums/bar_look/tr1_pc, "TR1 PC") GS_DEFINE(dynamic/enums/bar_look/tr2_pc, "TR2 PC") GS_DEFINE(dynamic/enums/bar_look/tr2_ps1, "TR2 PS1") GS_DEFINE(dynamic/enums/bar_look/tr3_pc, "TR3 PC") GS_DEFINE(dynamic/enums/bar_look/tr3_ps1, "TR3 PS1") GS_DEFINE(dynamic/enums/lara_outfit/default, "Default") GS_DEFINE(dynamic/enums/lara_outfit/golden_sophia, "Golden Sophia") GS_DEFINE(dynamic/enums/lara_outfit/sophia, "Sophia") GS_DEFINE(dynamic/enums/lara_outfit/tr1_bacon_lara, "Bacon Lara") GS_DEFINE(dynamic/enums/lara_outfit/tr1_classic, "TR1 Classic") GS_DEFINE(dynamic/enums/lara_outfit/tr1_combo, "TR1 Combo") GS_DEFINE(dynamic/enums/lara_outfit/tr1_golden_bacon_lara, "Golden Bacon Lara") GS_DEFINE(dynamic/enums/lara_outfit/tr1_golden_lara, "TR1 Golden Lara") GS_DEFINE(dynamic/enums/lara_outfit/tr1_gym, "TR1 Gym") GS_DEFINE(dynamic/enums/lara_outfit/tr1_mauled, "TR1 Mauled") GS_DEFINE(dynamic/enums/lara_outfit/tr1_ngage, "TR1 N-Gage") GS_DEFINE(dynamic/enums/lara_outfit/tr23_golden_lara, "TR2/3 Golden Lara") GS_DEFINE(dynamic/enums/lara_outfit/tr2_bomber_jacket, "Bomber Jacket") GS_DEFINE(dynamic/enums/lara_outfit/tr2_classic, "TR2 Classic") GS_DEFINE(dynamic/enums/lara_outfit/tr2_diving_suit, "Diving Suit 1") GS_DEFINE(dynamic/enums/lara_outfit/tr2_diving_suit_alpha, "Diving Suit 2") GS_DEFINE(dynamic/enums/lara_outfit/tr2_gym, "TR2 Gym") GS_DEFINE(dynamic/enums/lara_outfit/tr2_robe, "Robe") GS_DEFINE(dynamic/enums/lara_outfit/tr2_vegas, "Vegas") GS_DEFINE(dynamic/enums/lara_outfit/tr3_antarctica, "Antarctica") GS_DEFINE(dynamic/enums/lara_outfit/tr3_catsuit, "Catsuit") GS_DEFINE(dynamic/enums/lara_outfit/tr3_classic, "TR3 Classic") GS_DEFINE(dynamic/enums/lara_outfit/tr3_gym, "TR3 Gym") GS_DEFINE(dynamic/enums/lara_outfit/tr3_nevada, "Nevada") GS_DEFINE(dynamic/enums/lara_outfit/tr3_south_pacific, "South Pacific") GS_DEFINE(enums/ALLY_HOSTILITY_POLICY/ALLY_HOSTILITY_POLICY_INDIVIDUAL, "Individual") GS_DEFINE(enums/ALLY_HOSTILITY_POLICY/ALLY_HOSTILITY_POLICY_SHARED, "Shared") GS_DEFINE(enums/ASPECT_MODE/ASPECT_MODE_16_10, "16:10") GS_DEFINE(enums/ASPECT_MODE/ASPECT_MODE_16_9, "16:9") GS_DEFINE(enums/ASPECT_MODE/ASPECT_MODE_4_3, "4:3") GS_DEFINE(enums/ASPECT_MODE/ASPECT_MODE_ANY, "Any") GS_DEFINE(enums/BACKGROUND_TYPE/BK_BLACK, "Black") GS_DEFINE(enums/BACKGROUND_TYPE/BK_IMAGE, "Image") GS_DEFINE(enums/BACKGROUND_TYPE/BK_MONOCHROME, "Monochrome") GS_DEFINE(enums/BACKGROUND_TYPE/BK_MONOCHROME_COOL, "Monochrome (cool)") GS_DEFINE(enums/BACKGROUND_TYPE/BK_MONOCHROME_WARM, "Monochrome (warm)") GS_DEFINE(enums/BACKGROUND_TYPE/BK_NONE, "Transparent") GS_DEFINE(enums/BACKGROUND_TYPE/BK_PATTERN_STATIC, "Static") GS_DEFINE(enums/BACKGROUND_TYPE/BK_PATTERN_WAVE, "Wave") GS_DEFINE(enums/BACKGROUND_TYPE/BK_TRANSPARENT_DARK, "Very dark") GS_DEFINE(enums/BACKGROUND_TYPE/BK_TRANSPARENT_MEDIUM, "Dark") GS_DEFINE(enums/BAR_SHOW_MODE/BAR_SHOW_MODE_ALWAYS, "Always") GS_DEFINE(enums/BAR_SHOW_MODE/BAR_SHOW_MODE_BOSS_ONLY, "Boss only") GS_DEFINE(enums/BAR_SHOW_MODE/BAR_SHOW_MODE_NEVER, "Never") GS_DEFINE(enums/BILLBOARD_LOCK_MODE/BILLBOARD_LOCK_NONE, "None") GS_DEFINE(enums/BILLBOARD_LOCK_MODE/BILLBOARD_LOCK_PERSPECTIVE, "Perspective") GS_DEFINE(enums/BILLBOARD_LOCK_MODE/BILLBOARD_LOCK_ROLL, "Roll") GS_DEFINE(enums/BILLBOARD_LOCK_MODE/BILLBOARD_LOCK_ROLL_PITCH, "Roll & pitch") GS_DEFINE(enums/BLOOD_EFFECTS/BLOOD_EFFECTS_DISABLED, "Disabled") GS_DEFINE(enums/BLOOD_EFFECTS/BLOOD_EFFECTS_PINK, "Pink") GS_DEFINE(enums/BLOOD_EFFECTS/BLOOD_EFFECTS_RED, "Red") GS_DEFINE(enums/CAMERA_MODE/CAMERA_MODE_TR1, "TR1") GS_DEFINE(enums/CAMERA_MODE/CAMERA_MODE_TR2, "TR2") GS_DEFINE(enums/CAMERA_MODE/CAMERA_MODE_TR3, "TR3") GS_DEFINE(enums/CREATURE_DROWN_POLICY/CREATURE_DROWN_POLICY_NEVER, "Never") GS_DEFINE(enums/CREATURE_DROWN_POLICY/CREATURE_DROWN_POLICY_DEFAULT, "Default") GS_DEFINE(enums/CREATURE_DROWN_POLICY/CREATURE_DROWN_POLICY_SUBMERGED, "Submerged") GS_DEFINE(enums/INPUT_BACKEND/INPUT_BACKEND_CONTROLLER, "Controller") GS_DEFINE(enums/INPUT_BACKEND/INPUT_BACKEND_KEYBOARD, "Keyboard") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_ACTION, "Action") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_CAMERA_BACK, "Camera Back") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_CAMERA_DOWN, "Camera Down") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_CAMERA_FORWARD, "Camera Forward") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_CAMERA_LEFT, "Camera Left") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_CAMERA_RESET, "Camera Reset") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_CAMERA_RIGHT, "Camera Right") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_CAMERA_UP, "Camera Up") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_CHANGE_OUTFIT, "Change Outfit") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_CHANGE_TARGET, "Change Target") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_CROUCH, "Crouch") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_CYCLE_LIGHTING_CONTRAST, "Cycle Lighting Contrast") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_DOWN, "Back") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_DRAW_WEAPON, "Draw Weapon") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_ENTER_CONSOLE, "Dev Console") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_EQUIP_AUTOS, "Equip Automatic Pistols") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_EQUIP_DESERT_EAGLE, "Equip Desert Eagle") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_EQUIP_GRENADE_LAUNCHER, "Equip Grenade Launcher") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_EQUIP_HARPOON, "Equip Harpoon Gun") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_EQUIP_M16, "Equip M16") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_EQUIP_MAGNUMS, "Equip Magnums") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_EQUIP_MP5, "Equip MP5") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_EQUIP_PISTOLS, "Equip Pistols") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_EQUIP_ROCKET_LAUNCHER, "Equip Rocket Launcher") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_EQUIP_SHOTGUN, "Equip Shotgun") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_EQUIP_UZIS, "Equip Uzis") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_FLY_CHEAT, "Fly Cheat") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_FPS, "Show FPS") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_INVENTORY, "Inventory") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_ITEM_CHEAT, "Item Cheat") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_JUMP, "Jump") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_LEFT, "Left") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_LEVEL_SKIP_CHEAT, "Level Skip") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_LOAD, "Load") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_LOOK, "Look") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_PAUSE, "Pause") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_QUICK_LOAD, "Quick Load") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_QUICK_SAVE, "Quick Save") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_RIGHT, "Right") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_ROLL, "Roll") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_SAVE, "Save") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_SCREENSHOT, "Screenshot") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_SLOW, "Walk") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_SPRINT, "Sprint") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_STEP_LEFT, "Step Left") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_STEP_RIGHT, "Step Right") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_SWITCH_BORDERS, "Switch Borders Size") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_SWITCH_UPSCALING, "Switch Upscaling Factor") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_TOGGLE_BILINEAR_FILTER, "Toggle Bilinear Filter") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_TOGGLE_FULLSCREEN, "Toggle Fullscreen") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_TOGGLE_PHOTO_MODE, "Toggle Photo Mode") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_TOGGLE_TEXTURES, "Toggle Textures") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_TOGGLE_TRAPEZOID_FILTER, "Toggle Trapezoid Filter") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_TOGGLE_UI, "Toggle UI") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_TOGGLE_WIREFRAME, "Toggle Wireframe") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_TURBO_CHEAT, "Turbo Speed") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_UP, "Run") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_USE_BIG_MEDI, "Large Medi") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_USE_FLARE, "Flare") GS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_USE_SMALL_MEDI, "Small Medi") GS_DEFINE(enums/JUMP_LOCK_MODE/JUMP_LOCK_DISABLED, "Disabled") GS_DEFINE(enums/JUMP_LOCK_MODE/JUMP_LOCK_LEGACY, "Legacy") GS_DEFINE(enums/JUMP_LOCK_MODE/JUMP_LOCK_TUNED, "Tuned") GS_DEFINE(enums/LIGHTING_CONTRAST/LIGHTING_CONTRAST_HIGH, "High") GS_DEFINE(enums/LIGHTING_CONTRAST/LIGHTING_CONTRAST_LOW, "Low") GS_DEFINE(enums/LIGHTING_CONTRAST/LIGHTING_CONTRAST_MEDIUM, "Medium") GS_DEFINE(enums/LOADING_SCREENS_MODE/LOADING_SCREENS_ALWAYS, "Always") GS_DEFINE(enums/LOADING_SCREENS_MODE/LOADING_SCREENS_DISABLED, "Disabled") GS_DEFINE(enums/LOADING_SCREENS_MODE/LOADING_SCREENS_NEW_GAMES, "New games") GS_DEFINE(enums/LOOK_MODE/LOOK_MODE_ENHANCED, "Enhanced") GS_DEFINE(enums/LOOK_MODE/LOOK_MODE_RESTRICTED, "Restricted") GS_DEFINE(enums/LOOK_MODE/LOOK_MODE_UNRESTRICTED, "Unrestricted") GS_DEFINE(enums/MUSIC_LOAD_CONDITION/MUSIC_LOAD_CONDITION_ALWAYS, "Always") GS_DEFINE(enums/MUSIC_LOAD_CONDITION/MUSIC_LOAD_CONDITION_NEVER, "Never") GS_DEFINE(enums/MUSIC_LOAD_CONDITION/MUSIC_LOAD_CONDITION_NON_AMBIENT, "Non-ambient") GS_DEFINE(enums/PROJECTILE_AREA_DAMAGE/PROJECTILE_AREA_DAMAGE_MULTI_SWEEP, "Multi-sweep") GS_DEFINE(enums/PROJECTILE_AREA_DAMAGE/PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP, "Single-sweep") GS_DEFINE(enums/QUICK_GUNS_MODE/QUICK_GUNS_MODE_DRAW_AND_HOLSTER, "Draw or holster") GS_DEFINE(enums/QUICK_GUNS_MODE/QUICK_GUNS_MODE_DRAW_ONLY, "Draw only") GS_DEFINE(enums/SCREENSHOT_FORMAT/SCREENSHOT_FORMAT_JPEG, "JPG") GS_DEFINE(enums/SCREENSHOT_FORMAT/SCREENSHOT_FORMAT_PNG, "PNG") GS_DEFINE(enums/SHADOW_TYPE/SHADOW_TYPE_CIRCLE, "Circle") GS_DEFINE(enums/SHADOW_TYPE/SHADOW_TYPE_OCTAGON, "Octagon") GS_DEFINE(enums/SHADOW_TYPE/SHADOW_TYPE_SPRITE, "Sprite") GS_DEFINE(enums/STATS_STYLE/STATS_STYLE_BARE, "Bare") GS_DEFINE(enums/STATS_STYLE/STATS_STYLE_BORDERED, "Bordered") GS_DEFINE(enums/SUNGLASSES_MODE/SUNGLASSES_MODE_OFF, "Off") GS_DEFINE(enums/SUNGLASSES_MODE/SUNGLASSES_MODE_OPAQUE, "Opaque") GS_DEFINE(enums/SUNGLASSES_MODE/SUNGLASSES_MODE_TRANSPARENT, "Transparent") GS_DEFINE(enums/TARGET_LOCK_MODE/TARGET_LOCK_MODE_FULL, "Full lock") GS_DEFINE(enums/TARGET_LOCK_MODE/TARGET_LOCK_MODE_NONE, "No lock") GS_DEFINE(enums/TARGET_LOCK_MODE/TARGET_LOCK_MODE_SEMI, "Semi lock") GS_DEFINE(enums/TEXTURE_FILTER/TEXTURE_FILTER_BILINEAR, "Bilinear") GS_DEFINE(enums/TEXTURE_FILTER/TEXTURE_FILTER_POINT, "Point") GS_DEFINE(enums/UI_ELEMENT_LOCATION/UI_ELEMENT_LOCATION_BOTTOM_CENTER, "Bottom center") GS_DEFINE(enums/UI_ELEMENT_LOCATION/UI_ELEMENT_LOCATION_BOTTOM_LEFT, "Bottom left") GS_DEFINE(enums/UI_ELEMENT_LOCATION/UI_ELEMENT_LOCATION_BOTTOM_RIGHT, "Bottom right") GS_DEFINE(enums/UI_ELEMENT_LOCATION/UI_ELEMENT_LOCATION_TOP_CENTER, "Top center") GS_DEFINE(enums/UI_ELEMENT_LOCATION/UI_ELEMENT_LOCATION_TOP_LEFT, "Top left") GS_DEFINE(enums/UI_ELEMENT_LOCATION/UI_ELEMENT_LOCATION_TOP_RIGHT, "Top right") GS_DEFINE(enums/UI_STYLE/UI_STYLE_PC, "PC") GS_DEFINE(enums/UI_STYLE/UI_STYLE_PS1, "PS1") GS_DEFINE(enums/WALL_GLITCH_MODE/WALL_GLITCH_FIXED, "Fixed") GS_DEFINE(enums/WALL_GLITCH_MODE/WALL_GLITCH_TR1, "TR1") GS_DEFINE(enums/WALL_GLITCH_MODE/WALL_GLITCH_TR2, "TR2") // Setting names GS_DEFINE(settings/audio.ambient_volume/title, "Ambient volume") GS_DEFINE(settings/audio.cutscene_volume/title, "Cutscene volume") GS_DEFINE(settings/audio.enable_lara_mic/title, "Microphone near Lara") GS_DEFINE(settings/audio.enable_music_in_inventory/title, "Play music in inventory") GS_DEFINE(settings/audio.enable_music_in_menu/title, "Main menu music") GS_DEFINE(settings/audio.enable_pitched_sounds/title, "Pitched sounds") GS_DEFINE(settings/audio.enable_ps1_sfx/title, "PS1 SFX replacements") GS_DEFINE(settings/audio.enable_underwater_anim_sfx/title, "Underwater animation SFX") GS_DEFINE(settings/audio.fix_chainblock_secret_sound/title, "Fix chain block sound") GS_DEFINE(settings/audio.fix_secrets_killing_music/title, "Layered secret music") GS_DEFINE(settings/audio.fix_speeches_killing_music/title, "Layered enemy speech") GS_DEFINE(settings/audio.fmv_volume/title, "FMV volume") GS_DEFINE(settings/audio.inventory_ambient_volume/title, "Ambient volume (inventory)") GS_DEFINE(settings/audio.inventory_music_volume/title, "Music volume (inventory)") GS_DEFINE(settings/audio.load_music_triggers/title, "Fix one-shot music triggers") GS_DEFINE(settings/audio.master_volume/title, "\\{icon music} Master volume") GS_DEFINE(settings/audio.music_load_condition/title, "Restore music on load") GS_DEFINE(settings/audio.music_volume/title, "Music volume") GS_DEFINE(settings/audio.mute_out_of_focus/title, "Mute audio when focus lost") GS_DEFINE(settings/audio.sound_volume/title, "\\{icon sound} Sound volume") GS_DEFINE(settings/audio.underwater_ambient_volume/title, "Ambient volume (underwater)") GS_DEFINE(settings/audio.underwater_music_volume/title, "Music volume (underwater)") GS_DEFINE(settings/debug.enable_endless_flare_time/title, "Endless flare time") GS_DEFINE(settings/debug.enable_endless_sprint/title, "Endless sprint") GS_DEFINE(settings/gameplay.ally_hostility_policy/title, "Ally hostility policy") GS_DEFINE(settings/gameplay.camera_speed/title, "Camera speed") GS_DEFINE(settings/gameplay.change_pierre_spawn/title, "Change Pierre spawn mode") GS_DEFINE(settings/gameplay.creature_drown_policy/title, "Creature drown policy") GS_DEFINE(settings/gameplay.disable_extra_guns/title, "Remove extra guns") GS_DEFINE(settings/gameplay.disable_healing_between_levels/title, "Persistent damage") GS_DEFINE(settings/gameplay.disable_medpacks/title, "Remove medipacks") GS_DEFINE(settings/gameplay.disable_trex_collision/title, "Remove dead T-Rex collision") GS_DEFINE(settings/gameplay.enable_ally_targeting/title, "Allow targeting allies") GS_DEFINE(settings/gameplay.enable_auto_item_selection/title, "Key item pre-selection") GS_DEFINE(settings/gameplay.enable_back_slope_stumble/title, "Backwards slope stumble") GS_DEFINE(settings/gameplay.enable_body_bags/title, "Body bag triggers") GS_DEFINE(settings/gameplay.enable_boulder_shake/title, "Enable boulder shake") GS_DEFINE(settings/gameplay.enable_bouncy_grenades/title, "Bouncy grenades") GS_DEFINE(settings/gameplay.enable_cheats/title, "Cheats") GS_DEFINE(settings/gameplay.enable_cinematics/title, "Cinematics") GS_DEFINE(settings/gameplay.enable_compass_stats/title, "Level statistics in compass") GS_DEFINE(settings/gameplay.enable_console/title, "Console") GS_DEFINE(settings/gameplay.enable_controlled_drops/title, "Controlled drops") GS_DEFINE(settings/gameplay.enable_crawl_jump/title, "Crawl exit jump") GS_DEFINE(settings/gameplay.enable_crawl_tilt/title, "Crawl tilt") GS_DEFINE(settings/gameplay.enable_crawling/title, "Crawling") GS_DEFINE(settings/gameplay.enable_credits/title, "Credit screens") GS_DEFINE(settings/gameplay.enable_crouch_roll/title, "Crouch roll") GS_DEFINE(settings/gameplay.enable_cutscenes/title, "Cutscenes") GS_DEFINE(settings/gameplay.enable_demo/title, "Demo mode") GS_DEFINE(settings/gameplay.enable_enemy_rotation/title, "Randomize enemy start angle") GS_DEFINE(settings/gameplay.enable_enhanced_saves/title, "Save effects") GS_DEFINE(settings/gameplay.enable_fmv/title, "FMVs") GS_DEFINE(settings/gameplay.enable_game_modes/title, "Game mode selection") GS_DEFINE(settings/gameplay.enable_idle_pose_camera/title, "Idle pose camera") GS_DEFINE(settings/gameplay.enable_inverted_look/title, "Inverted look") GS_DEFINE(settings/gameplay.enable_item_examining/title, "Item examination") GS_DEFINE(settings/gameplay.enable_jump_twists/title, "Jump twists") GS_DEFINE(settings/gameplay.enable_killer_pushblocks/title, "Enable killer pushblocks") GS_DEFINE(settings/gameplay.enable_lean_jumping/title, "Lean jumping") GS_DEFINE(settings/gameplay.enable_ledge_jumps/title, "Ledge jumps") GS_DEFINE(settings/gameplay.enable_legal/title, "Legal screens") GS_DEFINE(settings/gameplay.enable_manual_camera/title, "Manual camera") GS_DEFINE(settings/gameplay.enable_neutral_twists/title, "Neutral twists") GS_DEFINE(settings/gameplay.enable_pickup_aids/title, "Pickup aids") GS_DEFINE(settings/gameplay.enable_play_previous_levels/title, "Play previous levels") GS_DEFINE(settings/gameplay.pause_on_focus_lost/title, "Pause when focus lost") GS_DEFINE(settings/gameplay.enable_responsive_crawl/title, "Responsive crawling") GS_DEFINE(settings/gameplay.enable_responsive_sprint/title, "Responsive sprinting") GS_DEFINE(settings/gameplay.enable_save_crystals/title, "Save crystals") GS_DEFINE(settings/gameplay.enable_slide_to_run/title, "Slide-to-run") GS_DEFINE(settings/gameplay.enable_slow_ledge_swing/title, "Slow ledge swing") GS_DEFINE(settings/gameplay.enable_smooth_wall_deflect/title, "Smooth wall deflection") GS_DEFINE(settings/gameplay.enable_soft_statics/title, "Soft static collision") GS_DEFINE(settings/gameplay.enable_sprint/title, "Sprinting") GS_DEFINE(settings/gameplay.enable_step_roll_boost/title, "Step roll boost") GS_DEFINE(settings/gameplay.enable_swing_cancel/title, "Swing cancels") GS_DEFINE(settings/gameplay.enable_target_change/title, "Target change") GS_DEFINE(settings/gameplay.enable_timer_in_inventory/title, "Timer counts in inventory") GS_DEFINE(settings/gameplay.enable_toggle_crouch/title, "Toggle crouch") GS_DEFINE(settings/gameplay.enable_toggle_sprint/title, "Toggle sprint") GS_DEFINE(settings/gameplay.enable_total_stats/title, "Final statistics screen") GS_DEFINE(settings/gameplay.enable_tr2_jumping/title, "Responsive jumping") GS_DEFINE(settings/gameplay.enable_tr2_swim_cancel/title, "Responsive swim cancel") GS_DEFINE(settings/gameplay.enable_tr2_swimming/title, "Smooth swimming") GS_DEFINE(settings/gameplay.enable_uw_roll/title, "Underwater roll") GS_DEFINE(settings/gameplay.enable_wading/title, "Wading") GS_DEFINE(settings/gameplay.enable_walk_to_items/title, "Animated interactions") GS_DEFINE(settings/gameplay.fix_alligator_ai/title, "Fix alligator AI") GS_DEFINE(settings/gameplay.fix_bear_ai/title, "Fix bear AI") GS_DEFINE(settings/gameplay.fix_bridge_collision/title, "Fix bridge collision") GS_DEFINE(settings/gameplay.fix_descending_glitch/title, "Fix breakable floor falls") GS_DEFINE(settings/gameplay.fix_flare_throw_priority/title, "Fix flare throw priority") GS_DEFINE(settings/gameplay.fix_floor_data_issues/title, "Fix floor data issues") GS_DEFINE(settings/gameplay.fix_free_flare_glitch/title, "Fix free flare glitch") GS_DEFINE(settings/gameplay.fix_item_duplication_glitch/title, "Fix item duplication glitch") GS_DEFINE(settings/gameplay.fix_lara_pickup_embed/title, "Fix pickup embed glitch") GS_DEFINE(settings/gameplay.fix_m16_accuracy/title, "Fix M16/MP5 accuracy") GS_DEFINE(settings/gameplay.fix_monkey_pickup_priority/title, "Fix monkey pickup priority") GS_DEFINE(settings/gameplay.fix_pipeman_aim/title, "Fix pipeman aim") GS_DEFINE(settings/gameplay.fix_qwop_glitch/title, "Fix QWOP glitch") GS_DEFINE(settings/gameplay.fix_step_glitch/title, "Fix step glitch") GS_DEFINE(settings/gameplay.fix_wade_wall_hit/title, "Fix wading wall hit") GS_DEFINE(settings/gameplay.fix_walk_run_jump/title, "Fix walk run jump") GS_DEFINE(settings/gameplay.fix_wall_geometry/title, "Fix wall geometry") GS_DEFINE(settings/gameplay.fix_water_exit/title, "Fix water exit") GS_DEFINE(settings/gameplay.harpoon_recoil/title, "Harpoon recoil") GS_DEFINE(settings/gameplay.idle_pose_timeout/title, "Idle pose timeout") GS_DEFINE(settings/gameplay.jump_lock_mode/title, "Jump lock mode") GS_DEFINE(settings/gameplay.loading_screens/title, "Loading screens") GS_DEFINE(settings/gameplay.look_mode/title, "Look mode") GS_DEFINE(settings/gameplay.maximum_quick_save_slots/title, "Number of quick save slots") GS_DEFINE(settings/gameplay.maximum_save_slots/title, "Number of save slots") GS_DEFINE(settings/gameplay.projectile_area_damage/title, "Projectile area damage") GS_DEFINE(settings/gameplay.remember_gun_status/title, "Remember guns between levels") GS_DEFINE(settings/gameplay.restore_ps1_enemies/title, "Restore PS1 enemies") GS_DEFINE(settings/gameplay.start_lara_hitpoints/title, "Lara's starting health") GS_DEFINE(settings/gameplay.target_mode/title, "Weapon lock mode") GS_DEFINE(settings/gameplay.wall_glitch_mode/title, "Wall glitch mode") GS_DEFINE(settings/input.enable_buffering_func_keys/title, "Buffering (F-keys)") GS_DEFINE(settings/input.enable_buffering_inventory/title, "Buffering (inventory)") GS_DEFINE(settings/input.enable_responsive_passport/title, "Responsive passport") GS_DEFINE(settings/input.enable_tr3_sidesteps/title, "Enhanced sidesteps") GS_DEFINE(settings/input.quick_guns_mode/title, "Quick gun keys") GS_DEFINE(settings/language/title, "Language") GS_DEFINE(settings/rendering.anisotropy_filter/title, "Anisotropy filter") GS_DEFINE(settings/rendering.aspect_mode/title, "Aspect mode") GS_DEFINE(settings/rendering.borders/title, "Borders") GS_DEFINE(settings/rendering.enable_trapezoid_filter/title, "Trapezoid filter") GS_DEFINE(settings/rendering.enable_vsync/title, "VSync") GS_DEFINE(settings/rendering.fps/title, "FPS") GS_DEFINE(settings/rendering.lighting_contrast/title, "Lighting contrast") GS_DEFINE(settings/rendering.screenshot_format/title, "Screenshot format") GS_DEFINE(settings/rendering.sprite_lock_mode/title, "Sprite lock mode") GS_DEFINE(settings/rendering.texture_filter/title, "Texture filter") GS_DEFINE(settings/rendering.ui_filter/title, "UI filter") GS_DEFINE(settings/rendering.upscaling_factor/title, "Upscaling factor") GS_DEFINE(settings/rendering.upscaling_filter/title, "Upscaling filter") GS_DEFINE(settings/ui.airbar_color/title, "Airbar color") GS_DEFINE(settings/ui.airbar_color_ps1/title, "Airbar color") GS_DEFINE(settings/ui.airbar_location/title, "Airbar location") GS_DEFINE(settings/ui.ammo_counter_location/title, "Ammo counter location") GS_DEFINE(settings/ui.bar_look/title, "Bars appearance") GS_DEFINE(settings/ui.bar_scale/title, "Bars scale") GS_DEFINE(settings/ui.enable_bar_flashing/title, "Flash bars") GS_DEFINE(settings/ui.enable_smooth_bars/title, "Smooth bars") GS_DEFINE(settings/ui.enable_wraparound/title, "Scroll wrap") GS_DEFINE(settings/ui.enemy_healthbar_color/title, "Enemy bar color") GS_DEFINE(settings/ui.enemy_healthbar_color_allies/title, "Ally bar color") GS_DEFINE(settings/ui.enemy_healthbar_color_allies_ps1/title, "Ally bar color") GS_DEFINE(settings/ui.enemy_healthbar_color_ps1/title, "Enemy bar color") GS_DEFINE(settings/ui.enemy_healthbar_location/title, "Enemy bar location") GS_DEFINE(settings/ui.enemy_healthbar_show_mode/title, "Enemy bar mode") GS_DEFINE(settings/ui.exposurebar_color/title, "Exposure bar color") GS_DEFINE(settings/ui.exposurebar_color_ps1/title, "Exposure bar color") GS_DEFINE(settings/ui.exposurebar_location/title, "Exposure bar location") GS_DEFINE(settings/ui.healthbar_color/title, "Healthbar color") GS_DEFINE(settings/ui.healthbar_color_ps1/title, "Healthbar color") GS_DEFINE(settings/ui.healthbar_location/title, "Healthbar location") GS_DEFINE(settings/ui.healthbar_poison_color/title, "Poison healthbar color") GS_DEFINE(settings/ui.healthbar_poison_color_ps1/title, "Poison healthbar color") GS_DEFINE(settings/ui.inventory_background_style/title, "Inventory background") GS_DEFINE(settings/ui.inventory_fade_effects/title, "Inventory fade effects") GS_DEFINE(settings/ui.menu_style/title, "Menu style") GS_DEFINE(settings/ui.pause_background_style/title, "Pause background") GS_DEFINE(settings/ui.pause_fade_effects/title, "Pause fade effects") GS_DEFINE(settings/ui.pickup_scale/title, "Pickup scale") GS_DEFINE(settings/ui.show_bars/title, "Show bars") GS_DEFINE(settings/ui.show_pickups_overlay/title, "Pickups overlay") GS_DEFINE(settings/ui.show_title_version/title, "Title version text") GS_DEFINE(settings/ui.sprintbar_color/title, "Sprintbar color") GS_DEFINE(settings/ui.sprintbar_color_ps1/title, "Sprintbar color") GS_DEFINE(settings/ui.sprintbar_location/title, "Sprintbar location") GS_DEFINE(settings/ui.stats.show_ammo/title, "Ammo hits/used") GS_DEFINE(settings/ui.stats.show_deaths/title, "Deaths") GS_DEFINE(settings/ui.stats.show_distance_travelled/title, "Distance traveled") GS_DEFINE(settings/ui.stats.show_kills/title, "Kills") GS_DEFINE(settings/ui.stats.show_level_header/title, "Level counter") GS_DEFINE(settings/ui.stats.show_medipacks_used/title, "Health packs used") GS_DEFINE(settings/ui.stats.show_crystals/title, "Crystals") GS_DEFINE(settings/ui.stats.show_pickups/title, "Pickups") GS_DEFINE(settings/ui.stats.show_secrets/title, "Secrets found") GS_DEFINE(settings/ui.stats.show_time_taken/title, "Time taken") GS_DEFINE(settings/ui.stats.show_totals/title, "Show totals") GS_DEFINE(settings/ui.stats.style/title, "Statistics style") GS_DEFINE(settings/ui.stats_background_style/title, "Stats background") GS_DEFINE(settings/ui.stats_fade_effects/title, "Stats fade effects") GS_DEFINE(settings/ui.text_scale/title, "Text scale") GS_DEFINE(settings/visuals.camera_mode/title, "Camera mode") GS_DEFINE(settings/visuals.enable_3d_pickups/title, "3D pickups") GS_DEFINE(settings/visuals.enable_braid/title, "Lara's braid") GS_DEFINE(settings/visuals.enable_breeze/title, "Breeze") GS_DEFINE(settings/visuals.blood_effects/title, "Blood effects") GS_DEFINE(settings/visuals.enable_exit_fade_effects/title, "Fade on game exit") GS_DEFINE(settings/visuals.enable_fade_effects/title, "Fade effects") GS_DEFINE(settings/visuals.enable_fire_lighting/title, "Fire lighting") GS_DEFINE(settings/visuals.enable_footprints/title, "Footprints") GS_DEFINE(settings/visuals.enable_glide_cameras/title, "Glide cameras") GS_DEFINE(settings/visuals.enable_gun_lighting/title, "Gun lighting") GS_DEFINE(settings/visuals.enable_ps1_crystals/title, "PS1 crystal tint") GS_DEFINE(settings/visuals.enable_reflections/title, "Reflections") GS_DEFINE(settings/visuals.enable_responsive_mesh_tint/title, "Responsive mesh tint") GS_DEFINE(settings/visuals.enable_shotgun_flash/title, "Shotgun flash") GS_DEFINE(settings/visuals.enable_skybox/title, "Skyboxes") GS_DEFINE(settings/visuals.enable_weather/title, "Weather") GS_DEFINE(settings/visuals.fix_animated_sprites/title, "Fix sprite animations") GS_DEFINE(settings/visuals.fix_item_rots/title, "Fix item rotation issues") GS_DEFINE(settings/visuals.fix_texture_issues/title, "Fix texture issues") GS_DEFINE(settings/visuals.fog_color/title, "Fog color") GS_DEFINE(settings/visuals.fog_end/title, "Fog end") GS_DEFINE(settings/visuals.fog_start/title, "Fog start") GS_DEFINE(settings/visuals.fog_transparency/title, "Fog transparency") GS_DEFINE(settings/visuals.fov/title, "Field of view") GS_DEFINE(settings/visuals.game_brightness/title, "Game brightness") GS_DEFINE(settings/visuals.gamma/title, "Gamma") GS_DEFINE(settings/visuals.lara_outfit/title, "Lara's outfit") GS_DEFINE(settings/visuals.shadow_type/title, "Shadows shape") GS_DEFINE(settings/visuals.sunglasses_mode/title, "Lara's sunglasses") GS_DEFINE(settings/visuals.ui_brightness/title, "UI brightness") GS_DEFINE(settings/visuals.water_color/title, "Water color") // Setting descriptions GS_DEFINE(settings/audio.ambient_volume/description, "Adjusts ambient volume.") GS_DEFINE(settings/audio.cutscene_volume/description, "Adjusts the ingame cutscenes volume.") GS_DEFINE(settings/audio.enable_lara_mic/description, "Set the microphone to be at Lara's position. If disabled, the microphone will be at the camera's position.") GS_DEFINE(settings/audio.enable_music_in_inventory/description, "Lets game sounds, ambient and music continue playing in the inventory screen.") GS_DEFINE(settings/audio.enable_music_in_menu/description, "Plays music in the main menu.") GS_DEFINE(settings/audio.enable_pitched_sounds/description, "Allows sound effects to be randomly, slightly pitched to vary the game sounds.") GS_DEFINE(settings/audio.enable_ps1_sfx/description, "Enables specific sound effect replacements using PS1 equivalents.\n\n- Uzi fire (TR1 only)\n- Lara barefoot sounds (TR2 only)") GS_DEFINE(settings/audio.enable_underwater_anim_sfx/description, "Allows control over playing specific animation sound effects - for objects such as doors and trapdoors - when the camera is underwater.") GS_DEFINE(settings/audio.fix_chainblock_secret_sound/description, "Prevents the secret sound from incorrectly playing when using the golden key in Tomb of Tihocan.") GS_DEFINE(settings/audio.fix_secrets_killing_music/description, "Fixes the sound of collecting a secret stopping the active music track.") GS_DEFINE(settings/audio.fix_speeches_killing_music/description, "Fixes enemies stopping the active music track when they speak.") GS_DEFINE(settings/audio.fmv_volume/description, "Adjusts the movies volume.") GS_DEFINE(settings/audio.inventory_ambient_volume/description, "Adjusts ambient volume in inventory screen.") GS_DEFINE(settings/audio.inventory_music_volume/description, "Adjusts music volume in inventory screen.") GS_DEFINE(settings/audio.load_music_triggers/description, "Loads previously triggered, one shot music so one shot music tracks do not replay.") GS_DEFINE(settings/audio.master_volume/description, "Adjusts all ingame volume. The rest of the settings are relative to this volume.") GS_DEFINE(settings/audio.music_load_condition/description, "Loads the music track that was playing when the game was saved.\n\n- Never: do not restore music tracks on load.\n- Non-ambient: restore only non-ambient music tracks on load.\n- Always: restore any kind of music track on load.") GS_DEFINE(settings/audio.music_volume/description, "Adjusts music volume.") GS_DEFINE(settings/audio.mute_out_of_focus/description, "Mutes all music and sound effects when the game window is not focused.") GS_DEFINE(settings/audio.sound_volume/description, "Adjusts sound effects volume.") GS_DEFINE(settings/audio.underwater_ambient_volume/description, "Adjusts ambient volume when underwater.") GS_DEFINE(settings/audio.underwater_music_volume/description, "Adjusts music volume when underwater.") GS_DEFINE(settings/debug.enable_endless_flare_time/description, "Prevents the handheld flares from ever going out. Thrown flares will still go out as normal.") GS_DEFINE(settings/debug.enable_endless_sprint/description, "Prevents Lara from ever getting tired when sprinting. Obstacles will still bring her to a stop.") GS_DEFINE(settings/gameplay.ally_hostility_policy/description, "Controls how friendly units react when taking damage.\n\n- Individual: each ally changes hostility on their own (TR3 style).\n- Shared: all allies become hostile together (TR2 monk style).") GS_DEFINE(settings/gameplay.camera_speed/description, "Changes how fast the manual camera moves.") GS_DEFINE(settings/gameplay.change_pierre_spawn/description, "Makes a freshly triggered (runaway) Pierre replace an already existing (runaway) Pierre.") GS_DEFINE(settings/gameplay.creature_drown_policy/description, "Controls how land creatures behave in water rooms.\n\n- Never: land creatures will never drown (TR1 style).\n- Default: land creatures will drown in 2-click or deeper water (TR2/3 style).\n- Submerged: land creatures will drown only when fully submerged.") GS_DEFINE(settings/gameplay.disable_extra_guns/description, "Removes all weapon and ammo pickups from the game except Pistols (for Pistols Only challenge runs).") GS_DEFINE(settings/gameplay.disable_healing_between_levels/description, "Stops Lara from healing when starting a new level (for no Heal challenge runs).") GS_DEFINE(settings/gameplay.disable_medpacks/description, "Removes all medipacks from the game (for No Meds challenge runs).") GS_DEFINE(settings/gameplay.disable_trex_collision/description, "Removes all collision with T-Rex upon death. This helps when the T-Rex's body blocks the passage out.") GS_DEFINE(settings/gameplay.enable_ally_targeting/description, "Allows Lara to target allies, such as monks. If disabled, allies will be immune to Lara's ammunition.") GS_DEFINE(settings/gameplay.enable_auto_item_selection/description, "When Lara presses action against a keyhole or puzzle slot, and she has the corresponding item in the inventory, that item will be pre-selected.") GS_DEFINE(settings/gameplay.enable_back_slope_stumble/description, "Makes Lara perform a stumble if she hops back and there is a slope behind her (TR3). If disabled, Lara will come to a hard stop against the slope (TR1/2).") GS_DEFINE(settings/gameplay.enable_body_bags/description, "Enables removal of killed enemies when Lara crosses specific triggers in certain levels. If disabled, dead enemies will always be drawn.") GS_DEFINE(settings/gameplay.enable_boulder_shake/description, "If enabled, the camera will shake when a boulder is in motion.") GS_DEFINE(settings/gameplay.enable_bouncy_grenades/description, "Enables TR3-style grenade behavior: they ricochet off walls and slopes and produce a larger blast radius, but at the expense of reduced velocity.") GS_DEFINE(settings/gameplay.enable_cheats/description, "Enables various cheats:\n\n- L: immediately end the level.\n- I: give Lara all weapons; a boost of ammo and medipacks; and all plot items for the current level.\n- O: enable fly cheat (swimming midair).\n - WALK key: exit fly mode.\n - GUN key: open the closest door (doesn't work in some places).") GS_DEFINE(settings/gameplay.enable_cinematics/description, "Enables cinematics at the beginning of certain levels that have them defined.") GS_DEFINE(settings/gameplay.enable_compass_stats/description, "Enables showing level statistics when the compass is selected.") GS_DEFINE(settings/gameplay.enable_console/description, "Enables the developer console.") GS_DEFINE(settings/gameplay.enable_controlled_drops/description, "Allows Lara to turn mid-air and grab the ledge she just stepped off, if the action input is held while falling.") GS_DEFINE(settings/gameplay.enable_crawl_jump/description, "Allows Lara to jump out of crawlspaces.") GS_DEFINE(settings/gameplay.enable_crawl_tilt/description, "Aligns Lara's rotation to the floor geometry when crawling.") GS_DEFINE(settings/gameplay.enable_crawling/description, "Allows Lara to crouch and crawl.") GS_DEFINE(settings/gameplay.enable_credits/description, "Enables credits screens shown after completing the game. Does not influence the final statistics screen.") GS_DEFINE(settings/gameplay.enable_crouch_roll/description, "Allows Lara to do a forward roll while crouched by pressing sprint.") GS_DEFINE(settings/gameplay.enable_cutscenes/description, "Enables cutscenes playing.") GS_DEFINE(settings/gameplay.enable_demo/description, "Enables demos showing in the main menu.") GS_DEFINE(settings/gameplay.enable_enemy_rotation/description, "Applies an additional random angle to some enemies when they are initialised.") GS_DEFINE(settings/gameplay.enable_enhanced_saves/description, "Enhances savegames so that graphic effects, waterfall mist, flame emitters, and more are saved instead of disappearing on load.") GS_DEFINE(settings/gameplay.enable_fmv/description, "Enables FMVs playing.") GS_DEFINE(settings/gameplay.enable_game_modes/description, "Allows new game plus options to be selected from the new game passport menu.\n\n- New Game+: unlocks all weapons with infinite ammo; enemies have double the HP.\n- Japanese NG: weapons do double damage and flare pickups contain 8 rather than 6.\n- Japanese NG+: combination of New Game+ and Japanese NG.") GS_DEFINE(settings/gameplay.enable_idle_pose_camera/description, "Adjusts the camera to face Lara during her idle pose animation. Pressing look will reset the camera.") GS_DEFINE(settings/gameplay.enable_inverted_look/description, "Inverts the Y axis controls when Lara looks.") GS_DEFINE(settings/gameplay.enable_item_examining/description, "For custom levels - allows item descriptions to be displayed in the inventory where the level author has provided suitable data.") GS_DEFINE(settings/gameplay.enable_jump_twists/description, "Enables jump twists and somersaults i.e. press roll during jump and swan dive animations.") GS_DEFINE(settings/gameplay.enable_killer_pushblocks/description, "If enabled, when a pushblock falls from the air and lands on Lara, it will kill her outright. Otherwise, Lara will clip on top of the block and survive.") GS_DEFINE(settings/gameplay.enable_lean_jumping/description, "Allows Lara to creep forwards or backwards further when performing neutral jumps with the relevant input key pressed.") GS_DEFINE(settings/gameplay.enable_ledge_jumps/description, "Allows Lara to jump upwards or backwards when hanging from a ledge, provided she has a solid surface in front of her to push against.") GS_DEFINE(settings/gameplay.enable_legal/description, "Enables legal screen and Core Design FMV at the game start.") GS_DEFINE(settings/gameplay.enable_manual_camera/description, "Enables the camera keys (\\{input camera_forward}\\{input camera_back}\\{input camera_left}\\{input camera_right}) used to control Photo Mode camera, to also rotate the ingame camera.") GS_DEFINE(settings/gameplay.enable_neutral_twists/description, "Allows Lara to twist in the air while performing a neutral jump. Press jump and roll inputs together while stationary.") GS_DEFINE(settings/gameplay.enable_pickup_aids/description, "Enables an intermittent twinkling effect near pickup items to highlight their presence.") GS_DEFINE(settings/gameplay.enable_play_previous_levels/description, "Enables the \"Play previous levels\" and \"Story so far...\" features in the New Game selection screen.") GS_DEFINE(settings/gameplay.pause_on_focus_lost/description, "Stops gameplay from advancing when the game window loses focus.") GS_DEFINE(settings/gameplay.enable_responsive_crawl/description, "Enables enhancements over original crawling mechanics.\n\n- Allows resuming crawling more quickly after coming to a stop.\n- Allows transitioning from run/sprint to crawl without first coming to a stop.\n- Allows transitioning from crawl to crouch-roll (if enabled) without manually crouching first.\n- Allows turning while crouched.\n- Restores Lara's crawl pickup animation (excluding flares).") GS_DEFINE(settings/gameplay.enable_responsive_sprint/description, "Enables a more responsive sprinting state for Lara.\n\n- allows sprinting whenever Lara has energy, rather than only when her stamina is full.\n- allows sprinting up stairs rather than being interrupted by Lara's regular run animation.") GS_DEFINE(settings/gameplay.enable_save_crystals/description, "Limits saving to the beginning of levels and save crystals. Levels have limited, single use save crystals like the PS1 version. Changing this option will require restarting the level.") GS_DEFINE(settings/gameplay.enable_slide_to_run/description, "Allows Lara to start running immediately when she reaches ground after sliding forwards on a slope. Hold the forward input to activate.") GS_DEFINE(settings/gameplay.enable_slow_ledge_swing/description, "Allows Lara to swing slowly when she has grabbed a very thin ledge (TR3 style). If disabled, Lara will swing briefly before coming to a resting hanging position (TR1/2 style).") GS_DEFINE(settings/gameplay.enable_smooth_wall_deflect/description, "Allows Lara to recover more quickly after hitting a wall and a direction key is held together with forward.") GS_DEFINE(settings/gameplay.enable_soft_statics/description, "Allows Lara to move smoothly against static meshes - similar to TR4+ - rather than coming to a hard stop.") GS_DEFINE(settings/gameplay.enable_sprint/description, "Allows Lara to sprint, similar to TR3+.") GS_DEFINE(settings/gameplay.enable_step_roll_boost/description, "Allows Lara to be boosted off a one-click high step if roll is pressed near the edge.") GS_DEFINE(settings/gameplay.enable_swing_cancel/description, "Allows Lara's ledge-swinging animation to be canceled by letting go and quickly grabbing again.") GS_DEFINE(settings/gameplay.enable_target_change/description, "Enables TR4+ target changing while aiming weapons. Press the Change Target button while aiming to change targets.") GS_DEFINE(settings/gameplay.enable_timer_in_inventory/description, "Makes the in-game timer work even while the game is showing the inventory.") GS_DEFINE(settings/gameplay.enable_toggle_crouch/description, "Allows Lara to stay crouched after pressing the crouch input once. Press crouch again to stand up.") GS_DEFINE(settings/gameplay.enable_toggle_sprint/description, "Allows Lara to keep sprinting after pressing the sprint input once. Press sprint again to stop sprinting.") GS_DEFINE(settings/gameplay.enable_total_stats/description, "Enables a total game statistics screen that plays after the credits.") GS_DEFINE(settings/gameplay.enable_tr2_jumping/description, "Allows Lara to jump at any point while running.") GS_DEFINE(settings/gameplay.enable_tr2_swim_cancel/description, "Allows Lara to stop more responsively underwater when the swim key is released.") GS_DEFINE(settings/gameplay.enable_tr2_swimming/description, "Gives Lara's underwater turn rate an acceleration curve for smoother movement, as per TR2+ originally. Disabling this option will give Lara a snappier turn rate, per original TR1.") GS_DEFINE(settings/gameplay.enable_uw_roll/description, "Allows Lara to roll while underwater.") GS_DEFINE(settings/gameplay.enable_wading/description, "Allows Lara to wade through shallow water, rather than becoming stuck on the water surface.") GS_DEFINE(settings/gameplay.enable_walk_to_items/description, "Makes Lara walk to pickups and switches when nearby instead of teleporting to them.") GS_DEFINE(settings/gameplay.fix_alligator_ai/description, "Fixes alligators dealing no damage if Lara remains still in the water.") GS_DEFINE(settings/gameplay.fix_bear_ai/description, "Fixes the bear pat attack so it does not miss Lara.") GS_DEFINE(settings/gameplay.fix_bridge_collision/description, "Fixes Lara not being able to grab parts of some bridges and invisible walls at the edge. Also fixes collision issues with drawbridges, trapdoors, and bridges when stacked over each other, over slopes, and near the ground.") GS_DEFINE(settings/gameplay.fix_descending_glitch/description, "Fixes sidestepping and walking backwards on breakable tiles causing Lara to immediately descend to the tile underneath.") GS_DEFINE(settings/gameplay.fix_flare_throw_priority/description, "Fixes Lara prioritising throwing a spent flare while in mid-air, which can lead to being unable to grab ledges.") GS_DEFINE(settings/gameplay.fix_floor_data_issues/description, "Fixes original issues with floor data/triggers.") GS_DEFINE(settings/gameplay.fix_free_flare_glitch/description, "Fixes the ability to spawn a free flare when pressing the flare input while picking up any item.") GS_DEFINE(settings/gameplay.fix_item_duplication_glitch/description, "Fixes the ability to duplicate usage of key items in the inventory.") GS_DEFINE(settings/gameplay.fix_lara_pickup_embed/description, "Fixes Lara sometimes drifting into walls when collecting underwater items, and fixes Lara embedding under steeply sloped ceilings when picking up an item above water.") GS_DEFINE(settings/gameplay.fix_m16_accuracy/description, "Fixes the accuracy of the M16/MP5 while Lara is running.") GS_DEFINE(settings/gameplay.fix_monkey_pickup_priority/description, "Attacked monkeys will prioritize retaliating over collecting Medi packs and Keys.") GS_DEFINE(settings/gameplay.fix_pipeman_aim/description, "Fixes the pipeman sometimes not being able to aim darts at Lara correctly.") GS_DEFINE(settings/gameplay.fix_qwop_glitch/description, "Fixes Lara jumping on small steps sometimes resulting in a weird running animation, known as a QWOP state.") GS_DEFINE(settings/gameplay.fix_step_glitch/description, "Fixes Lara sometimes being pushed into walls adjacent to steps when running up them in a specific way.") GS_DEFINE(settings/gameplay.fix_wade_wall_hit/description, "Fixes Lara not responding to hitting a wall while wading.") GS_DEFINE(settings/gameplay.fix_walk_run_jump/description, "Fixes Lara at times not being able to jump immediately after going from her walking to running animation.") GS_DEFINE(settings/gameplay.fix_wall_geometry/description, "Fixes cases in OG level geometry where tilts inside walls can lead to inaccurate height calculations.") GS_DEFINE(settings/gameplay.fix_water_exit/description, "Fixes Lara being able to go directly from a water room to an adjacent dry room, or to a dry room below. Additionally, this will prevent Lara from being able to climb out of water onto non-standable slopes.") GS_DEFINE(settings/gameplay.harpoon_recoil/description, "Sets how often Lara must reload the harpoon gun, based on her current ammo count. For example, if set to 3, she'll need to reload after every third shot. Set to 0 to disable reloading entirely.") GS_DEFINE(settings/gameplay.idle_pose_timeout/description, "Allows Lara to enter a pose animation when she has been idle for the specified number of seconds. Set to 0 to disable.") GS_DEFINE(settings/gameplay.jump_lock_mode/description, "For responsive jumping, allows controlling how soon after starting to run that Lara is permitted to jump.\n\n- Legacy: matches original TR2 timing.\n- Tuned: jumping is possible 2 frames earlier.\n- Disabled: jumping is possible immediately after the start-to-run animation.") GS_DEFINE(settings/gameplay.loading_screens/description, "Controls loading screens before level loads.\n\n- Disabled: never show loading screens.\n- Always: show loading screens.\n- New games: skip showing loading screens when loading a save.") GS_DEFINE(settings/gameplay.look_mode/description, "Allows controlling when Lara is able to use look.\n\n- Restricted: look is only permitted when Lara is stationary, and never when underwater.\n- Enhanced: look is permitted during most animations, aside from ones such as pushing a block.\n- Unrestricted: look is permitted at any time during normal Lara control.") GS_DEFINE(settings/gameplay.maximum_quick_save_slots/description, "Changes the number of available quick save slots.") GS_DEFINE(settings/gameplay.maximum_save_slots/description, "Changes the number of available save slots.") GS_DEFINE(settings/gameplay.projectile_area_damage/description, "Controls how the area-of-effect for Rocket Launcher and Grenade Launcher propagates.\n\n- Single-sweep: TR1 & TR2 behavior.\n- Multi-sweep: TR3 behavior.\n\nThe multi-sweep option often ends up doing double damage to individual enemies.") GS_DEFINE(settings/gameplay.remember_gun_status/description, "Makes Lara remember which gun she was using last in the previous level when starting a new level. If disabled, Lara will revert to holstered pistols.") GS_DEFINE(settings/gameplay.restore_ps1_enemies/description, "Adds the mummy that appears in the PlayStation version of City of Khamoon, room 25.\nChanging this option will require restarting the game.") GS_DEFINE(settings/gameplay.start_lara_hitpoints/description, "Sets Lara's health value for the beginning of each level.") GS_DEFINE(settings/gameplay.target_mode/description, "Changes the behavior of how weapons lock onto targets.\n\n- Full lock: always keep target lock even if the enemy moves out of sight or dies (OG TR1-3).\n- Semi lock: keep target lock if the enemy moves out of sight but lose target lock if the enemy dies.\n- No lock: lose target lock if the enemy goes out of sight or dies (TR4+).") GS_DEFINE(settings/gameplay.wall_glitch_mode/description, "Allows using TR1 wall glitch behavior in TR2 and vice-versa; equally allows fixing all types of wall glitch.") GS_DEFINE(settings/input.enable_buffering_func_keys/description, "Enables F-key (1-frame) buffering to achieve precise control of Lara's movement. This function originally only exists in the TombATI port (TR1).") GS_DEFINE(settings/input.enable_buffering_inventory/description, "Enables inventory (2-frame) buffering to achieve precise control of Lara's movement.") GS_DEFINE(settings/input.enable_responsive_passport/description, "Disables blocking user input when passport flips pages, scheduling the page flips instead.") GS_DEFINE(settings/input.enable_tr3_sidesteps/description, "Enables TR3+ style sidesteps, e.g. shift+directional arrows. Dedicated sidestep buttons will still work.") GS_DEFINE(settings/input.quick_guns_mode/description, "Controls the behavior of the quick gun equip keys.\n\n- Draw only: pressing a key will cause Lara to equip the assigned gun.\n- Draw or holster: same as above, plus Lara will undraw the assigned gun if she's currently carrying it.") GS_DEFINE(settings/language/description, "Changes the language of the UI text.") GS_DEFINE(settings/rendering.anisotropy_filter/description, "Enhances texture filtering at distances.") GS_DEFINE(settings/rendering.aspect_mode/description, "Forces certain game aspect ratios with letterbox.") GS_DEFINE(settings/rendering.borders/description, "Adds black borders around the game window.") GS_DEFINE(settings/rendering.enable_trapezoid_filter/description, "Corrects rendering of quadrilaterals.") GS_DEFINE(settings/rendering.enable_vsync/description, "Turns V-Sync on or off.") GS_DEFINE(settings/rendering.fps/description, "Sets game frames per second.") GS_DEFINE(settings/rendering.lighting_contrast/description, "Boosts contrast for dynamic light sources such as flares and gun flashes.") GS_DEFINE(settings/rendering.screenshot_format/description, "Screenshot file format.") GS_DEFINE(settings/rendering.sprite_lock_mode/description, "Controls which axes to lock when showing sprites on the screen.\n\n- None: show the sprites normally.\n- Roll: lock the roll axis – useful only in photo mode.\n- Roll & pitch: ensure the sprites stand upright and do not lie on the ground when looking at them from above.\n- Perspective: lock roll and pitch axes and addititonally, rotate the sprites slightly towards the center of the screen.") GS_DEFINE(settings/rendering.texture_filter/description, "Switches between smooth and pixel ingame textures.") GS_DEFINE(settings/rendering.ui_filter/description, "Switches between smooth and pixel UI textures.") GS_DEFINE(settings/rendering.upscaling_factor/description, "Upscales game by a set factor, maintaining pixellated look.") GS_DEFINE(settings/rendering.upscaling_filter/description, "Switches smooth or pixel look for the whole screen.") GS_DEFINE(settings/ui.airbar_color/description, "Color of the airbar.") GS_DEFINE(settings/ui.airbar_color_ps1/description, "Color of the airbar.") GS_DEFINE(settings/ui.airbar_location/description, "Location where the airbar is displayed.") GS_DEFINE(settings/ui.ammo_counter_location/description, "Location where the ammo counter is displayed.") GS_DEFINE(settings/ui.bar_look/description, "Controls the visual appearance of the UI bars.") GS_DEFINE(settings/ui.bar_scale/description, "Changes size of UI bars.") GS_DEFINE(settings/ui.enable_bar_flashing/description, "Makes Lara's health and oxygen bars blink when she's running low on either resource.") GS_DEFINE(settings/ui.enable_smooth_bars/description, "Makes the UI bars use smooth color transitions.") GS_DEFINE(settings/ui.enable_wraparound/description, "Lets directional navigation in menus loop around.") GS_DEFINE(settings/ui.enemy_healthbar_color/description, "Color of the enemy healthbar.") GS_DEFINE(settings/ui.enemy_healthbar_color_allies/description, "Color of the allies healthbar. Shown in the location of the enemy healthbars.") GS_DEFINE(settings/ui.enemy_healthbar_color_allies_ps1/description, "Color of the allies healthbar. Shown in the location of the enemy healthbars.") GS_DEFINE(settings/ui.enemy_healthbar_color_ps1/description, "Color of the enemy healthbar.") GS_DEFINE(settings/ui.enemy_healthbar_location/description, "Location where the enemy healthbar is displayed.") GS_DEFINE(settings/ui.enemy_healthbar_show_mode/description, "Enables showing a healthbar for the active enemy.") GS_DEFINE(settings/ui.exposurebar_color/description, "Color of the cold water exposure bar.") GS_DEFINE(settings/ui.exposurebar_color_ps1/description, "Color of the cold water exposure bar.") GS_DEFINE(settings/ui.exposurebar_location/description, "Location where the cold water exposure bar is displayed.") GS_DEFINE(settings/ui.healthbar_color/description, "Color of the healthbar.") GS_DEFINE(settings/ui.healthbar_color_ps1/description, "Color of the healthbar.") GS_DEFINE(settings/ui.healthbar_location/description, "Location where the healthbar is displayed.") GS_DEFINE(settings/ui.healthbar_poison_color/description, "Color of the healthbar when Lara is poisoned.") GS_DEFINE(settings/ui.healthbar_poison_color_ps1/description, "Color of the healthbar when Lara is poisoned.") GS_DEFINE(settings/ui.inventory_background_style/description, "Changes the way the background for the inventory ring is displayed.\n\n- Dark: TR1 (PC).\n- Very dark: TR1 (PS1).\n- Static: TR2 (PC).\n- Wave: TR2 (PS1).\n- Monochrome: TR3.") GS_DEFINE(settings/ui.inventory_fade_effects/description, "Fine-tunes the fade effects to be enabled or disabled in the in-game inventory ring. Needs the Fade Effects option to be enabled to work.") GS_DEFINE(settings/ui.menu_style/description, "Changes how menus are displayed.\n\n - PC: UI style matches the PC version.\n - PS1: UI style matches the PS1 version.") GS_DEFINE(settings/ui.pause_background_style/description, "Changes the way the background for the pause screen is displayed.\n\n- Dark: TR1 (PC).\n- Very dark: TR1 (PS1).\n- Static: TR2 (PC).\n- Wave: TR2 (PS1).\n- Monochrome: TR3.") GS_DEFINE(settings/ui.pause_fade_effects/description, "Fine-tunes the fade effects to be enabled or disabled in the pause screen. Needs the Fade Effects option to be enabled to work.") GS_DEFINE(settings/ui.pickup_scale/description, "Changes size of items animated in the UI when Lara picks something up.") GS_DEFINE(settings/ui.show_bars/description, "Disables all ingame bars, obscuring information on Lara's health and other resources (for challenge runs).") GS_DEFINE(settings/ui.show_pickups_overlay/description, "Shows items in the bottom-right corner when Lara picks something up.") GS_DEFINE(settings/ui.show_title_version/description, "Shows the TRX version string in the title inventory ring.") GS_DEFINE(settings/ui.sprintbar_color/description, "Color of the sprintbar.") GS_DEFINE(settings/ui.sprintbar_color_ps1/description, "Color of the sprintbar.") GS_DEFINE(settings/ui.sprintbar_location/description, "Location where the sprintbar is displayed.") GS_DEFINE(settings/ui.stats.show_ammo/description, "Shows the ammo row in the level statistics.") GS_DEFINE(settings/ui.stats.show_deaths/description, "Shows Lara's deaths in the compass statistics and in the level statistics. Death count is updated in the currently loaded save as soon as Lara dies.") GS_DEFINE(settings/ui.stats.show_distance_travelled/description, "Shows the distance traveled row in the level statistics.") GS_DEFINE(settings/ui.stats.show_kills/description, "Shows the kills row in the level statistics.") GS_DEFINE(settings/ui.stats.show_level_header/description, "Shows the current level number at the top of the level statistics.") GS_DEFINE(settings/ui.stats.show_medipacks_used/description, "Shows the health packs used row in the level statistics.") GS_DEFINE(settings/ui.stats.show_crystals/description, "Shows the crystals row in the level statistics.") GS_DEFINE(settings/ui.stats.show_pickups/description, "Shows the pickups row in the level statistics.") GS_DEFINE(settings/ui.stats.show_secrets/description, "Shows the secrets found row in the level statistics.") GS_DEFINE(settings/ui.stats.show_time_taken/description, "Shows the time taken row in the level statistics.") GS_DEFINE(settings/ui.stats.show_totals/description, "Shows totals next to stats when applicable. Secrets remain unaffected by this setting.") GS_DEFINE(settings/ui.stats.style/description, "Controls how the statistics dialog is displayed.\n\n- Bare: shows the simpler unframed layout.\n- Bordered: shows the boxed layout.") GS_DEFINE(settings/ui.stats_background_style/description, "Changes the way the background for the end of level stats is displayed.\n\n- Dark: TR1 (PC).\n- Very dark: TR1 (PS1).\n- Static: TR2 (PC).\n- Wave: TR2 (PS1).\n- Monochrome: TR3.") GS_DEFINE(settings/ui.stats_fade_effects/description, "Fine-tunes the fade effects to be enabled or disabled in the end of the level statistics screen. Needs the Fade Effects option to be enabled to work.") GS_DEFINE(settings/ui.text_scale/description, "Changes the size of UI text.") GS_DEFINE(settings/visuals.camera_mode/description, "Adjusts how camera behaves during actions like using keys.") GS_DEFINE(settings/visuals.enable_3d_pickups/description, "Enables 3D models to be rendered in place of the sprites for pickup items.") GS_DEFINE(settings/visuals.enable_braid/description, "Enables Lara's braid.") GS_DEFINE(settings/visuals.enable_breeze/description, "Enables the breeze effect on Lara's braid in appropriate rooms.") GS_DEFINE(settings/visuals.blood_effects/description, "Controls blood spark colors.\n\n- Disabled: no blood sparks are shown.\n- Pink: the default in German PC releases of TR3.\n- Red: the default in all other retail releases.") GS_DEFINE(settings/visuals.enable_exit_fade_effects/description, "Enables the fade effects when exiting the game to desktop.") GS_DEFINE(settings/visuals.enable_fade_effects/description, "Enable fade transitions, for example between credit graphics or for inventory and pause screen transitions.") GS_DEFINE(settings/visuals.enable_fire_lighting/description, "Enables dynamic lighting to be generated beside active flames.") GS_DEFINE(settings/visuals.enable_footprints/description, "Enables rendering of Lara's footprints on certain surfaces in supported levels.") GS_DEFINE(settings/visuals.enable_glide_cameras/description, "Enables a glide action on fixed cameras that look at Lara by adopting a smooth speed curve. If disabled, such cameras will change the view to look at Lara immediately.") GS_DEFINE(settings/visuals.enable_gun_lighting/description, "Enables dynamic lighting to be generated for gunshots and explosions.") GS_DEFINE(settings/visuals.enable_ps1_crystals/description, "Save crystals will be drawn with a purple tint, more similar to the PS1 type.") GS_DEFINE(settings/visuals.enable_reflections/description, "Enables reflections on certain objects.") GS_DEFINE(settings/visuals.enable_responsive_mesh_tint/description, "Enables Lara's individual meshes to be drawn with a water tint if they are themselves located underwater (TR3 style). Otherwise, if Lara is in water, each of her meshes will be drawn with the tint (TR1/2 style).") GS_DEFINE(settings/visuals.enable_shotgun_flash/description, "Draws flames when firing the shotgun, like for other guns.") GS_DEFINE(settings/visuals.enable_skybox/description, "Enables the skybox in supported levels.") GS_DEFINE(settings/visuals.enable_weather/description, "Enables rendering of weather effects in supported levels.") GS_DEFINE(settings/visuals.fix_animated_sprites/description, "Fixes original underwater plant sprites so they animate properly in water areas.") GS_DEFINE(settings/visuals.fix_item_rots/description, "Fixes original issues with some incorrectly rotated pickups when using the 3D pickups option.") GS_DEFINE(settings/visuals.fix_texture_issues/description, "Fixes original issues with missing or incorrect textures/meshes.") GS_DEFINE(settings/visuals.fog_color/description, "Color of the fog.") GS_DEFINE(settings/visuals.fog_end/description, "Sets distance in tiles where fog makes everything fully obscured.") GS_DEFINE(settings/visuals.fog_start/description, "Sets distance in tiles where fog begins to appear.") GS_DEFINE(settings/visuals.fog_transparency/description, "Whether to enable blending distant geometry into 100% transparent faces.") GS_DEFINE(settings/visuals.fov/description, "Viewing angle in degrees. Larger values widen the field of view, smaller ones narrow it.") GS_DEFINE(settings/visuals.game_brightness/description, "Changes game brightness.") GS_DEFINE(settings/visuals.gamma/description, "Adjusts the gamma curve. Higher values mean brighter lighting. The value of 2.5 means default colors.") GS_DEFINE(settings/visuals.lara_outfit/description, "Changes Lara's appearance. Choosing Default will respect any regular outfit changes between levels, otherwise the chosen value will persist until changed manually.") GS_DEFINE(settings/visuals.shadow_type/description, "Selects how entity shadows are rendered.\n\n- Octagon: old TR1 and TR2 shadows\n- Circle: round shadows\n- Sprite: TR3 texture-based shadows") GS_DEFINE(settings/visuals.sunglasses_mode/description, "Changes the style of Lara's sunglasses. Note that lenses will be reflective if the relevant option is enabled.\n\n- Off: Lara will not wear sunglasses.\n- Opaque: Lara's sunglasses will have opaque lenses.\n- Transparent: Lara's sunglasses will have semi-transparent lenses.") GS_DEFINE(settings/visuals.ui_brightness/description, "Changes UI brightness.") GS_DEFINE(settings/visuals.water_color/description, "Color of the water.") GS_DEFINE(general/stats/basic_fmt, "%d") GS_DEFINE(general/stats/detail_fmt, "%d of %d") GS_DEFINE(general/stats/final_statistics, "Final Statistics") GS_DEFINE(general/stats/bonus_statistics, "Bonus Statistics") GS_DEFINE(general/stats/assault_title, "BEST TIMES") GS_DEFINE(general/stats/assault_no_times_set, "No Times Set") GS_DEFINE(general/stats/assault_finish, "Finish") GS_DEFINE(general/stats/assault_best_time_fmt, "%s") GS_DEFINE(general/stats/assault_other_times_fmt, "%s") GS_DEFINE(general/stats/gym_assault_course, "Assault Course") GS_DEFINE(general/stats/gym_racetrack_course, "Race Track Course") // Misc GS_DEFINE(general/inventory_ring/heading_inventory, "INVENTORY") GS_DEFINE(general/inventory_ring/heading_game_over, "GAME OVER") GS_DEFINE(general/inventory_ring/heading_option, "OPTION") GS_DEFINE(general/inventory_ring/heading_items, "ITEMS") GS_DEFINE(general/inventory_ring/heading_adventure, "Adventure") GS_DEFINE(general/inventory_ring/heading_fmt, "%s") GS_DEFINE(general/inventory_ring/object_name_fmt, "%s") GS_DEFINE(general/inventory_ring/item_count_fmt, "\\{small}%s") GS_DEFINE(general/overlay/item_count_fmt_pc, "\\{small}%s") GS_DEFINE(general/overlay/item_count_fmt_ps1, "\\{small}%s") GS_DEFINE(general/osd/pos_lara_pos_fmt, "Room: %d\nPosition: %.3f, %.3f, %.3f\nRotation: %.3f, %.3f, %.3f") GS_DEFINE(general/osd/pos_lara_missing, "Lara not present") GS_DEFINE(general/osd/pos_level_fmt, "Level %d") GS_DEFINE(general/osd/pos_level_fmt_demo, "Demo %d") GS_DEFINE(general/osd/pos_level_fmt_cutscene, "Cutscene %d") GS_DEFINE(general/osd/current_health_get, "Current Lara's health: %d") GS_DEFINE(general/osd/current_health_set, "Lara's health set to %d") GS_DEFINE(general/osd/config_option_get, "%s is currently set to %s") GS_DEFINE(general/osd/config_option_set, "%s changed to %s") GS_DEFINE(general/osd/config_option_unknown_option, "Unknown option: %s") GS_DEFINE(general/osd/speed_get, "Current speed: %d") GS_DEFINE(general/osd/speed_set, "Speed set to %d") GS_DEFINE(general/osd/lighting_contrast_fmt, "Lighting Contrast: %s") GS_DEFINE(general/osd/bilinear_filter_on, "Bilinear filter: on") GS_DEFINE(general/osd/bilinear_filter_off, "Bilinear filter: off") GS_DEFINE(general/osd/wireframe_mode_on, "Wireframe mode: on") GS_DEFINE(general/osd/wireframe_mode_off, "Wireframe mode: off") GS_DEFINE(general/osd/textures_on, "Textures: on") GS_DEFINE(general/osd/textures_off, "Textures: off") GS_DEFINE(general/overlay/debug_position, "Position: ") GS_DEFINE(general/overlay/debug_camera_pos, "Camera origin: ") GS_DEFINE(general/overlay/debug_camera_target, "Camera target: ") GS_DEFINE(general/overlay/debug_rotation, "Rotation: ") GS_DEFINE(general/overlay/debug_speed, "Speed: ") GS_DEFINE(general/overlay/debug_animation, "Animation: ") GS_DEFINE(general/overlay/debug_animation_state, "State: ") GS_DEFINE(general/overlay/debug_immune, "Invulnerability on") GS_DEFINE(general/misc/on, "On") GS_DEFINE(general/misc/off, "Off") GS_DEFINE(general/misc/demo_mode, "Demo Mode") GS_DEFINE(general/misc/direction_keys_keyboard, "Arrows") GS_DEFINE(general/misc/direction_keys_controller, "D-Pad") GS_DEFINE(general/osd/heal_already_full_hp, "Lara's already at full health") GS_DEFINE(general/osd/heal_success, "Healed Lara back to full health") GS_DEFINE(general/osd/give_item, "Added %s to Lara's inventory") GS_DEFINE(general/osd/invalid_item, "Unknown item: %s") GS_DEFINE(general/osd/kill_all, "Poof! %d enemies gone!") GS_DEFINE(general/osd/kill_all_fail, "Uh-oh, there are no enemies left to kill...") GS_DEFINE(general/osd/kill, "Bye-bye!") GS_DEFINE(general/osd/kill_fail, "No enemy nearby...") GS_DEFINE(general/osd/invalid_object, "Invalid object") GS_DEFINE(general/osd/invalid_sample, "Invalid sound: %d") GS_DEFINE(general/osd/object_not_found, "Object not found") GS_DEFINE(general/osd/sound_available_samples, "Available sounds: %s") GS_DEFINE(general/osd/sound_playing_sample, "Playing sound %d") GS_DEFINE(general/osd/unknown_command, "Unknown command: %s") GS_DEFINE(general/osd/command_bad_invocation, "Invalid invocation: %s") GS_DEFINE(general/osd/command_valid_values, "Valid values: %s") GS_DEFINE(general/osd/command_bool, "on, off") GS_DEFINE(general/osd/command_integer, "[integer]") GS_DEFINE(general/osd/command_decimal, "[decimal]") GS_DEFINE(general/osd/command_percent, "[integer]") GS_DEFINE(general/osd/command_unavailable, "This command is not currently available") GS_DEFINE(general/osd/invalid_room, "Invalid room: %d. Valid rooms are 0-%d") GS_DEFINE(console/cmd/teleport/pos, "Teleported to position: %.3f %.3f %.3f") GS_DEFINE(console/cmd/teleport/pos_fail, "Failed to teleport to position: %.3f %.3f %.3f") GS_DEFINE(console/cmd/teleport/room, "Teleported to room: %d") GS_DEFINE(console/cmd/teleport/room_fail, "Failed to teleport to room: %d") GS_DEFINE(console/cmd/teleport/object, "Teleported to object: %s") GS_DEFINE(console/cmd/teleport/object_fail, "Failed to teleport to object: %s") GS_DEFINE(console/cmd/teleport/item, "Teleported to item: %d") GS_DEFINE(console/cmd/teleport/item_fail, "Failed to teleport to item: %d") GS_DEFINE(general/osd/play_level, "Loading %s") GS_DEFINE(general/osd/play_cutscene, "Loading cutscene %d") GS_DEFINE(general/osd/play_demo, "Loading demo %d") GS_DEFINE(general/osd/invalid_level, "Invalid level") GS_DEFINE(general/osd/invalid_cutscene, "Invalid cutscene") GS_DEFINE(general/osd/invalid_demo, "Invalid demo") GS_DEFINE(general/osd/load_game, "Loaded game from save slot %d") GS_DEFINE(general/osd/load_game_fail_unavailable_slot, "Save slot %d is not available") GS_DEFINE(general/osd/load_game_fail_invalid_slot, "Invalid save slot %d") GS_DEFINE(general/osd/save_game, "Saved game to save slot %d") GS_DEFINE(general/osd/save_game_fail_invalid_slot, "Invalid save slot %d") GS_DEFINE(general/osd/quick_save, "Quick-saved") GS_DEFINE(general/osd/quick_save_fail_no_slots, "No quick save slots are configured") GS_DEFINE(general/osd/quick_load, "Quick-loaded slot %d") GS_DEFINE(general/osd/quick_load_fail_no_bound_slot, "No save slot is currently bound") GS_DEFINE(general/osd/quick_load_fail_unavailable_bound_slot, "The bound save slot is not available") GS_DEFINE(general/osd/flipmap_on, "Flipmap set to ON") GS_DEFINE(general/osd/flipmap_off, "Flipmap set to OFF") GS_DEFINE(general/osd/flipmap_fail_already_on, "Flipmap is already ON") GS_DEFINE(general/osd/flipmap_fail_already_off, "Flipmap is already OFF") GS_DEFINE(general/osd/ambiguous_input_2, "Ambiguous input: %s and %s") GS_DEFINE(general/osd/ambiguous_input_3, "Ambiguous input: %s, %s, ...") GS_DEFINE(general/osd/ui_on, "UI enabled") GS_DEFINE(general/osd/ui_off, "UI disabled") GS_DEFINE(general/osd/give_item_all_keys, "Surprise! Every key item Lara needs is now in her backpack.") GS_DEFINE(general/osd/give_item_all_guns, "Lock'n'load - Lara's armed to the teeth!") GS_DEFINE(general/osd/give_item_cheat, "Lara's backpack just got way heavier!") GS_DEFINE(general/osd/complete_level, "Level complete!") GS_DEFINE(general/osd/door_open, "Open Sesame!") GS_DEFINE(general/osd/door_close, "Close Sesame!") GS_DEFINE(general/osd/door_open_fail, "No doors in Lara's proximity") GS_DEFINE(general/osd/fly_mode_on, "Fly mode enabled") GS_DEFINE(general/osd/fly_mode_off, "Fly mode disabled") GS_DEFINE(general/settings/controls/layout/default, "Default Keys") GS_DEFINE(general/settings/controls/layout/custom_1, "User Keys 1") GS_DEFINE(general/settings/controls/layout/custom_2, "User Keys 2") GS_DEFINE(general/settings/controls/layout/custom_3, "User Keys 3") GS_DEFINE(general/settings/controls/backend/keyboard, "Keyboard") GS_DEFINE(general/settings/controls/backend/controller, "Controller") GS_DEFINE(general/settings/controls/customize, "Customize Controls") GS_DEFINE(general/settings/controls/tabs/basics, "Movement") GS_DEFINE(general/settings/controls/tabs/items, "Items") GS_DEFINE(general/settings/controls/tabs/misc, "Misc") GS_DEFINE(general/settings/controls/tabs/system, "System") GS_DEFINE(general/pause/paused, "Paused") GS_DEFINE(general/pause/exit_to_title, "Exit to title?") GS_DEFINE(general/pause/continue, "Continue") GS_DEFINE(general/pause/quit, "Quit") GS_DEFINE(general/pause/are_you_sure, "Are you sure?") GS_DEFINE(general/pause/yes, "Yes") GS_DEFINE(general/pause/no, "No") GS_DEFINE(general/photo_mode/camera_move_prompt, "Move camera") GS_DEFINE(general/photo_mode/camera_reset_prompt, "Reset camera") GS_DEFINE(general/photo_mode/camera_roll_prompt, "Roll camera") GS_DEFINE(general/photo_mode/camera_rotate_90_prompt, "Rotate camera 90°") GS_DEFINE(general/photo_mode/camera_rotate_prompt, "Rotate camera") GS_DEFINE(general/photo_mode/lara_move_prompt, "Move Lara") GS_DEFINE(general/photo_mode/lara_reset_prompt, "Reset Lara") GS_DEFINE(general/photo_mode/lara_roll_prompt, "Roll Lara") GS_DEFINE(general/photo_mode/lara_rotate_90_prompt, "Rotate Lara 90°") GS_DEFINE(general/photo_mode/lara_rotate_prompt, "Rotate Lara") GS_DEFINE(general/photo_mode/fov_prompt, "Adjust FOV") GS_DEFINE(general/photo_mode/snap_prompt, "Take picture") GS_DEFINE(general/photo_mode/title_camera_pos, "Photo Mode") GS_DEFINE(general/photo_mode/title_lara_pos, "Moving Lara") GS_DEFINE(general/photo_mode/advance_frame, "Advance frame") GS_DEFINE(general/photo_mode/change_lara_pose, "Change Lara's pose") GS_DEFINE(general/photo_mode/toggle_help, "Toggle help") GS_DEFINE(general/misc/exit, "Exit") GS_DEFINE(general/misc/hold_fmt, "Hold %s") GS_DEFINE(general/misc/empty_slot_fmt, "- EMPTY SLOT -") GS_DEFINE(general/passport/exit_game, "Exit Game") GS_DEFINE(general/passport/exit_to_title, "Exit to Title") GS_DEFINE(general/passport/load_game, "Load Game") GS_DEFINE(general/passport/mode_new_game, "New Game") GS_DEFINE(general/passport/mode_new_game_jp, "Japanese NG") GS_DEFINE(general/passport/mode_new_game_jp_plus, "Japanese NG+") GS_DEFINE(general/passport/mode_new_game_plus, "New Game+") GS_DEFINE(general/passport/new_game, "New Game") GS_DEFINE(general/passport/delete_save, "Delete") GS_DEFINE(general/passport/delete_save_confirm, "Delete this save?") GS_DEFINE(general/passport/delete_save_failed, "Failed to delete the chosen save.") GS_DEFINE(general/passport/delete_save_yes, "Yes") GS_DEFINE(general/passport/delete_save_no, "No") GS_DEFINE(general/passport/play_previous_levels, "Play previous levels") GS_DEFINE(general/passport/restart_level, "Restart Level") GS_DEFINE(general/passport/save_game, "Save Game") GS_DEFINE(general/passport/save_slot_unsupported, "This save does not support this feature.") GS_DEFINE(general/passport/select_level, "Select Level") GS_DEFINE(general/passport/select_mod, "Select Game") GS_DEFINE(general/passport/select_mode, "Select Mode") GS_DEFINE(general/passport/select_save, "Select Save") GS_DEFINE(general/passport/story_so_far, "Story so far...") GS_DEFINE(general/passport/switch_mod, "Switch Game") GS_DEFINE(general/osd/trapezoid_filter_on, "Trapezoid filter: on") GS_DEFINE(general/osd/trapezoid_filter_off, "Trapezoid filter: off") GS_DEFINE(general/osd/fps_counter_on, "FPS counter: on") GS_DEFINE(general/osd/fps_counter_off, "FPS counter: off") GS_DEFINE(general/osd/strings_reloaded, "Language files reloaded") GS_DEFINE(general/osd/strings_failed, "Failed to reload the language files") GS_DEFINE(general/osd/upscaling_factor, "Upscaling Factor: x%d") GS_DEFINE(general/misc/pagination_nav, "%d / %d") GS_DEFINE(general/actions/examine_item, "Examine") GS_DEFINE(general/actions/hide_dialog, "Hide dialog") GS_DEFINE(general/actions/rotate, "Rotate") GS_DEFINE(general/actions/use_item, "Use") GS_DEFINE(general/actions/reset_defaults, "Reset All") GS_DEFINE(general/actions/unbind, "Unbind") GS_DEFINE(console/cmd/lua/syntax_error, "Lua syntax error: %s") GS_DEFINE(console/cmd/lua/runtime_error, "Lua runtime error: %s") GS_DEFINE(console/cmd/give/secret_list, "Secrets collected: %d of %d (%s)") GS_DEFINE(console/cmd/give/secret_none, "Secrets collected: %d of %d") GS_DEFINE(console/cmd/give/secret_given, "Added secret %s") GS_DEFINE(console/cmd/give/secret_taken, "Removed secret %s") GS_DEFINE(console/cmd/give/invalid_secret, "Invalid secret: %s (valid secrets: %s)") GS_DEFINE(console/cmd/immune/on, "Lara is now impervious to damage") GS_DEFINE(console/cmd/immune/off, "Lara is now vulnerable") GS_DEFINE(console/cmd/inf_sprint/on, "Lara can now sprint forever") GS_DEFINE(console/cmd/inf_sprint/off, "Lara can no longer sprint forever") GS_DEFINE(console/cmd/spawn/success, "Requested object spawned near Lara") GS_DEFINE(console/cmd/spawn/fail, "Failed to spawn requested object") GS_DEFINE(console/cmd/trigger/triggered, "Triggered item(s): %s") GS_DEFINE(console/cmd/trigger/untriggered, "Untriggered item(s): %s") GS_DEFINE(console/cmd/trigger/invalid_item, "Invalid item: %s") GS_DEFINE(console/cmd/trigger/no_match, "Unknown target: %s") GS_DEFINE(console/cmd/trigger/not_found, "No matching items found for: %s") GS_DEFINE(console/cmd/winston/spawned, "Summoned Winston near Lara") GS_DEFINE(console/cmd/winston/spawn_failed, "Failed to summon Winston") GS_DEFINE(console/cmd/winston/teleported, "Summoned Winston near Lara") GS_DEFINE(console/cmd/winston/dead, "Your butler is dead. You monster!") GS_DEFINE(console/cmd/play_music/track, "Playing music track %d") GS_DEFINE(console/cmd/play_music/stopped, "Music stopped") GS_DEFINE(console/cmd/play_music/invalid_track, "Invalid music track") GS_DEFINE(console/cmd/weather/set, "Weather set to %s") GS_DEFINE(console/cmd/weather/invalid, "Invalid weather: %s (valid: %s)") GS_DEFINE(console/cmd/help/list, "Available commands:") GS_DEFINE(console/cmd/braid/help, "Toggles Lara's braid.") GS_DEFINE(console/cmd/cheats/help, "Toggles in-game cheats on or off.") GS_DEFINE(console/cmd/clear/help, "Clears visible console logs.") GS_DEFINE(console/cmd/debug/help, "Toggles visual debug information.") GS_DEFINE(console/cmd/drain/help, "Dries the current room, removing the water.") GS_DEFINE(console/cmd/end_level/help, "Ends the current level.") GS_DEFINE(console/cmd/exit/help, "Exits the game.") GS_DEFINE(console/cmd/flipmap/help, "Toggles the flip map.") GS_DEFINE(console/cmd/flood/help, "Submerges the current room with water.") GS_DEFINE(console/cmd/fly/help, "Toggles the fly-mode cheat.") GS_DEFINE(console/cmd/fps/help, "Changes the FPS value.") GS_DEFINE(console/cmd/give/help, "Adds a given item to Lara's inventory.") GS_DEFINE(console/cmd/give_secret/help, "Lists Lara's secrets, or takes/gives a secret by number.") GS_DEFINE(console/cmd/heal/help, "Heals Lara back to full health.") GS_DEFINE(console/cmd/help/help, "Shows help for all commands or detailed help for one.") GS_DEFINE(console/cmd/hp/help, "Sets Lara's health to the specified value.") GS_DEFINE(console/cmd/immune/help, "Toggles invulnerability. (Lara can still be killed in some circumstances.)") GS_DEFINE(console/cmd/inf_sprint/help, "Toggles infinite sprint.") GS_DEFINE(console/cmd/kill/help, "Kills nearby enemies.") GS_DEFINE(console/cmd/lighting/help, "Toggles lighting system.") GS_DEFINE(console/cmd/load/help, "Loads game from the given save slot or from a quick save.") GS_DEFINE(console/cmd/lua/help, "Executes the given Lua code string.") GS_DEFINE(console/cmd/mod/help, "Switches to the specified mod and restarts the game.") GS_DEFINE(console/cmd/music/help, "Plays a music track with the given id.") GS_DEFINE(console/cmd/play_cutscene/help, "Plays a cutscene with the given number.") GS_DEFINE(console/cmd/play_demo/help, "Plays a demo with the given number.") GS_DEFINE(console/cmd/play_gym/help, "Plays the Gym level.") GS_DEFINE(console/cmd/play_level/help, "Plays a level with the given name or number.") GS_DEFINE(console/cmd/pos/help, "Shows Lara's position.") GS_DEFINE(console/cmd/save/help, "Saves game to the given save slot or to the next quick save slot.") GS_DEFINE(console/cmd/screenshot/help, "Saves a screenshot to disk at optional location.") GS_DEFINE(console/cmd/set/help, "Displays or updates the given configuration setting.") GS_DEFINE(console/cmd/sfx/help, "Plays a sound effect with the given id.") GS_DEFINE(console/cmd/speed/help, "Changes the game's speed.") GS_DEFINE(console/cmd/strings/help, "Reloads the current language files from disk.") GS_DEFINE(console/cmd/textures/help, "Toggles textures.") GS_DEFINE(console/cmd/title/help, "Returns to the title screen.") GS_DEFINE(console/cmd/tp/help, "Teleports Lara to a given position or room number.") GS_DEFINE(console/cmd/trigger/help, "Triggers or untriggers an item by id, item name, or object name.") GS_DEFINE(console/cmd/vsync/help, "Toggles vertical sync.") GS_DEFINE(console/cmd/weather/help, "Changes the current weather type.") GS_DEFINE(console/cmd/wireframe/help, "Toggles wireframe rendering.") GS_DEFINE(general/settings/common/all_hidden_disclaimer, "Settings are disabled for this level set.") GS_DEFINE(general/settings/common/chroma, "Chroma") GS_DEFINE(general/settings/common/edit_value, "Edit value") GS_DEFINE(general/settings/common/frozen_option_disclaimer, "This setting is enforced by the level builder and cannot be changed.") GS_DEFINE(general/settings/common/hue, "Hue") GS_DEFINE(general/settings/common/lightness, "Lightness") GS_DEFINE(general/settings/common/restore_default, "Restore default") GS_DEFINE(general/settings/common/toggle_help, "Toggle help") GS_DEFINE(general/settings/sound/title, "Sound Options") GS_DEFINE(general/settings/sound/tabs/volume, "Volume") GS_DEFINE(general/settings/sound/tabs/misc, "Misc") GS_DEFINE(general/settings/gameplay/title, "Gameplay Options") GS_DEFINE(general/settings/gameplay/tabs/general, "General") GS_DEFINE(general/settings/gameplay/tabs/controls, "Controls") GS_DEFINE(general/settings/gameplay/tabs/mods, "Mods") GS_DEFINE(general/settings/gameplay/tabs/fixes, "Fixes") GS_DEFINE(general/settings/gameplay/tabs/presets, "Presets") GS_DEFINE(general/config_presets/confirm_description, "The following settings will be changed:") GS_DEFINE(general/config_presets/confirm_restart_note, "Note: some settings may require a game restart to take effect.") GS_DEFINE(general/config_presets/applied, "Preset applied.") GS_DEFINE(general/config_presets/no_changes, "No changes to apply.") GS_DEFINE(general/config_presets/title_fmt, "Apply preset %s?") GS_DEFINE(general/config_presets/empty, "No presets found.") GS_DEFINE(dynamic/config_presets/tr1_ps1, "TR1 PS1") GS_DEFINE(dynamic/config_presets/tr1_pc, "TR1 PC") GS_DEFINE(dynamic/config_presets/tr2_ps1, "TR2 PS1") GS_DEFINE(dynamic/config_presets/tr2_pc, "TR2 PC") GS_DEFINE(dynamic/config_presets/tr3_ps1, "TR3 PS1") GS_DEFINE(dynamic/config_presets/tr3_pc, "TR3 PC") GS_DEFINE(dynamic/mods/tr1/title, "Tomb Raider I") GS_DEFINE(dynamic/mods/tr1-ub/title, "Unfinished Business") GS_DEFINE(dynamic/mods/tr1-demo-pc/title, "Tomb Raider I Demo") GS_DEFINE(dynamic/mods/tr2/title, "Tomb Raider II") GS_DEFINE(dynamic/mods/tr2-gm/title, "The Golden Mask") GS_DEFINE(dynamic/mods/tr3/title, "Tomb Raider III") GS_DEFINE(dynamic/mods/tr3-la/title, "The Lost Artifact") GS_DEFINE(general/settings/graphic_settings/title, "Graphic Options") GS_DEFINE(general/settings/graphic_settings/tabs/visuals, "Visuals") GS_DEFINE(general/settings/graphic_settings/tabs/ui, "UI") GS_DEFINE(general/settings/graphic_settings/tabs/stats, "Stats") GS_DEFINE(general/settings/graphic_settings/tabs/bars, "Bars") GS_DEFINE(general/settings/graphic_settings/tabs/rendering, "Rendering") GS_DEFINE(general/stats/level, "Level") GS_DEFINE(general/stats/time_taken, "Time Taken") GS_DEFINE(general/stats/ammo, "Ammo Hits/Used") GS_DEFINE(general/stats/ammo_used, "Ammo Used") GS_DEFINE(general/stats/ammo_hits, "Hits") GS_DEFINE(general/stats/kills, "Kills") GS_DEFINE(general/stats/crystals, "Crystals") GS_DEFINE(general/stats/pickups, "Pickups") GS_DEFINE(general/stats/deaths, "Deaths") GS_DEFINE(general/stats/medipacks_used, "Health Packs Used") GS_DEFINE(general/stats/distance_travelled, "Distance Traveled") GS_DEFINE(general/stats/secrets, "Secrets Found") GS_DEFINE(general/stats/none, "None") GS_DEFINE(general/globe_select/area_1, "Area 1") GS_DEFINE(general/globe_select/area_2, "Area 2") GS_DEFINE(general/globe_select/area_3, "Area 3") GS_DEFINE(general/globe_select/area_4, "Area 4") GS_DEFINE(general/globe_select/area_5, "Area 5") GS_DEFINE(general/globe_select/area_6, "Area 6") ================================================ FILE: src/trx/game/game_strings/entries.h ================================================ #pragma once // Define a game string mapping: associates an ID with a text value. // @param id Compile-time identifier for the string. // @param value Text to map to the identifier. #define GS_DEFINE(id, value) GameString_Define(#id, value); // Retrieve a game string by a slash-separated path literal. #define GS(path) GameString_Get(path) // Return a slash-separated path literal as-is. #define GS_ID(path) (path) // Retrieve a stable slot pointer for a slash-separated path literal. #define GS_PTR(path) GameString_GetPtr(path) typedef const char *GAME_STRING_ID; // Initialize the GameString subsystem. // Must be called before any GameString_* functions. void GameString_Init(void); // Shutdown the GameString subsystem and free all associated resources. void GameString_Shutdown(void); // Define a new game string mapping. // @param key Identifier for the string (e.g. "GAME_OVER"). // @param value Text to associate with the identifier. void GameString_Define(const char *key, const char *value); // Check whether a game string identifier has been defined. // @param key Identifier to test. // @return true if the identifier is known, false otherwise. bool GameString_IsKnown(const char *key); // Retrieve the game string associated with an identifier. // @param key Identifier for the string. // @return Text mapped to the identifier, or nullptr if unknown. const char *GameString_Get(const char *key); // Clear all existing game string mappings. // Slots remain allocated but values reset. void GameString_Clear(void); // Like GameString_Get(), but returns a stable slot pointer that always // reflects the current string value for this identifier (updates automatically // on reload). // @param key Identifier for the string. // @return Address of the internal string pointer, or nullptr if unknown. const char *const *GameString_GetPtr(const char *key); ================================================ FILE: src/trx/game/game_strings/manager.c ================================================ #include #include #include #include #include #include #include #include #include #include #include typedef struct { char *path; bool load_levels; } M_FILE_ENTRY; typedef struct { char *lang; VECTOR *files; char *display_name; char *extends; } M_LANG_ENTRY; static VECTOR *m_SourceFiles = nullptr; static VECTOR *m_LangEntries = nullptr; static EVENT_MANAGER *m_EventManager = nullptr; static void M_ClearFileEntries(VECTOR *const files) { for (int32_t i = 0; i < files->count; i++) { const M_FILE_ENTRY *const file_entry = Vector_Get(files, i); Memory_Free(file_entry->path); } Vector_Free(files); } static void M_ClearLanguageEntries(void) { if (m_LangEntries != nullptr) { for (int32_t i = 0; i < m_LangEntries->count; i++) { const M_LANG_ENTRY *const lang_entry = Vector_Get(m_LangEntries, i); Memory_Free(lang_entry->lang); Memory_Free(lang_entry->display_name); Memory_Free(lang_entry->extends); M_ClearFileEntries(lang_entry->files); } Vector_Free(m_LangEntries); m_LangEntries = nullptr; } } static void M_ClearManager(void) { M_ClearLanguageEntries(); if (m_SourceFiles != nullptr) { M_ClearFileEntries(m_SourceFiles); m_SourceFiles = nullptr; } } static M_LANG_ENTRY *M_FindLangEntry(const char *const lang) { for (int32_t i = 0; i < m_LangEntries->count; i++) { M_LANG_ENTRY *const lang_entry = Vector_Get(m_LangEntries, i); if (String_Equivalent(lang_entry->lang, lang)) { return lang_entry; } } return nullptr; } // Create a new entry with the given file for the given language. static void M_AddPathForLang( const char *const lang, char *const path, const bool load_levels) { const M_LANG_ENTRY *lang_entry = M_FindLangEntry(lang); if (lang_entry == nullptr) { M_LANG_ENTRY new_ent = { .lang = Memory_DupStr(lang), .files = Vector_Create(sizeof(M_FILE_ENTRY)), }; Vector_Add(m_LangEntries, &new_ent); lang_entry = M_FindLangEntry(lang); } const M_FILE_ENTRY file_entry = { .path = path, .load_levels = load_levels, }; Vector_Add(lang_entry->files, &file_entry); } static void M_LoadLanguageNames(void) { if (m_LangEntries == nullptr) { return; } for (int32_t i = 0; i < m_LangEntries->count; i++) { M_LANG_ENTRY *const lang_entry = Vector_Get(m_LangEntries, i); Memory_FreePointer(&lang_entry->display_name); Memory_FreePointer(&lang_entry->extends); if (lang_entry->files->count <= 0) { continue; } const M_FILE_ENTRY *const file_entry = Vector_Get(lang_entry->files, 0); char *data = nullptr; size_t size = 0; if (!File_Load(file_entry->path, &data, &size) || data == nullptr) { continue; } JSON_PARSE_RESULT pr = { 0 }; JSON_VALUE *const root = JSON_ParseEx( data, size, JSON_PARSE_FLAGS_ALLOW_JSON5, nullptr, nullptr, &pr); if (root != nullptr) { JSON_OBJECT *const obj = JSON_ValueAsObject(root); const char *const name = JSON_ObjectGetString(obj, "language_name", JSON_INVALID_STRING); if (name != JSON_INVALID_STRING) { lang_entry->display_name = Memory_DupStr(name); } const char *const ext = JSON_ObjectGetString(obj, "extends", JSON_INVALID_STRING); if (ext != JSON_INVALID_STRING) { lang_entry->extends = Memory_DupStr(ext); } JSON_ValueFree(root); } else { LOG_WARNING( "failed to parse 'language_name' in %s: %s", file_entry->path, JSON_GetErrorDescription(pr.error)); } Memory_Free(data); } } static void M_ReorderLanguages(void) { if (m_LangEntries->count > 1) { VECTOR *ordered = Vector_Create(sizeof(M_LANG_ENTRY)); const M_LANG_ENTRY *en_orig = M_FindLangEntry("en"); if (en_orig != nullptr) { M_LANG_ENTRY en_entry = *en_orig; Vector_Add(ordered, &en_entry); } VECTOR *others = Vector_Create(sizeof(M_LANG_ENTRY)); for (int32_t i = 0; i < m_LangEntries->count; ++i) { const M_LANG_ENTRY *entry = Vector_Get(m_LangEntries, i); if (en_orig != nullptr && String_Equivalent(entry->lang, "en")) { continue; } M_LANG_ENTRY e = *entry; Vector_Add(others, &e); } for (int32_t i = 0; i + 1 < others->count; ++i) { for (int32_t j = 0; j + 1 < others->count - i; ++j) { M_LANG_ENTRY *a = Vector_Get(others, j); M_LANG_ENTRY *b = Vector_Get(others, j + 1); const char *an = a->display_name ? a->display_name : ""; const char *bn = b->display_name ? b->display_name : ""; if (strcmp(an, bn) > 0) { Vector_Swap(others, j, j + 1); } } } for (int32_t i = 0; i < others->count; ++i) { M_LANG_ENTRY *entry = Vector_Get(others, i); Vector_Add(ordered, entry); } Vector_Free(others); Vector_Free(m_LangEntries); m_LangEntries = ordered; } } void GameStringManager_Init(void) { m_EventManager = EventManager_Create(); M_ClearManager(); m_SourceFiles = Vector_Create(sizeof(M_FILE_ENTRY)); } void GameStringManager_Shutdown(void) { if (m_EventManager != nullptr) { EventManager_Free(m_EventManager); m_EventManager = nullptr; } GameStringTable_Shutdown(); M_ClearManager(); } // Clear all previously set source strings files. // Must be called before GameStringManager_AddSourceFile. void GameStringManager_ClearSourceFiles(void) { M_ClearManager(); m_SourceFiles = Vector_Create(sizeof(M_FILE_ENTRY)); } // Add a source strings file for language discovery and loading. // base_path: path to a base strings JSON5 file. // load_levels: true to load level entries from this source; false otherwise. void GameStringManager_AddSourceFile( const char *const base_path, const bool load_levels) { ASSERT(m_SourceFiles != nullptr); if (base_path == nullptr) { return; } const M_FILE_ENTRY fe = { .path = Memory_DupStr(base_path), .load_levels = load_levels, }; Vector_Add(m_SourceFiles, &fe); } void GameStringManager_DiscoverLanguages(void) { if (m_SourceFiles == nullptr) { return; } M_ClearLanguageEntries(); m_LangEntries = Vector_Create(sizeof(M_LANG_ENTRY)); for (int32_t i = 0; i < m_SourceFiles->count; ++i) { const M_FILE_ENTRY *src = Vector_Get(m_SourceFiles, i); char *dir = File_GetParentDirectory(src->path); const char *base = MAX(strrchr(src->path, '\\'), strrchr(src->path, '/')); base = (base != nullptr) ? base + 1 : src->path; const char *ext = strrchr(base, '.'); if (dir == nullptr || ext == nullptr) { Memory_Free(dir); continue; } size_t stem_len = (size_t)(ext - base); size_t ext_len = strlen(ext); void *dh = File_OpenDirectory(dir); if (dh != nullptr) { const char *ent; while ((ent = File_ReadDirectory(dh))) { if (ent[0] == '.') { continue; } size_t name_len = strlen(ent); if (name_len < stem_len + ext_len) { continue; } if (strncmp(ent, base, stem_len) != 0 || strcmp(ent + name_len - ext_len, ext) != 0) { continue; } char *code; if (name_len == stem_len + ext_len) { code = Memory_DupStr("en"); } else if (ent[stem_len] == '-') { size_t code_len = name_len - stem_len - ext_len - 1; code = String_Format( "%.*s", (int32_t)code_len, ent + stem_len + 1); } else { continue; } char *path = String_Format("%s/%s", dir, ent); M_AddPathForLang(code, path, src->load_levels); Memory_Free(code); } File_CloseDirectory(dh); } Memory_Free(dir); } M_LoadLanguageNames(); M_ReorderLanguages(); } VECTOR *GameStringManager_GetAvailableLanguages(void) { if (m_LangEntries == nullptr) { return nullptr; } VECTOR *const out = Vector_Create(sizeof(char *)); for (int32_t i = 0; i < m_LangEntries->count; i++) { const M_LANG_ENTRY *const lang_entry = Vector_Get(m_LangEntries, i); char *const c = Memory_DupStr(lang_entry->lang); Vector_Add(out, &c); } return out; } // Recursive load of language chain (handles 'extends' fallback between // dialects) static bool M_ReloadLangRec(const char *const lang, VECTOR *const visited) { for (int32_t i = 0; i < visited->count; i++) { const char *const prev = *(char **)Vector_Get(visited, i); if (String_Equivalent(prev, lang)) { LOG_WARNING("cyclic language extends detected: %s", lang); return false; } } Vector_Add(visited, &lang); M_LANG_ENTRY *const entry = M_FindLangEntry(lang); if (entry == nullptr) { return false; } if (entry->extends) { if (!M_ReloadLangRec(entry->extends, visited)) { return false; } } for (int32_t i = 0; i < entry->files->count; i++) { const M_FILE_ENTRY *const fe = Vector_Get(entry->files, i); if (!GameStringTable_Load(fe->path, fe->load_levels)) { return false; } } return true; } bool GameStringManager_ReloadLanguage(const char *lang) { const M_LANG_ENTRY *const base_entry = m_LangEntries ? M_FindLangEntry(lang) : nullptr; if (base_entry == nullptr) { LOG_WARNING("language '%s' not found, defaulting to base", lang); lang = "en"; } GameStringTable_Shutdown(); GameStringTable_Init(); VECTOR *const visited = Vector_Create(sizeof(char *)); const bool success = M_ReloadLangRec(lang, visited); Vector_Free(visited); if (success) { GameStringTable_Apply(GF_GetCurrentLevel()); if (m_EventManager != nullptr) { const EVENT event = { .name = "reload_language", .sender = nullptr, .data = (void *)lang, }; EventManager_Fire(m_EventManager, &event); } } return success; } const char *GameStringManager_GetLanguageName(const char *const code) { if (m_LangEntries == nullptr || code == nullptr) { return nullptr; } const M_LANG_ENTRY *const ent = M_FindLangEntry(code); return ent != nullptr ? ent->display_name : nullptr; } int32_t GameStringManager_SubscribeReload( const EVENT_LISTENER listener, void *const user_data) { ASSERT(m_EventManager != nullptr); return EventManager_Subscribe( m_EventManager, "reload_language", nullptr, listener, user_data); } void GameStringManager_UnsubscribeReload(const int32_t listener_id) { if (m_EventManager != nullptr) { EventManager_Unsubscribe(m_EventManager, listener_id); } } ================================================ FILE: src/trx/game/game_strings/manager.h ================================================ // Manages autodiscovery and runtime reload of string bundles per language and // mod/OG fallback. #pragma once #include #include // Initialize the string bundle manager. void GameStringManager_Init(void); // Shutdown the string bundle manager. void GameStringManager_Shutdown(void); // Clear all previously set source strings files. // Must be called before GameStringManager_AddSourceFile. void GameStringManager_ClearSourceFiles(void); // Add a source strings file for language discovery and loading. // base_path: path to a base strings JSON5 file (e.g. cfg/common_strings.json5). // load_levels: true to load level names from this source; false otherwise. void GameStringManager_AddSourceFile(const char *base_path, bool load_levels); // Discover all available languages from the added source files. // Must be called after AddSourceFile calls and before ReloadLanguage. void GameStringManager_DiscoverLanguages(void); // Returns a vector of char* of available language codes discovered. // Caller owns the returned VECTOR and must free it via Vector_Free and free // each string. VECTOR *GameStringManager_GetAvailableLanguages(void); // Get the display name for a language code as defined by "language_name" // in the strings JSON file. Returns nullptr if unavailable. // The returned pointer is owned by the manager; do not free. const char *GameStringManager_GetLanguageName(const char *code); // Reload all game strings for the given language code. // Clears any previously loaded strings, loads OG fallback and mod overrides. // lang: language code (e.g. "en", "fr"). Must be one returned by // GetAvailableLanguages. bool GameStringManager_ReloadLanguage(const char *lang); // Subscribe to be notified when the game strings language is reloaded. // The listener will receive an EVENT with name "reload_language", // and .data pointing to the language code (const char *). int32_t GameStringManager_SubscribeReload( EVENT_LISTENER listener, void *user_data); // Unsubscribe from language reload events. void GameStringManager_UnsubscribeReload(int32_t listener_id); ================================================ FILE: src/trx/game/game_strings/table/common.c ================================================ #include #include #include #include #include #include #include #include #include #include typedef void (*M_LOAD_STRING_FUNC)(const char *, const char *); static VECTOR *m_GST_Layers = nullptr; static void M_Apply(const GS_TABLE *const table) { for (const GS_GAME_STRING_ENTRY *cur = table->game_strings; cur != nullptr && cur->key != nullptr; cur++) { if (cur->value == nullptr) { LOG_ERROR("Invalid game string value: %s", cur->key); } else { GameString_Define(cur->key, cur->value); } } for (const GS_OBJECT_ENTRY *cur = table->objects; cur != nullptr && cur->key != nullptr; cur++) { const OBJECT_ID obj_id = Object_IdFromKey(cur->key); if (obj_id == NO_OBJECT) { LOG_ERROR("Invalid object id: %s", cur->key); } else if (cur->names == nullptr) { LOG_ERROR("Invalid object name(s): %s", cur->key); } else { Object_ClearNames(obj_id); for (const char *const *name = cur->names; *name != nullptr; name++) { Object_AddName(obj_id, *name); } Object_SetDescription(obj_id, cur->description); } } } static void M_ApplyLevelTitles( const GS_FILE *const gs_file, const GF_LEVEL_TABLE_TYPE level_table_type) { const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(level_table_type); const GS_LEVEL_TABLE *const gs_level_table = &gs_file->level_tables[level_table_type]; if (gs_level_table->count == 0) { return; } ASSERT(gs_level_table->count == level_table->count); for (int32_t i = 0; i < level_table->count; i++) { if (gs_level_table->entries[i].title != nullptr) { GF_SetLevelTitle( &level_table->levels[i], gs_level_table->entries[i].title); } } } static void M_ApplyLayer( const GF_LEVEL *const level, const GS_FILE *const gs_file) { LOG_DEBUG("applying layer: %s", gs_file->path); M_Apply(&gs_file->global); for (int32_t i = 0; i < GFLT_NUMBER_OF; i++) { M_ApplyLevelTitles(gs_file, i); } if (level != nullptr) { const GS_LEVEL_TABLE *gs_level_table = nullptr; switch (level->type) { case GFL_TITLE: gs_level_table = nullptr; break; default: { const GF_LEVEL_TABLE_TYPE level_table_type = GF_GetLevelTableType(level->type); gs_level_table = &gs_file->level_tables[level_table_type]; } } if (gs_level_table != nullptr && gs_level_table->count != 0) { ASSERT(level->num >= 0); ASSERT(level->num < gs_level_table->count); M_Apply(&gs_level_table->entries[level->num].table); } } } void GameStringTable_Apply(const GF_LEVEL *const level) { Object_ResetAllNames(); ASSERT(m_GST_Layers != nullptr); for (int32_t i = 0; i < m_GST_Layers->count; i++) { const GS_FILE *const gs_file = *(GS_FILE **)Vector_Get(m_GST_Layers, i); M_ApplyLayer(level, gs_file); } } void GameStringTable_Init(void) { m_GST_Layers = Vector_Create(sizeof(GS_FILE *)); } void GameStringTable_Shutdown(void) { if (m_GST_Layers != nullptr) { for (int32_t i = 0; i < m_GST_Layers->count; i++) { GS_FILE *const gs_file = *(GS_FILE **)Vector_Get(m_GST_Layers, i); GS_File_Free(gs_file); } Vector_Free(m_GST_Layers); m_GST_Layers = nullptr; } } bool GameStringTable_Load(const char *const path, const bool load_levels) { GS_FILE *gs_file = GS_File_CreateFromPath(path, load_levels); if (gs_file == nullptr) { return false; } ASSERT(m_GST_Layers != nullptr); Vector_Add(m_GST_Layers, &gs_file); return true; } ================================================ FILE: src/trx/game/game_strings/table/priv.c ================================================ #include #include #include #include static void M_FreeTable(GS_TABLE *const gs_table) { if (gs_table == nullptr) { return; } if (gs_table->objects != nullptr) { GS_OBJECT_ENTRY *cur = gs_table->objects; while (cur->key != nullptr) { Memory_FreePointer(&cur->key); if (cur->names != nullptr) { for (size_t j = 0; cur->names[j] != nullptr; j++) { Memory_FreePointer((void **)&cur->names[j]); } Memory_Free(cur->names); cur->names = nullptr; } Memory_FreePointer(&cur->description); cur++; } Memory_Free(gs_table->objects); gs_table->objects = nullptr; } if (gs_table->game_strings != nullptr) { GS_GAME_STRING_ENTRY *cur = gs_table->game_strings; while (cur->key != nullptr) { Memory_FreePointer(&cur->key); Memory_FreePointer(&cur->value); cur++; } Memory_Free(gs_table->game_strings); gs_table->game_strings = nullptr; } } static void M_FreeLevelsTable(GS_LEVEL_TABLE *const levels) { if (levels->entries != nullptr) { for (int32_t i = 0; i < levels->count; i++) { Memory_FreePointer(&levels->entries[i].title); M_FreeTable(&levels->entries[i].table); } Memory_FreePointer(&levels->entries); } levels->count = 0; } void GS_File_Free(GS_FILE *const gs_file) { if (gs_file == nullptr) { return; } M_FreeTable(&gs_file->global); for (int32_t i = 0; i < GFLT_NUMBER_OF; i++) { M_FreeLevelsTable(&gs_file->level_tables[i]); } Memory_FreePointer(&gs_file->path); Memory_Free(gs_file); } ================================================ FILE: src/trx/game/game_strings/table/priv.h ================================================ #pragma once #include #include #include typedef struct { const char *key; const char **names; const char *description; } GS_OBJECT_ENTRY; typedef struct { const char *key; const char *value; } GS_GAME_STRING_ENTRY; typedef struct { GS_OBJECT_ENTRY *objects; GS_GAME_STRING_ENTRY *game_strings; } GS_TABLE; typedef struct { const char *title; GS_TABLE table; } GS_LEVEL; typedef struct { int32_t count; GS_LEVEL *entries; } GS_LEVEL_TABLE; typedef struct { char *path; GS_TABLE global; GS_LEVEL_TABLE level_tables[GFLT_NUMBER_OF]; } GS_FILE; void GS_Table_Free(GS_TABLE *gs_table); GS_FILE *GS_File_CreateFromPath(const char *path, bool load_levels); void GS_File_Free(GS_FILE *gs_file); ================================================ FILE: src/trx/game/game_strings/table/reader.c ================================================ #include #include #include #include #include #include #include #include #include #include #include typedef struct { JSON_OBJECT *obj; char *prefix; } M_STACK_ITEM; static void M_AppendGameStringEntry( GS_TABLE *const out_table, size_t *const io_count, size_t *const io_capacity, char *const key, const char *const value) { if (*io_count + 2 > *io_capacity) { *io_capacity *= 2; out_table->game_strings = Memory_Realloc( out_table->game_strings, sizeof(GS_GAME_STRING_ENTRY) * (*io_capacity)); } out_table->game_strings[(*io_count)++] = (GS_GAME_STRING_ENTRY) { .key = key, .value = Memory_DupStr(value), }; } static void M_LoadNestedGameStrings( JSON_OBJECT *const root_obj, const char *const root_key, GS_TABLE *const out_table, size_t *const io_count, size_t *const io_capacity) { JSON_OBJECT *const settings_obj = JSON_ObjectGetObject(root_obj, root_key); if (settings_obj == nullptr) { return; } VECTOR *const stack = Vector_Create(sizeof(M_STACK_ITEM)); const M_STACK_ITEM root_item = { .obj = settings_obj, .prefix = Memory_DupStr(root_key), }; Vector_Add(stack, &root_item); while (stack->count > 0) { const int32_t top_idx = stack->count - 1; M_STACK_ITEM cur = *(M_STACK_ITEM *)Vector_Get(stack, top_idx); Vector_RemoveAt(stack, top_idx); for (JSON_OBJECT_ELEMENT *elem = cur.obj->start; elem != nullptr; elem = elem->next) { const char *const key = elem->name->string; if (key == JSON_INVALID_STRING) { LOG_WARNING("Invalid game string key"); continue; } char *full_key = String_Format("%s/%s", cur.prefix, key); const char *const value = JSON_ValueGetString(elem->value, JSON_INVALID_STRING); if (value != JSON_INVALID_STRING) { M_AppendGameStringEntry( out_table, io_count, io_capacity, full_key, value); continue; } JSON_OBJECT *const child = JSON_ValueAsObject(elem->value); if (child != nullptr) { const M_STACK_ITEM child_item = { .obj = child, .prefix = full_key, }; Vector_Add(stack, &child_item); continue; } LOG_WARNING("Invalid game string entry '%s'", full_key); Memory_FreePointer(&full_key); } Memory_FreePointer(&cur.prefix); } Vector_Free(stack); } static void M_LoadTableFromJSON( JSON_OBJECT *const root_obj, GS_TABLE *const out_table) { // Load objects JSON_OBJECT *const jobjs = JSON_ObjectGetObject(root_obj, "objects"); if (jobjs != nullptr) { const size_t object_count = jobjs->length; out_table->objects = Memory_Alloc(sizeof(GS_OBJECT_ENTRY) * (object_count + 1)); JSON_OBJECT_ELEMENT *jobj_elem = jobjs->start; for (size_t i = 0; i < object_count; i++, jobj_elem = jobj_elem->next) { JSON_OBJECT *const jobj_obj = JSON_ValueAsObject(jobj_elem->value); const char *const key = jobj_elem->name->string; if (key == JSON_INVALID_STRING) { LOG_WARNING( "Invalid game string object entry %d: missing key.", i); continue; } const char *const single_name = JSON_ObjectGetString(jobj_obj, "name", JSON_INVALID_STRING); JSON_ARRAY *jnames_arr = JSON_ObjectGetArray(jobj_obj, "name"); if (jnames_arr == nullptr) { jnames_arr = JSON_ObjectGetArray(jobj_obj, "names"); } if (single_name == JSON_INVALID_STRING && (jnames_arr == nullptr || jnames_arr->length == 0)) { LOG_WARNING( "Invalid game string object entry %s: missing name.", key); continue; } GS_OBJECT_ENTRY *const object_entry = &out_table->objects[i]; object_entry->key = Memory_DupStr(key); if (jnames_arr != nullptr) { object_entry->names = Memory_Alloc( sizeof(const char *) * (jnames_arr->length + 1)); JSON_ARRAY_ELEMENT *elem = jnames_arr->start; size_t count = 0; for (size_t j = 0; j < jnames_arr->length; j++, elem = elem->next) { const char *const name = JSON_ValueGetString(elem->value, JSON_INVALID_STRING); if (name != JSON_INVALID_STRING) { object_entry->names[count] = Memory_DupStr(name); count++; } } object_entry->names[count] = nullptr; } else { object_entry->names = Memory_Alloc(sizeof(const char *) * 2); object_entry->names[0] = Memory_DupStr(single_name); object_entry->names[1] = nullptr; } const char *const description = JSON_ObjectGetString( jobj_obj, "description", JSON_INVALID_STRING); object_entry->description = description != JSON_INVALID_STRING ? Memory_DupStr(description) : nullptr; } } // Load localized string tables. const char *const nested_sections[] = { "general", "console", "settings", "enums", "dynamic", }; size_t gs_count = 0; size_t gs_capacity = 0; for (size_t i = 0; i < ARRAY_SIZE(nested_sections); i++) { if (JSON_ObjectGetObject(root_obj, nested_sections[i]) == nullptr) { continue; } if (gs_capacity == 0) { gs_capacity = 64; out_table->game_strings = Memory_Alloc(sizeof(GS_GAME_STRING_ENTRY) * gs_capacity); } M_LoadNestedGameStrings( root_obj, nested_sections[i], out_table, &gs_count, &gs_capacity); } if (gs_capacity != 0) { if (gs_count + 1 > gs_capacity) { gs_capacity++; out_table->game_strings = Memory_Realloc( out_table->game_strings, sizeof(GS_GAME_STRING_ENTRY) * gs_capacity); } out_table->game_strings[gs_count] = (GS_GAME_STRING_ENTRY) { .key = nullptr, .value = nullptr }; } } static void M_LoadLevelsFromJSON( JSON_OBJECT *const obj, GS_FILE *const gs_file, const char *const key, const GF_LEVEL_TABLE_TYPE level_table_type) { const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(level_table_type); GS_LEVEL_TABLE *const gs_level_table = &gs_file->level_tables[level_table_type]; if (level_table->count == 0) { return; } JSON_ARRAY *const jlvl_arr = JSON_ObjectGetArray(obj, key); if (jlvl_arr == nullptr) { return; } if (jlvl_arr->length != (size_t)level_table->count) { Shell_ExitSystemFmt( "%s: '%s' length must match with the game flow level count (got: " "%d, expected: %d)", gs_file->path, key, jlvl_arr->length, level_table->count); } gs_level_table->count = jlvl_arr->length; gs_level_table->entries = Memory_Alloc(sizeof(GS_LEVEL) * jlvl_arr->length); JSON_ARRAY_ELEMENT *jlvl_elem = jlvl_arr->start; for (size_t i = 0; i < jlvl_arr->length; i++, jlvl_elem = jlvl_elem->next) { GS_LEVEL *const level = &gs_level_table->entries[i]; JSON_OBJECT *const jlvl_obj = JSON_ValueAsObject(jlvl_elem->value); if (jlvl_obj == nullptr) { Shell_ExitSystemFmt( "%s: 'levels' elements must be dictionaries", gs_file->path); return; } const char *const title = JSON_ObjectGetString(jlvl_obj, "title", JSON_INVALID_STRING); if (title != JSON_INVALID_STRING) { level->title = Memory_DupStr(title); } M_LoadTableFromJSON(jlvl_obj, &level->table); } } GS_FILE *GS_File_CreateFromPath(const char *const path, const bool load_levels) { GS_FILE *const gs_file = Memory_Alloc(sizeof(*gs_file)); gs_file->path = Memory_DupStr(path); JSON_VALUE *const doc = JSONFile_ReadEx(path, true); JSON_OBJECT *root_obj = JSON_ValueAsObject(doc); M_LoadTableFromJSON(root_obj, &gs_file->global); if (load_levels) { M_LoadLevelsFromJSON(root_obj, gs_file, "levels", GFLT_MAIN); M_LoadLevelsFromJSON(root_obj, gs_file, "demos", GFLT_DEMOS); M_LoadLevelsFromJSON(root_obj, gs_file, "cutscenes", GFLT_CUTSCENES); } JSON_ValueFree(doc); return gs_file; } ================================================ FILE: src/trx/game/game_strings/table.h ================================================ #pragma once #include #include void GameStringTable_Init(void); void GameStringTable_Shutdown(void); bool GameStringTable_Load(const char *path, bool load_levels); void GameStringTable_Apply(const GF_LEVEL *level); ================================================ FILE: src/trx/game/gun/common.c ================================================ #include #include #include #include #include #include #include #include #define M_SHOTGUN_AMMO_CLIP 6 static bool M_IsGunType( const LARA_GUN_TYPE gun_type, const WEAPON_TYPE weapon_type) { return g_Weapons[gun_type].type == weapon_type; } static int32_t M_GetAmmoQuantity( const LARA_GUN_TYPE gun_type, const int32_t shell_count) { return MAX(1, shell_count) * Gun_GetAmmoClipCount(gun_type); } void Gun_AddDynamicLight(void) { if (!g_Config.visuals.enable_gun_lighting) { return; } const ITEM *const lara_item = Lara_GetItem(); const int32_t c = Math_Cos(lara_item->rot.y); const int32_t s = Math_Sin(lara_item->rot.y); const XYZ_32 pos = { .x = lara_item->pos.x + (s >> (W2V_SHIFT - 10)), .y = lara_item->pos.y - WALL_L / 2, .z = lara_item->pos.z + (c >> (W2V_SHIFT - 10)), }; if (g_TRVersion >= 3) { Output_AddDynamicLightRGB(pos, 12, (RGB_888) { 192, 144, 0 }); } else { Output_AddDynamicLight(pos, 12, 11); } } OBJECT_ID Gun_GetLaraAnim(const LARA_GUN_TYPE gun_type) { return M_IsGunType(gun_type, WEAPON_TYPE_DUAL_PISTOLS) ? O_LARA_PISTOLS : Gun_GetWeaponAnim(gun_type); } OBJECT_ID Gun_GetWeaponAnim(const LARA_GUN_TYPE gun_type) { // clang-format off switch (gun_type) { case LGT_UNKNOWN: return O_LARA; case LGT_UNARMED: return O_LARA; case LGT_PISTOLS: return O_LARA_PISTOLS; case LGT_MAGNUMS: return O_LARA_MAGNUMS; case LGT_AUTOS: return O_LARA_AUTOS; case LGT_DESERT_EAGLE: return O_LARA_DESERT_EAGLE; case LGT_UZIS: return O_LARA_UZIS; case LGT_SHOTGUN: return O_LARA_SHOTGUN; case LGT_M16: return O_LARA_M16; case LGT_MP5: return O_LARA_MP5; case LGT_GRENADE: return O_LARA_GRENADE_GUN; case LGT_ROCKET: return O_LARA_ROCKET_GUN; case LGT_HARPOON: return O_LARA_HARPOON_GUN; case LGT_FLARE: return O_LARA_FLARE; default: return NO_OBJECT; } // clang-format on } LARA_GUN_TYPE Gun_GetType(const OBJECT_ID obj_id) { // clang-format off switch (obj_id) { case O_PISTOL_ITEM: return LGT_PISTOLS; case O_MAGNUM_ITEM: return LGT_MAGNUMS; case O_AUTOS_ITEM: return LGT_AUTOS; case O_DESERT_EAGLE_ITEM: return LGT_DESERT_EAGLE; case O_UZI_ITEM: return LGT_UZIS; case O_SHOTGUN_ITEM: return LGT_SHOTGUN; case O_HARPOON_ITEM: return LGT_HARPOON; case O_M16_ITEM: return LGT_M16; case O_MP5_ITEM: return LGT_MP5; case O_GRENADE_GUN_ITEM: return LGT_GRENADE; case O_ROCKET_GUN_ITEM: return LGT_ROCKET; default: return LGT_UNARMED; } // clang-format on } OBJECT_ID Gun_GetGunObject(const LARA_GUN_TYPE gun_type) { // clang-format off switch (gun_type) { case LGT_PISTOLS: return O_PISTOL_ITEM; case LGT_MAGNUMS: return O_MAGNUM_ITEM; case LGT_AUTOS: return O_AUTOS_ITEM; case LGT_DESERT_EAGLE: return O_DESERT_EAGLE_ITEM; case LGT_UZIS: return O_UZI_ITEM; case LGT_SHOTGUN: return O_SHOTGUN_ITEM; case LGT_HARPOON: return O_HARPOON_ITEM; case LGT_M16: return O_M16_ITEM; case LGT_MP5: return O_MP5_ITEM; case LGT_GRENADE: return O_GRENADE_GUN_ITEM; case LGT_ROCKET: return O_ROCKET_GUN_ITEM; default: return NO_OBJECT; } // clang-format on } OBJECT_ID Gun_GetAmmoObject(const LARA_GUN_TYPE gun_type) { // clang-format off switch (gun_type) { case LGT_PISTOLS: return O_PISTOL_AMMO_ITEM; case LGT_MAGNUMS: return O_MAGNUM_AMMO_ITEM; case LGT_AUTOS: return O_AUTOS_AMMO_ITEM; case LGT_DESERT_EAGLE: return O_DESERT_EAGLE_AMMO_ITEM; case LGT_UZIS: return O_UZI_AMMO_ITEM; case LGT_SHOTGUN: return O_SHOTGUN_AMMO_ITEM; case LGT_HARPOON: return O_HARPOON_AMMO_ITEM; case LGT_M16: return O_M16_AMMO_ITEM; case LGT_MP5: return O_MP5_AMMO_ITEM; case LGT_GRENADE: return O_GRENADE_AMMO_ITEM; case LGT_ROCKET: return O_ROCKET_AMMO_ITEM; default: return NO_OBJECT; } // clang-format on } int32_t Gun_GetAmmoInitialQuantity(const LARA_GUN_TYPE gun_type) { return M_GetAmmoQuantity(gun_type, g_Weapons[gun_type].ammo.initial_qty); } int32_t Gun_GetAmmoPickupQuantity(const LARA_GUN_TYPE gun_type) { return M_GetAmmoQuantity(gun_type, g_Weapons[gun_type].ammo.pickup_qty); } int32_t Gun_GetAmmoClipCount(const LARA_GUN_TYPE gun_type) { return gun_type == LGT_SHOTGUN ? M_SHOTGUN_AMMO_CLIP : 1; } int32_t Gun_GetAmmoShellCount(const LARA_GUN_TYPE gun_type) { return Gun_GetAmmoPickupQuantity(gun_type) / Gun_GetAmmoClipCount(gun_type); } AMMO_INFO *Gun_GetAmmoInfo(const LARA_GUN_TYPE gun_type) { LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (lara_info == nullptr) { return nullptr; } // clang-format off switch (gun_type) { case LGT_PISTOLS: return &lara_info->pistol_ammo; case LGT_MAGNUMS: return &lara_info->magnum_ammo; case LGT_AUTOS: return &lara_info->autos_ammo; case LGT_DESERT_EAGLE: return &lara_info->desert_eagle_ammo; case LGT_UZIS: return &lara_info->uzi_ammo; case LGT_SHOTGUN: return &lara_info->shotgun_ammo; case LGT_HARPOON: return &lara_info->harpoon_ammo; case LGT_M16: return &lara_info->m16_ammo; case LGT_MP5: return &lara_info->mp5_ammo; case LGT_GRENADE: return &lara_info->grenade_ammo; case LGT_ROCKET: return &lara_info->rocket_ammo; case LGT_SKIDOO: return &lara_info->pistol_ammo; default: return nullptr; } // clang-format on } bool Gun_IsRifleType(const LARA_GUN_TYPE gun_type) { return M_IsGunType(gun_type, WEAPON_TYPE_RIFLE); } void Gun_SetLaraHandLMesh(const LARA_GUN_TYPE weapon_type) { Lara_Skin_SetGunEquipment(LM_HAND_L, weapon_type); } void Gun_SetLaraHandRMesh(const LARA_GUN_TYPE weapon_type) { Lara_Skin_SetGunEquipment(LM_HAND_R, weapon_type); } void Gun_SetLaraBackMesh(const LARA_GUN_TYPE weapon_type) { Lara_Skin_SetGunEquipment(LM_TORSO, weapon_type); LARA_INFO *const lara_info = Lara_GetLaraInfo(); lara_info->back_gun_type = weapon_type; } void Gun_SetLaraHolsterLMesh(const LARA_GUN_TYPE weapon_type) { Lara_Skin_SetGunEquipment(LM_THIGH_L, weapon_type); LARA_INFO *const lara_info = Lara_GetLaraInfo(); lara_info->holsters_gun_type = weapon_type; } void Gun_SetLaraHolsterRMesh(const LARA_GUN_TYPE weapon_type) { Lara_Skin_SetGunEquipment(LM_THIGH_R, weapon_type); LARA_INFO *const lara_info = Lara_GetLaraInfo(); lara_info->holsters_gun_type = weapon_type; } ================================================ FILE: src/trx/game/gun/common.h ================================================ #pragma once #include void Gun_InitialiseNewWeapon(void); void Gun_SetLaraBackMesh(LARA_GUN_TYPE weapon_type); void Gun_SetLaraHandLMesh(LARA_GUN_TYPE weapon_type); void Gun_SetLaraHandRMesh(LARA_GUN_TYPE weapon_type); void Gun_SetLaraHolsterLMesh(LARA_GUN_TYPE weapon_type); void Gun_SetLaraHolsterRMesh(LARA_GUN_TYPE weapon_type); // TODO: make this a struct OBJECT_ID Gun_GetLaraAnim(LARA_GUN_TYPE gun_type); OBJECT_ID Gun_GetWeaponAnim(LARA_GUN_TYPE gun_type); LARA_GUN_TYPE Gun_GetType(OBJECT_ID obj_id); OBJECT_ID Gun_GetGunObject(LARA_GUN_TYPE gun_type); OBJECT_ID Gun_GetAmmoObject(LARA_GUN_TYPE gun_type); int32_t Gun_GetAmmoInitialQuantity(LARA_GUN_TYPE gun_type); int32_t Gun_GetAmmoPickupQuantity(LARA_GUN_TYPE gun_type); int32_t Gun_GetAmmoShellCount(LARA_GUN_TYPE gun_type); int32_t Gun_GetAmmoClipCount(LARA_GUN_TYPE gun_type); AMMO_INFO *Gun_GetAmmoInfo(LARA_GUN_TYPE gun_type); bool Gun_IsRifleType(LARA_GUN_TYPE gun_type); void Gun_AddDynamicLight(void); ================================================ FILE: src/trx/game/gun/control.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static struct { LARA_GUN_TYPE gun_type; INPUT_ROLE input_role; } m_QuicDrawKeys[] = { { .gun_type = LGT_PISTOLS, .input_role = INPUT_ROLE_EQUIP_PISTOLS }, { .gun_type = LGT_SHOTGUN, .input_role = INPUT_ROLE_EQUIP_SHOTGUN }, { .gun_type = LGT_MAGNUMS, .input_role = INPUT_ROLE_EQUIP_MAGNUMS }, { .gun_type = LGT_AUTOS, .input_role = INPUT_ROLE_EQUIP_AUTOS }, { .gun_type = LGT_DESERT_EAGLE, .input_role = INPUT_ROLE_EQUIP_DESERT_EAGLE }, { .gun_type = LGT_UZIS, .input_role = INPUT_ROLE_EQUIP_UZIS }, { .gun_type = LGT_HARPOON, .input_role = INPUT_ROLE_EQUIP_HARPOON }, { .gun_type = LGT_M16, .input_role = INPUT_ROLE_EQUIP_M16 }, { .gun_type = LGT_MP5, .input_role = INPUT_ROLE_EQUIP_MP5 }, { .gun_type = LGT_GRENADE, .input_role = INPUT_ROLE_EQUIP_GRENADE_LAUNCHER }, { .gun_type = LGT_ROCKET, .input_role = INPUT_ROLE_EQUIP_ROCKET_LAUNCHER }, { .gun_type = LGT_UNKNOWN, .input_role = (INPUT_ROLE)-1 }, }; static void M_CheckSmashablesBehindTarget( const ITEM *const target, const GAME_VECTOR start, const GAME_VECTOR hit_pos, const int32_t max_dist) { if (target == nullptr || target->object_id != O_SOPHIA) { return; } // OG does a raycast instead of segment cast when checking for smashables. // TRX normally doesn't do that, but in the Reunion battle against Sophia, // Sophia stands directly in front of the Fuse Box, preventing Lara from // shooting her which is a breaking behavior. // // This function does additional smashable pass by emulating the OG raycast. const int32_t hit_dist = XYZ_32_GetDistance(start.pos, hit_pos.pos); if (hit_dist >= max_dist) { return; } const int32_t offset = STEP_L / 16; GAME_VECTOR follow_start = { .x = hit_pos.x + ((offset * g_MatrixPtr->_20) >> W2V_SHIFT), .y = hit_pos.y + ((offset * g_MatrixPtr->_21) >> W2V_SHIFT), .z = hit_pos.z + ((offset * g_MatrixPtr->_22) >> W2V_SHIFT), .room_num = hit_pos.room_num, }; Room_GetSector(follow_start.pos, &follow_start.room_num); GAME_VECTOR follow_end = { .x = start.x + ((max_dist * g_MatrixPtr->_20) >> W2V_SHIFT), .y = start.y + ((max_dist * g_MatrixPtr->_21) >> W2V_SHIFT), .z = start.z + ((max_dist * g_MatrixPtr->_22) >> W2V_SHIFT), .room_num = start.room_num, }; Room_GetSector(follow_end.pos, &follow_end.room_num); Gun_SmashItems(follow_start, follow_end, nullptr, NO_OBJECT); } static bool M_IsUsableUnderwater(const LARA_GUN_TYPE gun_type) { return gun_type == LGT_HARPOON; } static bool M_IsTooSubmerged(const LARA_GUN_TYPE gun_type) { const LARA_INFO *const lara = Lara_GetLaraInfo(); return lara->water_surface_dist > -g_Weapons[gun_type].gun_height; } static LARA_GUN_TYPE M_NeedToQuickDraw(void) { LARA_INFO *const lara = Lara_GetLaraInfo(); for (int32_t i = 0; m_QuicDrawKeys[i].gun_type != LGT_UNKNOWN; i++) { if (Input_IsPressedDB(m_QuicDrawKeys[i].input_role) && Inv_RequestItem(Gun_GetGunObject(m_QuicDrawKeys[i].gun_type)) > 0) { return m_QuicDrawKeys[i].gun_type; } } return LGT_UNKNOWN; } static bool M_QuickDrawWeapon(void) { LARA_INFO *const lara = Lara_GetLaraInfo(); const LARA_GUN_TYPE gun_type = M_NeedToQuickDraw(); if (gun_type != LGT_UNKNOWN) { lara->request_gun_type = gun_type; return true; } return false; } static bool M_CanEquip(void) { const LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->request_gun_type == LGT_FLARE) { return lara->gun_type != LGT_FLARE; } if (lara->is_crouched && Gun_IsRifleType(lara->request_gun_type)) { return false; } if (Lara_Vehicle_IsMounted()) { return false; } if (!Inv_RequestItem(Gun_GetGunObject(lara->request_gun_type))) { return false; } switch (lara->water_status) { case LWS_CHEAT: return false; case LWS_ABOVE_WATER: return true; case LWS_SURFACE: case LWS_UNDERWATER: return M_IsUsableUnderwater(lara->request_gun_type); case LWS_WADE: { if (M_IsUsableUnderwater(lara->request_gun_type)) { return true; } if (lara->gun_status == LGS_ARMLESS || M_IsTooSubmerged(lara->gun_type)) { return true; } return false; default: ASSERT_FAIL(); return false; } } } static bool M_HasWeaponAnim(const LARA_GUN_TYPE gun_type) { const OBJECT *const obj = Object_Get(Gun_GetLaraAnim(gun_type)); return obj->loaded && obj->frame_base != nullptr; } static bool M_NeedToDraw(void) { const LARA_INFO *const lara = Lara_GetLaraInfo(); if (g_Input.draw || lara->request_gun_type != lara->gun_type) { return true; } return false; } static bool M_NeedToUndraw(void) { LARA_INFO *const lara = Lara_GetLaraInfo(); if (g_Input.draw || lara->request_gun_type != lara->gun_type) { return true; } if (M_QuickDrawWeapon()) { if (g_Config.input.quick_guns_mode == QUICK_GUNS_MODE_DRAW_AND_HOLSTER || lara->request_gun_type != lara->gun_type) { return true; } } switch (lara->water_status) { case LWS_CHEAT: return true; case LWS_UNDERWATER: case LWS_SURFACE: return !M_IsUsableUnderwater(lara->request_gun_type); case LWS_ABOVE_WATER: return false; case LWS_WADE: return !M_IsUsableUnderwater(lara->request_gun_type) && !M_IsTooSubmerged(lara->gun_type); default: return false; } } static void M_DecideRequestedWeapon(void) { LARA_INFO *const lara = Lara_GetLaraInfo(); const ITEM *const lara_item = Lara_GetItem(); if (g_Input.draw) { LARA_GUN_TYPE requested_gun = lara->last_gun_type != LGT_UNARMED ? lara->last_gun_type : LGT_PISTOLS; if (Inv_RequestItem(Gun_GetGunObject(requested_gun)) == 0) { for (LARA_GUN_TYPE gun = 0; gun < NUM_WEAPONS; gun++) { if (Inv_RequestItem(Gun_GetGunObject(gun)) > 0) { requested_gun = gun; break; } } } if (Inv_RequestItem(Gun_GetGunObject(requested_gun)) != 0) { lara->request_gun_type = requested_gun; } return; } if (g_Input.use_flare) { if (lara->gun_type == LGT_FLARE) { lara->gun_status = LGS_UNDRAW; } else if ( Inv_RequestItem(O_FLAREBOX_ITEM) && (!g_Config.gameplay.fix_free_flare_glitch || lara_item->current_anim_state != LS(LS_PICKUP))) { lara->request_gun_type = LGT_FLARE; } } } static void M_DrawRequestedWeapon(void) { LARA_INFO *const lara = Lara_GetLaraInfo(); if (M_CanEquip()) { if (!M_HasWeaponAnim(lara->request_gun_type)) { lara->request_gun_type = LGT_UNARMED; lara->gun_type = LGT_UNARMED; lara->gun_status = LGS_ARMLESS; return; } if (lara->gun_type == LGT_FLARE) { Lara_Flare_Dispose(false); } lara->gun_type = lara->request_gun_type; Gun_InitialiseNewWeapon(); lara->gun_status = LGS_DRAW; lara->right_arm.frame_num = 0; lara->left_arm.frame_num = 0; } else { if (lara->request_gun_type != LGT_FLARE && lara->request_gun_type != LGT_UNARMED) { lara->last_gun_type = lara->request_gun_type; } if (lara->gun_type == LGT_FLARE) { lara->request_gun_type = LGT_FLARE; } else { lara->gun_type = lara->request_gun_type; } } } static void M_TryUndrawWeapon(void) { LARA_INFO *const lara = Lara_GetLaraInfo(); if (g_Input.use_flare && Inv_RequestItem(O_FLAREBOX_ITEM)) { lara->request_gun_type = LGT_FLARE; } if (M_NeedToUndraw()) { lara->gun_status = LGS_UNDRAW; } } static void M_UpdateGunState(void) { LARA_INFO *const lara = Lara_GetLaraInfo(); const ITEM *const lara_item = Lara_GetItem(); if (lara_item->hit_points <= 0) { lara->gun_status = LGS_ARMLESS; } else if (lara->gun_status == LGS_ARMLESS) { if (M_QuickDrawWeapon()) { M_DrawRequestedWeapon(); } else { M_DecideRequestedWeapon(); if (M_NeedToDraw()) { M_DrawRequestedWeapon(); } } } else if (lara->gun_status == LGS_READY) { M_TryUndrawWeapon(); } else { M_QuickDrawWeapon(); } } void Gun_Control(void) { LARA_INFO *const lara = Lara_GetLaraInfo(); const ITEM *const lara_item = Lara_GetItem(); if (lara->extra_anim && lara->gun_status != LGS_HANDS_BUSY) { lara->request_gun_type = LGT_UNARMED; return; } if (lara->left_arm.flash_gun > 0) { lara->left_arm.flash_gun--; } if (lara->right_arm.flash_gun > 0) { lara->right_arm.flash_gun--; } Gun_Smoke_Control(); M_UpdateGunState(); switch (lara->gun_status) { case LGS_ARMLESS: case LGS_HANDS_BUSY: if (lara->gun_type == LGT_FLARE) { Lara_Flare_Control(); } break; case LGS_DRAW: if (lara->gun_type != LGT_FLARE && lara->gun_type != LGT_UNARMED) { lara->last_gun_type = lara->gun_type; } switch (lara->gun_type) { case LGT_PISTOLS: case LGT_MAGNUMS: case LGT_AUTOS: case LGT_DESERT_EAGLE: case LGT_UZIS: if (g_Camera.type != CAM_CINEMATIC && g_Camera.type != CAM_LOOK) { g_Camera.type = CAM_COMBAT; } Gun_Pistols_Draw(lara->gun_type); break; case LGT_SHOTGUN: case LGT_M16: case LGT_MP5: case LGT_GRENADE: case LGT_ROCKET: case LGT_HARPOON: if (g_Camera.type != CAM_CINEMATIC && g_Camera.type != CAM_LOOK) { g_Camera.type = CAM_COMBAT; } Gun_Rifle_Draw(lara->gun_type); break; case LGT_FLARE: Lara_Flare_Draw(); break; default: lara->gun_status = LGS_ARMLESS; break; } break; case LGS_UNDRAW: Lara_Skin_SetCombatFace(false); switch (lara->gun_type) { case LGT_PISTOLS: case LGT_MAGNUMS: case LGT_AUTOS: case LGT_DESERT_EAGLE: case LGT_UZIS: Gun_Pistols_Undraw(lara->gun_type); break; case LGT_SHOTGUN: case LGT_M16: case LGT_MP5: case LGT_GRENADE: case LGT_ROCKET: case LGT_HARPOON: Gun_Rifle_Undraw(lara->gun_type); break; case LGT_FLARE: Lara_Flare_Undraw(); break; default: return; } break; case LGS_READY: const bool is_firing = lara->pistol_ammo.ammo != 0 && g_Input.action; Lara_Skin_SetCombatFace(is_firing); if (g_Camera.type != CAM_CINEMATIC && g_Camera.type != CAM_LOOK) { g_Camera.type = CAM_COMBAT; } if (g_Input.action) { AMMO_INFO *const ammo = Gun_GetAmmoInfo(lara->gun_type); ASSERT(ammo != nullptr); if (ammo->ammo <= 0) { ammo->ammo = 0; if (g_TRVersion >= 2) { Sound_Effect(SFX_CLICK, &lara_item->pos, SPM_NORMAL); } lara->request_gun_type = Inv_RequestItem(O_PISTOL_ITEM) ? LGT_PISTOLS : LGT_UNARMED; break; } } switch (lara->gun_type) { case LGT_PISTOLS: case LGT_MAGNUMS: case LGT_AUTOS: case LGT_DESERT_EAGLE: case LGT_UZIS: Gun_Pistols_Control(lara->gun_type); break; case LGT_SHOTGUN: case LGT_M16: case LGT_MP5: case LGT_GRENADE: case LGT_ROCKET: case LGT_HARPOON: Gun_Rifle_Control(lara->gun_type); break; default: return; } break; case LGS_SPECIAL: Lara_Flare_Draw(); break; default: return; } } void Gun_EnsureReady(void) { const LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->gun_status == LGS_READY && Gun_IsRifleType(lara->gun_type)) { Gun_Rifle_EnsureReady(lara->gun_type); } } int32_t Gun_FireWeapon( const LARA_GUN_TYPE weapon_type, ITEM *const target, const ITEM *const src, const int16_t *const angles) { const WEAPON_INFO *const weapon = &g_Weapons[weapon_type]; LARA_INFO *const lara = Lara_GetLaraInfo(); AMMO_INFO *const ammo = Gun_GetAmmoInfo(weapon_type); ASSERT(ammo != nullptr); if (ammo == &lara->pistol_ammo || Game_IsBonusFlagSet(GBF_NGPLUS)) { ammo->ammo = 1000; } if (ammo->ammo <= 0) { ammo->ammo = 0; if (g_TRVersion == 1) { Sound_Effect(SFX_LARA_EMPTY, &src->pos, SPM_NORMAL); if (Inv_RequestItem(O_PISTOL_ITEM)) { lara->request_gun_type = LGT_PISTOLS; } else { lara->gun_status = LGS_UNDRAW; } } return 0; } ammo->ammo--; Stats_AddAmmoUsed(); lara->has_fired = true; const XYZ_32 view_pos = { .x = src->pos.x, .y = src->pos.y - weapon->gun_height, .z = src->pos.z, }; const XYZ_16 view_rot = { .x = angles[1] + weapon->shot_accuracy * (Random_GetControl() - DEG_90) / DEG_360, .y = angles[0] + weapon->shot_accuracy * (Random_GetControl() - DEG_90) / DEG_360, .z = 0, }; Matrix_GenerateW2V(&view_pos, &view_rot); SPHERE spheres[33]; int32_t sphere_count = Collide_GetSpheres(target, spheres, false); int32_t best_sphere = -1; int32_t best_dist = INT32_MAX; for (int32_t i = 0; i < sphere_count; i++) { const SPHERE *const sphere = &spheres[i]; const int32_t r = sphere->r; if (ABS(sphere->pos.x) < r && ABS(sphere->pos.y) < r && sphere->pos.z > r && SQUARE(sphere->pos.x) + SQUARE(sphere->pos.y) <= SQUARE(r)) { const int32_t dist = sphere->pos.z - r; if (dist < best_dist) { best_dist = dist; best_sphere = i; } } } GAME_VECTOR start = { .pos = view_pos, .room_num = src->room_num, }; if (best_sphere < 0) { const int32_t dist = weapon->target_dist; GAME_VECTOR hit_pos = g_TRVersion == 1 ? (GAME_VECTOR) { .x = start.x + g_MatrixPtr->_20, .y = start.y + g_MatrixPtr->_21, .z = start.z + g_MatrixPtr->_22, .room_num = start.room_num, } : (GAME_VECTOR) { .x = start.x + ((dist * g_MatrixPtr->_20) >> W2V_SHIFT), .y = start.y + ((dist * g_MatrixPtr->_21) >> W2V_SHIFT), .z = start.z + ((dist * g_MatrixPtr->_22) >> W2V_SHIFT), .room_num = start.room_num, }; Room_GetSector(hit_pos.pos, &hit_pos.room_num); const bool object_on_los = LOS_Check(&start, &hit_pos, true); if (Gun_SmashItems(start, hit_pos, &hit_pos.pos, NO_OBJECT) == PROJECTILE_HIT_STOP) { Room_GetSector(hit_pos.pos, &hit_pos.room_num); } if (!object_on_los) { Spawn_RicochetRay(start, hit_pos); } return -1; } Stats_AddAmmoHits(); GAME_VECTOR hit_pos = { .x = start.x + ((best_dist * g_MatrixPtr->_20) >> W2V_SHIFT), .y = start.y + ((best_dist * g_MatrixPtr->_21) >> W2V_SHIFT), .z = start.z + ((best_dist * g_MatrixPtr->_22) >> W2V_SHIFT), .room_num = start.room_num, }; Room_GetSector(hit_pos.pos, &hit_pos.room_num); Gun_SmashItems(start, hit_pos, nullptr, NO_OBJECT); Gun_HitTarget( target, &start, &hit_pos, weapon->damage * (Game_IsBonusFlagSet(GBF_JAPANESE) ? 2 : 1)); M_CheckSmashablesBehindTarget(target, start, hit_pos, weapon->target_dist); return 1; } ================================================ FILE: src/trx/game/gun/control.h ================================================ #pragma once #include #include int32_t Gun_FireWeapon( LARA_GUN_TYPE weapon_type, ITEM *target, const ITEM *src, const int16_t *angles); void Gun_Control(void); void Gun_EnsureReady(void); ================================================ FILE: src/trx/game/gun/misc.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_NEAR_ANGLE (DEG_1 * 15) // = 2730 static ITEM *m_TargetList[LOT_SLOT_COUNT] = {}; static ITEM *m_LastTargetList[LOT_SLOT_COUNT] = {}; static int16_t m_TargetCount = 0; static bool M_TargetListContains(const ITEM *const item, const int16_t count) { for (int16_t i = 0; i < count; i++) { if (m_TargetList[i] == item) { return true; } } return false; } typedef struct { const WEAPON_INFO *weapon; const GAME_VECTOR *start; const ITEM *lara_item; const LARA_INFO *lara; const ITEM *old_target; int32_t max_dist; ITEM *best_target; int16_t best_y_rot; int32_t best_dist; int16_t num_targets; int32_t old_target_dist; int16_t old_target_y_rot; bool old_target_in_list; } M_TARGET_CONTEXT; static void M_ConsiderTarget(M_TARGET_CONTEXT *const ctx, ITEM *const item) { if (item == ctx->lara_item || !Item_IsTargetable(item)) { return; } const int32_t dx = item->pos.x - ctx->start->x; const int32_t dy = item->pos.y - ctx->start->y; const int32_t dz = item->pos.z - ctx->start->z; if (ABS(dx) > ctx->max_dist || ABS(dy) > ctx->max_dist || ABS(dz) > ctx->max_dist) { return; } const int32_t dist = SQUARE(dx) + SQUARE(dy) + SQUARE(dz); if (dist >= SQUARE(ctx->max_dist)) { return; } GAME_VECTOR target; Gun_FindTargetPoint(item, &target); if (!LOS_Check(ctx->start, &target, true)) { return; } int16_t angles[2]; Math_GetVectorAngles( target.x - ctx->start->x, target.y - ctx->start->y, target.z - ctx->start->z, angles); angles[0] -= ctx->lara->torso_rot.y + ctx->lara_item->rot.y; angles[1] -= ctx->lara->torso_rot.x + ctx->lara_item->rot.x; if (angles[0] < ctx->weapon->lock_angles[0] || angles[0] > ctx->weapon->lock_angles[1] || angles[1] < ctx->weapon->lock_angles[2] || angles[1] > ctx->weapon->lock_angles[3]) { return; } if (ctx->num_targets < LOT_SLOT_COUNT) { m_TargetList[ctx->num_targets] = item; ctx->num_targets++; } const int16_t y_rot = ABS(angles[0]); if (item == ctx->old_target) { ctx->old_target_dist = dist; ctx->old_target_y_rot = y_rot; ctx->old_target_in_list = true; } if (g_TRVersion == 1) { if (y_rot < ctx->best_y_rot) { ctx->best_dist = dist; ctx->best_y_rot = y_rot; ctx->best_target = item; } } else { if (y_rot < ctx->best_y_rot + M_NEAR_ANGLE && dist < ctx->best_dist) { ctx->best_dist = dist; ctx->best_y_rot = y_rot; ctx->best_target = item; } } } static void M_DrawGunGlow(const XYZ_32 offset, const RGB_F color) { if (g_TRVersion < 3) { return; } const OBJECT *const glow_obj = Object_Get(O_GLOW); if (!glow_obj->loaded) { return; } Matrix_Push(); Matrix_TranslateRel32(offset); const XYZ_32 pos = { .x = (int32_t)(g_WMatrixPtr->_03 >> W2V_SHIFT), .y = (int32_t)(g_WMatrixPtr->_13 >> W2V_SHIFT), .z = (int32_t)(g_WMatrixPtr->_23 >> W2V_SHIFT), }; Matrix_Pop(); Output_DrawSprite( pos.x, pos.y, pos.z, glow_obj->mesh_idx, 0, color, DRAW_BLEND_ADD); } void Gun_FindTargetPoint(const ITEM *const item, GAME_VECTOR *const target) { const BOUNDS_16 *const bounds = &Item_GetBestFrame(item)->bounds; const int32_t x = bounds->min.x + (bounds->max.x - bounds->min.x) / 2; const int32_t y = bounds->min.y + (bounds->max.y - bounds->min.y) / 3; const int32_t z = bounds->min.z + (bounds->max.z - bounds->min.z) / 2; const int32_t cy = Math_Cos(item->rot.y); const int32_t sy = Math_Sin(item->rot.y); target->pos.x = item->pos.x + ((cy * x + sy * z) >> W2V_SHIFT); target->pos.y = item->pos.y + y; target->pos.z = item->pos.z + ((cy * z - sy * x) >> W2V_SHIFT); target->room_num = item->room_num; } void Gun_AimWeapon(const WEAPON_INFO *const weapon, LARA_ARM *const arm) { const LARA_INFO *const lara = Lara_GetLaraInfo(); const int16_t speed = weapon->aim_speed; int16_t dest_x = 0; int16_t dest_y = 0; if (arm->lock) { dest_y = lara->target_angles[0]; dest_x = lara->target_angles[1]; } if (arm->rot.y >= dest_y - speed && arm->rot.y <= dest_y + speed) { arm->rot.y = dest_y; } else if (arm->rot.y < dest_y) { arm->rot.y += speed; } else { arm->rot.y -= speed; } if (arm->rot.x >= dest_x - speed && arm->rot.x <= dest_x + speed) { arm->rot.x = dest_x; } else if (arm->rot.x < dest_x) { arm->rot.x += speed; } else { arm->rot.x -= speed; } arm->rot.z = 0; } void Gun_TargetInfo(const WEAPON_INFO *const weapon) { LARA_INFO *const lara = Lara_GetLaraInfo(); const ITEM *const lara_item = Lara_GetItem(); if (lara->target == nullptr) { lara->left_arm.lock = 0; lara->right_arm.lock = 0; lara->target_angles[0] = 0; lara->target_angles[1] = 0; return; } GAME_VECTOR target; GAME_VECTOR start = { .pos = { .x = lara_item->pos.x, .y = lara_item->pos.y - 650, .z = lara_item->pos.z, }, .room_num = lara_item->room_num, }; Gun_FindTargetPoint(lara->target, &target); int16_t angles[2]; // clang-format off Math_GetVectorAngles( target.pos.x - start.pos.x, target.pos.y - start.pos.y, target.pos.z - start.pos.z, angles); // clang-format on angles[0] -= lara_item->rot.y; angles[1] -= lara_item->rot.x; if (!LOS_Check(&start, &target, true)) { lara->left_arm.lock = 0; lara->right_arm.lock = 0; } else if ( angles[0] >= weapon->lock_angles[0] && angles[0] <= weapon->lock_angles[1] && angles[1] >= weapon->lock_angles[2] && angles[1] <= weapon->lock_angles[3]) { lara->left_arm.lock = 1; lara->right_arm.lock = 1; } else { if (lara->left_arm.lock && (angles[0] < weapon->left_angles[0] || angles[0] > weapon->left_angles[1] || angles[1] < weapon->left_angles[2] || angles[1] > weapon->left_angles[3])) { lara->left_arm.lock = 0; } if (lara->right_arm.lock && (angles[0] < weapon->right_angles[0] || angles[0] > weapon->right_angles[1] || angles[1] < weapon->right_angles[2] || angles[1] > weapon->right_angles[3])) { lara->right_arm.lock = 0; } } lara->target_angles[0] = angles[0]; lara->target_angles[1] = angles[1]; } void Gun_InitialiseNewWeapon(void) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->target = nullptr; lara->left_arm.flash_gun = 0; lara->left_arm.frame_num = LF_G_AIM_START; lara->left_arm.lock = 0; lara->left_arm.rot.x = 0; lara->left_arm.rot.y = 0; lara->left_arm.rot.z = 0; lara->right_arm.flash_gun = 0; lara->right_arm.frame_num = LF_G_AIM_START; lara->right_arm.lock = 0; lara->right_arm.rot.x = 0; lara->right_arm.rot.y = 0; lara->right_arm.rot.z = 0; const OBJECT_ID anim_type = Gun_GetLaraAnim(lara->gun_type); const OBJECT *const obj = Object_Get(anim_type); lara->left_arm.frame_base = obj->frame_base; lara->right_arm.frame_base = obj->frame_base; if (lara->gun_status != LGS_ARMLESS) { switch (lara->gun_type) { case LGT_PISTOLS: case LGT_MAGNUMS: case LGT_AUTOS: case LGT_DESERT_EAGLE: case LGT_UZIS: Gun_Pistols_DrawMeshes(lara->gun_type); break; case LGT_SHOTGUN: case LGT_M16: case LGT_MP5: case LGT_GRENADE: case LGT_ROCKET: case LGT_HARPOON: Gun_Rifle_DrawMeshes(lara->gun_type); break; case LGT_FLARE: Lara_Flare_DrawMeshes(); break; default: break; } } } void Gun_DrawFlash( const LARA_GUN_TYPE weapon_type, const CLIP clip, const bool interpolated) { if (weapon_type == LGT_SHOTGUN && !g_Config.visuals.enable_shotgun_flash) { return; } OBJECT_ID flash_object_id = O_GUN_FLASH; XYZ_16 rot = {}; switch (weapon_type) { case LGT_M16: case LGT_MP5: rot.x = -85 * DEG_1; rot.z = ((2 * Random_GetDraw()) & 0x4000); if (weapon_type == LGT_M16) { rot.z += 0x2000; } else { rot.z += (Random_GetDraw() & 0xFFF) + 0x1800; } flash_object_id = O_M16_FLASH; break; case LGT_FLARE: rot.x = -DEG_90; rot.y = 2 * Random_GetDraw(); flash_object_id = O_FLARE_FIRE; break; default: rot.x = -DEG_90; rot.z = 2 * Random_GetDraw(); break; } const WEAPON_INFO weapon = g_Weapons[weapon_type]; if (interpolated) { Matrix_TranslateRel32_I(weapon.flash_pos); Matrix_RotX_I(rot.x); Matrix_RotY_I(rot.y); Matrix_RotZ_I(rot.z); } else { Matrix_TranslateRel32(weapon.flash_pos); Matrix_RotX(rot.x); Matrix_RotY(rot.y); Matrix_RotZ(rot.z); } const GAME_VECTOR pos = { .room_num = Lara_GetItem()->room_num, .pos = Matrix_MulVec32_M(g_WMatrixPtr, (XYZ_32) {}), }; Output_PushTintOverride(Lara_GetMeshTint(pos)); if (g_TRVersion < 3) { Output_CalculateStaticLight(weapon.flash_shade); } else { Output_CalculateStaticLightRGB_F(weapon.flash_color); } const OBJECT *const flash_obj = Object_Get(flash_object_id); if (flash_obj->loaded) { Object_DrawMesh(flash_obj->mesh_idx, clip, interpolated); } M_DrawGunGlow(weapon.glow_pos, weapon.glow_color); Output_PopTintOverride(); } void Gun_UpdateLaraMeshes(const OBJECT_ID obj_id) { const bool lara_has_rifle = Inv_RequestItem(O_SHOTGUN_ITEM) || Inv_RequestItem(O_HARPOON_ITEM) || Inv_RequestItem(O_M16_ITEM) || Inv_RequestItem(O_MP5_ITEM) || Inv_RequestItem(O_GRENADE_GUN_ITEM) || Inv_RequestItem(O_ROCKET_GUN_ITEM); const bool lara_has_pistols = Inv_RequestItem(O_PISTOL_ITEM) || Inv_RequestItem(O_MAGNUM_ITEM) || Inv_RequestItem(O_AUTOS_ITEM) || Inv_RequestItem(O_DESERT_EAGLE_ITEM) || Inv_RequestItem(O_UZI_ITEM); LARA_GUN_TYPE back_gun_type = LGT_UNARMED; LARA_GUN_TYPE holsters_gun_type = LGT_UNARMED; if (!lara_has_rifle && obj_id == O_SHOTGUN_ITEM) { back_gun_type = LGT_SHOTGUN; } else if (!lara_has_rifle && obj_id == O_HARPOON_ITEM) { back_gun_type = LGT_HARPOON; } else if (!lara_has_rifle && obj_id == O_M16_ITEM) { back_gun_type = LGT_M16; } else if (!lara_has_rifle && obj_id == O_MP5_ITEM) { back_gun_type = LGT_MP5; } else if (!lara_has_rifle && obj_id == O_GRENADE_GUN_ITEM) { back_gun_type = LGT_GRENADE; } else if (!lara_has_rifle && obj_id == O_ROCKET_GUN_ITEM) { back_gun_type = LGT_ROCKET; } else if (!lara_has_pistols && obj_id == O_PISTOL_ITEM) { holsters_gun_type = LGT_PISTOLS; } else if (!lara_has_pistols && obj_id == O_MAGNUM_ITEM) { holsters_gun_type = LGT_MAGNUMS; } else if (!lara_has_pistols && obj_id == O_AUTOS_ITEM) { holsters_gun_type = LGT_AUTOS; } else if (!lara_has_pistols && obj_id == O_DESERT_EAGLE_ITEM) { holsters_gun_type = LGT_DESERT_EAGLE; } else if (!lara_has_pistols && obj_id == O_UZI_ITEM) { holsters_gun_type = LGT_UZIS; } if (back_gun_type != LGT_UNARMED) { Gun_SetLaraBackMesh(back_gun_type); } if (holsters_gun_type != LGT_UNARMED) { Gun_SetLaraHolsterLMesh(holsters_gun_type); Gun_SetLaraHolsterRMesh(holsters_gun_type); } } void Gun_HitTarget( ITEM *const item, const GAME_VECTOR *const start, const GAME_VECTOR *const hit_pos, int32_t damage) { OBJECT *const obj = Object_Get(item->object_id); if (obj->gun_hit_func != nullptr) { const bool use_default = obj->gun_hit_func(item, start, hit_pos, &damage); if (!use_default) { return; } } const bool make_ricochet = !Item_ShouldSpawnBlood(item); if (item->object_id == O_SHIVA && make_ricochet) { damage = 0; } LARA_INFO *const lara = Lara_GetLaraInfo(); const bool was_alive = item->hit_points > 0; const bool clears_target = was_alive && damage >= item->hit_points; if (clears_target) { if (item->include_in_kill_stats) { Stats_AddKill(); } if (g_Config.gameplay.target_mode == TARGET_LOCK_MODE_SEMI) { lara->target = nullptr; } } Item_TakeDamage(item, damage, true); if (item->creature_data != nullptr && Object_Get(item->object_id)->intelligent) { Creature_Hurt(item, damage); } if (hit_pos != nullptr) { if (make_ricochet) { const GAME_VECTOR pos = { .pos = hit_pos->pos, .room_num = item->room_num, }; if (start != nullptr) { Spawn_RicochetRay(*start, pos); } else { Spawn_Ricochet(pos); } } else { Spawn_Blood( hit_pos->x, hit_pos->y, hit_pos->z, item->speed, item->rot.y, item->room_num); } } if (item->hit_points > 0) { switch (item->object_id) { case O_WOLF: Sound_Effect(SFX_WOLF_HURT, &item->pos, SPM_NORMAL); break; case O_BEAR: Sound_Effect(SFX_BEAR_HURT, &item->pos, SPM_NORMAL); break; case O_LION: case O_LIONESS: Sound_Effect(SFX_LION_HURT, &item->pos, SPM_NORMAL); break; case O_RAT: Sound_Effect(SFX_RAT_CHIRP, &item->pos, SPM_NORMAL); break; case O_SKATEKID: Sound_Effect(SFX_SKATEBOARD_HIT, &item->pos, SPM_NORMAL); break; case O_TORSO: Sound_Effect(SFX_TORSO_HIT, &item->pos, SPM_NORMAL); break; default: break; } } } void Gun_GetNewTarget(const WEAPON_INFO *const weapon) { const ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara = Lara_GetLaraInfo(); const ITEM *const old_target = lara->target; // Preserve OG targeting behavior. if (g_Config.gameplay.target_mode == TARGET_LOCK_MODE_FULL && !g_Config.gameplay.enable_target_change && !g_Input.action) { lara->target = nullptr; } const GAME_VECTOR start = { .x = lara_item->pos.x, .y = lara_item->pos.y - 650, .z = lara_item->pos.z, .room_num = lara_item->room_num, }; M_TARGET_CONTEXT ctx = { .weapon = weapon, .start = &start, .lara_item = lara_item, .lara = lara, .old_target = old_target, .max_dist = weapon->target_dist, .best_target = nullptr, .best_y_rot = INT16_MAX, .best_dist = INT32_MAX, .num_targets = 0, .old_target_dist = INT32_MAX, .old_target_y_rot = INT16_MAX, .old_target_in_list = false, }; // First pass: active creatures for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) { const CREATURE *const creature = LOT_GetBaddieSlot(i); if (creature->item_num == NO_ITEM) { continue; } M_ConsiderTarget(&ctx, Item_Get(creature->item_num)); } // Second pass: other objects, including skidoo driver, whose targetable // ITEM is NOT in the active item list for (int32_t item_num = 0; item_num < Item_GetLevelCount(); item_num++) { ITEM *const item = Item_Get(item_num); if (M_TargetListContains(item, ctx.num_targets)) { continue; } M_ConsiderTarget(&ctx, item); } m_TargetCount = ctx.num_targets; if ((g_Config.gameplay.target_mode == TARGET_LOCK_MODE_FULL || g_Config.gameplay.target_mode == TARGET_LOCK_MODE_SEMI) && g_Input.action && lara->target != nullptr) { Gun_TargetInfo(weapon); return; } if (ctx.num_targets > 0) { bool found_current_target = false; for (int16_t slot = 0; slot < ctx.num_targets; slot++) { if (m_TargetList[slot] == lara->target) { found_current_target = true; break; } } if (!found_current_target) { lara->target = ctx.best_target; m_LastTargetList[0] = nullptr; } } else { lara->target = nullptr; } if (lara->target != m_LastTargetList[0]) { for (int32_t slot = LOT_SLOT_COUNT - 1; slot > 0; slot--) { m_LastTargetList[slot] = m_LastTargetList[slot - 1]; } m_LastTargetList[0] = lara->target; } Gun_TargetInfo(weapon); } void Gun_ChangeTarget(const WEAPON_INFO *const weapon) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->target = nullptr; bool found_new_target = false; for (int16_t new_target = 0; new_target < m_TargetCount; new_target++) { for (int32_t last_target = 0; last_target < LOT_SLOT_COUNT; last_target++) { if (!m_LastTargetList[last_target]) { found_new_target = true; break; } if (m_LastTargetList[last_target] == m_TargetList[new_target]) { break; } } if (found_new_target) { lara->target = m_TargetList[new_target]; break; } } if (lara->target != m_LastTargetList[0]) { for (int32_t last_target = LOT_SLOT_COUNT - 1; last_target > 0; last_target--) { m_LastTargetList[last_target] = m_LastTargetList[last_target - 1]; } m_LastTargetList[0] = lara->target; } Gun_TargetInfo(weapon); } ================================================ FILE: src/trx/game/gun/misc.h ================================================ #pragma once // Private gun routines. #include #include #include #include void Gun_FindTargetPoint(const ITEM *item, GAME_VECTOR *target); void Gun_AimWeapon(const WEAPON_INFO *weapon, LARA_ARM *arm); void Gun_TargetInfo(const WEAPON_INFO *weapon); void Gun_UpdateLaraMeshes(OBJECT_ID obj_id); void Gun_GetNewTarget(const WEAPON_INFO *weapon); void Gun_ChangeTarget(const WEAPON_INFO *weapon); void Gun_HitTarget( ITEM *item, const GAME_VECTOR *start, const GAME_VECTOR *hit_pos, int32_t damage); void Gun_DrawFlash(LARA_GUN_TYPE weapon_type, CLIP clip, bool interpolated); ================================================ FILE: src/trx/game/gun/pistols.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_ENABLE_FAST_UZI (g_TRVersion >= 2) typedef enum { // clang-format off LA_PISTOLS_AIM = 0, LA_PISTOLS_UNDRAW = 1, LA_PISTOLS_DRAW = 2, LA_PISTOLS_RECOIL = 3, // clang-format on } LARA_PISTOLS_ANIMATION; typedef struct { struct { int16_t start; int16_t extend; int16_t bend; int16_t end; } aim, undraw, draw, recoil; } M_FRAME_SETUP; static const M_FRAME_SETUP m_DefaultSetup = { .aim.start = LF_G_AIM_START, .aim.bend = LF_G_AIM_BEND, .aim.extend = LF_G_AIM_EXTEND, .aim.end = LF_G_AIM_END, .undraw.start = LF_G_UNDRAW_START, .undraw.bend = LF_G_UNDRAW_BEND, .undraw.end = LF_G_UNDRAW_END, .draw.start = LF_G_DRAW_START, .draw.end = LF_G_DRAW_END, .recoil.start = LF_G_RECOIL_START, .recoil.end = LF_G_RECOIL_END, }; static const M_FRAME_SETUP m_DesertEagleSetup = { .aim.start = LF_G_AIM_START, .aim.bend = LF_G_AIM_BEND, .aim.extend = 6, .aim.end = 7, .undraw.start = 8, .undraw.bend = 9, .undraw.end = 14, .draw.start = 15, .draw.end = 28, .recoil.start = 29, .recoil.end = 44, }; static bool m_SoundRight = false; static bool m_SoundLeft = false; static bool M_EnableFastSound(const LARA_GUN_TYPE weapon_type) { return g_TRVersion >= 2 && weapon_type == LGT_UZIS; } static void M_FireSound(const SAMPLE_TRX_ID sample_trx_id, const bool alternate) { SAMPLE_ID sample_id = Sound_ToGameID(sample_trx_id); if (sample_id == SFX_INVALID) { return; } if (alternate) { sample_id += 1; } Sound_Effect_Direct(sample_id, &Lara_GetItem()->pos, SPM_NORMAL); } static const M_FRAME_SETUP *M_GetSetup(const LARA_GUN_TYPE weapon_type) { return weapon_type == LGT_DESERT_EAGLE ? &m_DesertEagleSetup : &m_DefaultSetup; } static void M_SetArmInfo(LARA_ARM *const arm, const int32_t frame) { const LARA_INFO *const lara = Lara_GetLaraInfo(); const M_FRAME_SETUP *const setup = M_GetSetup(lara->gun_type); int16_t anim_idx; if (Anim_TestAbsFrameRange(frame, setup->aim.start, setup->aim.end)) { anim_idx = LA_PISTOLS_AIM; } else if ( Anim_TestAbsFrameRange(frame, setup->undraw.start, setup->undraw.end)) { anim_idx = LA_PISTOLS_UNDRAW; } else if ( Anim_TestAbsFrameRange(frame, setup->draw.start, setup->draw.end)) { anim_idx = LA_PISTOLS_DRAW; } else if ( Anim_TestAbsFrameRange(frame, setup->recoil.start, setup->recoil.end)) { anim_idx = LA_PISTOLS_RECOIL; } else { return; } const OBJECT_ID obj_id = Gun_GetLaraAnim(lara->gun_type); const OBJECT *const obj = Object_Get(obj_id); const ANIM *const anim = Object_GetAnim(obj, anim_idx); arm->anim_num = obj->anim_idx + anim_idx; arm->frame_num = frame; arm->frame_base = anim->frame_ptr; } static void M_Animate(const LARA_GUN_TYPE weapon_type) { LARA_INFO *const lara = Lara_GetLaraInfo(); const M_FRAME_SETUP *const setup = M_GetSetup(weapon_type); const WEAPON_INFO *const weapon = &g_Weapons[weapon_type]; const ITEM *const lara_item = Lara_GetItem(); bool sound_already = false; int16_t angles[2]; int32_t frame_r = lara->right_arm.frame_num; if (!lara->right_arm.lock && (!g_Input.action || lara->target != nullptr)) { if (Anim_TestAbsFrameRange( frame_r, setup->recoil.start, setup->recoil.end)) { frame_r = setup->aim.end; } else if ( Anim_TestAbsFrameRange(frame_r, setup->aim.bend, setup->aim.end)) { frame_r--; } if (m_SoundRight) { M_FireSound(weapon->sample_num, true); m_SoundRight = false; } } else { if (Anim_TestAbsFrameRange( frame_r, setup->aim.start, setup->aim.extend)) { frame_r++; } else if (frame_r == setup->aim.end) { if (g_Input.action) { angles[0] = lara->right_arm.rot.y + lara_item->rot.y; angles[1] = lara->right_arm.rot.x; if (weapon_type != LGT_DESERT_EAGLE && Gun_FireWeapon( weapon_type, lara->target, lara_item, angles)) { lara->right_arm.flash_gun = weapon->flash_time; Spawn_GunShell(weapon_type, true); Gun_Smoke_OnFire(weapon_type, true); if (!sound_already) { M_FireSound(weapon->sample_num, false); } sound_already = true; if (M_EnableFastSound(weapon_type)) { m_SoundRight = true; } } frame_r = setup->recoil.start; } else if (m_SoundRight) { M_FireSound(weapon->sample_num, true); m_SoundRight = false; } } else if ( Anim_TestAbsFrameRange( frame_r, setup->recoil.start, setup->recoil.end)) { frame_r++; if (frame_r == setup->recoil.start + weapon->recoil_frame) { frame_r = setup->aim.end; } if (M_EnableFastSound(weapon_type)) { M_FireSound(weapon->sample_num, false); m_SoundRight = true; } } } M_SetArmInfo(&lara->right_arm, frame_r); int16_t frame_l = lara->left_arm.frame_num; if (!lara->left_arm.lock && (!g_Input.action || lara->target != nullptr)) { if (Anim_TestAbsFrameRange( frame_l, setup->recoil.start, setup->recoil.end)) { frame_l = setup->aim.end; } else if ( Anim_TestAbsFrameRange(frame_l, setup->aim.bend, setup->aim.end)) { frame_l--; } if (m_SoundLeft) { M_FireSound(weapon->sample_num, true); m_SoundLeft = false; } } else if ( Anim_TestAbsFrameRange(frame_l, setup->aim.start, setup->aim.extend)) { frame_l++; } else if (frame_l == setup->aim.end) { if (g_Input.action) { angles[0] = lara->left_arm.rot.y + lara_item->rot.y; angles[1] = lara->left_arm.rot.x; if (Gun_FireWeapon(weapon_type, lara->target, lara_item, angles)) { if (weapon_type == LGT_DESERT_EAGLE) { lara->right_arm.flash_gun = weapon->flash_time; Spawn_GunShell(weapon_type, true); Gun_Smoke_OnFire(weapon_type, true); } else { lara->left_arm.flash_gun = weapon->flash_time; Spawn_GunShell(weapon_type, false); Gun_Smoke_OnFire(weapon_type, false); } if (!sound_already) { M_FireSound(weapon->sample_num, false); } if (M_EnableFastSound(weapon_type)) { m_SoundLeft = true; } } frame_l = setup->recoil.start; } else if (m_SoundLeft) { M_FireSound(weapon->sample_num, true); m_SoundLeft = false; } } else if ( Anim_TestAbsFrameRange( frame_l, setup->recoil.start, setup->recoil.end)) { frame_l++; if (frame_l == setup->recoil.start + weapon->recoil_frame) { frame_l = setup->aim.end; } if (M_EnableFastSound(weapon_type)) { M_FireSound(weapon->sample_num, false); m_SoundLeft = true; } } M_SetArmInfo(&lara->left_arm, frame_l); } void Gun_Pistols_Control(const LARA_GUN_TYPE weapon_type) { const WEAPON_INFO *const weapon = &g_Weapons[weapon_type]; LARA_INFO *const lara = Lara_GetLaraInfo(); Gun_GetNewTarget(weapon); if (g_InputDB.change_target && g_Config.gameplay.enable_target_change) { Gun_ChangeTarget(weapon); } Gun_AimWeapon(weapon, &lara->left_arm); if (weapon_type != LGT_DESERT_EAGLE) { Gun_AimWeapon(weapon, &lara->right_arm); } const bool lock_head = g_Config.gameplay.look_mode != LOOK_MODE_UNRESTRICTED || g_Camera.type != CAM_LOOK; if (weapon_type == LGT_DESERT_EAGLE) { if (lara->left_arm.lock) { lara->torso_rot.x = lara->left_arm.rot.x; lara->torso_rot.y = lara->left_arm.rot.y; if (lock_head) { lara->head_rot.x = 0; lara->head_rot.y = 0; } } } else if (lara->left_arm.lock && !lara->right_arm.lock) { if (lock_head) { lara->head_rot.x = lara->left_arm.rot.x / 2; lara->head_rot.y = lara->left_arm.rot.y / 2; } lara->torso_rot.x = lara->left_arm.rot.x / 2; lara->torso_rot.y = lara->left_arm.rot.y / 2; } else if (!lara->left_arm.lock && lara->right_arm.lock) { if (lock_head) { lara->head_rot.x = lara->right_arm.rot.x / 2; lara->head_rot.y = lara->right_arm.rot.y / 2; } lara->torso_rot.x = lara->right_arm.rot.x / 2; lara->torso_rot.y = lara->right_arm.rot.y / 2; } else if (lara->right_arm.lock) { if (lock_head) { lara->head_rot.x = (lara->right_arm.rot.x + lara->left_arm.rot.x) / 4; lara->head_rot.y = (lara->right_arm.rot.y + lara->left_arm.rot.y) / 4; } lara->torso_rot.x = (lara->right_arm.rot.x + lara->left_arm.rot.x) / 4; lara->torso_rot.y = (lara->right_arm.rot.y + lara->left_arm.rot.y) / 4; } M_Animate(weapon_type); if (lara->left_arm.flash_gun || lara->right_arm.flash_gun) { Gun_AddDynamicLight(); } } void Gun_Pistols_Draw(const LARA_GUN_TYPE weapon_type) { LARA_INFO *const lara = Lara_GetLaraInfo(); int16_t frame = lara->left_arm.frame_num + 1; const M_FRAME_SETUP *const setup = M_GetSetup(weapon_type); if (!Anim_TestAbsFrameRange(frame, setup->undraw.start, setup->draw.end)) { frame = setup->undraw.start; } else if (Anim_TestAbsFrameEqual(frame, setup->draw.start)) { Gun_Pistols_DrawMeshes(weapon_type); Sound_Effect(SFX_LARA_DRAW, &Lara_GetItem()->pos, SPM_NORMAL); } else if (Anim_TestAbsFrameEqual(frame, setup->draw.end)) { Gun_Pistols_Ready(weapon_type); frame = setup->aim.start; } M_SetArmInfo(&lara->right_arm, frame); M_SetArmInfo(&lara->left_arm, frame); } void Gun_Pistols_Undraw(const LARA_GUN_TYPE weapon_type) { LARA_INFO *const lara = Lara_GetLaraInfo(); const M_FRAME_SETUP *const setup = M_GetSetup(weapon_type); int16_t frame_l = lara->left_arm.frame_num; if (Anim_TestAbsFrameRange( frame_l, setup->recoil.start, setup->recoil.end)) { frame_l = setup->aim.end; } else if ( Anim_TestAbsFrameRange(frame_l, setup->aim.bend, setup->aim.end)) { lara->left_arm.rot.x -= lara->left_arm.rot.x / frame_l; lara->left_arm.rot.y -= lara->left_arm.rot.y / frame_l; frame_l--; } else if (Anim_TestAbsFrameEqual(frame_l, setup->aim.start)) { lara->left_arm.rot.x = 0; lara->left_arm.rot.y = 0; lara->left_arm.rot.z = 0; frame_l = setup->draw.end; } else if (Anim_TestAbsFrameEqual(frame_l, setup->draw.start)) { Gun_Pistols_UndrawMeshLeft(weapon_type); frame_l--; } else if ( Anim_TestAbsFrameRange(frame_l, setup->undraw.bend, setup->draw.end)) { frame_l--; } M_SetArmInfo(&lara->left_arm, frame_l); int16_t frame_r = lara->right_arm.frame_num; if (Anim_TestAbsFrameRange( frame_r, setup->recoil.start, setup->recoil.end)) { frame_r = setup->aim.end; } else if ( Anim_TestAbsFrameRange(frame_r, setup->aim.bend, setup->aim.end)) { lara->right_arm.rot.x -= lara->right_arm.rot.x / frame_r; lara->right_arm.rot.y -= lara->right_arm.rot.y / frame_r; frame_r--; } else if (Anim_TestAbsFrameEqual(frame_r, setup->aim.start)) { lara->right_arm.rot.x = 0; lara->right_arm.rot.y = 0; lara->right_arm.rot.z = 0; frame_r = setup->draw.end; } else if (Anim_TestAbsFrameEqual(frame_r, setup->draw.start)) { Gun_Pistols_UndrawMeshRight(weapon_type); frame_r--; } else if ( Anim_TestAbsFrameRange(frame_r, setup->undraw.bend, setup->draw.end)) { frame_r--; } M_SetArmInfo(&lara->right_arm, frame_r); if (Anim_TestAbsFrameEqual(frame_l, setup->undraw.start) && Anim_TestAbsFrameEqual(frame_r, setup->undraw.start)) { lara->gun_status = LGS_ARMLESS; lara->left_arm.lock = 0; lara->right_arm.lock = 0; lara->left_arm.frame_num = setup->aim.start; lara->right_arm.frame_num = setup->aim.start; lara->target = nullptr; } if (!g_Input.look || g_Config.gameplay.look_mode == LOOK_MODE_RESTRICTED) { lara->head_rot.x = (lara->left_arm.rot.x + lara->right_arm.rot.x) / 4; lara->head_rot.y = (lara->left_arm.rot.y + lara->right_arm.rot.y) / 4; lara->torso_rot.x = lara->head_rot.x; lara->torso_rot.y = lara->head_rot.y; } } void Gun_Pistols_Ready(const LARA_GUN_TYPE weapon_type) { LARA_INFO *const lara = Lara_GetLaraInfo(); const M_FRAME_SETUP *const setup = M_GetSetup(weapon_type); lara->gun_status = LGS_READY; lara->target = nullptr; const OBJECT *const obj = Object_Get(O_LARA_PISTOLS); lara->left_arm.frame_base = obj->frame_base; lara->left_arm.frame_num = setup->aim.start; lara->left_arm.lock = 0; lara->left_arm.rot.x = 0; lara->left_arm.rot.y = 0; lara->left_arm.rot.z = 0; lara->right_arm.frame_base = obj->frame_base; lara->right_arm.frame_num = setup->aim.start; lara->right_arm.lock = 0; lara->right_arm.rot.x = 0; lara->right_arm.rot.y = 0; lara->right_arm.rot.z = 0; if (g_Config.gameplay.look_mode == LOOK_MODE_RESTRICTED) { lara->head_rot.x = 0; lara->head_rot.y = 0; lara->torso_rot.x = 0; lara->torso_rot.y = 0; } } void Gun_Pistols_DrawMeshes(const LARA_GUN_TYPE weapon_type) { Gun_SetLaraHandRMesh(weapon_type); Gun_SetLaraHolsterRMesh(LGT_UNARMED); if (weapon_type != LGT_DESERT_EAGLE) { Gun_SetLaraHandLMesh(weapon_type); Gun_SetLaraHolsterLMesh(LGT_UNARMED); } } void Gun_Pistols_UndrawMeshLeft(const LARA_GUN_TYPE weapon_type) { if (weapon_type != LGT_DESERT_EAGLE) { Gun_SetLaraHandLMesh(LGT_UNARMED); Gun_SetLaraHolsterLMesh(weapon_type); Sound_Effect(SFX_LARA_HOLSTER, &Lara_GetItem()->pos, SPM_NORMAL); } } void Gun_Pistols_UndrawMeshRight(const LARA_GUN_TYPE weapon_type) { Gun_SetLaraHandRMesh(LGT_UNARMED); Gun_SetLaraHolsterRMesh(weapon_type); Sound_Effect(SFX_LARA_HOLSTER, &Lara_GetItem()->pos, SPM_NORMAL); } ================================================ FILE: src/trx/game/gun/pistols.h ================================================ #pragma once #include void Gun_Pistols_Control(LARA_GUN_TYPE weapon_type); void Gun_Pistols_Draw(LARA_GUN_TYPE weapon_type); void Gun_Pistols_Undraw(LARA_GUN_TYPE weapon_type); void Gun_Pistols_Ready(LARA_GUN_TYPE weapon_type); void Gun_Pistols_DrawMeshes(LARA_GUN_TYPE weapon_type); void Gun_Pistols_UndrawMeshLeft(LARA_GUN_TYPE weapon_type); void Gun_Pistols_UndrawMeshRight(LARA_GUN_TYPE weapon_type); ================================================ FILE: src/trx/game/gun/rifle.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_SHOTGUN_PELLET_SCATTER (DEG_1 * 20) // = 3640 #define M_HARPOON_BOLT_SPEED_TR12 150 #define M_HARPOON_BOLT_SPEED_TR3 256 #define M_GRENADE_SPEED 200 typedef enum { LA_G_AIM = 0, LA_G_DRAW = 1, LA_G_RECOIL = 2, LA_G_UNDRAW = 3, LA_G_UNAIM = 4, LA_G_RELOAD = 5, LA_G_UAIM = 6, LA_G_UUNAIM = 7, LA_G_URECOIL = 8, LA_G_SURF_UNDRAW = 9, } M_ANIM; static bool m_M16Firing = false; static bool m_ReloadHarpoon = false; static void M_SetTR3ProjectileShade(ITEM *const item) { if (item == nullptr) { return; } // OG TR3 uses `item->shade = -0x3DF0` on projectiles; in TRX any negative // shade forces the dynamic/smoothed lighting path. item->shade.value_1 = -1; item->shade.value_2 = -1; } static M_ANIM M_GetReadyAnim(const LARA_GUN_TYPE weapon_type) { const LARA_INFO *const lara = Lara_GetLaraInfo(); switch (weapon_type) { case LGT_HARPOON: return lara->water_status == LWS_UNDERWATER ? LA_G_UAIM : LA_G_AIM; case LGT_GRENADE: return LA_G_DRAW; default: return LA_G_AIM; } } static void M_AnimateGun(ITEM *const item) { // While the item is drawn in Lara_Draw, it needs a world position for // sound effect commands in Item_Animate. const ITEM *const lara_item = Lara_GetItem(); item->pos.x = lara_item->pos.x; item->pos.y = lara_item->pos.y - LARA_HEIGHT; item->pos.z = lara_item->pos.z; Item_Animate(item); } static void M_Ready(const LARA_GUN_TYPE weapon_type) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->gun_status = LGS_READY; lara->target = nullptr; const OBJECT *const obj = Object_Get(Gun_GetWeaponAnim(weapon_type)); lara->left_arm.frame_base = obj->frame_base; lara->left_arm.frame_num = LF_G_AIM_START; lara->left_arm.lock = 0; lara->left_arm.rot.x = 0; lara->left_arm.rot.y = 0; lara->left_arm.rot.z = 0; lara->right_arm.frame_base = obj->frame_base; lara->right_arm.frame_num = LF_G_AIM_START; lara->right_arm.lock = 0; lara->right_arm.rot.x = 0; lara->right_arm.rot.y = 0; lara->right_arm.rot.z = 0; if (g_Config.gameplay.look_mode == LOOK_MODE_RESTRICTED) { lara->head_rot.x = 0; lara->head_rot.y = 0; lara->torso_rot.x = 0; lara->torso_rot.y = 0; } } static void M_FireGeneric(const LARA_GUN_TYPE weapon_type) { const ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara = Lara_GetLaraInfo(); bool fired = false; int16_t angles[2] = { lara->left_arm.rot.y + lara_item->rot.y, lara->left_arm.rot.x, }; if (g_TRVersion == 3 && !lara->left_arm.lock) { angles[0] += lara->torso_rot.y; angles[1] += lara->torso_rot.x; } const int32_t clip = Gun_GetAmmoClipCount(weapon_type); for (int32_t i = 0; i < clip; i++) { int16_t dangles[2] = { angles[0] + M_SHOTGUN_PELLET_SCATTER * (Random_GetControl() - 0x4000) / 0x10000, angles[1] + M_SHOTGUN_PELLET_SCATTER * (Random_GetControl() - 0x4000) / 0x10000, }; if (Gun_FireWeapon(weapon_type, lara->target, lara_item, dangles)) { fired = true; } } if (fired) { lara->right_arm.flash_gun = g_Weapons[weapon_type].flash_time; Gun_Smoke_OnFire(weapon_type, true); Sound_Effect( g_Weapons[weapon_type].sample_num, &lara_item->pos, SPM_NORMAL); } } static void M_FireM16(const bool running, const LARA_GUN_TYPE weapon_type) { const ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara = Lara_GetLaraInfo(); int16_t angles[2] = { lara->left_arm.rot.y + lara_item->rot.y, lara->left_arm.rot.x, }; if (g_TRVersion == 3 && !lara->left_arm.lock) { angles[0] += lara->torso_rot.y; angles[1] += lara->torso_rot.x; } if (g_Config.gameplay.fix_m16_accuracy && running) { g_Weapons[weapon_type].shot_accuracy = DEG_1 * 12; g_Weapons[weapon_type].damage = 1; } else { g_Weapons[weapon_type].shot_accuracy = DEG_1 * 4; g_Weapons[weapon_type].damage = 3; } if (Gun_FireWeapon(weapon_type, lara->target, lara_item, angles)) { lara->right_arm.flash_gun = g_Weapons[weapon_type].flash_time; Spawn_GunShell(weapon_type, true); Gun_Smoke_OnFire(weapon_type, true); } } static void M_FireHarpoon(void) { const ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->harpoon_ammo.ammo <= 0) { goto finish; } const int16_t item_num = Item_Create(); if (item_num == NO_ITEM) { goto finish; } const WEAPON_INFO *const weapon = &g_Weapons[LGT_HARPOON]; const GAME_VECTOR origin = { .pos = { .x = lara_item->pos.x, .y = lara_item->pos.y - weapon->gun_height, .z = lara_item->pos.z, }, .room_num = lara_item->room_num, }; ITEM *const projectile_item = Item_Get(item_num); projectile_item->object_id = O_HARPOON_BOLT; projectile_item->room_num = lara_item->room_num; XYZ_32 offset = { .x = -2, .y = 373, .z = 77, }; Lara_GetJointAbsPosition(&offset, LM_HAND_R); projectile_item->pos = offset; projectile_item->interp.prev.pos = projectile_item->pos; Item_Initialise(item_num); if (lara->target != nullptr) { GAME_VECTOR lara_vec; Gun_FindTargetPoint(lara->target, &lara_vec); const int32_t dx = lara_vec.pos.x - projectile_item->pos.x; const int32_t dz = lara_vec.pos.z - projectile_item->pos.z; const int32_t dy = lara_vec.pos.y - projectile_item->pos.y; const int32_t dxz = Math_Sqrt(SQUARE(dx) + SQUARE(dz)); projectile_item->rot.y = Math_Atan(dz, dx); projectile_item->rot.x = -Math_Atan(dxz, dy); projectile_item->rot.z = 0; } else { if (g_TRVersion == 3) { projectile_item->rot.x = lara->torso_rot.x + lara_item->rot.x; projectile_item->rot.y = lara->torso_rot.y + lara_item->rot.y; } else { projectile_item->rot.x = lara->left_arm.rot.x + lara_item->rot.x; projectile_item->rot.y = lara->left_arm.rot.y + lara_item->rot.y; } projectile_item->rot.z = 0; } const int32_t bolt_speed = g_TRVersion == 3 ? M_HARPOON_BOLT_SPEED_TR3 : M_HARPOON_BOLT_SPEED_TR12; projectile_item->fall_speed = (-bolt_speed * Math_Sin(projectile_item->rot.x)) >> W2V_SHIFT; projectile_item->speed = (bolt_speed * Math_Cos(projectile_item->rot.x)) >> W2V_SHIFT; if (g_TRVersion == 3) { M_SetTR3ProjectileShade(projectile_item); projectile_item->hit_points = 256; } Item_AddActive(item_num); projectile_item->status = IS_ACTIVE; Gun_SmashItems( origin, (GAME_VECTOR) { .pos = projectile_item->pos, .room_num = projectile_item->room_num, }, nullptr, projectile_item->object_id); lara->harpoon_ammo.ammo--; Stats_AddAmmoUsed(); finish: const int32_t recoil = g_Config.gameplay.harpoon_recoil; const bool is_ngplus = Game_IsBonusFlagSet(GBF_NGPLUS); if (recoil <= 0) { if (is_ngplus) { lara->harpoon_ammo.ammo++; } } else if ((lara->harpoon_ammo.ammo % recoil) == 0) { if (is_ngplus) { lara->harpoon_ammo.ammo += recoil; } m_ReloadHarpoon = lara->harpoon_ammo.ammo > 0; } } static void M_FireGrenade(void) { LARA_INFO *const lara = Lara_GetLaraInfo(); const ITEM *const lara_item = Lara_GetItem(); if (lara->grenade_ammo.ammo <= 0) { return; } const WEAPON_INFO *const weapon = &g_Weapons[LGT_GRENADE]; const GAME_VECTOR origin = { .pos = { .x = lara_item->pos.x, .y = lara_item->pos.y - weapon->gun_height, .z = lara_item->pos.z, }, .room_num = lara_item->room_num, }; const int16_t item_num = Item_Create(); if (item_num == NO_ITEM) { return; } ITEM *const projectile_item = Item_Get(item_num); projectile_item->object_id = O_GRENADE; projectile_item->room_num = lara_item->room_num; XYZ_32 offset = g_TRVersion == 3 ? (XYZ_32) { 0, 276, 80 } : (XYZ_32) { -2, 373, 77 }; Lara_GetJointAbsPosition(&offset, LM_HAND_R); projectile_item->pos = offset; projectile_item->interp.prev.pos = projectile_item->pos; Item_Initialise(item_num); int16_t room_num = projectile_item->room_num; const SECTOR *const sector = Room_GetSector(origin.pos, &room_num); const int32_t height = Room_GetHeight(sector, origin.pos); if (height < origin.pos.y) { projectile_item->pos = (XYZ_32) { .x = lara_item->pos.x, .y = origin.pos.y, .z = lara_item->pos.z, }; } Room_GetSector(projectile_item->pos, &room_num); Item_UpdateRoom(item_num, room_num); projectile_item->rot.x = lara->left_arm.rot.x + lara_item->rot.x; projectile_item->rot.y = lara->left_arm.rot.y + lara_item->rot.y; projectile_item->rot.z = 0; if (g_TRVersion == 3 && !lara->left_arm.lock) { projectile_item->rot.x += lara->torso_rot.x; projectile_item->rot.y += lara->torso_rot.y; } if (g_Config.gameplay.enable_bouncy_grenades) { // TR3 grenades use a timed fuse and bounce/roll physics, so use speed // as horizontal velocity magnitude and fall_speed as vertical velocity. projectile_item->speed = 128; projectile_item->fall_speed = -(projectile_item->speed * Math_Sin(projectile_item->rot.x)) >> W2V_SHIFT; projectile_item->current_anim_state = projectile_item->rot.x; projectile_item->goal_anim_state = projectile_item->rot.y; projectile_item->required_anim_state = 0; projectile_item->hit_points = 120; } else { projectile_item->speed = M_GRENADE_SPEED; projectile_item->fall_speed = 0; } Item_AddActive(item_num); projectile_item->status = IS_ACTIVE; Gun_SmashItems( origin, (GAME_VECTOR) { .pos = projectile_item->pos, .room_num = projectile_item->room_num, }, nullptr, projectile_item->object_id); if (!Game_IsBonusFlagSet(GBF_NGPLUS)) { lara->grenade_ammo.ammo--; } Stats_AddAmmoUsed(); Gun_Smoke_OnFire(LGT_GRENADE, true); } static void M_FireRocket(void) { LARA_INFO *const lara = Lara_GetLaraInfo(); const ITEM *const lara_item = Lara_GetItem(); if (lara->rocket_ammo.ammo <= 0) { return; } const WEAPON_INFO *const weapon = &g_Weapons[LGT_ROCKET]; const GAME_VECTOR origin = { .pos = { .x = lara_item->pos.x, .y = lara_item->pos.y - weapon->gun_height, .z = lara_item->pos.z, }, .room_num = lara_item->room_num, }; const int16_t item_num = Item_Create(); if (item_num == NO_ITEM) { return; } ITEM *const projectile_item = Item_Get(item_num); projectile_item->object_id = O_ROCKET; projectile_item->room_num = lara_item->room_num; XYZ_32 offset = { .x = 0, .y = 180, .z = 72, }; Lara_GetJointAbsPosition(&offset, LM_HAND_R); projectile_item->pos = offset; projectile_item->interp.prev.pos = projectile_item->pos; Item_Initialise(item_num); projectile_item->rot.x = lara->left_arm.rot.x + lara_item->rot.x; projectile_item->rot.y = lara->left_arm.rot.y + lara_item->rot.y; projectile_item->rot.z = 0; if (!lara->left_arm.lock) { projectile_item->rot.x += lara->torso_rot.x; projectile_item->rot.y += lara->torso_rot.y; } projectile_item->speed = 16; Item_AddActive(item_num); projectile_item->status = IS_ACTIVE; Gun_SmashItems( origin, (GAME_VECTOR) { .pos = projectile_item->pos, .room_num = projectile_item->room_num, }, nullptr, projectile_item->object_id); if (!Game_IsBonusFlagSet(GBF_NGPLUS)) { lara->rocket_ammo.ammo--; } Stats_AddAmmoUsed(); if (g_TRVersion >= 3) { Sound_Effect(SFX_EXPLOSION_1, &lara_item->pos, 0x5000000 | SPM_PITCH); } Gun_Smoke_OnFire(LGT_ROCKET, true); if (g_TRVersion == 3) { M_SetTR3ProjectileShade(projectile_item); const XYZ_32 back_128 = XYZ_32_FromYawPitch( projectile_item->rot.y, projectile_item->rot.x, -128); for (int32_t i = 0; i < 8; i++) { const int32_t dist = -(Random_GetControl() & 0x7FF); const XYZ_32 back_vel = XYZ_32_FromYawPitch( projectile_item->rot.y, projectile_item->rot.x, dist); Sparks_TriggerRocketFlame( back_128, (XYZ_32) { .x = back_vel.x - back_128.x, .y = back_vel.y - back_128.y, .z = back_vel.z - back_128.z, }, item_num, projectile_item->room_num); } } } static void M_Fire(const LARA_GUN_TYPE weapon_type, const bool running) { switch (weapon_type) { case LGT_HARPOON: M_FireHarpoon(); break; case LGT_GRENADE: if (!running) { M_FireGrenade(); } break; case LGT_ROCKET: M_FireRocket(); break; case LGT_M16: case LGT_MP5: M_FireM16(running, weapon_type); break; default: if (!running) { M_FireGeneric(weapon_type); } break; } } static void M_PlayMachineGunSound( const LARA_GUN_TYPE weapon_type, const bool stopping) { const ITEM *const lara_item = Lara_GetItem(); if (weapon_type == LGT_M16) { Sound_Effect( stopping ? SFX_M16_STOP : SFX_M16_FIRE, &lara_item->pos, SPM_NORMAL); return; } // The MP5 uses a high-pitched explosion when either firing or stopping. // This is intentionally omitted in TR1/2 due to the sample's quality when // played in rapid succession. if (g_TRVersion >= 3) { Sound_Effect(SFX_EXPLOSION_1, &lara_item->pos, 0x5000000 | SPM_PITCH); } if (!stopping) { Sound_Effect(SFX_MP5_FIRE, &lara_item->pos, SPM_NORMAL); } } static void M_Animate(const LARA_GUN_TYPE weapon_type) { const ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara = Lara_GetLaraInfo(); const bool running = (weapon_type == LGT_M16 || weapon_type == LGT_MP5) && lara_item->speed != 0; ITEM *const item = Item_Get(lara->gun_item_num); switch (item->current_anim_state) { case LA_G_AIM: m_M16Firing = false; if (m_ReloadHarpoon) { item->goal_anim_state = LA_G_RELOAD; m_ReloadHarpoon = false; } else if (lara->water_status == LWS_UNDERWATER || running) { item->goal_anim_state = LA_G_UAIM; } else if ( (g_Input.action && lara->target == nullptr) || lara->left_arm.lock) { item->goal_anim_state = LA_G_RECOIL; } else { item->goal_anim_state = LA_G_UNAIM; } break; case LA_G_UAIM: m_M16Firing = false; if (m_ReloadHarpoon) { item->goal_anim_state = LA_G_RELOAD; m_ReloadHarpoon = false; } else if (lara->water_status != LWS_UNDERWATER && !running) { item->goal_anim_state = LA_G_AIM; } else if ( (g_Input.action && lara->target == nullptr) || lara->left_arm.lock) { item->goal_anim_state = LA_G_URECOIL; } else { item->goal_anim_state = LA_G_UUNAIM; } break; case LA_G_RECOIL: if (Item_TestFrameEqual(item, 0)) { item->goal_anim_state = LA_G_UNAIM; if (lara->water_status != LWS_UNDERWATER && !running && !m_ReloadHarpoon) { if (g_Input.action) { if (lara->target == nullptr || lara->left_arm.lock) { M_Fire(weapon_type, false); if (weapon_type == LGT_M16 || weapon_type == LGT_MP5) { M_PlayMachineGunSound(weapon_type, false); m_M16Firing = true; } item->goal_anim_state = LA_G_RECOIL; } } else if (lara->left_arm.lock) { item->goal_anim_state = LA_G_AIM; } } if (item->goal_anim_state != LA_G_RECOIL && m_M16Firing) { M_PlayMachineGunSound(weapon_type, true); m_M16Firing = false; } } else if (m_M16Firing) { M_PlayMachineGunSound(weapon_type, false); } else if ( weapon_type == LGT_SHOTGUN && !g_Input.action && !lara->left_arm.lock) { item->goal_anim_state = LA_G_UNAIM; } if (weapon_type == LGT_SHOTGUN && Item_TestFrameEqual(item, 12)) { Spawn_GunShell(LGT_SHOTGUN, true); } break; case LA_G_URECOIL: if (Item_TestFrameEqual(item, 0)) { item->goal_anim_state = LA_G_UUNAIM; if ((lara->water_status == LWS_UNDERWATER || running) && !m_ReloadHarpoon) { if (g_Input.action) { if (lara->target == nullptr || lara->left_arm.lock) { M_Fire(weapon_type, true); item->goal_anim_state = LA_G_URECOIL; } } else if (lara->left_arm.lock) { item->goal_anim_state = LA_G_UAIM; } } } if (item->goal_anim_state == LA_G_URECOIL && (weapon_type == LGT_M16 || weapon_type == LGT_MP5)) { M_PlayMachineGunSound(weapon_type, false); } break; default: break; } M_AnimateGun(item); lara->left_arm.anim_num = item->anim_num; lara->left_arm.frame_base = Item_GetAnim(item)->frame_ptr; lara->left_arm.frame_num = Item_GetRelativeFrame(item); lara->right_arm.anim_num = item->anim_num; lara->right_arm.frame_base = Item_GetAnim(item)->frame_ptr; lara->right_arm.frame_num = Item_GetRelativeFrame(item); } void Gun_Rifle_Control(const LARA_GUN_TYPE weapon_type) { const WEAPON_INFO *const weapon = &g_Weapons[weapon_type]; LARA_INFO *const lara = Lara_GetLaraInfo(); Gun_GetNewTarget(weapon); if (g_InputDB.change_target && g_Config.gameplay.enable_target_change) { Gun_ChangeTarget(weapon); } Gun_AimWeapon(weapon, &lara->left_arm); if (lara->left_arm.lock) { lara->torso_rot.x = lara->left_arm.rot.x; lara->torso_rot.y = lara->left_arm.rot.y; if (g_Config.gameplay.look_mode != LOOK_MODE_UNRESTRICTED || g_Camera.type != CAM_LOOK) { lara->head_rot.x = 0; lara->head_rot.y = 0; } } M_Animate(weapon_type); if (lara->right_arm.flash_gun && (weapon_type == LGT_SHOTGUN || weapon_type == LGT_M16 || weapon_type == LGT_MP5)) { Gun_AddDynamicLight(); } } void Gun_Rifle_Draw(const LARA_GUN_TYPE weapon_type) { ITEM *item; LARA_INFO *const lara = Lara_GetLaraInfo(); const WEAPON_INFO *const weapon = &g_Weapons[weapon_type]; if (lara->gun_item_num != NO_ITEM) { item = Item_Get(lara->gun_item_num); } else { lara->gun_item_num = Item_Create(); item = Item_Get(lara->gun_item_num); item->object_id = Gun_GetWeaponAnim(weapon_type); Item_SwitchToAnim(item, weapon->equip_anim_idx, 0); item->goal_anim_state = LA_G_DRAW; item->current_anim_state = LA_G_DRAW; item->status = IS_ACTIVE; item->room_num = NO_ROOM; const OBJECT *const obj = Object_Get(item->object_id); lara->right_arm.frame_base = obj->frame_base; lara->left_arm.frame_base = obj->frame_base; } M_AnimateGun(item); if (item->current_anim_state == LA_G_AIM || item->current_anim_state == LA_G_UAIM) { M_Ready(weapon_type); } else if (Item_TestFrameEqual(item, weapon->draw_frame)) { Gun_Rifle_DrawMeshes(weapon_type); } else if (lara->water_status == LWS_UNDERWATER) { item->goal_anim_state = LA_G_UAIM; } lara->left_arm.anim_num = item->anim_num; lara->left_arm.frame_base = Item_GetAnim(item)->frame_ptr; lara->left_arm.frame_num = Item_GetRelativeFrame(item); lara->right_arm.anim_num = item->anim_num; lara->right_arm.frame_base = Item_GetAnim(item)->frame_ptr; lara->right_arm.frame_num = Item_GetRelativeFrame(item); } void Gun_Rifle_Undraw(const LARA_GUN_TYPE weapon_type) { LARA_INFO *const lara = Lara_GetLaraInfo(); ITEM *const item = Item_Get(lara->gun_item_num); const ANIM *const anim = Item_GetAnim(item); if (lara->water_status == LWS_SURFACE && Anim_HasChange(anim, LA_G_SURF_UNDRAW)) { item->goal_anim_state = LA_G_SURF_UNDRAW; } else { item->goal_anim_state = LA_G_UNDRAW; } M_AnimateGun(item); const WEAPON_INFO *const weapon = &g_Weapons[weapon_type]; if (item->status == IS_DEACTIVATED) { Item_Kill(lara->gun_item_num); lara->gun_item_num = NO_ITEM; lara->gun_status = LGS_ARMLESS; lara->target = nullptr; lara->left_arm.frame_num = 0; lara->left_arm.lock = 0; lara->right_arm.frame_num = 0; lara->right_arm.lock = 0; } else if ( item->current_anim_state == LA_G_UNDRAW && Item_TestFrameEqual(item, weapon->undraw_frame)) { Gun_Rifle_UndrawMeshes(weapon_type); } if (!g_Input.look || g_Config.gameplay.look_mode == LOOK_MODE_RESTRICTED) { lara->head_rot.x = 0; lara->head_rot.y = 0; lara->torso_rot.x += lara->torso_rot.x / -2; lara->torso_rot.y += lara->torso_rot.y / -2; } lara->left_arm.anim_num = item->anim_num; lara->left_arm.frame_base = Item_GetAnim(item)->frame_ptr; lara->left_arm.frame_num = Item_GetRelativeFrame(item); lara->right_arm.anim_num = item->anim_num; lara->right_arm.frame_base = Item_GetAnim(item)->frame_ptr; lara->right_arm.frame_num = Item_GetRelativeFrame(item); } void Gun_Rifle_DrawMeshes(const LARA_GUN_TYPE weapon_type) { Gun_SetLaraHandLMesh(LGT_UNARMED); Gun_SetLaraHandRMesh(weapon_type); Gun_SetLaraBackMesh(LGT_UNARMED); } void Gun_Rifle_UndrawMeshes(const LARA_GUN_TYPE weapon_type) { Gun_SetLaraHandLMesh(LGT_UNARMED); Gun_SetLaraHandRMesh(LGT_UNARMED); Gun_SetLaraBackMesh(weapon_type); } void Gun_Rifle_EnsureReady(const LARA_GUN_TYPE weapon_type) { Gun_Rifle_Draw(weapon_type); const LARA_INFO *const lara = Lara_GetLaraInfo(); const ITEM *const item = Item_Get(lara->gun_item_num); const int16_t goal_anim = M_GetReadyAnim(weapon_type); do { Gun_Rifle_Draw(weapon_type); } while (Item_GetRelativeAnim(item) != goal_anim); } ================================================ FILE: src/trx/game/gun/rifle.h ================================================ #pragma once #include void Gun_Rifle_Control(LARA_GUN_TYPE weapon_type); void Gun_Rifle_Draw(LARA_GUN_TYPE weapon_type); void Gun_Rifle_Undraw(LARA_GUN_TYPE weapon_type); void Gun_Rifle_DrawMeshes(LARA_GUN_TYPE weapon_type); void Gun_Rifle_UndrawMeshes(LARA_GUN_TYPE weapon_type); void Gun_Rifle_EnsureReady(LARA_GUN_TYPE weapon_type); ================================================ FILE: src/trx/game/gun/smashing.c ================================================ #include #include #include #include // TODO: meh extern void Smashable_Smash(int16_t item_num); GUN_SMASH_POLICY Gun_GetSmashPolicy(const ITEM *const item) { if (Object_IsType(item->object_id, g_ShatterableObjects)) { return GUN_SMASH_POLICY_CONTINUE; } if (Object_IsType(item->object_id, g_SmashableObjects)) { return GUN_SMASH_POLICY_STOP; } if (Object_IsType(item->object_id, g_HeavyShatterableObjects)) { return GUN_SMASH_POLICY_HEAVY; } return GUN_SMASH_POLICY_NONE; } void Gun_SmashItem(const int16_t item_num) { ITEM *const item = Item_Get(item_num); switch (item->object_id) { case O_SMASH_OBJECT_1: case O_SMASH_OBJECT_4: Smashable_Smash(item_num); break; case O_BELL: case O_CARCASS: case O_FUSE_BOX: case O_SMASH_OBJECT_2: case O_SMASH_OBJECT_3: if (item->status != IS_ACTIVE) { item->status = IS_ACTIVE; Item_AddActive(item_num); } break; case O_SCION_ITEM_3: Gun_HitTarget(item, nullptr, nullptr, item->hit_points); break; default: break; } } PROJECTILE_HIT Gun_SmashItems( const GAME_VECTOR start, const GAME_VECTOR target, XYZ_32 *const out_hit_pos, const OBJECT_ID missile_obj_id) { int32_t hits = 0; int16_t last_item_num = NO_ITEM; const bool is_heavy_missile = Object_IsType(missile_obj_id, g_HeavyMissileObjects); while (true) { const int16_t item_num = LOS_CheckSmashable(start, target, out_hit_pos); if (item_num == NO_ITEM || item_num == last_item_num) { break; } last_item_num = item_num; const ITEM *const item = Item_Get(item_num); const GUN_SMASH_POLICY policy = Gun_GetSmashPolicy(item); switch (policy) { case GUN_SMASH_POLICY_HEAVY: if (is_heavy_missile) { Gun_SmashItem(item_num); } return PROJECTILE_HIT_STOP; case GUN_SMASH_POLICY_STOP: Gun_SmashItem(item_num); return PROJECTILE_HIT_STOP; case GUN_SMASH_POLICY_CONTINUE: Gun_SmashItem(item_num); hits++; break; default: break; } } return hits > 0 ? PROJECTILE_HIT_SHATTER : PROJECTILE_HIT_NONE; } ================================================ FILE: src/trx/game/gun/smashing.h ================================================ #pragma once #include typedef enum { PROJECTILE_HIT_NONE, PROJECTILE_HIT_SHATTER, // some objects were shattered PROJECTILE_HIT_STOP, // a hard object (eg a bell) has been hit } PROJECTILE_HIT; typedef enum { GUN_SMASH_POLICY_NONE, GUN_SMASH_POLICY_CONTINUE, GUN_SMASH_POLICY_STOP, GUN_SMASH_POLICY_HEAVY, } GUN_SMASH_POLICY; GUN_SMASH_POLICY Gun_GetSmashPolicy(const ITEM *item); void Gun_SmashItem(int16_t item_num); PROJECTILE_HIT Gun_SmashItems( GAME_VECTOR start, GAME_VECTOR target, XYZ_32 *out_hit_pos, OBJECT_ID missile_obj_id); ================================================ FILE: src/trx/game/gun/smoke.c ================================================ #include #include #include #include #include #include #include #include static XYZ_32 M_GetHandAbsPosition(const LARA_MESH hand, XYZ_32 offset) { Lara_GetMeshPos(hand, &offset); return offset; } static XYZ_32 M_GetMuzzleOffset( const LARA_GUN_TYPE weapon_type, const bool is_right_hand) { return is_right_hand ? g_Weapons[weapon_type].muzzle_pos : g_Weapons[weapon_type].muzzle_pos_alt; } void Gun_Smoke_OnFire(const LARA_GUN_TYPE weapon_type, const bool is_right_hand) { if (g_TRVersion != 3) { return; } LARA_INFO *const lara = Lara_GetLaraInfo(); const int32_t count = g_Weapons[weapon_type].smoke_count; if (count == 0) { return; } lara->tr3_smoke_weapon = weapon_type; if (is_right_hand) { lara->tr3_smoke_count_r = count; } else { lara->tr3_smoke_count_l = count; } const LARA_MESH hand = is_right_hand ? LM_HAND_R : LM_HAND_L; const XYZ_32 muzzle_pos = weapon_type == LGT_SHOTGUN && is_right_hand ? M_GetHandAbsPosition(hand, (XYZ_32) { .x = 0, .y = 228, .z = 32 }) : M_GetHandAbsPosition( hand, M_GetMuzzleOffset(weapon_type, is_right_hand)); GAME_VECTOR pos = { .pos = muzzle_pos, .room_num = Lara_GetItem()->room_num }; Room_GetSector(pos.pos, &pos.room_num); if (weapon_type == LGT_SHOTGUN && is_right_hand) { const XYZ_32 muzzle_tip_pos = M_GetHandAbsPosition(hand, (XYZ_32) { .x = 0, .y = 1508, .z = 32 }); const XYZ_32 vel = { .x = muzzle_tip_pos.x - muzzle_pos.x, .y = muzzle_tip_pos.y - muzzle_pos.y, .z = muzzle_tip_pos.z - muzzle_pos.z, }; for (int32_t i = 0; i < 7; i++) { Sparks_TriggerGunSmokeDirected(pos, vel, true, weapon_type, count); } const XYZ_32 vel_sparks = { .x = (muzzle_tip_pos.x - muzzle_pos.x) << 1, .y = (muzzle_tip_pos.y - muzzle_pos.y) << 1, .z = (muzzle_tip_pos.z - muzzle_pos.z) << 1, }; for (int32_t i = 0; i < 12; i++) { Sparks_TriggerShotgunSparks(muzzle_pos, vel_sparks); } } else { Sparks_TriggerGunSmoke(pos, true, weapon_type, count); } } void Gun_Smoke_Control(void) { if (g_TRVersion != 3) { return; } LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->tr3_smoke_count_l == 0 && lara->tr3_smoke_count_r == 0) { return; } const LARA_GUN_TYPE weapon_type = lara->tr3_smoke_weapon; if (lara->tr3_smoke_count_l > 0) { const XYZ_32 muzzle_pos = M_GetHandAbsPosition( LM_HAND_L, M_GetMuzzleOffset(weapon_type, false)); GAME_VECTOR pos = { .pos = muzzle_pos, .room_num = Lara_GetItem()->room_num }; Room_GetSector(pos.pos, &pos.room_num); Sparks_TriggerGunSmoke( pos, false, weapon_type, lara->tr3_smoke_count_l); lara->tr3_smoke_count_l--; } if (lara->tr3_smoke_count_r > 0) { const XYZ_32 muzzle_pos = M_GetHandAbsPosition( LM_HAND_R, M_GetMuzzleOffset(weapon_type, true)); GAME_VECTOR pos = { .pos = muzzle_pos, .room_num = Lara_GetItem()->room_num }; Room_GetSector(pos.pos, &pos.room_num); Sparks_TriggerGunSmoke( pos, false, weapon_type, lara->tr3_smoke_count_r); lara->tr3_smoke_count_r--; } } ================================================ FILE: src/trx/game/gun/smoke.h ================================================ #pragma once #include void Gun_Smoke_Control(void); void Gun_Smoke_OnFire(LARA_GUN_TYPE weapon_type, bool is_right_hand); ================================================ FILE: src/trx/game/gun/types.h ================================================ #pragma once #include #include #include typedef enum { WEAPON_TYPE_DUAL_PISTOLS, WEAPON_TYPE_SINGLE_PISTOL, WEAPON_TYPE_RIFLE, WEAPON_TYPE_MOUNTED, NUM_WEAPON_TYPES, } WEAPON_TYPE; typedef struct { int32_t initial_qty; int32_t pickup_qty; int32_t pickup_qty_alt; } WEAPON_AMMO_INFO; typedef struct { WEAPON_TYPE type; int16_t lock_angles[4]; int16_t left_angles[4]; int16_t right_angles[4]; int16_t aim_speed; int16_t shot_accuracy; int32_t gun_height; int32_t damage; WEAPON_AMMO_INFO ammo; int32_t target_dist; int16_t equip_anim_idx; int16_t draw_frame; int16_t undraw_frame; int16_t recoil_frame; int16_t flash_time; int16_t flash_shade; RGB_F flash_color; XYZ_32 flash_pos; XYZ_32 flash_pos_alt; SAMPLE_TRX_ID sample_num; RGB_F glow_color; XYZ_32 glow_pos; XYZ_32 muzzle_pos; XYZ_32 muzzle_pos_alt; XYZ_32 shell_pos; XYZ_32 shell_pos_alt; int32_t smoke_count; bool is_available; } WEAPON_INFO; ================================================ FILE: src/trx/game/gun/vars.c ================================================ #include #include #include #include #include #include #include WEAPON_INFO g_Weapons[NUM_WEAPONS] = {}; static void M_ReadAngles( JSON_OBJECT *const obj, const char *const name, const char *const path, const char *const key, int16_t *const angles) { JSON_ARRAY *const arr = JSON_ObjectGetArray(obj, key); if (arr == nullptr) { return; } if (arr->length != 4) { Shell_ExitSystemFmt("invalid '%s' for '%s' in %s", key, name, path); } for (size_t i = 0; i < 4; i++) { angles[i] = JSON_ArrayGetInt(arr, i, angles[i]) * DEG_1; } } static void M_ReadRGB_F(JSON_VALUE *const value, RGB_F *const target) { JSON_OBJECT *const obj = JSON_ValueAsObject(value); if (obj != nullptr) { target->r = JSON_ObjectGetDouble(obj, "r", 0.0); target->g = JSON_ObjectGetDouble(obj, "g", 0.0); target->b = JSON_ObjectGetDouble(obj, "b", 0.0); } JSON_ARRAY *const arr = JSON_ValueAsArray(value); if (arr != nullptr && arr->length == 3) { target->r = JSON_ArrayGetDouble(arr, 0, 0.0); target->g = JSON_ArrayGetDouble(arr, 1, 0.0); target->b = JSON_ArrayGetDouble(arr, 2, 0.0); } } static void M_ReadXYZ32(JSON_VALUE *const value, XYZ_32 *const target) { JSON_OBJECT *const obj = JSON_ValueAsObject(value); if (obj != nullptr) { target->x = JSON_ObjectGetInt(obj, "x", 0); target->y = JSON_ObjectGetInt(obj, "y", 0); target->z = JSON_ObjectGetInt(obj, "z", 0); } JSON_ARRAY *const arr = JSON_ValueAsArray(value); if (arr != nullptr && arr->length == 3) { target->x = JSON_ArrayGetInt(arr, 0, 0); target->y = JSON_ArrayGetInt(arr, 1, 0); target->z = JSON_ArrayGetInt(arr, 2, 0); } } static void M_ReadAmmoInfo(JSON_OBJECT *const obj, const int32_t type) { JSON_OBJECT *const ammo_obj = JSON_ObjectGetObject(obj, "ammo"); if (ammo_obj == nullptr) { return; } g_Weapons[type].ammo.initial_qty = JSON_ObjectGetInt( ammo_obj, "initial_qty", g_Weapons[type].ammo.initial_qty); g_Weapons[type].ammo.pickup_qty = JSON_ObjectGetInt( ammo_obj, "pickup_qty", g_Weapons[type].ammo.pickup_qty); g_Weapons[type].ammo.pickup_qty_alt = JSON_ObjectGetInt( ammo_obj, "pickup_qty_alt", g_Weapons[type].ammo.pickup_qty_alt); } void Gun_LoadVars(const char *const path) { #define L_READ_ANGLE(name, target) \ target = JSON_ObjectGetInt(obj, name, target) * DEG_1; #define L_READ_DIST(name, target) \ target = JSON_ObjectGetDouble(obj, name, target / (float)WALL_L) * WALL_L; #define L_READ_INT(name, target) target = JSON_ObjectGetInt(obj, name, target) JSON_VALUE *const root = JSONFile_ReadEx(path, true); JSON_OBJECT *const root_obj = JSON_ValueAsObject(root); if (root_obj == nullptr) { Shell_ExitSystemFmt("invalid weapons vars file: %s", path); } for (JSON_OBJECT_ELEMENT *elem = root_obj->start; elem != nullptr; elem = elem->next) { const char *const name = elem->name->string; const int32_t type = ENUM_MAP_GET(LARA_GUN_TYPE, name, -1); if (type < 0 || type >= NUM_WEAPONS) { Shell_ExitSystemFmt("unknown weapon type '%s' in %s", name, path); } JSON_OBJECT *const obj = JSON_ValueAsObject(elem->value); // weapon type const char *const weapon_type = JSON_ObjectGetString(obj, "type", JSON_INVALID_STRING); if (weapon_type != JSON_INVALID_STRING && weapon_type[0] != '\0') { const int32_t weapon_type_val = ENUM_MAP_GET(WEAPON_TYPE, weapon_type, -1); if (weapon_type_val < 0 || weapon_type_val >= NUM_WEAPON_TYPES) { Shell_ExitSystemFmt( "unknown weapon type '%s' in %s", weapon_type, path); } else { g_Weapons[type].type = weapon_type_val; } } // angles M_ReadAngles( obj, name, path, "lock_angles", g_Weapons[type].lock_angles); M_ReadAngles( obj, name, path, "left_angles", g_Weapons[type].left_angles); M_ReadAngles( obj, name, path, "right_angles", g_Weapons[type].right_angles); // scalar properties L_READ_ANGLE("aim_speed", g_Weapons[type].aim_speed); L_READ_ANGLE("shot_accuracy", g_Weapons[type].shot_accuracy); L_READ_INT("gun_height", g_Weapons[type].gun_height); L_READ_INT("damage", g_Weapons[type].damage); L_READ_DIST("target_dist", g_Weapons[type].target_dist); L_READ_INT("equip_anim_idx", g_Weapons[type].equip_anim_idx); L_READ_INT("draw_frame", g_Weapons[type].draw_frame); L_READ_INT("undraw_frame", g_Weapons[type].undraw_frame); L_READ_INT("recoil_frame", g_Weapons[type].recoil_frame); L_READ_INT("flash_time", g_Weapons[type].flash_time); L_READ_INT("flash_shade", g_Weapons[type].flash_shade); L_READ_INT("smoke_count", g_Weapons[type].smoke_count); M_ReadXYZ32( JSON_ObjectGetValue(obj, "flash_pos"), &g_Weapons[type].flash_pos); M_ReadXYZ32( JSON_ObjectGetValue(obj, "flash_pos_alt"), &g_Weapons[type].flash_pos_alt); M_ReadRGB_F( JSON_ObjectGetValue(obj, "flash_color"), &g_Weapons[type].flash_color); M_ReadXYZ32( JSON_ObjectGetValue(obj, "glow_pos"), &g_Weapons[type].glow_pos); M_ReadRGB_F( JSON_ObjectGetValue(obj, "glow_color"), &g_Weapons[type].glow_color); M_ReadXYZ32( JSON_ObjectGetValue(obj, "muzzle_pos"), &g_Weapons[type].muzzle_pos); M_ReadXYZ32( JSON_ObjectGetValue(obj, "muzzle_pos_alt"), &g_Weapons[type].muzzle_pos_alt); M_ReadXYZ32( JSON_ObjectGetValue(obj, "shell_pos"), &g_Weapons[type].shell_pos); M_ReadXYZ32( JSON_ObjectGetValue(obj, "shell_pos_alt"), &g_Weapons[type].shell_pos_alt); M_ReadAmmoInfo(obj, type); // sample_num const char *const sample = JSON_ObjectGetString(obj, "sample_num", JSON_INVALID_STRING); if (sample != JSON_INVALID_STRING && sample[0] != '\0') { CATALOG_ID sample_id; if (!Catalog_NameToEnum(CATALOG_SAMPLES, sample, &sample_id)) { LOG_WARNING( "unknown sample '%s' for '%s' in %s", sample, name, path); } else { g_Weapons[type].sample_num = sample_id; } } g_Weapons[type].is_available = JSON_ObjectGetBool(obj, "is_available", true); } JSON_ValueFree(root); #undef L_READ_ANGLE #undef L_READ_DIST #undef L_READ_INT } ================================================ FILE: src/trx/game/gun/vars.h ================================================ #pragma once #include #include extern WEAPON_INFO g_Weapons[NUM_WEAPONS]; void Gun_LoadVars(const char *path); ================================================ FILE: src/trx/game/gun.h ================================================ #pragma once #include #include #include #include #include #include #include ================================================ FILE: src/trx/game/gym.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_NO_TIME (-1) #define M_MAX_ASSAULT_TIME_FRAMES (60 * 60 * LOGIC_FPS - 3) // 59:59 typedef struct { int32_t is_inventory_open_enabled; int16_t completion_timer; GYM_TRACK_TYPE active_track_type; struct { bool timer_display; bool timer_active; int32_t penalty_display_timer; int32_t penalty_frames; int32_t target_penalty_frames; int32_t timer_auto_hide_timer; bool pad_touched_this_frame; bool pad_lock; } assault_course; struct { bool timer_display; bool timer_active; int32_t lap_time; int32_t lap_time_display_timer; } quad_course; } M_PRIV; static M_PRIV m_Priv = { .is_inventory_open_enabled = -1, }; static int32_t M_CountAssaultTargets(void) { int32_t remaining = 0; for (int16_t item_num = 0; item_num < Item_GetLevelCount(); item_num++) { ITEM *const item = Item_Get(item_num); if (item->object_id != O_ASSAULT_TARGET) { continue; } if ((item->flags & IF_KILLED) == 0 && item->timer > GYM_ASSAULT_TARGET_TIME) { remaining++; } } LOG_INFO("remaining=%d", remaining); return remaining; } static void M_ResetAssaultTargets(void) { M_PRIV *const p = &m_Priv; const OBJECT *const obj = Object_Get(O_ASSAULT_TARGET); if (!obj->loaded) { return; } for (int16_t item_num = 0; item_num < Item_GetLevelCount(); item_num++) { ITEM *const item = Item_Get(item_num); if (item->object_id != O_ASSAULT_TARGET) { continue; } if ((item->flags & IF_KILLED) != 0) { // Rockets can kill targets via Creature_Die()+Item_Kill(), which // removes them from room draw + item lists. Use Item_Initialise() // to re-link them back. // Clear the flags so that Item_Initialise doesn't mark the item // as active preemptively. item->flags = 0; Item_Initialise(item_num); } else if ( item->status != IS_INACTIVE && obj->initialise_func != nullptr) { obj->initialise_func(item_num); } } } static int32_t M_GetBestTime(void) { const GYM_TRACK_STATS *const assault = &g_Config.profile.assault_stats; return assault->total_attempts > 0 ? (int32_t)assault->entries[0].time : M_NO_TIME; } static bool M_StoreCourseTime(GYM_TRACK_STATS *const stats, const uint32_t time) { int32_t insert_idx = -1; const int32_t limit = MAX_ASSAULT_TIMES; for (int32_t i = 0; i < limit; i++) { if (stats->entries[i].time == 0 || time < stats->entries[i].time) { insert_idx = i; break; } } if (insert_idx == -1) { return false; } for (int32_t i = limit - 1; i > insert_idx; i--) { stats->entries[i] = stats->entries[i - 1]; } stats->total_attempts++; stats->entries[insert_idx].time = time; stats->entries[insert_idx].attempt_num = stats->total_attempts; Config_Update(); return true; } static bool M_IsOnQuadBike(void) { const ITEM *const vehicle = Lara_Vehicle_GetItem(); return vehicle != nullptr && vehicle->object_id == O_QUAD_BIKE; } void Gym_SetInventoryOpenEnabled(const bool enabled) { M_PRIV *const p = &m_Priv; p->is_inventory_open_enabled = enabled; } bool Gym_IsInventoryOpenEnabled(void) { M_PRIV *const p = &m_Priv; if (p->is_inventory_open_enabled == -1) { p->is_inventory_open_enabled = g_TRVersion >= 2; } return p->is_inventory_open_enabled; } void Gym_Control(void) { if (g_TRVersion < 3) { return; } M_PRIV *const p = &m_Priv; if (p->assault_course.pad_lock && !p->assault_course.pad_touched_this_frame) { p->assault_course.pad_lock = false; } p->assault_course.pad_touched_this_frame = false; if (p->assault_course.penalty_display_timer > 0) { p->assault_course.penalty_display_timer--; } if (!p->assault_course.timer_active && p->assault_course.timer_display && p->assault_course.timer_auto_hide_timer > 0) { p->assault_course.timer_auto_hide_timer--; if (p->assault_course.timer_auto_hide_timer == 0) { p->assault_course.timer_display = false; p->active_track_type = GYM_TRACK_NONE; } } if (p->quad_course.lap_time_display_timer > 0) { p->quad_course.lap_time_display_timer--; } } static void M_Assault_Finish(void) { M_PRIV *const p = &m_Priv; if (!p->assault_course.timer_active) { return; } RESUME_INFO *const resume = Savegame_GetCurrentInfo(Game_GetCurrentLevel()); uint32_t final_time = resume->stats.timer; if (g_TRVersion >= 3) { const int32_t targets_remaining = M_CountAssaultTargets(); p->assault_course.penalty_display_timer = 10 * LOGIC_FPS; p->assault_course.target_penalty_frames = 10 * LOGIC_FPS * targets_remaining; CLAMPG( p->assault_course.target_penalty_frames, M_MAX_ASSAULT_TIME_FRAMES); final_time += (uint32_t)p->assault_course.penalty_frames + (uint32_t)p->assault_course.target_penalty_frames; CLAMPG(final_time, M_MAX_ASSAULT_TIME_FRAMES); resume->stats.timer = final_time; } if (g_TRVersion >= 3) { if (final_time < (uint32_t)(180 * LOGIC_FPS)) { Music_Play(MX_TR3_GYM_HINT_FAST_TIME, MPM_ONCE); } p->assault_course.timer_auto_hide_timer = 15 * LOGIC_FPS; } else { const int32_t current_best_time = M_GetBestTime(); if (current_best_time <= 0) { if (final_time < (uint32_t)(100 * LOGIC_FPS)) { // "Gosh! That was my best time yet!" Music_Play(MX_TR2_GYM_HINT_15, MPM_ONCE); } else { // "Congratulations! You did it! But perhaps I could've been // faster." Music_Play(MX_TR2_GYM_HINT_17, MPM_ONCE); } } else if (final_time < (uint32_t)current_best_time) { // "Gosh! That was my best time yet!" Music_Play(MX_TR2_GYM_HINT_15, MPM_ONCE); } else if (final_time < (uint32_t)current_best_time + 5 * LOGIC_FPS) { // "Almost. Perhaps another try and I might beat it." Music_Play(MX_TR2_GYM_HINT_16, MPM_ONCE); } else { // "Great. But nowhere near my best time." Music_Play(MX_TR2_GYM_HINT_14, MPM_ONCE); } } p->assault_course.timer_active = false; M_StoreCourseTime(&g_Config.profile.assault_stats, final_time); } static void M_Racetrack_Finish(void) { if (!M_IsOnQuadBike()) { return; } M_PRIV *const p = &m_Priv; if (!p->quad_course.timer_active) { return; } RESUME_INFO *const resume = Savegame_GetCurrentInfo(Game_GetCurrentLevel()); uint32_t final_time = resume->stats.timer; CLAMPG(final_time, M_MAX_ASSAULT_TIME_FRAMES); resume->stats.timer = final_time; p->quad_course.lap_time = (int32_t)final_time; p->quad_course.lap_time_display_timer = 10 * LOGIC_FPS; M_StoreCourseTime(&g_Config.profile.racetrack_stats, final_time); p->quad_course.timer_active = false; } GYM_TRACK_TYPE Gym_TrackManager_GetActiveTrackType(void) { M_PRIV *const p = &m_Priv; return p->active_track_type; } bool Gym_TrackManager_HasStats(const GYM_TRACK_TYPE track) { switch (track) { case GYM_TRACK_ASSAULT: return g_TRVersion >= 2; case GYM_TRACK_QUAD: return g_TRVersion >= 3; default: return false; } } const GYM_TRACK_STATS *Gym_TrackManager_GetStats(const GYM_TRACK_TYPE track) { switch (track) { case GYM_TRACK_ASSAULT: return &g_Config.profile.assault_stats; case GYM_TRACK_QUAD: return &g_Config.profile.racetrack_stats; default: return nullptr; } } bool Gym_TrackManager_IsTimerDisplay(const GYM_TRACK_TYPE track) { M_PRIV *const p = &m_Priv; switch (track) { case GYM_TRACK_ASSAULT: return p->assault_course.timer_display; case GYM_TRACK_QUAD: return p->quad_course.timer_display; default: return false; } } bool Gym_TrackManager_IsTimerActive(const GYM_TRACK_TYPE track) { M_PRIV *const p = &m_Priv; switch (track) { case GYM_TRACK_ASSAULT: return p->assault_course.timer_active; case GYM_TRACK_QUAD: return p->quad_course.timer_active; default: return false; } } void Gym_TrackManager_Reset(const GYM_TRACK_TYPE track) { M_PRIV *const p = &m_Priv; p->active_track_type = GYM_TRACK_NONE; switch (track) { case GYM_TRACK_ASSAULT: p->assault_course.timer_active = false; p->assault_course.timer_display = false; p->assault_course.penalty_frames = 0; p->assault_course.target_penalty_frames = 0; p->assault_course.penalty_display_timer = 0; p->assault_course.timer_auto_hide_timer = 0; p->assault_course.pad_touched_this_frame = false; p->assault_course.pad_lock = false; break; case GYM_TRACK_QUAD: p->quad_course.timer_active = false; p->quad_course.timer_display = false; p->quad_course.lap_time = 0; p->quad_course.lap_time_display_timer = 0; break; default: break; } } void Gym_TrackManager_Start(const GYM_TRACK_TYPE track) { M_PRIV *const p = &m_Priv; p->active_track_type = track; RESUME_INFO *const resume = Savegame_GetCurrentInfo(Game_GetCurrentLevel()); resume->stats.timer = 0; switch (track) { case GYM_TRACK_ASSAULT: { p->assault_course.timer_active = true; p->assault_course.timer_display = true; p->assault_course.penalty_frames = 0; p->assault_course.target_penalty_frames = 0; p->assault_course.penalty_display_timer = 0; p->assault_course.timer_auto_hide_timer = 0; p->assault_course.pad_touched_this_frame = false; p->assault_course.pad_lock = false; M_ResetAssaultTargets(); break; } case GYM_TRACK_QUAD: { if (!M_IsOnQuadBike()) { return; } p->quad_course.timer_active = true; p->quad_course.timer_display = true; break; default: break; } } } void Gym_TrackManager_Stop(const GYM_TRACK_TYPE track) { M_PRIV *const p = &m_Priv; p->active_track_type = GYM_TRACK_NONE; switch (track) { case GYM_TRACK_ASSAULT: p->assault_course.timer_active = false; p->assault_course.timer_display = true; p->assault_course.timer_auto_hide_timer = 0; break; case GYM_TRACK_QUAD: if (!M_IsOnQuadBike()) { return; } p->quad_course.timer_active = false; p->quad_course.timer_display = true; break; default: break; } } void Gym_TrackManager_Finish(const GYM_TRACK_TYPE track) { M_PRIV *const p = &m_Priv; switch (track) { case GYM_TRACK_ASSAULT: M_Assault_Finish(); break; case GYM_TRACK_QUAD: M_Racetrack_Finish(); break; default: break; } } void Gym_TrackManager_AddPenaltySeconds( const GYM_TRACK_TYPE track, const int32_t seconds) { ASSERT(track == GYM_TRACK_ASSAULT); M_PRIV *const p = &m_Priv; if (seconds <= 0 || !p->assault_course.timer_active) { return; } p->assault_course.penalty_display_timer = 4 * LOGIC_FPS; p->assault_course.penalty_frames += seconds * LOGIC_FPS; CLAMPG(p->assault_course.penalty_frames, M_MAX_ASSAULT_TIME_FRAMES); } int32_t Gym_TrackManager_GetPenaltyDisplayTimer(const GYM_TRACK_TYPE track) { if (track != GYM_TRACK_ASSAULT) { return 0; } M_PRIV *const p = &m_Priv; return p->assault_course.penalty_display_timer; } int32_t Gym_TrackManager_GetPenaltyFrames(const GYM_TRACK_TYPE track) { if (track != GYM_TRACK_ASSAULT) { return 0; } M_PRIV *const p = &m_Priv; return p->assault_course.penalty_frames; } int32_t Gym_TrackManager_GetTargetPenaltyFrames(const GYM_TRACK_TYPE track) { if (track != GYM_TRACK_ASSAULT) { return 0; } M_PRIV *const p = &m_Priv; return p->assault_course.target_penalty_frames; } bool Gym_TrackManager_OnPadContact( const GYM_TRACK_TYPE track, const bool on_ground) { if (g_TRVersion < 3) { return true; } if (track != GYM_TRACK_ASSAULT) { return true; } if (!Game_IsInGym()) { return true; } M_PRIV *const p = &m_Priv; p->assault_course.pad_touched_this_frame = true; if (!on_ground) { return true; } if (p->assault_course.pad_lock) { return false; } p->assault_course.pad_lock = true; return true; } int32_t Gym_TrackManager_GetLapTimeDisplayTimer(const GYM_TRACK_TYPE track) { if (track != GYM_TRACK_QUAD) { return 0; } M_PRIV *const p = &m_Priv; return p->quad_course.lap_time_display_timer; } int32_t Gym_TrackManager_GetLapTime(const GYM_TRACK_TYPE track) { if (track != GYM_TRACK_QUAD) { return 0; } M_PRIV *const p = &m_Priv; return p->quad_course.lap_time; } bool Gym_CanPlayMusicTrack(MUSIC_ID *const track_id) { const uint16_t flags = Music_GetTrackFlags(*track_id); const ITEM *const lara = Lara_GetItem(); switch (Music_FromGameID(*track_id)) { case MX_TR1_GYM_HINT_03: if ((flags & IF_ONE_SHOT) != 0 && lara->current_anim_state == LS(LS_JUMP_UP)) { *track_id = Music_ToGameID(MX_TR1_GYM_HINT_04); } break; case MX_TR1_GYM_HINT_12: if (lara->current_anim_state != LS(LS_HANG)) { return false; } break; case MX_TR1_GYM_HINT_16: if (lara->current_anim_state != LS(LS_HANG)) { return false; } break; case MX_TR1_GYM_HINT_17: if ((flags & IF_ONE_SHOT) != 0 && lara->current_anim_state == LS(LS_HANG)) { *track_id = Music_ToGameID(MX_TR1_GYM_HINT_18); } break; case MX_TR1_GYM_HINT_24: if (lara->current_anim_state != LS(LS_SURF_TREAD)) { return false; } break; case MX_TR1_GYM_HINT_25: if ((flags & IF_ONE_SHOT) != 0) { M_PRIV *const p = &m_Priv; p->completion_timer++; if (p->completion_timer == LOGIC_FPS * 4) { Game_SetIsLevelComplete(true); p->completion_timer = 0; } } else if (lara->current_anim_state != LS(LS_WATER_OUT)) { return false; } break; default: return true; } return true; } ================================================ FILE: src/trx/game/gym.h ================================================ #pragma once #include #include #include #define GYM_ASSAULT_TARGET_TIME (10 * LOGIC_FPS) typedef enum { GYM_TRACK_NONE = -1, GYM_TRACK_QUAD, GYM_TRACK_ASSAULT, GYM_TRACK_NUMBER_OF } GYM_TRACK_TYPE; void Gym_SetInventoryOpenEnabled(bool enabled); bool Gym_IsInventoryOpenEnabled(void); GYM_TRACK_TYPE Gym_TrackManager_GetActiveTrackType(void); bool Gym_TrackManager_HasStats(GYM_TRACK_TYPE track); const GYM_TRACK_STATS *Gym_TrackManager_GetStats(GYM_TRACK_TYPE track); bool Gym_TrackManager_IsTimerDisplay(GYM_TRACK_TYPE track); bool Gym_TrackManager_IsTimerActive(GYM_TRACK_TYPE track); void Gym_TrackManager_Reset(GYM_TRACK_TYPE track); void Gym_TrackManager_Start(GYM_TRACK_TYPE track); void Gym_TrackManager_Stop(GYM_TRACK_TYPE track); void Gym_TrackManager_Finish(GYM_TRACK_TYPE track); // Assault-only extensions (no-op / 0 for other tracks). void Gym_TrackManager_AddPenaltySeconds(GYM_TRACK_TYPE track, int32_t seconds); int32_t Gym_TrackManager_GetPenaltyDisplayTimer(GYM_TRACK_TYPE track); int32_t Gym_TrackManager_GetPenaltyFrames(GYM_TRACK_TYPE track); int32_t Gym_TrackManager_GetTargetPenaltyFrames(GYM_TRACK_TYPE track); bool Gym_TrackManager_OnPadContact(GYM_TRACK_TYPE track, bool on_ground); // Quad-only extensions (0 for other tracks). int32_t Gym_TrackManager_GetLapTimeDisplayTimer(GYM_TRACK_TYPE track); int32_t Gym_TrackManager_GetLapTime(GYM_TRACK_TYPE track); // TR3 assault course extensions (targets + penalties). void Gym_Control(void); // Potentially converts the requested track id based on Lara's state. Returns // true if the track should be played. bool Gym_CanPlayMusicTrack(MUSIC_ID *track_id); ================================================ FILE: src/trx/game/inject/common.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_VIRTUAL_NAME "virtual_injection" typedef struct { INJECTION *injection; const char *path; } M_LOAD_JOB; static bool (*m_Testers[ITT_NUMBER_OF])( const INJECTION_CONTEXT *, const INJECTION *injection) = {}; static void (*m_Handlers[ICT_NUMBER_OF])( const INJECTION_CONTEXT *, INJECTION_CHUNK chunk) = {}; static INJECTION_CONTEXT m_Context = {}; static int32_t m_NumInjections = 0; static INJECTION *m_Injections = nullptr; static int32_t m_DataCounts[IDT_NUMBER_OF] = {}; static int32_t m_MaxStaticObject3DId = -1; static int32_t m_MaxStaticObject2DId = -1; static VECTOR *m_RoomMeta = nullptr; static LEVEL_CONTEXT_INFO m_CachedInfo = {}; static uint16_t *m_PaletteMap = nullptr; static size_t m_PaletteMapSize = 0; static bool M_IsRelevant( const INJECTION_CONTEXT *const ctx, const INJECTION_FILE_TYPE type) { const bool stats = (ctx->mode == INJECTION_MODE_STATS); if (stats) { switch (type) { case IFT_GENERAL: case IFT_FLOOR_DATA: case IFT_PS1_ENEMY: break; default: return false; } } switch (type) { case IFT_GENERAL: case IFT_LARA_ANIMS: case IFT_BRAID: case IFT_SKYBOX: return true; case IFT_FLOOR_DATA: return g_Config.gameplay.fix_floor_data_issues; case IFT_ITEM_POSITION: return g_Config.visuals.fix_item_rots; case IFT_TEXTURE_FIX: return g_Config.visuals.fix_texture_issues; case IFT_ALTER_ANIM_SPRITE: return g_Config.visuals.fix_animated_sprites == (g_TRVersion >= 2); case IFT_PS1_SFX: return g_Config.audio.enable_ps1_sfx; case IFT_PS1_ENEMY: { if (!g_Config.gameplay.restore_ps1_enemies) { return false; } const SAVEGAME_INFO *const info = Savegame_GetSavegameInfo(Savegame_GetBoundSlot()); if (info != nullptr && (info->initial_version == SG_VERSION_LEGACY)) { return false; } return true; } default: return false; } } static INJECTION_CHUNK M_ReadChunk(const INJECTION *const injection) { return (INJECTION_CHUNK) { .injection = injection, .type = VFile_ReadS32(injection->fp), .num_blocks = VFile_ReadS32(injection->fp), .total_size = VFile_ReadS32(injection->fp), }; } static void M_InitialiseBlock( VFILE *const file, const INJECTION_VERSION version) { const INJECTION_DATA_TYPE data_type = VFile_ReadS32(file); const int32_t data_count = VFile_ReadS32(file); const int32_t data_size = VFile_ReadS32(file); if (data_type >= 0 && data_type < IDT_NUMBER_OF) { m_DataCounts[data_type] += data_count; } switch (data_type) { case IDT_STATIC_OBJECTS: { for (int32_t i = 0; i < data_count; i++) { const int32_t static_id = VFile_ReadS32(file); if (static_id > m_MaxStaticObject3DId) { m_MaxStaticObject3DId = static_id; } VFile_Skip(file, 28); } return; } case IDT_SPRITE_SEQUENCES: { for (int32_t i = 0; i < data_count; i++) { const INJECT_OBJECT_TYPE obj_type = VFile_ReadS32(file); const int32_t obj_id = VFile_ReadS32(file); if (obj_type == OBJ_TYPE_STATIC2D && obj_id > m_MaxStaticObject2DId) { m_MaxStaticObject2DId = obj_id; } if (obj_type == OBJ_TYPE_OBJECT && version < INJ_VERSION_5) { VFile_Skip(file, 16); } VFile_Skip(file, sizeof(int16_t) * 2); } return; } case IDT_ROOM_EDIT_META: { if (m_RoomMeta == nullptr) { m_RoomMeta = Vector_Create(sizeof(INJECTION_MESH_META)); } for (int32_t i = 0; i < data_count; i++) { INJECTION_MESH_META meta = { .room_index = VFile_ReadS16(file), .num_vertices = VFile_ReadS16(file), .num_quads = VFile_ReadS16(file), .num_triangles = VFile_ReadS16(file), .num_static_2ds = VFile_ReadS16(file), }; if (version >= INJ_VERSION_3) { meta.num_static_3ds = VFile_ReadS16(file); } Vector_Add(m_RoomMeta, &meta); } return; } case IDT_SAMPLE_INFOS: { for (int32_t i = 0; i < data_count; i++) { // Skip ID, volume and chance VFile_Skip(file, 3 * sizeof(int16_t)); const int16_t flags = VFile_ReadS16(file); if (version >= INJ_VERSION_6) { // Skip range and pitch VFile_Skip(file, sizeof(int32_t) + sizeof(int8_t)); } const int16_t num_samples = (flags >> 2) & 0xF; m_DataCounts[IDT_SAMPLE_INDICES] += num_samples; if (g_TRVersion == 1 || version >= INJ_VERSION_4) { for (int32_t j = 0; j < num_samples; j++) { const int32_t sample_length = VFile_ReadS32(file); m_DataCounts[IDT_SAMPLE_DATA] += sample_length; VFile_Skip(file, sizeof(char) * sample_length); } } else if (g_TRVersion >= 2) { VFile_Skip(file, sizeof(uint32_t)); } } return; } default: break; } VFile_Skip(file, data_size); } static void M_ReadVFile( INJECTION *const injection, VFILE *const file, const char *const file_name) { const char *const inj_name = file_name == nullptr ? M_VIRTUAL_NAME : file_name; char *payload = nullptr; injection->path = Memory_DupStr(inj_name); const uint32_t magic = VFile_ReadU32(file); if (magic != INJECTION_MAGIC) { LOG_WARNING("Invalid injection magic in %s", inj_name); goto cleanup; } injection->version = VFile_ReadS32(file); if (injection->version < INJ_VERSION_2 || injection->version > INJ_CURRENT_VERSION) { LOG_WARNING( "%s uses unsupported version %d", inj_name, injection->version); goto cleanup; } injection->type = VFile_ReadS32(file); if (injection->type < 0 || injection->type >= IFT_NUMBER_OF) { LOG_WARNING("%s is of unknown type %d", inj_name, injection->type); goto cleanup; } injection->relevant = M_IsRelevant(&m_Context, injection->type); if (!injection->relevant) { goto cleanup; } const int32_t uncompressed_size = VFile_ReadS32(file); const int32_t compressed_size = VFile_ReadS32(file); const char *compressed = file->cur_ptr; payload = Memory_Alloc(uncompressed_size); uLongf uncompressed_sizef = uncompressed_size; const int32_t error_code = uncompress( (Bytef *)payload, &uncompressed_sizef, (const Bytef *)compressed, (uLongf)compressed_size); if (error_code != Z_OK) { LOG_WARNING("Failed to decompress injection payload (%d)", error_code); injection->relevant = false; goto cleanup; } injection->fp = VFile_CreateFromBuffer(payload, uncompressed_size); if (m_Context.mode != INJECTION_MODE_STATS) { LOG_INFO("%s queued for injection", inj_name); } cleanup: Memory_FreePointer(&payload); VFile_Close(file); } static void M_InitialiseInjection(INJECTION *const injection) { if (!injection->relevant || injection->fp == nullptr) { return; } VFile_SetPos(injection->fp, 0); { // Tests are executed after the main level data is loaded. VFile_Skip(injection->fp, sizeof(int32_t)); const int32_t test_size = VFile_ReadS32(injection->fp); VFile_Skip(injection->fp, test_size); } const int32_t num_chunks = VFile_ReadS32(injection->fp); for (int32_t i = 0; i < num_chunks; i++) { const INJECTION_CHUNK chunk = M_ReadChunk(injection); for (int32_t j = 0; j < chunk.num_blocks; j++) { M_InitialiseBlock(injection->fp, injection->version); } } VFile_SetPos(injection->fp, 0); } static void M_LoadFromFile( INJECTION *const injection, const char *const file_name) { VFILE *const file = VFile_CreateFromPath(file_name); if (file == nullptr) { LOG_WARNING("Could not open %s", file_name); return; } M_ReadVFile(injection, file, file_name); } static void M_LoadInjectionJob(void *const user_data) { const M_LOAD_JOB *const job = user_data; M_LoadFromFile(job->injection, job->path); } static bool M_IsApplicable(const INJECTION *const injection) { const int32_t test_count = VFile_ReadS32(injection->fp); VFile_Skip(injection->fp, sizeof(int32_t)); bool applicable = true; for (int32_t i = 0; i < test_count; i++) { const INJECTION_TEST_TYPE type = VFile_ReadS32(injection->fp); if (m_Testers[type] == nullptr) { LOG_WARNING("Unknown injection test type %d", type); applicable = false; break; } else { applicable &= m_Testers[type](&m_Context, injection); } } return applicable; } void Inject_RegisterTester( const INJECTION_TEST_TYPE type, bool (*test_func)(const INJECTION_CONTEXT *, const INJECTION *injection)) { m_Testers[type] = test_func; } void Inject_RegisterHandler( const INJECTION_CHUNK_TYPE type, void (*handle_func)(const INJECTION_CONTEXT *, INJECTION_CHUNK chunk)) { m_Handlers[type] = handle_func; } void Inject_RegisterPaletteMap(const uint16_t *palette_map, const int32_t size) { Memory_FreePointer(&m_PaletteMap); m_PaletteMap = Memory_Alloc(size * sizeof(int16_t)); m_PaletteMapSize = size; memcpy(m_PaletteMap, palette_map, size * sizeof(int16_t)); } uint16_t Inject_GetPaletteIndex(const uint16_t index) { ASSERT(index < m_PaletteMapSize); return m_PaletteMap == nullptr ? 0 : m_PaletteMap[index]; } void Inject_InitLevel(const GF_LEVEL *const level, const INJECTION_MODE mode) { m_Context.mode = mode; m_NumInjections = level->injections.count; if (m_NumInjections == 0) { return; } BENCHMARK benchmark = Benchmark_Start(); m_Injections = Memory_Alloc(sizeof(INJECTION) * m_NumInjections); if (m_NumInjections > 1) { M_LOAD_JOB *const jobs = Memory_Alloc(sizeof(M_LOAD_JOB) * m_NumInjections); THREAD_POOL *const pool = ThreadPool_Create(-1); ASSERT(pool != nullptr); for (int32_t i = 0; i < m_NumInjections; i++) { jobs[i] = (M_LOAD_JOB) { .injection = &m_Injections[i], .path = level->injections.data_paths[i], }; ThreadPool_AddJob(pool, M_LoadInjectionJob, &jobs[i]); } ThreadPool_Wait(pool); ThreadPool_Destroy(pool); Memory_Free(jobs); } else { M_LoadFromFile(&m_Injections[0], level->injections.data_paths[0]); } for (int32_t i = 0; i < m_NumInjections; i++) { M_InitialiseInjection(&m_Injections[i]); } if (m_Context.mode != INJECTION_MODE_STATS) { Benchmark_End(&benchmark, nullptr); } } void Inject_AppendInjection(VFILE *const file) { m_Injections = Memory_Realloc(m_Injections, sizeof(INJECTION) * (m_NumInjections + 1)); INJECTION *const injection = &m_Injections[m_NumInjections++]; M_ReadVFile(injection, file, nullptr); M_InitialiseInjection(injection); } void Inject_AllInjections(void) { if (m_Injections == nullptr) { return; } BENCHMARK benchmark = Benchmark_Start(); for (int32_t i = 0; i < m_NumInjections; i++) { INJECTION *const injection = &m_Injections[i]; if (!injection->relevant) { continue; } // Allow checks to be done on an injection's applicability after the // main level data has loaded. if (!M_IsApplicable(injection)) { LOG_WARNING( "Injection type %d is not applicable to the current level", injection->type); continue; } if (m_Context.mode != INJECTION_MODE_STATS) { LOG_DEBUG("Processing %s", injection->path); } // Cache the current status to allow individual handlers to increment // counts but still have access to current indices as required. m_CachedInfo = *Level_Context_GetInfo(); const int32_t num_chunks = VFile_ReadS32(injection->fp); for (int32_t j = 0; j < num_chunks; j++) { const INJECTION_CHUNK chunk = M_ReadChunk(injection); if (chunk.type < 0 || chunk.type >= ICT_NUMBER_OF || m_Handlers[chunk.type] == nullptr) { LOG_WARNING("Unrecognised chunk type %d", chunk.type); VFile_Skip(injection->fp, chunk.total_size); continue; } m_Handlers[chunk.type](&m_Context, chunk); } ASSERT(VFile_GetPos(injection->fp) == injection->fp->size); } if (m_Context.mode != INJECTION_MODE_STATS) { Benchmark_End(&benchmark, nullptr); } } void Inject_Cleanup(void) { if (m_Injections == nullptr) { return; } BENCHMARK benchmark = Benchmark_Start(); for (int32_t i = 0; i < m_NumInjections; i++) { INJECTION *const injection = &m_Injections[i]; if (injection->fp != nullptr) { VFile_Close(injection->fp); } Memory_FreePointer(&injection->path); } for (int32_t i = 0; i < IDT_NUMBER_OF; i++) { m_DataCounts[i] = 0; } m_MaxStaticObject3DId = -1; m_MaxStaticObject2DId = -1; Memory_FreePointer(&m_Injections); Memory_FreePointer(&m_PaletteMap); m_NumInjections = 0; m_CachedInfo = (LEVEL_CONTEXT_INFO) {}; if (m_RoomMeta != nullptr) { Vector_Free(m_RoomMeta); m_RoomMeta = nullptr; } Benchmark_End(&benchmark, nullptr); } INJECTION_MESH_META Inject_GetRoomMeshMeta(const int32_t room_index) { INJECTION_MESH_META summed_meta = {}; if (m_RoomMeta == nullptr) { return summed_meta; } for (int32_t i = 0; i < m_RoomMeta->count; i++) { const INJECTION_MESH_META *const meta = Vector_Get(m_RoomMeta, i); if (meta->room_index != room_index) { continue; } summed_meta.num_vertices += meta->num_vertices; summed_meta.num_quads += meta->num_quads; summed_meta.num_triangles += meta->num_triangles; summed_meta.num_static_2ds += meta->num_static_2ds; summed_meta.num_static_3ds += meta->num_static_3ds; } return summed_meta; } int32_t Inject_GetDataCount(const INJECTION_DATA_TYPE type) { return m_DataCounts[type]; } int32_t Inject_GetMaxStaticObject3DId(void) { return m_MaxStaticObject3DId; } int32_t Inject_GetMaxStaticObject2DId(void) { return m_MaxStaticObject2DId; } LEVEL_CONTEXT_INFO Inject_GetCachedInfo(void) { return m_CachedInfo; } ================================================ FILE: src/trx/game/inject/common.h ================================================ #include #include #include #include #define INJECTION_MAGIC MKTAG('T', 'R', 'X', 'J') void Inject_InitLevel(const GF_LEVEL *level, INJECTION_MODE mode); void Inject_AppendInjection(VFILE *file); void Inject_AllInjections(void); void Inject_Cleanup(void); INJECTION_MESH_META Inject_GetRoomMeshMeta(int32_t room_index); int32_t Inject_GetDataCount(INJECTION_DATA_TYPE type); int32_t Inject_GetMaxStaticObject3DId(void); int32_t Inject_GetMaxStaticObject2DId(void); LEVEL_CONTEXT_INFO Inject_GetCachedInfo(void); void Inject_RegisterPaletteMap(const uint16_t *palette_map, int32_t size); uint16_t Inject_GetPaletteIndex(uint16_t index); void Inject_RegisterTester( const INJECTION_TEST_TYPE type, bool (*test_func)(const INJECTION_CONTEXT *, const INJECTION *injection)); void Inject_RegisterHandler( INJECTION_CHUNK_TYPE type, void (*handle_func)(const INJECTION_CONTEXT *, INJECTION_CHUNK chunk)); #define REGISTER_INJECT_TESTER(test_type, test_func) \ __attribute__((constructor)) static void \ M_RegisterInjectTester##test_type(void) \ { \ Inject_RegisterTester(test_type, test_func); \ } #define REGISTER_INJECTOR(chunk_type, handle_func) \ __attribute__((constructor)) static void M_RegisterInjector##chunk_type( \ void) \ { \ Inject_RegisterHandler(chunk_type, handle_func); \ } ================================================ FILE: src/trx/game/inject/data/anims.c ================================================ #include #include #include #include #include #include #include #include static void M_HandleAnimData( const INJECTION_CONTEXT *const ctx, const INJECTION_CHUNK chunk) { LEVEL_CONTEXT_INFO *const level_info = Level_Context_GetInfo(); const LEVEL_CONTEXT_INFO cached_info = Inject_GetCachedInfo(); for (int32_t i = 0; i < chunk.num_blocks; i++) { const INJECTION_DATA_TYPE data_type = VFile_ReadS32(chunk.injection->fp); const int32_t data_count = VFile_ReadS32(chunk.injection->fp); const int32_t data_size = VFile_ReadS32(chunk.injection->fp); if (ctx->mode == INJECTION_MODE_STATS) { VFile_Skip(chunk.injection->fp, data_size); continue; } switch (data_type) { case IDT_ANIMS: { Level_Section_AppendAnims( level_info->anims.anim_count, data_count, chunk.injection->fp); level_info->anims.anim_count += data_count; for (int32_t j = 0; j < data_count; j++) { ANIM *const anim = Anim_GetAnim(cached_info.anims.anim_count + j); anim->jump_anim_num += cached_info.anims.anim_count; anim->frame_ofs += cached_info.anims.frame_count * 2; anim->change_idx += cached_info.anims.change_count; anim->command_idx += cached_info.anims.command_count; } break; } case IDT_ANIM_BONES: { Level_Section_AppendAnimBones( level_info->anims.bone_count, data_count, chunk.injection->fp); level_info->anims.bone_count += data_count; break; } case IDT_ANIM_CHANGES: { Level_Section_AppendAnimChanges( level_info->anims.change_count, data_count, chunk.injection->fp); level_info->anims.change_count += data_count; for (int32_t j = 0; j < data_count; j++) { ANIM_CHANGE *const change = Anim_GetChange(cached_info.anims.change_count + j); change->range_idx += cached_info.anims.range_count; } break; } case IDT_ANIM_COMMANDS: { Level_Section_AppendAnimCommands( level_info->anims.command_count, data_count, chunk.injection->fp); level_info->anims.command_count += data_count; break; } case IDT_ANIM_FRAMES: { Level_Section_AppendAnimFrames( level_info->anims.frame_count, data_count, chunk.injection->fp); level_info->anims.frame_count += data_count; break; } case IDT_ANIM_RANGES: { Level_Section_AppendAnimRanges( level_info->anims.range_count, data_count, chunk.injection->fp); level_info->anims.range_count += data_count; for (int32_t j = 0; j < data_count; j++) { ANIM_RANGE *const range = Anim_GetRange(cached_info.anims.range_count + j); range->link_anim_num += cached_info.anims.anim_count; } break; } default: LOG_WARNING("Unknown data type: %d", data_type); VFile_Skip(chunk.injection->fp, data_size); break; } } } static void M_CommandEdits( const INJECTION_CONTEXT *const ctx, const INJECTION *const injection, const int32_t data_count) { const LEVEL_CONTEXT_INFO cached_info = Inject_GetCachedInfo(); int16_t cmd_idx = cached_info.anims.command_count; for (int32_t i = 0; i < data_count; i++) { const INJECTION_OBJECT_INFO obj_info = Inject_ReadObjectPtr(injection); const int32_t anim_idx = VFile_ReadS32(injection->fp); const int32_t num_raw_cmds = VFile_ReadS32(injection->fp); const int32_t num_anim_cmds = VFile_ReadS32(injection->fp); const OBJECT *const obj = Object_Get(obj_info.id); if (ctx->mode == INJECTION_MODE_STATS || !obj->loaded) { continue; } ANIM *const anim = Object_GetAnim(obj, anim_idx); anim->command_idx = cmd_idx; anim->num_commands = num_anim_cmds; cmd_idx += num_raw_cmds; } } REGISTER_INJECTOR(ICT_ANIMATION_DATA, M_HandleAnimData) REGISTER_INJECT_EDITOR(IDT_ANIM_CMD_EDITS, M_CommandEdits) ================================================ FILE: src/trx/game/inject/data/camera.c ================================================ #include #include #include static void M_ReadVertex(XYZ_16 *const vertex, VFILE *const file) { vertex->x = VFile_ReadS16(file); vertex->y = VFile_ReadS16(file); vertex->z = VFile_ReadS16(file); } static void M_HandleCineFrames( const INJECTION *const injection, const int32_t data_count) { for (int32_t i = 0; i < data_count; i++) { CINE_FRAME *const frame = Camera_GetCineFrame(i); M_ReadVertex(&frame->target.shift, injection->fp); M_ReadVertex(&frame->camera.shift, injection->fp); frame->fov = VFile_ReadS16(injection->fp); frame->roll = VFile_ReadS16(injection->fp); } } static void M_HandleCameraData( const INJECTION_CONTEXT *const ctx, const INJECTION_CHUNK chunk) { for (int32_t i = 0; i < chunk.num_blocks; i++) { const INJECTION_DATA_TYPE data_type = VFile_ReadS32(chunk.injection->fp); const int32_t data_count = VFile_ReadS32(chunk.injection->fp); const int32_t data_size = VFile_ReadS32(chunk.injection->fp); if (ctx->mode == INJECTION_MODE_STATS) { VFile_Skip(chunk.injection->fp, data_size); continue; } switch (data_type) { case IDT_CINEMATIC_FRAMES: M_HandleCineFrames(chunk.injection, data_count); break; default: LOG_WARNING("Unknown data type: %d", data_type); VFile_Skip(chunk.injection->fp, data_size); break; } } } REGISTER_INJECTOR(ICT_CAMERA_DATA, M_HandleCameraData) ================================================ FILE: src/trx/game/inject/data/meshes.c ================================================ #include #include #include #include #include static void M_HandleMeshData( const INJECTION_CONTEXT *const ctx, const INJECTION_CHUNK chunk) { int32_t mesh_ptr_count = 0; int32_t *mesh_indices = nullptr; const size_t chunk_start_pos = VFile_GetPos(chunk.injection->fp); for (int32_t i = 0; i < chunk.num_blocks; i++) { const INJECTION_DATA_TYPE data_type = VFile_ReadS32(chunk.injection->fp); const int32_t data_count = VFile_ReadS32(chunk.injection->fp); const int32_t data_size = VFile_ReadS32(chunk.injection->fp); if (ctx->mode == INJECTION_MODE_STATS) { VFile_Skip(chunk.injection->fp, data_size); continue; } switch (data_type) { case IDT_MESH_POINTERS: { const int32_t alloc_size = data_count * sizeof(int32_t); mesh_indices = Memory_Alloc(alloc_size); VFile_Read(chunk.injection->fp, mesh_indices, alloc_size); mesh_ptr_count = data_count; break; } case IDT_OBJECT_MESHES: { ASSERT(mesh_indices != nullptr); Level_Section_AppendObjectMeshes( mesh_ptr_count, mesh_indices, chunk.injection->fp); LEVEL_CONTEXT_INFO *const info = Level_Context_GetInfo(); info->mesh_ptr_count += mesh_ptr_count; break; } default: LOG_WARNING("Unknown data type: %d", data_type); VFile_Skip(chunk.injection->fp, data_size); break; } } Memory_FreePointer(&mesh_indices); // Not all mesh data is necessarily read, so ensure to move to the end. VFile_SetPos(chunk.injection->fp, chunk_start_pos + chunk.total_size); } REGISTER_INJECTOR(ICT_MESH_DATA, M_HandleMeshData) ================================================ FILE: src/trx/game/inject/data/objects.c ================================================ #include #include #include #include static VECTOR *m_ProcessedMeshes = nullptr; static void M_ReadBounds16(BOUNDS_16 *const bounds, VFILE *const file) { bounds->min.x = VFile_ReadS16(file); bounds->max.x = VFile_ReadS16(file); bounds->min.y = VFile_ReadS16(file); bounds->max.y = VFile_ReadS16(file); bounds->min.z = VFile_ReadS16(file); bounds->max.z = VFile_ReadS16(file); } static void M_AlignTextureReferences( OBJECT_MESH *const mesh, const int32_t tex_info_base) { if (Vector_Contains(m_ProcessedMeshes, (void *)mesh)) { return; } Vector_Add(m_ProcessedMeshes, (void *)mesh); for (int32_t j = 0; j < mesh->tex_faces.count; j++) { mesh->tex_faces.data[j].texture_idx += tex_info_base; } for (int32_t j = 0; j < mesh->flat_faces.count; j++) { FACE *const face = &mesh->flat_faces.data[j]; face->palette_idx = Inject_GetPaletteIndex(face->palette_idx); } } static void M_ReadObject(const INJECTION_CHUNK chunk) { const LEVEL_CONTEXT_INFO cached_info = Inject_GetCachedInfo(); const INJECTION_OBJECT_INFO obj_info = Inject_ReadObjectPtr(chunk.injection); OBJECT *const obj = Object_TryGet(obj_info.id); if (obj == nullptr) { LOG_WARNING("Invalid object %d", obj_info.id); VFile_Skip(chunk.injection->fp, 14); return; } const int16_t num_meshes = VFile_ReadS16(chunk.injection->fp); const int16_t mesh_idx = VFile_ReadS16(chunk.injection->fp); const int32_t bone_idx = VFile_ReadS32(chunk.injection->fp) / ANIM_BONE_SIZE; // Omitted mesh data indicates that we wish to retain what's already // defined in level data to avoid duplicate texture page usage. if (!obj->loaded || num_meshes != 0) { obj->mesh_count = num_meshes; obj->mesh_idx = mesh_idx + cached_info.mesh_ptr_count; obj->bone_idx = bone_idx + cached_info.anims.bone_count; } // Ommitted animation data marks that existing related object data should be // retained i.e. mesh replacement only. const uint32_t frame_ofs = VFile_ReadU32(chunk.injection->fp); const int16_t anim_idx = VFile_ReadS16(chunk.injection->fp); if ((int32_t)frame_ofs != -1) { obj->frame_ofs = frame_ofs; obj->frame_base = nullptr; obj->anim_idx = anim_idx; if (obj->anim_idx != -1) { obj->anim_idx += cached_info.anims.anim_count; } obj->loaded = true; } for (int32_t i = 0; i < num_meshes; i++) { OBJECT_MESH *const mesh = Object_GetMesh(obj->mesh_idx + i); M_AlignTextureReferences(mesh, cached_info.textures.object_count); } } static void M_ReadStaticObject3D(const INJECTION_CHUNK chunk) { const LEVEL_CONTEXT_INFO cached_info = Inject_GetCachedInfo(); const int32_t static_id = VFile_ReadS32(chunk.injection->fp); if (static_id < 0 || static_id >= Object_GetStaticObjects3DCount()) { LOG_WARNING("Invalid static 3D %d", static_id); VFile_Skip(chunk.injection->fp, 2 + 12 + 12 + 2); return; } STATIC_OBJECT_3D *const obj = Object_Get3DStatic(static_id); obj->mesh_idx = VFile_ReadS16(chunk.injection->fp); obj->mesh_idx += cached_info.mesh_ptr_count; obj->loaded = true; M_ReadBounds16(&obj->draw_bounds, chunk.injection->fp); M_ReadBounds16(&obj->collision_bounds, chunk.injection->fp); const uint16_t flags = VFile_ReadU16(chunk.injection->fp); obj->collidable = (flags & 1) == 0; obj->visible = (flags & 2) != 0; OBJECT_MESH *const mesh = Object_GetMesh(obj->mesh_idx); M_AlignTextureReferences(mesh, cached_info.textures.object_count); } static void M_HandleObjectData( const INJECTION_CONTEXT *const ctx, const INJECTION_CHUNK chunk) { m_ProcessedMeshes = Vector_Create(sizeof(OBJECT_MESH *)); for (int32_t i = 0; i < chunk.num_blocks; i++) { const INJECTION_DATA_TYPE data_type = VFile_ReadS32(chunk.injection->fp); const int32_t data_count = VFile_ReadS32(chunk.injection->fp); const int32_t data_size = VFile_ReadS32(chunk.injection->fp); if (ctx->mode == INJECTION_MODE_STATS) { VFile_Skip(chunk.injection->fp, data_size); continue; } for (int32_t j = 0; j < data_count; j++) { switch (data_type) { case IDT_OBJECTS: M_ReadObject(chunk); break; case IDT_STATIC_OBJECTS: M_ReadStaticObject3D(chunk); break; default: LOG_WARNING("Unrecognised object data type %d", data_type); VFile_Skip(chunk.injection->fp, data_size); break; } } } Vector_Free(m_ProcessedMeshes); } REGISTER_INJECTOR(ICT_OBJECT_DATA, M_HandleObjectData) ================================================ FILE: src/trx/game/inject/data/sound.c ================================================ #include #include #include #include #include #include static void M_HandleSFXData( const INJECTION_CONTEXT *const ctx, const INJECTION_CHUNK chunk) { ASSERT(chunk.num_blocks == 1); const INJECTION_DATA_TYPE data_type = VFile_ReadS32(chunk.injection->fp); ASSERT(data_type == IDT_SAMPLE_INFOS); const int32_t data_count = VFile_ReadS32(chunk.injection->fp); VFile_Skip(chunk.injection->fp, sizeof(int32_t)); for (int32_t i = 0; i < data_count; i++) { const SAMPLE_ID sfx_id = VFile_ReadS16(chunk.injection->fp); SAMPLE_INFO *const sample_info = Sound_GetOrCreateSample(sfx_id); sample_info->volume = VFile_ReadS16(chunk.injection->fp); sample_info->randomness = VFile_ReadS16(chunk.injection->fp); sample_info->flags.all = VFile_ReadU16(chunk.injection->fp); if (chunk.injection->version >= INJ_VERSION_6) { sample_info->range = VFile_ReadS32(chunk.injection->fp); sample_info->pitch = VFile_ReadS8(chunk.injection->fp); } else { sample_info->range = 10 * WALL_L; sample_info->pitch = 0; } if (g_TRVersion == 1) { switch (sample_info->flags.mode_bits) { case 0: sample_info->mode = SAMPLE_MODE_WAIT; break; case 1: sample_info->mode = SAMPLE_MODE_RESTART; break; case 2: sample_info->mode = SAMPLE_MODE_LOOPED; break; case 3: sample_info->mode = SAMPLE_MODE_NORMAL; break; } } else { switch (sample_info->flags.mode_bits) { case 0: sample_info->mode = SAMPLE_MODE_NORMAL; break; case 1: sample_info->mode = SAMPLE_MODE_WAIT; break; case 2: sample_info->mode = SAMPLE_MODE_RESTART; break; case 3: sample_info->mode = SAMPLE_MODE_LOOPED; break; } } const int16_t num_samples = sample_info->flags.num_samples; if (g_TRVersion == 1 || chunk.injection->version >= INJ_VERSION_4) { sample_info->number = Sound_ReserveSampleData(-1, num_samples); for (int32_t j = 0; j < num_samples; j++) { const int32_t sample_length = VFile_ReadS32(chunk.injection->fp); char *const data = Memory_Alloc(sample_length); VFile_Read(chunk.injection->fp, data, sample_length); Sound_LoadSampleData( sample_info->number + j, data, sample_length); Memory_Free(data); } } else if (g_TRVersion >= 2) { VFile_Skip(chunk.injection->fp, sizeof(int32_t)); } } } REGISTER_INJECTOR(ICT_SFX_DATA, M_HandleSFXData) ================================================ FILE: src/trx/game/inject/data/textures.c ================================================ #include #include #include #include #include #include #include #include #include #include static uint16_t M_RemapRGB8(const RGB_888 rgb) { const LEVEL_CONTEXT_INFO *const level_info = Level_Context_GetInfo(); uint16_t best_idx = 0; int32_t best_diff = INT32_MAX; for (int32_t i = 1; i < level_info->palette.size; i++) { const RGB_888 test_rgb = level_info->palette.data_24[i]; const int32_t dr = rgb.r - test_rgb.r; const int32_t dg = rgb.g - test_rgb.g; const int32_t db = rgb.b - test_rgb.b; const int32_t diff = SQUARE(dr) + SQUARE(dg) + SQUARE(db); if (diff < best_diff) { best_diff = diff; best_idx = i; } } return best_idx; } static void M_HandlePalette( const INJECTION *const injection, const int32_t data_count) { uint16_t palette_map[data_count]; for (int32_t i = 0; i < data_count; i++) { const RGB_888 rgb = { .r = VFile_ReadU8(injection->fp) * 4, .g = VFile_ReadU8(injection->fp) * 4, .b = VFile_ReadU8(injection->fp) * 4, }; palette_map[i] = i == 0 ? 0 : M_RemapRGB8(rgb); } Inject_RegisterPaletteMap(palette_map, data_count); } static void M_HandleTexturePages( const INJECTION *const injection, const int32_t data_count) { LEVEL_CONTEXT_INFO *const info = Level_Context_GetInfo(); if (info->textures.pages_32 == nullptr) { VFile_Skip( injection->fp, data_count * TEXTURE_PAGE_SIZE * sizeof(RGBA_8888)); VFile_Skip(injection->fp, data_count * TEXTURE_PAGE_SIZE); return; } RGBA_8888 *const output_32 = &info->textures.pages_32[info->textures.page_count * TEXTURE_PAGE_SIZE]; VFile_Read( injection->fp, output_32, data_count * TEXTURE_PAGE_SIZE * sizeof(RGBA_8888)); uint8_t *output_8 = &info->textures.pages_8[info->textures.page_count * TEXTURE_PAGE_SIZE]; uint8_t *input_8 = Memory_Alloc(data_count * TEXTURE_PAGE_SIZE); VFile_Read(injection->fp, input_8, data_count * TEXTURE_PAGE_SIZE); uint8_t *input_ptr = input_8; for (int32_t i = 0; i < data_count * TEXTURE_PAGE_SIZE; i++) { *output_8++ = Inject_GetPaletteIndex(*input_ptr++); } Memory_FreePointer(&input_8); info->textures.page_count += data_count; } static void M_HandleSpriteSequences( const INJECTION *const injection, const int32_t data_count) { LEVEL_CONTEXT_INFO *const level_info = Level_Context_GetInfo(); for (int32_t i = 0; i < data_count; i++) { const INJECTION_OBJECT_INFO obj_info = Inject_ReadObjectPtr(injection); const int16_t num_meshes = VFile_ReadS16(injection->fp); const int16_t mesh_idx = VFile_ReadS16(injection->fp); if (obj_info.type == OBJ_TYPE_OBJECT) { OBJECT *const obj = Object_TryGet(obj_info.id); if (obj == nullptr) { LOG_WARNING("Invalid object %d", obj_info.id); } else { obj->mesh_count = num_meshes; obj->mesh_idx = mesh_idx + level_info->textures.sprite_count; obj->loaded = true; } } else if (obj_info.type == OBJ_TYPE_STATIC2D) { STATIC_OBJECT_2D *const obj = Object_Get2DStatic(obj_info.id); if (obj == nullptr) { LOG_WARNING("Invalid static 2D %d", obj_info.id); continue; } obj->frame_count = ABS(num_meshes); obj->texture_idx = mesh_idx + level_info->textures.sprite_count; obj->loaded = true; } else { LOG_WARNING("Invalid object type %d", obj_info.type); } level_info->textures.sprite_count += ABS(num_meshes); } } static void M_HandleTextureData( const INJECTION_CONTEXT *const ctx, const INJECTION_CHUNK chunk) { for (int32_t i = 0; i < chunk.num_blocks; i++) { const INJECTION_DATA_TYPE data_type = VFile_ReadS32(chunk.injection->fp); const int32_t data_count = VFile_ReadS32(chunk.injection->fp); const int32_t data_size = VFile_ReadS32(chunk.injection->fp); if (ctx->mode == INJECTION_MODE_STATS) { VFile_Skip(chunk.injection->fp, data_size); continue; } switch (data_type) { case IDT_PALETTE: M_HandlePalette(chunk.injection, data_count); break; case IDT_TEXTURE_PAGES: M_HandleTexturePages(chunk.injection, data_count); break; default: LOG_WARNING("Unknown data type: %d", data_type); VFile_Skip(chunk.injection->fp, data_size); break; } } } static void M_HandleTextureInfo( const INJECTION_CONTEXT *const ctx, const INJECTION_CHUNK chunk) { LEVEL_CONTEXT_INFO *const level_info = Level_Context_GetInfo(); const LEVEL_CONTEXT_INFO cached_info = Inject_GetCachedInfo(); const int32_t page_base = cached_info.textures.page_count; for (int32_t i = 0; i < chunk.num_blocks; i++) { const INJECTION_DATA_TYPE data_type = VFile_ReadS32(chunk.injection->fp); const int32_t data_count = VFile_ReadS32(chunk.injection->fp); const int32_t data_size = VFile_ReadS32(chunk.injection->fp); if (ctx->mode == INJECTION_MODE_STATS) { VFile_Skip(chunk.injection->fp, data_size); continue; } switch (data_type) { case IDT_OBJECT_TEXTURES: Level_Section_AppendObjectTextures( level_info->textures.object_count, page_base, data_count, chunk.injection->fp, false); level_info->textures.object_count += data_count; break; case IDT_SPRITE_TEXTURES: Level_Section_AppendSpriteTextures( level_info->textures.sprite_count, page_base, data_count, chunk.injection->fp); break; case IDT_SPRITE_SEQUENCES: M_HandleSpriteSequences(chunk.injection, data_count); break; default: LOG_WARNING("Unknown data type: %d", data_type); VFile_Skip(chunk.injection->fp, data_size); break; } } } REGISTER_INJECTOR(ICT_TEXTURE_DATA, M_HandleTextureData) REGISTER_INJECTOR(ICT_TEXTURE_INFO, M_HandleTextureInfo) ================================================ FILE: src/trx/game/inject/editor.c ================================================ #include #include static void (*m_Handlers[IDT_NUMBER_OF])( const INJECTION_CONTEXT *ctx, const INJECTION *injection, int32_t element_count) = {}; static void M_HandleDataEdits( const INJECTION_CONTEXT *const ctx, const INJECTION_CHUNK chunk) { for (int32_t i = 0; i < chunk.num_blocks; i++) { const INJECTION_DATA_TYPE data_type = VFile_ReadS32(chunk.injection->fp); const int32_t data_count = VFile_ReadS32(chunk.injection->fp); const int32_t data_size = VFile_ReadS32(chunk.injection->fp); if (m_Handlers[data_type] == nullptr) { if (data_type != IDT_ROOM_EDIT_META) { LOG_WARNING("Unknown data type: %d", data_type); } VFile_Skip(chunk.injection->fp, data_size); } else { m_Handlers[data_type](ctx, chunk.injection, data_count); } } } void Inject_RegisterEditor( const INJECTION_DATA_TYPE type, void (*handle_func)( const INJECTION_CONTEXT *ctx, const INJECTION *injection, int32_t element_count)) { m_Handlers[type] = handle_func; } REGISTER_INJECTOR(ICT_DATA_EDITS, M_HandleDataEdits) ================================================ FILE: src/trx/game/inject/editor.h ================================================ #include void Inject_RegisterEditor( INJECTION_DATA_TYPE type, void (*handle_func)( const INJECTION_CONTEXT *ctx, const INJECTION *injection, int32_t element_count)); #define REGISTER_INJECT_EDITOR(data_type, handle_func) \ __attribute__((constructor)) static void \ M_RegisterInjectEditor##data_type(void) \ { \ Inject_RegisterEditor(data_type, handle_func); \ } ================================================ FILE: src/trx/game/inject/editors/anims.c ================================================ #include #include #include static void M_FrameEdits( const INJECTION_CONTEXT *const ctx, const INJECTION *const injection, const int32_t data_count) { LEVEL_CONTEXT_INFO *const level_info = Level_Context_GetInfo(); for (int32_t i = 0; i < data_count; i++) { const INJECTION_OBJECT_INFO obj_info = Inject_ReadObjectPtr(injection); const int32_t anim_idx = VFile_ReadS32(injection->fp); const int32_t packed_rot = VFile_ReadS32(injection->fp); const OBJECT *const obj = Object_Get(obj_info.id); if (ctx->mode == INJECTION_MODE_STATS || !obj->loaded) { continue; } const ANIM *const anim = Object_GetAnim(obj, anim_idx); int16_t *data_ptr = &level_info->anims.frames[anim->frame_ofs / sizeof(int16_t)]; data_ptr += 10; memcpy(data_ptr, &packed_rot, sizeof(int32_t)); } } static void M_FrameReplacements( const INJECTION_CONTEXT *const ctx, const INJECTION *const injection, const int32_t data_count) { LEVEL_CONTEXT_INFO *const level_info = Level_Context_GetInfo(); for (int32_t i = 0; i < data_count; i++) { const INJECTION_OBJECT_INFO obj_info = Inject_ReadObjectPtr(injection); const int32_t num_anims = VFile_ReadS32(injection->fp); const OBJECT *const obj = Object_Get(obj_info.id); for (int32_t j = 0; j < num_anims; j++) { const int32_t anim_idx = VFile_ReadS32(injection->fp); const int32_t num_frames = VFile_ReadS32(injection->fp); if (ctx->mode == INJECTION_MODE_STATS) { VFile_Skip(injection->fp, num_frames * sizeof(int16_t)); } else { const ANIM *const anim = Object_GetAnim(obj, anim_idx); int16_t *const data_ptr = &level_info->anims .frames[anim->frame_ofs / sizeof(int16_t)]; VFile_Read( injection->fp, data_ptr, num_frames * sizeof(int16_t)); } } } } static void M_AnimEdits( const INJECTION_CONTEXT *const ctx, const INJECTION *const injection, const int32_t data_count) { for (int32_t i = 0; i < data_count; i++) { const INJECTION_OBJECT_INFO obj_info = Inject_ReadObjectPtr(injection); const OBJECT *const obj = Object_Get(obj_info.id); const int32_t anim_idx = VFile_ReadS32(injection->fp); const int32_t velocity = VFile_ReadS32(injection->fp); if (ctx->mode == INJECTION_MODE_STATS) { continue; } ANIM *const anim = Object_GetAnim(obj, anim_idx); anim->velocity = velocity; } } REGISTER_INJECT_EDITOR(IDT_FRAME_EDITS, M_FrameEdits) REGISTER_INJECT_EDITOR(IDT_FRAME_REPLACE, M_FrameReplacements) REGISTER_INJECT_EDITOR(IDT_ANIM_EDITS, M_AnimEdits) ================================================ FILE: src/trx/game/inject/editors/floor_data.c ================================================ #include #include #include #include #include #include #include #include #include #define NULL_FD_INDEX ((uint16_t)(-1)) static void M_TriggerTypeChange( const INJECTION *const injection, const SECTOR *const sector) { const uint8_t new_type = VFile_ReadU8(injection->fp); if (sector != nullptr && sector->trigger != nullptr) { sector->trigger->type = new_type; } } static void M_TriggerParameterChange( const INJECTION *const injection, const SECTOR *const sector) { const uint8_t cmd_type = VFile_ReadU8(injection->fp); const int16_t old_param = VFile_ReadS16(injection->fp); const int16_t new_param = VFile_ReadS16(injection->fp); if (sector == nullptr || sector->trigger == nullptr) { return; } // If we can find an action item for the given sector that matches // the command type and old (current) parameter, change it to the // new parameter. TRIGGER_CMD *cmd = sector->trigger->command; for (; cmd != nullptr; cmd = cmd->next_cmd) { if (cmd->type != cmd_type) { continue; } if (cmd->type == TO_CAMERA) { TRIGGER_CAMERA_DATA *const cam_data = (TRIGGER_CAMERA_DATA *)cmd->parameter; if (cam_data->camera_num == old_param) { cam_data->camera_num = new_param; break; } } else { if ((int16_t)(intptr_t)cmd->parameter == old_param) { cmd->parameter = (void *)(intptr_t)new_param; break; } } } } static void M_SetMusicOneShot(const SECTOR *const sector) { if (sector == nullptr || sector->trigger == nullptr) { return; } const TRIGGER_CMD *cmd = sector->trigger->command; for (; cmd != nullptr; cmd = cmd->next_cmd) { if (cmd->type == TO_CD) { sector->trigger->one_shot = true; break; } } } static void M_FixGlideCamera( const INJECTION_CONTEXT *const ctx, const INJECTION *const injection, const SECTOR *const sector) { if (ctx->mode == INJECTION_MODE_STATS) { VFile_Skip(injection->fp, 8); return; } const uint8_t camera_timer = VFile_ReadU8(injection->fp); const uint8_t glide_timer = VFile_ReadU8(injection->fp); const XYZ_16 camera_shift = { .x = VFile_ReadS16(injection->fp), .y = VFile_ReadS16(injection->fp), .z = VFile_ReadS16(injection->fp), }; if (sector == nullptr || sector->trigger == nullptr) { return; } if (!g_Config.visuals.enable_glide_cameras) { return; } const TRIGGER_CMD *cmd = sector->trigger->command; for (; cmd != nullptr; cmd = cmd->next_cmd) { if (cmd->type != TO_CAMERA) { continue; } TRIGGER_CAMERA_DATA *const cam_data = (TRIGGER_CAMERA_DATA *)cmd->parameter; cam_data->timer = camera_timer; cam_data->glide = glide_timer << 3; OBJECT_VECTOR *const camera = Camera_GetFixedObject(cam_data->camera_num); camera->pos.x += camera_shift.x; camera->pos.y += camera_shift.y; camera->pos.z += camera_shift.z; break; } } static void M_InsertFloorData( const INJECTION *const injection, SECTOR *const sector) { const int32_t data_length = VFile_ReadS32(injection->fp); if (data_length < 0) { LOG_WARNING( "Skipping floor data insert with invalid length: %d", data_length); return; } if (data_length == 0) { Room_PopulateSectorData(sector, nullptr, NULL_FD_INDEX, NULL_FD_INDEX); return; } int16_t *data = Memory_Alloc(sizeof(int16_t) * data_length); VFile_Read(injection->fp, data, sizeof(int16_t) * data_length); if (sector == nullptr) { Memory_FreePointer(&data); return; } // This will reset all FD properties in the sector based on the raw data // imported. We pass a dummy null index to allow it to read from the // beginning of the array. Room_PopulateSectorData(sector, data, 0, NULL_FD_INDEX); Memory_FreePointer(&data); } static void M_RoomShift( const INJECTION *const injection, const int16_t room_num) { const uint32_t x_shift = ROUND_TO_SECTOR(VFile_ReadU32(injection->fp)); const uint32_t z_shift = ROUND_TO_SECTOR(VFile_ReadU32(injection->fp)); const int32_t y_shift = ROUND_TO_CLICK(VFile_ReadS32(injection->fp)); ROOM *const room = Room_Get(room_num); room->pos.x += x_shift; room->pos.z += z_shift; room->min_floor += y_shift; room->max_ceiling += y_shift; // Move any items in the room to match. for (int32_t i = 0; i < Item_GetTotalCount(); i++) { ITEM *const item = Item_Get(i); if (item->room_num != room_num) { continue; } item->pos.x += x_shift; item->pos.y += y_shift; item->pos.z += z_shift; } if (y_shift == 0) { return; } // Update the sector floor and ceiling heights to match. for (int32_t i = 0; i < room->size.z * room->size.x; i++) { SECTOR *const sector = &room->sectors[i]; if (sector->floor.height == NO_HEIGHT || sector->ceiling.height == NO_HEIGHT) { continue; } sector->floor.height += y_shift; sector->ceiling.height += y_shift; } // Update vertex Y values to match; x and z are room-relative. for (int32_t i = 0; i < room->mesh.num_vertices; i++) { room->mesh.vertices[i].pos.y += y_shift; } } static void M_TriggeredItem(const INJECTION *const injection) { if (Item_GetLevelCount() == MAX_ITEMS) { VFile_Skip( injection->fp, sizeof(int16_t) * 4 + sizeof(int32_t) * 3 + sizeof(uint16_t)); LOG_WARNING("Cannot add more than %d items", MAX_ITEMS); return; } const int16_t item_num = Item_CreateLevelItem(); ITEM *const item = Item_Get(item_num); const INJECTION_OBJECT_INFO obj_info = Inject_ReadObjectPtr(injection); item->object_id = obj_info.id; item->room_num = VFile_ReadS16(injection->fp); item->pos.x = VFile_ReadS32(injection->fp); item->pos.y = VFile_ReadS32(injection->fp); item->pos.z = VFile_ReadS32(injection->fp); item->rot.y = VFile_ReadS16(injection->fp); item->shade.value_1 = VFile_ReadS16(injection->fp); if (g_TRVersion >= 2) { item->shade.value_2 = item->shade.value_1; } item->flags = VFile_ReadU16(injection->fp); if (injection->version < INJ_VERSION_7) { return; } const int32_t name_length = VFile_ReadS32(injection->fp); if (name_length <= 0) { return; } if (name_length > 4096) { LOG_WARNING("Item name too long %d", name_length); VFile_Skip(injection->fp, name_length); return; } char *name = Memory_Alloc((size_t)(name_length + 1)); VFile_Read(injection->fp, name, name_length); name[name_length] = '\0'; Item_SetName(item_num, name); Memory_FreePointer(&name); } static void M_RoomProperties( const INJECTION *const injection, const int16_t room_num) { const uint16_t flags = VFile_ReadU16(injection->fp); ROOM *const room = Room_Get(room_num); // clang-format off room->flags.underwater = (flags & 0x01) != 0; room->flags.outside = (flags & 0x08) != 0; room->flags.dynamic_lit = (flags & 0x10) != 0; room->flags.wind = (flags & 0x20) != 0; room->flags.inside = (flags & 0x40) != 0; room->flags.swamp = (flags & 0x80) != 0; // clang-format on } static void M_SectorOverwrite( const INJECTION *const injection, SECTOR *const sector) { const uint16_t fd_idx = VFile_ReadU16(injection->fp); const int16_t box_idx = VFile_ReadS16(injection->fp); const int16_t pit_room = VFile_ReadS16(injection->fp); const int16_t floor = VFile_ReadS16(injection->fp); const int16_t sky_room = VFile_ReadS16(injection->fp); const int16_t ceiling = VFile_ReadS16(injection->fp); if (sector == nullptr) { return; } sector->idx = fd_idx; sector->box = box_idx; sector->portal_room.pit = pit_room; sector->floor.height = floor; sector->portal_room.sky = sky_room; sector->ceiling.height = ceiling; } static void M_FixZones( const INJECTION_CONTEXT *const ctx, const INJECTION *const injection, const SECTOR *const sector) { if (ctx->mode == INJECTION_MODE_STATS || sector == nullptr || sector->box == NO_BOX) { VFile_Skip( injection->fp, 2 * sizeof(int16_t) * (Box_GetZoneCount() + 1)); return; } const int16_t box_idx = sector->box; for (int32_t flip_status = 0; flip_status < 2; flip_status++) { for (int32_t zone_idx = 0; zone_idx < Box_GetZoneCount(); zone_idx++) { int16_t *const ground_zone = Box_GetGroundZone(flip_status, zone_idx); ground_zone[box_idx] = VFile_ReadS16(injection->fp); } int16_t *const fly_zone = Box_GetFlyZone(flip_status); fly_zone[box_idx] = VFile_ReadS16(injection->fp); } } static void M_SetSectorPortals( const INJECTION *const injection, SECTOR *const sector) { if (sector == nullptr) { VFile_Skip(injection->fp, 3 * sizeof(int16_t)); return; } sector->portal_room.wall = VFile_ReadS16(injection->fp); sector->portal_room.sky = VFile_ReadS16(injection->fp); sector->portal_room.pit = VFile_ReadS16(injection->fp); } static void M_SetSectorClimbability( const INJECTION *const injection, SECTOR *const sector) { const int32_t direction = VFile_ReadS32(injection->fp); if (sector != nullptr) { sector->ladder = (LADDER_DIRECTION)direction; } } static void M_SetSectorTriangulation( const INJECTION *const injection, SECTOR *const sector) { const int32_t type = VFile_ReadS32(injection->fp); #define L_TRIANGULATE(test_type, surface) \ do { \ if ((type & (1 << test_type)) != 0) { \ const int16_t func_data = VFile_ReadS16(injection->fp); \ const int16_t tilt_data = VFile_ReadS16(injection->fp); \ if (sector != nullptr) { \ sector->surface.tilt = (XZ_16) {}; \ Room_ReadTriangulation( \ §or->surface, func_data, tilt_data); \ } \ } \ } while (0) L_TRIANGULATE(SURFACE_FLOOR, floor); L_TRIANGULATE(SURFACE_CEILING, ceiling); #undef L_TRIANGULATE } static void M_FloorDataEdits( const INJECTION_CONTEXT *const ctx, const INJECTION *const injection, const int32_t data_count) { for (int32_t i = 0; i < data_count; i++) { const int16_t room_num = VFile_ReadS16(injection->fp); const uint16_t x = VFile_ReadU16(injection->fp); const uint16_t z = VFile_ReadU16(injection->fp); const int32_t fd_edit_count = VFile_ReadS32(injection->fp); // Verify that the given room and coordinates are accurate. // Individual FD functions must check that sector is actually set. const ROOM *room = nullptr; SECTOR *sector = nullptr; if (room_num < 0 || room_num >= Room_GetCount()) { LOG_WARNING("Room index %d is invalid", room_num); } else { room = Room_Get(room_num); if (x >= room->size.x || z >= room->size.z) { LOG_WARNING( "Sector [%d,%d] is invalid for room %d", x, z, room_num); } else { sector = Room_GetUnitSector(room, x, z); } } for (int32_t j = 0; j < fd_edit_count; j++) { const FLOOR_EDIT_TYPE edit_type = VFile_ReadS32(injection->fp); switch (edit_type) { case FET_TRIGGER_TYPE: M_TriggerTypeChange(injection, sector); break; case FET_TRIGGER_PARAM: M_TriggerParameterChange(injection, sector); break; case FET_MUSIC_ONESHOT: M_SetMusicOneShot(sector); break; case FET_GLIDE_CAMERA: M_FixGlideCamera(ctx, injection, sector); break; case FET_FD_INSERT: M_InsertFloorData(injection, sector); break; case FET_ROOM_SHIFT: M_RoomShift(injection, room_num); break; case FET_TRIGGER_ITEM: M_TriggeredItem(injection); break; case FET_ROOM_PROPERTIES: M_RoomProperties(injection, room_num); break; case FET_SECTOR_OVERWRITE: M_SectorOverwrite(injection, sector); break; case FET_ZONE_FIX: M_FixZones(ctx, injection, sector); break; case FET_PORTALS: M_SetSectorPortals(injection, sector); break; case FET_CLIMB: M_SetSectorClimbability(injection, sector); break; case FET_TRIANGULATE: M_SetSectorTriangulation(injection, sector); break; case FET_DELETE_TRIGGER: if (sector != nullptr) { sector->trigger = nullptr; } break; default: LOG_WARNING("Unknown floor data edit type: %d", edit_type); break; } } } } REGISTER_INJECT_EDITOR(IDT_FLOOR_EDITS, M_FloorDataEdits) ================================================ FILE: src/trx/game/inject/editors/items.c ================================================ #include #include #include #include static void M_ItemPosEdits( const INJECTION_CONTEXT *const ctx, const INJECTION *const injection, const int32_t data_count) { for (int32_t i = 0; i < data_count; i++) { const int16_t item_num = VFile_ReadS16(injection->fp); const int16_t y_rot = VFile_ReadS16(injection->fp); const GAME_VECTOR pos = { .x = VFile_ReadS32(injection->fp), .y = VFile_ReadS32(injection->fp), .z = VFile_ReadS32(injection->fp), .room_num = VFile_ReadS16(injection->fp), }; if (item_num < 0 || item_num >= Item_GetTotalCount()) { LOG_WARNING("Item number %d is out of level item range", item_num); continue; } ITEM *const item = Item_Get(item_num); item->rot.y = y_rot; item->pos = pos.pos; item->room_num = pos.room_num; } } static void M_ItemFlagEdits( const INJECTION_CONTEXT *const ctx, const INJECTION *const injection, const int32_t data_count) { for (int32_t i = 0; i < data_count; i++) { const int16_t item_num = VFile_ReadS16(injection->fp); const INJECTION_OBJECT_INFO obj_info = Inject_ReadObjectPtr(injection); const uint16_t flags = VFile_ReadU16(injection->fp); if (item_num < 0 || item_num >= Item_GetTotalCount()) { LOG_WARNING("Item number %d is out of level item range", item_num); continue; } ITEM *const item = Item_Get(item_num); item->object_id = obj_info.id; item->flags = flags; } } static void M_CameraEdits( const INJECTION_CONTEXT *const ctx, const INJECTION *const injection, const int32_t data_count) { for (int32_t i = 0; i < data_count; i++) { const int16_t camera_num = VFile_ReadS16(injection->fp); const XYZ_32 pos = { .x = VFile_ReadS32(injection->fp), .y = VFile_ReadS32(injection->fp), .z = VFile_ReadS32(injection->fp), }; const int16_t room_num = VFile_ReadS16(injection->fp); const int16_t flags = VFile_ReadS16(injection->fp); if (camera_num < 0 || camera_num >= Camera_GetFixedObjectCount()) { LOG_WARNING( "Camera number %d is out of level camera range", camera_num); continue; } OBJECT_VECTOR *const camera = Camera_GetFixedObject(camera_num); camera->pos = pos; camera->data = room_num; camera->flags = flags; } } REGISTER_INJECT_EDITOR(IDT_ITEM_POS_EDITS, M_ItemPosEdits) REGISTER_INJECT_EDITOR(IDT_ITEM_FLAG_EDITS, M_ItemFlagEdits) REGISTER_INJECT_EDITOR(IDT_CAMERA_EDITS, M_CameraEdits) ================================================ FILE: src/trx/game/inject/editors/meshes.c ================================================ #include #include #include #include #include #include typedef struct { INJECTION_OBJECT_INFO obj_info; int16_t source_identifier; FACE_TYPE face_type; int16_t face_index; int32_t target_count; int16_t *targets; } FACE_EDIT; typedef struct { int16_t index; XYZ_16 shift; } VERTEX_EDIT; typedef struct { INJECTION_OBJECT_INFO obj_info; int16_t mesh_idx; XYZ_16 centre_shift; int32_t radius_shift; int32_t face_edit_count; int32_t vertex_edit_count; FACE_EDIT *face_edits; VERTEX_EDIT *vertex_edits; } MESH_EDIT; static BOUNDS_16 M_ReadBounds16(VFILE *const file) { BOUNDS_16 bounds = {}; bounds.min.x = VFile_ReadS16(file); bounds.max.x = VFile_ReadS16(file); bounds.min.y = VFile_ReadS16(file); bounds.max.y = VFile_ReadS16(file); bounds.min.z = VFile_ReadS16(file); bounds.max.z = VFile_ReadS16(file); return bounds; } static uint16_t *M_GetMeshTexture(const FACE_EDIT *const edit) { const OBJECT *const obj = Object_Get(edit->obj_info.id); if (!obj->loaded) { return nullptr; } ASSERT(edit->source_identifier >= 0); ASSERT(edit->source_identifier < obj->mesh_count); const OBJECT_MESH *const mesh = Object_GetMesh(obj->mesh_idx + edit->source_identifier); if (edit->face_type == FT_TEXTURED_QUAD) { ASSERT(edit->face_index >= 0); ASSERT(edit->face_index < mesh->tex_face4s.count); FACE *const face = &mesh->tex_face4s.data[edit->face_index]; return &face->texture_idx; } if (edit->face_type == FT_TEXTURED_TRIANGLE) { ASSERT(edit->face_index >= 0); ASSERT(edit->face_index < mesh->tex_face3s.count); FACE *const face = &mesh->tex_face3s.data[edit->face_index]; return &face->texture_idx; } if (edit->face_type == FT_COLOURED_QUAD) { ASSERT(edit->face_index >= 0); ASSERT(edit->face_index < mesh->flat_face4s.count); FACE *const face = &mesh->flat_face4s.data[edit->face_index]; return &face->palette_idx; } if (edit->face_type == FT_COLOURED_TRIANGLE) { ASSERT(edit->face_index >= 0); ASSERT(edit->face_index < mesh->flat_face3s.count); FACE *const face = &mesh->flat_face3s.data[edit->face_index]; return &face->palette_idx; } return nullptr; } static void M_ApplyFaceEdit( const FACE_EDIT *const edit, FACE *const faces, const int32_t face_count, const uint16_t texture) { for (int32_t i = 0; i < edit->target_count; i++) { ASSERT(edit->targets[i] >= 0); ASSERT(edit->targets[i] < face_count); FACE *const face = &faces[edit->targets[i]]; face->texture_idx = texture; } } static void M_ApplyMeshEdit(const MESH_EDIT *const edit) { OBJECT_MESH *mesh; if (edit->obj_info.type == OBJ_TYPE_OBJECT) { const OBJECT *const obj = Object_Get(edit->obj_info.id); if (!obj->loaded) { return; } ASSERT(edit->mesh_idx >= 0); ASSERT(edit->mesh_idx < obj->mesh_count); mesh = Object_GetMesh(obj->mesh_idx + edit->mesh_idx); } else if (edit->obj_info.type == OBJ_TYPE_STATIC3D) { if (edit->obj_info.id < 0 || edit->obj_info.id >= Object_GetStaticObjects3DCount()) { return; } const STATIC_OBJECT_3D *const obj = Object_Get3DStatic(edit->obj_info.id); mesh = Object_GetMesh(obj->mesh_idx); } else { LOG_WARNING("Invalid mesh edit type %d", edit->obj_info.type); return; } mesh->center.x += edit->centre_shift.x; mesh->center.y += edit->centre_shift.y; mesh->center.z += edit->centre_shift.z; mesh->radius += edit->radius_shift; for (int32_t i = 0; i < edit->vertex_edit_count; i++) { const VERTEX_EDIT *const vertex_edit = &edit->vertex_edits[i]; if (vertex_edit->index < 0 || vertex_edit->index >= mesh->num_vertices) { const int32_t object_id = edit->obj_info.type == OBJ_TYPE_OBJECT ? Object_ToGameID(edit->obj_info.id) : edit->obj_info.id; LOG_ERROR( "Invalid mesh vertex edit: obj_type=%d obj_id=%d mesh_idx=%d " "vertex_idx=%d num_vertices=%d", edit->obj_info.type, object_id, edit->mesh_idx, vertex_edit->index, mesh->num_vertices); } ASSERT(vertex_edit->index >= 0); ASSERT(vertex_edit->index < mesh->num_vertices); XYZ_16 *const vertex = &mesh->vertices[vertex_edit->index]; vertex->x += vertex_edit->shift.x; vertex->y += vertex_edit->shift.y; vertex->z += vertex_edit->shift.z; } // Find each face we are interested in and replace its texture // or palette reference with the one selected from each edit's // instructions. for (int32_t i = 0; i < edit->face_edit_count; i++) { const FACE_EDIT *const face_edit = &edit->face_edits[i]; uint16_t texture; if (face_edit->source_identifier < 0) { texture = Inject_GetPaletteIndex(-face_edit->source_identifier); } else { const uint16_t *const tex_ptr = M_GetMeshTexture(face_edit); if (tex_ptr == nullptr) { continue; } texture = *tex_ptr; } switch (face_edit->face_type) { case FT_TEXTURED_QUAD: M_ApplyFaceEdit( face_edit, mesh->tex_face4s.data, mesh->tex_face4s.count, texture); break; case FT_TEXTURED_TRIANGLE: M_ApplyFaceEdit( face_edit, mesh->tex_face3s.data, mesh->tex_face3s.count, texture); break; case FT_COLOURED_QUAD: M_ApplyFaceEdit( face_edit, mesh->flat_face4s.data, mesh->flat_face4s.count, texture); break; case FT_COLOURED_TRIANGLE: M_ApplyFaceEdit( face_edit, mesh->flat_face3s.data, mesh->flat_face3s.count, texture); break; } } } static void M_MeshEdits( const INJECTION_CONTEXT *const ctx, const INJECTION *const injection, const int32_t data_count) { for (int32_t i = 0; i < data_count; i++) { MESH_EDIT edit = { .obj_info = Inject_ReadObjectPtr(injection), .mesh_idx = VFile_ReadS16(injection->fp), .centre_shift.x = VFile_ReadS16(injection->fp), .centre_shift.y = VFile_ReadS16(injection->fp), .centre_shift.z = VFile_ReadS16(injection->fp), .radius_shift = VFile_ReadS32(injection->fp), }; edit.face_edit_count = VFile_ReadS32(injection->fp); edit.face_edits = Memory_Alloc(sizeof(FACE_EDIT) * edit.face_edit_count); for (int32_t j = 0; j < edit.face_edit_count; j++) { FACE_EDIT *const face_edit = &edit.face_edits[j]; face_edit->obj_info = Inject_ReadObjectPtr(injection); face_edit->source_identifier = VFile_ReadS16(injection->fp); face_edit->face_type = VFile_ReadS32(injection->fp); face_edit->face_index = VFile_ReadS16(injection->fp); face_edit->target_count = VFile_ReadS32(injection->fp); face_edit->targets = Memory_Alloc(sizeof(int16_t) * face_edit->target_count); VFile_Read( injection->fp, face_edit->targets, sizeof(int16_t) * face_edit->target_count); } edit.vertex_edit_count = VFile_ReadS32(injection->fp); edit.vertex_edits = Memory_Alloc(sizeof(VERTEX_EDIT) * edit.vertex_edit_count); for (int32_t j = 0; j < edit.vertex_edit_count; j++) { VERTEX_EDIT *vertex_edit = &edit.vertex_edits[j]; vertex_edit->index = VFile_ReadS16(injection->fp); vertex_edit->shift.x = VFile_ReadS16(injection->fp); vertex_edit->shift.y = VFile_ReadS16(injection->fp); vertex_edit->shift.z = VFile_ReadS16(injection->fp); } if (ctx->mode != INJECTION_MODE_STATS) { M_ApplyMeshEdit(&edit); } for (int32_t j = 0; j < edit.face_edit_count; j++) { FACE_EDIT *face_edit = &edit.face_edits[j]; Memory_FreePointer(&face_edit->targets); } Memory_FreePointer(&edit.face_edits); Memory_FreePointer(&edit.vertex_edits); } } static void M_Object3DEdits( const INJECTION_CONTEXT *const ctx, const INJECTION *const injection, const int32_t data_count) { for (int32_t i = 0; i < data_count; i++) { const int32_t obj_id = VFile_ReadS32(injection->fp); const bool collidable = VFile_ReadU8(injection->fp) == 1; const bool visible = VFile_ReadU8(injection->fp) == 1; const BOUNDS_16 collision_bounds = M_ReadBounds16(injection->fp); const BOUNDS_16 draw_bounds = M_ReadBounds16(injection->fp); if (obj_id < 0 || obj_id >= Object_GetStaticObjects3DCount()) { continue; } STATIC_OBJECT_3D *const obj = Object_Get3DStatic(obj_id); if (!obj->loaded) { continue; } obj->collidable = collidable; obj->visible = visible; obj->collision_bounds = collision_bounds; obj->draw_bounds = draw_bounds; } } REGISTER_INJECT_EDITOR(IDT_MESH_EDITS, M_MeshEdits) REGISTER_INJECT_EDITOR(IDT_OBJECT_3D_EDITS, M_Object3DEdits) ================================================ FILE: src/trx/game/inject/editors/objects.c ================================================ #include #include static void M_ObjectTypeEdits( const INJECTION_CONTEXT *const ctx, const INJECTION *const injection, const int32_t data_count) { for (int32_t i = 0; i < data_count; i++) { const INJECTION_OBJECT_INFO base_obj_info = Inject_ReadObjectPtr(injection); const INJECTION_OBJECT_INFO target_obj_info = Inject_ReadObjectPtr(injection); OBJECT *const base_obj = Object_TryGet(base_obj_info.id); const OBJECT *const target_obj = Object_TryGet(target_obj_info.id); if (base_obj == nullptr || target_obj == nullptr) { continue; } base_obj->setup_func = target_obj->setup_func; } } REGISTER_INJECT_EDITOR(IDT_OBJ_TYPE_EDITS, M_ObjectTypeEdits) ================================================ FILE: src/trx/game/inject/editors/rooms.c ================================================ #include #include #include #include #include static uint16_t *M_GetRoomTexture( const int16_t room_num, const FACE_TYPE face_type, const int16_t face_index) { const ROOM *const room = Room_Get(room_num); if (face_type == FT_TEXTURED_QUAD && face_index < room->mesh.face4s.count) { FACE *const face = &room->mesh.face4s.data[face_index]; return &face->texture_idx; } if (face_type == FT_TEXTURED_TRIANGLE && face_index < room->mesh.face3s.count) { FACE *const face = &room->mesh.face3s.data[face_index]; return &face->texture_idx; } LOG_WARNING( "Invalid room face lookup: %d, %d, %d", room_num, face_type, face_index); return nullptr; } static void M_TextureRoomFace(const INJECTION *const injection) { const int16_t target_room = VFile_ReadS16(injection->fp); const FACE_TYPE target_face_type = VFile_ReadS32(injection->fp); const int16_t target_face = VFile_ReadS16(injection->fp); const int16_t source_room = VFile_ReadS16(injection->fp); const FACE_TYPE source_face_type = VFile_ReadS32(injection->fp); const int16_t source_face = VFile_ReadS16(injection->fp); const uint16_t *const source_texture = M_GetRoomTexture(source_room, source_face_type, source_face); uint16_t *const target_texture = M_GetRoomTexture(target_room, target_face_type, target_face); if (source_texture != nullptr && target_texture != nullptr) { *target_texture = *source_texture; } } static uint16_t *M_GetRoomFaceVertices( const int16_t room_num, const FACE_TYPE face_type, const int16_t face_index) { if (room_num < 0 || room_num >= Room_GetCount()) { LOG_WARNING("Room index %d is invalid", room_num); return nullptr; } const ROOM *const room = Room_Get(room_num); if (face_type == FT_TEXTURED_QUAD) { if (face_index < 0 || face_index >= room->mesh.face4s.count) { LOG_WARNING( "Face4 index %d, room %d is invalid", face_index, room_num); return nullptr; } FACE *const face = &room->mesh.face4s.data[face_index]; return (uint16_t *)(void *)&face->vertices; } if (face_type == FT_TEXTURED_TRIANGLE) { if (face_index < 0 || face_index >= room->mesh.face3s.count) { LOG_WARNING( "Face3 index %d, room %d is invalid", face_index, room_num); return nullptr; } FACE *const face = &room->mesh.face3s.data[face_index]; return (uint16_t *)(void *)&face->vertices; } return nullptr; } static void M_MoveRoomFace(const INJECTION *const injection) { const int16_t target_room = VFile_ReadS16(injection->fp); const FACE_TYPE face_type = VFile_ReadS32(injection->fp); const int16_t target_face = VFile_ReadS16(injection->fp); const int32_t vertex_count = VFile_ReadS32(injection->fp); for (int32_t j = 0; j < vertex_count; j++) { const int16_t vertex_index = VFile_ReadS16(injection->fp); const int16_t new_vertex = VFile_ReadS16(injection->fp); uint16_t *const vertices = M_GetRoomFaceVertices(target_room, face_type, target_face); if (vertices != nullptr) { vertices[vertex_index] = new_vertex; } } } static void M_AlterRoomVertex(const INJECTION *const injection) { const int16_t target_room = VFile_ReadS16(injection->fp); VFile_Skip(injection->fp, sizeof(int32_t)); const int16_t target_vertex = VFile_ReadS16(injection->fp); const int16_t x_change = VFile_ReadS16(injection->fp); const int16_t y_change = VFile_ReadS16(injection->fp); const int16_t z_change = VFile_ReadS16(injection->fp); const int16_t shade_change = VFile_ReadS16(injection->fp); if (target_room < 0 || target_room >= Room_GetCount()) { LOG_WARNING("Room index %d is invalid", target_room); return; } const ROOM *const room = Room_Get(target_room); if (target_vertex < 0 || target_vertex >= room->mesh.num_vertices) { LOG_WARNING( "Vertex index %d, room %d is invalid", target_vertex, target_room); return; } ROOM_VERTEX *const vertex = &room->mesh.vertices[target_vertex]; vertex->pos.x += x_change; vertex->pos.y += y_change; vertex->pos.z += z_change; vertex->light_base += shade_change; } static void M_SetVertexFlags(const INJECTION *const injection) { const int16_t target_room = VFile_ReadS16(injection->fp); VFile_Skip(injection->fp, sizeof(int32_t)); const int16_t target_vertex = VFile_ReadS16(injection->fp); const uint16_t flags = VFile_ReadU16(injection->fp); if (target_room < 0 || target_room >= Room_GetCount()) { LOG_WARNING("Room index %d is invalid", target_room); return; } const ROOM *const room = Room_Get(target_room); if (target_vertex < 0 || target_vertex >= room->mesh.num_vertices) { LOG_WARNING( "Vertex index %d, room %d is invalid", target_vertex, target_room); return; } ROOM_VERTEX *const vertex = &room->mesh.vertices[target_vertex]; if (g_TRVersion == 1) { vertex->flags.disable_wibble = (flags & 0x2000u) != 0u; } else { vertex->flags.disable_wibble = (flags & 0x8000u) != 0u; vertex->light_table_value = flags & 0xFF; } } static void M_RotateRoomFace(const INJECTION *const injection) { const int16_t target_room = VFile_ReadS16(injection->fp); const FACE_TYPE face_type = VFile_ReadS32(injection->fp); const int16_t target_face = VFile_ReadS16(injection->fp); const uint8_t num_rotations = VFile_ReadU8(injection->fp); uint16_t *const face_vertices = M_GetRoomFaceVertices(target_room, face_type, target_face); if (face_vertices == nullptr) { return; } const int32_t num_vertices = face_type == FT_TEXTURED_QUAD ? 4 : 3; uint16_t *vertices[num_vertices]; for (int32_t i = 0; i < num_vertices; i++) { vertices[i] = face_vertices + i; } for (int32_t i = 0; i < num_rotations; i++) { const uint16_t first = *vertices[0]; for (int32_t j = 0; j < num_vertices - 1; j++) { *vertices[j] = *vertices[j + 1]; } *vertices[num_vertices - 1] = first; } } static void M_AddRoomFace(const INJECTION *const injection) { const int16_t target_room = VFile_ReadS16(injection->fp); const FACE_TYPE face_type = VFile_ReadS32(injection->fp); const int16_t source_room = VFile_ReadS16(injection->fp); const int16_t source_face = VFile_ReadS16(injection->fp); const int32_t num_vertices = face_type == FT_TEXTURED_QUAD ? 4 : 3; uint16_t vertices[num_vertices]; for (int32_t i = 0; i < num_vertices; i++) { vertices[i] = VFile_ReadU16(injection->fp); } if (target_room < 0 || target_room >= Room_GetCount()) { LOG_WARNING("Room index %d is invalid", target_room); return; } const uint16_t *const source_texture = M_GetRoomTexture(source_room, face_type, source_face); if (source_texture == nullptr) { return; } ROOM *const room = Room_Get(target_room); uint16_t *face_vertices; if (face_type == FT_TEXTURED_QUAD) { FACE *const face = &room->mesh.face4s.data[room->mesh.face4s.count]; face->texture_idx = *source_texture; for (int32_t i = 0; i < face->vertex_count; i++) { face->texture_zw[i].z = 1.0f; face->texture_zw[i].w = 1.0f; } face_vertices = face->vertices; room->mesh.face4s.count++; } else { FACE *const face = &room->mesh.face3s.data[room->mesh.face3s.count]; face->vertex_count = 3; for (int32_t i = 0; i < face->vertex_count; i++) { face->texture_zw[i].z = 1.0f; face->texture_zw[i].w = 1.0f; } face->texture_idx = *source_texture; face_vertices = face->vertices; room->mesh.face3s.count++; } for (int32_t i = 0; i < num_vertices; i++) { face_vertices[i] = vertices[i]; } } static void M_AddRoomVertex(const INJECTION *const injection) { const int16_t target_room = VFile_ReadS16(injection->fp); VFile_Skip(injection->fp, sizeof(int32_t)); const XYZ_16 pos = { .x = VFile_ReadS16(injection->fp), .y = VFile_ReadS16(injection->fp), .z = VFile_ReadS16(injection->fp), }; int16_t shade = 0; RGBA_8888 color = COLOR_RGBA_8888_WHITE; if (g_TRVersion < 3) { shade = VFile_ReadS16(injection->fp); } else { color = Color_ARGB1555ToRGBA8888(VFile_ReadU16(injection->fp)); color.a = 255; } ROOM *const room = Room_Get(target_room); ROOM_VERTEX *const vertex = &room->mesh.vertices[room->mesh.num_vertices]; vertex->pos = pos; vertex->light_base = shade; vertex->flags.disable_wibble = false; vertex->flags.move = false; vertex->flags.glow = false; vertex->light_table_value = 0; vertex->color = color; room->mesh.num_vertices++; } static void M_AddRoomStatic2D(const INJECTION *const injection) { const int16_t target_room = VFile_ReadS16(injection->fp); VFile_Skip(injection->fp, sizeof(int32_t)); const int32_t id = VFile_ReadS32(injection->fp); const uint16_t vertex = VFile_ReadU16(injection->fp); const uint16_t frame_idx = VFile_ReadU16(injection->fp); const STATIC_OBJECT_2D *const obj = Object_Get2DStatic(id); if (obj == nullptr) { LOG_WARNING("Invalid static 2D id: %d", id); return; } if (!obj->loaded) { LOG_WARNING("Static 2D %d is not loaded", id); return; } if (frame_idx >= obj->frame_count) { LOG_WARNING("Invalid frame (%d) on static 2D %d", frame_idx, id); return; } ROOM *const room = Room_Get(target_room); ROOM_SPRITE *const sprite = &room->mesh.sprites.data[room->mesh.sprites.count]; sprite->vertex = vertex; sprite->texture = obj->texture_idx + frame_idx; room->mesh.sprites.count++; } static void M_AddRoomStatic3D(const INJECTION *const injection) { const int16_t target_room = VFile_ReadS16(injection->fp); VFile_Skip(injection->fp, sizeof(int32_t)); ROOM *const room = Room_Get(target_room); STATIC_MESH *const mesh = &room->static_meshes[room->num_static_meshes]; mesh->pos.x = VFile_ReadS32(injection->fp); mesh->pos.y = VFile_ReadS32(injection->fp); mesh->pos.z = VFile_ReadS32(injection->fp); mesh->rot.y = VFile_ReadS16(injection->fp); mesh->shade.value_1 = VFile_ReadS16(injection->fp); if (g_TRVersion >= 2) { mesh->shade.value_2 = mesh->shade.value_1; } mesh->static_num = VFile_ReadS16(injection->fp); mesh->draw_num = -1; room->num_static_meshes++; } static void M_EditRoomStatic3D(const INJECTION *const injection) { const int16_t target_room = VFile_ReadS16(injection->fp); VFile_Skip(injection->fp, sizeof(int32_t)); const ROOM *const room = Room_Get(target_room); const int32_t mesh_idx = VFile_ReadS32(injection->fp); if (mesh_idx < 0 || mesh_idx >= room->num_static_meshes) { LOG_WARNING( "Invalid static mesh index (%d) for room %d", mesh_idx, target_room); VFile_Skip(injection->fp, 4 * sizeof(int32_t)); return; } STATIC_MESH *const mesh = &room->static_meshes[mesh_idx]; mesh->pos.x = VFile_ReadS32(injection->fp); mesh->pos.y = VFile_ReadS32(injection->fp); mesh->pos.z = VFile_ReadS32(injection->fp); mesh->rot.y = VFile_ReadS16(injection->fp); mesh->shade.value_1 = VFile_ReadS16(injection->fp); if (g_TRVersion >= 2) { mesh->shade.value_2 = mesh->shade.value_1; } } static void M_RoomMeshEdits( const INJECTION_CONTEXT *const ctx, const INJECTION *const injection, const int32_t data_count) { for (int32_t i = 0; i < data_count; i++) { const ROOM_MESH_EDIT_TYPE type = VFile_ReadS32(injection->fp); switch (type) { case RMET_TEXTURE_FACE: M_TextureRoomFace(injection); break; case RMET_MOVE_FACE: M_MoveRoomFace(injection); break; case RMET_ALTER_VERTEX: M_AlterRoomVertex(injection); break; case RMET_VERTEX_FLAGS: M_SetVertexFlags(injection); break; case RMET_ROTATE_FACE: M_RotateRoomFace(injection); break; case RMET_ADD_FACE: M_AddRoomFace(injection); break; case RMET_ADD_VERTEX: M_AddRoomVertex(injection); break; case RMET_ADD_STATIC_2D: M_AddRoomStatic2D(injection); break; case RMET_ADD_STATIC_3D: M_AddRoomStatic3D(injection); break; case RMET_EDIT_STATIC_3D: M_EditRoomStatic3D(injection); break; default: LOG_WARNING("Unrecognised room mesh edit type: %d", type); break; } } } static void M_RoomPortalEdits( const INJECTION_CONTEXT *const ctx, const INJECTION *const injection, const int32_t data_count) { for (int32_t i = 0; i < data_count; i++) { const int16_t base_room = VFile_ReadS16(injection->fp); const int16_t link_room = VFile_ReadS16(injection->fp); const int16_t portal_index = VFile_ReadS16(injection->fp); if (base_room < 0 || base_room >= Room_GetCount()) { VFile_Skip(injection->fp, sizeof(int16_t) * 12); LOG_WARNING("Room index %d is invalid", base_room); continue; } const ROOM *const room = Room_Get(base_room); PORTAL *portal = nullptr; for (int32_t j = 0; j < room->portals->count; j++) { const PORTAL room_portal = room->portals->portal[j]; if (room_portal.room_num == link_room && j == portal_index) { portal = &room->portals->portal[j]; break; } } if (portal == nullptr) { VFile_Skip(injection->fp, sizeof(int16_t) * 12); LOG_WARNING( "Room index %d has no matching portal to %d", base_room, link_room); continue; } bool empty_portal = true; for (int32_t j = 0; j < 4; j++) { portal->vertex[j].x += VFile_ReadS16(injection->fp); portal->vertex[j].y += VFile_ReadS16(injection->fp); portal->vertex[j].z += VFile_ReadS16(injection->fp); empty_portal &= portal->vertex[j].x == 0 && portal->vertex[j].y == 0 && portal->vertex[j].z == 0; } // An injection that has reset all vertices such that the portal size is // now 0, should be interpreted as a command to ignore that portal. if (empty_portal) { for (int32_t j = portal_index + 1; j < room->portals->count; j++) { room->portals->portal[j - 1] = room->portals->portal[j]; } room->portals->portal[room->portals->count - 1] = *portal; room->portals->count--; } } } REGISTER_INJECT_EDITOR(IDT_ROOM_EDITS, M_RoomMeshEdits) REGISTER_INJECT_EDITOR(IDT_VIS_PORTAL_EDITS, M_RoomPortalEdits) ================================================ FILE: src/trx/game/inject/editors/textures.c ================================================ #include #include #include #include #include static void M_TextureEdits( const INJECTION_CONTEXT *const ctx, const INJECTION *const injection, const int32_t data_count) { const LEVEL_CONTEXT_INFO *const level_info = Level_Context_GetInfo(); for (int32_t i = 0; i < data_count; i++) { const uint16_t target_page = VFile_ReadU16(injection->fp); const uint8_t target_x = VFile_ReadU8(injection->fp); const uint8_t target_y = VFile_ReadU8(injection->fp); const uint16_t source_width = VFile_ReadU16(injection->fp); const uint16_t source_height = VFile_ReadU16(injection->fp); const int32_t size = source_width * source_height * sizeof(RGBA_8888); RGBA_8888 *source_img = Memory_Alloc(size); VFile_Read(injection->fp, source_img, size); if (target_page >= level_info->textures.page_count) { LOG_WARNING("Texture page %d is beyond level range", target_page); continue; } RGBA_8888 *page = &level_info->textures.pages_32[target_page * TEXTURE_PAGE_SIZE]; for (int32_t y = 0; y < source_height; y++) { for (int32_t x = 0; x < source_width; x++) { const int32_t target_pixel = (y + target_y) * TEXTURE_PAGE_WIDTH + x + target_x; RGBA_8888 *const target_rgb = &page[target_pixel]; *target_rgb = source_img[y * source_width + x]; } } Memory_FreePointer(&source_img); } } static void M_SpriteEdits( const INJECTION_CONTEXT *const ctx, const INJECTION *const injection, const int32_t data_count) { for (int32_t i = 0; i < data_count; i++) { const INJECTION_OBJECT_INFO obj_info = Inject_ReadObjectPtr(injection); int16_t x0 = VFile_ReadS16(injection->fp); int16_t y0 = VFile_ReadS16(injection->fp); int16_t x1 = VFile_ReadS16(injection->fp); int16_t y1 = VFile_ReadS16(injection->fp); const OBJECT *const obj = Object_TryGet(obj_info.id); if (obj == nullptr || !obj->loaded) { continue; } if (obj->mesh_idx < 0 || obj->mesh_idx >= Output_GetSpriteTextureCount()) { LOG_WARNING( "Invalid sprite texture index %d for object %d", obj->mesh_idx, obj_info.id); continue; } SPRITE_TEXTURE *const sprite_texture = Output_GetSpriteTexture(obj->mesh_idx); sprite_texture->x0 = x0; sprite_texture->x1 = x1; sprite_texture->y0 = y0; sprite_texture->y1 = y1; } } static void M_AnimTextureEdits( const INJECTION_CONTEXT *const ctx, const INJECTION *const injection, const int32_t data_count) { for (int32_t i = 0; i < data_count; i++) { const int32_t range_idx = VFile_ReadS32(injection->fp); const int32_t num_textures = VFile_ReadS32(injection->fp); if (num_textures <= 0) { continue; } ANIMATED_TEXTURE_RANGE *const range = Output_GetAnimatedTextureRange(range_idx); const size_t range_size = num_textures * sizeof(int16_t); if (range->num_textures == num_textures) { VFile_Read(injection->fp, range->textures, range_size); } else { LOG_WARNING( "Expected %d textures for animation range %d; got %d", range->num_textures, range_idx, num_textures); VFile_Skip(injection->fp, range_size); } } } REGISTER_INJECT_EDITOR(IDT_TEXTURE_EDITS, M_TextureEdits) REGISTER_INJECT_EDITOR(IDT_SPRITE_EDITS, M_SpriteEdits) REGISTER_INJECT_EDITOR(IDT_ANIM_TEXTURES, M_AnimTextureEdits) ================================================ FILE: src/trx/game/inject/enum.h ================================================ #pragma once // clang-format off typedef enum { INJECTION_MODE_STATS, INJECTION_MODE_FULL, } INJECTION_MODE; typedef enum { INJ_VERSION_1 = 1, INJ_VERSION_2 = 2, INJ_VERSION_3 = 3, INJ_VERSION_4 = 4, INJ_VERSION_5 = 5, INJ_VERSION_6 = 6, INJ_VERSION_7 = 7, INJ_CURRENT_VERSION = INJ_VERSION_7, } INJECTION_VERSION; typedef enum { IFT_GENERAL = 0, IFT_BRAID = 1, IFT_TEXTURE_FIX = 2, IFT_PS1_SFX = 3, IFT_FLOOR_DATA = 4, IFT_LARA_ANIMS = 5, IFT_ITEM_POSITION = 6, IFT_PS1_ENEMY = 7, IFT_ALTER_ANIM_SPRITE = 8, IFT_SKYBOX = 9, IFT_PS1_CRYSTAL = 10, IFT_NUMBER_OF = 11, } INJECTION_FILE_TYPE; typedef enum { ITT_ITEM_META = 0, ITT_ROOM_COUNT = 1, ITT_ROOM_META = 2, ITT_NUMBER_OF = 3, } INJECTION_TEST_TYPE; typedef enum { ICT_TEXTURE_DATA = 0, ICT_TEXTURE_INFO = 1, ICT_MESH_DATA = 2, ICT_ANIMATION_DATA = 3, ICT_OBJECT_DATA = 4, ICT_SFX_DATA = 5, ICT_DATA_EDITS = 6, ICT_CAMERA_DATA = 7, ICT_NUMBER_OF = 8, } INJECTION_CHUNK_TYPE; typedef enum { IDT_PALETTE = 0, IDT_TEXTURE_PAGES = 1, IDT_OBJECT_TEXTURES = 2, IDT_SPRITE_TEXTURES = 3, IDT_SPRITE_SEQUENCES = 4, IDT_OBJECT_MESHES = 5, IDT_MESH_POINTERS = 6, IDT_ANIM_CHANGES = 7, IDT_ANIM_RANGES = 8, IDT_ANIM_COMMANDS = 9, IDT_ANIM_BONES = 10, IDT_ANIM_FRAMES = 11, IDT_ANIMS = 12, IDT_OBJECTS = 13, IDT_SAMPLE_INFOS = 14, IDT_SAMPLE_INDICES = 15, IDT_SAMPLE_DATA = 16, IDT_FLOOR_EDITS = 17, IDT_ITEM_POS_EDITS = 18, IDT_MESH_EDITS = 19, IDT_TEXTURE_EDITS = 20, IDT_ROOM_EDIT_META = 21, IDT_ROOM_EDITS = 22, IDT_VIS_PORTAL_EDITS = 23, IDT_CAMERA_EDITS = 24, IDT_FRAME_EDITS = 25, IDT_OBJECT_3D_EDITS = 26, IDT_ANIM_CMD_EDITS = 27, IDT_SPRITE_EDITS = 28, IDT_STATIC_OBJECTS = 29, IDT_CINEMATIC_FRAMES = 30, IDT_OBJ_TYPE_EDITS = 31, IDT_FRAME_REPLACE = 32, IDT_ITEM_FLAG_EDITS = 33, IDT_ANIM_EDITS = 34, IDT_ANIM_TEXTURES = 35, IDT_NUMBER_OF = 36, } INJECTION_DATA_TYPE; typedef enum { FT_TEXTURED_QUAD = 0, FT_TEXTURED_TRIANGLE = 1, FT_COLOURED_QUAD = 2, FT_COLOURED_TRIANGLE = 3, } FACE_TYPE; typedef enum { FET_TRIGGER_PARAM = 0, FET_MUSIC_ONESHOT = 1, FET_FD_INSERT = 2, FET_ROOM_SHIFT = 3, FET_TRIGGER_ITEM = 4, FET_ROOM_PROPERTIES = 5, FET_TRIGGER_TYPE = 6, FET_SECTOR_OVERWRITE = 7, FET_GLIDE_CAMERA = 8, FET_ZONE_FIX = 9, FET_PORTALS = 10, FET_CLIMB = 11, FET_DELETE_TRIGGER = 12, FET_TRIANGULATE = 13, } FLOOR_EDIT_TYPE; typedef enum { RMET_TEXTURE_FACE = 0, RMET_MOVE_FACE = 1, RMET_ALTER_VERTEX = 2, RMET_ROTATE_FACE = 3, RMET_ADD_FACE = 4, RMET_ADD_VERTEX = 5, RMET_ADD_STATIC_2D = 6, RMET_ADD_STATIC_3D = 7, RMET_EDIT_STATIC_3D = 8, RMET_VERTEX_FLAGS = 9, } ROOM_MESH_EDIT_TYPE; // clang-format on typedef enum { OBJ_TYPE_OBJECT = 0, OBJ_TYPE_STATIC2D = 1, OBJ_TYPE_STATIC3D = 2, } INJECT_OBJECT_TYPE; ================================================ FILE: src/trx/game/inject/testers/items.c ================================================ #include #include #include static bool M_TestItemMeta( const INJECTION_CONTEXT *const ctx, const INJECTION *const injection) { const int32_t item_num = VFile_ReadS32(injection->fp); const INJECTION_OBJECT_INFO obj_info = Inject_ReadObjectPtr(injection); const XYZ_32 pos = { .x = VFile_ReadS32(injection->fp), .y = VFile_ReadS32(injection->fp), .z = VFile_ReadS32(injection->fp), }; const int16_t room_num = VFile_ReadS16(injection->fp); const int16_t y_rot = VFile_ReadS16(injection->fp); if (item_num < 0 || item_num >= Item_GetTotalCount()) { return false; } const ITEM *const item = Item_Get(item_num); return item->object_id == obj_info.id && XYZ_32_AreEquivalent(item->pos, pos) && item->room_num == room_num && item->rot.y == y_rot; } REGISTER_INJECT_TESTER(ITT_ITEM_META, M_TestItemMeta) ================================================ FILE: src/trx/game/inject/testers/rooms.c ================================================ #include #include static bool M_TestRoomCount( const INJECTION_CONTEXT *const ctx, const INJECTION *const injection) { const int32_t num_rooms = VFile_ReadS32(injection->fp); return num_rooms == Room_GetCount(); } static bool M_TestRoomMeta( const INJECTION_CONTEXT *const ctx, const INJECTION *const injection) { const int32_t room_num = VFile_ReadS32(injection->fp); const int32_t x_pos = VFile_ReadS32(injection->fp); const int32_t z_pos = VFile_ReadS32(injection->fp); const int32_t min_floor = VFile_ReadS32(injection->fp); const int32_t max_ceiling = VFile_ReadS32(injection->fp); const uint16_t x_size = VFile_ReadU16(injection->fp); const uint16_t z_size = VFile_ReadU16(injection->fp); if (room_num < 0 || room_num >= Room_GetCount()) { return false; } const ROOM *const room = Room_Get(room_num); return room->pos.x == x_pos && room->pos.z == z_pos && room->min_floor == min_floor && room->max_ceiling == max_ceiling && room->size.x == x_size && room->size.z == z_size; } REGISTER_INJECT_TESTER(ITT_ROOM_COUNT, M_TestRoomCount) REGISTER_INJECT_TESTER(ITT_ROOM_META, M_TestRoomMeta) ================================================ FILE: src/trx/game/inject/types.h ================================================ #pragma once #include #include #include #include typedef struct { INJECTION_MODE mode; } INJECTION_CONTEXT; typedef struct { char *path; VFILE *fp; INJECTION_VERSION version; INJECTION_FILE_TYPE type; bool relevant; } INJECTION; typedef struct { const INJECTION *injection; INJECTION_CHUNK_TYPE type; int32_t num_blocks; int32_t total_size; } INJECTION_CHUNK; typedef struct { int16_t room_index; int16_t num_vertices; int16_t num_quads; int16_t num_triangles; int16_t num_static_2ds; int16_t num_static_3ds; } INJECTION_MESH_META; typedef struct { INJECT_OBJECT_TYPE type; int32_t id; } INJECTION_OBJECT_INFO; ================================================ FILE: src/trx/game/inject/utils.c ================================================ #include #include INJECTION_OBJECT_INFO Inject_ReadObjectPtr(const INJECTION *const injection) { INJECTION_OBJECT_INFO obj_info = { .type = VFile_ReadS32(injection->fp), .id = VFile_ReadS32(injection->fp), }; if (obj_info.type == OBJ_TYPE_OBJECT) { obj_info.id = Object_FromGameID(obj_info.id); if (injection->version < INJ_VERSION_5) { VFile_Skip(injection->fp, 16); } } return obj_info; } ================================================ FILE: src/trx/game/inject/utils.h ================================================ #pragma once #include #include INJECTION_OBJECT_INFO Inject_ReadObjectPtr(const INJECTION *injection); ================================================ FILE: src/trx/game/inject.h ================================================ #pragma once #include #include #include #include ================================================ FILE: src/trx/game/input/backends/base.h ================================================ #pragma once #include #include typedef struct { void (*init)(void); void (*shutdown)(void); void (*discover)(void); bool (*custom_update)(INPUT_STATE *result, INPUT_LAYOUT layout); void (*process_event)(const SDL_Event *event); bool (*is_pressed)(INPUT_LAYOUT layout, INPUT_ROLE role); bool (*is_role_conflicted)(INPUT_LAYOUT layout, INPUT_ROLE role); const char *(*get_name)(INPUT_LAYOUT layout, INPUT_ROLE role, int32_t slot); void (*unassign_role)(INPUT_LAYOUT layout, INPUT_ROLE role, int32_t slot); bool (*assign_from_json_object)( INPUT_LAYOUT layout, INPUT_ROLE role, int32_t slot, JSON_OBJECT *bind_obj); bool (*assign_to_json_object)( INPUT_LAYOUT layout, INPUT_ROLE role, int32_t slot, JSON_OBJECT *bind_obj); void (*reset_layout)(INPUT_LAYOUT layout); bool (*read_and_assign)(INPUT_LAYOUT layout, INPUT_ROLE role, int32_t slot); void (*resolve_combos)(INPUT_LAYOUT layout, INPUT_STATE *result); } INPUT_BACKEND_IMPL; ================================================ FILE: src/trx/game/input/backends/controller.c ================================================ #include #include #include #include #include #include #include typedef enum { BT_BUTTON = 0, BT_AXIS = 1, } BUTTON_TYPE; typedef struct { BUTTON_TYPE type; union { SDL_GameControllerButton button; SDL_GameControllerAxis axis; } bind; int16_t axis_dir; } CONTROLLER_MAP; typedef struct { INPUT_ROLE role; CONTROLLER_MAP map; } BUILTIN_CONTROLLER_LAYOUT; static BUILTIN_CONTROLLER_LAYOUT m_BuiltinLayout[] = { #define INPUT_CONTROLLER_ASSIGN_BUTTON(role, bind) \ { role, { BT_BUTTON, { .button = bind }, 0 } }, #define INPUT_CONTROLLER_ASSIGN_AXIS(role, bind, axis_dir) \ { role, { BT_AXIS, { .axis = bind }, axis_dir } }, #include // guard { -1, { 0, { 0 }, 0 } }, }; // clang-format off #define M_ICON_X "\\{controller button cross}" #define M_ICON_CIRCLE "\\{controller button circle}" #define M_ICON_SQUARE "\\{controller button square}" #define M_ICON_TRIANGLE "\\{controller button triangle}" #define M_ICON_UP "\\{controller dpad up}" #define M_ICON_DOWN "\\{controller dpad down}" #define M_ICON_LEFT "\\{controller dpad left}" #define M_ICON_RIGHT "\\{controller dpad right}" #define M_ICON_L1 "\\{controller button l1}" #define M_ICON_R1 "\\{controller button r1}" #define M_ICON_L2 "\\{controller button l2}" #define M_ICON_R2 "\\{controller button r2}" #define M_NAME_L_STICK "\\{controller lstick}" #define M_NAME_L_ANALOG_UP "\\{controller lstick up}" #define M_NAME_L_ANALOG_RIGHT "\\{controller lstick right}" #define M_NAME_L_ANALOG_DOWN "\\{controller lstick down}" #define M_NAME_L_ANALOG_LEFT "\\{controller lstick left}" #define M_NAME_R_STICK "\\{controller rstick}" #define M_NAME_R_ANALOG_UP "\\{controller rstick up}" #define M_NAME_R_ANALOG_RIGHT "\\{controller rstick right}" #define M_NAME_R_ANALOG_DOWN "\\{controller rstick down}" #define M_NAME_R_ANALOG_LEFT "\\{controller rstick left}" #define M_NAME_L_TRIGGER "\\{controller trigger left}" #define M_NAME_R_TRIGGER "\\{controller trigger right}" #define M_NAME_ZL "\\{controller button zl}" #define M_NAME_ZR "\\{controller button zr}" #define M_NAME_XBOX "\\{controller button xbox}" #define M_NAME_PS "\\{controller button ps}" #define M_NAME_PS_SHARE "\\{controller button share}" #define M_NAME_PS_OPTIONS "\\{controller button options}" #define M_NAME_BACK "\\{controller button back}" #define M_NAME_HOME "\\{controller button home}" #define M_NAME_START "\\{controller button home}" #define M_NAME_SHARE "\\{controller button share}" #define M_NAME_CAPTURE "\\{controller button capture}" #define M_NAME_TOUCHPAD "\\{controller button touchpad}" #define M_NAME_MIC "\\{controller button mic}" #define M_NAME_PADDLE_1 "\\{controller button paddle 1}" #define M_NAME_PADDLE_2 "\\{controller button paddle 2}" #define M_NAME_PADDLE_3 "\\{controller button paddle 3}" #define M_NAME_PADDLE_4 "\\{controller button paddle 4}" #define M_NAME_L_BUMPER "\\{controller bumper left}" #define M_NAME_R_BUMPER "\\{controller bumper right}" #define M_NAME_A "\\{controller button a}" #define M_NAME_B "\\{controller button b}" #define M_NAME_X "\\{controller button x}" #define M_NAME_Y "\\{controller button y}" // clang-format on typedef struct { int32_t key_count; CONTROLLER_MAP keys[INPUT_COMBO_MAX_KEYS]; } CONTROLLER_BINDING; typedef struct { CONTROLLER_BINDING slots[INPUT_BINDING_SLOTS]; } CONTROLLER_ROLE_BINDING; static CONTROLLER_ROLE_BINDING m_Layout[INPUT_LAYOUT_NUMBER_OF] [INPUT_ROLE_NUMBER_OF]; static SDL_GameController *m_Controller = nullptr; static const char *m_ControllerName = nullptr; static SDL_GameControllerType m_ControllerType = SDL_CONTROLLER_TYPE_UNKNOWN; static bool m_Conflicts[INPUT_LAYOUT_NUMBER_OF][INPUT_ROLE_NUMBER_OF] = {}; // Internal controller state tables updated via SDL events static bool m_ButtonState[SDL_CONTROLLER_BUTTON_MAX] = {}; static int16_t m_AxisState[SDL_CONTROLLER_AXIS_MAX] = {}; static const char *M_GetButtonName(const SDL_GameControllerButton button) { // First switch: Handle platform-specific deviations from defaults switch (m_ControllerType) { case SDL_CONTROLLER_TYPE_PS3: case SDL_CONTROLLER_TYPE_PS4: case SDL_CONTROLLER_TYPE_PS5: // clang-format off switch (button) { case SDL_CONTROLLER_BUTTON_A: return M_ICON_X; case SDL_CONTROLLER_BUTTON_B: return M_ICON_CIRCLE; case SDL_CONTROLLER_BUTTON_X: return M_ICON_SQUARE; case SDL_CONTROLLER_BUTTON_Y: return M_ICON_TRIANGLE; case SDL_CONTROLLER_BUTTON_BACK: return M_NAME_PS_OPTIONS; case SDL_CONTROLLER_BUTTON_START: return M_NAME_PS_SHARE; case SDL_CONTROLLER_BUTTON_LEFTSTICK: return M_NAME_L_STICK; case SDL_CONTROLLER_BUTTON_RIGHTSTICK: return M_NAME_R_STICK; case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: return M_ICON_L1; case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: return M_ICON_R1; case SDL_CONTROLLER_BUTTON_MISC1: return M_NAME_MIC; case SDL_CONTROLLER_BUTTON_GUIDE: return M_NAME_PS; default: break; } // clang-format on break; case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO: case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_PAIR: // clang-format off switch (button) { case SDL_CONTROLLER_BUTTON_A: return M_NAME_B; case SDL_CONTROLLER_BUTTON_B: return M_NAME_A; case SDL_CONTROLLER_BUTTON_X: return M_NAME_Y; case SDL_CONTROLLER_BUTTON_Y: return M_NAME_X; case SDL_CONTROLLER_BUTTON_START: return M_NAME_START; case SDL_CONTROLLER_BUTTON_MISC1: return M_NAME_CAPTURE; default: break; } // clang-format on break; case SDL_CONTROLLER_TYPE_XBOX360: case SDL_CONTROLLER_TYPE_XBOXONE: // clang-format off switch (button) { case SDL_CONTROLLER_BUTTON_GUIDE: return M_NAME_XBOX; default: break; } // clang-format on break; default: break; } // Second switch: Provide default mappings for all keys switch (button) { case SDL_CONTROLLER_BUTTON_INVALID: case SDL_CONTROLLER_BUTTON_MAX: return nullptr; // clang-format off case SDL_CONTROLLER_BUTTON_A: return M_NAME_A; case SDL_CONTROLLER_BUTTON_B: return M_NAME_B; case SDL_CONTROLLER_BUTTON_X: return M_NAME_X; case SDL_CONTROLLER_BUTTON_Y: return M_NAME_Y; case SDL_CONTROLLER_BUTTON_BACK: return M_NAME_BACK; case SDL_CONTROLLER_BUTTON_GUIDE: return M_NAME_HOME; case SDL_CONTROLLER_BUTTON_START: return M_NAME_START; case SDL_CONTROLLER_BUTTON_LEFTSTICK: return M_NAME_L_STICK; case SDL_CONTROLLER_BUTTON_RIGHTSTICK: return M_NAME_R_STICK; case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: return M_NAME_L_BUMPER; case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: return M_NAME_R_BUMPER; case SDL_CONTROLLER_BUTTON_DPAD_UP: return M_ICON_UP; case SDL_CONTROLLER_BUTTON_DPAD_DOWN: return M_ICON_DOWN; case SDL_CONTROLLER_BUTTON_DPAD_LEFT: return M_ICON_LEFT; case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: return M_ICON_RIGHT; case SDL_CONTROLLER_BUTTON_MISC1: return M_NAME_SHARE; case SDL_CONTROLLER_BUTTON_PADDLE1: return M_NAME_PADDLE_1; case SDL_CONTROLLER_BUTTON_PADDLE2: return M_NAME_PADDLE_2; case SDL_CONTROLLER_BUTTON_PADDLE3: return M_NAME_PADDLE_3; case SDL_CONTROLLER_BUTTON_PADDLE4: return M_NAME_PADDLE_4; case SDL_CONTROLLER_BUTTON_TOUCHPAD: return M_NAME_TOUCHPAD; // clang-format on default: return "????"; } } // Update internal controller button/axis state from SDL events. // @param event Event to process. static void M_ProcessEvent(const SDL_Event *const event) { switch (event->type) { case SDL_CONTROLLERBUTTONDOWN: m_ButtonState[event->cbutton.button] = true; break; case SDL_CONTROLLERBUTTONUP: m_ButtonState[event->cbutton.button] = false; break; case SDL_CONTROLLERAXISMOTION: { const Sint16 value = event->caxis.value; if (value < -SDL_JOYSTICK_AXIS_MAX / 2) { m_AxisState[event->caxis.axis] = -1; } else if (value > SDL_JOYSTICK_AXIS_MAX / 2) { m_AxisState[event->caxis.axis] = 1; } else { m_AxisState[event->caxis.axis] = 0; } break; } default: break; } } static bool M_JoyBtn(const SDL_GameControllerButton button) { if (m_Controller == nullptr || button == SDL_CONTROLLER_BUTTON_INVALID) { return false; } return m_ButtonState[button]; } static int16_t M_JoyAxis(const SDL_GameControllerAxis axis) { if (m_Controller == nullptr || axis == SDL_CONTROLLER_AXIS_INVALID) { return false; } return m_AxisState[axis]; } static bool M_CheckMap(const CONTROLLER_MAP *const map) { if (map->type == BT_BUTTON) { return M_JoyBtn(map->bind.button); } else { return M_JoyAxis(map->bind.axis) == map->axis_dir; } } static bool M_CheckBinding(const CONTROLLER_BINDING *const bind) { if (bind->key_count == 0) { return false; } for (int32_t k = 0; k < bind->key_count; k++) { if (!M_CheckMap(&bind->keys[k])) { return false; } } return true; } // Combo adapter forward declarations. static INPUT_COMBO_BINDING M_GetComboBinding( INPUT_LAYOUT layout, INPUT_ROLE role, int32_t slot); static bool M_ComboKeysEqual(const void *a, const void *b); static bool M_GetBindState(const INPUT_LAYOUT layout, const INPUT_ROLE role) { for (int32_t slot = 0; slot < INPUT_BINDING_SLOTS; slot++) { const CONTROLLER_BINDING *bind = &m_Layout[layout][role].slots[slot]; if (bind->key_count >= 2 && Input_ComboIsKeyImmediate( layout, &bind->keys[0], M_GetComboBinding, M_ComboKeysEqual)) { continue; } if (M_CheckBinding(bind)) { return true; } } return false; } static const CONTROLLER_BINDING *M_GetBinding( const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot) { return &m_Layout[layout][role].slots[slot]; } static bool M_MapsEqual( const CONTROLLER_MAP *const a, const CONTROLLER_MAP *const b) { if (a->type != b->type) { return false; } if (a->type == BT_BUTTON) { return a->bind.button == b->bind.button; } return a->bind.axis == b->bind.axis && a->axis_dir == b->axis_dir; } static bool M_BindingsEqual( const CONTROLLER_BINDING *const a, const CONTROLLER_BINDING *const b) { if (a->key_count != b->key_count || a->key_count == 0) { return false; } for (int32_t i = 0; i < a->key_count; i++) { if (!M_MapsEqual(&a->keys[i], &b->keys[i])) { return false; } } return true; } static bool M_CheckConflict( const INPUT_LAYOUT layout, const INPUT_ROLE role1, const INPUT_ROLE role2) { for (int32_t s1 = 0; s1 < INPUT_BINDING_SLOTS; s1++) { const CONTROLLER_BINDING *b1 = M_GetBinding(layout, role1, s1); if (b1->key_count == 0) { continue; } for (int32_t s2 = 0; s2 < INPUT_BINDING_SLOTS; s2++) { const CONTROLLER_BINDING *b2 = M_GetBinding(layout, role2, s2); if (M_BindingsEqual(b1, b2)) { return true; } } } return false; } static void M_AssignConflict( const INPUT_LAYOUT layout, const INPUT_ROLE role, const bool conflict) { m_Conflicts[layout][role] = conflict; } static void M_CheckConflicts(const INPUT_LAYOUT layout) { Input_ConflictHelper(layout, M_CheckConflict, M_AssignConflict); Input_ComboCheckConflicts( layout, M_GetComboBinding, M_ComboKeysEqual, m_Conflicts[layout]); } static int16_t M_GetAssignedButtonType( const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot) { const CONTROLLER_BINDING *bind = M_GetBinding(layout, role, slot); return bind->key_count > 0 ? bind->keys[0].type : BT_BUTTON; } static int16_t M_GetAssignedBind( const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot) { const CONTROLLER_BINDING *bind = M_GetBinding(layout, role, slot); if (bind->key_count == 0) { return SDL_CONTROLLER_BUTTON_INVALID; } const CONTROLLER_MAP *map = &bind->keys[0]; if (map->type == BT_BUTTON) { return map->bind.button; } else { return map->bind.axis; } } static int16_t M_GetAssignedAxisDir( const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot) { const CONTROLLER_BINDING *bind = M_GetBinding(layout, role, slot); return bind->key_count > 0 ? bind->keys[0].axis_dir : 0; } static void M_AssignBinding( const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot, const CONTROLLER_BINDING *const bind) { m_Layout[layout][role].slots[slot] = *bind; M_CheckConflicts(layout); } static void M_AssignButton( const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot, const int16_t button) { const CONTROLLER_BINDING bind = { .key_count = button != SDL_CONTROLLER_BUTTON_INVALID ? 1 : 0, .keys = { { BT_BUTTON, { .button = button }, 0 } }, }; M_AssignBinding(layout, role, slot, &bind); } static void M_AssignAxis( const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot, const int16_t axis, const int16_t axis_dir) { const CONTROLLER_BINDING bind = { .key_count = 1, .keys = { { BT_AXIS, { .axis = axis }, axis_dir } }, }; M_AssignBinding(layout, role, slot, &bind); } static SDL_GameController *M_FindController(void) { if (m_Controller != nullptr) { return m_Controller; } int32_t controllers = SDL_NumJoysticks(); LOG_INFO("%d controllers", controllers); for (int32_t i = 0; i < controllers; i++) { m_ControllerName = SDL_GameControllerNameForIndex(i); m_ControllerType = SDL_GameControllerTypeForIndex(i); bool is_game_controller = SDL_IsGameController(i); LOG_DEBUG( "controller %d: %s %d (%d)", i, m_ControllerName, m_ControllerType, is_game_controller); if (is_game_controller) { SDL_GameController *const result = SDL_GameControllerOpen(i); if (result == nullptr) { LOG_ERROR("Could not open controller: %s", SDL_GetError()); } return result; } } return nullptr; } static void M_ResetLayout(const INPUT_LAYOUT layout) { for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) { m_Layout[layout][role] = m_Layout[INPUT_LAYOUT_DEFAULT][role]; } M_CheckConflicts(layout); } static void M_Discover(void) { if (m_Controller != nullptr) { SDL_GameControllerClose(m_Controller); m_Controller = nullptr; } m_Controller = M_FindController(); } static void M_Init(void) { // first, reset all roles to unbound for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) { for (int32_t slot = 0; slot < INPUT_BINDING_SLOTS; slot++) { m_Layout[INPUT_LAYOUT_DEFAULT][role].slots[slot] = (CONTROLLER_BINDING) { .key_count = 0 }; } } // then load actually defined default bindings into slot 0 for (int32_t i = 0; m_BuiltinLayout[i].role != (INPUT_ROLE)-1; i++) { const BUILTIN_CONTROLLER_LAYOUT *const builtin = &m_BuiltinLayout[i]; m_Layout[INPUT_LAYOUT_DEFAULT][builtin->role].slots[0] = (CONTROLLER_BINDING) { .key_count = 1, .keys = { builtin->map }, }; } M_CheckConflicts(INPUT_LAYOUT_DEFAULT); for (int32_t layout = INPUT_LAYOUT_CUSTOM_1; layout < INPUT_LAYOUT_NUMBER_OF; layout++) { M_ResetLayout(layout); } int32_t result = SDL_Init(SDL_INIT_GAMECONTROLLER | SDL_INIT_SENSOR); if (result < 0) { LOG_ERROR("Error while calling SDL_Init: 0x%lx", result); } else { M_Discover(); } } static void M_Shutdown(void) { if (m_Controller != nullptr) { SDL_GameControllerClose(m_Controller); m_Controller = nullptr; } } static bool M_CustomUpdate(INPUT_STATE *const result, const INPUT_LAYOUT layout) { if (m_Controller == nullptr) { return false; } result->menu_back |= M_JoyBtn(SDL_CONTROLLER_BUTTON_Y); result->menu_skip = result->menu_confirm || result->menu_back; return true; } static bool M_IsPressed(const INPUT_LAYOUT layout, const INPUT_ROLE role) { return M_GetBindState(layout, role); } static const char *M_GetAxisName( const SDL_GameControllerAxis axis, const int16_t axis_dir) { // clang-format off switch (m_ControllerType) { case SDL_CONTROLLER_TYPE_PS3: case SDL_CONTROLLER_TYPE_PS4: case SDL_CONTROLLER_TYPE_PS5: switch (axis) { case SDL_CONTROLLER_AXIS_INVALID: return nullptr; case SDL_CONTROLLER_AXIS_LEFTX: return axis_dir == -1 ? M_NAME_L_ANALOG_LEFT : M_NAME_L_ANALOG_RIGHT; case SDL_CONTROLLER_AXIS_LEFTY: return axis_dir == -1 ? M_NAME_L_ANALOG_UP : M_NAME_L_ANALOG_DOWN; case SDL_CONTROLLER_AXIS_RIGHTX: return axis_dir == -1 ? M_NAME_R_ANALOG_LEFT : M_NAME_R_ANALOG_RIGHT; case SDL_CONTROLLER_AXIS_RIGHTY: return axis_dir == -1 ? M_NAME_R_ANALOG_UP : M_NAME_R_ANALOG_DOWN; case SDL_CONTROLLER_AXIS_TRIGGERLEFT: return M_ICON_L2; case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: return M_ICON_R2; case SDL_CONTROLLER_AXIS_MAX: return nullptr; } break; case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO: case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_PAIR: switch (axis) { case SDL_CONTROLLER_AXIS_INVALID: return nullptr; case SDL_CONTROLLER_AXIS_LEFTX: return axis_dir == -1 ? M_NAME_L_ANALOG_LEFT : M_NAME_L_ANALOG_RIGHT; case SDL_CONTROLLER_AXIS_LEFTY: return axis_dir == -1 ? M_NAME_L_ANALOG_UP : M_NAME_L_ANALOG_DOWN; case SDL_CONTROLLER_AXIS_RIGHTX: return axis_dir == -1 ? M_NAME_R_ANALOG_LEFT : M_NAME_R_ANALOG_RIGHT; case SDL_CONTROLLER_AXIS_RIGHTY: return axis_dir == -1 ? M_NAME_R_ANALOG_UP : M_NAME_R_ANALOG_DOWN; case SDL_CONTROLLER_AXIS_TRIGGERLEFT: return M_NAME_ZL; case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: return M_NAME_ZR; case SDL_CONTROLLER_AXIS_MAX: return nullptr; } break; case SDL_CONTROLLER_TYPE_XBOX360: case SDL_CONTROLLER_TYPE_XBOXONE: default: switch (axis) { case SDL_CONTROLLER_AXIS_INVALID: return nullptr; case SDL_CONTROLLER_AXIS_LEFTX: return axis_dir == -1 ? M_NAME_L_ANALOG_LEFT : M_NAME_L_ANALOG_RIGHT; case SDL_CONTROLLER_AXIS_LEFTY: return axis_dir == -1 ? M_NAME_L_ANALOG_UP : M_NAME_L_ANALOG_DOWN; case SDL_CONTROLLER_AXIS_RIGHTX: return axis_dir == -1 ? M_NAME_R_ANALOG_LEFT : M_NAME_R_ANALOG_RIGHT; case SDL_CONTROLLER_AXIS_RIGHTY: return axis_dir == -1 ? M_NAME_R_ANALOG_UP : M_NAME_R_ANALOG_DOWN; case SDL_CONTROLLER_AXIS_TRIGGERLEFT: return M_NAME_L_TRIGGER; case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: return M_NAME_R_TRIGGER; case SDL_CONTROLLER_AXIS_MAX: return nullptr; } break; } // clang-format on return nullptr; } static bool M_IsRoleConflicted(const INPUT_LAYOUT layout, const INPUT_ROLE role) { return m_Conflicts[layout][role]; } static const char *M_GetMapName(const CONTROLLER_MAP *const map) { if (map->type == BT_BUTTON) { return M_GetButtonName(map->bind.button); } else { return M_GetAxisName(map->bind.axis, map->axis_dir); } } static const char *M_GetName( const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot) { const CONTROLLER_BINDING *bind = M_GetBinding(layout, role, slot); if (bind->key_count == 0) { return nullptr; } if (bind->key_count == 1) { return M_GetMapName(&bind->keys[0]); } // Build composite name for multi-key combo static char buf[256]; buf[0] = '\0'; for (int32_t k = 0; k < bind->key_count; k++) { if (k > 0) { strcat(buf, "+"); } const char *name = M_GetMapName(&bind->keys[k]); if (name != nullptr) { strcat(buf, name); } } return buf; } static void M_UnassignRole( const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot) { const CONTROLLER_BINDING empty = { .key_count = 0 }; M_AssignBinding(layout, role, slot, &empty); } static bool M_AssignFromJSONObject( const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot, JSON_OBJECT *const bind_obj) { JSON_ARRAY *const combo_arr = JSON_ObjectGetArray(bind_obj, "combo"); if (combo_arr != nullptr) { // New combo format: "combo": [{button_type, bind, axis_dir}, ...] const int32_t count = combo_arr->length < INPUT_COMBO_MAX_KEYS ? (int32_t)combo_arr->length : INPUT_COMBO_MAX_KEYS; CONTROLLER_BINDING cb = { .key_count = count }; for (int32_t i = 0; i < count; i++) { JSON_OBJECT *const key_obj = JSON_ArrayGetObject(combo_arr, i); cb.keys[i].type = JSON_ObjectGetInt(key_obj, "button_type", BT_BUTTON); const int16_t b = JSON_ObjectGetInt( key_obj, "bind", SDL_CONTROLLER_BUTTON_INVALID); if (cb.keys[i].type == BT_BUTTON) { cb.keys[i].bind.button = b; } else { cb.keys[i].bind.axis = b; } cb.keys[i].axis_dir = JSON_ObjectGetInt(key_obj, "axis_dir", 0); } M_AssignBinding(layout, role, slot, &cb); } else { // Legacy single-key format int16_t button_type = M_GetAssignedButtonType(layout, role, slot); button_type = JSON_ObjectGetInt(bind_obj, "button_type", button_type); int16_t bind = M_GetAssignedBind(layout, role, slot); bind = JSON_ObjectGetInt(bind_obj, "bind", bind); int16_t axis_dir = M_GetAssignedAxisDir(layout, role, slot); axis_dir = JSON_ObjectGetInt(bind_obj, "axis_dir", axis_dir); if (button_type == BT_BUTTON) { M_AssignButton(layout, role, slot, bind); } else { M_AssignAxis(layout, role, slot, bind, axis_dir); } } return true; } static bool M_AssignToJSONObject( const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot, JSON_OBJECT *const bind_obj) { const CONTROLLER_BINDING *user = M_GetBinding(layout, role, slot); const CONTROLLER_BINDING *def = M_GetBinding(INPUT_LAYOUT_DEFAULT, role, slot); if (M_BindingsEqual(user, def) || (user->key_count == 0 && def->key_count == 0)) { return false; } if (user->key_count == 0) { // Explicitly unbound JSON_ObjectAppendInt(bind_obj, "button_type", (int16_t)BT_BUTTON); JSON_ObjectAppendInt( bind_obj, "bind", (int16_t)SDL_CONTROLLER_BUTTON_INVALID); JSON_ObjectAppendInt(bind_obj, "axis_dir", (int16_t)0); } else if (user->key_count == 1) { // Single key (legacy format for backward compat) const CONTROLLER_MAP *map = &user->keys[0]; JSON_ObjectAppendInt(bind_obj, "button_type", map->type); JSON_ObjectAppendInt( bind_obj, "bind", map->type == BT_BUTTON ? map->bind.button : map->bind.axis); JSON_ObjectAppendInt(bind_obj, "axis_dir", map->axis_dir); } else { // Multi-key combo JSON_ARRAY *const arr = JSON_ArrayNew(); for (int32_t i = 0; i < user->key_count; i++) { const CONTROLLER_MAP *map = &user->keys[i]; JSON_OBJECT *const key_obj = JSON_ObjectNew(); JSON_ObjectAppendInt(key_obj, "button_type", map->type); JSON_ObjectAppendInt( key_obj, "bind", map->type == BT_BUTTON ? map->bind.button : map->bind.axis); JSON_ObjectAppendInt(key_obj, "axis_dir", map->axis_dir); JSON_ArrayAppendObject(arr, key_obj); } JSON_ObjectAppendArray(bind_obj, "combo", arr); } return true; } // Per-button/axis tracking for combo prefix deferral. // Index: buttons use their enum directly, axes use // SDL_CONTROLLER_BUTTON_MAX + axis*2 + (axis_dir == 1 ? 1 : 0). #define M_PREFIX_SLOTS (SDL_CONTROLLER_BUTTON_MAX + SDL_CONTROLLER_AXIS_MAX * 2) static bool m_PrefixWasHeld[M_PREFIX_SLOTS]; static bool m_PrefixComboFired[M_PREFIX_SLOTS]; static int32_t M_MapToPrefixIdx(const CONTROLLER_MAP *const map) { if (map->type == BT_BUTTON) { return map->bind.button; } return SDL_CONTROLLER_BUTTON_MAX + map->bind.axis * 2 + (map->axis_dir == 1 ? 1 : 0); } static bool M_IsInputHeld(const CONTROLLER_MAP *const map) { return M_CheckMap(map); } // Combo adapter functions for the shared combo layer. static INPUT_COMBO_BINDING M_GetComboBinding( const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot) { const CONTROLLER_BINDING *b = M_GetBinding(layout, role, slot); return (INPUT_COMBO_BINDING) { .key_count = b->key_count, .keys = b->keys, .key_stride = sizeof(CONTROLLER_MAP), }; } static bool M_ComboKeysEqual(const void *const a, const void *const b) { return M_MapsEqual((const CONTROLLER_MAP *)a, (const CONTROLLER_MAP *)b); } static INPUT_COMBO_BINDING M_ToCombo(const CONTROLLER_BINDING *const b) { return (INPUT_COMBO_BINDING) { .key_count = b->key_count, .keys = b->keys, .key_stride = sizeof(CONTROLLER_MAP), }; } static const CONTROLLER_BINDING *M_GetPressedBinding( const INPUT_LAYOUT layout, const INPUT_ROLE role) { for (int32_t slot = 0; slot < INPUT_BINDING_SLOTS; slot++) { const CONTROLLER_BINDING *bind = M_GetBinding(layout, role, slot); if (M_CheckBinding(bind)) { return bind; } } return nullptr; } static void M_ResolvePrefixDeferral( const INPUT_LAYOUT layout, INPUT_STATE *const result, const CONTROLLER_MAP *const map) { const int32_t idx = M_MapToPrefixIdx(map); const bool held = M_IsInputHeld(map); if (held && Input_ComboIsStarter( layout, map, M_GetComboBinding, M_ComboKeysEqual)) { const INPUT_ROLE role = Input_ComboFindDeferrableRole( layout, map, M_GetComboBinding, M_ComboKeysEqual); if (role != (INPUT_ROLE)-1) { InputState_ClearRole(result, role); } } if (!held && m_PrefixWasHeld[idx] && !m_PrefixComboFired[idx]) { const INPUT_ROLE role = Input_ComboFindDeferrableRole( layout, map, M_GetComboBinding, M_ComboKeysEqual); if (role != (INPUT_ROLE)-1) { InputState_SetRole(result, role, true); } } m_PrefixWasHeld[idx] = held; } // Per-role deferral tracking for combo disambiguation. static bool m_RoleWasActive[INPUT_ROLE_NUMBER_OF]; static bool m_RoleLongerFired[INPUT_ROLE_NUMBER_OF]; static void M_ResolveCombos( const INPUT_LAYOUT layout, INPUT_STATE *const result) { // Phase 1: Collect active bindings. const CONTROLLER_BINDING *active[INPUT_ROLE_NUMBER_OF] = {}; for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) { if (InputState_GetRole(*result, role)) { active[role] = M_GetPressedBinding(layout, role); } } // Suppress invalid combos (non-capturing sustained + immediate). for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) { if (active[role] != nullptr && Input_ComboSustainedHasImmediate( layout, M_ToCombo(active[role]), M_GetComboBinding, M_ComboKeysEqual)) { InputState_ClearRole(result, role); active[role] = nullptr; } } // Phase 2: Subset suppression — longer active combos suppress shorter. for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) { if (active[role] == nullptr) { continue; } for (INPUT_ROLE other = 0; other < INPUT_ROLE_NUMBER_OF; other++) { if (other == role || active[other] == nullptr) { continue; } if (Input_ComboIsProperSubset( M_ToCombo(active[role]), M_ToCombo(active[other]), M_ComboKeysEqual)) { InputState_ClearRole(result, role); break; } } } // Phase 3: Combo deferral — if an active combo's binding is a proper // subset of some (not necessarily active) longer binding, defer it. for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) { if (active[role] == nullptr) { continue; } if (((Input_IsRoleImmediate(role) || Input_IsRoleSustained(role)) && active[role]->key_count <= 1) || !Input_IsRoleRebindable(role)) { continue; } if (Input_ComboHasLonger( layout, role, M_ToCombo(active[role]), M_GetComboBinding, M_ComboKeysEqual)) { InputState_ClearRole(result, role); if (!m_RoleWasActive[role]) { m_RoleLongerFired[role] = false; } m_RoleWasActive[role] = true; } } // Reset prefix tracking for newly pressed inputs. for (SDL_GameControllerButton btn = 0; btn < SDL_CONTROLLER_BUTTON_MAX; btn++) { const int32_t idx = (int32_t)btn; if (M_JoyBtn(btn) && !m_PrefixWasHeld[idx]) { m_PrefixComboFired[idx] = false; } } for (SDL_GameControllerAxis axis = 0; axis < SDL_CONTROLLER_AXIS_MAX; axis++) { for (int16_t dir = -1; dir <= 1; dir += 2) { const CONTROLLER_MAP map = { BT_AXIS, { .axis = axis }, dir }; const int32_t idx = M_MapToPrefixIdx(&map); if (M_JoyAxis(axis) == dir && !m_PrefixWasHeld[idx]) { m_PrefixComboFired[idx] = false; } } } // Phase 4: Mark longer-combo-fired state. for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) { if (active[role] == nullptr || active[role]->key_count < 2) { continue; } for (int32_t k = 0; k < active[role]->key_count; k++) { const int32_t idx = M_MapToPrefixIdx(&active[role]->keys[k]); m_PrefixComboFired[idx] = true; } for (INPUT_ROLE other = 0; other < INPUT_ROLE_NUMBER_OF; other++) { if (!m_RoleWasActive[other]) { continue; } const CONTROLLER_BINDING *ob = M_GetPressedBinding(layout, other); if (ob != nullptr && Input_ComboIsProperSubset( M_ToCombo(ob), M_ToCombo(active[role]), M_ComboKeysEqual)) { m_RoleLongerFired[other] = true; } } } // Phase 5: Fire deferred roles on release. for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) { if (!m_RoleWasActive[role]) { continue; } const CONTROLLER_BINDING *bind = M_GetPressedBinding(layout, role); if (bind != nullptr) { continue; } if (!m_RoleLongerFired[role]) { InputState_SetRole(result, role, true); } m_RoleWasActive[role] = false; m_RoleLongerFired[role] = false; } // Phase 6: Single-input prefix deferral. for (SDL_GameControllerButton btn = 0; btn < SDL_CONTROLLER_BUTTON_MAX; btn++) { const CONTROLLER_MAP map = { BT_BUTTON, { .button = btn }, 0 }; M_ResolvePrefixDeferral(layout, result, &map); } for (SDL_GameControllerAxis axis = 0; axis < SDL_CONTROLLER_AXIS_MAX; axis++) { for (int16_t dir = -1; dir <= 1; dir += 2) { const CONTROLLER_MAP map = { BT_AXIS, { .axis = axis }, dir }; M_ResolvePrefixDeferral(layout, result, &map); } } } // Combo capture state for listen mode. static CONTROLLER_BINDING m_CaptureBuffer = { .key_count = 0 }; static bool m_CaptureActive = false; static bool M_CaptureHasMap(const CONTROLLER_MAP *const map) { for (int32_t i = 0; i < m_CaptureBuffer.key_count; i++) { if (M_MapsEqual(&m_CaptureBuffer.keys[i], map)) { return true; } } return false; } static bool M_ReadAndAssign( const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot) { // Count currently held inputs and accumulate new ones into the buffer. bool any_held = false; for (SDL_GameControllerButton button = 0; button < SDL_CONTROLLER_BUTTON_MAX; button++) { if (!M_JoyBtn(button)) { continue; } any_held = true; const CONTROLLER_MAP map = { BT_BUTTON, { .button = button }, 0 }; if (!M_CaptureHasMap(&map) && m_CaptureBuffer.key_count < INPUT_COMBO_MAX_KEYS) { m_CaptureBuffer.keys[m_CaptureBuffer.key_count++] = map; } m_CaptureActive = true; } for (SDL_GameControllerAxis axis = 0; axis < SDL_CONTROLLER_AXIS_MAX; axis++) { const int16_t axis_dir = M_JoyAxis(axis); if (axis_dir == 0) { continue; } any_held = true; const CONTROLLER_MAP map = { BT_AXIS, { .axis = axis }, axis_dir }; if (!M_CaptureHasMap(&map) && m_CaptureBuffer.key_count < INPUT_COMBO_MAX_KEYS) { m_CaptureBuffer.keys[m_CaptureBuffer.key_count++] = map; } m_CaptureActive = true; } // If the first input captured is bound to an immediate role (movement, // action, etc.), assign as single key right away — don't wait for combo. if (m_CaptureActive && m_CaptureBuffer.key_count == 1 && any_held && Input_ComboIsKeyImmediate( layout, &m_CaptureBuffer.keys[0], M_GetComboBinding, M_ComboKeysEqual)) { M_AssignBinding(layout, role, slot, &m_CaptureBuffer); m_CaptureBuffer.key_count = 0; m_CaptureActive = false; return true; } // All inputs released after at least one was captured — assign the chord. if (!any_held && m_CaptureActive) { M_AssignBinding(layout, role, slot, &m_CaptureBuffer); m_CaptureBuffer.key_count = 0; m_CaptureActive = false; return true; } return false; } INPUT_BACKEND_IMPL g_Input_Controller = { .init = M_Init, .shutdown = M_Shutdown, .discover = M_Discover, .process_event = M_ProcessEvent, .custom_update = M_CustomUpdate, .is_pressed = M_IsPressed, .is_role_conflicted = M_IsRoleConflicted, .get_name = M_GetName, .unassign_role = M_UnassignRole, .assign_from_json_object = M_AssignFromJSONObject, .assign_to_json_object = M_AssignToJSONObject, .reset_layout = M_ResetLayout, .read_and_assign = M_ReadAndAssign, .resolve_combos = M_ResolveCombos, }; ================================================ FILE: src/trx/game/input/backends/controller.def ================================================ INPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_UP, SDL_CONTROLLER_BUTTON_DPAD_UP) INPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_DOWN, SDL_CONTROLLER_BUTTON_DPAD_DOWN) INPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_LEFT, SDL_CONTROLLER_BUTTON_DPAD_LEFT) INPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_RIGHT, SDL_CONTROLLER_BUTTON_DPAD_RIGHT) INPUT_CONTROLLER_ASSIGN_AXIS(INPUT_ROLE_STEP_LEFT, SDL_CONTROLLER_AXIS_TRIGGERLEFT, 1) INPUT_CONTROLLER_ASSIGN_AXIS(INPUT_ROLE_STEP_RIGHT, SDL_CONTROLLER_AXIS_TRIGGERRIGHT, 1) INPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_SLOW, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) INPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_JUMP, SDL_CONTROLLER_BUTTON_X) INPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_ACTION, SDL_CONTROLLER_BUTTON_A) INPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_DRAW_WEAPON, SDL_CONTROLLER_BUTTON_Y) INPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_LOOK, SDL_CONTROLLER_BUTTON_LEFTSHOULDER) INPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_ROLL, SDL_CONTROLLER_BUTTON_B) INPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_INVENTORY, SDL_CONTROLLER_BUTTON_BACK) INPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_CROUCH, SDL_CONTROLLER_BUTTON_RIGHTSTICK) INPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_PAUSE, SDL_CONTROLLER_BUTTON_START) INPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_CHANGE_TARGET, SDL_CONTROLLER_BUTTON_LEFTSTICK) INPUT_CONTROLLER_ASSIGN_AXIS(INPUT_ROLE_CAMERA_FORWARD, SDL_CONTROLLER_AXIS_RIGHTY, -1) INPUT_CONTROLLER_ASSIGN_AXIS(INPUT_ROLE_CAMERA_BACK, SDL_CONTROLLER_AXIS_RIGHTY, 1) INPUT_CONTROLLER_ASSIGN_AXIS(INPUT_ROLE_CAMERA_LEFT, SDL_CONTROLLER_AXIS_RIGHTX, -1) INPUT_CONTROLLER_ASSIGN_AXIS(INPUT_ROLE_CAMERA_RIGHT, SDL_CONTROLLER_AXIS_RIGHTX, 1) INPUT_CONTROLLER_ASSIGN_AXIS(INPUT_ROLE_CAMERA_UP, SDL_CONTROLLER_AXIS_LEFTY, -1) INPUT_CONTROLLER_ASSIGN_AXIS(INPUT_ROLE_CAMERA_DOWN, SDL_CONTROLLER_AXIS_LEFTY, 1) INPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_MENU_CONFIRM, SDL_CONTROLLER_BUTTON_A) INPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_MENU_BACK, SDL_CONTROLLER_BUTTON_B) INPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_RESET_BINDINGS, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) INPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_UNBIND_KEY, SDL_CONTROLLER_BUTTON_X) INPUT_CONTROLLER_ASSIGN_AXIS(INPUT_ROLE_MENU_TAB_LEFT, SDL_CONTROLLER_AXIS_TRIGGERLEFT, 1) INPUT_CONTROLLER_ASSIGN_AXIS(INPUT_ROLE_MENU_TAB_RIGHT, SDL_CONTROLLER_AXIS_TRIGGERRIGHT, 1) ================================================ FILE: src/trx/game/input/backends/controller.h ================================================ #include extern INPUT_BACKEND_IMPL g_Input_Controller; ================================================ FILE: src/trx/game/input/backends/internal.c ================================================ #include #include static bool M_IsManualCameraInput(const INPUT_ROLE role) { return role == INPUT_ROLE_CAMERA_FORWARD || role == INPUT_ROLE_CAMERA_BACK || role == INPUT_ROLE_CAMERA_LEFT || role == INPUT_ROLE_CAMERA_RIGHT; } void Input_ConflictHelper( const INPUT_LAYOUT layout, bool (*check_conflict_func)( INPUT_LAYOUT layout, INPUT_ROLE role1, INPUT_ROLE role2), void (*assign_conflict_func)( INPUT_LAYOUT layout, INPUT_ROLE role, bool conflict)) { for (INPUT_ROLE role1 = 0; role1 < INPUT_ROLE_NUMBER_OF; role1++) { if (!Input_IsRoleRebindable(role1)) { continue; } bool conflict = false; for (INPUT_ROLE role2 = 0; role2 < INPUT_ROLE_NUMBER_OF; role2++) { if (!Input_IsRoleRebindable(role2)) { continue; } if (role1 == role2) { continue; } if (!g_Config.gameplay.enable_manual_camera && (M_IsManualCameraInput(role1) || M_IsManualCameraInput(role2))) { continue; } if (check_conflict_func(layout, role1, role2)) { conflict = true; } } assign_conflict_func(layout, role1, conflict); } } ================================================ FILE: src/trx/game/input/backends/internal.h ================================================ #pragma once #include #include void Input_UpdateFromBackend( INPUT_STATE *result, INPUT_LAYOUT layout, const INPUT_BACKEND_IMPL *backend); void Input_ConflictHelper( INPUT_LAYOUT layout, bool (*check_conflict_func)( INPUT_LAYOUT layout, INPUT_ROLE role1, INPUT_ROLE role2), void (*assign_conflict_func)( INPUT_LAYOUT layout, INPUT_ROLE role, bool conflict)); ================================================ FILE: src/trx/game/input/backends/keyboard.c ================================================ #include #include #include #include #include #include #include // Key state table updated via SDL events. #define KEY_DOWN(a) (m_KeyboardState[(a)]) typedef struct { INPUT_ROLE role; SDL_Scancode scancode; } BUILTIN_KEYBOARD_LAYOUT; typedef struct { int32_t key_count; SDL_Scancode keys[INPUT_COMBO_MAX_KEYS]; } KEYBOARD_BINDING; typedef struct { KEYBOARD_BINDING slots[INPUT_BINDING_SLOTS]; } KEYBOARD_ROLE_BINDING; static bool m_KeyboardState[SDL_NUM_SCANCODES] = {}; static bool m_Conflicts[INPUT_LAYOUT_NUMBER_OF][INPUT_ROLE_NUMBER_OF] = {}; static const BUILTIN_KEYBOARD_LAYOUT m_BuiltinLayoutBase[] = { // clang-format off #define INPUT_KEYBOARD_ASSIGN(role, key) { role, key }, #include { -1, SDL_SCANCODE_UNKNOWN }, // clang-format on }; static BUILTIN_KEYBOARD_LAYOUT m_BuiltinLayout[ARRAY_SIZE(m_BuiltinLayoutBase)]; static KEYBOARD_ROLE_BINDING m_Layout[INPUT_LAYOUT_NUMBER_OF] [INPUT_ROLE_NUMBER_OF]; // Update internal controller button/axis state from SDL events. // @param event Event to process. static void M_ProcessEvent(const SDL_Event *const event) { switch (event->type) { case SDL_KEYDOWN: if (!event->key.repeat) { m_KeyboardState[event->key.keysym.scancode] = true; } break; case SDL_KEYUP: m_KeyboardState[event->key.keysym.scancode] = false; break; default: break; } } static const char *M_GetScancodeName(SDL_Scancode scancode) { // clang-format off switch (scancode) { case SDL_SCANCODE_LCTRL: return "\\{keyboard l_ctrl}"; case SDL_SCANCODE_RCTRL: return "\\{keyboard r_ctrl}"; case SDL_SCANCODE_RSHIFT: return "\\{keyboard r_shift}"; case SDL_SCANCODE_LSHIFT: return "\\{keyboard l_shift}"; case SDL_SCANCODE_RALT: return "\\{keyboard l_alt}"; case SDL_SCANCODE_LALT: return "\\{keyboard r_alt}"; case SDL_SCANCODE_LGUI: return "\\{keyboard l_win}"; case SDL_SCANCODE_RGUI: return "\\{keyboard r_win}"; case SDL_SCANCODE_LEFT: return "\\{keyboard left}"; case SDL_SCANCODE_UP: return "\\{keyboard up}"; case SDL_SCANCODE_RIGHT: return "\\{keyboard right}"; case SDL_SCANCODE_DOWN: return "\\{keyboard down}"; case SDL_SCANCODE_RETURN: return "\\{keyboard return}"; case SDL_SCANCODE_ESCAPE: return "\\{keyboard escape}"; case SDL_SCANCODE_BACKSPACE: return "\\{keyboard backspace}"; case SDL_SCANCODE_TAB: return "\\{keyboard tab}"; case SDL_SCANCODE_SPACE: return "\\{keyboard space}"; case SDL_SCANCODE_CAPSLOCK: return "\\{keyboard caps_lock}"; case SDL_SCANCODE_PRINTSCREEN: return "\\{keyboard print_screen}"; case SDL_SCANCODE_SCROLLLOCK: return "\\{keyboard scroll_lock}"; case SDL_SCANCODE_PAUSE: return "\\{keyboard pause}"; case SDL_SCANCODE_INSERT: return "\\{keyboard insert}"; case SDL_SCANCODE_HOME: return "\\{keyboard home}"; case SDL_SCANCODE_PAGEUP: return "\\{keyboard page_up}"; case SDL_SCANCODE_DELETE: return "\\{keyboard delete}"; case SDL_SCANCODE_END: return "\\{keyboard end}"; case SDL_SCANCODE_PAGEDOWN: return "\\{keyboard page_down}"; case SDL_SCANCODE_A: return "\\{keyboard a}"; case SDL_SCANCODE_B: return "\\{keyboard b}"; case SDL_SCANCODE_C: return "\\{keyboard c}"; case SDL_SCANCODE_D: return "\\{keyboard d}"; case SDL_SCANCODE_E: return "\\{keyboard e}"; case SDL_SCANCODE_F: return "\\{keyboard f}"; case SDL_SCANCODE_G: return "\\{keyboard g}"; case SDL_SCANCODE_H: return "\\{keyboard h}"; case SDL_SCANCODE_I: return "\\{keyboard i}"; case SDL_SCANCODE_J: return "\\{keyboard j}"; case SDL_SCANCODE_K: return "\\{keyboard k}"; case SDL_SCANCODE_L: return "\\{keyboard l}"; case SDL_SCANCODE_M: return "\\{keyboard m}"; case SDL_SCANCODE_N: return "\\{keyboard n}"; case SDL_SCANCODE_O: return "\\{keyboard o}"; case SDL_SCANCODE_P: return "\\{keyboard p}"; case SDL_SCANCODE_Q: return "\\{keyboard q}"; case SDL_SCANCODE_R: return "\\{keyboard r}"; case SDL_SCANCODE_S: return "\\{keyboard s}"; case SDL_SCANCODE_T: return "\\{keyboard t}"; case SDL_SCANCODE_U: return "\\{keyboard u}"; case SDL_SCANCODE_V: return "\\{keyboard v}"; case SDL_SCANCODE_W: return "\\{keyboard w}"; case SDL_SCANCODE_X: return "\\{keyboard x}"; case SDL_SCANCODE_Y: return "\\{keyboard y}"; case SDL_SCANCODE_Z: return "\\{keyboard z}"; case SDL_SCANCODE_0: return "\\{keyboard 0}"; case SDL_SCANCODE_1: return "\\{keyboard 1}"; case SDL_SCANCODE_2: return "\\{keyboard 2}"; case SDL_SCANCODE_3: return "\\{keyboard 3}"; case SDL_SCANCODE_4: return "\\{keyboard 4}"; case SDL_SCANCODE_5: return "\\{keyboard 5}"; case SDL_SCANCODE_6: return "\\{keyboard 6}"; case SDL_SCANCODE_7: return "\\{keyboard 7}"; case SDL_SCANCODE_8: return "\\{keyboard 8}"; case SDL_SCANCODE_9: return "\\{keyboard 9}"; case SDL_SCANCODE_MINUS: return "\\{keyboard minus}"; case SDL_SCANCODE_EQUALS: return "\\{keyboard equals}"; case SDL_SCANCODE_LEFTBRACKET: return "\\{keyboard left_square_bracket}"; case SDL_SCANCODE_RIGHTBRACKET: return "\\{keyboard right_square_bracket}"; case SDL_SCANCODE_BACKSLASH: return "\\{keyboard backslash}"; case SDL_SCANCODE_NONUSHASH: return "\\{keyboard hash}"; case SDL_SCANCODE_SEMICOLON: return "\\{keyboard semicolon}"; case SDL_SCANCODE_APOSTROPHE: return "\\{keyboard apostrophe}"; case SDL_SCANCODE_GRAVE: return "\\{keyboard backtick}"; case SDL_SCANCODE_COMMA: return "\\{keyboard comma}"; case SDL_SCANCODE_PERIOD: return "\\{keyboard period}"; case SDL_SCANCODE_SLASH: return "\\{keyboard slash}"; case SDL_SCANCODE_NONUSBACKSLASH: return "\\{keyboard backslash}"; case SDL_SCANCODE_F1: return "\\{keyboard f1}"; case SDL_SCANCODE_F2: return "\\{keyboard f2}"; case SDL_SCANCODE_F3: return "\\{keyboard f3}"; case SDL_SCANCODE_F4: return "\\{keyboard f4}"; case SDL_SCANCODE_F5: return "\\{keyboard f5}"; case SDL_SCANCODE_F6: return "\\{keyboard f6}"; case SDL_SCANCODE_F7: return "\\{keyboard f7}"; case SDL_SCANCODE_F8: return "\\{keyboard f8}"; case SDL_SCANCODE_F9: return "\\{keyboard f9}"; case SDL_SCANCODE_F10: return "\\{keyboard f10}"; case SDL_SCANCODE_F11: return "\\{keyboard f11}"; case SDL_SCANCODE_F12: return "\\{keyboard f12}"; case SDL_SCANCODE_F13: return "\\{keyboard f13}"; case SDL_SCANCODE_F14: return "\\{keyboard f14}"; case SDL_SCANCODE_F15: return "\\{keyboard f15}"; case SDL_SCANCODE_F16: return "\\{keyboard f16}"; case SDL_SCANCODE_F17: return "\\{keyboard f17}"; case SDL_SCANCODE_F18: return "\\{keyboard f18}"; case SDL_SCANCODE_F19: return "\\{keyboard f19}"; case SDL_SCANCODE_F20: return "\\{keyboard f20}"; case SDL_SCANCODE_F21: return "\\{keyboard f21}"; case SDL_SCANCODE_F22: return "\\{keyboard f22}"; case SDL_SCANCODE_F23: return "\\{keyboard f23}"; case SDL_SCANCODE_F24: return "\\{keyboard f24}"; case SDL_SCANCODE_NUMLOCKCLEAR: return "\\{keyboard num_lock}"; case SDL_SCANCODE_KP_0: return "\\{keyboard num_0}"; case SDL_SCANCODE_KP_1: return "\\{keyboard num_1}"; case SDL_SCANCODE_KP_2: return "\\{keyboard num_2}"; case SDL_SCANCODE_KP_3: return "\\{keyboard num_3}"; case SDL_SCANCODE_KP_4: return "\\{keyboard num_4}"; case SDL_SCANCODE_KP_5: return "\\{keyboard num_5}"; case SDL_SCANCODE_KP_6: return "\\{keyboard num_6}"; case SDL_SCANCODE_KP_7: return "\\{keyboard num_7}"; case SDL_SCANCODE_KP_8: return "\\{keyboard num_8}"; case SDL_SCANCODE_KP_9: return "\\{keyboard num_9}"; case SDL_SCANCODE_KP_PERIOD: return "\\{keyboard num_period}"; case SDL_SCANCODE_KP_DIVIDE: return "\\{keyboard num_divide}"; case SDL_SCANCODE_KP_MULTIPLY: return "\\{keyboard num_multiply}"; case SDL_SCANCODE_KP_MINUS: return "\\{keyboard num_minus}"; case SDL_SCANCODE_KP_PLUS: return "\\{keyboard num_plus}"; case SDL_SCANCODE_KP_EQUALS: return "\\{keyboard num_equals}"; case SDL_SCANCODE_KP_EQUALSAS400: return "\\{keyboard num_equals}"; case SDL_SCANCODE_KP_COMMA: return "\\{keyboard num_comma}"; case SDL_SCANCODE_KP_ENTER: return "\\{keyboard num_enter}"; // extra keys case SDL_SCANCODE_APPLICATION: return "MENU"; case SDL_SCANCODE_POWER: return "POWER"; case SDL_SCANCODE_EXECUTE: return "EXEC"; case SDL_SCANCODE_HELP: return "HELP"; case SDL_SCANCODE_MENU: return "MENU"; case SDL_SCANCODE_SELECT: return "SEL"; case SDL_SCANCODE_STOP: return "STOP"; case SDL_SCANCODE_AGAIN: return "AGAIN"; case SDL_SCANCODE_UNDO: return "UNDO"; case SDL_SCANCODE_CUT: return "CUT"; case SDL_SCANCODE_COPY: return "COPY"; case SDL_SCANCODE_PASTE: return "PASTE"; case SDL_SCANCODE_FIND: return "FIND"; case SDL_SCANCODE_MUTE: return "MUTE"; case SDL_SCANCODE_VOLUMEUP: return "VOLUP"; case SDL_SCANCODE_VOLUMEDOWN: return "VOLDN"; case SDL_SCANCODE_ALTERASE: return "ALTER"; case SDL_SCANCODE_SYSREQ: return "SYSRQ"; case SDL_SCANCODE_CANCEL: return "CNCEL"; case SDL_SCANCODE_CLEAR: return "CLEAR"; case SDL_SCANCODE_PRIOR: return "PRIOR"; case SDL_SCANCODE_RETURN2: return "RETURN"; case SDL_SCANCODE_SEPARATOR: return "SEP"; case SDL_SCANCODE_OUT: return "OUT"; case SDL_SCANCODE_OPER: return "OPER"; case SDL_SCANCODE_CLEARAGAIN: return "CLEAR"; case SDL_SCANCODE_CRSEL: return "CRSEL"; case SDL_SCANCODE_EXSEL: return "EXSEL"; case SDL_SCANCODE_KP_00: return "PAD00"; case SDL_SCANCODE_KP_000: return "PAD000"; case SDL_SCANCODE_THOUSANDSSEPARATOR: return "TSEP"; case SDL_SCANCODE_DECIMALSEPARATOR: return "DSEP"; case SDL_SCANCODE_CURRENCYUNIT: return "CURU"; case SDL_SCANCODE_CURRENCYSUBUNIT: return "CURSU"; case SDL_SCANCODE_KP_LEFTPAREN: return "PAD("; case SDL_SCANCODE_KP_RIGHTPAREN: return "PAD)"; case SDL_SCANCODE_KP_LEFTBRACE: return "PAD{"; case SDL_SCANCODE_KP_RIGHTBRACE: return "PAD}"; case SDL_SCANCODE_KP_TAB: return "PADT"; case SDL_SCANCODE_KP_BACKSPACE: return "PADBK"; case SDL_SCANCODE_KP_A: return "PADA"; case SDL_SCANCODE_KP_B: return "PADB"; case SDL_SCANCODE_KP_C: return "PADC"; case SDL_SCANCODE_KP_D: return "PADD"; case SDL_SCANCODE_KP_E: return "PADE"; case SDL_SCANCODE_KP_F: return "PADF"; case SDL_SCANCODE_KP_XOR: return "PADXR"; case SDL_SCANCODE_KP_POWER: return "PAD^"; case SDL_SCANCODE_KP_PERCENT: return "PAD%"; case SDL_SCANCODE_KP_LESS: return "PAD<"; case SDL_SCANCODE_KP_GREATER: return "PAD>"; case SDL_SCANCODE_KP_AMPERSAND: return "PAD&"; case SDL_SCANCODE_KP_DBLAMPERSAND: return "PAD&&"; case SDL_SCANCODE_KP_VERTICALBAR: return "PAD|"; case SDL_SCANCODE_KP_DBLVERTICALBAR: return "PAD||"; case SDL_SCANCODE_KP_COLON: return "PAD:"; case SDL_SCANCODE_KP_HASH: return "PAD#"; case SDL_SCANCODE_KP_SPACE: return "PADSP"; case SDL_SCANCODE_KP_AT: return "PAD@"; case SDL_SCANCODE_KP_EXCLAM: return "PAD!"; case SDL_SCANCODE_KP_MEMSTORE: return "PADMS"; case SDL_SCANCODE_KP_MEMRECALL: return "PADMR"; case SDL_SCANCODE_KP_MEMCLEAR: return "PADMC"; case SDL_SCANCODE_KP_MEMADD: return "PADMA"; case SDL_SCANCODE_KP_MEMSUBTRACT: return "PADM-"; case SDL_SCANCODE_KP_MEMMULTIPLY: return "PADM*"; case SDL_SCANCODE_KP_MEMDIVIDE: return "PADM/"; case SDL_SCANCODE_KP_PLUSMINUS: return "PAD+-"; case SDL_SCANCODE_KP_CLEAR: return "PADCL"; case SDL_SCANCODE_KP_CLEARENTRY: return "PADCL"; case SDL_SCANCODE_KP_BINARY: return "PAD02"; case SDL_SCANCODE_KP_OCTAL: return "PAD08"; case SDL_SCANCODE_KP_DECIMAL: return "PAD10"; case SDL_SCANCODE_KP_HEXADECIMAL: return "PAD16"; case SDL_SCANCODE_MODE: return "MODE"; case SDL_SCANCODE_AUDIONEXT: return "NEXT"; case SDL_SCANCODE_AUDIOPREV: return "PREV"; case SDL_SCANCODE_AUDIOSTOP: return "STOP"; case SDL_SCANCODE_AUDIOPLAY: return "PLAY"; case SDL_SCANCODE_AUDIOMUTE: return "MUTE"; case SDL_SCANCODE_MEDIASELECT: return "MEDIA"; case SDL_SCANCODE_WWW: return "WWW"; case SDL_SCANCODE_MAIL: return "MAIL"; case SDL_SCANCODE_CALCULATOR: return "CALC"; case SDL_SCANCODE_COMPUTER: return "COMP"; case SDL_SCANCODE_AC_SEARCH: return "SRCH"; case SDL_SCANCODE_AC_HOME: return "HOME"; case SDL_SCANCODE_AC_BACK: return "BACK"; case SDL_SCANCODE_AC_FORWARD: return "FRWD"; case SDL_SCANCODE_AC_STOP: return "STOP"; case SDL_SCANCODE_AC_REFRESH: return "RFRSH"; case SDL_SCANCODE_AC_BOOKMARKS: return "BKMK"; case SDL_SCANCODE_BRIGHTNESSDOWN: return "BNDN"; case SDL_SCANCODE_BRIGHTNESSUP: return "BNUP"; case SDL_SCANCODE_DISPLAYSWITCH: return "DPSW"; case SDL_SCANCODE_KBDILLUMTOGGLE: return "KBDIT"; case SDL_SCANCODE_KBDILLUMDOWN: return "KBDID"; case SDL_SCANCODE_KBDILLUMUP: return "KBDIU"; case SDL_SCANCODE_EJECT: return "EJECT"; case SDL_SCANCODE_SLEEP: return "SLEEP"; case SDL_SCANCODE_APP1: return "APP1"; case SDL_SCANCODE_APP2: return "APP2"; case SDL_SCANCODE_AUDIOREWIND: return "RWND"; case SDL_SCANCODE_AUDIOFASTFORWARD: return "FF"; case SDL_SCANCODE_UNKNOWN: return nullptr; default: return "\\{keyboard unknown}"; } // clang-format on } static bool M_CheckScancode(const SDL_Scancode scancode) { if (scancode == SDL_SCANCODE_UNKNOWN) { return false; } if (scancode == SDL_SCANCODE_RETURN && KEY_DOWN(SDL_SCANCODE_LALT)) { return false; } #ifdef _WIN32 if (scancode == SDL_SCANCODE_F4 && (KEY_DOWN(SDL_SCANCODE_LALT) || KEY_DOWN(SDL_SCANCODE_RALT))) { return false; } #endif if (KEY_DOWN(scancode)) { return true; } if (scancode == SDL_SCANCODE_LCTRL) { return KEY_DOWN(SDL_SCANCODE_RCTRL); } if (scancode == SDL_SCANCODE_RCTRL) { return KEY_DOWN(SDL_SCANCODE_LCTRL); } if (scancode == SDL_SCANCODE_LSHIFT) { return KEY_DOWN(SDL_SCANCODE_RSHIFT); } if (scancode == SDL_SCANCODE_RSHIFT) { return KEY_DOWN(SDL_SCANCODE_LSHIFT); } if (scancode == SDL_SCANCODE_LALT) { return KEY_DOWN(SDL_SCANCODE_RALT); } if (scancode == SDL_SCANCODE_RALT) { return KEY_DOWN(SDL_SCANCODE_LALT); } return false; } static bool M_CheckBinding(const KEYBOARD_BINDING *const bind) { if (bind->key_count == 0) { return false; } for (int32_t k = 0; k < bind->key_count; k++) { if (!M_CheckScancode(bind->keys[k])) { return false; } } return true; } // Combo adapter forward declarations. static INPUT_COMBO_BINDING M_GetComboBinding( INPUT_LAYOUT layout, INPUT_ROLE role, int32_t slot); static bool M_ComboKeysEqual(const void *a, const void *b); static bool M_Key(const INPUT_LAYOUT layout, const INPUT_ROLE role) { for (int32_t slot = 0; slot < INPUT_BINDING_SLOTS; slot++) { const KEYBOARD_BINDING *bind = &m_Layout[layout][role].slots[slot]; if (bind->key_count >= 2 && Input_ComboIsKeyImmediate( layout, &bind->keys[0], M_GetComboBinding, M_ComboKeysEqual)) { continue; } if (M_CheckBinding(bind)) { return true; } } return false; } static const KEYBOARD_BINDING *M_GetBinding( const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot) { return &m_Layout[layout][role].slots[slot]; } static bool M_BindingsEqual( const KEYBOARD_BINDING *const a, const KEYBOARD_BINDING *const b) { if (a->key_count != b->key_count || a->key_count == 0) { return false; } for (int32_t i = 0; i < a->key_count; i++) { if (a->keys[i] != b->keys[i]) { return false; } } return true; } static bool M_CheckConflict( const INPUT_LAYOUT layout, const INPUT_ROLE role1, const INPUT_ROLE role2) { for (int32_t s1 = 0; s1 < INPUT_BINDING_SLOTS; s1++) { const KEYBOARD_BINDING *b1 = M_GetBinding(layout, role1, s1); if (b1->key_count == 0) { continue; } for (int32_t s2 = 0; s2 < INPUT_BINDING_SLOTS; s2++) { const KEYBOARD_BINDING *b2 = M_GetBinding(layout, role2, s2); if (M_BindingsEqual(b1, b2)) { return true; } } } return false; } static void M_AssignConflict( const INPUT_LAYOUT layout, const INPUT_ROLE role, bool conflict) { m_Conflicts[layout][role] = conflict; } static void M_CheckConflicts(const INPUT_LAYOUT layout) { Input_ConflictHelper(layout, M_CheckConflict, M_AssignConflict); Input_ComboCheckConflicts( layout, M_GetComboBinding, M_ComboKeysEqual, m_Conflicts[layout]); } static void M_AssignBinding( const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot, const KEYBOARD_BINDING *const bind) { m_Layout[layout][role].slots[slot] = *bind; M_CheckConflicts(layout); } static void M_ResetLayout(const INPUT_LAYOUT layout) { for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) { m_Layout[layout][role] = m_Layout[INPUT_LAYOUT_DEFAULT][role]; } M_CheckConflicts(layout); } static BUILTIN_KEYBOARD_LAYOUT *M_GetBuiltInLayout(const INPUT_ROLE role) { for (int32_t i = 0; m_BuiltinLayout[i].role != (INPUT_ROLE)-1; i++) { BUILTIN_KEYBOARD_LAYOUT *const builtin = &m_BuiltinLayout[i]; if (builtin->role == role) { return builtin; } } return nullptr; } static void M_HandleBuiltInDefaults(void) { #define L_BIND(role, code) \ do { \ M_GetBuiltInLayout(role)->scancode = code; \ } while (0) if (g_TRVersion == 2) { L_BIND(INPUT_ROLE_EQUIP_MAGNUMS, SDL_SCANCODE_UNKNOWN); L_BIND(INPUT_ROLE_EQUIP_AUTOS, SDL_SCANCODE_3); } else if (g_TRVersion == 3) { L_BIND(INPUT_ROLE_USE_SMALL_MEDI, SDL_SCANCODE_0); L_BIND(INPUT_ROLE_USE_BIG_MEDI, SDL_SCANCODE_9); L_BIND(INPUT_ROLE_EQUIP_MAGNUMS, SDL_SCANCODE_UNKNOWN); L_BIND(INPUT_ROLE_EQUIP_DESERT_EAGLE, SDL_SCANCODE_3); L_BIND(INPUT_ROLE_EQUIP_M16, SDL_SCANCODE_UNKNOWN); L_BIND(INPUT_ROLE_EQUIP_MP5, SDL_SCANCODE_6); L_BIND(INPUT_ROLE_EQUIP_ROCKET_LAUNCHER, SDL_SCANCODE_7); L_BIND(INPUT_ROLE_EQUIP_GRENADE_LAUNCHER, SDL_SCANCODE_8); } #undef L_BIND } static void M_Init(void) { memcpy(m_BuiltinLayout, m_BuiltinLayoutBase, sizeof(m_BuiltinLayout)); // first, reset all roles to unbound for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) { for (int32_t slot = 0; slot < INPUT_BINDING_SLOTS; slot++) { m_Layout[INPUT_LAYOUT_DEFAULT][role].slots[slot] = (KEYBOARD_BINDING) { .key_count = 0 }; } } // allow specific engines to re-assign default bindings M_HandleBuiltInDefaults(); // then load actually defined default bindings into slot 0 for (int32_t i = 0; m_BuiltinLayout[i].role != (INPUT_ROLE)-1; i++) { const BUILTIN_KEYBOARD_LAYOUT *const builtin = &m_BuiltinLayout[i]; m_Layout[INPUT_LAYOUT_DEFAULT][builtin->role].slots[0] = (KEYBOARD_BINDING) { .key_count = builtin->scancode != SDL_SCANCODE_UNKNOWN ? 1 : 0, .keys = { builtin->scancode }, }; } M_CheckConflicts(INPUT_LAYOUT_DEFAULT); for (int32_t layout = INPUT_LAYOUT_CUSTOM_1; layout < INPUT_LAYOUT_NUMBER_OF; layout++) { M_ResetLayout(layout); } } static bool M_CustomUpdate(INPUT_STATE *const result, const INPUT_LAYOUT layout) { // we only do this for keyboard input result->menu_confirm |= result->action; result->toggle_fullscreen = KEY_DOWN(SDL_SCANCODE_RETURN) && KEY_DOWN(SDL_SCANCODE_LALT); result->menu_skip = result->menu_confirm || result->menu_back; return true; } static bool M_IsPressed(const INPUT_LAYOUT layout, const INPUT_ROLE role) { return M_Key(layout, role); } static bool M_IsRoleConflicted(const INPUT_LAYOUT layout, const INPUT_ROLE role) { return m_Conflicts[layout][role]; } static const char *M_GetName( const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot) { const KEYBOARD_BINDING *bind = M_GetBinding(layout, role, slot); if (bind->key_count == 0) { return nullptr; } if (bind->key_count == 1) { return M_GetScancodeName(bind->keys[0]); } // Build composite name for multi-key combo static char buf[256]; buf[0] = '\0'; for (int32_t k = 0; k < bind->key_count; k++) { if (k > 0) { strcat(buf, "+"); } const char *name = M_GetScancodeName(bind->keys[k]); if (name != nullptr) { strcat(buf, name); } } return buf; } static void M_UnassignRole( const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot) { const KEYBOARD_BINDING empty = { .key_count = 0 }; M_AssignBinding(layout, role, slot, &empty); } static bool M_AssignFromJSONObject( const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot, JSON_OBJECT *const bind_obj) { JSON_ARRAY *const combo_arr = JSON_ObjectGetArray(bind_obj, "combo"); if (combo_arr != nullptr) { // New combo format: "combo": [scancode1, scancode2, ...] const int32_t count = combo_arr->length < INPUT_COMBO_MAX_KEYS ? (int32_t)combo_arr->length : INPUT_COMBO_MAX_KEYS; KEYBOARD_BINDING bind = { .key_count = count }; for (int32_t i = 0; i < count; i++) { bind.keys[i] = JSON_ArrayGetInt(combo_arr, i, SDL_SCANCODE_UNKNOWN); } M_AssignBinding(layout, role, slot, &bind); } else { // Legacy single-key format: "scancode": N const KEYBOARD_BINDING *current = M_GetBinding(layout, role, slot); const SDL_Scancode default_sc = current->key_count > 0 ? current->keys[0] : SDL_SCANCODE_UNKNOWN; const SDL_Scancode user_sc = JSON_ObjectGetInt(bind_obj, "scancode", default_sc); const KEYBOARD_BINDING bind = { .key_count = user_sc != SDL_SCANCODE_UNKNOWN ? 1 : 0, .keys = { user_sc }, }; M_AssignBinding(layout, role, slot, &bind); } return true; } static bool M_AssignToJSONObject( const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot, JSON_OBJECT *const bind_obj) { const KEYBOARD_BINDING *user = M_GetBinding(layout, role, slot); const KEYBOARD_BINDING *def = M_GetBinding(INPUT_LAYOUT_DEFAULT, role, slot); if (M_BindingsEqual(user, def) || (user->key_count == 0 && def->key_count == 0)) { return false; } if (user->key_count <= 1) { // Single key: use legacy "scancode" for backward compatibility JSON_ObjectAppendInt( bind_obj, "scancode", user->key_count == 1 ? user->keys[0] : SDL_SCANCODE_UNKNOWN); } else { // Multi-key combo JSON_ARRAY *const arr = JSON_ArrayNew(); for (int32_t i = 0; i < user->key_count; i++) { JSON_ArrayAppendInt(arr, user->keys[i]); } JSON_ObjectAppendArray(bind_obj, "combo", arr); } return true; } // Per-scancode tracking for combo prefix deferral. static bool m_PrefixWasHeld[SDL_NUM_SCANCODES]; static bool m_PrefixComboFired[SDL_NUM_SCANCODES]; // Combo adapter functions for the shared combo layer. static INPUT_COMBO_BINDING M_GetComboBinding( const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot) { const KEYBOARD_BINDING *b = M_GetBinding(layout, role, slot); return (INPUT_COMBO_BINDING) { .key_count = b->key_count, .keys = b->keys, .key_stride = sizeof(SDL_Scancode), }; } static bool M_ComboKeysEqual(const void *const a, const void *const b) { return *(const SDL_Scancode *)a == *(const SDL_Scancode *)b; } static INPUT_COMBO_BINDING M_ToCombo(const KEYBOARD_BINDING *const b) { return (INPUT_COMBO_BINDING) { .key_count = b->key_count, .keys = b->keys, .key_stride = sizeof(SDL_Scancode), }; } static const KEYBOARD_BINDING *M_GetPressedBinding( const INPUT_LAYOUT layout, const INPUT_ROLE role) { for (int32_t slot = 0; slot < INPUT_BINDING_SLOTS; slot++) { const KEYBOARD_BINDING *bind = M_GetBinding(layout, role, slot); if (M_CheckBinding(bind)) { return bind; } } return nullptr; } // Per-role deferral tracking for combo disambiguation. static bool m_RoleWasActive[INPUT_ROLE_NUMBER_OF]; static bool m_RoleLongerFired[INPUT_ROLE_NUMBER_OF]; static void M_ResolveCombos( const INPUT_LAYOUT layout, INPUT_STATE *const result) { // Phase 1: Collect active bindings. const KEYBOARD_BINDING *active[INPUT_ROLE_NUMBER_OF] = {}; for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) { if (InputState_GetRole(*result, role)) { active[role] = M_GetPressedBinding(layout, role); } } // Suppress invalid combos (non-capturing sustained + immediate). for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) { if (active[role] != nullptr && Input_ComboSustainedHasImmediate( layout, M_ToCombo(active[role]), M_GetComboBinding, M_ComboKeysEqual)) { InputState_ClearRole(result, role); active[role] = nullptr; } } // Phase 2: Subset suppression — longer active combos suppress shorter. for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) { if (active[role] == nullptr) { continue; } for (INPUT_ROLE other = 0; other < INPUT_ROLE_NUMBER_OF; other++) { if (other == role || active[other] == nullptr) { continue; } if (Input_ComboIsProperSubset( M_ToCombo(active[role]), M_ToCombo(active[other]), M_ComboKeysEqual)) { InputState_ClearRole(result, role); break; } } } // Phase 3: Combo deferral — if an active combo's binding is a proper // subset of some (not necessarily active) longer binding, defer it. // This handles both single-key and multi-key prefix disambiguation. for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) { if (active[role] == nullptr) { continue; } if (((Input_IsRoleImmediate(role) || Input_IsRoleSustained(role)) && active[role]->key_count <= 1) || !Input_IsRoleRebindable(role)) { continue; } if (Input_ComboHasLonger( layout, role, M_ToCombo(active[role]), M_GetComboBinding, M_ComboKeysEqual)) { InputState_ClearRole(result, role); if (!m_RoleWasActive[role]) { m_RoleLongerFired[role] = false; } m_RoleWasActive[role] = true; } } // Reset prefix tracking for newly pressed keys. for (SDL_Scancode sc = 0; sc < SDL_NUM_SCANCODES; sc++) { if (KEY_DOWN(sc) && !m_PrefixWasHeld[sc]) { m_PrefixComboFired[sc] = false; } } // Phase 4: Mark longer-combo-fired state. // When a combo fires, mark all shorter deferred combos as superseded. for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) { if (active[role] == nullptr || active[role]->key_count < 2) { continue; } // Mark scancodes for single-key prefix deferral. for (int32_t k = 0; k < active[role]->key_count; k++) { m_PrefixComboFired[active[role]->keys[k]] = true; } // Mark shorter deferred roles as superseded. for (INPUT_ROLE other = 0; other < INPUT_ROLE_NUMBER_OF; other++) { if (!m_RoleWasActive[other]) { continue; } const KEYBOARD_BINDING *ob = M_GetPressedBinding(layout, other); if (ob != nullptr && Input_ComboIsProperSubset( M_ToCombo(ob), M_ToCombo(active[role]), M_ComboKeysEqual)) { m_RoleLongerFired[other] = true; } } } // Phase 5: Fire deferred roles on release. // When a deferred role's binding is no longer fully pressed and no // longer combo fired, inject the role for one frame. for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) { if (!m_RoleWasActive[role]) { continue; } const KEYBOARD_BINDING *bind = M_GetPressedBinding(layout, role); if (bind != nullptr) { continue; } if (!m_RoleLongerFired[role]) { InputState_SetRole(result, role, true); } m_RoleWasActive[role] = false; m_RoleLongerFired[role] = false; } // Phase 6: Single-key prefix deferral. for (SDL_Scancode sc = 0; sc < SDL_NUM_SCANCODES; sc++) { const bool held = KEY_DOWN(sc); if (held && Input_ComboIsStarter( layout, &sc, M_GetComboBinding, M_ComboKeysEqual)) { const INPUT_ROLE role = Input_ComboFindDeferrableRole( layout, &sc, M_GetComboBinding, M_ComboKeysEqual); if (role != (INPUT_ROLE)-1) { InputState_ClearRole(result, role); } } if (!held && m_PrefixWasHeld[sc] && !m_PrefixComboFired[sc]) { const INPUT_ROLE role = Input_ComboFindDeferrableRole( layout, &sc, M_GetComboBinding, M_ComboKeysEqual); if (role != (INPUT_ROLE)-1) { InputState_SetRole(result, role, true); } } m_PrefixWasHeld[sc] = held; } } // Combo capture state for listen mode. static KEYBOARD_BINDING m_CaptureBuffer = { .key_count = 0 }; static bool m_CaptureActive = false; static bool M_CaptureHasKey(const SDL_Scancode scancode) { for (int32_t i = 0; i < m_CaptureBuffer.key_count; i++) { if (m_CaptureBuffer.keys[i] == scancode) { return true; } } return false; } static bool M_ReadAndAssign( const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot) { // Count currently held keys and accumulate new ones into the buffer. bool any_held = false; for (SDL_Scancode sc = 0; sc < SDL_NUM_SCANCODES; sc++) { if (!KEY_DOWN(sc)) { continue; } any_held = true; if (!M_CaptureHasKey(sc) && m_CaptureBuffer.key_count < INPUT_COMBO_MAX_KEYS) { m_CaptureBuffer.keys[m_CaptureBuffer.key_count++] = sc; } m_CaptureActive = true; } // If the first key captured is bound to an immediate role (movement, // action, etc.), assign as single key right away — don't wait for combo. if (m_CaptureActive && m_CaptureBuffer.key_count == 1 && any_held && Input_ComboIsKeyImmediate( layout, &m_CaptureBuffer.keys[0], M_GetComboBinding, M_ComboKeysEqual)) { M_AssignBinding(layout, role, slot, &m_CaptureBuffer); m_CaptureBuffer.key_count = 0; m_CaptureActive = false; return true; } // All keys released after at least one was captured — assign the chord. if (!any_held && m_CaptureActive) { M_AssignBinding(layout, role, slot, &m_CaptureBuffer); m_CaptureBuffer.key_count = 0; m_CaptureActive = false; return true; } return false; } INPUT_BACKEND_IMPL g_Input_Keyboard = { .init = M_Init, .shutdown = nullptr, .discover = nullptr, .process_event = M_ProcessEvent, .custom_update = M_CustomUpdate, .is_pressed = M_IsPressed, .is_role_conflicted = M_IsRoleConflicted, .get_name = M_GetName, .unassign_role = M_UnassignRole, .assign_from_json_object = M_AssignFromJSONObject, .assign_to_json_object = M_AssignToJSONObject, .reset_layout = M_ResetLayout, .read_and_assign = M_ReadAndAssign, .resolve_combos = M_ResolveCombos, }; ================================================ FILE: src/trx/game/input/backends/keyboard.def ================================================ INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_UP, SDL_SCANCODE_UP) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_DOWN, SDL_SCANCODE_DOWN) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_LEFT, SDL_SCANCODE_LEFT) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_RIGHT, SDL_SCANCODE_RIGHT) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_JUMP, SDL_SCANCODE_RALT) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_DRAW_WEAPON, SDL_SCANCODE_SPACE) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_ACTION, SDL_SCANCODE_RCTRL) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_SLOW, SDL_SCANCODE_RSHIFT) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_LOOK, SDL_SCANCODE_KP_0) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_STEP_LEFT, SDL_SCANCODE_DELETE) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_STEP_RIGHT, SDL_SCANCODE_PAGEDOWN) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_ROLL, SDL_SCANCODE_END) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_SPRINT, SDL_SCANCODE_PERIOD) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_CROUCH, SDL_SCANCODE_C) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_ENTER_CONSOLE, SDL_SCANCODE_SLASH) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_SAVE, SDL_SCANCODE_F5) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_LOAD, SDL_SCANCODE_F6) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_QUICK_SAVE, SDL_SCANCODE_UNKNOWN) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_QUICK_LOAD, SDL_SCANCODE_UNKNOWN) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_PAUSE, SDL_SCANCODE_P) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_TOGGLE_UI, SDL_SCANCODE_H) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_TOGGLE_PHOTO_MODE, SDL_SCANCODE_F1) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_CHANGE_TARGET, SDL_SCANCODE_Z) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_FLY_CHEAT, SDL_SCANCODE_O) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_ITEM_CHEAT, SDL_SCANCODE_I) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_LEVEL_SKIP_CHEAT, SDL_SCANCODE_L) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_TURBO_CHEAT, SDL_SCANCODE_TAB) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_CAMERA_UP, SDL_SCANCODE_Q) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_CAMERA_DOWN, SDL_SCANCODE_E) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_CAMERA_FORWARD, SDL_SCANCODE_W) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_CAMERA_BACK, SDL_SCANCODE_S) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_CAMERA_LEFT, SDL_SCANCODE_A) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_CAMERA_RIGHT, SDL_SCANCODE_D) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_EQUIP_PISTOLS, SDL_SCANCODE_1) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_EQUIP_SHOTGUN, SDL_SCANCODE_2) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_EQUIP_MAGNUMS, SDL_SCANCODE_3) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_EQUIP_AUTOS, SDL_SCANCODE_UNKNOWN) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_EQUIP_DESERT_EAGLE, SDL_SCANCODE_UNKNOWN) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_EQUIP_UZIS, SDL_SCANCODE_4) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_EQUIP_HARPOON, SDL_SCANCODE_5) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_EQUIP_M16, SDL_SCANCODE_6) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_EQUIP_MP5, SDL_SCANCODE_UNKNOWN) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_EQUIP_GRENADE_LAUNCHER, SDL_SCANCODE_7) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_EQUIP_ROCKET_LAUNCHER, SDL_SCANCODE_UNKNOWN) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_USE_SMALL_MEDI, SDL_SCANCODE_8) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_USE_BIG_MEDI, SDL_SCANCODE_9) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_USE_FLARE, SDL_SCANCODE_COMMA) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_TOGGLE_FULLSCREEN, SDL_SCANCODE_UNKNOWN) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_FPS, SDL_SCANCODE_F2) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_TOGGLE_BILINEAR_FILTER, SDL_SCANCODE_F3) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_TOGGLE_TRAPEZOID_FILTER, SDL_SCANCODE_F4) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_TOGGLE_WIREFRAME, SDL_SCANCODE_F7) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_TOGGLE_TEXTURES, SDL_SCANCODE_F8) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_CYCLE_LIGHTING_CONTRAST, SDL_SCANCODE_F9) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_CHANGE_OUTFIT, SDL_SCANCODE_T) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_RESET_BINDINGS, SDL_SCANCODE_R) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_UNBIND_KEY, SDL_SCANCODE_BACKSPACE) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_INVENTORY, SDL_SCANCODE_ESCAPE) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_MENU_BACK, SDL_SCANCODE_ESCAPE) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_MENU_CONFIRM, SDL_SCANCODE_RETURN) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_MENU_UP, SDL_SCANCODE_UP) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_MENU_DOWN, SDL_SCANCODE_DOWN) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_MENU_LEFT, SDL_SCANCODE_LEFT) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_MENU_RIGHT, SDL_SCANCODE_RIGHT) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_MENU_TAB_LEFT, SDL_SCANCODE_DELETE) INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_MENU_TAB_RIGHT, SDL_SCANCODE_PAGEDOWN) ================================================ FILE: src/trx/game/input/backends/keyboard.h ================================================ #include extern INPUT_BACKEND_IMPL g_Input_Keyboard; ================================================ FILE: src/trx/game/input/combo.c ================================================ #include #include static bool M_IsKeyNonCapturingSustained( const INPUT_LAYOUT layout, const void *const key, const INPUT_COMBO_GET_BINDING get_binding, const INPUT_COMBO_KEYS_EQUAL keys_equal) { for (INPUT_ROLE r = 0; r < INPUT_ROLE_NUMBER_OF; r++) { if (!Input_IsRoleSustained(r) || Input_IsRoleCapturing(r)) { continue; } for (int32_t s = 0; s < INPUT_BINDING_SLOTS; s++) { const INPUT_COMBO_BINDING b = get_binding(layout, r, s); if (b.key_count == 1 && keys_equal(Input_ComboKeyAt(&b, 0), key)) { return true; } } } return false; } bool Input_ComboIsProperSubset( const INPUT_COMBO_BINDING sub, const INPUT_COMBO_BINDING super, const INPUT_COMBO_KEYS_EQUAL keys_equal) { if (sub.key_count == 0 || sub.key_count >= super.key_count) { return false; } for (int32_t i = 0; i < sub.key_count; i++) { bool found = false; for (int32_t j = 0; j < super.key_count; j++) { if (keys_equal( Input_ComboKeyAt(&sub, i), Input_ComboKeyAt(&super, j))) { found = true; break; } } if (!found) { return false; } } return true; } bool Input_ComboHasLonger( const INPUT_LAYOUT layout, const INPUT_ROLE skip_role, const INPUT_COMBO_BINDING bind, const INPUT_COMBO_GET_BINDING get_binding, const INPUT_COMBO_KEYS_EQUAL keys_equal) { for (INPUT_ROLE r = 0; r < INPUT_ROLE_NUMBER_OF; r++) { if (r == skip_role) { continue; } for (int32_t s = 0; s < INPUT_BINDING_SLOTS; s++) { const INPUT_COMBO_BINDING b = get_binding(layout, r, s); if (Input_ComboIsProperSubset(bind, b, keys_equal) && keys_equal( Input_ComboKeyAt(&bind, 0), Input_ComboKeyAt(&b, 0))) { return true; } } } return false; } bool Input_ComboIsStarter( const INPUT_LAYOUT layout, const void *const key, const INPUT_COMBO_GET_BINDING get_binding, const INPUT_COMBO_KEYS_EQUAL keys_equal) { for (INPUT_ROLE r = 0; r < INPUT_ROLE_NUMBER_OF; r++) { for (int32_t s = 0; s < INPUT_BINDING_SLOTS; s++) { const INPUT_COMBO_BINDING b = get_binding(layout, r, s); if (b.key_count >= 2 && keys_equal(Input_ComboKeyAt(&b, 0), key)) { return true; } } } return false; } INPUT_ROLE Input_ComboFindDeferrableRole( const INPUT_LAYOUT layout, const void *const key, const INPUT_COMBO_GET_BINDING get_binding, const INPUT_COMBO_KEYS_EQUAL keys_equal) { for (INPUT_ROLE r = 0; r < INPUT_ROLE_NUMBER_OF; r++) { if (Input_IsRoleImmediate(r) || Input_IsRoleSustained(r) || !Input_IsRoleRebindable(r)) { continue; } for (int32_t s = 0; s < INPUT_BINDING_SLOTS; s++) { const INPUT_COMBO_BINDING b = get_binding(layout, r, s); if (b.key_count == 1 && keys_equal(Input_ComboKeyAt(&b, 0), key)) { return r; } } } return (INPUT_ROLE)-1; } bool Input_ComboIsKeyImmediate( const INPUT_LAYOUT layout, const void *const key, const INPUT_COMBO_GET_BINDING get_binding, const INPUT_COMBO_KEYS_EQUAL keys_equal) { for (INPUT_ROLE r = 0; r < INPUT_ROLE_NUMBER_OF; r++) { if (!Input_IsRoleImmediate(r) || !Input_IsRoleRebindable(r)) { continue; } for (int32_t s = 0; s < INPUT_BINDING_SLOTS; s++) { const INPUT_COMBO_BINDING b = get_binding(layout, r, s); if (b.key_count == 1 && keys_equal(Input_ComboKeyAt(&b, 0), key)) { return true; } } } return false; } bool Input_ComboStartsWithImmediate( const INPUT_LAYOUT layout, const INPUT_COMBO_BINDING bind, const INPUT_COMBO_GET_BINDING get_binding, const INPUT_COMBO_KEYS_EQUAL keys_equal) { if (bind.key_count < 2) { return false; } return Input_ComboIsKeyImmediate( layout, Input_ComboKeyAt(&bind, 0), get_binding, keys_equal); } bool Input_ComboSustainedHasImmediate( const INPUT_LAYOUT layout, const INPUT_COMBO_BINDING bind, const INPUT_COMBO_GET_BINDING get_binding, const INPUT_COMBO_KEYS_EQUAL keys_equal) { if (bind.key_count < 2) { return false; } if (!M_IsKeyNonCapturingSustained( layout, Input_ComboKeyAt(&bind, 0), get_binding, keys_equal)) { return false; } for (int32_t k = 1; k < bind.key_count; k++) { if (Input_ComboIsKeyImmediate( layout, Input_ComboKeyAt(&bind, k), get_binding, keys_equal)) { return true; } } return false; } void Input_ComboCheckConflicts( const INPUT_LAYOUT layout, const INPUT_COMBO_GET_BINDING get_binding, const INPUT_COMBO_KEYS_EQUAL keys_equal, bool conflicts[INPUT_ROLE_NUMBER_OF]) { for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) { if (conflicts[role]) { continue; } for (int32_t s = 0; s < INPUT_BINDING_SLOTS; s++) { const INPUT_COMBO_BINDING b = get_binding(layout, role, s); if (Input_ComboStartsWithImmediate( layout, b, get_binding, keys_equal) || Input_ComboSustainedHasImmediate( layout, b, get_binding, keys_equal)) { conflicts[role] = true; break; } } } } ================================================ FILE: src/trx/game/input/combo.h ================================================ #pragma once #include #include #include // Generic view of a backend-specific binding for combo operations. // Backends create these from their concrete binding types. typedef struct { int32_t key_count; const void *keys; int32_t key_stride; } INPUT_COMBO_BINDING; typedef bool (*INPUT_COMBO_KEYS_EQUAL)(const void *a, const void *b); typedef INPUT_COMBO_BINDING (*INPUT_COMBO_GET_BINDING)( INPUT_LAYOUT layout, INPUT_ROLE role, int32_t slot); static inline const void *Input_ComboKeyAt( const INPUT_COMBO_BINDING *b, int32_t idx) { return (const char *)b->keys + idx * b->key_stride; } // Check if sub's keys are a proper subset of super's keys. bool Input_ComboIsProperSubset( INPUT_COMBO_BINDING sub, INPUT_COMBO_BINDING super, INPUT_COMBO_KEYS_EQUAL keys_equal); // Check if any binding in the layout is a longer combo that shares the // same starter key (keys[0]) as bind. bool Input_ComboHasLonger( INPUT_LAYOUT layout, INPUT_ROLE skip_role, INPUT_COMBO_BINDING bind, INPUT_COMBO_GET_BINDING get_binding, INPUT_COMBO_KEYS_EQUAL keys_equal); // Check if a key is the first key of any multi-key combo. bool Input_ComboIsStarter( INPUT_LAYOUT layout, const void *key, INPUT_COMBO_GET_BINDING get_binding, INPUT_COMBO_KEYS_EQUAL keys_equal); // Find the role with a single-key binding matching the given key, // excluding immediate, sustained, and non-rebindable roles (which are // never deferred). Returns (INPUT_ROLE)-1 if none found. INPUT_ROLE Input_ComboFindDeferrableRole( INPUT_LAYOUT layout, const void *key, INPUT_COMBO_GET_BINDING get_binding, INPUT_COMBO_KEYS_EQUAL keys_equal); // Check if a key (as a single-key binding) is bound to a rebindable // immediate role. bool Input_ComboIsKeyImmediate( INPUT_LAYOUT layout, const void *key, INPUT_COMBO_GET_BINDING get_binding, INPUT_COMBO_KEYS_EQUAL keys_equal); // Check if a combo starts with an immediate role's key. bool Input_ComboStartsWithImmediate( INPUT_LAYOUT layout, INPUT_COMBO_BINDING bind, INPUT_COMBO_GET_BINDING get_binding, INPUT_COMBO_KEYS_EQUAL keys_equal); // Check if a combo starts with a non-capturing sustained role's key and // contains an immediate role's key. Such combos are invalid because the // immediate action fires before the combo can form. bool Input_ComboSustainedHasImmediate( INPUT_LAYOUT layout, INPUT_COMBO_BINDING bind, INPUT_COMBO_GET_BINDING get_binding, INPUT_COMBO_KEYS_EQUAL keys_equal); // Run combo-specific conflict checks on all roles. Flags bindings that // start with an immediate key or are invalid sustained+immediate combos. // Must be called after the normal conflict helper has run. void Input_ComboCheckConflicts( INPUT_LAYOUT layout, INPUT_COMBO_GET_BINDING get_binding, INPUT_COMBO_KEYS_EQUAL keys_equal, bool conflicts[INPUT_ROLE_NUMBER_OF]); ================================================ FILE: src/trx/game/input/common.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include typedef enum { HOLD_INACTIVE, HOLD_DELAY, HOLD_REPEATING, } M_HOLD_STATE; typedef struct { CLOCK_TIMER delay_timer; CLOCK_TIMER repeat_timer; double delay_time; double hold_time; M_HOLD_STATE state; INPUT_ROLE role; } M_HOLD_CHECK; INPUT_STATE g_Input = {}; INPUT_STATE g_InputDB = {}; INPUT_STATE g_OldInputDB = {}; static bool m_ListenMode = false; static M_HOLD_CHECK m_HoldChecks[] = { { .role = INPUT_ROLE_MENU_UP, .delay_time = 0.4, .hold_time = 0.1 }, { .role = INPUT_ROLE_MENU_DOWN, .delay_time = 0.4, .hold_time = 0.1 }, { .role = INPUT_ROLE_MENU_LEFT, .delay_time = 0.4, .hold_time = 0.2 }, { .role = INPUT_ROLE_MENU_RIGHT, .delay_time = 0.4, .hold_time = 0.2 }, { .role = INPUT_ROLE_MENU_SKIP, .delay_time = 0.4, .hold_time = 0.1 }, { .role = (INPUT_ROLE)-1 }, // sentinel }; static bool m_IsRoleHardcoded[INPUT_ROLE_NUMBER_OF] = { // clang-format off [INPUT_ROLE_RESET_BINDINGS] = true, [INPUT_ROLE_UNBIND_KEY] = true, [INPUT_ROLE_MENU_CONFIRM] = true, [INPUT_ROLE_MENU_BACK] = true, [INPUT_ROLE_MENU_LEFT] = true, [INPUT_ROLE_MENU_RIGHT] = true, [INPUT_ROLE_MENU_UP] = true, [INPUT_ROLE_MENU_DOWN] = true, [INPUT_ROLE_MENU_TAB_LEFT] = true, [INPUT_ROLE_MENU_TAB_RIGHT] = true, // clang-format on }; static bool m_IsRoleImmediate[INPUT_ROLE_NUMBER_OF] = { // clang-format off [INPUT_ROLE_UP] = true, [INPUT_ROLE_DOWN] = true, [INPUT_ROLE_LEFT] = true, [INPUT_ROLE_RIGHT] = true, [INPUT_ROLE_JUMP] = true, [INPUT_ROLE_ROLL] = true, // clang-format on }; static bool m_IsRoleSustained[INPUT_ROLE_NUMBER_OF] = { // clang-format off [INPUT_ROLE_ACTION] = true, [INPUT_ROLE_STEP_LEFT] = true, [INPUT_ROLE_STEP_RIGHT] = true, [INPUT_ROLE_LOOK] = true, [INPUT_ROLE_SLOW] = true, [INPUT_ROLE_CROUCH] = true, [INPUT_ROLE_SPRINT] = true, // clang-format on }; static bool m_IsRoleCapturing[INPUT_ROLE_NUMBER_OF] = { // clang-format off [INPUT_ROLE_LOOK] = true, // clang-format on }; static bool m_IsRoleNonUnbindable[INPUT_ROLE_NUMBER_OF] = { // clang-format off [INPUT_ROLE_UP] = true, [INPUT_ROLE_DOWN] = true, [INPUT_ROLE_LEFT] = true, [INPUT_ROLE_RIGHT] = true, [INPUT_ROLE_DRAW_WEAPON] = true, [INPUT_ROLE_ACTION] = true, [INPUT_ROLE_JUMP] = true, [INPUT_ROLE_ROLL] = true, [INPUT_ROLE_LOOK] = true, [INPUT_ROLE_SLOW] = true, [INPUT_ROLE_INVENTORY] = true, // clang-format on }; static const GAME_STRING_ID m_LayoutMap[INPUT_LAYOUT_NUMBER_OF] = { [INPUT_LAYOUT_DEFAULT] = GS_ID("general/settings/controls/layout/default"), [INPUT_LAYOUT_CUSTOM_1] = GS_ID("general/settings/controls/layout/custom_1"), [INPUT_LAYOUT_CUSTOM_2] = GS_ID("general/settings/controls/layout/custom_2"), [INPUT_LAYOUT_CUSTOM_3] = GS_ID("general/settings/controls/layout/custom_3"), }; static INPUT_BACKEND_IMPL *M_GetBackend(const INPUT_BACKEND backend) { switch (backend) { case INPUT_BACKEND_KEYBOARD: return &g_Input_Keyboard; case INPUT_BACKEND_CONTROLLER: return &g_Input_Controller; default: return nullptr; } } static bool M_IsPressed(const INPUT_STATE input, const INPUT_ROLE role) { switch (role) { #define X_INPUT_ROLE(role_name, state_name) \ case role_name: \ return input.state_name; #include #undef X_INPUT_ROLE case INPUT_ROLE_NUMBER_OF: break; } return false; } static INPUT_STATE M_SetPressed( INPUT_STATE input, const INPUT_ROLE role, const bool is_pressed) { switch (role) { #define X_INPUT_ROLE(role_name, state_name) \ case role_name: \ input.state_name = is_pressed; \ break; #include #undef X_INPUT_ROLE case INPUT_ROLE_NUMBER_OF: break; } return input; } void Input_Reset(void) { InputState_Clear(&g_Input); InputState_Clear(&g_InputDB); InputState_Clear(&g_OldInputDB); for (int32_t i = 0; m_HoldChecks[i].role != (INPUT_ROLE)-1; i++) { M_HOLD_CHECK *const hold_check = &m_HoldChecks[i]; hold_check->state = HOLD_INACTIVE; ClockTimer_Sync(&hold_check->delay_timer); ClockTimer_Sync(&hold_check->repeat_timer); } } void Input_Init(void) { for (int32_t i = 0; m_HoldChecks[i].role != (INPUT_ROLE)-1; i++) { m_HoldChecks[i].delay_timer.type = CLOCK_TIMER_REAL; m_HoldChecks[i].repeat_timer.type = CLOCK_TIMER_REAL; } Input_Reset(); if (g_Input_Keyboard.init != nullptr) { g_Input_Keyboard.init(); } if (g_Input_Controller.init != nullptr) { g_Input_Controller.init(); } } void Input_Shutdown(void) { Input_Reset(); if (g_Input_Keyboard.shutdown != nullptr) { g_Input_Keyboard.shutdown(); } if (g_Input_Controller.shutdown != nullptr) { g_Input_Controller.shutdown(); } } void Input_Discover(void) { if (g_Input_Keyboard.discover != nullptr) { g_Input_Keyboard.discover(); } if (g_Input_Controller.discover != nullptr) { g_Input_Controller.discover(); } } bool Input_IsRoleRebindable(const INPUT_ROLE role) { return !m_IsRoleHardcoded[role]; } bool Input_IsRoleUnbindable(const INPUT_ROLE role) { return !m_IsRoleNonUnbindable[role]; } bool Input_IsRoleImmediate(const INPUT_ROLE role) { return m_IsRoleImmediate[role]; } bool Input_IsRoleSustained(const INPUT_ROLE role) { return m_IsRoleSustained[role]; } bool Input_IsRoleCapturing(const INPUT_ROLE role) { return m_IsRoleCapturing[role]; } bool Input_IsPressed(const INPUT_ROLE role) { return M_IsPressed(g_Input, role); } bool Input_IsPressedDB(const INPUT_ROLE role) { return M_IsPressed(g_InputDB, role); } bool Input_IsPressedEx( const INPUT_BACKEND backend, const INPUT_LAYOUT layout, const INPUT_ROLE role) { return M_GetBackend(backend)->is_pressed(layout, role); } bool Input_IsKeyConflicted( const INPUT_BACKEND backend, const INPUT_LAYOUT layout, const INPUT_ROLE role) { return M_GetBackend(backend)->is_role_conflicted(layout, role); } bool Input_ReadAndAssignRole( const INPUT_BACKEND backend, const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot) { // Check for canceling from other devices for (INPUT_BACKEND other_backend = 0; other_backend < INPUT_BACKEND_NUMBER_OF; other_backend++) { if (other_backend == backend) { continue; } if (Input_IsPressedEx(other_backend, layout, INPUT_ROLE_MENU_BACK) || Input_IsPressedEx(other_backend, layout, INPUT_ROLE_INVENTORY)) { return true; } } return M_GetBackend(backend)->read_and_assign(layout, role, slot); } void Input_UnassignRole( const INPUT_BACKEND backend, const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot) { M_GetBackend(backend)->unassign_role(layout, role, slot); } const char *Input_GetKeyName( const INPUT_BACKEND backend, const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot) { return M_GetBackend(backend)->get_name(layout, role, slot); } void Input_ResetLayout(const INPUT_BACKEND backend, const INPUT_LAYOUT layout) { M_GetBackend(backend)->reset_layout(layout); } void Input_EnterListenMode(void) { m_ListenMode = true; } void Input_ExitListenMode(void) { m_ListenMode = false; Input_Update(); InputState_Copy(&g_OldInputDB, g_Input); InputState_Copy(&g_InputDB, g_Input); } bool Input_IsInListenMode(void) { return m_ListenMode; } void Input_ProcessEvent(const SDL_Event *event) { if (g_Input_Keyboard.process_event != nullptr) { g_Input_Keyboard.process_event(event); } if (g_Input_Controller.process_event != nullptr) { g_Input_Controller.process_event(event); } } bool Input_AssignFromJSONObject( const INPUT_BACKEND backend, const INPUT_LAYOUT layout, JSON_OBJECT *const bind_obj) { INPUT_ROLE role = (INPUT_ROLE)-1; // TR1X <=4.5, TR2X <=0.5 const int32_t role_idx = JSON_ObjectGetInt(bind_obj, "role", -1); // clang-format off switch (role_idx) { case 0: role = INPUT_ROLE_UP; break; case 1: role = INPUT_ROLE_DOWN; break; case 2: role = INPUT_ROLE_LEFT; break; case 3: role = INPUT_ROLE_RIGHT; break; case 4: role = INPUT_ROLE_STEP_LEFT; break; case 5: role = INPUT_ROLE_STEP_RIGHT; break; case 6: role = INPUT_ROLE_SLOW; break; case 7: role = INPUT_ROLE_JUMP; break; case 8: role = INPUT_ROLE_ACTION; break; case 9: role = INPUT_ROLE_DRAW_WEAPON; break; case 10: role = INPUT_ROLE_LOOK; break; case 11: role = INPUT_ROLE_ROLL; break; case 12: role = INPUT_ROLE_INVENTORY; break; case 13: role = INPUT_ROLE_FLY_CHEAT; break; case 14: role = INPUT_ROLE_ITEM_CHEAT; break; case 15: role = INPUT_ROLE_LEVEL_SKIP_CHEAT; break; case 16: role = INPUT_ROLE_TURBO_CHEAT; break; case 17: role = INPUT_ROLE_PAUSE; break; case 18: role = INPUT_ROLE_CAMERA_FORWARD; break; case 19: role = INPUT_ROLE_CAMERA_BACK; break; case 20: role = INPUT_ROLE_CAMERA_LEFT; break; case 21: role = INPUT_ROLE_CAMERA_RIGHT; break; case 22: role = INPUT_ROLE_CAMERA_RESET; break; case 23: role = INPUT_ROLE_EQUIP_PISTOLS; break; case 24: role = INPUT_ROLE_EQUIP_SHOTGUN; break; case 25: role = INPUT_ROLE_EQUIP_MAGNUMS; break; case 26: role = INPUT_ROLE_EQUIP_UZIS; break; case 27: role = INPUT_ROLE_USE_SMALL_MEDI; break; case 28: role = INPUT_ROLE_USE_BIG_MEDI; break; case 29: role = INPUT_ROLE_SAVE; break; case 30: role = INPUT_ROLE_LOAD; break; case 31: role = INPUT_ROLE_FPS; break; case 32: role = INPUT_ROLE_TOGGLE_BILINEAR_FILTER; break; case 33: role = INPUT_ROLE_ENTER_CONSOLE; break; case 34: role = INPUT_ROLE_CHANGE_TARGET; break; case 35: role = INPUT_ROLE_TOGGLE_UI; break; case 36: role = INPUT_ROLE_CAMERA_UP; break; case 37: role = INPUT_ROLE_CAMERA_DOWN; break; case 38: role = INPUT_ROLE_TOGGLE_PHOTO_MODE; break; case 39: role = INPUT_ROLE_UNBIND_KEY; break; case 40: role = INPUT_ROLE_RESET_BINDINGS; break; case 42: role = INPUT_ROLE_TOGGLE_TRAPEZOID_FILTER; break; case 43: role = INPUT_ROLE_MENU_CONFIRM; break; case 44: role = INPUT_ROLE_MENU_BACK; break; case 45: role = INPUT_ROLE_MENU_LEFT; break; case 46: role = INPUT_ROLE_MENU_UP; break; case 47: role = INPUT_ROLE_MENU_DOWN; break; case 48: role = INPUT_ROLE_MENU_RIGHT; break; case 49: role = INPUT_ROLE_SCREENSHOT; break; case 50: role = INPUT_ROLE_TOGGLE_FULLSCREEN; break; } // clang-format on // TR1X >= 4.6, TR2X >= 0.6 if (role == (INPUT_ROLE)-1) { role = ENUM_MAP_GET( INPUT_ROLE, JSON_ObjectGetString(bind_obj, "role", ""), (int32_t)(INPUT_ROLE)-1); } if (role == (INPUT_ROLE)-1) { return false; } const int32_t slot = JSON_ObjectGetInt(bind_obj, "slot", 0); return M_GetBackend(backend)->assign_from_json_object( layout, role, slot, bind_obj); } bool Input_AssignToJSONObject( const INPUT_BACKEND backend, const INPUT_LAYOUT layout, JSON_OBJECT *const bind_obj, const INPUT_ROLE role, const int32_t slot) { JSON_ObjectAppendString( bind_obj, "role", ENUM_MAP_TO_STRING(INPUT_ROLE, role)); if (slot != 0) { JSON_ObjectAppendInt(bind_obj, "slot", slot); } return M_GetBackend(backend)->assign_to_json_object( layout, role, slot, bind_obj); } const char *const *Input_GetLayoutNamePtr(const INPUT_LAYOUT layout) { return GameString_GetPtr(m_LayoutMap[layout]); } INPUT_STATE Input_GetDebounced(const INPUT_STATE input) { INPUT_STATE result; for (int32_t i = 0; i < INPUT_STATE_ANY_WORDS; i++) { result.any[i] = input.any[i] & ~g_OldInputDB.any[i]; } // Allow holding certain keys for (int32_t i = 0; m_HoldChecks[i].role != (INPUT_ROLE)-1; i++) { M_HOLD_CHECK *const hold_check = &m_HoldChecks[i]; if (!M_IsPressed(input, hold_check->role)) { hold_check->state = HOLD_INACTIVE; } else if (hold_check->state == HOLD_INACTIVE) { hold_check->state = HOLD_DELAY; ClockTimer_Sync(&hold_check->delay_timer); } else if ( hold_check->state == HOLD_DELAY && ClockTimer_CheckElapsedAndTake( &hold_check->delay_timer, hold_check->delay_time)) { hold_check->state = HOLD_REPEATING; } else if ( hold_check->state == HOLD_REPEATING && ClockTimer_CheckElapsedAndTake( &hold_check->repeat_timer, hold_check->hold_time)) { result = M_SetPressed(result, hold_check->role, true); } } g_OldInputDB = input; return result; } const char *Input_GetRoleName(const INPUT_ROLE role) { return EnumMap_GetLabel(ENUM_MAP_NAME(INPUT_ROLE), role); } const char *Input_KeyDescFromSDL(SDL_Scancode scancode, SDL_Keymod mod) { // clang-format off const char *mods = ""; if (mod & KMOD_CTRL) { mods = String_FormatStatic("%sctrl+", mods); } if (mod & KMOD_SHIFT) { mods = String_FormatStatic("%sshift+", mods); } if (mod & KMOD_ALT) { mods = String_FormatStatic("%salt+", mods); } if (mod & KMOD_GUI) { mods = String_FormatStatic("%sgui+", mods); } // clang-format on const char *const name = SDL_GetScancodeName(scancode); if (name == nullptr || name[0] == '\0') { return nullptr; } char *const full = (char *)String_FormatStatic("%s%s", mods, name); for (size_t i = 0; i < strlen(full); i++) { full[i] = (char)tolower((unsigned char)full[i]); } return full; } bool Input_ParseKeyDesc( const char *const desc, SDL_Scancode *const scancode, SDL_Keymod *const mod) { if (desc == nullptr || scancode == nullptr || mod == nullptr) { return false; } SDL_Keymod m = KMOD_NONE; const char *keystr = desc; const char *last = strrchr(desc, '+'); if (last != nullptr) { for (const char *tok = desc; tok < last; tok = strchr(tok, '+') + 1) { const size_t len = strchr(tok, '+') ? strchr(tok, '+') - tok : last - tok; if (strncmp(tok, "ctrl", len) == 0) { m |= KMOD_CTRL; } else if (strncmp(tok, "shift", len) == 0) { m |= KMOD_SHIFT; } else if (strncmp(tok, "alt", len) == 0) { m |= KMOD_ALT; } else if (strncmp(tok, "gui", len) == 0) { m |= KMOD_GUI; } } keystr = last + 1; } *scancode = SDL_GetScancodeFromName(keystr); *mod = m; return *scancode != SDL_SCANCODE_UNKNOWN; } void InputState_Clear(INPUT_STATE *const state) { for (int32_t i = 0; i < INPUT_STATE_ANY_WORDS; i++) { state->any[i] = 0; } } void InputState_Copy(INPUT_STATE *const dst, const INPUT_STATE src) { for (int32_t i = 0; i < INPUT_STATE_ANY_WORDS; i++) { dst->any[i] = src.any[i]; } } bool InputState_IsAnyPressed(const INPUT_STATE state) { for (int32_t i = 0; i < INPUT_STATE_ANY_WORDS; i++) { if (state.any[i] != 0) { return true; } } return false; } bool InputState_GetRole(const INPUT_STATE state, const INPUT_ROLE role) { return M_IsPressed(state, role); } void InputState_SetRole( INPUT_STATE *const state, const INPUT_ROLE role, const bool value) { *state = M_SetPressed(*state, role, value); } void InputState_ClearRole(INPUT_STATE *const state, const INPUT_ROLE role) { *state = M_SetPressed(*state, role, false); } ================================================ FILE: src/trx/game/input/common.h ================================================ #pragma once #include #include #include #include #define INPUT_COMBO_MAX_KEYS 3 #define INPUT_BINDING_SLOTS 2 typedef enum { #define X_INPUT_ROLE(role_name, state_name) role_name, #include INPUT_ROLE_NUMBER_OF, #undef X_INPUT_ROLE } INPUT_ROLE; #define INPUT_STATE_ANY_WORDS ((INPUT_ROLE_NUMBER_OF + 63) / 64) typedef union { uint64_t any[INPUT_STATE_ANY_WORDS]; struct { #define X_INPUT_ROLE(role_name, state_name) uint64_t state_name : 1; #include #undef X_INPUT_ROLE }; } INPUT_STATE; extern INPUT_STATE g_Input; extern INPUT_STATE g_InputDB; extern INPUT_STATE g_OldInputDB; void Input_Init(void); void Input_Shutdown(void); void Input_Discover(void); void Input_Update(void); void Input_Reset(void); // Processes a SDL event to update global input state before polling. // @param event Event to process. void Input_ProcessEvent(const SDL_Event *event); // Checks whether the given role can be assigned to by the player. // Hard-coded roles are exempt from conflict checks (eg will never flash in the // controls dialog). bool Input_IsRoleRebindable(INPUT_ROLE role); // Checks whether the given role can be completely unbound by the player. bool Input_IsRoleUnbindable(INPUT_ROLE role); // Checks whether the given role uses keys that fire immediately and cannot // be the first key of a combo (movement, jump, action, roll). bool Input_IsRoleImmediate(INPUT_ROLE role); // Checks whether the given role is a held state that fires immediately but // can still be the first key of a combo (look, walk). Unlike immediate // roles, these don't block combo formation because the held state is not // disrupted when a longer combo completes. bool Input_IsRoleSustained(INPUT_ROLE role); // Checks whether the given role captures other inputs when held, redirecting // them to different actions (e.g. look mode repurposes movement keys for // camera control). Capturing roles can start combos with immediate keys // because the immediate keys lose their normal meaning while captured. bool Input_IsRoleCapturing(INPUT_ROLE role); // Returns whether the key assigned to the given role is also used elsewhere // within the custom layout. bool Input_IsKeyConflicted( INPUT_BACKEND backend, INPUT_LAYOUT layout, INPUT_ROLE role); // Checks if the key is currently pressed. Tied to Input_Update(), so updates // at most at the game running FPS. bool Input_IsPressed(INPUT_ROLE role); // Checks if the key is currently pressed with a debounce, e.g. only true // for the game frame the player starts to hold the key at. bool Input_IsPressedDB(INPUT_ROLE role); // Given the input layout and input key role, check if the assorted key is // pressed, bypassing Input_Update. bool Input_IsPressedEx( INPUT_BACKEND backend, INPUT_LAYOUT layout, INPUT_ROLE role); // If there is anything pressed, assigns the pressed key to the given key role // and returns true. If nothing is pressed, immediately returns false. bool Input_ReadAndAssignRole( INPUT_BACKEND backend, INPUT_LAYOUT layout, INPUT_ROLE role, int32_t slot); // Remove assigned key from a given key role. void Input_UnassignRole( INPUT_BACKEND backend, INPUT_LAYOUT layout, INPUT_ROLE role, int32_t slot); // Get a stable pointer to the layout human-readable name. const char *const *Input_GetLayoutNamePtr(const INPUT_LAYOUT layout); // Given the input layout and input key role, get the assigned key name. const char *Input_GetKeyName( INPUT_BACKEND backend, INPUT_LAYOUT layout, INPUT_ROLE role, int32_t slot); // Reset a given layout to the default. void Input_ResetLayout(INPUT_BACKEND backend, INPUT_LAYOUT layout); // Disables updating g_Input. void Input_EnterListenMode(void); // Enables updating g_Input. void Input_ExitListenMode(void); // Checks whether updates are disabled. bool Input_IsInListenMode(void); // Restores the user configuration by converting the JSON object back into the // original input layout. bool Input_AssignFromJSONObject( INPUT_BACKEND backend, INPUT_LAYOUT layout, JSON_OBJECT *bind_obj); // Converts the original input layout into a JSON object for storing the user // configuration. bool Input_AssignToJSONObject( INPUT_BACKEND backend, INPUT_LAYOUT layout, JSON_OBJECT *bind_obj, INPUT_ROLE role, int32_t slot); // Return a copy of the input state with only newly pressed roles set. INPUT_STATE Input_GetDebounced(const INPUT_STATE input); // Get the human-readable name of the given role. const char *Input_GetRoleName(INPUT_ROLE role); // Serialize a scancode and modifier mask into a human-readable key // description, e.g. "ctrl+shift+up". The returned string must not be held onto. const char *Input_KeyDescFromSDL(SDL_Scancode scancode, SDL_Keymod mod); // Parse a human-readable key description into scancode and modifier mask. // e.g. "ctrl+shift+up" → scancode SDL_SCANCODE_UP, mod KMOD_CTRL|KMOD_SHIFT. // Returns true if parsing succeeded, false otherwise. bool Input_ParseKeyDesc( const char *desc, SDL_Scancode *scancode, SDL_Keymod *mod); // Reset all roles in the input state to inactive. void InputState_Clear(INPUT_STATE *state); // Copy the source input state into the destination. void InputState_Copy(INPUT_STATE *dst, INPUT_STATE src); // Check whether any role is active in the input state. bool InputState_IsAnyPressed(INPUT_STATE state); // Check whether the given role is active in the input state. bool InputState_GetRole(INPUT_STATE state, INPUT_ROLE role); // Set or clear the given role in the input state. void InputState_SetRole(INPUT_STATE *state, INPUT_ROLE role, bool value); // Clear the given role in the input state. void InputState_ClearRole(INPUT_STATE *state, INPUT_ROLE role); ================================================ FILE: src/trx/game/input/roles.def ================================================ X_INPUT_ROLE(INPUT_ROLE_UP, forward) X_INPUT_ROLE(INPUT_ROLE_DOWN, back) X_INPUT_ROLE(INPUT_ROLE_LEFT, left) X_INPUT_ROLE(INPUT_ROLE_RIGHT, right) X_INPUT_ROLE(INPUT_ROLE_STEP_LEFT, step_left) X_INPUT_ROLE(INPUT_ROLE_STEP_RIGHT, step_right) X_INPUT_ROLE(INPUT_ROLE_SLOW, slow) X_INPUT_ROLE(INPUT_ROLE_CROUCH, crouch) X_INPUT_ROLE(INPUT_ROLE_JUMP, jump) X_INPUT_ROLE(INPUT_ROLE_ACTION, action) X_INPUT_ROLE(INPUT_ROLE_DRAW_WEAPON, draw) X_INPUT_ROLE(INPUT_ROLE_LOOK, look) X_INPUT_ROLE(INPUT_ROLE_ROLL, roll) X_INPUT_ROLE(INPUT_ROLE_SPRINT, sprint) X_INPUT_ROLE(INPUT_ROLE_INVENTORY, option) X_INPUT_ROLE(INPUT_ROLE_CHANGE_TARGET, change_target) X_INPUT_ROLE(INPUT_ROLE_ENTER_CONSOLE, enter_console) X_INPUT_ROLE(INPUT_ROLE_MENU_CONFIRM, menu_confirm) X_INPUT_ROLE(INPUT_ROLE_MENU_BACK, menu_back) X_INPUT_ROLE(INPUT_ROLE_MENU_LEFT, menu_left) X_INPUT_ROLE(INPUT_ROLE_MENU_UP, menu_up) X_INPUT_ROLE(INPUT_ROLE_MENU_DOWN, menu_down) X_INPUT_ROLE(INPUT_ROLE_MENU_RIGHT, menu_right) X_INPUT_ROLE(INPUT_ROLE_MENU_SKIP, menu_skip) X_INPUT_ROLE(INPUT_ROLE_MENU_TAB_LEFT, menu_tab_left) X_INPUT_ROLE(INPUT_ROLE_MENU_TAB_RIGHT, menu_tab_right) X_INPUT_ROLE(INPUT_ROLE_FLY_CHEAT, fly_cheat) X_INPUT_ROLE(INPUT_ROLE_ITEM_CHEAT, item_cheat) X_INPUT_ROLE(INPUT_ROLE_LEVEL_SKIP_CHEAT, level_skip_cheat) X_INPUT_ROLE(INPUT_ROLE_TURBO_CHEAT, turbo_cheat) X_INPUT_ROLE(INPUT_ROLE_SAVE, save) X_INPUT_ROLE(INPUT_ROLE_LOAD, load) X_INPUT_ROLE(INPUT_ROLE_QUICK_SAVE, quick_save) X_INPUT_ROLE(INPUT_ROLE_QUICK_LOAD, quick_load) X_INPUT_ROLE(INPUT_ROLE_SCREENSHOT, screenshot) X_INPUT_ROLE(INPUT_ROLE_FPS, toggle_fps_counter) X_INPUT_ROLE(INPUT_ROLE_TOGGLE_FULLSCREEN, toggle_fullscreen) X_INPUT_ROLE(INPUT_ROLE_EQUIP_PISTOLS, equip_pistols) X_INPUT_ROLE(INPUT_ROLE_EQUIP_SHOTGUN, equip_shotgun) X_INPUT_ROLE(INPUT_ROLE_EQUIP_MAGNUMS, equip_magnums) X_INPUT_ROLE(INPUT_ROLE_EQUIP_AUTOS, equip_autos) X_INPUT_ROLE(INPUT_ROLE_EQUIP_DESERT_EAGLE, equip_desert_eagle) X_INPUT_ROLE(INPUT_ROLE_EQUIP_UZIS, equip_uzis) X_INPUT_ROLE(INPUT_ROLE_EQUIP_HARPOON, equip_harpoon) X_INPUT_ROLE(INPUT_ROLE_EQUIP_M16, equip_m16) X_INPUT_ROLE(INPUT_ROLE_EQUIP_MP5, equip_mp5) X_INPUT_ROLE(INPUT_ROLE_EQUIP_GRENADE_LAUNCHER, equip_grenade_launcher) X_INPUT_ROLE(INPUT_ROLE_EQUIP_ROCKET_LAUNCHER, equip_rocket_launcher) X_INPUT_ROLE(INPUT_ROLE_USE_SMALL_MEDI, use_small_medi) X_INPUT_ROLE(INPUT_ROLE_USE_BIG_MEDI, use_big_medi) X_INPUT_ROLE(INPUT_ROLE_USE_FLARE, use_flare) X_INPUT_ROLE(INPUT_ROLE_PAUSE, pause) X_INPUT_ROLE(INPUT_ROLE_TOGGLE_PHOTO_MODE, toggle_photo_mode) X_INPUT_ROLE(INPUT_ROLE_TOGGLE_UI, toggle_ui) X_INPUT_ROLE(INPUT_ROLE_TOGGLE_BILINEAR_FILTER, toggle_bilinear_filter) X_INPUT_ROLE(INPUT_ROLE_CYCLE_LIGHTING_CONTRAST, cycle_lighting_contrast) X_INPUT_ROLE(INPUT_ROLE_CHANGE_OUTFIT, change_outfit) X_INPUT_ROLE(INPUT_ROLE_TOGGLE_TRAPEZOID_FILTER, toggle_trapezoid_filter) X_INPUT_ROLE(INPUT_ROLE_TOGGLE_WIREFRAME, toggle_wireframe) X_INPUT_ROLE(INPUT_ROLE_TOGGLE_TEXTURES, toggle_textures) X_INPUT_ROLE(INPUT_ROLE_SWITCH_UPSCALING, switch_upscaling) X_INPUT_ROLE(INPUT_ROLE_SWITCH_BORDERS, switch_borders) X_INPUT_ROLE(INPUT_ROLE_RESET_BINDINGS, reset_bindings) X_INPUT_ROLE(INPUT_ROLE_UNBIND_KEY, unbind_key) X_INPUT_ROLE(INPUT_ROLE_CAMERA_FORWARD, camera_forward) X_INPUT_ROLE(INPUT_ROLE_CAMERA_BACK, camera_back) X_INPUT_ROLE(INPUT_ROLE_CAMERA_LEFT, camera_left) X_INPUT_ROLE(INPUT_ROLE_CAMERA_RIGHT, camera_right) X_INPUT_ROLE(INPUT_ROLE_CAMERA_UP, camera_up) X_INPUT_ROLE(INPUT_ROLE_CAMERA_DOWN, camera_down) X_INPUT_ROLE(INPUT_ROLE_CAMERA_RESET, camera_reset) ================================================ FILE: src/trx/game/input/update.c ================================================ #include #include #include #include #include #include #include #include #include static void M_UpdateFromBackend( INPUT_STATE *const s, const INPUT_BACKEND_IMPL *const backend, const int32_t layout) { #define X_INPUT_ROLE(role, state) s->state |= backend->is_pressed(layout, role); #include #undef X_INPUT_ROLE backend->custom_update(s, layout); } void Input_Update(void) { InputState_Clear(&g_Input); M_UpdateFromBackend( &g_Input, &g_Input_Keyboard, g_Config.input.keyboard_layout); M_UpdateFromBackend( &g_Input, &g_Input_Controller, g_Config.input.controller_layout); // Suppress roles whose bindings are subsets of longer active combos. g_Input_Keyboard.resolve_combos(g_Config.input.keyboard_layout, &g_Input); g_Input_Controller.resolve_combos( g_Config.input.controller_layout, &g_Input); g_Input.camera_reset |= g_Input.look; g_Input.menu_up |= g_Input.forward; g_Input.menu_down |= g_Input.back; g_Input.menu_left |= g_Input.left; g_Input.menu_right |= g_Input.right; g_Input.menu_back |= g_Input.option; g_Input.option &= g_Camera.type != CAM_CINEMATIC; g_Input.roll |= g_Input.forward && g_Input.back; if (g_Input.left && g_Input.right) { g_Input.left = 0; g_Input.right = 0; } if (!g_Config.gameplay.enable_crawling) { g_Input.crouch = 0; } if (g_Config.input.enable_tr3_sidesteps) { if (g_Input.slow && !g_Input.forward && !g_Input.back && !g_Input.step_left && !g_Input.step_right) { if (g_Input.left) { g_Input.left = 0; g_Input.step_left = 1; } else if (g_Input.right) { g_Input.right = 0; g_Input.step_right = 1; } } } if (!g_Config.gameplay.enable_target_change || Lara_GetLaraInfo()->gun_status != LGS_READY) { g_Input.change_target = 0; } g_InputDB = Input_GetDebounced(g_Input); if (Input_IsInListenMode()) { InputState_Clear(&g_Input); InputState_Clear(&g_InputDB); } } ================================================ FILE: src/trx/game/input.h ================================================ #pragma once #include ================================================ FILE: src/trx/game/interpolation.c ================================================ #include #include #include #include #include #include #include #include #include #include #define CAM_MAX_DELTA (STEP_L * 3 / 2) #define REMEMBER(target, member) (target)->interp.prev.member = (target)->member #define COMMIT(target, member) (target)->interp.result.member = (target)->member #define DIFF(target, member) \ (ABS(((target)->member) - ((target)->interp.prev.member))) #define INTERPOLATE_F(target, member, ratio) \ (target)->interp.result.member = ((target)->interp.prev.member) \ + (((target)->member - ((target)->interp.prev.member)) * (ratio)); #define INTERPOLATE(target, member, ratio, max_diff) \ if (DIFF((target), member) >= (max_diff)) { \ COMMIT((target), member); \ } else { \ INTERPOLATE_F(target, member, ratio); \ } #define INTERPOLATE_ROT_F(target, member, ratio) \ (target)->interp.result.member = Math_AngleMean( \ (target)->interp.prev.member, (target)->member, (ratio)) #define INTERPOLATE_ROT(target, member, ratio, max_diff) \ if (!Math_AngleInCone( \ (target)->member, (target)->interp.prev.member, (max_diff))) { \ COMMIT((target), member); \ } else { \ INTERPOLATE_ROT_F(target, member, ratio); \ } static bool m_IsEnabled = true; static double m_Rate = 0.0; static double m_WorldRate = 0.0; static double m_CameraRate = 0.0; static int32_t M_GetFPS(void) { return g_Config.rendering.fps; } static XYZ_32 M_GetItemMaxDelta(const ITEM *const item) { int32_t max_xz = 128; int32_t max_y = MAX(128, ABS(item->fall_speed) * 2); switch (item->object_id) { case O_BAT: max_xz = 0; max_y = 0; break; case O_DART: case O_DISC: case O_BOAT: case O_RIB: case O_SKIDOO_ARMED: case O_SKIDOO_TRACK: case O_SKIDOO_FAST: case O_SKIDOO_DRIVER: case O_GRENADE: max_xz = 200; break; case O_QUAD_BIKE: case O_KAYAK: case O_UPV: max_xz = 300; break; case O_MINE_CART: max_xz = 512; max_y = 200; break; case O_POISON_DART: case O_ROCKET: case O_HEAVY_ROCKET: max_xz = 1000; max_y = 200; break; case O_SOPHIA_LASER_BOLT: max_xz = 1000; max_y = 1000; break; case O_HARPOON_BOLT: max_xz = 300; max_y = 200; break; case O_LARA: case O_LARA_EXTRA: { const ITEM *const vehicle = Lara_Vehicle_GetItem(); if (vehicle == nullptr) { break; } return M_GetItemMaxDelta(vehicle); } default: break; } return (XYZ_32) { .x = max_xz, .y = max_y, .z = max_xz }; } static XYZ_32 M_GetEffectMaxDelta(const EFFECT *const effect) { int32_t max_xz = 128; int32_t max_y = MAX(128, effect->fall_speed * 2); switch (effect->object_id) { case O_NATLA_GUN: case O_MISSILE_ATLANTEAN_BOMB: max_xz = 220; break; case O_MISSILE_ATLANTEAN_SHARD: max_xz = 250; break; case O_MISSILE_FLAME: max_xz = 200; break; case O_MISSILE_KNIFE: case O_MISSILE_HARPOON: max_xz = 150; break; default: break; } return (XYZ_32) { .x = max_xz, .y = max_y, .z = max_xz }; } static void M_RememberCamera(void) { REMEMBER(&g_Camera, shift); REMEMBER(&g_Camera, pos.x); REMEMBER(&g_Camera, pos.y); REMEMBER(&g_Camera, pos.z); REMEMBER(&g_Camera, target.x); REMEMBER(&g_Camera, target.y); REMEMBER(&g_Camera, target.z); } static void M_CommitCamera(void) { COMMIT(&g_Camera, shift); COMMIT(&g_Camera, pos.x); COMMIT(&g_Camera, pos.y); COMMIT(&g_Camera, pos.z); COMMIT(&g_Camera, target.x); COMMIT(&g_Camera, target.y); COMMIT(&g_Camera, target.z); } static void M_InterpolateCamera(const double ratio) { INTERPOLATE_F(&g_Camera, shift, ratio); INTERPOLATE_F(&g_Camera, pos.x, ratio); INTERPOLATE_F(&g_Camera, pos.y, ratio); INTERPOLATE_F(&g_Camera, pos.z, ratio); INTERPOLATE_F(&g_Camera, target.x, ratio); INTERPOLATE_F(&g_Camera, target.y, ratio); INTERPOLATE_F(&g_Camera, target.z, ratio); } static void M_RememberLara(LARA_INFO *const lara) { ASSERT(lara != nullptr); REMEMBER(&lara->left_arm, rot.x); REMEMBER(&lara->left_arm, rot.y); REMEMBER(&lara->left_arm, rot.z); REMEMBER(&lara->right_arm, rot.x); REMEMBER(&lara->right_arm, rot.y); REMEMBER(&lara->right_arm, rot.z); REMEMBER(lara, torso_rot.x); REMEMBER(lara, torso_rot.y); REMEMBER(lara, torso_rot.z); REMEMBER(lara, head_rot.x); REMEMBER(lara, head_rot.y); REMEMBER(lara, head_rot.z); } static void M_InterpolateLara(const double ratio, LARA_INFO *const lara) { ASSERT(lara != nullptr); INTERPOLATE_ROT(&lara->left_arm, rot.x, ratio, DEG_45); INTERPOLATE_ROT(&lara->left_arm, rot.y, ratio, DEG_45); INTERPOLATE_ROT(&lara->left_arm, rot.z, ratio, DEG_45); INTERPOLATE_ROT(&lara->right_arm, rot.x, ratio, DEG_45); INTERPOLATE_ROT(&lara->right_arm, rot.y, ratio, DEG_45); INTERPOLATE_ROT(&lara->right_arm, rot.z, ratio, DEG_45); INTERPOLATE_ROT(lara, torso_rot.x, ratio, DEG_45); INTERPOLATE_ROT(lara, torso_rot.y, ratio, DEG_45); INTERPOLATE_ROT(lara, torso_rot.z, ratio, DEG_45); INTERPOLATE_ROT(lara, head_rot.x, ratio, DEG_45); INTERPOLATE_ROT(lara, head_rot.y, ratio, DEG_45); INTERPOLATE_ROT(lara, head_rot.z, ratio, DEG_45); } static void M_CommitLara(LARA_INFO *const lara) { ASSERT(lara != nullptr); COMMIT(&lara->left_arm, rot.x); COMMIT(&lara->left_arm, rot.y); COMMIT(&lara->left_arm, rot.z); COMMIT(&lara->right_arm, rot.x); COMMIT(&lara->right_arm, rot.y); COMMIT(&lara->right_arm, rot.z); COMMIT(lara, torso_rot.x); COMMIT(lara, torso_rot.y); COMMIT(lara, torso_rot.z); COMMIT(lara, head_rot.x); COMMIT(lara, head_rot.y); COMMIT(lara, head_rot.z); } static void M_RememberBraidSegment(HAIR_SEGMENT *const segment) { ASSERT(segment != nullptr); REMEMBER(segment, pos.x); REMEMBER(segment, pos.y); REMEMBER(segment, pos.z); REMEMBER(segment, rot.x); REMEMBER(segment, rot.y); REMEMBER(segment, rot.z); } static void M_InterpolateBraidSegment( HAIR_SEGMENT *const segment, const double ratio, const XYZ_32 max_delta) { ASSERT(segment != nullptr); INTERPOLATE(segment, pos.x, ratio, max_delta.x); INTERPOLATE(segment, pos.y, ratio, max_delta.y); INTERPOLATE(segment, pos.z, ratio, max_delta.z); INTERPOLATE_ROT(segment, rot.x, ratio, DEG_45); INTERPOLATE_ROT(segment, rot.y, ratio, DEG_45); INTERPOLATE_ROT(segment, rot.z, ratio, DEG_45); } static void M_CommitBraidSegment(HAIR_SEGMENT *const segment) { ASSERT(segment != nullptr); COMMIT(segment, pos.x); COMMIT(segment, pos.y); COMMIT(segment, pos.z); COMMIT(segment, rot.x); COMMIT(segment, rot.y); COMMIT(segment, rot.z); } static void M_RememberBraid(void) { for (int32_t i = 0; i < Lara_Hair_GetSegmentCount(); i++) { M_RememberBraidSegment(Lara_Hair_GetSegment(i)); } } static void M_CommitBraid(void) { for (int32_t i = 0; i < Lara_Hair_GetSegmentCount(); i++) { M_CommitBraidSegment(Lara_Hair_GetSegment(i)); } } static void M_InterpolateBraid(const double ratio, ITEM *const lara_item) { ASSERT(lara_item != nullptr); const XYZ_32 max_delta = M_GetItemMaxDelta(lara_item); for (int32_t i = 0; i < Lara_Hair_GetSegmentCount(); i++) { M_InterpolateBraidSegment(Lara_Hair_GetSegment(i), ratio, max_delta); } } static void M_RememberItem(ITEM *const item) { REMEMBER(item, floor); REMEMBER(item, pos.x); REMEMBER(item, pos.y); REMEMBER(item, pos.z); REMEMBER(item, rot.x); REMEMBER(item, rot.y); REMEMBER(item, rot.z); item->prev_frame_num = item->frame_num; } static void M_CommitItem(ITEM *const item) { COMMIT(item, floor); COMMIT(item, pos.x); COMMIT(item, pos.y); COMMIT(item, pos.z); COMMIT(item, rot.x); COMMIT(item, rot.y); COMMIT(item, rot.z); } static void M_InterpolateItem(ITEM *const item, const double ratio) { const XYZ_32 max_delta = M_GetItemMaxDelta(item); INTERPOLATE(item, floor, ratio, max_delta.y); INTERPOLATE(item, pos.x, ratio, max_delta.x); INTERPOLATE(item, pos.y, ratio, max_delta.y); INTERPOLATE(item, pos.z, ratio, max_delta.z); INTERPOLATE_ROT(item, rot.x, ratio, DEG_45); INTERPOLATE_ROT(item, rot.y, ratio, DEG_45); INTERPOLATE_ROT(item, rot.z, ratio, DEG_45); } static void M_RememberItems(void) { for (int32_t i = 0; i < Item_GetTotalCount(); i++) { M_RememberItem(Item_Get(i)); } ITEM *const lara_item = Lara_GetItem(); if (lara_item != nullptr) { M_RememberItem(lara_item); } } static void M_InterpolateItems(const double ratio) { const int16_t lara_vehicle_num = Lara_Vehicle_GetIndex(); for (int32_t i = 0; i < Item_GetTotalCount(); i++) { ITEM *const item = Item_Get(i); if (((item->flags & IF_KILLED) || item->status == IS_INACTIVE) && i != lara_vehicle_num) { M_CommitItem(item); } else { M_InterpolateItem(item, ratio); } } ITEM *const lara_item = Lara_GetItem(); if (lara_item != nullptr) { M_InterpolateItem(lara_item, ratio); } } static void M_RememberEffect(EFFECT *const effect) { ASSERT(effect != nullptr); REMEMBER(effect, pos.x); REMEMBER(effect, pos.y); REMEMBER(effect, pos.z); REMEMBER(effect, rot.x); REMEMBER(effect, rot.y); REMEMBER(effect, rot.z); } static void M_InterpolateEffect(const double ratio, EFFECT *const effect) { ASSERT(effect != nullptr); const XYZ_32 max_delta = M_GetEffectMaxDelta(effect); INTERPOLATE(effect, pos.x, ratio, max_delta.x); INTERPOLATE(effect, pos.y, ratio, max_delta.y); INTERPOLATE(effect, pos.z, ratio, max_delta.z); INTERPOLATE_ROT(effect, rot.x, ratio, DEG_45); INTERPOLATE_ROT(effect, rot.y, ratio, DEG_45); INTERPOLATE_ROT(effect, rot.z, ratio, DEG_45); } static void M_RememberEffects(void) { int16_t effect_num = Effect_GetActiveNum(); while (effect_num != NO_EFFECT) { EFFECT *const effect = Effect_Get(effect_num); M_RememberEffect(effect); effect_num = effect->next_active; } } static void M_InterpolateEffects(const double ratio) { int16_t effect_num = Effect_GetActiveNum(); while (effect_num != NO_EFFECT) { EFFECT *const effect = Effect_Get(effect_num); M_InterpolateEffect(ratio, effect); effect_num = effect->next_active; } } void Interpolation_Enable(void) { m_IsEnabled = true; } void Interpolation_Disable(void) { m_IsEnabled = false; } bool Interpolation_IsEnabled(void) { return m_IsEnabled; } bool Interpolation_IsActive(void) { return m_IsEnabled && M_GetFPS() == 60 && !Shell_IsExiting(); } double Interpolation_GetWorldRate(void) { if (!Interpolation_IsActive()) { return 1.0; } return m_WorldRate; } double Interpolation_GetCameraRate(void) { if (!Interpolation_IsActive()) { return 1.0; } return m_CameraRate; } double Interpolation_GetRate(void) { return m_Rate; } void Interpolation_SetRate(double rate) { m_Rate = rate; } void Interpolation_Remember(void) { if (g_Camera.pos.room_num != NO_ROOM) { M_RememberCamera(); } if (g_Camera.type == CAM_PHOTO_MODE) { return; } LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara != nullptr) { M_RememberLara(lara); } if (Lara_Hair_IsActive()) { M_RememberBraid(); } M_RememberItems(); M_RememberEffects(); } void Interpolation_Interpolate(void) { if (g_Camera.type != CAM_PHOTO_MODE) { m_WorldRate = m_Rate; } m_CameraRate = m_Rate; if (g_Camera.pos.room_num != NO_ROOM) { if (DIFF(&g_Camera, shift) >= 128 || DIFF(&g_Camera, pos.x) >= CAM_MAX_DELTA || DIFF(&g_Camera, pos.y) >= CAM_MAX_DELTA || DIFF(&g_Camera, pos.z) >= CAM_MAX_DELTA || DIFF(&g_Camera, target.x) >= CAM_MAX_DELTA || DIFF(&g_Camera, target.y) >= CAM_MAX_DELTA || DIFF(&g_Camera, target.z) >= CAM_MAX_DELTA) { M_CommitCamera(); } else { M_InterpolateCamera(m_CameraRate); } g_Camera.interp.room_num = g_Camera.pos.room_num; Camera_ClampInterpResult(); } if (g_Camera.type == CAM_PHOTO_MODE) { return; } ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara != nullptr) { M_InterpolateLara(m_WorldRate, lara); } M_InterpolateItems(m_WorldRate); M_InterpolateEffects(m_WorldRate); if (lara_item != nullptr && Lara_Hair_IsActive()) { M_InterpolateBraid(m_WorldRate, lara_item); } } void Interpolation_CommitLara(void) { ITEM *const lara_item = Lara_GetItem(); if (lara_item != nullptr) { M_CommitItem(lara_item); } LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara != nullptr) { M_CommitLara(lara); } M_CommitBraid(); } void Interpolation_CommitBraid(void) { M_CommitBraid(); } ================================================ FILE: src/trx/game/interpolation.h ================================================ #pragma once void Interpolation_Enable(void); void Interpolation_Disable(void); bool Interpolation_IsEnabled(void); bool Interpolation_IsActive(void); double Interpolation_GetWorldRate(void); double Interpolation_GetCameraRate(void); double Interpolation_GetRate(void); void Interpolation_SetRate(double rate); void Interpolation_Interpolate(void); void Interpolation_Remember(void); // Instantly discard interpolation data void Interpolation_CommitLara(void); void Interpolation_CommitBraid(void); ================================================ FILE: src/trx/game/inventory.c ================================================ #include #include #include #include #include #include #include #include #include INVENTORY_MODE g_Inv_Mode = INV_TITLE_MODE; static int32_t M_GetFlareQuantity(void) { return Game_IsBonusFlagSet(GBF_JAPANESE) ? g_Weapons[LGT_FLARE].ammo.pickup_qty_alt : g_Weapons[LGT_FLARE].ammo.pickup_qty; } static INVENTORY_ITEM *M_GetGunInvItem(const LARA_GUN_TYPE gun_type) { // clang-format off switch (gun_type) { case LGT_PISTOLS: return InvRing_GetByObjectID(O_PISTOL_OPTION); case LGT_SHOTGUN: return InvRing_GetByObjectID(O_SHOTGUN_OPTION); case LGT_MAGNUMS: return InvRing_GetByObjectID(O_MAGNUM_OPTION); case LGT_AUTOS: return InvRing_GetByObjectID(O_AUTOS_OPTION); case LGT_DESERT_EAGLE: return InvRing_GetByObjectID(O_DESERT_EAGLE_OPTION); case LGT_UZIS: return InvRing_GetByObjectID(O_UZI_OPTION); case LGT_HARPOON: return InvRing_GetByObjectID(O_HARPOON_OPTION); case LGT_M16: return InvRing_GetByObjectID(O_M16_OPTION); case LGT_MP5: return InvRing_GetByObjectID(O_MP5_OPTION); case LGT_GRENADE: return InvRing_GetByObjectID(O_GRENADE_GUN_OPTION); case LGT_ROCKET: return InvRing_GetByObjectID(O_ROCKET_GUN_OPTION); default: return nullptr; } // clang-format on } static INVENTORY_ITEM *M_GetAmmoInvItem(const LARA_GUN_TYPE gun_type) { // clang-format off switch (gun_type) { case LGT_PISTOLS: return InvRing_GetByObjectID(O_PISTOL_AMMO_OPTION); case LGT_SHOTGUN: return InvRing_GetByObjectID(O_SHOTGUN_AMMO_OPTION); case LGT_MAGNUMS: return InvRing_GetByObjectID(O_MAGNUM_AMMO_OPTION); case LGT_AUTOS: return InvRing_GetByObjectID(O_AUTOS_AMMO_OPTION); case LGT_DESERT_EAGLE: return InvRing_GetByObjectID(O_DESERT_EAGLE_AMMO_OPTION); case LGT_UZIS: return InvRing_GetByObjectID(O_UZI_AMMO_OPTION); case LGT_HARPOON: return InvRing_GetByObjectID(O_HARPOON_AMMO_OPTION); case LGT_M16: return InvRing_GetByObjectID(O_M16_AMMO_OPTION); case LGT_MP5: return InvRing_GetByObjectID(O_MP5_AMMO_OPTION); case LGT_GRENADE: return InvRing_GetByObjectID(O_GRENADE_AMMO_OPTION); case LGT_ROCKET: return InvRing_GetByObjectID(O_ROCKET_AMMO_OPTION); default: return nullptr; } // clang-format on } static void M_IncreaseAmmo(const LARA_GUN_TYPE gun_type, const int32_t qty) { AMMO_INFO *const ammo = Gun_GetAmmoInfo(gun_type); ammo->ammo += qty; CLAMPG(ammo->ammo, MAX_QTY); } static RING_TYPE M_GetRingType(const INVENTORY_ITEM *const inv_item) { if (inv_item->inv_pos < 100) { return RT_MAIN; } else if (inv_item->inv_pos < 200) { return RT_KEYS; } else if (inv_item->inv_pos < 300) { return RT_OPTION; } else { return RT_GLOBE_SELECT; } } static void M_AddGun(const LARA_GUN_TYPE gun_type) { const OBJECT_ID gun_object = Gun_GetGunObject(gun_type); const OBJECT_ID ammo_object = Gun_GetAmmoObject(gun_type); LARA_INFO *const lara = Lara_GetLaraInfo(); for (int32_t i = Inv_RequestItem(ammo_object); i > 0; i--) { Inv_RemoveItem(ammo_object); M_IncreaseAmmo(gun_type, Gun_GetAmmoPickupQuantity(gun_type)); } M_IncreaseAmmo(gun_type, Gun_GetAmmoInitialQuantity(gun_type)); Inv_InsertItem(M_GetGunInvItem(gun_type)); if (lara->last_gun_type == LGT_UNARMED) { lara->last_gun_type = gun_type; } Item_GlobalReplace(gun_object, ammo_object); } static void M_AddAmmo(const LARA_GUN_TYPE gun_type) { const OBJECT_ID gun_object = Gun_GetGunObject(gun_type); if (Inv_RequestItem(gun_object)) { M_IncreaseAmmo(gun_type, Gun_GetAmmoPickupQuantity(gun_type)); } else { Inv_InsertItem(M_GetAmmoInvItem(gun_type)); } } bool Inv_AddItemNTimes(const OBJECT_ID object_id, const int32_t qty) { bool result = false; for (int32_t i = 0; i < qty; i++) { result |= Inv_AddItem(object_id); } return result; } OBJECT_ID Inv_GetItemOption(const OBJECT_ID object_id) { if (Object_IsType(object_id, g_InvObjects)) { return object_id; } return Object_GetCognate(object_id, g_ItemToInvObjectMap); } OBJECT_ID Inv_GetItemPickup(const OBJECT_ID object_id) { if (Object_IsType(object_id, g_InvObjects)) { return Object_GetCognateInverse(object_id, g_ItemToInvObjectMap); } return object_id; } void Inv_InsertItem(INVENTORY_ITEM *const inv_item) { Inv_InsertItemEx(inv_item, 1); } void Inv_InsertItemEx(INVENTORY_ITEM *const inv_item, const int32_t qty) { ASSERT(inv_item != nullptr); INV_RING_SOURCE *const source = &g_InvRing_Source[M_GetRingType(inv_item)]; int32_t n; for (n = 0; n < source->count; n++) { if (source->items[n]->inv_pos > inv_item->inv_pos) { break; } } for (int32_t i = source->count; i > n - 1; i--) { source->items[i + 1] = source->items[i]; source->qtys[i + 1] = source->qtys[i]; } source->items[n] = inv_item; source->qtys[n] = MIN(qty, MAX_QTY); source->count++; } bool Inv_RemoveItem(const OBJECT_ID object_id) { const OBJECT_ID inv_object_id = Inv_GetItemOption(object_id); for (RING_TYPE ring_type = 0; ring_type < RT_NUMBER_OF; ring_type++) { INV_RING_SOURCE *const source = &g_InvRing_Source[ring_type]; for (int32_t i = 0; i < source->count; i++) { if (source->items[i]->object_id != inv_object_id) { continue; } source->qtys[i]--; if (g_Config.gameplay.fix_item_duplication_glitch) { for (int32_t j = i; j < source->count; j++) { if (j == source->current) { source->current = 0; } } } if (source->qtys[i] == 0) { source->count--; for (int32_t j = i; j < source->count; j++) { source->items[j] = source->items[j + 1]; source->qtys[j] = source->qtys[j + 1]; } } return true; } } return false; } int32_t Inv_RequestItem(const OBJECT_ID object_id) { const OBJECT_ID inv_object_id = Inv_GetItemOption(object_id); for (RING_TYPE ring_type = 0; ring_type < RT_NUMBER_OF; ring_type++) { INV_RING_SOURCE *const source = &g_InvRing_Source[ring_type]; for (int32_t i = 0; i < source->count; i++) { if (source->items[i] != nullptr && source->items[i]->object_id == inv_object_id) { return source->qtys[i]; } } } return 0; } void Inv_ClearSelection(void) { g_InvRing_Source[RT_MAIN].current = 0; g_InvRing_Source[RT_KEYS].current = 0; } void Inv_RemoveAllItems(void) { g_InvRing_Source[RT_MAIN].count = 0; g_InvRing_Source[RT_KEYS].count = 0; g_InvRing_Source[RT_GLOBE_SELECT].count = 0; // Reset main ring Inv_AddItem(O_STOPWATCH_OPTION); Inv_AddItem(O_COMPASS_OPTION); Inv_AddItem(O_GLOBE_SELECT_OPTION); Inv_ClearSelection(); } bool Inv_AddItem(const OBJECT_ID object_id) { const OBJECT_ID inv_object_id = Inv_GetItemOption(object_id); const OBJECT_ID pickup_object_id = Inv_GetItemPickup(object_id); const OBJECT *const object = Object_Get(inv_object_id == NO_OBJECT ? object_id : inv_object_id); if (!object->loaded) { return false; } LARA_INFO *const lara = Lara_GetLaraInfo(); if (Object_IsType(pickup_object_id, g_GunObjects)) { Gun_UpdateLaraMeshes(pickup_object_id); if (lara->gun_type == LGT_UNARMED) { lara->gun_type = Gun_GetType(pickup_object_id); const bool hands_busy = lara->gun_status == LGS_HANDS_BUSY; lara->gun_status = LGS_ARMLESS; Gun_InitialiseNewWeapon(); if (hands_busy) { lara->gun_status = LGS_HANDS_BUSY; } } } const int32_t qty = object_id == O_FLAREBOX_ITEM ? M_GetFlareQuantity() : 1; for (RING_TYPE ring_type = 0; ring_type < RT_NUMBER_OF; ring_type++) { INV_RING_SOURCE *const source = &g_InvRing_Source[ring_type]; for (int32_t i = 0; i < source->count; i++) { if (source->items[i]->object_id == inv_object_id) { source->qtys[i] += qty; CLAMPG(source->qtys[i], MAX_QTY); return true; } } } // Pistols if (inv_object_id == O_PISTOL_OPTION) { Inv_InsertItem(InvRing_GetByObjectID(O_PISTOL_OPTION)); if (lara->last_gun_type == LGT_UNARMED) { lara->last_gun_type = LGT_PISTOLS; } return true; } // Other guns if (Object_IsType(pickup_object_id, g_GunObjects)) { M_AddGun(Gun_GetType(pickup_object_id)); return true; } if (Object_IsType(pickup_object_id, g_GunAmmoObjects)) { M_AddAmmo(Gun_GetType( Object_GetCognateInverse(pickup_object_id, g_GunAmmoObjectMap))); return true; } // Other cases for (int32_t i = 0; i < g_InvRing_Items->count; i++) { INVENTORY_ITEM *const inv_item = *(INVENTORY_ITEM **)Vector_Get(g_InvRing_Items, i); if (inv_item->object_id == object_id || inv_item->object_id == inv_object_id) { Inv_InsertItemEx(inv_item, qty); return true; } } return false; } bool Inv_AddPickup(const ITEM *const item) { if (Object_IsType(item->object_id, g_SecretObjects)) { Stats_MarkSecretCollected(item); if (Stats_CheckAllLevelSecretsPickedUp()) { GF_InventoryModifier_Apply(Game_GetCurrentLevel(), GF_INV_SECRET); } return true; } return Inv_AddItem(item->object_id); } ================================================ FILE: src/trx/game/inventory.h ================================================ #pragma once #include #include #include #include extern INVENTORY_MODE g_Inv_Mode; bool Inv_AddItemNTimes(OBJECT_ID obj_id, int32_t qty); OBJECT_ID Inv_GetItemOption(OBJECT_ID obj_id); OBJECT_ID Inv_GetItemPickup(OBJECT_ID obj_id); void Inv_InsertItem(INVENTORY_ITEM *inv_item); void Inv_InsertItemEx(INVENTORY_ITEM *inv_item, int32_t qty); bool Inv_RemoveItem(OBJECT_ID obj_id); int32_t Inv_RequestItem(OBJECT_ID obj_id); void Inv_ClearSelection(void); void Inv_RemoveAllItems(void); bool Inv_AddItem(OBJECT_ID obj_id); bool Inv_AddPickup(const ITEM *item); ================================================ FILE: src/trx/game/inventory_ring/control.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_INV_RING_FADE_TIME_FAST \ (INV_RING_CLOSE_FRAMES / INV_RING_FRAMES / (float)LOGIC_FPS) #define M_INV_RING_FADE_TIME_TO_BLACK 0.25f #define M_RING_SWITCH_FRAMES (96 / 2) #define M_SELECTING_FRAMES (32 / 2) static CLOCK_TIMER m_DemoTimer = { .type = CLOCK_TIMER_SIM }; static int32_t m_StartLevel; static OBJECT_ID m_InvChosen = NO_OBJECT; static INV_RING *m_ActiveRing = nullptr; INV_RING *InvRing_GetActiveRing(void) { return m_ActiveRing; } static void M_ShowAmmoQuantity(const char *const fmt, const int32_t qty) { if (!Game_IsBonusFlagSet(GBF_NGPLUS)) { InvRing_ShowItemQuantity(fmt, qty); } } static void M_RingIsOpen(INV_RING *const ring) { InvRing_ShowHeader(ring); } static void M_RingIsNotOpen(INV_RING *const ring) { InvRing_RemoveHeader(); InvRing_ShowExamine(NO_OBJECT, false); } static void M_RingNotActive( const INV_RING *const ring, const INVENTORY_ITEM *const inv_item) { InvRing_ShowItemName(inv_item); const LARA_INFO *const lara = Lara_GetLaraInfo(); const int32_t qty = Inv_RequestItem(inv_item->object_id); switch (inv_item->object_id) { case O_SHOTGUN_OPTION: M_ShowAmmoQuantity( g_TRVersion == 1 ? "%5d \\{ammo shotgun}" : "%5d", lara->shotgun_ammo.ammo / Gun_GetAmmoClipCount(LGT_SHOTGUN)); break; case O_MAGNUM_OPTION: M_ShowAmmoQuantity( g_TRVersion == 1 ? "%5d \\{ammo magnums}" : "%5d", lara->magnum_ammo.ammo); break; case O_AUTOS_OPTION: M_ShowAmmoQuantity("%5d", lara->autos_ammo.ammo); break; case O_DESERT_EAGLE_OPTION: M_ShowAmmoQuantity("%5d", lara->desert_eagle_ammo.ammo); break; case O_UZI_OPTION: M_ShowAmmoQuantity( g_TRVersion == 1 ? "%5d \\{ammo uzis}" : "%5d", lara->uzi_ammo.ammo); break; case O_HARPOON_OPTION: M_ShowAmmoQuantity("%5d", lara->harpoon_ammo.ammo); break; case O_M16_OPTION: M_ShowAmmoQuantity("%5d", lara->m16_ammo.ammo); break; case O_MP5_OPTION: M_ShowAmmoQuantity("%5d", lara->mp5_ammo.ammo); break; case O_GRENADE_GUN_OPTION: M_ShowAmmoQuantity("%5d", lara->grenade_ammo.ammo); break; case O_ROCKET_GUN_OPTION: M_ShowAmmoQuantity("%5d", lara->rocket_ammo.ammo); break; case O_SHOTGUN_AMMO_OPTION: M_ShowAmmoQuantity("%d", qty * Gun_GetAmmoShellCount(LGT_SHOTGUN)); break; case O_MAGNUM_AMMO_OPTION: case O_AUTOS_AMMO_OPTION: case O_DESERT_EAGLE_AMMO_OPTION: case O_UZI_AMMO_OPTION: case O_HARPOON_AMMO_OPTION: case O_M16_AMMO_OPTION: case O_MP5_AMMO_OPTION: M_ShowAmmoQuantity("%d", qty * 2); break; case O_GRENADE_AMMO_OPTION: case O_ROCKET_AMMO_OPTION: case O_FLAREBOX_OPTION: M_ShowAmmoQuantity("%d", qty); break; case O_SMALL_MEDIPACK_OPTION: case O_LARGE_MEDIPACK_OPTION: Overlay_ForceHealthBar(true); if (qty > 1) { InvRing_ShowItemQuantity("%d", qty); } break; case O_PUZZLE_OPTION_1: case O_PUZZLE_OPTION_2: case O_PUZZLE_OPTION_3: case O_PUZZLE_OPTION_4: case O_KEY_OPTION_1: case O_KEY_OPTION_2: case O_KEY_OPTION_3: case O_KEY_OPTION_4: case O_QUEST_OPTION_1: case O_QUEST_OPTION_2: case O_QUEST_OPTION_3: case O_QUEST_OPTION_4: case O_PICKUP_OPTION_1: case O_PICKUP_OPTION_2: case O_LEADBAR_OPTION: case O_SCION_OPTION: if (qty > 1) { InvRing_ShowItemQuantity("%d", qty); } break; default: break; } InvRing_ShowExamine( inv_item->object_id, ring->status == RNG_OPEN && Option_Examine_CanExamine(inv_item->object_id)); } static void M_RingActive(void) { InvRing_RemoveItemTexts(); InvRing_ShowExamine(NO_OBJECT, false); } static bool M_AnimateInventoryItem(INVENTORY_ITEM *const inv_item) { if (inv_item->current_frame == inv_item->goal_frame) { InvRing_SelectMeshes(inv_item); return false; } if (inv_item->anim_count > 0) { inv_item->anim_count--; } else { inv_item->anim_count = inv_item->anim_speed; inv_item->current_frame += inv_item->anim_direction; if (inv_item->current_frame >= inv_item->frames_total) { inv_item->current_frame = 0; } else if (inv_item->current_frame < 0) { inv_item->current_frame = inv_item->frames_total - 1; } } InvRing_SelectMeshes(inv_item); return true; } static GF_COMMAND M_Finish(INV_RING *const ring, const bool apply_changes) { // TODO: Make this function not have any side effects. // Consider adding new GF_ constants, but research other solutions first. if (ring->mode == INV_GLOBE_SELECT_MODE) { if (ring->globe_select.confirmed && ring->globe_select.selection >= 0 && ring->globe_select.selection < MAX_GLOBE_ZONES) { const int32_t start_level_num = ring->globe_select .start_level_num[ring->globe_select.selection]; if (start_level_num >= 0) { return (GF_COMMAND) { .action = GF_START_GAME, .param = start_level_num, }; } } return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE }; } if (m_StartLevel != -1) { return (GF_COMMAND) { .action = GF_SELECT_GAME, .param = m_StartLevel, }; } if (Shell_IsExiting()) { return (GF_COMMAND) { .action = GF_EXIT_GAME }; } else if (GF_GetOverrideCommand().action != GF_NOOP) { return GF_GetOverrideCommand(); } else if (ring->is_demo_needed) { return (GF_COMMAND) { .action = GF_START_DEMO, .param = -1 }; } switch (m_InvChosen) { case O_PASSPORT_OPTION: switch (g_Passport.select_action) { case PASSPORT_ACTION_LOAD_GAME: { if (apply_changes) { Inv_RemoveAllItems(); } return (GF_COMMAND) { .action = GF_START_SAVED_GAME, .param = Savegame_SlotToParam(g_Passport.select_save_slot), }; } case PASSPORT_ACTION_NEW_GAME: if (apply_changes) { Savegame_InitCurrentInfo(); } Savegame_UnbindSlot(); return (GF_COMMAND) { .action = GF_START_GAME, .param = g_Passport.select_level, }; case PASSPORT_ACTION_SWITCH_MOD: return (GF_COMMAND) { .action = GF_SWITCH_MOD }; case PASSPORT_ACTION_SAVE_GAME: { if (apply_changes) { Savegame_Save(g_Passport.select_save_slot); } return (GF_COMMAND) { .action = GF_NOOP }; } case PASSPORT_ACTION_RESTART: return (GF_COMMAND) { .action = GF_RESTART_GAME, .param = Game_GetCurrentLevel()->num, }; case PASSPORT_ACTION_EXIT_TO_TITLE: return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE }; case PASSPORT_ACTION_EXIT_GAME: return (GF_COMMAND) { .action = GF_EXIT_GAME }; case PASSPORT_ACTION_SELECT_LEVEL: return (GF_COMMAND) { .action = GF_SELECT_GAME, .param = g_Passport.select_level, }; case PASSPORT_ACTION_GLOBE_SELECT: return (GF_COMMAND) { .action = GF_GLOBE_SELECT, .param = g_Passport.select_level, }; case PASSPORT_ACTION_STORY_SO_FAR: return (GF_COMMAND) { .action = GF_STORY_SO_FAR, .param = Savegame_SlotToParam(g_Passport.select_save_slot), }; } break; case O_PHOTO_OPTION: if (apply_changes) { Savegame_UnbindSlot(); } if (GF_GetGymLevel() != nullptr) { return (GF_COMMAND) { .action = GF_START_GAME, .param = GF_GetGymLevel()->num, }; } break; case O_PISTOL_OPTION: case O_SHOTGUN_OPTION: case O_MAGNUM_OPTION: case O_AUTOS_OPTION: case O_DESERT_EAGLE_OPTION: case O_UZI_OPTION: case O_HARPOON_OPTION: case O_M16_OPTION: case O_MP5_OPTION: case O_GRENADE_GUN_OPTION: case O_ROCKET_GUN_OPTION: case O_SMALL_MEDIPACK_OPTION: case O_LARGE_MEDIPACK_OPTION: case O_FLAREBOX_OPTION: case O_KEY_OPTION_1: case O_KEY_OPTION_2: case O_KEY_OPTION_3: case O_KEY_OPTION_4: case O_PUZZLE_OPTION_1: case O_PUZZLE_OPTION_2: case O_PUZZLE_OPTION_3: case O_PUZZLE_OPTION_4: case O_LEADBAR_OPTION: case O_SCION_OPTION: if (apply_changes) { Lara_UseItem(m_InvChosen); } break; default: break; } return (GF_COMMAND) { .action = GF_NOOP }; } static bool M_CheckDemoTimer(const INV_RING *const ring) { if (!g_Config.gameplay.enable_demo || GF_GetLevelTable(GFLT_DEMOS)->count == 0) { return false; } if (ring->mode != INV_TITLE_MODE || InputState_IsAnyPressed(g_Input) || InputState_IsAnyPressed(g_InputDB) || Console_IsOpened()) { ClockTimer_Sync(&m_DemoTimer); return false; } return ring->status == RNG_OPEN && ClockTimer_CheckElapsed(&m_DemoTimer, g_Config.flow.demo_delay); } static void M_SetupRingSwitchClose( INV_RING *const ring, const RING_STATUS status_target) { InvRing_SetStatusTransition( ring, RNG_CLOSING, status_target, M_RING_SWITCH_FRAMES / 2); } static void M_TransitionToRing( INV_RING *const ring, const RING_TYPE source_type, const RING_TYPE target_type) { g_InvRing_Source[source_type].current = ring->current_object; g_InvRing_Source[source_type].count = ring->number_of_objects; ring->type = target_type; ring->list = g_InvRing_Source[target_type].items; ring->number_of_objects = g_InvRing_Source[target_type].count; ring->current_object = g_InvRing_Source[target_type].current; InvRing_SetStatusTransition( ring, RNG_OPENING, RNG_OPEN, M_RING_SWITCH_FRAMES / 2); } static void M_SnapshotRingState(INV_RING *const ring) { ring->prev_radius = ring->radius; ring->prev_camera_y = ring->camera.pos.y; ring->prev_camera_pitch = ring->camera_pitch; ring->prev_ring_rot_y = ring->ring_pos.rot.y; } static void M_SnapshotItemState(INVENTORY_ITEM *const inv_item) { inv_item->prev_x_rot_pt = inv_item->x_rot_pt; inv_item->prev_x_rot = inv_item->x_rot; inv_item->prev_y_rot = inv_item->y_rot; inv_item->prev_y_trans = inv_item->y_trans; inv_item->prev_z_trans = inv_item->z_trans; inv_item->prev_manual_rot = inv_item->manual_rot; } static void M_SnapshotFrameState(INV_RING *const ring) { M_SnapshotRingState(ring); for (int32_t i = 0; i < ring->number_of_objects; i++) { M_SnapshotItemState(ring->list[i]); } } static GF_COMMAND M_Control(INV_RING *const ring) { if (ring->status == RNG_OPENING) { if (ring->mode == INV_TITLE_MODE && (Fader_IsActive(&ring->top_fader) || Fader_IsActive(&ring->back_fader))) { return (GF_COMMAND) { .action = GF_NOOP }; } ClockTimer_Sync(&m_DemoTimer); if (!ring->has_spun_out) { Sound_Effect(SFX_MENU_SPININ, nullptr, SPM_ALWAYS); ring->has_spun_out = true; } } if (ring->status == RNG_FADING_OUT) { if (ring->mode == INV_TITLE_MODE) { const GF_COMMAND gf_cmd = M_Finish(ring, true); ring->is_done = true; ring->status = RNG_DONE; return gf_cmd; } if (!Fader_IsActive(&ring->back_fader) && !Fader_IsActive(&ring->top_fader)) { Fader_InitFromCurrentHold( &ring->top_fader, 1.0f, M_INV_RING_FADE_TIME_TO_BLACK, 1.0f / (float)LOGIC_FPS); } if (Fader_IsActive(&ring->top_fader) || Fader_IsActive(&ring->back_fader)) { return (GF_COMMAND) { .action = GF_NOOP }; } ring->status = RNG_DONE; } if (ring->status == RNG_DONE && !ring->is_done) { const GF_COMMAND gf_cmd = M_Finish(ring, true); ring->is_done = true; // Returning to game – resume music if (gf_cmd.action == GF_NOOP) { Music_Unpause(); Sound_UnpauseAll(); } return gf_cmd; } InvRing_CalcAdders(ring, INV_RING_ROTATE_DURATION); Input_Update(); // Do the demo inactivity check prior to postprocessing of the inputs. if (M_CheckDemoTimer(ring)) { ring->is_demo_needed = true; } Shell_ProcessInput(); Game_ProcessInput(); if (ring->mode == INV_GLOBE_SELECT_MODE) { m_StartLevel = -1; } else { m_StartLevel = Game_IsLevelComplete() ? g_Passport.select_level : -1; } if (g_Config.gameplay.enable_timer_in_inventory && !(Game_IsInGym() && Gym_TrackManager_HasStats(GYM_TRACK_ASSAULT))) { Stats_UpdateTimer(); } if (Shell_IsExiting()) { return (GF_COMMAND) { .action = GF_EXIT_GAME }; } if ((ring->mode == INV_SAVE_MODE || ring->mode == INV_SAVE_CRYSTAL_MODE || ring->mode == INV_LOAD_MODE || ring->mode == INV_DEATH_MODE || ring->mode == INV_GLOBE_SELECT_MODE) && !ring->is_pass_open) { g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) { .menu_confirm = 1 }; } if (ring->mode != INV_TITLE_MODE && !Fader_IsActive(&ring->back_fader) && !Fader_IsActive(&ring->top_fader) && ring->status != RNG_OPENING) { for (int i = 0; i < ring->number_of_objects; i++) { INVENTORY_ITEM *const inv_item = ring->list[i]; if (inv_item->object_id == O_COMPASS_OPTION) { Option_Stats_UpdateCompassNeedle(inv_item); } } } if (ring->rotating) { return (GF_COMMAND) { .action = GF_NOOP }; } switch (ring->status) { case RNG_OPEN: if (g_Input.menu_right && ring->number_of_objects > 1) { InvRing_RotateLeft(ring); Sound_Effect(SFX_MENU_ROTATE, nullptr, SPM_ALWAYS); break; } if (g_Input.menu_left && ring->number_of_objects > 1) { InvRing_RotateRight(ring); Sound_Effect(SFX_MENU_ROTATE, nullptr, SPM_ALWAYS); break; } if (m_StartLevel != -1 || ring->is_demo_needed || (g_InputDB.menu_back && ring->mode != INV_TITLE_MODE && ring->mode != INV_GLOBE_SELECT_MODE)) { Sound_Effect(SFX_MENU_SPINOUT, nullptr, SPM_ALWAYS); m_InvChosen = NO_OBJECT; if (ring->type == RT_MAIN) { g_InvRing_Source[RT_MAIN].current = ring->current_object; } else if (ring->type != RT_NUMBER_OF) { g_InvRing_Source[ring->type].current = ring->current_object; } if (M_Finish(ring, false).action != GF_NOOP) { InvRing_SetStatusTransition( ring, RNG_CLOSING, RNG_FADING_OUT, INV_RING_CLOSE_FRAMES); } else { InvRing_SetStatusTransition( ring, RNG_CLOSING, RNG_DONE, INV_RING_CLOSE_FRAMES); if (g_Config.visuals.enable_fade_effects && g_Config.ui.inventory_fade_effects) { Fader_InitFromCurrent( &ring->back_fader, 0.0f, M_INV_RING_FADE_TIME_FAST); } } g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; } const bool examine = g_InputDB.look && InvRing_CanExamine(); if (g_InputDB.menu_confirm || examine) { if ((ring->mode == INV_SAVE_MODE || ring->mode == INV_SAVE_CRYSTAL_MODE || ring->mode == INV_LOAD_MODE || ring->mode == INV_DEATH_MODE || ring->mode == INV_GLOBE_SELECT_MODE) && !ring->is_pass_open) { ring->is_pass_open = true; } g_InvRing_Source[ring->type].current = ring->current_object; INVENTORY_ITEM *const inv_item = g_InvRing_Source[ring->type].items[ring->current_object]; if (examine) { inv_item->action = ACTION_EXAMINE; inv_item->goal_frame = 0; inv_item->anim_direction = 1; } else { inv_item->action = ACTION_USE; inv_item->goal_frame = inv_item->open_frame; inv_item->anim_direction = 1; } InvRing_SetStatusTransition( ring, RNG_SELECTING, RNG_SELECTED, M_SELECTING_FRAMES); g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; switch (inv_item->object_id) { case O_COMPASS_OPTION: Sound_Effect(SFX_MENU_COMPASS, nullptr, SPM_ALWAYS); break; case O_STOPWATCH_OPTION: break; case O_PHOTO_OPTION: Sound_Effect(SFX_MENU_LARA_HOME, nullptr, SPM_ALWAYS); break; case O_CONTROL_OPTION: Sound_Effect(SFX_MENU_GAMEBOY, nullptr, SPM_ALWAYS); break; case O_PISTOL_OPTION: case O_SHOTGUN_OPTION: case O_MAGNUM_OPTION: case O_AUTOS_OPTION: case O_DESERT_EAGLE_OPTION: case O_UZI_OPTION: case O_HARPOON_OPTION: case O_M16_OPTION: case O_MP5_OPTION: case O_GRENADE_GUN_OPTION: case O_ROCKET_GUN_OPTION: Sound_Effect(SFX_MENU_GUNS, nullptr, SPM_ALWAYS); break; default: Sound_Effect(SFX_MENU_CHOOSE, nullptr, SPM_ALWAYS); break; } } if (g_InputDB.menu_up && ring->mode != INV_TITLE_MODE && ring->mode != INV_KEYS_MODE && ring->mode != INV_GLOBE_SELECT_MODE) { if (ring->type == RT_MAIN) { if (g_InvRing_Source[RT_KEYS].count > 0) { M_SetupRingSwitchClose(ring, RNG_MAIN2KEYS); } g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; } else if (ring->type == RT_OPTION) { if (g_InvRing_Source[RT_MAIN].count > 0) { M_SetupRingSwitchClose(ring, RNG_OPTION2MAIN); } g_InputDB = (INPUT_STATE) {}; } } else if ( g_InputDB.menu_down && ring->mode != INV_TITLE_MODE && ring->mode != INV_KEYS_MODE && ring->mode != INV_GLOBE_SELECT_MODE) { if (ring->type == RT_MAIN) { if (g_InvRing_Source[RT_OPTION].count > 0 && !InvRing_IsOptionLockedOut()) { M_SetupRingSwitchClose(ring, RNG_MAIN2OPTION); } g_InputDB = (INPUT_STATE) {}; } else if (ring->type == RT_KEYS) { if (g_InvRing_Source[RT_MAIN].count > 0) { M_SetupRingSwitchClose(ring, RNG_KEYS2MAIN); } g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; } } break; case RNG_MAIN2OPTION: M_TransitionToRing(ring, RT_MAIN, RT_OPTION); break; case RNG_MAIN2KEYS: M_TransitionToRing(ring, RT_MAIN, RT_KEYS); break; case RNG_KEYS2MAIN: M_TransitionToRing(ring, RT_KEYS, RT_MAIN); break; case RNG_OPTION2MAIN: M_TransitionToRing(ring, RT_OPTION, RT_MAIN); break; case RNG_SELECTED: { INVENTORY_ITEM *inv_item = ring->list[ring->current_object]; if (inv_item->object_id == O_PASSPORT_CLOSED) { inv_item->object_id = O_PASSPORT_OPTION; } bool busy = false; for (int32_t frame = 0; frame < INV_RING_FRAMES; frame++) { busy = false; if (inv_item->y_rot == inv_item->y_rot_sel) { busy = M_AnimateInventoryItem(inv_item); } } Option_Control(inv_item, busy); if (!busy) { if (g_InputDB.menu_back && ring->mode != INV_GLOBE_SELECT_MODE) { InvRing_SetStatusTransition( ring, RNG_CLOSING_ITEM, RNG_DESELECT, 0); g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; if (ring->mode == INV_LOAD_MODE || ring->mode == INV_SAVE_MODE || ring->mode == INV_SAVE_CRYSTAL_MODE) { InvRing_SetStatusTransition( ring, RNG_CLOSING_ITEM, RNG_EXITING_INVENTORY, 0); g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; } } if (g_InputDB.menu_confirm) { m_InvChosen = inv_item->object_id; if (ring->type == RT_MAIN) { g_InvRing_Source[RT_MAIN].current = ring->current_object; } else if (ring->type != RT_NUMBER_OF) { g_InvRing_Source[ring->type].current = ring->current_object; } if (ring->mode == INV_TITLE_MODE && (inv_item->object_id == O_DETAIL_OPTION || inv_item->object_id == O_SOUND_OPTION || inv_item->object_id == O_PDA_OPTION || inv_item->object_id == O_CONTROL_OPTION || inv_item->object_id == O_GLOBE_SELECT_OPTION)) { InvRing_SetStatusTransition( ring, RNG_CLOSING_ITEM, RNG_DESELECT, 0); } else { InvRing_SetStatusTransition( ring, RNG_CLOSING_ITEM, RNG_EXITING_INVENTORY, 0); } g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; } } break; } case RNG_DESELECT: { INVENTORY_ITEM *const inv_item = ring->list[ring->current_object]; Option_Close(inv_item); Sound_Effect(SFX_MENU_SPINOUT, nullptr, SPM_ALWAYS); InvRing_SetStatusTransition( ring, RNG_DESELECTING, RNG_OPEN, M_SELECTING_FRAMES); g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; break; } case RNG_CLOSING_ITEM: { INVENTORY_ITEM *inv_item = ring->list[ring->current_object]; for (int32_t frame = 0; frame < INV_RING_FRAMES; frame++) { if (!M_AnimateInventoryItem(inv_item)) { if (inv_item->object_id == O_PASSPORT_OPTION) { inv_item->object_id = O_PASSPORT_CLOSED; inv_item->current_frame = 0; } InvRing_SetStatusTransition( ring, ring->status_target, ring->status_target, M_SELECTING_FRAMES); break; } } break; } case RNG_EXITING_INVENTORY: if (ring->status_frames == 0) { if (M_Finish(ring, false).action != GF_NOOP) { // Fade to black. Do it later once reaching RNG_FADING_OUT. InvRing_SetStatusTransition( ring, RNG_CLOSING, RNG_FADING_OUT, INV_RING_CLOSE_FRAMES); } else { // Fade to game. Do it as soon as the ring starts to close. InvRing_SetStatusTransition( ring, RNG_CLOSING, RNG_DONE, INV_RING_CLOSE_FRAMES); if (g_Config.visuals.enable_fade_effects && g_Config.ui.inventory_fade_effects) { Fader_InitFromCurrent( &ring->back_fader, 0.0f, M_INV_RING_FADE_TIME_FAST); } } } break; default: break; } if (ring->status == RNG_OPEN || ring->status == RNG_SELECTING || ring->status == RNG_SELECTED || ring->status == RNG_DESELECTING || ring->status == RNG_DESELECT || ring->status == RNG_CLOSING_ITEM) { if (!ring->rotating && ((!g_Input.menu_left && !g_Input.menu_right) || ring->number_of_objects <= 1)) { INVENTORY_ITEM *const inv_item = ring->list[ring->current_object]; M_RingNotActive(ring, inv_item); } M_RingIsOpen(ring); } else { M_RingIsNotOpen(ring); } if (ring->status == RNG_OPENING || ring->status == RNG_CLOSING || ring->status == RNG_MAIN2OPTION || ring->status == RNG_OPTION2MAIN || ring->status == RNG_EXITING_INVENTORY || ring->status == RNG_FADING_OUT || ring->status == RNG_DONE || ring->rotating) { M_RingActive(); } Interpolation_Remember(); return (GF_COMMAND) { .action = GF_NOOP }; } void InvRing_RemoveAllText(void) { InvRing_RemoveHeader(); InvRing_RemoveItemTexts(); InvRing_ClearButtonHint(); } INV_RING *InvRing_Open(const INVENTORY_MODE mode) { if (mode == INV_KEYS_MODE && g_InvRing_Source[RT_KEYS].count == 0) { m_InvChosen = NO_OBJECT; return nullptr; } m_InvChosen = NO_OBJECT; g_InvRing_OldCamera = g_Camera; m_StartLevel = -1; if (mode == INV_TITLE_MODE) { InvRing_ShowVersionText(); Savegame_ScanSavedGames(); } else { InvRing_RemoveVersionText(); } if (mode != INV_GLOBE_SELECT_MODE) { // Reset option ring g_InvRing_Source[RT_OPTION].count = 0; Inv_InsertItem( InvRing_GetByObjectID(O_PASSPORT_CLOSED) != nullptr ? InvRing_GetByObjectID(O_PASSPORT_CLOSED) : InvRing_GetByObjectID(O_PASSPORT_OPTION)); if (g_TRVersion == 1) { Inv_InsertItem(InvRing_GetByObjectID(O_CONTROL_OPTION)); Inv_InsertItem(InvRing_GetByObjectID(O_SOUND_OPTION)); Inv_InsertItem(InvRing_GetByObjectID(O_DETAIL_OPTION)); } else { Inv_InsertItem(InvRing_GetByObjectID(O_DETAIL_OPTION)); Inv_InsertItem(InvRing_GetByObjectID(O_CONTROL_OPTION)); Inv_InsertItem(InvRing_GetByObjectID(O_SOUND_OPTION)); } Inv_InsertItem(InvRing_GetByObjectID(O_PDA_OPTION)); if (mode == INV_TITLE_MODE && GF_GetGymLevel() != nullptr) { Inv_InsertItem(InvRing_GetByObjectID(O_PHOTO_OPTION)); } } else if (g_InvRing_Source[RT_GLOBE_SELECT].count == 0) { INVENTORY_ITEM *const globe = InvRing_GetByObjectID(O_GLOBE_SELECT_OPTION); if (globe != nullptr) { Inv_InsertItem(globe); } } g_InvRing_Source[RT_KEYS].current = 0; for (int32_t i = 0; i < g_InvRing_Source[RT_KEYS].count; i++) { InvRing_InitInvItem(g_InvRing_Source[RT_KEYS].items[i]); } g_InvRing_Source[RT_MAIN].current = 0; for (int32_t i = 0; i < g_InvRing_Source[RT_MAIN].count; i++) { InvRing_InitInvItem(g_InvRing_Source[RT_MAIN].items[i]); } g_InvRing_Source[RT_OPTION].current = 0; for (int32_t i = 0; i < g_InvRing_Source[RT_OPTION].count; i++) { g_InvRing_Source[RT_OPTION].qtys[i] = 1; InvRing_InitInvItem(g_InvRing_Source[RT_OPTION].items[i]); } g_InvRing_Source[RT_GLOBE_SELECT].current = 0; for (int32_t i = 0; i < g_InvRing_Source[RT_GLOBE_SELECT].count; i++) { g_InvRing_Source[RT_GLOBE_SELECT].qtys[i] = 1; InvRing_InitInvItem(g_InvRing_Source[RT_GLOBE_SELECT].items[i]); } if (mode == INV_TITLE_MODE && GF_GetGymLevel() != nullptr && Gym_IsInventoryOpenEnabled()) { for (int32_t i = 0; i < g_InvRing_Source[RT_OPTION].count; i++) { if (g_InvRing_Source[RT_OPTION].items[i]->object_id == O_PHOTO_OPTION) { g_InvRing_Source[RT_OPTION].current = i; } } } if (!g_Config.audio.enable_music_in_inventory && mode != INV_TITLE_MODE) { Music_Pause(); Sound_PauseAll(); } INV_RING *const ring = Memory_Alloc(sizeof(INV_RING)); ring->mode = mode; ring->background_style = mode == INV_TITLE_MODE ? BK_IMAGE : g_Config.ui.inventory_background_style; ring->background_path = ring->background_style == BK_IMAGE ? g_GameFlow.main_menu_background_path : nullptr; switch (mode) { case INV_GLOBE_SELECT_MODE: ring->background_style = BK_NONE; ring->background_path = nullptr; InvRing_InitRing( ring, RT_GLOBE_SELECT, g_InvRing_Source[RT_GLOBE_SELECT].items, g_InvRing_Source[RT_GLOBE_SELECT].count, g_InvRing_Source[RT_GLOBE_SELECT].current); Option_GlobeSelect_UpdateSelectable(ring); break; case INV_TITLE_MODE: case INV_SAVE_MODE: case INV_SAVE_CRYSTAL_MODE: case INV_LOAD_MODE: case INV_DEATH_MODE: InvRing_InitRing( ring, RT_OPTION, g_InvRing_Source[RT_OPTION].items, g_InvRing_Source[RT_OPTION].count, g_InvRing_Source[RT_OPTION].current); break; case INV_KEYS_MODE: InvRing_InitRing( ring, RT_KEYS, g_InvRing_Source[RT_KEYS].items, g_InvRing_Source[RT_KEYS].count, g_InvRing_Source[RT_MAIN].current); break; default: if (g_InvRing_Source[RT_MAIN].count > 0) { InvRing_InitRing( ring, RT_MAIN, g_InvRing_Source[RT_MAIN].items, g_InvRing_Source[RT_MAIN].count, g_InvRing_Source[RT_MAIN].current); } else { InvRing_InitRing( ring, RT_OPTION, g_InvRing_Source[RT_OPTION].items, g_InvRing_Source[RT_OPTION].count, g_InvRing_Source[RT_OPTION].current); } break; } g_Inv_Mode = mode; Interpolation_Remember(); if (mode == INV_TITLE_MODE) { if (ring->background_path != nullptr) { Output_Overlay_LoadImage(ring->background_path); } Fader_InitTo(&ring->top_fader, 1.0f, 0.0f, M_INV_RING_FADE_TIME_FAST); } else { Fader_InitTo(&ring->back_fader, 0.0f, 1.0f, M_INV_RING_FADE_TIME_FAST); } return ring; } void InvRing_Close(INV_RING *const ring) { InvRing_RemoveAllText(); InvRing_RemoveVersionText(); if (ring->list != nullptr) { INVENTORY_ITEM *const inv_item = ring->list[ring->current_object]; if (inv_item != nullptr) { Option_Close(inv_item); } } if (ring->mode == INV_TITLE_MODE) { Music_Stop(); Sound_StopAll(); } if (g_Config.input.enable_buffering_inventory) { g_OldInputDB = (INPUT_STATE) {}; } m_InvChosen = NO_OBJECT; Memory_Free(ring); } GF_COMMAND InvRing_Control(INV_RING *const ring) { InvRing_AdjustMusicVolume(ring); m_ActiveRing = ring; INVENTORY_ITEM **const prev_list = ring->list; M_SnapshotFrameState(ring); GF_COMMAND gf_cmd = M_Control(ring); if (ring->status != RNG_OPENING && ring->status != RNG_DONE && ring->status != RNG_FADING_OUT) { for (int32_t frame = 0; frame < INV_RING_FRAMES; frame++) { for (int32_t i = 0; i < ring->number_of_objects; i++) { InvRing_UpdateInventoryItem(ring, ring->list[i]); } } } if (ring->status != RNG_DONE && (ring->status != RNG_OPENING || (ring->mode != INV_TITLE_MODE || (!Fader_IsActive(&ring->top_fader) && !Fader_IsActive(&ring->back_fader))))) { for (int32_t frame = 0; frame < INV_RING_FRAMES; frame++) { InvRing_DoMotions(ring); } } if (ring->list != prev_list) { M_SnapshotFrameState(ring); } // Running motions in control can reach RNG_DONE in this same tick. // Finalize immediately so phase code receives the non-NOOP GF command. if (gf_cmd.action == GF_NOOP && ring->status == RNG_DONE && !ring->is_done) { gf_cmd = M_Control(ring); } m_ActiveRing = nullptr; Overlay_Animate(1); return gf_cmd; } bool InvRing_IsRingAvailable(const RING_TYPE ring_type) { if (ring_type == RT_OPTION && InvRing_IsOptionLockedOut()) { return false; } return g_InvRing_Source[ring_type].count > 0; } bool InvRing_IsOptionLockedOut(void) { return g_Config.flow.lockout_option_ring; } INVENTORY_ITEM *InvRing_GetByObjectID(const OBJECT_ID object_id) { for (int32_t i = 0; i < g_InvRing_Items->count; i++) { INVENTORY_ITEM *const item = *(INVENTORY_ITEM **)Vector_Get(g_InvRing_Items, i); if (item->object_id == object_id) { return item; } } return nullptr; } ================================================ FILE: src/trx/game/inventory_ring/control.h ================================================ #pragma once #include #include #include typedef void (*INV_RING_BUTTON_HINT_DRAWER)(void *user_data); INV_RING *InvRing_Open(INVENTORY_MODE mode); void InvRing_Close(INV_RING *ring); GF_COMMAND InvRing_Control(INV_RING *ring); bool InvRing_IsRingAvailable(RING_TYPE ring_type); INV_RING *InvRing_GetActiveRing(void); void InvRing_AdjustMusicVolume(const INV_RING *ring); void InvRing_SetRequestedObjectID(OBJECT_ID obj_id); void InvRing_SetButtonHintDrawer( INV_RING_BUTTON_HINT_DRAWER draw_func, void *user_data); void InvRing_ClearButtonHint(void); void InvRing_RemoveAllText(void); INVENTORY_ITEM *InvRing_GetByObjectID(OBJECT_ID object_id); ================================================ FILE: src/trx/game/inventory_ring/draw.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_CAMERA_2_RING 598 #define M_SHADE_NORMAL SHADE_LOW #define M_SHADE_SELECTED SHADE_NEUTRAL static XYZ_32 M_VectorViewFromWorld(const XYZ_32 v_world) { return Matrix_MulVec32_M(&g_ViewMatrix, v_world); } static int16_t M_LerpI16( const int16_t prev_value, const int16_t cur_value, const double rate) { return (int16_t)round(LERP(prev_value, cur_value, rate)); } static int32_t M_LerpI32( const int32_t prev_value, const int32_t cur_value, const double rate) { return (int32_t)round(LERP(prev_value, cur_value, rate)); } static int16_t M_LerpAngleI16( const int16_t prev_value, const int16_t cur_value, const double rate) { const int32_t prev_u16 = (uint16_t)prev_value; const int32_t cur_u16 = (uint16_t)cur_value; const int32_t interp = Math_AngleMean(prev_u16, cur_u16, rate); return (int16_t)(uint16_t)interp; } static float M_GlobeSelectPulse01(const float time) { const int16_t angle = (((uint64_t)time) % 16ULL) * DEG_360 / 16; const float s = (float)Math_Sin(angle); return (s + 16384.0f) / (16384.0f * 2.0f); } static void M_GlobeSelectApplyLight( const INV_RING *const ring, const uint32_t bit, const int32_t mesh_idx) { const float ambient_u8 = 32.0f / 255.0f; const RGB_F ambient = { ambient_u8, ambient_u8, ambient_u8 }; RGB_F colors[3] = {}; if (bit == 1u) { colors[0] = (RGB_F) { 0, 256.0f / 4096.0f, 3840.0f / 4096.0f }; colors[1] = (RGB_F) { 0, 256.0f / 4096.0f, 3840.0f / 4096.0f }; colors[2] = (RGB_F) { 0, 256.0f / 4096.0f, 3840.0f / 4096.0f }; } else if ((bit & 0x7Eu) != 0u) { const float pulse = M_GlobeSelectPulse01(Output_GetTime()); const int32_t area_idx = Option_GlobeSelect_AreaFromMeshIdx(mesh_idx); const bool completed = area_idx >= 0 && area_idx < MAX_GLOBE_ZONES && !ring->globe_select.selectable[area_idx]; const RGB_F marker = completed ? (RGB_F) { pulse, 0.0f, 0.0f } : (RGB_F) { 0.0f, pulse, 0.0f }; colors[0] = marker; colors[1] = marker; colors[2] = marker; } else { colors[0] = (RGB_F) { 256.0f / 4096.0f, 1024.0f / 4096.0f, 256.0f / 4096.0f }; colors[1] = (RGB_F) { 256.0f / 4096.0f, 1024.0f / 4096.0f, 256.0f / 4096.0f }; colors[2] = (RGB_F) { 256.0f / 4096.0f, 1024.0f / 4096.0f, 256.0f / 4096.0f }; } const XYZ_32 dirs_offsets[3] = { { .x = 0x1000, .y = -0x1000, .z = 0xC00 }, { .x = -0x1000, .y = -0x1000, .z = 0xC00 }, { .x = 0, .y = 0x800, .z = 0xC00 }, }; const XYZ_32 dirs_view[3] = { M_VectorViewFromWorld(dirs_offsets[0]), M_VectorViewFromWorld(dirs_offsets[1]), M_VectorViewFromWorld(dirs_offsets[2]), }; Output_SetTR3Light(ambient, colors, dirs_view); } static int32_t M_GetFrames( const INV_RING *const ring, const INVENTORY_ITEM *const inv_item, ANIM_FRAME **const out_frame1, ANIM_FRAME **const out_frame2, int32_t *const out_rate) { const OBJECT *const obj = Object_Get(inv_item->object_id); const INVENTORY_ITEM *const cur_inv_item = ring->list[ring->current_object]; if (inv_item != cur_inv_item || inv_item->current_frame == 0 || (ring->status != RNG_SELECTED && ring->status != RNG_CLOSING_ITEM)) { // only apply to animations, eg. the states where Inv_AnimateItem is // being actively called goto fallback; } if (inv_item->current_frame == inv_item->goal_frame || inv_item->frames_total == 1 || g_Config.rendering.fps == 30) { goto fallback; } const int32_t cur_frame_num = inv_item->current_frame; int32_t next_frame_num = inv_item->current_frame + inv_item->anim_direction; if (next_frame_num < 0) { next_frame_num = 0; } if (next_frame_num >= inv_item->frames_total) { next_frame_num = 0; } *out_frame1 = &obj->frame_base[cur_frame_num]; *out_frame2 = &obj->frame_base[next_frame_num]; *out_rate = 10; return (Interpolation_GetRate() - 0.5) * 10.0; // OG fallback: *out_frame1 = &obj->frame_base[inv_item->current_frame]; *out_frame2 = *out_frame1; *out_rate = 1; return 0; } static void M_DrawItem( const INV_RING *const ring, const INVENTORY_ITEM *const inv_item, const int16_t view_rot_y) { const double interp_rate = Interpolation_GetRate(); const int16_t draw_x_rot_pt = M_LerpAngleI16( inv_item->prev_x_rot_pt, inv_item->x_rot_pt, interp_rate); const int16_t draw_x_rot = M_LerpAngleI16(inv_item->prev_x_rot, inv_item->x_rot, interp_rate); const int16_t draw_y_rot = M_LerpAngleI16(inv_item->prev_y_rot, inv_item->y_rot, interp_rate); const int32_t draw_y_trans = M_LerpI32(inv_item->prev_y_trans, inv_item->y_trans, interp_rate); const int32_t draw_z_trans = M_LerpI32(inv_item->prev_z_trans, inv_item->z_trans, interp_rate); MATRIX draw_manual_rot = inv_item->prev_manual_rot; Matrix_Slerp3x3_M(&draw_manual_rot, &inv_item->manual_rot, interp_rate); int32_t shade = M_SHADE_NORMAL; if (ring->status != RNG_FADING_OUT && ring->status != RNG_DONE) { if (ring->rotating) { float t = (ring->rot_count / (float)INV_RING_ROTATE_DURATION); CLAMP(t, 0.0f, 1.0f); if (inv_item == ring->list[ring->rotate_from_object]) { t = 1.0f - t; } else if (inv_item != ring->list[ring->rotate_to_object]) { t = 1.0f; } shade = LERP((float)M_SHADE_SELECTED, (float)M_SHADE_NORMAL, t); } else if (inv_item == ring->list[ring->current_object]) { shade = M_SHADE_SELECTED; } } Output_SetLightAdder(shade); Matrix_TranslateRel(0, draw_y_trans, draw_z_trans); Matrix_RotX(-draw_x_rot_pt); Matrix_RotY(-view_rot_y); Matrix_Mul3x3(&draw_manual_rot); Matrix_RotY(view_rot_y); Matrix_RotX(draw_x_rot_pt); Matrix_RotY(draw_y_rot); Matrix_RotX(draw_x_rot); const OBJECT *const obj = Object_Get(inv_item->object_id); if (!obj->loaded || obj->mesh_count < 0) { return; } if (inv_item->object_id == O_GLOBE_SELECT_OPTION) { Matrix_Rot16(ring->globe_select.rot); InvRing_Light(ring); ANIM_FRAME *const frame = &obj->frame_base[0]; const uint32_t mesh_bits = ring->globe_select.meshes_drawn; for (int32_t mesh_idx = 0; mesh_idx < obj->mesh_count; mesh_idx++) { if (mesh_idx == 0) { Matrix_TranslateRel16(frame->offset); Matrix_Rot16(frame->mesh_rots[mesh_idx]); } else { const ANIM_BONE *const bone = Object_GetBone(obj, mesh_idx - 1); if (bone->matrix_pop) { Matrix_Pop(); } if (bone->matrix_push) { Matrix_Push(); } Matrix_TranslateRel32(bone->pos); Matrix_Rot16(frame->mesh_rots[mesh_idx]); } const uint32_t bit = 1u << mesh_idx; if ((mesh_bits & bit) == 0u) { continue; } M_GlobeSelectApplyLight(ring, bit, mesh_idx); Object_DrawMesh(obj->mesh_idx + mesh_idx, 0, false); } return; } int32_t rate; ANIM_FRAME *frame1; ANIM_FRAME *frame2; const int32_t frac = M_GetFrames(ring, inv_item, &frame1, &frame2, &rate); if (inv_item->object_id == O_COMPASS_OPTION) { const int16_t extra_rotation[1] = { Option_Stats_GetCompassNeedleAngle() }; Object_GetBone(obj, 0)->rot.y = true; Object_DrawInterpolatedObject( obj, inv_item->meshes_drawn, extra_rotation, frame1, frame2, frac, rate); } else if (inv_item->object_id == O_STOPWATCH_OPTION) { const RESUME_INFO *const current_info = Savegame_GetCurrentInfo(Game_GetCurrentLevel()); const int32_t total_seconds = current_info->stats.timer / LOGIC_FPS; const int32_t hours = (total_seconds % 43200) * DEG_1 * -360 / 43200; const int32_t minutes = (total_seconds % 3600) * DEG_1 * -360 / 3600; const int32_t seconds = (total_seconds % 60) * DEG_1 * -360 / 60; const int16_t extra_rotation[3] = { hours, minutes, seconds }; Object_GetBone(obj, 3)->rot.z = true; Object_GetBone(obj, 4)->rot.z = true; Object_GetBone(obj, 5)->rot.z = true; Object_DrawInterpolatedObject( obj, inv_item->meshes_drawn, extra_rotation, frame1, frame2, frac, rate); } else { Object_DrawInterpolatedObject( obj, inv_item->meshes_drawn, nullptr, frame1, frame2, frac, rate); } } const INVENTORY_ITEM *InvRing_GetInvItem(const OBJECT_ID obj_id) { for (int32_t i = 0; i < g_InvRing_Items->count; i++) { INVENTORY_ITEM *const item = *(INVENTORY_ITEM **)Vector_Get(g_InvRing_Items, i); if (item->object_id == obj_id) { return item; } } return nullptr; } void InvRing_Draw(INV_RING *const ring) { InvRing_DrawUI(ring); const double interp_rate = Interpolation_GetRate(); const int16_t draw_radius = M_LerpI16(ring->prev_radius, ring->radius, interp_rate); const int16_t draw_camera_y = M_LerpI16(ring->prev_camera_y, ring->camera.pos.y, interp_rate); const int16_t draw_ring_rot_y = M_LerpAngleI16( ring->prev_ring_rot_y, ring->ring_pos.rot.y, interp_rate); const int16_t draw_camera_pitch = M_LerpAngleI16( ring->prev_camera_pitch, ring->camera_pitch, interp_rate); INV_RING draw_ring = *ring; draw_ring.radius = draw_radius; draw_ring.camera.pos.y = draw_camera_y; draw_ring.camera_pitch = draw_camera_pitch; draw_ring.ring_pos.rot.y = draw_ring_rot_y; draw_ring.camera.pos.z = draw_radius + M_CAMERA_2_RING; if (ring->mode == INV_TITLE_MODE) { if (ring->background_path != nullptr) { Output_Overlay_DrawImageBilinear(ring->background_path); } Interpolation_Interpolate(); } else { const float opacity = g_Config.ui.inventory_fade_effects ? Fader_GetCurrentValue(&ring->back_fader) : ring->back_fader.args.target; switch (ring->background_style) { case BK_NONE: if (ring->mode != INV_GLOBE_SELECT_MODE) { Output_Overlay_DrawGame(); } break; case BK_TRANSPARENT_MEDIUM: Output_Overlay_DrawGame(); Output_Overlay_DrawBlackRectangle(opacity * 0.5f, false); break; case BK_TRANSPARENT_DARK: Output_Overlay_DrawGame(); Output_Overlay_DrawBlackRectangle(opacity * 0.8f, false); break; case BK_BLACK: Output_Overlay_DrawGame(); Output_Overlay_DrawBlackRectangle(opacity * 1.0f, false); break; case BK_MONOCHROME: Output_Overlay_DrawGameMono(opacity); break; case BK_MONOCHROME_COOL: Output_Overlay_DrawGameMonoCool(opacity); break; case BK_MONOCHROME_WARM: Output_Overlay_DrawGameMonoWarm(opacity); break; case BK_PATTERN_STATIC: case BK_PATTERN_WAVE: if (opacity < 1.0f) { Output_Overlay_DrawGame(); } Output_Overlay_DrawPatternOpacity( ring->background_style == BK_PATTERN_WAVE, opacity); break; case BK_IMAGE: if (ring->background_path != nullptr && Output_Overlay_LoadImage(ring->background_path)) { Output_Overlay_DrawImageBilinear(ring->background_path); Output_Overlay_DrawBlackRectangle(1.0f - opacity, false); } else { Output_Overlay_DrawBlackRectangle(1.0f, false); } break; default: Output_Overlay_DrawGame(); break; } } Output_Flush(); const int16_t old_fov = Viewport_GetSystemFOV(); const FOV_MODE old_fov_mode = Viewport_GetFOVMode(); Viewport_AlterFOV(FOV_VALUE_PASSPORT * DEG_1, FOV_MODE_PASSPORT); Output_ApplyFOV(); XYZ_32 view_pos; XYZ_16 view_rot; InvRing_GetView(&draw_ring, &view_pos, &view_rot); Matrix_GenerateW2V(&view_pos, &view_rot); const int32_t old_fog_start = Output_GetFogStart(); const int32_t old_fog_end = Output_GetFogEnd(); Output_SetFogStart(20 * WALL_L); Output_SetFogEnd(100 * WALL_L); InvRing_Light(&draw_ring); Matrix_Push(); Matrix_TranslateAbs32(draw_ring.ring_pos.pos); Matrix_Rot16(draw_ring.ring_pos.rot); if (!(ring->mode == INV_TITLE_MODE && (Fader_IsActive(&ring->top_fader) || Fader_IsActive(&ring->back_fader)) && ring->status == RNG_OPENING)) { int16_t angle = 0; for (int32_t i = 0; i < draw_ring.number_of_objects; i++) { INVENTORY_ITEM *const inv_item = draw_ring.list[i]; Matrix_Push(); Matrix_RotY(angle); Matrix_TranslateRel(draw_ring.radius, 0, 0); Matrix_RotY(DEG_90); const int16_t draw_x_rot_pt = M_LerpAngleI16( inv_item->prev_x_rot_pt, inv_item->x_rot_pt, interp_rate); Matrix_RotX(draw_x_rot_pt); M_DrawItem(&draw_ring, inv_item, view_rot.y); angle += draw_ring.angle_adder; Matrix_Pop(); } } Matrix_Pop(); SceneCompositor_Flush(); Output_SetFogStart(old_fog_start); Output_SetFogEnd(old_fog_end); Viewport_AlterFOV(old_fov, old_fov_mode); if (ring->status == RNG_SELECTED) { INVENTORY_ITEM *const inv_item = ring->list[ring->current_object]; if (inv_item->object_id == O_PASSPORT_CLOSED) { inv_item->object_id = O_PASSPORT_OPTION; } Option_Draw(inv_item); } float top_opacity = Fader_GetCurrentValue(&ring->top_fader); if (ring->mode == INV_TITLE_MODE && ring->status != RNG_OPENING) { top_opacity = 0.0f; } Output_Overlay_DrawBlackRectangle(top_opacity, true); } ================================================ FILE: src/trx/game/inventory_ring/draw.h ================================================ #pragma once #include void InvRing_Draw(INV_RING *ring); const INVENTORY_ITEM *InvRing_GetInvItem(OBJECT_ID obj_id); ================================================ FILE: src/trx/game/inventory_ring/enum.h ================================================ #pragma once typedef enum { INV_GAME_MODE, INV_TITLE_MODE, INV_KEYS_MODE, INV_SAVE_MODE, INV_LOAD_MODE, INV_DEATH_MODE, INV_SAVE_CRYSTAL_MODE, INV_GLOBE_SELECT_MODE, } INVENTORY_MODE; typedef enum { RT_MAIN = 0, RT_OPTION = 1, RT_KEYS = 2, RT_GLOBE_SELECT = 3, RT_NUMBER_OF, } RING_TYPE; typedef enum { RNG_OPENING, RNG_OPEN, RNG_CLOSING, RNG_MAIN2OPTION, RNG_MAIN2KEYS, RNG_KEYS2MAIN, RNG_OPTION2MAIN, RNG_SELECTING, RNG_SELECTED, RNG_DESELECTING, RNG_DESELECT, RNG_CLOSING_ITEM, RNG_EXITING_INVENTORY, RNG_DONE, RNG_FADING_OUT, } RING_STATUS; ================================================ FILE: src/trx/game/inventory_ring/priv.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_RING_SWITCH_FRAMES (96 / 2) #define M_CAMERA_Y_OFFSET (-96) #define M_MANUAL_ROT_RESET_RATE 0.15 typedef enum { // clang-format off PASS_MESH_SPINE = 1 << 0, PASS_MESH_FRONT = 1 << 1, PASS_MESH_IN_FRONT = 1 << 2, PASS_MESH_PAGE_2 = 1 << 3, PASS_MESH_BACK = 1 << 4, PASS_MESH_IN_BACK = 1 << 5, PASS_MESH_PAGE_1 = 1 << 6, PASS_MESH_COMMON = PASS_MESH_SPINE | PASS_MESH_BACK | PASS_MESH_IN_BACK | PASS_MESH_FRONT, // clang-format on } PASS_MESH; static bool m_ShowExamine = false; static bool m_ShowUseItemButton = false; static void (*m_ButtonHintDrawFunc)(void *) = nullptr; static void *m_ButtonHintUserData = nullptr; static char *m_CountText = nullptr; static size_t m_CountTextCap = 0; static OBJECT_ID m_RequestedObjectID = NO_OBJECT; static void M_DrawExamineHint(void *const user_data) { UI_BeginStack(UI_STACK_HORIZONTAL); UI_ButtonLabel(INPUT_ROLE_LOOK, GS("general/actions/examine_item")); if (m_ShowUseItemButton) { UI_Spacer(60.0f, 0.0f); UI_ButtonLabel(INPUT_ROLE_ACTION, GS("general/actions/use_item")); } UI_EndStack(); } static void M_AdjustRot(int16_t *const rot, const int16_t dest_rot) { const int32_t delta = dest_rot - *rot; if (delta != 0) { if (delta > 0 && delta < DEG_180) { *rot += 1024; } else { *rot -= 1024; } *rot &= ~(1024 - 1); } } static XYZ_32 M_VectorViewFromWorld(const XYZ_32 v_world) { return Matrix_MulVec32_M(&g_ViewMatrix, v_world); } static void M_HandleRequestedObject(INV_RING *const ring) { if (m_RequestedObjectID == NO_OBJECT) { return; } for (int32_t i = 0; i < ring->number_of_objects; i++) { const OBJECT_ID object_id = ring->list[i]->object_id; if (object_id == m_RequestedObjectID && Inv_RequestItem(object_id) > 0) { ring->current_object = i; break; } } m_RequestedObjectID = NO_OBJECT; } static void M_MotionInit(INV_RING *const ring) { INV_RING_MOTION *const motion = &ring->motion; motion->radius_target = 0; motion->radius_rate = 0; motion->camera_y_target = 0; motion->camera_y_rate = 0; motion->camera_pitch_target = 0; motion->camera_pitch_rate = 0; motion->rotate_target = 0; motion->rotate_rate = 0; motion->item_pt_x_rot_target = 0; motion->item_pt_x_rot_rate = 0; motion->item_x_rot_target = 0; motion->item_x_rot_rate = 0; motion->item_y_trans_target = 0; motion->item_y_trans_rate = 0; motion->item_z_trans_target = 0; motion->item_z_trans_rate = 0; motion->misc = 0; } static void M_MotionCameraPos(INV_RING *const ring, const int16_t target) { INV_RING_MOTION *const motion = &ring->motion; motion->camera_y_target = target; motion->camera_y_rate = (target - ring->camera.pos.y) / ring->status_frames; } static void M_MotionCameraPitch(INV_RING *const ring, const int16_t target) { INV_RING_MOTION *const motion = &ring->motion; motion->camera_pitch_target = target; motion->camera_pitch_rate = target / ring->status_frames; motion->misc = target; } static void M_MotionRotation( INV_RING *const ring, const int16_t rotation, const int16_t target) { INV_RING_MOTION *const motion = &ring->motion; motion->rotate_target = target; motion->rotate_rate = rotation / ring->status_frames; } static void M_MotionRadius(INV_RING *const ring, const int16_t target) { INV_RING_MOTION *const motion = &ring->motion; motion->radius_target = target; motion->radius_rate = (target - ring->radius) / ring->status_frames; } static void M_MotionItemSelect( INV_RING *const ring, const INVENTORY_ITEM *const inv_item) { INV_RING_MOTION *const motion = &ring->motion; motion->item_pt_x_rot_target = inv_item->x_rot_pt_sel; motion->item_pt_x_rot_rate = inv_item->x_rot_pt_sel / ring->status_frames; motion->item_x_rot_target = inv_item->x_rot_sel; motion->item_x_rot_rate = (inv_item->x_rot_sel - inv_item->x_rot_nosel) / ring->status_frames; motion->item_y_trans_target = inv_item->y_trans_sel; motion->item_y_trans_rate = inv_item->y_trans_sel / ring->status_frames; motion->item_z_trans_target = inv_item->z_trans_sel; motion->item_z_trans_rate = inv_item->z_trans_sel / ring->status_frames; } static void M_MotionItemDeselect( INV_RING *const ring, const INVENTORY_ITEM *const inv_item) { INV_RING_MOTION *const motion = &ring->motion; motion->item_pt_x_rot_target = 0; motion->item_pt_x_rot_rate = -(inv_item->x_rot_pt_sel / ring->status_frames); motion->item_x_rot_target = inv_item->x_rot_nosel; motion->item_x_rot_rate = (inv_item->x_rot_nosel - inv_item->x_rot_sel) / ring->status_frames; motion->item_y_trans_target = 0; motion->item_y_trans_rate = -(inv_item->y_trans_sel / ring->status_frames); motion->item_z_trans_target = 0; motion->item_z_trans_rate = -(inv_item->z_trans_sel / ring->status_frames); } void InvRing_AdjustMusicVolume(const INV_RING *const ring) { if (ring->mode == INV_TITLE_MODE) { Music_SetVolume(g_Config.audio.music_volume); return; } const bool is_ambient = Music_GetCurrentPlayingTrack() == Music_GetCurrentLoopedTrack(); const double base_volume = is_ambient ? g_Config.audio.ambient_volume : g_Config.audio.music_volume; const double multiplier = is_ambient ? g_Config.audio.inventory_ambient_volume : g_Config.audio.inventory_music_volume; Music_SetVolume(base_volume * multiplier); if (ring->mode != INV_GLOBE_SELECT_MODE) { Sound_ResetAmbient(); Sound_UpdateEffects(); } } void InvRing_SetRequestedObjectID(const OBJECT_ID obj_id) { m_RequestedObjectID = obj_id; } void InvRing_InitRing( INV_RING *const ring, const RING_TYPE type, INVENTORY_ITEM **const list, const int16_t qty, const int16_t current) { ring->type = type; ring->list = list; ring->radius = 0; ring->prev_radius = 0; ring->number_of_objects = qty; ring->current_object = current; ring->angle_adder = DEG_360 / qty; ring->is_pass_open = false; ring->is_demo_needed = false; ring->has_spun_out = false; M_HandleRequestedObject(ring); if (ring->mode == INV_TITLE_MODE) { ring->camera_pitch = 1024; } else { ring->camera_pitch = 0; } ring->prev_camera_pitch = ring->camera_pitch; ring->rotating = false; ring->rotate_from_object = 0; ring->rotate_to_object = 0; ring->rot_count = 0; ring->target_object = 0; ring->rot_adder = 0; ring->rot_adder_l = 0; ring->rot_adder_r = 0; ring->camera.pos.x = 0; ring->camera.pos.y = INV_RING_CAMERA_START_HEIGHT; ring->camera.pos.z = 896; ring->camera.rot.x = 0; ring->camera.rot.y = 0; ring->camera.rot.z = 0; ring->status = RNG_OPENING; ring->status_target = RNG_OPEN; ring->status_frames = INV_RING_OPEN_FRAMES; M_MotionRadius(ring, INV_RING_RADIUS); M_MotionCameraPos(ring, INV_RING_CAMERA_HEIGHT); M_MotionRotation( ring, INV_RING_OPEN_ROTATION, -DEG_90 - ring->current_object * ring->angle_adder); ring->ring_pos.pos.x = 0; ring->ring_pos.pos.y = 0; ring->ring_pos.pos.z = 0; ring->ring_pos.rot.x = 0; ring->ring_pos.rot.y = ring->motion.rotate_target - INV_RING_OPEN_ROTATION; ring->prev_ring_rot_y = ring->ring_pos.rot.y; ring->ring_pos.rot.z = 0; ring->light.x = -1536; ring->light.y = 256; ring->light.z = 1024; ring->prev_camera_y = ring->camera.pos.y; m_ShowExamine = false; m_ShowUseItemButton = false; m_ButtonHintDrawFunc = nullptr; m_ButtonHintUserData = nullptr; } void InvRing_InitInvItem(INVENTORY_ITEM *const inv_item) { inv_item->meshes_drawn = inv_item->meshes_sel; inv_item->current_frame = 0; inv_item->goal_frame = 0; inv_item->manual_rot = g_IDMatrix; inv_item->x_rot_pt = 0; inv_item->prev_x_rot_pt = inv_item->x_rot_pt; inv_item->x_rot = inv_item->x_rot_nosel; inv_item->prev_x_rot = inv_item->x_rot; inv_item->y_rot = 0; inv_item->prev_y_rot = inv_item->y_rot; inv_item->y_trans = 0; inv_item->prev_y_trans = inv_item->y_trans; inv_item->z_trans = 0; inv_item->prev_z_trans = inv_item->z_trans; inv_item->action = ACTION_USE; inv_item->prev_manual_rot = inv_item->manual_rot; if (inv_item->object_id == O_PASSPORT_OPTION) { inv_item->object_id = O_PASSPORT_CLOSED; } } void InvRing_GetView( const INV_RING *const ring, XYZ_32 *const out_pos, XYZ_16 *const out_rot) { int16_t angles[2]; Math_GetVectorAngles( -ring->camera.pos.x, M_CAMERA_Y_OFFSET - ring->camera.pos.y, ring->radius - ring->camera.pos.z, angles); out_pos->x = ring->camera.pos.x; out_pos->y = ring->camera.pos.y; out_pos->z = ring->camera.pos.z; out_rot->x = angles[1] + ring->camera_pitch; out_rot->y = angles[0]; out_rot->z = 0; } void InvRing_Light(const INV_RING *const ring) { int16_t angles[2]; Math_GetVectorAngles(ring->light.x, ring->light.y, ring->light.z, angles); Output_SetLightDivider(0x6000); Output_RotateLight(angles[1], angles[0]); if (g_TRVersion >= 3) { // OG Inv_RingLight() LightCol columns are (sun, spot, dynamic): // sun = (3312, 1664, 0); // spot = (3312, 3312, 3312); // dynamic = (0, 0, 3072) with an ambient of (32, 32, 32). const float ambient_u8 = 32.0f / 255.0f; const RGB_F ambient = { ambient_u8, ambient_u8, ambient_u8 }; const RGB_F colors[3] = { { .r = 3312.0f / 4096.0f, .g = 1664.0f / 4096.0f, .b = 0.0f, }, { .r = 3312.0f / 4096.0f, .g = 3312.0f / 4096.0f, .b = 3312.0f / 4096.0f, }, { .r = 0.0f, .g = 0.0f, .b = 3072.0f / 4096.0f, }, }; const XYZ_32 dirs_view[3] = { M_VectorViewFromWorld((XYZ_32) { .x = 0x4000, .y = -0x4000, .z = 0x3000, }), M_VectorViewFromWorld((XYZ_32) { .x = -0x4000, .y = -0x4000, .z = 0x3000, }), M_VectorViewFromWorld( (XYZ_32) { .x = 0, .y = 0x2000, .z = 0x3000 }), }; Output_SetTR3Light(ambient, colors, dirs_view); } } void InvRing_CalcAdders(INV_RING *const ring, const int16_t rotation_duration) { ring->angle_adder = DEG_360 / ring->number_of_objects; ring->rot_adder_l = ring->angle_adder / rotation_duration; ring->rot_adder_r = -ring->angle_adder / rotation_duration; } void InvRing_DoMotions(INV_RING *const ring) { INV_RING_MOTION *const motion = &ring->motion; if (ring->status_frames != 0) { ring->radius += motion->radius_rate; ring->camera.pos.y += motion->camera_y_rate; ring->ring_pos.rot.y += motion->rotate_rate; ring->camera_pitch += motion->camera_pitch_rate; INVENTORY_ITEM *const inv_item = ring->list[ring->current_object]; inv_item->x_rot_pt += motion->item_pt_x_rot_rate; inv_item->x_rot += motion->item_x_rot_rate; inv_item->y_trans += motion->item_y_trans_rate; inv_item->z_trans += motion->item_z_trans_rate; ring->status_frames--; if (ring->status_frames == 0) { ring->status = ring->status_target; if (motion->radius_rate != 0) { motion->radius_rate = 0; ring->radius = motion->radius_target; } if (motion->camera_y_rate != 0) { motion->camera_y_rate = 0; ring->camera.pos.y = motion->camera_y_target; } if (motion->rotate_rate != 0) { motion->rotate_rate = 0; ring->ring_pos.rot.y = motion->rotate_target; } if (motion->item_pt_x_rot_rate != 0) { motion->item_pt_x_rot_rate = 0; inv_item->x_rot_pt = motion->item_pt_x_rot_target; } if (motion->item_x_rot_rate != 0) { motion->item_x_rot_rate = 0; inv_item->x_rot = motion->item_x_rot_target; } if (motion->item_y_trans_rate != 0) { motion->item_y_trans_rate = 0; inv_item->y_trans = motion->item_y_trans_target; } if (motion->item_z_trans_rate != 0) { motion->item_z_trans_rate = 0; inv_item->z_trans = motion->item_z_trans_target; } if (motion->camera_pitch_rate != 0) { motion->camera_pitch_rate = 0; ring->camera_pitch = motion->camera_pitch_target; } } } if (ring->rotating) { ring->ring_pos.rot.y += ring->rot_adder; ring->rot_count--; if (ring->rot_count == 0) { ring->current_object = ring->target_object; ring->ring_pos.rot.y = -DEG_90 - ring->target_object * ring->angle_adder; ring->rotating = false; } } } void InvRing_RotateLeft(INV_RING *const ring) { ring->rotating = true; ring->rotate_from_object = ring->current_object; if (ring->current_object <= 0) { ring->target_object = ring->number_of_objects - 1; } else { ring->target_object = ring->current_object - 1; } ring->rotate_to_object = ring->target_object; ring->rot_count = INV_RING_ROTATE_DURATION; ring->rot_adder = ring->rot_adder_l; } void InvRing_RotateRight(INV_RING *const ring) { ring->rotating = true; ring->rotate_from_object = ring->current_object; if (ring->current_object + 1 >= ring->number_of_objects) { ring->target_object = 0; } else { ring->target_object = ring->current_object + 1; } ring->rotate_to_object = ring->target_object; ring->rot_count = INV_RING_ROTATE_DURATION; ring->rot_adder = ring->rot_adder_r; } void InvRing_SetStatusTransition( INV_RING *const ring, const RING_STATUS status, const RING_STATUS status_target, const int16_t frames) { INV_RING_MOTION *const motion = &ring->motion; ring->status_frames = frames; ring->status = status; ring->status_target = status_target; motion->radius_rate = 0; motion->camera_y_rate = 0; const INVENTORY_ITEM *const inv_item = ring->list[ring->current_object]; switch (status) { case RNG_OPENING: M_MotionRadius(ring, INV_RING_RADIUS); ring->camera_pitch = -ring->motion.misc; ring->motion.camera_pitch_rate = ring->motion.misc / (M_RING_SWITCH_FRAMES / 2); ring->motion.camera_pitch_target = 0; InvRing_CalcAdders(ring, INV_RING_ROTATE_DURATION); M_MotionRotation( ring, INV_RING_OPEN_ROTATION, -DEG_90 - ring->angle_adder * ring->current_object); ring->ring_pos.rot.y = ring->motion.rotate_target + INV_RING_OPEN_ROTATION; break; case RNG_CLOSING: M_MotionRadius(ring, 0); switch (status_target) { case RNG_DONE: case RNG_FADING_OUT: M_MotionCameraPos(ring, INV_RING_CAMERA_START_HEIGHT); break; case RNG_MAIN2KEYS: case RNG_OPTION2MAIN: M_MotionCameraPitch(ring, DEG_45); break; case RNG_MAIN2OPTION: case RNG_KEYS2MAIN: M_MotionCameraPitch(ring, -DEG_45); break; default: break; } M_MotionRotation( ring, INV_RING_CLOSE_ROTATION, ring->ring_pos.rot.y - INV_RING_CLOSE_ROTATION); break; case RNG_SELECTING: M_MotionRotation( ring, 0, -DEG_90 - ring->angle_adder * ring->current_object); M_MotionItemSelect(ring, inv_item); break; case RNG_DESELECT: case RNG_EXITING_INVENTORY: M_MotionItemDeselect(ring, inv_item); break; case RNG_DESELECTING: M_MotionRotation( ring, 0, -DEG_90 - ring->angle_adder * ring->current_object); break; default: break; } } void InvRing_SelectMeshes(INVENTORY_ITEM *const inv_item) { switch (inv_item->object_id) { case O_PASSPORT_OPTION: { struct { int32_t frame; uint32_t meshes; } frame_map[] = { { 14, PASS_MESH_IN_FRONT | PASS_MESH_PAGE_1 }, { 18, PASS_MESH_IN_FRONT | PASS_MESH_PAGE_1 | PASS_MESH_PAGE_2 }, { 19, PASS_MESH_PAGE_1 | PASS_MESH_PAGE_2 }, { 23, PASS_MESH_PAGE_1 | PASS_MESH_PAGE_2 | PASS_MESH_IN_BACK }, { 28, PASS_MESH_PAGE_2 | PASS_MESH_IN_BACK }, { 29, 0 }, { -1, -1 }, // sentinel }; for (int32_t i = 0; frame_map[i].frame != -1; i++) { if (inv_item->current_frame <= frame_map[i].frame) { inv_item->meshes_drawn = PASS_MESH_COMMON | frame_map[i].meshes; break; } } break; } case O_COMPASS_OPTION: case O_STOPWATCH_OPTION: if (inv_item->current_frame == 0 || inv_item->current_frame >= 18) { inv_item->meshes_drawn = inv_item->meshes_sel; } else { inv_item->meshes_drawn = -1; } break; default: inv_item->meshes_drawn = -1; break; } } void InvRing_ShowItemName(const INVENTORY_ITEM *const inv_item) { if (inv_item->object_id == O_PASSPORT_OPTION || inv_item->object_id == O_GLOBE_SELECT_OPTION) { return; } Overlay_SetBottomText((OVERLAY_TEXT) { .kind = UI_OVERLAY_TEXT_OBJECT_NAME, .object_id = inv_item->object_id, .fmt_gs_key = GS_ID("general/inventory_ring/object_name_fmt"), }); } void InvRing_ShowItemQuantity(const char *const fmt, const int32_t qty) { const char *const full_fmt = String_FormatStatic(GS("general/inventory_ring/item_count_fmt"), fmt); String_FormatInto(&m_CountText, &m_CountTextCap, full_fmt, qty); } void InvRing_SetButtonHintDrawer(void (*draw_func)(void *), void *user_data) { m_ButtonHintDrawFunc = draw_func; m_ButtonHintUserData = user_data; } void InvRing_ClearButtonHint(void) { InvRing_SetButtonHintDrawer(nullptr, nullptr); } void InvRing_ShowExamine(const OBJECT_ID object_id, const bool show) { m_ShowExamine = show; m_ShowUseItemButton = show; if (show) { switch (object_id) { case O_QUEST_ITEM_1: case O_QUEST_ITEM_2: case O_QUEST_ITEM_3: case O_QUEST_ITEM_4: case O_QUEST_OPTION_1: case O_QUEST_OPTION_2: case O_QUEST_OPTION_3: case O_QUEST_OPTION_4: case O_PICKUP_OPTION_1: case O_PICKUP_OPTION_2: m_ShowUseItemButton = false; break; default: break; } InvRing_SetButtonHintDrawer(M_DrawExamineHint, nullptr); } else if (m_ButtonHintDrawFunc == M_DrawExamineHint) { InvRing_ClearButtonHint(); } } void InvRing_DrawUI(INV_RING *const ring) { UI_BeginModal(0.5f, 1.0f); UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, .align = { .h = UI_STACK_H_ALIGN_CENTER }, .spacing = { .v = 20.0f }, }); if (m_ButtonHintDrawFunc != nullptr) { m_ButtonHintDrawFunc(m_ButtonHintUserData); } if (m_CountText != nullptr && m_CountText[0] != '\0') { UI_BeginOffset(64.0f, 0.0f); UI_Label(m_CountText); UI_EndOffset(); } UI_Spacer(0.0f, 50.0f); UI_EndStack(); UI_EndModal(); } void InvRing_RemoveItemTexts(void) { Overlay_SetBottomText((OVERLAY_TEXT) { 0 }); if (m_CountText != nullptr) { strcpy(m_CountText, ""); } } void InvRing_ShowHeader(INV_RING *const ring) { if (ring->mode == INV_TITLE_MODE) { return; } switch (ring->type) { case RT_MAIN: Overlay_SetTopText((OVERLAY_TEXT) { .kind = UI_OVERLAY_TEXT_GS_KEY, .gs_key = GS_ID("general/inventory_ring/heading_inventory"), .fmt_gs_key = GS_ID("general/inventory_ring/heading_fmt"), }); break; case RT_OPTION: if (ring->mode == INV_DEATH_MODE) { Overlay_SetTopText((OVERLAY_TEXT) { .kind = UI_OVERLAY_TEXT_GS_KEY, .gs_key = GS_ID("general/inventory_ring/heading_game_over"), .fmt_gs_key = GS_ID("general/inventory_ring/heading_fmt"), }); } else { Overlay_SetTopText((OVERLAY_TEXT) { .kind = UI_OVERLAY_TEXT_GS_KEY, .gs_key = GS_ID("general/inventory_ring/heading_option"), .fmt_gs_key = GS_ID("general/inventory_ring/heading_fmt"), }); } break; case RT_KEYS: Overlay_SetTopText((OVERLAY_TEXT) { .kind = UI_OVERLAY_TEXT_GS_KEY, .gs_key = GS_ID("general/inventory_ring/heading_items"), .fmt_gs_key = GS_ID("general/inventory_ring/heading_fmt"), }); break; case RT_GLOBE_SELECT: break; case RT_NUMBER_OF: break; } if (ring->mode != INV_GAME_MODE) { return; } const bool show_up_arrow = ring->type == RT_OPTION || (ring->type == RT_MAIN && g_InvRing_Source[RT_KEYS].count > 0); const bool show_bottom_arrow = ring->type == RT_KEYS || (ring->type == RT_MAIN && !InvRing_IsOptionLockedOut()); Overlay_ShowArrow(UI_OVERLAY_ARROW_TL, show_up_arrow); Overlay_ShowArrow(UI_OVERLAY_ARROW_TR, show_up_arrow); Overlay_ShowArrow(UI_OVERLAY_ARROW_BL, show_bottom_arrow); Overlay_ShowArrow(UI_OVERLAY_ARROW_BR, show_bottom_arrow); } void InvRing_RemoveHeader(void) { Overlay_SetTopText((OVERLAY_TEXT) { 0 }); Overlay_ShowArrow(UI_OVERLAY_ARROW_TL, false); Overlay_ShowArrow(UI_OVERLAY_ARROW_TR, false); Overlay_ShowArrow(UI_OVERLAY_ARROW_BL, false); Overlay_ShowArrow(UI_OVERLAY_ARROW_BR, false); } bool InvRing_CanExamine(void) { return g_Config.gameplay.enable_item_examining && m_ShowExamine; } void InvRing_ShowVersionText(void) { Overlay_ShowVersion(true); } void InvRing_RemoveVersionText(void) { Overlay_ShowVersion(false); } void InvRing_UpdateInventoryItem( const INV_RING *const ring, INVENTORY_ITEM *const inv_item) { if (inv_item != ring->list[ring->current_object]) { if (inv_item->y_rot < 0) { inv_item->y_rot += 256; } else if (inv_item->y_rot > 0) { inv_item->y_rot -= 256; } } else if (ring->rotating) { if (inv_item->y_rot > 0) { inv_item->y_rot -= 512; } else if (inv_item->y_rot < 0) { inv_item->y_rot += 512; } } else if ( ring->status == RNG_SELECTED || ring->status == RNG_DESELECTING || ring->status == RNG_SELECTING || ring->status == RNG_DESELECT || ring->status == RNG_CLOSING_ITEM) { if (inv_item->has_manual_rot) { return; } M_AdjustRot(&inv_item->y_rot, inv_item->y_rot_sel); Matrix_Slerp3x3_M( &inv_item->manual_rot, &g_IDMatrix, M_MANUAL_ROT_RESET_RATE); } else if ( ring->number_of_objects == 1 || (!g_Input.menu_right && !g_Input.menu_left)) { inv_item->y_rot += 256; } } ================================================ FILE: src/trx/game/inventory_ring/priv.h ================================================ #pragma once #include #include #define INV_RING_FRAMES 2 #define INV_RING_CLOSE_FRAMES 32 #define INV_RING_CLOSE_ROTATION -DEG_180 #define INV_RING_OPEN_ROTATION -DEG_180 #define INV_RING_ROTATE_DURATION 24 #define INV_RING_OPEN_FRAMES 32 #define INV_RING_CAMERA_HEIGHT (-0x100) // = -256 #define INV_RING_CAMERA_START_HEIGHT (-0x600) // = -1536 #define INV_RING_RADIUS 688 typedef enum { INV_RING_ARROW_TL, INV_RING_ARROW_TR, INV_RING_ARROW_BL, INV_RING_ARROW_BR, } INV_RING_ARROW; void InvRing_InitRing( INV_RING *ring, RING_TYPE type, INVENTORY_ITEM **list, int16_t qty, int16_t current); void InvRing_InitInvItem(INVENTORY_ITEM *inv_item); void InvRing_GetView(const INV_RING *ring, XYZ_32 *out_pos, XYZ_16 *out_rot); void InvRing_Light(const INV_RING *ring); void InvRing_CalcAdders(INV_RING *ring, int16_t rotation_duration); void InvRing_DoMotions(INV_RING *ring); void InvRing_RotateLeft(INV_RING *ring); void InvRing_RotateRight(INV_RING *ring); void InvRing_SetStatusTransition( INV_RING *ring, RING_STATUS status, RING_STATUS status_target, int16_t frames); void InvRing_ShowItemName(const INVENTORY_ITEM *inv_item); void InvRing_ShowItemQuantity(const char *fmt, int32_t qty); void InvRing_RemoveItemTexts(void); void InvRing_SelectMeshes(INVENTORY_ITEM *inv_item); void InvRing_ShowHeader(INV_RING *ring); void InvRing_RemoveHeader(void); void InvRing_SetButtonHintDrawer(void (*draw_func)(void *), void *user_data); void InvRing_ClearButtonHint(void); void InvRing_ShowExamine(OBJECT_ID object_id, bool show); bool InvRing_CanExamine(void); void InvRing_ShowVersionText(void); void InvRing_RemoveVersionText(void); void InvRing_DrawUI(INV_RING *ring); void InvRing_UpdateInventoryItem( const INV_RING *ring, INVENTORY_ITEM *inv_item); bool InvRing_IsOptionLockedOut(void); ================================================ FILE: src/trx/game/inventory_ring/types.h ================================================ #pragma once #include #include #include #include #include #include #define MAX_QTY 999999 #define MAX_GLOBE_ZONES 6 typedef struct { int16_t shape; XYZ_16 pos; int32_t param1; int32_t param2; int16_t sprite_num; } INVENTORY_SPRITE; typedef enum { ACTION_USE = 0, ACTION_EXAMINE = 1, } INVENTORY_ITEM_ACTION; typedef struct { OBJECT_ID object_id; int16_t frames_total; int16_t current_frame; int16_t goal_frame; int16_t open_frame; int16_t anim_direction; int16_t anim_speed; int16_t anim_count; int16_t x_rot_pt_sel; int16_t x_rot_pt; int16_t x_rot_sel; int16_t x_rot_nosel; int16_t x_rot; int16_t y_rot_sel; int16_t y_rot; int16_t prev_y_rot; int32_t y_trans_sel; int32_t y_trans; int32_t prev_y_trans; int32_t z_trans_sel; int32_t z_trans; int32_t prev_z_trans; int16_t prev_x_rot_pt; int16_t prev_x_rot; MATRIX manual_rot; MATRIX prev_manual_rot; bool has_manual_rot; uint32_t meshes_sel; uint32_t meshes_drawn; int16_t inv_pos; INVENTORY_ITEM_ACTION action; } INVENTORY_ITEM; typedef struct { int16_t radius_target; int16_t radius_rate; int16_t camera_y_target; int16_t camera_y_rate; int16_t camera_pitch_target; int16_t camera_pitch_rate; int16_t rotate_target; int16_t rotate_rate; int16_t item_pt_x_rot_target; int16_t item_pt_x_rot_rate; int16_t item_x_rot_target; int16_t item_x_rot_rate; int32_t item_y_trans_target; int32_t item_y_trans_rate; int32_t item_z_trans_target; int32_t item_z_trans_rate; int32_t misc; } INV_RING_MOTION; typedef struct { int16_t current; int16_t count; int32_t qtys[24]; INVENTORY_ITEM *items[24]; } INV_RING_SOURCE; typedef struct { INVENTORY_MODE mode; INVENTORY_ITEM **list; RING_TYPE type; int16_t radius; int16_t prev_radius; int16_t camera_pitch; int16_t prev_camera_pitch; bool rotating; int16_t rotate_from_object; int16_t rotate_to_object; int16_t rot_count; int16_t current_object; int16_t target_object; int16_t number_of_objects; RING_STATUS status; RING_STATUS status_target; int16_t status_frames; int16_t angle_adder; int16_t rot_adder; int16_t rot_adder_l; int16_t rot_adder_r; struct { XYZ_32 pos; XYZ_16 rot; } ring_pos; int16_t prev_ring_rot_y; struct { XYZ_32 pos; XYZ_16 rot; } camera; int16_t prev_camera_y; XYZ_32 light; INV_RING_MOTION motion; bool is_demo_needed; bool is_pass_open; bool is_done; bool has_spun_out; int32_t old_fov; FADER top_fader; FADER back_fader; BACKGROUND_TYPE background_style; const char *background_path; struct { XYZ_16 rot; int32_t selection; uint32_t meshes_drawn; bool confirmed; bool selectable[MAX_GLOBE_ZONES]; int32_t start_level_num[MAX_GLOBE_ZONES]; } globe_select; } INV_RING; ================================================ FILE: src/trx/game/inventory_ring/vars.c ================================================ #include #include #include #include #include #include CAMERA_INFO g_InvRing_OldCamera = {}; VECTOR *g_InvRing_Items = nullptr; INV_RING_SOURCE g_InvRing_Source[RT_NUMBER_OF] = {}; __attribute__((constructor)) static void M_Init(void) { g_InvRing_Items = Vector_Create(sizeof(INVENTORY_ITEM *)); } __attribute__((destructor)) static void M_Shutdown(void) { for (int32_t i = 0; i < g_InvRing_Items->count; i++) { INVENTORY_ITEM *const item = *(INVENTORY_ITEM **)Vector_Get(g_InvRing_Items, i); Memory_Free(item); } Vector_Free(g_InvRing_Items); g_InvRing_Items = nullptr; } void InvRing_LoadVars(const char *const path) { #define L_READ_INT(key, target) target = JSON_ObjectGetInt(obj, key, target); for (int32_t i = 0; i < g_InvRing_Items->count; i++) { INVENTORY_ITEM *const item = *(INVENTORY_ITEM **)Vector_Get(g_InvRing_Items, i); Memory_Free(item); } Vector_Clear(g_InvRing_Items); for (int32_t i = 0; i < RT_NUMBER_OF; i++) { g_InvRing_Source[i].count = 0; } JSON_VALUE *const root = JSONFile_ReadEx(path, true); JSON_ARRAY *const arr = JSON_ValueAsArray(root); if (arr == nullptr) { Shell_ExitSystemFmt("invalid inventory ring vars file: %s", path); } ASSERT(g_InvRing_Items != nullptr); for (size_t i = 0; i < arr->length; i++) { JSON_OBJECT *const obj = JSON_ArrayGetObject(arr, i); const char *const name = JSON_ObjectGetString(obj, "object_id", JSON_INVALID_STRING); CATALOG_ID id; if (!Catalog_NameToEnum(CATALOG_OBJECTS, name, &id)) { Shell_ExitSystemFmt("unknown object_id '%s' in %s", name, path); } INVENTORY_ITEM *const item = Memory_Alloc(sizeof(*item)); item->object_id = id; L_READ_INT("frames_total", item->frames_total); L_READ_INT("current_frame", item->current_frame); L_READ_INT("goal_frame", item->goal_frame); L_READ_INT("open_frame", item->open_frame); L_READ_INT("anim_direction", item->anim_direction); L_READ_INT("anim_speed", item->anim_speed); L_READ_INT("anim_count", item->anim_count); L_READ_INT("x_rot_pt_sel", item->x_rot_pt_sel); L_READ_INT("x_rot_pt", item->x_rot_pt); L_READ_INT("x_rot_sel", item->x_rot_sel); L_READ_INT("x_rot_nosel", item->x_rot_nosel); L_READ_INT("x_rot", item->x_rot); L_READ_INT("y_rot_sel", item->y_rot_sel); L_READ_INT("y_rot", item->y_rot); L_READ_INT("y_trans_sel", item->y_trans_sel); L_READ_INT("y_trans", item->y_trans); L_READ_INT("z_trans_sel", item->z_trans_sel); L_READ_INT("z_trans", item->z_trans); L_READ_INT("meshes_sel", item->meshes_sel); L_READ_INT("meshes_drawn", item->meshes_drawn); L_READ_INT("inv_pos", item->inv_pos); Vector_Add(g_InvRing_Items, &item); } JSON_ValueFree(root); #undef L_READ_INT } ================================================ FILE: src/trx/game/inventory_ring/vars.h ================================================ #pragma once #include #include #include extern CAMERA_INFO g_InvRing_OldCamera; extern INV_RING_SOURCE g_InvRing_Source[RT_NUMBER_OF]; extern VECTOR *g_InvRing_Items; void InvRing_LoadVars(const char *path); ================================================ FILE: src/trx/game/inventory_ring.h ================================================ #pragma once #include #include #include #include #include ================================================ FILE: src/trx/game/items/actions/common.c ================================================ #include #include static void (*m_Routines[ITEM_ACTION_NUMBER_OF])(ITEM *item) = {}; static int16_t m_FXType = 0; int16_t ItemAction_GetFXType(void) { return m_FXType; } void ItemAction_Register( const ITEM_TRX_ACTION action, void (*const action_func)(ITEM *item)) { m_Routines[action] = action_func; } void ItemAction_Run(const ITEM_TRX_ACTION action_id, ITEM *const item) { if (action_id >= 0 && action_id < ITEM_ACTION_NUMBER_OF && m_Routines[action_id] != nullptr) { m_Routines[action_id](item); } } static void M_RunWithFX( const ITEM_TRX_ACTION action_id, ITEM *const item, const int16_t fx_type) { m_FXType = fx_type; ItemAction_Run(action_id, item); m_FXType = 0; } void ItemAction_RunDirect(const ITEM_ACTION action_id, ITEM *const item) { const ITEM_TRX_ACTION trx_id = ItemAction_FromGameID(action_id); ItemAction_Run(trx_id, item); } void ItemAction_RunDirectWithFX( const ITEM_ACTION action_id, ITEM *const item, const int16_t fx_type) { const ITEM_TRX_ACTION trx_id = ItemAction_FromGameID(action_id); M_RunWithFX(trx_id, item, fx_type); } void ItemAction_RunActive(void) { const int32_t flip_effect = Room_GetFlipEffect(); if (flip_effect != -1) { ItemAction_RunDirect(flip_effect, nullptr); } } ================================================ FILE: src/trx/game/items/actions/effects.c ================================================ #include #include #include #include #include #include #include static void M_ChainBlock(ITEM *const item) { const int32_t flip_timer = Room_GetFlipTimer(); if (g_Config.audio.fix_chainblock_secret_sound) { if (flip_timer == 0) { Sound_Effect(SFX_CHAINBLOCK_FX, nullptr, SPM_NORMAL); Room_SetFlipTimer(1); return; } } if (flip_timer == 0) { Sound_Effect(SFX_SECRET, nullptr, SPM_NORMAL); } if (flip_timer == 54) { Sound_Effect(SFX_LARA_SPLASH, nullptr, SPM_NORMAL); Room_SetFlipEffect(-1); } Room_IncrementFlipTimer(1); } static void M_Flood(ITEM *const item) { const int32_t flip_timer = Room_GetFlipTimer(); if (flip_timer > 4 * LOGIC_FPS) { Room_SetFlipEffect(-1); Room_IncrementFlipTimer(1); return; } const ITEM *const lara_item = Lara_GetItem(); XYZ_32 pos = { .x = lara_item->pos.x, .y = g_Camera.target.pos.y, .z = lara_item->pos.z, }; if (flip_timer >= LOGIC_FPS) { pos.y += 100 * (flip_timer - LOGIC_FPS); } else { pos.y += 100 * (LOGIC_FPS - flip_timer); } Sound_Effect(SFX_FLOOD, &pos, SPM_ALWAYS); Room_IncrementFlipTimer(1); } static void M_Explosion(ITEM *const item) { // TODO: unify Sound_Effect( g_TRVersion == 1 ? SFX_EXPLOSION_FX : SFX_EXPLOSION_1, nullptr, SPM_NORMAL); g_Camera.bounce = -75; Room_SetFlipEffect(-1); } static void M_Earthquake(ITEM *const item) { const int32_t flip_timer = Room_GetFlipTimer(); if (flip_timer == 0) { Sound_Effect(SFX_EXPLOSION_2, nullptr, SPM_NORMAL); g_Camera.bounce = -250; } else if (flip_timer == 3) { Sound_Effect(SFX_EARTHQUAKE_1, nullptr, SPM_NORMAL); } else if (flip_timer == 35) { Sound_Effect(SFX_EXPLOSION_2, nullptr, SPM_NORMAL); } else if (flip_timer == 20 || flip_timer == 50 || flip_timer == 70) { Sound_Effect(SFX_EARTHQUAKE_2, nullptr, SPM_NORMAL); } if (flip_timer == 104) { Room_SetFlipEffect(-1); } Room_IncrementFlipTimer(1); } static void M_Flicker(ITEM *const item) { const int32_t flip_timer = Room_GetFlipTimer(); if (flip_timer > 125) { Room_FlipMap(); Room_SetFlipEffect(-1); } else if ( flip_timer == 90 || flip_timer == 92 || flip_timer == 105 || flip_timer == 107) { Room_FlipMap(); } Room_IncrementFlipTimer(1); } static void M_FloorShake(ITEM *item) { const int32_t max_dist = WALL_L * 16; // = 0x4000 const int32_t max_bounce = 100; if (item == nullptr) { item = Lara_GetItem(); } const int32_t dx = item->pos.x - g_Camera.pos.x; const int32_t dy = item->pos.y - g_Camera.pos.y; const int32_t dz = item->pos.z - g_Camera.pos.z; const int32_t dist = SQUARE(dz) + SQUARE(dy) + SQUARE(dx); if (ABS(dx) < max_dist && ABS(dy) < max_dist && ABS(dz) < max_dist) { g_Camera.bounce = max_bounce * (SQUARE(WALL_L) - dist / 256) / SQUARE(WALL_L); } } static void M_RaisingBlock(ITEM *const item) { Sound_Effect(SFX_RAISINGBLOCK_FX, nullptr, SPM_NORMAL); Room_SetFlipEffect(-1); } static void M_PowerUp(ITEM *const item) { const int32_t flip_timer = Room_GetFlipTimer(); if (flip_timer > LOGIC_FPS * 4) { Room_SetFlipEffect(-1); } else { const XYZ_32 pos = { .x = g_Camera.target.x, .y = g_Camera.target.y + flip_timer * 100, .z = g_Camera.target.z, }; Sound_Effect(SFX_POWERUP_FX, &pos, SPM_NORMAL); } Room_IncrementFlipTimer(1); } static void M_Stairs2Slope(ITEM *const item) { const int32_t flip_timer = Room_GetFlipTimer(); if (flip_timer == 5) { Sound_Effect(SFX_STAIRS_2_SLOPE_FX, nullptr, SPM_NORMAL); Room_SetFlipEffect(-1); } Room_IncrementFlipTimer(1); } static void M_DropSand(ITEM *const item) { const int32_t flip_timer = Room_GetFlipTimer(); if (flip_timer > LOGIC_FPS * 4) { Room_SetFlipEffect(-1); } else { if (flip_timer == 0) { Sound_Effect(SFX_TRAPDOOR_OPEN, nullptr, SPM_NORMAL); } const XYZ_32 pos = { .x = g_Camera.target.x, .y = g_Camera.target.y + flip_timer * 100, .z = g_Camera.target.z, }; Sound_Effect(SFX_SAND_FX, &pos, SPM_NORMAL); } Room_IncrementFlipTimer(1); } static void M_Chandelier(ITEM *const item) { const int32_t flip_timer = Room_GetFlipTimer(); Sound_Effect(SFX_CHAIN_PULLEY, nullptr, SPM_NORMAL); if (flip_timer >= LOGIC_FPS) { Room_SetFlipEffect(-1); } Room_IncrementFlipTimer(1); } static void M_Rubble(ITEM *const item) { Sound_Effect(SFX_MASSIVE_CRASH, nullptr, SPM_NORMAL); g_Camera.bounce = -350; Room_SetFlipEffect(-1); } static void M_Piston(ITEM *const item) { Sound_Effect(SFX_PULLEY_CRANE, nullptr, SPM_NORMAL); Room_SetFlipEffect(-1); } static void M_Curtain(ITEM *const item) { Sound_Effect(SFX_CURTAIN, nullptr, SPM_NORMAL); Room_SetFlipEffect(-1); } static void M_SetChange(ITEM *const item) { Sound_Effect(SFX_STAGE_BACKDROP, nullptr, SPM_NORMAL); Room_SetFlipEffect(-1); } static void M_Statue(ITEM *const item) { Sound_Effect(SFX_STONE_DOOR_SLIDE, nullptr, SPM_NORMAL); Room_SetFlipEffect(-1); } static void M_Boiler(ITEM *const item) { Sound_Effect(SFX_BOILER, nullptr, SPM_NORMAL); Room_SetFlipEffect(-1); } static void M_CameraShake(ITEM *const item) { g_Camera.bounce = -350; Room_SetFlipEffect(-1); } static void M_LoweringBlock(ITEM *const item) { Sound_Effect(SFX_LOWERING_BLOCK, nullptr, SPM_NORMAL); Room_SetFlipEffect(-1); } REGISTER_ITEM_ACTION(ITEM_ACTION_CHAIN_BLOCK, M_ChainBlock) REGISTER_ITEM_ACTION(ITEM_ACTION_FLOOD, M_Flood) REGISTER_ITEM_ACTION(ITEM_ACTION_EXPLOSION, M_Explosion) REGISTER_ITEM_ACTION(ITEM_ACTION_EARTHQUAKE, M_Earthquake) REGISTER_ITEM_ACTION(ITEM_ACTION_FLICKER, M_Flicker) REGISTER_ITEM_ACTION(ITEM_ACTION_FLOOR_SHAKE, M_FloorShake) REGISTER_ITEM_ACTION(ITEM_ACTION_RAISING_BLOCK, M_RaisingBlock) REGISTER_ITEM_ACTION(ITEM_ACTION_POWER_UP, M_PowerUp) REGISTER_ITEM_ACTION(ITEM_ACTION_STAIRS_TO_SLOPE, M_Stairs2Slope) REGISTER_ITEM_ACTION(ITEM_ACTION_DROP_SAND, M_DropSand) REGISTER_ITEM_ACTION(ITEM_ACTION_CHANDELIER, M_Chandelier) REGISTER_ITEM_ACTION(ITEM_ACTION_RUBBLE, M_Rubble) REGISTER_ITEM_ACTION(ITEM_ACTION_PISTON, M_Piston) REGISTER_ITEM_ACTION(ITEM_ACTION_CURTAIN, M_Curtain) REGISTER_ITEM_ACTION(ITEM_ACTION_SET_CHANGE, M_SetChange) REGISTER_ITEM_ACTION(ITEM_ACTION_STATUE, M_Statue) REGISTER_ITEM_ACTION(ITEM_ACTION_BOILER, M_Boiler) REGISTER_ITEM_ACTION(ITEM_ACTION_CAMERA_SHAKE, M_CameraShake) REGISTER_ITEM_ACTION(ITEM_ACTION_LOWERING_BLOCK, M_LoweringBlock) ================================================ FILE: src/trx/game/items/actions/footprint.c ================================================ #include #include static void M_Footprint(ITEM *const item) { if (item == nullptr) { return; } const bool is_left = ItemAction_GetFXType() == 0x4000; FX_Footprint_Add(item, is_left); } REGISTER_ITEM_ACTION(ITEM_ACTION_FOOTPRINT, M_Footprint) ================================================ FILE: src/trx/game/items/actions/general.c ================================================ #include #include #include #include static void M_FinishLevel(ITEM *const item) { Game_SetIsLevelComplete(true); } static void M_FlipMap(ITEM *const item) { Room_FlipMap(); } static void M_AssaultStart(ITEM *const item) { Gym_TrackManager_Start(GYM_TRACK_ASSAULT); Room_SetFlipEffect(-1); } static void M_AssaultStop(ITEM *const item) { Gym_TrackManager_Stop(GYM_TRACK_ASSAULT); Room_SetFlipEffect(-1); } static void M_AssaultReset(ITEM *const item) { Gym_TrackManager_Reset(GYM_TRACK_ASSAULT); Room_SetFlipEffect(-1); } static void M_AssaultFinished(ITEM *const item) { Gym_TrackManager_Finish(GYM_TRACK_ASSAULT); Room_SetFlipEffect(-1); } static void M_AssaultPenalty8(ITEM *const item) { Gym_TrackManager_AddPenaltySeconds(GYM_TRACK_ASSAULT, 8); Room_SetFlipEffect(-1); } static void M_AssaultPenalty30(ITEM *const item) { Gym_TrackManager_AddPenaltySeconds(GYM_TRACK_ASSAULT, 30); Room_SetFlipEffect(-1); } static void M_RacetrackStart(ITEM *const item) { Gym_TrackManager_Start(GYM_TRACK_QUAD); Room_SetFlipEffect(-1); } static void M_RacetrackReset(ITEM *const item) { Gym_TrackManager_Stop(GYM_TRACK_QUAD); Room_SetFlipEffect(-1); } static void M_RacetrackFinished(ITEM *const item) { Gym_TrackManager_Finish(GYM_TRACK_QUAD); Room_SetFlipEffect(-1); } REGISTER_ITEM_ACTION(ITEM_ACTION_FINISH_LEVEL, M_FinishLevel) REGISTER_ITEM_ACTION(ITEM_ACTION_FLIP_MAP, M_FlipMap) REGISTER_ITEM_ACTION(ITEM_ACTION_ASSAULT_RESET, M_AssaultReset) REGISTER_ITEM_ACTION(ITEM_ACTION_ASSAULT_STOP, M_AssaultStop) REGISTER_ITEM_ACTION(ITEM_ACTION_ASSAULT_START, M_AssaultStart) REGISTER_ITEM_ACTION(ITEM_ACTION_ASSAULT_FINISHED, M_AssaultFinished) REGISTER_ITEM_ACTION(ITEM_ACTION_ASSAULT_PENALTY_8, M_AssaultPenalty8) REGISTER_ITEM_ACTION(ITEM_ACTION_ASSAULT_PENALTY_30, M_AssaultPenalty30) REGISTER_ITEM_ACTION(ITEM_ACTION_RACETRACK_START, M_RacetrackStart) REGISTER_ITEM_ACTION(ITEM_ACTION_RACETRACK_RESET, M_RacetrackReset) REGISTER_ITEM_ACTION(ITEM_ACTION_RACETRACK_FINISHED, M_RacetrackFinished) ================================================ FILE: src/trx/game/items/actions/gym_tr3.c ================================================ #include #include #include #include #include static int32_t m_ExerciseNumber = 0; static void M_PlayExerciseTrack( const int32_t expected_num, const MUSIC_TRX_ID track) { if (!Game_IsInGym()) { m_ExerciseNumber = 0; return; } if (m_ExerciseNumber == expected_num) { Music_Play(track, MPM_ONCE); m_ExerciseNumber++; } } static void M_Exercise01(ITEM *const item) { M_PlayExerciseTrack(0, MX_TR3_GYM_EXERCISE_01); Room_SetFlipEffect(-1); } static void M_Exercise02(ITEM *const item) { M_PlayExerciseTrack(1, MX_TR3_GYM_EXERCISE_02); Room_SetFlipEffect(-1); } static void M_Exercise03(ITEM *const item) { M_PlayExerciseTrack(2, MX_TR3_GYM_EXERCISE_03); Room_SetFlipEffect(-1); } static void M_Exercise04(ITEM *const item) { M_PlayExerciseTrack(3, MX_TR3_GYM_EXERCISE_04); Room_SetFlipEffect(-1); } static void M_Exercise05(ITEM *const item) { M_PlayExerciseTrack(4, MX_TR3_GYM_EXERCISE_05); Room_SetFlipEffect(-1); } static void M_Exercise06(ITEM *const item) { M_PlayExerciseTrack(5, MX_TR3_GYM_EXERCISE_06); Room_SetFlipEffect(-1); } static void M_Exercise07(ITEM *const item) { M_PlayExerciseTrack(6, MX_TR3_GYM_EXERCISE_07); Room_SetFlipEffect(-1); } static void M_Exercise08(ITEM *const item) { M_PlayExerciseTrack(7, MX_TR3_GYM_EXERCISE_08); Room_SetFlipEffect(-1); } static void M_Exercise09(ITEM *const item) { M_PlayExerciseTrack(8, MX_TR3_GYM_EXERCISE_09); Room_SetFlipEffect(-1); } static void M_Exercise10(ITEM *const item) { M_PlayExerciseTrack(9, MX_TR3_GYM_EXERCISE_10); Room_SetFlipEffect(-1); } static void M_Exercise11(ITEM *const item) { M_PlayExerciseTrack(10, MX_TR3_GYM_EXERCISE_11); Room_SetFlipEffect(-1); } static void M_Exercise12(ITEM *const item) { M_PlayExerciseTrack(11, MX_TR3_GYM_EXERCISE_12); Room_SetFlipEffect(-1); } static void M_Exercise13(ITEM *const item) { M_PlayExerciseTrack(12, MX_TR3_GYM_EXERCISE_13); Room_SetFlipEffect(-1); } static void M_Exercise14(ITEM *const item) { M_PlayExerciseTrack(13, MX_TR3_GYM_EXERCISE_14); Room_SetFlipEffect(-1); } static void M_Exercise15(ITEM *const item) { M_PlayExerciseTrack(14, MX_TR3_GYM_EXERCISE_15); Room_SetFlipEffect(-1); } static void M_Exercise16(ITEM *const item) { M_PlayExerciseTrack(15, MX_TR3_GYM_EXERCISE_16); Room_SetFlipEffect(-1); } static void M_Exercise17(ITEM *const item) { M_PlayExerciseTrack(16, MX_TR3_GYM_EXERCISE_17); Room_SetFlipEffect(-1); } static void M_Exercise18_SurfaceOnly(ITEM *const item) { if (!Game_IsInGym()) { m_ExerciseNumber = 0; Room_SetFlipEffect(-1); return; } if (m_ExerciseNumber == 17 && Lara_GetLaraInfo()->water_status == LWS_SURFACE) { Music_Play(MX_TR3_GYM_EXERCISE_18, MPM_ONCE); m_ExerciseNumber++; } Room_SetFlipEffect(-1); } static void M_Exercise19_Reset(ITEM *const item) { if (!Game_IsInGym()) { m_ExerciseNumber = 0; Room_SetFlipEffect(-1); return; } if (m_ExerciseNumber == 18) { Music_Play(MX_TR3_GYM_EXERCISE_19, MPM_ONCE); m_ExerciseNumber = 0; } Room_SetFlipEffect(-1); } static void M_ResetExercises(ITEM *const item) { m_ExerciseNumber = 0; Room_SetFlipEffect(-1); } REGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_1, M_Exercise01) REGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_2, M_Exercise02) REGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_3, M_Exercise03) REGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_4, M_Exercise04) REGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_5, M_Exercise05) REGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_6, M_Exercise06) REGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_7, M_Exercise07) REGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_8, M_Exercise08) REGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_9, M_Exercise09) REGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_10, M_Exercise10) REGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_11, M_Exercise11) REGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_12, M_Exercise12) REGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_13, M_Exercise13) REGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_14, M_Exercise14) REGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_15, M_Exercise15) REGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_16, M_Exercise16) REGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_17, M_Exercise17) REGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_18, M_Exercise18_SurfaceOnly) REGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_19, M_Exercise19_Reset) REGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_RESET, M_ResetExercises) ================================================ FILE: src/trx/game/items/actions/ids.c ================================================ #include #include ITEM_ACTION ItemAction_ToGameID(const ITEM_TRX_ACTION action) { ITEM_ACTION out; if (Catalog_EnumToGameID(CATALOG_ITEM_ACTIONS, action, &out)) { return out; } return ITEM_ACTION_INVALID; } ITEM_TRX_ACTION ItemAction_FromGameID(const ITEM_ACTION action) { ITEM_TRX_ACTION out; if (Catalog_GameIDToEnum(CATALOG_ITEM_ACTIONS, action, &out)) { return out; } return ITEM_TRX_ACTION_INVALID; } ================================================ FILE: src/trx/game/items/actions/ids.h ================================================ #pragma once #include typedef enum { ITEM_ACTION_INVALID = -1, } ITEM_ACTION; typedef enum { ITEM_TRX_ACTION_INVALID = -1, #define X_CATALOG_ID(enum_value) enum_value, #include #undef X_CATALOG_ID ITEM_ACTION_NUMBER_OF, } ITEM_TRX_ACTION; ITEM_ACTION ItemAction_ToGameID(ITEM_TRX_ACTION action); ITEM_TRX_ACTION ItemAction_FromGameID(ITEM_ACTION action); ================================================ FILE: src/trx/game/items/actions/items.c ================================================ #include static void M_Turn180(ITEM *const item) { if (item == nullptr) { return; } item->rot.x = -item->rot.x; item->rot.y += DEG_180; if (item == Lara_GetItem() && item->current_anim_state != LS(LS_ROLL_CONT)) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle += DEG_180; } } static void M_Turn90(ITEM *const item) { if (item == nullptr) { return; } item->rot.y += DEG_90; } static void M_InvisibilityOn(ITEM *const item) { if (item != nullptr) { item->status = IS_INVISIBLE; } } static void M_InvisibilityOff(ITEM *const item) { if (item != nullptr) { item->status = IS_ACTIVE; } } static void M_ShadowOn(ITEM *const item) { if (item != nullptr) { item->enable_shadow = true; } } static void M_ShadowOff(ITEM *const item) { if (item != nullptr) { item->enable_shadow = false; } } static void M_DynamicLightOn(ITEM *const item) { if (item != nullptr) { item->dynamic_light = true; } } static void M_DynamicLightOff(ITEM *const item) { if (item != nullptr) { item->dynamic_light = false; } } static void M_SwapMeshes(ITEM *const item, const OBJECT_ID swap_id) { if (item == nullptr) { return; } const OBJECT *const obj_1 = Object_Get(item->object_id); for (int32_t mesh_idx = 0; mesh_idx < obj_1->mesh_count; mesh_idx++) { Object_SwapMesh(item->object_id, swap_id, mesh_idx); } } static void M_SwapMeshesWithMeshSwap1(ITEM *const item) { if (item == nullptr) { return; } M_SwapMeshes(item, O_MESH_SWAP_1); } static void M_SwapMeshesWithMeshSwap2(ITEM *const item) { if (item == nullptr) { return; } M_SwapMeshes(item, O_MESH_SWAP_2); } static void M_SwapMeshesWithMeshSwap3(ITEM *const item) { if (item == nullptr) { return; } M_SwapMeshes(item, O_MESH_SWAP_3); } REGISTER_ITEM_ACTION(ITEM_ACTION_TURN_180, M_Turn180) REGISTER_ITEM_ACTION(ITEM_ACTION_TURN_90, M_Turn90) REGISTER_ITEM_ACTION(ITEM_ACTION_INVISIBILITY_ON, M_InvisibilityOn) REGISTER_ITEM_ACTION(ITEM_ACTION_INVISIBILITY_OFF, M_InvisibilityOff) REGISTER_ITEM_ACTION(ITEM_ACTION_SHADOW_ON, M_ShadowOn) REGISTER_ITEM_ACTION(ITEM_ACTION_SHADOW_OFF, M_ShadowOff) REGISTER_ITEM_ACTION(ITEM_ACTION_DYNAMIC_LIGHT_ON, M_DynamicLightOn) REGISTER_ITEM_ACTION(ITEM_ACTION_DYNAMIC_LIGHT_OFF, M_DynamicLightOff) REGISTER_ITEM_ACTION( ITEM_ACTION_SWAP_MESHES_WITH_MESH_SWAP_1, M_SwapMeshesWithMeshSwap1) REGISTER_ITEM_ACTION( ITEM_ACTION_SWAP_MESHES_WITH_MESH_SWAP_2, M_SwapMeshesWithMeshSwap2) REGISTER_ITEM_ACTION( ITEM_ACTION_SWAP_MESHES_WITH_MESH_SWAP_3, M_SwapMeshesWithMeshSwap3) ================================================ FILE: src/trx/game/items/actions/lara.c ================================================ #include #include #include #include #include #include #include #include #include static void M_Normal(ITEM *const item) { if (item == nullptr) { return; } LARA_INFO *const lara = Lara_GetLaraInfo(); lara->extra_anim = false; item->current_anim_state = LS(LS_STOP); item->goal_anim_state = LS(LS_STOP); Item_SwitchToAnim(item, LA(LA_STAND_STILL), 0); g_Camera.type = CAM_CHASE; Viewport_AlterFOV(-1, FOV_MODE_GAME); } static void M_HandsFree(ITEM *const item) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->gun_status = LGS_ARMLESS; } static void M_ToggleGun( ITEM *const item, const LARA_MESH thigh_mesh_idx, const LARA_MESH hand_mesh_idx) { if (item == nullptr) { return; } const bool armed = Lara_Skin_GetEquipment(hand_mesh_idx)->type == EQUIPMENT_TYPE_WEAPON; if (armed) { Lara_Skin_SetGunEquipment(thigh_mesh_idx, LGT_PISTOLS); Lara_Skin_SetGunEquipment(hand_mesh_idx, LGT_UNARMED); } else { Lara_Skin_SetGunEquipment(thigh_mesh_idx, LGT_UNARMED); Lara_Skin_SetGunEquipment(hand_mesh_idx, LGT_PISTOLS); } } static void M_ToggleRightGun(ITEM *const item) { M_ToggleGun(item, LM_THIGH_R, LM_HAND_R); } static void M_ToggleLeftGun(ITEM *const item) { M_ToggleGun(item, LM_THIGH_L, LM_HAND_L); } static void M_ShootRightGun(ITEM *const item) { Lara_GetLaraInfo()->right_arm.flash_gun = 3; if (g_TRVersion == 3) { Spawn_GunShell(LGT_PISTOLS, true); Gun_Smoke_OnFire(LGT_PISTOLS, true); } } static void M_ShootLeftGun(ITEM *const item) { Lara_GetLaraInfo()->left_arm.flash_gun = 3; if (g_TRVersion == 3) { Spawn_GunShell(LGT_PISTOLS, false); Gun_Smoke_OnFire(LGT_PISTOLS, false); } } static void M_ResetHair(ITEM *const item) { Lara_Hair_Initialise(); } static void M_Bubbles(ITEM *const item) { // XXX: until we get RoboLara, it makes sense for her to breathe underwater const LARA_INFO *const lara = Lara_GetLaraInfo(); const ITEM *const lara_item = Lara_GetItem(); if (lara->water_status == LWS_CHEAT && !Room_Get(lara_item->room_num)->flags.underwater) { return; } const int32_t count = g_TRVersion == 3 ? (Random_GetControl() & 3) + 2 : (Random_GetDraw() * 3) / 0x8000; if (count == 0) { return; } Sound_Effect(SFX_LARA_BUBBLES, &lara_item->pos, SPM_UNDERWATER); XYZ_32 offset = { .x = 0, .y = 0, .z = 50 }; Collide_GetJointAbsPosition(lara_item, &offset, LM_HEAD); for (int32_t i = 0; i < count; i++) { Spawn_Bubble(&offset, lara_item->room_num); } } REGISTER_ITEM_ACTION(ITEM_ACTION_LARA_NORMAL, M_Normal) REGISTER_ITEM_ACTION(ITEM_ACTION_LARA_HANDS_FREE, M_HandsFree) REGISTER_ITEM_ACTION(ITEM_ACTION_LARA_DRAW_RIGHT_GUN, M_ToggleRightGun) REGISTER_ITEM_ACTION(ITEM_ACTION_LARA_DRAW_LEFT_GUN, M_ToggleLeftGun) REGISTER_ITEM_ACTION(ITEM_ACTION_LARA_SHOOT_RIGHT_GUN, M_ShootRightGun) REGISTER_ITEM_ACTION(ITEM_ACTION_LARA_SHOOT_LEFT_GUN, M_ShootLeftGun) REGISTER_ITEM_ACTION(ITEM_ACTION_RESET_HAIR, M_ResetHair) REGISTER_ITEM_ACTION(ITEM_ACTION_BUBBLES, M_Bubbles) ================================================ FILE: src/trx/game/items/actions.h ================================================ #pragma once #include #include #include void ItemAction_Register( ITEM_TRX_ACTION action, void (*action_func)(ITEM *item)); void ItemAction_Run(ITEM_TRX_ACTION action_id, ITEM *item); void ItemAction_RunDirect(ITEM_ACTION action_id, ITEM *item); void ItemAction_RunDirectWithFX( ITEM_ACTION action_id, ITEM *item, int16_t fx_type); void ItemAction_RunActive(void); int16_t ItemAction_GetFXType(void); #define REGISTER_ITEM_ACTION(action, action_func) \ __attribute__((constructor)) static void M_RegisterActionHandler##action( \ void) \ { \ ItemAction_Register(action, action_func); \ } ================================================ FILE: src/trx/game/items/anim.c ================================================ #include #include #include #include #include #include #include #include #include #define M_SFX_SURF_DISTANCE ((STEP_L * 2) + 1) #define M_FRAME_INTERP_SCALE 1024 static bool M_ShouldPlaySFXAlways( const ITEM *const item, const bool item_underwater) { if (item == Lara_GetItem()) { return true; } if (item->object_id == O_LARA_HARPOON_GUN) { return true; } int16_t room_num = item->room_num; if (room_num == NO_ROOM) { return false; } const int32_t dist = item_underwater ? -M_SFX_SURF_DISTANCE : +M_SFX_SURF_DISTANCE; Room_GetSector( (XYZ_32) { item->pos.x, item->pos.y + dist, item->pos.z }, &room_num); const ROOM *const nearby_room = Room_Get(room_num); const bool near_underwater = nearby_room->flags.underwater; return item_underwater != near_underwater; } ANIM *Item_GetAnim(const ITEM *const item) { return Anim_GetAnim(item->anim_num); } bool Item_TestAnimEqual(const ITEM *const item, const int16_t anim_idx) { return Item_TestObjAnimEqual(item, anim_idx, item->object_id); } bool Item_TestObjAnimEqual( const ITEM *const item, const int16_t anim_idx, const OBJECT_ID obj_id) { const OBJECT *const obj = Object_Get(obj_id); return item->anim_num == obj->anim_idx + anim_idx; } int16_t Item_GetRelativeAnim(const ITEM *const item) { return Item_GetRelativeObjAnim(item, item->object_id); } int16_t Item_GetRelativeObjAnim(const ITEM *const item, const OBJECT_ID obj_id) { return item->anim_num - Object_Get(obj_id)->anim_idx; } int16_t Item_GetRelativeFrame(const ITEM *const item) { return item->frame_num - Item_GetAnim(item)->frame_base; } void Item_SwitchToAnim( ITEM *const item, const int16_t anim_idx, const int16_t frame) { Item_SwitchToObjAnim(item, anim_idx, frame, item->object_id); } void Item_SwitchToObjAnim( ITEM *const item, const int16_t anim_idx, const int16_t frame, const OBJECT_ID obj_id) { const OBJECT *const obj = Object_Get(obj_id); if (obj->anim_idx == NO_ANIM) { item->anim_num = NO_ANIM; } else { item->anim_num = obj->anim_idx + anim_idx; } const ANIM *const anim = Item_GetAnim(item); if (frame < 0) { item->frame_num = anim->frame_end + frame + 1; } else { item->frame_num = anim->frame_base + frame; } } bool Item_TestFrameEqual(const ITEM *const item, const int16_t frame) { const ANIM *const anim = Item_GetAnim(item); const int16_t base_frame = frame < 0 ? (anim->frame_end + 1) : anim->frame_base; return Anim_TestAbsFrameEqual(item->frame_num, base_frame + frame); } bool Item_TestFrameRange( const ITEM *const item, const int16_t start, const int16_t end) { return Anim_TestAbsFrameRange( item->frame_num, Item_GetAnim(item)->frame_base + start, Item_GetAnim(item)->frame_base + end); } ANIM_FRAME *Item_GetBestFrame(const ITEM *const item) { ANIM_FRAME *frames[2]; int32_t rate = 0; const int32_t frac = Item_GetFrames(item, frames, &rate); return frames[(frac > rate / 2) ? 1 : 0]; } int32_t Item_GetFrames(const ITEM *item, ANIM_FRAME *frames[], int32_t *rate) { const ANIM *const anim = Item_GetAnim(item); if (anim->frame_ptr == nullptr) { frames[0] = nullptr; return 0; } const int32_t cur_frame_num = item->frame_num - anim->frame_base; const int32_t last_frame_num = anim->frame_end - anim->frame_base; const int32_t key_frame_span = anim->interpolation; const int32_t first_key_frame_num = cur_frame_num / key_frame_span; const int32_t second_key_frame_num = first_key_frame_num + 1; int32_t interp_frame_num = cur_frame_num; double interp_frame_sub = 0.0; const double alpha = Interpolation_GetWorldRate(); if (alpha >= 0.0 && alpha <= 1.0) { const bool prev_in_anim = item->prev_frame_num >= anim->frame_base && item->prev_frame_num <= anim->frame_end; if (prev_in_anim) { const int32_t prev_frame_num = item->prev_frame_num - anim->frame_base; const int32_t frame_delta = cur_frame_num - prev_frame_num; if (frame_delta > 0) { const OBJECT *const obj = Object_Get(item->object_id); const bool allow_interp = obj->can_interpolate_func == nullptr || obj->can_interpolate_func( item, first_key_frame_num, second_key_frame_num); if (allow_interp) { const double frame_pos = prev_frame_num + (frame_delta * alpha); if (frame_pos < last_frame_num) { interp_frame_num = (int32_t)frame_pos; interp_frame_sub = frame_pos - interp_frame_num; } } } } } const int32_t key_frame_shift = interp_frame_num % key_frame_span; const int32_t frame_a = interp_frame_num / key_frame_span; const int32_t frame_b = frame_a + 1; frames[0] = &anim->frame_ptr[frame_a]; frames[1] = &anim->frame_ptr[frame_b]; int32_t denominator = key_frame_span; if (key_frame_shift != 0 || interp_frame_sub > 0.0) { const int32_t second_key_frame_num2 = (interp_frame_num / key_frame_span + 1) * key_frame_span; if (second_key_frame_num2 > anim->frame_end) { denominator += anim->frame_end - second_key_frame_num2; } } const double numerator = key_frame_shift + interp_frame_sub; *rate = denominator * M_FRAME_INTERP_SCALE; return (int32_t)((numerator * M_FRAME_INTERP_SCALE) + 0.5); } void Item_Animate(ITEM *const item) { item->hit_status = false; item->touch_bits = 0; item->prev_frame_num = item->frame_num; item->frame_num++; const ANIM *anim = Item_GetAnim(item); if (anim->num_changes > 0 && Item_GetAnimChange(item, anim)) { anim = Item_GetAnim(item); item->current_anim_state = anim->current_anim_state; if (item->required_anim_state == anim->current_anim_state) { item->required_anim_state = 0; } } if (item->frame_num > anim->frame_end) { for (int32_t i = 0; i < anim->num_commands; i++) { const ANIM_COMMAND *const command = &anim->commands[i]; switch (command->type) { case AC_MOVE_ORIGIN: { const XYZ_16 *const pos = (XYZ_16 *)command->data; Item_Translate(item, pos->x, pos->y, pos->z); break; } case AC_JUMP_VELOCITY: { const ANIM_COMMAND_VELOCITY_DATA *const data = (ANIM_COMMAND_VELOCITY_DATA *)command->data; item->fall_speed = data->fall_speed; item->speed = data->speed; item->gravity = true; break; } case AC_DEACTIVATE: const OBJECT *const obj = Object_Get(item->object_id); item->after_death = obj->intelligent ? 1 : 64; item->status = IS_DEACTIVATED; break; default: break; } } item->anim_num = anim->jump_anim_num; item->frame_num = anim->jump_frame_num; anim = Item_GetAnim(item); if (item->current_anim_state != anim->current_anim_state) { item->current_anim_state = anim->current_anim_state; item->goal_anim_state = anim->current_anim_state; } if (item->required_anim_state == item->current_anim_state) { item->required_anim_state = 0; } } for (int32_t i = 0; i < anim->num_commands; i++) { const ANIM_COMMAND *const command = &anim->commands[i]; switch (command->type) { case AC_SOUND_FX: { const ANIM_COMMAND_EFFECT_DATA *const data = (ANIM_COMMAND_EFFECT_DATA *)command->data; Item_PlayAnimSFX(item, data); break; } case AC_EFFECT: const ANIM_COMMAND_EFFECT_DATA *const data = (ANIM_COMMAND_EFFECT_DATA *)command->data; if (item->frame_num == data->frame_num) { ItemAction_RunDirectWithFX( data->effect_num, item, data->fx_type); } break; default: break; } } if (item->gravity) { item->fall_speed += item->fall_speed < FAST_FALL_SPEED ? GRAVITY : 1; item->pos.y += item->fall_speed; } else { int32_t speed = anim->velocity; if (anim->acceleration != 0) { speed += anim->acceleration * (item->frame_num - anim->frame_base); } item->speed = speed >> 16; } item->pos.x += (item->speed * Math_Sin(item->rot.y)) >> W2V_SHIFT; item->pos.z += (item->speed * Math_Cos(item->rot.y)) >> W2V_SHIFT; } bool Item_GetAnimChange(ITEM *const item, const ANIM *const anim) { if (item->current_anim_state == item->goal_anim_state) { return false; } for (int32_t i = 0; i < anim->num_changes; i++) { const ANIM_CHANGE *const change = Anim_GetChange(anim->change_idx + i); if (change->goal_anim_state != item->goal_anim_state) { continue; } for (int32_t j = 0; j < change->num_ranges; j++) { const ANIM_RANGE *const range = Anim_GetRange(change->range_idx + j); if (Anim_TestAbsFrameRange( item->frame_num, range->start_frame, range->end_frame)) { item->anim_num = range->link_anim_num; item->frame_num = range->link_frame_num; return true; } } } return false; } void Item_PlayAnimSFX( const ITEM *const item, const ANIM_COMMAND_EFFECT_DATA *const data) { if (item->frame_num != data->frame_num) { return; } const bool is_lara = item == Lara_GetItem(); const bool item_underwater = item->room_num != NO_ROOM && Room_Get(item->room_num)->flags.underwater; const ANIM_COMMAND_ENVIRONMENT mode = data->environment; if (mode != ACE_ALL && item->room_num != NO_ROOM) { int32_t height = NO_HEIGHT; if (is_lara) { height = Lara_GetLaraInfo()->water_surface_dist; } else if (item_underwater) { height = -STEP_L; } const bool in_water = height < 0 && height != NO_HEIGHT; if ((mode == ACE_WATER && !in_water) || (mode == ACE_LAND && in_water)) { return; } } const bool play_always = M_ShouldPlaySFXAlways(item, item_underwater); SOUND_PLAY_MODE play_mode = SPM_NORMAL; if (play_always) { play_mode = SPM_ALWAYS; } else if ( Object_IsType(item->object_id, g_WaterObjects) || (g_Config.audio.enable_underwater_anim_sfx && item_underwater)) { play_mode = SPM_UNDERWATER; } const SAMPLE_ID sfx_num = is_lara ? Lara_Skin_GetAnimSFX(data->effect_num) : data->effect_num; Sound_Effect_Direct(sfx_num, &item->pos, play_mode); } ================================================ FILE: src/trx/game/items/anim.h ================================================ #pragma once #include #include ANIM *Item_GetAnim(const ITEM *item); bool Item_TestAnimEqual(const ITEM *item, int16_t anim_idx); bool Item_TestObjAnimEqual( const ITEM *item, int16_t anim_idx, OBJECT_ID obj_id); int16_t Item_GetRelativeAnim(const ITEM *item); int16_t Item_GetRelativeObjAnim(const ITEM *item, OBJECT_ID obj_id); int16_t Item_GetRelativeFrame(const ITEM *item); void Item_SwitchToAnim(ITEM *item, int16_t anim_idx, int16_t frame); void Item_SwitchToObjAnim( ITEM *item, int16_t anim_idx, int16_t frame, OBJECT_ID obj_id); // Tests if the given item's current relative animation frame matches the // provided value. If a negative value is passed, the test is performed from the // end of the animation's frame set. bool Item_TestFrameEqual(const ITEM *item, int16_t frame); bool Item_TestFrameRange(const ITEM *item, int16_t start, int16_t end); ANIM_FRAME *Item_GetBestFrame(const ITEM *item); int32_t Item_GetFrames(const ITEM *item, ANIM_FRAME *frmptr[], int32_t *rate); void Item_Animate(ITEM *item); bool Item_GetAnimChange(ITEM *item, const ANIM *anim); void Item_PlayAnimSFX(const ITEM *item, const ANIM_COMMAND_EFFECT_DATA *data); ================================================ FILE: src/trx/game/items/carrier.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #define M_DROP_FAST_RATE GRAVITY #define M_DROP_SLOW_RATE 1 #define M_DROP_FAST_TURN (DEG_1 * 5) #define M_DROP_SLOW_TURN (DEG_1 * 3) static int16_t m_AnimatingCount = 0; static const GAME_OBJECT_PAIR m_LegacyMap[] = { { O_PIERRE, O_SCION_ITEM_2 }, { O_COWBOY, O_MAGNUM_ITEM }, { O_SKATEKID, O_UZI_ITEM }, { O_BALDY, O_SHOTGUN_ITEM }, { NO_OBJECT, NO_OBJECT }, }; static bool M_ShouldCenterDrop(const OBJECT_ID obj_id) { switch (obj_id) { case O_QUEST_ITEM_1: case O_QUEST_ITEM_2: case O_QUEST_ITEM_3: case O_QUEST_ITEM_4: return false; default: return g_TRVersion == 3; } } static OBJECT_ID M_ConvertDroppedGun(const OBJECT_ID obj_id) { if (g_GameFlow.convert_dropped_guns && Object_IsType(obj_id, g_GunObjects) && Inv_RequestItem(obj_id) && obj_id != O_PISTOL_ITEM) { return Object_GetCognate(obj_id, g_GunAmmoObjectMap); } return obj_id; } static ITEM *M_GetCarrier(const int16_t item_num) { if (item_num < 0 || item_num >= Item_GetLevelCount()) { return nullptr; } // Allow carried items to be allocated to holder objects (pods/statues), // but then have those items dropped by the actual creatures within. ITEM *item = Item_Get(item_num); const OBJECT *obj = Object_Get(item->object_id); if (obj->carrier_item_num_func != nullptr) { const int16_t child_item_num = obj->carrier_item_num_func(item); if (child_item_num == NO_ITEM) { return nullptr; } item = Item_Get(child_item_num); } obj = Object_Get(item->object_id); if (!obj->loaded) { return nullptr; } return item; } static ITEM *M_EnsureCarriedPickupItem( const ITEM *const carrier, CARRIED_ITEM *const carried_item) { if (carried_item->spawn_num == NO_ITEM) { return nullptr; } if (carried_item->spawn_num < Item_GetTotalCount()) { return Item_Get(carried_item->spawn_num); } // Gameflow drops can reference runtime-spawned pickup indices that do not // exist yet after a fresh level load. Re-spawn and rebind the index. const int16_t spawn_num = Item_Spawn(carrier, carried_item->object_id); if (spawn_num == NO_ITEM) { carried_item->spawn_num = NO_ITEM; return nullptr; } carried_item->spawn_num = spawn_num; return Item_Get(carried_item->spawn_num); } static bool M_IsCarrierType(const OBJECT_ID obj_id) { bool is_enemy = Object_IsType(obj_id, g_CreatureObjects); // Eels are hostile but cannot be killed, so must be excluded. Monks may be // allocated drop items whether or not they are hostile. Drop items must be // assigned to the skidoo and not the rider to avoid issues with /kill, and // O_DRAGON_BACK is the active dragon, but having this in g_CreatureObjects // also creates issues with /kill, hence a separate check is required here. is_enemy &= obj_id != O_EEL && obj_id != O_BIG_EEL; is_enemy &= obj_id != O_SKIDOO_DRIVER; is_enemy |= obj_id == O_DRAGON_BACK || obj_id == O_SKIDOO_ARMED; return is_enemy; } static CARRIED_ITEM *M_GetFirstDropItem(const ITEM *const carrier) { bool can_drop = carrier->hit_points <= 0; const OBJECT *const obj = Object_Get(carrier->object_id); if (obj->can_drop_items_func != nullptr) { can_drop = obj->can_drop_items_func(carrier); } return can_drop ? carrier->carried_item : nullptr; } static void M_AnimateDrop(CARRIED_ITEM *const item) { if (item->status != DS_FALLING) { return; } ITEM *const pickup = Item_Get(item->spawn_num); int16_t room_num = pickup->room_num; // For cases where a flyer has dropped an item exactly on a portal, we need // to ensure that the initial sector is in the room above, hence we test // slightly above the initial y position. const SECTOR *const sector = Room_GetSector( (XYZ_32) { pickup->pos.x, pickup->pos.y - 10, pickup->pos.z }, &room_num); const int32_t height = Room_GetHeight(sector, pickup->pos); const bool in_water = Room_Get(pickup->room_num)->flags.underwater; if (sector->portal_room.pit == NO_ROOM && pickup->pos.y >= height) { item->status = DS_DROPPED; pickup->status = IS_INACTIVE; pickup->pos.y = height; pickup->fall_speed = 0; m_AnimatingCount--; } else { pickup->status = IS_ACTIVE; pickup->fall_speed += (!in_water && pickup->fall_speed < FAST_FALL_SPEED) ? M_DROP_FAST_RATE : M_DROP_SLOW_RATE; pickup->pos.y += pickup->fall_speed; pickup->rot.y += in_water ? M_DROP_SLOW_TURN : M_DROP_FAST_TURN; if (sector->portal_room.pit != NO_ROOM && pickup->pos.y > sector->floor.height) { room_num = sector->portal_room.pit; } } Item_UpdateRoom(item->spawn_num, room_num); // Track animating status in the carrier for saving/loading. item->pos = pickup->pos; item->rot = pickup->rot; item->room_num = pickup->room_num; item->fall_speed = pickup->fall_speed; } static void M_InitialiseDataDrops(void) { VECTOR *const pickups = Vector_Create(sizeof(int16_t)); for (int32_t i = 0; i < Item_GetLevelCount(); i++) { ITEM *const carrier = M_GetCarrier(i); if (carrier == nullptr || !M_IsCarrierType(carrier->object_id)) { continue; } const ROOM *const room = Room_Get(carrier->room_num); int16_t pickup_num = room->item_num; while (pickup_num != NO_ITEM) { ITEM *const pickup = Item_Get(pickup_num); if (Object_IsType(pickup->object_id, g_PickupObjects) && XYZ_32_AreEquivalent(pickup->pos, carrier->pos)) { Vector_Add(pickups, (void *)&pickup_num); Item_RemoveDrawn(pickup_num); pickup->room_num = NO_ROOM; } pickup_num = pickup->next_item; } if (pickups->count == 0) { continue; } carrier->carried_item = GameBuf_Alloc(sizeof(CARRIED_ITEM) * pickups->count, GBUF_ITEMS); CARRIED_ITEM *drop = carrier->carried_item; for (int32_t j = 0; j < pickups->count; j++) { drop->spawn_num = *(const int16_t *)Vector_Get(pickups, j); Item_RemoveDrawn(drop->spawn_num); drop->room_num = NO_ROOM; drop->fall_speed = 0; drop->status = DS_CARRIED; if (j < pickups->count - 1) { drop->next_item = drop + 1; drop++; } else { drop->next_item = nullptr; } } Vector_Clear(pickups); } Vector_Free(pickups); } static void M_InitialiseGameFlowDrops(const GF_LEVEL *const level) { int32_t total_item_count = Item_GetLevelCount(); for (int32_t i = 0; i < level->item_drops.count; i++) { const GF_DROP_ITEM_DATA *const data = &level->item_drops.data[i]; ITEM *const item = M_GetCarrier(data->enemy_num); if (!item) { LOG_WARNING("%d does not refer to a loaded item", data->enemy_num); continue; } if (total_item_count + data->count > MAX_ITEMS) { LOG_WARNING("Too many items being loaded"); return; } if (item->carried_item) { LOG_WARNING("Item %d is already carrying", data->enemy_num); continue; } if (!M_IsCarrierType(item->object_id)) { LOG_WARNING( "Item %d of type %d cannot carry items", data->enemy_num, item->object_id); continue; } if (data->count == 0) { LOG_WARNING( "There are no drop items defined for enemy %d", data->enemy_num); continue; } item->carried_item = GameBuf_Alloc(sizeof(CARRIED_ITEM) * data->count, GBUF_ITEMS); CARRIED_ITEM *drop = item->carried_item; for (int32_t j = 0; j < data->count; j++) { drop->object_id = data->object_ids[j]; drop->spawn_num = NO_ITEM; drop->room_num = NO_ROOM; drop->fall_speed = 0; if (Object_IsType(drop->object_id, g_PickupObjects)) { drop->status = DS_CARRIED; total_item_count++; } else { LOG_WARNING( "Items of type %d cannot be carried", drop->object_id); drop->object_id = NO_OBJECT; drop->status = DS_COLLECTED; } if (j < data->count - 1) { drop->next_item = drop + 1; drop++; } else { drop->next_item = nullptr; } } } } void Carrier_InitialiseLevel(const GF_LEVEL *const level) { m_AnimatingCount = 0; if (g_GameFlow.enable_tr2_item_drops) { M_InitialiseDataDrops(); } else { M_InitialiseGameFlowDrops(level); } } int32_t Carrier_GetItemCount(const int16_t item_num) { const ITEM *const carrier = M_GetCarrier(item_num); if (carrier == nullptr) { return 0; } const CARRIED_ITEM *item = carrier->carried_item; int32_t count = 0; while (item != nullptr) { if (item->object_id != NO_OBJECT) { count++; } item = item->next_item; } return count; } bool Carrier_IsItemCarried(const int16_t item_num) { // This only applies to TR2-style drops; gameflow drop item numbers are not // assigned until they are dropped, so this would always logically be false. const ITEM *const item = Item_Get(item_num); return item->room_num == NO_ROOM; } DROP_STATUS Carrier_GetSaveStatus(const CARRIED_ITEM *item) { if (item->status == DS_DROPPED) { const ITEM *const pickup = Item_Get(item->spawn_num); return pickup->status == IS_INVISIBLE ? DS_COLLECTED : DS_DROPPED; } return item->status; } void Carrier_SyncItem( const int16_t carrier_item_num, CARRIED_ITEM *const carried_item) { const ITEM *const carrier = Item_Get(carrier_item_num); ITEM *const pickup_item = M_EnsureCarriedPickupItem(carrier, carried_item); if (pickup_item == nullptr) { return; } switch (carried_item->status) { case DS_CARRIED: if (pickup_item->room_num != NO_ROOM) { Item_UpdateRoom(carried_item->spawn_num, NO_ROOM); } break; case DS_FALLING: case DS_DROPPED: pickup_item->pos = carried_item->pos; pickup_item->rot.y = carried_item->rot.y; pickup_item->fall_speed = carried_item->fall_speed; if (carried_item->status == DS_DROPPED) { pickup_item->status = IS_INACTIVE; } else { m_AnimatingCount++; } pickup_item->object_id = M_ConvertDroppedGun(pickup_item->object_id); Item_UpdateRoom(carried_item->spawn_num, carried_item->room_num); break; case DS_COLLECTED: if (pickup_item->room_num != NO_ROOM) { Item_UpdateRoom(carried_item->spawn_num, NO_ROOM); } pickup_item->status = IS_INVISIBLE; break; } } void Carrier_TestItemDrops(const int16_t item_num) { const ITEM *const carrier = Item_Get(item_num); CARRIED_ITEM *item = M_GetFirstDropItem(carrier); if (item == nullptr) { return; } // The enemy is killed (plus is not runaway) and is carrying at // least one item. Ensure that each item has not already spawned, // convert guns to ammo if applicable, and spawn the items. do { if (item->status != DS_CARRIED) { continue; } if (item->spawn_num == NO_ITEM) { // This is a gameflow-defined drop, so a spawn number is required. const OBJECT_ID obj_id = M_ConvertDroppedGun(item->object_id); item->spawn_num = Item_Spawn(carrier, obj_id); } else { // TR2-style item drops will already have a spawn number. Item_UpdateRoom(item->spawn_num, carrier->room_num); ITEM *const pickup = Item_Get(item->spawn_num); pickup->pos = carrier->pos; pickup->rot = carrier->rot; pickup->status = IS_INACTIVE; } ITEM *const pickup = Item_Get(item->spawn_num); if (M_ShouldCenterDrop(pickup->object_id)) { int16_t room_num = carrier->room_num; pickup->pos.x = ROUND_TO_SECTOR(carrier->pos.x) + WALL_L / 2; pickup->pos.z = ROUND_TO_SECTOR(carrier->pos.z) + WALL_L / 2; const SECTOR *const sector = Room_GetSector(pickup->pos, &room_num); pickup->pos.y = Room_GetHeight( sector, (XYZ_32) { pickup->pos.x, carrier->pos.y, pickup->pos.z }); } item->status = DS_FALLING; m_AnimatingCount++; if (item->room_num != NO_ROOM) { // Handle reloading a save with a falling or landed item. pickup->pos = item->pos; pickup->fall_speed = item->fall_speed; Item_UpdateRoom(item->spawn_num, item->room_num); } } while ((item = item->next_item) != nullptr); } void Carrier_AnimateDrops(void) { if (m_AnimatingCount == 0) { return; } // Make items that spawn in mid-air or water gracefully fall to the floor. for (int32_t i = 0; i < Item_GetLevelCount(); i++) { const ITEM *const carrier = Item_Get(i); CARRIED_ITEM *item = carrier->carried_item; while (item != nullptr) { M_AnimateDrop(item); item = item->next_item; } } } ================================================ FILE: src/trx/game/items/carrier.h ================================================ #pragma once #include #include void Carrier_InitialiseLevel(const GF_LEVEL *level); int32_t Carrier_GetItemCount(int16_t item_num); bool Carrier_IsItemCarried(int16_t item_num); void Carrier_TestItemDrops(int16_t item_num); void Carrier_SyncItem(int16_t item_num, CARRIED_ITEM *carried_item); DROP_STATUS Carrier_GetSaveStatus(const CARRIED_ITEM *item); void Carrier_AnimateDrops(void); ================================================ FILE: src/trx/game/items/col.c ================================================ #include #include #include #include static BOUNDS_16 m_NullBounds = {}; static BOUNDS_16 m_InterpolatedBounds = {}; int16_t Item_GetHeight(const ITEM *const item) { int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); const int32_t height = Room_GetHeight(sector, item->pos); return height; } int32_t Item_GetDistance(const ITEM *const item, const XYZ_32 target) { return XYZ_32_GetDistance(item->pos, target); } void Item_Translate( ITEM *const item, const int32_t x, const int32_t y, const int32_t z) { const int32_t c = Math_Cos(item->rot.y); const int32_t s = Math_Sin(item->rot.y); item->pos.x += ((c * x + s * z) >> W2V_SHIFT); item->pos.y += y; item->pos.z += ((c * z - s * x) >> W2V_SHIFT); } bool Item_Test3DRange( const int32_t x, const int32_t y, const int32_t z, const int32_t range) { return ABS(x) < range && ABS(y) < range && ABS(z) < range && (SQUARE(x) + SQUARE(y) + SQUARE(z) < SQUARE(range)); } bool Item_IsNearby( const ITEM *const item_1, const ITEM *const item_2, const int32_t distance) { return XYZ_32_IsNearby(item_1->pos, item_2->pos, distance); } bool Item_TestBoundsCollide( const ITEM *const src_item, const ITEM *const dst_item, const int32_t radius) { const COLL_ITEM src = { .bounds = Item_GetBestFrame(src_item)->bounds, .pos = src_item->pos, .rot = src_item->rot, }; const COLL_ITEM dst = { .bounds = Item_GetBestFrame(dst_item)->bounds, .pos = dst_item->pos, .rot = dst_item->rot, }; return Collide_TestBoundsCollide(&src, &dst, radius); } bool Item_TestStatic3DBoundsCollide( const STATIC_MESH *const mesh, const ITEM *const dst_item, const int32_t radius) { const COLL_ITEM src = { .bounds = Object_Get3DStatic(mesh->static_num)->collision_bounds, .pos = mesh->pos, .rot = { .y = mesh->rot.y }, }; const COLL_ITEM dst = { .bounds = Item_GetBestFrame(dst_item)->bounds, .pos = dst_item->pos, .rot = dst_item->rot, }; return Collide_TestBoundsCollide(&src, &dst, radius); } const BOUNDS_16 *Item_GetBoundsAccurate(const ITEM *const item) { int32_t rate; ANIM_FRAME *frames[2]; const int32_t frac = Item_GetFrames(item, frames, &rate); if (frames[0] == nullptr) { return &m_NullBounds; } if (frac == 0) { return &frames[0]->bounds; } #define CALC(target, b1, b2, prop) \ target->prop = (b1)->prop + ((((b2)->prop - (b1)->prop) * frac) / rate); BOUNDS_16 *const result = &m_InterpolatedBounds; CALC(result, &frames[0]->bounds, &frames[1]->bounds, min.x); CALC(result, &frames[0]->bounds, &frames[1]->bounds, max.x); CALC(result, &frames[0]->bounds, &frames[1]->bounds, min.y); CALC(result, &frames[0]->bounds, &frames[1]->bounds, max.y); CALC(result, &frames[0]->bounds, &frames[1]->bounds, min.z); CALC(result, &frames[0]->bounds, &frames[1]->bounds, max.z); #undef CALC return result; } BOUNDS_16 Item_RotateBounds(const ITEM *const item, const int16_t rot_y) { const BOUNDS_16 *bounds = &Item_GetBestFrame(item)->bounds; BOUNDS_16 rot_bounds = {}; if (bounds == nullptr) { return rot_bounds; } switch (rot_y) { case 0: default: rot_bounds = *bounds; break; case DEG_90: rot_bounds.min.x = bounds->min.z; rot_bounds.max.x = bounds->max.z; rot_bounds.min.z = -bounds->max.x; rot_bounds.max.z = -bounds->min.x; break; case -DEG_180: rot_bounds.min.x = -bounds->max.x; rot_bounds.max.x = -bounds->min.x; rot_bounds.min.z = -bounds->max.z; rot_bounds.max.z = -bounds->min.z; break; case -DEG_90: rot_bounds.min.x = -bounds->max.z; rot_bounds.max.x = -bounds->min.z; rot_bounds.min.z = bounds->min.x; rot_bounds.max.z = bounds->max.x; break; } return rot_bounds; } ================================================ FILE: src/trx/game/items/col.h ================================================ #pragma once #include #include int16_t Item_GetHeight(const ITEM *item); int32_t Item_GetDistance(const ITEM *item, XYZ_32 target); void Item_Translate(ITEM *item, int32_t x, int32_t y, int32_t z); bool Item_Test3DRange(int32_t x, int32_t y, int32_t z, int32_t range); bool Item_IsNearby(const ITEM *item_1, const ITEM *item_2, int32_t distance); bool Item_TestBoundsCollide( const ITEM *src_item, const ITEM *dst_item, int32_t radius); bool Item_TestStatic3DBoundsCollide( const STATIC_MESH *mesh, const ITEM *dst_item, int32_t radius); const BOUNDS_16 *Item_GetBoundsAccurate(const ITEM *item); BOUNDS_16 Item_RotateBounds(const ITEM *item, int16_t rot_y); ================================================ FILE: src/trx/game/items/const.h ================================================ #pragma once #define NO_ITEM (-1) #define MAX_ITEMS 10240 ================================================ FILE: src/trx/game/items/draw.c ================================================ #include #include #include #include #include void Item_ControlDraw(ITEM *const item) { if (g_TRVersion == 3 && item->status != IS_INVISIBLE && item->after_death < 32 && item->after_death > 0) { item->after_death++; if (!Item_ShouldSpawnBlood(item)) { return; } if (item->after_death == 2 || item->after_death == 5 || item->after_death == 11 || item->after_death == 20 || item->after_death == 27 || item->after_death == 32 || (Random_GetDraw() & 7) == 0) { Spawn_BloodBathD( item->pos.x, item->pos.y - 64, item->pos.z, 0, Random_GetDraw() << 1, item->room_num, 1); } } } ================================================ FILE: src/trx/game/items/draw.h ================================================ #pragma once #include void Item_ControlDraw(ITEM *item); ================================================ FILE: src/trx/game/items/enum.h ================================================ #pragma once typedef enum { // clang-format off DS_CARRIED = 0, DS_FALLING = 1, DS_DROPPED = 2, DS_COLLECTED = 3, // clang-format on } DROP_STATUS; typedef enum { // clang-format off IF_ONE_SHOT_SWITCH = 0x0040, IF_ONE_SHOT_ANTITRIGGER = 0x0080, IF_ONE_SHOT = 0x0100, IF_CODE_BITS = 0x3E00, IF_REVERSE = 0x4000, IF_INVISIBLE = 0x0100, IF_KILLED = 0x8000, // clang-format on } ITEM_FLAG; typedef enum { // clang-format off AI_GUARD = 1 << 0, AI_AMBUSH = 1 << 1, AI_PATROL_1 = 1 << 2, AI_MODIFY = 1 << 3, AI_FOLLOW = 1 << 4, // clang-format on } AI_BITS; typedef enum { // clang-format off IS_INACTIVE = 0, IS_ACTIVE = 1, IS_DEACTIVATED = 2, IS_INVISIBLE = 3, // clang-format on } ITEM_STATUS; ================================================ FILE: src/trx/game/items/manager.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static int32_t m_LevelItemCount = 0; static int16_t m_MaxUsedItemCount = 0; static ITEM *m_Items = nullptr; static int16_t m_NextItemActive = NO_ITEM; static int16_t m_NextItemFree = NO_ITEM; static inline bool M_ItemBoundsIntersectsPortal( const ITEM *item, const ROOM *room, const PORTAL *const portal) { // Axis-aligned bound intersection; ignores item rotation. const BOUNDS_16 *const frame_bounds = &Item_GetBestFrame(item)->bounds; if (frame_bounds == nullptr) { return false; } const BOUNDS_32 bounds = { .min = { item->pos.x + frame_bounds->min.x, item->pos.y + frame_bounds->min.y, item->pos.z + frame_bounds->min.z, }, .max = { item->pos.x + frame_bounds->max.x, item->pos.y + frame_bounds->max.y, item->pos.z + frame_bounds->max.z, }, }; if (Bounds32_Intersect(&bounds, &portal->bounds)) { return true; } const ROOM *const own_room = Room_Get(item->room_num); if (own_room == nullptr) { return false; } const BOUNDS_32 room_bounds = Room_GetRoomBounds(own_room); return !Bounds32_Intersect(&bounds, &room_bounds); } void Item_InitialiseItems(const int32_t num_items) { m_Items = GameBuf_Alloc(sizeof(ITEM) * MAX_ITEMS, GBUF_ITEMS); m_LevelItemCount = num_items; m_MaxUsedItemCount = num_items; m_NextItemFree = num_items; m_NextItemActive = NO_ITEM; for (int32_t i = m_NextItemFree; i < MAX_ITEMS - 1; i++) { ITEM *const item = &m_Items[i]; item->active = false; item->next_item = i + 1; } m_Items[MAX_ITEMS - 1].next_item = NO_ITEM; } ITEM *Item_Get(const int16_t item_num) { if (item_num == NO_ITEM) { return nullptr; } return &m_Items[item_num]; } int16_t Item_GetIndex(const ITEM *const item) { return item - Item_Get(0); } ITEM *Item_Find(const OBJECT_ID obj_id) { for (int32_t item_num = 0; item_num < Item_GetTotalCount(); item_num++) { ITEM *const item = Item_Get(item_num); if (item->object_id == obj_id) { return item; } } return nullptr; } bool Item_SetName(const int16_t item_num, const char *const name) { ITEM *const item = Item_Get(item_num); if (item == nullptr) { return false; } if (name != nullptr) { ITEM *const existing = Item_GetByName(name); if (existing != nullptr && existing != item) { return false; } } if (name != nullptr) { item->name = GameBuf_Alloc(strlen(name) + 1, GBUF_ITEMS); strcpy(item->name, name); } else { item->name = nullptr; } return true; } ITEM *Item_GetByName(const char *const name) { if (name == nullptr) { return nullptr; } // search through all items for matching name for (int32_t i = 0; i < Item_GetTotalCount(); i++) { ITEM *const item = Item_Get(i); if (item->name != nullptr && strcmp(item->name, name) == 0) { return item; } } return nullptr; } int32_t Item_GetLevelCount(void) { return m_LevelItemCount; } int32_t Item_GetTotalCount(void) { return m_MaxUsedItemCount; } int16_t Item_GetNextActive(void) { return m_NextItemActive; } int16_t Item_Create(void) { const int16_t item_num = m_NextItemFree; if (item_num != NO_ITEM) { m_Items[item_num].flags = 0; m_NextItemFree = m_Items[item_num].next_item; } m_MaxUsedItemCount = MAX(m_MaxUsedItemCount, item_num + 1); return item_num; } int16_t Item_CreateLevelItem(void) { const int16_t item_num = Item_Create(); if (item_num != NO_ITEM) { m_LevelItemCount++; } return item_num; } int16_t Item_Spawn(const ITEM *const item, const OBJECT_ID obj_id) { const int16_t spawn_num = Item_Create(); if (spawn_num != NO_ITEM) { ITEM *const spawn = Item_Get(spawn_num); spawn->object_id = obj_id; spawn->room_num = item->room_num; spawn->pos = item->pos; spawn->rot = item->rot; Item_Initialise(spawn_num); spawn->status = IS_INACTIVE; spawn->shade.value_1 = SHADE_NEUTRAL; } return spawn_num; } void Item_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); const OBJECT *const obj = Object_Get(item->object_id); Item_SwitchToAnim(item, 0, 0); item->goal_anim_state = Item_GetAnim(item)->current_anim_state; item->current_anim_state = item->goal_anim_state; item->required_anim_state = 0; item->rot.x = 0; item->rot.z = 0; item->speed = 0; item->fall_speed = 0; item->hit_points = obj->hit_points; item->max_hit_points = obj->hit_points; item->timer = 0; item->mesh_bits = 0xFFFFFFFF; item->touch_bits = 0; item->ai_bits = 0; item->ai_tag = 0; item->after_death = 0; item->creature_data = nullptr; item->extra_rotations = nullptr; item->priv = nullptr; item->carried_item = nullptr; item->interp.result.pos = item->pos; item->interp.result.rot = item->rot; item->active = false; item->status = IS_INACTIVE; item->gravity = false; item->hit_status = false; item->collidable = true; item->looked_at = false; item->enable_interpolation = true; item->enable_shadow = true; item->dynamic_light = false; item->include_in_kill_stats = true; item->clear_body = false; if ((item->flags & IF_KILLED) != 0) { item->clear_body = true; item->flags &= ~IF_KILLED; } if ((item->flags & IF_INVISIBLE) != 0) { item->status = IS_INVISIBLE; item->flags &= ~IF_INVISIBLE; } else if (g_TRVersion >= 2 && obj->intelligent) { item->status = IS_INVISIBLE; } if ((item->flags & IF_CODE_BITS) == IF_CODE_BITS) { item->flags &= ~IF_CODE_BITS; item->flags |= IF_REVERSE; Item_AddActive(item_num); item->status = IS_ACTIVE; } ROOM *const room = Room_Get(item->room_num); item->next_item = room->item_num; room->item_num = item_num; const SECTOR *const sector = Room_GetWorldSector(room, item->pos.x, item->pos.z); item->floor = sector->floor.height; // TODO: remove GF check once demo config reset is run before level load if (Game_IsBonusFlagSet(GBF_NGPLUS) && GF_GetCurrentLevel()->type != GFL_DEMO) { item->hit_points *= 2; } if (obj->priv_size != 0) { if (item->priv != nullptr) { memset(item->priv, 0, obj->priv_size); } else { item->priv = GameBuf_Alloc(obj->priv_size, GBUF_ITEM_DATA); } } if (obj->initialise_func != nullptr) { obj->initialise_func(item_num); } if (item->room_num != NO_ROOM) { Room_AddDrawnItem(item->room_num, item_num); } } void Item_Control(void) { int16_t item_num = Item_GetNextActive(); while (item_num != NO_ITEM) { const ITEM *const item = Item_Get(item_num); const int16_t next = item->next_active; const OBJECT *obj = Object_Get(item->object_id); if ((item->flags & IF_KILLED) == 0 && obj->control_func != nullptr) { obj->control_func(item_num); } item_num = next; } Carrier_AnimateDrops(); } void Item_Kill(const int16_t item_num) { Sparks_DetachItem(item_num); Item_RemoveActive(item_num); Item_RemoveDrawn(item_num); ITEM *const item = &m_Items[item_num]; LARA_INFO *const lara = Lara_GetLaraInfo(); if (item == lara->target) { lara->target = nullptr; } item->flags |= IF_KILLED; if (item_num >= m_LevelItemCount) { item->next_item = m_NextItemFree; m_NextItemFree = item_num; } while (m_MaxUsedItemCount > 0 && m_Items[m_MaxUsedItemCount - 1].flags & IF_KILLED) { m_MaxUsedItemCount--; } } void Item_KillAllActive(void) { int16_t item_num = Item_GetNextActive(); while (item_num != NO_ITEM) { ITEM *const item = Item_Get(item_num); const int16_t next_item_num = item->next_active; if (item->active && (item->flags & IF_REVERSE) == 0 && item->object_id != O_LARA && item->object_id != O_SAVE_CRYSTAL_ITEM && !Object_IsType(item->object_id, g_PickupObjects) && !Object_IsType(item->object_id, g_DoorObjects)) { Item_Kill(item_num); if (Object_IsType(item->object_id, g_ShoalObjects)) { Shoal_TriggerDeactivate(item); } } item_num = next_item_num; } } void Item_RemoveActive(const int16_t item_num) { ITEM *const item = &m_Items[item_num]; if (!item->active) { return; } item->active = false; int16_t link_num = m_NextItemActive; if (link_num == item_num) { m_NextItemActive = item->next_active; return; } while (link_num != NO_ITEM) { if (m_Items[link_num].next_active == item_num) { m_Items[link_num].next_active = item->next_active; return; } link_num = m_Items[link_num].next_active; } } void Item_RemoveDrawn(const int16_t item_num) { const ITEM *const item = &m_Items[item_num]; if (item->room_num == NO_ROOM) { return; } ROOM *const room = Room_Get(item->room_num); Room_RemoveDrawnItem(item->room_num, item_num); if (room->portals != nullptr) { for (int32_t i = 0; i < room->portals->count; i++) { const PORTAL *const portal = &room->portals->portal[i]; Room_RemoveDrawnItem(portal->room_num, item_num); } } int16_t link_num = room->item_num; if (link_num == item_num) { room->item_num = item->next_item; return; } while (link_num != NO_ITEM) { if (m_Items[link_num].next_item == item_num) { m_Items[link_num].next_item = item->next_item; return; } link_num = m_Items[link_num].next_item; } } void Item_ClearKilled(void) { // Remove corpses and other killed items. Part of OG performance // improvements, generously used in Opera House and Barkhang Monastery for (int32_t i = 0; i < Item_GetLevelCount(); i++) { ITEM *const item = Item_Get(i); const OBJECT *const obj = Object_Get(item->object_id); if (obj->intelligent && item->clear_body && item->hit_points <= 0 && (item->flags & IF_KILLED) == 0) { Item_Kill(i); } } } void Item_AddActive(const int16_t item_num) { ITEM *const item = &m_Items[item_num]; if (Object_Get(item->object_id)->control_func == nullptr) { item->status = IS_INACTIVE; return; } if (item->active) { return; } item->active = true; item->next_active = m_NextItemActive; m_NextItemActive = item_num; } void Item_UpdateRoom(const int16_t item_num, const int16_t room_num) { ITEM *const item = &m_Items[item_num]; const int16_t old_room_num = item->room_num; // Add to new-room draw queues (including portal rooms) // draw queue removal for primary room and portal rooms if (old_room_num != NO_ROOM) { Room_RemoveDrawnItem(old_room_num, item_num); const ROOM *const room = Room_Get(old_room_num); if (room != nullptr && room->portals != nullptr) { for (int32_t i = 0; i < room->portals->count; i++) { Room_RemoveDrawnItem( room->portals->portal[i].room_num, item_num); } } } if (room_num != NO_ROOM) { Room_AddDrawnItem(room_num, item_num); const ROOM *const neighbor_room = Room_Get(room_num); if (neighbor_room != nullptr && neighbor_room->portals != nullptr) { for (int32_t i = 0; i < neighbor_room->portals->count; i++) { const PORTAL *const portal = &neighbor_room->portals->portal[i]; if (M_ItemBoundsIntersectsPortal(item, neighbor_room, portal)) { Room_AddDrawnItem(portal->room_num, item_num); } } } } if (old_room_num != room_num) { ROOM *room = nullptr; if (old_room_num != NO_ROOM) { room = Room_Get(old_room_num); int16_t link_num = room->item_num; if (link_num == item_num) { room->item_num = item->next_item; } else { while (link_num != NO_ITEM) { if (m_Items[link_num].next_item == item_num) { m_Items[link_num].next_item = item->next_item; break; } link_num = m_Items[link_num].next_item; } } } if (room_num == NO_ROOM) { Item_RemoveDrawn(item_num); item->room_num = NO_ROOM; } else { room = Room_Get(room_num); item->room_num = room_num; item->next_item = room->item_num; room->item_num = item_num; } } } int32_t Item_GlobalReplace( const OBJECT_ID src_obj_id, const OBJECT_ID dst_obj_id) { int32_t changed = 0; for (int32_t item_num = 0; item_num < m_MaxUsedItemCount; item_num++) { ITEM *const item = &m_Items[item_num]; if (item->object_id == src_obj_id) { item->object_id = dst_obj_id; changed++; } } return changed; } ================================================ FILE: src/trx/game/items/manager.h ================================================ #pragma once #include #include void Item_InitialiseItems(int32_t num_items); ITEM *Item_Get(int16_t num); int16_t Item_GetIndex(const ITEM *item); ITEM *Item_Find(OBJECT_ID obj_id); int32_t Item_GetLevelCount(void); int32_t Item_GetTotalCount(void); int16_t Item_GetNextActive(void); int16_t Item_Create(void); int16_t Item_CreateLevelItem(void); int16_t Item_Spawn(const ITEM *item, OBJECT_ID obj_id); void Item_Initialise(int16_t item_num); void Item_Control(void); void Item_Kill(int16_t item_num); void Item_KillAllActive(void); void Item_RemoveActive(int16_t item_num); void Item_RemoveDrawn(int16_t item_num); void Item_ClearKilled(void); void Item_AddActive(int16_t item_num); void Item_UpdateRoom(int16_t item_num, int16_t room_num); int32_t Item_GlobalReplace(OBJECT_ID src_obj_id, OBJECT_ID dst_obj_id); // Set the name of the item, storing a copy of the provided string. // Returns false if the name is already used by another item. bool Item_SetName(int16_t item_num, const char *name); // Retrieve an item by its name, or nullptr if not found ITEM *Item_GetByName(const char *name); ================================================ FILE: src/trx/game/items/types.h ================================================ #pragma once #include #include #include #include typedef struct CARRIED_ITEM { OBJECT_ID object_id; int16_t spawn_num; XYZ_32 pos; XYZ_16 rot; int16_t room_num; int16_t fall_speed; DROP_STATUS status; struct CARRIED_ITEM *next_item; } CARRIED_ITEM; typedef struct TRAP_DATA TRAP_DATA; typedef struct CREATURE CREATURE; typedef struct { int32_t floor; uint32_t touch_bits; uint32_t mesh_bits; int16_t after_death; OBJECT_ID object_id; int16_t current_anim_state; int16_t goal_anim_state; int16_t required_anim_state; int16_t anim_num; int16_t frame_num; int16_t prev_frame_num; int16_t room_num; int16_t next_item; int16_t next_active; int16_t speed; int16_t fall_speed; int16_t hit_points; int16_t max_hit_points; int16_t box_num; int16_t timer; uint16_t flags; uint8_t ai_bits; int16_t ai_tag; SHADE shade; union { CREATURE *creature_data; TRAP_DATA *trap_data; }; int16_t *extra_rotations; void *priv; CARRIED_ITEM *carried_item; char *name; XYZ_32 pos; XYZ_16 rot; ITEM_STATUS status; bool enable_interpolation; bool enable_shadow; bool active; bool gravity; bool hit_status; bool collidable; bool looked_at; bool dynamic_light; bool clear_body; bool include_in_kill_stats; struct { struct { int32_t floor; XYZ_32 pos; XYZ_16 rot; } result, prev; } interp; } ITEM; ================================================ FILE: src/trx/game/items/utils.c ================================================ #include #include #include #include #include #include #include #include #include #include #include static bool M_UseTR3ExplodingEffects(const ITEM *const item) { if (g_TRVersion < 3) { return false; } // TODO: potentially add a flag/function ptr to OBJECT return item->object_id != O_CLAW_MUTANT && !Object_IsType(item->object_id, g_ShatterableObjects) && !Object_IsType(item->object_id, g_HeavyShatterableObjects); } static bool M_IsFloating(const ITEM *const item) { return Object_IsType(item->object_id, g_WaterObjects) && Object_Get(item->object_id)->intelligent && item->hit_points <= 0; } bool Item_IsAlive(const ITEM *const item) { const OBJECT *const obj = Object_Get(item->object_id); if (obj->is_alive_func != nullptr) { return obj->is_alive_func(item); } if (obj->intelligent && Object_IsType(item->object_id, g_WaterObjects)) { return item->hit_points > 0; } return (item->hit_points > 0) || (item->active); } bool Item_IsTargetable(const ITEM *const item) { const OBJECT *const obj = Object_Get(item->object_id); if (Object_IsType(item->object_id, g_ProjectileObjects)) { return false; } if (obj->is_targetable_func != nullptr) { return obj->is_targetable_func(item); } return item->hit_points > 0 && item->status == IS_ACTIVE && (g_Config.gameplay.enable_ally_targeting || Creature_IsHostile(item)); } bool Item_CanTakeDamage(const ITEM *const item) { const OBJECT *const obj = Object_Get(item->object_id); if (obj->can_take_damage_func != nullptr) { return obj->can_take_damage_func(item); } return Item_IsAlive(item) || M_IsFloating(item); } bool Item_CanBeProjectileTarget(const ITEM *const item) { if (Object_IsType(item->object_id, g_ProjectileObjects)) { return false; } const OBJECT *const obj = Object_Get(item->object_id); if (obj->can_be_projectile_target_func != nullptr) { return obj->can_be_projectile_target_func(item); } if (Object_IsType(item->object_id, g_ShatterableObjects) || Object_IsType(item->object_id, g_SmashableObjects)) { return true; } if (M_IsFloating(item)) { return true; } if (!item->collidable || item->status == IS_INVISIBLE || obj->collision_func == nullptr) { return false; } return Item_IsTargetable(item); } void Item_TakeDamage( ITEM *const item, const int16_t damage, const bool hit_status) { if (!Item_CanTakeDamage(item)) { return; } item->hit_points -= damage; CLAMPL(item->hit_points, 0); if (hit_status) { item->hit_status = true; } } bool Item_IsMeshVisible(const ITEM *const item, const int32_t mesh_num) { if (mesh_num < 0 || mesh_num >= 32) { return false; } const uint32_t bit = 1u << mesh_num; return (item->mesh_bits & bit) != 0; } void Item_SetMeshVisibleMask( ITEM *const item, const uint32_t mesh_mask, const bool visible) { if (visible) { item->mesh_bits |= mesh_mask; } else { item->mesh_bits &= ~mesh_mask; } } void Item_SetMeshVisible( ITEM *const item, const int32_t mesh_num, const bool visible) { if (mesh_num < 0 || mesh_num >= 32) { return; } const uint32_t bit = 1u << mesh_num; Item_SetMeshVisibleMask(item, bit, visible); } void Item_ResetMeshBits(ITEM *const item) { item->mesh_bits = UINT32_MAX; } int32_t Item_Explode( const int16_t item_num, const int32_t mesh_bits, const int16_t damage) { ITEM *const item = Item_Get(item_num); const OBJECT *const obj = Object_Get(item->object_id); if (!obj->loaded) { return 0; } Output_CalculateLight(item->pos, item->room_num); const ANIM_FRAME *const best_frame = Item_GetBestFrame(item); Matrix_PushUnit(); Matrix_Rot16(item->rot); Matrix_TranslateRel16(best_frame->offset); Matrix_Rot16(best_frame->mesh_rots[0]); const int32_t speed_shift = item->object_id == O_TORSO ? 7 : 8; const bool is_tr3 = M_UseTR3ExplodingEffects(item); // main mesh int32_t bit = 1; if ((mesh_bits & bit) && (item->mesh_bits & bit)) { const int16_t effect_num = Effect_Create(item->room_num); if (effect_num != NO_EFFECT) { EFFECT *const effect = Effect_Get(effect_num); effect->pos.x = item->pos.x + (g_MatrixPtr->_03 >> W2V_SHIFT); effect->pos.y = item->pos.y + (g_MatrixPtr->_13 >> W2V_SHIFT); effect->pos.z = item->pos.z + (g_MatrixPtr->_23 >> W2V_SHIFT); effect->rot.y = (Random_GetControl() - 0x4000) * 2; effect->room_num = item->room_num; effect->speed = Random_GetControl() >> speed_shift; effect->fall_speed = -Random_GetControl() >> speed_shift; effect->counter = is_tr3 ? ((damage << 2) | (Random_GetControl() & 3)) : damage; effect->object_id = O_BODY_PART; effect->frame_num = Object_GetItemMeshIndex(item, 0); effect->shade = Output_GetLightAdder() - 0x300; } item->mesh_bits &= ~bit; } // additional meshes const int16_t *extra_rotation = item->extra_rotations; for (int32_t i = 1; i < obj->mesh_count; i++) { const ANIM_BONE *const bone = Object_GetBone(obj, i - 1); if (bone->matrix_pop) { Matrix_Pop(); } if (bone->matrix_push) { Matrix_Push(); } Matrix_TranslateRel32(bone->pos); Matrix_Rot16(best_frame->mesh_rots[i]); Object_ApplyExtraRotation(&extra_rotation, bone->rot, false); bit <<= 1; if ((mesh_bits & bit) && (item->mesh_bits & bit)) { const int16_t effect_num = Effect_Create(item->room_num); if (effect_num != NO_EFFECT) { EFFECT *const effect = Effect_Get(effect_num); effect->pos.x = item->pos.x + (g_MatrixPtr->_03 >> W2V_SHIFT); effect->pos.y = item->pos.y + (g_MatrixPtr->_13 >> W2V_SHIFT); effect->pos.z = item->pos.z + (g_MatrixPtr->_23 >> W2V_SHIFT); effect->rot.y = (Random_GetControl() - 0x4000) * 2; effect->room_num = item->room_num; effect->speed = Random_GetControl() >> speed_shift; effect->fall_speed = -Random_GetControl() >> speed_shift; effect->counter = is_tr3 ? ((damage << 2) | (Random_GetControl() & 3)) : damage; effect->object_id = O_BODY_PART; effect->frame_num = Object_GetItemMeshIndex(item, i); effect->shade = Output_GetLightAdder() - 0x300; } item->mesh_bits &= ~bit; } } Matrix_Pop(); return !(item->mesh_bits & (0x7FFFFFFF >> (31 - obj->mesh_count))); } bool Item_ShouldSpawnBlood(const ITEM *const item) { if (item == nullptr) { return true; } const OBJECT *const obj = Object_Get(item->object_id); if (obj->should_spawn_blood_func != nullptr) { return obj->should_spawn_blood_func(item); } return true; } int16_t Item_FindTypeInRoom(const int16_t room_num, const OBJECT_ID obj_id) { int16_t linked_item_num = Room_Get(room_num)->item_num; while (linked_item_num != NO_ITEM) { const ITEM *const linked_item = Item_Get(linked_item_num); if (linked_item->object_id == obj_id) { return linked_item_num; } linked_item_num = linked_item->next_item; } return NO_ITEM; } int16_t Item_FindTypeAtPos( const int16_t room_num, const XYZ_32 pos, const OBJECT_ID obj_id) { const ROOM *const room = Room_Get(room_num); int16_t item_num = room->item_num; while (item_num != NO_ITEM) { const ITEM *const item = Item_Get(item_num); if (item->object_id == obj_id && XYZ_32_AreEquivalent(item->pos, pos)) { return item_num; } item_num = item->next_item; } return NO_ITEM; } bool Item_IsTriggerActiveRO(const ITEM *const item) { const bool ok = (item->flags & IF_REVERSE) == 0; if ((item->flags & IF_CODE_BITS) != IF_CODE_BITS) { return !ok; } if (item->timer == 0) { return ok; } if (item->timer == -1) { return !ok; } return ok; } bool Item_IsTriggerActive(ITEM *const item) { const bool result = Item_IsTriggerActiveRO(item); if (item->timer != 0 && item->timer != -1) { item->timer--; if (item->timer == 0) { item->timer = -1; } } return result; } ================================================ FILE: src/trx/game/items/utils.h ================================================ #pragma once #include #define ITEM_ADJUST_ROT(source, target, rot) \ do { \ if ((int16_t)(target - source) > rot) { \ source += rot; \ } else if ((int16_t)(target - source) < -rot) { \ source -= rot; \ } else { \ source = target; \ } \ } while (0) bool Item_IsTriggerActiveRO(const ITEM *item); bool Item_IsTriggerActive(ITEM *item); bool Item_IsAlive(const ITEM *item); bool Item_IsTargetable(const ITEM *item); bool Item_CanTakeDamage(const ITEM *item); bool Item_CanBeProjectileTarget(const ITEM *item); void Item_TakeDamage(ITEM *item, int16_t damage, bool hit_status); bool Item_IsMeshVisible(const ITEM *item, int32_t mesh_num); void Item_SetMeshVisible(ITEM *item, int32_t mesh_num, bool visible); void Item_SetMeshVisibleMask(ITEM *item, uint32_t mesh_mask, bool visible); void Item_ResetMeshBits(ITEM *item); // Mesh_bits: which meshes to affect. // Damage: // * Positive values - deal damage, enable body part explosions. // * Negative values - deal damage, disable body part explosions. // * Zero - don't deal any damage, disable body part explosions. int32_t Item_Explode(int16_t item_num, int32_t mesh_bits, int16_t damage); bool Item_ShouldSpawnBlood(const ITEM *item); int16_t Item_FindTypeInRoom(int16_t room_num, OBJECT_ID obj_id); int16_t Item_FindTypeAtPos(int16_t room_num, XYZ_32 pos, OBJECT_ID obj_id); ================================================ FILE: src/trx/game/items/walkable.c ================================================ #include #include #include #include #include #include #include #include #define M_QUADRANT_COUNT 4 typedef struct { SECTOR *sectors[M_QUADRANT_COUNT]; int32_t count; } M_CANDIDATE_SECTORS; typedef struct { WALKABLE *nodes; int32_t capacity; int32_t active_count; } M_SETUP; static M_SETUP *m_Setup = nullptr; static int32_t m_SetupCount = 0; static SECTOR *M_GetItemPitSector(const XYZ_32 pos, int16_t room_num) { SECTOR *const sector = Room_GetSector(pos, &room_num); return Room_GetPitSector(sector, pos.x, pos.z); } static bool M_HasCandidateSector( const M_CANDIDATE_SECTORS *const candidates, const SECTOR *const sector) { for (int32_t i = 0; i < candidates->count; i++) { if (candidates->sectors[i] == sector) { return true; } } return false; } static M_CANDIDATE_SECTORS M_GetCandidateSectors( const XYZ_32 base_pos, int16_t room_num) { // Probe evenly around the centre position for cases where a walkable is // placed above a triangle portal, so detecting the correct sector at all // possible positions. M_CANDIDATE_SECTORS candidates = { 0 }; const XYZ_32 mid_pos = { .x = ROUND_TO_SECTOR(base_pos.x) + STEP_L * 2, .y = base_pos.y, .z = ROUND_TO_SECTOR(base_pos.z) + STEP_L * 2, }; const XZ_32 deltas[M_QUADRANT_COUNT] = { { -STEP_L, 0 }, { STEP_L, 0 }, { 0, -STEP_L }, { 0, STEP_L }, }; for (int32_t i = 0; i < M_QUADRANT_COUNT; i++) { const XZ_32 delta = deltas[i]; const XYZ_32 pos = { .x = mid_pos.x + delta.x, .y = base_pos.y, .z = mid_pos.z + delta.z, }; SECTOR *const sector = M_GetItemPitSector(pos, room_num); if (!M_HasCandidateSector(&candidates, sector)) { candidates.sectors[candidates.count] = sector; candidates.count++; } } return candidates; } static M_SETUP *M_GetSetup(const int16_t item_num) { ASSERT(m_Setup != nullptr); ASSERT(item_num >= 0 && item_num < m_SetupCount); return &m_Setup[item_num]; } static bool M_SectorContainsWalkable( const SECTOR *const sector, const int16_t item_num) { const WALKABLE *walkable = sector->walkable; while (walkable != nullptr) { if (walkable->item_num == item_num) { return true; } walkable = walkable->next; } return false; } static void M_InsertSorted(WALKABLE **walkables, WALKABLE *const node) { while (*walkables != nullptr && (*walkables)->pos.y >= node->pos.y) { walkables = &(*walkables)->next; } node->next = *walkables; *walkables = node; } static void M_Remove( const int16_t item_num, const XYZ_32 pos, const int16_t room_num) { const ITEM *const item = Item_Get(item_num); const OBJECT *const obj = Object_Get(item->object_id); if (obj->add_walkable_func == nullptr) { return; } M_SETUP *const setup = M_GetSetup(item_num); if (setup->capacity == 0 || setup->nodes == nullptr) { return; } const M_CANDIDATE_SECTORS sectors = M_GetCandidateSectors(pos, room_num); for (int32_t i = 0; i < sectors.count; i++) { SECTOR *const sector = sectors.sectors[i]; WALKABLE *walkable = sector->walkable; WALKABLE *prev = nullptr; while (walkable != nullptr) { if (walkable->item_num == item_num) { if (prev != nullptr) { prev->next = walkable->next; } else { sector->walkable = walkable->next; } break; } prev = walkable; walkable = walkable->next; } } setup->active_count = 0; } void Walkable_AllocateNodes(const ITEM *const item, const int32_t footprint) { const int16_t item_num = Item_GetIndex(item); M_SETUP *const setup = M_GetSetup(item_num); setup->capacity = footprint * M_QUADRANT_COUNT; setup->active_count = 0; setup->nodes = nullptr; if (setup->capacity > 0) { setup->nodes = GameBuf_Alloc(sizeof(WALKABLE) * setup->capacity, GBUF_WALKABLES); } } void Walkable_Add(const int16_t item_num, const XYZ_32 pos) { const ITEM *const item = Item_Get(item_num); const OBJECT *const obj = Object_Get(item->object_id); if (obj->add_walkable_func == nullptr) { return; } M_SETUP *const setup = M_GetSetup(item_num); if (setup->capacity == 0 || setup->nodes == nullptr) { return; } const M_CANDIDATE_SECTORS sectors = M_GetCandidateSectors(pos, item->room_num); for (int32_t i = 0; i < sectors.count; i++) { SECTOR *const sector = sectors.sectors[i]; if (M_SectorContainsWalkable(sector, item_num)) { continue; } if (setup->active_count >= setup->capacity) { LOG_WARNING( "Walkable %d at (%d, %d, %d) has no more allocated sector " "nodes.", item_num, pos.x, pos.y, pos.z); break; } WALKABLE *const node = &setup->nodes[setup->active_count]; node->item_num = item_num; node->pos = pos; node->next = nullptr; M_InsertSorted(§or->walkable, node); setup->active_count++; } } void Walkable_Remove(const int16_t item_num) { const ITEM *const item = Item_Get(item_num); M_Remove(item_num, item->pos, item->room_num); } void Walkable_Reposition( const int16_t item_num, const GAME_VECTOR start, const GAME_VECTOR target) { M_Remove(item_num, start.pos, start.room_num); Walkable_Add(item_num, target.pos); } void Walkable_Reset(void) { const int32_t room_count = Room_GetCount(); for (int32_t room_idx = 0; room_idx < room_count; room_idx++) { const ROOM *const room = Room_Get(room_idx); const int32_t num_sectors = room->size.x * room->size.z; for (int32_t i = 0; i < num_sectors; i++) { room->sectors[i].walkable = nullptr; } } for (int32_t i = 0; i < m_SetupCount; i++) { M_SETUP *const setup = M_GetSetup(i); setup->active_count = 0; } } void Walkable_ResetLevel(void) { Walkable_Reset(); const int32_t item_count = Item_GetLevelCount(); m_SetupCount = item_count; m_Setup = nullptr; if (item_count > 0) { m_Setup = GameBuf_Alloc(sizeof(M_SETUP) * item_count, GBUF_WALKABLES); } } void Walkable_Shutdown(void) { m_Setup = nullptr; m_SetupCount = 0; } ================================================ FILE: src/trx/game/items/walkable.h ================================================ #pragma once #include #include #include void Walkable_AllocateNodes(const ITEM *item, int32_t footprint); void Walkable_Add(int16_t item_num, XYZ_32 pos); void Walkable_Remove(int16_t item_num); void Walkable_Reset(void); void Walkable_ResetLevel(void); void Walkable_Shutdown(void); void Walkable_Reposition( int16_t item_num, GAME_VECTOR start, GAME_VECTOR target); ================================================ FILE: src/trx/game/items.h ================================================ #pragma once #include #include #include #include #include #include #include #include #include #include ================================================ FILE: src/trx/game/lara/breath.c ================================================ #include #include #include #include #include #include #include #include #include #include static bool M_CanBreatheVisible(const ITEM *const lara_item) { if (lara_item == nullptr) { return false; } if (g_TRVersion != 3) { return false; } if (!Level_HasColdWater()) { return false; } const LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->extra_anim) { return false; } if (lara->water_status == LWS_CHEAT || lara->water_status == LWS_UNDERWATER || lara_item->hit_points < 0) { return false; } if (lara_item->current_anim_state == LS(LS_STOP)) { return Item_GetRelativeFrame(lara_item) >= 30; } if (lara_item->current_anim_state == LS(LS_CROUCH_IDLE)) { return Item_GetRelativeFrame(lara_item) >= 30; } const int32_t time = (int32_t)Output_GetTimeInGame() % 64; return time >= 32 && time <= 48; } void Lara_Breath_Control(const ITEM *const lara_item) { if (!M_CanBreatheVisible(lara_item)) { return; } XYZ_32 pos = { .x = 0, .y = -4, .z = 64 }; Collide_GetJointAbsPosition(lara_item, &pos, LM_HEAD); XYZ_32 end = { .x = (Random_GetControl() & 7) - 4, .y = (Random_GetControl() & 7) - 8, .z = (Random_GetControl() & 0x7F) + 64, }; Collide_GetJointAbsPosition(lara_item, &end, LM_HEAD); const XYZ_32 vel = { .x = end.x - pos.x, .y = end.y - pos.y, .z = end.z - pos.z, }; Sparks_TriggerBreath(pos, vel, lara_item->room_num); } ================================================ FILE: src/trx/game/lara/breath.h ================================================ #pragma once #include void Lara_Breath_Control(const ITEM *lara_item); ================================================ FILE: src/trx/game/lara/cheat.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static void M_GiveAllKeysImpl(void) { Inv_AddItem(O_PUZZLE_ITEM_1); Inv_AddItem(O_PUZZLE_ITEM_2); Inv_AddItem(O_PUZZLE_ITEM_3); Inv_AddItem(O_PUZZLE_ITEM_4); Inv_AddItem(O_KEY_ITEM_1); Inv_AddItem(O_KEY_ITEM_2); Inv_AddItem(O_KEY_ITEM_3); Inv_AddItem(O_KEY_ITEM_4); Inv_AddItem(O_QUEST_ITEM_1); Inv_AddItem(O_QUEST_ITEM_2); Inv_AddItem(O_QUEST_ITEM_3); Inv_AddItem(O_QUEST_ITEM_4); Inv_AddItem(O_PICKUP_ITEM_1); Inv_AddItem(O_PICKUP_ITEM_2); Inv_AddItem(O_LEADBAR_ITEM); } static void M_GiveAllGunsImpl(const bool ignore_exclusions) { LARA_INFO *const lara_info = Lara_GetLaraInfo(); const bool bonus_flag = Game_IsBonusFlagSet(GBF_NGPLUS); Inv_AddItem(O_PISTOL_ITEM); if (Lara_Cheat_GiveGun(LGT_SHOTGUN, ignore_exclusions)) { lara_info->shotgun_ammo.ammo = bonus_flag ? 10001 : 300; } if (Lara_Cheat_GiveGun(LGT_MAGNUMS, ignore_exclusions)) { lara_info->magnum_ammo.ammo = bonus_flag ? 10001 : 1000; } if (Lara_Cheat_GiveGun(LGT_AUTOS, ignore_exclusions)) { lara_info->autos_ammo.ammo = bonus_flag ? 10001 : 1000; } if (Lara_Cheat_GiveGun(LGT_DESERT_EAGLE, ignore_exclusions)) { lara_info->desert_eagle_ammo.ammo = bonus_flag ? 10001 : 1000; } if (Lara_Cheat_GiveGun(LGT_UZIS, ignore_exclusions)) { lara_info->uzi_ammo.ammo = bonus_flag ? 10001 : 2000; } if (Lara_Cheat_GiveGun(LGT_HARPOON, ignore_exclusions)) { lara_info->harpoon_ammo.ammo = bonus_flag ? 10001 : 300; } if (Lara_Cheat_GiveGun(LGT_M16, ignore_exclusions)) { lara_info->m16_ammo.ammo = bonus_flag ? 10001 : 300; } if (Lara_Cheat_GiveGun(LGT_MP5, ignore_exclusions)) { lara_info->mp5_ammo.ammo = bonus_flag ? 10001 : 300; } if (Lara_Cheat_GiveGun(LGT_GRENADE, ignore_exclusions)) { lara_info->grenade_ammo.ammo = bonus_flag ? 10001 : 300; } if (Lara_Cheat_GiveGun(LGT_ROCKET, ignore_exclusions)) { lara_info->rocket_ammo.ammo = bonus_flag ? 10001 : 300; } } static void M_GiveAllMedpacksImpl(void) { if (g_Weapons[LGT_FLARE].is_available) { Inv_AddItemNTimes(O_FLAREBOX_ITEM, 10); } Inv_AddItemNTimes(O_SMALL_MEDIPACK_ITEM, 10); Inv_AddItemNTimes(O_LARGE_MEDIPACK_ITEM, 10); } static void M_ReinitialiseGunMeshes(void) { Lara_Mesh_Initialise(Game_GetCurrentLevel()); Gun_InitialiseNewWeapon(); } static void M_ClearHandWeaponMeshes(void) { Gun_SetLaraHandRMesh(LGT_UNARMED); if (!Lara_Flare_IsMeshActive()) { Gun_SetLaraHandLMesh(LGT_UNARMED); } } static void M_ResetGunStatus(void) { const ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara_info = Lara_GetLaraInfo(); const bool has_flare = Lara_Flare_IsMeshActive(); if (has_flare) { lara_info->gun_type = LGT_FLARE; return; } lara_info->gun_status = LGS_ARMLESS; lara_info->gun_type = LGT_UNARMED; lara_info->request_gun_type = LGT_UNARMED; lara_info->gun_item_num = NO_ITEM; lara_info->left_arm.frame_num = 0; lara_info->left_arm.lock = 0; lara_info->right_arm.frame_num = 0; lara_info->right_arm.lock = 0; lara_info->left_arm.anim_num = lara_item->anim_num; lara_info->right_arm.anim_num = lara_item->anim_num; const ANIM *const anim = Item_GetAnim(lara_item); lara_info->left_arm.frame_base = anim->frame_ptr; lara_info->right_arm.frame_base = anim->frame_ptr; } bool Lara_Cheat_GiveAllKeys(void) { if (Lara_GetItem() == nullptr) { return false; } M_GiveAllKeysImpl(); Sound_Effect(SFX_LARA_KEY, nullptr, SPM_ALWAYS); Console_Log(GS("general/osd/give_item_all_keys")); return true; } bool Lara_Cheat_GiveAllGuns(const bool ignore_exclusions) { if (Lara_GetItem() == nullptr) { return false; } M_GiveAllGunsImpl(ignore_exclusions); Sound_Effect(SFX_LARA_RELOAD, nullptr, SPM_ALWAYS); Console_Log(GS("general/osd/give_item_all_guns")); return true; } bool Lara_Cheat_GiveGun( const LARA_GUN_TYPE gun_type, const bool ignore_exclusions) { const OBJECT_ID gun_object_id = Gun_GetGunObject(gun_type); if (gun_object_id == NO_OBJECT) { return false; } if (!ignore_exclusions && !g_Weapons[gun_type].is_available) { return false; } return Inv_AddItem(gun_object_id); } bool Lara_Cheat_GiveAllItems(void) { if (Lara_GetItem() == nullptr) { return false; } M_GiveAllGunsImpl(false); M_GiveAllKeysImpl(); M_GiveAllMedpacksImpl(); Sound_Effect(SFX_LARA_HOLSTER, nullptr, SPM_NORMAL); Console_Log(GS("general/osd/give_item_cheat")); return true; } void Lara_Cheat_GetStuff(void) { M_GiveAllGunsImpl(false); M_GiveAllMedpacksImpl(); } void Lara_Cheat_EndLevel(void) { Game_SetIsLevelComplete(true); Console_Log(GS("general/osd/complete_level")); } bool Lara_Cheat_KillEnemy(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if ((item->flags & IF_KILLED) != 0) { return false; } if (!Item_IsAlive(item) && item->status != IS_ACTIVE) { return false; } if (Object_IsType(item->object_id, g_LoyalObjects)) { LARA_INFO *const lara_info = Lara_GetLaraInfo(); lara_info->killed_loyal_item = true; } Sound_Effect(SFX_EXPLOSION_1, &item->pos, SPM_NORMAL); Creature_Die(item_num, true); return true; } bool Lara_Cheat_OpenNearestDoor(void) { const ITEM *const lara_item = Lara_GetItem(); if (lara_item == nullptr) { return false; } int32_t opened = 0; int32_t closed = 0; const int32_t shift = 8; // constant shift to avoid overflow errors const int32_t max_dist = SQUARE((WALL_L * 2) >> shift); for (int32_t item_num = 0; item_num < Item_GetLevelCount(); item_num++) { ITEM *const item = Item_Get(item_num); if (!Object_IsType(item->object_id, g_DoorObjects) && !Object_IsType(item->object_id, g_TrapdoorObjects)) { continue; } const int32_t dx = (item->pos.x - lara_item->pos.x) >> shift; const int32_t dy = (item->pos.y - lara_item->pos.y) >> shift; const int32_t dz = (item->pos.z - lara_item->pos.z) >> shift; const int32_t dist = SQUARE(dx) + SQUARE(dy) + SQUARE(dz); if (dist > max_dist) { continue; } if (!item->active) { Item_AddActive(item_num); item->flags |= IF_CODE_BITS; opened++; } else if ((item->flags & IF_CODE_BITS) != 0) { item->flags &= ~IF_CODE_BITS; closed++; } else { item->flags |= IF_CODE_BITS; opened++; } item->timer = 0; item->touch_bits = 0; } if (opened > 0 || closed > 0) { Console_Log( opened > 0 ? GS("general/osd/door_open") : GS("general/osd/door_close")); return true; } Console_LogError(GS("general/osd/door_open_fail")); return false; } bool Lara_Cheat_EnterFlyMode(void) { ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (lara_item == nullptr) { return false; } if ((lara_item->flags & IF_ONE_SHOT) != 0) { // The explosion cheat has been used, so Lara's death is permanent. return false; } Viewport_AlterFOV(-1, FOV_MODE_GAME); if (lara_info->extra_anim || lara_item->hit_points < 0) { M_ResetGunStatus(); M_ClearHandWeaponMeshes(); } else if (Gun_IsRifleType(lara_info->gun_type)) { while (lara_info->gun_item_num != NO_ITEM) { Gun_Rifle_Undraw(lara_info->gun_type); } } if (lara_info->gun_status == LGS_HANDS_BUSY || (lara_info->gun_status == LGS_UNDRAW && Lara_Skin_GetEquipment(LM_TORSO)->type == EQUIPMENT_TYPE_WEAPON)) { lara_info->gun_status = LGS_ARMLESS; M_ClearHandWeaponMeshes(); } lara_info->extra_anim = false; Lara_Vehicle_Dismount(); if (lara_info->water_status != LWS_UNDERWATER || lara_item->hit_points <= 0) { lara_item->pos.y -= STEP_L; lara_item->current_anim_state = LS(LS_SWIM); lara_item->goal_anim_state = LS(LS_SWIM); Item_SwitchToAnim(lara_item, LA(LA_UNDERWATER_SWIM_FORWARD_DRIFT), 0); lara_item->gravity = false; lara_item->rot.x = 30 * DEG_1; lara_item->fall_speed = 30; lara_info->head_rot.x = 0; lara_info->head_rot.y = 0; lara_info->torso_rot.x = 0; lara_info->torso_rot.y = 0; } lara_info->water_status = LWS_CHEAT; lara_info->hit_effect_count = 0; lara_info->hit_effect = nullptr; lara_info->hit_frame = 0; lara_info->hit_direction = DIR_UNKNOWN; lara_info->air = LARA_MAX_AIR; lara_info->death_timer = 0; lara_info->mesh_effects = 0; lara_item->enable_shadow = true; lara_item->hit_points = LARA_MAX_HITPOINTS; lara_info->interact_target.item_num = NO_ITEM; lara_info->interact_target.is_moving = false; lara_info->interact_target.move_count = 0; Lara_Extinguish(); M_ReinitialiseGunMeshes(); Lara_Skin_ApplyOutfit(); g_Camera.type = CAM_CHASE; Console_Log(GS("general/osd/fly_mode_on")); return true; } bool Lara_Cheat_ExitFlyMode(void) { ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (lara_item == nullptr) { return false; } const ROOM *const room = Room_Get(lara_item->room_num); const int16_t water_height = Room_GetWaterHeight(lara_item->pos, lara_item->room_num); if (room->flags.underwater || (water_height != NO_HEIGHT && water_height > 0 && !room->flags.swamp)) { lara_info->water_status = LWS_UNDERWATER; } else { lara_info->water_status = room->flags.swamp ? LWS_WADE : LWS_ABOVE_WATER; Item_SwitchToAnim(lara_item, LA(LA_STAND_STILL), 0); lara_item->goal_anim_state = LS(LS_STOP); lara_item->current_anim_state = LS(LS_STOP); lara_item->rot.x = 0; lara_item->rot.z = 0; lara_info->head_rot.x = 0; lara_info->head_rot.y = 0; lara_info->torso_rot.x = 0; lara_info->torso_rot.y = 0; } if (lara_info->gun_item_num != NO_ITEM) { lara_info->gun_status = LGS_UNDRAW; } else { lara_info->gun_status = LGS_ARMLESS; M_ClearHandWeaponMeshes(); M_ReinitialiseGunMeshes(); } if (lara_info->water_status == LWS_ABOVE_WATER) { // Prevent Lara from jumping if the player holds the swim button // during the fly cheat exit (#4470) InputState_Clear(&g_Input); InputState_Clear(&g_InputDB); Lara_Control(); } Console_Log(GS("general/osd/fly_mode_off")); return true; } bool Lara_Cheat_Teleport(XYZ_32 pos, int16_t room_num) { if (!Room_FindValidPos(&pos, &room_num)) { return false; } const SECTOR *const sector = Room_GetSector(pos, &room_num); const int32_t height = Room_GetHeightEx(sector, pos, true, NO_ITEM); if (height == NO_HEIGHT) { return false; } ITEM *const lara_item = Lara_GetItem(); lara_item->pos.x = pos.x; lara_item->pos.y = pos.y; lara_item->pos.z = pos.z; lara_item->floor = height; const int16_t item_num = Item_GetIndex(lara_item); Item_UpdateRoom(item_num, room_num); LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (lara_info->gun_status == LGS_HANDS_BUSY) { lara_info->gun_status = LGS_ARMLESS; } Lara_Vehicle_Dismount(); if (lara_info->extra_anim) { const ROOM *const room = Room_Get(lara_item->room_num); const bool room_submerged = room->flags.underwater; const int16_t water_height = Room_GetWaterHeight(lara_item->pos, lara_item->room_num); if (room_submerged || (water_height != NO_HEIGHT && water_height > 0)) { lara_info->water_status = LWS_UNDERWATER; lara_item->current_anim_state = LS(LS_SWIM); lara_item->goal_anim_state = LS(LS_SWIM); Item_SwitchToAnim( lara_item, LA(LA_UNDERWATER_SWIM_FORWARD_DRIFT), 0); } else { lara_info->water_status = LWS_ABOVE_WATER; lara_item->current_anim_state = LS(LS_STOP); lara_item->goal_anim_state = LS(LS_STOP); Item_SwitchToAnim(lara_item, LA(LA_STAND_STILL), 0); lara_item->rot.x = 0; lara_item->rot.z = 0; lara_info->head_rot.x = 0; lara_info->head_rot.y = 0; lara_info->torso_rot.x = 0; lara_info->torso_rot.y = 0; } lara_info->extra_anim = false; M_ResetGunStatus(); M_ReinitialiseGunMeshes(); } lara_info->hit_effect_count = 0; lara_info->hit_effect = nullptr; lara_info->hit_frame = 0; lara_info->hit_direction = DIR_UNKNOWN; lara_info->air = LARA_MAX_AIR; lara_info->death_timer = 0; lara_info->mesh_effects = 0; if (g_Camera.type == CAM_PHOTO_MODE) { Lara_Hair_Control(false); Interpolation_CommitLara(); } else { g_Camera.type = CAM_CHASE; Viewport_AlterFOV(-1, FOV_MODE_GAME); Camera_ResetPosition(); } return true; } ================================================ FILE: src/trx/game/lara/cheat.h ================================================ #pragma once #include #include bool Lara_Cheat_GiveAllKeys(void); bool Lara_Cheat_GiveAllGuns(bool ignore_exclusions); bool Lara_Cheat_GiveGun(LARA_GUN_TYPE gun_type, bool ignore_exclusions); bool Lara_Cheat_GiveAllItems(void); void Lara_Cheat_GetStuff(void); void Lara_Cheat_EndLevel(void); bool Lara_Cheat_KillEnemy(int16_t item_num); bool Lara_Cheat_OpenNearestDoor(void); bool Lara_Cheat_EnterFlyMode(void); bool Lara_Cheat_ExitFlyMode(void); bool Lara_Cheat_Teleport(XYZ_32 pos, int16_t room_num); ================================================ FILE: src/trx/game/lara/cheat_keys.c ================================================ #include #include #include #include #include #include #include #define M_MIN_TURN 94208 typedef enum { // clang-format off CHEAT_INITIAL, CHEAT_STEP_FORWARD, CHEAT_STEP_FORWARD_STOP, CHEAT_STEP_BACK, CHEAT_STEP_BACK_STOP, CHEAT_TURN_LEFT, CHEAT_TURN_RIGHT, CHEAT_TURN_STOP, CHEAT_FINAL_JUMP, // clang-format on } M_CHEAT_STATE; static int32_t m_CheatState = CHEAT_INITIAL; static LARA_GUN_TYPE m_InitialGunType = LGT_UNARMED; static LARA_GUN_STATE m_InitialGunState = LGS_ARMLESS; static int16_t m_CheatAngle = 0; static int32_t m_CheatTurn = 0; static void M_CompleteLevel(void) { Game_SetIsLevelComplete(true); } static void M_GiveItems(void) { LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (Lara_Cheat_GiveGun(LGT_SHOTGUN, false)) { lara_info->shotgun_ammo.ammo = 500; } if (Lara_Cheat_GiveGun(LGT_MAGNUMS, false)) { lara_info->magnum_ammo.ammo = 500; } if (Lara_Cheat_GiveGun(LGT_AUTOS, false)) { lara_info->autos_ammo.ammo = 500; } if (Lara_Cheat_GiveGun(LGT_DESERT_EAGLE, false)) { lara_info->desert_eagle_ammo.ammo = 500; } if (Lara_Cheat_GiveGun(LGT_UZIS, false)) { lara_info->uzi_ammo.ammo = 5000; } if (Lara_Cheat_GiveGun(LGT_HARPOON, false)) { lara_info->harpoon_ammo.ammo = 5000; } if (Lara_Cheat_GiveGun(LGT_GRENADE, false)) { lara_info->grenade_ammo.ammo = 5000; } if (Lara_Cheat_GiveGun(LGT_ROCKET, false)) { lara_info->rocket_ammo.ammo = 5000; } if (Lara_Cheat_GiveGun(LGT_M16, false)) { lara_info->m16_ammo.ammo = 5000; } if (Lara_Cheat_GiveGun(LGT_MP5, false)) { lara_info->mp5_ammo.ammo = 5000; } Inv_AddItemNTimes(O_SMALL_MEDIPACK_ITEM, 50); Inv_AddItemNTimes(O_LARGE_MEDIPACK_ITEM, 50); if (g_Weapons[LGT_FLARE].is_available) { Inv_AddItemNTimes(O_FLARE_ITEM, 50); } Sound_Effect(SFX_LARA_HOLSTER, nullptr, SPM_ALWAYS); } static void M_ExplodeLara(void) { const LARA_INFO *const lara_info = Lara_GetLaraInfo(); ITEM *const lara_item = Lara_GetItem(); Item_Explode(lara_info->item_num, -1, 1); Sound_Effect(SFX_EXPLOSION_1, &lara_item->pos, SPM_NORMAL); lara_item->hit_points = 0; lara_item->status = IS_INVISIBLE; lara_item->collidable = false; lara_item->flags |= IF_INVISIBLE; } static bool M_ProcessOutcome( const LARA_INFO *const lara_info, const ITEM *const lara_item) { if (lara_item->fall_speed <= 0) { return false; } const LARA_STATE state = lara_item->current_anim_state; switch (g_TRVersion) { case 1: if (state == LS(LS_JUMP_FORWARD)) { M_CompleteLevel(); } else if (state == LS(LS_JUMP_BACK)) { M_GiveItems(); } else if (state == LS(LS_SWAN_DIVE)) { M_ExplodeLara(); } break; case 2: if (m_InitialGunType == LGT_FLARE && lara_info->gun_type == m_InitialGunType && lara_info->gun_status == m_InitialGunState) { if (state == LS(LS_JUMP_FORWARD)) { M_CompleteLevel(); } else if (state == LS(LS_JUMP_BACK)) { M_GiveItems(); } } else if (state == LS(LS_JUMP_FORWARD) || state == LS(LS_JUMP_BACK)) { M_ExplodeLara(); } break; case 3: if (m_InitialGunType == LGT_PISTOLS && m_InitialGunState == LGS_READY && lara_info->gun_type == m_InitialGunType && lara_info->gun_status == m_InitialGunState) { if (state == LS(LS_JUMP_FORWARD)) { M_CompleteLevel(); } else if (state == LS(LS_JUMP_BACK)) { M_GiveItems(); } } else if (state == LS(LS_JUMP_FORWARD) || state == LS(LS_JUMP_BACK)) { M_ExplodeLara(); } } return true; } static LARA_STATE M_GetBackstepState(void) { return g_TRVersion == 3 ? LS(LS_CROUCH_IDLE) : LS(LS_WALK_BACK); } void Lara_Cheat_CheckKeys(void) { if (Game_IsInGym()) { return; } const LARA_INFO *const lara_info = Lara_GetLaraInfo(); const ITEM *const lara_item = Lara_GetItem(); const LARA_STATE ls = lara_item->current_anim_state; const LARA_STATE backstep_state = M_GetBackstepState(); switch (m_CheatState) { case CHEAT_INITIAL: m_CheatState = ls == LS(LS_WALK) ? CHEAT_STEP_FORWARD : CHEAT_INITIAL; break; case CHEAT_STEP_FORWARD: m_InitialGunType = lara_info->gun_type; m_InitialGunState = lara_info->gun_status; if (ls != LS(LS_WALK)) { m_CheatState = ls == LS(LS_STOP) ? CHEAT_STEP_FORWARD_STOP : CHEAT_INITIAL; } break; case CHEAT_STEP_FORWARD_STOP: if (ls != LS(LS_STOP)) { m_CheatState = ls == backstep_state ? CHEAT_STEP_BACK : CHEAT_INITIAL; } break; case CHEAT_STEP_BACK: if (ls != backstep_state) { m_CheatState = ls == LS(LS_STOP) ? CHEAT_STEP_BACK_STOP : CHEAT_INITIAL; } break; case CHEAT_STEP_BACK_STOP: if (ls != LS(LS_STOP)) { m_CheatTurn = 0; m_CheatAngle = lara_item->rot.y; if (ls == LS(LS_TURN_LEFT)) { m_CheatState = CHEAT_TURN_LEFT; } else if (ls == LS(LS_TURN_RIGHT)) { m_CheatState = CHEAT_TURN_RIGHT; } else { m_CheatState = CHEAT_INITIAL; } } break; case CHEAT_TURN_LEFT: if (ls != LS(LS_TURN_LEFT) && ls != LS(LS_FAST_TURN)) { m_CheatState = m_CheatTurn < -M_MIN_TURN ? CHEAT_TURN_STOP : CHEAT_INITIAL; } else { m_CheatTurn += (int16_t)(lara_item->rot.y - m_CheatAngle); m_CheatAngle = lara_item->rot.y; } break; case CHEAT_TURN_RIGHT: if (ls != LS(LS_TURN_RIGHT) && ls != LS(LS_FAST_TURN)) { m_CheatState = m_CheatTurn > M_MIN_TURN ? CHEAT_TURN_STOP : CHEAT_INITIAL; } else { m_CheatTurn += (int16_t)(lara_item->rot.y - m_CheatAngle); m_CheatAngle = lara_item->rot.y; } break; case CHEAT_TURN_STOP: if (ls != LS(LS_STOP)) { m_CheatState = ls == LS(LS_COMPRESS) ? CHEAT_FINAL_JUMP : CHEAT_INITIAL; } break; case CHEAT_FINAL_JUMP: if (M_ProcessOutcome(lara_info, lara_item)) { m_CheatState = CHEAT_INITIAL; } break; default: m_CheatState = CHEAT_INITIAL; break; } } ================================================ FILE: src/trx/game/lara/cheat_keys.h ================================================ #pragma once void Lara_Cheat_CheckKeys(void); ================================================ FILE: src/trx/game/lara/col/climb.c ================================================ #include #include #include #include #include #include // clang-format off #define M_CLIMB_SHIFT 70 #define M_CLIMB_HANG 900 #define M_CLIMB_WIDTH_LEFT 80 #define M_CLIMB_WIDTH_RIGHT 120 #define M_CLIMB_HEIGHT (WALL_L / 2) // = 512 #define M_VAULT_ANGLE (30 * DEG_1) // = 5460 #define M_VAULT_GAP (-LARA_HEIGHT + STEP_L / 8) // = -730 #define M_LF_HANG 21 #define M_LF_STOP_HANG 9 #define M_LF_CLIMB_L_SHIFT_START 28 #define M_LF_CLIMB_L_SHIFT_END 29 #define M_LF_CLIMB_R_SHIFT 57 #define M_LEDGE_JUMP_PUSH_HEIGHT (STEP_L - 16) // = 240 #define M_LEDGE_JUMP_HEIGHT_UP (LARA_HEIGHT + (STEP_L * 3) / 8) // = 858 #define M_LEDGE_JUMP_HEIGHT_BACK (LARA_HEIGHT - (STEP_L * 5) / 4) // = 442 #define M_HANG_SHIFT (g_TRVersion >= 3 ? 4 : 2) // clang-format on typedef enum { // clang-format off CLIMB_RESULT_CRAWL = -2, CLIMB_RESULT_NEG = -1, CLIMB_RESULT_NONE = 0, CLIMB_RESULT_POS = 1, // clang-format on } M_CLIMB_RESULT; static M_CLIMB_RESULT M_TestClimbPos( const ITEM *const item, const int32_t front, const int32_t right, const int32_t origin, const int32_t item_height, int32_t *const shift) { const int32_t y = item->pos.y + origin; int32_t x; int32_t z; int32_t x_front = 0; int32_t z_front = 0; switch (Math_GetDirection(item->rot.y)) { case DIR_NORTH: x = item->pos.x + right; z = item->pos.z + front; z_front = M_HANG_SHIFT; break; case DIR_EAST: x = item->pos.x + front; z = item->pos.z - right; x_front = M_HANG_SHIFT; break; case DIR_SOUTH: x = item->pos.x - right; z = item->pos.z - front; z_front = -M_HANG_SHIFT; break; case DIR_WEST: x = item->pos.x - front; z = item->pos.z + right; x_front = -M_HANG_SHIFT; break; default: x = front; z = front; break; } *shift = 0; bool hang = true; if (!Lara_GetLaraInfo()->climb_status) { return CLIMB_RESULT_NONE; } int16_t room_num = item->room_num; XYZ_32 sample_pos = { x, y - 128, z }; const SECTOR *sector = Room_GetSector(sample_pos, &room_num); sample_pos.y = y; int32_t height = Room_GetHeight(sector, sample_pos); if (height == NO_HEIGHT) { return CLIMB_RESULT_NONE; } height -= y + item_height + STEP_L / 2; if (height < -M_CLIMB_SHIFT) { return CLIMB_RESULT_NONE; } if (height < 0) { *shift = height; } int32_t ceiling = Room_GetCeiling(sector, sample_pos) - y; if (ceiling > M_CLIMB_SHIFT) { return CLIMB_RESULT_NONE; } if (ceiling > 0) { if (*shift) { return CLIMB_RESULT_NONE; } *shift = ceiling; } if (item_height + height < M_CLIMB_HANG) { hang = false; } const int32_t x2 = x + x_front; const int32_t z2 = z + z_front; sample_pos.x = x2; sample_pos.y = y; sample_pos.z = z2; sector = Room_GetSector(sample_pos, &room_num); height = Room_GetHeight(sector, sample_pos); if (height != NO_HEIGHT) { height -= y; } if (height > M_CLIMB_SHIFT) { ceiling = Room_GetCeiling(sector, sample_pos) - y; if (ceiling >= M_CLIMB_HEIGHT) { return CLIMB_RESULT_POS; } if (ceiling > M_CLIMB_HEIGHT - M_CLIMB_SHIFT) { if (*shift > 0) { return hang ? CLIMB_RESULT_NEG : CLIMB_RESULT_NONE; } *shift = ceiling - M_CLIMB_HEIGHT; return CLIMB_RESULT_POS; } if (ceiling > 0) { return hang ? CLIMB_RESULT_NEG : CLIMB_RESULT_NONE; } if (ceiling > -M_CLIMB_SHIFT && hang && *shift <= 0) { if (*shift > ceiling) { *shift = ceiling; } return CLIMB_RESULT_NEG; } return CLIMB_RESULT_NONE; } if (height > 0) { if (*shift < 0) { return CLIMB_RESULT_NONE; } if (height > *shift) { *shift = height; } } room_num = item->room_num; sample_pos = (XYZ_32) { x, item_height + y, z }; Room_GetSector(sample_pos, &room_num); sample_pos.x = x2; sample_pos.z = z2; sector = Room_GetSector(sample_pos, &room_num); ceiling = Room_GetCeiling(sector, sample_pos); if (ceiling == NO_HEIGHT) { return CLIMB_RESULT_POS; } ceiling -= y; if (ceiling <= height) { return CLIMB_RESULT_POS; } if (ceiling >= M_CLIMB_HEIGHT) { return CLIMB_RESULT_POS; } if (ceiling > M_CLIMB_HEIGHT - M_CLIMB_SHIFT) { if (*shift > 0) { return hang ? CLIMB_RESULT_NEG : CLIMB_RESULT_NONE; } *shift = ceiling - M_CLIMB_HEIGHT; return CLIMB_RESULT_POS; } return hang ? CLIMB_RESULT_NEG : CLIMB_RESULT_NONE; } static bool M_TestClimbStance(ITEM *const item, COLL_INFO *const coll) { int32_t shift_r; const M_CLIMB_RESULT result_r = M_TestClimbPos( item, coll->radius, coll->radius + M_CLIMB_WIDTH_RIGHT, -700, STEP_L * 2, &shift_r); if (result_r != CLIMB_RESULT_POS) { return false; } int32_t shift_l; const M_CLIMB_RESULT result_l = M_TestClimbPos( item, coll->radius, -(coll->radius + M_CLIMB_WIDTH_LEFT), -700, STEP_L * 2, &shift_l); if (result_l != CLIMB_RESULT_POS) { return false; } int32_t shift = 0; if (shift_r) { if (shift_l) { if ((shift_r < 0) != (shift_l < 0)) { return false; } if (shift_r < 0 && shift_l < shift_r) { shift = shift_l; } else if (shift_r > 0 && shift_l > shift_r) { shift = shift_l; } else { shift = shift_r; } } else { shift = shift_r; } } else if (shift_l) { shift = shift_l; } item->pos.y += shift; return true; } static bool M_TestHangStop( const ITEM *const item, const COLL_INFO *const coll, const bool front_floor, int32_t *const height_diff) { const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item); *height_diff = coll->side_front.floor - bounds->min.y; return ABS(coll->side_left2.floor - coll->side_right2.floor) >= SLOPE_DIF || coll->side_mid.ceiling >= 0 || coll->coll_type != COLL_FRONT || front_floor || coll->hit_static || *height_diff < -SLOPE_DIF || *height_diff > SLOPE_DIF; } void Lara_Col_HangTest(ITEM *const item, COLL_INFO *const coll) { coll->bad_pos = NO_BAD_POS; coll->bad_neg = NO_BAD_NEG; coll->bad_ceiling = 0; Lara_Col_GetInfo(item, coll); const bool flag = coll->side_front.floor < 200; item->gravity = false; item->fall_speed = 0; LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y; const DIRECTION dir = Math_GetDirection(item->rot.y); switch (dir) { case DIR_NORTH: item->pos.z += M_HANG_SHIFT; break; case DIR_EAST: item->pos.x += M_HANG_SHIFT; break; case DIR_SOUTH: item->pos.z -= M_HANG_SHIFT; break; case DIR_WEST: item->pos.x -= M_HANG_SHIFT; break; default: break; } coll->bad_pos = NO_BAD_POS; coll->bad_neg = -STEPUP_HEIGHT; coll->bad_ceiling = 0; Lara_Col_GetInfo(item, coll); if (lara->climb_status) { if (!g_Input.action || item->hit_points <= 0) { XYZ_32 pos = { .x = 0, .y = 0, .z = 0, }; Collide_GetJointAbsPosition(item, &pos, 0); if (dir == DIR_NORTH || dir == DIR_SOUTH) { item->pos.x = pos.x; } else { item->pos.z = pos.z; } item->goal_anim_state = LS(LS_JUMP_FORWARD); item->current_anim_state = LS(LS_JUMP_FORWARD); Item_SwitchToAnim(item, LA(LA_FALL_START), 0); item->pos.y += STEP_L; item->gravity = true; item->speed = 2; item->fall_speed = 1; lara->gun_status = LGS_ARMLESS; return; } if (!Lara_Col_TestLadderHang(item, coll)) { int32_t height_diff = 0; if ((item->current_anim_state != LS(LS_SHIMMY_LEFT) && item->current_anim_state != LS(LS_SHIMMY_RIGHT)) || M_TestHangStop(item, coll, flag, &height_diff)) { item->pos = coll->old; item->goal_anim_state = LS(LS_HANG); item->current_anim_state = LS(LS_HANG); Item_SwitchToAnim(item, LA(LA_REACH_TO_HANG), M_LF_HANG); } return; } if (Item_TestAnimEqual(item, LA(LA_REACH_TO_HANG)) && Item_TestFrameEqual(item, M_LF_HANG) && M_TestClimbStance(item, coll)) { item->goal_anim_state = LS(LS_CLIMB_STANCE); } return; } if (!g_Input.action || item->hit_points <= 0 || coll->side_front.floor > 0) { item->goal_anim_state = LS(LS_JUMP_UP); item->current_anim_state = LS(LS_JUMP_UP); Item_SwitchToAnim(item, LA(LA_JUMP_UP), M_LF_STOP_HANG); const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item); if (g_Config.gameplay.enable_swing_cancel && item->hit_points > 0) { item->pos.y += bounds->max.y; } else { item->pos.y += coll->side_front.floor - bounds->min.y + 2; } item->pos.x += coll->shift.x; item->pos.z += coll->shift.z; item->gravity = true; item->speed = 2; item->fall_speed = 1; lara->gun_status = LGS_ARMLESS; return; } int32_t height_diff = 0; if (M_TestHangStop(item, coll, flag, &height_diff)) { item->pos = coll->old; if (item->current_anim_state == LS(LS_SHIMMY_LEFT) || item->current_anim_state == LS(LS_SHIMMY_RIGHT)) { item->goal_anim_state = LS(LS_HANG); item->current_anim_state = LS(LS_HANG); Item_SwitchToAnim(item, LA(LA_REACH_TO_HANG), M_LF_HANG); } return; } switch (dir) { case DIR_NORTH: case DIR_SOUTH: item->pos.z += coll->shift.z; break; case DIR_EAST: case DIR_WEST: item->pos.x += coll->shift.x; break; default: break; } if (g_TRVersion >= 2 || (height_diff >= -STEP_L && height_diff <= STEP_L)) { item->pos.y += height_diff; } } static bool M_TestLadderRelease(ITEM *const item) { item->gravity = false; item->fall_speed = 0; if (g_Input.action && item->hit_points > 0) { return false; } item->goal_anim_state = LS(LS_JUMP_FORWARD); item->current_anim_state = LS(LS_JUMP_FORWARD); Item_SwitchToAnim(item, LA(LA_FALL_START), 0); item->gravity = true; item->speed = 2; item->fall_speed = 1; LARA_INFO *const lara = Lara_GetLaraInfo(); lara->gun_status = LGS_ARMLESS; return true; } static M_CLIMB_RESULT M_TestClimbUpPos( const ITEM *const item, const int32_t front, const int32_t right, int32_t *const shift, int32_t *const ledge) { const int32_t y = item->pos.y - M_CLIMB_HEIGHT - STEP_L; int32_t x; int32_t z; int32_t x_front = 0; int32_t z_front = 0; switch (Math_GetDirection(item->rot.y)) { case DIR_NORTH: x = item->pos.x + right; z = item->pos.z + front; z_front = M_HANG_SHIFT; break; case DIR_EAST: x = item->pos.x + front; z = item->pos.z - right; x_front = M_HANG_SHIFT; break; case DIR_SOUTH: x = item->pos.x - right; z = item->pos.z - front; z_front = -M_HANG_SHIFT; break; case DIR_WEST: z = item->pos.z + right; x = item->pos.x - front; x_front = -M_HANG_SHIFT; break; default: x = front; z = front; break; } *shift = 0; const SECTOR *sector; int32_t height; int32_t ceiling; int16_t room_num = item->room_num; XYZ_32 sample_pos = { x, y, z }; sector = Room_GetSector(sample_pos, &room_num); ceiling = Room_GetCeiling(sector, sample_pos) + STEP_L - y; if (ceiling > M_CLIMB_SHIFT) { return CLIMB_RESULT_NONE; } if (ceiling > 0) { *shift = ceiling; } const int32_t x2 = x + x_front; const int32_t z2 = z + z_front; sample_pos.x = x2; sample_pos.z = z2; sector = Room_GetSector(sample_pos, &room_num); height = Room_GetHeightEx(sector, sample_pos, true, NO_ITEM); if (height == NO_HEIGHT) { *ledge = NO_HEIGHT; return CLIMB_RESULT_POS; } height -= y; *ledge = height; if (height > STEP_L / 2) { ceiling = Room_GetCeiling(sector, sample_pos) - y; if (ceiling >= M_CLIMB_HEIGHT) { return CLIMB_RESULT_POS; } if (height - ceiling > LARA_HEIGHT) { *shift = height; return CLIMB_RESULT_NEG; } if (g_Config.gameplay.enable_crawling && height - ceiling >= M_CLIMB_HEIGHT) { return CLIMB_RESULT_CRAWL; } return CLIMB_RESULT_NONE; } if (height > 0 && height > *shift) { *shift = height; } room_num = item->room_num; sample_pos = (XYZ_32) { x, y + M_CLIMB_HEIGHT, z }; Room_GetSector(sample_pos, &room_num); sample_pos.x = x2; sample_pos.z = z2; sector = Room_GetSector(sample_pos, &room_num); ceiling = Room_GetCeiling(sector, sample_pos) - y; if (ceiling <= height) { return CLIMB_RESULT_POS; } if (ceiling >= M_CLIMB_HEIGHT) { return CLIMB_RESULT_POS; } return CLIMB_RESULT_NONE; } static bool M_TestLedgeJump(const ITEM *const item, const COLL_INFO *const coll) { if (!g_Input.jump || !(g_Input.forward ^ g_Input.back) || (g_Input.forward && g_Input.slow) || !g_Config.gameplay.enable_ledge_jumps || !Lara_State_IsResponsive(LA_REACH_TO_HANG)) { return false; } // Lara needs sufficient space above to avoid the animation pushing her into // the ceiling. const int32_t jump_height = g_Input.forward ? M_LEDGE_JUMP_HEIGHT_UP : M_LEDGE_JUMP_HEIGHT_BACK; if (coll->side_mid.ceiling >= -jump_height) { return false; } // Test for a solid surface in front of Lara to push against. const XYZ_32 pos = { .x = item->pos.x + ((Math_Sin(item->rot.y) * STEP_L) >> W2V_SHIFT), .z = item->pos.z + ((Math_Cos(item->rot.y) * STEP_L) >> W2V_SHIFT), .y = item->pos.y, }; int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); const int32_t height = Room_GetHeight(sector, pos); const int32_t ceiling = Room_GetCeiling(sector, pos); return height == NO_HEIGHT || height < pos.y || (ceiling - pos.y) >= -M_LEDGE_JUMP_PUSH_HEIGHT; } static void M_Hang(ITEM *const item, COLL_INFO *const coll) { Lara_Col_HangTest(item, coll); if (item->goal_anim_state != LS(LS_HANG)) { return; } LARA_INFO *const lara = Lara_GetLaraInfo(); if (!lara->climb_status && M_TestLedgeJump(item, coll)) { item->goal_anim_state = LS(g_Input.forward ? LS_JUMP_UP : LS_JUMP_BACK); return; } if (g_Input.forward) { if (coll->side_front.floor > -850 && coll->side_front.floor < -650 && coll->side_front.floor - coll->side_front.ceiling >= 0 && coll->side_left2.floor - coll->side_left2.ceiling >= 0 && coll->side_right2.floor - coll->side_right2.ceiling >= 0 && !coll->hit_static) { item->goal_anim_state = LS(g_Input.slow ? LS_GYMNAST : LS_PULL_UP); return; } else if ( lara->climb_status && Item_TestAnimEqual(item, LA(LA_REACH_TO_HANG)) && Item_TestFrameEqual(item, M_LF_HANG) && coll->side_mid.ceiling <= -256) { item->goal_anim_state = LS(LS_HANG); item->current_anim_state = LS(LS_HANG); Item_SwitchToAnim(item, LA(LA_LADDER_UP_HANGING), 0); return; } } if (g_Config.gameplay.enable_crawling && (g_Input.forward || g_Input.crouch) && coll->side_front.floor > -850 && coll->side_front.floor < -650 && coll->side_front.floor - coll->side_front.ceiling >= -256 && coll->side_left2.floor - coll->side_left2.ceiling >= -256 && coll->side_right2.floor - coll->side_right2.ceiling >= -256 && !coll->hit_static) { item->goal_anim_state = LS(LS_CLIMB_TO_CRAWL); item->required_anim_state = LS(LS_CROUCH_IDLE); lara->crouching = true; } else if ( g_Input.back && lara->climb_status && Item_TestAnimEqual(item, LA(LA_REACH_TO_HANG)) && Item_TestFrameEqual(item, M_LF_HANG)) { item->goal_anim_state = LS(LS_HANG); item->current_anim_state = LS(LS_HANG); Item_SwitchToAnim(item, LA(LA_LADDER_DOWN_HANGING), 0); } } static void M_Shimmy(ITEM *const item, COLL_INFO *const coll) { const int32_t angle = item->current_anim_state == LS(LS_SHIMMY_LEFT) ? -DEG_90 : DEG_90; LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y + angle; Lara_Col_HangTest(item, coll); lara->move_angle = item->rot.y + angle; } static void M_StanceLadder(ITEM *const item, COLL_INFO *const coll) { if (M_TestLadderRelease(item) || !Item_TestAnimEqual(item, LA(LA_LADDER_IDLE))) { return; } LARA_INFO *const lara = Lara_GetLaraInfo(); if (g_Input.forward) { if (item->goal_anim_state == LS(LS_PULL_UP)) { return; } item->goal_anim_state = LS(LS_CLIMB_STANCE); int32_t shift_r = 0; int32_t ledge_r = 0; M_CLIMB_RESULT result_r = M_TestClimbUpPos( item, coll->radius, coll->radius + M_CLIMB_WIDTH_RIGHT, &shift_r, &ledge_r); int32_t shift_l = 0; int32_t ledge_l = 0; M_CLIMB_RESULT result_l = M_TestClimbUpPos( item, coll->radius, -(coll->radius + M_CLIMB_WIDTH_LEFT), &shift_l, &ledge_l); if (result_r == CLIMB_RESULT_NONE || result_l == CLIMB_RESULT_NONE) { return; } if (result_r == CLIMB_RESULT_NEG || result_l == CLIMB_RESULT_NEG || result_r == CLIMB_RESULT_CRAWL || result_l == CLIMB_RESULT_CRAWL) { if (ABS(ledge_l - ledge_r) > 120) { return; } if (result_r == CLIMB_RESULT_NEG && result_l == CLIMB_RESULT_NEG) { item->goal_anim_state = LS(LS_PULL_UP); item->pos.y += (ledge_l + ledge_r) / 2 - STEP_L; } else { item->goal_anim_state = LS(LS_CLIMB_TO_CRAWL); item->required_anim_state = LS(LS_CROUCH_IDLE); lara->crouching = true; } return; } int32_t shift = shift_l; if (shift_r) { if (shift_l) { if ((shift_r < 0) != (shift_l < 0)) { return; } if (shift_r > 0 && shift_r > shift_l) { shift = shift_r; } else if (shift_r < 0 && shift_r < shift_l) { shift = shift_r; } } else { shift = shift_r; } } item->goal_anim_state = LS(LS_CLIMBING); item->pos.y += shift; } else if (g_Input.back) { if (item->goal_anim_state == LS(LS_HANG)) { return; } item->goal_anim_state = LS(LS_CLIMB_STANCE); item->pos.y += STEP_L; int32_t shift_r = 0; const M_CLIMB_RESULT result_r = M_TestClimbPos( item, coll->radius, coll->radius + M_CLIMB_WIDTH_RIGHT, -M_CLIMB_HEIGHT, M_CLIMB_HEIGHT, &shift_r); int32_t shift_l = 0; const M_CLIMB_RESULT result_l = M_TestClimbPos( item, coll->radius, -(coll->radius + M_CLIMB_WIDTH_LEFT), -M_CLIMB_HEIGHT, M_CLIMB_HEIGHT, &shift_l); item->pos.y -= STEP_L; if (result_r == CLIMB_RESULT_NONE || result_l == CLIMB_RESULT_NONE) { return; } int32_t shift = shift_l; if (shift_r && shift_l) { if ((shift_r < 0) != (shift_l < 0)) { return; } if (shift_r < 0 && shift_r < shift_l) { shift = shift_r; } else if (shift_r > 0 && shift_r > shift_l) { shift = shift_r; } } if (result_r == CLIMB_RESULT_POS && result_l == CLIMB_RESULT_POS) { item->goal_anim_state = LS(LS_CLIMB_DOWN); item->pos.y += shift; } else { item->goal_anim_state = LS(LS_HANG); } } } static void M_SideLadder(ITEM *const item, COLL_INFO *const coll) { if (M_TestLadderRelease(item)) { return; } LARA_INFO *const lara = Lara_GetLaraInfo(); int32_t right; if (item->current_anim_state == LS(LS_CLIMB_LEFT)) { lara->move_angle = item->rot.y - DEG_90; right = -(coll->radius + M_CLIMB_WIDTH_LEFT); } else { lara->move_angle = item->rot.y + DEG_90; right = coll->radius + M_CLIMB_WIDTH_RIGHT; } int32_t shift; const M_CLIMB_RESULT result = M_TestClimbPos( item, coll->radius, right, -M_CLIMB_HEIGHT, M_CLIMB_HEIGHT, &shift); if (result == CLIMB_RESULT_POS) { if (g_Input.left) { item->goal_anim_state = LS(LS_CLIMB_LEFT); } else if (g_Input.right) { item->goal_anim_state = LS(LS_CLIMB_RIGHT); } else { item->goal_anim_state = LS(LS_CLIMB_STANCE); } item->pos.y += shift; return; } if (result != CLIMB_RESULT_NONE) { item->goal_anim_state = LS(LS_HANG); do { Item_Animate(item); } while (item->current_anim_state != LS(LS_HANG)); item->pos.x = coll->old.x; item->pos.z = coll->old.z; return; } item->pos.x = coll->old.x; item->pos.z = coll->old.z; item->goal_anim_state = LS(LS_CLIMB_STANCE); item->current_anim_state = LS(LS_CLIMB_STANCE); if (coll->old_anim_state == LS(LS_CLIMB_STANCE)) { item->frame_num = coll->old_frame_num; item->anim_num = coll->old_anim_num; Lara_Animate(item); } else { Item_SwitchToAnim(item, LA(LA_LADDER_IDLE), 0); } } static void M_UpLadder(ITEM *const item, COLL_INFO *const coll) { if (M_TestLadderRelease(item) || !Item_TestAnimEqual(item, LA(LA_LADDER_UP))) { return; } int32_t yshift; if (Item_TestFrameEqual(item, 0)) { yshift = 0; } else if ( Item_TestFrameRange( item, M_LF_CLIMB_L_SHIFT_START, M_LF_CLIMB_L_SHIFT_END)) { yshift = -STEP_L; } else if (Item_TestFrameEqual(item, M_LF_CLIMB_R_SHIFT)) { yshift = -STEP_L * 2; } else { return; } item->pos.y += yshift - STEP_L; int32_t shift_r = 0; int32_t ledge_r = 0; M_CLIMB_RESULT result_r = M_TestClimbUpPos( item, coll->radius, coll->radius + M_CLIMB_WIDTH_RIGHT, &shift_r, &ledge_r); int32_t shift_l = 0; int32_t ledge_l = 0; M_CLIMB_RESULT result_l = M_TestClimbUpPos( item, coll->radius, -(coll->radius + M_CLIMB_WIDTH_LEFT), &shift_l, &ledge_l); item->pos.y += STEP_L; if (result_r == CLIMB_RESULT_NONE || result_l == CLIMB_RESULT_NONE || !g_Input.forward) { item->goal_anim_state = LS(LS_CLIMB_STANCE); if (yshift) { Lara_Animate(item); } return; } LARA_INFO *const lara = Lara_GetLaraInfo(); if (result_r == CLIMB_RESULT_NEG || result_l == CLIMB_RESULT_NEG || result_r == CLIMB_RESULT_CRAWL || result_l == CLIMB_RESULT_CRAWL) { item->goal_anim_state = LS(LS_CLIMB_STANCE); Lara_Animate(item); if (ABS(ledge_l - ledge_r) <= 120) { if (result_r == CLIMB_RESULT_NEG || result_l == CLIMB_RESULT_NEG) { item->goal_anim_state = LS(LS_PULL_UP); item->pos.y += (ledge_r + ledge_l) / 2 - STEP_L; } else { item->goal_anim_state = LS(LS_CLIMB_TO_CRAWL); item->required_anim_state = LS(LS_CROUCH_IDLE); lara->crouching = true; } } return; } item->goal_anim_state = LS(LS_CLIMBING); item->pos.y -= yshift; } static void M_DownLadder(ITEM *const item, COLL_INFO *const coll) { if (M_TestLadderRelease(item) || !Item_TestAnimEqual(item, LA(LA_LADDER_DOWN))) { return; } int32_t yshift; if (Item_TestFrameEqual(item, 0)) { yshift = 0; } else if ( Item_TestFrameRange( item, M_LF_CLIMB_L_SHIFT_START, M_LF_CLIMB_L_SHIFT_END)) { yshift = STEP_L; } else if (Item_TestFrameEqual(item, M_LF_CLIMB_R_SHIFT)) { yshift = STEP_L * 2; } else { return; } item->pos.y += yshift + STEP_L; int32_t shift_r = 0; const M_CLIMB_RESULT result_r = M_TestClimbPos( item, coll->radius, coll->radius + M_CLIMB_WIDTH_RIGHT, -M_CLIMB_HEIGHT, M_CLIMB_HEIGHT, &shift_r); int32_t shift_l = 0; const M_CLIMB_RESULT result_l = M_TestClimbPos( item, coll->radius, -(coll->radius + M_CLIMB_WIDTH_LEFT), -M_CLIMB_HEIGHT, M_CLIMB_HEIGHT, &shift_l); item->pos.y -= STEP_L; if (result_r == CLIMB_RESULT_NONE || result_l == CLIMB_RESULT_NONE || !g_Input.back) { item->goal_anim_state = LS(LS_CLIMB_STANCE); if (yshift != 0) { Lara_Animate(item); } return; } #if 0 int32_t shift = shift_l; #endif if (shift_r && shift_l) { if ((shift_r < 0) != (shift_l < 0)) { item->goal_anim_state = LS(LS_CLIMB_STANCE); Lara_Animate(item); return; } #if 0 if (shift_r < 0 && shift_r < shift_l) { shift = shift_r; } else if (shift_r > 0 && shift_r > shift_l) { shift = shift_r; } #endif } if (result_r == CLIMB_RESULT_NEG || result_l == CLIMB_RESULT_NEG) { Item_SwitchToAnim(item, LA(LA_LADDER_IDLE), 0); item->current_anim_state = LS(LS_CLIMB_STANCE); item->goal_anim_state = LS(LS_HANG); Lara_Animate(item); return; } item->goal_anim_state = LS(LS_CLIMB_DOWN); item->pos.y -= yshift; } bool Lara_Col_TestVault(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); if (coll->coll_type != COLL_FRONT || !g_Input.action || lara->gun_status != LGS_ARMLESS) { return false; } const DIRECTION dir = Math_GetDirectionCone(item->rot.y, M_VAULT_ANGLE); if (dir == DIR_UNKNOWN) { return false; } const int16_t angle = Math_DirectionToAngle(dir); const int32_t left_floor = coll->side_left2.floor; const int32_t left_ceiling = coll->side_left2.ceiling; const int32_t right_floor = coll->side_right2.floor; const int32_t right_ceiling = coll->side_right2.ceiling; const int32_t front_floor = coll->side_front.floor; const int32_t front_ceiling = coll->side_front.ceiling; const bool slope = ABS(left_floor - right_floor) >= SLOPE_DIF; const int32_t mid = STEP_L / 2; const ROOM *const room = Room_Get(item->room_num); if (front_floor >= -STEP_L * 2 - mid && front_floor <= -STEP_L * 2 + mid) { if (slope || front_floor - front_ceiling < 0 || left_floor - left_ceiling < 0 || right_floor - right_ceiling < 0 || (room->flags.swamp && lara->water_surface_dist < -768)) { return false; } item->goal_anim_state = LS(LS_STOP); item->current_anim_state = LS(LS_PULL_UP); Item_SwitchToAnim(item, LA(LA_CLIMB_2CLICK), 0); item->pos.y += front_floor + STEP_L * 2; lara->gun_status = LGS_HANDS_BUSY; } else if ( front_floor >= -STEP_L * 3 - mid && front_floor <= -STEP_L * 3 + mid) { if (slope || front_floor - front_ceiling < 0 || left_floor - left_ceiling < 0 || right_floor - right_ceiling < 0 || (room->flags.swamp && lara->water_surface_dist < -768)) { return false; } item->goal_anim_state = LS(LS_STOP); item->current_anim_state = LS(LS_PULL_UP); Item_SwitchToAnim(item, LA(LA_CLIMB_3CLICK), 0); item->pos.y += front_floor + STEP_L * 3; lara->gun_status = LGS_HANDS_BUSY; } else if ( !lara->climb_status && front_floor - coll->side_mid.ceiling < M_VAULT_GAP) { return false; } else if ( !slope && front_floor >= -STEP_L * 7 - mid && front_floor <= -STEP_L * 4 + mid) { if (room->flags.swamp) { return false; } item->goal_anim_state = LS(LS_JUMP_UP); item->current_anim_state = LS(LS_STOP); Item_SwitchToAnim(item, LA(LA_STAND_STILL), 0); lara->calc_fall_speed = -(Math_Sqrt(-2 * GRAVITY * (front_floor + 800)) + 3); Lara_Animate(item); } else if ( lara->climb_status && front_floor <= -1920 && lara->water_status != LWS_WADE && left_floor <= -STEP_L * 8 + mid && right_floor <= -STEP_L * 8 && coll->side_mid.ceiling <= -STEP_L * 8 + mid + LARA_HEIGHT) { item->goal_anim_state = LS(LS_JUMP_UP); item->current_anim_state = LS(LS_STOP); Item_SwitchToAnim(item, LA(LA_STAND_STILL), 0); lara->calc_fall_speed = -116; Lara_Animate(item); } else if ( lara->climb_status && (front_floor < -STEP_L * 4 || front_ceiling >= LARA_HEIGHT - STEP_L) && coll->side_mid.ceiling <= -STEP_L * 5 + LARA_HEIGHT) { Lara_Col_Shift(coll); if (M_TestClimbStance(item, coll)) { item->goal_anim_state = LS(LS_CLIMB_STANCE); item->current_anim_state = LS(LS_STOP); Item_SwitchToAnim(item, LA(LA_STAND_STILL), 0); Lara_Animate(item); item->rot.y = angle; lara->gun_status = LGS_HANDS_BUSY; lara->sprinting = false; return true; } return false; } else { return false; } item->rot.y = angle; Lara_Col_Shift(coll); lara->sprinting = false; lara->crouching = false; return true; } bool Lara_Col_TestLadderHang(ITEM *const item, const COLL_INFO *const coll) { const LARA_INFO *const lara = Lara_GetLaraInfo(); if (!lara->climb_status || item->fall_speed < 0) { return false; } const DIRECTION dir = Math_GetDirection(item->rot.y); switch (dir) { case DIR_NORTH: case DIR_SOUTH: item->pos.z += coll->shift.z; break; case DIR_EAST: case DIR_WEST: item->pos.x += coll->shift.x; break; default: break; } const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item); const int32_t y = bounds->min.y; const int32_t h = bounds->max.y - y; int32_t shift; if (M_TestClimbPos(item, coll->radius, coll->radius, y, h, &shift) == CLIMB_RESULT_NONE) { return false; } if (M_TestClimbPos(item, coll->radius, -coll->radius, y, h, &shift) == CLIMB_RESULT_NONE) { return false; } const M_CLIMB_RESULT result = M_TestClimbPos(item, coll->radius, 0, y, h, &shift); if (result == CLIMB_RESULT_NEG) { item->pos.y += shift; } return result != CLIMB_RESULT_NONE; } // clang-format off REGISTER_LARA_COL(LS_HANG, M_Hang) REGISTER_LARA_COL(LS_SHIMMY_LEFT, M_Shimmy) REGISTER_LARA_COL(LS_SHIMMY_RIGHT, M_Shimmy) REGISTER_LARA_COL(LS_CLIMB_STANCE, M_StanceLadder) REGISTER_LARA_COL(LS_CLIMB_LEFT, M_SideLadder) REGISTER_LARA_COL(LS_CLIMB_RIGHT, M_SideLadder) REGISTER_LARA_COL(LS_CLIMBING, M_UpLadder) REGISTER_LARA_COL(LS_CLIMB_DOWN, M_DownLadder) // clang-format on ================================================ FILE: src/trx/game/lara/col/crouch.c ================================================ #include #include #include #include #include #include #include #include #include // clang-format off #define M_CROUCH_RADIUS 200 #define M_CRAWL_BACK_RADIUS 250 #define M_CRAWL_BAD_POS 255 #define M_CRAWL_BAD_NEG -255 #define M_CRAWL_BAD_CEILING 400 #define M_CROUCH_CEILING_THRESHOLD -362 #define M_CRAWL_TO_HANG_RADIUS 200 #define M_CRAWL_TO_HANG_HEIGHT 870 #define M_CRAWL_TO_HANG_XZ_OFFSET 100 #define M_CRAWL_TO_HANG_FALL_SPEED 512 #define M_CRAWL_TO_HANG_BAD_CEILING ((STEP_L * 3) / 4) // = 192 #define M_CRAWL_TO_HANG_FALL_FRAME 9 #define M_CRAWL_TILT_RADIUS 140 #define M_CRAWL_TILT_HEIGHT 238 #define M_CRAWL_TILT_RATE (DEG_1 * 3) // = 546 #define M_CRAWL_TILT_MAX DEG_45 // clang-format on static bool M_DeflectEdgeCrawl(ITEM *const item, COLL_INFO *const coll) { switch (coll->coll_type) { case COLL_FRONT: case COLL_TOP_FRONT: Lara_Col_Shift(coll); item->speed = 0; item->gravity = false; return true; case COLL_LEFT: Lara_Col_Shift(coll); item->rot.y += LARA_TURN_UNDO; break; case COLL_RIGHT: Lara_Col_Shift(coll); item->rot.y -= LARA_TURN_UNDO; break; default: break; } return false; } static bool M_HasStaticBehind(const ITEM *const item, const int16_t angle) { COLL_INFO test = { .radius = 50, }; const int32_t x = item->pos.x + ((Math_Sin(angle) * 512) >> W2V_SHIFT); const int32_t z = item->pos.z + ((Math_Cos(angle) * 512) >> W2V_SHIFT); return Collide_CollideStaticObjects( &test, x, item->pos.y, z, item->room_num, 300); } static bool M_IsBadDestination(const ITEM *const item, const int16_t angle) { XYZ_32 pos = XYZ_32_OffsetYaw(item->pos, angle, STEP_L); int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); pos.y = Room_GetHeight(sector, pos) - STEP_L; Room_GetSector(pos, &room_num); const ROOM *const room = Room_Get(room_num); return room->flags.swamp || room->flags.underwater; } static void M_Crouch(ITEM *const item, COLL_INFO *const coll) { item->gravity = false; item->fall_speed = 0; LARA_INFO *const lara = Lara_GetLaraInfo(); lara->is_crouched = true; lara->move_angle = item->rot.y; coll->facing = lara->move_angle; coll->radius = M_CROUCH_RADIUS; coll->bad_pos = STEPUP_HEIGHT; coll->bad_neg = -STEPUP_HEIGHT; coll->bad_ceiling = 0; coll->slopes_are_walls = 1; Collide_GetCollisionInfo( coll, item->pos.x, item->pos.y, item->pos.z, item->room_num, -LARA_HEIGHT_CROUCH); if (Lara_Col_Fallen(item, coll)) { lara->gun_status = LGS_ARMLESS; return; } lara->keep_crouched = coll->side_mid.ceiling >= M_CROUCH_CEILING_THRESHOLD; Lara_Col_Shift(coll); item->pos.y += coll->side_mid.floor; const bool crouch_active = g_Config.gameplay.enable_toggle_crouch ? !(lara->crouching && g_InputDB.crouch) : g_Input.crouch; if ((!crouch_active || lara->water_status == LWS_WADE) && !lara->keep_crouched && Item_TestAnimEqual(item, LA(LA_CROUCH_IDLE))) { lara->crouching = false; item->goal_anim_state = LS(LS_STOP); } else if (g_Config.gameplay.enable_responsive_crawl) { if (g_Input.left) { item->goal_anim_state = LS(LS_CROUCH_TURN_LEFT); } else if (g_Input.right) { item->goal_anim_state = LS(LS_CROUCH_TURN_RIGHT); } } } static void M_CrouchRoll(ITEM *const item, COLL_INFO *const coll) { item->gravity = false; item->fall_speed = 0; LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y; coll->facing = lara->move_angle; coll->radius = M_CROUCH_RADIUS; coll->bad_pos = STEPUP_HEIGHT; coll->bad_neg = -STEPUP_HEIGHT; coll->bad_ceiling = 0; coll->slopes_are_walls = 1; Collide_GetCollisionInfo( coll, item->pos.x, item->pos.y, item->pos.z, item->room_num, -LARA_HEIGHT_CROUCH); if (Lara_Col_Fallen(item, coll)) { lara->gun_status = LGS_ARMLESS; } else if (!Lara_Col_TestSlide(item, coll)) { lara->keep_crouched = coll->side_mid.ceiling >= M_CROUCH_CEILING_THRESHOLD; if (coll->side_mid.floor < coll->bad_neg || coll->side_front.floor > coll->bad_pos || M_IsBadDestination(item, lara->move_angle)) { item->pos = coll->old; return; } Lara_Col_Shift(coll); if (coll->coll_type == COLL_TOP || coll->coll_type == COLL_CLAMP) { item->pos = coll->old; item->speed = 0; } else { item->pos.y += coll->side_mid.floor; } } } static void M_CrouchTurn(ITEM *const item, COLL_INFO *const coll) { item->gravity = false; item->fall_speed = 0; LARA_INFO *const lara = Lara_GetLaraInfo(); lara->is_crouched = true; lara->move_angle = item->rot.y; coll->facing = lara->move_angle; coll->radius = M_CROUCH_RADIUS; coll->bad_pos = STEPUP_HEIGHT; coll->bad_neg = -STEPUP_HEIGHT; coll->bad_ceiling = 0; coll->slopes_are_walls = 1; Collide_GetCollisionInfo( coll, item->pos.x, item->pos.y, item->pos.z, item->room_num, LARA_HEIGHT_CROUCH); if (Lara_Col_Fallen(item, coll)) { lara->gun_status = LGS_ARMLESS; return; } if (Lara_Col_TestSlide(item, coll)) { return; } lara->keep_crouched = coll->side_mid.ceiling >= M_CROUCH_CEILING_THRESHOLD; Lara_Col_Shift(coll); item->pos.y += coll->side_mid.floor; } static void M_CrawlIdle(ITEM *const item, COLL_INFO *const coll) { item->gravity = false; item->fall_speed = 0; if (item->goal_anim_state == LS(LS_CRAWL_TO_CLIMB)) { return; } LARA_INFO *const lara = Lara_GetLaraInfo(); lara->is_crouched = true; lara->move_angle = item->rot.y; coll->facing = lara->move_angle; coll->radius = M_CROUCH_RADIUS; coll->bad_pos = M_CRAWL_BAD_POS; coll->bad_neg = M_CRAWL_BAD_NEG; coll->bad_ceiling = M_CRAWL_BAD_CEILING; coll->slopes_are_walls = 1; coll->slopes_are_pits = 1; Collide_GetCollisionInfo( coll, item->pos.x, item->pos.y, item->pos.z, item->room_num, LARA_HEIGHT_CROUCH); Lara_Col_CrawlTilt(item); if (Lara_Col_Fallen(item, coll)) { lara->gun_status = LGS_ARMLESS; return; } lara->keep_crouched = coll->side_mid.ceiling >= M_CROUCH_CEILING_THRESHOLD; Lara_Col_Shift(coll); item->pos.y += coll->side_mid.floor; const bool crouch_active = g_Config.gameplay.enable_toggle_crouch ? lara->crouching : g_Input.crouch; if ((!crouch_active && !lara->keep_crouched) || g_Input.draw || (g_Config.gameplay.enable_toggle_crouch && !g_Input.forward && g_InputDB.crouch)) { item->goal_anim_state = LS(LS_CROUCH_IDLE); return; } bool allow_movement = Item_TestAnimEqual(item, LA(LA_CRAWL_IDLE)) || Item_TestAnimEqual(item, LA(LA_CROUCH_TO_CRAWL_END)); if (g_Config.gameplay.enable_responsive_crawl) { allow_movement |= Item_TestAnimEqual(item, LA(LA_CRAWL_FORWARD_TO_IDLE_END_LEFT)) || Item_TestAnimEqual(item, LA(LA_CRAWL_FORWARD_TO_IDLE_END_RIGHT)); } if (!allow_movement) { return; } if (g_Input.forward) { const int16_t h = Lara_FloorFront(item, item->rot.y, 256); if (h < 255 && h > -255 && Room_GetHeightType() != HT_BIG_SLOPE) { item->goal_anim_state = LS(LS_CRAWL_FORWARD); } } else if (g_Input.back) { int32_t h = Lara_CeilingFront(item, item->rot.y, -300, LARA_HEIGHT); if (h == NO_HEIGHT || h > 256) { return; } h = Lara_FloorFront(item, item->rot.y, -300); if (h < 255 && h > -255 && Room_GetHeightType() != HT_BIG_SLOPE) { item->goal_anim_state = LS(LS_CRAWL_BACK); return; } if (g_Input.action && h > 768 && !M_HasStaticBehind(item, item->rot.y + DEG_180)) { const XYZ_32 old_pos = item->pos; const XYZ_16 old_rot = item->rot; const DIRECTION dir = Math_GetDirection(item->rot.y); switch (dir) { case DIR_NORTH: item->rot.y = 0; item->pos.z = ROUND_TO_SECTOR(item->pos.z) + 225; break; case DIR_EAST: item->rot.y = DEG_90; item->pos.x = ROUND_TO_SECTOR(item->pos.x) + 225; break; case DIR_SOUTH: item->rot.y = -DEG_180; item->pos.z = ROUND_TO_SECTOR_END(item->pos.z) - 225; break; case DIR_WEST: item->rot.y = -DEG_90; item->pos.x = ROUND_TO_SECTOR_END(item->pos.x) - 225; break; default: break; } h = Lara_FloorFront(item, item->rot.y, 0); if (h > 255 || h < -255 || Room_GetHeightType() == HT_BIG_SLOPE) { item->pos = old_pos; item->rot = old_rot; } else { item->goal_anim_state = LS(LS_CRAWL_TO_CLIMB); } } } else if (g_Input.left) { Item_SwitchToAnim(item, LA(LA_CRAWL_TURN_LEFT), 0); item->goal_anim_state = LS(LS_CRAWL_TURN_LEFT); item->current_anim_state = LS(LS_CRAWL_TURN_LEFT); } else if (g_Input.right) { Item_SwitchToAnim(item, LA(LA_CRAWL_TURN_RIGHT), 0); item->goal_anim_state = LS(LS_CRAWL_TURN_RIGHT); item->current_anim_state = LS(LS_CRAWL_TURN_RIGHT); } } static void M_CrawlForward(ITEM *const item, COLL_INFO *const coll) { item->gravity = false; item->fall_speed = 0; LARA_INFO *const lara = Lara_GetLaraInfo(); lara->is_crouched = true; lara->move_angle = item->rot.y; coll->radius = M_CROUCH_RADIUS; coll->bad_pos = M_CRAWL_BAD_POS; coll->bad_neg = M_CRAWL_BAD_NEG; coll->bad_ceiling = M_CRAWL_BAD_CEILING; coll->slopes_are_walls = 1; coll->slopes_are_pits = 1; coll->facing = lara->move_angle; Collide_GetCollisionInfo( coll, item->pos.x, item->pos.y, item->pos.z, item->room_num, -LARA_HEIGHT_CROUCH); Lara_Col_CrawlTilt(item); if (M_DeflectEdgeCrawl(item, coll) || M_IsBadDestination(item, lara->move_angle)) { item->current_anim_state = LS(LS_CRAWL_IDLE); item->goal_anim_state = LS(LS_CRAWL_IDLE); if (!Item_TestAnimEqual(item, LA(LA_CRAWL_IDLE))) { Item_SwitchToAnim(item, LA(LA_CRAWL_IDLE), 0); } } else if (Lara_Col_Fallen(item, coll)) { lara->gun_status = LGS_ARMLESS; } else { Lara_Col_Shift(coll); item->pos.y += coll->side_mid.floor; } } static void M_CrawlTurn(ITEM *const item, COLL_INFO *const coll) { Collide_GetCollisionInfo( coll, item->pos.x, item->pos.y, item->pos.z, item->room_num, LARA_HEIGHT_CROUCH); Lara_Col_CrawlTilt(item); } static void M_CrawlBack(ITEM *const item, COLL_INFO *const coll) { item->gravity = false; item->fall_speed = 0; LARA_INFO *const lara = Lara_GetLaraInfo(); lara->is_crouched = true; lara->move_angle = item->rot.y + DEG_180; coll->radius = M_CRAWL_BACK_RADIUS; coll->bad_pos = M_CRAWL_BAD_POS; coll->bad_neg = M_CRAWL_BAD_NEG; coll->bad_ceiling = M_CRAWL_BAD_CEILING; coll->slopes_are_walls = 1; coll->slopes_are_pits = 1; coll->facing = lara->move_angle; Collide_GetCollisionInfo( coll, item->pos.x, item->pos.y, item->pos.z, item->room_num, -LARA_HEIGHT_CROUCH); Lara_Col_CrawlTilt(item); if (M_DeflectEdgeCrawl(item, coll) || M_IsBadDestination(item, lara->move_angle)) { item->current_anim_state = LS(LS_CRAWL_IDLE); item->goal_anim_state = LS(LS_CRAWL_IDLE); if (!Item_TestAnimEqual(item, LA(LA_CRAWL_IDLE))) { Item_SwitchToAnim(item, LA(LA_CRAWL_IDLE), 0); } } else if (Lara_Col_Fallen(item, coll)) { lara->gun_status = LGS_ARMLESS; } else { Lara_Col_Shift(coll); item->pos.y += coll->side_mid.floor; lara->move_angle = item->rot.y; } } static void M_CrawlToClimb(ITEM *const item, COLL_INFO *const coll) { if (!Item_TestAnimEqual(item, LA(LA_CRAWL_TO_HANG_END))) { return; } item->fall_speed = M_CRAWL_TO_HANG_FALL_SPEED; item->pos.y |= 255; LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y; coll->radius = M_CRAWL_TO_HANG_RADIUS; coll->bad_pos = NO_BAD_POS; coll->bad_neg = -STEPUP_HEIGHT; coll->bad_ceiling = M_CRAWL_TO_HANG_BAD_CEILING; coll->facing = lara->move_angle; Collide_GetCollisionInfo( coll, item->pos.x, item->pos.y, item->pos.z, item->room_num, M_CRAWL_TO_HANG_HEIGHT); int32_t edge = 0; const EDGE_CATCH edge_catch = Lara_Col_TestEdgeCatch(item, coll, &edge); if (edge_catch == EDGE_CATCH_NONE || (edge_catch == EDGE_CATCH_NEG && !Lara_Col_TestLadderHang(item, coll))) { // LA_CRAWL_TO_HANG_END will loop indefinitely, so in cases where Lara // cannot grab the edge, make her fall and she will then either re-grab // it on a better position, or continue falling if the ledge is a slope. Item_SwitchToAnim(item, LA(LA_JUMP_UP), M_CRAWL_TO_HANG_FALL_FRAME); item->current_anim_state = LS(LS_JUMP_UP); item->goal_anim_state = LS(LS_JUMP_UP); item->gravity = true; item->speed = 2; item->fall_speed = 1; lara->gun_status = LGS_ARMLESS; return; } const DIRECTION dir = Math_GetDirectionCone(item->rot.y, LARA_HANG_ANGLE); if (dir == DIR_UNKNOWN) { return; } const int16_t angle = Math_DirectionToAngle(dir); const SWING_CATCH swing_catch = Lara_Col_TestHangSwingIn(item, angle); if (swing_catch == SWING_CATCH_SLOW) { lara->head_rot.x = 0; lara->head_rot.y = 0; lara->torso_rot.x = 0; lara->torso_rot.y = 0; Item_SwitchToAnim(item, LA(LA_SWING_IN_SLOW), 0); } else if (swing_catch == SWING_CATCH_FAST) { Item_SwitchToAnim(item, LA(LA_SWING_IN_FAST), 0); } else { Item_SwitchToAnim(item, LA(LA_REACH_TO_HANG), 0); } const ANIM *const anim = Item_GetAnim(item); item->current_anim_state = anim->current_anim_state; item->goal_anim_state = anim->current_anim_state; const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item); if (edge_catch == EDGE_CATCH_POS) { item->pos.y += coll->side_front.floor - bounds->min.y; switch (dir) { case DIR_NORTH: item->pos.z = ROUND_TO_SECTOR_END(item->pos.z) - M_CRAWL_TO_HANG_XZ_OFFSET; item->pos.x += coll->shift.x; break; case DIR_EAST: item->pos.x = ROUND_TO_SECTOR_END(item->pos.x) - M_CRAWL_TO_HANG_XZ_OFFSET; item->pos.z += coll->shift.z; break; case DIR_SOUTH: item->pos.z = ROUND_TO_SECTOR(item->pos.z) + M_CRAWL_TO_HANG_XZ_OFFSET; item->pos.x += coll->shift.x; break; case DIR_WEST: item->pos.x = ROUND_TO_SECTOR(item->pos.x) + M_CRAWL_TO_HANG_XZ_OFFSET; item->pos.z += coll->shift.z; break; default: break; } } else { item->pos.y = edge - bounds->min.y; } item->rot.y = angle; item->speed = 2; item->fall_speed = 1; item->gravity = true; lara->gun_status = LGS_HANDS_BUSY; } static XZ_32 M_GetWalkableTilt(const ITEM *const item, const int32_t y_pos) { const int32_t base_x = ROUND_TO_SECTOR(item->pos.x); const int32_t base_z = ROUND_TO_SECTOR(item->pos.z); const XZ_32 offsets[3] = { { 1, 1 }, { 3, 1 }, { 1, 3 }, }; int32_t heights[3] = {}; for (int32_t i = 0; i < 3; i++) { const XYZ_32 off_pos = { .x = base_x | (offsets[i].x * STEP_L - 1), .z = base_z | (offsets[i].z * STEP_L - 1), .y = y_pos, }; int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(off_pos, &room_num); heights[i] = Room_GetHeight(sector, off_pos); } return (XZ_32) { heights[1] - heights[0], heights[2] - heights[0] }; } static int16_t M_GetTilt(const int32_t delta, const int32_t radius) { int16_t rot = Math_Atan(2 * radius, delta); if ((delta > 0 && rot > 0) || (delta < 0 && rot < 0)) { rot = -rot; } return rot; } static void M_ApproachTilt(const int16_t target, int16_t *const current) { if (ABS(target - *current) < M_CRAWL_TILT_RATE) { *current = target; } else if (target > *current) { *current += M_CRAWL_TILT_RATE; } else { *current -= M_CRAWL_TILT_RATE; } CLAMP(*current, -M_CRAWL_TILT_MAX, M_CRAWL_TILT_MAX); } void Lara_Col_CrawlTilt(ITEM *const item) { if (!g_Config.gameplay.enable_crawl_tilt) { return; } const XYZ_32 pos = { .x = item->pos.x, .y = item->pos.y - M_CRAWL_TILT_HEIGHT, .z = item->pos.z, }; int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); const int32_t height = Room_GetHeight(sector, pos); XYZ_F plane = {}; if (Room_IsOnWalkable(sector, pos, height, NO_ITEM)) { const XZ_32 tilt = M_GetWalkableTilt(item, pos.y); plane.x = tilt.x * 2.0f / WALL_L; plane.z = tilt.z * 2.0f / WALL_L; } else { const XZ_16 tilt = Room_GetTiltType(sector, pos); plane.x = -tilt.x / 4.0f; plane.z = -tilt.z / 4.0f; } plane.y = item->pos.y - plane.x * item->pos.x - plane.z * item->pos.z; int32_t heights[4] = {}; for (int32_t i = 0; i < 4; i++) { const XYZ_32 test_pos = XYZ_32_OffsetYaw( pos, item->rot.y + DEG_90 * i, M_CRAWL_TILT_RADIUS); room_num = item->room_num; const SECTOR *const test_sector = Room_GetSector(test_pos, &room_num); heights[i] = Room_GetHeight(test_sector, test_pos); if (ABS(height - heights[i]) > M_CRAWL_TILT_RADIUS / 2) { heights[i] = plane.x * test_pos.x + plane.z * test_pos.z + plane.y; } } const XZ_32 delta = { .x = heights[0] - heights[2], .z = heights[3] - heights[1], }; const XZ_16 rot = { .x = M_GetTilt(delta.x, M_CRAWL_TILT_RADIUS), .z = M_GetTilt(delta.z, M_CRAWL_TILT_RADIUS), }; M_ApproachTilt(rot.x, &item->rot.x); M_ApproachTilt(rot.z, &item->rot.z); } // clang-format off REGISTER_LARA_COL(LS_CROUCH_IDLE, M_Crouch) REGISTER_LARA_COL(LS_CROUCH_ROLL, M_CrouchRoll) REGISTER_LARA_COL(LS_CROUCH_TURN_LEFT, M_CrouchTurn) REGISTER_LARA_COL(LS_CROUCH_TURN_RIGHT, M_CrouchTurn) REGISTER_LARA_COL(LS_CRAWL_IDLE, M_CrawlIdle) REGISTER_LARA_COL(LS_CRAWL_FORWARD, M_CrawlForward) REGISTER_LARA_COL(LS_CRAWL_TURN_LEFT, M_CrawlTurn) REGISTER_LARA_COL(LS_CRAWL_TURN_RIGHT, M_CrawlTurn) REGISTER_LARA_COL(LS_CRAWL_BACK, M_CrawlBack) REGISTER_LARA_COL(LS_CRAWL_TO_CLIMB, M_CrawlToClimb) // clang-format on ================================================ FILE: src/trx/game/lara/col/jump.c ================================================ #include #include #include #include #include #include #include #include #include // clang-format off #define M_LF_START_HANG 12 #define M_LF_FAST_FALL 1 #define M_BAD_JUMP_CEILING ((STEP_L * 3) / 4) // = 192 #define M_HEAD_CLEARANCE (-STEP_L / 8) // = -32 #define M_LADDER_CLEARANCE (-STEPUP_HEIGHT) // = -384 // clang-format on EDGE_CATCH Lara_Col_TestEdgeCatch( const ITEM *const item, const COLL_INFO *const coll, int32_t *const edge) { const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item); int32_t hdif1 = coll->side_front.floor - bounds->min.y; int32_t hdif2 = hdif1 + item->fall_speed; if ((hdif1 < 0 && hdif2 < 0) || (hdif1 > 0 && hdif2 > 0)) { hdif1 = item->pos.y + bounds->min.y; hdif2 = hdif1 + item->fall_speed; if ((hdif1 >> (WALL_SHIFT - 2)) == (hdif2 >> (WALL_SHIFT - 2))) { return EDGE_CATCH_NONE; } if (item->fall_speed > 0) { *edge = hdif2 & ~(STEP_L - 1); } else { *edge = hdif1 & ~(STEP_L - 1); } return EDGE_CATCH_NEG; } return ABS(coll->side_left2.floor - coll->side_right2.floor) < SLOPE_DIF ? EDGE_CATCH_POS : EDGE_CATCH_NONE; } SWING_CATCH Lara_Col_TestHangSwingIn( const ITEM *const item, const int16_t angle) { // Tests whether a forward hang grab should transition into thin-ledge // swing ("swinging inwards"). The probe samples one click ahead in the // hang direction and requires: // - valid floor at probe point; // - probe floor above Lara; // - enough overhead clearance for swing-in. // The extra clearance guard follows TR3-5 logic: `y - ceiling - 819 > -72` // but uses a relaxed threshold to preserve the animation on certain slope // edge cases. XYZ_32 pos = item->pos; int16_t room_num = item->room_num; switch (angle) { case 0: pos.z += STEP_L; break; case DEG_90: pos.x += STEP_L; break; case -DEG_180: pos.z -= STEP_L; break; case -DEG_90: pos.x -= STEP_L; break; } const SECTOR *const sector = Room_GetSector(pos, &room_num); int32_t height = Room_GetHeight(sector, pos); int32_t ceiling = Room_GetCeiling(sector, pos); const bool has_height = height != NO_HEIGHT; const int32_t height_delta = height - pos.y; const int32_t ceiling_delta = ceiling - pos.y; if (!has_height || height_delta <= 0 || ceiling_delta >= -400) { return SWING_CATCH_NONE; } const bool thin_ledge = pos.y - ceiling - 819 > -110; return thin_ledge && g_Config.gameplay.enable_slow_ledge_swing ? SWING_CATCH_SLOW : SWING_CATCH_FAST; } static bool M_TestHangJump(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); if (!g_Input.action || lara->gun_status != LGS_ARMLESS || coll->hit_static) { return false; } if (coll->coll_type == COLL_TOP || coll->coll_type == COLL_TOP_FRONT) { int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector( (XYZ_32) { item->pos.x, MAX_HEIGHT, item->pos.z }, &room_num); if ((sector->ladder & LADDER_CEILING) != 0) { Item_SwitchToAnim(item, LA(LA_SWING_IN_SLOW), 0); item->current_anim_state = LS(LS_MONKEY_IDLE); item->goal_anim_state = LS(LS_MONKEY_IDLE); item->gravity = false; item->speed = 0; item->fall_speed = 0; lara->gun_status = LGS_HANDS_BUSY; Lara_Col_MonkeySwingSnap(item); return true; } } if (coll->coll_type != COLL_FRONT || coll->side_mid.ceiling > -STEPUP_HEIGHT || coll->side_mid.floor < 200) { return false; } int32_t edge; const EDGE_CATCH edge_catch = Lara_Col_TestEdgeCatch(item, coll, &edge); bool ladder_hang = false; if (edge_catch == EDGE_CATCH_NEG) { ladder_hang = Lara_Col_TestLadderHang(item, coll); } if (edge_catch == EDGE_CATCH_NONE || (edge_catch == EDGE_CATCH_NEG && !ladder_hang)) { return false; } const DIRECTION dir = Math_GetDirectionCone(item->rot.y, LARA_HANG_ANGLE); if (dir == DIR_UNKNOWN) { return false; } const int16_t angle = Math_DirectionToAngle(dir); const SWING_CATCH swing_catch = Lara_Col_TestHangSwingIn(item, angle); if (swing_catch == SWING_CATCH_SLOW) { Item_SwitchToAnim(item, LA(LA_SWING_IN_SLOW), 0); } else if (swing_catch == SWING_CATCH_FAST) { Item_SwitchToAnim(item, LA(LA_SWING_IN_FAST), 0); } else { Item_SwitchToAnim(item, LA(LA_REACH_TO_HANG), 0); } const ANIM *const anim = Item_GetAnim(item); item->current_anim_state = anim->current_anim_state; item->goal_anim_state = anim->current_anim_state; const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item); if (edge_catch == EDGE_CATCH_POS) { item->pos.y += coll->side_front.floor - bounds->min.y; switch (dir) { case DIR_NORTH: item->pos.z = ROUND_TO_SECTOR_END(item->pos.z) - LARA_RADIUS; item->pos.x += coll->shift.x; break; case DIR_EAST: item->pos.x = ROUND_TO_SECTOR_END(item->pos.x) - LARA_RADIUS; item->pos.z += coll->shift.z; break; case DIR_SOUTH: item->pos.z = ROUND_TO_SECTOR(item->pos.z) + LARA_RADIUS; item->pos.x += coll->shift.x; break; case DIR_WEST: item->pos.x = ROUND_TO_SECTOR(item->pos.x) + LARA_RADIUS; item->pos.z += coll->shift.z; break; default: item->pos.x += coll->shift.x; item->pos.z += coll->shift.z; break; } } else { item->pos.y = edge - bounds->min.y; } item->rot.y = angle; item->speed = 2; item->gravity = true; item->fall_speed = 1; lara->gun_status = LGS_HANDS_BUSY; return true; } static bool M_TestHangJumpUp(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); if (!g_Input.action || lara->gun_status != LGS_ARMLESS || coll->hit_static) { return false; } if (coll->coll_type == COLL_TOP || coll->coll_type == COLL_TOP_FRONT) { int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector( (XYZ_32) { item->pos.x, MAX_HEIGHT, item->pos.z }, &room_num); if ((sector->ladder & LADDER_CEILING) != 0) { Item_SwitchToAnim(item, LA(LA_MONKEY_GRAB), 0); item->current_anim_state = LS(LS_MONKEY_IDLE); item->goal_anim_state = LS(LS_MONKEY_IDLE); item->gravity = false; item->speed = 0; item->fall_speed = 0; lara->gun_status = LGS_HANDS_BUSY; Lara_Col_MonkeySwingSnap(item); return true; } } if (coll->coll_type != COLL_FRONT || coll->side_mid.ceiling > (lara->climb_status ? M_LADDER_CLEARANCE : M_HEAD_CLEARANCE)) { return false; } int32_t edge; const EDGE_CATCH edge_catch = Lara_Col_TestEdgeCatch(item, coll, &edge); if (edge_catch == EDGE_CATCH_NONE || (edge_catch == EDGE_CATCH_NEG && !Lara_Col_TestLadderHang(item, coll))) { return false; } const DIRECTION dir = Math_GetDirectionCone(item->rot.y, LARA_HANG_ANGLE); if (dir == DIR_UNKNOWN) { return false; } const int16_t angle = Math_DirectionToAngle(dir); item->goal_anim_state = LS(LS_HANG); item->current_anim_state = LS(LS_HANG); Item_SwitchToAnim(item, LA(LA_REACH_TO_HANG), M_LF_START_HANG); const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item); if (edge_catch == EDGE_CATCH_POS) { item->pos.y += coll->side_front.floor - bounds->min.y; } else { item->pos.y = edge - bounds->min.y + (g_TRVersion >= 3 ? 4 : 0); } item->pos.x += coll->shift.x; item->pos.z += coll->shift.z; item->rot.y = angle; item->speed = 0; item->gravity = false; item->fall_speed = 0; lara->gun_status = LGS_HANDS_BUSY; return true; } static void M_SlideEdgeJump(ITEM *const item, COLL_INFO *const coll) { Lara_Col_Shift(coll); switch (coll->coll_type) { case COLL_LEFT: item->rot.y += LARA_DEFLECT_ANGLE; break; case COLL_RIGHT: item->rot.y -= LARA_DEFLECT_ANGLE; break; case COLL_TOP: case COLL_TOP_FRONT: CLAMPL(item->fall_speed, 1); break; case COLL_CLAMP: item->pos.z -= (Math_Cos(coll->facing) * 100) >> W2V_SHIFT; item->pos.x -= (Math_Sin(coll->facing) * 100) >> W2V_SHIFT; item->speed = 0; coll->side_mid.floor = 0; if (item->fall_speed <= 0) { item->fall_speed = 16; } break; } } static void M_Compress(ITEM *const item, COLL_INFO *const coll) { item->gravity = false; item->fall_speed = 0; coll->bad_pos = NO_BAD_POS; coll->bad_neg = NO_BAD_NEG; coll->bad_ceiling = 0; Lara_Col_GetInfo(item, coll); if (coll->side_mid.ceiling > -100) { Item_SwitchToAnim(item, LA(LA_STAND_STILL), 0); item->goal_anim_state = LS(LS_STOP); item->current_anim_state = LS(LS_STOP); item->gravity = false; item->speed = 0; item->fall_speed = 0; item->pos = coll->old; } if (g_TRVersion >= 2 && coll->side_mid.floor > -STEP_L && coll->side_mid.floor < STEP_L) { item->pos.y += coll->side_mid.floor; } } static void M_NeutralJumpRoll(ITEM *const item, COLL_INFO *const coll) { item->gravity = false; item->fall_speed = 0; coll->bad_pos = NO_BAD_POS; coll->bad_neg = NO_BAD_NEG; coll->bad_ceiling = 0; Lara_Col_GetInfo(item, coll); if (coll->side_mid.ceiling > -100) { Item_SwitchToAnim(item, LA(LA_STAND_STILL), 0); item->goal_anim_state = LS(LS_STOP); item->current_anim_state = LS(LS_STOP); item->speed = 0; item->pos = coll->old; } } static void M_UpJump(ITEM *const item, COLL_INFO *const coll) { if (item->hit_points <= 0) { item->goal_anim_state = LS(LS_STOP); return; } LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y; coll->bad_pos = NO_BAD_POS; coll->bad_neg = -STEPUP_HEIGHT; coll->bad_ceiling = M_BAD_JUMP_CEILING; coll->facing = lara->move_angle; if (g_Config.gameplay.enable_lean_jumping && item->speed < 0) { coll->facing += DEG_180; } Collide_GetCollisionInfo( coll, item->pos.x, item->pos.y, item->pos.z, item->room_num, 870); if (M_TestHangJumpUp(item, coll)) { return; } M_SlideEdgeJump(item, coll); if (g_Config.gameplay.enable_lean_jumping) { if (coll->coll_type != COLL_NONE) { item->speed = item->speed > 0 ? 2 : -2; } else if (item->fall_speed < -70) { if (g_Input.forward && item->speed < 5) { item->speed++; } else if (g_Input.back && item->speed > -5) { item->speed -= 2; } } } if (item->fall_speed <= 0 || coll->side_mid.floor > 0) { return; } switch (Lara_Col_LandedBad(item)) { case LANDED_OK: item->goal_anim_state = LS(LS_STOP); break; case LANDED_BAD: item->goal_anim_state = LS(LS_DEATH); break; case LANDED_HANDLED: break; } item->gravity = false; item->fall_speed = 0; item->pos.y += coll->side_mid.floor; } static void M_ForwardJump(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); if (item->speed < 0 && g_Config.gameplay.wall_glitch_mode != WALL_GLITCH_TR1) { lara->move_angle = item->rot.y + DEG_180; } else { lara->move_angle = item->rot.y; } coll->bad_pos = NO_BAD_POS; coll->bad_neg = -STEPUP_HEIGHT; coll->bad_ceiling = M_BAD_JUMP_CEILING; Lara_Col_GetInfo(item, coll); Lara_Col_DeflectEdgeJump(item, coll); if (item->speed < 0 && g_Config.gameplay.wall_glitch_mode != WALL_GLITCH_TR1) { lara->move_angle = item->rot.y; } if (coll->side_mid.floor > 0 || item->fall_speed <= 0) { return; } switch (Lara_Col_LandedBad(item)) { case LANDED_OK: if (lara->water_status != LWS_WADE && g_Input.forward && !g_Input.slow) { item->goal_anim_state = LS(LS_RUN); } else { item->goal_anim_state = LS(LS_STOP); } break; case LANDED_BAD: item->goal_anim_state = LS(LS_DEATH); break; case LANDED_HANDLED: break; } item->gravity = false; item->fall_speed = 0; item->pos.y += coll->side_mid.floor; item->speed = 0; if (g_Config.gameplay.wall_glitch_mode != WALL_GLITCH_FIXED || coll->side_front.type != COLL_FRONT) { Lara_Animate(item); } } static void M_SideBackJump(ITEM *const item, COLL_INFO *const coll) { int32_t angle = 0; switch (LS_U(item->current_anim_state)) { case LS_JUMP_BACK: angle = DEG_180; break; case LS_JUMP_RIGHT: angle = DEG_90; break; case LS_JUMP_LEFT: angle = -DEG_90; break; default: return; } LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y + angle; coll->bad_pos = NO_BAD_POS; coll->bad_neg = -STEPUP_HEIGHT; coll->bad_ceiling = M_BAD_JUMP_CEILING; Lara_Col_GetInfo(item, coll); Lara_Col_DeflectEdgeJump(item, coll); if (item->fall_speed <= 0 || coll->side_mid.floor > 0) { return; } switch (Lara_Col_LandedBad(item)) { case LANDED_OK: item->goal_anim_state = LS(LS_STOP); break; case LANDED_BAD: item->goal_anim_state = LS(LS_DEATH); break; case LANDED_HANDLED: break; } item->gravity = false; item->fall_speed = 0; item->pos.y += coll->side_mid.floor; } static void M_FallBack(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y + DEG_180; coll->bad_pos = NO_BAD_POS; coll->bad_neg = -STEPUP_HEIGHT; coll->bad_ceiling = M_BAD_JUMP_CEILING; Lara_Col_GetInfo(item, coll); Lara_Col_DeflectEdgeJump(item, coll); if (coll->side_mid.floor > 0 || item->fall_speed <= 0) { return; } switch (Lara_Col_LandedBad(item)) { case LANDED_OK: item->goal_anim_state = LS(LS_STOP); break; case LANDED_BAD: item->goal_anim_state = LS(LS_DEATH); break; case LANDED_HANDLED: break; } item->gravity = false; item->fall_speed = 0; item->pos.y += coll->side_mid.floor; } static void M_Reach(ITEM *const item, COLL_INFO *const coll) { item->gravity = true; LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y; coll->bad_pos = NO_BAD_POS; coll->bad_neg = 0; coll->bad_ceiling = M_BAD_JUMP_CEILING; Lara_Col_GetInfo(item, coll); if (M_TestHangJump(item, coll)) { return; } M_SlideEdgeJump(item, coll); if (item->fall_speed <= 0 || coll->side_mid.floor > 0) { return; } switch (Lara_Col_LandedBad(item)) { case LANDED_OK: item->goal_anim_state = LS(LS_STOP); break; case LANDED_BAD: item->goal_anim_state = LS(LS_DEATH); break; case LANDED_HANDLED: break; } item->gravity = false; item->fall_speed = 0; item->pos.y += coll->side_mid.floor; } static void M_SwanDive(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y; coll->bad_pos = NO_BAD_POS; coll->bad_neg = -STEPUP_HEIGHT; coll->bad_ceiling = M_BAD_JUMP_CEILING; Lara_Col_GetInfo(item, coll); Lara_Col_DeflectEdgeJump(item, coll); if (coll->side_mid.floor > 0 || item->fall_speed <= 0) { return; } item->goal_anim_state = LS(LS_STOP); item->gravity = false; item->fall_speed = 0; item->pos.y += coll->side_mid.floor; } static void M_FastDive(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y; coll->bad_pos = NO_BAD_POS; coll->bad_neg = -STEPUP_HEIGHT; coll->bad_ceiling = M_BAD_JUMP_CEILING; Lara_Col_GetInfo(item, coll); Lara_Col_DeflectEdgeJump(item, coll); if (coll->side_mid.floor > 0 || item->fall_speed <= 0) { return; } if (item->fall_speed > 133 && !g_Config.debug.enable_invulnerability) { item->goal_anim_state = LS(LS_DEATH); } else { item->goal_anim_state = LS(LS_STOP); } item->gravity = false; item->fall_speed = 0; item->pos.y += coll->side_mid.floor; } static void M_FastFall(ITEM *const item, COLL_INFO *const coll) { item->gravity = true; coll->bad_pos = NO_BAD_POS; coll->bad_neg = -STEPUP_HEIGHT; coll->bad_ceiling = M_BAD_JUMP_CEILING; Lara_Col_GetInfo(item, coll); M_SlideEdgeJump(item, coll); if (coll->side_mid.floor > 0) { return; } switch (Lara_Col_LandedBad(item)) { case LANDED_OK: item->goal_anim_state = LS(LS_STOP); item->current_anim_state = LS(LS_STOP); Item_SwitchToAnim(item, LA(LA_FREEFALL_LAND), 0); break; case LANDED_BAD: item->goal_anim_state = LS(LS_DEATH); break; case LANDED_HANDLED: break; } Sound_StopEffect(SFX_LARA_FALL); item->gravity = false; item->fall_speed = 0; item->pos.y += coll->side_mid.floor; } void Lara_Col_DeflectEdgeJump(ITEM *const item, COLL_INFO *const coll) { Lara_Col_Shift(coll); switch (coll->coll_type) { case COLL_FRONT: case COLL_TOP_FRONT: LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->climb_status && item->speed == 2) { break; } if (g_Config.gameplay.wall_glitch_mode == WALL_GLITCH_TR1 || coll->side_mid.floor > (STEP_L * 2)) { item->goal_anim_state = LS(LS_FAST_FALL); item->current_anim_state = LS(LS_FAST_FALL); Item_SwitchToAnim(item, LA(LA_SMASH_JUMP), M_LF_FAST_FALL); } else if (coll->side_mid.floor <= (STEP_L / 2)) { item->goal_anim_state = LS(LS_LAND); item->current_anim_state = LS(LS_LAND); Item_SwitchToAnim(item, LA(LA_JUMP_UP_LAND), 0); } item->speed /= 4; lara->move_angle += DEG_180; CLAMPL(item->fall_speed, 1); break; case COLL_LEFT: item->rot.y += LARA_DEFLECT_ANGLE; break; case COLL_RIGHT: item->rot.y -= LARA_DEFLECT_ANGLE; break; case COLL_TOP: CLAMPL(item->fall_speed, 1); break; case COLL_CLAMP: item->pos.z -= (Math_Cos(coll->facing) * 100) >> W2V_SHIFT; item->pos.x -= (Math_Sin(coll->facing) * 100) >> W2V_SHIFT; item->speed = 0; coll->side_mid.floor = 0; if (item->fall_speed <= 0) { item->fall_speed = 16; } break; } } LANDED_STATE Lara_Col_LandedBad(ITEM *const item) { const XYZ_32 pos = item->pos; int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); const int32_t height = Room_GetHeight(sector, (XYZ_32) { pos.x, pos.y - LARA_HEIGHT, pos.z }); item->pos.y = height; item->floor = height; LARA_INFO *const lara = Lara_GetLaraInfo(); const bool was_alive = item->hit_points > 0; const bool was_extra_anim = lara->extra_anim; Room_TestTriggers(item); if (was_alive && item->hit_points <= 0 && !was_extra_anim && lara->extra_anim) { // Support rapids drown from any height return LANDED_HANDLED; } item->pos.y = pos.y; const int32_t land_speed = item->fall_speed - DAMAGE_START; if (land_speed <= 0) { return LANDED_OK; } if (g_Config.debug.enable_invulnerability) { return false; } else if (land_speed <= DAMAGE_LENGTH) { Lara_TakeDamage( LARA_MAX_HITPOINTS * SQUARE(land_speed) / SQUARE(DAMAGE_LENGTH), false); } else { item->hit_points = -1; } // #675: Original bug to keep. Correct operator would be <= return item->hit_points < 0 ? LANDED_BAD : LANDED_OK; } // clang-format off REGISTER_LARA_COL(LS_COMPRESS, M_Compress) REGISTER_LARA_COL(LS_NEUTRAL_ROLL, M_NeutralJumpRoll) REGISTER_LARA_COL(LS_JUMP_UP, M_UpJump) REGISTER_LARA_COL(LS_JUMP_FORWARD, M_ForwardJump) REGISTER_LARA_COL(LS_JUMP_BACK, M_SideBackJump) REGISTER_LARA_COL(LS_JUMP_RIGHT, M_SideBackJump) REGISTER_LARA_COL(LS_JUMP_LEFT, M_SideBackJump) REGISTER_LARA_COL(LS_FALL_BACK, M_FallBack) REGISTER_LARA_COL(LS_REACH, M_Reach) REGISTER_LARA_COL(LS_SWAN_DIVE, M_SwanDive) REGISTER_LARA_COL(LS_FAST_DIVE, M_FastDive) REGISTER_LARA_COL(LS_FAST_FALL, M_FastFall) // clang-format on ================================================ FILE: src/trx/game/lara/col/land.c ================================================ #include #include #include #include #include #include #include #define M_LF_WALK_STEP_L_START 0 #define M_LF_WALK_STEP_L_NEAR_END 5 #define M_LF_WALK_STEP_L_END 6 #define M_LF_WALK_STEP_R_START 7 #define M_LF_WALK_STEP_R_MID 22 #define M_LF_WALK_STEP_R_NEAR_END 23 #define M_LF_WALK_STEP_R_END 25 #define M_LF_WALK_STEP_L_2_START 26 #define M_LF_WALK_STEP_L_2_END 35 #define M_LF_WALK_BACK_R_START 26 #define M_LF_WALK_BACK_R_END 55 #define M_LF_RUN_L_START 0 #define M_LF_RUN_L_HEEL_GROUND 3 #define M_LF_RUN_L_END 9 #define M_LF_RUN_R_START 10 #define M_LF_RUN_R_FOOT_GROUND 14 #define M_LF_RUN_R_END 21 #define M_LF_WADE_L_START 0 #define M_LF_WADE_L_END 9 #define M_LF_WADE_R_START 10 #define M_LF_WADE_R_END 21 #define M_LF_WADE_STEP_L_START 3 #define M_LF_WADE_STEP_L_END 14 #define M_LF_SPRINT_STEP_L_START 4 #define M_LF_SPRINT_STEP_L_END 13 #define M_CONTROLLED_DROP_MIN_HEIGHT (LARA_HEIGHT + (STEP_L * 3) / 4) // 954 static int16_t m_OldSlideAngle = 1; static bool M_TestWall( const ITEM *const item, const int32_t front, const int32_t right, const int32_t down) { XYZ_32 pos = item->pos; pos.y += down; const DIRECTION dir = Math_GetDirection(item->rot.y); switch (dir) { case DIR_NORTH: pos.x -= right; break; case DIR_EAST: pos.z -= right; break; case DIR_SOUTH: pos.x += right; break; case DIR_WEST: pos.z += right; break; default: break; } int16_t room_num = item->room_num; Room_GetSector(pos, &room_num); switch (dir) { case DIR_NORTH: pos.z += front; break; case DIR_EAST: pos.x += front; break; case DIR_SOUTH: pos.z -= front; break; case DIR_WEST: pos.x -= front; break; default: break; } const SECTOR *const sector = Room_GetSector(pos, &room_num); const int32_t height = Room_GetHeight(sector, pos); const int32_t ceiling = Room_GetCeiling(sector, pos); if (height != NO_HEIGHT && height - pos.y > 0 && ceiling - pos.y < 0) { return false; } return true; } static bool M_CanControlDrop( const ITEM *const item, const COLL_INFO *const coll) { const LARA_INFO *const lara = Lara_GetLaraInfo(); if (!g_Input.action || lara->gun_status != LGS_ARMLESS || !g_Config.gameplay.enable_controlled_drops || coll->side_mid.floor < M_CONTROLLED_DROP_MIN_HEIGHT) { return false; } COLL_INFO old_coll = { .facing = lara->move_angle, .bad_pos = STEPUP_HEIGHT, .bad_neg = -STEPUP_HEIGHT, .slopes_are_pits = 1, .slopes_are_walls = 1, }; Collide_GetCollisionInfo( &old_coll, coll->old.x, coll->old.y, coll->old.z, item->room_num, LARA_HEIGHT); if (old_coll.side_mid.floor != 0) { return false; } const DIRECTION dir = Math_GetDirectionCone(item->rot.y + DEG_180, LARA_HANG_ANGLE); if (dir == DIR_UNKNOWN) { return false; } switch (old_coll.quadrant) { case DIR_NORTH: case DIR_SOUTH: return ABS(old_coll.tilt.x) < MAX_SLOPE; case DIR_EAST: case DIR_WEST: return ABS(old_coll.tilt.z) < MAX_SLOPE; default: return false; } } bool Lara_Col_Fallen(ITEM *const item, const COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); if (coll->side_mid.floor <= STEPUP_HEIGHT || lara->water_status == LWS_WADE) { return false; } if (M_CanControlDrop(item, coll)) { item->current_anim_state = LS(LS_REACH); item->goal_anim_state = LS(LS_REACH); Item_SwitchToAnim(item, LA(LA_CONTROLLED_DROP), 0); item->speed = 2; } else { item->current_anim_state = LS(LS_JUMP_FORWARD); item->goal_anim_state = LS(LS_JUMP_FORWARD); Item_SwitchToAnim(item, LA(LA_FALL_START), 0); } item->gravity = true; item->fall_speed = 0; lara->sprinting = false; lara->crouching = false; return true; } bool Lara_Col_TestSlide(ITEM *const item, COLL_INFO *const coll) { if (ABS(coll->tilt.x) <= MAX_SLOPE && ABS(coll->tilt.z) <= MAX_SLOPE) { return false; } const ROOM *const room = Room_Get(item->room_num); if (room->flags.swamp) { return false; } int16_t angle = 0; if (coll->tilt.x > MAX_SLOPE) { angle = -DEG_90; } else if (coll->tilt.x < -MAX_SLOPE) { angle = DEG_90; } if (coll->tilt.z > 2 && coll->tilt.z > ABS(coll->tilt.x)) { angle = -DEG_180; } else if (coll->tilt.z < -2 && -coll->tilt.z > ABS(coll->tilt.x)) { angle = 0; } const int16_t angle_dif = angle - item->rot.y; Lara_Col_Shift(coll); LARA_INFO *const lara = Lara_GetLaraInfo(); if (angle_dif >= -DEG_90 && angle_dif <= DEG_90) { if (item->current_anim_state == LS(LS_SLIDE) && m_OldSlideAngle == angle) { lara->sprinting = false; lara->crouching = false; return true; } item->goal_anim_state = LS(LS_SLIDE); item->current_anim_state = LS(LS_SLIDE); Item_SwitchToAnim(item, LA(LA_SLIDE_FORWARD), 0); item->rot.y = angle; } else { if (item->current_anim_state == LS(LS_SLIDE_BACK) && m_OldSlideAngle == angle) { lara->sprinting = false; lara->crouching = false; return true; } item->goal_anim_state = LS(LS_SLIDE_BACK); item->current_anim_state = LS(LS_SLIDE_BACK); Item_SwitchToAnim(item, LA(LA_SLIDE_BACKWARD_START), 0); item->rot.y = angle + DEG_180; } lara->move_angle = angle; lara->sprinting = false; lara->crouching = false; m_OldSlideAngle = angle; return true; } static bool M_DeflectEdge(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); switch (coll->coll_type) { case COLL_FRONT: case COLL_TOP_FRONT: Lara_Col_Shift(coll); item->goal_anim_state = LS(LS_STOP); item->current_anim_state = LS(LS_STOP); item->gravity = false; item->speed = 0; return true; case COLL_LEFT: Lara_Col_Shift(coll); item->rot.y += LARA_DEFLECT_ANGLE; return false; case COLL_RIGHT: Lara_Col_Shift(coll); item->rot.y -= LARA_DEFLECT_ANGLE; return false; default: return false; } } bool Lara_Col_TestCeiling(ITEM *const item, const COLL_INFO *const coll) { if (coll->coll_type != COLL_TOP && coll->coll_type != COLL_CLAMP) { return false; } LARA_INFO *const lara = Lara_GetLaraInfo(); lara->sprinting = false; lara->crouching = false; item->pos = coll->old; item->goal_anim_state = LS(LS_STOP); item->current_anim_state = LS(LS_STOP); Item_SwitchToAnim(item, LA(LA_STAND_STILL), 0); item->speed = 0; item->gravity = false; item->fall_speed = 0; return true; } static void M_CollideStop(ITEM *const item, const COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->sprinting = false; lara->crouching = false; if (g_Config.gameplay.enable_smooth_wall_deflect) { switch (LS_U(coll->old_anim_state)) { case LS_STOP: case LS_TURN_RIGHT: case LS_TURN_LEFT: case LS_FAST_TURN: item->current_anim_state = coll->old_anim_state; item->anim_num = coll->old_anim_num; item->frame_num = coll->old_frame_num; if (g_Input.left) { item->goal_anim_state = LS(LS_TURN_LEFT); } else if (g_Input.right) { item->goal_anim_state = LS(LS_TURN_RIGHT); } else { item->goal_anim_state = LS(LS_STOP); } Lara_Animate(item); return; default: break; } } Item_SwitchToAnim(item, LA(LA_STAND_STILL), 0); } static void M_Default(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y; coll->bad_pos = STEPUP_HEIGHT; coll->bad_neg = -STEPUP_HEIGHT; coll->bad_ceiling = 0; coll->slopes_are_pits = 1; coll->slopes_are_walls = 1; Lara_Col_GetInfo(item, coll); } static void M_Pickup(ITEM *const item, COLL_INFO *const coll) { M_Default(item, coll); if (Item_TestAnimEqual(item, LA(LA_CRAWL_PICKUP))) { Lara_Col_CrawlTilt(item); } } static void M_PullUp(ITEM *const item, COLL_INFO *const coll) { M_Default(item, coll); if (Item_TestAnimEqual(item, LA(LA_CLIMB_2CLICK)) && Item_TestFrameEqual(item, -1)) { Lara_UpdateRoomToHeight(-WALL_L); Lara_Animate(item); } } static void M_Walk(ITEM *const item, COLL_INFO *const coll) { item->gravity = false; item->fall_speed = 0; coll->lava_is_pit = 1; M_Default(item, coll); if (Lara_Col_TestCeiling(item, coll) || Lara_Col_TestVault(item, coll)) { return; } if (M_DeflectEdge(item, coll)) { if (Item_TestAnimEqual(item, LA(LA_WALK_FORWARD)) && Item_TestFrameRange( item, M_LF_WALK_STEP_R_START, M_LF_WALK_STEP_R_END)) { Item_SwitchToAnim(item, LA(LA_WALK_STOP_RIGHT), 0); } else if ( Item_TestAnimEqual(item, LA(LA_WALK_FORWARD)) && (Item_TestFrameRange( item, M_LF_WALK_STEP_L_START, M_LF_WALK_STEP_L_END) || Item_TestFrameRange( item, M_LF_WALK_STEP_L_2_START, M_LF_WALK_STEP_L_2_END))) { Item_SwitchToAnim(item, LA(LA_WALK_STOP_LEFT), 0); } else { M_CollideStop(item, coll); } } if (Lara_Col_Fallen(item, coll)) { return; } if (coll->side_mid.floor > STEP_L / 2) { if (Item_TestAnimEqual(item, LA(LA_WALK_FORWARD)) && Item_TestFrameRange( item, M_LF_WALK_STEP_L_END, M_LF_WALK_STEP_R_NEAR_END)) { Item_SwitchToAnim(item, LA(LA_WALK_DOWN_LEFT), 0); } else { Item_SwitchToAnim(item, LA(LA_WALK_DOWN_RIGHT), 0); } } if (coll->side_mid.floor >= -STEPUP_HEIGHT && coll->side_mid.floor < -STEP_L / 2) { if (Item_TestAnimEqual(item, LA(LA_WALK_FORWARD)) && Item_TestFrameRange( item, M_LF_WALK_STEP_L_NEAR_END, M_LF_WALK_STEP_R_MID)) { Item_SwitchToAnim(item, LA(LA_WALK_UP_STEP_LEFT), 0); } else { Item_SwitchToAnim(item, LA(LA_WALK_UP_STEP_RIGHT), 0); } } if (Lara_Col_TestSlide(item, coll)) { return; } item->pos.y += coll->side_mid.floor; } static void M_WalkBack(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y + DEG_180; item->gravity = false; item->fall_speed = 0; if (lara->water_status == LWS_WADE) { coll->bad_pos = NO_BAD_POS; } else { coll->bad_pos = STEPUP_HEIGHT; } coll->slopes_are_pits = 1; coll->slopes_are_walls = 1; coll->bad_neg = -STEPUP_HEIGHT; coll->bad_ceiling = 0; coll->lava_is_pit = 1; Lara_Col_GetInfo(item, coll); if (Lara_Col_TestCeiling(item, coll)) { return; } if (M_DeflectEdge(item, coll)) { M_CollideStop(item, coll); } if (g_Config.gameplay.fix_descending_glitch && Lara_Col_Fallen(item, coll)) { return; } if (coll->side_mid.floor > STEP_L / 2 && coll->side_mid.floor < STEPUP_HEIGHT) { if (Item_TestFrameRange( item, M_LF_WALK_BACK_R_START, M_LF_WALK_BACK_R_END)) { Item_SwitchToAnim(item, LA(LA_WALK_DOWN_BACK_RIGHT), 0); } else { Item_SwitchToAnim(item, LA(LA_WALK_DOWN_BACK_LEFT), 0); } } if (Lara_Col_TestSlide(item, coll)) { return; } const ROOM *const room = Room_Get(item->room_num); if (coll->side_mid.floor >= 0 && room->flags.swamp) { item->pos.y += 2; } else if (lara->water_status == LWS_WADE && coll->side_mid.floor >= 50) { item->pos.y += 50; } else { item->pos.y += coll->side_mid.floor; } } static void M_SideStep(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); if (item->current_anim_state == LS(LS_STEP_RIGHT)) { lara->move_angle = item->rot.y + DEG_90; } else { lara->move_angle = item->rot.y - DEG_90; } item->gravity = false; item->fall_speed = 0; if (lara->water_status == LWS_WADE) { coll->bad_pos = NO_BAD_POS; } else { coll->bad_pos = STEP_L / 2; } coll->slopes_are_pits = 1; coll->slopes_are_walls = 1; coll->bad_neg = -STEP_L / 2; coll->bad_ceiling = 0; coll->lava_is_pit = 1; Lara_Col_GetInfo(item, coll); if (Lara_Col_TestCeiling(item, coll)) { return; } if (M_DeflectEdge(item, coll)) { M_CollideStop(item, coll); } if (g_Config.gameplay.fix_descending_glitch && Lara_Col_Fallen(item, coll)) { return; } if (!Lara_Col_TestSlide(item, coll)) { item->pos.y += coll->side_mid.floor; } } static void M_Run(ITEM *const item, COLL_INFO *const coll) { if (g_Config.gameplay.fix_qwop_glitch) { item->gravity = false; item->fall_speed = 0; } LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y; coll->slopes_are_walls = 1; coll->bad_pos = NO_BAD_POS; coll->bad_neg = -STEPUP_HEIGHT; coll->bad_ceiling = 0; Lara_Col_GetInfo(item, coll); if (Lara_Col_TestCeiling(item, coll) || Lara_Col_TestVault(item, coll)) { return; } if (M_DeflectEdge(item, coll)) { item->rot.z = 0; if (M_TestWall(item, STEP_L, 0, -STEP_L * 5 / 2)) { item->current_anim_state = LS(LS_SPLAT); const bool is_run_anim = Item_TestAnimEqual(item, LA(LA_RUN)); if (is_run_anim && Item_TestFrameRange( item, M_LF_RUN_L_START, M_LF_RUN_L_END)) { Item_SwitchToAnim(item, LA(LA_WALL_SMASH_LEFT), 0); return; } if (is_run_anim && Item_TestFrameRange( item, M_LF_RUN_R_START, M_LF_RUN_R_END)) { Item_SwitchToAnim(item, LA(LA_WALL_SMASH_RIGHT), 0); return; } } M_CollideStop(item, coll); } if (Lara_Col_Fallen(item, coll)) { return; } if (coll->side_mid.floor >= -STEPUP_HEIGHT && coll->side_mid.floor < -STEP_L / 2) { if (g_Config.gameplay.fix_step_glitch && (coll->side_front.floor < -STEPUP_HEIGHT || coll->side_front.floor >= -STEP_L / 2)) { coll->side_mid.floor = 0; } else { if (Item_TestFrameRange( item, M_LF_RUN_L_HEEL_GROUND, M_LF_RUN_R_FOOT_GROUND)) { Item_SwitchToAnim(item, LA(LA_RUN_UP_STEP_LEFT), 0); } else { Item_SwitchToAnim(item, LA(LA_RUN_UP_STEP_RIGHT), 0); } } } if (Lara_Col_TestSlide(item, coll)) { return; } item->pos.y += MIN(coll->side_mid.floor, 50); } static void M_Stop(ITEM *const item, COLL_INFO *const coll) { item->gravity = false; item->fall_speed = 0; M_Default(item, coll); if (Lara_Col_TestCeiling(item, coll) || Lara_Col_Fallen(item, coll) || Lara_Col_TestSlide(item, coll)) { return; } const ROOM *const room = Room_Get(item->room_num); if (!room->flags.swamp && g_Config.gameplay.fix_step_glitch && coll->side_mid.floor > 100) { item->current_anim_state = LS(LS_JUMP_FORWARD); item->goal_anim_state = LS(LS_JUMP_FORWARD); Item_SwitchToAnim(item, LA(LA_FALL_START), 0); item->gravity = true; item->fall_speed = 0; return; } Lara_Col_Shift(coll); if (room->flags.swamp && coll->side_mid.floor >= 0) { item->pos.y += 2; CLAMPG(item->pos.y, item->floor); } else { item->pos.y += coll->side_mid.floor; } } static void M_FastBack(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y + DEG_180; item->gravity = false; item->fall_speed = 0; coll->slopes_are_pits = 1; coll->slopes_are_walls = !g_Config.gameplay.enable_back_slope_stumble; coll->bad_pos = NO_BAD_POS; coll->bad_neg = -STEPUP_HEIGHT; coll->bad_ceiling = 0; Lara_Col_GetInfo(item, coll); if (Lara_Col_TestCeiling(item, coll)) { return; } if (coll->side_mid.floor <= 200) { if (!g_Config.gameplay.enable_back_slope_stumble || !Lara_Col_TestSlide(item, coll)) { if (M_DeflectEdge(item, coll)) { M_CollideStop(item, coll); } item->pos.y += coll->side_mid.floor; } } else { Item_SwitchToAnim(item, LA(LA_FALL_BACK), 0); item->current_anim_state = LS(LS_FALL_BACK); item->goal_anim_state = LS(LS_FALL_BACK); item->gravity = true; item->fall_speed = 0; } } static void M_Turn(ITEM *const item, COLL_INFO *const coll) { item->gravity = false; item->fall_speed = 0; M_Default(item, coll); const ROOM *const room = Room_Get(item->room_num); if (coll->side_mid.floor > 100 && !room->flags.swamp) { Item_SwitchToAnim(item, LA(LA_FALL_START), 0); item->current_anim_state = LS(LS_JUMP_FORWARD); item->goal_anim_state = LS(LS_JUMP_FORWARD); item->gravity = true; item->fall_speed = 0; return; } if (Lara_Col_TestSlide(item, coll)) { return; } if (coll->side_mid.floor < 0 || !room->flags.swamp) { item->pos.y += coll->side_mid.floor; } else { item->pos.y += 2; } } static void M_Death(ITEM *const item, COLL_INFO *const coll) { if (g_TRVersion >= 2) { Sound_StopEffect(SFX_LARA_FALL); } LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y; coll->bad_pos = STEPUP_HEIGHT; coll->bad_neg = -STEPUP_HEIGHT; coll->bad_ceiling = 0; coll->radius = LARA_RADIUS * 4; Lara_Col_GetInfo(item, coll); Lara_Col_Shift(coll); item->pos.y += coll->side_mid.floor; item->hit_points = -1; lara->air = -1; } static void M_Splat(ITEM *const item, COLL_INFO *const coll) { M_Default(item, coll); Lara_Col_Shift(coll); if (!g_Config.gameplay.fix_step_glitch && coll->side_mid.floor > -STEP_L && coll->side_mid.floor < STEP_L) { item->pos.y += coll->side_mid.floor; } } static void M_Slide(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y; if (item->current_anim_state == LS(LS_SLIDE_BACK)) { lara->move_angle += DEG_180; } coll->bad_pos = NO_BAD_POS; coll->bad_neg = -STEP_L * 2; coll->bad_ceiling = 0; Lara_Col_GetInfo(item, coll); if (Lara_Col_TestCeiling(item, coll)) { return; } M_DeflectEdge(item, coll); if (coll->side_mid.floor > 200) { if (item->current_anim_state == LS(LS_SLIDE)) { if (M_CanControlDrop(item, coll)) { item->current_anim_state = LS(LS_REACH); item->goal_anim_state = LS(LS_REACH); Item_SwitchToAnim(item, LA(LA_CONTROLLED_DROP), 2); item->speed = 2; } else { item->goal_anim_state = LS(LS_JUMP_FORWARD); item->current_anim_state = LS(LS_JUMP_FORWARD); Item_SwitchToAnim(item, LA(LA_FALL_START), 0); } } else { item->goal_anim_state = LS(LS_FALL_BACK); item->current_anim_state = LS(LS_FALL_BACK); Item_SwitchToAnim(item, LA(LA_FALL_BACK), 0); } item->gravity = true; item->fall_speed = 0; return; } Lara_Col_TestSlide(item, coll); item->pos.y += coll->side_mid.floor; if (ABS(coll->tilt.x) <= MAX_SLOPE && ABS(coll->tilt.z) <= MAX_SLOPE) { item->goal_anim_state = LS(LS_STOP); } } static void M_Roll(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y; item->gravity = false; item->fall_speed = 0; coll->bad_pos = NO_BAD_POS; coll->bad_neg = -STEPUP_HEIGHT; coll->bad_ceiling = 0; coll->slopes_are_walls = 1; Lara_Col_GetInfo(item, coll); if (Lara_Col_TestCeiling(item, coll) || Lara_Col_TestSlide(item, coll)) { return; } if (g_Config.gameplay.enable_step_roll_boost) { if (coll->side_mid.floor > 200) { item->current_anim_state = LS(LS_JUMP_FORWARD); item->goal_anim_state = LS(LS_JUMP_FORWARD); Item_SwitchToAnim(item, LA(LA_FALL_START), 0); item->gravity = true; item->fall_speed = 0; return; } } else if (Lara_Col_Fallen(item, coll)) { return; } Lara_Col_Shift(coll); item->pos.y += coll->side_mid.floor; } static void M_RollContinue(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); item->gravity = false; item->fall_speed = 0; lara->move_angle = item->rot.y + DEG_180; coll->slopes_are_walls = 1; coll->bad_pos = NO_BAD_POS; coll->bad_neg = -STEPUP_HEIGHT; coll->bad_ceiling = 0; Lara_Col_GetInfo(item, coll); if (Lara_Col_TestCeiling(item, coll) || Lara_Col_TestSlide(item, coll)) { return; } if (coll->side_mid.floor > 200) { Item_SwitchToAnim(item, LA(LA_FALL_BACK), 0); item->current_anim_state = LS(LS_FALL_BACK); item->goal_anim_state = LS(LS_FALL_BACK); item->gravity = true; item->fall_speed = 0; } else { Lara_Col_Shift(coll); item->pos.y += coll->side_mid.floor; } } static void M_Wade(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y; coll->slopes_are_walls = 1; coll->bad_pos = NO_BAD_POS; coll->bad_neg = -STEPUP_HEIGHT; coll->bad_ceiling = 0; Lara_Col_GetInfo(item, coll); if (Lara_Col_TestCeiling(item, coll) || Lara_Col_TestVault(item, coll)) { return; } const ROOM *const room = Room_Get(item->room_num); if (M_DeflectEdge(item, coll)) { item->rot.z = 0; if (g_Config.gameplay.fix_wade_wall_hit && (coll->side_front.type == HT_WALL || coll->side_front.type == HT_SPLIT_TRI) && coll->side_front.floor < -STEP_L * 5 / 2 && coll->old_anim_state == LS(LS_WADE) && Item_TestAnimEqual(item, LA(LA_WADE)) && !room->flags.swamp) { item->current_anim_state = LS(LS_SPLAT); if (Item_TestFrameRange(item, M_LF_WADE_L_START, M_LF_WADE_L_END)) { Item_SwitchToAnim(item, LA(LA_WALL_SMASH_LEFT), 0); return; } if (Item_TestFrameRange(item, M_LF_WADE_R_START, M_LF_WADE_R_END)) { Item_SwitchToAnim(item, LA(LA_WALL_SMASH_RIGHT), 0); return; } } M_CollideStop(item, coll); } if (!room->flags.swamp && Lara_Col_Fallen(item, coll)) { return; } if (coll->side_mid.floor >= -STEPUP_HEIGHT && coll->side_mid.floor < -STEP_L / 2 && !room->flags.swamp) { if (Item_TestFrameRange( item, M_LF_WADE_STEP_L_START, M_LF_WADE_STEP_L_END)) { Item_SwitchToAnim(item, LA(LA_RUN_UP_STEP_LEFT), 0); } else { Item_SwitchToAnim(item, LA(LA_RUN_UP_STEP_RIGHT), 0); } } if (Lara_Col_TestSlide(item, coll)) { return; } if (coll->side_mid.floor >= 50 && !room->flags.swamp) { item->pos.y += 50; } else if (coll->side_mid.floor < 0 || !room->flags.swamp) { item->pos.y += coll->side_mid.floor; } else { item->pos.y += 2; } } static void M_Sprint(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y; coll->bad_pos = NO_BAD_POS; coll->bad_neg = -STEPUP_HEIGHT; coll->bad_ceiling = 0; coll->slopes_are_walls = 1; Lara_Col_GetInfo(item, coll); if (Lara_Col_TestCeiling(item, coll) || Lara_Col_TestVault(item, coll)) { return; } if (M_DeflectEdge(item, coll)) { item->rot.z = 0; if (M_TestWall(item, STEP_L, 0, -STEP_L * 5 / 2)) { Item_SwitchToAnim(item, LA(LA_WALL_SMASH_LEFT), 0); lara->sprinting = false; return; } M_CollideStop(item, coll); } if (Lara_Col_Fallen(item, coll)) { return; } if (!g_Config.gameplay.enable_responsive_sprint && coll->side_mid.floor >= -STEPUP_HEIGHT && coll->side_mid.floor < -STEP_L / 2) { if (Item_TestFrameRange( item, M_LF_SPRINT_STEP_L_START, M_LF_SPRINT_STEP_L_END)) { Item_SwitchToAnim(item, LA(LA_RUN_UP_STEP_LEFT), 0); } else { Item_SwitchToAnim(item, LA(LA_RUN_UP_STEP_RIGHT), 0); } } if (Lara_Col_TestSlide(item, coll)) { return; } item->pos.y += MIN(coll->side_mid.floor, 50); } static void M_SprintRoll(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y; if (item->speed < 0) { lara->move_angle += DEG_180; } coll->bad_pos = NO_BAD_POS; coll->bad_neg = -STEP_L; coll->bad_ceiling = STEPUP_HEIGHT / 2; coll->slopes_are_walls = 1; Lara_Col_GetInfo(item, coll); Lara_Col_DeflectEdgeJump(item, coll); if (Lara_Col_Fallen(item, coll)) { return; } if (item->speed < 0) { lara->move_angle = item->rot.y; } if (coll->side_mid.floor <= 0 && item->fall_speed > 0) { if (Lara_Col_LandedBad(item)) { item->goal_anim_state = LS(LS_DEATH); } else if ( lara->water_status == LWS_WADE || !g_Input.forward || g_Input.slow) { item->goal_anim_state = LS(LS_STOP); } else { item->goal_anim_state = LS(LS_RUN); } item->fall_speed = 0; item->gravity = false; item->speed = 0; item->pos.y += coll->side_mid.floor; Lara_Animate(item); } Lara_Col_Shift(coll); item->pos.y += coll->side_mid.floor; } // clang-format off REGISTER_LARA_COL(LS_PUSH_BLOCK, M_Default) REGISTER_LARA_COL(LS_PULL_BLOCK, M_Default) REGISTER_LARA_COL(LS_PP_READY, M_Default) REGISTER_LARA_COL(LS_PICKUP, M_Pickup) REGISTER_LARA_COL(LS_SWITCH_ON, M_Default) REGISTER_LARA_COL(LS_SWITCH_OFF, M_Default) REGISTER_LARA_COL(LS_USE_KEY, M_Default) REGISTER_LARA_COL(LS_USE_PUZZLE, M_Default) REGISTER_LARA_COL(LS_USE_MIDAS, M_Default) REGISTER_LARA_COL(LS_DIE_MIDAS, M_Default) REGISTER_LARA_COL(LS_GYMNAST, M_Default) REGISTER_LARA_COL(LS_WATER_OUT, M_Default) REGISTER_LARA_COL(LS_PULL_UP, M_PullUp) REGISTER_LARA_COL(LS_CONTROLLED, M_Default) REGISTER_LARA_COL(LS_FLARE_PICKUP, M_Default) REGISTER_LARA_COL(LS_WALK, M_Walk) REGISTER_LARA_COL(LS_WALK_BACK, M_WalkBack) REGISTER_LARA_COL(LS_STEP_RIGHT, M_SideStep) REGISTER_LARA_COL(LS_STEP_LEFT, M_SideStep) REGISTER_LARA_COL(LS_RUN, M_Run) REGISTER_LARA_COL(LS_STOP, M_Stop) REGISTER_LARA_COL(LS_POSE, M_Stop) REGISTER_LARA_COL(LS_POSE_START, M_Stop) REGISTER_LARA_COL(LS_POSE_END, M_Stop) REGISTER_LARA_COL(LS_LAND, M_Stop) REGISTER_LARA_COL(LS_FAST_TURN, M_Stop) REGISTER_LARA_COL(LS_FAST_BACK, M_FastBack) REGISTER_LARA_COL(LS_TURN_RIGHT, M_Turn) REGISTER_LARA_COL(LS_TURN_LEFT, M_Turn) REGISTER_LARA_COL(LS_DEATH, M_Death) REGISTER_LARA_COL(LS_SPLAT, M_Splat) REGISTER_LARA_COL(LS_SLIDE, M_Slide) REGISTER_LARA_COL(LS_SLIDE_BACK, M_Slide) REGISTER_LARA_COL(LS_ROLL, M_Roll) REGISTER_LARA_COL(LS_ROLL_CONT, M_RollContinue) REGISTER_LARA_COL(LS_WADE, M_Wade) REGISTER_LARA_COL(LS_SPRINT, M_Sprint) REGISTER_LARA_COL(LS_SPRINT_ROLL, M_SprintRoll) // clang-format on ================================================ FILE: src/trx/game/lara/col/monkey.c ================================================ #include #include #include #include #include #include // clang-format off #define M_MONKEY_RADIUS 100 #define M_MONKEY_HEIGHT 600 #define M_MONKEY_CEILING_SHIFT 50 #define M_MONKEY_FALL_FRAME 9 #define M_CAM_MONKEY_ELEVATION (10 * DEG_1) // = 1820 #define M_LF_CLIMB_1_START 54 #define M_LF_CLIMB_1_END 60 #define M_LF_CLIMB_2_START 78 #define M_LF_CLIMB_2_END 84 #define M_LF_CLIMB_3_START 102 #define M_LF_CLIMB_3_END 155 // clang-format on static bool M_CanMonkeySwing(const ITEM *const item) { int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector( (XYZ_32) { item->pos.x, MAX_HEIGHT, item->pos.z }, &room_num); return (sector->ladder & LADDER_CEILING) != 0; } static bool M_CanClimb(const ITEM *const item, const COLL_INFO *const coll) { const LARA_INFO *const lara = Lara_GetLaraInfo(); if (!lara->climb_status || !g_Input.forward || coll->side_mid.ceiling > -STEP_L) { return false; } if (Item_TestAnimEqual(item, LA(LA_MONKEY_IDLE))) { return true; } if (!Item_TestAnimEqual(item, LA(LA_SWING_IN_SLOW))) { return false; } return Item_TestFrameRange(item, M_LF_CLIMB_1_START, M_LF_CLIMB_1_END) || Item_TestFrameRange(item, M_LF_CLIMB_2_START, M_LF_CLIMB_2_END) || Item_TestFrameRange(item, M_LF_CLIMB_3_START, M_LF_CLIMB_3_END); } static void M_MonkeySwingFall(ITEM *const item) { item->goal_anim_state = LS(LS_JUMP_UP); item->current_anim_state = LS(LS_JUMP_UP); Item_SwitchToAnim(item, LA(LA_JUMP_UP), M_MONKEY_FALL_FRAME); item->gravity = true; item->speed = 2; item->fall_speed = 1; item->pos.y += STEP_L; LARA_INFO *const lara = Lara_GetLaraInfo(); lara->gun_status = LGS_ARMLESS; } static void M_GetMonkeyCollisionInfo( ITEM *const item, COLL_INFO *const coll, const int16_t move_angle, const int32_t bad_neg, const bool slopes_are_walls) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = move_angle; coll->bad_pos = NO_BAD_POS; coll->bad_neg = bad_neg; coll->bad_ceiling = 0; coll->facing = lara->move_angle; coll->radius = M_MONKEY_RADIUS; coll->slopes_are_walls = slopes_are_walls; Collide_GetCollisionInfo( coll, item->pos.x, item->pos.y, item->pos.z, item->room_num, M_MONKEY_HEIGHT); } static bool M_IsDirOctant(const int16_t rot) { const int16_t abs_rot = ABS(rot); return abs_rot >= DEG_45 && abs_rot <= DEG_135; } static bool M_TestMonkeySide( ITEM *const item, COLL_INFO *const coll, const int16_t angle_delta, const bool is_right) { LARA_INFO *const lara = Lara_GetLaraInfo(); const int16_t old_move_angle = lara->move_angle; lara->move_angle = item->rot.y + angle_delta; M_GetMonkeyCollisionInfo( item, coll, lara->move_angle, is_right ? -STEPUP_HEIGHT : NO_BAD_NEG, false); bool ok = true; if (ABS(coll->side_mid.ceiling - coll->side_front.ceiling) > M_MONKEY_CEILING_SHIFT) { ok = false; goto cleanup; } if (coll->coll_type != COLL_NONE) { const bool oct = M_IsDirOctant(item->rot.y); if (!oct && coll->coll_type == COLL_FRONT) { ok = false; goto cleanup; } if (!is_right) { if ((!oct && coll->coll_type == COLL_LEFT) || (oct && (coll->coll_type == COLL_RIGHT || coll->coll_type == COLL_LEFT))) { ok = false; goto cleanup; } } else { if (oct && (coll->coll_type == COLL_FRONT || coll->coll_type == COLL_RIGHT || coll->coll_type == COLL_LEFT)) { ok = false; goto cleanup; } } } cleanup: lara->move_angle = old_move_angle; return ok; } static bool M_HandleIdleState(ITEM *const item, COLL_INFO *const coll) { if (!M_CanMonkeySwing(item)) { return false; } if (!g_Input.action || item->hit_points <= 0) { M_MonkeySwingFall(item); return true; } item->gravity = false; item->fall_speed = 0; item->speed = 0; M_GetMonkeyCollisionInfo(item, coll, item->rot.y, NO_BAD_NEG, false); if (coll->side_mid.ceiling < -STEP_L) { // Lara is in a slow-swing state far below the ceiling. return false; } if (g_Input.forward && coll->coll_type != COLL_FRONT && ABS(coll->side_mid.ceiling - coll->side_front.ceiling) < M_MONKEY_CEILING_SHIFT) { item->goal_anim_state = LS(LS_MONKEY_FORWARD); } else if ( g_Input.step_left && M_TestMonkeySide(item, coll, -DEG_90, false)) { item->goal_anim_state = LS(LS_MONKEY_LEFT); } else if ( g_Input.step_right && M_TestMonkeySide(item, coll, DEG_90, true)) { item->goal_anim_state = LS(LS_MONKEY_RIGHT); } else if (g_Input.left) { item->goal_anim_state = LS(LS_MONKEY_TURN_LEFT); } else if (g_Input.right) { item->goal_anim_state = LS(LS_MONKEY_TURN_RIGHT); } Lara_Col_MonkeySwingSnap(item); return true; } static void M_MonkeyIdle(ITEM *const item, COLL_INFO *const coll) { if (M_HandleIdleState(item, coll)) { return; } // Monkey idle state can be the result of swinging on a thin ledge as well // as actually being on monkeybars. LA_SWING_IN_SLOW links to this state. Lara_Col_HangTest(item, coll); if (item->goal_anim_state != LS(LS_MONKEY_IDLE)) { return; } if (g_Input.forward && coll->side_front.floor > -850 && coll->side_front.floor < -650 && coll->side_front.floor - coll->side_front.ceiling >= 0 && coll->side_left2.floor - coll->side_left2.ceiling >= 0 && coll->side_right2.floor - coll->side_right2.ceiling >= 0 && !coll->hit_static) { item->goal_anim_state = LS(g_Input.slow ? LS_GYMNAST : LS_PULL_UP); return; } if (M_CanClimb(item, coll)) { item->goal_anim_state = LS(LS_HANG); item->current_anim_state = LS(LS_HANG); Item_SwitchToAnim(item, LA(LA_LADDER_UP_HANGING), 0); return; } if ((g_Input.forward || g_Input.crouch) && coll->side_front.floor > -850 && coll->side_front.floor < -650 && coll->side_front.floor - coll->side_front.ceiling >= -256 && coll->side_left2.floor - coll->side_left2.ceiling >= -256 && coll->side_right2.floor - coll->side_right2.ceiling >= -256 && !coll->hit_static) { item->goal_anim_state = LS(LS_CLIMB_TO_CRAWL); item->required_anim_state = LS(LS_CROUCH_IDLE); } else if (g_Input.left || g_Input.step_left) { item->goal_anim_state = LS(LS_SHIMMY_LEFT); } else if (g_Input.right || g_Input.step_right) { item->goal_anim_state = LS(LS_SHIMMY_RIGHT); } } static void M_MonkeyForward(ITEM *const item, COLL_INFO *const coll) { if (!g_Input.action || !M_CanMonkeySwing(item)) { M_MonkeySwingFall(item); return; } item->gravity = false; item->fall_speed = 0; M_GetMonkeyCollisionInfo(item, coll, item->rot.y, NO_BAD_NEG, false); if (coll->coll_type == COLL_FRONT || ABS(coll->side_mid.ceiling - coll->side_front.ceiling) > M_MONKEY_CEILING_SHIFT) { Item_SwitchToAnim(item, LA(LA_MONKEY_IDLE), 0); item->current_anim_state = LS(LS_MONKEY_IDLE); item->goal_anim_state = LS(LS_MONKEY_IDLE); Lara_Col_MonkeySwingSnap(item); return; } g_Camera.target_elevation = M_CAM_MONKEY_ELEVATION; Lara_Col_MonkeySwingSnap(item); } static void M_MonkeySide(ITEM *const item, COLL_INFO *const coll) { if (!g_Input.action || !M_CanMonkeySwing(item)) { M_MonkeySwingFall(item); return; } item->gravity = false; item->fall_speed = 0; item->speed = 0; const bool is_right = item->current_anim_state == LS(LS_MONKEY_RIGHT); const int16_t angle_delta = is_right ? DEG_90 : -DEG_90; if (M_TestMonkeySide(item, coll, angle_delta, is_right)) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y + angle_delta; g_Camera.target_elevation = M_CAM_MONKEY_ELEVATION; Lara_Col_MonkeySwingSnap(item); } else { Item_SwitchToAnim(item, LA(LA_MONKEY_IDLE), 0); item->current_anim_state = LS(LS_MONKEY_IDLE); item->goal_anim_state = LS(LS_MONKEY_IDLE); Lara_Col_MonkeySwingSnap(item); } } static void M_MonkeyTurn(ITEM *const item, COLL_INFO *const coll) { if (!g_Input.action || !M_CanMonkeySwing(item)) { M_MonkeySwingFall(item); return; } item->gravity = false; item->fall_speed = 0; item->speed = 0; M_GetMonkeyCollisionInfo(item, coll, item->rot.y, -STEPUP_HEIGHT, true); Lara_Col_MonkeySwingSnap(item); } static void M_MonkeyRoll(ITEM *const item, COLL_INFO *const coll) { M_MonkeyForward(item, coll); } // clang-format off REGISTER_LARA_COL(LS_MONKEY_IDLE, M_MonkeyIdle) REGISTER_LARA_COL(LS_MONKEY_FORWARD, M_MonkeyForward) REGISTER_LARA_COL(LS_MONKEY_LEFT, M_MonkeySide) REGISTER_LARA_COL(LS_MONKEY_RIGHT, M_MonkeySide) REGISTER_LARA_COL(LS_MONKEY_TURN_LEFT, M_MonkeyTurn) REGISTER_LARA_COL(LS_MONKEY_TURN_RIGHT, M_MonkeyTurn) REGISTER_LARA_COL(LS_MONKEY_ROLL, M_MonkeyRoll) // clang-format on ================================================ FILE: src/trx/game/lara/col/swim.c ================================================ #include #include #include #include #include #define M_HEIGHT_SURF 700 static bool M_TestWaterStepOut(ITEM *const item, const COLL_INFO *const coll) { if (coll->coll_type == COLL_FRONT || coll->side_mid.type == HT_BIG_SLOPE || coll->side_mid.type == HT_DIAGONAL || coll->side_mid.floor >= 0) { return false; } if (coll->side_mid.floor < -STEP_L / 2) { item->current_anim_state = LS(LS_WATER_OUT); item->goal_anim_state = LS(LS_STOP); Item_SwitchToAnim(item, LA(LA_ONWATER_TO_WADE), 0); } else if (item->goal_anim_state == LS(LS_SURF_LEFT)) { item->goal_anim_state = LS(LS_STEP_LEFT); } else if (item->goal_anim_state == LS(LS_SURF_RIGHT)) { item->goal_anim_state = LS(LS_STEP_RIGHT); } else { item->current_anim_state = LS(LS_WADE); item->goal_anim_state = LS(LS_WADE); Item_SwitchToAnim(item, LA(LA_WADE), 0); } item->pos.y += coll->side_front.floor + M_HEIGHT_SURF - 5; Lara_UpdateRoomToHeight(-LARA_HEIGHT / 2); item->gravity = false; item->rot.x = 0; item->rot.z = 0; item->speed = 0; item->fall_speed = 0; LARA_INFO *const lara = Lara_GetLaraInfo(); lara->water_status = LWS_WADE; return true; } static bool M_TestWaterClimbOut(ITEM *const item, const COLL_INFO *const coll) { const int32_t coll_hdif = ABS(coll->side_left2.floor - coll->side_right2.floor); if (coll->coll_type != COLL_FRONT || !g_Input.action || coll_hdif >= SLOPE_DIF) { return false; } if (coll->side_front.ceiling > 0 || coll->side_mid.ceiling > -STEPUP_HEIGHT) { return false; } LARA_INFO *const lara = Lara_GetLaraInfo(); if (g_Config.gameplay.fix_water_exit) { if (coll->side_front.type == HT_BIG_SLOPE) { return false; } } else if (item->rot.y != lara->move_angle) { return false; } if (lara->gun_status != LGS_ARMLESS && (lara->gun_status != LGS_READY || lara->gun_type != LGT_FLARE)) { return false; } const int32_t lara_hdif = coll->side_front.floor + M_HEIGHT_SURF; if (lara_hdif <= -STEP_L * 2 || lara_hdif > M_HEIGHT_SURF - STEPUP_HEIGHT) { return false; } const DIRECTION dir = Math_GetDirectionCone(item->rot.y, LARA_HANG_ANGLE); if (dir == DIR_UNKNOWN) { return false; } item->pos.y += lara_hdif - 5; Lara_UpdateRoomToHeight(-LARA_HEIGHT / 2); switch (dir) { case DIR_NORTH: item->pos.z = ROUND_TO_SECTOR(item->pos.z) + WALL_L + LARA_RADIUS; break; case DIR_EAST: item->pos.x = ROUND_TO_SECTOR(item->pos.x) + WALL_L + LARA_RADIUS; break; case DIR_SOUTH: item->pos.z = ROUND_TO_SECTOR(item->pos.z) - LARA_RADIUS; break; case DIR_WEST: item->pos.x = ROUND_TO_SECTOR(item->pos.x) - LARA_RADIUS; break; case DIR_UNKNOWN: return false; } if (lara_hdif < -STEP_L / 2) { Item_SwitchToAnim(item, LA(LA_ONWATER_TO_STAND_HIGH), 0); } else if (lara_hdif < STEP_L / 2) { Item_SwitchToAnim(item, LA(LA_ONWATER_TO_STAND_MEDIUM), 0); } else { Item_SwitchToAnim(item, LA(LA_ONWATER_TO_WADE_LOW), 0); } item->current_anim_state = LS(LS_WATER_OUT); item->goal_anim_state = LS(LS_STOP); item->rot.y = Math_DirectionToAngle(dir); item->rot.x = 0; item->rot.z = 0; item->gravity = false; item->speed = 0; item->fall_speed = 0; lara->gun_status = LGS_HANDS_BUSY; lara->water_status = LWS_ABOVE_WATER; return true; } static void M_TestWaterDepth(ITEM *const item, const COLL_INFO *const coll) { int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); const int32_t water_depth = Lara_GetWaterDepth(item->pos.x, item->pos.y, item->pos.z, room_num); if (g_Config.gameplay.fix_water_exit && water_depth == NO_HEIGHT) { item->pos = coll->old; item->fall_speed = 0; return; } if (water_depth == NO_HEIGHT || water_depth > STEP_L * 2) { return; } Item_SwitchToAnim(item, LA(LA_UNDERWATER_TO_STAND), 0); item->current_anim_state = LS(LS_WATER_OUT); item->goal_anim_state = LS(LS_STOP); item->rot.x = 0; item->rot.z = 0; item->gravity = false; item->speed = 0; item->fall_speed = 0; LARA_INFO *const lara = Lara_GetLaraInfo(); lara->water_status = LWS_WADE; item->pos.y = Room_GetHeight(sector, item->pos); } static void M_CommonSurface(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); coll->facing = lara->move_angle; int32_t obj_height = M_HEIGHT_SURF; if (g_Config.gameplay.enable_wading) { obj_height += 100; } Collide_GetCollisionInfo( coll, item->pos.x, item->pos.y + M_HEIGHT_SURF, item->pos.z, item->room_num, obj_height); Lara_Col_Shift(coll); if (coll->coll_type == COLL_LEFT) { item->rot.y += 5 * DEG_1; } else if (coll->coll_type == COLL_RIGHT) { item->rot.y -= 5 * DEG_1; } else if ( coll->coll_type != COLL_NONE || (coll->side_mid.floor < 0 && (coll->side_mid.type == HT_BIG_SLOPE || coll->side_mid.type == HT_DIAGONAL))) { item->fall_speed = 0; item->pos = coll->old; } const int32_t water_height = Room_GetWaterHeight(item->pos, item->room_num); if (water_height - item->pos.y <= -100) { item->current_anim_state = LS(LS_DIVE); item->goal_anim_state = LS(LS_SWIM); Item_SwitchToAnim(item, LA(LA_ONWATER_DIVE), 0); item->rot.x = -45 * DEG_1; item->fall_speed = 80; lara->water_status = LWS_UNDERWATER; return; } if (g_Config.gameplay.enable_wading) { M_TestWaterStepOut(item, coll); } else { M_TestWaterClimbOut(item, coll); } } static void M_ForwardSurface(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y; coll->bad_neg = -STEPUP_HEIGHT; M_CommonSurface(item, coll); if (g_Config.gameplay.enable_wading) { M_TestWaterClimbOut(item, coll); } } static void M_SideBackSurface(ITEM *const item, COLL_INFO *const coll) { int32_t angle = 0; switch (LS_U(item->current_anim_state)) { case LS_SURF_BACK: angle = -DEG_180; break; case LS_SURF_LEFT: angle = -DEG_90; break; case LS_SURF_RIGHT: angle = DEG_90; break; default: break; } LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y + angle; M_CommonSurface(item, coll); } static void M_Swim(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); if (item->rot.x < -DEG_90 || item->rot.x > DEG_90) { lara->move_angle = item->rot.y + DEG_180; } else { lara->move_angle = item->rot.y; } coll->facing = lara->move_angle; int32_t height; if (g_Config.gameplay.enable_wading) { height = (LARA_HEIGHT * Math_Sin(item->rot.x)) >> W2V_SHIFT; if (height < 0) { height = -height; } CLAMPL(height, 200); coll->bad_neg = -height; } else { height = LARA_HEIGHT_UW; } Collide_GetCollisionInfo( coll, item->pos.x, item->pos.y + height / 2, item->pos.z, item->room_num, height); Lara_Col_Shift(coll); switch (coll->coll_type) { case COLL_FRONT: if (item->rot.x > 35 * DEG_1) { item->rot.x += LARA_UW_WALL_DEFLECT; } else if (item->rot.x < -35 * DEG_1) { item->rot.x -= LARA_UW_WALL_DEFLECT; } else { item->fall_speed = 0; } break; case COLL_TOP: if (item->rot.x >= -45 * DEG_1) { item->rot.x -= LARA_UW_WALL_DEFLECT; } break; case COLL_TOP_FRONT: item->fall_speed = 0; break; case COLL_LEFT: item->rot.y += 5 * DEG_1; break; case COLL_RIGHT: item->rot.y -= 5 * DEG_1; break; case COLL_CLAMP: item->pos = coll->old; item->fall_speed = 0; return; } if (coll->side_mid.floor < 0) { item->rot.x += LARA_UW_WALL_DEFLECT; item->pos.y = coll->side_mid.floor + item->pos.y; } if (g_Config.gameplay.enable_wading && lara->water_status != LWS_CHEAT && !lara->extra_anim) { M_TestWaterDepth(item, coll); } } static void M_UWDeath(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->air = -1; lara->gun_status = LGS_HANDS_BUSY; item->hit_points = -1; const int32_t water_height = Room_GetWaterHeight(item->pos, item->room_num); if (water_height != NO_HEIGHT && water_height < item->pos.y - 100) { item->pos.y -= 5; } M_Swim(item, coll); } // clang-format off REGISTER_LARA_COL(LS_SURF_SWIM, M_ForwardSurface) REGISTER_LARA_COL(LS_SURF_TREAD, M_SideBackSurface) REGISTER_LARA_COL(LS_SURF_BACK, M_SideBackSurface) REGISTER_LARA_COL(LS_SURF_LEFT, M_SideBackSurface) REGISTER_LARA_COL(LS_SURF_RIGHT, M_SideBackSurface) REGISTER_LARA_COL(LS_SWIM, M_Swim) REGISTER_LARA_COL(LS_TREAD, M_Swim) REGISTER_LARA_COL(LS_GLIDE, M_Swim) REGISTER_LARA_COL(LS_DIVE, M_Swim) REGISTER_LARA_COL(LS_WATER_ROLL, M_Swim) REGISTER_LARA_COL(LS_UW_DEATH, M_UWDeath) // clang-format on ================================================ FILE: src/trx/game/lara/col.c ================================================ #include #include #include #include #include #include #define M_MONKEY_CEILING_SNAP 704 #define M_PUSH_TIMEOUT 15 static void (*m_CollisionRoutines[LS_NUMBER_OF])( ITEM *item, COLL_INFO *coll) = {}; static void M_Push( const COLL_ITEM *const item, COLL_INFO *const coll, const bool hit_on, const bool big_push) { ITEM *const target_item = Lara_GetItem(); int32_t dx = target_item->pos.x - item->pos.x; int32_t dz = target_item->pos.z - item->pos.z; const int32_t c = Math_Cos(item->rot.y); const int32_t s = Math_Sin(item->rot.y); int32_t rx = (c * dx - s * dz) >> W2V_SHIFT; int32_t rz = (c * dz + s * dx) >> W2V_SHIFT; const BOUNDS_16 *const bounds = &item->bounds; int32_t min_x = bounds->min.x; int32_t max_x = bounds->max.x; int32_t min_z = bounds->min.z; int32_t max_z = bounds->max.z; if (big_push) { max_x += coll->radius; min_z -= coll->radius; max_z += coll->radius; min_x -= coll->radius; } if (rx < min_x || rx > max_x || rz < min_z || rz > max_z) { return; } const int32_t l = rx - min_x; const int32_t r = max_x - rx; const int32_t t = max_z - rz; const int32_t b = rz - min_z; if (l <= r && l <= t && l <= b) { rx -= l; } else if (r <= l && r <= t && r <= b) { rx += r; } else if (t <= l && t <= r && t <= b) { rz += t; } else { rz = min_z; } target_item->pos.x = item->pos.x + ((rz * s + rx * c) >> W2V_SHIFT); target_item->pos.z = item->pos.z + ((rz * c - rx * s) >> W2V_SHIFT); rz = (bounds->max.z + bounds->min.z) / 2; rx = (bounds->max.x + bounds->min.x) / 2; dx -= (c * rx + s * rz) >> W2V_SHIFT; dz -= (c * rz - s * rx) >> W2V_SHIFT; if (hit_on && bounds->max.y - bounds->min.y > STEP_L) { Lara_TakeHit(target_item, dx, dz); } const int16_t old_facing = coll->facing; coll->bad_pos = NO_BAD_POS; coll->bad_neg = -STEPUP_HEIGHT; coll->bad_ceiling = 0; coll->facing = Math_Atan( target_item->pos.z - coll->old.z, target_item->pos.x - coll->old.x); Collide_GetCollisionInfo( coll, target_item->pos.x, target_item->pos.y, target_item->pos.z, target_item->room_num, LARA_HEIGHT); coll->facing = old_facing; if (coll->coll_type != COLL_NONE) { target_item->pos.x = coll->old.x; target_item->pos.z = coll->old.z; } else { coll->old = target_item->pos; Lara_UpdateRoomToHeight(-10); } LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (lara_info->interact_target.is_moving && lara_info->interact_target.move_count > M_PUSH_TIMEOUT) { lara_info->interact_target.is_moving = false; lara_info->gun_status = LGS_ARMLESS; } } void Lara_Col_Register( const LARA_TRX_STATE state, void (*const handle_func)(ITEM *item, COLL_INFO *coll)) { ASSERT(state >= 0 && state < LS_NUMBER_OF); m_CollisionRoutines[state] = handle_func; } void Lara_Col_Update(ITEM *const item, COLL_INFO *const coll) { const LARA_TRX_STATE state = LS_U(item->current_anim_state); if (state >= 0 && state < LS_NUMBER_OF && m_CollisionRoutines[state] != nullptr) { m_CollisionRoutines[state](item, coll); } } void Lara_Col_GetInfo(const ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); coll->facing = lara->move_angle; Collide_GetCollisionInfo( coll, item->pos.x, item->pos.y, item->pos.z, item->room_num, LARA_HEIGHT); } void Lara_Col_Shift(COLL_INFO *const coll) { Collide_ShiftItem(Lara_GetItem(), coll); } void Lara_Col_MonkeySwingSnap(ITEM *const item) { int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); const int32_t ceiling = Room_GetCeiling(sector, item->pos); if (ceiling != NO_HEIGHT) { item->pos.y = ceiling + M_MONKEY_CEILING_SNAP; } } void Lara_Col_ItemPush( const ITEM *const item, COLL_INFO *const coll, const bool hit_on, const bool big_push) { const COLL_ITEM src_item = { .bounds = Item_GetBestFrame(item)->bounds, .pos = item->pos, .rot = item->rot, }; M_Push(&src_item, coll, hit_on, big_push); } void Lara_Col_Static3DPush(const STATIC_MESH *const mesh, COLL_INFO *const coll) { const COLL_ITEM src_item = { .bounds = Object_Get3DStatic(mesh->static_num)->collision_bounds, .pos = mesh->pos, .rot = { .y = mesh->rot.y }, }; M_Push(&src_item, coll, false, true); } void Lara_Col_WadeSplash(ITEM *const item) { if (!g_Config.gameplay.enable_wading) { return; } const LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->water_status == LWS_CHEAT) { return; } const int32_t water_depth = Lara_GetWaterDepth( item->pos.x, item->pos.y, item->pos.z, item->room_num); const int32_t water_height = Room_GetWaterHeight(item->pos, item->room_num); const BOUNDS_16 *const bounds = &Item_GetBestFrame(item)->bounds; if (water_height != NO_HEIGHT && water_depth != NO_HEIGHT && bounds != nullptr && item->pos.y + bounds->min.y <= water_height && item->pos.y + bounds->max.y >= water_height && water_depth < LARA_SWIM_DEPTH - STEP_L) { Spawn_Splash(item); } } ================================================ FILE: src/trx/game/lara/col.h ================================================ #pragma once #include #include #include typedef enum { // clang-format off EDGE_CATCH_NEG = -1, EDGE_CATCH_NONE = 0, EDGE_CATCH_POS = 1, // clang-format on } EDGE_CATCH; typedef enum { LANDED_OK, LANDED_BAD, LANDED_HANDLED, } LANDED_STATE; typedef enum { SWING_CATCH_NONE, SWING_CATCH_FAST, SWING_CATCH_SLOW, } SWING_CATCH; void Lara_Col_Register( LARA_TRX_STATE state, void (*handle_func)(ITEM *item, COLL_INFO *coll)); void Lara_Col_Update(ITEM *item, COLL_INFO *coll); void Lara_Col_GetInfo(const ITEM *item, COLL_INFO *coll); void Lara_Col_Shift(COLL_INFO *coll); bool Lara_Col_TestVault(ITEM *item, COLL_INFO *coll); bool Lara_Col_TestSlide(ITEM *item, COLL_INFO *coll); bool Lara_Col_TestLadderHang(ITEM *item, const COLL_INFO *coll); bool Lara_Col_TestCeiling(ITEM *item, const COLL_INFO *coll); SWING_CATCH Lara_Col_TestHangSwingIn(const ITEM *item, int16_t angle); EDGE_CATCH Lara_Col_TestEdgeCatch( const ITEM *item, const COLL_INFO *coll, int32_t *edge); bool Lara_Col_Fallen(ITEM *item, const COLL_INFO *coll); void Lara_Col_DeflectEdgeJump(ITEM *item, COLL_INFO *coll); LANDED_STATE Lara_Col_LandedBad(ITEM *item); void Lara_Col_MonkeySwingSnap(ITEM *item); void Lara_Col_HangTest(ITEM *item, COLL_INFO *coll); void Lara_Col_ItemPush( const ITEM *item, COLL_INFO *coll, bool hit_on, bool big_push); void Lara_Col_Static3DPush(const STATIC_MESH *mesh, COLL_INFO *coll); void Lara_Col_WadeSplash(ITEM *item); void Lara_Col_CrawlTilt(ITEM *item); ================================================ FILE: src/trx/game/lara/common.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_MOVE_ANIM_VELOCITY 12 #define M_MOVE_SPEED 16 #define M_MOVE_ANGLE (2 * DEG_1) // = 364 static const LARA_TRX_ANIMATION m_InvalidInterpAnims[] = { // clang-format off LA_JUMP_NEUTRAL_ROLL, LA_JUMP_BACK_ROLL_START, LA_JUMP_BACK_ROLL_END, LA_CONTROLLED_DROP_CONTINUE, LA_HANG_TO_JUMP_BACK, LA_TRX_INVALID, // sentinel // clang-format on }; static LARA_INFO m_Lara = {}; static ITEM *m_LaraItem = nullptr; static bool m_Controllable = false; static int16_t m_DeathCameraTarget = NO_ITEM; static LARA_EXTRA_STATE m_StartAnimState = LS_EXTRA_BREATH; static bool M_IsInvalidInterpAnim(const LARA_TRX_ANIMATION anim_idx) { for (int32_t i = 0; m_InvalidInterpAnims[i] != LA_TRX_INVALID; i++) { if (m_InvalidInterpAnims[i] == anim_idx) { return true; } } return false; } LARA_INFO *Lara_GetLaraInfo(void) { return &m_Lara; } ITEM *Lara_GetItem(void) { return m_LaraItem; } void Lara_InitialiseLoad(int16_t item_num) { m_Lara.item_num = item_num; if (item_num == NO_ITEM) { m_LaraItem = nullptr; } else { m_LaraItem = Item_Get(item_num); } } static int32_t M_GetStartingHitPoints(void) { if (g_Config.gameplay.disable_healing_between_levels) { const GF_LEVEL *const current_level = Game_GetCurrentLevel(); RESUME_INFO *const resume = Savegame_GetCurrentInfo(current_level); if (resume != nullptr) { return resume->lara_hitpoints; } } return g_Config.gameplay.start_lara_hitpoints; } void Lara_Initialise(const GF_LEVEL *const level) { ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara_info = Lara_GetLaraInfo(); lara_item->collidable = false; m_Controllable = true; m_DeathCameraTarget = NO_ITEM; Lara_Vehicle_SetIndex(NO_ITEM); lara_item->hit_points = M_GetStartingHitPoints(); lara_info->gun_item_num = NO_ITEM; lara_info->flare.age = 0; lara_info->flare.control = false; lara_info->flare.frame_num = 0; lara_info->calc_fall_speed = 0; lara_info->pose_count = 0; lara_info->hit_direction = DIR_UNKNOWN; lara_info->hit_effect = nullptr; lara_info->hit_effect_count = 0; lara_info->hit_frame = 0; lara_info->air = LARA_MAX_AIR; lara_info->sprint_timer = LARA_MAX_SPRINT; lara_info->exposure_timer = LARA_MAX_EXPOSURE; lara_info->water_surface_dist = 100; lara_info->death_timer = 0; lara_info->dive_timer = 0; lara_info->idle_timer = 0; lara_info->current.active = 0; lara_info->extra_anim = false; lara_info->burn = false; lara_info->electric = 0; lara_info->climb_status = false; lara_info->sprinting = false; lara_info->killed_loyal_item = false; lara_info->mesh_effects = 0; lara_info->torso_rot.x = 0; lara_info->torso_rot.y = 0; lara_info->torso_rot.z = 0; lara_info->head_rot.x = 0; lara_info->head_rot.y = 0; lara_info->head_rot.z = 0; lara_info->move_angle = 0; lara_info->turn_rate = 0; lara_info->target = nullptr; lara_info->last_pos = lara_item->pos; lara_info->right_arm.flash_gun = 0; lara_info->left_arm.flash_gun = 0; lara_info->right_arm.lock = 0; lara_info->left_arm.lock = 0; lara_info->interact_target.is_moving = false; lara_info->interact_target.item_num = NO_ITEM; lara_info->interact_target.move_count = 0; lara_info->poison_timer = 0; lara_info->tr3_smoke_count_l = 0; lara_info->tr3_smoke_count_r = 0; lara_info->mesh_pos_matrices_valid = false; LOT_InitialiseLOT(&lara_info->lot); lara_info->lot.setup.step = WALL_L * 20; lara_info->lot.setup.drop = -WALL_L * 20; lara_info->lot.setup.fly = STEP_L; Lara_Skin_Initialise(); if (level->type == GFL_CUTSCENE) { Lara_Mesh_Initialise(level); lara_info->gun_status = LGS_ARMLESS; } else { Lara_InitialiseInventory(level); } Lara_Control_Initialise(level->type, m_StartAnimState); } void Lara_InitialiseInventory(const GF_LEVEL *const level) { Inv_RemoveAllItems(); LARA_INFO *const lara_info = Lara_GetLaraInfo(); RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); if (resume != nullptr) { lara_info->pistol_ammo.ammo = 1000; if (resume->flags.has_pistols) { Inv_AddItem(O_PISTOL_ITEM); } if (resume->flags.has_magnums) { Inv_AddItem(O_MAGNUM_ITEM); lara_info->magnum_ammo.ammo = resume->magnum_ammo; Item_GlobalReplace(O_MAGNUM_ITEM, O_MAGNUM_AMMO_ITEM); } else { Inv_AddItemNTimes( O_MAGNUM_AMMO_ITEM, resume->magnum_ammo / Gun_GetAmmoPickupQuantity(LGT_MAGNUMS)); lara_info->magnum_ammo.ammo = 0; } if (resume->flags.has_autos) { Inv_AddItem(O_AUTOS_ITEM); lara_info->autos_ammo.ammo = resume->autos_ammo; Item_GlobalReplace(O_AUTOS_ITEM, O_AUTOS_AMMO_ITEM); } else { Inv_AddItemNTimes( O_AUTOS_AMMO_ITEM, resume->autos_ammo / Gun_GetAmmoPickupQuantity(LGT_AUTOS)); lara_info->autos_ammo.ammo = 0; } if (resume->flags.has_desert_eagle) { Inv_AddItem(O_DESERT_EAGLE_ITEM); lara_info->desert_eagle_ammo.ammo = resume->desert_eagle_ammo; Item_GlobalReplace(O_DESERT_EAGLE_ITEM, O_DESERT_EAGLE_AMMO_ITEM); } else { Inv_AddItemNTimes( O_DESERT_EAGLE_AMMO_ITEM, resume->desert_eagle_ammo / Gun_GetAmmoPickupQuantity(LGT_DESERT_EAGLE)); lara_info->desert_eagle_ammo.ammo = 0; } if (resume->flags.has_uzis) { Inv_AddItem(O_UZI_ITEM); lara_info->uzi_ammo.ammo = resume->uzi_ammo; Item_GlobalReplace(O_UZI_ITEM, O_UZI_AMMO_ITEM); } else { Inv_AddItemNTimes( O_UZI_AMMO_ITEM, resume->uzi_ammo / Gun_GetAmmoPickupQuantity(LGT_UZIS)); lara_info->uzi_ammo.ammo = 0; } if (resume->flags.has_shotgun) { Inv_AddItem(O_SHOTGUN_ITEM); lara_info->shotgun_ammo.ammo = resume->shotgun_ammo; Item_GlobalReplace(O_SHOTGUN_ITEM, O_SHOTGUN_AMMO_ITEM); } else { Inv_AddItemNTimes( O_SHOTGUN_AMMO_ITEM, resume->shotgun_ammo / Gun_GetAmmoPickupQuantity(LGT_SHOTGUN)); lara_info->shotgun_ammo.ammo = 0; } Inv_AddItemNTimes(O_SMALL_MEDIPACK_ITEM, resume->small_medipacks); Inv_AddItemNTimes(O_LARGE_MEDIPACK_ITEM, resume->large_medipacks); Inv_AddItemNTimes(O_FLARE_ITEM, resume->flares); Inv_AddItemNTimes(O_SCION_ITEM_1, resume->num_scions); Inv_AddItemNTimes(O_QUEST_ITEM_1, resume->num_quest_item_1); Inv_AddItemNTimes(O_QUEST_ITEM_2, resume->num_quest_item_2); Inv_AddItemNTimes(O_QUEST_ITEM_3, resume->num_quest_item_3); Inv_AddItemNTimes(O_QUEST_ITEM_4, resume->num_quest_item_4); if (resume->flags.has_m16) { Inv_AddItem(O_M16_ITEM); lara_info->m16_ammo.ammo = resume->m16_ammo; Item_GlobalReplace(O_M16_ITEM, O_M16_AMMO_ITEM); } else { Inv_AddItemNTimes( O_M16_AMMO_ITEM, resume->m16_ammo / Gun_GetAmmoPickupQuantity(LGT_M16)); lara_info->m16_ammo.ammo = 0; } if (resume->flags.has_mp5) { Inv_AddItem(O_MP5_ITEM); lara_info->mp5_ammo.ammo = resume->mp5_ammo; Item_GlobalReplace(O_MP5_ITEM, O_MP5_AMMO_ITEM); } else { Inv_AddItemNTimes( O_MP5_AMMO_ITEM, resume->mp5_ammo / Gun_GetAmmoPickupQuantity(LGT_MP5)); lara_info->mp5_ammo.ammo = 0; } if (resume->flags.has_grenade) { Inv_AddItem(O_GRENADE_GUN_ITEM); lara_info->grenade_ammo.ammo = resume->grenade_ammo; Item_GlobalReplace(O_GRENADE_GUN_ITEM, O_GRENADE_AMMO_ITEM); } else { Inv_AddItemNTimes( O_GRENADE_AMMO_ITEM, resume->grenade_ammo / Gun_GetAmmoPickupQuantity(LGT_GRENADE)); lara_info->grenade_ammo.ammo = 0; } if (resume->flags.has_rocket) { Inv_AddItem(O_ROCKET_GUN_ITEM); lara_info->rocket_ammo.ammo = resume->rocket_ammo; Item_GlobalReplace(O_ROCKET_GUN_ITEM, O_ROCKET_AMMO_ITEM); } else { Inv_AddItemNTimes( O_ROCKET_AMMO_ITEM, resume->rocket_ammo / Gun_GetAmmoPickupQuantity(LGT_ROCKET)); lara_info->rocket_ammo.ammo = 0; } if (resume->flags.has_harpoon) { Inv_AddItem(O_HARPOON_ITEM); lara_info->harpoon_ammo.ammo = resume->harpoon_ammo; Item_GlobalReplace(O_HARPOON_ITEM, O_HARPOON_AMMO_ITEM); } else { Inv_AddItemNTimes( O_HARPOON_AMMO_ITEM, resume->harpoon_ammo / Gun_GetAmmoPickupQuantity(LGT_HARPOON)); lara_info->harpoon_ammo.ammo = 0; } if (g_Config.gameplay.remember_gun_status) { lara_info->gun_status = resume->gun_status; lara_info->gun_type = resume->equipped_gun_type; } lara_info->last_gun_type = resume->equipped_gun_type; lara_info->holsters_gun_type = resume->holsters_gun_type; lara_info->back_gun_type = resume->back_gun_type; } if (!g_Config.gameplay.remember_gun_status) { lara_info->gun_status = LGS_ARMLESS; lara_info->gun_type = lara_info->last_gun_type; } lara_info->request_gun_type = lara_info->last_gun_type; Lara_Mesh_Initialise(level); Gun_InitialiseNewWeapon(); Gun_EnsureReady(); } void Lara_RevertToPistolsIfNeeded(void) { if (g_Config.gameplay.remember_gun_status || !Inv_RequestItem(O_PISTOL_ITEM)) { return; } LARA_INFO *const lara_info = Lara_GetLaraInfo(); lara_info->last_gun_type = LGT_PISTOLS; lara_info->holsters_gun_type = LGT_PISTOLS; if (lara_info->gun_status != LGS_ARMLESS) { lara_info->holsters_gun_type = LGT_UNARMED; lara_info->request_gun_type = LGT_PISTOLS; lara_info->gun_type = LGT_PISTOLS; } if (Inv_RequestItem(O_SHOTGUN_ITEM)) { lara_info->back_gun_type = LGT_SHOTGUN; } else { lara_info->back_gun_type = LGT_UNARMED; } Gun_InitialiseNewWeapon(); Gun_SetLaraHolsterLMesh(lara_info->holsters_gun_type); Gun_SetLaraHolsterRMesh(lara_info->holsters_gun_type); Gun_SetLaraBackMesh(lara_info->back_gun_type); } void Lara_UseItem(const OBJECT_ID obj_id) { LARA_INFO *const lara_info = Lara_GetLaraInfo(); ITEM *const lara_item = Lara_GetItem(); LARA_GUN_TYPE request_gun_type = LGT_UNARMED; switch (obj_id) { case O_PISTOL_ITEM: case O_PISTOL_OPTION: request_gun_type = LGT_PISTOLS; break; case O_SHOTGUN_ITEM: case O_SHOTGUN_OPTION: request_gun_type = LGT_SHOTGUN; break; case O_MAGNUM_ITEM: case O_MAGNUM_OPTION: request_gun_type = LGT_MAGNUMS; break; case O_AUTOS_ITEM: case O_AUTOS_OPTION: request_gun_type = LGT_AUTOS; break; case O_DESERT_EAGLE_ITEM: case O_DESERT_EAGLE_OPTION: request_gun_type = LGT_DESERT_EAGLE; break; case O_UZI_ITEM: case O_UZI_OPTION: request_gun_type = LGT_UZIS; break; case O_HARPOON_ITEM: case O_HARPOON_OPTION: request_gun_type = LGT_HARPOON; break; case O_M16_ITEM: case O_M16_OPTION: request_gun_type = LGT_M16; break; case O_MP5_ITEM: case O_MP5_OPTION: request_gun_type = LGT_MP5; break; case O_GRENADE_GUN_ITEM: case O_GRENADE_GUN_OPTION: request_gun_type = LGT_GRENADE; break; case O_ROCKET_GUN_ITEM: case O_ROCKET_GUN_OPTION: request_gun_type = LGT_ROCKET; break; case O_FLAREBOX_ITEM: case O_FLAREBOX_OPTION: lara_info->request_gun_type = LGT_FLARE; break; case O_SMALL_MEDIPACK_ITEM: case O_SMALL_MEDIPACK_OPTION: if ((lara_item->hit_points > 0 && lara_item->hit_points < LARA_MAX_HITPOINTS) || lara_info->poison_timer != 0) { lara_info->poison_timer = 0; lara_item->hit_points += LARA_MAX_HITPOINTS / 2; CLAMPG(lara_item->hit_points, LARA_MAX_HITPOINTS); Inv_RemoveItem(O_SMALL_MEDIPACK_ITEM); Sound_Effect(SFX_MENU_MEDI, nullptr, SPM_ALWAYS); Stats_AddMedipacksUsed(0.5); } break; case O_LARGE_MEDIPACK_ITEM: case O_LARGE_MEDIPACK_OPTION: if ((lara_item->hit_points > 0 && lara_item->hit_points < LARA_MAX_HITPOINTS) || lara_info->poison_timer != 0) { lara_info->poison_timer = 0; lara_item->hit_points = LARA_MAX_HITPOINTS; Inv_RemoveItem(O_LARGE_MEDIPACK_ITEM); Sound_Effect(SFX_MENU_MEDI, nullptr, SPM_ALWAYS); Stats_AddMedipacksUsed(1); } break; case O_KEY_ITEM_1: case O_KEY_OPTION_1: case O_KEY_ITEM_2: case O_KEY_OPTION_2: case O_KEY_ITEM_3: case O_KEY_OPTION_3: case O_KEY_ITEM_4: case O_KEY_OPTION_4: case O_PUZZLE_ITEM_1: case O_PUZZLE_OPTION_1: case O_PUZZLE_ITEM_2: case O_PUZZLE_OPTION_2: case O_PUZZLE_ITEM_3: case O_PUZZLE_OPTION_3: case O_PUZZLE_ITEM_4: case O_PUZZLE_OPTION_4: case O_LEADBAR_ITEM: case O_LEADBAR_OPTION: case O_SCION_ITEM_1: case O_SCION_ITEM_2: case O_SCION_ITEM_3: case O_SCION_ITEM_4: case O_SCION_OPTION: { const int16_t receptacle_item_num = Object_FindReceptacle(obj_id); if (receptacle_item_num == NO_ITEM || lara_info->interact_target.item_num != NO_ITEM) { Sound_Effect(SFX_LARA_NO, nullptr, SPM_NORMAL); return; } lara_info->interact_target.item_num = receptacle_item_num; lara_info->interact_target.is_moving = true; lara_info->interact_target.move_count = 0; break; } default: break; } if (request_gun_type != LGT_UNARMED) { lara_info->request_gun_type = request_gun_type; if (lara_info->gun_status == LGS_ARMLESS && lara_info->gun_type == request_gun_type) { lara_info->gun_type = LGT_UNARMED; } } } void Lara_SetStartAnimState(const LARA_EXTRA_STATE state) { m_StartAnimState = state; } bool Lara_IsControllable(void) { return m_Controllable; } void Lara_SetControllable(const bool controllable) { m_Controllable = controllable; } bool Lara_CanInterpolate( const ITEM *const item, const int32_t frame_a, const int32_t frame_b) { if (item->frame_num == item->prev_frame_num) { return false; } const LARA_ANIMATION anim_idx = Item_GetRelativeAnim(item); if (!M_IsInvalidInterpAnim(LA_U(anim_idx))) { return true; } // Avoid the flip 180 command having a bad effect on interpolated frames // on rate 1 animations, such as neutral jump twist. TODO: improve this. const ANIM *const anim = Item_GetAnim(item); return !Anim_HasFXCommandBetween( anim, ITEM_ACTION_TURN_180, frame_a, frame_b); } ITEM *Lara_GetDeathCameraTarget(void) { return Item_Get(m_DeathCameraTarget); } void Lara_SetDeathCameraTarget(const int16_t item_num) { m_DeathCameraTarget = item_num; } OBJECT_ID Lara_GetAnimationObject(void) { const LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (lara_info->extra_anim) { return O_LARA_EXTRA; } const ITEM *const vehicle = Lara_Vehicle_GetItem(); if (vehicle == nullptr) { return O_LARA; } switch (vehicle->object_id) { case O_BOAT: return O_LARA_BOAT; case O_SKIDOO_FAST: return O_LARA_SKIDOO; default: return O_LARA_VEHICLE_ANIM; } } void Lara_Animate(ITEM *const item) { const ROOM *const room = Room_Get(item->room_num); LARA_INFO *const lara = Lara_GetLaraInfo(); item->prev_frame_num = item->frame_num; item->frame_num++; const ANIM *anim = Item_GetAnim(item); if (anim->num_changes > 0 && Item_GetAnimChange(item, anim)) { anim = Item_GetAnim(item); item->current_anim_state = anim->current_anim_state; } if (item->frame_num > anim->frame_end) { for (int32_t i = 0; i < anim->num_commands; i++) { const ANIM_COMMAND *const command = &anim->commands[i]; switch (command->type) { case AC_MOVE_ORIGIN: { const XYZ_16 *const pos = (XYZ_16 *)command->data; Item_Translate(item, pos->x, pos->y, pos->z); break; } case AC_JUMP_VELOCITY: { const ANIM_COMMAND_VELOCITY_DATA *const data = (ANIM_COMMAND_VELOCITY_DATA *)command->data; item->fall_speed = data->fall_speed; item->speed = data->speed; item->gravity = true; if (lara->calc_fall_speed != 0) { item->fall_speed = lara->calc_fall_speed; lara->calc_fall_speed = 0; } break; } case AC_ATTACK_READY: if (lara->gun_status != LGS_SPECIAL) { lara->gun_status = LGS_ARMLESS; } break; default: break; } } item->anim_num = anim->jump_anim_num; item->frame_num = anim->jump_frame_num; anim = Item_GetAnim(item); item->current_anim_state = anim->current_anim_state; } for (int32_t i = 0; i < anim->num_commands; i++) { const ANIM_COMMAND *const command = &anim->commands[i]; switch (command->type) { case AC_SOUND_FX: { const ANIM_COMMAND_EFFECT_DATA *const data = (ANIM_COMMAND_EFFECT_DATA *)command->data; Item_PlayAnimSFX(item, data); break; } case AC_EFFECT: { const ANIM_COMMAND_EFFECT_DATA *const data = (ANIM_COMMAND_EFFECT_DATA *)command->data; if (item->frame_num != data->frame_num) { break; } if (g_TRVersion == 3) { ItemAction_RunDirectWithFX( data->effect_num, item, data->fx_type); break; } const ANIM_COMMAND_ENVIRONMENT type = data->environment; const int32_t height = lara->water_surface_dist; if ((type == ACE_WATER && (height >= 0 || height == NO_HEIGHT)) || (type == ACE_LAND && height < 0 && height != NO_HEIGHT && !room->flags.swamp)) { break; } ItemAction_RunDirect(data->effect_num, item); break; } default: break; } } const int32_t rel_frame = item->frame_num - anim->frame_base; if (!item->gravity) { int32_t speed = anim->velocity; if (lara->water_status == LWS_WADE && room->flags.swamp) { speed /= 2; speed += (anim->acceleration * rel_frame) / 4; } else { speed += anim->acceleration * rel_frame; } item->speed = (int16_t)(speed >> 16); } else if (room->flags.swamp) { item->speed -= item->speed >> 3; if (ABS(item->speed) < 8) { item->speed = 0; item->gravity = false; } if (item->fall_speed > 128) { item->fall_speed /= 2; } item->fall_speed -= item->fall_speed / 4; CLAMPL(item->fall_speed, 4); } else { int32_t speed = anim->velocity + anim->acceleration * (rel_frame - 1); item->speed -= (int16_t)(speed >> 16); speed += anim->acceleration; item->speed += (int16_t)(speed >> 16); item->fall_speed += item->fall_speed < FAST_FALL_SPEED ? GRAVITY : 1; item->pos.y += item->fall_speed; } item->pos.x += (item->speed * Math_Sin(lara->move_angle)) >> W2V_SHIFT; item->pos.z += (item->speed * Math_Cos(lara->move_angle)) >> W2V_SHIFT; } void Lara_AnimateUntil(ITEM *lara_item, int32_t goal) { lara_item->goal_anim_state = goal; do { Lara_Animate(lara_item); } while (lara_item->current_anim_state != goal); } const ANIM_FRAME *Lara_GetHitFrame(const ITEM *const item) { const LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->hit_direction < 0) { return nullptr; } // clang-format off LARA_ANIMATION anim_idx; if (lara->is_crouched) { switch (lara->hit_direction) { case DIR_EAST: anim_idx = LA(LA_CROUCH_HIT_LEFT); break; case DIR_SOUTH: anim_idx = LA(LA_CROUCH_HIT_BACK); break; case DIR_WEST: anim_idx = LA(LA_CROUCH_HIT_RIGHT); break; default: anim_idx = LA(LA_CROUCH_HIT_FRONT); break; } } else { switch (lara->hit_direction) { case DIR_EAST: anim_idx = LA(LA_HIT_LEFT); break; case DIR_SOUTH: anim_idx = LA(LA_HIT_BACK); break; case DIR_WEST: anim_idx = LA(LA_HIT_RIGHT); break; default: anim_idx = LA(LA_HIT_FRONT); break; } } // clang-format on const OBJECT *const obj = Object_Get(item->object_id); const ANIM *const anim = Object_GetAnim(obj, anim_idx); return &anim->frame_ptr[lara->hit_frame]; } void Lara_TakeDamage(const int16_t damage, const bool hit_status) { if (g_Config.debug.enable_invulnerability) { return; } Item_TakeDamage(Lara_GetItem(), damage, hit_status); } // TODO: This does the same thing in principle as Lara_GetJointAbsPosition(). // Consider merging these functions into a single function. bool Lara_GetMeshPos(const LARA_MESH mesh, XYZ_32 *const out_pos) { ASSERT(out_pos != nullptr); const ITEM *const lara_item = Lara_GetItem(); const LARA_INFO *const lara = Lara_GetLaraInfo(); if (!lara->mesh_pos_matrices_valid) { return false; } MATRIX *const m = g_MatrixPtr; *m = lara->mesh_pos_matrices[mesh]; Matrix_TranslateRel32(*out_pos); *out_pos = (XYZ_32) { .x = (m->_03 >> W2V_SHIFT), .y = (m->_13 >> W2V_SHIFT), .z = (m->_23 >> W2V_SHIFT), }; return true; } bool Lara_TestBoundsCollide(const ITEM *const item, const int32_t radius) { return Item_TestBoundsCollide(item, Lara_GetItem(), radius); } bool Lara_TestPosition( const ITEM *const item, const OBJECT_BOUNDS *const bounds) { const ITEM *const lara = Lara_GetItem(); const XYZ_16 ref_rot = bounds->ignore_rot ? (XYZ_16) { .x = 0, .y = lara->rot.y, .z = 0 } : item->rot; const XYZ_16 rot = { .x = lara->rot.x - ref_rot.x, .y = lara->rot.y - ref_rot.y, .z = lara->rot.z - ref_rot.z, }; const XYZ_32 dist = { .x = lara->pos.x - item->pos.x, .y = lara->pos.y - item->pos.y, .z = lara->pos.z - item->pos.z, }; // clang-format off if (rot.x < bounds->rot.min.x || rot.x > bounds->rot.max.x || rot.y < bounds->rot.min.y || rot.y > bounds->rot.max.y || rot.z < bounds->rot.min.z || rot.z > bounds->rot.max.z ) { return false; } // clang-format on Matrix_PushUnit(); Matrix_Rot16(ref_rot); const MATRIX *const m = g_MatrixPtr; const XYZ_32 shift = { .x = (dist.x * m->_00 + dist.y * m->_10 + dist.z * m->_20) >> W2V_SHIFT, .y = (dist.x * m->_01 + dist.y * m->_11 + dist.z * m->_21) >> W2V_SHIFT, .z = (dist.x * m->_02 + dist.y * m->_12 + dist.z * m->_22) >> W2V_SHIFT, }; Matrix_Pop(); // clang-format off return ( shift.x >= bounds->shift.min.x && shift.x <= bounds->shift.max.x && shift.y >= bounds->shift.min.y && shift.y <= bounds->shift.max.y && shift.z >= bounds->shift.min.z && shift.z <= bounds->shift.max.z ); // clang-format on } void Lara_AlignPosition(const ITEM *const item, const XYZ_32 *const vec) { ITEM *const lara = Lara_GetItem(); lara->rot = item->rot; Matrix_PushUnit(); Matrix_Rot16(item->rot); const MATRIX *const m = g_MatrixPtr; const XYZ_32 shift = { .x = (vec->x * m->_00 + vec->y * m->_01 + vec->z * m->_02) >> W2V_SHIFT, .y = (vec->x * m->_10 + vec->y * m->_11 + vec->z * m->_12) >> W2V_SHIFT, .z = (vec->x * m->_20 + vec->y * m->_21 + vec->z * m->_22) >> W2V_SHIFT, }; Matrix_Pop(); const XYZ_32 new_pos = { .x = item->pos.x + shift.x, .y = item->pos.y + shift.y, .z = item->pos.z + shift.z, }; if (g_Config.gameplay.fix_lara_pickup_embed) { int16_t room_num = lara->room_num; const SECTOR *const sector = Room_GetSector(new_pos, &room_num); const int32_t height = Room_GetHeight(sector, new_pos); const int32_t ceiling = Room_GetCeiling(sector, new_pos); if (ABS(height - lara->pos.y) > STEP_L || ABS(ceiling - lara->pos.y) < LARA_HEIGHT) { return; } } lara->pos = new_pos; } bool Lara_IsNearItem(const XYZ_32 *const pos, const int32_t distance) { const ITEM *const item = Lara_GetItem(); const XYZ_32 d = { .x = pos->x - item->pos.x, .y = pos->y - item->pos.y, .z = pos->z - item->pos.z, }; if (ABS(d.x) > distance || ABS(d.z) > distance || ABS(d.y) > WALL_L * 3) { return false; } if (SQUARE(d.x) + SQUARE(d.z) > SQUARE(distance)) { return false; } const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item); return d.y >= bounds->min.y && d.y <= bounds->max.y + 100; } bool Lara_MovePosition(const ITEM *const ref_item, const XYZ_32 *const vec) { LARA_INFO *const lara_info = Lara_GetLaraInfo(); const bool walk_to_items = g_Config.gameplay.enable_walk_to_items && ref_item->object_id != O_FLARE_ITEM; const bool lara_on_land = lara_info->water_status != LWS_UNDERWATER && lara_info->water_status != LWS_CHEAT; const int32_t velocity = walk_to_items && lara_on_land ? M_MOVE_ANIM_VELOCITY : M_MOVE_SPEED; ITEM *const lara_item = Lara_GetItem(); const XYZ_16 new_rot = ref_item->rot; Matrix_PushUnit(); Matrix_Rot16(new_rot); const MATRIX *const m = g_MatrixPtr; const XYZ_32 shift = { .x = (vec->y * m->_01 + vec->z * m->_02 + vec->x * m->_00) >> W2V_SHIFT, .y = (vec->x * m->_10 + vec->z * m->_12 + vec->y * m->_11) >> W2V_SHIFT, .z = (vec->y * m->_21 + vec->x * m->_20 + vec->z * m->_22) >> W2V_SHIFT, }; Matrix_Pop(); const XYZ_32 new_pos = { .x = ref_item->pos.x + shift.x, .y = ref_item->pos.y + shift.y, .z = ref_item->pos.z + shift.z, }; if (ref_item->object_id == O_FLARE_ITEM) { int16_t room_num = lara_item->room_num; const SECTOR *const sector = Room_GetSector(new_pos, &room_num); const int32_t height = Room_GetHeight(sector, new_pos); if (ABS(height - lara_item->pos.y) > STEP_L * 2) { return false; } if (XYZ_32_GetDistance(new_pos, lara_item->pos) < STEP_L) { return true; } } const XYZ_32 dpos = { .x = new_pos.x - lara_item->pos.x, .y = new_pos.y - lara_item->pos.y, .z = new_pos.z - lara_item->pos.z, }; const int32_t length = XYZ_32_GetLength(dpos); if (velocity >= length) { lara_item->pos = new_pos; } else { lara_item->pos.x += velocity * dpos.x / length; lara_item->pos.y += velocity * dpos.y / length; lara_item->pos.z += velocity * dpos.z / length; } if (walk_to_items && !lara_info->interact_target.is_moving) { if (lara_on_land) { const int16_t step_to_anim_num[4] = { LA(LA_SIDE_STEP_LEFT), LA(LA_WALK_FORWARD), LA(LA_SIDE_STEP_RIGHT), LA(LA_WALK_BACK), }; const int16_t step_to_anim_state[4] = { LS(LS_STEP_LEFT), LS(LS_WALK), LS(LS_STEP_RIGHT), LS(LS_WALK_BACK), }; const int32_t dx = lara_item->pos.x - new_pos.x; const int32_t dz = lara_item->pos.z - new_pos.z; const int32_t angle = (DEG_360 - Math_Atan(dx, dz)) % DEG_360; const uint32_t src_quadrant = (uint32_t)(angle + DEG_45) / DEG_90; const uint32_t dst_quadrant = (uint32_t)(new_rot.y + DEG_45) / DEG_90; const DIRECTION quadrant = (src_quadrant - dst_quadrant) % 4; Item_SwitchToAnim(lara_item, step_to_anim_num[quadrant], 0); lara_item->goal_anim_state = step_to_anim_state[quadrant]; lara_item->current_anim_state = step_to_anim_state[quadrant]; lara_info->gun_status = LGS_HANDS_BUSY; } lara_info->interact_target.is_moving = lara_on_land; lara_info->interact_target.move_count = 0; } const int16_t rotation = M_MOVE_ANGLE; ITEM_ADJUST_ROT(lara_item->rot.x, new_rot.x, rotation); ITEM_ADJUST_ROT(lara_item->rot.y, new_rot.y, rotation); ITEM_ADJUST_ROT(lara_item->rot.z, new_rot.z, rotation); return XYZ_32_AreEquivalent(lara_item->pos, new_pos) && XYZ_16_AreEquivalent(lara_item->rot, new_rot); } LARA_ANIMATION Lara_AnimToGameID(const LARA_TRX_ANIMATION anim) { int32_t out; if (!Catalog_EnumToGameID(CATALOG_LARA_ANIMS, anim, &out)) { out = -1; } return out; } LARA_STATE Lara_StateToGameID(const LARA_TRX_STATE state) { int32_t out; if (!Catalog_EnumToGameID(CATALOG_LARA_STATES, state, &out)) { out = -1; } return out; } LARA_TRX_ANIMATION Lara_AnimFromGameID(const LARA_ANIMATION anim) { int32_t out; if (!Catalog_GameIDToEnum(CATALOG_LARA_ANIMS, anim, &out)) { out = -1; } return out; } LARA_TRX_STATE Lara_StateFromGameID(const LARA_STATE state) { int32_t out; if (!Catalog_GameIDToEnum(CATALOG_LARA_STATES, state, &out)) { out = -1; } return out; } ================================================ FILE: src/trx/game/lara/common.h ================================================ #pragma once #include #include #include #include #include #define LA(anim) Lara_AnimToGameID(anim) #define LA_U(anim) Lara_AnimFromGameID(anim) #define LS(state) Lara_StateToGameID(state) #define LS_U(state) Lara_StateFromGameID(state) LARA_INFO *Lara_GetLaraInfo(void); ITEM *Lara_GetItem(void); void Lara_Initialise(const GF_LEVEL *level); void Lara_InitialiseLoad(int16_t item_num); void Lara_InitialiseInventory(const GF_LEVEL *level); void Lara_RevertToPistolsIfNeeded(void); void Lara_UseItem(OBJECT_ID obj_id); void Lara_SetStartAnimState(LARA_EXTRA_STATE state); bool Lara_IsControllable(void); void Lara_SetControllable(bool controllable); bool Lara_CanInterpolate(const ITEM *item, int32_t frame_a, int32_t frame_b); ITEM *Lara_GetDeathCameraTarget(void); void Lara_SetDeathCameraTarget(int16_t item_num); OBJECT_ID Lara_GetAnimationObject(void); void Lara_Animate(ITEM *item); void Lara_AnimateUntil(ITEM *lara_item, int32_t goal); const ANIM_FRAME *Lara_GetHitFrame(const ITEM *item); void Lara_TakeDamage(int16_t damage, bool hit_status); bool Lara_GetMeshPos(LARA_MESH mesh, XYZ_32 *out_pos); bool Lara_TestBoundsCollide(const ITEM *item, int32_t radius); bool Lara_TestPosition(const ITEM *item, const OBJECT_BOUNDS *bounds); void Lara_AlignPosition(const ITEM *item, const XYZ_32 *vec); bool Lara_MovePosition(const ITEM *item, const XYZ_32 *vec); bool Lara_IsNearItem(const XYZ_32 *pos, int32_t distance); LARA_ANIMATION Lara_AnimToGameID(LARA_TRX_ANIMATION anim); LARA_STATE Lara_StateToGameID(LARA_TRX_STATE state); LARA_TRX_ANIMATION Lara_AnimFromGameID(LARA_ANIMATION anim); LARA_TRX_STATE Lara_StateFromGameID(LARA_STATE state); ================================================ FILE: src/trx/game/lara/const.h ================================================ #pragma once #include #include #define LARA_ORIGINAL_ANIM_COUNT (g_TRVersion == 1 ? 160 : 218) #define LARA_MAX_HITPOINTS 1000 #define LARA_MAX_AIR 1800 #define LARA_DIVE_WAIT 10 #define LARA_MAX_SPRINT (4 * LOGIC_FPS) #define LARA_MAX_EXPOSURE (20 * LOGIC_FPS) #define LARA_HEIGHT 762 #define LARA_HEIGHT_UW 400 #define LARA_HEIGHT_CROUCH 400 #define LARA_RADIUS 100 #define LARA_SWIM_DEPTH 730 #define LARA_TURN_UNDO (2 * DEG_1) // = 364 #define LARA_TURN_RATE ((DEG_1 / 4) + LARA_TURN_UNDO) // = 409 #define LARA_SLOW_TURN ((DEG_1 * 2) + LARA_TURN_UNDO) // = 728 #define LARA_MED_TURN ((DEG_1 * 4) + LARA_TURN_UNDO) // = 1092 #define LARA_LEAN_UNDO DEG_1 // = 182 #define LARA_LEAN_RATE 273 #define LARA_LEAN_MAX ((10 * DEG_1) + LARA_LEAN_UNDO) // = 2002 #define LARA_UW_WALL_DEFLECT (2 * DEG_1) // = 364 #define LARA_DEFLECT_ANGLE (5 * DEG_1) // = 910 #define LARA_HANG_ANGLE (35 * DEG_1) // = 6370 #define NO_BAD_POS (-NO_HEIGHT) // = 32512 #define NO_BAD_NEG (NO_HEIGHT) // = -32512 #define STEPUP_HEIGHT ((STEP_L * 3) / 2) // = 384 #define SLOPE_DIF 60 #define DAMAGE_START 140 #define DAMAGE_LENGTH 14 // TODO: move to merged game.c #define DEATH_WAIT (5 * 2 * LOGIC_FPS) // = 300 #define DEATH_WAIT_INPUT (2 * LOGIC_FPS) // = 60 #define CAM_WADE_ELEVATION (-22 * DEG_1) // = -4004 ================================================ FILE: src/trx/game/lara/control.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // clang-format off #define M_MAX_COLL_ROOMS 20 #define M_ITEM_COLL_DIST CREATURE_TARGET_DIST // = 4096 #define M_STATIC_COLL_DIST (WALL_L * 3) // = 3072 #define M_MOVE_TIMEOUT 90 #define M_UW_DAMAGE 5 #define M_SWAMP_DAMAGE 10 #define M_DIVE_TILT_MED (45 * DEG_1) // = 8190 #define M_DIVE_TILT_MAX (85 * DEG_1) // = 15470 #define M_DIVE_TILT_MAX_ALT (100 * DEG_1) // = 18200 #define M_RADIUS_SURF LARA_RADIUS // = 100 #define M_RADIUS_UW 300 #define M_WADE_DEPTH 256 #define M_LEAN_UNDO_SURF (LARA_LEAN_UNDO * 2) // = 364 #define M_LEAN_UNDO_UW M_LEAN_UNDO_SURF // = 364 #define M_LEAN_MAX_UW (LARA_LEAN_MAX * 2) // = 4004 // clang-format on static int32_t m_OpenDoorsCheatCooldown = 0; extern bool Skidoo_Control(void); extern bool UPV_Control(void); extern bool QuadBike_Control(void); extern bool Kayak_Control(void); extern bool MountedGun_Control(void); extern bool MineCart_Control(void); static SECTOR *M_GetCurrentSector(void) { const ITEM *const lara_item = Lara_GetItem(); int16_t room_num = lara_item->room_num; return Room_GetSector( (XYZ_32) { lara_item->pos.x, MAX_HEIGHT, lara_item->pos.z }, &room_num); } static void M_Cheat(void) { if (!g_Config.gameplay.enable_cheats) { return; } if (g_InputDB.level_skip_cheat) { Lara_Cheat_EndLevel(); } if (g_InputDB.item_cheat) { Lara_Cheat_GiveAllItems(); } const LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (lara_info->water_status != LWS_CHEAT && g_InputDB.fly_cheat) { Lara_Cheat_EnterFlyMode(); } } static void M_WaterCurrent_TR12(COLL_INFO *const coll) { ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara = Lara_GetLaraInfo(); int16_t room_num = lara_item->room_num; const ROOM *const room = Room_Get(lara_item->room_num); lara_item->box_num = Room_GetWorldSector(room, lara_item->pos.x, lara_item->pos.z)->box; XYZ_32 target; if (Box_CalculateTarget(&target, lara_item, &lara->lot) == TARGET_NONE) { return; } #define L_SHIFT(_axis) \ do { \ target._axis -= lara_item->pos._axis; \ if (target._axis > lara->current.active) { \ lara_item->pos._axis += lara->current.active; \ } else if (target._axis < -lara->current.active) { \ lara_item->pos._axis -= lara->current.active; \ } else { \ lara_item->pos._axis += target._axis; \ } \ } while (0) L_SHIFT(x); L_SHIFT(y); L_SHIFT(z); #undef L_SHIFT lara->current.active = 0; coll->facing = Math_Atan( lara_item->pos.z - coll->old.z, lara_item->pos.x - coll->old.x); Collide_GetCollisionInfo( coll, lara_item->pos.x, lara_item->pos.y + LARA_HEIGHT_UW / 2, lara_item->pos.z, room_num, LARA_HEIGHT_UW); switch (coll->coll_type) { case COLL_FRONT: if (lara_item->rot.x > 35 * DEG_1) { lara_item->rot.x += LARA_UW_WALL_DEFLECT; } else if (lara_item->rot.x < -35 * DEG_1) { lara_item->rot.x -= LARA_UW_WALL_DEFLECT; } else { lara_item->fall_speed = 0; } break; case COLL_TOP: lara_item->rot.x -= LARA_UW_WALL_DEFLECT; break; case COLL_TOP_FRONT: lara_item->fall_speed = 0; break; case COLL_LEFT: lara_item->rot.y += 5 * DEG_1; break; case COLL_RIGHT: lara_item->rot.y -= 5 * DEG_1; break; default: break; } if (coll->side_mid.floor < 0) { lara_item->pos.y += coll->side_mid.floor; lara_item->rot.x += LARA_UW_WALL_DEFLECT; } Lara_Col_Shift(coll); coll->old = lara_item->pos; } static void M_WaterCurrent_TR3(COLL_INFO *const coll) { ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->current.active != 0) { const OBJECT_VECTOR *const sink = Camera_GetFixedObject(lara->current.active - 1); const int32_t speed = sink->data; const int32_t angle = -Math_Atan(lara_item->pos.x - sink->x, lara_item->pos.z - sink->z) - DEG_90; lara->current.vel.x += (((speed * Math_Sin(angle)) >> 4) - lara->current.vel.x) >> 4; lara->current.vel.z += (((speed * Math_Cos(angle)) >> 4) - lara->current.vel.z) >> 4; lara_item->pos.y += (sink->y - lara_item->pos.y) >> 4; } else { int32_t shifter; int32_t abs_vel; abs_vel = ABS(lara->current.vel.x); if (abs_vel > 16) { shifter = 4; } else if (abs_vel > 8) { shifter = 3; } else { shifter = 2; } lara->current.vel.x -= lara->current.vel.x >> shifter; if (ABS(lara->current.vel.x) < 4) { lara->current.vel.x = 0; } abs_vel = ABS(lara->current.vel.z); if (abs_vel > 16) { shifter = 4; } else if (abs_vel > 8) { shifter = 3; } else { shifter = 2; } lara->current.vel.z -= lara->current.vel.z >> shifter; if (ABS(lara->current.vel.z) < 4) { lara->current.vel.z = 0; } if (!lara->current.vel.x && !lara->current.vel.z) { return; } } lara_item->pos.x += lara->current.vel.x >> 8; lara_item->pos.z += lara->current.vel.z >> 8; lara->current.active = 0; coll->facing = Math_Atan( lara_item->pos.z - coll->old.z, lara_item->pos.x - coll->old.x); Collide_GetCollisionInfo( coll, lara_item->pos.x, lara_item->pos.y + 200, lara_item->pos.z, lara_item->room_num, 400); switch (coll->coll_type) { case COLL_FRONT: if (lara_item->rot.x > 35 * DEG_1) { lara_item->rot.x += 2 * DEG_1; } else if (lara_item->rot.x < -35 * DEG_1) { lara_item->rot.x -= 2 * DEG_1; } else { lara_item->fall_speed = 0; } break; case COLL_TOP: lara_item->rot.x -= 2 * DEG_1; break; case COLL_TOP_FRONT: lara_item->fall_speed = 0; break; case COLL_LEFT: lara_item->rot.y += 5 * DEG_1; break; case COLL_RIGHT: lara_item->rot.y -= 5 * DEG_1; break; } if (coll->side_mid.floor < 0) { lara_item->pos.y += coll->side_mid.floor; } Lara_Col_Shift(coll); coll->old = lara_item->pos; } static void M_WaterCurrent(COLL_INFO *const coll) { if (g_TRVersion < 3) { M_WaterCurrent_TR12(coll); } else { M_WaterCurrent_TR3(coll); } } static void M_SoftStaticCollision(COLL_INFO *const coll) { ITEM *const lara_item = Lara_GetItem(); Room_GetNearbyRooms( lara_item->pos, coll->radius + 50, LARA_HEIGHT + 50, lara_item->room_num); for (int32_t i = 0; i < Room_DrawGetCount(); i++) { const ROOM *const room = Room_Get(Room_DrawGetRoom(i)); for (int32_t j = 0; j < room->num_static_meshes; j++) { const STATIC_MESH *const mesh = &room->static_meshes[j]; const STATIC_OBJECT_3D *const obj = Object_Get3DStatic(mesh->static_num); if (!obj->collidable || !XYZ_32_IsNearby( lara_item->pos, mesh->pos, M_STATIC_COLL_DIST)) { continue; } if (Item_TestStatic3DBoundsCollide(mesh, lara_item, coll->radius)) { Lara_Col_Static3DPush(mesh, coll); } } } } static void M_ObjectCollision(COLL_INFO *const coll) { ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara_info = Lara_GetLaraInfo(); lara_info->hit_direction = DIR_UNKNOWN; lara_item->hit_status = false; if (lara_item->hit_points <= 0) { return; } int16_t nearby_rooms[M_MAX_COLL_ROOMS]; const int32_t room_count = Room_GetAdjoiningRooms( lara_item->room_num, nearby_rooms, M_MAX_COLL_ROOMS); for (int32_t i = 0; i < room_count; i++) { const ROOM *const room = Room_Get(nearby_rooms[i]); int16_t item_num = room->item_num; while (item_num != NO_ITEM) { const ITEM *const item = Item_Get(item_num); // The collision routine can destroy the item - need to store the // next item beforehand. const int16_t next_item_num = item->next_item; if (lara_info->water_status == LWS_CHEAT && !Object_IsType(item->object_id, g_PickupObjects) && !Object_IsType(item->object_id, g_SwitchObjects)) { goto loop_end; } if (!item->collidable || item->status == IS_INVISIBLE) { goto loop_end; } const OBJECT *const obj = Object_Get(item->object_id); if (obj->collision_func == nullptr || !Item_IsNearby(lara_item, item, M_ITEM_COLL_DIST)) { goto loop_end; } obj->collision_func(item_num, lara_item, coll); loop_end: item_num = next_item_num; } } if (g_Config.gameplay.enable_soft_statics) { M_SoftStaticCollision(coll); } if (lara_info->hit_effect_count != 0 && lara_info->hit_effect != nullptr && coll->enable_hit) { const int32_t dx = lara_info->hit_effect->pos.x - lara_item->pos.x; const int32_t dz = lara_info->hit_effect->pos.z - lara_item->pos.z; Lara_TakeHit(lara_item, dx, dz); lara_info->hit_effect_count--; } if (lara_info->hit_direction < 0) { lara_info->hit_frame = 0; } } static void M_UpdateEnvironment(void) { ITEM *const item = Lara_GetItem(); LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (lara_info->extra_anim) { return; } if (Lara_Vehicle_IsMounted()) { return; } const ROOM *const room = Room_Get(item->room_num); const int32_t water_depth = Lara_GetWaterDepth( item->pos.x, item->pos.y, item->pos.z, item->room_num); const int32_t water_height = Room_GetWaterHeight(item->pos, item->room_num); const int32_t water_height_diff = water_height == NO_HEIGHT ? NO_HEIGHT : item->pos.y - water_height; lara_info->water_surface_dist = -water_height_diff; if (g_TRVersion == 3) { FX_Water_WadeSplash(item, water_height, water_depth); } else if ( g_Config.gameplay.enable_wading && lara_info->water_status != LWS_CHEAT) { // Create splash if Lara lands in wading height water. TR3+ feature. const BOUNDS_16 *const bounds = &Item_GetBestFrame(item)->bounds; if (bounds != nullptr && item->pos.y + bounds->min.y <= water_height && item->pos.y + bounds->max.y >= water_height && item->fall_speed > 0 && water_depth < LARA_SWIM_DEPTH - STEP_L) { Spawn_Splash(item); } } switch (lara_info->water_status) { case LWS_ABOVE_WATER: { if (g_Config.gameplay.enable_wading && (water_height_diff == NO_HEIGHT || water_height_diff < M_WADE_DEPTH)) { break; } if (water_depth > LARA_SWIM_DEPTH - STEP_L && !room->flags.swamp) { if (room->flags.underwater) { lara_info->air = LARA_MAX_AIR; lara_info->water_status = LWS_UNDERWATER; item->gravity = false; item->pos.y += 100; Lara_UpdateRoomToHeight(0); Sound_StopEffect(SFX_LARA_FALL); if (item->current_anim_state == LS(LS_SWAN_DIVE)) { item->rot.x = -M_DIVE_TILT_MED; item->goal_anim_state = LS(LS_DIVE); Lara_Animate(item); item->fall_speed *= 2; } else if (item->current_anim_state == LS(LS_FAST_DIVE)) { item->rot.x = -M_DIVE_TILT_MAX; item->goal_anim_state = LS(LS_DIVE); Lara_Animate(item); item->fall_speed *= 2; } else { item->rot.x = -M_DIVE_TILT_MED; Item_SwitchToAnim(item, LA(LA_FREEFALL_TO_UNDERWATER), 0); item->current_anim_state = LS(LS_DIVE); item->goal_anim_state = LS(LS_SWIM); item->fall_speed = (item->fall_speed * 3) / 2; } lara_info->head_rot.x = 0; lara_info->head_rot.y = 0; lara_info->torso_rot.x = 0; lara_info->torso_rot.y = 0; Spawn_Splash(item); } } else if ( g_Config.gameplay.enable_wading && water_height_diff > M_WADE_DEPTH) { lara_info->water_status = LWS_WADE; if (!item->gravity) { item->goal_anim_state = LS(LS_STOP); } else if (room->flags.swamp) { if (item->current_anim_state == LS(LS_SWAN_DIVE) || item->current_anim_state == LS(LS_FAST_DIVE)) { item->pos.y = water_height + 1000; } Item_SwitchToAnim(item, LA(LA_WADE), 0); item->current_anim_state = LS(LS_WADE); item->goal_anim_state = LS(LS_WADE); } } break; } case LWS_UNDERWATER: { if (room->flags.underwater) { break; } if (water_depth == NO_HEIGHT || ABS(water_height_diff) >= STEP_L) { lara_info->water_status = LWS_ABOVE_WATER; Item_SwitchToAnim(item, LA(LA_FALL_START), 0); item->current_anim_state = LS(LS_JUMP_FORWARD); item->goal_anim_state = LS(LS_JUMP_FORWARD); item->gravity = true; item->speed = item->fall_speed / 4; item->fall_speed = 0; item->rot.x = 0; item->rot.z = 0; lara_info->head_rot.x = 0; lara_info->head_rot.y = 0; lara_info->torso_rot.x = 0; lara_info->torso_rot.y = 0; if (g_TRVersion == 1) { lara_info->gun_status = LGS_ARMLESS; } } else { lara_info->water_status = LWS_SURFACE; Item_SwitchToAnim(item, LA(LA_UNDERWATER_TO_ONWATER), 0); item->current_anim_state = LS(LS_SURF_TREAD); item->goal_anim_state = LS(LS_SURF_TREAD); item->fall_speed = 0; item->pos.y += 1 - water_height_diff; item->rot.x = 0; item->rot.z = 0; lara_info->dive_timer = LARA_DIVE_WAIT + 1; lara_info->head_rot.x = 0; lara_info->head_rot.y = 0; lara_info->torso_rot.x = 0; lara_info->torso_rot.y = 0; Lara_UpdateRoomToHeight(-LARA_HEIGHT / 2); Sound_Effect(SFX_LARA_BREATH, &item->pos, SPM_ALWAYS); } break; } case LWS_SURFACE: { if (room->flags.underwater) { break; } if (g_Config.gameplay.enable_wading && water_height_diff > M_WADE_DEPTH) { lara_info->water_status = LWS_WADE; Item_SwitchToAnim(item, LA(LA_STAND_IDLE), 0); item->current_anim_state = LS(LS_STOP); item->goal_anim_state = LS(LS_WADE); Item_Animate(item); item->fall_speed = 0; } else { lara_info->water_status = LWS_ABOVE_WATER; Item_SwitchToAnim(item, LA(LA_FALL_START), 0); item->current_anim_state = LS(LS_JUMP_FORWARD); item->goal_anim_state = LS(LS_JUMP_FORWARD); item->gravity = true; item->speed = item->fall_speed / 4; if (g_TRVersion == 1) { lara_info->gun_status = LGS_ARMLESS; } } item->rot.x = 0; item->rot.z = 0; lara_info->head_rot.x = 0; lara_info->head_rot.y = 0; lara_info->torso_rot.x = 0; lara_info->torso_rot.y = 0; break; } case LWS_WADE: { g_Camera.target_elevation = CAM_WADE_ELEVATION; if (water_height_diff <= M_WADE_DEPTH) { lara_info->water_status = LWS_ABOVE_WATER; if (item->current_anim_state == LS(LS_WADE)) { item->goal_anim_state = LS(LS_RUN); } } else if (water_height_diff > LARA_SWIM_DEPTH && !room->flags.swamp) { lara_info->water_status = LWS_SURFACE; item->pos.y += 1 - water_height_diff; LARA_ANIMATION anim_idx; switch (LS_U(item->current_anim_state)) { case LS_WALK_BACK: item->goal_anim_state = LS(LS_SURF_BACK); anim_idx = LA(LA_ONWATER_IDLE_TO_SWIM_BACK); break; case LS_STEP_RIGHT: item->goal_anim_state = LS(LS_SURF_RIGHT); anim_idx = LA(LA_ONWATER_SWIM_RIGHT); break; case LS_STEP_LEFT: item->goal_anim_state = LS(LS_SURF_LEFT); anim_idx = LA(LA_ONWATER_SWIM_LEFT); break; default: item->goal_anim_state = LS(LS_SURF_SWIM); anim_idx = LA(LA_ONWATER_SWIM_FORWARD); break; } item->current_anim_state = item->goal_anim_state; Item_SwitchToAnim(item, anim_idx, 0); item->rot.z = 0; item->rot.x = 0; item->gravity = false; item->fall_speed = 0; lara_info->dive_timer = 0; lara_info->torso_rot.y = 0; lara_info->torso_rot.x = 0; lara_info->head_rot.y = 0; lara_info->head_rot.x = 0; Lara_UpdateRoomToHeight(-LARA_HEIGHT / 2); } break; } default: break; } } static void M_UndoRot(int16_t *const rot, const int16_t rate) { if (*rot < -rate) { *rot += rate; } else if (*rot > rate) { *rot -= rate; } else { *rot = 0; } } static void M_HandleAboveWater(COLL_INFO *const coll) { ITEM *const item = Lara_GetItem(); LARA_INFO *const lara_info = Lara_GetLaraInfo(); coll->old = item->pos; coll->old_anim_state = item->current_anim_state; coll->old_anim_num = item->anim_num; coll->old_frame_num = item->frame_num; coll->radius = LARA_RADIUS; coll->lava_is_pit = 0; coll->slopes_are_walls = 0; coll->slopes_are_pits = 0; coll->enable_hit = 1; coll->enable_baddie_push = 1; Lara_Look_Update(); const ITEM *const vehicle = Lara_Vehicle_GetItem(); if (vehicle != nullptr) { // TODO: make this Object_Get(…)->control switch (vehicle->object_id) { case O_SKIDOO_FAST: if (Skidoo_Control()) { return; } break; case O_QUAD_BIKE: if (QuadBike_Control()) { return; } break; case O_KAYAK: if (Kayak_Control()) { return; } break; case O_UPV: if (UPV_Control()) { return; } break; case O_MOUNTED_GUN: if (MountedGun_Control()) { coll->enable_hit = false; coll->enable_baddie_push = false; M_ObjectCollision(coll); return; } break; case O_MINE_CART: if (MineCart_Control()) { return; } break; default: Gun_Control(); return; } if (!Lara_Vehicle_IsMounted() && (lara_info->water_status == LWS_UNDERWATER || lara_info->water_status == LWS_SURFACE)) { // When dismounting an underwater vehicle, do not continue // with above-surface control, and instead run relevant // underwater or surface routines return; } } lara_info->is_crouched = false; Lara_State_Update(item, coll); M_UndoRot(&item->rot.x, LARA_LEAN_UNDO); M_UndoRot(&item->rot.z, LARA_LEAN_UNDO); M_UndoRot(&lara_info->turn_rate, LARA_TURN_UNDO); item->rot.y += lara_info->turn_rate; Lara_Animate(item); const SECTOR *const sector = M_GetCurrentSector(); if (!lara_info->extra_anim && lara_info->water_status != LWS_CHEAT) { M_ObjectCollision(coll); if (!Lara_Vehicle_IsMounted() && !lara_info->extra_anim) { Lara_Col_Update(item, coll); } } Lara_UpdateRoomToHeight(-LARA_HEIGHT / 2); Gun_Control(); Room_TestSectorTrigger(item, sector); } static void M_HandleUnderwater(COLL_INFO *const coll) { ITEM *const item = Lara_GetItem(); LARA_INFO *const lara_info = Lara_GetLaraInfo(); coll->old = item->pos; coll->radius = M_RADIUS_UW; coll->bad_pos = NO_BAD_POS; coll->bad_neg = -LARA_HEIGHT_UW; coll->bad_ceiling = LARA_HEIGHT_UW; coll->slopes_are_walls = 0; coll->slopes_are_pits = 0; coll->lava_is_pit = 0; coll->enable_hit = 0; coll->enable_baddie_push = 1; Lara_Look_Update(); Lara_State_Update(item, coll); if (item->rot.z > M_LEAN_UNDO_UW) { item->rot.z -= M_LEAN_UNDO_UW; } else if (item->rot.z < -M_LEAN_UNDO_UW) { item->rot.z += M_LEAN_UNDO_UW; } else { item->rot.z = 0; } if (g_Config.gameplay.enable_tr2_swimming) { CLAMP(item->rot.x, -M_DIVE_TILT_MAX, M_DIVE_TILT_MAX); CLAMP(item->rot.z, -M_LEAN_MAX_UW, M_LEAN_MAX_UW); if (lara_info->turn_rate < -LARA_TURN_UNDO) { lara_info->turn_rate += LARA_TURN_UNDO; } else if (lara_info->turn_rate > LARA_TURN_UNDO) { lara_info->turn_rate -= LARA_TURN_UNDO; } else { lara_info->turn_rate = 0; } item->rot.y += lara_info->turn_rate; } else { CLAMP(item->rot.x, -M_DIVE_TILT_MAX_ALT, M_DIVE_TILT_MAX_ALT); CLAMP(item->rot.z, -M_LEAN_MAX_UW, M_LEAN_MAX_UW); } if (lara_info->current.active && lara_info->water_status != LWS_CHEAT) { M_WaterCurrent(coll); } else { LOT_ClearLOT(&lara_info->lot); } Lara_Animate(item); item->pos.y -= (item->fall_speed * Math_Sin(item->rot.x)) >> (W2V_SHIFT + 2); item->pos.x += (Math_Cos(item->rot.x) * ((item->fall_speed * Math_Sin(item->rot.y)) >> (W2V_SHIFT + 2))) >> W2V_SHIFT; item->pos.z += (Math_Cos(item->rot.x) * ((item->fall_speed * Math_Cos(item->rot.y)) >> (W2V_SHIFT + 2))) >> W2V_SHIFT; const SECTOR *const sector = M_GetCurrentSector(); if (!lara_info->extra_anim) { M_ObjectCollision(coll); } if (lara_info->water_status == LWS_CHEAT) { if (m_OpenDoorsCheatCooldown > 0) { m_OpenDoorsCheatCooldown--; } else if (g_Input.draw) { m_OpenDoorsCheatCooldown = LOGIC_FPS; Lara_Cheat_OpenNearestDoor(); } } if (!Lara_Vehicle_IsMounted() && !lara_info->extra_anim) { Lara_Col_Update(item, coll); } Lara_UpdateRoomToHeight(0); Gun_Control(); Room_TestSectorTrigger(item, sector); } static void M_HandleSurface(COLL_INFO *const coll) { ITEM *const item = Lara_GetItem(); LARA_INFO *const lara_info = Lara_GetLaraInfo(); g_Camera.target_elevation = CAM_WADE_ELEVATION; coll->old = item->pos; coll->radius = M_RADIUS_SURF; coll->bad_pos = NO_BAD_POS; coll->bad_neg = g_TRVersion == 1 ? -100 : -STEP_L / 2; coll->bad_ceiling = 100; coll->slopes_are_walls = 0; coll->slopes_are_pits = 0; coll->lava_is_pit = 0; coll->enable_hit = 0; coll->enable_baddie_push = 0; Lara_Look_Update(); Lara_State_Update(item, coll); if (item->rot.z > M_LEAN_UNDO_SURF) { item->rot.z -= M_LEAN_UNDO_SURF; } else if (item->rot.z < -M_LEAN_UNDO_SURF) { item->rot.z += M_LEAN_UNDO_SURF; } else { item->rot.z = 0; } if (lara_info->current.active && lara_info->water_status != LWS_CHEAT) { M_WaterCurrent(coll); } else { LOT_ClearLOT(&lara_info->lot); } Lara_Animate(item); item->pos.x += (item->fall_speed * Math_Sin(lara_info->move_angle)) >> (W2V_SHIFT + 2); item->pos.z += (item->fall_speed * Math_Cos(lara_info->move_angle)) >> (W2V_SHIFT + 2); const SECTOR *const sector = M_GetCurrentSector(); M_ObjectCollision(coll); if (!Lara_Vehicle_IsMounted() && !lara_info->extra_anim) { Lara_Col_Update(item, coll); } Lara_UpdateRoomToHeight(100); Gun_Control(); Room_TestSectorTrigger(item, sector); } static void M_HandleExposure(void) { if (!Level_HasColdWater()) { return; } LARA_INFO *const lara_info = Lara_GetLaraInfo(); switch (lara_info->water_status) { case LWS_ABOVE_WATER: lara_info->exposure_timer++; CLAMPG(lara_info->exposure_timer, LARA_MAX_EXPOSURE); break; case LWS_WADE: lara_info->exposure_timer--; break; case LWS_UNDERWATER: case LWS_SURFACE: lara_info->exposure_timer -= 2; break; case LWS_CHEAT: lara_info->exposure_timer = LARA_MAX_EXPOSURE; break; } if (lara_info->exposure_timer < 0) { lara_info->exposure_timer = -1; Lara_TakeDamage(10, false); } } static void M_HandleEnvironment(void) { ITEM *const item = Lara_GetItem(); LARA_INFO *const lara_info = Lara_GetLaraInfo(); COLL_INFO coll = {}; if (item->current_anim_state != LS(LS_SPRINT)) { lara_info->sprint_timer++; CLAMPG(lara_info->sprint_timer, LARA_MAX_SPRINT); } if (item->current_anim_state != LS(LS_STOP) && item->current_anim_state != LS(LS_POSE)) { lara_info->idle_timer = 0; } switch (lara_info->water_status) { case LWS_ABOVE_WATER: case LWS_WADE: { const ROOM *const room = Room_Get(item->room_num); if (room->flags.swamp && lara_info->water_surface_dist < -775) { if (item->hit_points >= 0) { lara_info->air -= 6; if (lara_info->air < 0) { lara_info->air = -1; Lara_TakeDamage(M_SWAMP_DAMAGE, false); } } } else if (!Lara_Vehicle_IsOnType(O_UPV) && item->hit_points >= 0) { // TODO: make option for air replenish mode lara_info->air += g_TRVersion >= 3 ? 10 : LARA_MAX_AIR; CLAMPG(lara_info->air, LARA_MAX_AIR); } M_HandleAboveWater(&coll); break; } case LWS_UNDERWATER: if (item->hit_points >= 0) { lara_info->air--; if (lara_info->air < 0) { lara_info->air = -1; Lara_TakeDamage(M_UW_DAMAGE, false); } } M_HandleUnderwater(&coll); break; case LWS_SURFACE: if (item->hit_points >= 0) { lara_info->air += 10; CLAMPG(lara_info->air, LARA_MAX_AIR); } M_HandleSurface(&coll); break; case LWS_CHEAT: item->hit_points = LARA_MAX_HITPOINTS; lara_info->poison_timer = 0; lara_info->death_timer = 0; M_HandleUnderwater(&coll); if (g_InputDB.slow && !g_Input.look && !g_Input.fly_cheat) { Lara_Cheat_ExitFlyMode(); } break; default: break; } M_HandleExposure(); } static void M_HandleStartState(const LARA_EXTRA_STATE start_state) { ITEM *const lara_item = Lara_GetItem(); const LARA_INFO *const lara_info = Lara_GetLaraInfo(); const XYZ_16 old_rot = lara_item->rot; Lara_SwitchToExtraState(start_state); if (g_Config.gameplay.enable_cinematics) { Camera_InvokeCinematic(lara_item, 0, 0); return; } // Skip the starting cinematic, but force animation control to play out to // honour extra state specifics. COLL_INFO coll = {}; do { Lara_State_Update(lara_item, &coll); Lara_Animate(lara_item); } while (lara_info->extra_anim); lara_item->rot = old_rot; } void Lara_Control_Initialise( const GF_LEVEL_TYPE level_type, const LARA_EXTRA_STATE start_state) { ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara_info = Lara_GetLaraInfo(); if ((level_type == GFL_NORMAL || level_type == GFL_BONUS) && start_state != LS_EXTRA_BREATH) { lara_info->water_status = LWS_ABOVE_WATER; M_HandleStartState(start_state); } else if (Room_Get(lara_item->room_num)->flags.underwater) { lara_info->water_status = LWS_UNDERWATER; lara_item->fall_speed = 0; lara_item->goal_anim_state = LS(LS_TREAD); lara_item->current_anim_state = LS(LS_TREAD); Item_SwitchToAnim(lara_item, LA(LA_UNDERWATER_IDLE), 0); } else { lara_info->water_status = LWS_ABOVE_WATER; lara_item->goal_anim_state = LS(LS_STOP); lara_item->current_anim_state = LS(LS_STOP); Item_SwitchToAnim(lara_item, LA(LA_STAND_STILL), 0); } } void Lara_Control(void) { ITEM *const item = Lara_GetItem(); LARA_INFO *const lara_info = Lara_GetLaraInfo(); const int32_t time4 = (int32_t)Output_GetTimeInGame() * 4; if (lara_info->poison_timer >= 16 && (time4 & 0xFF) == 0) { CLAMPG(lara_info->poison_timer, 256); Lara_TakeDamage(lara_info->poison_timer >> 4, false); } if (lara_info->electric != 0) { if (lara_info->electric < 16) { lara_info->electric++; } Lara_Electricity_UpdatePoints(); Lara_Electricity_EmitLight(); } if (lara_info->has_fired && (time4 & 0x7F) == 0) { Creature_AlertNearbyGuards(item); lara_info->has_fired = false; } if (item->hit_points > 0 && g_Config.debug.enable_invulnerability) { item->hit_points = LARA_MAX_HITPOINTS; lara_info->poison_timer = 0; } M_Cheat(); if (lara_info->interact_target.is_moving && lara_info->interact_target.move_count++ > M_MOVE_TIMEOUT) { lara_info->interact_target.is_moving = false; lara_info->gun_status = LGS_ARMLESS; } M_UpdateEnvironment(); if (item->hit_points <= 0) { item->hit_points = -1; if (Game_IsInGym()) { Gym_SetInventoryOpenEnabled(true); } if (lara_info->death_timer == 0) { Music_Stop(); Stats_AddDeath(); } lara_info->death_timer++; lara_info->target = nullptr; if ((item->flags & IF_ONE_SHOT) != 0) { lara_info->death_timer++; return; } } else if (Room_IsAbyssHeight(item->pos.y)) { item->hit_points = -1; lara_info->death_timer = 9 * LOGIC_FPS; } Camera_MoveManual(); M_HandleEnvironment(); Lara_Breath_Control(item); Stats_AddDistanceTravelled(item->pos, lara_info->last_pos); lara_info->last_pos = item->pos; } ================================================ FILE: src/trx/game/lara/control.h ================================================ #pragma once #include #include void Lara_Control_Initialise( GF_LEVEL_TYPE level_type, LARA_EXTRA_STATE start_state); void Lara_Control(void); ================================================ FILE: src/trx/game/lara/draw.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include static bool m_CacheMatrices = false; static bool m_IsLara = true; static GAME_VECTOR M_GetLaraLightSample(const ITEM *const item) { GAME_VECTOR sample_pos = { .pos = { 0, 0, 0 }, .room_num = item->room_num, }; Lara_GetJointAbsPosition(&sample_pos.pos, LM_TORSO); return sample_pos; } static void M_CacheMatrix(const LARA_MESH mesh) { if (!m_CacheMatrices) { return; } LARA_INFO *const lara = Lara_GetLaraInfo(); lara->mesh_pos_matrices[mesh] = *g_WMatrixPtr; } static void M_DrawEquipmentMesh( const OBJECT_MESH *const mesh, const CLIP clip, const bool interpolated) { const GAME_VECTOR pos = { .room_num = Lara_GetItem()->room_num, .pos = Matrix_MulVec32_M( g_WMatrixPtr, (XYZ_32) { mesh->center.x, mesh->center.y - 24, mesh->center.z, }), }; Output_PushTintOverride(Lara_GetMeshTint(pos)); if (interpolated) { Output_DrawObjectMesh_I(mesh, clip); } else { Output_DrawObjectMesh(mesh, clip); } Output_PopTintOverride(); } static void M_DrawLaraMesh( const ITEM *const item, const LARA_MESH mesh_num, const CLIP clip, const bool interpolated) { if (m_IsLara && !Item_IsMeshVisible(item, mesh_num)) { return; } const OBJECT_MESH *const mesh = Lara_Mesh_Get(mesh_num); XYZ_32 origin = XYZ_32_From16(mesh->center); switch (mesh_num) { case LM_TORSO: case LM_CALF_L: case LM_CALF_R: case LM_FOOT_L: case LM_FOOT_R: origin.y -= 24; break; case LM_HEAD: origin.y -= 8; break; default: break; } const GAME_VECTOR pos = { .room_num = item->room_num, .pos = Matrix_MulVec32_M(g_WMatrixPtr, origin), }; Output_PushTintOverride(Lara_GetMeshTint(pos)); if (interpolated) { Output_DrawObjectMesh_I(mesh, clip); } else { Output_DrawObjectMesh(mesh, clip); } Output_PopTintOverride(); } static void M_DrawBodyPart( const LARA_MESH mesh, const ANIM_BONE *const bone, const XYZ_16 *mesh_rots_1, const XYZ_16 *mesh_rots_2, const CLIP clip) { const LARA_INFO *const lara = Lara_GetLaraInfo(); if (mesh_rots_2 != nullptr) { Matrix_TranslateRel32_I(bone[mesh - 1].pos); Matrix_Rot16_ID(mesh_rots_1[mesh], mesh_rots_2[mesh]); M_CacheMatrix(mesh); M_DrawLaraMesh(Lara_GetItem(), mesh, clip, true); } else { Matrix_TranslateRel32(bone[mesh - 1].pos); Matrix_Rot16(mesh_rots_1[mesh]); M_CacheMatrix(mesh); M_DrawLaraMesh(Lara_GetItem(), mesh, clip, false); } } static inline void M_DrawEquipment( const LARA_MESH mesh, const CLIP clip, const bool interpolated) { if (!m_IsLara) { return; } if (!Item_IsMeshVisible(Lara_GetItem(), mesh)) { return; } const LARA_SKIN_EQUIPMENT *const equipment = Lara_Skin_GetEquipment(mesh); if (!equipment->visible || equipment->mesh == nullptr) { return; } M_DrawEquipmentMesh(equipment->mesh, clip, interpolated); } static bool M_Draw_I( const ITEM *const item, const ANIM_FRAME *const frame1, const ANIM_FRAME *const frame2, const int32_t frac, const int32_t rate) { const OBJECT *const obj = Object_Get(item->object_id); LARA_INFO *const lara = Lara_GetLaraInfo(); const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item); if (!Lara_Vehicle_IsMounted()) { Output_DrawShadow(obj->shadow_size, bounds, item); } MATRIX saved_matrix = *g_MatrixPtr; MATRIX wsaved_matrix = *g_WMatrixPtr; Matrix_Push(); Matrix_TranslateAbs32(item->interp.result.pos); Matrix_Rot16(item->interp.result.rot); const CLIP clip = Output_CheckBoundsClip(&frame1->bounds); if (clip == CLIP_NOT_VISIBLE && !m_IsLara) { m_CacheMatrices = false; Matrix_Pop(); return false; } if (g_Config.debug.enable_debug_bounding_boxes) { Output_DrawCuboid(&frame1->bounds); } m_CacheMatrices = m_IsLara; if (m_CacheMatrices) { lara->mesh_pos_matrices_valid = false; } Matrix_Push(); Output_CalculateObjectLightingAt(item, M_GetLaraLightSample(item)); const ANIM_BONE *const bone = m_IsLara ? Lara_Skin_GetBoneBase() : Object_GetBone(obj, 0); const XYZ_16 *mesh_rots_1 = frame1->mesh_rots; const XYZ_16 *mesh_rots_2 = frame2->mesh_rots; Matrix_InitInterpolate(frac, rate); Matrix_TranslateRel16_ID(frame1->offset, frame2->offset); Matrix_Rot16_ID(mesh_rots_1[LM_HIPS], mesh_rots_2[LM_HIPS]); M_CacheMatrix(LM_HIPS); M_DrawLaraMesh(item, LM_HIPS, clip, true); M_DrawEquipment(LM_HIPS, clip, true); Matrix_Push_I(); M_DrawBodyPart(LM_THIGH_L, bone, mesh_rots_1, mesh_rots_2, clip); M_DrawEquipment(LM_THIGH_L, clip, true); M_DrawBodyPart(LM_CALF_L, bone, mesh_rots_1, mesh_rots_2, clip); M_DrawBodyPart(LM_FOOT_L, bone, mesh_rots_1, mesh_rots_2, clip); Matrix_Pop_I(); Matrix_Push_I(); M_DrawBodyPart(LM_THIGH_R, bone, mesh_rots_1, mesh_rots_2, clip); M_DrawEquipment(LM_THIGH_R, clip, true); M_DrawBodyPart(LM_CALF_R, bone, mesh_rots_1, mesh_rots_2, clip); M_DrawBodyPart(LM_FOOT_R, bone, mesh_rots_1, mesh_rots_2, clip); Matrix_Pop_I(); Matrix_TranslateRel32_I(bone[LM_TORSO - 1].pos); if (Lara_IsM16Active()) { mesh_rots_2 = lara->right_arm.frame_base[lara->right_arm.frame_num].mesh_rots; mesh_rots_1 = mesh_rots_2; } Matrix_Rot16_ID(mesh_rots_1[LM_TORSO], mesh_rots_2[LM_TORSO]); Matrix_Rot16_I(lara->interp.result.torso_rot); M_CacheMatrix(LM_TORSO); M_DrawLaraMesh(item, LM_TORSO, clip, true); M_DrawEquipment(LM_TORSO, clip, true); Matrix_Push_I(); Matrix_TranslateRel32_I(bone[LM_HEAD - 1].pos); Matrix_Rot16_ID(mesh_rots_1[LM_HEAD], mesh_rots_2[LM_HEAD]); Matrix_Rot16_I(lara->interp.result.head_rot); M_CacheMatrix(LM_HEAD); M_DrawLaraMesh(item, LM_HEAD, clip, true); M_DrawEquipment(LM_HEAD, clip, true); *g_MatrixPtr = saved_matrix; *g_WMatrixPtr = wsaved_matrix; if (m_IsLara) { Lara_Hair_Draw(); } Matrix_Pop_I(); LARA_GUN_TYPE gun_type = LGT_UNARMED; if (lara->gun_status == LGS_READY || lara->gun_status == LGS_SPECIAL || lara->gun_status == LGS_DRAW || lara->gun_status == LGS_UNDRAW) { gun_type = lara->gun_type; } switch (gun_type) { case LGT_UNARMED: case LGT_FLARE: Matrix_Push_I(); M_DrawBodyPart(LM_UARM_R, bone, mesh_rots_1, mesh_rots_2, clip); M_DrawBodyPart(LM_LARM_R, bone, mesh_rots_1, mesh_rots_2, clip); M_DrawBodyPart(LM_HAND_R, bone, mesh_rots_1, mesh_rots_2, clip); M_DrawEquipment(LM_HAND_R, clip, true); Matrix_Pop_I(); Matrix_Push_I(); Matrix_TranslateRel32_I(bone[LM_UARM_L - 1].pos); if (lara->flare.control) { const ANIM *const anim = Anim_GetAnim(lara->left_arm.anim_num); mesh_rots_1 = lara->left_arm .frame_base[lara->left_arm.frame_num - anim->frame_base] .mesh_rots; mesh_rots_2 = mesh_rots_1; } Matrix_Rot16_ID(mesh_rots_1[LM_UARM_L], mesh_rots_2[LM_UARM_L]); M_CacheMatrix(LM_UARM_L); M_DrawLaraMesh(item, LM_UARM_L, clip, true); M_DrawBodyPart(LM_LARM_L, bone, mesh_rots_1, mesh_rots_2, clip); M_DrawBodyPart(LM_HAND_L, bone, mesh_rots_1, mesh_rots_2, clip); M_DrawEquipment(LM_HAND_L, clip, true); if (g_TRVersion < 3 && lara->gun_type == LGT_FLARE && lara->left_arm.flash_gun) { Gun_DrawFlash(LGT_FLARE, clip, true); } Matrix_Pop(); break; case LGT_PISTOLS: case LGT_MAGNUMS: case LGT_AUTOS: case LGT_DESERT_EAGLE: case LGT_UZIS: { Matrix_Push_I(); Matrix_TranslateRel32_I(bone[LM_UARM_R - 1].pos); Matrix_InterpolateArm(); if (gun_type == LGT_DESERT_EAGLE) { Matrix_Rot16(lara->interp.result.torso_rot); } else { Matrix_Rot16(lara->right_arm.interp.result.rot); } const ANIM *anim = Anim_GetAnim(lara->right_arm.anim_num); mesh_rots_1 = lara->right_arm .frame_base[lara->right_arm.frame_num - anim->frame_base] .mesh_rots; Matrix_Rot16(mesh_rots_1[LM_UARM_R]); M_CacheMatrix(LM_UARM_R); M_DrawLaraMesh(item, LM_UARM_R, clip, false); M_DrawBodyPart(LM_LARM_R, bone, mesh_rots_1, nullptr, clip); M_DrawBodyPart(LM_HAND_R, bone, mesh_rots_1, nullptr, clip); M_DrawEquipment(LM_HAND_R, clip, false); if (lara->right_arm.flash_gun) { saved_matrix = *g_MatrixPtr; wsaved_matrix = *g_WMatrixPtr; } Matrix_Pop_I(); Matrix_Push_I(); Matrix_TranslateRel32_I(bone[LM_UARM_L - 1].pos); Matrix_InterpolateArm(); if (gun_type == LGT_DESERT_EAGLE) { Matrix_Rot16(lara->interp.result.torso_rot); } else { Matrix_Rot16(lara->left_arm.interp.result.rot); } anim = Anim_GetAnim(lara->left_arm.anim_num); mesh_rots_1 = lara->left_arm .frame_base[lara->left_arm.frame_num - anim->frame_base] .mesh_rots; Matrix_Rot16(mesh_rots_1[LM_UARM_L]); M_CacheMatrix(LM_UARM_L); M_DrawLaraMesh(item, LM_UARM_L, clip, false); M_DrawBodyPart(LM_LARM_L, bone, mesh_rots_1, nullptr, clip); M_DrawBodyPart(LM_HAND_L, bone, mesh_rots_1, nullptr, clip); M_DrawEquipment(LM_HAND_L, clip, false); if (lara->left_arm.flash_gun) { Gun_DrawFlash(gun_type, clip, false); } if (lara->right_arm.flash_gun) { *g_MatrixPtr = saved_matrix; *g_WMatrixPtr = wsaved_matrix; Gun_DrawFlash(gun_type, clip, false); } Matrix_Pop(); break; } case LGT_SHOTGUN: case LGT_M16: case LGT_MP5: case LGT_GRENADE: case LGT_ROCKET: case LGT_HARPOON: { Matrix_Push_I(); Matrix_TranslateRel32_I(bone[LM_UARM_R - 1].pos); mesh_rots_1 = lara->right_arm.frame_base[lara->right_arm.frame_num].mesh_rots; mesh_rots_2 = mesh_rots_1; Matrix_Rot16_ID(mesh_rots_1[LM_UARM_R], mesh_rots_2[LM_UARM_R]); M_CacheMatrix(LM_UARM_R); M_DrawLaraMesh(item, LM_UARM_R, clip, true); // NOTE: gcc wrongly complains about mesh_rots_1 possibly being nullptr. // While this is not the case, it's curious how the pistols subtract the // frame_base from lara->*_arm.frame_num to access the mesh_rots, and the // rifles do not. #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Warray-bounds" M_DrawBodyPart(LM_LARM_R, bone, mesh_rots_1, mesh_rots_2, clip); M_DrawBodyPart(LM_HAND_R, bone, mesh_rots_1, mesh_rots_2, clip); M_DrawEquipment(LM_HAND_R, clip, true); if (lara->right_arm.flash_gun) { saved_matrix = *g_MatrixPtr; wsaved_matrix = *g_WMatrixPtr; } Matrix_Pop_I(); Matrix_Push_I(); M_DrawBodyPart(LM_UARM_L, bone, mesh_rots_1, mesh_rots_2, clip); M_DrawBodyPart(LM_LARM_L, bone, mesh_rots_1, mesh_rots_2, clip); M_DrawBodyPart(LM_HAND_L, bone, mesh_rots_1, mesh_rots_2, clip); #pragma GCC diagnostic pop if (lara->right_arm.flash_gun) { *g_MatrixPtr = saved_matrix; *g_WMatrixPtr = wsaved_matrix; Gun_DrawFlash(gun_type, clip, false); } Matrix_Pop(); break; } default: break; } if (m_CacheMatrices) { lara->mesh_pos_matrices_valid = true; } m_CacheMatrices = false; Matrix_Pop(); Matrix_Pop(); return true; } bool Lara_Draw(const ITEM *const item) { const ITEM *const lara_item = Lara_GetItem(); m_IsLara = item == lara_item; if (m_IsLara && (item->status == IS_INVISIBLE || (item->flags & IF_ONE_SHOT) != 0)) { return false; } const int32_t top = g_PhdTop; const int32_t left = g_PhdLeft; const int32_t right = g_PhdRight; const int32_t bottom = g_PhdBottom; g_PhdLeft = Viewport_GetMinX(VIEWPORT_GAME); g_PhdRight = Viewport_GetMaxX(VIEWPORT_GAME); g_PhdTop = Viewport_GetMinY(VIEWPORT_GAME); g_PhdBottom = Viewport_GetMaxY(VIEWPORT_GAME); LARA_INFO *const lara = Lara_GetLaraInfo(); ANIM_FRAME *frames[2]; if (lara->hit_direction < 0) { int32_t rate; const int32_t frac = Item_GetFrames(lara_item, frames, &rate); if (frac != 0 && Lara_Pose_Get() == nullptr) { M_Draw_I(item, frames[0], frames[1], frac, rate); goto finish; } } const ANIM_FRAME *const hit_frame = Lara_GetHitFrame(item); const ANIM_FRAME *const frame = hit_frame == nullptr ? frames[0] : hit_frame; const OBJECT *const obj = Object_Get(item->object_id); const BOUNDS_16 *const shadow_bounds = Item_GetBoundsAccurate(item); if (!Lara_Vehicle_IsMounted()) { Output_DrawShadow(obj->shadow_size, shadow_bounds, item); } MATRIX saved_matrix = *g_MatrixPtr; MATRIX wsaved_matrix = *g_WMatrixPtr; Matrix_Push(); Matrix_TranslateAbs32(item->interp.result.pos); Matrix_Rot16(item->interp.result.rot); const MATRIX item_matrix = *g_MatrixPtr; const MATRIX item_wmatrix = *g_WMatrixPtr; const CLIP clip = Output_CheckBoundsClip(&frame->bounds); if (clip == CLIP_NOT_VISIBLE && !m_IsLara) { m_CacheMatrices = false; Matrix_Pop(); return false; } if (g_Config.debug.enable_debug_bounding_boxes) { Output_DrawCuboid(&frame->bounds); } m_CacheMatrices = m_IsLara; if (m_CacheMatrices) { lara->mesh_pos_matrices_valid = false; } Matrix_Push(); Output_CalculateObjectLightingAt(item, M_GetLaraLightSample(item)); const ANIM_BONE *const bone = m_IsLara ? Lara_Skin_GetBoneBase() : Object_GetBone(obj, 0); const LARA_POSE *const pose = Lara_Pose_Get(); const XYZ_16 *mesh_rots = pose != nullptr ? pose->rots : frame->mesh_rots; Matrix_TranslateRel16(pose != nullptr ? pose->offset : frame->offset); Matrix_Rot16(mesh_rots[LM_HIPS]); M_CacheMatrix(LM_HIPS); M_DrawLaraMesh(item, LM_HIPS, clip, false); M_DrawEquipment(LM_HIPS, clip, false); Matrix_Push(); M_DrawBodyPart(LM_THIGH_L, bone, mesh_rots, nullptr, clip); M_DrawEquipment(LM_THIGH_L, clip, false); M_DrawBodyPart(LM_CALF_L, bone, mesh_rots, nullptr, clip); M_DrawBodyPart(LM_FOOT_L, bone, mesh_rots, nullptr, clip); Matrix_Pop(); Matrix_Push(); M_DrawBodyPart(LM_THIGH_R, bone, mesh_rots, nullptr, clip); M_DrawEquipment(LM_THIGH_R, clip, false); M_DrawBodyPart(LM_CALF_R, bone, mesh_rots, nullptr, clip); M_DrawBodyPart(LM_FOOT_R, bone, mesh_rots, nullptr, clip); Matrix_Pop(); Matrix_TranslateRel32(bone[LM_TORSO - 1].pos); if (Lara_IsM16Active() && pose == nullptr) { mesh_rots = lara->right_arm.frame_base[lara->right_arm.frame_num].mesh_rots; } Matrix_Rot16(mesh_rots[LM_TORSO]); Matrix_Rot16(lara->interp.result.torso_rot); M_CacheMatrix(LM_TORSO); M_DrawLaraMesh(item, LM_TORSO, clip, false); M_DrawEquipment(LM_TORSO, clip, false); Matrix_Push(); Matrix_TranslateRel32(bone[LM_HEAD - 1].pos); Matrix_Rot16(mesh_rots[LM_HEAD]); Matrix_Rot16(lara->interp.result.head_rot); M_CacheMatrix(LM_HEAD); M_DrawLaraMesh(item, LM_HEAD, clip, false); M_DrawEquipment(LM_HEAD, clip, false); *g_MatrixPtr = saved_matrix; *g_WMatrixPtr = wsaved_matrix; if (m_IsLara) { Lara_Hair_Draw(); } Matrix_Pop(); LARA_GUN_TYPE gun_type = LGT_UNARMED; if (pose == nullptr && (lara->gun_status == LGS_READY || lara->gun_status == LGS_SPECIAL || lara->gun_status == LGS_DRAW || lara->gun_status == LGS_UNDRAW)) { gun_type = lara->gun_type; } switch (gun_type) { case LGT_UNARMED: case LGT_FLARE: Matrix_Push(); M_DrawBodyPart(LM_UARM_R, bone, mesh_rots, nullptr, clip); M_DrawBodyPart(LM_LARM_R, bone, mesh_rots, nullptr, clip); M_DrawBodyPart(LM_HAND_R, bone, mesh_rots, nullptr, clip); M_DrawEquipment(LM_HAND_R, clip, false); Matrix_Pop(); Matrix_Push(); Matrix_TranslateRel32(bone[LM_UARM_L - 1].pos); if (lara->flare.control && pose == nullptr) { const ANIM *const anim = Anim_GetAnim(lara->left_arm.anim_num); mesh_rots = lara->left_arm .frame_base[lara->left_arm.frame_num - anim->frame_base] .mesh_rots; } Matrix_Rot16(mesh_rots[LM_UARM_L]); M_DrawLaraMesh(item, LM_UARM_L, clip, false); M_DrawBodyPart(LM_LARM_L, bone, mesh_rots, nullptr, clip); M_DrawBodyPart(LM_HAND_L, bone, mesh_rots, nullptr, clip); M_DrawEquipment(LM_HAND_L, clip, false); if (g_TRVersion < 3 && lara->gun_type == LGT_FLARE && lara->left_arm.flash_gun) { Gun_DrawFlash(LGT_FLARE, clip, false); } Matrix_Pop(); break; case LGT_PISTOLS: case LGT_MAGNUMS: case LGT_AUTOS: case LGT_DESERT_EAGLE: case LGT_UZIS: { Matrix_Push(); Matrix_TranslateRel32(bone[LM_UARM_R - 1].pos); g_MatrixPtr->_00 = item_matrix._00; g_MatrixPtr->_01 = item_matrix._01; g_MatrixPtr->_02 = item_matrix._02; g_MatrixPtr->_10 = item_matrix._10; g_MatrixPtr->_11 = item_matrix._11; g_MatrixPtr->_12 = item_matrix._12; g_MatrixPtr->_20 = item_matrix._20; g_MatrixPtr->_21 = item_matrix._21; g_MatrixPtr->_22 = item_matrix._22; g_WMatrixPtr->_00 = item_wmatrix._00; g_WMatrixPtr->_01 = item_wmatrix._01; g_WMatrixPtr->_02 = item_wmatrix._02; g_WMatrixPtr->_10 = item_wmatrix._10; g_WMatrixPtr->_11 = item_wmatrix._11; g_WMatrixPtr->_12 = item_wmatrix._12; g_WMatrixPtr->_20 = item_wmatrix._20; g_WMatrixPtr->_21 = item_wmatrix._21; g_WMatrixPtr->_22 = item_wmatrix._22; if (gun_type == LGT_DESERT_EAGLE) { Matrix_Rot16(lara->interp.result.torso_rot); } else { Matrix_Rot16(lara->right_arm.interp.result.rot); } if (pose == nullptr) { const ANIM *const anim = Anim_GetAnim(lara->right_arm.anim_num); mesh_rots = lara->right_arm .frame_base[lara->right_arm.frame_num - anim->frame_base] .mesh_rots; } Matrix_Rot16(mesh_rots[LM_UARM_R]); M_DrawLaraMesh(item, LM_UARM_R, clip, false); M_DrawBodyPart(LM_LARM_R, bone, mesh_rots, nullptr, clip); M_DrawBodyPart(LM_HAND_R, bone, mesh_rots, nullptr, clip); M_DrawEquipment(LM_HAND_R, clip, false); if (lara->right_arm.flash_gun) { saved_matrix = *g_MatrixPtr; wsaved_matrix = *g_WMatrixPtr; } Matrix_Pop(); Matrix_Push(); Matrix_TranslateRel32(bone[LM_UARM_L - 1].pos); g_MatrixPtr->_00 = item_matrix._00; g_MatrixPtr->_01 = item_matrix._01; g_MatrixPtr->_02 = item_matrix._02; g_MatrixPtr->_10 = item_matrix._10; g_MatrixPtr->_11 = item_matrix._11; g_MatrixPtr->_12 = item_matrix._12; g_MatrixPtr->_20 = item_matrix._20; g_MatrixPtr->_21 = item_matrix._21; g_MatrixPtr->_22 = item_matrix._22; g_WMatrixPtr->_00 = item_wmatrix._00; g_WMatrixPtr->_01 = item_wmatrix._01; g_WMatrixPtr->_02 = item_wmatrix._02; g_WMatrixPtr->_10 = item_wmatrix._10; g_WMatrixPtr->_11 = item_wmatrix._11; g_WMatrixPtr->_12 = item_wmatrix._12; g_WMatrixPtr->_20 = item_wmatrix._20; g_WMatrixPtr->_21 = item_wmatrix._21; g_WMatrixPtr->_22 = item_wmatrix._22; if (gun_type == LGT_DESERT_EAGLE) { Matrix_Rot16(lara->interp.result.torso_rot); } else { Matrix_Rot16(lara->left_arm.interp.result.rot); } if (pose == nullptr) { const ANIM *const anim = Anim_GetAnim(lara->left_arm.anim_num); mesh_rots = lara->left_arm .frame_base[lara->left_arm.frame_num - anim->frame_base] .mesh_rots; } Matrix_Rot16(mesh_rots[LM_UARM_L]); M_CacheMatrix(LM_UARM_L); M_DrawLaraMesh(item, LM_UARM_L, clip, false); M_DrawBodyPart(LM_LARM_L, bone, mesh_rots, nullptr, clip); M_DrawBodyPart(LM_HAND_L, bone, mesh_rots, nullptr, clip); M_DrawEquipment(LM_HAND_L, clip, false); if (lara->left_arm.flash_gun) { Gun_DrawFlash(gun_type, clip, false); } if (lara->right_arm.flash_gun) { *g_MatrixPtr = saved_matrix; *g_WMatrixPtr = wsaved_matrix; Gun_DrawFlash(gun_type, clip, false); } Matrix_Pop(); break; } case LGT_SHOTGUN: case LGT_M16: case LGT_MP5: case LGT_GRENADE: case LGT_ROCKET: case LGT_HARPOON: { Matrix_Push(); Matrix_TranslateRel32(bone[LM_UARM_R - 1].pos); if (pose == nullptr) { mesh_rots = lara->right_arm.frame_base[lara->right_arm.frame_num].mesh_rots; } Matrix_Rot16(mesh_rots[LM_UARM_R]); M_CacheMatrix(LM_UARM_R); M_DrawLaraMesh(item, LM_UARM_R, clip, false); M_DrawBodyPart(LM_LARM_R, bone, mesh_rots, nullptr, clip); M_DrawBodyPart(LM_HAND_R, bone, mesh_rots, nullptr, clip); M_DrawEquipment(LM_HAND_R, clip, false); if (lara->right_arm.flash_gun) { saved_matrix = *g_MatrixPtr; wsaved_matrix = *g_WMatrixPtr; } Matrix_Pop(); Matrix_Push(); M_DrawBodyPart(LM_UARM_L, bone, mesh_rots, nullptr, clip); M_DrawBodyPart(LM_LARM_L, bone, mesh_rots, nullptr, clip); M_DrawBodyPart(LM_HAND_L, bone, mesh_rots, nullptr, clip); if (lara->right_arm.flash_gun) { *g_MatrixPtr = saved_matrix; *g_WMatrixPtr = wsaved_matrix; Gun_DrawFlash(gun_type, clip, false); } Matrix_Pop(); break; } default: break; } if (m_CacheMatrices) { lara->mesh_pos_matrices_valid = true; } m_CacheMatrices = false; Matrix_Pop(); Matrix_Pop(); finish: if (m_IsLara && lara->electric != 0) { Lara_Electricity_Draw(0, lara_item); Lara_Electricity_Draw(1, lara_item); } g_PhdLeft = left; g_PhdRight = right; g_PhdTop = top; g_PhdBottom = bottom; return true; } ================================================ FILE: src/trx/game/lara/draw.h ================================================ #pragma once #include bool Lara_Draw(const ITEM *item); ================================================ FILE: src/trx/game/lara/electric.c ================================================ #include #include #include #include #include #include #include static const uint8_t m_LaraMeshes[28] = { 0, 1, 1, 2, 2, 3, 0, 4, 4, 5, 5, 6, 0, 7, 7, 8, 8, 9, 9, 10, 7, 11, 11, 12, 12, 13, 7, 14, }; static const uint8_t m_LaraLastPoints[14] = { 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, }; static const uint8_t m_LaraLineCounts[6] = { 12, 12, 4, 12, 12, 4 }; typedef struct { XYZ_16 pos; XYZ_16 vel; } M_ELECTRIC_POINT; static M_ELECTRIC_POINT m_ElectricityPoints[32] = {}; XYZ_16 Lara_Electricity_GetPoint(const int32_t idx) { return m_ElectricityPoints[idx & 31].pos; } void Lara_Electricity_UpdatePoints(void) { for (int32_t i = 0; i < 32; i++) { const int32_t rnd = Random_GetDraw(); int16_t x = m_ElectricityPoints[i].pos.x; int16_t y = m_ElectricityPoints[i].pos.y; int16_t z = m_ElectricityPoints[i].pos.z; int16_t xv = m_ElectricityPoints[i].vel.x; int16_t yv = m_ElectricityPoints[i].vel.y; int16_t zv = m_ElectricityPoints[i].vel.z; if (((x > 256 || x < -256) && (y > 256 || y < -256) && (z > 256 || z < -256)) || x > 384 || x < -128 || y > 384 || y < -128 || z > 384 || z < -128) { x = 0; y = 0; z = 0; xv = 0; yv = 0; zv = 0; } if (xv != 0) { if (xv >= 0) { xv += 2; } else { xv -= 2; } } else if ((rnd & 1) != 0) { xv = -1 - (Random_GetDraw() & 3); } else { xv = (Random_GetDraw() & 3) + 1; } if (yv != 0) { if (yv >= 0) { yv += 2; } else { yv -= 2; } } else if ((rnd & 2) != 0) { yv = -1 - (Random_GetDraw() & 3); } else { yv = (Random_GetDraw() & 3) + 1; } if (zv != 0) { if (zv >= 0) { zv++; } else { zv--; } } else if ((rnd & 4) != 0) { zv = -1 - (Random_GetDraw() & 3); } else { zv = (Random_GetDraw() & 3) + 1; } x += xv; y += yv; z += zv; m_ElectricityPoints[i].pos.x = x; m_ElectricityPoints[i].pos.y = y; m_ElectricityPoints[i].pos.z = z; m_ElectricityPoints[i].vel.x = xv; m_ElectricityPoints[i].vel.y = yv; m_ElectricityPoints[i].vel.z = zv; } } void Lara_Electricity_EmitLight(void) { const LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->electric == 0) { return; } const ITEM *const lara_item = Lara_GetItem(); const int32_t electric = lara->electric; int32_t r = 0; int32_t g = 0; int32_t b = 0; int32_t falloff = 0; if (electric < 12) { r = (Random_GetControl() & 7) - electric + 16; g = 32 - electric; b = 255; falloff = (Random_GetControl() & 1) - 2 * electric + 25; r <<= 3; g <<= 3; } else { r = 0; g = (Random_GetControl() & 0x3F) + 64; b = (Random_GetControl() & 0x3F) + 128; falloff = (Random_GetControl() & 3) + 8; } CLAMP(r, 0, 255); CLAMP(g, 0, 255); CLAMP(b, 0, 255); CLAMP(falloff, 0, 255); Output_AddDynamicLightRGB(lara_item->pos, falloff, (RGB_888) { r, g, b }); } void Lara_Electricity_Draw(const int32_t lr, const ITEM *const item) { XYZ_32 pos[96]; int16_t dists[96]; int32_t num = 0; for (int32_t i = 0; i < 14; i++) { const int32_t mesh1 = m_LaraMeshes[2 * i]; const int32_t mesh2 = m_LaraMeshes[2 * i + 1]; const M_ELECTRIC_POINT *const points = &m_ElectricityPoints[(5 * i) & 0xF]; int32_t point_idx = 0; XYZ_32 pos1 = { .x = 0, .y = 0, .z = 0 }; XYZ_32 pos2 = { .x = 0, .y = 0, .z = 0 }; if (lr != 0) { pos1.x = -48; pos1.z = -48; } else { pos1.x = 48; pos1.z = 48; } Collide_GetJointAbsPosition(item, &pos1, mesh1); if (m_LaraLastPoints[i] == 0 || i == 13) { if (lr != 0) { pos2.x = -48; pos2.z = -48; } else { pos2.x = 48; pos2.z = 48; } if (i == 13) { pos2.y = -64; } } Collide_GetJointAbsPosition(item, &pos2, mesh2); int32_t x = pos1.x; int32_t y = pos1.y; int32_t z = pos1.z; const int32_t x_step = (pos2.x - pos1.x) >> 2; const int32_t y_step = (pos2.y - pos1.y) >> 2; const int32_t z_step = (pos2.z - pos1.z) >> 2; for (int32_t j = 0; j < 5; j++) { if (j == 4 && m_LaraLastPoints[i] == 0) { break; } int32_t mx = x; int32_t my = y; int32_t mz = z; if (j == 4 && m_LaraLastPoints[i] != 0) { mx = pos2.x; my = pos2.y; mz = pos2.z; } if (j == 0 || j == 4) { dists[num] = 0; } else { const XYZ_16 point = points[point_idx].pos; point_idx++; if (lr != 0) { mx -= point.x >> 3; my -= point.y >> 3; mz -= point.z >> 3; } else { mx += point.x >> 3; my += point.y >> 3; mz += point.z >> 3; } int32_t p_x = ABS(point.x); int32_t p_y = ABS(point.y); int32_t p_z = ABS(point.z); if (p_y > p_x) { p_x = p_y; } if (p_z > p_x) { p_x = p_z; } dists[num] = (int16_t)p_x; } pos[num].x = mx; pos[num].y = my; pos[num].z = mz; num++; x += x_step; y += y_step; z += z_step; } } int32_t idx = 0; for (int32_t i = 0; i < 6; i++) { for (int32_t j = 0; j < m_LaraLineCounts[i]; j++) { const XYZ_32 from = pos[idx]; const XYZ_32 to = pos[idx + 1]; int32_t c0 = dists[idx]; int32_t c1 = dists[idx + 1]; idx++; if (c0 > 255) { c0 = 511 - c0; if (c0 < 0) { c0 = 0; } } if (c1 > 255) { c1 = 511 - c1; if (c1 < 0) { c1 = 0; } } if (lr != 0) { c0 >>= 1; c1 >>= 1; } const RGBA_8888 from_color = { 0, (uint8_t)c0, (uint8_t)c0, 0xC0 }; const RGBA_8888 to_color = { 0, (uint8_t)c1, (uint8_t)c1, 0xC0 }; OutputSource_PolyFX_StageLineSegment( from, from_color, to, to_color, 1.0f, DRAW_BLEND_ADD); } idx++; } } ================================================ FILE: src/trx/game/lara/electric.h ================================================ #pragma once #include void Lara_Electricity_UpdatePoints(void); void Lara_Electricity_EmitLight(void); void Lara_Electricity_Draw(int32_t lr, const ITEM *item); XYZ_16 Lara_Electricity_GetPoint(int32_t idx); ================================================ FILE: src/trx/game/lara/enum.h ================================================ #pragma once typedef enum { LS_INVALID = -1, } LARA_STATE; typedef enum { LS_TRX_INVALID = -1, #define X_CATALOG_ID(enum_value) enum_value, #include #undef X_CATALOG_ID LS_NUMBER_OF, } LARA_TRX_STATE; typedef enum { LA_INVALID = -1, } LARA_ANIMATION; typedef enum { LA_TRX_INVALID = -1, #define X_CATALOG_ID(enum_value) enum_value, #include #undef X_CATALOG_ID LA_NUMBER_OF, } LARA_TRX_ANIMATION; // clang-format off typedef enum { LWS_ABOVE_WATER = 0, LWS_UNDERWATER = 1, LWS_SURFACE = 2, LWS_CHEAT = 3, LWS_WADE = 4, } LARA_WATER_STATE; typedef enum { LGS_ARMLESS = 0, LGS_HANDS_BUSY = 1, LGS_DRAW = 2, LGS_UNDRAW = 3, LGS_READY = 4, LGS_SPECIAL = 5, } LARA_GUN_STATE; typedef enum { LM_HIPS = 0, LM_THIGH_L = 1, LM_CALF_L = 2, LM_FOOT_L = 3, LM_THIGH_R = 4, LM_CALF_R = 5, LM_FOOT_R = 6, LM_TORSO = 7, LM_UARM_R = 8, LM_LARM_R = 9, LM_HAND_R = 10, LM_UARM_L = 11, LM_LARM_L = 12, LM_HAND_L = 13, LM_HEAD = 14, LM_FIRST = LM_HIPS, LM_NUMBER_OF = 15, } LARA_MESH; // clang-format on // clang-format off typedef enum { LS_EXTRA_BREATH = 0, LS_EXTRA_TREX_KILL = 1, LS_EXTRA_SCION_PICKUP_1 = 2, LS_EXTRA_USE_MIDAS = 3, LS_EXTRA_MIDAS_KILL = 4, LS_EXTRA_SCION_PICKUP_2 = 5, LS_EXTRA_TORSO_KILL = 6, LS_EXTRA_PLUNGER = 7, LS_EXTRA_START_ANIM = 8, LS_EXTRA_AIRLOCK = 9, LS_EXTRA_SHARK_KILL = 10, LS_EXTRA_YETI_KILL = 11, LS_EXTRA_GONG_BONG = 12, LS_EXTRA_GUARD_KILL = 13, LS_EXTRA_PULL_DAGGER = 14, LS_EXTRA_START_HOUSE = 15, LS_EXTRA_END_HOUSE = 16, LS_EXTRA_SHIVA_KILL = 17, LS_EXTRA_RAPIDS_DROWN = 18, LS_EXTRA_TRAIN_KILL = 19, LS_EXTRA_JAIL_WAKE_UP = 20, LS_EXTRA_WILLARD_KILL = 21, LS_EXTRA_NUMBER_OF, } LARA_EXTRA_STATE; // clang-format on // clang-format off typedef enum { LGT_UNKNOWN = -1, // for legacy saves LGT_UNARMED = 0, LGT_PISTOLS = 1, LGT_MAGNUMS = 2, LGT_UZIS = 3, LGT_SHOTGUN = 4, LGT_M16 = 5, LGT_GRENADE = 6, LGT_HARPOON = 7, LGT_FLARE = 8, LGT_SKIDOO = 9, LGT_AUTOS = 10, LGT_DESERT_EAGLE = 11, LGT_MP5 = 12, LGT_ROCKET = 13, NUM_WEAPONS, } LARA_GUN_TYPE; // clang-format on // clang-format off typedef enum { LF_G_AIM_START = 0, LF_G_AIM_BEND = 1, LF_G_AIM_EXTEND = 3, LF_G_AIM_END = 4, LF_G_UNDRAW_START = 5, LF_G_UNDRAW_BEND = 6, LF_G_UNDRAW_END = 12, LF_G_DRAW_START = 13, LF_G_DRAW_END = 23, LF_G_RECOIL_START = 24, LF_G_RECOIL_END = 32, } LARA_GUN_ANIMATION_FRAME; // clang-format on ================================================ FILE: src/trx/game/lara/flare.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include typedef enum { // clang-format off LA_FLARES_HOLD = 0, LA_FLARES_THROW = 1, LA_FLARES_DRAW = 2, LA_FLARES_IGNITE = 3, LA_FLARES_IDLE = 4, // clang-format on } M_LARA_FLARE_ANIMATION; typedef enum { // clang-format off LF_FL_HOLD_FT = 1, LF_FL_THROW_FT = 32, LF_FL_DRAW_FT = 39, LF_FL_IGNITE_FT = 23, LF_FL_2_HOLD_FT = 15, LF_FL_HOLD = 0, LF_FL_THROW = (LF_FL_HOLD + LF_FL_HOLD_FT), // = 1 LF_FL_THROW_RELEASE = (LF_FL_THROW + 20), // = 21 LF_FL_DRAW = (LF_FL_THROW + LF_FL_THROW_FT), // = 33 LF_FL_IGNITE = (LF_FL_DRAW + LF_FL_DRAW_FT), // = 72 LF_FL_2_HOLD = (LF_FL_IGNITE + LF_FL_IGNITE_FT), // = 95 LF_FL_END = (LF_FL_2_HOLD + LF_FL_2_HOLD_FT), // = 110 LF_FL_DRAW_GOT_IT = (LF_FL_DRAW + 13), // = 46 // clang-format on } M_LARA_FLARE_FRAME; static const LARA_TRX_STATE m_HoldStates[] = { // clang-format off LS_WALK, LS_STOP, LS_POSE, LS_TURN_RIGHT, LS_TURN_LEFT, LS_WALK_BACK, LS_FAST_TURN, LS_STEP_LEFT, LS_STEP_RIGHT, LS_WADE, LS_PICKUP, LS_SWITCH_ON, LS_SWITCH_OFF, LS_TRX_INVALID, // sentinel // clang-format on }; static const LARA_TRX_STATE m_ThrowStates[] = { // clang-format off LS_FAST_FALL, LS_SWAN_DIVE, LS_FAST_DIVE, LS_TRX_INVALID, // sentinel // clang-format on }; static XYZ_32 m_IgnitePos = {}; static void M_InitialiseState(void) { LARA_INFO *const lara_info = Lara_GetLaraInfo(); lara_info->gun_status = LGS_ARMLESS; lara_info->left_arm.rot.x = 0; lara_info->left_arm.rot.y = 0; lara_info->left_arm.rot.z = 0; lara_info->right_arm.rot.x = 0; lara_info->right_arm.rot.y = 0; lara_info->right_arm.rot.z = 0; lara_info->left_arm.lock = 0; lara_info->right_arm.lock = 0; lara_info->target = nullptr; } static void M_DoIgniteEffects(const XYZ_32 flare_pos, int16_t room_num) { m_IgnitePos = flare_pos; Room_GetSector(m_IgnitePos, &room_num); const ROOM *const room = Room_Get(room_num); const SOUND_PLAY_MODE mode = room->flags.underwater ? SPM_UNDERWATER : SPM_NORMAL; Sound_Effect(SFX_LARA_FLARE_IGNITE, &m_IgnitePos, mode); } static bool M_CanThrowFlare(void) { const ITEM *const lara_item = Lara_GetItem(); const LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (lara_info->gun_status != LGS_ARMLESS) { return false; } if (!g_Config.gameplay.fix_flare_throw_priority) { return true; } if (lara_info->water_status != LWS_ABOVE_WATER && lara_info->water_status != LWS_WADE) { return true; } // Airborne states that would not allow ledge grabbing anyway. if (Lara_HasState(m_ThrowStates)) { return true; } // Neither airborne nor about to be. return !lara_item->gravity && !g_Input.jump; } static void M_ControlInHand(void) { LARA_INFO *const lara_info = Lara_GetLaraInfo(); const int32_t flare_age = g_Config.debug.enable_endless_flare_time ? MIN(Flare_GetMaxAge() / 2, lara_info->flare.age) : lara_info->flare.age; XYZ_32 vec = { .x = 11, .y = 32, .z = 41, }; Lara_GetJointAbsPosition(&vec, LM_HAND_L); const ITEM *const lara_item = Lara_GetItem(); if (flare_age == 0) { M_DoIgniteEffects(vec, lara_item->room_num); } lara_info->left_arm.flash_gun = Flare_GenerateLight(vec, flare_age); if (flare_age >= Flare_GetMaxAge()) { if (M_CanThrowFlare()) { lara_info->gun_status = LGS_UNDRAW; } return; } lara_info->flare.age = flare_age + 1; Flare_GenerateEffects(&lara_item->pos, vec, lara_item->room_num); if (!lara_info->left_arm.flash_gun) { return; } if (g_TRVersion < 3) { return; } XYZ_32 vec_2 = { .x = 8, .y = 36, .z = WALL_L + (Random_GetDraw() & 0xFF), }; Lara_GetJointAbsPosition(&vec_2, LM_HAND_L); const XYZ_32 vel = { .x = vec_2.x - vec.x, .y = vec_2.y - vec.y, .z = vec_2.z - vec.z, }; for (int32_t i = 0; i < (Random_GetDraw() & 3) + 4; i++) { const bool smoke = (i >> 2) != 0; Sparks_TriggerFlareSparks(vec, vel, smoke); } } static void M_SetArm(const int32_t flare_frame) { int16_t anim_idx; if (flare_frame < LF_FL_THROW) { anim_idx = LA_FLARES_HOLD; } else if (flare_frame < LF_FL_DRAW) { anim_idx = LA_FLARES_THROW; } else if (flare_frame < LF_FL_IGNITE) { anim_idx = LA_FLARES_DRAW; } else if (flare_frame < LF_FL_2_HOLD) { anim_idx = LA_FLARES_IGNITE; } else { anim_idx = LA_FLARES_IDLE; } const OBJECT *const obj = Object_Get(O_LARA_FLARE); const ANIM *const anim = Object_GetAnim(obj, anim_idx); LARA_INFO *const lara_info = Lara_GetLaraInfo(); lara_info->left_arm.anim_num = obj->anim_idx + anim_idx; lara_info->left_arm.frame_base = anim->frame_ptr; } static bool M_CanUseFlareControl(void) { const ITEM *const lara_item = Lara_GetItem(); if (lara_item->current_anim_state == LS(LS_PICKUP)) { const LARA_TRX_ANIMATION anim = LA_U(Item_GetRelativeAnim(lara_item)); return anim != LA_CROUCH_PICKUP && anim != LA_CRAWL_PICKUP; } return Lara_Vehicle_IsMounted() || Lara_HasState(m_HoldStates); } static void M_ControlArmless(void) { LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (M_CanUseFlareControl()) { if (!lara_info->flare.control) { lara_info->left_arm.frame_num = LF_FL_2_HOLD; lara_info->flare.control = true; } else if (lara_info->left_arm.frame_num != LF_FL_HOLD) { lara_info->left_arm.frame_num++; if (lara_info->left_arm.frame_num == LF_FL_END) { lara_info->left_arm.frame_num = LF_FL_HOLD; } } } else { lara_info->flare.control = false; } M_ControlInHand(); M_SetArm(lara_info->left_arm.frame_num); } static void M_ControlBusyHands(void) { const ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara_info = Lara_GetLaraInfo(); lara_info->flare.control = M_CanUseFlareControl(); M_ControlInHand(); M_SetArm(lara_info->left_arm.frame_num); } static void M_UndrawMeshes(void) { Lara_Skin_ClearEquipment(LM_HAND_L); } void Lara_Flare_Control(void) { LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (lara_info->gun_status == LGS_ARMLESS) { M_ControlArmless(); } else if (lara_info->gun_status == LGS_HANDS_BUSY) { M_ControlBusyHands(); } } void Lara_Flare_Draw(void) { const ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (lara_item->current_anim_state == LS(LS_FLARE_PICKUP) || lara_item->current_anim_state == LS(LS_PICKUP)) { M_ControlInHand(); lara_info->flare.control = false; lara_info->left_arm.frame_num = LF_FL_2_HOLD - 2; M_SetArm(lara_info->left_arm.frame_num); return; } int32_t frame_num = lara_info->left_arm.frame_num + 1; lara_info->flare.control = true; if (frame_num < LF_FL_DRAW || frame_num > LF_FL_2_HOLD - 1) { frame_num = LF_FL_DRAW; } else if (frame_num == LF_FL_DRAW_GOT_IT) { Lara_Flare_DrawMeshes(); if (!Game_IsBonusFlagSet(GBF_NGPLUS)) { Inv_RemoveItem(O_FLAREBOX_ITEM); } } else if (frame_num >= LF_FL_IGNITE && frame_num <= LF_FL_2_HOLD - 2) { if (frame_num == LF_FL_IGNITE) { lara_info->flare.age = 0; } M_ControlInHand(); } else if (frame_num == LF_FL_2_HOLD - 1) { M_InitialiseState(); M_ControlInHand(); frame_num = LF_FL_HOLD; } lara_info->left_arm.frame_num = frame_num; M_SetArm(frame_num); } void Lara_Flare_Undraw(void) { ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara_info = Lara_GetLaraInfo(); int16_t frame_num_1 = lara_info->left_arm.frame_num; int16_t frame_num_2 = lara_info->flare.frame_num; const bool is_mounted = Lara_Vehicle_IsMounted(); lara_info->flare.control = true; if (lara_item->goal_anim_state == LS(LS_STOP) && !is_mounted) { if (Item_TestAnimEqual(lara_item, LA(LA_STAND_IDLE))) { int16_t throw_frame = frame_num_1; if (throw_frame < LF_FL_THROW || throw_frame >= LF_FL_DRAW) { throw_frame = LF_FL_THROW; } Item_SwitchToAnim(lara_item, LA(LA_FLARE_THROW), throw_frame); lara_info->flare.frame_num = lara_item->frame_num; frame_num_2 = lara_item->frame_num; frame_num_1 = throw_frame; } if (Item_TestAnimEqual(lara_item, LA(LA_FLARE_THROW))) { lara_info->flare.control = false; const OBJECT *const obj = Object_Get(O_LARA); const ANIM *const anim = Object_GetAnim(obj, LA(LA_FLARE_THROW)); if (frame_num_2 >= anim->frame_base + LF_FL_THROW_FT - 1) { lara_info->gun_type = lara_info->last_gun_type; lara_info->request_gun_type = lara_info->last_gun_type; lara_info->gun_status = LGS_ARMLESS; Gun_InitialiseNewWeapon(); lara_info->target = nullptr; lara_info->right_arm.lock = 0; lara_info->left_arm.lock = 0; Item_SwitchToAnim(lara_item, LA(LA_STAND_STILL), 0); lara_info->flare.frame_num = lara_item->frame_num; lara_item->current_anim_state = LS(LS_STOP); lara_item->goal_anim_state = LS(LS_STOP); return; } lara_info->flare.frame_num = frame_num_2 + 1; } } else if (lara_item->current_anim_state == LS(LS_STOP) && !is_mounted) { Item_SwitchToAnim(lara_item, LA(LA_STAND_STILL), 0); } if (frame_num_1 == LF_FL_HOLD) { frame_num_1 = LF_FL_THROW; } else if (frame_num_1 >= LF_FL_IGNITE && frame_num_1 < LF_FL_2_HOLD) { frame_num_1++; if (frame_num_1 == LF_FL_2_HOLD - 1) { frame_num_1 = LF_FL_THROW; } } else if (frame_num_1 >= LF_FL_THROW && frame_num_1 < LF_FL_DRAW) { frame_num_1++; if (frame_num_1 == LF_FL_THROW_RELEASE) { Lara_Flare_Dispose(true); } else if (frame_num_1 == LF_FL_DRAW) { frame_num_1 = 0; lara_info->gun_type = lara_info->last_gun_type; lara_info->request_gun_type = lara_info->last_gun_type; lara_info->gun_status = LGS_ARMLESS; Gun_InitialiseNewWeapon(); lara_info->target = nullptr; lara_info->flare.control = false; lara_info->right_arm.lock = 0; lara_info->left_arm.lock = 0; lara_info->flare.frame_num = 0; } } else if (frame_num_1 >= LF_FL_2_HOLD && frame_num_1 < LF_FL_END) { frame_num_1++; if (frame_num_1 == LF_FL_END) { frame_num_1 = LF_FL_THROW; } } if (frame_num_1 >= LF_FL_THROW && frame_num_1 < LF_FL_THROW_RELEASE) { M_ControlInHand(); } lara_info->left_arm.frame_num = frame_num_1; M_SetArm(frame_num_1); } void Lara_Flare_Dispose(const bool thrown) { const ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara_info = Lara_GetLaraInfo(); const int16_t item_num = Item_Create(); if (item_num == NO_ITEM) { goto finish; } ITEM *const item = Item_Get(item_num); item->object_id = O_FLARE_ITEM; item->room_num = lara_item->room_num; XYZ_32 vec = { .x = -16, .y = 32, .z = 42, }; Lara_GetJointAbsPosition(&vec, LM_HAND_L); const SECTOR *const sector = Room_GetSector(vec, &item->room_num); const int32_t height = Room_GetHeight(sector, vec); if (height < vec.y) { item->pos.x = lara_item->pos.x; item->pos.y = vec.y; item->pos.z = lara_item->pos.z; item->rot.y = -lara_item->rot.y; item->room_num = lara_item->room_num; } else { item->pos.x = vec.x; item->pos.y = vec.y; item->pos.z = vec.z; if (thrown) { item->rot.y = lara_item->rot.y; } else { item->rot.y = lara_item->rot.y - DEG_45; } } Item_Initialise(item_num); item->rot.z = 0; item->rot.x = 0; item->shade.value_1 = -1; if (thrown) { item->speed = lara_item->speed + 50; item->fall_speed = lara_item->fall_speed - 50; } else { item->speed = lara_item->speed + 10; item->fall_speed = lara_item->fall_speed + 50; } if (Flare_GenerateLight(item->pos, lara_info->flare.age)) { FlareItem_SetAge(item, lara_info->flare.age, true); } else { FlareItem_SetAge(item, lara_info->flare.age, false); } Item_AddActive(item_num); item->status = IS_ACTIVE; finish: M_UndrawMeshes(); if (!thrown) { lara_info->flare.control = false; } } bool Lara_Flare_IsMeshActive(void) { const LARA_SKIN_EQUIPMENT *const equipment = Lara_Skin_GetEquipment(LM_HAND_L); return equipment->type == EQUIPMENT_TYPE_WEAPON && equipment->data == LGT_FLARE; } void Lara_Flare_DrawMeshes(void) { Lara_Skin_SetGunEquipment(LM_HAND_L, LGT_FLARE); } ================================================ FILE: src/trx/game/lara/flare.h ================================================ #pragma once bool Lara_Flare_IsMeshActive(void); void Lara_Flare_DrawMeshes(void); void Lara_Flare_Control(void); void Lara_Flare_Draw(void); void Lara_Flare_Undraw(void); void Lara_Flare_Dispose(bool thrown); ================================================ FILE: src/trx/game/lara/hair.c ================================================ #include #include #include #include #include #include #include #include #include #include #define M_HAIR_SEGMENTS 6 #define M_HAIR_SPHERES 5 #define M_BONE_IDX(segment) \ (segment == M_HAIR_SEGMENTS ? (segment - 2) : (segment - 1)) static bool m_IsFirstHair; static SPHERE m_HairSpheres[M_HAIR_SPHERES]; static XYZ_32 m_HairVelocity[M_HAIR_SEGMENTS + 1]; static HAIR_SEGMENT m_HairSegments[M_HAIR_SEGMENTS + 1]; static void M_CalculateSpheres(const ANIM_FRAME *const frame) { const LARA_INFO *const lara = Lara_GetLaraInfo(); const OBJECT *const lara_obj = Object_Get(O_LARA); const LARA_POSE *const pose = Lara_Pose_Get(); const XYZ_16 *mesh_rots = pose != nullptr ? pose->rots : frame->mesh_rots; Matrix_TranslateRel16(pose != nullptr ? pose->offset : frame->offset); Matrix_Rot16(mesh_rots[LM_HIPS]); Matrix_Push(); const OBJECT_MESH *mesh = Object_GetMesh(lara_obj->mesh_idx + LM_HIPS); Matrix_TranslateRel16(mesh->center); m_HairSpheres[0].pos.x = g_MatrixPtr->_03 >> W2V_SHIFT; m_HairSpheres[0].pos.y = g_MatrixPtr->_13 >> W2V_SHIFT; m_HairSpheres[0].pos.z = g_MatrixPtr->_23 >> W2V_SHIFT; m_HairSpheres[0].r = mesh->radius; Matrix_Pop(); const ANIM_BONE *bone = Object_GetBone(lara_obj, 0); Matrix_TranslateRel32(bone[LM_TORSO - 1].pos); if (Lara_IsM16Active() && pose == nullptr) { mesh_rots = lara->right_arm.frame_base[lara->right_arm.frame_num].mesh_rots; } Matrix_Rot16(mesh_rots[LM_TORSO]); Matrix_Rot16(lara->interp.result.torso_rot); Matrix_Push(); mesh = Object_GetMesh(lara_obj->mesh_idx + LM_TORSO); Matrix_TranslateRel16(mesh->center); m_HairSpheres[1].pos.x = g_MatrixPtr->_03 >> W2V_SHIFT; m_HairSpheres[1].pos.y = g_MatrixPtr->_13 >> W2V_SHIFT; m_HairSpheres[1].pos.z = g_MatrixPtr->_23 >> W2V_SHIFT; m_HairSpheres[1].r = mesh->radius; Matrix_Pop(); Matrix_Push(); Matrix_TranslateRel32(bone[LM_UARM_R - 1].pos); Matrix_Rot16(mesh_rots[LM_UARM_R]); mesh = Object_GetMesh(lara_obj->mesh_idx + LM_UARM_R); Matrix_TranslateRel16(mesh->center); m_HairSpheres[3].pos.x = g_MatrixPtr->_03 >> W2V_SHIFT; m_HairSpheres[3].pos.y = g_MatrixPtr->_13 >> W2V_SHIFT; m_HairSpheres[3].pos.z = g_MatrixPtr->_23 >> W2V_SHIFT; m_HairSpheres[3].r = mesh->radius * 3 / 2; Matrix_Pop(); Matrix_Push(); Matrix_TranslateRel32(bone[LM_UARM_L - 1].pos); Matrix_Rot16(mesh_rots[LM_UARM_L]); mesh = Object_GetMesh(lara_obj->mesh_idx + LM_UARM_L); Matrix_TranslateRel16(mesh->center); m_HairSpheres[4].pos.x = g_MatrixPtr->_03 >> W2V_SHIFT; m_HairSpheres[4].pos.y = g_MatrixPtr->_13 >> W2V_SHIFT; m_HairSpheres[4].pos.z = g_MatrixPtr->_23 >> W2V_SHIFT; m_HairSpheres[4].r = mesh->radius * 3 / 2; Matrix_Pop(); Matrix_TranslateRel32(bone[LM_HEAD - 1].pos); Matrix_Rot16(mesh_rots[LM_HEAD]); Matrix_Rot16(lara->interp.result.head_rot); Matrix_Push(); mesh = Object_GetMesh(lara_obj->mesh_idx + LM_HEAD); Matrix_TranslateRel16(mesh->center); m_HairSpheres[2].pos.x = g_MatrixPtr->_03 >> W2V_SHIFT; m_HairSpheres[2].pos.y = g_MatrixPtr->_13 >> W2V_SHIFT; m_HairSpheres[2].pos.z = g_MatrixPtr->_23 >> W2V_SHIFT; m_HairSpheres[2].r = mesh->radius; Matrix_Pop(); Matrix_TranslateRel32(Lara_Skin_GetBraidOffset()); } static void M_CalculateSpheres_I( const ANIM_FRAME *const frame_1, const ANIM_FRAME *const frame_2, const int32_t frac, const int32_t rate) { const LARA_INFO *const lara = Lara_GetLaraInfo(); const OBJECT *const lara_obj = Object_Get(O_LARA); const XYZ_16 *mesh_rots_1 = frame_1->mesh_rots; const XYZ_16 *mesh_rots_2 = frame_2->mesh_rots; Matrix_InitInterpolate(frac, rate); Matrix_TranslateRel16_ID(frame_1->offset, frame_2->offset); Matrix_Rot16_ID(mesh_rots_1[LM_HIPS], mesh_rots_2[LM_HIPS]); Matrix_Push_I(); const OBJECT_MESH *mesh = Object_GetMesh(lara_obj->mesh_idx + LM_HIPS); Matrix_TranslateRel16_I(mesh->center); Matrix_Interpolate(); m_HairSpheres[0].pos.x = g_MatrixPtr->_03 >> W2V_SHIFT; m_HairSpheres[0].pos.y = g_MatrixPtr->_13 >> W2V_SHIFT; m_HairSpheres[0].pos.z = g_MatrixPtr->_23 >> W2V_SHIFT; m_HairSpheres[0].r = mesh->radius; Matrix_Pop_I(); const ANIM_BONE *bone = Object_GetBone(lara_obj, 0); Matrix_TranslateRel32_I(bone[LM_TORSO - 1].pos); if (Lara_IsM16Active()) { mesh_rots_1 = lara->right_arm.frame_base[lara->right_arm.frame_num].mesh_rots; mesh_rots_2 = mesh_rots_1; } Matrix_Rot16_ID(mesh_rots_1[LM_TORSO], mesh_rots_2[LM_TORSO]); Matrix_Rot16_I(lara->interp.result.torso_rot); Matrix_Push_I(); mesh = Object_GetMesh(lara_obj->mesh_idx + LM_TORSO); Matrix_TranslateRel16_I(mesh->center); Matrix_Interpolate(); m_HairSpheres[1].pos.x = g_MatrixPtr->_03 >> W2V_SHIFT; m_HairSpheres[1].pos.y = g_MatrixPtr->_13 >> W2V_SHIFT; m_HairSpheres[1].pos.z = g_MatrixPtr->_23 >> W2V_SHIFT; m_HairSpheres[1].r = mesh->radius; Matrix_Pop_I(); Matrix_Push_I(); Matrix_TranslateRel32_I(bone[LM_UARM_R - 1].pos); Matrix_Rot16_ID(mesh_rots_1[LM_UARM_R], mesh_rots_2[LM_UARM_R]); mesh = Object_GetMesh(lara_obj->mesh_idx + LM_UARM_R); Matrix_TranslateRel16_I(mesh->center); Matrix_Interpolate(); m_HairSpheres[3].pos.x = g_MatrixPtr->_03 >> W2V_SHIFT; m_HairSpheres[3].pos.y = g_MatrixPtr->_13 >> W2V_SHIFT; m_HairSpheres[3].pos.z = g_MatrixPtr->_23 >> W2V_SHIFT; m_HairSpheres[3].r = mesh->radius * 3 / 2; Matrix_Pop_I(); Matrix_Push_I(); Matrix_TranslateRel32_I(bone[LM_UARM_L - 1].pos); Matrix_Rot16_ID(mesh_rots_1[LM_UARM_L], mesh_rots_2[LM_UARM_L]); mesh = Object_GetMesh(lara_obj->mesh_idx + LM_UARM_L); Matrix_TranslateRel16_I(mesh->center); Matrix_Interpolate(); m_HairSpheres[4].pos.x = g_MatrixPtr->_03 >> W2V_SHIFT; m_HairSpheres[4].pos.y = g_MatrixPtr->_13 >> W2V_SHIFT; m_HairSpheres[4].pos.z = g_MatrixPtr->_23 >> W2V_SHIFT; m_HairSpheres[4].r = mesh->radius * 3 / 2; Matrix_Pop_I(); Matrix_TranslateRel32_I(bone[LM_HEAD - 1].pos); Matrix_Rot16_ID(mesh_rots_1[LM_HEAD], mesh_rots_2[LM_HEAD]); Matrix_Rot16_I(lara->interp.result.head_rot); Matrix_Push_I(); mesh = Object_GetMesh(lara_obj->mesh_idx + LM_HEAD); Matrix_TranslateRel16_I(mesh->center); Matrix_Interpolate(); m_HairSpheres[2].pos.x = g_MatrixPtr->_03 >> W2V_SHIFT; m_HairSpheres[2].pos.y = g_MatrixPtr->_13 >> W2V_SHIFT; m_HairSpheres[2].pos.z = g_MatrixPtr->_23 >> W2V_SHIFT; m_HairSpheres[2].r = mesh->radius; Matrix_Pop_I(); Matrix_TranslateRel32_I(Lara_Skin_GetBraidOffset()); Matrix_Interpolate(); } void Lara_Hair_Initialise(void) { const ANIM_BONE *const bones = Lara_Skin_GetBraidBoneBase(); if (bones == nullptr) { return; } m_IsFirstHair = true; m_HairSegments[0].rot.x = -DEG_90; m_HairSegments[0].rot.y = 0; for (int32_t i = 1; i <= M_HAIR_SEGMENTS; i++) { const ANIM_BONE *const bone = &bones[M_BONE_IDX(i)]; m_HairSegments[i].pos = bone->pos; m_HairSegments[i].rot.x = -DEG_90; m_HairSegments[i].rot.y = 0; m_HairSegments[i].rot.z = 0; m_HairVelocity[i - 1] = (XYZ_32) {}; } } void Lara_Hair_Control(const bool in_cutscene) { if (!Lara_Hair_IsActive()) { return; } const ITEM *const lara_item = Lara_GetItem(); const LARA_INFO *const lara_info = Lara_GetLaraInfo(); const ANIM_FRAME *frame_1; const ANIM_FRAME *frame_2; int32_t frac; int32_t rate; const ANIM_FRAME *const hit_frame = Lara_GetHitFrame(lara_item); if (!in_cutscene && hit_frame != nullptr) { frame_1 = hit_frame; frac = 0; } else { ANIM_FRAME *frmptr[2]; frac = Item_GetFrames(lara_item, frmptr, &rate); frame_1 = frmptr[0]; frame_2 = frmptr[1]; } Matrix_PushUnit(); Matrix_TranslateSet32(lara_item->pos); Matrix_Rot16(lara_item->rot); if (frac == 0 || Lara_Pose_Get() != nullptr) { M_CalculateSpheres(frame_1); } else { M_CalculateSpheres_I(frame_1, frame_2, frac, rate); } const XYZ_32 pos = { .x = g_MatrixPtr->_03 >> W2V_SHIFT, .y = g_MatrixPtr->_13 >> W2V_SHIFT, .z = g_MatrixPtr->_23 >> W2V_SHIFT, }; Matrix_Pop(); const ANIM_BONE *const bones = Lara_Skin_GetBraidBoneBase(); HAIR_SEGMENT *const fs = &m_HairSegments[0]; fs->pos = pos; if (m_IsFirstHair) { m_IsFirstHair = false; for (int32_t i = 1; i <= M_HAIR_SEGMENTS; i++) { const ANIM_BONE *const bone = &bones[M_BONE_IDX(i)]; const HAIR_SEGMENT *const ps = &m_HairSegments[i - 1]; HAIR_SEGMENT *const s = &m_HairSegments[i]; Matrix_PushUnit(); Matrix_TranslateSet32(ps->pos); Matrix_RotY(ps->rot.y); Matrix_RotX(ps->rot.x); Matrix_TranslateRel32(bone->pos); s->pos.x = g_MatrixPtr->_03 >> W2V_SHIFT; s->pos.y = g_MatrixPtr->_13 >> W2V_SHIFT; s->pos.z = g_MatrixPtr->_23 >> W2V_SHIFT; Matrix_Pop(); } return; } int16_t room_num = lara_item->room_num; int32_t water_height; if (in_cutscene) { water_height = NO_HEIGHT; } else { water_height = Room_GetWaterHeight( (XYZ_32) { lara_item->pos.x + (frame_1->bounds.min.x + frame_1->bounds.max.x) / 2, lara_item->pos.y + (frame_1->bounds.max.y + frame_1->bounds.min.y) / 2, lara_item->pos.z + (frame_1->bounds.max.z + frame_1->bounds.min.z) / 2, }, room_num); } const SECTOR *const sector = Room_GetSector(fs->pos, &room_num); int32_t height = Room_GetHeight(sector, fs->pos); if (height < fs->pos.y) { height = lara_item->floor; } const XZ_32 smoke_wind = Sparks_GetSmokeWind(); const int32_t hair_wind_z = Sparks_GetHairWindZ(); for (int32_t i = 1; i <= M_HAIR_SEGMENTS; i++) { HAIR_SEGMENT *const ps = &m_HairSegments[i - 1]; HAIR_SEGMENT *const s = &m_HairSegments[i]; m_HairVelocity[0] = s->pos; s->pos.x += m_HairVelocity[i].x * 3 / 4; s->pos.y += m_HairVelocity[i].y * 3 / 4; s->pos.z += m_HairVelocity[i].z * 3 / 4; if (g_TRVersion == 3) { if (lara_info->water_status == LWS_ABOVE_WATER && Room_Get(room_num)->flags.wind) { s->pos.x += smoke_wind.x; s->pos.z += smoke_wind.z; } if (water_height == NO_HEIGHT || s->pos.y < water_height) { s->pos.y += 10; if (water_height != NO_HEIGHT && s->pos.y > water_height) { s->pos.y = water_height; } } if (s->pos.y > height) { s->pos.x = m_HairVelocity[0].x; if (s->pos.y - height <= STEP_L) { s->pos.y = height; } s->pos.z = m_HairVelocity[0].z; } } else { switch (lara_info->water_status) { case LWS_ABOVE_WATER: s->pos.y += 10; if (water_height != NO_HEIGHT && s->pos.y > water_height) { s->pos.y = water_height; } else if (s->pos.y > height) { s->pos.y = height; } else { s->pos.z += hair_wind_z; } break; case LWS_UNDERWATER: case LWS_SURFACE: case LWS_WADE: CLAMP(s->pos.y, water_height, height); break; default: break; } } for (int32_t j = 0; j < M_HAIR_SPHERES; j++) { const SPHERE *const sphere = &m_HairSpheres[j]; const int32_t dx = s->pos.x - sphere->pos.x; const int32_t dy = s->pos.y - sphere->pos.y; const int32_t dz = s->pos.z - sphere->pos.z; int32_t dist = SQUARE(dz) + SQUARE(dy) + SQUARE(dx); if (dist < SQUARE(sphere->r)) { dist = Math_Sqrt(dist); CLAMPL(dist, 1); s->pos.x = sphere->pos.x + sphere->r * dx / dist; s->pos.y = sphere->pos.y + sphere->r * dy / dist; s->pos.z = sphere->pos.z + sphere->r * dz / dist; } } const int32_t dx = s->pos.x - ps->pos.x; const int32_t dz = s->pos.z - ps->pos.z; const int32_t distance = Math_Sqrt(SQUARE(dx) + SQUARE(dz)); ps->rot.y = Math_Atan(dz, dx); ps->rot.x = -Math_Atan(distance, s->pos.y - ps->pos.y); Matrix_PushUnit(); Matrix_TranslateSet32(ps->pos); Matrix_RotY(ps->rot.y); Matrix_RotX(ps->rot.x); const ANIM_BONE *const bone = &bones[M_BONE_IDX(i)]; Matrix_TranslateRel32(bone->pos); s->pos.x = g_MatrixPtr->_03 >> W2V_SHIFT; s->pos.y = g_MatrixPtr->_13 >> W2V_SHIFT; s->pos.z = g_MatrixPtr->_23 >> W2V_SHIFT; m_HairVelocity[i].x = s->pos.x - m_HairVelocity[0].x; m_HairVelocity[i].y = s->pos.y - m_HairVelocity[0].y; m_HairVelocity[i].z = s->pos.z - m_HairVelocity[0].z; Matrix_Pop(); } } void Lara_Hair_Draw(void) { if (!Lara_Hair_IsActive()) { return; } const ITEM *const lara_item = Lara_GetItem(); const int32_t mesh_idx = Lara_Skin_GetBraidMeshIdx(); for (int32_t i = 0; i < M_HAIR_SEGMENTS; i++) { const HAIR_SEGMENT *const s = &m_HairSegments[i]; Matrix_Push(); Matrix_TranslateAbs32(s->interp.result.pos); Matrix_RotY(s->interp.result.rot.y); Matrix_RotX(s->interp.result.rot.x); Output_PushTintOverride(Lara_GetMeshTint((GAME_VECTOR) { .pos = s->interp.result.pos, .room_num = lara_item->room_num })); Object_DrawMesh(mesh_idx + i, CLIP_FULLY_VISIBLE, false); Output_PopTintOverride(); Matrix_Pop(); } } bool Lara_Hair_IsActive(void) { return g_Config.visuals.enable_braid && Object_Get(O_LARA)->loaded && Lara_Skin_IsBraidSupported(); } int32_t Lara_Hair_GetSegmentCount(void) { return M_HAIR_SEGMENTS; } HAIR_SEGMENT *Lara_Hair_GetSegment(const int32_t n) { return &m_HairSegments[n]; } ================================================ FILE: src/trx/game/lara/hair.h ================================================ #pragma once #include #include typedef struct { XYZ_32 pos; XYZ_16 rot; struct { struct { XYZ_32 pos; XYZ_16 rot; } result, prev; } interp; } HAIR_SEGMENT; void Lara_Hair_Initialise(void); bool Lara_Hair_IsActive(void); void Lara_Hair_Control(bool in_cutscene); void Lara_Hair_Draw(void); int32_t Lara_Hair_GetSegmentCount(void); HAIR_SEGMENT *Lara_Hair_GetSegment(int32_t n); ================================================ FILE: src/trx/game/lara/look.c ================================================ #include #include #include #include #include #include static const LARA_TRX_STATE m_StopStates[] = { LS_STOP, LS_SURF_TREAD, LS_POSE, LS_TRX_INVALID, }; static const LARA_TRX_STATE m_BlockingStates[] = { // clang-format off LS_JUMP_RIGHT, LS_JUMP_LEFT, LS_SPLAT, LS_STEP_RIGHT, LS_STEP_LEFT, LS_PUSH_BLOCK, LS_PULL_BLOCK, LS_PICKUP, LS_FLARE_PICKUP, LS_SWITCH_ON, LS_SWITCH_OFF, LS_USE_KEY, LS_USE_PUZZLE, LS_NEUTRAL_ROLL, LS_TRX_INVALID, // clang-format on }; static const LARA_EXTRA_STATE m_PermittedExtraStates[] = { // clang-format off LS_EXTRA_BREATH, LS_EXTRA_AIRLOCK, (LARA_EXTRA_STATE)-1, // clang-format on }; static void M_Reset(void) { if (g_Camera.type == CAM_LOOK) { return; } LARA_INFO *const lara = Lara_GetLaraInfo(); const CAMERA_LOOK_SETTINGS *const look = Camera_GetLookSettings(false); if (lara->head_rot.x <= -look->head_turn || lara->head_rot.x >= look->head_turn) { lara->head_rot.x -= lara->head_rot.x / 8; } else { lara->head_rot.x = 0; } if (lara->head_rot.y <= -look->head_turn || lara->head_rot.y >= look->head_turn) { lara->head_rot.y += lara->head_rot.y / -8; } else { lara->head_rot.y = 0; } lara->torso_rot.x = lara->head_rot.x; lara->torso_rot.y = lara->head_rot.y; } static bool M_IsLaraIdle(void) { const ITEM *const vehicle = Lara_Vehicle_GetItem(); if (vehicle != nullptr) { return vehicle->speed == 0; } return Lara_HasState(m_StopStates); } static bool M_IsStatePermitted(void) { const ITEM *const lara_item = Lara_GetItem(); if (lara_item->hit_points <= 0) { return false; } const LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (lara_info->extra_anim) { return g_Config.gameplay.look_mode == LOOK_MODE_UNRESTRICTED && Lara_HasExtraState(m_PermittedExtraStates); } return g_Config.gameplay.look_mode == LOOK_MODE_UNRESTRICTED || !Lara_HasState(m_BlockingStates); } void Lara_Look_LeftRight(void) { g_Camera.type = CAM_LOOK; LARA_INFO *const lara = Lara_GetLaraInfo(); const CAMERA_LOOK_SETTINGS *const look = Camera_GetLookSettings(lara->water_status == LWS_SURFACE); if (g_Input.left) { g_Input.left = 0; if (lara->head_rot.y > look->min_head_rotation) { lara->head_rot.y -= look->head_turn; } } else if (g_Input.right) { g_Input.right = 0; if (lara->head_rot.y < look->max_head_rotation) { lara->head_rot.y += look->head_turn; } } if (lara->gun_status != LGS_HANDS_BUSY && !Lara_Vehicle_IsMounted()) { lara->torso_rot.y = lara->head_rot.y * look->torso_head_rot_y; } } void Lara_Look_UpDown(void) { g_Camera.type = CAM_LOOK; if (g_Config.gameplay.enable_inverted_look) { bool temp_forward; SWAP2(g_Input.forward, g_Input.back, temp_forward); } LARA_INFO *const lara = Lara_GetLaraInfo(); const CAMERA_LOOK_SETTINGS *const look = Camera_GetLookSettings(lara->water_status == LWS_SURFACE); if (g_Input.forward) { g_Input.forward = 0; if (lara->head_rot.x > look->min_head_tilt) { lara->head_rot.x -= look->head_turn; } } else if (g_Input.back) { g_Input.back = 0; if (lara->head_rot.x < look->max_head_tilt) { lara->head_rot.x += look->head_turn; } } if (lara->gun_status != LGS_HANDS_BUSY) { lara->torso_rot.x = lara->head_rot.x * look->torso_head_rot_x; } } void Lara_Look_Update(void) { if (g_Input.look && g_Config.gameplay.look_mode == LOOK_MODE_RESTRICTED && !M_IsLaraIdle()) { if (g_Camera.type == CAM_LOOK) { g_Camera.type = CAM_CHASE; } M_Reset(); return; } if (g_Input.look && M_IsStatePermitted()) { Lara_Look_LeftRight(); } else { M_Reset(); } } ================================================ FILE: src/trx/game/lara/look.h ================================================ #pragma once void Lara_Look_UpDown(void); void Lara_Look_LeftRight(void); void Lara_Look_Update(void); ================================================ FILE: src/trx/game/lara/mesh.c ================================================ #include #include #include #include #include #include #include #include #include static OBJECT_MESH *m_Meshes[LM_NUMBER_OF] = {}; static LARA_GUN_TYPE M_DetermineHolsterGun(void) { const LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (lara_info->holsters_gun_type == LGT_UNARMED) { if (lara_info->gun_type != LGT_UNARMED && !Gun_IsRifleType(lara_info->gun_type)) { return lara_info->gun_type; } else if (Inv_RequestItem(O_PISTOL_ITEM)) { return LGT_PISTOLS; } else if (Inv_RequestItem(O_MAGNUM_ITEM)) { return LGT_MAGNUMS; } else if (Inv_RequestItem(O_AUTOS_ITEM)) { return LGT_AUTOS; } else if (Inv_RequestItem(O_DESERT_EAGLE_ITEM)) { return LGT_DESERT_EAGLE; } else if (Inv_RequestItem(O_UZI_ITEM)) { return LGT_UZIS; } } return lara_info->holsters_gun_type; } static LARA_GUN_TYPE M_DetermineBackGun(void) { const LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (lara_info->back_gun_type != LGT_UNARMED) { return lara_info->back_gun_type; } if (Inv_RequestItem(O_SHOTGUN_ITEM)) { return LGT_SHOTGUN; } else if (Inv_RequestItem(O_M16_ITEM)) { return LGT_M16; } else if (Inv_RequestItem(O_MP5_ITEM)) { return LGT_MP5; } else if (Inv_RequestItem(O_GRENADE_GUN_ITEM)) { return LGT_GRENADE; } else if (Inv_RequestItem(O_ROCKET_GUN_ITEM)) { return LGT_ROCKET; } else if (Inv_RequestItem(O_HARPOON_ITEM)) { return LGT_HARPOON; } return LGT_UNARMED; } static void M_InitialiseCutsceneLevel(void) { Lara_Skin_SetGunEquipment(LM_THIGH_L, LGT_PISTOLS); Lara_Skin_SetGunEquipment(LM_THIGH_R, LGT_PISTOLS); } static void M_InitialiseNormalLevel(const GF_LEVEL *const level) { const RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); const LARA_GUN_TYPE holster_gun = M_DetermineHolsterGun(); if (holster_gun != LGT_UNARMED && holster_gun != LGT_FLARE) { Gun_SetLaraHolsterLMesh(holster_gun); Gun_SetLaraHolsterRMesh(holster_gun); } const LARA_GUN_TYPE back_gun = M_DetermineBackGun(); if (back_gun != LGT_UNARMED) { Gun_SetLaraBackMesh(back_gun); } if (resume != nullptr && resume->equipped_gun_type == LGT_FLARE) { Lara_Skin_SetGunEquipment(LM_HAND_L, LGT_FLARE); } } void Lara_Mesh_Initialise(const GF_LEVEL *const level) { const OBJECT *const skin_obj = Object_Get(O_LARA_SKIN); if (skin_obj->loaded) { OBJECT *const lara_obj = Object_Get(O_LARA); lara_obj->mesh_idx = skin_obj->mesh_idx; } if (level->type == GFL_CUTSCENE) { M_InitialiseCutsceneLevel(); } else { M_InitialiseNormalLevel(level); } } void Lara_Mesh_SwapSingle(const LARA_MESH mesh, const OBJECT_ID obj_id) { const OBJECT *const obj = Object_Get(obj_id); Lara_Mesh_Set(mesh, Object_GetMesh(obj->mesh_idx + mesh)); } void Lara_Mesh_SwapAll(const OBJECT_ID obj_id) { if (!Object_Get(obj_id)->loaded) { return; } for (LARA_MESH mesh = LM_FIRST; mesh < LM_NUMBER_OF; mesh++) { Lara_Mesh_SwapSingle(mesh, obj_id); } } void Lara_Mesh_Set(const LARA_MESH mesh, OBJECT_MESH *const mesh_ptr) { m_Meshes[mesh] = mesh_ptr; } OBJECT_MESH *Lara_Mesh_Get(const LARA_MESH mesh) { return m_Meshes[mesh]; } RGB_F Lara_GetMeshTint(const GAME_VECTOR pos) { if (!g_Config.visuals.enable_responsive_mesh_tint || g_Camera.underwater) { return Output_GetTint(); } int16_t room_num = pos.room_num; Room_GetSector(pos.pos, &room_num); const int32_t water_height = Room_GetWaterHeight(pos.pos, room_num); if (!Room_Get(room_num)->flags.underwater) { return COLOR_RGB_F_WHITE; } else if (water_height == NO_HEIGHT) { return Output_GetWaterColor(); } else if (pos.y > water_height) { return Output_GetWaterColor(); } else { return COLOR_RGB_F_WHITE; } } int32_t Lara_GetMeshIndex(const ITEM *const item, const int32_t mesh_idx) { const OBJECT *const obj = Object_Get(item->object_id); const int32_t fallback = obj->mesh_idx + mesh_idx; const OBJECT_MESH *const mesh = Lara_Mesh_Get(mesh_idx); if (mesh == nullptr) { return fallback; } const int32_t resolved = Object_GetMeshIndex(mesh); if (resolved < 0) { return fallback; } return resolved; } ================================================ FILE: src/trx/game/lara/mesh.h ================================================ #pragma once #include #include #include #include void Lara_Mesh_Initialise(const GF_LEVEL *level); void Lara_Mesh_SwapSingle(LARA_MESH mesh, OBJECT_ID obj_id); void Lara_Mesh_SwapAll(OBJECT_ID obj_id); void Lara_Mesh_Set(LARA_MESH mesh, OBJECT_MESH *mesh_ptr); OBJECT_MESH *Lara_Mesh_Get(LARA_MESH mesh); RGB_F Lara_GetMeshTint(GAME_VECTOR pos); int32_t Lara_GetMeshIndex(const ITEM *item, int32_t mesh_idx); ================================================ FILE: src/trx/game/lara/misc.c ================================================ #include #include #include #include #include #include #include #include #include #include #include static void M_GetJointAbsPosition_I( XYZ_32 *const vec, const ANIM_FRAME *const frame1, const ANIM_FRAME *const frame2, const int32_t frac, const int32_t rate) { const LARA_INFO *const lara_info = Lara_GetLaraInfo(); const ITEM *const item = Lara_GetItem(); const OBJECT *obj = Object_Get(item->object_id); Matrix_PushUnit(); Matrix_Rot16(item->rot); const ANIM_BONE *const bone = Object_GetBone(obj, 0); const XYZ_16 *mesh_rots_1 = frame1->mesh_rots; const XYZ_16 *mesh_rots_2 = frame2->mesh_rots; Matrix_InitInterpolate(frac, rate); Matrix_TranslateRel16_ID(frame1->offset, frame2->offset); Matrix_Rot16_ID(mesh_rots_1[LM_HIPS], mesh_rots_2[LM_HIPS]); Matrix_TranslateRel32_I(bone[LM_TORSO - 1].pos); Matrix_Rot16_ID(mesh_rots_1[LM_TORSO], mesh_rots_2[LM_TORSO]); Matrix_Rot16_I(lara_info->torso_rot); LARA_GUN_TYPE gun_type = LGT_UNARMED; if (lara_info->gun_status == LGS_READY || lara_info->gun_status == LGS_SPECIAL || lara_info->gun_status == LGS_DRAW || lara_info->gun_status == LGS_UNDRAW) { gun_type = lara_info->gun_type; } if (lara_info->gun_type == LGT_FLARE) { Matrix_Interpolate(); Matrix_TranslateRel32(bone[LM_UARM_L - 1].pos); if (lara_info->flare.control) { const LARA_ARM *const arm = &lara_info->left_arm; const ANIM *const anim = Anim_GetAnim(arm->anim_num); mesh_rots_1 = arm->frame_base[arm->frame_num - anim->frame_base].mesh_rots; } Matrix_Rot16(mesh_rots_1[LM_UARM_L]); Matrix_TranslateRel32(bone[LM_LARM_L - 1].pos); Matrix_Rot16(mesh_rots_1[LM_LARM_L]); Matrix_TranslateRel32(bone[LM_HAND_L - 1].pos); Matrix_Rot16(mesh_rots_1[LM_HAND_L]); } else if (gun_type != LGT_UNARMED) { Matrix_Interpolate(); Matrix_TranslateRel32(bone[LM_UARM_R - 1].pos); const LARA_ARM *const arm = &lara_info->right_arm; const ANIM *const anim = Anim_GetAnim(arm->anim_num); mesh_rots_1 = arm->frame_base[arm->frame_num].mesh_rots; Matrix_Rot16(mesh_rots_1[LM_UARM_R]); Matrix_TranslateRel32(bone[LM_LARM_R - 1].pos); Matrix_Rot16(mesh_rots_1[LM_LARM_R]); Matrix_TranslateRel32(bone[LM_HAND_R - 1].pos); Matrix_Rot16(mesh_rots_1[LM_HAND_R]); } Matrix_TranslateRel32(*vec); vec->x = item->pos.x + (g_MatrixPtr->_03 >> W2V_SHIFT); vec->y = item->pos.y + (g_MatrixPtr->_13 >> W2V_SHIFT); vec->z = item->pos.z + (g_MatrixPtr->_23 >> W2V_SHIFT); Matrix_Pop(); } // TODO: joint is ignored - this only works for hands. void Lara_GetJointAbsPosition(XYZ_32 *const vec, const LARA_MESH joint) { const LARA_INFO *const lara_info = Lara_GetLaraInfo(); const ITEM *const lara_item = Lara_GetItem(); ANIM_FRAME *frmptr[2] = { nullptr, nullptr }; if (lara_info->hit_direction < 0) { int32_t rate; const int32_t frac = Item_GetFrames(lara_item, frmptr, &rate); if (frac != 0) { M_GetJointAbsPosition_I(vec, frmptr[0], frmptr[1], frac, rate); return; } } const ANIM_FRAME *const hit_frame = Lara_GetHitFrame(lara_item); const ANIM_FRAME *const frame_ptr = hit_frame == nullptr ? frmptr[0] : hit_frame; Matrix_PushUnit(); Matrix_Rot16(lara_item->rot); const XYZ_16 *mesh_rots = frame_ptr->mesh_rots; const OBJECT *const obj = Object_Get(lara_item->object_id); const ANIM_BONE *bone = Object_GetBone(obj, 0); Matrix_TranslateRel16(frame_ptr->offset); Matrix_Rot16(mesh_rots[LM_HIPS]); Matrix_TranslateRel32(bone[LM_TORSO - 1].pos); Matrix_Rot16(mesh_rots[LM_TORSO]); Matrix_Rot16(lara_info->torso_rot); LARA_GUN_TYPE gun_type = LGT_UNARMED; if (lara_info->gun_status == LGS_READY || lara_info->gun_status == LGS_SPECIAL || lara_info->gun_status == LGS_DRAW || lara_info->gun_status == LGS_UNDRAW) { gun_type = lara_info->gun_type; } if (lara_info->gun_type == LGT_FLARE) { Matrix_TranslateRel32(bone[LM_UARM_L - 1].pos); if (lara_info->flare.control) { const LARA_ARM *const arm = &lara_info->left_arm; const ANIM *const anim = Anim_GetAnim(arm->anim_num); mesh_rots = arm->frame_base[arm->frame_num - anim->frame_base].mesh_rots; } Matrix_Rot16(mesh_rots[LM_UARM_L]); Matrix_TranslateRel32(bone[LM_LARM_L - 1].pos); Matrix_Rot16(mesh_rots[LM_LARM_L]); Matrix_TranslateRel32(bone[LM_HAND_L - 1].pos); Matrix_Rot16(mesh_rots[LM_HAND_L]); } else if (gun_type != LGT_UNARMED) { Matrix_TranslateRel32(bone[LM_UARM_R - 1].pos); const LARA_ARM *const arm = &lara_info->right_arm; const ANIM *const anim = Anim_GetAnim(arm->anim_num); mesh_rots = arm->frame_base[arm->frame_num].mesh_rots; Matrix_Rot16(mesh_rots[LM_UARM_R]); Matrix_TranslateRel32(bone[LM_LARM_R - 1].pos); Matrix_Rot16(mesh_rots[LM_LARM_R]); Matrix_TranslateRel32(bone[LM_HAND_R - 1].pos); Matrix_Rot16(mesh_rots[LM_HAND_R]); } Matrix_TranslateRel32(*vec); vec->x = lara_item->pos.x + (g_MatrixPtr->_03 >> W2V_SHIFT); vec->y = lara_item->pos.y + (g_MatrixPtr->_13 >> W2V_SHIFT); vec->z = lara_item->pos.z + (g_MatrixPtr->_23 >> W2V_SHIFT); Matrix_Pop(); } void Lara_RefuseInteraction(void) { const ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (!XYZ_32_AreEquivalent( lara_info->interact_target.initial_pos, lara_item->pos)) { lara_info->interact_target.initial_pos = lara_item->pos; Sound_Effect(SFX_LARA_NO, &lara_item->pos, SPM_ALWAYS); } } void Lara_TakeHit(ITEM *const lara_item, const int32_t dx, const int32_t dz) { LARA_INFO *const lara_info = Lara_GetLaraInfo(); const int16_t hit_angle = lara_item->rot.y + DEG_180 - Math_Atan(dz, dx); lara_info->hit_direction = Math_GetDirection(hit_angle); if (lara_info->hit_frame == 0) { Sound_Effect( g_TRVersion == 1 ? SFX_LARA_BODYSL : SFX_LARA_INJURY, &lara_item->pos, SPM_NORMAL); } lara_info->hit_frame++; if (lara_info->interact_target.is_moving && lara_info->gun_status == LGS_HANDS_BUSY) { lara_info->gun_status = LGS_ARMLESS; } lara_info->interact_target.is_moving = false; lara_info->interact_target.item_num = NO_ITEM; CLAMPG(lara_info->hit_frame, 34); } void Lara_TouchDeathSector(const GF_DEATH_TILE death_tile) { ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (lara_item->hit_points < 0 || lara_info->water_status == LWS_CHEAT) { return; } int16_t room_num = lara_item->room_num; const XYZ_32 pos = { lara_item->pos.x, MAX_HEIGHT, lara_item->pos.z }; const SECTOR *const sector = Room_GetSector(pos, &room_num); const int32_t height = Room_GetHeight(sector, pos); if (lara_item->floor != height) { return; } if (g_Config.debug.enable_invulnerability) { switch (death_tile) { case GF_DEATH_TILE_RAPIDS: case GF_DEATH_TILE_ELECTRIC: Lara_CatchFire(); break; case GF_DEATH_TILE_LAVA: Lara_TouchLava(); break; } return; } lara_item->hit_points = -1; lara_item->hit_status = true; switch (death_tile) { case GF_DEATH_TILE_RAPIDS: Lara_RapidsDrown(); break; case GF_DEATH_TILE_ELECTRIC: lara_info->electric = 1; break; case GF_DEATH_TILE_LAVA: Lara_TouchLava(); break; } } void Lara_TouchLava(void) { if (g_TRVersion == 3) { Lara_CatchFire(); return; } ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (lara_info->burn || lara_info->water_status != LWS_ABOVE_WATER) { return; } const OBJECT *const obj = Object_Get(O_FLAME); for (int32_t i = 0; i < 10; i++) { const int16_t effect_num = Effect_Create(lara_item->room_num); if (effect_num != NO_EFFECT) { EFFECT *const effect = Effect_Get(effect_num); effect->object_id = O_FLAME; effect->frame_num = obj->mesh_count * Random_GetControl() / 0x7FFF; effect->counter = -1 - 24 * Random_GetControl() / 0x7FFF; } } lara_info->burn = true; } void Lara_RapidsDrown(void) { ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara_info = Lara_GetLaraInfo(); Lara_SwitchToExtraState(LS_EXTRA_RAPIDS_DROWN); lara_item->gravity = false; lara_item->hit_points = -1; lara_item->hit_status = true; lara_item->fall_speed = 0; lara_item->speed = 0; lara_info->gun_type = LGT_UNARMED; } int32_t Lara_FloorFront( const ITEM *const item, const int16_t ang, const int32_t dist) { XYZ_32 pos = item->pos; pos.y -= LARA_HEIGHT; pos = XYZ_32_OffsetYaw(pos, ang, dist); int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); int32_t height = Room_GetHeight(sector, pos); if (height != NO_HEIGHT) { height -= item->pos.y; if (height > 0 && Room_GetPitSector(sector, pos.x, pos.z)->is_death_sector) { return STEP_L * 2; } } return height; } int32_t Lara_CeilingFront( const ITEM *const item, const int16_t ang, const int32_t dist, const int32_t item_height) { const int32_t x = item->pos.x + ((dist * Math_Sin(ang)) >> W2V_SHIFT); const int32_t y = item->pos.y - item_height; const int32_t z = item->pos.z + ((dist * Math_Cos(ang)) >> W2V_SHIFT); const XYZ_32 pos = { x, y, z }; int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); int32_t height = Room_GetCeiling(sector, pos); if (height != NO_HEIGHT) { height += item_height - item->pos.y; } return height; } void Lara_UpdateRoomToHeight(const int32_t height) { ITEM *const lara_item = Lara_GetItem(); XYZ_32 pos = lara_item->pos; pos.y += height; int16_t room_num = lara_item->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); lara_item->floor = Room_GetHeight(sector, pos); const int16_t item_num = Item_GetIndex(lara_item); Item_UpdateRoom(item_num, room_num); } int32_t Lara_GetWaterDepth( const int32_t x, const int32_t y, const int32_t z, int16_t room_num) { const ROOM *room = Room_Get(room_num); const SECTOR *sector; while (true) { int32_t z_sector = (z - room->pos.z) >> WALL_SHIFT; int32_t x_sector = (x - room->pos.x) >> WALL_SHIFT; if (z_sector <= 0) { z_sector = 0; if (x_sector < 1) { x_sector = 1; } else if (x_sector > room->size.x - 2) { x_sector = room->size.x - 2; } } else if (z_sector >= room->size.z - 1) { z_sector = room->size.z - 1; if (x_sector < 1) { x_sector = 1; } else if (x_sector > room->size.x - 2) { x_sector = room->size.x - 2; } } else if (x_sector < 0) { x_sector = 0; } else if (x_sector >= room->size.x) { x_sector = room->size.x - 1; } sector = Room_GetUnitSector(room, x_sector, z_sector); if (sector->portal_room.wall == NO_ROOM) { break; } room_num = sector->portal_room.wall; room = Room_Get(room_num); } if (room->flags.underwater || room->flags.swamp) { while (sector->portal_room.sky != NO_ROOM) { room = Room_Get(sector->portal_room.sky); if (!room->flags.underwater && !room->flags.swamp) { const XYZ_32 pos = { x, y, z }; const int32_t water_height = Room_GetWaterHeight(pos, room_num); sector = Room_GetSector(pos, &room_num); return Room_GetHeight(sector, pos) - water_height; } sector = Room_GetWorldSector(room, x, z); } return 0x7FFF; } while (sector->portal_room.pit != NO_ROOM) { room = Room_Get(sector->portal_room.pit); if (room->flags.underwater || room->flags.swamp) { const XYZ_32 pos = { x, y, z }; const int32_t water_height = Room_GetWaterHeight(pos, room_num); sector = Room_GetSector(pos, &room_num); return Room_GetHeight(sector, pos) - water_height; } sector = Room_GetWorldSector(room, x, z); } return NO_HEIGHT; } bool Lara_IsM16Active(void) { const LARA_INFO *const lara = Lara_GetLaraInfo(); ITEM *const lara_item = Lara_GetItem(); if (lara->gun_item_num == NO_ITEM || lara_item->hit_points <= 0 || (lara->gun_type != LGT_M16 && lara->gun_type != LGT_MP5)) { return false; } const ITEM *const item = Item_Get(lara->gun_item_num); return item->current_anim_state == 0 || item->current_anim_state == 2 || item->current_anim_state == 4; } void Lara_CatchFireEx(const FLAME_TYPE type) { LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (lara_info->burn || lara_info->water_status == LWS_CHEAT) { return; } const ITEM *const lara_item = Lara_GetItem(); const int16_t effect_num = Effect_Create(lara_item->room_num); if (effect_num == NO_EFFECT) { return; } EFFECT *const effect = Effect_Get(effect_num); if (g_TRVersion == 3) { // TR3 effects use Collide_GetJointAbsPosition but only every x frames, // which lets Lara briefly catch fire even if she touches liquids // (for example, when running into the boiling water in Tony's room). effect->pos = (XYZ_32) {}; } else { effect->pos = lara_item->pos; } effect->frame_num = g_TRVersion == 3 ? type : 0; effect->object_id = O_FLAME; effect->counter = -1; lara_info->burn = true; } void Lara_CatchFire(void) { Lara_CatchFireEx(FLAME_SMALL); } void Lara_Extinguish(void) { LARA_INFO *const lara_info = Lara_GetLaraInfo(); lara_info->electric = 0; if (!lara_info->burn) { return; } lara_info->burn = false; // put out flame objects int16_t effect_num = Effect_GetActiveNum(); while (effect_num != NO_EFFECT) { EFFECT *const effect = Effect_Get(effect_num); const int16_t next_effect_num = effect->next_active; if (effect->object_id == O_FLAME && effect->counter < 0) { effect->counter = 0; Effect_Kill(effect_num); } effect_num = next_effect_num; } } bool Lara_HasState(const LARA_TRX_STATE *const test_arr) { const LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (lara_info->extra_anim) { return false; } const ITEM *const lara_item = Lara_GetItem(); for (int32_t i = 0; test_arr[i] != LS_TRX_INVALID; i++) { if (test_arr[i] == LS_U(lara_item->current_anim_state)) { return true; } } return false; } bool Lara_HasExtraState(const LARA_EXTRA_STATE *const test_arr) { const LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (!lara_info->extra_anim) { return false; } const ITEM *const lara_item = Lara_GetItem(); for (int32_t i = 0; test_arr[i] != (LARA_EXTRA_STATE)-1; i++) { if (test_arr[i] == (LARA_EXTRA_STATE)lara_item->current_anim_state) { return true; } } return false; } void Lara_SwitchToExtraState(const LARA_EXTRA_STATE goal_state) { ITEM *const lara_item = Lara_GetItem(); Item_SwitchToObjAnim(lara_item, LS_EXTRA_BREATH, 0, O_LARA_EXTRA); lara_item->current_anim_state = LS_EXTRA_BREATH; lara_item->goal_anim_state = goal_state; Item_Animate(lara_item); LARA_INFO *const lara = Lara_GetLaraInfo(); lara->gun_status = LGS_HANDS_BUSY; lara->hit_direction = DIR_UNKNOWN; lara->extra_anim = true; } ================================================ FILE: src/trx/game/lara/misc.h ================================================ #pragma once #include #include #include #include void Lara_GetJointAbsPosition(XYZ_32 *vec, LARA_MESH joint); void Lara_RefuseInteraction(void); void Lara_TakeHit(ITEM *lara_item, int32_t dx, int32_t dz); void Lara_Extinguish(void); void Lara_TouchLava(void); void Lara_TouchDeathSector(GF_DEATH_TILE death_tile); void Lara_RapidsDrown(void); int32_t Lara_FloorFront(const ITEM *item, int16_t ang, int32_t dist); int32_t Lara_CeilingFront( const ITEM *item, int16_t ang, int32_t dist, int32_t item_height); void Lara_CatchFireEx(FLAME_TYPE type); void Lara_CatchFire(void); void Lara_UpdateRoomToHeight(int32_t height); int32_t Lara_GetWaterDepth(int32_t x, int32_t y, int32_t z, int16_t room_num); // Returns true if Lara has the M16 equipped and is in either anim state: 0 // (start aim); 2 (firing); or 4 (stopping firing). bool Lara_IsM16Active(void); bool Lara_HasState(const LARA_TRX_STATE *test_arr); bool Lara_HasExtraState(const LARA_EXTRA_STATE *test_arr); void Lara_SwitchToExtraState(LARA_EXTRA_STATE goal_state); ================================================ FILE: src/trx/game/lara/pose.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_NO_POSE (-1) static VECTOR *m_Poses = nullptr; static int32_t m_ActivePose = M_NO_POSE; static void M_WarnWithJSONError(const JSON_READ_IO *const io) { char warning_message[1024]; JSON_ReadIO_FormatError( io, false, warning_message, sizeof(warning_message)); LOG_WARNING("%s", warning_message); } static bool M_LoadPose(JSON_READ_IO *const io, LARA_POSE *const pose) { JSON_MUST(JSON_READ(io, "offset", &pose->offset)); JSON_MUST(JSON_PUSH(io, "rots")); const int32_t rot_count = JSON_ARRAY_LEN(io); if (rot_count < 0) { JSON_MUST(JSON_POP(io)); JSON_FAIL(); } if (rot_count != LM_NUMBER_OF) { JSON_ReadIO_SetError( io, "expected exactly %d rotations, got %d", LM_NUMBER_OF, rot_count); JSON_MUST(JSON_POP(io)); JSON_FAIL(); } for (int32_t i = 0; i < LM_NUMBER_OF; i++) { JSON_MUST(JSON_READ_A(io, i, &pose->rots[i])); } JSON_MUST(JSON_POP(io)); JSON_FINISH(); } static bool M_LoadPosesArray(JSON_READ_IO *const io, VECTOR *const poses) { const int32_t pose_count = JSON_ARRAY_LEN(io); if (pose_count < 0) { JSON_FAIL(); } for (int32_t i = 0; i < pose_count; i++) { JSON_MUST(JSON_PUSH_INDEX(io, i)); LARA_POSE pose = {}; if (JSON_SHOULD(M_LoadPose(io, &pose))) { Vector_Add(poses, &pose); } JSON_MUST(JSON_POP(io)); } JSON_FINISH(); } static void M_LoadPoses(void) { m_Poses = Vector_Create(sizeof(LARA_POSE)); ASSERT(m_Poses != nullptr); const char *const poses_path = TRXPath_TryResolve(TRX_DYNAMIC_PATH_COMMON_CONFIG, "poses.json5"); if (poses_path == nullptr) { return; } JSON_VALUE *const doc = JSONFile_Read(poses_path); if (doc == nullptr) { return; } JSON_READ_IO *const io = JSON_ReadIO_Create(doc, 0, poses_path); if (!M_LoadPosesArray(io, m_Poses)) { M_WarnWithJSONError(io); } JSON_ReadIO_Destroy(io); JSON_ValueFree(doc); } void Lara_Pose_Init(void) { if (m_Poses == nullptr) { M_LoadPoses(); } } void Lara_Pose_Shutdown(void) { if (m_Poses != nullptr) { Vector_Free(m_Poses); m_Poses = nullptr; } } bool Lara_Pose_IsAvailable(void) { return m_Poses->count > 0 && Object_Get(O_LARA)->loaded && GF_GetCurrentLevel()->type != GFL_CUTSCENE; } void Lara_Pose_Clear(void) { if (m_ActivePose != M_NO_POSE) { LOG_DEBUG("Clearing Lara's pose"); } m_ActivePose = M_NO_POSE; } void Lara_Pose_Cycle(const int32_t dir) { if (!Lara_Pose_IsAvailable()) { return; } if (m_ActivePose == M_NO_POSE) { m_ActivePose = (dir > 0) ? 0 : m_Poses->count - 1; } else { m_ActivePose += dir; m_ActivePose += m_Poses->count; m_ActivePose %= m_Poses->count; } LOG_DEBUG("Active Lara pose: %d", m_ActivePose); Lara_Hair_Control(true); Interpolation_CommitBraid(); } const LARA_POSE *Lara_Pose_Get(void) { if (m_ActivePose == M_NO_POSE) { return nullptr; } return Vector_Get(m_Poses, m_ActivePose); } ================================================ FILE: src/trx/game/lara/pose.h ================================================ #pragma once #include #include typedef struct { XYZ_16 offset; XYZ_16 rots[LM_NUMBER_OF]; } LARA_POSE; void Lara_Pose_Init(); void Lara_Pose_Shutdown(); bool Lara_Pose_IsAvailable(void); void Lara_Pose_Clear(void); void Lara_Pose_Cycle(int32_t dir); const LARA_POSE *Lara_Pose_Get(void); ================================================ FILE: src/trx/game/lara/skin/common.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #define M_NO_OUTFIT (-1) #define M_NO_MESH (-1) static LARA_SKIN_TYPE m_SkinType = LARA_SKIN_TYPE_DEFAULT; static bool m_HolstersVisible = true; static bool m_UseCombatFace = false; static LARA_GUN_TYPE m_HolsterType_L = LGT_UNARMED; static LARA_GUN_TYPE m_HolsterType_R = LGT_UNARMED; static LARA_SKIN_EQUIPMENT m_Equipment[LM_NUMBER_OF] = {}; static inline const LARA_SKIN_OUTFIT *M_GetCurrentOutfit(void) { if (!Lara_Skin_IsOutfitAvailable(m_SkinType)) { m_SkinType = Lara_Skin_GetDefaultType(); } return Lara_Skin_GetOutfit(m_SkinType); } static LARA_SKIN_TYPE M_ResolveOutfitTypeFromName( const char *const outfit_name, const bool warn_on_invalid, const char *const source) { if (outfit_name == nullptr) { return LARA_SKIN_TYPE_DEFAULT; } const LARA_SKIN_TYPE type = Lara_Skin_FindOutfitByName(outfit_name); if (Lara_Skin_IsOutfitAvailable(type)) { return type; } if (warn_on_invalid) { LOG_WARNING( "Invalid outfit '%s' from %s; falling back to default", outfit_name, source); } return LARA_SKIN_TYPE_DEFAULT; } static LARA_SKIN_TYPE M_GetFallbackOutfitType(void) { return Lara_Skin_GetDefaultType(); } static void M_SetConfigOutfit(const char *const outfit_name) { ASSERT(outfit_name != nullptr); char *const old = g_Config.visuals.lara_outfit; g_Config.visuals.lara_outfit = Memory_DupStr(outfit_name); // Keep the old pointer alive until after the duplication so Config_Update // can reliably detect a string change via pointer identity. Memory_Free(old); } static LARA_SKIN_TYPE M_GetCurrentLevelOutfitType(void) { const GF_LEVEL *const level = GF_GetCurrentLevel(); if (level == nullptr) { return M_GetFallbackOutfitType(); } const LARA_SKIN_TYPE level_type = M_ResolveOutfitTypeFromName( level->lara_outfit, true, "gameflow level setting"); if (level_type != LARA_SKIN_TYPE_DEFAULT) { return level_type; } return M_GetFallbackOutfitType(); } static int32_t M_GetBraidDependentMeshIdx( const LARA_MESH mesh_idx, const LARA_SKIN_OUTFIT *const outfit) { if (mesh_idx != LM_TORSO && mesh_idx != LM_HEAD) { return M_NO_MESH; } LARA_SKIN_EXTRA_MESH extra_id; switch (outfit->braid.mode) { case BRAID_MODE_TR1_HEAD_ONLY: if (mesh_idx != LM_HEAD) { return M_NO_MESH; } extra_id = EXTRA_MESH_TR1_BRAID_DEFAULT_HEAD; break; case BRAID_MODE_TR1_FULL: extra_id = mesh_idx == LM_TORSO ? EXTRA_MESH_TR1_BRAID_DEFAULT_TORSO : EXTRA_MESH_TR1_BRAID_DEFAULT_HEAD; break; case BRAID_MODE_TR1_MAULED: extra_id = mesh_idx == LM_TORSO ? EXTRA_MESH_TR1_BRAID_MAULED_TORSO : EXTRA_MESH_TR1_BRAID_DEFAULT_HEAD; break; case BRAID_MODE_TR1_GOLD: extra_id = mesh_idx == LM_TORSO ? EXTRA_MESH_TR1_BRAID_GOLD_TORSO : EXTRA_MESH_TR1_BRAID_GOLD_HEAD; break; default: return M_NO_MESH; } const OBJECT *const extra_obj = Object_Get(O_LARA_SKIN_SWAP_EXTRA); const int32_t offset = Lara_Skin_GetExtraMeshOffset(extra_id); return extra_obj->mesh_idx + offset; } static int32_t M_GetNoHolsterMeshIdx( const LARA_MESH mesh, const LARA_SKIN_OUTFIT *const outfit) { if (m_HolstersVisible) { return M_NO_MESH; } if (mesh != LM_THIGH_L && mesh != LM_THIGH_R) { return M_NO_MESH; } const OBJECT *const obj = Object_Get(O_LARA_SKIN_SWAP_LEGS); if (!obj->loaded) { return M_NO_MESH; } const int32_t offset = mesh == LM_THIGH_L ? outfit->no_holster_offsets.left : outfit->no_holster_offsets.right; if (offset == M_NO_MESH) { return M_NO_MESH; } return obj->mesh_idx + offset; } static inline int32_t M_GetRelativeBraidOffset(void) { const LARA_SKIN_OUTFIT *const outfit = M_GetCurrentOutfit(); if (!outfit->braid.enabled) { return M_NO_MESH; } const LARA_INFO *const lara = Lara_GetLaraInfo(); int32_t offset = outfit->braid.mesh_offset; if (outfit->is_reflective || (lara->mesh_effects & (1 << LM_HEAD)) != 0) { offset = outfit->braid.gold_offset; } return offset; } static inline int32_t M_GetMeshIdx( const LARA_MESH mesh, const LARA_SKIN_OUTFIT *const outfit) { const OBJECT *const skin_obj = Object_Get(outfit->obj_id); int32_t offset = M_NO_MESH; if (g_Config.visuals.enable_braid) { offset = M_GetBraidDependentMeshIdx(mesh, outfit); } if (offset == M_NO_MESH) { offset = M_GetNoHolsterMeshIdx(mesh, outfit); } if (offset == M_NO_MESH) { offset = skin_obj->mesh_idx + mesh; } return offset; } static inline void M_ApplyMeshIfValid( const LARA_MESH mesh, const LARA_SKIN_OUTFIT *const outfit) { const int32_t mesh_idx = M_GetMeshIdx(mesh, outfit); if (mesh_idx != M_NO_MESH) { Lara_Mesh_Set(mesh, Object_GetMesh(mesh_idx)); } } static int32_t M_GetCombatFaceMeshIdx(const LARA_SKIN_OUTFIT *const outfit) { int32_t offset = outfit->combat_face_offset; if (offset == M_NO_MESH) { return M_NO_MESH; } if (g_Config.visuals.enable_braid) { switch (outfit->braid.mode) { case BRAID_MODE_TR1_HEAD_ONLY: case BRAID_MODE_TR1_FULL: case BRAID_MODE_TR1_MAULED: offset = Lara_Skin_GetExtraMeshOffset(EXTRA_MESH_TR1_BRAID_COMBAT_HEAD); break; default: break; } } const OBJECT *const extra_obj = Object_Get(O_LARA_SKIN_SWAP_EXTRA); return extra_obj->mesh_idx + offset; } static const LARA_SKIN_OUTFIT *M_GetExtraOutfit(const LARA_EXTRA_STATE state) { const LARA_SKIN_OUTFIT *const outfit = M_GetCurrentOutfit(); const LARA_SKIN_TYPE extra_type = outfit->extra_outfits[state]; if (extra_type == LARA_SKIN_TYPE_DEFAULT) { return nullptr; } if (!Lara_Skin_IsOutfitAvailable(extra_type)) { return nullptr; } return Lara_Skin_GetOutfit(extra_type); } static void M_SetEquipment( const LARA_MESH mesh, const LARA_SKIN_EQUIPMENT_TYPE type, const int32_t data, const int32_t offset) { LARA_SKIN_EQUIPMENT *const equipment = &m_Equipment[mesh]; equipment->type = type; equipment->data = data; switch (type) { case EQUIPMENT_TYPE_WEAPON: const OBJECT *const gun_swap_obj = Object_Get(O_LARA_SKIN_SWAP_GUNS); equipment->mesh = Object_GetMesh(gun_swap_obj->mesh_idx + offset); break; case EQUIPMENT_TYPE_EXTRA: const OBJECT *const extra_obj = Object_Get(O_LARA_SKIN_SWAP_EXTRA); equipment->mesh = Object_GetMesh(extra_obj->mesh_idx + offset); break; default: equipment->mesh = nullptr; break; } } static void M_SetGunEquipment( const LARA_MESH mesh, const LARA_GUN_TYPE gun_type, const LARA_SKIN_OUTFIT *const outfit) { const LARA_SKIN_MESH_MAP map = outfit->gun_map->mesh_offsets[gun_type]; int32_t offset = M_NO_MESH; switch (mesh) { case LM_THIGH_L: offset = map.thigh.left; break; case LM_THIGH_R: offset = map.thigh.right; break; case LM_HAND_L: offset = map.hand.left; break; case LM_HAND_R: offset = map.hand.right; break; case LM_TORSO: offset = map.torso; break; default: break; } if (offset == M_NO_MESH) { Lara_Skin_ClearEquipment(mesh); } else { M_SetEquipment(mesh, EQUIPMENT_TYPE_WEAPON, gun_type, offset); } } static void M_SetCombatFace(const bool enabled) { const LARA_SKIN_OUTFIT *const outfit = M_GetCurrentOutfit(); int32_t mesh_idx = M_NO_MESH; if (enabled) { mesh_idx = M_GetCombatFaceMeshIdx(outfit); } else { mesh_idx = M_GetMeshIdx(LM_HEAD, outfit); } if (mesh_idx != M_NO_MESH) { Lara_Mesh_Set(LM_HEAD, Object_GetMesh(mesh_idx)); m_UseCombatFace = enabled; } } static void M_UpdateSunglasses(void) { const SUNGLASSES_MODE mode = g_Config.visuals.sunglasses_mode; if (mode == SUNGLASSES_MODE_OFF || !M_GetCurrentOutfit()->supports_sunglasses) { Lara_Skin_ClearEquipment(LM_HEAD); return; } const LARA_SKIN_EXTRA_MESH mesh = mode == SUNGLASSES_MODE_OPAQUE ? EXTRA_MESH_GLASSES_OPAQUE : EXTRA_MESH_GLASSES_TRANSPARENT; Lara_Skin_SetExtraEquipment(LM_HEAD, mesh); } void Lara_Skin_Initialise(void) { const OBJECT *const extra_obj = Object_Get(O_LARA_SKIN_SWAP_EXTRA); ASSERT(extra_obj->loaded); const OBJECT *const gun_swap_obj = Object_Get(O_LARA_SKIN_SWAP_GUNS); ASSERT(gun_swap_obj->loaded); m_SkinType = M_NO_OUTFIT; m_HolsterType_L = LGT_UNARMED; m_HolsterType_R = LGT_UNARMED; m_UseCombatFace = false; m_HolstersVisible = true; for (int32_t i = 0; i < LM_NUMBER_OF; i++) { m_Equipment[i].visible = true; Lara_Skin_ClearEquipment(i); } const int32_t hair_segment_count = Lara_Hair_GetSegmentCount(); const int32_t outfit_count = Lara_Skin_GetOutfitCount(); for (int32_t i = 0; i < outfit_count; i++) { const LARA_SKIN_OUTFIT *const outfit = Lara_Skin_GetOutfit(i); if (!outfit->is_defined) { continue; } const OBJECT *const skin_obj = Object_Get(outfit->obj_id); ASSERT(skin_obj->loaded); ASSERT(skin_obj->mesh_count == LM_NUMBER_OF); if (!outfit->is_reflective) { continue; } for (int32_t j = 0; j < LM_NUMBER_OF; j++) { Object_SetMeshReflectiveEx(skin_obj->mesh_idx + j, true); const int32_t extra_idx = M_GetBraidDependentMeshIdx(j, outfit); if (extra_idx != M_NO_MESH) { Object_SetMeshReflectiveEx(extra_idx, true); } } for (int32_t j = 0; j < NUM_WEAPONS; j++) { const LARA_SKIN_MESH_MAP map = outfit->gun_map->mesh_offsets[j]; if (map.thigh.left != M_NO_MESH) { Object_SetMeshReflectiveEx( gun_swap_obj->mesh_idx + map.thigh.left, true); } if (map.thigh.right != M_NO_MESH) { Object_SetMeshReflectiveEx( gun_swap_obj->mesh_idx + map.thigh.right, true); } } if (!outfit->braid.enabled || outfit->braid.gold_offset == M_NO_MESH) { continue; } for (int32_t j = 0; j < hair_segment_count; j++) { Object_SetMeshReflectiveEx( extra_obj->mesh_idx + outfit->braid.gold_offset + j, true); } } Lara_Skin_ApplyOutfitFromConfig(); } void Lara_Skin_ApplyOutfitFromConfig(void) { if (!Game_IsLoaded()) { return; } LARA_SKIN_TYPE skin_type = M_GetCurrentLevelOutfitType(); if (g_Config.visuals.lara_outfit != nullptr) { const LARA_SKIN_TYPE config_type = Lara_Skin_FindOutfitByName(g_Config.visuals.lara_outfit); if (!Lara_Skin_IsOutfitAvailable(config_type)) { LOG_WARNING( "Invalid outfit '%s' from config.visuals.lara_outfit; falling " "back to default", g_Config.visuals.lara_outfit); skin_type = M_GetCurrentLevelOutfitType(); } else { skin_type = config_type; } } Lara_Skin_SetType(skin_type); } void Lara_Skin_CycleOutfit(const int32_t dir) { if (!Game_IsLoaded()) { return; } if (Config_IsOptionEnforced(&g_Config.visuals.lara_outfit)) { return; } // Update the config twice to guarantee the change is submitted in cases // where Lara_Skin_SetType has been called manually for non-permanent swaps // e.g. by Lua in cutscenes. const char *const current_name = Lara_Skin_GetOutfitName(m_SkinType); ASSERT(current_name != nullptr); if (g_Config.visuals.lara_outfit == nullptr || !String_Equivalent(g_Config.visuals.lara_outfit, current_name)) { M_SetConfigOutfit(current_name); Config_Update(); } const int32_t outfit_count = Lara_Skin_GetOutfitCount(); int32_t type = m_SkinType; do { type += dir; type += outfit_count; type %= outfit_count; } while (!Lara_Skin_IsOutfitAvailable(type) || !Lara_Skin_GetOutfit(type)->is_selectable); M_SetConfigOutfit(Lara_Skin_GetOutfitName(type)); Config_Update(); } LARA_SKIN_TYPE Lara_Skin_GetType(void) { return m_SkinType; } bool Lara_Skin_IsDefaultType(void) { if (g_Config.visuals.lara_outfit != nullptr) { const LARA_SKIN_TYPE config_type = Lara_Skin_FindOutfitByName(g_Config.visuals.lara_outfit); if (Lara_Skin_IsOutfitAvailable(config_type)) { return m_SkinType == config_type; } return m_SkinType == M_GetCurrentLevelOutfitType(); } return m_SkinType == M_GetCurrentLevelOutfitType(); } void Lara_Skin_SetType(const LARA_SKIN_TYPE skin_type) { LARA_SKIN_TYPE new_skin_type = skin_type; if (!Lara_Skin_IsOutfitAvailable(new_skin_type)) { new_skin_type = M_GetFallbackOutfitType(); } if (m_SkinType == new_skin_type) { return; } m_SkinType = new_skin_type; Lara_Skin_ApplyOutfit(); } void Lara_Skin_ApplyOutfit(void) { const LARA_SKIN_OUTFIT *const outfit = M_GetCurrentOutfit(); for (int32_t i = 0; i < LM_NUMBER_OF; i++) { M_ApplyMeshIfValid(i, outfit); } M_SetGunEquipment(LM_THIGH_L, m_HolsterType_L, outfit); M_SetGunEquipment(LM_THIGH_R, m_HolsterType_R, outfit); M_SetCombatFace(m_UseCombatFace); M_UpdateSunglasses(); } void Lara_Skin_SetCombatFace(const bool enabled) { if (m_UseCombatFace != enabled) { M_SetCombatFace(enabled); } } void Lara_Skin_SwapAllExtra(const LARA_EXTRA_STATE state) { const LARA_SKIN_OUTFIT *const outfit = M_GetExtraOutfit(state); if (outfit == nullptr) { return; } for (int32_t i = 0; i < LM_NUMBER_OF; i++) { M_ApplyMeshIfValid(i, outfit); } M_SetGunEquipment(LM_THIGH_L, m_HolsterType_L, outfit); M_SetGunEquipment(LM_THIGH_R, m_HolsterType_R, outfit); } void Lara_Skin_SwapSingleExtra( const LARA_MESH mesh, const LARA_EXTRA_STATE state) { const LARA_SKIN_OUTFIT *const outfit = M_GetExtraOutfit(state); if (outfit == nullptr) { return; } M_ApplyMeshIfValid(mesh, outfit); if (mesh == LM_THIGH_L) { M_SetGunEquipment(LM_THIGH_L, m_HolsterType_L, outfit); } else if (mesh == LM_THIGH_R) { M_SetGunEquipment(LM_THIGH_R, m_HolsterType_R, outfit); } } const ANIM_BONE *Lara_Skin_GetBoneBase(void) { const LARA_SKIN_OUTFIT *const outfit = M_GetCurrentOutfit(); const OBJECT *const skin_obj = Object_Get(outfit->obj_id); return Object_TryGetBone(skin_obj, 0); } bool Lara_Skin_IsBraidSupported(void) { return Lara_Skin_GetBraidMeshIdx() != M_NO_MESH; } XYZ_32 Lara_Skin_GetBraidOffset(void) { const LARA_SKIN_OUTFIT *const outfit = M_GetCurrentOutfit(); return outfit->braid.hair_pos; } int32_t Lara_Skin_GetBraidMeshIdx(void) { const int32_t offset = M_GetRelativeBraidOffset(); if (offset == M_NO_MESH) { return offset; } const OBJECT *const obj = Object_Get(O_LARA_SKIN_SWAP_EXTRA); return obj->mesh_idx + offset; } const ANIM_BONE *Lara_Skin_GetBraidBoneBase(void) { const int32_t offset = M_GetRelativeBraidOffset(); if (offset == M_NO_MESH) { return nullptr; } const OBJECT *const obj = Object_Get(O_LARA_SKIN_SWAP_EXTRA); return Object_TryGetBone(obj, offset); } bool Lara_Skin_AreHolstersVisible(void) { return m_HolstersVisible; } void Lara_Skin_SetHolstersVisible(const bool visible) { m_HolstersVisible = visible; m_Equipment[LM_THIGH_L].visible = visible; m_Equipment[LM_THIGH_R].visible = visible; const LARA_SKIN_OUTFIT *const outfit = M_GetCurrentOutfit(); M_ApplyMeshIfValid(LM_THIGH_L, outfit); M_ApplyMeshIfValid(LM_THIGH_R, outfit); } void Lara_Skin_ClearEquipment(const LARA_MESH mesh) { M_SetEquipment(mesh, EQUIPMENT_TYPE_NONE, M_NO_MESH, M_NO_MESH); } void Lara_Skin_SetExtraEquipment( const LARA_MESH mesh, const LARA_SKIN_EXTRA_MESH extra_mesh) { const int32_t offset = Lara_Skin_GetExtraMeshOffset(extra_mesh); M_SetEquipment(mesh, EQUIPMENT_TYPE_EXTRA, extra_mesh, offset); } void Lara_Skin_SetGunEquipment( const LARA_MESH mesh, const LARA_GUN_TYPE gun_type) { if (gun_type < 0 || gun_type >= NUM_WEAPONS) { return; } M_SetGunEquipment(mesh, gun_type, M_GetCurrentOutfit()); if (mesh == LM_THIGH_L) { m_HolsterType_L = gun_type; } else if (mesh == LM_THIGH_R) { m_HolsterType_R = gun_type; } if ((mesh == LM_THIGH_L || mesh == LM_THIGH_R) && !Gun_IsRifleType(gun_type)) { Lara_Skin_SetHolstersVisible(true); } } const LARA_SKIN_EQUIPMENT *Lara_Skin_GetEquipment(const LARA_MESH mesh) { return &m_Equipment[mesh]; } SAMPLE_ID Lara_Skin_GetAnimSFX(const SAMPLE_ID sample_id) { if (g_TRVersion == 2 && !g_Config.audio.enable_ps1_sfx) { return sample_id; } const LARA_SKIN_OUTFIT *const outfit = M_GetCurrentOutfit(); if (outfit->footstep_sample_id == SFX_TRX_INVALID || Sound_FromGameID(sample_id) != SFX_LARA_FOOTSTEP) { return sample_id; } return Sound_ToGameID(outfit->footstep_sample_id); } // TODO: remove in TRX 1.5. void Lara_Skin_ExtractLegacyEquipment(const OBJECT_MESH **const meshes) { #define L_DETERMINE_EQUIPMENT(mesh) \ do { \ if (meshes[mesh] == nullptr) { \ break; \ } \ for (int32_t i = 0; i < NUM_WEAPONS; i++) { \ if (i == LGT_SKIDOO) { \ continue; \ } \ const OBJECT *const obj = Object_Get(Gun_GetWeaponAnim(i)); \ if (obj->loaded \ && Object_GetMesh(obj->mesh_idx + mesh) == meshes[mesh]) { \ Lara_Skin_SetGunEquipment(mesh, i); \ break; \ } \ } \ } while (0) L_DETERMINE_EQUIPMENT(LM_THIGH_L); L_DETERMINE_EQUIPMENT(LM_THIGH_R); L_DETERMINE_EQUIPMENT(LM_HAND_L); L_DETERMINE_EQUIPMENT(LM_HAND_R); #undef L_DETERMINE_EQUIPMENT } ================================================ FILE: src/trx/game/lara/skin/common.h ================================================ #pragma once #include #include #include void Lara_Skin_Initialise(void); void Lara_Skin_ApplyOutfitFromConfig(void); void Lara_Skin_CycleOutfit(int32_t dir); LARA_SKIN_TYPE Lara_Skin_GetType(void); bool Lara_Skin_IsDefaultType(void); void Lara_Skin_SetType(LARA_SKIN_TYPE skin_type); void Lara_Skin_ApplyOutfit(void); void Lara_Skin_SetCombatFace(bool enabled); void Lara_Skin_SwapAllExtra(LARA_EXTRA_STATE state); void Lara_Skin_SwapSingleExtra(LARA_MESH mesh, LARA_EXTRA_STATE state); const ANIM_BONE *Lara_Skin_GetBoneBase(void); bool Lara_Skin_IsBraidSupported(void); XYZ_32 Lara_Skin_GetBraidOffset(void); int32_t Lara_Skin_GetBraidMeshIdx(void); const ANIM_BONE *Lara_Skin_GetBraidBoneBase(void); bool Lara_Skin_AreHolstersVisible(void); void Lara_Skin_SetHolstersVisible(bool visible); void Lara_Skin_ClearEquipment(LARA_MESH mesh); void Lara_Skin_SetGunEquipment(LARA_MESH mesh, LARA_GUN_TYPE gun_type); void Lara_Skin_SetExtraEquipment( LARA_MESH mesh, LARA_SKIN_EXTRA_MESH extra_mesh); const LARA_SKIN_EQUIPMENT *Lara_Skin_GetEquipment(LARA_MESH mesh); SAMPLE_ID Lara_Skin_GetAnimSFX(SAMPLE_ID sample_id); void Lara_Skin_ExtractLegacyEquipment(const OBJECT_MESH **meshes); ================================================ FILE: src/trx/game/lara/skin/enum.h ================================================ #pragma once #include typedef int32_t LARA_SKIN_TYPE; #define LARA_SKIN_TYPE_DEFAULT (-1) typedef enum { // clang-format off BRAID_MODE_NONE, // No body adjustments needed BRAID_MODE_TR1_HEAD_ONLY, // Head replacement only (no backpack present) BRAID_MODE_TR1_FULL, // Head and torso replacement BRAID_MODE_TR1_MAULED, // Head and mauled torso replacement BRAID_MODE_TR1_GOLD, // Gold head and torso replacement NUM_BRAID_MODES, // clang-format on } LARA_SKIN_BRAID_MODE; typedef enum { EXTRA_MESH_TR1_BRAID_DEFAULT_HEAD, EXTRA_MESH_TR1_BRAID_COMBAT_HEAD, EXTRA_MESH_TR1_BRAID_DEFAULT_TORSO, EXTRA_MESH_TR1_BRAID_MAULED_TORSO, EXTRA_MESH_TR1_BRAID_GOLD_HEAD, EXTRA_MESH_TR1_BRAID_GOLD_TORSO, EXTRA_MESH_DAGGER_HAND, EXTRA_MESH_DAGGER_HIPS, EXTRA_MESH_OAR, EXTRA_MESH_SPANNER, EXTRA_MESH_DRINK_CAN, EXTRA_MESH_GLASSES_OPAQUE, EXTRA_MESH_GLASSES_TRANSPARENT, NUM_EXTRA_MESHES, } LARA_SKIN_EXTRA_MESH; typedef enum { // clang-format off EQUIPMENT_TYPE_NONE = 0, EQUIPMENT_TYPE_WEAPON = 1, EQUIPMENT_TYPE_EXTRA = 2, // clang-format on } LARA_SKIN_EQUIPMENT_TYPE; ================================================ FILE: src/trx/game/lara/skin/storage.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef struct { char *name; char *name_gs; LARA_SKIN_OUTFIT outfit; } M_OUTFIT_ENTRY; typedef struct M_OUTFIT_LOOKUP { char *name; int32_t index; UT_hash_handle hh; } M_OUTFIT_LOOKUP; static VECTOR *m_GunMaps = nullptr; static M_OUTFIT_ENTRY *m_Outfits = nullptr; static int32_t m_OutfitCount = 0; static M_OUTFIT_LOOKUP *m_OutfitLookup = nullptr; static int32_t m_ExtraMeshOffsets[NUM_EXTRA_MESHES] = {}; static void M_ExitWithJSONError( const char *const source_path, const JSON_READ_IO *const io) { JSONFile_ExitWithReadIOError( io, String_FormatStatic("%s: outfits parse error", source_path)); } static void M_SeedDynamicEnumValues(void) { const CONFIG_OPTION *const option = Config_GetOption(&g_Config.visuals.lara_outfit); Config_DynamicEnum_ResetValues(option); Config_DynamicEnum_AddValue( option, nullptr, GS_ID("dynamic/enums/lara_outfit/default")); for (int32_t i = 0; i < m_OutfitCount; i++) { if (!m_Outfits[i].outfit.is_selectable) { continue; } Config_DynamicEnum_AddValue( option, m_Outfits[i].name, m_Outfits[i].name_gs); } } static void M_ResetOutfits(void) { M_OUTFIT_LOOKUP *entry = nullptr; M_OUTFIT_LOOKUP *tmp = nullptr; HASH_ITER(hh, m_OutfitLookup, entry, tmp) { HASH_DEL(m_OutfitLookup, entry); Memory_FreePointer(&entry); } if (m_Outfits != nullptr) { for (int32_t i = 0; i < m_OutfitCount; i++) { Memory_FreePointer(&m_Outfits[i].name); Memory_FreePointer(&m_Outfits[i].name_gs); } Memory_FreePointer(&m_Outfits); } m_OutfitCount = 0; m_OutfitLookup = nullptr; } LARA_SKIN_TYPE Lara_Skin_FindOutfitByName(const char *const name) { if (name == nullptr) { return LARA_SKIN_TYPE_DEFAULT; } M_OUTFIT_LOOKUP *entry = nullptr; HASH_FIND_STR(m_OutfitLookup, name, entry); if (entry == nullptr) { return -1; } return entry->index; } LARA_SKIN_TYPE Lara_Skin_GetDefaultType(void) { return m_OutfitCount > 0 ? 0 : LARA_SKIN_TYPE_DEFAULT; } static bool M_ReadGunMaps(JSON_READ_IO *const io) { JSON_MUST(JSON_PUSH(io, "gun_maps")); const int32_t map_count = JSON_ARRAY_LEN(io); if (map_count < 0) { JSON_FAIL(); } for (int32_t i = 0; i < map_count; ++i) { JSON_MUST(JSON_PUSH_INDEX(io, i)); if (JSON_ReadIO_GetCurrentObject(io) == nullptr) { JSON_ReadIO_SetError(io, "gun map %d must be an object", i); JSON_FAIL(); } LARA_SKIN_GUN_MAP map = {}; for (int32_t j = 0; j < NUM_WEAPONS; j++) { LARA_SKIN_MESH_MAP *const mesh_map = &map.mesh_offsets[j]; memset(mesh_map, -1, sizeof(LARA_SKIN_MESH_MAP)); const char *const gun_name = EnumMap_ToString(ENUM_MAP_NAME(LARA_GUN_TYPE), j); if (!JSON_OPTIONAL(JSON_PUSH(io, gun_name))) { continue; } JSON_OPTIONAL(JSON_READ(io, "hand_r", &mesh_map->hand.right)); JSON_OPTIONAL(JSON_READ(io, "hand_l", &mesh_map->hand.left)); JSON_OPTIONAL(JSON_READ(io, "thigh_r", &mesh_map->thigh.right)); JSON_OPTIONAL(JSON_READ(io, "thigh_l", &mesh_map->thigh.left)); JSON_OPTIONAL(JSON_READ(io, "torso", &mesh_map->torso)); JSON_MUST(JSON_POP(io)); } Vector_Add(m_GunMaps, &map); JSON_MUST(JSON_POP(io)); } JSON_MUST(JSON_POP(io)); JSON_FINISH(); } static bool M_ReadExtraMeshes(JSON_READ_IO *const io) { if (!JSON_OPTIONAL(JSON_PUSH(io, "extra_meshes"))) { return false; } JSON_OBJECT *const extra_obj = JSON_ReadIO_GetCurrentObject(io); if (extra_obj == nullptr) { JSON_ReadIO_SetError(io, "'extra_meshes' must be an object"); JSON_MUST(JSON_POP(io)); JSON_FAIL(); } for (JSON_OBJECT_ELEMENT *elem = extra_obj->start; elem != nullptr; elem = elem->next) { const char *const name = elem->name->string; const int32_t type = ENUM_MAP_GET(LARA_SKIN_EXTRA_MESH, name, -1); if (type < 0 || type >= NUM_EXTRA_MESHES) { JSON_ReadIO_SetError(io, "unknown extra mesh type '%s'", name); JSON_MUST(JSON_POP(io)); JSON_FAIL(); } JSON_MUST(JSON_READ(io, name, &m_ExtraMeshOffsets[type])); } JSON_MUST(JSON_POP(io)); JSON_FINISH(); } static bool M_LoadBraid(JSON_READ_IO *const io, LARA_SKIN_OUTFIT *const outfit) { if (JSON_OPTIONAL(JSON_PUSH(io, "braid"))) { const char *braid_mode_name = nullptr; if (JSON_OPTIONAL(JSON_READ(io, "mode", &braid_mode_name))) { const int32_t mode = ENUM_MAP_GET(LARA_SKIN_BRAID_MODE, braid_mode_name, -1); if (mode < 0 || mode >= NUM_BRAID_MODES) { JSON_ReadIO_SetError( io, "unknown braid mode '%s'", braid_mode_name); JSON_MUST(JSON_POP(io)); JSON_FAIL(); } outfit->braid.mode = mode; } JSON_READ_D(io, "mesh_offset", &outfit->braid.mesh_offset, 0); JSON_READ_D(io, "gold_offset", &outfit->braid.gold_offset, 0); JSON_READ_D(io, "hair_pos", &outfit->braid.hair_pos, (XYZ_32) {}); outfit->braid.enabled = true; JSON_MUST(JSON_POP(io)); } else { outfit->braid.enabled = false; } JSON_FINISH(); } static bool M_LoadGunMap(JSON_READ_IO *const io, LARA_SKIN_OUTFIT *const outfit) { int32_t map_idx = -1; JSON_READ_D(io, "gun_map", &map_idx, -1); if (map_idx < 0 || map_idx >= m_GunMaps->count) { JSON_ReadIO_SetError(io, "invalid gun map '%d'", map_idx); JSON_FAIL(); } outfit->gun_map = (LARA_SKIN_GUN_MAP *)Vector_Get(m_GunMaps, map_idx); JSON_FINISH(); } static bool M_LoadSFX(JSON_READ_IO *const io, LARA_SKIN_OUTFIT *const outfit) { const char *feet_sample_name = nullptr; if (JSON_OPTIONAL(JSON_READ(io, "footstep_sample_id", &feet_sample_name))) { CATALOG_ID feet_sample_id; if (!Catalog_NameToEnum( CATALOG_SAMPLES, feet_sample_name, &feet_sample_id)) { JSON_ReadIO_SetError( io, "unknown sample id '%s'", feet_sample_name); JSON_FAIL(); } outfit->footstep_sample_id = feet_sample_id; } else { outfit->footstep_sample_id = SFX_TRX_INVALID; } JSON_FINISH(); } static bool M_LoadNoHolsters( JSON_READ_IO *const io, LARA_SKIN_OUTFIT *const outfit) { if (JSON_OPTIONAL(JSON_PUSH(io, "no_holster_offsets"))) { JSON_READ_D(io, "thigh_l", &outfit->no_holster_offsets.left, -1); JSON_READ_D(io, "thigh_r", &outfit->no_holster_offsets.right, -1); JSON_MUST(JSON_POP(io)); } else { outfit->no_holster_offsets.left = -1; outfit->no_holster_offsets.right = -1; } JSON_FINISH(); } static bool M_LoadExtras(JSON_READ_IO *const io, LARA_SKIN_OUTFIT *const outfit) { for (int32_t j = 0; j < LS_EXTRA_NUMBER_OF; j++) { outfit->extra_outfits[j] = LARA_SKIN_TYPE_DEFAULT; } if (JSON_OPTIONAL(JSON_PUSH(io, "extra_outfits"))) { JSON_OBJECT *const extra_obj = JSON_ReadIO_GetCurrentObject(io); if (extra_obj == nullptr) { JSON_ReadIO_SetError(io, "'extra_outfits' must be an object"); JSON_MUST(JSON_POP(io)); JSON_FAIL(); } for (JSON_OBJECT_ELEMENT *elem = extra_obj->start; elem != nullptr; elem = elem->next) { const char *const state_name = elem->name->string; const int32_t state = ENUM_MAP_GET(LARA_EXTRA_STATE, state_name, -1); if (state < 0 || state >= LS_EXTRA_NUMBER_OF) { JSON_ReadIO_SetError( io, "unknown Lara extra state '%s'", state_name); JSON_MUST(JSON_POP(io)); JSON_FAIL(); } const char *outfit_name = nullptr; JSON_MUST(JSON_READ(io, state_name, &outfit_name)); const LARA_SKIN_TYPE type = Lara_Skin_FindOutfitByName(outfit_name); if (type < 0 || type >= m_OutfitCount) { JSON_ReadIO_SetError(io, "unknown outfit '%s'", outfit_name); JSON_MUST(JSON_POP(io)); JSON_FAIL(); } outfit->extra_outfits[state] = type; } JSON_MUST(JSON_POP(io)); } JSON_FINISH(); } static bool M_LoadOutfit(JSON_READ_IO *const io, LARA_SKIN_OUTFIT *const outfit) { const char *mesh_obj_name = nullptr; JSON_MUST(JSON_READ(io, "mesh_object", &mesh_obj_name)); CATALOG_ID mesh_object_id; if (!Catalog_NameToEnum(CATALOG_OBJECTS, mesh_obj_name, &mesh_object_id)) { JSON_ReadIO_SetError( io, "unknown outfit object_id '%s'", mesh_obj_name); JSON_FAIL(); } outfit->obj_id = mesh_object_id; JSON_READ_D(io, "is_reflective", &outfit->is_reflective, false); JSON_READ_D(io, "is_selectable", &outfit->is_selectable, true); JSON_READ_D(io, "combat_face_offset", &outfit->combat_face_offset, -1); JSON_READ_D(io, "supports_sunglasses", &outfit->supports_sunglasses, true); JSON_MUST(M_LoadBraid(io, outfit)); JSON_MUST(M_LoadGunMap(io, outfit)); JSON_MUST(M_LoadSFX(io, outfit)); JSON_MUST(M_LoadNoHolsters(io, outfit)); JSON_MUST(M_LoadExtras(io, outfit)); outfit->is_defined = true; JSON_FINISH(); } static bool M_ReadOutfits(JSON_READ_IO *const io) { JSON_MUST(JSON_PUSH(io, "outfits")); JSON_OBJECT *const outfits_map = JSON_ReadIO_GetCurrentObject(io); if (outfits_map == nullptr) { JSON_ReadIO_SetError(io, "'outfits' must be an object"); JSON_FAIL(); } size_t outfit_count = 0; for (JSON_OBJECT_ELEMENT *elem = outfits_map->start; elem != nullptr; elem = elem->next) { outfit_count++; } if (outfit_count == 0) { JSON_ReadIO_SetError(io, "missing outfits in configuration"); JSON_MUST(JSON_POP(io)); JSON_FAIL(); } m_Outfits = Memory_Alloc(sizeof(*m_Outfits) * outfit_count); m_OutfitCount = (int32_t)outfit_count; size_t idx = 0; for (JSON_OBJECT_ELEMENT *elem = outfits_map->start; elem != nullptr; elem = elem->next) { const char *const name = elem->name->string; JSON_MUST(JSON_PUSH(io, name)); M_OUTFIT_ENTRY *const outfit = &m_Outfits[idx]; outfit->name = Memory_DupStr(name); const char *name_gs = nullptr; if (!JSON_READ(io, "name_gs", &name_gs)) { JSON_MUST(JSON_POP(io)); JSON_FAIL(); } outfit->name_gs = Memory_DupStr(name_gs); M_OUTFIT_LOOKUP *existing = nullptr; HASH_FIND_STR(m_OutfitLookup, outfit->name, existing); if (existing != nullptr) { JSON_ReadIO_SetError(io, "duplicate outfit '%s'", name); JSON_MUST(JSON_POP(io)); JSON_FAIL(); } M_OUTFIT_LOOKUP *const lookup = Memory_Alloc(sizeof(*lookup)); lookup->name = outfit->name; lookup->index = (int32_t)idx; HASH_ADD_KEYPTR( hh, m_OutfitLookup, lookup->name, strlen(lookup->name), lookup); JSON_MUST(JSON_POP(io)); idx++; } idx = 0; for (JSON_OBJECT_ELEMENT *elem = outfits_map->start; elem != nullptr; elem = elem->next) { JSON_MUST(JSON_PUSH(io, elem->name->string)); if (!M_LoadOutfit(io, &m_Outfits[idx].outfit)) { JSON_MUST(JSON_POP(io)); JSON_FAIL(); } JSON_MUST(JSON_POP(io)); idx++; } JSON_MUST(JSON_POP(io)); JSON_FINISH(); } static bool M_LoadFile(JSON_READ_IO *const io) { JSON_MUST(M_ReadGunMaps(io)); JSON_MUST(M_ReadExtraMeshes(io)); JSON_MUST(M_ReadOutfits(io)); JSON_FINISH(); } void Lara_Skin_LoadFromFile(const char *const path) { char *source_path = Memory_DupStr(path); JSON_READ_IO *io = nullptr; if (m_GunMaps != nullptr) { Vector_Free(m_GunMaps); m_GunMaps = nullptr; } m_GunMaps = Vector_Create(sizeof(LARA_SKIN_GUN_MAP)); M_ResetOutfits(); M_SeedDynamicEnumValues(); memset(m_ExtraMeshOffsets, 0, sizeof(m_ExtraMeshOffsets)); LOG_INFO("Reading outfit definitions from %s", source_path); JSON_VALUE *const doc = JSONFile_ReadEx(source_path, true); if (doc == nullptr) { Shell_ExitSystemFmt("invalid outfits file: %s", source_path); goto cleanup; } io = JSON_ReadIO_Create(doc, 0, source_path); if (!M_LoadFile(io)) { const char *const error = JSON_ReadIO_GetError(io); if (error != nullptr && error[0] != '\0') { M_ExitWithJSONError(source_path, io); } } M_SeedDynamicEnumValues(); cleanup: if (io != nullptr) { JSON_ReadIO_Destroy(io); } JSON_ValueFree(doc); Memory_FreePointer(&source_path); } void Lara_Skin_Shutdown(void) { if (m_GunMaps != nullptr) { Vector_Free(m_GunMaps); m_GunMaps = nullptr; } M_ResetOutfits(); } int32_t Lara_Skin_GetOutfitCount(void) { return m_OutfitCount; } bool Lara_Skin_IsOutfitAvailable(const LARA_SKIN_TYPE skin_type) { return skin_type >= 0 && skin_type < m_OutfitCount && m_Outfits[skin_type].outfit.is_defined; } const LARA_SKIN_OUTFIT *Lara_Skin_GetOutfit(const LARA_SKIN_TYPE skin_type) { ASSERT(skin_type >= 0 && skin_type < m_OutfitCount); return &m_Outfits[skin_type].outfit; } const char *Lara_Skin_GetOutfitName(const LARA_SKIN_TYPE skin_type) { if (skin_type < 0 || skin_type >= m_OutfitCount) { return nullptr; } return m_Outfits[skin_type].name; } int32_t Lara_Skin_GetExtraMeshOffset(const LARA_SKIN_EXTRA_MESH mesh) { ASSERT(mesh >= 0 && mesh < NUM_EXTRA_MESHES); return m_ExtraMeshOffsets[mesh]; } ================================================ FILE: src/trx/game/lara/skin/storage.h ================================================ #pragma once #include void Lara_Skin_LoadFromFile(const char *path); void Lara_Skin_Shutdown(void); int32_t Lara_Skin_GetOutfitCount(void); bool Lara_Skin_IsOutfitAvailable(LARA_SKIN_TYPE skin_type); const LARA_SKIN_OUTFIT *Lara_Skin_GetOutfit(LARA_SKIN_TYPE skin_type); const char *Lara_Skin_GetOutfitName(LARA_SKIN_TYPE skin_type); LARA_SKIN_TYPE Lara_Skin_FindOutfitByName(const char *name); LARA_SKIN_TYPE Lara_Skin_GetDefaultType(void); int32_t Lara_Skin_GetExtraMeshOffset(LARA_SKIN_EXTRA_MESH mesh); ================================================ FILE: src/trx/game/lara/skin/types.h ================================================ #pragma once #include #include #include typedef struct { int32_t right; int32_t left; } MESH_PAIR; typedef struct { MESH_PAIR hand; MESH_PAIR thigh; int32_t torso; } LARA_SKIN_MESH_MAP; typedef struct { LARA_SKIN_MESH_MAP mesh_offsets[NUM_WEAPONS]; } LARA_SKIN_GUN_MAP; typedef struct { LARA_SKIN_BRAID_MODE mode; bool enabled; int32_t mesh_offset; int32_t gold_offset; XYZ_32 hair_pos; } LARA_SKIN_BRAID; typedef struct { bool is_defined; OBJECT_ID obj_id; LARA_SKIN_GUN_MAP *gun_map; LARA_SKIN_BRAID braid; bool is_selectable; bool is_reflective; bool supports_sunglasses; SAMPLE_TRX_ID footstep_sample_id; int32_t combat_face_offset; MESH_PAIR no_holster_offsets; int32_t extra_outfits[LS_EXTRA_NUMBER_OF]; } LARA_SKIN_OUTFIT; typedef struct { LARA_SKIN_EQUIPMENT_TYPE type; int32_t data; bool visible; const OBJECT_MESH *mesh; } LARA_SKIN_EQUIPMENT; ================================================ FILE: src/trx/game/lara/skin.h ================================================ #pragma once #include #include ================================================ FILE: src/trx/game/lara/state/climb.c ================================================ #include #include #include #include #include #include // clang-format off #define M_CAM_HANG_ANGLE 0 #define M_CAM_HANG_ELEVATION (-60 * DEG_1) // = -10920 #define M_CAM_CLIMB_LEFT_ANGLE (-30 * DEG_1) // = -5460 #define M_CAM_CLIMB_LEFT_ELEVATION (-15 * DEG_1) // = -2730 #define M_CAM_CLIMB_RIGHT_ANGLE (-M_CAM_CLIMB_LEFT_ANGLE) // = 5460 #define M_CAM_CLIMB_RIGHT_ELEVATION M_CAM_CLIMB_LEFT_ELEVATION // = -2730 #define M_CAM_CLIMB_STANCE_ELEVATION (-20 * DEG_1) // = -3640 #define M_CAM_CLIMBING_ELEVATION (30 * DEG_1) // = 5460 #define M_CAM_CLIMB_END_ELEVATION (-45 * DEG_1) // = -8190 #define M_CAM_CLIMB_DOWN_ELEVATION M_CAM_CLIMB_END_ELEVATION // = -8190 // clang-format on static void M_Hang(ITEM *const item, COLL_INFO *const coll) { if (item->hit_points <= 0) { item->goal_anim_state = LS(LS_STOP); return; } if (g_Config.gameplay.look_mode != LOOK_MODE_RESTRICTED && g_Input.look) { Lara_Look_UpDown(); } coll->enable_hit = 0; coll->enable_baddie_push = 0; g_Camera.target_angle = M_CAM_HANG_ANGLE; g_Camera.target_elevation = M_CAM_HANG_ELEVATION; if (g_Input.left || g_Input.step_left) { item->goal_anim_state = LS(LS_SHIMMY_LEFT); } else if (g_Input.right || g_Input.step_right) { item->goal_anim_state = LS(LS_SHIMMY_RIGHT); } } static void M_Shimmy(ITEM *const item, COLL_INFO *const coll) { coll->enable_hit = 0; coll->enable_baddie_push = 0; g_Camera.target_angle = M_CAM_HANG_ANGLE; g_Camera.target_elevation = M_CAM_HANG_ELEVATION; const bool stop = item->current_anim_state == LS(LS_SHIMMY_LEFT) ? (!g_Input.left && !g_Input.step_left) : (!g_Input.right && !g_Input.step_right); if (stop) { item->goal_anim_state = LS(LS_HANG); } } static void M_StanceLadder(ITEM *const item, COLL_INFO *const coll) { coll->enable_hit = 0; coll->enable_baddie_push = 0; g_Camera.target_elevation = M_CAM_CLIMB_STANCE_ELEVATION; if (g_Input.look) { Lara_Look_UpDown(); } if (g_Input.left || g_Input.step_left) { item->goal_anim_state = LS(LS_CLIMB_LEFT); } else if (g_Input.right || g_Input.step_right) { item->goal_anim_state = LS(LS_CLIMB_RIGHT); } else if (g_Input.jump) { item->goal_anim_state = LS(LS_JUMP_BACK); LARA_INFO *const lara = Lara_GetLaraInfo(); lara->gun_status = LGS_ARMLESS; lara->move_angle = item->rot.y + DEG_180; } } static void M_SideLadder(ITEM *const item, COLL_INFO *const coll) { coll->enable_hit = 0; coll->enable_baddie_push = 0; if (item->current_anim_state == LS(LS_CLIMB_LEFT)) { g_Camera.target_angle = M_CAM_CLIMB_LEFT_ANGLE; g_Camera.target_elevation = M_CAM_CLIMB_LEFT_ELEVATION; if (!g_Input.left && !g_Input.step_left) { item->goal_anim_state = LS(LS_CLIMB_STANCE); } } else { g_Camera.target_angle = M_CAM_CLIMB_RIGHT_ANGLE; g_Camera.target_elevation = M_CAM_CLIMB_RIGHT_ELEVATION; if (!g_Input.right && !g_Input.step_right) { item->goal_anim_state = LS(LS_CLIMB_STANCE); } } } static void M_UpDownLadder(ITEM *const item, COLL_INFO *const coll) { coll->enable_hit = 0; coll->enable_baddie_push = 0; switch (LS_U(item->current_anim_state)) { case LS_CLIMBING: g_Camera.target_elevation = M_CAM_CLIMBING_ELEVATION; break; case LS_CLIMB_DOWN: g_Camera.target_elevation = M_CAM_CLIMB_DOWN_ELEVATION; break; case LS_CLIMB_END: g_Camera.flags = CF_FOLLOW_CENTRE; g_Camera.target_angle = M_CAM_CLIMB_END_ELEVATION; break; default: break; } } // clang-format off REGISTER_LARA_STATE(LS_HANG, M_Hang) REGISTER_LARA_STATE(LS_SHIMMY_LEFT, M_Shimmy) REGISTER_LARA_STATE(LS_SHIMMY_RIGHT, M_Shimmy) REGISTER_LARA_STATE(LS_CLIMB_STANCE, M_StanceLadder) REGISTER_LARA_STATE(LS_CLIMB_LEFT, M_SideLadder) REGISTER_LARA_STATE(LS_CLIMB_RIGHT, M_SideLadder) REGISTER_LARA_STATE(LS_CLIMBING, M_UpDownLadder) REGISTER_LARA_STATE(LS_CLIMB_DOWN, M_UpDownLadder) REGISTER_LARA_STATE(LS_CLIMB_END, M_UpDownLadder) // clang-format on ================================================ FILE: src/trx/game/lara/state/crouch.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include // clang-format off #define M_CAM_CRAWL_ELEVATION (-DEG_1 * 23) // = -4186 #define M_CRAWL_TURN_RATE ((DEG_1 * 2) + 45) // = 409 #define M_CRAWL_TURN_MAX (DEG_1 * 3) // = 546 #define M_CRAWL_TURN_SLOW (DEG_1 * 3 / 2) // = 273 #define M_JUMP_DIST (STEP_L * 3) // = 768 #define M_JUMP_HEIGHT (STEP_L * 2) // = 512 #define M_JUMP_START_SHIFT (STEP_L * 3 / 8) // = 96 #define M_JUMP_TARGET_SHIFT (STEP_L * 5 / 8) // = 160 // clang-format on static bool M_CanEnterCrawlFromCrouch(const ITEM *const item) { return item->current_anim_state == LS(LS_CROUCH_IDLE) && Item_GetRelativeFrame(item) > 1; } static bool M_CanCrouchRoll(const ITEM *const item, const LARA_INFO *const lara) { if (!g_Config.gameplay.enable_crouch_roll || g_Input.draw || !g_Input.sprint) { return false; } if (item->current_anim_state == LS(LS_CROUCH_IDLE) && lara->gun_status != LGS_ARMLESS) { return false; } if (Room_Get(item->room_num)->flags.swamp) { return false; } if (!(g_Config.gameplay.enable_toggle_crouch ? lara->crouching : g_Input.crouch) && (!lara->keep_crouched || lara->water_status == LWS_WADE)) { return false; } const int32_t height_far = Lara_FloorFront(item, item->rot.y, STEP_L * 2); const int32_t height_near = Lara_FloorFront(item, item->rot.y, STEP_L); if (height_far >= STEPUP_HEIGHT || height_near < -STEPUP_HEIGHT) { return false; } if (Item_TestAnimEqual(item, LA(LA_CRAWL_IDLE))) { if (!g_Config.gameplay.enable_responsive_crawl) { return false; } } else if ( !Item_TestAnimEqual(item, LA(LA_CROUCH_IDLE)) && !Item_TestAnimEqual(item, LA(LA_STAND_TO_CROUCH_END))) { return false; } if (lara->gun_type == LGT_FLARE && (lara->flare.age <= 0 || lara->flare.age >= Flare_GetMaxAge())) { return false; } return true; } static bool M_CanJumpDown(const ITEM *const item, const LARA_INFO *const lara) { if (!g_Config.gameplay.enable_crawl_jump || !g_Input.jump) { return false; } if (item->current_anim_state == LS(LS_CROUCH_IDLE) && lara->gun_status != LGS_ARMLESS) { return false; } if (!Item_TestAnimEqual(item, LA(LA_CRAWL_IDLE)) && !Item_TestAnimEqual(item, LA(LA_CROUCH_TO_CRAWL_END)) && !Item_TestAnimEqual(item, LA(LA_CRAWL_FORWARD_TO_IDLE_END_RIGHT)) && !Item_TestAnimEqual(item, LA(LA_CRAWL_FORWARD_TO_IDLE_END_LEFT)) && !Item_TestAnimEqual(item, LA(LA_CROUCH_IDLE))) { return false; } const int32_t floor_front = Lara_FloorFront(item, item->rot.y, M_JUMP_DIST); const int32_t ceiling_front = Lara_CeilingFront(item, item->rot.y, M_JUMP_DIST, M_JUMP_HEIGHT); if (floor_front < M_JUMP_HEIGHT || ceiling_front == NO_HEIGHT || ceiling_front > 0) { return false; } const GAME_VECTOR start = { .x = item->pos.x, .y = item->pos.y - M_JUMP_START_SHIFT, .z = item->pos.z, .room_num = item->room_num, }; GAME_VECTOR target = { .pos = XYZ_32_OffsetYaw( start.pos, item->rot.y, M_JUMP_DIST) }; target.y += M_JUMP_TARGET_SHIFT; return LOS_Check(&start, &target, false); } static void M_CrouchIdle(ITEM *const item, COLL_INFO *const coll) { coll->enable_hit = 0; coll->enable_baddie_push = 1; LARA_INFO *const lara = Lara_GetLaraInfo(); const bool crouch_active = g_Config.gameplay.enable_toggle_crouch ? lara->crouching || lara->keep_crouched : g_Input.crouch || lara->keep_crouched; lara->sprinting = false; lara->is_crouched = true; if (item->hit_points <= 0) { item->goal_anim_state = LS(LS_CRAWL_IDLE); return; } if (Lara_Col_TestSlide(item, coll)) { return; } if (g_Input.look) { Lara_Look_UpDown(); } if ((g_Input.forward || g_Input.back) && crouch_active && lara->gun_status == LGS_ARMLESS && M_CanEnterCrawlFromCrouch(item)) { lara->torso_rot.x = 0; lara->torso_rot.y = 0; item->goal_anim_state = LS(LS_CRAWL_IDLE); return; } if (M_CanCrouchRoll(item, lara)) { lara->torso_rot.x = 0; lara->torso_rot.y = 0; Item_SwitchToAnim(item, LA(LA_CROUCH_ROLL_FORWARD_START), 0); item->current_anim_state = LS(LS_CROUCH_ROLL); item->goal_anim_state = LS(LS_CROUCH_ROLL); } else if (crouch_active && M_CanJumpDown(item, lara)) { Lara_AnimateUntil(item, LS(LS_CRAWL_IDLE)); item->goal_anim_state = LS(LS_CRAWL_JUMP_DOWN); } } static void M_CrouchRoll(ITEM *const item, COLL_INFO *const coll) { g_Camera.target_elevation = -3640; item->goal_anim_state = LS(LS_CROUCH_IDLE); LARA_INFO *const lara = Lara_GetLaraInfo(); lara->is_crouched = true; } static void M_CrouchTurn(ITEM *const item, COLL_INFO *const coll) { if (item->hit_points <= 0 || !g_Config.gameplay.enable_responsive_crawl) { item->goal_anim_state = LS(LS_CROUCH_IDLE); return; } coll->enable_hit = 0; const bool left_turn = item->current_anim_state == LS(LS_CROUCH_TURN_LEFT); item->rot.y += left_turn ? -M_CRAWL_TURN_SLOW : M_CRAWL_TURN_SLOW; if (!(left_turn ? g_Input.left : g_Input.right)) { item->goal_anim_state = LS(LS_CROUCH_IDLE); } } static void M_CrawlIdle(ITEM *const item, COLL_INFO *const coll) { if (item->hit_points <= 0) { item->goal_anim_state = LS(LS_DEATH); return; } if (g_Input.look) { Lara_Look_UpDown(); } LARA_INFO *const lara = Lara_GetLaraInfo(); lara->torso_rot.x = 0; lara->torso_rot.y = 0; coll->enable_hit = 0; coll->enable_baddie_push = 1; g_Camera.target_elevation = M_CAM_CRAWL_ELEVATION; if (Item_TestAnimEqual(item, LA(LA_CROUCH_TO_CRAWL_START))) { lara->gun_status = LGS_HANDS_BUSY; } if (Lara_Col_TestSlide(item, coll)) { return; } if (M_CanCrouchRoll(item, lara)) { Lara_AnimateUntil(item, LS(LS_CROUCH_IDLE)); item->goal_anim_state = LS(LS_CROUCH_ROLL); } else if (M_CanJumpDown(item, lara)) { item->goal_anim_state = LS(LS_CRAWL_JUMP_DOWN); } g_Camera.target_elevation = M_CAM_CRAWL_ELEVATION; } static void M_CrawlForward(ITEM *const item, COLL_INFO *const coll) { if (item->hit_points <= 0) { item->goal_anim_state = LS(LS_CRAWL_IDLE); return; } if (g_Input.look) { Lara_Look_UpDown(); } LARA_INFO *const lara = Lara_GetLaraInfo(); const bool crouch_active = g_Config.gameplay.enable_toggle_crouch ? lara->crouching : g_Input.crouch || lara->keep_crouched; lara->torso_rot.x = 0; lara->torso_rot.y = 0; coll->enable_hit = 0; coll->enable_baddie_push = 1; g_Camera.target_elevation = M_CAM_CRAWL_ELEVATION; if (Lara_Col_TestSlide(item, coll)) { return; } if (!g_Input.forward || (!crouch_active && !lara->keep_crouched)) { item->goal_anim_state = LS(LS_CRAWL_IDLE); return; } if (g_Input.left) { lara->turn_rate -= M_CRAWL_TURN_RATE; CLAMPL(lara->turn_rate, -M_CRAWL_TURN_MAX); } else if (g_Input.right) { lara->turn_rate += M_CRAWL_TURN_RATE; CLAMPG(lara->turn_rate, M_CRAWL_TURN_MAX); } } static void M_CrawlTurn(ITEM *const item, COLL_INFO *const coll) { if (item->hit_points <= 0) { item->goal_anim_state = LS(LS_CRAWL_IDLE); return; } LARA_INFO *const lara = Lara_GetLaraInfo(); lara->torso_rot.x = 0; lara->torso_rot.y = 0; coll->enable_hit = 0; coll->enable_baddie_push = 1; g_Camera.target_elevation = M_CAM_CRAWL_ELEVATION; if (Lara_Col_TestSlide(item, coll)) { return; } const bool left_turn = item->current_anim_state == LS(LS_CRAWL_TURN_LEFT); item->rot.y += left_turn ? -M_CRAWL_TURN_SLOW : M_CRAWL_TURN_SLOW; if (!(left_turn ? g_Input.left : g_Input.right)) { item->goal_anim_state = LS(LS_CRAWL_IDLE); } } static void M_CrawlBack(ITEM *const item, COLL_INFO *const coll) { if (item->hit_points <= 0) { item->goal_anim_state = LS(LS_CRAWL_IDLE); return; } if (g_Input.look) { Lara_Look_UpDown(); } LARA_INFO *const lara = Lara_GetLaraInfo(); lara->torso_rot.x = 0; lara->torso_rot.y = 0; coll->enable_hit = 0; coll->enable_baddie_push = 1; g_Camera.target_elevation = M_CAM_CRAWL_ELEVATION; if (Lara_Col_TestSlide(item, coll)) { return; } if (g_Input.back) { if (g_Input.right) { lara->turn_rate -= M_CRAWL_TURN_RATE; CLAMPL(lara->turn_rate, -M_CRAWL_TURN_MAX); } else if (g_Input.left) { lara->turn_rate += M_CRAWL_TURN_RATE; CLAMPG(lara->turn_rate, M_CRAWL_TURN_MAX); } } else { item->goal_anim_state = LS(LS_CRAWL_IDLE); } } static void M_CrawlJumpDown(ITEM *const item, COLL_INFO *const coll) { coll->enable_baddie_push = 0; coll->enable_hit = 0; LARA_INFO *const lara = Lara_GetLaraInfo(); lara->gun_status = LGS_ARMLESS; } // clang-format off REGISTER_LARA_STATE(LS_CROUCH_IDLE, M_CrouchIdle) REGISTER_LARA_STATE(LS_CROUCH_ROLL, M_CrouchRoll) REGISTER_LARA_STATE(LS_CROUCH_TURN_LEFT, M_CrouchTurn) REGISTER_LARA_STATE(LS_CROUCH_TURN_RIGHT, M_CrouchTurn) REGISTER_LARA_STATE(LS_CRAWL_IDLE, M_CrawlIdle) REGISTER_LARA_STATE(LS_CRAWL_FORWARD, M_CrawlForward) REGISTER_LARA_STATE(LS_CRAWL_TURN_LEFT, M_CrawlTurn) REGISTER_LARA_STATE(LS_CRAWL_TURN_RIGHT, M_CrawlTurn) REGISTER_LARA_STATE(LS_CRAWL_BACK, M_CrawlBack) REGISTER_LARA_STATE(LS_CRAWL_JUMP_DOWN, M_CrawlJumpDown) // clang-format on ================================================ FILE: src/trx/game/lara/state/extra.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // clang-format off #define M_LF_PICKUP_SCION 44 #define M_LF_PICKUP_GOLD_BAR 113 #define M_LF_SHARK_DEATH_END 56 #define M_LF_SHARK_DEATH_TIMER_DELAY 25 #define M_LF_TREX_DEATH_TIMER_DELAY 45 #define M_LF_YETI_DEATH_TIMER_DELAY 70 #define M_LF_DRAGON_DAGGER_PULLED 1 #define M_LF_DRAGON_DAGGER_STORED 180 #define M_LF_DRAGON_DAGGER_DISPLAY 210 #define M_LF_DRAGON_DAGGER_ANIM_END 239 #define M_LF_START_HOUSE_BEGIN 1 #define M_LF_START_HOUSE_DAGGER_STORED 401 #define M_LF_SHOWER_START 1 #define M_LF_SHOWER_SHOTGUN_PICKUP 316 #define M_CAM_YETI_KILL_ANGLE (160 * DEG_1) // = 29120 #define M_CAM_YETI_KILL_DISTANCE (3 * WALL_L) // = 3072 #define M_CAM_SHARK_KILL_ANGLE (160 * DEG_1) // = 29120 #define M_CAM_SHARK_KILL_DISTANCE (3 * WALL_L) // = 3072 #define M_CAM_AIRLOCK_ANGLE (80 * DEG_1) // = 14560 #define M_CAM_AIRLOCK_ELEVATION (-25 * DEG_1) // = -4550 #define M_CAM_GONG_BONG_ANGLE (-25 * DEG_1) // = -4550 #define M_CAM_GONG_BONG_ELEVATION (-20 * DEG_1) // = -3640 #define M_CAM_GONG_BONG_DISTANCE (3 * WALL_L) // = 3072 #define M_CAM_BEAST_KILL_ANGLE (170 * DEG_1) // = 30940 #define M_CAM_BEAST_KILL_ELEVATION (-25 * DEG_1) // = -4550 #define M_CAM_TORSO_KILL_DISTANCE (2 * WALL_L) // = 2048 // clang-format on typedef struct { int16_t frame_idx; LARA_MESH mesh; } M_MIDAS_STEP; static const M_MIDAS_STEP m_MidasSteps[] = { { .frame_idx = 5, .mesh = LM_FOOT_L }, { .frame_idx = 5, .mesh = LM_FOOT_R }, { .frame_idx = 70, .mesh = LM_CALF_L }, { .frame_idx = 90, .mesh = LM_THIGH_L }, { .frame_idx = 100, .mesh = LM_CALF_R }, { .frame_idx = 120, .mesh = LM_HIPS }, { .frame_idx = 120, .mesh = LM_THIGH_R }, { .frame_idx = 135, .mesh = LM_TORSO }, { .frame_idx = 150, .mesh = LM_UARM_L }, { .frame_idx = 163, .mesh = LM_LARM_L }, { .frame_idx = 174, .mesh = LM_HAND_L }, { .frame_idx = 186, .mesh = LM_UARM_R }, { .frame_idx = 195, .mesh = LM_LARM_R }, { .frame_idx = 218, .mesh = LM_HAND_R }, { .frame_idx = 225, .mesh = LM_HEAD }, }; static void M_ScionPedestal(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); if (!Item_TestFrameEqual(item, M_LF_PICKUP_SCION) || lara->interact_target.item_num == NO_ITEM) { return; } ITEM *const scion = Item_Get(lara->interact_target.item_num); const ITEM_ACTION action = ItemAction_ToGameID(ITEM_ACTION_FINISH_LEVEL); if (!Anim_HasFXCommand(Item_GetAnim(item), action)) { Overlay_AddDisplayPickup(scion->object_id); } Inv_AddItem(scion->object_id); scion->status = IS_INVISIBLE; Item_RemoveDrawn(lara->interact_target.item_num); Stats_AddPickup(); lara->interact_target.item_num = NO_ITEM; } static void M_UseMidas(ITEM *const item, COLL_INFO *const coll) { coll->enable_hit = 0; coll->enable_baddie_push = 0; Twinkle_SparkleItem(item, (1 << LM_HAND_L) | (1 << LM_HAND_R)); if (Item_TestFrameEqual(item, M_LF_PICKUP_GOLD_BAR)) { Overlay_AddDisplayPickup(O_PUZZLE_ITEM_1); Inv_RemoveItem(O_LEADBAR_ITEM); Inv_AddItem(O_PUZZLE_ITEM_1); LARA_INFO *const lara = Lara_GetLaraInfo(); lara->interact_target.item_num = NO_ITEM; } } static void M_MidasKill(ITEM *const item, COLL_INFO *const coll) { item->gravity = false; coll->enable_hit = 0; coll->enable_baddie_push = 0; LARA_INFO *const lara = Lara_GetLaraInfo(); const int32_t frame_num = Item_GetRelativeFrame(item); for (size_t i = 0; i < ARRAY_SIZE(m_MidasSteps); i++) { const M_MIDAS_STEP *const step = &m_MidasSteps[i]; if (step->frame_idx > frame_num) { continue; } lara->mesh_effects |= (1 << step->mesh); Lara_Skin_SwapSingleExtra(step->mesh, LS_EXTRA_MIDAS_KILL); switch (step->mesh) { case LM_TORSO: case LM_HAND_L: case LM_HAND_R: Lara_Skin_ClearEquipment(step->mesh); break; default: break; } } Twinkle_SparkleItem(item, lara->mesh_effects); } static void M_Breath(ITEM *const item, COLL_INFO *const coll) { Item_SwitchToAnim(item, LA(LA_STAND_IDLE), 0); item->goal_anim_state = LS(LS_STOP); item->current_anim_state = LS(LS_STOP); LARA_INFO *const lara = Lara_GetLaraInfo(); lara->extra_anim = false; lara->gun_status = LGS_ARMLESS; if (g_Camera.type != CAM_HEAVY) { g_Camera.type = CAM_CHASE; } Viewport_AlterFOV(-1, FOV_MODE_GAME); } static void M_YetiKill(ITEM *const item, COLL_INFO *const coll) { g_Camera.target_angle = M_CAM_YETI_KILL_ANGLE; g_Camera.target_distance = M_CAM_YETI_KILL_DISTANCE; LARA_INFO *const lara = Lara_GetLaraInfo(); lara->hit_direction = DIR_UNKNOWN; if (Item_TestFrameRange(item, 0, M_LF_YETI_DEATH_TIMER_DELAY)) { lara->death_timer = 1; } } static void M_SharkKill(ITEM *const item, COLL_INFO *const coll) { g_Camera.target_angle = M_CAM_SHARK_KILL_ANGLE; g_Camera.target_distance = M_CAM_SHARK_KILL_DISTANCE; LARA_INFO *const lara = Lara_GetLaraInfo(); lara->hit_direction = DIR_UNKNOWN; if (Item_TestFrameEqual(item, M_LF_SHARK_DEATH_END)) { const int32_t water_height = Room_GetWaterHeight(item->pos, item->room_num); if (water_height != NO_HEIGHT && water_height < item->pos.y - 100) { item->pos.y -= 5; } } if (Item_TestFrameRange(item, 0, M_LF_SHARK_DEATH_TIMER_DELAY)) { lara->death_timer = 1; } } static void M_Airlock(ITEM *const item, COLL_INFO *const coll) { g_Camera.target_angle = M_CAM_AIRLOCK_ANGLE; g_Camera.target_elevation = M_CAM_AIRLOCK_ELEVATION; } static void M_GongBong(ITEM *const item, COLL_INFO *const coll) { g_Camera.target_angle = M_CAM_GONG_BONG_ANGLE; g_Camera.target_elevation = M_CAM_GONG_BONG_ELEVATION; g_Camera.target_distance = M_CAM_GONG_BONG_DISTANCE; } static void M_BeastKill(ITEM *const item, COLL_INFO *const coll) { g_Camera.flags = CF_FOLLOW_CENTRE; g_Camera.target_angle = M_CAM_BEAST_KILL_ANGLE; g_Camera.target_elevation = M_CAM_BEAST_KILL_ELEVATION; LARA_INFO *const lara = Lara_GetLaraInfo(); lara->hit_direction = DIR_UNKNOWN; if (item->current_anim_state == LS_EXTRA_TREX_KILL) { if (Item_TestFrameRange(item, 0, M_LF_TREX_DEATH_TIMER_DELAY)) { lara->death_timer = 1; } } else if (item->current_anim_state == LS_EXTRA_TORSO_KILL) { g_Camera.target_distance = M_CAM_TORSO_KILL_DISTANCE; } } static void M_WillardKill(ITEM *const item, COLL_INFO *const coll) { g_Camera.type = CAM_CHASE; g_Camera.flags = CF_FOLLOW_CENTRE; g_Camera.target_angle = M_CAM_BEAST_KILL_ANGLE; g_Camera.target_elevation = M_CAM_BEAST_KILL_ELEVATION; } static void M_RapidsDrown(ITEM *const item, COLL_INFO *const coll) { Collide_GetCollisionInfo( coll, item->pos.x, item->pos.y, item->pos.z, item->room_num, LARA_HEIGHT); int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); item->pos.y = Room_GetHeight(sector, item->pos) + 384; item->rot.y += 1024; const int32_t time4 = (int32_t)Output_GetTimeInGame() * 4; if ((time4 & 3) == 0) { Sparks_TriggerWaterfallMist( item->pos.x, item->pos.y, item->pos.z, Random_GetControl() & 0x0FFF); } } static void M_PullDagger(ITEM *const item, COLL_INFO *const coll) { if (Item_TestFrameEqual(item, M_LF_DRAGON_DAGGER_PULLED)) { Lara_Skin_SetExtraEquipment(LM_HAND_R, EXTRA_MESH_DAGGER_HAND); Music_Play(MX_DAGGER_PULL, MPM_ONCE); } else if (Item_TestFrameEqual(item, M_LF_DRAGON_DAGGER_STORED)) { Lara_Skin_ClearEquipment(LM_HAND_R); Inv_AddItem(O_PUZZLE_ITEM_2); Stats_AddPickup(); } else if (Item_TestFrameEqual(item, M_LF_DRAGON_DAGGER_DISPLAY)) { Overlay_AddDisplayPickup(O_PUZZLE_ITEM_2); } else if (Item_TestFrameEqual(item, M_LF_DRAGON_DAGGER_ANIM_END)) { item->rot.y += DEG_90; } } static void M_StartHouse(ITEM *const item, COLL_INFO *const coll) { if (Item_TestFrameEqual(item, M_LF_START_HOUSE_BEGIN)) { Music_Play(MX_REVEAL_2, MPM_ONCE); Lara_Skin_SetExtraEquipment(LM_HAND_R, EXTRA_MESH_DAGGER_HAND); } else if (Item_TestFrameEqual(item, M_LF_START_HOUSE_DAGGER_STORED)) { Lara_Skin_ClearEquipment(LM_HAND_R); Lara_Skin_SetExtraEquipment(LM_HIPS, EXTRA_MESH_DAGGER_HIPS); Inv_AddItem(O_PUZZLE_ITEM_1); } } static void M_EndHouse(ITEM *const item, COLL_INFO *const coll) { item->hit_points = LARA_MAX_HITPOINTS; Lara_SetControllable(false); if (Item_TestFrameEqual(item, M_LF_SHOWER_START)) { LARA_INFO *const lara = Lara_GetLaraInfo(); Lara_Skin_ClearEquipment(LM_TORSO); Lara_Skin_SetCombatFace(false); Lara_Skin_ClearEquipment(LM_HAND_R); Lara_Skin_ClearEquipment(LM_HIPS); Music_Play(MX_CUTSCENE_BATH, MPM_ONCE); } else if (Item_TestFrameEqual(item, M_LF_SHOWER_SHOTGUN_PICKUP)) { Lara_Skin_SetGunEquipment(LM_HAND_R, LGT_SHOTGUN); } else if (Item_TestFrameEqual(item, -1)) { Game_SetIsLevelComplete(true); } if (Music_GetCurrentPlayingTrack() == Music_ToGameID(MX_CUTSCENE_BATH)) { const int32_t frame_num = Item_GetRelativeFrame(item); const double ts = (frame_num - M_LF_SHOWER_START) / (double)LOGIC_FPS; Music_SyncTimestamp(ts); } } static void M_TrainKill(ITEM *const item, COLL_INFO *const coll) { g_Camera.num = Camera_GetDynamicFixedObjectIdx(); g_Camera.type = CAM_FIXED; g_Camera.speed = 1; LARA_INFO *const lara = Lara_GetLaraInfo(); lara->hit_direction = DIR_UNKNOWN; item->gravity = false; item->hit_points = -1; int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); item->pos.y = Room_GetHeight(sector, item->pos); if (Item_TestFrameEqual(item, -30)) { lara->death_timer = 1; } } static void M_JailWakeUp(ITEM *const item, COLL_INFO *const coll) { if (!Item_TestFrameEqual(item, -2)) { return; } Item_Animate(item); if (!g_Config.gameplay.enable_cinematics) { return; } XYZ_32 pos = {}; Lara_GetMeshPos(LM_HIPS, &pos); item->pos.x = pos.x; item->pos.z = pos.z; item->interp.prev.pos = item->pos; item->interp.prev.rot = item->rot; } // clang-format off REGISTER_LARA_EXTRA(LS_EXTRA_BREATH, M_Breath) REGISTER_LARA_EXTRA(LS_EXTRA_SCION_PICKUP_1, M_ScionPedestal) REGISTER_LARA_EXTRA(LS_EXTRA_USE_MIDAS, M_UseMidas) REGISTER_LARA_EXTRA(LS_EXTRA_MIDAS_KILL, M_MidasKill) REGISTER_LARA_EXTRA(LS_EXTRA_YETI_KILL, M_YetiKill) REGISTER_LARA_EXTRA(LS_EXTRA_GUARD_KILL, M_YetiKill) REGISTER_LARA_EXTRA(LS_EXTRA_SHARK_KILL, M_SharkKill) REGISTER_LARA_EXTRA(LS_EXTRA_AIRLOCK, M_Airlock) REGISTER_LARA_EXTRA(LS_EXTRA_GONG_BONG, M_GongBong) REGISTER_LARA_EXTRA(LS_EXTRA_TREX_KILL, M_BeastKill) REGISTER_LARA_EXTRA(LS_EXTRA_TORSO_KILL, M_BeastKill) REGISTER_LARA_EXTRA(LS_EXTRA_PULL_DAGGER, M_PullDagger) REGISTER_LARA_EXTRA(LS_EXTRA_START_HOUSE, M_StartHouse) REGISTER_LARA_EXTRA(LS_EXTRA_END_HOUSE, M_EndHouse) REGISTER_LARA_EXTRA(LS_EXTRA_SHIVA_KILL, M_BeastKill) REGISTER_LARA_EXTRA(LS_EXTRA_WILLARD_KILL, M_WillardKill) REGISTER_LARA_EXTRA(LS_EXTRA_RAPIDS_DROWN, M_RapidsDrown) REGISTER_LARA_EXTRA(LS_EXTRA_TRAIN_KILL, M_TrainKill) REGISTER_LARA_EXTRA(LS_EXTRA_JAIL_WAKE_UP, M_JailWakeUp) // clang-format on ================================================ FILE: src/trx/game/lara/state/jump.c ================================================ #include #include #include #include #include #include #include // clang-format off #define M_SCREAM_SPEED (DAMAGE_START + DAMAGE_LENGTH) // = 154 #define M_JUMP_TURN ((DEG_1 * 1) + LARA_TURN_UNDO) // = 546 #define M_FAST_FALL_SPEED (FAST_FALL_SPEED + 3) // = 131 #define M_SWING_FAST_FALL_SPEED (M_FAST_FALL_SPEED + 2) // = 133 #define M_CAM_BACK_JUMP_ANGLE (135 * DEG_1) // = 24570 #define M_CAM_REACH_ANGLE (85 * DEG_1) // = 15470 #define M_CAM_ZIPLINE_ANGLE (70 * DEG_1) // = 12740 #define M_LF_NEUTRAL_TWIST_WADE_SPLASH -5 // clang-format on static void M_Compress(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->water_status != LWS_WADE) { if (g_Input.forward && Lara_FloorFront(item, item->rot.y, STEP_L) >= -STEPUP_HEIGHT) { item->goal_anim_state = LS(LS_JUMP_FORWARD); lara->move_angle = item->rot.y; } else if ( g_Input.left && Lara_FloorFront(item, item->rot.y - DEG_90, STEP_L) >= -STEPUP_HEIGHT) { item->goal_anim_state = LS(LS_JUMP_LEFT); lara->move_angle = item->rot.y - DEG_90; } else if ( g_Input.right && Lara_FloorFront(item, item->rot.y + DEG_90, STEP_L) >= -STEPUP_HEIGHT) { item->goal_anim_state = LS(LS_JUMP_RIGHT); lara->move_angle = item->rot.y + DEG_90; } else if ( g_Input.back && Lara_FloorFront(item, item->rot.y + DEG_180, STEP_L) >= -STEPUP_HEIGHT) { item->goal_anim_state = LS(LS_JUMP_BACK); lara->move_angle = item->rot.y + DEG_180; } else if ( g_Input.roll && g_Config.gameplay.enable_neutral_twists && Lara_State_IsResponsive(LA_STAND_TO_JUMP)) { item->goal_anim_state = LS(LS_RESPONSIVE); } } if (item->fall_speed > M_FAST_FALL_SPEED) { item->goal_anim_state = LS(LS_FAST_FALL); } } static void M_UpJump(ITEM *const item, COLL_INFO *const coll) { const int16_t fast_speed = g_Config.gameplay.enable_swing_cancel ? M_SWING_FAST_FALL_SPEED : M_FAST_FALL_SPEED; if (item->fall_speed > fast_speed) { item->goal_anim_state = LS(LS_FAST_FALL); } } static void M_NeutralJumpRoll(ITEM *const item, COLL_INFO *const coll) { coll->enable_hit = 0; if (g_Input.jump && g_Input.roll) { item->goal_anim_state = LS(LS_RESPONSIVE); } else if (g_Input.jump) { item->goal_anim_state = LS(LS_COMPRESS); } else if (g_Input.roll) { item->goal_anim_state = LS(LS_ROLL); } else { if (Item_TestFrameEqual(item, M_LF_NEUTRAL_TWIST_WADE_SPLASH)) { Lara_Col_WadeSplash(item); } item->goal_anim_state = LS(LS_STOP); } } static void M_ForwardJump(ITEM *const item, COLL_INFO *const coll) { if (item->goal_anim_state == LS(LS_SWAN_DIVE) || item->goal_anim_state == LS(LS_REACH)) { item->goal_anim_state = LS(LS_JUMP_FORWARD); } LARA_INFO *const lara = Lara_GetLaraInfo(); if (item->goal_anim_state != LS(LS_DEATH) && item->goal_anim_state != LS(LS_STOP) && item->goal_anim_state != LS(LS_RUN)) { if (g_Input.action && lara->gun_status == LGS_ARMLESS) { item->goal_anim_state = LS(LS_REACH); } if (g_Config.gameplay.enable_jump_twists && (g_Input.roll || g_Input.back)) { item->goal_anim_state = LS(LS_TWIST); } if (g_Input.slow && lara->gun_status == LGS_ARMLESS) { item->goal_anim_state = LS(LS_SWAN_DIVE); } if (item->fall_speed > M_FAST_FALL_SPEED) { item->goal_anim_state = LS(LS_FAST_FALL); } } if (g_Input.left) { lara->turn_rate -= LARA_TURN_RATE; CLAMPL(lara->turn_rate, -M_JUMP_TURN); } else if (g_Input.right) { lara->turn_rate += LARA_TURN_RATE; CLAMPG(lara->turn_rate, +M_JUMP_TURN); } } static void M_BackJump(ITEM *const item, COLL_INFO *const coll) { if (!Item_TestAnimEqual(item, LA(LA_HANG_TO_JUMP_BACK))) { g_Camera.target_angle = M_CAM_BACK_JUMP_ANGLE; } if (item->fall_speed > M_FAST_FALL_SPEED) { item->goal_anim_state = LS(LS_FAST_FALL); return; } if (item->goal_anim_state == LS(LS_RUN)) { item->goal_anim_state = LS(LS_STOP); } else if ( g_Config.gameplay.enable_jump_twists && (g_Input.forward || g_Input.roll) && item->goal_anim_state != LS(LS_STOP)) { item->goal_anim_state = LS(LS_TWIST); } } static void M_SideJump(ITEM *const item, COLL_INFO *const coll) { if (item->fall_speed > M_FAST_FALL_SPEED) { item->goal_anim_state = LS(LS_FAST_FALL); return; } // TODO: unused animation transition, perhaps look at restoring const bool twist_input = item->current_anim_state == LS(LS_JUMP_LEFT) ? g_Input.right : g_Input.left; if (g_Config.gameplay.enable_jump_twists && twist_input && item->goal_anim_state != LS(LS_STOP)) { item->goal_anim_state = LS(LS_TWIST); } } static void M_FallBack(ITEM *const item, COLL_INFO *const coll) { if (item->fall_speed > M_FAST_FALL_SPEED) { item->goal_anim_state = LS(LS_FAST_FALL); return; } const LARA_INFO *const lara = Lara_GetLaraInfo(); if (g_Input.action && lara->gun_status == LGS_ARMLESS) { item->goal_anim_state = LS(LS_REACH); } } static void M_Reach(ITEM *const item, COLL_INFO *const coll) { g_Camera.target_angle = M_CAM_REACH_ANGLE; if (item->fall_speed > M_FAST_FALL_SPEED) { item->goal_anim_state = LS(LS_FAST_FALL); } } static void M_SwanDive(ITEM *const item, COLL_INFO *const coll) { coll->enable_hit = 0; coll->enable_baddie_push = 1; if (item->fall_speed > M_FAST_FALL_SPEED && item->goal_anim_state != LS(LS_DIVE)) { item->goal_anim_state = LS(LS_FAST_DIVE); } } static void M_FastDive(ITEM *item, COLL_INFO *coll) { if (g_Config.gameplay.enable_jump_twists && g_Input.roll && item->goal_anim_state == LS(LS_FAST_DIVE)) { item->goal_anim_state = LS(LS_TWIST); } coll->enable_hit = 0; coll->enable_baddie_push = 1; item->speed = item->speed * 95 / 100; } static void M_FastFall(ITEM *const item, COLL_INFO *const coll) { item->speed = item->speed * 95 / 100; const bool scream = g_TRVersion == 1 ? (item->fall_speed >= M_SCREAM_SPEED) : (item->fall_speed == M_SCREAM_SPEED); if (scream && !g_Config.debug.enable_invulnerability) { Sound_Effect(SFX_LARA_FALL, &item->pos, SPM_NORMAL); } } static void M_Zipline(ITEM *const item, COLL_INFO *const coll) { g_Camera.target_angle = M_CAM_ZIPLINE_ANGLE; if (!g_Input.action) { item->goal_anim_state = LS(LS_JUMP_FORWARD); Lara_Animate(item); item->gravity = true; item->speed = 100; item->fall_speed = 40; LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y; } } // clang-format off REGISTER_LARA_STATE(LS_COMPRESS, M_Compress) REGISTER_LARA_STATE(LS_JUMP_UP, M_UpJump) REGISTER_LARA_STATE(LS_NEUTRAL_ROLL, M_NeutralJumpRoll) REGISTER_LARA_STATE(LS_JUMP_FORWARD, M_ForwardJump) REGISTER_LARA_STATE(LS_JUMP_BACK, M_BackJump) REGISTER_LARA_STATE(LS_JUMP_RIGHT, M_SideJump) REGISTER_LARA_STATE(LS_JUMP_LEFT, M_SideJump) REGISTER_LARA_STATE(LS_FALL_BACK, M_FallBack) REGISTER_LARA_STATE(LS_REACH, M_Reach) REGISTER_LARA_STATE(LS_SWAN_DIVE, M_SwanDive) REGISTER_LARA_STATE(LS_FAST_DIVE, M_FastDive) REGISTER_LARA_STATE(LS_FAST_FALL, M_FastFall) REGISTER_LARA_STATE(LS_ZIPLINE, M_Zipline) // clang-format on ================================================ FILE: src/trx/game/lara/state/land.c ================================================ #include #include #include #include #include #include #include #include #include // clang-format off #define M_WALK_DIST 104 #define M_WALK_BACK_DIST 140 #define M_LF_ROLL 2 #define M_CANCEL_POSE_TIME (10 * LOGIC_FPS) // = 300 #define M_CANCEL_POSE_CHANCE 0x40 // = 64 #define M_FAST_TURN ((DEG_1 * 6) + LARA_TURN_UNDO) // = 1456 #define M_FAST_FALL_SPEED (FAST_FALL_SPEED + 3) // = 131 #define M_SPRINT_TURN_RATE ((DEG_1 * 2) + 45) // = 409 #define M_SPRINT_TURN_MAX (DEG_1 * 4) // = 728 #define M_SPRINT_LEAN_MAX (DEG_1 * 16) // = 2192 #define M_CAM_SLIDE_ELEVATION (-45 * DEG_1) // = -8190 #define M_CAM_PUSH_BLOCK_ANGLE (35 * DEG_1) // = 6370 #define M_CAM_PUSH_BLOCK_ELEVATION (-25 * DEG_1) // = -4550 #define M_CAM_PP_READY_ANGLE (75 * DEG_1) // = 13650 #define M_CAM_PICKUP_ANGLE (-130 * DEG_1) // = -23660 #define M_CAM_PICKUP_ELEVATION (-15 * DEG_1) // = -2730 #define M_CAM_PICKUP_DISTANCE WALL_L // = 1024 #define M_CAM_SWITCH_ON_ANGLE (80 * DEG_1) // = 14560 #define M_CAM_SWITCH_ON_ELEVATION (-25 * DEG_1) // = -4550 #define M_CAM_SWITCH_ON_DISTANCE WALL_L // = 1024 #define M_CAM_SWITCH_ON_SPEED 6 #define M_CAM_USE_KEY_ANGLE (-M_CAM_SWITCH_ON_ANGLE) // = -14560 #define M_CAM_USE_KEY_ELEVATION M_CAM_SWITCH_ON_ELEVATION // = -4550 #define M_CAM_USE_KEY_DISTANCE WALL_L // = 1024 #define M_CAM_SPECIAL_ANGLE (170 * DEG_1) // = 30940 #define M_CAM_SPECIAL_ELEVATION (-25 * DEG_1) // = -4550 #define M_CAM_SPECIAL_DISTANCE (2 * WALL_L) // = 2048 #define M_CAM_POSE_RIGHT_ANGLE M_CAM_SPECIAL_ANGLE // = 30940 #define M_CAM_POSE_LEFT_ANGLE -M_CAM_SPECIAL_ANGLE // = -30940 // clang-format on static bool m_JumpPermitted = true; static const int16_t m_JumpLockFrames[JUMP_LOCK_NUMBER_OF] = { // clang-format off [JUMP_LOCK_LEGACY] = 4, [JUMP_LOCK_TUNED] = 2, [JUMP_LOCK_DISABLED] = 19, // clang-format on }; static bool M_CanPose(void) { if (g_Config.gameplay.idle_pose_timeout == 0) { return false; } const LARA_INFO *const lara = Lara_GetLaraInfo(); return !g_Input.draw && !g_Input.look && lara->hit_direction == DIR_UNKNOWN && lara->gun_status == LGS_ARMLESS && lara->water_status == LWS_ABOVE_WATER && !g_Input.use_flare && !lara->flare.control && !Lara_Vehicle_IsMounted(); } static bool M_ShouldStopPosing(void) { return !M_CanPose() || g_Input.forward || g_Input.back || g_Input.left || g_Input.right || g_Input.step_left || g_Input.step_right || g_Input.jump || g_Input.action; } static void M_Default(ITEM *const item, COLL_INFO *const coll) { coll->enable_hit = 0; coll->enable_baddie_push = 0; } static void M_PullUp(ITEM *const item, COLL_INFO *const coll) { M_Default(item, coll); if (g_Input.forward && Item_TestAnimEqual(item, LA(LA_CLIMB_2CLICK_END))) { item->goal_anim_state = LS(LS_RUN); } } static void M_Walk(ITEM *const item, COLL_INFO *const coll) { if (item->hit_points <= 0) { item->goal_anim_state = LS(LS_STOP); return; } LARA_INFO *const lara = Lara_GetLaraInfo(); if (g_Input.left) { lara->turn_rate -= LARA_TURN_RATE; CLAMPL(lara->turn_rate, -LARA_SLOW_TURN); } else if (g_Input.right) { lara->turn_rate += LARA_TURN_RATE; CLAMPG(lara->turn_rate, +LARA_SLOW_TURN); } if (g_Input.forward) { if (lara->water_status == LWS_WADE) { item->goal_anim_state = LS(LS_WADE); } else if (g_Input.slow) { item->goal_anim_state = LS(LS_WALK); } else { if (g_Config.gameplay.fix_walk_run_jump) { m_JumpPermitted = true; } item->goal_anim_state = LS(LS_RUN); } } else { item->goal_anim_state = LS(LS_STOP); } } static LARA_STATE M_GetRunToCrouchState(void) { return LS( g_Config.gameplay.enable_responsive_crawl ? LS_CROUCH_IDLE : LS_STOP); } static bool M_RequestSprint(LARA_INFO *const lara) { if (g_Config.gameplay.enable_toggle_sprint) { if (g_InputDB.sprint) { lara->sprinting = !lara->sprinting; } return lara->sprinting; } else { lara->sprinting = false; return g_Input.sprint; } } static bool M_RequestDuck(LARA_INFO *const lara) { if (g_Config.gameplay.enable_toggle_crouch) { if (g_InputDB.crouch) { lara->crouching = true; } return lara->crouching; } else { lara->crouching = false; return g_Input.crouch; } } static void M_Run(ITEM *const item, COLL_INFO *const coll) { if (item->hit_points <= 0) { item->goal_anim_state = LS(LS_DEATH); return; } if (g_Input.roll) { Lara_Col_WadeSplash(item); item->current_anim_state = LS(LS_ROLL); item->goal_anim_state = LS(LS_STOP); Item_SwitchToAnim(item, LA(LA_ROLL_START), M_LF_ROLL); return; } LARA_INFO *const lara = Lara_GetLaraInfo(); const bool sprint_requested = M_RequestSprint(lara); if ((item->hit_points <= 0 || lara->sprint_timer <= 0 || lara->water_status == LWS_WADE || !g_Config.gameplay.enable_sprint) && g_Config.gameplay.enable_toggle_sprint) { lara->sprinting = false; } if (sprint_requested && g_Config.gameplay.enable_sprint && lara->water_status != LWS_WADE && item->current_anim_state == LS(LS_RUN) && lara->sprint_timer > 0 && (g_Config.gameplay.enable_responsive_sprint || lara->sprint_timer == LARA_MAX_SPRINT)) { item->goal_anim_state = LS(LS_SPRINT); return; } if (M_RequestDuck(lara)) { item->goal_anim_state = M_GetRunToCrouchState(); return; } if (g_Input.left) { lara->turn_rate -= LARA_TURN_RATE; CLAMPL(lara->turn_rate, -M_FAST_TURN); item->rot.z -= LARA_LEAN_RATE; CLAMPL(item->rot.z, -LARA_LEAN_MAX); } else if (g_Input.right) { lara->turn_rate += LARA_TURN_RATE; CLAMPG(lara->turn_rate, +M_FAST_TURN); item->rot.z += LARA_LEAN_RATE; CLAMPG(item->rot.z, +LARA_LEAN_MAX); } const bool responsive_jumping = g_Config.gameplay.enable_tr2_jumping && Lara_State_IsResponsive(LA_RUN); if (responsive_jumping) { const int16_t unlock_frame = m_JumpLockFrames[g_Config.gameplay.jump_lock_mode]; if (Item_TestAnimEqual(item, LA(LA_RUN_START))) { m_JumpPermitted = g_Config.gameplay.jump_lock_mode == JUMP_LOCK_DISABLED; } else if ( !Item_TestAnimEqual(item, LA(LA_RUN)) || Item_TestFrameEqual(item, unlock_frame)) { m_JumpPermitted = true; } } else { m_JumpPermitted = true; } if (g_Input.jump && m_JumpPermitted && !item->gravity) { item->goal_anim_state = LS(responsive_jumping ? LS_RESPONSIVE : LS_JUMP_FORWARD); } else if (g_Input.forward) { if (lara->water_status == LWS_WADE) { item->goal_anim_state = LS(LS_WADE); } else if (g_Input.slow) { item->goal_anim_state = LS(LS_WALK); } else { item->goal_anim_state = LS(LS_RUN); } } else { item->goal_anim_state = LS(LS_STOP); } } static void M_Wade(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->sprinting = false; if (item->hit_points <= 0) { item->goal_anim_state = LS(LS_STOP); return; } g_Camera.target_elevation = CAM_WADE_ELEVATION; const ROOM *const room = Room_Get(item->room_num); if (room->flags.swamp) { if (g_Input.left) { lara->turn_rate -= LARA_TURN_RATE; CLAMPL(lara->turn_rate, -LARA_SLOW_TURN); item->rot.z -= LARA_LEAN_RATE; CLAMPL(item->rot.z, -LARA_LEAN_MAX / 2); } else if (g_Input.right) { lara->turn_rate += LARA_TURN_RATE; CLAMPG(lara->turn_rate, LARA_SLOW_TURN); item->rot.z += LARA_LEAN_RATE; CLAMPG(item->rot.z, LARA_LEAN_MAX / 2); } if (g_Input.forward) { item->goal_anim_state = LS(LS_WADE); } else { item->goal_anim_state = LS(LS_STOP); } } else { if (g_Input.left) { lara->turn_rate -= LARA_TURN_RATE; CLAMPL(lara->turn_rate, -M_FAST_TURN); item->rot.z -= LARA_LEAN_RATE; CLAMPL(item->rot.z, -LARA_LEAN_MAX); } else if (g_Input.right) { lara->turn_rate += LARA_TURN_RATE; CLAMPG(lara->turn_rate, M_FAST_TURN); item->rot.z += LARA_LEAN_RATE; CLAMPG(item->rot.z, LARA_LEAN_MAX); } if (g_Input.forward) { if (lara->water_status != LWS_ABOVE_WATER) { item->goal_anim_state = LS(LS_WADE); } else { item->goal_anim_state = LS(LS_RUN); } } else { item->goal_anim_state = LS(LS_STOP); } } } static void M_WalkBack(ITEM *const item, COLL_INFO *const coll) { if (item->hit_points <= 0) { item->goal_anim_state = LS(LS_STOP); return; } LARA_INFO *const lara = Lara_GetLaraInfo(); if (g_Input.back && (g_Input.slow || lara->water_status == LWS_WADE)) { item->goal_anim_state = LS(LS_WALK_BACK); } else { item->goal_anim_state = LS(LS_STOP); } if (g_Input.left) { lara->turn_rate -= LARA_TURN_RATE; CLAMPL(lara->turn_rate, -LARA_SLOW_TURN); } else if (g_Input.right) { lara->turn_rate += LARA_TURN_RATE; CLAMPG(lara->turn_rate, LARA_SLOW_TURN); } } static void M_Stop(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->sprinting = false; if (item->hit_points <= 0) { item->goal_anim_state = LS(LS_DEATH); return; } if (lara->interact_target.is_moving) { return; } if (M_RequestDuck(lara) && lara->water_status != LWS_WADE && item->current_anim_state == LS(LS_STOP) && (lara->gun_status == LGS_ARMLESS || !Gun_IsRifleType(lara->gun_type)) && !Lara_Vehicle_IsMounted()) { item->goal_anim_state = LS(LS_CROUCH_IDLE); return; } if (g_Input.roll && lara->water_status != LWS_WADE) { if (g_Input.jump && g_Config.gameplay.enable_neutral_twists && Item_TestAnimEqual(item, LA(LA_STAND_IDLE)) && Lara_State_IsResponsive(LA_STAND_TO_JUMP)) { item->current_anim_state = LS(LS_NEUTRAL_ROLL); item->goal_anim_state = LS(LS_STOP); Item_SwitchToAnim(item, LA(LA_JUMP_NEUTRAL_ROLL), 0); } else if (!g_Input.jump || !g_Config.gameplay.enable_neutral_twists) { Lara_Col_WadeSplash(item); item->current_anim_state = LS(LS_ROLL); item->goal_anim_state = LS(LS_STOP); Item_SwitchToAnim(item, LA(LA_ROLL_START), M_LF_ROLL); } return; } lara->crouching = false; item->goal_anim_state = LS(LS_STOP); if (g_Input.look) { Lara_Look_UpDown(); if (g_Config.gameplay.look_mode == LOOK_MODE_RESTRICTED) { Lara_Look_LeftRight(); return; } } int32_t fheight = NO_HEIGHT; int32_t rheight = NO_HEIGHT; if (g_Input.forward) { fheight = Lara_FloorFront(item, item->rot.y, M_WALK_DIST); } else if (g_Input.back) { rheight = Lara_FloorFront(item, item->rot.y + DEG_180, M_WALK_BACK_DIST); } const ROOM *const room = Room_Get(item->room_num); if (room->flags.swamp) { if (g_Input.left) { item->goal_anim_state = LS(LS_TURN_LEFT); } else if (g_Input.right) { item->goal_anim_state = LS(LS_TURN_RIGHT); } } else if (g_Input.step_left) { const int32_t h = Lara_FloorFront(item, item->rot.y - DEG_90, 148); const int32_t c = Lara_CeilingFront(item, item->rot.y - DEG_90, 148, LARA_HEIGHT); if (g_TRVersion < 3 || (h < 128 && h > -128 && Room_GetHeightType() != HT_BIG_SLOPE && c <= 0)) { item->goal_anim_state = LS(LS_STEP_LEFT); } } else if (g_Input.step_right) { const int32_t h = Lara_FloorFront(item, item->rot.y + DEG_90, 148); const int32_t c = Lara_CeilingFront(item, item->rot.y + DEG_90, 148, LARA_HEIGHT); if (g_TRVersion < 3 || (h < 128 && h > -128 && Room_GetHeightType() != HT_BIG_SLOPE && c <= 0)) { item->goal_anim_state = LS(LS_STEP_RIGHT); } } else if (g_Input.left) { item->goal_anim_state = LS(LS_TURN_LEFT); } else if (g_Input.right) { item->goal_anim_state = LS(LS_TURN_RIGHT); } if (lara->water_status == LWS_WADE) { if (g_Input.jump && !room->flags.swamp) { item->goal_anim_state = LS(LS_COMPRESS); } if (g_Input.forward) { if (room->flags.swamp || g_Input.slow) { M_Wade(item, coll); } else { M_Walk(item, coll); } } else if (g_Input.back) { M_WalkBack(item, coll); } } else if (g_Input.jump) { item->goal_anim_state = LS(LS_COMPRESS); } else if (g_Input.forward) { bool bad_floor = false; bool bad_ceiling = false; if (g_Config.gameplay.wall_glitch_mode == WALL_GLITCH_FIXED) { const int32_t h = Lara_FloorFront(item, item->rot.y, M_WALK_DIST); const int32_t c = Lara_CeilingFront(item, item->rot.y, M_WALK_DIST, LARA_HEIGHT); const HEIGHT_TYPE height_type = Room_GetHeightType(); bad_floor = height_type == HT_BIG_SLOPE && h < 0; bad_ceiling = c > 0 && !g_Input.action; } if (bad_floor || bad_ceiling) { item->goal_anim_state = LS_STOP; } else if (g_Input.slow) { M_Walk(item, coll); } else { M_Run(item, coll); } } else if (g_Input.back) { if (g_Input.slow) { if (g_TRVersion < 3 || (rheight < (STEPUP_HEIGHT - 1) && rheight > -(STEPUP_HEIGHT - 1) && Room_GetHeightType() != HT_BIG_SLOPE)) { M_WalkBack(item, coll); } } else if (g_TRVersion < 3 || rheight > -(STEPUP_HEIGHT - 1)) { item->goal_anim_state = LS(LS_FAST_BACK); } } if (item->goal_anim_state == LS(LS_STOP) && M_CanPose()) { lara->idle_timer++; const int32_t timeout = g_Config.gameplay.idle_pose_timeout * LOGIC_FPS; CLAMPG(lara->idle_timer, timeout); if (lara->idle_timer == timeout) { lara->idle_timer = 0; item->goal_anim_state = LS(Random_GetControl() < 0x4000 ? LS_POSE_LEFT : LS_POSE_RIGHT); } } else { lara->idle_timer = 0; } } static void M_Pose(ITEM *const item, COLL_INFO *const coll) { if (item->hit_points <= 0) { item->goal_anim_state = LS(LS_DEATH); return; } if (g_Input.look) { Lara_Look_UpDown(); if (g_Config.gameplay.look_mode == LOOK_MODE_RESTRICTED) { Lara_Look_LeftRight(); return; } } bool cancel_camera = false; if (g_Input.roll && !g_Input.jump) { item->goal_anim_state = LS(LS_ROLL); cancel_camera = true; } else if (item->current_anim_state == LS(LS_POSE)) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->idle_timer++; if (M_ShouldStopPosing() || (lara->idle_timer >= M_CANCEL_POSE_TIME && Random_GetControl() < M_CANCEL_POSE_CHANCE)) { item->goal_anim_state = LS(LS_STOP); cancel_camera = true; } } if (g_Config.gameplay.enable_idle_pose_camera) { if (item->current_anim_state == LS(LS_POSE_START) && Item_TestFrameEqual(item, -1) && g_Camera.type == CAM_CHASE) { g_Camera.additional_angle = Item_TestAnimEqual(item, LA(LA_POSE_RIGHT_START)) ? M_CAM_POSE_RIGHT_ANGLE : M_CAM_POSE_LEFT_ANGLE; } else if (cancel_camera) { g_Camera.additional_angle = 0; } } } static void M_FastBack(ITEM *const item, COLL_INFO *const coll) { item->goal_anim_state = LS(LS_STOP); LARA_INFO *const lara = Lara_GetLaraInfo(); if (g_Input.left) { lara->turn_rate -= LARA_TURN_RATE; CLAMPL(lara->turn_rate, -LARA_MED_TURN); } else if (g_Input.right) { lara->turn_rate += LARA_TURN_RATE; CLAMPG(lara->turn_rate, LARA_MED_TURN); } } static void M_Turn(ITEM *const item, COLL_INFO *const coll) { if (item->hit_points <= 0) { item->goal_anim_state = LS(LS_STOP); return; } if (g_Config.gameplay.look_mode != LOOK_MODE_RESTRICTED && g_Input.look) { item->goal_anim_state = LS(LS_STOP); return; } const bool left_turn = item->current_anim_state == LS(LS_TURN_LEFT); const bool turn_input = left_turn ? g_Input.left : g_Input.right; LARA_INFO *const lara = Lara_GetLaraInfo(); if (left_turn) { lara->turn_rate -= LARA_TURN_RATE; } else { lara->turn_rate += LARA_TURN_RATE; } if (lara->gun_status == LGS_READY) { item->goal_anim_state = LS(LS_FAST_TURN); } else if (left_turn && lara->turn_rate < -LARA_SLOW_TURN) { if (g_Input.slow) { lara->turn_rate = -LARA_SLOW_TURN; } else { item->goal_anim_state = LS(LS_FAST_TURN); } } else if (!left_turn && lara->turn_rate > LARA_SLOW_TURN) { if (g_Input.slow) { lara->turn_rate = LARA_SLOW_TURN; } else { item->goal_anim_state = LS(LS_FAST_TURN); } } if (g_Input.forward) { if (lara->water_status == LWS_WADE) { item->goal_anim_state = LS(LS_WADE); } else if (g_Input.slow) { item->goal_anim_state = LS(LS_WALK); } else { item->goal_anim_state = LS(LS_RUN); } } else if (!turn_input) { item->goal_anim_state = LS(LS_STOP); } } static void M_FastTurn(ITEM *const item, COLL_INFO *const coll) { if (item->hit_points <= 0) { item->goal_anim_state = LS(LS_STOP); return; } if (g_Config.gameplay.look_mode != LOOK_MODE_RESTRICTED && g_Input.look) { item->goal_anim_state = LS(LS_STOP); return; } LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->turn_rate >= 0) { lara->turn_rate = M_FAST_TURN; if (!g_Input.right) { item->goal_anim_state = LS(LS_STOP); } } else { lara->turn_rate = -M_FAST_TURN; if (!g_Input.left) { item->goal_anim_state = LS(LS_STOP); } } } static void M_SideStep(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); if (item->hit_points <= 0) { item->goal_anim_state = LS(LS_STOP); return; } const bool step_input = item->current_anim_state == LS(LS_STEP_LEFT) ? g_Input.step_left : g_Input.step_right; if (!step_input) { item->goal_anim_state = LS(LS_STOP); } if (g_Input.left) { lara->turn_rate -= LARA_TURN_RATE; CLAMPL(lara->turn_rate, -LARA_SLOW_TURN); } else if (g_Input.right) { lara->turn_rate += LARA_TURN_RATE; CLAMPG(lara->turn_rate, LARA_SLOW_TURN); } } static void M_Slide(ITEM *const item, COLL_INFO *const coll) { const bool sliding_forward = item->current_anim_state == LS(LS_SLIDE); bool opposite_input; if (sliding_forward) { g_Camera.flags = CF_NO_CHUNKY; g_Camera.target_elevation = M_CAM_SLIDE_ELEVATION; opposite_input = g_Input.back; } else { opposite_input = g_Input.forward; } if (Item_TestAnimEqual(item, LA(LA_SLIDE_FORWARD_TO_RUN))) { item->goal_anim_state = LS(g_Input.sprint && g_Config.gameplay.enable_sprint ? LS_SPRINT : LS_RUN); } else if ( sliding_forward && g_Config.gameplay.enable_slide_to_run && item->goal_anim_state == LS(LS_STOP) && g_Input.forward && Lara_State_IsResponsive(LA_SLIDE_FORWARD)) { item->goal_anim_state = LS(LS_RESPONSIVE); } else if ( g_Input.jump && (!g_Config.gameplay.enable_jump_twists || !opposite_input)) { item->goal_anim_state = LS(sliding_forward ? LS_JUMP_FORWARD : LS_JUMP_BACK); } } static void M_Roll(ITEM *const item, COLL_INFO *const coll) { coll->enable_hit = 0; } static void M_PushBlock(ITEM *const item, COLL_INFO *const coll) { M_Default(item, coll); g_Camera.flags = CF_FOLLOW_CENTRE; g_Camera.target_angle = M_CAM_PUSH_BLOCK_ANGLE; g_Camera.target_elevation = M_CAM_PUSH_BLOCK_ELEVATION; } static void M_PPReady(ITEM *const item, COLL_INFO *const coll) { M_Default(item, coll); g_Camera.target_angle = M_CAM_PP_READY_ANGLE; if (!g_Input.action) { item->goal_anim_state = LS(LS_STOP); } } static void M_Pickup(ITEM *const item, COLL_INFO *const coll) { M_Default(item, coll); g_Camera.target_angle = M_CAM_PICKUP_ANGLE; g_Camera.target_elevation = M_CAM_PICKUP_ELEVATION; g_Camera.target_distance = M_CAM_PICKUP_DISTANCE; if (item->current_anim_state == LS(LS_FLARE_PICKUP) && Item_TestFrameEqual(item, -1)) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->gun_status = LGS_ARMLESS; } } static void M_SwitchOn(ITEM *const item, COLL_INFO *const coll) { M_Default(item, coll); g_Camera.target_angle = M_CAM_SWITCH_ON_ANGLE; g_Camera.target_elevation = M_CAM_SWITCH_ON_ELEVATION; g_Camera.target_distance = M_CAM_SWITCH_ON_DISTANCE; g_Camera.speed = M_CAM_SWITCH_ON_SPEED; } static void M_UseKey(ITEM *const item, COLL_INFO *const coll) { M_Default(item, coll); g_Camera.target_angle = M_CAM_USE_KEY_ANGLE; g_Camera.target_elevation = M_CAM_USE_KEY_ELEVATION; g_Camera.target_distance = M_CAM_USE_KEY_DISTANCE; } static void M_Special(ITEM *const item, COLL_INFO *const coll) { ITEM *const target_item = Lara_GetDeathCameraTarget(); if (target_item != nullptr) { g_Camera.item = target_item; g_Camera.flags = CF_CHASE_OBJECT; g_Camera.type = CAM_FIXED; g_Camera.target_angle = item->rot.y; g_Camera.target_distance = M_CAM_SPECIAL_DISTANCE; } else { g_Camera.flags = CF_FOLLOW_CENTRE; g_Camera.target_angle = M_CAM_SPECIAL_ANGLE; } g_Camera.target_elevation = M_CAM_SPECIAL_ELEVATION; } static void M_Sprint(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); if (item->hit_points <= 0 || lara->sprint_timer <= 0 || lara->water_status == LWS_WADE) { lara->sprinting = false; item->goal_anim_state = LS(LS_RUN); return; } if (g_Config.gameplay.enable_toggle_sprint ? (!lara->sprinting || g_InputDB.sprint) : !g_Input.sprint) { lara->sprinting = false; item->goal_anim_state = LS(LS_RUN); return; } if (!g_Config.debug.enable_endless_sprint) { lara->sprint_timer--; } if (g_Input.roll) { Lara_Col_WadeSplash(item); lara->sprinting = false; item->current_anim_state = LS(LS_ROLL); item->goal_anim_state = LS(LS_STOP); Item_SwitchToAnim(item, LA(LA_ROLL_START), M_LF_ROLL); return; } if (M_RequestDuck(lara)) { item->goal_anim_state = M_GetRunToCrouchState(); return; } if (g_Input.left) { lara->turn_rate -= M_SPRINT_TURN_RATE; CLAMPL(lara->turn_rate, -M_SPRINT_TURN_MAX); item->rot.z -= LARA_LEAN_RATE; CLAMPL(item->rot.z, -M_SPRINT_LEAN_MAX); } else if (g_Input.right) { lara->turn_rate += M_SPRINT_TURN_RATE; CLAMPG(lara->turn_rate, M_SPRINT_TURN_MAX); item->rot.z += LARA_LEAN_RATE; CLAMPG(item->rot.z, M_SPRINT_LEAN_MAX); } if (g_Input.jump && !item->gravity) { item->goal_anim_state = LS(LS_SPRINT_ROLL); } else if (g_Input.forward) { item->goal_anim_state = LS(g_Input.slow ? LS_WALK : LS_SPRINT); } else if (!g_Input.left && !g_Input.right) { item->goal_anim_state = LS(LS_STOP); } } static void M_SprintRoll(ITEM *const item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->sprinting = false; if (item->goal_anim_state != LS(LS_DEATH) && item->goal_anim_state != LS(LS_STOP) && item->goal_anim_state != LS(LS_RUN) && item->fall_speed > M_FAST_FALL_SPEED) { item->goal_anim_state = LS(LS_FAST_FALL); } } // clang-format off REGISTER_LARA_STATE(LS_GYMNAST, M_Default) REGISTER_LARA_STATE(LS_PULL_UP, M_PullUp) REGISTER_LARA_STATE(LS_WALK, M_Walk) REGISTER_LARA_STATE(LS_RUN, M_Run) REGISTER_LARA_STATE(LS_STOP, M_Stop) REGISTER_LARA_STATE(LS_POSE, M_Pose) REGISTER_LARA_STATE(LS_POSE_START, M_Pose) REGISTER_LARA_STATE(LS_POSE_END, M_Pose) REGISTER_LARA_STATE(LS_FAST_BACK, M_FastBack) REGISTER_LARA_STATE(LS_TURN_RIGHT, M_Turn) REGISTER_LARA_STATE(LS_TURN_LEFT, M_Turn) REGISTER_LARA_STATE(LS_FAST_TURN, M_FastTurn) REGISTER_LARA_STATE(LS_DEATH, M_Default) REGISTER_LARA_STATE(LS_WALK_BACK, M_WalkBack) REGISTER_LARA_STATE(LS_STEP_RIGHT, M_SideStep) REGISTER_LARA_STATE(LS_STEP_LEFT, M_SideStep) REGISTER_LARA_STATE(LS_SLIDE, M_Slide) REGISTER_LARA_STATE(LS_SLIDE_BACK, M_Slide) REGISTER_LARA_STATE(LS_ROLL, M_Roll) REGISTER_LARA_STATE(LS_ROLL_CONT, M_Roll) REGISTER_LARA_STATE(LS_PUSH_BLOCK, M_PushBlock) REGISTER_LARA_STATE(LS_PULL_BLOCK, M_PushBlock) REGISTER_LARA_STATE(LS_PP_READY, M_PPReady) REGISTER_LARA_STATE(LS_PICKUP, M_Pickup) REGISTER_LARA_STATE(LS_SWITCH_ON, M_SwitchOn) REGISTER_LARA_STATE(LS_SWITCH_OFF, M_SwitchOn) REGISTER_LARA_STATE(LS_USE_KEY, M_UseKey) REGISTER_LARA_STATE(LS_USE_PUZZLE, M_UseKey) REGISTER_LARA_STATE(LS_SPECIAL, M_Special) REGISTER_LARA_STATE(LS_WADE, M_Wade) REGISTER_LARA_STATE(LS_SPRINT, M_Sprint) REGISTER_LARA_STATE(LS_SPRINT_ROLL, M_SprintRoll) REGISTER_LARA_STATE(LS_CONTROLLED, M_Default) REGISTER_LARA_STATE(LS_FLARE_PICKUP, M_Pickup) // clang-format on ================================================ FILE: src/trx/game/lara/state/monkey.c ================================================ #include #include #include #include #include #include #include #include // clang-format off #define M_CAM_MONKEY_ELEVATION (10 * DEG_1) // = 1820 #define M_CAM_HANG_ANGLE 0 #define M_CAM_HANG_ELEVATION (-60 * DEG_1) // = -10920 #define M_MONKEY_TURN ((DEG_1 * 1) + LARA_TURN_UNDO) // = 546 // clang-format on static bool M_CanMonkeySwing(const ITEM *const item) { int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector( (XYZ_32) { item->pos.x, MAX_HEIGHT, item->pos.z }, &room_num); return (sector->ladder & LADDER_CEILING) != 0; } static void M_MonkeyIdle(ITEM *const item, COLL_INFO *const coll) { coll->enable_hit = 0; coll->enable_baddie_push = 0; if (M_CanMonkeySwing(item)) { if (g_Input.action && item->hit_points > 0) { g_Camera.target_angle = M_CAM_HANG_ANGLE; g_Camera.target_elevation = M_CAM_HANG_ELEVATION; } return; } if (g_Config.gameplay.look_mode != LOOK_MODE_RESTRICTED && g_Input.look) { Lara_Look_UpDown(); } } static void M_MonkeyForward(ITEM *const item, COLL_INFO *const coll) { coll->enable_hit = 0; coll->enable_baddie_push = 0; if (item->hit_points <= 0) { item->goal_anim_state = LS(LS_MONKEY_IDLE); return; } LARA_INFO *const lara = Lara_GetLaraInfo(); lara->move_angle = item->rot.y; if (g_Input.forward) { item->goal_anim_state = LS(LS_MONKEY_FORWARD); } else { item->goal_anim_state = LS(LS_MONKEY_IDLE); } if (g_Input.left) { lara->turn_rate -= LARA_TURN_RATE; CLAMPL(lara->turn_rate, -M_MONKEY_TURN); } else if (g_Input.right) { lara->turn_rate += LARA_TURN_RATE; CLAMPG(lara->turn_rate, +M_MONKEY_TURN); } } static void M_MonkeyShimmy(ITEM *const item, COLL_INFO *const coll) { coll->enable_hit = 0; coll->enable_baddie_push = 0; g_Camera.target_elevation = M_CAM_MONKEY_ELEVATION; LARA_INFO *const lara = Lara_GetLaraInfo(); if (item->current_anim_state == LS(LS_MONKEY_LEFT)) { lara->move_angle = item->rot.y - DEG_90; } else { lara->move_angle = item->rot.y + DEG_90; } const bool stop = item->current_anim_state == LS(LS_MONKEY_LEFT) ? !g_Input.step_left : !g_Input.step_right; if (stop) { item->goal_anim_state = LS(LS_MONKEY_IDLE); } } static void M_MonkeyTurn(ITEM *const item, COLL_INFO *const coll) { coll->enable_hit = 0; coll->enable_baddie_push = 0; g_Camera.target_elevation = M_CAM_MONKEY_ELEVATION; if (item->hit_points <= 0) { item->goal_anim_state = LS(LS_MONKEY_IDLE); return; } if (item->current_anim_state == LS(LS_MONKEY_TURN_LEFT)) { item->rot.y -= LARA_LEAN_RATE; if (!g_Input.left) { item->goal_anim_state = LS(LS_MONKEY_IDLE); } } else { item->rot.y += LARA_LEAN_RATE; if (!g_Input.right) { item->goal_anim_state = LS(LS_MONKEY_IDLE); } } } static void M_MonkeyRoll(ITEM *const item, COLL_INFO *const coll) { coll->enable_hit = 0; coll->enable_baddie_push = 0; item->goal_anim_state = LS(LS_MONKEY_IDLE); } // clang-format off REGISTER_LARA_STATE(LS_MONKEY_IDLE, M_MonkeyIdle) REGISTER_LARA_STATE(LS_MONKEY_FORWARD, M_MonkeyForward) REGISTER_LARA_STATE(LS_MONKEY_LEFT, M_MonkeyShimmy) REGISTER_LARA_STATE(LS_MONKEY_RIGHT, M_MonkeyShimmy) REGISTER_LARA_STATE(LS_MONKEY_TURN_LEFT, M_MonkeyTurn) REGISTER_LARA_STATE(LS_MONKEY_TURN_RIGHT, M_MonkeyTurn) REGISTER_LARA_STATE(LS_MONKEY_ROLL, M_MonkeyRoll) // clang-format on ================================================ FILE: src/trx/game/lara/state/swim.c ================================================ #include #include #include #include #include // clang-format off #define M_FRICTION 6 #define M_LEAN_RATE (2 * LARA_LEAN_RATE) // = 546 #define M_TURN_RATE (2 * DEG_1) // = 364 #define M_MAX_SURF_SPEED 60 #define M_MAX_SWIM_SPEED 200 // clang-format on static void M_SwimTurn(ITEM *const item) { if (g_Input.forward) { item->rot.x -= M_TURN_RATE; } else if (g_Input.back) { item->rot.x += M_TURN_RATE; } if (g_Config.gameplay.enable_tr2_swimming) { LARA_INFO *const lara = Lara_GetLaraInfo(); if (g_Input.left) { lara->turn_rate -= LARA_TURN_RATE; CLAMPL(lara->turn_rate, -LARA_MED_TURN); item->rot.z -= M_LEAN_RATE; } else if (g_Input.right) { lara->turn_rate += LARA_TURN_RATE; CLAMPG(lara->turn_rate, LARA_MED_TURN); item->rot.z += M_LEAN_RATE; } } else { if (g_Input.left) { item->rot.y -= LARA_MED_TURN; item->rot.z -= M_LEAN_RATE; } else if (g_Input.right) { item->rot.y += LARA_MED_TURN; item->rot.z += M_LEAN_RATE; } } } static void M_Tread(ITEM *const item, COLL_INFO *const coll) { if (item->hit_points <= 0) { item->goal_anim_state = LS(LS_UW_DEATH); return; } coll->enable_hit = 0; if (g_Config.gameplay.enable_uw_roll && g_Input.roll) { item->current_anim_state = LS(LS_WATER_ROLL); Item_SwitchToAnim(item, LA(LA_UNDERWATER_ROLL_START), 0); return; } if (g_Config.gameplay.look_mode != LOOK_MODE_RESTRICTED && g_Input.look) { Lara_Look_UpDown(); } M_SwimTurn(item); if (g_Input.jump) { item->goal_anim_state = LS(LS_SWIM); } item->fall_speed -= M_FRICTION; CLAMPL(item->fall_speed, 0); LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->gun_status == LGS_HANDS_BUSY) { lara->gun_status = LGS_ARMLESS; } } static void M_Swim(ITEM *const item, COLL_INFO *const coll) { if (item->hit_points <= 0) { item->goal_anim_state = LS(LS_UW_DEATH); return; } coll->enable_hit = 0; if (g_Config.gameplay.enable_uw_roll && g_Input.roll) { item->current_anim_state = LS(LS_WATER_ROLL); Item_SwitchToAnim(item, LA(LA_UNDERWATER_ROLL_START), 0); return; } M_SwimTurn(item); item->fall_speed += 8; LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->water_status == LWS_CHEAT) { CLAMPG(item->fall_speed, M_MAX_SWIM_SPEED * 2); } else { CLAMPG(item->fall_speed, M_MAX_SWIM_SPEED); } if (!g_Input.jump) { item->goal_anim_state = LS(g_Config.gameplay.enable_tr2_swim_cancel && Lara_State_IsResponsive(LA_UNDERWATER_SWIM_FORWARD) ? LS_RESPONSIVE : LS_GLIDE); } } static void M_Glide(ITEM *item, COLL_INFO *coll) { if (item->hit_points <= 0) { item->goal_anim_state = LS(LS_UW_DEATH); return; } coll->enable_hit = 0; if (g_Config.gameplay.enable_uw_roll && g_Input.roll) { item->current_anim_state = LS(LS_WATER_ROLL); Item_SwitchToAnim(item, LA(LA_UNDERWATER_ROLL_START), 0); return; } M_SwimTurn(item); if (g_Input.jump) { item->goal_anim_state = LS(LS_SWIM); } item->fall_speed -= M_FRICTION; CLAMPL(item->fall_speed, 0); if (item->fall_speed <= M_MAX_SWIM_SPEED * 2 / 3) { item->goal_anim_state = LS(LS_TREAD); } } static void M_TreadSurface(ITEM *const item, COLL_INFO *const coll) { item->fall_speed -= 4; CLAMPL(item->fall_speed, 0); if (item->hit_points <= 0) { item->goal_anim_state = LS(LS_UW_DEATH); return; } coll->enable_hit = 0; if (g_Input.look) { Lara_Look_UpDown(); return; } if (g_Input.left) { item->rot.y -= LARA_SLOW_TURN; } else if (g_Input.right) { item->rot.y += LARA_SLOW_TURN; } if (g_Input.forward) { item->goal_anim_state = LS(LS_SURF_SWIM); } else if (g_Input.back) { item->goal_anim_state = LS(LS_SURF_BACK); } if (g_Input.step_left) { item->goal_anim_state = LS(LS_SURF_LEFT); } else if (g_Input.step_right) { item->goal_anim_state = LS(LS_SURF_RIGHT); } LARA_INFO *const lara = Lara_GetLaraInfo(); if (g_Input.jump) { lara->dive_timer++; if (lara->dive_timer == LARA_DIVE_WAIT) { Item_SwitchToAnim(item, LA(LA_ONWATER_DIVE), 0); item->goal_anim_state = LS(LS_SWIM); item->current_anim_state = LS(LS_DIVE); item->rot.x = -45 * DEG_1; item->fall_speed = 80; lara->water_status = LWS_UNDERWATER; } } else { lara->dive_timer = 0; } } static void M_ForwardSurface(ITEM *const item, COLL_INFO *const coll) { if (item->hit_points <= 0) { item->goal_anim_state = LS(LS_UW_DEATH); return; } coll->enable_hit = 0; LARA_INFO *const lara = Lara_GetLaraInfo(); lara->dive_timer = 0; if (g_Input.left) { item->rot.y -= LARA_SLOW_TURN; } else if (g_Input.right) { item->rot.y += LARA_SLOW_TURN; } if (!g_Input.forward || g_Input.jump) { item->goal_anim_state = LS(LS_SURF_TREAD); } item->fall_speed += 8; CLAMPG(item->fall_speed, M_MAX_SURF_SPEED); } static void M_SideBackSurface(ITEM *const item, COLL_INFO *const coll) { if (item->hit_points <= 0) { item->goal_anim_state = LS(LS_UW_DEATH); return; } coll->enable_hit = 0; LARA_INFO *const lara = Lara_GetLaraInfo(); lara->dive_timer = 0; if (g_Input.left) { item->rot.y -= M_TURN_RATE; } else if (g_Input.right) { item->rot.y += M_TURN_RATE; } bool stop = false; switch (LS_U(item->current_anim_state)) { case LS_SURF_BACK: stop = !g_Input.back; break; case LS_SURF_LEFT: stop = !g_Input.step_left; break; case LS_SURF_RIGHT: stop = !g_Input.step_right; break; default: break; } if (stop) { item->goal_anim_state = LS(LS_SURF_TREAD); } item->fall_speed += 8; CLAMPG(item->fall_speed, M_MAX_SURF_SPEED); } static void M_Dive(ITEM *const item, COLL_INFO *const coll) { if (g_Input.forward) { item->rot.x -= DEG_1; } } static void M_UWDeath(ITEM *const item, COLL_INFO *const coll) { coll->enable_hit = 0; item->gravity = false; item->fall_speed -= 8; CLAMPL(item->fall_speed, 0); if (item->rot.x >= -M_TURN_RATE && item->rot.x <= M_TURN_RATE) { item->rot.x = 0; } else if (item->rot.x >= 0) { item->rot.x -= M_TURN_RATE; } else { item->rot.x += M_TURN_RATE; } } static void M_WaterOut(ITEM *const item, COLL_INFO *const coll) { coll->enable_hit = 0; coll->enable_baddie_push = 0; g_Camera.flags = CF_FOLLOW_CENTRE; } static void M_UWTwist(ITEM *const item, COLL_INFO *const coll) { item->fall_speed = 0; item->goal_anim_state = LS(LS_TREAD); } // clang-format off REGISTER_LARA_STATE(LS_TREAD, M_Tread) REGISTER_LARA_STATE(LS_SWIM, M_Swim) REGISTER_LARA_STATE(LS_GLIDE, M_Glide) REGISTER_LARA_STATE(LS_SURF_TREAD, M_TreadSurface) REGISTER_LARA_STATE(LS_SURF_SWIM, M_ForwardSurface) REGISTER_LARA_STATE(LS_DIVE, M_Dive) REGISTER_LARA_STATE(LS_UW_DEATH, M_UWDeath) REGISTER_LARA_STATE(LS_SURF_BACK, M_SideBackSurface) REGISTER_LARA_STATE(LS_SURF_LEFT, M_SideBackSurface) REGISTER_LARA_STATE(LS_SURF_RIGHT, M_SideBackSurface) REGISTER_LARA_STATE(LS_WATER_OUT, M_WaterOut) REGISTER_LARA_STATE(LS_WATER_ROLL, M_UWTwist) // clang-format on ================================================ FILE: src/trx/game/lara/state.c ================================================ #include #include #include static const LARA_TRX_ANIMATION m_TestResponsiveAnims[] = { // clang-format off LA_RUN, LA_UNDERWATER_SWIM_FORWARD, LA_SLIDE_FORWARD, LA_STAND_TO_JUMP, LA_REACH_TO_HANG, LA_TRX_INVALID, // clang-format on }; static bool m_ResponsiveAnims[LA_NUMBER_OF] = {}; static void (*m_StateRoutines[LS_NUMBER_OF])(ITEM *item, COLL_INFO *coll) = {}; static void (*m_ExtraRoutines[LS_EXTRA_NUMBER_OF])( ITEM *item, COLL_INFO *coll) = {}; static bool M_HasResponsiveState(const LARA_TRX_ANIMATION anim_idx) { const OBJECT *const obj = Object_Get(O_LARA); if (!obj->loaded) { return false; } const ANIM *const anim = Object_GetAnim(obj, LA(anim_idx)); for (int32_t i = 0; i < anim->num_changes; i++) { const ANIM_CHANGE *const change = Anim_GetChange(anim->change_idx + i); if (change->goal_anim_state == LS(LS_RESPONSIVE)) { return true; } } return false; } void Lara_State_Register( const LARA_TRX_STATE state, void (*const handle_func)(ITEM *item, COLL_INFO *coll)) { m_StateRoutines[state] = handle_func; } void Lara_State_RegisterExtra( const LARA_EXTRA_STATE state, void (*const handle_func)(ITEM *item, COLL_INFO *coll)) { ASSERT(state >= 0 && state < LS_EXTRA_NUMBER_OF); m_ExtraRoutines[state] = handle_func; } void Lara_State_Initialise(void) { for (int32_t i = 0; m_TestResponsiveAnims[i] != LA_TRX_INVALID; i++) { const LARA_TRX_ANIMATION anim = m_TestResponsiveAnims[i]; m_ResponsiveAnims[anim] = M_HasResponsiveState(anim); } } bool Lara_State_IsResponsive(const LARA_TRX_ANIMATION anim_idx) { return m_ResponsiveAnims[anim_idx]; } void Lara_State_Update(ITEM *const item, COLL_INFO *const coll) { const LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->water_status != LWS_SURFACE && lara->extra_anim) { if (m_ExtraRoutines[item->current_anim_state] != nullptr) { m_ExtraRoutines[item->current_anim_state](item, coll); } return; } const LARA_TRX_STATE state = LS_U(item->current_anim_state); if (state >= 0 && state < LS_NUMBER_OF) { if (m_StateRoutines[state] != nullptr) { m_StateRoutines[state](item, coll); } return; } } ================================================ FILE: src/trx/game/lara/state.h ================================================ #include #include void Lara_State_Register( LARA_TRX_STATE state, void (*handle_func)(ITEM *item, COLL_INFO *coll)); void Lara_State_RegisterExtra( LARA_EXTRA_STATE state, void (*handle_func)(ITEM *item, COLL_INFO *coll)); void Lara_State_Initialise(void); bool Lara_State_IsResponsive(LARA_TRX_ANIMATION anim_idx); void Lara_State_Update(ITEM *item, COLL_INFO *coll); ================================================ FILE: src/trx/game/lara/types.h ================================================ #pragma once #include #include #include #include #include #include #include #include typedef struct { ANIM_FRAME *frame_base; int16_t frame_num; int16_t anim_num; int16_t lock; XYZ_16 rot; int16_t flash_gun; struct { struct { XYZ_16 rot; } result, prev; } interp; } LARA_ARM; typedef struct { int32_t ammo; } AMMO_INFO; typedef struct { int16_t item_num; LARA_GUN_STATE gun_status; LARA_GUN_TYPE gun_type; LARA_GUN_TYPE request_gun_type; LARA_GUN_TYPE last_gun_type; LARA_WATER_STATE water_status; int32_t water_surface_dist; int16_t turn_rate; int16_t move_angle; XYZ_16 head_rot; XYZ_16 torso_rot; int16_t calc_fall_speed; int16_t pose_count; int16_t hit_frame; int16_t hit_direction; int16_t air; int16_t dive_timer; int16_t death_timer; int16_t sprint_timer; int16_t exposure_timer; int16_t poison_timer; int32_t idle_timer; struct { int32_t active; XZ_16 vel; } current; LOT_INFO lot; XYZ_32 last_pos; int16_t hit_effect_count; EFFECT *hit_effect; int32_t mesh_effects; OBJECT_MESH *mesh_ptrs[LM_NUMBER_OF]; ITEM *target; int16_t target_angles[2]; LARA_ARM left_arm; LARA_ARM right_arm; AMMO_INFO pistol_ammo; AMMO_INFO magnum_ammo; AMMO_INFO autos_ammo; AMMO_INFO desert_eagle_ammo; AMMO_INFO uzi_ammo; AMMO_INFO shotgun_ammo; struct { struct { XYZ_16 head_rot; XYZ_16 torso_rot; } result, prev; } interp; bool extra_anim; bool burn; int16_t electric; bool climb_status; bool is_crouched; bool keep_crouched; bool killed_loyal_item; struct { int32_t item_num; int32_t move_count; bool is_moving; XYZ_32 initial_pos; } interact_target; LARA_GUN_TYPE holsters_gun_type; LARA_GUN_TYPE back_gun_type; int16_t gun_item_num; AMMO_INFO harpoon_ammo; AMMO_INFO grenade_ammo; AMMO_INFO rocket_ammo; AMMO_INFO m16_ammo; AMMO_INFO mp5_ammo; struct { bool control; int16_t age; int16_t frame_num; } flare; MATRIX mesh_pos_matrices[LM_NUMBER_OF]; bool mesh_pos_matrices_valid; // TR3: persistent gun smoke spawned from muzzle after firing. int32_t tr3_smoke_count_l; int32_t tr3_smoke_count_r; LARA_GUN_TYPE tr3_smoke_weapon; bool has_fired; // TRR modern controls stuff bool crouching; bool sprinting; } LARA_INFO; ================================================ FILE: src/trx/game/lara/util.h ================================================ #pragma once #include #include #define REGISTER_LARA_COL(state, handle_func) \ __attribute__((constructor)) static void M_RegisterColHandler##state(void) \ { \ Lara_Col_Register(state, handle_func); \ } #define REGISTER_LARA_STATE(state, handle_func) \ __attribute__((constructor)) static void M_RegisterStateHandler##state( \ void) \ { \ Lara_State_Register(state, handle_func); \ } #define REGISTER_LARA_EXTRA(state, handle_func) \ __attribute__((constructor)) static void \ M_RegisterExtraStateHandler##state(void) \ { \ Lara_State_RegisterExtra(state, handle_func); \ } ================================================ FILE: src/trx/game/lara/vehicle.c ================================================ #include #include #include #include static int16_t m_VehicleItemNum = NO_ITEM; bool Lara_Vehicle_IsMounted(void) { return m_VehicleItemNum != NO_ITEM; } bool Lara_Vehicle_IsOnType(const OBJECT_ID obj_id) { if (!Lara_Vehicle_IsMounted()) { return false; } const ITEM *const vehicle = Lara_Vehicle_GetItem(); return vehicle->object_id == obj_id; } void Lara_Vehicle_SetIndex(const int16_t item_num) { m_VehicleItemNum = item_num; } int16_t Lara_Vehicle_GetIndex(void) { return m_VehicleItemNum; } ITEM *Lara_Vehicle_GetItem(void) { return m_VehicleItemNum == NO_ITEM ? nullptr : Item_Get(m_VehicleItemNum); } void Lara_Vehicle_Dismount(void) { if (!Lara_Vehicle_IsMounted()) { return; } ITEM *const lara_item = Lara_GetItem(); ITEM *const vehicle = Lara_Vehicle_GetItem(); Item_SwitchToAnim(vehicle, 0, 0); Lara_Vehicle_SetIndex(NO_ITEM); lara_item->current_anim_state = LS(LS_STOP); lara_item->goal_anim_state = LS(LS_STOP); Item_SwitchToAnim(lara_item, LA(LA_STAND_STILL), 0); lara_item->rot.x = 0; lara_item->rot.z = 0; const LARA_SKIN_EQUIPMENT *const hand_r_equipment = Lara_Skin_GetEquipment(LM_HAND_R); if (hand_r_equipment->type == EQUIPMENT_TYPE_EXTRA && hand_r_equipment->data == EXTRA_MESH_OAR) { Lara_Skin_ClearEquipment(LM_HAND_R); Item_SetMeshVisibleMask(lara_item, INT32_MAX, true); } } ================================================ FILE: src/trx/game/lara/vehicle.h ================================================ #pragma once #include bool Lara_Vehicle_IsMounted(void); bool Lara_Vehicle_IsOnType(OBJECT_ID obj_id); void Lara_Vehicle_SetIndex(int16_t item_num); int16_t Lara_Vehicle_GetIndex(void); ITEM *Lara_Vehicle_GetItem(void); void Lara_Vehicle_Dismount(void); ================================================ FILE: src/trx/game/lara.h ================================================ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include ================================================ FILE: src/trx/game/level/cache.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #define M_CACHE_MAGIC UINT32_C(0x4C434831) typedef struct { uint32_t magic; uint64_t checksum; } M_CACHE_HEADER; typedef struct M_LEVEL_HASH_ENTRY { const GF_LEVEL *level; uint64_t hash; UT_hash_handle hh; } M_LEVEL_HASH_ENTRY; static M_LEVEL_HASH_ENTRY *m_LevelHashMap = nullptr; static void M_ClearLevelHashMap(void) { M_LEVEL_HASH_ENTRY *current = nullptr; M_LEVEL_HASH_ENTRY *tmp = nullptr; HASH_ITER(hh, m_LevelHashMap, current, tmp) { HASH_DEL(m_LevelHashMap, current); Memory_FreePointer(¤t); } } static __attribute__((constructor)) void M_Initialise(void) { m_LevelHashMap = nullptr; } static __attribute__((destructor)) void M_Shutdown(void) { M_ClearLevelHashMap(); } static void M_GetFileMeta( const char *const path, uint64_t *const out_size, uint64_t *const out_mtime) { uint64_t size = 0; uint64_t mtime = 0; File_GetMeta(path, &size, &mtime); if (out_size != nullptr) { *out_size = size; } if (out_mtime != nullptr) { *out_mtime = mtime; } } static uint64_t M_ComputeLevelHash(const GF_LEVEL *const level) { uint64_t checksum = Hash_FNV1a64_Init(); checksum = Hash_FNV1a64_UpdateU32(checksum, (uint32_t)level->num); checksum = Hash_FNV1a64_UpdateU32(checksum, (uint32_t)level->type); checksum = Hash_FNV1a64_UpdateString(checksum, level->path); checksum = Hash_FNV1a64_UpdateU32(checksum, (uint32_t)level->injections.count); if (level->path != nullptr) { uint64_t file_size = 0; uint64_t file_mtime = 0; M_GetFileMeta(level->path, &file_size, &file_mtime); checksum = Hash_FNV1a64_UpdateU64(checksum, file_size); checksum = Hash_FNV1a64_UpdateU64(checksum, file_mtime); } for (int32_t i = 0; i < level->injections.count; i++) { const char *const path = level->injections.data_paths[i]; uint64_t file_size = 0; uint64_t file_mtime = 0; checksum = Hash_FNV1a64_UpdateString(checksum, path); if (path != nullptr) { M_GetFileMeta(path, &file_size, &file_mtime); checksum = Hash_FNV1a64_UpdateU64(checksum, file_size); checksum = Hash_FNV1a64_UpdateU64(checksum, file_mtime); } } return checksum; } static uint64_t M_GetLevelHash(const GF_LEVEL *const level) { M_LEVEL_HASH_ENTRY *entry = nullptr; HASH_FIND_PTR(m_LevelHashMap, &level, entry); if (entry != nullptr) { return entry->hash; } entry = Memory_Alloc(sizeof(*entry)); entry->level = level; entry->hash = M_ComputeLevelHash(level); HASH_ADD_PTR(m_LevelHashMap, level, entry); return entry->hash; } uint64_t LevelCache_InitChecksum( const char *const scope, const uint32_t version) { uint64_t checksum = Hash_FNV1a64_Init(); checksum = Hash_FNV1a64_UpdateString(checksum, scope); checksum = Hash_FNV1a64_UpdateU32(checksum, version); return checksum; } uint64_t LevelCache_UpdateLevelChecksum( uint64_t checksum, const GF_LEVEL *const level) { if (level == nullptr) { return checksum; } checksum = Hash_FNV1a64_UpdateU64(checksum, M_GetLevelHash(level)); return checksum; } const char *LevelCache_GetLevelKey(const GF_LEVEL *const level) { if (level == nullptr) { return nullptr; } char type_key = 'u'; switch (level->type) { case GFL_TITLE: type_key = 't'; break; case GFL_NORMAL: type_key = 'l'; break; case GFL_CUTSCENE: type_key = 'c'; break; case GFL_DEMO: type_key = 'd'; break; case GFL_GYM: type_key = 'g'; break; case GFL_BONUS: type_key = 'b'; break; case GFL_DUMMY: type_key = 'x'; break; case GFL_CURRENT: type_key = 'r'; break; } const char *name = level->path != nullptr ? level->path : "unknown"; const char *const slash = strrchr(name, '/'); const char *const backslash = strrchr(name, '\\'); if (slash != nullptr && backslash != nullptr) { name = slash > backslash ? slash + 1 : backslash + 1; } else if (slash != nullptr) { name = slash + 1; } else if (backslash != nullptr) { name = backslash + 1; } const size_t stem_len = strcspn(name, "."); return String_FormatStatic( "%c%d_%.*s", type_key, level->num, (int)stem_len, name); } static const char *M_GetPath(const char *const filename) { const SHELL_ARGS *const args = Shell_GetArgs(); if (args == nullptr || args->mod == nullptr || args->mod->name == nullptr) { return nullptr; } return String_FormatStatic( "%s/%s/%s", Shell_GetCacheDir(), args->mod->name, filename); } MYFILE *LevelCache_OpenBinaryRead( const char *const filename, const uint64_t checksum) { const char *const path = M_GetPath(filename); if (path == nullptr) { return nullptr; } MYFILE *const file = File_Open(path, FILE_OPEN_READ); if (file == nullptr) { return nullptr; } M_CACHE_HEADER header; if (!File_ReadData(file, &header, sizeof(header)) || header.magic != M_CACHE_MAGIC || header.checksum != checksum) { File_Close(file); return nullptr; } return file; } MYFILE *LevelCache_OpenBinaryWrite( const char *const filename, const uint64_t checksum) { const char *const path = M_GetPath(filename); if (path == nullptr) { return nullptr; } File_EnsureParentDirectories(path); MYFILE *const file = File_Open(path, FILE_OPEN_WRITE); if (file == nullptr) { return nullptr; } const M_CACHE_HEADER header = { .magic = M_CACHE_MAGIC, .checksum = checksum, }; File_WriteData(file, &header, sizeof(header)); return file; } JSON_VALUE *LevelCache_ReadJSON( const char *const filename, const uint64_t checksum) { const char *const path = M_GetPath(filename); if (path == nullptr) { return nullptr; } JSON_VALUE *const root = JSONFile_Read(path); if (root == nullptr) { return nullptr; } JSON_OBJECT *const root_obj = JSON_ValueAsObject(root); const char *const checksum_str = root_obj != nullptr ? JSON_ObjectGetString(root_obj, "checksum", nullptr) : nullptr; if (checksum_str == nullptr || (uint64_t)strtoull(checksum_str, nullptr, 16) != checksum) { JSON_ValueFree(root); return nullptr; } return root; } bool LevelCache_WriteJSON( const char *const filename, const uint64_t checksum, JSON_VALUE *const root) { const char *const path = M_GetPath(filename); JSON_OBJECT *const root_obj = root != nullptr ? JSON_ValueAsObject(root) : nullptr; if (path == nullptr || root_obj == nullptr) { return false; } File_EnsureParentDirectories(path); JSON_ObjectAppendString( root_obj, "checksum", String_FormatStatic("%016" PRIx64, checksum)); return JSONFile_Write(path, root); } ================================================ FILE: src/trx/game/level/cache.h ================================================ #pragma once #include #include #include #include #include uint64_t LevelCache_InitChecksum(const char *scope, uint32_t version); uint64_t LevelCache_UpdateLevelChecksum( uint64_t checksum, const GF_LEVEL *level); const char *LevelCache_GetLevelKey(const GF_LEVEL *level); MYFILE *LevelCache_OpenBinaryRead(const char *filename, uint64_t checksum); MYFILE *LevelCache_OpenBinaryWrite(const char *filename, uint64_t checksum); JSON_VALUE *LevelCache_ReadJSON(const char *filename, uint64_t checksum); bool LevelCache_WriteJSON( const char *filename, uint64_t checksum, JSON_VALUE *root); ================================================ FILE: src/trx/game/level/common.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include void Level_Unload(void) { Music_ResetTrackFlags(); Sound_ResetSamples(); Lara_InitialiseLoad(NO_ITEM); Gym_TrackManager_Reset(GYM_TRACK_ASSAULT); Gym_TrackManager_Reset(GYM_TRACK_QUAD); Creature_Reset(); Object_Reset(); Camera_Reset(); Walkable_Reset(); Output_SetTimeInGame(0.0f); Output_DispatchLevelUnload(); Sound_StopAll(); Viewport_AlterFOV(-1, FOV_MODE_GAME); } bool Level_Initialise( const GF_LEVEL *const level, const GF_SEQUENCE_CONTEXT seq_ctx) { BENCHMARK benchmark = Benchmark_Start(); LOG_DEBUG("num=%d (%s)", level->num, level->path); if (level->type == GFL_DEMO) { Random_SeedDraw(0xD371F947); Random_SeedControl(0xD371F947); } Game_SetIsLevelComplete(false); if (level->type != GFL_TITLE && level->type != GFL_DEMO) { Gym_SetInventoryOpenEnabled(false); } if (level->type != GFL_TITLE && level->type != GFL_CUTSCENE) { Game_SetCurrentLevel(level); } GF_SetCurrentLevel(level); if (level->type != GFL_TITLE) { // TODO: move me elsewhere RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); if (resume != nullptr) { resume->stats.timer = 0; resume->stats.secret_flags = 0; resume->stats.secret_count = 0; resume->stats.pickup_count = 0; resume->stats.kill_count = 0; resume->stats.ammo_hits = 0; resume->stats.ammo_used = 0; resume->stats.medipacks_used = 0; resume->stats.distance_travelled = 0; } } if (level == nullptr) { return false; } Level_Unload(); Lua_FireEventInt32(LUA_EVENT_BEFORE_LEVEL_FILE, level->num); Level_Pipeline_Load(level); UI_LoadText(); Output_SetSkyboxEnabled(Object_Get(O_SKYBOX)->loaded); Output_DispatchLevelLoad(); GameStringTable_Apply(level); Effect_InitialiseArray(); LOT_InitialiseArray(); FX_Reset(); FX_Weather_SetWeather(level->weather_type); Sparks_Reset(); Option_Reset(); Overlay_Reset(); Overlay_SetHealthBarTimer(100); Benchmark_End(&benchmark, nullptr); return true; } ================================================ FILE: src/trx/game/level/common.h ================================================ #pragma once #include void Level_Unload(void); bool Level_Initialise(const GF_LEVEL *level, GF_SEQUENCE_CONTEXT seq_ctx); ================================================ FILE: src/trx/game/level/context.c ================================================ #include #include static LEVEL_CONTEXT m_LoadContext = {}; void Level_Context_Reset(const LEVEL_FORMAT_LOADER *const loader) { memset(&m_LoadContext, 0, sizeof(m_LoadContext)); m_LoadContext.loader = loader; } LEVEL_CONTEXT *Level_Context_Get(void) { return &m_LoadContext; } LEVEL_CONTEXT_INFO *Level_Context_GetInfo(void) { return &m_LoadContext.info; } ================================================ FILE: src/trx/game/level/context.h ================================================ #pragma once #include typedef struct LEVEL_FORMAT_LOADER LEVEL_FORMAT_LOADER; typedef struct { struct { int32_t anim_count; int32_t change_count; int32_t range_count; int32_t command_count; int16_t *commands; int32_t bone_count; int32_t frame_count; int16_t *frames; } anims; struct { int32_t object_count; int32_t sprite_count; int32_t page_count; uint8_t *pages_8; RGBA_8888 *pages_32; } textures; struct { int32_t size; RGB_888 *data_24; RGB_888 *data_32; } palette; struct { int32_t offset_count; int32_t *offsets; // TR1-specific int32_t data_size; char *data; } samples; int32_t mesh_ptr_count; } LEVEL_CONTEXT_INFO; typedef struct { LEVEL_CONTEXT_INFO info; const LEVEL_FORMAT_LOADER *loader; } LEVEL_CONTEXT; void Level_Context_Reset(const LEVEL_FORMAT_LOADER *loader); LEVEL_CONTEXT *Level_Context_Get(void); LEVEL_CONTEXT_INFO *Level_Context_GetInfo(void); ================================================ FILE: src/trx/game/level/finalize/animations.c ================================================ #include #include #include void Level_Finalize_LoadAnimCommands(LEVEL_CONTEXT *const ctx) { LEVEL_CONTEXT_INFO *const info = &ctx->info; Anim_LoadCommands(info->anims.commands); Memory_FreePointer(&info->anims.commands); } void Level_Finalize_LoadAnimFrames(LEVEL_CONTEXT *const ctx) { const LEVEL_FORMAT_LOADER *const loader = ctx->loader; LEVEL_CONTEXT_INFO *const info = &ctx->info; const int32_t frame_count = Anim_GetTotalFrameCount(loader, info->anims.frame_count); Anim_InitialiseFrames(frame_count); Anim_LoadFrames(loader, info->anims.frames, info->anims.frame_count); Memory_FreePointer(&info->anims.frames); } ================================================ FILE: src/trx/game/level/finalize/gameplay_objects.c ================================================ #include #include #include #include #include #include #include #include static uint8_t M_GetAIBit(const OBJECT_ID object_id) { switch (object_id) { // clang-format off case O_AI_GUARD: return AI_GUARD; case O_AI_AMBUSH: return AI_AMBUSH; case O_AI_PATROL_1: return AI_PATROL_1; case O_AI_MODIFY: return AI_MODIFY; case O_AI_FOLLOW: return AI_FOLLOW; // clang-format on default: return 0; } } static void M_AssignAIBits(void) { const int32_t item_count = Item_GetLevelCount(); for (int32_t i = 0; i < item_count; i++) { ITEM *const item = Item_Get(i); const OBJECT *const obj = Object_Get(item->object_id); if (!obj->intelligent || item->room_num == NO_ROOM) { continue; } ROOM *const room = Room_Get(item->room_num); int16_t ai_item_num = room->item_num; while (ai_item_num != NO_ITEM) { ITEM *const ai_item = Item_Get(ai_item_num); const int16_t next_num = ai_item->next_item; const uint8_t ai_bit = M_GetAIBit(ai_item->object_id); if (ai_bit != 0 && ai_item->pos.x == item->pos.x && ai_item->pos.z == item->pos.z) { item->ai_bits |= ai_bit; item->ai_tag = ai_item->rot.y; if (!(ai_item->object_id == O_AI_PATROL_1 && (GF_BadGetLevelNum() == 15 || GF_BadGetLevelNum() == 14))) { Item_Kill(ai_item_num); ai_item->room_num = NO_ROOM; } } ai_item_num = next_num; } } } void Level_Finalize_LoadObjectsAndItems(LEVEL_CONTEXT *const ctx) { // Object and item setup/initialisation must take place after injections // have been processed. A cached item count must be used as individual // initialisations may increment the total item count. Object_SetupAllObjects(); Walkable_ResetLevel(); const int32_t item_count = Item_GetLevelCount(); for (int32_t i = 0; i < item_count; i++) { Item_Initialise(i); } // Must take place after item initialization. Level_Finalize_LoadWalkables(ctx); M_AssignAIBits(); Lara_State_Initialise(); } void Level_Finalize_LoadWalkables(LEVEL_CONTEXT *const ctx) { for (int32_t item_num = 0; item_num < Item_GetLevelCount(); item_num++) { ITEM *const item = Item_Get(item_num); const OBJECT *const obj = Object_Get(item->object_id); if (obj->add_walkable_func != nullptr) { obj->add_walkable_func(item_num); } } } ================================================ FILE: src/trx/game/level/finalize/render_assets.c ================================================ #include #include #include #include #include #include #include #include #include #include static void M_FixTrapezoidRatios(FACE *const face, const XYZ_16 vertices[4]) { // This function attempts to correct texture coordinate ratios for a // quadrilateral so the GPU, which typically renders a quad as two // triangles, does not warp the texture disproportionately across the quad's // diagonal divisions. By comparing the 3D edge lengths (in world space) // with the corresponding 2D UV edge lengths, it computes a scale factor // that preserves the trapezoidal shape in texture space and allows the // shader to warp the texture uniformly across all four corners. // // In many software rasterization or older GPU pipelines, triangles can get // rendered using affine interpolation of texture coordinates, causing // visible warping when a four-sided polygon is split internally. // The original approach (coded by XProger) handled only rectangular UV // maps by simply scaling the edges; this updated version takes into // account the actual UV trapezoid to achieve a more uniform texture // projection. // 1) Gather the coordinate and texture information const OBJECT_TEXTURE *const tex = Output_GetObjectTexture(face->texture_idx); const TEXTURE_UV *uvs[4] = { &tex->uv[0], &tex->uv[1], &tex->uv[2], &tex->uv[3], }; TEXTURE_ZW_F *zws[4] = { &face->texture_zw[0], &face->texture_zw[1], &face->texture_zw[2], &face->texture_zw[3], }; XYZ_F c0, c1, c2, c3, *coords[4] = { &c0, &c1, &c2, &c3 }; for (int32_t i = 0; i < 4; i++) { coords[i]->x = vertices[i].x; coords[i]->y = vertices[i].y; coords[i]->z = vertices[i].z; } // 2) Compute geometric edges // a = c0-c1, b = c3-c2, c = c0-c3, d = c1-c2 const XYZ_F a = XYZ_F_Subtract(c0, c1); const XYZ_F b = XYZ_F_Subtract(c3, c2); const XYZ_F c = XYZ_F_Subtract(c0, c3); const XYZ_F d = XYZ_F_Subtract(c1, c2); const float a_l = XYZ_F_Length(a); const float b_l = XYZ_F_Length(b); const float c_l = XYZ_F_Length(c); const float d_l = XYZ_F_Length(d); // 3) Compute dot-products in 3D to see which edges differ more const float ab = XYZ_F_DotProduct(a, b) / (a_l * b_l); const float cd = XYZ_F_DotProduct(c, d) / (c_l * d_l); // 4) Compute tx, ty in for orientation const float tx = ABS(uvs[0]->u - uvs[3]->u); const float ty = ABS(uvs[0]->v - uvs[3]->v); // 5) Measure the same edges in UV space so we know the "current" shape XYZ_F uv0 = { uvs[0]->u, uvs[0]->v, 0.0f }; XYZ_F uv1 = { uvs[1]->u, uvs[1]->v, 0.0f }; XYZ_F uv2 = { uvs[2]->u, uvs[2]->v, 0.0f }; XYZ_F uv3 = { uvs[3]->u, uvs[3]->v, 0.0f }; // au = uv0 - uv1, bu = uv3 - uv2, cu = uv0 - uv3, du = uv1 - uv2 const XYZ_F au = XYZ_F_Subtract(uv0, uv1); const XYZ_F bu = XYZ_F_Subtract(uv3, uv2); const XYZ_F cu = XYZ_F_Subtract(uv0, uv3); const XYZ_F du = XYZ_F_Subtract(uv1, uv2); const float au_l = XYZ_F_Length(au); const float bu_l = XYZ_F_Length(bu); const float cu_l = XYZ_F_Length(cu); const float du_l = XYZ_F_Length(du); // We'll reuse the same ab/cd dot logic in UV if needed, but typically // we only need the lengths to find the ratio vs. geometry. // 6) Figure out the correction ratios per-corner, taking care of both // geometry and UV mesh proportions if (ab > cd) { const int k = (tx > ty) ? 1 : 0; // pick axis if (a_l > b_l) { // geometry ratio = (b_l / a_l) // uv ratio = (bu_l / au_l) (avoid /0 check if needed) const float geom_ratio = (a_l > 1e-6f) ? (b_l / a_l) : 1.0f; const float uv_ratio = (au_l > 1e-6f) ? (bu_l / au_l) : 1.0f; const float fix = geom_ratio / uv_ratio; // final scale zws[2]->zw[k] = fix; zws[3]->zw[k] = fix; } else if (a_l < b_l) { const float geom_ratio = (b_l > 1e-6f) ? (a_l / b_l) : 1.0f; const float uv_ratio = (bu_l > 1e-6f) ? (au_l / bu_l) : 1.0f; const float fix = geom_ratio / uv_ratio; zws[0]->zw[k] = fix; zws[1]->zw[k] = fix; } } else if (ab < cd) { const int k = (tx > ty) ? 0 : 1; // pick axis if (c_l > d_l) { const float geom_ratio = (c_l > 1e-6f) ? (d_l / c_l) : 1.0f; const float uv_ratio = (cu_l > 1e-6f) ? (du_l / cu_l) : 1.0f; const float fix = geom_ratio / uv_ratio; zws[1]->zw[k] = fix; zws[2]->zw[k] = fix; } else if (c_l < d_l) { const float geom_ratio = (d_l > 1e-6f) ? (c_l / d_l) : 1.0f; const float uv_ratio = (du_l > 1e-6f) ? (cu_l / du_l) : 1.0f; const float fix = geom_ratio / uv_ratio; zws[0]->zw[k] = fix; zws[3]->zw[k] = fix; } } } static void M_PremultiplyTexturePage(void *userdata) { const int32_t page = *(int32_t *)userdata; Output_LockTexturePage32(page); RGBA_8888 *ptr = Output_GetTexturePage32(page); const float inv255 = 1.0f / 255.0f; for (int32_t i = 0; i < TEXTURE_PAGE_SIZE; i++, ptr++) { ptr->r *= ptr->a * inv255; ptr->g *= ptr->a * inv255; ptr->b *= ptr->a * inv255; } Output_UnlockTexturePage32(page); } static void M_UpdateReflectivity(OBJECT_MESH *const mesh, FACE *const face) { const OBJECT_TEXTURE *const texture = Output_GetObjectTexture(face->texture_idx); const bool reflective = texture->draw_type == DRAW_REFLECTIVE_OPAQUE || texture->draw_type == DRAW_REFLECTIVE_BLEND_ADD; face->enable_reflections |= reflective; mesh->enable_reflections |= reflective; } void Level_Finalize_LoadTextures(LEVEL_CONTEXT *const ctx) { for (int32_t room_num = 0; room_num < Room_GetCount(); room_num++) { const ROOM *const room = Room_Get(room_num); for (int32_t j = 0; j < room->mesh.face3s.count; j++) { const FACE *const face = &room->mesh.face3s.data[j]; OBJECT_TEXTURE *const texture = Output_GetObjectTexture(face->texture_idx); texture->uv_count = 3; } } for (int32_t i = 0; i < Object_GetMeshCount(); i++) { OBJECT_MESH *const mesh = Object_GetMesh(i); for (int32_t j = 0; j < mesh->tex_face3s.count; j++) { FACE *const face = &mesh->tex_face3s.data[j]; OBJECT_TEXTURE *const texture = Output_GetObjectTexture(face->texture_idx); texture->uv_count = 3; M_UpdateReflectivity(mesh, face); } } for (int32_t room_num = 0; room_num < Room_GetCount(); room_num++) { ROOM *const room = Room_Get(room_num); for (int32_t j = 0; j < room->mesh.face4s.count; j++) { FACE *const face = &room->mesh.face4s.data[j]; XYZ_16 vertices[4] = { room->mesh.vertices[face->vertices[0]].pos, room->mesh.vertices[face->vertices[1]].pos, room->mesh.vertices[face->vertices[2]].pos, room->mesh.vertices[face->vertices[3]].pos, }; M_FixTrapezoidRatios(face, vertices); } } for (int32_t i = 0; i < Object_GetMeshCount(); i++) { OBJECT_MESH *const mesh = Object_GetMesh(i); for (int32_t j = 0; j < mesh->tex_face4s.count; j++) { FACE *const face = &mesh->tex_face4s.data[j]; XYZ_16 vertices[4] = { mesh->vertices[face->vertices[0]], mesh->vertices[face->vertices[1]], mesh->vertices[face->vertices[2]], mesh->vertices[face->vertices[3]], }; M_FixTrapezoidRatios(face, vertices); M_UpdateReflectivity(mesh, face); } } } void Level_Finalize_LoadTexturePages(LEVEL_CONTEXT *const ctx) { BENCHMARK benchmark = Benchmark_Start(); const LEVEL_FORMAT_LOADER *const loader = ctx->loader; LEVEL_CONTEXT_INFO *const info = &ctx->info; const int32_t num_pages = info->textures.page_count; Output_InitialiseTexturePages(num_pages, loader->game_version >= 2); for (int32_t i = 0; i < num_pages; i++) { if (loader->game_version >= 2) { uint8_t *const target_8 = Output_GetTexturePage8(i); const uint8_t *const source_8 = &info->textures.pages_8[i * TEXTURE_PAGE_SIZE]; memcpy(target_8, source_8, TEXTURE_PAGE_SIZE * sizeof(uint8_t)); } const RGBA_8888 *const source_32 = &info->textures.pages_32[i * TEXTURE_PAGE_SIZE]; RGBA_8888 *const target_32 = Output_GetTexturePage32(i); memcpy(target_32, source_32, TEXTURE_PAGE_SIZE * sizeof(RGBA_8888)); } Benchmark_End(&benchmark, "copied texture data"); { int32_t *pages = Memory_Alloc(num_pages * sizeof(int32_t)); THREAD_POOL *const pool = ThreadPool_Create(-1); for (int32_t i = 0; i < num_pages; i++) { pages[i] = i; } for (int32_t i = 0; i < num_pages; i++) { ThreadPool_AddJob(pool, M_PremultiplyTexturePage, &pages[i]); } ThreadPool_Wait(pool); ThreadPool_Destroy(pool); Memory_Free(pages); } Benchmark_End(&benchmark, "premultiplied alpha"); Memory_FreePointer(&info->textures.pages_8); Memory_FreePointer(&info->textures.pages_32); Benchmark_End(&benchmark, nullptr); } void Level_Finalize_LoadPalettes(LEVEL_CONTEXT *const ctx) { LEVEL_CONTEXT_INFO *const info = &ctx->info; Output_InitialisePalettes( info->palette.size, info->palette.data_24, info->palette.data_32); Memory_FreePointer(&info->palette.data_24); Memory_FreePointer(&info->palette.data_32); } ================================================ FILE: src/trx/game/level/finalize/rooms.c ================================================ #include #include #include #include #include #include #include #include #include #include static inline bool M_BoundsIntersectsPortal( const STATIC_MESH *const mesh, const ROOM *const room, const PORTAL *const portal) { const STATIC_OBJECT_3D *const obj = Object_Get3DStatic(mesh->static_num); const BOUNDS_32 bounds = { .min = { .x = mesh->pos.x + obj->draw_bounds.min.x, .y = mesh->pos.y + obj->draw_bounds.min.y, .z = mesh->pos.z + obj->draw_bounds.min.z, }, .max = { .x = mesh->pos.x + obj->draw_bounds.max.x, .y = mesh->pos.y + obj->draw_bounds.max.y, .z = mesh->pos.z + obj->draw_bounds.max.z, }, }; return Bounds32_Intersect(&bounds, &portal->bounds); } static void M_ComputePortalBounds(void) { for (int32_t i = 0; i < Room_GetCount(); i++) { ROOM *const room = Room_Get(i); PORTALS *const portals = room->portals; if (portals == nullptr) { continue; } for (uint16_t p = 0; p < portals->count; p++) { PORTAL *const portal = &portals->portal[p]; BOUNDS_32 *const bounds = &portal->bounds; bounds->min.x = room->pos.x + portal->vertex[0].x; bounds->min.y = room->pos.y + portal->vertex[0].y; bounds->min.z = room->pos.z + portal->vertex[0].z; bounds->max.x = room->pos.x + portal->vertex[0].x; bounds->max.y = room->pos.y + portal->vertex[0].y; bounds->max.z = room->pos.z + portal->vertex[0].z; for (int32_t k = 1; k < 4; k++) { bounds->min.x = MIN(bounds->min.x, room->pos.x + portal->vertex[k].x); bounds->min.y = MIN(bounds->min.y, room->pos.y + portal->vertex[k].y); bounds->min.z = MIN(bounds->min.z, room->pos.z + portal->vertex[k].z); bounds->max.x = MAX(bounds->max.x, room->pos.x + portal->vertex[k].x); bounds->max.y = MAX(bounds->max.y, room->pos.y + portal->vertex[k].y); bounds->max.z = MAX(bounds->max.z, room->pos.z + portal->vertex[k].z); } } } } static void M_FixStaticsVisibility(void) { int32_t total_rooms = Room_GetCount(); int32_t draw_num = 0; VECTOR **room_stat_vecs = Memory_Alloc(sizeof(*room_stat_vecs) * total_rooms); for (int32_t i = 0; i < total_rooms; i++) { room_stat_vecs[i] = Vector_Create(sizeof(STATIC_MESH)); ROOM *const room = Room_Get(i); for (int32_t m = 0; m < room->num_static_meshes; m++) { STATIC_MESH *const static_mesh = &room->static_meshes[m]; if (Object_IsValidStatid3D(static_mesh->static_num)) { ASSERT(draw_num < MAX_ITEMS); static_mesh->draw_num = draw_num++; Vector_Add(room_stat_vecs[i], static_mesh); } else { LOG_WARNING( "Invalid static 3D (id %d) in room %d", static_mesh->static_num, i); } } } for (int32_t i = 0; i < total_rooms; i++) { ROOM *const room = Room_Get(i); PORTALS *const portals = room->portals; if (portals == nullptr) { continue; } for (uint16_t p = 0; p < portals->count; p++) { const PORTAL *const portal = &portals->portal[p]; ROOM *const dest_room = Room_Get(portal->room_num); if (room->flip_status != dest_room->flip_status) { continue; } int32_t orig_count = room_stat_vecs[i]->count; for (int32_t m = 0; m < orig_count; m++) { const STATIC_MESH *const mesh = Vector_Get(room_stat_vecs[i], m); if (!M_BoundsIntersectsPortal(mesh, room, portal)) { continue; } if (Vector_Contains(room_stat_vecs[portal->room_num], mesh)) { continue; } Vector_Add(room_stat_vecs[portal->room_num], mesh); LOG_WARNING( "Static #%d bleeds into room #%d", mesh->static_num, portal->room_num); } } } int32_t total_needed = 0; for (int32_t i = 0; i < total_rooms; i++) { total_needed += room_stat_vecs[i]->count; } if (total_needed == 0) { for (int32_t i = 0; i < total_rooms; i++) { Vector_Free(room_stat_vecs[i]); } Memory_FreePointer(&room_stat_vecs); } else { STATIC_MESH *all_statics = GameBuf_Alloc( sizeof(STATIC_MESH) * total_needed, GBUF_ROOM_STATIC_MESHES); int32_t offset = 0; for (int32_t i = 0; i < total_rooms; i++) { ROOM *const room = Room_Get(i); room->static_meshes = &all_statics[offset]; room->num_static_meshes = 0; VECTOR *vec = room_stat_vecs[i]; for (int32_t m = 0; m < vec->count; m++) { room->static_meshes[room->num_static_meshes++] = *(STATIC_MESH *)Vector_Get(vec, m); } offset += vec->count; Vector_Free(vec); } Memory_FreePointer(&room_stat_vecs); } } static void M_FixStaticsCollision(void) { const int32_t count = Object_GetStaticObjects3DCount(); for (int32_t i = 0; i < count; i++) { STATIC_OBJECT_3D *const obj = Object_Get3DStatic(i); if (!obj->loaded || !obj->collidable) { continue; } const XYZ_32 hitbox = { .x = obj->collision_bounds.max.x - obj->collision_bounds.min.x, .y = obj->collision_bounds.max.y - obj->collision_bounds.min.y, .z = obj->collision_bounds.max.z - obj->collision_bounds.min.z, }; if (hitbox.x <= 0 && hitbox.y <= 0 && hitbox.z <= 0) { LOG_WARNING( "Static %d is marked as collidable, but has degenerate " "hitbox (%d x %d x %d)", i, hitbox.x, hitbox.y, hitbox.z); obj->collidable = false; } } } void Level_Finalize_LoadRooms(LEVEL_CONTEXT *const ctx) { M_ComputePortalBounds(); M_FixStaticsCollision(); M_FixStaticsVisibility(); } ================================================ FILE: src/trx/game/level/finalize.h ================================================ #pragma once #include void Level_Finalize_LoadRooms(LEVEL_CONTEXT *ctx); void Level_Finalize_LoadTextures(LEVEL_CONTEXT *ctx); void Level_Finalize_LoadTexturePages(LEVEL_CONTEXT *ctx); void Level_Finalize_LoadPalettes(LEVEL_CONTEXT *ctx); void Level_Finalize_LoadAnimCommands(LEVEL_CONTEXT *ctx); void Level_Finalize_LoadAnimFrames(LEVEL_CONTEXT *ctx); void Level_Finalize_LoadObjectsAndItems(LEVEL_CONTEXT *ctx); void Level_Finalize_LoadWalkables(LEVEL_CONTEXT *ctx); ================================================ FILE: src/trx/game/level/format/format.h ================================================ #pragma once #include #include typedef enum { LEVEL_FORMAT_PROBE_MINIMAL, LEVEL_FORMAT_PROBE_STATS, } LEVEL_FORMAT_PROBE_MODE; typedef enum { LEVEL_FORMAT_LAYOUT_UNKNOWN = -1, LEVEL_FORMAT_LAYOUT_TR1X, LEVEL_FORMAT_LAYOUT_TR1, LEVEL_FORMAT_LAYOUT_TR1_DEMO_PC, LEVEL_FORMAT_LAYOUT_TR2X, LEVEL_FORMAT_LAYOUT_TR2, LEVEL_FORMAT_LAYOUT_TR3, LEVEL_FORMAT_LAYOUT_TR3X, LEVEL_FORMAT_LAYOUT_NUMBER_OF, } LEVEL_FORMAT_LAYOUT; typedef struct LEVEL_FORMAT_LOADER { int32_t game_version; LEVEL_FORMAT_LAYOUT layout; bool (*probe)( const struct LEVEL_FORMAT_LOADER *, VFILE *file, LEVEL_FORMAT_PROBE_MODE mode); bool (*load)(const struct LEVEL_FORMAT_LOADER *, VFILE *file); } LEVEL_FORMAT_LOADER; LEVEL_FORMAT_LAYOUT Level_Format_GuessLayout(VFILE *file); const LEVEL_FORMAT_LOADER *Level_Format_GuessLoader(VFILE *file); const LEVEL_FORMAT_LOADER *Level_Format_LoadFromFile(const GF_LEVEL *level); ================================================ FILE: src/trx/game/level/format/format_tr1.c ================================================ #include #include #include #include #include #include #define M_SAMPLE_COUNT 256 static bool M_Probe( const LEVEL_FORMAT_LOADER *const loader, VFILE *const file, const LEVEL_FORMAT_PROBE_MODE mode) { VFile_SetPos(file, 0); LEVEL_CONTEXT probe_ctx = { .loader = loader, }; int32_t version; LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadS32(file, &version)); if (version != 32) { return false; } LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(TEXTURE_PAGE_SIZE); // textures LEVEL_FORMAT_SKIP_OR_FAIL(4); if (mode == LEVEL_FORMAT_PROBE_MINIMAL) { uint16_t room_count; LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadU16(file, &room_count)); for (int32_t i = 0; i < room_count; i++) { LEVEL_FORMAT_SKIP_OR_FAIL(16); LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // meshes LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(32); // portals int16_t size_z; int16_t size_x; LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadS16(file, &size_z)); LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadS16(file, &size_x)); LEVEL_FORMAT_SKIP_OR_FAIL(size_z * size_x * 8); LEVEL_FORMAT_SKIP_OR_FAIL(2); LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(18); // lights LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(18); // static meshes LEVEL_FORMAT_SKIP_OR_FAIL(4); } LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // floor data } else { Level_Section_ReadRooms(&probe_ctx, file); } LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // object meshes LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(4); // object mesh pointers LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(32); // animations LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(6); // animation changes LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(8); // animation ranges LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // animation commands LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(4); // animation bones LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // animation frames if (mode == LEVEL_FORMAT_PROBE_MINIMAL) { LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(18); // objects } else { Level_Section_ReadObjects(&probe_ctx, file); } LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(32); // static objects LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(20); // textures LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(16); // sprites LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(8); // sprites sequences if (loader->layout == LEVEL_FORMAT_LAYOUT_TR1_DEMO_PC) { LEVEL_FORMAT_SKIP_OR_FAIL(768); // palette } LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(16); // cameras LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(16); // sound effects int32_t box_count; LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadS32(file, &box_count)); LEVEL_FORMAT_SKIP_OR_FAIL(box_count * 20); LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // overlaps LEVEL_FORMAT_SKIP_OR_FAIL(box_count * 12); // zones LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // animated texture ranges if (mode == LEVEL_FORMAT_PROBE_MINIMAL) { LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(22); // items } else { Level_Section_ReadItems(&probe_ctx, file); } LEVEL_FORMAT_SKIP_OR_FAIL(32 * 256); // light table if (loader->layout != LEVEL_FORMAT_LAYOUT_TR1_DEMO_PC) { LEVEL_FORMAT_SKIP_OR_FAIL(768); // palette } LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(16); // cinematic frames LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(1); // demo data LEVEL_FORMAT_SKIP_OR_FAIL(2 * M_SAMPLE_COUNT); // sample lut LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(8); // sample infos LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(1); // sample data LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(4); // samples if (loader->layout == LEVEL_FORMAT_LAYOUT_TR1X) { uint32_t inj_magic; LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadU32(file, &inj_magic)); LEVEL_FORMAT_TRY_OR_FAIL(inj_magic == INJECTION_MAGIC); } return true; } static bool M_Load(const LEVEL_FORMAT_LOADER *const loader, VFILE *const file) { LEVEL_CONTEXT *const ctx = Level_Context_Get(); VFile_SetPos(file, 4); // Read texture pages once the palette is available. const int32_t num_pages = VFile_ReadS32(file); VFile_Skip(file, num_pages * TEXTURE_PAGE_SIZE * sizeof(uint8_t)); const int32_t file_level_num = VFile_ReadS32(file); LOG_INFO("file level num: %d", file_level_num); Level_Section_ReadRooms(ctx, file); Level_Section_ReadObjectMeshes(ctx, file); Level_Section_ReadAnims(ctx, file); Level_Section_ReadAnimChanges(ctx, file); Level_Section_ReadAnimRanges(ctx, file); Level_Section_ReadAnimCommands(ctx, file); Level_Section_ReadAnimBones(ctx, file); Level_Section_ReadAnimFrames(ctx, file); Level_Section_ReadObjects(ctx, file); Level_Section_ReadStaticObjects(ctx, file); Level_Section_ReadObjectTextures(ctx, file); Level_Section_ReadSpriteTextures(ctx, file); Level_Section_ReadSpriteSequences(ctx, file); if (loader->layout == LEVEL_FORMAT_LAYOUT_TR1_DEMO_PC) { Level_Section_ReadPalettes(ctx, file); } Level_Section_ReadCamerasAndSinks(ctx, file); Level_Section_ReadSoundSources(ctx, file); Level_Section_ReadPathingData(ctx, file); Level_Section_ReadAnimatedTextureRanges(ctx, file); Level_Section_ReadItems(ctx, file); Level_Section_ReadLightMap(ctx, file); if (loader->layout != LEVEL_FORMAT_LAYOUT_TR1_DEMO_PC) { Level_Section_ReadPalettes(ctx, file); } Level_Section_ReadCinematicFrames(ctx, file); Level_Section_ReadDemoData(ctx, file); Level_Section_ReadSamples(ctx, file); if (loader->layout == LEVEL_FORMAT_LAYOUT_TR1X) { VFILE *const embedded_injection = VFile_CreateFromBuffer( file->cur_ptr, file->size - VFile_GetPos(file)); Inject_AppendInjection(embedded_injection); } VFile_SetPos(file, 4); Level_Section_ReadTexturePages(ctx, file); return true; } static const LEVEL_FORMAT_LOADER m_LevelLoaderTR1 = { .game_version = 1, .layout = LEVEL_FORMAT_LAYOUT_TR1, .probe = M_Probe, .load = M_Load, }; static const LEVEL_FORMAT_LOADER m_LevelLoaderTR1DemoPC = { .game_version = 1, .layout = LEVEL_FORMAT_LAYOUT_TR1_DEMO_PC, .probe = M_Probe, .load = M_Load, }; static const LEVEL_FORMAT_LOADER m_LevelLoaderTR1X = { .game_version = 1, .layout = LEVEL_FORMAT_LAYOUT_TR1X, .probe = M_Probe, .load = M_Load, }; REGISTER_LEVEL_FORMAT_LOADER(100, m_LevelLoaderTR1X) REGISTER_LEVEL_FORMAT_LOADER(110, m_LevelLoaderTR1) REGISTER_LEVEL_FORMAT_LOADER(120, m_LevelLoaderTR1DemoPC) ================================================ FILE: src/trx/game/level/format/format_tr2.c ================================================ #include #include #include #include #include #define M_SAMPLE_COUNT 370 static bool M_Probe( const LEVEL_FORMAT_LOADER *const loader, VFILE *const file, const LEVEL_FORMAT_PROBE_MODE mode) { VFile_SetPos(file, 0); LEVEL_CONTEXT probe_ctx = { .loader = loader, }; int32_t version; LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadS32(file, &version)); if (version != 45) { return false; } LEVEL_FORMAT_SKIP_OR_FAIL(1792); // palettes LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(TEXTURE_PAGE_SIZE * 3); // texture pages LEVEL_FORMAT_SKIP_OR_FAIL(4); // unused version number if (mode == LEVEL_FORMAT_PROBE_MINIMAL) { uint16_t room_count; LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadU16(file, &room_count)); for (int32_t i = 0; i < room_count; i++) { LEVEL_FORMAT_SKIP_OR_FAIL(16); LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // meshes LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(32); // portals int16_t size_z; int16_t size_x; LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadS16(file, &size_z)); LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadS16(file, &size_x)); LEVEL_FORMAT_SKIP_OR_FAIL(size_z * size_x * 8); // sectors LEVEL_FORMAT_SKIP_OR_FAIL(6); // lighting LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(24); // lights LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(20); // static meshes LEVEL_FORMAT_SKIP_OR_FAIL(4); } LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // floor data } else { Level_Section_ReadRooms(&probe_ctx, file); } LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // object meshes LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(4); // object mesh pointers LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(32); // animations LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(6); // animation changes LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(8); // animation ranges LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // animation commands LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(4); // animation bones LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // animation frames if (mode == LEVEL_FORMAT_PROBE_MINIMAL) { LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(18); // objects } else { Level_Section_ReadObjects(&probe_ctx, file); } LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(32); // static objects LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(20); // object textures LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(16); // sprite textures LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(8); // sprites sequences LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(16); // cameras/sinks LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(16); // sound sources int32_t box_count; LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadS32(file, &box_count)); LEVEL_FORMAT_SKIP_OR_FAIL(box_count * 8); LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // overlaps LEVEL_FORMAT_SKIP_OR_FAIL(box_count * 20); // zones LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // animated texture ranges if (mode == LEVEL_FORMAT_PROBE_MINIMAL) { LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(24); // items } else { Level_Section_ReadItems(&probe_ctx, file); } LEVEL_FORMAT_SKIP_OR_FAIL(32 * 256); // light table LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(16); // cinematic frames LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(1); // demo data LEVEL_FORMAT_SKIP_OR_FAIL(2 * M_SAMPLE_COUNT); // sample lut LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(8); // sample infos LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(4); // samples if (loader->layout == LEVEL_FORMAT_LAYOUT_TR2X) { uint32_t inj_magic; LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadU32(file, &inj_magic)); LEVEL_FORMAT_TRY_OR_FAIL(inj_magic == INJECTION_MAGIC); } return true; } static bool M_Load(const LEVEL_FORMAT_LOADER *const loader, VFILE *const file) { LEVEL_CONTEXT *const ctx = Level_Context_Get(); VFile_SetPos(file, 4); Level_Section_ReadPalettes(ctx, file); Level_Section_ReadTexturePages(ctx, file); VFile_Skip(file, 4); Level_Section_ReadRooms(ctx, file); Level_Section_ReadObjectMeshes(ctx, file); Level_Section_ReadAnims(ctx, file); Level_Section_ReadAnimChanges(ctx, file); Level_Section_ReadAnimRanges(ctx, file); Level_Section_ReadAnimCommands(ctx, file); Level_Section_ReadAnimBones(ctx, file); Level_Section_ReadAnimFrames(ctx, file); Level_Section_ReadObjects(ctx, file); Level_Section_ReadStaticObjects(ctx, file); Level_Section_ReadObjectTextures(ctx, file); Level_Section_ReadSpriteTextures(ctx, file); Level_Section_ReadSpriteSequences(ctx, file); Level_Section_ReadCamerasAndSinks(ctx, file); Level_Section_ReadSoundSources(ctx, file); Level_Section_ReadPathingData(ctx, file); Level_Section_ReadAnimatedTextureRanges(ctx, file); Level_Section_ReadItems(ctx, file); Level_Section_ReadLightMap(ctx, file); Level_Section_ReadCinematicFrames(ctx, file); Level_Section_ReadDemoData(ctx, file); Level_Section_ReadSamples(ctx, file); if (loader->layout == LEVEL_FORMAT_LAYOUT_TR2X) { VFILE *const embedded_injection = VFile_CreateFromBuffer( file->cur_ptr, file->size - VFile_GetPos(file)); Inject_AppendInjection(embedded_injection); } return true; } static const LEVEL_FORMAT_LOADER m_LevelLoaderTR2 = { .game_version = 2, .layout = LEVEL_FORMAT_LAYOUT_TR2, .load = M_Load, .probe = M_Probe, }; static const LEVEL_FORMAT_LOADER m_LevelLoaderTR2X = { .game_version = 2, .layout = LEVEL_FORMAT_LAYOUT_TR2X, .probe = M_Probe, .load = M_Load, }; REGISTER_LEVEL_FORMAT_LOADER(200, m_LevelLoaderTR2X) REGISTER_LEVEL_FORMAT_LOADER(210, m_LevelLoaderTR2) ================================================ FILE: src/trx/game/level/format/format_tr3.c ================================================ #include #include #include #include #include #define M_SAMPLE_COUNT 370 static bool M_Probe( const LEVEL_FORMAT_LOADER *const loader, VFILE *const file, const LEVEL_FORMAT_PROBE_MODE mode) { VFile_SetPos(file, 0); LEVEL_CONTEXT probe_ctx = { .loader = loader, }; uint32_t version; LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadU32(file, &version)); if (!(version == 0xFF080038ULL || version == 0xFF180038ULL)) { return false; } LEVEL_FORMAT_SKIP_OR_FAIL(1792); // palettes LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(TEXTURE_PAGE_SIZE * 3); // texture pages LEVEL_FORMAT_SKIP_OR_FAIL(4); // unused version number if (mode == LEVEL_FORMAT_PROBE_MINIMAL) { uint16_t room_count; LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadU16(file, &room_count)); for (int32_t i = 0; i < room_count; i++) { LEVEL_FORMAT_SKIP_OR_FAIL(16); LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // meshes LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(32); // portals int16_t size_z; int16_t size_x; LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadS16(file, &size_z)); LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadS16(file, &size_x)); LEVEL_FORMAT_SKIP_OR_FAIL(size_z * size_x * 8); // sectors LEVEL_FORMAT_SKIP_OR_FAIL(4); // lighting LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(24); // lights LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(20); // static meshes LEVEL_FORMAT_SKIP_OR_FAIL(7); } LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // floor data } else { Level_Section_ReadRooms(&probe_ctx, file); } LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // object meshes LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(4); // object mesh pointers LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(32); // animations LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(6); // animation changes LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(8); // animation ranges LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // animation commands LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(4); // animation bones LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // animation frames if (mode == LEVEL_FORMAT_PROBE_MINIMAL) { LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(18); // objects } else { Level_Section_ReadObjects(&probe_ctx, file); } LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(32); // static objects LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(16); // sprite textures LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(8); // sprites sequences LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(16); // cameras/sinks LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(16); // sound sources int32_t box_count; LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadS32(file, &box_count)); LEVEL_FORMAT_SKIP_OR_FAIL(box_count * 8); LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // overlaps LEVEL_FORMAT_SKIP_OR_FAIL(box_count * 20); // zones LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // animated texture ranges LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(20); // object textures if (mode == LEVEL_FORMAT_PROBE_MINIMAL) { LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(24); // items } else { Level_Section_ReadItems(&probe_ctx, file); } LEVEL_FORMAT_SKIP_OR_FAIL(32 * 256); // light table LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(16); // cinematic frames LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(1); // demo data LEVEL_FORMAT_SKIP_OR_FAIL(2 * M_SAMPLE_COUNT); // sample lut LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(8); // sample infos LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(4); // samples if (loader->layout == LEVEL_FORMAT_LAYOUT_TR3X) { uint32_t inj_magic; LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadU32(file, &inj_magic)); LEVEL_FORMAT_TRY_OR_FAIL(inj_magic == INJECTION_MAGIC); } return true; } static bool M_Load(const LEVEL_FORMAT_LOADER *const loader, VFILE *const file) { LEVEL_CONTEXT *const ctx = Level_Context_Get(); VFile_SetPos(file, 4); Level_Section_ReadPalettes(ctx, file); Level_Section_ReadTexturePages(ctx, file); VFile_Skip(file, 4); Level_Section_ReadRooms(ctx, file); Level_Section_ReadObjectMeshes(ctx, file); Level_Section_ReadAnims(ctx, file); Level_Section_ReadAnimChanges(ctx, file); Level_Section_ReadAnimRanges(ctx, file); Level_Section_ReadAnimCommands(ctx, file); Level_Section_ReadAnimBones(ctx, file); Level_Section_ReadAnimFrames(ctx, file); Level_Section_ReadObjects(ctx, file); Level_Section_ReadStaticObjects(ctx, file); Level_Section_ReadSpriteTextures(ctx, file); Level_Section_ReadSpriteSequences(ctx, file); Level_Section_ReadCamerasAndSinks(ctx, file); Level_Section_ReadSoundSources(ctx, file); Level_Section_ReadPathingData(ctx, file); Level_Section_ReadAnimatedTextureRanges(ctx, file); Level_Section_ReadObjectTextures(ctx, file); Level_Section_ReadItems(ctx, file); Level_Section_ReadLightMap(ctx, file); Level_Section_ReadCinematicFrames(ctx, file); Level_Section_ReadDemoData(ctx, file); Level_Section_ReadSamples(ctx, file); if (loader->layout == LEVEL_FORMAT_LAYOUT_TR3X) { VFILE *const embedded_injection = VFile_CreateFromBuffer( file->cur_ptr, file->size - VFile_GetPos(file)); Inject_AppendInjection(embedded_injection); } return true; } static const LEVEL_FORMAT_LOADER m_LevelLoaderTR3 = { .game_version = 3, .layout = LEVEL_FORMAT_LAYOUT_TR3, .load = M_Load, .probe = M_Probe, }; REGISTER_LEVEL_FORMAT_LOADER(300, m_LevelLoaderTR3) ================================================ FILE: src/trx/game/level/format/pipeline.c ================================================ #include #include #include #include #include #include #include #include #include typedef struct { int32_t priority; const LEVEL_FORMAT_LOADER *loader; } M_REGISTERED_LOADER; static VECTOR *m_Loaders = nullptr; __attribute__((destructor)) static void M_Shutdown(void) { if (m_Loaders != nullptr) { Vector_Free(m_Loaders); m_Loaders = nullptr; } } void Level_Format_RegisterLoader( const int32_t priority, const LEVEL_FORMAT_LOADER *const loader) { if (m_Loaders == nullptr) { m_Loaders = Vector_Create(sizeof(M_REGISTERED_LOADER)); } M_REGISTERED_LOADER registered = { .priority = priority, .loader = loader, }; int32_t insert_idx = m_Loaders->count; for (int32_t i = 0; i < m_Loaders->count; i++) { const M_REGISTERED_LOADER *const test = Vector_Get(m_Loaders, i); if (priority < test->priority) { insert_idx = i; break; } } Vector_Insert(m_Loaders, insert_idx, ®istered); } static int32_t M_GetRegisteredLoaderCount(void) { if (m_Loaders == nullptr) { return 0; } return m_Loaders->count; } static const LEVEL_FORMAT_LOADER *M_GetRegisteredLoader(const int32_t index) { return ((M_REGISTERED_LOADER *)Vector_Get(m_Loaders, index))->loader; } const LEVEL_FORMAT_LOADER *Level_Format_GuessLoader(VFILE *const file) { const LEVEL_FORMAT_LOADER *result = nullptr; BENCHMARK benchmark = Benchmark_Start(); const int32_t loader_count = M_GetRegisteredLoaderCount(); for (int32_t i = 0; i < loader_count; i++) { const LEVEL_FORMAT_LOADER *const loader = M_GetRegisteredLoader(i); if (loader->probe(loader, file, LEVEL_FORMAT_PROBE_MINIMAL)) { result = loader; break; } } Benchmark_End(&benchmark, nullptr); return result; } LEVEL_FORMAT_LAYOUT Level_Format_GuessLayout(VFILE *const file) { const LEVEL_FORMAT_LOADER *const loader = Level_Format_GuessLoader(file); if (loader != nullptr) { return loader->layout; } return LEVEL_FORMAT_LAYOUT_UNKNOWN; } const LEVEL_FORMAT_LOADER *Level_Format_LoadFromFile( const GF_LEVEL *const level) { GameBuf_Reset(); BENCHMARK benchmark = Benchmark_Start(); VFILE *const file = VFile_CreateFromPath(level->path); if (file == nullptr) { Shell_ExitSystemFmt("Could not open %s", level->path); } const LEVEL_FORMAT_LOADER *const loader = Level_Format_GuessLoader(file); if (loader == nullptr) { Shell_ExitSystemFmt("Failed to load %s", level->path); } g_TRVersion = loader->game_version; Level_Context_Reset(loader); ASSERT(loader->load != nullptr); loader->load(loader, file); VFile_Close(file); Benchmark_End(&benchmark, nullptr); return loader; } ================================================ FILE: src/trx/game/level/format/priv.h ================================================ #pragma once #include #include void Level_Format_RegisterLoader( int32_t priority, const LEVEL_FORMAT_LOADER *loader); #define REGISTER_LEVEL_FORMAT_LOADER(priority_, loader_) \ __attribute__((__constructor__)) static void CONCAT( \ M_RegisterLevelFormatLoader_, __LINE__)(void) \ { \ Level_Format_RegisterLoader(priority_, &(loader_)); \ } // Helper control-flow macros // ============================================================================= #define LEVEL_FORMAT_TRY_OR_FAIL(call_) \ do { \ if (!(call_)) { \ return false; \ } \ } while (0) #define LEVEL_FORMAT_SKIP_OR_FAIL(size_) \ LEVEL_FORMAT_TRY_OR_FAIL(VFile_TrySkip(file, size_)) #define LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(size_) \ do { \ int32_t count; \ LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadS32(file, &count)); \ LEVEL_FORMAT_SKIP_OR_FAIL(count * (size_)); \ } while (0) #define LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(size_) \ do { \ uint16_t count; \ LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadU16(file, &count)); \ LEVEL_FORMAT_SKIP_OR_FAIL(count * (size_)); \ } while (0) ================================================ FILE: src/trx/game/level/pipeline.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef struct { int32_t game_index; int32_t file_index; } M_SAMPLE_ENTRY; static int32_t M_CompareSampleOffsets(const void *const a, const void *const b) { const M_SAMPLE_ENTRY *const entry_a = (M_SAMPLE_ENTRY *)a; const M_SAMPLE_ENTRY *const entry_b = (M_SAMPLE_ENTRY *)b; return entry_a->file_index - entry_b->file_index; } static void M_InitialiseSamplesFromFile( LEVEL_CONTEXT *const ctx, const char *file_name) { BENCHMARK benchmark = Benchmark_Start(); M_SAMPLE_ENTRY *entries = nullptr; LEVEL_CONTEXT_INFO *const info = &ctx->info; MYFILE *fp = nullptr; if (file_name == nullptr) { goto finish; } fp = File_Open(file_name, FILE_OPEN_READ); if (fp == nullptr) { LOG_ERROR("Could not open %s samples file", file_name); goto finish; } LOG_DEBUG("Loading samples from %s", file_name); const int32_t sample_count = info->samples.offset_count; entries = Memory_Alloc(sizeof(M_SAMPLE_ENTRY) * sample_count); for (int32_t i = 0; i < sample_count; i++) { entries[i].game_index = i; entries[i].file_index = info->samples.offsets[i]; } qsort( entries, sample_count, sizeof(M_SAMPLE_ENTRY), M_CompareSampleOffsets); for (int32_t i = 0, current_sample = 0; current_sample < sample_count; i++) { uint32_t header[11] = {}; File_ReadData(fp, header, 11 * sizeof(uint32_t)); if (header[0] != MKTAG('R', 'I', 'F', 'F') || header[2] != MKTAG('W', 'A', 'V', 'E') || header[9] != MKTAG('d', 'a', 't', 'a')) { LOG_ERROR("Unexpected sample header for sample %d", i); goto finish; } const size_t header_size = 11 * sizeof(uint32_t); const size_t aligned_size = (header[10] + 1) & ~1; const size_t size = aligned_size + header_size; const M_SAMPLE_ENTRY *const entry = &entries[current_sample]; if (entry->file_index != i) { File_Seek(fp, aligned_size, FILE_SEEK_CUR); continue; } char *sample_data = Memory_Alloc(size); memcpy(sample_data, header, header_size); File_ReadData(fp, sample_data + header_size, aligned_size); Sound_LoadSampleData(entry->game_index, sample_data, size); Memory_FreePointer(&sample_data); current_sample++; } finish: if (fp != nullptr) { File_Close(fp); } Memory_FreePointer(&entries); Memory_FreePointer(&info->samples.offsets); Benchmark_End(&benchmark, nullptr); } static void M_InitialiseSamplesFromLevelInfo(LEVEL_CONTEXT *const ctx) { BENCHMARK benchmark = Benchmark_Start(); LEVEL_CONTEXT_INFO *const info = &ctx->info; const int32_t sample_count = info->samples.offset_count; // TODO: this assumes that sample pointers are sorted - adopt TR2's approach // of sorting by index, verifying WAV headers and using WAV sample size. for (int32_t i = 0; i < sample_count; i++) { const int32_t current_offset = info->samples.offsets[i]; const int32_t next_offset = i + 1 >= sample_count ? info->samples.data_size : info->samples.offsets[i + 1]; const char *const sample_data = &info->samples.data[current_offset]; const size_t sample_size = next_offset - current_offset; Sound_LoadSampleData(i, sample_data, sample_size); } Memory_FreePointer(&info->samples.offsets); Benchmark_End(&benchmark, nullptr); } static void M_MarkWaterEdgeVertices(void) { if (!g_Config.visuals.fix_texture_issues) { return; } BENCHMARK benchmark = Benchmark_Start(); for (int32_t i = 0; i < Room_GetCount(); i++) { const ROOM *const room = Room_Get(i); const int32_t y_test = room->flags.underwater ? room->max_ceiling : room->min_floor; for (int32_t j = 0; j < room->mesh.num_vertices; j++) { ROOM_VERTEX *const vertex = &room->mesh.vertices[j]; if (vertex->pos.y == y_test) { vertex->flags.disable_wibble = true; } } } Benchmark_End(&benchmark, nullptr); } static void M_CompleteSetup( const LEVEL_FORMAT_LOADER *const loader, const GF_LEVEL *const level) { BENCHMARK benchmark = Benchmark_Start(); LEVEL_CONTEXT *const ctx = Level_Context_Get(); // We inject explosions sprites and sounds, although in the original game, // some levels lack them, resulting in no audio or visual effects when // killing mutants. This is to maintain that feature. Atlantean_ToggleExplosions(Object_Get(O_EXPLOSION_1)->loaded); Inject_AllInjections(); Level_Finalize_LoadAnimFrames(ctx); Level_Finalize_LoadAnimCommands(ctx); if (g_TRVersion == 1) { M_MarkWaterEdgeVertices(); } Level_Finalize_LoadObjectsAndItems(ctx); // Configure enemies who carry and drop items Carrier_InitialiseLevel(level); Level_Finalize_LoadRooms(ctx); Level_Finalize_LoadTextures(ctx); Level_Finalize_LoadTexturePages(ctx); Level_Finalize_LoadPalettes(ctx); if (loader->game_version == 1) { M_InitialiseSamplesFromLevelInfo(ctx); } else { M_InitialiseSamplesFromFile(ctx, level->settings.sfx_path); } Benchmark_End(&benchmark, nullptr); } void Level_Pipeline_Load(const GF_LEVEL *const level) { LOG_INFO("%d (%s)", level->num, level->path); BENCHMARK benchmark = Benchmark_Start(); Inject_InitLevel(level, INJECTION_MODE_FULL); const LEVEL_FORMAT_LOADER *const loader = Level_Format_LoadFromFile(level); M_CompleteSetup(loader, level); Inject_Cleanup(); Benchmark_End(&benchmark, nullptr); } ================================================ FILE: src/trx/game/level/pipeline.h ================================================ #pragma once #include #include #include #include #include void Level_Pipeline_Load(const GF_LEVEL *level); ================================================ FILE: src/trx/game/level/sections/anims.c ================================================ #include #include #include #include #include #include #include static void M_ReadPosition(XYZ_32 *const pos, VFILE *const file) { pos->x = VFile_ReadS32(file); pos->y = VFile_ReadS32(file); pos->z = VFile_ReadS32(file); } void Level_Section_ReadAnims(LEVEL_CONTEXT *const ctx, VFILE *const file) { BENCHMARK benchmark = Benchmark_Start(); const int32_t num_anims = VFile_ReadS32(file); LEVEL_CONTEXT_INFO *const info = &ctx->info; info->anims.anim_count = num_anims; LOG_INFO("anims: %d", num_anims); Anim_InitialiseAnims(num_anims + Inject_GetDataCount(IDT_ANIMS)); Level_Section_AppendAnims(0, num_anims, file); Benchmark_End(&benchmark, nullptr); } void Level_Section_AppendAnims( const int32_t base_idx, const int32_t num_anims, VFILE *const file) { for (int32_t i = 0; i < num_anims; i++) { ANIM *const anim = Anim_GetAnim(base_idx + i); anim->frame_ofs = VFile_ReadU32(file); anim->frame_ptr = nullptr; // filled later by the animation frame loader anim->interpolation = VFile_ReadU8(file); anim->frame_size = VFile_ReadU8(file); anim->current_anim_state = VFile_ReadS16(file); anim->velocity = VFile_ReadS32(file); anim->acceleration = VFile_ReadS32(file); anim->frame_base = VFile_ReadS16(file); anim->frame_end = VFile_ReadS16(file); anim->jump_anim_num = VFile_ReadS16(file); anim->jump_frame_num = VFile_ReadS16(file); anim->num_changes = VFile_ReadS16(file); anim->change_idx = VFile_ReadS16(file); anim->num_commands = VFile_ReadS16(file); anim->command_idx = VFile_ReadS16(file); } } void Level_Section_ReadAnimChanges(LEVEL_CONTEXT *const ctx, VFILE *const file) { BENCHMARK benchmark = Benchmark_Start(); const int32_t num_anim_changes = VFile_ReadS32(file); LEVEL_CONTEXT_INFO *const info = &ctx->info; info->anims.change_count = num_anim_changes; LOG_INFO("anim changes: %d", num_anim_changes); Anim_InitialiseChanges( num_anim_changes + Inject_GetDataCount(IDT_ANIM_CHANGES)); Level_Section_AppendAnimChanges(0, num_anim_changes, file); Benchmark_End(&benchmark, nullptr); } void Level_Section_AppendAnimChanges( const int32_t base_idx, const int32_t num_changes, VFILE *const file) { for (int32_t i = 0; i < num_changes; i++) { ANIM_CHANGE *const anim_change = Anim_GetChange(base_idx + i); anim_change->goal_anim_state = VFile_ReadS16(file); anim_change->num_ranges = VFile_ReadS16(file); anim_change->range_idx = VFile_ReadS16(file); } } void Level_Section_ReadAnimRanges(LEVEL_CONTEXT *const ctx, VFILE *const file) { BENCHMARK benchmark = Benchmark_Start(); const int32_t num_anim_ranges = VFile_ReadS32(file); LEVEL_CONTEXT_INFO *const info = &ctx->info; info->anims.range_count = num_anim_ranges; LOG_INFO("anim ranges: %d", num_anim_ranges); Anim_InitialiseRanges( num_anim_ranges + Inject_GetDataCount(IDT_ANIM_RANGES)); Level_Section_AppendAnimRanges(0, num_anim_ranges, file); Benchmark_End(&benchmark, nullptr); } void Level_Section_AppendAnimRanges( const int32_t base_idx, const int32_t num_ranges, VFILE *const file) { for (int32_t i = 0; i < num_ranges; i++) { ANIM_RANGE *const anim_range = Anim_GetRange(base_idx + i); anim_range->start_frame = VFile_ReadS16(file); anim_range->end_frame = VFile_ReadS16(file); anim_range->link_anim_num = VFile_ReadS16(file); anim_range->link_frame_num = VFile_ReadS16(file); } } void Level_Section_ReadAnimCommands(LEVEL_CONTEXT *const ctx, VFILE *const file) { BENCHMARK benchmark = Benchmark_Start(); const int32_t num_commands = VFile_ReadS32(file); LEVEL_CONTEXT_INFO *const info = &ctx->info; info->anims.command_count = num_commands; LOG_INFO("anim commands: %d", num_commands); info->anims.commands = Memory_Alloc( sizeof(int16_t) * (num_commands + Inject_GetDataCount(IDT_ANIM_COMMANDS))); Level_Section_AppendAnimCommands(0, num_commands, file); Benchmark_End(&benchmark, nullptr); } void Level_Section_AppendAnimCommands( const int32_t base_idx, const int32_t num_commands, VFILE *const file) { LEVEL_CONTEXT_INFO *const info = Level_Context_GetInfo(); VFile_Read( file, &info->anims.commands[base_idx], sizeof(int16_t) * num_commands); } void Level_Section_ReadAnimBones(LEVEL_CONTEXT *const ctx, VFILE *const file) { BENCHMARK benchmark = Benchmark_Start(); const int32_t num_anim_bones = VFile_ReadS32(file) / ANIM_BONE_SIZE; LEVEL_CONTEXT_INFO *const info = &ctx->info; info->anims.bone_count = num_anim_bones; LOG_INFO("anim bones: %d", num_anim_bones); Anim_InitialiseBones(num_anim_bones + Inject_GetDataCount(IDT_ANIM_BONES)); Level_Section_AppendAnimBones(0, num_anim_bones, file); Benchmark_End(&benchmark, nullptr); } void Level_Section_AppendAnimBones( const int32_t base_idx, const int32_t num_bones, VFILE *const file) { for (int32_t i = 0; i < num_bones; i++) { ANIM_BONE *const bone = Anim_GetBone(base_idx + i); const int32_t flags = VFile_ReadS32(file); bone->matrix_pop = (flags & 1) != 0; bone->matrix_push = (flags & 2) != 0; bone->rot.x = false; bone->rot.y = false; bone->rot.z = false; M_ReadPosition(&bone->pos, file); } } void Level_Section_ReadAnimFrames(LEVEL_CONTEXT *const ctx, VFILE *const file) { BENCHMARK benchmark = Benchmark_Start(); const int32_t raw_data_count = VFile_ReadS32(file); LEVEL_CONTEXT_INFO *const info = &ctx->info; info->anims.frame_count = raw_data_count; LOG_INFO("raw anim frames: %d", raw_data_count); info->anims.frames = Memory_Alloc( sizeof(int16_t) * (raw_data_count + Inject_GetDataCount(IDT_ANIM_FRAMES))); Level_Section_AppendAnimFrames(0, raw_data_count, file); Benchmark_End(&benchmark, nullptr); } void Level_Section_AppendAnimFrames( const int32_t base_idx, const int32_t num_frames, VFILE *const file) { LEVEL_CONTEXT_INFO *const info = Level_Context_GetInfo(); VFile_Read( file, &info->anims.frames[base_idx], sizeof(int16_t) * num_frames); } ================================================ FILE: src/trx/game/level/sections/append.h ================================================ #pragma once #include #include void Level_Section_AppendObjectMeshes( int32_t num_offsets, const int32_t *offsets, VFILE *file); void Level_Section_AppendAnims( int32_t base_idx, int32_t num_anims, VFILE *file); void Level_Section_AppendAnimChanges( int32_t base_idx, int32_t num_changes, VFILE *file); void Level_Section_AppendAnimRanges( int32_t base_idx, int32_t num_ranges, VFILE *file); void Level_Section_AppendAnimCommands( int32_t base_idx, int32_t num_commands, VFILE *file); void Level_Section_AppendAnimBones( int32_t base_idx, int32_t num_bones, VFILE *file); void Level_Section_AppendAnimFrames( int32_t base_idx, int32_t num_frames, VFILE *file); void Level_Section_AppendObjectTextures( int32_t base_idx, int16_t base_page_idx, int32_t num_textures, VFILE *file, bool use_tr3_adjustment); void Level_Section_AppendSpriteTextures( int32_t base_idx, int16_t base_page_idx, int32_t num_textures, VFILE *file); ================================================ FILE: src/trx/game/level/sections/audio.c ================================================ #include #include #include #include #include #include #include #include #include #include static size_t M_GetSampleCount(const LEVEL_FORMAT_LOADER *const loader) { switch (loader->game_version) { case 1: return 256; case 2: case 3: return 370; default: ASSERT_FAIL(); } return 0; } void Level_Section_ReadSamples(LEVEL_CONTEXT *const ctx, VFILE *const file) { BENCHMARK benchmark = Benchmark_Start(); const LEVEL_FORMAT_LOADER *const loader = ctx->loader; const int32_t sample_count = M_GetSampleCount(loader); int16_t *const sample_lut = Memory_Alloc(sizeof(int16_t) * sample_count); int16_t *const sample_lut_inv = Memory_Alloc(sizeof(int16_t) * sample_count); VFile_Read(file, sample_lut, sizeof(int16_t) * sample_count); for (int32_t i = 0; i < sample_count; i++) { if (sample_lut[i] != -1) { sample_lut_inv[sample_lut[i]] = i; } } const int32_t num_sample_infos = VFile_ReadS32(file); LOG_INFO("sample infos: %d", num_sample_infos); for (int32_t i = 0; i < num_sample_infos; i++) { SAMPLE_INFO *const sample_info = Sound_GetOrCreateSample(sample_lut_inv[i]); ASSERT(sample_info != nullptr); sample_info->number = VFile_ReadS16(file); if (loader->game_version >= 3) { sample_info->volume = VFile_ReadU8(file) << 7; sample_info->range = VFile_ReadU8(file) * WALL_L; } else { sample_info->volume = VFile_ReadU16(file); sample_info->range = 10 * WALL_L; } if (loader->game_version >= 3) { sample_info->randomness = VFile_ReadU8(file); sample_info->pitch = VFile_ReadS8(file); } else { sample_info->randomness = VFile_ReadU16(file); sample_info->pitch = 0; } sample_info->flags.all = VFile_ReadU16(file); Sound_ReserveSampleData( sample_info->number, sample_info->flags.num_samples); if (loader->game_version == 1) { switch (sample_info->flags.mode_bits) { case 0: sample_info->mode = SAMPLE_MODE_WAIT; break; case 1: sample_info->mode = SAMPLE_MODE_RESTART; break; case 2: sample_info->mode = SAMPLE_MODE_LOOPED; break; case 3: LOG_WARNING( "Unexpected sample mode for sample %d. flags=%0X", i, sample_info->flags); break; } } else { switch (sample_info->flags.mode_bits) { case 0: sample_info->mode = SAMPLE_MODE_NORMAL; break; case 1: sample_info->mode = SAMPLE_MODE_WAIT; break; case 2: sample_info->mode = SAMPLE_MODE_RESTART; break; case 3: sample_info->mode = SAMPLE_MODE_LOOPED; break; } } } LEVEL_CONTEXT_INFO *const info = &ctx->info; if (loader->game_version == 1) { const int32_t data_size = VFile_ReadS32(file); info->samples.data_size = data_size; LOG_INFO("%d sample data size", data_size); info->samples.data = GameBuf_Alloc( data_size + Inject_GetDataCount(IDT_SAMPLE_DATA), GBUF_SAMPLES); VFile_Read(file, info->samples.data, sizeof(char) * data_size); } const int32_t num_offsets = VFile_ReadS32(file); LOG_INFO("samples: %d", num_offsets); info->samples.offset_count = num_offsets; info->samples.offsets = Memory_Alloc( sizeof(int32_t) * (num_offsets + Inject_GetDataCount(IDT_SAMPLE_INDICES))); VFile_Read(file, info->samples.offsets, sizeof(int32_t) * num_offsets); Memory_Free(sample_lut); Memory_Free(sample_lut_inv); Benchmark_End(&benchmark, nullptr); } ================================================ FILE: src/trx/game/level/sections/cinematics.c ================================================ #include #include #include #include #include #include #include #include static void M_ReadPosition(XYZ_32 *const pos, VFILE *const file) { pos->x = VFile_ReadS32(file); pos->y = VFile_ReadS32(file); pos->z = VFile_ReadS32(file); } static void M_ReadVertex(XYZ_16 *const vertex, VFILE *const file) { vertex->x = VFile_ReadS16(file); vertex->y = VFile_ReadS16(file); vertex->z = VFile_ReadS16(file); } static void M_ReadObjectVector(OBJECT_VECTOR *const obj, VFILE *const file) { M_ReadPosition(&obj->pos, file); obj->data = VFile_ReadS16(file); obj->flags = VFile_ReadS16(file); } void Level_Section_ReadCinematicFrames( LEVEL_CONTEXT *const ctx, VFILE *const file) { BENCHMARK benchmark = Benchmark_Start(); const int16_t num_frames = VFile_ReadS16(file); const int32_t inj_frames = Inject_GetDataCount(IDT_CINEMATIC_FRAMES); LOG_INFO("cinematic frames: %d", num_frames); Camera_InitialiseCineFrames(MAX(num_frames, inj_frames)); for (int32_t i = 0; i < num_frames; i++) { CINE_FRAME *const frame = Camera_GetCineFrame(i); M_ReadVertex(&frame->target.shift, file); M_ReadVertex(&frame->camera.shift, file); frame->fov = VFile_ReadS16(file); frame->roll = VFile_ReadS16(file); } Benchmark_End(&benchmark, nullptr); } void Level_Section_ReadCamerasAndSinks( LEVEL_CONTEXT *const ctx, VFILE *const file) { BENCHMARK benchmark = Benchmark_Start(); const int32_t num_objects = VFile_ReadS32(file); LOG_DEBUG("fixed cameras/sinks: %d", num_objects); Camera_InitialiseFixedObjects(num_objects); for (int32_t i = 0; i < num_objects; i++) { M_ReadObjectVector(Camera_GetFixedObject(i), file); } Benchmark_End(&benchmark, nullptr); } void Level_Section_ReadDemoData(LEVEL_CONTEXT *const ctx, VFILE *const file) { BENCHMARK benchmark = Benchmark_Start(); const uint16_t size = VFile_ReadU16(file); LOG_INFO("demo buffer size: %d", size); Demo_LoadData(file, size); Benchmark_End(&benchmark, nullptr); } void Level_Section_ReadSoundSources(LEVEL_CONTEXT *const ctx, VFILE *const file) { BENCHMARK benchmark = Benchmark_Start(); const int32_t num_sources = VFile_ReadS32(file); LOG_INFO("sound sources: %d", num_sources); Sound_InitialiseSources(num_sources); for (int32_t i = 0; i < num_sources; i++) { M_ReadObjectVector(Sound_GetSource(i), file); } Benchmark_End(&benchmark, nullptr); } ================================================ FILE: src/trx/game/level/sections/meshes.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include static void M_ReadVertex(XYZ_16 *const vertex, VFILE *const file) { vertex->x = VFile_ReadS16(file); vertex->y = VFile_ReadS16(file); vertex->z = VFile_ReadS16(file); } static void M_ReadFace( FACE *const face, const size_t vertex_count, VFILE *const file) { face->vertex_count = vertex_count; for (size_t i = 0; i < vertex_count; i++) { face->vertices[i] = VFile_ReadU16(file); face->texture_zw[i].z = 1.0f; face->texture_zw[i].w = 1.0f; } const uint16_t texture_idx = VFile_ReadU16(file); face->texture_idx = texture_idx & 0x7FFF; face->double_sided = (texture_idx & 0x8000) != 0; face->enable_reflections = false; } static void M_ReadObjectMesh(OBJECT_MESH *const mesh, VFILE *const file) { M_ReadVertex(&mesh->center, file); mesh->radius = VFile_ReadS16(file); VFile_Skip(file, sizeof(int16_t)); mesh->enable_reflections = false; mesh->enable_caustics = false; mesh->depth_adjustment = 0.005; { mesh->num_vertices = VFile_ReadS16(file); mesh->vertices = GameBuf_Alloc(sizeof(XYZ_16) * mesh->num_vertices, GBUF_MESHES); for (int32_t i = 0; i < mesh->num_vertices; i++) { M_ReadVertex(&mesh->vertices[i], file); } } { mesh->num_lights = VFile_ReadS16(file); if (mesh->num_lights > 0) { mesh->lighting.normals = GameBuf_Alloc(sizeof(XYZ_16) * mesh->num_lights, GBUF_MESHES); for (int32_t i = 0; i < mesh->num_lights; i++) { M_ReadVertex(&mesh->lighting.normals[i], file); } } else { mesh->lighting.lights = GameBuf_Alloc( sizeof(int16_t) * ABS(mesh->num_lights), GBUF_MESHES); for (int32_t i = 0; i < ABS(mesh->num_lights); i++) { mesh->lighting.lights[i] = VFile_ReadS16(file); } } } { mesh->tex_face4s.count = VFile_ReadS16(file); size_t pos = VFile_GetPos(file); VFile_Skip(file, 10 * mesh->tex_face4s.count); mesh->tex_face3s.count = VFile_ReadS16(file); VFile_Skip(file, 8 * mesh->tex_face3s.count); mesh->flat_face4s.count = VFile_ReadS16(file); VFile_Skip(file, 10 * mesh->flat_face4s.count); mesh->flat_face3s.count = VFile_ReadS16(file); VFile_SetPos(file, pos); mesh->tex_faces.count = mesh->tex_face4s.count + mesh->tex_face3s.count; mesh->flat_faces.count = mesh->flat_face4s.count + mesh->flat_face3s.count; mesh->all_faces.count = mesh->tex_faces.count + mesh->flat_faces.count; FACE *face_ptr = GameBuf_Alloc(sizeof(FACE) * mesh->all_faces.count, GBUF_MESHES); mesh->all_faces.data = face_ptr; mesh->tex_faces.data = face_ptr; mesh->tex_face4s.data = face_ptr; for (int32_t i = 0; i < mesh->tex_face4s.count; i++) { M_ReadFace(face_ptr++, 4, file); } VFile_Skip(file, 2); mesh->tex_face3s.data = face_ptr; for (int32_t i = 0; i < mesh->tex_face3s.count; i++) { M_ReadFace(face_ptr++, 3, file); } VFile_Skip(file, 2); mesh->flat_faces.data = face_ptr; mesh->flat_face4s.data = face_ptr; for (int32_t i = 0; i < mesh->flat_face4s.count; i++) { M_ReadFace(face_ptr++, 4, file); } VFile_Skip(file, 2); mesh->flat_face3s.data = face_ptr; for (int32_t i = 0; i < mesh->flat_face3s.count; i++) { M_ReadFace(face_ptr++, 3, file); } VFile_Skip(file, 2); } } void Level_Section_ReadObjectMeshes(LEVEL_CONTEXT *const ctx, VFILE *const file) { BENCHMARK benchmark = Benchmark_Start(); const int32_t num_meshes = VFile_ReadS32(file); LOG_INFO("object mesh data: %d", num_meshes); const size_t data_start_pos = VFile_GetPos(file); VFile_Skip(file, num_meshes * sizeof(int16_t)); LEVEL_CONTEXT_INFO *const info = &ctx->info; info->mesh_ptr_count = VFile_ReadS32(file); LOG_INFO("object mesh offsets: %d", info->mesh_ptr_count); const int32_t alloc_size = info->mesh_ptr_count * sizeof(int32_t); int32_t *mesh_offsets = Memory_Alloc(alloc_size); VFile_Read(file, mesh_offsets, alloc_size); const size_t end_pos = VFile_GetPos(file); VFile_SetPos(file, data_start_pos); Object_InitialiseMeshes( info->mesh_ptr_count + Inject_GetDataCount(IDT_MESH_POINTERS)); Level_Section_AppendObjectMeshes(info->mesh_ptr_count, mesh_offsets, file); VFile_SetPos(file, end_pos); Memory_FreePointer(&mesh_offsets); Benchmark_End(&benchmark, nullptr); } void Level_Section_AppendObjectMeshes( const int32_t num_offsets, const int32_t *const offsets, VFILE *const file) { #define L_ALIGN 2 // Savegames identify meshes by their file pointer values divided by 2. // (Historically, meshes were stored in int16_t[] arrays, so the so-called // "pointers" are really just array indices into that layout.) // // Original level meshes work fine under this scheme, but injected meshes // are different, as they come from separate VFiles and bring their own // pointer values. To prevent conflicts, calling // Level_Section_AppendObjectMeshes() for injected content must assign // unique pseudo-pointers. // // Rules for injected meshes: // - Pointers do not need to match real file offsets. // - They only need to be unique and preserve ordering. // // Only the original level data requires true offset congruence so that old // savegames remain compatible. For everything else, simple linear indexing // is sufficient. int32_t base_index = 0; if (Object_GetMeshCount() > 0) { // NOTE(Dash): Not assuming offsets are strictly increasing, so we scan // all meshes and pick the max. for (int32_t i = 0; i < Object_GetMeshCount(); i++) { base_index = MAX(base_index, Object_GetMeshOffset(Object_GetMesh(i))); } base_index += L_ALIGN; } // Construct and store distinct meshes only e.g. Lara's hips are referenced // by several pointers as a dummy mesh. VECTOR *const unique_offsets = Vector_CreateAtCapacity(sizeof(int32_t), num_offsets); int32_t pointer_map[num_offsets]; for (int32_t i = 0; i < num_offsets; i++) { const int32_t pointer = offsets[i] + base_index; const int32_t index = Vector_IndexOf(unique_offsets, (void *)&pointer); if (index == -1) { pointer_map[i] = unique_offsets->count; Vector_Add(unique_offsets, (void *)&pointer); } else { pointer_map[i] = index; } } OBJECT_MESH *const meshes = GameBuf_Alloc(sizeof(OBJECT_MESH) * unique_offsets->count, GBUF_MESHES); size_t start_pos = VFile_GetPos(file); for (int32_t i = 0; i < unique_offsets->count; i++) { const int32_t pointer = *(const int32_t *)Vector_Get(unique_offsets, i); VFile_SetPos(file, start_pos + pointer - base_index); M_ReadObjectMesh(&meshes[i], file); // The original data position is required for backward compatibility // with savegame files, specifically for Lara's mesh pointers. Object_SetMeshOffset(&meshes[i], pointer / L_ALIGN); } for (int32_t i = 0; i < num_offsets; i++) { Object_StoreMesh(&meshes[pointer_map[i]]); } #undef L_ALIGN LOG_INFO("%d unique meshes constructed", unique_offsets->count); Vector_Free(unique_offsets); } ================================================ FILE: src/trx/game/level/sections/objects.c ================================================ #include #include #include #include #include #include #include #include #include #include static void M_ReadPosition(XYZ_32 *const pos, VFILE *const file) { pos->x = VFile_ReadS32(file); pos->y = VFile_ReadS32(file); pos->z = VFile_ReadS32(file); } static void M_ReadShade( const LEVEL_FORMAT_LOADER *const loader, SHADE *const shade, VFILE *const file) { shade->value_1 = VFile_ReadS16(file); if (loader->game_version == 1) { shade->value_2 = shade->value_1; } else { shade->value_2 = VFile_ReadS16(file); } } static void M_ReadBounds16(BOUNDS_16 *const bounds, VFILE *const file) { bounds->min.x = VFile_ReadS16(file); bounds->max.x = VFile_ReadS16(file); bounds->min.y = VFile_ReadS16(file); bounds->max.y = VFile_ReadS16(file); bounds->min.z = VFile_ReadS16(file); bounds->max.z = VFile_ReadS16(file); } void Level_Section_ReadObjects(LEVEL_CONTEXT *const ctx, VFILE *const file) { BENCHMARK benchmark = Benchmark_Start(); const LEVEL_FORMAT_LOADER *const loader = ctx->loader; const int32_t num_objects = VFile_ReadS32(file); LOG_INFO("objects: %d", num_objects); for (int32_t i = 0; i < num_objects; i++) { OBJECT fallback_obj = {}; const int32_t game_obj_id = VFile_ReadS32(file); OBJECT *obj = Object_GetByGameID(game_obj_id); if (obj == nullptr) { if (loader->game_version == 3) { // TODO: remove this check after we implement the items obj = &fallback_obj; } else { Shell_ExitSystemFmt("Invalid object ID: %d", game_obj_id); } } obj->mesh_count = VFile_ReadS16(file); obj->mesh_idx = VFile_ReadS16(file); obj->bone_idx = VFile_ReadS32(file) / ANIM_BONE_SIZE; obj->frame_ofs = VFile_ReadU32(file); obj->frame_base = nullptr; obj->anim_idx = VFile_ReadS16(file); obj->loaded = true; } Benchmark_End(&benchmark, nullptr); } void Level_Section_ReadStaticObjects( LEVEL_CONTEXT *const ctx, VFILE *const file) { BENCHMARK benchmark = Benchmark_Start(); const int32_t num_objects = VFile_ReadS32(file); LOG_INFO("static objects: %d", num_objects); typedef struct { int32_t static_id; int16_t mesh_idx; BOUNDS_16 draw_bounds; BOUNDS_16 collision_bounds; uint16_t flags; } M_STATIC_OBJ_3D_TEMP; M_STATIC_OBJ_3D_TEMP *tmp_statics = Memory_Alloc(sizeof(M_STATIC_OBJ_3D_TEMP) * num_objects); int32_t max_static_id = -1; for (int32_t i = 0; i < num_objects; i++) { tmp_statics[i].static_id = VFile_ReadS32(file); if (tmp_statics[i].static_id < 0) { Shell_ExitSystemFmt( "Invalid static ID: %d", tmp_statics[i].static_id); } max_static_id = MAX(max_static_id, tmp_statics[i].static_id); tmp_statics[i].mesh_idx = VFile_ReadS16(file); M_ReadBounds16(&tmp_statics[i].draw_bounds, file); M_ReadBounds16(&tmp_statics[i].collision_bounds, file); tmp_statics[i].flags = VFile_ReadU16(file); } LOG_INFO("max static id: %d", max_static_id); int32_t injection_max_id = Inject_GetMaxStaticObject3DId(); if (injection_max_id < 0) { injection_max_id = -1; } const int32_t capacity = MAX(max_static_id, injection_max_id) + 1; Object_InitialiseStaticObjects3D(capacity); for (int32_t i = 0; i < num_objects; i++) { STATIC_OBJECT_3D *const obj = Object_Get3DStatic(tmp_statics[i].static_id); obj->mesh_idx = tmp_statics[i].mesh_idx; obj->loaded = true; obj->draw_bounds = tmp_statics[i].draw_bounds; obj->collision_bounds = tmp_statics[i].collision_bounds; obj->collidable = (tmp_statics[i].flags & 1) == 0; obj->visible = (tmp_statics[i].flags & 2) != 0; Object_GetMesh(obj->mesh_idx)->enable_caustics = obj->visible; } Memory_FreePointer(&tmp_statics); Benchmark_End(&benchmark, nullptr); } void Level_Section_ReadSpriteSequences( LEVEL_CONTEXT *const ctx, VFILE *const file) { BENCHMARK benchmark = Benchmark_Start(); const int32_t num_sequences = VFile_ReadS32(file); LOG_DEBUG("sprite sequences: %d", num_sequences); int32_t injection_max_id = Inject_GetMaxStaticObject2DId(); if (injection_max_id < 0) { injection_max_id = -1; } const int32_t capacity = MAX(num_sequences - 1, injection_max_id) + 1; Object_InitialiseStaticObjects2D(capacity); int32_t static_id = 0; for (int32_t i = 0; i < num_sequences; i++) { const int32_t id = VFile_ReadS32(file); const int16_t num_meshes = VFile_ReadS16(file); const int16_t mesh_idx = VFile_ReadS16(file); // In OG, a sprite was determined as either a game or static type based // on the original total game object count. As IDs are freely assignable // in TRX, a defined list of game sprites must instead be referred to. const OBJECT_ID object_id = Object_FromGameID(id); if (object_id != NO_OBJECT && Object_IsType(object_id, g_GameSpriteObjects)) { OBJECT *const obj = Object_Get(object_id); obj->mesh_count = num_meshes; obj->mesh_idx = mesh_idx; obj->anim_idx = NO_ANIM; obj->loaded = true; } else { STATIC_OBJECT_2D *const obj = Object_Get2DStatic(static_id); if (obj == nullptr) { Shell_ExitSystemFmt("Invalid sprite slot (%d)", id); break; } obj->frame_count = ABS(num_meshes); obj->texture_idx = mesh_idx; obj->loaded = true; static_id++; } } Benchmark_End(&benchmark, nullptr); } void Level_Section_ReadItems(LEVEL_CONTEXT *const ctx, VFILE *const file) { BENCHMARK benchmark = Benchmark_Start(); const LEVEL_FORMAT_LOADER *const loader = ctx->loader; const int32_t num_items = VFile_ReadS32(file); LOG_INFO("items: %d", num_items); if (num_items > MAX_ITEMS) { Shell_ExitSystem("Too many items"); goto finish; } Item_InitialiseItems(num_items); for (int32_t i = 0; i < num_items; i++) { ITEM *const item = Item_Get(i); const int16_t obj_id = VFile_ReadS16(file); item->object_id = Object_FromGameID(obj_id); if (item->object_id == NO_OBJECT) { if (loader->game_version == 3) { // TODO: remove this check after we implement the items LOG_ERROR("Unsupported object #%d", obj_id); item->object_id = O_DUMMY; } else { Shell_ExitSystemFmt( "Bad object number (%d) on item %d", obj_id, i); goto finish; } } item->room_num = VFile_ReadS16(file); M_ReadPosition(&item->pos, file); item->rot.y = VFile_ReadS16(file); M_ReadShade(loader, &item->shade, file); item->flags = VFile_ReadS16(file); } finish: Benchmark_End(&benchmark, nullptr); } ================================================ FILE: src/trx/game/level/sections/pathing.c ================================================ #include #include #include #include #include #include void Level_Section_ReadPathingData(LEVEL_CONTEXT *const ctx, VFILE *const file) { BENCHMARK benchmark = Benchmark_Start(); const LEVEL_FORMAT_LOADER *const loader = ctx->loader; const int32_t num_boxes = VFile_ReadS32(file); Box_InitialiseBoxes(num_boxes); for (int32_t i = 0; i < num_boxes; i++) { BOX_INFO *const box = Box_GetBox(i); if (loader->game_version == 1) { box->left = VFile_ReadS32(file); box->right = VFile_ReadS32(file); box->top = VFile_ReadS32(file); box->bottom = VFile_ReadS32(file); } else { box->left = VFile_ReadU8(file) << WALL_SHIFT; box->right = (VFile_ReadU8(file) << WALL_SHIFT) - 1; box->top = VFile_ReadU8(file) << WALL_SHIFT; box->bottom = (VFile_ReadU8(file) << WALL_SHIFT) - 1; } box->height = VFile_ReadS16(file); box->overlap_index = VFile_ReadS16(file); if (loader->game_version == 3 && (box->overlap_index & BOX_BLOCKABLE) != 0) { box->overlap_index |= BOX_BLOCKED; } } const int32_t num_overlaps = VFile_ReadS32(file); int16_t *const overlaps = Box_InitialiseOverlaps(num_overlaps); VFile_Read(file, overlaps, sizeof(int16_t) * num_overlaps); for (int32_t flip_status = 0; flip_status < 2; flip_status++) { for (int32_t zone_idx = 0; zone_idx < Box_GetZoneCount(); zone_idx++) { int16_t *const ground_zone = Box_GetGroundZone(flip_status, zone_idx); VFile_Read(file, ground_zone, sizeof(int16_t) * num_boxes); if (loader->game_version == 1 && zone_idx == 1) { // TODO: remove once TombEditor is updated to generate the same // number of zones as TR2 via injections. This allows enemies of // LOT_SETUP_CLIMBER type to safely be used in TR1 in the // meantime. int16_t *const duped_zone = Box_GetGroundZone(flip_status, 3); memcpy(duped_zone, ground_zone, sizeof(int16_t) * num_boxes); } } int16_t *const fly_zone = Box_GetFlyZone(flip_status); VFile_Read(file, fly_zone, sizeof(int16_t) * num_boxes); } Benchmark_End(&benchmark, nullptr); } ================================================ FILE: src/trx/game/level/sections/read.h ================================================ #pragma once #include #include #define ANIM_BONE_SIZE 4 void Level_Section_ReadPalettes(LEVEL_CONTEXT *ctx, VFILE *file); void Level_Section_ReadTexturePages(LEVEL_CONTEXT *ctx, VFILE *file); void Level_Section_ReadRooms(LEVEL_CONTEXT *ctx, VFILE *file); void Level_Section_ReadObjectMeshes(LEVEL_CONTEXT *ctx, VFILE *file); void Level_Section_ReadAnims(LEVEL_CONTEXT *ctx, VFILE *file); void Level_Section_ReadAnimChanges(LEVEL_CONTEXT *ctx, VFILE *file); void Level_Section_ReadAnimRanges(LEVEL_CONTEXT *ctx, VFILE *file); void Level_Section_ReadAnimCommands(LEVEL_CONTEXT *ctx, VFILE *file); void Level_Section_ReadAnimBones(LEVEL_CONTEXT *ctx, VFILE *file); void Level_Section_ReadAnimFrames(LEVEL_CONTEXT *ctx, VFILE *file); void Level_Section_ReadObjects(LEVEL_CONTEXT *ctx, VFILE *file); void Level_Section_ReadStaticObjects(LEVEL_CONTEXT *ctx, VFILE *file); void Level_Section_ReadObjectTextures(LEVEL_CONTEXT *ctx, VFILE *file); void Level_Section_ReadSpriteTextures(LEVEL_CONTEXT *ctx, VFILE *file); void Level_Section_ReadSpriteSequences(LEVEL_CONTEXT *ctx, VFILE *file); void Level_Section_ReadPathingData(LEVEL_CONTEXT *ctx, VFILE *file); void Level_Section_ReadAnimatedTextureRanges(LEVEL_CONTEXT *ctx, VFILE *file); void Level_Section_ReadLightMap(LEVEL_CONTEXT *ctx, VFILE *file); void Level_Section_ReadCinematicFrames(LEVEL_CONTEXT *ctx, VFILE *file); void Level_Section_ReadCamerasAndSinks(LEVEL_CONTEXT *ctx, VFILE *file); void Level_Section_ReadItems(LEVEL_CONTEXT *ctx, VFILE *file); void Level_Section_ReadDemoData(LEVEL_CONTEXT *ctx, VFILE *file); void Level_Section_ReadSoundSources(LEVEL_CONTEXT *ctx, VFILE *file); void Level_Section_ReadSamples(LEVEL_CONTEXT *ctx, VFILE *file); ================================================ FILE: src/trx/game/level/sections/rooms.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_NO_ROOM_LEGACY 255 #define M_NO_BOX_TR3_LEGACY 0x7FF static void M_ReadPosition(XYZ_32 *const pos, VFILE *const file) { pos->x = VFile_ReadS32(file); pos->y = VFile_ReadS32(file); pos->z = VFile_ReadS32(file); } static void M_ReadVertex(XYZ_16 *const vertex, VFILE *const file) { vertex->x = VFile_ReadS16(file); vertex->y = VFile_ReadS16(file); vertex->z = VFile_ReadS16(file); } static void M_ReadShade( const LEVEL_FORMAT_LOADER *const loader, SHADE *const shade, VFILE *const file) { shade->value_1 = VFile_ReadS16(file); if (loader->game_version == 1) { shade->value_2 = shade->value_1; } else { shade->value_2 = VFile_ReadS16(file); } } static void M_ReadFace( FACE *const face, const size_t vertex_count, VFILE *const file) { face->vertex_count = vertex_count; for (size_t i = 0; i < vertex_count; i++) { face->vertices[i] = VFile_ReadU16(file); face->texture_zw[i].z = 1.0f; face->texture_zw[i].w = 1.0f; } const uint16_t texture_idx = VFile_ReadU16(file); face->texture_idx = texture_idx & 0x7FFF; face->double_sided = (texture_idx & 0x8000) != 0; face->enable_reflections = false; } static void M_ReadRoomMesh( const LEVEL_FORMAT_LOADER *const loader, const int32_t room_num, VFILE *const file, const INJECTION_MESH_META inj_data) { ROOM *const room = Room_Get(room_num); const uint32_t mesh_length = VFile_ReadU32(file); const size_t start_pos = VFile_GetPos(file); { room->mesh.num_vertices = VFile_ReadS16(file); const int32_t alloc_count = room->mesh.num_vertices + inj_data.num_vertices; room->mesh.vertices = GameBuf_Alloc(sizeof(ROOM_VERTEX) * alloc_count, GBUF_ROOM_MESH); for (int32_t i = 0; i < room->mesh.num_vertices; i++) { ROOM_VERTEX *const vertex = &room->mesh.vertices[i]; M_ReadVertex(&vertex->pos, file); if (loader->game_version == 1) { vertex->light_base = VFile_ReadS16(file); vertex->flags.disable_wibble = false; vertex->flags.move = false; vertex->flags.glow = false; vertex->color = COLOR_RGBA_8888_WHITE; } else if (loader->game_version == 2) { vertex->light_base = VFile_ReadS16(file); vertex->light_table_value = VFile_ReadU8(file); const uint8_t flags = VFile_ReadU8(file); vertex->flags.disable_wibble = (flags & 0x80u) != 0u; vertex->flags.move = false; vertex->flags.glow = false; VFile_Skip(file, 2); vertex->color = COLOR_RGBA_8888_WHITE; } else if (loader->game_version == 3) { VFile_Skip(file, 2); // lighting - unused in TR3 const uint16_t flags = VFile_ReadU16(file); vertex->flags.disable_wibble = (flags & 0x8000u) != 0u; vertex->flags.move = (flags & 0x2000u) != 0u; vertex->flags.glow = (flags & 0x4000u) != 0u; vertex->color = Color_ARGB1555ToRGBA8888(VFile_ReadU16(file)); vertex->color.a = 255; vertex->light_base = 0; } } } { room->mesh.face4s.count = VFile_ReadS16(file); const size_t pos = VFile_GetPos(file); VFile_Skip(file, 10 * room->mesh.face4s.count); room->mesh.face3s.count = VFile_ReadS16(file); VFile_SetPos(file, pos); room->mesh.all_faces.count = room->mesh.face4s.count + inj_data.num_quads + room->mesh.face3s.count + inj_data.num_triangles; FACE *face_ptr = GameBuf_Alloc( sizeof(FACE) * room->mesh.all_faces.count, GBUF_ROOM_MESH); room->mesh.all_faces.data = face_ptr; room->mesh.face4s.data = face_ptr; for (int32_t i = 0; i < room->mesh.face4s.count; i++) { M_ReadFace(face_ptr++, 4, file); } for (int32_t i = 0; i < inj_data.num_quads; i++) { face_ptr->vertex_count = 4; face_ptr++; } VFile_Skip(file, 2); room->mesh.face3s.data = face_ptr; for (int32_t i = 0; i < room->mesh.face3s.count; i++) { M_ReadFace(face_ptr++, 3, file); } for (int32_t i = 0; i < inj_data.num_triangles; i++) { face_ptr->vertex_count = 3; face_ptr++; } } { room->mesh.sprites.count = VFile_ReadS16(file); const int32_t alloc_count = room->mesh.sprites.count + inj_data.num_static_2ds; room->mesh.sprites.data = GameBuf_Alloc(sizeof(ROOM_SPRITE) * alloc_count, GBUF_ROOM_MESH); for (int32_t i = 0; i < room->mesh.sprites.count; i++) { ROOM_SPRITE *const sprite = &room->mesh.sprites.data[i]; sprite->vertex = VFile_ReadU16(file); sprite->texture = VFile_ReadU16(file); } } const size_t total_read = (VFile_GetPos(file) - start_pos) / sizeof(int16_t); ASSERT(total_read == mesh_length); } static XYZ_16 M_ComputePortalNormal(PORTAL *const p) { // This fixes a bug in TombEditor where certain portals would get emitted // with wrong normals. TE is guaranteed to emit normals with a good sign in // the Y component, but for sloped ceiling portals, their X and Z // compontents have the wrong sign. // // To fix this, we compute the normal the regular way. We don't know which // way the portal faces, but since the Y component is guaranteed to be // good, we can orient our vector using this information, which should fix // the X/Z components. ASSERT(p != nullptr); // Geometric normal (ab x ac) const XYZ_32 a = { p->vertex[0].x, p->vertex[0].y, p->vertex[0].z }; const XYZ_32 b = { p->vertex[1].x, p->vertex[1].y, p->vertex[1].z }; const XYZ_32 c = { p->vertex[2].x, p->vertex[2].y, p->vertex[2].z }; const XYZ_32 ab = { b.x - a.x, b.y - a.y, b.z - a.z }; const XYZ_32 ac = { c.x - a.x, c.y - a.y, c.z - a.z }; XYZ_32 n = { (ab.y * ac.z) - (ab.z * ac.y), (ab.z * ac.x) - (ab.x * ac.z), (ab.x * ac.y) - (ab.y * ac.x), }; // Degenerate guard if (n.x == 0 && n.y == 0 && n.z == 0) { return (XYZ_16) { .x = 0, .y = 1, .z = 0 }; } // Integer normalization const int32_t gx = ABS(n.x); const int32_t gy = ABS(n.y); const int32_t gz = ABS(n.z); int32_t g = gx; if (gy != 0) { g = Math_GCD(g, gy); } if (gz != 0) { g = Math_GCD(g, gz); } if (g == 0) { g = 1; } n.x /= g; n.y /= g; n.z /= g; // NOTE: we only care about horizontal portals. if (p->normal.y == 0) { return p->normal; } if (p->normal.y != n.y) { n.x *= -1; n.y *= -1; n.z *= -1; } return (XYZ_16) { n.x, n.y, n.z }; } void Level_Section_ReadRooms(LEVEL_CONTEXT *const ctx, VFILE *const file) { BENCHMARK benchmark = Benchmark_Start(); const LEVEL_FORMAT_LOADER *const loader = ctx->loader; const int32_t num_rooms = VFile_ReadS16(file); LOG_INFO("rooms: %d", num_rooms); if (num_rooms > MAX_ROOMS) { Shell_ExitSystem("Too many rooms"); goto finish; } Room_InitialiseRooms(num_rooms); for (int32_t i = 0; i < num_rooms; i++) { ROOM *const room = Room_Get(i); room->pos.x = VFile_ReadS32(file); room->pos.y = 0; room->pos.z = VFile_ReadS32(file); room->min_floor = VFile_ReadS32(file); room->max_ceiling = VFile_ReadS32(file); const INJECTION_MESH_META inj_data = Inject_GetRoomMeshMeta(i); M_ReadRoomMesh(loader, i, file, inj_data); const int16_t num_portals = VFile_ReadS16(file); if (num_portals <= 0) { room->portals = nullptr; } else { room->portals = GameBuf_Alloc( sizeof(PORTAL) * num_portals + sizeof(PORTALS), GBUF_ROOM_PORTALS); room->portals->count = num_portals; for (int32_t j = 0; j < num_portals; j++) { PORTAL *const portal = &room->portals->portal[j]; portal->room_num = VFile_ReadS16(file); M_ReadVertex(&portal->normal, file); for (int32_t k = 0; k < 4; k++) { M_ReadVertex(&portal->vertex[k], file); } } } room->size.z = VFile_ReadS16(file); room->size.x = VFile_ReadS16(file); const int32_t sector_count = room->size.x * room->size.z; room->sectors = GameBuf_Alloc(sizeof(SECTOR) * sector_count, GBUF_ROOM_SECTORS); for (int32_t j = 0; j < sector_count; j++) { SECTOR *const sector = &room->sectors[j]; sector->idx = VFile_ReadU16(file); if (loader->game_version == 3) { uint16_t misc_info = VFile_ReadU16(file); sector->fx = (uint8_t)(misc_info & 0x0F); sector->box = (int16_t)((misc_info & 0x7FF0) >> 4); sector->stopper = (bool)((misc_info & 0x8000) >> 15); if (sector->box == M_NO_BOX_TR3_LEGACY) { sector->box = NO_BOX; } } else { sector->fx = 0; sector->box = VFile_ReadS16(file); sector->stopper = false; } sector->portal_room.pit = VFile_ReadU8(file); sector->floor.height = VFile_ReadS8(file) * STEP_L; sector->portal_room.sky = VFile_ReadU8(file); sector->ceiling.height = VFile_ReadS8(file) * STEP_L; if (sector->portal_room.pit == M_NO_ROOM_LEGACY) { sector->portal_room.pit = NO_ROOM; } if (sector->portal_room.sky == M_NO_ROOM_LEGACY) { sector->portal_room.sky = NO_ROOM; } } room->ambient = VFile_ReadS16(file); if (loader->game_version == 1) { room->light_mode = RLM_NORMAL; } else if (loader->game_version == 2) { VFile_Skip(file, sizeof(int16_t)); // Unused second ambient room->light_mode = VFile_ReadS16(file); } else { room->light_mode = VFile_ReadS16(file); } room->num_lights = VFile_ReadS16(file); room->lights = room->num_lights == 0 ? nullptr : GameBuf_Alloc(sizeof(LIGHT) * room->num_lights, GBUF_ROOM_LIGHTS); for (int32_t j = 0; j < room->num_lights; j++) { LIGHT *const light = &room->lights[j]; if (loader->game_version == 3) { // TR3 room lights use the LIGHT_INFO struct layout: // pos (s32*3) + rgb (u8*3) + type (u8) + union (8 bytes). M_ReadPosition(&light->pos, file); light->color.r = VFile_ReadU8(file); light->color.g = VFile_ReadU8(file); light->color.b = VFile_ReadU8(file); light->type = VFile_ReadU8(file); if (light->type != 0u) { light->dir.x = VFile_ReadS16(file); light->dir.y = VFile_ReadS16(file); light->dir.z = VFile_ReadS16(file); VFile_Skip(file, sizeof(int16_t)); // pad light->shade.value_1 = 0; light->shade.value_2 = 0; light->falloff.value_1 = 0; light->falloff.value_2 = 0; } else { int32_t intensity = VFile_ReadS32(file); const int32_t falloff = VFile_ReadS32(file); CLAMP(intensity, INT16_MIN, INT16_MAX); light->shade.value_1 = (int16_t)intensity; light->shade.value_2 = (int16_t)intensity; light->falloff.value_1 = falloff; light->falloff.value_2 = falloff; light->dir = (XYZ_16) { 0, 0, 0 }; } } else { M_ReadPosition(&light->pos, file); M_ReadShade(loader, &light->shade, file); light->falloff.value_1 = VFile_ReadS32(file); if (loader->game_version >= 2) { light->falloff.value_2 = VFile_ReadS32(file); } else { light->falloff.value_2 = light->falloff.value_1; } light->color = COLOR_RGB_888_WHITE; light->type = 0; light->dir = (XYZ_16) { 0, 0, 0 }; } } room->num_static_meshes = VFile_ReadS16(file); const int32_t static_count = room->num_static_meshes + inj_data.num_static_3ds; room->static_meshes = static_count == 0 ? nullptr : GameBuf_Alloc( sizeof(STATIC_MESH) * static_count, GBUF_ROOM_STATIC_MESHES); for (int32_t j = 0; j < room->num_static_meshes; j++) { STATIC_MESH *const mesh = &room->static_meshes[j]; M_ReadPosition(&mesh->pos, file); mesh->rot.y = VFile_ReadS16(file); M_ReadShade(loader, &mesh->shade, file); mesh->static_num = VFile_ReadS16(file); mesh->draw_num = -1; } room->flipped_room = VFile_ReadS16(file); const uint16_t flags = VFile_ReadU16(file); // clang-format off room->flags.underwater = (flags & 0x01) != 0; room->flags.outside = (flags & 0x08) != 0; room->flags.dynamic_lit = (flags & 0x10) != 0; room->flags.wind = (flags & 0x20) != 0; room->flags.inside = (flags & 0x40) != 0; room->flags.swamp = (flags & 0x80) != 0; // clang-format on OUTPUT_ROOM_BIND *const bind = Output_Bind_GetRoom(room); bind->bound_left = Viewport_GetMaxX(VIEWPORT_GAME); bind->bound_top = Viewport_GetMaxY(VIEWPORT_GAME); bind->bound_bottom = Viewport_GetMinY(VIEWPORT_GAME); bind->bound_right = Viewport_GetMinX(VIEWPORT_GAME); room->item_num = NO_ITEM; room->effect_num = NO_EFFECT; if (loader->game_version == 3) { room->water_scheme = VFile_ReadU8(file); room->reverb_info = VFile_ReadU8(file); VFile_Skip(file, 1); } } for (int32_t i = 0; i < num_rooms; i++) { ROOM *const room = Room_Get(i); if (room->portals == nullptr) { continue; } for (int32_t j = 0; j < room->portals->count; j++) { PORTAL *const portal = &room->portals->portal[j]; const XYZ_16 new_normal = M_ComputePortalNormal(portal); if (new_normal.x != portal->normal.x || new_normal.y != portal->normal.y || new_normal.z != portal->normal.z) { LOG_WARNING("Fixed room %d, portal normal %d", i, j); portal->normal = new_normal; } } } Room_InitialiseFlipStatus(); const int32_t floor_data_size = VFile_ReadS32(file); int16_t *floor_data = Memory_Alloc(sizeof(int16_t) * floor_data_size); VFile_Read(file, floor_data, sizeof(int16_t) * floor_data_size); Room_ParseFloorData(floor_data); Memory_FreePointer(&floor_data); Room_BuildOutsideTable(); finish: Benchmark_End(&benchmark, nullptr); } ================================================ FILE: src/trx/game/level/sections/textures.c ================================================ #include #include #include #include #include #include #include #include #include #include #include typedef struct { const RGB_888 *palette; const uint8_t *input_8_page; const uint16_t *input_16_page; RGBA_8888 *output_32_page; } M_TEXTURE_PAGE_DECODE_JOB; static void M_DecodeTR3ObjectTextureUVs(OBJECT_TEXTURE *const texture) { int16_t *const uv = (int16_t *)&texture->uv[0].u; uint8_t flags = 0; for (int32_t i = 0; i < 8; i++) { if ((uv[i] & 0x80) != 0) { uv[i] |= 0x00FF; flags |= 1 << i; } else { uv[i] &= 0xFF00; } } for (int32_t i = 0; i < 8; i++) { if ((flags & 1) != 0) { uv[i] -= 256; } else { uv[i] += 256; } flags >>= 1; } } static void M_Decode8BitTexturePage(void *const userdata) { const M_TEXTURE_PAGE_DECODE_JOB *const job = userdata; const uint8_t *input = job->input_8_page; RGBA_8888 *output = job->output_32_page; for (int32_t i = 0; i < TEXTURE_PAGE_SIZE; i++) { const uint8_t index = *input++; const RGB_888 pix = job->palette[index]; output->r = pix.r; output->g = pix.g; output->b = pix.b; output->a = index == 0 ? 0 : 0xFF; output++; } } static void M_Decode16BitTexturePage(void *const userdata) { const M_TEXTURE_PAGE_DECODE_JOB *const job = userdata; const uint16_t *input = job->input_16_page; RGBA_8888 *output = job->output_32_page; for (int32_t i = 0; i < TEXTURE_PAGE_SIZE; i++) { *output++ = Color_ARGB1555ToRGBA8888(*input++); } } void Level_Section_ReadPalettes(LEVEL_CONTEXT *const ctx, VFILE *const file) { BENCHMARK benchmark = Benchmark_Start(); const int32_t palette_size = 256; const LEVEL_FORMAT_LOADER *const loader = ctx->loader; LEVEL_CONTEXT_INFO *const info = &ctx->info; info->palette.size = palette_size; info->palette.data_24 = Memory_Alloc(sizeof(RGB_888) * palette_size); VFile_Read(file, info->palette.data_24, sizeof(RGB_888) * palette_size); info->palette.data_24[0].r = 0; info->palette.data_24[0].g = 0; info->palette.data_24[0].b = 0; for (int32_t i = 1; i < palette_size; i++) { RGB_888 *const col = &info->palette.data_24[i]; col->r = (col->r << 2) | (col->r >> 4); col->g = (col->g << 2) | (col->g >> 4); col->b = (col->b << 2) | (col->b >> 4); } if (loader->game_version == 1) { info->palette.data_32 = nullptr; } else { RGBA_8888 palette_16[palette_size]; info->palette.data_32 = Memory_Alloc(sizeof(RGB_888) * palette_size); VFile_Read(file, palette_16, sizeof(RGBA_8888) * palette_size); for (int32_t i = 0; i < palette_size; i++) { info->palette.data_32[i].r = palette_16[i].r; info->palette.data_32[i].g = palette_16[i].g; info->palette.data_32[i].b = palette_16[i].b; } } Benchmark_End(&benchmark, nullptr); } void Level_Section_ReadTexturePages(LEVEL_CONTEXT *const ctx, VFILE *const file) { BENCHMARK benchmark = Benchmark_Start(); const int32_t num_pages = VFile_ReadS32(file); const LEVEL_FORMAT_LOADER *const loader = ctx->loader; LEVEL_CONTEXT_INFO *const info = &ctx->info; info->textures.page_count = num_pages; LOG_INFO("texture pages: %d", num_pages); const int32_t extra_pages = Inject_GetDataCount(IDT_TEXTURE_PAGES); const int32_t texture_size_8_bit = (num_pages + extra_pages) * TEXTURE_PAGE_SIZE * sizeof(uint8_t); const int32_t texture_size_32_bit = (num_pages + extra_pages) * TEXTURE_PAGE_SIZE * sizeof(RGBA_8888); info->textures.pages_8 = Memory_Alloc(texture_size_8_bit); VFile_Read(file, info->textures.pages_8, num_pages * TEXTURE_PAGE_SIZE); info->textures.pages_32 = Memory_Alloc(texture_size_32_bit); THREAD_POOL *const pool = ThreadPool_Create(-1); M_TEXTURE_PAGE_DECODE_JOB *const jobs = Memory_Alloc(sizeof(*jobs) * num_pages); uint16_t *input_16 = nullptr; for (int32_t i = 0; i < num_pages; i++) { jobs[i].palette = info->palette.data_24; jobs[i].input_8_page = &info->textures.pages_8[i * TEXTURE_PAGE_SIZE]; jobs[i].input_16_page = nullptr; jobs[i].output_32_page = &info->textures.pages_32[i * TEXTURE_PAGE_SIZE]; } if (loader->game_version == 1) { for (int32_t i = 0; i < num_pages; i++) { ThreadPool_AddJob(pool, M_Decode8BitTexturePage, &jobs[i]); } } else { const int32_t texture_size_16_bit = num_pages * TEXTURE_PAGE_SIZE * sizeof(uint16_t); input_16 = Memory_Alloc(texture_size_16_bit); VFile_Read(file, input_16, texture_size_16_bit); for (int32_t i = 0; i < num_pages; i++) { jobs[i].input_16_page = &input_16[i * TEXTURE_PAGE_SIZE]; ThreadPool_AddJob(pool, M_Decode16BitTexturePage, &jobs[i]); } } ThreadPool_Wait(pool); Memory_FreePointer(&input_16); Memory_Free(jobs); ThreadPool_Destroy(pool); Benchmark_End(&benchmark, nullptr); } void Level_Section_ReadObjectTextures( LEVEL_CONTEXT *const ctx, VFILE *const file) { BENCHMARK benchmark = Benchmark_Start(); const int32_t num_textures = VFile_ReadS32(file); LEVEL_CONTEXT_INFO *const info = &ctx->info; info->textures.object_count = num_textures; LOG_INFO("object textures: %d", num_textures); Output_InitialiseObjectTextures( num_textures + Inject_GetDataCount(IDT_OBJECT_TEXTURES)); Level_Section_AppendObjectTextures( 0, 0, num_textures, file, ctx->loader->game_version >= 3); Benchmark_End(&benchmark, nullptr); } void Level_Section_AppendObjectTextures( const int32_t base_idx, const int16_t base_page_idx, const int32_t num_textures, VFILE *const file, const bool use_tr3_adjustment) { for (int32_t i = 0; i < num_textures; i++) { OBJECT_TEXTURE *const texture = Output_GetObjectTexture(base_idx + i); texture->uv_count = 4; // Default to 4 vertices texture->draw_type = VFile_ReadU16(file); texture->tex_page = VFile_ReadU16(file) + base_page_idx; for (int32_t j = 0; j < 4; j++) { texture->uv[j].u = VFile_ReadU16(file); texture->uv[j].v = VFile_ReadU16(file); } if (use_tr3_adjustment) { M_DecodeTR3ObjectTextureUVs(texture); } } } void Level_Section_ReadSpriteTextures( LEVEL_CONTEXT *const ctx, VFILE *const file) { BENCHMARK benchmark = Benchmark_Start(); const int32_t num_textures = VFile_ReadS32(file); LEVEL_CONTEXT_INFO *const info = &ctx->info; info->textures.sprite_count = num_textures; LOG_INFO("sprite textures: %d", num_textures); Output_InitialiseSpriteTextures( num_textures + Inject_GetDataCount(IDT_SPRITE_TEXTURES)); Level_Section_AppendSpriteTextures(0, 0, num_textures, file); Benchmark_End(&benchmark, nullptr); } void Level_Section_AppendSpriteTextures( const int32_t base_idx, const int16_t base_page_idx, const int32_t num_textures, VFILE *const file) { for (int32_t i = 0; i < num_textures; i++) { SPRITE_TEXTURE *const sprite = Output_GetSpriteTexture(base_idx + i); sprite->tex_page = VFile_ReadU16(file) + base_page_idx; sprite->offset = VFile_ReadU16(file); sprite->width = VFile_ReadU16(file); sprite->height = VFile_ReadU16(file); sprite->x0 = VFile_ReadS16(file); sprite->y0 = VFile_ReadS16(file); sprite->x1 = VFile_ReadS16(file); sprite->y1 = VFile_ReadS16(file); } } void Level_Section_ReadAnimatedTextureRanges( LEVEL_CONTEXT *const ctx, VFILE *const file) { BENCHMARK benchmark = Benchmark_Start(); const int32_t data_size = VFile_ReadS32(file); const size_t end_position = VFile_GetPos(file) + data_size * sizeof(int16_t); const int16_t num_ranges = VFile_ReadS16(file); LOG_INFO("animated texture ranges: %d", num_ranges); Output_InitialiseAnimatedTextures(num_ranges); for (int32_t i = 0; i < num_ranges; i++) { ANIMATED_TEXTURE_RANGE *const range = Output_GetAnimatedTextureRange(i); range->next_range = i == num_ranges - 1 ? nullptr : Output_GetAnimatedTextureRange(i + 1); // Level data is tied to the original logic in Output_AnimateTextures // and hence stores one less than the actual count here. range->num_textures = VFile_ReadS16(file) + 1; range->textures = GameBuf_Alloc( sizeof(int16_t) * range->num_textures, GBUF_ANIMATED_TEXTURE_RANGES); VFile_Read( file, range->textures, sizeof(int16_t) * range->num_textures); } VFile_SetPos(file, end_position); Benchmark_End(&benchmark, nullptr); } void Level_Section_ReadLightMap(LEVEL_CONTEXT *const ctx, VFILE *const file) { BENCHMARK benchmark = Benchmark_Start(); for (int32_t i = 0; i < 32; i++) { LIGHT_MAP *const light_map = Output_GetLightMap(i); VFile_Read(file, light_map->index, sizeof(uint8_t) * 256); light_map->index[0] = 0; } for (int32_t i = 0; i < 32; i++) { const LIGHT_MAP *const light_map = Output_GetLightMap(i); for (int32_t j = 0; j < 256; j++) { SHADE_MAP *const shade_map = Output_GetShadeMap(j); shade_map->index[i] = light_map->index[j]; } } Benchmark_End(&benchmark, nullptr); } ================================================ FILE: src/trx/game/level/settings.c ================================================ #include #include #include #include RGB_888 Level_GetWaterColor(void) { const GF_LEVEL *const level = GF_GetCurrentLevel(); if (level != nullptr && level->settings.water_color.is_present) { return level->settings.water_color.value; } return g_Config.visuals.water_color; } RGBA_8888 Level_GetFogColor(void) { const GF_LEVEL *const level = GF_GetCurrentLevel(); RGB_888 color = { 0, 0, 0 }; uint8_t alpha = 255; if (level != nullptr && level->settings.fog_transparency.is_present && level->settings.fog_transparency.value) { alpha = 0; } else if (level != nullptr && level->settings.fog_color.is_present) { color = level->settings.fog_color.value; } else if (g_Config.visuals.fog_transparency) { alpha = 0; } else { color = g_Config.visuals.fog_color; } return (RGBA_8888) { .r = color.r, .g = color.g, .b = color.b, .a = alpha }; } float Level_GetFogStart(void) { const GF_LEVEL *const level = GF_GetCurrentLevel(); if (level != nullptr && level->settings.fog_start.is_present) { return level->settings.fog_start.value; } return g_Config.visuals.fog_start; } float Level_GetFogEnd(void) { const GF_LEVEL *const level = GF_GetCurrentLevel(); if (level != nullptr && level->settings.fog_end.is_present) { return level->settings.fog_end.value; } return g_Config.visuals.fog_end; } bool Level_HasColdWater(void) { const GF_LEVEL *const level = GF_GetCurrentLevel(); if (level != nullptr && level->settings.cold_water.is_present) { return level->settings.cold_water.value; } if (g_GameFlow.settings.cold_water.is_present) { return g_GameFlow.settings.cold_water.value; } return false; } GF_DEATH_TILE Level_GetDeathTile(void) { const GF_LEVEL *const level = GF_GetCurrentLevel(); if (level != nullptr && level->settings.death_tile.is_present) { return level->settings.death_tile.value; } if (g_GameFlow.settings.death_tile.is_present) { return g_GameFlow.settings.death_tile.value; } return GF_DEATH_TILE_LAVA; } ================================================ FILE: src/trx/game/level/settings.h ================================================ #pragma once #include #include RGB_888 Level_GetWaterColor(void); RGBA_8888 Level_GetFogColor(void); float Level_GetFogStart(void); float Level_GetFogEnd(void); bool Level_HasColdWater(void); GF_DEATH_TILE Level_GetDeathTile(void); ================================================ FILE: src/trx/game/level.h ================================================ #pragma once #include #include #include #include #include ================================================ FILE: src/trx/game/los.h ================================================ #pragma once #include ================================================ FILE: src/trx/game/lua/assault_stats.c ================================================ #include #include #include #include #include #include #include static bool M_StoreAssaultTime(const float time) { GYM_TRACK_STATS *const assault = &g_Config.profile.assault_stats; uint32_t logic_time = (uint32_t)(time * LOGIC_FPS); int32_t insert_idx = -1; for (int32_t i = 0; i < MAX_ASSAULT_TIMES; i++) { if (assault->entries[i].time == 0 || logic_time < assault->entries[i].time) { insert_idx = i; break; } } if (insert_idx == -1) { return false; } for (int32_t i = MAX_ASSAULT_TIMES - 1; i > insert_idx; i--) { assault->entries[i] = assault->entries[i - 1]; } assault->total_attempts++; assault->entries[insert_idx].time = logic_time; assault->entries[insert_idx].attempt_num = assault->total_attempts; Config_Update(); return true; } static bool M_RemoveAssaultTimeAtIndex(const int32_t idx) { GYM_TRACK_STATS *const assault = &g_Config.profile.assault_stats; if (idx < 0 || idx >= MAX_ASSAULT_TIMES) { return false; } if (assault->entries[idx].time == 0) { return false; } for (int32_t i = idx; i < MAX_ASSAULT_TIMES - 1; i++) { assault->entries[i] = assault->entries[i + 1]; } assault->entries[MAX_ASSAULT_TIMES - 1].time = 0; assault->entries[MAX_ASSAULT_TIMES - 1].attempt_num = 0; Config_Update(); return true; } // trxc.assault_stats.record(time) -> bool static int M_L_AssaultStatsRecord(lua_State *const L) { if (!Gym_TrackManager_HasStats(GYM_TRACK_ASSAULT)) { return luaL_error(L, "Assault stats unavailable"); } const float time = (float)luaL_checknumber(L, 1); if (time <= 0.0f) { return luaL_error(L, "Time must be > 0"); } const bool ok = M_StoreAssaultTime(time); lua_pushboolean(L, ok); return 1; } // trxc.assault_stats.remove(index) -> bool static int M_L_AssaultStatsRemove(lua_State *const L) { if (!Gym_TrackManager_HasStats(GYM_TRACK_ASSAULT)) { return luaL_error(L, "Assault stats unavailable"); } const int64_t index_1 = luaL_checkinteger(L, 1); if (index_1 < 1 || index_1 > MAX_ASSAULT_TIMES) { return luaL_error( L, "Index out of range: %lld (expected 1..%d)", (long long)index_1, MAX_ASSAULT_TIMES); } const bool ok = M_RemoveAssaultTimeAtIndex(index_1 - 1); lua_pushboolean(L, ok); return 1; } // trxc.assault_stats.list() -> { { time = float, attempt_num = int }, ... } static int M_L_AssaultStatsList(lua_State *const L) { if (!Gym_TrackManager_HasStats(GYM_TRACK_ASSAULT)) { return luaL_error(L, "Assault stats unavailable"); } const GYM_TRACK_STATS *const assault = &g_Config.profile.assault_stats; lua_newtable(L); int32_t out_idx = 1; for (int32_t i = 0; i < MAX_ASSAULT_TIMES; i++) { if (assault->entries[i].time == 0) { break; } lua_newtable(L); lua_pushnumber( L, (lua_Number)((float)assault->entries[i].time / LOGIC_FPS)); lua_setfield(L, -2, "time"); lua_pushinteger(L, (lua_Integer)assault->entries[i].attempt_num); lua_setfield(L, -2, "attempt_num"); lua_seti(L, -2, out_idx); out_idx++; } return 1; } void LUA_CreateAssaultStats(lua_State *const L) { lua_getglobal(L, "trxc"); lua_newtable(L); lua_pushcfunction(L, M_L_AssaultStatsRecord); lua_setfield(L, -2, "record"); lua_pushcfunction(L, M_L_AssaultStatsRemove); lua_setfield(L, -2, "remove"); lua_pushcfunction(L, M_L_AssaultStatsList); lua_setfield(L, -2, "list"); lua_setfield(L, -2, "assault_stats"); lua_pop(L, 1); } ================================================ FILE: src/trx/game/lua/camera.c ================================================ #include #include #include #include // trxc.camera.get_pos() → {x, y, z} static int M_L_CameraGetPos(lua_State *const L) { lua_newtable(L); lua_pushinteger(L, g_Camera.pos.x); lua_setfield(L, -2, "x"); lua_pushinteger(L, g_Camera.pos.y); lua_setfield(L, -2, "y"); lua_pushinteger(L, g_Camera.pos.z); lua_setfield(L, -2, "z"); return 1; } // trxc.camera.get_room() → int (1-based) or nil static int M_L_CameraGetRoom(lua_State *const L) { if (g_Camera.pos.room_num == NO_ROOM) { lua_pushnil(L); } else { lua_pushinteger(L, g_Camera.pos.room_num + 1); } return 1; } // trxc.camera.get_target_pos() → {x, y, z} static int M_L_CameraGetTargetPos(lua_State *const L) { lua_newtable(L); lua_pushinteger(L, g_Camera.target.x); lua_setfield(L, -2, "x"); lua_pushinteger(L, g_Camera.target.y); lua_setfield(L, -2, "y"); lua_pushinteger(L, g_Camera.target.z); lua_setfield(L, -2, "z"); return 1; } // trxc.camera.get_target_room() → int (1-based) or nil static int M_L_CameraGetTargetRoom(lua_State *const L) { if (g_Camera.target.room_num == NO_ROOM) { lua_pushnil(L); } else { lua_pushinteger(L, g_Camera.target.room_num + 1); } return 1; } // trxc.camera.shake(intensity) static int M_L_CameraShake(lua_State *const L) { g_Camera.bounce = (int32_t)luaL_checkinteger(L, 1); return 0; } // trxc.camera.reset() static int M_L_CameraReset(lua_State *const L) { Camera_ResetPosition(); return 0; } void LUA_CreateCamera(lua_State *const L) { lua_getglobal(L, "trxc"); lua_newtable(L); lua_pushcfunction(L, M_L_CameraGetPos); lua_setfield(L, -2, "get_pos"); lua_pushcfunction(L, M_L_CameraGetRoom); lua_setfield(L, -2, "get_room"); lua_pushcfunction(L, M_L_CameraGetTargetPos); lua_setfield(L, -2, "get_target_pos"); lua_pushcfunction(L, M_L_CameraGetTargetRoom); lua_setfield(L, -2, "get_target_room"); lua_pushcfunction(L, M_L_CameraShake); lua_setfield(L, -2, "shake"); lua_pushcfunction(L, M_L_CameraReset); lua_setfield(L, -2, "reset"); lua_setfield(L, -2, "camera"); lua_pop(L, 1); } ================================================ FILE: src/trx/game/lua/catalog.c ================================================ #include #include #include #include #include #include static void M_PushCatalogKey( lua_State *const L, const char *const name, const char *const prefix, const int32_t value) { const char *key = name; if (prefix != nullptr) { const size_t prefix_len = strlen(prefix); if (strncmp(name, prefix, prefix_len) == 0) { key = name + prefix_len; } } const size_t key_len = strlen(key); char *const lower_key = Memory_Alloc(key_len + 1); for (size_t i = 0; i < key_len; i++) { lower_key[i] = (char)tolower((unsigned char)key[i]); } lower_key[key_len] = '\0'; lua_pushinteger(L, value); lua_setfield(L, -2, lower_key); Memory_Free(lower_key); } static void M_PushObjects(lua_State *const L) { lua_newtable(L); int32_t id = 0; #define X_CATALOG_ID(enum_value) M_PushCatalogKey(L, #enum_value, "O_", id++); #include "trx/game/catalog/objects.def" #undef X_CATALOG_ID lua_setfield(L, -2, "objects"); } static void M_PushFlipEffects(lua_State *const L) { lua_newtable(L); int32_t id = 0; #define X_CATALOG_ID(enum_value) \ M_PushCatalogKey(L, #enum_value, "ITEM_ACTION_", id++); #include "trx/game/catalog/item_actions.def" #undef X_CATALOG_ID lua_setfield(L, -2, "flip_effects"); } static void M_PushLaraStates(lua_State *const L) { lua_newtable(L); int32_t id = 0; #define X_CATALOG_ID(enum_value) M_PushCatalogKey(L, #enum_value, "LS_", id++); #include "trx/game/catalog/lara_states.def" #undef X_CATALOG_ID lua_setfield(L, -2, "lara_states"); } static void M_PushWeapons(lua_State *const L) { lua_newtable(L); #define X_LUA_WEAPON(enum_value) \ M_PushCatalogKey(L, #enum_value, "LGT_", enum_value); X_LUA_WEAPON(LGT_UNARMED); X_LUA_WEAPON(LGT_PISTOLS); X_LUA_WEAPON(LGT_MAGNUMS); X_LUA_WEAPON(LGT_UZIS); X_LUA_WEAPON(LGT_SHOTGUN); X_LUA_WEAPON(LGT_M16); X_LUA_WEAPON(LGT_GRENADE); X_LUA_WEAPON(LGT_HARPOON); X_LUA_WEAPON(LGT_FLARE); X_LUA_WEAPON(LGT_SKIDOO); X_LUA_WEAPON(LGT_AUTOS); X_LUA_WEAPON(LGT_DESERT_EAGLE); X_LUA_WEAPON(LGT_MP5); X_LUA_WEAPON(LGT_ROCKET); #undef X_LUA_WEAPON lua_setfield(L, -2, "weapons"); } static void M_PushLaraAnims(lua_State *const L) { lua_newtable(L); int32_t id = 0; #define X_CATALOG_ID(enum_value) M_PushCatalogKey(L, #enum_value, "LA_", id++); #include "trx/game/catalog/lara_anims.def" #undef X_CATALOG_ID lua_setfield(L, -2, "lara_anims"); } static void M_PushMusic(lua_State *const L) { lua_newtable(L); int32_t id = 0; #define X_CATALOG_ID(enum_value) M_PushCatalogKey(L, #enum_value, "MX_", id++); #include "trx/game/catalog/music.def" #undef X_CATALOG_ID lua_setfield(L, -2, "music"); } static void M_PushSamples(lua_State *const L) { lua_newtable(L); int32_t id = 0; #define X_CATALOG_ID(enum_value) M_PushCatalogKey(L, #enum_value, "SFX_", id++); #include "trx/game/catalog/samples.def" #undef X_CATALOG_ID lua_setfield(L, -2, "samples"); } void LUA_CreateCatalog(lua_State *const L) { lua_getglobal(L, "trxc"); lua_newtable(L); M_PushObjects(L); M_PushFlipEffects(L); M_PushLaraStates(L); M_PushWeapons(L); M_PushLaraAnims(L); M_PushMusic(L); M_PushSamples(L); lua_setfield(L, -2, "catalog"); lua_pop(L, 1); } ================================================ FILE: src/trx/game/lua/common.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include typedef struct { lua_State *state; LUA_CONTEXT context; } M_PRIV; static M_PRIV m_Priv = { .context = LUA_CONTEXT_GLOBAL, }; // Initialize internal APIs extern void LUA_CreateCatalog(lua_State *L); extern void LUA_CreateCamera(lua_State *L); extern void LUA_CreateConsole(lua_State *L); extern void LUA_CreateEvents(lua_State *L); extern void LUA_CreateItems(lua_State *L); extern void LUA_CreateLara(lua_State *L); extern void LUA_CreateLog(lua_State *L); extern void LUA_CreateMusic(lua_State *L); extern void LUA_CreateSound(lua_State *L); extern void LUA_CreateConfig(lua_State *L); extern void LUA_CreateRooms(lua_State *L); extern void LUA_CreateGame(lua_State *L); extern void LUA_CreateCreatures(lua_State *L); extern void LUA_CreateObjects(lua_State *const L); extern void LUA_CreateAssaultStats(lua_State *const L); static int M_LoadFile(lua_State *const L, const char *const path) { return luaL_loadfile(L, path); } // Shared loader+pcall helper for Eval/EvalFile to capture errors with source static LUA_RESULT M_LuaLoadAndRun( lua_State *const L, int (*const loader)(lua_State *, const char *), const char *const src) { LUA_RESULT result = { .code = LUA_OK, .message = nullptr }; int status = loader(L, src); if (status != LUA_OK) { result.code = status; result.message = Memory_DupStr(lua_tostring(L, -1)); lua_pop(L, 1); return result; } status = lua_pcall(L, 0, LUA_MULTRET, 0); if (status != LUA_OK) { result.code = status; result.message = Memory_DupStr(lua_tostring(L, -1)); lua_pop(L, 1); } return result; } // Loader closure for embedded TRX modules, invoked via package.preload. static int M_TRXEmbeddedModuleLoader(lua_State *const L) { const uint8_t *const data = lua_touserdata(L, lua_upvalueindex(1)); const size_t size = (size_t)lua_tointeger(L, lua_upvalueindex(2)); const char *const chunk_name = lua_tostring(L, lua_upvalueindex(3)); int status = luaL_loadbuffer(L, (const char *)data, size, chunk_name); if (status != LUA_OK) { lua_error(L); } status = lua_pcall(L, 0, LUA_MULTRET, 0); if (status != LUA_OK) { lua_error(L); } // Return all values pushed by the chunk. return lua_gettop(L); } static void M_LoadTRXCModule(lua_State *const L, void (*loader)(lua_State *)) { LOG_DEBUG("Loading TRXC module %p", loader); loader(L); } static char *M_DeriveTRXModuleName(const char *path) { char *raw = Memory_DupStr(path); size_t raw_len = strlen(raw); // Drop ".lua" if (raw_len > 4 && strcmp(raw + raw_len - 4, ".lua") == 0) { raw[raw_len - 4] = '\0'; } // Convert '/' → '.' for (char *c = raw; *c; ++c) { if (*c == '/') { *c = '.'; } } // Prefix "trx." const char *modprefix = "trx."; size_t prefix_len = strlen(modprefix); raw_len = strlen(raw); char *name = Memory_Alloc(prefix_len + raw_len + 1); memcpy(name, modprefix, prefix_len); memcpy(name + prefix_len, raw, raw_len + 1); Memory_FreePointer(&raw); return name; } static void M_RegisterTRXPreloadEmbedded( lua_State *const L, const uint8_t *const data, const size_t size, const char *const chunk_name, const char *const name) { lua_getglobal(L, "package"); lua_getfield(L, -1, "preload"); lua_pushlightuserdata(L, (void *)data); lua_pushinteger(L, (lua_Integer)size); lua_pushstring(L, chunk_name); lua_pushcclosure(L, M_TRXEmbeddedModuleLoader, 3); lua_setfield(L, -2, name); lua_pop(L, 2); } static void M_RequireTRXModule(lua_State *const L, const char *name) { lua_getglobal(L, "require"); lua_pushstring(L, name); if (lua_pcall(L, 1, LUA_MULTRET, 0) != LUA_OK) { LOG_ERROR("Failed to require module %s: %s", name, lua_tostring(L, -1)); lua_pop(L, 1); } lua_settop(L, 0); } static void M_LoadTRXScripts(lua_State *const L) { for (const LUA_EMBEDDED_SCRIPT *script = g_LUA_EmbeddedScripts; script->path != nullptr; script++) { LOG_DEBUG("Loading TRX module %s", script->path); char *name = M_DeriveTRXModuleName(script->path); const char *const chunk_name = String_FormatStatic("@trx/%s", script->path); M_RegisterTRXPreloadEmbedded( L, script->data, script->size, chunk_name, name); M_RequireTRXModule(L, name); Memory_FreePointer(&name); } } void LUA_Init(void) { lua_State *const L = luaL_newstate(); ASSERT(L != nullptr); luaL_openlibs(L); lua_newtable(L); lua_setglobal(L, "trxc"); lua_newtable(L); lua_setglobal(L, "trx"); // Initialize internal modules M_LoadTRXCModule(L, LUA_CreateCatalog); M_LoadTRXCModule(L, LUA_CreateCamera); M_LoadTRXCModule(L, LUA_CreateConsole); M_LoadTRXCModule(L, LUA_CreateEvents); M_LoadTRXCModule(L, LUA_CreateItems); M_LoadTRXCModule(L, LUA_CreateLara); M_LoadTRXCModule(L, LUA_CreateLog); M_LoadTRXCModule(L, LUA_CreateMusic); M_LoadTRXCModule(L, LUA_CreateSound); M_LoadTRXCModule(L, LUA_CreateConfig); M_LoadTRXCModule(L, LUA_CreateRooms); M_LoadTRXCModule(L, LUA_CreateGame); M_LoadTRXCModule(L, LUA_CreateCreatures); M_LoadTRXCModule(L, LUA_CreateObjects); M_LoadTRXCModule(L, LUA_CreateAssaultStats); M_PRIV *const p = &m_Priv; p->state = L; M_LoadTRXScripts(L); } void LUA_Shutdown(void) { M_PRIV *const p = &m_Priv; Lua_ShutdownEvents(); if (p->state != nullptr) { lua_close(p->state); p->state = nullptr; } } LUA_CONTEXT Lua_GetScriptContext(void) { M_PRIV *const p = &m_Priv; return p->context; } void Lua_SetScriptContext(const LUA_CONTEXT context) { M_PRIV *const p = &m_Priv; p->context = context; } LUA_RESULT Lua_Eval(const char *const code) { M_PRIV *const p = &m_Priv; return M_LuaLoadAndRun(p->state, luaL_loadstring, code); } LUA_RESULT Lua_EvalFile(const char *const path) { M_PRIV *const p = &m_Priv; return M_LuaLoadAndRun(p->state, M_LoadFile, path); } void Lua_ReloadLevelScript(void) { const GF_LEVEL *const level = GF_GetCurrentLevel(); if (level == nullptr) { return; } Lua_ClearLevelListeners(); Lua_SetScriptContext(LUA_CONTEXT_LEVEL); if (level->script_path != nullptr) { LUA_RESULT res = Lua_EvalFile(level->script_path); if (res.code != LUA_OK) { LOG_ERROR("Lua level script error: %s", res.message); } Lua_FreeResult(&res); } Lua_SetScriptContext(LUA_CONTEXT_GLOBAL); } void Lua_FreeResult(LUA_RESULT *const result) { if (result != nullptr) { Memory_FreePointer(&result->message); } } ================================================ FILE: src/trx/game/lua/common.h ================================================ #pragma once #include #include // Result of evaluating a Lua chunk. typedef struct { int32_t code; // LUA_OK, LUA_ERRSYNTAX, LUA_ERRRUN, etc. char *message; // Error text (nullptr if code == LUA_OK). } LUA_RESULT; typedef enum { LUA_CONTEXT_GLOBAL, LUA_CONTEXT_LEVEL, } LUA_CONTEXT; void LUA_Init(void); void LUA_Shutdown(void); // Set script context: level script vs global script LUA_CONTEXT Lua_GetScriptContext(void); void Lua_SetScriptContext(LUA_CONTEXT context); // Evaluate a Lua code string. Caller must free the result with Lua_FreeResult. LUA_RESULT Lua_Eval(const char *code); // Free the LUA eval result. void Lua_FreeResult(LUA_RESULT *result); // Evaluate a Lua script file. Caller must free the result with Lua_FreeResult. LUA_RESULT Lua_EvalFile(const char *path); // Reload current level script and reset level-scoped listeners. void Lua_ReloadLevelScript(void); ================================================ FILE: src/trx/game/lua/config.c ================================================ #include #include #include // trxc.config.get(key) static int M_L_ConfigGet(lua_State *const L) { const char *const key = luaL_checkstring(L, 1); const CONFIG_OPTION *const opt = Config_GetOptionByPath(key); if (opt == nullptr) { return luaL_error(L, "Unknown option: %s", key); } const char *const value = Config_GetOptionValueAsString(opt, false); lua_pushstring(L, value); return 1; } // trxc.config.set(key, value) static int M_L_ConfigSet(lua_State *const L) { const char *const key = luaL_checkstring(L, 1); const char *const new_value = luaL_checkstring(L, 2); const CONFIG_OPTION *const opt = Config_GetOptionByPath(key); if (opt == nullptr) { return luaL_error(L, "Unknown option: %s", key); } const bool ok = Config_SetOptionValueFromString(opt, new_value); if (!ok) { return luaL_error(L, "Failed to set option %s to %s", key, new_value); } Config_Update(); return 0; } // trxc.config.list() static int M_L_ConfigList(lua_State *const L) { lua_newtable(L); const CONFIG_OPTION *opt = Config_GetOptionMap(); while (opt->name != nullptr) { const char *const value = Config_GetOptionValueAsString(opt, false); lua_pushstring(L, value); lua_setfield(L, -2, opt->name); opt++; } return 1; } void LUA_CreateConfig(lua_State *const L) { lua_getglobal(L, "trxc"); lua_newtable(L); lua_pushcfunction(L, M_L_ConfigGet); lua_setfield(L, -2, "get"); lua_pushcfunction(L, M_L_ConfigSet); lua_setfield(L, -2, "set"); lua_pushcfunction(L, M_L_ConfigList); lua_setfield(L, -2, "list"); lua_setfield(L, -2, "config"); lua_pop(L, 1); } ================================================ FILE: src/trx/game/lua/console.c ================================================ #include #include #include #include #include #include #include // trxc.console.log(...) static int M_L_ConsoleLog(lua_State *const L) { int num_args = lua_gettop(L); if (num_args < 2) { return 0; } const LOG_LEVEL log_level = (int)lua_tointeger(L, 1); const char *msg = nullptr; for (int32_t i = 2; i <= num_args; i++) { lua_getglobal(L, "tostring"); lua_pushvalue(L, i); lua_call(L, 1, 1); const char *arg = lua_tostring(L, -1); lua_pop(L, 1); msg = (i > 2) ? String_FormatStatic("%s %s", msg, arg) : String_FormatStatic("%s", arg); } lua_Debug ar; const char *src = "?"; const char *func = "?"; int line = 0; if (lua_getstack(L, 2, &ar) && lua_getinfo(L, "nSl", &ar)) { src = ar.short_src; func = ar.name ? ar.name : "?"; line = ar.currentline; } Console_LogEx(log_level, src, line, func, "%s", msg); return 0; } // trxc.console.clear() static int M_L_ConsoleClear(lua_State *const L) { Console_Clear(); return 0; } // trxc.console.eval(cmd, { verbose = bool }) static int M_L_ConsoleEval(lua_State *const L) { const char *cmd = luaL_checkstring(L, 1); bool verbose = false; if (lua_gettop(L) >= 2 && lua_istable(L, 2)) { lua_getfield(L, 2, "verbose"); verbose = lua_toboolean(L, -1); lua_pop(L, 1); } const bool old_verbose = Console_IsVerbose(); Console_SetVerbose(verbose); COMMAND_RESULT res = Console_Eval(cmd); Console_SetVerbose(old_verbose); const char *err = "unknown error"; switch (res) { case CR_BAD_INVOCATION: err = "bad invocation"; break; case CR_UNAVAILABLE: err = "unavailable"; break; case CR_FAILURE: err = "failure"; break; case CR_SUCCESS: return 0; } return luaL_error(L, "console.eval %s: %s", err, cmd); } void LUA_CreateConsole(lua_State *const L) { lua_getglobal(L, "trxc"); lua_newtable(L); lua_pushcfunction(L, M_L_ConsoleLog); lua_setfield(L, -2, "log"); lua_pushcfunction(L, M_L_ConsoleEval); lua_setfield(L, -2, "eval"); lua_pushcfunction(L, M_L_ConsoleClear); lua_setfield(L, -2, "clear"); lua_setfield(L, -2, "console"); lua_pop(L, 1); } ================================================ FILE: src/trx/game/lua/creatures.c ================================================ #include #include // trxc.creatures.are_allies_hostile() → bool static int M_L_CreaturesAreAlliesHostile(lua_State *const L) { const bool hostile = Creature_AreAlliesHostile(); lua_pushboolean(L, hostile); return 1; } // trxc.creatures.set_allies_hostile(enable) static int M_L_CreaturesSetAlliesHostile(lua_State *const L) { const bool hostile = lua_toboolean(L, 1) != 0; Creature_SetAlliesHostile(hostile); return 0; } // trxc.creatures.add_ally(obj_id) static int M_L_CreaturesAddAlly(lua_State *const L) { const OBJECT_ID obj_id = luaL_checkinteger(L, 1); Creature_AddAlly(obj_id); return 0; } // trxc.creatures.add_ally_target(obj_id) static int M_L_CreaturesAddAllyTarget(lua_State *const L) { const OBJECT_ID obj_id = luaL_checkinteger(L, 1); Creature_AddAllyTargetingEnemy(obj_id); return 0; } void LUA_CreateCreatures(lua_State *const L) { lua_getglobal(L, "trxc"); lua_newtable(L); lua_pushcfunction(L, M_L_CreaturesAreAlliesHostile); lua_setfield(L, -2, "are_allies_hostile"); lua_pushcfunction(L, M_L_CreaturesSetAlliesHostile); lua_setfield(L, -2, "set_allies_hostile"); lua_pushcfunction(L, M_L_CreaturesAddAlly); lua_setfield(L, -2, "add_ally"); lua_pushcfunction(L, M_L_CreaturesAddAllyTarget); lua_setfield(L, -2, "add_ally_target"); lua_setfield(L, -2, "creatures"); lua_pop(L, 1); } ================================================ FILE: src/trx/game/lua/embedded_scripts.h ================================================ #pragma once #include #include typedef struct { const char *path; const uint8_t *data; size_t size; } LUA_EMBEDDED_SCRIPT; extern const LUA_EMBEDDED_SCRIPT g_LUA_EmbeddedScripts[]; ================================================ FILE: src/trx/game/lua/events.c ================================================ #include #include #include #include #include #include typedef struct { int32_t ref; LUA_EVENT_TYPE type; bool level_scoped; } M_LISTENER; static lua_State *m_L = nullptr; static VECTOR *m_Listeners = nullptr; static void M_ClearAllListeners(const bool unref_from_lua) { if (m_Listeners == nullptr) { return; } if (unref_from_lua && m_L != nullptr) { for (int32_t i = 0; i < m_Listeners->count; i++) { const M_LISTENER *const lst = Vector_Get(m_Listeners, i); luaL_unref(m_L, LUA_REGISTRYINDEX, lst->ref); } } Vector_Free(m_Listeners); m_Listeners = nullptr; } __attribute__((destructor)) static void M_Shutdown(void) { M_ClearAllListeners(false); m_L = nullptr; } void Lua_ShutdownEvents(void) { M_ClearAllListeners(true); m_L = nullptr; } // trxc.events.attach(event_type, callback) → id static int32_t M_L_EventsAttach(lua_State *const L) { const LUA_EVENT_TYPE ev = luaL_checkinteger(L, 1); luaL_checktype(L, 2, LUA_TFUNCTION); lua_pushvalue(L, 2); const int32_t ref = luaL_ref(L, LUA_REGISTRYINDEX); if (m_Listeners == nullptr) { m_Listeners = Vector_Create(sizeof(M_LISTENER)); } const M_LISTENER listener = { .ref = ref, .type = ev, .level_scoped = Lua_GetScriptContext() == LUA_CONTEXT_LEVEL, }; Vector_Add(m_Listeners, &listener); lua_pushinteger(L, ref); return 1; } // trxc.events.detach(id) static int32_t M_L_EventsDetach(lua_State *const L) { int32_t id = luaL_checkinteger(L, 1); if (m_Listeners == nullptr) { return 0; } for (int32_t i = 0; i < m_Listeners->count; i++) { const M_LISTENER *const lst = Vector_Get(m_Listeners, i); if (lst->ref == id) { luaL_unref(L, LUA_REGISTRYINDEX, lst->ref); Vector_RemoveAt(m_Listeners, i); break; } } return 0; } void Lua_ClearLevelListeners(void) { lua_State *const L = m_L; if (L == nullptr) { return; } if (m_Listeners == nullptr) { return; } for (int32_t i = 0; i < m_Listeners->count;) { M_LISTENER *const lst = Vector_Get(m_Listeners, i); if (lst->level_scoped) { luaL_unref(L, LUA_REGISTRYINDEX, lst->ref); Vector_RemoveAt(m_Listeners, i); } else { i++; } } } static void M_PushArg(lua_State *const L, const LUA_EVENT_ARG arg) { switch (arg.type) { case LUA_EVENT_ARG_NIL: lua_pushnil(L); break; case LUA_EVENT_ARG_INT32: lua_pushinteger(L, arg.value.i32); break; case LUA_EVENT_ARG_BOOL: lua_pushboolean(L, arg.value.b); break; case LUA_EVENT_ARG_NUMBER: lua_pushnumber(L, arg.value.number); break; case LUA_EVENT_ARG_STRING: if (arg.value.str != nullptr) { lua_pushstring(L, arg.value.str); } else { lua_pushnil(L); } break; } } void Lua_FireEventEx( const LUA_EVENT_TYPE ev, const LUA_EVENT_ARG *const args, const int32_t arg_count) { lua_State *const L = m_L; if (L == nullptr || m_Listeners == nullptr) { return; } for (int32_t i = 0; i < m_Listeners->count; i++) { M_LISTENER *const lst = Vector_Get(m_Listeners, i); if (lst->type != ev) { continue; } lua_rawgeti(L, LUA_REGISTRYINDEX, lst->ref); for (int32_t arg_idx = 0; arg_idx < arg_count; arg_idx++) { M_PushArg(L, args[arg_idx]); } if (lua_pcall(L, arg_count, 0, 0) != LUA_OK) { LOG_ERROR("Lua event handler error: %s", lua_tostring(L, -1)); lua_pop(L, 1); } } } void Lua_FireEventInt32(const LUA_EVENT_TYPE ev, const int32_t arg) { const LUA_EVENT_ARG args[] = { { .type = LUA_EVENT_ARG_INT32, .value = { .i32 = arg } }, }; Lua_FireEventEx(ev, args, 1); } void LUA_CreateEvents(lua_State *const L) { m_L = L; lua_getglobal(L, "trxc"); lua_newtable(L); lua_pushcfunction(L, M_L_EventsAttach); lua_setfield(L, -2, "attach"); lua_pushcfunction(L, M_L_EventsDetach); lua_setfield(L, -2, "detach"); lua_newtable(L); lua_pushinteger(L, LUA_EVENT_BEFORE_LEVEL_FILE); lua_setfield(L, -2, "BEFORE_LEVEL_FILE"); lua_pushinteger(L, LUA_EVENT_AFTER_LEVEL_FILE); lua_setfield(L, -2, "AFTER_LEVEL_FILE"); lua_pushinteger(L, LUA_EVENT_AFTER_LEVEL_STATE); lua_setfield(L, -2, "AFTER_LEVEL_STATE"); lua_pushinteger(L, LUA_EVENT_GAME_START); lua_setfield(L, -2, "GAME_START"); lua_pushinteger(L, LUA_EVENT_PICKUP); lua_setfield(L, -2, "PICKUP"); lua_pushinteger(L, LUA_EVENT_BEFORE_CONTROL); lua_setfield(L, -2, "BEFORE_CONTROL"); lua_pushinteger(L, LUA_EVENT_AFTER_CONTROL); lua_setfield(L, -2, "AFTER_CONTROL"); lua_setfield(L, -2, "EventType"); lua_setfield(L, -2, "events"); lua_pop(L, 1); } ================================================ FILE: src/trx/game/lua/events.h ================================================ // Lua event listener support #pragma once #include #include // Event types for Lua listeners typedef enum { LUA_EVENT_BEFORE_LEVEL_FILE, LUA_EVENT_AFTER_LEVEL_FILE, LUA_EVENT_AFTER_LEVEL_STATE, LUA_EVENT_GAME_START, LUA_EVENT_PICKUP, LUA_EVENT_BEFORE_CONTROL, LUA_EVENT_AFTER_CONTROL, } LUA_EVENT_TYPE; typedef enum { LUA_EVENT_ARG_NIL, LUA_EVENT_ARG_INT32, LUA_EVENT_ARG_BOOL, LUA_EVENT_ARG_NUMBER, LUA_EVENT_ARG_STRING, } LUA_EVENT_ARG_TYPE; typedef struct { LUA_EVENT_ARG_TYPE type; union { int32_t i32; bool b; double number; const char *str; } value; } LUA_EVENT_ARG; // Initialize event API in Lua state void LUA_CreateEvents(lua_State *L); void Lua_ShutdownEvents(void); // Clear all listeners declared during the current level script void Lua_ClearLevelListeners(void); // Fire a Lua event of given type with arbitrary arguments void Lua_FireEventEx( LUA_EVENT_TYPE ev, const LUA_EVENT_ARG *args, int32_t arg_count); // Fire a Lua event of given type with int32 argument void Lua_FireEventInt32(LUA_EVENT_TYPE ev, int32_t arg); ================================================ FILE: src/trx/game/lua/game.c ================================================ #include #include #include #include #include // trxc.game.get_version() → int static int M_L_GameVersion(lua_State *const L) { lua_pushinteger(L, g_TRVersion); return 1; } // trxc.game.get_trx_version() → string static int M_L_TRXVersion(lua_State *const L) { lua_pushstring(L, g_TRXVersion); return 1; } // trxc.game.count_levels() → int static int M_L_GameCountLevels(lua_State *const L) { const GF_LEVEL_TABLE_TYPE table_type = luaL_checkinteger(L, 1); lua_pushinteger(L, GF_GetLevelCount(table_type)); return 1; } // Level property getters static int M_L_GameLevelGetNum(lua_State *const L) { const GF_LEVEL_TABLE_TYPE table_type = luaL_checkinteger(L, 1); const int32_t idx = luaL_checkinteger(L, 2); const GF_LEVEL *const lvl = GF_GetLevel(table_type, idx - 1); lua_pushinteger( L, lvl != nullptr ? GF_GetLevelOrdinalNumber(table_type, lvl) : 0); return 1; } static int M_L_GameLevelGetName(lua_State *const L) { const GF_LEVEL_TABLE_TYPE table_type = luaL_checkinteger(L, 1); const int32_t idx = luaL_checkinteger(L, 2); const GF_LEVEL *const lvl = GF_GetLevel(table_type, idx - 1); if (lvl != nullptr && lvl->title != nullptr) { lua_pushstring(L, lvl->title); } else { lua_pushnil(L); } return 1; } static int M_L_GameLevelGetPath(lua_State *const L) { const GF_LEVEL_TABLE_TYPE table_type = luaL_checkinteger(L, 1); const int32_t idx = luaL_checkinteger(L, 2); const GF_LEVEL *const lvl = GF_GetLevel(table_type, idx - 1); if (lvl != nullptr && lvl->path != nullptr) { lua_pushstring(L, lvl->path); } else { lua_pushnil(L); } return 1; } static int M_L_GameLevelGetType(lua_State *const L) { const GF_LEVEL_TABLE_TYPE table_type = luaL_checkinteger(L, 1); const int32_t idx = luaL_checkinteger(L, 2); const GF_LEVEL *const lvl = GF_GetLevel(table_type, idx - 1); lua_pushinteger(L, lvl != nullptr ? lvl->type : 0); return 1; } static int M_L_GameLevelGetCurrentLevelTable(lua_State *const L) { const GF_LEVEL *const lvl = GF_GetCurrentLevel(); lua_pushinteger( L, lvl != nullptr ? GF_GetLevelTableType(lvl->type) : GFLT_UNKNOWN); return 1; } static int M_L_GameLevelGetCurrentLevelIndex(lua_State *const L) { const GF_LEVEL *const lvl = GF_GetCurrentLevel(); lua_pushinteger(L, lvl != nullptr ? lvl->num : -1); return 1; } // trxc.game.play_level(num) → nil static int M_L_GamePlayLevel(lua_State *const L) { const int32_t level_idx = luaL_checkinteger(L, 1) - 1; const int32_t count = GF_GetLevelCount(GFLT_MAIN); if (level_idx < 0 || level_idx >= count) { return luaL_error(L, "invalid level number: %d", level_idx); } const GF_LEVEL *const current_level = GF_GetCurrentLevel(); if (current_level != nullptr) { const GF_LEVEL *const next_level = GF_GetLevel(GFLT_MAIN, level_idx); if (next_level != nullptr) { Savegame_PersistGameToCurrentInfo(next_level); RESUME_INFO *const resume = Savegame_GetCurrentInfo(next_level); if (resume != nullptr) { resume->prev_level = current_level->num; } } } GF_OverrideCommand((GF_COMMAND) { .action = GF_START_GAME, .param = level_idx, }); return 0; } // trxc.game.play_cutscene(num) → nil static int M_L_GamePlayCutscene(lua_State *const L) { const int32_t idx = luaL_checkinteger(L, 1) - 1; const int32_t count = GF_GetLevelCount(GFLT_CUTSCENES); if (idx < 0 || idx >= count) { return luaL_error(L, "invalid cutscene number: %d", idx); } GF_OverrideCommand((GF_COMMAND) { .action = GF_START_CINE, .param = idx, }); return 0; } // trxc.game.play_demo(num) → nil static int M_L_GamePlayDemo(lua_State *const L) { const int32_t idx = luaL_checkinteger(L, 1) - 1; const int32_t count = GF_GetLevelCount(GFLT_DEMOS); if (idx < 0 || idx >= count) { return luaL_error(L, "invalid demo number: %d", idx); } GF_OverrideCommand((GF_COMMAND) { .action = GF_START_DEMO, .param = idx, }); return 0; } void LUA_CreateGame(lua_State *const L) { lua_getglobal(L, "trxc"); lua_newtable(L); lua_pushcfunction(L, M_L_GameVersion); lua_setfield(L, -2, "get_version"); lua_pushcfunction(L, M_L_TRXVersion); lua_setfield(L, -2, "get_trx_version"); lua_pushcfunction(L, M_L_GameCountLevels); lua_setfield(L, -2, "count_levels"); lua_pushcfunction(L, M_L_GameLevelGetNum); lua_setfield(L, -2, "get_level_num"); lua_pushcfunction(L, M_L_GameLevelGetName); lua_setfield(L, -2, "get_level_name"); lua_pushcfunction(L, M_L_GameLevelGetPath); lua_setfield(L, -2, "get_level_path"); lua_pushcfunction(L, M_L_GameLevelGetType); lua_setfield(L, -2, "get_level_type"); lua_pushcfunction(L, M_L_GameLevelGetCurrentLevelTable); lua_setfield(L, -2, "get_current_level_table"); lua_pushcfunction(L, M_L_GameLevelGetCurrentLevelIndex); lua_setfield(L, -2, "get_current_level_idx"); lua_pushcfunction(L, M_L_GamePlayLevel); lua_setfield(L, -2, "play_level"); lua_pushcfunction(L, M_L_GamePlayCutscene); lua_setfield(L, -2, "play_cutscene"); lua_pushcfunction(L, M_L_GamePlayDemo); lua_setfield(L, -2, "play_demo"); lua_newtable(L); lua_pushinteger(L, GFLT_MAIN); lua_setfield(L, -2, "MAIN"); lua_pushinteger(L, GFLT_CUTSCENES); lua_setfield(L, -2, "CUTSCENES"); lua_pushinteger(L, GFLT_DEMOS); lua_setfield(L, -2, "DEMOS"); lua_setfield(L, -2, "LevelTable"); lua_newtable(L); lua_pushinteger(L, GFL_NORMAL); lua_setfield(L, -2, "NORMAL"); lua_pushinteger(L, GFL_CUTSCENE); lua_setfield(L, -2, "CUTSCENE"); lua_pushinteger(L, GFL_DEMO); lua_setfield(L, -2, "DEMO"); lua_pushinteger(L, GFL_GYM); lua_setfield(L, -2, "GYM"); lua_pushinteger(L, GFL_BONUS); lua_setfield(L, -2, "BONUS"); lua_setfield(L, -2, "LevelType"); lua_setfield(L, -2, "game"); lua_pop(L, 1); } ================================================ FILE: src/trx/game/lua/items.c ================================================ #include #include #include #include #include #include #include #define M_ITEM_GETTER(L) \ const int idx = luaL_checkinteger(L, 1); \ const ITEM *const item = Item_Get(idx - 1); \ if (item == nullptr) { \ lua_pushnil(L); \ return 1; \ } #define M_ITEM_SETTER(L) \ const int idx = luaL_checkinteger(L, 1); \ ITEM *const item = Item_Get(idx - 1); \ if (item == nullptr) { \ return 1; \ } // trxc.items.item_count() → int static int M_L_ItemsCount(lua_State *const L) { lua_pushinteger(L, Item_GetTotalCount()); return 1; } // trxc.items.get(index or name) → int (1-based) or nil static int M_L_ItemsGet(lua_State *const L) { int result = 0; if (lua_type(L, 1) == LUA_TNUMBER) { const int idx = luaL_checkinteger(L, 1); const ITEM *const item = Item_Get(idx - 1); if (item != nullptr) { result = idx; } } else { const char *const name = luaL_checkstring(L, 1); const ITEM *const item = Item_GetByName(name); if (item != nullptr) { result = Item_GetIndex(item) + 1; } } if (result) { lua_pushinteger(L, result); } else { lua_pushnil(L); } return 1; } // trxc.items.get_pos(index) → {x, y, z} or nil static int M_L_ItemGetPos(lua_State *const L) { M_ITEM_GETTER(L); lua_newtable(L); lua_pushinteger(L, item->pos.x); lua_setfield(L, -2, "x"); lua_pushinteger(L, item->pos.y); lua_setfield(L, -2, "y"); lua_pushinteger(L, item->pos.z); lua_setfield(L, -2, "z"); return 1; } // trxc.items.get_rot(index) → {x, y, z} or nil static int M_L_ItemGetRot(lua_State *const L) { M_ITEM_GETTER(L); lua_newtable(L); lua_pushinteger(L, item->rot.x); lua_setfield(L, -2, "x"); lua_pushinteger(L, item->rot.y); lua_setfield(L, -2, "y"); lua_pushinteger(L, item->rot.z); lua_setfield(L, -2, "z"); return 1; } // trxc.items.get_anim(index) → int or nil static int M_L_ItemGetAnim(lua_State *const L) { M_ITEM_GETTER(L); lua_pushinteger(L, Item_GetRelativeAnim(item)); return 1; } // trxc.items.get_frame(index) → int or nil static int M_L_ItemGetFrame(lua_State *const L) { M_ITEM_GETTER(L); lua_pushinteger(L, Item_GetRelativeFrame(item)); return 1; } // trxc.items.get_room(index) → int or nil static int M_L_ItemGetRoom(lua_State *const L) { M_ITEM_GETTER(L); lua_pushinteger(L, item->room_num + 1); return 1; } // trxc.items.get_status(index) → int or nil static int M_L_ItemGetStatus(lua_State *const L) { M_ITEM_GETTER(L); lua_pushinteger(L, (int)item->status); return 1; } // trxc.items.get_flags(index) → int or nil static int M_L_ItemGetFlags(lua_State *const L) { M_ITEM_GETTER(L); lua_pushinteger(L, (int)item->flags); return 1; } // trxc.items.get_timer(index) → int or nil static int M_L_ItemGetTimer(lua_State *const L) { M_ITEM_GETTER(L); lua_pushinteger(L, (int)item->timer); return 1; } // trxc.items.get_object_id(index) → int or nil static int M_L_ItemGetObjectId(lua_State *const L) { M_ITEM_GETTER(L); lua_pushinteger(L, item->object_id); return 1; } // trxc.items.get_hit_points(index) → int or nil static int M_L_ItemGetHitPoints(lua_State *const L) { M_ITEM_GETTER(L); lua_pushinteger(L, item->hit_points); return 1; } // trxc.items.get_max_hit_points(index) → int or nil static int M_L_ItemGetMaxHitPoints(lua_State *const L) { M_ITEM_GETTER(L); lua_pushinteger(L, item->max_hit_points); return 1; } // trxc.items.get_name(index) → string or nil static int M_L_ItemGetName(lua_State *const L) { M_ITEM_GETTER(L); if (item->name == nullptr) { lua_pushnil(L); } else { lua_pushstring(L, item->name); } return 1; } // trxc.items.set_pos(index, {x,y,z}) static int M_L_ItemSetPos(lua_State *const L) { M_ITEM_SETTER(L); luaL_checktype(L, 2, LUA_TTABLE); lua_getfield(L, 2, "x"); item->pos.x = luaL_checkinteger(L, -1); lua_pop(L, 1); lua_getfield(L, 2, "y"); item->pos.y = luaL_checkinteger(L, -1); lua_pop(L, 1); lua_getfield(L, 2, "z"); item->pos.z = luaL_checkinteger(L, -1); lua_pop(L, 1); const int16_t room_num = Room_GetIndexFromPos(item->pos); Item_UpdateRoom(idx - 1, room_num); return 0; } // trxc.items.set_anim(index, anim_idx) static int M_L_ItemSetAnim(lua_State *const L) { M_ITEM_SETTER(L); const int32_t anim_idx = luaL_checkinteger(L, 2); const OBJECT *const obj = Object_Get(item->object_id); if (obj->anim_idx == NO_ANIM) { return luaL_error(L, "object has no animations"); } if (anim_idx < 0 || anim_idx >= Anim_GetTotalCount() || anim_idx >= obj->anim_count) { return luaL_error(L, "invalid animation index"); } ANIM *const anim = Anim_GetAnim(obj->anim_idx + anim_idx); if (anim->frame_ptr == nullptr) { return luaL_error(L, "invalid animation index"); } item->anim_num = obj->anim_idx + anim_idx; item->frame_num = anim->frame_base; return 0; } // trxc.items.set_frame(index, frame_idx) static int M_L_ItemSetFrame(lua_State *const L) { M_ITEM_SETTER(L); const int32_t frame_idx = luaL_checkinteger(L, 2); const OBJECT *const obj = Object_Get(item->object_id); if (obj->anim_idx == NO_ANIM) { return luaL_error(L, "object has no animations"); } const ANIM *const anim = Item_GetAnim(item); if (frame_idx < 0) { if (anim->frame_end + frame_idx + 1 < anim->frame_base) { return luaL_error(L, "invalid frame index"); } item->frame_num = anim->frame_end + frame_idx + 1; } else { if (anim->frame_base + frame_idx >= anim->frame_end) { return luaL_error(L, "invalid frame index"); } item->frame_num = anim->frame_base + frame_idx; } return 0; } // trxc.items.set_hit_points(index, hp) static int M_L_ItemSetHitPoints(lua_State *const L) { M_ITEM_SETTER(L); item->hit_points = luaL_checkinteger(L, 2); item->max_hit_points = MAX(item->hit_points, item->max_hit_points); return 0; } // trxc.items.set_max_hit_points(index, max_hp) static int M_L_ItemSetMaxHitPoints(lua_State *const L) { M_ITEM_SETTER(L); item->max_hit_points = luaL_checkinteger(L, 2); return 0; } // trxc.items.set_rot(index, {x,y,z}) static int M_L_ItemSetRot(lua_State *const L) { M_ITEM_SETTER(L); luaL_checktype(L, 2, LUA_TTABLE); lua_getfield(L, 2, "x"); item->rot.x = luaL_checkinteger(L, -1); lua_pop(L, 1); lua_getfield(L, 2, "y"); item->rot.y = luaL_checkinteger(L, -1); lua_pop(L, 1); lua_getfield(L, 2, "z"); item->rot.z = luaL_checkinteger(L, -1); lua_pop(L, 1); return 0; } // trxc.items.set_name(index, name) static int M_L_ItemSetName(lua_State *const L) { M_ITEM_SETTER(L); const char *const new_name = luaL_checkstring(L, 2); if (!Item_SetName(Item_GetIndex(item), new_name)) { return luaL_error(L, "item name '%s' already in use", new_name); } return 0; } void LUA_CreateItems(lua_State *const L) { lua_getglobal(L, "trxc"); lua_newtable(L); lua_pushcfunction(L, M_L_ItemsCount); lua_setfield(L, -2, "count"); lua_pushcfunction(L, M_L_ItemsGet); lua_setfield(L, -2, "get"); lua_pushcfunction(L, M_L_ItemGetPos); lua_setfield(L, -2, "get_pos"); lua_pushcfunction(L, M_L_ItemGetRot); lua_setfield(L, -2, "get_rot"); lua_pushcfunction(L, M_L_ItemGetAnim); lua_setfield(L, -2, "get_anim"); lua_pushcfunction(L, M_L_ItemGetFrame); lua_setfield(L, -2, "get_frame"); lua_pushcfunction(L, M_L_ItemGetRoom); lua_setfield(L, -2, "get_room"); lua_pushcfunction(L, M_L_ItemGetStatus); lua_setfield(L, -2, "get_status"); lua_pushcfunction(L, M_L_ItemGetFlags); lua_setfield(L, -2, "get_flags"); lua_pushcfunction(L, M_L_ItemGetTimer); lua_setfield(L, -2, "get_timer"); lua_pushcfunction(L, M_L_ItemGetObjectId); lua_setfield(L, -2, "get_object_id"); lua_pushcfunction(L, M_L_ItemGetHitPoints); lua_setfield(L, -2, "get_hit_points"); lua_pushcfunction(L, M_L_ItemGetMaxHitPoints); lua_setfield(L, -2, "get_max_hit_points"); lua_pushcfunction(L, M_L_ItemGetName); lua_setfield(L, -2, "get_name"); lua_pushcfunction(L, M_L_ItemSetPos); lua_setfield(L, -2, "set_pos"); lua_pushcfunction(L, M_L_ItemSetRot); lua_setfield(L, -2, "set_rot"); lua_pushcfunction(L, M_L_ItemSetAnim); lua_setfield(L, -2, "set_anim"); lua_pushcfunction(L, M_L_ItemSetFrame); lua_setfield(L, -2, "set_frame"); lua_pushcfunction(L, M_L_ItemSetHitPoints); lua_setfield(L, -2, "set_hit_points"); lua_pushcfunction(L, M_L_ItemSetMaxHitPoints); lua_setfield(L, -2, "set_max_hit_points"); lua_pushcfunction(L, M_L_ItemSetName); lua_setfield(L, -2, "set_name"); lua_setfield(L, -2, "items"); lua_pop(L, 1); } ================================================ FILE: src/trx/game/lua/lara.c ================================================ #include #include #include #include #include #include // item_num = trxc.lara.get_item() static int M_L_GetLaraItem(lua_State *const L) { const ITEM *const item = Lara_GetItem(); int result = 0; if (item != nullptr) { result = Item_GetIndex(item) + 1; } if (result == 0) { lua_pushnil(L); } else { lua_pushinteger(L, result); } return 1; } // item_num = trxc.lara.get_target() static int M_L_GetLaraTarget(lua_State *const L) { const LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->target == nullptr) { lua_pushnil(L); } else { lua_pushinteger(L, Item_GetIndex(lara->target) + 1); } return 1; } // trxc.lara.get_exposure_bar() → int static int M_L_LaraGetExposureBar(lua_State *const L) { LARA_INFO *const lara = Lara_GetLaraInfo(); lua_pushinteger(L, lara->exposure_timer); return 1; } // trxc.lara.set_exposure_bar(timer) static int M_L_LaraSetExposureBar(lua_State *const L) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->exposure_timer = luaL_checkinteger(L, 1); return 0; } // trxc.lara.get_air_bar() → int static int M_L_LaraGetAirBar(lua_State *const L) { LARA_INFO *const lara = Lara_GetLaraInfo(); lua_pushinteger(L, lara->air); return 1; } // trxc.lara.set_air_bar(timer) static int M_L_LaraSetAirBar(lua_State *const L) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->air = luaL_checkinteger(L, 1); return 0; } // trxc.lara.get_outfit() → string static int M_L_LaraGetOutfit(lua_State *const L) { const int32_t outfit_idx = Lara_Skin_GetType(); const char *const outfit_name = Lara_Skin_GetOutfitName(outfit_idx); if (outfit_name == nullptr) { lua_pushnil(L); } else { lua_pushstring(L, outfit_name); } return 1; } // trxc.lara.set_outfit(outfit_name) static int M_L_LaraSetOutfit(lua_State *const L) { const char *const outfit_name = luaL_checkstring(L, 1); const int32_t outfit_idx = Lara_Skin_FindOutfitByName(outfit_name); if (!Lara_Skin_IsOutfitAvailable(outfit_idx)) { return luaL_error(L, "unknown Lara outfit: %s", outfit_name); } Lara_Skin_SetType(outfit_idx); return 0; } // trxc.lara.set_extra_equipment(lara_mesh, extra_mesh) static int M_L_LaraSetExtraEquipment(lua_State *const L) { const LARA_MESH lara_mesh = luaL_checkinteger(L, 1); const LARA_SKIN_EXTRA_MESH extra_mesh = luaL_checkinteger(L, 2); Lara_Skin_SetExtraEquipment(lara_mesh, extra_mesh); return 0; } // trxc.lara.clear_equipment(lara_mesh) static int M_L_LaraClearEquipment(lua_State *const L) { const LARA_MESH lara_mesh = luaL_checkinteger(L, 1); Lara_Skin_ClearEquipment(lara_mesh); return 0; } // trxc.lara.are_holsters_visible() → bool static int M_L_LaraAreHolstersVisible(lua_State *const L) { lua_pushboolean(L, Lara_Skin_AreHolstersVisible()); return 1; } // trxc.lara.set_holsters_visible(visible) static int M_L_LaraSetHolstersVisible(lua_State *const L) { const bool visible = lua_toboolean(L, 1) != 0; Lara_Skin_SetHolstersVisible(visible); return 0; } // trxc.lara.has_pistol_weapon() → bool static int M_L_LaraHasPistolWeapon(lua_State *const L) { bool has_pistol = false; for (int32_t i = 0; i < NUM_WEAPONS; i++) { const WEAPON_INFO *const weapon = &g_Weapons[i]; if ((weapon->type == WEAPON_TYPE_DUAL_PISTOLS || weapon->type == WEAPON_TYPE_SINGLE_PISTOL) && Inv_RequestItem(Gun_GetGunObject(i))) { has_pistol = true; break; } } lua_pushboolean(L, has_pistol); return 1; } // trxc.lara.get_extra_anim() → int static int M_L_LaraGetExtraAnim(lua_State *const L) { if (Lara_GetLaraInfo()->extra_anim) { lua_pushinteger( L, Item_GetRelativeObjAnim(Lara_GetItem(), O_LARA_EXTRA)); } else { lua_pushinteger(L, NO_ANIM); } return 1; } // trxc.lara.get_equipped_gun() → int static int M_L_LaraGetEquippedGun(lua_State *const L) { lua_pushinteger(L, Lara_GetLaraInfo()->gun_type); return 1; } void LUA_CreateLara(lua_State *const L) { lua_getglobal(L, "trxc"); lua_newtable(L); lua_newtable(L); lua_pushinteger(L, LM_HIPS); lua_setfield(L, -2, "hips"); lua_pushinteger(L, LM_THIGH_L); lua_setfield(L, -2, "thigh_l"); lua_pushinteger(L, LM_CALF_L); lua_setfield(L, -2, "calf_l"); lua_pushinteger(L, LM_FOOT_L); lua_setfield(L, -2, "foot_l"); lua_pushinteger(L, LM_THIGH_R); lua_setfield(L, -2, "thigh_r"); lua_pushinteger(L, LM_CALF_R); lua_setfield(L, -2, "calf_r"); lua_pushinteger(L, LM_FOOT_R); lua_setfield(L, -2, "foot_r"); lua_pushinteger(L, LM_TORSO); lua_setfield(L, -2, "torso"); lua_pushinteger(L, LM_UARM_R); lua_setfield(L, -2, "uarm_r"); lua_pushinteger(L, LM_LARM_R); lua_setfield(L, -2, "larm_r"); lua_pushinteger(L, LM_HAND_R); lua_setfield(L, -2, "hand_r"); lua_pushinteger(L, LM_UARM_L); lua_setfield(L, -2, "uarm_l"); lua_pushinteger(L, LM_LARM_L); lua_setfield(L, -2, "larm_l"); lua_pushinteger(L, LM_HAND_L); lua_setfield(L, -2, "hand_l"); lua_pushinteger(L, LM_HEAD); lua_setfield(L, -2, "head"); lua_setfield(L, -2, "mesh"); lua_newtable(L); lua_pushinteger(L, EXTRA_MESH_DAGGER_HAND); lua_setfield(L, -2, "dagger_hand"); lua_pushinteger(L, EXTRA_MESH_DAGGER_HIPS); lua_setfield(L, -2, "dagger_hips"); lua_pushinteger(L, EXTRA_MESH_OAR); lua_setfield(L, -2, "oar"); lua_pushinteger(L, EXTRA_MESH_SPANNER); lua_setfield(L, -2, "spanner"); lua_pushinteger(L, EXTRA_MESH_DRINK_CAN); lua_setfield(L, -2, "drink_can"); lua_pushinteger(L, EXTRA_MESH_GLASSES_OPAQUE); lua_setfield(L, -2, "glasses_opaque"); lua_pushinteger(L, EXTRA_MESH_GLASSES_TRANSPARENT); lua_setfield(L, -2, "glasses_transparent"); lua_setfield(L, -2, "extra_mesh"); lua_pushcfunction(L, M_L_GetLaraItem); lua_setfield(L, -2, "get_item"); lua_pushcfunction(L, M_L_GetLaraTarget); lua_setfield(L, -2, "get_target"); lua_pushcfunction(L, M_L_LaraGetExposureBar); lua_setfield(L, -2, "get_exposure_bar"); lua_pushcfunction(L, M_L_LaraSetExposureBar); lua_setfield(L, -2, "set_exposure_bar"); lua_pushcfunction(L, M_L_LaraGetAirBar); lua_setfield(L, -2, "get_air_bar"); lua_pushcfunction(L, M_L_LaraSetAirBar); lua_setfield(L, -2, "set_air_bar"); lua_pushcfunction(L, M_L_LaraGetOutfit); lua_setfield(L, -2, "get_outfit"); lua_pushcfunction(L, M_L_LaraSetOutfit); lua_setfield(L, -2, "set_outfit"); lua_pushcfunction(L, M_L_LaraSetExtraEquipment); lua_setfield(L, -2, "set_extra_equipment"); lua_pushcfunction(L, M_L_LaraClearEquipment); lua_setfield(L, -2, "clear_equipment"); lua_pushcfunction(L, M_L_LaraAreHolstersVisible); lua_setfield(L, -2, "are_holsters_visible"); lua_pushcfunction(L, M_L_LaraSetHolstersVisible); lua_setfield(L, -2, "set_holsters_visible"); lua_pushcfunction(L, M_L_LaraHasPistolWeapon); lua_setfield(L, -2, "has_pistol_weapon"); lua_pushcfunction(L, M_L_LaraGetExtraAnim); lua_setfield(L, -2, "get_extra_anim"); lua_pushcfunction(L, M_L_LaraGetEquippedGun); lua_setfield(L, -2, "get_equipped_gun"); lua_setfield(L, -2, "lara"); lua_pop(L, 1); } ================================================ FILE: src/trx/game/lua/log.c ================================================ #include #include #include // trxc.log.log(level, msg) static int M_L_LogGeneric(lua_State *const L) { const LOG_LEVEL level = luaL_checkinteger(L, 1); const char *const msg = luaL_checkstring(L, 2); lua_Debug ar; const char *src = "?"; const char *func = "?"; int line = 0; if (lua_getstack(L, 2, &ar) && lua_getinfo(L, "nSl", &ar)) { src = ar.short_src; func = ar.name ? ar.name : "?"; line = ar.currentline; } Log_Message(level, src, line, func, "%s", msg); return 0; } void LUA_CreateLog(lua_State *const L) { lua_getglobal(L, "trxc"); lua_newtable(L); lua_pushcfunction(L, M_L_LogGeneric); lua_setfield(L, -2, "log"); lua_newtable(L); lua_pushinteger(L, LOG_LEVEL_INFO); lua_setfield(L, -2, "INFO"); lua_pushinteger(L, LOG_LEVEL_WARNING); lua_setfield(L, -2, "WARNING"); lua_pushinteger(L, LOG_LEVEL_ERROR); lua_setfield(L, -2, "ERROR"); lua_pushinteger(L, LOG_LEVEL_DEBUG); lua_setfield(L, -2, "DEBUG"); lua_setfield(L, -2, "LogLevel"); lua_setfield(L, -2, "log"); lua_pop(L, 1); } ================================================ FILE: src/trx/game/lua/music.c ================================================ #include #include #include // trxc.music.get_track() static int M_L_MusicGetTrack(lua_State *const L) { const MUSIC_ID track = Music_GetCurrentPlayingTrack(); if (track < 0) { lua_pushnil(L); } else { lua_pushinteger(L, (lua_Integer)track); } return 1; } // trxc.music.play_track(id[, opts]) static int M_L_MusicPlayTrack(lua_State *const L) { const lua_Integer id = luaL_checkinteger(L, 1); const MUSIC_PLAY_MODE mode = luaL_checkinteger(L, 2); if (!Music_Play_Direct((MUSIC_ID)id, mode)) { return luaL_error( L, "invalid music track or mode (id=%d, mode=%d)", id, mode); } return 0; } // trxc.music.pause() static int M_L_MusicPause(lua_State *const L) { Music_Pause(); return 0; } // trxc.music.unpause() static int M_L_MusicUnpause(lua_State *const L) { Music_Unpause(); return 0; } // trxc.music.stop() static int M_L_MusicStop(lua_State *const L) { Music_Stop(); return 0; } void LUA_CreateMusic(lua_State *const L) { lua_getglobal(L, "trxc"); lua_newtable(L); lua_newtable(L); lua_pushinteger(L, MPM_ONCE); lua_setfield(L, -2, "ONCE"); lua_pushinteger(L, MPM_LOOP); lua_setfield(L, -2, "LOOP"); lua_pushinteger(L, MPM_DELAY); lua_setfield(L, -2, "DELAY"); lua_pushinteger(L, MPM_NO_REPEAT); lua_setfield(L, -2, "NO_REPEAT"); lua_pushinteger(L, MPM_OVERLAY); lua_setfield(L, -2, "OVERLAY"); lua_setfield(L, -2, "PlayMode"); lua_pushcfunction(L, M_L_MusicGetTrack); lua_setfield(L, -2, "get_track"); lua_pushcfunction(L, M_L_MusicPlayTrack); lua_setfield(L, -2, "play_track"); lua_pushcfunction(L, M_L_MusicPlayTrack); lua_setfield(L, -2, "play"); lua_pushcfunction(L, M_L_MusicPause); lua_setfield(L, -2, "pause"); lua_pushcfunction(L, M_L_MusicUnpause); lua_setfield(L, -2, "unpause"); lua_pushcfunction(L, M_L_MusicStop); lua_setfield(L, -2, "stop"); lua_setfield(L, -2, "music"); lua_pop(L, 1); } ================================================ FILE: src/trx/game/lua/objects.c ================================================ #include #include // trxc.objects.swap_mesh(obj1_id, obj2_id, mesh1_num, mesh2_num) static int M_L_ObjectsSwapMesh(lua_State *const L) { const int32_t arg_count = lua_gettop(L); const OBJECT_ID obj1_id = luaL_checkinteger(L, 1); const OBJECT_ID obj2_id = luaL_checkinteger(L, 2); if (arg_count == 2) { Object_SwapAllMeshes(obj1_id, obj2_id); } else { const int32_t mesh1_num = luaL_checkinteger(L, 3); const int32_t mesh2_num = luaL_checkinteger(L, 4); Object_SwapMeshEx(obj1_id, obj2_id, mesh1_num, mesh2_num); } return 0; } void LUA_CreateObjects(lua_State *const L) { lua_getglobal(L, "trxc"); lua_newtable(L); lua_pushcfunction(L, M_L_ObjectsSwapMesh); lua_setfield(L, -2, "swap_mesh"); lua_setfield(L, -2, "objects"); lua_pop(L, 1); } ================================================ FILE: src/trx/game/lua/rooms.c ================================================ #include #include #include #include #define M_ROOM_GETTER(L) \ const int idx = luaL_checkinteger(L, 1); \ const ROOM *const room = Room_Get(idx - 1); \ if (room == nullptr) { \ lua_pushnil(L); \ return 1; \ } #define M_ROOM_SETTER(L) \ const int idx = luaL_checkinteger(L, 1); \ ROOM *const room = Room_Get(idx - 1); \ if (room == nullptr) { \ return 1; \ } // trxc.rooms.count() → int static int M_L_RoomsCount(lua_State *const L) { lua_pushinteger(L, Room_GetCount()); return 1; } // trxc.rooms.get(index) → int (1-based) or nil static int M_L_RoomGet(lua_State *const L) { const int idx = luaL_checkinteger(L, 1); const ROOM *const room = Room_Get(idx - 1); if (room == nullptr) { lua_pushnil(L); } else { lua_pushinteger(L, idx); } return 1; } // trxc.rooms.get_underwater(index) → bool or nil static int M_L_RoomGetUnderwater(lua_State *const L) { M_ROOM_GETTER(L); lua_pushboolean(L, room->flags.underwater); return 1; } // trxc.rooms.get_wind(index) → bool or nil static int M_L_RoomGetWind(lua_State *const L) { M_ROOM_GETTER(L); lua_pushboolean(L, room->flags.wind); return 1; } // trxc.rooms.get_flip_status(index) → integer static int M_L_RoomGetFlipStatus(lua_State *const L) { M_ROOM_GETTER(L); lua_pushinteger(L, room->flip_status); return 1; } // trxc.rooms.get_flip_room(index) → integer or nil static int M_L_RoomGetFlippedRoom(lua_State *const L) { M_ROOM_GETTER(L); if (room->flipped_room == NO_ROOM) { lua_pushnil(L); } else { lua_pushinteger(L, room->flipped_room + 1); } return 1; } // trxc.rooms.set_underwater(index, bool) static int M_L_RoomSetUnderwater(lua_State *const L) { M_ROOM_SETTER(L); room->flags.underwater = lua_toboolean(L, 2); return 1; } // trxc.rooms.set_wind(index, bool) static int M_L_RoomSetWind(lua_State *const L) { M_ROOM_SETTER(L); room->flags.wind = lua_toboolean(L, 2); return 1; } // trxc.rooms.get_bounds() → table static int M_L_RoomGetBounds(lua_State *const L) { M_ROOM_GETTER(L); const BOUNDS_32 bounds = Room_GetRoomBounds(Room_Get(idx - 1)); lua_newtable(L); lua_pushinteger(L, bounds.min.x); lua_setfield(L, -2, "min_x"); lua_pushinteger(L, bounds.min.y); lua_setfield(L, -2, "min_y"); lua_pushinteger(L, bounds.min.z); lua_setfield(L, -2, "min_z"); lua_pushinteger(L, bounds.max.x); lua_setfield(L, -2, "max_x"); lua_pushinteger(L, bounds.max.y); lua_setfield(L, -2, "max_y"); lua_pushinteger(L, bounds.max.z); lua_setfield(L, -2, "max_z"); return 1; } // trxc.rooms.flip() static int M_L_RoomFlip(lua_State *const L) { Room_FlipMap(); return 0; } // trxc.rooms.flip_effect(effect_id, [timer]) static int M_L_RoomFlipEffect(lua_State *const L) { const int32_t trx_effect_id = luaL_checkinteger(L, 1); if (trx_effect_id == -1) { Room_SetFlipEffect(-1); } else { const ITEM_ACTION game_id = ItemAction_ToGameID(trx_effect_id); if (game_id == ITEM_ACTION_INVALID) { return luaL_error(L, "invalid flip effect id"); } Room_SetFlipEffect(game_id); } const int arg_count = lua_gettop(L); if (arg_count >= 2) { const int32_t timer = luaL_checkinteger(L, 2); Room_SetFlipTimer(timer); } return 0; } void LUA_CreateRooms(lua_State *const L) { lua_getglobal(L, "trxc"); lua_newtable(L); lua_newtable(L); lua_pushinteger(L, RFS_UNFLIPPED); lua_setfield(L, -2, "UNFLIPPED"); lua_pushinteger(L, RFS_FLIPPED); lua_setfield(L, -2, "FLIPPED"); lua_pushinteger(L, RFS_NONE); lua_setfield(L, -2, "NONE"); lua_setfield(L, -2, "FlipStatus"); lua_pushcfunction(L, M_L_RoomsCount); lua_setfield(L, -2, "count"); lua_pushcfunction(L, M_L_RoomGet); lua_setfield(L, -2, "get"); lua_pushcfunction(L, M_L_RoomGetUnderwater); lua_setfield(L, -2, "get_underwater"); lua_pushcfunction(L, M_L_RoomGetWind); lua_setfield(L, -2, "get_wind"); lua_pushcfunction(L, M_L_RoomSetUnderwater); lua_setfield(L, -2, "set_underwater"); lua_pushcfunction(L, M_L_RoomSetWind); lua_setfield(L, -2, "set_wind"); lua_pushcfunction(L, M_L_RoomGetBounds); lua_setfield(L, -2, "get_bounds"); lua_pushcfunction(L, M_L_RoomGetFlipStatus); lua_setfield(L, -2, "get_flip_status"); lua_pushcfunction(L, M_L_RoomGetFlippedRoom); lua_setfield(L, -2, "get_flipped_room"); lua_pushcfunction(L, M_L_RoomFlip); lua_setfield(L, -2, "flip"); lua_pushcfunction(L, M_L_RoomFlipEffect); lua_setfield(L, -2, "flip_effect"); lua_setfield(L, -2, "rooms"); lua_pop(L, 1); } ================================================ FILE: src/trx/game/lua/sound.c ================================================ #include #include #include // trxc.sound.is_available(id) static int M_L_SoundIsAvailable(lua_State *const L) { const SAMPLE_ID id = (SAMPLE_ID)luaL_checkinteger(L, 1); lua_pushboolean(L, Sound_IsAvailable_Direct(id)); return 1; } // trxc.sound.play(id[, opts]) static int M_L_SoundPlay(lua_State *const L) { const SAMPLE_ID id = (SAMPLE_ID)luaL_checkinteger(L, 1); if (!Sound_IsAvailable_Direct(id)) { return luaL_error(L, "invalid sound track: %d", (int)id); } XYZ_32 pos; const XYZ_32 *pos_ptr = nullptr; if (lua_gettop(L) >= 2 && lua_istable(L, 2)) { lua_getfield(L, 2, "pos"); if (lua_istable(L, -1)) { lua_getfield(L, -1, "x"); pos.x = (int32_t)luaL_optinteger(L, -1, 0); lua_pop(L, 1); lua_getfield(L, -1, "y"); pos.y = (int32_t)luaL_optinteger(L, -1, 0); lua_pop(L, 1); lua_getfield(L, -1, "z"); pos.z = (int32_t)luaL_optinteger(L, -1, 0); lua_pop(L, 1); pos_ptr = &pos; } lua_pop(L, 1); } Sound_Effect_Direct(id, pos_ptr, SPM_ALWAYS | SPM_STATIC_POS); return 0; } // trxc.sound.stop(id) static int M_L_SoundStop(lua_State *const L) { const SAMPLE_ID id = (SAMPLE_ID)luaL_checkinteger(L, 1); Sound_StopEffect_Direct(id); return 0; } // trxc.sound.stop_all() static int M_L_SoundStopAll(lua_State *const L) { Sound_StopAll(); return 0; } void LUA_CreateSound(lua_State *const L) { lua_getglobal(L, "trxc"); lua_newtable(L); lua_pushcfunction(L, M_L_SoundIsAvailable); lua_setfield(L, -2, "is_available"); lua_pushcfunction(L, M_L_SoundPlay); lua_setfield(L, -2, "play"); lua_pushcfunction(L, M_L_SoundStop); lua_setfield(L, -2, "stop"); lua_pushcfunction(L, M_L_SoundStopAll); lua_setfield(L, -2, "stop_all"); lua_setfield(L, -2, "sound"); lua_pop(L, 1); } ================================================ FILE: src/trx/game/lua.h ================================================ #pragma once #include #include ================================================ FILE: src/trx/game/matrix.c ================================================ #include #include #include #include #include #include #define MAX_MATRICES 40 #define MAX_NESTED_MATRICES 32 static MATRIX m_MatrixStack[MAX_MATRICES] = {}; static MATRIX m_WMatrixStack[MAX_MATRICES] = {}; static int32_t m_IMRate = 0; static int32_t m_IMFrac = 0; static MATRIX *m_IMMatrixPtr = nullptr; static MATRIX *m_WIMMatrixPtr = nullptr; static MATRIX m_IMMatrixStack[MAX_NESTED_MATRICES] = {}; static MATRIX m_WIMMatrixStack[MAX_NESTED_MATRICES] = {}; MATRIX *g_MatrixPtr = &m_MatrixStack[0]; MATRIX *g_WMatrixPtr = &m_WMatrixStack[0]; XYZ_32 g_ViewPos = {}; MATRIX g_ViewMatrix = {}; MATRIX g_IDMatrix = { // clang-format off ._00 = 1 << W2V_SHIFT, ._01 = 0, ._02 = 0, ._03 = 0, ._10 = 0, ._11 = 1 << W2V_SHIFT, ._12 = 0, ._13 = 0, ._20 = 0, ._21 = 0, ._22 = 1 << W2V_SHIFT, ._23 = 0, // clang-format on }; static inline void M_QuaternionNormalize(QUATERNION *q) { const double n2 = q->x * q->x + q->y * q->y + q->z * q->z + q->w * q->w; if (n2 > 0.0) { const double inv = 1.0 / sqrt(n2); q->x *= inv; q->y *= inv; q->z *= inv; q->w *= inv; } else { // fallback: identity q->x = q->y = q->z = 0.0; q->w = 1.0; } } // One inexpensive polar-decomposition iteration to orthonormalize R (3x3). // R <- R * (3I - R^T R) / 2 (Newton step toward orthogonal) // Works great if R is already close to rotation. static void M_Double3x3Ortho(double r[3][3]) { double rt_r[3][3] = {}; for (int32_t i = 0; i < 3; i++) { for (int32_t j = 0; j < 3; j++) { for (int32_t k = 0; k < 3; k++) { rt_r[i][j] += r[k][i] * r[k][j]; } } } double m[3][3]; for (int32_t i = 0; i < 3; i++) { for (int32_t j = 0; j < 3; j++) { m[i][j] = 3.0 * (i == j) - rt_r[i][j]; } } double rn[3][3] = {}; for (int32_t i = 0; i < 3; i++) { for (int32_t j = 0; j < 3; j++) { for (int32_t k = 0; k < 3; k++) { rn[i][j] += r[i][k] * m[k][j]; } } } // divide by 2 for (int32_t i = 0; i < 3; i++) { for (int32_t j = 0; j < 3; j++) { r[i][j] = 0.5 * rn[i][j]; } } } // Extract 3x3 rotation (in doubles) from fixed-point MATRIX. static void M_Double3x3FromMatrix(const MATRIX *const m, double e[3][3]) { const double s = (1 << W2V_SHIFT); e[0][0] = m->_00 / s; e[0][1] = m->_01 / s; e[0][2] = m->_02 / s; e[1][0] = m->_10 / s; e[1][1] = m->_11 / s; e[1][2] = m->_12 / s; e[2][0] = m->_20 / s; e[2][1] = m->_21 / s; e[2][2] = m->_22 / s; } // Remove uniform scale if present (estimate from row lengths). // Use average row length to reduce noise. static void M_Double3x3RemoveScale(double e[3][3]) { const double rl0 = sqrt(e[0][0] * e[0][0] + e[0][1] * e[0][1] + e[0][2] * e[0][2]); const double rl1 = sqrt(e[1][0] * e[1][0] + e[1][1] * e[1][1] + e[1][2] * e[1][2]); const double rl2 = sqrt(e[2][0] * e[2][0] + e[2][1] * e[2][1] + e[2][2] * e[2][2]); const double scale = (rl0 + rl1 + rl2) / 3.0; if (scale <= 0.0) { return; } const double inv = 1.0 / scale; for (int32_t i = 0; i < 3; i++) { for (int32_t j = 0; j < 3; j++) { e[i][j] *= inv; } } } // Write 3x3 back to MATRIX as fixed-point, with rounding. static void M_Double3x3ToMatrix(const double e[3][3], MATRIX *m) { const double s = (double)(1 << W2V_SHIFT); m->_00 = (int32_t)llround(e[0][0] * s); m->_01 = (int32_t)llround(e[0][1] * s); m->_02 = (int32_t)llround(e[0][2] * s); m->_10 = (int32_t)llround(e[1][0] * s); m->_11 = (int32_t)llround(e[1][1] * s); m->_12 = (int32_t)llround(e[1][2] * s); m->_20 = (int32_t)llround(e[2][0] * s); m->_21 = (int32_t)llround(e[2][1] * s); m->_22 = (int32_t)llround(e[2][2] * s); } static void M_MatrixToQuaternion(const MATRIX *m, QUATERNION *q) { double e[3][3]; M_Double3x3FromMatrix(m, e); M_Double3x3RemoveScale(e); // Orthonormalize (fast, one Newton step is usually enough). M_Double3x3Ortho(e); const double tr = e[0][0] + e[1][1] + e[2][2]; if (tr > 0.0) { const double s = sqrt(tr + 1.0) * 2.0; // 4*w q->w = 0.25 * s; q->x = (e[2][1] - e[1][2]) / s; q->y = (e[0][2] - e[2][0]) / s; q->z = (e[1][0] - e[0][1]) / s; } else { // Pick the biggest diagonal for numerical stability int32_t i = 0; if (e[1][1] > e[0][0]) { i = 1; } if (e[2][2] > e[i][i]) { i = 2; } const int32_t j = (i + 1) % 3; const int32_t k = (i + 2) % 3; const double s = sqrt(e[i][i] - e[j][j] - e[k][k] + 1.0) * 2.0; double qv[3]; qv[i] = 0.25 * s; qv[j] = (e[j][i] + e[i][j]) / s; qv[k] = (e[k][i] + e[i][k]) / s; q->x = qv[0]; q->y = qv[1]; q->z = qv[2]; q->w = (e[k][j] - e[j][k]) / s; } M_QuaternionNormalize(q); } static void M_MatrixFromQuaternion(const QUATERNION *const qin, MATRIX *const m) { QUATERNION q = *qin; M_QuaternionNormalize(&q); const double xx = q.x * q.x, yy = q.y * q.y, zz = q.z * q.z; const double xy = q.x * q.y, xz = q.x * q.z, yz = q.y * q.z; const double wx = q.w * q.x, wy = q.w * q.y, wz = q.w * q.z; double e[3][3]; e[0][0] = 1.0 - 2.0 * (yy + zz); e[0][1] = 2.0 * (xy - wz); e[0][2] = 2.0 * (xz + wy); e[1][0] = 2.0 * (xy + wz); e[1][1] = 1.0 - 2.0 * (xx + zz); e[1][2] = 2.0 * (yz - wx); e[2][0] = 2.0 * (xz - wy); e[2][1] = 2.0 * (yz + wx); e[2][2] = 1.0 - 2.0 * (xx + yy); // Optional: one more orthonormalization step to crush rounding noise M_Double3x3Ortho(e); M_Double3x3ToMatrix(e, m); } static void M_QuaternionSlerp( const QUATERNION *const qa, const QUATERNION *const qb, const double t, QUATERNION *const out) { QUATERNION a = *qa, b = *qb; M_QuaternionNormalize(&a); M_QuaternionNormalize(&b); double cosom = a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w; if (cosom < 0.0) { // take shortest path cosom = -cosom; b.x = -b.x; b.y = -b.y; b.z = -b.z; b.w = -b.w; } // Guard acos input, and use nlerp for tiny angles CLAMP(cosom, -1.0, 1.0); // Threshold tuned for double precision const double EPS = 1e-12; double scale0; double scale1; if (1.0 - cosom > EPS) { const double omega = acos(cosom); const double sinom = 1.0 / sin(omega); scale0 = sin((1.0 - t) * omega) * sinom; scale1 = sin(t * omega) * sinom; } else { // Nearly parallel: nlerp, then normalize scale0 = 1.0 - t; scale1 = t; } out->x = scale0 * a.x + scale1 * b.x; out->y = scale0 * a.y + scale1 * b.y; out->z = scale0 * a.z + scale1 * b.z; out->w = scale0 * a.w + scale1 * b.w; M_QuaternionNormalize(out); } static void M_ScaleX(MATRIX *const m, const int32_t scale) { m->_00 = ((int64_t)m->_00 * scale) >> W2V_SHIFT; m->_10 = ((int64_t)m->_10 * scale) >> W2V_SHIFT; m->_20 = ((int64_t)m->_20 * scale) >> W2V_SHIFT; } static void M_ScaleY(MATRIX *const m, const int32_t scale) { m->_01 = ((int64_t)m->_01 * scale) >> W2V_SHIFT; m->_11 = ((int64_t)m->_11 * scale) >> W2V_SHIFT; m->_21 = ((int64_t)m->_21 * scale) >> W2V_SHIFT; } static void M_ScaleZ(MATRIX *const m, const int32_t angle) { m->_02 = ((int64_t)m->_02 * angle) >> W2V_SHIFT; m->_12 = ((int64_t)m->_12 * angle) >> W2V_SHIFT; m->_22 = ((int64_t)m->_22 * angle) >> W2V_SHIFT; } static void M_RotX(MATRIX *const m, const int16_t angle) { if (angle == 0) { return; } const int32_t sx = Math_Sin(angle); const int32_t cx = Math_Cos(angle); int32_t r0, r1; r0 = m->_01 * cx + m->_02 * sx; r1 = m->_02 * cx - m->_01 * sx; m->_01 = r0 >> W2V_SHIFT; m->_02 = r1 >> W2V_SHIFT; r0 = m->_11 * cx + m->_12 * sx; r1 = m->_12 * cx - m->_11 * sx; m->_11 = r0 >> W2V_SHIFT; m->_12 = r1 >> W2V_SHIFT; r0 = m->_21 * cx + m->_22 * sx; r1 = m->_22 * cx - m->_21 * sx; m->_21 = r0 >> W2V_SHIFT; m->_22 = r1 >> W2V_SHIFT; } static void M_RotY(MATRIX *const m, const int16_t angle) { if (angle == 0) { return; } const int32_t sy = Math_Sin(angle); const int32_t cy = Math_Cos(angle); int32_t r0, r1; r0 = m->_00 * cy - m->_02 * sy; r1 = m->_02 * cy + m->_00 * sy; m->_00 = r0 >> W2V_SHIFT; m->_02 = r1 >> W2V_SHIFT; r0 = m->_10 * cy - m->_12 * sy; r1 = m->_12 * cy + m->_10 * sy; m->_10 = r0 >> W2V_SHIFT; m->_12 = r1 >> W2V_SHIFT; r0 = m->_20 * cy - m->_22 * sy; r1 = m->_22 * cy + m->_20 * sy; m->_20 = r0 >> W2V_SHIFT; m->_22 = r1 >> W2V_SHIFT; } static void M_RotZ(MATRIX *const m, const int16_t angle) { if (angle == 0) { return; } const int32_t sz = Math_Sin(angle); const int32_t cz = Math_Cos(angle); int32_t r0, r1; r0 = m->_00 * cz + m->_01 * sz; r1 = m->_01 * cz - m->_00 * sz; m->_00 = r0 >> W2V_SHIFT; m->_01 = r1 >> W2V_SHIFT; r0 = m->_10 * cz + m->_11 * sz; r1 = m->_11 * cz - m->_10 * sz; m->_10 = r0 >> W2V_SHIFT; m->_11 = r1 >> W2V_SHIFT; r0 = m->_20 * cz + m->_21 * sz; r1 = m->_21 * cz - m->_20 * sz; m->_20 = r0 >> W2V_SHIFT; m->_21 = r1 >> W2V_SHIFT; } static void M_RotYXZ(MATRIX *const m, const XYZ_16 rotation) { M_RotY(m, rotation.y); M_RotX(m, rotation.x); M_RotZ(m, rotation.z); } static void M_TranslateRel(MATRIX *const m, const XYZ_32 offset) { m->_03 += offset.x * m->_00 + offset.y * m->_01 + offset.z * m->_02; m->_13 += offset.x * m->_10 + offset.y * m->_11 + offset.z * m->_12; m->_23 += offset.x * m->_20 + offset.y * m->_21 + offset.z * m->_22; } static void M_TranslateSet(MATRIX *const m, const XYZ_32 pos) { const int64_t scale = (int64_t)(1 << W2V_SHIFT); m->_03 = (int64_t)pos.x * scale; m->_13 = (int64_t)pos.y * scale; m->_23 = (int64_t)pos.z * scale; } void Matrix_Mul3x3_M( MATRIX *const out, const MATRIX *const lhs, const MATRIX *const rhs) { #define L_MUL(r_, c_) \ (((lhs->_##r_##0 * rhs->_0##c_) + (lhs->_##r_##1 * rhs->_1##c_) \ + (lhs->_##r_##2 * rhs->_2##c_)) \ >> W2V_SHIFT) const int64_t r00 = L_MUL(0, 0); const int64_t r01 = L_MUL(0, 1); const int64_t r02 = L_MUL(0, 2); const int64_t r10 = L_MUL(1, 0); const int64_t r11 = L_MUL(1, 1); const int64_t r12 = L_MUL(1, 2); const int64_t r20 = L_MUL(2, 0); const int64_t r21 = L_MUL(2, 1); const int64_t r22 = L_MUL(2, 2); out->_00 = r00; out->_01 = r01; out->_02 = r02; out->_10 = r10; out->_11 = r11; out->_12 = r12; out->_20 = r20; out->_21 = r21; out->_22 = r22; #undef L_MUL } static void M_InterpolateArm(MATRIX *const m, const MATRIX *const mi) { m->_00 = m[-2]._00; m->_01 = m[-2]._01; m->_02 = m[-2]._02; m->_03 += ((mi->_03 - m->_03) * m_IMFrac) / m_IMRate; m->_10 = m[-2]._10; m->_11 = m[-2]._11; m->_12 = m[-2]._12; m->_13 += ((mi->_13 - m->_13) * m_IMFrac) / m_IMRate; m->_20 = m[-2]._20; m->_21 = m[-2]._21; m->_22 = m[-2]._22; m->_23 += ((mi->_23 - m->_23) * m_IMFrac) / m_IMRate; } static void M_Interpolate( const MATRIX *const m1, const MATRIX *const m2, MATRIX *const result) { double rate = (m_IMRate != 0) ? ((double)m_IMFrac / (double)m_IMRate) : 0.0; CLAMP(rate, 0.0, 1.0); QUATERNION q1, q2, q; M_MatrixToQuaternion(m1, &q1); M_MatrixToQuaternion(m2, &q2); M_QuaternionSlerp(&q1, &q2, rate, &q); M_MatrixFromQuaternion(&q, result); result->_03 = (int32_t)llround(m1->_03 + (m2->_03 - m1->_03) * rate); result->_13 = (int32_t)llround(m1->_13 + (m2->_13 - m1->_13) * rate); result->_23 = (int32_t)llround(m1->_23 + (m2->_23 - m1->_23) * rate); } void Matrix_ResetStack(void) { g_MatrixPtr = &m_MatrixStack[0]; g_WMatrixPtr = &m_WMatrixStack[0]; } void Matrix_GenerateW2V(const XYZ_32 *pos, const XYZ_16 *rot) { const int32_t sx = Math_Sin(rot->x); const int32_t cx = Math_Cos(rot->x); const int32_t sy = Math_Sin(rot->y); const int32_t cy = Math_Cos(rot->y); const int32_t sz = Math_Sin(rot->z); const int32_t cz = Math_Cos(rot->z); g_ViewPos = *pos; g_ViewMatrix._00 = TRIGMULT3(sx, sy, sz) + TRIGMULT2(cy, cz); g_ViewMatrix._01 = TRIGMULT2(cx, sz); g_ViewMatrix._02 = TRIGMULT3(sx, cy, sz) - TRIGMULT2(sy, cz); g_ViewMatrix._10 = TRIGMULT3(sx, sy, cz) - TRIGMULT2(cy, sz); g_ViewMatrix._11 = TRIGMULT2(cx, cz); g_ViewMatrix._12 = TRIGMULT3(sx, cy, cz) + TRIGMULT2(sy, sz); g_ViewMatrix._20 = TRIGMULT2(cx, sy); g_ViewMatrix._21 = -sx; g_ViewMatrix._22 = TRIGMULT2(cx, cy); g_ViewMatrix._03 = 0; g_ViewMatrix._13 = 0; g_ViewMatrix._23 = 0; M_TranslateRel(&g_ViewMatrix, (XYZ_32) { -pos->x, -pos->y, -pos->z }); g_MatrixPtr = &m_MatrixStack[0]; m_MatrixStack[0] = g_ViewMatrix; g_WMatrixPtr = &m_WMatrixStack[0]; g_WMatrixPtr[0] = g_IDMatrix; } bool Matrix_Push(void) { if (g_MatrixPtr + 1 - m_MatrixStack >= MAX_MATRICES) { return false; } if (g_WMatrixPtr + 1 - m_WMatrixStack >= MAX_MATRICES) { return false; } g_MatrixPtr++; g_MatrixPtr[0] = g_MatrixPtr[-1]; g_WMatrixPtr++; g_WMatrixPtr[0] = g_WMatrixPtr[-1]; return true; } bool Matrix_PushUnit(void) { if (g_MatrixPtr + 1 - m_MatrixStack >= MAX_MATRICES) { return false; } if (g_WMatrixPtr + 1 - m_WMatrixStack >= MAX_MATRICES) { return false; } g_MatrixPtr++; *g_MatrixPtr = g_IDMatrix; g_WMatrixPtr++; *g_WMatrixPtr = g_IDMatrix; return true; } void Matrix_Pop(void) { g_MatrixPtr--; g_WMatrixPtr--; } void Matrix_Scale(const int32_t scale) { Matrix_ScaleX(scale); Matrix_ScaleY(scale); Matrix_ScaleZ(scale); } void Matrix_ScaleX(const int32_t scale) { M_ScaleX(g_MatrixPtr, scale); M_ScaleX(g_WMatrixPtr, scale); } void Matrix_ScaleY(const int32_t scale) { M_ScaleY(g_MatrixPtr, scale); M_ScaleY(g_WMatrixPtr, scale); } void Matrix_ScaleZ(const int32_t scale) { M_ScaleZ(g_MatrixPtr, scale); M_ScaleZ(g_WMatrixPtr, scale); } void Matrix_RotX(const int16_t angle) { M_RotX(g_MatrixPtr, angle); M_RotX(g_WMatrixPtr, angle); } void Matrix_RotY(const int16_t angle) { M_RotY(g_MatrixPtr, angle); M_RotY(g_WMatrixPtr, angle); } void Matrix_RotZ(const int16_t angle) { M_RotZ(g_MatrixPtr, angle); M_RotZ(g_WMatrixPtr, angle); } void Matrix_Rot16(const XYZ_16 rotation) { M_RotYXZ(g_MatrixPtr, rotation); M_RotYXZ(g_WMatrixPtr, rotation); } void Matrix_RotX_M(MATRIX *const m, const int16_t angle) { M_RotX(m, angle); } void Matrix_RotY_M(MATRIX *const m, const int16_t angle) { M_RotY(m, angle); } void Matrix_RotZ_M(MATRIX *const m, const int16_t angle) { M_RotZ(m, angle); } void Matrix_Slerp3x3_M( MATRIX *const lhs_out, const MATRIX *const rhs, const double t) { QUATERNION q1, q2, q; M_MatrixToQuaternion(lhs_out, &q1); M_MatrixToQuaternion(rhs, &q2); double clamped_t = t; CLAMP(clamped_t, 0.0, 1.0); M_QuaternionSlerp(&q1, &q2, clamped_t, &q); M_MatrixFromQuaternion(&q, lhs_out); } void Matrix_Mul3x3(const MATRIX *const rhs) { Matrix_Mul3x3_M(g_MatrixPtr, g_MatrixPtr, rhs); Matrix_Mul3x3_M(g_WMatrixPtr, g_WMatrixPtr, rhs); } void Matrix_TranslateRel(const int32_t dx, const int32_t dy, const int32_t dz) { Matrix_TranslateRel32((XYZ_32) { dx, dy, dz }); } void Matrix_TranslateRel16(const XYZ_16 offset) { Matrix_TranslateRel32(XYZ_32_From16(offset)); } void Matrix_TranslateRel32(const XYZ_32 offset) { M_TranslateRel(g_MatrixPtr, offset); M_TranslateRel(g_WMatrixPtr, offset); } void Matrix_TranslateAbs(const int32_t x, const int32_t y, const int32_t z) { MATRIX *const m = g_MatrixPtr; const MATRIX *const v = &g_ViewMatrix; m->_03 = x * v->_00 + y * v->_01 + z * v->_02 + v->_03; m->_13 = x * v->_10 + y * v->_11 + z * v->_12 + v->_13; m->_23 = x * v->_20 + y * v->_21 + z * v->_22 + v->_23; M_TranslateSet(g_WMatrixPtr, (XYZ_32) { x, y, z }); } void Matrix_TranslateAbs16(const XYZ_16 offset) { Matrix_TranslateAbs(offset.x, offset.y, offset.z); } void Matrix_TranslateAbs32(const XYZ_32 offset) { Matrix_TranslateAbs(offset.x, offset.y, offset.z); } void Matrix_TranslateSet32(const XYZ_32 origin) { M_TranslateSet(g_MatrixPtr, origin); M_TranslateSet(g_WMatrixPtr, origin); } void Matrix_TranslateSet32_M(MATRIX *const m, const XYZ_32 origin) { M_TranslateSet(m, origin); } void Matrix_InitInterpolate(const int32_t frac, const int32_t rate) { m_IMFrac = frac; m_IMRate = rate; m_IMMatrixPtr = &m_IMMatrixStack[0]; *m_IMMatrixPtr = *g_MatrixPtr; m_WIMMatrixPtr = &m_WIMMatrixStack[0]; *m_WIMMatrixPtr = *g_WMatrixPtr; } void Matrix_Interpolate(void) { M_Interpolate(g_MatrixPtr, m_IMMatrixPtr, g_MatrixPtr); M_Interpolate(g_WMatrixPtr, m_WIMMatrixPtr, g_WMatrixPtr); } void Matrix_InterpolateArm(void) { M_InterpolateArm(g_MatrixPtr, m_IMMatrixPtr); M_InterpolateArm(g_WMatrixPtr, m_WIMMatrixPtr); } void Matrix_Push_I(void) { Matrix_Push(); m_IMMatrixPtr[1] = m_IMMatrixPtr[0]; m_IMMatrixPtr++; m_WIMMatrixPtr[1] = m_WIMMatrixPtr[0]; m_WIMMatrixPtr++; } void Matrix_Pop_I(void) { Matrix_Pop(); m_IMMatrixPtr--; m_WIMMatrixPtr--; } void Matrix_TranslateRel_I(const int32_t x, const int32_t y, const int32_t z) { Matrix_TranslateRel32_I((XYZ_32) { x, y, z }); } void Matrix_TranslateRel16_I(const XYZ_16 offset) { Matrix_TranslateRel32_I(XYZ_32_From16(offset)); } void Matrix_TranslateRel32_I(const XYZ_32 offset) { M_TranslateRel(g_MatrixPtr, offset); M_TranslateRel(g_WMatrixPtr, offset); M_TranslateRel(m_IMMatrixPtr, offset); M_TranslateRel(m_WIMMatrixPtr, offset); } void Matrix_TranslateRel_ID( const int32_t x, const int32_t y, const int32_t z, const int32_t x2, const int32_t y2, const int32_t z2) { Matrix_TranslateRel32_ID((XYZ_32) { x, y, z }, (XYZ_32) { x2, y2, z2 }); } void Matrix_TranslateRel16_ID(const XYZ_16 offset_1, const XYZ_16 offset_2) { Matrix_TranslateRel32_ID(XYZ_32_From16(offset_1), XYZ_32_From16(offset_2)); } void Matrix_TranslateRel32_ID(const XYZ_32 offset_1, const XYZ_32 offset_2) { M_TranslateRel(g_MatrixPtr, offset_1); M_TranslateRel(g_WMatrixPtr, offset_1); M_TranslateRel(m_IMMatrixPtr, offset_2); M_TranslateRel(m_WIMMatrixPtr, offset_2); } void Matrix_RotY_I(const int16_t angle) { M_RotY(g_MatrixPtr, angle); M_RotY(g_WMatrixPtr, angle); M_RotY(m_IMMatrixPtr, angle); M_RotY(m_WIMMatrixPtr, angle); } void Matrix_RotX_I(const int16_t angle) { M_RotX(g_MatrixPtr, angle); M_RotX(g_WMatrixPtr, angle); M_RotX(m_IMMatrixPtr, angle); M_RotX(m_WIMMatrixPtr, angle); } void Matrix_RotZ_I(const int16_t angle) { M_RotZ(g_MatrixPtr, angle); M_RotZ(g_WMatrixPtr, angle); M_RotZ(m_IMMatrixPtr, angle); M_RotZ(m_WIMMatrixPtr, angle); } void Matrix_Rot16_I(const XYZ_16 rotation) { M_RotYXZ(g_MatrixPtr, rotation); M_RotYXZ(g_WMatrixPtr, rotation); M_RotYXZ(m_IMMatrixPtr, rotation); M_RotYXZ(m_WIMMatrixPtr, rotation); } void Matrix_Rot16_ID(const XYZ_16 rotation_1, const XYZ_16 rotation_2) { M_RotYXZ(g_MatrixPtr, rotation_1); M_RotYXZ(g_WMatrixPtr, rotation_1); M_RotYXZ(m_IMMatrixPtr, rotation_2); M_RotYXZ(m_WIMMatrixPtr, rotation_2); } void Matrix_LookAt( const int32_t source_x, const int32_t source_y, const int32_t source_z, const int32_t target_x, const int32_t target_y, const int32_t target_z, const int16_t roll) { int16_t angles[2]; Math_GetVectorAngles( target_x - source_x, target_y - source_y, target_z - source_z, angles); const XYZ_32 view_pos = { .x = source_x, .y = source_y, .z = source_z, }; const XYZ_16 view_rot = { .x = angles[1], .y = angles[0], .z = roll, }; Matrix_GenerateW2V(&view_pos, &view_rot); } XYZ_32 Matrix_MulVec32_M(const MATRIX *const m, const XYZ_32 v) { return (XYZ_32) { (m->_00 * v.x + m->_01 * v.y + m->_02 * v.z + m->_03) >> W2V_SHIFT, (m->_10 * v.x + m->_11 * v.y + m->_12 * v.z + m->_13) >> W2V_SHIFT, (m->_20 * v.x + m->_21 * v.y + m->_22 * v.z + m->_23) >> W2V_SHIFT, }; } XYZ_32 Matrix_GetOffset_M(const MATRIX *const m) { return (XYZ_32) { .x = m->_03 >> W2V_SHIFT, .y = m->_13 >> W2V_SHIFT, .z = m->_23 >> W2V_SHIFT, }; } ================================================ FILE: src/trx/game/matrix.h ================================================ #pragma once #include #define TRIGMULT2(A, B) (((A) * (B)) >> W2V_SHIFT) #define TRIGMULT3(A, B, C) (TRIGMULT2((TRIGMULT2(A, B)), C)) typedef struct QUATERNION { double x; double y; double z; double w; } QUATERNION; typedef struct { int64_t _00, _01, _02, _03, _10, _11, _12, _13, _20, _21, _22, _23; } MATRIX; extern MATRIX *g_MatrixPtr; extern MATRIX *g_WMatrixPtr; extern XYZ_32 g_ViewPos; extern MATRIX g_ViewMatrix; extern MATRIX g_IDMatrix; void Matrix_ResetStack(void); void Matrix_GenerateW2V(const XYZ_32 *pos, const XYZ_16 *rot); void Matrix_LookAt( int32_t xsrc, int32_t ysrc, int32_t zsrc, int32_t xtar, int32_t ytar, int32_t ztar, int16_t roll); bool Matrix_Push(void); bool Matrix_PushUnit(void); void Matrix_Pop(void); void Matrix_Scale(int32_t scale); void Matrix_ScaleX(int32_t sx); void Matrix_ScaleY(int32_t sy); void Matrix_ScaleZ(int32_t sz); void Matrix_RotX(int16_t rx); void Matrix_RotY(int16_t ry); void Matrix_RotZ(int16_t rz); void Matrix_Rot16(XYZ_16 rotation); void Matrix_RotX_M(MATRIX *m, int16_t rx); void Matrix_RotY_M(MATRIX *m, int16_t ry); void Matrix_RotZ_M(MATRIX *m, int16_t rz); void Matrix_Mul3x3_M(MATRIX *out, const MATRIX *lhs, const MATRIX *rhs); void Matrix_Slerp3x3_M(MATRIX *lhs_out, const MATRIX *rhs, double t); void Matrix_Mul3x3(const MATRIX *rhs); void Matrix_TranslateRel(int32_t x, int32_t y, int32_t z); void Matrix_TranslateRel16(XYZ_16 offset); void Matrix_TranslateRel32(XYZ_32 offset); void Matrix_TranslateAbs(int32_t x, int32_t y, int32_t z); void Matrix_TranslateAbs16(XYZ_16 offset); void Matrix_TranslateAbs32(XYZ_32 offset); void Matrix_TranslateSet32(XYZ_32 origin); void Matrix_TranslateSet32_M(MATRIX *out, XYZ_32 origin); void Matrix_Push_I(void); void Matrix_Pop_I(void); void Matrix_RotY_I(int16_t ang); void Matrix_RotX_I(int16_t ang); void Matrix_RotZ_I(int16_t ang); void Matrix_Rot16_I(const XYZ_16 rotation); void Matrix_Rot16_ID(XYZ_16 rotation_1, XYZ_16 rotation_2); void Matrix_TranslateRel_I(int32_t x, int32_t y, int32_t z); void Matrix_TranslateRel16_I(XYZ_16 offset); void Matrix_TranslateRel32_I(XYZ_32 offset); void Matrix_TranslateRel_ID( int32_t x, int32_t y, int32_t z, int32_t x2, int32_t y2, int32_t z2); void Matrix_TranslateRel16_ID(XYZ_16 offset_1, XYZ_16 offset_2); void Matrix_TranslateRel32_ID(XYZ_32 offset_1, XYZ_32 offset_2); void Matrix_InitInterpolate(int32_t frac, int32_t rate); void Matrix_Interpolate(void); void Matrix_InterpolateArm(void); XYZ_32 Matrix_MulVec32_M(const MATRIX *m, const XYZ_32 v); XYZ_32 Matrix_GetOffset_M(const MATRIX *m); ================================================ FILE: src/trx/game/music/backend_cdaudio.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include typedef struct { uint64_t from; uint64_t to; bool active; } M_CDAUDIO_TRACK; typedef struct { const char *path; const char *control_path; const char *description; int32_t num_tracks; M_CDAUDIO_TRACK *tracks; } M_BACKEND_DATA; static bool M_Parse(M_BACKEND_DATA *const data) { ASSERT(data != nullptr); char *track_content = nullptr; size_t track_content_size; if (!File_Load(data->control_path, &track_content, &track_content_size)) { LOG_WARNING("Cannot find CDAudio control file: %s", data->control_path); return false; } VECTOR *const tracks = Vector_Create(sizeof(M_CDAUDIO_TRACK)); size_t offset = 0; while (offset < track_content_size) { while (track_content[offset] == '\n' || track_content[offset] == '\r') { if (++offset >= track_content_size) { goto parse_end; } } uint64_t track_num; uint64_t from; uint64_t to; const int32_t result = sscanf( &track_content[offset], "%" PRIu64 " %" PRIu64 " %" PRIu64, &track_num, &from, &to); M_CDAUDIO_TRACK track = {}; if (result == 3 && track_num > 0) { track.active = true; track.from = from; track.to = to; } Vector_Add(tracks, (void *)&track); while (track_content[offset] != '\n' && track_content[offset] != '\r') { if (++offset >= track_content_size) { goto parse_end; } } } parse_end: Memory_Free(track_content); data->num_tracks = tracks->count; const size_t data_size = sizeof(M_CDAUDIO_TRACK) * data->num_tracks; data->tracks = Memory_Alloc(data_size); memcpy(data->tracks, Vector_GetData(tracks), data_size); Vector_Free(tracks); // reindex wrong track boundaries for (int32_t i = 0; i < data->num_tracks; i++) { if (!data->tracks[i].active) { continue; } if (i < data->num_tracks - 1 && data->tracks[i].from >= data->tracks[i].to) { for (int32_t j = i + 1; j < data->num_tracks; j++) { if (data->tracks[j].active) { data->tracks[i].to = data->tracks[j].from; break; } } } if (data->tracks[i].from >= data->tracks[i].to && i > 0) { for (int32_t j = i - 1; j >= 0; j--) { if (data->tracks[j].active) { data->tracks[i].from = data->tracks[j].to; break; } } } } return true; } static bool M_Init(MUSIC_BACKEND *const backend) { ASSERT(backend != nullptr); M_BACKEND_DATA *const data = backend->data; ASSERT(data != nullptr); MYFILE *const fp = File_Open(data->path, FILE_OPEN_READ); if (fp == nullptr) { return false; } File_Close(fp); if (!M_Parse(data)) { LOG_ERROR("Failed to parse CDAudio data"); return false; } return true; } static const char *M_Describe(const MUSIC_BACKEND *const backend) { ASSERT(backend != nullptr); const M_BACKEND_DATA *const data = backend->data; ASSERT(data != nullptr); return data->description; } static int32_t M_Play( const MUSIC_BACKEND *const backend, const int32_t track_id) { ASSERT(backend != nullptr); const M_BACKEND_DATA *const data = backend->data; ASSERT(data != nullptr); const int32_t track_idx = track_id - 1; const M_CDAUDIO_TRACK *track = &data->tracks[track_idx]; if (track_idx < 0 || track_idx >= data->num_tracks) { LOG_ERROR("Invalid track: %d", track_id); return -1; } if (!track->active) { LOG_ERROR("Invalid track: %d", track_id); return -1; } const int32_t audio_stream_id = Audio_Stream_CreateFromFile(data->path); Audio_Stream_SetStartTimestamp(audio_stream_id, track->from / 1000.0); Audio_Stream_SetStopTimestamp(audio_stream_id, track->to / 1000.0); Audio_Stream_SeekTimestamp(audio_stream_id, 0.0f); return audio_stream_id; } static void M_Shutdown(MUSIC_BACKEND *backend) { if (backend == nullptr) { return; } if (backend->data != nullptr) { M_BACKEND_DATA *const data = backend->data; Memory_FreePointer(&data->path); Memory_FreePointer(&data->control_path); Memory_FreePointer(&data->description); Memory_FreePointer(&data->tracks); } Memory_FreePointer(&backend->data); Memory_FreePointer(&backend); } MUSIC_BACKEND *Music_Backend_CDAudio_Factory( const char *const path, const char *const control_path) { ASSERT(path != nullptr); ASSERT(control_path != nullptr); const char *description_fmt = "CDAudio (path: %s)"; const size_t description_size = snprintf(nullptr, 0, description_fmt, path); char *description = Memory_Alloc(description_size + 1); sprintf(description, description_fmt, path); M_BACKEND_DATA *const data = Memory_Alloc(sizeof(M_BACKEND_DATA)); data->path = Memory_DupStr(path); data->control_path = Memory_DupStr(control_path); data->description = description; MUSIC_BACKEND *const backend = Memory_Alloc(sizeof(MUSIC_BACKEND)); backend->data = data; backend->init = M_Init; backend->describe = M_Describe; backend->play = M_Play; backend->shutdown = M_Shutdown; return backend; } ================================================ FILE: src/trx/game/music/backend_cdaudio.h ================================================ #pragma once #include MUSIC_BACKEND *Music_Backend_CDAudio_Factory( const char *path, const char *control_path); ================================================ FILE: src/trx/game/music/backend_cdaudio_wad.c ================================================ #include #include #include #include #include #include #include #include #include #include #define M_CDAUDIO_WAD_TRACK_COUNT 130 #define M_CDAUDIO_WAD_WFX_OFFSET 20 #define M_CDAUDIO_WAD_WFX_SIZE 50 #define M_CDAUDIO_WAD_WAV_HEADER_SIZE 90 typedef struct { char name[260]; uint32_t size; uint32_t offset; } M_CDAUDIO_WAD_TRACK_DESC; _Static_assert( sizeof(M_CDAUDIO_WAD_TRACK_DESC) == 268, "Unexpected cdaudio.wad track info size"); typedef struct { uint32_t size; uint32_t offset; bool active; } M_CDAUDIO_WAD_TRACK; typedef struct { const char *path; const char *description; M_CDAUDIO_WAD_TRACK tracks[M_CDAUDIO_WAD_TRACK_COUNT]; uint8_t wfx[M_CDAUDIO_WAD_WFX_SIZE]; bool has_wfx; } M_BACKEND_DATA; static bool M_LoadTrackAsWaveFile( const MUSIC_BACKEND *const backend, const int32_t track_id, uint8_t **const out_data, size_t *const out_size) { ASSERT(backend != nullptr); ASSERT(out_data != nullptr); ASSERT(out_size != nullptr); const M_BACKEND_DATA *const data = backend->data; ASSERT(data != nullptr); *out_data = nullptr; *out_size = 0; const int32_t track_idx = track_id - 1; if (track_idx < 0 || track_idx >= M_CDAUDIO_WAD_TRACK_COUNT) { LOG_ERROR("Invalid track: %d", track_id); return false; } const M_CDAUDIO_WAD_TRACK *const track = &data->tracks[track_idx]; if (!track->active || track->size == 0) { LOG_ERROR("Invalid track: %d", track_id); return false; } MYFILE *const fp = File_Open(data->path, FILE_OPEN_READ); if (fp == nullptr) { return false; } const size_t file_size = File_Size(fp); if ((size_t)track->offset >= file_size) { LOG_ERROR( "Invalid track offset for %d: offset %lu, file size %zu", track_id, track->offset, file_size); File_Close(fp); return false; } // Some installations have invalid track sizes which would result in reading // beyond EOF if left unchecked. The data can still be valid, so clamp such // tracks to the logical remaining file length. const size_t remaining = file_size - (size_t)track->offset; const size_t track_size = M_CDAUDIO_WAD_WAV_HEADER_SIZE + (size_t)track->size; const size_t total_size = MIN(track_size, remaining); uint8_t *const buf = Memory_Alloc(total_size); File_Seek(fp, (size_t)track->offset, FILE_SEEK_SET); const bool ok = File_ReadData(fp, buf, total_size); File_Close(fp); if (!ok) { Memory_Free(buf); return false; } *out_data = buf; *out_size = total_size; return true; } static bool M_ReadAllTrackInfos(MYFILE *const fp, M_BACKEND_DATA *const data) { ASSERT(fp != nullptr); ASSERT(data != nullptr); M_CDAUDIO_WAD_TRACK_DESC track_infos[M_CDAUDIO_WAD_TRACK_COUNT] = {}; File_Skip(fp, sizeof(M_CDAUDIO_WAD_TRACK_DESC)); if (!File_ReadItems( fp, track_infos, M_CDAUDIO_WAD_TRACK_COUNT, sizeof(M_CDAUDIO_WAD_TRACK_DESC))) { return false; } data->has_wfx = false; int32_t first_active_idx = -1; for (int32_t i = 0; i < M_CDAUDIO_WAD_TRACK_COUNT; i++) { const bool is_active = track_infos[i].size != 0; data->tracks[i].active = is_active; data->tracks[i].size = track_infos[i].size; data->tracks[i].offset = track_infos[i].offset; if (first_active_idx < 0 && is_active) { first_active_idx = i; } } if (first_active_idx < 0) { return true; } const size_t wfx_pos = (size_t)data->tracks[first_active_idx].offset + M_CDAUDIO_WAD_WFX_OFFSET; File_Seek(fp, wfx_pos, FILE_SEEK_SET); if (!File_ReadData(fp, data->wfx, M_CDAUDIO_WAD_WFX_SIZE)) { return false; } data->has_wfx = true; return true; } static bool M_Init(MUSIC_BACKEND *const backend) { ASSERT(backend != nullptr); M_BACKEND_DATA *const data = backend->data; ASSERT(data != nullptr); MYFILE *const fp = File_Open(data->path, FILE_OPEN_READ); if (fp == nullptr) { return false; } const bool ok = M_ReadAllTrackInfos(fp, data); File_Close(fp); return ok; } static const char *M_Describe(const MUSIC_BACKEND *const backend) { ASSERT(backend != nullptr); const M_BACKEND_DATA *const data = backend->data; ASSERT(data != nullptr); return data->description; } static int32_t M_Play( const MUSIC_BACKEND *const backend, const int32_t track_id) { ASSERT(backend != nullptr); uint8_t *wav_data = nullptr; size_t wav_size = 0; if (!M_LoadTrackAsWaveFile(backend, track_id, &wav_data, &wav_size)) { return AUDIO_NO_SOUND; } const int32_t stream_id = Audio_Stream_CreateFromMemory(wav_data, wav_size); if (stream_id < 0) { Memory_Free(wav_data); } return stream_id; } static void M_Shutdown(MUSIC_BACKEND *backend) { if (backend == nullptr) { return; } if (backend->data != nullptr) { M_BACKEND_DATA *const data = backend->data; Memory_FreePointer(&data->path); Memory_FreePointer(&data->description); } Memory_FreePointer(&backend->data); Memory_FreePointer(&backend); } MUSIC_BACKEND *Music_Backend_CDAudioWad_Factory(const char *const path) { ASSERT(path != nullptr); M_BACKEND_DATA *const data = Memory_Alloc(sizeof(M_BACKEND_DATA)); *data = (M_BACKEND_DATA) { .path = Memory_DupStr(path), .description = String_Format("CDAudio WAD (path: %s)", path), }; MUSIC_BACKEND *const backend = Memory_Alloc(sizeof(MUSIC_BACKEND)); backend->data = data; backend->init = M_Init; backend->describe = M_Describe; backend->play = M_Play; backend->shutdown = M_Shutdown; return backend; } ================================================ FILE: src/trx/game/music/backend_cdaudio_wad.h ================================================ #pragma once #include MUSIC_BACKEND *Music_Backend_CDAudioWad_Factory(const char *path); ================================================ FILE: src/trx/game/music/backend_files.c ================================================ #include #include #include #include #include #include #include #include #include typedef struct { const char *dir; const char *description; } M_BACKEND_DATA; static const char *m_ExtensionsToTry[] = { ".flac", ".ogg", ".mp3", ".wav", ".wma", nullptr, }; static char *M_GetTrackFileName(const char *base_dir, int32_t track) { char *tmp_path = String_Format("%s/track%02d.flac", base_dir, track); char *result = TRXPath_GuessExtension(tmp_path, m_ExtensionsToTry); Memory_FreePointer(&tmp_path); if (result == nullptr) { tmp_path = String_Format("%s/%d.flac", base_dir, track); result = TRXPath_GuessExtension(tmp_path, m_ExtensionsToTry); Memory_FreePointer(&tmp_path); } return result; } static bool M_Init(MUSIC_BACKEND *const backend) { ASSERT(backend != nullptr); const M_BACKEND_DATA *data = backend->data; ASSERT(data->dir != nullptr); return File_DirExists(data->dir); } static const char *M_Describe(const MUSIC_BACKEND *const backend) { ASSERT(backend != nullptr); const M_BACKEND_DATA *const data = backend->data; ASSERT(data != nullptr); return data->description; } static int32_t M_Play( const MUSIC_BACKEND *const backend, const int32_t track_id) { ASSERT(backend != nullptr); const M_BACKEND_DATA *const data = backend->data; ASSERT(data != nullptr); char *file_path = M_GetTrackFileName(data->dir, track_id); if (file_path == nullptr) { LOG_ERROR("Invalid track: %d", track_id); return -1; } const int32_t stream_id = Audio_Stream_CreateFromFile(file_path); Memory_Free(file_path); return stream_id; } static void M_Shutdown(MUSIC_BACKEND *backend) { if (backend == nullptr) { return; } if (backend->data != nullptr) { M_BACKEND_DATA *const data = backend->data; Memory_FreePointer(&data->dir); Memory_FreePointer(&data->description); } Memory_FreePointer(&backend->data); Memory_FreePointer(&backend); } MUSIC_BACKEND *Music_Backend_Files_Factory(const char *path) { ASSERT(path != nullptr); const char *description_fmt = "Directory (directory: %s)"; const size_t description_size = snprintf(nullptr, 0, description_fmt, path); char *description = Memory_Alloc(description_size + 1); sprintf(description, description_fmt, path); M_BACKEND_DATA *const data = Memory_Alloc(sizeof(M_BACKEND_DATA)); data->dir = Memory_DupStr(path); data->description = description; MUSIC_BACKEND *const backend = Memory_Alloc(sizeof(MUSIC_BACKEND)); backend->data = data; backend->init = M_Init; backend->describe = M_Describe; backend->play = M_Play; backend->shutdown = M_Shutdown; return backend; } ================================================ FILE: src/trx/game/music/backend_files.h ================================================ #pragma once #include MUSIC_BACKEND *Music_Backend_Files_Factory(const char *path); ================================================ FILE: src/trx/game/music/common.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include static bool m_Initialised = false; static uint16_t m_MusicTrackFlags[MAX_MUSIC_TRACKS] = {}; static MUSIC_ID m_TrackCurrent = MX_INACTIVE; static MUSIC_ID m_TrackDelayed = MX_INACTIVE; static MUSIC_ID m_TrackLooped = MX_INACTIVE; // Remember the last played track, whether normal or looped, to prevent // immediately restarting it if Lara remains on the same trigger. static MUSIC_ID m_TrackLastPlayed = MX_INACTIVE; static MUSIC_ID m_TrackLastLooped = MX_INACTIVE; typedef struct { int32_t audio_stream_id; MUSIC_ID track_id; MUSIC_PLAY_MODE mode; bool active; } M_MUSIC_STREAM; static float m_MusicVolume = 0.0f; static MUSIC_BACKEND *m_Backend = nullptr; static M_MUSIC_STREAM m_MainStream = { .audio_stream_id = -1, .track_id = MX_INACTIVE, .mode = MPM_ONCE, .active = false, }; static M_MUSIC_STREAM m_OverlayStreams[MUSIC_MAX_OVERLAY_TRACKS] = {}; static MUSIC_BACKEND *M_FindBackend(void) { VECTOR *all_backends = Vector_Create(sizeof(MUSIC_BACKEND *)); const char *const music_dir = TRXPath_PeekResolve(TRX_DYNAMIC_PATH_MUSIC_DIR, nullptr); if (music_dir != nullptr) { Vector_Add( all_backends, &(MUSIC_BACKEND *) { Music_Backend_Files_Factory(music_dir) }); } if (g_TRVersion >= 2) { const char *const cdaudio_dat_path = TRXPath_PeekResolve(TRX_DYNAMIC_PATH_CDAUDIO_FILE, "cdaudio.dat"); const char *const cdaudio_wav_path = TRXPath_PeekResolve(TRX_DYNAMIC_PATH_CDAUDIO_FILE, "cdaudio.wav"); const char *const cdaudio_mp3_path = TRXPath_PeekResolve(TRX_DYNAMIC_PATH_CDAUDIO_FILE, "cdaudio.mp3"); if (cdaudio_dat_path != nullptr && cdaudio_wav_path != nullptr) { Vector_Add( all_backends, &(MUSIC_BACKEND *) { Music_Backend_CDAudio_Factory( cdaudio_wav_path, cdaudio_dat_path) }); } if (cdaudio_dat_path != nullptr && cdaudio_mp3_path != nullptr) { Vector_Add( all_backends, &(MUSIC_BACKEND *) { Music_Backend_CDAudio_Factory( cdaudio_mp3_path, cdaudio_dat_path) }); } } if (g_TRVersion >= 3) { const char *const cdaudio_wad_path = TRXPath_PeekResolve(TRX_DYNAMIC_PATH_CDAUDIO_FILE, "cdaudio.wad"); if (cdaudio_wad_path != nullptr) { Vector_Add( all_backends, &(MUSIC_BACKEND *) { Music_Backend_CDAudioWad_Factory(cdaudio_wad_path) }); } } MUSIC_BACKEND *backend = nullptr; for (int32_t i = 0; i < all_backends->count; i++) { MUSIC_BACKEND *const tmp_backend = *(MUSIC_BACKEND **)Vector_Get(all_backends, i); if (tmp_backend->init(tmp_backend)) { backend = tmp_backend; break; } } for (int32_t i = 0; i < all_backends->count; i++) { MUSIC_BACKEND *const tmp_backend = *(MUSIC_BACKEND **)Vector_Get(all_backends, i); if (tmp_backend != backend) { tmp_backend->shutdown(tmp_backend); } } Vector_Free(all_backends); return backend; } static void M_StreamReset(M_MUSIC_STREAM *const stream) { stream->audio_stream_id = -1; stream->track_id = MX_INACTIVE; stream->mode = MPM_ONCE; stream->active = false; } static void M_StreamClose(M_MUSIC_STREAM *const stream) { if (!stream->active || stream->audio_stream_id < 0) { M_StreamReset(stream); return; } // We are only interested in calling M_StreamFinished if a stream // finished by itself. In cases where we end the streams early by hand, // we clear the finish callback in order to avoid resuming the BGM playback // just after we stop it. Audio_Stream_SetFinishCallback(stream->audio_stream_id, nullptr, nullptr); Audio_Stream_Close(stream->audio_stream_id); M_StreamReset(stream); } static void M_StopMainStream(void) { M_StreamClose(&m_MainStream); } static void M_StopOverlayStreams(void) { for (int32_t i = 0; i < MUSIC_MAX_OVERLAY_TRACKS; i++) { M_StreamClose(&m_OverlayStreams[i]); } } static void M_ResetStreamState(void) { M_StreamReset(&m_MainStream); for (int32_t i = 0; i < MUSIC_MAX_OVERLAY_TRACKS; i++) { M_StreamReset(&m_OverlayStreams[i]); } } static void M_StreamFinished(const int32_t stream_id, void *const user_data) { M_MUSIC_STREAM *const stream = user_data; if (stream == nullptr) { return; } if (!stream->active || stream->audio_stream_id != stream_id) { return; } if (stream == &m_MainStream) { // When the main stream finishes, play the remembered BGM. m_TrackCurrent = MX_INACTIVE; M_StreamReset(stream); if (m_TrackLooped >= 0) { m_TrackLastLooped = MX_INACTIVE; Music_Play_Direct(m_TrackLooped, MPM_LOOP); } } else { M_StreamReset(stream); } } static bool M_IsBrokenTrack(const MUSIC_ID track_id) { if (track_id < 0) { return true; } if (g_TRVersion > 1) { return false; } const MUSIC_TRX_ID track = Music_FromGameID(track_id); return track == MX_UNUSED_0 || track == MX_UNUSED_1 || track == MX_UNUSED_2; } static bool M_IsAmbientTrack(const MUSIC_ID track_id) { const GF_LEVEL *const level = GF_GetCurrentLevel(); if (level != nullptr && level->music_track == track_id) { return true; } const GF_AMBIENT_DATA *const ambient_data = &g_GameFlow.ambient_tracks; if (ambient_data == nullptr) { return false; } for (int32_t i = 0; i < ambient_data->count; i++) { if (ambient_data->ids[i] == track_id) { return true; } } return false; } static void M_SyncVolume(const M_MUSIC_STREAM *const stream) { if (stream == nullptr || !stream->active || stream->audio_stream_id < 0) { return; } const float volume = stream->mode == MPM_OVERLAY ? g_Config.audio.music_volume * g_Config.audio.master_volume : m_MusicVolume; Audio_Stream_SetVolume(stream->audio_stream_id, volume); } static void M_SyncVolumes(void) { M_SyncVolume(&m_MainStream); for (int32_t i = 0; i < MUSIC_MAX_OVERLAY_TRACKS; i++) { M_SyncVolume(&m_OverlayStreams[i]); } } static int32_t M_GetFreeOverlaySlot(void) { for (int32_t i = 0; i < MUSIC_MAX_OVERLAY_TRACKS; i++) { if (!m_OverlayStreams[i].active) { return i; } } return -1; } static bool M_PlayOverlayTrack(const MUSIC_ID track_id) { if (m_Backend == nullptr) { LOG_WARNING( "Not playing overlay track %d because no backend is available", track_id); return false; } const int32_t slot = M_GetFreeOverlaySlot(); if (slot < 0) { LOG_WARNING( "Not playing overlay track %d because all %d overlay slots are in " "use", track_id, MUSIC_MAX_OVERLAY_TRACKS); return false; } const int32_t stream_id = m_Backend->play(m_Backend, track_id); if (stream_id < 0) { LOG_ERROR("Failed to create overlay stream for track %d", track_id); return false; } m_OverlayStreams[slot].audio_stream_id = stream_id; m_OverlayStreams[slot].track_id = track_id; m_OverlayStreams[slot].mode = MPM_OVERLAY; m_OverlayStreams[slot].active = true; M_SyncVolume(&m_OverlayStreams[slot]); Audio_Stream_SetIsLooped(stream_id, false); Audio_Stream_SetFinishCallback( stream_id, M_StreamFinished, &m_OverlayStreams[slot]); return true; } static bool M_GetMainTrackState(MUSIC_STREAM_STATE *const state) { if (!m_MainStream.active || state == nullptr) { return false; } state->track_id = MX_INACTIVE; state->mode = MPM_ONCE; state->timestamp = Audio_Stream_GetTimestamp(m_MainStream.audio_stream_id); if (m_TrackCurrent != MX_INACTIVE) { state->track_id = m_TrackCurrent; return true; } if (m_TrackLooped != MX_INACTIVE) { state->track_id = m_TrackLooped; state->mode = MPM_LOOP; return true; } return false; } bool Music_Init(void) { m_Initialised = true; if (m_Backend != nullptr) { m_Backend->shutdown(m_Backend); m_Backend = nullptr; } m_Backend = M_FindBackend(); if (m_Backend == nullptr) { LOG_ERROR("No music backend is available"); goto finish; } LOG_INFO("Chosen music backend: %s", m_Backend->describe(m_Backend)); Music_SetVolume(g_Config.audio.music_volume); finish: m_TrackCurrent = MX_INACTIVE; m_TrackLastPlayed = MX_INACTIVE; m_TrackDelayed = MX_INACTIVE; m_TrackLooped = MX_INACTIVE; m_TrackLastLooped = MX_INACTIVE; M_ResetStreamState(); return Audio_Init(); } void Music_Shutdown(void) { m_Initialised = false; M_StopMainStream(); M_StopOverlayStreams(); M_ResetStreamState(); if (m_Backend != nullptr) { m_Backend->shutdown(m_Backend); m_Backend = nullptr; } Audio_Shutdown(); } bool Music_Play_Direct(const MUSIC_ID track_id, const MUSIC_PLAY_MODE mode) { if (!m_Initialised) { return false; } if (M_IsBrokenTrack(track_id)) { return false; } if (mode == MPM_OVERLAY) { LOG_INFO("Playing overlay track %d", track_id); return M_PlayOverlayTrack(track_id); } if (track_id == m_TrackCurrent) { return true; } if (mode == MPM_NO_REPEAT && track_id == m_TrackLastPlayed) { return true; } const bool is_looped = mode == MPM_LOOP || M_IsAmbientTrack(track_id); if (is_looped && track_id == m_TrackLastLooped) { return true; } if (mode == MPM_DELAY) { m_TrackDelayed = track_id; return true; } if (is_looped && m_TrackCurrent != MX_INACTIVE) { // OG TR3 behaviour: do not interrupt a regular track when the ambient // changes; remember the new ambient and restore it when the track ends. m_TrackDelayed = MX_INACTIVE; m_TrackLooped = track_id; m_TrackLastLooped = track_id; return true; } M_StopMainStream(); if (m_Backend == nullptr) { LOG_WARNING( "Not playing track %d because no backend is available", track_id); goto finish; } LOG_INFO("Playing track %d, mode: %d", track_id, mode); const int32_t stream_id = m_Backend->play(m_Backend, track_id); if (stream_id < 0) { LOG_ERROR("Failed to create music stream for track %d", track_id); goto finish; } m_MainStream.audio_stream_id = stream_id; m_MainStream.track_id = track_id; m_MainStream.mode = is_looped ? MPM_LOOP : MPM_ONCE; m_MainStream.active = true; M_SyncVolume(&m_MainStream); Audio_Stream_SetIsLooped(stream_id, is_looped); Audio_Stream_SetFinishCallback(stream_id, M_StreamFinished, &m_MainStream); finish: m_TrackDelayed = MX_INACTIVE; if (is_looped) { // Reset the regular track outside of M_StreamFinished so that // Music_GetCurrentPlayingTrack returns the looped track; otherwise, the // stopped track could be stored in the savegame despite being inactive. m_TrackCurrent = MX_INACTIVE; m_TrackLooped = track_id; m_TrackLastLooped = track_id; } else { m_TrackCurrent = track_id; m_TrackLastPlayed = track_id; } return true; } bool Music_Play(const MUSIC_TRX_ID track, const MUSIC_PLAY_MODE mode) { return Music_Play_Direct(Music_ToGameID(track), mode); } void Music_Stop(void) { m_TrackCurrent = MX_INACTIVE; m_TrackLastPlayed = MX_INACTIVE; m_TrackDelayed = MX_INACTIVE; m_TrackLooped = MX_INACTIVE; m_TrackLastLooped = MX_INACTIVE; M_StopMainStream(); M_StopOverlayStreams(); M_ResetStreamState(); } void Music_StopTrack_Direct(const MUSIC_ID track) { if (track != m_TrackCurrent || M_IsBrokenTrack(track)) { return; } M_StopMainStream(); m_TrackCurrent = MX_INACTIVE; if (m_TrackLooped >= 0) { Music_Play_Direct(m_TrackLooped, MPM_LOOP); } } void Music_Pause(void) { if (m_MainStream.active && m_MainStream.audio_stream_id >= 0) { Audio_Stream_Pause(m_MainStream.audio_stream_id); } for (int32_t i = 0; i < MUSIC_MAX_OVERLAY_TRACKS; i++) { if (m_OverlayStreams[i].active && m_OverlayStreams[i].audio_stream_id >= 0) { Audio_Stream_Pause(m_OverlayStreams[i].audio_stream_id); } } } void Music_Unpause(void) { if (m_MainStream.active && m_MainStream.audio_stream_id >= 0) { Audio_Stream_Unpause(m_MainStream.audio_stream_id); } for (int32_t i = 0; i < MUSIC_MAX_OVERLAY_TRACKS; i++) { if (m_OverlayStreams[i].active && m_OverlayStreams[i].audio_stream_id >= 0) { Audio_Stream_Unpause(m_OverlayStreams[i].audio_stream_id); } } } double Music_GetTimestamp(void) { if (!m_MainStream.active || m_MainStream.audio_stream_id < 0) { return -1.0; } return Audio_Stream_GetTimestamp(m_MainStream.audio_stream_id); } bool Music_SeekTimestamp(const double timestamp) { if (!m_MainStream.active || m_MainStream.audio_stream_id < 0) { return false; } return Audio_Stream_SeekTimestamp(m_MainStream.audio_stream_id, timestamp); } bool Music_SyncTimestamp(const double timestamp) { if (!m_MainStream.active || m_MainStream.audio_stream_id < 0) { return false; } return Audio_Stream_SyncTimestamp(m_MainStream.audio_stream_id, timestamp); } int32_t Music_GetStreamCount(void) { int32_t count = 0; if (m_MainStream.active) { count++; } for (int32_t i = 0; i < MUSIC_MAX_OVERLAY_TRACKS; i++) { if (m_OverlayStreams[i].active) { count++; } } return count; } bool Music_GetStreamState( const int32_t index, MUSIC_STREAM_STATE *const out_state) { if (index < 0 || out_state == nullptr) { return false; } int32_t stream_index = 0; if (m_MainStream.active) { if (stream_index == index) { return M_GetMainTrackState(out_state); } stream_index++; } for (int32_t i = 0; i < MUSIC_MAX_OVERLAY_TRACKS; i++) { if (!m_OverlayStreams[i].active) { continue; } if (stream_index == index) { out_state->track_id = m_OverlayStreams[i].track_id; out_state->mode = m_OverlayStreams[i].mode; out_state->timestamp = Audio_Stream_GetTimestamp(m_OverlayStreams[i].audio_stream_id); return true; } stream_index++; } return false; } bool Music_SeekTrackTimestamp( const MUSIC_ID track_id, const MUSIC_PLAY_MODE mode, const double timestamp) { if (mode == MPM_OVERLAY) { for (int32_t i = MUSIC_MAX_OVERLAY_TRACKS - 1; i >= 0; i--) { if (!m_OverlayStreams[i].active || m_OverlayStreams[i].track_id != track_id) { continue; } return Audio_Stream_SeekTimestamp( m_OverlayStreams[i].audio_stream_id, timestamp); } return false; } MUSIC_STREAM_STATE state = {}; if (!M_GetMainTrackState(&state)) { return false; } if (state.track_id != track_id || state.mode != mode) { return false; } return Audio_Stream_SeekTimestamp(m_MainStream.audio_stream_id, timestamp); } MUSIC_ID Music_GetDelayedTrack(void) { return m_TrackDelayed; } MUSIC_ID Music_GetCurrentPlayingTrack(void) { return m_TrackCurrent == MX_INACTIVE ? m_TrackLooped : m_TrackCurrent; } MUSIC_ID Music_GetCurrentLoopedTrack(void) { return m_TrackLooped; } void Music_SetVolume(float volume) { volume *= g_Config.audio.master_volume; if (volume != m_MusicVolume) { m_MusicVolume = volume; M_SyncVolumes(); } } void Music_ResetTrackFlags(void) { for (int32_t i = 0; i < MAX_MUSIC_TRACKS; i++) { m_MusicTrackFlags[i] = 0; } } uint16_t Music_GetTrackFlags(const MUSIC_ID track_id) { return m_MusicTrackFlags[track_id]; } void Music_SetTrackFlags(const MUSIC_ID track_id, const uint16_t flags) { m_MusicTrackFlags[track_id] = flags; } ================================================ FILE: src/trx/game/music/common.h ================================================ #pragma once #include #include #include #define MUSIC_MAX_OVERLAY_TRACKS 3 typedef struct { MUSIC_ID track_id; MUSIC_PLAY_MODE mode; double timestamp; } MUSIC_STREAM_STATE; bool Music_Init(void); void Music_Shutdown(void); // Stops playing current track and plays a single track. // // MPM_ONCE: // Plays the track once. Once playback is done, if there is an active looped // track, the playback resumes from the start of the looped track. // MPM_LOOP: // Activates looped playback for the chosen track. // MPM_NO_REPEAT: // A track with this play mode will not trigger in succession. // MPM_DELAY: // A track does not get played and instead is only marked for later playback. // The track to play is available with Music_GetDelayedTrack(). // MPM_OVERLAY: // Plays a non-looping track without interrupting active background music. bool Music_Play_Direct(MUSIC_ID track, MUSIC_PLAY_MODE mode); // Stops the provided single track and restarts the looped track if applicable. void Music_StopTrack_Direct(MUSIC_ID track); // Play a music track with a semantical ID that will get mapped to a specific // music track slot depending on the game. bool Music_Play(MUSIC_TRX_ID track, MUSIC_PLAY_MODE mode); // Stops all music streams, including looped, active, and overlay tracks. void Music_Stop(void); // Pauses the music. void Music_Pause(void); // Unpauses the music. void Music_Unpause(void); // Get the current timestamp of the current stream in seconds. double Music_GetTimestamp(void); // Seek to timestamp of current stream. bool Music_SeekTimestamp(double timestamp); // Seeks to the given timestamp if the drift is too big. bool Music_SyncTimestamp(double timestamp); // Returns the number of currently active serializable streams. int32_t Music_GetStreamCount(void); // Returns stream state by active index [0..Music_GetStreamCount()). bool Music_GetStreamState(int32_t index, MUSIC_STREAM_STATE *state); // Seeks timestamp for the active stream that matches track and mode. bool Music_SeekTrackTimestamp( MUSIC_ID track, MUSIC_PLAY_MODE mode, double timestamp); // Returns the delayed track. Ignores looped tracks. MUSIC_ID Music_GetDelayedTrack(void); // Returns the currently playing track. Includes looped music. MUSIC_ID Music_GetCurrentPlayingTrack(void); // Returns the looped track. MUSIC_ID Music_GetCurrentLoopedTrack(void); // Sets the game volume. void Music_SetVolume(float volume); // Resets all track trigger mask flags. void Music_ResetTrackFlags(void); // Returns trigger mask flags for the given track. uint16_t Music_GetTrackFlags(MUSIC_ID track_id); // Sets the trigger mask flags for the given track. void Music_SetTrackFlags(MUSIC_ID track_id, uint16_t flags); ================================================ FILE: src/trx/game/music/const.h ================================================ #pragma once #define MAX_MUSIC_TRACKS 1024 ================================================ FILE: src/trx/game/music/enum.h ================================================ #pragma once typedef enum { MPM_ONCE, MPM_LOOP, MPM_DELAY, MPM_NO_REPEAT, MPM_OVERLAY, } MUSIC_PLAY_MODE; ================================================ FILE: src/trx/game/music/ids.c ================================================ #include #include MUSIC_ID Music_ToGameID(const MUSIC_TRX_ID music_track) { int32_t out; if (Catalog_EnumToGameID(CATALOG_MUSIC, music_track, &out)) { return out; } return MX_INACTIVE; } MUSIC_TRX_ID Music_FromGameID(const MUSIC_ID track_id) { CATALOG_ID out; if (Catalog_GameIDToEnum(CATALOG_MUSIC, track_id, &out)) { return out; } return MX_TRX_INVALID; } ================================================ FILE: src/trx/game/music/ids.h ================================================ #pragma once #include typedef enum { MX_INACTIVE = -1, } MUSIC_ID; typedef enum { MX_TRX_INVALID = -1, #define X_CATALOG_ID(enum_value) enum_value, #include #undef X_CATALOG_ID MX_NUMBER_OF, } MUSIC_TRX_ID; MUSIC_ID Music_ToGameID(MUSIC_TRX_ID music_track); MUSIC_TRX_ID Music_FromGameID(MUSIC_ID track_id); ================================================ FILE: src/trx/game/music/types.h ================================================ #pragma once #include typedef struct MUSIC_BACKEND { bool (*init)(struct MUSIC_BACKEND *backend); const char *(*describe)(const struct MUSIC_BACKEND *backend); int32_t (*play)(const struct MUSIC_BACKEND *backend, int32_t track_id); void (*shutdown)(struct MUSIC_BACKEND *backend); void *data; } MUSIC_BACKEND; ================================================ FILE: src/trx/game/music.h ================================================ #pragma once #include #include #include #include ================================================ FILE: src/trx/game/objects/col.c ================================================ #include #include void Object_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); if (!Lara_TestBoundsCollide(item, coll->radius)) { return; } if (!Collide_TestCollision(item, lara_item)) { return; } if (coll->enable_baddie_push) { Lara_Col_ItemPush(item, coll, false, true); } } void Object_Collision_Trap( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); if (item->status == IS_ACTIVE) { if (Lara_TestBoundsCollide(item, coll->radius)) { Collide_TestCollision(item, lara_item); } } else if (item->status != IS_INVISIBLE) { Object_Collision(item_num, lara_item, coll); } } ================================================ FILE: src/trx/game/objects/col.h ================================================ #include void Object_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); void Object_Collision_Trap(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); ================================================ FILE: src/trx/game/objects/common.c ================================================ #include #include #include #include #include #include #include #include #include #include #include static OBJECT m_Objects[O_NUMBER_OF] = {}; static STATIC_OBJECT_3D *m_StaticObjects3D = nullptr; static STATIC_OBJECT_2D *m_StaticObjects2D = nullptr; static int32_t m_StaticObjects3DCount = 0; static int32_t m_StaticObjects2DCount = 0; static OBJECT_MESH **m_MeshPointers = nullptr; static int32_t m_MeshCount = 0; void Object_Reset(void) { for (int32_t i = O_FIRST; i < O_NUMBER_OF; i++) { m_Objects[i].loaded = false; } m_StaticObjects3D = nullptr; m_StaticObjects2D = nullptr; m_StaticObjects3DCount = 0; m_StaticObjects2DCount = 0; m_MeshPointers = nullptr; m_MeshCount = 0; } void Object_InitialiseStaticObjects3D(const int32_t count) { ASSERT(count >= 0); m_StaticObjects3DCount = count; m_StaticObjects3D = GameBuf_Alloc(sizeof(STATIC_OBJECT_3D) * count, GBUF_STATIC_OBJECTS_3D); } void Object_InitialiseStaticObjects2D(const int32_t count) { ASSERT(count >= 0); m_StaticObjects2DCount = count; m_StaticObjects2D = GameBuf_Alloc(sizeof(STATIC_OBJECT_2D) * count, GBUF_STATIC_OBJECTS_2D); } int32_t Object_GetStaticObjects3DCount(void) { return m_StaticObjects3DCount; } int32_t Object_GetStaticObjects2DCount(void) { return m_StaticObjects2DCount; } OBJECT *Object_TryGet(const OBJECT_ID object_id) { if (object_id < O_FIRST || object_id >= O_NUMBER_OF) { return nullptr; } return &m_Objects[object_id]; } OBJECT *Object_Get(const OBJECT_ID object_id) { ASSERT(object_id >= O_FIRST && object_id < O_NUMBER_OF); return &m_Objects[object_id]; } OBJECT *Object_GetByGameID(const int32_t game_id) { OBJECT_ID object_id = Object_FromGameID(game_id); if (object_id == NO_OBJECT) { return nullptr; } return &m_Objects[object_id]; } STATIC_OBJECT_3D *Object_Get3DStatic(const int32_t static_id) { return &m_StaticObjects3D[static_id]; } bool Object_IsValidStatid3D(const int32_t static_id) { return static_id >= 0 && static_id < m_StaticObjects3DCount; } STATIC_OBJECT_2D *Object_Get2DStatic(const int32_t static_id) { if (m_StaticObjects2D == nullptr) { return nullptr; } if (static_id < 0 || static_id >= m_StaticObjects2DCount) { return nullptr; } return &m_StaticObjects2D[static_id]; } OBJECT_ID Object_FromGameID(const int32_t game_id) { int32_t out; if (Catalog_GameIDToEnum(CATALOG_OBJECTS, game_id, &out)) { return out; } return NO_OBJECT; } int32_t Object_ToGameID(const OBJECT_ID object_id) { int32_t out; if (Catalog_EnumToGameID(CATALOG_OBJECTS, object_id, &out)) { return out; } return -1; } bool Object_IsType(const OBJECT_ID object_id, const OBJECT_ID *test_arr) { for (int32_t i = 0; test_arr[i] != NO_OBJECT; i++) { if (test_arr[i] == object_id) { return true; } } return false; } OBJECT_ID Object_GetCognate(OBJECT_ID key_id, const GAME_OBJECT_PAIR *test_map) { const GAME_OBJECT_PAIR *pair = &test_map[0]; while (pair->key_id != NO_OBJECT) { if (pair->key_id == key_id) { return pair->value_id; } pair++; } return NO_OBJECT; } OBJECT_ID Object_GetCognateInverse( OBJECT_ID value_id, const GAME_OBJECT_PAIR *test_map) { const GAME_OBJECT_PAIR *pair = &test_map[0]; while (pair->key_id != NO_OBJECT) { if (pair->value_id == value_id) { return pair->key_id; } pair++; } return NO_OBJECT; } void Object_InitialiseMeshes(const int32_t mesh_count) { m_MeshPointers = GameBuf_Alloc(sizeof(OBJECT_MESH *) * mesh_count, GBUF_MESH_POINTERS); m_MeshCount = 0; } void Object_StoreMesh(OBJECT_MESH *const mesh) { m_MeshPointers[m_MeshCount] = mesh; m_MeshCount++; } OBJECT_MESH *Object_GetMesh(const int32_t index) { return m_MeshPointers[index]; } int32_t Object_GetItemMeshIndex(const ITEM *const item, const int32_t mesh_idx) { const OBJECT *const obj = Object_Get(item->object_id); const int32_t fallback = obj->mesh_idx + mesh_idx; if (obj->get_mesh_index_func == nullptr) { return fallback; } const int32_t resolved = obj->get_mesh_index_func(item, mesh_idx); if (resolved < 0) { return fallback; } return resolved; } int32_t Object_GetMeshIndex(const OBJECT_MESH *const mesh) { for (int32_t i = 0; i < m_MeshCount; i++) { if (mesh == m_MeshPointers[i]) { return i; } } return -1; } int32_t Object_GetMeshCount(void) { return m_MeshCount; } OBJECT_MESH *Object_FindMesh(const int32_t data_offset) { for (int32_t i = 0; i < m_MeshCount; i++) { OBJECT_MESH *const mesh = m_MeshPointers[i]; if (Object_GetMeshOffset(mesh) == data_offset) { return mesh; } } return nullptr; } int32_t Object_GetMeshOffset(const OBJECT_MESH *const mesh) { return (int32_t)(intptr_t)mesh->priv; } void Object_SetMeshOffset(OBJECT_MESH *const mesh, const int32_t data_offset) { mesh->priv = (void *)(intptr_t)data_offset; } void Object_SwapMesh( const OBJECT_ID object1_id, const OBJECT_ID object2_id, const int32_t mesh_num) { Object_SwapMeshEx(object1_id, object2_id, mesh_num, mesh_num); } void Object_SwapAllMeshes( const OBJECT_ID object1_id, const OBJECT_ID object2_id) { const OBJECT *const obj1 = Object_Get(object1_id); const OBJECT *const obj2 = Object_Get(object2_id); if (!obj1->loaded || !obj2->loaded) { return; } const int32_t mesh_count = MIN(obj1->mesh_count, obj2->mesh_count); for (int32_t i = 0; i < mesh_count; i++) { Object_SwapMeshEx(object1_id, object2_id, i, i); } } void Object_SwapMeshEx( const OBJECT_ID object1_id, const OBJECT_ID object2_id, const int32_t mesh_num1, const int32_t mesh_num2) { const OBJECT *const obj1 = Object_Get(object1_id); const OBJECT *const obj2 = Object_Get(object2_id); if (!obj1->loaded || !obj2->loaded) { return; } const int32_t mesh_idx1 = obj1->mesh_idx + mesh_num1; const int32_t mesh_idx2 = obj2->mesh_idx + mesh_num2; SWAP(m_MeshPointers[mesh_idx1], m_MeshPointers[mesh_idx2]); Output_DispatchObjectMeshSwap(mesh_idx1, mesh_idx2); } ANIM *Object_GetAnim(const OBJECT *const obj, const int32_t anim_idx) { return Anim_GetAnim(obj->anim_idx + anim_idx); } ANIM_BONE *Object_GetBone(const OBJECT *const obj, const int32_t bone_idx) { return Anim_GetBone(obj->bone_idx + bone_idx); } ANIM_BONE *Object_TryGetBone(const OBJECT *const obj, const int32_t bone_idx) { return Anim_TryGetBone(obj->bone_idx + bone_idx); } OBJECT_ID Object_FindReceptacleKey(const OBJECT_ID receptacle_obj_id) { return Object_GetCognateInverse( receptacle_obj_id, g_KeyItemToReceptacleMap); } int16_t Object_FindReceptacle(const OBJECT_ID object_id) { // Iterate through all matching receptacles const GAME_OBJECT_PAIR *const map = g_KeyItemToReceptacleMap; for (int32_t i = 0; map[i].key_id != NO_OBJECT; i++) { if (map[i].key_id != object_id) { continue; } // Iterate through all level items that match this receptacle const OBJECT_ID receptacle_to_check = map[i].value_id; for (int16_t item_num = 0; item_num < Item_GetLevelCount(); item_num++) { const ITEM *const item = Item_Get(item_num); if (item->object_id != receptacle_to_check) { continue; } const OBJECT *const obj = Object_Get(item->object_id); if (obj->is_usable_func != nullptr && !obj->is_usable_func(item_num)) { continue; } // If Lara is standing near one, that's our keyhole if (Lara_TestPosition(item, obj->bounds_func())) { return item_num; } } } return NO_ITEM; } bool Object_CanInterpolate( const ITEM *const item, const int32_t frame_a, const int32_t frame_b) { const OBJECT *const obj = Object_Get(item->object_id); return item->enable_interpolation && obj->enable_interpolation && item->prev_frame_num != item->frame_num; } void Object_SetReflective(const OBJECT_ID obj_id, const bool enabled) { const OBJECT *const obj = Object_Get(obj_id); for (int32_t i = 0; i < obj->mesh_count; i++) { Object_SetMeshReflective(obj_id, i, enabled); } } void Object_SetMeshReflective( const OBJECT_ID obj_id, const int32_t mesh_idx, const bool enabled) { const OBJECT *const obj = Object_Get(obj_id); if (!obj->loaded) { return; } Object_SetMeshReflectiveEx(obj->mesh_idx + mesh_idx, enabled); } void Object_SetMeshReflectiveEx(const int32_t abs_mesh_idx, const bool enabled) { OBJECT_MESH *const mesh = Object_GetMesh(abs_mesh_idx); mesh->enable_reflections = enabled; for (int32_t i = 0; i < mesh->all_faces.count; i++) { mesh->all_faces.data[i].enable_reflections = enabled; } Output_DispatchObjectMeshUpdate(abs_mesh_idx); } ================================================ FILE: src/trx/game/objects/common.h ================================================ #pragma once #include #include #include #include #include #include #include void Object_Reset(void); // Retrieve an object by its TRX internal index. Trying to retrieve an invalid // object is a fatal error. OBJECT *Object_Get(OBJECT_ID object_id); // Retrieve an object by its TRX internal index. Trying to retrieve an invalid // object returns nullptr. OBJECT *Object_TryGet(OBJECT_ID object_id); // Retrieve an object by its game ID. Returns nullptr if not found. OBJECT *Object_GetByGameID(int32_t game_id); // Convert a game ID to OBJECT_ID. OBJECT_ID Object_FromGameID(int32_t game_id); // Convert a OBJECT_ID to a game ID (opposite of Object_FromGameID). int32_t Object_ToGameID(OBJECT_ID object_id); // Other functions ============================================================ void Object_InitialiseStaticObjects3D(int32_t count); void Object_InitialiseStaticObjects2D(int32_t count); int32_t Object_GetStaticObjects3DCount(void); int32_t Object_GetStaticObjects2DCount(void); bool Object_IsValidStatid3D(int32_t static_id); STATIC_OBJECT_3D *Object_Get3DStatic(int32_t static_id); STATIC_OBJECT_2D *Object_Get2DStatic(int32_t static_id); bool Object_IsType(OBJECT_ID object_id, const OBJECT_ID *test_arr); OBJECT_ID Object_GetCognate(OBJECT_ID key_id, const GAME_OBJECT_PAIR *test_map); OBJECT_ID Object_GetCognateInverse( OBJECT_ID value_id, const GAME_OBJECT_PAIR *test_map); void Object_InitialiseMeshes(int32_t mesh_count); void Object_StoreMesh(OBJECT_MESH *mesh); int32_t Object_GetMeshCount(void); OBJECT_MESH *Object_FindMesh(int32_t data_offset); int32_t Object_GetMeshIndex(const OBJECT_MESH *mesh); int32_t Object_GetMeshOffset(const OBJECT_MESH *mesh); void Object_SetMeshOffset(OBJECT_MESH *mesh, int32_t data_offset); OBJECT_MESH *Object_GetMesh(int32_t index); int32_t Object_GetItemMeshIndex(const ITEM *item, int32_t mesh_idx); void Object_SwapMesh( OBJECT_ID object1_id, OBJECT_ID object2_id, int32_t mesh_num); void Object_SwapAllMeshes(OBJECT_ID object1_id, OBJECT_ID object2_id); void Object_SwapMeshEx( OBJECT_ID object1_id, OBJECT_ID object2_id, int32_t mesh_num1, int32_t mesh_num2); ANIM *Object_GetAnim(const OBJECT *obj, int32_t anim_idx); ANIM_BONE *Object_GetBone(const OBJECT *obj, int32_t bone_idx); ANIM_BONE *Object_TryGetBone(const OBJECT *obj, int32_t bone_idx); // Given a key or puzzle object, find a matching receptacle item number to // establish the interaction target. Takes into account current Lara's // position. int16_t Object_FindReceptacle(OBJECT_ID obj_id); // Given a receptacle object ID, find a matching key/puzzle object ID. OBJECT_ID Object_FindReceptacleKey(const OBJECT_ID receptacle_obj_id); void Object_SetReflective(OBJECT_ID obj_id, bool enabled); void Object_SetMeshReflective(OBJECT_ID obj_id, int32_t mesh_idx, bool enabled); void Object_SetMeshReflectiveEx(int32_t abs_mesh_idx, bool enabled); bool Object_CanInterpolate(const ITEM *item, int32_t frame_a, int32_t frame_b); #define REGISTER_OBJECT(object_id, setup_func_) \ __attribute__((constructor)) static void M_RegisterObject##object_id(void) \ { \ Object_Get(object_id)->setup_func = setup_func_; \ } ================================================ FILE: src/trx/game/objects/creatures/ape.c ================================================ #include #include #include #include #include #include #define APE_ATTACK_DAMAGE 200 #define APE_TOUCH 0xFF00 #define APE_DIE_ANIM 7 #define APE_RUN_TURN (DEG_1 * 5) // = 910 #define APE_DISPLAY_ANGLE (DEG_1 * 45) // = 8190 #define APE_ATTACK_RANGE SQUARE(430) // = 184900 #define APE_PANIC_RANGE SQUARE(WALL_L * 2) // = 4194304 #define APE_JUMP_CHANCE 160 #define APE_WARN1_CHANCE (APE_JUMP_CHANCE + 160) // = 320 #define APE_WARN2_CHANCE (APE_WARN1_CHANCE + 160) // = 480 #define APE_RUN_LEFT_CHANCE (APE_WARN2_CHANCE + 272) // = 752 #define APE_ATTACK_FLAG 1 #define APE_VAULT_ANIM 19 #define APE_TURN_L_FLAG 2 #define APE_TURN_R_FLAG 4 #define APE_SHIFT 75 #define APE_HITPOINTS 22 #define APE_RADIUS (WALL_L / 3) // = 341 #define APE_SMARTNESS 0x7FFF typedef enum { APE_STATE_EMPTY = 0, APE_STATE_STOP = 1, APE_STATE_WALK = 2, APE_STATE_RUN = 3, APE_STATE_ATTACK = 4, APE_STATE_DEATH = 5, APE_STATE_WARNING_1 = 6, APE_STATE_WARNING_2 = 7, APE_STATE_RUN_LEFT = 8, APE_STATE_RUN_RIGHT = 9, APE_STATE_JUMP = 10, APE_STATE_VAULT = 11, } APE_STATE; static BITE m_ApeBite = { .pos = { 0, -19, 75 }, .mesh_num = 15 }; static bool M_Vault(int16_t item_num, int16_t angle) { ITEM *const item = Item_Get(item_num); CREATURE *const ape = item->creature_data; int32_t x = item->pos.x >> WALL_SHIFT; int32_t y = item->pos.y; int32_t z = item->pos.z >> WALL_SHIFT; int16_t room_num = item->room_num; if (ape->flags & APE_TURN_L_FLAG) { item->rot.y -= DEG_90; ape->flags &= ~APE_TURN_L_FLAG; } else if (ape->flags & APE_TURN_R_FLAG) { item->rot.y += DEG_90; ape->flags &= ~APE_TURN_R_FLAG; } Creature_Animate(item_num, angle, 0); if (item->pos.y > y - STEP_L * 3 / 2) { return false; } int32_t x_floor = item->pos.x >> WALL_SHIFT; int32_t z_floor = item->pos.z >> WALL_SHIFT; if (z == z_floor) { if (x == x_floor) { return false; } if (x >= x_floor) { item->rot.y = -DEG_90; item->pos.x = (x << WALL_SHIFT) + APE_SHIFT; } else { item->rot.y = DEG_90; item->pos.x = (x_floor << WALL_SHIFT) - APE_SHIFT; } } else if (x == x_floor) { if (z < z_floor) { item->rot.y = 0; item->pos.z = (z_floor << WALL_SHIFT) - APE_SHIFT; } else { item->rot.y = -DEG_180; item->pos.z = (z << WALL_SHIFT) + APE_SHIFT; } } item->floor = y; item->pos.y = y; Item_UpdateRoom(item_num, room_num); return true; } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const ape = item->creature_data; int16_t head = 0; int16_t angle = 0; if (item->hit_points <= 0) { if (item->current_anim_state != APE_STATE_DEATH) { item->current_anim_state = APE_STATE_DEATH; Item_SwitchToAnim( item, APE_DIE_ANIM + (int16_t)(Random_GetControl() / 0x4000), 0); } } else { AI_INFO info; Creature_AIInfo(item, &info); if (info.ahead) { head = info.angle; } Creature_Mood(item, &info, false); angle = Creature_Turn(item, ape->maximum_turn); if (item->hit_status || info.distance < APE_PANIC_RANGE) { ape->flags |= APE_ATTACK_FLAG; } switch (item->current_anim_state) { case APE_STATE_STOP: if (ape->flags & APE_TURN_L_FLAG) { item->rot.y -= DEG_90; ape->flags &= ~APE_TURN_L_FLAG; } else if (ape->flags & APE_TURN_R_FLAG) { item->rot.y += DEG_90; ape->flags &= ~APE_TURN_R_FLAG; } if (item->required_anim_state) { item->goal_anim_state = item->required_anim_state; } else if (info.bite && info.distance < APE_ATTACK_RANGE) { item->goal_anim_state = APE_STATE_ATTACK; } else if ( !(ape->flags & APE_ATTACK_FLAG) && info.zone_num == info.enemy_zone_num && info.ahead) { int16_t random = Random_GetControl() >> 5; if (random < APE_JUMP_CHANCE) { item->goal_anim_state = APE_STATE_JUMP; } else if (random < APE_WARN1_CHANCE) { item->goal_anim_state = APE_STATE_WARNING_1; } else if (random < APE_WARN2_CHANCE) { item->goal_anim_state = APE_STATE_WARNING_2; } else if (random < APE_RUN_LEFT_CHANCE) { item->goal_anim_state = APE_STATE_RUN_LEFT; ape->maximum_turn = 0; } else { item->goal_anim_state = APE_STATE_RUN_RIGHT; ape->maximum_turn = 0; } } else { item->goal_anim_state = APE_STATE_RUN; } break; case APE_STATE_RUN: ape->maximum_turn = APE_RUN_TURN; if (!ape->flags && info.angle > -APE_DISPLAY_ANGLE && info.angle < APE_DISPLAY_ANGLE) { item->goal_anim_state = APE_STATE_STOP; } else if (info.ahead && (item->touch_bits & APE_TOUCH)) { item->required_anim_state = APE_STATE_ATTACK; item->goal_anim_state = APE_STATE_STOP; } else if (ape->mood != MOOD_ESCAPE) { int16_t random = Random_GetControl(); if (random < APE_JUMP_CHANCE) { item->required_anim_state = APE_STATE_JUMP; item->goal_anim_state = APE_STATE_STOP; } else if (random < APE_WARN1_CHANCE) { item->required_anim_state = APE_STATE_WARNING_1; item->goal_anim_state = APE_STATE_STOP; } else if (random < APE_WARN2_CHANCE) { item->required_anim_state = APE_STATE_WARNING_2; item->goal_anim_state = APE_STATE_STOP; } } break; case APE_STATE_RUN_LEFT: if (!(ape->flags & APE_TURN_R_FLAG)) { item->rot.y -= DEG_90; ape->flags |= APE_TURN_R_FLAG; } item->goal_anim_state = APE_STATE_STOP; break; case APE_STATE_RUN_RIGHT: if (!(ape->flags & APE_TURN_L_FLAG)) { item->rot.y += DEG_90; ape->flags |= APE_TURN_L_FLAG; } item->goal_anim_state = APE_STATE_STOP; break; case APE_STATE_ATTACK: if (!item->required_anim_state && (item->touch_bits & APE_TOUCH)) { Creature_Effect(item, &m_ApeBite, Spawn_Blood); Lara_TakeDamage(APE_ATTACK_DAMAGE, true); item->required_anim_state = APE_STATE_STOP; } break; } } Creature_Head(item, head); if (item->current_anim_state == APE_STATE_VAULT) { Creature_Animate(item_num, angle, 0); } else if (M_Vault(item_num, angle)) { ape->maximum_turn = 0; item->current_anim_state = APE_STATE_VAULT; Item_SwitchToAnim(item, APE_VAULT_ANIM, 0); } } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = Creature_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->shadow_size = UNIT_SHADOW / 2; obj->hit_points = APE_HITPOINTS; obj->pivot_length = 250; obj->radius = APE_RADIUS; obj->smartness = APE_SMARTNESS; obj->lot_setup = LOT_Setup(LOT_SETUP_JUMPER); obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_anim = true; obj->save_flags = true; Object_GetBone(obj, 13)->rot.y = true; } REGISTER_OBJECT(O_APE, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/atlantean.c ================================================ #include #include #include #include #include #include #include #include // clang-format off #define M_CHARGE_DAMAGE 100 #define M_LUNGE_DAMAGE 150 #define M_PUNCH_DAMAGE 200 #define M_PART_DAMAGE 100 #define M_WALK_TURN (DEG_1 * 2) // = 364 #define M_RUN_TURN (DEG_1 * 6) // = 1092 #define M_POSE_CHANCE 80 #define M_UNPOSE_CHANCE 256 #define M_WALK_RANGE SQUARE(WALL_L * 9 / 2) // = 21233664 #define M_ATTACK_1_RANGE SQUARE(600) // = 360000 #define M_ATTACK_2_RANGE SQUARE(WALL_L * 5 / 2) // = 6553600 #define M_ATTACK_3_RANGE SQUARE(300) // = 90000 #define M_ATTACK_RANGE SQUARE(WALL_L * 15 / 4) // = 14745600 #define M_TOUCH_BITS 0b00000110'01111000 // = 0x678 #define M_HITPOINTS 50 #define M_RADIUS (WALL_L / 3) // = 341 #define M_DEFAULT_SMARTNESS 0x7FFF #define M_SHOOTER_SMARTNESS 0x2000 // clang-format on typedef enum { M_STATE_EMPTY, M_STATE_STOP, M_STATE_WALK, M_STATE_RUN, M_STATE_ATTACK_1, M_STATE_DEATH, M_STATE_POSE, M_STATE_ATTACK_2, M_STATE_ATTACK_3, M_STATE_AIM_1, M_STATE_AIM_2, M_STATE_SHOOT, M_STATE_MUMMY, M_STATE_FLY, } M_STATE; typedef enum { // clang-format off M_FLAG_BULLET_1 = 1 << 0, M_FLAG_BULLET_2 = 1 << 1, M_FLAG_FLY = 1 << 2, M_FLAG_TWIST = 1 << 3, // clang-format on } M_FLAG; static bool m_EnableExplosions = true; static const BITE m_Bite = { .pos = { -27, 98, 0 }, .mesh_num = 10 }; static const BITE m_Rocket = { .pos = { 51, 213, 0 }, .mesh_num = 14 }; static const BITE m_Shard = { .pos = { -35, 269, 0 }, .mesh_num = 9 }; static void M_InitialiseGround(const int16_t item_num) { Creature_Initialise(item_num); Item_Get(item_num)->mesh_bits = 0xFFE07FFF; } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t head = 0; int16_t angle = 0; if (item->hit_points <= 0) { Item_Explode( item_num, -1, m_EnableExplosions ? M_PART_DAMAGE : -M_PART_DAMAGE); Sound_Effect(SFX_ATLANTEAN_DEATH, &item->pos, SPM_NORMAL); LOT_DisableBaddieAI(item_num); Item_Kill(item_num); item->status = IS_DEACTIVATED; Carrier_TestItemDrops(item_num); return; } creature->lot.setup.step = STEP_L; creature->lot.setup.drop = -STEP_L; creature->lot.setup.fly = 0; AI_INFO info; Creature_AIInfo(item, &info); bool shoot_1 = false; bool shoot_2 = false; if (item->object_id != O_ATLANTEAN_GROUND && Creature_CanTargetEnemy(item, &info) && (info.zone_num != info.enemy_zone_num || info.distance > M_ATTACK_RANGE)) { if (info.angle > 0 && info.angle < DEG_45) { shoot_1 = true; } else if (info.angle < 0 && info.angle > -DEG_45) { shoot_2 = true; } } if (item->object_id == O_ATLANTEAN_WINGED) { if (item->current_anim_state == M_STATE_FLY) { if ((creature->flags & M_FLAG_FLY) != 0 && creature->mood != MOOD_ESCAPE && info.zone_num == info.enemy_zone_num) { creature->flags &= ~M_FLAG_FLY; } if ((creature->flags & M_FLAG_FLY) == 0) { Creature_Mood(item, &info, true); } creature->lot.setup.step = WALL_L * 30; creature->lot.setup.drop = -WALL_L * 30; creature->lot.setup.fly = STEP_L / 8; Creature_AIInfo(item, &info); } else if ( (info.zone_num != info.enemy_zone_num && !shoot_1 && !shoot_2 && (!info.ahead || creature->mood == MOOD_BORED)) || creature->mood == MOOD_ESCAPE) { creature->flags |= M_FLAG_FLY; } } if (info.ahead) { head = info.angle; } if (item->current_anim_state != M_STATE_FLY) { Creature_Mood(item, &info, false); } else if ((creature->flags & M_FLAG_FLY) != 0) { Creature_Mood(item, &info, true); } angle = Creature_Turn(item, creature->maximum_turn); switch (item->current_anim_state) { case M_STATE_MUMMY: item->goal_anim_state = M_STATE_STOP; break; case M_STATE_STOP: creature->flags &= ~(M_FLAG_BULLET_1 | M_FLAG_BULLET_2 | M_FLAG_TWIST); if ((creature->flags & M_FLAG_FLY) != 0) { item->goal_anim_state = M_STATE_FLY; } else if ((item->touch_bits & M_TOUCH_BITS) != 0) { item->goal_anim_state = M_STATE_ATTACK_3; } else if (info.bite && info.distance < M_ATTACK_3_RANGE) { item->goal_anim_state = M_STATE_ATTACK_3; } else if (info.bite && info.distance < M_ATTACK_1_RANGE) { item->goal_anim_state = M_STATE_ATTACK_1; } else if (shoot_1) { item->goal_anim_state = M_STATE_AIM_1; } else if (shoot_2) { item->goal_anim_state = M_STATE_AIM_2; } else if ( creature->mood == MOOD_BORED || (creature->mood == MOOD_STALK && info.distance < M_WALK_RANGE)) { item->goal_anim_state = M_STATE_POSE; } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_POSE: head = 0; if (shoot_1 || shoot_2 || (creature->flags & M_FLAG_FLY) != 0) { item->goal_anim_state = M_STATE_STOP; } else if (creature->mood == MOOD_STALK) { if (info.distance < M_WALK_RANGE) { if (info.zone_num == info.enemy_zone_num || Random_GetControl() < M_UNPOSE_CHANCE) { item->goal_anim_state = M_STATE_WALK; } } else { item->goal_anim_state = M_STATE_STOP; } } else if ( creature->mood == MOOD_BORED && Random_GetControl() < M_UNPOSE_CHANCE) { item->goal_anim_state = M_STATE_WALK; } else if ( creature->mood == MOOD_ATTACK || creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_WALK: creature->maximum_turn = M_WALK_TURN; if (shoot_1 || shoot_2 || (creature->flags & M_FLAG_FLY) != 0) { item->goal_anim_state = M_STATE_STOP; } else if ( creature->mood == MOOD_ATTACK || creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_STOP; } else if ( creature->mood == MOOD_BORED || (creature->mood == MOOD_STALK && info.zone_num != info.enemy_zone_num)) { if (Random_GetControl() < M_POSE_CHANCE) { item->goal_anim_state = M_STATE_POSE; } } else if ( creature->mood == MOOD_STALK && info.distance > M_WALK_RANGE) { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_RUN: creature->maximum_turn = M_RUN_TURN; if ((creature->flags & M_FLAG_FLY) != 0) { item->goal_anim_state = M_STATE_STOP; } else if ((item->touch_bits & M_TOUCH_BITS) != 0) { item->goal_anim_state = M_STATE_STOP; } else if (info.bite && info.distance < M_ATTACK_1_RANGE) { item->goal_anim_state = M_STATE_STOP; } else if (info.ahead && info.distance < M_ATTACK_2_RANGE) { item->goal_anim_state = M_STATE_ATTACK_2; } else if (shoot_1 || shoot_2) { item->goal_anim_state = M_STATE_STOP; } else if ( creature->mood == MOOD_BORED || (creature->mood == MOOD_STALK && info.distance < M_WALK_RANGE)) { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_ATTACK_1: if (item->required_anim_state == M_STATE_EMPTY && (item->touch_bits & M_TOUCH_BITS) != 0) { Creature_Effect(item, &m_Bite, Spawn_Blood); Lara_TakeDamage(M_LUNGE_DAMAGE, true); item->required_anim_state = M_STATE_STOP; } break; case M_STATE_ATTACK_2: if (item->required_anim_state == M_STATE_EMPTY && (item->touch_bits & M_TOUCH_BITS) != 0) { Creature_Effect(item, &m_Bite, Spawn_Blood); Lara_TakeDamage(M_CHARGE_DAMAGE, true); item->required_anim_state = M_STATE_RUN; } break; case M_STATE_ATTACK_3: if (item->required_anim_state == M_STATE_EMPTY && (item->touch_bits & M_TOUCH_BITS) != 0) { Creature_Effect(item, &m_Bite, Spawn_Blood); Lara_TakeDamage(M_PUNCH_DAMAGE, true); item->required_anim_state = M_STATE_STOP; } break; case M_STATE_AIM_1: creature->flags |= M_FLAG_TWIST; creature->flags |= M_FLAG_BULLET_1; if (shoot_1) { item->goal_anim_state = M_STATE_SHOOT; } else { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_AIM_2: creature->flags |= M_FLAG_BULLET_2; if (shoot_2) { item->goal_anim_state = M_STATE_SHOOT; } else { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_SHOOT: if ((creature->flags & M_FLAG_BULLET_1) != 0) { creature->flags &= ~M_FLAG_BULLET_1; Creature_Effect(item, &m_Shard, Spawn_AtlanteanShard); } else if ((creature->flags & M_FLAG_BULLET_2) != 0) { creature->flags &= ~M_FLAG_BULLET_2; Creature_Effect(item, &m_Rocket, Spawn_AtlanteanBomb); } break; case M_STATE_FLY: if ((creature->flags & M_FLAG_FLY) == 0 && item->pos.y == item->floor) { item->goal_anim_state = M_STATE_STOP; } break; } if ((creature->flags & M_FLAG_TWIST) == 0) { creature->head_rotation = creature->neck_rotation; } Creature_Head(item, head); if ((creature->flags & M_FLAG_TWIST) == 0) { creature->neck_rotation = creature->head_rotation; creature->head_rotation = 0; } else { creature->neck_rotation = 0; } Creature_Animate(item_num, angle, 0); } static void M_SetupWinged(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = Creature_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->shadow_size = UNIT_SHADOW / 3; obj->hit_points = M_HITPOINTS; obj->pivot_length = 150; obj->radius = M_RADIUS; obj->smartness = M_DEFAULT_SMARTNESS; obj->lot_setup = LOT_Setup(LOT_SETUP_BEAST); obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_anim = true; obj->save_flags = true; Object_GetBone(obj, 0)->rot.y = true; Object_GetBone(obj, 2)->rot.y = true; } static void M_SetupShooter(OBJECT *const obj) { if (!obj->loaded) { return; } *obj = *Object_Get(O_ATLANTEAN_WINGED); obj->setup_func = M_SetupShooter; obj->initialise_func = M_InitialiseGround; obj->smartness = M_SHOOTER_SMARTNESS; obj->lot_setup = LOT_Setup(LOT_SETUP_DEFAULT); } static void M_SetupGround(OBJECT *const obj) { if (!obj->loaded) { return; } *obj = *Object_Get(O_ATLANTEAN_WINGED); obj->setup_func = M_SetupGround; obj->initialise_func = M_InitialiseGround; obj->lot_setup = LOT_Setup(LOT_SETUP_DEFAULT); } void Atlantean_ToggleExplosions(bool enable) { m_EnableExplosions = enable; } REGISTER_OBJECT(O_ATLANTEAN_WINGED, M_SetupWinged) REGISTER_OBJECT(O_ATLANTEAN_SHOOTER, M_SetupShooter) REGISTER_OBJECT(O_ATLANTEAN_GROUND, M_SetupGround) ================================================ FILE: src/trx/game/objects/creatures/atlantean.h ================================================ #pragma once void Atlantean_ToggleExplosions(bool enable); ================================================ FILE: src/trx/game/objects/creatures/bacon_lara.c ================================================ #include #include #include #include #include #include #include #include #define M_SMASH_JUMP_FRAME 1 typedef struct { bool status; } M_PRIV; static int32_t m_AnchorX = -1; static int32_t m_AnchorZ = -1; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "status", &p->status)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "status", p->status); } static void M_Initialise(const int16_t item_num) { const ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; const OBJECT *const lara_obj = Object_Get(O_LARA); OBJECT *const bacon_obj = Object_Get(O_BACON_LARA); bacon_obj->anim_idx = lara_obj->anim_idx; bacon_obj->frame_base = lara_obj->frame_base; p->status = false; } static void M_Control(const int16_t item_num) { if (m_AnchorX == -1) { return; } ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; const ITEM *const lara_item = Lara_GetItem(); if (Item_IsTriggerActive(item)) { if (!LOT_EnableBaddieAI(item_num, true)) { return; } item->status = IS_ACTIVE; } if (item->hit_points < LARA_MAX_HITPOINTS) { Lara_TakeDamage((LARA_MAX_HITPOINTS - item->hit_points) * 10, false); item->hit_points = LARA_MAX_HITPOINTS; } if (!p->status) { const XYZ_32 pos = { .x = 2 * m_AnchorX - lara_item->pos.x, .z = 2 * m_AnchorZ - lara_item->pos.z, .y = lara_item->pos.y, }; int16_t room_num = item->room_num; const SECTOR *sector = Room_GetSector(pos, &room_num); const int32_t h = Room_GetHeight(sector, pos); item->floor = h; room_num = lara_item->room_num; sector = Room_GetSector(lara_item->pos, &room_num); int32_t lh = Room_GetHeight(sector, lara_item->pos); const int16_t relative_anim = Item_GetRelativeAnim(lara_item); const int16_t relative_frame = Item_GetRelativeFrame(lara_item); Item_SwitchToObjAnim(item, relative_anim, relative_frame, O_LARA); item->pos = pos; item->rot = lara_item->rot; item->rot.y -= DEG_180; Item_UpdateRoom(item_num, lara_item->room_num); if (h >= lh + WALL_L && !lara_item->gravity) { item->current_anim_state = LS(LS_FAST_FALL); item->goal_anim_state = LS(LS_FAST_FALL); Item_SwitchToAnim(item, LA(LA_SMASH_JUMP), M_SMASH_JUMP_FRAME); item->speed = 0; item->fall_speed = 0; item->gravity = true; item->pos.y += 50; p->status = true; } } if (p->status) { Item_Animate(item); int16_t room_num = item->room_num; const SECTOR *sector = Room_GetSector(item->pos, &room_num); const int32_t h = Room_GetHeight(sector, item->pos); item->floor = h; Room_TestTriggers(item); if (item->pos.y >= h) { item->floor = h; item->pos.y = h; Room_TestTriggers(item); item->gravity = false; item->fall_speed = 0; item->goal_anim_state = LS(LS_DEATH); item->required_anim_state = LS(LS_DEATH); } } } static bool M_Draw(const ITEM *const item) { M_PRIV *const p = item->priv; if (p->status || item->current_anim_state == LS(LS_DEATH)) { return Object_DrawAnimatingItem(item); } OBJECT_MESH *old_mesh_ptrs[LM_NUMBER_OF]; for (LARA_MESH mesh = LM_FIRST; mesh < LM_NUMBER_OF; mesh++) { old_mesh_ptrs[mesh] = Lara_Mesh_Get(mesh); Lara_Mesh_SwapSingle(mesh, O_BACON_LARA); } Lara_Draw(item); for (LARA_MESH mesh = LM_FIRST; mesh < LM_NUMBER_OF; mesh++) { Lara_Mesh_Set(mesh, old_mesh_ptrs[mesh]); } return true; } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->draw_func = M_Draw; obj->collision_func = Creature_Collision; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->hit_points = LARA_MAX_HITPOINTS; obj->shadow_size = (UNIT_SHADOW * 10) / 16; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; } bool BaconLara_InitialiseAnchor(const int32_t room_index) { if (room_index >= Room_GetCount()) { return false; } const ROOM *const room = Room_Get(room_index); m_AnchorX = room->pos.x + room->size.x * (WALL_L >> 1); m_AnchorZ = room->pos.z + room->size.z * (WALL_L >> 1); return true; } REGISTER_OBJECT(O_BACON_LARA, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/bacon_lara.h ================================================ #pragma once #include bool BaconLara_InitialiseAnchor(int32_t room_index); ================================================ FILE: src/trx/game/objects/creatures/baldy.c ================================================ #include #include #include #include #include #include #include #define BALDY_SHOT_DAMAGE 150 #define BALDY_WALK_TURN (DEG_1 * 3) // = 546 #define BALDY_RUN_TURN (DEG_1 * 6) // = 1092 #define BALDY_WALK_RANGE SQUARE(WALL_L * 4) // = 16777216 #define BALDY_DIE_ANIM 14 #define BALDY_HITPOINTS 200 #define BALDY_RADIUS (WALL_L / 10) // = 102 #define BALDY_SMARTNESS 0x7FFF typedef enum { BALDY_STATE_EMPTY = 0, BALDY_STATE_STOP = 1, BALDY_STATE_WALK = 2, BALDY_STATE_RUN = 3, BALDY_STATE_AIM = 4, BALDY_STATE_DEATH = 5, BALDY_STATE_SHOOT = 6, } BALDY_STATE; static const CREATURE_GUN m_BaldyGun = { .muzzle = { .pos = { -20, 440, 20 }, .mesh_num = 9 }, }; static void M_Initialise(const int16_t item_num) { Creature_Initialise(item_num); Item_Get(item_num)->current_anim_state = BALDY_STATE_RUN; } static void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage) { if (stage == SAVEGAME_STAGE_AFTER_LOAD) { if (item->hit_points <= 0) { const uint16_t flags = Music_GetTrackFlags(Music_ToGameID(MX_BALDY_SPEECH)); Music_SetTrackFlags( Music_ToGameID(MX_BALDY_SPEECH), flags | IF_ONE_SHOT); } } } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const baldy = item->creature_data; int16_t head = 0; int16_t angle = 0; int16_t tilt = 0; if (item->hit_points <= 0) { if (item->current_anim_state != BALDY_STATE_DEATH) { item->current_anim_state = BALDY_STATE_DEATH; Item_SwitchToAnim(item, BALDY_DIE_ANIM, 0); } } else { AI_INFO info; Creature_AIInfo(item, &info); if (info.ahead) { head = info.angle; } Creature_Mood(item, &info, true); angle = Creature_Turn(item, baldy->maximum_turn); switch (item->current_anim_state) { case BALDY_STATE_STOP: if (item->required_anim_state) { item->goal_anim_state = item->required_anim_state; } else if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = BALDY_STATE_AIM; } else if (baldy->mood == MOOD_BORED) { item->goal_anim_state = BALDY_STATE_WALK; } else { item->goal_anim_state = BALDY_STATE_RUN; } break; case BALDY_STATE_WALK: baldy->maximum_turn = BALDY_WALK_TURN; if (baldy->mood == MOOD_ESCAPE || !info.ahead) { item->required_anim_state = BALDY_STATE_RUN; item->goal_anim_state = BALDY_STATE_STOP; } else if (Creature_CanTargetEnemy(item, &info)) { item->required_anim_state = BALDY_STATE_AIM; item->goal_anim_state = BALDY_STATE_STOP; } else if (info.distance > BALDY_WALK_RANGE) { item->required_anim_state = BALDY_STATE_RUN; item->goal_anim_state = BALDY_STATE_STOP; } break; case BALDY_STATE_RUN: baldy->maximum_turn = BALDY_RUN_TURN; tilt = angle / 2; if (baldy->mood != MOOD_ESCAPE || info.ahead) { if (Creature_CanTargetEnemy(item, &info)) { item->required_anim_state = BALDY_STATE_AIM; item->goal_anim_state = BALDY_STATE_STOP; } else if (info.ahead && info.distance < BALDY_WALK_RANGE) { item->required_anim_state = BALDY_STATE_WALK; item->goal_anim_state = BALDY_STATE_STOP; } } break; case BALDY_STATE_AIM: baldy->flags = 0; if (item->required_anim_state) { item->goal_anim_state = BALDY_STATE_STOP; } else if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = BALDY_STATE_SHOOT; } else { item->goal_anim_state = BALDY_STATE_STOP; } break; case BALDY_STATE_SHOOT: if (!baldy->flags) { info.distance /= 2; Creature_Shoot( item, &info, &m_BaldyGun, head, BALDY_SHOT_DAMAGE); baldy->flags = 1; } if (baldy->mood == MOOD_ESCAPE) { item->required_anim_state = BALDY_STATE_RUN; } break; } } Creature_Tilt(item, tilt); Creature_Head(item, head); Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->handle_save_func = M_HandleSave; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->shadow_size = UNIT_SHADOW / 2; obj->hit_points = BALDY_HITPOINTS; obj->radius = BALDY_RADIUS; obj->smartness = BALDY_SMARTNESS; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_anim = true; obj->save_flags = true; Object_GetBone(obj, 0)->rot.y = true; } REGISTER_OBJECT(O_BALDY, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/bandit_1.c ================================================ #include #include #include #include #include #include #include #include // clang-format off #define BANDIT_1_HITPOINTS 45 #define BANDIT_1_SHOOT_DAMAGE 8 #define BANDIT_1_WALK_TURN (DEG_1 * 4) // = 728 #define BANDIT_1_RUN_TURN (DEG_1 * 6) // = 1092 #define BANDIT_1_WALK_RANGE SQUARE(WALL_L * 2) // = 4194304 #define BANDIT_1_SHOOT_1_CHANCE 0x2000 #define BANDIT_1_SHOOT_2_CHANCE 0x4000 // clang-format on typedef enum { // clang-format off BANDIT_1_STATE_EMPTY = 0, BANDIT_1_STATE_WAIT = 1, BANDIT_1_STATE_WALK = 2, BANDIT_1_STATE_RUN = 3, BANDIT_1_STATE_AIM_1 = 4, BANDIT_1_STATE_SHOOT_1 = 5, BANDIT_1_STATE_AIM_2 = 6, BANDIT_1_STATE_SHOOT_2 = 7, BANDIT_1_STATE_SHOOT_3A = 8, BANDIT_1_STATE_SHOOT_3B = 9, BANDIT_1_STATE_SHOOT_4A = 10, BANDIT_1_STATE_AIM_3 = 11, BANDIT_1_STATE_AIM_4 = 12, BANDIT_1_STATE_DEATH = 13, BANDIT_1_STATE_SHOOT_4B = 14, // clang-format on } BANDIT_1_STATE; typedef enum { BANDIT_1_ANIM_DEATH = 14, } BANDIT_1_ANIM; static const CREATURE_GUN m_Bandit1Gun = { .muzzle = { .pos = { .x = -2, .y = 150, .z = 19 }, .mesh_num = 17 }, }; static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t head = 0; int16_t tilt = 0; int16_t neck = 0; int16_t angle = 0; if (item->hit_points <= 0) { item->hit_points = 0; if (item->current_anim_state != BANDIT_1_STATE_DEATH) { Item_SwitchToAnim(item, BANDIT_1_ANIM_DEATH, 0); item->current_anim_state = BANDIT_1_STATE_DEATH; } } else { AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, false); angle = Creature_Turn(item, creature->maximum_turn); switch (item->current_anim_state) { case BANDIT_1_STATE_WAIT: if (info.ahead) { neck = info.angle; } creature->maximum_turn = 0; if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = BANDIT_1_STATE_RUN; } else if (Creature_CanTargetEnemy(item, &info)) { if (info.distance > BANDIT_1_WALK_RANGE) { item->goal_anim_state = BANDIT_1_STATE_WALK; } else { const int32_t random = Random_GetControl(); if (random < BANDIT_1_SHOOT_1_CHANCE) { item->goal_anim_state = BANDIT_1_STATE_SHOOT_1; } else if (random < BANDIT_1_SHOOT_2_CHANCE) { item->goal_anim_state = BANDIT_1_STATE_SHOOT_2; } else { item->goal_anim_state = BANDIT_1_STATE_AIM_3; } } } else if (creature->mood == MOOD_BORED) { if (info.ahead) { item->goal_anim_state = BANDIT_1_STATE_WAIT; } else { item->goal_anim_state = BANDIT_1_STATE_WALK; } } else { item->goal_anim_state = BANDIT_1_STATE_RUN; } break; case BANDIT_1_STATE_WALK: if (info.ahead) { neck = info.angle; } creature->maximum_turn = BANDIT_1_WALK_TURN; if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = BANDIT_1_STATE_RUN; } else if (Creature_CanTargetEnemy(item, &info)) { if (info.distance > BANDIT_1_WALK_RANGE && info.zone_num == info.enemy_zone_num) { item->goal_anim_state = BANDIT_1_STATE_AIM_4; } else { item->goal_anim_state = BANDIT_1_STATE_WAIT; } } else if (creature->mood == MOOD_BORED) { if (info.ahead) { item->goal_anim_state = BANDIT_1_STATE_WALK; } else { item->goal_anim_state = BANDIT_1_STATE_WAIT; } } else { item->goal_anim_state = BANDIT_1_STATE_RUN; } break; case BANDIT_1_STATE_RUN: if (info.ahead) { neck = info.angle; } tilt = angle / 2; creature->maximum_turn = BANDIT_1_RUN_TURN; if (creature->mood == MOOD_ESCAPE) { } else if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = BANDIT_1_STATE_WAIT; } else if (creature->mood == MOOD_BORED) { item->goal_anim_state = BANDIT_1_STATE_WALK; } break; case BANDIT_1_STATE_SHOOT_1: case BANDIT_1_STATE_SHOOT_2: case BANDIT_1_STATE_SHOOT_3A: case BANDIT_1_STATE_SHOOT_3B: if (info.ahead) { head = info.angle; } if (!Creature_Shoot( item, &info, &m_Bandit1Gun, head, BANDIT_1_SHOOT_DAMAGE)) { item->goal_anim_state = BANDIT_1_STATE_WAIT; } break; case BANDIT_1_STATE_SHOOT_4A: case BANDIT_1_STATE_SHOOT_4B: if (info.ahead) { head = info.angle; } if (!Creature_Shoot( item, &info, &m_Bandit1Gun, head, BANDIT_1_SHOOT_DAMAGE)) { item->goal_anim_state = BANDIT_1_STATE_WALK; } if (info.distance < BANDIT_1_WALK_RANGE) { item->goal_anim_state = BANDIT_1_STATE_WALK; } break; default: break; } } Creature_Tilt(item, tilt); Creature_Head(item, head); Creature_Neck(item, neck); Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->hit_points = BANDIT_1_HITPOINTS; obj->radius = BANDIT_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->pivot_length = 0; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 6)->rot.y = true; Object_GetBone(obj, 8)->rot.y = true; } REGISTER_OBJECT(O_BANDIT_1, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/bandit_2.c ================================================ #include #include #include #include #include #include #include #include // clang-format off #define BANDIT_2_HITPOINTS 50 #define BANDIT_2_SHOOT_DAMAGE 50 #define BANDIT_2_WALK_TURN (DEG_1 * 4) // = 728 #define BANDIT_2_RUN_TURN (DEG_1 * 6) // = 1092 #define BANDIT_2_WALK_RANGE SQUARE(WALL_L * 2) // = 4194304 #define BANDIT_2_WALK_CHANCE 0x4000 #define BANDIT_2_SHOOT_1_CHANCE 0x2000 #define BANDIT_2_SHOOT_2_CHANCE 0x5000 // clang-format on typedef enum { // clang-format off BANDIT_2_STATE_EMPTY = 0, BANDIT_2_STATE_AIM_4 = 1, BANDIT_2_STATE_WAIT = 2, BANDIT_2_STATE_WALK = 3, BANDIT_2_STATE_RUN = 4, BANDIT_2_STATE_AIM_1 = 5, BANDIT_2_STATE_AIM_2 = 6, BANDIT_2_STATE_SHOOT_1 = 7, BANDIT_2_STATE_SHOOT_2 = 8, BANDIT_2_STATE_SHOOT_4A = 9, BANDIT_2_STATE_SHOOT_4B = 10, BANDIT_2_STATE_DEATH = 11, BANDIT_2_STATE_AIM_5 = 12, BANDIT_2_STATE_SHOOT_5 = 13, // clang-format on } BANDIT_2_STATE; typedef enum { BANDIT_2_ANIM_DEATH = 9, } BANDIT_2_ANIM; static const CREATURE_GUN m_Bandit2Gun = { .muzzle = { .pos = { .x = -1, .y = 230, .z = 9 }, .mesh_num = 17 }, }; static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t head = 0; int16_t tilt = 0; int16_t neck = 0; int16_t angle = 0; if (item->hit_points <= 0) { if (item->current_anim_state != BANDIT_2_STATE_DEATH) { Item_SwitchToAnim(item, BANDIT_2_ANIM_DEATH, 0); item->current_anim_state = BANDIT_2_STATE_DEATH; } } else { AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, true); angle = Creature_Turn(item, creature->maximum_turn); switch (item->current_anim_state) { case BANDIT_2_STATE_WAIT: if (info.ahead) { neck = info.angle; } creature->maximum_turn = 0; if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = BANDIT_2_STATE_RUN; } else if (Creature_CanTargetEnemy(item, &info)) { if (info.distance > BANDIT_2_WALK_RANGE && Random_GetControl() < BANDIT_2_WALK_CHANCE) { item->goal_anim_state = BANDIT_2_STATE_WALK; } else { const int32_t random = Random_GetControl(); if (random < BANDIT_2_SHOOT_1_CHANCE) { item->goal_anim_state = BANDIT_2_STATE_SHOOT_1; } else if (random < BANDIT_2_SHOOT_2_CHANCE) { item->goal_anim_state = BANDIT_2_STATE_SHOOT_2; } else { item->goal_anim_state = BANDIT_2_STATE_AIM_5; } } } else if (creature->mood == MOOD_BORED) { if (!info.ahead || Random_GetControl() < 0x100) { item->goal_anim_state = BANDIT_2_STATE_WALK; } } else { item->goal_anim_state = BANDIT_2_STATE_RUN; } break; case BANDIT_2_STATE_WALK: if (info.ahead) { neck = info.angle; } creature->maximum_turn = BANDIT_2_WALK_TURN; if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = BANDIT_2_STATE_RUN; } else if (Creature_CanTargetEnemy(item, &info)) { if (info.distance < BANDIT_2_WALK_RANGE || info.zone_num == info.enemy_zone_num || Random_GetControl() < 0x400) { item->goal_anim_state = BANDIT_2_STATE_WAIT; } else { item->goal_anim_state = BANDIT_2_STATE_AIM_4; } } else if (creature->mood != MOOD_BORED) { item->goal_anim_state = BANDIT_2_STATE_RUN; } else if (info.ahead && Random_GetControl() < 0x400) { item->goal_anim_state = BANDIT_2_STATE_WAIT; } break; case BANDIT_2_STATE_RUN: if (info.ahead) { neck = info.angle; } creature->maximum_turn = BANDIT_2_RUN_TURN; tilt = angle / 2; if (creature->mood == MOOD_ESCAPE) { } else if ( creature->mood == MOOD_BORED || Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = BANDIT_2_STATE_WAIT; } break; case BANDIT_2_STATE_AIM_1: case BANDIT_2_STATE_AIM_2: case BANDIT_2_STATE_AIM_4: if (info.ahead) { head = info.angle; } creature->flags = 0; break; case BANDIT_2_STATE_AIM_5: if (info.ahead) { head = info.angle; } creature->flags = 0; if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = BANDIT_2_STATE_SHOOT_5; } else { item->goal_anim_state = BANDIT_2_STATE_WAIT; } break; case BANDIT_2_STATE_SHOOT_1: case BANDIT_2_STATE_SHOOT_2: case BANDIT_2_STATE_SHOOT_5: if (info.ahead) { head = info.angle; } if (creature->flags == 0) { if (!Creature_Shoot( item, &info, &m_Bandit2Gun, head, BANDIT_2_SHOOT_DAMAGE) || Random_GetControl() < 0x2000) { item->goal_anim_state = BANDIT_2_STATE_WAIT; } creature->flags = 1; } break; case BANDIT_2_STATE_SHOOT_4A: if (info.ahead) { head = info.angle; } if (creature->flags != 1) { if (!Creature_Shoot( item, &info, &m_Bandit2Gun, head, BANDIT_2_SHOOT_DAMAGE)) { item->goal_anim_state = BANDIT_2_STATE_WALK; } creature->flags = 1; } if (info.distance < BANDIT_2_WALK_RANGE) { item->goal_anim_state = BANDIT_2_STATE_WALK; } break; case BANDIT_2_STATE_SHOOT_4B: if (info.ahead) { head = info.angle; } if (creature->flags != 2) { if (!Creature_Shoot( item, &info, &m_Bandit2Gun, head, BANDIT_2_SHOOT_DAMAGE)) { item->goal_anim_state = BANDIT_2_STATE_WALK; } creature->flags = 2; } if (info.distance < BANDIT_2_WALK_RANGE) { item->goal_anim_state = BANDIT_2_STATE_WALK; } break; default: break; } } Creature_Tilt(item, tilt); Creature_Head(item, head); Creature_Neck(item, neck); Creature_Animate(item_num, angle, 0); } static void M_Setup2A(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->hit_points = BANDIT_2_HITPOINTS; obj->radius = BANDIT_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->pivot_length = 0; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 6)->rot.y = true; Object_GetBone(obj, 8)->rot.y = true; } static void M_Setup2B(OBJECT *const obj) { if (!obj->loaded) { return; } const OBJECT *const ref_obj = Object_Get(O_BANDIT_2); if (ref_obj->loaded) { obj->anim_idx = ref_obj->anim_idx; obj->frame_base = ref_obj->frame_base; } obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->hit_points = BANDIT_2_HITPOINTS; obj->radius = BANDIT_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->pivot_length = 0; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 6)->rot.y = true; Object_GetBone(obj, 8)->rot.y = true; } REGISTER_OBJECT(O_BANDIT_2, M_Setup2A) REGISTER_OBJECT(O_BANDIT_2B, M_Setup2B) ================================================ FILE: src/trx/game/objects/creatures/bandit_common.h ================================================ #pragma once #define BANDIT_RADIUS (WALL_L / 10) // = 102 ================================================ FILE: src/trx/game/objects/creatures/barracuda.c ================================================ #include #include #include #include #include #include #include // clang-format off #define BARRACUDA_HITPOINTS 12 #define BARRACUDA_TOUCH_BITS 0b11100000 // = 0xE0 #define BARRACUDA_RADIUS (WALL_L / 5) // = 204 #define BARRACUDA_BITE_DAMAGE 100 #define BARA_SWIM_1_TURN (DEG_1 * 2) // = 364 #define BARA_SWIM_2_TURN (DEG_1 * 4) // = 728 #define BARA_ATTACK_1_RANGE SQUARE(WALL_L * 2 / 3) // = 465124 #define BARA_ATTACK_2_RANGE SQUARE(WALL_L / 3) // = 116281 // clang-format on typedef enum { // clang-format off BARRACUDA_STATE_EMPTY = 0, BARRACUDA_STATE_STOP = 1, BARRACUDA_STATE_SWIM_1 = 2, BARRACUDA_STATE_SWIM_2 = 3, BARRACUDA_STATE_ATTACK_1 = 4, BARRACUDA_STATE_ATTACK_2 = 5, BARRACUDA_STATE_DEATH = 6, // clang-format on } BARRACUDA_STATE; typedef enum { BARRACUDA_ANIM_DEATH = 6, } BARRACUDA_ANIM; static const BITE m_BarracudaBite = { .pos = { .x = 2, .y = -60, .z = 121 }, .mesh_num = 7, }; static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; if (item->hit_points > 0) { AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, false); int16_t head = 0; int16_t angle = Creature_Turn(item, creature->maximum_turn); switch (item->current_anim_state) { case BARRACUDA_STATE_STOP: creature->flags = 0; if (creature->mood == MOOD_BORED) { item->goal_anim_state = BARRACUDA_STATE_SWIM_1; } else if (info.ahead && info.distance < BARA_ATTACK_1_RANGE) { item->goal_anim_state = BARRACUDA_STATE_ATTACK_1; } else if (creature->mood == MOOD_STALK) { item->goal_anim_state = BARRACUDA_STATE_SWIM_1; } else { item->goal_anim_state = BARRACUDA_STATE_SWIM_2; } break; case BARRACUDA_STATE_SWIM_1: creature->maximum_turn = BARA_SWIM_1_TURN; if (creature->mood == MOOD_BORED) { } else if ( info.ahead && (item->touch_bits & BARRACUDA_TOUCH_BITS) != 0) { item->goal_anim_state = BARRACUDA_STATE_STOP; } else if (creature->mood != MOOD_STALK) { item->goal_anim_state = BARRACUDA_STATE_SWIM_2; } break; case BARRACUDA_STATE_SWIM_2: creature->maximum_turn = BARA_SWIM_2_TURN; creature->flags = 0; if (creature->mood == MOOD_BORED) { item->goal_anim_state = BARRACUDA_STATE_SWIM_1; } else if (info.ahead && info.distance < BARA_ATTACK_2_RANGE) { item->goal_anim_state = BARRACUDA_STATE_ATTACK_2; } else if (info.ahead && info.distance < BARA_ATTACK_1_RANGE) { item->goal_anim_state = BARRACUDA_STATE_STOP; } else if (creature->mood == MOOD_STALK) { item->goal_anim_state = BARRACUDA_STATE_SWIM_1; } break; case BARRACUDA_STATE_ATTACK_1: case BARRACUDA_STATE_ATTACK_2: if (info.ahead) { head = info.angle; } if (creature->flags == 0 && (item->touch_bits & BARRACUDA_TOUCH_BITS) != 0) { Lara_TakeDamage(BARRACUDA_BITE_DAMAGE, true); Creature_Effect(item, &m_BarracudaBite, Spawn_Blood); creature->flags = 1; } break; default: break; } Creature_Head(item, head); Creature_Animate(item_num, angle, 0); Creature_Underwater(item, STEP_L); } else { if (item->current_anim_state != BARRACUDA_ANIM_DEATH) { Item_SwitchToAnim(item, BARRACUDA_ANIM_DEATH, 0); item->current_anim_state = BARRACUDA_STATE_DEATH; Carrier_TestItemDrops(item_num); } Creature_Float(item_num); } } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->hit_points = BARRACUDA_HITPOINTS; obj->radius = BARRACUDA_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->pivot_length = 200; obj->lot_setup = LOT_Setup(LOT_SETUP_FLYER); obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 6)->rot.y = true; } REGISTER_OBJECT(O_BARRACUDA, M_Setup) REGISTER_OBJECT(O_FISH, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/bartoli.c ================================================ #include #include #include #include #include #include #define M_BOOM_TIME 130 #define M_BARTOLI_RANGE (WALL_L * 5) // = 5120 typedef struct { int16_t dragon_item_num; } M_PRIV; static void M_CreateBoom(const OBJECT_ID obj_id, const ITEM *const origin_item) { const int16_t item_num = Item_Create(); if (item_num == NO_ITEM) { return; } ITEM *const sphere_item = Item_Get(item_num); sphere_item->object_id = obj_id; sphere_item->pos.x = origin_item->pos.x; sphere_item->pos.y = origin_item->pos.y + 256; sphere_item->pos.z = origin_item->pos.z; sphere_item->room_num = origin_item->room_num; sphere_item->shade.value_1 = -1; Item_Initialise(item_num); Item_AddActive(item_num); sphere_item->status = IS_ACTIVE; } static void M_ConvertBartoliToDragon(const int16_t item_num) { const ITEM *const bartoli_item = Item_Get(item_num); const M_PRIV *const p = bartoli_item->priv; const int16_t dragon_item_num = p->dragon_item_num; if (dragon_item_num != NO_ITEM) { ITEM *const dragon_item = Item_Get(dragon_item_num); const OBJECT *const dragon_obj = Object_Get(dragon_item->object_id); if (dragon_obj->activate_func != nullptr) { dragon_obj->activate_func(dragon_item); } } Item_Kill(item_num); } static bool M_CheckLaraProximity(const ITEM *const origin_item) { const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = ABS(lara_item->pos.x - origin_item->pos.x); const int32_t dz = ABS(lara_item->pos.z - origin_item->pos.z); return dx < M_BARTOLI_RANGE && dz < M_BARTOLI_RANGE; } static int16_t M_GetCarrierItemNum(const ITEM *const item) { const M_PRIV *const p = item->priv; return p->dragon_item_num; } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; p->dragon_item_num = Item_CreateLevelItem(); ASSERT(p->dragon_item_num != NO_ITEM); ITEM *const dragon_item = Item_Get(p->dragon_item_num); dragon_item->object_id = O_DRAGON_BACK; dragon_item->pos = item->pos; dragon_item->rot.y = item->rot.y; dragon_item->room_num = item->room_num; Item_Initialise(p->dragon_item_num); } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (item->timer == 0) { if (M_CheckLaraProximity(item)) { item->timer = 1; } return; } item->timer++; if ((item->timer & 7) == 0) { g_Camera.bounce = item->timer; } Spawn_MysticLight(item_num); Item_Animate(item); if (item->timer == M_BOOM_TIME + 0) { M_CreateBoom(O_SPHERE_OF_DOOM_1, item); } else if (item->timer == M_BOOM_TIME + 10) { M_CreateBoom(O_SPHERE_OF_DOOM_2, item); } else if (item->timer == M_BOOM_TIME + 20) { M_CreateBoom(O_SPHERE_OF_DOOM_3, item); } else if (item->timer >= M_BOOM_TIME + 20) { M_ConvertBartoliToDragon(item_num); } } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->carrier_item_num_func = M_GetCarrierItemNum; obj->priv_size = sizeof(M_PRIV); obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_BARTOLI, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/bat.c ================================================ #include #include #include #include #include #include #define BAT_ATTACK_DAMAGE 2 #define BAT_TURN (20 * DEG_1) // = 3640 #define BAT_HITPOINTS 1 #define BAT_RADIUS (WALL_L / 10) // = 102 #define BAT_SMARTNESS 0x400 typedef enum { BAT_STATE_EMPTY = 0, BAT_STATE_STOP = 1, BAT_STATE_FLY = 2, BAT_STATE_ATTACK = 3, BAT_STATE_FALL = 4, BAT_STATE_DEATH = 5, } BAT_STATE; static BITE m_BatBite = { .pos = { 0, 16, 45 }, .mesh_num = 4 }; static void M_FixEmbeddedPosition(int16_t item_num) { ITEM *const item = Item_Get(item_num); if (item->status == IS_ACTIVE) { return; } int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); const int32_t ceiling = Room_GetCeiling(sector, item->pos); // The bats animation and frame have to be changed to the hanging // one to properly measure them. Save it so it can be restored // after. const int16_t old_anim = Item_GetRelativeAnim(item); const int16_t old_frame = Item_GetRelativeFrame(item); Item_SwitchToAnim(item, 0, 0); const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item); Item_SwitchToAnim(item, old_anim, old_frame); const int16_t bat_height = ABS(bounds->min.y); // Only move the bat if it's above the calculated position, // Palace Midas has many bats that aren't intended to be at // ceiling level. if (item->pos.y < ceiling + bat_height) { item->pos.y = ceiling + bat_height; } } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const bat = item->creature_data; int16_t angle = 0; if (item->hit_points <= 0) { if (item->pos.y < item->floor) { item->gravity = true; item->goal_anim_state = BAT_STATE_FALL; item->speed = 0; } else { item->gravity = false; item->fall_speed = 0; item->goal_anim_state = BAT_STATE_DEATH; item->pos.y = item->floor; } Creature_Animate(item_num, 0, 0); return; } else { AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, false); angle = Creature_Turn(item, BAT_TURN); switch (item->current_anim_state) { case BAT_STATE_STOP: item->goal_anim_state = BAT_STATE_FLY; break; case BAT_STATE_FLY: if (item->touch_bits) { item->goal_anim_state = BAT_STATE_ATTACK; Creature_Animate(item_num, angle, 0); return; } break; case BAT_STATE_ATTACK: if (item->touch_bits) { Creature_Effect(item, &m_BatBite, Spawn_Blood); Lara_TakeDamage(BAT_ATTACK_DAMAGE, true); } else { item->goal_anim_state = BAT_STATE_FLY; bat->mood = MOOD_BORED; } break; } } Creature_Animate(item_num, angle, 0); } static void M_Initialise(const int16_t item_num) { Creature_Initialise(item_num); // Almost all of the bats in the OG levels are embedded in the ceiling. // This will move all bats up to the ceiling of their rooms and down // by the height of their hanging animation. M_FixEmbeddedPosition(item_num); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->shadow_size = UNIT_SHADOW / 2; obj->hit_points = BAT_HITPOINTS; obj->radius = BAT_RADIUS; obj->smartness = BAT_SMARTNESS; obj->lot_setup = LOT_Setup(LOT_SETUP_FLYER); obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_anim = true; obj->save_flags = true; } REGISTER_OBJECT(O_BAT, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/bear.c ================================================ #include #include #include #include #include #include #include #include // clang-format off #define BEAR_CHARGE_DAMAGE 3 #define BEAR_SLAM_DAMAGE 200 #define BEAR_ATTACK_DAMAGE 200 #define BEAR_PAT_DAMAGE 400 #define BEAR_TOUCH 0x2406C #define BEAR_ROAR_CHANCE 80 #define BEAR_REAR_CHANCE 768 #define BEAR_DROP_CHANCE 1536 #define BEAR_REAR_RANGE SQUARE(WALL_L * 2) // = 4194304 #define BEAR_ATTACK_RANGE SQUARE(WALL_L) // = 1048576 #define BEAR_PAT_RANGE SQUARE(600) // = 360000 #define BEAR_FIX_PAT_RANGE SQUARE(300) // = 90000 #define BEAR_RUN_TURN (5 * DEG_1) // = 910 #define BEAR_WALK_TURN (2 * DEG_1) // = 364 #define BEAR_EAT_RANGE SQUARE(WALL_L * 3 / 4) // = 589824 #define BEAR_HITPOINTS (g_TRVersion == 1 ? 20 : 30) #define BEAR_RADIUS (WALL_L / 3) // = 341 #define BEAR_SMARTNESS 0x4000 // clang-format on typedef enum { // clang-format off BEAR_STATE_STROLL = 0, BEAR_STATE_STOP = 1, BEAR_STATE_WALK = 2, BEAR_STATE_RUN = 3, BEAR_STATE_REAR = 4, BEAR_STATE_ROAR = 5, BEAR_STATE_ATTACK_1 = 6, BEAR_STATE_ATTACK_2 = 7, BEAR_STATE_EAT = 8, BEAR_STATE_DEATH = 9, // clang-format on } BEAR_STATE; static BITE m_BearHeadBite = { .pos = { 0, 96, 335 }, .mesh_num = 14 }; static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); OBJECT *const obj = Object_Get(item->object_id); obj->pivot_length = g_Config.gameplay.fix_bear_ai ? 0 : 500; CREATURE *const bear = item->creature_data; int16_t head = 0; int16_t angle = 0; if (item->hit_points <= 0) { angle = Creature_Turn(item, DEG_1); switch (item->current_anim_state) { case BEAR_STATE_WALK: item->goal_anim_state = BEAR_STATE_REAR; break; case BEAR_STATE_RUN: case BEAR_STATE_STROLL: item->goal_anim_state = BEAR_STATE_STOP; break; case BEAR_STATE_REAR: bear->flags = 1; item->goal_anim_state = BEAR_STATE_DEATH; break; case BEAR_STATE_STOP: bear->flags = 0; item->goal_anim_state = BEAR_STATE_DEATH; break; case BEAR_STATE_DEATH: if (bear != nullptr && bear->flags != 0 && (item->touch_bits & BEAR_TOUCH) != 0) { Lara_TakeDamage(BEAR_SLAM_DAMAGE, true); bear->flags = 0; } break; } } else { AI_INFO info; Creature_AIInfo(item, &info); if (info.ahead) { head = info.angle; } Creature_Mood(item, &info, true); angle = Creature_Turn(item, bear->maximum_turn); const bool dead_enemy = Lara_GetItem()->hit_points <= 0; if (item->hit_status) { bear->flags = 1; } switch ((int16_t)item->current_anim_state) { case BEAR_STATE_STOP: if (dead_enemy) { if (info.bite && info.distance < BEAR_EAT_RANGE) { item->goal_anim_state = BEAR_STATE_EAT; } else { item->goal_anim_state = BEAR_STATE_STROLL; } } else if (item->required_anim_state) { item->goal_anim_state = item->required_anim_state; } else if (bear->mood == MOOD_BORED) { item->goal_anim_state = BEAR_STATE_STROLL; } else { item->goal_anim_state = BEAR_STATE_RUN; } break; case BEAR_STATE_STROLL: bear->maximum_turn = BEAR_WALK_TURN; if (dead_enemy && (item->touch_bits & BEAR_TOUCH) && info.ahead) { item->goal_anim_state = BEAR_STATE_STOP; } else if (bear->mood != MOOD_BORED) { item->goal_anim_state = BEAR_STATE_STOP; if (bear->mood == MOOD_ESCAPE) { item->required_anim_state = BEAR_STATE_STROLL; } } else if (Random_GetControl() < BEAR_ROAR_CHANCE) { item->required_anim_state = BEAR_STATE_ROAR; item->goal_anim_state = BEAR_STATE_STOP; } break; case BEAR_STATE_RUN: bear->maximum_turn = BEAR_RUN_TURN; if (item->touch_bits & BEAR_TOUCH) { Lara_TakeDamage(BEAR_CHARGE_DAMAGE, true); } if (bear->mood == MOOD_BORED || dead_enemy) { item->goal_anim_state = BEAR_STATE_STOP; } else if (info.ahead && !item->required_anim_state) { if (!bear->flags && info.distance < BEAR_REAR_RANGE && Random_GetControl() < BEAR_REAR_CHANCE) { item->required_anim_state = BEAR_STATE_REAR; item->goal_anim_state = BEAR_STATE_STOP; } else if (info.distance < BEAR_ATTACK_RANGE) { item->goal_anim_state = BEAR_STATE_ATTACK_1; } } break; case BEAR_STATE_REAR: if (bear->flags) { item->required_anim_state = BEAR_STATE_STROLL; item->goal_anim_state = BEAR_STATE_STOP; } else if (item->required_anim_state) { item->goal_anim_state = item->required_anim_state; } else if (bear->mood == MOOD_BORED || bear->mood == MOOD_ESCAPE) { item->goal_anim_state = BEAR_STATE_STOP; } else if ( info.bite && info.distance < (g_Config.gameplay.fix_bear_ai ? BEAR_FIX_PAT_RANGE : BEAR_PAT_RANGE)) { item->goal_anim_state = BEAR_STATE_ATTACK_2; } else { item->goal_anim_state = BEAR_STATE_WALK; } break; case BEAR_STATE_WALK: if (bear->flags) { item->required_anim_state = BEAR_STATE_STROLL; item->goal_anim_state = BEAR_STATE_REAR; } else if (info.ahead && (item->touch_bits & BEAR_TOUCH)) { item->goal_anim_state = BEAR_STATE_REAR; } else if (bear->mood == MOOD_ESCAPE) { item->goal_anim_state = BEAR_STATE_REAR; item->required_anim_state = BEAR_STATE_STROLL; } else if ( bear->mood == MOOD_BORED || Random_GetControl() < BEAR_ROAR_CHANCE) { item->required_anim_state = BEAR_STATE_ROAR; item->goal_anim_state = BEAR_STATE_REAR; } else if ( info.distance > BEAR_REAR_RANGE || Random_GetControl() < BEAR_DROP_CHANCE) { item->required_anim_state = BEAR_STATE_STOP; item->goal_anim_state = BEAR_STATE_REAR; } break; case BEAR_STATE_ATTACK_1: if (!item->required_anim_state && (item->touch_bits & BEAR_TOUCH)) { Creature_Effect(item, &m_BearHeadBite, Spawn_Blood); Lara_TakeDamage(BEAR_ATTACK_DAMAGE, true); item->required_anim_state = BEAR_STATE_STOP; } break; case BEAR_STATE_ATTACK_2: if (!item->required_anim_state && (item->touch_bits & BEAR_TOUCH)) { Lara_TakeDamage(BEAR_PAT_DAMAGE, true); item->required_anim_state = BEAR_STATE_REAR; } break; } } Creature_Head(item, head); Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = Creature_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->shadow_size = UNIT_SHADOW / 2; obj->hit_points = BEAR_HITPOINTS; obj->radius = BEAR_RADIUS; obj->smartness = BEAR_SMARTNESS; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_anim = true; obj->save_flags = true; Object_GetBone(obj, 13)->rot.y = true; } REGISTER_OBJECT(O_BEAR, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/big_eel.c ================================================ #include #include #include #include #include #include #include // clang-format off #define BIG_EEL_HITPOINTS 20 #define BIG_EEL_TOUCH_BITS 0b00000001'10000000 // = 0x180 #define BIG_EEL_DAMAGE 500 #define BIG_EEL_ANGLE (DEG_1 * 10) // = 1820 #define BIG_EEL_RANGE (WALL_L * 6) // = 6144 #define BIG_EEL_MOVE (WALL_L / 10) // = 102 #define BIG_EEL_LENGTH (WALL_L * 5 / 2) // = 2560 #define BIG_EEL_SLIDE (BIG_EEL_RANGE - BIG_EEL_LENGTH) // = 3584 // clang-format on typedef enum { // clang-format off BIG_EEL_STATE_EMPTY = 0, BIG_EEL_STATE_ATTACK = 1, BIG_EEL_STATE_STOP = 2, BIG_EEL_STATE_DEATH = 3, // clang-format on } BIG_EEL_STATE; typedef enum { BIG_EEL_ANIM_DEATH = 2, } BIG_EEL_ANIM; typedef struct { int32_t pos; } M_PRIV; static const BITE m_BigEelBite = { .pos = { .x = 7, .y = 157, .z = 333 }, .mesh_num = 7, }; static bool M_IsTargetable(const ITEM *const item) { return false; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); const ITEM *const lara_item = Lara_GetItem(); M_PRIV *const p = item->priv; int32_t pos = p->pos; item->pos.z -= (pos * Math_Cos(item->rot.y)) >> W2V_SHIFT; item->pos.x -= ((pos * Math_Sin(item->rot.y)) >> W2V_SHIFT); if (item->hit_points <= 0) { if (pos < BIG_EEL_SLIDE) { pos += BIG_EEL_MOVE; } if (item->current_anim_state != BIG_EEL_STATE_DEATH) { Item_SwitchToAnim(item, BIG_EEL_ANIM_DEATH, 0); item->current_anim_state = BIG_EEL_STATE_DEATH; } } else { const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; const int16_t angle = Math_Atan(dz, dx) - item->rot.y; const int32_t distance = Math_Sqrt(SQUARE(dx) + SQUARE(dz)); switch (item->current_anim_state) { case BIG_EEL_STATE_STOP: if (pos > 0) { pos -= BIG_EEL_MOVE; } if (distance <= BIG_EEL_RANGE && ABS(angle) < BIG_EEL_ANGLE) { item->goal_anim_state = BIG_EEL_STATE_ATTACK; } break; case BIG_EEL_STATE_ATTACK: if (pos < distance - BIG_EEL_LENGTH) { pos += BIG_EEL_MOVE; } if (item->required_anim_state == BIG_EEL_STATE_EMPTY && (item->touch_bits & BIG_EEL_TOUCH_BITS) != 0) { Lara_TakeDamage(BIG_EEL_DAMAGE, true); Creature_Effect(item, &m_BigEelBite, Spawn_Blood); item->required_anim_state = BIG_EEL_STATE_STOP; } break; } } item->pos.x += (pos * Math_Sin(item->rot.y)) >> W2V_SHIFT; item->pos.z += (pos * Math_Cos(item->rot.y)) >> W2V_SHIFT; p->pos = pos; Item_Animate(item); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->is_targetable_func = M_IsTargetable; obj->priv_size = sizeof(M_PRIV); obj->hit_points = BIG_EEL_HITPOINTS; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_BIG_EEL, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/big_spider.c ================================================ #include #include #include #include #include // clang-format off #define BIG_SPIDER_HITPOINTS 40 #define BIG_SPIDER_RADIUS (WALL_L / 3) // = 341 #define BIG_SPIDER_TURN (DEG_1 * 4) // = 728 #define BIG_SPIDER_DAMAGE 100 // clang-format on typedef enum { // clang-format off BIG_SPIDER_STATE_EMPTY = 0, BIG_SPIDER_STATE_STOP = 1, BIG_SPIDER_STATE_WALK_1 = 2, BIG_SPIDER_STATE_WALK_2 = 3, BIG_SPIDER_STATE_ATTACK_1 = 4, BIG_SPIDER_STATE_ATTACK_2 = 5, BIG_SPIDER_STATE_ATTACK_3 = 6, BIG_SPIDER_STATE_DEATH = 7, // clang-format on } BIG_SPIDER_STATE; typedef enum { BIG_SPIDER_ANIM_DEATH = 2, } BIG_SPIDER_ANIM; static const BITE m_SpiderBite = { .pos = { .x = 0, .y = 0, .z = 41 }, .mesh_num = 1, }; static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t tilt = 0; int16_t angle = 0; if (item->hit_points > 0) { AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, true); angle = Creature_Turn(item, BIG_SPIDER_TURN); switch (item->current_anim_state) { case BIG_SPIDER_STATE_STOP: creature->flags = 0; if (creature->mood == MOOD_BORED) { break; } else if (info.ahead && item->touch_bits != 0) { item->goal_anim_state = BIG_SPIDER_STATE_ATTACK_1; } else if (creature->mood == MOOD_STALK) { item->goal_anim_state = BIG_SPIDER_STATE_WALK_1; } else if ( creature->mood == MOOD_ESCAPE || creature->mood == MOOD_ATTACK) { item->goal_anim_state = BIG_SPIDER_STATE_WALK_2; } break; case BIG_SPIDER_STATE_WALK_1: if (creature->mood == MOOD_BORED) { break; } else if (info.ahead && item->touch_bits != 0) { item->goal_anim_state = BIG_SPIDER_STATE_STOP; } else if ( creature->mood == MOOD_ESCAPE || creature->mood == MOOD_ATTACK) { item->goal_anim_state = BIG_SPIDER_STATE_WALK_2; } break; case BIG_SPIDER_STATE_WALK_2: creature->flags = 0; if (info.ahead && item->touch_bits != 0) { item->goal_anim_state = BIG_SPIDER_STATE_STOP; } else if ( creature->mood == MOOD_BORED || creature->mood == MOOD_STALK) { item->goal_anim_state = BIG_SPIDER_STATE_WALK_1; } break; case BIG_SPIDER_STATE_ATTACK_1: if (!creature->flags && item->touch_bits != 0) { Lara_TakeDamage(BIG_SPIDER_DAMAGE, true); Creature_Effect(item, &m_SpiderBite, Spawn_Blood); creature->flags = 1; } break; default: break; } } else if (item->current_anim_state != BIG_SPIDER_STATE_DEATH) { Item_SwitchToAnim(item, BIG_SPIDER_ANIM_DEATH, 0); item->current_anim_state = BIG_SPIDER_STATE_DEATH; } Creature_Animate(item_num, angle, tilt); } void BigSpider_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->hit_points = BIG_SPIDER_HITPOINTS; obj->radius = BIG_SPIDER_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_BIG_SPIDER, BigSpider_Setup) ================================================ FILE: src/trx/game/objects/creatures/big_spider.h ================================================ #pragma once #include void BigSpider_Setup(OBJECT *obj); ================================================ FILE: src/trx/game/objects/creatures/bird.c ================================================ #include #include #include #include #include // clang-format off #define M_DAMAGE 20 #define M_RADIUS (WALL_L / 5) // = 204 #define M_ATTACK_RANGE SQUARE(WALL_L / 2) // = 262144 #define M_TURN (DEG_1 * 3) // = 546 #define M_START_ANIM 5 #define M_DIE_ANIM 8 #define M_EAGLE_HITPOINTS 20 #define M_CROW_HITPOINTS (g_TRVersion == 3 ? 8 : 15) #define M_CROW_START_ANIM 14 #define M_CROW_DIE_ANIM 1 #define M_VULTURE_HITPOINTS 18 // clang-format on typedef enum { M_STATE_EMPTY = 0, M_STATE_FLY = 1, M_STATE_STOP = 2, M_STATE_GLIDE = 3, M_STATE_FALL = 4, M_STATE_DEATH = 5, M_STATE_ATTACK = 6, M_STATE_EAT = 7, } M_STATE; static const BITE m_BirdBite = { .pos = { .x = 15, .y = 46, .z = 21 }, .mesh_num = 6, }; static const BITE m_CrowBite = { .pos = { .x = 2, .y = 10, .z = 60 }, .mesh_num = 14, }; static void M_Initialise(const int16_t item_num) { Creature_Initialise(item_num); ITEM *const item = Item_Get(item_num); if (item->object_id == O_CROW) { Item_SwitchToAnim(item, M_CROW_START_ANIM, 0); item->goal_anim_state = M_STATE_EAT; item->current_anim_state = M_STATE_EAT; } else { Item_SwitchToAnim(item, M_START_ANIM, 0); item->goal_anim_state = M_STATE_STOP; item->current_anim_state = M_STATE_STOP; } } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const bird = item->creature_data; if (item->hit_points <= 0) { switch (item->current_anim_state) { case M_STATE_FALL: if (item->pos.y > item->floor) { item->pos.y = item->floor; item->gravity = false; item->fall_speed = 0; item->goal_anim_state = M_STATE_DEATH; } break; case M_STATE_DEATH: item->pos.y = item->floor; break; default: const int16_t anim_idx = item->object_id == O_CROW ? M_CROW_DIE_ANIM : M_DIE_ANIM; Item_SwitchToAnim(item, anim_idx, 0); item->current_anim_state = M_STATE_FALL; item->gravity = true; item->speed = 0; break; } item->rot.x = 0; Creature_Animate(item_num, 0, 0); return; } AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, false); const int16_t angle = Creature_Turn(item, M_TURN); switch (item->current_anim_state) { case M_STATE_FLY: bird->flags = 0; if (item->required_anim_state != M_STATE_EMPTY) { item->goal_anim_state = item->required_anim_state; } if (bird->mood == MOOD_BORED) { item->goal_anim_state = M_STATE_STOP; } else if (info.ahead && info.distance < M_ATTACK_RANGE) { item->goal_anim_state = M_STATE_ATTACK; } else { item->goal_anim_state = M_STATE_GLIDE; } break; case M_STATE_STOP: case M_STATE_EAT: item->pos.y = item->floor; if (bird->mood != MOOD_BORED) { item->goal_anim_state = M_STATE_FLY; } break; case M_STATE_GLIDE: if (bird->mood == MOOD_BORED) { item->required_anim_state = M_STATE_STOP; item->goal_anim_state = M_STATE_FLY; } else if (info.ahead && info.distance < M_ATTACK_RANGE) { item->goal_anim_state = M_STATE_ATTACK; } break; case M_STATE_ATTACK: if (bird->flags == 0 && item->touch_bits != 0) { Lara_TakeDamage(M_DAMAGE, true); if (item->object_id == O_CROW) { Creature_Effect(item, &m_CrowBite, Spawn_Blood); } else { Creature_Effect(item, &m_BirdBite, Spawn_Blood); } bird->flags = 1; } break; } Creature_Animate(item_num, angle, 0); } static bool M_SetupCommon(OBJECT *const obj) { if (!obj->loaded) { return false; } obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->radius = M_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->pivot_length = 0; obj->lot_setup = LOT_Setup(LOT_SETUP_FLYER); obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; return true; } static void M_SetupEagle(OBJECT *const obj) { if (!M_SetupCommon(obj)) { return; } obj->hit_points = M_EAGLE_HITPOINTS; } static void M_SetupCrow(OBJECT *const obj) { if (!M_SetupCommon(obj)) { return; } obj->hit_points = M_CROW_HITPOINTS; } static void M_SetupVulture(OBJECT *const obj) { if (!M_SetupCommon(obj)) { return; } obj->hit_points = M_VULTURE_HITPOINTS; } REGISTER_OBJECT(O_EAGLE, M_SetupEagle) REGISTER_OBJECT(O_CROW, M_SetupCrow) REGISTER_OBJECT(O_VULTURE, M_SetupVulture) ================================================ FILE: src/trx/game/objects/creatures/bird_guardian.c ================================================ #include #include #include #include #include #include #include // clang-format off #define BIRD_GUARDIAN_HITPOINTS 200 #define BIRD_GUARDIAN_TOUCH_BITS_L 0b00001100'00000000'00000000 // = 0x0C0000 #define BIRD_GUARDIAN_TOUCH_BITS_R 0b01100000'00000000'00000000 // = 0x600000 #define BIRD_GUARDIAN_RADIUS (WALL_L / 3) // = 341 #define BIRD_GUARDIAN_WALK_TURN (DEG_1 * 4) // = 728 #define BIRD_GUARDIAN_ATTACK_1_RANGE SQUARE(WALL_L) // = 1048576 #define BIRD_GUARDIAN_ATTACK_2_RANGE SQUARE(WALL_L * 2) // = 4194304 #define BIRD_GUARDIAN_PUNCH_DAMAGE 200 // clang-format on typedef enum { // clang-format off BIRD_GUARDIAN_STATE_EMPTY = 0, BIRD_GUARDIAN_STATE_WAIT = 1, BIRD_GUARDIAN_STATE_WALK = 2, BIRD_GUARDIAN_STATE_AIM_1 = 3, BIRD_GUARDIAN_STATE_PUNCH_1 = 4, BIRD_GUARDIAN_STATE_AIM_2 = 5, BIRD_GUARDIAN_STATE_PUNCH_2 = 6, BIRD_GUARDIAN_STATE_PUNCH_R = 7, BIRD_GUARDIAN_STATE_WAIT_2 = 8, BIRD_GUARDIAN_STATE_DEATH = 9, BIRD_GUARDIAN_STATE_AIM_3 = 10, BIRD_GUARDIAN_STATE_PUNCH_3 = 11, // clang-format on } BIRD_GUARDIAN_STATE; typedef enum { BIRD_GUARDIAN_ANIM_DEATH = 20, } BIRD_GUARDIAN_ANIM; static const BITE m_BirdGuardianBiteL = { .pos = { .x = 0, .y = 224, .z = 0, }, .mesh_num = 19, }; static const BITE m_BirdGuardianBiteR = { .pos = { .x = 0, .y = 224, .z = 0, }, .mesh_num = 22, }; static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t head = 0; int16_t angle = 0; if (item->hit_points > 0) { AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, true); if (info.ahead) { head = info.angle; } angle = Creature_Turn(item, creature->maximum_turn); switch (item->current_anim_state) { case BIRD_GUARDIAN_STATE_WAIT: creature->maximum_turn = 0; if (info.ahead && info.distance < BIRD_GUARDIAN_ATTACK_1_RANGE) { if (Random_GetControl() < 0x4000) { item->goal_anim_state = BIRD_GUARDIAN_STATE_AIM_1; } else { item->goal_anim_state = BIRD_GUARDIAN_STATE_AIM_3; } } else if (info.ahead && creature->mood == MOOD_BORED) { item->goal_anim_state = BIRD_GUARDIAN_STATE_WAIT_2; } else if (info.ahead && creature->mood == MOOD_STALK) { item->goal_anim_state = BIRD_GUARDIAN_STATE_WAIT_2; } else { item->goal_anim_state = BIRD_GUARDIAN_STATE_WALK; } break; case BIRD_GUARDIAN_STATE_WAIT_2: if (creature->mood != MOOD_BORED) { item->goal_anim_state = BIRD_GUARDIAN_STATE_WAIT; } else if (!info.ahead) { item->goal_anim_state = BIRD_GUARDIAN_STATE_WAIT; } break; case BIRD_GUARDIAN_STATE_WALK: creature->maximum_turn = BIRD_GUARDIAN_WALK_TURN; if (info.ahead && info.distance < BIRD_GUARDIAN_ATTACK_2_RANGE) { item->goal_anim_state = BIRD_GUARDIAN_STATE_AIM_2; } else if (info.ahead && creature->mood == MOOD_BORED) { item->goal_anim_state = BIRD_GUARDIAN_STATE_WAIT; } else if (info.ahead && creature->mood == MOOD_STALK) { item->goal_anim_state = BIRD_GUARDIAN_STATE_WAIT; } break; case BIRD_GUARDIAN_STATE_AIM_1: creature->flags = 0; if (info.ahead && info.distance < BIRD_GUARDIAN_ATTACK_1_RANGE) { item->goal_anim_state = BIRD_GUARDIAN_STATE_PUNCH_1; } else { item->goal_anim_state = BIRD_GUARDIAN_STATE_WAIT; } break; case BIRD_GUARDIAN_STATE_AIM_2: creature->flags = 0; if (info.ahead && info.distance < BIRD_GUARDIAN_ATTACK_2_RANGE) { item->goal_anim_state = BIRD_GUARDIAN_STATE_PUNCH_2; } else { item->goal_anim_state = BIRD_GUARDIAN_STATE_WALK; } break; case BIRD_GUARDIAN_STATE_AIM_3: creature->flags = 0; if (info.ahead && info.distance < BIRD_GUARDIAN_ATTACK_1_RANGE) { item->goal_anim_state = BIRD_GUARDIAN_STATE_PUNCH_3; } else { item->goal_anim_state = BIRD_GUARDIAN_STATE_WAIT; } break; case BIRD_GUARDIAN_STATE_PUNCH_1: case BIRD_GUARDIAN_STATE_PUNCH_2: case BIRD_GUARDIAN_STATE_PUNCH_R: case BIRD_GUARDIAN_STATE_PUNCH_3: if ((creature->flags & 1) == 0 && (item->touch_bits & BIRD_GUARDIAN_TOUCH_BITS_R) != 0) { Creature_Effect(item, &m_BirdGuardianBiteR, Spawn_Blood); Lara_TakeDamage(BIRD_GUARDIAN_PUNCH_DAMAGE, true); creature->flags |= 1; } if ((creature->flags & 2) == 0 && (item->touch_bits & BIRD_GUARDIAN_TOUCH_BITS_L) != 0) { Creature_Effect(item, &m_BirdGuardianBiteL, Spawn_Blood); Lara_TakeDamage(BIRD_GUARDIAN_PUNCH_DAMAGE, true); creature->flags |= 2; } break; default: break; } } else if (item->current_anim_state != BIRD_GUARDIAN_STATE_DEATH) { Item_SwitchToAnim(item, BIRD_GUARDIAN_ANIM_DEATH, 0); item->current_anim_state = BIRD_GUARDIAN_STATE_DEATH; } Creature_Head(item, head); Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->hit_points = BIRD_GUARDIAN_HITPOINTS; obj->radius = BIRD_GUARDIAN_RADIUS; if (g_Config.visuals.fix_texture_issues) { obj->shadow_size = UNIT_SHADOW / 2; } obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 14)->rot.y = true; } REGISTER_OBJECT(O_BIRD_GUARDIAN, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/centaur.c ================================================ #include #include #include #include #include #include #include #include #define CENTAUR_PART_DAMAGE 100 #define CENTAUR_REAR_DAMAGE 200 #define CENTAUR_TOUCH 0x30199 #define CENTAUR_DIE_ANIM 8 #define CENTAUR_TURN (DEG_1 * 4) // = 728 #define CENTAUR_REAR_CHANCE 96 #define CENTAUR_REAR_RANGE SQUARE(WALL_L * 3 / 2) // = 2359296 #define CENTAUR_HITPOINTS 120 #define CENTAUR_RADIUS (WALL_L / 3) // = 341 #define CENTAUR_SMARTNESS 0x7FFF typedef enum { CENTAUR_STATE_EMPTY = 0, CENTAUR_STATE_STOP = 1, CENTAUR_STATE_SHOOT = 2, CENTAUR_STATE_RUN = 3, CENTAUR_STATE_AIM = 4, CENTAUR_STATE_DEATH = 5, CENTAUR_STATE_WARNING = 6, } CENTAUR_STATE; static BITE m_CentaurRocket = { .pos = { 11, 415, 41 }, .mesh_num = 13 }; static BITE m_CentaurRear = { .pos = { 50, 30, 0 }, .mesh_num = 5 }; static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const centaur = item->creature_data; int16_t head = 0; int16_t angle = 0; if (item->hit_points <= 0) { if (item->current_anim_state != CENTAUR_STATE_DEATH) { item->current_anim_state = CENTAUR_STATE_DEATH; Item_SwitchToAnim(item, CENTAUR_DIE_ANIM, 0); } } else { AI_INFO info; Creature_AIInfo(item, &info); if (info.ahead) { head = info.angle; } Creature_Mood(item, &info, true); angle = Creature_Turn(item, CENTAUR_TURN); switch (item->current_anim_state) { case CENTAUR_STATE_STOP: centaur->neck_rotation = 0; if (item->required_anim_state) { item->goal_anim_state = item->required_anim_state; } else if (info.bite && info.distance < CENTAUR_REAR_RANGE) { item->goal_anim_state = CENTAUR_STATE_RUN; } else if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = CENTAUR_STATE_AIM; } else { item->goal_anim_state = CENTAUR_STATE_RUN; } break; case CENTAUR_STATE_RUN: if (info.bite && info.distance < CENTAUR_REAR_RANGE) { item->required_anim_state = CENTAUR_STATE_WARNING; item->goal_anim_state = CENTAUR_STATE_STOP; } else if (Creature_CanTargetEnemy(item, &info)) { item->required_anim_state = CENTAUR_STATE_AIM; item->goal_anim_state = CENTAUR_STATE_STOP; } else if (Random_GetControl() < CENTAUR_REAR_CHANCE) { item->required_anim_state = CENTAUR_STATE_WARNING; item->goal_anim_state = CENTAUR_STATE_STOP; } break; case CENTAUR_STATE_AIM: if (item->required_anim_state) { item->goal_anim_state = item->required_anim_state; } else if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = CENTAUR_STATE_SHOOT; } else { item->goal_anim_state = CENTAUR_STATE_STOP; } break; case CENTAUR_STATE_SHOOT: if (item->required_anim_state == CENTAUR_STATE_EMPTY) { item->required_anim_state = CENTAUR_STATE_AIM; int16_t effect_num = Creature_Effect( item, &m_CentaurRocket, Spawn_AtlanteanBomb); if (effect_num != NO_EFFECT) { centaur->neck_rotation = Effect_Get(effect_num)->rot.x; } } break; case CENTAUR_STATE_WARNING: if (item->required_anim_state == CENTAUR_STATE_EMPTY && (item->touch_bits & CENTAUR_TOUCH)) { Creature_Effect(item, &m_CentaurRear, Spawn_Blood); Lara_TakeDamage(CENTAUR_REAR_DAMAGE, true); item->required_anim_state = CENTAUR_STATE_STOP; } break; } } Creature_Head(item, head); Creature_Animate(item_num, angle, 0); if (item->status == IS_DEACTIVATED) { Sound_Effect(SFX_ATLANTEAN_DEATH, &item->pos, SPM_NORMAL); Item_Explode(item_num, -1, CENTAUR_PART_DAMAGE); Item_Kill(item_num); item->status = IS_DEACTIVATED; } } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = Creature_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->shadow_size = UNIT_SHADOW / 3; obj->hit_points = CENTAUR_HITPOINTS; obj->pivot_length = 400; obj->radius = CENTAUR_RADIUS; obj->smartness = CENTAUR_SMARTNESS; obj->lot_setup = LOT_Setup(LOT_SETUP_BEAST); obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_anim = true; obj->save_flags = true; Object_GetBone(obj, 10)->rot.x = true; Object_GetBone(obj, 10)->rot.y = true; } REGISTER_OBJECT(O_CENTAUR, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/centaur_statue.c ================================================ #include #include #include #include #include #include #define STATUE_EXPLODE_DIST (WALL_L * 7 / 2) // = 3584 #define CENTAUR_REARING_ANIM 7 #define CENTAUR_REARING_FRAME 36 typedef struct { int16_t centaur_item_num; } M_PRIV; static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; OBJECT *const obj = Object_Get(O_CENTAUR); if (!obj->loaded) { p->centaur_item_num = NO_ITEM; return; } const int16_t centaur_item_num = Item_CreateLevelItem(); ASSERT(centaur_item_num != NO_ITEM); ITEM *const centaur = Item_Get(centaur_item_num); centaur->object_id = O_CENTAUR; centaur->room_num = item->room_num; centaur->pos.x = item->pos.x; centaur->pos.y = item->pos.y; centaur->pos.z = item->pos.z; centaur->flags = IF_INVISIBLE; centaur->shade.value_1 = -1; Item_Initialise(centaur_item_num); Item_SwitchToAnim(centaur, CENTAUR_REARING_ANIM, CENTAUR_REARING_FRAME); centaur->current_anim_state = Item_GetAnim(centaur)->current_anim_state; centaur->goal_anim_state = centaur->current_anim_state; centaur->rot.y = item->rot.y; p->centaur_item_num = centaur_item_num; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (item->flags & IF_KILLED) { return; } const ITEM *const lara_item = Lara_GetItem(); const int32_t x = lara_item->pos.x - item->pos.x; const int32_t y = lara_item->pos.y - item->pos.y; const int32_t z = lara_item->pos.z - item->pos.z; if (y > -WALL_L && y < WALL_L && SQUARE(x) + SQUARE(z) < SQUARE(STATUE_EXPLODE_DIST)) { Item_Explode(item_num, -1, 0); Item_Kill(item_num); item->status = IS_DEACTIVATED; const M_PRIV *const p = item->priv; if (p->centaur_item_num != NO_ITEM) { ITEM *const centaur = Item_Get(p->centaur_item_num); centaur->touch_bits = 0; Item_AddActive(p->centaur_item_num); LOT_EnableBaddieAI(p->centaur_item_num, true); centaur->status = IS_ACTIVE; Sound_Effect(SFX_EXPLOSION_1, ¢aur->pos, SPM_NORMAL); } else { Sound_Effect(SFX_EXPLOSION_1, &item->pos, SPM_NORMAL); } } } static int16_t M_GetCarrierItemNum(const ITEM *const item) { const M_PRIV *const p = item->priv; return p->centaur_item_num; } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Object_Collision; obj->carrier_item_num_func = M_GetCarrierItemNum; obj->priv_size = sizeof(M_PRIV); obj->save_anim = true; obj->save_flags = true; obj->enable_interpolation = false; } REGISTER_OBJECT(O_CENTAUR_STATUE, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/civilian.c ================================================ #include #include #include #include #include #include #include // clang-format off #define M_RADIUS (WALL_L / 10) // = 102 #define M_HIT_POINTS 15 #define M_PUNCH_1_DAMAGE 40 #define M_PUNCH_3_DAMAGE 50 #define M_TOUCH_BITS 0b00100100'00000000 #define M_ALERT_DIST SQUARE(WALL_L) // = 1048576 #define M_ESCAPE_DIST SQUARE(WALL_L * 3) // = 9437184 #define M_RUN_DIST SQUARE(WALL_L * 2) // = 4194304 #define M_WALK_DIST SQUARE(WALL_L) // = 1048576 #define M_ATTACK_DIST_1 SQUARE(WALL_L / 3) // = 116281 #define M_ATTACK_DIST_2 SQUARE(WALL_L * 2 / 3) // = 465124 #define M_ATTACK_DIST_3 SQUARE(WALL_L) // = 1048576 #define M_WALK_TURN (DEG_1 * 5) // = 910 #define M_RUN_TURN (DEG_1 * 6) // = 1092 // clang-format on typedef enum { M_STATE_NULL, M_STATE_STOP, M_STATE_WALK, M_STATE_PUNCH_3, M_STATE_AIM_3, M_STATE_WAIT, M_STATE_AIM_2, M_STATE_AIM_1, M_STATE_PUNCH_2, M_STATE_PUNCH_1, M_STATE_RUN, M_STATE_DEATH, M_STATE_UP_4, M_STATE_UP_2, M_STATE_UP_3, M_STATE_DOWN_4, } M_STATE; typedef enum { // clang-format off M_ANIM_STOP = 6, M_ANIM_DEATH = 26, M_ANIM_UP_4 = 27, M_ANIM_UP_2 = 28, M_ANIM_UP_3 = 29, M_ANIM_DOWN_4 = 30, // clang-format on } M_ANIM; static const BITE m_Bite = { .pos = { 0, 0, 0 }, .mesh_num = 13, }; static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); Item_SwitchToAnim(item, M_ANIM_STOP, 0); item->current_anim_state = M_STATE_STOP; item->goal_anim_state = M_STATE_STOP; } static bool M_Vault(ITEM *const item, const int16_t angle) { const int32_t vault_result = Creature_Vault(Item_GetIndex(item), angle, 2, 260); switch (vault_result) { case -4: Item_SwitchToAnim(item, M_ANIM_DOWN_4, 0); item->current_anim_state = M_STATE_DOWN_4; return true; case 2: Item_SwitchToAnim(item, M_ANIM_UP_2, 0); item->current_anim_state = M_STATE_UP_2; return true; case 3: Item_SwitchToAnim(item, M_ANIM_UP_3, 0); item->current_anim_state = M_STATE_UP_3; return true; case 4: Item_SwitchToAnim(item, M_ANIM_UP_4, 0); item->current_anim_state = M_STATE_UP_4; return true; default: return false; } } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t angle = 0; int16_t head = 0; int16_t tilt = 0; int16_t torso_x = 0; int16_t torso_y = 0; Creature_TestBoxDamage(item_num); if (item->hit_points <= 0) { if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, M_ANIM_DEATH, 0); item->current_anim_state = M_STATE_DEATH; creature->lot.setup.step = STEP_L; } goto finish; } ITEM *const lara_item = Lara_GetItem(); const LARA_INFO *const lara = Lara_GetLaraInfo(); if (item->ai_bits != 0) { Creature_GetAITarget(creature); } else { creature->enemy = lara_item; } AI_INFO info = {}; Creature_AIInfo(item, &info); AI_INFO lara_info = {}; if (creature->enemy == lara_item) { lara_info.distance = info.distance; lara_info.angle = info.angle; } else { const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; lara_info.angle = Math_Atan(dz, dx) - item->rot.y; lara_info.distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz }); } Creature_UpdateMood(item, &info, true); if (creature->enemy == lara_item && info.distance > M_ESCAPE_DIST && ABS(info.enemy_facing) < 0x3000) { creature->mood = MOOD_ESCAPE; } Creature_ApplyMood(item, &info, true); angle = Creature_Turn(item, creature->maximum_turn); ITEM *const enemy = creature->enemy; creature->enemy = lara_item; if ((item->ai_bits & AI_FOLLOW) == 0 && (item->hit_status || lara_info.distance < M_ALERT_DIST || Creature_CanSeeEnemy(item, &lara_info))) { if (!creature->alerted) { Sound_Effect(SFX_AMERICAN_HOY, &item->pos, SPM_NORMAL); } Creature_AlertAllGuards(item_num); } creature->enemy = enemy; switch (item->current_anim_state) { case M_STATE_STOP: case M_STATE_WAIT: if (item->current_anim_state == M_STATE_WAIT && (creature->alerted || item->goal_anim_state == M_STATE_RUN)) { item->goal_anim_state = M_STATE_STOP; break; } creature->flags = 0; creature->maximum_turn = 0; head = lara_info.angle; if ((item->ai_bits & AI_GUARD) != 0) { head = Creature_AIGuard(creature); if ((Random_GetControl() & 0xFF) == 0) { if (item->current_anim_state == M_STATE_STOP) { item->goal_anim_state = M_STATE_WAIT; } else { item->goal_anim_state = M_STATE_STOP; } } } else if ((item->ai_bits & AI_PATROL_1) != 0) { item->goal_anim_state = M_STATE_WALK; } else if (creature->mood == MOOD_ESCAPE) { if (lara->target != item && info.ahead) { item->goal_anim_state = M_STATE_STOP; } else { item->goal_anim_state = M_STATE_RUN; } } else if ( creature->mood == MOOD_BORED || ((item->ai_bits & AI_FOLLOW) != 0 && (creature->reached_goal || lara_info.distance > M_RUN_DIST))) { if (item->required_anim_state != M_STATE_NULL) { item->goal_anim_state = item->required_anim_state; } else if (info.ahead) { item->goal_anim_state = M_STATE_STOP; } else { item->goal_anim_state = M_STATE_RUN; } } else if (info.bite && info.distance < M_ATTACK_DIST_1) { item->goal_anim_state = M_STATE_AIM_1; } else if (info.bite && info.distance < M_ATTACK_DIST_2) { item->goal_anim_state = M_STATE_AIM_2; } else if (info.bite && info.distance < M_WALK_DIST) { item->goal_anim_state = M_STATE_WALK; } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_WALK: head = lara_info.angle; creature->maximum_turn = M_WALK_TURN; if ((item->ai_bits & AI_PATROL_1) != 0) { item->goal_anim_state = M_STATE_WALK; head = 0; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if (creature->mood == MOOD_BORED) { if (Random_GetControl() < 256) { item->required_anim_state = M_STATE_WAIT; item->goal_anim_state = M_STATE_STOP; } } else if (info.bite && info.distance < M_ATTACK_DIST_1) { item->goal_anim_state = M_STATE_STOP; } else if (info.bite && info.distance < M_ATTACK_DIST_3) { item->goal_anim_state = M_STATE_AIM_3; } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_RUN: if (info.ahead) { head = info.angle; } creature->maximum_turn = M_RUN_TURN; tilt = angle / 2; if ((item->ai_bits & AI_GUARD) != 0) { item->goal_anim_state = M_STATE_WAIT; } else if (creature->mood == MOOD_ESCAPE) { if (lara->target != item && info.ahead) { item->goal_anim_state = M_STATE_STOP; } } else if ( (item->ai_bits & AI_FOLLOW) != 0 && (creature->reached_goal || lara_info.distance > M_RUN_DIST)) { item->goal_anim_state = M_STATE_STOP; } else if (creature->mood == MOOD_BORED) { item->goal_anim_state = M_STATE_WALK; } else if (info.ahead && info.distance < M_WALK_DIST) { item->goal_anim_state = M_STATE_WALK; } break; case M_STATE_AIM_1: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } creature->maximum_turn = M_WALK_TURN; creature->flags = 0; if (info.bite && info.distance < M_ATTACK_DIST_1) { item->goal_anim_state = M_STATE_PUNCH_1; } else { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_AIM_2: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } creature->maximum_turn = M_WALK_TURN; creature->flags = 0; if (info.ahead && info.distance < M_ATTACK_DIST_2) { item->goal_anim_state = M_STATE_PUNCH_2; } else { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_AIM_3: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } creature->maximum_turn = M_WALK_TURN; creature->flags = 0; if (info.bite && info.distance < M_ATTACK_DIST_3) { item->goal_anim_state = M_STATE_PUNCH_3; } else { item->goal_anim_state = M_STATE_WALK; } break; case M_STATE_PUNCH_1: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } creature->maximum_turn = M_WALK_TURN; if (creature->flags == 0 && (item->touch_bits & M_TOUCH_BITS) != 0) { Lara_TakeDamage(M_PUNCH_1_DAMAGE, true); Creature_Effect(item, &m_Bite, Spawn_Blood); Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL); creature->flags = 1; } break; case M_STATE_PUNCH_2: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } creature->maximum_turn = M_WALK_TURN; if (creature->flags == 0 && (item->touch_bits & M_TOUCH_BITS) != 0) { Lara_TakeDamage(M_PUNCH_1_DAMAGE, true); Creature_Effect(item, &m_Bite, Spawn_Blood); Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL); creature->flags = 1; } if (info.ahead && info.distance > M_ATTACK_DIST_2 && info.distance < M_ATTACK_DIST_3) { item->goal_anim_state = M_STATE_PUNCH_3; } break; case M_STATE_PUNCH_3: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } creature->maximum_turn = M_WALK_TURN; if (creature->flags != 2 && (item->touch_bits & M_TOUCH_BITS) != 0) { Lara_TakeDamage(M_PUNCH_3_DAMAGE, true); Creature_Effect(item, &m_Bite, Spawn_Blood); Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL); creature->flags = 2; } break; } finish: Creature_Tilt(item, tilt); Creature_Joint(item, 0, torso_y); Creature_Joint(item, 1, torso_x); Creature_Joint(item, 2, head); if (item->current_anim_state >= M_STATE_DEATH) { creature->maximum_turn = 0; Creature_Animate(item_num, angle, 0); } else if (M_Vault(item, angle)) { creature->maximum_turn = 0; } } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->collision_func = Creature_Collision; obj->control_func = M_Control; obj->shadow_size = UNIT_SHADOW / 2; obj->radius = M_RADIUS; obj->hit_points = M_HIT_POINTS; obj->lot_setup = LOT_Setup(LOT_SETUP_CLIMBER); obj->intelligent = true; obj->save_anim = true; obj->save_flags = true; obj->save_hitpoints = true; obj->save_position = true; Object_GetBone(obj, 6)->rot.x = true; Object_GetBone(obj, 6)->rot.y = true; Object_GetBone(obj, 13)->rot.y = true; } REGISTER_OBJECT(O_CIVILIAN, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/claw_mutant.c ================================================ #include #include #include #include #include #include #include #include #include #include #include // clang-format off #define M_RADIUS STEP_L #define M_HIT_POINTS 130 #define M_DAMAGE 100 #define M_TOUCH_BITS 0b00000000'10010000 #define M_ALERT_DIST SQUARE(WALL_L) // = 1048576 #define M_ATTACK_DIST_1 SQUARE(WALL_L) // = 1048576 #define M_ATTACK_DIST_2 SQUARE(WALL_L * 2) // = 4194304 #define M_ATTACK_DIST_3 SQUARE(WALL_L * 4 / 3) // = 1864135 #define M_FIRE_DIST SQUARE(WALL_L * 3) // = 9437184 #define M_WALK_TURN (DEG_1 * 3) // = 546 #define M_RUN_TURN (DEG_1 * 4) // = 728 #define M_PLASMA_FRAME 28 // clang-format on typedef enum { M_STATE_STOP, M_STATE_WALK, M_STATE_RUN, M_STATE_RUN_ATTACK, M_STATE_WALK_ATTACK_1, M_STATE_WALK_ATTACK_2, M_STATE_SLASH_LEFT, M_STATE_SLASH_RIGHT, M_STATE_DEATH, M_STATE_CLAW_ATTACK, M_STATE_FIRE_ATTACK, } M_STATE; typedef enum { M_ANIM_DEATH = 20, } M_ANIM; typedef struct { bool recently_fired; } M_PRIV; static const BITE m_ClawLeft = { .pos = { .x = 19, .y = -13, .z = 3 }, .mesh_num = 7, }; static const BITE m_ClawRight = { .pos = { .x = 19, .y = -13, .z = 3 }, .mesh_num = 4, }; static const BITE m_PlasmaEmitter = { .pos = { .x = -32, .y = -16, .z = -192 }, .mesh_num = 13, }; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "recently_fired", &p->recently_fired)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "recently_fired", p->recently_fired); } static void M_TriggerPlasmaCharge(const int16_t item_num) { const ITEM *const item = Item_Get(item_num); const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; const int32_t max_dist = 16 * WALL_L; if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 48; spark->src_color.g = (Random_GetControl() & 0x1F) + 48; spark->src_color.b = 255; spark->dst_color.r = 32; spark->dst_color.g = (Random_GetControl() & 0x3F) + 128; spark->dst_color.b = (Random_GetControl() & 0x3F) + 192; spark->fade_to_black = 8; spark->col_fade_speed = (Random_GetControl() & 3) + 12; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->life = (Random_GetControl() & 7) + 24; spark->s_life = spark->life; spark->dynamic = -1; spark->friction = 3; spark->pos.x = (Random_GetControl() & 0xF) - 8; spark->pos.y = 0; spark->pos.z = (Random_GetControl() & 0xF) - 8; spark->vel.x = (Random_GetControl() & 0x1F) - 16; spark->vel.y = (Random_GetControl() & 0xF) + 16; spark->vel.z = (Random_GetControl() & 0x1F) - 16; if ((Random_GetControl() & 1) != 0) { spark->flags = SPARK_F_ATTACHED_NODE | SPARK_F_ALT_SPRITE | SPARK_F_ITEM | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = Random_GetControl() & 0xFFF; if ((Random_GetControl() & 1) != 0) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = (Random_GetControl() & 0xF) + 16; } } else { spark->flags = SPARK_F_ATTACHED_NODE | SPARK_F_ALT_SPRITE | SPARK_F_ITEM | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->gravity = (Random_GetControl() & 0x1F) + 16; spark->node_num = 6; spark->max_y_vel = (Random_GetControl() & 7) + 16; spark->effect_num = item_num; spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->scalar = 1; spark->size.width = (Random_GetControl() & 0x1F) + 64; spark->src_size.width = spark->size.width; spark->dst_size.width = spark->size.width >> 2; spark->size.height = spark->size.width; spark->src_size.height = spark->size.height; spark->dst_size.height = spark->size.height >> 2; Sparks_FinishSetup(spark); } static void M_TriggerLight(const ITEM *const item) { const int16_t frame_idx = Item_GetRelativeFrame(item); int32_t scale = 0; if (frame_idx > 16) { const ANIM *const anim = Item_GetAnim(item); const int16_t temp = anim->frame_base - item->frame_num + 44; scale = MIN(temp, 16); } else { scale = frame_idx; } if (scale <= 0) { return; } const int32_t rnd = Random_GetControl(); const RGB_888 color = { .r = (scale * (rnd & 0x3F)) >> 4, .g = (scale * (192 - ((rnd >> 6) & 0x1F))) >> 4, .b = (scale * (255 - ((rnd >> 4) & 0x1F))) >> 4, }; XYZ_32 pos = m_PlasmaEmitter.pos; Collide_GetJointAbsPosition(item, &pos, m_PlasmaEmitter.mesh_num); Output_AddDynamicLightRGB(pos, 13, color); } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; CREATURE *const creature = item->creature_data; int16_t tilt = 0; int16_t angle = 0; int16_t head = 0; int16_t torso_x = 0; int16_t torso_y = 0; if (item->hit_points <= 0) { if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, M_ANIM_DEATH, 0); item->current_anim_state = M_STATE_DEATH; } else if (Item_TestFrameEqual(item, -1)) { Creature_Die(item_num, true); for (int32_t i = 0; i < 3; i++) { const int32_t dynamic = i == 0 ? -2 : -1; Sparks_TriggerExplosionSparks(item->pos, 3, dynamic, 2, 0); } Sound_Effect(SFX_EXPLOSION_2, &item->pos, SPM_NORMAL); return; } goto finish; } if (item->ai_bits != 0) { Creature_GetAITarget(creature); } AI_INFO info = {}; Creature_AIInfo(item, &info); ITEM *const lara_item = Lara_GetItem(); AI_INFO lara_info = {}; if (creature->enemy == lara_item) { lara_info.angle = info.angle; lara_info.distance = info.distance; } else { const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; lara_info.angle = Math_Atan(dz, dx) - item->rot.y; lara_info.distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz }); } const bool violent = info.zone_num == info.enemy_zone_num; Creature_Mood(item, &info, violent); angle = Creature_Turn(item, creature->maximum_turn); ITEM *const enemy = creature->enemy; creature->enemy = lara_item; if (item->hit_status || lara_info.distance < M_ALERT_DIST || Creature_CanSeeEnemy(item, &lara_info)) { Creature_AlertAllGuards(item_num); } creature->enemy = enemy; switch (item->current_anim_state) { case M_STATE_STOP: creature->maximum_turn = 0; creature->flags = 0; head = info.angle; if ((item->ai_bits & AI_GUARD) != 0) { head = Creature_AIGuard(creature); item->goal_anim_state = M_STATE_STOP; } else if ((item->ai_bits & AI_PATROL_1) != 0) { item->goal_anim_state = M_STATE_WALK; head = 0; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if (info.bite && info.distance < M_ATTACK_DIST_1) { torso_x = info.x_angle; torso_y = info.angle; if (info.angle < 0) { item->goal_anim_state = M_STATE_SLASH_LEFT; } else { item->goal_anim_state = M_STATE_SLASH_RIGHT; } } else if (info.bite && info.distance < M_ATTACK_DIST_3) { torso_x = info.x_angle; torso_y = info.angle; item->goal_anim_state = M_STATE_CLAW_ATTACK; } else if ( Creature_CanTargetEnemy(item, &info) && ((info.distance > M_FIRE_DIST && !p->recently_fired) || info.zone_num != info.enemy_zone_num)) { item->goal_anim_state = M_STATE_FIRE_ATTACK; } else if (creature->mood == MOOD_BORED) { Random_GetControl(); item->goal_anim_state = M_STATE_WALK; } else if (item->required_anim_state) { item->goal_anim_state = item->required_anim_state; } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_WALK: if (info.ahead) { head = info.angle; } creature->maximum_turn = M_WALK_TURN; if ((item->ai_bits & AI_PATROL_1) != 0) { item->goal_anim_state = M_STATE_WALK; head = 0; } else if (info.bite && info.distance < M_ATTACK_DIST_3) { if (info.angle < 0) { item->goal_anim_state = M_STATE_WALK_ATTACK_1; } else { item->goal_anim_state = M_STATE_WALK_ATTACK_2; } } else if ( Creature_CanTargetEnemy(item, &info) && ((info.distance > M_FIRE_DIST && !p->recently_fired) || info.zone_num != info.enemy_zone_num)) { item->goal_anim_state = M_STATE_STOP; } else if ( creature->mood == MOOD_ESCAPE || creature->mood == MOOD_ATTACK) { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_RUN: if (info.ahead) { head = info.angle; } creature->maximum_turn = M_RUN_TURN; if ((item->ai_bits & AI_GUARD) != 0 || creature->mood == MOOD_BORED || (creature->flags != 0 && info.ahead)) { item->goal_anim_state = M_STATE_STOP; } else if (info.bite && info.distance < M_ATTACK_DIST_2) { if (lara_item->speed != 0) { item->goal_anim_state = M_STATE_RUN_ATTACK; } else { item->goal_anim_state = M_STATE_STOP; } } else if ( Creature_CanTargetEnemy(item, &info) && ((info.distance > M_FIRE_DIST && !p->recently_fired) || info.zone_num != info.enemy_zone_num)) { creature->maximum_turn = M_WALK_TURN; item->goal_anim_state = M_STATE_STOP; } creature->flags = 0; break; case M_STATE_RUN_ATTACK: case M_STATE_WALK_ATTACK_1: case M_STATE_WALK_ATTACK_2: case M_STATE_SLASH_LEFT: case M_STATE_SLASH_RIGHT: case M_STATE_CLAW_ATTACK: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } if (creature->flags == 0 && (item->touch_bits & M_TOUCH_BITS) != 0) { Lara_TakeDamage(100, true); Creature_Effect(item, &m_ClawLeft, Spawn_Blood); Creature_Effect(item, &m_ClawRight, Spawn_Blood); creature->flags = 1; } p->recently_fired = false; break; case M_STATE_FIRE_ATTACK: if (ABS(info.angle) < M_WALK_TURN) { item->rot.y += info.angle; } else if (info.angle < 0) { item->rot.y -= M_WALK_TURN; } else { item->rot.y += M_WALK_TURN; } if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle >> 1; } const int16_t frame_idx = Item_GetRelativeFrame(item); if (frame_idx == 0 && (Random_GetControl() & 3) == 0) { p->recently_fired = true; } if (frame_idx < M_PLASMA_FRAME) { M_TriggerPlasmaCharge(item_num); } else if (frame_idx == M_PLASMA_FRAME) { ClawMutant_TriggerPlasmaBall(item, nullptr, item->room_num); } M_TriggerLight(item); break; } finish: Creature_Tilt(item, tilt); Creature_Joint(item, 0, torso_x); Creature_Joint(item, 1, torso_y); Creature_Joint(item, 2, head); Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->collision_func = Creature_Collision; obj->control_func = M_Control; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->shadow_size = UNIT_SHADOW / 2; obj->radius = M_RADIUS; obj->hit_points = M_HIT_POINTS; obj->intelligent = true; obj->save_anim = true; obj->save_flags = true; obj->save_hitpoints = true; obj->save_position = true; Object_GetBone(obj, 0)->rot.x = true; Object_GetBone(obj, 0)->rot.z = true; Object_GetBone(obj, 7)->rot.y = true; } REGISTER_OBJECT(O_CLAW_MUTANT, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/claw_mutant_internal.h ================================================ #pragma once #include void ClawMutant_TriggerPlasmaBall( const ITEM *item, const XYZ_32 *pos, int16_t room_num); ================================================ FILE: src/trx/game/objects/creatures/claw_mutant_plasma_ball.c ================================================ #include #include #include #include #include #include #include #include #define M_DAMAGE 200 typedef enum { M_TYPE_ATTACHED, M_TYPE_DETACHED, } M_TYPE; static const BITE m_Bite = { .pos = { .x = -32, .y = -16, .z = -192 }, .mesh_num = 13, }; static const uint8_t m_Falloffs[2] = { 13, 7 }; static void M_TriggerPlasmaBallFlame( const int16_t effect_num, const M_TYPE type, const XYZ_32 vel) { const EFFECT *const effect = Effect_Get(effect_num); const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - effect->pos.x; const int32_t dz = lara_item->pos.z - effect->pos.z; const int32_t max_dist = 16 * WALL_L; if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 48; spark->src_color.g = (Random_GetControl() & 0x1F) + 48; spark->src_color.b = 255; spark->dst_color.r = 32; spark->dst_color.g = (Random_GetControl() & 0x3F) + 128; spark->dst_color.b = (Random_GetControl() & 0x3F) + 192; spark->fade_to_black = 8; spark->col_fade_speed = (Random_GetControl() & 3) + 12; spark->draw_type = DRAW_BLEND_ADD; spark->life = (Random_GetControl() & 7) + 24; spark->s_life = spark->life; spark->extras = 0; spark->dynamic = -1; spark->pos.x = (Random_GetControl() & 0xF) - 8; spark->pos.y = 0; spark->pos.z = (Random_GetControl() & 0xF) - 8; spark->vel.x = vel.x + (Random_GetControl() & 0xFF) - 128; spark->vel.y = vel.y; spark->vel.z = vel.z + (Random_GetControl() & 0xFF) - 128; spark->friction = 5; if ((Random_GetControl() & 1) != 0) { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = Random_GetControl() & 0xFFF; if ((Random_GetControl() & 1) != 0) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = +16 + (Random_GetControl() & 0xF); } } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->effect_num = effect_num; spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->scalar = 1; spark->size.width = (Random_GetControl() & 0x1F) + 64; spark->src_size.width = spark->size.width; spark->dst_size.width = spark->size.width >> 2; spark->size.height = spark->size.width; spark->src_size.height = spark->size.height; spark->dst_size.height = spark->size.height >> 2; if (type == M_TYPE_ATTACHED) { spark->scalar = 2; spark->vel.x <<= 2; spark->vel.y = (Random_GetControl() & 0x1FF) - 256; spark->vel.z <<= 2; spark->friction = 85; spark->dst_size.width >>= 1; spark->dst_size.height >>= 1; } spark->max_y_vel = 0; spark->gravity = 0; Sparks_FinishSetup(spark); } static void M_Control(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); const XYZ_32 old_pos = effect->pos; const M_TYPE type = effect->flag1; if (effect->speed < 384 && type == M_TYPE_ATTACHED) { effect->speed += (effect->speed >> 3) + 4; } if (type == M_TYPE_DETACHED) { effect->fall_speed++; if (effect->speed > 8) { effect->speed -= 2; } if (effect->rot.x > -0x3C00) { effect->rot.x -= 0x100; } } const int32_t speed = (effect->speed * Math_Cos(effect->rot.x)) >> W2V_SHIFT; effect->pos = XYZ_32_OffsetYaw(effect->pos, effect->rot.y, speed); effect->pos.y += effect->fall_speed - ((effect->speed * Math_Sin(effect->rot.x)) >> W2V_SHIFT); const int32_t time4 = Output_GetTimeInGame() * 4; if ((time4 & 4) != 0) { XYZ_32 vel = {}; if (type == M_TYPE_DETACHED) { vel.y = ABS(old_pos.y - effect->pos.y) << 3; } M_TriggerPlasmaBallFlame(effect_num, type, vel); } int16_t room_num = effect->room_num; const SECTOR *const sector = Room_GetSector(effect->pos, &room_num); const int32_t height = Room_GetHeight(sector, effect->pos); const int32_t ceiling = Room_GetCeiling(sector, effect->pos); if (effect->pos.y >= height || effect->pos.y < ceiling) { if (type == M_TYPE_ATTACHED && !Room_Get(room_num)->flags.underwater) { const int32_t rnd = (Random_GetControl() & 3) + 5; for (int32_t i = 0; i < rnd; i++) { ClawMutant_TriggerPlasmaBall( nullptr, &old_pos, effect->room_num); } } Effect_Kill(effect_num); return; } if (type == M_TYPE_ATTACHED && Lara_IsNearItem(&effect->pos, 200)) { const int32_t rnd = (Random_GetControl() & 1) + 3; for (int32_t i = 0; i < rnd; i++) { ClawMutant_TriggerPlasmaBall( nullptr, &effect->pos, effect->room_num); } Lara_TakeDamage(M_DAMAGE, true); Effect_Kill(effect_num); return; } if (effect->room_num != room_num) { Effect_UpdateRoom(effect_num, room_num); } const int32_t color_base = Random_GetControl(); const RGB_888 color = { .r = color_base & 0x3F, .g = 192 - ((color_base >> 6) & 0x1F), .b = 255 - ((color_base >> 4) & 0x1F), }; Output_AddDynamicLightRGB(effect->pos, m_Falloffs[type], color); } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->draw_func = nullptr; } void ClawMutant_TriggerPlasmaBall( const ITEM *const item, const XYZ_32 *const pos, const int16_t room_num) { const M_TYPE type = item == nullptr ? M_TYPE_DETACHED : M_TYPE_ATTACHED; XYZ_32 spawn_pos; int16_t angles[2]; int16_t speed; if (type == M_TYPE_DETACHED) { ASSERT(pos != nullptr); spawn_pos = *pos; angles[0] = Random_GetControl() << 1; angles[1] = DEG_45; speed = (Random_GetControl() & 0xF) + 16; } else { spawn_pos = m_Bite.pos; Collide_GetJointAbsPosition(item, &spawn_pos, m_Bite.mesh_num); const ITEM *const lara_item = Lara_GetItem(); Math_GetVectorAngles( lara_item->pos.x - spawn_pos.x, lara_item->pos.y - spawn_pos.y - STEP_L, lara_item->pos.z - spawn_pos.z, angles); angles[0] = item->rot.y; speed = (Random_GetControl() & 7) + 8; } const int16_t effect_num = Effect_Create(room_num); if (effect_num == NO_ITEM) { return; } EFFECT *const effect = Effect_Get(effect_num); effect->pos = spawn_pos; effect->rot.x = angles[1]; effect->rot.y = angles[0]; effect->object_id = O_CLAW_MUTANT_PLASMA_BALL; effect->speed = speed; effect->fall_speed = 0; effect->flag1 = type; } REGISTER_OBJECT(O_CLAW_MUTANT_PLASMA_BALL, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/cobra.c ================================================ #include #include #include #include #include #include #include static BITE m_CobraBite = { .pos = { 0, 0, 0 }, .mesh_num = 13 }; typedef enum { COBRA_STATE_WAKING_UP = 0, COBRA_STATE_ALERT = 1, COBRA_STATE_BITE = 2, COBRA_STATE_SLEEP = 3, COBRA_STATE_DEATH = 4, } M_COBRA_STATE; typedef enum { COBRA_ANIM_SLEEP = 2, COBRA_ANIM_DEATH = 4, } M_COBRA_ANIM; static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); Creature_Initialise(item_num); Item_SwitchToAnim(item, COBRA_ANIM_SLEEP, 45); item->current_anim_state = COBRA_STATE_SLEEP; item->goal_anim_state = COBRA_STATE_SLEEP; } static bool M_IsTargetable(const ITEM *const item) { return item->hit_points > 0 && item->status == IS_ACTIVE && item->current_anim_state != COBRA_STATE_SLEEP; } static bool M_CanTakeDamage(const ITEM *const item) { return item->hit_points > 0; } static bool M_CanBeProjectileTarget(const ITEM *const item) { return item->hit_points > 0 && item->collidable; } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } int32_t forget_radius = SQUARE(3 * WALL_L); int32_t alert_radius = SQUARE(1.5 * WALL_L); int32_t attack_radius = SQUARE(WALL_L); // TODO: do not hardcode this if (g_TRVersion == 3 && GF_BadGetLevelNum() >= 9) { forget_radius = SQUARE(2.5 * WALL_L); alert_radius = SQUARE(1.25 * WALL_L); attack_radius = SQUARE(682); } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; if (creature == nullptr) { return; } int16_t angle = 0; if (item->hit_points <= 0) { if (item->current_anim_state != COBRA_STATE_DEATH) { Item_SwitchToAnim(item, COBRA_ANIM_DEATH, 0); item->current_anim_state = COBRA_ANIM_DEATH; } goto finish; } const ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara = Lara_GetLaraInfo(); AI_INFO info; Creature_AIInfo(item, &info); info.angle += 3072; creature->target.x = lara_item->pos.x; creature->target.z = lara_item->pos.z; angle = Creature_Turn(item, creature->maximum_turn); if (ABS(info.angle) < DEG_1 * 10) { item->rot.y += info.angle; } else if (info.angle < 0) { item->rot.y -= DEG_1 * 10; } else { item->rot.y += DEG_1 * 10; } switch (item->current_anim_state) { case COBRA_STATE_WAKING_UP: break; case COBRA_STATE_ALERT: creature->flags = 0; if (info.distance > forget_radius) { item->goal_anim_state = COBRA_STATE_SLEEP; } else if ( lara_item->hit_points > 0 && ((info.ahead && info.distance < attack_radius) || item->hit_status || lara_item->speed > 15)) { item->goal_anim_state = COBRA_STATE_BITE; } break; case COBRA_STATE_BITE: if (creature->flags != 1 && (item->touch_bits & 0x2000) != 0) { creature->flags = 1; Lara_TakeDamage(80, true); lara->poison_timer = 256; Creature_Effect(item, &m_CobraBite, Spawn_Blood); } break; case COBRA_STATE_SLEEP: creature->flags = 0; if (info.distance < alert_radius && lara_item->hit_points > 0) { item->goal_anim_state = COBRA_STATE_WAKING_UP; } break; } finish: Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->is_targetable_func = M_IsTargetable; obj->can_take_damage_func = M_CanTakeDamage; obj->can_be_projectile_target_func = M_CanBeProjectileTarget; obj->shadow_size = UNIT_SHADOW / 2; obj->hit_points = 8; obj->radius = 102; // obj->non_lot = true; // TODO(TR3) obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 0)->rot.y = true; Object_GetBone(obj, 6)->rot.y = true; } REGISTER_OBJECT(O_COBRA, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/compy.c ================================================ #include #include #include #include #include #include #include #include #include #include // clang-format off #define M_RUN_TURN (10 * DEG_1) #define M_STOP_TURN (3 * DEG_1) #define M_UPSET_SPEED 15 #define M_HIT_RANGE SQUARE(WALL_L / 3) #define M_ATTACK_ANGLE 0x3000 #define M_JUMP_CHANCE 0x1000 #define M_ATTACK_CHANCE 0x1F #define M_TOUCH_BITS 0x04 #define M_HIT_FLAG 1 // clang-format on typedef enum { M_STATE_STOP, M_STATE_RUN, M_STATE_JUMP, M_STATE_ATTACK, M_STATE_DEATH, } M_STATE; typedef enum { M_ANIM_DEATH = 6, } M_ANIM; typedef struct { int32_t scared_timer; bool attack_lara; } M_SHARED_PRIV; typedef struct { bool attack_lara; int32_t scared_timer; int32_t carcass_item_num; M_SHARED_PRIV *shared; } M_PRIV; static M_SHARED_PRIV m_SharedPriv = {}; static BITE m_Bite = { .pos = { .x = 0, .y = 0, .z = 0 }, .mesh_num = 2, }; static bool M_FindCarcass(ITEM *const item) { M_PRIV *const p = item->priv; p->carcass_item_num = Item_FindTypeInRoom(item->room_num, O_ANIMATING_6); return p->carcass_item_num != NO_ITEM; } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; Creature_Initialise(item_num); p->carcass_item_num = NO_ITEM; p->shared = &m_SharedPriv; p->shared->attack_lara = false; } static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "scared_timer", &p->scared_timer)); JSON_SHOULD(JSON_READ(io, "attack_lara", &p->attack_lara)); JSON_SHOULD(JSON_READ(io, "shared_scared_timer", &p->shared->scared_timer)); JSON_SHOULD(JSON_READ(io, "shared_attack_lara", &p->shared->attack_lara)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "scared_timer", p->scared_timer); JSONW_WRITE(io, "attack_lara", p->attack_lara); JSONW_WRITE(io, "shared_scared_timer", p->shared->scared_timer); JSONW_WRITE(io, "shared_attack_lara", p->shared->attack_lara); } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; CREATURE *const creature = item->creature_data; int16_t angle = 0; int16_t torso = 0; int16_t head = 0; if (p->carcass_item_num == NO_ITEM) { M_FindCarcass(item); } if (item->hit_points <= 0) { if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, M_ANIM_DEATH, 0); item->current_anim_state = M_STATE_DEATH; } goto finish; } AI_INFO info; Creature_AIInfo(item, &info); if (creature->mood == MOOD_BORED && p->carcass_item_num != NO_ITEM) { ITEM *const raptor = Item_Get(p->carcass_item_num); const int32_t dx = raptor->pos.x - item->pos.x; const int32_t dz = raptor->pos.z - item->pos.z; info.distance = SQUARE(dx) + SQUARE(dz); info.angle = Math_Atan(dz, dx) - item->rot.y; info.ahead = info.angle > -DEG_90 && info.angle < DEG_90; } const int16_t bits = (item_num & 7) * 0x200 - 0x700; ITEM *const lara_item = Lara_GetItem(); if (p->shared->scared_timer == 0 && !p->shared->attack_lara && ((info.enemy_facing < M_ATTACK_ANGLE && info.enemy_facing > -M_ATTACK_ANGLE && lara_item->speed > M_UPSET_SPEED) || lara_item->current_anim_state == LS(LS_ROLL) || item->hit_status)) { p->scared_timer = (bits + 0x700) >> 7; p->shared->scared_timer = 280; } else if (p->shared->scared_timer > 0) { if (p->scared_timer > 0) { p->scared_timer--; } else { creature->mood = MOOD_ESCAPE; p->shared->scared_timer--; } if (Random_GetControl() < M_ATTACK_CHANCE && item->timer > 180) { p->shared->attack_lara = true; } } else if (info.zone_num == info.enemy_zone_num) { creature->mood = MOOD_ATTACK; } else { creature->mood = MOOD_BORED; } switch (creature->mood) { case MOOD_ATTACK: creature->target = XYZ_32_OffsetYaw(creature->enemy->pos, bits + item->rot.y, WALL_L); break; case MOOD_ESCAPE: case MOOD_STALK: { creature->target = XYZ_32_OffsetYaw(item->pos, bits + info.angle + DEG_180, WALL_L); int16_t room_num = item->room_num; SECTOR *const sector = Room_GetSector(creature->target, &room_num); if (ABS(Box_GetBox(sector->box)->height - item->pos.y) > STEP_L) { creature->mood = MOOD_BORED; p->scared_timer = p->shared->scared_timer; } break; } case MOOD_BORED: if (p->carcass_item_num != NO_ITEM) { ITEM *const raptor = Item_Get(p->carcass_item_num); creature->target.x = raptor->pos.x; creature->target.z = raptor->pos.z; } break; } angle = Creature_Turn(item, creature->maximum_turn); torso = info.ahead ? info.angle : 0; head = -(info.angle / 4); item->timer++; if (item->hit_status && item->timer > 200 && Random_GetControl() < 0xC1C) { p->shared->attack_lara = true; } switch (item->current_anim_state) { case M_STATE_STOP: creature->flags &= ~M_HIT_FLAG; creature->maximum_turn = M_STOP_TURN; if (creature->mood == MOOD_ATTACK) { if (info.ahead && info.distance < M_HIT_RANGE * 4) { if (!p->shared->attack_lara) { item->goal_anim_state = M_STATE_STOP; } else if (Random_GetControl() < DEG_90) { item->goal_anim_state = M_STATE_ATTACK; } else { item->goal_anim_state = M_STATE_JUMP; } } else if ( info.distance > M_HIT_RANGE * (9 - 4 * p->shared->attack_lara)) { item->goal_anim_state = M_STATE_RUN; } } else if (creature->mood == MOOD_BORED) { if (info.ahead && info.distance < M_HIT_RANGE * 3 && p->carcass_item_num != NO_ITEM) { if (Random_GetControl() < DEG_90) { item->goal_anim_state = M_STATE_ATTACK; } else { item->goal_anim_state = M_STATE_JUMP; } } else if (info.distance > M_HIT_RANGE * 3) { item->goal_anim_state = M_STATE_RUN; } } else if (Random_GetControl() < M_JUMP_CHANCE) { item->goal_anim_state = M_STATE_JUMP; } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_RUN: creature->flags &= ~M_HIT_FLAG; creature->maximum_turn = M_RUN_TURN; if (info.angle < M_ATTACK_ANGLE && info.angle > -M_ATTACK_ANGLE && info.distance < M_HIT_RANGE * (9 - 4 * p->shared->attack_lara)) { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_JUMP: case M_STATE_ATTACK: creature->maximum_turn = M_RUN_TURN; if (!(creature->flags & M_HIT_FLAG)) { if ((item->touch_bits & M_TOUCH_BITS) && p->shared->attack_lara) { creature->flags |= M_HIT_FLAG; Lara_TakeDamage(90, true); Creature_Effect(item, &m_Bite, Spawn_Blood); } else if ( info.distance < M_HIT_RANGE && info.ahead && p->carcass_item_num != NO_ITEM && creature->mood != MOOD_ATTACK) { creature->flags |= M_HIT_FLAG; Creature_Effect(item, &m_Bite, Spawn_Blood); } } break; } finish: Creature_Tilt(item, angle >> 1); Creature_Joint(item, 0, torso); Creature_Joint(item, 1, head); Creature_Animate(item_num, angle, angle >> 1); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->radius = 102; obj->shadow_size = 85; obj->hit_points = 10; obj->pivot_length = 50; // obj->non_lot = true; // TODO(TR3) obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 1)->rot.y = true; Object_GetBone(obj, 2)->rot.y = true; } REGISTER_OBJECT(O_COMPY, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/cowboy.c ================================================ #include #include #include #include #include #include #include #include #define COWBOY_SHOT_DAMAGE 70 #define COWBOY_WALK_TURN (DEG_1 * 3) // = 546 #define COWBOY_RUN_TURN (DEG_1 * 6) // = 1092 #define COWBOY_WALK_RANGE SQUARE(WALL_L * 3) // = 9437184 #define COWBOY_DIE_ANIM 7 #define COWBOY_HITPOINTS 150 #define COWBOY_RADIUS (WALL_L / 10) // = 102 #define COWBOY_SMARTNESS 0x7FFF typedef enum { COWBOY_STATE_EMPTY = 0, COWBOY_STATE_STOP = 1, COWBOY_STATE_WALK = 2, COWBOY_STATE_RUN = 3, COWBOY_STATE_AIM = 4, COWBOY_STATE_DEATH = 5, COWBOY_STATE_SHOOT = 6, } COWBOY_STATE; static const CREATURE_GUN m_CowboyGun1 = { .muzzle = { .pos = { 1, 200, 41 }, .mesh_num = 5 }, }; static const CREATURE_GUN m_CowboyGun2 = { .muzzle = { .pos = { -2, 200, 40 }, .mesh_num = 8 }, }; static void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage) { if (stage == SAVEGAME_STAGE_AFTER_LOAD) { if (item->hit_points <= 0) { const uint16_t flags = Music_GetTrackFlags(Music_ToGameID(MX_COWBOY_SPEECH)); Music_SetTrackFlags( Music_ToGameID(MX_COWBOY_SPEECH), flags | IF_ONE_SHOT); } } } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const cowboy = item->creature_data; int16_t head = 0; int16_t angle = 0; int16_t tilt = 0; if (item->hit_points <= 0) { if (item->current_anim_state != COWBOY_STATE_DEATH) { item->current_anim_state = COWBOY_STATE_DEATH; Item_SwitchToAnim(item, COWBOY_DIE_ANIM, 0); } } else { AI_INFO info; Creature_AIInfo(item, &info); if (info.ahead) { head = info.angle; } Creature_Mood(item, &info, false); angle = Creature_Turn(item, cowboy->maximum_turn); switch (item->current_anim_state) { case COWBOY_STATE_STOP: if (item->required_anim_state) { item->goal_anim_state = item->required_anim_state; } else if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = COWBOY_STATE_AIM; } else if (cowboy->mood == MOOD_BORED) { item->goal_anim_state = COWBOY_STATE_WALK; } else { item->goal_anim_state = COWBOY_STATE_RUN; } break; case COWBOY_STATE_WALK: cowboy->maximum_turn = COWBOY_WALK_TURN; if (cowboy->mood == MOOD_ESCAPE || !info.ahead) { item->required_anim_state = COWBOY_STATE_RUN; item->goal_anim_state = COWBOY_STATE_STOP; } else if (Creature_CanTargetEnemy(item, &info)) { item->required_anim_state = COWBOY_STATE_AIM; item->goal_anim_state = COWBOY_STATE_STOP; } else if (info.distance > COWBOY_WALK_RANGE) { item->required_anim_state = COWBOY_STATE_RUN; item->goal_anim_state = COWBOY_STATE_STOP; } break; case COWBOY_STATE_RUN: cowboy->maximum_turn = COWBOY_RUN_TURN; tilt = angle / 2; if (cowboy->mood != MOOD_ESCAPE || info.ahead) { if (Creature_CanTargetEnemy(item, &info)) { item->required_anim_state = COWBOY_STATE_AIM; item->goal_anim_state = COWBOY_STATE_STOP; } else if (info.ahead && info.distance < COWBOY_WALK_RANGE) { item->required_anim_state = COWBOY_STATE_WALK; item->goal_anim_state = COWBOY_STATE_STOP; } } break; case COWBOY_STATE_AIM: cowboy->flags = 0; if (item->required_anim_state) { item->goal_anim_state = COWBOY_STATE_STOP; } else if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = COWBOY_STATE_SHOOT; } else { item->goal_anim_state = COWBOY_STATE_STOP; } break; case COWBOY_STATE_SHOOT: if (!cowboy->flags) { Creature_Shoot( item, &info, &m_CowboyGun1, head, COWBOY_SHOT_DAMAGE); } else if (cowboy->flags == 6) { if (Creature_CanTargetEnemy(item, &info)) { Creature_Shoot( item, &info, &m_CowboyGun2, head, COWBOY_SHOT_DAMAGE); } else { int16_t effect_num = Creature_Effect( item, &m_CowboyGun2.muzzle, Spawn_GunShot); if (effect_num != NO_EFFECT) { Effect_Get(effect_num)->rot.y += head; } } } cowboy->flags++; if (cowboy->mood == MOOD_ESCAPE) { item->required_anim_state = COWBOY_STATE_RUN; } break; } } Creature_Tilt(item, tilt); Creature_Head(item, head); Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = Creature_Initialise; obj->handle_save_func = M_HandleSave; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->shadow_size = UNIT_SHADOW / 2; obj->hit_points = COWBOY_HITPOINTS; obj->radius = COWBOY_RADIUS; obj->smartness = COWBOY_SMARTNESS; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_anim = true; obj->save_flags = true; Object_GetBone(obj, 0)->rot.y = true; } REGISTER_OBJECT(O_COWBOY, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/crawler_mutant.c ================================================ #include #include #include #include #include #include #include #include #include #include // clang-format off #define M_RADIUS (WALL_L / 5) // = 204 #define M_HIT_POINTS 50 #define M_MAX_POISON 256 #define M_MAX_BURN_TIME 80 #define M_START_BURN_MESH 9 #define M_ALERT_DIST SQUARE(WALL_L) // = 1048576 #define M_ATTACK_DIST SQUARE(WALL_L * 2) // = 4194304 #define M_WALK_TURN (DEG_1 * 3) // = 546 // clang-format on typedef enum { M_STATE_STOP, M_STATE_WALK, M_STATE_BURP, M_STATE_DEATH, } M_STATE; typedef enum { M_ANIM_DEATH = 5, } M_ANIM; typedef struct { int16_t burn_timer; } M_PRIV; static const BITE m_Gas = { .pos = { 0, 48, 140 }, .mesh_num = 10, }; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "burn_timer", &p->burn_timer)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "burn_timer", p->burn_timer); } static void M_TriggerGas( const XYZ_32 pos, const XYZ_32 vel, const int16_t effect_num) { SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = (Random_GetControl() & 0x3F) + 128; spark->src_color.g = (Random_GetControl() & 0x3F) + 128; spark->src_color.b = 32; spark->dst_color.r = (Random_GetControl() & 0xF) + 32; spark->dst_color.g = (Random_GetControl() & 0xF) + 32; spark->dst_color.b = 0; if (vel.x != 0 || vel.y != 0 || vel.z != 0) { spark->col_fade_speed = 6; spark->fade_to_black = 2; spark->life = (Random_GetControl() & 1) + 16; } else { spark->col_fade_speed = 8; spark->fade_to_black = 16; spark->life = (Random_GetControl() & 3) + 28; } spark->s_life = spark->life; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16; spark->pos.y = pos.y; spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16; spark->vel.x = vel.x + (Random_GetControl() & 0xF) - 16; spark->vel.y = vel.y; spark->vel.z = vel.z + (Random_GetControl() & 0xF) - 16; spark->friction = 0; if ((Random_GetControl() & 1) != 0) { if (effect_num < 0) { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->rot_angle = Random_GetControl() & 0xFFF; if ((Random_GetControl() & 1) != 0) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = +16 + (Random_GetControl() & 0xF); } } else if (effect_num < 0) { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE; } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->max_y_vel = 0; spark->effect_num = effect_num; spark->gravity = 0; spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; const int32_t size = (Random_GetControl() & 0x1F) + 48; if (vel.x != 0 || vel.y != 0 || vel.z != 0) { spark->size.width = size >> 5; spark->src_size.width = spark->size.width; spark->dst_size.width = size >> 1; spark->size.height = spark->size.width; spark->src_size.height = spark->size.height; spark->dst_size.height = spark->dst_size.width; if (effect_num == -2) { spark->scalar = 2; } else { spark->scalar = 3; } } else { spark->scalar = 4; spark->size.width = size >> 4; spark->src_size.width = spark->size.width; spark->dst_size.width = size >> 1; spark->size.height = spark->size.width; spark->src_size.height = spark->size.height; spark->dst_size.height = spark->dst_size.width; } Sparks_FinishSetup(spark); } static void M_TriggerGasThrower( const ITEM *const item, const BITE *const bite, const int16_t speed) { const int16_t effect_num = Effect_Create(item->room_num); if (effect_num == NO_ITEM) { return; } XYZ_32 pos_1 = bite->pos; Collide_GetJointAbsPosition(item, &pos_1, bite->mesh_num); XYZ_32 pos_2 = { .x = bite->pos.x, .y = bite->pos.y << 1, .z = bite->pos.z << 3, }; Collide_GetJointAbsPosition(item, &pos_2, bite->mesh_num); int16_t angles[2]; Math_GetVectorAngles( pos_2.x - pos_1.x, pos_2.y - pos_1.y, pos_2.z - pos_1.z, angles); EFFECT *const effect = Effect_Get(effect_num); effect->pos = pos_1; effect->rot.x = angles[1]; effect->rot.y = angles[0]; effect->speed = speed << 2; effect->object_id = O_MISSILE_POISON; effect->counter = 20; effect->flag1 = 1; M_TriggerGas((XYZ_32) {}, (XYZ_32) {}, effect_num); for (int32_t i = 0; i < 2; i++) { const int32_t s = Random_GetControl() % (speed << 2) + 32; const int32_t r = (s * Math_Cos(effect->rot.x)) >> W2V_SHIFT; const XYZ_32 vel = { .x = (r * Math_Sin(effect->rot.y)) >> W2V_SHIFT, .y = -((s * Math_Sin(effect->rot.x)) >> W2V_SHIFT), .z = (r * Math_Cos(effect->rot.y)) >> W2V_SHIFT, }; M_TriggerGas( effect->pos, (XYZ_32) { vel.x << 5, vel.y << 5, vel.z << 5 }, -1); } { const int32_t r = ((speed << 1) * Math_Cos(effect->rot.x)) >> W2V_SHIFT; const XYZ_32 vel = { .x = (r * Math_Sin(effect->rot.y)) >> W2V_SHIFT, .y = -(((speed << 1) * Math_Sin(effect->rot.x)) >> W2V_SHIFT), .z = (r * Math_Cos(effect->rot.y)) >> W2V_SHIFT, }; M_TriggerGas( effect->pos, (XYZ_32) { vel.x << 5, vel.y << 5, vel.z << 5 }, -2); } } static void M_BurnDeath(ITEM *const item) { const OBJECT *const obj = Object_Get(item->object_id); for (int32_t mesh = M_START_BURN_MESH; mesh < obj->mesh_count; mesh++) { XYZ_32 pos = {}; Collide_GetJointAbsPosition(item, &pos, mesh); Sparks_TriggerFireFlame(pos, -1, 255); } int32_t scale = 0; const int16_t frame_idx = Item_GetRelativeFrame(item); if (frame_idx > 16) { const ANIM *const anim = Item_GetAnim(item); const int16_t remaining_frames = anim->frame_end - item->frame_num; scale = MIN(remaining_frames, 16); } else { scale = frame_idx; } const int32_t rnd = Random_GetControl(); const RGB_888 color = { .r = (scale * (255 - ((rnd >> 4) & 0x1F))) >> 4, .g = (scale * (192 - ((rnd >> 6) & 0x3F))) >> 4, .b = (scale * (rnd & 0x3F)) >> 4, }; Output_AddDynamicLightRGB(item->pos, 12, color); } static void M_GasDeath(ITEM *const item) { const ANIM *const anim = Item_GetAnim(item); const int16_t frame_idx = Item_GetRelativeFrame(item); const int16_t end_frame_idx = anim->frame_end - anim->frame_base - 8; if (frame_idx >= 1 && frame_idx <= end_frame_idx) { int16_t speed = frame_idx + 1; if (speed > 24) { const int16_t remaining_frames = end_frame_idx - frame_idx; if (remaining_frames <= 0) { speed = 1; } else if (remaining_frames > 24) { speed = (Random_GetControl() & 0xF) + 8; } else { speed = remaining_frames; } } CLAMPL(speed, 1); M_TriggerGasThrower(item, &m_Gas, speed); } } static void M_CalculateEnemy(ITEM *const item) { CREATURE *const mutant = item->creature_data; ITEM *const lara_item = Lara_GetItem(); mutant->enemy = lara_item; int32_t dx = lara_item->pos.x - item->pos.x; int32_t dz = lara_item->pos.z - item->pos.z; int32_t best_distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz }); for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) { const CREATURE *const creature = LOT_GetBaddieSlot(i); if (creature->item_num == NO_ITEM || creature == mutant) { continue; } const ITEM *const candidate = Item_Get(creature->item_num); if (candidate->object_id != O_LARA && candidate->object_id != O_RX_WORKER_3) { continue; } dx = candidate->pos.x - item->pos.x; dz = candidate->pos.z - item->pos.z; const int32_t distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz }); if (distance < best_distance) { mutant->enemy = (ITEM *)candidate; best_distance = distance; } } } static void M_ControlCrawler(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t tilt = 0; int16_t angle = 0; int16_t head = 0; int16_t torso_x = 0; int16_t torso_y = 0; M_PRIV *const p = item->priv; if (p->burn_timer > M_MAX_BURN_TIME) { item->hit_points = 0; } if (item->hit_points <= 0) { if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, M_ANIM_DEATH, 0); item->current_anim_state = M_STATE_DEATH; creature->flags = 0; } else if (p->burn_timer > M_MAX_BURN_TIME) { M_BurnDeath(item); } else { M_GasDeath(item); } goto finish; } if (item->ai_bits != 0) { Creature_GetAITarget(creature); } else { M_CalculateEnemy(item); } ITEM *const lara_item = Lara_GetItem(); const LARA_INFO *const lara = Lara_GetLaraInfo(); AI_INFO info = {}; Creature_AIInfo(item, &info); AI_INFO lara_info = {}; if (creature->enemy == lara_item) { lara_info.angle = info.angle; lara_info.distance = info.distance; } else { const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; lara_info.angle = Math_Atan(dz, dx) - item->rot.y; lara_info.distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz }); } const bool violent = info.zone_num == info.enemy_zone_num; Creature_UpdateMood(item, &info, violent); if (creature->enemy == lara_item && lara->poison_timer >= M_MAX_POISON) { creature->mood = MOOD_ESCAPE; } Creature_ApplyMood(item, &info, violent); angle = Creature_Turn(item, creature->maximum_turn); ITEM *const enemy = creature->enemy; creature->enemy = lara_item; if ((lara_info.distance < M_ALERT_DIST || item->hit_status || Creature_CanSeeEnemy(item, &lara_info)) && (item->ai_bits & AI_FOLLOW) == 0) { Creature_AlertAllGuards(item_num); } creature->enemy = enemy; switch (item->current_anim_state) { case M_STATE_STOP: creature->maximum_turn = 0; creature->flags = 0; head = info.angle; if ((item->ai_bits & AI_GUARD) != 0) { head = Creature_AIGuard(creature); item->goal_anim_state = M_STATE_STOP; } else if ((item->ai_bits & AI_PATROL_1) != 0) { head = 0; item->goal_anim_state = M_STATE_WALK; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_WALK; } else if ( Creature_CanTargetEnemy(item, &info) && info.distance < M_ATTACK_DIST) { item->goal_anim_state = M_STATE_BURP; } else if (item->required_anim_state != 0) { item->goal_anim_state = item->required_anim_state; } else { item->goal_anim_state = M_STATE_WALK; } break; case M_STATE_WALK: if (info.ahead) { head = info.angle; } creature->maximum_turn = M_WALK_TURN; if ((item->ai_bits & AI_PATROL_1) != 0) { head = 0; item->goal_anim_state = M_STATE_WALK; } else if ( Creature_CanTargetEnemy(item, &info) && info.distance < M_ATTACK_DIST) { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_BURP: if (ABS(info.angle) < M_WALK_TURN) { item->rot.y += info.angle; } else if (info.angle < 0) { item->rot.y -= M_WALK_TURN; } else { item->rot.y += M_WALK_TURN; } if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle / 2; } if (Item_TestFrameRange(item, 35, 58)) { if (creature->flags < 24) { creature->flags += 3; } const int16_t speed = creature->flags < 24 ? creature->flags : ((Random_GetControl() & 0xF) + 8); M_TriggerGasThrower(item, &m_Gas, speed); if (creature->enemy != nullptr && creature->enemy != lara_item) { creature->enemy->hit_status = true; } } break; } finish: Creature_Tilt(item, 0); Creature_Joint(item, 0, torso_x); Creature_Joint(item, 1, torso_y); Creature_Joint(item, 2, head); Creature_Animate(item_num, angle, 0); } static void M_HandleEvent( ITEM *const item, const OBJECT_EVENT event, const void *const data) { if (event != OBJECT_EVENT_BURNT) { return; } M_PRIV *const p = item->priv; p->burn_timer++; } static void M_ControlDying(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (item->status != IS_ACTIVE) { return; } M_GasDeath(item); Item_Animate(item); } static void M_SetupCrawler(OBJECT *const obj) { if (!obj->loaded) { return; } obj->collision_func = Creature_Collision; obj->control_func = M_ControlCrawler; obj->event_func = M_HandleEvent; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->shadow_size = UNIT_SHADOW / 2; obj->radius = M_RADIUS; obj->hit_points = M_HIT_POINTS; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 8)->rot.x = true; Object_GetBone(obj, 8)->rot.z = true; Object_GetBone(obj, 9)->rot.y = true; } static void M_SetupDying(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_ControlDying; obj->save_position = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_CRAWLER_MUTANT, M_SetupCrawler) REGISTER_OBJECT(O_DYING_MUTANT, M_SetupDying) ================================================ FILE: src/trx/game/objects/creatures/crocodile.c ================================================ #include #include #include #include #include #include #include #include #include // clang-format off #define M_SHADOW_SIZE (UNIT_SHADOW / (g_TRVersion < 3 ? 3 : 2)) #define M_PIVOT_LENGTH (g_TRVersion < 3 ? 600 : 200) #define M_HITPOINTS (g_TRVersion < 3 ? 20 : 42) #define M_CROCODILE_BITE_DAMAGE 100 #define M_CROCODILE_BITE_RANGE SQUARE(435) // = 189225 #define M_CROCODILE_FASTTURN_ANGLE 0x4000 #define M_CROCODILE_FASTTURN_RANGE SQUARE(WALL_L * 3) // = 9437184 #define M_CROCODILE_FASTTURN_TURN (6 * DEG_1) // = 1092 #define M_CROCODILE_TOUCH 0x3FC #define M_CROCODILE_TURN (3 * DEG_1) // = 546 #define M_CROCODILE_HITPOINTS M_HITPOINTS #define M_CROCODILE_RADIUS (WALL_L / 3) // = 341 #define M_CROCODILE_SMARTNESS 0x2000 #define M_ALLIGATOR_BITE_DAMAGE 100 #define M_ALLIGATOR_FLOAT_SPEED (WALL_L / 32) // = 32 #define M_ALLIGATOR_TURN (3 * DEG_1) // = 546 #define M_ALLIGATOR_HITPOINTS M_HITPOINTS #define M_ALLIGATOR_RADIUS (WALL_L / 3) // = 341 #define M_ALLIGATOR_SMARTNESS 0x400 #define M_ALLIGATOR_BITE_FRAME 42 // clang-format on typedef enum { M_CROCODILE_DIE_ANIM = 11, } M_CROCODILE_ANIM; typedef enum { M_ALLIGATOR_DIE_ANIM = 4, } M_ALLIGATOR_ANIM; typedef enum { M_CROCODILE_STATE_EMPTY = 0, M_CROCODILE_STATE_STOP = 1, M_CROCODILE_STATE_RUN = 2, M_CROCODILE_STATE_WALK = 3, M_CROCODILE_STATE_FAST_TURN = 4, M_CROCODILE_STATE_ATTACK_1 = 5, M_CROCODILE_STATE_ATTACK_2 = 6, M_CROCODILE_STATE_DEATH = 7, } M_CROCODILE_STATE; typedef enum { M_ALLIGATOR_STATE_EMPTY = 0, M_ALLIGATOR_STATE_SWIM = 1, M_ALLIGATOR_STATE_ATTACK = 2, M_ALLIGATOR_STATE_DEATH = 3, } M_ALLIGATOR_STATE; static BITE m_CrocodileBite = { .pos = { 5, -21, 467 }, .mesh_num = 9 }; static const HYBRID_INFO m_CrocodileInfo = { .land.id = O_CROCODILE, .land.active_anim = M_CROCODILE_STATE_EMPTY, .land.death_anim = M_CROCODILE_DIE_ANIM, .land.death_state = M_CROCODILE_STATE_DEATH, .water.id = O_ALLIGATOR, .water.active_anim = M_ALLIGATOR_STATE_EMPTY, .water.death_anim = M_ALLIGATOR_DIE_ANIM, .water.death_state = M_ALLIGATOR_STATE_DEATH, }; static void M_UpdateCreatureLOT(const ITEM *const item) { CREATURE *const creature = item->creature_data; if (creature == nullptr) { return; } OBJECT *const obj = Object_Get(item->object_id); creature->lot.setup = obj->lot_setup; } static void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage) { if (stage == SAVEGAME_STAGE_AFTER_LOAD) { if (Creature_EnsureHabitat( Item_GetIndex(item), nullptr, &m_CrocodileInfo)) { M_UpdateCreatureLOT(item); } } } static void M_ControlCrocodile(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const croc = item->creature_data; int16_t head = 0; int16_t angle = 0; if (item->hit_points <= 0) { if (item->current_anim_state != M_CROCODILE_STATE_DEATH) { item->current_anim_state = M_CROCODILE_STATE_DEATH; Item_SwitchToAnim(item, M_CROCODILE_DIE_ANIM, 0); } } else { AI_INFO info; Creature_AIInfo(item, &info); if (info.ahead) { head = info.angle; } Creature_Mood(item, &info, true); if (item->current_anim_state == M_CROCODILE_STATE_FAST_TURN) { item->rot.y += M_CROCODILE_FASTTURN_TURN; } else { angle = Creature_Turn(item, M_CROCODILE_TURN); } switch (item->current_anim_state) { case M_CROCODILE_STATE_STOP: if (info.bite && info.distance < M_CROCODILE_BITE_RANGE) { item->goal_anim_state = M_CROCODILE_STATE_ATTACK_1; } else if (croc->mood == MOOD_ESCAPE) { item->goal_anim_state = M_CROCODILE_STATE_RUN; } else if (croc->mood == MOOD_ATTACK) { if ((info.angle < -M_CROCODILE_FASTTURN_ANGLE || info.angle > M_CROCODILE_FASTTURN_ANGLE) && info.distance > M_CROCODILE_FASTTURN_RANGE) { item->goal_anim_state = M_CROCODILE_STATE_FAST_TURN; } else { item->goal_anim_state = M_CROCODILE_STATE_RUN; } } else if (croc->mood == MOOD_STALK) { item->goal_anim_state = M_CROCODILE_STATE_WALK; } break; case M_CROCODILE_STATE_WALK: if (info.ahead && (item->touch_bits & M_CROCODILE_TOUCH)) { item->goal_anim_state = M_CROCODILE_STATE_STOP; } else if (croc->mood == MOOD_ATTACK || croc->mood == MOOD_ESCAPE) { item->goal_anim_state = M_CROCODILE_STATE_RUN; } else if (croc->mood == MOOD_BORED) { item->goal_anim_state = M_CROCODILE_STATE_STOP; } break; case M_CROCODILE_STATE_FAST_TURN: if (info.angle > -M_CROCODILE_FASTTURN_ANGLE && info.angle < M_CROCODILE_FASTTURN_ANGLE) { item->goal_anim_state = M_CROCODILE_STATE_WALK; } break; case M_CROCODILE_STATE_RUN: if (info.ahead && (item->touch_bits & M_CROCODILE_TOUCH)) { item->goal_anim_state = M_CROCODILE_STATE_STOP; } else if (croc->mood == MOOD_STALK) { item->goal_anim_state = M_CROCODILE_STATE_WALK; } else if (croc->mood == MOOD_BORED) { item->goal_anim_state = M_CROCODILE_STATE_STOP; } else if ( croc->mood == MOOD_ATTACK && info.distance > M_CROCODILE_FASTTURN_RANGE && (info.angle < -M_CROCODILE_FASTTURN_ANGLE || info.angle > M_CROCODILE_FASTTURN_ANGLE)) { item->goal_anim_state = M_CROCODILE_STATE_STOP; } break; case M_CROCODILE_STATE_ATTACK_1: if (item->required_anim_state == M_CROCODILE_STATE_EMPTY) { Creature_Effect(item, &m_CrocodileBite, Spawn_Blood); Lara_TakeDamage(M_CROCODILE_BITE_DAMAGE, true); item->required_anim_state = M_CROCODILE_STATE_STOP; } break; } } if (croc != nullptr) { Creature_Head(item, head); } // Test conversion to alligator and set relevant pathfinding values. if (Creature_EnsureHabitat(item_num, nullptr, &m_CrocodileInfo)) { M_UpdateCreatureLOT(item); } if (croc != nullptr) { Creature_Animate(item_num, angle, 0); } else { Item_Animate(item); } } static void M_ControlAlligator(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const gator = item->creature_data; int16_t head = 0; int16_t angle = 0; if (item->hit_points <= 0) { if (item->current_anim_state != M_ALLIGATOR_STATE_DEATH) { item->current_anim_state = M_ALLIGATOR_STATE_DEATH; Item_SwitchToAnim(item, M_ALLIGATOR_DIE_ANIM, 0); item->hit_points = 0; Carrier_TestItemDrops(item_num); } // Test if we should convert to a crocodile. If not, control the death // pose of the alligator in the water. if (g_TRVersion >= 3 || !Creature_EnsureHabitat(item_num, nullptr, &m_CrocodileInfo)) { Creature_Float(item_num); } return; } AI_INFO info; Creature_AIInfo(item, &info); if (info.ahead) { head = info.angle; } Creature_UpdateMood(item, &info, true); if (g_TRVersion >= 3) { const ITEM *const lara_item = Lara_GetItem(); if (!Room_Get(lara_item->room_num)->flags.underwater && !Lara_Vehicle_IsMounted()) { gator->mood = MOOD_BORED; } } Creature_ApplyMood(item, &info, true); Creature_Turn(item, M_ALLIGATOR_TURN); switch (item->current_anim_state) { case M_ALLIGATOR_STATE_SWIM: if (info.bite && item->touch_bits) { item->goal_anim_state = M_ALLIGATOR_STATE_ATTACK; if (g_Config.gameplay.fix_alligator_ai) { item->required_anim_state = M_ALLIGATOR_STATE_SWIM; } } break; case M_ALLIGATOR_STATE_ATTACK: if (item->frame_num == (g_Config.gameplay.fix_alligator_ai ? M_ALLIGATOR_BITE_FRAME : Item_GetAnim(item)->frame_base)) { item->required_anim_state = M_ALLIGATOR_STATE_EMPTY; } if (info.bite && item->touch_bits) { if (item->required_anim_state == M_ALLIGATOR_STATE_EMPTY) { Creature_Effect(item, &m_CrocodileBite, Spawn_Blood); Lara_TakeDamage(M_ALLIGATOR_BITE_DAMAGE, true); item->required_anim_state = M_ALLIGATOR_STATE_SWIM; } if (g_Config.gameplay.fix_alligator_ai) { item->goal_anim_state = M_ALLIGATOR_STATE_SWIM; } } else { item->goal_anim_state = M_ALLIGATOR_STATE_SWIM; } break; } Creature_Joint(item, 0, head); if (g_TRVersion < 3) { int32_t wh = 0; if (Creature_EnsureHabitat(item_num, &wh, &m_CrocodileInfo)) { M_UpdateCreatureLOT(item); } else { CLAMPL(item->pos.y, wh + STEP_L); } } else { Creature_Underwater(item, STEP_L); } Creature_Animate(item_num, angle, 0); } static void M_SetupBase(OBJECT *const obj) { obj->initialise_func = Creature_Initialise; obj->collision_func = Creature_Collision; obj->shadow_size = M_SHADOW_SIZE; obj->pivot_length = M_PIVOT_LENGTH; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_anim = true; obj->save_flags = true; obj->handle_save_func = M_HandleSave; Object_GetBone(obj, 7)->rot.y = true; } static void M_SetupCrocodile(OBJECT *const obj) { if (!obj->loaded) { return; } M_SetupBase(obj); obj->control_func = M_ControlCrocodile; obj->hit_points = M_CROCODILE_HITPOINTS; obj->radius = M_CROCODILE_RADIUS; obj->smartness = M_CROCODILE_SMARTNESS; } static void M_SetupAlligator(OBJECT *const obj) { if (!obj->loaded) { return; } M_SetupBase(obj); obj->control_func = M_ControlAlligator; obj->hit_points = M_ALLIGATOR_HITPOINTS; obj->radius = M_ALLIGATOR_RADIUS; obj->smartness = M_ALLIGATOR_SMARTNESS; obj->lot_setup = LOT_Setup(LOT_SETUP_FLYER); } REGISTER_OBJECT(O_ALLIGATOR, M_SetupAlligator) REGISTER_OBJECT(O_CROCODILE, M_SetupCrocodile) ================================================ FILE: src/trx/game/objects/creatures/cultist_1.c ================================================ #include #include #include #include #include #include // clang-format off #define CULTIST_1_HITPOINTS 25 #define CULTIST_1_SHOOT_DAMAGE 50 #define CULTIST_1_WALK_TURN (DEG_1 * 5) // = 910 #define CULTIST_1_RUN_TURN (DEG_1 * 5) // = 910 #define CULTIST_1_RUN_RANGE SQUARE(WALL_L * 2) // = 4194304 #define CULTIST_1_POSE_CHANCE 0x500 // = 1280 #define CULTIST_1_UNPOSE_CHANCE 0x100 // = 256 #define CULTIST_1_WALK_CHANCE (CULTIST_1_POSE_CHANCE + 0x500) // = 2560 #define CULTIST_1_UNWALK_CHANCE 0x300 // = 768 // clang-format on typedef enum { // clang-format off CULTIST_1_STATE_EMPTY = 0, CULTIST_1_STATE_WALK = 1, CULTIST_1_STATE_RUN = 2, CULTIST_1_STATE_STOP = 3, CULTIST_1_STATE_WAIT_1 = 4, CULTIST_1_STATE_WAIT_2 = 5, CULTIST_1_STATE_AIM_1 = 6, CULTIST_1_STATE_SHOOT_1 = 7, CULTIST_1_STATE_AIM_2 = 8, CULTIST_1_STATE_SHOOT_2 = 9, CULTIST_1_STATE_AIM_3 = 10, CULTIST_1_STATE_SHOOT_3 = 11, CULTIST_1_STATE_DEATH = 12, // clang-format on } CULTIST_1_STATE; typedef enum { CULTIST_1_ANIM_DEATH = 20, } CULTIST_1_ANIM; static const CREATURE_GUN m_Cultist1Gun = { .muzzle = { .pos = { .x = 3, .y = 331, .z = 56 }, .mesh_num = 10 }, }; static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (Random_GetControl() < 0x4000) { item->mesh_bits &= ~0b00110000; } if (item->object_id == O_CULT_1B) { // clang-format off // TODO: clang-format >=20 formats this wrongly item->mesh_bits &= ~0b00011111'10000000'00000000; // clang-format on } } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t head = 0; int16_t tilt = 0; int16_t angle = 0; if (item->hit_points <= 0) { if (item->current_anim_state != CULTIST_1_STATE_DEATH) { Item_SwitchToAnim( item, Random_GetControl() / 0x4000 + CULTIST_1_ANIM_DEATH, 0); item->current_anim_state = CULTIST_1_STATE_DEATH; } } else { AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, false); angle = Creature_Turn(item, creature->maximum_turn); switch (item->current_anim_state) { case CULTIST_1_STATE_STOP: creature->maximum_turn = 0; if (item->required_anim_state != CULTIST_1_STATE_EMPTY) { item->goal_anim_state = item->required_anim_state; } break; case CULTIST_1_STATE_WAIT_1: if (creature->mood == MOOD_ESCAPE) { item->required_anim_state = CULTIST_1_STATE_RUN; item->goal_anim_state = CULTIST_1_STATE_STOP; } else if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = CULTIST_1_STATE_STOP; item->required_anim_state = Random_GetControl() < 0x4000 ? CULTIST_1_STATE_AIM_1 : CULTIST_1_STATE_AIM_3; } else if (creature->mood == MOOD_BORED && info.ahead) { const int16_t random = Random_GetControl(); if (random < CULTIST_1_POSE_CHANCE) { item->required_anim_state = CULTIST_1_STATE_WAIT_2; item->goal_anim_state = CULTIST_1_STATE_STOP; } else if (random < CULTIST_1_WALK_CHANCE) { item->required_anim_state = CULTIST_1_STATE_WALK; item->goal_anim_state = CULTIST_1_STATE_STOP; } } else if ( creature->mood == MOOD_BORED || info.distance < CULTIST_1_RUN_RANGE) { item->required_anim_state = CULTIST_1_STATE_WALK; item->goal_anim_state = CULTIST_1_STATE_STOP; } else { item->required_anim_state = CULTIST_1_STATE_RUN; item->goal_anim_state = CULTIST_1_STATE_STOP; } break; case CULTIST_1_STATE_WAIT_2: if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = CULTIST_1_STATE_STOP; item->required_anim_state = CULTIST_1_STATE_AIM_1; } else if ( creature->mood != MOOD_BORED || Random_GetControl() < CULTIST_1_UNPOSE_CHANCE || !info.ahead) { item->goal_anim_state = CULTIST_1_STATE_STOP; } break; case CULTIST_1_STATE_WALK: creature->maximum_turn = CULTIST_1_WALK_TURN; if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = CULTIST_1_STATE_RUN; } else if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = CULTIST_1_STATE_STOP; item->required_anim_state = Random_GetControl() < 0x4000 ? CULTIST_1_STATE_AIM_1 : CULTIST_1_STATE_AIM_3; } else if (info.distance > CULTIST_1_RUN_RANGE || !info.ahead) { item->goal_anim_state = CULTIST_1_STATE_RUN; } else if ( creature->mood == MOOD_BORED && info.ahead && Random_GetControl() < CULTIST_1_UNWALK_CHANCE) { item->goal_anim_state = CULTIST_1_STATE_STOP; } break; case CULTIST_1_STATE_RUN: creature->maximum_turn = CULTIST_1_RUN_TURN; creature->flags = 0; tilt = angle / 4; if (creature->mood == MOOD_ESCAPE) { if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = CULTIST_1_STATE_SHOOT_2; } } else if (Creature_CanTargetEnemy(item, &info)) { if (info.distance < CULTIST_1_RUN_RANGE || info.zone_num != info.enemy_zone_num) { item->goal_anim_state = CULTIST_1_STATE_STOP; } else { item->goal_anim_state = CULTIST_1_STATE_SHOOT_2; } } else if (creature->mood == MOOD_BORED) { item->goal_anim_state = CULTIST_1_STATE_STOP; } break; case CULTIST_1_STATE_AIM_1: case CULTIST_1_STATE_AIM_3: creature->flags = 0; if (info.ahead) { head = info.angle; } if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = CULTIST_1_STATE_STOP; } else if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = item->current_anim_state == CULTIST_1_STATE_AIM_1 ? CULTIST_1_STATE_SHOOT_1 : CULTIST_1_STATE_SHOOT_3; } else { item->goal_anim_state = CULTIST_1_STATE_STOP; } break; case CULTIST_1_STATE_SHOOT_1: case CULTIST_1_STATE_SHOOT_3: if (info.ahead) { head = info.angle; } if (creature->flags == 0) { Creature_Shoot( item, &info, &m_Cultist1Gun, head, CULTIST_1_SHOOT_DAMAGE); creature->flags = 1; } break; case CULTIST_1_STATE_SHOOT_2: if (info.ahead) { head = info.angle; } if (item->required_anim_state == CULTIST_1_STATE_EMPTY) { if (!Creature_Shoot( item, &info, &m_Cultist1Gun, head, CULTIST_1_SHOOT_DAMAGE)) { item->goal_anim_state = CULTIST_1_STATE_RUN; } item->required_anim_state = CULTIST_1_STATE_SHOOT_2; } break; default: break; } } Creature_Tilt(item, tilt); Creature_Head(item, head); Creature_Animate(item_num, angle, 0); } static void M_Setup1(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->hit_points = CULTIST_1_HITPOINTS; obj->radius = CULTIST_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->pivot_length = 50; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 0)->rot.y = true; } static void M_Setup1A(OBJECT *const obj) { if (!obj->loaded) { return; } const OBJECT *const ref_obj = Object_Get(O_CULT_1); if (ref_obj->loaded) { obj->frame_base = ref_obj->frame_base; obj->anim_idx = ref_obj->anim_idx; } obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->hit_points = CULTIST_1_HITPOINTS; obj->radius = CULTIST_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->pivot_length = 50; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 0)->rot.y = true; } static void M_Setup1B(OBJECT *const obj) { if (!obj->loaded) { return; } const OBJECT *const ref_obj = Object_Get(O_CULT_1); if (ref_obj->loaded) { obj->frame_base = ref_obj->frame_base; obj->anim_idx = ref_obj->anim_idx; } obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->hit_points = CULTIST_1_HITPOINTS; obj->radius = CULTIST_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->pivot_length = 50; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 0)->rot.y = true; } REGISTER_OBJECT(O_CULT_1, M_Setup1) REGISTER_OBJECT(O_CULT_1A, M_Setup1A) REGISTER_OBJECT(O_CULT_1B, M_Setup1B) ================================================ FILE: src/trx/game/objects/creatures/cultist_2.c ================================================ #include #include #include #include #include #include // clang-format off #define CULTIST_2_HITPOINTS 60 #define CULTIST_2_WALK_TURN (DEG_1 * 3) // = 546 #define CULTIST_2_RUN_TURN (DEG_1 * 6) // = 1092 #define CULTIST_2_WALK_RANGE SQUARE(WALL_L * 4) // = 16777216 #define CULTIST_2_KNIFE_RANGE SQUARE(WALL_L * 6) // = 37748736 #define CULTIST_2_STOP_RANGE SQUARE(WALL_L * 5 / 2) // = 6553600 // clang-format on typedef enum { // clang-format off CULTIST_2_STATE_EMPTY = 0, CULTIST_2_STATE_STOP = 1, CULTIST_2_STATE_WALK = 2, CULTIST_2_STATE_RUN = 3, CULTIST_2_STATE_AIM_1_L = 4, CULTIST_2_STATE_SHOOT_1_L = 5, CULTIST_2_STATE_AIM_1_R = 6, CULTIST_2_STATE_SHOOT_1_R = 7, CULTIST_2_STATE_AIM_2 = 8, CULTIST_2_STATE_SHOOT_2 = 9, CULTIST_2_STATE_DEATH = 10, // clang-format on } CULTIST_2_STATE; typedef enum { CULTIST_2_ANIM_DEATH = 23, } CULTIST_2_ANIM; static const BITE m_Cultist2LeftHand = { .pos = { .x = 0, .y = 0, .z = 0 }, .mesh_num = 5, }; static const BITE m_Cultist2RightHand = { .pos = { .x = 0, .y = 0, .z = 0 }, .mesh_num = 8, }; static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t angle = 0; int16_t tilt = 0; int16_t neck = 0; int16_t head = 0; if (item->hit_points <= 0) { if (item->current_anim_state != CULTIST_2_STATE_DEATH) { Item_SwitchToAnim(item, CULTIST_2_ANIM_DEATH, 0); item->current_anim_state = CULTIST_2_STATE_DEATH; } } else { AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, false); angle = Creature_Turn(item, creature->maximum_turn); switch (item->current_anim_state) { case CULTIST_2_STATE_STOP: creature->maximum_turn = 0; if (info.ahead) { neck = info.angle; } if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = CULTIST_2_STATE_RUN; } else if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = CULTIST_2_STATE_AIM_2; } else if (creature->mood == MOOD_BORED) { if (!info.ahead || info.distance > CULTIST_2_KNIFE_RANGE) { item->goal_anim_state = CULTIST_2_STATE_WALK; } } else if (info.ahead && info.distance < CULTIST_2_WALK_RANGE) { item->goal_anim_state = CULTIST_2_STATE_WALK; } else { item->goal_anim_state = CULTIST_2_STATE_RUN; } break; case CULTIST_2_STATE_WALK: creature->maximum_turn = CULTIST_2_WALK_TURN; if (info.ahead) { neck = info.angle; } if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = CULTIST_2_STATE_RUN; } else if (Creature_CanTargetEnemy(item, &info)) { if (info.distance < CULTIST_2_STOP_RANGE || info.zone_num != info.enemy_zone_num) { item->goal_anim_state = CULTIST_2_STATE_STOP; } else if (Random_GetControl() < 0x4000) { item->goal_anim_state = CULTIST_2_STATE_AIM_1_L; } else { item->goal_anim_state = CULTIST_2_STATE_AIM_1_R; } } else if (creature->mood == MOOD_BORED) { if (info.ahead && info.distance < CULTIST_2_KNIFE_RANGE) { item->goal_anim_state = CULTIST_2_STATE_STOP; } } else if (!info.ahead || info.distance > CULTIST_2_WALK_RANGE) { item->goal_anim_state = CULTIST_2_STATE_RUN; } break; case CULTIST_2_STATE_RUN: creature->maximum_turn = CULTIST_2_RUN_TURN; tilt = angle / 4; if (info.ahead) { neck = info.angle; } if (creature->mood == MOOD_ESCAPE) { } else if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = CULTIST_2_STATE_WALK; } else if (creature->mood == MOOD_BORED) { if (info.ahead && info.distance < CULTIST_2_KNIFE_RANGE) { item->goal_anim_state = CULTIST_2_STATE_STOP; } else { item->goal_anim_state = CULTIST_2_STATE_WALK; } } else if (info.ahead && info.distance < CULTIST_2_WALK_RANGE) { item->goal_anim_state = CULTIST_2_STATE_WALK; } break; case CULTIST_2_STATE_AIM_1_L: creature->flags = 0; if (info.ahead) { head = info.angle; } if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = CULTIST_2_STATE_SHOOT_1_L; } else { item->goal_anim_state = CULTIST_2_STATE_WALK; } break; case CULTIST_2_STATE_AIM_1_R: creature->flags = 0; if (info.ahead) { head = info.angle; } if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = CULTIST_2_STATE_SHOOT_1_R; } else { item->goal_anim_state = CULTIST_2_STATE_WALK; } break; case CULTIST_2_STATE_AIM_2: creature->flags = 0; if (info.ahead) { head = info.angle; } if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = CULTIST_2_STATE_SHOOT_2; } else { item->goal_anim_state = CULTIST_2_STATE_STOP; } break; case CULTIST_2_STATE_SHOOT_1_L: if (info.ahead) { head = info.angle; } if (creature->flags == 0) { Creature_Effect(item, &m_Cultist2LeftHand, Spawn_Knife); creature->flags = 1; } break; case CULTIST_2_STATE_SHOOT_1_R: if (info.ahead) { head = info.angle; } if (creature->flags == 0) { Creature_Effect(item, &m_Cultist2RightHand, Spawn_Knife); creature->flags = 1; } break; case CULTIST_2_STATE_SHOOT_2: if (info.ahead) { head = info.angle; } if (creature->flags == 0) { Creature_Effect(item, &m_Cultist2LeftHand, Spawn_Knife); Creature_Effect(item, &m_Cultist2RightHand, Spawn_Knife); creature->flags = CULTIST_2_STATE_STOP; } break; default: break; } } Creature_Tilt(item, tilt); Creature_Neck(item, neck); Creature_Head(item, head); Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->hit_points = CULTIST_2_HITPOINTS; obj->radius = CULTIST_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->pivot_length = 50; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 0)->rot.y = true; Object_GetBone(obj, 8)->rot.y = true; } REGISTER_OBJECT(O_CULT_2, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/cultist_3.c ================================================ #include #include #include #include #include #include // clang-format off #define CULTIST_3_HITPOINTS 150 #define CULTIST_3_SHOT_DAMAGE 50 #define CULTIST_3_WALK_TURN (DEG_1 * 3) // = 546 #define CULTIST_3_RUN_TURN (DEG_1 * 3) // = 546 #define CULTIST_3_STOP_RANGE SQUARE(WALL_L * 3) // = 9437184 #define CULTIST_3_RUN_RANGE SQUARE(WALL_L * 5) // = 26214400 // clang-format on typedef enum { // clang-format off CULTIST_3_STATE_EMPTY = 0, CULTIST_3_STATE_STOP = 1, CULTIST_3_STATE_WAIT = 2, CULTIST_3_STATE_WALK = 3, CULTIST_3_STATE_RUN = 4, CULTIST_3_STATE_AIM_L = 5, CULTIST_3_STATE_AIM_R = 6, CULTIST_3_STATE_SHOOT_L = 7, CULTIST_3_STATE_SHOOT_R = 8, CULTIST_3_STATE_AIM_2 = 9, CULTIST_3_STATE_SHOOT_2 = 10, CULTIST_3_STATE_DEATH = 11, // clang-format on } CULTIST_3_STATE; typedef enum { // clang-format off CULTIST_3_ANIM_WAIT = 3, CULTIST_3_ANIM_DEATH = 32, // clang-format on } CULTIST_3_ANIM; static const CREATURE_GUN m_Cultist3LeftGun = { .muzzle = { .pos = { .x = -2, .y = 275, .z = 23 }, .mesh_num = 6 }, }; static const CREATURE_GUN m_Cultist3RightGun = { .muzzle = { .pos = { .x = 2, .y = 275, .z = 23 }, .mesh_num = 10 }, }; static void M_Initialise(const int16_t item_num) { Creature_Initialise(item_num); ITEM *const item = Item_Get(item_num); Item_SwitchToAnim(item, CULTIST_3_ANIM_WAIT, 0); item->goal_anim_state = CULTIST_3_STATE_WAIT; item->current_anim_state = CULTIST_3_STATE_WAIT; } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t head = 0; int16_t tilt = 0; int16_t angle = 0; int16_t body = 0; int16_t left = 0; int16_t right = 0; if (item->hit_points <= 0) { if (item->current_anim_state != CULTIST_3_STATE_DEATH) { Item_SwitchToAnim(item, CULTIST_3_ANIM_DEATH, 0); item->current_anim_state = CULTIST_3_STATE_DEATH; } } else { AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, true); angle = Creature_Turn(item, creature->maximum_turn); const ITEM *const lara_item = Lara_GetItem(); switch (item->current_anim_state) { case CULTIST_3_STATE_STOP: case CULTIST_3_STATE_WAIT: if (info.ahead) { head = info.angle; } if (creature->mood == MOOD_BORED && lara_item->hit_points <= 0) { item->goal_anim_state = CULTIST_3_STATE_WAIT; } else if (Creature_CanTargetEnemy(item, &info)) { if (info.distance > CULTIST_3_STOP_RANGE) { item->goal_anim_state = CULTIST_3_STATE_WALK; } else { item->goal_anim_state = CULTIST_3_STATE_AIM_2; } } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = CULTIST_3_STATE_RUN; } else if (creature->mood == MOOD_ATTACK) { if (info.distance > CULTIST_3_RUN_RANGE || !info.ahead) { item->goal_anim_state = CULTIST_3_STATE_RUN; } else { item->goal_anim_state = CULTIST_3_STATE_WALK; } } else if (creature->mood == MOOD_STALK || !info.ahead) { item->goal_anim_state = CULTIST_3_STATE_WALK; } break; case CULTIST_3_STATE_WALK: creature->maximum_turn = CULTIST_3_WALK_TURN; if (info.ahead) { head = info.angle; } if (Creature_CanTargetEnemy(item, &info)) { if (info.distance < CULTIST_3_STOP_RANGE || info.zone_num != info.enemy_zone_num) { item->goal_anim_state = CULTIST_3_STATE_STOP; } else if (info.angle < 0) { item->goal_anim_state = CULTIST_3_STATE_AIM_L; } else { item->goal_anim_state = CULTIST_3_STATE_AIM_R; } } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = CULTIST_3_STATE_RUN; } else if ( creature->mood == MOOD_STALK || creature->mood == MOOD_ATTACK) { if (info.distance > CULTIST_3_RUN_RANGE || !info.ahead) { item->goal_anim_state = CULTIST_3_STATE_RUN; } } else if (lara_item->hit_points <= 0) { item->goal_anim_state = CULTIST_3_STATE_WAIT; } else if (info.ahead) { item->goal_anim_state = CULTIST_3_STATE_STOP; } break; case CULTIST_3_STATE_RUN: creature->maximum_turn = CULTIST_3_RUN_TURN; tilt = angle / 4; if (info.ahead) { head = info.angle; } if (Creature_CanTargetEnemy(item, &info)) { if (info.zone_num != info.enemy_zone_num) { item->goal_anim_state = CULTIST_3_STATE_STOP; } else if (info.angle < 0) { item->goal_anim_state = CULTIST_3_STATE_AIM_L; } else { item->goal_anim_state = CULTIST_3_STATE_AIM_R; } } else if (creature->mood == MOOD_BORED) { if (lara_item->hit_points <= 0) { item->goal_anim_state = CULTIST_3_STATE_WAIT; } else { item->goal_anim_state = CULTIST_3_STATE_STOP; } } else if (info.ahead && info.distance < CULTIST_3_RUN_RANGE) { item->goal_anim_state = CULTIST_3_STATE_WALK; } break; case CULTIST_3_STATE_AIM_L: creature->flags = 0; if (info.ahead) { head = info.angle; left = info.angle; } if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = CULTIST_3_STATE_SHOOT_L; } else { item->goal_anim_state = CULTIST_3_STATE_WALK; } break; case CULTIST_3_STATE_AIM_R: creature->flags = 0; if (info.ahead) { head = info.angle; right = info.angle; } if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = CULTIST_3_STATE_SHOOT_R; } else { item->goal_anim_state = CULTIST_3_STATE_WALK; } break; case CULTIST_3_STATE_AIM_2: creature->flags = 0; if (info.ahead) { body = info.angle; } if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = CULTIST_3_STATE_SHOOT_2; } else { item->goal_anim_state = CULTIST_3_STATE_STOP; } break; case CULTIST_3_STATE_SHOOT_L: if (info.ahead) { head = info.angle; left = info.angle; } if (creature->flags == 0) { Creature_Shoot( item, &info, &m_Cultist3LeftGun, head, CULTIST_3_SHOT_DAMAGE); creature->flags = 1; } break; case CULTIST_3_STATE_SHOOT_R: if (info.ahead) { head = info.angle; right = info.angle; } if (creature->flags == 0) { Creature_Shoot( item, &info, &m_Cultist3RightGun, head, CULTIST_3_SHOT_DAMAGE); creature->flags = 1; } break; case CULTIST_3_STATE_SHOOT_2: if (info.ahead) { body = info.angle; } if (creature->flags == 0) { Creature_Shoot( item, &info, &m_Cultist3LeftGun, 0, CULTIST_3_SHOT_DAMAGE); Creature_Shoot( item, &info, &m_Cultist3RightGun, 0, CULTIST_3_SHOT_DAMAGE); creature->flags = 1; } break; default: break; } } Creature_Tilt(item, tilt); const OBJECT *const obj = Object_Get(item->object_id); Object_GetBone(obj, 0)->rot.y = body != 0; Object_GetBone(obj, 2)->rot.y = left != 0; Object_GetBone(obj, 6)->rot.y = right != 0; Object_GetBone(obj, 10)->rot.y = head != 0; if (body != 0) { Creature_Head(item, body); } else if (left != 0) { Creature_Head(item, left); Creature_Neck(item, head); } else if (right != 0) { Creature_Head(item, right); Creature_Neck(item, head); } else if (head != 0) { Creature_Head(item, head); } Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->hit_points = CULTIST_3_HITPOINTS; obj->radius = CULTIST_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->pivot_length = 0; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_CULT_3, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/cultist_common.h ================================================ #pragma once #include #define CULTIST_RADIUS (WALL_L / 10) // = 102 ================================================ FILE: src/trx/game/objects/creatures/diver.c ================================================ #include #include #include #include #include #include #include #include // clang-format off #define M_SWIM_TURN (3 * DEG_1) // = 546 #define M_FRONT_ARC DEG_45 #define M_HITPOINTS 20 #define M_RADIUS (WALL_L / 3) // = 341 // clang-format on typedef enum { M_STATE_EMPTY, M_STATE_SWIM_1, M_STATE_SWIM_2, M_STATE_SHOOT_1, M_STATE_AIM_1, M_STATE_NULL_1, M_STATE_AIM_2, M_STATE_SHOOT_2, M_STATE_NULL_2, M_STATE_DEATH, // clang-format on } DIVER_STATE; typedef enum { M_ANIM_DEATH = 16, } M_ANIM; static const BITE m_DiverBite = { .pos = { .x = 17, .y = 164, .z = 44 }, .mesh_num = 18, }; static int32_t M_GetWaterSurface( const int32_t x, const int32_t y, const int32_t z, const int16_t room_num) { const ROOM *room = Room_Get(room_num); const SECTOR *sector = Room_GetWorldSector(room, x, z); if (room->flags.underwater) { while (sector->portal_room.sky != NO_ROOM) { room = Room_Get(sector->portal_room.sky); if (!room->flags.underwater) { return sector->ceiling.height; } sector = Room_GetWorldSector(room, x, z); } } else { while (sector->portal_room.pit != NO_ROOM) { room = Room_Get(sector->portal_room.pit); if (room->flags.underwater) { return sector->floor.height; } sector = Room_GetWorldSector(room, x, z); } } return NO_HEIGHT; } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; if (item->hit_points <= 0) { if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, M_ANIM_DEATH, 0); item->current_anim_state = M_STATE_DEATH; Carrier_TestItemDrops(item_num); } Creature_Float(item_num); return; } AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, false); bool shoot; const ITEM *const lara_item = Lara_GetItem(); const LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->water_status == LWS_ABOVE_WATER) { const GAME_VECTOR start = { .x = item->pos.x, .y = item->pos.y - STEP_L, .z = item->pos.z, .room_num = item->room_num, }; GAME_VECTOR target = { .x = lara_item->pos.x, .y = lara_item->pos.y - (LARA_HEIGHT - 150), .z = lara_item->pos.z, .room_num = lara_item->room_num, }; shoot = LOS_Check(&start, &target, true); if (shoot) { creature->target = lara_item->pos; } if (info.angle < -M_FRONT_ARC || info.angle > M_FRONT_ARC) { shoot = false; } } else if (info.angle > -M_FRONT_ARC && info.angle < M_FRONT_ARC) { const GAME_VECTOR start = { .x = item->pos.x, .y = item->pos.y, .z = item->pos.z, .room_num = item->room_num, }; GAME_VECTOR target = { .x = lara_item->pos.x, .y = lara_item->pos.y, .z = lara_item->pos.z, .room_num = lara_item->room_num, }; shoot = LOS_Check(&start, &target, true); } else { shoot = false; } int16_t head = 0; int16_t neck = 0; int16_t angle = Creature_Turn(item, creature->maximum_turn); int32_t water_level = M_GetWaterSurface(item->pos.x, item->pos.y, item->pos.z, item->room_num) + STEP_L * 2; switch (item->current_anim_state) { case M_STATE_SWIM_1: creature->maximum_turn = M_SWIM_TURN; if (shoot) { neck = -info.angle; } if (creature->target.y < water_level && item->pos.y < water_level + creature->lot.setup.fly) { item->goal_anim_state = M_STATE_SWIM_2; } else if (creature->mood != MOOD_ESCAPE && shoot) { item->goal_anim_state = M_STATE_AIM_1; } break; case M_STATE_SWIM_2: creature->maximum_turn = M_SWIM_TURN; if (shoot) { head = info.angle; } if (creature->target.y > water_level) { item->goal_anim_state = M_STATE_SWIM_1; } else if (creature->mood != MOOD_ESCAPE && shoot) { item->goal_anim_state = M_STATE_AIM_2; } break; case M_STATE_SHOOT_1: if (shoot) { neck = -info.angle; } if (creature->flags == 0) { Creature_Effect(item, &m_DiverBite, Spawn_Harpoon); creature->flags = 1; } break; case M_STATE_SHOOT_2: if (shoot) { head = info.angle; } if (creature->flags == 0) { Creature_Effect(item, &m_DiverBite, Spawn_Harpoon); creature->flags = 1; } break; case M_STATE_AIM_1: creature->flags = 0; if (shoot) { neck = -info.angle; } if (!shoot || creature->mood == MOOD_ESCAPE || (creature->target.y < water_level && item->pos.y < water_level + creature->lot.setup.fly)) { item->goal_anim_state = M_STATE_SWIM_1; } else { item->goal_anim_state = M_STATE_SHOOT_1; } break; case M_STATE_AIM_2: creature->flags = 0; if (shoot) { head = info.angle; } if (!shoot || creature->mood == MOOD_ESCAPE || creature->target.y > water_level) { item->goal_anim_state = M_STATE_SWIM_2; } else { item->goal_anim_state = M_STATE_SHOOT_2; } break; default: break; } Creature_Head(item, head); Creature_Neck(item, neck); Creature_Animate(item_num, angle, 0); switch (item->current_anim_state) { case M_STATE_SWIM_1: case M_STATE_AIM_1: case M_STATE_SHOOT_1: Creature_Underwater(item, WALL_L / 2); break; default: item->pos.y = water_level - WALL_L / 2; break; } } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->hit_points = M_HITPOINTS; obj->radius = M_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->pivot_length = 50; obj->lot_setup = LOT_Setup(LOT_SETUP_FLYER); obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; if (g_TRVersion < 3) { Object_GetBone(obj, 10)->rot.y = true; Object_GetBone(obj, 14)->rot.z = true; } } REGISTER_OBJECT(O_DIVER, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/dog.c ================================================ #include #include #include #include #include #include // clang-format off #define M_HITPOINTS 10 #define M_TOUCH_BITS 0b00000001'00111111'01110000'00000000 #define M_RADIUS (WALL_L / 3) // = 341 #define M_WALK_TURN (3 * DEG_1) // = 546 #define M_RUN_TURN (6 * DEG_1) // = 1092 #define M_ATTACK_1_RANGE SQUARE(WALL_L / 3) // = 116281 #define M_ATTACK_2_RANGE SQUARE(WALL_L * 3 / 4) // = 589824 #define M_ATTACK_3_RANGE SQUARE(WALL_L * 2 / 3) // = 465124 #define M_LEAP_DAMAGE 200 #define M_BITE_DAMAGE 100 #define M_LUNGE_DAMAGE 100 #define M_BARK_CHANCE 0x300 #define M_CROUCH_CHANCE (M_BARK_CHANCE + 0x300) // = 0x600 #define M_STAND_CHANCE (M_CROUCH_CHANCE + 0x500) // = 0xB00 #define M_WALK_CHANCE (M_STAND_CHANCE + 0x2000) // = 0x2600 #define M_UNCROUCH_CHANCE 0x100 #define M_UNSTAND_CHANCE 0x200 #define M_UNBARK_CHANCE 0x500 // clang-format on typedef enum { M_STATE_EMPTY, M_STATE_WALK, M_STATE_RUN, M_STATE_STOP, M_STATE_BARK, M_STATE_CROUCH, M_STATE_STAND, M_STATE_ATTACK_1, M_STATE_ATTACK_2, M_STATE_ATTACK_3, M_STATE_DEATH, } M_STATE; typedef enum { M_ANIM_DEATH = 13, } M_ANIM; static const BITE m_DogBite = { .pos = { .x = 0, .y = 30, .z = 141 }, .mesh_num = 20, }; static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t head = 0; int16_t angle = 0; int16_t tilt = 0; if (item->hit_points > 0) { AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, false); if (info.ahead) { head = info.angle; } angle = Creature_Turn(item, creature->maximum_turn); switch (item->current_anim_state) { case M_STATE_WALK: creature->maximum_turn = M_WALK_TURN; if (creature->mood == MOOD_BORED) { const int16_t random = Random_GetControl(); if (random < M_BARK_CHANCE) { item->required_anim_state = M_STATE_BARK; item->goal_anim_state = M_STATE_STOP; } else if (random < M_CROUCH_CHANCE) { item->required_anim_state = M_STATE_CROUCH; item->goal_anim_state = M_STATE_STOP; } else if (random < M_STAND_CHANCE) { item->goal_anim_state = M_STATE_STOP; } } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_RUN: tilt = angle; creature->maximum_turn = M_RUN_TURN; if (creature->mood == MOOD_BORED) { item->goal_anim_state = M_STATE_STOP; } else if (info.distance < M_ATTACK_2_RANGE) { item->goal_anim_state = M_STATE_ATTACK_2; } break; case M_STATE_STOP: creature->maximum_turn = 0; creature->flags = 0; if (creature->mood != MOOD_BORED) { if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if (info.distance < M_ATTACK_1_RANGE && info.ahead) { item->goal_anim_state = M_STATE_ATTACK_1; } else { item->goal_anim_state = M_STATE_RUN; } } else if (item->required_anim_state != M_STATE_EMPTY) { item->goal_anim_state = item->required_anim_state; } else { const int16_t random = Random_GetControl(); if (random < M_BARK_CHANCE) { item->goal_anim_state = M_STATE_BARK; } else if (random < M_CROUCH_CHANCE) { item->goal_anim_state = M_STATE_CROUCH; } else if (random < M_WALK_CHANCE) { item->goal_anim_state = M_STATE_WALK; } } break; case M_STATE_BARK: if (creature->mood || Random_GetControl() < M_UNBARK_CHANCE) { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_CROUCH: if (creature->mood || Random_GetControl() < M_UNCROUCH_CHANCE) { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_STAND: if (creature->mood || Random_GetControl() < M_UNSTAND_CHANCE) { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_ATTACK_1: creature->maximum_turn = 0; if (creature->flags != 1 && info.ahead && (item->touch_bits & M_TOUCH_BITS) != 0) { Creature_Effect(item, &m_DogBite, Spawn_Blood); Lara_TakeDamage(M_BITE_DAMAGE, true); creature->flags = 1; } if (info.distance > M_ATTACK_1_RANGE && info.distance < M_ATTACK_3_RANGE) { item->goal_anim_state = M_STATE_ATTACK_3; } else { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_ATTACK_2: if (creature->flags != 2 && (item->touch_bits & M_TOUCH_BITS) != 0) { Creature_Effect(item, &m_DogBite, Spawn_Blood); Lara_TakeDamage(M_LUNGE_DAMAGE, true); creature->flags = 2; } if (info.distance < M_ATTACK_1_RANGE) { item->goal_anim_state = M_STATE_ATTACK_1; } else if (info.distance < M_ATTACK_3_RANGE) { item->goal_anim_state = M_STATE_ATTACK_3; } break; case M_STATE_ATTACK_3: creature->maximum_turn = M_RUN_TURN; if (creature->flags != 3 && (item->touch_bits & M_TOUCH_BITS) != 0) { Creature_Effect(item, &m_DogBite, Spawn_Blood); Lara_TakeDamage(M_LUNGE_DAMAGE, true); creature->flags = 3; } if (info.distance < M_ATTACK_1_RANGE) { item->goal_anim_state = M_STATE_ATTACK_1; } break; default: break; } } else if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, M_ANIM_DEATH, 0); item->current_anim_state = M_STATE_DEATH; } Creature_Tilt(item, tilt); Creature_Head(item, head); Creature_Animate(item_num, angle, tilt); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->hit_points = M_HITPOINTS; obj->radius = M_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->pivot_length = 300; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 19)->rot.y = true; } REGISTER_OBJECT(O_DOG, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/dragon.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // clang-format off #define M_CLOSE_SHIFT 900 #define M_FAR_SHIFT 2300 #define M_MID_SHIFT ((M_CLOSE_SHIFT + M_FAR_SHIFT) / 2) // = 1600 #define M_COL_L (-WALL_L / 2) // = -512 #define M_COL_R (+WALL_L / 2) // = +512 #define M_CLOSE_RANGE SQUARE(WALL_L * 3) // = 9437184 #define M_STOP_RANGE SQUARE(WALL_L * 6) // = 37748736 #define M_WALK_TURN (DEG_1 * 2) // = 364 #define M_NEED_TURN (DEG_1) // = 182 #define M_TOUCH_L 0x7F000000 #define M_TOUCH_R 0x000000FE #define M_ALMOST_LIVE 100 #define M_LIVE_TIME 330 #define M_ONE_PHASE_TIME 178 #define M_LIGHT_TIME (-20) #define M_BONE_TIME (-100) #define M_DISSOLVE_TIME (-240) #define M_DISSOLVE_SHIFT 10 #define M_HITPOINTS 300 #define M_TOUCH_DAMAGE 10 #define M_SWIPE_DAMAGE 250 #define M_RADIUS (WALL_L / 3) // = 341 // clang-format on typedef enum { M_STATE_EMPTY, M_STATE_WALK, M_STATE_LEFT, M_STATE_RIGHT, M_STATE_AIM, M_STATE_FIRE, M_STATE_STOP, M_STATE_TURN_LEFT, M_STATE_TURN_RIGHT, M_STATE_SWIPE_LEFT, M_STATE_SWIPE_RIGHT, M_STATE_DEATH, } M_STATE; typedef enum { // clang-format off M_ANIM_DIE = 21, M_ANIM_DEAD = 22, M_ANIM_RESURRECT = 23, // clang-format on } M_ANIM; typedef enum { M_MODE_ONE_PHASE, M_MODE_TWO_PHASE, } M_MODE; typedef struct { int16_t dragon_front_item_num; M_MODE mode; } M_PRIV; static const BITE m_DragonMouth = { .pos = { .x = 35, .y = 171, .z = 1168 }, .mesh_num = 12, }; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "mode", &p->mode)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "mode", p->mode); } static int16_t M_GetFrontItemNum(const ITEM *const dragon_back_item) { const M_PRIV *const p = dragon_back_item->priv; if (p == nullptr) { return NO_ITEM; } return p->dragon_front_item_num; } static bool M_IsTwoPhaseMode(const ITEM *const item) { const M_PRIV *const p = item->priv; return p != nullptr && p->mode == M_MODE_TWO_PHASE; } static bool M_CanDropItemsBack(const ITEM *const item) { return item->hit_points <= 0 && item->status == IS_DEACTIVATED; } static void M_InitialiseFront(const int16_t item_num) { ITEM *const item = Item_Get(item_num); item->include_in_kill_stats = false; } static void M_InitialiseBack(const int16_t item_num) { ITEM *const dragon_back_item = Item_Get(item_num); M_PRIV *const p = dragon_back_item->priv; p->mode = M_MODE_TWO_PHASE; dragon_back_item->status = IS_INVISIBLE; dragon_back_item->shade.value_1 = -1; dragon_back_item->mesh_bits = 0x1FFFFF; p->dragon_front_item_num = Item_CreateLevelItem(); ASSERT(p->dragon_front_item_num != NO_ITEM); ITEM *const dragon_front_item = Item_Get(p->dragon_front_item_num); dragon_front_item->object_id = O_DRAGON_FRONT; dragon_front_item->pos = dragon_back_item->pos; dragon_front_item->rot.y = dragon_back_item->rot.y; dragon_front_item->room_num = dragon_back_item->room_num; dragon_front_item->flags = IF_INVISIBLE; dragon_front_item->shade.value_1 = -1; Item_Initialise(p->dragon_front_item_num); } static bool M_TriggerBack(ITEM *const item, const TRIGGER *const trigger) { M_PRIV *const p = item->priv; p->mode = M_MODE_ONE_PHASE; return true; } static void M_ActivateBack(ITEM *const dragon_back_item) { if (dragon_back_item->active || dragon_back_item->status == IS_DEACTIVATED) { return; } const int16_t dragon_front_item_num = M_GetFrontItemNum(dragon_back_item); if (dragon_front_item_num == NO_ITEM) { return; } ITEM *const dragon_front_item = Item_Get(dragon_front_item_num); dragon_back_item->touch_bits = 0; dragon_front_item->touch_bits = 0; LOT_EnableBaddieAI(dragon_front_item_num, true); Item_AddActive(dragon_front_item_num); Item_AddActive(Item_GetIndex(dragon_back_item)); dragon_back_item->status = IS_ACTIVE; } static void M_MarkDragonDead(ITEM *const dragon_back_item) { const int16_t dragon_front_item_num = M_GetFrontItemNum(dragon_back_item); if (dragon_front_item_num == NO_ITEM) { return; } const ITEM *const dragon_front_item = Item_Get(dragon_front_item_num); CREATURE *const creature = dragon_front_item->creature_data; creature->flags = -1; Stats_AddKill(); // Allow drops to occur at the beginning of the cinematic camera for a // better window to avoid seeing the items spawn. const ITEM_STATUS current_status = dragon_back_item->status; dragon_back_item->status = IS_DEACTIVATED; Carrier_TestItemDrops(Item_GetIndex(dragon_back_item)); dragon_back_item->status = current_status; } static void M_PushLaraAway( ITEM *const lara_item, ITEM *const dragon_item, const int32_t shift) { const int32_t cy = Math_Cos(dragon_item->rot.y); const int32_t sy = Math_Sin(dragon_item->rot.y); const int32_t base = shift < M_MID_SHIFT ? M_CLOSE_SHIFT : M_FAR_SHIFT; lara_item->pos.x += (cy * (base - shift)) >> W2V_SHIFT; lara_item->pos.z -= (sy * (base - shift)) >> W2V_SHIFT; } static void M_PullDagger(ITEM *const lara_item, ITEM *const dragon_back_item) { lara_item->pos = dragon_back_item->pos; lara_item->rot = dragon_back_item->rot; lara_item->fall_speed = 0; lara_item->gravity = false; lara_item->speed = 0; const LARA_INFO *const lara = Lara_GetLaraInfo(); Item_UpdateRoom(lara->item_num, dragon_back_item->room_num); Lara_SwitchToExtraState(LS_EXTRA_PULL_DAGGER); Camera_InvokeCinematic(lara_item, 0, 0); M_MarkDragonDead(dragon_back_item); } static void M_Bones(const int16_t item_num) { const int16_t bone_front_item_num = Item_Create(); const int16_t bone_back_item_num = Item_Create(); if (bone_back_item_num == NO_ITEM || bone_front_item_num == NO_ITEM) { return; } const ITEM *const dragon_item = Item_Get(item_num); ITEM *const bone_back = Item_Get(bone_back_item_num); bone_back->object_id = O_DRAGON_BONES_3; bone_back->pos = dragon_item->pos; bone_back->rot.x = 0; bone_back->rot.y = dragon_item->rot.y; bone_back->rot.z = 0; bone_back->room_num = dragon_item->room_num; bone_back->shade.value_1 = -1; Item_Initialise(bone_back_item_num); ITEM *const bone_front = Item_Get(bone_front_item_num); bone_front->object_id = O_DRAGON_BONES_2; bone_front->pos = dragon_item->pos; bone_front->rot.x = 0; bone_front->rot.y = dragon_item->rot.y; bone_front->rot.z = 0; bone_front->room_num = dragon_item->room_num; bone_front->shade.value_1 = -1; Item_Initialise(bone_front_item_num); bone_front->mesh_bits = ~0xC00000u; } static void M_HandleSaveBack(ITEM *const item, const SAVEGAME_STAGE stage) { if (stage == SAVEGAME_STAGE_AFTER_LOAD) { if (item->status == IS_DEACTIVATED && M_IsTwoPhaseMode(item)) { const int32_t y_pos = item->pos.y; int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); item->pos.y = Room_GetHeight(sector, item->pos); const int16_t item_num = Item_GetIndex(item); M_Bones(item_num); item->pos.y = y_pos; } } } static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); if (!Item_TestBoundsCollide(item, lara_item, coll->radius)) { return; } if (!Collide_TestCollision(item, lara_item)) { return; } if (item->current_anim_state != M_STATE_DEATH) { Lara_Col_ItemPush(item, coll, true, false); return; } const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; const int32_t cy = Math_Cos(item->rot.y); const int32_t sy = Math_Sin(item->rot.y); const int32_t side_shift = (cy * dz + sy * dx) >> W2V_SHIFT; if (side_shift <= M_COL_L || side_shift >= M_COL_R) { Lara_Col_ItemPush(item, coll, true, false); return; } const int32_t shift = (cy * dx - sy * dz) >> W2V_SHIFT; const int32_t angle = lara_item->rot.y - item->rot.y; if (g_Input.action && item->object_id == O_DRAGON_BACK && M_IsTwoPhaseMode(item) && (Item_TestAnimEqual(item, M_ANIM_DEAD) || (Item_TestAnimEqual(item, M_ANIM_RESURRECT) && Item_TestFrameRange(item, 0, M_ALMOST_LIVE))) && !lara_item->gravity && shift <= M_MID_SHIFT && shift > M_CLOSE_SHIFT - 350 && side_shift > -350 && side_shift < 350 && angle > DEG_90 - 30 * DEG_1 && angle < DEG_90 + 30 * DEG_1) { M_PullDagger(lara_item, item); } else { M_PushLaraAway(lara_item, item, shift); } } static void M_ControlFront(const int16_t item_num) { } static void M_ControlBack(const int16_t item_num) { const int16_t dragon_back_item_num = item_num; ITEM *const dragon_back_item = Item_Get(item_num); const int16_t dragon_front_item_num = M_GetFrontItemNum(dragon_back_item); if (dragon_front_item_num == NO_ITEM) { return; } ITEM *const dragon_front_item = Item_Get(dragon_front_item_num); if (!Creature_Activate(dragon_front_item_num)) { return; } int16_t angle = 0; int16_t head = 0; CREATURE *const creature = dragon_front_item->creature_data; const OBJECT *const front_obj = Object_Get(O_DRAGON_FRONT); const bool is_two_phase = M_IsTwoPhaseMode(dragon_back_item); if (dragon_front_item->hit_points <= 0) { if (dragon_front_item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(dragon_front_item, M_ANIM_DIE, 0); dragon_front_item->goal_anim_state = M_STATE_DEATH; dragon_front_item->current_anim_state = M_STATE_DEATH; creature->flags = 0; } else if (creature->flags >= 0) { creature->flags++; if (is_two_phase) { Spawn_MysticLight(dragon_front_item_num); if (creature->flags == M_LIVE_TIME) { dragon_front_item->goal_anim_state = M_STATE_STOP; } if (creature->flags > M_LIVE_TIME + M_ALMOST_LIVE) { dragon_front_item->hit_points = front_obj->hit_points / 2; } } else if (creature->flags == M_ONE_PHASE_TIME) { M_MarkDragonDead(dragon_back_item); creature->flags = M_DISSOLVE_TIME; } } else { if (creature->flags > M_LIGHT_TIME) { Output_AddDynamicLight( dragon_front_item->pos, ((4 * Random_GetDraw()) >> 15) + 12 + creature->flags / 2, ((4 * Random_GetDraw()) >> 15) + 10 + creature->flags / 2); } if (creature->flags == M_BONE_TIME) { M_Bones(dragon_back_item_num); } else if (creature->flags == M_DISSOLVE_TIME) { Room_TestTriggers(dragon_back_item); LOT_DisableBaddieAI(dragon_front_item_num); dragon_front_item->status = IS_DEACTIVATED; dragon_back_item->status = IS_DEACTIVATED; if (is_two_phase) { Item_Kill(dragon_front_item_num); Item_Kill(dragon_back_item_num); } else { Item_RemoveActive(dragon_front_item_num); Item_RemoveActive(dragon_back_item_num); dragon_front_item->collidable = false; dragon_back_item->collidable = false; dragon_front_item->flags |= IF_ONE_SHOT; dragon_back_item->flags |= IF_ONE_SHOT; } } else if (creature->flags < M_BONE_TIME) { dragon_front_item->pos.y += M_DISSOLVE_SHIFT; dragon_back_item->pos.y += M_DISSOLVE_SHIFT; } creature->flags--; return; } } else { AI_INFO info; Creature_AIInfo(dragon_front_item, &info); Creature_Mood(dragon_front_item, &info, true); angle = Creature_Turn(dragon_front_item, M_WALK_TURN); const bool is_ahead = info.ahead && info.distance > M_CLOSE_RANGE && info.distance < M_STOP_RANGE; if (dragon_front_item->touch_bits) { Lara_TakeDamage(M_TOUCH_DAMAGE, true); } switch (dragon_front_item->current_anim_state) { case M_STATE_WALK: creature->flags = 0; if (is_ahead) { dragon_front_item->goal_anim_state = M_STATE_STOP; } else if (angle < -M_NEED_TURN) { if (info.distance < M_STOP_RANGE && info.ahead) { dragon_front_item->goal_anim_state = M_STATE_STOP; } else { dragon_front_item->goal_anim_state = M_STATE_LEFT; } } else if (angle > M_NEED_TURN) { if (info.distance < M_STOP_RANGE && info.ahead) { dragon_front_item->goal_anim_state = M_STATE_STOP; } else { dragon_front_item->goal_anim_state = M_STATE_RIGHT; } } break; case M_STATE_LEFT: if (angle > -M_NEED_TURN || is_ahead) { dragon_front_item->goal_anim_state = M_STATE_WALK; } break; case M_STATE_RIGHT: if (angle < M_NEED_TURN || is_ahead) { dragon_front_item->goal_anim_state = M_STATE_WALK; } break; case M_STATE_AIM: dragon_front_item->rot.y -= angle; if (info.ahead) { head = -info.angle; } if (is_ahead) { creature->flags = 30; dragon_front_item->goal_anim_state = M_STATE_FIRE; } else { creature->flags = 0; dragon_front_item->goal_anim_state = M_STATE_AIM; } break; case M_STATE_FIRE: dragon_front_item->rot.y -= angle; if (info.ahead) { head = -info.angle; } Sound_Effect(SFX_DRAGON_FIRE, &dragon_front_item->pos, SPM_NORMAL); if (creature->flags != 0) { if (info.ahead) { Creature_Effect( dragon_front_item, &m_DragonMouth, Spawn_FireStream); } creature->flags--; } else { dragon_front_item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_STOP: dragon_front_item->rot.y -= angle; if (is_ahead) { dragon_front_item->goal_anim_state = M_STATE_AIM; } else if (info.distance > M_STOP_RANGE || !info.ahead) { dragon_front_item->goal_anim_state = M_STATE_WALK; } else if (info.distance >= M_CLOSE_RANGE || creature->flags != 0) { if (info.angle < 0) { dragon_front_item->goal_anim_state = M_STATE_TURN_LEFT; } else { dragon_front_item->goal_anim_state = M_STATE_TURN_RIGHT; } } else { creature->flags = 1; if (info.angle < 0) { dragon_front_item->goal_anim_state = M_STATE_SWIPE_LEFT; } else { dragon_front_item->goal_anim_state = M_STATE_SWIPE_RIGHT; } } break; case M_STATE_TURN_LEFT: creature->flags = 0; dragon_front_item->rot.y += -DEG_1 - angle; break; case M_STATE_TURN_RIGHT: creature->flags = 0; dragon_front_item->rot.y += DEG_1 - angle; break; case M_STATE_SWIPE_LEFT: if ((dragon_front_item->touch_bits & M_TOUCH_L) != 0) { Lara_TakeDamage(M_SWIPE_DAMAGE, true); creature->flags = 0; } break; case M_STATE_SWIPE_RIGHT: if ((dragon_front_item->touch_bits & M_TOUCH_R) != 0) { Lara_TakeDamage(M_SWIPE_DAMAGE, true); creature->flags = 0; } break; default: break; } } Creature_Head(dragon_front_item, head); Creature_Animate(dragon_front_item_num, angle, 0); dragon_back_item->current_anim_state = dragon_front_item->current_anim_state; const int16_t anim_num = Item_GetRelativeAnim(dragon_front_item); const int16_t frame_num = Item_GetRelativeFrame(dragon_front_item); Item_SwitchToAnim(dragon_back_item, anim_num, frame_num); dragon_back_item->pos = dragon_front_item->pos; dragon_back_item->rot = dragon_front_item->rot; Item_UpdateRoom(dragon_back_item_num, dragon_front_item->room_num); } static void M_SetupFront(OBJECT *const obj) { if (!obj->loaded) { return; } SOFT_ASSERT( Object_Get(O_DRAGON_BACK)->loaded, "Dragon back object missing"); obj->initialise_func = M_InitialiseFront; obj->control_func = M_ControlFront; obj->collision_func = M_Collision; obj->hit_points = M_HITPOINTS; obj->radius = M_RADIUS; obj->pivot_length = 300; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 10)->rot.z = true; } static void M_SetupBack(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_InitialiseBack; obj->handle_save_func = M_HandleSaveBack; obj->trigger_func = M_TriggerBack; obj->activate_func = M_ActivateBack; obj->control_func = M_ControlBack; obj->collision_func = M_Collision; obj->can_drop_items_func = M_CanDropItemsBack; obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->radius = M_RADIUS; obj->save_position = true; obj->save_flags = true; obj->save_anim = true; obj->priv_size = sizeof(M_PRIV); } REGISTER_OBJECT(O_DRAGON_FRONT, M_SetupFront) REGISTER_OBJECT(O_DRAGON_BACK, M_SetupBack) ================================================ FILE: src/trx/game/objects/creatures/eel.c ================================================ #include #include #include #include #include #include // clang-format off #define EEL_HITPOINTS 5 #define EEL_TOUCH_BITS 0b00000001'10000000 // = 0x180 #define EEL_DAMAGE 50 #define EEL_ANGLE (DEG_1 * 10) // = 1820 #define EEL_RANGE (WALL_L * 2) // = 2048 #define EEL_MOVE (WALL_L / 10) // = 102 #define EEL_TURN (DEG_1 / 2) // = 91 #define EEL_LENGTH (WALL_L / 2) // = 512 #define EEL_SLIDE (EEL_RANGE - EEL_LENGTH) // = 1536 // clang-format on typedef enum { // clang-format off EEL_STATE_EMPTY = 0, EEL_STATE_ATTACK = 1, EEL_STATE_STOP = 2, EEL_STATE_DEATH = 3, // clang-format on } EEL_STATE; typedef enum { EEL_ANIM_DEATH = 3, } EEL_ANIM; typedef struct { int32_t pos; } M_PRIV; static const BITE m_EelBite = { .pos = { .x = 7, .y = 157, .z = 333 }, .mesh_num = 7, }; static bool M_IsTargetable(const ITEM *const item) { return false; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; int32_t pos = p->pos; item->pos.x -= (pos * Math_Sin(item->rot.y)) >> W2V_SHIFT; item->pos.z -= (pos * Math_Cos(item->rot.y)) >> W2V_SHIFT; if (item->hit_points <= 0) { if (pos < EEL_SLIDE) { pos += EEL_MOVE; } if (item->current_anim_state != EEL_STATE_DEATH) { Item_SwitchToAnim(item, EEL_ANIM_DEATH, 0); item->current_anim_state = EEL_STATE_DEATH; } } else { const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; const int16_t quadrant = (item->rot.y + DEG_45) & 0xC000; const int16_t angle = Math_Atan(dz, dx); const int32_t distance = Math_Sqrt(SQUARE(dx) + SQUARE(dz)); switch (item->current_anim_state) { case EEL_STATE_STOP: if (pos > 0) { pos -= EEL_MOVE; } if (distance <= EEL_RANGE && ABS(angle - quadrant) < EEL_ANGLE) { item->goal_anim_state = EEL_STATE_ATTACK; } break; case EEL_STATE_ATTACK: if (pos < distance - EEL_LENGTH) { pos += EEL_MOVE; } if (angle < item->rot.y - EEL_TURN) { item->rot.y -= EEL_TURN; } else if (angle > item->rot.y + EEL_TURN) { item->rot.y += EEL_TURN; } if (item->required_anim_state == EEL_STATE_EMPTY && (item->touch_bits & EEL_TOUCH_BITS) != 0) { Lara_TakeDamage(EEL_DAMAGE, true); Creature_Effect(item, &m_EelBite, Spawn_Blood); item->required_anim_state = EEL_STATE_STOP; } break; } } item->pos.x += (pos * Math_Sin(item->rot.y)) >> W2V_SHIFT; item->pos.z += (pos * Math_Cos(item->rot.y)) >> W2V_SHIFT; p->pos = pos; Item_Animate(item); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->is_targetable_func = M_IsTargetable; obj->priv_size = sizeof(M_PRIV); obj->hit_points = EEL_HITPOINTS; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_EEL, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/hybrid_mutant.c ================================================ #include #include #include #include // clang-format off #define M_RADIUS (WALL_L / 8) // = 128 #define M_HIT_POINTS 90 #define M_SLASH_DAMAGE 100 #define M_KICK_DAMAGE 80 #define M_JUMP_DAMAGE 20 #define M_TOUCH_BITS_LEFT 0b00000000'10000000 #define M_TOUCH_BITS_RIGHT 0b00001000'00000000 #define M_ALERT_DIST SQUARE(WALL_L) // = 1048576 #define M_ATTACK_DIST_1 SQUARE(WALL_L) // = 1048576 #define M_ATTACK_DIST_2 SQUARE(WALL_L * 2) // = 4194304 #define M_ATTACK_DIST_3 SQUARE(WALL_L * 4 / 3) // = 1863225 #define M_JUMP_ANGLE DEG_45 #define M_WALK_TURN (DEG_1 * 3) // = 546 #define M_RUN_TURN (DEG_1 * 6) // = 1092 // clang-format on typedef enum { M_STATE_NULL, M_STATE_STOP, M_STATE_WALK, M_STATE_RUN, M_STATE_JUMP_START, M_STATE_JUMP_MID, M_STATE_JUMP_END, M_STATE_SLASH, M_STATE_KICK, M_STATE_RUN_ATTACK, M_STATE_WALK_ATTACK, M_STATE_DEATH, } M_STATE; typedef enum { M_ANIM_DEATH = 18, } M_ANIM; static const BITE m_BiteLeft = { .pos = { 19, -13, 3 }, .mesh_num = 7, }; static const BITE m_BiteRight = { .pos = { 19, -13, 3 }, .mesh_num = 14, }; static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t tilt = 0; int16_t angle = 0; int16_t head = 0; int16_t torso_x = 0; int16_t torso_y = 0; if (item->hit_points <= 0) { if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, M_ANIM_DEATH, 0); item->current_anim_state = M_STATE_DEATH; } goto finish; } if (item->ai_bits != 0) { Creature_GetAITarget(creature); } AI_INFO info = {}; Creature_AIInfo(item, &info); ITEM *const lara_item = Lara_GetItem(); AI_INFO lara_info = {}; if (creature->enemy == lara_item) { lara_info.angle = info.angle; lara_info.distance = info.distance; } else { const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; lara_info.angle = Math_Atan(dz, dx) - item->rot.y; lara_info.distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz }); } Creature_Mood(item, &info, true); angle = Creature_Turn(item, creature->maximum_turn); ITEM *const enemy = creature->enemy; creature->enemy = lara_item; if ((lara_info.distance < M_ALERT_DIST || item->hit_status || Creature_CanSeeEnemy(item, &lara_info))) { Creature_AlertAllGuards(item_num); } creature->enemy = enemy; const LARA_INFO *const lara = Lara_GetLaraInfo(); switch (item->current_anim_state) { case M_STATE_STOP: head = info.angle; creature->maximum_turn = 0; creature->flags = 0; if ((item->ai_bits & AI_GUARD) != 0) { head = Creature_AIGuard(creature); item->goal_anim_state = M_STATE_STOP; } else if ((item->ai_bits & AI_PATROL_1) != 0) { item->goal_anim_state = M_STATE_WALK; head = 0; } else if (creature->mood == MOOD_ESCAPE) { if (lara->target == item || !info.ahead) { item->goal_anim_state = M_STATE_RUN; } else { item->goal_anim_state = M_STATE_STOP; } } else if ( info.angle < M_JUMP_ANGLE && info.angle > -M_JUMP_ANGLE && info.distance > M_ATTACK_DIST_1) { item->goal_anim_state = M_STATE_JUMP_START; } else if (info.bite && info.distance < M_ATTACK_DIST_1) { torso_x = info.x_angle; torso_y = info.angle; if (info.angle < 0) { item->goal_anim_state = M_STATE_SLASH; } else { item->goal_anim_state = M_STATE_KICK; } } else if (item->required_anim_state != M_STATE_NULL) { item->goal_anim_state = item->required_anim_state; } else if (info.distance < M_ATTACK_DIST_2) { item->goal_anim_state = M_STATE_WALK; } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_WALK: if (info.ahead) { head = info.angle; } creature->maximum_turn = M_WALK_TURN; if ((item->ai_bits & AI_PATROL_1) != 0) { item->goal_anim_state = M_STATE_WALK; head = 0; } else if (info.bite && info.distance < M_ATTACK_DIST_3) { creature->maximum_turn = M_WALK_TURN; item->goal_anim_state = M_STATE_WALK_ATTACK; } else if ( creature->mood == MOOD_ESCAPE || creature->mood == MOOD_ATTACK) { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_RUN: if (info.ahead) { head = info.angle; } creature->maximum_turn = M_RUN_TURN; if ((item->ai_bits & AI_GUARD) != 0) { item->goal_anim_state = M_STATE_STOP; } else if ( creature->mood == MOOD_BORED || (creature->mood == MOOD_ESCAPE && lara->target != item && info.ahead)) { item->goal_anim_state = M_STATE_STOP; } else if (creature->flags != 0 && info.ahead) { item->goal_anim_state = M_STATE_STOP; } else if (info.bite && info.distance < M_ATTACK_DIST_2) { if (lara_item->speed != 0) { item->goal_anim_state = M_STATE_RUN_ATTACK; } else { item->goal_anim_state = M_STATE_STOP; } } else if (info.distance < M_ATTACK_DIST_2) { item->goal_anim_state = M_STATE_WALK; } creature->flags = 0; break; case M_STATE_JUMP_START: creature->maximum_turn = M_WALK_TURN; break; case M_STATE_JUMP_MID: case M_STATE_JUMP_END: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } creature->maximum_turn = 0; if ((item->touch_bits & M_TOUCH_BITS_LEFT) != 0) { Lara_TakeDamage(M_JUMP_DAMAGE, true); Creature_Effect(item, &m_BiteLeft, Spawn_Blood); } break; case M_STATE_SLASH: case M_STATE_RUN_ATTACK: case M_STATE_WALK_ATTACK: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } creature->maximum_turn = M_WALK_TURN; if (creature->flags == 0 && (item->touch_bits & M_TOUCH_BITS_LEFT) != 0) { Lara_TakeDamage(M_SLASH_DAMAGE, true); Creature_Effect(item, &m_BiteLeft, Spawn_Blood); creature->flags = 1; } if (!info.bite || info.distance >= M_ATTACK_DIST_1) { item->goal_anim_state = M_STATE_STOP; } if (Item_TestFrameEqual(item, -1)) { creature->flags = 0; } break; case M_STATE_KICK: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } creature->maximum_turn = M_WALK_TURN; if (creature->flags == 0 && (item->touch_bits & M_TOUCH_BITS_RIGHT) != 0) { Lara_TakeDamage(M_KICK_DAMAGE, true); Creature_Effect(item, &m_BiteRight, Spawn_Blood); creature->flags = 1; } if (!info.bite || info.distance >= M_ATTACK_DIST_1) { item->goal_anim_state = M_STATE_STOP; } if (Item_TestFrameEqual(item, -1)) { creature->flags = 0; } break; } finish: Creature_Tilt(item, 0); Creature_Joint(item, 0, torso_x); Creature_Joint(item, 1, torso_y); Creature_Joint(item, 2, head); Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->collision_func = Creature_Collision; obj->control_func = M_Control; obj->shadow_size = UNIT_SHADOW / 2; obj->radius = M_RADIUS; obj->hit_points = M_HIT_POINTS; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 0)->rot.x = true; Object_GetBone(obj, 0)->rot.z = true; Object_GetBone(obj, 7)->rot.y = true; } REGISTER_OBJECT(O_HYBRID_MUTANT, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/jelly.c ================================================ #include #include #include #include #include // clang-format off #define JELLY_HITPOINTS 10 #define JELLY_RADIUS (WALL_L / 10) // = 102 #define JELLY_STING_DAMAGE 5 #define JELLY_TURN (DEG_1 * 90) // = 16380 // clang-format on typedef enum { // clang-format off JELLY_STATE_EMPTY = 0, JELLY_STATE_MOVE = 1, JELLY_STATE_STOP = 2, // clang-format on } JELLY_STATE; static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; if (item->hit_points <= 0) { if (Item_Explode(item_num, -1, 0)) { LOT_DisableBaddieAI(item_num); Item_Kill(item_num); item->status = IS_DEACTIVATED; } } else { AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, false); int16_t angle = Creature_Turn(item, JELLY_TURN); switch (item->current_anim_state) { case JELLY_STATE_STOP: if (creature->mood != MOOD_BORED) { item->goal_anim_state = JELLY_STATE_MOVE; } break; case JELLY_STATE_MOVE: if (creature->mood == MOOD_BORED || item->touch_bits != 0) { item->goal_anim_state = JELLY_STATE_STOP; } break; } if (item->touch_bits != 0) { Lara_TakeDamage(JELLY_STING_DAMAGE, true); } Creature_Head(item, 0); Creature_Animate(item_num, angle, 0); Creature_Underwater(item, 0); } } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->hit_points = JELLY_HITPOINTS; obj->radius = JELLY_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->lot_setup = LOT_Setup(LOT_SETUP_FLYER); obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_JELLY, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/larson.c ================================================ #include #include #include #include #include #include #include #define LARSON_POSE_CHANCE 0x60 // = 96 #define LARSON_SHOT_DAMAGE 50 #define LARSON_WALK_TURN (DEG_1 * 3) // = 546 #define LARSON_RUN_TURN (DEG_1 * 6) // = 1092 #define LARSON_WALK_RANGE SQUARE(WALL_L * 3) // = 9437184 #define LARSON_DIE_ANIM 15 #define LARSON_HITPOINTS 50 #define LARSON_RADIUS (WALL_L / 10) // = 102 #define LARSON_SMARTNESS 0x7FFF typedef enum { LARSON_STATE_EMPTY = 0, LARSON_STATE_STOP = 1, LARSON_STATE_WALK = 2, LARSON_STATE_RUN = 3, LARSON_STATE_AIM = 4, LARSON_STATE_DEATH = 5, LARSON_STATE_POSE = 6, LARSON_STATE_SHOOT = 7, } LARSON_STATE; static const CREATURE_GUN m_LarsonGun = { .muzzle = { .pos = { -60, 170, 0 }, .mesh_num = 14 }, }; static void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage) { if (stage == SAVEGAME_STAGE_AFTER_LOAD) { if (item->hit_points <= 0) { const uint16_t flags = Music_GetTrackFlags(Music_ToGameID(MX_LARSON_SPEECH)); Music_SetTrackFlags( Music_ToGameID(MX_LARSON_SPEECH), flags | IF_ONE_SHOT); } } } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const person = item->creature_data; int16_t head = 0; int16_t angle = 0; int16_t tilt = 0; if (item->hit_points <= 0) { if (item->current_anim_state != LARSON_STATE_DEATH) { item->current_anim_state = LARSON_STATE_DEATH; Item_SwitchToAnim(item, LARSON_DIE_ANIM, 0); } } else { AI_INFO info; Creature_AIInfo(item, &info); if (info.ahead) { head = info.angle; } Creature_Mood(item, &info, false); angle = Creature_Turn(item, person->maximum_turn); switch (item->current_anim_state) { case LARSON_STATE_STOP: if (item->required_anim_state) { item->goal_anim_state = item->required_anim_state; } else if (person->mood == MOOD_BORED) { item->goal_anim_state = Random_GetControl() < LARSON_POSE_CHANCE ? LARSON_STATE_POSE : LARSON_STATE_WALK; } else if (person->mood == MOOD_ESCAPE) { item->goal_anim_state = LARSON_STATE_RUN; } else { item->goal_anim_state = LARSON_STATE_WALK; } break; case LARSON_STATE_POSE: if (person->mood != MOOD_BORED) { item->goal_anim_state = LARSON_STATE_STOP; } else if (Random_GetControl() < LARSON_POSE_CHANCE) { item->required_anim_state = LARSON_STATE_WALK; item->goal_anim_state = LARSON_STATE_STOP; } break; case LARSON_STATE_WALK: person->maximum_turn = LARSON_WALK_TURN; if (person->mood == MOOD_BORED && Random_GetControl() < LARSON_POSE_CHANCE) { item->required_anim_state = LARSON_STATE_POSE; item->goal_anim_state = LARSON_STATE_STOP; } else if (person->mood == MOOD_ESCAPE) { item->required_anim_state = LARSON_STATE_RUN; item->goal_anim_state = LARSON_STATE_STOP; } else if (Creature_CanTargetEnemy(item, &info)) { item->required_anim_state = LARSON_STATE_AIM; item->goal_anim_state = LARSON_STATE_STOP; } else if (!info.ahead || info.distance > LARSON_WALK_RANGE) { item->required_anim_state = LARSON_STATE_RUN; item->goal_anim_state = LARSON_STATE_STOP; } break; case LARSON_STATE_RUN: person->maximum_turn = LARSON_RUN_TURN; tilt = angle / 2; if (person->mood == MOOD_BORED && Random_GetControl() < LARSON_POSE_CHANCE) { item->required_anim_state = LARSON_STATE_POSE; item->goal_anim_state = LARSON_STATE_STOP; } else if (Creature_CanTargetEnemy(item, &info)) { item->required_anim_state = LARSON_STATE_AIM; item->goal_anim_state = LARSON_STATE_STOP; } else if (info.ahead && info.distance < LARSON_WALK_RANGE) { item->required_anim_state = LARSON_STATE_WALK; item->goal_anim_state = LARSON_STATE_STOP; } break; case LARSON_STATE_AIM: if (item->required_anim_state) { item->goal_anim_state = item->required_anim_state; } else if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = LARSON_STATE_SHOOT; } else { item->goal_anim_state = LARSON_STATE_STOP; } break; case LARSON_STATE_SHOOT: if (!item->required_anim_state) { Creature_Shoot( item, &info, &m_LarsonGun, head, LARSON_SHOT_DAMAGE); item->required_anim_state = LARSON_STATE_AIM; } if (person->mood == MOOD_ESCAPE) { item->required_anim_state = LARSON_STATE_STOP; } break; } } Creature_Tilt(item, tilt); Creature_Head(item, head); Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = Creature_Initialise; obj->handle_save_func = M_HandleSave; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->shadow_size = UNIT_SHADOW / 2; obj->hit_points = LARSON_HITPOINTS; obj->radius = LARSON_RADIUS; obj->smartness = LARSON_SMARTNESS; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_anim = true; obj->save_flags = true; Object_GetBone(obj, 6)->rot.y = true; } REGISTER_OBJECT(O_LARSON, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/lion.c ================================================ #include #include #include #include #include #include #define LION_BITE_DAMAGE 250 #define LION_POUNCE_DAMAGE 150 #define LION_TOUCH 0x380066 #define LION_WALK_TURN (2 * DEG_1) // = 364 #define LION_RUN_TURN (5 * DEG_1) // = 910 #define LION_ROAR_CHANCE 128 #define LION_POUNCE_RANGE SQUARE(WALL_L) // = 1048576 #define LION_DIE_ANIM 7 #define LION_HITPOINTS 30 #define LION_RADIUS (WALL_L / 3) // = 341 #define LION_SMARTNESS 0x7FFF #define LIONESS_HITPOINTS 25 #define LIONESS_RADIUS (WALL_L / 3) // = 341 #define LIONESS_SMARTNESS 0x2000 #define PUMA_DIE_ANIM 4 #define PUMA_HITPOINTS 45 #define PUMA_RADIUS (WALL_L / 3) // = 341 #define PUMA_SMARTNESS 0x2000 typedef enum { LION_STATE_EMPTY = 0, LION_STATE_STOP = 1, LION_STATE_WALK = 2, LION_STATE_RUN = 3, LION_STATE_ATTACK_1 = 4, LION_STATE_DEATH = 5, LION_STATE_WARNING = 6, LION_STATE_ATTACK_2 = 7, } LION_STATE; static BITE m_LionBite = { .pos = { -2, -10, 132 }, .mesh_num = 21 }; static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const lion = item->creature_data; int16_t head = 0; int16_t angle = 0; int16_t tilt = 0; if (item->hit_points <= 0) { if (item->current_anim_state != LION_STATE_DEATH) { item->current_anim_state = LION_STATE_DEATH; int16_t anim_idx = item->object_id == O_PUMA ? PUMA_DIE_ANIM : LION_DIE_ANIM; Item_SwitchToAnim( item, anim_idx + (int16_t)(Random_GetControl() / 0x4000), 0); } } else { AI_INFO info; Creature_AIInfo(item, &info); if (info.ahead) { head = info.angle; } Creature_Mood(item, &info, true); angle = Creature_Turn(item, lion->maximum_turn); switch (item->current_anim_state) { case LION_STATE_STOP: if (item->required_anim_state) { item->goal_anim_state = item->required_anim_state; } else if (lion->mood == MOOD_BORED) { item->goal_anim_state = LION_STATE_WALK; } else if (info.ahead && (item->touch_bits & LION_TOUCH)) { item->goal_anim_state = LION_STATE_ATTACK_2; } else if (info.ahead && info.distance < LION_POUNCE_RANGE) { item->goal_anim_state = LION_STATE_ATTACK_1; } else { item->goal_anim_state = LION_STATE_RUN; } break; case LION_STATE_WALK: lion->maximum_turn = LION_WALK_TURN; if (lion->mood != MOOD_BORED) { item->goal_anim_state = LION_STATE_STOP; } else if (Random_GetControl() < LION_ROAR_CHANCE) { item->required_anim_state = LION_STATE_WARNING; item->goal_anim_state = LION_STATE_STOP; } break; case LION_STATE_RUN: lion->maximum_turn = LION_RUN_TURN; tilt = angle; if (lion->mood == MOOD_BORED) { item->goal_anim_state = LION_STATE_STOP; } else if (info.ahead && info.distance < LION_POUNCE_RANGE) { item->goal_anim_state = LION_STATE_STOP; } else if ((item->touch_bits & LION_TOUCH) && info.ahead) { item->goal_anim_state = LION_STATE_STOP; } else if ( lion->mood != MOOD_ESCAPE && Random_GetControl() < LION_ROAR_CHANCE) { item->required_anim_state = LION_STATE_WARNING; item->goal_anim_state = LION_STATE_STOP; } break; case LION_STATE_ATTACK_1: if (item->required_anim_state == LION_STATE_EMPTY && (item->touch_bits & LION_TOUCH)) { Lara_TakeDamage(LION_POUNCE_DAMAGE, true); item->required_anim_state = LION_STATE_STOP; } break; case LION_STATE_ATTACK_2: if (item->required_anim_state == LION_STATE_EMPTY && (item->touch_bits & LION_TOUCH)) { Creature_Effect(item, &m_LionBite, Spawn_Blood); Lara_TakeDamage(LION_BITE_DAMAGE, true); item->required_anim_state = LION_STATE_STOP; } break; } } Creature_Tilt(item, tilt); Creature_Head(item, head); Creature_Animate(item_num, angle, tilt); } static void M_SetupBase(OBJECT *const obj) { obj->initialise_func = Creature_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->shadow_size = UNIT_SHADOW / 2; obj->lot_setup = LOT_Setup(LOT_SETUP_QUADRUPED); obj->pivot_length = 400; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_anim = true; obj->save_flags = true; Object_GetBone(obj, 19)->rot.y = true; } static void M_SetupLion(OBJECT *const obj) { if (!obj->loaded) { return; } M_SetupBase(obj); obj->hit_points = LION_HITPOINTS; obj->radius = LION_RADIUS; obj->smartness = LION_SMARTNESS; } static void M_SetupLioness(OBJECT *const obj) { if (!obj->loaded) { return; } M_SetupBase(obj); obj->hit_points = LIONESS_HITPOINTS; obj->radius = LIONESS_RADIUS; obj->smartness = LIONESS_SMARTNESS; } static void M_SetupPuma(OBJECT *const obj) { if (!obj->loaded) { return; } M_SetupBase(obj); obj->hit_points = PUMA_HITPOINTS; obj->radius = PUMA_RADIUS; obj->smartness = PUMA_SMARTNESS; } REGISTER_OBJECT(O_LION, M_SetupLion) REGISTER_OBJECT(O_LIONESS, M_SetupLioness) REGISTER_OBJECT(O_PUMA, M_SetupPuma) ================================================ FILE: src/trx/game/objects/creatures/lizard.c ================================================ #include #include #include #include #include #include #include #include #include // clang-format off #define M_SWIPE_DAMAGE 120 #define M_BITE_DAMAGE 100 #define M_RUN_TURN (DEG_1 * 4) // = 728 #define M_WALK_TURN (DEG_1 * 10) // = 1820 #define M_ATTACK_0_RANGE SQUARE(WALL_L * 5 / 2) // = 0x640000 #define M_ATTACK_1_RANGE SQUARE(WALL_L * 3 / 4) // = 0x90000 #define M_ATTACK_2_RANGE SQUARE(WALL_L * 3 / 2) // = 0x240000 #define M_WALK_RANGE SQUARE(WALL_L * 2) #define M_WALK_CHANCE 0x100 #define M_WAIT_CHANCE 0x100 #define M_BITE_TOUCH_BITS 0xC00 #define M_SWIPE_TOUCH_BITS 0x20 #define M_VAULT_SHIFT 260 // clang-format on typedef enum { M_STATE_NULL, M_STATE_STOP, M_STATE_WALK, M_STATE_PUNCH_2, M_STATE_AIM_2, M_STATE_WAIT, M_STATE_AIM_1, M_STATE_AIM_0, M_STATE_PUNCH_1, M_STATE_PUNCH_B, M_STATE_RUN, M_STATE_DEATH, M_STATE_CLIMB_3, M_STATE_CLIMB_1, M_STATE_CLIMB_2, M_STATE_FALL_3 } M_STATE; typedef enum { M_ANIM_SLIDE_1 = 23, M_ANIM_DEATH = 26, M_ANIM_CLIMB_3 = 27, M_ANIM_CLIMB_1 = 28, M_ANIM_CLIMB_2 = 29, M_ANIM_FALL_3 = 30, M_ANIM_SLIDE_2 = 31, } M_ANIM; static BITE m_BiteHit = { .pos = { .x = 0, .y = -120, .z = 120 }, .mesh_num = 10, }; static BITE m_SwipeHit = { .pos = { .x = 0, .y = 0, .z = 0 }, .mesh_num = 5, }; static BITE m_GasHit = { .pos = { .x = 0, .y = -64, .z = 56 }, .mesh_num = 9, }; static void M_TriggerGas( const XYZ_32 pos, const XYZ_32 vel, const int32_t effect_num) { SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 0; spark->src_color.g = (Random_GetControl() & 0x3F) + 128; spark->src_color.b = 32; spark->dst_color.r = 0; spark->dst_color.g = (Random_GetControl() & 0xF) + 32; spark->dst_color.b = 0; if (vel.x != 0 || vel.y != 0 || vel.z != 0) { spark->col_fade_speed = 6; spark->fade_to_black = 2; spark->life = (Random_GetControl() & 1) + 12; } else { spark->col_fade_speed = 8; spark->fade_to_black = 16; spark->life = (Random_GetControl() & 3) + 20; } spark->s_life = spark->life; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16; spark->pos.y = pos.y; spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16; spark->vel.x = vel.x + (Random_GetControl() & 0xF) - 16; spark->vel.y = vel.y; spark->vel.z = vel.z + (Random_GetControl() & 0xF) - 16; spark->friction = 0; if (Random_GetControl() & 1) { if (effect_num < 0) { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->rot_angle = Random_GetControl() & 0xFFF; if (Random_GetControl() & 1) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = (Random_GetControl() & 0xF) + 16; } } else if (effect_num < 0) { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE; } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->max_y_vel = 0; spark->effect_num = (uint8_t)effect_num; spark->gravity = 0; spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; const int32_t size = (Random_GetControl() & 0x1F) + 48; if (vel.x != 0 || vel.y != 0 || vel.z != 0) { spark->size.width = size >> 5; spark->src_size.width = spark->size.width; spark->dst_size.width = size >> 1; spark->size.height = spark->size.width; spark->src_size.height = spark->size.height; spark->dst_size.height = spark->dst_size.width; if (effect_num == -2) { spark->scalar = 2; } else { spark->scalar = 3; } } else { spark->scalar = 4; spark->size.width = size >> 4; spark->src_size.width = spark->size.width; spark->dst_size.width = size >> 1; spark->size.height = spark->size.width; spark->src_size.height = spark->size.height; spark->dst_size.height = spark->dst_size.width; } Sparks_FinishSetup(spark); } static int16_t M_TriggerGasThrower( const ITEM *const item, const BITE *const bite, const int16_t speed) { const int16_t effect_num = Effect_Create(item->room_num); if (effect_num == NO_ITEM) { return NO_ITEM; } XYZ_32 pos = bite->pos; Collide_GetJointAbsPosition(item, &pos, bite->mesh_num); XYZ_32 pos1 = { .x = bite->pos.x, .y = bite->pos.y << 3, .z = bite->pos.z << 2, }; Collide_GetJointAbsPosition(item, &pos1, bite->mesh_num); int16_t angles[2]; Math_GetVectorAngles( pos1.x - pos.x, pos1.y - pos.y, pos1.z - pos.z, angles); EFFECT *const effect = Effect_Get(effect_num); effect->pos = pos; effect->room_num = item->room_num; effect->rot.x = angles[1]; effect->rot.z = 0; effect->rot.y = angles[0]; effect->speed = speed << 2; effect->object_id = O_MISSILE_POISON; effect->counter = 20; M_TriggerGas((XYZ_32) {}, (XYZ_32) {}, effect_num); for (int32_t i = 0; i < 2; i++) { const int32_t s = Random_GetControl() % (speed << 2) + 32; const int32_t r = (s * Math_Cos(effect->rot.x)) >> W2V_SHIFT; XYZ_32 vel = { .x = (r * Math_Sin(effect->rot.y)) >> W2V_SHIFT, .y = -((s * Math_Sin(effect->rot.x)) >> W2V_SHIFT), .z = (r * Math_Cos(effect->rot.y)) >> W2V_SHIFT, }; M_TriggerGas( effect->pos, (XYZ_32) { vel.x << 5, vel.y << 5, vel.z << 5 }, -1); } { const int32_t r = ((speed << 1) * Math_Cos(effect->rot.x)) >> W2V_SHIFT; const XYZ_32 vel = { .x = (r * Math_Sin(effect->rot.y)) >> W2V_SHIFT, .y = -(((speed << 1) * Math_Sin(effect->rot.x)) >> W2V_SHIFT), .z = (r * Math_Cos(effect->rot.y)) >> W2V_SHIFT, }; M_TriggerGas( effect->pos, (XYZ_32) { vel.x << 5, vel.y << 5, vel.z << 5 }, -2); } return effect_num; } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t tilt = 0; int16_t angle = 0; int16_t neck = 0; if (item->hit_points <= 0) { if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, M_ANIM_DEATH, 0); item->current_anim_state = M_STATE_DEATH; } else if ( TribeBoss_IsLizardActive() && Item_GetRelativeFrame(item) == 50) { Creature_Die(item_num, true); } } else { AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, true); if (Box_GetBox(creature->enemy->box_num)->overlap_index & BOX_BLOCKED_SEARCH) { creature->mood = MOOD_ATTACK; } LARA_INFO *const lara_info = Lara_GetLaraInfo(); angle = Creature_Turn(item, creature->maximum_turn); switch (item->current_anim_state) { case M_STATE_STOP: creature->flags = 0; if (info.ahead) { neck = info.angle; } creature->maximum_turn = 0; if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if (creature->mood == MOOD_BORED) { if (item->required_anim_state) { item->goal_anim_state = item->required_anim_state; } else if (Random_GetControl() < 0x4000) { item->goal_anim_state = M_STATE_WALK; } else { item->goal_anim_state = M_STATE_WAIT; } } else if (info.bite && info.distance < M_ATTACK_1_RANGE) { item->goal_anim_state = M_STATE_AIM_1; } else if ( Creature_CanTargetEnemy(item, &info) && info.bite && info.distance < M_ATTACK_0_RANGE && (lara_info->poison_timer < 256 || (Box_GetBox(creature->enemy->box_num)->overlap_index & BOX_BLOCKED_SEARCH))) { item->goal_anim_state = M_STATE_AIM_0; } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_WALK: if (info.ahead) { neck = info.angle; } if (Item_GetRelativeAnim(item) == M_ANIM_SLIDE_1 || Item_GetRelativeAnim(item) == M_ANIM_SLIDE_2) { creature->maximum_turn = 0; } else { creature->maximum_turn = M_WALK_TURN; } if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if (creature->mood == MOOD_BORED) { if (Random_GetControl() < M_WAIT_CHANCE) { item->required_anim_state = M_STATE_WAIT; item->goal_anim_state = M_STATE_STOP; } } else if (info.bite && info.distance < M_ATTACK_1_RANGE) { item->goal_anim_state = M_STATE_STOP; } else if (info.bite && info.distance < M_ATTACK_2_RANGE) { item->goal_anim_state = M_STATE_AIM_2; } else if ( Creature_CanTargetEnemy(item, &info) && info.distance < M_ATTACK_0_RANGE && (lara_info->poison_timer < 256 || (Box_GetBox(creature->enemy->box_num)->overlap_index & BOX_BLOCKED_SEARCH))) { item->goal_anim_state = M_STATE_STOP; } else if (info.distance > M_WALK_RANGE) { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_PUNCH_2: if (info.ahead) { neck = info.angle; } if (creature->flags != 2 && item->touch_bits & M_BITE_TOUCH_BITS) { Lara_TakeDamage(M_BITE_DAMAGE, true); Creature_Effect(item, &m_BiteHit, Spawn_Blood); creature->flags = 2; } break; case M_STATE_AIM_2: if (info.ahead) { neck = info.angle; } creature->maximum_turn = M_WALK_TURN; creature->flags = 0; if (info.bite && info.distance < M_ATTACK_2_RANGE) { item->goal_anim_state = M_STATE_PUNCH_2; } else { item->goal_anim_state = M_STATE_WALK; } break; case M_STATE_WAIT: if (info.ahead) { neck = info.angle; } creature->maximum_turn = 0; if (creature->mood != MOOD_BORED) { item->goal_anim_state = M_STATE_STOP; } else if (Random_GetControl() < M_WALK_CHANCE) { item->required_anim_state = M_STATE_WALK; item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_AIM_1: if (info.ahead) { neck = info.angle; } creature->maximum_turn = M_WALK_TURN; creature->flags = 0; if (info.ahead && info.distance < M_ATTACK_1_RANGE) { item->goal_anim_state = M_STATE_PUNCH_1; } else { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_AIM_0: if (info.ahead) { neck = info.angle; } creature->maximum_turn = 0; if (ABS(info.angle) < M_RUN_TURN) { item->rot.y += info.angle; } else if (info.angle < 0) { item->rot.y -= M_RUN_TURN; } else { item->rot.y += M_RUN_TURN; } if (info.bite && info.distance < M_ATTACK_0_RANGE && (lara_info->poison_timer < 256 || (Box_GetBox(creature->enemy->box_num)->overlap_index & BOX_BLOCKED_SEARCH))) { item->goal_anim_state = M_STATE_PUNCH_B; } else { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_PUNCH_1: if (info.ahead) { neck = info.angle; } if (!creature->flags && item->touch_bits & M_SWIPE_TOUCH_BITS) { Lara_TakeDamage(M_SWIPE_DAMAGE, true); Creature_Effect(item, &m_SwipeHit, Spawn_Blood); creature->flags = 1; } if (info.distance < M_ATTACK_2_RANGE) { item->goal_anim_state = M_STATE_PUNCH_2; } break; case M_STATE_PUNCH_B: if (info.ahead) { neck = info.angle; } if (ABS(info.angle) < M_RUN_TURN) { item->rot.y += info.angle; } else if (info.angle < 0) { item->rot.y -= M_RUN_TURN; } else { item->rot.y += M_RUN_TURN; } if (Item_GetRelativeFrame(item) >= 7 && Item_GetRelativeFrame(item) <= 28) { if (creature->flags < 24) { creature->flags += 2; } int32_t f; if (creature->flags < 24) { f = creature->flags; } else { f = (Random_GetControl() & 0xF) + 8; } M_TriggerGasThrower(item, &m_GasHit, f); } if (Item_GetRelativeFrame(item) > 28) { creature->flags = 0; } break; case M_STATE_RUN: if (info.ahead) { neck = info.angle; } creature->maximum_turn = M_RUN_TURN; tilt = angle / 2; if (creature->mood != MOOD_ESCAPE) { if (creature->mood == MOOD_BORED) { item->goal_anim_state = M_STATE_WALK; } else if (info.bite && info.distance < M_ATTACK_1_RANGE) { item->goal_anim_state = M_STATE_STOP; } else if ( Creature_CanTargetEnemy(item, &info) && info.distance < M_ATTACK_0_RANGE && (lara_info->poison_timer < 256 || (Box_GetBox(creature->enemy->box_num)->overlap_index & BOX_BLOCKED_SEARCH))) { item->goal_anim_state = M_STATE_STOP; } else if (info.ahead && info.distance < M_WALK_RANGE) { item->goal_anim_state = M_STATE_WALK; } } break; } } Creature_Tilt(item, tilt); Creature_Joint(item, 0, 0); Creature_Joint(item, 1, neck); if (item->current_anim_state >= M_STATE_DEATH) { Creature_Animate(item_num, angle, 0); } else { switch (Creature_Vault(item_num, angle, 2, M_VAULT_SHIFT)) { case -4: creature->maximum_turn = 0; Item_SwitchToAnim(item, M_ANIM_FALL_3, 0); item->current_anim_state = M_STATE_FALL_3; break; case 2: creature->maximum_turn = 0; Item_SwitchToAnim(item, M_ANIM_CLIMB_1, 0); item->current_anim_state = M_STATE_CLIMB_1; break; case 3: creature->maximum_turn = 0; Item_SwitchToAnim(item, M_ANIM_CLIMB_2, 0); item->current_anim_state = M_STATE_CLIMB_2; break; case 4: creature->maximum_turn = 0; Item_SwitchToAnim(item, M_ANIM_CLIMB_3, 0); item->current_anim_state = M_STATE_CLIMB_3; break; } } } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->shadow_size = UNIT_SHADOW / 2; obj->hit_points = 36; obj->radius = 204; obj->pivot_length = 0; obj->lot_setup = LOT_Setup(LOT_SETUP_CLIMBER); obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 1)->rot.z = true; Object_GetBone(obj, 9)->rot.z = true; } REGISTER_OBJECT(O_LIZARD, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/mercenary.c ================================================ #include #include #include #include #include // clang-format off #define M_HITPOINTS 30 #define M_DAMAGE 28 #define M_RADIUS (WALL_L / 10) // = 102 #define M_WALK_TURN (DEG_1 * 5) // = 910 #define M_RUN_TURN (DEG_1 * 10) // = 1820 #define M_RUN_RANGE SQUARE(WALL_L * 2) // = 4194304 #define M_SHOOT_RANGE SQUARE(WALL_L * 3) // = 9437184 // clang-format on typedef enum { // clang-format off M_ANIM_STOP = 12, M_ANIM_WALK_TO_STOP = 17, M_ANIM_DEATH = 19, // clang-format on } M_ANIM; typedef enum { M_STATE_EMPTY, M_STATE_STOP, M_STATE_WALK, M_STATE_RUN, M_STATE_WAIT, M_STATE_SHOOT_1, M_STATE_SHOOT_2, M_STATE_DEATH, M_STATE_AIM_1, M_STATE_AIM_2, M_STATE_AIM_3, M_STATE_SHOOT_3 } M_STATE; static const CREATURE_GUN m_MercenaryGun = { .muzzle = { .pos = { 0, 300, 64 }, .mesh_num = 7 }, .tr3_enemy_flash = true, .tr3_flash = { .pos = { 0, 300, 56 }, .mesh_num = 7 }, .tr3_enemy_weapon_flags = 1, .tr3_flash_shade = 600, .tr3_flash_rot_x = -DEG_90, }; static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); Creature_Initialise(item_num); Item_SwitchToAnim(item, M_ANIM_STOP, 0); item->current_anim_state = M_STATE_STOP; item->goal_anim_state = M_STATE_STOP; } static void M_CalculateTarget(ITEM *const item) { CREATURE *const mercenary = item->creature_data; const ITEM *const lara_item = Lara_GetItem(); if (mercenary->hurt_by_lara) { mercenary->enemy = (ITEM *)lara_item; return; } int32_t best_distance = INT32_MAX; mercenary->enemy = nullptr; for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) { const CREATURE *const creature = LOT_GetBaddieSlot(i); if (creature->item_num == NO_ITEM || creature == mercenary) { continue; } const ITEM *const candidate = Item_Get(creature->item_num); if (candidate == lara_item || candidate->object_id == item->object_id) { continue; } const XYZ_32 delta = { .x = candidate->pos.x - item->pos.x, .y = 0, .z = candidate->pos.z - item->pos.z, }; const int32_t distance = XYZ_32_GetLength2(delta); if (distance < best_distance) { mercenary->enemy = (ITEM *)candidate; best_distance = distance; } } } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t tilt = 0; int16_t angle = 0; int16_t head = 0; int16_t torso_x = 0; int16_t torso_y = 0; if (item->hit_points <= 0) { if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, M_ANIM_DEATH, 0); item->current_anim_state = M_STATE_DEATH; creature->flags = (Random_GetControl() & 3) == 0 ? 1 : 0; } goto finish; } if (item->ai_bits != 0) { Creature_GetAITarget(creature); } else { M_CalculateTarget(item); } AI_INFO info = {}; Creature_AIInfo(item, &info); int32_t enemy_dist; int32_t enemy_angle; const ITEM *const lara_item = Lara_GetItem(); if (creature->enemy == lara_item) { enemy_dist = info.distance; enemy_angle = info.angle; } else { const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; enemy_angle = Math_Atan(dz, dx) - item->rot.y; enemy_dist = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz }); } Creature_Mood(item, &info, creature->enemy != lara_item); angle = Creature_Turn(item, creature->maximum_turn); if (item->hit_status) { if (!creature->alerted) { Sound_Effect(SFX_AMERICAN_HOY, &item->pos, SPM_NORMAL); } Creature_AlertAllGuards(item_num); } switch (item->current_anim_state) { case M_STATE_STOP: creature->flags = 0; creature->maximum_turn = 0; head = enemy_angle; if (Item_TestAnimEqual(item, M_ANIM_WALK_TO_STOP)) { if (ABS(info.angle) < M_RUN_TURN) { item->rot.y += info.angle; } else if (info.angle < 0) { item->rot.y -= M_RUN_TURN; } else { item->rot.y += M_RUN_TURN; } } if ((item->ai_bits & AI_GUARD) != 0) { head = Creature_AIGuard(creature); if ((Random_GetControl() & 0xFF) == 0) { if (item->current_anim_state == M_STATE_STOP) { item->goal_anim_state = M_STATE_WAIT; } else { item->goal_anim_state = M_STATE_STOP; } } } else if ((item->ai_bits & AI_PATROL_1) != 0) { item->goal_anim_state = M_STATE_WALK; head = 0; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if (Creature_CanTargetEnemy(item, &info)) { if (info.distance >= M_SHOOT_RANGE && info.zone_num == info.enemy_zone_num) { item->goal_anim_state = M_STATE_WALK; } else if (Random_GetControl() < 0x4000) { item->goal_anim_state = M_STATE_AIM_1; } else { item->goal_anim_state = M_STATE_AIM_3; } } else if ( (!creature->alerted && creature->mood == MOOD_BORED) || ((item->ai_bits & AI_FOLLOW) != 0 && (creature->reached_goal || enemy_dist > M_RUN_RANGE))) { item->goal_anim_state = M_STATE_STOP; } else if ( creature->mood != MOOD_BORED && info.distance > M_RUN_RANGE) { item->goal_anim_state = M_STATE_RUN; } else { item->goal_anim_state = M_STATE_WALK; } break; case M_STATE_WALK: head = enemy_angle; creature->flags = 0; creature->maximum_turn = M_WALK_TURN; if ((item->ai_bits & AI_PATROL_1) != 0) { item->goal_anim_state = M_STATE_WALK; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if ( (item->ai_bits & AI_GUARD) != 0 || ((item->ai_bits & AI_FOLLOW) != 0 && (creature->reached_goal || enemy_dist > M_RUN_RANGE))) { item->goal_anim_state = M_STATE_STOP; } else if (Creature_CanTargetEnemy(item, &info)) { if (info.distance < M_SHOOT_RANGE || info.zone_num != info.enemy_zone_num) { item->goal_anim_state = M_STATE_STOP; } else { item->goal_anim_state = M_STATE_AIM_2; } } else if (creature->mood == MOOD_BORED) { if (info.ahead) { item->goal_anim_state = M_STATE_STOP; } } else if (info.distance > M_RUN_RANGE) { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_RUN: if (info.ahead) { head = info.angle; } creature->maximum_turn = M_RUN_TURN; tilt = angle / 2; if ((item->ai_bits & AI_GUARD) != 0 || ((item->ai_bits & AI_FOLLOW) != 0 && (creature->reached_goal || enemy_dist > M_RUN_RANGE))) { item->goal_anim_state = M_STATE_WALK; } else if (creature->mood == MOOD_ESCAPE) { break; } else if ( Creature_CanTargetEnemy(item, &info) || creature->mood == MOOD_BORED) { item->goal_anim_state = M_STATE_WALK; } else if ( creature->mood == MOOD_STALK && (item->ai_bits & AI_FOLLOW) == 0 && info.distance < M_RUN_RANGE) { item->goal_anim_state = M_STATE_WALK; } break; case M_STATE_WAIT: head = enemy_angle; creature->flags = 0; creature->maximum_turn = 0; if ((item->ai_bits & AI_GUARD) != 0) { head = Creature_AIGuard(creature); if ((Random_GetControl() & 0xFF) == 0) { item->goal_anim_state = M_STATE_STOP; } } else if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = M_STATE_SHOOT_1; } else if (creature->mood != MOOD_BORED || !info.ahead) { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_SHOOT_1: case M_STATE_SHOOT_2: case M_STATE_SHOOT_3: if (item->current_anim_state == M_STATE_SHOOT_3 && item->goal_anim_state != M_STATE_STOP && (creature->mood == MOOD_ESCAPE || info.distance > M_SHOOT_RANGE || !Creature_CanTargetEnemy(item, &info))) { item->goal_anim_state = M_STATE_STOP; } if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } if (creature->flags != 0) { creature->flags--; } else if (creature->enemy != nullptr) { Creature_Shoot(item, &info, &m_MercenaryGun, torso_y, M_DAMAGE); creature->flags = 5; } break; case M_STATE_AIM_1: case M_STATE_AIM_2: case M_STATE_AIM_3: creature->flags = 0; if (!info.ahead) { break; } torso_x = info.x_angle; torso_y = info.angle; if (Creature_CanTargetEnemy(item, &info)) { if (item->current_anim_state == M_STATE_AIM_1) { item->goal_anim_state = M_STATE_SHOOT_1; } else if (item->current_anim_state == M_STATE_AIM_2) { item->goal_anim_state = M_STATE_SHOOT_2; } else { item->goal_anim_state = M_STATE_SHOOT_3; } } else { item->goal_anim_state = item->current_anim_state == M_STATE_AIM_2 ? M_STATE_WALK : M_STATE_STOP; } break; default: break; } finish: Creature_Tilt(item, tilt); Creature_Joint(item, 0, torso_y); Creature_Joint(item, 1, torso_x); Creature_Joint(item, 2, head); Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->shadow_size = UNIT_SHADOW / 2; obj->hit_points = M_HITPOINTS; obj->radius = M_RADIUS; obj->pivot_length = 0; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 0)->rot.x = true; Object_GetBone(obj, 0)->rot.y = true; Object_GetBone(obj, 7)->rot.y = true; } REGISTER_OBJECT(O_STHPAC_MERCENARY, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/monk.c ================================================ #include #include #include #include #include #include #include // clang-format off #define MONK_HITPOINTS 30 #define MONK_RADIUS (WALL_L / 10) // = 102 #define MONK_BIFF_DAMAGE 150 #define MONK_BIFF_ENEMY_DAMAGE 5 #define MONK_WALK_TURN (DEG_1 * 3) // = 546 #define MONK_RUN_TURN (DEG_1 * 4) // = 728 #define MONK_RUN_TURN_FAST (DEG_1 * 5) // = 910 #define MONK_CLOSE_RANGE SQUARE(WALL_L / 2) // = 262144 #define MONK_LONG_RANGE SQUARE(WALL_L) // = 1048576 #define MONK_ATTACK_5_RANGE SQUARE(WALL_L * 3) // = 9437184 #define MONK_WALK_RANGE SQUARE(WALL_L * 2) // = 4194304 #define MONK_HIT_RANGE (STEP_L * 2) // = 512 #define MONK_TOUCH_BITS 0b01000000'00000000 // = 0x4000 // clang-format on typedef enum { // clang-format off MONK_STATE_EMPTY = 0, MONK_STATE_WAIT_1 = 1, MONK_STATE_WALK = 2, MONK_STATE_RUN = 3, MONK_STATE_ATTACK_1 = 4, MONK_STATE_ATTACK_2 = 5, MONK_STATE_ATTACK_3 = 6, MONK_STATE_ATTACK_4 = 7, MONK_STATE_AIM_3 = 8, MONK_STATE_DEATH = 9, MONK_STATE_ATTACK_5 = 10, MONK_STATE_WAIT_2 = 11, // clang-format on } MONK_STATE; typedef enum { MONK_ANIM_DEATH = 20, } MONK_ANIM; static const BITE m_MonkHit = { .pos = { .x = -23, .y = 16, .z = 265 }, .mesh_num = 14, }; static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t head = 0; int16_t tilt = 0; int16_t angle = 0; if (item->hit_points <= 0) { if (item->current_anim_state != MONK_STATE_DEATH) { Item_SwitchToAnim( item, Random_GetControl() / 0x4000 + MONK_ANIM_DEATH, 0); item->current_anim_state = MONK_STATE_DEATH; } } else { AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, true); angle = Creature_Turn(item, creature->maximum_turn); if (info.ahead) { head = info.angle; } const LARA_INFO *const lara = Lara_GetLaraInfo(); switch (item->current_anim_state) { case MONK_STATE_WAIT_1: creature->flags &= 0xFFF; if (!Creature_IsHostile(item) && info.ahead && lara->target == item) { } else if (creature->mood == MOOD_BORED) { item->goal_anim_state = MONK_STATE_WALK; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = MONK_STATE_RUN; } else if (info.ahead && info.distance < MONK_CLOSE_RANGE) { if (Random_GetControl() < 0x7000) { item->goal_anim_state = MONK_STATE_ATTACK_1; } else { item->goal_anim_state = MONK_STATE_WAIT_2; } } else if (!info.ahead) { item->goal_anim_state = MONK_STATE_RUN; } else if (info.distance < MONK_LONG_RANGE) { item->goal_anim_state = MONK_STATE_ATTACK_4; } else if (info.distance < MONK_WALK_RANGE) { item->goal_anim_state = MONK_STATE_WALK; } else { item->goal_anim_state = MONK_STATE_RUN; } break; case MONK_STATE_WAIT_2: creature->flags &= 0xFFF; if (!Creature_IsHostile(item) && info.ahead && lara->target == item) { } else if (creature->mood == MOOD_BORED) { item->goal_anim_state = MONK_STATE_WALK; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = MONK_STATE_RUN; } else if (info.ahead && info.distance < MONK_CLOSE_RANGE) { const int16_t random = Random_GetControl(); if (random < 0x3000) { item->goal_anim_state = MONK_STATE_ATTACK_2; } else if (random < 0x6000) { item->goal_anim_state = MONK_STATE_AIM_3; } else { item->goal_anim_state = MONK_STATE_WAIT_1; } } else if (info.ahead && info.distance < MONK_WALK_RANGE) { item->goal_anim_state = MONK_STATE_WALK; } else { item->goal_anim_state = MONK_STATE_RUN; } break; case MONK_STATE_WALK: creature->maximum_turn = MONK_WALK_TURN; if (creature->mood == MOOD_BORED) { if (!Creature_IsHostile(item) && info.ahead && lara->target == item) { if (Random_GetControl() < 0x4000) { item->goal_anim_state = MONK_STATE_WAIT_1; } else { item->goal_anim_state = MONK_STATE_WAIT_2; } } } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = MONK_STATE_RUN; } else if (info.ahead && info.distance < MONK_CLOSE_RANGE) { if (Random_GetControl() < 0x4000) { item->goal_anim_state = MONK_STATE_WAIT_1; } else { item->goal_anim_state = MONK_STATE_WAIT_2; } } else if (!info.ahead || info.distance > MONK_WALK_RANGE) { item->goal_anim_state = MONK_STATE_RUN; } break; case MONK_STATE_RUN: creature->flags &= 0xFFF; creature->maximum_turn = MONK_RUN_TURN; if (Creature_IsHostile(item)) { creature->maximum_turn = MONK_RUN_TURN_FAST; } tilt = angle / 4; if (creature->mood == MOOD_BORED) { item->goal_anim_state = MONK_STATE_WAIT_1; } else if (creature->mood == MOOD_ESCAPE) { } else if (info.ahead && info.distance < MONK_CLOSE_RANGE) { if (Random_GetControl() < 0x4000) { item->goal_anim_state = MONK_STATE_WAIT_1; } else { item->goal_anim_state = MONK_STATE_WAIT_2; } } else if (info.ahead && info.distance < MONK_ATTACK_5_RANGE) { item->goal_anim_state = MONK_STATE_ATTACK_5; } break; case MONK_STATE_AIM_3: if (!info.ahead || info.distance > MONK_CLOSE_RANGE) { item->goal_anim_state = MONK_STATE_WAIT_2; } else { item->goal_anim_state = MONK_STATE_ATTACK_3; } break; case MONK_STATE_ATTACK_1: case MONK_STATE_ATTACK_2: case MONK_STATE_ATTACK_3: case MONK_STATE_ATTACK_4: case MONK_STATE_ATTACK_5: if (creature->enemy == Lara_GetItem()) { if ((creature->flags & 0xF000) == 0 && (item->touch_bits & MONK_TOUCH_BITS) != 0) { Lara_TakeDamage(MONK_BIFF_DAMAGE, true); Sound_Effect(SFX_MONK_CRUNCH, &item->pos, SPM_NORMAL); Creature_Effect(item, &m_MonkHit, Spawn_Blood); creature->flags |= 0x1000; } } else if ( (creature->flags & 0xF000) == 0 && creature->enemy != nullptr) { const int32_t dx = ABS(creature->enemy->pos.x - item->pos.x); const int32_t dy = ABS(creature->enemy->pos.y - item->pos.y); const int32_t dz = ABS(creature->enemy->pos.z - item->pos.z); if (dx < MONK_HIT_RANGE && dy < MONK_HIT_RANGE && dz < MONK_HIT_RANGE) { Item_TakeDamage( creature->enemy, MONK_BIFF_ENEMY_DAMAGE, true); Sound_Effect(SFX_MONK_CRUNCH, &item->pos, SPM_NORMAL); creature->flags |= 0x1000; } } break; default: break; } } Creature_Tilt(item, tilt); Creature_Head(item, head); Creature_Animate(item_num, angle, 0); } static void M_SetupBase(OBJECT *const obj) { obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->hit_points = MONK_HITPOINTS; obj->radius = MONK_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 6)->rot.y = true; } static void M_Setup1(OBJECT *const obj) { if (!obj->loaded) { return; } M_SetupBase(obj); obj->pivot_length = 0; } static void M_Setup2(OBJECT *const obj) { if (!obj->loaded) { return; } M_SetupBase(obj); } static void M_Setup3(OBJECT *const obj) { if (!obj->loaded) { return; } M_SetupBase(obj); obj->pivot_length = 0; obj->shadow_size = 0; } REGISTER_OBJECT(O_MONK_1, M_Setup1) REGISTER_OBJECT(O_MONK_2, M_Setup2) REGISTER_OBJECT(O_MONK_3, M_Setup3) ================================================ FILE: src/trx/game/objects/creatures/monkey.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include // clang-format off #define M_DAMAGE_NORMAL 40 #define M_DAMAGE_JUMP 50 #define M_WALK_TURN (7 * DEG_1) #define M_RUN_TURN (11 * DEG_1) #define M_JUMP_RANGE SQUARE(WALL_L * 2/3) // = 465124 #define M_WALK_RANGE SQUARE(WALL_L * 2/3) // = 465124 #define M_ATTACK_RANGE SQUARE(WALL_L / 3) // = 116281 #define M_ROLL_RANGE SQUARE(WALL_L) // = 1048576 #define M_WAIT_CHANCE 256 #define M_F_PICKUP 12 // clang-format on static BITE m_MonkeyBite = { .pos = { 10, 10, 11 }, .mesh_num = 13, }; typedef enum { M_STATE_EMPTY, M_STATE_STOP, M_STATE_WALK, M_STATE_STAND, M_STATE_RUN, M_STATE_PICKUP, M_STATE_SIT, M_STATE_EAT, M_STATE_SCRATCH, M_STATE_ROLL, M_STATE_ANGRY, M_STATE_DEATH, M_STATE_ATTACK_LOW, M_STATE_ATTACK_HIGH, M_STATE_ATTACK_JUMP, M_STATE_CLIMB_4, M_STATE_CLIMB_3, M_STATE_CLIMB_2, M_STATE_DOWN_4, M_STATE_DOWN_3, M_STATE_DOWN_2 } M_STATE; typedef enum { M_ANIM_SIT = 2, M_ANIM_DEATH = 14, M_ANIM_CLIMB_2 = 19, M_ANIM_CLIMB_3 = 18, M_ANIM_CLIMB_4 = 17, M_ANIM_DOWN_2 = 22, M_ANIM_DOWN_3 = 21, M_ANIM_DOWN_4 = 20, } M_ANIM; static void M_Bite(ITEM *const item, ITEM *const enemy, const int32_t dmg) { CREATURE *const creature = item->creature_data; if (enemy == Lara_GetItem()) { if (creature->flags == 0 && item->touch_bits & 0x2400) { Lara_TakeDamage(dmg, true); creature->flags = 1; Creature_Effect(item, &m_MonkeyBite, Spawn_Blood); } } else if (creature->flags == 0 && enemy != nullptr) { if (ABS(enemy->pos.x - item->pos.x) < STEP_L && ABS(enemy->pos.y - item->pos.y) <= STEP_L && ABS(enemy->pos.z - item->pos.z) < STEP_L) { Item_TakeDamage(enemy, dmg / 2, true); creature->flags = 1; Creature_Effect(item, &m_MonkeyBite, Spawn_Blood); } } } static bool M_CarryPickup( ITEM *const item, CREATURE *const creature, const int16_t item_num) { if (creature->enemy == nullptr) { return false; } if (creature->enemy->object_id != O_SMALL_MEDIPACK_ITEM && creature->enemy->object_id != O_KEY_ITEM_4) { return false; } if (!Item_TestFrameEqual(item, M_F_PICKUP)) { return false; } if (creature->enemy->room_num == NO_ROOM || creature->enemy->status == IS_INVISIBLE || creature->enemy->clear_body) { creature->enemy = nullptr; return true; } const int16_t pickup_num = Item_GetIndex(creature->enemy); if (item->carried_item == nullptr) { item->carried_item = GameBuf_Alloc(sizeof(CARRIED_ITEM), GBUF_ITEMS); item->carried_item->next_item = nullptr; } item->carried_item->object_id = creature->enemy->object_id; item->carried_item->spawn_num = pickup_num; item->carried_item->pos = creature->enemy->pos; item->carried_item->rot = creature->enemy->rot; item->carried_item->room_num = NO_ROOM; item->carried_item->fall_speed = 0; item->carried_item->status = DS_CARRIED; Item_UpdateRoom(pickup_num, NO_ROOM); creature->enemy->carried_item = nullptr; for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) { CREATURE *const slot = LOT_GetBaddieSlot(i); if (slot->item_num != NO_ITEM && slot->item_num != item_num && slot->enemy == creature->enemy) { slot->enemy = nullptr; } } creature->enemy = nullptr; if (item->ai_bits != AI_MODIFY) { item->ai_bits |= AI_AMBUSH | AI_MODIFY; } return true; } static bool M_DropPickup(ITEM *const item, CREATURE *const creature) { if (creature->enemy == nullptr) { return false; } if (creature->enemy->object_id != O_AI_AMBUSH) { return false; } if (!Item_TestFrameEqual(item, M_F_PICKUP)) { return false; } item->ai_bits = 0; ITEM *const pickup = Item_Get(item->carried_item->spawn_num); pickup->pos = item->pos; Item_UpdateRoom(item->carried_item->spawn_num, item->room_num); pickup->ai_bits = AI_GUARD; item->carried_item = nullptr; creature->enemy = nullptr; return true; } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); Creature_Initialise(item_num); Item_SwitchToAnim(item, M_ANIM_SIT, 0); item->current_anim_state = M_STATE_SIT; item->goal_anim_state = M_STATE_SIT; } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara = Lara_GetLaraInfo(); int16_t angle = 0; int16_t tilt = 0; int16_t torso_y = 0; if (item->hit_points <= 0) { if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, M_ANIM_DEATH, 0); item->current_anim_state = M_STATE_DEATH; item->mesh_bits = -1; } } else { Creature_GetAITarget(creature); if (creature->hurt_by_lara && g_Config.gameplay.fix_monkey_pickup_priority) { creature->enemy = lara_item; } if (item->ai_bits == AI_MODIFY) { if (item->carried_item == nullptr) { item->mesh_bits = 0xFFFF6F6F; } else { item->mesh_bits = 0xFFFF6E6F; } } else if (item->carried_item == nullptr) { item->mesh_bits = -1; } else { item->mesh_bits = 0xFFFFFEFF; } AI_INFO info; Creature_AIInfo(item, &info); int32_t dist; if (creature->enemy == lara_item) { dist = info.distance; } else { const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; Math_Atan(dz, dx); dist = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz }); } Creature_UpdateMood(item, &info, true); if (Lara_Vehicle_GetIndex() != NO_ITEM) { creature->mood = MOOD_ESCAPE; } Creature_ApplyMood(item, &info, true); angle = Creature_Turn(item, creature->maximum_turn); if (item->hit_status) { ITEM *const enemy = creature->enemy; creature->enemy = lara_item; Creature_AlertAllGuards(item_num); creature->enemy = enemy; } switch (item->current_anim_state) { case M_STATE_WALK: creature->maximum_turn = M_WALK_TURN; if (item->ai_bits & AI_PATROL_1) { item->goal_anim_state = M_STATE_WALK; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if (creature->mood == MOOD_BORED) { if (Random_GetControl() < M_WAIT_CHANCE) { item->goal_anim_state = M_STATE_SIT; } } else if (info.bite && info.distance < M_JUMP_RANGE) { item->goal_anim_state = M_STATE_STAND; } else { item->goal_anim_state = M_STATE_STAND; } break; case M_STATE_STAND: creature->flags = 0; creature->maximum_turn = 0; if (item->ai_bits & AI_GUARD) { Creature_AIGuard(creature); if (!(Random_GetControl() & 0xF)) { if (Random_GetControl() & 1) { item->goal_anim_state = M_STATE_ANGRY; } else { item->goal_anim_state = M_STATE_SIT; } } } else if (item->ai_bits & AI_PATROL_1) { item->goal_anim_state = M_STATE_WALK; } else if (creature->mood == MOOD_ESCAPE) { if (lara->target != item && info.ahead) { item->goal_anim_state = M_STATE_STAND; } else { item->goal_anim_state = M_STATE_RUN; } } else if (creature->mood == MOOD_BORED) { if (item->required_anim_state != M_STATE_EMPTY) { item->goal_anim_state = item->required_anim_state; } else if (!(Random_GetControl() & 0xF)) { item->goal_anim_state = M_STATE_WALK; } else if (!(Random_GetControl() & 0xF)) { if (Random_GetControl() & 1) { item->goal_anim_state = M_STATE_ANGRY; } else { item->goal_anim_state = M_STATE_SIT; } } } else if ( item->ai_bits & AI_FOLLOW && (creature->reached_goal || dist > SQUARE(WALL_L * 2))) { if (item->required_anim_state != M_STATE_EMPTY) { item->goal_anim_state = item->required_anim_state; } else if (info.ahead) { item->goal_anim_state = M_STATE_SIT; } else { item->goal_anim_state = M_STATE_RUN; } } else if (info.bite && info.distance < M_ATTACK_RANGE) { if (lara_item->pos.y < item->pos.y) { item->goal_anim_state = M_STATE_ATTACK_HIGH; } else { item->goal_anim_state = M_STATE_ATTACK_LOW; } } else if (info.bite && info.distance < M_JUMP_RANGE) { item->goal_anim_state = M_STATE_ATTACK_JUMP; } else if (info.bite && info.distance < M_WALK_RANGE) { item->goal_anim_state = M_STATE_WALK; } else if ( info.distance < M_WALK_RANGE && creature->enemy != lara_item && creature->enemy != nullptr && creature->enemy->object_id != O_AI_PATROL_1 && creature->enemy->object_id != O_AI_PATROL_2 && ABS(item->pos.y - creature->enemy->pos.y) < STEP_L) { item->goal_anim_state = M_STATE_PICKUP; } else if (info.bite && info.distance < M_ROLL_RANGE) { item->goal_anim_state = M_STATE_ROLL; } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_RUN: creature->maximum_turn = M_RUN_TURN; tilt = angle / 2; if (item->ai_bits & AI_GUARD) { item->goal_anim_state = M_STATE_STAND; } else if (creature->mood == MOOD_ESCAPE) { if (lara->target != item && info.ahead) { item->goal_anim_state = M_STATE_STAND; } } else if ( item->ai_bits & AI_FOLLOW && (creature->reached_goal || dist > SQUARE(WALL_L * 2))) { item->goal_anim_state = M_STATE_STAND; } else if (creature->mood == MOOD_BORED) { item->goal_anim_state = M_STATE_ROLL; } else if (info.distance < M_WALK_RANGE) { item->goal_anim_state = M_STATE_STAND; } else if (info.bite && info.distance < M_ROLL_RANGE) { item->goal_anim_state = M_STATE_ROLL; } break; case M_STATE_PICKUP: creature->reached_goal = true; if (creature->enemy == nullptr) { break; } if (M_CarryPickup(item, creature, item_num)) { break; } else if (M_DropPickup(item, creature)) { break; } else { creature->maximum_turn = 0; if (ABS(info.angle) < M_WALK_TURN) { item->rot.y += info.angle; } else if (info.angle < 0) { item->rot.y -= M_WALK_TURN; } else { item->rot.y += M_WALK_TURN; } } break; case M_STATE_SIT: creature->flags = 0; creature->maximum_turn = 0; if (item->ai_bits & AI_GUARD) { Creature_AIGuard(creature); if (!(Random_GetControl() & 0xF)) { if (Random_GetControl() & 1) { item->goal_anim_state = M_STATE_SCRATCH; } else { item->goal_anim_state = M_STATE_EAT; } } } else if (item->ai_bits & AI_PATROL_1) { item->goal_anim_state = M_STATE_WALK; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_STAND; } else if (creature->mood == MOOD_BORED) { if (item->required_anim_state != M_STATE_EMPTY) { item->goal_anim_state = item->required_anim_state; } else if (!(Random_GetControl() & 0xF)) { item->goal_anim_state = M_STATE_WALK; } else if (!(Random_GetControl() & 0xF)) { if (Random_GetControl() & 1) { item->goal_anim_state = M_STATE_SCRATCH; } else { item->goal_anim_state = M_STATE_EAT; } } } else if ( item->ai_bits & AI_FOLLOW && (creature->reached_goal || dist > SQUARE(WALL_L * 2))) { if (item->required_anim_state != M_STATE_EMPTY) { item->goal_anim_state = item->required_anim_state; } else if (info.ahead) { item->goal_anim_state = M_STATE_SIT; } else { item->goal_anim_state = M_STATE_STAND; } } else if (info.bite && info.distance < M_JUMP_RANGE) { item->goal_anim_state = M_STATE_STAND; } else if (info.bite && info.distance < M_WALK_RANGE) { item->goal_anim_state = M_STATE_WALK; } else { item->goal_anim_state = M_STATE_STAND; } break; case M_STATE_ATTACK_LOW: if (info.ahead) { torso_y = info.angle; } creature->maximum_turn = 0; if (ABS(info.angle) < M_WALK_TURN) { item->rot.y += info.angle; } else if (info.angle < 0) { item->rot.y -= M_WALK_TURN; } else { item->rot.y += M_WALK_TURN; } M_Bite(item, creature->enemy, M_DAMAGE_NORMAL); break; #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wimplicit-fallthrough" case M_STATE_ATTACK_HIGH: if (info.ahead) { torso_y = info.angle; } creature->maximum_turn = 0; if (ABS(info.angle) < M_WALK_TURN) { item->rot.y += info.angle; } else if (info.angle < 0) { item->rot.y -= M_WALK_TURN; } else { item->rot.y += M_WALK_TURN; } M_Bite(item, creature->enemy, M_DAMAGE_NORMAL); // OG mistake // break; case M_STATE_ATTACK_JUMP: if (info.ahead) { torso_y = info.angle; } creature->maximum_turn = 0; if (ABS(info.angle) < M_WALK_TURN) { item->rot.y += info.angle; } else if (info.angle < 0) { item->rot.y -= M_WALK_TURN; } else { item->rot.y += M_WALK_TURN; } M_Bite(item, creature->enemy, M_DAMAGE_JUMP); break; #pragma GCC diagnostic pop } } Creature_Tilt(item, tilt); Creature_Joint(item, 0, torso_y); if (item->current_anim_state >= M_STATE_CLIMB_4) { creature->maximum_turn = 0; Creature_Animate(item_num, angle, 0); } else { switch (Creature_Vault(item_num, angle, 2, 128)) { case -4: creature->maximum_turn = 0; Item_SwitchToObjAnim(item, M_ANIM_DOWN_4, 0, O_MONKEY); item->current_anim_state = M_STATE_DOWN_4; break; case -3: creature->maximum_turn = 0; Item_SwitchToObjAnim(item, M_ANIM_DOWN_3, 0, O_MONKEY); item->current_anim_state = M_STATE_DOWN_3; break; case -2: creature->maximum_turn = 0; Item_SwitchToObjAnim(item, M_ANIM_DOWN_2, 0, O_MONKEY); item->current_anim_state = M_STATE_DOWN_2; break; case 2: creature->maximum_turn = 0; Item_SwitchToObjAnim(item, M_ANIM_CLIMB_2, 0, O_MONKEY); item->current_anim_state = M_STATE_CLIMB_2; break; case 3: creature->maximum_turn = 0; Item_SwitchToObjAnim(item, M_ANIM_CLIMB_3, 0, O_MONKEY); item->current_anim_state = M_STATE_CLIMB_3; break; case 4: creature->maximum_turn = 0; Item_SwitchToObjAnim(item, M_ANIM_CLIMB_4, 0, O_MONKEY); item->current_anim_state = M_STATE_CLIMB_4; break; } } } static bool M_Draw(const ITEM *const item) { const OBJECT *swap; if (item->ai_bits == AI_MODIFY) { swap = Object_Get(O_MESH_SWAP_3); } else { swap = Object_Get(O_MESH_SWAP_2); } return Object_DrawAnimatingItemWithSwap(item, swap); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } if (!Object_Get(O_MESH_SWAP_2)->loaded) { Shell_ExitSystem("Monkey requires O_MESH_SWAP_2 (pickups)"); } obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->draw_func = M_Draw; obj->shadow_size = UNIT_SHADOW / 2; obj->hit_points = 8; obj->radius = 102; obj->pivot_length = 0; obj->lot_setup = LOT_Setup(LOT_SETUP_CLIMBER); obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 0)->rot.z = true; Object_GetBone(obj, 7)->rot.x = true; Object_GetBone(obj, 7)->rot.y = true; } REGISTER_OBJECT(O_MONKEY, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/mouse.c ================================================ #include #include #include #include #include #include // clang-format off #define M_HITPOINTS 4 #define M_TOUCH_BITS 0b01111111 // = 0x7F #define M_RADIUS (WALL_L / 10) // = 102 #define M_RUN_TURN (DEG_1 * 6) // = 1092 #define M_ATTACK_RANGE SQUARE(WALL_L / 3) // = 116281 #define M_BITE_DAMAGE 20 #define M_WAIT_1_CHANCE 0x500 // = 1280 #define M_WAIT_2_CHANCE (M_WAIT_1_CHANCE + 0x500) // = 2560 // clang-format on typedef enum { M_STATE_NULL, M_STATE_RUN, M_STATE_STOP, M_STATE_WAIT_1, M_STATE_WAIT_2, M_STATE_ATTACK, M_STATE_DEATH, } M_STATE; typedef enum { M_ANIM_DEATH = 9, } M_ANIM; static const BITE m_MouseBite = { .pos = { .x = 0, .y = 0, .z = 57 }, .mesh_num = 2, }; static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t head = 0; int16_t angle = 0; if (item->hit_points <= 0) { if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, M_ANIM_DEATH, 0); item->current_anim_state = M_STATE_DEATH; } } else { AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, false); if (info.ahead) { head = info.angle; } angle = Creature_Turn(item, M_RUN_TURN); switch (item->current_anim_state) { case M_STATE_RUN: creature->maximum_turn = M_RUN_TURN; if (creature->mood == MOOD_BORED || creature->mood == MOOD_STALK) { const int32_t random = Random_GetControl(); if (random < M_WAIT_1_CHANCE) { item->required_anim_state = M_STATE_WAIT_1; item->goal_anim_state = M_STATE_STOP; } else if (random < M_WAIT_2_CHANCE) { item->goal_anim_state = M_STATE_STOP; } } else if (info.ahead && info.distance < M_ATTACK_RANGE) { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_STOP: creature->maximum_turn = 0; if (item->required_anim_state != M_STATE_NULL) { item->goal_anim_state = item->required_anim_state; } break; case M_STATE_WAIT_1: if (Random_GetControl() < M_WAIT_1_CHANCE) { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_WAIT_2: if (creature->mood == MOOD_BORED || creature->mood == MOOD_STALK) { const int32_t random = Random_GetControl(); if (random < M_WAIT_1_CHANCE) { item->required_anim_state = M_STATE_WAIT_1; } else if (random > M_WAIT_2_CHANCE) { item->required_anim_state = M_STATE_RUN; } } else if (info.distance < M_ATTACK_RANGE) { item->required_anim_state = M_STATE_ATTACK; } else { item->required_anim_state = M_STATE_RUN; } if (item->required_anim_state != M_STATE_NULL) { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_ATTACK: if (item->required_anim_state == M_STATE_NULL && (item->touch_bits & M_TOUCH_BITS) != 0) { Lara_TakeDamage(M_BITE_DAMAGE, true); Creature_Effect(item, &m_MouseBite, Spawn_Blood); item->required_anim_state = M_STATE_STOP; } break; default: break; } } Creature_Head(item, head); Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->hit_points = M_HITPOINTS; obj->radius = M_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->pivot_length = 50; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 3)->rot.y = true; } REGISTER_OBJECT(O_MOUSE, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/mp_1.c ================================================ #include #include #include #include #include #include #include // clang-format off #define M_RADIUS (WALL_L / 5) // = 204 #define M_HIT_POINTS 25 #define M_PUNCH_1_DAMAGE 80 #define M_PUNCH_3_DAMAGE 100 #define M_KICK_DAMAGE 150 #define M_HIT_TOUCH_BITS 0b00100100'00000000 #define M_KICK_TOUCH_BITS 0b00000000'01100000 #define M_ALERT_DIST SQUARE(WALL_L) // = 1048576 #define M_ALERT_HEIGHT WALL_L // = 1024 #define M_RUN_DIST SQUARE(WALL_L * 2) // = 4194304 #define M_WALK_DIST SQUARE(WALL_L) // = 1048576 #define M_ATTACK_DIST_1 SQUARE(WALL_L / 2) // = 262144 #define M_ATTACK_DIST_2 SQUARE(WALL_L) // = 1048576 #define M_ATTACK_DIST_3 SQUARE(WALL_L * 5 / 4) // = 1638400 #define M_KICK_DIST SQUARE(WALL_L * 3 / 2) // = 2359296 #define M_WALK_TURN (DEG_1 * 6) // = 1092 #define M_RUN_TURN (DEG_1 * 7) // = 1274 // clang-format on typedef enum { M_STATE_NULL, M_STATE_STOP, M_STATE_WALK, M_STATE_PUNCH_3, M_STATE_AIM_3, M_STATE_WAIT, M_STATE_AIM_2, M_STATE_AIM_1, M_STATE_PUNCH_2, M_STATE_PUNCH_1, M_STATE_RUN, M_STATE_DEATH, M_STATE_KICK, M_STATE_UP_4, M_STATE_UP_2, M_STATE_UP_3, M_STATE_DOWN_4, } M_STATE; typedef enum { // clang-format off M_ANIM_STOP = 6, M_ANIM_DEATH = 26, M_ANIM_UP_4 = 27, M_ANIM_UP_2 = 28, M_ANIM_UP_3 = 29, M_ANIM_DOWN_4 = 30, // clang-format on } M_ANIM; static const BITE m_HitBite = { .pos = { 247, 10, 11 }, .mesh_num = 13, }; static const BITE m_KickBite = { .pos = { 0, 0, 100 }, .mesh_num = 6, }; static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); Item_SwitchToAnim(item, M_ANIM_STOP, 0); item->current_anim_state = M_STATE_STOP; item->goal_anim_state = M_STATE_STOP; } static void M_CalculateEnemy(ITEM *const item) { CREATURE *const mp = item->creature_data; const ITEM *const lara_item = Lara_GetItem(); mp->enemy = (ITEM *)lara_item; const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; int32_t best_distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz }); for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) { const CREATURE *const creature = LOT_GetBaddieSlot(i); if (creature->item_num == NO_ITEM || creature == mp) { continue; } const ITEM *const candidate = Item_Get(creature->item_num); if (candidate != lara_item && candidate->object_id != O_PRISONER) { continue; } const XYZ_32 delta = { .x = candidate->pos.x - item->pos.x, .y = 0, .z = candidate->pos.z - item->pos.z, }; if (ABS(delta.x) > 0x7D00 || ABS(delta.z) > 0x7D00) { continue; } const int32_t distance = XYZ_32_GetLength2(delta); if (distance < best_distance) { mp->enemy = (ITEM *)candidate; best_distance = distance; } } } static bool M_Vault(ITEM *const item, const int16_t angle) { const int32_t vault_result = Creature_Vault(Item_GetIndex(item), angle, 2, 260); switch (vault_result) { case -4: Item_SwitchToAnim(item, M_ANIM_DOWN_4, 0); item->current_anim_state = M_STATE_DOWN_4; return true; case 2: Item_SwitchToAnim(item, M_ANIM_UP_2, 0); item->current_anim_state = M_STATE_UP_2; return true; case 3: Item_SwitchToAnim(item, M_ANIM_UP_3, 0); item->current_anim_state = M_STATE_UP_3; return true; case 4: Item_SwitchToAnim(item, M_ANIM_UP_4, 0); item->current_anim_state = M_STATE_UP_4; return true; default: return false; } } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t angle = 0; int16_t head = 0; int16_t tilt = 0; int16_t torso_x = 0; int16_t torso_y = 0; Creature_TestBoxDamage(item_num); if (item->hit_points <= 0) { if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, M_ANIM_DEATH, 0); item->current_anim_state = M_STATE_DEATH; creature->lot.setup.step = STEP_L; } goto finish; } if (item->ai_bits != 0) { Creature_GetAITarget(creature); } else { M_CalculateEnemy(item); } AI_INFO info = {}; Creature_AIInfo(item, &info); ITEM *const lara_item = Lara_GetItem(); AI_INFO lara_info = {}; if (creature->enemy == lara_item) { lara_info.distance = info.distance; lara_info.angle = info.angle; } else { const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; lara_info.angle = Math_Atan(dz, dx) - item->rot.y; lara_info.distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz }); } Creature_Mood(item, &info, true); angle = Creature_Turn(item, creature->maximum_turn); ITEM *const enemy = creature->enemy; creature->enemy = lara_item; if (item->hit_status || ((lara_info.distance < M_ALERT_DIST || Creature_CanSeeEnemy(item, &lara_info)) && ABS(lara_item->pos.y - item->pos.y) < M_ALERT_HEIGHT)) { if (!creature->alerted) { Sound_Effect(SFX_AMERICAN_HOY, &item->pos, SPM_NORMAL); } Creature_AlertAllGuards(item_num); } creature->enemy = enemy; const LARA_INFO *const lara = Lara_GetLaraInfo(); switch (item->current_anim_state) { case M_STATE_STOP: case M_STATE_WAIT: if (item->current_anim_state == M_STATE_WAIT && (creature->alerted || item->goal_anim_state == M_STATE_RUN)) { item->goal_anim_state = M_STATE_STOP; break; } creature->flags = 0; creature->maximum_turn = 0; head = lara_info.angle; if ((item->ai_bits & AI_GUARD) != 0) { head = Creature_AIGuard(creature); if ((Random_GetControl() & 0xFF) == 0) { if (item->current_anim_state == M_STATE_STOP) { item->goal_anim_state = M_STATE_WAIT; } else { item->goal_anim_state = M_STATE_STOP; } } } else if ((item->ai_bits & AI_PATROL_1) != 0) { item->goal_anim_state = M_STATE_WALK; } else if (creature->mood == MOOD_ESCAPE) { if (lara->target != item && info.ahead && !item->hit_status) { item->goal_anim_state = M_STATE_STOP; } else { item->goal_anim_state = M_STATE_RUN; } } else if ( creature->mood == MOOD_BORED || ((item->ai_bits & AI_FOLLOW) != 0 && (creature->reached_goal || lara_info.distance > M_RUN_DIST))) { if (item->required_anim_state != M_STATE_NULL) { item->goal_anim_state = item->required_anim_state; } else if (info.ahead) { item->goal_anim_state = M_STATE_STOP; } else { item->goal_anim_state = M_STATE_RUN; } } else if (info.bite && info.distance < M_ATTACK_DIST_1) { item->goal_anim_state = M_STATE_AIM_1; } else if (info.bite && info.distance < M_ATTACK_DIST_2) { item->goal_anim_state = M_STATE_AIM_2; } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_WALK: creature->maximum_turn = M_WALK_TURN; head = lara_info.angle; creature->flags = 0; if ((item->ai_bits & AI_PATROL_1) != 0) { item->goal_anim_state = M_STATE_WALK; head = 0; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if (creature->mood == MOOD_BORED) { if (Random_GetControl() < 256) { item->required_anim_state = M_STATE_WAIT; item->goal_anim_state = M_STATE_STOP; } } else if ( info.bite && info.distance < M_KICK_DIST && info.x_angle < 0) { item->goal_anim_state = M_STATE_KICK; } else if (info.bite) { if (info.distance < M_ATTACK_DIST_1) { item->goal_anim_state = M_STATE_STOP; } else if (info.distance < M_ATTACK_DIST_3) { item->goal_anim_state = M_STATE_AIM_3; } else { item->goal_anim_state = M_STATE_RUN; } } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_RUN: if (info.ahead) { head = info.angle; } creature->maximum_turn = M_RUN_TURN; tilt = angle / 2; if ((item->ai_bits & AI_GUARD) != 0) { item->goal_anim_state = M_STATE_WAIT; } else if (creature->mood == MOOD_ESCAPE) { if (lara->target != item && info.ahead) { item->goal_anim_state = M_STATE_STOP; } } else if ( (item->ai_bits & AI_FOLLOW) != 0 && (creature->reached_goal || lara_info.distance > M_RUN_DIST)) { item->goal_anim_state = M_STATE_STOP; } else if (creature->mood == MOOD_BORED) { item->goal_anim_state = M_STATE_WALK; } else if (info.ahead && info.distance < M_WALK_DIST) { item->goal_anim_state = M_STATE_WALK; } break; case M_STATE_AIM_1: if (info.ahead) { torso_y = info.angle; torso_x = info.x_angle; } creature->maximum_turn = M_WALK_TURN; creature->flags = 0; if (info.bite && info.distance < M_ATTACK_DIST_1) { item->goal_anim_state = M_STATE_PUNCH_1; } else { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_AIM_2: if (info.ahead) { torso_y = info.angle; torso_x = info.x_angle; } creature->maximum_turn = M_WALK_TURN; creature->flags = 0; if (info.ahead && info.distance < M_ATTACK_DIST_2) { item->goal_anim_state = M_STATE_PUNCH_2; } else { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_AIM_3: if (info.ahead) { torso_y = info.angle; torso_x = info.x_angle; } creature->maximum_turn = M_WALK_TURN; creature->flags = 0; if (info.bite && info.distance < M_ATTACK_DIST_3) { item->goal_anim_state = M_STATE_PUNCH_3; } else { item->goal_anim_state = M_STATE_WALK; } break; case M_STATE_PUNCH_1: if (info.ahead) { torso_y = info.angle; torso_x = info.x_angle; } creature->maximum_turn = M_WALK_TURN; if (enemy == lara_item) { if (creature->flags == 0 && (item->touch_bits & M_HIT_TOUCH_BITS) != 0) { Lara_TakeDamage(M_PUNCH_1_DAMAGE, true); Creature_Effect(item, &m_HitBite, Spawn_Blood); Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL); creature->flags = 1; } } else if (creature->flags == 0 && enemy != nullptr) { if (ABS(enemy->pos.x - item->pos.x) < STEP_L && ABS(enemy->pos.y - item->pos.y) <= STEP_L && ABS(enemy->pos.z - item->pos.z) < STEP_L) { Item_TakeDamage(enemy, M_PUNCH_1_DAMAGE / 16, true); Creature_Effect(item, &m_HitBite, Spawn_Blood); Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL); creature->flags = 1; } } break; case M_STATE_PUNCH_2: if (info.ahead) { torso_y = info.angle; torso_x = info.x_angle; } creature->maximum_turn = M_WALK_TURN; if (enemy == lara_item) { if (creature->flags == 0 && (item->touch_bits & M_HIT_TOUCH_BITS) != 0) { Lara_TakeDamage(M_PUNCH_1_DAMAGE, true); Creature_Effect(item, &m_HitBite, Spawn_Blood); Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL); creature->flags = 1; } } else if (creature->flags == 0 && enemy != nullptr) { if (ABS(enemy->pos.x - item->pos.x) < STEP_L && ABS(enemy->pos.y - item->pos.y) <= STEP_L && ABS(enemy->pos.z - item->pos.z) < STEP_L) { Item_TakeDamage(enemy, M_PUNCH_1_DAMAGE / 16, true); Creature_Effect(item, &m_HitBite, Spawn_Blood); Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL); creature->flags = 1; } } if (info.ahead && info.distance > M_ATTACK_DIST_2 && info.distance < M_ATTACK_DIST_3) { item->goal_anim_state = M_STATE_PUNCH_3; } break; case M_STATE_PUNCH_3: if (info.ahead) { torso_y = info.angle; torso_x = info.x_angle; } creature->maximum_turn = M_WALK_TURN; if (enemy == lara_item) { if (creature->flags != 2 && (item->touch_bits & M_HIT_TOUCH_BITS) != 0) { Lara_TakeDamage(M_PUNCH_3_DAMAGE, true); Creature_Effect(item, &m_HitBite, Spawn_Blood); Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL); creature->flags = 2; } } else if (creature->flags != 2 && enemy != nullptr) { if (ABS(enemy->pos.x - item->pos.x) < STEP_L && ABS(enemy->pos.y - item->pos.y) <= STEP_L && ABS(enemy->pos.z - item->pos.z) < STEP_L) { Item_TakeDamage(enemy, M_PUNCH_3_DAMAGE / 16, true); Creature_Effect(item, &m_HitBite, Spawn_Blood); Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL); creature->flags = 2; } } break; case M_STATE_KICK: if (info.ahead) { torso_y = info.angle; } creature->maximum_turn = M_WALK_TURN; const int16_t frame_num = Item_GetRelativeFrame(item); if (enemy == lara_item) { if (creature->flags != 1 && (item->touch_bits & M_KICK_TOUCH_BITS) != 0 && frame_num > 8) { Lara_TakeDamage(M_KICK_DAMAGE, true); Creature_Effect(item, &m_KickBite, Spawn_Blood); Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL); creature->flags = 1; } } else if (creature->flags != 0 && enemy != nullptr && frame_num > 8) { if (ABS(enemy->pos.x - item->pos.x) < STEP_L && ABS(enemy->pos.y - item->pos.y) <= STEP_L && ABS(enemy->pos.z - item->pos.z) < STEP_L) { Item_TakeDamage(enemy, M_KICK_DAMAGE / 16, true); Creature_Effect(item, &m_KickBite, Spawn_Blood); Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL); creature->flags = 1; } } break; } finish: Creature_Tilt(item, tilt); Creature_Joint(item, 0, torso_y); Creature_Joint(item, 1, torso_x); Creature_Joint(item, 2, head); if (item->current_anim_state >= M_STATE_DEATH) { creature->maximum_turn = 0; Creature_Animate(item_num, angle, 0); } else if (M_Vault(item, angle)) { creature->maximum_turn = 0; } } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->collision_func = Creature_Collision; obj->control_func = M_Control; obj->shadow_size = UNIT_SHADOW / 2; obj->radius = M_RADIUS; obj->hit_points = M_HIT_POINTS; obj->lot_setup = LOT_Setup(LOT_SETUP_CLIMBER); obj->intelligent = true; obj->save_anim = true; obj->save_flags = true; obj->save_hitpoints = true; obj->save_position = true; Object_GetBone(obj, 6)->rot.x = true; Object_GetBone(obj, 6)->rot.y = true; Object_GetBone(obj, 13)->rot.y = true; } REGISTER_OBJECT(O_MP_1, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/mp_2.c ================================================ #include #include #include #include #include #include #include #include // clang-format off #define M_RADIUS (WALL_L / 10) // = 102 #define M_HIT_POINTS 28 #define M_DAMAGE 32 #define M_ALERT_DIST SQUARE(WALL_L) // = 1048576 #define M_WALK_DIST SQUARE(WALL_L * 3 / 2) // = 2359296 #define M_RUN_DIST SQUARE(WALL_L * 2) // = 4194304 #define M_WALK_TURN (DEG_1 * 6) // = 1092 #define M_RUN_TURN (DEG_1 * 10) // = 1820 #define M_DUCK_TURN (DEG_1 * 1) // = 182 #define M_SHOOT_1_CHANCE 0x2000 #define M_SHOOT_2_CHANCE 0x4000 #define M_DUCK_CHANCE 0x3 #define M_DUCK_END_CHANCE 0x1F // clang-format on typedef enum { M_STATE_NULL, M_STATE_WAIT, M_STATE_WALK, M_STATE_RUN, M_STATE_AIM_1, M_STATE_SHOOT_1, M_STATE_AIM_2, M_STATE_SHOOT_2, M_STATE_SHOOT_3A, M_STATE_SHOOT_3B, M_STATE_SHOOT_4A, M_STATE_AIM_3, M_STATE_AIM_4, M_STATE_DEATH, M_STATE_SHOOT_4B, M_STATE_DUCK_START, M_STATE_DUCKED, M_STATE_DUCK_AIM, M_STATE_DUCK_SHOOT, M_STATE_DUCK_WALK, M_STATE_DUCK_END, } M_STATE; typedef enum { // clang-format off M_ANIM_SHOOT_1 = 1, M_ANIM_AIM_1 = 12, M_ANIM_DEATH = 14, M_ANIM_WALK_STOP = 17, M_ANIM_AIM_4A = 18, M_ANIM_AIM_4B = 19, M_ANIM_RUN_STOP_1 = 27, M_ANIM_RUN_STOP_2 = 28, // clang-format on } M_ANIM; static const CREATURE_GUN m_Gun = { .muzzle = { .pos = { 0, 160, 40 }, .mesh_num = 13 }, .tr3_enemy_flash = true, .tr3_flash = { .pos = { 0, 192, 40 }, .mesh_num = 13 }, .tr3_enemy_weapon_flags = 0, .tr3_flash_shade = 600, .tr3_flash_rot_x = -DEG_90, }; static void M_FireFinalShot( ITEM *const item, int16_t *const head, int16_t *const torso_y) { if (!Item_TestFrameEqual(item, 1)) { return; } AI_INFO info = {}; Creature_AIInfo(item, &info); if (!Creature_CanTargetEnemy(item, &info) || ABS(info.angle) >= DEG_45) { return; } *head = info.angle; *torso_y = info.angle; Creature_Shoot(item, &info, &m_Gun, info.angle, M_DAMAGE); Sound_Effect(SFX_LONDON_SWAT_FIRE, &item->pos, SPM_NORMAL); } static void M_CalculateEnemy(ITEM *const item) { CREATURE *const mp = item->creature_data; const ITEM *const lara_item = Lara_GetItem(); mp->enemy = (ITEM *)lara_item; const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; int32_t best_distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz }); for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) { const CREATURE *const creature = LOT_GetBaddieSlot(i); if (creature->item_num == NO_ITEM || creature == mp) { continue; } const ITEM *const candidate = Item_Get(creature->item_num); if (candidate != lara_item && candidate->object_id != O_PRISONER) { continue; } const XYZ_32 delta = { .x = candidate->pos.x - item->pos.x, .y = 0, .z = candidate->pos.z - item->pos.z, }; const int32_t distance = XYZ_32_GetLength2(delta); if (distance < best_distance) { mp->enemy = (ITEM *)candidate; best_distance = distance; } } } static bool M_IsNearCover(const ITEM *const item, const AI_INFO *const info) { const XYZ_32 pos = XYZ_32_OffsetYaw(item->pos, item->rot.y + info->angle, WALL_L); int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); const int32_t height = Room_GetHeight(sector, pos); return item->pos.y > height + STEPUP_HEIGHT && item->pos.y < height + STEPUP_HEIGHT * 3 && info->distance > M_ALERT_DIST; } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t angle = 0; int16_t head = 0; int16_t tilt = 0; int16_t torso_x = 0; int16_t torso_y = 0; Creature_TestBoxDamage(item_num); if (item->hit_points <= 0) { item->hit_points = 0; if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, M_ANIM_DEATH, 0); item->current_anim_state = M_STATE_DEATH; } else if ((Random_GetControl() & 3) == 0) { M_FireFinalShot(item, &head, &torso_y); } goto finish; } if (item->ai_bits != 0) { Creature_GetAITarget(creature); } else { M_CalculateEnemy(item); } AI_INFO info = {}; Creature_AIInfo(item, &info); ITEM *const lara_item = Lara_GetItem(); AI_INFO lara_info = {}; if (creature->enemy == lara_item) { lara_info.distance = info.distance; lara_info.angle = info.angle; } else { const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; lara_info.angle = Math_Atan(dz, dx) - item->rot.y; lara_info.distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz }); } Creature_Mood(item, &info, creature->enemy != lara_item); angle = Creature_Turn(item, creature->maximum_turn); const bool near_cover = M_IsNearCover(item, &lara_info); ITEM *const enemy = creature->enemy; creature->enemy = lara_item; if (item->hit_status || lara_info.distance < M_ALERT_DIST || Creature_CanSeeEnemy(item, &lara_info)) { if (!creature->alerted) { Sound_Effect(SFX_AMERICAN_HOY, &item->pos, SPM_NORMAL); } Creature_AlertAllGuards(item_num); } creature->enemy = enemy; const int16_t anim_idx = Item_GetRelativeAnim(item); const int16_t frame_idx = Item_GetRelativeFrame(item); const LARA_INFO *const lara = Lara_GetLaraInfo(); switch (item->current_anim_state) { case M_STATE_WAIT: head = lara_info.angle; creature->maximum_turn = 0; if (anim_idx == M_ANIM_WALK_STOP || anim_idx == M_ANIM_RUN_STOP_1 || anim_idx == M_ANIM_RUN_STOP_2) { if (ABS(info.angle) < M_RUN_TURN) { item->rot.y += info.angle; } else if (info.angle < 0) { item->rot.y -= M_RUN_TURN; } else { item->rot.y += M_RUN_TURN; } } if ((item->ai_bits & AI_GUARD) != 0) { head = Creature_AIGuard(creature); item->goal_anim_state = M_STATE_WAIT; } else if ((item->ai_bits & AI_PATROL_1) != 0) { item->goal_anim_state = M_STATE_WALK; head = 0; } else if (near_cover && (lara->target == item || item->hit_status)) { item->goal_anim_state = M_STATE_DUCK_START; } else if (item->required_anim_state == M_STATE_DUCK_START) { item->goal_anim_state = M_STATE_DUCK_START; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if (Creature_CanTargetEnemy(item, &info)) { const int32_t rnd = Random_GetControl(); if (rnd < M_SHOOT_1_CHANCE) { item->goal_anim_state = M_STATE_SHOOT_1; } else if (rnd < M_SHOOT_2_CHANCE) { item->goal_anim_state = M_STATE_SHOOT_2; } else { item->goal_anim_state = M_STATE_AIM_3; } } else if ( creature->mood == MOOD_BORED || ((item->ai_bits & AI_FOLLOW) != 0 && (creature->reached_goal || lara_info.distance > M_RUN_DIST))) { if (info.ahead) { item->goal_anim_state = M_STATE_WAIT; } else { item->goal_anim_state = M_STATE_WALK; } } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_WALK: head = lara_info.angle; creature->maximum_turn = M_WALK_TURN; if ((item->ai_bits & AI_PATROL_1) != 0) { item->goal_anim_state = M_STATE_WALK; head = 0; } else if (near_cover && (lara->target == item || item->hit_status)) { item->required_anim_state = M_STATE_DUCK_START; item->goal_anim_state = M_STATE_WAIT; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if (Creature_CanTargetEnemy(item, &info)) { if (info.distance > M_WALK_DIST && info.zone_num == info.enemy_zone_num) { item->goal_anim_state = M_STATE_AIM_4; } else { item->goal_anim_state = M_STATE_WAIT; } } else if (creature->mood != MOOD_BORED) { item->goal_anim_state = M_STATE_RUN; } else if (info.ahead) { item->goal_anim_state = M_STATE_WALK; } else { item->goal_anim_state = M_STATE_WAIT; } break; case M_STATE_RUN: if (info.ahead) { head = info.angle; } creature->maximum_turn = M_RUN_TURN; tilt = angle / 2; if ((item->ai_bits & AI_GUARD) != 0) { item->goal_anim_state = M_STATE_WAIT; } else if (near_cover && (lara->target == item || item->hit_status)) { item->required_anim_state = M_STATE_DUCK_START; item->goal_anim_state = M_STATE_WAIT; } else if (creature->mood != MOOD_ESCAPE) { if (Creature_CanTargetEnemy(item, &info) || ((item->ai_bits & AI_FOLLOW) != 0 && (creature->reached_goal || lara_info.distance > M_RUN_DIST))) { item->goal_anim_state = M_STATE_WAIT; } else if (creature->mood == MOOD_BORED) { item->goal_anim_state = M_STATE_WALK; } } break; case M_STATE_AIM_1: if (info.ahead) { torso_y = info.angle; torso_x = info.x_angle; } if (anim_idx == M_ANIM_AIM_1 || (anim_idx == M_ANIM_SHOOT_1 && frame_idx == 10)) { if (!Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE)) { item->required_anim_state = M_STATE_WAIT; } } else if ( item->hit_status && (Random_GetControl() & M_DUCK_CHANCE) == 0 && near_cover) { item->required_anim_state = M_STATE_DUCK_START; item->goal_anim_state = M_STATE_WAIT; } break; case M_STATE_SHOOT_1: if (info.ahead) { torso_y = info.angle; torso_x = info.x_angle; } if (item->required_anim_state == M_STATE_WAIT) { item->goal_anim_state = M_STATE_WAIT; } break; case M_STATE_SHOOT_2: if (info.ahead) { torso_y = info.angle; torso_x = info.x_angle; } if (frame_idx == 0) { if (!Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE)) { item->goal_anim_state = M_STATE_WAIT; } } else if ( item->hit_status && (Random_GetControl() & M_DUCK_CHANCE) == 0 && near_cover) { item->required_anim_state = M_STATE_DUCK_START; item->goal_anim_state = M_STATE_WAIT; } break; case M_STATE_SHOOT_3A: case M_STATE_SHOOT_3B: if (info.ahead) { torso_y = info.angle; torso_x = info.x_angle; } if (frame_idx == 0 || frame_idx == 11) { if (!Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE)) { item->goal_anim_state = M_STATE_WAIT; } } else if ( item->hit_status && (Random_GetControl() & M_DUCK_CHANCE) == 0 && near_cover) { item->required_anim_state = M_STATE_DUCK_START; item->goal_anim_state = M_STATE_WAIT; } break; case M_STATE_AIM_4: if (info.ahead) { torso_y = info.angle; torso_x = info.x_angle; } if ((anim_idx == M_ANIM_AIM_4A && frame_idx == 17) || (anim_idx == M_ANIM_AIM_4B && frame_idx == 6)) { if (!Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE)) { item->required_anim_state = M_STATE_WALK; } } else if ( item->hit_status && (Random_GetControl() & M_DUCK_CHANCE) == 0 && near_cover) { item->required_anim_state = M_STATE_DUCK_START; item->goal_anim_state = M_STATE_WAIT; } if (info.distance < M_WALK_DIST) { item->required_anim_state = M_STATE_WALK; } break; case M_STATE_SHOOT_4A: case M_STATE_SHOOT_4B: if (info.ahead) { torso_y = info.angle; torso_x = info.x_angle; } if (item->required_anim_state == M_STATE_WALK) { item->goal_anim_state = M_STATE_WALK; } if (frame_idx == 16 && !Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE)) { item->goal_anim_state = M_STATE_WALK; } if (info.distance < M_WALK_DIST) { item->goal_anim_state = M_STATE_WALK; } break; case M_STATE_DUCKED: if (info.ahead) { head = info.angle; } creature->maximum_turn = 0; if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = M_STATE_DUCK_AIM; } else if ( item->hit_status || !near_cover || (info.ahead && (Random_GetControl() & M_DUCK_END_CHANCE) == 0)) { item->goal_anim_state = M_STATE_DUCK_END; } else { item->goal_anim_state = M_STATE_DUCK_WALK; } break; case M_STATE_DUCK_AIM: if (info.ahead) { torso_y = info.angle; } creature->maximum_turn = M_DUCK_TURN; if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = M_STATE_DUCK_SHOOT; } else { item->goal_anim_state = M_STATE_DUCKED; } break; case M_STATE_DUCK_SHOOT: if (info.ahead) { torso_y = info.angle; } if (frame_idx == 0 && (!Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE) || (Random_GetControl() & 7) == 0)) { item->goal_anim_state = M_STATE_DUCKED; } break; case M_STATE_DUCK_WALK: if (info.ahead) { head = info.angle; } creature->maximum_turn = M_WALK_TURN; if (Creature_CanTargetEnemy(item, &info) || item->hit_status || !near_cover || (info.ahead && (Random_GetControl() & M_DUCK_END_CHANCE) == 0)) { item->goal_anim_state = M_STATE_DUCKED; } break; } finish: Creature_Tilt(item, tilt); Creature_Joint(item, 0, torso_y); Creature_Joint(item, 1, torso_x); Creature_Joint(item, 2, head); Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->collision_func = Creature_Collision; obj->control_func = M_Control; obj->shadow_size = UNIT_SHADOW / 2; obj->radius = M_RADIUS; obj->hit_points = M_HIT_POINTS; obj->intelligent = true; obj->save_anim = true; obj->save_flags = true; obj->save_hitpoints = true; obj->save_position = true; Object_GetBone(obj, 6)->rot.x = true; Object_GetBone(obj, 6)->rot.y = true; Object_GetBone(obj, 13)->rot.y = true; } REGISTER_OBJECT(O_MP_2, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/mummy.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #define MUMMY_HITPOINTS 18 typedef enum { MUMMY_STATE_EMPTY = 0, MUMMY_STATE_STOP = 1, MUMMY_STATE_DEATH = 2, } MUMMY_STATE; static bool M_CanDropItems(const ITEM *const item) { return item->hit_points <= 0 || item->status == IS_DEACTIVATED; } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); item->touch_bits = 0; item->mesh_bits = 0xFFFF87FF; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); int16_t head = 0; if (Item_IsTriggerActive(item)) { if (!LOT_EnableBaddieAI(item_num, true)) { return; } item->status = IS_ACTIVE; } if (item->current_anim_state == MUMMY_STATE_STOP) { const ITEM *const lara_item = Lara_GetItem(); head = Math_Atan( lara_item->pos.z - item->pos.z, lara_item->pos.x - item->pos.x) - item->rot.y; CLAMP(head, -FRONT_ARC, FRONT_ARC); if (item->hit_points <= 0 || item->touch_bits) { item->goal_anim_state = MUMMY_STATE_DEATH; } } Creature_Head(item, head); Item_Animate(item); if (item->status == IS_DEACTIVATED) { // Count kill if Lara touches mummy and it falls. if (item->hit_points > 0) { Stats_AddKill(); } Item_RemoveActive(item_num); Carrier_TestItemDrops(item_num); item->hit_points = 0; } } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Object_Collision; obj->can_drop_items_func = M_CanDropItems; obj->hit_points = MUMMY_HITPOINTS; obj->save_flags = true; obj->save_hitpoints = true; obj->save_anim = true; Object_GetBone(obj, 2)->rot.y = true; } REGISTER_OBJECT(O_MUMMY, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/natla.c ================================================ #include #include #include #include #include #include #include #include #include #include #define NATLA_FLY_MODE 0x8000 #define NATLA_TIMER 0x7FFF #define NATLA_FIRE_ARC (DEG_1 * 30) // = 5460 #define NATLA_FLY_TURN (DEG_1 * 5) // = 910 #define NATLA_RUN_TURN (DEG_1 * 6) // = 1092 #define NATLA_LAND_CHANCE 256 #define NATLA_DIE_TIME (LOGIC_FPS * 16) // = 480 #define NATLA_HITPOINTS 400 #define NATLA_RADIUS (WALL_L / 5) // = 204 #define NATLA_SMARTNESS 0x7FFF typedef enum { NATLA_STATE_EMPTY = 0, NATLA_STATE_STOP = 1, NATLA_STATE_FLY = 2, NATLA_STATE_RUN = 3, NATLA_STATE_AIM = 4, NATLA_STATE_SEMIDEATH = 5, NATLA_STATE_SHOOT = 6, NATLA_STATE_FALL = 7, NATLA_STATE_STAND = 8, NATLA_STATE_DEATH = 9, } NATLA_STATE; static BITE m_NatlaGun = { .pos = { 5, 220, 7 }, .mesh_num = 4 }; static int32_t M_GetStage2HitPoints(const ITEM *const item) { return item->max_hit_points / 2; } static bool M_GunHit( ITEM *const item, const GAME_VECTOR *const start, const GAME_VECTOR *const hit_pos, int32_t *const damage) { if (item->current_anim_state == NATLA_STATE_SEMIDEATH) { if (damage != nullptr) { *damage = 0; } return false; } return true; } static bool M_IsTargetable(const ITEM *const item) { return item->hit_points > 0 && item->status == IS_ACTIVE && item->current_anim_state != NATLA_STATE_SEMIDEATH; } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const natla = item->creature_data; int16_t head = 0; int16_t angle = 0; int16_t tilt = 0; int16_t gun = natla->head_rotation * 7 / 8; int16_t timer = natla->flags & NATLA_TIMER; int16_t facing = (int16_t)(intptr_t)item->priv; if (item->hit_points <= 0 && item->current_anim_state != NATLA_STATE_SEMIDEATH) { item->goal_anim_state = NATLA_STATE_DEATH; } else if (item->hit_points <= M_GetStage2HitPoints(item)) { natla->lot.setup.step = STEP_L; natla->lot.setup.drop = -STEP_L; natla->lot.setup.fly = 0; AI_INFO info; Creature_AIInfo(item, &info); if (info.ahead && item->current_anim_state != NATLA_STATE_SEMIDEATH) { head = info.angle; } Creature_Mood(item, &info, true); angle = Creature_Turn(item, NATLA_RUN_TURN); int8_t shoot = info.angle > -NATLA_FIRE_ARC && info.angle < NATLA_FIRE_ARC && Creature_CanTargetEnemy(item, &info); if (facing) { item->rot.y += facing; facing = 0; } switch (item->current_anim_state) { case NATLA_STATE_FALL: if (item->pos.y < item->floor) { item->gravity = true; item->speed = 0; } else { item->gravity = false; item->goal_anim_state = NATLA_STATE_SEMIDEATH; item->pos.y = item->floor; timer = 0; } break; case NATLA_STATE_STAND: if (!shoot) { item->goal_anim_state = NATLA_STATE_RUN; } if (timer >= 20) { int16_t effect_num = Creature_Effect(item, &m_NatlaGun, Spawn_AtlanteanShard); if (effect_num != NO_EFFECT) { EFFECT *effect = Effect_Get(effect_num); gun = effect->rot.x; Sound_Effect( SFX_ATLANTEAN_NEEDLE, &effect->pos, SPM_NORMAL); } timer = 0; } break; case NATLA_STATE_RUN: tilt = angle; if (timer >= 20) { int16_t effect_num = Creature_Effect(item, &m_NatlaGun, Spawn_AtlanteanShard); if (effect_num != NO_EFFECT) { EFFECT *effect = Effect_Get(effect_num); gun = effect->rot.x; Sound_Effect( SFX_ATLANTEAN_NEEDLE, &effect->pos, SPM_NORMAL); } timer = 0; } if (shoot) { item->goal_anim_state = NATLA_STATE_STAND; } break; case NATLA_STATE_SEMIDEATH: if (timer == NATLA_DIE_TIME) { item->goal_anim_state = NATLA_STATE_STAND; natla->flags = 0; timer = 0; item->hit_points = M_GetStage2HitPoints(item); const MUSIC_PLAY_MODE mode = g_Config.audio.fix_speeches_killing_music ? MPM_OVERLAY : MPM_NO_REPEAT; Music_Play(MX_NATLA_SPEECH, mode); } else { if (g_Config.gameplay.target_mode == TARGET_LOCK_MODE_SEMI || g_Config.gameplay.target_mode == TARGET_LOCK_MODE_NONE) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->target = nullptr; } item->hit_points = 0; } break; case NATLA_STATE_FLY: item->goal_anim_state = NATLA_STATE_FALL; timer = 0; break; case NATLA_STATE_STOP: case NATLA_STATE_AIM: case NATLA_STATE_SHOOT: item->goal_anim_state = NATLA_STATE_SEMIDEATH; item->flags = 0; timer = 0; break; } } else { natla->lot.setup.step = STEP_L; natla->lot.setup.drop = -STEP_L; natla->lot.setup.fly = 0; AI_INFO info; Creature_AIInfo(item, &info); int8_t shoot = info.angle > -NATLA_FIRE_ARC && info.angle < NATLA_FIRE_ARC && Creature_CanTargetEnemy(item, &info); if (item->current_anim_state == NATLA_STATE_FLY && (natla->flags & NATLA_FLY_MODE)) { if (shoot && Random_GetControl() < NATLA_LAND_CHANCE) { natla->flags &= ~NATLA_FLY_MODE; } if (!(natla->flags & NATLA_FLY_MODE)) { Creature_Mood(item, &info, true); } natla->lot.setup.step = WALL_L * 20; natla->lot.setup.drop = -WALL_L * 20; natla->lot.setup.fly = STEP_L / 8; Creature_AIInfo(item, &info); } else if (!shoot) { natla->flags |= NATLA_FLY_MODE; } if (info.ahead) { head = info.angle; } if (item->current_anim_state != NATLA_STATE_FLY || (natla->flags & NATLA_FLY_MODE)) { Creature_Mood(item, &info, false); } item->rot.y -= facing; angle = Creature_Turn(item, NATLA_FLY_TURN); if (item->current_anim_state == NATLA_STATE_FLY) { if (info.angle > NATLA_FLY_TURN) { facing += NATLA_FLY_TURN; } else if (info.angle < -NATLA_FLY_TURN) { facing -= NATLA_FLY_TURN; } else { facing += info.angle; } item->rot.y += facing; } else { item->rot.y += facing - angle; facing = 0; } switch (item->current_anim_state) { case NATLA_STATE_STOP: timer = 0; if (natla->flags & NATLA_FLY_MODE) { item->goal_anim_state = NATLA_STATE_FLY; } else { item->goal_anim_state = NATLA_STATE_AIM; } break; case NATLA_STATE_FLY: if (!(natla->flags & NATLA_FLY_MODE) && item->pos.y == item->floor) { item->goal_anim_state = NATLA_STATE_STOP; } if (timer >= 30) { int16_t effect_num = Creature_Effect(item, &m_NatlaGun, Spawn_AtlanteanBomb); if (effect_num != NO_EFFECT) { EFFECT *effect = Effect_Get(effect_num); gun = effect->rot.x; Sound_Effect( SFX_ATLANTEAN_NEEDLE, &effect->pos, SPM_NORMAL); } timer = 0; } break; case NATLA_STATE_AIM: if (item->required_anim_state) { item->goal_anim_state = item->required_anim_state; } else if (shoot) { item->goal_anim_state = NATLA_STATE_SHOOT; } else { item->goal_anim_state = NATLA_STATE_STOP; } break; case NATLA_STATE_SHOOT: if (!item->required_anim_state) { int16_t effect_num = Creature_Effect(item, &m_NatlaGun, Spawn_AtlanteanBomb); if (effect_num != NO_EFFECT) { EFFECT *effect = Effect_Get(effect_num); gun = effect->rot.x; } effect_num = Creature_Effect(item, &m_NatlaGun, Spawn_AtlanteanBomb); if (effect_num != NO_EFFECT) { EFFECT *effect = Effect_Get(effect_num); effect->rot.y += (Random_GetControl() - 0x4000) / 4; } effect_num = Creature_Effect(item, &m_NatlaGun, Spawn_AtlanteanBomb); if (effect_num != NO_EFFECT) { EFFECT *effect = Effect_Get(effect_num); effect->rot.y += (Random_GetControl() - 0x4000) / 4; } item->required_anim_state = NATLA_STATE_STOP; } break; } } Creature_Tilt(item, tilt); natla->neck_rotation = -head; if (gun) { natla->head_rotation = gun; } timer++; natla->flags &= ~NATLA_TIMER; natla->flags |= timer & NATLA_TIMER; item->rot.y -= facing; Creature_Animate(item_num, angle, 0); item->rot.y += facing; item->priv = (void *)(intptr_t)facing; } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->collision_func = Creature_Collision; obj->initialise_func = Creature_Initialise; obj->control_func = M_Control; obj->gun_hit_func = M_GunHit; obj->is_targetable_func = M_IsTargetable; obj->shadow_size = UNIT_SHADOW / 2; obj->hit_points = NATLA_HITPOINTS; obj->radius = NATLA_RADIUS; obj->smartness = NATLA_SMARTNESS; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_anim = true; obj->save_flags = true; Object_GetBone(obj, 2)->rot.x = true; Object_GetBone(obj, 2)->rot.z = true; } REGISTER_OBJECT(O_NATLA, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/natla_gun.c ================================================ #include #include #include #include static void M_Control(const int16_t effect_num) { EFFECT *effect = Effect_Get(effect_num); const OBJECT *const obj = Object_Get(effect->object_id); effect->frame_num--; if (effect->frame_num <= obj->mesh_count) { Effect_Kill(effect_num); } if (effect->frame_num == -1) { return; } const XYZ_32 pos = XYZ_32_OffsetYaw(effect->pos, effect->rot.y, effect->speed); int16_t room_num = effect->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); if (pos.y >= Room_GetHeight(sector, pos) || pos.y <= Room_GetCeiling(sector, pos)) { return; } const int16_t new_effect_num = Effect_Create(room_num); if (new_effect_num != NO_EFFECT) { EFFECT *const new_effect = Effect_Get(new_effect_num); new_effect->pos = pos; new_effect->rot.y = effect->rot.y; new_effect->room_num = room_num; new_effect->speed = effect->speed; new_effect->frame_num = 0; new_effect->object_id = O_NATLA_GUN; } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; } REGISTER_OBJECT(O_NATLA_GUN, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/orca.c ================================================ #include #include #include #include #include #include #include #include #define M_FAST_RANGE SQUARE(WALL_L * 3 / 2) // = 2359296 #define M_ATTACK_1_RANGE SQUARE(WALL_L * 3 / 4) // = 589824 #define M_FAST_TURN (DEG_1 * 2) // = 364 #define M_SLOW_TURN (DEG_1 * 2) // = 364 #define M_RADIUS (WALL_L / 3) // = 341 typedef enum { M_STATE_SLOW, M_STATE_FAST, M_STATE_JUMP, M_STATE_SPLASH, M_STATE_SLOW_BUTT, M_STATE_FAST_BUTT, M_STATE_BREACH, M_STATE_ROLL_180 } M_STATE; static bool M_IsTargetable(const ITEM *const item) { return false; } static bool M_CanBeProjectileTarget(const ITEM *const item) { return false; } static void M_Control(int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; const ITEM *const lara_item = Lara_GetItem(); AI_INFO info = {}; Creature_AIInfo(item, &info); Creature_UpdateMood(item, &info, true); if (!(Room_Get(lara_item->room_num)->flags.underwater) && Lara_Vehicle_GetItem() == nullptr) { creature->mood = MOOD_BORED; } Creature_ApplyMood(item, &info, true); const int16_t angle = Creature_Turn(item, creature->maximum_turn); switch (item->current_anim_state) { case M_STATE_SLOW: creature->flags = 0; creature->maximum_turn = M_SLOW_TURN; if (creature->mood == MOOD_BORED) { if (Random_GetControl() & 0xFF) { item->goal_anim_state = M_STATE_SLOW; } else { item->goal_anim_state = M_STATE_JUMP; } } else if (info.ahead && info.distance < M_ATTACK_1_RANGE) { item->goal_anim_state = M_STATE_SLOW_BUTT; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_FAST; } else if (info.distance > M_FAST_RANGE) { if (info.angle >= 0x5000 || info.angle <= -0x5000) { item->goal_anim_state = M_STATE_ROLL_180; } else if (Random_GetControl() & 0x3F) { item->goal_anim_state = M_STATE_FAST; } else { item->goal_anim_state = M_STATE_BREACH; } } break; case M_STATE_FAST: creature->flags = 0; creature->maximum_turn = M_FAST_TURN; if (creature->mood == MOOD_BORED) { if (Random_GetControl() & 0xFF) { item->goal_anim_state = M_STATE_SLOW; } else { item->goal_anim_state = M_STATE_JUMP; } } else if (creature->mood != MOOD_ESCAPE) { if (info.ahead && info.distance < M_FAST_RANGE && info.zone_num == info.enemy_zone_num) { item->goal_anim_state = M_STATE_SLOW; } else if ( info.distance > M_FAST_RANGE && !(Random_GetControl() & 0x7F)) { item->goal_anim_state = M_STATE_JUMP; } else if (info.distance > M_FAST_RANGE && !info.ahead) { item->goal_anim_state = M_STATE_SLOW; } } break; case M_STATE_ROLL_180: creature->maximum_turn = 0; if (Item_GetRelativeFrame(item) == 59) { item->rot.x = -item->rot.x; item->rot.y += DEG_180; item->interp.prev.pos = item->pos; item->interp.prev.rot = item->rot; item->interp.result.pos = item->pos; item->interp.result.rot = item->rot; } break; } Creature_Animate(item_num, angle, 0); Creature_Underwater(item, WALL_L / 5); const int32_t time4 = Output_GetTimeInGame() * 4; if ((time4 & 4) != 0) { XYZ_32 pos = { .x = -32, .y = 16, .z = -300 }; Collide_GetJointAbsPosition(item, &pos, 5); int16_t room_num = item->room_num; Room_GetSector(pos, &room_num); const int32_t water_height = Room_GetWaterHeight(pos, room_num); if (water_height != NO_HEIGHT && pos.y < water_height) { FX_WATER_RIPPLE *const ripple = FX_Water_SetupRipple( pos.x, water_height, pos.z, -2 - (Random_GetControl() & 1), 0); if (ripple != nullptr) { ripple->init = 0; } } } } static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { if (!GF_BadIsMod("tr3-la")) { Creature_Collision(item_num, lara_item, coll); return; } ITEM *const item = Item_Get(item_num); const LARA_INFO *const lara = Lara_GetLaraInfo(); if (!Lara_TestBoundsCollide(item, coll->radius)) { return; } if (!Collide_TestCollision(item, lara_item)) { return; } Lara_Col_ItemPush(item, coll, coll->enable_hit, false); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = M_Collision; obj->is_targetable_func = M_IsTargetable; obj->can_be_projectile_target_func = M_CanBeProjectileTarget; obj->shadow_size = 128; obj->pivot_length = 200; obj->radius = M_RADIUS; obj->lot_setup = LOT_Setup(LOT_SETUP_FLYER); obj->lot_setup.block_mask = BOX_BLOCKABLE; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_ORCA, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/patrol_dog.c ================================================ #include #include #include #include #include #include // clang-format off #define M_LUNGE_RANGE SQUARE(WALL_L) #define M_LUNGE_TOUCH_BITS 0x6648 #define M_LUNGE_DAMAGE 50 #define M_STALK_RANGE SQUARE(WALL_L + (WALL_L / 2)) // = 0x240000 #define M_STALK_TURN (3 * DEG_1) #define M_BITE_RANGE SQUARE(WALL_L * 5 / 12) // = 0x2c4e4 #define M_BITE_TOUCH_BITS 0x48 #define M_BITE_DAMAGE 12 #define M_RUN_TURN (6 * DEG_1) #define M_STAT_TURN (1 * DEG_1) #define M_WALK_TURN (3 * DEG_1) #define M_MINIMUM_SLEEP_TIME (30 * 10) // = 300 #define M_SLEEP_CHANCE 0x100 // = 256 #define M_SLEEP_2_STAND_CHANCE 0x80 // = 128 #define M_STAT_CHANCE 0x100 // = 256 #define M_WALK_CHANCE 0x1000 // = 4096 #define M_AWARE_RANGE SQUARE(3 * WALL_L) // = 0x900000 // clang-format on static BITE m_DogBite = { .pos = { .x = 0, .y = 0, .z = 100 }, .mesh_num = 3, }; typedef enum { M_STATE_NULL, M_STATE_STOP, M_STATE_WALK, M_STATE_RUN, M_STATE_JUMP, M_STATE_STALK, M_STATE_ATTACK_1, M_STATE_HOWL, M_STATE_SLEEP, M_STATE_CROUCH, M_STATE_TURN, M_STATE_DEATH, M_STATE_ATTACK_2 } M_STATE; typedef enum { M_ANIM_STOP = 8, M_ANIM_DEATH_1 = 20, M_ANIM_DEATH_2 = 21, M_ANIM_DEATH_3 = 22, } M_ANIM; static M_ANIM m_DeathAnimCount = 4; static M_ANIM m_DeathAnims[4] = { M_ANIM_DEATH_1, M_ANIM_DEATH_2, M_ANIM_DEATH_3, M_ANIM_DEATH_1, }; static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); Item_SwitchToAnim(item, M_ANIM_STOP, 0); item->current_anim_state = M_STATE_STOP; } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t angle = 0; int16_t head = 0; int16_t x_head = 0; ITEM *const lara_item = Lara_GetItem(); if (item->hit_points <= 0) { if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim( item, m_DeathAnims[Random_GetControl() % m_DeathAnimCount], 0); item->current_anim_state = M_STATE_DEATH; } } else { if (item->ai_bits != 0) { Creature_GetAITarget(creature); } else { creature->enemy = lara_item; } AI_INFO info; Creature_AIInfo(item, &info); int32_t dist; if (creature->enemy == lara_item) { dist = info.distance; } else { dist = XYZ_32_GetLength2((XYZ_32) { .x = lara_item->pos.x - item->pos.x, .y = 0, .z = lara_item->pos.z - item->pos.z, }); } if (info.ahead) { head = info.angle; x_head = info.x_angle; } Creature_Mood(item, &info, true); if (creature->mood == MOOD_BORED) { creature->maximum_turn >>= 1; } angle = Creature_Turn(item, creature->maximum_turn); if (creature->hurt_by_lara || (dist < M_AWARE_RANGE && !(item->ai_bits & AI_MODIFY))) { Creature_AlertAllGuards(item_num); item->ai_bits &= ~AI_MODIFY; } const int16_t rnd = Random_GetControl(); const int16_t frame = Item_GetRelativeFrame(item); switch (item->current_anim_state) { case M_STATE_NULL: case M_STATE_SLEEP: head = 0; x_head = 0; if (creature->mood != MOOD_BORED && item->ai_bits != AI_MODIFY) { item->goal_anim_state = M_STATE_STOP; } else { creature->flags++; creature->maximum_turn = 0; if (creature->flags > M_MINIMUM_SLEEP_TIME && rnd < M_SLEEP_2_STAND_CHANCE) { item->goal_anim_state = M_STATE_STOP; } } break; #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wimplicit-fallthrough" case M_STATE_CROUCH: if (item->required_anim_state != 0) { item->goal_anim_state = item->required_anim_state; break; } #pragma GCC diagnostic pop case M_STATE_STOP: creature->maximum_turn = 0; if (item->ai_bits & AI_GUARD) { head = Creature_AIGuard(creature); if (!(Random_GetControl() & 0xFF)) { if (item->current_anim_state == M_STATE_STOP) { item->goal_anim_state = M_STATE_CROUCH; } else { item->goal_anim_state = M_STATE_STOP; } } } else if ( item->current_anim_state == M_STATE_CROUCH && rnd < M_SLEEP_2_STAND_CHANCE) { item->goal_anim_state = M_STATE_STOP; } else if (item->ai_bits & AI_PATROL_1) { if (item->current_anim_state == M_STATE_STOP) { item->goal_anim_state = M_STATE_WALK; } else { item->goal_anim_state = M_STATE_STOP; } } else if (creature->mood == MOOD_ESCAPE) { LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->target == item || !info.ahead || item->hit_status) { item->required_anim_state = M_STATE_RUN; item->goal_anim_state = M_STATE_CROUCH; } else { item->goal_anim_state = M_STATE_STOP; } } else if (creature->mood == MOOD_BORED) { creature->flags = 0; creature->maximum_turn = M_STAT_TURN; if (rnd < M_SLEEP_CHANCE && item->ai_bits & AI_MODIFY && item->current_anim_state == M_STATE_STOP) { item->goal_anim_state = M_STATE_SLEEP; creature->flags = 0; } else if (rnd < M_WALK_CHANCE) { if (item->current_anim_state == M_STATE_STOP) { item->goal_anim_state = M_STATE_WALK; } else { item->goal_anim_state = M_STATE_STOP; } } else if (!(rnd & 0x1F)) { item->goal_anim_state = M_STATE_HOWL; } } else { item->required_anim_state = M_STATE_RUN; if (item->current_anim_state == M_STATE_STOP) { item->goal_anim_state = M_STATE_CROUCH; } } break; case M_STATE_WALK: creature->maximum_turn = M_WALK_TURN; if (item->ai_bits & AI_PATROL_1) { item->goal_anim_state = M_STATE_WALK; } else if (creature->mood == MOOD_BORED && rnd < M_STAT_CHANCE) { item->goal_anim_state = M_STATE_STOP; } else { item->goal_anim_state = M_STATE_STALK; } break; case M_STATE_RUN: creature->maximum_turn = M_RUN_TURN; if (creature->mood == MOOD_ESCAPE) { LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->target != item && info.ahead) { item->goal_anim_state = M_STATE_CROUCH; } } else if (creature->mood == MOOD_BORED) { item->goal_anim_state = M_STATE_CROUCH; } else if (info.bite && info.distance < M_LUNGE_RANGE) { item->goal_anim_state = M_STATE_ATTACK_1; } else if (info.distance < M_STALK_RANGE) { item->required_anim_state = M_STATE_STALK; item->goal_anim_state = M_STATE_CROUCH; } break; case M_STATE_STALK: creature->maximum_turn = M_STALK_TURN; if (creature->mood == MOOD_BORED) { item->goal_anim_state = M_STATE_CROUCH; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if (info.bite && info.distance < M_BITE_RANGE) { item->goal_anim_state = M_STATE_ATTACK_2; item->required_anim_state = M_STATE_STALK; } else if (info.distance > M_STALK_RANGE || item->hit_status) { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_ATTACK_1: if (info.bite && item->touch_bits & M_LUNGE_TOUCH_BITS && frame >= 4 && frame <= 14) { Creature_Effect(item, &m_DogBite, Spawn_Blood); Lara_TakeDamage(M_LUNGE_DAMAGE, true); } item->goal_anim_state = M_STATE_RUN; break; case M_STATE_HOWL: head = 0; x_head = 0; break; case M_STATE_ATTACK_2: if (info.bite && item->touch_bits & M_BITE_TOUCH_BITS && ((frame >= 9 && frame <= 12) || (frame >= 22 && frame <= 25))) { Creature_Effect(item, &m_DogBite, Spawn_Blood); Lara_TakeDamage(M_BITE_DAMAGE, true); } break; } } Creature_Tilt(item, 0); Creature_Joint(item, 0, head); Creature_Joint(item, 1, x_head); Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->shadow_size = 128; obj->hit_points = 16; obj->pivot_length = 300; obj->radius = 341; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 2)->rot.y = true; Object_GetBone(obj, 2)->rot.x = true; } REGISTER_OBJECT(O_PATROL_DOG, M_Setup) REGISTER_OBJECT(O_HUSKIE, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/pierre.c ================================================ #include #include #include #include #include #include #include #include #include #include #define PIERRE_POSE_CHANCE 0x60 // = 96 #define PIERRE_SHOT_DAMAGE 50 #define PIERRE_WALK_TURN (DEG_1 * 3) // = 546 #define PIERRE_RUN_TURN (DEG_1 * 6) // = 1092 #define PIERRE_WALK_RANGE SQUARE(WALL_L * 3) // = 9437184 #define PIERRE_DIE_ANIM 12 #define PIERRE_WIMP_CHANCE 0x2000 #define PIERRE_RUN_HITPOINTS 40 #define PIERRE_DISAPPEAR 10 #define PIERRE_HITPOINTS 70 #define PIERRE_RADIUS (WALL_L / 10) // = 102 #define PIERRE_SMARTNESS 0x7FFF typedef enum { PIERRE_STATE_EMPTY = 0, PIERRE_STATE_STOP = 1, PIERRE_STATE_WALK = 2, PIERRE_STATE_RUN = 3, PIERRE_STATE_AIM = 4, PIERRE_STATE_DEATH = 5, PIERRE_STATE_POSE = 6, PIERRE_STATE_SHOOT = 7, } PIERRE_STATE; static const CREATURE_GUN m_PierreGun1 = { .muzzle = { .pos = { 60, 200, 0 }, .mesh_num = 11 }, }; static const CREATURE_GUN m_PierreGun2 = { .muzzle = { .pos = { -57, 200, 0 }, .mesh_num = 14 }, }; static int16_t m_PierreItemNum = NO_ITEM; static bool M_CanDropItems(const ITEM *const item) { return item->hit_points <= 0 && (item->flags & IF_ONE_SHOT) != 0; } static void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage) { if (stage == SAVEGAME_STAGE_AFTER_LOAD) { if (item->hit_points <= 0 && (item->flags & IF_ONE_SHOT)) { const uint16_t flags = Music_GetTrackFlags(Music_ToGameID(MX_PIERRE_SPEECH)); Music_SetTrackFlags( Music_ToGameID(MX_PIERRE_SPEECH), flags | IF_ONE_SHOT); } } } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (g_Config.gameplay.change_pierre_spawn) { if (m_PierreItemNum == NO_ITEM) { m_PierreItemNum = item_num; } else if (m_PierreItemNum != item_num) { ITEM *old_pierre = Item_Get(m_PierreItemNum); if (old_pierre->flags & IF_ONE_SHOT) { if (!(item->flags & IF_ONE_SHOT)) { Item_Kill(item_num); } } else { Item_Kill(m_PierreItemNum); m_PierreItemNum = item_num; } } } else { if (m_PierreItemNum == NO_ITEM) { m_PierreItemNum = item_num; } else if (m_PierreItemNum != item_num) { if (item->flags & IF_ONE_SHOT) { Item_Kill(m_PierreItemNum); } else { Item_Kill(item_num); } } } if (!Creature_Activate(item_num)) { return; } CREATURE *const pierre = item->creature_data; int16_t head = 0; int16_t angle = 0; int16_t tilt = 0; if (item->hit_points <= PIERRE_RUN_HITPOINTS && !(item->flags & IF_ONE_SHOT)) { item->hit_points = PIERRE_RUN_HITPOINTS; pierre->flags++; } if (item->hit_points <= 0) { if (item->current_anim_state != PIERRE_STATE_DEATH) { item->current_anim_state = PIERRE_STATE_DEATH; Item_SwitchToAnim(item, PIERRE_DIE_ANIM, 0); } } else { AI_INFO info; Creature_AIInfo(item, &info); if (info.ahead) { head = info.angle; } if (pierre->flags) { info.enemy_zone_num = -1; item->hit_status = true; } Creature_Mood(item, &info, false); angle = Creature_Turn(item, pierre->maximum_turn); switch (item->current_anim_state) { case PIERRE_STATE_STOP: if (item->required_anim_state) { item->goal_anim_state = item->required_anim_state; } else if (pierre->mood == MOOD_BORED) { item->goal_anim_state = Random_GetControl() < PIERRE_POSE_CHANCE ? PIERRE_STATE_POSE : PIERRE_STATE_WALK; } else if (pierre->mood == MOOD_ESCAPE) { item->goal_anim_state = PIERRE_STATE_RUN; } else { item->goal_anim_state = PIERRE_STATE_WALK; } break; case PIERRE_STATE_POSE: if (pierre->mood != MOOD_BORED) { item->goal_anim_state = PIERRE_STATE_STOP; } else if (Random_GetControl() < PIERRE_POSE_CHANCE) { item->required_anim_state = PIERRE_STATE_WALK; item->goal_anim_state = PIERRE_STATE_STOP; } break; case PIERRE_STATE_WALK: pierre->maximum_turn = PIERRE_WALK_TURN; if (pierre->mood == MOOD_BORED && Random_GetControl() < PIERRE_POSE_CHANCE) { item->required_anim_state = PIERRE_STATE_POSE; item->goal_anim_state = PIERRE_STATE_STOP; } else if (pierre->mood == MOOD_ESCAPE) { item->required_anim_state = PIERRE_STATE_RUN; item->goal_anim_state = PIERRE_STATE_STOP; } else if (Creature_CanTargetEnemy(item, &info)) { item->required_anim_state = PIERRE_STATE_AIM; item->goal_anim_state = PIERRE_STATE_STOP; } else if (!info.ahead || info.distance > PIERRE_WALK_RANGE) { item->required_anim_state = PIERRE_STATE_RUN; item->goal_anim_state = PIERRE_STATE_STOP; } break; case PIERRE_STATE_RUN: pierre->maximum_turn = PIERRE_RUN_TURN; tilt = angle / 2; if (pierre->mood == MOOD_BORED && Random_GetControl() < PIERRE_POSE_CHANCE) { item->required_anim_state = PIERRE_STATE_POSE; item->goal_anim_state = PIERRE_STATE_STOP; } else if (Creature_CanTargetEnemy(item, &info)) { item->required_anim_state = PIERRE_STATE_AIM; item->goal_anim_state = PIERRE_STATE_STOP; } else if (info.ahead && info.distance < PIERRE_WALK_RANGE) { item->required_anim_state = PIERRE_STATE_WALK; item->goal_anim_state = PIERRE_STATE_STOP; } break; case PIERRE_STATE_AIM: if (item->required_anim_state) { item->goal_anim_state = item->required_anim_state; } else if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = PIERRE_STATE_SHOOT; } else { item->goal_anim_state = PIERRE_STATE_STOP; } break; case PIERRE_STATE_SHOOT: if (!item->required_anim_state) { Creature_Shoot( item, &info, &m_PierreGun1, head, PIERRE_SHOT_DAMAGE / 2); Creature_Shoot( item, &info, &m_PierreGun2, head, PIERRE_SHOT_DAMAGE / 2); item->required_anim_state = PIERRE_STATE_AIM; } if (pierre->mood == MOOD_ESCAPE && Random_GetControl() > PIERRE_WIMP_CHANCE) { item->required_anim_state = PIERRE_STATE_STOP; } break; } } Creature_Tilt(item, tilt); Creature_Head(item, head); Creature_Animate(item_num, angle, 0); if (pierre->flags) { GAME_VECTOR target; target.x = item->pos.x; target.y = item->pos.y - WALL_L; target.z = item->pos.z; GAME_VECTOR start; start.x = g_Camera.pos.x; start.y = g_Camera.pos.y; start.z = g_Camera.pos.z; start.room_num = g_Camera.pos.room_num; if (LOS_Check(&start, &target, true)) { pierre->flags = 1; } else if (pierre->flags > PIERRE_DISAPPEAR) { item->hit_points = 0; LOT_DisableBaddieAI(item_num); Item_Kill(item_num); m_PierreItemNum = NO_ITEM; } } int16_t wh = Room_GetWaterHeight(item->pos, item->room_num); if (wh != NO_HEIGHT) { item->hit_points = 0; LOT_DisableBaddieAI(item_num); Item_Kill(item_num); m_PierreItemNum = NO_ITEM; } } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = Creature_Initialise; obj->handle_save_func = M_HandleSave; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->can_drop_items_func = M_CanDropItems; obj->shadow_size = UNIT_SHADOW / 2; obj->hit_points = PIERRE_HITPOINTS; obj->radius = PIERRE_RADIUS; obj->smartness = PIERRE_SMARTNESS; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_anim = true; obj->save_flags = true; m_PierreItemNum = NO_ITEM; Object_GetBone(obj, 6)->rot.y = true; } REGISTER_OBJECT(O_PIERRE, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/pod.c ================================================ #include #include #include #include #include #define POD_EXPLODE_DIST (WALL_L * 4) // = 4096 typedef enum { POD_STATE_SET = 0, POD_STATE_EXPLODE = 1, } POD_STATE; typedef struct { int16_t bug_item_num; } M_PRIV; static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; p->bug_item_num = NO_ITEM; const int16_t bug_item_num = Item_CreateLevelItem(); if (bug_item_num != NO_ITEM) { ITEM *const bug = Item_Get(bug_item_num); bug->object_id = Pod_GetBugObjectID(item); bug->room_num = item->room_num; bug->pos.x = item->pos.x; bug->pos.y = item->pos.y; bug->pos.z = item->pos.z; bug->rot.y = item->rot.y; bug->flags = IF_INVISIBLE; bug->shade.value_1 = -1; Item_Initialise(bug_item_num); p->bug_item_num = bug_item_num; } item->flags = 0; item->mesh_bits = 0xFF0001FF; } static void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage) { if (stage == SAVEGAME_STAGE_AFTER_LOAD) { if (item->status == IS_DEACTIVATED) { item->mesh_bits = 0x1FF; item->collidable = false; } } } static int16_t M_GetCarrierItemNum(const ITEM *const item) { const M_PRIV *const p = item->priv; return p->bug_item_num; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (item->goal_anim_state != POD_STATE_EXPLODE) { int32_t explode = 0; if (item->flags & IF_ONE_SHOT) { explode = 1; } else if (item->object_id == O_BIG_POD) { explode = 1; } else { const ITEM *const lara_item = Lara_GetItem(); int32_t x = lara_item->pos.x - item->pos.x; int32_t y = lara_item->pos.y - item->pos.y; int32_t z = lara_item->pos.z - item->pos.z; if (ABS(x) < POD_EXPLODE_DIST && ABS(y) < POD_EXPLODE_DIST && ABS(z) < POD_EXPLODE_DIST) { explode = 1; } } if (explode) { item->goal_anim_state = POD_STATE_EXPLODE; item->mesh_bits = 0xFFFFFF; item->collidable = false; Item_Explode(item_num, 0xFFFE00, 0); const M_PRIV *const p = item->priv; if (p->bug_item_num != NO_ITEM) { ITEM *const bug = Item_Get(p->bug_item_num); if (Object_Get(bug->object_id)->loaded) { bug->touch_bits = 0; Item_AddActive(p->bug_item_num); if (LOT_EnableBaddieAI(p->bug_item_num, false)) { bug->status = IS_ACTIVE; } else { bug->status = IS_INVISIBLE; } } } item->status = IS_DEACTIVATED; } } Item_Animate(item); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->handle_save_func = M_HandleSave; obj->control_func = M_Control; obj->collision_func = Object_Collision; obj->carrier_item_num_func = M_GetCarrierItemNum; obj->priv_size = sizeof(M_PRIV); obj->save_anim = true; obj->save_flags = true; } OBJECT_ID Pod_GetBugObjectID(const ITEM *const item) { switch ((item->flags & IF_CODE_BITS) >> 9) { case 1: return O_ATLANTEAN_SHOOTER; case 2: return O_CENTAUR; case 4: return O_TORSO; case 8: return O_ATLANTEAN_GROUND; default: return O_ATLANTEAN_WINGED; } } REGISTER_OBJECT(O_PODS, M_Setup) REGISTER_OBJECT(O_BIG_POD, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/pod.h ================================================ #pragma once #include OBJECT_ID Pod_GetBugObjectID(const ITEM *item); ================================================ FILE: src/trx/game/objects/creatures/prisoner.c ================================================ #include #include #include #include #include #include #include #include // clang-format off #define M_RADIUS (WALL_L / 10) // = 102 #define M_HIT_POINTS 20 #define M_PUNCH_1_DAMAGE 40 #define M_PUNCH_3_DAMAGE 50 #define M_TOUCH_BITS 0b00100100'00000000 #define M_RUN_DIST SQUARE(WALL_L * 2) // = 4194304 #define M_WALK_DIST SQUARE(WALL_L) // = 1048576 #define M_ATTACK_DIST_1 SQUARE(WALL_L / 3) // = 116281 #define M_ATTACK_DIST_2 SQUARE(WALL_L * 2 / 3) // = 465124 #define M_ATTACK_DIST_3 SQUARE(WALL_L * 3 / 4) // = 589824 #define M_WALK_TURN (DEG_1 * 7) // = 1274 #define M_RUN_TURN (DEG_1 * 11) // = 2002 // clang-format on typedef enum { M_STATE_NULL, M_STATE_STOP, M_STATE_WALK, M_STATE_PUNCH_3, M_STATE_AIM_3, M_STATE_WAIT, M_STATE_AIM_2, M_STATE_AIM_1, M_STATE_PUNCH_2, M_STATE_PUNCH_1, M_STATE_RUN, M_STATE_DEATH, M_STATE_UP_4, M_STATE_UP_2, M_STATE_UP_3, M_STATE_DOWN_4, } M_STATE; typedef enum { // clang-format off M_ANIM_STOP = 6, M_ANIM_DEATH = 26, M_ANIM_UP_4 = 27, M_ANIM_UP_2 = 28, M_ANIM_UP_3 = 29, M_ANIM_DOWN_4 = 30, // clang-format on } M_ANIM; static const BITE m_Bite = { .pos = { 10, 10, 11 }, .mesh_num = 13, }; static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); Item_SwitchToAnim(item, M_ANIM_STOP, 0); item->current_anim_state = M_STATE_STOP; item->goal_anim_state = M_STATE_STOP; } static void M_CalculateEnemy(ITEM *const item) { CREATURE *const prisoner = item->creature_data; const ITEM *const lara_item = Lara_GetItem(); if (prisoner->hurt_by_lara) { prisoner->enemy = (ITEM *)lara_item; return; } int32_t best_distance = INT32_MAX; prisoner->enemy = nullptr; for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) { const CREATURE *const creature = LOT_GetBaddieSlot(i); if (creature->item_num == NO_ITEM || creature == prisoner) { continue; } const ITEM *const candidate = Item_Get(creature->item_num); if (candidate == lara_item || candidate->object_id == item->object_id || candidate->object_id == O_SENTRY_GUN || candidate->hit_points <= 0) { continue; } const XYZ_32 delta = { .x = candidate->pos.x - item->pos.x, .y = 0, .z = candidate->pos.z - item->pos.z, }; if (ABS(delta.x) > 0x7D00 || ABS(delta.z) > 0x7D00) { continue; } const int32_t distance = XYZ_32_GetLength2(delta); if (distance < best_distance) { prisoner->enemy = (ITEM *)candidate; best_distance = distance; } } } static bool M_Vault(ITEM *const item, const int16_t angle) { const int32_t vault_result = Creature_Vault(Item_GetIndex(item), angle, 2, 260); switch (vault_result) { case -4: Item_SwitchToAnim(item, M_ANIM_DOWN_4, 0); item->current_anim_state = M_STATE_DOWN_4; return true; case 2: Item_SwitchToAnim(item, M_ANIM_UP_2, 0); item->current_anim_state = M_STATE_UP_2; return true; case 3: Item_SwitchToAnim(item, M_ANIM_UP_3, 0); item->current_anim_state = M_STATE_UP_3; return true; case 4: Item_SwitchToAnim(item, M_ANIM_UP_4, 0); item->current_anim_state = M_STATE_UP_4; return true; default: return false; } } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t angle = 0; int16_t head = 0; int16_t tilt = 0; int16_t torso_x = 0; int16_t torso_y = 0; Creature_TestBoxDamage(item_num); if (item->hit_points <= 0) { if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, M_ANIM_DEATH, 0); item->current_anim_state = M_STATE_DEATH; creature->lot.setup.step = STEP_L; } goto finish; } if (item->ai_bits != 0 && item->ai_bits != AI_MODIFY) { Creature_GetAITarget(creature); } else { M_CalculateEnemy(item); } if (item->ai_bits == AI_MODIFY) { item->hit_points = M_HIT_POINTS * 10; } ITEM *const lara_item = Lara_GetItem(); const bool hurt_by_lara = creature->hurt_by_lara; if (creature->enemy == nullptr && creature->alerted && g_Config.gameplay.ally_hostility_policy == ALLY_HOSTILITY_POLICY_SHARED) { creature->enemy = lara_item; } else if (!hurt_by_lara && creature->enemy == lara_item) { creature->enemy = nullptr; } // Enforce the following state to avoid Creature_AIInfo resetting ahead, // bite and distance when the prisoner is friendly. ITEM *const enemy = creature->enemy; if (enemy == nullptr) { creature->enemy = lara_item; creature->hurt_by_lara = true; } AI_INFO info = {}; Creature_AIInfo(item, &info); creature->enemy = enemy; creature->hurt_by_lara = hurt_by_lara; int32_t enemy_dist; int32_t enemy_angle; if (creature->enemy == lara_item) { enemy_dist = info.distance; enemy_angle = info.angle; } else { const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; enemy_angle = Math_Atan(dz, dx) - item->rot.y; enemy_dist = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz }); } Creature_Mood(item, &info, true); angle = Creature_Turn(item, creature->maximum_turn); if (creature->hurt_by_lara) { if (!creature->alerted) { Sound_Effect(SFX_AMERICAN_HOY, &item->pos, SPM_NORMAL); } Creature_AlertAllGuards(item_num); } const LARA_INFO *const lara = Lara_GetLaraInfo(); switch (item->current_anim_state) { case M_STATE_STOP: case M_STATE_WAIT: if (item->current_anim_state == M_STATE_WAIT && (creature->alerted || item->goal_anim_state == M_STATE_RUN)) { item->goal_anim_state = M_STATE_STOP; break; } creature->flags = 0; creature->maximum_turn = 0; head = enemy_angle; if ((item->ai_bits & AI_GUARD) != 0) { head = Creature_AIGuard(creature); if ((Random_GetControl() & 0xFF) == 0) { if (item->current_anim_state == M_STATE_STOP) { item->goal_anim_state = M_STATE_WAIT; } else { item->goal_anim_state = M_STATE_STOP; } } } else if ((item->ai_bits & AI_PATROL_1) != 0) { item->goal_anim_state = M_STATE_WALK; } else if (creature->mood == MOOD_ESCAPE) { if (lara->target != item && info.ahead && !item->hit_status) { item->goal_anim_state = M_STATE_STOP; } else { item->goal_anim_state = M_STATE_RUN; } } else if ( creature->mood == MOOD_BORED || ((item->ai_bits & AI_FOLLOW) != 0 && (creature->reached_goal || enemy_dist > M_RUN_DIST))) { if (item->required_anim_state != M_STATE_NULL) { item->goal_anim_state = item->required_anim_state; } else if (info.ahead) { item->goal_anim_state = M_STATE_STOP; } else { item->goal_anim_state = M_STATE_RUN; } } else if (info.bite && info.distance < M_ATTACK_DIST_1) { item->goal_anim_state = M_STATE_AIM_1; } else if (info.bite && info.distance < M_ATTACK_DIST_2) { item->goal_anim_state = M_STATE_AIM_2; } else if (info.bite && info.distance < M_WALK_DIST) { item->goal_anim_state = M_STATE_WALK; } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_WALK: head = enemy_angle; creature->maximum_turn = M_WALK_TURN; if ((item->ai_bits & AI_PATROL_1) != 0) { item->goal_anim_state = M_STATE_WALK; head = 0; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if (creature->mood == MOOD_BORED) { if (Random_GetControl() < 256) { item->required_anim_state = M_STATE_WAIT; item->goal_anim_state = M_STATE_STOP; } } else if (info.bite && info.distance < M_ATTACK_DIST_1) { item->goal_anim_state = M_STATE_STOP; } else if (info.bite && info.distance < M_ATTACK_DIST_3) { item->goal_anim_state = M_STATE_AIM_3; } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_RUN: if (info.ahead) { head = info.angle; } creature->maximum_turn = M_RUN_TURN; tilt = angle / 2; if ((item->ai_bits & AI_GUARD) != 0) { item->goal_anim_state = M_STATE_STOP; } else if (creature->mood == MOOD_ESCAPE) { if (lara->target != item && info.ahead) { item->goal_anim_state = M_STATE_STOP; } } else if ( (item->ai_bits & AI_FOLLOW) != 0 && (creature->reached_goal || enemy_dist > M_RUN_DIST)) { item->goal_anim_state = M_STATE_STOP; } else if (creature->mood == MOOD_BORED) { item->goal_anim_state = M_STATE_WALK; } else if (info.ahead && info.distance < M_WALK_DIST) { item->goal_anim_state = M_STATE_WALK; } break; case M_STATE_AIM_1: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } creature->maximum_turn = M_WALK_TURN; creature->flags = 0; if (info.bite && info.distance < M_ATTACK_DIST_1) { item->goal_anim_state = M_STATE_PUNCH_1; } else { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_AIM_2: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } creature->maximum_turn = M_WALK_TURN; creature->flags = 0; if (info.ahead && info.distance < M_ATTACK_DIST_2) { item->goal_anim_state = M_STATE_PUNCH_2; } else { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_AIM_3: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } creature->maximum_turn = M_WALK_TURN; creature->flags = 0; if (info.bite && info.distance < M_ATTACK_DIST_3) { item->goal_anim_state = M_STATE_PUNCH_3; } else { item->goal_anim_state = M_STATE_WALK; } break; case M_STATE_PUNCH_1: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } creature->maximum_turn = M_WALK_TURN; if (enemy == lara_item) { if (creature->flags == 0 && (item->touch_bits & M_TOUCH_BITS) != 0) { Lara_TakeDamage(M_PUNCH_1_DAMAGE, true); Creature_Effect(item, &m_Bite, Spawn_Blood); Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL); creature->flags = 1; } } else if (creature->flags == 0 && enemy != nullptr) { if (ABS(enemy->pos.x - item->pos.x) < STEP_L && ABS(enemy->pos.y - item->pos.y) <= STEP_L && ABS(enemy->pos.z - item->pos.z) < STEP_L) { Item_TakeDamage(enemy, M_PUNCH_1_DAMAGE / 2, true); Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL); creature->flags = 1; Creature_Effect(item, &m_Bite, Spawn_Blood); } } break; case M_STATE_PUNCH_2: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } creature->maximum_turn = M_WALK_TURN; if (enemy == lara_item) { if (creature->flags == 0 && (item->touch_bits & M_TOUCH_BITS) != 0) { Lara_TakeDamage(M_PUNCH_1_DAMAGE, true); Creature_Effect(item, &m_Bite, Spawn_Blood); Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL); creature->flags = 1; } } else if (creature->flags == 0 && enemy != nullptr) { if (ABS(enemy->pos.x - item->pos.x) < STEP_L && ABS(enemy->pos.y - item->pos.y) <= STEP_L && ABS(enemy->pos.z - item->pos.z) < STEP_L) { Item_TakeDamage(enemy, M_PUNCH_1_DAMAGE / 2, true); creature->flags = 1; Creature_Effect(item, &m_Bite, Spawn_Blood); Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL); } } if (info.ahead && info.distance > M_ATTACK_DIST_2 && info.distance < M_ATTACK_DIST_3) { item->goal_anim_state = M_STATE_PUNCH_3; } break; case M_STATE_PUNCH_3: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } creature->maximum_turn = M_WALK_TURN; if (enemy == lara_item) { if (creature->flags != 2 && (item->touch_bits & M_TOUCH_BITS) != 0) { Lara_TakeDamage(M_PUNCH_3_DAMAGE, true); Creature_Effect(item, &m_Bite, Spawn_Blood); Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL); creature->flags = 2; } } else if (creature->flags != 2 && enemy != nullptr) { if (ABS(enemy->pos.x - item->pos.x) < STEP_L && ABS(enemy->pos.y - item->pos.y) <= STEP_L && ABS(enemy->pos.z - item->pos.z) < STEP_L) { Item_TakeDamage(enemy, M_PUNCH_3_DAMAGE / 2, true); Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL); creature->flags = 2; Creature_Effect(item, &m_Bite, Spawn_Blood); } } break; } finish: Creature_Tilt(item, tilt); Creature_Joint(item, 0, torso_y); Creature_Joint(item, 1, torso_x); Creature_Joint(item, 2, head); if (item->current_anim_state >= M_STATE_DEATH) { creature->maximum_turn = 0; Creature_Animate(item_num, angle, 0); } else if (M_Vault(item, angle)) { creature->maximum_turn = 0; } } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->collision_func = Creature_Collision; obj->control_func = M_Control; obj->shadow_size = UNIT_SHADOW / 2; obj->radius = M_RADIUS; obj->hit_points = M_HIT_POINTS; obj->lot_setup = LOT_Setup(LOT_SETUP_CLIMBER); obj->intelligent = true; obj->save_anim = true; obj->save_flags = true; obj->save_hitpoints = true; obj->save_position = true; Object_GetBone(obj, 6)->rot.x = true; Object_GetBone(obj, 6)->rot.y = true; Object_GetBone(obj, 13)->rot.y = true; } REGISTER_OBJECT(O_PRISONER, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/punk.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include // clang-format off #define M_RADIUS (WALL_L / 10) // = 102 #define M_HITPOINTS 20 #define M_TOUCH_BITS 0b0100100'00000000 #define M_IGNITE_COUNT 3 #define M_SWIPE_DAMAGE 100 #define M_HIT_DAMAGE 80 #define M_MAX_FIRE_DIST (WALL_L * 16) // = 16384 #define M_ALERT_DIST SQUARE(WALL_L) // = 1048576 #define M_ALERT_HEIGHT (STEP_L * 5) // = 1280 #define M_WALK_DIST SQUARE(WALL_L) // = 1048576 #define M_RUN_DIST SQUARE(WALL_L * 2) // = 4194304 #define M_ATTACK_RANGE_1 SQUARE(WALL_L / 2) // = 262144 #define M_ATTACK_RANGE_2 SQUARE(WALL_L) // = 1048576 #define M_ATTACK_RANGE_3 SQUARE(WALL_L * 5 / 4) // = 1638400 #define M_WALK_TURN (DEG_1 * 5) // = 910 #define M_RUN_TURN (DEG_1 * 6) // = 1092 #define M_WAIT_CHANCE 0x100 // clang-format on typedef enum { M_STATE_NULL, M_STATE_STOP, M_STATE_WALK, M_STATE_PUNCH_3, M_STATE_AIM_3, M_STATE_WAIT, M_STATE_AIM_2, M_STATE_AIM_1, M_STATE_PUNCH_2, M_STATE_PUNCH_1, M_STATE_RUN, M_STATE_DEATH, M_STATE_UP_2, M_STATE_UP_3, M_STATE_UP_4, M_STATE_DOWN_4, } M_STATE; typedef enum { // clang-format off M_ANIM_STAND = 6, M_ANIM_DEATH = 26, M_ANIM_UP_4 = 27, M_ANIM_UP_2 = 28, M_ANIM_UP_3 = 29, M_ANIM_DOWN_4 = 30, // clang-format on } M_ANIM; typedef struct { struct { bool initialised; bool on_fire; uint8_t hit_count; } stick; } M_PRIV; static const BITE m_Bite = { .pos = { 16, 48, 320 }, .mesh_num = 13, }; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "stick_initialised", &p->stick.initialised)); JSON_SHOULD(JSON_READ(io, "stick_on_fire", &p->stick.on_fire)); JSON_SHOULD(JSON_READ(io, "stick_hit_count", &p->stick.hit_count)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "stick_initialised", p->stick.initialised); JSONW_WRITE(io, "stick_on_fire", p->stick.on_fire); JSONW_WRITE(io, "stick_hit_count", p->stick.hit_count); } static void M_Initialise(const int16_t item_num) { Creature_Initialise(item_num); ITEM *const item = Item_Get(item_num); Item_SwitchToAnim(item, M_ANIM_STAND, 0); item->current_anim_state = M_STATE_STOP; item->goal_anim_state = M_STATE_STOP; } static void M_InitialiseStick(ITEM *const punk_item) { M_PRIV *const p = punk_item->priv; const int16_t fire_item_idx = Item_FindTypeAtPos( punk_item->room_num, punk_item->pos, O_FLAME_EMITTER_BIG); if (fire_item_idx != NO_ITEM) { ITEM *const fire_item = Item_Get(fire_item_idx); Item_Kill(fire_item_idx); fire_item->room_num = NO_ROOM; p->stick.on_fire = true; } p->stick.initialised = true; } static void M_TriggerFireSparks(const ITEM *const item) { const ITEM *const lara_item = Lara_GetItem(); const XZ_32 delta = { .x = lara_item->pos.x - item->pos.x, .z = lara_item->pos.z - item->pos.z, }; if (ABS(delta.x) > M_MAX_FIRE_DIST || ABS(delta.z) > M_MAX_FIRE_DIST) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 255; spark->src_color.g = (Random_GetControl() & 0x1F) + 48; spark->src_color.b = 48; spark->dst_color.r = (Random_GetControl() & 0x3F) + 192; spark->dst_color.g = (Random_GetControl() & 0x3F) + 128; spark->dst_color.b = 32; spark->fade_to_black = 8; spark->col_fade_speed = (Random_GetControl() & 3) + 12; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->life = (Random_GetControl() & 7) + 24; spark->s_life = spark->life; spark->dynamic = -1; spark->pos.x = (Random_GetControl() & 0xF) - 8; spark->pos.y = 0; spark->pos.z = (Random_GetControl() & 0xF) - 8; spark->vel.x = (Random_GetControl() & 0xFF) - 128; spark->vel.y = -16 - (Random_GetControl() & 0xF); spark->vel.z = (Random_GetControl() & 0xFF) - 128; spark->friction = 5; if ((Random_GetControl() & 1) != 0) { spark->gravity = -16 - (Random_GetControl() & 0x1F); spark->flags = SPARK_F_ATTACHED_NODE | SPARK_F_ALT_SPRITE | SPARK_F_ITEM | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->max_y_vel = -16 - (Random_GetControl() & 7); spark->rot_angle = Random_GetControl() & 0xFFF; if (Random_GetControl() & 1) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = (Random_GetControl() & 0xF) + 16; } } else { spark->flags = SPARK_F_ATTACHED_NODE | SPARK_F_ALT_SPRITE | SPARK_F_ITEM | SPARK_F_SPRITE | SPARK_F_SCALE; spark->gravity = -16 - (Random_GetControl() & 0x1F); spark->max_y_vel = -16 - (Random_GetControl() & 7); } spark->node_num = 2; spark->item_num = Item_GetIndex(item); spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->scalar = 1; uint8_t size = (Random_GetControl() & 0x1F) + 64; spark->src_size.width = size; spark->size.width = size; spark->src_size.height = size; spark->size.height = size; size >>= 2; spark->dst_size.width = size; spark->dst_size.height = size; Sparks_FinishSetup(spark); } static void M_TriggerFireLight(const ITEM *const item) { const int32_t rnd = Random_GetControl(); XYZ_32 pos = { .x = m_Bite.pos.x + (rnd & 0xF) - 8, .y = m_Bite.pos.y + ((rnd >> 4) & 0xF) - 8, .z = m_Bite.pos.z + ((rnd >> 8) & 0xF) - 8, }; Collide_GetJointAbsPosition(item, &pos, m_Bite.mesh_num); const RGB_888 color = { .r = 255 - ((rnd >> 4) & 0x1F), .g = 192 - ((rnd >> 6) & 0x1F), .b = rnd & 0x3F, }; Output_AddDynamicLightRGB(pos, 13, color); } static void M_HitLara(ITEM *const item, const int16_t damage) { Lara_TakeDamage(damage, true); Creature_Effect(item, &m_Bite, Spawn_Blood); Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL); M_PRIV *const p = item->priv; p->stick.hit_count++; CLAMPG(p->stick.hit_count, M_IGNITE_COUNT); if (p->stick.on_fire && p->stick.hit_count == M_IGNITE_COUNT) { Lara_CatchFire(); } } static bool M_Vault(ITEM *const item, const int16_t angle) { const int32_t vault_result = Creature_Vault(Item_GetIndex(item), angle, 2, 260); switch (vault_result) { case -4: Item_SwitchToAnim(item, M_ANIM_DOWN_4, 0); item->current_anim_state = M_STATE_DOWN_4; return true; case 2: Item_SwitchToAnim(item, M_ANIM_UP_2, 0); item->current_anim_state = M_STATE_UP_2; return true; case 3: Item_SwitchToAnim(item, M_ANIM_UP_3, 0); item->current_anim_state = M_STATE_UP_3; return true; case 4: Item_SwitchToAnim(item, M_ANIM_UP_4, 0); item->current_anim_state = M_STATE_UP_4; return true; default: return false; } } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; M_PRIV *const p = item->priv; if (!p->stick.initialised) { M_InitialiseStick(item); } if (p->stick.on_fire) { M_TriggerFireSparks(item); M_TriggerFireLight(item); } int16_t angle = 0; int16_t head = 0; int16_t tilt = 0; int16_t torso_x = 0; int16_t torso_y = 0; if (item->hit_points <= 0) { if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, M_ANIM_DEATH, 0); item->current_anim_state = M_STATE_DEATH; item->goal_anim_state = M_STATE_DEATH; creature->lot.setup.step = STEP_L; } goto finish; } ITEM *const lara_item = Lara_GetItem(); const LARA_INFO *const lara = Lara_GetLaraInfo(); if (item->ai_bits != 0) { Creature_GetAITarget(creature); } else { creature->enemy = lara_item; } AI_INFO info = {}; Creature_AIInfo(item, &info); AI_INFO lara_info = {}; if (creature->enemy == lara_item) { lara_info.distance = info.distance; lara_info.angle = info.angle; } else { const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; lara_info.angle = Math_Atan(dz, dx) - item->rot.y; lara_info.distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz }); } if (!creature->alerted && creature->enemy == lara_item) { creature->enemy = nullptr; } Creature_Mood(item, &info, true); angle = Creature_Turn(item, creature->maximum_turn); ITEM *const enemy = creature->enemy; creature->enemy = lara_item; if (item->hit_status || ((lara_info.distance < M_ALERT_DIST || Creature_CanSeeEnemy(item, &lara_info)) && ABS(lara_item->pos.y - item->pos.y) < M_ALERT_HEIGHT && Creature_IsHostile(item) && (item->ai_bits & AI_FOLLOW) == 0)) { if (!creature->alerted) { Sound_Effect(SFX_ENGLISH_HOY, &item->pos, SPM_NORMAL); } Creature_AlertAllGuards(item_num); } creature->enemy = enemy; switch (item->current_anim_state) { case M_STATE_STOP: case M_STATE_WAIT: if (item->current_anim_state == M_STATE_WAIT && (creature->alerted || item->goal_anim_state == M_STATE_RUN)) { item->goal_anim_state = M_STATE_STOP; break; } creature->flags = 0; creature->maximum_turn = 0; head = lara_info.angle; if ((item->ai_bits & AI_GUARD) != 0) { head = Creature_AIGuard(creature); if ((Random_GetControl() & 0xFF) == 0) { if (item->current_anim_state == M_STATE_STOP) { item->goal_anim_state = M_STATE_WAIT; } else { item->goal_anim_state = M_STATE_STOP; } } } else if ((item->ai_bits & AI_PATROL_1) != 0) { item->goal_anim_state = M_STATE_WALK; } else if (creature->mood == MOOD_ESCAPE) { if (lara->target != item && info.ahead && !item->hit_status) { item->goal_anim_state = M_STATE_STOP; } else { item->goal_anim_state = M_STATE_RUN; } } else if ( creature->mood == MOOD_BORED || ((item->ai_bits & AI_FOLLOW) != 0 && (creature->reached_goal || lara_info.distance > M_RUN_DIST))) { if (item->required_anim_state != M_STATE_NULL) { item->goal_anim_state = item->required_anim_state; } else if (info.ahead) { item->goal_anim_state = M_STATE_STOP; } else { item->goal_anim_state = M_STATE_RUN; } } else if (info.bite && info.distance < M_ATTACK_RANGE_1) { item->goal_anim_state = M_STATE_AIM_1; } else if (info.bite && info.distance < M_ATTACK_RANGE_2) { item->goal_anim_state = M_STATE_AIM_2; } else if (info.bite && info.distance < M_WALK_DIST) { item->goal_anim_state = M_STATE_WALK; } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_WALK: creature->maximum_turn = M_WALK_TURN; head = lara_info.angle; if ((item->ai_bits & AI_PATROL_1) != 0) { item->goal_anim_state = M_STATE_WALK; head = 0; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if (creature->mood == MOOD_BORED) { if (Random_GetControl() < M_WAIT_CHANCE) { item->required_anim_state = M_STATE_WAIT; item->goal_anim_state = M_STATE_STOP; } } else if (info.bite && info.distance < M_ATTACK_RANGE_1) { item->goal_anim_state = M_STATE_STOP; } else if (info.bite && info.distance < M_ATTACK_RANGE_3) { item->goal_anim_state = M_STATE_AIM_3; } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_RUN: if (info.ahead) { head = info.angle; } creature->maximum_turn = M_RUN_TURN; tilt = angle / 2; if ((item->ai_bits & AI_GUARD) != 0) { item->goal_anim_state = M_STATE_WAIT; } else if (creature->mood == MOOD_ESCAPE) { if (lara->target != item && info.ahead) { item->goal_anim_state = M_STATE_STOP; } } else if ( (item->ai_bits & AI_FOLLOW) != 0 && (creature->reached_goal || lara_info.distance > M_RUN_DIST)) { item->goal_anim_state = M_STATE_STOP; } else if (creature->mood == MOOD_BORED) { item->goal_anim_state = M_STATE_WALK; } else if (info.ahead && info.distance < M_WALK_DIST) { item->goal_anim_state = M_STATE_WALK; } break; case M_STATE_AIM_1: if (info.ahead) { torso_y = info.angle; torso_x = info.x_angle; } creature->maximum_turn = M_WALK_TURN; creature->flags = 0; if (info.bite && info.distance < M_ATTACK_RANGE_1) { item->goal_anim_state = M_STATE_PUNCH_1; } else { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_AIM_2: if (info.ahead) { torso_y = info.angle; torso_x = info.x_angle; } creature->maximum_turn = M_WALK_TURN; creature->flags = 0; if (info.ahead && info.distance < M_ATTACK_RANGE_2) { item->goal_anim_state = M_STATE_PUNCH_2; } else { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_AIM_3: if (info.ahead) { torso_y = info.angle; torso_x = info.x_angle; } creature->maximum_turn = M_WALK_TURN; creature->flags = 0; if (info.bite && info.distance < M_ATTACK_RANGE_3) { item->goal_anim_state = M_STATE_PUNCH_3; } else { item->goal_anim_state = M_STATE_WALK; } break; case M_STATE_PUNCH_1: case M_STATE_PUNCH_2: if (info.ahead) { torso_y = info.angle; torso_x = info.x_angle; } creature->maximum_turn = M_WALK_TURN; if (creature->flags == 0 && (item->touch_bits & M_TOUCH_BITS) != 0) { M_HitLara(item, M_HIT_DAMAGE); creature->flags = 1; } if (item->current_anim_state == M_STATE_PUNCH_2 && info.ahead && info.distance > M_ATTACK_RANGE_2 && info.distance < M_ATTACK_RANGE_3) { item->goal_anim_state = M_STATE_PUNCH_3; } break; case M_STATE_PUNCH_3: if (info.ahead) { torso_y = info.angle; torso_x = info.x_angle; } creature->maximum_turn = M_WALK_TURN; if (creature->flags != 2 && (item->touch_bits & M_TOUCH_BITS) != 0) { M_HitLara(item, M_SWIPE_DAMAGE); creature->flags = 2; } break; } finish: Creature_Tilt(item, tilt); Creature_Joint(item, 0, torso_y); Creature_Joint(item, 1, torso_x); Creature_Joint(item, 2, head); if (item->current_anim_state >= M_STATE_DEATH) { creature->maximum_turn = 0; Creature_Animate(item_num, angle, 0); } else if (M_Vault(item, angle)) { creature->maximum_turn = 0; } } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->collision_func = Creature_Collision; obj->control_func = M_Control; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->lot_setup = LOT_Setup(LOT_SETUP_CLIMBER); obj->radius = M_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->hit_points = M_HITPOINTS; obj->intelligent = true; obj->save_anim = true; obj->save_flags = true; obj->save_hitpoints = true; obj->save_position = true; Object_GetBone(obj, 6)->rot.x = true; Object_GetBone(obj, 6)->rot.y = true; Object_GetBone(obj, 13)->rot.y = true; } REGISTER_OBJECT(O_PUNK_1, M_Setup) REGISTER_OBJECT(O_PUNK_2, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/raptor.c ================================================ #include #include #include #include #include #include #include // clang-format off #define M_ATTACK_RANGE SQUARE(WALL_L * 3 / 2) // = 2359296 #define M_BITE_DAMAGE 100 #define M_CHARGE_DAMAGE 100 #define M_CLOSE_RANGE SQUARE(g_TRVersion < 3 ? 680 : 585) // = 462400 / 342225 #define M_LUNGE_DAMAGE 100 #define M_LUNGE_RANGE SQUARE(WALL_L * 3 / 2) // = 2359296 #define M_ROAR_CHANCE (g_TRVersion < 3 ? 256 : 128) #define M_RUN_TURN (4 * DEG_1) // = 728 #define M_TOUCH 0xFF7C00 #define M_WALK_TURN (g_TRVersion < 3 ? (1 * DEG_1) : (2 * DEG_1)) // = 182 / 364 #define M_HITPOINTS (g_TRVersion == 3 ? 90 : 20) #define M_RADIUS (WALL_L / 3) // = 341 #define M_PIVOT_LENGTH (g_TRVersion == 3 ? 600 : 400) #define M_SMARTNESS 0x4000 #define M_INFIGHT_CHANCE 0x400 #define M_INFIGHT_RANGE 0x400000 // clang-format on typedef enum { M_STATE_EMPTY, M_STATE_STOP, M_STATE_WALK, M_STATE_RUN, M_STATE_ATTACK_1, M_STATE_DEATH, M_STATE_WARNING, M_STATE_ATTACK_2, M_STATE_ATTACK_3, } M_STATE; typedef enum { M_ANIM_DEATH = 9, } M_ANIM; static BITE m_RaptorBite = { .pos = { 0, 66, 318 }, .mesh_num = 22, }; static void M_CalculateTarget(const ITEM *const item) { CREATURE *const raptor = item->creature_data; int32_t best_distance = INT32_MAX; const ITEM *best_item = nullptr; for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) { const CREATURE *const creature = LOT_GetBaddieSlot(i); if (creature->item_num == NO_ITEM || creature == raptor) { continue; } const ITEM *const candidate = Item_Get(creature->item_num); const XYZ_32 delta = { .x = (candidate->pos.x - item->pos.x) >> 6, .y = (candidate->pos.y - item->pos.y) >> 6, .z = (candidate->pos.z - item->pos.z) >> 6, }; const int32_t distance = XYZ_32_GetLength2(delta); if (distance < best_distance) { best_item = candidate; best_distance = distance; } } if (best_item != nullptr && (best_item->object_id != item->object_id || (Random_GetControl() < M_INFIGHT_CHANCE && best_distance < M_INFIGHT_RANGE))) { raptor->enemy = (ITEM *)best_item; } const ITEM *const lara_item = Lara_GetItem(); const XYZ_32 lara_delta = { .x = (lara_item->pos.x - item->pos.x) >> 6, .y = (lara_item->pos.y - item->pos.y) >> 6, .z = (lara_item->pos.z - item->pos.z) >> 6, }; if (XYZ_32_GetLength2(lara_delta) < best_distance) { raptor->enemy = (ITEM *)lara_item; } } static void M_Attack(ITEM *const item, const AI_INFO *const info) { int32_t damage = 0; int16_t next_state = M_STATE_EMPTY; switch (item->current_anim_state) { case M_STATE_ATTACK_1: damage = M_LUNGE_DAMAGE; next_state = M_STATE_STOP; break; case M_STATE_ATTACK_2: damage = M_CHARGE_DAMAGE; next_state = M_STATE_RUN; break; case M_STATE_ATTACK_3: damage = M_BITE_DAMAGE; next_state = M_STATE_STOP; break; default: return; } if (g_TRVersion < 3) { if (item->required_anim_state == M_STATE_EMPTY && info->ahead && (item->touch_bits & M_TOUCH) != 0) { Creature_Effect(item, &m_RaptorBite, Spawn_Blood); Lara_TakeDamage(damage, true); item->required_anim_state = next_state; } return; } CREATURE *const raptor = item->creature_data; const ITEM *const lara_item = Lara_GetItem(); if (raptor->enemy == lara_item) { if ((raptor->flags & 1) != 0 || (item->touch_bits & M_TOUCH) == 0) { return; } raptor->flags |= 1; Creature_Effect(item, &m_RaptorBite, Spawn_Blood); if (lara_item->hit_points <= 0) { raptor->flags |= 2; } Lara_TakeDamage(damage, true); item->required_anim_state = next_state; return; } if ((raptor->flags & 1) != 0 || raptor->enemy == nullptr || !XYZ_32_IsNearby(raptor->enemy->pos, item->pos, WALL_L / 2)) { return; } raptor->flags |= 1; Creature_Effect(item, &m_RaptorBite, Spawn_Blood); raptor->enemy->hit_points -= damage / 4; raptor->enemy->hit_status = true; if (raptor->enemy->hit_points <= 0) { raptor->flags |= 2; } } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const raptor = item->creature_data; int16_t head = 0; int16_t neck = 0; int16_t angle = 0; int16_t tilt = 0; const bool is_tr3 = g_TRVersion == 3; if (item->hit_points <= 0) { if (item->current_anim_state != M_STATE_DEATH) { item->current_anim_state = M_STATE_DEATH; const int32_t offset = Random_GetControl() > 0x4000 ? 0 : 1; Item_SwitchToAnim(item, M_ANIM_DEATH + offset, 0); } goto finish; } if (is_tr3 && (raptor->enemy == nullptr || (Random_GetControl() & 0x7F) == 0)) { M_CalculateTarget(item); } if (item->ai_bits != 0) { Creature_GetAITarget(raptor); } AI_INFO info; Creature_AIInfo(item, &info); if (info.ahead) { head = info.angle; } Creature_Mood(item, &info, true); if (is_tr3 && raptor->mood == MOOD_BORED) { raptor->maximum_turn /= 2; } angle = Creature_Turn(item, raptor->maximum_turn); neck = angle * -6; const LARA_INFO *const lara = Lara_GetLaraInfo(); const ITEM *const lara_item = Lara_GetItem(); switch (item->current_anim_state) { case M_STATE_STOP: raptor->flags &= ~1; raptor->maximum_turn = 0; if (item->required_anim_state != M_STATE_EMPTY) { item->goal_anim_state = item->required_anim_state; } else if (is_tr3 && (raptor->flags & 2) != 0) { raptor->flags &= ~2; item->goal_anim_state = M_STATE_WARNING; } else if ((item->touch_bits & M_TOUCH) != 0) { item->goal_anim_state = M_STATE_ATTACK_3; } else if (info.bite && info.distance < M_CLOSE_RANGE) { item->goal_anim_state = M_STATE_ATTACK_3; } else if (info.bite && info.distance < M_LUNGE_RANGE) { item->goal_anim_state = M_STATE_ATTACK_1; } else if ( is_tr3 && raptor->mood == MOOD_ESCAPE && lara->target != item && info.ahead && !item->hit_status) { item->goal_anim_state = M_STATE_STOP; } else if (raptor->mood == MOOD_BORED) { item->goal_anim_state = M_STATE_WALK; } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_WALK: raptor->flags &= ~1; raptor->maximum_turn = M_WALK_TURN; if (raptor->mood != MOOD_BORED) { item->goal_anim_state = M_STATE_STOP; } else if (info.ahead && Random_GetControl() < M_ROAR_CHANCE) { item->required_anim_state = M_STATE_WARNING; item->goal_anim_state = M_STATE_STOP; raptor->flags &= ~2; } break; case M_STATE_RUN: tilt = angle; raptor->flags &= ~1; raptor->maximum_turn = M_RUN_TURN; if ((item->touch_bits & M_TOUCH) != 0) { item->goal_anim_state = M_STATE_STOP; } else if (is_tr3 && (raptor->flags & 2) != 0) { item->goal_anim_state = M_STATE_STOP; item->required_anim_state = M_STATE_WARNING; raptor->flags &= ~2; } else if (info.bite && info.distance < M_ATTACK_RANGE) { if (item->goal_anim_state == M_STATE_RUN) { if (Random_GetControl() < 0x2000) { item->goal_anim_state = M_STATE_STOP; } else { item->goal_anim_state = M_STATE_ATTACK_2; } } } else if ( info.ahead && raptor->mood != MOOD_ESCAPE && Random_GetControl() < M_ROAR_CHANCE && (!is_tr3 || (raptor->enemy != nullptr && raptor->enemy->object_id != O_ANIMATING_6))) { item->required_anim_state = M_STATE_WARNING; item->goal_anim_state = M_STATE_STOP; } else if (raptor->mood == MOOD_BORED) { item->goal_anim_state = M_STATE_STOP; } else if ( is_tr3 && raptor->mood == MOOD_ESCAPE && lara->target != item && info.ahead) { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_ATTACK_1: case M_STATE_ATTACK_2: case M_STATE_ATTACK_3: raptor->maximum_turn = M_WALK_TURN; tilt = angle; M_Attack(item, &info); break; default: break; } finish: Creature_Tilt(item, tilt); if (is_tr3) { Creature_Joint(item, 0, head / 2); Creature_Joint(item, 1, head / 2); Creature_Joint(item, 2, neck); Creature_Joint(item, 3, neck); } else { Creature_Head(item, head); } Creature_Animate(item_num, angle, tilt); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = Creature_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->shadow_size = UNIT_SHADOW / 2; obj->hit_points = M_HITPOINTS; obj->pivot_length = M_PIVOT_LENGTH; obj->radius = M_RADIUS; obj->smartness = M_SMARTNESS; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_anim = true; obj->save_flags = true; Object_GetBone(obj, 21)->rot.y = true; if (g_TRVersion >= 3) { Object_GetBone(obj, 20)->rot.y = true; Object_GetBone(obj, 23)->rot.y = true; Object_GetBone(obj, 25)->rot.y = true; } } REGISTER_OBJECT(O_RAPTOR, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/rat.c ================================================ #include #include #include #include #include #include #include #define RAT_BITE_DAMAGE 20 #define RAT_CHARGE_DAMAGE 20 #define RAT_TOUCH 0x300018F #define RAT_DIE_ANIM 8 #define RAT_RUN_TURN (DEG_1 * 6) // = 1092 #define RAT_BITE_RANGE SQUARE(341) // = 116281 #define RAT_CHARGE_RANGE SQUARE(WALL_L * 3 / 2) // = 2359296 #define RAT_POSE_CHANCE 256 #define RAT_HITPOINTS 5 #define RAT_RADIUS (WALL_L / 5) // = 204 #define RAT_SMARTNESS 0x2000 #define VOLE_DIE_ANIM 2 #define VOLE_SWIM_TURN (DEG_1 * 3) // = 546 #define VOLE_ATTACK_RANGE SQUARE(300) // = 90000 typedef enum { RAT_STATE_EMPTY = 0, RAT_STATE_STOP = 1, RAT_STATE_ATTACK_2 = 2, RAT_STATE_RUN = 3, RAT_STATE_ATTACK_1 = 4, RAT_STATE_DEATH = 5, RAT_STATE_POSE = 6, } RAT_STATE; typedef enum { VOLE_STATE_EMPTY = 0, VOLE_STATE_SWIM = 1, VOLE_STATE_ATTACK = 2, VOLE_STATE_DEATH = 3, } VOLE_STATE; static BITE m_RatBite = { .pos = { 0, -11, 108 }, .mesh_num = 3 }; static const HYBRID_INFO m_RatInfo = { .land.id = O_RAT, .land.active_anim = RAT_STATE_EMPTY, .land.death_anim = RAT_DIE_ANIM, .land.death_state = RAT_STATE_DEATH, .water.id = O_VOLE, .water.active_anim = VOLE_STATE_EMPTY, .water.death_anim = VOLE_DIE_ANIM, .water.death_state = VOLE_STATE_DEATH, }; static void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage) { if (stage == SAVEGAME_STAGE_AFTER_LOAD) { Creature_EnsureHabitat(Item_GetIndex(item), nullptr, &m_RatInfo); } } static void M_ControlRat(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const rat = item->creature_data; int16_t head = 0; int16_t angle = 0; if (item->hit_points <= 0) { if (item->current_anim_state != RAT_STATE_DEATH) { item->current_anim_state = RAT_STATE_DEATH; Item_SwitchToAnim(item, RAT_DIE_ANIM, 0); } } else { AI_INFO info; Creature_AIInfo(item, &info); if (info.ahead) { head = info.angle; } Creature_Mood(item, &info, false); angle = Creature_Turn(item, RAT_RUN_TURN); switch (item->current_anim_state) { case RAT_STATE_STOP: if (item->required_anim_state) { item->goal_anim_state = item->required_anim_state; } else if (info.bite && info.distance < RAT_BITE_RANGE) { item->goal_anim_state = RAT_STATE_ATTACK_1; } else { item->goal_anim_state = RAT_STATE_RUN; } break; case RAT_STATE_RUN: if (info.ahead && (item->touch_bits & RAT_TOUCH)) { item->goal_anim_state = RAT_STATE_STOP; } else if (info.bite && info.distance < RAT_CHARGE_RANGE) { item->goal_anim_state = RAT_STATE_ATTACK_2; } else if (info.ahead && Random_GetControl() < RAT_POSE_CHANCE) { item->required_anim_state = RAT_STATE_POSE; item->goal_anim_state = RAT_STATE_STOP; } break; case RAT_STATE_ATTACK_1: if (item->required_anim_state == RAT_STATE_EMPTY && info.ahead && (item->touch_bits & RAT_TOUCH)) { Creature_Effect(item, &m_RatBite, Spawn_Blood); Lara_TakeDamage(RAT_BITE_DAMAGE, true); item->required_anim_state = RAT_STATE_STOP; } break; case RAT_STATE_ATTACK_2: if (item->required_anim_state == RAT_STATE_EMPTY && info.ahead && (item->touch_bits & RAT_TOUCH)) { Creature_Effect(item, &m_RatBite, Spawn_Blood); Lara_TakeDamage(RAT_CHARGE_DAMAGE, true); item->required_anim_state = RAT_STATE_RUN; } break; case RAT_STATE_POSE: if (rat->mood != MOOD_BORED || Random_GetControl() < RAT_POSE_CHANCE) { item->goal_anim_state = RAT_STATE_STOP; } break; } } Creature_Head(item, head); Creature_EnsureHabitat(item_num, nullptr, &m_RatInfo); Creature_Animate(item_num, angle, 0); } static void M_ControlVole(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); int16_t head = 0; int16_t angle = 0; if (item->hit_points <= 0) { if (item->current_anim_state != VOLE_STATE_DEATH) { item->current_anim_state = VOLE_STATE_DEATH; Item_SwitchToAnim(item, VOLE_DIE_ANIM, 0); Carrier_TestItemDrops(item_num); } Creature_Head(item, head); Item_Animate(item); Creature_EnsureHabitat(item_num, nullptr, &m_RatInfo); } else { AI_INFO info; Creature_AIInfo(item, &info); if (info.ahead) { head = info.angle; } Creature_Mood(item, &info, true); angle = Creature_Turn(item, VOLE_SWIM_TURN); switch (item->current_anim_state) { case VOLE_STATE_SWIM: if (info.ahead && (item->touch_bits & RAT_TOUCH)) { item->goal_anim_state = VOLE_STATE_ATTACK; } break; case VOLE_STATE_ATTACK: if (item->required_anim_state == VOLE_STATE_EMPTY && info.ahead && (item->touch_bits & RAT_TOUCH)) { Creature_Effect(item, &m_RatBite, Spawn_Blood); Lara_TakeDamage(RAT_BITE_DAMAGE, true); item->required_anim_state = VOLE_STATE_SWIM; } item->goal_anim_state = VOLE_STATE_EMPTY; break; } Creature_Head(item, head); int32_t wh; Creature_EnsureHabitat(item_num, &wh, &m_RatInfo); int32_t height = item->pos.y; item->pos.y = item->floor; Creature_Animate(item_num, angle, 0); if (height != NO_HEIGHT) { if (wh - height < -STEP_L / 8) { item->pos.y = height - STEP_L / 8; } else if (wh - height > STEP_L / 8) { item->pos.y = height + STEP_L / 8; } else { item->pos.y = wh; } } } } static void M_SetupBase(OBJECT *const obj) { obj->initialise_func = Creature_Initialise; obj->collision_func = Creature_Collision; obj->shadow_size = UNIT_SHADOW / 2; obj->hit_points = RAT_HITPOINTS; obj->pivot_length = 200; obj->radius = RAT_RADIUS; obj->smartness = RAT_SMARTNESS; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_anim = true; obj->save_flags = true; obj->handle_save_func = M_HandleSave; Object_GetBone(obj, 1)->rot.y = true; } static void M_SetupRat(OBJECT *const obj) { if (!obj->loaded) { return; } M_SetupBase(obj); obj->control_func = M_ControlRat; } static void M_SetupVole(OBJECT *const obj) { if (!obj->loaded) { return; } M_SetupBase(obj); obj->control_func = M_ControlVole; } REGISTER_OBJECT(O_RAT, M_SetupRat) REGISTER_OBJECT(O_VOLE, M_SetupVole) ================================================ FILE: src/trx/game/objects/creatures/rx_worker_1.c ================================================ #include #include #include #include #include #include // clang-format off #define M_RADIUS (WALL_L / 10) // = 102 #define M_HIT_POINTS 34 #define M_DAMAGE 35 #define M_ALERT_DIST SQUARE(WALL_L) // = 1048576 #define M_WALK_DIST SQUARE(WALL_L * 2) // = 4194304 #define M_WALK_TURN (DEG_1 * 6) // = 1092 #define M_RUN_TURN (DEG_1 * 10) // = 1820 #define M_DUCK_TURN DEG_1 // = 182 #define M_SHOOT_1_CHANCE 0x2000 #define M_SHOOT_2_CHANCE 0x4000 #define M_DUCK_CHANCE 0x3 #define M_DUCK_END_CHANCE 0x1F // clang-format on typedef enum { M_STATE_NULL, M_STATE_WAIT, M_STATE_WALK, M_STATE_RUN, M_STATE_AIM_1, M_STATE_SHOOT_1, M_STATE_AIM_2, M_STATE_SHOOT_2, M_STATE_SHOOT_3A, M_STATE_SHOOT_3B, M_STATE_SHOOT_4A, M_STATE_AIM_3, M_STATE_AIM_4, M_STATE_DEATH, M_STATE_SHOOT_4B, M_STATE_DUCK_START, M_STATE_DUCKED, M_STATE_DUCK_AIM, M_STATE_DUCK_SHOOT, M_STATE_DUCK_WALK, M_STATE_DUCK_END, } M_STATE; typedef enum { // clang-format off M_ANIM_AIM_1 = 1, M_ANIM_SHOOT_1 = 12, M_ANIM_DEATH = 14, M_ANIM_WALK_STOP = 17, M_ANIM_AIM_4A = 18, M_ANIM_AIM_4B = 19, M_ANIM_RUN_STOP_1 = 27, M_ANIM_RUN_STOP_2 = 28, // clang-format on } M_ANIM; static const CREATURE_GUN m_Gun = { .muzzle = { .pos = { 0, 160, 40 }, .mesh_num = 13 }, .tr3_enemy_flash = true, .tr3_flash = { .pos = { 0, 192, 40 }, .mesh_num = 13 }, .tr3_enemy_weapon_flags = 0, .tr3_flash_shade = 600, .tr3_flash_rot_x = -DEG_90, }; static void M_FireFinalShot( ITEM *const item, int16_t *const head, int16_t *const torso_y) { if (!Item_TestFrameEqual(item, 47)) { return; } AI_INFO info = {}; Creature_AIInfo(item, &info); if (!Creature_CanTargetEnemy(item, &info) || ABS(info.angle) >= DEG_45) { return; } *head = info.angle; *torso_y = info.angle; Creature_Shoot(item, &info, &m_Gun, info.angle, M_DAMAGE * 3); Sound_Effect(SFX_LONDON_SWAT_FIRE, &item->pos, SPM_NORMAL); } static bool M_IsNearCover(const ITEM *const item, const AI_INFO *const info) { const XYZ_32 pos = XYZ_32_OffsetYaw(item->pos, item->rot.y + info->angle, WALL_L); int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); const int32_t height = Room_GetHeight(sector, pos); return item->pos.y > height + STEPUP_HEIGHT && item->pos.y < height + STEPUP_HEIGHT * 3 && info->distance > M_ALERT_DIST; } static bool M_ShouldDuck(const ITEM *const item, const bool near_cover) { return item->hit_status && (Random_GetControl() & M_DUCK_CHANCE) == 0 && near_cover; } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t angle = 0; int16_t head = 0; int16_t tilt = 0; int16_t torso_x = 0; int16_t torso_y = 0; if (item->hit_points <= 0) { item->hit_points = 0; if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, M_ANIM_DEATH, 0); item->current_anim_state = M_STATE_DEATH; } else { M_FireFinalShot(item, &head, &torso_y); } goto finish; } ITEM *const lara_item = Lara_GetItem(); if (item->ai_bits != 0) { Creature_GetAITarget(creature); } else { creature->enemy = lara_item; } AI_INFO info = {}; Creature_AIInfo(item, &info); AI_INFO lara_info = {}; if (creature->enemy == lara_item) { lara_info.distance = info.distance; lara_info.angle = info.angle; } else { const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; lara_info.angle = Math_Atan(dz, dx) - item->rot.y; lara_info.distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz }); } Creature_Mood(item, &info, creature->enemy != lara_item); angle = Creature_Turn(item, creature->maximum_turn); const bool near_cover = M_IsNearCover(item, &lara_info); ITEM *const enemy = creature->enemy; creature->enemy = lara_item; if ((lara_info.distance < M_ALERT_DIST || item->hit_status || Creature_CanSeeEnemy(item, &lara_info)) && (item->ai_bits & AI_FOLLOW) == 0) { if (!creature->alerted) { Sound_Effect(SFX_AMERICAN_HOY, &item->pos, SPM_NORMAL); } Creature_AlertAllGuards(item_num); } creature->enemy = enemy; const int16_t anim_idx = Item_GetRelativeAnim(item); const int16_t frame_idx = Item_GetRelativeFrame(item); const LARA_INFO *const lara = Lara_GetLaraInfo(); switch (item->current_anim_state) { case M_STATE_WAIT: head = lara_info.angle; creature->maximum_turn = 0; if (anim_idx == M_ANIM_WALK_STOP || anim_idx == M_ANIM_RUN_STOP_1 || anim_idx == M_ANIM_RUN_STOP_2) { if (ABS(info.angle) < M_RUN_TURN) { item->rot.y += info.angle; } else if (info.angle < 0) { item->rot.y -= M_RUN_TURN; } else { item->rot.y += M_RUN_TURN; } } if ((item->ai_bits & AI_GUARD) != 0) { head = Creature_AIGuard(creature); item->goal_anim_state = M_STATE_WAIT; } else if ((item->ai_bits & AI_PATROL_1) != 0) { item->goal_anim_state = M_STATE_WALK; } else if (near_cover && (lara->target == item || item->hit_status)) { item->goal_anim_state = M_STATE_DUCK_START; } else if (item->required_anim_state == M_STATE_DUCK_START) { item->goal_anim_state = M_STATE_DUCK_START; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if (Creature_CanTargetEnemy(item, &info)) { if (info.distance > M_WALK_DIST) { item->goal_anim_state = M_STATE_WALK; } else { const int32_t rnd = Random_GetControl(); if (rnd < M_SHOOT_1_CHANCE) { item->goal_anim_state = M_STATE_SHOOT_1; } else if (rnd < M_SHOOT_2_CHANCE) { item->goal_anim_state = M_STATE_SHOOT_2; } else { item->goal_anim_state = M_STATE_AIM_3; } } } else if ( creature->mood == MOOD_BORED || ((item->ai_bits & AI_FOLLOW) != 0 && (creature->reached_goal || lara_info.distance > M_WALK_DIST))) { if (info.ahead) { item->goal_anim_state = M_STATE_WAIT; } else { item->goal_anim_state = M_STATE_WALK; } } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_WALK: head = lara_info.angle; creature->maximum_turn = M_WALK_TURN; if ((item->ai_bits & AI_PATROL_1) != 0) { item->goal_anim_state = M_STATE_WALK; head = 0; } else if (near_cover && (lara->target == item || item->hit_status)) { item->required_anim_state = M_STATE_DUCK_START; item->goal_anim_state = M_STATE_WAIT; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if (Creature_CanTargetEnemy(item, &info)) { if (info.distance > M_WALK_DIST && info.zone_num == info.enemy_zone_num) { item->goal_anim_state = M_STATE_AIM_4; } else { item->goal_anim_state = M_STATE_WAIT; } } else if (creature->mood == MOOD_BORED) { if (info.ahead) { item->goal_anim_state = M_STATE_WALK; } else { item->goal_anim_state = M_STATE_WAIT; } } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_RUN: if (info.ahead) { head = info.angle; } creature->maximum_turn = M_RUN_TURN; tilt = angle / 2; if ((item->ai_bits & AI_GUARD) != 0) { item->goal_anim_state = M_STATE_WAIT; } else if (near_cover && (lara->target == item || item->hit_status)) { item->required_anim_state = M_STATE_DUCK_START; item->goal_anim_state = M_STATE_WAIT; } else if (creature->mood != MOOD_ESCAPE) { if (Creature_CanTargetEnemy(item, &info) || ((item->ai_bits & AI_FOLLOW) != 0 && (creature->reached_goal || lara_info.distance > M_WALK_DIST))) { item->goal_anim_state = M_STATE_WAIT; } else if (creature->mood == MOOD_BORED) { item->goal_anim_state = M_STATE_WALK; } } break; case M_STATE_AIM_1: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } if (anim_idx == M_ANIM_SHOOT_1 || (anim_idx == M_ANIM_AIM_1 && frame_idx == 10)) { if (!Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE)) { item->required_anim_state = M_STATE_WAIT; } } else if (M_ShouldDuck(item, near_cover)) { item->required_anim_state = M_STATE_DUCK_START; item->goal_anim_state = M_STATE_WAIT; } break; case M_STATE_SHOOT_1: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } if (item->required_anim_state == M_STATE_WAIT) { item->goal_anim_state = M_STATE_WAIT; } break; case M_STATE_SHOOT_2: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } if (frame_idx == 0) { if (!Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE)) { item->goal_anim_state = M_STATE_WAIT; } } else if (M_ShouldDuck(item, near_cover)) { item->required_anim_state = M_STATE_DUCK_START; item->goal_anim_state = M_STATE_WAIT; } break; case M_STATE_SHOOT_3A: case M_STATE_SHOOT_3B: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } if (frame_idx == 0 || frame_idx == 11) { if (!Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE)) { item->goal_anim_state = M_STATE_WAIT; } } else if (M_ShouldDuck(item, near_cover)) { item->required_anim_state = M_STATE_DUCK_START; item->goal_anim_state = M_STATE_WAIT; } break; case M_STATE_AIM_4: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } if ((anim_idx == M_ANIM_AIM_4A && frame_idx == 17) || (anim_idx == M_ANIM_AIM_4B && frame_idx == 6)) { if (!Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE)) { item->required_anim_state = M_STATE_WALK; } } else if (M_ShouldDuck(item, near_cover)) { item->required_anim_state = M_STATE_DUCK_START; item->goal_anim_state = M_STATE_WAIT; } if (info.distance < M_WALK_DIST) { item->required_anim_state = M_STATE_WALK; } break; case M_STATE_SHOOT_4A: case M_STATE_SHOOT_4B: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } if (item->required_anim_state == M_STATE_WALK) { item->goal_anim_state = M_STATE_WALK; } if (frame_idx == 16 && !Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE)) { item->goal_anim_state = M_STATE_WALK; } if (info.distance < M_WALK_DIST) { item->goal_anim_state = M_STATE_WALK; } break; case M_STATE_DUCKED: if (info.ahead) { head = info.angle; } creature->maximum_turn = 0; if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = M_STATE_DUCK_AIM; } else if ( item->hit_status || !near_cover || (info.ahead && (Random_GetControl() & M_DUCK_END_CHANCE) == 0)) { item->goal_anim_state = M_STATE_DUCK_END; } else { item->goal_anim_state = M_STATE_DUCK_WALK; } break; case M_STATE_DUCK_AIM: if (info.ahead) { torso_y = info.angle; } creature->maximum_turn = M_DUCK_TURN; if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = M_STATE_DUCK_SHOOT; } else { item->goal_anim_state = M_STATE_DUCKED; } break; case M_STATE_DUCK_SHOOT: if (info.ahead) { torso_y = info.angle; } if (frame_idx == 0) { if (!Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE) || (Random_GetControl() & 7) == 0) { item->goal_anim_state = M_STATE_DUCKED; } } break; case M_STATE_DUCK_WALK: if (info.ahead) { head = info.angle; } creature->maximum_turn = M_WALK_TURN; if (Creature_CanTargetEnemy(item, &info) || item->hit_status || !near_cover || (info.ahead && (Random_GetControl() & M_DUCK_END_CHANCE) == 0)) { item->goal_anim_state = M_STATE_DUCKED; } break; case M_STATE_DUCK_END: if (ABS(info.angle) < M_WALK_TURN) { item->rot.y += info.angle; } else if (info.angle < 0) { item->rot.y -= M_WALK_TURN; } else { item->rot.y += M_WALK_TURN; } break; } finish: CLAMP(torso_y, -DEG_45, DEG_45); Creature_Tilt(item, tilt); Creature_Joint(item, 0, torso_y); Creature_Joint(item, 1, torso_x); Creature_Joint(item, 2, head); Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->collision_func = Creature_Collision; obj->control_func = M_Control; obj->shadow_size = UNIT_SHADOW / 2; obj->radius = M_RADIUS; obj->hit_points = M_HIT_POINTS; obj->intelligent = true; obj->save_anim = true; obj->save_flags = true; obj->save_hitpoints = true; obj->save_position = true; Object_GetBone(obj, 6)->rot.x = true; Object_GetBone(obj, 6)->rot.y = true; Object_GetBone(obj, 13)->rot.y = true; } REGISTER_OBJECT(O_RX_WORKER_1, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/rx_worker_2.c ================================================ #include #include #include #include #include // clang-format off #define M_RADIUS (WALL_L / 10) // = 102 #define M_HIT_POINTS 30 #define M_DAMAGE 28 #define M_ALERT_DIST SQUARE(WALL_L) // = 1048576 #define M_RUN_DIST SQUARE(WALL_L * 2) // = 4194304 #define M_SHOOT_DIST SQUARE(WALL_L * 3) // = 9437184 #define M_RUN_TURN (DEG_1 * 10) // = 1820 #define M_WALK_TURN (DEG_1 * 5) // = 910 // clang-format on typedef enum { M_STATE_NULL, M_STATE_STOP, M_STATE_WALK, M_STATE_RUN, M_STATE_WAIT, M_STATE_SHOOT_1, M_STATE_SHOOT_2, M_STATE_DEATH, M_STATE_AIM_1, M_STATE_AIM_2, M_STATE_AIM_3, M_STATE_SHOOT_3, } M_STATE; typedef enum { // clang-format off M_ANIM_STOP = 12, M_ANIM_WALK_TO_STOP = 17, M_ANIM_DEATH = 19, // clang-format on } M_ANIM; static const CREATURE_GUN m_Gun = { .muzzle = { .pos = { 0, 400, 64 }, .mesh_num = 7 }, .tr3_enemy_flash = true, .tr3_flash = { .pos = { 0, 400, 64 }, .mesh_num = 7 }, .tr3_enemy_weapon_flags = 1, .tr3_flash_shade = 600, .tr3_flash_rot_x = -DEG_90, }; static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); Creature_Initialise(item_num); Item_SwitchToAnim(item, M_ANIM_STOP, 0); item->current_anim_state = M_STATE_STOP; item->goal_anim_state = M_STATE_STOP; } static void M_FireFinalShot( ITEM *const item, int16_t *const head, int16_t *const torso_y) { const int16_t frame_idx = Item_GetRelativeFrame(item); if (frame_idx <= 3 || frame_idx >= 31 || (item->frame_num & 3) != 0) { return; } AI_INFO info = {}; Creature_AIInfo(item, &info); *head = info.angle; *torso_y = info.angle; Creature_Shoot(item, &info, &m_Gun, 0, 0); Sound_Effect(SFX_LONDON_SWAT_FIRE, &item->pos, SPM_NORMAL); } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t tilt = 0; int16_t angle = 0; int16_t head = 0; int16_t torso_x = 0; int16_t torso_y = 0; if (item->hit_points <= 0) { if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, M_ANIM_DEATH, 0); item->current_anim_state = M_STATE_DEATH; creature->flags = (Random_GetControl() & 3) == 0 ? 1 : 0; } else if (creature->flags != 0) { M_FireFinalShot(item, &head, &torso_y); } goto finish; } ITEM *const lara_item = Lara_GetItem(); if (item->ai_bits != 0) { Creature_GetAITarget(creature); } else { creature->enemy = lara_item; } AI_INFO info = {}; Creature_AIInfo(item, &info); AI_INFO lara_info = {}; if (creature->enemy == lara_item) { lara_info.distance = info.distance; lara_info.angle = info.angle; } else { const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; lara_info.angle = Math_Atan(dz, dx) - item->rot.y; lara_info.distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz }); } Creature_Mood(item, &info, creature->enemy != lara_item); angle = Creature_Turn(item, creature->maximum_turn); ITEM *const enemy = creature->enemy; creature->enemy = lara_item; if ((lara_info.distance < M_ALERT_DIST || item->hit_status || Creature_CanSeeEnemy(item, &lara_info)) && (item->ai_bits & AI_FOLLOW) == 0) { if (!creature->alerted) { Sound_Effect(SFX_AMERICAN_HOY, &item->pos, SPM_NORMAL); } Creature_AlertAllGuards(item_num); } creature->enemy = enemy; const LARA_INFO *const lara = Lara_GetLaraInfo(); switch (item->current_anim_state) { case M_STATE_STOP: head = lara_info.angle; creature->flags = 0; creature->maximum_turn = 0; if (Item_TestAnimEqual(item, M_ANIM_WALK_TO_STOP)) { if (ABS(info.angle) < M_RUN_TURN) { item->rot.y += info.angle; } else if (info.angle < 0) { item->rot.y -= M_RUN_TURN; } else { item->rot.y += M_RUN_TURN; } } if ((item->ai_bits & AI_GUARD) != 0) { head = Creature_AIGuard(creature); if ((Random_GetControl() & 0xFF) == 0) { if (item->current_anim_state == M_STATE_STOP) { item->goal_anim_state = M_STATE_WAIT; } else { item->goal_anim_state = M_STATE_STOP; } } } else if ((item->ai_bits & AI_PATROL_1) != 0) { item->goal_anim_state = M_STATE_WALK; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if (Creature_CanTargetEnemy(item, &info)) { if (info.distance >= M_SHOOT_DIST && info.zone_num == info.enemy_zone_num) { item->goal_anim_state = M_STATE_WALK; } else if (Random_GetControl() < 0x4000) { item->goal_anim_state = M_STATE_AIM_1; } else { item->goal_anim_state = M_STATE_AIM_3; } } else if ( creature->mood == MOOD_BORED || ((item->ai_bits & AI_FOLLOW) != 0 && (creature->reached_goal || lara_info.distance > M_RUN_DIST))) { item->goal_anim_state = M_STATE_STOP; } else if (creature->mood != MOOD_BORED && info.distance > M_RUN_DIST) { item->goal_anim_state = M_STATE_RUN; } else { item->goal_anim_state = M_STATE_WALK; } break; case M_STATE_WALK: head = lara_info.angle; creature->flags = 0; creature->maximum_turn = M_WALK_TURN; if ((item->ai_bits & AI_PATROL_1) != 0) { item->goal_anim_state = M_STATE_WALK; head = 0; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if ( (item->ai_bits & AI_GUARD) != 0 || ((item->ai_bits & AI_FOLLOW) != 0 && (creature->reached_goal || lara_info.distance > M_RUN_DIST))) { item->goal_anim_state = M_STATE_STOP; } else if (Creature_CanTargetEnemy(item, &info)) { if (info.distance < M_SHOOT_DIST || info.zone_num != info.enemy_zone_num) { item->goal_anim_state = M_STATE_STOP; } else { item->goal_anim_state = M_STATE_AIM_2; } } else if (creature->mood == MOOD_BORED) { if (info.ahead) { item->goal_anim_state = M_STATE_STOP; } } else if (info.distance > M_RUN_DIST) { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_RUN: if (info.ahead) { head = info.angle; } creature->maximum_turn = M_RUN_TURN; tilt = angle / 2; if ((item->ai_bits & AI_GUARD) != 0 || ((item->ai_bits & AI_FOLLOW) != 0 && (creature->reached_goal || lara_info.distance > M_RUN_DIST))) { item->goal_anim_state = M_STATE_WALK; } else if (creature->mood != MOOD_ESCAPE) { if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = M_STATE_WALK; } else if ( creature->mood == MOOD_BORED || (creature->mood == MOOD_STALK && (item->ai_bits & AI_FOLLOW) == 0 && info.distance < M_RUN_DIST)) { item->goal_anim_state = M_STATE_WALK; } } break; case M_STATE_WAIT: head = lara_info.angle; creature->flags = 0; creature->maximum_turn = 0; if ((item->ai_bits & AI_GUARD) != 0) { head = Creature_AIGuard(creature); if ((Random_GetControl() & 0xFF) == 0) { if (item->current_anim_state == M_STATE_STOP) { item->goal_anim_state = M_STATE_WAIT; } else { item->goal_anim_state = M_STATE_STOP; } } } else if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = M_STATE_SHOOT_1; } else if (creature->mood != MOOD_BORED || !info.ahead) { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_SHOOT_1: case M_STATE_SHOOT_2: case M_STATE_SHOOT_3: if (item->current_anim_state == M_STATE_SHOOT_3 && item->goal_anim_state != M_STATE_STOP && (creature->mood == MOOD_ESCAPE || info.distance > M_SHOOT_DIST || !Creature_CanTargetEnemy(item, &info))) { item->goal_anim_state = M_STATE_STOP; } if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } if (creature->flags != 0) { creature->flags--; } else { Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE); creature->flags = 5; } break; case M_STATE_AIM_1: case M_STATE_AIM_3: creature->flags = 0; if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = item->current_anim_state == M_STATE_AIM_1 ? M_STATE_SHOOT_1 : M_STATE_SHOOT_3; } else { item->goal_anim_state = M_STATE_STOP; } } break; case M_STATE_AIM_2: creature->flags = 0; if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = M_STATE_SHOOT_2; } else { item->goal_anim_state = M_STATE_WALK; } } break; } finish: Creature_Tilt(item, tilt); Creature_Joint(item, 0, torso_y); Creature_Joint(item, 1, torso_x); Creature_Joint(item, 2, head); Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->collision_func = Creature_Collision; obj->control_func = M_Control; obj->shadow_size = UNIT_SHADOW / 2; obj->hit_points = M_HIT_POINTS; obj->radius = M_RADIUS; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 0)->rot.x = true; Object_GetBone(obj, 0)->rot.y = true; Object_GetBone(obj, 7)->rot.y = true; } REGISTER_OBJECT(O_RX_WORKER_2, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/rx_worker_3.c ================================================ #include #include #include #include #include #include #include #include #include // clang-format off #define M_RADIUS (WALL_L / 10) // = 102 #define M_HIT_POINTS 36 #define M_MAX_FLAME_SPEED 40 #define M_ALERT_DIST SQUARE(WALL_L) // = 1048576 #define M_ATTACK_DIST SQUARE(WALL_L * 4) // = 16777216 #define M_WALK_TURN (DEG_1 * 5) // = 910 // clang-format on typedef enum { M_STATE_NULL, M_STATE_STOP, M_STATE_WALK, M_STATE_RUN, M_STATE_WAIT, M_STATE_SHOOT_1, M_STATE_SHOOT_2, M_STATE_DEATH, M_STATE_AIM_1, M_STATE_AIM_2, M_STATE_AIM_3, M_STATE_SHOOT_3, } M_STATE; typedef enum { // clang-format off M_ANIM_STOP = 12, M_ANIM_DEATH = 19, // clang-format on } M_ANIM; static const BITE m_Gun = { .pos = { 0, 340, 64 }, .mesh_num = 7, }; static void M_TriggerPilotFlame(const ITEM *const item) { const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; if (dx < -0x4000 || dx > 0x4000 || dz < -0x4000 || dz > 0x4000) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = (Random_GetControl() & 0x1F) + 48; spark->src_color.g = spark->src_color.r; spark->src_color.b = (Random_GetControl() & 0x3F) - 64; spark->dst_color.r = (Random_GetControl() & 0x3F) - 64; spark->dst_color.g = (Random_GetControl() & 0x3F) + 0x80; spark->dst_color.b = 32; spark->fade_to_black = 4; spark->col_fade_speed = (Random_GetControl() & 3) + 12; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->life = (Random_GetControl() & 3) + 20; spark->s_life = spark->life; spark->dynamic = -1; spark->pos.x = (Random_GetControl() & 0x1F) - 16; spark->pos.y = (Random_GetControl() & 0x1F) - 16; spark->pos.z = (Random_GetControl() & 0x1F) - 16; spark->vel.x = (Random_GetControl() & 0x1F) - 16; spark->vel.y = -(Random_GetControl() & 3); spark->vel.z = (Random_GetControl() & 0x1F) - 16; spark->flags = SPARK_F_ATTACHED_NODE | SPARK_F_ALT_SPRITE | SPARK_F_ITEM | SPARK_F_SPRITE | SPARK_F_SCALE; spark->effect_num = Item_GetIndex(item); spark->node_num = 0; spark->friction = 4; spark->gravity = -2 - (Random_GetControl() & 3); spark->max_y_vel = -4 - (Random_GetControl() & 3); spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->scalar = 0; spark->dst_size.width = (Random_GetControl() & 7) + 32; spark->src_size.width = spark->dst_size.width >> 1; spark->size.width = spark->src_size.width; spark->src_size.height = spark->src_size.width; spark->size.height = spark->src_size.width; spark->dst_size.height = spark->dst_size.width; Sparks_FinishSetup(spark); } static void M_TriggerFlameSparks( const XYZ_32 pos, const XYZ_32 vel, const int16_t effect_num) { SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = (Random_GetControl() & 0x1F) + 48; spark->src_color.g = spark->src_color.r; spark->src_color.b = (Random_GetControl() & 0x3F) - 64; spark->dst_color.r = (Random_GetControl() & 0x3F) - 64; spark->dst_color.g = (Random_GetControl() & 0x3F) + 0x80; spark->dst_color.b = 32; if (vel.x != 0 || vel.y != 0 || vel.z != 0) { spark->col_fade_speed = 6; spark->fade_to_black = 2; spark->life = (Random_GetControl() & 1) + 12; } else { spark->col_fade_speed = 8; spark->fade_to_black = 16; spark->life = (Random_GetControl() & 3) + 20; } spark->s_life = spark->life; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16; spark->pos.y = pos.y; spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16; spark->vel.x = vel.x + (Random_GetControl() & 0xF) - 16; spark->vel.y = vel.y; spark->vel.z = vel.z + (Random_GetControl() & 0xF) - 16; spark->friction = 0; if ((Random_GetControl() & 1) != 0) { if (effect_num < 0) { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->rot_angle = Random_GetControl() & 0xFFF; if ((Random_GetControl() & 1) != 0) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = +16 + (Random_GetControl() & 0xF); } } else if (effect_num < 0) { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE; } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->max_y_vel = 0; spark->effect_num = effect_num; spark->gravity = 0; spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; const int32_t size = (Random_GetControl() & 0x1F) + 64; if (vel.x != 0 || vel.y != 0 || vel.z != 0) { spark->size.width = size >> 5; spark->src_size.width = size >> 5; spark->size.height = size >> 5; spark->src_size.height = size >> 5; if (effect_num == -2) { spark->scalar = 2; } else { spark->scalar = 3; } spark->dst_size.width = size >> 1; spark->dst_size.height = size >> 1; } else { spark->scalar = 4; spark->size.width = size >> 4; spark->src_size.width = size >> 4; spark->size.height = size >> 4; spark->src_size.height = size >> 4; spark->dst_size.width = size >> 1; spark->dst_size.height = size >> 1; } Sparks_FinishSetup(spark); } static void M_TriggerFlamethrower(const ITEM *const item, const int16_t speed) { const int16_t effect_num = Effect_Create(item->room_num); if (effect_num == NO_EFFECT) { return; } XYZ_32 pos_1 = m_Gun.pos; Collide_GetJointAbsPosition(item, &pos_1, m_Gun.mesh_num); XYZ_32 pos_2 = m_Gun.pos; pos_2.y <<= 1; Collide_GetJointAbsPosition(item, &pos_2, m_Gun.mesh_num); int16_t angles[2]; Math_GetVectorAngles( pos_2.x - pos_1.x, pos_2.y - pos_1.y, pos_2.z - pos_1.z, angles); EFFECT *const effect = Effect_Get(effect_num); effect->pos = pos_1; effect->rot.x = angles[1]; effect->rot.y = angles[0]; effect->speed = speed << 2; effect->object_id = O_MISSILE_FLAME; effect->counter = 20; effect->flag1 = 0; M_TriggerFlameSparks((XYZ_32) {}, (XYZ_32) {}, effect_num); for (int32_t i = 0; i < 2; i++) { const int32_t s = Random_GetControl() % (speed << 2) + 32; const int32_t r = (s * Math_Cos(effect->rot.x)) >> W2V_SHIFT; const XYZ_32 vel = { .x = (r * Math_Sin(effect->rot.y)) >> W2V_SHIFT, .y = -((s * Math_Sin(effect->rot.x)) >> W2V_SHIFT), .z = (r * Math_Cos(effect->rot.y)) >> W2V_SHIFT, }; M_TriggerFlameSparks( effect->pos, (XYZ_32) { vel.x << 5, vel.y << 5, vel.z << 5 }, -1); } { const int32_t r = ((speed << 1) * Math_Cos(effect->rot.x)) >> W2V_SHIFT; const XYZ_32 vel = { .x = (r * Math_Sin(effect->rot.y)) >> W2V_SHIFT, .y = -(((speed << 1) * Math_Sin(effect->rot.x)) >> W2V_SHIFT), .z = (r * Math_Cos(effect->rot.y)) >> W2V_SHIFT, }; M_TriggerFlameSparks( effect->pos, (XYZ_32) { vel.x << 5, vel.y << 5, vel.z << 5 }, -2); } } static void M_TriggerLights(const ITEM *const item) { XYZ_32 pos = m_Gun.pos; Collide_GetJointAbsPosition(item, &pos, m_Gun.mesh_num); const int32_t rnd = Random_GetControl(); if (item->current_anim_state == M_STATE_SHOOT_2 || item->current_anim_state == M_STATE_SHOOT_3) { const RGB_888 color = { .r = 255 - ((rnd >> 4) & 0x1F), .g = 192 - ((rnd >> 6) & 0x1F), .b = rnd & 0x3F, }; Output_AddDynamicLightRGB(pos, (rnd & 3) + 10, color); } else { const RGB_888 color = { .r = 192 - ((rnd >> 4) & 0x1F), .g = 128 - ((rnd >> 6) & 0x1F), .b = rnd & 0x1F, }; Output_AddDynamicLightRGB(pos, (rnd & 3) + 6, color); M_TriggerPilotFlame(item); } } static void M_CalculateEnemy(ITEM *const item) { CREATURE *const worker = item->creature_data; if (Creature_IsHostile(item)) { worker->enemy = Lara_GetItem(); return; } worker->enemy = nullptr; int32_t best_distance = INT32_MAX; for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) { const CREATURE *const creature = LOT_GetBaddieSlot(i); if (creature->item_num == NO_ITEM || creature == worker) { continue; } const ITEM *const candidate = Item_Get(creature->item_num); if (candidate->object_id == O_LARA || candidate->object_id == O_RX_WORKER_2 || candidate->object_id == O_RX_WORKER_3) { continue; } const int32_t dx = candidate->pos.x - item->pos.x; const int32_t dz = candidate->pos.z - item->pos.z; const int32_t distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz }); if (distance < best_distance) { worker->enemy = (ITEM *)candidate; best_distance = distance; } } } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t tilt = 0; int16_t angle = 0; int16_t head = 0; int16_t torso_x = 0; int16_t torso_y = 0; M_TriggerLights(item); if (item->hit_points <= 0) { if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, M_ANIM_DEATH, 0); item->current_anim_state = M_STATE_DEATH; } goto finish; } if (item->ai_bits != 0) { Creature_GetAITarget(creature); } else { M_CalculateEnemy(item); } // Enforce the following state to avoid Creature_AIInfo resetting ahead, // bite and distance when the creature is friendly. ITEM *const lara_item = Lara_GetItem(); const bool hurt_by_lara = creature->hurt_by_lara; ITEM *const enemy = creature->enemy; if (enemy == nullptr) { creature->enemy = lara_item; creature->hurt_by_lara = true; } AI_INFO info = {}; Creature_AIInfo(item, &info); creature->enemy = enemy; creature->hurt_by_lara = hurt_by_lara; AI_INFO lara_info = {}; const bool is_ally = !Creature_IsHostile(item); if (creature->enemy == lara_item) { lara_info.angle = info.angle; lara_info.distance = info.distance; if (!creature->hurt_by_lara && is_ally) { creature->enemy = nullptr; } } else { const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; info.x_angle -= DEG_45 / 4; lara_info.angle = Math_Atan(dz, dx) - item->rot.y; lara_info.distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz }); } Creature_Mood(item, &info, false); angle = Creature_Turn(item, creature->maximum_turn); if (item->hit_status || (!is_ally && (lara_info.distance < M_ALERT_DIST || Creature_CanSeeEnemy(item, &lara_info)))) { if (!creature->alerted) { Sound_Effect(SFX_AMERICAN_HOY, &item->pos, SPM_NORMAL); } Creature_AlertAllGuards(item_num); } switch (item->current_anim_state) { case M_STATE_STOP: head = lara_info.angle; creature->flags = 0; creature->maximum_turn = 0; if ((item->ai_bits & AI_GUARD) != 0) { head = Creature_AIGuard(creature); if ((Random_GetControl() & 0xFF) == 0) { item->goal_anim_state = M_STATE_WAIT; } } else if ((item->ai_bits & AI_PATROL_1) != 0) { item->goal_anim_state = M_STATE_WALK; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_WALK; } else if ( Creature_CanTargetEnemy(item, &info) && (enemy != lara_item || creature->hurt_by_lara || !is_ally)) { if (info.distance >= M_ATTACK_DIST) { item->goal_anim_state = M_STATE_WALK; } else { item->goal_anim_state = M_STATE_AIM_3; } } else if ( creature->mood == MOOD_BORED && info.ahead && (Random_GetControl() & 0xFF) == 0) { item->goal_anim_state = M_STATE_WAIT; } else if ( creature->mood == MOOD_ATTACK || (Random_GetControl() & 0xFF) == 0) { item->goal_anim_state = M_STATE_WALK; } break; case M_STATE_WALK: head = lara_info.angle; creature->flags = 0; creature->maximum_turn = M_WALK_TURN; if ((item->ai_bits & AI_GUARD) != 0) { Item_SwitchToAnim(item, M_ANIM_STOP, 0); item->current_anim_state = M_STATE_STOP; item->goal_anim_state = M_STATE_STOP; } else if ((item->ai_bits & AI_PATROL_1) != 0) { item->goal_anim_state = M_STATE_WALK; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_WALK; } else if ( Creature_CanTargetEnemy(item, &info) && (enemy != lara_item || creature->hurt_by_lara || !is_ally)) { if (info.distance >= M_ATTACK_DIST) { item->goal_anim_state = M_STATE_AIM_2; } else { item->goal_anim_state = M_STATE_STOP; } } else if (creature->mood == MOOD_BORED && info.ahead) { item->goal_anim_state = M_STATE_STOP; } else { item->goal_anim_state = M_STATE_WALK; } break; case M_STATE_WAIT: head = lara_info.angle; if ((item->ai_bits & AI_GUARD) != 0) { head = Creature_AIGuard(creature); if ((Random_GetControl() & 0xFF) == 0) { item->goal_anim_state = M_STATE_STOP; } } else if ( (Creature_CanTargetEnemy(item, &info) && info.distance < M_ATTACK_DIST && (enemy != lara_item || creature->hurt_by_lara || !is_ally)) || creature->mood != MOOD_BORED || (Random_GetControl() & 0xFF) == 0) { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_AIM_2: case M_STATE_AIM_3: creature->flags = 0; if (info.ahead) { torso_y = info.angle; torso_x = info.x_angle; if (Creature_CanTargetEnemy(item, &info) && info.distance < M_ATTACK_DIST && (enemy != lara_item || creature->hurt_by_lara || !is_ally)) { item->goal_anim_state = item->current_anim_state == M_STATE_AIM_2 ? M_STATE_SHOOT_2 : M_STATE_SHOOT_3; } else { item->goal_anim_state = M_STATE_WALK; } } break; case M_STATE_SHOOT_2: case M_STATE_SHOOT_3: if (creature->flags < M_MAX_FLAME_SPEED) { creature->flags += (creature->flags >> 2) + 1; } const M_STATE stop_state = item->current_anim_state == M_STATE_SHOOT_2 ? M_STATE_WALK : M_STATE_STOP; if (info.ahead) { torso_y = info.angle; torso_x = info.x_angle; if (Creature_CanTargetEnemy(item, &info) && info.distance < M_ATTACK_DIST && (enemy != lara_item || creature->hurt_by_lara || !is_ally)) { item->goal_anim_state = item->current_anim_state; } else { item->goal_anim_state = stop_state; } } else { item->goal_anim_state = stop_state; } const int16_t speed = creature->flags < M_MAX_FLAME_SPEED ? creature->flags : ((Random_GetControl() & 0x1F) + 12); M_TriggerFlamethrower(item, speed); Sound_Effect(SFX_FLAME_THROWER_LOOP, &item->pos, SPM_NORMAL); if (enemy != nullptr) { const OBJECT *const obj = Object_Get(enemy->object_id); if (obj->event_func != nullptr) { obj->event_func(enemy, OBJECT_EVENT_BURNT, nullptr); } } break; } finish: Creature_Tilt(item, tilt); Creature_Joint(item, 0, torso_y); Creature_Joint(item, 1, torso_x); Creature_Joint(item, 2, head); Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->collision_func = Creature_Collision; obj->control_func = M_Control; obj->shadow_size = UNIT_SHADOW / 2; obj->radius = M_RADIUS; obj->hit_points = M_HIT_POINTS; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 0)->rot.x = true; Object_GetBone(obj, 0)->rot.y = true; Object_GetBone(obj, 7)->rot.y = true; } REGISTER_OBJECT(O_RX_WORKER_3, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/security_guard.c ================================================ #include #include #include #include #include #include // clang-format off #define M_RADIUS (WALL_L / 10) // = 102 #define M_HIT_POINTS 28 #define M_DAMAGE 32 #define M_ALERT_DIST SQUARE(WALL_L) // = 1048576 #define M_ALERT_HEIGHT (WALL_L * 2) // = 2048 #define M_WALK_DIST SQUARE(WALL_L * 2) // = 4194304 #define M_WALK_TURN (DEG_1 * 5) // = 910 #define M_RUN_TURN (DEG_1 * 10) // = 1820 #define M_DUCK_TURN DEG_1 // = 182 #define M_SHOOT_1_CHANCE 0x2000 #define M_SHOOT_2_CHANCE 0x4000 #define M_DUCK_CHANCE 0x3 #define M_DUCK_END_CHANCE 0x1F // clang-format on typedef enum { M_STATE_NULL, M_STATE_WAIT, M_STATE_WALK, M_STATE_RUN, M_STATE_AIM_1, M_STATE_SHOOT_1, M_STATE_AIM_2, M_STATE_SHOOT_2, M_STATE_SHOOT_3A, M_STATE_SHOOT_3B, M_STATE_SHOOT_4A, M_STATE_AIM_3, M_STATE_AIM_4, M_STATE_DEATH, M_STATE_SHOOT_4B, M_STATE_DUCK_START, M_STATE_DUCKED, M_STATE_DUCK_AIM, M_STATE_DUCK_SHOOT, M_STATE_DUCK_WALK, M_STATE_DUCK_END, } M_STATE; typedef enum { // clang-format off M_ANIM_SHOOT_1 = 1, M_ANIM_AIM_1 = 12, M_ANIM_DEATH = 14, M_ANIM_WALK_STOP = 17, M_ANIM_AIM_4A = 18, M_ANIM_AIM_4B = 19, M_ANIM_RUN_STOP_1 = 27, M_ANIM_RUN_STOP_2 = 28, // clang-format on } M_ANIM; static const CREATURE_GUN m_GuardGun = { .muzzle = { .pos = { 0, 160, 40 }, .mesh_num = 13 }, .tr3_enemy_flash = true, .tr3_flash = { .pos = { 0, 192, 40 }, .mesh_num = 13 }, .tr3_enemy_weapon_flags = 0, .tr3_flash_shade = 600, .tr3_flash_rot_x = -DEG_90, }; static void M_FireFinalShot( ITEM *const item, int16_t *const head, int16_t *const torso_y) { const int16_t frame_idx = Item_GetRelativeFrame(item); if (frame_idx != 3 && frame_idx != 28) { return; } AI_INFO info = {}; Creature_AIInfo(item, &info); if (!Creature_CanTargetEnemy(item, &info) || ABS(info.angle) >= DEG_45) { return; } *head = info.angle; *torso_y = info.angle; Creature_Shoot(item, &info, &m_GuardGun, info.angle, M_DAMAGE * 2); Sound_Effect(SFX_SECURITY_GUARD_FIRE, &item->pos, SPM_NORMAL); } static bool M_IsNearCover(const ITEM *const item, const AI_INFO *const info) { const XYZ_32 pos = XYZ_32_OffsetYaw(item->pos, item->rot.y + info->angle, WALL_L); int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); const int32_t height = Room_GetHeight(sector, pos); return item->pos.y > height + STEPUP_HEIGHT && item->pos.y < height + STEPUP_HEIGHT * 3 && info->distance > M_ALERT_DIST; } static bool M_ShouldDuck(const ITEM *const item, const bool near_cover) { return item->hit_status && (Random_GetControl() & M_DUCK_CHANCE) == 0 && near_cover; } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t angle = 0; int16_t head = 0; int16_t tilt = 0; int16_t torso_x = 0; int16_t torso_y = 0; if (item->hit_points <= 0) { item->hit_points = 0; if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, M_ANIM_DEATH, 0); item->current_anim_state = M_STATE_DEATH; } else if ((Random_GetControl() & 1) != 0) { M_FireFinalShot(item, &head, &torso_y); } goto finish; } ITEM *const lara_item = Lara_GetItem(); const LARA_INFO *const lara = Lara_GetLaraInfo(); if (item->ai_bits != 0) { Creature_GetAITarget(creature); } else { creature->enemy = lara_item; } AI_INFO info = {}; Creature_AIInfo(item, &info); AI_INFO lara_info = {}; if (creature->enemy == lara_item) { lara_info.distance = info.distance; lara_info.angle = info.angle; } else { const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; lara_info.angle = Math_Atan(dz, dx) - item->rot.y; lara_info.distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz }); } Creature_Mood(item, &info, creature->enemy != lara_item); angle = Creature_Turn(item, creature->maximum_turn); const bool near_cover = M_IsNearCover(item, &lara_info); ITEM *const enemy = creature->enemy; creature->enemy = lara_item; if (item->hit_status || ((lara_info.distance < M_ALERT_DIST || Creature_CanSeeEnemy(item, &lara_info)) && ABS(lara_item->pos.y - item->pos.y) < M_ALERT_HEIGHT)) { if (!creature->alerted) { Sound_Effect(SFX_ENGLISH_HOY, &item->pos, SPM_NORMAL); } Creature_AlertAllGuards(item_num); } creature->enemy = enemy; const int16_t anim_idx = Item_GetRelativeAnim(item); const int16_t frame_idx = Item_GetRelativeFrame(item); switch (item->current_anim_state) { case M_STATE_WAIT: head = lara_info.angle; creature->maximum_turn = 0; if (anim_idx == M_ANIM_WALK_STOP || anim_idx == M_ANIM_RUN_STOP_1 || anim_idx == M_ANIM_RUN_STOP_2) { if (ABS(info.angle) < M_RUN_TURN) { item->rot.y += info.angle; } else if (info.angle < 0) { item->rot.y -= M_RUN_TURN; } else { item->rot.y += M_RUN_TURN; } } if ((item->ai_bits & AI_GUARD) != 0) { head = Creature_AIGuard(creature); item->goal_anim_state = M_STATE_WAIT; } else if ((item->ai_bits & AI_PATROL_1) != 0) { item->goal_anim_state = M_STATE_WALK; head = 0; } else if (near_cover && (lara->target == item || item->hit_status)) { item->goal_anim_state = M_STATE_DUCK_START; } else if (item->required_anim_state == M_STATE_DUCK_START) { item->goal_anim_state = M_STATE_DUCK_START; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if (Creature_CanTargetEnemy(item, &info)) { if (info.distance > M_WALK_DIST) { item->goal_anim_state = M_STATE_WALK; } else { const int32_t rnd = Random_GetControl(); if (rnd < M_SHOOT_1_CHANCE) { item->goal_anim_state = M_STATE_SHOOT_1; } else if (rnd < M_SHOOT_2_CHANCE) { item->goal_anim_state = M_STATE_SHOOT_2; } else { item->goal_anim_state = M_STATE_AIM_3; } } } else if ( creature->mood == MOOD_BORED || ((item->ai_bits & AI_FOLLOW) != 0 && (creature->reached_goal || lara_info.distance > M_WALK_DIST))) { if (info.ahead) { item->goal_anim_state = M_STATE_WAIT; } else { item->goal_anim_state = M_STATE_WALK; } } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_WALK: head = lara_info.angle; creature->maximum_turn = M_WALK_TURN; if ((item->ai_bits & AI_PATROL_1) != 0) { item->goal_anim_state = M_STATE_WALK; head = 0; } else if (near_cover && (lara->target == item || item->hit_status)) { item->required_anim_state = M_STATE_DUCK_START; item->goal_anim_state = M_STATE_WAIT; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if (Creature_CanTargetEnemy(item, &info)) { if (info.distance > M_WALK_DIST && info.zone_num == info.enemy_zone_num) { item->goal_anim_state = M_STATE_AIM_4; } else { item->goal_anim_state = M_STATE_WAIT; } } else if (creature->mood == MOOD_BORED) { if (info.ahead) { item->goal_anim_state = M_STATE_WALK; } else { item->goal_anim_state = M_STATE_WAIT; } } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_RUN: if (info.ahead) { head = info.angle; } creature->maximum_turn = M_RUN_TURN; tilt = angle / 2; if ((item->ai_bits & AI_GUARD) != 0) { item->goal_anim_state = M_STATE_WAIT; } else if (near_cover && (lara->target == item || item->hit_status)) { item->required_anim_state = M_STATE_DUCK_START; item->goal_anim_state = M_STATE_WAIT; } else if (creature->mood != MOOD_ESCAPE) { if (Creature_CanTargetEnemy(item, &info) || ((item->ai_bits & AI_FOLLOW) != 0 && (creature->reached_goal || lara_info.distance > M_WALK_DIST))) { item->goal_anim_state = M_STATE_WAIT; } else if (creature->mood == MOOD_BORED) { item->goal_anim_state = M_STATE_WALK; } } break; case M_STATE_AIM_1: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } if (anim_idx == M_ANIM_AIM_1 || (anim_idx == M_ANIM_SHOOT_1 && frame_idx == 10)) { if (!Creature_Shoot(item, &info, &m_GuardGun, torso_y, M_DAMAGE)) { item->required_anim_state = M_STATE_WAIT; } } else if (M_ShouldDuck(item, near_cover)) { item->required_anim_state = M_STATE_DUCK_START; item->goal_anim_state = M_STATE_WAIT; } break; case M_STATE_SHOOT_1: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } if (item->required_anim_state == M_STATE_WAIT) { item->goal_anim_state = M_STATE_WAIT; } break; case M_STATE_SHOOT_2: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } if (frame_idx == 0) { if (!Creature_Shoot(item, &info, &m_GuardGun, torso_y, M_DAMAGE)) { item->goal_anim_state = M_STATE_WAIT; } } else if (M_ShouldDuck(item, near_cover)) { item->required_anim_state = M_STATE_DUCK_START; item->goal_anim_state = M_STATE_WAIT; } break; case M_STATE_SHOOT_3A: case M_STATE_SHOOT_3B: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } if (frame_idx == 0 || frame_idx == 11) { if (!Creature_Shoot(item, &info, &m_GuardGun, torso_y, M_DAMAGE)) { item->goal_anim_state = M_STATE_WAIT; } } else if (M_ShouldDuck(item, near_cover)) { item->required_anim_state = M_STATE_DUCK_START; item->goal_anim_state = M_STATE_WAIT; } break; case M_STATE_AIM_4: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } if ((anim_idx == M_ANIM_AIM_4A && frame_idx == 16) || (anim_idx == M_ANIM_AIM_4B && frame_idx == 6)) { if (!Creature_Shoot(item, &info, &m_GuardGun, torso_y, M_DAMAGE)) { item->goal_anim_state = M_STATE_WALK; } } else if (M_ShouldDuck(item, near_cover)) { item->required_anim_state = M_STATE_DUCK_START; item->goal_anim_state = M_STATE_WAIT; } if (info.distance < M_WALK_DIST) { item->required_anim_state = M_STATE_WALK; } break; case M_STATE_SHOOT_4A: case M_STATE_SHOOT_4B: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } if (item->required_anim_state == M_STATE_WALK) { item->goal_anim_state = M_STATE_WALK; } if (frame_idx == 16 && !Creature_Shoot(item, &info, &m_GuardGun, torso_y, M_DAMAGE)) { item->goal_anim_state = M_STATE_WALK; } if (info.distance < M_WALK_DIST) { item->goal_anim_state = M_STATE_WALK; } break; case M_STATE_DUCKED: if (info.ahead) { head = info.angle; } creature->maximum_turn = 0; if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = M_STATE_DUCK_AIM; } else if ( item->hit_status || !near_cover || (info.ahead && (Random_GetControl() & M_DUCK_END_CHANCE) == 0)) { item->goal_anim_state = M_STATE_DUCK_END; } else { item->goal_anim_state = M_STATE_DUCK_WALK; } break; case M_STATE_DUCK_AIM: if (info.ahead) { torso_y = info.angle; } creature->maximum_turn = M_DUCK_TURN; if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = M_STATE_DUCK_SHOOT; } else { item->goal_anim_state = M_STATE_DUCKED; } break; case M_STATE_DUCK_SHOOT: if (info.ahead) { torso_y = info.angle; } if (frame_idx != 0) { break; } if (!Creature_Shoot(item, &info, &m_GuardGun, torso_y, M_DAMAGE) || (Random_GetControl() & 7) == 0) { item->goal_anim_state = M_STATE_DUCKED; } break; case M_STATE_DUCK_WALK: if (info.ahead) { head = info.angle; } creature->maximum_turn = M_WALK_TURN; if (Creature_CanTargetEnemy(item, &info) || item->hit_status || !near_cover || (info.ahead && (Random_GetControl() & M_DUCK_END_CHANCE) == 0)) { item->goal_anim_state = M_STATE_DUCKED; } break; default: break; } finish: Creature_Tilt(item, tilt); Creature_Joint(item, 0, torso_y); Creature_Joint(item, 1, torso_x); Creature_Joint(item, 2, head); Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->shadow_size = UNIT_SHADOW / 2; obj->radius = M_RADIUS; obj->hit_points = M_HIT_POINTS; obj->intelligent = true; obj->save_anim = true; obj->save_flags = true; obj->save_hitpoints = true; obj->save_position = true; Object_GetBone(obj, 6)->rot.x = true; Object_GetBone(obj, 6)->rot.y = true; Object_GetBone(obj, 13)->rot.y = true; } REGISTER_OBJECT(O_SECURITY_GUARD, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/shark.c ================================================ #include #include #include #include #include #include #include #include // clang-format off #define SHARK_HITPOINTS 30 #define SHARK_TOUCH_BITS 0b00110100'00000000 // = 0x3400 #define SHARK_RADIUS (WALL_L / 3) // = 341 #define SHARK_BITE_DAMAGE 400 #define SHARK_SWIM_2_RANGE SQUARE(WALL_L * 3) // = 9437184 #define SHARK_ATTACK_1_RANGE SQUARE(WALL_L * 3 / 4) // = 589824 #define SHARK_ATTACK_2_RANGE SQUARE(WALL_L * 4 / 3) // = 1863225 #define SHARK_SWIM_1_TURN (DEG_1 / 2) // = 91 #define SHARK_SWIM_2_TURN (DEG_1 * 2) // = 364 #define SHARK_ATTACK_1_CHANCE 0x800 // clang-format on typedef enum { // clang-format off SHARK_STATE_STOP = 0, SHARK_STATE_SWIM_1 = 1, SHARK_STATE_SWIM_2 = 2, SHARK_STATE_ATTACK_1 = 3, SHARK_STATE_ATTACK_2 = 4, SHARK_STATE_DEATH = 5, SHARK_STATE_KILL = 6, // clang-format on } SHARK_STATE; typedef enum { // clang-format off SHARK_ANIM_DEATH = 4, SHARK_ANIM_KILL = 19, // clang-format on } SHARK_ANIM; static const BITE m_SharkBite = { .pos = { .x = 17, .y = -22, .z = 344 }, .mesh_num = 12, }; static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; const ITEM *const lara_item = Lara_GetItem(); const bool lara_was_alive = lara_item->hit_points > 0; if (item->hit_points <= 0) { if (item->current_anim_state != SHARK_STATE_DEATH) { Item_SwitchToAnim(item, SHARK_ANIM_DEATH, 0); item->current_anim_state = SHARK_STATE_DEATH; Carrier_TestItemDrops(item_num); } Creature_Float(item_num); } else { AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, true); int16_t head = 0; int16_t angle = Creature_Turn(item, creature->maximum_turn); switch (item->current_anim_state) { case SHARK_STATE_STOP: creature->flags = 0; creature->maximum_turn = 0; if (info.ahead && info.distance < SHARK_ATTACK_1_RANGE && info.zone_num == info.enemy_zone_num) { item->goal_anim_state = SHARK_STATE_ATTACK_1; } else { item->goal_anim_state = SHARK_STATE_SWIM_1; } break; case SHARK_STATE_SWIM_1: creature->maximum_turn = SHARK_SWIM_1_TURN; if (creature->mood == MOOD_BORED) { } else if (info.ahead && info.distance < SHARK_ATTACK_1_RANGE) { item->goal_anim_state = SHARK_STATE_STOP; } else if ( creature->mood == MOOD_ESCAPE || info.distance > SHARK_SWIM_2_RANGE || !info.ahead) { item->goal_anim_state = SHARK_STATE_SWIM_2; } break; case SHARK_STATE_SWIM_2: creature->flags = 0; creature->maximum_turn = SHARK_SWIM_2_TURN; if (creature->mood == MOOD_BORED) { item->goal_anim_state = SHARK_STATE_SWIM_1; } else if (creature->mood == MOOD_ESCAPE) { } else if ( info.ahead && info.distance < SHARK_ATTACK_2_RANGE && info.zone_num == info.enemy_zone_num) { if (Random_GetControl() < SHARK_ATTACK_1_CHANCE) { item->goal_anim_state = SHARK_STATE_STOP; } else if (info.distance < SHARK_ATTACK_1_RANGE) { item->goal_anim_state = SHARK_STATE_ATTACK_2; } } break; case SHARK_STATE_ATTACK_1: case SHARK_STATE_ATTACK_2: if (info.ahead) { head = info.angle; } if (creature->flags == 0 && (item->touch_bits & SHARK_TOUCH_BITS) != 0) { Lara_TakeDamage(SHARK_BITE_DAMAGE, true); Creature_Effect(item, &m_SharkBite, Spawn_Blood); creature->flags = 1; } break; default: break; } if (lara_was_alive && lara_item->hit_points <= 0) { Creature_SpecialKill( item, SHARK_ANIM_KILL, SHARK_STATE_KILL, LS_EXTRA_SHARK_KILL); } else if (item->current_anim_state == SHARK_STATE_KILL) { Item_Animate(item); } else { Creature_Head(item, head); Creature_Animate(item_num, angle, 0); Creature_Underwater(item, SHARK_RADIUS); } } } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->draw_func = Object_DrawUnclippedItem; obj->collision_func = Creature_Collision; obj->hit_points = SHARK_HITPOINTS; obj->radius = SHARK_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->pivot_length = 200; obj->lot_setup = LOT_Setup(LOT_SETUP_FLYER); obj->lot_setup.block_mask = BOX_BLOCKABLE; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 9)->rot.y = true; } REGISTER_OBJECT(O_SHARK, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/shiva.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include // clang-format off #define M_WALK_TURN (4 * DEG_1) #define M_PINCER_RANGE SQUARE(WALL_L * 5 / 4) #define M_CHOPPER_RANGE SQUARE(WALL_L * 4 / 3) // clang-format on static BITE m_RightBlade = { .pos = { 0, 0, 920 }, .mesh_num = 22 }; static BITE m_LeftBlade = { .pos = { 0, 0, 920 }, .mesh_num = 13 }; typedef enum { M_STATE_WAIT, M_STATE_WALK, M_STATE_WAIT_DEF, M_STATE_WALK_DEF, M_STATE_START, M_STATE_PINCER, M_STATE_KILL, M_STATE_CHOPPER, M_STATE_WALK_BACK, M_STATE_DEATH } M_STATE; typedef enum { M_ANIM_DEATH = 22, M_ANIM_START_ANIM = 14, M_ANIM_KILL_ANIM = 18, } M_ANIM; typedef struct { int32_t effect_mesh; } M_PRIV; static bool M_ShouldSpawnBlood(const ITEM *const item) { if (item->current_anim_state != M_STATE_WAIT_DEF && item->current_anim_state != M_STATE_WALK_DEF && item->current_anim_state != M_STATE_START) { return true; } const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = item->pos.x - lara_item->pos.x; const int32_t dz = item->pos.z - lara_item->pos.z; const int16_t angle = DEG_180 - item->rot.y + Math_Atan(dz, dx); if (angle <= -DEG_90 || angle >= DEG_90) { return true; } const LARA_INFO *const lara = Lara_GetLaraInfo(); // XXX: This uses Lara's currently equipped gun. If she swaps weapons before // a projectile impact resolves, this can differ from the projectile weapon. if (lara->gun_type == LGT_ROCKET || lara->gun_type == LGT_GRENADE || lara->gun_type == LGT_HARPOON) { return true; } return false; } static void M_TriggerSmoke(const XYZ_32 pos, const bool uw) { const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - pos.x; const int32_t dz = lara_item->pos.z - pos.z; if (dx < -0x4000 || dx > 0x4000 || dz < -0x4000 || dz > 0x4000) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; if (uw) { spark->src_color.r = 0; spark->src_color.g = 0; spark->src_color.b = 0; spark->dst_color.r = 192; spark->dst_color.g = 192; spark->dst_color.b = 208; } else { spark->src_color.r = 144; spark->src_color.g = 144; spark->src_color.b = 144; spark->dst_color.r = 64; spark->dst_color.g = 64; spark->dst_color.b = 64; } spark->col_fade_speed = 8; spark->fade_to_black = 64; spark->life = (Random_GetControl() & 0x1F) + 96; spark->s_life = spark->life; if (uw) { spark->draw_type = DRAW_BLEND_ADD; } else { spark->draw_type = DRAW_BLEND_SUB; } spark->extras = 0; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16; spark->pos.y = pos.y + (Random_GetControl() & 0x1F) - 16; spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16; spark->vel.x = ((Random_GetControl() & 0xFFF) - 2048) >> 2; spark->vel.y = (Random_GetControl() & 0xFF) - 128; spark->vel.z = ((Random_GetControl() & 0xFFF) - 2048) >> 2; if (uw) { spark->friction = 20; spark->pos.y += 32; spark->vel.y >>= 4; } else { spark->friction = 6; } spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = Random_GetControl() & 0xFFF; if (Random_GetControl() & 1) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = (Random_GetControl() & 0xF) + 16; } spark->scalar = 3; spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; if (uw) { spark->max_y_vel = 0; spark->gravity = 0; } else { spark->gravity = -3 - (Random_GetControl() & 3); spark->max_y_vel = -4 - (Random_GetControl() & 3); } spark->dst_size.width = (Random_GetControl() & 0x1F) + 128; spark->size.width = spark->dst_size.width >> 2; spark->src_size.width = spark->size.width >> 2; spark->dst_size.height = spark->dst_size.width + (Random_GetControl() & 0x1F) + 32; spark->size.height = spark->dst_size.height >> 3; spark->src_size.height = spark->size.height; Sparks_FinishSetup(spark); } static void M_Damage( ITEM *const item, CREATURE *const creature, const int32_t damage) { if (creature->flags == 0 && item->touch_bits & 0x2400000) { Lara_TakeDamage(damage, true); Creature_Effect(item, &m_RightBlade, Spawn_Blood); creature->flags = 1; Sound_Effect(SFX_MACAQUE_ROLL, &item->pos, SPM_NORMAL); } if (creature->flags == 0 && item->touch_bits & 0x2400) { Lara_TakeDamage(damage, true); Creature_Effect(item, &m_LeftBlade, Spawn_Blood); creature->flags = 1; Sound_Effect(SFX_MACAQUE_ROLL, &item->pos, SPM_NORMAL); } } static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "effect_mesh", &p->effect_mesh)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "effect_mesh", p->effect_mesh); } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; if (item->object_id == O_SHIVA) { Item_SwitchToAnim(item, M_ANIM_START_ANIM, 0); item->current_anim_state = M_STATE_START; item->goal_anim_state = M_STATE_START; } item->status = IS_INACTIVE; item->mesh_bits = 0; p->effect_mesh = 0; } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } const ITEM *const lara_item = Lara_GetItem(); ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; M_PRIV *const p = item->priv; const bool lara_alive = lara_item->hit_points > 0; int16_t torso_x = 0; int16_t torso_y = 0; int16_t head_y = 0; int16_t angle = 0; if (item->hit_points <= 0) { if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, M_ANIM_DEATH, 0); item->current_anim_state = M_STATE_DEATH; } } else { AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, true); if (creature->mood == MOOD_ESCAPE) { creature->target.x = lara_item->pos.x; creature->target.z = lara_item->pos.z; } angle = Creature_Turn(item, creature->maximum_turn); if (item->current_anim_state != M_STATE_START) { item->mesh_bits = INT32_MAX; } switch (item->current_anim_state) { case M_STATE_WAIT: if (info.ahead) { head_y = info.angle; } if (creature->flags < 0) { creature->flags++; const XYZ_32 smoke_pos = { .x = item->pos.x + (Random_GetControl() & 0x5FF) - 768, .y = item->pos.y - (Random_GetControl() & 0x5FF), .z = item->pos.z + (Random_GetControl() & 0x5FF) - 768, }; M_TriggerSmoke(smoke_pos, true); } else { if (creature->flags == 1) { creature->flags = 0; } creature->maximum_turn = 0; if (creature->mood == MOOD_ESCAPE) { int16_t room_num = item->room_num; const XYZ_32 offset = XYZ_32_OffsetYaw( item->pos, item->rot.y + DEG_180, WALL_L); const SECTOR *const sector = Room_GetSector(offset, &room_num); if (creature->flags != 0 || sector->box == 0x7FF || Box_GetBox(sector->box)->overlap_index & BOX_BLOCKABLE) { item->goal_anim_state = M_STATE_WAIT_DEF; } else { item->goal_anim_state = M_STATE_WALK_BACK; } } else if (creature->mood == MOOD_BORED) { if (Random_GetControl() < 1024) { item->goal_anim_state = M_STATE_WALK; } } else if (info.bite && info.distance < M_PINCER_RANGE) { item->goal_anim_state = M_STATE_PINCER; creature->flags = 0; } else if (info.bite && info.distance < M_CHOPPER_RANGE) { item->goal_anim_state = M_STATE_CHOPPER; creature->flags = 0; } else if (item->hit_status && info.ahead) { item->goal_anim_state = M_STATE_WAIT_DEF; creature->flags = 4; } else { item->goal_anim_state = M_STATE_WALK; } } break; case M_STATE_WALK: if (info.ahead) { head_y = info.angle; } creature->maximum_turn = M_WALK_TURN; if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_WAIT; } else if (creature->mood == MOOD_BORED) { item->goal_anim_state = M_STATE_WAIT; } else if (info.bite && info.distance < M_CHOPPER_RANGE) { item->goal_anim_state = M_STATE_WAIT; creature->flags = 0; } else if (item->hit_status) { item->goal_anim_state = M_STATE_WALK_DEF; creature->flags = 4; } break; case M_STATE_WAIT_DEF: if (info.ahead) { head_y = info.angle; } creature->maximum_turn = 0; if (item->hit_status || creature->mood == MOOD_ESCAPE) { creature->flags = 4; } if ((info.bite && info.distance < M_CHOPPER_RANGE) || (Item_GetRelativeFrame(item) == 0 && creature->flags == 0) || !info.ahead) { item->goal_anim_state = M_STATE_WAIT; creature->flags = 0; } else if (creature->flags != 0) { item->goal_anim_state = M_STATE_WAIT_DEF; } if (Item_GetRelativeFrame(item) == 0 && creature->flags > 1) { creature->flags -= 2; } break; case M_STATE_WALK_DEF: if (info.ahead) { head_y = info.angle; } creature->maximum_turn = M_WALK_TURN; if (item->hit_status) { creature->flags = 4; } if ((info.bite && info.distance < M_PINCER_RANGE) || (Item_GetRelativeFrame(item) == 0 && creature->flags == 0)) { item->goal_anim_state = M_STATE_WALK; creature->flags = 0; } else if (creature->flags != 0) { item->goal_anim_state = M_STATE_WALK_DEF; } if (Item_GetRelativeFrame(item) == 0) { creature->flags = 0; } break; case M_STATE_START: creature->maximum_turn = 0; if (creature->flags != 0) { creature->flags--; } else { if (item->mesh_bits == 0) { p->effect_mesh = 0; } item->mesh_bits <<= 1; item->mesh_bits |= 1; creature->flags = 1; XYZ_32 smoke_pos = { 0, 0, 256 }; Collide_GetJointAbsPosition(item, &smoke_pos, p->effect_mesh++); Sparks_TriggerExplosionSparks( smoke_pos, 2, 0, 0, item->room_num); M_TriggerSmoke(smoke_pos, true); } if (item->mesh_bits == INT32_MAX) { item->goal_anim_state = M_STATE_WAIT; p->effect_mesh = 0; creature->flags = -45; } break; case M_STATE_PINCER: if (info.ahead) { torso_y = info.angle; torso_x = info.x_angle; head_y = info.angle; } creature->maximum_turn = M_WALK_TURN; M_Damage(item, creature, 150); break; case M_STATE_KILL: { creature->maximum_turn = 0; head_y = 0; torso_x = 0; torso_y = 0; const int32_t frame = Item_GetRelativeFrame(item); if (frame == 10 || frame == 21 || frame == 33) { Creature_Effect(item, &m_RightBlade, Spawn_Blood); Creature_Effect(item, &m_LeftBlade, Spawn_Blood); } break; } case M_STATE_CHOPPER: head_y = info.angle; torso_y = info.angle; if (info.x_angle > 0) { torso_x = info.x_angle; } creature->maximum_turn = M_WALK_TURN; M_Damage(item, creature, 180); break; case M_STATE_WALK_BACK: if (info.ahead) { head_y = info.angle; } creature->maximum_turn = M_WALK_TURN; if ((info.ahead && info.distance < M_CHOPPER_RANGE) || (Item_GetRelativeFrame(item) == 0 && creature->flags == 0)) { item->goal_anim_state = M_STATE_WAIT; } else if (item->hit_status) { creature->flags = 4; item->goal_anim_state = M_STATE_WAIT; } break; } } if (lara_alive && lara_item->hit_points <= 0) { Creature_SpecialKill( item, M_ANIM_KILL_ANIM, M_STATE_KILL, LS_EXTRA_SHIVA_KILL); return; } Creature_Tilt(item, 0); Creature_Joint(item, 0, torso_y); Creature_Joint(item, 1, torso_x); Creature_Joint(item, 2, head_y - torso_y); Creature_Joint(item, 3, 0); Creature_Animate(item_num, angle, 0); } bool M_Draw(const ITEM *const item) { M_PRIV *const p = item->priv; if (item->hit_points <= 0 && item->status != IS_ACTIVE && item->mesh_bits != 0) { ITEM *const mutable_item = (ITEM *)item; mutable_item->mesh_bits >>= 1; XYZ_32 smoke_pos = { 0, 0, 256 }; Collide_GetJointAbsPosition(item, &smoke_pos, p->effect_mesh++); M_TriggerSmoke(smoke_pos, true); } const OBJECT *const swap = Object_Get(O_MESH_SWAP_1); return Object_DrawAnimatingItemWithSwap(item, swap); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } if (!Object_Get(O_MESH_SWAP_1)->loaded) { Shell_ExitSystem("Shiva requires O_MESH_SWAP_1 (statue)"); } obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->should_spawn_blood_func = M_ShouldSpawnBlood; obj->draw_func = M_Draw; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->shadow_size = UNIT_SHADOW / 2; obj->hit_points = 100; obj->radius = 341; obj->pivot_length = 0; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 6)->rot.x = true; Object_GetBone(obj, 6)->rot.y = true; Object_GetBone(obj, 25)->rot.x = true; Object_GetBone(obj, 25)->rot.y = true; } REGISTER_OBJECT(O_SHIVA, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/skate_kid.c ================================================ #include #include #include #include #include #include #include #include #define SKATE_KID_STOP_SHOT_DAMAGE 50 #define SKATE_KID_SKATE_SHOT_DAMAGE 40 #define SKATE_KID_STOP_RANGE SQUARE(WALL_L * 4) // = 16777216 #define SKATE_KID_DONT_STOP_RANGE SQUARE(WALL_L * 5 / 2) // = 6553600 #define SKATE_KID_TOO_CLOSE SQUARE(WALL_L) // = 1048576 #define SKATE_KID_SKATE_TURN (DEG_1 * 4) // = 728 #define SKATE_KID_PUSH_CHANCE 0x200 #define SKATE_KID_SKATE_CHANCE 0x400 #define SKATE_KID_DIE_ANIM 13 #define SKATE_KID_HITPOINTS 125 #define SKATE_KID_RADIUS (WALL_L / 5) // = 204 #define SKATE_KID_SMARTNESS 0x7FFF #define SKATE_KID_SPEECH_HITPOINTS 120 #define SKATE_KID_SPEECH_STARTED 1 typedef enum { SKATE_KID_STATE_STOP = 0, SKATE_KID_STATE_SHOOT_1 = 1, SKATE_KID_STATE_SKATE = 2, SKATE_KID_STATE_PUSH = 3, SKATE_KID_STATE_SHOOT_2 = 4, SKATE_KID_STATE_DEATH = 5, } SKATE_KID_STATE; typedef struct { int16_t skateboard_item_num; } M_PRIV; static const CREATURE_GUN m_KidGun1 = { .muzzle = { .pos = { 0, 150, 34 }, .mesh_num = 7 }, }; static const CREATURE_GUN m_KidGun2 = { .muzzle = { .pos = { 0, 150, 37 }, .mesh_num = 4 }, }; static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; p->skateboard_item_num = NO_ITEM; Creature_Initialise(item_num); item->current_anim_state = SKATE_KID_STATE_SKATE; if (!Object_Get(O_SKATEBOARD)->loaded) { return; } const int16_t skateboard_item_num = Item_Create(); if (skateboard_item_num == NO_ITEM) { LOG_WARNING("Failed to create skateboard item for skate kid."); return; } ITEM *const skateboard_item = Item_Get(skateboard_item_num); skateboard_item->object_id = O_SKATEBOARD; skateboard_item->pos = item->pos; skateboard_item->rot = item->rot; skateboard_item->room_num = item->room_num; skateboard_item->status = item->status; skateboard_item->collidable = false; skateboard_item->shade.value_1 = -1; Item_Initialise(skateboard_item_num); p->skateboard_item_num = skateboard_item_num; } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; CREATURE *const kid = item->creature_data; int16_t head = 0; int16_t angle = 0; if (item->hit_points <= 0) { if (item->current_anim_state != SKATE_KID_STATE_DEATH) { item->current_anim_state = SKATE_KID_STATE_DEATH; Item_SwitchToAnim(item, SKATE_KID_DIE_ANIM, 0); } } else { AI_INFO info; Creature_AIInfo(item, &info); if (info.ahead) { head = info.angle; } Creature_Mood(item, &info, false); angle = Creature_Turn(item, SKATE_KID_SKATE_TURN); if (item->hit_points < SKATE_KID_SPEECH_HITPOINTS && !(item->flags & SKATE_KID_SPEECH_STARTED)) { const MUSIC_PLAY_MODE mode = g_Config.audio.fix_speeches_killing_music ? MPM_OVERLAY : MPM_NO_REPEAT; Music_Play(MX_SKATEKID_SPEECH, mode); item->flags |= SKATE_KID_SPEECH_STARTED; } switch (item->current_anim_state) { case SKATE_KID_STATE_STOP: kid->flags = 0; if (item->required_anim_state) { item->goal_anim_state = item->required_anim_state; } else if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = SKATE_KID_STATE_SHOOT_1; } else { item->goal_anim_state = SKATE_KID_STATE_SKATE; } break; case SKATE_KID_STATE_SKATE: kid->flags = 0; if (Random_GetControl() < SKATE_KID_PUSH_CHANCE) { item->goal_anim_state = SKATE_KID_STATE_PUSH; } else if (Creature_CanTargetEnemy(item, &info)) { if (info.distance > SKATE_KID_DONT_STOP_RANGE && info.distance < SKATE_KID_STOP_RANGE && kid->mood != MOOD_ESCAPE) { item->goal_anim_state = SKATE_KID_STATE_STOP; } else { item->goal_anim_state = SKATE_KID_STATE_SHOOT_2; } } break; case SKATE_KID_STATE_PUSH: if (Random_GetControl() < SKATE_KID_SKATE_CHANCE) { item->goal_anim_state = SKATE_KID_STATE_SKATE; } break; case SKATE_KID_STATE_SHOOT_1: case SKATE_KID_STATE_SHOOT_2: if (!kid->flags && Creature_CanTargetEnemy(item, &info)) { Creature_Shoot( item, &info, &m_KidGun1, head, item->current_anim_state == SKATE_KID_STATE_SHOOT_1 ? SKATE_KID_STOP_SHOT_DAMAGE : SKATE_KID_SKATE_SHOT_DAMAGE); Creature_Shoot( item, &info, &m_KidGun2, head, item->current_anim_state == SKATE_KID_STATE_SHOOT_1 ? SKATE_KID_STOP_SHOT_DAMAGE : SKATE_KID_SKATE_SHOT_DAMAGE); kid->flags = 1; } if (kid->mood == MOOD_ESCAPE || info.distance < SKATE_KID_TOO_CLOSE) { item->required_anim_state = SKATE_KID_STATE_SKATE; } break; } } Creature_Head(item, head); Creature_Animate(item_num, angle, 0); if (p->skateboard_item_num != NO_ITEM) { ITEM *const skateboard_item = Item_Get(p->skateboard_item_num); skateboard_item->pos = item->pos; skateboard_item->rot = item->rot; skateboard_item->status = item->status; Item_UpdateRoom(p->skateboard_item_num, item->room_num); const int16_t relative_anim = Item_GetRelativeAnim(item); const int16_t relative_frame = Item_GetRelativeFrame(item); Item_SwitchToObjAnim( skateboard_item, relative_anim, relative_frame, O_SKATEBOARD); } } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->priv_size = sizeof(M_PRIV); obj->shadow_size = UNIT_SHADOW / 2; obj->hit_points = SKATE_KID_HITPOINTS; obj->radius = SKATE_KID_RADIUS; obj->smartness = SKATE_KID_SMARTNESS; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_anim = true; obj->save_flags = true; Object_GetBone(obj, 0)->rot.y = true; if (!Object_Get(O_SKATEBOARD)->loaded) { LOG_WARNING( "Skateboard object (%d) is not loaded and so will not be drawn.", O_SKATEBOARD); } } static void M_SetupSkateboard(OBJECT *const obj) { obj->control_func = nullptr; } REGISTER_OBJECT(O_SKATEKID, M_Setup) REGISTER_OBJECT(O_SKATEBOARD, M_SetupSkateboard) ================================================ FILE: src/trx/game/objects/creatures/skidoo_driver.c ================================================ #include #include #include #include #include #include #include #include #include #define SKIDOO_DRIVER_MIN_TURN (SKIDOO_MAX_TURN / 3) // = 364 #define SKIDOO_DRIVER_TARGET_ANGLE (DEG_1 * 15) // = 2730 #define SKIDOO_DRIVER_WAIT_RANGE SQUARE(WALL_L * 4) // = 0x1000000 #define SKIDOO_DRIVER_SHOT_DAMAGE 10 #define SKIDOO_DRIVER_LARA_DAMAGE 50 typedef enum { // clang-format off SKIDOO_DRIVER_STATE_EMPTY = 0, SKIDOO_DRIVER_STATE_WAIT = 1, SKIDOO_DRIVER_STATE_MOVING = 2, SKIDOO_DRIVER_STATE_START_LEFT = 3, SKIDOO_DRIVER_STATE_START_RIGHT = 4, SKIDOO_DRIVER_STATE_LEFT = 5, SKIDOO_DRIVER_STATE_RIGHT = 6, SKIDOO_DRIVER_STATE_DEATH = 7, // clang-format on } SKIDOO_DRIVER_STATE; typedef enum { SKIDOO_DRIVER_ANIM_DEATH = 10, } SKIDOO_DRIVER_ANIM; typedef struct { int16_t skidoo_item_num; } M_PRIV; static void M_KillDriver(ITEM *const driver_item) { const int32_t driver_item_num = Item_GetIndex(driver_item); Item_RemoveActive(driver_item_num); driver_item->collidable = 0; driver_item->flags |= IF_ONE_SHOT; driver_item->hit_points = 0; } static void M_MakeMountable(ITEM *const skidoo_item) { if (skidoo_item->status == IS_INVISIBLE) { return; } const int32_t skidoo_item_num = Item_GetIndex(skidoo_item); LOT_DisableBaddieAI(skidoo_item_num); skidoo_item->object_id = O_SKIDOO_FAST; skidoo_item->status = IS_DEACTIVATED; Skidoo_Initialise(skidoo_item_num); SKIDOO_INFO *const skidoo_data = skidoo_item->priv; skidoo_data->track_mesh = SKIDOO_GUN_MESH; } static void M_ControlDead(ITEM *const driver_item, ITEM *const skidoo_item) { if (driver_item->current_anim_state == SKIDOO_DRIVER_STATE_DEATH) { Item_Animate(driver_item); } else { driver_item->pos.x = skidoo_item->pos.x; driver_item->pos.y = skidoo_item->pos.y; driver_item->pos.z = skidoo_item->pos.z; driver_item->rot.y = skidoo_item->rot.y; driver_item->room_num = skidoo_item->room_num; Item_SwitchToAnim(driver_item, SKIDOO_DRIVER_ANIM_DEATH, 0); driver_item->current_anim_state = SKIDOO_DRIVER_STATE_DEATH; Carrier_TestItemDrops(Item_GetIndex(skidoo_item)); LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->target == skidoo_item) { lara->target = nullptr; } } switch (skidoo_item->current_anim_state) { case SKIDOO_DRIVER_STATE_MOVING: case SKIDOO_DRIVER_STATE_WAIT: skidoo_item->goal_anim_state = SKIDOO_DRIVER_STATE_WAIT; break; default: skidoo_item->goal_anim_state = SKIDOO_DRIVER_STATE_MOVING; break; } } static int16_t M_ControlAlive(ITEM *const driver_item, ITEM *const skidoo_item) { CREATURE *const driver_data = skidoo_item->creature_data; AI_INFO info; Creature_AIInfo(skidoo_item, &info); Creature_Mood(skidoo_item, &info, true); int16_t angle = Creature_Turn(skidoo_item, SKIDOO_MAX_TURN / 2); switch (skidoo_item->current_anim_state) { case SKIDOO_DRIVER_STATE_WAIT: if (driver_data->mood != MOOD_BORED && (ABS(info.angle) >= SKIDOO_DRIVER_TARGET_ANGLE || info.distance >= SKIDOO_DRIVER_WAIT_RANGE)) { skidoo_item->goal_anim_state = SKIDOO_DRIVER_STATE_MOVING; } break; case SKIDOO_DRIVER_STATE_MOVING: if (driver_data->mood == MOOD_BORED) { skidoo_item->goal_anim_state = SKIDOO_DRIVER_STATE_WAIT; } else if ( ABS(info.angle) < SKIDOO_DRIVER_TARGET_ANGLE && info.distance < SKIDOO_DRIVER_WAIT_RANGE) { skidoo_item->goal_anim_state = SKIDOO_DRIVER_STATE_WAIT; } else if (angle < -SKIDOO_DRIVER_MIN_TURN) { skidoo_item->goal_anim_state = SKIDOO_DRIVER_STATE_START_LEFT; } else if (angle > SKIDOO_DRIVER_MIN_TURN) { skidoo_item->goal_anim_state = SKIDOO_DRIVER_STATE_START_RIGHT; } break; case SKIDOO_DRIVER_STATE_START_LEFT: case SKIDOO_DRIVER_STATE_LEFT: if (angle >= -SKIDOO_DRIVER_MIN_TURN) { skidoo_item->goal_anim_state = SKIDOO_DRIVER_STATE_MOVING; } else { skidoo_item->goal_anim_state = SKIDOO_DRIVER_STATE_LEFT; } break; case SKIDOO_DRIVER_STATE_START_RIGHT: case SKIDOO_DRIVER_STATE_RIGHT: if (angle >= -SKIDOO_DRIVER_MIN_TURN) { skidoo_item->goal_anim_state = SKIDOO_DRIVER_STATE_MOVING; } else { skidoo_item->goal_anim_state = SKIDOO_DRIVER_STATE_LEFT; } break; } if (driver_item->current_anim_state != SKIDOO_DRIVER_STATE_DEATH) { const ITEM *const lara_item = Lara_GetItem(); if (driver_data->flags == 0 && ABS(info.angle) < SKIDOO_DRIVER_TARGET_ANGLE && lara_item->hit_points > 0) { const int32_t damage = Lara_Vehicle_IsMounted() ? SKIDOO_DRIVER_SHOT_DAMAGE : SKIDOO_DRIVER_LARA_DAMAGE; const bool left_targetable = Creature_Shoot( skidoo_item, &info, &(CREATURE_GUN) { .muzzle = g_Skidoo_RightGun, }, 0, damage); const bool right_targetable = Creature_Shoot( skidoo_item, &info, &(CREATURE_GUN) { .muzzle = g_Skidoo_LeftGun, }, 0, damage); if (left_targetable || right_targetable) { driver_data->flags = 5; } } if (driver_data->flags != 0) { Sound_Effect(SFX_LARA_UZI_FIRE, &skidoo_item->pos, SPM_NORMAL); driver_data->flags--; } } return angle; } static void M_Initialise(const int16_t item_num) { ITEM *const skidoo_driver = Item_Get(item_num); M_PRIV *const p = skidoo_driver->priv; const int16_t skidoo_item_num = Item_CreateLevelItem(); ASSERT(skidoo_item_num != NO_ITEM); ITEM *const skidoo = Item_Get(skidoo_item_num); skidoo->object_id = O_SKIDOO_ARMED; skidoo->pos.x = skidoo_driver->pos.x; skidoo->pos.y = skidoo_driver->pos.y; skidoo->pos.z = skidoo_driver->pos.z; skidoo->rot.y = skidoo_driver->rot.y; skidoo->room_num = skidoo_driver->room_num; skidoo->flags = IF_ONE_SHOT; skidoo->shade.value_1 = -1; Item_Initialise(skidoo_item_num); p->skidoo_item_num = skidoo_item_num; } static void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage) { if (stage == SAVEGAME_STAGE_AFTER_LOAD) { if (item->status == IS_DEACTIVATED) { item->hit_points = 0; const M_PRIV *const p = item->priv; const int16_t skidoo_num = p->skidoo_item_num; ITEM *const skidoo = Item_Get(skidoo_num); skidoo->object_id = O_SKIDOO_FAST; Skidoo_Initialise(skidoo_num); } } } static void M_Control(const int16_t driver_item_num) { ITEM *const driver_item = Item_Get(driver_item_num); const M_PRIV *const p = driver_item->priv; const int16_t skidoo_item_num = p->skidoo_item_num; ITEM *const skidoo_item = Item_Get(skidoo_item_num); if (skidoo_item->creature_data == nullptr) { LOT_EnableBaddieAI(skidoo_item_num, true); skidoo_item->status = IS_ACTIVE; } CREATURE *const driver_data = skidoo_item->creature_data; int16_t angle = 0; if (skidoo_item->hit_points <= 0) { M_ControlDead(driver_item, skidoo_item); } else { angle = M_ControlAlive(driver_item, skidoo_item); } if (skidoo_item->current_anim_state == SKIDOO_DRIVER_STATE_WAIT) { driver_data->head_rotation = 0; Sound_Effect(SFX_SKIDOO_IDLE, &skidoo_item->pos, SPM_NORMAL); } else { driver_data->head_rotation = driver_data->head_rotation == 1 ? 2 : 1; if (skidoo_item->status != IS_INVISIBLE) { Skidoo_DoSnowEffect(skidoo_item); } const int32_t pitch_delta = (SKIDOO_MAX_SPEED - skidoo_item->speed) * 100; const int32_t pitch = (SOUND_DEFAULT_PITCH - pitch_delta) << 8; Sound_Effect(SFX_SKIDOO_MOVING, &skidoo_item->pos, SPM_PITCH | pitch); } Creature_Animate(skidoo_item_num, angle, 0); if (driver_item->current_anim_state == SKIDOO_DRIVER_STATE_DEATH) { if (driver_item->status == IS_DEACTIVATED && skidoo_item->speed == 0 && skidoo_item->fall_speed == 0) { M_KillDriver(driver_item); M_MakeMountable(skidoo_item); } } else { driver_item->pos.x = skidoo_item->pos.x; driver_item->pos.y = skidoo_item->pos.y; driver_item->pos.z = skidoo_item->pos.z; driver_item->rot.y = skidoo_item->rot.y; Item_UpdateRoom(driver_item_num, skidoo_item->room_num); const int16_t anim_num = Item_GetRelativeObjAnim(skidoo_item, O_SKIDOO_ARMED); const int16_t frame_num = Item_GetRelativeFrame(skidoo_item); Item_SwitchToAnim(driver_item, anim_num, frame_num); } } static bool M_IsTargetable(const ITEM *const item) { return false; } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->handle_save_func = M_HandleSave; obj->control_func = M_Control; obj->priv_size = sizeof(M_PRIV); obj->is_targetable_func = M_IsTargetable; obj->hit_points = 1; obj->save_position = true; obj->save_flags = true; obj->save_anim = true; } int16_t SkidooDriver_GetSkidooItemNum(const ITEM *const driver_item) { const M_PRIV *const p = driver_item->priv; return p->skidoo_item_num; } REGISTER_OBJECT(O_SKIDOO_DRIVER, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/skidoo_driver.h ================================================ #pragma once #include #define SKIDOO_DRIVER_HITPOINTS 100 int16_t SkidooDriver_GetSkidooItemNum(const ITEM *driver_item); ================================================ FILE: src/trx/game/objects/creatures/sophia.c ================================================ #include "sophia_internal.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // clang-format off #define M_SMALL_FLASH 10 #define M_RIGHT_PRONG 0 #define M_CENTER_PRONG 1 #define M_LEFT_PRONG 2 #define M_VAULT_SHIFT 96 #define M_AWARE_DISTANCE SQUARE(WALL_L) #define M_WALK_TURN (4 * DEG_1) #define M_RUN_TURN (7 * DEG_1) #define M_WALK_RANGE SQUARE(WALL_L) #define M_LAUGH_CHANCE 0x100 #define M_BIG_ZAP_TIMER 600 // clang-format on typedef enum { M_STATE_NULL, M_STATE_STAND, M_STATE_WALK, M_STATE_RUN, M_STATE_SUMMON, M_STATE_BIG_ZAP, M_STATE_DEATH, M_STATE_LAUGH, M_STATE_LITTLE_ZAP, M_STATE_VAULT_2, M_STATE_VAULT_3, M_STATE_VAULT_4, M_STATE_GO_DOWN } M_STATE; typedef enum { // clang-format off M_ANIM_STAND_TO_SUMMON = 1, M_ANIM_SUMMON = 2, M_ANIM_UP_2 = 9, M_ANIM_UP_3 = 18, M_ANIM_UP_4 = 15, M_ANIM_DEATH = 17, M_ANIM_DOWN_4 = 21, // clang-format on } M_ANIM; typedef struct { XYZ_16 pos; RGB_888 sub; RGB_888 color; } M_SHIELD_POINT; typedef struct { bool dropped_item; uint8_t ring_count; int16_t explode_count; bool dead; bool charged; int16_t death_counter; int16_t hp_counter; int16_t fuse_box_num; uint8_t wand_glow_phase; bool knockback_active; M_SHIELD_POINT shield[5][8]; int32_t final_height; } M_PRIV; static const BITE m_WandBite[3] = { { .pos = { .x = 16, .y = 56, .z = 356 }, .mesh_num = 10 }, { .pos = { .x = -28, .y = 48, .z = 304 }, .mesh_num = 10 }, { .pos = { .x = -72, .y = 48, .z = 356 }, .mesh_num = 10 }, }; static const int32_t m_Heights[5] = { -1536, -1280, -832, -384, 0 }; static const int32_t m_Dist[5] = { 200, 400, 500, 500, 475 }; static const int32_t m_DDist[5] = { 1600, 5600, 6400, 5600, 1600 }; static const int32_t m_DHeights1[5] = { -7680, -4224, -768, 2688, 6144 }; static const int32_t m_DHeights2[5] = { -1536, -1152, -768, -384, 0 }; static int32_t m_DeathDist[5] = {}; static int32_t m_DeathHeights[5] = {}; static void M_ExplodeLondonBoss(const ITEM *const item) { M_PRIV *const p = item->priv; if (p->explode_count == 1 || p->explode_count == 15 || p->explode_count == 25 || p->explode_count == 35 || p->explode_count == 45 || p->explode_count == 55) { const XYZ_32 pos = { .x = item->pos.x + (Random_GetDraw() & 0x3FF) - 512, .y = item->pos.y - (Random_GetDraw() & 0x3FF) - 256, .z = item->pos.z + (Random_GetDraw() & 0x3FF) - 512, }; FX_RING *const ring = FX_Ring_GetRing(FX_RING_TYPE_BLAST, p->ring_count); if (ring != nullptr) { ring->pos = pos; ring->on = 3; FX_Ring_Sync(ring); p->ring_count++; } Sparks_TriggerExplosionSparks(pos, 3, -2, 2, 0); for (int32_t i = 0; i < 2; i++) { Sparks_TriggerExplosionSparks(pos, 3, -1, 2, 0); } Sound_Effect(SFX_BLAST_CIRCLE, &item->pos, 0x800000 | SPM_PITCH); } for (int32_t i = 0; i < 5; i++) { if (p->explode_count < 128) { m_DeathDist[i] = (m_DDist[i] >> 4) + ((p->explode_count * m_DDist[i]) >> 7); m_DeathHeights[i] = m_DHeights2[i] + ((p->explode_count * (m_DHeights1[i] - m_DHeights2[i])) >> 7); } } for (int32_t i = 0; i < 5; i++) { const int32_t y = m_DeathHeights[i]; const int32_t dist = m_DeathDist[i]; const int32_t time4 = Output_GetTimeInGame() * 4; int32_t angle = (time4 & 0x3F) << 3; for (int32_t j = 0; j < 8; j++) { M_SHIELD_POINT *const shield = &p->shield[i][j]; shield->pos.x = (dist * Math_Sin(angle << 4)) >> 13; shield->pos.y = y; shield->pos.z = (dist * Math_Cos(angle << 4)) >> 13; if (i != 0 && i != 4 && p->explode_count < 64) { const int32_t r = Random_GetDraw() & 0x3F; const int32_t g = (Random_GetDraw() & 0x1F) + 224; const int32_t b = (g >> 2) + (Random_GetDraw() & 0x3F); shield->color = (RGB_888) { .r = ((64 - p->explode_count) * r) >> 6, .g = ((64 - p->explode_count) * g) >> 6, .b = ((64 - p->explode_count) * b) >> 6, }; } else { shield->color = COLOR_RGB_888_BLACK; } angle = (angle + 512) & 0xFFF; } } } static void M_Die(const int16_t item_num) { ITEM *const item = Item_Get(item_num); item->hit_points = 0; item->collidable = false; Item_Kill(item_num); LOT_DisableBaddieAI(item_num); item->flags |= IF_INVISIBLE; } static bool M_KnockBackCollision(const FX_RING *const ring) { ITEM *const lara_item = Lara_GetItem(); if (Lara_GetLaraInfo()->water_status == LWS_CHEAT) { return false; } const XYZ_32 delta = { .x = lara_item->pos.x - ring->pos.x, .y = 0, .z = lara_item->pos.z - ring->pos.z, }; if (XYZ_32_GetLength2(delta) >= SQUARE(ring->radius)) { return false; } Lara_TakeDamage(200, true); const int16_t angle = Math_Atan(delta.z, delta.x); const int16_t dy = lara_item->rot.y - angle; if (ABS(dy) >= DEG_90) { lara_item->rot.y = angle + DEG_180; lara_item->speed = -75; } else { lara_item->rot.y = angle; lara_item->speed = 75; } lara_item->gravity = true; lara_item->fall_speed = -50; lara_item->rot.x = 0; lara_item->rot.z = 0; Item_SwitchToAnim(lara_item, LA(LA_FALL_START), 0); lara_item->current_anim_state = LS_JUMP_FORWARD; lara_item->goal_anim_state = LS_JUMP_FORWARD; Sparks_TriggerExplosionSparks( lara_item->pos, 3, -2, 2, lara_item->room_num); for (int32_t i = 0; i < 3; i++) { Sophia_TriggerPlasmaBall( 2, lara_item->pos, lara_item->room_num, Random_GetControl() << 1); } return true; } static int32_t M_FindFinalHeight(void) { int32_t height = NO_HEIGHT; for (int16_t i = 0; i < Item_GetLevelCount(); i++) { const ITEM *const item = Item_Get(i); if (item->object_id != O_AI_AMBUSH) { continue; } if (height == NO_HEIGHT || item->pos.y < height) { height = item->pos.y; } } return height; } static int16_t M_FindFuseBox(const ITEM *const item) { int16_t fuse_box_num = NO_ITEM; int32_t best_dist = INT32_MAX; for (int16_t i = 0; i < Item_GetLevelCount(); i++) { const ITEM *const other_item = Item_Get(i); if (other_item->object_id != O_FUSE_BOX) { continue; } const int32_t dist = XYZ_32_GetLength2((XYZ_32) { .x = other_item->pos.x - item->pos.x, .y = 0, .z = other_item->pos.z - item->pos.z, }); if (dist < best_dist) { best_dist = dist; fuse_box_num = i; } } return fuse_box_num; } static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "dropped_item", &p->dropped_item)); JSON_SHOULD(JSON_READ(io, "ring_count", &p->ring_count)); JSON_SHOULD(JSON_READ(io, "explode_count", &p->explode_count)); JSON_SHOULD(JSON_READ(io, "dead", &p->dead)); JSON_SHOULD(JSON_READ(io, "charged", &p->charged)); JSON_SHOULD(JSON_READ(io, "death_counter", &p->death_counter)); JSON_SHOULD(JSON_READ(io, "hp_counter", &p->hp_counter)); JSON_SHOULD(JSON_READ(io, "fuse_box_num", &p->fuse_box_num)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "dropped_item", p->dropped_item); JSONW_WRITE(io, "ring_count", p->ring_count); JSONW_WRITE(io, "explode_count", p->explode_count); JSONW_WRITE(io, "dead", p->dead); JSONW_WRITE(io, "charged", p->charged); JSONW_WRITE(io, "death_counter", p->death_counter); JSONW_WRITE(io, "hp_counter", p->hp_counter); JSONW_WRITE(io, "fuse_box_num", p->fuse_box_num); } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; p->dropped_item = false; p->dead = 0; p->ring_count = 0; p->explode_count = 0; p->final_height = M_FindFinalHeight(); p->fuse_box_num = M_FindFuseBox(item); for (int32_t i = 0; i < 5; i++) { const int32_t dist = m_Dist[i]; int32_t angle = 0; for (int32_t j = 0; j < 8; j++) { M_SHIELD_POINT *const shield = &p->shield[i][j]; shield->pos.x = (dist * Math_Sin(angle << 4)) >> 13; shield->pos.y = m_Heights[i]; shield->pos.z = (dist * Math_Cos(angle << 4)) >> 13; shield->color = COLOR_RGB_888_BLACK; angle += 512; } } } static bool M_GunHit( ITEM *const item, const GAME_VECTOR *const start, const GAME_VECTOR *const hit_pos, int32_t *const damage) { if (damage != nullptr) { *damage = 0; } return true; } static bool M_ShouldSpawnBlood(const ITEM *const item) { return false; } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; if (p->death_counter == 0 && p->fuse_box_num != NO_ITEM) { const ITEM *const fuse_box = Item_Get(p->fuse_box_num); if (!Item_TestAnimEqual(fuse_box, 0)) { Stats_AddKill(); p->death_counter = 1; } } ITEM *const lara_item = Lara_GetItem(); CREATURE *const creature = item->creature_data; int16_t angle = 0; int16_t tilt = 0; int16_t head = 0; int16_t torso_x = 0; int16_t torso_y = 0; if (p->death_counter != 0) { if (p->death_counter == 1) { item->hit_points = 0; } RGB_888 color; int32_t falloff; if (p->death_counter < 12) { falloff = (Random_GetControl() & 1) - (p->death_counter << 1) + 25; color = (RGB_888) { .r = (Random_GetControl() & 0x3F) - (p->death_counter << 3) + 128, .g = 256 - (p->death_counter << 3), .b = 255, }; } else { falloff = (Random_GetControl() & 3) + 8; color = (RGB_888) { .r = 0, .g = (Random_GetControl() & 0x3F) + 64, .b = (Random_GetControl() & 0x3F) + 128, }; } Output_AddDynamicLightRGB(item->pos, falloff, color); } XYZ_32 points[3]; for (int32_t i = 0; i < 3; i++) { points[i] = m_WandBite[i].pos; Collide_GetJointAbsPosition(item, &points[i], m_WandBite[i].mesh_num); } if (item->hit_points <= 0) { if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, M_ANIM_DEATH, 0); item->current_anim_state = M_STATE_DEATH; } if (Item_TestFrameEqual(item, -2)) { item->mesh_bits = 0; item->frame_num = Item_GetAnim(item)->frame_end - 1; if (p->explode_count == 0) { p->ring_count = 0; for (int32_t i = 0; i < 6; i++) { FX_RING *const ring = FX_Ring_GetRing(FX_RING_TYPE_BLAST, i); ring->on = 0; ring->life = 32; ring->radius = 512; ring->speed = 128 + (i << 5); ring->rot.x = ((Random_GetControl() & 0x1FF) - 256) << 4; ring->rot.z = ((Random_GetControl() & 0x1FF) - 256) << 4; FX_Ring_Sync(ring); } if (!p->dropped_item) { Carrier_TestItemDrops(item_num); p->dropped_item = true; } } if (p->explode_count < 256) { p->explode_count++; } if (p->explode_count > 128 && p->ring_count == 6 && FX_Ring_GetRing(FX_RING_TYPE_BLAST, 5)->life == 0) { M_Die(item_num); p->dead = 1; } else { M_ExplodeLondonBoss(item); } return; } } else { if (item->ai_bits) { Creature_GetAITarget(creature); } AI_INFO info; Creature_AIInfo(item, &info); AI_INFO lara_info; if (creature->enemy == lara_item) { lara_info.angle = info.angle; lara_info.x_angle = info.x_angle; lara_info.distance = info.distance; } else { int32_t x = lara_item->pos.x - item->pos.x; int32_t y = item->pos.y - lara_item->pos.y; int32_t z = lara_item->pos.z - item->pos.z; lara_info.angle = Math_Atan(z, x) - item->rot.y; lara_info.distance = SQUARE(x) + SQUARE(z); if (ABS(x) <= ABS(z)) { z = z + (x >> 1); } else { z = x + (z >> 1); } lara_info.x_angle = Math_Atan(z, y); } Creature_Mood(item, &info, true); angle = Creature_Turn(item, creature->maximum_turn); ITEM *const enemy = creature->enemy; creature->enemy = lara_item; if (item->hit_status || lara_info.distance < M_AWARE_DISTANCE || Creature_CanSeeEnemy(item, &lara_info) || lara_item->pos.y < item->pos.y) { Creature_AlertAllGuards(item_num); } creature->enemy = enemy; if (lara_item->pos.y < item->pos.y) { creature->hurt_by_lara = 1; } if (item->timer > 0) { item->timer--; } item->hit_points = 300; switch (item->current_anim_state) { case M_STATE_LAUGH: if (ABS(lara_info.angle) < M_WALK_TURN) { item->rot.y += lara_info.angle; } else if (lara_info.angle >= 0) { item->rot.y += M_WALK_TURN; } else { item->rot.y -= M_WALK_TURN; } #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wimplicit-fallthrough" if (creature->alerted) { item->goal_anim_state = M_STATE_STAND; break; } #pragma GCC diagnostic pop case M_STATE_STAND: creature->flags = 0; creature->maximum_turn = 0; if (creature->reached_goal) { creature->reached_goal = 0; item->ai_bits |= AI_AMBUSH; item->ai_tag += 0x2000; } head = lara_info.angle; if (item->ai_bits & AI_GUARD) { if ((lara_info.angle < -0x3000 || lara_info.angle > 0x3000) && item->pos.y > p->final_height) { item->goal_anim_state = M_STATE_WALK; creature->maximum_turn = M_WALK_TURN; } } else if ( (item->pos.y <= p->final_height || item->pos.y < lara_item->pos.y) && !(Random_GetControl() & 0xF) && !p->charged && item->timer) { item->goal_anim_state = M_STATE_LAUGH; } else if ( creature->reached_goal || lara_item->pos.y > item->pos.y || item->pos.y <= p->final_height) { if (p->charged) { item->goal_anim_state = M_STATE_BIG_ZAP; } else if (item->timer) { item->goal_anim_state = M_STATE_LITTLE_ZAP; } else { item->goal_anim_state = M_STATE_SUMMON; } } else if (item->ai_bits & AI_PATROL_1) { item->goal_anim_state = M_STATE_WALK; } else if ( creature->mood == MOOD_ESCAPE || item->pos.y > lara_item->pos.y) { item->goal_anim_state = M_STATE_RUN; } else if ( creature->mood == MOOD_BORED || (item->ai_bits & AI_FOLLOW && (creature->reached_goal || lara_info.distance > 0x400000))) { if (item->required_anim_state) { item->goal_anim_state = item->required_anim_state; } else if (info.ahead) { item->goal_anim_state = M_STATE_STAND; } else { item->goal_anim_state = M_STATE_RUN; } } else if (info.bite && info.distance < M_WALK_RANGE) { item->goal_anim_state = M_STATE_WALK; } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_WALK: head = lara_info.angle; creature->flags = 0; creature->maximum_turn = M_WALK_TURN; if (item->ai_bits & AI_GUARD || (creature->reached_goal && !(item->ai_bits & AI_FOLLOW))) { item->goal_anim_state = M_STATE_STAND; } else if (item->ai_bits & AI_PATROL_1) { item->goal_anim_state = M_STATE_WALK; head = 0; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if (creature->mood == MOOD_BORED) { if (Random_GetControl() < M_LAUGH_CHANCE) { item->required_anim_state = M_STATE_LAUGH; item->goal_anim_state = M_STATE_STAND; } } else if (info.distance > M_WALK_RANGE) { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_RUN: if (info.ahead) { head = info.angle; } creature->maximum_turn = M_RUN_TURN; tilt = angle >> 1; if (item->ai_bits & AI_GUARD || (creature->reached_goal && !(item->ai_bits & AI_FOLLOW))) { item->goal_anim_state = M_STATE_STAND; } else if (creature->mood != MOOD_ESCAPE) { if (item->ai_bits & AI_FOLLOW && (creature->reached_goal || lara_info.distance > 0x400000)) { item->goal_anim_state = M_STATE_STAND; } else if (creature->mood == MOOD_BORED) { item->goal_anim_state = M_STATE_WALK; } else if (info.ahead && info.distance < M_WALK_RANGE) { item->goal_anim_state = M_STATE_WALK; } } break; case M_STATE_SUMMON: head = lara_info.angle; if (creature->reached_goal) { creature->reached_goal = 0; item->ai_bits = AI_AMBUSH; item->ai_tag += 0x2000; } if (Item_TestAnimEqual(item, M_ANIM_STAND_TO_SUMMON)) { if (Item_TestFrameEqual(item, 0)) { p->hp_counter = item->hit_points; item->timer = M_BIG_ZAP_TIMER; } else if ( item->hit_status && item->goal_anim_state != M_STATE_STAND) { Sound_StopEffect(SFX_SOPHIA_SUMMON); Sound_Effect(SFX_SOPHIA_TAKE_HIT, &item->pos, SPM_NORMAL); Sound_Effect(SFX_SOPHIA_SUMMON_NOT, &item->pos, SPM_NORMAL); item->goal_anim_state = M_STATE_STAND; } } else if ( Item_TestAnimEqual(item, M_ANIM_SUMMON) && Item_TestFrameEqual(item, -1)) { p->charged = true; } if (ABS(lara_info.angle) < M_WALK_TURN) { item->rot.y += lara_info.angle; } else if (lara_info.angle >= 0) { item->rot.y += M_WALK_TURN; } else { item->rot.y -= M_WALK_TURN; } const int32_t time4 = Output_GetTimeInGame() * 4; if ((time4 & 7) == 0) { XYZ_32 pos = { .x = item->pos.x, .y = (Random_GetControl() & 0x1FF) - 256, .z = item->pos.z, }; Sophia_TriggerLaserBolt(pos, item, 2, 0); for (int32_t i = 0; i < 6; i++) { FX_RING *const ring = FX_Ring_GetRing(FX_RING_TYPE_SUMMON, i); if (ring->on == 0) { const int32_t r = Random_GetControl() & 0x3FF; ring->on = 3; ring->life = 64; ring->speed = (Random_GetControl() & 0xF) + 16; ring->pos.x = item->pos.x; ring->pos.y = item->pos.y - r + 128; ring->pos.z = item->pos.z; ring->rot.x = 16 * ((Random_GetControl() & 0x1FF) - 256); ring->rot.z = 16 * ((Random_GetControl() & 0x1FF) - 256); ring->radius = 2048 - ABS(r - 512); FX_Ring_Sync(ring); break; } } } creature->maximum_turn = 0; break; case M_STATE_BIG_ZAP: if (creature->reached_goal) { creature->reached_goal = 0; item->ai_bits = AI_AMBUSH; item->ai_tag += 0x2000; } p->charged = false; if (ABS(lara_info.angle) < M_WALK_TURN) { item->rot.y += lara_info.angle; } else if (lara_info.angle >= 0) { item->rot.y += M_WALK_TURN; } else { item->rot.y -= M_WALK_TURN; } creature->maximum_turn = 0; torso_x = lara_info.x_angle; torso_y = lara_info.angle; if (Item_TestFrameEqual(item, 36)) { Sophia_TriggerLaserBolt( points[M_RIGHT_PRONG], item, 0, item->rot.y + 512); Sophia_TriggerLaserBolt( points[M_CENTER_PRONG], item, 1, item->rot.y); Sophia_TriggerLaserBolt( points[M_LEFT_PRONG], item, 0, item->rot.y - 512); } break; case M_STATE_LITTLE_ZAP: if (creature->reached_goal) { creature->reached_goal = 0; item->ai_bits = AI_AMBUSH; item->ai_tag += 0x2000; } if (ABS(lara_info.angle) < M_WALK_TURN) { item->rot.y += lara_info.angle; } else if (lara_info.angle >= 0) { item->rot.y += M_WALK_TURN; } else { item->rot.y -= M_WALK_TURN; } creature->maximum_turn = 0; torso_x = lara_info.x_angle; torso_y = lara_info.angle; if (Item_TestFrameEqual(item, 14)) { Sophia_TriggerLaserBolt( points[M_RIGHT_PRONG], item, 0, item->rot.y + 512); Sophia_TriggerLaserBolt( points[M_LEFT_PRONG], item, 0, item->rot.y - 512); } break; } } Creature_Tilt(item, tilt); Creature_Joint(item, 0, torso_y); Creature_Joint(item, 1, torso_x); Creature_Joint(item, 2, head); if ((item->current_anim_state >= M_STATE_VAULT_2 && item->current_anim_state <= M_STATE_GO_DOWN) || item->current_anim_state == M_STATE_DEATH) { creature->maximum_turn = 0; Creature_Animate(item_num, angle, 0); } else { switch (Creature_Vault(item_num, angle, 2, M_VAULT_SHIFT)) { case -4: creature->maximum_turn = 0; Item_SwitchToAnim(item, M_ANIM_DOWN_4, 0); item->current_anim_state = M_STATE_GO_DOWN; break; case 2: creature->maximum_turn = 0; Item_SwitchToAnim(item, M_ANIM_UP_2, 0); item->current_anim_state = M_STATE_VAULT_2; break; case 3: creature->maximum_turn = 0; Item_SwitchToAnim(item, M_ANIM_UP_3, 0); item->current_anim_state = M_STATE_VAULT_3; break; case 4: creature->maximum_turn = 0; Item_SwitchToAnim(item, M_ANIM_UP_4, 0); item->current_anim_state = M_STATE_VAULT_4; break; } } int32_t g = (Random_GetControl() & 7) + ABS(Math_Sin(p->wand_glow_phase << 10) >> 9); CLAMPG(g, 31); g <<= 3; Output_AddDynamicLightRGB( points[M_CENTER_PRONG], M_SMALL_FLASH, (RGB_888) { 0, g >> 1, g >> 2 }); p->wand_glow_phase = (p->wand_glow_phase + 1) & 0x3F; if (item->hit_points > 0 && !p->knockback_active && lara_item->hit_points > 0) { const XYZ_32 delta = { .x = lara_item->pos.x - item->pos.x, .y = lara_item->pos.y - item->pos.y - 256, .z = lara_item->pos.z - item->pos.z, }; if (XYZ_32_GetLength(delta) < 2816) { p->knockback_active = true; FX_Ring_SpawnKnockBack(item->pos); } } else if (p->knockback_active) { const FX_RING *const ring = FX_Ring_PeekRing(FX_RING_TYPE_KNOCKBACK, 1); if (ring != nullptr && ring->on != 0 && ring->speed >= 0 && M_KnockBackCollision(ring)) { FX_Ring_BounceKnockBack(); } if (!FX_Ring_IsRingActive(FX_RING_TYPE_KNOCKBACK)) { p->knockback_active = false; } } if (item->hit_points <= 0 && p->explode_count == 0) { Lara_Electricity_UpdatePoints(); } } static void M_DrawShield(const ITEM *const item) { const M_PRIV *const p = item->priv; const int32_t time4 = Output_GetTimeInGame() * 4; const int32_t sprite_base = Object_Get(O_EXPLOSION_1)->mesh_idx; for (int32_t band = 0; band < 4; band++) { const int32_t sprite_idx = sprite_base + 18 + ((band + (time4 >> 3)) & 7); for (int32_t j = 0; j < 8; j++) { const int32_t j2 = (j == 7) ? 0 : (j + 1); const M_SHIELD_POINT *const s00 = &p->shield[band][j]; const M_SHIELD_POINT *const s01 = &p->shield[band][j2]; const M_SHIELD_POINT *const s10 = &p->shield[band + 1][j]; const M_SHIELD_POINT *const s11 = &p->shield[band + 1][j2]; if (((s00->color.r | s00->color.g | s00->color.b | s01->color.r | s01->color.g | s01->color.b | s11->color.r | s11->color.g | s11->color.b | s10->color.r | s10->color.g | s10->color.b) == 0U)) { continue; } const XYZ_32 world_pos[4] = { { item->pos.x + s00->pos.x, item->pos.y + s00->pos.y, item->pos.z + s00->pos.z, }, { item->pos.x + s01->pos.x, item->pos.y + s01->pos.y, item->pos.z + s01->pos.z, }, { item->pos.x + s11->pos.x, item->pos.y + s11->pos.y, item->pos.z + s11->pos.z, }, { item->pos.x + s10->pos.x, item->pos.y + s10->pos.y, item->pos.z + s10->pos.z, }, }; const RGBA_8888 color[4] = { { s00->color.r, s00->color.g, s00->color.b, 255 }, { s01->color.r, s01->color.g, s01->color.b, 255 }, { s11->color.r, s11->color.g, s11->color.b, 255 }, { s10->color.r, s10->color.g, s10->color.b, 255 }, }; OutputSource_PolyFX_StageSpriteQuadWorld( sprite_idx, world_pos, color, DRAW_BLEND_ADD); } } } static bool M_Draw(const ITEM *const item) { const bool result = Object_DrawAnimatingItem(item); const M_PRIV *const p = item->priv; if (p->explode_count != 0) { M_DrawShield(item); } if (item->hit_points <= 0 && p->explode_count == 0) { Lara_Electricity_Draw(0, item); Lara_Electricity_Draw(1, item); } return result; } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->draw_func = M_Draw; obj->gun_hit_func = M_GunHit; obj->should_spawn_blood_func = M_ShouldSpawnBlood; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->lot_setup = LOT_Setup(LOT_SETUP_CLIMBER); obj->lot_setup.drop = -STEP_L * 3; obj->shadow_size = 0; obj->hit_points = 300; obj->pivot_length = 50; obj->radius = 102; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 6)->rot.x = true; Object_GetBone(obj, 6)->rot.y = true; Object_GetBone(obj, 13)->rot.y = true; } REGISTER_OBJECT(O_SOPHIA, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/sophia_internal.h ================================================ #pragma once #include #include void Sophia_TriggerPlasmaBall( int32_t type, XYZ_32 pos, int16_t room_num, int16_t angle); void Sophia_TriggerLaserBolt( XYZ_32 pos, const ITEM *item, int32_t type, int16_t angle); ================================================ FILE: src/trx/game/objects/creatures/sophia_laser_bolt.c ================================================ #include "sophia_internal.h" #include #include #include #include #include #include #include #include #include #include #define M_SPEED 384 typedef struct { int16_t light_falloff; int16_t light_phase; bool summon_bolt; int16_t summon_lifetime; } M_PRIV; static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; const XYZ_32 old_pos = item->pos; const int16_t old_room_num = item->room_num; const int32_t speed = (item->speed * Math_Cos(item->rot.x)) >> W2V_SHIFT; item->pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, speed); item->pos.y -= (item->speed * Math_Sin(item->rot.x)) >> W2V_SHIFT; if (item->speed < M_SPEED) { item->speed += (item->speed >> 3) + 2; } if (p->summon_bolt && item->speed > 192) { p->summon_lifetime++; if (p->summon_lifetime >= 16) { Item_Kill(item_num); return; } } int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); if (item->room_num != room_num) { Item_UpdateRoom(item_num, room_num); } if (!p->summon_bolt) { const bool hit = Lara_IsNearItem(&item->pos, 400); item->floor = Room_GetHeight(sector, item->pos); const int32_t c = Room_GetCeiling(sector, item->pos); if (hit || item->pos.y >= item->floor || item->pos.y <= c) { Sound_Effect(SFX_EXPLOSION_1, &item->pos, SPM_NORMAL); int32_t extras = (p->light_falloff >= 0) + 2; Sparks_TriggerExplosionSparks( old_pos, extras, -2, 2, item->room_num); for (int32_t i = 0; i < extras; i++) { Sparks_TriggerExplosionSparks( old_pos, 2, -1, 2, item->room_num); } extras++; for (int32_t i = 0; i < extras; i++) { Sophia_TriggerPlasmaBall(1, old_pos, old_room_num, item->rot.y); } if (hit) { Lara_TakeDamage(30 + ((p->light_falloff >= 0) << 9), true); } else { const ITEM *const lara_item = Lara_GetItem(); const XYZ_32 delta = { .x = lara_item->pos.x - item->pos.x, .y = lara_item->pos.y - item->pos.y - 256, .z = lara_item->pos.z - item->pos.z, }; const int32_t dist = XYZ_32_GetLength(delta); if (dist < WALL_L) { Lara_TakeDamage( (WALL_L - dist) >> (6 - 2 * (p->light_falloff >= 0)), true); } } Item_Kill(item_num); return; } } int32_t g = 255 - (Random_GetControl() & 0x3F); int32_t b = g >> 1; int32_t falloff; if (p->light_falloff < 0) { if (p->summon_bolt) { falloff = 16 - p->summon_lifetime; g = (falloff * g) >> 4; b = (falloff * b) >> 4; } falloff = -p->light_falloff; if (falloff > 10) { p->light_phase += 2; p->light_falloff++; } } else { falloff = p->light_falloff; if (falloff > 16) { p->light_phase += 4; p->light_falloff--; } } Output_AddDynamicLightRGB(item->pos, falloff, (RGB_888) { 0, g, b }); } static bool M_Draw(const ITEM *const item) { const M_PRIV *const p = item->priv; const XYZ_32 origin = item->interp.result.pos; const XYZ_16 rot = item->interp.result.rot; const int32_t beam_speed = (item->speed * Math_Cos(rot.x)) >> W2V_SHIFT; const XYZ_32 dir = { .x = (beam_speed * Math_Sin(rot.y)) >> W2V_SHIFT, .y = -((item->speed * Math_Sin(rot.x)) >> W2V_SHIFT), .z = (beam_speed * Math_Cos(rot.y)) >> W2V_SHIFT, }; Matrix_PushUnit(); Matrix_Rot16(rot); const int32_t radius = p->summon_bolt ? p->light_phase >> 1 : p->light_phase; XYZ_32 base[4] = {}; for (int32_t i = 0; i < 4; i++) { const XYZ_32 local = { .x = (i == 0 || i == 3) ? -radius : radius, .y = (i < 2) ? -radius : radius, .z = 0, }; base[i] = Matrix_MulVec32_M(g_WMatrixPtr, local); } Matrix_Pop(); const int32_t section_count = p->summon_bolt ? 3 : 6; XYZ_32 sections[6][4] = {}; uint8_t intensities[6] = {}; for (int32_t i = 0; i < 4; i++) { sections[0][i] = origin; } intensities[0] = 128; const int32_t ring_count = section_count - 2; for (int32_t i = 0; i < ring_count; i++) { const int32_t step = i + 2; const XYZ_32 center = { .x = origin.x - dir.x * step, .y = origin.y - dir.y * step, .z = origin.z - dir.z * step, }; const int32_t scale = 4 - i; for (int32_t j = 0; j < 4; j++) { sections[i + 1][j] = (XYZ_32) { .x = center.x + (base[j].x * scale) / 4, .y = center.y + (base[j].y * scale) / 4, .z = center.z + (base[j].z * scale) / 4, }; } intensities[i + 1] = 64 - (i << 4); } const int32_t end_step = p->summon_bolt ? 5 : 6; const XYZ_32 end = { .x = origin.x - dir.x * end_step, .y = origin.y - dir.y * end_step, .z = origin.z - dir.z * end_step, }; for (int32_t i = 0; i < 4; i++) { sections[section_count - 1][i] = end; } const int32_t fade = p->summon_bolt ? 16 - p->summon_lifetime : 16; for (int32_t i = 0; i < section_count - 1; i++) { int32_t c0 = intensities[i]; int32_t c1 = intensities[i + 1]; if (p->summon_bolt) { c0 = (c0 * fade) >> 4; c1 = (c1 * fade) >> 4; } const RGBA_8888 color0 = { c0 >> 2, c0, c0 >> 1, 0xC0 }; const RGBA_8888 color1 = { c1 >> 2, c1, c1 >> 1, 0xC0 }; for (int32_t j = 0; j < 4; j++) { const int32_t next = (j + 1) & 3; const XYZ_32 world_pos[4] = { sections[i][j], sections[i + 1][next], sections[i + 1][j], sections[i][next], }; const RGBA_8888 color[4] = { color0, color0, color1, color1 }; OutputSource_PolyFX_StageQuadExt( -1, world_pos, nullptr, color, VERT_FLAT_SHADED | VERT_NO_LIGHTING | VERT_NO_WIBBLE, DRAW_BLEND_ADD); const XYZ_32 world_pos_rev[4] = { world_pos[3], world_pos[2], world_pos[1], world_pos[0], }; OutputSource_PolyFX_StageQuadExt( -1, world_pos_rev, nullptr, color, VERT_FLAT_SHADED | VERT_NO_LIGHTING | VERT_NO_WIBBLE, DRAW_BLEND_ADD); } } return true; } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->draw_func = M_Draw; obj->priv_size = sizeof(M_PRIV); } void Sophia_TriggerLaserBolt( const XYZ_32 pos, const ITEM *const item, const int32_t type, const int16_t angle) { const int16_t item_num = Item_Create(); if (item_num == NO_ITEM) { return; } ITEM *const bolt = Item_Get(item_num); bolt->object_id = O_SOPHIA_LASER_BOLT; bolt->room_num = item->room_num; bolt->pos = pos; Item_Initialise(item_num); M_PRIV *const bolt_priv = bolt->priv; if (type == 2) { bolt->pos.y += item->pos.y - 384; bolt->rot.x = -pos.y << 5; bolt->rot.y = Random_GetControl() << 1; } else { const ITEM *const lara_item = Lara_GetItem(); int16_t angles[2]; Math_GetVectorAngles( lara_item->pos.x - pos.x, lara_item->pos.y - pos.y - 256, lara_item->pos.z - pos.z, angles); bolt->rot.x = angles[1]; bolt->rot.y = angle; bolt->rot.z = 0; } if (type == 1) { bolt->speed = 24; bolt_priv->light_falloff = 31; bolt_priv->light_phase = 16; } else { bolt->speed = 16; bolt_priv->light_falloff = -24; bolt_priv->light_phase = 4; if (type == 2) { bolt_priv->summon_bolt = true; } } Item_AddActive(item_num); bolt->status = IS_ACTIVE; } REGISTER_OBJECT(O_SOPHIA_LASER_BOLT, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/sophia_plasma_ball.c ================================================ #include "sophia_internal.h" #include #include #include #include #include #include #include static const uint8_t m_Falloffs[2] = { 13, 7 }; static void M_TriggerPlasmaBallFlame(const int16_t effect_num, const XYZ_32 vel) { const EFFECT *const effect = Effect_Get(effect_num); const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - effect->pos.x; const int32_t dz = lara_item->pos.z - effect->pos.z; const int32_t max_dist = 16 * WALL_L; if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 48; spark->src_color.g = 255; spark->src_color.b = (Random_GetControl() & 0x1F) + 48; spark->dst_color.r = 32; spark->dst_color.g = (Random_GetControl() & 0x3F) + 192; spark->dst_color.b = (Random_GetControl() & 0x3F) + 128; spark->fade_to_black = 8; spark->col_fade_speed = (Random_GetControl() & 3) + 12; spark->draw_type = DRAW_BLEND_ADD; spark->life = (Random_GetControl() & 7) + 24; spark->s_life = spark->life; spark->extras = 0; spark->dynamic = -1; spark->pos.x = (Random_GetControl() & 0xF) - 8; spark->pos.y = 0; spark->pos.z = (Random_GetControl() & 0xF) - 8; spark->vel.x = vel.x + (Random_GetControl() & 0xFF) - 128; spark->vel.y = vel.y; spark->vel.z = vel.z + (Random_GetControl() & 0xFF) - 128; spark->friction = 5; if (Random_GetControl() & 1) { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = Random_GetControl() & 0xFFF; if (Random_GetControl() & 1) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = (Random_GetControl() & 0xF) + 16; } } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->effect_num = effect_num; spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->scalar = 1; spark->max_y_vel = 0; spark->gravity = 0; spark->size.width = (Random_GetControl() & 0x1F) + 64; spark->size.height = spark->size.width; spark->src_size.width = spark->size.width; spark->src_size.height = spark->size.height; spark->dst_size.width = spark->size.width >> 2; spark->dst_size.height = spark->size.height >> 2; Sparks_FinishSetup(spark); } static void M_Control(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); effect->fall_speed++; const int32_t old_y = effect->pos.y; if (effect->speed > 8) { effect->speed -= 2; } if (effect->rot.x > -15360) { effect->rot.x -= 256; } const int32_t speed = (effect->speed * Math_Cos(effect->rot.x)) >> W2V_SHIFT; effect->pos = XYZ_32_OffsetYaw(effect->pos, effect->rot.y, speed); effect->pos.y += effect->fall_speed - ((effect->speed * Math_Sin(effect->rot.x)) >> W2V_SHIFT); const int32_t time4 = Output_GetTimeInGame() * 4; if ((time4 & 0xF) == 0) { M_TriggerPlasmaBallFlame( effect_num, (XYZ_32) { .x = 0, .y = ABS(old_y - effect->pos.y) << 3, .z = 0 }); } int16_t room_num = effect->room_num; const SECTOR *const sector = Room_GetSector(effect->pos, &room_num); const int32_t h = Room_GetHeight(sector, effect->pos); const int32_t c = Room_GetCeiling(sector, effect->pos); if (effect->pos.y >= h || effect->pos.y < c || Room_Get(room_num)->flags.underwater) { Effect_Kill(effect_num); return; } if (effect->flag2 == 0 && Lara_IsNearItem(&effect->pos, 200)) { Lara_TakeDamage(25, true); Effect_Kill(effect_num); return; } if (effect->room_num != room_num) { Effect_UpdateRoom(effect_num, room_num); } const int32_t color_base = Random_GetControl(); const RGB_888 color = { .r = color_base & 0x3F, .g = 255 - ((color_base >> 4) & 0x1F), .b = 192 - ((color_base >> 6) & 0x1F), }; Output_AddDynamicLightRGB(effect->pos, m_Falloffs[effect->flag1], color); } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->draw_func = nullptr; } void Sophia_TriggerPlasmaBall( const int32_t type, const XYZ_32 pos, const int16_t room_num, const int16_t angle) { const int16_t fx_num = Effect_Create(room_num); if (fx_num == NO_ITEM) { return; } EFFECT *const effect = Effect_Get(fx_num); effect->speed = (Random_GetControl() & 0x1F) + 64; effect->pos = pos; effect->rot.x = DEG_45; effect->rot.y = angle + Random_GetControl() + DEG_90; effect->object_id = O_SOPHIA_PLASMA_BALL; effect->fall_speed = 0; effect->flag1 = 1; effect->flag2 = type == 2; } REGISTER_OBJECT(O_SOPHIA_PLASMA_BALL, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/spider.c ================================================ #include #include #include #include #include #include #include #include #include // clang-format off #define SPIDER_HITPOINTS 5 #define SPIDER_TURN (DEG_1 * 8) // = 1456 #define SPIDER_RADIUS (WALL_L / 10) // = 102 #define SPIDER_ATTACK_2_RANGE SQUARE(WALL_L / 2) // = 262144 #define SPIDER_ATTACK_3_RANGE SQUARE(WALL_L / 5) // = 41616 #define SPIDER_DAMAGE 25 // clang-format on typedef enum { // clang-format off SPIDER_STATE_EMPTY = 0, SPIDER_STATE_STOP = 1, SPIDER_STATE_WALK_1 = 2, SPIDER_STATE_WALK_2 = 3, SPIDER_STATE_ATTACK_1 = 4, SPIDER_STATE_ATTACK_2 = 5, SPIDER_STATE_ATTACK_3 = 6, SPIDER_STATE_DEATH = 7, // clang-format on } SPIDER_STATE; typedef enum { SPIDER_ANIM_LEAP = 2, } SPIDER_ANIM; static const BITE m_SpiderBite = { .pos = { .x = 0, .y = 0, .z = 41 }, .mesh_num = 1, }; static void M_Leap(const int16_t item_num, const int16_t angle) { ITEM *const item = Item_Get(item_num); const XYZ_32 old_pos = item->pos; const int16_t old_room_num = item->room_num; Creature_Animate(item_num, angle, 0); if (item->pos.y > old_pos.y - STEP_L * 3 / 2) { return; } item->pos = old_pos; Item_UpdateRoom(item_num, old_room_num); Item_SwitchToAnim(item, SPIDER_ANIM_LEAP, 0); item->current_anim_state = SPIDER_STATE_ATTACK_2; Creature_Animate(item_num, angle, 0); } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } int16_t angle = 0; ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; if (item->hit_points > 0) { AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, false); angle = Creature_Turn(item, SPIDER_TURN); switch (item->current_anim_state) { case SPIDER_STATE_STOP: creature->flags = 0; if (creature->mood == MOOD_BORED) { if (Random_GetControl() < 256) { item->goal_anim_state = SPIDER_STATE_WALK_1; } } else if (info.ahead && item->touch_bits != 0) { item->goal_anim_state = SPIDER_STATE_ATTACK_1; } else if (creature->mood == MOOD_STALK) { item->goal_anim_state = SPIDER_STATE_WALK_1; } else if ( creature->mood == MOOD_ESCAPE || creature->mood == MOOD_ATTACK) { item->goal_anim_state = SPIDER_STATE_WALK_2; } break; case SPIDER_STATE_WALK_1: if (creature->mood == MOOD_BORED) { if (Random_GetControl() < 256) { item->goal_anim_state = SPIDER_STATE_STOP; } } else if ( creature->mood == MOOD_ESCAPE || creature->mood == MOOD_ATTACK) { item->goal_anim_state = SPIDER_STATE_WALK_2; } break; case SPIDER_STATE_WALK_2: creature->flags = 0; if (creature->mood == MOOD_BORED || creature->mood == MOOD_STALK) { item->goal_anim_state = SPIDER_STATE_WALK_1; } else if (info.ahead && item->touch_bits != 0) { item->goal_anim_state = SPIDER_STATE_STOP; } else if (info.ahead && info.distance < SPIDER_ATTACK_3_RANGE) { item->goal_anim_state = SPIDER_STATE_ATTACK_3; } else if (info.ahead && info.distance < SPIDER_ATTACK_2_RANGE) { item->goal_anim_state = SPIDER_STATE_ATTACK_2; } break; case SPIDER_STATE_ATTACK_1: case SPIDER_STATE_ATTACK_2: case SPIDER_STATE_ATTACK_3: if (creature->flags == 0 && item->touch_bits != 0) { Creature_Effect(item, &m_SpiderBite, Spawn_Blood); Lara_TakeDamage(SPIDER_DAMAGE, true); creature->flags = 1; } break; default: break; } } else if (Item_Explode(item_num, -1, 0)) { LOT_DisableBaddieAI(item_num); Item_Kill(item_num); item->status = IS_DEACTIVATED; Sound_Effect(SFX_SPIDER_EXPLODE, &item->pos, SPM_NORMAL); Carrier_TestItemDrops(item_num); return; } M_Leap(item_num, angle); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->hit_points = SPIDER_HITPOINTS; obj->radius = SPIDER_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->lot_setup = LOT_Setup(LOT_SETUP_JUMPER); obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_SPIDER, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/swat.c ================================================ #include #include #include #include #include #include #include #include // clang-format off #define M_RADIUS (WALL_L / 10) // = 102 #define M_HIT_POINTS 45 #define M_DAMAGE 28 #define M_ALERT_DIST SQUARE(WALL_L) // = 1048576 #define M_ALERT_HEIGHT (WALL_L * 2) // = 2048 #define M_RUN_DIST SQUARE(WALL_L * 2) // = 4194304 #define M_SHOOT_DIST SQUARE(WALL_L * 3) // = 9437184 #define M_WALK_TURN (DEG_1 * 6) // = 1092 #define M_RUN_TURN (DEG_1 * 9) // = 1638 // clang-format on typedef enum { M_STATE_NULL, M_STATE_STOP, M_STATE_WALK, M_STATE_RUN, M_STATE_WAIT, M_STATE_SHOOT_1, M_STATE_SHOOT_2, M_STATE_DEATH, M_STATE_AIM_1, M_STATE_AIM_2, M_STATE_AIM_3, M_STATE_SHOOT_3, } M_STATE; typedef enum { // clang-format off M_ANIM_STAND = 12, M_ANIM_WALK_END = 17, M_ANIM_DEATH = 19, // clang-format on } M_ANIM; static const CREATURE_GUN m_SwatGun = { .muzzle = { .pos = { 0, 300, 64 }, .mesh_num = 7 }, .tr3_enemy_flash = true, .tr3_flash = { .pos = { 0, 300, 56 }, .mesh_num = 7 }, .tr3_enemy_weapon_flags = 1, .tr3_flash_shade = 600, .tr3_flash_rot_x = -DEG_90, .tr3_laser = { .bite = { .pos = { 0, 200, 106 }, .mesh_num = 7, }, .color = { 0xFF, 0x02, 0x03, 0xDE }, .width = 2.0f, }, }; static void M_Initialise(const int16_t item_num) { Creature_Initialise(item_num); ITEM *const item = Item_Get(item_num); Item_SwitchToAnim(item, M_ANIM_STAND, 0); item->goal_anim_state = M_STATE_STOP; item->current_anim_state = M_STATE_STOP; } static void M_TriggerLaser(const ITEM *const item) { if (item->hit_points > 0 || !Item_TestFrameEqual(item, -1)) { FX_Laser_Spawn(item, &m_SwatGun); } } static void M_FireFinalShot( ITEM *const item, int16_t *const head, int16_t *const torso_y) { const int16_t frame_idx = Item_GetRelativeFrame(item); if (frame_idx <= 44 || frame_idx >= 52 || (item->frame_num & 3) != 0) { return; } AI_INFO info = {}; Creature_AIInfo(item, &info); if (!Creature_CanTargetEnemy(item, &info) || ABS(info.angle) >= DEG_45) { return; } *head = info.angle; *torso_y = info.angle; Creature_Shoot(item, &info, &m_SwatGun, info.angle, M_DAMAGE * 3); const SAMPLE_TRX_ID fire_sfx = item->object_id == O_SWAT_3 ? SFX_AMERICAN_SWAT_FIRE : SFX_LONDON_SWAT_FIRE; Sound_Effect(fire_sfx, &item->pos, SPM_NORMAL); } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t angle = 0; int16_t head = 0; int16_t tilt = 0; int16_t torso_x = 0; int16_t torso_y = 0; Creature_TestBoxDamage(item_num); if (item->hit_points <= 0) { if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, M_ANIM_DEATH, 0); item->current_anim_state = M_STATE_DEATH; creature->flags = (Random_GetControl() & 1) == 0 ? 1 : 0; } else if (creature->flags != 0) { M_FireFinalShot(item, &head, &torso_y); } goto finish; } ITEM *const lara_item = Lara_GetItem(); const LARA_INFO *const lara = Lara_GetLaraInfo(); if (item->ai_bits != 0) { Creature_GetAITarget(creature); } else { creature->enemy = lara_item; } AI_INFO info = {}; Creature_AIInfo(item, &info); AI_INFO lara_info = {}; if (creature->enemy == lara_item) { lara_info.distance = info.distance; lara_info.angle = info.angle; } else { const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; lara_info.angle = Math_Atan(dz, dx) - item->rot.y; lara_info.distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz }); } Creature_Mood(item, &info, creature->enemy != lara_item); angle = Creature_Turn(item, creature->maximum_turn); ITEM *const enemy = creature->enemy; creature->enemy = lara_item; if (item->hit_status || ((lara_info.distance < M_ALERT_DIST || Creature_CanSeeEnemy(item, &lara_info)) && ABS(lara_item->pos.y - item->pos.y) < M_ALERT_HEIGHT)) { if (!creature->alerted) { const SAMPLE_TRX_ID alert_sfx = item->object_id == O_SWAT_3 ? SFX_AMERICAN_HOY : SFX_ENGLISH_HOY; Sound_Effect(alert_sfx, &item->pos, SPM_NORMAL); } Creature_AlertAllGuards(item_num); } creature->enemy = enemy; switch (item->current_anim_state) { case M_STATE_STOP: head = lara_info.angle; creature->flags = 0; creature->maximum_turn = 0; if (Item_GetRelativeAnim(item) == M_ANIM_WALK_END) { if (ABS(info.angle) < M_RUN_TURN) { item->rot.y += info.angle; } else if (info.angle < 0) { item->rot.y -= M_RUN_TURN; } else { item->rot.y += M_RUN_TURN; } } if ((item->ai_bits & AI_GUARD) != 0) { head = Creature_AIGuard(creature); if ((Random_GetControl() & 0xFF) == 0) { item->goal_anim_state = M_STATE_WAIT; } } else if ((item->ai_bits & AI_PATROL_1) != 0) { head = 0; item->goal_anim_state = M_STATE_WALK; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if (Creature_CanTargetEnemy(item, &info)) { if (info.distance >= M_SHOOT_DIST && info.zone_num == info.enemy_zone_num) { item->goal_anim_state = M_STATE_WALK; } else if (Random_GetControl() < 0x4000) { item->goal_anim_state = M_STATE_AIM_1; } else { item->goal_anim_state = M_STATE_AIM_3; } } else if ( creature->mood == MOOD_BORED || ((item->ai_bits & AI_FOLLOW) != 0 && (creature->reached_goal || lara_info.distance > M_RUN_DIST))) { item->goal_anim_state = M_STATE_STOP; } else if (info.distance > M_RUN_DIST) { item->goal_anim_state = M_STATE_RUN; } else { item->goal_anim_state = M_STATE_WALK; } break; case M_STATE_WAIT: head = lara_info.angle; creature->flags = 0; creature->maximum_turn = 0; if ((item->ai_bits & AI_GUARD) != 0) { head = Creature_AIGuard(creature); if ((Random_GetControl() & 0xFF) == 0) { item->goal_anim_state = M_STATE_STOP; } } else if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = M_STATE_SHOOT_1; } else if (creature->mood != MOOD_BORED || !info.ahead) { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_WALK: head = lara_info.angle; creature->flags = 0; creature->maximum_turn = M_WALK_TURN; if ((item->ai_bits & AI_PATROL_1) != 0) { head = 0; item->goal_anim_state = M_STATE_WALK; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if ( (item->ai_bits & AI_GUARD) != 0 || ((item->ai_bits & AI_FOLLOW) != 0 && (creature->reached_goal || lara_info.distance > M_RUN_DIST))) { item->goal_anim_state = M_STATE_STOP; } else if (Creature_CanTargetEnemy(item, &info)) { if (info.distance < M_SHOOT_DIST || info.zone_num != info.enemy_zone_num) { item->goal_anim_state = M_STATE_STOP; } else { item->goal_anim_state = M_STATE_AIM_2; } } else if (creature->mood == MOOD_BORED) { if (info.ahead) { item->goal_anim_state = M_STATE_STOP; } } else if (info.distance > M_RUN_DIST) { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_RUN: if (info.ahead) { head = info.angle; } creature->maximum_turn = M_RUN_TURN; tilt = angle / 2; if ((item->ai_bits & AI_GUARD) != 0 || ((item->ai_bits & AI_FOLLOW) != 0 && (creature->reached_goal || lara_info.distance > M_RUN_DIST))) { item->goal_anim_state = M_STATE_WALK; } else if (creature->mood != MOOD_ESCAPE) { if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = M_STATE_WALK; } else if ( creature->mood == MOOD_BORED || (creature->mood == MOOD_STALK && (item->ai_bits & AI_FOLLOW) == 0 && info.distance < M_RUN_DIST)) { item->goal_anim_state = M_STATE_WALK; } } break; case M_STATE_AIM_1: case M_STATE_AIM_2: case M_STATE_AIM_3: creature->flags = 0; if (info.ahead) { torso_y = info.angle; torso_x = info.x_angle; if (!Creature_CanTargetEnemy(item, &info)) { if (item->current_anim_state == M_STATE_AIM_2) { item->goal_anim_state = M_STATE_WALK; } else { item->goal_anim_state = M_STATE_STOP; } } else if (item->current_anim_state == M_STATE_AIM_1) { item->goal_anim_state = M_STATE_SHOOT_1; } else if (item->current_anim_state == M_STATE_AIM_2) { item->goal_anim_state = M_STATE_SHOOT_2; } else { item->goal_anim_state = M_STATE_SHOOT_3; } } break; case M_STATE_SHOOT_1: case M_STATE_SHOOT_2: case M_STATE_SHOOT_3: if (item->current_anim_state == M_STATE_SHOOT_3 && item->goal_anim_state != M_STATE_STOP && (creature->mood == MOOD_ESCAPE || info.distance > M_SHOOT_DIST || !Creature_CanTargetEnemy(item, &info))) { item->goal_anim_state = M_STATE_STOP; } if (info.ahead) { torso_y = info.angle; torso_x = info.x_angle; } if (creature->flags == 0) { Creature_Shoot(item, &info, &m_SwatGun, torso_y, M_DAMAGE); creature->flags = 5; } else { creature->flags--; } break; default: break; } finish: Creature_Tilt(item, tilt); Creature_Joint(item, 0, torso_y); Creature_Joint(item, 1, torso_x); Creature_Joint(item, 2, head); Creature_Animate(item_num, angle, 0); M_TriggerLaser(item); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->collision_func = Creature_Collision; obj->control_func = M_Control; obj->shadow_size = UNIT_SHADOW / 2; obj->radius = M_RADIUS; obj->hit_points = M_HIT_POINTS; obj->intelligent = true; obj->save_anim = true; obj->save_flags = true; obj->save_hitpoints = true; obj->save_position = true; Object_GetBone(obj, 0)->rot.x = true; Object_GetBone(obj, 0)->rot.y = true; Object_GetBone(obj, 7)->rot.y = true; } REGISTER_OBJECT(O_SWAT_1, M_Setup) REGISTER_OBJECT(O_SWAT_2, M_Setup) REGISTER_OBJECT(O_SWAT_3, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/tiger.c ================================================ #include #include #include #include #include #include #include // clang-format off #define TIGER_HITPOINTS (g_TRVersion == 3 ? 24 : 20) #define TIGER_TOUCH_BITS 0b00000111'11111101'11000000'00000000 #define TIGER_RADIUS (WALL_L / 3) // = 341 #define TIGER_WALK_TURN (DEG_1 * 3) // = 546 #define TIGER_RUN_TURN (DEG_1 * 6) // = 1092 #define TIGER_ATTACK_1_RANGE SQUARE(WALL_L / 3) // = 116281 #define TIGER_ATTACK_2_RANGE SQUARE(WALL_L * 3 / 2) // = 2359296 #define TIGER_ATTACK_3_RANGE SQUARE(WALL_L) // = 1048576 #define TIGER_BITE_DAMAGE (g_TRVersion == 3 ? 90 : 100) #define TIGER_ROAR_CHANCE 96 // clang-format on typedef enum { // clang-format off TIGER_STATE_EMPTY = 0, TIGER_STATE_STOP = 1, TIGER_STATE_WALK = 2, TIGER_STATE_RUN = 3, TIGER_STATE_WAIT = 4, TIGER_STATE_ROAR = 5, TIGER_STATE_ATTACK_1 = 6, TIGER_STATE_ATTACK_2 = 7, TIGER_STATE_ATTACK_3 = 8, TIGER_STATE_DEATH = 9, // clang-format on } TIGER_STATE; typedef enum { TIGER_ANIM_DEATH = 11, } TIGER_ANIM; static const BITE m_TigerBite = { .pos = { .x = 19, .y = -13, .z = 3 }, .mesh_num = 26, }; static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t head = 0; int16_t angle = 0; int16_t tilt = 0; LARA_INFO *const lara = Lara_GetLaraInfo(); if (item->hit_points > 0) { AI_INFO info; Creature_AIInfo(item, &info); Creature_UpdateMood(item, &info, true); if (info.ahead) { head = info.angle; } if (g_TRVersion == 3 && creature->alerted && info.zone_num != info.enemy_zone_num) { creature->mood = MOOD_ESCAPE; } Creature_ApplyMood(item, &info, true); angle = Creature_Turn(item, creature->maximum_turn); switch (item->current_anim_state) { case TIGER_STATE_STOP: creature->maximum_turn = 0; creature->flags = 0; if (creature->mood == MOOD_ESCAPE) { if (g_TRVersion < 3) { item->goal_anim_state = TIGER_STATE_RUN; } else if (lara->target == item || !info.ahead) { item->goal_anim_state = TIGER_STATE_RUN; } else { item->goal_anim_state = TIGER_STATE_STOP; } } else if (creature->mood == MOOD_BORED) { if (Random_GetControl() < TIGER_ROAR_CHANCE) { item->goal_anim_state = TIGER_STATE_ROAR; } item->goal_anim_state = TIGER_STATE_WALK; } else if ( (g_TRVersion == 3 ? info.bite : info.ahead) && info.distance < TIGER_ATTACK_1_RANGE) { item->goal_anim_state = TIGER_STATE_ATTACK_1; } else if ( (g_TRVersion == 3 ? info.bite : info.ahead) && info.distance < TIGER_ATTACK_3_RANGE) { creature->maximum_turn = TIGER_WALK_TURN; item->goal_anim_state = TIGER_STATE_ATTACK_3; } else if (item->required_anim_state != TIGER_STATE_EMPTY) { item->goal_anim_state = item->required_anim_state; } else if ( creature->mood != MOOD_ATTACK && Random_GetControl() < TIGER_ROAR_CHANCE) { item->goal_anim_state = TIGER_STATE_ROAR; } else { item->goal_anim_state = TIGER_STATE_RUN; } break; case TIGER_STATE_WALK: creature->maximum_turn = TIGER_WALK_TURN; if (g_TRVersion == 3 && (creature->mood == MOOD_ESCAPE || creature->mood == MOOD_ATTACK)) { item->goal_anim_state = TIGER_STATE_RUN; } else if (Random_GetControl() < TIGER_ROAR_CHANCE) { item->goal_anim_state = TIGER_STATE_STOP; item->required_anim_state = TIGER_STATE_ROAR; } break; case TIGER_STATE_RUN: creature->maximum_turn = TIGER_RUN_TURN; if (creature->mood == MOOD_BORED) { item->goal_anim_state = TIGER_STATE_STOP; } else if (creature->flags != 0 && info.ahead) { item->goal_anim_state = TIGER_STATE_STOP; } else if ( (g_TRVersion == 3 ? info.bite : info.ahead) && info.distance < TIGER_ATTACK_2_RANGE) { const ITEM *const lara_item = Lara_GetItem(); if (lara_item->speed != 0) { item->goal_anim_state = TIGER_STATE_ATTACK_2; } else { item->goal_anim_state = TIGER_STATE_STOP; } } else if ( creature->mood != MOOD_ATTACK && Random_GetControl() < TIGER_ROAR_CHANCE) { item->required_anim_state = TIGER_STATE_ROAR; item->goal_anim_state = TIGER_STATE_STOP; } else if ( g_TRVersion == 3 && creature->mood == MOOD_ESCAPE && lara->target != item && info.ahead) { item->goal_anim_state = TIGER_STATE_STOP; } creature->flags = 0; break; case TIGER_STATE_ATTACK_1: case TIGER_STATE_ATTACK_2: case TIGER_STATE_ATTACK_3: if (creature->flags == 0 && (item->touch_bits & TIGER_TOUCH_BITS) != 0) { Lara_TakeDamage(TIGER_BITE_DAMAGE, true); Creature_Effect(item, &m_TigerBite, Spawn_Blood); creature->flags = 1; } break; default: break; } } else if (item->current_anim_state != TIGER_STATE_DEATH) { Item_SwitchToAnim(item, TIGER_ANIM_DEATH, 0); item->current_anim_state = TIGER_STATE_DEATH; } Creature_Tilt(item, tilt); Creature_Head(item, head); Creature_Animate(item_num, angle, tilt); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->hit_points = TIGER_HITPOINTS; obj->radius = TIGER_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->pivot_length = 200; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 21)->rot.y = true; } REGISTER_OBJECT(O_TIGER, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/tony.c ================================================ #include "tony_internal.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef enum { M_PHASE_DORMANT = 0, M_PHASE_AWAKENED = 1, M_PHASE_PHASE2 = 2, } M_PHASE; typedef enum { M_STATE_WAIT, M_STATE_RISE, M_STATE_FLOAT, M_STATE_ZAPP, M_STATE_ROCK_ZAPP, M_STATE_BIG_ROOM, M_STATE_DEATH } M_STATE; typedef struct { XYZ_16 pos; RGB_888 sub; RGB_888 color; } M_SHIELD_POINT; typedef struct { bool dropped_item; uint8_t ring_count; int16_t explode_count; bool dead; M_SHIELD_POINT shield[5][8]; M_PHASE phase; // Alternates the chosen attack while in the FLOAT state (ROCK_ZAPP vs ZAPP) bool attack_toggle; } M_PRIV; static const int32_t m_Heights[5] = { -1536, -1280, -832, -384, 0 }; static const int32_t m_Dist[5] = { 200, 400, 500, 500, 475 }; static const int32_t m_DDist[5] = { 1600, 5600, 6400, 5600, 1600 }; static const int32_t m_DHeights1[5] = { -7680, -4224, -768, 2688, 6144 }; static const int32_t m_DHeights2[5] = { -1536, -1152, -768, -384, 0 }; static int32_t m_DeathDist[5] = {}; static int32_t m_DeathHeights[5] = {}; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "dropped_item", &p->dropped_item)); JSON_SHOULD(JSON_READ(io, "ring_count", &p->ring_count)); JSON_SHOULD(JSON_READ(io, "explode_count", &p->explode_count)); JSON_SHOULD(JSON_READ(io, "dead", &p->dead)); JSON_SHOULD(JSON_READ(io, "phase", &p->phase)); JSON_SHOULD(JSON_READ(io, "attack_toggle", &p->attack_toggle)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "dropped_item", p->dropped_item); JSONW_WRITE(io, "ring_count", p->ring_count); JSONW_WRITE(io, "explode_count", p->explode_count); JSONW_WRITE(io, "dead", p->dead); JSONW_WRITE(io, "phase", p->phase); JSONW_WRITE(io, "attack_toggle", p->attack_toggle); } static void M_TriggerFlame(int16_t item_num, int32_t node) { const ITEM *const lara_item = Lara_GetItem(); ITEM *const item = Item_Get(item_num); const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; if (dx < -0x4000 || dx > 0x4000 || dz < -0x4000 || dz > 0x4000) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 255; spark->src_color.g = (Random_GetControl() & 0x1F) + 48; spark->src_color.b = 48; spark->dst_color.r = (Random_GetControl() & 0x3F) + 192; spark->dst_color.g = (Random_GetControl() & 0x3F) + 128; spark->dst_color.b = 32; spark->fade_to_black = 8; spark->col_fade_speed = (Random_GetControl() & 3) + 12; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->life = (Random_GetControl() & 7) + 24; spark->s_life = spark->life; spark->dynamic = -1; spark->pos.x = (Random_GetControl() & 0xF) - 8; spark->pos.y = 0; spark->pos.z = (Random_GetControl() & 0xF) - 8; spark->vel.x = (Random_GetControl() & 0xFF) - 128; spark->vel.y = -16 - (Random_GetControl() & 0xF); spark->vel.z = (Random_GetControl() & 0xFF) - 128; spark->friction = 5; if (Random_GetControl() & 1) { spark->flags = SPARK_F_ATTACHED_NODE | SPARK_F_ALT_SPRITE | SPARK_F_ITEM | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = Random_GetControl() & 0xFFF; if (Random_GetControl() & 1) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = (Random_GetControl() & 0xF) + 16; } } else { spark->flags = SPARK_F_ATTACHED_NODE | SPARK_F_ALT_SPRITE | SPARK_F_ITEM | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->gravity = -16 - (Random_GetControl() & 0x1F); spark->max_y_vel = -16 - (Random_GetControl() & 7); spark->item_num = item_num; spark->node_num = node; spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->scalar = 1; spark->size.width = (Random_GetControl() & 0x1F) + 64; spark->src_size.width = spark->size.width; spark->dst_size.width = spark->size.width >> 2; spark->size.height = spark->size.width; spark->src_size.height = spark->size.height; spark->dst_size.height = spark->size.height >> 2; Sparks_FinishSetup(spark); } static void M_Explode(ITEM *const item) { M_PRIV *const p = item->priv; if (item->hit_points <= 0 && (p->explode_count == 1 || p->explode_count == 15 || p->explode_count == 25 || p->explode_count == 35 || p->explode_count == 45 || p->explode_count == 55)) { const XYZ_32 pos = { .x = item->pos.x + (Random_GetDraw() & 0x3FF) - 512, .y = item->pos.y - (Random_GetDraw() & 0x3FF) - 256, .z = item->pos.z + (Random_GetDraw() & 0x3FF) - 512, }; FX_RING *const ring = FX_Ring_GetRing(FX_RING_TYPE_BLAST, p->ring_count); if (ring != nullptr) { ring->pos = pos; ring->on = 2; FX_Ring_Sync(ring); p->ring_count++; } Sparks_TriggerExplosionSparks(pos, 3, -2, 0, 0); for (int32_t i = 0; i < 2; i++) { Sparks_TriggerExplosionSparks(pos, 3, -1, 0, 0); } Sound_Effect(SFX_BLAST_CIRCLE, &item->pos, 0x800000 | SPM_PITCH); } for (int32_t i = 0; i < 5; i++) { if (p->explode_count < 128) { m_DeathDist[i] = (m_DDist[i] >> 4) + ((p->explode_count * m_DDist[i]) >> 7); m_DeathHeights[i] = m_DHeights2[i] + ((p->explode_count * (m_DHeights1[i] - m_DHeights2[i])) >> 7); } } if (p->explode_count > 64) { return; } for (int32_t i = 0; i < 5; i++) { const int32_t y = m_DeathHeights[i]; const int32_t dist = m_DeathDist[i]; const int32_t time4 = Output_GetTimeInGame() * 4; int32_t angle = (time4 & 0x3F) << 3; for (int32_t j = 0; j < 8; j++) { M_SHIELD_POINT *const shield = &p->shield[i][j]; shield->pos.x = (dist * Math_Sin(angle << 4)) >> 13; shield->pos.y = y; shield->pos.z = (dist * Math_Cos(angle << 4)) >> 13; if (i != 0 && i != 4 && p->explode_count < 64) { int32_t r = (Random_GetDraw() & 0x1F) + 224; int32_t g = (r >> 2) + (Random_GetDraw() & 0x3F); int32_t b = Random_GetDraw() & 0x3F; if (item->hit_points <= 0) { r = ((64 - p->explode_count) * r) >> 6; g = ((64 - p->explode_count) * g) >> 6; b = ((64 - p->explode_count) * b) >> 6; } else { r = ((128 - p->explode_count) * r) >> 7; g = ((128 - p->explode_count) * g) >> 7; b = ((128 - p->explode_count) * b) >> 7; } shield->color = (RGB_888) { r, g, b }; } else { shield->color = COLOR_RGB_888_BLACK; } angle = (angle + 512) & 0xFFF; } } } static bool M_CanDropItems(const ITEM *const item) { if (item->hit_points > 0) { return false; } if ((item->flags & IF_KILLED) != 0) { return true; } return item->current_anim_state == M_STATE_DEATH && Item_GetRelativeFrame(item) >= 110; } static bool M_CanBeExploded(const ITEM *const item) { return false; } static void M_Die(const int16_t item_num) { ITEM *const item = Item_Get(item_num); item->hit_points = 0; item->collidable = false; Item_Kill(item_num); LOT_DisableBaddieAI(item_num); item->flags |= IF_INVISIBLE; } static void M_Initialise(int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; p->dead = false; p->dropped_item = false; p->ring_count = 0; p->explode_count = 0; p->attack_toggle = false; p->phase = M_PHASE_DORMANT; for (int32_t i = 0; i < 5; i++) { const int32_t dist = m_Dist[i]; int32_t angle = 0; for (int32_t j = 0; j < 8; j++) { M_SHIELD_POINT *const shield = &p->shield[i][j]; shield->pos.x = (dist * Math_Sin(angle << 4)) >> 13; shield->pos.y = m_Heights[i]; shield->pos.z = (dist * Math_Cos(angle << 4)) >> 13; shield->color = COLOR_RGB_888_BLACK; angle += 512; } } } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } const ITEM *const lara_item = Lara_GetItem(); ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; CREATURE *const tony = item->creature_data; int16_t angle = 0; int16_t torso_x = 0; int16_t torso_y = 0; if (item->hit_points <= 0) { if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, 6, 0); item->current_anim_state = M_STATE_DEATH; } if (Item_GetRelativeFrame(item) > 110) { Item_SwitchToAnim(item, Item_GetRelativeAnim(item), 110); item->mesh_bits = 0; if (!p->explode_count) { p->ring_count = 0; for (int32_t i = 0; i < 6; i++) { FX_RING *const ring = FX_Ring_GetRing(FX_RING_TYPE_BLAST, i); if (ring == nullptr) { continue; } ring->on = 0; ring->life = 32; ring->radius = 512; ring->speed = 128 + (i << 5); ring->rot.x = ((Random_GetControl() & 0x1FF) - 256) & 0xFFF; ring->rot.z = ((Random_GetControl() & 0x1FF) - 256) & 0xFFF; FX_Ring_Sync(ring); } if (!p->dropped_item) { Carrier_TestItemDrops(item_num); p->dropped_item = true; } } if (p->explode_count < 256) { p->explode_count++; } if (p->explode_count > 128 && p->ring_count == 6 && FX_Ring_GetRing(FX_RING_TYPE_BLAST, 5)->life == 0) { M_Die(item_num); p->dead = true; } else { M_Explode(item); } return; } } else { if (p->phase != M_PHASE_PHASE2) { item->hit_points = item->max_hit_points; } AI_INFO info; Creature_AIInfo(item, &info); if (p->phase != M_PHASE_DORMANT) { tony->target.x = lara_item->pos.x; tony->target.z = lara_item->pos.z; angle = Creature_Turn(item, tony->maximum_turn); } else { const int32_t x = item->pos.x - lara_item->pos.x; const int32_t z = item->pos.z - lara_item->pos.z; if (SQUARE(x) + SQUARE(z) < 0x1900000) { p->phase = M_PHASE_AWAKENED; } angle = 0; } switch (item->current_anim_state) { case M_STATE_WAIT: tony->maximum_turn = 0; if (item->goal_anim_state != M_STATE_RISE && p->phase != M_PHASE_DORMANT) { item->goal_anim_state = M_STATE_RISE; } break; case M_STATE_RISE: if (Item_GetRelativeFrame(item) <= 16) { tony->maximum_turn = 0; } else { tony->maximum_turn = 364; } break; case M_STATE_FLOAT: torso_y = info.angle; torso_x = info.x_angle; tony->maximum_turn = 364; if (p->explode_count == 0) { if (item->goal_anim_state != M_STATE_BIG_ROOM && p->phase != M_PHASE_PHASE2) { item->goal_anim_state = M_STATE_BIG_ROOM; tony->maximum_turn = 0; } const int32_t time4 = Output_GetTimeInGame() * 4; if (item->goal_anim_state != M_STATE_ROCK_ZAPP && p->phase == M_PHASE_PHASE2 && !(time4 & 0xFF) && !p->attack_toggle) { item->goal_anim_state = M_STATE_ROCK_ZAPP; p->attack_toggle = true; } if (item->goal_anim_state != M_STATE_ZAPP && item->goal_anim_state != M_STATE_ROCK_ZAPP && p->phase == M_PHASE_PHASE2 && !(time4 & 0xFF) && p->attack_toggle) { item->goal_anim_state = M_STATE_ZAPP; p->attack_toggle = false; } } break; case M_STATE_ZAPP: torso_y = info.angle; torso_x = info.x_angle; tony->maximum_turn = 182; if (Item_GetRelativeFrame(item) == 28) { TonyBoss_TriggerFireBall( item, 2, nullptr, item->room_num, item->rot.y, 0); } break; case M_STATE_ROCK_ZAPP: torso_y = info.angle; torso_x = info.x_angle; tony->maximum_turn = 0; if (Item_GetRelativeFrame(item) == 40) { TonyBoss_TriggerFireBall( item, 0, nullptr, item->room_num, 0, 0); TonyBoss_TriggerFireBall( item, 1, nullptr, item->room_num, 0, 0); } break; case M_STATE_BIG_ROOM: tony->maximum_turn = 0; if (Item_GetRelativeFrame(item) == 56) { p->phase = M_PHASE_PHASE2; p->explode_count = 1; } break; } } if (item->current_anim_state == M_STATE_ROCK_ZAPP || item->current_anim_state == M_STATE_ZAPP || item->current_anim_state == M_STATE_BIG_ROOM) { int32_t f = Item_GetRelativeFrame(item); if (f > 16) { f = Item_GetAnim(item)->frame_end - item->frame_num; CLAMPG(f, 16); } const int32_t r = Random_GetControl(); const RGB_888 color = { .r = (f * (255 - ((r >> 4) & 0x1F))) >> 4, .g = (f * (192 - ((r >> 6) & 0x1F))) >> 4, .b = (f * (r & 0x3F)) >> 4, }; XYZ_32 pos = {}; Collide_GetJointAbsPosition(item, &pos, 10); Output_AddDynamicLightRGB(pos, 12, color); M_TriggerFlame(item_num, 5); if (item->current_anim_state == M_STATE_ROCK_ZAPP || item->current_anim_state == M_STATE_BIG_ROOM) { pos.x = 0; pos.y = 0; pos.z = 0; Collide_GetJointAbsPosition(item, &pos, 13); Output_AddDynamicLightRGB(pos, 12, color); M_TriggerFlame(item_num, 4); } } if (p->explode_count != 0 && item->hit_points > 0) { M_Explode(item); p->explode_count++; if (p->explode_count == 32) { Room_FlipMap(); } if (p->explode_count > 64) { p->ring_count = 0; p->explode_count = 0; } } Creature_Joint(item, 0, torso_y >> 1); Creature_Joint(item, 1, torso_x); Creature_Joint(item, 2, torso_y >> 1); Creature_Animate(item_num, angle, 0); } static void M_DrawShield(const ITEM *const item) { const M_PRIV *const p = item->priv; const int32_t time4 = Output_GetTimeInGame() * 4; const int32_t sprite_base = Object_Get(O_EXPLOSION_1)->mesh_idx; for (int32_t band = 0; band < 4; band++) { const int32_t sprite_idx = sprite_base + 18 + ((band + (time4 >> 3)) & 7); for (int32_t j = 0; j < 8; j++) { const int32_t j2 = (j == 7) ? 0 : (j + 1); const M_SHIELD_POINT *const s00 = &p->shield[band][j]; const M_SHIELD_POINT *const s01 = &p->shield[band][j2]; const M_SHIELD_POINT *const s10 = &p->shield[band + 1][j]; const M_SHIELD_POINT *const s11 = &p->shield[band + 1][j2]; const int32_t idx00 = band * 8 + j; const int32_t idx01 = band * 8 + j2; const int32_t idx10 = (band + 1) * 8 + j; const int32_t idx11 = (band + 1) * 8 + j2; RGB_888 c00 = s00->color; RGB_888 c01 = s01->color; RGB_888 c10 = s10->color; RGB_888 c11 = s11->color; if (idx00 >= 8 && idx00 <= 31) { c00.r = (uint8_t)MAX(0, (int32_t)c00.r - (int32_t)s00->sub.r); c00.g = (uint8_t)MAX(0, (int32_t)c00.g - (int32_t)s00->sub.g); c00.b = (uint8_t)MAX(0, (int32_t)c00.b - (int32_t)s00->sub.b); } if (idx01 >= 8 && idx01 <= 31) { c01.r = (uint8_t)MAX(0, (int32_t)c01.r - (int32_t)s01->sub.r); c01.g = (uint8_t)MAX(0, (int32_t)c01.g - (int32_t)s01->sub.g); c01.b = (uint8_t)MAX(0, (int32_t)c01.b - (int32_t)s01->sub.b); } if (idx10 >= 8 && idx10 <= 31) { c10.r = (uint8_t)MAX(0, (int32_t)c10.r - (int32_t)s10->sub.r); c10.g = (uint8_t)MAX(0, (int32_t)c10.g - (int32_t)s10->sub.g); c10.b = (uint8_t)MAX(0, (int32_t)c10.b - (int32_t)s10->sub.b); } if (idx11 >= 8 && idx11 <= 31) { c11.r = (uint8_t)MAX(0, (int32_t)c11.r - (int32_t)s11->sub.r); c11.g = (uint8_t)MAX(0, (int32_t)c11.g - (int32_t)s11->sub.g); c11.b = (uint8_t)MAX(0, (int32_t)c11.b - (int32_t)s11->sub.b); } if (((c00.r | c00.g | c00.b | c01.r | c01.g | c01.b | c11.r | c11.g | c11.b | c10.r | c10.g | c10.b) == 0U)) { continue; } const XYZ_32 world_pos[4] = { { item->pos.x + s00->pos.x, item->pos.y + s00->pos.y, item->pos.z + s00->pos.z, }, { item->pos.x + s01->pos.x, item->pos.y + s01->pos.y, item->pos.z + s01->pos.z, }, { item->pos.x + s11->pos.x, item->pos.y + s11->pos.y, item->pos.z + s11->pos.z, }, { item->pos.x + s10->pos.x, item->pos.y + s10->pos.y, item->pos.z + s10->pos.z, }, }; const RGBA_8888 color[4] = { { c00.r, c00.g, c00.b, 255 }, { c01.r, c01.g, c01.b, 255 }, { c11.r, c11.g, c11.b, 255 }, { c10.r, c10.g, c10.b, 255 }, }; OutputSource_PolyFX_StageSpriteQuadWorld( sprite_idx, world_pos, color, DRAW_BLEND_ADD); } } } static bool M_Draw(const ITEM *const item) { M_PRIV *const p = item->priv; if ((item->hit_points <= 0 && p->explode_count > 64)) { return false; } const bool result = Object_DrawAnimatingItem(item); if (p->explode_count != 0) { if (p->explode_count != 0 && p->explode_count <= 64) { M_DrawShield(item); } } return result; } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->draw_func = M_Draw; obj->can_drop_items_func = M_CanDropItems; obj->can_be_exploded_func = M_CanBeExploded; obj->shadow_size = 0; obj->hit_points = 100; obj->pivot_length = 50; obj->radius = 102; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 6)->rot.x = true; Object_GetBone(obj, 6)->rot.y = true; Object_GetBone(obj, 13)->rot.y = true; } REGISTER_OBJECT(O_TONY, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/tony_fire_ball.c ================================================ #include "tony_internal.h" #include #include #include #include #include #include #include #include #include static void M_TriggerFireBallFlame( const int16_t effect_num, const int32_t type, const int32_t xv, const int32_t yv, const int32_t zv) { const ITEM *const lara_item = Lara_GetItem(); const EFFECT *const effect = Effect_Get(effect_num); const int32_t dx = lara_item->pos.x - effect->pos.x; const int32_t dz = lara_item->pos.z - effect->pos.z; if (dx < -0x4000 || dx > 0x4000 || dz < -0x4000 || dz > 0x4000) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 255; spark->src_color.g = (Random_GetControl() & 0x1F) + 48; spark->src_color.b = 48; spark->dst_color.r = (Random_GetControl() & 0x3F) + 192; spark->dst_color.g = (Random_GetControl() & 0x3F) + 128; spark->dst_color.b = 32; spark->fade_to_black = 8; spark->col_fade_speed = (Random_GetControl() & 3) + 12; spark->draw_type = DRAW_BLEND_ADD; spark->life = (Random_GetControl() & 7) + 24; spark->s_life = spark->life; spark->extras = 0; spark->dynamic = -1; spark->pos.x = (Random_GetControl() & 0xF) - 8; spark->pos.y = 0; spark->pos.z = (Random_GetControl() & 0xF) - 8; spark->vel.x = (Random_GetControl() & 0xFF) + xv - 128; spark->vel.y = (int16_t)yv; spark->vel.z = (Random_GetControl() & 0xFF) + zv - 128; spark->friction = 5; if (Random_GetControl() & 1) { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = Random_GetControl() & 0xFFF; if (Random_GetControl() & 1) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = (Random_GetControl() & 0xF) + 16; } } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->effect_num = effect_num; spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->scalar = 1; spark->size.width = (Random_GetControl() & 0x1F) + 64; spark->src_size.width = spark->size.width; spark->dst_size.width = spark->size.width >> 2; spark->size.height = spark->size.width; spark->src_size.height = spark->size.height; spark->dst_size.height = spark->size.height >> 2; if (!type || type == 1) { spark->gravity = (Random_GetControl() & 0x1F) + 16; spark->max_y_vel = (Random_GetControl() & 0xF) + 48; spark->scalar = 2; spark->vel.y *= -16; } else if (type == 4 || type == 5 || type == 6) { spark->max_y_vel = 0; spark->gravity = 0; } else if (type == 3) { spark->gravity = -16 - (Random_GetControl() & 0x1F); spark->max_y_vel = -64 - (Random_GetControl() & 0x1F); spark->scalar = 2; spark->vel.y <<= 4; } else if (type == 2) { spark->max_y_vel = 0; spark->gravity = 0; spark->scalar = 2; } Sparks_FinishSetup(spark); } void TonyBoss_TriggerFireBall( ITEM *const item, const int32_t type, const XYZ_32 *const pos, const int16_t room_num, int16_t angle, int32_t speed) { XYZ_32 effect_pos = {}; int32_t fall_speed; switch (type) { case 0: Collide_GetJointAbsPosition(item, &effect_pos, 10); angle = item->rot.y; fall_speed = -16; speed = 0; break; case 1: Collide_GetJointAbsPosition(item, &effect_pos, 13); angle = item->rot.y; fall_speed = -16; speed = 0; break; case 2: Collide_GetJointAbsPosition(item, &effect_pos, 13); speed = 160; fall_speed = -32 - (Random_GetControl() & 7); break; case 3: effect_pos = *pos; speed = 0; fall_speed = (Random_GetControl() & 3) + 4; break; case 4: effect_pos = *pos; speed += (Random_GetControl() & 3); angle = Random_GetControl() << 1; fall_speed = (Random_GetControl() & 3) - 2; break; case 5: effect_pos = *pos; speed = (Random_GetControl() & 7) + 48; angle += (Random_GetControl() & 0x1FFF) + 0x7000; fall_speed = -16 - (Random_GetControl() & 0xF); break; default: effect_pos = *pos; speed = (Random_GetControl() & 0x1F) + 32; angle = Random_GetControl() << 1; fall_speed = -32 - (Random_GetControl() & 0x1F); break; } const int16_t fx_num = Effect_Create(room_num); if (fx_num == NO_EFFECT) { return; } EFFECT *const effect = Effect_Get(fx_num); effect->pos = effect_pos; effect->rot.y = angle; effect->object_id = O_TONY_FIRE_BALL; effect->speed = speed; effect->fall_speed = fall_speed; effect->flag1 = type; effect->flag2 = (Random_GetControl() & 3) + 1; if (type == 5) { effect->flag2 <<= 1; } else if (type == 2) { effect->flag2 = 0; } } static void M_Control(const int16_t effect_num) { const ITEM *const lara_item = Lara_GetItem(); uint8_t falloffs[7] = { 16, 0, 14, 9, 7, 7, 7 }; EFFECT *const effect = Effect_Get(effect_num); XYZ_32 old_pos = effect->pos; const int32_t time4 = Output_GetTimeInGame() * 4; if (!effect->flag1 || effect->flag1 == 1) { effect->fall_speed += (effect->fall_speed >> 3) + 1; CLAMPL(effect->fall_speed, -4096); effect->pos.y += effect->fall_speed; if (time4 & 4) { M_TriggerFireBallFlame(effect_num, effect->flag1, 0, 0, 0); } } else if (effect->flag1 == 3) { effect->fall_speed += 2; effect->pos.y += effect->fall_speed; if (time4 & 4) { M_TriggerFireBallFlame(effect_num, 3, 0, 0, 0); } } else { if (effect->flag1 != 2) { if (effect->speed > 48) { effect->speed--; } } effect->fall_speed += effect->flag2; if (effect->fall_speed > 512) { effect->fall_speed = 512; } effect->pos.x += effect->speed * Math_Sin(effect->rot.y) >> W2V_SHIFT; effect->pos.y += effect->fall_speed >> 1; effect->pos.z += effect->speed * Math_Cos(effect->rot.y) >> W2V_SHIFT; const int32_t dx = (old_pos.x - effect->pos.x) << 3; const int32_t dy = (old_pos.y - effect->pos.y) << 3; const int32_t dz = (old_pos.z - effect->pos.z) << 3; if (time4 & 4) { M_TriggerFireBallFlame(effect_num, effect->flag1, dx, dy, dz); } } int16_t room_num = effect->room_num; SECTOR *sector = Room_GetSector(effect->pos, &room_num); const int32_t h = Room_GetHeight(sector, effect->pos); const int32_t c = Room_GetCeiling(sector, effect->pos); if (effect->pos.y >= h || effect->pos.y < c) { if (!effect->flag1 || effect->flag1 == 1 || effect->flag1 == 2 || effect->flag1 == 3) { Sparks_TriggerExplosionSparks(old_pos, 3, -2, 0, effect->room_num); if (!effect->flag1 || effect->flag1 == 1) { for (int32_t i = 0; i < 2; i++) { Sparks_TriggerExplosionSparks( old_pos, 3, -1, 0, effect->room_num); } } XYZ_32 pos = old_pos; int32_t count; if (effect->flag1 == 2) { count = 7; } else { count = 3; } int32_t type; if (effect->flag1 == 2) { type = 5; } else if (effect->flag1 == 3) { type = 6; } else { type = 4; } for (int32_t i = 0; i < count; i++) { TonyBoss_TriggerFireBall( nullptr, type, &pos, effect->room_num, effect->rot.y, (i << 2) + 32); } if (!effect->flag1 || effect->flag1 == 1) { room_num = lara_item->room_num; sector = Room_GetSector(lara_item->pos, &room_num); pos.x = lara_item->pos.x + (Random_GetControl() & 0x3FF) - 512; pos.z = lara_item->pos.z + (Random_GetControl() & 0x3FF) - 512; pos.y = Room_GetCeiling(sector, lara_item->pos) + 256; Sparks_TriggerExplosionSparks(pos, 3, -2, 0, room_num); TonyBoss_TriggerFireBall(nullptr, 3, &pos, room_num, 0, 0); } } Effect_Kill(effect_num); return; } if (Room_Get(room_num)->flags.underwater) { Effect_Kill(effect_num); return; } LARA_INFO *const lara = Lara_GetLaraInfo(); if (!lara->burn && Lara_IsNearItem(&effect->pos, 200)) { Effect_Kill(effect_num); Lara_TakeDamage(200, true); Lara_CatchFire(); return; } if (effect->room_num != room_num) { Effect_UpdateRoom(effect_num, lara_item->room_num); } if (falloffs[effect->flag1]) { const uint8_t r = Random_GetControl(); const RGB_888 color = { .r = 255 - ((r >> 4) & 0x1F), .g = 192 - ((r >> 6) & 0x1F), .b = r & 0x3F, }; Output_AddDynamicLightRGB(effect->pos, falloffs[effect->flag1], color); } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->draw_func = nullptr; } REGISTER_OBJECT(O_TONY_FIRE_BALL, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/tony_internal.h ================================================ #pragma once #include #include void TonyBoss_TriggerFireBall( ITEM *item, int32_t type, const XYZ_32 *pos, int16_t room_num, int16_t angle, int32_t speed); ================================================ FILE: src/trx/game/objects/creatures/torso.c ================================================ #include #include #include #include #include #include #include #include #include // clang-format off #define M_PART_DAMAGE 250 #define M_ATTACK_DAMAGE 500 #define M_TOUCH_DAMAGE 5 #define M_NEED_TURN (DEG_1 * 45) // = 8190 #define M_TURN (DEG_1 * 3) // = 546 #define M_ATTACK_RANGE SQUARE(2600) // = 6760000 #define M_CLOSE_RANGE SQUARE(2250) // = 5062500 #define M_TOUCH_LEFT 0x7FF0 #define M_TOUCH_RIGHT 0x3FF8000 #define M_TOUCH (M_TOUCH_LEFT | M_TOUCH_RIGHT) #define M_HITPOINTS 500 #define M_RADIUS (WALL_L / 3) // = 341 #define M_SMARTNESS 0x7FFF #define M_FRAME_TURN_L_START 14 #define M_FRAME_TURN_L_END 22 #define M_FRAME_TURN_R_START 17 #define M_FRAME_TURN_R_END 22 // clang-format on typedef enum { // clang-format off TORSO_ANIM_TURN_L = 8, TORSO_ANIM_DIE = 13, TORSO_ANIM_TURN_R = 17, TORSO_ANIM_KILL = 19, // clang-format on } M_ANIM; typedef enum { // clang-format off TORSO_STATE_EMPTY = 0, TORSO_STATE_STOP = 1, TORSO_STATE_TURN_L = 2, TORSO_STATE_TURN_R = 3, TORSO_STATE_ATTACK_1 = 4, TORSO_STATE_ATTACK_2 = 5, TORSO_STATE_ATTACK_3 = 6, TORSO_STATE_FORWARD = 7, TORSO_STATE_SET = 8, TORSO_STATE_FALL = 9, TORSO_STATE_DEATH = 10, TORSO_STATE_KILL = 11, // clang-format on } M_STATE; static void M_KillLara(ITEM *const item) { const ITEM *const lara_item = Lara_GetItem(); Lara_TakeDamage(lara_item->hit_points, true); Creature_SpecialKill( item, TORSO_ANIM_KILL, TORSO_STATE_KILL, LS_EXTRA_TORSO_KILL); } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const torso = item->creature_data; int16_t head = 0; ITEM *const lara_item = Lara_GetItem(); if (item->hit_points <= 0) { if (item->current_anim_state != TORSO_STATE_DEATH) { item->current_anim_state = TORSO_STATE_DEATH; Item_SwitchToAnim(item, TORSO_ANIM_DIE, 0); } } else { AI_INFO info; Creature_AIInfo(item, &info); if (info.ahead) { head = info.angle; } Creature_Mood(item, &info, true); int16_t angle = Math_Atan( torso->target.z - item->pos.z, torso->target.x - item->pos.x) - item->rot.y; if (item->touch_bits) { Lara_TakeDamage(M_TOUCH_DAMAGE, true); } switch (item->current_anim_state) { case TORSO_STATE_SET: item->goal_anim_state = TORSO_STATE_FALL; item->gravity = true; break; case TORSO_STATE_STOP: if (lara_item->hit_points <= 0) { break; } torso->flags = 0; if (angle > M_NEED_TURN) { item->goal_anim_state = TORSO_STATE_TURN_R; } else if (angle < -M_NEED_TURN) { item->goal_anim_state = TORSO_STATE_TURN_L; } else if (info.distance >= M_ATTACK_RANGE) { item->goal_anim_state = TORSO_STATE_FORWARD; } else if (lara_item->hit_points > M_ATTACK_DAMAGE) { if (Random_GetControl() < 0x4000) { item->goal_anim_state = TORSO_STATE_ATTACK_1; } else { item->goal_anim_state = TORSO_STATE_ATTACK_2; } } else if (info.distance < M_CLOSE_RANGE) { item->goal_anim_state = TORSO_STATE_ATTACK_3; } else { item->goal_anim_state = TORSO_STATE_FORWARD; } break; case TORSO_STATE_FORWARD: if (angle < -M_TURN) { item->goal_anim_state -= M_TURN; } else if (angle > M_TURN) { item->goal_anim_state += M_TURN; } else { item->goal_anim_state += angle; } if (angle > M_NEED_TURN || angle < -M_NEED_TURN) { item->goal_anim_state = TORSO_STATE_STOP; } else if (info.distance < M_ATTACK_RANGE) { item->goal_anim_state = TORSO_STATE_STOP; } break; case TORSO_STATE_TURN_L: if (!torso->flags) { torso->flags = item->frame_num; } else if ( Item_TestAnimEqual(item, TORSO_ANIM_TURN_L) && Item_TestFrameRange( item, M_FRAME_TURN_L_START, M_FRAME_TURN_L_END)) { item->rot.y -= DEG_1 * 9; } if (angle > -M_NEED_TURN) { item->goal_anim_state = TORSO_STATE_STOP; } break; case TORSO_STATE_TURN_R: if (!torso->flags) { torso->flags = item->frame_num; } else if ( Item_TestAnimEqual(item, TORSO_ANIM_TURN_R) && Item_TestFrameRange( item, M_FRAME_TURN_R_START, M_FRAME_TURN_R_END)) { item->rot.y += DEG_1 * 14; } if (angle < M_NEED_TURN) { item->goal_anim_state = TORSO_STATE_STOP; } break; case TORSO_STATE_ATTACK_1: if (!torso->flags && (item->touch_bits & M_TOUCH_RIGHT)) { Lara_TakeDamage(M_ATTACK_DAMAGE, true); torso->flags = 1; } break; case TORSO_STATE_ATTACK_2: if (!torso->flags && (item->touch_bits & M_TOUCH)) { Lara_TakeDamage(M_ATTACK_DAMAGE, true); torso->flags = 1; } break; case TORSO_STATE_ATTACK_3: if ((item->touch_bits & M_TOUCH_RIGHT) || lara_item->hit_points <= 0) { M_KillLara(item); } break; case TORSO_STATE_KILL: g_Camera.target_distance = WALL_L * 2; g_Camera.flags = CF_FOLLOW_CENTRE; break; } } Creature_Head(item, head); if (item->current_anim_state == TORSO_STATE_FALL) { Item_Animate(item); if (item->pos.y > item->floor) { item->goal_anim_state = TORSO_STATE_STOP; item->gravity = false; item->pos.y = item->floor; g_Camera.bounce = 500; } } else { Creature_Animate(item_num, 0, 0); } if (item->status == IS_DEACTIVATED) { Sound_Effect(SFX_ATLANTEAN_DEATH, &item->pos, SPM_NORMAL); Item_Explode(item_num, -1, M_PART_DAMAGE); Room_TestTriggers(item); Item_Kill(item_num); item->status = IS_DEACTIVATED; } } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = Creature_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->shadow_size = UNIT_SHADOW / 3; obj->hit_points = M_HITPOINTS; obj->radius = M_RADIUS; obj->smartness = M_SMARTNESS; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_anim = true; obj->save_flags = true; Object_GetBone(obj, 1)->rot.y = true; } REGISTER_OBJECT(O_TORSO, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/trex.c ================================================ #include #include #include #include #include #include #include #include // clang-format off #define M_SHADOW_SIZE (UNIT_SHADOW / (g_TRVersion == 1 ? 4 : 2)) #define M_PIVOT_LENGTH (g_TRVersion == 1 ? 2000 : 1800) #define M_HITPOINTS 100 #define M_TOUCH_BITS 0b00110000'00000000 #define M_RADIUS (WALL_L / 3) // = 341 #define M_RUN_TURN (DEG_1 * 4) // = 728 #define M_WALK_TURN (DEG_1 * 2) // = 364 #define M_FRONT_ARC FRONT_ARC #define M_RUN_RANGE SQUARE(WALL_L * 5) // = 26214400 #define M_ATTACK_RANGE SQUARE(WALL_L * 4) // = 16777216 #define M_BITE_RANGE SQUARE(1500) // = 2250000 #define M_TOUCH_DAMAGE 1 #define M_TRAMPLE_DAMAGE 10 #define M_BITE_DAMAGE 10000 #define M_ROAR_CHANCE 512 #define M_SMARTNESS 0x7FFF // clang-format on typedef enum { M_ANIM_KILL = 11, } M_ANIM; typedef enum { M_STATE_EMPTY, M_STATE_STOP, M_STATE_WALK, M_STATE_RUN, M_STATE_ATTACK_1, M_STATE_DEATH, M_STATE_ROAR, M_STATE_ATTACK_2, M_STATE_KILL, } M_STATE; static void M_KillLara(ITEM *const item) { Lara_TakeDamage(M_BITE_DAMAGE, true); Creature_SpecialKill(item, M_ANIM_KILL, M_STATE_KILL, LS_EXTRA_TREX_KILL); Lara_Skin_SwapAllExtra(LS_EXTRA_TREX_KILL); } static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { if (g_Config.gameplay.disable_trex_collision && Item_Get(item_num)->hit_points <= 0) { return; } Creature_Collision(item_num, lara_item, coll); } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t head = 0; int16_t angle = 0; if (item->hit_points <= 0) { item->goal_anim_state = item->current_anim_state == M_STATE_STOP ? M_STATE_DEATH : M_STATE_STOP; goto finish; } AI_INFO info; Creature_AIInfo(item, &info); if (info.ahead) { head = info.angle; } Creature_Mood(item, &info, true); angle = Creature_Turn(item, creature->maximum_turn); if (item->touch_bits != 0) { if (item->current_anim_state == M_STATE_RUN) { Lara_TakeDamage(M_TRAMPLE_DAMAGE, false); } else { Lara_TakeDamage(M_TOUCH_DAMAGE, false); } } creature->flags = creature->mood != MOOD_ESCAPE && !info.ahead && info.enemy_facing > -M_FRONT_ARC && info.enemy_facing < M_FRONT_ARC; if (creature->flags == 0 && info.distance > M_BITE_RANGE && info.distance < M_ATTACK_RANGE && info.bite) { creature->flags = 1; } switch (item->current_anim_state) { case M_STATE_STOP: if (item->required_anim_state != M_STATE_EMPTY) { item->goal_anim_state = item->required_anim_state; } else if (info.distance < M_BITE_RANGE && info.bite) { item->goal_anim_state = M_STATE_ATTACK_2; } else if (creature->mood == MOOD_BORED || creature->flags != 0) { item->goal_anim_state = M_STATE_WALK; } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_WALK: creature->maximum_turn = M_WALK_TURN; if (creature->mood != MOOD_BORED || creature->flags == 0) { item->goal_anim_state = M_STATE_STOP; } else if (info.ahead && Random_GetControl() < M_ROAR_CHANCE) { item->required_anim_state = M_STATE_ROAR; item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_RUN: creature->maximum_turn = M_RUN_TURN; if (info.distance < M_RUN_RANGE && info.bite) { item->goal_anim_state = M_STATE_STOP; } else if (creature->flags != 0) { item->goal_anim_state = M_STATE_STOP; } else if ( creature->mood != MOOD_ESCAPE && info.ahead && Random_GetControl() < M_ROAR_CHANCE) { item->required_anim_state = M_STATE_ROAR; item->goal_anim_state = M_STATE_STOP; } else if (creature->mood == MOOD_BORED) { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_ATTACK_2: if ((item->touch_bits & M_TOUCH_BITS) != 0) { M_KillLara(item); } item->required_anim_state = M_STATE_WALK; break; } finish: Creature_Head(item, head / 2); if (creature != nullptr) { creature->neck_rotation = creature->head_rotation; } Creature_Animate(item_num, angle, 0); if (g_TRVersion == 1) { item->collidable = true; } } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } if (g_TRVersion == 1) { obj->initialise_func = Creature_Initialise; } obj->control_func = M_Control; obj->collision_func = M_Collision; obj->hit_points = M_HITPOINTS; obj->radius = M_RADIUS; obj->shadow_size = M_SHADOW_SIZE; obj->pivot_length = M_PIVOT_LENGTH; obj->smartness = M_SMARTNESS; obj->lot_setup = LOT_Setup(LOT_SETUP_BEAST); obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_anim = true; obj->save_flags = true; Object_GetBone(obj, 10)->rot.y = true; Object_GetBone(obj, 11)->rot.y = true; } REGISTER_OBJECT(O_TREX, M_Setup) REGISTER_OBJECT(O_DINO_WARRIOR, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/trex_alpha.c ================================================ #include #include #include #include #include #include #include #include #include #include // clang-format off #define M_PIVOT_LENGTH 1800 #define M_HITPOINTS 800 #define M_TOUCH_BITS 0b00110000'00000000 #define M_RADIUS (WALL_L / 3) // = 341 #define M_RUN_TURN (DEG_1 * 4) // = 728 #define M_WALK_TURN (DEG_1 * 2) // = 364 #define M_FRONT_ARC FRONT_ARC #define M_RUN_RANGE SQUARE(WALL_L * 5) // = 26214400 #define M_ATTACK_RANGE SQUARE(WALL_L * 4) // = 16777216 #define M_HIT_RADIUS SQUARE(M_RADIUS * 2) // = 465124 #define M_BITE_RANGE SQUARE(1500) // = 2250000 #define M_TOUCH_DAMAGE 1 #define M_TRAMPLE_DAMAGE 10 #define M_BITE_DAMAGE 10000 #define M_RAPTOR_DAMAGE (M_TRAMPLE_DAMAGE * 5) // = 50 #define M_ROAR_CHANCE 256 #define M_SMARTNESS 0x7FFF #define M_ATTACK_FRAME 20 #define M_AGGRESSION_TIME (LOGIC_FPS * 4) // = 120 #define M_DISTRACTION_COUNT 3 #define M_FLARE_SEEN (-1) // clang-format on typedef enum { M_ANIM_KILL = 11, } M_ANIM; typedef enum { M_STATE_EMPTY, M_STATE_STOP, M_STATE_WALK, M_STATE_RUN, M_STATE_ATTACK_1, M_STATE_DEATH, M_STATE_ROAR, M_STATE_ATTACK_2, M_STATE_KILL, M_STATE_LONG_ROAR_START, M_STATE_LONG_ROAR_MID, M_STATE_LONG_ROAR_END, M_STATE_SNIFF_START, M_STATE_SNIFF_MID, M_STATE_SNIFF_END, } M_STATE; typedef struct { int32_t aggression_timer; int32_t distraction_count; } M_PRIV; static BITE m_Bite = { .pos = { .x = 0, .y = 32, .z = 64 }, .mesh_num = 13, }; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "aggression_timer", &p->aggression_timer)); JSON_SHOULD(JSON_READ(io, "distraction_count", &p->distraction_count)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "aggression_timer", p->aggression_timer); JSONW_WRITE(io, "distraction_count", p->distraction_count); } static void M_KillLara(ITEM *const item) { Lara_TakeDamage(M_BITE_DAMAGE, true); Creature_SpecialKill(item, M_ANIM_KILL, M_STATE_KILL, LS_EXTRA_TREX_KILL); Lara_Skin_SwapAllExtra(LS_EXTRA_TREX_KILL); } static bool M_IsCandidateTarget(const ITEM *const item) { if (item->object_id == O_RAPTOR) { return item->status == IS_ACTIVE && item->hit_points > 0; } if (item->object_id == O_FLARE_ITEM) { return FlareItem_IsActive(item) && item->hit_points != M_FLARE_SEEN; } return false; } static void M_CalculateTarget(ITEM *const item) { CREATURE *const creature = item->creature_data; if (creature->hurt_by_lara) { creature->enemy = Lara_GetItem(); return; } creature->enemy = nullptr; int32_t best_distance = INT32_MAX; Room_GetNearbyRooms(item->pos, WALL_L * 4, 0, item->room_num); for (int32_t i = 0; i < Room_DrawGetCount(); i++) { const ROOM *const nearby_room = Room_Get(Room_DrawGetRoom(i)); int16_t target_item_num = nearby_room->item_num; while (target_item_num != NO_ITEM) { const ITEM *const candidate = Item_Get(target_item_num); if (!M_IsCandidateTarget(candidate)) { goto loopend; } const XYZ_32 delta = { .x = (candidate->pos.x - item->pos.x) >> 6, .y = (candidate->pos.y - item->pos.y) >> 6, .z = (candidate->pos.z - item->pos.z) >> 6, }; const int32_t distance = XYZ_32_GetLength2(delta); if (distance < best_distance) { creature->enemy = (ITEM *)candidate; best_distance = distance; } loopend: target_item_num = candidate->next_item; } } if (creature->enemy != nullptr && creature->enemy->object_id == O_FLARE_ITEM) { creature->enemy->hit_points = 1; } } static bool M_CanAttack(const ITEM *const item, const ITEM *const target) { if (target == Lara_GetItem()) { return (item->touch_bits & M_TOUCH_BITS) != 0; } if (target == nullptr || Item_GetRelativeFrame(item) != M_ATTACK_FRAME) { return false; } const XYZ_32 pos = { .x = ABS(target->pos.x - (item->pos.x + ((M_PIVOT_LENGTH * Math_Sin(item->rot.y)) >> W2V_SHIFT))), .y = ABS(target->pos.y - item->pos.y), .z = ABS(target->pos.z - (item->pos.z + ((M_PIVOT_LENGTH * Math_Cos(item->rot.y)) >> W2V_SHIFT))), }; return pos.x < M_HIT_RADIUS && pos.y <= M_HIT_RADIUS && pos.z < M_HIT_RADIUS; } static void M_Attack(ITEM *const item, ITEM *const target) { if (target == Lara_GetItem()) { Creature_Effect(item, &m_Bite, Spawn_Blood); M_KillLara(item); } else if (target->object_id == O_RAPTOR) { Creature_Effect(item, &m_Bite, Spawn_Blood); target->hit_points -= M_RAPTOR_DAMAGE; target->hit_status = true; } else if (target->object_id == O_FLARE_ITEM) { target->hit_points = M_FLARE_SEEN; } } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; CREATURE *const creature = item->creature_data; int16_t angle = 0; if (item->hit_points <= 0) { item->goal_anim_state = item->current_anim_state == M_STATE_STOP ? M_STATE_DEATH : M_STATE_STOP; goto finish; } M_CalculateTarget(item); AI_INFO info; Creature_AIInfo(item, &info); Creature_UpdateMood(item, &info, true); ITEM *const lara_item = Lara_GetItem(); if (p->aggression_timer == 0 && p->distraction_count == 0 && creature->enemy == lara_item) { creature->mood = MOOD_BORED; } Creature_ApplyMood(item, &info, true); if (creature->mood == MOOD_BORED) { creature->maximum_turn >>= 1; } angle = Creature_Turn(item, creature->maximum_turn); if (item->touch_bits != 0) { if (item->current_anim_state == M_STATE_RUN) { Lara_TakeDamage(M_TRAMPLE_DAMAGE, false); } else { Lara_TakeDamage(M_TOUCH_DAMAGE, false); } } creature->flags = creature->mood != MOOD_ESCAPE && !info.ahead && info.enemy_facing > -M_FRONT_ARC && info.enemy_facing < M_FRONT_ARC; if (creature->flags == 0 && info.distance > M_BITE_RANGE && info.distance < M_ATTACK_RANGE && info.bite) { creature->flags = 1; } const LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->gun_type != LGT_FLARE && (lara_item->current_anim_state == LS(LS_STOP) || lara_item->current_anim_state == LS(LS_CROUCH_IDLE)) && lara_item->current_anim_state == lara_item->goal_anim_state && !item->hit_status) { p->aggression_timer--; CLAMPL(p->aggression_timer, 0); } else { p->aggression_timer = M_AGGRESSION_TIME; p->distraction_count = M_DISTRACTION_COUNT; } switch (item->current_anim_state) { case M_STATE_STOP: if (item->required_anim_state != M_STATE_EMPTY) { item->goal_anim_state = item->required_anim_state; } else if (creature->mood == MOOD_BORED || creature->flags != 0) { item->goal_anim_state = M_STATE_WALK; } else if (creature->mood == MOOD_ESCAPE) { if (lara->target != item && info.ahead && !item->hit_status) { item->goal_anim_state = Random_GetControl() < M_ROAR_CHANCE ? M_STATE_ROAR : M_STATE_STOP; } else { item->goal_anim_state = M_STATE_RUN; } } else if (info.distance < M_BITE_RANGE && info.bite) { if (p->aggression_timer != 0) { item->goal_anim_state = M_STATE_ATTACK_2; } else if ((Random_GetControl() & 1) != 0) { if (p->distraction_count != 0) { item->goal_anim_state = M_STATE_LONG_ROAR_START; } } else if (p->distraction_count != 0) { item->goal_anim_state = M_STATE_SNIFF_START; } } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_WALK: creature->maximum_turn = M_WALK_TURN; if (creature->mood != MOOD_BORED || creature->flags == 0) { item->goal_anim_state = M_STATE_STOP; } else if (info.ahead && Random_GetControl() < M_ROAR_CHANCE) { item->required_anim_state = M_STATE_ROAR; item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_RUN: creature->maximum_turn = M_RUN_TURN; if (info.distance < M_RUN_RANGE && info.bite) { item->goal_anim_state = M_STATE_STOP; } else if (creature->flags != 0) { item->goal_anim_state = M_STATE_STOP; } else if ( creature->mood == MOOD_ESCAPE || !info.ahead || Random_GetControl() >= M_ROAR_CHANCE) { if (creature->mood == MOOD_BORED || (creature->mood == MOOD_ESCAPE && lara->target != item && info.ahead)) { item->goal_anim_state = M_STATE_STOP; } } else { item->required_anim_state = M_STATE_ROAR; item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_ROAR: creature->maximum_turn = 0; break; case M_STATE_ATTACK_2: creature->maximum_turn = M_WALK_TURN; if (M_CanAttack(item, creature->enemy)) { if (creature->enemy == lara_item) { creature->maximum_turn = 0; } M_Attack(item, creature->enemy); } if ((Random_GetControl() & 3) == 0) { item->required_anim_state = M_STATE_WALK; } break; case M_STATE_KILL: creature->maximum_turn = 0; Creature_Effect(item, &m_Bite, Spawn_Blood); break; case M_STATE_LONG_ROAR_START: case M_STATE_SNIFF_START: const bool roar_state = item->current_anim_state == M_STATE_LONG_ROAR_START; creature->maximum_turn = 0; if (p->distraction_count > 0 && Item_TestFrameEqual(item, 0)) { p->distraction_count--; if (creature->enemy != nullptr && creature->enemy->object_id == O_FLARE_ITEM) { M_Attack(item, creature->enemy); if (roar_state) { p->distraction_count--; } else { p->distraction_count = 0; } } } if (roar_state) { item->goal_anim_state = M_STATE_LONG_ROAR_END; } break; case M_STATE_SNIFF_MID: creature->maximum_turn = 0; if (!Item_TestFrameEqual(item, 0)) { break; } if ((Random_GetControl() & 1) != 0 && p->distraction_count != 0 && p->aggression_timer == 0) { item->goal_anim_state = M_STATE_SNIFF_MID; p->distraction_count--; CLAMPL(p->distraction_count, 0); } else { item->goal_anim_state = M_STATE_SNIFF_END; } break; } finish: Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->collision_func = Creature_Collision; obj->control_func = M_Control; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->radius = M_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->pivot_length = M_PIVOT_LENGTH; obj->smartness = M_SMARTNESS; obj->lot_setup = LOT_Setup(LOT_SETUP_BEAST); obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_anim = true; obj->save_flags = true; obj->hit_points = M_HITPOINTS; Object_GetBone(obj, 9)->rot.y = true; Object_GetBone(obj, 11)->rot.y = true; Object_GetBone(obj, 20)->rot.y = true; Object_GetBone(obj, 22)->rot.y = true; } REGISTER_OBJECT(O_TREX_ALPHA, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/tribe_axeman.c ================================================ #include #include #include #include #include #include #include #include #include // clang-format off #define M_WALK_TURN (9 * DEG_1) #define M_RUN_TURN (6 * DEG_1) #define M_OTHER_TURN (4 * DEG_1) #define M_CLOSE_RANGE SQUARE(WALL_L * 2 / 3) #define M_LONG_RANGE SQUARE(WALL_L) #define M_WALK_RANGE SQUARE(WALL_L * 2) #define M_ESCAPE_RANGE SQUARE(WALL_L * 3) #define M_HIT_RANGE (STEP_L * 2) #define M_TOUCH_BITS (1 << 13) // = 0x2000 // clang-format on typedef struct { bool wants_wait_2; } M_PRIV; typedef enum { M_STATE_NULL, M_STATE_WAIT_1, M_STATE_WALK, M_STATE_RUN, M_STATE_ATTACK_1, M_STATE_ATTACK_2, M_STATE_ATTACK_3, M_STATE_ATTACK_4, M_STATE_AIM_3, M_STATE_DEATH, M_STATE_ATTACK_5, M_STATE_WAIT_2, M_STATE_ATTACK_6 } M_STATE; typedef enum { M_ANIM_DEATH_STAND = 20, M_ANIM_DEATH_DOWN = 21, } M_ANIM; typedef struct { uint8_t start_frame; uint8_t end_frame; uint8_t damage; } M_HIT_FRAME; static BITE m_AxeHit = { .pos = { .x = 0, .y = 16, .z = 265 }, .mesh_num = 13, }; static M_HIT_FRAME m_HitFrames[13] = { {}, {}, {}, {}, {}, { .start_frame = 2, .end_frame = 12, .damage = 8 }, { .start_frame = 8, .end_frame = 9, .damage = 32 }, { .start_frame = 19, .end_frame = 28, .damage = 8 }, {}, {}, { .start_frame = 7, .end_frame = 14, .damage = 8 }, {}, { .start_frame = 15, .end_frame = 19, .damage = 32 } }; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "wants_wait_2", &p->wants_wait_2)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "wants_wait_2", p->wants_wait_2); } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } const ITEM *const lara_item = Lara_GetItem(); const LARA_INFO *const lara = Lara_GetLaraInfo(); ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; CREATURE *const creature = item->creature_data; int16_t angle = 0; int16_t head = 0; int16_t tilt = 0; if (item->hit_points <= 0) { if (item->current_anim_state != M_STATE_DEATH) { if (item->current_anim_state == M_STATE_WAIT_1 || item->current_anim_state == M_STATE_ATTACK_4) { Item_SwitchToAnim(item, M_ANIM_DEATH_DOWN, 0); } else { Item_SwitchToAnim(item, M_ANIM_DEATH_STAND, 0); } item->current_anim_state = M_STATE_DEATH; } } else { AI_INFO info; Creature_AIInfo(item, &info); Creature_UpdateMood(item, &info, true); if (creature->enemy == lara_item && creature->hurt_by_lara && info.distance > M_ESCAPE_RANGE && info.enemy_facing < 0x3000 && info.enemy_facing > -0x3000) { creature->mood = MOOD_ESCAPE; } Creature_ApplyMood(item, &info, true); angle = Creature_Turn(item, creature->maximum_turn); if (info.ahead) { head = info.angle; } switch (item->current_anim_state) { case M_STATE_WAIT_1: creature->maximum_turn = M_OTHER_TURN; creature->flags = 0; if (creature->mood == MOOD_BORED) { creature->maximum_turn = 0; if (Random_GetControl() < 0x100) { item->goal_anim_state = M_STATE_WALK; } } else if (creature->mood == MOOD_ESCAPE) { if (lara->target != item && info.ahead && !item->hit_status) { item->goal_anim_state = M_STATE_WAIT_1; } else { item->goal_anim_state = M_STATE_RUN; } } else if (p->wants_wait_2) { p->wants_wait_2 = false; item->goal_anim_state = M_STATE_WAIT_2; } else if (info.ahead && info.distance < M_CLOSE_RANGE) { item->goal_anim_state = M_STATE_ATTACK_4; } else if (info.ahead && info.distance < M_LONG_RANGE) { if (Random_GetControl() < 0x4000) { item->goal_anim_state = M_STATE_WALK; } else { item->goal_anim_state = M_STATE_ATTACK_4; } } else if (info.ahead && info.distance < M_WALK_RANGE) { item->goal_anim_state = M_STATE_WALK; } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_WALK: creature->maximum_turn = M_WALK_TURN; creature->flags = 0; tilt = angle >> 3; if (creature->mood == MOOD_BORED) { creature->maximum_turn = 409; if (Random_GetControl() < 0x100) { if (Random_GetControl() < 0x2000) { item->goal_anim_state = M_STATE_WAIT_1; } else { item->goal_anim_state = M_STATE_WAIT_2; } } } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if (info.ahead && info.distance < M_CLOSE_RANGE) { if (Random_GetControl() < 0x2000) { item->goal_anim_state = M_STATE_WAIT_1; } else { item->goal_anim_state = M_STATE_WAIT_2; } } else if (info.distance > M_WALK_RANGE) { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_RUN: creature->maximum_turn = M_RUN_TURN; creature->flags = 0; tilt = angle >> 2; if (creature->mood == MOOD_BORED) { creature->maximum_turn = 1.5f * DEG_1; if (Random_GetControl() < 0x100) { if (Random_GetControl() < 0x4000) { item->goal_anim_state = M_STATE_WAIT_1; } else { item->goal_anim_state = M_STATE_WAIT_2; } } } else if ( creature->mood == MOOD_ESCAPE && lara->target != item && info.ahead) { item->goal_anim_state = M_STATE_WAIT_2; } else if (info.bite || info.distance < M_WALK_RANGE) { if (Random_GetControl() < 0x4000) { item->goal_anim_state = M_STATE_ATTACK_6; } else if (Random_GetControl() < 0x2000) { item->goal_anim_state = M_STATE_ATTACK_5; } else { item->goal_anim_state = M_STATE_WALK; } } break; case M_STATE_ATTACK_2: case M_STATE_ATTACK_3: case M_STATE_ATTACK_4: case M_STATE_ATTACK_5: case M_STATE_ATTACK_6: p->wants_wait_2 = true; creature->maximum_turn = M_OTHER_TURN; creature->flags = Item_GetRelativeFrame(item); const M_HIT_FRAME *const hit_frame = &m_HitFrames[item->current_anim_state]; ITEM *const enemy = creature->enemy; if (enemy == lara_item) { if (item->touch_bits & M_TOUCH_BITS && creature->flags >= hit_frame->start_frame && creature->flags <= hit_frame->end_frame) { Lara_TakeDamage(hit_frame->damage, true); for (int32_t i = 0; i < hit_frame->damage; i += 8) { Creature_Effect(item, &m_AxeHit, Spawn_Blood); } Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL); } } else if (enemy != nullptr) { if (Item_IsNearby(enemy, item, M_HIT_RANGE)) { if (creature->flags >= hit_frame->start_frame && creature->flags <= hit_frame->end_frame) { enemy->hit_points -= 2; enemy->hit_status = 1; Creature_Effect(item, &m_AxeHit, Spawn_Blood); Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL); } } } break; case M_STATE_AIM_3: creature->maximum_turn = M_OTHER_TURN; if (info.bite || info.distance < M_CLOSE_RANGE) { item->goal_anim_state = M_STATE_ATTACK_3; } else { item->goal_anim_state = M_STATE_WAIT_2; } break; case M_STATE_WAIT_2: creature->maximum_turn = M_OTHER_TURN; creature->flags = 0; if (creature->mood == MOOD_BORED) { creature->maximum_turn = 0; if (Random_GetControl() < 0x100) { item->goal_anim_state = M_STATE_WALK; } } else if (creature->mood == MOOD_ESCAPE) { if (lara->target != item && info.ahead && !item->hit_status) { item->goal_anim_state = M_STATE_WAIT_1; } else { item->goal_anim_state = M_STATE_RUN; } } else if (info.ahead && info.distance < M_CLOSE_RANGE) { if (Random_GetControl() < 0x800) { item->goal_anim_state = M_STATE_ATTACK_2; } else { item->goal_anim_state = M_STATE_AIM_3; } } else if (info.distance < M_WALK_RANGE) { item->goal_anim_state = M_STATE_WALK; } else { item->goal_anim_state = M_STATE_RUN; } break; } } Creature_Tilt(item, tilt); Creature_Joint(item, 0, head >> 1); Creature_Joint(item, 1, head >> 1); Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->shadow_size = UNIT_SHADOW / 2; obj->hit_points = 28; obj->radius = 102; obj->pivot_length = 0; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 13)->rot.y = true; Object_GetBone(obj, 6)->rot.y = true; } REGISTER_OBJECT(O_TRIBE_AXEMAN, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/tribe_boss.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_MAX_HEAD_ATTACKS 4 typedef enum { M_STATE_WAIT = 0, M_STATE_ATTACK_HEAD = 1, M_STATE_ATTACK_HAND = 2, M_STATE_DEATH = 3, } M_STATE; typedef enum { M_ANIM_DEATH = 3, } M_ANIM; typedef enum { M_ATTACK_HEAD, M_ATTACK_HAND_1, M_ATTACK_HAND_2, } M_ATTACK_TYPE; typedef struct { XYZ_16 pos; RGB_888 sub; RGB_888 color; } M_SHIELD_POINT; typedef struct { bool lizard_active; } M_SHARED_PRIV; typedef struct { XYZ_32 pos; uint16_t y_rot; } M_LIZARD_SUMMON_COORDS; typedef struct { uint8_t dead; int16_t attack_count; int16_t death_count; uint8_t attack_flag; M_ATTACK_TYPE attack_type; uint8_t attack_head_count; uint8_t ring_count; int16_t explode_count; int16_t lizard_item_num; int16_t lizard_room_num; bool dropped_item; XYZ_32 beam_target; M_SHIELD_POINT shield[5][8]; XYZ_32 trig_dynamics[3]; bool shield_on; bool shield_active; bool turned; M_LIZARD_SUMMON_COORDS lizard_summon_coords[2]; M_SHARED_PRIV *shared; } M_PRIV; static const BITE m_HelmetSpikes[5] = { // Helmet spikes { .pos = { .x = 120, .y = 68, .z = 136 }, .mesh_num = 8 }, { .pos = { .x = 128, .y = -64, .z = 136 }, .mesh_num = 8 }, { .pos = { .x = 8, .y = -120, .z = 136 }, .mesh_num = 8 }, { .pos = { .x = -128, .y = -64, .z = 136 }, .mesh_num = 8 }, { .pos = { .x = -124, .y = 64, .z = 126 }, .mesh_num = 8 }, }; static BITE m_EnergyHit = { .pos = { .x = 8, .y = 32, .z = 400 }, .mesh_num = 8, }; static const int32_t m_Heights[5] = { -1536, -1280, -832, -384, 0 }; static const int32_t m_Dist[5] = { 200, 400, 500, 500, 475 }; static const int32_t m_DDist[5] = { 1600, 5600, 6400, 5600, 1600 }; static const int32_t m_DHeights1[5] = { -7680, -4224, -768, 2688, 6144 }; static const int32_t m_DHeights2[5] = { -1536, -1152, -768, -384, 0 }; static int32_t m_DeathDist[5] = {}; static int32_t m_DeathHeights[5] = {}; static M_SHARED_PRIV m_SharedPriv = {}; static int16_t M_FindLizard(const int16_t room_num) { for (int32_t item_num = 0; item_num < Item_GetTotalCount(); item_num++) { const ITEM *const item = Item_Get(item_num); if (item->object_id == O_LIZARD && item->room_num == room_num) { return item_num; } } return NO_ITEM; } static void M_RotateHeadXAngle(ITEM *const item) { XYZ_32 lpos = {}; Collide_GetJointAbsPosition(Lara_GetItem(), &lpos, LM_HIPS); XYZ_32 pos = {}; Collide_GetJointAbsPosition(item, &pos, 0); const int32_t dx = ABS(pos.x - lpos.x); const int32_t dy = pos.y - lpos.y; const int32_t dz = ABS(pos.z - lpos.z); const int16_t ang = Math_Atan(Math_Sqrt(SQUARE(dx) + SQUARE(dz)), dy); if (ABS(ang) < 0x2000) { Creature_Joint(item, 2, ang); } else { Creature_Joint(item, 2, 0); } } static void M_Explode(ITEM *const item) { M_PRIV *const p = item->priv; p->shield_on = false; if (p->explode_count == 1 || p->explode_count == 15 || p->explode_count == 25 || p->explode_count == 35 || p->explode_count == 45 || p->explode_count == 55) { const XYZ_32 pos = { .x = item->pos.x + (Random_GetDraw() & 0x3FF) - 512, .y = item->pos.y - (Random_GetDraw() & 0x3FF) - 256, .z = item->pos.z + (Random_GetDraw() & 0x3FF) - 512, }; FX_RING *const ring = FX_Ring_GetRing(FX_RING_TYPE_BLAST, p->ring_count); if (ring != nullptr) { ring->pos = pos; ring->on = 4; FX_Ring_Sync(ring); } p->ring_count++; Sparks_TriggerExplosionSparks(pos, 3, -2, 2, 0); for (int32_t i = 0; i < 2; i++) { Sparks_TriggerExplosionSparks(pos, 3, -1, 2, 0); } Sound_Effect(SFX_BLAST_CIRCLE, &item->pos, 0x800000 | SPM_PITCH); } for (int32_t i = 0; i < 5; i++) { if (p->explode_count < 128) { m_DeathDist[i] = (m_DDist[i] >> 4) + ((p->explode_count * m_DDist[i]) >> 7); m_DeathHeights[i] = m_DHeights2[i] + ((p->explode_count * (m_DHeights1[i] - m_DHeights2[i])) >> 7); } } const int32_t time4 = Output_GetTimeInGame() * 4; for (int32_t i = 0; i < 5; i++) { const int32_t y = m_DeathHeights[i]; const int32_t rad = m_DeathDist[i]; int32_t angle = (time4 & 0x3F) << 3; for (int32_t j = 0; j < 8; j++) { M_SHIELD_POINT *const shield = &p->shield[i][j]; shield->pos = (XYZ_16) { .x = ((rad * Math_Sin(angle << 4)) >> 13), .y = y, .z = ((rad * Math_Cos(angle << 4)) >> 13), }; if (i != 0 && i != 4 && p->explode_count < 64) { const int32_t m = 64 - p->explode_count; int32_t r = (m * (Random_GetDraw() & 0x1F)) >> 6; int32_t b = (Random_GetDraw() & 0x3F) + 224; int32_t g = (m * ((b >> 2) + (Random_GetDraw() & 0x3F))) >> 6; b = (m * b) >> 6; shield->color = (RGB_888) { r, g, b }; } else { shield->color = (RGB_888) {}; } angle = (angle + 512) & 0xFFF; } } } static void M_Die(int16_t item_num) { ITEM *const item = Item_Get(item_num); item->collidable = 0; item->hit_points = 0; Item_Kill(item_num); LOT_DisableBaddieAI(item_num); item->flags |= IF_INVISIBLE; } static bool M_CanBeExploded(const ITEM *const item) { return false; } static void M_TriggerSummonSmoke(const XYZ_32 pos) { SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 16; spark->src_color.g = 64; spark->src_color.b = 0; spark->dst_color.r = 8; spark->dst_color.g = 32; spark->dst_color.b = 0; spark->fade_to_black = 64; spark->col_fade_speed = (Random_GetControl() & 7) + 16; spark->draw_type = DRAW_BLEND_ADD; spark->life = (Random_GetControl() & 0xF) + 96; spark->s_life = spark->life; spark->extras = 0; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 0x7F) - 64; spark->pos.y = pos.y - (Random_GetControl() & 0x1F); spark->pos.z = pos.z + (Random_GetControl() & 0x7F) - 64; spark->vel.x = (Random_GetControl() & 0xFF) - 128; spark->vel.y = -16 - (Random_GetControl() & 0xF); spark->vel.z = (Random_GetControl() & 0xFF) - 128; spark->friction = 0; if (Random_GetControl() & 1) { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_OUTSIDE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = Random_GetControl() & 0xFFF; if (Random_GetControl() & 1) { spark->rot_add = -4 - (Random_GetControl() & 7); } else { spark->rot_add = (Random_GetControl() & 7) + 4; } } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_OUTSIDE | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->scalar = 3; spark->sprite_idx = (uint8_t)Object_Get(O_EXPLOSION_1)->mesh_idx; spark->gravity = -8 - (Random_GetControl() & 7); spark->max_y_vel = -4 - (Random_GetControl() & 7); spark->dst_size.width = (Random_GetControl() & 0x1F) + 128; spark->src_size.width = spark->dst_size.width >> 1; spark->size.width = spark->dst_size.width >> 1; spark->dst_size.height = spark->dst_size.width + (Random_GetControl() & 0x1F) + 32; spark->src_size.height = spark->dst_size.height >> 1; spark->size.height = spark->dst_size.height >> 1; Sparks_FinishSetup(spark); } static bool M_CanDropItems(const ITEM *const item) { if (item->hit_points > 0) { return false; } if ((item->flags & IF_KILLED) != 0) { return true; } return item->current_anim_state == M_STATE_DEATH && Item_GetRelativeFrame(item) > 119; } static const M_LIZARD_SUMMON_COORDS *M_GetLizardSummonCoords( const M_PRIV *const p) { return &p->lizard_summon_coords[p->attack_type == M_ATTACK_HAND_1 ? 0 : 1]; } static void M_TriggerLizard(M_PRIV *const p) { ITEM *const item = Item_Get(p->lizard_item_num); int16_t room_num = p->lizard_room_num; item->object_id = O_LIZARD; item->pos = M_GetLizardSummonCoords(p)->pos; item->anim_num = Object_Get(O_LIZARD)->anim_idx; item->frame_num = Item_GetAnim(item)->frame_base; item->current_anim_state = Item_GetAnim(item)->current_anim_state; item->goal_anim_state = Item_GetAnim(item)->current_anim_state; item->required_anim_state = M_STATE_WAIT; item->rot.x = 0; if (p->attack_type == M_ATTACK_HAND_1) { item->rot.y = -0x8000; } else { item->rot.y = 0; } item->rot.z = 0; item->timer = 0; item->flags = 0; item->creature_data = nullptr; item->mesh_bits = -1; item->hit_points = Object_Get(O_LIZARD)->hit_points; item->active = 0; item->status = IS_ACTIVE; item->collidable = 1; item->flags &= ~(IF_KILLED | IF_ONE_SHOT); item->include_in_kill_stats = false; // Item_Kill removes it from room item chains; reinsert even when room is // unchanged. Item_UpdateRoom(p->lizard_item_num, NO_ROOM); Item_UpdateRoom(p->lizard_item_num, room_num); Item_AddActive(p->lizard_item_num); LOT_EnableBaddieAI(p->lizard_item_num, true); Room_GetSector(item->pos, &room_num); if (item->room_num != room_num) { Item_UpdateRoom(p->lizard_item_num, room_num); } TribeBoss_SetLizardActive(true); } static void M_TriggerElectricSparks( const ITEM *const item, const XYZ_32 pos, const bool shield) { M_PRIV *const p = item->priv; const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - pos.x; const int32_t dz = lara_item->pos.z - pos.z; if (dx < -0x5000 || dx > 0x5000 || dz < -0x5000 || dz > 0x5000) { return; } p->trig_dynamics[1] = pos; SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 255; spark->src_color.g = 255; spark->src_color.b = 255; if (shield) { spark->dst_color.r = 255; spark->dst_color.g = (Random_GetControl() & 0x7F) + 64; spark->dst_color.b = 0; } else if ( p->attack_type == M_ATTACK_HAND_1 || p->attack_type == M_ATTACK_HAND_2) { spark->dst_color.r = 0; spark->dst_color.b = (Random_GetControl() & 0x7F) + 64; spark->dst_color.g = (spark->dst_color.b >> 1) + 128; } else { spark->dst_color.r = 0; spark->dst_color.g = (Random_GetControl() & 0x7F) + 64; spark->dst_color.b = (spark->dst_color.g >> 1) + 128; } spark->col_fade_speed = 3; spark->fade_to_black = 8; spark->life = 16; spark->s_life = 16; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16; spark->pos.y = pos.y + (Random_GetControl() & 0x1F) - 16; spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16; spark->vel.x = 4 * (Random_GetControl() & 0x1FF) - 1024; spark->vel.y = 2 * (Random_GetControl() & 0x1FF) - 512; spark->vel.z = 4 * (Random_GetControl() & 0x1FF) - 1024; if (shield) { spark->vel.x >>= 1; spark->vel.y >>= 1; spark->vel.z >>= 1; } spark->friction = 4; spark->flags = SPARK_F_SCALE; spark->scalar = 3; spark->size.width = (Random_GetControl() & 1) + 1; spark->src_size.width = spark->size.width; spark->dst_size.width = (Random_GetControl() & 3) + 4; spark->size.height = (Random_GetControl() & 1) + 1; spark->src_size.height = spark->size.height; spark->dst_size.height = (Random_GetControl() & 3) + 4; spark->gravity = 15; spark->max_y_vel = 0; Sparks_FinishSetup(spark); } static bool M_LaraOnLOS( const GAME_VECTOR *const src, const GAME_VECTOR *const beam_target) { XYZ_32 lara_pos = {}; Collide_GetJointAbsPosition(Lara_GetItem(), &lara_pos, LM_HIPS); const int32_t bx = beam_target->x - src->x; const int32_t by = beam_target->y - src->y; const int32_t bz = beam_target->z - src->z; const int32_t lx = lara_pos.x - src->x; const int32_t ly = lara_pos.y - src->y; const int32_t lz = lara_pos.z - src->z; const int64_t beam_len2 = (int64_t)bx * bx + (int64_t)by * by + (int64_t)bz * bz; if (beam_len2 == 0) { return false; } const int64_t proj = (int64_t)lx * bx + (int64_t)ly * by + (int64_t)lz * bz; if (proj <= 0 || proj > beam_len2) { return false; } // Keep kill logic close to the actual beam spine, not just generic LOS. const int64_t lara_len2 = (int64_t)lx * lx + (int64_t)ly * ly + (int64_t)lz * lz; int64_t dist2_num = lara_len2 * beam_len2 - proj * proj; if (dist2_num < 0) { dist2_num = 0; } const int32_t hit_radius = 192; const int64_t max_dist2_num = (int64_t)hit_radius * hit_radius * beam_len2; if (dist2_num > max_dist2_num) { return false; } GAME_VECTOR lara_target = { .pos = { .x = lara_pos.x, .y = lara_pos.y, .z = lara_pos.z }, .room_num = Lara_GetItem()->room_num, }; return LOS_Check(src, &lara_target, true); } static int32_t M_GetElectricIntensity( const int32_t c, const int32_t scale, const bool copy) { int32_t value = c; if (value > 255) { value = 511 - value; if (value < 0) { value = 0; } } if (copy) { value >>= 1; } return (scale * value) >> 6; } static void M_DrawElectricBeamQuad( const ITEM *const item, const XYZ_32 prev_l, const XYZ_32 prev_r, const XYZ_32 cur_l, const XYZ_32 cur_r, const int32_t c0, const int32_t c1, const bool copy) { const M_PRIV *const p = item->priv; const int32_t i0 = M_GetElectricIntensity(c0, 64, copy); const int32_t i1 = M_GetElectricIntensity(c1, 64, copy); RGBA_8888 c_prev = {}; RGBA_8888 c_cur = {}; if (p->attack_type == M_ATTACK_HEAD) { c_prev = (RGBA_8888) { i0 >> 1, i0, i0, 0xC0 }; c_cur = (RGBA_8888) { i1 >> 1, i1, i1, 0xC0 }; } else { c_prev = (RGBA_8888) { i0 >> 1, i0, i0 >> 1, 0xC0 }; c_cur = (RGBA_8888) { i1 >> 1, i1, i1 >> 1, 0xC0 }; } const XYZ_32 world_pos[4] = { prev_l, prev_r, cur_r, cur_l }; const RGBA_8888 color[4] = { c_prev, c_prev, c_cur, c_cur }; const float disp[4][2] = {}; OutputSource_PolyFX_StageQuadExt( -1, world_pos, disp, color, VERT_FLAT_SHADED | VERT_NO_LIGHTING | VERT_NO_WIBBLE, DRAW_BLEND_ADD); } static void M_TriggerElectricBeam( const ITEM *const item, GAME_VECTOR *const src, const bool copy, const bool do_state, const bool do_render) { M_PRIV *const p = item->priv; GAME_VECTOR target = {}; const int16_t angle = (item->rot.y >> 4) & 0xFFF; src->room_num = item->room_num; int32_t dx = p->beam_target.x - src->x; int32_t dy = p->beam_target.y - src->y; int32_t dz = p->beam_target.z - src->z; int32_t longest = ABS(dx); if (ABS(dy) > longest) { longest = ABS(dy); } if (ABS(dz) > longest) { longest = ABS(dz); } if (longest < 20480) { longest = 20480 / longest + 1; dx *= longest; dy *= longest; dz *= longest; } target.x = src->x + dx; target.y = src->y + dy; target.z = src->z + dz; LOS_Check(src, &target, true); if (do_state) { LARA_INFO *const lara_info = Lara_GetLaraInfo(); ITEM *const lara_item = Lara_GetItem(); if (lara_info->electric == 0 && !copy && p->attack_type == M_ATTACK_HEAD && M_LaraOnLOS(src, &target) && !g_Config.debug.enable_invulnerability) { lara_item->hit_points = 0; lara_info->electric = 1; } M_TriggerElectricSparks(item, target.pos, false); } if (!do_render) { return; } dx = ABS(target.x - src->x); dz = ABS(target.z - src->z); int32_t n_segments = dx >= dz ? dx >> 8 : dz >> 8; CLAMP(n_segments, 8, 24); const XYZ_32 src_pos = { src->x, src->y, src->z }; const XYZ_32 dst_pos = { target.x, target.y, target.z }; const int16_t side_angle = (angle + 1024) & 0xFFF; const int32_t x_off = (p->attack_type == M_ATTACK_HEAD) ? (Math_Sin(side_angle << 4) >> 10) : (Math_Sin(side_angle << 4) >> 11); const int32_t z_off = (p->attack_type == M_ATTACK_HEAD) ? (Math_Cos(side_angle << 4) >> 10) : (Math_Cos(side_angle << 4) >> 11); int32_t y_off1 = 0; int32_t y_off2 = 0; XYZ_32 prev_l = src_pos; XYZ_32 prev_r = src_pos; int32_t prev_c = 0; for (int32_t i = 1; i <= n_segments; i++) { const XYZ_32 center = { .x = src_pos.x + ((dst_pos.x - src_pos.x) * i) / n_segments, .y = src_pos.y + ((dst_pos.y - src_pos.y) * i) / n_segments, .z = src_pos.z + ((dst_pos.z - src_pos.z) * i) / n_segments, }; int32_t c = 0; XYZ_32 cur_l = center; XYZ_32 cur_r = center; if (i != n_segments) { const XYZ_16 point = Lara_Electricity_GetPoint((copy ? 4 : 0) + i); const int32_t xs = copy ? -point.x : point.x; const int32_t ys = copy ? -(point.y >> 1) : (point.y >> 1); const int32_t zs = copy ? -point.z : point.z; y_off1 += (Random_GetDraw() & 0x1F) - 16; y_off2 += (Random_GetDraw() & 0x1F) - 16; CLAMP(y_off1, -192, 192); CLAMP(y_off2, -192, 192); cur_l = (XYZ_32) { center.x + xs + x_off, center.y + ys + y_off1, center.z + zs + z_off, }; cur_r = (XYZ_32) { center.x + xs - x_off, center.y + ys + y_off2, center.z + zs - z_off, }; c = (Random_GetDraw() & 0xFF) >> copy; } M_DrawElectricBeamQuad( item, prev_l, prev_r, cur_l, cur_r, prev_c, c, copy); prev_l = cur_l; prev_r = cur_r; prev_c = c; } } static void M_DrawElectricChain( const XYZ_32 start, const XYZ_32 end, const bool copy, const int32_t scale, int32_t *const point_idx) { XYZ_32 prev = start; int32_t prev_c = 0; for (int32_t j = 1; j <= 4; j++) { XYZ_32 cur = { .x = start.x + ((end.x - start.x) * j) / 4, .y = start.y + ((end.y - start.y) * j) / 4, .z = start.z + ((end.z - start.z) * j) / 4, }; int32_t cur_c = 0; if (j != 4) { const XYZ_16 point = Lara_Electricity_GetPoint(*point_idx); (*point_idx)++; const int32_t ex = copy ? -point.x : point.x; const int32_t ey = copy ? -point.y : point.y; const int32_t ez = copy ? -point.z : point.z; cur.x += ex >> 3; cur.y += ey >> 3; cur.z += ez >> 3; cur_c = ABS(ex); if (ABS(ey) > cur_c) { cur_c = ABS(ey); } if (ABS(ez) > cur_c) { cur_c = ABS(ez); } } int32_t i0 = M_GetElectricIntensity(prev_c, scale, copy); int32_t i1 = M_GetElectricIntensity(cur_c, scale, copy); CLAMP(i0, 0, 255); CLAMP(i1, 0, 255); const RGBA_8888 c0 = { 0, (uint8_t)i0, (uint8_t)i0, 0xC0 }; const RGBA_8888 c1 = { 0, (uint8_t)i1, (uint8_t)i1, 0xC0 }; const float width = 4.0f; OutputSource_PolyFX_StageLineSegment( prev, c0, cur, c1, width, DRAW_BLEND_ADD); prev = cur; prev_c = cur_c; } } static void M_TriggerHeadElectricity( const ITEM *const item, const bool copy, const bool do_state, const bool do_render) { M_PRIV *const p = item->priv; const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; if (dx < -0x4800 || dx > 0x4800 || dz < -0x4800 || dz > 0x4800) { return; } const int32_t time4 = Output_GetTimeInGame() * 4; const int32_t s = (Math_Sin(time4 << 8) >> 10) + 64; int32_t point_idx = 0; for (int32_t i = 0; i < 4; i++) { XYZ_32 pos1 = m_HelmetSpikes[i].pos; XYZ_32 pos2 = m_HelmetSpikes[i + 1].pos; Collide_GetJointAbsPosition(item, &pos1, m_HelmetSpikes[i].mesh_num); Collide_GetJointAbsPosition( item, &pos2, m_HelmetSpikes[i + 1].mesh_num); if (i == 2 && do_state) { p->trig_dynamics[0] = pos1; } if (do_render) { M_DrawElectricChain(pos1, pos2, copy, s, &point_idx); } } if (p->attack_count != 0 && p->death_count == 0 && p->attack_type == M_ATTACK_HEAD) { int32_t extra_idx = 16; for (int32_t i = 0; i < 5; i++) { XYZ_32 pos1 = m_HelmetSpikes[i].pos; XYZ_32 pos2 = m_EnergyHit.pos; Collide_GetJointAbsPosition( item, &pos1, m_HelmetSpikes[i].mesh_num); Collide_GetJointAbsPosition(item, &pos2, m_EnergyHit.mesh_num); int32_t scale = s; if (p->attack_count < 64) { scale = (p->attack_count * scale) >> 6; } if (do_render) { M_DrawElectricChain(pos1, pos2, copy, scale, &extra_idx); } if (do_state && i == 4 && p->attack_count >= 64 && p->attack_count <= 128) { p->trig_dynamics[2] = pos2; } } } if (p->attack_count != 0 && p->death_count == 0 && (p->attack_type == M_ATTACK_HAND_1 || p->attack_type == M_ATTACK_HAND_2)) { XYZ_32 pos = {}; Collide_GetJointAbsPosition(item, &pos, 14); GAME_VECTOR src = { .pos = { .x = pos.x, .y = pos.y, .z = pos.z }, .room_num = item->room_num, }; if (p->attack_count >= 64) { if (p->attack_count <= 128) { if (do_state) { p->trig_dynamics[2] = pos; } } if (p->attack_count <= 96) { if (do_state && p->attack_count > 90 && !p->shared->lizard_active) { M_TriggerLizard(p); } M_TriggerElectricBeam(item, &src, copy, do_state, do_render); if (do_state) { for (int32_t i = 0; i < 3; i++) { M_TriggerSummonSmoke(p->beam_target); } } } } } else if ( p->attack_count > 64 && p->death_count == 0 && p->attack_type == M_ATTACK_HEAD) { GAME_VECTOR src = { .pos = m_EnergyHit.pos, .room_num = item->room_num }; Collide_GetJointAbsPosition(item, &src.pos, 8); M_TriggerElectricBeam(item, &src, copy, do_state, do_render); } } static void M_FindClosestShieldPoint(ITEM *const item, const XYZ_32 pos) { M_PRIV *const p = item->priv; const int32_t affected[5] = { 0, -1, 1, -8, 8, }; int32_t best_dist = INT32_MAX; int32_t point = 0; for (int32_t i = 0; i < 40; i++) { const M_SHIELD_POINT *const shield = &p->shield[i >> 3][i & 7]; if (i >= 16 && i <= 23) { const int32_t dx = shield->pos.x + item->pos.x - pos.x; const int32_t dy = shield->pos.y + item->pos.y - pos.y; const int32_t dz = shield->pos.z + item->pos.z - pos.z; const int32_t dist = SQUARE(dx) + SQUARE(dy) + SQUARE(dz); if (dist < best_dist) { best_dist = dist; point = i; } } } LARA_INFO *const lara_info = Lara_GetLaraInfo(); int32_t c; switch (lara_info->gun_type) { case LGT_PISTOLS: case LGT_UZIS: c = 144; break; case LGT_MAGNUMS: case LGT_AUTOS: case LGT_DESERT_EAGLE: case LGT_HARPOON: c = 200; break; case LGT_SHOTGUN: case LGT_M16: case LGT_MP5: c = 192; break; case LGT_ROCKET: case LGT_GRENADE: c = 224; break; default: c = 180; break; } for (int32_t i = 0; i < 5; i++) { int32_t n = point + affected[i]; if ((n & 7) == 7 && affected[i] == -1) { n += 8; } if ((n & 7) == 0 && affected[i] == 1) { n -= 8; } M_SHIELD_POINT *const shield = &p->shield[n >> 3][n & 7]; int32_t r = shield->color.r; int32_t g = shield->color.g; int32_t b = shield->color.b; if (i == 0) { if (c >= 200) { r = c; } else { r += c >> 2; if (r > c) { r = c; } } } else { if (c >= 200) { r = c >> 1; } else { r += c >> 3; if (r > (c >> 1)) { r = c >> 1; } } } if (i == 0) { if (c >= 200) { g = c; } else { g += c >> 2; if (g > c) { g = c; } } } else { if (c >= 200) { g = c >> 1; } else { g += c >> 3; if (g > (c >> 1)) { g = c >> 1; } } } if (i == 0) { if (c >= 200) { b = c; } else { b += c >> 2; if (b > c) { b = c; } } } else { if (c >= 200) { b = c >> 1; } else { b += c >> 3; if (b > (c >> 1)) { b = c >> 1; } } } shield->sub.r = (Random_GetControl() & 7) + 8; shield->sub.g = (Random_GetControl() & 7) + 8; shield->sub.b = (Random_GetControl() & 7) + 8; if (lara_info->gun_type == LGT_ROCKET || lara_info->gun_type == LGT_GRENADE) { shield->sub.r >>= 1; shield->sub.g >>= 1; shield->sub.b >>= 1; } shield->color = (RGB_888) { r, g, b }; } for (int32_t i = 0; i < 7; i++) { M_TriggerElectricSparks(item, pos, true); } } static bool M_GunHit( ITEM *const item, const GAME_VECTOR *const start, const GAME_VECTOR *const hit_pos, int32_t *const damage) { M_PRIV *const p = item->priv; if (p->shield_on) { p->shield_active = true; if (hit_pos != nullptr) { M_FindClosestShieldPoint(item, hit_pos->pos); } if (damage != nullptr) { *damage = 0; } return false; } return true; } static bool M_ShouldSpawnBlood(const ITEM *const item) { M_PRIV *const p = item->priv; return !p->shield_on; } static void M_UpdateShield(ITEM *const item) { M_PRIV *const p = item->priv; for (int32_t i = 8; i <= 31; i++) { M_SHIELD_POINT *const shield = &p->shield[i >> 3][i & 7]; shield->color.r = MAX(0, (int32_t)shield->color.r - shield->sub.r); shield->color.g = MAX(0, (int32_t)shield->color.g - shield->sub.g); shield->color.b = MAX(0, (int32_t)shield->color.b - shield->sub.b); } } static void M_DrawShield(const ITEM *const item) { const M_PRIV *const p = item->priv; const int32_t time4 = Output_GetTimeInGame() * 4; const int32_t sprite_base = Object_Get(O_EXPLOSION_1)->mesh_idx; for (int32_t band = 0; band < 4; band++) { const int32_t sprite_idx = sprite_base + 18 + ((band + (time4 >> 3)) & 7); for (int32_t j = 0; j < 8; j++) { const int32_t j2 = (j == 7) ? 0 : (j + 1); const M_SHIELD_POINT *const s00 = &p->shield[band][j]; const M_SHIELD_POINT *const s01 = &p->shield[band][j2]; const M_SHIELD_POINT *const s10 = &p->shield[band + 1][j]; const M_SHIELD_POINT *const s11 = &p->shield[band + 1][j2]; const RGB_888 c00 = s00->color; const RGB_888 c01 = s01->color; const RGB_888 c10 = s10->color; const RGB_888 c11 = s11->color; if (((c00.r | c00.g | c00.b | c01.r | c01.g | c01.b | c11.r | c11.g | c11.b | c10.r | c10.g | c10.b) == 0U)) { continue; } const XYZ_32 world_pos[4] = { { item->pos.x + s00->pos.x, item->pos.y + s00->pos.y, item->pos.z + s00->pos.z, }, { item->pos.x + s01->pos.x, item->pos.y + s01->pos.y, item->pos.z + s01->pos.z, }, { item->pos.x + s11->pos.x, item->pos.y + s11->pos.y, item->pos.z + s11->pos.z, }, { item->pos.x + s10->pos.x, item->pos.y + s10->pos.y, item->pos.z + s10->pos.z, }, }; const RGBA_8888 color[4] = { { c00.r, c00.g, c00.b, 255 }, { c01.r, c01.g, c01.b, 255 }, { c11.r, c11.g, c11.b, 255 }, { c10.r, c10.g, c10.b, 255 }, }; OutputSource_PolyFX_StageSpriteQuadWorld( sprite_idx, world_pos, color, DRAW_BLEND_ADD); } } } static bool M_Draw(const ITEM *const item) { M_PRIV *const p = item->priv; Object_DrawAnimatingItem(item); if (p->explode_count == 0) { M_TriggerHeadElectricity(item, false, false, true); M_TriggerHeadElectricity(item, true, false, true); } M_DrawShield(item); return true; } static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; p->shared = &m_SharedPriv; JSON_SHOULD(JSON_READ(io, "dead", &p->dead)); JSON_SHOULD(JSON_READ(io, "attack_count", &p->attack_count)); JSON_SHOULD(JSON_READ(io, "death_count", &p->death_count)); JSON_SHOULD(JSON_READ(io, "attack_flag", &p->attack_flag)); JSON_SHOULD(JSON_READ(io, "attack_type", &p->attack_type)); JSON_SHOULD(JSON_READ(io, "attack_head_count", &p->attack_head_count)); JSON_SHOULD(JSON_READ(io, "ring_count", &p->ring_count)); JSON_SHOULD(JSON_READ(io, "explode_count", &p->explode_count)); JSON_SHOULD(JSON_READ(io, "lizard_item_num", &p->lizard_item_num)); JSON_SHOULD(JSON_READ(io, "lizard_room_num", &p->lizard_room_num)); JSON_SHOULD(JSON_READ(io, "dropped_item", &p->dropped_item)); JSON_SHOULD(JSON_READ(io, "shield_on", &p->shield_on)); JSON_SHOULD(JSON_READ(io, "shield_active", &p->shield_active)); JSON_SHOULD(JSON_READ(io, "turned", &p->turned)); JSON_SHOULD(JSON_READ(io, "beam_target_x", &p->beam_target.x)); JSON_SHOULD(JSON_READ(io, "beam_target_y", &p->beam_target.y)); JSON_SHOULD(JSON_READ(io, "beam_target_z", &p->beam_target.z)); JSON_SHOULD( JSON_READ(io, "shared_lizard_active", &p->shared->lizard_active)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "dead", p->dead); JSONW_WRITE(io, "attack_count", p->attack_count); JSONW_WRITE(io, "death_count", p->death_count); JSONW_WRITE(io, "attack_flag", p->attack_flag); JSONW_WRITE(io, "attack_type", p->attack_type); JSONW_WRITE(io, "attack_head_count", p->attack_head_count); JSONW_WRITE(io, "ring_count", p->ring_count); JSONW_WRITE(io, "explode_count", p->explode_count); JSONW_WRITE(io, "lizard_item_num", p->lizard_item_num); JSONW_WRITE(io, "lizard_room_num", p->lizard_room_num); JSONW_WRITE(io, "dropped_item", p->dropped_item); JSONW_WRITE(io, "shield_on", p->shield_on); JSONW_WRITE(io, "shield_active", p->shield_active); JSONW_WRITE(io, "turned", p->turned); JSONW_WRITE(io, "beam_target_x", p->beam_target.x); JSONW_WRITE(io, "beam_target_y", p->beam_target.y); JSONW_WRITE(io, "beam_target_z", p->beam_target.z); JSONW_WRITE(io, "shared_lizard_active", p->shared->lizard_active); } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; p->shared = &m_SharedPriv; p->lizard_item_num = M_FindLizard(item->room_num); p->lizard_room_num = Item_Get(p->lizard_item_num)->room_num; for (int32_t i = 0; i < 2; i++) { const int32_t sign = i == 0 ? -1 : 1; int32_t y_rot = item->rot.y + DEG_180; // after turning XYZ_32 pos = item->pos; pos = XYZ_32_OffsetYaw(pos, y_rot, WALL_L * 3); pos = XYZ_32_OffsetYaw(pos, y_rot + DEG_270 * sign, WALL_L * 5); int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); pos.y = Room_GetHeight(sector, pos); p->lizard_summon_coords[i].pos = pos; p->lizard_summon_coords[i].y_rot = y_rot - DEG_45 * sign; } for (int32_t i = 0; i < 3; i++) { p->trig_dynamics[i].x = 0; } p->dropped_item = false; p->dead = 0; p->ring_count = 0; p->explode_count = 0; p->attack_head_count = 0; p->death_count = 0; p->attack_flag = 0; p->attack_count = 0; p->shield_active = false; p->shield_on = false; TribeBoss_SetLizardActive(false); for (int32_t i = 0; i < 5; i++) { const int32_t y = m_Heights[i]; const int32_t r = m_Dist[i]; int32_t angle = 0; for (int32_t j = 0; j < 8; j++) { p->shield[i][j].pos.x = (int16_t)((r * Math_Sin(angle << 4)) >> 13); p->shield[i][j].pos.y = (int16_t)y; p->shield[i][j].pos.z = (int16_t)((r * Math_Cos(angle << 4)) >> 13); p->shield[i][j].color = (RGB_888) {}; angle += 512; } } } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; CREATURE *const creature = item->creature_data; int16_t angle = 0; int16_t head = 0; int16_t old_y_rot = INT16_MAX; Lara_Electricity_UpdatePoints(); p->shield_active = false; M_UpdateShield(item); if (item->hit_points > 0) { AI_INFO info; Creature_AIInfo(item, &info); if (item->hit_status) { Sound_Effect(SFX_TRIBOSS_TAKE_HIT, &item->pos, SPM_NORMAL); } old_y_rot = item->rot.y; const ITEM *const lara_item = Lara_GetItem(); if (p->attack_flag == 0) { const int32_t x_dist = item->pos.x - lara_item->pos.x; const int32_t z_dist = item->pos.z - lara_item->pos.z; if (SQUARE(x_dist) + SQUARE(z_dist) < 0x400000) { p->attack_flag = 1; } } creature->target.x = lara_item->pos.x; creature->target.z = lara_item->pos.z; if (!p->shared->lizard_active || item->current_anim_state != M_STATE_WAIT) { angle = Creature_Turn(item, creature->maximum_turn); } else { const uint16_t y_test = item->rot.y; if (ABS(0xC000 - y_test) > DEG_1) { item->rot.y += (0xC000 - y_test) >> 3; } else { item->rot.y = -0x4000; } } M_RotateHeadXAngle(item); if (info.ahead) { head = info.angle; } switch (item->current_anim_state) { case M_STATE_WAIT: p->attack_count = 0; if (item->goal_anim_state != M_STATE_ATTACK_HEAD && item->goal_anim_state != M_STATE_ATTACK_HAND) { p->shield_on = true; } if (p->shared->lizard_active) { Creature_Joint(item, 1, head); } else { Creature_Joint(item, 1, 0); } if (p->attack_flag == 0 || p->shared->lizard_active) { creature->maximum_turn = 0; } else { creature->maximum_turn = 546; } if (item->goal_anim_state != M_STATE_ATTACK_HEAD && info.angle > -128 && info.angle < 128 && lara_item->hit_points > 0 && p->attack_head_count < M_MAX_HEAD_ATTACKS && !p->shared->lizard_active && !p->shield_active) { XYZ_32 pos = {}; Collide_GetJointAbsPosition(lara_item, &pos, 0); p->beam_target = pos; item->goal_anim_state = M_STATE_ATTACK_HEAD; creature->maximum_turn = 0; p->shield_on = false; p->attack_head_count++; break; } if (item->goal_anim_state == M_STATE_ATTACK_HAND || p->attack_head_count < M_MAX_HEAD_ATTACKS || lara_item->hit_points <= 0) { break; } creature->maximum_turn = 0; if (p->attack_type == M_ATTACK_HEAD) { const int32_t x1 = lara_item->pos.x - p->lizard_summon_coords[0].pos.x; const int32_t z1 = lara_item->pos.z - p->lizard_summon_coords[0].pos.z; const int32_t x2 = lara_item->pos.x - p->lizard_summon_coords[1].pos.x; const int32_t z2 = lara_item->pos.z - p->lizard_summon_coords[1].pos.z; if (SQUARE(x1) + SQUARE(z1) > SQUARE(x2) + SQUARE(z2)) { p->attack_type = M_ATTACK_HAND_1; } else { p->attack_type = M_ATTACK_HAND_2; } } const uint16_t y_test = item->rot.y; const uint16_t y_rot = M_GetLizardSummonCoords(p)->y_rot; if (ABS(y_rot - y_test) >= DEG_1) { item->rot.y += (y_rot - y_test) >> 4; } else { item->rot.y = y_rot; if (!p->shield_active) { item->goal_anim_state = M_STATE_ATTACK_HAND; p->beam_target = M_GetLizardSummonCoords(p)->pos; creature->maximum_turn = 0; p->attack_head_count = 0; p->shield_on = false; } } break; case M_STATE_ATTACK_HEAD: creature->maximum_turn = 0; p->attack_count += 3; p->attack_type = M_ATTACK_HEAD; Creature_Joint(item, 1, 0); break; case M_STATE_ATTACK_HAND: creature->maximum_turn = 0; if (p->attack_count < 64) { p->attack_count += 2; } else { p->attack_count += 3; } Creature_Joint(item, 1, 0); break; } } else { if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, M_ANIM_DEATH, 0); item->current_anim_state = M_STATE_DEATH; p->death_count = 1; } if (Item_GetRelativeFrame(item) > 119) { item->mesh_bits = 0; Item_SwitchToAnim(item, Item_GetRelativeAnim(item), 120); p->death_count = -1; if (p->explode_count == 0) { p->ring_count = 0; Sound_Effect(SFX_EXPLOSION_2, &item->pos, SPM_NORMAL); for (int32_t i = 0; i < 6; i++) { FX_RING *const ring = FX_Ring_GetRing(FX_RING_TYPE_BLAST, i); if (ring == nullptr) { continue; } ring->on = 0; ring->life = 32; ring->radius = 512; ring->speed = (i << 5) + 128; ring->rot.x = ((Random_GetControl() & 0x1FF) - 256) & 0xFFF; ring->rot.z = ((Random_GetControl() & 0x1FF) - 256) & 0xFFF; FX_Ring_Sync(ring); } if (!p->dropped_item) { Carrier_TestItemDrops(item_num); p->dropped_item = true; } } if (p->explode_count < 256) { p->explode_count++; } if (p->explode_count <= 128 || p->ring_count != 6 || FX_Ring_GetRing(FX_RING_TYPE_BLAST, 5)->life != 0) { M_Explode(item); } else { M_Die(item_num); p->dead = 1; } return; } item->rot.z = Random_GetControl() % p->death_count - (p->death_count >> 1); if (p->death_count < 2048) { p->death_count += 32; } } if (p->attack_count != 0 && p->attack_type == M_ATTACK_HEAD && p->attack_count < 64) { m_EnergyHit.pos.z = 4 * p->attack_count + 136; } creature->joint_rotation[0] += 1274; Creature_Animate(item_num, angle, 0); p->trig_dynamics[0].x = 0; p->trig_dynamics[1].x = 0; p->trig_dynamics[2].x = 0; if (p->explode_count == 0) { M_TriggerHeadElectricity(item, false, true, false); M_TriggerHeadElectricity(item, true, true, false); } for (int32_t i = 0; i < 3; i++) { if (!p->trig_dynamics[i].x) { continue; } XYZ_32 pos; RGB_888 color; int32_t falloff; if (i == 0) { pos = p->trig_dynamics[0]; falloff = (Random_GetControl() & 3) + 8; color.r = 0; color.g = (Random_GetControl() & 0x3F) + 64; color.b = (Random_GetControl() & 0x3F) + 128; } else if (i == 1) { pos = p->trig_dynamics[1]; falloff = (Random_GetControl() & 7) + 8; color.r = 0; if (p->attack_type == M_ATTACK_HEAD) { color.g = (Random_GetControl() & 0x3F) + 64; color.b = (Random_GetControl() & 0x3F) + 128; } else { color.g = (Random_GetControl() & 0x3F) + 128; color.b = (Random_GetControl() & 0x3F) + 64; } } else { if (p->attack_count == 0) { continue; } pos = p->trig_dynamics[2]; falloff = (128 - p->attack_count) >> 1; CLAMPG(falloff, 31); if (falloff <= 0) { continue; } color.r = falloff << 1; if (p->attack_type == M_ATTACK_HEAD) { color.g = (Random_GetControl() & 0x3F) + 128; color.b = (Random_GetControl() & 0x3F) + 192; } else { color.g = (Random_GetControl() & 0x3F) + 192; color.b = (Random_GetControl() & 0x3F) + 128; } } Output_AddDynamicLightRGB(pos, falloff, color); } if (old_y_rot != item->rot.y && !p->turned) { p->turned = true; Sound_Effect(SFX_TRIBOSS_TURN_CHAIR, &item->pos, 0x800000 | SPM_PITCH); return; } if (old_y_rot == item->rot.y) { p->turned = false; } } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->draw_func = M_Draw; obj->gun_hit_func = M_GunHit; obj->should_spawn_blood_func = M_ShouldSpawnBlood; obj->can_drop_items_func = M_CanDropItems; obj->can_be_exploded_func = M_CanBeExploded; obj->shadow_size = 0; obj->hit_points = 200; obj->pivot_length = 50; obj->radius = 102; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 4)->rot.y = true; Object_GetBone(obj, 7)->rot.y = true; Object_GetBone(obj, 7)->rot.x = true; } bool TribeBoss_IsLizardActive(void) { return m_SharedPriv.lizard_active; } void TribeBoss_SetLizardActive(const bool active) { m_SharedPriv.lizard_active = active; } REGISTER_OBJECT(O_TRIBE_BOSS, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/tribe_boss.h ================================================ #pragma once bool TribeBoss_IsLizardActive(void); void TribeBoss_SetLizardActive(bool active); ================================================ FILE: src/trx/game/objects/creatures/tribe_pipeman.c ================================================ #include #include #include #include #include #include #include #include #include // clang-format off #define M_BIFF_DAMAGE 100 #define M_BIFF_ENEMY_DAMAGE 5 #define M_WALK_TURN (9 * DEG_1) // = 1638 #define M_RUN_TURN (6 * DEG_1) // = 1092 #define M_WAIT_TURN (2 * DEG_1) // = 364 #define M_PIPE_RANGE SQUARE(WALL_L * 8) // = 0x4000000 #define M_CLOSE_RANGE SQUARE(WALL_L / 2) // = 0x40000 #define M_WALK_RANGE SQUARE(WALL_L * 2) // = 0x400000 #define M_AWARE_DISTANCE (WALL_L) #define M_HIT_RANGE (STEP_L * 2) #define M_TOUCH_BITS 0x2400 // clang-format on typedef enum { M_ANIM_DEATH_STANDING = 20, M_ANIM_DEATH_KNEELING = 21, } M_ANIM; typedef enum { M_STATE_NULL, M_STATE_WAIT_1, M_STATE_WALK, M_STATE_RUN, M_STATE_ATTACK_1, M_STATE_ATTACK_2, M_STATE_ATTACK_3, M_STATE_ATTACK_4, M_STATE_AIM_3, M_STATE_DEATH, M_STATE_ATTACK_5, M_STATE_WAIT_2 } M_STATE; static BITE m_BiffHit = { .pos = { .x = 0, .y = 0, .z = -200 }, .mesh_num = 13, }; static BITE m_ShootHit = { .pos = { .x = 8, .y = 40, .z = -248 }, .mesh_num = 13, }; static void M_SpawnDart(ITEM *const item) { const int16_t dart_item_num = Item_Create(); if (dart_item_num == NO_ITEM) { return; } ITEM *const dart_item = Item_Get(dart_item_num); dart_item->object_id = O_POISON_DART; dart_item->room_num = item->room_num; XYZ_32 pos1 = m_ShootHit.pos; Collide_GetJointAbsPosition(item, &pos1, m_ShootHit.mesh_num); XYZ_32 pos2 = {}; const CREATURE *const creature = item->creature_data; if (g_Config.gameplay.fix_pipeman_aim && creature->enemy != nullptr) { if (creature->enemy == Lara_GetItem()) { Lara_GetMeshPos(LM_TORSO, &pos2); } else { pos2 = creature->enemy->pos; } } else { pos2 = m_ShootHit.pos; pos2.z <<= 1; Collide_GetJointAbsPosition(item, &pos2, m_ShootHit.mesh_num); } int16_t angles[2]; Math_GetVectorAngles( pos2.x - pos1.x, pos2.y - pos1.y, pos2.z - pos1.z, angles); dart_item->pos = pos1; Item_Initialise(dart_item_num); dart_item->rot.x = angles[1]; dart_item->rot.y = angles[0]; dart_item->speed = 256; Item_AddActive(dart_item_num); dart_item->status = IS_ACTIVE; XYZ_32 smoke_pos = { .x = m_ShootHit.pos.x, .y = m_ShootHit.pos.y, .z = m_ShootHit.pos.z + 96, }; Collide_GetJointAbsPosition(item, &smoke_pos, m_ShootHit.mesh_num); for (int32_t i = 0; i < 2; i++) { Sparks_TriggerDartSmoke(smoke_pos, (XZ_32) {}, true); } } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } const ITEM *const lara_item = Lara_GetItem(); const LARA_INFO *const lara = Lara_GetLaraInfo(); ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t angle = 0; int16_t tilt = 0; int16_t torso_x = 0; int16_t torso_y = 0; int16_t head = 0; if (item->hit_points <= 0) { if (item->current_anim_state != M_STATE_DEATH) { if (item->current_anim_state == M_STATE_WAIT_1 || item->current_anim_state == M_STATE_ATTACK_1) { Item_SwitchToAnim(item, M_ANIM_DEATH_KNEELING, 0); } else { Item_SwitchToAnim(item, M_ANIM_DEATH_STANDING, 0); } item->current_anim_state = M_STATE_DEATH; } } else { if (item->ai_bits != 0) { Creature_GetAITarget(creature); } AI_INFO info; Creature_AIInfo(item, &info); Creature_UpdateMood(item, &info, info.zone_num == info.enemy_zone_num); if (item->hit_status && lara->poison_timer >= 256 && creature->mood == MOOD_BORED) { creature->mood = MOOD_ESCAPE; } Creature_ApplyMood(item, &info, false); angle = Creature_Turn( item, creature->mood == MOOD_BORED ? M_WAIT_TURN : creature->maximum_turn); if (info.ahead) { head = info.angle >> 1; torso_y = info.angle >> 1; } if (item->hit_status || (creature->enemy == lara_item && (info.distance < M_AWARE_DISTANCE || Creature_CanSeeEnemy(item, &info)) && (ABS(lara_item->pos.y - item->pos.y) < WALL_L * 2))) { Creature_AlertAllGuards(item_num); } switch (item->current_anim_state) { case M_STATE_WAIT_1: if (info.ahead) { torso_x = info.x_angle >> 1; torso_y = info.angle; } creature->flags &= 0xFFF; creature->maximum_turn = M_WAIT_TURN; if (item->ai_bits & AI_GUARD) { head = Creature_AIGuard(creature); torso_x = 0; torso_y = 0; creature->maximum_turn = 0; if (!(Random_GetControl() & 0xFF)) { item->goal_anim_state = M_STATE_WAIT_2; } } else if (creature->mood == MOOD_ESCAPE) { if (lara->target == item || !info.ahead || item->hit_status) { item->goal_anim_state = M_STATE_RUN; } else { item->goal_anim_state = M_STATE_WAIT_1; } } else if (info.bite && info.distance < M_CLOSE_RANGE) { item->goal_anim_state = M_STATE_WAIT_2; } else if (info.bite && info.distance < M_WALK_RANGE) { item->goal_anim_state = M_STATE_WALK; } else if ( Creature_CanTargetEnemy(item, &info) && info.distance < M_PIPE_RANGE) { item->goal_anim_state = M_STATE_ATTACK_1; } else if (creature->mood == MOOD_BORED) { if (Random_GetControl() < 512) { item->goal_anim_state = M_STATE_WALK; } } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_WALK: creature->maximum_turn = M_WALK_TURN; if (info.bite && info.distance < M_CLOSE_RANGE) { item->goal_anim_state = M_STATE_WAIT_2; } else if (info.bite && info.distance < M_WALK_RANGE) { item->goal_anim_state = M_STATE_WALK; } else if ( Creature_CanTargetEnemy(item, &info) && info.distance < M_PIPE_RANGE) { item->goal_anim_state = M_STATE_WAIT_1; } else if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if (creature->mood == MOOD_BORED) { if (Random_GetControl() > 512) { item->goal_anim_state = M_STATE_WALK; } else if (Random_GetControl() > 512) { item->goal_anim_state = M_STATE_WAIT_2; } else { item->goal_anim_state = M_STATE_WAIT_1; } } else if (info.distance > M_WALK_RANGE) { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_RUN: creature->flags &= 0xFFF; creature->maximum_turn = M_RUN_TURN; tilt = angle >> 2; if (info.bite && info.distance < M_CLOSE_RANGE) { item->goal_anim_state = M_STATE_WAIT_2; } else if ( Creature_CanTargetEnemy(item, &info) && info.distance < M_PIPE_RANGE) { item->goal_anim_state = M_STATE_WAIT_1; } if (item->ai_bits & AI_GUARD) { item->goal_anim_state = M_STATE_WAIT_2; } else if ( creature->mood == MOOD_ESCAPE && lara->target != item && info.ahead) { item->goal_anim_state = M_STATE_WAIT_2; } else if (creature->mood == MOOD_BORED) { item->goal_anim_state = M_STATE_WAIT_1; } break; case M_STATE_ATTACK_1: if (info.ahead) { torso_x = info.x_angle; torso_y = info.angle; } creature->maximum_turn = 0; if (ABS(info.angle) < M_WAIT_TURN) { item->rot.y += info.angle; } else if (info.angle < 0) { item->rot.y -= M_WAIT_TURN; } else { item->rot.y += M_WAIT_TURN; } if (Item_GetRelativeFrame(item) == 15) { M_SpawnDart(item); item->goal_anim_state = M_STATE_WAIT_1; } break; case M_STATE_ATTACK_3: ITEM *const enemy = creature->enemy; if (enemy == lara_item) { if (!(creature->flags & 0xF000) && item->touch_bits & M_TOUCH_BITS) { Lara_TakeDamage(M_BIFF_DAMAGE, true); creature->flags |= 0x1000; Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL); Creature_Effect(item, &m_BiffHit, Spawn_Blood); } } else if (!(creature->flags & 0xF000) && enemy != nullptr) { if (Item_IsNearby(enemy, item, M_HIT_RANGE)) { Item_TakeDamage(enemy, M_BIFF_ENEMY_DAMAGE, true); creature->flags |= 0x1000; Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL); } } break; case M_STATE_AIM_3: if (!info.bite || info.distance > M_CLOSE_RANGE) { item->goal_anim_state = M_STATE_WAIT_2; } else { item->goal_anim_state = M_STATE_ATTACK_3; } break; case M_STATE_WAIT_2: creature->flags &= 0xFFF; creature->maximum_turn = M_WAIT_TURN; if (item->ai_bits & AI_GUARD) { head = Creature_AIGuard(creature); torso_x = 0; torso_y = 0; creature->maximum_turn = 0; if (!(Random_GetControl() & 0xFF)) { item->goal_anim_state = M_STATE_WAIT_1; } } else if (creature->mood == MOOD_ESCAPE) { if (lara->target != item && info.ahead && !item->hit_status) { item->goal_anim_state = M_STATE_WAIT_1; } else { item->goal_anim_state = M_STATE_RUN; } } else if (info.bite && info.distance < M_CLOSE_RANGE) { item->goal_anim_state = M_STATE_ATTACK_3; } else if (info.bite && info.distance < M_WALK_RANGE) { item->goal_anim_state = M_STATE_WALK; } else if ( Creature_CanTargetEnemy(item, &info) && info.distance < M_PIPE_RANGE) { item->goal_anim_state = M_STATE_WAIT_1; } else if ( creature->mood == MOOD_BORED && Random_GetControl() < 512) { item->goal_anim_state = M_STATE_WALK; } else { item->goal_anim_state = M_STATE_RUN; } break; } } Creature_Tilt(item, tilt); Creature_Joint(item, 0, torso_y); Creature_Joint(item, 1, torso_x); Creature_Joint(item, 2, head - torso_y); Creature_Joint(item, 3, 0); Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->shadow_size = UNIT_SHADOW / 2; obj->hit_points = 28; obj->radius = 102; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 6)->rot.y = true; Object_GetBone(obj, 6)->rot.x = true; Object_GetBone(obj, 13)->rot.y = true; Object_GetBone(obj, 13)->rot.x = true; } REGISTER_OBJECT(O_TRIBE_PIPEMAN, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/wasp_mutant.c ================================================ #include #include #include #include #include #include #include #include #include #include // clang-format off #define M_RADIUS (WALL_L / 5) // = 204 #define M_HIT_POINTS 24 #define M_DAMAGE 50 #define M_TOUCH_BITS 0b00010000'00000000 #define M_WAIT_TURN DEG_1 // = 182 #define M_FLY_TURN (DEG_1 * 3) // = 546 #define M_LAND_SPEED (STEP_L / 5) // = 51 #define M_ATTACK_DIST SQUARE(WALL_L / 2) // = 262144 #define M_TAKEOFF_DIST SQUARE(WALL_L * 3) // = 9437184 #define M_TAKEOFF_CHANCE 0x80 // clang-format on typedef enum { M_STATE_HOVER, M_STATE_LAND, M_STATE_WAIT, M_STATE_TAKEOFF, M_STATE_ATTACK, M_STATE_FALL, M_STATE_DEATH, M_STATE_MOVE, } M_STATE; typedef enum { M_ANIM_WAIT = 2, M_ANIM_FALL = 5, } M_ANIM; typedef struct { int16_t light; } M_PRIV; static const BITE m_Sting = { .pos = {}, .mesh_num = 12, }; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_OPTIONAL(JSON_READ(io, "light", &p->light)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "light", p->light); } static void M_TriggerParticles(const ITEM *const item) { const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; if (dx < -0x4000 || dx > 0x4000 || dz < -0x4000 || dz > 0x4000) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.g = (Random_GetControl() & 0x3F) + 32; spark->src_color.b = spark->src_color.g >> 1; spark->src_color.r = spark->src_color.g >> 2; spark->dst_color.g = (Random_GetControl() & 0x1F) + 224; spark->dst_color.b = spark->dst_color.g >> 1; spark->dst_color.r = spark->dst_color.g >> 2; spark->life = 8; spark->s_life = 8; spark->col_fade_speed = 4; spark->fade_to_black = 2; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->dynamic = -1; spark->pos.x = (Random_GetControl() & 0xF) - 8; spark->pos.y = (Random_GetControl() & 0xF) - 8; spark->pos.z = (Random_GetControl() & 0x7F) - 64; spark->vel.x = (Random_GetControl() & 0x1F) - 16; spark->vel.y = (Random_GetControl() & 0x1F) - 16; spark->vel.z = (Random_GetControl() & 0x1F) - 16; spark->flags = SPARK_F_ATTACHED_NODE | SPARK_F_ITEM | SPARK_F_SPRITE | SPARK_F_SCALE; spark->friction = 34; spark->max_y_vel = 0; spark->gravity = 0; spark->effect_num = Item_GetIndex(item); spark->node_num = 1; spark->scalar = 3; spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->size.width = (Random_GetControl() & 3) + 3; spark->src_size.width = spark->size.width; spark->dst_size.width = spark->size.width >> 1; spark->size.height = spark->size.width; spark->src_size.height = spark->size.height; spark->dst_size.height = spark->size.height >> 1; Sparks_FinishSetup(spark); } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); Item_SwitchToAnim(item, M_ANIM_WAIT, 0); item->current_anim_state = M_STATE_WAIT; item->goal_anim_state = M_STATE_WAIT; M_PRIV *const p = item->priv; p->light = Random_GetControl() & 0x7F; } static void M_ControlDeath(ITEM *const item) { switch (item->current_anim_state) { case M_STATE_FALL: if (item->pos.y > item->floor) { item->pos.y = item->floor; item->fall_speed = 0; item->gravity = false; item->goal_anim_state = M_STATE_DEATH; } item->rot.x = 0; break; case M_STATE_DEATH: item->pos.y = item->floor; item->rot.x = 0; break; default: Item_SwitchToAnim(item, M_ANIM_FALL, 0); item->current_anim_state = M_STATE_FALL; item->gravity = true; item->speed = 0; item->rot.x = 0; break; } } static void M_TriggerLight(ITEM *const item) { M_PRIV *const p = item->priv; const int32_t intensity = ABS(Math_Sin(p->light << 10) * 31) >> 14; const RGB_888 color = { .r = 0, .g = intensity << 3, .b = 0, }; XYZ_32 pos = {}; Collide_GetJointAbsPosition(item, &pos, 10); Output_AddDynamicLightRGB(pos, 10, color); p->light = (p->light + 1) & 0x3F; } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t angle = 0; if (item->hit_points <= 0) { M_ControlDeath(item); goto finish; } AI_INFO info = {}; Creature_AIInfo(item, &info); Creature_Mood(item, &info, true); angle = Creature_Turn(item, creature->maximum_turn); switch (item->current_anim_state) { case M_STATE_HOVER: creature->flags = 0; creature->maximum_turn = M_FLY_TURN; if (item->required_anim_state != 0) { item->goal_anim_state = item->required_anim_state; } else if ( item->hit_status || Random_GetControl() < M_TAKEOFF_CHANCE * 3 || item->ai_bits == AI_MODIFY) { item->goal_anim_state = M_STATE_MOVE; } else if ( (creature->mood != MOOD_BORED && Random_GetControl() >= M_TAKEOFF_CHANCE) || item->hit_status || item->ai_bits == AI_MODIFY) { if (info.ahead && info.distance < M_ATTACK_DIST) { item->goal_anim_state = M_STATE_ATTACK; } } else { item->goal_anim_state = M_STATE_LAND; } break; case M_STATE_LAND: item->pos.y += M_LAND_SPEED; CLAMPG(item->pos.y, item->floor); break; case M_STATE_WAIT: item->pos.y = item->floor; creature->maximum_turn = M_WAIT_TURN; if (item->hit_status || info.distance < M_TAKEOFF_DIST || creature->hurt_by_lara || item->ai_bits == AI_MODIFY) { item->goal_anim_state = M_STATE_TAKEOFF; } break; case M_STATE_ATTACK: creature->maximum_turn = M_FLY_TURN; if (info.ahead && info.distance < M_ATTACK_DIST) { item->goal_anim_state = M_STATE_ATTACK; } else if (info.distance < M_ATTACK_DIST) { item->goal_anim_state = M_STATE_HOVER; } else { item->goal_anim_state = M_STATE_HOVER; item->required_anim_state = M_STATE_MOVE; } if (creature->flags == 0 && (item->touch_bits & M_TOUCH_BITS) != 0) { Lara_TakeDamage(M_DAMAGE, true); Creature_Effect(item, &m_Sting, Spawn_Blood); creature->flags = 1; } break; case M_STATE_MOVE: creature->flags = 0; creature->maximum_turn = M_FLY_TURN; if (item->required_anim_state != 0) { item->goal_anim_state = item->required_anim_state; } else if ( (creature->mood != MOOD_BORED && Random_GetControl() >= M_TAKEOFF_CHANCE) || creature->hurt_by_lara || item->ai_bits == AI_MODIFY) { if (info.ahead && info.distance < M_ATTACK_DIST) { item->goal_anim_state = M_STATE_ATTACK; } } else { item->goal_anim_state = M_STATE_HOVER; } break; } finish: M_TriggerLight(item); for (int32_t i = 0; i < 2; i++) { M_TriggerParticles(item); } Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->collision_func = Creature_Collision; obj->control_func = M_Control; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->shadow_size = UNIT_SHADOW / 2; obj->radius = M_RADIUS; obj->hit_points = M_HIT_POINTS; obj->lot_setup = LOT_Setup(LOT_SETUP_FLYER); obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_WASP_MUTANT, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/willard.c ================================================ #include "willard_internal.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // clang-format off #define M_TURN (5 * DEG_1) #define M_ATTACK_TURN (2 * DEG_1) #define M_TOUCH_BITS 0x900000 #define M_BITE_DAMAGE 220 #define M_TOUCH_DAMAGE 10 #define M_ATTACK_RANGE SQUARE(WALL_L * 3 / 2) #define M_LUNGE_RANGE SQUARE(WALL_L * 2) #define M_FIRE_RANGE SQUARE(WALL_L * 4) #define M_HP_AFTER_KO 200 #define M_KO_TIME 280 #define M_WALK_ATTACK_FRAME 30 #define M_TURN_180_FRAME 51 #define M_SHOOT_FRAME 40 #define M_CHARGE_FRAME_MAX 16 #define M_PLASMA_X 64 #define M_PLASMA_Y 410 // clang-format on typedef enum { M_STATE_STOP, M_STATE_WALK, M_STATE_LUNGE, M_STATE_BIG_KILL, M_STATE_STUNNED, M_STATE_KNOCKOUT, M_STATE_GET_UP, M_STATE_WALK_ATTACK_1, M_STATE_WALK_ATTACK_2, M_STATE_TURN_180, M_STATE_SHOOT, } M_STATE; typedef enum { M_ANIM_BIG_KILL = 6, M_ANIM_STUNNED = 7, } M_ANIM; typedef struct { XYZ_16 pos; RGB_888 sub; RGB_888 color; } M_SHIELD_POINT; typedef struct { XYZ_32 pos; XYZ_16 rot; } M_AI_POINT; typedef struct { bool puzzle_ready; uint8_t ring_count; int16_t explode_count; bool dead; int16_t death_count; int32_t direction; int32_t desired_direction; M_SHIELD_POINT shield[5][8]; int32_t closest_ai_path; int32_t lara_ai_path; int32_t lara_junction; int32_t junction_index[4]; M_AI_POINT ai_path[16]; M_AI_POINT ai_junction[4]; } M_PRIV; static const BITE m_BiteLeft = { .pos = { .x = 19, .y = -13, .z = 3 }, .mesh_num = 20, }; static const BITE m_BiteRight = { .pos = { .x = 19, .y = -13, .z = 3 }, .mesh_num = 23, }; static const int32_t m_DDist[5] = { 1600, 5600, 6400, 5600, 1600 }; static const int32_t m_DHeights1[5] = { -7680, -4224, -768, 2688, 6144 }; static const int32_t m_DHeights2[5] = { -1536, -1152, -768, -384, 0 }; static int32_t m_DeathDist[5] = {}; static int32_t m_DeathHeights[5] = {}; static void M_ResetPriv(M_PRIV *const p) { *p = (M_PRIV) {}; p->closest_ai_path = -1; p->lara_ai_path = -1; p->lara_junction = -1; p->direction = 1; p->desired_direction = 1; } static void M_LoadShieldPoint( JSON_READ_IO *const io, M_SHIELD_POINT *const point) { JSON_SHOULD(JSON_READ(io, "pos", &point->pos)); JSON_SHOULD(JSON_READ(io, "sub", &point->sub)); JSON_SHOULD(JSON_READ(io, "color", &point->color)); } static void M_SaveShieldPoint( JSON_WRITE_IO *const io, const M_SHIELD_POINT *const point) { JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "pos", point->pos); JSONW_WRITE(io, "sub", point->sub); JSONW_WRITE(io, "color", point->color); JSONW_POP_AND_APPEND(io); } static void M_LoadAIPoint(JSON_READ_IO *const io, M_AI_POINT *const point) { JSON_SHOULD(JSON_READ(io, "pos", &point->pos)); JSON_SHOULD(JSON_READ(io, "rot", &point->rot)); } static void M_SaveAIPoint( JSON_WRITE_IO *const io, const M_AI_POINT *const point) { JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "pos", point->pos); JSONW_WRITE(io, "rot", point->rot); JSONW_POP_AND_APPEND(io); } static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; M_ResetPriv(p); JSON_SHOULD(JSON_READ(io, "puzzle_ready", &p->puzzle_ready)); JSON_SHOULD(JSON_READ(io, "ring_count", &p->ring_count)); JSON_SHOULD(JSON_READ(io, "explode_count", &p->explode_count)); JSON_SHOULD(JSON_READ(io, "dead", &p->dead)); JSON_SHOULD(JSON_READ(io, "death_count", &p->death_count)); JSON_SHOULD(JSON_READ(io, "direction", &p->direction)); JSON_SHOULD(JSON_READ(io, "desired_direction", &p->desired_direction)); JSON_SHOULD(JSON_READ(io, "closest_ai_path", &p->closest_ai_path)); JSON_SHOULD(JSON_READ(io, "lara_ai_path", &p->lara_ai_path)); JSON_SHOULD(JSON_READ(io, "lara_junction", &p->lara_junction)); if (JSON_SHOULD(JSON_PUSH(io, "shield"))) { for (int32_t i = 0; i < 5; i++) { if (!JSON_SHOULD(JSON_PUSH_INDEX(io, i))) { continue; } for (int32_t j = 0; j < 8; j++) { if (!JSON_SHOULD(JSON_PUSH_INDEX(io, j))) { continue; } M_LoadShieldPoint(io, &p->shield[i][j]); JSON_POP(io); } JSON_POP(io); } JSON_POP(io); } if (JSON_SHOULD(JSON_PUSH(io, "junction_index"))) { for (int32_t i = 0; i < 4; i++) { JSON_SHOULD(JSON_READ_A(io, i, &p->junction_index[i])); } JSON_POP(io); } if (JSON_SHOULD(JSON_PUSH(io, "ai_path"))) { for (int32_t i = 0; i < 16; i++) { if (!JSON_SHOULD(JSON_PUSH_INDEX(io, i))) { continue; } M_LoadAIPoint(io, &p->ai_path[i]); JSON_POP(io); } JSON_POP(io); } if (JSON_SHOULD(JSON_PUSH(io, "ai_junction"))) { for (int32_t i = 0; i < 4; i++) { if (!JSON_SHOULD(JSON_PUSH_INDEX(io, i))) { continue; } M_LoadAIPoint(io, &p->ai_junction[i]); JSON_POP(io); } JSON_POP(io); } } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "puzzle_ready", p->puzzle_ready); JSONW_WRITE(io, "ring_count", p->ring_count); JSONW_WRITE(io, "explode_count", p->explode_count); JSONW_WRITE(io, "dead", p->dead); JSONW_WRITE(io, "death_count", p->death_count); JSONW_WRITE(io, "direction", p->direction); JSONW_WRITE(io, "desired_direction", p->desired_direction); JSONW_WRITE(io, "closest_ai_path", p->closest_ai_path); JSONW_WRITE(io, "lara_ai_path", p->lara_ai_path); JSONW_WRITE(io, "lara_junction", p->lara_junction); JSONW_PUSH_ARRAY(io); for (int32_t i = 0; i < 5; i++) { JSONW_PUSH_ARRAY(io); for (int32_t j = 0; j < 8; j++) { M_SaveShieldPoint(io, &p->shield[i][j]); } JSONW_POP_AND_APPEND(io); } JSONW_POP_AND_SET(io, "shield"); JSONW_PUSH_ARRAY(io); for (int32_t i = 0; i < 4; i++) { JSONW_PUSH_VALUE(io, p->junction_index[i]); JSONW_POP_AND_APPEND(io); } JSONW_POP_AND_SET(io, "junction_index"); JSONW_PUSH_ARRAY(io); for (int32_t i = 0; i < 16; i++) { M_SaveAIPoint(io, &p->ai_path[i]); } JSONW_POP_AND_SET(io, "ai_path"); JSONW_PUSH_ARRAY(io); for (int32_t i = 0; i < 4; i++) { M_SaveAIPoint(io, &p->ai_junction[i]); } JSONW_POP_AND_SET(io, "ai_junction"); } static void M_TriggerPlasma( const int16_t item_num, const int32_t node, int32_t size) { const ITEM *const item = Item_Get(item_num); const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; const int32_t max_dist = 16 * WALL_L; if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 48; spark->src_color.g = 255; spark->src_color.b = (Random_GetControl() & 0x1F) + 48; spark->dst_color.r = 32; spark->dst_color.g = (Random_GetControl() & 0x3F) + 192; spark->dst_color.b = (Random_GetControl() & 0x3F) + 128; spark->fade_to_black = 8; spark->col_fade_speed = (Random_GetControl() & 3) + 12; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->life = (Random_GetControl() & 7) + 24; spark->s_life = spark->life; spark->dynamic = -1; spark->friction = 3; spark->pos.x = (Random_GetControl() & 0xF) - 8; spark->pos.y = 0; spark->pos.z = (Random_GetControl() & 0xF) - 8; spark->vel.x = (Random_GetControl() & 0x1F) - 16; spark->vel.y = (Random_GetControl() & 7) + 8; spark->vel.z = (Random_GetControl() & 0x1F) - 16; if (Random_GetControl() & 1) { spark->flags = SPARK_F_ATTACHED_NODE | SPARK_F_ALT_SPRITE | SPARK_F_ITEM | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = Random_GetControl() & 0xFFF; if (Random_GetControl() & 1) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = (Random_GetControl() & 0xF) + 16; } } else { spark->flags = SPARK_F_ATTACHED_NODE | SPARK_F_ALT_SPRITE | SPARK_F_ITEM | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->gravity = (Random_GetControl() & 7) + 8; spark->node_num = (uint8_t)node; spark->max_y_vel = (Random_GetControl() & 7) + 16; spark->item_num = item_num; spark->sprite_idx = (uint8_t)Object_Get(O_EXPLOSION_1)->mesh_idx; spark->scalar = 1; size += Random_GetControl() & 0xF; spark->size.width = (uint8_t)size; spark->src_size.width = spark->size.width; spark->dst_size.width = spark->size.width >> 2; spark->size.height = spark->size.width; spark->src_size.height = spark->size.height; spark->dst_size.height = spark->size.height >> 2; Sparks_FinishSetup(spark); } static void M_Explode(ITEM *const item) { M_PRIV *const p = item->priv; if (p->explode_count == 1 || p->explode_count == 15 || p->explode_count == 25 || p->explode_count == 35 || p->explode_count == 45 || p->explode_count == 55) { XYZ_32 pos = { .x = item->pos.x + (Random_GetDraw() & 0x3FF) - 512, .y = item->pos.y - (Random_GetDraw() & 0x3FF) - 256, .z = item->pos.z + (Random_GetDraw() & 0x3FF) - 512, }; FX_RING *const ring = FX_Ring_GetRing(FX_RING_TYPE_BLAST, p->ring_count); if (ring != nullptr) { ring->pos = pos; ring->on = 1; FX_Ring_Sync(ring); p->ring_count++; } for (int32_t i = 0; i < 24; i += 3) { pos = (XYZ_32) {}; Collide_GetJointAbsPosition(item, &pos, i); Willard_TriggerPlasmaBall( pos, item->room_num, (int16_t)(Random_GetControl() << 1), 4); } Sparks_TriggerExplosionSparks(pos, 3, -2, 2, 0); Sound_Effect(SFX_BLAST_CIRCLE, &item->pos, 0x800000 | SPM_PITCH); } for (int32_t i = 0; i < 5; i++) { if (p->explode_count < 128) { m_DeathDist[i] = (m_DDist[i] >> 4) + ((p->explode_count * m_DDist[i]) >> 7); m_DeathHeights[i] = m_DHeights2[i] + ((p->explode_count * (m_DHeights1[i] - m_DHeights2[i])) >> 7); } } for (int32_t i = 0; i < 5; i++) { const int32_t y = m_DeathHeights[i]; const int32_t dist = m_DeathDist[i]; const int32_t time4 = Output_GetTimeInGame() * 4; int32_t angle = (time4 & 0x3F) << 3; for (int32_t j = 0; j < 8; j++) { M_SHIELD_POINT *const shield = &p->shield[i][j]; shield->pos.x = (dist * Math_Sin(angle << 4)) >> 13; shield->pos.y = y; shield->pos.z = (dist * Math_Cos(angle << 4)) >> 13; shield->sub = (RGB_888) { 0, 0, 0 }; if (i != 0 && i != 4 && p->explode_count < 64) { int32_t r = Random_GetDraw() & 0x3F; int32_t g = (Random_GetDraw() & 0x1F) + 224; int32_t b = (g >> 1) + (Random_GetDraw() & 0x3F); const int32_t m = 64 - p->explode_count; r = (m * r) >> 6; g = (m * g) >> 6; b = (m * b) >> 6; shield->color = (RGB_888) { r, g, b }; } else { shield->color = COLOR_RGB_888_BLACK; } angle = (angle + 512) & 0xFFF; } } } static void M_Die(const int16_t item_num) { ITEM *const item = Item_Get(item_num); Stats_AddKill(); item->hit_points = 0; item->collidable = false; Item_Kill(item_num); LOT_DisableBaddieAI(item_num); item->flags |= IF_INVISIBLE; } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; M_ResetPriv(p); item->include_in_kill_stats = false; } static void M_Control(const int16_t item_num) { const ITEM *const lara_item = Lara_GetItem(); if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; CREATURE *const creature = item->creature_data; const bool lara_was_alive = lara_item->hit_points > 0; XYZ_32 pos; if (p->closest_ai_path == -1) { int32_t n_junction = 0; int32_t n_path = 0; for (int32_t i = Room_Get(item->room_num)->item_num; i != NO_ITEM; i = Item_Get(i)->next_item) { const ITEM *const ai = Item_Get(i); if (ai->object_id == O_AI_X1 && n_path < 16) { p->ai_path[n_path].pos = ai->pos; p->ai_path[n_path].rot = ai->rot; n_path++; } else if (ai->object_id == O_AI_X2 && n_junction < 4) { p->ai_junction[n_junction].pos = ai->pos; p->ai_junction[n_junction].rot = ai->rot; n_junction++; } } p->closest_ai_path = -1; int32_t best_dist = INT32_MAX; for (int32_t i = 0; i < 16; i++) { const int32_t x = (p->ai_path[i].pos.x - item->pos.x) >> 6; const int32_t z = (p->ai_path[i].pos.z - item->pos.z) >> 6; const int32_t dist = SQUARE(x) + SQUARE(z); if (dist < best_dist) { p->closest_ai_path = i; best_dist = dist; } } p->lara_ai_path = -1; best_dist = INT32_MAX; for (int32_t i = 0; i < 16; i++) { const int32_t x = (p->ai_path[i].pos.x - lara_item->pos.x) >> 6; const int32_t z = (p->ai_path[i].pos.z - lara_item->pos.z) >> 6; const int32_t dist = SQUARE(x) + SQUARE(z); if (dist < best_dist) { p->lara_ai_path = i; best_dist = dist; } } for (int32_t j = 0; j < 4; j++) { int32_t index = -1; best_dist = INT32_MAX; for (int32_t i = 0; i < 16; i++) { const int32_t x = ABS((p->ai_path[i].pos.x - p->ai_junction[j].pos.x) >> 6); const int32_t z = ABS((p->ai_path[i].pos.z - p->ai_junction[j].pos.z) >> 6); const int32_t dist = x + (z >> 1); if (dist < best_dist) { index = i; best_dist = dist; } } p->junction_index[j] = index; } } int32_t best_dist = INT32_MAX; int32_t j = p->closest_ai_path; for (int32_t i = j - 1; i < j + 2; i++) { int32_t n_path; if (i < 0) { n_path = i + 16; } else if (i > 15) { n_path = i - 16; } else { n_path = i; } const int32_t x = (p->ai_path[n_path].pos.x - item->pos.x) >> 6; const int32_t z = (p->ai_path[n_path].pos.z - item->pos.z) >> 6; const int32_t dist = SQUARE(x) + SQUARE(z); if (dist < best_dist) { p->closest_ai_path = n_path; best_dist = dist; } } j = p->lara_ai_path; best_dist = INT32_MAX; for (int32_t i = j - 1; i < j + 2; i++) { int32_t n_path; if (i < 0) { n_path = i + 16; } else if (i > 15) { n_path = i - 16; } else { n_path = i; } const int32_t x = (p->ai_path[n_path].pos.x - lara_item->pos.x) >> 6; const int32_t z = (p->ai_path[n_path].pos.z - lara_item->pos.z) >> 6; const int32_t dist = SQUARE(x) + SQUARE(z); if (dist < best_dist) { p->lara_ai_path = n_path; best_dist = dist; } } int32_t best_dist2 = INT32_MAX; for (int32_t i = 0; i < 4; i++) { const int32_t x = (p->ai_junction[i].pos.x - lara_item->pos.x) >> 6; const int32_t z = (p->ai_junction[i].pos.z - lara_item->pos.z) >> 6; const int32_t dist = SQUARE(x) + SQUARE(z); if (dist < best_dist2) { p->lara_junction = i; best_dist2 = dist; } } const bool fire = best_dist2 < best_dist || item->pos.y > lara_item->pos.y + 2048; const int32_t x = p->ai_junction[p->lara_junction].pos.x - item->pos.x; const int32_t z = p->ai_junction[p->lara_junction].pos.z - item->pos.z; const int32_t dist = SQUARE(x) + SQUARE(z); if (item->hit_points <= 0) { const bool puzzle_complete = Inv_RequestItem(O_QUEST_ITEM_1) > 0 && Inv_RequestItem(O_QUEST_ITEM_2) > 0 && Inv_RequestItem(O_QUEST_ITEM_3) > 0 && Inv_RequestItem(O_QUEST_ITEM_4) > 0; if (puzzle_complete && p->puzzle_ready) { if (item->current_anim_state != M_STATE_STUNNED) { Item_SwitchToAnim(item, M_ANIM_STUNNED, 0); item->current_anim_state = M_STATE_STUNNED; } else if ( Item_GetRelativeFrame(item) >= Item_GetAnim(item)->frame_end - Item_GetAnim(item)->frame_base - 2) { item->mesh_bits = 0; Item_SwitchToAnim(item, Item_GetRelativeAnim(item), -2); if (!p->explode_count) { p->ring_count = 0; for (int32_t i = 0; i < 6; i++) { FX_RING *const ring = FX_Ring_GetRing(FX_RING_TYPE_BLAST, i); if (ring == nullptr) { continue; } ring->on = 0; ring->life = 32; ring->radius = 512; ring->speed = (i + 4) << 5; ring->rot.x = ((Random_GetControl() & 0x1FF) - 256) & 0xFFF; ring->rot.z = ((Random_GetControl() & 0x1FF) - 256) & 0xFFF; FX_Ring_Sync(ring); } } if (p->explode_count < 256) { p->explode_count++; } if (p->explode_count <= 128 || p->ring_count != 6 || FX_Ring_GetRing(FX_RING_TYPE_BLAST, 5)->life != 0) { M_Explode(item); } else { M_Die(item_num); p->dead = true; } return; } } else { creature->maximum_turn = 0; switch (item->current_anim_state) { case M_STATE_STOP: item->goal_anim_state = M_STATE_STUNNED; break; case M_STATE_STUNNED: p->death_count = M_KO_TIME; break; case M_STATE_KNOCKOUT: p->death_count--; if (p->death_count < 0) { item->goal_anim_state = M_STATE_GET_UP; } break; case M_STATE_GET_UP: item->hit_points = M_HP_AFTER_KO; if (puzzle_complete) { p->puzzle_ready = true; } creature->maximum_turn = M_ATTACK_TURN; break; default: item->goal_anim_state = M_STATE_STOP; break; } } } else { AI_INFO info = {}; Creature_AIInfo(item, &info); if (item->touch_bits) { Lara_TakeDamage(M_TOUCH_DAMAGE, false); } const int32_t index = p->lara_ai_path - p->closest_ai_path; if (p->direction == -1 && ((index < 0 && index > -6) || index > 10)) { p->desired_direction = 1; } else if ( p->direction == 1 && ((index > 0 && index < 6) || index < -10)) { p->desired_direction = -1; } creature->target = XYZ_32_OffsetYaw( p->ai_path[p->closest_ai_path].pos, p->ai_path[p->closest_ai_path].rot.y, WALL_L * p->direction); switch (item->current_anim_state) { case M_STATE_STOP: creature->maximum_turn = 0; creature->flags = 0; if (p->direction != p->desired_direction) { item->goal_anim_state = M_STATE_TURN_180; } else if ( fire && info.ahead && dist < M_FIRE_RANGE && lara_item->hit_points > 0) { item->goal_anim_state = M_STATE_SHOOT; } else if (!info.bite || info.distance >= M_LUNGE_RANGE) { item->goal_anim_state = M_STATE_WALK; } else { item->goal_anim_state = M_STATE_LUNGE; } break; case M_STATE_WALK: creature->maximum_turn = M_TURN; creature->flags = 0; if (p->direction != p->desired_direction) { item->goal_anim_state = M_STATE_STOP; } else if (fire && info.ahead && dist < M_FIRE_RANGE) { item->goal_anim_state = M_STATE_STOP; } else if (info.bite && info.distance < M_ATTACK_RANGE) { if ((Random_GetControl() & 3) == 1) { item->goal_anim_state = M_STATE_STOP; } else if ( item->frame_num >= Item_GetAnim(item)->frame_base + M_WALK_ATTACK_FRAME) { item->goal_anim_state = M_STATE_WALK_ATTACK_1; } else { item->goal_anim_state = M_STATE_WALK_ATTACK_2; } } break; case M_STATE_LUNGE: creature->target.x = lara_item->pos.x; creature->target.z = lara_item->pos.z; creature->maximum_turn = M_ATTACK_TURN; if (!creature->flags && item->touch_bits & M_TOUCH_BITS) { Lara_TakeDamage(2 * M_BITE_DAMAGE, true); Creature_Effect(item, &m_BiteLeft, Spawn_Blood); Creature_Effect(item, &m_BiteRight, Spawn_Blood); creature->flags = 1; } break; case M_STATE_BIG_KILL: switch (Item_GetRelativeFrame(item)) { case 0: case 43: case 95: case 105: Creature_Effect(item, &m_BiteLeft, Spawn_Blood); break; case 61: case 91: case 101: Creature_Effect(item, &m_BiteRight, Spawn_Blood); break; } break; case M_STATE_WALK_ATTACK_1: case M_STATE_WALK_ATTACK_2: if (!creature->flags && (item->touch_bits & M_TOUCH_BITS) != 0) { Lara_TakeDamage(M_BITE_DAMAGE, true); Creature_Effect(item, &m_BiteLeft, Spawn_Blood); Creature_Effect(item, &m_BiteRight, Spawn_Blood); creature->flags = 1; } if (fire && info.bite && dist < M_FIRE_RANGE) { item->goal_anim_state = M_STATE_WALK; } else if (info.bite && info.distance < M_ATTACK_RANGE) { if (item->current_anim_state == M_STATE_WALK_ATTACK_1) { item->goal_anim_state = M_STATE_WALK_ATTACK_2; } else { item->goal_anim_state = M_STATE_WALK_ATTACK_1; } } else { item->goal_anim_state = M_STATE_WALK; } break; case M_STATE_TURN_180: creature->maximum_turn = 0; creature->flags = 0; if (Item_GetRelativeFrame(item) == M_TURN_180_FRAME) { item->rot.y += DEG_180; p->direction = -p->direction; } break; case M_STATE_SHOOT: creature->target.x = lara_item->pos.x; creature->target.z = lara_item->pos.z; creature->maximum_turn = M_ATTACK_TURN; if (Item_GetRelativeFrame(item) == M_SHOOT_FRAME && lara_item->hit_points > 0) { pos.x = -M_PLASMA_X; pos.y = M_PLASMA_Y; pos.z = 0; Collide_GetJointAbsPosition(item, &pos, 20); Willard_TriggerPlasmaBall( pos, item->room_num, item->rot.y - 4096, 0); pos.x = M_PLASMA_X; pos.y = M_PLASMA_Y; pos.z = 0; Collide_GetJointAbsPosition(item, &pos, 23); Willard_TriggerPlasmaBall( pos, item->room_num, item->rot.y + 4096, 0); } int32_t f = Item_GetRelativeFrame(item); if (f > M_CHARGE_FRAME_MAX) { f = Item_GetAnim(item)->frame_end - item->frame_num; CLAMPG(f, M_CHARGE_FRAME_MAX); } pos.x = 0; pos.y = 0; pos.z = 0; Collide_GetJointAbsPosition(item, &pos, 17); const int32_t color_base = Random_GetControl(); const int32_t r = (f * (color_base & 0x3F)) >> 4; const int32_t g = (f * (255 - ((color_base >> 4) & 0x1F))) >> 4; const int32_t b = (f * (192 - ((color_base >> 6) & 0x1F))) >> 4; Output_AddDynamicLightRGB(pos, 12, (RGB_888) { r, g, b }); M_TriggerPlasma(item_num, 7, f << 2); M_TriggerPlasma(item_num, 8, f << 2); break; } if (lara_was_alive && lara_item->hit_points <= 0) { Creature_SpecialKill( item, M_ANIM_BIG_KILL, M_STATE_BIG_KILL, LS_EXTRA_WILLARD_KILL); creature->maximum_turn = 0; return; } } const int16_t angle = Creature_Turn(item, creature->maximum_turn); Creature_Animate(item_num, angle, 0); } static bool M_CanBeExploded(const ITEM *const item) { return false; } static void M_DrawShield(const ITEM *const item) { const M_PRIV *const p = item->priv; const int32_t time4 = Output_GetTimeInGame() * 4; const int32_t sprite_base = Object_Get(O_EXPLOSION_1)->mesh_idx; for (int32_t band = 0; band < 4; band++) { const int32_t sprite_idx = sprite_base + 18 + ((band + (time4 >> 3)) & 7); for (int32_t j = 0; j < 8; j++) { const int32_t j2 = (j == 7) ? 0 : (j + 1); const M_SHIELD_POINT *const s00 = &p->shield[band][j]; const M_SHIELD_POINT *const s01 = &p->shield[band][j2]; const M_SHIELD_POINT *const s10 = &p->shield[band + 1][j]; const M_SHIELD_POINT *const s11 = &p->shield[band + 1][j2]; const int32_t idx00 = band * 8 + j; const int32_t idx01 = band * 8 + j2; const int32_t idx10 = (band + 1) * 8 + j; const int32_t idx11 = (band + 1) * 8 + j2; RGB_888 c00 = s00->color; RGB_888 c01 = s01->color; RGB_888 c10 = s10->color; RGB_888 c11 = s11->color; if (idx00 >= 8 && idx00 <= 31) { c00.r = (uint8_t)MAX(0, (int32_t)c00.r - (int32_t)s00->sub.r); c00.g = (uint8_t)MAX(0, (int32_t)c00.g - (int32_t)s00->sub.g); c00.b = (uint8_t)MAX(0, (int32_t)c00.b - (int32_t)s00->sub.b); } if (idx01 >= 8 && idx01 <= 31) { c01.r = (uint8_t)MAX(0, (int32_t)c01.r - (int32_t)s01->sub.r); c01.g = (uint8_t)MAX(0, (int32_t)c01.g - (int32_t)s01->sub.g); c01.b = (uint8_t)MAX(0, (int32_t)c01.b - (int32_t)s01->sub.b); } if (idx10 >= 8 && idx10 <= 31) { c10.r = (uint8_t)MAX(0, (int32_t)c10.r - (int32_t)s10->sub.r); c10.g = (uint8_t)MAX(0, (int32_t)c10.g - (int32_t)s10->sub.g); c10.b = (uint8_t)MAX(0, (int32_t)c10.b - (int32_t)s10->sub.b); } if (idx11 >= 8 && idx11 <= 31) { c11.r = (uint8_t)MAX(0, (int32_t)c11.r - (int32_t)s11->sub.r); c11.g = (uint8_t)MAX(0, (int32_t)c11.g - (int32_t)s11->sub.g); c11.b = (uint8_t)MAX(0, (int32_t)c11.b - (int32_t)s11->sub.b); } if (((c00.r | c00.g | c00.b | c01.r | c01.g | c01.b | c11.r | c11.g | c11.b | c10.r | c10.g | c10.b) == 0U)) { continue; } const XYZ_32 world_pos[4] = { { item->pos.x + s00->pos.x, item->pos.y + s00->pos.y, item->pos.z + s00->pos.z, }, { item->pos.x + s01->pos.x, item->pos.y + s01->pos.y, item->pos.z + s01->pos.z, }, { item->pos.x + s11->pos.x, item->pos.y + s11->pos.y, item->pos.z + s11->pos.z, }, { item->pos.x + s10->pos.x, item->pos.y + s10->pos.y, item->pos.z + s10->pos.z, }, }; const RGBA_8888 color[4] = { { c00.r, c00.g, c00.b, 255 }, { c01.r, c01.g, c01.b, 255 }, { c11.r, c11.g, c11.b, 255 }, { c10.r, c10.g, c10.b, 255 }, }; OutputSource_PolyFX_StageSpriteQuadWorld( sprite_idx, world_pos, color, DRAW_BLEND_ADD); } } } static bool M_Draw(const ITEM *const item) { const M_PRIV *const p = item->priv; const bool result = Object_DrawAnimatingItem(item); if (p->explode_count != 0) { FX_Ring_Draw(); if (p->explode_count <= 64) { M_DrawShield(item); } } return result; } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->draw_func = M_Draw; obj->can_be_exploded_func = M_CanBeExploded; obj->shadow_size = 128; obj->hit_points = 200; obj->pivot_length = 50; obj->radius = 102; obj->intelligent = true; obj->lot_setup = LOT_Setup(LOT_SETUP_CLIMBER); obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_WILLARD, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/willard_internal.h ================================================ #pragma once #include void Willard_TriggerPlasmaBall( XYZ_32 pos, int16_t room_num, int16_t angle, int16_t type); ================================================ FILE: src/trx/game/objects/creatures/willard_plasma_ball.c ================================================ #include "willard_internal.h" #include #include #include #include #include #include #include #include #include static const uint8_t m_Falloffs[5] = { 13, 7, 7, 7, 7 }; static void M_TriggerPlasmaBallFlame( const int16_t effect_num, const int32_t type, const XYZ_32 vel) { const EFFECT *const effect = Effect_Get(effect_num); const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - effect->pos.x; const int32_t dz = lara_item->pos.z - effect->pos.z; const int32_t max_dist = 16 * WALL_L; if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 48; spark->src_color.g = 255; spark->src_color.b = (Random_GetControl() & 0x1F) + 48; spark->dst_color.r = 32; spark->dst_color.g = (Random_GetControl() & 0x3F) + 192; spark->dst_color.b = (Random_GetControl() & 0x3F) + 128; spark->fade_to_black = 8; spark->col_fade_speed = (Random_GetControl() & 3) + 12; spark->draw_type = DRAW_BLEND_ADD; spark->life = (Random_GetControl() & 7) + 24; spark->s_life = spark->life; spark->extras = 0; spark->dynamic = -1; spark->friction = 85; spark->pos.x = (Random_GetControl() & 0xF) - 8; spark->pos.y = 0; spark->pos.z = (Random_GetControl() & 0xF) - 8; spark->vel.x = 2 * (vel.x + (Random_GetControl() & 0xFF)) - 256; spark->vel.y = (Random_GetControl() & 0x1FF) - 256; spark->vel.z = 2 * (vel.z + (Random_GetControl() & 0xFF)) - 256; if (Random_GetControl() & 1) { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = Random_GetControl() & 0xFFF; if (Random_GetControl() & 1) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = (Random_GetControl() & 0xF) + 16; } } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->effect_num = effect_num; spark->sprite_idx = (uint8_t)Object_Get(O_EXPLOSION_1)->mesh_idx; spark->max_y_vel = 0; spark->gravity = 0; if (type < 0) { if (type >= -2) { spark->scalar = 2; } else { spark->scalar = 4; } spark->size.width = (Random_GetControl() & 0xF) + 16; spark->friction = 5; spark->vel.x = vel.x + (Random_GetControl() & 0xFF) - 128; spark->vel.y = vel.y; spark->vel.z = vel.z + (Random_GetControl() & 0xFF) - 128; } else { spark->scalar = 3; spark->size.width = (uint8_t)type; } spark->src_size.width = spark->size.width; spark->dst_size.width = spark->size.width >> 3; spark->size.height = spark->size.width; spark->src_size.height = spark->size.height; spark->dst_size.height = spark->size.height >> 3; Sparks_FinishSetup(spark); } static void M_Control(const int16_t effect_num) { const ITEM *const lara_item = Lara_GetItem(); EFFECT *const effect = Effect_Get(effect_num); const XYZ_32 old_pos = effect->pos; const int32_t time4 = Output_GetTimeInGame() * 4; if (effect->flag1 != 0) { effect->fall_speed += (effect->flag1 != 1) + 1; if ((time4 & 0xC) == 0) { if (effect->speed != 0) { effect->speed--; } M_TriggerPlasmaBallFlame( effect_num, -1 - effect->flag1, (XYZ_32) { .x = 0, .y = -(Random_GetControl() & 0x1F), .z = 0, }); } } else { int16_t angles[2]; Math_GetVectorAngles( lara_item->pos.x - old_pos.x, lara_item->pos.y - old_pos.y - 256, lara_item->pos.z - old_pos.z, angles); effect->rot.x = angles[1]; effect->rot.y = angles[0]; if (effect->speed < 512) { effect->speed += (effect->speed >> 4) + 4; } if ((time4 & 4) != 0) { M_TriggerPlasmaBallFlame( effect_num, effect->speed >> 1, (XYZ_32) {}); } } const int32_t speed = (effect->speed * Math_Cos(effect->rot.x)) >> W2V_SHIFT; effect->pos = XYZ_32_OffsetYaw(effect->pos, effect->rot.y, speed); effect->pos.y += effect->fall_speed - ((effect->speed * Math_Sin(effect->rot.x)) >> W2V_SHIFT); int16_t room_num = effect->room_num; const SECTOR *const sector = Room_GetSector(effect->pos, &room_num); const int32_t height = Room_GetHeight(sector, effect->pos); const int32_t ceiling = Room_GetCeiling(sector, effect->pos); if (effect->pos.y >= height || effect->pos.y < ceiling) { if (effect->flag1 == 0) { const int32_t count = (Random_GetControl() & 3) + 2; for (int32_t i = 0; i < count; i++) { Willard_TriggerPlasmaBall( old_pos, effect->room_num, effect->rot.y + (Random_GetControl() & 0x3FFF) + 0x6000, 1); } } Effect_Kill(effect_num); return; } if (effect->flag1 == 0 && Lara_IsNearItem(&effect->pos, 200)) { for (int32_t i = 14; i >= 0; i -= 2) { XYZ_32 pos = { 0, 0, 0 }; Collide_GetJointAbsPosition(lara_item, &pos, i); Willard_TriggerPlasmaBall( pos, effect->room_num, Random_GetControl() << 1, 1); } Lara_CatchFireEx(FLAME_GREEN); Lara_TakeDamage(lara_item->hit_points, false); Effect_Kill(effect_num); return; } if (effect->room_num != room_num) { Effect_UpdateRoom(effect_num, lara_item->room_num); } const int32_t falloff = m_Falloffs[effect->flag1]; if (falloff != 0) { const int32_t color_base = Random_GetControl(); Output_AddDynamicLightRGB( effect->pos, falloff, (RGB_888) { .r = color_base & 0x3F, .g = 255 - ((color_base >> 4) & 0x1F), .b = 192 - ((color_base >> 6) & 0x1F), }); } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->draw_func = nullptr; } void Willard_TriggerPlasmaBall( const XYZ_32 pos, const int16_t room_num, const int16_t angle, const int16_t type) { const int16_t effect_num = Effect_Create(room_num); if (effect_num == NO_ITEM) { return; } EFFECT *const effect = Effect_Get(effect_num); effect->pos = pos; effect->rot.x = 0; effect->rot.y = angle; effect->object_id = O_WILLARD_PLASMA_BALL; effect->speed = type != -16 ? (Random_GetControl() & 0x1F) + 16 : 0; effect->fall_speed = -16 * type; effect->flag1 = type; } REGISTER_OBJECT(O_WILLARD_PLASMA_BALL, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/winston.c ================================================ #include #include #include #include #include #include #include // clang-format off #define M_RADIUS (WALL_L / 10) // = 102 #define M_STOP_RANGE SQUARE(WALL_L * 3 / 2) // = 2359296 // clang-format on typedef enum { // clang-format off WINSTON_STATE_EMPTY = 0, WINSTON_STATE_STOP = 1, WINSTON_STATE_WALK = 2, // clang-format on } M_STATE; static bool M_IsAlive(const ITEM *const item) { return item->hit_points > 0; } static bool M_IsTargetable(const ITEM *const item) { return false; } static bool M_CanTakeDamage(const ITEM *const item) { return false; } static bool M_CanBeProjectileTarget(const ITEM *const item) { return false; } static bool M_ShouldSpawnBlood(const ITEM *const item) { return false; } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; if (creature == nullptr) { return; } AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, true); const int16_t angle = Creature_Turn(item, creature->maximum_turn); if (item->current_anim_state == WINSTON_STATE_STOP) { if (item->goal_anim_state != WINSTON_STATE_WALK && (info.distance > M_STOP_RANGE || !info.ahead)) { item->goal_anim_state = WINSTON_STATE_WALK; Sound_Effect(SFX_WINSTON_GRUNT_2, &item->pos, SPM_NORMAL); } } else if (info.distance < M_STOP_RANGE) { if (info.ahead) { item->goal_anim_state = WINSTON_STATE_STOP; if ((creature->flags & 1) != 0) { creature->flags--; } } else if ((creature->flags & 1) == 0) { Sound_Effect(SFX_WINSTON_GRUNT_1, &item->pos, SPM_NORMAL); Sound_Effect(SFX_WINSTON_CUPS, &item->pos, SPM_NORMAL); creature->flags |= 1; } } if (item->touch_bits != 0 && (creature->flags & 2) == 0) { Sound_Effect(SFX_WINSTON_GRUNT_3, &item->pos, SPM_NORMAL); Sound_Effect(SFX_WINSTON_CUPS, &item->pos, SPM_NORMAL); creature->flags |= 2; } else if (item->touch_bits == 0 && (creature->flags & 2) != 0) { creature->flags -= 2; } if (Random_GetDraw() < 0x100) { Sound_Effect(SFX_WINSTON_CUPS, &item->pos, SPM_NORMAL); } Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = Object_Collision; obj->should_spawn_blood_func = M_ShouldSpawnBlood; obj->is_alive_func = M_IsAlive; obj->is_targetable_func = M_IsTargetable; obj->can_take_damage_func = M_CanTakeDamage; obj->can_be_projectile_target_func = M_CanBeProjectileTarget; obj->hit_points = 1; obj->radius = M_RADIUS; obj->shadow_size = UNIT_SHADOW / 4; obj->smartness = -1; obj->intelligent = true; obj->save_position = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_WINSTON, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/winston_army.c ================================================ #include #include #include #include #include #include #include #include #include #include #include // clang-format off #define M_RADIUS (WALL_L / 10) // = 102 #define M_STOP_RANGE SQUARE(WALL_L * 3 / 2) // = 2359296 // clang-format on typedef enum { // clang-format off M_STATE_EMPTY = 0, M_STATE_STOP = 1, M_STATE_WALK = 2, M_STATE_DEF_1 = 3, M_STATE_DEF_2 = 4, M_STATE_DEF_3 = 5, M_STATE_HIT_1 = 6, M_STATE_HIT_2 = 7, M_STATE_HIT_3 = 8, M_STATE_HIT_DOWN = 9, M_STATE_FALL_DOWN = 10, M_STATE_GET_UP = 11, M_STATE_BRUSH_OFF = 12, M_STATE_ON_FLOOR = 13, // clang-format on } M_STATE; typedef struct { int16_t knockdown_timer; bool spawn_checked; } M_PRIV; static bool M_ShouldSpawnBlood(const ITEM *const item) { return false; } static bool M_CanBeExploded(const ITEM *const item) { return false; } static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "knockdown_timer", &p->knockdown_timer)); JSON_SHOULD(JSON_READ(io, "spawn_checked", &p->spawn_checked)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "knockdown_timer", p->knockdown_timer); JSONW_WRITE(io, "spawn_checked", p->spawn_checked); } static bool M_RemoveNormalWinston(void) { const int32_t item_count = Item_GetTotalCount(); for (int32_t item_num = 0; item_num < item_count; item_num++) { ITEM *const item = Item_Get(item_num); if (item->object_id != O_WINSTON || (item->flags & IF_KILLED) != 0) { continue; } item->status = IS_INVISIBLE; Item_Kill(item_num); return true; } return false; } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } const LARA_INFO *const lara = Lara_GetLaraInfo(); ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; M_PRIV *const p = item->priv; AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, true); const int16_t angle = Creature_Turn(item, creature->maximum_turn); if (!p->spawn_checked) { M_RemoveNormalWinston(); p->spawn_checked = true; } if (item->hit_points <= 0) { creature->maximum_turn = 0; switch (item->current_anim_state) { case M_STATE_HIT_DOWN: case M_STATE_FALL_DOWN: if (item->hit_status) { item->goal_anim_state = M_STATE_HIT_DOWN; } else { p->knockdown_timer--; if (p->knockdown_timer < 0) { item->goal_anim_state = M_STATE_ON_FLOOR; } } break; case M_STATE_GET_UP: item->hit_points = 16; if (Random_GetControl() & 1) { creature->flags = 999; } break; case M_STATE_ON_FLOOR: if (item->hit_status) { item->goal_anim_state = M_STATE_HIT_DOWN; } else { p->knockdown_timer--; if (p->knockdown_timer < 0) { item->goal_anim_state = M_STATE_GET_UP; } } break; default: Item_SwitchToObjAnim(item, 16, 0, O_WINSTON_ARMY); item->current_anim_state = M_STATE_FALL_DOWN; item->goal_anim_state = M_STATE_FALL_DOWN; p->knockdown_timer = 150; break; } } else { switch (item->current_anim_state) { case M_STATE_STOP: creature->maximum_turn = DEG_1 * 2; if (creature->flags == 999) { item->goal_anim_state = M_STATE_BRUSH_OFF; } else if (lara->target == item) { item->goal_anim_state = M_STATE_DEF_1; } else if ( (info.distance > M_STOP_RANGE || !info.ahead) && item->goal_anim_state != M_STATE_WALK) { item->goal_anim_state = M_STATE_WALK; Sound_Effect(SFX_WINSTON_GRUNT_2, &item->pos, SPM_NORMAL); } break; case M_STATE_WALK: creature->maximum_turn = DEG_1 * 2; if (lara->target == item) { item->goal_anim_state = M_STATE_STOP; } else if (info.distance < M_STOP_RANGE) { if (info.ahead) { item->goal_anim_state = M_STATE_STOP; if (creature->flags & 1) { creature->flags--; } } else if ((creature->flags & 1) == 0) { Sound_Effect(SFX_WINSTON_GRUNT_1, &item->pos, SPM_NORMAL); Sound_Effect(SFX_WINSTON_CUPS, &item->pos, SPM_NORMAL); creature->flags |= 1; } } break; case M_STATE_DEF_1: creature->maximum_turn = DEG_1 * 2; if (item->required_anim_state) { item->goal_anim_state = item->required_anim_state; } if (item->hit_status) { item->goal_anim_state = M_STATE_HIT_1; } else if (lara->target != item) { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_DEF_2: creature->maximum_turn = DEG_1 * 2; if (item->required_anim_state) { item->goal_anim_state = item->required_anim_state; } if (item->hit_status) { item->goal_anim_state = M_STATE_HIT_2; } else if (lara->target != item) { item->goal_anim_state = M_STATE_DEF_1; } break; case M_STATE_DEF_3: creature->maximum_turn = DEG_1 * 2; if (item->required_anim_state) { item->goal_anim_state = item->required_anim_state; } if (item->hit_status) { item->goal_anim_state = M_STATE_HIT_3; } else if (lara->target != item) { item->goal_anim_state = M_STATE_DEF_1; } break; case M_STATE_HIT_1: if (Random_GetControl() & 1) { item->required_anim_state = M_STATE_DEF_3; } else { item->required_anim_state = M_STATE_DEF_2; } break; case M_STATE_HIT_2: case M_STATE_HIT_3: item->required_anim_state = M_STATE_DEF_1; break; case M_STATE_BRUSH_OFF: creature->maximum_turn = 0; creature->flags = 0; break; } } if (Random_GetControl() < 0x100) { Sound_Effect(SFX_WINSTON_CUPS, &item->pos, SPM_NORMAL); } Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->control_func = M_Control; obj->collision_func = Object_Collision; obj->should_spawn_blood_func = M_ShouldSpawnBlood; obj->can_be_exploded_func = M_CanBeExploded; obj->hit_points = 20; obj->shadow_size = UNIT_SHADOW / 4; obj->radius = M_RADIUS; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_WINSTON_ARMY, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/wolf.c ================================================ #include #include #include #include #include #include #include // clang-format off #define WOLF_SLEEP_FRAME 96 #define WOLF_BITE_DAMAGE 100 #define WOLF_POUNCE_DAMAGE 50 #define WOLF_WALK_TURN (2 * DEG_1) // = 364 #define WOLF_RUN_TURN (5 * DEG_1) // = 910 #define WOLF_STALK_TURN (2 * DEG_1) // = 364 #define WOLF_ATTACK_RANGE SQUARE(WALL_L * 3 / 2) // = 2359296 #define WOLF_STALK_RANGE SQUARE(WALL_L * 3) // = 9437184 #define WOLF_BITE_RANGE SQUARE(345) // = 119025 #define WOLF_WAKE_CHANCE 32 #define WOLF_SLEEP_CHANCE 32 #define WOLF_HOWL_CHANCE 384 #define WOLF_TOUCH 0x774F #define WOLF_HITPOINTS (g_TRVersion == 1 ? 6 : 10) #define WOLF_RADIUS (WALL_L / 3) // = 341 #define WOLF_SMARTNESS 0x2000 // clang-format on typedef enum { // clang-format off WOLF_STATE_EMPTY = 0, WOLF_STATE_STOP = 1, WOLF_STATE_WALK = 2, WOLF_STATE_RUN = 3, WOLF_STATE_JUMP = 4, WOLF_STATE_STALK = 5, WOLF_STATE_ATTACK = 6, WOLF_STATE_HOWL = 7, WOLF_STATE_SLEEP = 8, WOLF_STATE_CROUCH = 9, WOLF_STATE_FAST_TURN = 10, WOLF_STATE_DEATH = 11, WOLF_STATE_BITE = 12, // clang-format on } WOLF_STATE; typedef enum { WOLF_ANIM_DEATH = 20, } WOLF_ANIM; static BITE m_WolfJawBite = { .pos = { 0, -14, 174 }, .mesh_num = 6 }; static void M_Initialise(const int16_t item_num) { Item_Get(item_num)->frame_num = WOLF_SLEEP_FRAME; Creature_Initialise(item_num); } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const wolf = item->creature_data; int16_t head = 0; int16_t angle = 0; int16_t tilt = 0; if (item->hit_points <= 0) { if (item->current_anim_state != WOLF_STATE_DEATH) { item->current_anim_state = WOLF_STATE_DEATH; Item_SwitchToAnim( item, WOLF_ANIM_DEATH + (int16_t)(Random_GetControl() / 11000), 0); } } else { AI_INFO info; Creature_AIInfo(item, &info); if (info.ahead) { head = info.angle; } Creature_Mood(item, &info, false); angle = Creature_Turn(item, wolf->maximum_turn); switch (item->current_anim_state) { case WOLF_STATE_SLEEP: head = 0; if (wolf->mood == MOOD_ESCAPE || info.zone_num == info.enemy_zone_num) { item->required_anim_state = WOLF_STATE_CROUCH; item->goal_anim_state = WOLF_STATE_STOP; } else if (Random_GetControl() < WOLF_WAKE_CHANCE) { item->required_anim_state = WOLF_STATE_WALK; item->goal_anim_state = WOLF_STATE_STOP; } break; case WOLF_STATE_STOP: if (item->required_anim_state) { item->goal_anim_state = item->required_anim_state; } else { item->goal_anim_state = WOLF_STATE_WALK; } break; case WOLF_STATE_WALK: wolf->maximum_turn = WOLF_WALK_TURN; if (wolf->mood != MOOD_BORED) { item->goal_anim_state = WOLF_STATE_STALK; item->required_anim_state = WOLF_STATE_EMPTY; } else if (Random_GetControl() < WOLF_SLEEP_CHANCE) { item->required_anim_state = WOLF_STATE_SLEEP; item->goal_anim_state = WOLF_STATE_STOP; } break; case WOLF_STATE_CROUCH: if (item->required_anim_state) { item->goal_anim_state = item->required_anim_state; } else if (wolf->mood == MOOD_ESCAPE) { item->goal_anim_state = WOLF_STATE_RUN; } else if (info.distance < WOLF_BITE_RANGE && info.bite) { item->goal_anim_state = WOLF_STATE_BITE; } else if (wolf->mood == MOOD_STALK) { item->goal_anim_state = WOLF_STATE_STALK; } else if (wolf->mood == MOOD_BORED) { item->goal_anim_state = WOLF_STATE_STOP; } else { item->goal_anim_state = WOLF_STATE_RUN; } break; case WOLF_STATE_STALK: wolf->maximum_turn = WOLF_STALK_TURN; if (wolf->mood == MOOD_ESCAPE) { item->goal_anim_state = WOLF_STATE_RUN; } else if (info.distance < WOLF_BITE_RANGE && info.bite) { item->goal_anim_state = WOLF_STATE_BITE; } else if (info.distance > WOLF_STALK_RANGE) { item->goal_anim_state = WOLF_STATE_RUN; } else if (wolf->mood == MOOD_ATTACK) { if (!info.ahead || info.distance > WOLF_ATTACK_RANGE || (info.enemy_facing < FRONT_ARC && info.enemy_facing > -FRONT_ARC)) { item->goal_anim_state = WOLF_STATE_RUN; } } else if (Random_GetControl() < WOLF_HOWL_CHANCE) { item->required_anim_state = WOLF_STATE_HOWL; item->goal_anim_state = WOLF_STATE_CROUCH; } else if (wolf->mood == MOOD_BORED) { item->goal_anim_state = WOLF_STATE_CROUCH; } break; case WOLF_STATE_RUN: wolf->maximum_turn = WOLF_RUN_TURN; tilt = angle; if (info.ahead && info.distance < WOLF_ATTACK_RANGE) { if (info.distance > (WOLF_ATTACK_RANGE / 2) && (info.enemy_facing > FRONT_ARC || info.enemy_facing < -FRONT_ARC)) { item->required_anim_state = WOLF_STATE_STALK; item->goal_anim_state = WOLF_STATE_CROUCH; } else { item->goal_anim_state = WOLF_STATE_ATTACK; item->required_anim_state = WOLF_STATE_EMPTY; } } else if ( wolf->mood == MOOD_STALK && info.distance < WOLF_STALK_RANGE) { item->required_anim_state = WOLF_STATE_STALK; item->goal_anim_state = WOLF_STATE_CROUCH; } else if (wolf->mood == MOOD_BORED) { item->goal_anim_state = WOLF_STATE_CROUCH; } break; case WOLF_STATE_ATTACK: tilt = angle; if (item->required_anim_state == WOLF_STATE_EMPTY && (item->touch_bits & WOLF_TOUCH)) { Creature_Effect(item, &m_WolfJawBite, Spawn_Blood); Lara_TakeDamage(WOLF_POUNCE_DAMAGE, true); item->required_anim_state = WOLF_STATE_RUN; } item->goal_anim_state = WOLF_STATE_RUN; break; case WOLF_STATE_BITE: if (item->required_anim_state == WOLF_STATE_EMPTY && (item->touch_bits & WOLF_TOUCH) && info.ahead) { Creature_Effect(item, &m_WolfJawBite, Spawn_Blood); Lara_TakeDamage(WOLF_BITE_DAMAGE, true); item->required_anim_state = WOLF_STATE_CROUCH; } break; } } Creature_Tilt(item, tilt); Creature_Head(item, head); Creature_Animate(item_num, angle, tilt); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->shadow_size = UNIT_SHADOW / 2; obj->hit_points = WOLF_HITPOINTS; obj->pivot_length = 375; obj->radius = WOLF_RADIUS; obj->smartness = WOLF_SMARTNESS; obj->lot_setup = LOT_Setup(LOT_SETUP_QUADRUPED); obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_anim = true; obj->save_flags = true; Object_GetBone(obj, 2)->rot.y = true; } REGISTER_OBJECT(O_WOLF, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/worker_1.c ================================================ #include #include #include #include #include #include // clang-format off #define WORKER_1_HITPOINTS 25 #define WORKER_1_SHOOT_DAMAGE 150 #define WORKER_1_WALK_TURN (DEG_1 * 3) // = 546 #define WORKER_1_RUN_TURN (DEG_1 * 5) // = 910 #define WORKER_1_RUN_RANGE SQUARE(WALL_L * 2) // = 4194304 #define WORKER_1_SHOOT_1_RANGE SQUARE(WALL_L * 3) // = 9437184 // clang-format on typedef enum { // clang-format off WORKER_1_STATE_EMPTY = 0, WORKER_1_STATE_WALK = 1, WORKER_1_STATE_STOP = 2, WORKER_1_STATE_WAIT = 3, WORKER_1_STATE_SHOOT_1 = 4, WORKER_1_STATE_RUN = 5, WORKER_1_STATE_SHOOT_2 = 6, WORKER_1_STATE_DEATH = 7, WORKER_1_STATE_AIM_1 = 8, WORKER_1_STATE_AIM_2 = 9, WORKER_1_STATE_SHOOT_3 = 10, // clang-format on } WORKER_1_STATE; typedef enum { WORKER_1_ANIM_DEATH = 18, } WORKER_1_ANIM; static const CREATURE_GUN m_Worker1Gun = { .muzzle = { .pos = { .x = 0, .y = 281, .z = 40 }, .mesh_num = 9 }, }; static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t head = 0; int16_t tilt = 0; int16_t neck = 0; int16_t angle = 0; if (item->hit_points <= 0) { if (item->current_anim_state != WORKER_1_STATE_DEATH) { Item_SwitchToAnim(item, WORKER_1_ANIM_DEATH, 0); item->current_anim_state = WORKER_1_STATE_DEATH; } } else { AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, false); angle = Creature_Turn(item, creature->maximum_turn); switch (item->current_anim_state) { case WORKER_1_STATE_STOP: if (info.ahead) { neck = info.angle; } creature->flags = 0; creature->maximum_turn = 0; if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = WORKER_1_STATE_RUN; } else if (Creature_CanTargetEnemy(item, &info)) { if (info.distance >= WORKER_1_SHOOT_1_RANGE && info.zone_num == info.enemy_zone_num) { item->goal_anim_state = WORKER_1_STATE_WALK; } else if (Random_GetControl() < 0x4000) { item->goal_anim_state = WORKER_1_STATE_AIM_1; } else { item->goal_anim_state = WORKER_1_STATE_AIM_2; } } else if (creature->mood == MOOD_BORED && info.ahead) { item->goal_anim_state = WORKER_1_STATE_WAIT; } else if (info.distance > WORKER_1_RUN_RANGE) { item->goal_anim_state = WORKER_1_STATE_RUN; } else { item->goal_anim_state = WORKER_1_STATE_WALK; } break; case WORKER_1_STATE_WAIT: if (info.ahead) { neck = info.angle; } if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = WORKER_1_STATE_SHOOT_1; } else if (creature->mood != MOOD_BORED || !info.ahead) { item->goal_anim_state = WORKER_1_STATE_STOP; } break; case WORKER_1_STATE_WALK: if (info.ahead) { neck = info.angle; } creature->flags = 0; creature->maximum_turn = WORKER_1_WALK_TURN; if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = WORKER_1_STATE_RUN; } else if (Creature_CanTargetEnemy(item, &info)) { if (info.distance < WORKER_1_SHOOT_1_RANGE || info.zone_num != info.enemy_zone_num) { item->goal_anim_state = WORKER_1_STATE_STOP; } else { item->goal_anim_state = WORKER_1_STATE_SHOOT_2; } } else if (creature->mood == MOOD_BORED && info.ahead) { item->goal_anim_state = WORKER_1_STATE_STOP; } else if (info.distance > WORKER_1_RUN_RANGE) { item->goal_anim_state = WORKER_1_STATE_RUN; } break; case WORKER_1_STATE_RUN: if (info.ahead) { neck = info.angle; } creature->maximum_turn = WORKER_1_RUN_TURN; tilt = angle / 2; if (creature->mood == MOOD_ESCAPE) { } else if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = WORKER_1_STATE_WALK; } else if ( creature->mood == MOOD_BORED || creature->mood == MOOD_STALK) { item->goal_anim_state = WORKER_1_STATE_WALK; } break; case WORKER_1_STATE_AIM_1: if (info.ahead) { head = info.angle; } creature->flags = 0; break; case WORKER_1_STATE_AIM_2: if (info.ahead) { head = info.angle; } creature->flags = 0; if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = WORKER_1_STATE_SHOOT_3; } break; case WORKER_1_STATE_SHOOT_1: case WORKER_1_STATE_SHOOT_3: if (info.ahead) { head = info.angle; } if (creature->flags == 0) { Creature_Shoot( item, &info, &m_Worker1Gun, head, WORKER_1_SHOOT_DAMAGE); creature->flags = 1; } if (item->goal_anim_state != WORKER_1_STATE_STOP && (creature->mood == MOOD_ESCAPE || info.distance > WORKER_1_SHOOT_1_RANGE || !Creature_CanTargetEnemy(item, &info))) { item->goal_anim_state = WORKER_1_STATE_STOP; } break; case WORKER_1_STATE_SHOOT_2: if (info.ahead) { head = info.angle; } if (creature->flags == 0) { Creature_Shoot( item, &info, &m_Worker1Gun, head, WORKER_1_SHOOT_DAMAGE); creature->flags = 1; } break; default: break; } } Creature_Tilt(item, tilt); Creature_Head(item, head); Creature_Neck(item, neck); Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->hit_points = WORKER_1_HITPOINTS; obj->radius = WORKER_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->pivot_length = 0; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 4)->rot.y = true; Object_GetBone(obj, 13)->rot.y = true; } REGISTER_OBJECT(O_WORKER_1, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/worker_2.c ================================================ #include #include #include #include #include #include #define WORKER_2_HITPOINTS 20 #define WORKER_5_HITPOINTS 20 // clang-format off #define WORKER_2_SHOOT_DAMAGE 30 #define WORKER_2_WALK_TURN (DEG_1 * 3) // = 546 #define WORKER_2_RUN_TURN (DEG_1 * 5) // = 910 #define WORKER_2_RUN_RANGE SQUARE(WALL_L * 2) // = 4194304 #define WORKER_2_SHOOT_1_RANGE SQUARE(WALL_L * 3) // = 9437184 // clang-format on typedef enum { // clang-format off WORKER_2_STATE_EMPTY = 0, WORKER_2_STATE_STOP = 1, WORKER_2_STATE_WALK = 2, WORKER_2_STATE_RUN = 3, WORKER_2_STATE_WAIT = 4, WORKER_2_STATE_SHOOT_1 = 5, WORKER_2_STATE_SHOOT_2 = 6, WORKER_2_STATE_DEATH = 7, WORKER_2_STATE_AIM_1 = 8, WORKER_2_STATE_AIM_2 = 9, WORKER_2_STATE_AIM_3 = 10, WORKER_2_STATE_SHOOT_3 = 11, // clang-format on } WORKER_2_STATE; typedef enum { WORKER_2_ANIM_DEATH = 19, } WORKER_2_ANIM; static const CREATURE_GUN m_Worker2Gun = { .muzzle = { .pos = { .x = 0, .y = 308, .z = 32 }, .mesh_num = 9 }, }; static void M_ShootAtLara( ITEM *const item, CREATURE *const creature, const AI_INFO *const info, const int16_t head) { if (item->object_id == O_WORKER_2) { if (creature->flags != 0) { creature->flags--; } else { Creature_Shoot( item, info, &m_Worker2Gun, head, WORKER_2_SHOOT_DAMAGE); creature->flags = 5; } } else { Creature_Effect(item, &m_Worker2Gun.muzzle, Spawn_FireStream); } } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t head = 0; int16_t tilt = 0; int16_t neck = 0; int16_t angle = 0; if (item->hit_points <= 0) { if (item->current_anim_state != WORKER_2_STATE_DEATH) { Item_SwitchToAnim(item, WORKER_2_ANIM_DEATH, 0); item->current_anim_state = WORKER_2_STATE_DEATH; } } else { AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, false); angle = Creature_Turn(item, creature->maximum_turn); switch (item->current_anim_state) { case WORKER_2_STATE_STOP: if (info.ahead) { neck = info.angle; } creature->flags = 0; creature->maximum_turn = 0; if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = WORKER_2_STATE_RUN; } else if (Creature_CanTargetEnemy(item, &info)) { if (info.distance >= WORKER_2_SHOOT_1_RANGE && info.zone_num == info.enemy_zone_num) { item->goal_anim_state = WORKER_2_STATE_WALK; } else if ( item->object_id == O_WORKER_5 || Random_GetControl() < 0x4000) { item->goal_anim_state = WORKER_2_STATE_AIM_1; } else { item->goal_anim_state = WORKER_2_STATE_AIM_3; } } else if (creature->mood == MOOD_BORED && info.ahead) { item->goal_anim_state = WORKER_2_STATE_WAIT; } else if (info.distance > WORKER_2_RUN_RANGE) { item->goal_anim_state = WORKER_2_STATE_RUN; } else { item->goal_anim_state = WORKER_2_STATE_WALK; } break; case WORKER_2_STATE_WAIT: if (info.ahead) { neck = info.angle; } if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = WORKER_2_STATE_SHOOT_1; } else if (creature->mood != MOOD_BORED || !info.ahead) { item->goal_anim_state = WORKER_2_STATE_STOP; } break; case WORKER_2_STATE_WALK: if (info.ahead) { neck = info.angle; } creature->flags = 0; creature->maximum_turn = WORKER_2_WALK_TURN; if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = WORKER_2_STATE_RUN; } else if (Creature_CanTargetEnemy(item, &info)) { if (info.distance < WORKER_2_SHOOT_1_RANGE || info.zone_num != info.enemy_zone_num) { item->goal_anim_state = WORKER_2_STATE_STOP; } else { item->goal_anim_state = WORKER_2_STATE_AIM_2; } } else if (creature->mood == MOOD_BORED && info.ahead) { item->goal_anim_state = WORKER_2_STATE_STOP; } else if (info.distance > WORKER_2_RUN_RANGE) { item->goal_anim_state = WORKER_2_STATE_RUN; } break; case WORKER_2_STATE_RUN: if (info.ahead) { neck = info.angle; } tilt = angle / 2; creature->maximum_turn = WORKER_2_RUN_TURN; if (creature->mood == MOOD_ESCAPE) { } else if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = WORKER_2_STATE_WALK; } else if ( creature->mood == MOOD_BORED || creature->mood == MOOD_STALK) { item->goal_anim_state = WORKER_2_STATE_WALK; } break; case WORKER_2_STATE_AIM_1: case WORKER_2_STATE_AIM_3: creature->flags = 0; if (info.ahead) { head = info.angle; if (!Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = WORKER_2_STATE_STOP; } else if (item->current_anim_state == WORKER_2_STATE_AIM_1) { item->goal_anim_state = WORKER_2_STATE_SHOOT_1; } else { item->goal_anim_state = WORKER_2_STATE_SHOOT_3; } } break; case WORKER_2_STATE_AIM_2: creature->flags = 0; if (info.ahead) { head = info.angle; if (Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = WORKER_2_STATE_SHOOT_2; } else { item->goal_anim_state = WORKER_2_STATE_WALK; } } break; case WORKER_2_STATE_SHOOT_1: case WORKER_2_STATE_SHOOT_2: if (info.ahead) { head = info.angle; } M_ShootAtLara(item, creature, &info, head); break; case WORKER_2_STATE_SHOOT_3: if (item->goal_anim_state != WORKER_2_STATE_STOP) { if (creature->mood == MOOD_ESCAPE || info.distance > WORKER_2_SHOOT_1_RANGE || !Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = WORKER_2_STATE_STOP; } } if (info.ahead) { head = info.angle; } M_ShootAtLara(item, creature, &info, head); break; default: break; } } Creature_Tilt(item, tilt); Creature_Head(item, head); Creature_Neck(item, neck); Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->hit_points = WORKER_2_HITPOINTS; obj->radius = WORKER_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->pivot_length = 0; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 4)->rot.y = true; Object_GetBone(obj, 13)->rot.y = true; } static void M_Setup5(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->hit_points = WORKER_5_HITPOINTS; obj->radius = WORKER_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->pivot_length = 0; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 4)->rot.y = true; Object_GetBone(obj, 13)->rot.y = true; } REGISTER_OBJECT(O_WORKER_2, M_Setup) REGISTER_OBJECT(O_WORKER_5, M_Setup5) ================================================ FILE: src/trx/game/objects/creatures/worker_3.c ================================================ #include #include #include #include #include #include #include #include #include // clang-format off #define WORKER_3_HITPOINTS 27 #define WORKER_4_HITPOINTS 27 #define WORKER_3_HIT_DAMAGE 80 #define WORKER_3_SWIPE_DAMAGE 100 #define WORKER_3_WALK_TURN (DEG_1 * 5) // = 910 #define WORKER_3_RUN_TURN (DEG_1 * 6) // = 1092 #define WORKER_3_ATTACK_0_RANGE SQUARE(WALL_L / 2) // = 262144 = 0x40000 #define WORKER_3_ATTACK_1_RANGE SQUARE(WALL_L) // = 1048576 = 0x100000 #define WORKER_3_ATTACK_2_RANGE SQUARE(WALL_L * 3 / 2) // = WORKER_3_ATTACK_2_RANGE #define WORKER_3_WALK_RANGE SQUARE(WALL_L * 2) // = 4194304 = 0x400000 #define WORKER_3_WALK_CHANCE 0x100 #define WORKER_3_WAIT_CHANCE 0x100 #define WORKER_3_TOUCH_BITS 0b00000110'00000000 // = 0x600 #define WORKER_3_VAULT_SHIFT 260 // clang-format on typedef enum { // clang-format off WORKER_3_STATE_EMPTY = 0, WORKER_3_STATE_STOP = 1, WORKER_3_STATE_WALK = 2, WORKER_3_STATE_PUNCH_2 = 3, WORKER_3_STATE_AIM_2 = 4, WORKER_3_STATE_WAIT = 5, WORKER_3_STATE_AIM_1 = 6, WORKER_3_STATE_AIM_0 = 7, WORKER_3_STATE_PUNCH_1 = 8, WORKER_3_STATE_PUNCH_0 = 9, WORKER_3_STATE_RUN = 10, WORKER_3_STATE_DEATH = 11, WORKER_3_STATE_CLIMB_3 = 12, WORKER_3_STATE_CLIMB_1 = 13, WORKER_3_STATE_CLIMB_2 = 14, WORKER_3_STATE_FALL_3 = 15, // clang-format on } WORKER_3_STATE; typedef enum { // clang-format off WORKER_3_ANIM_DEATH = 26, WORKER_3_ANIM_CLIMB_1 = 28, WORKER_3_ANIM_CLIMB_2 = 29, WORKER_3_ANIM_CLIMB_3 = 27, WORKER_3_ANIM_FALL_3 = 30, // clang-format on } WORKER_3_ANIM; static const BITE m_Worker3Hit = { .pos = { .x = 247, .y = 10, .z = 11 }, .mesh_num = 10, }; static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t tilt = 0; int16_t angle = 0; int16_t neck = 0; int16_t head = 0; if (item->hit_points <= 0) { if (item->current_anim_state != WORKER_3_STATE_DEATH) { Item_SwitchToObjAnim(item, WORKER_3_ANIM_DEATH, 0, O_WORKER_3); item->current_anim_state = WORKER_3_STATE_DEATH; } } else { AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, false); angle = Creature_Turn(item, creature->maximum_turn); switch (item->current_anim_state) { case WORKER_3_STATE_STOP: creature->flags = 0; if (info.ahead) { neck = info.angle; } creature->maximum_turn = 0; if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = WORKER_3_STATE_RUN; } else if (creature->mood == MOOD_BORED) { if (item->required_anim_state != WORKER_3_STATE_EMPTY) { item->goal_anim_state = item->required_anim_state; } else if (Random_GetControl() < 0x4000) { item->goal_anim_state = WORKER_3_STATE_WALK; } else { item->goal_anim_state = WORKER_3_STATE_WAIT; } } else if (!info.bite) { item->goal_anim_state = WORKER_3_STATE_RUN; } else if (info.distance < WORKER_3_ATTACK_0_RANGE) { item->goal_anim_state = WORKER_3_STATE_AIM_0; } else if (info.distance < WORKER_3_ATTACK_1_RANGE) { item->goal_anim_state = WORKER_3_STATE_AIM_1; } else if (info.distance < WORKER_3_WALK_RANGE) { item->goal_anim_state = WORKER_3_STATE_WALK; } else { item->goal_anim_state = WORKER_3_STATE_RUN; } break; case WORKER_3_STATE_WAIT: if (info.ahead) { neck = info.angle; } if (creature->mood != MOOD_BORED) { item->goal_anim_state = WORKER_3_STATE_STOP; } else if (Random_GetControl() < WORKER_3_WALK_CHANCE) { item->required_anim_state = WORKER_3_STATE_WALK; item->goal_anim_state = WORKER_3_STATE_STOP; } break; case WORKER_3_STATE_WALK: if (info.ahead) { neck = info.angle; } creature->maximum_turn = WORKER_3_WALK_TURN; if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = WORKER_3_STATE_RUN; } else if (creature->mood == MOOD_BORED) { if (Random_GetControl() < WORKER_3_WAIT_CHANCE) { item->required_anim_state = WORKER_3_STATE_WAIT; item->goal_anim_state = WORKER_3_STATE_STOP; } } else if (!info.bite) { item->goal_anim_state = WORKER_3_STATE_RUN; } else if (info.distance < WORKER_3_ATTACK_0_RANGE) { item->goal_anim_state = WORKER_3_STATE_STOP; } else if (info.distance < WORKER_3_ATTACK_2_RANGE) { item->goal_anim_state = WORKER_3_STATE_AIM_2; } else { item->goal_anim_state = WORKER_3_STATE_RUN; } break; case WORKER_3_STATE_RUN: if (info.ahead) { neck = info.angle; } creature->maximum_turn = WORKER_3_RUN_TURN; tilt = angle / 2; if (creature->mood == MOOD_ESCAPE) { } else if (creature->mood == MOOD_BORED) { item->goal_anim_state = WORKER_3_STATE_WALK; } else if (info.ahead && info.distance < WORKER_3_WALK_RANGE) { item->goal_anim_state = WORKER_3_STATE_WALK; } break; case WORKER_3_STATE_AIM_0: if (info.ahead) { head = info.angle; } creature->flags = 0; if (info.bite && info.distance < WORKER_3_ATTACK_0_RANGE) { item->goal_anim_state = WORKER_3_STATE_PUNCH_0; } else { item->goal_anim_state = WORKER_3_STATE_STOP; } break; case WORKER_3_STATE_AIM_1: if (info.ahead) { head = info.angle; } creature->flags = 0; if (info.ahead && info.distance < WORKER_3_ATTACK_1_RANGE) { item->goal_anim_state = WORKER_3_STATE_PUNCH_1; } else { item->goal_anim_state = WORKER_3_STATE_STOP; } break; case WORKER_3_STATE_AIM_2: if (info.ahead) { head = info.angle; } creature->flags = 0; if (info.bite && info.distance < WORKER_3_ATTACK_2_RANGE) { item->goal_anim_state = WORKER_3_STATE_PUNCH_2; } else { item->goal_anim_state = WORKER_3_STATE_WALK; } break; case WORKER_3_STATE_PUNCH_0: if (info.ahead) { head = info.angle; } if (creature->flags == 0 && (item->touch_bits & WORKER_3_TOUCH_BITS) != 0) { Lara_TakeDamage(WORKER_3_HIT_DAMAGE, true); Creature_Effect(item, &m_Worker3Hit, Spawn_Blood); Sound_Effect(SFX_ENEMY_HIT_2, &item->pos, SPM_NORMAL); creature->flags = 1; } break; case WORKER_3_STATE_PUNCH_1: if (info.ahead) { head = info.angle; } if (creature->flags == 0 && (item->touch_bits & WORKER_3_TOUCH_BITS) != 0) { Lara_TakeDamage(WORKER_3_HIT_DAMAGE, true); Creature_Effect(item, &m_Worker3Hit, Spawn_Blood); Sound_Effect(SFX_ENEMY_HIT_1, &item->pos, SPM_NORMAL); creature->flags = 1; } if (info.ahead && info.distance > WORKER_3_ATTACK_1_RANGE && info.distance < WORKER_3_ATTACK_2_RANGE) { item->goal_anim_state = WORKER_3_STATE_PUNCH_2; } break; case WORKER_3_STATE_PUNCH_2: if (info.ahead) { head = info.angle; } if (creature->flags != 2 && (item->touch_bits & WORKER_3_TOUCH_BITS) != 0) { Lara_TakeDamage(WORKER_3_SWIPE_DAMAGE, true); Creature_Effect(item, &m_Worker3Hit, Spawn_Blood); Sound_Effect(SFX_ENEMY_HIT_1, &item->pos, SPM_NORMAL); creature->flags = 2; } break; default: break; } } Creature_Tilt(item, tilt); Creature_Head(item, head); Creature_Neck(item, neck); if (item->current_anim_state >= WORKER_3_STATE_CLIMB_3) { Creature_Animate(item_num, angle, 0); } else { switch (Creature_Vault(item_num, angle, 2, WORKER_3_VAULT_SHIFT)) { case -4: Item_SwitchToObjAnim(item, WORKER_3_ANIM_FALL_3, 0, O_WORKER_3); item->current_anim_state = WORKER_3_STATE_FALL_3; break; case 2: Item_SwitchToObjAnim(item, WORKER_3_ANIM_CLIMB_1, 0, O_WORKER_3); item->current_anim_state = WORKER_3_STATE_CLIMB_1; break; case 3: Item_SwitchToObjAnim(item, WORKER_3_ANIM_CLIMB_2, 0, O_WORKER_3); item->current_anim_state = WORKER_3_STATE_CLIMB_2; break; case 4: Item_SwitchToObjAnim(item, WORKER_3_ANIM_CLIMB_3, 0, O_WORKER_3); item->current_anim_state = WORKER_3_STATE_CLIMB_3; break; default: return; } } } static void M_SetupBase(OBJECT *const obj) { obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->radius = WORKER_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->pivot_length = 0; obj->lot_setup = LOT_Setup(LOT_SETUP_CLIMBER); obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 0)->rot.y = true; Object_GetBone(obj, 4)->rot.y = true; } static void M_Setup3(OBJECT *const obj) { if (!obj->loaded) { return; } M_SetupBase(obj); obj->hit_points = WORKER_3_HITPOINTS; } static void M_Setup4(OBJECT *const obj) { if (!obj->loaded) { return; } M_SetupBase(obj); obj->hit_points = WORKER_4_HITPOINTS; } REGISTER_OBJECT(O_WORKER_3, M_Setup3) REGISTER_OBJECT(O_WORKER_4, M_Setup4) ================================================ FILE: src/trx/game/objects/creatures/worker_common.h ================================================ #pragma once #include #define WORKER_RADIUS (WALL_L / 10) // = 102 ================================================ FILE: src/trx/game/objects/creatures/xian_common.c ================================================ #include #include #include bool XianWarrior_Draw(const ITEM *item) { const OBJECT *swap; if (item->object_id == O_XIAN_SPEARMAN) { swap = Object_Get(O_XIAN_SPEARMAN_STATUE); } else { swap = Object_Get(O_XIAN_KNIGHT_STATUE); } return Object_DrawAnimatingItemWithSwap(item, swap); } ================================================ FILE: src/trx/game/objects/creatures/xian_common.h ================================================ #pragma once #include bool XianWarrior_Draw(const ITEM *item); ================================================ FILE: src/trx/game/objects/creatures/xian_knight.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include // clang-format off #define XIAN_KNIGHT_HITPOINTS 80 #define XIAN_KNIGHT_TOUCH_BITS 0b11000000'00000000 // = 0xC000 #define XIAN_KNIGHT_RADIUS (WALL_L / 5) // = 204 #define XIAN_KNIGHT_HACK_DAMAGE 300 #define XIAN_KNIGHT_WALK_TURN (DEG_1 * 5) // = 910 #define XIAN_KNIGHT_FLY_TURN (DEG_1 * 4) // = 728 #define XIAN_KNIGHT_ATTACK_1_RANGE SQUARE(WALL_L) // = 1048576 #define XIAN_KNIGHT_ATTACK_3_RANGE SQUARE(WALL_L * 2) // = 4194304 // clang-format on typedef enum { // clang-format off XIAN_KNIGHT_STATE_EMPTY = 0, XIAN_KNIGHT_STATE_STOP = 1, XIAN_KNIGHT_STATE_WALK = 2, XIAN_KNIGHT_STATE_AIM_1 = 3, XIAN_KNIGHT_STATE_SLASH_1 = 4, XIAN_KNIGHT_STATE_AIM_2 = 5, XIAN_KNIGHT_STATE_SLASH_2 = 6, XIAN_KNIGHT_STATE_WAIT = 7, XIAN_KNIGHT_STATE_FLY = 8, XIAN_KNIGHT_STATE_START = 9, XIAN_KNIGHT_STATE_AIM_3 = 10, XIAN_KNIGHT_STATE_SLASH_3 = 11, XIAN_KNIGHT_STATE_DEATH = 12, // clang-format on } XIAN_KNIGHT_STATE; static const BITE m_XianKnightSword = { .pos = { .x = 0, .y = 37, .z = 550 }, .mesh_num = 15, }; static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); item->status = IS_INACTIVE; item->mesh_bits = 0; } static void M_SparkleTrail(const ITEM *const item) { const int16_t effect_num = Effect_Create(item->room_num); if (effect_num != NO_EFFECT) { EFFECT *const effect = Effect_Get(effect_num); effect->object_id = O_TWINKLE; effect->pos.x = item->pos.x + (Random_GetDraw() << 8 >> 15) - 128; effect->pos.y = item->pos.y + (Random_GetDraw() << 8 >> 15) - 256; effect->pos.z = item->pos.z + (Random_GetDraw() << 8 >> 15) - 128; effect->room_num = item->room_num; effect->counter = -30; effect->frame_num = 0; } Sound_Effect(SFX_WARRIOR_HOVER, &item->pos, SPM_NORMAL); } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t head = 0; int16_t neck = 0; int16_t angle = 0; if (item->hit_points <= 0) { item->current_anim_state = XIAN_KNIGHT_STATE_DEATH; item->mesh_bits >>= 1; item->enable_interpolation = false; if (item->mesh_bits == 0) { Sound_Effect(SFX_EXPLOSION_1, nullptr, SPM_NORMAL); item->mesh_bits = -1; item->object_id = O_XIAN_KNIGHT_STATUE; Item_Explode(item_num, -1, 0); item->object_id = O_XIAN_KNIGHT; LOT_DisableBaddieAI(item_num); Item_Kill(item_num); item->status = IS_DEACTIVATED; item->flags |= IF_ONE_SHOT; Carrier_TestItemDrops(item_num); } return; } creature->lot.setup.step = STEP_L; creature->lot.setup.drop = -STEP_L; creature->lot.setup.fly = 0; AI_INFO info; Creature_AIInfo(item, &info); if (item->current_anim_state == XIAN_KNIGHT_STATE_FLY && info.zone_num != info.enemy_zone_num) { creature->lot.setup.step = WALL_L * 20; creature->lot.setup.drop = -WALL_L * 20; creature->lot.setup.fly = STEP_L / 4; Creature_AIInfo(item, &info); } Creature_Mood(item, &info, true); angle = Creature_Turn(item, creature->maximum_turn); if (item->current_anim_state != XIAN_KNIGHT_STATE_START) { item->mesh_bits = -1; } const ITEM *const lara_item = Lara_GetItem(); switch (item->current_anim_state) { case XIAN_KNIGHT_STATE_START: if (creature->flags == 0) { item->mesh_bits = (item->mesh_bits << 1) | 1; creature->flags = 3; } else { creature->flags--; } break; case XIAN_KNIGHT_STATE_STOP: creature->maximum_turn = 0; if (info.ahead) { neck = info.angle; } if (lara_item->hit_points <= 0) { item->goal_anim_state = XIAN_KNIGHT_STATE_WAIT; } else if (info.bite && info.distance < XIAN_KNIGHT_ATTACK_1_RANGE) { if (Random_GetControl() < 0x4000) { item->goal_anim_state = XIAN_KNIGHT_STATE_AIM_1; } else { item->goal_anim_state = XIAN_KNIGHT_STATE_AIM_2; } } else if (info.zone_num != info.enemy_zone_num) { item->goal_anim_state = XIAN_KNIGHT_STATE_FLY; } else { item->goal_anim_state = XIAN_KNIGHT_STATE_WALK; } break; case XIAN_KNIGHT_STATE_WALK: creature->maximum_turn = XIAN_KNIGHT_WALK_TURN; if (info.ahead) { neck = info.angle; } if (lara_item->hit_points <= 0) { item->goal_anim_state = XIAN_KNIGHT_STATE_STOP; } else if (info.bite && info.distance < XIAN_KNIGHT_ATTACK_3_RANGE) { item->goal_anim_state = XIAN_KNIGHT_STATE_AIM_3; } else if (info.zone_num != info.enemy_zone_num) { item->goal_anim_state = XIAN_KNIGHT_STATE_STOP; } break; case XIAN_KNIGHT_STATE_FLY: creature->maximum_turn = XIAN_KNIGHT_FLY_TURN; if (info.ahead) { neck = info.angle; } M_SparkleTrail(item); if (creature->lot.setup.fly == 0) { item->goal_anim_state = XIAN_KNIGHT_STATE_STOP; } break; case XIAN_KNIGHT_STATE_AIM_1: creature->flags = 0; if (info.ahead) { head = info.angle; } if (info.bite && info.distance < XIAN_KNIGHT_ATTACK_1_RANGE) { item->goal_anim_state = XIAN_KNIGHT_STATE_SLASH_1; } else { item->goal_anim_state = XIAN_KNIGHT_STATE_STOP; } break; case XIAN_KNIGHT_STATE_AIM_2: creature->flags = 0; if (info.ahead) { head = info.angle; } if (info.bite && info.distance < XIAN_KNIGHT_ATTACK_1_RANGE) { item->goal_anim_state = XIAN_KNIGHT_STATE_SLASH_2; } else { item->goal_anim_state = XIAN_KNIGHT_STATE_STOP; } break; case XIAN_KNIGHT_STATE_AIM_3: creature->flags = 0; if (info.ahead) { head = info.angle; } if (info.bite && info.distance < XIAN_KNIGHT_ATTACK_3_RANGE) { item->goal_anim_state = XIAN_KNIGHT_STATE_SLASH_3; } else { item->goal_anim_state = XIAN_KNIGHT_STATE_WALK; } break; case XIAN_KNIGHT_STATE_SLASH_1: case XIAN_KNIGHT_STATE_SLASH_2: case XIAN_KNIGHT_STATE_SLASH_3: if (info.ahead) { head = info.angle; } if (creature->flags == 0 && (item->touch_bits & XIAN_KNIGHT_TOUCH_BITS) != 0) { Lara_TakeDamage(XIAN_KNIGHT_HACK_DAMAGE, true); Creature_Effect(item, &m_XianKnightSword, Spawn_Blood); creature->flags = 1; } break; default: break; } Creature_Tilt(item, 0); Creature_Head(item, head); Creature_Neck(item, neck); Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } SOFT_ASSERT( Object_Get(O_XIAN_KNIGHT_STATUE)->loaded, "Xian swordsman statue object missing"); obj->initialise_func = M_Initialise; obj->draw_func = XianWarrior_Draw; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->hit_points = XIAN_KNIGHT_HITPOINTS; obj->radius = XIAN_KNIGHT_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->pivot_length = 0; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 6)->rot.y = true; Object_GetBone(obj, 16)->rot.y = true; } REGISTER_OBJECT(O_XIAN_KNIGHT, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/xian_spearman.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include // clang-format off #define XIAN_SPEARMAN_HITPOINTS 100 #define XIAN_SPEARMAN_HIT_1_DAMAGE 75 #define XIAN_SPEARMAN_HIT_2_DAMAGE 75 #define XIAN_SPEARMAN_HIT_5_DAMAGE 75 #define XIAN_SPEARMAN_HIT_6_DAMAGE 120 #define XIAN_SPEARMAN_RADIUS (WALL_L / 5) // = 204 #define XIAN_SPEARMAN_TOUCH_L_BITS 0b00000000'00001000'00000000 // = 0x00800 #define XIAN_SPEARMAN_TOUCH_R_BITS 0b00000100'00000000'00000000 // = 0x40000 #define XIAN_WALK_TURN (DEG_1 * 3) // = 546 #define XIAN_RUN_TURN (DEG_1 * 5) // = 910 #define XIAN_ATTACK_1_RANGE SQUARE(WALL_L) // = 1048576 #define XIAN_ATTACK_2_RANGE SQUARE(WALL_L * 3 / 2) // = 2359296 #define XIAN_ATTACK_3_RANGE SQUARE(WALL_L * 2) // = 4194304 #define XIAN_ATTACK_4_RANGE SQUARE(WALL_L * 2) // = 4194304 #define XIAN_ATTACK_5_RANGE SQUARE(WALL_L) // = 1048576 #define XIAN_ATTACK_6_RANGE SQUARE(WALL_L * 2) // = 4194304 #define XIAN_RUN_RANGE SQUARE(WALL_L * 3) // = 9437184 #define XIAN_STOP_CHANCE 0x200 #define XIAN_WALK_CHANCE (XIAN_STOP_CHANCE + 0x200) // = 0x400 // clang-format on typedef enum { // clang-format off XIAN_SPEARMAN_STATE_EMPTY = 0, XIAN_SPEARMAN_STATE_STOP = 1, XIAN_SPEARMAN_STATE_STOP_2 = 2, XIAN_SPEARMAN_STATE_WALK = 3, XIAN_SPEARMAN_STATE_RUN = 4, XIAN_SPEARMAN_STATE_AIM_1 = 5, XIAN_SPEARMAN_STATE_HIT_1 = 6, XIAN_SPEARMAN_STATE_AIM_2 = 7, XIAN_SPEARMAN_STATE_HIT_2 = 8, XIAN_SPEARMAN_STATE_AIM_3 = 9, XIAN_SPEARMAN_STATE_HIT_3 = 10, XIAN_SPEARMAN_STATE_AIM_4 = 11, XIAN_SPEARMAN_STATE_HIT_4 = 12, XIAN_SPEARMAN_STATE_AIM_5 = 13, XIAN_SPEARMAN_STATE_HIT_5 = 14, XIAN_SPEARMAN_STATE_AIM_6 = 15, XIAN_SPEARMAN_STATE_HIT_6 = 16, XIAN_SPEARMAN_STATE_DEATH = 17, XIAN_SPEARMAN_STATE_START = 18, XIAN_SPEARMAN_STATE_KILL = 19, // clang-format on } XIAN_SPEARMAN_STATE; typedef enum { // clang-format off XIAN_SPEARMAN_ANIM_DEATH = 0, XIAN_SPEARMAN_ANIM_START = 48, XIAN_SPEARMAN_ANIM_KILL = 49, // clang-format on } XIAN_SPEARMAN_ANIM; static const BITE m_XianSpearmanLeftSpear = { .pos = { .x = 0, .y = 0, .z = 920 }, .mesh_num = 11, }; static const BITE m_XianSpearmanRightSpear = { .pos = { .x = 0, .y = 0, .z = 920 }, .mesh_num = 18, }; static void M_DoDamage( const ITEM *const item, CREATURE *const creature, const int32_t damage) { if ((creature->flags & 1) == 0 && (item->touch_bits & XIAN_SPEARMAN_TOUCH_R_BITS) != 0) { Lara_TakeDamage(damage, true); Creature_Effect(item, &m_XianSpearmanRightSpear, Spawn_Blood); creature->flags |= 1; Sound_Effect(SFX_CRUNCH_2, &item->pos, SPM_NORMAL); } if ((creature->flags & 2) == 0 && (item->touch_bits & XIAN_SPEARMAN_TOUCH_L_BITS) != 0) { Lara_TakeDamage(damage, true); Creature_Effect(item, &m_XianSpearmanLeftSpear, Spawn_Blood); creature->flags |= 2; Sound_Effect(SFX_CRUNCH_2, &item->pos, SPM_NORMAL); } } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); Item_SwitchToAnim(item, XIAN_SPEARMAN_ANIM_START, 0); item->goal_anim_state = XIAN_SPEARMAN_STATE_START; item->current_anim_state = XIAN_SPEARMAN_STATE_START; item->status = IS_INACTIVE; item->mesh_bits = 0; } static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t head = 0; int16_t neck = 0; int16_t angle = 0; const ITEM *const lara_item = Lara_GetItem(); const bool lara_was_alive = lara_item->hit_points > 0; if (item->hit_points <= 0) { item->current_anim_state = XIAN_SPEARMAN_STATE_DEATH; item->mesh_bits >>= 1; item->enable_interpolation = false; if (item->mesh_bits == 0) { Sound_Effect(SFX_EXPLOSION_1, nullptr, SPM_NORMAL); item->mesh_bits = -1; item->object_id = O_XIAN_SPEARMAN_STATUE; Item_Explode(item_num, -1, 0); item->object_id = O_XIAN_SPEARMAN; LOT_DisableBaddieAI(item_num); Item_Kill(item_num); item->status = IS_DEACTIVATED; item->flags |= IF_ONE_SHOT; Carrier_TestItemDrops(item_num); } return; } AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, true); angle = Creature_Turn(item, creature->maximum_turn); if (item->current_anim_state != XIAN_SPEARMAN_STATE_START) { item->mesh_bits = -1; } switch (item->current_anim_state) { case XIAN_SPEARMAN_STATE_START: if (creature->flags == 0) { item->mesh_bits = (item->mesh_bits << 1) | 1; creature->flags = 3; } else { creature->flags--; } break; case XIAN_SPEARMAN_STATE_STOP: if (info.ahead) { neck = info.angle; } creature->maximum_turn = 0; if (creature->mood == MOOD_BORED) { const int32_t random = Random_GetControl(); if (random < XIAN_STOP_CHANCE) { item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP_2; } else if (random < XIAN_WALK_CHANCE) { item->goal_anim_state = XIAN_SPEARMAN_STATE_WALK; } } else if (info.ahead && info.distance < XIAN_ATTACK_1_RANGE) { item->goal_anim_state = XIAN_SPEARMAN_STATE_AIM_1; } else { item->goal_anim_state = XIAN_SPEARMAN_STATE_WALK; } break; case XIAN_SPEARMAN_STATE_STOP_2: if (info.ahead) { neck = info.angle; } creature->maximum_turn = 0; if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = XIAN_SPEARMAN_STATE_WALK; } else if (creature->mood == MOOD_BORED) { const int32_t random = Random_GetControl(); if (random < XIAN_STOP_CHANCE) { item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP; } else if (random < XIAN_WALK_CHANCE) { item->goal_anim_state = XIAN_SPEARMAN_STATE_WALK; } } else if (info.ahead && info.distance < XIAN_ATTACK_5_RANGE) { item->goal_anim_state = XIAN_SPEARMAN_STATE_AIM_5; } else { item->goal_anim_state = XIAN_SPEARMAN_STATE_WALK; } break; case XIAN_SPEARMAN_STATE_WALK: if (info.ahead) { neck = info.angle; } creature->maximum_turn = XIAN_WALK_TURN; if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = XIAN_SPEARMAN_STATE_RUN; } else if (creature->mood == MOOD_BORED) { const int32_t random = Random_GetControl(); if (random < XIAN_STOP_CHANCE) { item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP; } else if (random < XIAN_WALK_CHANCE) { item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP_2; } } else if (info.ahead && info.distance < XIAN_ATTACK_4_RANGE) { if (info.distance < XIAN_ATTACK_2_RANGE) { item->goal_anim_state = XIAN_SPEARMAN_STATE_AIM_2; } else { if (Random_GetControl() < 0x4000) { item->goal_anim_state = XIAN_SPEARMAN_STATE_AIM_3; } else { item->goal_anim_state = XIAN_SPEARMAN_STATE_AIM_4; } } } else if (!info.ahead || info.distance > XIAN_RUN_RANGE) { item->goal_anim_state = XIAN_SPEARMAN_STATE_RUN; } break; case XIAN_SPEARMAN_STATE_RUN: if (info.ahead) { neck = info.angle; } creature->maximum_turn = XIAN_RUN_TURN; if (creature->mood == MOOD_ESCAPE) { } else if (creature->mood == MOOD_BORED) { if (Random_GetControl() < 0x4000) { item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP; } else { item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP_2; } } else if (info.ahead && info.distance < XIAN_ATTACK_6_RANGE) { item->goal_anim_state = XIAN_SPEARMAN_STATE_AIM_6; } break; case XIAN_SPEARMAN_STATE_AIM_1: if (info.ahead) { head = info.angle; } creature->flags = 0; if (!info.ahead || info.distance > XIAN_ATTACK_1_RANGE) { item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP; } else { item->goal_anim_state = XIAN_SPEARMAN_STATE_HIT_1; } break; case XIAN_SPEARMAN_STATE_AIM_2: if (info.ahead) { head = info.angle; } creature->flags = 0; if (!info.ahead || info.distance > XIAN_ATTACK_2_RANGE) { item->goal_anim_state = XIAN_SPEARMAN_STATE_WALK; } else { item->goal_anim_state = XIAN_SPEARMAN_STATE_HIT_2; } break; case XIAN_SPEARMAN_STATE_AIM_3: if (info.ahead) { head = info.angle; } creature->flags = 0; if (!info.ahead || info.distance > XIAN_ATTACK_3_RANGE) { item->goal_anim_state = XIAN_SPEARMAN_STATE_WALK; } else { item->goal_anim_state = XIAN_SPEARMAN_STATE_HIT_2; } break; case XIAN_SPEARMAN_STATE_AIM_4: if (info.ahead) { head = info.angle; } creature->flags = 0; if (!info.ahead || info.distance > XIAN_ATTACK_4_RANGE) { item->goal_anim_state = XIAN_SPEARMAN_STATE_WALK; } else { item->goal_anim_state = XIAN_SPEARMAN_STATE_HIT_2; } break; case XIAN_SPEARMAN_STATE_AIM_5: if (info.ahead) { head = info.angle; } creature->flags = 0; if (!info.ahead || info.distance > XIAN_ATTACK_5_RANGE) { item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP_2; } else { item->goal_anim_state = XIAN_SPEARMAN_STATE_HIT_5; } break; case XIAN_SPEARMAN_STATE_AIM_6: if (info.ahead) { head = info.angle; } creature->flags = 0; if (!info.ahead || info.distance > XIAN_ATTACK_6_RANGE) { item->goal_anim_state = XIAN_SPEARMAN_STATE_RUN; } else { item->goal_anim_state = XIAN_SPEARMAN_STATE_HIT_6; } break; case XIAN_SPEARMAN_STATE_HIT_1: M_DoDamage(item, creature, XIAN_SPEARMAN_HIT_1_DAMAGE); break; case XIAN_SPEARMAN_STATE_HIT_2: case XIAN_SPEARMAN_STATE_HIT_3: case XIAN_SPEARMAN_STATE_HIT_4: if (info.ahead) { head = info.angle; } M_DoDamage(item, creature, XIAN_SPEARMAN_HIT_2_DAMAGE); if (info.ahead && info.distance < XIAN_ATTACK_1_RANGE) { const int32_t random = Random_GetControl(); if (random < 0x4000) { item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP; } else { item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP_2; } } else { item->goal_anim_state = XIAN_SPEARMAN_STATE_WALK; } break; case XIAN_SPEARMAN_STATE_HIT_5: if (info.ahead) { head = info.angle; } M_DoDamage(item, creature, XIAN_SPEARMAN_HIT_5_DAMAGE); if (info.ahead && info.distance < XIAN_ATTACK_1_RANGE) { item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP; } else { item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP_2; } break; case XIAN_SPEARMAN_STATE_HIT_6: if (info.ahead) { head = info.angle; } M_DoDamage(item, creature, XIAN_SPEARMAN_HIT_6_DAMAGE); if (info.ahead && info.distance < XIAN_ATTACK_1_RANGE) { const int32_t random = Random_GetControl(); if (random < 0x4000) { item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP; } else { item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP_2; } } else if (info.ahead && info.distance < XIAN_ATTACK_4_RANGE) { item->goal_anim_state = XIAN_SPEARMAN_STATE_WALK; } else { item->goal_anim_state = XIAN_SPEARMAN_STATE_RUN; } break; default: break; } if (lara_was_alive && lara_item->hit_points <= 0) { Creature_SpecialKill( item, XIAN_SPEARMAN_ANIM_KILL, XIAN_SPEARMAN_STATE_KILL, LS_EXTRA_GUARD_KILL); return; } Creature_Tilt(item, 0); Creature_Head(item, head); Creature_Neck(item, neck); Creature_Animate(item_num, angle, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } SOFT_ASSERT( Object_Get(O_XIAN_SPEARMAN_STATUE)->loaded, "Xian spearman statue object missing"); obj->initialise_func = M_Initialise; obj->draw_func = XianWarrior_Draw; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->hit_points = XIAN_SPEARMAN_HITPOINTS; obj->radius = XIAN_SPEARMAN_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->pivot_length = 0; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 6)->rot.y = true; Object_GetBone(obj, 12)->rot.y = true; } REGISTER_OBJECT(O_XIAN_SPEARMAN, M_Setup) ================================================ FILE: src/trx/game/objects/creatures/yeti.c ================================================ #include #include #include #include #include #include #include // clang-format off #define M_HITPOINTS 30 #define M_TOUCH_BITS_R 0b00000111'00000000 // = 0x0700 #define M_TOUCH_BITS_L 0b00111000'00000000 // = 0x3800 #define M_TOUCH_BITS_LR (M_TOUCH_BITS_R | M_TOUCH_BITS_L) // = 0x3F00 #define M_RADIUS (WALL_L / 8) // = 128 #define M_WALK_TURN (DEG_1 * 4) // = 728 #define M_RUN_TURN (DEG_1 * 6) // = 1092 #define M_VAULT_SHIFT 300 #define M_ATTACK_1_RANGE SQUARE(WALL_L / 2) // = 262144 #define M_ATTACK_2_RANGE SQUARE(WALL_L * 2 / 3) // = 465124 #define M_ATTACK_3_RANGE SQUARE(WALL_L * 2) // = 4194304 #define M_RUN_RANGE SQUARE(WALL_L * 2) // = 4194304 #define M_PUNCH_DAMAGE 100 #define M_THUMP_DAMAGE 150 #define M_CHARGE_DAMAGE 200 #define M_ATTACK_1_CHANCE 0x4000 #define M_WAIT_1_CHANCE 0x100 #define M_WAIT_2_CHANCE (M_WAIT_1_CHANCE + 0x100) // = 512 #define M_WALK_CHANCE (M_WAIT_2_CHANCE + 0x100) // = 768 #define M_STOP_ROAR_CHANCE 0x200 // clang-format on typedef enum { M_STATE_EMPTY, M_STATE_RUN, M_STATE_STOP, M_STATE_WALK, M_STATE_ATTACK_1, M_STATE_ATTACK_2, M_STATE_ATTACK_3, M_STATE_WAIT_1, M_STATE_DEATH, M_STATE_WAIT_2, M_STATE_CLIMB_1, M_STATE_CLIMB_2, M_STATE_CLIMB_3, M_STATE_FALL, M_STATE_KILL, } M_STATE; typedef enum { // clang-format off M_ANIM_KILL = 36, M_ANIM_FALL = 35, M_ANIM_CLIMB_1 = 34, M_ANIM_CLIMB_2 = 33, M_ANIM_CLIMB_3 = 32, M_ANIM_DEATH = 31, // clang-format on } M_ANIM; static const BITE m_YetiBiteL = { .pos = { .x = 12, .y = 101, .z = 19 }, .mesh_num = 13, }; static const BITE m_YetiBiteR = { .pos = { .x = 12, .y = 101, .z = 19 }, .mesh_num = 10, }; static void M_Control(const int16_t item_num) { if (!Creature_Activate(item_num)) { return; } ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; int16_t head = 0; int16_t body = 0; int16_t angle = 0; const ITEM *const lara_item = Lara_GetItem(); const bool lara_was_alive = lara_item->hit_points > 0; if (item->hit_points > 0) { AI_INFO info; Creature_AIInfo(item, &info); Creature_Mood(item, &info, true); if (info.ahead) { head = info.angle; } angle = Creature_Turn(item, creature->maximum_turn); switch (item->current_anim_state) { case M_STATE_STOP: creature->flags = 0; creature->maximum_turn = 0; if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if (item->required_anim_state != M_STATE_EMPTY) { item->goal_anim_state = item->required_anim_state; } else if (creature->mood == MOOD_BORED) { const int32_t random = Random_GetControl(); if (random < M_WAIT_1_CHANCE || !lara_was_alive) { item->goal_anim_state = M_STATE_WAIT_1; } else if (random < M_WAIT_2_CHANCE) { item->goal_anim_state = M_STATE_WAIT_2; } else if (random < M_WALK_CHANCE) { item->goal_anim_state = M_STATE_WALK; } } else if ( info.ahead && info.distance < M_ATTACK_1_RANGE && Random_GetControl() < M_ATTACK_1_CHANCE) { item->goal_anim_state = M_STATE_ATTACK_1; break; } else if (info.ahead && info.distance < M_ATTACK_2_RANGE) { item->goal_anim_state = M_STATE_ATTACK_2; } else if (creature->mood == MOOD_STALK) { item->goal_anim_state = M_STATE_WALK; } else { item->goal_anim_state = M_STATE_RUN; } break; case M_STATE_WAIT_1: if (creature->mood == MOOD_ESCAPE || item->hit_status) { item->goal_anim_state = M_STATE_STOP; } else if (creature->mood == MOOD_BORED) { if (lara_was_alive) { const int32_t random = Random_GetControl(); if (random < M_WAIT_1_CHANCE) { item->goal_anim_state = M_STATE_STOP; } else if (random < M_WAIT_2_CHANCE) { item->goal_anim_state = M_STATE_WAIT_2; } else if (random < M_WALK_CHANCE) { item->goal_anim_state = M_STATE_STOP; item->required_anim_state = M_STATE_WALK; } } } else if (Random_GetControl() < M_STOP_ROAR_CHANCE) { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_WAIT_2: if (creature->mood == MOOD_ESCAPE || item->hit_status) { item->goal_anim_state = M_STATE_STOP; } else if (creature->mood == MOOD_BORED) { const int32_t random = Random_GetControl(); if (random < M_WAIT_1_CHANCE || !lara_was_alive) { item->goal_anim_state = M_STATE_WAIT_1; } else if (random < M_WAIT_2_CHANCE) { item->goal_anim_state = M_STATE_STOP; } else if (random < M_WALK_CHANCE) { item->goal_anim_state = M_STATE_STOP; item->required_anim_state = M_STATE_WALK; } } else if (Random_GetControl() < M_STOP_ROAR_CHANCE) { item->goal_anim_state = M_STATE_STOP; } break; case M_STATE_WALK: creature->maximum_turn = M_WALK_TURN; if (creature->mood == MOOD_ESCAPE) { item->goal_anim_state = M_STATE_RUN; } else if (creature->mood == MOOD_BORED) { const int32_t random = Random_GetControl(); if (random < M_WAIT_1_CHANCE || !lara_was_alive) { item->goal_anim_state = M_STATE_STOP; item->required_anim_state = M_STATE_WAIT_1; } else if (random < M_WAIT_2_CHANCE) { item->goal_anim_state = M_STATE_STOP; item->required_anim_state = M_STATE_WAIT_2; } else if (random < M_WALK_CHANCE) { item->goal_anim_state = M_STATE_STOP; } } else if (creature->mood == MOOD_ATTACK) { if (info.ahead && info.distance < M_ATTACK_2_RANGE) { item->goal_anim_state = M_STATE_STOP; } else if (info.distance > M_RUN_RANGE) { item->goal_anim_state = M_STATE_RUN; } } break; case M_STATE_RUN: creature->flags = 0; creature->maximum_turn = M_RUN_TURN; if (creature->mood == MOOD_ESCAPE) { break; } else if (creature->mood == MOOD_BORED) { item->goal_anim_state = M_STATE_WALK; } else if (info.ahead && info.distance < M_ATTACK_2_RANGE) { item->goal_anim_state = M_STATE_STOP; } else if (info.ahead && info.distance < M_ATTACK_3_RANGE) { item->goal_anim_state = M_STATE_ATTACK_3; } else if (creature->mood == MOOD_STALK) { item->goal_anim_state = M_STATE_WALK; } break; case M_STATE_ATTACK_1: body = head; head = 0; if (creature->flags == 0 && (item->touch_bits & M_TOUCH_BITS_R) != 0) { Creature_Effect(item, &m_YetiBiteR, Spawn_Blood); Lara_TakeDamage(M_PUNCH_DAMAGE, true); creature->flags = 1; break; } break; case M_STATE_ATTACK_2: body = head; head = 0; creature->maximum_turn = M_WALK_TURN; if (creature->flags == 0 && (item->touch_bits & M_TOUCH_BITS_LR) != 0) { if ((item->touch_bits & M_TOUCH_BITS_L) != 0) { Creature_Effect(item, &m_YetiBiteL, Spawn_Blood); } if ((item->touch_bits & M_TOUCH_BITS_R) != 0) { Creature_Effect(item, &m_YetiBiteR, Spawn_Blood); } Lara_TakeDamage(M_THUMP_DAMAGE, true); creature->flags = 1; } break; case M_STATE_ATTACK_3: body = head; head = 0; if (creature->flags == 0 && (item->touch_bits & M_TOUCH_BITS_LR) != 0) { if ((item->touch_bits & M_TOUCH_BITS_L) != 0) { Creature_Effect(item, &m_YetiBiteL, Spawn_Blood); } if ((item->touch_bits & M_TOUCH_BITS_R) != 0) { Creature_Effect(item, &m_YetiBiteR, Spawn_Blood); } Lara_TakeDamage(M_CHARGE_DAMAGE, true); creature->flags = 1; } break; default: break; } } else if (item->current_anim_state != M_STATE_DEATH) { Item_SwitchToAnim(item, M_ANIM_DEATH, 0); item->current_anim_state = M_STATE_DEATH; } if (lara_was_alive && lara_item->hit_points <= 0) { Creature_SpecialKill( item, M_ANIM_KILL, M_STATE_KILL, LS_EXTRA_YETI_KILL); return; } Creature_Head(item, body); Creature_Neck(item, head); if (item->current_anim_state >= M_STATE_CLIMB_1) { Creature_Animate(item_num, angle, 0); } else { switch (Creature_Vault(item_num, angle, 2, M_VAULT_SHIFT)) { case -4: Item_SwitchToAnim(item, M_ANIM_FALL, 0); item->current_anim_state = M_STATE_FALL; break; case 2: Item_SwitchToAnim(item, M_ANIM_CLIMB_1, 0); item->current_anim_state = M_STATE_CLIMB_1; break; case 3: Item_SwitchToAnim(item, M_ANIM_CLIMB_2, 0); item->current_anim_state = M_STATE_CLIMB_2; break; case 4: Item_SwitchToAnim(item, M_ANIM_CLIMB_3, 0); item->current_anim_state = M_STATE_CLIMB_3; break; default: return; } } } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->hit_points = M_HITPOINTS; obj->radius = M_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->pivot_length = 100; obj->lot_setup = LOT_Setup(LOT_SETUP_CLIMBER); obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 6)->rot.y = true; Object_GetBone(obj, 14)->rot.y = true; } REGISTER_OBJECT(O_YETI, M_Setup) ================================================ FILE: src/trx/game/objects/draw.c ================================================ #include #include #include #include #include #include #include #include #include #include static BOUNDS_16 M_GetBoundingBox( const OBJECT *const obj, const ANIM_FRAME *const frame, const uint32_t mesh_bits) { const XYZ_16 *const mesh_rots = frame != nullptr ? frame->mesh_rots : nullptr; Matrix_PushUnit(); if (frame != nullptr) { Matrix_TranslateRel16(frame->offset); } if (mesh_rots != nullptr) { Matrix_Rot16(mesh_rots[0]); } BOUNDS_16 new_bounds = { .min.x = 0x7FFF, .min.y = 0x7FFF, .min.z = 0x7FFF, .max.x = -0x7FFF, .max.y = -0x7FFF, .max.z = -0x7FFF, }; for (int32_t mesh_idx = 0; mesh_idx < obj->mesh_count; mesh_idx++) { if (mesh_idx != 0) { const ANIM_BONE *const bone = Object_GetBone(obj, mesh_idx - 1); if (bone->matrix_pop) { Matrix_Pop(); } if (bone->matrix_push) { Matrix_Push(); } Matrix_TranslateRel32(bone->pos); if (mesh_rots != nullptr) { Matrix_Rot16(mesh_rots[mesh_idx]); } } if (!(mesh_bits & (1 << mesh_idx))) { continue; } const OBJECT_MESH *const mesh = Object_GetMesh(obj->mesh_idx + mesh_idx); for (int32_t i = 0; i < mesh->num_vertices; i++) { // clang-format off const XYZ_16 *const vertex = &mesh->vertices[i]; const MATRIX *const mptr = g_MatrixPtr; const double xv = ( mptr->_00 * vertex->x + mptr->_01 * vertex->y + mptr->_02 * vertex->z + mptr->_03 ); const double yv = ( mptr->_10 * vertex->x + mptr->_11 * vertex->y + mptr->_12 * vertex->z + mptr->_13 ); double zv = ( mptr->_20 * vertex->x + mptr->_21 * vertex->y + mptr->_22 * vertex->z + mptr->_23 ); // clang-format on const int32_t x = ((int32_t)xv) >> W2V_SHIFT; const int32_t y = ((int32_t)yv) >> W2V_SHIFT; const int32_t z = ((int32_t)zv) >> W2V_SHIFT; new_bounds.min.x = MIN(new_bounds.min.x, x); new_bounds.min.y = MIN(new_bounds.min.y, y); new_bounds.min.z = MIN(new_bounds.min.z, z); new_bounds.max.x = MAX(new_bounds.max.x, x); new_bounds.max.y = MAX(new_bounds.max.y, y); new_bounds.max.z = MAX(new_bounds.max.z, z); } } Matrix_Pop(); return new_bounds; } bool Object_DrawUnclippedItem(const ITEM *const item) { const int32_t left = g_PhdLeft; const int32_t top = g_PhdTop; const int32_t right = g_PhdRight; const int32_t bottom = g_PhdBottom; g_PhdLeft = Viewport_GetMinX(VIEWPORT_GAME); g_PhdTop = Viewport_GetMinY(VIEWPORT_GAME); g_PhdRight = Viewport_GetMaxX(VIEWPORT_GAME); g_PhdBottom = Viewport_GetMaxY(VIEWPORT_GAME); Object_DrawAnimatingItem(item); g_PhdLeft = left; g_PhdTop = top; g_PhdRight = right; g_PhdBottom = bottom; return true; } void Object_DrawMesh( const int32_t mesh_idx, const CLIP clip, const bool interpolated) { const OBJECT_MESH *const mesh = Object_GetMesh(mesh_idx); if (interpolated) { Output_DrawObjectMesh_I(mesh, clip); } else { Output_DrawObjectMesh(mesh, clip); } } void Object_DrawStaticObject( const OBJECT *const obj, const ANIM_FRAME *const frame) { Matrix_Push(); Object_DrawMesh(obj->mesh_idx, 0, false); for (int32_t i = 1; i < obj->mesh_count; i++) { const ANIM_BONE *const bone = Object_GetBone(obj, i - 1); if (bone->matrix_pop) { Matrix_Pop(); } if (bone->matrix_push) { Matrix_Push(); } Matrix_TranslateRel32(bone->pos); Matrix_Rot16(frame->mesh_rots[i]); Object_DrawMesh(obj->mesh_idx + i, 0, false); } Matrix_Pop(); } bool Object_DrawAnimatingItem(const ITEM *item) { return Object_DrawAnimatingItemWithSwap(item, nullptr); } bool Object_DrawAnimatingItemWithSwap( const ITEM *const item, const OBJECT *const mesh_swap) { ANIM_FRAME *frames[2]; int32_t rate; const int32_t frac = Item_GetFrames(item, frames, &rate); const OBJECT *const obj = Object_Get(item->object_id); const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item); const OBJECT *swap_obj = mesh_swap; if (swap_obj != nullptr && !swap_obj->loaded) { swap_obj = nullptr; } if (swap_obj != nullptr) { ASSERT(swap_obj->mesh_count == obj->mesh_count); } if (obj->shadow_size != 0) { Output_DrawShadow(obj->shadow_size, bounds, item); } Matrix_Push(); Matrix_TranslateAbs32(item->interp.result.pos); Matrix_Rot16(item->interp.result.rot); const CLIP clip = Output_CheckBoundsClip(bounds); if (clip == CLIP_NOT_VISIBLE) { Matrix_Pop(); return false; } Output_CalculateObjectLighting(item, bounds); const int16_t *extra_rotation = item->extra_rotations; bool result = Object_DrawInterpolatedObjectWithSwap( obj, item->mesh_bits, extra_rotation, frames[0], frames[1], frac, rate, swap_obj); if (g_Config.debug.enable_debug_bounding_boxes) { Output_DrawCuboid(bounds); } Matrix_Pop(); return result; } bool Object_DrawInterpolatedObject( const OBJECT *const obj, const uint32_t mesh_mask, const int16_t *extra_rotation, const ANIM_FRAME *const frame1, const ANIM_FRAME *const frame2, const int32_t frac, const int32_t rate) { return Object_DrawInterpolatedObjectWithSwap( obj, mesh_mask, extra_rotation, frame1, frame2, frac, rate, nullptr); } bool Object_DrawInterpolatedObjectWithSwap( const OBJECT *const obj, const uint32_t mesh_mask, const int16_t *extra_rotation, const ANIM_FRAME *const frame1, const ANIM_FRAME *const frame2, const int32_t frac, const int32_t rate, const OBJECT *const mesh_swap) { if (frame1 == nullptr) { return false; } ASSERT(frame1 != nullptr); const CLIP clip = Output_CheckBoundsClip(&frame1->bounds); if (clip == CLIP_NOT_VISIBLE) { return false; } ASSERT(rate != 0); Matrix_Push(); if (frac != 0) { for (int32_t mesh_idx = 0; mesh_idx < obj->mesh_count; mesh_idx++) { if (mesh_idx == 0) { Matrix_InitInterpolate(frac, rate); Matrix_TranslateRel16_ID(frame1->offset, frame2->offset); Matrix_Rot16_ID( frame1->mesh_rots[mesh_idx], frame2->mesh_rots[mesh_idx]); Object_ApplyExtraRotation(&extra_rotation, obj->base_rot, true); } else { const ANIM_BONE *const bone = Object_GetBone(obj, mesh_idx - 1); if (bone->matrix_pop) { Matrix_Pop_I(); } if (bone->matrix_push) { Matrix_Push_I(); } Matrix_TranslateRel32_I(bone->pos); Matrix_Rot16_ID( frame1->mesh_rots[mesh_idx], frame2->mesh_rots[mesh_idx]); Object_ApplyExtraRotation(&extra_rotation, bone->rot, true); } if ((mesh_mask & (1u << mesh_idx)) != 0) { Object_DrawMesh(obj->mesh_idx + mesh_idx, clip, true); } else if (mesh_swap != nullptr) { Object_DrawMesh(mesh_swap->mesh_idx + mesh_idx, clip, true); } } } else { for (int32_t mesh_idx = 0; mesh_idx < obj->mesh_count; mesh_idx++) { if (mesh_idx == 0) { Matrix_TranslateRel16(frame1->offset); Matrix_Rot16(frame1->mesh_rots[mesh_idx]); Object_ApplyExtraRotation( &extra_rotation, obj->base_rot, false); } else { const ANIM_BONE *const bone = Object_GetBone(obj, mesh_idx - 1); if (bone->matrix_pop) { Matrix_Pop(); } if (bone->matrix_push) { Matrix_Push(); } Matrix_TranslateRel32(bone->pos); Matrix_Rot16(frame1->mesh_rots[mesh_idx]); Object_ApplyExtraRotation(&extra_rotation, bone->rot, false); } if ((mesh_mask & (1u << mesh_idx)) != 0) { Object_DrawMesh(obj->mesh_idx + mesh_idx, clip, false); } else if (mesh_swap != nullptr) { Object_DrawMesh(mesh_swap->mesh_idx + mesh_idx, clip, false); } } } Matrix_Pop(); return true; } void Object_ApplyExtraRotation( const int16_t **const extra_rotation, const XYZ_BOOL rot_flags, const bool interpolated) { const int16_t *rot_ptr = *extra_rotation; if (rot_ptr == nullptr) { return; } #define APPLY_ROTATION(axis_, flag_) \ if (rot_flags.flag_) { \ if (interpolated) { \ Matrix_Rot##axis_##_I(*rot_ptr++); \ } else { \ Matrix_Rot##axis_(*rot_ptr++); \ } \ } APPLY_ROTATION(Y, y); APPLY_ROTATION(X, x); APPLY_ROTATION(Z, z); #undef APPLY_ROTATION *extra_rotation = rot_ptr; } bool Object_DrawSpriteItem(const ITEM *const item) { const RGB_F tint = Output_GetTint(); SHADE shade = item->shade; if (shade.value_1 < 0) { shade.value_1 = SHADE_NEUTRAL; } if (g_TRVersion > 1) { Output_CalculateStaticMeshLight( item->interp.result.pos, shade, Room_Get(item->room_num)); shade.value_1 = Output_GetLightAdder() + SHADE_NEUTRAL; } const OBJECT *const obj = Object_Get(item->object_id); Output_DrawSprite( item->interp.result.pos.x, item->interp.result.pos.y, item->interp.result.pos.z, obj->mesh_idx - item->frame_num, shade.value_1, tint, DRAW_BLEND); return true; } bool Object_DrawPickupItem(const ITEM *const item) { if ((item->flags & IF_INVISIBLE) != 0) { return false; } if (!g_Config.visuals.enable_3d_pickups && Object_Get(item->object_id)->loaded) { return Object_DrawSpriteItem(item); } // Convert item to menu display item. const OBJECT *obj = Object_TryGet(Inv_GetItemPickup(item->object_id)); if (obj == nullptr || !obj->loaded || obj->mesh_count < 0) { obj = Object_TryGet(Inv_GetItemOption(item->object_id)); } if (obj == nullptr || !obj->loaded || obj->mesh_count < 0) { return Object_DrawSpriteItem(item); } // Standardize the bounds and offsets of all pickup items, and handle cases // such as the prayer wheels in Barkhang Monastery, which have no frames. const BOUNDS_16 bounds = M_GetBoundingBox(obj, nullptr, item->mesh_bits); XYZ_16 offset = {}; const XYZ_16 *mesh_rots = nullptr; if (obj->anim_idx != NO_ANIM) { const ANIM_FRAME *const frame = obj->frame_base; mesh_rots = frame->mesh_rots; offset = frame->offset; if (Object_IsType(item->object_id, g_ElevatedPickupObjects)) { offset.y = (frame->bounds.min.y - frame->offset.y) / 2; } else { offset.y -= frame->bounds.max.y; } } else { offset.y = (bounds.max.y - bounds.min.y) / -2; } Matrix_Push(); Matrix_TranslateAbs32(item->interp.result.pos); Matrix_Rot16(item->interp.result.rot); Matrix_TranslateRel16(offset); Output_CalculateLight(item->pos, item->room_num); const CLIP clip = Output_CheckBoundsClip(&bounds); if (clip != CLIP_NOT_VISIBLE) { for (int32_t mesh_idx = 0; mesh_idx < obj->mesh_count; mesh_idx++) { if (mesh_idx > 0) { const ANIM_BONE *const bone = Object_GetBone(obj, mesh_idx - 1); if (bone->matrix_pop) { Matrix_Pop(); } if (bone->matrix_push) { Matrix_Push(); } Matrix_TranslateRel32(bone->pos); } if (mesh_rots != nullptr) { Matrix_Rot16(mesh_rots[mesh_idx]); } if ((item->mesh_bits & (1 << mesh_idx)) != 0) { Object_DrawMesh(obj->mesh_idx + mesh_idx, clip, false); } } } Matrix_Pop(); return true; } ================================================ FILE: src/trx/game/objects/draw.h ================================================ #pragma once #include #include #include #include #include #include bool Object_DrawUnclippedItem(const ITEM *item); void Object_DrawMesh(int32_t mesh_idx, CLIP clip, bool interpolated); bool Object_DrawSpriteItem(const ITEM *item); bool Object_DrawPickupItem(const ITEM *item); void Object_DrawStaticObject(const OBJECT *obj, const ANIM_FRAME *frame); bool Object_DrawAnimatingItem(const ITEM *item); bool Object_DrawAnimatingItemWithSwap( const ITEM *item, const OBJECT *mesh_swap); bool Object_DrawInterpolatedObject( const OBJECT *obj, uint32_t mesh_mask, const int16_t *extra_rotation, const ANIM_FRAME *frame1, const ANIM_FRAME *frame2, int32_t frac, int32_t rate); bool Object_DrawInterpolatedObjectWithSwap( const OBJECT *obj, uint32_t mesh_mask, const int16_t *extra_rotation, const ANIM_FRAME *frame1, const ANIM_FRAME *frame2, int32_t frac, int32_t rate, const OBJECT *mesh_swap); void Object_ApplyExtraRotation( const int16_t **extra_rotation, const XYZ_BOOL rot_flags, bool interpolated); ================================================ FILE: src/trx/game/objects/effects/blood.c ================================================ #include #include #include #include #include static void M_Control(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); const OBJECT *const obj = Object_Get(effect->object_id); effect->pos.x += (effect->speed * Math_Sin(effect->rot.y)) >> W2V_SHIFT; effect->pos.z += (effect->speed * Math_Cos(effect->rot.y)) >> W2V_SHIFT; effect->counter++; if (effect->counter == 4) { effect->frame_num--; effect->counter = 0; if (effect->frame_num <= obj->mesh_count) { Effect_Kill(effect_num); } } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->semi_transparent = g_TRVersion >= 2; } REGISTER_OBJECT(O_BLOOD, M_Setup) REGISTER_OBJECT(O_BLOOD_PINK, M_Setup) ================================================ FILE: src/trx/game/objects/effects/body_part.c ================================================ #include #include #include #include #include #include #include static void M_SpawnSplash(const GAME_VECTOR pos) { const int16_t effect_num = Effect_Create(pos.room_num); if (effect_num != NO_EFFECT) { EFFECT *const effect = Effect_Get(effect_num); effect->pos = pos.pos; effect->rot.y = 0; effect->speed = 0; effect->frame_num = 0; effect->object_id = O_SPLASH_1; } } static void M_Control_TR12(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); effect->rot.x += 5 * DEG_1; effect->rot.z += 10 * DEG_1; effect->pos.x += (effect->speed * Math_Sin(effect->rot.y)) >> W2V_SHIFT; effect->pos.z += (effect->speed * Math_Cos(effect->rot.y)) >> W2V_SHIFT; effect->pos.y += effect->fall_speed; effect->fall_speed += GRAVITY; int16_t room_num = effect->room_num; const SECTOR *const sector = Room_GetSector(effect->pos, &room_num); const ROOM *const current_room = Room_Get(effect->room_num); const ROOM *const next_room = Room_Get(room_num); if (!current_room->flags.underwater && next_room->flags.underwater) { M_SpawnSplash( (GAME_VECTOR) { .pos = effect->pos, .room_num = effect->room_num }); } const int32_t ceiling = Room_GetCeiling(sector, effect->pos); if (effect->pos.y < ceiling) { effect->pos.y = ceiling; effect->fall_speed = -effect->fall_speed; } const int32_t height = Room_GetHeight(sector, effect->pos); if (effect->pos.y >= height) { if (effect->counter > 0) { effect->speed = 0; effect->frame_num = 0; effect->counter = 0; effect->object_id = O_EXPLOSION_1; effect->shade = SHADE_NEUTRAL; Sound_Effect(SFX_EXPLOSION_1, &effect->pos, SPM_NORMAL); } else { Effect_Kill(effect_num); } return; } const int16_t counter_value = (g_TRVersion == 1) ? ABS(effect->counter) : effect->counter; const bool trigger_explosion = (g_TRVersion == 1) ? (effect->counter > 0) : (effect->counter == 0); if (Lara_IsNearItem(&effect->pos, counter_value * 2)) { Lara_TakeDamage(counter_value, true); if (trigger_explosion) { effect->speed = 0; effect->frame_num = 0; effect->counter = 0; effect->object_id = O_EXPLOSION_1; effect->shade = SHADE_NEUTRAL; Sound_Effect(SFX_EXPLOSION_1, &effect->pos, SPM_NORMAL); LARA_INFO *const lara = Lara_GetLaraInfo(); lara->hit_effect_count = 5; lara->hit_effect = effect; } else { Effect_Kill(effect_num); } } if (room_num != effect->room_num) { Effect_UpdateRoom(effect_num, room_num); } } static void M_Control_TR3(const int16_t effect_num) { int32_t lp; EFFECT *const effect = Effect_Get(effect_num); effect->rot.x += 5 * DEG_1; effect->rot.z += 10 * DEG_1; effect->fall_speed += 3; effect->pos.x += (effect->speed * Math_Sin(effect->rot.y)) >> (W2V_SHIFT + 2); effect->pos.y += effect->fall_speed; effect->pos.z += (effect->speed * Math_Cos(effect->rot.y)) >> (W2V_SHIFT + 2); const int32_t time4 = (int32_t)Output_GetTimeInGame() * 4; if (!(time4 & 0xC)) { if (effect->counter & 1) { Sparks_TriggerFireFlame(effect->pos, effect_num, 0); } if (effect->counter & 2) { Sparks_TriggerFireSmoke(effect->pos, -1, 0); } } int16_t room_num = effect->room_num; SECTOR *const sector = Room_GetSector(effect->pos, &room_num); int32_t c = Room_GetCeiling(sector, effect->pos); if (effect->pos.y < c) { effect->pos.y = c; effect->fall_speed = -effect->fall_speed; } int32_t h = Room_GetHeight(sector, effect->pos); if (effect->pos.y >= h) { if (effect->counter & 3) { for (int32_t i = 0; i < 3; i++) { if (effect->counter & 1) { Sparks_TriggerFireFlame( (XYZ_32) { effect->pos.x, h, effect->pos.z }, -1, 0); } if (effect->counter & 2) { Sparks_TriggerFireSmoke( (XYZ_32) { effect->pos.x, h, effect->pos.z }, -1, 0); } } Sound_Effect(SFX_EXPLOSION_1, &effect->pos, SPM_NORMAL); } Effect_Kill(effect_num); return; } if (Lara_IsNearItem(&effect->pos, effect->counter & ~3)) { Lara_TakeDamage(effect->counter >> 2, true); if (effect->counter & 3) { for (int32_t i = 0; i < 3; i++) { if (effect->counter & 1) { Sparks_TriggerFireFlame( (XYZ_32) { effect->pos.x, h, effect->pos.z }, -1, 0); } if (effect->counter & 2) { Sparks_TriggerFireSmoke( (XYZ_32) { effect->pos.x, h, effect->pos.z }, -1, 0); } } Sound_Effect(SFX_EXPLOSION_1, &effect->pos, SPM_NORMAL); } Effect_Kill(effect_num); } if (effect->room_num != room_num) { Effect_UpdateRoom(effect_num, room_num); } } static void M_Setup(OBJECT *const obj) { obj->control_func = g_TRVersion == 3 ? M_Control_TR3 : M_Control_TR12; obj->loaded = true; obj->mesh_count = 0; } REGISTER_OBJECT(O_BODY_PART, M_Setup) ================================================ FILE: src/trx/game/objects/effects/bubble.c ================================================ #include #include #include #include #include #include #include static void M_Control_TR1TR2(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); effect->rot.y += 9 * DEG_1; effect->rot.x += 13 * DEG_1; const XYZ_32 pos = { .x = effect->pos.x + ((Math_Sin(effect->rot.y) * 11) >> W2V_SHIFT), .y = effect->pos.y - effect->speed, .z = effect->pos.z + ((Math_Cos(effect->rot.x) * 8) >> W2V_SHIFT), }; int16_t room_num = effect->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); if (sector == nullptr || !Room_Get(room_num)->flags.underwater) { Effect_Kill(effect_num); return; } const int32_t ceiling = Room_GetCeiling(sector, pos); if (ceiling == NO_HEIGHT || pos.y <= ceiling) { Effect_Kill(effect_num); return; } if (effect->room_num != room_num) { Effect_UpdateRoom(effect_num, room_num); } effect->pos = pos; } static void M_Control_TR3(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); effect->rot.y += 9 * DEG_1; effect->rot.x += 13 * DEG_1; effect->speed += effect->fall_speed; const XYZ_32 pos = { .x = effect->pos.x + ((3 * Math_Sin(effect->rot.y)) >> W2V_SHIFT), .y = effect->pos.y - ((int32_t)effect->speed >> 8), .z = effect->pos.z + (Math_Cos(effect->rot.x) >> W2V_SHIFT), }; int16_t room_num = effect->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); if (sector == nullptr) { Effect_Kill(effect_num); return; } const int32_t floor = Room_GetHeight(sector, pos); if (pos.y > floor) { Effect_Kill(effect_num); return; } if (!Room_Get(room_num)->flags.underwater) { const ROOM *const old_room = Room_Get(effect->room_num); FX_Water_SetupRipple( effect->pos.x, old_room->max_ceiling, effect->pos.z, -2 - (Random_GetControl() & 1), true); Effect_Kill(effect_num); return; } const int32_t ceiling = Room_GetCeiling(sector, pos); if (ceiling == NO_HEIGHT || pos.y <= ceiling) { Effect_Kill(effect_num); return; } if (effect->room_num != room_num) { Effect_UpdateRoom(effect_num, room_num); } effect->pos = pos; } static void M_Setup(OBJECT *const obj) { obj->control_func = g_TRVersion == 3 ? M_Control_TR3 : M_Control_TR1TR2; if (obj->loaded) { for (int32_t i = 0; i < -obj->mesh_count; i++) { Output_GetSpriteTexture(obj->mesh_idx + i)->flags = VERT_ABS_SPRITE; } } } REGISTER_OBJECT(O_BUBBLE_1, M_Setup) ================================================ FILE: src/trx/game/objects/effects/dart_effect.c ================================================ #include #include #include static void M_Control(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); const OBJECT *const obj = Object_Get(effect->object_id); effect->counter++; if (effect->counter >= 3) { effect->frame_num--; effect->counter = 0; if (effect->frame_num <= obj->mesh_count) { Effect_Kill(effect_num); } } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->draw_func = Object_DrawSpriteItem; obj->semi_transparent = g_TRVersion >= 2; } REGISTER_OBJECT(O_DART_EFFECT, M_Setup) ================================================ FILE: src/trx/game/objects/effects/ember.c ================================================ #include #include #include #include #define M_RANGE 200 #define M_DAMAGE 10 static void M_Control(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); effect->fall_speed += GRAVITY; effect->pos.z += (effect->speed * Math_Cos(effect->rot.y)) >> W2V_SHIFT; effect->pos.x += (effect->speed * Math_Sin(effect->rot.y)) >> W2V_SHIFT; effect->pos.y += effect->fall_speed; int16_t room_num = effect->room_num; const SECTOR *const sector = Room_GetSector(effect->pos, &room_num); const int32_t ceiling = Room_GetCeiling(sector, effect->pos); const int32_t height = Room_GetHeight(sector, effect->pos); if (effect->pos.y >= height || effect->pos.y < ceiling) { Effect_Kill(effect_num); } else if (Lara_IsNearItem(&effect->pos, M_RANGE)) { Lara_TakeDamage(M_DAMAGE, true); Effect_Kill(effect_num); } else if (room_num != effect->room_num) { Effect_UpdateRoom(effect_num, room_num); } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->semi_transparent = g_TRVersion >= 2; } REGISTER_OBJECT(O_EMBER, M_Setup) ================================================ FILE: src/trx/game/objects/effects/explosion.c ================================================ #include #include #include #include #include static void M_Control(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); const OBJECT *const obj = Object_Get(effect->object_id); effect->counter++; if (effect->counter == 2) { effect->frame_num--; effect->counter = 0; if (g_Config.visuals.enable_gun_lighting && effect->frame_num > obj->mesh_count) { Output_AddDynamicLight(effect->pos, 13, 11); } else if (effect->frame_num <= obj->mesh_count) { Effect_Kill(effect_num); } } else if (g_Config.visuals.enable_gun_lighting) { Output_AddDynamicLight(effect->pos, 12, 10); } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->semi_transparent = g_TRVersion >= 2; } REGISTER_OBJECT(O_EXPLOSION_1, M_Setup) ================================================ FILE: src/trx/game/objects/effects/flame.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #define M_LIGHT_INTENSITY 11 #define M_LIGHT_FALLOFF 10 #define M_DAMAGE_PROXIMITY 600 #define M_IGNITE_PROXIMITY (g_TRVersion == 1 ? 300 : 450) #define M_TOO_NEAR_DAMAGE (g_TRVersion == 1 ? 3 : 5) #define M_ON_FIRE_DAMAGE (g_TRVersion == 1 ? 5 : 7) static const uint8_t m_TR3_XZOffsets[16][2] = { { 9, 9 }, { 24, 9 }, { 40, 9 }, { 55, 9 }, { 9, 24 }, { 24, 24 }, { 40, 24 }, { 55, 24 }, { 9, 40 }, { 24, 40 }, { 40, 40 }, { 55, 40 }, { 9, 55 }, { 24, 55 }, { 40, 55 }, { 55, 55 }, }; static bool M_IsGreenAttachedFlame(const EFFECT *const effect) { return effect->counter < 0 && effect->frame_num == FLAME_GREEN; } static void M_TR3_SideFlameDetection( const EFFECT *const effect, const int32_t length) { ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - effect->pos.x; const int32_t dz = lara_item->pos.z - effect->pos.z; const int32_t max_dist = 20 * WALL_L; if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) { return; } const int32_t half_w = STEP_L; const int32_t offset = WALL_L / 2; int32_t origin_x = effect->pos.x; int32_t origin_z = effect->pos.z; int32_t xs = 0, xe = 0, zs = 0, ze = 0; switch (effect->rot.y) { case 0: origin_z += offset; xs = -half_w; xe = half_w; zs = -length; ze = 0; break; case DEG_90: origin_x += offset; xs = -length; xe = 0; zs = -half_w; ze = half_w; break; case -DEG_90: origin_x -= offset; xs = 0; xe = length; zs = -half_w; ze = half_w; break; case -DEG_180: origin_z -= offset; xs = -half_w; xe = half_w; zs = 0; ze = length; break; default: return; } const int32_t min_x = origin_x + xs; const int32_t max_x = origin_x + xe; const int32_t min_z = origin_z + zs; const int32_t max_z = origin_z + ze; const BOUNDS_16 *const b = Item_GetBoundsAccurate(lara_item); const int32_t lara_min_y = lara_item->pos.y + b->min.y; const int32_t lara_max_y = lara_item->pos.y + b->max.y; const int32_t fire_min_y = effect->pos.y - 384; const int32_t fire_max_y = effect->pos.y + 128; if (lara_item->pos.x < min_x || lara_item->pos.x > max_x || lara_item->pos.z < min_z || lara_item->pos.z > max_z || lara_min_y > fire_max_y || lara_max_y < fire_min_y) { return; } if (effect->flag1 >= 18) { Lara_CatchFire(); } else { Lara_TakeDamage(M_TOO_NEAR_DAMAGE, true); } } static inline XYZ_32 M_OffsetPos( const XYZ_32 base_pos, const int32_t dx, const int32_t dy, const int32_t dz) { return (XYZ_32) { .x = base_pos.x + dx, .y = base_pos.y + dy, .z = base_pos.z + dz, }; } static void M_TR3_ControlBig( EFFECT *const effect, const int32_t time4, const int32_t rnd) { if ((time4 & 0xC) == 0) { Sparks_TriggerFireFlame(effect->pos, -1, 0); Sparks_TriggerFireSmoke(effect->pos, -1, 0); } Sparks_TriggerStaticFlame(effect->pos, (Random_GetControl() & 0xF) + 96); } static void M_TR3_ControlSmall( EFFECT *const effect, const int32_t time4, const int32_t rnd) { if (effect->counter >= 0) { const int32_t angle = ((effect->rot.y >> 4) & 4095) << 1; const int32_t s = (288 * Math_Sin(angle << 3)) >> W2V_SHIFT; const int32_t c = (288 * Math_Cos(angle << 3)) >> W2V_SHIFT; Sparks_TriggerStaticFlame( M_OffsetPos(effect->pos, s, -192, c), (Random_GetControl() & 15) + 32); if ((time4 & 0x18) != 0) { return; } Sparks_TriggerFireFlame(M_OffsetPos(effect->pos, s, -224, c), -1, 1); if ((time4 & 0x18) == 0) { Sparks_TriggerFireSmoke(M_OffsetPos(effect->pos, s, 0, c), -1, 1); } return; } const ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara = Lara_GetLaraInfo(); // Lara is on fire; attach flames to joints and apply damage unless // water extinguishes it. if (lara->water_status == LWS_CHEAT) { effect->counter = 0; Effect_Kill(Effect_GetIndex(effect)); lara->burn = 0; return; } const bool is_green = M_IsGreenAttachedFlame(effect); const int32_t flame_type = is_green ? 254 : 255; for (int i = 0; i < LM_NUMBER_OF; i++) { if ((time4 & 0xC) == 0) { effect->pos.x = 0; effect->pos.y = 0; effect->pos.z = 0; Collide_GetJointAbsPosition(lara_item, &effect->pos, i); Sparks_TriggerFireFlame(effect->pos, -1, flame_type); } } if (g_Config.visuals.enable_fire_lighting) { RGB_888 color; if (is_green) { color.r = (rnd >> 2) & 0x3F; color.g = (rnd & 0x3F) + 192; color.b = ((rnd >> 4) & 0x1F) + 96; } else { color.r = (rnd & 0x3F) + 192; color.g = ((rnd >> 4) & 0x1F) + 96; color.b = 0; } Output_AddDynamicLightRGB(lara_item->pos, 13, color); } if (lara_item->room_num != effect->room_num) { Effect_UpdateRoom(Effect_GetIndex(effect), lara_item->room_num); } const int32_t wh = Room_GetWaterHeight(effect->pos, effect->room_num); if (wh == NO_HEIGHT || effect->pos.y <= wh || (Room_Get(effect->room_num)->flags.swamp && (GF_BadGetLevelNum() == 4 || GF_BadGetLevelNum() == 18 || GF_BadGetLevelNum() == 19))) { Sound_Effect(SFX_LOOP_FOR_SMALL_FIRES, &effect->pos, SPM_NORMAL); Lara_TakeDamage(M_ON_FIRE_DAMAGE, true); } else { effect->counter = 0; Effect_Kill(Effect_GetIndex(effect)); lara->burn = false; } } static void M_TR3_ControlJet( EFFECT *const effect, const int32_t time4, const int32_t rnd) { // Jets cycle between two spawn positions, chosen from a 16-cell grid. if (effect->flag1 != 0) { effect->flag1--; } else { effect->flag1 = (Random_GetControl() & 3) + 8; int32_t new_index = Random_GetControl() & 0x3F; if (effect->flag2 == new_index) { new_index = (new_index + 13) & 0x3F; } effect->flag2 = (int16_t)new_index; } const int32_t idx0 = effect->flag2 & 7; const int32_t idx1 = (effect->flag2 >> 3) + 8; const int32_t x0 = (m_TR3_XZOffsets[idx0][0] << 4) - 512; const int32_t z0 = (m_TR3_XZOffsets[idx0][1] << 4) - 512; const int32_t x1 = (m_TR3_XZOffsets[idx1][0] << 4) - 512; const int32_t z1 = (m_TR3_XZOffsets[idx1][1] << 4) - 512; if ((time4 & 4) == 0) { Sparks_TriggerFireFlame(M_OffsetPos(effect->pos, x0, 0, z0), -1, 2); } else { Sparks_TriggerFireFlame(M_OffsetPos(effect->pos, x1, 0, z1), -1, 2); } } static void M_TR3_ControlSide( EFFECT *const effect, const int32_t time4, const int32_t rnd) { // Side flames: alternate between direction and length phases. const int32_t dist = (rnd & 0xFF) + 512; const int32_t angle = (effect->rot.y >> 3) & 0x1FFE; const int32_t s = (dist * Math_Sin(angle << 3)) >> W2V_SHIFT; const int32_t c = (dist * Math_Cos(angle << 3)) >> W2V_SHIFT; if (effect->flag2 != 0) { if ((time4 & 4) != 0) { Sparks_TriggerSideFlame( M_OffsetPos(effect->pos, s, 0, c), ((angle - 4096) & 0x1FFF) << 3, (!(Random_GetControl() & 7)) ? 1 : 0, 1); } effect->flag2--; } else { if (effect->flag1 != 0) { if ((time4 & 4) != 0) { int32_t size = 9; if (effect->flag1 > 112) { size = (129 - effect->flag1) >> 1; } else if (effect->flag1 < 18) { size = (effect->flag1 >> 1) + 1; } Sparks_TriggerSideFlame( M_OffsetPos(effect->pos, s, 0, c), ((angle + 4096) & 0x1FFE) << 3, size, 0); } effect->flag1 -= 2; } else { effect->flag1 = 128; // TODO: do not hardcode this if (GF_BadGetLevelNum() == 7) { effect->flag2 = 120; } else { effect->flag2 = 60; } } } } static void M_TR3_Control(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); const int32_t rnd = Random_GetControl(); const int32_t time4 = Output_GetTimeInGame() * 4; // Animation & particle logic. if (effect->frame_num == FLAME_BIG) { M_TR3_ControlBig(effect, time4, rnd); } else if ( effect->frame_num == FLAME_SMALL || effect->frame_num == FLAME_GREEN) { M_TR3_ControlSmall(effect, time4, rnd); } else if (effect->frame_num == FLAME_JET) { M_TR3_ControlJet(effect, time4, rnd); } else { M_TR3_ControlSide(effect, time4, rnd); } // Light & proximity damage logic (applies to most flame types). const XYZ_32 light_pos = { .x = effect->pos.x + ((rnd & 0xF) << 5), .y = effect->pos.y + ((rnd & 0xF0) << 1), .z = effect->pos.z + ((rnd >> 3) & 0x1E0), }; RGB_888 color = { .r = (rnd & 0x3F) + 192, .g = ((rnd >> 4) & 0x1F) + 96, .b = 0, }; int32_t dist = 0; if (effect->frame_num == FLAME_SIDE) { if (effect->flag2 != 0) { dist = 0; } else if (effect->flag1 < 18) { dist = 2048; } else if (effect->flag1 < 64) { dist = 2048; } else { dist = (128 - effect->flag1) << 5; } } if (g_Config.visuals.enable_fire_lighting) { if (effect->frame_num == FLAME_SIDE) { const int16_t angle = ((((effect->rot.y >> 3) & 0xFFFE) - 4096) & 0x1FFE) << 3; Output_AddDynamicLightRGB( XYZ_32_OffsetYaw(light_pos, angle, dist), (effect->flag2 != 0) ? 6 : 13, color); } else { Output_AddDynamicLightRGB( light_pos, 16 - (effect->frame_num << 2), color); } } Sound_Effect(SFX_LOOP_FOR_SMALL_FIRES, &effect->pos, SPM_NORMAL); const ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara = Lara_GetLaraInfo(); if (effect->counter != 0) { effect->counter--; } else if (effect->frame_num == FLAME_SIDE) { if (!lara->burn && dist) { M_TR3_SideFlameDetection(effect, dist); } } else if ( effect->frame_num != FLAME_SMALL && effect->frame_num != FLAME_GREEN) { if (Lara_IsNearItem(&effect->pos, M_DAMAGE_PROXIMITY)) { const XYZ_32 delta = { .x = lara_item->pos.x - effect->pos.x, .y = 0, .z = lara_item->pos.z - effect->pos.z, }; dist = XYZ_32_GetLength2(delta); Lara_TakeDamage(M_TOO_NEAR_DAMAGE, true); if (dist < 202500) { effect->counter = 100; Lara_CatchFire(); } } } } static void M_TR12_DoEffects(const EFFECT *const effect) { if (!Object_Get(O_FLAME)->loaded) { return; } Sound_Effect(SFX_LOOP_FOR_SMALL_FIRES, &effect->pos, SPM_NORMAL); if (!g_Config.visuals.enable_fire_lighting) { return; } const int32_t random = Random_GetControl(); const XYZ_32 light_pos = { .x = effect->pos.x + (random & 0x140) - 0xA0, .y = effect->pos.y - STEP_L - (random & 0x50), .z = effect->pos.z + (random & 0x140) - 0xA0, }; if (random > 0x4000) { Output_AddDynamicLight(light_pos, M_LIGHT_INTENSITY, M_LIGHT_FALLOFF); } else if (random > 0x2000) { Output_AddDynamicLight( light_pos, M_LIGHT_INTENSITY - (random & 2), M_LIGHT_FALLOFF); } else { Output_AddDynamicLight( light_pos, M_LIGHT_INTENSITY, M_LIGHT_FALLOFF / 2); } } static void M_TR12_Control(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); const ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara_info = Lara_GetLaraInfo(); effect->frame_num--; if (effect->frame_num <= Object_Get(O_FLAME)->mesh_count) { effect->frame_num = 0; } if (effect->counter >= 0) { M_TR12_DoEffects(effect); if (effect->counter != 0) { effect->counter--; } else if (Lara_IsNearItem(&effect->pos, M_DAMAGE_PROXIMITY)) { Lara_TakeDamage(M_TOO_NEAR_DAMAGE, true); const int32_t dx = lara_item->pos.x - effect->pos.x; const int32_t dz = lara_item->pos.z - effect->pos.z; const int32_t dist = SQUARE(dx) + SQUARE(dz); if (dist < SQUARE(M_IGNITE_PROXIMITY)) { effect->counter = 100; Lara_CatchFire(); } } } else { effect->pos.x = 0; effect->pos.y = 0; if (effect->counter == -1) { effect->pos.z = -100; } else { effect->pos.z = 0; } Collide_GetJointAbsPosition( lara_item, &effect->pos, -1 - effect->counter); const int16_t room_num = lara_item->room_num; if (room_num != effect->room_num) { Effect_UpdateRoom(effect_num, room_num); } const int32_t water_height = Room_GetWaterHeight(effect->pos, effect->room_num); if ((water_height != NO_HEIGHT && effect->pos.y > water_height) || lara_info->water_status == LWS_CHEAT) { effect->counter = 0; Effect_Kill(Effect_GetIndex(effect)); lara_info->burn = false; } else { M_TR12_DoEffects(effect); Lara_TakeDamage(M_ON_FIRE_DAMAGE, false); lara_info->burn = true; } } } static void M_Setup(OBJECT *const obj) { obj->control_func = g_TRVersion == 3 ? M_TR3_Control : M_TR12_Control; obj->semi_transparent = true; } REGISTER_OBJECT(O_FLAME, M_Setup) ================================================ FILE: src/trx/game/objects/effects/flame.h ================================================ #pragma once typedef enum { FLAME_BIG, FLAME_SMALL, FLAME_JET, FLAME_SIDE, FLAME_GREEN, } FLAME_TYPE; ================================================ FILE: src/trx/game/objects/effects/glow.c ================================================ #include #include static void M_Control(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); effect->counter--; if (effect->counter == 0) { Effect_Kill(effect_num); return; } effect->shade += effect->speed; effect->frame_num += effect->fall_speed; } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; } REGISTER_OBJECT(O_GLOW, M_Setup) ================================================ FILE: src/trx/game/objects/effects/gun_flash.c ================================================ #include #include #include #include #include #include #include static void M_Control(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); effect->counter--; if (effect->counter == 0) { Effect_Kill(effect_num); return; } effect->rot.z = Random_GetControl(); if (g_Config.visuals.enable_gun_lighting) { if (g_TRVersion >= 3) { Output_AddDynamicLightRGB( effect->pos, 12, (RGB_888) { 192, 144, 0 }); } else { Output_AddDynamicLight(effect->pos, 12, 11); } } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; } REGISTER_OBJECT(O_GUN_FLASH, M_Setup) ================================================ FILE: src/trx/game/objects/effects/gun_shell.c ================================================ #include #include #include #include #include #include #include #include #include static void M_Control(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); const int32_t old_x = effect->pos.x; const int32_t old_y = effect->pos.y; const int32_t old_z = effect->pos.z; effect->fall_speed += 6; effect->rot.x += 182 * ((effect->speed >> 1) + 7); effect->rot.y += 182 * effect->speed; effect->rot.z += 4186; effect->pos.x += (effect->speed * Math_Sin(effect->flag1)) >> (W2V_SHIFT + 1); effect->pos.y += effect->fall_speed; effect->pos.z += (effect->speed * Math_Cos(effect->flag1)) >> (W2V_SHIFT + 1); int16_t room_num = effect->room_num; const SECTOR *const sector = Room_GetSector(effect->pos, &room_num); if (effect->room_num != room_num) { Effect_UpdateRoom(effect_num, room_num); } const ROOM *const room = Room_Get(room_num); if (room->flags.underwater) { FX_Water_SetupRipple( effect->pos.x, room->max_ceiling, effect->pos.z, -8 - (Random_GetControl() & 3), true); Effect_Kill(effect_num); return; } const int32_t ceiling = Room_GetCeiling(sector, effect->pos); if (effect->pos.y < ceiling) { Sound_Effect(SFX_LARA_SHOTGUN_SHELL, &effect->pos, SPM_NORMAL); effect->speed -= 4; effect->counter--; if (effect->counter < 0 || effect->speed < 8) { Effect_Kill(effect_num); return; } effect->fall_speed = -effect->fall_speed; effect->pos.y = ceiling; } const int32_t height = Room_GetHeight(sector, effect->pos); if (effect->pos.y >= height) { Sound_Effect(SFX_LARA_SHOTGUN_SHELL, &effect->pos, SPM_NORMAL); effect->speed -= 8; effect->counter--; if (effect->counter < 0 || effect->speed < 8) { Effect_Kill(effect_num); return; } if (old_y > height) { effect->flag1 += 0x8000; effect->pos.x = old_x; effect->pos.z = old_z; } else { effect->fall_speed = -effect->fall_speed >> 1; } effect->pos.y = old_y; } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->mesh_count = 0; } REGISTER_OBJECT(O_GUN_SHELL, M_Setup) REGISTER_OBJECT(O_SHOTGUN_SHELL, M_Setup) ================================================ FILE: src/trx/game/objects/effects/hot_liquid.c ================================================ #include #include #include #include #include #include static void M_Control(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); OBJECT *const obj = Object_Get(O_HOT_LIQUID); effect->frame_num--; if (effect->frame_num <= obj->mesh_count) { effect->frame_num = 0; } effect->pos.y += effect->fall_speed; effect->fall_speed += GRAVITY; int16_t room_num = effect->room_num; const SECTOR *const sector = Room_GetSector(effect->pos, &room_num); const int32_t height = Room_GetHeight(sector, effect->pos); if (effect->pos.y >= height) { Sound_Effect(SFX_WATERFALL_2, &effect->pos, SPM_NORMAL); effect->object_id = O_SPLASH_1; effect->pos.y = height; effect->rot.y = 2 * Random_GetDraw(); effect->fall_speed = 0; effect->speed = 50; return; } if (effect->room_num != room_num) { Effect_UpdateRoom(effect_num, room_num); } Sound_Effect(SFX_BOWL_POUR, &effect->pos, SPM_NORMAL); } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->semi_transparent = true; } REGISTER_OBJECT(O_HOT_LIQUID, M_Setup) ================================================ FILE: src/trx/game/objects/effects/missile.c ================================================ #include #include #include #include #include #include #include #include #include #include #define M_SHARD_DAMAGE 30 #define M_EXPLOSION_DAMAGE 100 #define M_EXPLOSION_RANGE_BASE WALL_L #define M_EXPLOSION_RANGE SQUARE(M_EXPLOSION_RANGE_BASE) // = 1048576 static void M_Move(EFFECT *const effect) { effect->pos.y += (effect->speed * Math_Sin(-effect->rot.x)) >> W2V_SHIFT; const int32_t speed = (effect->speed * Math_Cos(effect->rot.x)) >> W2V_SHIFT; effect->pos.z += (speed * Math_Cos(effect->rot.y)) >> W2V_SHIFT; effect->pos.x += (speed * Math_Sin(effect->rot.y)) >> W2V_SHIFT; } static bool M_HitFloorOrCeiling(EFFECT *const effect) { int16_t room_num = effect->room_num; const SECTOR *const sector = Room_GetSector(effect->pos, &room_num); const int32_t height = Room_GetHeight(sector, effect->pos); const int32_t ceiling = Room_GetCeiling(sector, effect->pos); if (effect->pos.y >= height || effect->pos.y <= ceiling) { return true; } if (room_num != effect->room_num) { Effect_UpdateRoom(Effect_GetIndex(effect), room_num); } return false; } static void M_ConvertToRicochet( EFFECT *const effect, const SAMPLE_TRX_ID sample_id) { effect->object_id = O_RICOCHET; effect->frame_num = -Random_GetControl() / 11000; effect->speed = 0; effect->counter = 6; Sound_Effect(sample_id, &effect->pos, SPM_NORMAL); } static void M_ConvertToBlood( EFFECT *const effect, const SAMPLE_TRX_ID sample_id) { ITEM *const lara_item = Lara_GetItem(); Spawn_Blood( effect->pos.x, effect->pos.y, effect->pos.z, lara_item->speed, lara_item->rot.y, lara_item->room_num); Effect_Kill(Effect_GetIndex(effect)); Sound_Effect(sample_id, &effect->pos, SPM_NORMAL); } static void M_ConvertToExplosion(EFFECT *const effect) { effect->object_id = O_EXPLOSION_1; effect->frame_num = 0; effect->speed = 0; effect->counter = 0; Sound_Effect(SFX_EXPLOSION_1, &effect->pos, SPM_NORMAL); } static void M_BlastDamage(EFFECT *const effect) { ITEM *const lara_item = Lara_GetItem(); const int32_t x = effect->pos.x - lara_item->pos.x; const int32_t y = effect->pos.y - lara_item->pos.y; const int32_t z = effect->pos.z - lara_item->pos.z; if (Item_Test3DRange(x, y, z, M_EXPLOSION_RANGE_BASE)) { const int32_t range = SQUARE(x) + SQUARE(y) + SQUARE(z); Lara_TakeDamage( M_EXPLOSION_DAMAGE * (M_EXPLOSION_RANGE - range) / M_EXPLOSION_RANGE, true); } } static void M_ControlAtlanteanShard(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); M_Move(effect); if (M_HitFloorOrCeiling(effect)) { M_ConvertToRicochet(effect, SFX_LARA_RICOCHET); return; } if (!Lara_IsNearItem(&effect->pos, 200)) { return; } Lara_TakeDamage(M_SHARD_DAMAGE, true); M_ConvertToBlood(effect, SFX_LARA_BULLETHIT); } static void M_ControlAtlanteanBomb(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); M_Move(effect); if (M_HitFloorOrCeiling(effect)) { M_ConvertToExplosion(effect); M_BlastDamage(effect); return; } if (!Lara_IsNearItem(&effect->pos, 200)) { return; } const ITEM *const lara_item = Lara_GetItem(); M_ConvertToExplosion(effect); effect->rot.y = lara_item->rot.y; effect->speed = lara_item->speed; Lara_TakeDamage(M_EXPLOSION_DAMAGE, true); if (lara_item->hit_points > 0) { Sound_Effect(SFX_LARA_INJURY, &lara_item->pos, SPM_NORMAL); LARA_INFO *const lara = Lara_GetLaraInfo(); lara->hit_effect = effect; lara->hit_effect_count = 5; } } static void M_ControlPoison(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); M_Move(effect); if (M_HitFloorOrCeiling(effect)) { Effect_Kill(effect_num); return; } if (Lara_IsNearItem(&effect->pos, 350)) { LARA_INFO *const lara_info = Lara_GetLaraInfo(); lara_info->poison_timer += 4; } if (effect->counter == 0) { Effect_Kill(effect_num); } effect->counter--; } static void M_ControlFlame(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); M_Move(effect); if (M_HitFloorOrCeiling(effect)) { if (g_TRVersion == 3) { Sparks_TriggerFlamethrowerHitFlame(effect->pos); const RGB_888 color = { 255, 192, Random_GetControl() & 0x3F }; Output_AddDynamicLightRGB(effect->pos, 24, color); } else { Output_AddDynamicLight(effect->pos, 14, 11); } Effect_Kill(effect_num); return; } if (g_TRVersion == 3 && Room_Get(effect->room_num)->flags.underwater) { if (Random_GetControl() & 1) { Sparks_TriggerFlamethrowerSmoke(effect->pos, true); } Effect_Kill(effect_num); return; } if (Lara_IsNearItem(&effect->pos, 350)) { Lara_TakeDamage(3, true); Lara_CatchFire(); return; } if (effect->counter == 0) { if (g_TRVersion < 3) { Output_AddDynamicLight(effect->pos, 14, 11); Sound_Effect(SFX_DRAGON_FIRE, &effect->pos, SPM_NORMAL); } Effect_Kill(effect_num); } effect->counter--; } static void M_ControlHarpoon(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); M_Move(effect); if (M_HitFloorOrCeiling(effect)) { M_ConvertToRicochet(effect, SFX_PROJECTILE_HIT); return; } if (!Room_Get(effect->room_num)->flags.underwater) { if (effect->rot.x > -0x3000) { effect->rot.x -= DEG_1; } } if (Lara_IsNearItem(&effect->pos, 200)) { Lara_TakeDamage(50, true); M_ConvertToBlood(effect, SFX_CRUNCH_1); } if (Room_Get(effect->room_num)->flags.underwater) { if (g_TRVersion == 3) { const int32_t time4 = Output_GetTimeInGame() * 4; if ((time4 & 0xF) == 0) { Spawn_BubbleEx(&effect->pos, effect->room_num, 8, 8); } Sparks_TriggerRocketSmoke(effect->pos, 64, effect->room_num); } else { Spawn_Bubble(&effect->pos, effect->room_num); } } } static void M_ControlKnife(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); M_Move(effect); if (M_HitFloorOrCeiling(effect)) { M_ConvertToRicochet(effect, SFX_PROJECTILE_HIT); return; } if (Lara_IsNearItem(&effect->pos, 200)) { Lara_TakeDamage(50, true); M_ConvertToBlood(effect, SFX_CRUNCH_1); } effect->rot.z += 30 * DEG_1; } static void M_SetupAtlanteanBomb(OBJECT *const obj) { obj->control_func = M_ControlAtlanteanBomb; obj->save_position = true; } static void M_SetupAtlanteanShard(OBJECT *const obj) { obj->control_func = M_ControlAtlanteanShard; obj->save_position = true; } static void M_SetupFlame(OBJECT *const obj) { obj->control_func = M_ControlFlame; obj->save_position = true; } static void M_SetupPoison(OBJECT *const obj) { obj->control_func = M_ControlPoison; obj->save_position = true; } static void M_SetupHarpoon(OBJECT *const obj) { obj->control_func = M_ControlHarpoon; obj->save_position = true; } static void M_SetupKnife(OBJECT *const obj) { obj->control_func = M_ControlKnife; obj->save_position = true; } REGISTER_OBJECT(O_MISSILE_ATLANTEAN_SHARD, M_SetupAtlanteanShard) REGISTER_OBJECT(O_MISSILE_ATLANTEAN_BOMB, M_SetupAtlanteanBomb) REGISTER_OBJECT(O_MISSILE_FLAME, M_SetupFlame) REGISTER_OBJECT(O_MISSILE_POISON, M_SetupPoison) REGISTER_OBJECT(O_MISSILE_HARPOON, M_SetupHarpoon) REGISTER_OBJECT(O_MISSILE_KNIFE, M_SetupKnife) ================================================ FILE: src/trx/game/objects/effects/pickup_aid.c ================================================ #include #include static void M_Control(int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); effect->counter++; if (effect->counter == 1) { effect->counter = 0; effect->frame_num--; if (effect->frame_num <= Object_Get(effect->object_id)->mesh_count) { Effect_Kill(effect_num); } } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; } REGISTER_OBJECT(O_PICKUP_AID, M_Setup) ================================================ FILE: src/trx/game/objects/effects/ricochet.c ================================================ #include #include static void M_Control(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); effect->counter--; if (effect->counter == 0) { Effect_Kill(effect_num); } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; } REGISTER_OBJECT(O_RICOCHET, M_Setup) ================================================ FILE: src/trx/game/objects/effects/snow_sprite.c ================================================ #include #include #include #include static void M_Control(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); const OBJECT *const obj = Object_Get(effect->object_id); effect->frame_num--; if (effect->frame_num <= obj->mesh_count) { Effect_Kill(effect_num); return; } effect->pos.z += (effect->speed * Math_Cos(effect->rot.y)) >> W2V_SHIFT; effect->pos.x += (effect->speed * Math_Sin(effect->rot.y)) >> W2V_SHIFT; if (effect->fall_speed != 0) { effect->pos.y += effect->fall_speed; effect->fall_speed += GRAVITY; } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; } REGISTER_OBJECT(O_SNOW_SPRITE, M_Setup) ================================================ FILE: src/trx/game/objects/effects/splash.c ================================================ #include #include #include #include static void M_Control(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); const OBJECT *const obj = Object_Get(effect->object_id); effect->frame_num--; if (effect->frame_num <= obj->mesh_count) { Effect_Kill(effect_num); return; } effect->pos.x += (effect->speed * Math_Sin(effect->rot.y)) >> W2V_SHIFT; effect->pos.z += (effect->speed * Math_Cos(effect->rot.y)) >> W2V_SHIFT; } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->semi_transparent = g_TRVersion >= 2; } REGISTER_OBJECT(O_SPLASH_1, M_Setup) ================================================ FILE: src/trx/game/objects/effects/twinkle.c ================================================ #include #include #include #include #include #include #include #include #define M_DISAPPEAR_RANGE STEP_L static void M_SpawnTwinkle(const GAME_VECTOR *const pos) { const int16_t effect_num = Effect_Create(pos->room_num); if (effect_num != NO_EFFECT) { EFFECT *const effect = Effect_Get(effect_num); effect->pos = pos->pos; effect->counter = Object_Get(O_TWINKLE)->mesh_count; effect->object_id = O_TWINKLE; effect->frame_num = 0; } } static XYZ_32 M_GetTargetPos(const ITEM *const item) { XYZ_32 pos = item->pos; if (item->object_id == O_DRAGON_FRONT) { const int32_t c = Math_Cos(item->rot.y); const int32_t s = Math_Sin(item->rot.y); pos.x += (c * 1100 + s * 490) >> W2V_SHIFT; pos.z += (c * 490 - s * 1100) >> W2V_SHIFT; pos.y -= 540; } return pos; } static void M_NudgeTowardsItem( EFFECT *const effect, const XYZ_32 *const target_pos) { effect->pos.x += (target_pos->x - effect->pos.x) >> 4; effect->pos.y += (target_pos->y - effect->pos.y) >> 4; effect->pos.z += (target_pos->z - effect->pos.z) >> 4; } static bool M_ShouldDisappear( const EFFECT *const effect, const XYZ_32 *const target_pos) { const int32_t dx = ABS(effect->pos.x - target_pos->x); const int32_t dy = ABS(effect->pos.y - target_pos->y); const int32_t dz = ABS(effect->pos.z - target_pos->z); return dx < M_DISAPPEAR_RANGE && dy < M_DISAPPEAR_RANGE && dz < M_DISAPPEAR_RANGE; } static void M_Control(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); const OBJECT *const obj = Object_Get(effect->object_id); effect->frame_num--; if (effect->frame_num <= obj->mesh_count) { effect->frame_num = 0; } if (effect->counter < 0) { effect->counter++; if (effect->counter == 0) { Effect_Kill(effect_num); } return; } const ITEM *const item = Item_Get(effect->counter); const XYZ_32 target_pos = M_GetTargetPos(item); M_NudgeTowardsItem(effect, &target_pos); if (M_ShouldDisappear(effect, &target_pos)) { Effect_Kill(effect_num); } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; } void Twinkle_SparkleItem(const ITEM *const item, const uint32_t mesh_mask) { SPHERE spheres[34]; const int32_t num_spheres = Collide_GetSpheres(item, spheres, true); GAME_VECTOR effect_pos = { .pos = {}, .room_num = item->room_num, }; for (int32_t i = 0; i < num_spheres; i++) { if ((mesh_mask & (1 << i)) == 0) { continue; } const SPHERE *const sphere = &spheres[i]; effect_pos.x = sphere->pos.x + sphere->r * (Random_GetDraw() - 0x4000) / 0x4000; effect_pos.y = sphere->pos.y + sphere->r * (Random_GetDraw() - 0x4000) / 0x4000; effect_pos.z = sphere->pos.z + sphere->r * (Random_GetDraw() - 0x4000) / 0x4000; M_SpawnTwinkle(&effect_pos); } } REGISTER_OBJECT(O_TWINKLE, M_Setup) ================================================ FILE: src/trx/game/objects/effects/twinkle.h ================================================ #pragma once #include void Twinkle_SparkleItem(const ITEM *item, uint32_t mesh_mask); ================================================ FILE: src/trx/game/objects/effects/water_sprite.c ================================================ #include #include #include #include static void M_Control(const int16_t effect_num) { EFFECT *const effect = Effect_Get(effect_num); const OBJECT *const obj = Object_Get(effect->object_id); effect->counter--; if (effect->counter % 4 == 0) { effect->frame_num--; if (effect->frame_num <= obj->mesh_count) { effect->frame_num = 0; } } if (effect->counter == 0 || effect->fall_speed > 0) { Effect_Kill(effect_num); return; } effect->pos.x += (effect->speed * Math_Sin(effect->rot.y)) >> W2V_SHIFT; effect->pos.z += (effect->speed * Math_Cos(effect->rot.y)) >> W2V_SHIFT; if (effect->fall_speed != 0) { effect->pos.y += effect->fall_speed; effect->fall_speed += GRAVITY; } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->semi_transparent = true; } REGISTER_OBJECT(O_WATER_SPRITE, M_Setup) ================================================ FILE: src/trx/game/objects/general/ai_node.c ================================================ #include static void M_Setup(OBJECT *const obj) { obj->draw_func = nullptr; obj->hit_points = 0; } REGISTER_OBJECT(O_AI_AMBUSH, M_Setup) REGISTER_OBJECT(O_AI_GUARD, M_Setup) REGISTER_OBJECT(O_AI_FOLLOW, M_Setup) REGISTER_OBJECT(O_AI_PATROL_1, M_Setup) REGISTER_OBJECT(O_AI_PATROL_2, M_Setup) REGISTER_OBJECT(O_AI_MODIFY, M_Setup) REGISTER_OBJECT(O_AI_X1, M_Setup) REGISTER_OBJECT(O_AI_X2, M_Setup) REGISTER_OBJECT(O_AI_X3, M_Setup) ================================================ FILE: src/trx/game/objects/general/alarm_sound.c ================================================ #include #include #include #include typedef struct { int32_t counter; } M_PRIV; static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if ((item->flags & IF_CODE_BITS) != IF_CODE_BITS) { return; } Sound_Effect(SFX_PLATFORM_ALARM, &item->pos, SPM_NORMAL); M_PRIV *const p = item->priv; p->counter++; if (p->counter > 6) { Output_AddDynamicLight(item->pos, 12, 11); if (p->counter > 12) { p->counter = 0; } } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->priv_size = sizeof(M_PRIV); obj->save_flags = true; } REGISTER_OBJECT(O_ALARM_SOUND, M_Setup) ================================================ FILE: src/trx/game/objects/general/animating.c ================================================ #include #include static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (!Item_IsTriggerActive(item)) { return; } Item_Animate(item); int16_t room_num = item->room_num; Room_GetSector(item->pos, &room_num); Item_UpdateRoom(item_num, room_num); } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->collision_func = Object_Collision; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_ANIMATING_1, M_Setup) REGISTER_OBJECT(O_ANIMATING_2, M_Setup) REGISTER_OBJECT(O_ANIMATING_3, M_Setup) REGISTER_OBJECT(O_ANIMATING_4, M_Setup) REGISTER_OBJECT(O_ANIMATING_5, M_Setup) REGISTER_OBJECT(O_ANIMATING_6, M_Setup) REGISTER_OBJECT(O_ANIMATING_7, M_Setup) REGISTER_OBJECT(O_ANIMATING_8, M_Setup) REGISTER_OBJECT(O_ANIMATING_9, M_Setup) REGISTER_OBJECT(O_ANIMATING_10, M_Setup) ================================================ FILE: src/trx/game/objects/general/area_51_rocket.c ================================================ #include #include #include #include #include #include #include #include #include #include #define M_SMOKE_END 512 typedef enum { M_SUPPORT_STATE_WAIT = 1, M_SUPPORT_STATE_FALL = 2, } M_SUPPORT_STATE; typedef struct { int16_t fire_room_num; } M_PRIV; static bool m_SupportFallen = false; static bool M_DoesRoomLeadToItemCeilingPos( const ITEM *const item, const int16_t room_num) { const SECTOR *const sector = Room_GetWorldSector(Room_Get(room_num), item->pos.x, item->pos.z); return sector->portal_room.sky == item->room_num; } static int16_t M_FindFireRoomNum(const ITEM *const item) { int16_t fire_room_num = item->room_num; const XYZ_32 probe_pos = { .x = item->pos.x, .y = item->pos.y + WALL_L, .z = item->pos.z, }; const int16_t room_num = Room_GetIndexFromPos(probe_pos); if (room_num != NO_ROOM && room_num != fire_room_num && M_DoesRoomLeadToItemCeilingPos(item, room_num)) { fire_room_num = room_num; } return fire_room_num; } static void M_InitialiseMain(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (item->priv == nullptr) { item->priv = GameBuf_Alloc(sizeof(M_PRIV), GBUF_ITEM_DATA); } M_PRIV *const p = item->priv; p->fire_room_num = M_FindFireRoomNum(item); } static int32_t M_GetFloorY(const ITEM *const item) { const M_PRIV *const p = item->priv; if (p != nullptr && p->fire_room_num != NO_ROOM) { return Room_Get(p->fire_room_num)->min_floor; } int16_t room_num = item->room_num; Room_GetSector(item->pos, &room_num); return Room_Get(room_num)->min_floor; } static void M_TriggerBlastFire( const XYZ_32 pos, const bool smoke, const int32_t end) { SPARK *const spark = end < 0 ? Sparks_GetFreeSpark() : Sparks_GetSpark(end); spark->on = true; if (smoke) { spark->src_color.r = 0; spark->src_color.g = 0; spark->src_color.b = 0; spark->dst_color.r = 64; spark->dst_color.g = 64; spark->dst_color.b = 64; } else { spark->src_color.r = (Random_GetControl() & 0x1F) + 128; spark->src_color.g = (Random_GetControl() & 0x1F) + 64; spark->src_color.b = 32; spark->dst_color.r = (Random_GetControl() & 0x1F) + 224; spark->dst_color.g = (Random_GetControl() & 0x1F) + 160; spark->dst_color.b = 32; } spark->col_fade_speed = 16; if (end) { spark->fade_to_black = (Random_GetControl() & 0x1F) + 32; spark->life = (end >> 1) + 72; } else { spark->fade_to_black = smoke ? 32 : 8; spark->life = (smoke ? 32 : 0) + (Random_GetControl() & 7) + 32; } spark->s_life = spark->life; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 0xF) - 8; spark->pos.y = pos.y; spark->pos.z = pos.z + (Random_GetControl() & 0xF) - 8; spark->vel.x = (Random_GetControl() & 0xFF) - 128; spark->vel.y = -(Random_GetControl() & 7); spark->vel.z = (Random_GetControl() & 0xFF) - 128; spark->friction = 4; spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = Random_GetControl() & 0xFFF; if (Random_GetControl() & 1) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = (Random_GetControl() & 0xF) + 16; } spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->scalar = 4; spark->max_y_vel = 0; spark->gravity = 0; const int32_t size = (Random_GetControl() & 0x3F) + 64; if (end) { spark->size.width = size; spark->src_size.width = spark->size.width; spark->dst_size.width = spark->size.width; spark->size.height = spark->size.width; spark->src_size.height = spark->size.height; spark->dst_size.height = spark->size.height; } else { spark->dst_size.width = size; spark->size.width = spark->dst_size.width >> 1; spark->src_size.width = spark->size.width; spark->dst_size.height = spark->dst_size.width; spark->size.height = spark->dst_size.height >> 1; spark->src_size.height = spark->size.height; } Sparks_FinishSetup(spark); } static void M_TriggerRocketSmoke( const XYZ_32 pos, const int32_t yv, const bool fire) { SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; if (fire) { spark->src_color.r = (Random_GetControl() & 0x1F) + 48; spark->src_color.g = spark->src_color.r; spark->src_color.b = (Random_GetControl() & 0x3F) + 192; spark->dst_color.r = (Random_GetControl() & 0x3F) + 192; spark->dst_color.g = (Random_GetControl() & 0x3F) + 128; spark->dst_color.b = 32; } else { spark->src_color.r = 0; spark->src_color.g = 0; spark->src_color.b = 0; spark->dst_color.r = (yv >> 5) + 32; spark->dst_color.g = spark->dst_color.r; spark->dst_color.b = spark->dst_color.r; } spark->col_fade_speed = 16 - (fire ? yv >> 9 : 0); spark->fade_to_black = !fire ? 16 : 0; spark->life = (Random_GetControl() & 3) - (fire ? yv >> 8 : 0) + 60; spark->s_life = spark->life; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 0xF) - 8; spark->pos.y = pos.y; spark->pos.z = pos.z + (Random_GetControl() & 0xF) - 8; spark->vel.x = (Random_GetControl() & 0xFF) - 128; spark->vel.y = yv + (Random_GetControl() & 0xF); spark->vel.z = (Random_GetControl() & 0xFF) - 128; spark->friction = 4; if (Random_GetControl() & 1) { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = Random_GetControl() & 0xFFF; if (Random_GetControl() & 1) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = (Random_GetControl() & 0xF) + 16; } } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->scalar = 3; spark->max_y_vel = 0; spark->gravity = 0; int32_t size = (Random_GetControl() & 0x3F) + (yv >> 5) + 64; CLAMPG(size, 255); spark->dst_size.width = (uint8_t)size; spark->size.width = spark->dst_size.width >> 2; spark->src_size.width = spark->size.width; spark->dst_size.height = spark->dst_size.width; spark->size.height = spark->dst_size.height >> 2; spark->src_size.height = spark->size.height; Sparks_FinishSetup(spark); } static void M_ControlRocket(ITEM *const item) { if (item->required_anim_state < M_SMOKE_END) { item->required_anim_state += 8; if (item->required_anim_state < M_SMOKE_END) { Sound_Effect(SFX_LARA_FLARE_BURN, nullptr, SPM_NORMAL); item->current_anim_state = 0; item->goal_anim_state = 0; } else { item->required_anim_state += 2048; item->goal_anim_state = 64; Sound_Effect(SFX_EXPLOSION_2, nullptr, SPM_NORMAL); } } else { if (item->current_anim_state) { Sound_Effect(SFX_HUGE_ROCKET_LOOP, nullptr, SPM_NORMAL); item->required_anim_state += 32; if (item->required_anim_state > 16000) { Item_Kill(Item_GetIndex(item)); } else { const int32_t base = 0x4000 - item->required_anim_state; const RGB_888 color = { .r = (base * ((Random_GetControl() & 0x1F) + 224)) >> 12, .g = (base * ((Random_GetControl() & 0x3F) + 96)) >> 12, .b = (base * (Random_GetControl() & 0x1F)) >> 12, }; Output_AddDynamicLightRGB( (XYZ_32) { .x = item->pos.x - 7680, .y = M_GetFloorY(item) - (Random_GetControl() & 0x1FF) - 256, .z = item->pos.z - 1024, }, 24, color); g_Camera.bounce = -((0x4000 - item->required_anim_state) >> 6); } return; } if (!Lara_GetLaraInfo()->burn) { int32_t rad = item->goal_anim_state; CLAMPG(rad, 8192); const ITEM *const lara_item = Lara_GetItem(); if (lara_item->pos.x > item->pos.x - rad - 1536) { Lara_TakeDamage(lara_item->hit_points, false); Lara_CatchFire(); } item->goal_anim_state += 80; } item->required_anim_state += 32; } if (item->required_anim_state < 4096 + M_SMOKE_END) { if (item->goal_anim_state > 768) { Sound_Effect(SFX_HUGE_ROCKET_LOOP, nullptr, SPM_NORMAL); } if (item->goal_anim_state > 1024) { item->goal_anim_state = 1024; } } else { Sound_Effect(SFX_HUGE_ROCKET_LOOP, nullptr, SPM_NORMAL); if (item->required_anim_state > 12288) { item->required_anim_state = 12288; } if (item->goal_anim_state > 22528) { for (int32_t i = 0; i < 64; i++) { const XYZ_32 pos = { .x = item->pos.x - (Random_GetControl() & 0xFFF) - 5632, .y = M_GetFloorY(item) - (Random_GetControl() & 0x7FF), .z = item->pos.z + (Random_GetControl() & 0x7FF) - 2048, }; M_TriggerBlastFire(pos, false, i); } for (int32_t i = 64; i < 96; i++) { const XYZ_32 pos = { .x = item->pos.x - (Random_GetControl() & 0xFFF) - 5632, .y = M_GetFloorY(item) - (Random_GetControl() & 0x7FF), .z = item->pos.z + (Random_GetControl() & 0x7FF) - 2048, }; M_TriggerBlastFire(pos, true, i); } g_Camera.bounce = -((0x4000 - item->required_anim_state) >> 6); item->current_anim_state = 1; return; } item->fall_speed--; CLAMPL(item->fall_speed, -1024); if (item->fall_speed < -72) { m_SupportFallen = true; } item->pos.y += item->fall_speed >> 2; int16_t room_num = item->room_num; Room_GetSector(item->pos, &room_num); if (item->room_num != room_num) { Item_UpdateRoom(Item_GetIndex(item), room_num); } } if (item->required_anim_state >= 0x2000) { g_Camera.bounce = -((0x4000 - item->required_anim_state) >> 6); } else if (item->required_anim_state <= 64) { g_Camera.bounce = -1; } else { g_Camera.bounce = -(item->required_anim_state >> 6); } } static void M_ControlBlast(ITEM *const item) { const int32_t time4 = Output_GetTimeInGame() * 4; if (!(time4 & 0xC)) { int32_t yv; if (item->required_anim_state >= M_SMOKE_END >> 2) { yv = 4 * (item->required_anim_state + (Random_GetControl() & 0x1F)) - M_SMOKE_END; } else { yv = Random_GetControl() & 0x1F; } CLAMPG(yv, 6144); const bool fire = item->required_anim_state >= M_SMOKE_END; M_TriggerRocketSmoke( (XYZ_32) { .x = item->pos.x - 896, .y = item->pos.y - 64, .z = item->pos.z - 512, }, yv, fire); M_TriggerRocketSmoke( (XYZ_32) { .x = item->pos.x - 128, .y = item->pos.y - 64, .z = item->pos.z - 512, }, yv, fire); M_TriggerRocketSmoke( (XYZ_32) { .x = item->pos.x - 512, .y = item->pos.y - 64, .z = item->pos.z - 896, }, yv, fire); M_TriggerRocketSmoke( (XYZ_32) { .x = item->pos.x - 512, .y = item->pos.y - 64, .z = item->pos.z - 128, }, yv, fire); } if (item->goal_anim_state != 0) { RGB_888 color = { .r = (Random_GetControl() & 0x1F) + 224, .g = (Random_GetControl() & 0x3F) + 96, .b = Random_GetControl() & 0x1F, }; Output_AddDynamicLightRGB( (XYZ_32) { .x = item->pos.x - 512, .y = item->pos.y, .z = item->pos.z - 512, }, 31, color); int32_t rad = item->goal_anim_state; CLAMPG(rad, 8192); const int32_t y_mask = item->goal_anim_state >= 1024 ? 2047 : 255; XYZ_32 pos = { .x = item->pos.x - (Random_GetControl() & 0x7FF) - rad + 512, .y = M_GetFloorY(item) - (y_mask & Random_GetControl()), .z = item->pos.z + (Random_GetControl() & 0x7FF) - 2048, }; if (time4 & 4) { M_TriggerBlastFire(pos, false, -1); } pos.x = item->pos.x - rad + 512; pos.z = item->pos.z - 1024; color.r = (Random_GetControl() & 0x1F) + 224; color.g = (Random_GetControl() & 0x3F) + 96; color.b = Random_GetControl() & 0x1F; Output_AddDynamicLightRGB(pos, 24, color); if (time4 & 4) { pos.x = item->pos.x - (Random_GetControl() & 0x3FF) - rad - 512; pos.y = M_GetFloorY(item) - (y_mask & Random_GetControl()); pos.z = item->pos.z + (Random_GetControl() & 0x7FF) - 2048; M_TriggerBlastFire(pos, true, -1); } } } static void M_ControlMain(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (!Item_IsTriggerActive(item)) { return; } if (item->object_id == O_AREA_51_ROCKET) { M_ControlRocket(item); } M_ControlBlast(item); } void M_InitialiseSupport(const int16_t item_num) { m_SupportFallen = false; } static void M_ControlSupport(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (!m_SupportFallen) { item->current_anim_state = M_SUPPORT_STATE_WAIT; } else if ( item->goal_anim_state != M_SUPPORT_STATE_FALL && item->current_anim_state != M_SUPPORT_STATE_FALL) { item->goal_anim_state = M_SUPPORT_STATE_FALL; } Item_Animate(item); } static void M_SetupRocket(OBJECT *const obj) { obj->initialise_func = M_InitialiseMain; obj->control_func = M_ControlMain; obj->save_position = true; obj->save_flags = true; obj->save_anim = true; } static void M_SetupBlast(OBJECT *const obj) { obj->initialise_func = M_InitialiseMain; obj->control_func = M_ControlMain; obj->draw_func = nullptr; obj->save_flags = true; obj->save_anim = true; } static void M_SetupSupport(OBJECT *const obj) { obj->initialise_func = M_InitialiseSupport; obj->control_func = M_ControlSupport; obj->save_anim = true; } REGISTER_OBJECT(O_AREA_51_ROCKET, M_SetupRocket) REGISTER_OBJECT(O_AREA_51_ROCKET_BLAST, M_SetupBlast) REGISTER_OBJECT(O_AREA_51_ROCKET_SUPPORT, M_SetupSupport) ================================================ FILE: src/trx/game/objects/general/assault_target.c ================================================ #include #include #include #include #include #include #include #include #include typedef enum { M_STATE_RISE = 0, M_STATE_HIT_1 = 1, M_STATE_HIT_2 = 2, M_STATE_HIT_3 = 3, } M_STATE; typedef struct { int32_t x_rot_speed; int32_t bounce_stage; bool destroyed; bool targetable; } M_PRIV; static bool M_ShouldSpawnBlood(const ITEM *const item) { return false; } static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "x_rot_speed", &p->x_rot_speed)); JSON_SHOULD(JSON_READ(io, "bounce_stage", &p->bounce_stage)); JSON_SHOULD(JSON_READ(io, "destroyed", &p->destroyed)); p->targetable = !p->destroyed; JSON_OPTIONAL(JSON_READ(io, "targetable", &p->targetable)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "x_rot_speed", p->x_rot_speed); JSONW_WRITE(io, "bounce_stage", p->bounce_stage); JSONW_WRITE(io, "destroyed", p->destroyed); JSONW_WRITE(io, "targetable", p->targetable); } static void M_ResetItemState(ITEM *const item, const OBJECT *const obj) { Item_SwitchToAnim(item, 0, 0); const ANIM *const anim = Item_GetAnim(item); item->current_anim_state = anim->current_anim_state; item->goal_anim_state = item->current_anim_state; item->required_anim_state = M_STATE_RISE; item->prev_frame_num = item->frame_num; item->rot.x = 0; item->rot.z = 0; item->timer = 0; item->hit_points = obj->hit_points; item->max_hit_points = obj->hit_points; } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); const OBJECT *const obj = Object_Get(item->object_id); if (item->active) { Item_RemoveActive(item_num); } if (item->creature_data != nullptr) { LOT_DisableBaddieAI(item_num); } M_PRIV *const p = item->priv; p->x_rot_speed = 0; p->bounce_stage = 0; p->destroyed = false; p->targetable = true; item->active = false; item->status = IS_INACTIVE; item->flags = 0; item->collidable = true; M_ResetItemState(item, obj); } static void M_Control(const int16_t item_num) { if (g_TRVersion < 3) { return; } ITEM *const item = Item_Get(item_num); if (item->status != IS_ACTIVE) { return; } M_PRIV *const p = item->priv; if (p == nullptr) { return; } if (p->targetable) { if (item->hit_status) { Sound_Effect(SFX_TARGET_HITS, &item->pos, SPM_NORMAL); } switch (item->current_anim_state) { case M_STATE_RISE: if (item->hit_points < 6) { item->hit_points = 6; item->goal_anim_state = M_STATE_HIT_1; } break; case M_STATE_HIT_1: if (item->hit_points < 4) { Item_SwitchToAnim(item, 2, 0); item->current_anim_state = M_STATE_HIT_2; item->goal_anim_state = M_STATE_HIT_2; item->hit_points = 4; } break; case M_STATE_HIT_2: if (item->hit_points < 2) { Item_SwitchToAnim(item, 3, 0); item->current_anim_state = M_STATE_HIT_3; item->goal_anim_state = M_STATE_HIT_3; item->hit_points = 2; } break; case M_STATE_HIT_3: if (item->hit_points <= 0) { LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->target == item) { lara->target = nullptr; } p->targetable = false; p->x_rot_speed = DEG_1 * 10; p->bounce_stage = 0; p->destroyed = true; } break; } item->timer++; if (item->timer > GYM_ASSAULT_TARGET_TIME) { LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->target == item) { lara->target = nullptr; } p->targetable = false; p->x_rot_speed = DEG_1; p->bounce_stage = 0; p->destroyed = false; } } else { if (p->destroyed) { int32_t rot_x = item->rot.x; rot_x += p->x_rot_speed; p->x_rot_speed += (DEG_1 * 4) >> p->bounce_stage; if (rot_x > 0x3800) { if (p->bounce_stage == 2) { item->rot.x = 0x3800; Item_RemoveActive(item_num); return; } if (p->bounce_stage == 1) { Sound_Effect(SFX_TARGET_SMASH, &item->pos, SPM_NORMAL); } p->x_rot_speed = (-p->x_rot_speed) >> 2; p->bounce_stage++; rot_x = 0x3800; } item->rot.x = rot_x; } else { int32_t rot_x = item->rot.x; rot_x -= p->x_rot_speed; p->x_rot_speed += 91 >> p->bounce_stage; if (rot_x < -0x2A00) { if (p->bounce_stage == 2) { item->rot.x = -0x2A00; Item_RemoveActive(item_num); return; } Sound_Effect( SFX_TARGET_HITS, &item->pos, SPM_PITCH | (0x20000 << 8)); p->x_rot_speed = (-p->x_rot_speed) >> 2; p->bounce_stage++; rot_x = -0x2A00; } item->rot.x = (int16_t)rot_x; } } Item_Animate(item); } static bool M_IsTargetable(const ITEM *const item) { const M_PRIV *const p = item->priv; return p != nullptr && p->targetable && item->status == IS_ACTIVE && item->hit_points > 0; } static bool M_CanTakeDamage(const ITEM *const item) { const M_PRIV *const p = item->priv; return p != nullptr && p->targetable && item->hit_points > 0; } static bool M_CanBeProjectileTarget(const ITEM *const item) { const M_PRIV *const p = item->priv; return p != nullptr && p->targetable && item->status == IS_ACTIVE && item->collidable && item->hit_points > 0; } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Object_Collision; obj->should_spawn_blood_func = M_ShouldSpawnBlood; obj->is_targetable_func = M_IsTargetable; obj->can_take_damage_func = M_CanTakeDamage; obj->can_be_projectile_target_func = M_CanBeProjectileTarget; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->hit_points = 8; obj->shadow_size = 128; obj->radius = 102; obj->intelligent = false; } REGISTER_OBJECT(O_ASSAULT_TARGET, M_Setup) ================================================ FILE: src/trx/game/objects/general/bat_emitter.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_MAX_BATS 32 #define M_BAT_SPRITE_OFFSET 12 typedef struct { XYZ_32 pos; XYZ_32 prev_pos; int16_t angle; int16_t prev_angle; int16_t speed; uint8_t wing_y_off; uint8_t prev_wing_y_off; bool active; uint8_t life; } M_BAT; typedef struct { bool bats_triggered; bool bats_alive; M_BAT bats[M_MAX_BATS]; struct { bool prepared; int32_t sprite_idx; OUTPUT_UVW tri_uvw[3][3]; OUTPUT_TEXTURE_SIZE tri_tex_size[3][3]; } draw; } M_PRIV; static const XYZ_16 m_BatMesh[5] = { { -192, 0, -48 }, { -192, 0, 48 }, { 96, 0, 0 }, { -144, 0, -192 }, { -144, 0, 192 }, }; static const uint8_t m_BatTriangles[3][3] = { { 0, 1, 2 }, { 3, 0, 2 }, { 1, 4, 2 }, }; // TR3 UV mapping differs per triangle in the original bat GT3 setup. static const uint8_t m_BatTriangleSpriteCorners[3][3] = { { 0, 2, 3 }, { 1, 0, 2 }, { 0, 1, 2 }, }; static int32_t M_GetWingYOffset(const int32_t corner, const uint8_t wing_y_off) { if (corner < 3) { const int16_t angle = (((wing_y_off - 32) & 0x3F) << 10); return (Math_Sin(angle) >> 10) - 512; } const int16_t angle = wing_y_off << 10; return (Math_Sin(angle) >> 6) - 512; } static void M_RememberBat(M_BAT *const bat) { bat->prev_pos = bat->pos; bat->prev_angle = bat->angle; bat->prev_wing_y_off = bat->wing_y_off; } static uint8_t M_GetInterpolatedWingYOffset( const M_BAT *const bat, const double ratio) { int32_t wing_diff = (int32_t)bat->wing_y_off - (int32_t)bat->prev_wing_y_off; if (wing_diff > 32) { wing_diff -= 64; } else if (wing_diff < -32) { wing_diff += 64; } int32_t wing_interp = LERP( (int32_t)bat->prev_wing_y_off, (int32_t)bat->prev_wing_y_off + wing_diff, ratio); wing_interp %= 64; if (wing_interp < 0) { wing_interp += 64; } return (uint8_t)wing_interp; } static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "bats_triggered", &p->bats_triggered)); JSON_SHOULD(JSON_READ(io, "bats_alive", &p->bats_alive)); for (int32_t i = 0; i < M_MAX_BATS; i++) { p->bats[i] = (M_BAT) {}; } if (p->bats_alive && JSON_SHOULD(JSON_PUSH(io, "bats"))) { for (int32_t i = 0; i < M_MAX_BATS; i++) { const char *const key = String_FormatStatic("bat_%d", i); if (JSON_SHOULD(JSON_PUSH(io, key))) { M_BAT *const bat = &p->bats[i]; JSON_SHOULD(JSON_READ(io, "pos", &bat->pos)); JSON_SHOULD(JSON_READ(io, "angle", &bat->angle)); JSON_SHOULD(JSON_READ(io, "speed", &bat->speed)); JSON_SHOULD(JSON_READ(io, "wing_y_off", &bat->wing_y_off)); JSON_SHOULD(JSON_READ(io, "active", &bat->active)); JSON_SHOULD(JSON_READ(io, "life", &bat->life)); JSON_SHOULD(JSON_POP(io)); } } JSON_SHOULD(JSON_POP(io)); } p->draw.prepared = false; p->draw.sprite_idx = -1; } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "bats_triggered", p->bats_triggered); JSONW_WRITE(io, "bats_alive", p->bats_alive); if (!p->bats_alive) { return; } JSONW_PUSH_OBJECT(io); for (int32_t i = 0; i < M_MAX_BATS; i++) { const M_BAT *const bat = &p->bats[i]; const char *const key = String_FormatStatic("bat_%d", i); JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "pos", bat->pos); JSONW_WRITE(io, "angle", bat->angle); JSONW_WRITE(io, "speed", bat->speed); JSONW_WRITE(io, "wing_y_off", bat->wing_y_off); JSONW_WRITE(io, "active", bat->active); JSONW_WRITE(io, "life", bat->life); JSONW_POP_AND_SET(io, key); } JSONW_POP_AND_SET(io, "bats"); } static void M_PrepareDrawData(M_PRIV *const p) { if (p->draw.prepared) { return; } p->draw.sprite_idx = -1; const OBJECT *const explosion = Object_Get(O_EXPLOSION_1); if (explosion == nullptr || !explosion->loaded) { return; } const int32_t sprite_idx = explosion->mesh_idx + M_BAT_SPRITE_OFFSET; if (sprite_idx < 0 || sprite_idx >= Output_GetSpriteTextureCount()) { return; } p->draw.sprite_idx = sprite_idx; for (size_t i = 0; i < ARRAY_SIZE(m_BatTriangleSpriteCorners); i++) { for (size_t j = 0; j < ARRAY_SIZE(m_BatTriangleSpriteCorners[0]); j++) { const int32_t uvw_idx = Output_Textures_GetSpriteUVWIndex( sprite_idx, m_BatTriangleSpriteCorners[i][j]); p->draw.tri_uvw[i][j] = Output_Textures_GetUVW(uvw_idx); p->draw.tri_tex_size[i][j] = Output_Textures_GetAtlasSize(uvw_idx / 4); } } p->draw.prepared = true; } static bool M_Draw(const ITEM *const item) { M_PRIV *const p = item->priv; if (!p->bats_alive) { return false; } if (!p->draw.prepared) { M_PrepareDrawData(p); } if (p->draw.sprite_idx < 0 || p->draw.sprite_idx >= Output_GetSpriteTextureCount()) { return false; } const RGBA_8888 color = { 0x60, 0xA0, 0xF8, 0xFF }; const RGBA_8888 tri_color[3] = { color, color, color }; const double ratio = Interpolation_GetWorldRate(); const bool do_interp = Interpolation_IsActive() && ratio > 0.0 && ratio < 1.0; for (int32_t i = 0; i < M_MAX_BATS; i++) { const M_BAT *const bat = &p->bats[i]; if (!bat->active) { continue; } const XYZ_32 draw_pos = do_interp ? (XYZ_32) { .x = (int32_t)LERP(bat->prev_pos.x, bat->pos.x, ratio), .y = (int32_t)LERP(bat->prev_pos.y, bat->pos.y, ratio), .z = (int32_t)LERP(bat->prev_pos.z, bat->pos.z, ratio), } : bat->pos; const int16_t draw_angle = do_interp ? (Math_AngleMean(bat->prev_angle << 4, bat->angle << 4, ratio) >> 4) : bat->angle; const uint8_t draw_wing_y_off = do_interp ? M_GetInterpolatedWingYOffset(bat, ratio) : bat->wing_y_off; XYZ_32 world[5] = {}; Matrix_Push(); Matrix_TranslateAbs32(draw_pos); Matrix_RotY(draw_angle << 4); for (int32_t j = 0; j < 5; j++) { const XYZ_32 local = { .x = m_BatMesh[j].x, .y = m_BatMesh[j].y + M_GetWingYOffset(j, draw_wing_y_off), .z = m_BatMesh[j].z, }; world[j] = Matrix_MulVec32_M(g_WMatrixPtr, local); } Matrix_Pop(); for (size_t j = 0; j < ARRAY_SIZE(m_BatTriangles); j++) { const uint8_t *const tri = m_BatTriangles[j]; const XYZ_32 tri_world[3] = { world[tri[0]], world[tri[1]], world[tri[2]], }; OutputSource_PolyFX_StageTriExtUV( tri_world, p->draw.tri_uvw[j], p->draw.tri_tex_size[j], nullptr, tri_color, VERT_NO_LIGHTING | VERT_NO_WIBBLE, DRAW_BLEND); } } return true; } static void M_Update(M_PRIV *const p) { bool any_alive = false; for (int32_t i = 0; i < M_MAX_BATS; i++) { M_BAT *const bat = &p->bats[i]; if (!bat->active) { continue; } M_RememberBat(bat); if ((i & 3) == 0 && (Random_GetControl() & 7) == 0) { Sound_Effect(SFX_BATS_1, &bat->pos, SPM_NORMAL); } const int16_t angle = bat->angle << 4; const int32_t sin_v = Math_Sin(angle) >> 2; const int32_t cos_v = Math_Cos(angle) >> 2; bat->pos.x -= ((int64_t)bat->speed * cos_v) >> W2V_SHIFT; bat->pos.y -= Random_GetControl() & 3; bat->pos.z += ((int64_t)bat->speed * sin_v) >> W2V_SHIFT; bat->wing_y_off = (bat->wing_y_off + 11) & 0x3F; if (bat->life < 128) { bat->pos.y += -4 - (i >> 1); if ((Random_GetControl() & 3) == 0) { bat->angle = (bat->angle + (Random_GetControl() & 0xFF) - 128) & 0xFFF; bat->speed += Random_GetControl() & 3; } } bat->speed += 12; CLAMPG(bat->speed, 300); const int32_t time4 = Output_GetTimeInGame() * 4; if (bat->life != 0 && (time4 & 4) != 0) { bat->life--; if (bat->life == 0) { bat->active = false; } } if (bat->active) { any_alive = true; } } p->bats_alive = any_alive; } static void M_TriggerBats(M_PRIV *const p, const XYZ_32 pos, int16_t ang) { ang = (ang - 1024) & 0xFFF; for (int32_t i = 0; i < M_MAX_BATS; i++) { M_BAT *const bat = &p->bats[i]; bat->pos.x = pos.x + (Random_GetControl() & 0x1FF) - 256; bat->pos.y = pos.y - (Random_GetControl() & 0xFF) + 256; bat->pos.z = pos.z + (Random_GetControl() & 0x1FF) - 256; bat->angle = ((Random_GetControl() & 0x7F) + ang - 64) & 0xFFF; bat->speed = (Random_GetControl() & 0x1F) + 64; bat->wing_y_off = Random_GetControl() & 0x3F; bat->life = (Random_GetControl() & 7) + 144; bat->active = true; M_RememberBat(bat); } p->bats_alive = true; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (item->active == 0) { return; } M_PRIV *const p = item->priv; if (!p->bats_triggered) { M_TriggerBats(p, item->pos, item->rot.y >> 4); p->bats_triggered = true; } else { M_Update(p); } if (!p->bats_alive) { Item_Kill(item_num); } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->draw_func = M_Draw; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->save_flags = true; } REGISTER_OBJECT(O_BAT_EMITTER, M_Setup) ================================================ FILE: src/trx/game/objects/general/bell.c ================================================ #include #include typedef enum { BELL_STATE_STOP = 0, BELL_STATE_SWING = 1, } BELL_STATE; static bool M_ShouldSpawnBlood(const ITEM *const item) { return false; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); item->goal_anim_state = BELL_STATE_SWING; const SECTOR *const sector = Room_GetSector(item->pos, &item->room_num); item->floor = Room_GetHeight(sector, item->pos); Room_TestTriggers(item); Item_Animate(item); if (item->current_anim_state == BELL_STATE_STOP) { item->status = IS_INACTIVE; Item_RemoveActive(item_num); } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->collision_func = Object_Collision; obj->should_spawn_blood_func = M_ShouldSpawnBlood; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_BELL, M_Setup) ================================================ FILE: src/trx/game/objects/general/big_bowl.c ================================================ #include #include #include #include typedef enum { // clang-format off BIG_BOWL_STATE_TIP = 0, BIG_BOWL_STATE_POUR = 1, // clang-format on } BIG_BOWL_STATE; static void M_CreateHotLiquid(const ITEM *const bowl_item) { const int16_t effect_num = Effect_Create(bowl_item->room_num); const OBJECT *const obj = Object_Get(O_HOT_LIQUID); if (effect_num != NO_EFFECT) { EFFECT *const effect = Effect_Get(effect_num); effect->object_id = O_HOT_LIQUID; effect->pos.x = bowl_item->pos.x + STEP_L * 2; effect->pos.z = bowl_item->pos.z + STEP_L * 2; effect->pos.y = bowl_item->pos.y + STEP_L * 2 + 100; effect->room_num = bowl_item->room_num; effect->frame_num = (obj->mesh_count * Random_GetDraw()) >> 15; effect->fall_speed = 0; effect->shade = 2048; } } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (item->current_anim_state == BIG_BOWL_STATE_POUR) { M_CreateHotLiquid(item); item->timer++; if (item->timer == 5 * LOGIC_FPS && !Room_GetFlipStatus()) { // TODO: poorly hardcoded flimap number Room_SetFlipSlotFlags(4, IF_CODE_BITS | IF_ONE_SHOT); Room_FlipMap(); } } Item_Animate(item); if (item->status == IS_DEACTIVATED && item->timer >= LOGIC_FPS * 7) { Item_RemoveActive(item_num); } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_BIG_BOWL, M_Setup) ================================================ FILE: src/trx/game/objects/general/bird_tweeter.c ================================================ #include #include #include static void M_Control(const int16_t item_num) { const ITEM *const item = Item_Get(item_num); if (item->object_id == O_BIRD_TWEETER_2) { if (Random_GetDraw() < 1024) { Sound_Effect(SFX_BIRDS_CHIRP, &item->pos, SPM_NORMAL); } } else if (Random_GetDraw() < 256) { Sound_Effect(SFX_DRIPS_REVERB, &item->pos, SPM_NORMAL); } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->draw_func = nullptr; } REGISTER_OBJECT(O_BIRD_TWEETER_1, M_Setup) REGISTER_OBJECT(O_BIRD_TWEETER_2, M_Setup) ================================================ FILE: src/trx/game/objects/general/boat.c ================================================ #include typedef enum { BOAT_STATE_EMPTY = 0, BOAT_STATE_SET = 1, BOAT_STATE_MOVE = 2, BOAT_STATE_STOP = 3, } BOAT_STATE; static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); switch (item->current_anim_state) { case BOAT_STATE_SET: item->goal_anim_state = BOAT_STATE_MOVE; break; case BOAT_STATE_MOVE: item->goal_anim_state = BOAT_STATE_STOP; break; case BOAT_STATE_STOP: Item_Kill(item_num); break; } Item_Animate(item); } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->save_flags = true; obj->save_anim = true; obj->save_position = true; } REGISTER_OBJECT(O_MOTOR_BOAT, M_Setup) ================================================ FILE: src/trx/game/objects/general/bridge_common.c ================================================ #include #include #include #include bool Bridge_IsSameSector( const int32_t x, const int32_t z, const ITEM *const item) { const int32_t sector_x = x / WALL_L; const int32_t sector_z = z / WALL_L; const int32_t item_sector_x = item->pos.x / WALL_L; const int32_t item_sector_z = item->pos.z / WALL_L; return sector_x == item_sector_x && sector_z == item_sector_z; } int32_t Bridge_GetOffset( const ITEM *const item, int32_t x, int32_t y, int32_t z) { // Set the offset to the max value of 1023 if Lara is outside of the // bridge x/z position depending on its angle. This makes sure // the height is calculated properly for the front collision since // the low end of tilted bridges have a lower height. int32_t offset = 0; if (item->rot.y == 0) { if (g_Config.gameplay.fix_bridge_collision && x <= item->pos.x - WALL_L / 2) { offset = WALL_L - 1; } else { offset = (WALL_L - x) & (WALL_L - 1); } } else if (item->rot.y == -DEG_180) { if (g_Config.gameplay.fix_bridge_collision && x >= item->pos.x + WALL_L / 2) { offset = 0; } else { offset = x & (WALL_L - 1); } } else if (item->rot.y == DEG_90) { if (g_Config.gameplay.fix_bridge_collision && z >= item->pos.z + WALL_L / 2) { offset = WALL_L - 1; } else { offset = z & (WALL_L - 1); } } else { if (g_Config.gameplay.fix_bridge_collision && z <= item->pos.z - WALL_L / 2) { offset = 0; } else { offset = (WALL_L - z) & (WALL_L - 1); } // Fixes an edge case of an invisible wall on the tilt2 bridge floor. // The offset would get set to 0 on a specific z pos at the bottom of a // slope. The game would then set an invisible wall because it thought // Lara was at the high end of the tilt2 slope which is higher than a // step. This fix sets the offset to the max value (1023) when Lara's at // the bottom of the slope. if (g_Config.gameplay.fix_bridge_collision && offset == 0 && y < item->pos.y) { offset = (WALL_L - 1 - z) & (WALL_L - 1); } } return offset; } void Bridge_FixEmbeddedPosition(int16_t item_num) { // Some bridges at floor level are embedded into the floor. // This checks if bridges are below a room's floor level // and moves them up. ITEM *const item = Item_Get(item_num); const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item); const int16_t bridge_height = ABS(bounds->max.y) - ABS(bounds->min.y); int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector( (XYZ_32) { item->pos.x, item->pos.y - bridge_height, item->pos.z }, &room_num); const int32_t floor_height = Room_GetHeight(sector, item->pos); // Only move the bridge up if it's at floor level and there // isn't a room portal below. if (item->pos.y != floor_height || sector->portal_room.pit != NO_ROOM) { return; } item->pos.y = floor_height - bridge_height; } void Bridge_AddWalkable(const int16_t item_num) { const ITEM *const item = Item_Get(item_num); Walkable_Add(item_num, item->pos); } ================================================ FILE: src/trx/game/objects/general/bridge_common.h ================================================ #pragma once #include bool Bridge_IsSameSector(int32_t x, int32_t z, const ITEM *item); int32_t Bridge_GetOffset(const ITEM *item, int32_t x, int32_t y, int32_t z); void Bridge_FixEmbeddedPosition(int16_t item_num); void Bridge_AddWalkable(int16_t item_num); ================================================ FILE: src/trx/game/objects/general/bridge_flat.c ================================================ #include #include #include #include static int16_t M_GetFloorHeight( const ITEM *item, const int32_t x, const int32_t y, const int32_t z, const int16_t height) { if (g_Config.gameplay.fix_bridge_collision && !Bridge_IsSameSector(x, z, item)) { return height; } if (y > item->pos.y) { return height; } if (g_Config.gameplay.fix_bridge_collision && item->pos.y >= height) { return height; } return item->pos.y; } static int16_t M_GetCeilingHeight( const ITEM *item, const int32_t x, const int32_t y, const int32_t z, const int16_t height) { if (g_Config.gameplay.fix_bridge_collision && !Bridge_IsSameSector(x, z, item)) { return height; } if (y <= item->pos.y) { return height; } if (g_Config.gameplay.fix_bridge_collision && item->pos.y <= height) { return height; } return item->pos.y + STEP_L; } static void M_Initialise(const int16_t item_num) { Bridge_FixEmbeddedPosition(item_num); Walkable_AllocateNodes(Item_Get(item_num), 1); } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->floor_height_func = M_GetFloorHeight; obj->ceiling_height_func = M_GetCeilingHeight; obj->add_walkable_func = Bridge_AddWalkable; } REGISTER_OBJECT(O_BRIDGE_FLAT, M_Setup) ================================================ FILE: src/trx/game/objects/general/bridge_tilt1.c ================================================ #include #include #include #include static int16_t M_GetFloorHeight( const ITEM *const item, const int32_t x, const int32_t y, const int32_t z, const int16_t height) { if (g_Config.gameplay.fix_bridge_collision && !Bridge_IsSameSector(x, z, item)) { return height; } const int32_t offset_height = item->pos.y + (Bridge_GetOffset(item, x, y, z) / 4); if (y > offset_height || item->pos.y >= height) { return height; } if (g_Config.gameplay.fix_bridge_collision && item->pos.y >= height) { return height; } return offset_height; } static int16_t M_GetCeilingHeight( const ITEM *const item, const int32_t x, const int32_t y, const int32_t z, const int16_t height) { if (g_Config.gameplay.fix_bridge_collision && !Bridge_IsSameSector(x, z, item)) { return height; } const int32_t offset_height = item->pos.y + (Bridge_GetOffset(item, x, y, z) / 4); if (y <= offset_height) { return height; } if (g_Config.gameplay.fix_bridge_collision && item->pos.y <= height) { return height; } return offset_height + STEP_L; } static void M_Initialise(const int16_t item_num) { Bridge_FixEmbeddedPosition(item_num); Walkable_AllocateNodes(Item_Get(item_num), 1); } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->floor_height_func = M_GetFloorHeight; obj->ceiling_height_func = M_GetCeilingHeight; obj->add_walkable_func = Bridge_AddWalkable; } REGISTER_OBJECT(O_BRIDGE_TILT_1, M_Setup) ================================================ FILE: src/trx/game/objects/general/bridge_tilt2.c ================================================ #include #include #include #include int16_t M_GetFloorHeight( const ITEM *item, const int32_t x, const int32_t y, const int32_t z, const int16_t height) { if (g_Config.gameplay.fix_bridge_collision && !Bridge_IsSameSector(x, z, item)) { return height; } const int32_t offset_height = item->pos.y + (Bridge_GetOffset(item, x, y, z) / 2); if (y > offset_height) { return height; } if (g_Config.gameplay.fix_bridge_collision && item->pos.y >= height) { return height; } return offset_height; } int16_t M_GetCeilingHeight( const ITEM *item, const int32_t x, const int32_t y, const int32_t z, const int16_t height) { if (g_Config.gameplay.fix_bridge_collision && !Bridge_IsSameSector(x, z, item)) { return height; } const int32_t offset_height = item->pos.y + (Bridge_GetOffset(item, x, y, z) / 2); if (y <= offset_height) { return height; } if (g_Config.gameplay.fix_bridge_collision && item->pos.y <= height) { return height; } return offset_height + STEP_L; } static void M_Initialise(const int16_t item_num) { Bridge_FixEmbeddedPosition(item_num); Walkable_AllocateNodes(Item_Get(item_num), 1); } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->floor_height_func = M_GetFloorHeight; obj->ceiling_height_func = M_GetCeilingHeight; obj->add_walkable_func = Bridge_AddWalkable; } REGISTER_OBJECT(O_BRIDGE_TILT_2, M_Setup) ================================================ FILE: src/trx/game/objects/general/cabin.c ================================================ #include #include typedef enum { CABIN_STATE_START = 0, CABIN_STATE_DROP_1 = 1, CABIN_STATE_DROP_2 = 2, CABIN_STATE_DROP_3 = 3, CABIN_STATE_FINISH = 4, } CABIN_STATE; static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if ((item->flags & IF_CODE_BITS) == IF_CODE_BITS) { switch (item->current_anim_state) { case CABIN_STATE_START: item->goal_anim_state = CABIN_STATE_DROP_1; break; case CABIN_STATE_DROP_1: item->goal_anim_state = CABIN_STATE_DROP_2; break; case CABIN_STATE_DROP_2: item->goal_anim_state = CABIN_STATE_DROP_3; break; } item->flags = 0; } if (item->current_anim_state == CABIN_STATE_FINISH) { Room_SetFlipSlotFlags(3, IF_CODE_BITS); Room_FlipMap(); Item_Kill(item_num); } Item_Animate(item); } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->draw_func = Object_DrawUnclippedItem; obj->collision_func = Object_Collision; obj->save_anim = true; obj->save_flags = true; } REGISTER_OBJECT(O_PORTACABIN, M_Setup) ================================================ FILE: src/trx/game/objects/general/camera_target.c ================================================ #include static void M_Setup(OBJECT *const obj) { obj->draw_func = nullptr; } REGISTER_OBJECT(O_CAMERA_TARGET, M_Setup) ================================================ FILE: src/trx/game/objects/general/carcass.c ================================================ #include #include #include #include // clang-format off #define M_ROLL_SHIFT 1 #define M_ROLL_SHIFT_UW 3 #define M_MAX_ROLL 0x6000 #define M_ACCEL 8 #define M_ACCEL_UW 1 #define M_MAX_SPEED (STEP_L * 2) // = 512 #define M_MAX_SPEED_UW (STEP_L / 4) // = 64 #define M_INITIAL_SPEED_UW (STEP_L / 16) // = 16 // clang-format on static void M_SpawnSplash(const ITEM *const item) { const ROOM *const room = Room_Get(item->room_num); const FX_WATER_SPLASH_SETUP setup = { .x = item->pos.x, .y = room->max_ceiling, .z = item->pos.z, .inner_xz_off = 16, .inner_xz_size = 16, .inner_y_size = -96, .inner_xz_vel = 160, .inner_y_vel = (int16_t)(-72 * item->fall_speed), .inner_gravity = 128, .inner_friction = 7, .middle_xz_off = 24, .middle_xz_size = 32, .middle_y_size = -64, .middle_xz_vel = 224, .middle_y_vel = (int16_t)(-36 * item->fall_speed), .middle_gravity = 72, .middle_friction = 8, .outer_xz_off = 32, .outer_xz_size = 32, .outer_xz_vel = 272, .outer_friction = 9, }; FX_Water_SetupSplash(&setup); } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (item->status != IS_ACTIVE) { return; } item->pos.y += item->fall_speed; const bool was_underwater = Room_Get(item->room_num)->flags.underwater; int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); Item_UpdateRoom(item_num, room_num); const int16_t height = Room_GetHeight(sector, item->pos) - M_MAX_SPEED_UW; if (item->pos.y >= height) { item->pos.y = height; item->fall_speed = 0; // TODO: this is tied to the slope the carcass lands on in Crash Site item->rot.z = M_MAX_ROLL; return; } const ROOM *const current_room = Room_Get(room_num); const bool is_underwater = current_room->flags.underwater; item->rot.z += item->fall_speed << (is_underwater ? M_ROLL_SHIFT_UW : M_ROLL_SHIFT); item->fall_speed += is_underwater ? M_ACCEL_UW : M_ACCEL; CLAMPG(item->rot.z, M_MAX_ROLL); CLAMPG(item->fall_speed, was_underwater ? M_MAX_SPEED_UW : M_MAX_SPEED); if (is_underwater && !was_underwater) { M_SpawnSplash(item); item->fall_speed = M_INITIAL_SPEED_UW; item->pos.y = current_room->max_ceiling + 1; } } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->collision_func = Object_Collision; obj->save_position = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_CARCASS, M_Setup) ================================================ FILE: src/trx/game/objects/general/clock_chimes.c ================================================ #include #include #include static void M_DoChimeSound(const ITEM *const item) { const ITEM *const lara_item = Lara_GetItem(); XYZ_32 pos = lara_item->pos; pos.x += (item->pos.x - lara_item->pos.x) >> 6; pos.y += (item->pos.y - lara_item->pos.y) >> 6; pos.z += (item->pos.z - lara_item->pos.z) >> 6; Sound_Effect(SFX_DOOR_CHIME, &pos, SPM_NORMAL); } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (item->timer == 0) { return; } if (item->timer % 60 == 59) { M_DoChimeSound(item); } item->timer--; if (item->timer == 0) { M_DoChimeSound(item); item->timer = -1; Item_RemoveActive(item_num); item->status = IS_INACTIVE; item->flags &= ~IF_CODE_BITS; } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->draw_func = nullptr; obj->save_flags = true; } REGISTER_OBJECT(O_CLOCK_CHIMES, M_Setup) ================================================ FILE: src/trx/game/objects/general/cog.c ================================================ #include #include typedef enum { COG_STATE_INACTIVE = 0, COG_STATE_ACTIVE = 1, } COG_STATE; static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (Item_IsTriggerActive(item)) { item->goal_anim_state = COG_STATE_ACTIVE; } else { item->goal_anim_state = COG_STATE_INACTIVE; } Item_Animate(item); int16_t room_num = item->room_num; Room_GetSector(item->pos, &room_num); Item_UpdateRoom(item_num, room_num); } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->save_flags = true; } REGISTER_OBJECT(O_COG_1, M_Setup) REGISTER_OBJECT(O_COG_2, M_Setup) REGISTER_OBJECT(O_COG_3, M_Setup) ================================================ FILE: src/trx/game/objects/general/combat_end.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #define M_CUTSCENE_DELAY (5 * LOGIC_FPS) // = 150 #define M_BOSS_TYPE O_CULT_3 static int16_t m_BossTimer = 0; static uint16_t m_BossCount = 0; static int32_t M_CountAliveEnemies(void) { int32_t count = 0; for (int32_t i = 0; i < Item_GetLevelCount(); i++) { const ITEM *const item = Item_Get(i); if (item->object_id != M_BOSS_TYPE && Item_IsAlive(item) && Creature_IsHostile(item)) { count++; } } return count; } static bool M_IsBossDead(void) { for (int32_t i = 0; i < Item_GetLevelCount(); i++) { const ITEM *const item = Item_Get(i); if (item->object_id == M_BOSS_TYPE && !Item_IsAlive(item)) { return true; } } return false; } static int16_t M_FindNearestBoss(void) { // Note that in the original, the first boss item was always selected here. // For speedruns, the change here means that is no longer guaranteed, but // positional manipulation can be used for the best outcome. int32_t best_dist = INT32_MAX; int16_t best_item = NO_ITEM; for (int32_t i = 0; i < Item_GetLevelCount(); i++) { const ITEM *const item = Item_Get(i); if (item->object_id != M_BOSS_TYPE) { continue; } if (item->status == IS_ACTIVE || item->status == IS_DEACTIVATED) { best_item = i; break; } const ITEM *const lara_item = Lara_GetItem(); const GAME_VECTOR start = { .x = lara_item->pos.x, .y = lara_item->pos.y - STEP_L * 2, .z = lara_item->pos.z, .room_num = lara_item->room_num, }; GAME_VECTOR target = { .x = item->pos.x, .y = item->pos.y - STEP_L * 2, .z = item->pos.z, .room_num = item->room_num, }; if (!LOS_Check(&start, &target, true)) { const int32_t dx = (lara_item->pos.x - item->pos.x) >> 6; const int32_t dy = (lara_item->pos.y - item->pos.y) >> 6; const int32_t dz = (lara_item->pos.z - item->pos.z) >> 6; const int32_t dist = SQUARE(dx) + SQUARE(dy) + SQUARE(dz); if (dist < best_dist) { best_dist = dist; best_item = i; } } } return best_item; } static void M_ActivateNearestBoss(void) { const int16_t item_num = M_FindNearestBoss(); if (item_num == NO_ITEM) { return; } ITEM *const item = Item_Get(item_num); if (item->status != IS_ACTIVE && item->status != IS_DEACTIVATED) { item->touch_bits = 0; item->status = IS_ACTIVE; item->mesh_bits = 0xFFFF1FFF; Item_AddActive(item_num); LOT_EnableBaddieAI(item_num, true); } } static void M_PrepareCutscene(const int16_t item_num) { LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->gun_type == LGT_FLARE) { Lara_Flare_Undraw(); lara->flare.control = false; lara->left_arm.lock = false; } Lara_Vehicle_Dismount(); Gun_SetLaraHandLMesh(LGT_UNARMED); Gun_SetLaraHandRMesh(LGT_UNARMED); lara->water_status = LWS_ABOVE_WATER; lara->target = nullptr; ITEM *const item = Item_Get(item_num); Creature_SpecialKill(item, 0, 0, LS_EXTRA_END_HOUSE); Camera_InvokeCinematic(item, 428, 0); } static void M_Control(const int16_t item_num) { const int32_t alive_enemies = M_CountAliveEnemies(); const int32_t is_boss_dead = M_IsBossDead(); if (alive_enemies == 0 && m_BossTimer == 0) { m_BossTimer = 1; M_ActivateNearestBoss(); } else if (alive_enemies == 0 && is_boss_dead) { m_BossTimer++; if (m_BossTimer == M_CUTSCENE_DELAY) { M_PrepareCutscene(item_num); } } } OBJECT_ID CombatEnd_GetBossType(void) { return M_BOSS_TYPE; } bool CombatEnd_IsWaitingForBoss(void) { for (int32_t i = 0; i < Item_GetTotalCount(); i++) { if (Item_Get(i)->object_id == O_COMBAT_END) { return m_BossTimer == 0; } } return false; } bool CombatEnd_IsComplete(void) { return m_BossTimer >= M_CUTSCENE_DELAY; } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->draw_func = nullptr; obj->save_flags = true; m_BossTimer = 0; } REGISTER_OBJECT(O_COMBAT_END, M_Setup) ================================================ FILE: src/trx/game/objects/general/combat_end.h ================================================ #pragma once #include OBJECT_ID CombatEnd_GetBossType(void); bool CombatEnd_IsWaitingForBoss(void); bool CombatEnd_IsComplete(void); ================================================ FILE: src/trx/game/objects/general/copter.c ================================================ #include #include #include #include typedef enum { // clang-format off COPTER_STATE_EMPTY = 0, COPTER_STATE_SPIN = 1, COPTER_STATE_TAKEOFF = 2, // clang-format on } COPTER_STATE; static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); const ITEM *const lara_item = Lara_GetItem(); if (item->current_anim_state == COPTER_STATE_SPIN && (item->flags & IF_ONE_SHOT)) { item->goal_anim_state = COPTER_STATE_TAKEOFF; } Item_Animate(item); int16_t room_num = item->room_num; Room_GetSector(item->pos, &room_num); Item_UpdateRoom(item_num, room_num); const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item); XYZ_32 pos = { .x = (bounds->min.x + bounds->max.x) / 2, .y = (bounds->min.y + bounds->max.y) / 2, .z = (bounds->min.z + bounds->max.z) / 2, }; pos.x = lara_item->pos.x + ((pos.x - lara_item->pos.x) >> 2); pos.y = lara_item->pos.y + ((pos.y - lara_item->pos.y) >> 2); pos.z = lara_item->pos.z + ((pos.z - lara_item->pos.z) >> 2); Sound_Effect(SFX_HELICOPTER_LOOP, &pos, SPM_NORMAL); if (item->status == IS_DEACTIVATED) { Item_Kill(item_num); } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_COPTER, M_Setup) ================================================ FILE: src/trx/game/objects/general/cutscene_player.c ================================================ #include #include #include #include #include #include #include #include static void M_Initialise(const int16_t item_num) { Item_AddActive(item_num); ITEM *const item = Item_Get(item_num); item->rot.y = 0; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); item->status = IS_ACTIVE; CAMERA_INFO *const camera = Cutscene_GetCamera(); item->rot.y = camera->target_angle; item->pos = camera->pos.pos; XYZ_32 pos = {}; Collide_GetJointAbsPosition(item, &pos, 0); const int16_t room_num = Room_GetIndexFromPos(pos); if (room_num != NO_ROOM) { Item_UpdateRoom(item_num, room_num); } int16_t floor_room_num = item->room_num; const SECTOR *const sector = Room_GetSector(pos, &floor_room_num); const int32_t height = Room_GetHeight(sector, pos); item->floor = height == NO_HEIGHT ? pos.y : height; if (item->dynamic_light && item->status != IS_INVISIBLE) { pos.x = 0; pos.y = 0; pos.z = 0; Collide_GetJointAbsPosition(item, &pos, 0); Output_AddDynamicLight(pos, 12, 11); } Item_Animate(item); } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->shadow_size = (UNIT_SHADOW * 10) / 16; obj->control_func = M_Control; obj->hit_points = 1; } REGISTER_OBJECT(O_PLAYER_1, M_Setup) REGISTER_OBJECT(O_PLAYER_2, M_Setup) REGISTER_OBJECT(O_PLAYER_3, M_Setup) REGISTER_OBJECT(O_PLAYER_4, M_Setup) REGISTER_OBJECT(O_PLAYER_5, M_Setup) REGISTER_OBJECT(O_PLAYER_6, M_Setup) REGISTER_OBJECT(O_PLAYER_7, M_Setup) REGISTER_OBJECT(O_PLAYER_8, M_Setup) REGISTER_OBJECT(O_PLAYER_9, M_Setup) REGISTER_OBJECT(O_PLAYER_10, M_Setup) ================================================ FILE: src/trx/game/objects/general/detonator_box.c ================================================ #include #include #include #include #include #include #include #include #include #define M_EXPLOSION_ACTION_FRAME 80 static XYZ_32 m_Position = { .x = 0, .y = 0, .z = 0 }; static const OBJECT_BOUNDS m_Bounds = { .shift = { .min = { .x = -WALL_L / 4, .y = -100, .z = -WALL_L / 4, }, .max = { .x = +WALL_L / 4, .y = +100, .z = +WALL_L / 4, }, }, .rot = { .min = { .x = -10 * DEG_1, .y = 0, .z = 0, }, .max = { .x = +10 * DEG_1, .y = 0, .z = 0, }, }, .ignore_rot = true, }; static void M_ConsumeKeyItem(ITEM *const receptacle_item) { const OBJECT_ID key_object_id = Object_FindReceptacleKey(receptacle_item->object_id); if (key_object_id != NO_OBJECT) { Inv_RemoveItem(key_object_id); } } static void M_Use(ITEM *const lara_item, ITEM *const receptacle_item) { receptacle_item->rot.y = lara_item->rot.y; Lara_AlignPosition(receptacle_item, &m_Position); Lara_SwitchToExtraState(LS_EXTRA_PLUNGER); if (Item_TestFrameEqual(lara_item, 0)) { M_ConsumeKeyItem(receptacle_item); } receptacle_item->status = IS_ACTIVE; Item_AddActive(Item_GetIndex(receptacle_item)); LARA_INFO *const lara = Lara_GetLaraInfo(); lara->interact_target.is_moving = false; lara->interact_target.item_num = NO_ITEM; } static const OBJECT_BOUNDS *M_Bounds(void) { return &m_Bounds; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); Item_Animate(item); if (item->dynamic_light) { Output_AddDynamicLight(item->pos, 13, 11); } if (Item_TestFrameEqual(item, M_EXPLOSION_ACTION_FRAME)) { g_Camera.bounce = -150; Sound_Effect(SFX_EXPLOSION_1, nullptr, SPM_ALWAYS); } if (item->status == IS_DEACTIVATED) { Item_RemoveActive(item_num); } } static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { const LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->extra_anim) { return; } ITEM *const item = Item_Get(item_num); const OBJECT *const obj = Object_Get(item->object_id); if (lara->interact_target.is_moving && lara->interact_target.item_num == item_num) { M_Use(lara_item, item); return; } if (item->status != IS_INACTIVE || !g_Input.action || lara->gun_status != LGS_ARMLESS || lara_item->gravity || lara_item->current_anim_state != LS(LS_STOP)) { goto normal_collision; } const XYZ_16 old_rot = item->rot; item->rot.x = 0; item->rot.y = lara_item->rot.y; item->rot.z = 0; if (!Lara_TestPosition(item, obj->bounds_func())) { item->rot = old_rot; goto normal_collision; } item->rot = old_rot; if (!GF_ShowInventoryKeys(item->object_id)) { Lara_RefuseInteraction(); } return; normal_collision: Object_Collision(item_num, lara_item, coll); } static void M_Setup(OBJECT *const obj) { obj->collision_func = M_Collision; obj->control_func = M_Control; obj->bounds_func = M_Bounds; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_DETONATOR_BOX, M_Setup) ================================================ FILE: src/trx/game/objects/general/ding_dong.c ================================================ #include #include static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if ((item->flags & IF_CODE_BITS) == IF_CODE_BITS) { Sound_Effect(SFX_DOORBELL, &item->pos, SPM_NORMAL); item->flags -= IF_CODE_BITS; } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->draw_func = nullptr; } REGISTER_OBJECT(O_DING_DONG, M_Setup) ================================================ FILE: src/trx/game/objects/general/disposable_animating.c ================================================ #include #include static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (!Item_IsTriggerActive(item)) { Item_Kill(item_num); return; } Item_Animate(item); int16_t room_num = item->room_num; Room_GetSector(item->pos, &room_num); Item_UpdateRoom(item_num, room_num); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->save_position = true; obj->save_flags = true; } REGISTER_OBJECT(O_DISPOSABLE_ANIMATING_1, M_Setup) REGISTER_OBJECT(O_DISPOSABLE_ANIMATING_2, M_Setup) REGISTER_OBJECT(O_DISPOSABLE_ANIMATING_3, M_Setup) REGISTER_OBJECT(O_DISPOSABLE_ANIMATING_4, M_Setup) REGISTER_OBJECT(O_DISPOSABLE_ANIMATING_5, M_Setup) REGISTER_OBJECT(O_DISPOSABLE_ANIMATING_6, M_Setup) REGISTER_OBJECT(O_DISPOSABLE_ANIMATING_7, M_Setup) REGISTER_OBJECT(O_DISPOSABLE_ANIMATING_8, M_Setup) REGISTER_OBJECT(O_DISPOSABLE_ANIMATING_9, M_Setup) REGISTER_OBJECT(O_DISPOSABLE_ANIMATING_10, M_Setup) ================================================ FILE: src/trx/game/objects/general/door.c ================================================ #include #include #include #include #include typedef struct { SECTOR *sector; SECTOR old_sector; int16_t box_num; } M_DOOR_POS; typedef struct { M_DOOR_POS d1; M_DOOR_POS d1flip; M_DOOR_POS d2; M_DOOR_POS d2flip; } M_PRIV; static const SECTOR m_BlockedSector = { .idx = 0, .box = NO_BOX, .ceiling.height = NO_HEIGHT, .floor.height = NO_HEIGHT, .ceiling.tilt = {}, .floor.tilt = {}, .portal_room.sky = NO_ROOM, .portal_room.pit = NO_ROOM, .portal_room.wall = NO_ROOM, }; static SECTOR *M_GetRoomRelSector( const ROOM *const room, const ITEM *item, const int32_t sector_dx, const int32_t sector_dz) { const XZ_32 sector = { .x = ((item->pos.x - room->pos.x) >> WALL_SHIFT) + sector_dx, .z = ((item->pos.z - room->pos.z) >> WALL_SHIFT) + sector_dz, }; return Room_GetUnitSector(room, sector.x, sector.z); } static bool M_LaraDoorCollision(const SECTOR *const sector) { // Check if Lara is on the same tile as the invisible block. const ITEM *const lara = Lara_GetItem(); if (lara == nullptr) { return false; } int16_t room_num = lara->room_num; const SECTOR *const lara_sector = Room_GetSector(lara->pos, &room_num); return lara_sector == sector; } static void M_CopySectorProperties( const SECTOR *const source_sector, SECTOR *const target_sector) { target_sector->idx = source_sector->idx; target_sector->box = source_sector->box; target_sector->ceiling.height = source_sector->ceiling.height; target_sector->floor.height = source_sector->floor.height; target_sector->floor.tilt = source_sector->floor.tilt; target_sector->ceiling.tilt = source_sector->ceiling.tilt; target_sector->portal_room.sky = source_sector->portal_room.sky; target_sector->portal_room.pit = source_sector->portal_room.pit; target_sector->portal_room.wall = source_sector->portal_room.wall; } static void M_Open(M_DOOR_POS *const d) { if (d->sector == nullptr) { return; } M_CopySectorProperties(&d->old_sector, d->sector); const int16_t box_num = d->box_num; if (box_num != NO_BOX) { Box_GetBox(box_num)->overlap_index &= ~BOX_BLOCKED; } } static void M_Check(M_DOOR_POS *const d) { // Forcefully remove the invisible block if Lara happens to occupy the same // tile. This ensures that Lara doesn't void if a timed door happens to // close right on her, or the player loads the game while standing on a // closed door's block tile. if (M_LaraDoorCollision(d->sector)) { M_Open(d); } } static void M_Shut(M_DOOR_POS *const d) { if (d->sector == nullptr) { return; } M_CopySectorProperties(&m_BlockedSector, d->sector); const int16_t box_num = d->box_num; if (box_num != NO_BOX) { Box_GetBox(box_num)->overlap_index |= BOX_BLOCKED; } } static void M_InitialisePortal( const ROOM *const room, const ITEM *const item, const int32_t sector_dx, const int32_t sector_dz, M_DOOR_POS *const door_pos) { door_pos->sector = M_GetRoomRelSector(room, item, sector_dx, sector_dz); const SECTOR *sector = door_pos->sector; const int16_t room_num = door_pos->sector->portal_room.wall; if (room_num != NO_ROOM) { sector = M_GetRoomRelSector(Room_Get(room_num), item, sector_dx, sector_dz); } int16_t box_num = sector->box; const BOX_INFO *const box = Box_GetBox(box_num); if ((box->overlap_index & BOX_BLOCKABLE) == 0) { box_num = NO_BOX; } door_pos->box_num = box_num; door_pos->old_sector = *door_pos->sector; } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; int32_t dx = 0; int32_t dz = 0; if (item->rot.y == 0) { dz = -1; } else if (item->rot.y == -DEG_180) { dz = 1; } else if (item->rot.y == DEG_90) { dx = -1; } else { dx = 1; } int16_t room_num = item->room_num; const ROOM *room = Room_Get(room_num); M_InitialisePortal(room, item, dx, dz, &p->d1); if (room->flipped_room == NO_ROOM) { p->d1flip.sector = nullptr; } else { room = Room_Get(room->flipped_room); M_InitialisePortal(room, item, dx, dz, &p->d1flip); } room_num = p->d1.sector->portal_room.wall; M_Shut(&p->d1); M_Shut(&p->d1flip); if (room_num == NO_ROOM) { p->d2.sector = nullptr; p->d2flip.sector = nullptr; } else { room = Room_Get(room_num); M_InitialisePortal(room, item, 0, 0, &p->d2); if (room->flipped_room == NO_ROOM) { p->d2flip.sector = nullptr; } else { room = Room_Get(room->flipped_room); M_InitialisePortal(room, item, 0, 0, &p->d2flip); } M_Shut(&p->d2); M_Shut(&p->d2flip); const int16_t prev_room = item->room_num; Item_UpdateRoom(item_num, room_num); item->room_num = prev_room; } } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; if (Item_IsTriggerActive(item)) { if (item->current_anim_state == DOOR_STATE_CLOSED) { item->goal_anim_state = DOOR_STATE_OPEN; } else { M_Open(&p->d1); M_Open(&p->d2); M_Open(&p->d1flip); M_Open(&p->d2flip); } } else { if (item->current_anim_state == DOOR_STATE_OPEN) { item->goal_anim_state = DOOR_STATE_CLOSED; } else { M_Shut(&p->d1); M_Shut(&p->d2); M_Shut(&p->d1flip); M_Shut(&p->d2flip); } } M_Check(&p->d1); M_Check(&p->d2); M_Check(&p->d1flip); M_Check(&p->d2flip); Item_Animate(item); } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->draw_func = Object_DrawUnclippedItem; obj->collision_func = Door_Collision; obj->priv_size = sizeof(M_PRIV); obj->save_flags = true; obj->save_anim = true; } void Door_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); if (!Item_TestBoundsCollide(item, lara_item, coll->radius)) { return; } if (!Collide_TestCollision(item, lara_item)) { return; } if (coll->enable_baddie_push) { Lara_Col_ItemPush( item, coll, coll->enable_hit && item->current_anim_state != item->goal_anim_state, true); } } REGISTER_OBJECT(O_DOOR_TYPE_1, M_Setup) REGISTER_OBJECT(O_DOOR_TYPE_2, M_Setup) REGISTER_OBJECT(O_DOOR_TYPE_3, M_Setup) REGISTER_OBJECT(O_DOOR_TYPE_4, M_Setup) REGISTER_OBJECT(O_DOOR_TYPE_5, M_Setup) REGISTER_OBJECT(O_DOOR_TYPE_6, M_Setup) REGISTER_OBJECT(O_DOOR_TYPE_7, M_Setup) REGISTER_OBJECT(O_DOOR_TYPE_8, M_Setup) ================================================ FILE: src/trx/game/objects/general/door.h ================================================ #pragma once #include typedef enum { DOOR_STATE_CLOSED = 0, DOOR_STATE_OPEN = 1, } DOOR_STATE; void Door_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); ================================================ FILE: src/trx/game/objects/general/drawbridge.c ================================================ #include #include #include #include #include #include #include #include typedef enum { DRAWBRIDGE_STATE_CLOSED = DOOR_STATE_CLOSED, DRAWBRIDGE_STATE_OPEN = DOOR_STATE_OPEN, } DRAWBRIDGE_STATE; typedef enum { DRAWBRIDGE_ANIM_CLOSED = 3, } DRAWBRIDGE_ANIM; static bool M_IsItemOnTop(const ITEM *item, int32_t x, int32_t z) { int32_t ix = item->pos.x >> WALL_SHIFT; int32_t iz = item->pos.z >> WALL_SHIFT; x >>= WALL_SHIFT; z >>= WALL_SHIFT; if (item->rot.y == 0 && x == ix && (z == iz - 1 || z == iz - 2)) { return true; } else if ( item->rot.y == -DEG_180 && x == ix && (z == iz + 1 || z == iz + 2)) { return true; } else if ( item->rot.y == DEG_90 && z == iz && (x == ix - 1 || x == ix - 2)) { return true; } else if ( item->rot.y == -DEG_90 && z == iz && (x == ix + 1 || x == ix + 2)) { return true; } return false; } static int16_t M_GetFloorHeight( const ITEM *item, const int32_t x, const int32_t y, const int32_t z, const int16_t height) { if (item->current_anim_state != DRAWBRIDGE_STATE_OPEN) { return height; } else if (!M_IsItemOnTop(item, x, z)) { return height; } else if (y > item->pos.y) { return height; } else if ( g_Config.gameplay.fix_bridge_collision && item->pos.y >= height) { return height; } return item->pos.y; } static int16_t M_GetCeilingHeight( const ITEM *item, const int32_t x, const int32_t y, const int32_t z, const int16_t height) { if (item->current_anim_state != DRAWBRIDGE_STATE_OPEN) { return height; } else if (!M_IsItemOnTop(item, x, z)) { return height; } else if (y <= item->pos.y) { return height; } else if ( g_Config.gameplay.fix_bridge_collision && item->pos.y <= height) { return height; } return item->pos.y + STEP_L; } static BOUNDS_16 M_RotateBounds(const BOUNDS_16 bounds, int16_t rot_y) { BOUNDS_16 rot_bounds = {}; switch (rot_y) { case 0: default: rot_bounds = bounds; break; case DEG_90: rot_bounds.min.x = bounds.min.z; rot_bounds.max.x = bounds.max.z; rot_bounds.min.z = -bounds.max.x; rot_bounds.max.z = -bounds.min.x; break; case -DEG_180: rot_bounds.min.x = -bounds.max.x; rot_bounds.max.x = -bounds.min.x; rot_bounds.min.z = -bounds.max.z; rot_bounds.max.z = -bounds.min.z; break; case -DEG_90: rot_bounds.min.x = -bounds.max.z; rot_bounds.max.x = -bounds.min.z; rot_bounds.min.z = bounds.min.x; rot_bounds.max.z = bounds.max.x; break; } return rot_bounds; } static void M_GetSectorPositions(const ITEM *const item, VECTOR *sector_pos) { const OBJECT *const obj = Object_Get(item->object_id); const ANIM_FRAME *const frame = Object_GetAnim(obj, DRAWBRIDGE_ANIM_CLOSED)->frame_ptr; const BOUNDS_16 rot_bounds = M_RotateBounds(frame->bounds, item->rot.y); const int32_t x0 = item->pos.x + rot_bounds.min.x; const int32_t x1 = item->pos.x + rot_bounds.max.x - 1; // inclusive const int32_t z0 = item->pos.z + rot_bounds.min.z; const int32_t z1 = item->pos.z + rot_bounds.max.z - 1; const int32_t sx0 = Math_FloorDiv(x0, WALL_L); const int32_t sx1 = Math_FloorDiv(x1, WALL_L); const int32_t sz0 = Math_FloorDiv(z0, WALL_L); const int32_t sz1 = Math_FloorDiv(z1, WALL_L); // Sector of the drawbridge original position. const int32_t sx_orig = Math_FloorDiv(item->pos.x, WALL_L); const int32_t sz_orig = Math_FloorDiv(item->pos.z, WALL_L); for (int32_t sx = sx0; sx <= sx1; ++sx) { for (int32_t sz = sz0; sz <= sz1; ++sz) { if (sx == sx_orig && sz == sz_orig) { // Skip the original sector since it's WALL_L away. continue; } const XYZ_32 pos = { .x = sx * WALL_L + WALL_L / 2, .y = item->pos.y, .z = sz * WALL_L + WALL_L / 2, }; Vector_Add(sector_pos, &pos); } } } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); VECTOR *const positions = Vector_Create(sizeof(XYZ_32)); M_GetSectorPositions(item, positions); Walkable_AllocateNodes(item, positions->count); Vector_Free(positions); } static void M_DropStack(const ITEM *const item) { VECTOR *positions = Vector_Create(sizeof(XYZ_32)); M_GetSectorPositions(item, positions); for (int32_t i = 0; i < positions->count; i++) { MovableBlock_DropStack( *(const XYZ_32 *)Vector_Get(positions, i), item->room_num); } Vector_Free(positions); } static void M_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll) { const ITEM *const item = Item_Get(item_num); if (item->current_anim_state == DRAWBRIDGE_STATE_CLOSED) { Door_Collision(item_num, lara_item, coll); } } static void M_Control(int16_t item_num) { ITEM *const item = Item_Get(item_num); if (Item_IsTriggerActive(item)) { item->goal_anim_state = DRAWBRIDGE_STATE_OPEN; } else { item->goal_anim_state = DRAWBRIDGE_STATE_CLOSED; if (item->current_anim_state == DRAWBRIDGE_STATE_OPEN) { M_DropStack(item); } } Item_Animate(item); int16_t room_num = item->room_num; Room_GetSector(item->pos, &room_num); Item_UpdateRoom(item_num, room_num); } static void M_AddWalkable(const int16_t item_num) { const ITEM *const item = Item_Get(item_num); VECTOR *positions = Vector_Create(sizeof(XYZ_32)); M_GetSectorPositions(item, positions); for (int32_t i = 0; i < positions->count; i++) { Walkable_Add(item_num, *(const XYZ_32 *)Vector_Get(positions, i)); } Vector_Free(positions); } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->ceiling_height_func = M_GetCeilingHeight; obj->collision_func = M_Collision; obj->control_func = M_Control; obj->save_anim = true; obj->save_flags = true; obj->floor_height_func = M_GetFloorHeight; obj->add_walkable_func = M_AddWalkable; } REGISTER_OBJECT(O_DRAWBRIDGE, M_Setup) ================================================ FILE: src/trx/game/objects/general/dummy.c ================================================ #include static void M_Setup(OBJECT *const obj) { obj->draw_func = nullptr; } REGISTER_OBJECT(O_DUMMY, M_Setup) ================================================ FILE: src/trx/game/objects/general/earthquake.c ================================================ #include #include #include #include #include #include #include typedef struct { int32_t shake_intensity; int32_t target_intensity; int32_t target_timer; } M_PRIV; static void M_ActivateRelatedItem(ITEM *const earth_item) { Item_AddActive(Item_GetIndex(earth_item)); earth_item->status = IS_ACTIVE; earth_item->flags = IF_CODE_BITS; earth_item->timer = 0; } static void M_FindAndActivateRelatedItems(const ITEM *const item) { OBJECT_ID object_id_to_activate = NO_OBJECT; const int32_t random = Random_GetControl(); if (random < 512) { object_id_to_activate = O_FLAME_EMITTER; } else if (random < 1024) { object_id_to_activate = O_FALLING_CEILING_1; } if (object_id_to_activate == NO_OBJECT || !Object_Get(object_id_to_activate)->loaded) { return; } int16_t related_item_num = Room_Get(item->room_num)->item_num; while (related_item_num != NO_ITEM) { ITEM *const earth_item = Item_Get(related_item_num); if (earth_item->object_id == object_id_to_activate && earth_item->status != IS_ACTIVE && earth_item->status != IS_DEACTIVATED) { M_ActivateRelatedItem(earth_item); break; } related_item_num = earth_item->next_item; } } static void M_Reset(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; p->shake_intensity = 0; p->target_intensity = 0; p->target_timer = 0; item->status = IS_INACTIVE; Item_RemoveActive(item_num); } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; if (!Item_IsTriggerActive(item)) { M_Reset(item_num); return; } switch (g_TRVersion) { case 1: if (Random_GetDraw() < 256) { g_Camera.bounce = -150; Sound_Effect(SFX_EARTHQUAKE_1, nullptr, SPM_NORMAL); } else if (Random_GetControl() < 1024) { g_Camera.bounce = 50; Sound_Effect(SFX_EARTHQUAKE_2, nullptr, SPM_NORMAL); } break; case 2: if (Random_GetDraw() < 512) { Sound_Effect(SFX_EARTHQUAKE_1, nullptr, SPM_NORMAL); g_Camera.bounce = -200; } break; case 3: { if (p->target_intensity == 0) { p->target_intensity = 100; } if (p->target_timer == 0 && ABS(p->shake_intensity - p->target_intensity) < 16) { if (p->target_intensity == 20) { p->target_intensity = 100; p->target_timer = (Random_GetControl() & 0x7F) + 90; } else { p->target_intensity = 20; p->target_timer = (Random_GetControl() & 0x7F) + 30; } } if (p->target_timer != 0) { p->target_timer--; } if (p->shake_intensity > p->target_intensity) { p->shake_intensity -= (Random_GetControl() & 7) + 2; } else { p->shake_intensity += (Random_GetControl() & 7) + 2; } Sound_Effect( SFX_EARTHQUAKE_LOOP, nullptr, ((p->shake_intensity << 16) + 0x1000000) | SPM_PITCH); g_Camera.bounce = -p->shake_intensity; break; } } M_FindAndActivateRelatedItems(item); } static void M_Setup(OBJECT *const obj) { obj->priv_size = sizeof(M_PRIV); obj->control_func = M_Control; obj->draw_func = nullptr; obj->save_flags = true; } REGISTER_OBJECT(O_EARTHQUAKE, M_Setup) ================================================ FILE: src/trx/game/objects/general/final_cutscene.c ================================================ #include #include #include #include #include #define M_CUTSCENE_DURATION (15 * LOGIC_FPS) #define M_FADE_DURATION (3 * LOGIC_FPS) static int32_t m_FadeTimer = -1; static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (CombatEnd_IsComplete()) { item->status = IS_ACTIVE; Item_Animate(item); if (m_FadeTimer == -1) { m_FadeTimer = M_CUTSCENE_DURATION; } else if (m_FadeTimer > 0) { m_FadeTimer--; } } else { item->status = IS_INVISIBLE; m_FadeTimer = -1; } } static bool M_Draw(const ITEM *const item) { Object_DrawAnimatingItem(item); if (m_FadeTimer < 0 || m_FadeTimer > M_FADE_DURATION) { return true; } const float opacity = 1.0f - (m_FadeTimer / (float)M_FADE_DURATION); Output_Overlay_DrawBlackRectangle(opacity, false); return true; } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->draw_func = M_Draw; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_CUT_SHOTGUN, M_Setup) ================================================ FILE: src/trx/game/objects/general/flare_item.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include // clang-format off #define M_FLARE_INTENSITY 12 #define M_FLARE_FALL_OFF 11 #define M_MAX_FLARE_AGE_TR12 (60 * LOGIC_FPS) // = 1800 #define M_FLARE_OLD_AGE_TR12 (M_MAX_FLARE_AGE_TR12 - 2 * LOGIC_FPS) // = 1740 #define M_FLARE_YOUNG_AGE_TR12 (LOGIC_FPS) // = 30 #define M_MAX_FLARE_AGE_TR3 (30 * LOGIC_FPS) // = 900 #define M_FLARE_DYING_AGE_TR3 (M_MAX_FLARE_AGE_TR3 - 90) // = 810 #define M_FLARE_END_AGE_TR3 (M_MAX_FLARE_AGE_TR3 - 24) // = 876 // clang-format on typedef struct { int32_t raw_age; } M_PRIV; static XYZ_32 M_TransformLocalOffset( const XYZ_32 pos, const XYZ_16 rot, const XYZ_32 local_offset) { Matrix_PushUnit(); Matrix_TranslateAbs32(pos); Matrix_Rot16(rot); Matrix_TranslateRel32(local_offset); const XYZ_32 out = { .x = g_WMatrixPtr->_03 >> W2V_SHIFT, .y = g_WMatrixPtr->_13 >> W2V_SHIFT, .z = g_WMatrixPtr->_23 >> W2V_SHIFT, }; Matrix_Pop(); return out; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); const ROOM *const room = Room_Get(item->room_num); if (room->flags.swamp) { Item_Kill(item_num); return; } if (item->fall_speed != 0) { item->rot.x += DEG_1 * 3; item->rot.z += DEG_1 * 5; } else { item->rot.x = 0; item->rot.z = 0; } const XYZ_32 old_pos = item->pos; item->pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, item->speed); if (room->flags.underwater) { item->fall_speed += (5 - item->fall_speed) / 2; item->speed = item->speed + (5 - item->speed) / 2; } else { item->fall_speed += GRAVITY; } item->pos.y += item->fall_speed; Collide_DoProperDetection(item, old_pos); int32_t flare_age = FlareItem_GetAge(item); bool is_active = FlareItem_IsActive(item); if (flare_age < Flare_GetMaxAge()) { flare_age++; } else if (item->fall_speed == 0 && item->speed == 0) { Item_Kill(item_num); return; } if (Flare_GenerateLight(item->pos, flare_age)) { is_active = true; Flare_GenerateEffects(&item->pos, item->pos, item->room_num); } if (g_TRVersion >= 3) { if (flare_age < Flare_GetMaxAge() && is_active) { const BOUNDS_16 *const bounds = &Item_GetBestFrame(item)->bounds; const XYZ_32 flare_size = { .x = bounds->max.x - bounds->min.x, .y = bounds->max.y - bounds->min.y, .z = bounds->max.z - bounds->min.z, }; const XYZ_32 flare_offset = { .x = -flare_size.x, .y = -flare_size.y, .z = -flare_size.z, }; const XYZ_32 flare_pos = item->pos; const XYZ_32 tip_local = { .x = flare_offset.x - 6, .y = flare_offset.y + 6, .z = flare_offset.z + 32, }; const XYZ_32 tip_pos = M_TransformLocalOffset(flare_pos, item->rot, tip_local); const XYZ_32 vel_local = { .x = (Random_GetControl() & 0x7F) - 64, .y = (Random_GetControl() & 0x7F) - 64, .z = (Random_GetControl() & 0x1FF) + 512, }; const XYZ_32 vel_pos = M_TransformLocalOffset(flare_pos, item->rot, vel_local); const XYZ_32 vel = { .x = vel_pos.x - flare_pos.x, .y = vel_pos.y - flare_pos.y, .z = vel_pos.z - flare_pos.z, }; for (int32_t i = 0; i < (Random_GetControl() & 3) + 4; i++) { const bool smoke = (i >> 2) != 0; Sparks_TriggerFlareSparks(tip_pos, vel, smoke); } } } FlareItem_SetAge(item, flare_age, is_active); } static void M_DrawFlash(const CLIP clip) { WEAPON_INFO *const flare_info = &g_Weapons[LGT_FLARE]; SWAP(flare_info->flash_pos, flare_info->flash_pos_alt); Gun_DrawFlash(LGT_FLARE, clip, false); SWAP(flare_info->flash_pos, flare_info->flash_pos_alt); } static bool M_Draw(const ITEM *const item) { int32_t rate; ANIM_FRAME *frames[2]; Item_GetFrames(item, frames, &rate); Matrix_Push(); Matrix_TranslateAbs32(item->interp.result.pos); Matrix_Rot16(item->interp.result.rot); const CLIP clip = Output_CheckBoundsClip(&frames[0]->bounds); const XYZ_32 flare_size = { .x = frames[0]->bounds.max.x - frames[0]->bounds.min.x, .y = frames[0]->bounds.max.y - frames[0]->bounds.min.y, .z = frames[0]->bounds.max.z - frames[0]->bounds.min.z, }; const XYZ_32 flare_offset = { .x = -flare_size.x, .y = -flare_size.y, .z = -flare_size.z, }; Matrix_TranslateRel32(flare_offset); if (clip == CLIP_NOT_VISIBLE) { goto end; } Output_CalculateObjectLighting(item, &frames[0]->bounds); Object_DrawMesh(Object_Get(O_FLARE_ITEM)->mesh_idx, clip, false); if (!FlareItem_IsActive(item)) { goto end; } if (g_TRVersion >= 3) { goto end; } M_DrawFlash(clip); end: Matrix_Pop(); return true; } static bool M_GenerateLight_TR12(const XYZ_32 pos, const int32_t flare_age) { if (flare_age >= M_MAX_FLARE_AGE_TR12) { return false; } const int32_t random = Random_GetDraw(); const XYZ_32 light_pos = { .x = pos.x + (random & 0xA0), .y = pos.y, .z = pos.z, }; if (flare_age < M_FLARE_YOUNG_AGE_TR12) { const int32_t intensity = M_FLARE_INTENSITY * (flare_age - M_FLARE_YOUNG_AGE_TR12) / (2 * M_FLARE_YOUNG_AGE_TR12) + M_FLARE_INTENSITY; Output_AddDynamicLight(light_pos, intensity, M_FLARE_FALL_OFF); return true; } if (flare_age < M_FLARE_OLD_AGE_TR12) { Output_AddDynamicLight(light_pos, M_FLARE_INTENSITY, M_FLARE_FALL_OFF); return true; } if (random > 0x2000) { Output_AddDynamicLight( light_pos, M_FLARE_INTENSITY - (random & 3), M_FLARE_FALL_OFF); return true; } Output_AddDynamicLight(light_pos, M_FLARE_INTENSITY, M_FLARE_FALL_OFF / 2); return false; } static bool M_GenerateLight_TR3(const XYZ_32 pos, const int32_t flare_age) { if (flare_age >= M_MAX_FLARE_AGE_TR3) { return false; } const int32_t rnd = Random_GetControl(); const XYZ_32 light_pos = { .x = pos.x + ((rnd & 0xF) << 3), .y = pos.y + ((rnd >> 1) & 0x78), .z = pos.z + ((rnd >> 5) & 0x78), }; int32_t r = 0; int32_t g = 0; int32_t b = 0; int32_t falloff = 0; if (flare_age < 4) { r = (rnd & 0x1F) + (flare_age << 4) + 160; g = ((rnd >> 4) & 0x1F) + (flare_age << 3) + 32; b = ((rnd >> 8) & 0x1F) + (flare_age << 4); falloff = (rnd & 3) + (flare_age << 2) + 4; if (falloff > 16) { falloff -= (rnd >> 12) & 3; } } else if (flare_age < 16) { r = (rnd & 0x3F) + (flare_age << 2) + 128; g = ((rnd >> 4) & 0x1F) + (flare_age << 2) + 64; b = ((rnd >> 8) & 0x1F) + (flare_age << 2) + 16; falloff = (rnd & 1) + flare_age + 2; } else if (flare_age < M_FLARE_DYING_AGE_TR3) { r = (rnd & 0x3F) + 192; g = ((rnd >> 4) & 0x1F) + 128; b = ((rnd >> 8) & 0x20) + (((rnd >> 6) & 0x10) << 1); falloff = 16; } else if (flare_age < M_FLARE_END_AGE_TR3) { if (rnd > 0x2000) { r = (rnd & 0x3F) + 192; g = ((rnd >> 4) & 0x1F) + 64; b = ((rnd >> 8) & 0x20) + (((rnd >> 6) & 0x10) << 1); falloff = 16; } else { const int32_t rnd2 = Random_GetControl(); const int32_t rnd3 = Random_GetControl(); const int32_t rnd4 = Random_GetControl(); r = (rnd2 & 0x3F) + 192; g = (rnd3 & 0x3F) + 64; b = rnd4 & 0x7F; falloff = (Random_GetControl() & 6) + 8; Output_AddDynamicLightRGB( light_pos, falloff, (RGB_888) { r, g, b }); return false; } } else { const int32_t rnd2 = Random_GetControl(); const int32_t rnd3 = Random_GetControl(); const int32_t rnd4 = Random_GetControl(); r = (rnd2 & 0x3F) + 192; g = (rnd3 & 0x3F) + 64; b = rnd4 & 0x1F; falloff = 16 - ((flare_age - M_FLARE_END_AGE_TR3) >> 1); Output_AddDynamicLightRGB(light_pos, falloff, (RGB_888) { r, g, b }); return (rnd & 1) != 0; } Output_AddDynamicLightRGB(light_pos, falloff, (RGB_888) { r, g, b }); return true; } void Flare_GenerateEffects( const XYZ_32 *const sound_pos, const XYZ_32 flare_pos, int16_t room_num) { Room_GetSector(flare_pos, &room_num); if (Room_Get(room_num)->flags.underwater) { Sound_Effect(SFX_LARA_FLARE_BURN, sound_pos, SPM_UNDERWATER); if (Random_GetDraw() < 0x4000) { Spawn_Bubble(&flare_pos, room_num); } } else { Sound_Effect(SFX_LARA_FLARE_BURN, sound_pos, SPM_NORMAL); } } bool Flare_GenerateLight(const XYZ_32 pos, const int32_t flare_age) { if (g_TRVersion >= 3) { return M_GenerateLight_TR3(pos, flare_age); } else { return M_GenerateLight_TR12(pos, flare_age); } } int32_t Flare_GetMaxAge(void) { return g_TRVersion >= 3 ? M_MAX_FLARE_AGE_TR3 : M_MAX_FLARE_AGE_TR12; } int32_t FlareItem_GetAge(const ITEM *const item) { const M_PRIV *const p = item->priv; return p->raw_age & 0x7FFF; } bool FlareItem_IsActive(const ITEM *const item) { const M_PRIV *const p = item->priv; return (p->raw_age & 0x8000) != 0; } void FlareItem_SetAge( ITEM *const item, const int32_t flare_age, const bool is_active) { M_PRIV *const p = item->priv; p->raw_age = flare_age & 0x7FFF; if (is_active) { p->raw_age |= 0x8000; } } static void M_Setup(OBJECT *const obj) { obj->collision_func = Pickup_Collision; obj->bounds_func = Pickup_Bounds; obj->control_func = M_Control; obj->draw_func = M_Draw; obj->priv_size = sizeof(M_PRIV); obj->save_position = true; obj->save_flags = true; if (obj->loaded) { for (int32_t i = 0; i < obj->mesh_count; i++) { OBJECT_MESH *const obj_mesh = Object_GetMesh(obj->mesh_idx + i); obj_mesh->depth_adjustment = -0.5; } } } REGISTER_OBJECT(O_FLARE_ITEM, M_Setup) ================================================ FILE: src/trx/game/objects/general/flare_item.h ================================================ #pragma once #include #include void Flare_GenerateEffects( const XYZ_32 *sound_pos, XYZ_32 flare_pos, int16_t room_num); bool Flare_GenerateLight(XYZ_32 pos, int32_t flare_age); int32_t Flare_GetMaxAge(void); int32_t FlareItem_GetAge(const ITEM *item); bool FlareItem_IsActive(const ITEM *item); void FlareItem_SetAge(ITEM *item, int32_t flare_age, bool is_active); ================================================ FILE: src/trx/game/objects/general/fuse_box.c ================================================ #include #include #include #include #include static bool M_CanTakeDamage(const ITEM *const item) { return Item_TestAnimEqual(item, 0); } static bool M_ShouldSpawnBlood(const ITEM *const item) { return false; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (item->hit_points <= 0 && Item_TestAnimEqual(item, 0)) { Item_SwitchToAnim(item, 1, 0); XYZ_32 pos = XYZ_32_OffsetYaw(item->pos, 420, item->rot.y + DEG_180); pos.y -= 768; Sparks_TriggerExplosionSparks(pos, 2, 0, 0, item->room_num); Sparks_TriggerExplosionSmoke(pos, 0, item->room_num); Room_TestTriggers(item); } Item_Animate(item); } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->collision_func = Object_Collision; obj->can_take_damage_func = M_CanTakeDamage; obj->should_spawn_blood_func = M_ShouldSpawnBlood; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_FUSE_BOX, M_Setup) ================================================ FILE: src/trx/game/objects/general/gas_emitter.c ================================================ #include #include #include #include #include #define M_DISTANCE (16 * WALL_L) // = 16384 static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); const int32_t time = Output_GetTimeInGame(); if (!Item_IsTriggerActive(item) || (time % 4) != 0) { return; } const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; if (ABS(dx) > M_DISTANCE || ABS(dz) > M_DISTANCE) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 0; spark->src_color.g = 0; spark->src_color.b = 0; spark->dst_color.r = 12; spark->dst_color.g = 32; spark->dst_color.b = 0; spark->fade_to_black = 32; spark->col_fade_speed = (Random_GetControl() & 7) + 24; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->life = (Random_GetControl() & 7) + 64; spark->s_life = spark->life; spark->dynamic = -1; spark->pos.x = item->pos.x + (Random_GetControl() & 0x1FF) - 256; spark->pos.y = item->pos.y - (Random_GetControl() & 0xF) - 264; spark->pos.z = item->pos.z + (Random_GetControl() & 0x3FF) - 512; spark->vel.x = (Random_GetControl() & 0xFF) - 128; spark->vel.y = -1 - (Random_GetControl() & 1); spark->vel.z = (Random_GetControl() & 0xFF) - 128; spark->friction = 4; spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE; if ((Random_GetControl() & 1) != 0) { spark->flags |= SPARK_F_ROTATE; spark->rot_angle = Random_GetControl() & 0xFFF; if ((Random_GetControl() & 1) != 0) { spark->rot_add = -4 - (Random_GetControl() & 7); } else { spark->rot_add = (Random_GetControl() & 7) + 4; } } spark->scalar = 3; spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->max_y_vel = 0; spark->gravity = 0; const uint8_t size = (Random_GetControl() & 0x1F) + 96; spark->size.width = size >> 1; spark->src_size.width = spark->size.width; spark->dst_size.width = spark->size.width; spark->size.height = (size + (Random_GetControl() & 0x1F) + 32) >> 1; spark->src_size.height = spark->size.height; spark->dst_size.height = spark->size.height; Sparks_FinishSetup(spark); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->control_func = M_Control; obj->draw_func = nullptr; obj->save_flags = true; } REGISTER_OBJECT(O_GAS_EMITTER_GREEN, M_Setup) ================================================ FILE: src/trx/game/objects/general/general.c ================================================ #include #include #include #include typedef enum { // clang-format off GENERAL_STATE_INACTIVE = 0, GENERAL_STATE_ACTIVE = 1, // clang-format on } GENERAL_STATE; static void M_Setup(OBJECT *const obj) { obj->control_func = General_Control; obj->collision_func = Object_Collision; obj->save_flags = true; obj->save_anim = true; } void General_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (Item_IsTriggerActive(item)) { item->goal_anim_state = GENERAL_STATE_ACTIVE; } else { item->goal_anim_state = GENERAL_STATE_INACTIVE; } Item_Animate(item); int16_t room_num = item->room_num; Room_GetSector(item->pos, &room_num); Item_UpdateRoom(item_num, room_num); XYZ_32 pos = { .x = 3000, .y = 720, .z = 0 }; Collide_GetJointAbsPosition(item, &pos, 0); Output_AddDynamicLight(pos, 14, 11); if (item->status == IS_DEACTIVATED) { Item_RemoveActive(item_num); item->flags |= IF_ONE_SHOT; } } REGISTER_OBJECT(O_GENERAL, M_Setup) ================================================ FILE: src/trx/game/objects/general/general.h ================================================ #pragma once #include void General_Control(int16_t item_num); ================================================ FILE: src/trx/game/objects/general/gong.c ================================================ #include #include #include #include #include #include static XYZ_32 m_Position = { .x = 0, .y = 0, .z = 0 }; static const OBJECT_BOUNDS m_Bounds = { .shift = { .min = { .x = -WALL_L / 2, .y = -100, .z = -WALL_L / 2 - 300, }, .max = { .x = +WALL_L, .y = +100, .z = -WALL_L / 2 + 100, }, }, .rot = { .min = { .x = -30 * DEG_1, .y = 0, .z = 0, }, .max = { .x = +30 * DEG_1, .y = 0, .z = 0, }, }, .ignore_rot = true, }; static void M_ConsumeKeyItem(ITEM *const receptacle_item) { const OBJECT_ID key_object_id = Object_FindReceptacleKey(receptacle_item->object_id); if (key_object_id != NO_OBJECT) { Inv_RemoveItem(key_object_id); } } static void M_CreateGongBonger(ITEM *const lara_item) { const int16_t item_gong_bonger_num = Item_Create(); if (item_gong_bonger_num == NO_ITEM) { return; } ITEM *const item_gong_bonger = Item_Get(item_gong_bonger_num); item_gong_bonger->object_id = O_GONG_BONGER; item_gong_bonger->pos.x = lara_item->pos.x; item_gong_bonger->pos.y = lara_item->pos.y; item_gong_bonger->pos.z = lara_item->pos.z; item_gong_bonger->rot.x = 0; item_gong_bonger->rot.y = lara_item->rot.y; lara_item->rot.z = 0; item_gong_bonger->room_num = lara_item->room_num; Item_Initialise(item_gong_bonger_num); Item_AddActive(item_gong_bonger_num); item_gong_bonger->status = IS_ACTIVE; item_gong_bonger->shade.value_1 = -1; } static void M_Use(ITEM *const lara_item, ITEM *const receptacle_item) { Lara_AlignPosition(receptacle_item, &m_Position); lara_item->rot.y += DEG_180; Lara_SwitchToExtraState(LS_EXTRA_GONG_BONG); if (Item_TestFrameEqual(lara_item, 0)) { M_ConsumeKeyItem(receptacle_item); } M_CreateGongBonger(lara_item); LARA_INFO *const lara = Lara_GetLaraInfo(); lara->interact_target.is_moving = false; lara->interact_target.item_num = NO_ITEM; } static const OBJECT_BOUNDS *M_Bounds(void) { return &m_Bounds; } static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { const LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->extra_anim) { return; } ITEM *const item = Item_Get(item_num); const OBJECT *const obj = Object_Get(item->object_id); if (lara->interact_target.is_moving && lara->interact_target.item_num == item_num) { M_Use(lara_item, item); return; } if (item->status != IS_INACTIVE || !g_Input.action || lara->gun_status != LGS_ARMLESS || lara_item->gravity || lara_item->current_anim_state != LS(LS_STOP)) { goto normal_collision; } const XYZ_16 old_rot = item->rot; item->rot.x = 0; item->rot.y = lara_item->rot.y; item->rot.z = 0; if (!Lara_TestPosition(item, obj->bounds_func())) { item->rot = old_rot; goto normal_collision; } item->rot = old_rot; if (!GF_ShowInventoryKeys(item->object_id)) { Lara_RefuseInteraction(); } return; normal_collision: Object_Collision(item_num, lara_item, coll); } static void M_Setup(OBJECT *const obj) { obj->collision_func = M_Collision; obj->bounds_func = M_Bounds; } REGISTER_OBJECT(O_GONG, M_Setup) ================================================ FILE: src/trx/game/objects/general/gong_bonger.c ================================================ #include #include #include #include #define GONG_BONGER_STRIKE_FRAME 41 #define GONG_BONGER_END_FRAME 79 static void M_ActivateHeavyTriggers(const int16_t item_num) { const ITEM *const item = Item_Get(item_num); Room_TestTriggers(item); Item_Kill(item_num); } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); Item_Animate(item); if (Item_TestFrameEqual(item, GONG_BONGER_STRIKE_FRAME)) { Music_Play(MX_REVEAL_1, MPM_ONCE); g_Camera.bounce -= 50; } if (Item_TestFrameEqual(item, GONG_BONGER_END_FRAME)) { M_ActivateHeavyTriggers(item_num); } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_GONG_BONGER, M_Setup) ================================================ FILE: src/trx/game/objects/general/grenade.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_SPEED 200 #define M_FALL_SPEED (M_SPEED - 10) // = 190 static int32_t M_GetBlastRadius(void) { return g_Config.gameplay.enable_bouncy_grenades ? WALL_L : WALL_L / 2; } static void M_SetTR3ProjectileShade(ITEM *const item) { if (item == nullptr) { return; } // OG TR3 uses `item->shade = -0x3DF0` on projectiles; in TRX any negative // shade forces the dynamic/smoothed lighting path. item->shade.value_1 = -1; item->shade.value_2 = -1; } static XYZ_32 M_GetLocalZOffset(const ITEM *const item, const int32_t dist) { const int32_t cx = Math_Cos(item->rot.x); const int32_t sx = Math_Sin(item->rot.x); const int32_t cy = Math_Cos(item->rot.y); const int32_t sy = Math_Sin(item->rot.y); const int32_t horz = (dist * cx) >> W2V_SHIFT; return (XYZ_32) { .x = (horz * sy) >> W2V_SHIFT, .y = -(dist * sx) >> W2V_SHIFT, .z = (horz * cy) >> W2V_SHIFT, }; } static void M_Explode(int16_t grenade_item_num, const XYZ_32 pos) { const ITEM *const grenade_item = Item_Get(grenade_item_num); const ROOM *const room = Room_Get(grenade_item->room_num); const bool is_underwater = room != nullptr && room->flags.underwater; if (g_TRVersion == 3) { if (is_underwater) { Sparks_TriggerUnderwaterExplosion(grenade_item); } else { Sparks_TriggerExplosionSparks( pos, 3, -2, 0, grenade_item->room_num); for (int32_t i = 0; i < 2; i++) { Sparks_TriggerExplosionSparks( pos, 3, -1, 0, grenade_item->room_num); } } Sound_Effect( SFX_EXPLOSION_1, &grenade_item->pos, 0x1800000 | SPM_PITCH); Sound_Effect(SFX_EXPLOSION_2, &grenade_item->pos, SPM_NORMAL); } else { const int16_t effect_num = Effect_Create(grenade_item->room_num); if (effect_num != NO_EFFECT) { EFFECT *const effect = Effect_Get(effect_num); effect->pos = pos; effect->speed = 0; effect->frame_num = 0; effect->counter = 0; effect->object_id = O_EXPLOSION_1; } Sound_Effect(SFX_EXPLOSION_3, nullptr, SPM_NORMAL); } Creature_AlertNearbyGuards(grenade_item); Item_Kill(grenade_item_num); } static bool M_CanExplodeTarget(const ITEM *const item) { const OBJECT *const object = Object_Get(item->object_id); if (object->can_be_exploded_func != nullptr) { return object->can_be_exploded_func(item); } // TODO: as some creatures have more than one death animation, have a // way to expose those specific ones for checking, or delegate // responsibility directly to the objects. const ITEM_ACTION action = ItemAction_ToGameID(ITEM_ACTION_FINISH_LEVEL); for (int32_t i = 0; i < object->anim_count; i++) { const ANIM *const anim = Object_GetAnim(object, i); if (Anim_HasFXCommand(anim, action)) { return false; } } return true; } static bool M_TryExplodeItem( const ITEM *const projectile_item, const GAME_VECTOR old_pos, const int16_t target_item_num, const int32_t radius) { ITEM *const target_item = Item_Get(target_item_num); const OBJECT *const target_obj = Object_Get(target_item->object_id); if (target_item == Lara_GetItem()) { return false; } if (!target_item->collidable) { return false; } if (target_item->status == IS_INVISIBLE || target_obj->collision_func == nullptr) { return false; } if (!Item_CanBeProjectileTarget(target_item)) { return false; } const ANIM_FRAME *const frame = Item_GetBestFrame(target_item); const BOUNDS_16 *const bounds = &frame->bounds; const int32_t cdy = projectile_item->pos.y - target_item->pos.y; if (cdy + radius < bounds->min.y || cdy - radius > bounds->max.y) { return false; } const int32_t cy = Math_Cos(target_item->rot.y); const int32_t sy = Math_Sin(target_item->rot.y); const int32_t cdx = projectile_item->pos.x - target_item->pos.x; const int32_t cdz = projectile_item->pos.z - target_item->pos.z; const int32_t odx = old_pos.x - target_item->pos.x; const int32_t odz = old_pos.z - target_item->pos.z; const int32_t rx = (cy * cdx - sy * cdz) >> W2V_SHIFT; const int32_t sx = (cy * odx - sy * odz) >> W2V_SHIFT; if ((rx + radius < bounds->min.x && sx + radius < bounds->min.x) || (rx - radius > bounds->max.x && sx - radius > bounds->max.x)) { return false; } const int32_t rz = (sy * cdx + cy * cdz) >> W2V_SHIFT; const int32_t sz = (sy * odx + cy * odz) >> W2V_SHIFT; if ((rz + radius < bounds->min.z && sz + radius < bounds->min.z) || (rz - radius > bounds->max.z && sz - radius > bounds->max.z)) { return false; } if (!Item_CanTakeDamage(target_item)) { return false; } const GAME_VECTOR hit_pos = { .pos = projectile_item->pos, .room_num = projectile_item->room_num, }; Gun_HitTarget( target_item, &old_pos, &hit_pos, g_Weapons[LGT_GRENADE].damage); Stats_AddAmmoHits(); if (Gun_GetSmashPolicy(target_item) != GUN_SMASH_POLICY_NONE) { Gun_SmashItem(target_item_num); } else if ( target_item->hit_points <= 0 && M_CanExplodeTarget(target_item)) { Creature_Die(target_item_num, true); } return true; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); const GAME_VECTOR old_pos = { .pos = item->pos, .room_num = item->room_num, }; const ROOM *const room = Room_Get(item->room_num); const bool was_underwater = room != nullptr && room->flags.underwater; if (g_Config.gameplay.enable_bouncy_grenades) { if (was_underwater) { item->fall_speed += (5 - item->fall_speed) >> 1; item->speed -= item->speed >> 2; if (item->speed != 0) { item->rot.z += DEG_1 * ((item->speed >> 4) + 3); if (item->required_anim_state != 0) { item->rot.y += DEG_1 * ((item->speed >> 2) + 3); } else { item->rot.x += DEG_1 * ((item->speed >> 2) + 3); } } } else { item->fall_speed += 3; if (item->speed != 0) { item->rot.z += DEG_1 * ((item->speed >> 2) + 7); if (item->required_anim_state != 0) { item->rot.y += DEG_1 * ((item->speed >> 1) + 7); } else { item->rot.x += DEG_1 * ((item->speed >> 1) + 7); } } } } if (g_TRVersion == 3) { M_SetTR3ProjectileShade(item); if (!was_underwater && item->speed != 0) { const XYZ_32 back_64 = M_GetLocalZOffset(item, -64); Sparks_TriggerRocketSmoke( (XYZ_32) { .x = item->pos.x + back_64.x, .y = item->pos.y + back_64.y, .z = item->pos.z + back_64.z, }, -1, item->room_num); } } bool explode = false; int32_t radius = 0; if (g_Config.gameplay.enable_bouncy_grenades) { const XYZ_32 vel = { .x = (item->speed * Math_Sin(item->goal_anim_state)) >> W2V_SHIFT, .y = item->fall_speed, .z = (item->speed * Math_Cos(item->goal_anim_state)) >> W2V_SHIFT, }; item->pos.x += vel.x; item->pos.y += vel.y; item->pos.z += vel.z; const int16_t y_rot = item->rot.y; item->rot.y = item->goal_anim_state; Collide_DoProperDetection(item, old_pos.pos); item->goal_anim_state = item->rot.y; item->rot.y = y_rot; if (item->hit_points > 0) { item->hit_points--; if (item->hit_points == 0) { radius = M_GetBlastRadius(); explode = true; } } } else { item->speed--; if (item->speed < M_FALL_SPEED) { item->fall_speed++; } item->pos.y += item->fall_speed - ((item->speed * Math_Sin(item->rot.x)) >> W2V_SHIFT); const int16_t speed = (item->speed * Math_Cos(item->rot.x)) >> W2V_SHIFT; item->pos.z += (speed * Math_Cos(item->rot.y)) >> W2V_SHIFT; item->pos.x += (speed * Math_Sin(item->rot.y)) >> W2V_SHIFT; int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); item->floor = Room_GetHeight(sector, item->pos); Item_UpdateRoom(item_num, room_num); if (item->pos.y >= item->floor || item->pos.y <= Room_GetCeiling(sector, item->pos)) { radius = M_GetBlastRadius(); explode = true; } } if (g_TRVersion == 3) { const ROOM *const new_room = Room_Get(item->room_num); const bool is_underwater = new_room != nullptr && new_room->flags.underwater; if (is_underwater && !was_underwater) { const int32_t inner_y_vel = -2048 - ((int32_t)item->fall_speed << 5); const int32_t middle_y_vel = -1024 - ((int32_t)item->fall_speed << 4); FX_Water_SetupSplash(&(FX_WATER_SPLASH_SETUP) { .x = item->pos.x, .y = new_room->max_ceiling, .z = item->pos.z, .inner_xz_off = 16, .inner_xz_size = 12, .inner_y_size = -96, .inner_xz_vel = 160, .inner_gravity = 128, .inner_y_vel = inner_y_vel, .inner_friction = 7, .middle_xz_off = 24, .middle_xz_size = 24, .middle_y_size = -64, .middle_xz_vel = 224, .middle_gravity = 72, .middle_y_vel = middle_y_vel, .middle_friction = 8, .outer_xz_off = 32, .outer_xz_size = 32, .outer_xz_vel = 272, .outer_friction = 9, }); } } const GAME_VECTOR new_pos = { .pos = item->pos, .room_num = item->room_num, }; if (Gun_SmashItems(old_pos, new_pos, nullptr, item->object_id) == PROJECTILE_HIT_STOP) { explode = true; radius = M_GetBlastRadius(); } if (g_Config.gameplay.projectile_area_damage == PROJECTILE_AREA_DAMAGE_MULTI_SWEEP) { Room_GetNearbyRooms(item->pos, radius * 4, radius * 4, item->room_num); for (int32_t i = 0; i < Room_DrawGetCount(); i++) { const ROOM *const nearby_room = Room_Get(Room_DrawGetRoom(i)); for (int16_t target_item_num = nearby_room->item_num; target_item_num != NO_ITEM; target_item_num = Item_Get(target_item_num)->next_item) { if (!M_TryExplodeItem(item, old_pos, target_item_num, radius)) { continue; } if (!explode) { explode = true; radius = M_GetBlastRadius(); i = -1; break; } } } } else { for (int16_t target_item_num = room->item_num; target_item_num != NO_ITEM; target_item_num = Item_Get(target_item_num)->next_item) { if (M_TryExplodeItem(item, old_pos, target_item_num, radius)) { explode = true; } } } if (explode) { M_Explode(item_num, old_pos.pos); } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->save_position = true; } REGISTER_OBJECT(O_GRENADE, M_Setup) ================================================ FILE: src/trx/game/objects/general/harpoon_bolt.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef struct { int16_t base_x_rot; bool base_x_rot_valid; } M_PRIV; #define M_TR3_HIT_POINTS 256 #define M_TR3_WOBBLE_START 192 #define M_TR3_SPEED_UW 128 #define M_TR3_SPEED_AIR 256 static void M_SetTR3ProjectileShade(ITEM *const item) { if (item == nullptr) { return; } // OG TR3 uses `item->shade = -0x3DF0` on projectiles; in TRX any negative // shade forces the dynamic/smoothed lighting path. item->shade.value_1 = -1; item->shade.value_2 = -1; } static void M_Initialise_TR3(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (item->priv == nullptr) { item->priv = GameBuf_Alloc(sizeof(M_PRIV), GBUF_ITEM_DATA); } M_PRIV *const p = item->priv; p->base_x_rot = 0; p->base_x_rot_valid = false; } static void M_Control_TR3(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; const GAME_VECTOR old_pos = { .pos = item->pos, .room_num = item->room_num, }; M_SetTR3ProjectileShade(item); item->pos.x += (item->speed * Math_Sin(item->rot.y)) >> W2V_SHIFT; item->pos.y += item->fall_speed; item->pos.z += (item->speed * Math_Cos(item->rot.y)) >> W2V_SHIFT; int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); item->floor = Room_GetHeight(sector, item->pos); Item_UpdateRoom(item_num, room_num); const GAME_VECTOR new_pos = { .pos = item->pos, .room_num = item->room_num, }; if (Gun_SmashItems(old_pos, new_pos, nullptr, item->object_id) == PROJECTILE_HIT_STOP) { Item_Kill(item_num); return; } for (int16_t target_num = Room_Get(item->room_num)->item_num; target_num != NO_ITEM; target_num = Item_Get(target_num)->next_item) { ITEM *const target_item = Item_Get(target_num); if (target_item == Lara_GetItem() || item_num == target_num) { continue; } if (!target_item->collidable) { continue; } if (!Item_CanBeProjectileTarget(target_item)) { continue; } const ANIM_FRAME *const frame = Item_GetBestFrame(target_item); if (frame == nullptr) { continue; } const BOUNDS_16 *const bounds = &frame->bounds; const int32_t cdy = item->pos.y - target_item->pos.y; if (cdy < bounds->min.y || cdy > bounds->max.y) { continue; } const int32_t cy = Math_Cos(target_item->rot.y); const int32_t sy = Math_Sin(target_item->rot.y); const int32_t cdx = item->pos.x - target_item->pos.x; const int32_t cdz = item->pos.z - target_item->pos.z; const int32_t odx = old_pos.x - target_item->pos.x; const int32_t odz = old_pos.z - target_item->pos.z; const int32_t rx = (cy * cdx - sy * cdz) >> W2V_SHIFT; const int32_t sx = (cy * odx - sy * odz) >> W2V_SHIFT; if ((rx < bounds->min.x && sx < bounds->min.x) || (rx > bounds->max.x && sx > bounds->max.x)) { continue; } const int32_t rz = (sy * cdx + cy * cdz) >> W2V_SHIFT; const int32_t sz = (sy * odx + cy * odz) >> W2V_SHIFT; if ((rz < bounds->min.z && sz < bounds->min.z) || (rz > bounds->max.z && sz > bounds->max.z)) { continue; } if (Item_CanTakeDamage(target_item)) { if (Item_ShouldSpawnBlood(target_item)) { Spawn_BloodBath( item->pos.x, item->pos.y, item->pos.z, 0, 0, item->room_num, 3); } const GAME_VECTOR hit_pos = { .pos = item->pos, .room_num = item->room_num }; Gun_HitTarget( target_item, &old_pos, &hit_pos, g_Weapons[LGT_HARPOON].damage); Stats_AddAmmoHits(); } Item_Kill(item_num); return; } const int32_t ceiling = Room_GetCeiling(sector, item->pos); if (item->pos.y >= item->floor || item->pos.y <= ceiling) { if (item->hit_points <= 0) { item->hit_points = M_TR3_HIT_POINTS; } if (item->hit_points == M_TR3_HIT_POINTS) { if (p != nullptr) { p->base_x_rot = item->rot.x; p->base_x_rot_valid = true; } } if (item->hit_points >= M_TR3_WOBBLE_START) { const int32_t base_x_rot = (p != nullptr && p->base_x_rot_valid) ? p->base_x_rot : item->rot.x; const int32_t wobble_angle = (item->hit_points & 7) * (DEG_360 / 16); const int32_t wobble = (Math_Sin(wobble_angle) >> 3) - 1024; item->rot.x = (int16_t)(base_x_rot + (((item->hit_points - M_TR3_WOBBLE_START) * wobble) >> 6)); item->hit_points--; } item->hit_points--; if (item->hit_points <= 0) { Item_Kill(item_num); return; } item->fall_speed = 0; item->speed = 0; return; } item->rot.z += 35 * DEG_1; const ROOM *const room = Room_Get(item->room_num); if (room != nullptr && room->flags.underwater) { const int32_t time4 = Output_GetTimeInGame() * 4; if ((time4 & 0xF) == 0) { Spawn_BubbleEx(&item->pos, item->room_num, 2, 8); } Sparks_TriggerRocketSmoke(item->pos, 64, item->room_num); item->fall_speed = (int16_t)((-M_TR3_SPEED_UW * Math_Sin(item->rot.x)) >> W2V_SHIFT); item->speed = (int16_t)((M_TR3_SPEED_UW * Math_Cos(item->rot.x)) >> W2V_SHIFT); } else { item->rot.x -= DEG_1; if (item->rot.x < -DEG_90) { item->rot.x = -DEG_90; } item->fall_speed = (int16_t)((-M_TR3_SPEED_AIR * Math_Sin(item->rot.x)) >> W2V_SHIFT); item->speed = (int16_t)((M_TR3_SPEED_AIR * Math_Cos(item->rot.x)) >> W2V_SHIFT); } } static void M_Control_TR12(const int16_t item_num) { ITEM *const item = Item_Get(item_num); const GAME_VECTOR old_pos = { .pos = item->pos, .room_num = item->room_num, }; if (!Room_Get(item->room_num)->flags.underwater) { item->fall_speed += GRAVITY / 2; } item->pos.x += (item->speed * Math_Sin(item->rot.y)) >> W2V_SHIFT; item->pos.z += (item->speed * Math_Cos(item->rot.y)) >> W2V_SHIFT; item->pos.y += item->fall_speed; int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); item->floor = Room_GetHeight(sector, item->pos); Item_UpdateRoom(item_num, room_num); const GAME_VECTOR new_pos = { .pos = item->pos, .room_num = item->room_num, }; bool hit = false; if (Gun_SmashItems(old_pos, new_pos, nullptr, item->object_id) == PROJECTILE_HIT_STOP) { hit = true; } for (int16_t target_num = Room_Get(item->room_num)->item_num; target_num != NO_ITEM; target_num = Item_Get(target_num)->next_item) { ITEM *const target_item = Item_Get(target_num); const OBJECT *const target_obj = Object_Get(target_item->object_id); if (target_item == Lara_GetItem() || item_num == target_num) { continue; } if (!target_item->collidable) { continue; } if (!Item_CanBeProjectileTarget(target_item)) { continue; } const ANIM_FRAME *const frame = Item_GetBestFrame(target_item); if (frame == nullptr) { continue; } const BOUNDS_16 *const bounds = &frame->bounds; const int32_t cdy = item->pos.y - target_item->pos.y; if (cdy < bounds->min.y || cdy > bounds->max.y) { continue; } const int32_t cy = Math_Cos(target_item->rot.y); const int32_t sy = Math_Sin(target_item->rot.y); const int32_t cdx = item->pos.x - target_item->pos.x; const int32_t cdz = item->pos.z - target_item->pos.z; const int32_t odx = old_pos.x - target_item->pos.x; const int32_t odz = old_pos.z - target_item->pos.z; const int32_t rx = (cy * cdx - sy * cdz) >> W2V_SHIFT; const int32_t sx = (cy * odx - sy * odz) >> W2V_SHIFT; if ((rx < bounds->min.x && sx < bounds->min.x) || (rx > bounds->max.x && sx > bounds->max.x)) { continue; } const int32_t rz = (sy * cdx + cy * cdz) >> W2V_SHIFT; const int32_t sz = (sy * odx + cy * odz) >> W2V_SHIFT; if ((rz < bounds->min.z && sz < bounds->min.z) || (rz > bounds->max.z && sz > bounds->max.z)) { continue; } if (Item_CanTakeDamage(target_item)) { if (Item_ShouldSpawnBlood(target_item)) { Spawn_BloodBath( item->pos.x, item->pos.y, item->pos.z, 0, 0, item->room_num, 5); } const GAME_VECTOR hit_pos = { .pos = item->pos, .room_num = item->room_num, }; Gun_HitTarget( target_item, &old_pos, &hit_pos, g_Weapons[LGT_HARPOON].damage); Stats_AddAmmoHits(); } hit = true; break; } if (!hit) { const int32_t ceiling = Room_GetCeiling(sector, item->pos); if (item->pos.y >= item->floor || item->pos.y <= ceiling) { hit = true; } } if (hit) { Item_Kill(item_num); } else if (Room_Get(item->room_num)->flags.underwater) { Spawn_Bubble(&item->pos, item->room_num); } } static void M_Setup(OBJECT *const obj) { obj->initialise_func = g_TRVersion == 3 ? M_Initialise_TR3 : nullptr; obj->control_func = g_TRVersion == 3 ? M_Control_TR3 : M_Control_TR12; obj->save_position = true; } REGISTER_OBJECT(O_HARPOON_BOLT, M_Setup) ================================================ FILE: src/trx/game/objects/general/keyhole.c ================================================ #include #include #include #include #include #define M_LF_USE_KEYHOLE 104 static XYZ_32 m_KeyholePosition = { .x = 0, .y = 0, .z = WALL_L / 2 - LARA_RADIUS - 50, }; static const OBJECT_BOUNDS m_KeyholeBounds = { .shift = { .min = { .x = -200, .y = +0, .z = +WALL_L / 2 - 200, }, .max = { .x = +200, .y = +0, .z = +WALL_L / 2, }, }, .rot = { .min = { .x = -10 * DEG_1, .y = -30 * DEG_1, .z = -10 * DEG_1, }, .max = { .x = +10 * DEG_1, .y = +30 * DEG_1, .z = +10 * DEG_1, }, }, }; static const OBJECT_BOUNDS *M_Bounds(void) { return &m_KeyholeBounds; } static void M_Use(ITEM *const lara_item, ITEM *const receptacle_item) { LARA_INFO *const lara = Lara_GetLaraInfo(); Lara_AlignPosition(receptacle_item, &m_KeyholePosition); Lara_AnimateUntil(lara_item, LS(LS_USE_KEY)); lara_item->goal_anim_state = LS(LS_STOP); lara->gun_status = LGS_HANDS_BUSY; lara->interact_target.is_moving = false; } static void M_ConsumeKeyItem(ITEM *const receptacle_item) { const OBJECT_ID key_object_id = Object_FindReceptacleKey(receptacle_item->object_id); if (key_object_id != NO_OBJECT) { Inv_RemoveItem(key_object_id); } LARA_INFO *const lara = Lara_GetLaraInfo(); lara->interact_target.item_num = NO_ITEM; } static void M_MarkDone(ITEM *const receptacle_item) { receptacle_item->status = IS_ACTIVE; } static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); const OBJECT *const obj = Object_Get(item->object_id); const LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara_item->current_anim_state != LS(LS_STOP)) { if (lara_item->current_anim_state == LS(LS_USE_KEY) && Lara_TestPosition(item, obj->bounds_func()) && Item_TestFrameEqual(lara_item, M_LF_USE_KEYHOLE)) { M_ConsumeKeyItem(item); M_MarkDone(item); } return; } if (lara->interact_target.is_moving && lara->interact_target.item_num == item_num) { M_Use(lara_item, item); } if (!g_Input.action || lara->gun_status != LGS_ARMLESS || lara_item->gravity || lara_item->current_anim_state != LS(LS_STOP)) { return; } if (!Lara_TestPosition(item, obj->bounds_func())) { return; } if (item->status != IS_INACTIVE) { Lara_RefuseInteraction(); } else if (!GF_ShowInventoryKeys(item->object_id)) { Lara_RefuseInteraction(); } } static bool M_IsUsable(const int16_t item_num) { const ITEM *const item = Item_Get(item_num); return item->status == IS_INACTIVE; } static void M_Setup(OBJECT *const obj) { obj->collision_func = M_Collision; obj->bounds_func = M_Bounds; obj->save_flags = true; obj->is_usable_func = M_IsUsable; } bool Keyhole_Trigger(const int16_t item_num) { ITEM *const item = Item_Get(item_num); const LARA_INFO *const lara = Lara_GetLaraInfo(); if (item->status != IS_ACTIVE || lara->gun_status == LGS_HANDS_BUSY) { return false; } item->status = IS_DEACTIVATED; return true; } REGISTER_OBJECT(O_KEY_HOLE_1, M_Setup) REGISTER_OBJECT(O_KEY_HOLE_2, M_Setup) REGISTER_OBJECT(O_KEY_HOLE_3, M_Setup) REGISTER_OBJECT(O_KEY_HOLE_4, M_Setup) ================================================ FILE: src/trx/game/objects/general/keyhole.h ================================================ #pragma once #include bool Keyhole_Trigger(int16_t item_num); ================================================ FILE: src/trx/game/objects/general/kill_all_triggered.c ================================================ #include #include static void M_Control(const int16_t item_num) { Item_KillAllActive(); Effect_KillAllActive(); } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->draw_func = nullptr; obj->save_flags = true; } REGISTER_OBJECT(O_KILL_ALL_TRIGGERED, M_Setup) ================================================ FILE: src/trx/game/objects/general/lara_alarm.c ================================================ #include #include static void M_Control(const int16_t item_num) { const ITEM *const item = Item_Get(item_num); if ((item->flags & IF_CODE_BITS) == IF_CODE_BITS) { Sound_Effect(SFX_BURGLAR_ALARM, &item->pos, SPM_NORMAL); } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->draw_func = nullptr; obj->save_flags = true; } REGISTER_OBJECT(O_LARA_ALARM, M_Setup) ================================================ FILE: src/trx/game/objects/general/lift.c ================================================ #include #include #include #include #include #include #include #include #include #define LIFT_WAIT_TIME (3 * LOGIC_FPS) // = 90 #define LIFT_SHIFT 16 #define LIFT_HEIGHT (STEP_L * 5) // = 1280 #define LIFT_TRAVEL_DIST (STEP_L * 22) #define M_LIFT_NUM_FLOOR_SECTORS 4 #define M_LIFT_NUM_SECTORS 8 typedef enum { LIFT_STATE_DOOR_CLOSED = 0, LIFT_STATE_DOOR_OPEN = 1, } LIFT_STATE; typedef enum { LIFT_ANIM_CLOSED = 0, } LIFT_ANIM; typedef struct { int32_t start_height; int32_t wait_time; bool is_moving; GAME_VECTOR linked[M_LIFT_NUM_SECTORS]; } M_PRIV; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "start_height", &p->start_height)); JSON_SHOULD(JSON_READ(io, "wait_time", &p->wait_time)); JSON_SHOULD(JSON_READ(io, "is_moving", &p->is_moving)); for (int32_t i = 0; i < M_LIFT_NUM_SECTORS; i++) { const char *const key = String_FormatStatic("linked_%d", i); if (JSON_SHOULD(JSON_PUSH(io, key))) { JSON_SHOULD(JSON_READ(io, "x", &p->linked[i].pos.x)); JSON_SHOULD(JSON_READ(io, "y", &p->linked[i].pos.y)); JSON_SHOULD(JSON_READ(io, "z", &p->linked[i].pos.z)); JSON_SHOULD(JSON_POP(io)); } } } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "start_height", p->start_height); JSONW_WRITE(io, "wait_time", p->wait_time); JSONW_WRITE(io, "is_moving", p->is_moving); for (int32_t i = 0; i < M_LIFT_NUM_SECTORS; i++) { const char *const key = String_FormatStatic("linked_%d", i); JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "x", p->linked[i].pos.x); JSONW_WRITE(io, "y", p->linked[i].pos.y); JSONW_WRITE(io, "z", p->linked[i].pos.z); JSONW_POP_AND_SET(io, key); } } static void M_FloorCeiling( const ITEM *const item, const int32_t x, const int32_t y, const int32_t z, int32_t *const out_floor, int32_t *const out_ceiling) { const XZ_32 lift_tile = { .x = item->pos.x >> WALL_SHIFT, .z = item->pos.z >> WALL_SHIFT, }; ITEM *const lara_item = Lara_GetItem(); const XZ_32 lara_tile = { .x = lara_item->pos.x >> WALL_SHIFT, .z = lara_item->pos.z >> WALL_SHIFT, }; const XZ_32 test_tile = { .x = x >> WALL_SHIFT, .z = z >> WALL_SHIFT, }; const DIRECTION direction = Math_GetDirection(item->rot.y); int32_t dx = 0; int32_t dz = 0; switch (direction) { case DIR_NORTH: dx = -1; dz = 1; break; case DIR_EAST: dx = 1; dz = 1; break; case DIR_SOUTH: dx = 1; dz = -1; break; case DIR_WEST: dx = -1; dz = -1; break; default: break; } // clang-format off const bool point_in_shaft = (test_tile.x == lift_tile.x || test_tile.x + dx == lift_tile.x) && (test_tile.z == lift_tile.z || test_tile.z + dz == lift_tile.z); const bool lara_in_shaft = (lara_tile.x == lift_tile.x || lara_tile.x + dx == lift_tile.x) && (lara_tile.z == lift_tile.z || lara_tile.z + dz == lift_tile.z); const int32_t lift_bottom = item->pos.y + STEP_L; const int32_t lift_floor = item->pos.y; const int32_t lift_ceiling = item->pos.y - LIFT_HEIGHT + STEP_L; const int32_t lift_top = item->pos.y - LIFT_HEIGHT; const bool lara_inside_lift = (lara_item->pos.y < lift_bottom) && (lara_item->pos.y > lift_ceiling); // clang-format on *out_floor = 0x7FFF; *out_ceiling = -0x7FFF; if (lara_in_shaft) { if (item->current_anim_state == LIFT_STATE_DOOR_CLOSED && lara_inside_lift) { if (point_in_shaft) { *out_floor = lift_floor; *out_ceiling = lift_ceiling; } else { *out_floor = NO_HEIGHT; *out_ceiling = 0x7FFF; } } else if (point_in_shaft) { if (lara_item->pos.y < lift_ceiling) { *out_floor = lift_top; } else if (lara_item->pos.y < lift_bottom) { *out_floor = lift_floor; *out_ceiling = lift_ceiling; } else { *out_ceiling = lift_bottom; } } } else if (point_in_shaft) { if (y <= lift_top) { *out_floor = lift_top; } else if (y >= lift_bottom) { *out_ceiling = lift_bottom; } else if (item->current_anim_state == LIFT_STATE_DOOR_OPEN) { *out_floor = lift_floor; *out_ceiling = lift_ceiling; } else { *out_floor = NO_HEIGHT; *out_ceiling = 0x7FFF; } } } static int16_t M_GetFloorHeight( const ITEM *const item, const int32_t x, const int32_t y, const int32_t z, const int16_t height) { int32_t new_floor; int32_t new_ceiling; M_FloorCeiling(item, x, y, z, &new_floor, &new_ceiling); if (new_floor >= height) { return height; } return new_floor; } static int16_t M_GetCeilingHeight( const ITEM *const item, const int32_t x, const int32_t y, const int32_t z, const int16_t height) { int32_t new_floor; int32_t new_ceiling; M_FloorCeiling(item, x, y, z, &new_floor, &new_ceiling); if (new_ceiling <= height) { return height; } return new_ceiling; } static void M_GetSectorPositions( const ITEM *const item, VECTOR *const sector_pos) { const XZ_32 lift_tile = { .x = item->pos.x >> WALL_SHIFT, .z = item->pos.z >> WALL_SHIFT, }; // Orient. const DIRECTION dir = Math_GetDirection(item->rot.y); int32_t dx = 0, dz = 0; switch (dir) { case DIR_NORTH: dx = -1; dz = 1; break; case DIR_EAST: dx = 1; dz = 1; break; case DIR_SOUTH: dx = 1; dz = -1; break; case DIR_WEST: dx = -1; dz = -1; break; default: break; } // Collect a 2×2 footprint that lines up with the shaft tiles. for (int32_t ix = 0; ix < 2; ix++) { for (int32_t iz = 0; iz < 2; iz++) { const int32_t sx = lift_tile.x - dx * ix; const int32_t sz = lift_tile.z - dz * iz; const XYZ_32 pos = { .x = sx * WALL_L + WALL_L / 2, .y = item->pos.y, .z = sz * WALL_L + WALL_L / 2, }; Vector_Add(sector_pos, &pos); } } // Collect a 2×2 footprint that lines up with the shaft ceiling tiles. for (int32_t ix = 0; ix < 2; ix++) { for (int32_t iz = 0; iz < 2; iz++) { const int32_t sx = lift_tile.x - dx * ix; const int32_t sz = lift_tile.z - dz * iz; const XYZ_32 pos = { .x = sx * WALL_L + WALL_L / 2, .y = item->pos.y - LIFT_HEIGHT, .z = sz * WALL_L + WALL_L / 2, }; Vector_Add(sector_pos, &pos); } } } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; p->start_height = item->pos.y; p->wait_time = 0; p->is_moving = false; VECTOR *const positions = Vector_Create(sizeof(XYZ_32)); M_GetSectorPositions(item, positions); for (int32_t i = 0; i < positions->count; i++) { const GAME_VECTOR linked = { .pos = *(const XYZ_32 *)Vector_Get(positions, i), .room_num = item->room_num, }; p->linked[i] = linked; } Walkable_AllocateNodes(item, positions->count); Vector_Free(positions); } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; const int32_t bottom = p->start_height; const int32_t top = bottom + LIFT_TRAVEL_DIST; const int32_t target = Item_IsTriggerActive(item) ? top : bottom; if (item->pos.y == target) { item->goal_anim_state = LIFT_STATE_DOOR_OPEN; p->wait_time = 0; if (p->is_moving) { for (int32_t i = 0; i < M_LIFT_NUM_FLOOR_SECTORS; i++) { MovableBlock_ShiftStackY( p->linked[i].pos.y, p->linked[i].pos, item->pos.y, item->room_num, true); // Don't reposition because item->pos links to a single sector. p->linked[i].pos.y = item->pos.y; } for (int32_t i = M_LIFT_NUM_FLOOR_SECTORS; i < M_LIFT_NUM_SECTORS; i++) { MovableBlock_ShiftStackY( p->linked[i].pos.y, p->linked[i].pos, item->pos.y - LIFT_HEIGHT, item->room_num, true); // Don't reposition because item->pos links to a single sector. p->linked[i].pos.y = item->pos.y - LIFT_HEIGHT; } } p->is_moving = false; } else if (p->wait_time < LIFT_WAIT_TIME) { item->goal_anim_state = LIFT_STATE_DOOR_OPEN; p->wait_time++; // Prevent Lara from interacting with blocks about to move. for (int32_t i = 0; i < M_LIFT_NUM_FLOOR_SECTORS; i++) { MovableBlock_ShiftStackY( p->linked[i].pos.y, p->linked[i].pos, item->pos.y, item->room_num, false); } for (int32_t i = M_LIFT_NUM_FLOOR_SECTORS; i < M_LIFT_NUM_SECTORS; i++) { MovableBlock_ShiftStackY( p->linked[i].pos.y, p->linked[i].pos, item->pos.y - LIFT_HEIGHT, item->room_num, false); } } else { item->goal_anim_state = LIFT_STATE_DOOR_CLOSED; p->is_moving = true; const int32_t delta = target - item->pos.y; const int32_t step = (delta > 0) ? (delta < LIFT_SHIFT ? delta : LIFT_SHIFT) : (delta > -LIFT_SHIFT ? delta : -LIFT_SHIFT); item->pos.y += step; // Raise/lower possible movable blocks on top. for (int32_t i = 0; i < M_LIFT_NUM_FLOOR_SECTORS; i++) { MovableBlock_ShiftStackY( p->linked[i].pos.y, p->linked[i].pos, item->pos.y, item->room_num, false); } // Double check linked positions on save vs load. for (int32_t i = M_LIFT_NUM_FLOOR_SECTORS; i < M_LIFT_NUM_SECTORS; i++) { MovableBlock_ShiftStackY( p->linked[i].pos.y, p->linked[i].pos, item->pos.y - LIFT_HEIGHT, item->room_num, false); } } Item_Animate(item); // Update room number one click up to avoid lift on a room portal. int16_t room_num = item->room_num; Room_GetSector( (XYZ_32) { item->pos.x, item->pos.y - STEP_L, item->pos.z }, &room_num); Item_UpdateRoom(item_num, room_num); } static void M_AddWalkable(const int16_t item_num) { const ITEM *const item = Item_Get(item_num); VECTOR *positions = Vector_Create(sizeof(XYZ_32)); M_GetSectorPositions(item, positions); for (int32_t i = 0; i < positions->count; i++) { Walkable_Add(item_num, *(const XYZ_32 *)Vector_Get(positions, i)); } Vector_Free(positions); } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->floor_height_func = M_GetFloorHeight; obj->ceiling_height_func = M_GetCeilingHeight; obj->add_walkable_func = M_AddWalkable; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->save_position = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_LIFT, M_Setup) ================================================ FILE: src/trx/game/objects/general/lights/beacon_light.c ================================================ #include #include #include typedef struct { int32_t timer; } M_PRIV; static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; if (!Item_IsTriggerActive(item)) { return; } p->timer = (p->timer + 1) & 0x3F; if (p->timer < 3) { const uint8_t rg = 255 - (Random_GetControl() & 3); const uint8_t b = 255 - (Random_GetControl() & 0x1F); Output_AddDynamicLightRGB(item->pos, 16, (RGB_888) { rg, rg, b }); } } static void M_Setup(OBJECT *const obj) { obj->priv_size = sizeof(M_PRIV); obj->control_func = M_Control; obj->draw_func = nullptr; obj->save_flags = true; } REGISTER_OBJECT(O_BEACON_LIGHT, M_Setup) ================================================ FILE: src/trx/game/objects/general/lights/colored_light.c ================================================ #include #include typedef struct { RGB_888 color; } M_PRIV; static void M_InitialiseGeneric(const int16_t item_num, const RGB_888 color) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; p->color = color; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; if (!Item_IsTriggerActive(item)) { return; } Output_AddDynamicLightRGB(item->pos, 24, p->color); } static void M_SetupCommon(OBJECT *const obj) { obj->priv_size = sizeof(M_PRIV); obj->control_func = M_Control; obj->draw_func = nullptr; obj->save_flags = true; } #define M_DEFINE_LIGHT(name, r, g, b) \ static void M_Initialise##name(const int16_t num) \ { \ M_InitialiseGeneric(num, (RGB_888) { r, g, b }); \ } \ static void M_Setup##name(OBJECT *const obj) \ { \ M_SetupCommon(obj); \ obj->initialise_func = M_Initialise##name; \ } \ REGISTER_OBJECT(O_##name##_LIGHT, M_Setup##name) M_DEFINE_LIGHT(RED, 255, 0, 0) M_DEFINE_LIGHT(GREEN, 0, 255, 0) M_DEFINE_LIGHT(BLUE, 0, 0, 255) M_DEFINE_LIGHT(AMBER, 255, 192, 0) M_DEFINE_LIGHT(WHITE, 224, 224, 255) #undef M_DEFINE_LIGHT ================================================ FILE: src/trx/game/objects/general/lights/electrical_light.c ================================================ #include #include #include #include #include #include typedef struct { int32_t life; } M_PRIV; static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; int32_t rg, b; if (!Item_IsTriggerActive(item)) { p->life = 0; return; } if (p->life < 16) { rg = (Random_GetControl() % 8) << 2; b = rg + (Random_GetControl() % 4); p->life++; } else if (p->life < 96) { if (((int32_t)Output_GetTimeInGame() % 16) && (Random_GetControl() % 8)) { rg = Random_GetControl() % 8; } else { rg = 24 - (Random_GetControl() % 8); } b = rg + (Random_GetControl() % 4); p->life++; } else if (p->life < 160) { rg = 12 - (Random_GetControl() % 4); b = rg + (Random_GetControl() % 4); if (!(Random_GetControl() % 0x20) && p->life > 128) { p->life = 160; } else { p->life++; } } else { rg = 31 - (Random_GetControl() % 4); b = 31 - (Random_GetControl() % 2); if (item->object_id == O_FLICKERING_LIGHT && (Random_GetControl() < 0x200)) { p->life = 0; } } rg <<= 3; b <<= 3; Output_AddDynamicLightRGB(item->pos, 16, (RGB_888) { rg, rg, b }); } static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "life", &p->life)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { M_PRIV *const p = item->priv; JSONW_WRITE(io, "life", p->life); } static void M_Setup(OBJECT *const obj) { obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->control_func = M_Control; obj->draw_func = nullptr; obj->save_flags = true; } REGISTER_OBJECT(O_ELECTRICAL_LIGHT, M_Setup) REGISTER_OBJECT(O_FLICKERING_LIGHT, M_Setup) ================================================ FILE: src/trx/game/objects/general/lights/on_off_light.c ================================================ #include #include static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (!Item_IsTriggerActive(item)) { return; } Output_AddDynamicLightRGB(item->pos, 16, COLOR_RGB_888_WHITE); } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->draw_func = nullptr; obj->save_flags = true; } REGISTER_OBJECT(O_ON_OFF_LIGHT, M_Setup) ================================================ FILE: src/trx/game/objects/general/lights/pulse_light.c ================================================ #include #include #include #include typedef struct { int32_t cycle; } M_PRIV; static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; if (!Item_IsTriggerActive(item)) { return; } p->cycle += 728; int32_t falloff = ABS((32 * Math_Sin(p->cycle)) >> W2V_SHIFT); if (falloff > 31) { falloff = 31; } else if (falloff < 8) { falloff = 8; p->cycle += 2048; } Output_AddDynamicLightRGB(item->pos, falloff, (RGB_888) { 255, 96, 0 }); } static void M_Setup(OBJECT *const obj) { obj->priv_size = sizeof(M_PRIV); obj->control_func = M_Control; obj->draw_func = nullptr; obj->save_flags = true; } REGISTER_OBJECT(O_PULSE_LIGHT, M_Setup) ================================================ FILE: src/trx/game/objects/general/lights/strobe_light.c ================================================ #include #include #include #include #include #include #include #include #include typedef struct { int32_t life; bool alarm_active; } M_PRIV; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_OPTIONAL(JSON_READ(io, "life", &p->life)); JSON_OPTIONAL(JSON_READ(io, "alarm_active", &p->alarm_active)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "life", p->life); JSONW_WRITE(io, "alarm_active", p->alarm_active); } void M_TriggerAlertLight( const XYZ_32 pos, const RGB_888 color, const int16_t angle, const int16_t room_num) { GAME_VECTOR src = { .pos = pos, .room_num = room_num }; Room_GetSector(pos, &src.room_num); const int32_t dist = 8 * WALL_L; GAME_VECTOR dst = { .pos = { .x = pos.x + ((dist * Math_Sin(angle)) >> W2V_SHIFT), .y = pos.y, .z = pos.z + ((dist * Math_Cos(angle)) >> W2V_SHIFT), }, }; if (!LOS_Check(&src, &dst, false)) { Output_AddDynamicLightRGB(dst.pos, 8, color); } } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; if (!Item_IsTriggerActive(item)) { return; } if (GF_BadGetLevelNum() == 15 && !p->alarm_active) { return; } item->rot.y += 2912; const int16_t angle = item->rot.y + 0x5800; M_TriggerAlertLight( (XYZ_32) { item->pos.x, item->pos.y - WALL_L / 2, item->pos.z }, (RGB_888) { 255, 64, 0 }, angle, item->room_num); Output_AddDynamicLightRGB( (XYZ_32) { item->pos.x + ((STEP_L * Math_Sin(angle)) >> W2V_SHIFT), item->pos.y - STEP_L * 3, item->pos.z + ((STEP_L * Math_Cos(angle)) >> W2V_SHIFT), }, 6, (RGB_888) { 255, 96, 0 }); const int32_t time4 = Output_GetTimeInGame() * 4; if (!(time4 & 0x7F)) { Sound_Effect(SFX_ALARM_1, &item->pos, SPM_NORMAL); } p->life++; if (p->life > 60 * LOGIC_FPS) { p->alarm_active = false; p->life = 0; } } static void M_HandleEvent( ITEM *const item, const OBJECT_EVENT event, const void *const data) { M_PRIV *const p = item->priv; if (event != OBJECT_EVENT_ALERT) { return; } p->alarm_active = true; p->life = 0; } static void M_Setup(OBJECT *const obj) { obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->control_func = M_Control; obj->event_func = M_HandleEvent; obj->save_flags = true; } REGISTER_OBJECT(O_STROBE_LIGHT, M_Setup) ================================================ FILE: src/trx/game/objects/general/mini_copter.c ================================================ #include #include #include #include #include static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); const ITEM *const lara_item = Lara_GetItem(); item->pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, 100); XYZ_32 pos = lara_item->pos; pos.x += ((item->pos.x - lara_item->pos.x) >> 2); pos.y += ((item->pos.y - lara_item->pos.y) >> 2); pos.z += ((item->pos.z - lara_item->pos.z) >> 2); Sound_Effect(SFX_HELICOPTER_LOOP, &pos, SPM_NORMAL); if (ABS(item->pos.z - lara_item->pos.z) > WALL_L * 30) { Item_Kill(item_num); } Item_Animate(item); int16_t room_num = item->room_num; Room_GetSector(item->pos, &room_num); Item_UpdateRoom(item_num, room_num); } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->save_position = true; obj->save_flags = true; } REGISTER_OBJECT(O_MINI_COPTER, M_Setup) ================================================ FILE: src/trx/game/objects/general/moving_bar.c ================================================ #include #include typedef enum { MOVING_BAR_STATE_INACTIVE = 0, MOVING_BAR_STATE_ACTIVE = 1, } MOVING_BAR_STATE; static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (Item_IsTriggerActive(item)) { item->goal_anim_state = MOVING_BAR_STATE_ACTIVE; } else { item->goal_anim_state = MOVING_BAR_STATE_INACTIVE; } Item_Animate(item); int16_t room_num = item->room_num; Room_GetSector(item->pos, &room_num); Item_UpdateRoom(item_num, room_num); } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->collision_func = Object_Collision; obj->save_flags = true; obj->save_anim = true; obj->save_position = true; } REGISTER_OBJECT(O_MOVING_BAR, M_Setup) ================================================ FILE: src/trx/game/objects/general/pickup.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // clang-format off #define M_LF_PICKUP_ERASE 42 #define M_LF_PICKUP_FLARE 58 #define M_LF_PICKUP_FLARE_UW 20 #define M_LF_PICKUP_UW 18 #define M_LF_PICKUP_CROUCH_1 20 #define M_LF_PICKUP_CROUCH_2 22 #define M_LF_PICKUP_CROUCH_FLARE 22 #define M_LF_PICKUP_CRAWL 20 #define M_AID_DIST_MIN (STEP_L * 5) // 1280 #define M_AID_DIST_MAX (WALL_L * 8) // 8192 #define M_AID_WAIT_MIN (LOGIC_FPS * 2.5) // 75 #define M_AID_WAIT_MAX (LOGIC_FPS * 5) // 150 #define M_AID_WAIT_BREAK_CHANCE 0x1200 // clang-format on static const OBJECT_BOUNDS m_PickUpBounds = { .shift = { .min = { .x = -WALL_L / 4, .y = -100, .z = -WALL_L / 4, }, .max = { .x = +WALL_L / 4, .y = +100, .z = +WALL_L / 4, }, }, .rot = { .min = { .x = -10 * DEG_1, .y = 0, .z = 0, }, .max = { .x = +10 * DEG_1, .y = 0, .z = 0, }, }, }; static const OBJECT_BOUNDS m_PickUpBoundsControlled = { .shift = { .min = { .x = -WALL_L / 4, .y = -200, .z = -WALL_L / 4, }, .max = { .x = +WALL_L / 4, .y = +200, .z = +WALL_L / 4, }, }, .rot = { .min = { .x = -10 * DEG_1, .y = 0, .z = 0, }, .max = { .x = +10 * DEG_1, .y = 0, .z = 0, }, }, }; static const OBJECT_BOUNDS m_PickUpBoundsUW = { .shift = { .min = { .x = -WALL_L / 2, .y = -WALL_L / 2, .z = -WALL_L / 2, }, .max = { .x = +WALL_L / 2, .y = +WALL_L / 2, .z = +WALL_L / 2, }, }, .rot = { .min = { .x = -45 * DEG_1, .y = -45 * DEG_1, .z = -45 * DEG_1, }, .max = { .x = +45 * DEG_1, .y = +45 * DEG_1, .z = +45 * DEG_1, }, }, }; static const XYZ_32 m_PickupPosition = { .x = 0, .y = 0, .z = -100 }; static const XYZ_32 m_PickupPositionUW = { .x = 0, .y = -200, .z = -350 }; static const OBJECT_ID m_QuestObjects[] = { // clang-format off O_QUEST_ITEM_1, O_QUEST_ITEM_2, O_QUEST_ITEM_3, O_QUEST_ITEM_4, NO_OBJECT, // clang-format on }; typedef struct { int32_t aid_timer; uint32_t secret_mask; } M_PRIV; uint32_t Pickup_GetSecretMask(const ITEM *const item) { const M_PRIV *const p = item->priv; return p->secret_mask; } static void M_Initialise(int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; p->aid_timer = -1; p->secret_mask = 0; if (Object_IsType(item->object_id, g_SecretObjects)) { const GF_LEVEL *const level = Game_GetCurrentLevel(); p->secret_mask = Stats_GetSecretMaskForItem(level, item_num); } if (item->status != IS_INVISIBLE) { Item_AddActive(item_num); } } static void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage) { if (stage == SAVEGAME_STAGE_AFTER_LOAD) { if (item->status == IS_DEACTIVATED) { const int16_t item_num = Item_GetIndex(item); Item_RemoveDrawn(item_num); } } } static bool M_Trigger(ITEM *const item, const TRIGGER *const trigger) { if (trigger == nullptr) { return false; } if (trigger->type == TT_SWITCH) { item->flags ^= trigger->mask; } else if (trigger->type == TT_ANTIPAD || trigger->type == TT_ANTITRIGGER) { item->flags &= ~trigger->mask; } else { item->flags |= trigger->mask; } if ((item->flags & IF_CODE_BITS) != IF_CODE_BITS) { item->status = IS_INVISIBLE; item->flags |= IF_KILLED; } else if (item->status == IS_INVISIBLE) { item->touch_bits = 0; item->status = IS_ACTIVE; const int16_t item_num = Item_GetIndex(item); Item_AddActive(item_num); } return false; } static void M_SpawnPickupAid(const ITEM *const item) { const OBJECT_ID obj_id = Object_GetCognate(item->object_id, g_ItemToInvObjectMap); if (obj_id == NO_OBJECT) { return; } const OBJECT *const obj = Object_Get(obj_id); const ANIM_FRAME *const frame = obj->frame_base; if (!obj->loaded || frame == nullptr) { return; } const GAME_VECTOR pos = { .x = item->pos.x + 20 * (Random_GetDraw() - 0x4000) / 0x4000, .y = item->pos.y - ABS(frame->bounds.max.y - frame->bounds.min.y) - 10 * (1 + (Random_GetDraw() - 0x4000) / 0x4000), .z = item->pos.z + 20 * (Random_GetDraw() - 0x4000) / 0x4000, .room_num = item->room_num, }; if (g_TRVersion >= 3) { for (int32_t i = 0; i < (Random_GetControl() & 3) + 4; i++) { Sparks_TriggerPickupAid(pos.pos, (XZ_32) {}); } } else { const int16_t effect_num = Effect_Create(pos.room_num); if (effect_num != NO_EFFECT) { EFFECT *const effect = Effect_Get(effect_num); effect->room_num = pos.room_num; effect->pos = pos.pos; effect->counter = 0; effect->object_id = O_PICKUP_AID; effect->frame_num = 0; } } } static void M_ControlPickupAids(ITEM *const item) { const ITEM *const lara = Lara_GetItem(); if (item->fall_speed != 0 || lara == nullptr || !Object_Get(O_PICKUP_AID)->loaded) { return; } const int32_t distance = Item_GetDistance(lara, item->pos); if (distance < M_AID_DIST_MIN || distance > M_AID_DIST_MAX) { return; } M_PRIV *const p = item->priv; int32_t timer = p->aid_timer; if (timer <= 0 || (timer < M_AID_WAIT_MIN && Random_GetDraw() < M_AID_WAIT_BREAK_CHANCE)) { M_SpawnPickupAid(item); timer = M_AID_WAIT_MAX; } else { timer--; } p->aid_timer = timer; } static void M_ControlPickupLights(ITEM *const item) { const int16_t timer = Output_GetTimeInGame(); const int16_t angle = Math_Cos((timer & 0x3F) << 10); int32_t c = ABS(angle >> 9); CLAMPG(c, 31); c <<= 3; Output_AddDynamicLightRGB(item->pos, 8, (RGB_888) { 0, c, c >> 1 }); } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (item->status == IS_INVISIBLE || item->status == IS_DEACTIVATED) { Item_RemoveActive(item_num); return; } if (item->room_num == NO_ROOM) { return; } if (g_TRVersion == 3 && Object_IsType(item->object_id, m_QuestObjects)) { item->rot.y += 1024; M_ControlPickupLights(item); } else if (g_Config.gameplay.enable_pickup_aids) { M_ControlPickupAids(item); } } static void M_DoPickup(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (item->object_id == O_FLARE_ITEM) { return; } Overlay_AddDisplayPickup(item->object_id); Inv_AddPickup(item); Stats_AddPickup(); // Notify Lua pickup listeners Lua_FireEventInt32(LUA_EVENT_PICKUP, item_num); // LUA uses 1-indexing item->status = IS_INVISIBLE; item->flags |= IF_KILLED; if (g_TRVersion == 3 && Object_IsType(item->object_id, m_QuestObjects)) { if (GF_BadGetLevelNum() == 19 || (GF_BadIsMod("tr3-la") && GF_BadGetLevelNum() == 4)) { Item_Kill(item_num); } else { Game_SetIsLevelComplete(true); } } else { Item_RemoveDrawn(item_num); Item_RemoveActive(item_num); } LARA_INFO *const lara = Lara_GetLaraInfo(); lara->interact_target.is_moving = false; } static void M_DoFlarePickup(const int16_t item_num) { const ITEM *const item = Item_Get(item_num); LARA_INFO *const lara = Lara_GetLaraInfo(); lara->request_gun_type = LGT_FLARE; lara->gun_type = LGT_FLARE; Gun_InitialiseNewWeapon(); lara->gun_status = LGS_SPECIAL; lara->flare.age = FlareItem_GetAge(item); Item_Kill(item_num); lara->interact_target.is_moving = false; } static void M_GetAllAtLaraPos(const ITEM *const item) { int16_t pickup_num = Room_Get(item->room_num)->item_num; while (pickup_num != NO_ITEM) { ITEM *const check_item = Item_Get(pickup_num); if (check_item->pos.x == item->pos.x && check_item->pos.z == item->pos.z && Object_Get(check_item->object_id)->collision_func == Pickup_Collision) { M_DoPickup(pickup_num); } pickup_num = check_item->next_item; } } static void M_DoControlled(const int16_t item_num, ITEM *const lara_item) { ITEM *const item = Item_Get(item_num); const XYZ_16 old_rot = item->rot; item->rot.x = 0; item->rot.y = lara_item->rot.y; item->rot.z = 0; LARA_INFO *const lara = Lara_GetLaraInfo(); if ((g_Input.action && lara->gun_status == LGS_ARMLESS && !lara_item->gravity && lara_item->current_anim_state == LS(LS_STOP) && !lara->interact_target.is_moving) || (lara->interact_target.is_moving && lara->interact_target.item_num == item_num)) { const OBJECT *const obj = Object_Get(item->object_id); if (Lara_TestPosition(item, obj->bounds_func())) { const XYZ_32 pos = { .x = m_PickupPosition.x, .y = lara_item->pos.y - item->pos.y, .z = m_PickupPosition.z, }; if (Lara_MovePosition(item, &pos)) { Item_SwitchToAnim(lara_item, LA(LA_PICKUP), 0); lara_item->current_anim_state = LS(LS_PICKUP); lara->head_rot.y = 0; lara->head_rot.x = 0; lara->torso_rot.y = 0; lara->torso_rot.x = 0; lara->interact_target.is_moving = false; lara->gun_status = LGS_HANDS_BUSY; } lara->interact_target.item_num = item_num; } else if ( lara->interact_target.is_moving && lara->interact_target.item_num == item_num) { lara->interact_target.is_moving = false; lara->interact_target.item_num = NO_ITEM; lara->gun_status = LGS_ARMLESS; } goto cleanup; } if (lara->interact_target.item_num != item_num) { goto cleanup; } if (lara_item->current_anim_state == LS(LS_PICKUP)) { if (Item_TestFrameEqual(lara_item, M_LF_PICKUP_ERASE)) { M_GetAllAtLaraPos(item); lara->interact_target.item_num = NO_ITEM; } goto cleanup; } cleanup: item->rot = old_rot; } static inline bool M_HasValidPickupState(const ITEM *const lara_item) { // TODO: unify under a pickup style config option, but retain sprint slide // test in TR1/2 mode. Snap-pickups in crawl state do not make sense, so // these always use TR3-style. const LARA_TRX_ANIMATION anim = LA_U(Item_GetRelativeAnim(lara_item)); const LARA_TRX_STATE state = LS_U(lara_item->current_anim_state); if (g_TRVersion < 3) { if (anim == LA_SPRINT_SLIDE_STAND_RIGHT || anim == LA_SPRINT_SLIDE_STAND_LEFT) { return false; } if (state == LS_STOP) { return true; } } return (state == LS_STOP && anim == LA_STAND_IDLE) || (state == LS_CROUCH_IDLE && anim == LA_CROUCH_IDLE) || (state == LS_CRAWL_IDLE && anim == LA_CRAWL_IDLE && g_Config.gameplay.enable_responsive_crawl); } static void M_DoAboveWater(const int16_t item_num, ITEM *const lara_item) { ITEM *const item = Item_Get(item_num); const LARA_TRX_ANIMATION anim = LA_U(Item_GetRelativeAnim(lara_item)); // clang-format off const bool is_ducked = ( anim == LA_CRAWL_IDLE || anim == LA_CRAWL_PICKUP || anim == LA_CROUCH_IDLE || anim == LA_CROUCH_PICKUP || anim == LA_CROUCH_PICKUP_FLARE); // clang-format on if (g_Config.gameplay.enable_walk_to_items && !is_ducked && item->object_id != O_FLARE_ITEM) { M_DoControlled(item_num, lara_item); return; } const OBJECT *const obj = Object_Get(item->object_id); const XYZ_16 old_rot = item->rot; item->rot = lara_item->rot; if (!Lara_TestPosition(item, obj->bounds_func())) { goto cleanup; } if (lara_item->current_anim_state == LS(LS_PICKUP)) { const int16_t rel_frame = Item_GetRelativeFrame(lara_item); const bool pickup_now = (anim == LA_PICKUP && rel_frame == M_LF_PICKUP_ERASE) || (anim == LA_CROUCH_PICKUP && (rel_frame == M_LF_PICKUP_CROUCH_1 || rel_frame == M_LF_PICKUP_CROUCH_2)) || (anim == LA_CRAWL_PICKUP && rel_frame == M_LF_PICKUP_CRAWL); if (pickup_now) { M_DoPickup(item_num); } goto cleanup; } LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara_item->current_anim_state == LS(LS_FLARE_PICKUP)) { const int16_t rel_frame = Item_GetRelativeFrame(lara_item); const bool pickup_now = Item_TestFrameEqual(lara_item, M_LF_PICKUP_FLARE) || (anim == LA_CROUCH_PICKUP_FLARE && rel_frame == M_LF_PICKUP_CROUCH_FLARE); if (pickup_now && item->object_id == O_FLARE_ITEM && lara->gun_type != LGT_FLARE) { M_DoFlarePickup(item_num); } goto cleanup; } const bool is_flare_item = item->object_id == O_FLARE_ITEM; if (g_Input.action && lara_item->current_anim_state == LS(LS_CRAWL_IDLE) && (is_flare_item || !g_Config.gameplay.enable_responsive_crawl)) { lara_item->goal_anim_state = LS(LS_CROUCH_IDLE); goto cleanup; } if (g_Input.action && !lara_item->gravity && (lara->gun_status == LGS_ARMLESS || anim == LA_CRAWL_IDLE) && (lara->gun_type != LGT_FLARE || !is_flare_item) && M_HasValidPickupState(lara_item)) { if (is_flare_item) { Lara_AnimateUntil(lara_item, LS(LS_FLARE_PICKUP)); } else { Lara_AlignPosition(item, &m_PickupPosition); Lara_AnimateUntil(lara_item, LS(LS_PICKUP)); } if (is_ducked) { lara_item->goal_anim_state = LS(anim == LA_CRAWL_IDLE ? LS_CRAWL_IDLE : LS_CROUCH_IDLE); } else { lara_item->goal_anim_state = LS(LS_STOP); } lara->gun_status = LGS_HANDS_BUSY; lara->head_rot.y = 0; lara->head_rot.x = 0; lara->torso_rot.y = 0; lara->torso_rot.x = 0; goto cleanup; } cleanup: item->rot = old_rot; } static void M_DoUnderwater(const int16_t item_num, ITEM *const lara_item) { ITEM *const item = Item_Get(item_num); const OBJECT *const obj = Object_Get(item->object_id); const XYZ_16 old_rot = item->rot; item->rot.x = -25 * DEG_1; item->rot.y = lara_item->rot.y; item->rot.z = 0; if (!Lara_TestPosition(item, obj->bounds_func())) { goto cleanup; } if (lara_item->current_anim_state == LS(LS_PICKUP)) { if (Item_TestFrameEqual(lara_item, M_LF_PICKUP_UW)) { M_DoPickup(item_num); } goto cleanup; } const LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara_item->current_anim_state == LS(LS_FLARE_PICKUP)) { if (Item_TestFrameEqual(lara_item, M_LF_PICKUP_FLARE_UW) && item->object_id == O_FLARE_ITEM && lara->gun_type != LGT_FLARE) { M_DoFlarePickup(item_num); Lara_Flare_DrawMeshes(); } goto cleanup; } if (g_Input.action && lara_item->current_anim_state == LS(LS_TREAD) && lara->gun_status == LGS_ARMLESS && (lara->gun_type != LGT_FLARE || item->object_id != O_FLARE_ITEM)) { if (!Lara_MovePosition(item, &m_PickupPositionUW)) { goto cleanup; } if (item->object_id == O_FLARE_ITEM) { lara_item->fall_speed = 0; Item_SwitchToAnim(lara_item, LA(LA_UNDERWATER_FLARE_PICKUP), 0); lara_item->current_anim_state = LS(LS_FLARE_PICKUP); } else { if (g_Config.gameplay.fix_lara_pickup_embed) { lara_item->fall_speed = 0; } Lara_AnimateUntil(lara_item, LS(LS_PICKUP)); } lara_item->goal_anim_state = LS(LS_TREAD); goto cleanup; } cleanup: item->rot = old_rot; } static void M_Setup(OBJECT *const obj) { obj->trigger_func = M_Trigger; obj->control_func = M_Control; obj->collision_func = Pickup_Collision; obj->bounds_func = Pickup_Bounds; obj->draw_func = Object_DrawPickupItem; obj->initialise_func = M_Initialise; obj->handle_save_func = M_HandleSave; obj->priv_size = sizeof(M_PRIV); obj->save_position = true; obj->save_flags = true; } const OBJECT_BOUNDS *Pickup_Bounds(void) { const LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->water_status == LWS_UNDERWATER || lara->water_status == LWS_CHEAT) { return &m_PickUpBoundsUW; } else if (g_Config.gameplay.enable_walk_to_items) { return &m_PickUpBoundsControlled; } else { return &m_PickUpBounds; } } bool Pickup_Trigger(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (item->status != IS_INVISIBLE) { return false; } item->status = IS_DEACTIVATED; return true; } void Pickup_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { const ITEM *const item = Item_Get(item_num); if ((item->flags & IF_INVISIBLE) != 0) { return; } const LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->water_status == LWS_ABOVE_WATER || lara->water_status == LWS_WADE) { M_DoAboveWater(item_num, lara_item); } else if ( lara->water_status == LWS_UNDERWATER || lara->water_status == LWS_CHEAT) { M_DoUnderwater(item_num, lara_item); } } REGISTER_OBJECT(O_EXPLOSIVE_ITEM, M_Setup) REGISTER_OBJECT(O_FLAREBOX_ITEM, M_Setup) REGISTER_OBJECT(O_GRENADE_AMMO_ITEM, M_Setup) REGISTER_OBJECT(O_GRENADE_GUN_ITEM, M_Setup) REGISTER_OBJECT(O_ROCKET_AMMO_ITEM, M_Setup) REGISTER_OBJECT(O_ROCKET_GUN_ITEM, M_Setup) REGISTER_OBJECT(O_HARPOON_AMMO_ITEM, M_Setup) REGISTER_OBJECT(O_HARPOON_ITEM, M_Setup) REGISTER_OBJECT(O_KEY_ITEM_1, M_Setup) REGISTER_OBJECT(O_KEY_ITEM_2, M_Setup) REGISTER_OBJECT(O_KEY_ITEM_3, M_Setup) REGISTER_OBJECT(O_KEY_ITEM_4, M_Setup) REGISTER_OBJECT(O_LARGE_MEDIPACK_ITEM, M_Setup) REGISTER_OBJECT(O_LEADBAR_ITEM, M_Setup) REGISTER_OBJECT(O_M16_AMMO_ITEM, M_Setup) REGISTER_OBJECT(O_M16_ITEM, M_Setup) REGISTER_OBJECT(O_MP5_AMMO_ITEM, M_Setup) REGISTER_OBJECT(O_MP5_ITEM, M_Setup) REGISTER_OBJECT(O_MAGNUM_AMMO_ITEM, M_Setup) REGISTER_OBJECT(O_MAGNUM_ITEM, M_Setup) REGISTER_OBJECT(O_AUTOS_AMMO_ITEM, M_Setup) REGISTER_OBJECT(O_AUTOS_ITEM, M_Setup) REGISTER_OBJECT(O_DESERT_EAGLE_AMMO_ITEM, M_Setup) REGISTER_OBJECT(O_DESERT_EAGLE_ITEM, M_Setup) REGISTER_OBJECT(O_PICKUP_ITEM_1, M_Setup) REGISTER_OBJECT(O_PICKUP_ITEM_2, M_Setup) REGISTER_OBJECT(O_QUEST_ITEM_1, M_Setup) REGISTER_OBJECT(O_QUEST_ITEM_2, M_Setup) REGISTER_OBJECT(O_QUEST_ITEM_3, M_Setup) REGISTER_OBJECT(O_QUEST_ITEM_4, M_Setup) REGISTER_OBJECT(O_PISTOL_AMMO_ITEM, M_Setup) REGISTER_OBJECT(O_PISTOL_ITEM, M_Setup) REGISTER_OBJECT(O_PUZZLE_ITEM_1, M_Setup) REGISTER_OBJECT(O_PUZZLE_ITEM_2, M_Setup) REGISTER_OBJECT(O_PUZZLE_ITEM_3, M_Setup) REGISTER_OBJECT(O_PUZZLE_ITEM_4, M_Setup) REGISTER_OBJECT(O_SCION_ITEM_2, M_Setup) REGISTER_OBJECT(O_SECRET_1, M_Setup) REGISTER_OBJECT(O_SECRET_2, M_Setup) REGISTER_OBJECT(O_SECRET_3, M_Setup) REGISTER_OBJECT(O_SHOTGUN_AMMO_ITEM, M_Setup) REGISTER_OBJECT(O_SHOTGUN_ITEM, M_Setup) REGISTER_OBJECT(O_SMALL_MEDIPACK_ITEM, M_Setup) REGISTER_OBJECT(O_UZI_AMMO_ITEM, M_Setup) REGISTER_OBJECT(O_UZI_ITEM, M_Setup) ================================================ FILE: src/trx/game/objects/general/pickup.h ================================================ #pragma once #include #include bool Pickup_Trigger(int16_t item_num); const OBJECT_BOUNDS *Pickup_Bounds(void); void Pickup_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); uint32_t Pickup_GetSecretMask(const ITEM *item); ================================================ FILE: src/trx/game/objects/general/puzzle_hole.c ================================================ #include #include #include #include #include #include #include #define M_LF_USE_PUZZLE 80 static XYZ_32 m_PuzzleHolePosition = { .x = 0, .y = 0, .z = WALL_L / 2 - LARA_RADIUS - 85, }; static const OBJECT_BOUNDS m_PuzzleHoleBounds = { .shift = { .min = { .x = -200, .y = 0, .z = WALL_L / 2 - 200, }, .max = { .x = +200, .y = 0, .z = WALL_L / 2, }, }, .rot = { .min = { .x = -10 * DEG_1, .y = -30 * DEG_1, .z = -10 * DEG_1, }, .max = { .x = +10 * DEG_1, .y = +30 * DEG_1, .z = +10 * DEG_1, }, }, }; static const OBJECT_BOUNDS *M_Bounds(void) { return &m_PuzzleHoleBounds; } static bool M_IsUsable(const int16_t item_num) { const ITEM *const item = Item_Get(item_num); return item->status == IS_INACTIVE; } static void M_Use(ITEM *const lara_item, ITEM *const receptacle_item) { LARA_INFO *const lara = Lara_GetLaraInfo(); Lara_AlignPosition(receptacle_item, &m_PuzzleHolePosition); Lara_AnimateUntil(lara_item, LS(LS_USE_PUZZLE)); lara_item->goal_anim_state = LS(LS_STOP); lara->gun_status = LGS_HANDS_BUSY; lara->interact_target.is_moving = false; } static void M_ConsumeKeyItem(ITEM *const receptacle_item) { LARA_INFO *const lara = Lara_GetLaraInfo(); const OBJECT_ID key_object_id = Object_FindReceptacleKey(receptacle_item->object_id); if (key_object_id != NO_OBJECT) { Inv_RemoveItem(key_object_id); } lara->interact_target.item_num = NO_ITEM; } static void M_MarkDone(ITEM *const receptacle_item) { const OBJECT_ID done_obj_id = Object_GetCognate( receptacle_item->object_id, g_ReceptacleToReceptacleDoneMap); if (done_obj_id != NO_OBJECT) { receptacle_item->object_id = done_obj_id; } if (receptacle_item->status == IS_ACTIVE) { return; } Item_SwitchToObjAnim(receptacle_item, 0, 0, receptacle_item->object_id); const ANIM *const anim = Item_GetAnim(receptacle_item); receptacle_item->current_anim_state = anim->current_anim_state; receptacle_item->goal_anim_state = receptacle_item->current_anim_state; receptacle_item->required_anim_state = 0; receptacle_item->flags = IF_CODE_BITS; receptacle_item->status = IS_ACTIVE; Item_AddActive(Item_GetIndex(receptacle_item)); Item_Animate(receptacle_item); } static void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage) { if (stage == SAVEGAME_STAGE_AFTER_LOAD) { if (item->status == IS_DEACTIVATED || item->status == IS_ACTIVE) { M_MarkDone(item); } } } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (Item_IsTriggerActive(item)) { Item_Animate(item); } } static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); const OBJECT *const obj = Object_Get(item->object_id); const LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara_item->current_anim_state != LS(LS_STOP)) { if (lara_item->current_anim_state == LS(LS_USE_PUZZLE) && Lara_TestPosition(item, obj->bounds_func()) && Item_TestFrameEqual(lara_item, M_LF_USE_PUZZLE)) { M_ConsumeKeyItem(item); M_MarkDone(item); } return; } if (lara->interact_target.is_moving && lara->interact_target.item_num == item_num) { M_Use(lara_item, item); } if (!g_Input.action || lara->gun_status != LGS_ARMLESS || lara_item->gravity) { return; } if (!Lara_TestPosition(item, obj->bounds_func())) { return; } if (!GF_ShowInventoryKeys(item->object_id)) { Lara_RefuseInteraction(); } } static void M_CollisionDone( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); const OBJECT *const obj = Object_Get(item->object_id); const LARA_INFO *const lara = Lara_GetLaraInfo(); if (!g_Input.action || lara->gun_status != LGS_ARMLESS || lara_item->gravity || lara_item->current_anim_state != LS(LS_STOP) || !Lara_TestPosition(item, obj->bounds_func())) { return; } // Trying to interact with a complete puzzle hole Lara_RefuseInteraction(); } static void M_SetupEmpty(OBJECT *const obj) { obj->control_func = M_Control; obj->collision_func = M_Collision; obj->handle_save_func = M_HandleSave; obj->is_usable_func = M_IsUsable; obj->bounds_func = M_Bounds; obj->save_flags = true; obj->save_anim = true; } static void M_SetupDone(OBJECT *const obj) { obj->control_func = M_Control; obj->collision_func = M_CollisionDone; obj->bounds_func = M_Bounds; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_PUZZLE_HOLE_1, M_SetupEmpty) REGISTER_OBJECT(O_PUZZLE_HOLE_2, M_SetupEmpty) REGISTER_OBJECT(O_PUZZLE_HOLE_3, M_SetupEmpty) REGISTER_OBJECT(O_PUZZLE_HOLE_4, M_SetupEmpty) REGISTER_OBJECT(O_PUZZLE_DONE_1, M_SetupDone) REGISTER_OBJECT(O_PUZZLE_DONE_2, M_SetupDone) REGISTER_OBJECT(O_PUZZLE_DONE_3, M_SetupDone) REGISTER_OBJECT(O_PUZZLE_DONE_4, M_SetupDone) ================================================ FILE: src/trx/game/objects/general/rocket.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_BLAST_RADIUS WALL_L // = 1024 #define M_SPEED (WALL_L / 2) // = 512 #define M_SPEED_UW (STEP_L / 2) // = 128 static void M_SetTR3ProjectileShade(ITEM *const item) { if (item == nullptr) { return; } // OG TR3 uses `item->shade = -0x3DF0` on projectiles; in TRX any negative // shade forces the dynamic/smoothed lighting path. item->shade.value_1 = -1; item->shade.value_2 = -1; } static XYZ_32 M_GetLocalZOffset(const ITEM *const item, const int32_t dist) { const int32_t cx = Math_Cos(item->rot.x); const int32_t sx = Math_Sin(item->rot.x); const int32_t cy = Math_Cos(item->rot.y); const int32_t sy = Math_Sin(item->rot.y); const int32_t horz = (dist * cx) >> W2V_SHIFT; return (XYZ_32) { .x = (horz * sy) >> W2V_SHIFT, .y = -(dist * sx) >> W2V_SHIFT, .z = (horz * cy) >> W2V_SHIFT, }; } static void M_Explode(const int16_t rocket_item_num, const XYZ_32 pos) { const ITEM *const rocket_item = Item_Get(rocket_item_num); const ROOM *const room = Room_Get(rocket_item->room_num); const bool is_underwater = room != nullptr && room->flags.underwater; if (g_TRVersion == 3) { if (is_underwater) { Sparks_TriggerUnderwaterExplosion(rocket_item); } else { Sparks_TriggerExplosionSparks(pos, 3, -2, 0, rocket_item->room_num); for (int32_t i = 0; i < 2; i++) { Sparks_TriggerExplosionSparks( pos, 3, -1, 0, rocket_item->room_num); } } } else { const int16_t effect_num = Effect_Create(rocket_item->room_num); if (effect_num != NO_EFFECT) { EFFECT *const effect = Effect_Get(effect_num); effect->pos = pos; effect->speed = 0; effect->frame_num = 0; effect->counter = 0; effect->object_id = O_EXPLOSION_1; } } const XYZ_32 *const sfx_pos = g_TRVersion >= 3 ? &rocket_item->pos : nullptr; const uint32_t flags = g_TRVersion >= 3 ? (0x1800000 | SPM_PITCH) : SPM_NORMAL; Sound_Effect(SFX_EXPLOSION_1, sfx_pos, flags); Sound_Effect(SFX_EXPLOSION_2, sfx_pos, SPM_NORMAL); Item_Kill(rocket_item_num); Creature_AlertNearbyGuards(rocket_item); } static bool M_CanExplodeTarget(const ITEM *const item) { const OBJECT *const object = Object_Get(item->object_id); if (object->can_be_exploded_func != nullptr) { return object->can_be_exploded_func(item); } const ITEM_ACTION action = ItemAction_ToGameID(ITEM_ACTION_FINISH_LEVEL); for (int32_t i = 0; i < object->anim_count; i++) { const ANIM *const anim = Object_GetAnim(object, i); if (Anim_HasFXCommand(anim, action)) { return false; } } return true; } static bool M_TryExplodeItem( const ITEM *const projectile_item, const GAME_VECTOR old_pos, const int16_t target_item_num, const int32_t radius) { ITEM *const target_item = Item_Get(target_item_num); const OBJECT *const target_obj = Object_Get(target_item->object_id); if (target_item == Lara_GetItem()) { return false; } if (!target_item->collidable) { return false; } if (target_item->status == IS_INVISIBLE || target_obj->collision_func == nullptr) { return false; } if (!Item_CanBeProjectileTarget(target_item)) { return false; } const ANIM_FRAME *const frame = Item_GetBestFrame(target_item); const BOUNDS_16 *const bounds = &frame->bounds; const int32_t cdy = projectile_item->pos.y - target_item->pos.y; if (cdy + radius < bounds->min.y || cdy - radius > bounds->max.y) { return false; } const int32_t cy = Math_Cos(target_item->rot.y); const int32_t sy = Math_Sin(target_item->rot.y); const int32_t cdx = projectile_item->pos.x - target_item->pos.x; const int32_t cdz = projectile_item->pos.z - target_item->pos.z; const int32_t odx = old_pos.x - target_item->pos.x; const int32_t odz = old_pos.z - target_item->pos.z; const int32_t rx = (cy * cdx - sy * cdz) >> W2V_SHIFT; const int32_t sx = (cy * odx - sy * odz) >> W2V_SHIFT; if ((rx + radius < bounds->min.x && sx + radius < bounds->min.x) || (rx - radius > bounds->max.x && sx - radius > bounds->max.x)) { return false; } const int32_t rz = (sy * cdx + cy * cdz) >> W2V_SHIFT; const int32_t sz = (sy * odx + cy * odz) >> W2V_SHIFT; if ((rz + radius < bounds->min.z && sz + radius < bounds->min.z) || (rz - radius > bounds->max.z && sz - radius > bounds->max.z)) { return false; } if (!Item_CanTakeDamage(target_item)) { return false; } const GAME_VECTOR hit_pos = { .pos = projectile_item->pos, .room_num = projectile_item->room_num, }; Gun_HitTarget( target_item, &old_pos, &hit_pos, g_Weapons[LGT_ROCKET].damage); Stats_AddAmmoHits(); if (Gun_GetSmashPolicy(target_item) == GUN_SMASH_POLICY_HEAVY) { if (Object_IsType(projectile_item->object_id, g_HeavyMissileObjects)) { Gun_SmashItem(target_item_num); } } else if (Gun_GetSmashPolicy(target_item) != GUN_SMASH_POLICY_NONE) { Gun_SmashItem(target_item_num); } else if ( target_item->hit_points <= 0 && M_CanExplodeTarget(target_item)) { Creature_Die(target_item_num, true); } return true; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); const GAME_VECTOR old_pos = { .pos = item->pos, .room_num = item->room_num, }; const ROOM *const room = Room_Get(item->room_num); const bool was_underwater = room != nullptr && room->flags.underwater; if (was_underwater) { if (item->speed < M_SPEED_UW) { item->speed += (item->speed >> 2) + 4; CLAMPG(item->speed, M_SPEED_UW); } else { item->speed -= item->speed >> 2; } item->rot.z += DEG_1 * ((item->speed >> 3) + 3); } else { if (item->speed < M_SPEED) { item->speed += (item->speed >> 2) + 4; } item->rot.z += DEG_1 * ((item->speed >> 2) + 7); } if (g_TRVersion == 3) { M_SetTR3ProjectileShade(item); const XYZ_32 back_128 = M_GetLocalZOffset(item, -128); const int32_t back_dist = -1536 - (Random_GetControl() & 0x1FF); const XYZ_32 back_vel = M_GetLocalZOffset(item, back_dist); const int32_t time4 = Output_GetTimeInGame() * 4; if ((time4 & 4) != 0) { Sparks_TriggerRocketFlame( back_128, (XYZ_32) { .x = back_vel.x - back_128.x, .y = back_vel.y - back_128.y, .z = back_vel.z - back_128.z, }, item_num, item->room_num); } Sparks_TriggerRocketSmoke( (XYZ_32) { .x = item->pos.x + back_128.x, .y = item->pos.y + back_128.y, .z = item->pos.z + back_128.z, }, -1, item->room_num); if (was_underwater) { const XYZ_32 bubble_pos = { .x = item->pos.x + back_128.x, .y = item->pos.y + back_128.y, .z = item->pos.z + back_128.z, }; Spawn_BubbleEx(&bubble_pos, item->room_num, 4, 8); } if (g_Config.visuals.enable_gun_lighting) { const int32_t rnd = Random_GetControl(); const XYZ_32 light_pos = { .x = item->pos.x + back_128.x + (rnd & 0xF) - 8, .y = item->pos.y + back_128.y + ((rnd >> 4) & 0xF) - 8, .z = item->pos.z + back_128.z + ((rnd >> 8) & 0xF) - 8, }; const int32_t c = Random_GetControl(); const RGB_888 color = { .r = (uint8_t)((c & 0x1F) + 224), .g = (uint8_t)(((c >> 5) & 0x3F) + 128), .b = (uint8_t)((c >> 11) & 0x3F), }; Output_AddDynamicLightRGB(light_pos, 14, color); } } const int32_t speed = (item->speed * Math_Cos(item->rot.x)) >> W2V_SHIFT; item->pos.x += (speed * Math_Sin(item->rot.y)) >> W2V_SHIFT; item->pos.y -= (item->speed * Math_Sin(item->rot.x)) >> W2V_SHIFT; item->pos.z += (speed * Math_Cos(item->rot.y)) >> W2V_SHIFT; int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); item->floor = Room_GetHeight(sector, item->pos); Item_UpdateRoom(item_num, room_num); if (g_TRVersion == 3) { const ROOM *const new_room = Room_Get(item->room_num); const bool is_underwater = new_room != nullptr && new_room->flags.underwater; if (is_underwater && !was_underwater) { FX_Water_SetupSplash(&(FX_WATER_SPLASH_SETUP) { .x = item->pos.x, .y = new_room->max_ceiling, .z = item->pos.z, .inner_xz_off = 16, .inner_xz_size = 12, .inner_y_size = -96, .inner_xz_vel = 160, .inner_y_vel = -0x4000, .inner_gravity = 128, .inner_friction = 7, .middle_xz_off = 24, .middle_xz_size = 24, .middle_y_size = -64, .middle_xz_vel = 224, .middle_y_vel = -0x2000, .middle_gravity = 72, .middle_friction = 8, .outer_xz_off = 32, .outer_xz_size = 32, .outer_xz_vel = 272, .outer_friction = 9, }); } } bool explode = false; int32_t radius = 0; if (item->pos.y >= item->floor || item->pos.y <= Room_GetCeiling(sector, item->pos)) { radius = M_BLAST_RADIUS; explode = true; } const GAME_VECTOR new_pos = { .pos = item->pos, .room_num = item->room_num, }; if (Gun_SmashItems(old_pos, new_pos, nullptr, item->object_id) == PROJECTILE_HIT_STOP) { explode = true; } if (g_Config.gameplay.projectile_area_damage == PROJECTILE_AREA_DAMAGE_MULTI_SWEEP) { Room_GetNearbyRooms(item->pos, radius * 4, radius * 4, item->room_num); for (int32_t i = 0; i < Room_DrawGetCount(); i++) { const ROOM *const nearby_room = Room_Get(Room_DrawGetRoom(i)); for (int16_t target_item_num = nearby_room->item_num; target_item_num != NO_ITEM; target_item_num = Item_Get(target_item_num)->next_item) { if (!M_TryExplodeItem(item, old_pos, target_item_num, radius)) { continue; } if (!explode) { explode = true; radius = WALL_L; i = -1; break; } } } } else { for (int16_t target_item_num = room->item_num; target_item_num != NO_ITEM; target_item_num = Item_Get(target_item_num)->next_item) { if (M_TryExplodeItem(item, old_pos, target_item_num, radius)) { explode = true; } } } if (explode) { if (was_underwater) { item->pos = old_pos.pos; Item_UpdateRoom(item_num, old_pos.room_num); } M_Explode(item_num, old_pos.pos); } } static void M_SetupCommon(OBJECT *const obj) { obj->control_func = M_Control; obj->save_position = true; } static void M_Setup(OBJECT *const obj) { M_SetupCommon(obj); } static void M_SetupHeavy(OBJECT *const obj) { const OBJECT *const ref_obj = Object_Get(O_ROCKET); if (!ref_obj->loaded) { return; } M_SetupCommon(obj); obj->frame_base = ref_obj->frame_base; obj->anim_idx = ref_obj->anim_idx; obj->mesh_idx = ref_obj->mesh_idx; obj->mesh_count = ref_obj->mesh_count; obj->loaded = true; } REGISTER_OBJECT(O_ROCKET, M_Setup) REGISTER_OBJECT(O_HEAVY_ROCKET, M_SetupHeavy) ================================================ FILE: src/trx/game/objects/general/save_crystal.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #define M_PC_MESH 0b00000000'00000001 #define M_PS_MESH 0b00000000'00000010 typedef struct { bool initialised; bool counted_for_stats; bool used_for_save; int16_t initial_angle; } M_PRIV; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "counted_for_stats", &p->counted_for_stats)); JSON_SHOULD(JSON_READ(io, "used_for_save", &p->used_for_save)); JSON_SHOULD(JSON_READ(io, "initialised", &p->initialised)); JSON_SHOULD(JSON_READ(io, "initial_angle", &p->initial_angle)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "counted_for_stats", p->counted_for_stats); JSONW_WRITE(io, "used_for_save", p->used_for_save); JSONW_WRITE(io, "initialised", p->initialised); JSONW_WRITE(io, "initial_angle", p->initial_angle); } static void M_CountCrystal(M_PRIV *const p) { if (p->counted_for_stats) { return; } p->counted_for_stats = true; Stats_AddCrystal(); } static const OBJECT_BOUNDS m_SaveCrystal_Bounds = { .shift = { .min = { .x = -STEP_L*3/2, .y = -100, .z = -STEP_L*3/2, }, .max = { .x = +STEP_L*3/2, .y = +WALL_L, .z = +STEP_L*3/2, }, }, .rot = { .min = { .x = -DEG_45, .y = 0, .z = 0, }, .max = { .x = +DEG_45, .y = 0, .z = 0, }, }, }; static const OBJECT_BOUNDS m_UW_Bounds = { .shift = { .min = { .x = -STEP_L*3/2, .y = -WALL_L, .z = -STEP_L*3/2, }, .max = { .x = +STEP_L*3/2, .y = +WALL_L, .z = +STEP_L*3/2, }, }, .rot = { .min = { .x = -DEG_90, .y = 0, .z = 0, }, .max = { .x = +DEG_90, .y = 0, .z = 0, }, }, }; static const LARA_TRX_STATE m_StopStates[] = { // clang-format off LS_STOP, LS_TREAD, LS_SURF_TREAD, LS_TRX_INVALID, // sentinel // clang-format on }; static const OBJECT_BOUNDS *M_Bounds(void) { const LARA_INFO *const lara = Lara_GetLaraInfo(); return lara->water_status == LWS_ABOVE_WATER || lara->water_status == LWS_WADE ? &m_SaveCrystal_Bounds : &m_UW_Bounds; } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; p->initialised = false; p->counted_for_stats = false; p->used_for_save = false; p->initial_angle = 0; if (g_TRVersion != 3) { if (g_Config.gameplay.enable_save_crystals) { Item_AddActive(item_num); } else { Item_Get(item_num)->status = IS_INVISIBLE; } } } static void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage) { switch (stage) { case SAVEGAME_STAGE_AFTER_LOAD: if (item->status == IS_DEACTIVATED) { const int16_t item_num = Item_GetIndex(item); Item_RemoveDrawn(item_num); } break; case SAVEGAME_STAGE_BEFORE_SAVE: M_PRIV *const p = item->priv; if (p->used_for_save) { // need to reset the crystal status item->status = IS_DEACTIVATED; p->used_for_save = false; const int16_t item_num = Item_GetIndex(item); Item_RemoveDrawn(item_num); } default: break; } } static void M_ControlHeal(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (item->status == IS_INVISIBLE || item->clear_body) { return; } M_PRIV *const p = item->priv; if (!p->initialised) { p->initialised = true; p->initial_angle = item->pos.y; } item->rot.y += 1024; const int32_t timer = Output_GetTimeInGame(); const int16_t angle = Math_Cos((timer & 0x3F) << 10); int32_t c = ABS(angle >> 9); CLAMPG(c, 31); c <<= 3; item->pos.y = p->initial_angle - ABS(angle >> 6) - 64; Output_AddDynamicLightRGB(item->pos, 8, (RGB_888) { 0, c, 0 }); ITEM *const lara_item = Lara_GetItem(); const int32_t dx = ABS(item->pos.x - lara_item->pos.x); const int32_t dy = ABS(item->pos.y - lara_item->pos.y); const int32_t dz = ABS(item->pos.z - lara_item->pos.z); if (dx < STEP_L && dy < WALL_L && dz < STEP_L) { M_CountCrystal(p); LARA_INFO *const lara = Lara_GetLaraInfo(); lara->poison_timer = 0; lara_item->hit_points += LARA_MAX_HITPOINTS / 2; CLAMPG(lara_item->hit_points, LARA_MAX_HITPOINTS); // PS1: SFX_SAVE_CRYSTAL, PC: SFX_MENU_MEDI Sound_Effect(SFX_MENU_MEDI, &lara_item->pos, SPM_NORMAL); Item_Kill(item_num); } } static void M_ControlSave(const int16_t item_num) { ITEM *const item = Item_Get(item_num); item->mesh_bits = g_Config.visuals.enable_ps1_crystals && Object_Get(item->object_id)->mesh_count > 1 ? M_PS_MESH : M_PC_MESH; Item_Animate(item); } static void M_CollisionSave( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); const OBJECT *const obj = Object_Get(item->object_id); Object_Collision(item_num, lara_item, coll); const LARA_INFO *const lara = Lara_GetLaraInfo(); if (!g_Input.action || lara->gun_status != LGS_ARMLESS || lara_item->gravity) { return; } if (!Lara_HasState(m_StopStates)) { return; } item->rot.y = lara_item->rot.y; item->rot.z = 0; item->rot.x = 0; if (!Lara_TestPosition(item, obj->bounds_func())) { return; } if (g_Config.flow.load_save_disabled) { Lara_RefuseInteraction(); return; } int16_t room_num = lara_item->room_num; const XYZ_32 pos = lara_item->pos; const SECTOR *const sector = Room_GetSector(pos, &room_num); const int32_t ceiling = Room_GetCeiling(sector, pos); const int32_t floor = Room_GetHeight(sector, pos); if (ceiling >= item->pos.y || floor < item->pos.y) { return; } M_PRIV *const p = item->priv; M_CountCrystal(p); p->used_for_save = true; GF_ShowInventory(INV_SAVE_CRYSTAL_MODE); } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->handle_save_func = M_HandleSave; obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->priv_size = sizeof(M_PRIV); if (g_TRVersion == 3) { obj->control_func = M_ControlHeal; obj->collision_func = nullptr; obj->save_flags = true; } else if (g_Config.gameplay.enable_save_crystals) { obj->control_func = M_ControlSave; obj->collision_func = M_CollisionSave; obj->save_flags = true; Object_SetReflective(O_SAVE_CRYSTAL_ITEM, true); } obj->bounds_func = M_Bounds; } REGISTER_OBJECT(O_SAVE_CRYSTAL_ITEM, M_Setup) ================================================ FILE: src/trx/game/objects/general/scion1.c ================================================ // Tomb of Qualopec and Sanctuary Scion pickup. // Triggers O_LARA_EXTRA pedestal pickup animation. #include #include #include #include #include #include #include #include #include #include static XYZ_32 m_Scion1_Position = { 0, 640, -310 }; static const OBJECT_BOUNDS m_Scion1_Bounds = { .shift = { .min = { .x = -256, .y = +640 - 100, .z = -350, }, .max = { .x = +256, .y = +640 + 100, .z = -200, }, }, .rot = { .min = { .x = -10 * DEG_1, .y = 0, .z = 0, }, .max = { .x = +10 * DEG_1, .y = 0, .z = 0, }, }, }; static const OBJECT_BOUNDS *M_Bounds(void) { return &m_Scion1_Bounds; } static void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage) { if (stage == SAVEGAME_STAGE_AFTER_LOAD) { if (item->status == IS_DEACTIVATED) { const int16_t item_num = Item_GetIndex(item); Item_RemoveDrawn(item_num); } } } static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); LARA_INFO *const lara = Lara_GetLaraInfo(); const OBJECT *const obj = Object_Get(item->object_id); const XYZ_16 old_rot = item->rot; item->rot.y = lara_item->rot.y; item->rot.x = 0; item->rot.z = 0; if (!Lara_TestPosition(item, obj->bounds_func())) { goto cleanup; } if (g_Input.action && lara->gun_status == LGS_ARMLESS && !lara_item->gravity && lara_item->current_anim_state == LS(LS_STOP)) { lara->interact_target.item_num = item_num; Lara_AlignPosition(item, &m_Scion1_Position); Lara_SwitchToExtraState(LS_EXTRA_SCION_PICKUP_1); Camera_InvokeCinematic(lara_item, 0, 0); } cleanup: item->rot = old_rot; } static void M_Setup(OBJECT *const obj) { obj->handle_save_func = M_HandleSave; obj->draw_func = Object_DrawPickupItem; obj->collision_func = M_Collision; obj->save_flags = true; obj->bounds_func = M_Bounds; } REGISTER_OBJECT(O_SCION_ITEM_1, M_Setup) ================================================ FILE: src/trx/game/objects/general/scion3.c ================================================ // The Great Pyramid shootable Scion. #include #include #include #include #include #include #include #include static bool M_ShouldSpawnBlood(const ITEM *const item) { return !g_Config.visuals.fix_texture_issues; } static bool M_CanTakeDamage(const ITEM *const item) { return item->status == IS_ACTIVE; } static void M_Control(const int16_t item_num) { static int32_t counter = 0; ITEM *const item = Item_Get(item_num); if (Item_IsTriggerActive(item)) { if (!LOT_EnableBaddieAI(item_num, true)) { return; } item->status = IS_ACTIVE; } if (item->hit_points > 0) { counter = 0; Item_Animate(item); return; } if (counter == 0) { item->status = IS_INVISIBLE; item->hit_points = 0; Room_TestTriggers(item); Item_RemoveDrawn(item_num); } if (counter % 10 == 0) { int16_t effect_num = Effect_Create(item->room_num); if (effect_num != NO_EFFECT) { EFFECT *effect = Effect_Get(effect_num); effect->pos.x = item->pos.x + (Random_GetControl() - 0x4000) / 32; effect->pos.y = item->pos.y + (Random_GetControl() - 0x4000) / 256 - 500; effect->pos.z = item->pos.z + (Random_GetControl() - 0x4000) / 32; effect->speed = 0; effect->frame_num = 0; effect->object_id = O_EXPLOSION_1; effect->counter = 0; Sound_Effect(SFX_EXPLOSION_1, &effect->pos, SPM_NORMAL); g_Camera.bounce = -200; } } counter++; if (counter >= LOGIC_FPS * 3) { Item_Kill(item_num); } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->can_take_damage_func = M_CanTakeDamage; obj->should_spawn_blood_func = M_ShouldSpawnBlood; obj->hit_points = 5; obj->save_flags = true; obj->save_hitpoints = true; } REGISTER_OBJECT(O_SCION_ITEM_3, M_Setup) ================================================ FILE: src/trx/game/objects/general/scion4.c ================================================ // Atlantis Scion - triggers O_LARA_EXTRA reach anim. #include #include #include #define EXTRA_ANIM_HOLDER_SCION 0 static XYZ_32 m_Scion4_Position = { 0, 280, -512 + 105 }; static const OBJECT_BOUNDS m_Scion4_Bounds = { .shift = { .min = { .x = -256, .y = +256 - 50, .z = -512 - 350, }, .max = { .x = +256, .y = +256 + 50, .z = -200, }, }, .rot = { .min = { .x = -10 * DEG_1, .y = 0, .z = 0, }, .max = { .x = +10 * DEG_1, .y = 0, .z = 0, }, }, }; static const OBJECT_BOUNDS *M_Bounds(void) { return &m_Scion4_Bounds; } static void M_Control(const int16_t item_num) { Item_Animate(Item_Get(item_num)); } static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); LARA_INFO *const lara = Lara_GetLaraInfo(); const OBJECT *const obj = Object_Get(item->object_id); int16_t rotx = item->rot.x; int16_t roty = item->rot.y; int16_t rotz = item->rot.z; item->rot.y = lara_item->rot.y; item->rot.x = 0; item->rot.z = 0; if (!Lara_TestPosition(item, obj->bounds_func())) { goto cleanup; } if (g_Input.action && lara->gun_status == LGS_ARMLESS && !lara_item->gravity && lara_item->current_anim_state == LS(LS_STOP)) { Lara_AlignPosition(item, &m_Scion4_Position); Lara_SwitchToExtraState(LS_EXTRA_SCION_PICKUP_2); Camera_InvokeCinematic(lara_item, 0, -DEG_90); } cleanup: item->rot.x = rotx; item->rot.y = roty; item->rot.z = rotz; } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->collision_func = M_Collision; obj->save_flags = true; obj->bounds_func = M_Bounds; } REGISTER_OBJECT(O_SCION_ITEM_4, M_Setup) ================================================ FILE: src/trx/game/objects/general/scion_holder.c ================================================ #include static void M_Control(const int16_t item_num) { Item_Animate(Item_Get(item_num)); } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->collision_func = Object_Collision; obj->save_anim = true; obj->save_flags = true; } REGISTER_OBJECT(O_SCION_HOLDER, M_Setup) ================================================ FILE: src/trx/game/objects/general/shoal.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_SHOAL_COUNT 8 #define M_FISH_PER_SHOAL 24 #define M_LEVEL_RANGES(id, ...) \ { id, sizeof((XYZ_16[])__VA_ARGS__) / sizeof(XYZ_16), __VA_ARGS__ } typedef struct { int16_t angle; uint8_t speed; bool on; int16_t angle_time; int16_t speed_time; XYZ_16 range; } M_LEADER; typedef struct { XYZ_16 pos; uint16_t angle; // 0..4095 int16_t dest_y; int8_t ang_add; uint8_t speed; uint8_t acc; uint8_t swim; struct { struct { XYZ_16 pos; uint16_t angle; // 0..4095 uint8_t swim; } prev, result; } interp; } M_FISH; typedef struct { int32_t leader_num; M_FISH fish[M_FISH_PER_SHOAL + 1]; M_LEADER leader; int32_t piranha_hit_wait; int16_t carcass_item_num; } M_PRIV; typedef struct { int32_t level_id; int32_t range_count; XYZ_16 ranges[M_SHOAL_COUNT]; } M_FISH_LEVEL_CONFIG; static const M_FISH_LEVEL_CONFIG m_FishLevelConfigs[] = { M_LEVEL_RANGES( 1, { { .x = 8, .z = 20, .y = 3 }, }), M_LEVEL_RANGES( 2, { { .x = 4, .z = 4, .y = 2 }, { .x = 4, .z = 16, .y = 2 }, { .x = 4, .z = 28, .y = 3 }, }), M_LEVEL_RANGES( 3, { { .x = 4, .z = 12, .y = 1 }, { .x = 0, .z = 12, .y = 2 }, { .x = 8, .z = 4, .y = 2 }, { .x = 4, .z = 8, .y = 1 }, { .x = 4, .z = 16, .y = 2 }, { .x = 4, .z = 24, .y = 1 }, { .x = 12, .z = 4, .y = 1 }, { .x = 16, .z = 4, .y = 1 }, }), M_LEVEL_RANGES( 0, { { .x = 4, .z = 4, .y = 1 }, { .x = 16, .z = 8, .y = 2 }, { .x = 24, .z = 8, .y = 2 }, { .x = 8, .z = 16, .y = 2 }, { .x = 8, .z = 12, .y = 1 }, { .x = 20, .z = 8, .y = 2 }, { .x = 16, .z = 8, .y = 1 }, }), M_LEVEL_RANGES( 5, { { .x = 12, .z = 12, .y = 6 }, { .x = 12, .z = 20, .y = 6 }, { .x = 20, .z = 4, .y = 8 }, }), M_LEVEL_RANGES( 6, { { .x = 20, .z = 4, .y = 6 }, }), M_LEVEL_RANGES( 7, { { .x = 16, .z = 16, .y = 8 }, { .x = 4, .z = 8, .y = 5 }, }), }; static bool M_IsValidShoalNum(const int32_t shoal_num) { return shoal_num >= 0 && shoal_num < M_SHOAL_COUNT; } static uint16_t M_GetFishAngle12( const int32_t x1, const int32_t z1, const int32_t x2, const int32_t z2) { const int32_t dx = x2 - x1; const int32_t dz = z2 - z1; const int32_t fish_angle16 = Math_Atan(dx, dz) - DEG_90; return (fish_angle16 >> 4) & 0xFFF; // 0..4095 } static int32_t M_GetAngle12Diff(const int32_t a, const int32_t b) { int32_t diff = a - b; if (diff > 2048) { diff -= 4096; } else if (diff < -2048) { diff += 4096; } return diff; } static bool M_FishNearItem( const XYZ_32 *const pos, const int32_t dist, const ITEM *const item) { const int32_t dx = pos->x - item->pos.x; const int32_t dy = ABS(pos->y - item->pos.y); const int32_t dz = pos->z - item->pos.z; // clang-format off if (dx < -dist || dx > dist || dz < -dist || dz > dist || dy < -3072 || dy > 3072 || SQUARE(dz) + SQUARE(dx) > SQUARE(dist) || dy > dist) { return false; } // clang-format on return true; } static void M_SetupShoal(M_PRIV *const p, const int32_t shoal_num) { if (p == nullptr || !M_IsValidShoalNum(shoal_num)) { return; } M_LEADER *const leader = &p->leader; if (g_TRVersion < 3) { goto fallback; } const int32_t lvl = GF_BadGetLevelNum(); for (size_t i = 0; i < ARRAY_SIZE(m_FishLevelConfigs); i++) { const M_FISH_LEVEL_CONFIG *cfg = &m_FishLevelConfigs[i]; if (cfg->level_id == lvl && shoal_num < cfg->range_count) { const XYZ_16 *const r = &cfg->ranges[shoal_num]; leader->range.x = (r->x + 2) << 8; leader->range.y = r->y << 8; leader->range.z = (r->z + 2) << 8; return; } } fallback: leader->range.x = 256; leader->range.y = 256; leader->range.z = 256; } static void M_SetupFish(M_PRIV *const p, const ITEM *const item) { if (p == nullptr || item == nullptr || !M_IsValidShoalNum(p->leader_num)) { return; } M_LEADER *const leader = &p->leader; M_FISH *fish = &p->fish[0]; const int16_t x = leader->range.x; const int16_t y = leader->range.y; const int16_t z = leader->range.z; fish->pos.x = 0; fish->pos.y = 0; fish->pos.z = 0; fish->angle = 0; fish->speed = ((Random_GetControl() & 0x3F) + 8); fish->swim = (Random_GetControl() & 0x3F); fish->interp.prev.pos = fish->pos; fish->interp.prev.angle = fish->angle; fish->interp.prev.swim = fish->swim; fish->interp.result.pos = fish->pos; fish->interp.result.angle = fish->angle; fish->interp.result.swim = fish->swim; for (int32_t i = 0; i < M_FISH_PER_SHOAL; i++) { fish = &p->fish[i + 1]; fish->pos.x = Random_GetControl() % (x << 1) - x; fish->pos.y = Random_GetControl() % y; fish->pos.z = Random_GetControl() % (z << 1) - z; fish->dest_y = Random_GetControl() % y; fish->angle = Random_GetControl() & 0xFFF; fish->speed = (Random_GetControl() & 0x1F) + 32; fish->swim = Random_GetControl() & 0x3F; fish->interp.prev.pos = fish->pos; fish->interp.prev.angle = fish->angle; fish->interp.prev.swim = fish->swim; fish->interp.result.pos = fish->pos; fish->interp.result.angle = fish->angle; fish->interp.result.swim = fish->swim; } leader->on = true; leader->angle = 0; leader->speed = (Random_GetControl() & 0x7F) + 32; leader->angle_time = 0; leader->speed_time = 0; p->piranha_hit_wait = 0; } static void M_FindCarcass(const ITEM *const shoal_item) { M_PRIV *const p = shoal_item->priv; p->carcass_item_num = Item_FindTypeInRoom(shoal_item->room_num, O_CARCASS); } static bool M_IsTargetable(const ITEM *const item) { return false; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (!Item_IsTriggerActive(item)) { return; } const int32_t leader_num = item->hit_points; if (!M_IsValidShoalNum(leader_num)) { return; } M_PRIV *const p = item->priv; if (p == nullptr) { return; } if (p->leader_num != leader_num) { p->leader_num = leader_num; p->leader.on = false; } M_LEADER *const leader = &p->leader; if (!leader->on) { M_SetupShoal(p, leader_num); M_SetupFish(p, item); } const ITEM *const lara_item = Lara_GetItem(); int32_t piranha_attack = 0; if (item->object_id == O_PIRAHNAS && lara_item != nullptr) { if (p->carcass_item_num == NO_ITEM) { M_FindCarcass(item); } if (p->carcass_item_num != NO_ITEM) { piranha_attack = 2; } else { piranha_attack = lara_item->room_num == item->room_num; } } if (p->piranha_hit_wait != 0) { p->piranha_hit_wait--; } M_FISH *const leader_fish = &p->fish[0]; const ITEM *enemy = lara_item; if (piranha_attack != 0) { if (piranha_attack >= 2) { enemy = Item_Get(p->carcass_item_num); } leader_fish->angle = M_GetFishAngle12( item->pos.x + leader_fish->pos.x, item->pos.z + leader_fish->pos.z, enemy->pos.x, enemy->pos.z); leader->angle = leader_fish->angle; leader->speed = (Random_GetControl() & 0x3F) - 64; } int32_t diff = M_GetAngle12Diff(leader_fish->angle, leader->angle); if (diff > 128) { leader_fish->ang_add -= 4; CLAMPL(leader_fish->ang_add, -120); } else if (diff < -128) { leader_fish->ang_add += 4; CLAMPG(leader_fish->ang_add, 120); } else { leader_fish->ang_add -= leader_fish->ang_add >> 2; if (ABS(leader_fish->ang_add) < 4) { leader_fish->ang_add = 0; } } leader_fish->angle = (leader_fish->angle + leader_fish->ang_add) & 0xFFF; if (diff > 1024) { leader_fish->angle = (leader_fish->angle + (leader_fish->ang_add >> 2)) & 0xFFF; } diff = (int32_t)leader_fish->speed - (int32_t)leader->speed; if (diff < -4) { int32_t new_speed = (int32_t)leader_fish->speed + (Random_GetControl() & 3) + 1; CLAMPL(new_speed, 0); leader_fish->speed = new_speed; } else if (diff > 4) { int32_t new_speed = (int32_t)leader_fish->speed - (Random_GetControl() & 3) - 1; CLAMPG(new_speed, 255); leader_fish->speed = new_speed; } leader_fish->swim = (leader_fish->swim + (leader_fish->speed >> 4)) & 0x3F; const int32_t angle16 = leader_fish->angle << 4; int32_t x = leader_fish->pos.x - (leader_fish->speed * Math_Sin(angle16) >> (W2V_SHIFT + 1)); int32_t z = leader_fish->pos.z + (leader_fish->speed * Math_Cos(angle16) >> (W2V_SHIFT + 1)); if (piranha_attack == 0) { if (z < -leader->range.z) { z = -leader->range.z; if (leader_fish->angle < 2048) { leader->angle = leader_fish->angle - (Random_GetControl() & 0x7F) - 128; } else { leader->angle = leader_fish->angle + (Random_GetControl() & 0x7F) + 128; } leader->angle_time = (Random_GetControl() & 0xF) + 8; leader->speed_time = 0; } else if (z > leader->range.z) { z = leader->range.z; if (leader_fish->angle > 3072) { leader->angle = leader_fish->angle - (Random_GetControl() & 0x7F) - 128; } else { leader->angle = leader_fish->angle + (Random_GetControl() & 0x7F) + 128; } leader->angle_time = (Random_GetControl() & 0xF) + 8; leader->speed_time = 0; } if (x < -leader->range.x) { x = -leader->range.x; if (leader_fish->angle < 1024) { leader->angle = leader_fish->angle - (Random_GetControl() & 0x7F) - 128; } else { leader->angle = leader_fish->angle + (Random_GetControl() & 0x7F) + 128; } leader->angle_time = (Random_GetControl() & 0xF) + 8; leader->speed_time = 0; } else if (x > leader->range.x) { x = leader->range.x; if (leader_fish->angle < 3072) { leader->angle = leader_fish->angle - (Random_GetControl() & 0x7F) - 128; } else { leader->angle = leader_fish->angle + (Random_GetControl() & 0x7F) + 128; } leader->angle_time = (Random_GetControl() & 0xF) + 8; leader->speed_time = 0; } if ((Random_GetControl() & 0xF) == 0) { leader->angle_time = 0; } if (leader->angle_time != 0) { leader->angle_time--; } else { leader->angle_time = (Random_GetControl() & 0xF) + 8; int32_t delta = (Random_GetControl() & 0x3F) - 24; if ((Random_GetControl() & 3) == 0) { delta *= 32; } leader->angle = (leader->angle + delta) & 0xFFF; } if (leader->speed_time != 0) { leader->speed_time--; } else { leader->speed_time = (Random_GetControl() & 0x1F) + 32; if ((Random_GetControl() & 7) == 0) { leader->speed = (Random_GetControl() & 0x7F) + 128; } else if ((Random_GetControl() & 3) == 0) { leader->speed += (Random_GetControl() & 0x7F) + 32; } else if (leader->speed > 140) { leader->speed += 208 - (Random_GetControl() & 0x1F); } else { leader->speed_time = (Random_GetControl() & 3) + 4; leader->speed += (Random_GetControl() & 0x1F) - 15; } } } leader_fish->pos.x = x; leader_fish->pos.z = z; for (int32_t i = 0; i < M_FISH_PER_SHOAL; i++) { M_FISH *const fish = &p->fish[i + 1]; if (item->object_id == O_PIRAHNAS) { const XYZ_32 fish_pos = { .x = item->pos.x + fish->pos.x, .y = item->pos.y + fish->pos.y, .z = item->pos.z + fish->pos.z, }; if (M_FishNearItem(&fish_pos, 256, enemy)) { if (p->piranha_hit_wait == 0) { Spawn_Blood( fish_pos.x, fish_pos.y, fish_pos.z, 0, 0, enemy->room_num); p->piranha_hit_wait = 8; } if (piranha_attack != 2) { Lara_TakeDamage(4, false); } } } const int32_t dx = SQUARE(fish->pos.x - x - 128 * i + 3072); const int32_t dz = SQUARE(fish->pos.z - z + 128 * i - 3072); const uint16_t desired = M_GetFishAngle12(fish->pos.x, fish->pos.z, x, z); diff = M_GetAngle12Diff(fish->angle, desired); if (diff > 128) { fish->ang_add -= 4; CLAMPL(fish->ang_add, -(i >> 1) - 92); } else if (diff < -128) { fish->ang_add += 4; CLAMPG(fish->ang_add, (i >> 1) + 92); } else { fish->ang_add -= fish->ang_add >> 2; if (ABS(fish->ang_add) < 4) { fish->ang_add = 0; } } fish->angle = (fish->angle + fish->ang_add) & 0xFFF; if (diff > 1024) { fish->angle = (fish->angle + (fish->ang_add >> 2)) & 0xFFF; } if (dx + dz < 16384 * SQUARE(i) + SQUARE(WALL_L)) { if (fish->speed > 2 * i + 32) { fish->speed -= fish->speed >> 5; } } else { if (fish->speed < (i >> 1) + 160) { fish->speed += (i >> 1) + (Random_GetControl() & 3) + 1; } if (fish->speed > (i >> 1) - 4 * i + 160) { fish->speed = (i >> 1) - 4 * i - 96; } } if ((Random_GetControl() & 1) != 0) { fish->speed -= Random_GetControl() & 1; } else { fish->speed += Random_GetControl() & 1; } CLAMP(fish->speed, 32, 200); fish->swim = (fish->swim + (fish->speed >> 4) + (fish->speed >> 5)) & 0x3F; const int32_t fish_angle16 = fish->angle << 4; int32_t next_x = fish->pos.x - (fish->speed * Math_Sin(fish_angle16) >> (W2V_SHIFT + 1)); int32_t next_z = fish->pos.z + (fish->speed * Math_Cos(fish_angle16) >> (W2V_SHIFT + 1)); CLAMP(next_x, -32000, 32000); CLAMP(next_z, -32000, 32000); fish->pos.x = next_x; fish->pos.z = next_z; if (piranha_attack == 0) { if (ABS(fish->pos.y - fish->dest_y) < 16) { fish->dest_y = Random_GetControl() % leader->range.y; } } else if (ABS(fish->pos.y - fish->dest_y) < 16 && enemy != nullptr) { fish->dest_y = (enemy->pos.y - item->pos.y + (Random_GetControl() & 0xFF)); } fish->pos.y += (fish->dest_y - fish->pos.y) >> 4; } } static bool M_Draw(const ITEM *const item) { if (!item->active) { return false; } if (item->hit_points == NO_ITEM) { return false; } const int32_t leader_num = item->hit_points; if (!M_IsValidShoalNum(leader_num)) { return false; } M_PRIV *const p = item->priv; if (p == nullptr) { return false; } if (p->leader_num != leader_num) { return false; } if (!p->leader.on) { return false; } const OBJECT *const explosion_obj = Object_Get(O_EXPLOSION_1); if (explosion_obj == nullptr || !explosion_obj->loaded) { return false; } int32_t sprite_idx = explosion_obj->mesh_idx; if (item->object_id == O_PIRAHNAS) { sprite_idx += 10; } else { sprite_idx += 11; } if (sprite_idx < 0 || sprite_idx >= Output_GetSpriteTextureCount()) { return false; } const XYZ_32 base_pos = item->interp.result.pos; const double ratio = Interpolation_GetWorldRate(); const bool do_interp = Interpolation_IsActive() && ratio > 0.0 && ratio < 1.0; M_FISH *fish = &p->fish[1]; for (int32_t i = 0; i < M_FISH_PER_SHOAL; i++, fish++) { if (do_interp) { fish->interp.result.pos.x = (int16_t)LERP( (int32_t)fish->interp.prev.pos.x, (int32_t)fish->pos.x, ratio); fish->interp.result.pos.y = (int16_t)LERP( (int32_t)fish->interp.prev.pos.y, (int32_t)fish->pos.y, ratio); fish->interp.result.pos.z = (int16_t)LERP( (int32_t)fish->interp.prev.pos.z, (int32_t)fish->pos.z, ratio); fish->interp.result.angle = (Math_AngleMean( fish->interp.prev.angle << 4, fish->angle << 4, ratio) >> 4) & 0xFFF; int32_t swim_diff = (int32_t)fish->swim - (int32_t)fish->interp.prev.swim; if (swim_diff > 32) { swim_diff -= 64; } else if (swim_diff < -32) { swim_diff += 64; } int32_t swim_interp = LERP( (int32_t)fish->interp.prev.swim, (int32_t)fish->interp.prev.swim + swim_diff, ratio); swim_interp %= 64; if (swim_interp < 0) { swim_interp += 64; } fish->interp.result.swim = swim_interp; } else { fish->interp.result.pos = fish->pos; fish->interp.result.angle = fish->angle; fish->interp.result.swim = fish->swim; } const int32_t x = base_pos.x + fish->interp.result.pos.x; const int32_t y = base_pos.y + fish->interp.result.pos.y; const int32_t z = base_pos.z + fish->interp.result.pos.z; const int32_t swim_ang16 = fish->interp.result.swim << 10; const int32_t swim_wibble = Math_Sin(swim_ang16) >> 7; const int32_t ang12 = (swim_wibble + fish->interp.result.angle - 2048) & 0xFFF; const int32_t size = ((128 * Math_Sin(i << 10)) >> W2V_SHIFT) + 192; const int32_t ang16 = ang12 << 4; const int32_t back_x = x - ((size * Math_Sin(ang16)) >> W2V_SHIFT); const int32_t back_z = z + ((size * Math_Cos(ang16)) >> W2V_SHIFT); const XYZ_32 tri_world[3] = { { .x = x, .y = y, .z = z }, { .x = back_x, .y = y - size, .z = back_z }, { .x = back_x, .y = y + size, .z = back_z }, }; int32_t shade = ang12; if (shade < 1024) { shade -= 512; } else if (shade < 2048) { shade -= 1536; } else if (shade < 3072) { shade -= 2560; } else { shade -= 3584; } if (shade > 512 || shade < 0) { shade = 0; } else if (shade < 256) { shade >>= 2; } else { shade = (512 - shade) >> 2; } shade += i; if (shade > 128) { shade = 128; } shade += 80; CLAMP(shade, 0, 255); const RGBA_8888 color = { shade, shade, shade, 255 }; const RGBA_8888 tri_color[3] = { color, color, color }; // OG flips the UV mapping depending on the shoal number (tropical // fish) or the fish index (piranhas). bool use_default_uv = false; if (item->object_id == O_PIRAHNAS) { use_default_uv = (i & 1) != 0; } else { use_default_uv = (leader_num & 1) != 0; } if (use_default_uv) { OutputSource_PolyFX_StageSpriteTriWorld( sprite_idx, tri_world, tri_color, DRAW_BLEND); } else { const int32_t sprite_corners[3] = { 2, 3, 1 }; // u2v2, u1v2, u2v1 OUTPUT_UVW uvw[3]; OUTPUT_TEXTURE_SIZE texture_size[3]; for (int32_t j = 0; j < 3; j++) { const int32_t uvw_idx = Output_Textures_GetSpriteUVWIndex( sprite_idx, sprite_corners[j]); uvw[j] = Output_Textures_GetUVW(uvw_idx); texture_size[j] = Output_Textures_GetAtlasSize(uvw_idx / 4); } OutputSource_PolyFX_StageTriExtUV( tri_world, uvw, texture_size, nullptr, tri_color, VERT_NO_LIGHTING | VERT_NO_WIBBLE, DRAW_BLEND); } } if (Interpolation_IsActive() && ratio >= 1.0) { for (int32_t i = 0; i < M_FISH_PER_SHOAL + 1; i++) { p->fish[i].interp.prev.pos = p->fish[i].pos; p->fish[i].interp.prev.angle = p->fish[i].angle; p->fish[i].interp.prev.swim = p->fish[i].swim; } } return true; } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); item->enable_shadow = false; item->collidable = false; if (item->priv == nullptr) { item->priv = GameBuf_Alloc(sizeof(M_PRIV), GBUF_ITEM_DATA); } M_PRIV *const p = item->priv; p->leader.on = false; p->leader_num = NO_ITEM; p->piranha_hit_wait = 0; p->carcass_item_num = NO_ITEM; } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->is_targetable_func = M_IsTargetable; obj->draw_func = M_Draw; obj->hit_points = NO_ITEM; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; } void Shoal_TriggerActivate(ITEM *const item, const int16_t trigger_timer) { // TO_OBJECT handling stores (timer & 7) in hit_points and clears timer so // it does not count down as a trigger delay. const int32_t leader_num = trigger_timer & 7; item->hit_points = leader_num; item->timer = 0; if (M_IsValidShoalNum(leader_num) && item->priv != nullptr) { M_PRIV *const p = item->priv; p->leader_num = leader_num; M_SetupShoal(p, leader_num); } } void Shoal_TriggerDeactivate(const ITEM *const item) { // Anti-trigger turns the leader off to force a re-setup. const int32_t leader_num = item->hit_points; if (M_IsValidShoalNum(leader_num) && item->priv != nullptr) { M_PRIV *const p = item->priv; p->leader.on = false; } } REGISTER_OBJECT(O_TROPICAL_FISH, M_Setup) REGISTER_OBJECT(O_PIRAHNAS, M_Setup) ================================================ FILE: src/trx/game/objects/general/shoal.h ================================================ #pragma once #include void Shoal_TriggerActivate(ITEM *item, int16_t trigger_timer); void Shoal_TriggerDeactivate(const ITEM *item); ================================================ FILE: src/trx/game/objects/general/smashable.c ================================================ #include #include #include #include #include #include #include #include static void M_SetBoxBlocked(const ITEM *const item, const bool blocked) { const ROOM *const room = Room_Get(item->room_num); const SECTOR *const sector = Room_GetWorldSector(room, item->pos.x, item->pos.z); BOX_INFO *const box = Box_GetBox(sector->box); if (blocked && (box->overlap_index & BOX_BLOCKABLE) != 0) { box->overlap_index |= BOX_BLOCKED; } else if (!blocked && (box->overlap_index & BOX_BLOCKED) != 0) { box->overlap_index &= ~BOX_BLOCKED; } } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); item->flags = 0; item->mesh_bits = 1; M_SetBoxBlocked(item, true); } static void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage) { if (stage == SAVEGAME_STAGE_AFTER_LOAD) { if ((item->flags & IF_ONE_SHOT) != 0) { item->mesh_bits = 0x100; M_SetBoxBlocked(item, false); } } } static void M_Control1(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if ((item->flags & IF_ONE_SHOT) != 0) { return; } if (Lara_Vehicle_IsMounted()) { if (Lara_IsNearItem(&item->pos, 512)) { Smashable_Smash(item_num); } } else if (item->touch_bits) { item->touch_bits = 0; const ITEM *const lara_item = Lara_GetItem(); const int32_t speed = ABS((lara_item->speed * Math_Cos(lara_item->rot.y - item->rot.y)) >> W2V_SHIFT); if (speed >= 50) { Smashable_Smash(item_num); } } } static void M_Control2(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if ((item->flags & IF_ONE_SHOT) != 0) { return; } M_SetBoxBlocked(item, false); item->mesh_bits = ~1; item->collidable = false; Item_Explode(item_num, 65278, 0); if (item->object_id == O_SMASH_OBJECT_2) { Sound_Effect(SFX_BRITTLE_GROUND_BREAK, &item->pos, SPM_NORMAL); } else if (item->object_id == O_SMASH_OBJECT_3) { Sound_Effect(SFX_EXPLOSION_1, &item->pos, SPM_NORMAL); Sound_Effect(SFX_EXPLOSION_2, &item->pos, SPM_NORMAL); } item->flags |= IF_ONE_SHOT; item->status = IS_DEACTIVATED; Item_RemoveActive(item_num); } static void M_SetupBase(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->handle_save_func = M_HandleSave; obj->collision_func = Object_Collision; obj->save_flags = true; obj->save_anim = true; } static void M_Setup1(OBJECT *const obj) { M_SetupBase(obj); obj->control_func = M_Control1; } static void M_Setup2(OBJECT *const obj) { M_SetupBase(obj); obj->control_func = M_Control2; } void Smashable_Smash(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_SetBoxBlocked(item, false); item->collidable = false; item->mesh_bits = ~1; Item_Explode(item_num, 0b11111110'11111110, 0); if (item->object_id == O_SMASH_OBJECT_1) { Sound_Effect(SFX_GLASS_BREAK, &item->pos, SPM_NORMAL); } else if (item->object_id == O_SMASH_OBJECT_4) { Sound_Effect(SFX_SHUTTERS_BREAK, &item->pos, SPM_NORMAL); } item->flags |= IF_ONE_SHOT; if (item->status == IS_ACTIVE) { Item_RemoveActive(item_num); } item->status = IS_DEACTIVATED; } REGISTER_OBJECT(O_SMASH_OBJECT_1, M_Setup1) REGISTER_OBJECT(O_SMASH_OBJECT_2, M_Setup2) REGISTER_OBJECT(O_SMASH_OBJECT_3, M_Setup2) REGISTER_OBJECT(O_SMASH_OBJECT_4, M_Setup1) ================================================ FILE: src/trx/game/objects/general/smashable.h ================================================ #pragma once #include void Smashable_Smash(int16_t item_num); ================================================ FILE: src/trx/game/objects/general/smoke_emitter.c ================================================ #include #include #include #include #include #define M_DISTANCE (16 * WALL_L) static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); ITEM *const lara_item = Lara_GetItem(); int32_t time = Output_GetTimeInGame(); if (!Item_IsTriggerActive(item)) { return; } if ((time % 4) || (item->object_id == O_STEAM_EMITTER && (time % 8))) { return; } const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; if (dx < -M_DISTANCE || dx > M_DISTANCE || dz < -M_DISTANCE || dz > M_DISTANCE) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 0; spark->src_color.g = 0; spark->src_color.b = 0; spark->dst_color.r = 32; spark->dst_color.g = 32; spark->dst_color.b = 32; spark->fade_to_black = 64; spark->col_fade_speed = (Random_GetControl() & 7) + 16; spark->life = (Random_GetControl() & 0xF) + 96; spark->s_life = spark->life; if (item->object_id == O_SMOKE_EMITTER_BLACK) { spark->draw_type = DRAW_BLEND_SUB; } else { spark->draw_type = DRAW_BLEND_ADD; } spark->extras = 0; spark->dynamic = -1; spark->pos.x = item->pos.x + (Random_GetControl() & 0xF) - 8; spark->pos.y = item->pos.y + (Random_GetControl() & 0xF) - 8; spark->pos.z = item->pos.z + (Random_GetControl() & 0xF) - 8; spark->vel.x = (Random_GetControl() & 0xFF) - 128; spark->vel.y = -16 - (Random_GetControl() & 0xF); spark->vel.z = (Random_GetControl() & 0xFF) - 128; spark->friction = 4; if (Random_GetControl() & 1) { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_OUTSIDE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = Random_GetControl() & 0xFFF; if (Random_GetControl() & 1) { spark->rot_add = -4 - (Random_GetControl() & 7); } else { spark->rot_add = (Random_GetControl() & 7) + 4; } } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_OUTSIDE | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->scalar = 3; spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->gravity = -8 - (Random_GetControl() & 7); spark->max_y_vel = -4 - (Random_GetControl() & 7); spark->dst_size.width = (Random_GetControl() & 0x1F) + 128; spark->dst_size.height = spark->dst_size.width + (Random_GetControl() & 0x1F) + 32; spark->size.width = spark->dst_size.width >> 2; spark->size.height = spark->dst_size.height >> 2; spark->src_size.width = spark->size.width; spark->src_size.height = spark->size.height >> 2; if (item->object_id == O_STEAM_EMITTER) { spark->gravity >>= 1; spark->vel.y >>= 1; spark->max_y_vel >>= 1; spark->dst_color.r = 24; spark->dst_color.g = 24; spark->dst_color.b = 24; } Sparks_FinishSetup(spark); } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->draw_func = nullptr; obj->save_flags = true; } REGISTER_OBJECT(O_SMOKE_EMITTER_WHITE, M_Setup) REGISTER_OBJECT(O_SMOKE_EMITTER_BLACK, M_Setup) REGISTER_OBJECT(O_STEAM_EMITTER, M_Setup) ================================================ FILE: src/trx/game/objects/general/sphere_of_doom.c ================================================ #include #include #include #include #include #include #define SPHERE_OF_DOOM_RADIUS (STEP_L * 5 / 2) // = 640 static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { if (Room_Get(lara_item->room_num)->flags.underwater) { return; } const ITEM *const item = Item_Get(item_num); const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; const int32_t radius = (SPHERE_OF_DOOM_RADIUS * item->timer) >> 8; if (SQUARE(dx) + SQUARE(dz) >= SQUARE(radius)) { return; } const int16_t angle = Math_Atan(dz, dx); const int16_t diff = lara_item->rot.y - angle; if (ABS(diff) < DEG_90) { lara_item->speed = 150; lara_item->rot.y = angle; } else { lara_item->speed = -150; lara_item->rot.y = angle + DEG_180; } lara_item->gravity = true; lara_item->fall_speed = -50; lara_item->pos.x = item->pos.x + (((radius + 50) * Math_Sin(angle)) >> W2V_SHIFT); lara_item->pos.z = item->pos.z + (((radius + 50) * Math_Cos(angle)) >> W2V_SHIFT); lara_item->rot.x = 0; lara_item->rot.z = 0; Item_SwitchToAnim(lara_item, LA(LA_FALL_START), 0); lara_item->current_anim_state = LS(LS_JUMP_FORWARD); lara_item->goal_anim_state = LS(LS_JUMP_FORWARD); } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); item->timer += 64; item->rot.y += item->object_id == O_SPHERE_OF_DOOM_2 ? DEG_1 * 10 : -DEG_1 * 10; const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = item->pos.x - lara_item->pos.x; const int32_t dy = item->pos.y - lara_item->pos.y; const int32_t dz = item->pos.z - lara_item->pos.z; const int32_t radius = (SPHERE_OF_DOOM_RADIUS * item->timer) >> 8; const int32_t dist = Math_Sqrt(SQUARE(dx) + SQUARE(dy) + SQUARE(dz)); XYZ_32 pos = lara_item->pos; pos.x += ((dist - radius) * dx) / radius; pos.y += ((dist - radius) * dy) / radius; pos.z += ((dist - radius) * dz) / radius; Sound_Effect(SFX_MARCO_BARTOLLI_TRANSFORM, &pos, SPM_NORMAL); if (item->timer > 60 * 64) { Item_Kill(item_num); } } static bool M_Draw(const ITEM *const item) { const int32_t radius = item->timer << 6; Matrix_Push(); Matrix_TranslateAbs32(item->interp.result.pos); Matrix_RotY(item->interp.result.rot.y); MATRIX *mptr = g_WMatrixPtr; mptr->_00 = ((int64_t)mptr->_00 * radius) >> W2V_SHIFT; mptr->_01 = ((int64_t)mptr->_01 * radius) >> W2V_SHIFT; mptr->_02 = ((int64_t)mptr->_02 * radius) >> W2V_SHIFT; mptr->_10 = ((int64_t)mptr->_10 * radius) >> W2V_SHIFT; mptr->_11 = ((int64_t)mptr->_11 * radius) >> W2V_SHIFT; mptr->_12 = ((int64_t)mptr->_12 * radius) >> W2V_SHIFT; mptr->_20 = ((int64_t)mptr->_20 * radius) >> W2V_SHIFT; mptr->_21 = ((int64_t)mptr->_21 * radius) >> W2V_SHIFT; mptr->_22 = ((int64_t)mptr->_22 * radius) >> W2V_SHIFT; mptr = g_MatrixPtr; mptr->_00 = ((int64_t)mptr->_00 * radius) >> W2V_SHIFT; mptr->_01 = ((int64_t)mptr->_01 * radius) >> W2V_SHIFT; mptr->_02 = ((int64_t)mptr->_02 * radius) >> W2V_SHIFT; mptr->_10 = ((int64_t)mptr->_10 * radius) >> W2V_SHIFT; mptr->_11 = ((int64_t)mptr->_11 * radius) >> W2V_SHIFT; mptr->_12 = ((int64_t)mptr->_12 * radius) >> W2V_SHIFT; mptr->_20 = ((int64_t)mptr->_20 * radius) >> W2V_SHIFT; mptr->_21 = ((int64_t)mptr->_21 * radius) >> W2V_SHIFT; mptr->_22 = ((int64_t)mptr->_22 * radius) >> W2V_SHIFT; const ANIM_FRAME *const frame_ptr = Item_GetAnim(item)->frame_ptr; const CLIP clip = Output_CheckBoundsClip(&frame_ptr->bounds); if (clip != CLIP_NOT_VISIBLE) { Output_CalculateObjectLighting(item, &frame_ptr->bounds); Object_DrawMesh(Object_Get(item->object_id)->mesh_idx, clip, false); } Matrix_Pop(); return true; } static void M_SetupBase(OBJECT *const obj, const bool transparent) { obj->collision_func = M_Collision; obj->control_func = M_Control; obj->draw_func = M_Draw; obj->save_position = true; obj->save_flags = true; obj->semi_transparent = transparent; } static void M_SetupTransparent(OBJECT *const obj) { M_SetupBase(obj, true); } static void M_SetupOpaque(OBJECT *const obj) { M_SetupBase(obj, false); } REGISTER_OBJECT(O_SPHERE_OF_DOOM_1, M_SetupTransparent) REGISTER_OBJECT(O_SPHERE_OF_DOOM_2, M_SetupTransparent) REGISTER_OBJECT(O_SPHERE_OF_DOOM_3, M_SetupOpaque) ================================================ FILE: src/trx/game/objects/general/switch.c ================================================ #include #include #include #include #include #include #include #include typedef struct { XYZ_32 normal; XYZ_32 controlled; } M_SWITCH_POS; static const OBJECT_BOUNDS m_SwitchBounds = { .shift = { .min = { .x = -220, .y = +0, .z = +WALL_L / 2 - 220, }, .max = { .x = +220, .y = +0, .z = +WALL_L / 2, }, }, .rot = { .min = { .x = -10 * DEG_1, .y = -30 * DEG_1, .z = -10 * DEG_1, }, .max = { .x = +10 * DEG_1, .y = +30 * DEG_1, .z = +10 * DEG_1, }, }, }; static const OBJECT_BOUNDS m_SwitchBoundsControlled = { .shift = { .min = { .x = -WALL_L / 2, .y = +0, .z = -200, }, .max = { .x = +WALL_L / 2, .y = +0, .z = +200, }, }, .rot = { .min = { .x = -10 * DEG_1, .y = -30 * DEG_1, .z = -10 * DEG_1, }, .max = { .x = +10 * DEG_1, .y = +30 * DEG_1, .z = +10 * DEG_1, }, }, }; static const OBJECT_BOUNDS m_SwitchBoundsUW = { .shift = { .min = { .x = -WALL_L, .y = -WALL_L, .z = -WALL_L, }, .max = { .x = +WALL_L, .y = +WALL_L, .z = +WALL_L / 2, }, }, .rot = { .min = { .x = -80 * DEG_1, .y = -80 * DEG_1, .z = -80 * DEG_1, }, .max = { .x = +80 * DEG_1, .y = +80 * DEG_1, .z = +80 * DEG_1, }, }, }; static const XYZ_32 m_SwitchUWPosition = { .x = 0, .y = 0, .z = 108 }; static const M_SWITCH_POS m_SmallSwitchPosition = { .normal = { .x = 0, .y = 0, .z = 362 }, .controlled = { .x = 0, .y = 0, .z = 80 }, }; static const M_SWITCH_POS m_PushSwitchPosition = { .normal = { .x = 0, .y = 0, .z = 292 }, .controlled = { .x = 0, .y = 0, .z = 146 }, }; static const M_SWITCH_POS m_WallSwitchPosition = { .normal = { .x = 0, .y = 0, .z = 128 }, .controlled = { .x = 0, .y = 0, .z = 64 }, }; static const M_SWITCH_POS m_AirlockPosition = { .normal = { .x = 0, .y = 0, .z = 212 }, .controlled = { .x = 0, .y = 0, .z = 106 }, }; static const OBJECT_BOUNDS *M_Bounds(void) { return g_Config.gameplay.enable_walk_to_items ? &m_SwitchBoundsControlled : &m_SwitchBounds; } static const OBJECT_BOUNDS *M_BoundsUW(void) { return &m_SwitchBoundsUW; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); item->flags |= IF_CODE_BITS; if (!Item_IsTriggerActive(item)) { item->goal_anim_state = SWITCH_STATE_OFF; item->timer = 0; } Item_Animate(item); if (g_TRVersion >= 3 && (item->flags & IF_ONE_SHOT_SWITCH) != 0) { item->flags &= ~IF_ONE_SHOT_SWITCH; item->flags |= IF_ONE_SHOT; } } static void M_AlignLara(ITEM *const lara_item, ITEM *const switch_item) { lara_item->rot.y = switch_item->rot.y; switch (switch_item->object_id) { case O_SWITCH_TYPE_AIRLOCK: case O_SWITCH_TYPE_WHEEL: Lara_AlignPosition(switch_item, &m_AirlockPosition.normal); break; case O_SWITCH_TYPE_SMALL: Lara_AlignPosition(switch_item, &m_SmallSwitchPosition.normal); break; case O_SWITCH_TYPE_BUTTON: Lara_AlignPosition(switch_item, &m_PushSwitchPosition.normal); break; default: break; } } static bool M_MoveLaraControlled( const ITEM *const item, const BOUNDS_16 *const bounds) { XYZ_32 shift; switch (item->object_id) { case O_SWITCH_TYPE_AIRLOCK: case O_SWITCH_TYPE_WHEEL: shift = m_AirlockPosition.controlled; break; case O_SWITCH_TYPE_SMALL: shift = m_SmallSwitchPosition.controlled; break; case O_SWITCH_TYPE_BUTTON: shift = m_PushSwitchPosition.controlled; break; default: shift = m_WallSwitchPosition.controlled; break; } const XYZ_32 move_vector = { .x = 0, .y = 0, .z = bounds->min.z - shift.z, }; return Lara_MovePosition(item, &move_vector); } static void M_TurnSwitchOn(ITEM *const switch_item, ITEM *const lara_item) { switch (switch_item->object_id) { case O_SWITCH_TYPE_WHEEL: Lara_SwitchToExtraState(LS_EXTRA_AIRLOCK); break; case O_SWITCH_TYPE_SMALL: Item_SwitchToAnim(lara_item, LA(LA_SWITCH_SMALL_DOWN), 0); break; case O_SWITCH_TYPE_BUTTON: Item_SwitchToAnim(lara_item, LA(LA_BUTTON_PUSH), 0); break; default: Item_SwitchToAnim(lara_item, LA(LA_WALL_SWITCH_DOWN), 0); break; } if (!Lara_GetLaraInfo()->extra_anim) { lara_item->current_anim_state = LS(LS_SWITCH_ON); } switch_item->goal_anim_state = SWITCH_STATE_ON; } static void M_TurnSwitchOff(ITEM *const switch_item, ITEM *const lara_item) { lara_item->current_anim_state = LS(LS_SWITCH_OFF); LARA_INFO *const lara = Lara_GetLaraInfo(); switch (switch_item->object_id) { case O_SWITCH_TYPE_AIRLOCK: case O_SWITCH_TYPE_WHEEL: Lara_SwitchToExtraState(LS_EXTRA_AIRLOCK); break; case O_SWITCH_TYPE_SMALL: Item_SwitchToAnim(lara_item, LA(LA_SWITCH_SMALL_UP), 0); break; case O_SWITCH_TYPE_BUTTON: Item_SwitchToAnim(lara_item, LA(LA_BUTTON_PUSH), 0); break; default: Item_SwitchToAnim(lara_item, LA(LA_WALL_SWITCH_UP), 0); break; } switch_item->goal_anim_state = SWITCH_STATE_OFF; } static void M_CollisionControlled( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); LARA_INFO *const lara = Lara_GetLaraInfo(); if ((g_Input.action && lara->gun_status == LGS_ARMLESS && !lara_item->gravity && lara_item->current_anim_state == LS(LS_STOP) && item->status == IS_INACTIVE) || (lara->interact_target.is_moving && lara->interact_target.item_num == item_num)) { const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item); OBJECT_BOUNDS col_bounds = *Object_Get(item->object_id)->bounds_func(); col_bounds.shift.min.x += bounds->min.x; col_bounds.shift.max.x += bounds->max.x; col_bounds.shift.min.z += bounds->min.z; col_bounds.shift.max.z += bounds->max.z; if (Lara_TestPosition(item, &col_bounds)) { if (M_MoveLaraControlled(item, bounds)) { if (item->current_anim_state == SWITCH_STATE_OFF) { M_TurnSwitchOn(item, lara_item); } else { M_TurnSwitchOff(item, lara_item); } lara->head_rot.x = 0; lara->head_rot.y = 0; lara->torso_rot.x = 0; lara->torso_rot.y = 0; lara->interact_target.is_moving = false; lara->interact_target.item_num = NO_ITEM; lara->gun_status = LGS_HANDS_BUSY; Item_AddActive(item_num); item->status = IS_ACTIVE; Item_Animate(item); } else { lara->interact_target.item_num = item_num; } } else if ( lara->interact_target.is_moving && lara->interact_target.item_num == item_num) { lara->interact_target.is_moving = false; lara->gun_status = LGS_ARMLESS; } } else if ( lara_item->current_anim_state != LS(LS_SWITCH_ON) && lara_item->current_anim_state != LS(LS_SWITCH_OFF)) { Object_Collision(item_num, lara_item, coll); } } static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); if (g_TRVersion >= 3 && (item->flags & IF_ONE_SHOT) != 0) { return; } if (g_Config.gameplay.enable_walk_to_items) { M_CollisionControlled(item_num, lara_item, coll); return; } LARA_INFO *const lara = Lara_GetLaraInfo(); const OBJECT *const obj = Object_Get(item->object_id); if (!g_Input.action || item->status != IS_INACTIVE || lara->gun_status != LGS_ARMLESS || lara_item->gravity || lara_item->current_anim_state != LS(LS_STOP) || !Lara_TestPosition(item, obj->bounds_func())) { return; } if (item->object_id == O_SWITCH_TYPE_AIRLOCK && item->current_anim_state == SWITCH_STATE_OFF) { return; } M_AlignLara(lara_item, item); if (item->current_anim_state == SWITCH_STATE_OFF) { M_TurnSwitchOn(item, lara_item); } else { M_TurnSwitchOff(item, lara_item); } if (!lara->extra_anim) { lara_item->goal_anim_state = LS(LS_STOP); } lara->gun_status = LGS_HANDS_BUSY; item->status = IS_ACTIVE; Item_AddActive(item_num); Item_Animate(item); } static void M_CollisionUW( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); LARA_INFO *const lara = Lara_GetLaraInfo(); const OBJECT *const obj = Object_Get(item->object_id); if (!g_Input.action || item->status != IS_INACTIVE || (lara->water_status != LWS_UNDERWATER && lara->water_status != LWS_CHEAT) || lara->gun_status != LGS_ARMLESS || lara_item->current_anim_state != LS(LS_TREAD)) { return; } if (!Lara_TestPosition(item, obj->bounds_func())) { return; } if (item->current_anim_state != SWITCH_STATE_ON && item->current_anim_state != SWITCH_STATE_OFF) { return; } if (!Lara_MovePosition(item, &m_SwitchUWPosition)) { return; } lara_item->fall_speed = 0; Lara_AnimateUntil(lara_item, LS(LS_SWITCH_ON)); lara_item->goal_anim_state = LS(LS_TREAD); lara->gun_status = LGS_HANDS_BUSY; if (item->current_anim_state == SWITCH_STATE_OFF) { item->goal_anim_state = SWITCH_STATE_ON; } else { item->goal_anim_state = SWITCH_STATE_OFF; } item->status = IS_ACTIVE; Item_AddActive(item_num); Item_Animate(item); } static void M_SetupBase(OBJECT *const obj) { obj->control_func = M_Control; obj->save_flags = true; obj->save_anim = true; } static void M_SetupCommon(OBJECT *const obj) { M_SetupBase(obj); obj->collision_func = M_Collision; obj->bounds_func = M_Bounds; } static void M_SetupPushButton(OBJECT *const obj) { M_SetupCommon(obj); obj->enable_interpolation = false; obj->bounds_func = M_Bounds; } static void M_SetupUW(OBJECT *const obj) { M_SetupBase(obj); obj->collision_func = M_CollisionUW; obj->bounds_func = M_BoundsUW; } static void M_SetupAirlock(OBJECT *const obj) { M_SetupCommon(obj); obj->draw_func = Object_DrawUnclippedItem; } bool Switch_Trigger(const int16_t item_num, const int16_t timer) { ITEM *const item = Item_Get(item_num); if (item->object_id == O_SWITCH_TYPE_AIRLOCK) { if (item->status == IS_DEACTIVATED) { Item_RemoveActive(item_num); item->status = IS_INACTIVE; return false; } else if ( (item->flags & IF_ONE_SHOT) != 0 || item->current_anim_state == SWITCH_STATE_ON) { return false; } item->flags |= IF_ONE_SHOT; return true; } if (item->status != IS_DEACTIVATED) { return false; } if (item->current_anim_state == SWITCH_STATE_ON && timer > 0) { item->timer = timer; if (timer != 1) { item->timer *= LOGIC_FPS; } item->status = IS_ACTIVE; } else { Item_RemoveActive(item_num); item->status = IS_INACTIVE; } return true; } REGISTER_OBJECT(O_SWITCH_TYPE_AIRLOCK, M_SetupAirlock) REGISTER_OBJECT(O_SWITCH_TYPE_BUTTON, M_SetupPushButton) REGISTER_OBJECT(O_SWITCH_TYPE_NORMAL, M_SetupCommon) REGISTER_OBJECT(O_SWITCH_TYPE_SMALL, M_SetupCommon) REGISTER_OBJECT(O_SWITCH_TYPE_UW, M_SetupUW) REGISTER_OBJECT(O_SWITCH_TYPE_WHEEL, M_SetupCommon) ================================================ FILE: src/trx/game/objects/general/switch.h ================================================ #pragma once #include typedef enum { SWITCH_STATE_ON = 0, SWITCH_STATE_OFF = 1, SWITCH_STATE_LINK = 2, } SWITCH_STATE; bool Switch_Trigger(int16_t item_num, int16_t timer); ================================================ FILE: src/trx/game/objects/general/trapdoor.c ================================================ #include #include #include #include #include #include typedef enum { TRAPDOOR_STATE_CLOSED, TRAPDOOR_STATE_OPEN, } TRAPDOOR_STATE; typedef enum { TRAPDOOR_ANIM_CLOSED = 0, } TRAPDOOR_ANIM; static bool M_IsItemOnTop( const ITEM *const item, const int32_t x, const int32_t z) { const BOUNDS_16 *const orig_bounds = &Item_GetBestFrame(item)->bounds; if (orig_bounds == nullptr) { return false; } BOUNDS_16 fixed_bounds = {}; // Bounds need to change in order to account for 2 sector trapdoors // and the trapdoor angle. if (item->rot.y == 0) { fixed_bounds.min.x = orig_bounds->min.x; fixed_bounds.max.x = orig_bounds->max.x; fixed_bounds.min.z = orig_bounds->min.z; fixed_bounds.max.z = orig_bounds->max.z; } else if (item->rot.y == DEG_90) { fixed_bounds.min.x = orig_bounds->min.z; fixed_bounds.max.x = orig_bounds->max.z; fixed_bounds.min.z = -orig_bounds->max.x; fixed_bounds.max.z = -orig_bounds->min.x; } else if (item->rot.y == -DEG_180) { fixed_bounds.min.x = -orig_bounds->max.x; fixed_bounds.max.x = -orig_bounds->min.x; fixed_bounds.min.z = -orig_bounds->max.z; fixed_bounds.max.z = -orig_bounds->min.z; } else if (item->rot.y == -DEG_90) { fixed_bounds.min.x = -orig_bounds->max.z; fixed_bounds.max.x = -orig_bounds->min.z; fixed_bounds.min.z = orig_bounds->min.x; fixed_bounds.max.z = orig_bounds->max.x; } if (x <= item->pos.x + fixed_bounds.max.x && x >= item->pos.x + fixed_bounds.min.x && z <= item->pos.z + fixed_bounds.max.z && z >= item->pos.z + fixed_bounds.min.z) { return true; } return false; } static int16_t M_GetFloorHeight( const ITEM *const item, const int32_t x, const int32_t y, const int32_t z, const int16_t height) { if (!M_IsItemOnTop(item, x, z)) { return height; } else if (item->current_anim_state != TRAPDOOR_STATE_CLOSED) { return height; } else if (y > item->pos.y || item->pos.y > height) { return height; } else { return item->pos.y; } } static int16_t M_GetCeilingHeight( const ITEM *const item, const int32_t x, const int32_t y, const int32_t z, const int16_t height) { if (!M_IsItemOnTop(item, x, z)) { return height; } else if (item->current_anim_state != TRAPDOOR_STATE_CLOSED) { return height; } else if (y <= item->pos.y || item->pos.y <= height) { return height; } else { return item->pos.y + STEP_L; } } static BOUNDS_16 M_RotateBounds(const BOUNDS_16 bounds, int16_t rot_y) { BOUNDS_16 rot_bounds = {}; switch (rot_y) { case 0: default: rot_bounds = bounds; break; case DEG_90: rot_bounds.min.x = bounds.min.z; rot_bounds.max.x = bounds.max.z; rot_bounds.min.z = -bounds.max.x; rot_bounds.max.z = -bounds.min.x; break; case -DEG_180: rot_bounds.min.x = -bounds.max.x; rot_bounds.max.x = -bounds.min.x; rot_bounds.min.z = -bounds.max.z; rot_bounds.max.z = -bounds.min.z; break; case -DEG_90: rot_bounds.min.x = -bounds.max.z; rot_bounds.max.x = -bounds.min.z; rot_bounds.min.z = bounds.min.x; rot_bounds.max.z = bounds.max.x; break; } return rot_bounds; } static void M_GetSectorPositions(const ITEM *const item, VECTOR *sector_pos) { const OBJECT *const obj = Object_Get(item->object_id); const ANIM_FRAME *const frame = Object_GetAnim(obj, TRAPDOOR_ANIM_CLOSED)->frame_ptr; const BOUNDS_16 rot_bounds = M_RotateBounds(frame->bounds, item->rot.y); const int32_t x0 = item->pos.x + rot_bounds.min.x; const int32_t x1 = item->pos.x + rot_bounds.max.x - 1; const int32_t z0 = item->pos.z + rot_bounds.min.z; const int32_t z1 = item->pos.z + rot_bounds.max.z - 1; const int32_t sx0 = Math_FloorDiv(x0, WALL_L); const int32_t sx1 = Math_FloorDiv(x1, WALL_L); const int32_t sz0 = Math_FloorDiv(z0, WALL_L); const int32_t sz1 = Math_FloorDiv(z1, WALL_L); for (int32_t sx = sx0; sx <= sx1; ++sx) { for (int32_t sz = sz0; sz <= sz1; ++sz) { XYZ_32 pos = { .x = sx * WALL_L + WALL_L / 2, .y = item->pos.y, .z = sz * WALL_L + WALL_L / 2, }; Vector_Add(sector_pos, &pos); } } } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); VECTOR *const positions = Vector_Create(sizeof(XYZ_32)); M_GetSectorPositions(item, positions); Walkable_AllocateNodes(item, positions->count); Vector_Free(positions); } static void M_DropStack(const ITEM *const item) { VECTOR *const positions = Vector_Create(sizeof(XYZ_32)); M_GetSectorPositions(item, positions); for (int32_t i = 0; i < positions->count; i++) { MovableBlock_DropStack( *(const XYZ_32 *)Vector_Get(positions, i), item->room_num); } Vector_Free(positions); } static void M_AddWalkable(const int16_t item_num) { const ITEM *const item = Item_Get(item_num); VECTOR *const positions = Vector_Create(sizeof(XYZ_32)); M_GetSectorPositions(item, positions); for (int32_t i = 0; i < positions->count; i++) { Walkable_Add(item_num, *(const XYZ_32 *)Vector_Get(positions, i)); } Vector_Free(positions); } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (Item_IsTriggerActive(item)) { if (item->current_anim_state == TRAPDOOR_STATE_CLOSED) { item->goal_anim_state = TRAPDOOR_STATE_OPEN; M_DropStack(item); } } else { if (item->current_anim_state == TRAPDOOR_STATE_OPEN) { item->goal_anim_state = TRAPDOOR_STATE_CLOSED; } } Item_Animate(item); Item_UpdateRoom(item_num, item->room_num); } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->floor_height_func = M_GetFloorHeight; obj->ceiling_height_func = M_GetCeilingHeight; obj->save_flags = true; obj->save_anim = true; obj->add_walkable_func = M_AddWalkable; } REGISTER_OBJECT(O_TRAPDOOR_TYPE_1, M_Setup) REGISTER_OBJECT(O_TRAPDOOR_TYPE_2, M_Setup) REGISTER_OBJECT(O_TRAPDOOR_TYPE_3, M_Setup) ================================================ FILE: src/trx/game/objects/general/trigger_gate.c ================================================ #include #include #include #include #include // clang-format off #define M_COLOR_ON ((RGBA_8888) { 0, 255, 0, 255 }) #define M_COLOR_OFF ((RGBA_8888) { 0, 255, 255, 255 }) #define M_RADIUS (STEP_L * 3 / 8) // clang-format on static void M_UpdateTrigger(const ITEM *const item, bool enabled) { int16_t room_num = item->room_num; SECTOR *const sector = Room_GetSector(item->pos, &room_num); if (sector->trigger != nullptr) { sector->trigger->enabled = enabled; } } static void M_Initialise(const int16_t item_num) { const ITEM *const item = Item_Get(item_num); const bool enabled = Item_IsTriggerActiveRO(item); M_UpdateTrigger(item, enabled); } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); const bool enabled = Item_IsTriggerActive(item); M_UpdateTrigger(item, enabled); } static bool M_Draw(const ITEM *const item) { if (!g_Config.debug.enable_debug_triggers) { return false; } const RGBA_8888 color = Item_IsTriggerActiveRO(item) ? M_COLOR_ON : M_COLOR_OFF; Matrix_Push(); Matrix_TranslateAbs32(item->pos); Output_DrawSphereEx((XYZ_16) { 0, -M_RADIUS, 0 }, M_RADIUS, color); Matrix_Pop(); return true; } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->draw_func = M_Draw; obj->save_flags = true; } REGISTER_OBJECT(O_TRIGGER_GATE, M_Setup) ================================================ FILE: src/trx/game/objects/general/waterfall.c ================================================ #include #include #include #include #include #include #include #include #define M_RANGE (WALL_L * 10) // = 10240 static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (!Item_IsTriggerActive(item)) { return; } const ITEM *const lara_item = Lara_GetItem(); if (g_TRVersion == 3) { if (!Item_IsNearby(item, lara_item, M_RANGE)) { return; } if ((int32_t)Output_GetTimeInGame() % 4 == 0) { const XYZ_32 pos = { .x = item->pos.x + ((544 * Math_Sin(item->rot.y)) >> W2V_SHIFT), .y = item->pos.y, .z = item->pos.z + ((544 * Math_Cos(item->rot.y)) >> W2V_SHIFT), }; Sparks_TriggerWaterfallMist(pos.x, pos.y, pos.z, item->rot.y); } Sound_Effect(SFX_WATERFALL_LOOP, &item->pos, SPM_NORMAL); return; } if (g_TRVersion >= 2 && Item_IsNearby(item, lara_item, M_RANGE)) { Sound_Effect(SFX_WATERFALL_LOOP, &item->pos, SPM_NORMAL); } const int16_t effect_num = Effect_Create(item->room_num); if (effect_num != NO_EFFECT) { EFFECT *const effect = Effect_Get(effect_num); effect->object_id = O_SPLASH_1; effect->pos.x = item->pos.x + ((Random_GetDraw() - 0x4000) * WALL_L) / 0x7FFF; effect->pos.y = item->pos.y; effect->pos.z = item->pos.z + ((Random_GetDraw() - 0x4000) * WALL_L) / 0x7FFF; effect->speed = 0; effect->frame_num = 0; effect->shade = -1; } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->draw_func = nullptr; obj->save_flags = true; } REGISTER_OBJECT(O_WATERFALL, M_Setup) ================================================ FILE: src/trx/game/objects/general/zipline.c ================================================ #include #include #include #include #include #define ZIPLINE_MAX_SPEED 100 #define ZIPLINE_ACCELERATION 5 typedef struct { GAME_VECTOR old_pos; } M_PRIV; typedef enum { ZIPLINE_STATE_EMPTY = 0, ZIPLINE_STATE_GRAB = 1, ZIPLINE_STATE_HANG = 2, } ZIPLINE_STATE; static XYZ_32 m_ZiplineHandlePosition = { .x = 0, .y = 0, .z = WALL_L / 2 - 141, }; static const OBJECT_BOUNDS m_ZiplineHandleBounds = { .shift = { .min = { .x = -WALL_L / 4, .y = -100, .z = +WALL_L / 4, }, .max = { .x = +WALL_L / 4, .y = +100, .z = +WALL_L / 2, }, }, .rot = { .min = { .x = +0, .y = -25 * DEG_1, .z = +0, }, .max = { .x = +0, .y = +25 * DEG_1, .z = +0, }, }, }; static const OBJECT_BOUNDS *M_Bounds(void) { return &m_ZiplineHandleBounds; } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; p->old_pos.pos = item->pos; p->old_pos.room_num = item->room_num; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (item->status != IS_ACTIVE) { return; } if (!(item->flags & IF_ONE_SHOT)) { const M_PRIV *const p = item->priv; item->pos = p->old_pos.pos; Item_UpdateRoom(item_num, p->old_pos.room_num); item->status = IS_INACTIVE; item->goal_anim_state = ZIPLINE_STATE_GRAB; item->current_anim_state = ZIPLINE_STATE_GRAB; Item_SwitchToAnim(item, 0, 0); Item_RemoveActive(item_num); return; } if (item->current_anim_state == ZIPLINE_STATE_GRAB) { Item_Animate(item); return; } Item_Animate(item); if (item->fall_speed < ZIPLINE_MAX_SPEED) { item->fall_speed += ZIPLINE_ACCELERATION; } item->pos.y += item->fall_speed >> 2; item->pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, item->fall_speed); int16_t room_num = item->room_num; Room_GetSector(item->pos, &room_num); Item_UpdateRoom(item_num, room_num); ITEM *const lara_item = Lara_GetItem(); const bool lara_on_zipline = lara_item->current_anim_state == LS(LS_ZIPLINE); if (lara_on_zipline) { lara_item->pos = item->pos; } XYZ_32 pos = item->pos; pos.y += STEP_L >> 2; pos = XYZ_32_OffsetYaw(pos, item->rot.y, WALL_L); const SECTOR *const sector = Room_GetSector(pos, &room_num); if (Room_GetHeight(sector, pos) > pos.y + STEP_L && Room_GetCeiling(sector, pos) < pos.y - STEP_L) { Sound_Effect(SFX_ZIPLINE_GO, &item->pos, SPM_ALWAYS); return; } if (lara_on_zipline) { lara_item->goal_anim_state = LS(LS_JUMP_FORWARD); Lara_Animate(lara_item); lara_item->gravity = true; lara_item->speed = item->fall_speed; lara_item->fall_speed = item->fall_speed >> 2; } Sound_Effect(SFX_ZIPLINE_STOP, &item->pos, SPM_ALWAYS); Item_RemoveActive(item_num); item->status = IS_INACTIVE; item->flags &= ~IF_ONE_SHOT; } static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); if (!g_Input.action || lara->gun_status != LGS_ARMLESS || lara_item->gravity || lara_item->current_anim_state != LS(LS_STOP)) { return; } ITEM *const item = Item_Get(item_num); if (item->status != IS_INACTIVE) { return; } const OBJECT *const obj = Object_Get(item->object_id); if (!Lara_TestPosition(item, obj->bounds_func())) { return; } Lara_AlignPosition(item, &m_ZiplineHandlePosition); lara->gun_status = LGS_HANDS_BUSY; lara_item->goal_anim_state = LS(LS_ZIPLINE); do { Item_Animate(lara_item); } while (lara_item->current_anim_state != LS(LS_PULL_UP)); if (!item->active) { Item_AddActive(item_num); } item->status = IS_ACTIVE; item->flags |= IF_ONE_SHOT; } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = M_Collision; obj->bounds_func = M_Bounds; obj->priv_size = sizeof(M_PRIV); obj->save_position = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_ZIPLINE_HANDLE, M_Setup) ================================================ FILE: src/trx/game/objects/ids.h ================================================ #pragma once #define O_FIRST 0 typedef enum { NO_OBJECT = -1, #define X_CATALOG_ID(enum_value) enum_value, #include #undef X_CATALOG_ID // sentinel O_NUMBER_OF, } OBJECT_ID; ================================================ FILE: src/trx/game/objects/names.c ================================================ #include #include #include #include #include #include #include #include #include typedef struct { OBJECT_ID target_object_id; OBJECT_ID source_object_id; } M_ALIAS; typedef struct { OBJECT_ID object_id; const char *key; const char **default_names; } M_DEFAULT; typedef struct { VECTOR *names; char *description; const char *slot; // stable first-name slot for this object } M_NAME_ENTRY; static M_NAME_ENTRY m_NamesTable[O_NUMBER_OF] = {}; static OBJECT_ID m_AliasResolver[O_NUMBER_OF] = {}; // Compile-time default names (ignoring key aliases) static const M_DEFAULT m_Defaults[] = { #define X_OBJ_NAMES(...) ((const char *[]) { __VA_ARGS__, nullptr }) #define X_OBJ_NAME_DEFINE(object_id_, key_name_, names_array_) \ { .object_id = object_id_, \ .key = key_name_, \ .default_names = names_array_ }, #define X_OBJ_ALIAS_DEFINE(target_object_id, source_object_id) #include #undef X_OBJ_ALIAS_DEFINE #undef X_OBJ_NAME_DEFINE #undef X_OBJ_NAMES { .object_id = NO_OBJECT, .key = nullptr, .default_names = nullptr }, }; // Compile-time aliases (ignoring key strings and names) static M_ALIAS m_ObjectAliases[] = { #define X_OBJ_NAMES(...) #define X_OBJ_NAME_DEFINE(object_id_, key_name_, default_name) #define X_OBJ_ALIAS_DEFINE(target_object_id_, source_object_id_) \ { .target_object_id = target_object_id_, \ .source_object_id = source_object_id_ }, #include #undef X_OBJ_ALIAS_DEFINE #undef X_OBJ_NAME_DEFINE #undef X_OBJ_NAMES { .target_object_id = NO_OBJECT }, }; static const M_DEFAULT *M_ResolveDefault(OBJECT_ID obj_id) { obj_id = m_AliasResolver[obj_id]; for (int32_t i = 0; m_Defaults[i].object_id != NO_OBJECT; i++) { if (m_Defaults[i].object_id == obj_id) { return &m_Defaults[i]; } } return nullptr; } static M_NAME_ENTRY *M_ResolveNameEntry(const OBJECT_ID obj_id) { return &m_NamesTable[m_AliasResolver[obj_id]]; } static void M_ClearAllNames(void) { for (OBJECT_ID obj_id = O_FIRST; obj_id < O_NUMBER_OF; obj_id++) { M_NAME_ENTRY *const entry = &m_NamesTable[obj_id]; if (entry->names != nullptr) { for (int32_t i = 0; i < entry->names->count; i++) { char *n = *(char **)Vector_Get(entry->names, i); Memory_FreePointer(&n); } Vector_Free(entry->names); entry->names = nullptr; } Memory_FreePointer(&entry->description); entry->slot = nullptr; } } __attribute__((destructor)) static void M_Shutdown(void) { M_ClearAllNames(); } void Object_ClearNames(const OBJECT_ID obj_id) { ASSERT(obj_id >= O_FIRST && obj_id < O_NUMBER_OF); M_NAME_ENTRY *const entry = M_ResolveNameEntry(obj_id); if (entry->names != nullptr) { for (int32_t i = 0; i < entry->names->count; i++) { char *n = *(char **)Vector_Get(entry->names, i); Memory_FreePointer(&n); } Vector_Clear(entry->names); } entry->slot = nullptr; } void Object_AddName(const OBJECT_ID obj_id, const char *const name) { ASSERT(obj_id >= O_FIRST && obj_id < O_NUMBER_OF); ASSERT(name != nullptr); M_NAME_ENTRY *const entry = M_ResolveNameEntry(obj_id); if (entry->names == nullptr) { entry->names = Vector_Create(sizeof(char *)); } char *const dup = Memory_DupStr(name); Vector_Add(entry->names, &dup); // on first insertion, update stable slot if (entry->names->count == 1) { entry->slot = dup; } } void Object_SetDescription( const OBJECT_ID obj_id, const char *const description) { M_NAME_ENTRY *const entry = M_ResolveNameEntry(obj_id); Memory_FreePointer(&entry->description); if (description != nullptr) { entry->description = Memory_DupStr(description); } } const char *Object_GetName(const OBJECT_ID obj_id) { M_NAME_ENTRY *const entry = M_ResolveNameEntry(obj_id); return entry ? entry->slot : nullptr; } const char *const *Object_GetNamePtr(const OBJECT_ID obj_id) { M_NAME_ENTRY *entry = M_ResolveNameEntry(obj_id); return entry ? &entry->slot : nullptr; } const char *Object_GetDescription(OBJECT_ID obj_id) { M_NAME_ENTRY *const entry = M_ResolveNameEntry(obj_id); return entry != nullptr ? entry->description : nullptr; } void Object_ResetAllNames(void) { M_ClearAllNames(); // Install compile-time aliases for (OBJECT_ID obj_id = O_FIRST; obj_id < O_NUMBER_OF; obj_id++) { m_AliasResolver[obj_id] = obj_id; } for (int32_t i = 0; m_ObjectAliases[i].target_object_id != NO_OBJECT; i++) { const OBJECT_ID target_object_id = m_ObjectAliases[i].target_object_id; const OBJECT_ID source_object_id = m_ObjectAliases[i].source_object_id; m_AliasResolver[target_object_id] = source_object_id; } // Now apply default names for (size_t i = 0; m_Defaults[i].object_id != NO_OBJECT; i++) { for (size_t j = 0; m_Defaults[i].default_names[j] != nullptr; j++) { Object_AddName( m_Defaults[i].object_id, m_Defaults[i].default_names[j]); } } } OBJECT_NAME_MATCH *Object_IdsFromName( const char *user_input, int32_t *out_match_count, bool (*filter)(OBJECT_ID)) { VECTOR *source = Vector_Create(sizeof(STRING_FUZZY_SOURCE)); for (OBJECT_ID obj_id = O_FIRST; obj_id < O_NUMBER_OF; obj_id++) { if (filter != nullptr && !filter(obj_id)) { continue; } const M_NAME_ENTRY *const name_entry = M_ResolveNameEntry(obj_id); if (name_entry->names != nullptr) { for (int32_t i = 0; i < name_entry->names->count; i++) { const char *name = *(char **)Vector_Get(name_entry->names, i); if (name != nullptr) { STRING_FUZZY_SOURCE source_item = { .key = name, .value = (void *)(intptr_t)obj_id, .weight = 2, }; Vector_Add(source, &source_item); } } } if (Object_IsType(obj_id, g_PickupObjects)) { STRING_FUZZY_SOURCE source_item = { .key = "pickup", .value = (void *)(intptr_t)obj_id, .weight = 1, }; Vector_Add(source, &source_item); } } VECTOR *matches = String_FuzzyMatch(user_input, source); // Fallback: if no localized matches, fuzzy-search the compile-time English // defaults. if (matches->count == 0) { Vector_Free(matches); Vector_Clear(source); for (OBJECT_ID obj_id = O_FIRST; obj_id < O_NUMBER_OF; obj_id++) { if (filter != nullptr && !filter(obj_id)) { continue; } const M_DEFAULT *const def = M_ResolveDefault(obj_id); if (def == nullptr) { continue; } for (const char **name = def->default_names; *name != nullptr; name++) { // Add primary compile-time default name if it passes the filter STRING_FUZZY_SOURCE s = { .key = *name, .value = (void *)(intptr_t)obj_id, .weight = 2, }; Vector_Add(source, &s); } } matches = String_FuzzyMatch(user_input, source); } OBJECT_NAME_MATCH *results = Memory_Alloc(sizeof(OBJECT_NAME_MATCH) * (matches->count + 1)); for (int32_t i = 0; i < matches->count; i++) { const STRING_FUZZY_MATCH *const match = Vector_Get(matches, i); results[i].object_id = (OBJECT_ID)(intptr_t)match->value; results[i].matched_name = match->key; } results[matches->count].object_id = NO_OBJECT; results[matches->count].matched_name = nullptr; if (out_match_count != nullptr) { *out_match_count = matches->count; } Vector_Free(matches); Vector_Free(source); return results; } OBJECT_ID Object_IdFromKey(const char *const key) { for (int32_t i = 0; m_Defaults[i].object_id != NO_OBJECT; i++) { if (strcmp(m_Defaults[i].key, key) == 0) { return m_Defaults[i].object_id; } } return NO_OBJECT; } ================================================ FILE: src/trx/game/objects/names.def ================================================ // Consumables X_OBJ_NAME_DEFINE(O_FLARE_ITEM, "flare", X_OBJ_NAMES("Flare")) X_OBJ_NAME_DEFINE(O_FLAREBOX_ITEM, "flares_box", X_OBJ_NAMES("Flares Box")) X_OBJ_NAME_DEFINE(O_EXPLOSIVE_ITEM, "grenade", X_OBJ_NAMES("Grenade")) X_OBJ_NAME_DEFINE(O_SMALL_MEDIPACK_ITEM, "small_medipack", X_OBJ_NAMES("Small Medipack")) X_OBJ_NAME_DEFINE(O_LARGE_MEDIPACK_ITEM, "large_medipack", X_OBJ_NAMES("Large Medipack")) // Guns X_OBJ_NAME_DEFINE(O_PISTOL_ITEM, "pistols", X_OBJ_NAMES("Pistols")) X_OBJ_NAME_DEFINE(O_SHOTGUN_ITEM, "shotgun", X_OBJ_NAMES("Shotgun")) X_OBJ_NAME_DEFINE(O_MAGNUM_ITEM, "magnums", X_OBJ_NAMES("Magnums")) X_OBJ_NAME_DEFINE(O_AUTOS_ITEM, "autos", X_OBJ_NAMES("Automatic Pistols")) X_OBJ_NAME_DEFINE(O_DESERT_EAGLE_ITEM, "desert_eagle", X_OBJ_NAMES("Desert Eagle")) X_OBJ_NAME_DEFINE(O_UZI_ITEM, "uzis", X_OBJ_NAMES("Uzis")) X_OBJ_NAME_DEFINE(O_HARPOON_ITEM, "harpoon_gun", X_OBJ_NAMES("Harpoon Gun")) X_OBJ_NAME_DEFINE(O_M16_ITEM, "m16", X_OBJ_NAMES("M16")) X_OBJ_NAME_DEFINE(O_MP5_ITEM, "mp5", X_OBJ_NAMES("MP5")) X_OBJ_NAME_DEFINE(O_GRENADE_GUN_ITEM, "grenade_launcher", X_OBJ_NAMES("Grenade Launcher")) X_OBJ_NAME_DEFINE(O_ROCKET_GUN_ITEM, "rocket_launcher", X_OBJ_NAMES("Rocket Launcher")) // Ammo X_OBJ_NAME_DEFINE(O_PISTOL_AMMO_ITEM, "pistols_ammo", X_OBJ_NAMES("Pistol Clips")) X_OBJ_NAME_DEFINE(O_SHOTGUN_AMMO_ITEM, "shotgun_ammo", X_OBJ_NAMES("Shotgun Shells")) X_OBJ_NAME_DEFINE(O_MAGNUM_AMMO_ITEM, "magnums_ammo", X_OBJ_NAMES("Magnum Clips")) X_OBJ_NAME_DEFINE(O_AUTOS_AMMO_ITEM, "autos_ammo", X_OBJ_NAMES("Automatic Pistol Clips")) X_OBJ_NAME_DEFINE(O_DESERT_EAGLE_AMMO_ITEM, "desert_eagle_ammo", X_OBJ_NAMES("Desert Eagle Clips")) X_OBJ_NAME_DEFINE(O_UZI_AMMO_ITEM, "uzis_ammo", X_OBJ_NAMES("Uzi Clips")) X_OBJ_NAME_DEFINE(O_HARPOON_AMMO_ITEM, "harpoon_gun_ammo", X_OBJ_NAMES("Harpoons")) X_OBJ_NAME_DEFINE(O_M16_AMMO_ITEM, "m16_ammo", X_OBJ_NAMES("M16 Clips")) X_OBJ_NAME_DEFINE(O_MP5_AMMO_ITEM, "mp5_ammo", X_OBJ_NAMES("MP5 Clips")) X_OBJ_NAME_DEFINE(O_GRENADE_AMMO_ITEM, "grenade_launcher_ammo", X_OBJ_NAMES("Grenades")) X_OBJ_NAME_DEFINE(O_ROCKET_AMMO_ITEM, "rocket_launcher_ammo", X_OBJ_NAMES("Rockets")) // Pickups X_OBJ_NAME_DEFINE(O_PICKUP_ITEM_1, "pickup_1", X_OBJ_NAMES("Pickup Item 1")) X_OBJ_NAME_DEFINE(O_PICKUP_ITEM_2, "pickup_2", X_OBJ_NAMES("Pickup Item 2")) X_OBJ_NAME_DEFINE(O_QUEST_ITEM_1, "quest_1", X_OBJ_NAMES("Quest Item 1")) X_OBJ_NAME_DEFINE(O_QUEST_ITEM_2, "quest_2", X_OBJ_NAMES("Quest Item 2")) X_OBJ_NAME_DEFINE(O_QUEST_ITEM_3, "quest_3", X_OBJ_NAMES("Quest Item 3")) X_OBJ_NAME_DEFINE(O_QUEST_ITEM_4, "quest_4", X_OBJ_NAMES("Quest Item 4")) X_OBJ_NAME_DEFINE(O_KEY_ITEM_1, "key_1", X_OBJ_NAMES("Key 1")) X_OBJ_NAME_DEFINE(O_KEY_ITEM_2, "key_2", X_OBJ_NAMES("Key 2")) X_OBJ_NAME_DEFINE(O_KEY_ITEM_3, "key_3", X_OBJ_NAMES("Key 3")) X_OBJ_NAME_DEFINE(O_KEY_ITEM_4, "key_4", X_OBJ_NAMES("Key 4")) X_OBJ_NAME_DEFINE(O_PUZZLE_ITEM_1, "puzzle_1", X_OBJ_NAMES("Puzzle Item 1")) X_OBJ_NAME_DEFINE(O_PUZZLE_ITEM_2, "puzzle_2", X_OBJ_NAMES("Puzzle Item 2")) X_OBJ_NAME_DEFINE(O_PUZZLE_ITEM_3, "puzzle_3", X_OBJ_NAMES("Puzzle Item 3")) X_OBJ_NAME_DEFINE(O_PUZZLE_ITEM_4, "puzzle_4", X_OBJ_NAMES("Puzzle Item 4")) X_OBJ_NAME_DEFINE(O_LEADBAR_ITEM, "lead_bar", X_OBJ_NAMES("Lead Bar")) X_OBJ_NAME_DEFINE(O_SCION_ITEM_1, "scion", X_OBJ_NAMES("Scion")) X_OBJ_NAME_DEFINE(O_SECRET_1, "secret_1", X_OBJ_NAMES("Secret 1")) X_OBJ_NAME_DEFINE(O_SECRET_2, "secret_2", X_OBJ_NAMES("Secret 2")) X_OBJ_NAME_DEFINE(O_SECRET_3, "secret_3", X_OBJ_NAMES("Secret 3")) X_OBJ_ALIAS_DEFINE(O_SCION_ITEM_2, O_SCION_ITEM_1) X_OBJ_ALIAS_DEFINE(O_SCION_ITEM_3, O_SCION_ITEM_1) X_OBJ_ALIAS_DEFINE(O_SCION_ITEM_4, O_SCION_ITEM_1) // Inventory ring X_OBJ_NAME_DEFINE(O_COMPASS_OPTION, "compass", X_OBJ_NAMES("Compass")) X_OBJ_NAME_DEFINE(O_STOPWATCH_OPTION, "stopwatch", X_OBJ_NAMES("Statistics")) X_OBJ_NAME_DEFINE(O_PASSPORT_OPTION, "passport", X_OBJ_NAMES("Game")) X_OBJ_NAME_DEFINE(O_PHOTO_OPTION, "photo", X_OBJ_NAMES("Lara's Home")) X_OBJ_NAME_DEFINE(O_DETAIL_OPTION, "graphics", X_OBJ_NAMES("Graphics")) X_OBJ_NAME_DEFINE(O_CONTROL_OPTION, "controls", X_OBJ_NAMES("Controls")) X_OBJ_NAME_DEFINE(O_SOUND_OPTION, "sound", X_OBJ_NAMES("Sound")) X_OBJ_NAME_DEFINE(O_GAMMA_OPTION, "gamma", X_OBJ_NAMES("Gamma")) X_OBJ_NAME_DEFINE(O_GLOBE_SELECT_OPTION, "globe", X_OBJ_NAMES("Globe")) X_OBJ_NAME_DEFINE(O_PDA_OPTION, "pda", X_OBJ_NAMES("Gameplay")) X_OBJ_ALIAS_DEFINE(O_PASSPORT_CLOSED, O_PASSPORT_OPTION) // Inventory ring - consumables X_OBJ_ALIAS_DEFINE(O_SMALL_MEDIPACK_OPTION, O_SMALL_MEDIPACK_ITEM) X_OBJ_ALIAS_DEFINE(O_LARGE_MEDIPACK_OPTION, O_LARGE_MEDIPACK_ITEM) X_OBJ_ALIAS_DEFINE(O_FLAREBOX_OPTION, O_FLARE_ITEM) // Inventory ring - guns X_OBJ_ALIAS_DEFINE(O_PISTOL_OPTION, O_PISTOL_ITEM) X_OBJ_ALIAS_DEFINE(O_SHOTGUN_OPTION, O_SHOTGUN_ITEM) X_OBJ_ALIAS_DEFINE(O_MAGNUM_OPTION, O_MAGNUM_ITEM) X_OBJ_ALIAS_DEFINE(O_AUTOS_OPTION, O_AUTOS_ITEM) X_OBJ_ALIAS_DEFINE(O_DESERT_EAGLE_OPTION, O_DESERT_EAGLE_ITEM) X_OBJ_ALIAS_DEFINE(O_UZI_OPTION, O_UZI_ITEM) X_OBJ_ALIAS_DEFINE(O_HARPOON_OPTION, O_HARPOON_ITEM) X_OBJ_ALIAS_DEFINE(O_M16_OPTION, O_M16_ITEM) X_OBJ_ALIAS_DEFINE(O_MP5_OPTION, O_MP5_ITEM) X_OBJ_ALIAS_DEFINE(O_GRENADE_GUN_OPTION, O_GRENADE_GUN_ITEM) X_OBJ_ALIAS_DEFINE(O_ROCKET_GUN_OPTION, O_ROCKET_GUN_ITEM) // Inventory ring - ammo X_OBJ_ALIAS_DEFINE(O_PISTOL_AMMO_OPTION, O_PISTOL_AMMO_ITEM) X_OBJ_ALIAS_DEFINE(O_SHOTGUN_AMMO_OPTION, O_SHOTGUN_AMMO_ITEM) X_OBJ_ALIAS_DEFINE(O_MAGNUM_AMMO_OPTION, O_MAGNUM_AMMO_ITEM) X_OBJ_ALIAS_DEFINE(O_AUTOS_AMMO_OPTION, O_AUTOS_AMMO_ITEM) X_OBJ_ALIAS_DEFINE(O_DESERT_EAGLE_AMMO_OPTION, O_DESERT_EAGLE_AMMO_ITEM) X_OBJ_ALIAS_DEFINE(O_UZI_AMMO_OPTION, O_UZI_AMMO_ITEM) X_OBJ_ALIAS_DEFINE(O_HARPOON_AMMO_OPTION, O_HARPOON_AMMO_ITEM) X_OBJ_ALIAS_DEFINE(O_M16_AMMO_OPTION, O_M16_AMMO_ITEM) X_OBJ_ALIAS_DEFINE(O_MP5_AMMO_OPTION, O_MP5_AMMO_ITEM) X_OBJ_ALIAS_DEFINE(O_GRENADE_AMMO_OPTION, O_GRENADE_AMMO_ITEM) X_OBJ_ALIAS_DEFINE(O_ROCKET_AMMO_OPTION, O_ROCKET_AMMO_ITEM) // Inventory ring - pickups X_OBJ_ALIAS_DEFINE(O_PUZZLE_OPTION_1, O_PUZZLE_ITEM_1) X_OBJ_ALIAS_DEFINE(O_PUZZLE_OPTION_2, O_PUZZLE_ITEM_2) X_OBJ_ALIAS_DEFINE(O_PUZZLE_OPTION_3, O_PUZZLE_ITEM_3) X_OBJ_ALIAS_DEFINE(O_PUZZLE_OPTION_4, O_PUZZLE_ITEM_4) X_OBJ_ALIAS_DEFINE(O_KEY_OPTION_1, O_KEY_ITEM_1) X_OBJ_ALIAS_DEFINE(O_KEY_OPTION_2, O_KEY_ITEM_2) X_OBJ_ALIAS_DEFINE(O_KEY_OPTION_3, O_KEY_ITEM_3) X_OBJ_ALIAS_DEFINE(O_KEY_OPTION_4, O_KEY_ITEM_4) X_OBJ_ALIAS_DEFINE(O_QUEST_OPTION_1, O_QUEST_ITEM_1) X_OBJ_ALIAS_DEFINE(O_QUEST_OPTION_2, O_QUEST_ITEM_2) X_OBJ_ALIAS_DEFINE(O_QUEST_OPTION_3, O_QUEST_ITEM_3) X_OBJ_ALIAS_DEFINE(O_QUEST_OPTION_4, O_QUEST_ITEM_4) X_OBJ_ALIAS_DEFINE(O_PICKUP_OPTION_1, O_PICKUP_ITEM_1) X_OBJ_ALIAS_DEFINE(O_PICKUP_OPTION_2, O_PICKUP_ITEM_2) X_OBJ_ALIAS_DEFINE(O_LEADBAR_OPTION, O_LEADBAR_ITEM) X_OBJ_ALIAS_DEFINE(O_SCION_OPTION, O_SCION_ITEM_1) // Lara animations X_OBJ_NAME_DEFINE(O_LARA, "lara", X_OBJ_NAMES("Lara")) X_OBJ_NAME_DEFINE(O_LARA_HAIR, "lara_hair", X_OBJ_NAMES("Lara's Braid")) X_OBJ_NAME_DEFINE(O_LARA_PISTOLS, "lara_pistols", X_OBJ_NAMES("Pistols Animation")) X_OBJ_NAME_DEFINE(O_LARA_SHOTGUN, "lara_shotgun", X_OBJ_NAMES("Shotgun Animation")) X_OBJ_NAME_DEFINE(O_LARA_MAGNUMS, "lara_magnums", X_OBJ_NAMES("Magnums Animation")) X_OBJ_NAME_DEFINE(O_LARA_AUTOS, "lara_autos", X_OBJ_NAMES("Automatic Pistols Animation")) X_OBJ_NAME_DEFINE(O_LARA_DESERT_EAGLE, "lara_desert_eagle", X_OBJ_NAMES("Desert Eagle Animation")) X_OBJ_NAME_DEFINE(O_LARA_UZIS, "lara_uzis", X_OBJ_NAMES("Uzis Animation")) X_OBJ_NAME_DEFINE(O_LARA_M16, "lara_m16", X_OBJ_NAMES("M16 Animation")) X_OBJ_NAME_DEFINE(O_LARA_MP5, "lara_mp5", X_OBJ_NAMES("MP5 Animation")) X_OBJ_NAME_DEFINE(O_LARA_GRENADE_GUN, "lara_grenade", X_OBJ_NAMES("Grenade Launcher Animation")) X_OBJ_NAME_DEFINE(O_LARA_GRENADE_GUN, "lara_rocket", X_OBJ_NAMES("Rocket Launcher Animation")) X_OBJ_NAME_DEFINE(O_LARA_HARPOON_GUN, "lara_harpoon", X_OBJ_NAMES("Harpoon Animation")) X_OBJ_NAME_DEFINE(O_LARA_FLARE, "lara_flare", X_OBJ_NAMES("Flare Animation")) X_OBJ_NAME_DEFINE(O_LARA_EXTRA, "lara_extra", X_OBJ_NAMES("Lara's Extra Animation")) X_OBJ_NAME_DEFINE(O_LARA_SKIDOO, "lara_skidoo", X_OBJ_NAMES("Snowmobile Animation")) X_OBJ_NAME_DEFINE(O_LARA_BOAT, "lara_boat", X_OBJ_NAMES("Boat Animation")) // Vehicles X_OBJ_NAME_DEFINE(O_SKIDOO_FAST, "skidoo_fast", X_OBJ_NAMES("Red Snowmobile")) X_OBJ_NAME_DEFINE(O_BOAT, "boat", X_OBJ_NAMES("Boat")) X_OBJ_NAME_DEFINE(O_QUAD_BIKE, "quad_bike", X_OBJ_NAMES("Quad Bike")) X_OBJ_NAME_DEFINE(O_KAYAK, "kayak", X_OBJ_NAMES("Kayak")) X_OBJ_NAME_DEFINE(O_UPV, "upv", X_OBJ_NAMES("UPV", "Minisub")) X_OBJ_NAME_DEFINE(O_MOUNTED_GUN, "mounted_gun", X_OBJ_NAMES("Mounted Gun")) X_OBJ_NAME_DEFINE(O_MINE_CART, "mine_cart", X_OBJ_NAMES("Mine Cart")) X_OBJ_NAME_DEFINE(O_RIB, "rib", X_OBJ_NAMES("Rigid Inflatable Boat", "RIB")) // Enemies X_OBJ_NAME_DEFINE(O_BACON_LARA, "bacon_lara", X_OBJ_NAMES("Bacon Lara")) X_OBJ_NAME_DEFINE(O_WOLF, "wolf", X_OBJ_NAMES("Wolf")) X_OBJ_NAME_DEFINE(O_BEAR, "bear", X_OBJ_NAMES("Bear")) X_OBJ_NAME_DEFINE(O_BAT, "bat", X_OBJ_NAMES("Bat")) X_OBJ_NAME_DEFINE(O_LIZARD, "lizard", X_OBJ_NAMES("Lizard")) X_OBJ_NAME_DEFINE(O_CROCODILE, "crocodile", X_OBJ_NAMES("Crocodile")) X_OBJ_NAME_DEFINE(O_ALLIGATOR, "alligator", X_OBJ_NAMES("Alligator")) X_OBJ_NAME_DEFINE(O_LION, "lion", X_OBJ_NAMES("Lion")) X_OBJ_NAME_DEFINE(O_LIONESS, "lioness", X_OBJ_NAMES("Lioness", "Lion")) X_OBJ_NAME_DEFINE(O_PUMA, "puma", X_OBJ_NAMES("Puma")) X_OBJ_NAME_DEFINE(O_APE, "ape", X_OBJ_NAMES("Ape")) X_OBJ_NAME_DEFINE(O_RAT, "rat", X_OBJ_NAMES("Rat", "Land Rat")) X_OBJ_NAME_DEFINE(O_VOLE, "vole", X_OBJ_NAMES("Vole", "Water Rat")) X_OBJ_NAME_DEFINE(O_ORCA, "orca", X_OBJ_NAMES("Orca")) X_OBJ_NAME_DEFINE(O_TREX, "trex", X_OBJ_NAMES("T-Rex")) X_OBJ_NAME_DEFINE(O_TREX_ALPHA, "trex_alpha", X_OBJ_NAMES("T-Rex Alpha")) X_OBJ_NAME_DEFINE(O_COMPY, "compy", X_OBJ_NAMES("Compsognathus")) X_OBJ_NAME_DEFINE(O_CARCASS, "carcass", X_OBJ_NAMES("Carcass")) X_OBJ_NAME_DEFINE(O_RAPTOR, "raptor", X_OBJ_NAMES("Raptor")) X_OBJ_NAME_DEFINE(O_ATLANTEAN_WINGED, "atlantean_winged", X_OBJ_NAMES("Winged Atlantean")) X_OBJ_NAME_DEFINE(O_ATLANTEAN_SHOOTER, "atlantean_shooter", X_OBJ_NAMES("Shooting Atlantean")) X_OBJ_NAME_DEFINE(O_ATLANTEAN_GROUND, "atlantean_ground", X_OBJ_NAMES("Ground Atlantean")) X_OBJ_NAME_DEFINE(O_CENTAUR, "centaur", X_OBJ_NAMES("Centaur")) X_OBJ_NAME_DEFINE(O_MUMMY, "mummy", X_OBJ_NAMES("Mummy")) X_OBJ_NAME_DEFINE(O_DINO_WARRIOR, "dino_mutant", X_OBJ_NAMES("Dino Mutant")) X_OBJ_NAME_DEFINE(O_FISH, "fish_mutant", X_OBJ_NAMES("Mutant Fish")) X_OBJ_NAME_DEFINE(O_LARSON, "larson", X_OBJ_NAMES("Larson")) X_OBJ_NAME_DEFINE(O_PIERRE, "pierre", X_OBJ_NAMES("Pierre")) X_OBJ_NAME_DEFINE(O_SKATEBOARD, "skateboard", X_OBJ_NAMES("Skateboard")) X_OBJ_NAME_DEFINE(O_SKATEKID, "skate_kid", X_OBJ_NAMES("Skate Kid")) X_OBJ_NAME_DEFINE(O_COWBOY, "cowboy", X_OBJ_NAMES("Cowboy")) X_OBJ_NAME_DEFINE(O_BALDY, "baldy", X_OBJ_NAMES("Baldy")) X_OBJ_NAME_DEFINE(O_NATLA, "natla", X_OBJ_NAMES("Natla")) X_OBJ_NAME_DEFINE(O_TORSO, "torso", X_OBJ_NAMES("Torso", "Adam", "Giant Mutant")) X_OBJ_NAME_DEFINE(O_DOG, "dog", X_OBJ_NAMES("Dog", "Doberman")) X_OBJ_NAME_DEFINE(O_PATROL_DOG, "patrol_dog", X_OBJ_NAMES("Dog", "Patrol Dog")) X_OBJ_NAME_DEFINE(O_HUSKIE, "huskie", X_OBJ_NAMES("Dog", "Patrol Dog", "Huskie")) X_OBJ_NAME_DEFINE(O_CULT_1, "cult_1", X_OBJ_NAMES("Masked Goon 1")) X_OBJ_NAME_DEFINE(O_CULT_1A, "cult_1a", X_OBJ_NAMES("Masked Goon 2")) X_OBJ_NAME_DEFINE(O_CULT_1B, "cult_1b", X_OBJ_NAMES("Masked Goon 3")) X_OBJ_NAME_DEFINE(O_CULT_2, "cult_2", X_OBJ_NAMES("Knife Thrower")) X_OBJ_NAME_DEFINE(O_CULT_3, "cult_3", X_OBJ_NAMES("Shotgun Goon")) X_OBJ_NAME_DEFINE(O_MOUSE, "mouse", X_OBJ_NAMES("Rat")) X_OBJ_NAME_DEFINE(O_MONKEY, "monkey", X_OBJ_NAMES("Monkey")) X_OBJ_NAME_DEFINE(O_DRAGON_FRONT, "dragon_front", X_OBJ_NAMES("Dragon Front")) X_OBJ_NAME_DEFINE(O_DRAGON_BACK, "dragon_back", X_OBJ_NAMES("Dragon Back")) X_OBJ_NAME_DEFINE(O_SHARK, "shark", X_OBJ_NAMES("Shark")) X_OBJ_NAME_DEFINE(O_EEL, "eel", X_OBJ_NAMES("Eel")) X_OBJ_NAME_DEFINE(O_BIG_EEL, "big_eel", X_OBJ_NAMES("Big Eel")) X_OBJ_NAME_DEFINE(O_BARRACUDA, "barracuda", X_OBJ_NAMES("Barracuda")) X_OBJ_NAME_DEFINE(O_DIVER, "diver", X_OBJ_NAMES("Scuba Diver")) X_OBJ_NAME_DEFINE(O_WORKER_1, "worker_1", X_OBJ_NAMES("Gunman Goon 1")) X_OBJ_NAME_DEFINE(O_WORKER_2, "worker_2", X_OBJ_NAMES("Gunman Goon 2")) X_OBJ_NAME_DEFINE(O_WORKER_3, "worker_3", X_OBJ_NAMES("Stick Wielding Goon 1")) X_OBJ_NAME_DEFINE(O_WORKER_4, "worker_4", X_OBJ_NAMES("Stick Wielding Goon 2")) X_OBJ_NAME_DEFINE(O_WORKER_5, "worker_5", X_OBJ_NAMES("Flamethrower Goon")) X_OBJ_NAME_DEFINE(O_JELLY, "jelly", X_OBJ_NAMES("Jellyfish")) X_OBJ_NAME_DEFINE(O_SPIDER, "spider", X_OBJ_NAMES("Spider")) X_OBJ_NAME_DEFINE(O_BIG_SPIDER, "big_spider", X_OBJ_NAMES("Giant Spider")) X_OBJ_NAME_DEFINE(O_CROW, "crow", X_OBJ_NAMES("Crow")) X_OBJ_NAME_DEFINE(O_TIGER, "tiger", X_OBJ_NAMES("Tiger")) X_OBJ_NAME_DEFINE(O_BARTOLI, "bartoli", X_OBJ_NAMES("Marco Bartoli")) X_OBJ_NAME_DEFINE(O_XIAN_SPEARMAN, "xian_spearman", X_OBJ_NAMES("Xian Spearman")) X_OBJ_NAME_DEFINE(O_XIAN_SPEARMAN_STATUE, "xian_spearman_statue", X_OBJ_NAMES("Xian Spearman Statue")) X_OBJ_NAME_DEFINE(O_XIAN_KNIGHT, "xian_knight", X_OBJ_NAMES("Xian Knight")) X_OBJ_NAME_DEFINE(O_XIAN_KNIGHT_STATUE, "xian_knight_statue", X_OBJ_NAMES("Xian Knight Statue")) X_OBJ_NAME_DEFINE(O_YETI, "yeti", X_OBJ_NAMES("Yeti")) X_OBJ_NAME_DEFINE(O_BIRD_GUARDIAN, "bird_guardian", X_OBJ_NAMES("Bird Monster")) X_OBJ_NAME_DEFINE(O_EAGLE, "eagle", X_OBJ_NAMES("Eagle")) X_OBJ_NAME_DEFINE(O_BANDIT_1, "bandit_1", X_OBJ_NAMES("Mercenary 1", "Masked Goon 1")) X_OBJ_NAME_DEFINE(O_BANDIT_2, "bandit_2", X_OBJ_NAMES("Mercenary 2", "Masked Goon 2")) X_OBJ_NAME_DEFINE(O_BANDIT_2B, "bandit_2b", X_OBJ_NAMES("Mercenary 3", "Masked Goon 3")) X_OBJ_NAME_DEFINE(O_SKIDOO_ARMED, "skidoo_armed", X_OBJ_NAMES("Black Snowmobile")) X_OBJ_NAME_DEFINE(O_SKIDOO_DRIVER, "skidoo_driver", X_OBJ_NAMES("Black Snowmobile Driver")) X_OBJ_NAME_DEFINE(O_MONK_1, "monk_1", X_OBJ_NAMES("Monk 1")) X_OBJ_NAME_DEFINE(O_MONK_2, "monk_2", X_OBJ_NAMES("Monk 2")) X_OBJ_NAME_DEFINE(O_CENTAUR_STATUE, "centaur_statue", X_OBJ_NAMES("Centaur Statue")) X_OBJ_NAME_DEFINE(O_PODS, "pods", X_OBJ_NAMES("Pod")) X_OBJ_NAME_DEFINE(O_BIG_POD, "big_pod", X_OBJ_NAMES("Big Pod")) X_OBJ_NAME_DEFINE(O_TONY, "tony", X_OBJ_NAMES("Tony")) X_OBJ_NAME_DEFINE(O_WILLARD, "willard", X_OBJ_NAMES("Willard")) X_OBJ_NAME_DEFINE(O_VULTURE, "vulture", X_OBJ_NAMES("Vulture")) X_OBJ_NAME_DEFINE(O_COBRA, "snake", X_OBJ_NAMES("Snake", "Cobra")) X_OBJ_NAME_DEFINE(O_SHIVA, "shiva", X_OBJ_NAMES("Shiva")) X_OBJ_NAME_DEFINE(O_STHPAC_MERCENARY, "sthpac_mercenary", X_OBJ_NAMES("South Pacific Mercenary")) X_OBJ_NAME_DEFINE(O_TRIBE_AXEMAN, "tribe_axeman", X_OBJ_NAMES("Tribe Axeman")) X_OBJ_NAME_DEFINE(O_TRIBE_PIPEMAN, "tribe_pipeman", X_OBJ_NAMES("Tribe Blowpipe User")) X_OBJ_NAME_DEFINE(O_TRIBE_BOSS, "tribe_boss", X_OBJ_NAMES("Tribe Boss")) X_OBJ_NAME_DEFINE(O_PUNK_1, "punk_1", X_OBJ_NAMES("Punk 1")) X_OBJ_NAME_DEFINE(O_PUNK_2, "punk_2", X_OBJ_NAMES("Punk 2")) X_OBJ_NAME_DEFINE(O_SECURITY_GUARD, "security_guard", X_OBJ_NAMES("Security Guard")) X_OBJ_NAME_DEFINE(O_SWAT_1, "swat_1", X_OBJ_NAMES("SWAT 1")) X_OBJ_NAME_DEFINE(O_SWAT_1, "swat_2", X_OBJ_NAMES("SWAT 2")) X_OBJ_NAME_DEFINE(O_SWAT_1, "swat_3", X_OBJ_NAMES("SWAT 3")) X_OBJ_NAME_DEFINE(O_SOPHIA, "sophia", X_OBJ_NAMES("Sophia")) X_OBJ_NAME_DEFINE(O_CIVILIAN, "civilian", X_OBJ_NAMES("Civilian")) X_OBJ_NAME_DEFINE(O_PRISONER, "prisoner", X_OBJ_NAMES("Prisoner")) X_OBJ_NAME_DEFINE(O_MP_1, "mp_1", X_OBJ_NAMES("MP 1")) X_OBJ_NAME_DEFINE(O_MP_2, "mp_2", X_OBJ_NAMES("MP 2")) X_OBJ_NAME_DEFINE(O_RX_WORKER_1, "rx_worker_1", X_OBJ_NAMES("RX Worker 1")) X_OBJ_NAME_DEFINE(O_RX_WORKER_2, "rx_worker_2", X_OBJ_NAMES("RX Worker 2")) X_OBJ_NAME_DEFINE(O_RX_WORKER_3, "rx_worker_3", X_OBJ_NAMES("RX Worker 3", "Flamethrower")) X_OBJ_NAME_DEFINE(O_CRAWLER_MUTANT, "crawler_mutant", X_OBJ_NAMES("Crawler Mutant")) X_OBJ_NAME_DEFINE(O_DYING_MUTANT, "dying_mutant", X_OBJ_NAMES("Dying Mutant")) X_OBJ_NAME_DEFINE(O_HYBRID_MUTANT, "hybrid_mutant", X_OBJ_NAMES("Hybrid Mutant")) X_OBJ_NAME_DEFINE(O_WASP_MUTANT, "wasp_mutant", X_OBJ_NAMES("Wasp Mutant")) X_OBJ_NAME_DEFINE(O_CLAW_MUTANT, "claw_mutant", X_OBJ_NAMES("Claw Mutant")) // Traps X_OBJ_NAME_DEFINE(O_BLADE, "blade", X_OBJ_NAMES("Wall-mounted Blade")) X_OBJ_NAME_DEFINE(O_CEILING_SPIKES, "ceiling_spikes", X_OBJ_NAMES("Spiky Ceiling")) X_OBJ_NAME_DEFINE(O_DAMOCLES_SWORD, "damocles_sword", X_OBJ_NAMES("Damocles Sword")) X_OBJ_NAME_DEFINE(O_DART, "dart", X_OBJ_NAMES("Dart")) X_OBJ_NAME_DEFINE(O_DART_EMITTER, "dart_emitter", X_OBJ_NAMES("Dart Emitter")) X_OBJ_NAME_DEFINE(O_DISC, "disc", X_OBJ_NAMES("Disc")) X_OBJ_NAME_DEFINE(O_DISC_EMITTER, "disc_emitter", X_OBJ_NAMES("Disc Emitter")) X_OBJ_NAME_DEFINE(O_ELECTRIC_CLEANER, "electric_cleaner", X_OBJ_NAMES("Electric Cleaner")) X_OBJ_NAME_DEFINE(O_EMBER, "ember", X_OBJ_NAMES("Ember")) X_OBJ_NAME_DEFINE(O_EMBER_EMITTER, "ember_emitter", X_OBJ_NAMES("Ember Emitter")) X_OBJ_NAME_DEFINE(O_FALLING_BLOCK_1, "falling_block_1", X_OBJ_NAMES("Falling Block 1", "Collapsible Floor 1", "Collapsible Tiles 1")) X_OBJ_NAME_DEFINE(O_FALLING_BLOCK_2, "falling_block_2", X_OBJ_NAMES("Falling Block 2", "Collapsible Floor 2", "Collapsible Tiles 2")) X_OBJ_NAME_DEFINE(O_FALLING_BLOCK_3, "falling_block_3", X_OBJ_NAMES("Falling Block 3", "Collapsible Floor 3", "Collapsible Tiles 3")) X_OBJ_NAME_DEFINE(O_FALLING_CEILING_1, "falling_ceiling_1", X_OBJ_NAMES("Falling Ceiling 1")) X_OBJ_NAME_DEFINE(O_FALLING_CEILING_2, "falling_ceiling_2", X_OBJ_NAMES("Falling Ceiling 2")) X_OBJ_NAME_DEFINE(O_FIRE_HEAD, "fire_head", X_OBJ_NAMES("Fire Head")) X_OBJ_NAME_DEFINE(O_FLAME, "flame", X_OBJ_NAMES("Flame", "Fire")) X_OBJ_NAME_DEFINE(O_FLAME_EMITTER, "flame_emitter", X_OBJ_NAMES("Flame Emitter", "Fire Emitter")) X_OBJ_NAME_DEFINE(O_FLAME_EMITTER_BIG, "flame_emitter_big", X_OBJ_NAMES("Flame Emitter (Big)", "Fire Emitter (Big)")) X_OBJ_NAME_DEFINE(O_FLAME_EMITTER_SMALL, "flame_emitter_small", X_OBJ_NAMES("Flame Emitter (Small)", "Fire Emitter (Small)")) X_OBJ_NAME_DEFINE(O_FLAME_EMITTER_JET, "flame_emitter_jet", X_OBJ_NAMES("Flame Emitter (Jet)", "Fire Emitter (Jet)")) X_OBJ_NAME_DEFINE(O_FLAME_EMITTER_SIDE, "flame_emitter_side", X_OBJ_NAMES("Flame Emitter (Side)", "Fire Emitter (Side)")) X_OBJ_NAME_DEFINE(O_GONDOLA, "gondola", X_OBJ_NAMES("Gondola")) X_OBJ_NAME_DEFINE(O_HOOK, "hook", X_OBJ_NAMES("Hook")) X_OBJ_NAME_DEFINE(O_ICICLE, "icicle", X_OBJ_NAMES("Icicles")) X_OBJ_NAME_DEFINE(O_KILLER_STATUE, "killer_statue", X_OBJ_NAMES("Statue with Sword")) X_OBJ_NAME_DEFINE(O_LAVA_WEDGE, "lava_wedge", X_OBJ_NAMES("Lava Wedge")) X_OBJ_NAME_DEFINE(O_LIGHTNING_EMITTER, "lightning_emitter", X_OBJ_NAMES("Lightning Emitter")) X_OBJ_NAME_DEFINE(O_MIDAS_TOUCH, "midas_touch", X_OBJ_NAMES("Midas Hand")) X_OBJ_NAME_DEFINE(O_MINE, "mine", X_OBJ_NAMES("Aquatic Mine")) X_OBJ_NAME_DEFINE(O_PENDULUM_1, "pendulum_1", X_OBJ_NAMES("Pendulum", "Sandbag", "Swinging box")) X_OBJ_NAME_DEFINE(O_PENDULUM_2, "pendulum_2", X_OBJ_NAMES("Pendulum", "Sandbag", "Swinging box")) X_OBJ_NAME_DEFINE(O_POISON_DART, "poison_dart", X_OBJ_NAMES("Poison Dart")) X_OBJ_NAME_DEFINE(O_POISON_DART_EMITTER, "poison_dart_emitter", X_OBJ_NAMES("Poison Dart Emitter")) X_OBJ_NAME_DEFINE(O_POWER_SAW, "power_saw", X_OBJ_NAMES("Power Saw")) X_OBJ_NAME_DEFINE(O_PROPELLER_1, "propeller_1", X_OBJ_NAMES("Airplane Propeller")) X_OBJ_NAME_DEFINE(O_PROPELLER_2, "propeller_2", X_OBJ_NAMES("Underwater Propeller")) X_OBJ_NAME_DEFINE(O_PROPELLER_3, "propeller_3", X_OBJ_NAMES("Air Fan")) X_OBJ_NAME_DEFINE(O_ROTATING_LASER, "rotating_laser", X_OBJ_NAMES("Rotating Laser")) X_OBJ_NAME_DEFINE(O_SECURITY_LASER_ALARM, "security_laser_alarm", X_OBJ_NAMES("Security Laser (Alarm)")) X_OBJ_NAME_DEFINE(O_SECURITY_LASER_DEADLY, "security_laser_deadly", X_OBJ_NAMES("Security Laser (Deadly)")) X_OBJ_NAME_DEFINE(O_SECURITY_LASER_KILLER, "security_laser_killer", X_OBJ_NAMES("Security Laser (Killer)")) X_OBJ_NAME_DEFINE(O_ROLLING_BALL_1, "rolling_ball_1", X_OBJ_NAMES("Boulder 1", "Rolling Ball 1")) X_OBJ_NAME_DEFINE(O_ROLLING_BALL_2, "rolling_ball_2", X_OBJ_NAMES("Boulder 2", "Rolling Ball 2")) X_OBJ_NAME_DEFINE(O_ROLLING_BALL_3, "rolling_ball_3", X_OBJ_NAMES("Boulder 3", "Rolling Ball 3")) X_OBJ_NAME_DEFINE(O_ROLLING_BALL_4, "rolling_ball_4", X_OBJ_NAMES("Boulder 4", "Rolling Ball 4")) X_OBJ_NAME_DEFINE(O_SPIKES, "spikes", X_OBJ_NAMES("Spikes")) X_OBJ_NAME_DEFINE(O_SPIKE_WALL, "spike_wall", X_OBJ_NAMES("Spike Wall")) X_OBJ_NAME_DEFINE(O_SPINNING_BLADE, "spinning_blade", X_OBJ_NAMES("Spinning Blade")) X_OBJ_NAME_DEFINE(O_SWINGING_AXE, "swinging_axe", X_OBJ_NAMES("Swinging Axe")) X_OBJ_NAME_DEFINE(O_TEETH_TRAP, "teeth_trap", X_OBJ_NAMES("Teeth Trap", "Clang-clang Door")) X_OBJ_NAME_DEFINE(O_THORS_HANDLE, "thors_handle", X_OBJ_NAMES("Thor's Hammer Handle")) X_OBJ_NAME_DEFINE(O_THORS_HEAD, "thors_head", X_OBJ_NAMES("Thor's Hammer")) X_OBJ_NAME_DEFINE(O_ELECTRIC_FENCE, "electric_fence", X_OBJ_NAMES("Electric Fence")) X_OBJ_NAME_DEFINE(O_RAPTOR_EMITTER, "raptor_emitter", X_OBJ_NAMES("Raptor Emitter")) X_OBJ_NAME_DEFINE(O_WASP_MUTANT_EMITTER, "wasp_mutant_emitter", X_OBJ_NAMES("Wasp Mutant Emitter")) X_OBJ_NAME_DEFINE(O_TRAIN, "train", X_OBJ_NAMES("Train")) // General objects X_OBJ_NAME_DEFINE(O_BAT_EMITTER, "bat_emitter", X_OBJ_NAMES("Bat Emitter")) X_OBJ_NAME_DEFINE(O_BELL, "bell", X_OBJ_NAMES("Bell")) X_OBJ_NAME_DEFINE(O_BIG_BOWL, "big_bowl", X_OBJ_NAMES("Lava Bowl")) X_OBJ_NAME_DEFINE(O_BRIDGE_FLAT, "bridge_flat", X_OBJ_NAMES("Bridge Flat")) X_OBJ_NAME_DEFINE(O_BRIDGE_TILT_1, "bridge_tilt_1", X_OBJ_NAMES("Bridge Tilt 1")) X_OBJ_NAME_DEFINE(O_BRIDGE_TILT_2, "bridge_tilt_2", X_OBJ_NAMES("Bridge Tilt 2")) X_OBJ_NAME_DEFINE(O_COG_1, "cog_1", X_OBJ_NAMES("Cog 1")) X_OBJ_NAME_DEFINE(O_COG_2, "cog_2", X_OBJ_NAMES("Cog 2")) X_OBJ_NAME_DEFINE(O_COG_3, "cog_3", X_OBJ_NAMES("Cog 3")) X_OBJ_NAME_DEFINE(O_DOOR_TYPE_1, "door_1", X_OBJ_NAMES("Door 1")) X_OBJ_NAME_DEFINE(O_DOOR_TYPE_2, "door_2", X_OBJ_NAMES("Door 2")) X_OBJ_NAME_DEFINE(O_DOOR_TYPE_3, "door_3", X_OBJ_NAMES("Door 3")) X_OBJ_NAME_DEFINE(O_DOOR_TYPE_4, "door_4", X_OBJ_NAMES("Door 4")) X_OBJ_NAME_DEFINE(O_DOOR_TYPE_5, "door_5", X_OBJ_NAMES("Door 5")) X_OBJ_NAME_DEFINE(O_DOOR_TYPE_6, "door_6", X_OBJ_NAMES("Door 6")) X_OBJ_NAME_DEFINE(O_DOOR_TYPE_7, "door_7", X_OBJ_NAMES("Door 7")) X_OBJ_NAME_DEFINE(O_DOOR_TYPE_8, "door_8", X_OBJ_NAMES("Door 8")) X_OBJ_NAME_DEFINE(O_DRAWBRIDGE, "drawbridge", X_OBJ_NAMES("Drawbridge")) X_OBJ_NAME_DEFINE(O_GENERAL, "general", X_OBJ_NAMES("Minisub")) X_OBJ_NAME_DEFINE(O_KEY_HOLE_1, "key_hole_1", X_OBJ_NAMES("Keyhole 1")) X_OBJ_NAME_DEFINE(O_KEY_HOLE_2, "key_hole_2", X_OBJ_NAMES("Keyhole 2")) X_OBJ_NAME_DEFINE(O_KEY_HOLE_3, "key_hole_3", X_OBJ_NAMES("Keyhole 3")) X_OBJ_NAME_DEFINE(O_KEY_HOLE_4, "key_hole_4", X_OBJ_NAMES("Keyhole 4")) X_OBJ_NAME_DEFINE(O_KILL_ALL_TRIGGERED, "kill_all_triggered", X_OBJ_NAMES("Kill All Triggered")) X_OBJ_NAME_DEFINE(O_LIFT, "lift", X_OBJ_NAMES("Lift")) X_OBJ_NAME_DEFINE(O_MOVABLE_BLOCK_1, "movable_block_1", X_OBJ_NAMES("Push Block 1", "Movable Block 1")) X_OBJ_NAME_DEFINE(O_MOVABLE_BLOCK_2, "movable_block_2", X_OBJ_NAMES("Push Block 2", "Movable Block 2")) X_OBJ_NAME_DEFINE(O_MOVABLE_BLOCK_3, "movable_block_3", X_OBJ_NAMES("Push Block 3", "Movable Block 3")) X_OBJ_NAME_DEFINE(O_MOVABLE_BLOCK_4, "movable_block_4", X_OBJ_NAMES("Push Block 4", "Movable Block 4")) X_OBJ_NAME_DEFINE(O_MOVING_BAR, "moving_bar", X_OBJ_NAMES("Moving Bar")) X_OBJ_NAME_DEFINE(O_PORTACABIN, "portacabin", X_OBJ_NAMES("Portable Cabin")) X_OBJ_NAME_DEFINE(O_PUZZLE_DONE_1, "puzzle_done_1", X_OBJ_NAMES("Puzzle Hole 1 (Done)")) X_OBJ_NAME_DEFINE(O_PUZZLE_DONE_2, "puzzle_done_2", X_OBJ_NAMES("Puzzle Hole 2 (Done)")) X_OBJ_NAME_DEFINE(O_PUZZLE_DONE_3, "puzzle_done_3", X_OBJ_NAMES("Puzzle Hole 3 (Done)")) X_OBJ_NAME_DEFINE(O_PUZZLE_DONE_4, "puzzle_done_4", X_OBJ_NAMES("Puzzle Hole 4 (Done)")) X_OBJ_NAME_DEFINE(O_PUZZLE_HOLE_1, "puzzle_hole_1", X_OBJ_NAMES("Puzzle Hole 1 (Empty)")) X_OBJ_NAME_DEFINE(O_PUZZLE_HOLE_2, "puzzle_hole_2", X_OBJ_NAMES("Puzzle Hole 2 (Empty)")) X_OBJ_NAME_DEFINE(O_PUZZLE_HOLE_3, "puzzle_hole_3", X_OBJ_NAMES("Puzzle Hole 3 (Empty)")) X_OBJ_NAME_DEFINE(O_PUZZLE_HOLE_4, "puzzle_hole_4", X_OBJ_NAMES("Puzzle Hole 4 (Empty)")) X_OBJ_NAME_DEFINE(O_SAVE_CRYSTAL_ITEM, "save_crystal", X_OBJ_NAMES("Savegame Crystal")) X_OBJ_NAME_DEFINE(O_SCION_HOLDER, "scion_holder", X_OBJ_NAMES("Scion Holder")) X_OBJ_NAME_DEFINE(O_SLIDING_PILLAR, "sliding_pillar", X_OBJ_NAMES("Sliding Pillar")) X_OBJ_NAME_DEFINE(O_SMASH_OBJECT_1, "smashable_1", X_OBJ_NAMES("Smashable 1", "Breakable Window 1")) X_OBJ_NAME_DEFINE(O_SMASH_OBJECT_2, "smashable_2", X_OBJ_NAMES("Smashable 2", "Breakable Window 2")) X_OBJ_NAME_DEFINE(O_SMASH_OBJECT_3, "smashable_3", X_OBJ_NAMES("Smashable 3", "Breakable Window 3")) X_OBJ_NAME_DEFINE(O_SMASH_OBJECT_4, "smashable_4", X_OBJ_NAMES("Smashable 4", "Breakable Window 4")) X_OBJ_NAME_DEFINE(O_SPRINGBOARD, "springboard", X_OBJ_NAMES("Springboard")) X_OBJ_NAME_DEFINE(O_SWITCH_TYPE_AIRLOCK, "switch_type_airlock", X_OBJ_NAMES("Airlock Switch")) X_OBJ_NAME_DEFINE(O_SWITCH_TYPE_BUTTON, "switch_type_button", X_OBJ_NAMES("Button", "Push Button", "Switch")) X_OBJ_NAME_DEFINE(O_SWITCH_TYPE_NORMAL, "switch_type_normal", X_OBJ_NAMES("Lever", "Switch")) X_OBJ_NAME_DEFINE(O_SWITCH_TYPE_SMALL, "switch_type_small", X_OBJ_NAMES("Small Switch")) X_OBJ_NAME_DEFINE(O_SWITCH_TYPE_UW, "switch_type_uw", X_OBJ_NAMES("Underwater Lever", "Underwater Switch")) X_OBJ_NAME_DEFINE(O_SWITCH_TYPE_WHEEL, "switch_type_wheel", X_OBJ_NAMES("Wheel Switch", "Pulley Switch", "Valve Switch")) X_OBJ_NAME_DEFINE(O_TRAPDOOR_TYPE_1, "trapdoor_1", X_OBJ_NAMES("Trapdoor 1")) X_OBJ_NAME_DEFINE(O_TRAPDOOR_TYPE_2, "trapdoor_2", X_OBJ_NAMES("Trapdoor 2")) X_OBJ_NAME_DEFINE(O_TRAPDOOR_TYPE_3, "trapdoor_3", X_OBJ_NAMES("Trapdoor 3")) X_OBJ_NAME_DEFINE(O_ZIPLINE_HANDLE, "zipline_handle", X_OBJ_NAMES("Zipline Handle")) X_OBJ_NAME_DEFINE(O_ELECTRICAL_LIGHT, "electrical_light", X_OBJ_NAMES("Electrical Light")) X_OBJ_NAME_DEFINE(O_RED_LIGHT, "red_light", X_OBJ_NAMES("Red Light")) X_OBJ_NAME_DEFINE(O_GREEN_LIGHT, "green_light", X_OBJ_NAMES("Green Light")) X_OBJ_NAME_DEFINE(O_BLUE_LIGHT, "blue_light", X_OBJ_NAMES("Blue Light")) X_OBJ_NAME_DEFINE(O_AMBER_LIGHT, "amber_light", X_OBJ_NAMES("Amber Light")) X_OBJ_NAME_DEFINE(O_WHITE_LIGHT, "white_light", X_OBJ_NAMES("White Light")) X_OBJ_NAME_DEFINE(O_ON_OFF_LIGHT, "on_off_light", X_OBJ_NAMES("On/Off Light")) X_OBJ_NAME_DEFINE(O_PULSE_LIGHT, "pulse_light", X_OBJ_NAMES("Pulse Light")) X_OBJ_NAME_DEFINE(O_STROBE_LIGHT, "strobe_light", X_OBJ_NAMES("Strobe Light")) X_OBJ_NAME_DEFINE(O_BEACON_LIGHT, "beacon_light", X_OBJ_NAMES("Beacon Light")) X_OBJ_NAME_DEFINE(O_SMOKE_EMITTER_WHITE, "smoke_emitter_white", X_OBJ_NAMES("Smoke Emitter (White)")) X_OBJ_NAME_DEFINE(O_SMOKE_EMITTER_BLACK, "smoke_emitter_black", X_OBJ_NAMES("Smoke Emitter (Black)")) X_OBJ_NAME_DEFINE(O_STEAM_EMITTER, "steam_emitter", X_OBJ_NAMES("Steam Emitter")) X_OBJ_NAME_DEFINE(O_GAS_EMITTER_GREEN, "gas_emitter_green", X_OBJ_NAMES("Gas Emitter (Green)")) X_OBJ_NAME_DEFINE(O_FUSE_BOX, "fuse_box", X_OBJ_NAMES("Fuse Box")) X_OBJ_NAME_DEFINE(O_SENTRY_GUN, "sentry_gun", X_OBJ_NAMES("Sentry Gun")) X_OBJ_NAME_DEFINE(O_AREA_51_ROCKET, "area_51_rocket", X_OBJ_NAMES("Area 51 Rocket")) X_OBJ_NAME_DEFINE(O_AREA_51_ROCKET_BLAST, "area_51_rocket_blast", X_OBJ_NAMES("Area 51 Rocket Blast")) X_OBJ_NAME_DEFINE(O_AREA_51_ROCKET_SUPPORT,"area_51_rocket_support",X_OBJ_NAMES("Area 51 Rocket Support")) // Misc X_OBJ_NAME_DEFINE(O_ALARM_SOUND, "alarm_sound", X_OBJ_NAMES("Alarm")) X_OBJ_NAME_DEFINE(O_ALPHABET, "alphabet", X_OBJ_NAMES("Default font")) X_OBJ_NAME_DEFINE(O_ALPHABET_SMALL, "alphabet_small", X_OBJ_NAMES("Small font")) X_OBJ_NAME_DEFINE(O_ANIMATING_1, "animating_1", X_OBJ_NAMES("Animating Object 1")) X_OBJ_NAME_DEFINE(O_ANIMATING_2, "animating_2", X_OBJ_NAMES("Animating Object 2")) X_OBJ_NAME_DEFINE(O_ANIMATING_3, "animating_3", X_OBJ_NAMES("Animating Object 3")) X_OBJ_NAME_DEFINE(O_ANIMATING_4, "animating_4", X_OBJ_NAMES("Animating Object 4")) X_OBJ_NAME_DEFINE(O_ANIMATING_5, "animating_5", X_OBJ_NAMES("Animating Object 5")) X_OBJ_NAME_DEFINE(O_ANIMATING_6, "animating_6", X_OBJ_NAMES("Animating Object 6")) X_OBJ_NAME_DEFINE(O_ANIMATING_7, "animating_7", X_OBJ_NAMES("Animating Object 7")) X_OBJ_NAME_DEFINE(O_ANIMATING_8, "animating_8", X_OBJ_NAMES("Animating Object 8")) X_OBJ_NAME_DEFINE(O_ANIMATING_9, "animating_9", X_OBJ_NAMES("Animating Object 9")) X_OBJ_NAME_DEFINE(O_ANIMATING_10, "animating_10", X_OBJ_NAMES("Animating Object 10")) X_OBJ_NAME_DEFINE(O_ASSAULT_DIGITS, "assault_digits", X_OBJ_NAMES("Assault Digits")) X_OBJ_NAME_DEFINE(O_ASSAULT_TARGET, "assault_target", X_OBJ_NAMES("Assault Target")) X_OBJ_NAME_DEFINE(O_BIRD_TWEETER_1, "bird_tweeter_1", X_OBJ_NAMES("Dripping Water")) X_OBJ_NAME_DEFINE(O_BIRD_TWEETER_2, "bird_tweeter_2", X_OBJ_NAMES("Singing Birds")) X_OBJ_NAME_DEFINE(O_BLOOD, "blood", X_OBJ_NAMES("Blood")) X_OBJ_NAME_DEFINE(O_BLOOD_PINK, "blood_pink", X_OBJ_NAMES("Blood (censored)")) X_OBJ_NAME_DEFINE(O_BOAT_BITS, "boat_bits", X_OBJ_NAMES("Boat Bits")) X_OBJ_NAME_DEFINE(O_BODY_PART, "body_part", X_OBJ_NAMES("Body Part")) X_OBJ_NAME_DEFINE(O_BUBBLE_1, "bubble_1", X_OBJ_NAMES("Bubble 1")) X_OBJ_NAME_DEFINE(O_BUBBLE_2, "bubble_2", X_OBJ_NAMES("Bubble 2")) X_OBJ_NAME_DEFINE(O_BUBBLE_EMITTER, "bubble_emitter", X_OBJ_NAMES("Bubble Emitter")) X_OBJ_NAME_DEFINE(O_CAMERA_TARGET, "camera_target", X_OBJ_NAMES("Camera Target")) X_OBJ_NAME_DEFINE(O_CLOCK_CHIMES, "clock_chimes", X_OBJ_NAMES("Bartoli Hideout clock")) X_OBJ_NAME_DEFINE(O_COMBAT_END, "combat_end", X_OBJ_NAMES("Combat End")) X_OBJ_NAME_DEFINE(O_COPTER, "copter", X_OBJ_NAMES("Helicopter")) X_OBJ_NAME_DEFINE(O_CUT_SHOTGUN, "cut_shotgun", X_OBJ_NAMES("Shotgun Shower Animation")) X_OBJ_NAME_DEFINE(O_DART_EFFECT, "dart_effect", X_OBJ_NAMES("Dart Effect")) X_OBJ_NAME_DEFINE(O_DETONATOR_BOX, "detonator_box", X_OBJ_NAMES("Detonator Box")) X_OBJ_NAME_DEFINE(O_DING_DONG, "ding_dong", X_OBJ_NAMES("Doorbell")) X_OBJ_NAME_DEFINE(O_DISPOSABLE_ANIMATING_1, "disposable_animating_1", X_OBJ_NAMES("Disposable Animating Object 1")) X_OBJ_NAME_DEFINE(O_DISPOSABLE_ANIMATING_2, "disposable_animating_2", X_OBJ_NAMES("Disposable Animating Object 2")) X_OBJ_NAME_DEFINE(O_DISPOSABLE_ANIMATING_3, "disposable_animating_3", X_OBJ_NAMES("Disposable Animating Object 3")) X_OBJ_NAME_DEFINE(O_DISPOSABLE_ANIMATING_4, "disposable_animating_4", X_OBJ_NAMES("Disposable Animating Object 4")) X_OBJ_NAME_DEFINE(O_DISPOSABLE_ANIMATING_5, "disposable_animating_5", X_OBJ_NAMES("Disposable Animating Object 5")) X_OBJ_NAME_DEFINE(O_DISPOSABLE_ANIMATING_6, "disposable_animating_6", X_OBJ_NAMES("Disposable Animating Object 6")) X_OBJ_NAME_DEFINE(O_DISPOSABLE_ANIMATING_7, "disposable_animating_7", X_OBJ_NAMES("Disposable Animating Object 7")) X_OBJ_NAME_DEFINE(O_DISPOSABLE_ANIMATING_8, "disposable_animating_8", X_OBJ_NAMES("Disposable Animating Object 8")) X_OBJ_NAME_DEFINE(O_DISPOSABLE_ANIMATING_9, "disposable_animating_9", X_OBJ_NAMES("Disposable Animating Object 9")) X_OBJ_NAME_DEFINE(O_DISPOSABLE_ANIMATING_10, "disposable_animating_10", X_OBJ_NAMES("Disposable Animating Object 10")) X_OBJ_NAME_DEFINE(O_DRAGON_BONES_1, "dragon_bones_1", X_OBJ_NAMES("Placeholder")) X_OBJ_NAME_DEFINE(O_DRAGON_BONES_2, "dragon_bones_2", X_OBJ_NAMES("Dragon Bones Front")) X_OBJ_NAME_DEFINE(O_DRAGON_BONES_3, "dragon_bones_3", X_OBJ_NAMES("Dragon Bones Back")) X_OBJ_NAME_DEFINE(O_DUST, "dust", X_OBJ_NAMES("Dust")) X_OBJ_NAME_DEFINE(O_DYING_MONK, "dying_monk", X_OBJ_NAMES("Dying monk")) X_OBJ_NAME_DEFINE(O_EARTHQUAKE, "earthquake", X_OBJ_NAMES("Earthquake")) X_OBJ_NAME_DEFINE(O_EXPLOSION_1, "explosion_1", X_OBJ_NAMES("Explosion 1")) X_OBJ_NAME_DEFINE(O_EXPLOSION_2, "explosion_2", X_OBJ_NAMES("Explosion 2")) X_OBJ_NAME_DEFINE(O_FLARE_FIRE, "flare_fire", X_OBJ_NAMES("Flare sparks")) X_OBJ_NAME_DEFINE(O_FLICKERING_LIGHT, "flickering_light", X_OBJ_NAMES("Flickering Light")) X_OBJ_NAME_DEFINE(O_FX_RESERVED, "fx_reserved", X_OBJ_NAMES("Gray disk")) X_OBJ_NAME_DEFINE(O_GLOW, "glow", X_OBJ_NAMES("Glow")) X_OBJ_NAME_DEFINE(O_GLOW_RESERVED, "glow_reserved", X_OBJ_NAMES("Map Glow")) X_OBJ_NAME_DEFINE(O_GONG, "gong", X_OBJ_NAMES("Gong")) X_OBJ_NAME_DEFINE(O_GONG_BONGER, "gong_bonger", X_OBJ_NAMES("Gong Stick")) X_OBJ_NAME_DEFINE(O_GRENADE, "grenade", X_OBJ_NAMES("Grenade")) X_OBJ_NAME_DEFINE(O_GUN_FLASH, "gun_flash", X_OBJ_NAMES("Gun Flash")) X_OBJ_NAME_DEFINE(O_GUN_SHELL, "gun_shell", X_OBJ_NAMES("Gun Shell")) X_OBJ_NAME_DEFINE(O_HARPOON_BOLT, "harpoon_bolt", X_OBJ_NAMES("Harpoon Bolt")) X_OBJ_NAME_DEFINE(O_HOT_LIQUID, "hot_liquid", X_OBJ_NAMES("Extra Fire")) X_OBJ_NAME_DEFINE(O_INV_BACKGROUND, "inv_background", X_OBJ_NAMES("Menu Background")) X_OBJ_NAME_DEFINE(O_LARA_ALARM, "lara_alarm", X_OBJ_NAMES("Alarm Bell")) X_OBJ_NAME_DEFINE(O_M16_FLASH, "m16_flash", X_OBJ_NAMES("M16 Flash")) X_OBJ_NAME_DEFINE(O_MESH_SWAP_1, "mesh_swap_1", X_OBJ_NAMES("Mesh Swap 1")) X_OBJ_NAME_DEFINE(O_MESH_SWAP_2, "mesh_swap_2", X_OBJ_NAMES("Mesh Swap 2")) X_OBJ_NAME_DEFINE(O_MESH_SWAP_3, "mesh_swap_3", X_OBJ_NAMES("Mesh Swap 3")) X_OBJ_NAME_DEFINE(O_MINI_COPTER, "mini_copter", X_OBJ_NAMES("Helicopter 2")) X_OBJ_NAME_DEFINE(O_MISSILE_ATLANTEAN_BOMB, "missile_atlantean_bomb", X_OBJ_NAMES("Missile (Atlantean Bomb)")) X_OBJ_NAME_DEFINE(O_MISSILE_ATLANTEAN_SHARD, "missile_atlantean_shard", X_OBJ_NAMES("Missile (Atlantean Shard)")) X_OBJ_NAME_DEFINE(O_MISSILE_FLAME, "missile_flame", X_OBJ_NAMES("Missile (Flame)")) X_OBJ_NAME_DEFINE(O_MISSILE_HARPOON, "missile_harpoon", X_OBJ_NAMES("Missile (Harpoon)")) X_OBJ_NAME_DEFINE(O_MISSILE_KNIFE, "missile_knife", X_OBJ_NAMES("Missile (Knife)")) X_OBJ_NAME_DEFINE(O_MISSILE_POISON, "missile_poison", X_OBJ_NAMES("Missile (Poison)")) X_OBJ_NAME_DEFINE(O_MOTOR_BOAT, "boat", X_OBJ_NAMES("Boat")) X_OBJ_NAME_DEFINE(O_NATLA_GUN, "natla_gun", X_OBJ_NAMES("Natla's Gun")) X_OBJ_NAME_DEFINE(O_PICKUP_AID, "pickup_aid", X_OBJ_NAMES("Pickup Aid")) X_OBJ_NAME_DEFINE(O_PIRAHNAS, "pirahnas", X_OBJ_NAMES("Pirahnas")) X_OBJ_NAME_DEFINE(O_PLAYER_1, "player_1", X_OBJ_NAMES("Cutscene Actor 1")) X_OBJ_NAME_DEFINE(O_PLAYER_10, "player_10", X_OBJ_NAMES("Cutscene Actor 10")) X_OBJ_NAME_DEFINE(O_PLAYER_2, "player_2", X_OBJ_NAMES("Cutscene Actor 2")) X_OBJ_NAME_DEFINE(O_PLAYER_3, "player_3", X_OBJ_NAMES("Cutscene Actor 3")) X_OBJ_NAME_DEFINE(O_PLAYER_4, "player_4", X_OBJ_NAMES("Cutscene Actor 4")) X_OBJ_NAME_DEFINE(O_PLAYER_5, "player_5", X_OBJ_NAMES("Cutscene Actor 5")) X_OBJ_NAME_DEFINE(O_PLAYER_6, "player_6", X_OBJ_NAMES("Cutscene Actor 6")) X_OBJ_NAME_DEFINE(O_PLAYER_7, "player_7", X_OBJ_NAMES("Cutscene Actor 7")) X_OBJ_NAME_DEFINE(O_PLAYER_8, "player_8", X_OBJ_NAMES("Cutscene Actor 8")) X_OBJ_NAME_DEFINE(O_PLAYER_9, "player_9", X_OBJ_NAMES("Cutscene Actor 9")) X_OBJ_NAME_DEFINE(O_RICOCHET, "ricochet", X_OBJ_NAMES("Ricochet")) X_OBJ_NAME_DEFINE(O_ROCKET, "rocket", X_OBJ_NAMES("Rocket")) X_OBJ_NAME_DEFINE(O_SHADOW, "shadow", X_OBJ_NAMES("Shadow")) X_OBJ_NAME_DEFINE(O_SHOTGUN_SHELL, "shotgun_shell", X_OBJ_NAMES("Shotgun Shell")) X_OBJ_NAME_DEFINE(O_SKIDOO_TRACK, "skidoo_track", X_OBJ_NAMES("Snowmobile Track")) X_OBJ_NAME_DEFINE(O_SKYBOX, "skybox", X_OBJ_NAMES("Skybox")) X_OBJ_NAME_DEFINE(O_SNOW_SPRITE, "snow_sprite", X_OBJ_NAMES("Snowmobile Wake")) X_OBJ_NAME_DEFINE(O_SPHERE_OF_DOOM_1, "sphere_of_doom_1", X_OBJ_NAMES("Dragon Explosion 1")) X_OBJ_NAME_DEFINE(O_SPHERE_OF_DOOM_2, "sphere_of_doom_2", X_OBJ_NAMES("Dragon Explosion 2")) X_OBJ_NAME_DEFINE(O_SPHERE_OF_DOOM_3, "sphere_of_doom_3", X_OBJ_NAMES("Dragon Explosion 3")) X_OBJ_NAME_DEFINE(O_SPLASH_1, "splash_1", X_OBJ_NAMES("Water Ripples 1")) X_OBJ_NAME_DEFINE(O_SPLASH_2, "splash_2", X_OBJ_NAMES("Water Ripples 2")) X_OBJ_NAME_DEFINE(O_TEXT_BOX, "text_box", X_OBJ_NAMES("UI Frame")) X_OBJ_NAME_DEFINE(O_TROPICAL_FISH, "tropical_fish", X_OBJ_NAMES("Tropical Fish")) X_OBJ_NAME_DEFINE(O_TWINKLE, "twinkle", X_OBJ_NAMES("Sparkles")) X_OBJ_NAME_DEFINE(O_WATERFALL, "waterfall", X_OBJ_NAMES("Waterfall Mist")) X_OBJ_NAME_DEFINE(O_WATER_SPRITE, "water_sprite", X_OBJ_NAMES("Boat Wake")) X_OBJ_NAME_DEFINE(O_WINSTON, "winston", X_OBJ_NAMES("Winston")) X_OBJ_NAME_DEFINE(O_WINSTON_ARMY, "winston_army", X_OBJ_NAMES("Winston (army)")) ================================================ FILE: src/trx/game/objects/names.h ================================================ #pragma once #include #include typedef struct { OBJECT_ID object_id; const char *matched_name; } OBJECT_NAME_MATCH; // Get the current name for an object (may change on language reload). const char *Object_GetName(OBJECT_ID obj_id); // Get a stable pointer-to-pointer for the object name, content of which // automatically udpates on each language reload. const char *const *Object_GetNamePtr(OBJECT_ID obj_id); const char *Object_GetDescription(OBJECT_ID obj_id); void Object_ResetAllNames(void); void Object_ClearNames(OBJECT_ID obj_id); void Object_AddName(OBJECT_ID obj_id, const char *name); void Object_SetDescription(OBJECT_ID obj_id, const char *description); // Return a list of matching names, with an optional filter callback to only // consider objects satisfying certain criteria. out_match_count may be // nullptr. The result must be freed by the caller with Memory_Free(). OBJECT_NAME_MATCH *Object_IdsFromName( const char *name, int32_t *out_match_count, bool (*filter)(OBJECT_ID)); // Return an unique object id for a given programmatic string. // Example: // Given a string "key_1", returns O_KEY_1. OBJECT_ID Object_IdFromKey(const char *key); ================================================ FILE: src/trx/game/objects/setup.c ================================================ #include #include #include #include #define M_DEFAULT_RADIUS 10 static void M_SetupLara(void) { OBJECT *const obj = Object_Get(O_LARA); obj->initialise_func = Lara_InitialiseLoad; obj->can_interpolate_func = Lara_CanInterpolate; obj->draw_func = nullptr; obj->get_mesh_index_func = Lara_GetMeshIndex; obj->shadow_size = (UNIT_SHADOW * 10) / 16; obj->hit_points = g_Config.gameplay.start_lara_hitpoints; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; } void Object_SetupAllObjects(void) { for (int32_t i = O_FIRST; i < O_NUMBER_OF; i++) { OBJECT *const obj = Object_Get(i); obj->initialise_func = nullptr; obj->control_func = nullptr; obj->floor_height_func = nullptr; obj->ceiling_height_func = nullptr; obj->draw_func = Object_DrawAnimatingItem; obj->collision_func = nullptr; obj->add_walkable_func = nullptr; obj->is_usable_func = nullptr; obj->can_drop_items_func = nullptr; obj->can_interpolate_func = Object_CanInterpolate; obj->should_spawn_blood_func = nullptr; obj->is_alive_func = nullptr; obj->is_targetable_func = nullptr; obj->can_take_damage_func = nullptr; obj->can_be_projectile_target_func = nullptr; obj->can_be_exploded_func = nullptr; obj->get_mesh_index_func = nullptr; obj->hit_points = 0; obj->pivot_length = 0; obj->radius = M_DEFAULT_RADIUS; obj->shadow_size = 0; obj->enable_interpolation = true; obj->lot_setup = LOT_Setup(LOT_SETUP_DEFAULT); obj->save_position = false; obj->save_hitpoints = false; obj->save_flags = false; obj->save_anim = false; obj->load_floor = false; obj->intelligent = false; obj->smartness = -1; if (obj->setup_func != nullptr) { obj->setup_func(obj); } } M_SetupLara(); Lara_Hair_Initialise(); } ================================================ FILE: src/trx/game/objects/setup.h ================================================ #pragma once void Object_SetupAllObjects(void); ================================================ FILE: src/trx/game/objects/traps/blade.c ================================================ #include #include #include // clang-format off #define BLADE_CUT_DAMAGE 100 #define BLADE_TOUCH_BITS 0b00000010 // = 2 // clang-format on typedef enum { // clang-format off BLADE_STATE_EMPTY = 0, BLADE_STATE_STOP = 1, BLADE_STATE_CUT = 2, // clang-format on } BLADE_STATE; typedef enum { // clang-format off BLADE_ANIM_RETURN = 0, BLADE_ANIM_FINISHED = 1, BLADE_ANIM_SET = 2, BLADE_ANIM_CUT = 3, // clang-format on } BLADE_ANIM; static void M_Initialise(const int16_t item_num) { const OBJECT *const obj = Object_Get(O_BLADE); ITEM *const item = Item_Get(item_num); Item_SwitchToAnim(item, BLADE_ANIM_SET, 0); item->current_anim_state = BLADE_STATE_STOP; } static void M_Stop(ITEM *const item) { const int16_t anim_idx = Item_GetRelativeAnim(item); if (anim_idx == BLADE_ANIM_CUT) { const ANIM *const anim = Item_GetAnim(item); if (!Item_IsTriggerActive(item) && anim->jump_anim_num == item->anim_num && Item_TestFrameEqual(item, -1)) { item->status = IS_INACTIVE; Item_RemoveActive(Item_GetIndex(item)); return; } } item->goal_anim_state = BLADE_STATE_STOP; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (Item_IsTriggerActive(item) && item->current_anim_state == BLADE_STATE_STOP) { item->goal_anim_state = BLADE_STATE_CUT; } else { M_Stop(item); } if ((item->touch_bits & BLADE_TOUCH_BITS) != 0 && item->current_anim_state == BLADE_STATE_CUT) { Lara_TakeDamage(BLADE_CUT_DAMAGE, true); const ITEM *const lara_item = Lara_GetItem(); Spawn_BloodBath( lara_item->pos.x, item->pos.y - STEP_L, lara_item->pos.z, lara_item->speed, lara_item->rot.y, lara_item->room_num, 2); } Item_Animate(item); } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Object_Collision_Trap; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_BLADE, M_Setup) ================================================ FILE: src/trx/game/objects/traps/bubble_emitter.c ================================================ #include #include #include #include #include static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (!Item_IsTriggerActive(item)) { item->status = IS_INACTIVE; Item_RemoveActive(item_num); return; } if (Random_GetControl() % 24) { return; } const int32_t count = 1 + Random_GetControl() % 2; for (int32_t i = 0; i < count; i++) { Spawn_Bubble(&item->pos, item->room_num); } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->collision_func = nullptr; obj->draw_func = nullptr; obj->save_flags = true; } REGISTER_OBJECT(O_BUBBLE_EMITTER, M_Setup) ================================================ FILE: src/trx/game/objects/traps/cleaner.c ================================================ #include #include #include #include #include #include #include #include #include // clang-format off #define M_NODE_COUNT 3 #define M_TOUCH_BITS 0b11111111'11111100 #define M_RADIUS (STEP_L * 2) // = 512 #define M_TURN (DEG_90 / 16) // = 1024 #define M_VELOCITY (STEP_L / 4) // = 64 #define M_MAX_DIST (WALL_L * 20) // = 20480 // clang-format on typedef struct { bool resume; int16_t turn; int16_t velocity; uint8_t sparks[M_NODE_COUNT]; } M_PRIV; typedef struct { int16_t joint_idx; int16_t node_idx; } M_SPARK_NODE; static const M_SPARK_NODE m_Nodes[M_NODE_COUNT] = { { .joint_idx = 5, .node_idx = 9 }, { .joint_idx = 9, .node_idx = 10 }, { .joint_idx = 13, .node_idx = 11 }, }; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "resume", &p->resume)); JSON_SHOULD(JSON_READ(io, "turn", &p->turn)); JSON_SHOULD(JSON_READ(io, "velocity", &p->velocity)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "resume", p->resume); JSONW_WRITE(io, "turn", p->turn); JSONW_WRITE(io, "velocity", p->velocity); } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; item->pos.x = ROUND_TO_SECTOR(item->pos.x) + STEP_L * 2; item->pos.z = ROUND_TO_SECTOR(item->pos.z) + STEP_L * 2; p->resume = false; p->turn = M_TURN; p->velocity = M_VELOCITY; } static void M_TriggerSparks( const XYZ_32 pos, const int16_t item_num, const int16_t node) { const ITEM *const lara_item = Lara_GetItem(); const XZ_32 delta = { .x = lara_item->pos.x - pos.x, .z = lara_item->pos.z - pos.z, }; if (ABS(delta.x) > M_MAX_DIST || ABS(delta.z) > M_MAX_DIST) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = (Random_GetControl() & 0x3F) + 192; spark->src_color.g = spark->src_color.r; spark->src_color.b = spark->src_color.r; spark->dst_color.r = spark->src_color.b >> 2; spark->dst_color.g = spark->src_color.b >> 1; spark->dst_color.b = (Random_GetControl() & 0x3F) + 192; spark->col_fade_speed = 8; spark->fade_to_black = 8; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->dynamic = -1; spark->life = (Random_GetControl() & 7) + 20; spark->s_life = spark->life; spark->pos.x = (Random_GetControl() & 0x1F) - 16; spark->pos.y = (Random_GetControl() & 0x1F) - 16; spark->pos.z = (Random_GetControl() & 0x1F) - 16; spark->vel.x = ((Random_GetControl() & 0xFF) << 2) - 512; spark->vel.y = (Random_GetControl() & 7) - 4; spark->vel.z = ((Random_GetControl() & 0xFF) << 2) - 512; spark->friction = 4; spark->flags = SPARK_F_ATTACHED_NODE | SPARK_F_ITEM | SPARK_F_SCALE; spark->item_num = item_num; spark->node_num = node; spark->scalar = 1; spark->size.width = (Random_GetControl() & 3) + 4; spark->src_size.width = spark->size.width; spark->dst_size.width = spark->size.width >> 1; spark->size.height = spark->size.width; spark->src_size.height = spark->size.height; spark->dst_size.height = spark->size.height >> 1; spark->max_y_vel = 0; spark->gravity = (Random_GetControl() & 3) + 4; Sparks_FinishSetup(spark); } static XZ_32 M_GetDirection(const int16_t yaw) { // clang-format off switch (yaw) { case 0: return (XZ_32) { +0, +1 }; case DEG_90: return (XZ_32) { +1, +0 }; case -DEG_90: return (XZ_32) { -1, +0 }; case -DEG_180: return (XZ_32) { +0, -1 }; default: return (XZ_32) { +0, +0 }; } // clang-format on } static bool M_IsMidSector(const ITEM *const item) { const XZ_32 dir = M_GetDirection(item->rot.y); if (dir.x != 0) { return (item->pos.x & (WALL_L - 1)) == STEP_L * 2; } if (dir.z != 0) { return (item->pos.z & (WALL_L - 1)) == STEP_L * 2; } return false; } static bool M_TestSector( const XYZ_32 pos, const XZ_32 dir, int16_t *const room_num) { const XYZ_32 test_pos = { .x = pos.x + dir.x * WALL_L, .y = pos.y, .z = pos.z + dir.z * WALL_L, }; const SECTOR *sector = Room_GetSector(test_pos, room_num); const int32_t height = Room_GetHeight(sector, test_pos); const ROOM *const room = Room_Get(*room_num); sector = Room_GetWorldSector(room, test_pos.x, test_pos.z); return height == test_pos.y && !sector->stopper; } static void M_DecideMove(ITEM *const item) { M_PRIV *const p = item->priv; const XZ_32 dir_ahead = M_GetDirection(item->rot.y); const XZ_32 dir_left = M_GetDirection(item->rot.y - DEG_90); int16_t room_num = item->room_num; const bool move_left = M_TestSector(item->pos, dir_left, &room_num); room_num = item->room_num; const bool move_ahead = M_TestSector(item->pos, dir_ahead, &room_num); if (!move_ahead && !move_left && p->turn > 0) { item->rot.y += M_TURN; p->turn = M_TURN; } else if (!move_ahead && !move_left && p->turn < 0) { item->rot.y -= M_TURN; p->turn = -M_TURN; } else if (move_left && p->turn > 0) { item->rot.y -= M_TURN; p->turn = -M_TURN; } else { p->turn = M_TURN; p->resume = true; item->pos.x += dir_ahead.x * p->velocity; item->pos.z += dir_ahead.z * p->velocity; XYZ_32 pos = item->pos; pos.x += dir_ahead.x * WALL_L; pos.z += dir_ahead.z * WALL_L; const ROOM *const room = Room_Get(room_num); SECTOR *const sector = Room_GetWorldSector(room, pos.x, pos.z); sector->stopper = true; } } static void M_HitLara(ITEM *const item) { LARA_INFO *const lara = Lara_GetLaraInfo(); if (g_Config.debug.enable_invulnerability || lara->electric != 0) { return; } lara->electric = 1; Lara_GetItem()->hit_points = 0; M_PRIV *const p = item->priv; p->velocity = 0; Sound_Effect(SFX_CLEANER_FUSEBOX, &item->pos, SPM_NORMAL); } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; if (p->velocity == 0) { return; } if ((item->rot.y & 0x3FFF) != 0) { item->rot.y += p->turn; } else if (M_IsMidSector(item)) { if (p->resume) { const XYZ_32 pos = XYZ_32_OffsetYaw(item->pos, item->rot.y + DEG_180, WALL_L); int16_t room_num = item->room_num; Room_GetSector(pos, &room_num); const ROOM *const room = Room_Get(room_num); SECTOR *const sector = Room_GetWorldSector(room, pos.x, pos.z); sector->stopper = false; p->resume = false; } M_DecideMove(item); if (Room_TestTriggers(item)) { p->velocity = 0; Sound_Effect(SFX_CLEANER_FUSEBOX, &item->pos, SPM_NORMAL); } } else { const XZ_32 dir = M_GetDirection(item->rot.y); item->pos.x += dir.x * p->velocity; item->pos.z += dir.z * p->velocity; } if ((item->touch_bits & M_TOUCH_BITS) != 0) { M_HitLara(item); } Item_Animate(item); int16_t room_num = item->room_num; Room_GetSector(item->pos, &room_num); Item_UpdateRoom(item_num, room_num); Sound_Effect(SFX_CLEANER_LOOP, &item->pos, SPM_NORMAL); for (int32_t i = 0; i < M_NODE_COUNT; i++) { const M_SPARK_NODE *const node = &m_Nodes[i]; const int32_t rnd = Random_GetControl(); if (p->sparks[i] == 0 && (rnd & 7) != 0) { continue; } if (p->sparks[i] == 0) { p->sparks[i] = (Random_GetControl() & 7) + 4; } else { p->sparks[i]--; } XYZ_32 pos = { .x = -160, .y = -8, .z = 16, }; Collide_GetJointAbsPosition(item, &pos, node->joint_idx); M_TriggerSparks(pos, item_num, node->node_idx); pos.x += (Random_GetControl() & 0x1F) - 16; pos.y += (Random_GetControl() & 0x1F) - 16; pos.z += (Random_GetControl() & 0x1F) - 16; const int32_t shade = (Random_GetControl() & 0x7F) + 128; const RGB_888 color = { shade >> 2, shade >> 1, shade }; Output_AddDynamicLightRGB(pos, 10, color); } } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Object_Collision; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->radius = M_RADIUS; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_ELECTRIC_CLEANER, M_Setup) ================================================ FILE: src/trx/game/objects/traps/common.c ================================================ #include #include #include #include void Trap_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); TRAP_DATA *const data = GameBuf_Alloc(sizeof(TRAP_DATA), GBUF_ITEM_DATA); data->pos = item->pos; data->room_num = item->room_num; item->trap_data = data; } void Trap_Reset(ITEM *const item) { const TRAP_DATA *const data = item->trap_data; const int16_t item_num = Item_GetIndex(item); item->status = IS_INACTIVE; item->pos = data->pos; Item_UpdateRoom(item_num, data->room_num); item->goal_anim_state = TRAP_SET; item->current_anim_state = TRAP_SET; Item_SwitchToAnim(item, 0, 0); item->goal_anim_state = Item_GetAnim(item)->current_anim_state; item->current_anim_state = item->goal_anim_state; item->required_anim_state = TRAP_SET; Item_RemoveActive(item_num); } ================================================ FILE: src/trx/game/objects/traps/common.h ================================================ #pragma once #include typedef struct TRAP_DATA { XYZ_32 pos; int16_t room_num; } TRAP_DATA; void Trap_Initialise(int16_t item_num); void Trap_Reset(ITEM *item); ================================================ FILE: src/trx/game/objects/traps/damocles_sword.c ================================================ #include #include #include #include #include #include #include #define M_ACTIVATE_DIST ((WALL_L * 3) / 2) #define M_DAMAGE 100 static void M_Reset(ITEM *const item) { item->required_anim_state = (Random_GetControl() - 0x4000) / 16; item->fall_speed = 50; int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector( (XYZ_32) { item->pos.x, MAX_HEIGHT, item->pos.z }, &room_num); item->floor = Room_GetHeight(sector, item->pos); } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); item->rot.y = Random_GetControl(); Trap_Initialise(item_num); M_Reset(item); } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (!Item_IsTriggerActive(item)) { Trap_Reset(item); M_Reset(item); return; } if (item->status == IS_DEACTIVATED) { return; } if (item->gravity) { item->rot.y += item->required_anim_state; item->fall_speed += item->fall_speed < FAST_FALL_SPEED ? GRAVITY : 1; item->pos.y += item->fall_speed; item->pos.x += item->current_anim_state; item->pos.z += item->goal_anim_state; int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); item->floor = Room_GetHeight(sector, item->pos); Item_UpdateRoom(item_num, room_num); if (item->pos.y > item->floor) { Sound_Effect(SFX_DAMOCLES_SWORD, &item->pos, SPM_NORMAL); item->pos.y = item->floor + 10; item->gravity = false; item->status = IS_DEACTIVATED; } } else if (item->pos.y != item->floor) { item->rot.y += item->required_anim_state; const ITEM *const lara_item = Lara_GetItem(); const int32_t x = lara_item->pos.x - item->pos.x; const int32_t y = lara_item->pos.y - item->pos.y; const int32_t z = lara_item->pos.z - item->pos.z; if (ABS(x) <= M_ACTIVATE_DIST && ABS(z) <= M_ACTIVATE_DIST && y > 0 && y < WALL_L * 3) { item->current_anim_state = x / 32; item->goal_anim_state = z / 32; item->gravity = true; } } } static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); if (!Lara_TestBoundsCollide(item, coll->radius)) { return; } if (coll->enable_baddie_push) { Lara_Col_ItemPush(item, coll, false, true); } if (item->gravity) { lara_item->hit_points -= M_DAMAGE; int32_t x = lara_item->pos.x + (Random_GetControl() - 0x4000) / 256; int32_t z = lara_item->pos.z + (Random_GetControl() - 0x4000) / 256; int32_t y = lara_item->pos.y - Random_GetControl() / 44; int32_t d = lara_item->rot.y + (Random_GetControl() - 0x4000) / 8; Spawn_Blood(x, y, z, lara_item->speed, d, lara_item->room_num); } } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = M_Collision; obj->shadow_size = UNIT_SHADOW; obj->save_position = true; obj->save_anim = true; obj->save_flags = true; } REGISTER_OBJECT(O_DAMOCLES_SWORD, M_Setup) ================================================ FILE: src/trx/game/objects/traps/dart.c ================================================ #include #include #include #include #include #include #include #include #include #include #define M_DART_DAMAGE 50 #define M_POISON_DART_DAMAGE 25 #define M_POISON_AMOUNT 160 #define M_PITCH (DEG_45 / 2) typedef struct { bool pending_kill; } M_PRIV; static void M_DamageLara(const ITEM *const item) { const bool is_poison = item->object_id == O_POISON_DART; const int32_t damage = is_poison ? M_POISON_DART_DAMAGE : M_DART_DAMAGE; Lara_TakeDamage(damage, true); if (is_poison) { LARA_INFO *const lara = Lara_GetLaraInfo(); lara->poison_timer += M_POISON_AMOUNT; } const ITEM *const lara_item = Lara_GetItem(); Spawn_Blood( item->pos.x, item->pos.y, item->pos.z, lara_item->speed, lara_item->rot.y, lara_item->room_num); } static void M_Hit( const int16_t item_num, const XYZ_32 pos, const int16_t old_room_num) { const ITEM *const item = Item_Get(item_num); if (item->object_id == O_POISON_DART) { for (int32_t i = 0; i < 4; i++) { Sparks_TriggerDartSmoke(pos, (XZ_32) {}, true); } ITEM *const poison_item = Item_Get(item_num); M_PRIV *const p = poison_item->priv; p->pending_kill = true; Item_UpdateRoom(item_num, old_room_num); return; } Item_Kill(item_num); Sound_Effect(SFX_PROJECTILE_HIT, &item->pos, SPM_NORMAL); int16_t room_num = item->room_num; Room_GetSector(pos, &room_num); const int16_t effect_num = Effect_Create(room_num); if (effect_num != NO_EFFECT) { EFFECT *const effect = Effect_Get(effect_num); effect->object_id = O_RICOCHET; effect->pos = pos; effect->rot = item->rot; effect->counter = 6; effect->frame_num = -3 * Random_GetControl() / 0x8000; } } static void M_Animate(ITEM *const item) { if (item->object_id != O_POISON_DART) { Item_Animate(item); return; } const int32_t speed = (item->speed * Math_Cos(item->rot.x)) >> W2V_SHIFT; item->pos.x += (speed * Math_Sin(item->rot.y)) >> W2V_SHIFT; item->pos.y -= (item->speed * Math_Sin(item->rot.x)) >> W2V_SHIFT; item->pos.z += (speed * Math_Cos(item->rot.y)) >> W2V_SHIFT; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (item->object_id == O_POISON_DART) { M_PRIV *const p = item->priv; if (p->pending_kill) { Item_Kill(item_num); return; } } if (item->touch_bits != 0) { M_DamageLara(item); if (item->object_id == O_POISON_DART) { Item_Kill(item_num); return; } } const GAME_VECTOR old_pos = { .pos = item->pos, .room_num = item->room_num }; M_Animate(item); int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); Item_UpdateRoom(item_num, room_num); if (item->object_id == O_DISC) { item->rot.x += M_PITCH; } item->floor = Room_GetHeight(sector, item->pos); const GAME_VECTOR new_pos = { .pos = item->pos, .room_num = item->room_num }; if (item->pos.y >= item->floor) { M_Hit( item_num, Spawn_GetRayPos(old_pos, new_pos, STEP_L / 12), old_pos.room_num); } } static bool M_DrawPoisonDart(const ITEM *const item) { const XYZ_32 origin = item->interp.result.pos; const XYZ_16 rot = item->interp.result.rot; const int32_t size = (-96 * Math_Cos(rot.x)) >> W2V_SHIFT; const XYZ_32 to = { .x = origin.x + ((size * Math_Sin(rot.y)) >> W2V_SHIFT), .y = origin.y + ((96 * Math_Sin(rot.x)) >> W2V_SHIFT), .z = origin.z + ((size * Math_Cos(rot.y)) >> W2V_SHIFT), }; const RGBA_8888 color = { 0x78, 0x3C, 0x14, 0xFF }; OutputSource_PolyFX_StageLineSegment( origin, color, to, color, 2.0f, DRAW_BLEND); return true; } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->collision_func = Object_Collision; obj->shadow_size = UNIT_SHADOW / 2; obj->save_flags = true; } static void M_SetupPoisonDart(OBJECT *const obj) { obj->control_func = M_Control; obj->collision_func = Object_Collision; obj->draw_func = M_DrawPoisonDart; obj->priv_size = sizeof(M_PRIV); obj->shadow_size = UNIT_SHADOW / 2; obj->save_flags = true; } REGISTER_OBJECT(O_DART, M_Setup) REGISTER_OBJECT(O_DISC, M_Setup) REGISTER_OBJECT(O_POISON_DART, M_SetupPoisonDart) ================================================ FILE: src/trx/game/objects/traps/dart_emitter.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #define M_POISON_FIRE_TIMER 24 typedef enum { // clang-format off STATE_IDLE = 0, STATE_FIRE = 1, STATE_RELOAD = 2, // clang-format on } M_STATE; typedef struct { int32_t fire_timer; } M_PRIV; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "fire_timer", &p->fire_timer)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "fire_timer", p->fire_timer); } static OBJECT_ID M_GetProjectileObjectID(const OBJECT_ID emitter_id) { switch (emitter_id) { case O_DART_EMITTER: return O_DART; case O_DISC_EMITTER: return O_DISC; case O_POISON_DART_EMITTER: return O_POISON_DART; default: return NO_OBJECT; } } static void M_TriggerPoisonDartSmoke( const ITEM *const item, const int32_t x, const int32_t z) { const int32_t x_limit = x != 0 ? ABS(x << 1) - 1 : 0; const int32_t z_limit = x == 0 ? ABS(z << 1) - 1 : 0; for (int32_t i = 0; i < 5; i++) { const int32_t rnd = -Random_GetControl(); const XZ_32 vel = { .x = x >= 0 ? (x_limit & rnd) : -(x_limit & rnd), .z = z >= 0 ? (z_limit & rnd) : -(z_limit & rnd), }; Sparks_TriggerDartSmoke(item->pos, vel, false); } } static void M_CreateProjectile(const ITEM *const item) { const OBJECT_ID projectile_obj_id = M_GetProjectileObjectID(item->object_id); if (!Object_Get(projectile_obj_id)->loaded) { LOG_ERROR( "Projectile object not loaded for item #%d", Item_GetIndex(item)); return; } const int16_t projectile_item_num = Item_Create(); if (projectile_item_num == NO_ITEM) { return; } ITEM *const projectile_item = Item_Get(projectile_item_num); projectile_item->object_id = projectile_obj_id; projectile_item->room_num = item->room_num; projectile_item->shade.value_1 = -1; projectile_item->rot.y = item->rot.y; projectile_item->pos.y = item->pos.y - 512; const bool is_poison = item->object_id == O_POISON_DART_EMITTER; const int32_t wall_inset = is_poison ? 0 : 100; int32_t x = 0; int32_t z = 0; switch (projectile_item->rot.y) { case 0: z = (is_poison ? 1 : -1) * (WALL_L / 2 - wall_inset); break; case DEG_90: x = (is_poison ? 1 : -1) * (WALL_L / 2 - wall_inset); break; case -DEG_180: z = (is_poison ? -1 : 1) * (WALL_L / 2 - wall_inset); break; case -DEG_90: x = (is_poison ? -1 : 1) * (WALL_L / 2 - wall_inset); break; } projectile_item->pos.x = item->pos.x + x; projectile_item->pos.z = item->pos.z + z; Item_Initialise(projectile_item_num); Item_AddActive(projectile_item_num); projectile_item->status = IS_ACTIVE; if (is_poison) { projectile_item->rot.y += DEG_180; projectile_item->speed = STEP_L; M_TriggerPoisonDartSmoke(projectile_item, x, z); Sound_Effect(SFX_BLOWPIPE_BLOW, &projectile_item->pos, SPM_NORMAL); } else if (item->object_id == O_DART_EMITTER) { const int16_t effect_num = Effect_Create(projectile_item->room_num); if (effect_num != NO_EFFECT) { EFFECT *const effect = Effect_Get(effect_num); effect->pos = projectile_item->pos; effect->rot = projectile_item->rot; effect->speed = 0; effect->frame_num = 0; effect->counter = 0; effect->object_id = O_DART_EFFECT; } Sound_Effect(SFX_DART, &projectile_item->pos, SPM_NORMAL); } else { Sound_Effect(SFX_DISC, &projectile_item->pos, SPM_NORMAL); } } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (Item_IsTriggerActive(item)) { if (item->current_anim_state == STATE_IDLE) { item->goal_anim_state = STATE_FIRE; } } else if (item->current_anim_state == STATE_FIRE) { item->goal_anim_state = STATE_IDLE; } if (item->current_anim_state == STATE_FIRE && Item_TestFrameEqual(item, 0)) { M_CreateProjectile(item); } Item_Animate(item); } static void M_ControlPoisonEmitter(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; if (!Item_IsTriggerActive(item)) { p->fire_timer = 0; return; } if (p->fire_timer > 0) { p->fire_timer--; return; } p->fire_timer = M_POISON_FIRE_TIMER; M_CreateProjectile(item); } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->save_flags = true; } static void M_SetupPoisonEmitter(OBJECT *const obj) { obj->draw_func = nullptr; obj->control_func = M_ControlPoisonEmitter; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->save_flags = true; } REGISTER_OBJECT(O_DART_EMITTER, M_Setup) REGISTER_OBJECT(O_DISC_EMITTER, M_Setup) REGISTER_OBJECT(O_POISON_DART_EMITTER, M_SetupPoisonEmitter) ================================================ FILE: src/trx/game/objects/traps/dying_monk.c ================================================ #include #include #include #include #include #define MAX_ROOMIES 2 typedef struct { int32_t roomies[MAX_ROOMIES]; } M_PRIV; static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; for (int32_t i = 0; i < MAX_ROOMIES; i++) { p->roomies[i] = NO_ITEM; } int32_t roomie_count = 0; int16_t test_item_num = Room_Get(item->room_num)->item_num; while (test_item_num != NO_ITEM) { const ITEM *const test_item = Item_Get(test_item_num); const OBJECT *const test_obj = Object_Get(test_item->object_id); if (test_obj->intelligent) { p->roomies[roomie_count++] = test_item_num; if (roomie_count >= MAX_ROOMIES) { break; } } test_item_num = test_item->next_item; } } static void M_Control(const int16_t item_num) { const ITEM *const item = Item_Get(item_num); const M_PRIV *const p = item->priv; for (int32_t i = 0; i < MAX_ROOMIES; i++) { int32_t test_item_num = p->roomies[i]; if (test_item_num != NO_ITEM) { const ITEM *const test_item = Item_Get(test_item_num); if (test_item->hit_points > 0) { return; } } } const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = ABS(lara_item->pos.x - item->pos.x); const int32_t dz = ABS(lara_item->pos.z - item->pos.z); if (dx < WALL_L && dz < WALL_L && !lara_item->gravity && lara_item->pos.y == item->pos.y) { Game_SetIsLevelComplete(true); } } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Object_Collision; obj->priv_size = sizeof(M_PRIV); obj->save_flags = true; } REGISTER_OBJECT(O_DYING_MONK, M_Setup) ================================================ FILE: src/trx/game/objects/traps/electric_fence.c ================================================ #include #include #include #include #include #include #include #include #define M_EPSILON 32 typedef struct { bool is_initialised; bool is_flat; } M_PRIV; static bool M_IsFenceOnDeathSector(const ITEM *const item) { int16_t room_num = item->room_num; const SECTOR *sector = Room_GetSector(item->pos, &room_num); return sector->is_death_sector; } static void M_TriggerFenceSparks(const XYZ_32 pos, const bool kill) { SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.b = (Random_GetControl() & 63) + 192; spark->src_color.r = spark->src_color.b; spark->src_color.g = spark->src_color.b; spark->dst_color.b = (Random_GetControl() & 0x3F) + 192; spark->dst_color.r = spark->dst_color.b >> 2; spark->dst_color.g = spark->dst_color.b >> 1; // OG: 1 spark->col_fade_speed = 8; spark->fade_to_black = 16; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->life = 32 + (Random_GetControl() & 7); spark->s_life = spark->life; spark->dynamic = -1; spark->pos = pos; spark->vel.x = ((Random_GetControl() & 0xFF) << 1) - 256; spark->vel.y = (Random_GetControl() & 0xF) - ((int32_t)kill << 5) - 8; spark->vel.z = ((Random_GetControl() & 0xFF) << 1) - 256; spark->friction = 4; spark->flags = SPARK_F_SCALE; spark->scalar = 1 + kill; spark->max_y_vel = 0; spark->gravity = 16 + (Random_GetControl() & 0xF); spark->size.width = 4 + (Random_GetControl() & 3); spark->src_size.width = spark->size.width; spark->dst_size.width = 1; spark->size.height = spark->size.width * 2; spark->src_size.height = spark->src_size.width * 2; spark->dst_size.height = spark->dst_size.width * 2; Sparks_FinishSetup(spark); } static void M_TouchFence(const XZ_32 spark_axis, XYZ_32 spark_pos) { ITEM *const lara_item = Lara_GetItem(); const XYZ_32 old_spark_pos = spark_pos; const int32_t iterations = (Random_GetControl() & 0xF) + 3; for (int32_t i = 0; i < iterations; i++) { if (spark_axis.x != 0) { spark_pos.x = lara_item->pos.x + (Random_GetControl() & 0x1FF) - 256; } else { spark_pos.z = lara_item->pos.z + (Random_GetControl() & 0x1FF) - 256; } spark_pos.y = lara_item->pos.y - Random_GetControl() % 768; const int32_t spark_count = (Random_GetControl() & 3) + 6; for (int32_t j = 0; j < spark_count; j++) { M_TriggerFenceSparks(spark_pos, true); if (spark_axis.x != 0) { spark_pos.x += (spark_axis.x & Random_GetControl() & 7) - 4; } else { spark_pos.z += (spark_axis.z & Random_GetControl() & 7) - 4; } spark_pos.y += (Random_GetControl() & 7) - 4; } spark_pos = old_spark_pos; } LARA_INFO *const lara = Lara_GetLaraInfo(); lara->electric = 1; lara_item->hit_points = 0; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (!Item_IsTriggerActive(item)) { return; } ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; if (dx < -0x5000 || dx > 0x5000 || dz < -0x5000 || dz > 0x5000) { return; } XYZ_32 fence_center = {}; XYZ_32 spark_pos = {}; XZ_32 spark_axis = {}; XZ_32 fence_size = {}; M_PRIV *const p = item->priv; if (!p->is_initialised) { p->is_initialised = true; p->is_flat = M_IsFenceOnDeathSector(item); } switch (item->rot.y) { case 0: fence_center.x = item->pos.x + WALL_L / 2; fence_center.z = item->pos.z + WALL_L / 2; fence_size.x = WALL_L + M_EPSILON; fence_size.z = 128; spark_pos.x = fence_center.x - (WALL_L - M_EPSILON); spark_pos.z = fence_center.z; if (p->is_flat) { spark_axis.x = Random_GetControl() & 0x3FF; spark_pos.z += spark_axis.x * -(Random_GetControl() & 1); } spark_axis.x = WALL_L; spark_axis.z = 0; break; case -DEG_90: fence_center.x = item->pos.x - WALL_L / 2; fence_center.z = item->pos.z + WALL_L / 2; fence_size.x = 128; fence_size.z = WALL_L + M_EPSILON; spark_pos.x = fence_center.x; spark_pos.z = fence_center.z - (WALL_L - M_EPSILON); if (p->is_flat) { spark_axis.x = Random_GetControl() & 0x3FF; spark_pos.x += spark_axis.x * -(Random_GetControl() & 1); } spark_axis.x = 0; spark_axis.z = WALL_L; break; case -DEG_180: fence_center.x = item->pos.x - WALL_L / 2; fence_center.z = item->pos.z - WALL_L / 2; fence_size.x = WALL_L + M_EPSILON; fence_size.z = 128; spark_pos.x = fence_center.x - (WALL_L - M_EPSILON); spark_pos.z = fence_center.z; if (p->is_flat) { spark_axis.x = Random_GetControl() & 0x3FF; spark_pos.z += spark_axis.x * -(Random_GetControl() & 1); } spark_axis.x = WALL_L; spark_axis.z = 0; break; case DEG_90: fence_center.x = item->pos.x + WALL_L / 2; fence_center.z = item->pos.z - WALL_L / 2; fence_size.x = 128; fence_size.z = WALL_L + M_EPSILON; spark_pos.x = fence_center.x; spark_pos.z = fence_center.z - (WALL_L - M_EPSILON); if (p->is_flat) { spark_axis.x = Random_GetControl() & 0x3FF; spark_pos.x += spark_axis.x * -(Random_GetControl() & 1); } spark_axis.x = 0; spark_axis.z = WALL_L; break; default: break; } if ((Random_GetControl() & 0x1F) == 0) { if (spark_axis.x != 0) { spark_pos.x += Random_GetControl() & spark_axis.x; } else { spark_pos.z += Random_GetControl() & spark_axis.z; } if (p->is_flat) { spark_pos.y = item->pos.y - (Random_GetControl() & 0x1F); } else { spark_pos.y = item->pos.y - (Random_GetControl() & 0x7FF) - (Random_GetControl() & 0x3FF); } const int32_t spark_count = (Random_GetControl() & 3) + 3; for (int32_t i = 0; i < spark_count; i++) { M_TriggerFenceSparks(spark_pos, false); if (spark_axis.x != 0) { spark_pos.x += (spark_axis.x & Random_GetControl() & 7) - 4; } else { spark_pos.z += (spark_axis.z & Random_GetControl() & 7) - 4; } spark_pos.y += (Random_GetControl() & 7) - 4; } } LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->electric != 0 || p->is_flat || lara_item->pos.x < fence_center.x - fence_size.x || lara_item->pos.x > fence_center.x + fence_size.x || lara_item->pos.z < fence_center.z - fence_size.z || lara_item->pos.z > fence_center.z + fence_size.z || lara_item->pos.y > item->pos.y + M_EPSILON || lara_item->pos.y < item->pos.y - 3 * WALL_L) { return; } M_TouchFence(spark_axis, spark_pos); } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->draw_func = nullptr; obj->priv_size = sizeof(M_PRIV); obj->save_flags = true; } REGISTER_OBJECT(O_ELECTRIC_FENCE, M_Setup) ================================================ FILE: src/trx/game/objects/traps/ember_emitter.c ================================================ #include #include #include #include static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (!Item_IsTriggerActive(item)) { item->status = IS_INACTIVE; Item_RemoveActive(item_num); return; } const int16_t effect_num = Effect_Create(item->room_num); if (effect_num == NO_EFFECT) { return; } EFFECT *const effect = Effect_Get(effect_num); effect->pos = item->pos; effect->rot.y = 2 * Random_GetControl() + 0x8000; effect->speed = Random_GetControl() >> 10; effect->fall_speed = Random_GetControl() / -200; effect->frame_num = (-4 * Random_GetControl()) / 0x7FFF; effect->object_id = O_EMBER; Sound_Effect(SFX_LAVA_FOUNTAIN, &item->pos, SPM_NORMAL); } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->collision_func = Object_Collision; obj->draw_func = nullptr; obj->save_flags = true; } REGISTER_OBJECT(O_EMBER_EMITTER, M_Setup) ================================================ FILE: src/trx/game/objects/traps/falling_block.c ================================================ #include #include #include #include #include #include typedef struct { bool heavy_triggered; int32_t origin; } M_PRIV; static int32_t M_GetOrigin(const ITEM *const item) { const M_PRIV *const p = item->priv; return p->origin; } static void M_CalculateOrigin(ITEM *const item) { const OBJECT *const obj = Object_Get(item->object_id); const ANIM *const anim = Object_GetAnim(obj, 0); const ANIM_FRAME *const frame = &anim->frame_ptr[0]; M_PRIV *const p = item->priv; p->origin = ROUND_TO_CLICK_SIGNED(frame->offset.y); } static void M_Initialise(const int16_t item_num) { Trap_Initialise(item_num); ITEM *const item = Item_Get(item_num); M_CalculateOrigin(item); Walkable_AllocateNodes(item, 1); } static void M_DropStack(const ITEM *const item) { const int32_t origin = M_GetOrigin(item); const XYZ_32 drop_pos = { .x = item->pos.x, .y = item->pos.y + origin, .z = item->pos.z, }; MovableBlock_DropStack(drop_pos, item->room_num); } static int16_t M_GetFloorHeight( const ITEM *const item, const int32_t x, const int32_t y, const int32_t z, const int16_t height) { const int32_t origin = M_GetOrigin(item); if (y <= item->pos.y + origin && (item->current_anim_state == TRAP_SET || item->current_anim_state == TRAP_ACTIVATE)) { return item->pos.y + origin; } return height; } static int16_t M_GetCeilingHeight( const ITEM *const item, const int32_t x, const int32_t y, const int32_t z, const int16_t height) { const int32_t origin = M_GetOrigin(item); if (y > item->pos.y + origin && (item->current_anim_state == TRAP_SET || item->current_anim_state == TRAP_ACTIVATE)) { return item->pos.y + origin + STEP_L; } return height; } static void M_AddWalkable(const int16_t item_num) { const ITEM *const item = Item_Get(item_num); Walkable_Add(item_num, item->pos); } static bool M_Trigger(ITEM *const item, const TRIGGER *const trigger) { M_PRIV *const p = item->priv; p->heavy_triggered = trigger->type == TT_HEAVY; return true; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); const int32_t origin = M_GetOrigin(item); switch (item->current_anim_state) { case TRAP_SET: const ITEM *const lara_item = Lara_GetItem(); M_PRIV *const p = item->priv; if (!p->heavy_triggered && lara_item->pos.y != item->pos.y + origin) { item->status = IS_INACTIVE; Item_RemoveActive(item_num); return; } item->goal_anim_state = TRAP_ACTIVATE; p->heavy_triggered = false; break; case TRAP_ACTIVATE: item->goal_anim_state = TRAP_WORKING; break; case TRAP_WORKING: if (item->goal_anim_state != TRAP_FINISHED) { if (!item->gravity) { M_DropStack(item); } item->gravity = true; } break; default: break; } Item_Animate(item); if (item->status == IS_DEACTIVATED) { if (!Item_IsTriggerActive(item)) { Trap_Reset(item); } return; } int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); Item_UpdateRoom(item_num, room_num); item->floor = Room_GetHeight(sector, item->pos); if (item->current_anim_state == TRAP_WORKING && item->pos.y >= item->floor) { item->goal_anim_state = TRAP_FINISHED; item->pos.y = item->floor; item->fall_speed = 0; item->gravity = false; } } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->trigger_func = M_Trigger; obj->priv_size = sizeof(M_PRIV); obj->floor_height_func = M_GetFloorHeight; obj->ceiling_height_func = M_GetCeilingHeight; obj->add_walkable_func = M_AddWalkable; obj->save_position = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_FALLING_BLOCK_1, M_Setup) REGISTER_OBJECT(O_FALLING_BLOCK_2, M_Setup) REGISTER_OBJECT(O_FALLING_BLOCK_3, M_Setup) ================================================ FILE: src/trx/game/objects/traps/falling_ceiling.c ================================================ #include #include #include #define M_DAMAGE 300 static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (item->current_anim_state == TRAP_SET) { item->goal_anim_state = TRAP_ACTIVATE; item->gravity = true; } else if ( item->current_anim_state == TRAP_ACTIVATE && item->touch_bits != 0) { Lara_TakeDamage(M_DAMAGE, true); } Item_Animate(item); if (item->status == IS_DEACTIVATED) { if (!Item_IsTriggerActive(item)) { Trap_Reset(item); } return; } int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); item->floor = Room_GetHeight(sector, item->pos); Item_UpdateRoom(item_num, room_num); if (item->current_anim_state == TRAP_ACTIVATE && item->pos.y >= item->floor) { item->pos.y = item->floor; item->goal_anim_state = TRAP_WORKING; item->fall_speed = 0; item->gravity = false; } } static void M_Setup(OBJECT *const obj) { obj->initialise_func = Trap_Initialise; obj->control_func = M_Control; obj->collision_func = Object_Collision_Trap; obj->save_position = true; obj->save_anim = true; obj->save_flags = true; } REGISTER_OBJECT(O_FALLING_CEILING_1, M_Setup) REGISTER_OBJECT(O_FALLING_CEILING_2, M_Setup) ================================================ FILE: src/trx/game/objects/traps/fire_head.c ================================================ #include #include #include #include #include #include #include #include // clang-format off #define M_MIN_FALLOFF 8 #define M_MAX_RANGE (WALL_L * 2) // = 2048 #define M_RANGE_STEP (STEP_L / 8) // = 32 #define M_SPEED_STEP (STEP_L / 4) // = 64 // clang-format on typedef enum { M_STATE_IDLE, M_STATE_REAR, M_STATE_BLOW, } M_STATE; typedef enum { M_ANIM_REAR = 1, } M_ANIM; typedef struct { struct { int32_t max; int32_t current; } blow_loops; int32_t speed; int32_t deadly_range; bool stop; } M_PRIV; static const BITE m_Mouth = { .pos = { .x = 0, .y = 128, .z = 0 }, .mesh_num = 7, }; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; if (JSON_SHOULD(JSON_PUSH(io, "blow_loops"))) { JSON_SHOULD(JSON_READ(io, "max", &p->blow_loops.max)); JSON_SHOULD(JSON_READ(io, "current", &p->blow_loops.current)); JSON_POP(io); } JSON_SHOULD(JSON_READ(io, "speed", &p->speed)); JSON_SHOULD(JSON_READ(io, "deadly_range", &p->deadly_range)); JSON_SHOULD(JSON_READ(io, "stop", &p->stop)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "max", p->blow_loops.max); JSONW_WRITE(io, "current", p->blow_loops.current); JSONW_POP_AND_SET(io, "blow_loops"); JSONW_WRITE(io, "speed", p->speed); JSONW_WRITE(io, "deadly_range", p->deadly_range); JSONW_WRITE(io, "stop", p->stop); } static void M_TriggerFlame( const XYZ_32 pos, const int32_t angle, const int32_t speed) { const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - pos.x; const int32_t dz = lara_item->pos.z - pos.z; const int32_t max_dist = 16 * WALL_L; if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = (Random_GetControl() & 0x1F) + 48; spark->src_color.g = spark->src_color.r; spark->src_color.b = (Random_GetControl() & 0x3F) + 192; spark->dst_color.r = (Random_GetControl() & 0x3F) + 192; spark->dst_color.g = (Random_GetControl() & 0x3F) + 128; spark->dst_color.b = 32; spark->fade_to_black = 8; spark->col_fade_speed = (Random_GetControl() & 3) + 12; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->life = (Random_GetControl() & 7) + 28; spark->s_life = spark->life; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16; spark->pos.y = pos.y + (Random_GetControl() & 0x1F) - 16; spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16; const int32_t dist = speed - (Random_GetControl() % ((speed >> 3) + 1)); spark->vel.x = ((dist * Math_Sin(angle)) >> 13) + (Random_GetControl() & 0x7F) - 64; spark->vel.y = (Random_GetControl() & 7) + 6; spark->vel.z = ((dist * Math_Cos(angle)) >> 13) + (Random_GetControl() & 0x7F) - 64; spark->friction = 4; spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE; if ((Random_GetControl() & 1) != 0) { spark->flags |= SPARK_F_ROTATE; spark->rot_angle = Random_GetControl() & 0xFFF; spark->rot_add = (Random_GetControl() & 0x3F) - 32; } spark->gravity = -8 - (Random_GetControl() & 0xF); spark->max_y_vel = -8 - (Random_GetControl() & 7); spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->scalar = 3; spark->dst_size.width = (speed >> 4) + (Random_GetControl() & 0xF); spark->size.width = spark->dst_size.width >> 2; spark->src_size.width = spark->size.width; spark->dst_size.height = spark->dst_size.width; spark->size.height = spark->size.width; spark->src_size.height = spark->size.height; Sparks_FinishSetup(spark); } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); Item_SwitchToAnim(item, M_ANIM_REAR, 0); item->current_anim_state = M_STATE_REAR; item->goal_anim_state = M_STATE_REAR; } static bool M_Trigger(ITEM *const item, const TRIGGER *const trigger) { M_PRIV *const p = item->priv; if (p == nullptr) { return true; } if (trigger == nullptr || trigger->type == TT_ANTITRIGGER || trigger->type == TT_ANTIPAD) { return true; } item->timer = 0; p->blow_loops.max = trigger->timer; return true; } static void M_Reset(M_PRIV *const p) { p->blow_loops.current = p->blow_loops.max; p->speed = 0; p->deadly_range = 0; p->stop = false; } static bool M_TestFireRange(const ITEM *const item, const XYZ_32 pos) { // Originally, item->pos.y was used as the baseline here, but this fails for // the alternate model used in the gold expansion. The following produces // the same baseline as the original base game. const XYZ_32 lara_pos = Lara_GetItem()->pos; const int32_t y_pos = ROUND_TO_CLICK_UP(pos.y); if (lara_pos.y <= y_pos - (WALL_L / 2) || lara_pos.y >= y_pos + (STEP_L * 3)) { return false; } const M_PRIV *const p = item->priv; const int32_t forward_range = p->deadly_range; const int32_t side_range = WALL_L / 2; XZ_32 min = { pos.x, pos.z }; XZ_32 max = { pos.x, pos.z }; const DIRECTION dir = Math_GetDirection(item->rot.y); switch (dir) { case DIR_NORTH: max.x += forward_range; min.z -= side_range; max.z += side_range; break; case DIR_EAST: min.x -= side_range; max.x += side_range; min.z -= forward_range; break; case DIR_SOUTH: min.x -= forward_range; min.z -= side_range; max.z += side_range; break; case DIR_WEST: min.x -= side_range; max.x += side_range; max.z += forward_range; break; default: break; } return lara_pos.x > min.x && lara_pos.x < max.x && lara_pos.z > min.z && lara_pos.z < max.z; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (!Item_IsTriggerActive(item)) { return; } if (item->current_anim_state == M_STATE_IDLE) { item->goal_anim_state = M_STATE_REAR; Item_Animate(item); return; } M_PRIV *const p = item->priv; const RGB_888 light_color = { .r = (Random_GetControl() & 0x3F) + 192, .g = (Random_GetControl() & 0x1F) + 96, .b = 0, }; if (item->current_anim_state == M_STATE_REAR) { XYZ_32 pos = m_Mouth.pos; Collide_GetJointAbsPosition(item, &pos, m_Mouth.mesh_num); Output_AddDynamicLightRGB(pos, M_MIN_FALLOFF, light_color); M_Reset(p); } else { XYZ_32 pos = {}; Collide_GetJointAbsPosition(item, &pos, m_Mouth.mesh_num); if (p->stop) { if (p->speed != 0) { p->speed -= M_SPEED_STEP; } p->deadly_range -= M_RANGE_STEP; CLAMPL(p->deadly_range, 0); } else { if (p->speed < M_MAX_RANGE) { p->speed += M_SPEED_STEP; } if (p->deadly_range < M_MAX_RANGE) { p->deadly_range += M_RANGE_STEP; } Sound_Effect(SFX_FLAME_THROWER_LOOP, &item->pos, SPM_NORMAL); } p->blow_loops.current--; CLAMPL(p->blow_loops.current, 0); const int32_t time4 = Output_GetTimeInGame() * 4; if ((time4 & 4) != 0) { M_TriggerFlame(pos, item->rot.y + DEG_90, p->speed); } if (p->blow_loops.current == 0 && !p->stop && Item_TestFrameEqual(item, 0)) { p->stop = true; item->goal_anim_state = M_STATE_REAR; } Output_AddDynamicLightRGB( pos, (p->speed >> 7) + M_MIN_FALLOFF, light_color); const LARA_INFO *const lara = Lara_GetLaraInfo(); if (!lara->burn && M_TestFireRange(item, pos)) { Lara_CatchFire(); } } Item_Animate(item); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->collision_func = Object_Collision; obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->trigger_func = M_Trigger; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_FIRE_HEAD, M_Setup) ================================================ FILE: src/trx/game/objects/traps/flame_emitter.c ================================================ #include #include #include #include #include #include #include #include #include typedef struct { int16_t effect_num; } M_PRIV; typedef void (*FLAME_INIT_FUNC)(EFFECT *const effect, const ITEM *const item); static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "fx_num", Effect_GetInOrderNum(p->effect_num)); } static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { if (!g_Config.gameplay.enable_enhanced_saves) { return; } M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "fx_num", &p->effect_num)); } static void M_KillIfAlive(const ITEM *const item) { M_PRIV *const p = item->priv; if (p->effect_num == NO_EFFECT) { return; } Effect_Kill(p->effect_num); p->effect_num = NO_EFFECT; if (g_TRVersion == 1) { Sound_StopEffect(SFX_LOOP_FOR_SMALL_FIRES); } } static int16_t M_Spawn(ITEM *const item, const FLAME_INIT_FUNC init_func) { M_PRIV *const p = item->priv; const int16_t effect_num = Effect_Create(item->room_num); if (effect_num != NO_EFFECT) { EFFECT *const effect = Effect_Get(effect_num); effect->pos = item->pos; effect->object_id = O_FLAME; effect->counter = 0; init_func(effect, item); } return effect_num; } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; p->effect_num = NO_EFFECT; } static void M_ControlCommon( const int16_t item_num, const FLAME_INIT_FUNC init_func) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; if (!Item_IsTriggerActive(item)) { M_KillIfAlive(item); } else if (p->effect_num == NO_EFFECT) { p->effect_num = M_Spawn(item, init_func); } } static void M_InitDefault(EFFECT *const effect, const ITEM *const item) { effect->frame_num = 0; } static void M_InitBig(EFFECT *const effect, const ITEM *const item) { effect->rot.y = item->rot.y; effect->frame_num = FLAME_BIG; } static void M_InitSmall(EFFECT *const effect, const ITEM *const item) { effect->rot.y = item->rot.y; effect->frame_num = FLAME_SMALL; } static void M_InitJet(EFFECT *const effect, const ITEM *const item) { effect->rot.y = item->rot.y; effect->frame_num = FLAME_JET; effect->flag1 = 0; effect->flag2 = Random_GetControl() & 0x3F; } static void M_InitSide(EFFECT *const effect, const ITEM *const item) { effect->rot.y = item->rot.y; effect->frame_num = FLAME_SIDE; effect->flag1 = 0; effect->flag2 = 0; } static void M_Control(const int16_t item_num) { M_ControlCommon(item_num, M_InitDefault); } static void M_ControlBig(const int16_t item_num) { M_ControlCommon(item_num, M_InitBig); } static void M_ControlSmall(const int16_t item_num) { M_ControlCommon(item_num, M_InitSmall); } static void M_ControlJet(const int16_t item_num) { M_ControlCommon(item_num, M_InitJet); } static void M_ControlSide(const int16_t item_num) { M_ControlCommon(item_num, M_InitSide); } static void M_SetupCommon(OBJECT *const obj, void (*control_func)(int16_t)) { obj->initialise_func = M_Initialise; obj->control_func = control_func; obj->draw_func = nullptr; obj->save_flags = true; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; } static void M_Setup(OBJECT *const obj) { M_SetupCommon(obj, M_Control); } static void M_SetupBig(OBJECT *const obj) { M_SetupCommon(obj, M_ControlBig); } static void M_SetupSmall(OBJECT *const obj) { M_SetupCommon(obj, M_ControlSmall); } static void M_SetupJet(OBJECT *const obj) { M_SetupCommon(obj, M_ControlJet); } static void M_SetupSide(OBJECT *const obj) { M_SetupCommon(obj, M_ControlSide); } REGISTER_OBJECT(O_FLAME_EMITTER, M_Setup) REGISTER_OBJECT(O_FLAME_EMITTER_BIG, M_SetupBig) REGISTER_OBJECT(O_FLAME_EMITTER_SMALL, M_SetupSmall) REGISTER_OBJECT(O_FLAME_EMITTER_JET, M_SetupJet) REGISTER_OBJECT(O_FLAME_EMITTER_SIDE, M_SetupSide) ================================================ FILE: src/trx/game/objects/traps/gondola.c ================================================ #include #include #include #define M_SINK_SPEED 50 #define M_SINK_ROOM_SHIFT (STEP_L * 3 / 2) static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); switch (item->current_anim_state) { case GONDOLA_STATE_FLOATING: if (item->goal_anim_state == GONDOLA_STATE_CRASH) { item->mesh_bits = 0xFF; Item_Explode(item_num, 240, 0); } break; case GONDOLA_STATE_SINK: { item->pos.y = item->pos.y + M_SINK_SPEED; const ANIM_FRAME *const frame = Item_GetBestFrame(item); const int16_t room_shift = frame->bounds.min.y + M_SINK_ROOM_SHIFT; int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector( (XYZ_32) { item->pos.x, item->pos.y + room_shift, item->pos.z }, &room_num); item->floor = Room_GetHeight(sector, item->pos); Item_UpdateRoom(item_num, room_num); if (item->pos.y >= item->floor) { item->goal_anim_state = GONDOLA_STATE_LAND; item->pos.y = item->floor; } break; } } Item_Animate(item); if (item->status == IS_DEACTIVATED) { Item_RemoveActive(item_num); } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->collision_func = Object_Collision; obj->save_flags = true; obj->save_anim = true; obj->save_position = true; } REGISTER_OBJECT(O_GONDOLA, M_Setup) ================================================ FILE: src/trx/game/objects/traps/gondola.h ================================================ #pragma once typedef enum { GONDOLA_STATE_EMPTY = 0, GONDOLA_STATE_FLOATING = 1, GONDOLA_STATE_CRASH = 2, GONDOLA_STATE_SINK = 3, GONDOLA_STATE_LAND = 4, } GONDOLA_STATE; ================================================ FILE: src/trx/game/objects/traps/hook.c ================================================ #include #include #include #include #include #include #define M_DAMAGE 50 static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (!Item_IsTriggerActive(item) && Item_TestFrameEqual(item, -1)) { Item_SwitchToAnim(item, 0, 0); item->status = IS_INACTIVE; Item_RemoveActive(item_num); item->enable_interpolation = false; return; } item->enable_interpolation = true; if (item->touch_bits != 0) { const ITEM *const lara_item = Lara_GetItem(); Lara_TakeDamage(M_DAMAGE, true); Spawn_BloodBath( lara_item->pos.x, lara_item->pos.y - WALL_L / 2, lara_item->pos.z, lara_item->speed, lara_item->rot.y, lara_item->room_num, 3); } Item_Animate(item); } static void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage) { if (stage == SAVEGAME_STAGE_AFTER_LOAD) { item->enable_interpolation = item->status == IS_ACTIVE; } } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->handle_save_func = M_HandleSave; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_HOOK, M_Setup) ================================================ FILE: src/trx/game/objects/traps/icicle.c ================================================ #include #include #include #include #include #define M_DAMAGE 200 typedef enum { // clang-format off ICICLE_EMPTY = 0, ICICLE_BREAK = 1, ICICLE_FALL = 2, ICICLE_LAND = 3, // clang-format on } M_STATE; static void M_Reset(ITEM *const item) { item->mesh_bits = 0xFFFFFFFF; Trap_Reset(item); } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); switch (item->current_anim_state) { case ICICLE_BREAK: item->goal_anim_state = ICICLE_FALL; break; case ICICLE_FALL: if (!item->gravity) { item->gravity = true; item->fall_speed = 50; } if (item->touch_bits != 0) { Lara_TakeDamage(M_DAMAGE, true); } break; case ICICLE_LAND: item->gravity = false; break; } Item_Animate(item); if (item->status == IS_DEACTIVATED) { if (!Item_IsTriggerActive(item)) { M_Reset(item); } return; } int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); Item_UpdateRoom(item_num, room_num); item->floor = Room_GetHeight(sector, item->pos); if (item->current_anim_state == ICICLE_FALL && item->pos.y >= item->floor) { item->pos.y = item->floor; item->gravity = false; item->goal_anim_state = ICICLE_LAND; item->fall_speed = 0; item->mesh_bits = 0b00101011; Sound_Effect(SFX_ICICLE, &item->pos, SPM_NORMAL); } } static void M_Setup(OBJECT *const obj) { obj->initialise_func = Trap_Initialise; obj->control_func = M_Control; obj->collision_func = Object_Collision_Trap; obj->save_position = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_ICICLE, M_Setup) ================================================ FILE: src/trx/game/objects/traps/killer_statue.c ================================================ #include #include #include #include #define KILLER_STATUE_CUT_DAMAGE 20 #define KILLER_STATUE_TOUCH_BITS 0b10000000 // = 128 // clang-format on typedef enum { // clang-format off KILLER_STATUE_STATE_EMPTY = 0, KILLER_STATUE_STATE_STOP = 1, KILLER_STATUE_STATE_CUT = 2, // clang-format on } KILLER_STATUE_STATE; typedef enum { // clang-format off KILLER_STATUE_ANIM_RETURN = 0, KILLER_STATUE_ANIM_FINISHED = 1, KILLER_STATUE_ANIM_CUT = 2, KILLER_STATUE_ANIM_SET = 3, // clang-format on } KILLER_STATUE_ANIM; static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); const OBJECT *const obj = Object_Get(item->object_id); Item_SwitchToAnim(item, KILLER_STATUE_ANIM_SET, 0); item->current_anim_state = KILLER_STATUE_STATE_STOP; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (Item_IsTriggerActive(item) && item->current_anim_state == KILLER_STATUE_STATE_STOP) { item->goal_anim_state = KILLER_STATUE_STATE_CUT; } else { item->goal_anim_state = KILLER_STATUE_STATE_STOP; } if ((item->touch_bits & KILLER_STATUE_TOUCH_BITS) != 0 && item->current_anim_state == KILLER_STATUE_STATE_CUT) { Lara_TakeDamage(KILLER_STATUE_CUT_DAMAGE, true); const ITEM *const lara_item = Lara_GetItem(); Spawn_Blood( lara_item->pos.x + (Random_GetControl() - 0x4000) / 256, lara_item->pos.y - Random_GetControl() / 44, lara_item->pos.z + (Random_GetControl() - 0x4000) / 256, lara_item->speed, lara_item->rot.y + (Random_GetControl() - 0x4000) / 8, lara_item->room_num); } Item_Animate(item); } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Object_Collision_Trap; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_KILLER_STATUE, M_Setup) ================================================ FILE: src/trx/game/objects/traps/lava_wedge.c ================================================ #include #include #include #include #include #include #define M_SPEED 25 static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (!Item_IsTriggerActive(item)) { Trap_Reset(item); return; } int16_t room_num = item->room_num; Room_GetSector(item->pos, &room_num); Item_UpdateRoom(item_num, room_num); if (item->status != IS_DEACTIVATED) { XYZ_32 pos = item->pos; switch (item->rot.y) { case 0: item->pos.z += M_SPEED; pos.z += 2 * WALL_L; break; case -DEG_180: item->pos.z -= M_SPEED; pos.z -= 2 * WALL_L; break; case DEG_90: item->pos.x += M_SPEED; pos.x += 2 * WALL_L; break; default: item->pos.x -= M_SPEED; pos.x -= 2 * WALL_L; break; } const SECTOR *const sector = Room_GetSector(pos, &room_num); if (Room_GetHeight(sector, pos) != item->pos.y) { item->status = IS_DEACTIVATED; } } const LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->water_status == LWS_CHEAT) { item->touch_bits = 0; } if (item->touch_bits) { const ITEM *const lara_item = Lara_GetItem(); if (lara_item->hit_points > 0) { Lara_TouchLava(); } if (g_Config.debug.enable_invulnerability) { return; } g_Camera.item = item; g_Camera.flags = CF_CHASE_OBJECT; g_Camera.type = CAM_FIXED; g_Camera.target_angle = -DEG_180; g_Camera.target_distance = WALL_L * 3; } } static void M_Setup(OBJECT *const obj) { obj->initialise_func = Trap_Initialise; obj->control_func = M_Control; obj->collision_func = Object_Collision; obj->save_position = true; obj->save_anim = true; obj->save_flags = true; } REGISTER_OBJECT(O_LAVA_WEDGE, M_Setup) ================================================ FILE: src/trx/game/objects/traps/lightning_emitter.c ================================================ #include #include #include #include #include #include #include #include #define M_DAMAGE 400 #define M_STEPS 8 #define M_RND 64 #define M_SHOOTS 2 typedef struct { bool active; int32_t count; bool zapped; bool no_target; XYZ_32 target; int32_t start[M_SHOOTS]; XYZ_32 end[M_SHOOTS]; XYZ_32 main[M_STEPS]; XYZ_32 wibble[M_STEPS]; XYZ_32 shoot[M_SHOOTS][M_STEPS]; } M_PRIV; static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; if (Object_Get(item->object_id)->mesh_count > 1) { item->mesh_bits = 1; p->no_target = false; } else { p->no_target = true; } p->active = false; p->count = 1; p->zapped = false; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; if (!Item_IsTriggerActive(item)) { p->count = 1; p->active = false; p->zapped = false; if (Room_GetFlipStatus()) { Room_FlipMap(); } Item_RemoveActive(item_num); item->status = IS_INACTIVE; return; } p->count--; if (p->count > 0) { return; } if (p->active) { p->active = false; p->count = 35 + (Random_GetControl() * 45) / 0x8000; p->zapped = false; if (Room_GetFlipStatus()) { Room_FlipMap(); } } else { p->active = true; p->count = 20; for (int32_t i = 0; i < M_STEPS; i++) { p->wibble[i].x = 0; p->wibble[i].y = 0; p->wibble[i].z = 0; } const int32_t radius = p->no_target ? WALL_L : WALL_L * 5 / 2; if (Lara_IsNearItem(&item->pos, radius)) { const ITEM *const lara_item = Lara_GetItem(); p->target.x = lara_item->pos.x; p->target.y = lara_item->pos.y; p->target.z = lara_item->pos.z; Lara_TakeDamage(M_DAMAGE, true); p->zapped = true; } else if (p->no_target) { const SECTOR *const sector = Room_GetSector(item->pos, &item->room_num); const int32_t h = Room_GetHeight(sector, item->pos); p->target.x = item->pos.x; p->target.y = h; p->target.z = item->pos.z; p->zapped = false; } else { p->target.x = 0; p->target.y = 0; p->target.z = 0; Collide_GetJointAbsPosition( item, &p->target, 1 + (Random_GetControl() * 5) / 0x7FFF); p->zapped = false; } for (int32_t i = 0; i < M_SHOOTS; i++) { p->start[i] = Random_GetControl() * (M_STEPS - 1) / 0x7FFF; p->end[i].x = p->target.x + (Random_GetControl() * WALL_L) / 0x7FFF; p->end[i].y = p->target.y; p->end[i].z = p->target.z + (Random_GetControl() * WALL_L) / 0x7FFF; for (int32_t j = 0; j < M_STEPS; j++) { p->shoot[i][j].x = 0; p->shoot[i][j].y = 0; p->shoot[i][j].z = 0; } } if (!Room_GetFlipStatus()) { Room_FlipMap(); } } Sound_Effect(SFX_THUNDER, &item->pos, SPM_NORMAL); } static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { const ITEM *const item = Item_Get(item_num); const M_PRIV *const p = item->priv; if (!p->zapped) { return; } LARA_INFO *const lara = Lara_GetLaraInfo(); lara->hit_direction = 1 + (Random_GetControl() * 4) / (DEG_180 - 1); lara->hit_frame++; CLAMPG(lara->hit_frame, 34); } static void M_DrawBolts(const ITEM *const item) { ANIM_FRAME *frmptr[2]; int32_t rate; Item_GetFrames(item, frmptr, &rate); M_PRIV *const p = item->priv; if (!p->active) { return; } int32_t x1 = item->interp.result.pos.x + frmptr[0]->offset.x; int32_t y1 = item->interp.result.pos.y + frmptr[0]->offset.y; int32_t z1 = item->interp.result.pos.z + frmptr[0]->offset.z; int32_t x2 = p->target.x; int32_t y2 = p->target.y; int32_t z2 = p->target.z; int32_t dx = (x2 - x1) / M_STEPS; int32_t dy = (y2 - y1) / M_STEPS; int32_t dz = (z2 - z1) / M_STEPS; for (int32_t i = 0; i < M_STEPS; i++) { XYZ_32 *pos = &p->wibble[i]; if (Game_IsPlaying()) { pos->x += (Random_GetDraw() - 0x4000) * M_RND / 0x8000; pos->y += (Random_GetDraw() - 0x4000) * M_RND / 0x8000; pos->z += (Random_GetDraw() - 0x4000) * M_RND / 0x8000; } if (i == M_STEPS - 1) { pos->y = 0; } x2 = x1 + dx + pos->x; y2 = y1 + dy + pos->y; z2 = z1 + dz + pos->z; if (i > 0) { Output_DrawLightningSegment((LIGHTNING_SEGMENT) { .from = { x1, y1 + p->wibble[i - 1].y, z1 }, .to = { x2, y2, z2 }, .thickness = Viewport_GetWidth(VIEWPORT_GAME) / 6 }); } else { Output_DrawLightningSegment((LIGHTNING_SEGMENT) { .from = { x1, y1, z1 }, .to = { x2, y2, z2 }, .thickness = Viewport_GetWidth(VIEWPORT_GAME) / 6 }); } x1 = x2; y1 += dy; z1 = z2; p->main[i].x = x2; p->main[i].y = y2; p->main[i].z = z2; } for (int32_t i = 0; i < M_SHOOTS; i++) { int32_t j = p->start[i]; x1 = p->main[j].x; y1 = p->main[j].y; z1 = p->main[j].z; x2 = p->end[i].x; y2 = p->end[i].y; z2 = p->end[i].z; int32_t steps = M_STEPS - j; dx = (x2 - x1) / steps; dy = (y2 - y1) / steps; dz = (z2 - z1) / steps; for (int32_t k = 0; k < steps; k++) { XYZ_32 *pos = &p->shoot[i][k]; if (Game_IsPlaying()) { pos->x += (Random_GetDraw() - 0x4000) * M_RND / 0x8000; pos->y += (Random_GetDraw() - 0x4000) * M_RND / 0x8000; pos->z += (Random_GetDraw() - 0x4000) * M_RND / 0x8000; } if (k == steps - 1) { pos->y = 0; } x2 = x1 + dx + pos->x; y2 = y1 + dy + pos->y; z2 = z1 + dz + pos->z; if (k > 0) { Output_DrawLightningSegment((LIGHTNING_SEGMENT) { .from = { x1, y1 + p->shoot[i][k - 1].y, z1 }, .to = { x2, y2, z2 }, .thickness = Viewport_GetWidth(VIEWPORT_GAME) / 16 }); } else { Output_DrawLightningSegment((LIGHTNING_SEGMENT) { .from = { x1, y1, z1 }, .to = { x2, y2, z2 }, .thickness = Viewport_GetWidth(VIEWPORT_GAME) / 16 }); } x1 = x2; y1 += dy; z1 = z2; } } } static bool M_Draw(const ITEM *const item) { const OBJECT *const obj = Object_Get(O_LIGHTNING_EMITTER); ANIM_FRAME *frmptr[2]; int32_t rate; Item_GetFrames(item, frmptr, &rate); Matrix_Push(); Matrix_TranslateAbs32(item->interp.result.pos); Matrix_Rot16(item->interp.result.rot); const CLIP clip = Output_CheckBoundsClip(&frmptr[0]->bounds); if (clip == CLIP_NOT_VISIBLE) { Matrix_Pop(); return false; } Output_CalculateObjectLighting(item, &frmptr[0]->bounds); Matrix_TranslateRel16(frmptr[0]->offset); Object_DrawMesh(obj->mesh_idx, clip, false); Matrix_Pop(); M_DrawBolts(item); return true; } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->draw_func = M_Draw; obj->collision_func = M_Collision; obj->priv_size = sizeof(M_PRIV); obj->save_flags = true; } REGISTER_OBJECT(O_LIGHTNING_EMITTER, M_Setup) ================================================ FILE: src/trx/game/objects/traps/midas_touch.c ================================================ #include #include #include #include #include #include #include #include #define M_RANGE_H (STEP_L * 2) #define M_RANGE_V (STEP_L * 3) static const OBJECT_BOUNDS m_MidasTouch_Bounds = { .shift = { .min = { .x = -700, .y = +384 - 100, .z = -700, }, .max = { .x = +700, .y = +384 + 100 + 512, .z = +700, }, }, .rot = { .min = { .x = -10 * DEG_1, .y = -30 * DEG_1, .z = -10 * DEG_1, }, .max = { .x = +10 * DEG_1, .y = +30 * DEG_1, .z = +10 * DEG_1, }, }, }; static const OBJECT_BOUNDS *M_Bounds(void) { return &m_MidasTouch_Bounds; } static void M_KillLara(const ITEM *const item) { ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara = Lara_GetLaraInfo(); Lara_SwitchToExtraState(LS_EXTRA_MIDAS_KILL); lara_item->hit_points = -1; lara_item->gravity = false; lara->gun_type = LGT_UNARMED; lara->air = -1; Camera_InvokeCinematic(lara_item, 0, 0); } static bool M_IsUsable(const int16_t item_num) { const ITEM *const lara_item = Lara_GetItem(); return lara_item->current_anim_state != LS(LS_USE_MIDAS); } static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); const OBJECT *const obj = Object_Get(item->object_id); const DIRECTION quadrant = Math_GetDirection(lara_item->rot.y); switch (quadrant) { case DIR_NORTH: item->rot.y = 0; break; case DIR_EAST: item->rot.y = DEG_90; break; case DIR_SOUTH: item->rot.y = -DEG_180; break; case DIR_WEST: item->rot.y = -DEG_90; break; default: break; } if (!lara_item->gravity && lara_item->current_anim_state == LS(LS_STOP) && lara_item->pos.x > item->pos.x - M_RANGE_H && lara_item->pos.x < item->pos.x + M_RANGE_H && lara_item->pos.y > item->pos.y - M_RANGE_V && lara_item->pos.y < item->pos.y + M_RANGE_V && lara_item->pos.z > item->pos.z - M_RANGE_H && lara_item->pos.z < item->pos.z + M_RANGE_H) { M_KillLara(item); return; } LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->interact_target.is_moving && lara->interact_target.item_num == item_num) { Lara_SwitchToExtraState(LS_EXTRA_USE_MIDAS); lara->interact_target.is_moving = false; } if (!g_Input.action || lara->gun_status != LGS_ARMLESS || lara_item->gravity || lara_item->current_anim_state != LS(LS_STOP)) { return; } if (!Lara_TestPosition(item, obj->bounds_func())) { return; } if (!GF_ShowInventoryKeys(item->object_id)) { Lara_RefuseInteraction(); } } static void M_Setup(OBJECT *const obj) { obj->collision_func = M_Collision; obj->draw_func = nullptr; obj->bounds_func = M_Bounds; obj->is_usable_func = M_IsUsable; } REGISTER_OBJECT(O_MIDAS_TOUCH, M_Setup) ================================================ FILE: src/trx/game/objects/traps/mine.c ================================================ #include #include #include #include #include #include #include #include static bool m_DetonateAllMines = false; static int16_t M_GetBoatItem(const XYZ_32 *const pos, int16_t *const room_num) { Room_GetSector(*pos, room_num); int16_t item_num = Room_Get(*room_num)->item_num; while (item_num != NO_ITEM) { const ITEM *const item = Item_Get(item_num); if (item->object_id == O_BOAT) { const int32_t dx = item->pos.x - pos->x; const int32_t dz = item->pos.z - pos->z; // TODO: fix overflows and no y check if (SQUARE(dx) + SQUARE(dz) < SQUARE(WALL_L / 2)) { break; } } item_num = item->next_item; } return item_num; } static void M_DetonateAll( const ITEM *const mine_item, const int16_t boat_item_num, int16_t boat_room_num) { ITEM *const boat_item = Item_Get(boat_item_num); if (Lara_Vehicle_GetIndex() == boat_item_num) { ITEM *const lara_item = Lara_GetItem(); Item_Explode(Item_GetIndex(lara_item), -1, 0); lara_item->hit_points = 0; lara_item->flags |= IF_ONE_SHOT; } const OBJECT *const obj = Object_Get(O_BOAT_BITS); if (obj->loaded) { boat_item->object_id = O_BOAT_BITS; boat_item->mesh_bits = (1 << obj->mesh_count) - 1; Item_Explode(boat_item_num, -1, 0); } Item_Kill(boat_item_num); boat_item->object_id = O_BOAT; Room_TestTriggers(mine_item); m_DetonateAllMines = true; } static void M_Explode(ITEM *const mine_item) { const int16_t effect_num = Effect_Create(mine_item->room_num); if (effect_num != NO_EFFECT) { EFFECT *const effect = Effect_Get(effect_num); effect->object_id = O_EXPLOSION_1; effect->pos.x = mine_item->pos.x; effect->pos.y = mine_item->pos.y - WALL_L; effect->pos.z = mine_item->pos.z; effect->speed = 0; effect->frame_num = 0; effect->counter = 0; } Spawn_Splash(mine_item); Sound_Effect(SFX_EXPLOSION_1, &mine_item->pos, SPM_NORMAL); mine_item->flags |= IF_ONE_SHOT; mine_item->mesh_bits = 1; mine_item->collidable = false; } static void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage) { if (stage == SAVEGAME_STAGE_AFTER_LOAD) { if (item->flags & IF_ONE_SHOT) { item->mesh_bits = 1; } } } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (item->flags & IF_ONE_SHOT) { return; } if (!m_DetonateAllMines) { int16_t boat_room_num = item->room_num; XYZ_32 test_pos = { .x = item->pos.x, .y = item->pos.y - WALL_L * 2, .z = item->pos.z, }; const int16_t boat_item_num = M_GetBoatItem(&test_pos, &boat_room_num); if (boat_item_num == NO_ITEM) { return; } M_DetonateAll(item, boat_item_num, boat_room_num); } else if (Random_GetControl() < 0x7800) { return; } M_Explode(item); } static void M_Setup(OBJECT *const obj) { obj->handle_save_func = M_HandleSave; obj->control_func = M_Control; obj->collision_func = Object_Collision; obj->save_flags = true; obj->enable_interpolation = false; m_DetonateAllMines = false; } REGISTER_OBJECT(O_MINE, M_Setup) ================================================ FILE: src/trx/game/objects/traps/movable_block.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define LF_PPREADY 19 typedef enum { MOVABLE_BLOCK_STATE_STILL = 1, MOVABLE_BLOCK_STATE_PUSH = 2, MOVABLE_BLOCK_STATE_PULL = 3, } MOVABLE_BLOCK_STATE; typedef struct { uint16_t gravity_frames; bool is_push_pull; bool is_forced_moving; int16_t extra_rotations[3]; int16_t original_rot; int16_t interaction_rot; GAME_VECTOR initial; GAME_VECTOR linked; } M_PRIV; static const OBJECT_BOUNDS m_MovableBlock_Bounds = { .shift = { .min = { .x = -300, .y = 0, .z = -WALL_L / 2 - (LARA_RADIUS + 80), }, .max = { .x = +300, .y = 0, .z = -WALL_L / 2, }, }, .rot = { .min = { .x = -10 * DEG_1, .y = -30 * DEG_1, .z = -10 * DEG_1, }, .max = { .x = +10 * DEG_1, .y = +30 * DEG_1, .z = +10 * DEG_1, }, }, }; // Collect a stack of blocks. static void M_GetStack( VECTOR *stack, XYZ_32 stack_pos, int32_t stack_height, int32_t step_y, int16_t room_num); // Restores blocks' original texturing in case they have unique textures on each // side. The game rotates the block in order to align the block with Lara when // she tries to push or pull. static void M_UpdateRotation(ITEM *const item, const int16_t rot_y) { item->rot.y = rot_y; M_PRIV *const p = item->priv; // All 3 indices are potentially used in other parts of the code that can // cast item->extra_rotations to structs such as XYZ_16. This is similar to // things such as the compass needle that apply extra rotation. p->extra_rotations[0] = p->original_rot - rot_y; } // Indicates if Lara is currently pushing or pulling a block. static void M_SetPushPull(ITEM *const item, const bool enable) { M_PRIV *const p = item->priv; p->is_push_pull = enable; } static bool M_IsPushPull(const ITEM *const item) { const M_PRIV *const p = item->priv; return p != nullptr && p->is_push_pull; } // Indicates if blocks are being forcefully moved by other objects such as // lifts. static void M_SetForcedMoving(ITEM *const item, const bool enable) { M_PRIV *const p = item->priv; p->is_forced_moving = enable; } static bool M_IsForcedMoving(const ITEM *const item) { const M_PRIV *const p = item->priv; return p != nullptr && p->is_forced_moving; } // If a stack of multiple blocks need to drop, each subsequently stacked block // is delayed by incrementing frames so that higher blocks don't "land" on lower // blocks and stop moving. static void M_SetGravityFrames(ITEM *const item, const uint8_t frames) { M_PRIV *const p = item->priv; p->gravity_frames = frames; } static uint16_t M_GetGravityFrames(const ITEM *const item) { const M_PRIV *const p = item->priv; return p != nullptr ? p->gravity_frames : 0; } // Handles the block's initial position and room number for walkables. static void M_SetInitial(ITEM *const item) { M_PRIV *const p = item->priv; p->initial.pos = item->pos; p->initial.room_num = item->room_num; } static GAME_VECTOR M_GetInitial(const ITEM *const item) { const M_PRIV *const p = item->priv; return p->initial; } // Handles the block's linked position and room number for walkables. static void M_SetLinked(ITEM *const item) { M_PRIV *const p = item->priv; p->linked.pos = item->pos; p->linked.room_num = item->room_num; } static GAME_VECTOR M_GetLinked(const ITEM *const item) { const M_PRIV *const p = item->priv; return p->linked; } static void M_UpdateStoppers(const ITEM *const item, const bool enabled) { const M_PRIV *const p = item->priv; int16_t dir = p->interaction_rot; if (!enabled) { dir += DEG_180; } const ROOM *room = Room_Get(item->room_num); SECTOR *sector = Room_GetWorldSector(room, item->pos.x, item->pos.z); sector->stopper = enabled; const XYZ_32 pos = XYZ_32_OffsetYaw(item->pos, dir, WALL_L); int16_t room_num = item->room_num; Room_GetSector(pos, &room_num); room = Room_Get(room_num); sector = Room_GetWorldSector(room, pos.x, pos.z); sector->stopper = enabled; } static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "gravity_frames", &p->gravity_frames)); JSON_SHOULD(JSON_READ(io, "is_push_pull", &p->is_push_pull)); JSON_SHOULD(JSON_READ(io, "is_forced_moving", &p->is_forced_moving)); if (JSON_SHOULD(JSON_PUSH(io, "linked"))) { JSON_SHOULD(JSON_READ(io, "x", &p->linked.pos.x)); JSON_SHOULD(JSON_READ(io, "y", &p->linked.pos.y)); JSON_SHOULD(JSON_READ(io, "z", &p->linked.pos.z)); JSON_SHOULD(JSON_POP(io)); } JSON_SHOULD(JSON_READ(io, "counter_rot_0", &p->extra_rotations[0])); JSON_SHOULD(JSON_READ(io, "counter_rot_1", &p->extra_rotations[1])); JSON_SHOULD(JSON_READ(io, "counter_rot_2", &p->extra_rotations[2])); JSON_SHOULD(JSON_READ(io, "original_rot", &p->original_rot)); JSON_SHOULD(JSON_READ(io, "interaction_rot", &p->interaction_rot)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "gravity_frames", p->gravity_frames); JSONW_WRITE(io, "is_push_pull", p->is_push_pull); JSONW_WRITE(io, "is_forced_moving", p->is_forced_moving); JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "x", p->linked.pos.x); JSONW_WRITE(io, "y", p->linked.pos.y); JSONW_WRITE(io, "z", p->linked.pos.z); JSONW_POP_AND_SET(io, "linked"); JSONW_WRITE(io, "counter_rot_0", p->extra_rotations[0]); JSONW_WRITE(io, "counter_rot_1", p->extra_rotations[1]); JSONW_WRITE(io, "counter_rot_2", p->extra_rotations[2]); JSONW_WRITE(io, "original_rot", p->original_rot); JSONW_WRITE(io, "interaction_rot", p->interaction_rot); } static bool M_TestCurrentSector( const ITEM *const item, const int32_t block_height) { int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); // Check if there is a hard wall above. if (Room_GetHeight(sector, item->pos) == NO_HEIGHT) { return true; } // Make sure there is nothing on top of the block. if (Room_GetHeight( sector, (XYZ_32) { item->pos.x, item->pos.y - block_height, item->pos.z }) != item->pos.y - block_height) { return false; } return true; } static bool M_TestPush( const ITEM *const item, const int32_t block_height, const DIRECTION quadrant) { if (!M_TestCurrentSector(item, block_height)) { return false; } XYZ_32 base_pos = item->pos; int16_t room_num = item->room_num; switch (quadrant) { case DIR_NORTH: base_pos.z += WALL_L; break; case DIR_EAST: base_pos.x += WALL_L; break; case DIR_SOUTH: base_pos.z -= WALL_L; break; case DIR_WEST: base_pos.x -= WALL_L; break; default: break; } const SECTOR *sector = Room_GetSector(base_pos, &room_num); COLL_INFO coll = { .quadrant = quadrant, .radius = 500, }; if (Collide_CollideStaticObjects( &coll, base_pos.x, base_pos.y, base_pos.z, room_num, 1000)) { return false; } if (Room_GetHeight(sector, base_pos) != base_pos.y) { return false; } if (sector->stopper) { return false; } const XYZ_32 sample_pos = { base_pos.x, base_pos.y - block_height, base_pos.z }; sector = Room_GetSector(sample_pos, &room_num); if (Room_GetCeiling(sector, sample_pos) > base_pos.y - block_height) { return false; } return true; } static bool M_TestPull( const ITEM *const item, const int32_t block_height, const DIRECTION quadrant) { if (!M_TestCurrentSector(item, block_height)) { return false; } int32_t x_add = 0; int32_t z_add = 0; switch (quadrant) { case DIR_NORTH: z_add = -WALL_L; break; case DIR_EAST: x_add = -WALL_L; break; case DIR_SOUTH: z_add = WALL_L; break; case DIR_WEST: x_add = WALL_L; break; default: break; } // Test block destination sector. XYZ_32 base_pos = { .x = item->pos.x + x_add, .y = item->pos.y, .z = item->pos.z + z_add, }; int16_t room_num = item->room_num; const SECTOR *sector = Room_GetSector(base_pos, &room_num); COLL_INFO coll = { .quadrant = quadrant, .radius = 500, }; if (Collide_CollideStaticObjects( &coll, base_pos.x, base_pos.y, base_pos.z, room_num, 1000)) { return false; } if (Room_GetHeight(sector, base_pos) != base_pos.y) { return false; } if (sector->stopper) { return false; } XYZ_32 sample_pos = { base_pos.x, base_pos.y - block_height, base_pos.z }; sector = Room_GetSector(sample_pos, &room_num); if (Room_GetCeiling(sector, sample_pos) > base_pos.y - block_height) { return false; } // Test Lara destination sector. base_pos.x += x_add; base_pos.z += z_add; room_num = item->room_num; sector = Room_GetSector(base_pos, &room_num); if (Room_GetHeight(sector, base_pos) != base_pos.y) { return false; } sample_pos = (XYZ_32) { base_pos.x, base_pos.y - LARA_HEIGHT, base_pos.z }; sector = Room_GetSector(sample_pos, &room_num); if (Room_GetCeiling(sector, sample_pos) > base_pos.y - LARA_HEIGHT) { return false; } const ITEM *const lara_item = Lara_GetItem(); base_pos.x = lara_item->pos.x + x_add; base_pos.y = lara_item->pos.y; base_pos.z = lara_item->pos.z + z_add; room_num = lara_item->room_num; sector = Room_GetSector(base_pos, &room_num); coll.radius = LARA_RADIUS; coll.quadrant = (quadrant + 2) & 3; if (Collide_CollideStaticObjects( &coll, base_pos.x, base_pos.y, base_pos.z, room_num, LARA_HEIGHT)) { return false; } return true; } static bool M_TestDoor(ITEM *lara_item, COLL_INFO *coll) { // OG fix: stop pushing blocks through doors const int32_t shift = 8; // constant shift to avoid overflow errors const int32_t max_dist = SQUARE((WALL_L * 2) >> shift); for (int item_num = 0; item_num < Item_GetLevelCount(); item_num++) { ITEM *const item = Item_Get(item_num); if (!Object_IsType(item->object_id, g_DoorObjects)) { continue; } const int32_t dx = (item->pos.x - lara_item->pos.x) >> shift; const int32_t dy = (item->pos.y - lara_item->pos.y) >> shift; const int32_t dz = (item->pos.z - lara_item->pos.z) >> shift; const int32_t dist = SQUARE(dx) + SQUARE(dy) + SQUARE(dz); if (dist > max_dist) { continue; } if (Lara_TestBoundsCollide(item, coll->radius) && Collide_TestCollision(item, lara_item)) { return true; } } return false; } static bool M_TestSolidPortal(const ITEM *const item) { const ITEM *const lara_item = Lara_GetItem(); int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(lara_item->pos, &room_num); const int32_t height = Room_GetHeightEx(sector, lara_item->pos, true, NO_ITEM); return height == NO_HEIGHT; } static bool M_TestDeathCollision(const ITEM *const item, const ITEM *const lara) { return g_Config.gameplay.enable_killer_pushblocks && !g_Config.debug.enable_invulnerability && item->gravity && Lara_TestBoundsCollide(item, 0); } static bool M_IsItemOnTop( const ITEM *const item, const int32_t x, const int32_t z) { const int32_t dx = x - item->pos.x; const int32_t dz = z - item->pos.z; // Movable blocks' bounds don't match sector so estimate. return (dx >= -WALL_L / 2 && dx < WALL_L / 2) && (dz >= -WALL_L / 2 && dz < WALL_L / 2); } static bool M_TestEmbedCollision(const ITEM *const item, const ITEM *const lara) { return M_IsItemOnTop(item, lara->pos.x, lara->pos.z) && lara->pos.y <= item->pos.y && lara->pos.y > item->pos.y - WALL_L && !item->gravity && !lara->gravity && item->current_anim_state == MOVABLE_BLOCK_STATE_STILL && lara->current_anim_state != LS(LS_PULL_BLOCK) && lara->current_anim_state != LS(LS_PUSH_BLOCK); } static void M_KillLara(const ITEM *const item, ITEM *const lara) { if (lara->hit_points <= 0) { return; } lara->hit_points = -1; lara->pos.y = lara->floor; lara->speed = 0; lara->fall_speed = 0; lara->gravity = false; lara->rot.x = 0; lara->rot.z = 0; lara->enable_shadow = false; lara->current_anim_state = LS(LS_SPECIAL); lara->goal_anim_state = LS(LS_SPECIAL); Item_SwitchToAnim(lara, LA(LA_BOULDER_DEATH), 0); for (int32_t i = 0; i < 15; i++) { const int32_t x = lara->pos.x + (Random_GetControl() - 0x4000) / 256; const int32_t z = lara->pos.z + (Random_GetControl() - 0x4000) / 256; const int32_t y = lara->pos.y - Random_GetControl() / 64; const int32_t d = lara->rot.y + (Random_GetControl() - 0x4000) / 8; Spawn_Blood(x, y, z, item->speed * 2, d, lara->room_num); } if (!Object_Get(O_CAMERA_TARGET)->loaded) { return; } const int16_t target_num = Item_Spawn(lara, O_CAMERA_TARGET); if (target_num != NO_ITEM) { ITEM *const target = Item_Get(target_num); target->rot.y = g_Camera.target_angle; target->pos.y = lara->floor - WALL_L; Lara_SetDeathCameraTarget(target_num); } } static bool M_IsAgainstFloor(const ITEM *const item) { int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); return !sector->floor.is_split && sector->floor.tilt.x == 0 && sector->floor.tilt.z == 0 && sector->floor.height == item->pos.y; } static bool M_IsAgainstCeiling(const ITEM *const item) { int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); const SECTOR *const sky_sector = Room_GetSkySector(sector, item->pos.x, item->pos.z); return !sector->ceiling.is_split && sky_sector->ceiling.tilt.x == 0 && sky_sector->ceiling.tilt.z == 0 && sky_sector->ceiling.height == item->pos.y - WALL_L; } static const OBJECT_BOUNDS *M_Bounds(void) { return &m_MovableBlock_Bounds; } static bool M_Draw(const ITEM *const item) { if (item->status == IS_ACTIVE) { return Object_DrawUnclippedItem(item); } else { return Object_DrawAnimatingItem(item); } } static void M_Initialise(const int16_t item_num) { // Ensure the block is snapped to the grid, otherwise the snapping occurs // during collision tests and can appear jarring. Additional angles are // stored to preserve item appearance in spite of control angle changes. ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; item->extra_rotations = p->extra_rotations; p->original_rot = (((item->rot.y + DEG_180) / DEG_90) * DEG_90) - DEG_180; M_UpdateRotation(item, p->original_rot); M_SetGravityFrames(item, 0); M_SetPushPull(item, false); M_SetForcedMoving(item, false); M_SetInitial(item); M_SetLinked(item); MovableBlock_UpdateBox(item, true); Walkable_AllocateNodes(item, 1); } static void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage) { if (stage == SAVEGAME_STAGE_BEFORE_LOAD) { MovableBlock_UpdateBox(item, false); } else if (stage == SAVEGAME_STAGE_AFTER_LOAD) { const OBJECT *const obj = Object_Get(item->object_id); if (item->anim_num < obj->anim_idx || item->anim_num >= obj->anim_idx + obj->anim_count) { // #4735 - resolve save issues caused by injections shifting anim // numbers. Remove after some time. Item_SwitchToAnim(item, 0, 0); } const int16_t item_num = Item_GetIndex(item); if (item->flags & IF_KILLED) { Walkable_Remove(item_num); return; } if (item->status == IS_ACTIVE && !item->gravity && !M_IsForcedMoving(item) && item->current_anim_state == MOVABLE_BLOCK_STATE_STILL) { Item_RemoveActive(Item_GetIndex(item)); item->status = IS_INACTIVE; } // Reposition walkable to its linked sector. Walkable_Reposition(item_num, M_GetInitial(item), M_GetLinked(item)); MovableBlock_UpdateBox(item, true); } } static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); const OBJECT *const obj = Object_Get(item->object_id); if (item->status == IS_INVISIBLE) { return; } if (M_TestDeathCollision(item, lara_item)) { M_KillLara(item, lara_item); return; } if (M_TestEmbedCollision(item, lara_item)) { lara_item->pos.y = item->pos.y - WALL_L; } if (item->current_anim_state == MOVABLE_BLOCK_STATE_STILL) { M_SetPushPull(item, false); } if (!g_Input.action || item->status == IS_ACTIVE || lara_item->gravity || lara_item->pos.y != item->pos.y || M_IsForcedMoving(item)) { return; } LARA_INFO *const lara = Lara_GetLaraInfo(); const DIRECTION quadrant = Math_GetDirection(lara_item->rot.y); if (lara_item->current_anim_state == LS(LS_STOP)) { if (g_Input.forward || g_Input.back || lara->gun_status != LGS_ARMLESS) { return; } switch (quadrant) { case DIR_NORTH: M_UpdateRotation(item, 0); break; case DIR_EAST: M_UpdateRotation(item, DEG_90); break; case DIR_SOUTH: M_UpdateRotation(item, -DEG_180); break; case DIR_WEST: M_UpdateRotation(item, -DEG_90); break; default: break; } if (!Lara_TestPosition(item, obj->bounds_func())) { return; } // OG fix: stop pushing blocks through doors if (M_TestDoor(lara_item, coll)) { return; } // Prevent Lara moving a block through a non-passable portal if (M_TestSolidPortal(item)) { return; } switch (quadrant) { case DIR_NORTH: lara_item->pos.z = ROUND_TO_SECTOR(lara_item->pos.z); lara_item->pos.z += WALL_L - LARA_RADIUS; break; case DIR_SOUTH: lara_item->pos.z = ROUND_TO_SECTOR(lara_item->pos.z); lara_item->pos.z += LARA_RADIUS; break; case DIR_EAST: lara_item->pos.x = ROUND_TO_SECTOR(lara_item->pos.x); lara_item->pos.x += WALL_L - LARA_RADIUS; break; case DIR_WEST: lara_item->pos.x = ROUND_TO_SECTOR(lara_item->pos.x); lara_item->pos.x += LARA_RADIUS; break; default: break; } lara_item->rot.y = item->rot.y; lara_item->goal_anim_state = LS(LS_PP_READY); Lara_Animate(lara_item); if (lara_item->current_anim_state == LS(LS_PP_READY)) { lara->gun_status = LGS_HANDS_BUSY; } } else if (Item_TestAnimEqual(lara_item, LA(LA_PUSHABLE_GRAB))) { if (!Item_TestFrameEqual(lara_item, LF_PPREADY)) { return; } if (!Lara_TestPosition(item, obj->bounds_func())) { return; } M_PRIV *const p = item->priv; if (g_Input.forward) { if (!M_TestPush(item, WALL_L, quadrant)) { return; } p->interaction_rot = lara_item->rot.y; item->goal_anim_state = MOVABLE_BLOCK_STATE_PUSH; lara_item->goal_anim_state = LS(LS_PUSH_BLOCK); } else if (g_Input.back) { if (!M_TestPull(item, WALL_L, quadrant)) { return; } p->interaction_rot = lara_item->rot.y + DEG_180; item->goal_anim_state = MOVABLE_BLOCK_STATE_PULL; lara_item->goal_anim_state = LS(LS_PULL_BLOCK); } else { return; } M_SetLinked(item); item->status = IS_ACTIVE; Item_AddActive(item_num); M_UpdateStoppers(item, true); MovableBlock_UpdateBox(item, false); Item_Animate(item); Lara_Animate(lara_item); M_SetPushPull(item, true); } } static void M_ResetPosition(ITEM *const item) { const int16_t item_num = Item_GetIndex(item); const GAME_VECTOR linked_pos = M_GetLinked(item); const GAME_VECTOR initial_pos = M_GetInitial(item); MovableBlock_UpdateBox(item, false); item->pos = initial_pos.pos; Item_UpdateRoom(item_num, initial_pos.room_num); Walkable_Reposition(item_num, linked_pos, initial_pos); M_SetLinked(item); MovableBlock_UpdateBox(item, true); Item_RemoveActive(item_num); item->timer = -1; item->status = IS_INACTIVE; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (item->status == IS_INVISIBLE) { return; } if (item->timer > 0 && !M_IsPushPull(item) && !M_IsForcedMoving(item)) { M_ResetPosition(item); return; } if (M_GetGravityFrames(item) > 0) { M_SetGravityFrames(item, M_GetGravityFrames(item) - 1); return; } if ((item->flags & IF_ONE_SHOT) != 0) { Item_Kill(item_num); Walkable_Remove(item_num); MovableBlock_UpdateBox(item, false); return; } Item_Animate(item); // Check if the block is floating, on a walkable, or on the pit floor. // ROUND_TO_HALF_CLICK because block can fall through floor to undefined // sector. int16_t room_num = item->room_num; XYZ_32 sample_pos = { item->pos.x, ROUND_TO_HALF_CLICK(item->pos.y), item->pos.z, }; const SECTOR *sector = Room_GetSector(sample_pos, &room_num); int32_t under_block_height = Room_GetHeightEx(sector, item->pos, true, item_num); bool update_room_num = true; // Check if tunneled into floor below. if (item->gravity && item->fall_speed > 0) { const int32_t y_prev = item->pos.y - item->fall_speed; // Query floor at previous y position. sample_pos.y = y_prev; const SECTOR *prev_sector = Room_GetSector(sample_pos, &room_num); int32_t prev_height = Room_GetHeightEx(prev_sector, sample_pos, true, item_num); // If on a walkable at the previous y position, use the rounded previous // y position as the floor. if (Room_IsOnWalkable( prev_sector, (XYZ_32) { item->pos.x, ROUND_TO_HALF_CLICK(y_prev), item->pos.z, }, ROUND_TO_HALF_CLICK(y_prev), item_num)) { prev_height = ROUND_TO_HALF_CLICK(y_prev); } // If tunneled into the floor, clamp to previous floor height. if (prev_height != NO_HEIGHT && y_prev < prev_height && item->pos.y >= prev_height) { under_block_height = prev_height; update_room_num = false; } } if (item->pos.y < under_block_height && !M_IsPushPull(item) && !M_IsForcedMoving(item)) { // Block is activated and floating in the air. item->gravity = true; } else if (item->gravity) { // Block hits the ground or another walkable. item->gravity = false; item->fall_speed = 0; item->pos.y = under_block_height; item->status = IS_DEACTIVATED; ItemAction_Run(ITEM_ACTION_FLOOR_SHAKE, item); Sound_Effect(SFX_PUSHBLOCK_LAND, &item->pos, SPM_NORMAL); } else if ( // If block is at/under floor height, no gravity, and isn't being // pushed/pulled anymore. Prevents blocks from getting stuck in // IS_INACTIVE if retriggered. item->pos.y >= under_block_height && !item->gravity && !M_IsPushPull(item) && !M_IsForcedMoving(item)) { item->status = IS_INACTIVE; Item_RemoveActive(item_num); } // Don't update room number if on a walkable because room number can fall // through to a pit room (e.g. trapdoors). if (update_room_num) { room_num = item->room_num; Room_GetSectorOnWalkable( (XYZ_32) { item->pos.x, item->pos.y - WALL_L, item->pos.z }, &room_num); Item_UpdateRoom(item_num, room_num); } if (item->status == IS_DEACTIVATED) { const GAME_VECTOR target = { .pos = item->pos, .room_num = item->room_num, }; Walkable_Reposition(item_num, M_GetLinked(item), target); M_SetLinked(item); item->status = IS_INACTIVE; Item_RemoveActive(item_num); M_UpdateStoppers(item, false); MovableBlock_UpdateBox(item, true); Room_TestTriggers(item); } } static int16_t M_GetFloorHeight( const ITEM *const item, const int32_t x, const int32_t y, const int32_t z, const int16_t height) { if (item->status == IS_INVISIBLE || item->gravity) { return height; } // TODO OG bug: camera and shadow behave like OG during push/pull. if (M_IsPushPull(item)) { return height; } if (!M_IsItemOnTop(item, x, z)) { return height; } if (M_IsAgainstFloor(item) && M_IsAgainstCeiling(item)) { return NO_HEIGHT; } // If partially embedded from below e.g. jumping up into an overhead block. if (y <= item->pos.y && y > item->pos.y - WALL_L && M_IsAgainstCeiling(item)) { const SECTOR *const sector = Room_GetWorldSector( Room_Get(item->room_num), item->pos.x, item->pos.z); if (item->pos.y < sector->floor.height) { // If partially embedded from below e.g. jumping up into an overhead // block. return height; } else if (M_IsAgainstFloor(item)) { // Clamped between floor and ceiling. Match up with similar case in // M_GetCeilingHeight to return same sentinel value; return item->pos.y - WALL_L; } } // If under the bottom of the block. if (y > item->pos.y) { return height; } // If the the top of the block is under the floor height. if (item->pos.y - WALL_L >= height) { return height; } return item->pos.y - WALL_L; } static int16_t M_GetCeilingHeight( const ITEM *const item, const int32_t x, const int32_t y, const int32_t z, const int16_t height) { if (item->status == IS_INVISIBLE || item->gravity) { return height; } // TODO OG bug: camera and shadow behave like OG during push/pull. if (M_IsPushPull(item)) { return height; } // Only care if we are inside the block footprint. if (!M_IsItemOnTop(item, x, z)) { return height; } if (M_IsAgainstFloor(item) && M_IsAgainstCeiling(item)) { return NO_HEIGHT; } if (y <= item->pos.y && y > item->pos.y - WALL_L && !item->gravity) { if (M_IsAgainstCeiling(item)) { // If clamped betwee floor and ceiling return same sentinel value as // M_GetFloorHeight. return M_IsAgainstFloor(item) ? item->pos.y - WALL_L : item->pos.y; } return height; } // If above the top of the block. if (y <= item->pos.y - WALL_L) { return height; } // If the the bottom of the block is above the ceiling height. if (item->pos.y <= height) { return height; } return item->pos.y; } static void M_AddWalkable(const int16_t item_num) { const ITEM *const item = Item_Get(item_num); Walkable_Add(item_num, item->pos); } static void M_GetStack( VECTOR *const stack, const XYZ_32 stack_pos, int32_t stack_height, const int32_t step_y, const int16_t room_num) { int16_t sector_room_num = room_num; SECTOR *sector = Room_GetSector(stack_pos, §or_room_num); sector = Room_GetPitSector(sector, stack_pos.x, stack_pos.z); for (WALKABLE *w = sector->walkable; w != nullptr; w = w->next) { const ITEM *item = Item_Get(w->item_num); if (!Object_IsType(item->object_id, g_MovableBlockObjects)) { continue; } if (w->pos.x == stack_pos.x && w->pos.y == stack_height && w->pos.z == stack_pos.z) { Vector_Add(stack, (void *)&w->item_num); stack_height += step_y; } } } static void M_Setup(OBJECT *const obj) { obj->bounds_func = M_Bounds; obj->draw_func = M_Draw; obj->initialise_func = M_Initialise; obj->handle_save_func = M_HandleSave; obj->collision_func = M_Collision; obj->control_func = M_Control; obj->floor_height_func = M_GetFloorHeight; obj->ceiling_height_func = M_GetCeilingHeight; obj->add_walkable_func = M_AddWalkable; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->base_rot.y = true; obj->save_anim = true; obj->save_flags = true; obj->save_position = true; } void MovableBlock_UpdateBox(const ITEM *const item, const bool blocked) { if (blocked && (item->status == IS_ACTIVE || item->status == IS_INVISIBLE || (item->flags & IF_KILLED) != 0)) { return; } int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); if (sector->floor.height == item->pos.y && sector->box != NO_BOX) { BOX_INFO *const box = Box_GetBox(sector->box); if (box != nullptr && (box->overlap_index & BOX_BLOCKABLE) != 0) { TOGGLE_BIT(box->overlap_index, BOX_BLOCKED, blocked); } } } void MovableBlock_DropStack(const XYZ_32 drop_pos, int16_t room_num) { VECTOR *stack = Vector_Create(sizeof(int16_t)); M_GetStack(stack, drop_pos, drop_pos.y, -WALL_L, room_num); for (int16_t i = stack->count - 1; i >= 0; i--) { const int16_t item_num = *(const int16_t *)Vector_Get(stack, i); ITEM *const item = Item_Get(item_num); M_SetGravityFrames(item, i); item->status = IS_ACTIVE; Item_AddActive(item_num); Item_Animate(item); } Vector_Free(stack); } void MovableBlock_ShiftStackY( int32_t stack_height, const XYZ_32 old_pos, const int32_t new_y, const int16_t room_num, const bool reposition) { VECTOR *stack = Vector_Create(sizeof(int16_t)); M_GetStack(stack, old_pos, stack_height, -WALL_L, room_num); for (int16_t i = 0; i < stack->count; i++) { const int16_t item_num = *(const int16_t *)Vector_Get(stack, i); ITEM *const item = Item_Get(item_num); item->status = IS_ACTIVE; M_SetForcedMoving(item, true); item->pos.y = new_y; int16_t sector_room_num = room_num; SECTOR *sector = Room_GetSector( (XYZ_32) { item->pos.x, item->pos.y - STEP_L, item->pos.z }, §or_room_num); Item_UpdateRoom(item_num, sector_room_num); if (reposition) { const GAME_VECTOR target = { .pos = item->pos, .room_num = item->room_num, }; Walkable_Reposition(item_num, M_GetLinked(item), target); M_SetLinked(item); item->status = IS_INACTIVE; M_SetForcedMoving(item, false); } } Vector_Free(stack); } void MovableBlock_SlideStack( int32_t stack_height, const XYZ_32 old_pos, const ITEM *const dest_item, const bool reposition) { VECTOR *stack = Vector_Create(sizeof(int16_t)); M_GetStack(stack, old_pos, stack_height, -WALL_L, dest_item->room_num); for (int16_t i = 0; i < stack->count; i++) { const int16_t item_num = *(const int16_t *)Vector_Get(stack, i); ITEM *const item = Item_Get(item_num); item->status = IS_ACTIVE; M_SetForcedMoving(item, true); item->pos.x = dest_item->pos.x; item->pos.z = dest_item->pos.z; int16_t sector_room_num = dest_item->room_num; Room_GetSector( (XYZ_32) { item->pos.x, item->pos.y - STEP_L, item->pos.z }, §or_room_num); Item_UpdateRoom(item_num, sector_room_num); if (reposition) { const GAME_VECTOR target = { .pos = item->pos, .room_num = item->room_num, }; Walkable_Reposition(item_num, M_GetLinked(item), target); M_SetLinked(item); item->status = IS_INACTIVE; M_SetForcedMoving(item, false); } } Vector_Free(stack); } REGISTER_OBJECT(O_MOVABLE_BLOCK_1, M_Setup) REGISTER_OBJECT(O_MOVABLE_BLOCK_2, M_Setup) REGISTER_OBJECT(O_MOVABLE_BLOCK_3, M_Setup) REGISTER_OBJECT(O_MOVABLE_BLOCK_4, M_Setup) ================================================ FILE: src/trx/game/objects/traps/movable_block.h ================================================ #pragma once #include #include // Block or unblock a block's box overlap index. void MovableBlock_UpdateBox(const ITEM *item, bool blocked); // Drop a stack of blocks. void MovableBlock_DropStack(XYZ_32 drop_pos, int16_t room_num); // Shift a stack of blocks up or down in the y direction. void MovableBlock_ShiftStackY( int32_t stack_height, XYZ_32 old_pos, int32_t new_y, int16_t room_num, bool reposition); // Shift a stack of blocks in the x or z direction.. void MovableBlock_SlideStack( int32_t stack_height, XYZ_32 old_sector, const ITEM *dest_item, bool reposition); ================================================ FILE: src/trx/game/objects/traps/pendulum.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include // clang-format off #define M_AXE_DAMAGE 100 #define M_PENDULUM_DAMAGE 50 #define M_MAX_FIRE_DIST (WALL_L * 16) // = 16384 #define M_FIRE_FALLOFF 11 // clang-format on typedef struct { bool initialised; bool on_fire; int16_t effect_num; } M_PRIV; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "initialised", &p->initialised)); JSON_SHOULD(JSON_READ(io, "on_fire", &p->on_fire)); if (g_Config.gameplay.enable_enhanced_saves) { JSON_SHOULD(JSON_READ(io, "fx_num", &p->effect_num)); } } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "initialised", p->initialised); JSONW_WRITE(io, "on_fire", p->on_fire); JSONW_WRITE(io, "fx_num", Effect_GetInOrderNum(p->effect_num)); } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; p->effect_num = NO_EFFECT; } static void M_InitialiseFire(ITEM *const pendulum_item) { M_PRIV *const p = pendulum_item->priv; const OBJECT_ID fire_obj_id = g_TRVersion < 3 ? O_FLAME_EMITTER : O_FLAME_EMITTER_BIG; const int16_t fire_item_idx = Item_FindTypeAtPos( pendulum_item->room_num, pendulum_item->pos, fire_obj_id); if (fire_item_idx != NO_ITEM) { ITEM *const fire_item = Item_Get(fire_item_idx); Item_Kill(fire_item_idx); fire_item->room_num = NO_ROOM; p->on_fire = true; } p->initialised = true; } static void M_TriggerFireSparks(const ITEM *const item) { const ITEM *const lara_item = Lara_GetItem(); const XZ_32 delta = { .x = lara_item->pos.x - item->pos.x, .z = lara_item->pos.z - item->pos.z, }; if (ABS(delta.x) > M_MAX_FIRE_DIST || ABS(delta.z) > M_MAX_FIRE_DIST) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = (Random_GetControl() & 0x1F) + 48; spark->src_color.g = spark->src_color.r >> 1; spark->src_color.b = 0; spark->dst_color.r = (Random_GetControl() & 0x3F) + 192; spark->dst_color.g = (Random_GetControl() & 0x3F) + 128; spark->dst_color.b = 32; spark->col_fade_speed = (Random_GetControl() & 3) + 12; spark->fade_to_black = 8; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->life = (Random_GetControl() & 7) + 28; spark->s_life = spark->life; spark->dynamic = -1; spark->pos.x = (Random_GetControl() & 0x1F) - 16; spark->pos.y = 0; spark->pos.z = (Random_GetControl() & 0x1F) - 16; spark->vel.x = (Random_GetControl() & 0x3F) - 32; spark->vel.y = -16 - (Random_GetControl() & 0xF); spark->vel.z = (Random_GetControl() & 0x3F) - 32; spark->friction = 4; spark->flags = SPARK_F_ATTACHED_NODE | SPARK_F_ALT_SPRITE | SPARK_F_ITEM | SPARK_F_SPRITE | SPARK_F_SCALE; if ((Random_GetControl() & 1) != 0) { spark->flags |= SPARK_F_ROTATE; spark->rot_angle = Random_GetControl() & 0xFFF; spark->rot_add = (Random_GetControl() & 0x1F) - 16; } spark->node_num = 3; spark->item_num = Item_GetIndex(item); spark->gravity = -16 - (Random_GetControl() & 0x1F); spark->max_y_vel = -16 - (Random_GetControl() & 7); spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->scalar = 3; spark->size.width = (Random_GetControl() & 7) + 32; spark->src_size.width = spark->size.width; spark->dst_size.width = spark->size.width >> 2; spark->size.height = spark->size.width; spark->src_size.height = spark->size.height; spark->dst_size.height = spark->size.height >> 2; Sparks_FinishSetup(spark); } static void M_TriggerFireLight(const ITEM *const item) { XYZ_32 pos = { 0, -STEP_L * 2, 0 }; Collide_GetJointAbsPosition(item, &pos, 5); const RGB_888 color = { .r = (Random_GetControl() & 0x3F) + 192, .g = (Random_GetControl() & 0x1F) + 96, .b = 0, }; Output_AddDynamicLightRGB(pos, M_FIRE_FALLOFF, color); } static void M_TriggerFireEffect(const ITEM *const item) { M_PRIV *const p = item->priv; EFFECT *effect = nullptr; if (p->effect_num == NO_EFFECT) { p->effect_num = Effect_Create(item->room_num); if (p->effect_num == NO_EFFECT) { return; } effect = Effect_Get(p->effect_num); effect->object_id = O_FLAME; effect->counter = 0; effect->frame_num = 0; } else { effect = Effect_Get(p->effect_num); } XYZ_32 pos = { -32, -STEP_L - 16, 0 }; Collide_GetJointAbsPosition(item, &pos, 5); effect->pos = pos; } static void M_KillFireEffect(ITEM *const item) { M_PRIV *const p = item->priv; if (p->effect_num == NO_EFFECT) { return; } Effect_Kill(p->effect_num); p->effect_num = NO_EFFECT; if (g_TRVersion == 1) { Sound_StopEffect(SFX_LOOP_FOR_SMALL_FIRES); } } static inline int16_t M_GetDamage(const OBJECT_ID obj_id) { return obj_id == O_SWINGING_AXE ? M_AXE_DAMAGE : M_PENDULUM_DAMAGE; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; item->enable_interpolation = true; if (!p->initialised) { M_InitialiseFire(item); } const OBJECT *const obj = Object_Get(item->object_id); const ANIM *const base_anim = Object_GetAnim(obj, 0); bool working; if (Anim_HasChange(base_anim, TRAP_WORKING)) { working = item->current_anim_state == TRAP_WORKING; if (Item_IsTriggerActive(item)) { if (item->current_anim_state == TRAP_SET) { item->goal_anim_state = TRAP_WORKING; } } else { if (item->current_anim_state == TRAP_WORKING) { item->goal_anim_state = TRAP_SET; } } } else { working = true; if (!Item_IsTriggerActive(item) && Item_TestFrameEqual(item, -1)) { Item_SwitchToAnim(item, 0, 0); item->status = IS_INACTIVE; Item_RemoveActive(item_num); item->enable_interpolation = false; M_KillFireEffect(item); return; } } if (working && item->touch_bits != 0) { const int16_t damage = M_GetDamage(item->object_id); Lara_TakeDamage(damage, true); if (p->on_fire) { Lara_CatchFire(); } else { const ITEM *const lara_item = Lara_GetItem(); const XYZ_32 pos = { .x = lara_item->pos.x + (Random_GetControl() - 0x4000) / 256, .z = lara_item->pos.z + (Random_GetControl() - 0x4000) / 256, .y = lara_item->pos.y - Random_GetControl() / 44, }; Spawn_Blood( pos.x, pos.y, pos.z, lara_item->speed, lara_item->rot.y + (Random_GetControl() - 0x4000) / 8, lara_item->room_num); } } const SECTOR *const sector = Room_GetSector(item->pos, &item->room_num); item->floor = Room_GetHeight(sector, item->pos); if (p->on_fire) { if (g_TRVersion >= 3) { M_TriggerFireSparks(item); M_TriggerFireLight(item); } else { M_TriggerFireEffect(item); } } Item_Animate(item); } static void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage) { if (stage == SAVEGAME_STAGE_AFTER_LOAD) { item->enable_interpolation = item->status == IS_ACTIVE; } } static void M_SetupCommon(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->handle_save_func = M_HandleSave; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->shadow_size = UNIT_SHADOW / 2; obj->save_flags = true; obj->save_anim = true; } static void M_SetupAxe(OBJECT *const obj) { M_SetupCommon(obj); obj->collision_func = Object_Collision_Trap; } static void M_SetupPendulum(OBJECT *const obj) { M_SetupCommon(obj); obj->collision_func = Object_Collision; } REGISTER_OBJECT(O_SWINGING_AXE, M_SetupAxe) REGISTER_OBJECT(O_PENDULUM_1, M_SetupPendulum) REGISTER_OBJECT(O_PENDULUM_2, M_SetupPendulum) ================================================ FILE: src/trx/game/objects/traps/power_saw.c ================================================ #include #include static void M_Setup(OBJECT *const obj) { obj->control_func = Propeller_Control; obj->collision_func = Object_Collision; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_POWER_SAW, M_Setup) ================================================ FILE: src/trx/game/objects/traps/propeller.c ================================================ #include #include #include #include #include #include #include #include #define M_DAMAGE 200 typedef enum { // clang-format off M_STATE_ON = 0, M_STATE_OFF = 1, // clang-format on } M_STATE; static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); if (item->status == IS_ACTIVE && item->object_id == O_PROPELLER_2 && item->current_anim_state == M_STATE_OFF) { Object_Collision(item_num, lara_item, coll); } else { Object_Collision_Trap(item_num, lara_item, coll); } } static void M_Setup(OBJECT *const obj) { obj->control_func = Propeller_Control; obj->collision_func = M_Collision; obj->save_flags = true; obj->save_anim = true; } static void M_SpawnBlood(const ITEM *const item, const int32_t count) { const ITEM *const lara_item = Lara_GetItem(); const XYZ_32 pos = lara_item->pos; Spawn_BloodBath( pos.x, pos.y - WALL_L / 2, pos.z, Random_GetDraw() >> 10, item->rot.y + DEG_90, lara_item->room_num, count); } void Propeller_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (Item_IsTriggerActive(item) && !(item->flags & IF_ONE_SHOT)) { item->goal_anim_state = M_STATE_ON; if ((item->touch_bits & 6) != 0) { Lara_TakeDamage(M_DAMAGE, true); if (g_TRVersion == 3 && GF_BadGetLevelNum() == 9) { // TODO: allow assigning trap damage via Lua Lara_GetItem()->hit_points = -1; M_SpawnBlood(item, 5); } M_SpawnBlood(item, 3); if (item->object_id == O_POWER_SAW) { Sound_Effect(SFX_SAW_STOP, &item->pos, SPM_NORMAL); } } else if (item->object_id == O_POWER_SAW) { Sound_Effect(SFX_SAW_REVVING, &item->pos, SPM_NORMAL); } else if (item->object_id == O_PROPELLER_1) { Sound_Effect(SFX_AIRPLANE_IDLE, &item->pos, SPM_NORMAL); } else if (item->object_id == O_PROPELLER_2) { Sound_Effect(SFX_UNDERWATER_FAN_ON, &item->pos, SPM_UNDERWATER); } else { Sound_Effect(SFX_SMALL_FAN_ON, &item->pos, SPM_NORMAL); } } else if (item->goal_anim_state != M_STATE_OFF) { if (item->object_id == O_PROPELLER_1) { Sound_Effect(SFX_AIRPLANE_IDLE, &item->pos, SPM_NORMAL); } else if (item->object_id == O_PROPELLER_2) { Sound_Effect(SFX_UNDERWATER_FAN_OFF, &item->pos, SPM_UNDERWATER); } item->goal_anim_state = M_STATE_OFF; } Item_Animate(item); if (item->status == IS_DEACTIVATED) { Item_RemoveActive(item_num); if (item->object_id != O_POWER_SAW) { item->collidable = false; } } } REGISTER_OBJECT(O_PROPELLER_1, M_Setup) REGISTER_OBJECT(O_PROPELLER_2, M_Setup) REGISTER_OBJECT(O_PROPELLER_3, M_Setup) ================================================ FILE: src/trx/game/objects/traps/propeller.h ================================================ #pragma once #include void Propeller_Control(int16_t item_num); ================================================ FILE: src/trx/game/objects/traps/raptor_emitter.c ================================================ #include #include #include #include #include #define M_MAX_SLOTS 3 typedef struct { int32_t cooldown; int16_t slots[M_MAX_SLOTS]; } M_PRIV; static void M_SpawnRaptor(const ITEM *const spawner_item, int32_t slot_idx) { M_PRIV *const p = spawner_item->priv; ITEM *const raptor_item = Item_Get(p->slots[slot_idx]); raptor_item->pos = spawner_item->pos; raptor_item->rot = spawner_item->rot; Item_SwitchToAnim(raptor_item, 0, 0); raptor_item->current_anim_state = Item_GetAnim(raptor_item)->current_anim_state; raptor_item->goal_anim_state = raptor_item->current_anim_state; raptor_item->required_anim_state = 0; raptor_item->flags &= ~(IF_INVISIBLE | IF_KILLED | 3); // 3? raptor_item->creature_data = nullptr; raptor_item->hit_points = Object_Get(raptor_item->object_id)->hit_points; raptor_item->mesh_bits = -1; raptor_item->status = IS_ACTIVE; raptor_item->collidable = true; if (raptor_item->active) { Item_RemoveActive(p->slots[slot_idx]); } Item_AddActive(p->slots[slot_idx]); Item_UpdateRoom(p->slots[slot_idx], NO_ITEM); Item_UpdateRoom(p->slots[slot_idx], spawner_item->room_num); LOT_EnableBaddieAI(p->slots[slot_idx], true); } static void M_PopulateSlots(M_PRIV *const p) { int32_t count = 0; for (int32_t i = 0; i < Item_GetLevelCount(); i++) { ITEM *const item = Item_Get(i); if (item->object_id != O_RAPTOR || !(item->ai_bits & AI_MODIFY)) { continue; } p->slots[count++] = i; if (count >= M_MAX_SLOTS) { break; } } } static int32_t M_GetEmptySlot(const M_PRIV *const p) { for (int32_t i = 0; i < M_MAX_SLOTS; i++) { const ITEM *const item = Item_Get(p->slots[i]); if (item->creature_data == nullptr) { return i; } } return -1; } static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "cooldown", &p->cooldown)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "cooldown", p->cooldown); } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; p->cooldown = 96 * (item_num & 3); for (int32_t i = 0; i < M_MAX_SLOTS; i++) { p->slots[i] = NO_ITEM; } } static void M_Control(int16_t item_num) { ITEM *const item = Item_Get(item_num); if (!item->active || item->timer <= 0) { return; } M_PRIV *const p = item->priv; if (p->slots[0] == NO_ITEM) { M_PopulateSlots(p); return; } int16_t m_EmptySlot = M_GetEmptySlot(p); if (m_EmptySlot == -1) { return; } const ITEM *const lara_item = Lara_GetItem(); const int32_t dist = XYZ_32_GetDistance(lara_item->pos, item->pos); if (dist < 4 * WALL_L) { return; } if (p->cooldown > 0) { p->cooldown--; return; } p->cooldown = 255; item->timer -= 30; M_SpawnRaptor(item, m_EmptySlot); } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->draw_func = nullptr; obj->save_flags = true; } REGISTER_OBJECT(O_RAPTOR_EMITTER, M_Setup) ================================================ FILE: src/trx/game/objects/traps/rolling_ball.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #define M_DAMAGE_AIR 100 #define M_SHAKE_RANGE (WALL_L * 10) // = 10240 #define M_CLEARANCE_UNIT (STEP_L * 3) // = 768 static void M_Roll(ITEM *const item) { item->gravity = false; item->fall_speed = 0; item->pos.y = item->floor; if (g_TRVersion > 1) { if (item->object_id == O_ROLLING_BALL_1) { Sound_Effect(SFX_ROLLING_BALL_1_ROLL, &item->pos, SPM_NORMAL); } else if (item->object_id == O_ROLLING_BALL_2) { Sound_Effect(SFX_ROLLING_BALL_2_ROLL, &item->pos, SPM_NORMAL); } else if (item->object_id == O_ROLLING_BALL_3) { Sound_Effect(SFX_ROLLING_BALL_3_ROLL, &item->pos, SPM_NORMAL); } else if (item->object_id == O_ROLLING_BALL_4) { Sound_Effect(SFX_ROLLING_BALL_4_ROLL, &item->pos, SPM_NORMAL); } } if (g_Config.gameplay.enable_boulder_shake) { XYZ_32 mic_pos = g_Camera.mic_pos.pos; mic_pos.y = item->pos.y; // Ignore vertical component const int32_t dist = XYZ_32_GetDistance(mic_pos, item->pos); if (dist < M_SHAKE_RANGE) { g_Camera.bounce = 40 * (dist - M_SHAKE_RANGE) / M_SHAKE_RANGE; } } } static bool M_TestStop(const ITEM *const item) { int32_t dist; switch (item->object_id) { case O_ROLLING_BALL_1: if (g_TRVersion == 1) { dist = WALL_L / 2; } else if (g_TRVersion == 2) { dist = STEP_L * 3 / 2; } else { dist = STEP_L * 5 / 4; } break; case O_ROLLING_BALL_4: dist = WALL_L * 17 / 16; break; default: dist = WALL_L; break; } int16_t room_num = item->room_num; XYZ_32 sample_pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, dist); const SECTOR *sector = Room_GetSector(sample_pos, &room_num); const int32_t height = Room_GetHeight(sector, sample_pos); if (height == NO_HEIGHT || height < item->pos.y) { // Stop at a wall or raised floor. return true; } const ANIM_FRAME *const frame = Item_GetBestFrame(item); const BOUNDS_16 *const bounds = &frame->bounds; int16_t item_height = ROUND_TO_CLICK_UP(ABS(bounds->max.y - bounds->min.y)); if (item_height > M_CLEARANCE_UNIT) { item_height = (item_height / M_CLEARANCE_UNIT) * M_CLEARANCE_UNIT; } sample_pos.y -= item_height; sector = Room_GetSector(sample_pos, &room_num); const int32_t ceiling = Room_GetCeiling(sector, sample_pos); if (ceiling == NO_HEIGHT || (ceiling > item->pos.y && !item->gravity)) { // Stop at a wall or if the ceiling in front is below the floor, as long // as the boulder is not falling. return true; } // Stop if the gap in front is too small to logically fit. return ABS(ceiling - height) < item_height; } static void M_Stop(ITEM *const item, const XYZ_32 old_pos) { if (item->object_id == O_ROLLING_BALL_1) { Sound_Effect(SFX_ROLLING_BALL_1_STOP, &item->pos, SPM_NORMAL); item->status = IS_DEACTIVATED; } else if (item->object_id == O_ROLLING_BALL_2) { Sound_Effect(SFX_ROLLING_BALL_2_STOP, &item->pos, SPM_NORMAL); item->goal_anim_state = TRAP_WORKING; } else if (item->object_id == O_ROLLING_BALL_3) { Sound_Effect(SFX_ROLLING_BALL_3_STOP, &item->pos, SPM_NORMAL); item->goal_anim_state = TRAP_WORKING; } else if (item->object_id == O_ROLLING_BALL_4) { Sound_Effect(SFX_ROLLING_BALL_4_STOP, &item->pos, SPM_NORMAL); item->status = IS_DEACTIVATED; } item->pos.x = old_pos.x; item->pos.y = item->floor; item->pos.z = old_pos.z; item->speed = 0; item->fall_speed = 0; item->touch_bits = 0; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); item->enable_interpolation = item->status == IS_ACTIVE; if (item->status == IS_DEACTIVATED && !Item_IsTriggerActive(item)) { Trap_Reset(item); return; } if (item->status != IS_ACTIVE) { int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); const int32_t height = Room_GetHeight(sector, item->pos); if (item->floor < height) { item->status = IS_ACTIVE; item->floor = height; } return; } if (item->goal_anim_state == TRAP_WORKING) { Item_Animate(item); return; } if (item->pos.y < item->floor) { if (!item->gravity) { item->gravity = true; item->fall_speed = -10; } } else if (item->current_anim_state == TRAP_SET) { item->goal_anim_state = TRAP_ACTIVATE; } const XYZ_32 old_pos = item->pos; Item_Animate(item); int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); Item_UpdateRoom(item_num, room_num); item->floor = Room_GetHeight(sector, item->pos); Room_TestTriggers(item); if (item->pos.y >= item->floor - STEP_L) { M_Roll(item); } if (M_TestStop(item)) { M_Stop(item, old_pos); } } static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); const LARA_INFO *const lara = Lara_GetLaraInfo(); if (item->status != IS_ACTIVE) { if (item->status != IS_INVISIBLE) { Object_Collision(item_num, lara_item, coll); } return; } if (!Lara_TestBoundsCollide(item, coll->radius)) { return; } if (!Collide_TestCollision(item, lara_item)) { return; } if (lara_item->gravity || g_Config.debug.enable_invulnerability) { if (coll->enable_baddie_push) { Lara_Col_ItemPush(item, coll, coll->enable_hit, true); } if (!g_Config.debug.enable_invulnerability) { lara_item->hit_points -= M_DAMAGE_AIR; } // TODO: handle overflows const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dy = (lara_item->pos.y - 350) - (item->pos.y - WALL_L / 2); const int32_t dz = lara_item->pos.z - item->pos.z; int32_t dist = Math_Sqrt(SQUARE(dx) + SQUARE(dy) + SQUARE(dz)); CLAMPL(dist, WALL_L / 2); Spawn_Blood( item->pos.x + (dx * WALL_L / 2) / dist, item->pos.y + (dy * WALL_L / 2) / dist - WALL_L / 2, item->pos.z + (dz * WALL_L / 2) / dist, item->speed, item->rot.y, item->room_num); } else { lara_item->hit_status = true; if (lara_item->hit_points > 0) { lara_item->hit_points = -1; Item_UpdateRoom(lara->item_num, item->room_num); lara_item->rot.x = 0; lara_item->rot.y = item->rot.y; lara_item->rot.z = 0; Item_SwitchToAnim(lara_item, LA(LA_BOULDER_DEATH), 0); lara_item->goal_anim_state = Item_GetAnim(lara_item)->current_anim_state; lara_item->current_anim_state = lara_item->goal_anim_state; g_Camera.flags = CF_FOLLOW_CENTRE; g_Camera.target_angle = 170 * DEG_1; g_Camera.target_elevation = -25 * DEG_1; for (int32_t i = 0; i < 15; i++) { Spawn_Blood( lara_item->pos.x + (Random_GetControl() - 0x4000) / 256, lara_item->pos.y - Random_GetControl() / 64, lara_item->pos.z + (Random_GetControl() - 0x4000) / 256, 2 * item->speed, item->rot.y + (Random_GetControl() - 0x4000) / 8, item->room_num); } } } } static void M_Setup(OBJECT *const obj) { obj->initialise_func = Trap_Initialise; obj->control_func = M_Control; obj->collision_func = M_Collision; obj->save_position = true; obj->save_flags = true; obj->save_anim = true; obj->load_floor = true; } REGISTER_OBJECT(O_ROLLING_BALL_1, M_Setup) REGISTER_OBJECT(O_ROLLING_BALL_2, M_Setup) REGISTER_OBJECT(O_ROLLING_BALL_3, M_Setup) REGISTER_OBJECT(O_ROLLING_BALL_4, M_Setup) ================================================ FILE: src/trx/game/objects/traps/rotating_laser.c ================================================ #include #include #include #include #include #include #include #include #include #include typedef struct { XYZ_32 origin; XYZ_32 target; int16_t direction; int16_t velocity; int16_t reverse_timer; } M_PRIV; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "origin", &p->origin)); JSON_SHOULD(JSON_READ(io, "target", &p->target)); JSON_SHOULD(JSON_READ(io, "direction", &p->direction)); JSON_SHOULD(JSON_READ(io, "velocity", &p->velocity)); JSON_SHOULD(JSON_READ(io, "reverse_timer", &p->reverse_timer)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "origin", p->origin); JSONW_WRITE(io, "target", p->target); JSONW_WRITE(io, "direction", p->direction); JSONW_WRITE(io, "velocity", p->velocity); JSONW_WRITE(io, "reverse_timer", p->reverse_timer); } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; p->origin = item->pos; p->target = XYZ_32_OffsetYaw(item->pos, item->rot.y, 2560); p->direction = 1; p->velocity = 0; p->reverse_timer = 0; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; if (!Item_IsTriggerActive(item)) { return; } item->current_anim_state = 0; Output_AddDynamicLightRGB( (XYZ_32) { item->pos.x, item->pos.y - 64, item->pos.z }, (Random_GetControl() & 1) + 8, (RGB_888) { (Random_GetControl() & 0x1F) + 192, Random_GetControl() & 0x1F, Random_GetControl() & 7, }); item->mesh_bits = -1 - (Random_GetControl() & 0x14); const int32_t dx = ABS(p->target.x - item->pos.x); const int32_t dz = ABS(p->target.z - item->pos.z); if (dx < 768 && dz < 768) { p->reverse_timer = 32; p->target = XYZ_32_OffsetYaw(p->origin, item->rot.y, -2560 * p->direction); } if (p->direction == 1) { if (p->reverse_timer != 0) { if (p->velocity != 0) { if (p->velocity > 4) { p->velocity -= p->velocity >> 2; } else { p->velocity = 0; } } else { p->reverse_timer--; if (p->reverse_timer == 1) { p->direction = -1; } } } else { p->velocity += 5; CLAMPG(p->velocity, 512); } } else if (p->reverse_timer != 0) { if (p->velocity != 0) { if (p->velocity < -4) { p->velocity -= p->velocity >> 2; } else { p->velocity = 0; } } else { p->reverse_timer--; if (p->reverse_timer == 1) { p->direction = -p->direction; } } } else { p->velocity -= 5; CLAMPL(p->velocity, -512); } item->pos.x += (p->velocity * Math_Sin(item->rot.y)) >> (W2V_SHIFT + 2); item->pos.z += (p->velocity * Math_Cos(item->rot.y)) >> (W2V_SHIFT + 2); int16_t room_num = item->room_num; Room_GetSector(item->pos, &room_num); if (room_num != item->room_num) { Item_UpdateRoom(item_num, room_num); } ITEM *const lara_item = Lara_GetItem(); if (Item_TestBoundsCollide(item, lara_item, 64)) { Lara_TakeDamage(25, false); Spawn_BloodBathD( lara_item->pos.x, item->pos.y - (Random_GetControl() & 0xFF) - 32, lara_item->pos.z, (Random_GetControl() & 0x7F) + 128, (int16_t)(Random_GetControl() << 1), lara_item->room_num, 3); } Item_Animate(item); } static void M_Setup(OBJECT *const obj) { obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->save_position = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_ROTATING_LASER, M_Setup) ================================================ FILE: src/trx/game/objects/traps/security_laser.c ================================================ #include #include #include #include #include #include #include #include #include #define M_DAMAGE 10 static uint8_t m_LaserShades[32] = {}; static const int16_t m_DefaultBeamCount = 1; static bool M_IsTargetable(const ITEM *const item) { return false; } static void M_LaserSplitterToggle(ITEM *const item) { int16_t room_num = item->room_num; SECTOR *sector = Room_GetSector(item->pos, &room_num); if ((Box_GetBox(sector->box)->overlap_index & BOX_BLOCKED_SEARCH) == 0) { return; } const bool is_active = Item_IsTriggerActive(item); if (is_active == ((Box_GetBox(sector->box)->overlap_index & BOX_BLOCKED) == BOX_BLOCKED)) { return; } XZ_32 step; switch (item->rot.y) { case 0: step = (XZ_32) { 0, -WALL_L }; break; case DEG_90: step = (XZ_32) { -WALL_L, 0 }; break; case -DEG_180: step = (XZ_32) { 0, WALL_L }; break; default: step = (XZ_32) { WALL_L, 0 }; break; } int32_t x = item->pos.x; int32_t z = item->pos.z; while (sector->box != NO_BOX && (Box_GetBox(sector->box)->overlap_index & BOX_BLOCKED_SEARCH) != 0) { if (is_active) { Box_GetBox(sector->box)->overlap_index |= BOX_BLOCKED; } else { Box_GetBox(sector->box)->overlap_index &= ~BOX_BLOCKED; } x += step.x; z += step.z; sector = Room_GetSector((XYZ_32) { x, item->pos.y, z }, &room_num); } } static void M_UpdateLaserShades(void) { for (int32_t shade_idx = 0; shade_idx < 32; shade_idx++) { uint8_t shade = m_LaserShades[shade_idx]; int32_t random_value = Random_GetDraw(); if (random_value < 1024) { random_value = (random_value & 0xF) + 16; } else if (random_value < 4096) { random_value &= 7; } else if ((random_value & 0x70) == 0) { random_value &= 3; } else { random_value = 0; } if (random_value != 0) { shade += (uint8_t)random_value; if (shade > 127) { shade = 127; } } else if (shade > 16) { shade -= shade >> 3; } else { shade = 16; } m_LaserShades[shade_idx] = shade; } } static RGB_888 M_GetLaserColor(const OBJECT_ID object_id) { switch (object_id) { case O_SECURITY_LASER_ALARM: return (RGB_888) { 0, UINT8_MAX, 0 }; case O_SECURITY_LASER_DEADLY: return (RGB_888) { UINT8_MAX, UINT8_MAX, 0 }; default: return (RGB_888) { UINT8_MAX, UINT8_MAX >> 2, 0 }; } } static XZ_32 M_GetLaserDirection(const ITEM *const item) { const int32_t edge_offset = WALL_L / 2 - 1; switch (item->rot.y) { case 0: return (XZ_32) { 0, edge_offset }; case DEG_90: return (XZ_32) { edge_offset, 0 }; case -DEG_180: return (XZ_32) { 0, -edge_offset }; default: return (XZ_32) { -edge_offset, 0 }; } } static bool M_GetLaserSegment( const ITEM *const item, const XZ_32 dir, const int32_t y, GAME_VECTOR *const s, GAME_VECTOR *const t) { s->x = item->pos.x + dir.x; s->y = item->pos.y + y; s->z = item->pos.z + dir.z; s->room_num = item->room_num; t->x = item->pos.x - (dir.x << 5); t->y = item->pos.y + y; t->z = item->pos.z - (dir.z << 5); LOS_Check(s, t, true); return LOS_CheckItemIntersectSegment(s, t, Lara_GetItem()); } static void M_DrawLaserBeam( const GAME_VECTOR *const src, const GAME_VECTOR *const dest, const RGB_888 color) { const XYZ_32 beam_delta = { .x = dest->x - src->x, .y = dest->y - src->y, .z = dest->z - src->z, }; int32_t segment_count = XYZ_32_GetDistance( (XYZ_32) { src->x, 0, src->z }, (XYZ_32) { dest->x, 0, dest->z }) >> 9; CLAMP(segment_count, 8, 32); XYZ_32 segment_start = src->pos; for (int32_t segment_idx = 0; segment_idx < segment_count; segment_idx++) { const float segment_end_ratio = (float)(segment_idx + 1) / segment_count; const XYZ_32 segment_end = { .x = src->x + (int32_t)(beam_delta.x * segment_end_ratio), .y = src->y + (int32_t)(beam_delta.y * segment_end_ratio), .z = src->z + (int32_t)(beam_delta.z * segment_end_ratio), }; RGBA_8888 from_color = COLOR_RGBA_8888_BLACK; RGBA_8888 to_color = COLOR_RGBA_8888_BLACK; if (segment_idx > 0) { const uint8_t shade = m_LaserShades[segment_idx]; from_color = (RGBA_8888) { .r = (uint8_t)((shade * color.r) / UINT8_MAX), .g = (uint8_t)((shade * color.g) / UINT8_MAX), .b = (uint8_t)((shade * color.b) / UINT8_MAX), .a = 0xFF, }; } if (segment_idx + 1 < segment_count) { const uint8_t shade = m_LaserShades[segment_idx + 1]; to_color = (RGBA_8888) { .r = (uint8_t)((shade * color.r) / UINT8_MAX), .g = (uint8_t)((shade * color.g) / UINT8_MAX), .b = (uint8_t)((shade * color.b) / UINT8_MAX), .a = 0xFF, }; } OutputSource_PolyFX_StageLineSegment( segment_start, from_color, segment_end, to_color, 4.0f, DRAW_BLEND_ADD); segment_start = segment_end; } } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_LaserSplitterToggle(item); } static void M_DamageLara(const ITEM *const item, const int32_t beam_y) { if (item->object_id == O_SECURITY_LASER_ALARM) { return; } const ITEM *const lara_item = Lara_GetItem(); if (item->object_id == O_SECURITY_LASER_KILLER) { Lara_TakeDamage(lara_item->hit_points, false); } else { Lara_TakeDamage(M_DAMAGE, false); } Spawn_BloodBath( lara_item->pos.x, item->pos.y + beam_y, lara_item->pos.z, (Random_GetDraw() & 0x7F) + 128, Random_GetDraw() << 1, lara_item->room_num, 1); } static void M_ActivateTriggers(const ITEM *const item) { for (int32_t i = 0; i < Item_GetLevelCount(); i++) { ITEM *const target_item = Item_Get(i); if ((target_item->object_id != O_STROBE_LIGHT && target_item->object_id != O_SENTRY_GUN) || !Item_IsTriggerActive(target_item)) { continue; } const OBJECT *const target_obj = Object_Get(target_item->object_id); if (target_obj->event_func != nullptr) { target_obj->event_func(target_item, OBJECT_EVENT_ALERT, nullptr); } } Room_TestTriggers(item); } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_LaserSplitterToggle(item); if (!Item_IsTriggerActiveRO(item)) { return; } M_UpdateLaserShades(); const XZ_32 direction = M_GetLaserDirection(item); int32_t beam_y = 0; bool tripped = false; for (int32_t beam_idx = 0; beam_idx < item->hit_points; beam_idx++) { GAME_VECTOR start; GAME_VECTOR target; if (M_GetLaserSegment(item, direction, beam_y, &start, &target)) { tripped = true; M_DamageLara(item, beam_y); } beam_y -= 256; } if (tripped) { M_ActivateTriggers(item); } } static bool M_Trigger(ITEM *const item, const TRIGGER *const trigger) { if (trigger != nullptr && trigger->type != TT_ANTIPAD && trigger->type != TT_ANTITRIGGER) { item->hit_points = trigger->timer & 7; if (item->hit_points == 0) { item->hit_points = 1; } item->max_hit_points = item->hit_points; item->timer = 0; } return true; } static bool M_DrawLaser(const ITEM *const item) { const RGB_888 color = M_GetLaserColor(item->object_id); if (!Item_IsTriggerActiveRO(item)) { return false; } const XZ_32 direction = M_GetLaserDirection(item); int32_t beam_y = 0; for (int32_t beam_idx = 0; beam_idx < item->hit_points; beam_idx++) { GAME_VECTOR start; GAME_VECTOR target; M_GetLaserSegment(item, direction, beam_y, &start, &target); M_DrawLaserBeam(&start, &target, color); beam_y -= 256; } return true; } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->draw_func = M_DrawLaser; obj->is_targetable_func = M_IsTargetable; obj->trigger_func = M_Trigger; obj->hit_points = m_DefaultBeamCount; obj->save_hitpoints = true; obj->save_flags = true; } REGISTER_OBJECT(O_SECURITY_LASER_ALARM, M_Setup) REGISTER_OBJECT(O_SECURITY_LASER_DEADLY, M_Setup) REGISTER_OBJECT(O_SECURITY_LASER_KILLER, M_Setup) ================================================ FILE: src/trx/game/objects/traps/sentry_gun.c ================================================ #include #include #include #include #include #include #include #include #include typedef enum { M_STATE_FIRE, M_STATE_STILL, } M_STATE; typedef enum { M_MUZZLE_LEFT, M_MUZZLE_RIGHT, } M_MUZZLE; typedef struct { int16_t active_muzzle; int16_t muzzle_flash_timer; bool is_alerted; bool has_fired; } M_PRIV; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_OPTIONAL(JSON_READ(io, "active_muzzle", &p->active_muzzle)); JSON_OPTIONAL(JSON_READ(io, "muzzle_flash_timer", &p->muzzle_flash_timer)); JSON_OPTIONAL(JSON_READ(io, "is_alerted", &p->is_alerted)); JSON_OPTIONAL(JSON_READ(io, "has_fired", &p->has_fired)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "active_muzzle", p->active_muzzle); JSONW_WRITE(io, "muzzle_flash_timer", p->muzzle_flash_timer); JSONW_WRITE(io, "is_alerted", p->is_alerted); JSONW_WRITE(io, "has_fired", p->has_fired); } static const CREATURE_GUN m_FireLeft = { .muzzle = { .pos = { .x = 110, .y = -30, .z = -530 }, .mesh_num = 2, }, .tr3_enemy_flash = true, .tr3_flash = { .pos = { .x = 110, .y = -30, .z = -530 }, .mesh_num = 2, }, .tr3_enemy_weapon_flags = 1, .tr3_flash_shade = 600, .tr3_flash_rot_x = -DEG_180, }; static const CREATURE_GUN m_FireRight = { .muzzle = { .pos = { .x = -110, .y = -30, .z = -530 }, .mesh_num = 2, }, .tr3_enemy_flash = true, .tr3_flash = { .pos = { .x = -110, .y = -30, .z = -530 }, .mesh_num = 2, }, .tr3_enemy_weapon_flags = 1, .tr3_flash_shade = 600, .tr3_flash_rot_x = -DEG_180, }; static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; Item_SwitchToAnim(item, 1, 0); item->current_anim_state = M_STATE_STILL; item->goal_anim_state = M_STATE_STILL; p->active_muzzle = M_MUZZLE_LEFT; p->muzzle_flash_timer = 0; p->is_alerted = false; p->has_fired = false; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; if (p->muzzle_flash_timer > 1) { p->muzzle_flash_timer--; XYZ_32 pos; Matrix_Push(); if ((Random_GetControl() & 1) != 0) { p->active_muzzle = M_MUZZLE_LEFT; pos = m_FireLeft.muzzle.pos; Collide_GetJointAbsPosition(item, &pos, m_FireLeft.muzzle.mesh_num); } else { p->active_muzzle = M_MUZZLE_RIGHT; pos = m_FireRight.muzzle.pos; Collide_GetJointAbsPosition( item, &pos, m_FireRight.muzzle.mesh_num); } const RGB_888 color = { 192, 128, 32 }; Output_AddDynamicLightRGB(pos, 2 * p->muzzle_flash_timer + 8, color); Matrix_Pop(); } if (!Creature_Activate(item_num)) { return; } CREATURE *const creature = item->creature_data; if (creature == nullptr) { return; } if (item->hit_status) { p->is_alerted = true; } if (item->hit_points <= 0) { Item_Explode(item_num, -1, 0); LOT_DisableBaddieAI(item_num); Item_Kill(item_num); item->flags |= IF_INVISIBLE; item->status = IS_DEACTIVATED; } if (!p->is_alerted) { return; } AI_INFO info; Creature_AIInfo(item, &info); const int16_t tilt = -info.x_angle; switch (item->current_anim_state) { case M_STATE_FIRE: if (!Creature_CanTargetEnemy(item, &info)) { item->goal_anim_state = M_STATE_STILL; } else if (Item_GetRelativeFrame(item) == 0) { p->has_fired = true; if (p->active_muzzle == M_MUZZLE_RIGHT) { Creature_Shoot( item, &info, &m_FireLeft, creature->joint_rotation[0], 10); } else { Creature_Shoot( item, &info, &m_FireRight, creature->joint_rotation[0], 10); } p->muzzle_flash_timer = 10; Sound_Effect(SFX_LARA_UZI_STOP, &item->pos, SPM_NORMAL); } break; case M_STATE_STILL: if (Creature_CanTargetEnemy(item, &info) && !p->has_fired) { item->goal_anim_state = M_STATE_FIRE; } else if (p->has_fired) { if (item->ai_bits == AI_MODIFY) { item->goal_anim_state = M_STATE_FIRE; } else { p->is_alerted = false; p->has_fired = false; } } break; } int16_t diff = info.angle - creature->joint_rotation[0]; CLAMP(diff, -DEG_1 * 10, DEG_1 * 10); creature->joint_rotation[0] += diff; Creature_Joint(item, 1, tilt); Item_Animate(item); if (info.angle > 0x4000) { item->rot.y += 0x8000; if (info.angle > 0 || info.angle < 0) { creature->joint_rotation[0] += 0x8000; } } else if (info.angle < -0x4000) { item->rot.y += 0x8000; if (info.angle > 0 || info.angle < 0) { creature->joint_rotation[0] += 0x8000; } } } static void M_HandleEvent( ITEM *const item, const OBJECT_EVENT event, const void *const data) { M_PRIV *const p = item->priv; if (event != OBJECT_EVENT_ALERT) { return; } p->is_alerted = true; } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Creature_Collision; obj->event_func = M_HandleEvent; obj->shadow_size = 0; obj->hit_points = 100; obj->radius = 102; obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 0)->rot.y = true; Object_GetBone(obj, 1)->rot.x = true; } REGISTER_OBJECT(O_SENTRY_GUN, M_Setup) ================================================ FILE: src/trx/game/objects/traps/sliding_pillar.c ================================================ #include #include #include #include #include #include #include #include #include typedef enum { // clang-format off PILLAR_STATE_READY_FORWARD = 0, PILLAR_STATE_READY_BACK = 1, PILLAR_STATE_MOVING = 2, // clang-format on } PILLAR_STATE; typedef enum { // clang-format off PILLAR_ANIM_READY_FORWARD = 0, PILLAR_ANIM_READY_BACK = 1, PILLAR_ANIM_FORWARD = 2, PILLAR_ANIM_BACK = 3, // clang-format on } PILLAR_ANIM; typedef struct { GAME_VECTOR initial; GAME_VECTOR linked; } M_PRIV; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; if (JSON_SHOULD(JSON_PUSH(io, "linked"))) { JSON_SHOULD(JSON_READ(io, "x", &p->linked.pos.x)); JSON_SHOULD(JSON_READ(io, "y", &p->linked.pos.y)); JSON_SHOULD(JSON_READ(io, "z", &p->linked.pos.z)); JSON_SHOULD(JSON_POP(io)); } } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "x", p->linked.pos.x); JSONW_WRITE(io, "y", p->linked.pos.y); JSONW_WRITE(io, "z", p->linked.pos.z); JSONW_POP_AND_SET(io, "linked"); } static void M_SetInitial(ITEM *const item) { M_PRIV *const p = item->priv; p->initial.pos = item->pos; p->initial.room_num = item->room_num; } static GAME_VECTOR M_GetInitial(const ITEM *const item) { const M_PRIV *const p = item->priv; return p->initial; } static void M_SetLinked(ITEM *const item) { M_PRIV *const p = item->priv; p->linked.pos = item->pos; p->linked.room_num = item->room_num; } static GAME_VECTOR M_GetLinked(const ITEM *const item) { const M_PRIV *const p = item->priv; return p->linked; } static bool M_IsItemOnTop( const ITEM *const item, const int32_t x, const int32_t z) { int32_t dx = x - item->pos.x; int32_t dz = z - item->pos.z; // Movable blocks' bounds don't match sector so estimate. return (dx >= -WALL_L / 2 && dx < WALL_L / 2) && (dz >= -WALL_L / 2 && dz < WALL_L / 2); } static int16_t M_GetFloorHeight( const ITEM *const item, int32_t x, int32_t y, int32_t z, int16_t height) { if (item->status == IS_INVISIBLE) { return height; } if (item->current_anim_state == PILLAR_STATE_MOVING) { return height; } if (!M_IsItemOnTop(item, x, z)) { return height; } // If under the bottom of the block. if (y > item->pos.y) { return height; } // If the the top of the block is under the floor height. if (item->pos.y - WALL_L * 2 >= height) { return height; } return item->pos.y - WALL_L * 2; } static int16_t M_GetCeilingHeight( const ITEM *item, int32_t x, int32_t y, int32_t z, int16_t height) { if (item->status == IS_INVISIBLE) { return height; } if (item->current_anim_state == PILLAR_STATE_MOVING) { return height; } // Only care if we are inside the block footprint. if (!M_IsItemOnTop(item, x, z)) { return height; } // If above the top of the block. if (y <= item->pos.y - WALL_L * 2) { return height; } // If the the bottom of the block is above the ceiling height. if (item->pos.y <= height) { return height; } return item->pos.y; } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); MovableBlock_UpdateBox(item, false); M_SetInitial(item); M_SetLinked(item); Walkable_AllocateNodes(item, 1); } static void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage) { if (stage == SAVEGAME_STAGE_BEFORE_LOAD) { MovableBlock_UpdateBox(item, false); } else if (stage == SAVEGAME_STAGE_AFTER_LOAD) { const int16_t item_num = Item_GetIndex(item); // Reposition walkable to its linked sector. Walkable_Reposition(item_num, M_GetInitial(item), M_GetLinked(item)); MovableBlock_UpdateBox(item, true); } } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (Item_IsTriggerActive(item)) { if (item->current_anim_state == PILLAR_STATE_READY_FORWARD) { item->goal_anim_state = PILLAR_STATE_READY_BACK; } } else if (item->current_anim_state == PILLAR_STATE_READY_BACK) { item->goal_anim_state = PILLAR_STATE_READY_FORWARD; } Item_Animate(item); int16_t room_num = item->room_num; Room_GetSector(item->pos, &room_num); Item_UpdateRoom(item_num, room_num); const GAME_VECTOR linked = M_GetLinked(item); if (item->status == IS_ACTIVE && (item->pos.x != linked.pos.x || item->pos.z != linked.pos.z)) { MovableBlock_SlideStack( item->pos.y - WALL_L * 2, linked.pos, item, false); MovableBlock_UpdateBox(item, false); } else if ( item->status == IS_DEACTIVATED && (item->pos.x != linked.pos.x || item->pos.z != linked.pos.z)) { item->pos.x &= -WALL_L; item->pos.x += WALL_L / 2; item->pos.z &= -WALL_L; item->pos.z += WALL_L / 2; const GAME_VECTOR target = { .pos = item->pos, .room_num = item->room_num, }; Walkable_Reposition(item_num, linked, target); M_SetLinked(item); // Reposition possible movable blocks on top. MovableBlock_SlideStack( item->pos.y - WALL_L * 2, linked.pos, item, true); MovableBlock_UpdateBox(item, true); item->status = IS_ACTIVE; } } static void M_AddWalkable(const int16_t item_num) { const ITEM *const item = Item_Get(item_num); Walkable_Add(item_num, item->pos); } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->handle_save_func = M_HandleSave; obj->control_func = M_Control; obj->floor_height_func = M_GetFloorHeight; obj->ceiling_height_func = M_GetCeilingHeight; obj->save_position = true; obj->save_anim = true; obj->save_flags = true; obj->add_walkable_func = M_AddWalkable; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; } REGISTER_OBJECT(O_SLIDING_PILLAR, M_Setup) ================================================ FILE: src/trx/game/objects/traps/spike_ceiling.c ================================================ #include #include #include #include #include #include #include #include #define M_DAMAGE 20 #define M_SPEED 1 #define M_STEP_SLOW 5 #define M_STEP_FAST 10 typedef struct { int32_t step; bool animate; } M_PRIV; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "step", &p->step)); JSON_SHOULD(JSON_READ(io, "animate", &p->animate)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "step", p->step); JSONW_WRITE(io, "animate", p->animate); } static bool M_Trigger(ITEM *const item, const TRIGGER *const trigger) { M_PRIV *const p = item->priv; if (p == nullptr) { return true; } if (trigger == nullptr || trigger->type == TT_ANTITRIGGER || trigger->type == TT_ANTIPAD) { return true; } item->timer = 0; if (trigger->timer == 1) { p->step = M_STEP_FAST; p->animate = true; } else { p->step = M_STEP_SLOW; p->animate = false; } return true; } static void M_Initialise(const int16_t item_num) { Trap_Initialise(item_num); ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; if (p != nullptr) { p->step = M_STEP_SLOW; p->animate = false; } } static void M_Move(const int16_t item_num) { ITEM *const item = Item_Get(item_num); const M_PRIV *const p = item->priv; const int32_t step = (p != nullptr && p->step > 0) ? p->step : M_STEP_SLOW; int16_t room_num = item->room_num; const XYZ_32 pos = { item->pos.x, item->pos.y + step, item->pos.z }; const SECTOR *const sector = Room_GetSector(pos, &room_num); if (Room_GetHeight(sector, pos) < pos.y + WALL_L) { item->status = IS_DEACTIVATED; Sound_StopEffect(SFX_SPIKE_WALL); } else { item->pos.y = pos.y; Item_UpdateRoom(item_num, room_num); Sound_Effect(SFX_SPIKE_WALL, &item->pos, SPM_NORMAL); } } static void M_HitLara(ITEM *const item) { Lara_TakeDamage(M_DAMAGE, true); const ITEM *const lara_item = Lara_GetItem(); Spawn_BloodBath( lara_item->pos.x, item->pos.y + LARA_HEIGHT, lara_item->pos.z, M_SPEED, item->rot.y, lara_item->room_num, 3); item->touch_bits = 0; Sound_Effect(SFX_LARA_FLESH_WOUND, &item->pos, SPM_NORMAL); } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (!Item_IsTriggerActive(item)) { Trap_Reset(item); } else if (item->status != IS_DEACTIVATED) { M_Move(item_num); } if (item->touch_bits) { M_HitLara(item); } if (Item_IsTriggerActive(item) && item->status != IS_DEACTIVATED) { const M_PRIV *const p = item->priv; if (p != nullptr && p->animate) { Item_Animate(item); } } } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Object_Collision_Trap; obj->priv_size = sizeof(M_PRIV); obj->trigger_func = M_Trigger; obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->save_position = true; obj->save_flags = true; } REGISTER_OBJECT(O_CEILING_SPIKES, M_Setup) ================================================ FILE: src/trx/game/objects/traps/spike_wall.c ================================================ #include #include #include #include #include #include #include #include #define M_DAMAGE 20 #define M_SPEED 1 static void M_Move(const int16_t item_num) { ITEM *const item = Item_Get(item_num); const XYZ_32 pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, M_SPEED << 4); int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); if (Room_GetHeight(sector, pos) != pos.y) { item->status = IS_DEACTIVATED; Sound_StopEffect(SFX_SPIKE_WALL); } else { item->pos = pos; Item_UpdateRoom(item_num, room_num); } Sound_Effect(SFX_SPIKE_WALL, &item->pos, SPM_NORMAL); } static void M_HitLara(ITEM *const item) { Lara_TakeDamage(M_DAMAGE, true); const ITEM *const lara_item = Lara_GetItem(); Spawn_BloodBath( lara_item->pos.x, lara_item->pos.y - WALL_L / 2, lara_item->pos.z, M_SPEED, item->rot.y, lara_item->room_num, 3); item->touch_bits = 0; Sound_Effect(SFX_LARA_FLESH_WOUND, &item->pos, SPM_NORMAL); } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (!Item_IsTriggerActive(item)) { Trap_Reset(item); } else if (item->status != IS_DEACTIVATED) { M_Move(item_num); } if (item->touch_bits) { M_HitLara(item); } } static void M_Setup(OBJECT *const obj) { obj->initialise_func = Trap_Initialise; obj->control_func = M_Control; obj->collision_func = Object_Collision; obj->save_position = true; obj->save_flags = true; } REGISTER_OBJECT(O_SPIKE_WALL, M_Setup) ================================================ FILE: src/trx/game/objects/traps/spikes.c ================================================ #include #include #include #include #include #include #define M_FALL_SPEED_LIMIT (g_TRVersion == 1 ? 0 : GRAVITY) #define M_DAMAGE 15 static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); if (lara_item->hit_points < 0) { return; } if (!Lara_TestBoundsCollide(item, coll->radius)) { return; } if (!Collide_TestCollision(item, lara_item)) { return; } int32_t blood_spawn_count = Random_GetControl() / 0x6000; if (lara_item->gravity) { if (lara_item->fall_speed > M_FALL_SPEED_LIMIT && !g_Config.debug.enable_invulnerability) { lara_item->hit_points = -1; blood_spawn_count = 20; } } else if (lara_item->speed < 30) { return; } lara_item->hit_points -= M_DAMAGE; for (int32_t i = 0; i < blood_spawn_count; i++) { const XYZ_32 pos = { .x = lara_item->pos.x + (Random_GetControl() - 0x4000) / 256, .z = lara_item->pos.z + (Random_GetControl() - 0x4000) / 256, .y = lara_item->pos.y - Random_GetControl() / 64, }; Spawn_Blood( pos.x, pos.y, pos.z, 20, Random_GetControl(), item->room_num); } if (lara_item->hit_points <= 0) { Item_SwitchToAnim(lara_item, LA(LA_SPIKE_DEATH), 0); lara_item->current_anim_state = LS(LS_DEATH); lara_item->goal_anim_state = LS(LS_DEATH); lara_item->pos.y = item->pos.y; lara_item->gravity = false; } } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (!Item_IsTriggerActive(item)) { return; } const int32_t level_num = GF_BadGetLevelNum(); if (level_num != 5 && level_num != 7) { return; } if (Item_GetRelativeFrame(item) == 0) { if (level_num == 5) { Sound_Effect(SFX_SHIVA_SWORD_2, &item->pos, SPM_ALWAYS); } else { Sound_Effect(SFX_LARA_GET_OUT, &item->pos, SPM_ALWAYS); } } Item_Animate(item); } static void M_Setup(OBJECT *const obj) { obj->collision_func = M_Collision; if (g_TRVersion == 3) { obj->control_func = M_Control; } } REGISTER_OBJECT(O_SPIKES, M_Setup) ================================================ FILE: src/trx/game/objects/traps/spinning_blade.c ================================================ #include #include #include #include #include #include #include #define M_DAMAGE 100 typedef enum { // clang-format off M_STATE_NULL = 0, M_STATE_STOP = 1, M_STATE_SPIN = 2, // clang-format on } M_STATE; typedef enum { // clang-format off M_ANIM_SPIN_FAST = 0, M_ANIM_SPIN_SLOW = 1, M_ANIM_SPIN_END = 2, M_ANIM_STOP = 3, // clang-format on } M_ANIM; static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); const OBJECT *const obj = Object_Get(item->object_id); Item_SwitchToAnim(item, M_ANIM_STOP, 0); item->current_anim_state = M_STATE_STOP; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); bool flip = false; if (item->current_anim_state == M_STATE_SPIN) { if (item->goal_anim_state != M_STATE_STOP) { const XYZ_32 pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, WALL_L * 3 / 2); int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); if (Room_GetHeight(sector, pos) == NO_HEIGHT) { item->goal_anim_state = M_STATE_STOP; } } flip = true; if (item->touch_bits != 0) { Lara_TakeDamage(M_DAMAGE, true); const ITEM *const lara_item = Lara_GetItem(); Spawn_BloodBath( lara_item->pos.x, lara_item->pos.y - WALL_L / 2, lara_item->pos.z, item->speed * 2, lara_item->rot.y, lara_item->room_num, 2); } Sound_Effect(SFX_ROLLING_BLADE, &item->pos, SPM_NORMAL); } else { if (Item_IsTriggerActive(item)) { item->goal_anim_state = M_STATE_SPIN; } flip = false; } Item_Animate(item); int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); const int32_t height = Room_GetHeight(sector, item->pos); item->floor = height; item->pos.y = height; Item_UpdateRoom(item_num, room_num); if (flip && item->current_anim_state == M_STATE_STOP) { item->rot.y += DEG_180; } } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Object_Collision; obj->save_position = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_SPINNING_BLADE, M_Setup) ================================================ FILE: src/trx/game/objects/traps/springboard.c ================================================ #include #include #include #include typedef enum { // clang-format off SPRINGBOARD_STATE_OFF = 0, SPRINGBOARD_STATE_ON = 1, // clang-format on } SPRINGBOARD_STATE; static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); ITEM *const lara_item = Lara_GetItem(); if (item->current_anim_state == SPRINGBOARD_STATE_OFF && lara_item->pos.y == item->pos.y && ROUND_TO_SECTOR(lara_item->pos.x) == ROUND_TO_SECTOR(item->pos.x) && ROUND_TO_SECTOR(lara_item->pos.z) == ROUND_TO_SECTOR(item->pos.z)) { if (lara_item->hit_points <= 0) { return; } ITEM *const vehicle = Lara_Vehicle_GetItem(); if (vehicle != nullptr) { if (vehicle->object_id != O_SKIDOO_FAST && vehicle->object_id != O_SKIDOO_ARMED) { return; } vehicle->fall_speed = -200; vehicle->pos.y -= STEP_L; } else { if (lara_item->current_anim_state == LS(LS_WALK_BACK) || lara_item->current_anim_state == LS(LS_FAST_BACK)) { lara_item->speed = -lara_item->speed; } lara_item->fall_speed = -240; lara_item->gravity = true; Item_SwitchToAnim(lara_item, LA(LA_FALL_START), 0); lara_item->current_anim_state = LS(LS_JUMP_FORWARD); lara_item->goal_anim_state = LS(LS_JUMP_FORWARD); } item->goal_anim_state = SPRINGBOARD_STATE_ON; } Item_Animate(item); } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_SPRINGBOARD, M_Setup) ================================================ FILE: src/trx/game/objects/traps/teeth_trap.c ================================================ #include #include #include #define M_DAMAGE 400 #define M_NUM_TEETH 6 typedef enum { TEETH_TRAP_STATE_NICE = 0, TEETH_TRAP_STATE_NASTY = 1, } TEETH_TRAP_STATE; static const BITE m_Teeth[M_NUM_TEETH] = { // clang-format off { .pos = { .x = -23, .y = 0, .z = -1718 }, .mesh_num = 0 }, { .pos = { .x = 71, .y = 0, .z = -1718 }, .mesh_num = 1 }, { .pos = { .x = -23, .y = 10, .z = -1718 }, .mesh_num = 0 }, { .pos = { .x = 71, .y = 10, .z = -1718 }, .mesh_num = 1 }, { .pos = { .x = -23, .y = -10, .z = -1718 }, .mesh_num = 0 }, { .pos = { .x = 71, .y = -10, .z = -1718 }, .mesh_num = 1 }, // clang-format on }; static void M_Bite(ITEM *const item, const BITE *const bite) { XYZ_32 pos = bite->pos; Collide_GetJointAbsPosition(item, &pos, bite->mesh_num); Spawn_Blood(pos.x, pos.y, pos.z, item->speed, item->rot.y, item->room_num); } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (Item_IsTriggerActive(item)) { item->goal_anim_state = TEETH_TRAP_STATE_NASTY; if (item->touch_bits != 0 && item->current_anim_state == TEETH_TRAP_STATE_NASTY) { Lara_TakeDamage(M_DAMAGE, true); for (int32_t i = 0; i < M_NUM_TEETH; i++) { M_Bite(item, &m_Teeth[i]); } } } else { item->goal_anim_state = TEETH_TRAP_STATE_NICE; } Item_Animate(item); } static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->collision_func = Object_Collision_Trap; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_TEETH_TRAP, M_Setup) ================================================ FILE: src/trx/game/objects/traps/thors_hammer.c ================================================ #include #include #include #include #include typedef enum { THOR_HAMMER_STATE_SET = 0, THOR_HAMMER_STATE_TEASE = 1, THOR_HAMMER_STATE_ACTIVE = 2, THOR_HAMMER_STATE_DONE = 3, } THOR_HAMMER_STATE; typedef struct { int16_t head_item_num; } M_PRIV; static void M_InitialiseHandle(const int16_t item_num) { ITEM *const hand_item = Item_Get(item_num); M_PRIV *const p = hand_item->priv; const int16_t head_item_num = Item_CreateLevelItem(); ASSERT(head_item_num != NO_ITEM); ITEM *const head_item = Item_Get(head_item_num); head_item->object_id = O_THORS_HEAD; head_item->room_num = hand_item->room_num; head_item->pos = hand_item->pos; head_item->rot = hand_item->rot; head_item->shade.value_1 = hand_item->shade.value_1; Item_Initialise(head_item_num); p->head_item_num = head_item_num; } static void M_ControlHandle(const int16_t item_num) { ITEM *const item = Item_Get(item_num); ITEM *const lara_item = Lara_GetItem(); switch (item->current_anim_state) { case THOR_HAMMER_STATE_SET: if (Item_IsTriggerActive(item)) { item->goal_anim_state = THOR_HAMMER_STATE_TEASE; } else { Item_RemoveActive(item_num); item->status = IS_INACTIVE; } break; case THOR_HAMMER_STATE_TEASE: if (Item_IsTriggerActive(item)) { item->goal_anim_state = THOR_HAMMER_STATE_ACTIVE; } else { item->goal_anim_state = THOR_HAMMER_STATE_SET; } break; case THOR_HAMMER_STATE_ACTIVE: { const int32_t frame_num = Item_GetRelativeFrame(item); if (frame_num > 30) { int32_t x = item->pos.x; int32_t z = item->pos.z; switch (item->rot.y) { case 0: z += WALL_L * 3; break; case DEG_90: x += WALL_L * 3; break; case -DEG_90: x -= WALL_L * 3; break; case -DEG_180: z -= WALL_L * 3; break; } if (lara_item->hit_points >= 0 && !g_Config.debug.enable_invulnerability && lara_item->pos.x > x - 520 && lara_item->pos.x < x + 520 && lara_item->pos.z > z - 520 && lara_item->pos.z < z + 520) { lara_item->hit_points = -1; lara_item->pos.y = item->pos.y; lara_item->gravity = false; lara_item->current_anim_state = LS(LS_SPECIAL); lara_item->goal_anim_state = LS(LS_SPECIAL); Item_SwitchToAnim(lara_item, LA(LA_BOULDER_DEATH), 0); } } break; } case THOR_HAMMER_STATE_DONE: { int32_t x = item->pos.x; int32_t z = item->pos.z; int32_t old_x = x; int32_t old_z = z; Room_TestTriggers(item); switch (item->rot.y) { case 0: z += WALL_L * 3; break; case DEG_90: x += WALL_L * 3; break; case -DEG_90: x -= WALL_L * 3; break; case -DEG_180: z -= WALL_L * 3; break; } item->pos.x = x; item->pos.z = z; if (lara_item->hit_points >= 0) { Room_AlterFloorHeight(item, -WALL_L * 2); } item->pos.x = old_x; item->pos.z = old_z; Item_RemoveActive(item_num); item->status = IS_DEACTIVATED; break; } } Item_Animate(item); M_PRIV *const p = item->priv; ITEM *const head_item = Item_Get(p->head_item_num); const int16_t relative_anim = Item_GetRelativeAnim(item); const int16_t relative_frame = Item_GetRelativeFrame(item); Item_SwitchToAnim(head_item, relative_anim, relative_frame); head_item->current_anim_state = item->current_anim_state; } static void M_CollisionHandle( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); if (!Lara_TestBoundsCollide(item, coll->radius)) { return; } if (coll->enable_baddie_push) { Lara_Col_ItemPush(item, coll, false, true); } } static void M_CollisionHead( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); if (!Lara_TestBoundsCollide(item, coll->radius)) { return; } if (coll->enable_baddie_push && item->current_anim_state != THOR_HAMMER_STATE_ACTIVE) { Lara_Col_ItemPush(item, coll, false, true); } } static void M_SetupHandle(OBJECT *const obj) { obj->initialise_func = M_InitialiseHandle; obj->control_func = M_ControlHandle; obj->draw_func = Object_DrawUnclippedItem; obj->collision_func = M_CollisionHandle; obj->priv_size = sizeof(M_PRIV); obj->save_flags = true; obj->save_anim = true; } static void M_SetupHead(OBJECT *const obj) { obj->collision_func = M_CollisionHead; obj->draw_func = Object_DrawUnclippedItem; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_THORS_HANDLE, M_SetupHandle) REGISTER_OBJECT(O_THORS_HEAD, M_SetupHead) ================================================ FILE: src/trx/game/objects/traps/train.c ================================================ #include #include #include #include #include #include #include // clang-format off #define M_DEFAULT_SPEED 260 #define M_HIT_SPEED 160 #define M_BRAKE_SPEED 48 #define M_FRONT_DIST (WALL_L * 5) // = 5120 #define M_LIGHT_DIST (WALL_L * 3) // = 3072 #define M_CAM_DIST (WALL_L * 8) // = 8192 // clang-format on typedef struct { int32_t max_speed; } M_PRIV; static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; const ANIM *const anim = Item_GetAnim(item); p->max_speed = anim->velocity != 0 ? anim->velocity : M_DEFAULT_SPEED; item->speed = p->max_speed; } static int32_t M_GetHeight( const ITEM *const item, const int32_t x, const int32_t z, int16_t *const room_num) { XYZ_32 pos = item->pos; const int32_t sy = Math_Sin(item->rot.y); const int32_t cy = Math_Cos(item->rot.y); const int32_t sx = Math_Sin(item->rot.x); const int32_t sz = Math_Sin(item->rot.z); pos.x += (z * sy + x * cy) >> W2V_SHIFT; pos.z += (z * cy - x * sy) >> W2V_SHIFT; pos.y = ((x * sz) >> W2V_SHIFT) + (item->pos.y - ((z * sx) >> W2V_SHIFT)); *room_num = item->room_num; const SECTOR *const sector = Room_GetSector(pos, room_num); return Room_GetHeight(sector, pos); } static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const train_item = Item_Get(item_num); if (train_item->status != IS_ACTIVE) { Object_Collision(item_num, lara_item, coll); return; } if (!Item_TestBoundsCollide(train_item, lara_item, coll->radius)) { return; } if (!Collide_TestCollision(train_item, lara_item)) { return; } Sound_Effect(SFX_LARA_GENERAL_DEATH, &lara_item->pos, SPM_ALWAYS); Sound_Effect(SFX_LARA_FALL_DEATH, &lara_item->pos, SPM_ALWAYS); Sound_StopEffect(SFX_TRAIN_LOOP); LARA_INFO *const lara = Lara_GetLaraInfo(); lara_item->hit_points = 0; lara_item->rot.y = train_item->rot.y; lara->move_angle = lara_item->rot.y; lara_item->gravity = false; lara_item->fall_speed = 0; lara_item->speed = 0; lara->gun_type = LGT_UNARMED; Lara_SwitchToExtraState(LS_EXTRA_TRAIN_KILL); if (train_item->speed != 0) { train_item->speed = M_HIT_SPEED; } XYZ_32 pos = XYZ_32_OffsetYaw(lara_item->pos, lara_item->rot.y, STEP_L); pos.y -= STEP_L * 2; Spawn_BloodBath( pos.x, pos.y, pos.z, WALL_L, lara_item->rot.y, lara_item->room_num, 15); } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (!Item_IsTriggerActive(item)) { return; } item->pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, item->speed); int16_t room_num = NO_ROOM; const int32_t front_height = M_GetHeight(item, 0, M_FRONT_DIST, &room_num); const int32_t mid_height = M_GetHeight(item, 0, 0, &room_num); item->pos.y = mid_height; if (item->pos.y == NO_HEIGHT) { Item_Kill(item_num); return; } item->pos.y -= 32; room_num = item->room_num; Room_GetSector(item->pos, &room_num); Item_UpdateRoom(item_num, room_num); item->rot.x = (mid_height - front_height) << 1; const XYZ_32 light_pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, M_LIGHT_DIST); Output_AddDynamicLightRGB(light_pos, 14, (RGB_888) { 0xFF, 0xFF, 0xFF }); const M_PRIV *const p = item->priv; if (item->speed == p->max_speed) { Sound_Effect(SFX_TRAIN_LOOP, &item->pos, SPM_ALWAYS); return; } if (item->speed == M_HIT_SPEED) { XYZ_32 cam_pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, M_CAM_DIST); cam_pos.y -= STEP_L * 2; const SECTOR *const sector = Room_GetSector(cam_pos, &room_num); cam_pos.y = Room_GetHeight(sector, cam_pos); Camera_UpdateDynamicFixedObject(cam_pos, item->room_num); } item->speed -= M_BRAKE_SPEED; CLAMPL(item->speed, 0); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->collision_func = M_Collision; obj->control_func = M_Control; obj->priv_size = sizeof(M_PRIV); obj->save_position = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_TRAIN, M_Setup) ================================================ FILE: src/trx/game/objects/traps/wasp_emitter.c ================================================ #include #include #include #include #include // clang-format off #define M_MAX_SLOTS 3 #define M_MAX_ACTIVE 2 #define M_MAX_DIST SQUARE(WALL_L * 12) // = 150994944 #define M_COOLDOWN 255 // clang-format on typedef struct { int32_t cooldown; int32_t spawn_count; int32_t spawn_total; int16_t slots[M_MAX_SLOTS]; } M_PRIV; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_OPTIONAL(JSON_READ(io, "cooldown", &p->cooldown)); JSON_OPTIONAL(JSON_READ(io, "spawn_count", &p->spawn_count)); JSON_OPTIONAL(JSON_READ(io, "spawn_total", &p->spawn_total)); if (JSON_SHOULD(JSON_PUSH(io, "slots"))) { for (int32_t i = 0; i < M_MAX_SLOTS; i++) { JSON_SHOULD(JSON_READ_A(io, i, &p->slots[i])); } JSON_POP(io); } } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "cooldown", p->cooldown); JSONW_WRITE(io, "spawn_count", p->spawn_count); JSONW_WRITE(io, "spawn_total", p->spawn_total); JSONW_PUSH_ARRAY(io); for (int32_t i = 0; i < M_MAX_SLOTS; i++) { JSONW_PUSH_VALUE(io, p->slots[i]); JSONW_POP_AND_APPEND(io); } JSONW_POP_AND_SET(io, "slots"); } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; for (int32_t i = 0; i < M_MAX_SLOTS; i++) { p->slots[i] = NO_ITEM; } } static void M_PopulateSlots(M_PRIV *const p) { int32_t count = 0; for (int32_t i = 0; i < Item_GetLevelCount(); i++) { ITEM *const item = Item_Get(i); if (item->object_id != O_WASP_MUTANT || (item->ai_bits & AI_MODIFY) == 0) { continue; } p->slots[count++] = i; if (count >= M_MAX_SLOTS) { break; } } } static int32_t M_GetEmptySlot(const M_PRIV *const p) { for (int32_t i = 0; i < M_MAX_SLOTS; i++) { const ITEM *const item = Item_Get(p->slots[i]); if (item->creature_data == nullptr) { return i; } } return -1; } static int32_t M_GetActiveCount(const M_PRIV *const p) { int32_t count = 0; for (int32_t i = 0; i < M_MAX_SLOTS; i++) { const ITEM *const item = Item_Get(p->slots[i]); if (item->active) { count++; } } return count; } static void M_SpawnWasp(const ITEM *const spawner_item, const int32_t slot_idx) { M_PRIV *const p = spawner_item->priv; ITEM *const wasp_item = Item_Get(p->slots[slot_idx]); wasp_item->pos = spawner_item->pos; wasp_item->rot = spawner_item->rot; Item_SwitchToAnim(wasp_item, 0, 0); wasp_item->current_anim_state = Item_GetAnim(wasp_item)->current_anim_state; wasp_item->goal_anim_state = wasp_item->current_anim_state; wasp_item->required_anim_state = 0; wasp_item->flags &= ~(IF_INVISIBLE | IF_KILLED | 3); wasp_item->creature_data = nullptr; wasp_item->hit_points = Object_Get(wasp_item->object_id)->hit_points; wasp_item->mesh_bits = -1; wasp_item->status = IS_ACTIVE; wasp_item->collidable = true; wasp_item->ai_bits = AI_MODIFY; if (wasp_item->active) { Item_RemoveActive(p->slots[slot_idx]); } Item_AddActive(p->slots[slot_idx]); Item_UpdateRoom(p->slots[slot_idx], NO_ITEM); Item_UpdateRoom(p->slots[slot_idx], spawner_item->room_num); LOT_EnableBaddieAI(p->slots[slot_idx], true); } static bool M_Trigger(ITEM *const item, const TRIGGER *const trigger) { if (trigger == nullptr || trigger->type == TT_ANTITRIGGER || trigger->type == TT_ANTIPAD) { return true; } item->timer = 0; item->flags |= IF_ONE_SHOT; M_PRIV *const p = item->priv; p->spawn_total = trigger->timer; p->spawn_count = 0; return true; } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; if (!item->active || p->spawn_count >= p->spawn_total) { return; } if (p->slots[0] == NO_ITEM) { M_PopulateSlots(p); return; } const int16_t m_EmptySlot = M_GetEmptySlot(p); if (m_EmptySlot == -1) { return; } const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; const int32_t dist = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz }); if (ABS(dx) > 32000 || ABS(dz) > 32000 || dist > M_MAX_DIST) { return; } if (p->cooldown > 0) { p->cooldown--; return; } p->cooldown = M_COOLDOWN; const int32_t active_count = M_GetActiveCount(item->priv); if (active_count >= M_MAX_ACTIVE) { return; } p->spawn_count++; M_SpawnWasp(item, m_EmptySlot); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->trigger_func = M_Trigger; obj->control_func = M_Control; obj->draw_func = nullptr; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->save_flags = true; } REGISTER_OBJECT(O_WASP_MUTANT_EMITTER, M_Setup) ================================================ FILE: src/trx/game/objects/types.h ================================================ #pragma once #include #include #include #include #include #include #include #include #include #include typedef struct { const OBJECT_ID key_id; const OBJECT_ID value_id; } GAME_OBJECT_PAIR; typedef struct { void *priv; XYZ_16 center; int32_t radius; int16_t num_lights; int16_t num_vertices; union { XYZ_16 *normals; int16_t *lights; } lighting; XYZ_16 *vertices; struct { int16_t count; FACE *data; } all_faces, tex_faces, tex_face4s, tex_face3s, flat_faces, flat_face4s, flat_face3s; float depth_adjustment; bool enable_reflections; bool enable_caustics; } OBJECT_MESH; typedef struct { struct { XYZ_16 min; XYZ_16 max; } shift, rot; bool ignore_rot; } OBJECT_BOUNDS; typedef struct JSON_READ_IO JSON_READ_IO; typedef struct JSON_WRITE_IO JSON_WRITE_IO; typedef enum { OBJECT_EVENT_ALERT, OBJECT_EVENT_BURNT, } OBJECT_EVENT; typedef struct OBJECT { int16_t mesh_count; int16_t mesh_idx; int32_t bone_idx; uint32_t frame_ofs; ANIM_FRAME *frame_base; void (*setup_func)(struct OBJECT *obj); void (*initialise_func)(int16_t item_num); void (*control_func)(int16_t item_num); bool (*draw_func)(const ITEM *item); // NOTE: not to be union'd with draw_func, due to default draw_func impl // being Object_DrawAnimatingItem which takes an ITEM* bool (*effect_draw_func)(const EFFECT *item); void (*collision_func)(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); int16_t (*floor_height_func)( const ITEM *item, int32_t x, int32_t y, int32_t z, int16_t height); int16_t (*ceiling_height_func)( const ITEM *item, int32_t x, int32_t y, int32_t z, int16_t height); void (*activate_func)(ITEM *item); void (*event_func)(ITEM *item, OBJECT_EVENT event, const void *data); bool (*trigger_func)(ITEM *item, const TRIGGER *trigger); bool (*gun_hit_func)( ITEM *item, const GAME_VECTOR *start, const GAME_VECTOR *hit_pos, int32_t *damage); void (*handle_flip_func)(ITEM *item, ROOM_FLIP_STATUS flip_status); void (*handle_save_func)(ITEM *item, SAVEGAME_STAGE stage); void (*priv_load_func)(ITEM *item, JSON_READ_IO *io); void (*priv_save_func)(const ITEM *item, JSON_WRITE_IO *io); const OBJECT_BOUNDS *(*bounds_func)(void); bool (*is_usable_func)(int16_t item_num); void (*add_walkable_func)(int16_t item_num); int16_t (*carrier_item_num_func)(const ITEM *item); bool (*can_drop_items_func)(const ITEM *item); bool (*can_interpolate_func)( const ITEM *item, int32_t frame_a, int32_t frame_b); bool (*should_spawn_blood_func)(const ITEM *item); bool (*is_alive_func)(const ITEM *item); bool (*is_targetable_func)(const ITEM *item); bool (*can_take_damage_func)(const ITEM *item); bool (*can_be_projectile_target_func)(const ITEM *item); bool (*can_be_exploded_func)(const ITEM *item); int32_t (*get_mesh_index_func)(const ITEM *item, int32_t mesh_idx); int16_t anim_idx; int16_t anim_count; int16_t hit_points; int16_t pivot_length; int16_t radius; int16_t shadow_size; int16_t smartness; XYZ_BOOL base_rot; LOT_SETUP lot_setup; size_t priv_size; bool enable_interpolation; bool loaded; bool intelligent; bool save_position; bool save_hitpoints; bool save_flags; bool save_anim; bool load_floor; bool semi_transparent; } OBJECT; typedef struct { bool loaded; int16_t mesh_idx; bool collidable; bool visible; BOUNDS_16 draw_bounds; BOUNDS_16 collision_bounds; } STATIC_OBJECT_3D; typedef struct { bool loaded; int16_t frame_count; int16_t texture_idx; } STATIC_OBJECT_2D; typedef enum { TRAP_SET = 0, TRAP_ACTIVATE = 1, TRAP_WORKING = 2, TRAP_FINISHED = 3, } TRAP_ANIM; ================================================ FILE: src/trx/game/objects/vars.c ================================================ #include const GAME_OBJECT_PAIR g_KeyItemToReceptacleMap[] = { // clang-format off { O_KEY_OPTION_1, O_KEY_HOLE_1 }, { O_KEY_OPTION_2, O_KEY_HOLE_2 }, { O_KEY_OPTION_3, O_KEY_HOLE_3 }, { O_KEY_OPTION_4, O_KEY_HOLE_4 }, { O_PUZZLE_OPTION_1, O_PUZZLE_HOLE_1 }, { O_PUZZLE_OPTION_2, O_PUZZLE_HOLE_2 }, { O_PUZZLE_OPTION_3, O_PUZZLE_HOLE_3 }, { O_PUZZLE_OPTION_4, O_PUZZLE_HOLE_4 }, { O_LEADBAR_OPTION, O_MIDAS_TOUCH }, { O_KEY_OPTION_2, O_GONG }, { O_KEY_OPTION_2, O_DETONATOR_BOX }, { NO_OBJECT, NO_OBJECT }, // clang-format on }; const GAME_OBJECT_PAIR g_ReceptacleToReceptacleDoneMap[] = { // clang-format off { O_PUZZLE_HOLE_1, O_PUZZLE_DONE_1 }, { O_PUZZLE_HOLE_2, O_PUZZLE_DONE_2 }, { O_PUZZLE_HOLE_3, O_PUZZLE_DONE_3 }, { O_PUZZLE_HOLE_4, O_PUZZLE_DONE_4 }, { NO_OBJECT, NO_OBJECT }, // clang-format on }; const OBJECT_ID g_ReceptacleObjects[] = { // clang-format off O_KEY_HOLE_1, O_KEY_HOLE_2, O_KEY_HOLE_3, O_KEY_HOLE_4, O_PUZZLE_HOLE_1, O_PUZZLE_HOLE_2, O_PUZZLE_HOLE_3, O_PUZZLE_HOLE_4, O_PUZZLE_DONE_1, O_PUZZLE_DONE_2, O_PUZZLE_DONE_3, O_PUZZLE_DONE_4, O_MIDAS_TOUCH, O_GONG, O_DETONATOR_BOX, NO_OBJECT, // clang-format on }; const OBJECT_ID g_CreatureObjects[] = { // clang-format off O_ALLIGATOR, O_APE, O_ATLANTEAN_GROUND, O_ATLANTEAN_SHOOTER, O_ATLANTEAN_WINGED, O_BALDY, O_BANDIT_1, O_BANDIT_2, O_BANDIT_2B, O_BARRACUDA, O_BAT, O_BEAR, O_BIG_EEL, O_BIG_SPIDER, O_BIRD_GUARDIAN, O_CENTAUR, O_CENTAUR_STATUE, O_CIVILIAN, O_CLAW_MUTANT, O_COBRA, O_COMPY, O_COWBOY, O_CRAWLER_MUTANT, O_CROCODILE, O_CROW, O_CULT_1, O_CULT_1A, O_CULT_1B, O_CULT_2, O_CULT_3, O_DINO_WARRIOR, O_DIVER, O_DOG, O_DRAGON_FRONT, O_EAGLE, O_EEL, O_FISH, O_HUSKIE, O_HYBRID_MUTANT, O_JELLY, O_LARSON, O_LION, O_LIONESS, O_LIZARD, O_MONKEY, O_MONK_1, O_MONK_2, O_MONK_3, O_MOUSE, O_MP_1, O_MP_2, O_MUMMY, O_NATLA, O_ORCA, O_PATROL_DOG, O_PIERRE, O_PRISONER, O_PUMA, O_PUNK_1, O_PUNK_2, O_RAPTOR, O_RAT, O_RX_WORKER_1, O_RX_WORKER_2, O_RX_WORKER_3, O_SECURITY_GUARD, O_SENTRY_GUN, O_SHARK, O_SHIVA, O_SKATEKID, O_SKIDOO_DRIVER, O_SOPHIA, O_SPIDER, O_STHPAC_MERCENARY, O_SWAT_1, O_SWAT_2, O_SWAT_3, O_TIGER, O_TONY, O_TORSO, O_TREX, O_TREX_ALPHA, O_TRIBE_AXEMAN, O_TRIBE_BOSS, O_TRIBE_PIPEMAN, O_VOLE, O_VULTURE, O_WASP_MUTANT, O_WILLARD, O_WOLF, O_WORKER_1, O_WORKER_2, O_WORKER_3, O_WORKER_4, O_WORKER_5, O_XIAN_KNIGHT, O_XIAN_KNIGHT_STATUE, O_XIAN_SPEARMAN, O_XIAN_SPEARMAN_STATUE, O_YETI, NO_OBJECT, // clang-format on }; const OBJECT_ID g_ProjectileObjects[] = { // clang-format off O_HARPOON_BOLT, O_GRENADE, O_ROCKET, NO_OBJECT, // clang-format on }; const OBJECT_ID g_WaterObjects[] = { // clang-format off O_ALLIGATOR, O_BARRACUDA, O_BIG_EEL, O_DIVER, O_EEL, O_FISH, O_GENERAL, O_JELLY, O_PROPELLER_2, O_SHARK, O_VOLE, O_ORCA, NO_OBJECT, // clang-format on }; const OBJECT_ID g_LoyalObjects[] = { // clang-format off O_LARA, O_WINSTON, NO_OBJECT, // clang-format on }; const OBJECT_ID g_BossObjects[] = { // clang-format off O_TREX, O_TREX_ALPHA, O_LARSON, O_PIERRE, O_SKATEKID, O_COWBOY, O_BALDY, O_NATLA, O_TORSO, O_CULT_3, O_DRAGON_FRONT, O_BARTOLI, O_BIRD_GUARDIAN, O_SKIDOO_DRIVER, O_SKIDOO_ARMED, O_TONY, O_TRIBE_BOSS, O_SOPHIA, O_WILLARD, NO_OBJECT, // clang-format on }; const OBJECT_ID g_MovableBlockObjects[] = { // clang-format off O_MOVABLE_BLOCK_1, O_MOVABLE_BLOCK_2, O_MOVABLE_BLOCK_3, O_MOVABLE_BLOCK_4, NO_OBJECT, // clang-format on }; const OBJECT_ID g_SecretObjects[] = { // clang-format off O_SECRET_1, O_SECRET_2, O_SECRET_3, NO_OBJECT, // clang-format on }; const OBJECT_ID g_PickupObjects[] = { // clang-format off O_PISTOL_ITEM, O_PISTOL_AMMO_ITEM, O_SHOTGUN_ITEM, O_SHOTGUN_AMMO_ITEM, O_MAGNUM_ITEM, O_MAGNUM_AMMO_ITEM, O_AUTOS_ITEM, O_AUTOS_AMMO_ITEM, O_DESERT_EAGLE_ITEM, O_DESERT_EAGLE_AMMO_ITEM, O_UZI_AMMO_ITEM, O_UZI_ITEM, O_HARPOON_ITEM, O_HARPOON_AMMO_ITEM, O_M16_ITEM, O_M16_AMMO_ITEM, O_MP5_ITEM, O_MP5_AMMO_ITEM, O_GRENADE_GUN_ITEM, O_GRENADE_AMMO_ITEM, O_ROCKET_GUN_ITEM, O_ROCKET_AMMO_ITEM, O_EXPLOSIVE_ITEM, O_SMALL_MEDIPACK_ITEM, O_LARGE_MEDIPACK_ITEM, O_FLAREBOX_ITEM, O_FLARE_ITEM, O_KEY_ITEM_1, O_KEY_ITEM_2, O_KEY_ITEM_3, O_KEY_ITEM_4, O_PICKUP_ITEM_1, O_PICKUP_ITEM_2, O_QUEST_ITEM_1, O_QUEST_ITEM_2, O_QUEST_ITEM_3, O_QUEST_ITEM_4, O_PUZZLE_ITEM_1, O_PUZZLE_ITEM_2, O_PUZZLE_ITEM_3, O_PUZZLE_ITEM_4, O_LEADBAR_ITEM, O_SCION_ITEM_1, O_SCION_ITEM_2, O_SECRET_1, O_SECRET_2, O_SECRET_3, NO_OBJECT, // clang-format on }; const OBJECT_ID g_ElevatedPickupObjects[] = { // clang-format off O_SCION_ITEM_1, NO_OBJECT, // clang-format on }; const OBJECT_ID g_SwitchObjects[] = { // clang-format off O_SWITCH_TYPE_AIRLOCK, O_SWITCH_TYPE_BUTTON, O_SWITCH_TYPE_NORMAL, O_SWITCH_TYPE_SMALL, O_SWITCH_TYPE_UW, O_SWITCH_TYPE_WHEEL, NO_OBJECT, // clang-format on }; const OBJECT_ID g_GunObjects[] = { // clang-format off O_PISTOL_ITEM, O_SHOTGUN_ITEM, O_MAGNUM_ITEM, O_AUTOS_ITEM, O_DESERT_EAGLE_ITEM, O_UZI_ITEM, O_HARPOON_ITEM, O_M16_ITEM, O_MP5_ITEM, O_GRENADE_GUN_ITEM, O_ROCKET_GUN_ITEM, NO_OBJECT, // clang-format on }; const OBJECT_ID g_GunAmmoObjects[] = { // clang-format off O_PISTOL_AMMO_ITEM, O_SHOTGUN_AMMO_ITEM, O_MAGNUM_AMMO_ITEM, O_AUTOS_AMMO_ITEM, O_DESERT_EAGLE_AMMO_ITEM, O_UZI_AMMO_ITEM, O_HARPOON_AMMO_ITEM, O_M16_AMMO_ITEM, O_MP5_AMMO_ITEM, O_GRENADE_AMMO_ITEM, O_ROCKET_AMMO_ITEM, NO_OBJECT, // clang-format on }; const OBJECT_ID g_DoorObjects[] = { // clang-format off O_DOOR_TYPE_1, O_DOOR_TYPE_2, O_DOOR_TYPE_3, O_DOOR_TYPE_4, O_DOOR_TYPE_5, O_DOOR_TYPE_6, O_DOOR_TYPE_7, O_DOOR_TYPE_8, NO_OBJECT, // clang-format on }; const OBJECT_ID g_TrapdoorObjects[] = { // clang-format off O_TRAPDOOR_TYPE_1, O_TRAPDOOR_TYPE_2, O_TRAPDOOR_TYPE_3, O_DRAWBRIDGE, NO_OBJECT, // clang-format on }; const OBJECT_ID g_AnimObjects[] = { // clang-format off O_LARA_PISTOLS, O_LARA_SHOTGUN, O_LARA_MAGNUMS, O_LARA_DESERT_EAGLE, O_LARA_UZIS, O_LARA_HARPOON_GUN, O_LARA_M16, O_LARA_MP5, O_LARA_GRENADE_GUN, O_LARA_FLARE, O_LARA_HAIR, O_LARA_EXTRA, O_LARA_SKIDOO, O_LARA_BOAT, NO_OBJECT, // clang-format on }; const OBJECT_ID g_NullObjects[] = { // clang-format off O_ALPHABET, O_ALPHABET_SMALL, O_ASSAULT_DIGITS, O_BLOOD, O_BLOOD_PINK, O_BODY_PART, O_BUBBLE_1, O_BUBBLE_2, O_BUBBLE_EMITTER, O_CAMERA_TARGET, O_COMBAT_END, O_CUT_SHOTGUN, O_DART_EFFECT, O_DRAGON_BONES_2, O_DRAGON_BONES_3, O_DUST, O_EARTHQUAKE, O_EXPLOSION_1, O_EXPLOSION_2, O_FLARE_FIRE, O_FLARE_ITEM, O_FX_RESERVED, O_GLOW, O_GLOW_RESERVED, O_GONG_BONGER, O_GRENADE, O_GUN_FLASH, O_GUN_SHELL, O_HARPOON_BOLT, O_HOT_LIQUID, O_INV_BACKGROUND, O_M16_FLASH, O_NATLA_GUN, O_MISSILE_ATLANTEAN_SHARD, O_MISSILE_ATLANTEAN_BOMB, O_MISSILE_FLAME, O_MISSILE_HARPOON, O_MISSILE_KNIFE, O_PICKUP_AID, O_ROCKET, O_RICOCHET, O_SHOTGUN_SHELL, O_SKYBOX, O_SNOW_SPRITE, O_SPHERE_OF_DOOM_1, O_SPHERE_OF_DOOM_2, O_SPHERE_OF_DOOM_3, O_SPLASH_1, O_SPLASH_2, O_TEXT_BOX, O_TWINKLE, O_WATER_SPRITE, O_DUMMY, NO_OBJECT, // clang-format on }; const OBJECT_ID g_InvObjects[] = { // clang-format off O_PISTOL_OPTION, O_PISTOL_AMMO_OPTION, O_MAGNUM_OPTION, O_MAGNUM_AMMO_OPTION, O_AUTOS_OPTION, O_AUTOS_AMMO_OPTION, O_DESERT_EAGLE_OPTION, O_DESERT_EAGLE_AMMO_OPTION, O_SHOTGUN_OPTION, O_SHOTGUN_AMMO_OPTION, O_UZI_OPTION, O_UZI_AMMO_OPTION, O_HARPOON_OPTION, O_HARPOON_AMMO_OPTION, O_M16_OPTION, O_M16_AMMO_OPTION, O_MP5_OPTION, O_MP5_AMMO_OPTION, O_GRENADE_GUN_OPTION, O_GRENADE_AMMO_OPTION, O_ROCKET_GUN_OPTION, O_ROCKET_AMMO_OPTION, O_EXPLOSIVE_OPTION, O_SMALL_MEDIPACK_OPTION, O_LARGE_MEDIPACK_OPTION, O_FLAREBOX_OPTION, O_PUZZLE_OPTION_1, O_PUZZLE_OPTION_2, O_PUZZLE_OPTION_3, O_PUZZLE_OPTION_4, O_KEY_OPTION_1, O_KEY_OPTION_2, O_KEY_OPTION_3, O_KEY_OPTION_4, O_QUEST_OPTION_1, O_QUEST_OPTION_2, O_QUEST_OPTION_3, O_QUEST_OPTION_4, O_PICKUP_OPTION_1, O_PICKUP_OPTION_2, O_COMPASS_OPTION, O_STOPWATCH_OPTION, O_CONTROL_OPTION, O_DETAIL_OPTION, O_GAMMA_OPTION, O_GLOBE_SELECT_OPTION, O_LEADBAR_OPTION, O_PASSPORT_OPTION, O_PHOTO_OPTION, O_SCION_OPTION, O_SOUND_OPTION, NO_OBJECT, // clang-format on }; const OBJECT_ID g_WaterSpriteObjects[] = { // clang-format off O_WATERFALL, O_SPLASH_1, O_SPLASH_2, O_BUBBLE_1, O_BUBBLE_2, NO_OBJECT, // clang-format on }; const OBJECT_ID g_GameSpriteObjects[] = { // clang-format off O_PISTOL_ITEM, O_SHOTGUN_ITEM, O_MAGNUM_ITEM, O_AUTOS_ITEM, O_DESERT_EAGLE_ITEM, O_UZI_ITEM, O_HARPOON_ITEM, O_M16_ITEM, O_MP5_ITEM, O_GRENADE_GUN_ITEM, O_ROCKET_GUN_ITEM, O_PISTOL_AMMO_ITEM, O_SHOTGUN_AMMO_ITEM, O_MAGNUM_AMMO_ITEM, O_AUTOS_AMMO_ITEM, O_DESERT_EAGLE_AMMO_ITEM, O_UZI_AMMO_ITEM, O_HARPOON_AMMO_ITEM, O_M16_AMMO_ITEM, O_MP5_AMMO_ITEM, O_GRENADE_AMMO_ITEM, O_ROCKET_AMMO_ITEM, O_EXPLOSIVE_ITEM, O_SMALL_MEDIPACK_ITEM, O_LARGE_MEDIPACK_ITEM, O_FLAREBOX_ITEM, O_PUZZLE_ITEM_1, O_PUZZLE_ITEM_2, O_PUZZLE_ITEM_3, O_PUZZLE_ITEM_4, O_KEY_ITEM_1, O_KEY_ITEM_2, O_KEY_ITEM_3, O_KEY_ITEM_4, O_PICKUP_ITEM_1, O_PICKUP_ITEM_2, O_QUEST_ITEM_1, O_QUEST_ITEM_2, O_QUEST_ITEM_3, O_QUEST_ITEM_4, O_LEADBAR_ITEM, O_SCION_ITEM_1, O_SCION_ITEM_2, O_SECRET_1, O_SECRET_2, O_SECRET_3, O_EXPLOSION_1, O_EXPLOSION_2, O_MISSILE_FLAME, O_SPLASH_1, O_SPLASH_2, O_BUBBLE_1, O_BUBBLE_2, O_BLOOD, O_BLOOD_PINK, O_DART_EFFECT, O_RICOCHET, O_TWINKLE, O_DUST, O_EMBER, O_FLAME, O_PICKUP_AID, O_GLOW, O_WATER_SPRITE, O_SNOW_SPRITE, O_HOT_LIQUID, O_SHADOW, O_GLOW_RESERVED, O_FX_RESERVED, O_ALPHABET, O_ALPHABET_SMALL, O_TEXT_BOX, O_ASSAULT_DIGITS, NO_OBJECT, // clang-format on }; const GAME_OBJECT_PAIR g_GunAmmoObjectMap[] = { // clang-format off { O_PISTOL_ITEM, O_PISTOL_AMMO_ITEM }, { O_SHOTGUN_ITEM, O_SHOTGUN_AMMO_ITEM }, { O_MAGNUM_ITEM, O_MAGNUM_AMMO_ITEM }, { O_AUTOS_ITEM, O_AUTOS_AMMO_ITEM }, { O_DESERT_EAGLE_ITEM, O_DESERT_EAGLE_AMMO_ITEM }, { O_UZI_ITEM, O_UZI_AMMO_ITEM }, { O_HARPOON_ITEM, O_HARPOON_AMMO_ITEM }, { O_M16_ITEM, O_M16_AMMO_ITEM }, { O_MP5_ITEM, O_MP5_AMMO_ITEM }, { O_GRENADE_GUN_ITEM, O_GRENADE_AMMO_ITEM }, { O_ROCKET_GUN_ITEM, O_ROCKET_AMMO_ITEM }, { NO_OBJECT, NO_OBJECT }, // clang-format on }; const GAME_OBJECT_PAIR g_ItemToInvObjectMap[] = { // clang-format off { O_PISTOL_ITEM, O_PISTOL_OPTION }, { O_SHOTGUN_ITEM, O_SHOTGUN_OPTION }, { O_MAGNUM_ITEM, O_MAGNUM_OPTION }, { O_AUTOS_ITEM, O_AUTOS_OPTION }, { O_DESERT_EAGLE_ITEM, O_DESERT_EAGLE_OPTION }, { O_UZI_ITEM, O_UZI_OPTION }, { O_HARPOON_ITEM, O_HARPOON_OPTION }, { O_M16_ITEM, O_M16_OPTION }, { O_MP5_ITEM, O_MP5_OPTION }, { O_GRENADE_GUN_ITEM, O_GRENADE_GUN_OPTION }, { O_ROCKET_GUN_ITEM, O_ROCKET_GUN_OPTION }, { O_PISTOL_AMMO_ITEM, O_PISTOL_AMMO_OPTION }, { O_SHOTGUN_AMMO_ITEM, O_SHOTGUN_AMMO_OPTION }, { O_MAGNUM_AMMO_ITEM, O_MAGNUM_AMMO_OPTION }, { O_AUTOS_AMMO_ITEM, O_AUTOS_AMMO_OPTION }, { O_DESERT_EAGLE_AMMO_ITEM, O_DESERT_EAGLE_AMMO_OPTION }, { O_UZI_AMMO_ITEM, O_UZI_AMMO_OPTION }, { O_HARPOON_AMMO_ITEM,O_HARPOON_AMMO_OPTION }, { O_M16_AMMO_ITEM, O_M16_AMMO_OPTION }, { O_MP5_AMMO_ITEM, O_MP5_AMMO_OPTION }, { O_GRENADE_AMMO_ITEM, O_GRENADE_AMMO_OPTION }, { O_ROCKET_AMMO_ITEM, O_ROCKET_AMMO_OPTION }, { O_EXPLOSIVE_ITEM, O_EXPLOSIVE_OPTION }, { O_SMALL_MEDIPACK_ITEM, O_SMALL_MEDIPACK_OPTION }, { O_LARGE_MEDIPACK_ITEM, O_LARGE_MEDIPACK_OPTION }, { O_FLARE_ITEM, O_FLAREBOX_OPTION }, { O_FLAREBOX_ITEM, O_FLAREBOX_OPTION }, { O_PUZZLE_ITEM_1, O_PUZZLE_OPTION_1 }, { O_PUZZLE_ITEM_2, O_PUZZLE_OPTION_2 }, { O_PUZZLE_ITEM_3, O_PUZZLE_OPTION_3 }, { O_PUZZLE_ITEM_4, O_PUZZLE_OPTION_4 }, { O_KEY_ITEM_1, O_KEY_OPTION_1 }, { O_KEY_ITEM_2, O_KEY_OPTION_2 }, { O_KEY_ITEM_3, O_KEY_OPTION_3 }, { O_KEY_ITEM_4, O_KEY_OPTION_4 }, { O_PICKUP_ITEM_1, O_PICKUP_OPTION_1 }, { O_PICKUP_ITEM_2, O_PICKUP_OPTION_2 }, { O_QUEST_ITEM_1, O_QUEST_OPTION_1 }, { O_QUEST_ITEM_2, O_QUEST_OPTION_2 }, { O_QUEST_ITEM_3, O_QUEST_OPTION_3 }, { O_QUEST_ITEM_4, O_QUEST_OPTION_4 }, { O_LEADBAR_ITEM, O_LEADBAR_OPTION }, { O_SCION_ITEM_1, O_SCION_OPTION }, { O_SCION_ITEM_2, O_SCION_OPTION }, { O_SECRET_1, O_SECRET_1_OPTION }, { O_SECRET_2, O_SECRET_2_OPTION }, { O_SECRET_3, O_SECRET_3_OPTION }, { NO_OBJECT, NO_OBJECT }, // clang-format on }; const OBJECT_ID g_ShatterableObjects[] = { // clang-format off O_SMASH_OBJECT_1, O_SMASH_OBJECT_4, NO_OBJECT, // clang-format on }; const OBJECT_ID g_HeavyShatterableObjects[] = { // clang-format off O_SMASH_OBJECT_2, O_SMASH_OBJECT_3, NO_OBJECT, // clang-format on }; const OBJECT_ID g_HeavyMissileObjects[] = { // clang-format off O_HEAVY_ROCKET, NO_OBJECT, // clang-format on }; const OBJECT_ID g_SmashableObjects[] = { // clang-format off O_BELL, O_SCION_ITEM_3, O_CARCASS, O_FUSE_BOX, NO_OBJECT, // clang-format on }; const OBJECT_ID g_ShoalObjects[] = { // clang-format off O_TROPICAL_FISH, O_PIRAHNAS, NO_OBJECT, // clang-format on }; ================================================ FILE: src/trx/game/objects/vars.h ================================================ #pragma once #include #include extern const OBJECT_ID g_CreatureObjects[]; extern const OBJECT_ID g_ProjectileObjects[]; extern const OBJECT_ID g_WaterObjects[]; extern const OBJECT_ID g_LoyalObjects[]; extern const OBJECT_ID g_PickupObjects[]; extern const OBJECT_ID g_ElevatedPickupObjects[]; extern const OBJECT_ID g_SwitchObjects[]; extern const OBJECT_ID g_ReceptacleObjects[]; extern const OBJECT_ID g_GunObjects[]; extern const OBJECT_ID g_GunAmmoObjects[]; extern const OBJECT_ID g_DoorObjects[]; extern const OBJECT_ID g_TrapdoorObjects[]; extern const OBJECT_ID g_AnimObjects[]; extern const OBJECT_ID g_NullObjects[]; extern const OBJECT_ID g_InvObjects[]; extern const OBJECT_ID g_WaterSpriteObjects[]; extern const OBJECT_ID g_BossObjects[]; extern const OBJECT_ID g_SecretObjects[]; extern const OBJECT_ID g_MovableBlockObjects[]; extern const OBJECT_ID g_GameSpriteObjects[]; extern const GAME_OBJECT_PAIR g_GunAmmoObjectMap[]; extern const GAME_OBJECT_PAIR g_ItemToInvObjectMap[]; extern const GAME_OBJECT_PAIR g_KeyItemToReceptacleMap[]; extern const GAME_OBJECT_PAIR g_ReceptacleToReceptacleDoneMap[]; extern const OBJECT_ID g_ShatterableObjects[]; extern const OBJECT_ID g_HeavyShatterableObjects[]; extern const OBJECT_ID g_HeavyMissileObjects[]; extern const OBJECT_ID g_SmashableObjects[]; extern const OBJECT_ID g_ShoalObjects[]; ================================================ FILE: src/trx/game/objects/vehicles/boat.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define BOAT_FALL_ANIM 15 #define BOAT_DEATH_ANIM 18 #define BOAT_GET_ON_LW_ANIM 0 #define BOAT_GET_ON_RW_ANIM 8 #define BOAT_GET_ON_J_ANIM 6 #define BOAT_GET_ON_START 1 #define LF_BOAT_EXIT_END 24 #define BOAT_RADIUS 500 #define BOAT_SIDE 300 #define BOAT_FRONT 750 #define BOAT_TIP (BOAT_FRONT + 250) #define BOAT_MIN_SPEED 20 #define BOAT_MAX_SPEED 90 #define BOAT_SLOW_SPEED (BOAT_MAX_SPEED / 3) // = 30 #define BOAT_FAST_SPEED (BOAT_MAX_SPEED + 50) // = 140 #define BOAT_MAX_BACK (-20) #define BOAT_ACCELERATION 5 #define BOAT_BRAKE 5 #define BOAT_REVERSE (-5) #define BOAT_SLOWDOWN 1 #define BOAT_WAKE 700 #define BOAT_UNDO_TURN (DEG_1 / 4) // = 45 #define BOAT_TURN (DEG_1 / 8) // = 22 #define BOAT_MAX_TURN (DEG_1 * 4) // = 728 #define BOAT_SOUND_CEILING (WALL_L * 5) // = 5120 #define BOAT_SHIFT_Y (-5) typedef enum { BOAT_STATE_GET_ON = 0, BOAT_STATE_STILL = 1, BOAT_STATE_MOVING = 2, BOAT_STATE_JUMP_R = 3, BOAT_STATE_JUMP_L = 4, BOAT_STATE_HIT = 5, BOAT_STATE_FALL = 6, BOAT_STATE_DEATH = 8, } BOAT_STATE; typedef struct { int32_t boat_turn; int32_t left_fallspeed; int32_t right_fallspeed; int16_t tilt_angle; int16_t extra_rotation; int32_t water; int32_t pitch; } M_PRIV; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "boat_turn", &p->boat_turn)); JSON_SHOULD(JSON_READ(io, "left_fallspeed", &p->left_fallspeed)); JSON_SHOULD(JSON_READ(io, "right_fallspeed", &p->right_fallspeed)); JSON_SHOULD(JSON_READ(io, "tilt_angle", &p->tilt_angle)); JSON_SHOULD(JSON_READ(io, "extra_rotation", &p->extra_rotation)); JSON_SHOULD(JSON_READ(io, "water", &p->water)); JSON_SHOULD(JSON_READ(io, "pitch", &p->pitch)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "boat_turn", p->boat_turn); JSONW_WRITE(io, "left_fallspeed", p->left_fallspeed); JSONW_WRITE(io, "right_fallspeed", p->right_fallspeed); JSONW_WRITE(io, "tilt_angle", p->tilt_angle); JSONW_WRITE(io, "extra_rotation", p->extra_rotation); JSONW_WRITE(io, "water", p->water); JSONW_WRITE(io, "pitch", p->pitch); } static int32_t M_CheckGetOn(const int16_t item_num, const COLL_INFO *const coll) { const LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->gun_status != LGS_ARMLESS) { return 0; } ITEM *const boat_item = Item_Get(item_num); const ITEM *const lara_item = Lara_GetItem(); const int32_t dist = ((lara_item->pos.z - boat_item->pos.z) * Math_Cos(-boat_item->rot.y) - (lara_item->pos.x - boat_item->pos.x) * Math_Sin(-boat_item->rot.y)) >> W2V_SHIFT; if (dist > 200) { return 0; } int32_t get_on = 0; const int16_t rot = boat_item->rot.y - lara_item->rot.y; if (lara->water_status == LWS_SURFACE || lara->water_status == LWS_WADE) { if (!g_Input.action || lara_item->gravity || boat_item->speed) { return 0; } if (rot > DEG_45 && rot < DEG_135) { get_on = 1; } else if (rot > -DEG_135 && rot < -DEG_45) { get_on = 2; } } else if (lara->water_status == LWS_ABOVE_WATER) { int16_t fall_speed = lara_item->fall_speed; if (fall_speed > 0) { if (rot > -DEG_135 && rot < DEG_135 && lara_item->pos.y > boat_item->pos.y) { get_on = 3; } } else if (!fall_speed && rot > -DEG_135 && rot < DEG_135) { if (lara_item->pos.x == boat_item->pos.x && lara_item->pos.y == boat_item->pos.y && lara_item->pos.z == boat_item->pos.z) { get_on = 4; } else { get_on = 3; } } } if (!get_on) { return 0; } if (!Item_TestBoundsCollide(boat_item, lara_item, coll->radius)) { return 0; } if (!Collide_TestCollision(boat_item, lara_item)) { return 0; } return get_on; } static int32_t M_TestWaterHeight( const ITEM *const item, const int32_t z_off, const int32_t x_off, XYZ_32 *const pos) { // clang-format off pos->y = item->pos.y + ((x_off * Math_Sin(item->rot.z)) >> W2V_SHIFT) - ((z_off * Math_Sin(item->rot.x)) >> W2V_SHIFT); // clang-format on const int32_t c = Math_Cos(item->rot.y); const int32_t s = Math_Sin(item->rot.y); pos->x = item->pos.x + ((x_off * c + z_off * s) >> W2V_SHIFT); pos->z = item->pos.z + ((z_off * c - x_off * s) >> W2V_SHIFT); int16_t room_num = item->room_num; Room_GetSector(*pos, &room_num); int32_t height = Room_GetWaterHeight(*pos, room_num); if (height == NO_HEIGHT) { const SECTOR *const sector = Room_GetSector(*pos, &room_num); height = Room_GetHeight(sector, *pos); if (height != NO_HEIGHT) { return height; } } return height + BOAT_SHIFT_Y; } static void M_DoWakeEffect(const ITEM *const boat_item) { g_MatrixPtr->_23 = 0; g_WMatrixPtr->_23 = 0; Output_CalculateLight(boat_item->pos, boat_item->room_num); const int16_t frame = (Random_GetDraw() * Object_Get(O_WATER_SPRITE)->mesh_count) >> 15; for (int32_t i = 0; i < 3; i++) { const int16_t effect_num = Effect_Create(boat_item->room_num); if (effect_num == NO_EFFECT) { continue; } EFFECT *const effect = Effect_Get(effect_num); effect->object_id = O_WATER_SPRITE; effect->room_num = boat_item->room_num; effect->frame_num = frame; const int32_t c = Math_Cos(boat_item->rot.y); const int32_t s = Math_Sin(boat_item->rot.y); const int32_t w = (1 - i) * BOAT_SIDE; const int32_t h = BOAT_WAKE; effect->pos.x = boat_item->pos.x + ((-c * w - s * h) >> W2V_SHIFT); effect->pos.y = boat_item->pos.y; effect->pos.z = boat_item->pos.z + ((-c * h + s * w) >> W2V_SHIFT); effect->rot.y = boat_item->rot.y + (i << W2V_SHIFT) - DEG_90; effect->counter = 20; effect->speed = boat_item->speed >> 2; if (boat_item->speed < 64) { effect->fall_speed = (Random_GetDraw() * (ABS(boat_item->speed) - 64)) >> 15; } else { effect->fall_speed = 0; } effect->shade = Output_GetLightAdder() - 768; CLAMPL(effect->shade, 0); } } static void M_DoShift(const int32_t boat_num) { ITEM *const boat_item = Item_Get(boat_num); int16_t item_num = Room_Get(boat_item->room_num)->item_num; while (item_num != NO_ITEM) { ITEM *const item = Item_Get(item_num); if (item->object_id == O_BOAT && item_num != boat_num && Lara_Vehicle_GetIndex() != item_num) { const int32_t dx = item->pos.x - boat_item->pos.x; const int32_t dz = item->pos.z - boat_item->pos.z; const int32_t dist = SQUARE(dx) + SQUARE(dz); if (dist < SQUARE(BOAT_RADIUS * 2)) { boat_item->pos.x = item->pos.x - SQUARE(BOAT_RADIUS * 2) * dx / dist; boat_item->pos.z = item->pos.z - SQUARE(BOAT_RADIUS * 2) * dz / dist; } break; } if (item->object_id == O_GONDOLA && item->current_anim_state == GONDOLA_STATE_FLOATING) { const int32_t c = Math_Cos(item->rot.y); const int32_t s = Math_Sin(item->rot.y); const int32_t ix = item->pos.x - ((s * STEP_L * 2) >> W2V_SHIFT); const int32_t iz = item->pos.z - ((c * STEP_L * 2) >> W2V_SHIFT); const int32_t dx = ix - boat_item->pos.x; const int32_t dz = iz - boat_item->pos.z; const int32_t dist = SQUARE(dx) + SQUARE(dz); if (dist < SQUARE(BOAT_RADIUS * 2)) { if (boat_item->speed < BOAT_MAX_SPEED - 10) { boat_item->pos.x = ix - SQUARE(BOAT_RADIUS * 2) * dx / dist; boat_item->pos.z = iz - SQUARE(BOAT_RADIUS * 2) * dz / dist; } else if (item->pos.y - boat_item->pos.y < WALL_L * 2) { Sound_Effect(SFX_BOAT_INTO_WATER, &item->pos, SPM_NORMAL); item->goal_anim_state = GONDOLA_STATE_CRASH; } } } item_num = item->next_item; } } static int32_t M_DoDynamics( const int32_t height, int32_t fall_speed, int32_t *const y) { if (height > *y) { *y = fall_speed + *y; if (*y > height) { *y = height; fall_speed = 0; } else { fall_speed += GRAVITY; } } else { fall_speed += ((height - fall_speed - *y) >> 3); CLAMPL(fall_speed, -20); CLAMPG(*y, height); } return fall_speed; } static int32_t M_Dynamics(const int16_t boat_num) { ITEM *const boat_item = Item_Get(boat_num); M_PRIV *const p = boat_item->priv; boat_item->rot.z -= p->tilt_angle; XYZ_32 fl_old; XYZ_32 bl_old; XYZ_32 fr_old; XYZ_32 br_old; XYZ_32 f_old; const int32_t hfl_old = M_TestWaterHeight(boat_item, BOAT_FRONT, -BOAT_SIDE, &fl_old); const int32_t hfr_old = M_TestWaterHeight(boat_item, BOAT_FRONT, BOAT_SIDE, &fr_old); const int32_t hbl_old = M_TestWaterHeight(boat_item, -BOAT_FRONT, -BOAT_SIDE, &bl_old); const int32_t hbr_old = M_TestWaterHeight(boat_item, -BOAT_FRONT, BOAT_SIDE, &br_old); const int32_t hf_old = M_TestWaterHeight(boat_item, BOAT_TIP, 0, &f_old); XYZ_32 old = boat_item->pos; CLAMPG(bl_old.y, hbl_old); CLAMPG(br_old.y, hbr_old); CLAMPG(fl_old.y, hfl_old); CLAMPG(fr_old.y, hfr_old); CLAMPG(f_old.y, hf_old); boat_item->rot.y += p->extra_rotation + p->boat_turn; p->tilt_angle = p->boat_turn * 6; boat_item->pos.z += (boat_item->speed * Math_Cos(boat_item->rot.y)) >> W2V_SHIFT; boat_item->pos.x += (boat_item->speed * Math_Sin(boat_item->rot.y)) >> W2V_SHIFT; int32_t slip = (Math_Sin(boat_item->rot.z) * 30) >> W2V_SHIFT; if (!slip && boat_item->rot.z) { slip = boat_item->rot.z > 0 ? 1 : -1; } boat_item->pos.z -= (slip * Math_Sin(boat_item->rot.y)) >> W2V_SHIFT; boat_item->pos.x += (slip * Math_Cos(boat_item->rot.y)) >> W2V_SHIFT; slip = (Math_Sin(boat_item->rot.x) * 10) >> W2V_SHIFT; if (!slip && boat_item->rot.x) { slip = boat_item->rot.x > 0 ? 1 : -1; } boat_item->pos.z -= (slip * Math_Cos(boat_item->rot.y)) >> W2V_SHIFT; boat_item->pos.x = boat_item->pos.x - ((slip * Math_Sin(boat_item->rot.y)) >> W2V_SHIFT); XYZ_32 moved = { .x = boat_item->pos.x, .y = 0, .z = boat_item->pos.z, }; M_DoShift(boat_num); int32_t rot = 0; XYZ_32 bl; const int32_t hbl = M_TestWaterHeight(boat_item, -BOAT_FRONT, -BOAT_SIDE, &bl); if (hbl < bl_old.y - STEP_L / 2) { rot = Vehicle_DoShift(boat_item, &bl, &bl_old); } XYZ_32 br; const int32_t hbr = M_TestWaterHeight(boat_item, -BOAT_FRONT, BOAT_SIDE, &br); if (hbr < br_old.y - STEP_L / 2) { rot += Vehicle_DoShift(boat_item, &br, &br_old); } XYZ_32 fl; const int32_t hfl = M_TestWaterHeight(boat_item, BOAT_FRONT, -BOAT_SIDE, &fl); if (hfl < fl_old.y - STEP_L / 2) { rot += Vehicle_DoShift(boat_item, &fl, &fl_old); } XYZ_32 fr; const int32_t hfr = M_TestWaterHeight(boat_item, BOAT_FRONT, BOAT_SIDE, &fr); if (hfr < fr_old.y - STEP_L / 2) { rot += Vehicle_DoShift(boat_item, &fr, &fr_old); } if (!slip) { XYZ_32 f; const int32_t hf = M_TestWaterHeight(boat_item, BOAT_TIP, 0, &f); if (hf < f_old.y - STEP_L / 2) { Vehicle_DoShift(boat_item, &f, &f_old); } } int16_t room_num = boat_item->room_num; const SECTOR *const sector = Room_GetSector(boat_item->pos, &room_num); int32_t height = Room_GetWaterHeight(boat_item->pos, room_num); if (height == NO_HEIGHT) { height = Room_GetHeight(sector, boat_item->pos); } if (height < boat_item->pos.y - STEP_L / 2) { Vehicle_DoShift(boat_item, &boat_item->pos, &old); } p->extra_rotation = rot; const int32_t collide = Vehicle_GetCollisionAnim(boat_item, &moved); if (slip || collide) { // clang-format off const int32_t new_speed = ( (boat_item->pos.z - old.z) * Math_Cos(boat_item->rot.y) + (boat_item->pos.x - old.x) * Math_Sin(boat_item->rot.y) ) >> W2V_SHIFT; // clang-format on if (Lara_Vehicle_GetIndex() == boat_num) { if (boat_item->speed > BOAT_MAX_SPEED + BOAT_ACCELERATION && new_speed < boat_item->speed - 10) { Lara_TakeDamage((boat_item->speed - new_speed) / 2, true); Sound_Effect(SFX_LARA_INJURY, &Lara_GetItem()->pos, SPM_NORMAL); } } if (slip) { if (boat_item->speed <= BOAT_MAX_SPEED + 10) { boat_item->speed = new_speed; } } else { if (boat_item->speed > 0 && new_speed < boat_item->speed) { boat_item->speed = new_speed; } else if (boat_item->speed < 0 && new_speed > boat_item->speed) { boat_item->speed = new_speed; } } CLAMPL(boat_item->speed, BOAT_MAX_BACK); } return collide; } static int32_t M_UserControl(ITEM *const boat_item) { int32_t no_turn = 1; M_PRIV *const p = boat_item->priv; if (boat_item->pos.y < p->water - STEP_L / 2 || p->water == NO_HEIGHT) { return no_turn; } if (g_Input.look && boat_item->speed == 0) { Lara_Look_UpDown(); return no_turn; } if (g_Input.jump) { return no_turn; } const bool look = g_Input.look && g_Config.gameplay.look_mode != LOOK_MODE_RESTRICTED; const bool left_input = g_Input.left && !look; const bool right_input = g_Input.right && !look; if ((left_input && !g_Input.back) || (right_input && g_Input.back)) { if (p->boat_turn > 0) { p->boat_turn -= BOAT_UNDO_TURN; } else { p->boat_turn -= BOAT_TURN; CLAMPL(p->boat_turn, -BOAT_MAX_TURN); } no_turn = 0; } else if ((right_input && !g_Input.back) || (left_input && g_Input.back)) { if (p->boat_turn < 0) { p->boat_turn += BOAT_UNDO_TURN; } else { p->boat_turn += BOAT_TURN; CLAMPG(p->boat_turn, BOAT_MAX_TURN); } no_turn = 0; } if (g_Input.back) { if (boat_item->speed > 0) { boat_item->speed -= BOAT_BRAKE; } else if (boat_item->speed > BOAT_MAX_BACK) { boat_item->speed += BOAT_REVERSE; } } else if (g_Input.forward) { int32_t max_speed; if (g_Input.action) { max_speed = BOAT_FAST_SPEED; } else { max_speed = g_Input.slow ? BOAT_SLOW_SPEED : BOAT_MAX_SPEED; } if (boat_item->speed < max_speed) { boat_item->speed += BOAT_ACCELERATION / 2 + BOAT_ACCELERATION * boat_item->speed / (2 * max_speed); } else if (boat_item->speed > max_speed + BOAT_SLOWDOWN) { boat_item->speed -= BOAT_SLOWDOWN; } } else if ( boat_item->speed >= 0 && boat_item->speed < BOAT_MIN_SPEED && (left_input || right_input)) { boat_item->speed = BOAT_MIN_SPEED; } else if (boat_item->speed > BOAT_SLOWDOWN) { boat_item->speed -= BOAT_SLOWDOWN; } else { boat_item->speed = 0; } return no_turn; } static void M_Animation(const ITEM *const boat_item, const int32_t collide) { ITEM *const lara_item = Lara_GetItem(); const M_PRIV *const p = boat_item->priv; if (lara_item->hit_points <= 0) { if (lara_item->current_anim_state == BOAT_STATE_DEATH) { return; } Item_SwitchToObjAnim(lara_item, BOAT_DEATH_ANIM, 0, O_LARA_BOAT); lara_item->goal_anim_state = BOAT_STATE_DEATH; lara_item->current_anim_state = BOAT_STATE_DEATH; return; } if (boat_item->pos.y < p->water - STEP_L / 2 && boat_item->fall_speed > 0) { if (lara_item->current_anim_state == BOAT_STATE_FALL) { return; } Item_SwitchToObjAnim(lara_item, BOAT_FALL_ANIM, 0, O_LARA_BOAT); lara_item->goal_anim_state = BOAT_STATE_FALL; lara_item->current_anim_state = BOAT_STATE_FALL; return; } if (collide) { if (lara_item->current_anim_state == BOAT_STATE_HIT) { return; } Item_SwitchToObjAnim(lara_item, collide, 0, O_LARA_BOAT); lara_item->goal_anim_state = BOAT_STATE_HIT; lara_item->current_anim_state = BOAT_STATE_HIT; return; } switch (lara_item->current_anim_state) { case BOAT_STATE_STILL: if (g_Input.jump) { if (g_Input.right) { lara_item->goal_anim_state = BOAT_STATE_JUMP_R; } else if (g_Input.left) { lara_item->goal_anim_state = BOAT_STATE_JUMP_L; } } if (boat_item->speed > 0) { lara_item->goal_anim_state = BOAT_STATE_MOVING; } break; case BOAT_STATE_MOVING: if (g_Input.jump) { if (g_Input.right) { lara_item->goal_anim_state = BOAT_STATE_JUMP_R; } else if (g_Input.left) { lara_item->goal_anim_state = BOAT_STATE_JUMP_L; } } else if (boat_item->speed <= 0) { lara_item->goal_anim_state = BOAT_STATE_STILL; } break; case BOAT_STATE_FALL: lara_item->goal_anim_state = BOAT_STATE_MOVING; break; } } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; p->boat_turn = 0; p->left_fallspeed = 0; p->right_fallspeed = 0; p->tilt_angle = 0; p->extra_rotation = 0; p->water = 0; p->pitch = 0; } static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { if (lara_item->hit_points < 0 || Lara_Vehicle_IsMounted()) { return; } const int32_t get_on = M_CheckGetOn(item_num, coll); if (!get_on) { coll->enable_baddie_push = 1; Object_Collision(item_num, lara_item, coll); return; } Lara_Vehicle_SetIndex(item_num); int16_t boat_anim_idx; switch (get_on) { case 1: boat_anim_idx = BOAT_GET_ON_RW_ANIM; break; case 2: boat_anim_idx = BOAT_GET_ON_LW_ANIM; break; case 3: boat_anim_idx = BOAT_GET_ON_J_ANIM; break; default: boat_anim_idx = BOAT_GET_ON_START; break; } Item_SwitchToObjAnim(lara_item, boat_anim_idx, 0, O_LARA_BOAT); LARA_INFO *const lara = Lara_GetLaraInfo(); lara->water_status = LWS_ABOVE_WATER; lara->hit_direction = DIR_UNKNOWN; ITEM *const boat_item = Item_Get(item_num); lara_item->pos.x = boat_item->pos.x; lara_item->pos.y = boat_item->pos.y + BOAT_SHIFT_Y; lara_item->pos.z = boat_item->pos.z; lara_item->gravity = false; lara_item->rot.x = 0; lara_item->rot.y = boat_item->rot.y; lara_item->rot.z = 0; lara_item->speed = 0; lara_item->fall_speed = 0; lara_item->goal_anim_state = 0; lara_item->current_anim_state = 0; Item_UpdateRoom(lara->item_num, boat_item->room_num); Item_Animate(lara_item); if (boat_item->status != IS_ACTIVE) { Item_AddActive(item_num); boat_item->status = IS_ACTIVE; } } static void M_Control(const int16_t item_num) { ITEM *const lara_item = Lara_GetItem(); const LARA_INFO *const lara = Lara_GetLaraInfo(); ITEM *const boat_item = Item_Get(item_num); M_PRIV *const p = boat_item->priv; bool drive = false; int32_t no_turn = 1; int32_t collide = M_Dynamics(item_num); XYZ_32 fl; XYZ_32 fr; const int32_t hfl = M_TestWaterHeight(boat_item, BOAT_FRONT, -BOAT_SIDE, &fl); const int32_t hfr = M_TestWaterHeight(boat_item, BOAT_FRONT, BOAT_SIDE, &fr); int16_t room_num = boat_item->room_num; const SECTOR *sector = Room_GetSector( (XYZ_32) { boat_item->pos.x, boat_item->pos.y + BOAT_SHIFT_Y, boat_item->pos.z, }, &room_num); int32_t height = Room_GetHeight(sector, boat_item->pos); const int32_t ceiling = Room_GetCeiling(sector, boat_item->pos); const int32_t water_height = Room_GetWaterHeight(boat_item->pos, room_num); p->water = water_height; if (Lara_Vehicle_GetIndex() == item_num && lara_item->hit_points > 0) { switch (lara_item->current_anim_state) { case BOAT_STATE_GET_ON: case BOAT_STATE_JUMP_R: case BOAT_STATE_JUMP_L: break; default: drive = true; no_turn = M_UserControl(boat_item); break; } } else if (boat_item->speed > BOAT_SLOWDOWN) { boat_item->speed -= BOAT_SLOWDOWN; } else { boat_item->speed = 0; } if (no_turn) { if (p->boat_turn < -BOAT_UNDO_TURN) { p->boat_turn += BOAT_UNDO_TURN; } else if (p->boat_turn > BOAT_UNDO_TURN) { p->boat_turn -= BOAT_UNDO_TURN; } else { p->boat_turn = 0; } } boat_item->floor = height + BOAT_SHIFT_Y; if (p->water == NO_HEIGHT) { p->water = height; } else { p->water -= 5; } p->left_fallspeed = M_DoDynamics(hfl, p->left_fallspeed, &fl.y); p->right_fallspeed = M_DoDynamics(hfr, p->right_fallspeed, &fr.y); boat_item->fall_speed = M_DoDynamics(p->water, boat_item->fall_speed, &boat_item->pos.y); height = (fr.y + fl.y) / 2; const int16_t x_rot = Math_Atan(BOAT_FRONT, boat_item->pos.y - height); const int16_t z_rot = Math_Atan(BOAT_SIDE, height - fl.y); boat_item->rot.x += (x_rot - boat_item->rot.x) / 2; boat_item->rot.z += (z_rot - boat_item->rot.z) / 2; if (x_rot == 0 && ABS(boat_item->rot.x) < 4) { boat_item->rot.x = 0; } if (z_rot == 0 && ABS(boat_item->rot.z) < 4) { boat_item->rot.z = 0; } if (Lara_Vehicle_GetIndex() == item_num) { M_Animation(boat_item, collide); Item_UpdateRoom(item_num, room_num); boat_item->rot.z += p->tilt_angle; lara_item->pos.x = boat_item->pos.x; lara_item->pos.y = boat_item->pos.y; lara_item->pos.z = boat_item->pos.z; lara_item->rot.x = boat_item->rot.x; lara_item->rot.y = boat_item->rot.y; lara_item->rot.z = boat_item->rot.z; Room_TestTriggers(lara_item); Room_TestTriggers(boat_item); sector = Room_GetSector( (XYZ_32) { lara_item->pos.x, lara_item->pos.y + BOAT_SHIFT_Y, lara_item->pos.z, }, &room_num); Item_UpdateRoom(lara->item_num, room_num); Item_Animate(lara_item); if (lara_item->hit_points > 0) { const int16_t lara_anim_num = Item_GetRelativeObjAnim(lara_item, O_LARA_BOAT); const int16_t lara_frame_num = Item_GetRelativeFrame(lara_item); Item_SwitchToAnim(boat_item, lara_anim_num, lara_frame_num); } g_Camera.target_elevation = -20 * DEG_1; g_Camera.target_distance = 2 * WALL_L; } else { Item_UpdateRoom(item_num, room_num); boat_item->rot.z += p->tilt_angle; } const int32_t pitch = water_height - ceiling < BOAT_SOUND_CEILING ? boat_item->speed * (water_height - ceiling) / BOAT_SOUND_CEILING : boat_item->speed; p->pitch += ((pitch - p->pitch) >> 2); if (boat_item->speed != 0 && water_height + BOAT_SHIFT_Y != boat_item->pos.y) { Sound_Effect(SFX_BOAT_ENGINE, &boat_item->pos, SPM_NORMAL); } else if (boat_item->speed > 20) { Sound_Effect( SFX_BOAT_MOVING, &boat_item->pos, SPM_PITCH | ((0x10000 - (BOAT_MAX_SPEED - p->pitch) * 100) << 8)); } else if (drive) { Sound_Effect( SFX_BOAT_IDLE, &boat_item->pos, SPM_PITCH | ((0x10000 - (BOAT_MAX_SPEED - p->pitch) * 100) << 8)); } if (boat_item->speed && water_height + BOAT_SHIFT_Y == boat_item->pos.y) { M_DoWakeEffect(boat_item); } if (Lara_Vehicle_GetIndex() != item_num) { return; } if ((lara_item->current_anim_state == BOAT_STATE_JUMP_R || lara_item->current_anim_state == BOAT_STATE_JUMP_L) && Item_TestFrameEqual(lara_item, LF_BOAT_EXIT_END)) { if (lara_item->current_anim_state == BOAT_STATE_JUMP_L) { lara_item->rot.y -= DEG_90; } else { lara_item->rot.y += DEG_90; } Item_SwitchToAnim(lara_item, LA(LA_JUMP_FORWARD), 0); lara_item->goal_anim_state = LS(LS_JUMP_FORWARD); lara_item->current_anim_state = LS(LS_JUMP_FORWARD); lara_item->gravity = true; lara_item->rot.x = 0; lara_item->rot.z = 0; lara_item->speed = 20; lara_item->fall_speed = -40; Lara_Vehicle_SetIndex(NO_ITEM); const XYZ_32 pos = { .x = lara_item->pos.x + ((360 * Math_Sin(lara_item->rot.y)) >> W2V_SHIFT), .y = lara_item->pos.y - 90, .z = lara_item->pos.z + ((360 * Math_Cos(lara_item->rot.y)) >> W2V_SHIFT), }; room_num = lara_item->room_num; sector = Room_GetSector(pos, &room_num); if (Room_GetHeight(sector, pos) >= pos.y - STEP_L) { lara_item->pos.x = pos.x; lara_item->pos.z = pos.z; Item_UpdateRoom(lara->item_num, room_num); } lara_item->pos.y = pos.y; Item_SwitchToAnim(boat_item, 0, 0); } } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = M_Collision; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->save_position = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_BOAT, M_Setup) ================================================ FILE: src/trx/game/objects/vehicles/common.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef enum { LA_VEHICLE_HIT_LEFT = 11, LA_VEHICLE_HIT_RIGHT = 12, LA_VEHICLE_HIT_FRONT = 13, LA_VEHICLE_HIT_BACK = 14, } LARA_ANIM_VEHICLE; int32_t Vehicle_DoShift( ITEM *const vehicle, const XYZ_32 *const pos, const XYZ_32 *const old) { int32_t x = pos->x >> WALL_SHIFT; int32_t z = pos->z >> WALL_SHIFT; const int32_t old_x = old->x >> WALL_SHIFT; const int32_t old_z = old->z >> WALL_SHIFT; const int32_t shift_x = pos->x & (WALL_L - 1); const int32_t shift_z = pos->z & (WALL_L - 1); if (x == old_x) { if (z == old_z) { vehicle->pos.x += old->x - pos->x; vehicle->pos.z += old->z - pos->z; } else if (z > old_z) { vehicle->pos.z -= shift_z + 1; return pos->x - vehicle->pos.x; } else { vehicle->pos.z += WALL_L - shift_z; return vehicle->pos.x - pos->x; } } else if (z == old_z) { if (x > old_x) { vehicle->pos.x -= shift_x + 1; return vehicle->pos.z - pos->z; } else { vehicle->pos.x += WALL_L - shift_x; return pos->z - vehicle->pos.z; } } else { int16_t room_num; const SECTOR *sector; int32_t height; x = 0; z = 0; XYZ_32 test_pos = (XYZ_32) { old->x, pos->y, pos->z }; room_num = vehicle->room_num; sector = Room_GetSector(test_pos, &room_num); height = Room_GetHeight(sector, test_pos); if (height < old->y - STEP_L) { if (pos->z > old->z) { z = -shift_z - 1; } else { z = WALL_L - shift_z; } } test_pos = (XYZ_32) { pos->x, pos->y, old->z }; room_num = vehicle->room_num; sector = Room_GetSector(test_pos, &room_num); height = Room_GetHeight(sector, test_pos); if (height < old->y - STEP_L) { if (pos->x > old->x) { x = -shift_x - 1; } else { x = WALL_L - shift_x; } } if (x != 0 && z != 0) { vehicle->pos.x += x; vehicle->pos.z += z; } else if (z != 0) { vehicle->pos.z += z; if (z > 0) { return vehicle->pos.x - pos->x; } else { return pos->x - vehicle->pos.x; } } else if (x != 0) { vehicle->pos.x += x; if (x > 0) { return pos->z - vehicle->pos.z; } else { return vehicle->pos.z - pos->z; } } else { vehicle->pos.x += old->x - pos->x; vehicle->pos.z += old->z - pos->z; } } return 0; } int32_t Vehicle_GetCollisionAnim(const ITEM *const vehicle, XYZ_32 *const moved) { moved->x = vehicle->pos.x - moved->x; moved->z = vehicle->pos.z - moved->z; if (moved->x != 0 || moved->z != 0) { const int32_t c = Math_Cos(vehicle->rot.y); const int32_t s = Math_Sin(vehicle->rot.y); const int32_t front = (moved->x * s + moved->z * c) >> W2V_SHIFT; const int32_t side = (moved->x * c - moved->z * s) >> W2V_SHIFT; if (ABS(front) > ABS(side)) { if (front > 0) { return LA_VEHICLE_HIT_BACK; } else { return LA_VEHICLE_HIT_FRONT; } } else { if (side > 0) { return LA_VEHICLE_HIT_LEFT; } else { return LA_VEHICLE_HIT_RIGHT; } } } return 0; } ================================================ FILE: src/trx/game/objects/vehicles/common.h ================================================ #pragma once #include int32_t Vehicle_DoShift(ITEM *vehicle, const XYZ_32 *pos, const XYZ_32 *old); int32_t Vehicle_GetCollisionAnim(const ITEM *vehicle, XYZ_32 *moved); ================================================ FILE: src/trx/game/objects/vehicles/kayak.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef enum { M_STATE_BACK = 0, M_STATE_POSE = 1, M_STATE_LEFT = 2, M_STATE_RIGHT = 3, M_STATE_CLIMB_IN = 4, M_STATE_DEATH_IN = 5, M_STATE_FORWARD = 6, M_STATE_ROLL = 7, M_STATE_DROWN_IN = 8, M_STATE_JUMP_OUT = 9, M_STATE_TURN_L = 10, M_STATE_TURN_R = 11, M_STATE_CLIMB_IN_R = 12, M_STATE_CLIMB_OUT_L = 13, M_STATE_CLIMB_OUT_R = 14, } M_STATE; typedef struct { struct { bool frame2_latched; uint8_t stroke_count; bool equipped; } paddle; int32_t vel; int32_t rot; int32_t fall_speed_f; int32_t fall_speed_l; int32_t fall_speed_r; int32_t water; XYZ_32 old_pos; bool turn; bool forward; bool true_water; uint8_t counter; } M_PRIV; // clang-format off // Hidden while mounting/in kayak to prevent clipping through the hull. static const uint32_t m_KayakHiddenBodyMeshes = (1u << LM_HIPS) | (1u << LM_THIGH_L) | (1u << LM_CALF_L) | (1u << LM_FOOT_L) | (1u << LM_THIGH_R) | (1u << LM_CALF_R) | (1u << LM_FOOT_R); static const XZ_16 m_MistPos[10] = { { .x = 32, .z = 900 }, { .x = 96, .z = 750 }, { .x = 170, .z = 600 }, { .x = 220, .z = 450 }, { .x = 300, .z = 300 }, { .x = 400, .z = 150 }, { .x = 400, .z = 0 }, { .x = 300, .z = -150 }, { .x = 200, .z = -300 }, { .x = 64, .z = -450 }, }; // clang-format on static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "vel", &p->vel)); JSON_SHOULD(JSON_READ(io, "rot", &p->rot)); JSON_SHOULD(JSON_READ(io, "fall_speed_f", &p->fall_speed_f)); JSON_SHOULD(JSON_READ(io, "fall_speed_l", &p->fall_speed_l)); JSON_SHOULD(JSON_READ(io, "fall_speed_r", &p->fall_speed_r)); JSON_SHOULD(JSON_READ(io, "water", &p->water)); JSON_SHOULD(JSON_READ(io, "old_pos", &p->old_pos)); JSON_SHOULD(JSON_READ(io, "turn", &p->turn)); JSON_SHOULD(JSON_READ(io, "forward", &p->forward)); JSON_SHOULD(JSON_READ(io, "true_water", &p->true_water)); JSON_SHOULD(JSON_READ(io, "counter", &p->counter)); JSON_SHOULD( JSON_READ(io, "paddle_frame2_latched", &p->paddle.frame2_latched)); JSON_SHOULD(JSON_READ(io, "paddle_stroke_count", &p->paddle.stroke_count)); JSON_SHOULD(JSON_READ(io, "paddle_equipped", &p->paddle.equipped)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "vel", p->vel); JSONW_WRITE(io, "rot", p->rot); JSONW_WRITE(io, "fall_speed_f", p->fall_speed_f); JSONW_WRITE(io, "fall_speed_l", p->fall_speed_l); JSONW_WRITE(io, "fall_speed_r", p->fall_speed_r); JSONW_WRITE(io, "water", p->water); JSONW_WRITE(io, "old_pos", p->old_pos); JSONW_WRITE(io, "turn", p->turn); JSONW_WRITE(io, "forward", p->forward); JSONW_WRITE(io, "true_water", p->true_water); JSONW_WRITE(io, "counter", p->counter); JSONW_WRITE(io, "paddle_frame2_latched", p->paddle.frame2_latched); JSONW_WRITE(io, "paddle_stroke_count", p->paddle.stroke_count); JSONW_WRITE(io, "paddle_equipped", p->paddle.equipped); } static void M_Initialise(int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; p->rot = 0; p->vel = 0; p->fall_speed_r = 0; p->fall_speed_l = 0; p->fall_speed_f = 0; p->old_pos = item->pos; p->paddle.equipped = false; FX_Wake_Reset(); } static int32_t M_GetInKayak(const int16_t item_num, const COLL_INFO *const coll) { const LARA_INFO *const lara_info = Lara_GetLaraInfo(); const ITEM *const lara_item = Lara_GetItem(); if (!g_Input.action || lara_info->gun_status != LGS_ARMLESS || lara_item->gravity) { return 0; } const ITEM *const item = Item_Get(item_num); const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; const int32_t dist = SQUARE(dx) + SQUARE(dz); if (dist > 130000) { return 0; } int16_t room_num = item->room_num; const SECTOR *const floor = Room_GetSector(item->pos, &room_num); const int32_t h = Room_GetHeight(floor, item->pos); if (h <= -32000) { return 0; } const int16_t ang = Math_Atan( item->pos.z - lara_item->pos.z, item->pos.x - lara_item->pos.x) - item->rot.y; const uint16_t temp_ang = lara_item->rot.y - item->rot.y; if (ang > -45 * DEG_1 && ang < 135 * DEG_1) { if (temp_ang > 45 * DEG_1 && temp_ang < 135 * DEG_1) { return -1; } } else { if (temp_ang > 225 * DEG_1 && temp_ang < 315 * DEG_1) { return 1; } } return 0; } static void M_Collision( const int16_t item_num, ITEM *const l, COLL_INFO *const coll) { if (l->hit_points < 0 || Lara_Vehicle_GetIndex() != NO_ITEM) { return; } const int32_t lr = M_GetInKayak(item_num, coll); if (lr == 0) { coll->enable_baddie_push = 1; Object_Collision(item_num, l, coll); return; } Lara_Vehicle_SetIndex(item_num); ITEM *const item = Item_Get(item_num); LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (lara_info->gun_type == LGT_FLARE) { Lara_Flare_Dispose(false); lara_info->flare.control = false; lara_info->gun_type = LGT_UNARMED; lara_info->request_gun_type = LGT_UNARMED; } if (lr > 0) { Item_SwitchToObjAnim(l, 3, 0, O_LARA_VEHICLE_ANIM); } else { Item_SwitchToObjAnim(l, 28, 0, O_LARA_VEHICLE_ANIM); } l->current_anim_state = M_STATE_CLIMB_IN; l->goal_anim_state = M_STATE_CLIMB_IN; lara_info->water_status = LWS_ABOVE_WATER; l->pos = item->pos; l->rot.x = 0; l->rot.y = item->rot.y; l->rot.z = 0; l->gravity = false; l->speed = 0; l->fall_speed = 0; if (l->room_num != item->room_num) { Item_UpdateRoom(lara_info->item_num, item->room_num); } M_PRIV *p = item->priv; p->water = item->pos.y; p->paddle.equipped = false; } static void M_DoRipple( const ITEM *const item, const int16_t x_offset, const int16_t z_offset) { XYZ_32 pos = item->pos; pos = XYZ_32_OffsetYaw(pos, item->rot.y, z_offset); pos = XYZ_32_OffsetYaw(pos, item->rot.y + DEG_90, x_offset); int16_t room_num = item->room_num; Room_GetSector(pos, &room_num); const int32_t wh = Room_GetWaterHeight(pos, room_num); if (wh == NO_HEIGHT) { return; } FX_WATER_RIPPLE *const ripple = FX_Water_SetupRipple( pos.x, pos.y, pos.z, -2 - (Random_GetControl() & 1), 0); if (ripple != nullptr) { ripple->init = 0; } } static int32_t M_TestHeight( const ITEM *const item, const int32_t x, const int32_t z, XYZ_32 *const pos) { const int32_t zs = Math_Sin(item->rot.z); const int32_t xs = Math_Sin(item->rot.x); *pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, z); *pos = XYZ_32_OffsetYaw(*pos, item->rot.y + DEG_90, x); pos->y = (item->pos.y + ((x * zs) >> W2V_SHIFT)) - ((z * xs) >> W2V_SHIFT); int16_t room_num = item->room_num; Room_GetSector(*pos, &room_num); int32_t h = Room_GetWaterHeight(*pos, room_num); if (h == NO_HEIGHT) { room_num = item->room_num; SECTOR *const floor = Room_GetSector(*pos, &room_num); h = Room_GetHeight(floor, *pos); if (h == NO_HEIGHT) { return h; } } return h - 5; } static bool M_CanGetOut(const ITEM *const item, const int32_t lr) { XYZ_32 pos; const int32_t h = M_TestHeight(item, lr >= 0 ? 768 : -768, 0, &pos); return item->pos.y - h <= 0; } static void M_KayakUserInput(ITEM *const item, ITEM *const l, M_PRIV *const p) { LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (l->hit_points <= 0 && l->current_anim_state != M_STATE_DEATH_IN) { Item_SwitchToObjAnim(l, 5, 0, O_LARA_VEHICLE_ANIM); l->current_anim_state = M_STATE_DEATH_IN; l->goal_anim_state = M_STATE_DEATH_IN; } const int16_t frame = Item_GetRelativeFrame(l); const int32_t time4 = Output_GetTimeInGame() * 4; switch (l->current_anim_state) { case M_STATE_BACK: if (!(g_Input.back)) { l->goal_anim_state = M_STATE_POSE; } if (l->anim_num - Object_Get(O_LARA_VEHICLE_ANIM)->anim_idx == 2) { if (frame == 8) { p->rot += 0x800000; p->vel -= 0x180000; } if (frame == 31) { p->rot -= 0x800000; p->vel -= 0x180000; } if (frame < 15 && (frame & 1) != 0) { M_DoRipple(item, 384, -128); } else if (frame >= 20 && frame <= 34 && frame & 1) { M_DoRipple(item, -384, -128); } } break; case M_STATE_POSE: if (g_Input.roll && lara_info->current.active == 0 && lara_info->current.vel.x == 0 && lara_info->current.vel.z == 0) { if (g_Input.left && M_CanGetOut(item, -1)) { l->goal_anim_state = M_STATE_JUMP_OUT; l->required_anim_state = M_STATE_CLIMB_OUT_L; } else if (g_Input.right && M_CanGetOut(item, 1)) { l->goal_anim_state = M_STATE_JUMP_OUT; l->required_anim_state = M_STATE_CLIMB_OUT_R; } } else if (g_Input.forward) { l->goal_anim_state = M_STATE_RIGHT; p->turn = false; p->forward = true; } else if (g_Input.back) { l->goal_anim_state = M_STATE_BACK; } else if (g_Input.left) { l->goal_anim_state = M_STATE_LEFT; if (p->vel) { p->turn = false; } else { p->turn = true; } p->forward = false; } else if (g_Input.right) { l->goal_anim_state = M_STATE_RIGHT; if (p->vel) { p->turn = false; } else { p->turn = true; } p->forward = false; } else if ( g_Input.step_left && (p->vel || lara_info->current.vel.x || lara_info->current.vel.z)) { l->goal_anim_state = M_STATE_TURN_L; } else if ( g_Input.step_right && (p->vel || lara_info->current.vel.x || lara_info->current.vel.z)) { l->goal_anim_state = M_STATE_TURN_R; } break; case M_STATE_LEFT: if (!p->forward) { if (!g_Input.left) { l->goal_anim_state = M_STATE_POSE; } } else { if (frame == 0) { p->paddle.frame2_latched = false; p->paddle.stroke_count = 0; } if (frame == 2 && !p->paddle.frame2_latched) { p->paddle.frame2_latched = true; p->paddle.stroke_count++; } else if (frame > 2) { p->paddle.frame2_latched = false; } if (!g_Input.forward) { l->goal_anim_state = M_STATE_POSE; } else if (!g_Input.left) { l->goal_anim_state = M_STATE_RIGHT; } else if (p->paddle.stroke_count >= 2) { l->goal_anim_state = M_STATE_RIGHT; } } if (frame == 7) { if (p->forward) { p->rot -= 0x800000; CLAMPL(p->rot, -0x1000000); p->vel += 0x180000; } else if (p->turn) { p->rot -= 0x1000000; CLAMPL(p->rot, -0x1000000); } else { p->rot -= 0xC00000; CLAMPL(p->rot, -0xC00000); p->vel += 0x100000; } } if (frame > 6 && frame < 24 && frame & 1) { M_DoRipple(item, -384, -64); } break; case M_STATE_RIGHT: if (!p->forward) { if (!g_Input.right) { l->goal_anim_state = M_STATE_POSE; } } else { if (frame == 0) { p->paddle.frame2_latched = false; p->paddle.stroke_count = 0; } if (frame == 2 && !p->paddle.frame2_latched) { p->paddle.frame2_latched = true; p->paddle.stroke_count++; } else if (frame > 2) { p->paddle.frame2_latched = false; } if (!g_Input.forward) { l->goal_anim_state = M_STATE_POSE; } else if (!g_Input.right) { l->goal_anim_state = M_STATE_LEFT; } else if (p->paddle.stroke_count >= 2) { l->goal_anim_state = M_STATE_LEFT; } } if (frame == 7) { if (p->forward) { p->rot += 0x800000; CLAMPG(p->rot, 0x1000000); p->vel += 0x180000; } else if (p->turn) { p->rot += 0x1000000; CLAMPG(p->rot, 0x1000000); } else { p->rot += 0xC00000; CLAMPG(p->rot, 0xC00000); p->vel += 0x100000; } } if (frame > 6 && frame < 24 && frame & 1) { M_DoRipple(item, 384, -64); } break; case M_STATE_CLIMB_IN: if (l->anim_num == Object_Get(O_LARA_VEHICLE_ANIM)->anim_idx + 4 && frame == 24 && !p->paddle.equipped) { Lara_Skin_SetExtraEquipment(LM_HAND_R, EXTRA_MESH_OAR); Item_SetMeshVisibleMask(l, m_KayakHiddenBodyMeshes, false); p->paddle.equipped = true; } break; case M_STATE_JUMP_OUT: if (l->anim_num == Object_Get(O_LARA_VEHICLE_ANIM)->anim_idx + 14 && frame == 27 && p->paddle.equipped) { Lara_Skin_ClearEquipment(LM_HAND_R); Item_SetMeshVisibleMask(l, m_KayakHiddenBodyMeshes, true); p->paddle.equipped = false; } l->goal_anim_state = l->required_anim_state; break; case M_STATE_TURN_L: if (!g_Input.step_left || (!p->vel && !lara_info->current.vel.x && !lara_info->current.vel.z)) { l->goal_anim_state = M_STATE_POSE; } else if ( l->anim_num - Object_Get(O_LARA_VEHICLE_ANIM)->anim_idx == 26) { if (p->vel >= 0) { p->rot -= 0x200000; CLAMPL(p->rot, -0x1000000); p->vel -= 0x8000; CLAMPL(p->vel, 0); } if (p->vel < 0) { p->vel += 0x8000; p->rot += 0x200000; CLAMPG(p->vel, 0); } if (!(time4 & 3)) { M_DoRipple(item, -256, -256); } } break; case M_STATE_TURN_R: if (!g_Input.step_right || (!p->vel && !lara_info->current.vel.x && !lara_info->current.vel.z)) { l->goal_anim_state = M_STATE_POSE; } else if ( l->anim_num - Object_Get(O_LARA_VEHICLE_ANIM)->anim_idx == 27) { if (p->vel >= 0) { p->rot += 0x200000; CLAMPG(p->rot, 0x1000000); p->vel -= 0x8000; CLAMPL(p->vel, 0); } if (p->vel < 0) { p->vel += 0x8000; p->rot -= 0x200000; CLAMPG(p->vel, 0); } if (!(time4 & 3)) { M_DoRipple(item, 256, -256); } } break; case M_STATE_CLIMB_OUT_L: if (l->anim_num == Object_Get(O_LARA_VEHICLE_ANIM)->anim_idx + 24 && frame == 83) { XYZ_32 pos = { .x = 0, .y = 350, .z = 500 }; Lara_GetMeshPos(LM_HIPS, &pos); l->pos = pos; l->rot.x = 0; l->rot.y = item->rot.y - 0x4000; l->rot.z = 0; Item_SwitchToAnim(l, LA(LA_FREEFALL), 0); l->current_anim_state = LS_FAST_FALL; l->goal_anim_state = LS_FAST_FALL; l->gravity = true; l->fall_speed = 0; lara_info->gun_status = LGS_ARMLESS; Lara_Vehicle_SetIndex(NO_ITEM); } break; case M_STATE_CLIMB_OUT_R: if (l->anim_num == Object_Get(O_LARA_VEHICLE_ANIM)->anim_idx + 32 && frame == 83) { XYZ_32 pos = { .x = 0, .y = 350, .z = 500 }; Lara_GetMeshPos(LM_HIPS, &pos); l->pos = pos; l->rot.x = 0; l->rot.y = item->rot.y + 0x4000; l->rot.z = 0; Item_SwitchToAnim(l, LA(LA_FREEFALL), 0); l->current_anim_state = LS_FAST_FALL; l->goal_anim_state = LS_FAST_FALL; l->gravity = true; l->fall_speed = 0; lara_info->gun_status = LGS_ARMLESS; Lara_Vehicle_SetIndex(NO_ITEM); } break; } if (p->vel > 0) { p->vel -= 0x8000; CLAMPL(p->vel, 0); } else if (p->vel < 0) { p->vel += 0x8000; CLAMPG(p->vel, 0); } CLAMP(p->vel, -0x380000, 0x380000); item->speed = p->vel >> 16; if (p->rot < 0) { p->rot += 0x50000; CLAMPG(p->rot, 0); } else { p->rot -= 0x50000; CLAMPL(p->rot, 0); } } static void M_DoCurrent(ITEM *const item) { LARA_INFO *const lara_info = Lara_GetLaraInfo(); const ITEM *const lara_item = Lara_GetItem(); if (lara_info->current.active != 0) { const OBJECT_VECTOR *const sink = Camera_GetFixedObject(lara_info->current.active - 1); const int32_t speed = sink->data; const int32_t angle = -Math_Atan(lara_item->pos.x - sink->x, lara_item->pos.z - sink->z) - DEG_90; const int32_t scaled_speed = speed << (W2V_SHIFT - 4); const XYZ_32 target_vel = XYZ_32_OffsetYaw((XYZ_32) {}, angle, scaled_speed); lara_info->current.vel.x += (target_vel.x - lara_info->current.vel.x) >> 4; lara_info->current.vel.z += (target_vel.z - lara_info->current.vel.z) >> 4; } else { int32_t shifter; int32_t abs_vel; abs_vel = ABS(lara_info->current.vel.x); if (abs_vel > 16) { shifter = 4; } else if (abs_vel > 8) { shifter = 3; } else { shifter = 2; } lara_info->current.vel.x -= lara_info->current.vel.x >> shifter; if (ABS(lara_info->current.vel.x) < 4) { lara_info->current.vel.x = 0; } abs_vel = ABS(lara_info->current.vel.z); if (abs_vel > 16) { shifter = 4; } else if (abs_vel > 8) { shifter = 3; } else { shifter = 2; } lara_info->current.vel.z -= lara_info->current.vel.z >> shifter; if (ABS(lara_info->current.vel.z) < 4) { lara_info->current.vel.z = 0; } if (lara_info->current.vel.x == 0 && lara_info->current.vel.z == 0) { return; } } item->pos.x += lara_info->current.vel.x >> 8; item->pos.z += lara_info->current.vel.z >> 8; lara_info->current.active = 0; } static int32_t M_DoDynamics( const int32_t h, int32_t fall_speed, int32_t *const y) { if (h <= *y) { int32_t kick = (h - *y) << 2; CLAMPL(kick, -80); fall_speed += (kick - fall_speed) >> 3; CLAMPG(*y, h); } else { *y += fall_speed; if (*y <= h) { fall_speed += 6; } else { *y = h; fall_speed = 0; } } return fall_speed; } static int32_t M_DoShift( ITEM *const item, const XYZ_32 *const new_pos, XYZ_32 *const old_pos) { const int32_t new_sector_x = new_pos->x >> WALL_SHIFT; const int32_t new_sector_z = new_pos->z >> WALL_SHIFT; const int32_t old_sector_x = old_pos->x >> WALL_SHIFT; const int32_t old_sector_z = old_pos->z >> WALL_SHIFT; const int32_t sector_offset_x = new_pos->x - ROUND_TO_SECTOR(new_pos->x); const int32_t sector_offset_z = new_pos->z - ROUND_TO_SECTOR(new_pos->z); int32_t shift_x = 0; int32_t shift_z = 0; if (new_sector_x == old_sector_x) { if (new_sector_z == old_sector_z) { item->pos.z += old_pos->z - new_pos->z; item->pos.x += old_pos->x - new_pos->x; return 0; } else if (new_sector_z <= old_sector_z) { item->pos.z += WALL_L - sector_offset_z; return item->pos.x - new_pos->x; } else { item->pos.z -= 1 + sector_offset_z; return new_pos->x - item->pos.x; } } if (new_sector_z == old_sector_z) { if (new_sector_x <= old_sector_x) { item->pos.x += WALL_L - sector_offset_x; return new_pos->z - item->pos.z; } else { item->pos.x -= 1 + sector_offset_x; return item->pos.z - new_pos->z; } } int16_t room_num = item->room_num; XYZ_32 sample_pos = { old_pos->x, new_pos->y, new_pos->z }; const SECTOR *floor = Room_GetSector(sample_pos, &room_num); int32_t height = Room_GetHeight(floor, sample_pos); if (height < old_pos->y - 256) { if (new_pos->z > old_pos->z) { shift_z = -1 - sector_offset_z; } else { shift_z = WALL_L - sector_offset_z; } } room_num = item->room_num; sample_pos = (XYZ_32) { new_pos->x, new_pos->y, old_pos->z }; floor = Room_GetSector(sample_pos, &room_num); height = Room_GetHeight(floor, sample_pos); if (height < old_pos->y - 256) { if (new_pos->x > old_pos->x) { shift_x = -1 - sector_offset_x; } else { shift_x = WALL_L - sector_offset_x; } } if (shift_x != 0 && shift_z != 0) { item->pos.x += shift_x; item->pos.z += shift_z; return 0; } if (shift_z != 0) { item->pos.z += shift_z; if (shift_z > 0) { return item->pos.x - new_pos->x; } else { return new_pos->x - item->pos.x; } } if (shift_x != 0) { item->pos.x += shift_x; if (shift_x > 0) { return new_pos->z - item->pos.z; } else { return item->pos.z - new_pos->z; } } item->pos.x += old_pos->x - new_pos->x; item->pos.z += old_pos->z - new_pos->z; return 0; } static int32_t M_GetCollisionAnim(const ITEM *const item, int32_t x, int32_t z) { x = item->pos.x - x; z = item->pos.z - z; if (x == 0 && z == 0) { return 0; } const int32_t s = Math_Sin(item->rot.y); const int32_t c = Math_Cos(item->rot.y); const int32_t front = (x * s + z * c) >> W2V_SHIFT; const int32_t side = (x * c - z * s) >> W2V_SHIFT; if (ABS(front) <= ABS(side)) { if (side > 0) { return 3; } else { return 4; } } else { if (front > 0) { return 1; } else { return 2; } } } static void M_KayakToBackground(ITEM *const item, M_PRIV *const p) { int32_t heights[8]; XYZ_32 old_pos[9]; p->old_pos = item->pos; heights[0] = M_TestHeight(item, 0, 1024, old_pos); heights[1] = M_TestHeight(item, -96, 512, &old_pos[1]); heights[2] = M_TestHeight(item, 96, 512, &old_pos[2]); heights[3] = M_TestHeight(item, -128, 128, &old_pos[3]); heights[4] = M_TestHeight(item, 128, 128, &old_pos[4]); heights[5] = M_TestHeight(item, -128, -320, &old_pos[5]); heights[6] = M_TestHeight(item, 128, -320, &old_pos[6]); heights[7] = M_TestHeight(item, 0, -640, &old_pos[7]); for (int32_t i = 0; i < 8; i++) { CLAMPG(old_pos[i].y, heights[i]); } old_pos[8] = item->pos; XYZ_32 front_pos; XYZ_32 left_pos; XYZ_32 right_pos; const int32_t front = M_TestHeight(item, 0, 1024, &front_pos); const int32_t left = M_TestHeight(item, -128, 128, &left_pos); const int32_t right = M_TestHeight(item, 128, 128, &right_pos); item->rot.y += p->rot >> 16; const XYZ_32 moved_pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, item->speed); item->pos.x = moved_pos.x; item->pos.z = moved_pos.z; M_DoCurrent(item); p->fall_speed_l = M_DoDynamics(left, p->fall_speed_l, &left_pos.y); p->fall_speed_r = M_DoDynamics(right, p->fall_speed_r, &right_pos.y); p->fall_speed_f = M_DoDynamics(front, p->fall_speed_f, &front_pos.y); item->fall_speed = M_DoDynamics(p->water, item->fall_speed, &item->pos.y); const int32_t pitch_diff = (right_pos.y + left_pos.y) >> 1; const int16_t x_rot = Math_Atan(1024, item->pos.y - front_pos.y); const int16_t z_rot = Math_Atan(128, pitch_diff - left_pos.y); item->rot.x = x_rot; item->rot.z = z_rot; const int32_t old_x = item->pos.x; const int32_t old_z = item->pos.z; XYZ_32 pos; int32_t rot = 0; int32_t h = M_TestHeight(item, 0, -640, &pos); if (h < old_pos[7].y - 64) { rot = M_DoShift(item, &pos, &old_pos[7]); } h = M_TestHeight(item, 128, -320, &pos); if (h < old_pos[6].y - 64) { rot += M_DoShift(item, &pos, &old_pos[6]); } h = M_TestHeight(item, -128, -320, &pos); if (h < old_pos[5].y - 64) { rot += M_DoShift(item, &pos, &old_pos[5]); } h = M_TestHeight(item, 128, 128, &pos); if (h < old_pos[4].y - 64) { rot += M_DoShift(item, &pos, &old_pos[4]); } h = M_TestHeight(item, -128, 128, &pos); if (h < old_pos[3].y - 64) { rot += M_DoShift(item, &pos, &old_pos[3]); } h = M_TestHeight(item, 96, 512, &pos); if (h < old_pos[2].y - 64) { rot += M_DoShift(item, &pos, &old_pos[2]); } h = M_TestHeight(item, -96, 512, &pos); if (h < old_pos[1].y - 64) { rot += M_DoShift(item, &pos, &old_pos[1]); } h = M_TestHeight(item, 0, 1024, &pos); if (h < old_pos[0].y - 64) { rot += M_DoShift(item, &pos, &old_pos[0]); } item->rot.y += rot; int16_t room_num = item->room_num; const SECTOR *floor = Room_GetSector(item->pos, &room_num); h = Room_GetWaterHeight(item->pos, room_num); if (h == NO_HEIGHT) { h = Room_GetHeight(floor, item->pos); } if (h < item->pos.y - 64) { h = M_DoShift(item, (XYZ_32 *)&item->pos, &old_pos[8]); } room_num = item->room_num; floor = Room_GetSector(item->pos, &room_num); h = Room_GetWaterHeight(item->pos, room_num); if (h == NO_HEIGHT) { h = Room_GetHeight(floor, item->pos); if (h == NO_HEIGHT) { GAME_VECTOR reset_pos = { .pos = p->old_pos, .room_num = item->room_num, }; Camera_Collide(&reset_pos, 256, 0); item->pos = reset_pos.pos; item->room_num = reset_pos.room_num; } } if (M_GetCollisionAnim(item, old_x, old_z)) { const int32_t sin_y = Math_Sin(item->rot.y); const int32_t cos_y = Math_Cos(item->rot.y); const int32_t dx = item->pos.x - old_pos[8].x; const int32_t dz = item->pos.z - old_pos[8].z; int32_t speed = (dx * sin_y + dz * cos_y) >> W2V_SHIFT; speed <<= 8; if ((p->vel > 0 && speed < p->vel) || (p->vel < 0 && speed > p->vel)) { p->vel = speed; } CLAMPL(p->vel, -0x380000); } } static void M_KayakSplash( const ITEM *const item, const int32_t fall_speed, const int32_t water) { if (water == NO_HEIGHT) { return; } FX_WATER_SPLASH_SETUP splash_setup = { .x = item->pos.x, .y = item->pos.y, .z = item->pos.z, .inner_xz_off = 128, .inner_xz_size = 48, .inner_y_size = -384, .inner_xz_vel = 160, .inner_y_vel = (-fall_speed << 5), .inner_gravity = 128, .inner_friction = 7, .middle_xz_off = 192, .middle_xz_size = 96, .middle_y_size = -256, .middle_xz_vel = 224, .middle_y_vel = (-fall_speed << 4), .middle_gravity = 72, .middle_friction = 8, .outer_xz_off = 256, .outer_xz_size = 128, .outer_xz_vel = 272, .outer_friction = -9, }; FX_Water_SetupSplash(&splash_setup); } static void M_DoWake( const ITEM *const item, const int32_t xoff, const int32_t zoff, const int16_t rotate) { int16_t angle1; int16_t angle2; XYZ_32 pos; const int32_t start_idx = FX_Wake_GetStartIndex(); if (FX_Wake_GetPoint(start_idx, rotate)->life != 0) { return; } pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, zoff); pos = XYZ_32_OffsetYaw(pos, item->rot.y + DEG_90, xoff); int16_t room_num = item->room_num; Room_GetSector(pos, &room_num); const int32_t wh = Room_GetWaterHeight(pos, room_num); if (wh == NO_HEIGHT) { return; } if (item->speed >= 0) { if (rotate) { angle1 = item->rot.y + 30940; angle2 = item->rot.y + 27300; } else { angle1 = item->rot.y - 30940; angle2 = item->rot.y - 27300; } } else { if (rotate) { angle1 = item->rot.y + 1820; angle2 = item->rot.y + 5460; } else { angle1 = item->rot.y - 1820; angle2 = item->rot.y - 5460; } } XYZ_32 vel[2] = { XYZ_32_OffsetYaw((XYZ_32) {}, angle1, 4), XYZ_32_OffsetYaw((XYZ_32) {}, angle2, 6), }; FX_WAKE_POINT *const pt = FX_Wake_GetPoint(start_idx, rotate); pt->life = 64; for (int32_t i = 0; i < 2; i++) { pt->pos[i].x = pos.x; pt->pos[i].y = item->pos.y + 32; pt->pos[i].z = pos.z; pt->prev_pos[i] = pt->pos[i]; pt->vel[i].x = vel[i].x; pt->vel[i].z = vel[i].z; } if (rotate == 1) { FX_Wake_AdvanceStartIndex(); } } static void M_TriggerRapidsMist(const XYZ_32 pos) { SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 128; spark->src_color.g = 128; spark->src_color.b = 128; spark->dst_color.r = 192; spark->dst_color.g = 192; spark->dst_color.b = 192; spark->col_fade_speed = 2; spark->fade_to_black = 4; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->life = (Random_GetControl() & 3) + 6; spark->s_life = spark->life; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 0xF) - 8; spark->pos.y = pos.y + (Random_GetControl() & 0xF) - 8; spark->pos.z = pos.z + (Random_GetControl() & 0xF) - 8; spark->vel.x = (Random_GetControl() & 0xFF) - 128; spark->vel.y = (Random_GetControl() & 0xFF) - 128; spark->vel.z = (Random_GetControl() & 0xFF) - 128; spark->friction = 3; if (Random_GetControl() & 1) { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = Random_GetControl() & 0xFFF; if (Random_GetControl() & 1) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = (Random_GetControl() & 0xF) + 16; } } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->scalar = 4; spark->sprite_idx = (uint8_t)Object_Get(O_EXPLOSION_1)->mesh_idx; spark->gravity = 0; spark->max_y_vel = 0; spark->dst_size.width = (Random_GetControl() & 7) + 16; spark->src_size.width = spark->dst_size.width >> 1; spark->size.width = spark->src_size.width; spark->src_size.height = spark->src_size.width; spark->size.height = spark->src_size.width; spark->dst_size.height = spark->dst_size.width; Sparks_FinishSetup(spark); } static void M_KayakToBaddieCollision(const ITEM *const p) { const ITEM *const lara_item = Lara_GetItem(); int16_t nearby_rooms[20] = { p->room_num }; int16_t nearby_room_count = 1; const PORTALS *const portals = Room_Get(p->room_num)->portals; if (portals != nullptr) { for (int32_t i = 0; i < portals->count && nearby_room_count < 20; i++) { nearby_rooms[nearby_room_count] = portals->portal[i].room_num; nearby_room_count++; } } for (int32_t i = 0; i < nearby_room_count; i++) { const ITEM *item; for (int16_t item_num = Room_Get(nearby_rooms[i])->item_num; item_num != NO_ITEM; item_num = item->next_item) { item = Item_Get(item_num); if (!item->collidable || item->status == IS_INVISIBLE) { continue; } const OBJECT_ID obj_num = item->object_id; const OBJECT *const obj = Object_Get(obj_num); if (obj->collision_func == nullptr) { continue; } const bool is_hazard = obj_num == O_SPIKES || obj_num == O_DART || obj_num == O_TEETH_TRAP || (obj_num == O_BLADE && item->current_anim_state != 1) || (obj_num == O_ICICLE && item->current_anim_state != 3); if (!is_hazard) { continue; } const int32_t dx = p->pos.x - item->pos.x; const int32_t dy = p->pos.y - item->pos.y; const int32_t dz = p->pos.z - item->pos.z; if (dx <= -2048 || dx >= 2048 || dz <= -2048 || dz >= 2048 || dy <= -2048 || dy >= 2048) { continue; } if (!Item_TestBoundsCollide(item, p, 256)) { continue; } Spawn_BloodBath( lara_item->pos.x, lara_item->pos.y - 256, lara_item->pos.z, p->speed, p->rot.y, lara_item->room_num, 3); Lara_TakeDamage(5, false); } } } static bool M_Draw(const ITEM *const item) { ((ITEM *)item)->pos.y += 32; Object_DrawAnimatingItem(item); ((ITEM *)item)->pos.y -= 32; FX_Wake_Draw(item); return true; } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->collision_func = M_Collision; obj->draw_func = M_Draw; obj->save_position = true; obj->save_flags = true; obj->save_anim = true; } bool Kayak_Control(void) { LARA_INFO *const lara_info = Lara_GetLaraInfo(); ITEM *const lara_item = Lara_GetItem(); ITEM *const item = Lara_Vehicle_GetItem(); M_PRIV *const p = item->priv; const int32_t time4 = Output_GetTimeInGame() * 4; const LARA_SKIN_EQUIPMENT *const hand_r_equipment = Lara_Skin_GetEquipment(LM_HAND_R); if (p->paddle.equipped) { Lara_Skin_SetExtraEquipment(LM_HAND_R, EXTRA_MESH_OAR); } else if ( hand_r_equipment->type == EQUIPMENT_TYPE_EXTRA && hand_r_equipment->data == EXTRA_MESH_OAR) { Lara_Skin_ClearEquipment(LM_HAND_R); } if (g_Input.look) { Lara_Look_UpDown(); } const int32_t old_fall_speed = item->fall_speed; M_KayakUserInput(item, lara_item, p); M_KayakToBackground(item, p); int16_t room_num = item->room_num; const SECTOR *floor = Room_GetSector(item->pos, &room_num); int32_t h = Room_GetHeight(floor, item->pos); int32_t wh = Room_GetWaterHeight(item->pos, room_num); p->water = wh; if (wh == NO_HEIGHT) { wh = h; p->water = h; p->true_water = false; } else { p->true_water = true; p->water = wh - 5; } const int32_t damage = old_fall_speed - item->fall_speed; if (damage > 128 && !item->fall_speed && wh != NO_HEIGHT) { if (damage > 160) { Lara_TakeDamage((damage - 160) << 3, false); } M_KayakSplash(item, old_fall_speed - item->fall_speed, wh); } if (Lara_Vehicle_GetIndex() != NO_ITEM) { lara_item->pos.x = item->pos.x; lara_item->pos.y = item->pos.y + 32; lara_item->pos.z = item->pos.z; lara_item->rot.x = item->rot.x; lara_item->rot.y = item->rot.y; lara_item->rot.z = item->rot.z >> 1; Item_Animate(lara_item); item->anim_num = lara_item->anim_num + Object_Get(O_KAYAK)->anim_idx - Object_Get(O_LARA_VEHICLE_ANIM)->anim_idx; item->frame_num = lara_item->frame_num + Item_GetAnim(item)->frame_base - Item_GetAnim(lara_item)->frame_base; Item_UpdateRoom(Lara_Vehicle_GetIndex(), room_num); Item_UpdateRoom(lara_info->item_num, room_num); Room_TestTriggers(lara_item); Room_TestTriggers(item); g_Camera.target_elevation = -5460; g_Camera.target_distance = 2048; } if (!(time4 & 0xF) && p->true_water) { M_DoWake(item, -128, 0, 0); M_DoWake(item, 128, 0, 1); } if (time4 & 7 && !p->true_water && item->fall_speed < 20) { p->counter ^= 1; for (int32_t i = p->counter; i < 10; i += 2) { int32_t x; if (Random_GetControl() & 1) { x = m_MistPos[i].x >> 1; } else { x = -(m_MistPos[i].x >> 1); } const int32_t y = 50; const int32_t z = m_MistPos[i].z; Matrix_PushUnit(); Matrix_Rot16(item->rot); // NOTE: not part of the OG PC const XYZ_32 pos = Matrix_MulVec32_M(g_MatrixPtr, (XYZ_32) { x, y, z }); M_TriggerRapidsMist((XYZ_32) { .x = item->pos.x + pos.x, .y = item->pos.y + pos.y, .z = item->pos.z + pos.z, }); Matrix_Pop(); } } uint8_t wake_shade = FX_Wake_GetShade(); if (item->speed != 0 || lara_info->current.vel.x != 0 || lara_info->current.vel.z != 0) { if (wake_shade < 16) { wake_shade++; } } else if (wake_shade) { wake_shade--; } FX_Wake_SetShade(wake_shade); M_KayakToBaddieCollision(item); return Lara_Vehicle_GetIndex() != NO_ITEM; } REGISTER_OBJECT(O_KAYAK, M_Setup) ================================================ FILE: src/trx/game/objects/vehicles/kayak.h ================================================ #pragma once bool Kayak_Control(void); ================================================ FILE: src/trx/game/objects/vehicles/mine_cart.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include // clang-format off #define M_MOUNT_DIST 200000 #define M_DISMOUNT_DIST 330 #define M_MAX_COLL_ROOMS 12 #define M_MIN_BEAM_DAMAGE 20 #define M_MESH_FRAME 20 #define M_RADIUS STEP_L #define M_ANGLE_CLAMP (DEG_90 - 1) // = 16383 #define M_MIN_VELOCITY (STEP_L / 8) // = 32 #define M_MAX_VELOCITY (STEP_L * 63) // = 16128 #define M_MIN_SPEED (STEP_L * 10) // = 2560 #define M_BRAKE_SPEED (STEP_L * 6) // = 1536 #define M_STOP_SPEED (WALL_L * 60) // = 61440 #define M_GRAVITY (WALL_L + 1) // = 1025 #define M_TARGET_DIST (WALL_L * 2) // = 2048 #define M_MAX_GRADIENT (STEP_L / 2) // = 128 #define M_MAX_ROLL (DEG_90 / 4) // = 4096 #define M_TURN_SHIFT (DEG_90 / 4) // = 4096 #define M_TURN_DIST (STEP_L * 14) // = 3584 #define M_JUMP_DIST (STEP_L * 9 / 4) // = 576 #define M_JUMP_VELOCITY (-WALL_L) // = -1024 #define M_CAM_RIDE_ELEVATION (-DEG_45) // = -8190 #define M_CAM_RIDE_DISTANCE (WALL_L * 2) // = 2048 #define M_CAM_CRASH_ELEVATION (-DEG_1 * 25) // = -4550 #define M_CAM_CRASH_DISTANCE (WALL_L * 4) // = 4096 // clang-format on typedef enum { // clang-format off M_ANIM_MOUNT_LEFT = 0, M_ANIM_PREPARE_RIDE = 5, M_ANIM_SWIPE = 6, M_ANIM_PREPARE_DISMOUNT = 7, M_ANIM_CRASH = 23, M_ANIM_TOPPLED = 30, M_ANIM_TOPPLE_START = 31, M_ANIM_HIT_BEAM = 34, M_ANIM_MOUNT_RIGHT = 46, // clang-format on } M_ANIM; typedef enum { M_STATE_MOUNT, M_STATE_STOP, M_STATE_DISMOUNT_LEFT, M_STATE_DISMOUNT_RIGHT, M_STATE_IDLE, M_STATE_DUCK, M_STATE_RIDE, M_STATE_RIGHT, M_STATE_HUG_RIGHT, M_STATE_LEFT, M_STATE_HUG_LEFT, M_STATE_BRAKE, M_STATE_TILT_FORWARD, M_STATE_TILT_BACK, M_STATE_DEATH, M_STATE_CRASH_START, M_STATE_CRASH, M_STATE_HIT_BEAM, M_STATE_SWIPE, M_STATE_BRAKING, } M_STATE; typedef enum { M_SIDE_NONE, M_SIDE_LEFT, M_SIDE_RIGHT, } M_SIDE; typedef struct { int32_t speed; int32_t mid_pos; int32_t front_pos; int16_t y_velocity; int16_t gradient; uint8_t stop_delay; M_SIDE dismount_side; struct { XZ_32 pos; int16_t angle; int16_t length; M_SIDE side; } turn; struct { bool control; bool dead; bool stopped; bool suppress_anim; } flags; } M_PRIV; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "speed", &p->speed)); JSON_SHOULD(JSON_READ(io, "mid_pos", &p->mid_pos)); JSON_SHOULD(JSON_READ(io, "front_pos", &p->front_pos)); JSON_SHOULD(JSON_READ(io, "y_velocity", &p->y_velocity)); JSON_SHOULD(JSON_READ(io, "gradient", &p->gradient)); JSON_SHOULD(JSON_READ(io, "stop_delay", &p->stop_delay)); if (JSON_SHOULD(JSON_PUSH(io, "turn"))) { JSON_SHOULD(JSON_READ(io, "pos.x", &p->turn.pos.x)); JSON_SHOULD(JSON_READ(io, "pos.z", &p->turn.pos.z)); JSON_SHOULD(JSON_READ(io, "angle", &p->turn.angle)); JSON_SHOULD(JSON_READ(io, "length", &p->turn.length)); JSON_SHOULD(JSON_READ(io, "side", &p->turn.side)); JSON_POP(io); } if (JSON_SHOULD(JSON_PUSH(io, "flags"))) { JSON_SHOULD(JSON_READ(io, "control", &p->flags.control)); JSON_SHOULD(JSON_READ(io, "dead", &p->flags.dead)); JSON_SHOULD(JSON_READ(io, "stopped", &p->flags.stopped)); JSON_SHOULD(JSON_READ(io, "suppress_anim", &p->flags.suppress_anim)); JSON_POP(io); } } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "speed", p->speed); JSONW_WRITE(io, "mid_pos", p->mid_pos); JSONW_WRITE(io, "front_pos", p->front_pos); JSONW_WRITE(io, "y_velocity", p->y_velocity); JSONW_WRITE(io, "gradient", p->gradient); JSONW_WRITE(io, "stop_delay", p->stop_delay); JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "pos.x", p->turn.pos.x); JSONW_WRITE(io, "pos.z", p->turn.pos.z); JSONW_WRITE(io, "angle", p->turn.angle); JSONW_WRITE(io, "length", p->turn.length); JSONW_WRITE(io, "side", p->turn.side); JSONW_POP_AND_SET(io, "turn"); JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "control", p->flags.control); JSONW_WRITE(io, "dead", p->flags.dead); JSONW_WRITE(io, "stopped", p->flags.stopped); JSONW_WRITE(io, "suppress_anim", p->flags.suppress_anim); JSONW_POP_AND_SET(io, "flags"); } static M_SIDE M_CheckMount(ITEM *const item, COLL_INFO *const coll) { const ITEM *const lara_item = Lara_GetItem(); const LARA_INFO *const lara = Lara_GetLaraInfo(); if (!g_Input.action || lara->gun_status != LGS_ARMLESS || lara_item->gravity) { return M_SIDE_NONE; } if (!Item_TestBoundsCollide(item, lara_item, coll->radius)) { return M_SIDE_NONE; } if (!Collide_TestCollision(item, lara_item)) { return M_SIDE_NONE; } const XYZ_32 delta = { .x = lara_item->pos.x - item->pos.x, .z = lara_item->pos.z - item->pos.z, .y = 0, }; if (XYZ_32_GetLength2(delta) > M_MOUNT_DIST) { return M_SIDE_NONE; } int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); const int32_t height = Room_GetHeight(sector, item->pos); if (height < -MAX_HEIGHT) { return M_SIDE_NONE; } const int16_t angle = (int16_t)Math_Atan( lara_item->pos.z - item->pos.z, lara_item->pos.x - item->pos.x) - item->rot.y; return angle > -0x1FFE && angle < 0x5FFA ? M_SIDE_RIGHT : M_SIDE_LEFT; } static bool M_CheckDismount(const M_SIDE side) { const ITEM *const item = Lara_Vehicle_GetItem(); const int16_t rot = item->rot.y + (side == M_SIDE_LEFT ? DEG_90 : -DEG_90); const XYZ_32 pos = XYZ_32_OffsetYaw(item->pos, rot, M_DISMOUNT_DIST); int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); const int32_t height = Room_GetHeight(sector, pos); const HEIGHT_TYPE height_type = Room_GetHeightType(); if (height_type == HT_BIG_SLOPE || height_type == HT_DIAGONAL || height == NO_HEIGHT || ABS(height) <= WALL_L / 2) { return false; } const int32_t ceiling = Room_GetCeiling(sector, pos); if (ceiling - item->pos.y > -LARA_HEIGHT) { return false; } if (height - ceiling < LARA_HEIGHT) { return false; } return true; } static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { if (lara_item->hit_points < 0 || Lara_Vehicle_IsMounted()) { return; } ITEM *const item = Item_Get(item_num); const M_SIDE mount_side = M_CheckMount(item, coll); if (mount_side == M_SIDE_NONE) { Object_Collision(item_num, lara_item, coll); return; } LARA_INFO *const lara = Lara_GetLaraInfo(); Lara_Vehicle_SetIndex(item_num); if (lara->gun_type == LGT_FLARE) { Lara_Flare_Dispose(false); lara->gun_type = LGT_UNARMED; lara->request_gun_type = LGT_UNARMED; } const M_ANIM anim_idx = mount_side == M_SIDE_LEFT ? M_ANIM_MOUNT_LEFT : M_ANIM_MOUNT_RIGHT; Item_SwitchToObjAnim(lara_item, anim_idx, 0, O_LARA_VEHICLE_ANIM); lara_item->current_anim_state = M_STATE_MOUNT; lara_item->goal_anim_state = M_STATE_MOUNT; lara_item->pos = item->pos; lara_item->rot = item->rot; lara->gun_status = LGS_HANDS_BUSY; lara->hit_direction = DIR_UNKNOWN; M_PRIV *const p = item->priv; p->speed = 0; p->y_velocity = 0; p->gradient = 0; p->turn.side = M_SIDE_NONE; p->dismount_side = M_SIDE_NONE; p->flags.control = false; p->flags.dead = false; p->flags.stopped = false; p->flags.suppress_anim = false; Music_Play(MX_MINE_CART_THEME, MPM_ONCE); } static void M_CheckStrikeSwitch(ITEM *const item) { if (item->status == IS_ACTIVE) { return; } const ITEM *const lara_item = Lara_GetItem(); if (lara_item->current_anim_state != M_STATE_SWIPE || !Item_TestObjAnimEqual(lara_item, M_ANIM_SWIPE, O_LARA_VEHICLE_ANIM) || !Item_TestFrameRange(lara_item, 12, 22)) { return; } Sound_Effect(SFX_SPANNER_CLUNK, &item->pos, SPM_ALWAYS); Room_TestTriggers(item); Item_AddActive(Item_GetIndex(item)); item->flags = IF_CODE_BITS; item->status = IS_ACTIVE; } static void M_CheckObjectCollision(ITEM *const item, ITEM *const cart) { if (!item->collidable || item->status == IS_INVISIBLE || item == Lara_GetItem() || item == cart) { return; } const OBJECT *const obj = Object_Get(item->object_id); const bool is_flip_switch = item->object_id == O_ANIMATING_2; if (obj->collision_func == nullptr || (!obj->intelligent && !is_flip_switch)) { return; } if (!Item_IsNearby(item, cart, M_TARGET_DIST)) { return; } if (!Item_TestBoundsCollide(item, cart, M_RADIUS)) { return; } if (is_flip_switch) { M_CheckStrikeSwitch(item); return; } if (Item_ShouldSpawnBlood(item)) { Spawn_BloodBath( item->pos.x, cart->pos.y - STEP_L, item->pos.z, cart->speed, cart->rot.y, item->room_num, 3); } if (item->hit_points > 0) { item->hit_points = 0; if (item->include_in_kill_stats) { Stats_AddKill(); } } } static void M_ObjectCollision(ITEM *const cart) { int16_t roomies[M_MAX_COLL_ROOMS]; const int32_t roomies_count = Room_GetAdjoiningRooms(cart->room_num, roomies, M_MAX_COLL_ROOMS); for (int32_t i = 0; i < roomies_count; i++) { const ROOM *const room = Room_Get(roomies[i]); int16_t item_num = room->item_num; while (item_num != NO_ITEM) { ITEM *const item = Item_Get(item_num); const int16_t next_item_num = item->next_item; M_CheckObjectCollision(item, cart); item_num = next_item_num; } } } static int32_t M_GetCollision( ITEM *const item, const int16_t angle, const int32_t distance, int32_t *const ceiling) { XYZ_32 pos = XYZ_32_OffsetYaw(item->pos, angle, distance); pos.y -= LARA_HEIGHT; int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); const int32_t height = Room_GetHeight(sector, pos); *ceiling = Room_GetCeiling(sector, pos); return height == NO_HEIGHT ? NO_HEIGHT : (height - item->pos.y); } static int32_t M_GetHeight(ITEM *const item, const int32_t x, const int32_t z) { const int32_t s = Math_Sin(item->rot.y); const int32_t c = Math_Cos(item->rot.y); const XYZ_32 pos = { .x = item->pos.x + ((x * c + z * s) >> W2V_SHIFT), .y = (item->pos.y + ((x * Math_Sin(item->rot.z)) >> W2V_SHIFT)) - ((z * Math_Sin(item->rot.x)) >> W2V_SHIFT), .z = item->pos.z + ((z * c - x * s) >> W2V_SHIFT), }; int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); return Room_GetHeight(sector, pos); } static void M_UserControl(ITEM *const item) { ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara = Lara_GetLaraInfo(); M_PRIV *const p = item->priv; switch (lara_item->current_anim_state) { case M_STATE_MOUNT: if (Item_TestObjAnimEqual( lara_item, M_ANIM_PREPARE_RIDE, O_LARA_VEHICLE_ANIM) && Item_TestFrameEqual(lara_item, M_MESH_FRAME)) { Lara_Skin_SetExtraEquipment(LM_HAND_R, EXTRA_MESH_SPANNER); } break; case M_STATE_STOP: if (Item_TestObjAnimEqual( lara_item, M_ANIM_PREPARE_DISMOUNT, O_LARA_VEHICLE_ANIM)) { if (Item_TestFrameEqual(lara_item, M_MESH_FRAME)) { Lara_Skin_ClearEquipment(LM_HAND_R); } if (p->dismount_side == M_SIDE_RIGHT) { lara_item->goal_anim_state = M_STATE_DISMOUNT_RIGHT; } else { lara_item->goal_anim_state = M_STATE_DISMOUNT_LEFT; } } break; case M_STATE_DISMOUNT_LEFT: case M_STATE_DISMOUNT_RIGHT: if (Item_TestFrameEqual(lara_item, -1)) { XYZ_32 pos = { .x = 0, .y = 640, .z = 0, }; Lara_GetMeshPos(LM_HIPS, &pos); lara_item->pos = pos; lara_item->rot.y = item->rot.y; if (lara_item->current_anim_state == M_STATE_DISMOUNT_LEFT) { lara_item->rot.y += DEG_90; } else { lara_item->rot.y -= DEG_90; } Lara_Vehicle_Dismount(); lara->gun_status = LGS_ARMLESS; } break; case M_STATE_IDLE: if (!p->flags.control) { Sound_Effect(SFX_MINE_CART_CLUNK_START, &item->pos, SPM_ALWAYS); p->stop_delay = 64; p->flags.control = true; } if (g_Input.roll && p->flags.stopped) { if (g_Input.left && M_CheckDismount(M_SIDE_LEFT)) { lara_item->goal_anim_state = M_STATE_STOP; p->dismount_side = M_SIDE_LEFT; } else if (g_Input.right && M_CheckDismount(M_SIDE_RIGHT)) { lara_item->goal_anim_state = M_STATE_STOP; p->dismount_side = M_SIDE_RIGHT; } } if (g_Input.crouch) { lara_item->goal_anim_state = M_STATE_DUCK; } else if (p->speed > M_MIN_VELOCITY) { lara_item->goal_anim_state = M_STATE_RIDE; } break; case M_STATE_DUCK: if (g_Input.action) { lara_item->goal_anim_state = M_STATE_SWIPE; } else if (g_Input.jump) { lara_item->goal_anim_state = M_STATE_BRAKE; } else if (!g_Input.crouch) { lara_item->goal_anim_state = M_STATE_IDLE; } break; case M_STATE_RIDE: if (g_Input.action) { lara_item->goal_anim_state = M_STATE_SWIPE; } else if (g_Input.crouch) { lara_item->goal_anim_state = M_STATE_DUCK; } else if (g_Input.jump) { lara_item->goal_anim_state = M_STATE_BRAKE; } else if (p->speed == M_MIN_VELOCITY || p->flags.stopped) { lara_item->goal_anim_state = M_STATE_IDLE; } else if (p->gradient < -M_MAX_GRADIENT) { lara_item->goal_anim_state = M_STATE_TILT_FORWARD; } else if (p->gradient > M_MAX_GRADIENT) { lara_item->goal_anim_state = M_STATE_TILT_BACK; } else if (g_Input.left) { lara_item->goal_anim_state = M_STATE_LEFT; } else if (g_Input.right) { lara_item->goal_anim_state = M_STATE_RIGHT; } break; case M_STATE_RIGHT: if (!g_Input.right) { lara_item->goal_anim_state = M_STATE_RIDE; } else if (g_Input.action) { lara_item->goal_anim_state = M_STATE_SWIPE; } else if (g_Input.crouch) { lara_item->goal_anim_state = M_STATE_DUCK; } else if (g_Input.jump) { lara_item->goal_anim_state = M_STATE_BRAKE; } break; case M_STATE_LEFT: if (!g_Input.left) { lara_item->goal_anim_state = M_STATE_RIDE; } else if (g_Input.action) { lara_item->goal_anim_state = M_STATE_SWIPE; } else if (g_Input.crouch) { lara_item->goal_anim_state = M_STATE_DUCK; } else if (g_Input.jump) { lara_item->goal_anim_state = M_STATE_BRAKE; } break; case M_STATE_BRAKE: lara_item->goal_anim_state = M_STATE_BRAKING; break; case M_STATE_TILT_FORWARD: case M_STATE_TILT_BACK: if (g_Input.action) { lara_item->goal_anim_state = M_STATE_SWIPE; } else if (g_Input.crouch) { lara_item->goal_anim_state = M_STATE_DUCK; } else if (g_Input.jump) { lara_item->goal_anim_state = M_STATE_BRAKE; } else { const bool forward = lara_item->current_anim_state == M_STATE_TILT_FORWARD; if ((forward && p->gradient > -M_MAX_GRADIENT) || (!forward && p->gradient < M_MAX_GRADIENT)) { lara_item->goal_anim_state = M_STATE_RIDE; } } break; case M_STATE_DEATH: { g_Camera.target_elevation = M_CAM_RIDE_ELEVATION; g_Camera.target_distance = M_CAM_RIDE_DISTANCE; int32_t ceiling = 0; const int32_t height = M_GetCollision(item, item->rot.y, STEP_L * 2, &ceiling); if (height > -STEP_L && height < STEP_L) { const int32_t time4 = Output_GetTimeInGame() * 4; if ((time4 & 7) == 0) { Sound_Effect(SFX_QUAD_FRONT_IMPACT, &item->pos, SPM_ALWAYS); } item->pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, STEP_L / 2); } else if ( Item_TestObjAnimEqual( lara_item, M_ANIM_TOPPLED, O_LARA_VEHICLE_ANIM)) { p->flags.suppress_anim = true; lara_item->hit_points = -1; } break; } case M_STATE_CRASH: g_Camera.target_elevation = M_CAM_CRASH_ELEVATION; g_Camera.target_distance = M_CAM_CRASH_DISTANCE; break; case M_STATE_HIT_BEAM: if (lara_item->hit_points <= 0 && Item_TestFrameEqual(lara_item, 28)) { p->flags.control = false; p->flags.suppress_anim = true; p->speed = 0; item->speed = 0; } break; case M_STATE_SWIPE: lara_item->goal_anim_state = M_STATE_RIDE; break; case M_STATE_BRAKING: if (g_Input.crouch) { lara_item->goal_anim_state = M_STATE_DUCK; Sound_StopEffect(SFX_MINE_CART_SREECH_BRAKE); } else if (!g_Input.jump || p->flags.stopped) { lara_item->goal_anim_state = M_STATE_RIDE; Sound_StopEffect(SFX_MINE_CART_SREECH_BRAKE); } else { p->speed -= M_BRAKE_SPEED; Sound_Effect( SFX_MINE_CART_SREECH_BRAKE, &lara_item->pos, SPM_ALWAYS); } break; default: break; } if (Lara_Vehicle_IsMounted() && !p->flags.suppress_anim) { Item_Animate(lara_item); const int16_t lara_anim_num = Item_GetRelativeObjAnim(lara_item, O_LARA_VEHICLE_ANIM); const int16_t lara_frame_num = Item_GetRelativeFrame(lara_item); Item_SwitchToAnim(item, lara_anim_num, lara_frame_num); } if (lara_item->current_anim_state == M_STATE_DEATH || lara_item->current_anim_state == M_STATE_CRASH || lara_item->hit_points <= 0) { return; } if (item->rot.z > M_MAX_ROLL || item->rot.z < -M_MAX_ROLL) { Item_SwitchToObjAnim( lara_item, M_ANIM_TOPPLE_START, 0, O_LARA_VEHICLE_ANIM); lara_item->current_anim_state = M_STATE_DEATH; lara_item->goal_anim_state = M_STATE_DEATH; p->flags.control = false; p->flags.stopped = true; p->flags.dead = true; p->speed = 0; item->speed = 0; return; } int32_t ceiling = 0; const int32_t height = M_GetCollision(item, item->rot.y, STEP_L * 2, &ceiling); if (height < -STEP_L * 2) { Item_SwitchToObjAnim(lara_item, M_ANIM_CRASH, 0, O_LARA_VEHICLE_ANIM); lara_item->current_anim_state = M_STATE_CRASH; lara_item->goal_anim_state = M_STATE_CRASH; p->flags.control = false; p->flags.stopped = true; p->flags.dead = true; p->speed = 0; item->speed = 0; lara_item->hit_points = -1; return; } if (lara_item->current_anim_state != M_STATE_DUCK && lara_item->current_anim_state != M_STATE_HIT_BEAM) { COLL_INFO coll = { .radius = 100, .quadrant = Math_GetDirection(item->rot.y), }; if (Collide_CollideStaticObjects( &coll, item->pos.x, item->pos.y, item->pos.z, item->room_num, STEP_L * 3)) { Item_SwitchToObjAnim( lara_item, M_ANIM_HIT_BEAM, 0, O_LARA_VEHICLE_ANIM); lara_item->current_anim_state = M_STATE_HIT_BEAM; lara_item->goal_anim_state = M_STATE_HIT_BEAM; Spawn_BloodBath( lara_item->pos.x, lara_item->pos.y - STEP_L * 3, lara_item->pos.z, item->speed, item->rot.y, lara_item->room_num, 3); int16_t damage = 25 * ((uint16_t)p->speed >> 11); CLAMPL(damage, M_MIN_BEAM_DAMAGE); Lara_TakeDamage(damage, false); } } if (height > M_JUMP_DIST && p->y_velocity == 0) { p->y_velocity = M_JUMP_VELOCITY; } M_ObjectCollision(item); } static MINE_CART_TYPE M_GetFloorType(const ITEM *const item) { const XYZ_32 pos = { .x = item->pos.x, .y = MAX_HEIGHT, .z = item->pos.z, }; int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); return sector->mine_cart_type; } static void M_Move(ITEM *const item) { M_PRIV *const p = item->priv; if (p->stop_delay != 0) { p->stop_delay--; } #define L_STOP_POS(p) ((p & (STEP_L * 7 / 2)) == STEP_L * 2) const MINE_CART_TYPE floor_type = M_GetFloorType(item); if (floor_type == MINE_CART_STOP && p->stop_delay == 0 && (L_STOP_POS(item->pos.x) || L_STOP_POS(item->pos.z))) { if (p->speed < M_STOP_SPEED) { p->flags.control = true; p->flags.stopped = true; p->speed = 0; item->speed = 0; return; } p->stop_delay = 16; } #undef L_STOP_POS if ((floor_type == MINE_CART_LEFT || floor_type == MINE_CART_RIGHT) && p->stop_delay == 0 && p->turn.side == M_SIDE_NONE) { uint16_t rot = ((uint16_t)item->rot.y) >> W2V_SHIFT; if (floor_type == MINE_CART_LEFT) { rot |= 4; } switch (rot) { case 0: p->turn.pos.x = ROUND_TO_SECTOR(item->pos.x + M_TURN_SHIFT); p->turn.pos.z = ROUND_TO_SECTOR(item->pos.z); break; case 1: p->turn.pos.x = ROUND_TO_SECTOR(item->pos.x); p->turn.pos.z = ROUND_TO_SECTOR_END(item->pos.z - M_TURN_SHIFT); break; case 2: p->turn.pos.x = ROUND_TO_SECTOR_END(item->pos.x - M_TURN_SHIFT); p->turn.pos.z = ROUND_TO_SECTOR_END(item->pos.z); break; case 3: p->turn.pos.x = ROUND_TO_SECTOR_END(item->pos.x); p->turn.pos.z = ROUND_TO_SECTOR(item->pos.z + M_TURN_SHIFT); break; case 4: p->turn.pos.x = ROUND_TO_SECTOR_END(item->pos.x - M_TURN_SHIFT); p->turn.pos.z = ROUND_TO_SECTOR(item->pos.z); break; case 5: p->turn.pos.x = ROUND_TO_SECTOR(item->pos.x); p->turn.pos.z = ROUND_TO_SECTOR(item->pos.z + M_TURN_SHIFT); break; case 6: p->turn.pos.x = ROUND_TO_SECTOR(item->pos.x + M_TURN_SHIFT); p->turn.pos.z = ROUND_TO_SECTOR_END(item->pos.z); break; case 7: p->turn.pos.x = ROUND_TO_SECTOR_END(item->pos.x); p->turn.pos.z = ROUND_TO_SECTOR_END(item->pos.z - M_TURN_SHIFT); break; default: break; } int16_t angle = Math_Atan(item->pos.z - p->turn.pos.z, item->pos.x - p->turn.pos.x) & M_ANGLE_CLAMP; if (rot >= 4 && angle != 0) { angle = DEG_90 - angle; } p->turn.angle = item->rot.y; p->turn.length = angle; p->turn.side = floor_type == MINE_CART_LEFT ? M_SIDE_LEFT : M_SIDE_RIGHT; } CLAMPL(p->speed, M_MIN_SPEED); p->speed += -4 * p->gradient; item->speed = (int16_t)(p->speed >> 8); if (item->speed < M_MIN_VELOCITY) { item->speed = M_MIN_VELOCITY; Sound_StopEffect(SFX_MINE_CART_TRACK_LOOP); if (p->y_velocity != 0) { Sound_StopEffect(SFX_MINE_CART_PULLY_LOOP); } else { Sound_Effect(SFX_MINE_CART_PULLY_LOOP, &item->pos, SPM_ALWAYS); } } else { Sound_StopEffect(SFX_MINE_CART_PULLY_LOOP); if (p->y_velocity != 0) { Sound_StopEffect(SFX_MINE_CART_TRACK_LOOP); } else { Sound_Effect( SFX_MINE_CART_TRACK_LOOP, &item->pos, ((item->speed << 15) + 0x1000000) | SPM_PITCH | SPM_ALWAYS); } } if (p->turn.side != M_SIDE_NONE) { p->turn.length += 3 * item->speed; if (p->turn.length > (DEG_1 * 90)) { if (p->turn.side == M_SIDE_LEFT) { item->rot.y = p->turn.angle - DEG_90; } else { item->rot.y = p->turn.angle + DEG_90; } p->turn.side = M_SIDE_NONE; } else if (p->turn.side == M_SIDE_LEFT) { item->rot.y = p->turn.angle - p->turn.length; } else { item->rot.y = p->turn.angle + p->turn.length; } if (p->turn.side != M_SIDE_NONE) { const uint16_t quadrant = (uint16_t)item->rot.y >> W2V_SHIFT; const int16_t angle = item->rot.y & M_ANGLE_CLAMP; XZ_32 shift = {}; switch (quadrant) { case DIR_NORTH: shift.x = -Math_Cos(angle); shift.z = Math_Sin(angle); break; case DIR_EAST: shift.x = Math_Sin(angle); shift.z = Math_Cos(angle); break; case DIR_SOUTH: shift.x = Math_Cos(angle); shift.z = -Math_Sin(angle); break; default: shift.x = -Math_Sin(angle); shift.z = -Math_Cos(angle); break; } if (p->turn.side == M_SIDE_LEFT) { shift.x = -shift.x; shift.z = -shift.z; } item->pos.x = p->turn.pos.x + ((M_TURN_DIST * shift.x) >> W2V_SHIFT); item->pos.z = p->turn.pos.z + ((M_TURN_DIST * shift.z) >> W2V_SHIFT); } } else { item->pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, item->speed); } p->mid_pos = M_GetHeight(item, 0, 0); if (p->y_velocity == 0) { p->front_pos = M_GetHeight(item, 0, STEP_L); p->gradient = (int16_t)(p->mid_pos - p->front_pos); item->pos.y = p->mid_pos; } else if (item->pos.y > p->mid_pos) { if (p->y_velocity > 0) { Sound_Effect(SFX_QUAD_FRONT_IMPACT, &item->pos, SPM_ALWAYS); } item->pos.y = p->mid_pos; p->y_velocity = 0; } else { p->y_velocity += M_GRAVITY; CLAMPG(p->y_velocity, M_MAX_VELOCITY); item->pos.y += p->y_velocity >> 8; } item->rot.x = p->gradient << 5; if (p->turn.side != M_SIDE_NONE) { const int16_t angle = item->rot.y & M_ANGLE_CLAMP; if (p->turn.side == M_SIDE_RIGHT) { item->rot.z = -(item->speed * angle) >> 9; } else { item->rot.z = (item->speed * (DEG_90 - angle)) >> 9; } } else { item->rot.z -= item->rot.z >> 3; } } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->collision_func = M_Collision; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->save_anim = true; obj->save_flags = true; obj->save_position = true; } bool MineCart_Control(void) { ITEM *const item = Lara_Vehicle_GetItem(); M_PRIV *const p = item->priv; M_UserControl(item); if (p->flags.control) { M_Move(item); } LARA_INFO *const lara = Lara_GetLaraInfo(); ITEM *const lara_item = Lara_GetItem(); const bool mounted = Lara_Vehicle_IsMounted(); if (mounted) { lara_item->pos = item->pos; lara_item->rot = item->rot; } int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); Item_UpdateRoom(lara->item_num, room_num); if (mounted) { Item_UpdateRoom(Lara_Vehicle_GetIndex(), room_num); } Room_TestTriggers(lara_item); if (!p->flags.dead) { g_Camera.target_elevation = M_CAM_RIDE_ELEVATION; g_Camera.target_distance = M_CAM_RIDE_DISTANCE; } return mounted; } REGISTER_OBJECT(O_MINE_CART, M_Setup) ================================================ FILE: src/trx/game/objects/vehicles/mine_cart.h ================================================ #pragma once bool MineCart_Control(void); ================================================ FILE: src/trx/game/objects/vehicles/mounted_gun.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include // clang-format off #define M_MOUNT_RADIUS_SQ 30000 #define M_NEUTRAL_TILT 30 #define M_MAX_TILT (2 * M_NEUTRAL_TILT - 1) // = 59 #define M_MAX_ROT 544 #define M_MIN_ROT (-M_MAX_ROT) // = -544 #define M_ROT_SPEED 8 #define M_MAX_ROT_SPEED 64 #define M_MIN_ROT_SPEED (-M_MAX_ROT_SPEED) // = -64 #define M_ROT_SCALE 45 #define M_FIRE_COOLDOWN 26 #define M_CAM_ELEVATION (DEG_1 * -15) // = -2730 // clang-format on typedef enum { M_GUN_STATE_IDLE, M_GUN_STATE_CONTROL, M_GUN_STATE_DISMOUNT, M_GUN_STATE_WAIT_END, } M_GUN_STATE; typedef struct { M_GUN_STATE state; int32_t fire_count; int16_t tilt; int16_t yaw; int16_t yaw_offset; int32_t yaw_speed; } M_PRIV; typedef enum { M_STATE_MOUNT, M_STATE_DISMOUNT, M_STATE_TILT, } M_STATE; typedef enum { M_ANIM_MOUNT, M_ANIM_DISMOUNT, M_ANIM_TILT, } M_ANIM; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "state", &p->state)); JSON_SHOULD(JSON_READ(io, "fire_count", &p->fire_count)); JSON_SHOULD(JSON_READ(io, "tilt", &p->tilt)); JSON_SHOULD(JSON_READ(io, "yaw", &p->yaw)); JSON_SHOULD(JSON_READ(io, "yaw_offset", &p->yaw_offset)); JSON_SHOULD(JSON_READ(io, "yaw_speed", &p->yaw_speed)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "state", p->state); JSONW_WRITE(io, "fire_count", p->fire_count); JSONW_WRITE(io, "tilt", p->tilt); JSONW_WRITE(io, "yaw", p->yaw); JSONW_WRITE(io, "yaw_offset", p->yaw_offset); JSONW_WRITE(io, "yaw_speed", p->yaw_speed); } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; p->tilt = M_NEUTRAL_TILT; p->yaw_offset = item->rot.y; } static bool M_Mount(const int16_t item_num) { const ITEM *const lara_item = Lara_GetItem(); const LARA_INFO *const lara = Lara_GetLaraInfo(); if (!g_Input.action || lara->gun_status != LGS_ARMLESS || lara_item->gravity) { return false; } const ITEM *const item = Item_Get(item_num); const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; const int32_t dist = SQUARE(dx) + SQUARE(dz); if (dist > M_MOUNT_RADIUS_SQ) { return false; } return true; } static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { if (!M_Mount(item_num)) { Object_Collision(item_num, lara_item, coll); return; } Lara_Vehicle_SetIndex(item_num); LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->gun_type == LGT_FLARE) { Lara_Flare_Dispose(false); lara->gun_type = LGT_UNARMED; lara->request_gun_type = LGT_UNARMED; } const ITEM *const item = Item_Get(item_num); lara->gun_status = LGS_HANDS_BUSY; lara_item->pos = item->pos; lara_item->rot = item->rot; Item_SwitchToObjAnim(lara_item, M_ANIM_MOUNT, 0, O_LARA_VEHICLE_ANIM); lara_item->current_anim_state = M_STATE_MOUNT; lara_item->goal_anim_state = M_STATE_MOUNT; M_PRIV *const p = item->priv; p->state = M_GUN_STATE_IDLE; p->tilt = M_NEUTRAL_TILT; } static void M_Fire(ITEM *const gun_item) { const int16_t item_num = Item_Create(); if (item_num == NO_ITEM) { return; } const ITEM *const lara_item = Lara_GetItem(); ITEM *const projectile_item = Item_Get(item_num); projectile_item->object_id = O_HEAVY_ROCKET; projectile_item->room_num = lara_item->room_num; XYZ_32 offset = { .x = 0, .y = 0, .z = STEP_L, }; Collide_GetJointAbsPosition(gun_item, &offset, 2); projectile_item->pos = offset; projectile_item->interp.prev.pos = projectile_item->pos; Item_Initialise(item_num); M_PRIV *const p = gun_item->priv; projectile_item->rot.x = DEG_1 * (32 - p->tilt); projectile_item->rot.y = gun_item->rot.y; projectile_item->rot.z = 0; projectile_item->speed = 16; Item_AddActive(item_num); projectile_item->status = IS_ACTIVE; Sound_Effect(SFX_ROCKET_FIRE, &projectile_item->pos, SPM_NORMAL); if (g_TRVersion >= 3) { Sound_Effect( SFX_EXPLOSION_1, &projectile_item->pos, 0x2000000 | SPM_PITCH); } const GAME_VECTOR pos = { .pos = projectile_item->pos, .room_num = projectile_item->room_num, }; const int32_t smoke_count = g_Weapons[LGT_ROCKET].smoke_count; Sparks_TriggerGunSmoke(pos, true, LGT_ROCKET, smoke_count); projectile_item->shade.value_1 = -1; projectile_item->shade.value_2 = -1; const XYZ_32 back_128 = XYZ_32_FromYawPitch( projectile_item->rot.y, projectile_item->rot.x, -128); for (int32_t i = 0; i < 8; i++) { const int32_t dist = -(Random_GetControl() & 0x7FF); const XYZ_32 back_vel = XYZ_32_FromYawPitch( projectile_item->rot.y, projectile_item->rot.x, dist); Sparks_TriggerRocketFlame( back_128, (XYZ_32) { .x = back_vel.x - back_128.x, .y = back_vel.y - back_128.y, .z = back_vel.z - back_128.z, }, item_num, projectile_item->room_num); } } static void M_UserControl(ITEM *const gun_item) { M_PRIV *const p = gun_item->priv; if (p->state != M_GUN_STATE_CONTROL) { return; } const ITEM *const lara_item = Lara_GetItem(); const int16_t frame_num = Item_GetRelativeFrame(gun_item); if (lara_item->hit_points <= 0 || g_Input.roll) { p->state = M_GUN_STATE_DISMOUNT; return; } if (g_Input.action && p->fire_count == 0) { M_Fire(gun_item); p->fire_count = M_FIRE_COOLDOWN; return; } if (g_Input.left) { if (p->yaw_speed > 0) { p->yaw_speed >>= 1; } p->yaw_speed -= M_ROT_SPEED; CLAMPL(p->yaw_speed, M_MIN_ROT_SPEED); if ((frame_num & 7) == 0 && ABS(p->yaw) < M_MAX_ROT) { Sound_Effect(SFX_LARA_UZI_STOP, &gun_item->pos, SPM_NORMAL); } } else if (g_Input.right) { if (p->yaw_speed < 0) { p->yaw_speed >>= 1; } p->yaw_speed += M_ROT_SPEED; CLAMPG(p->yaw_speed, M_MAX_ROT_SPEED); if ((frame_num & 7) == 0 && ABS(p->yaw) < M_MAX_ROT) { Sound_Effect(SFX_LARA_UZI_STOP, &gun_item->pos, SPM_NORMAL); } } else { p->yaw_speed -= p->yaw_speed >> 2; if (ABS(p->yaw_speed) < M_ROT_SPEED) { p->yaw_speed = 0; } } p->yaw += (int16_t)(p->yaw_speed >> 2); if (p->yaw < M_MIN_ROT) { p->yaw = M_MIN_ROT; p->yaw_speed = 0; } else if (p->yaw > M_MAX_ROT) { p->yaw = M_MAX_ROT; p->yaw_speed = 0; } if (g_Input.forward && p->tilt < M_MAX_TILT) { p->tilt++; } else if (g_Input.back && p->tilt != 0) { p->tilt--; } } static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->collision_func = M_Collision; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->save_position = true; obj->save_flags = true; obj->save_anim = true; } bool MountedGun_Control(void) { LARA_INFO *const lara = Lara_GetLaraInfo(); ITEM *const lara_item = Lara_GetItem(); ITEM *const gun_item = Lara_Vehicle_GetItem(); M_PRIV *const p = gun_item->priv; M_UserControl(gun_item); if (p->state == M_GUN_STATE_DISMOUNT) { if (p->tilt < M_NEUTRAL_TILT) { p->tilt++; } else if (p->tilt > M_NEUTRAL_TILT) { p->tilt--; } else { Item_SwitchToObjAnim( lara_item, M_ANIM_DISMOUNT, 0, O_LARA_VEHICLE_ANIM); lara_item->current_anim_state = M_STATE_DISMOUNT; lara_item->goal_anim_state = M_STATE_DISMOUNT; p->state = M_GUN_STATE_WAIT_END; } } switch (lara_item->current_anim_state) { case M_STATE_MOUNT: case M_STATE_DISMOUNT: Item_Animate(lara_item); const ANIM *const anim = Item_GetAnim(lara_item); const int16_t anim_num = lara_item->anim_num - Object_Get(O_LARA_VEHICLE_ANIM)->anim_idx; const int16_t frame_num = lara_item->frame_num - anim->frame_base; Item_SwitchToAnim(gun_item, anim_num, frame_num); if (p->state == M_GUN_STATE_WAIT_END && Item_TestFrameEqual(gun_item, -1)) { Lara_Vehicle_Dismount(); lara->gun_status = LGS_ARMLESS; } break; case M_STATE_TILT: Item_SwitchToObjAnim( lara_item, M_ANIM_TILT, p->tilt, O_LARA_VEHICLE_ANIM); Item_SwitchToAnim(gun_item, M_ANIM_TILT, p->tilt); if (p->fire_count != 0) { p->fire_count--; } p->state = M_GUN_STATE_CONTROL; break; default: break; } gun_item->rot.y = p->yaw_offset + M_ROT_SCALE * p->yaw; lara_item->rot.y = gun_item->rot.y; g_Camera.target_elevation = M_CAM_ELEVATION; return true; } REGISTER_OBJECT(O_MOUNTED_GUN, M_Setup) ================================================ FILE: src/trx/game/objects/vehicles/mounted_gun.h ================================================ #pragma once bool MountedGun_Control(void); ================================================ FILE: src/trx/game/objects/vehicles/quad_bike.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static const BITE m_QuadBites[6] = { { .pos = { .x = -56, .y = -32, .z = -380 }, .mesh_num = 0 }, { .pos = { .x = 56, .y = -32, .z = -380 }, .mesh_num = 0 }, { .pos = { .x = -8, .y = 180, .z = -48 }, .mesh_num = 3 }, { .pos = { .x = 8, .y = 180, .z = -48 }, .mesh_num = 4 }, { .pos = { .x = 90, .y = 180, .z = -32 }, .mesh_num = 6 }, { .pos = { .x = -90, .y = 180, .z = -32 }, .mesh_num = 7 }, }; typedef enum { M_STATE_EMPTY, M_STATE_DRIVE, M_STATE_TURN_L, M_STATE_3, M_STATE_4, M_STATE_SLOW, M_STATE_BRAKE, M_STATE_BIKE_DEATH, M_STATE_FALL, M_STATE_GET_ON_R, M_STATE_GET_OFF_R, M_STATE_HIT_BACK, M_STATE_HIT_FRONT, M_STATE_HIT_LEFT, M_STATE_HIT_RIGHT, M_STATE_STOP, M_STATE_16, M_STATE_LAND, M_STATE_STOP_SLOWLY, M_STATE_FALL_DEATH, M_STATE_FALL_OFF, M_STATE_WHEELIE, M_STATE_TURN_R, M_STATE_GET_ON_L, M_STATE_GET_OFF_L, } M_STATE; static bool m_DontExitQuad; static bool m_HandbrakeStarting; static bool m_CanHandbrakeStart; static uint8_t m_ExhaustSmokeVel; typedef struct { int32_t velocity; int16_t front_rot; int16_t rear_rot; int32_t revs; int32_t engine_revs; int16_t track_mesh; int32_t skidoo_turn; int32_t left_fall_speed; int32_t right_fall_speed; int16_t momentum_angle; int16_t extra_rotation; int32_t pitch; uint8_t flags; } M_QUAD_BIKE_INFO; typedef struct { M_QUAD_BIKE_INFO quad; int16_t *extra_rotation; int32_t extra_rotation_count; int32_t rear_rot_x_idx[2]; int32_t front_rot_x_idx[2]; } M_PRIV; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "velocity", &p->quad.velocity)); JSON_SHOULD(JSON_READ(io, "front_rot", &p->quad.front_rot)); JSON_SHOULD(JSON_READ(io, "rear_rot", &p->quad.rear_rot)); JSON_SHOULD(JSON_READ(io, "revs", &p->quad.revs)); JSON_SHOULD(JSON_READ(io, "engine_revs", &p->quad.engine_revs)); JSON_SHOULD(JSON_READ(io, "track_mesh", &p->quad.track_mesh)); JSON_SHOULD(JSON_READ(io, "skidoo_turn", &p->quad.skidoo_turn)); JSON_SHOULD(JSON_READ(io, "left_fall_speed", &p->quad.left_fall_speed)); JSON_SHOULD(JSON_READ(io, "right_fall_speed", &p->quad.right_fall_speed)); JSON_SHOULD(JSON_READ(io, "momentum_angle", &p->quad.momentum_angle)); JSON_SHOULD(JSON_READ(io, "extra_rotation", &p->quad.extra_rotation)); JSON_SHOULD(JSON_READ(io, "pitch", &p->quad.pitch)); JSON_SHOULD(JSON_READ(io, "flags", &p->quad.flags)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "velocity", p->quad.velocity); JSONW_WRITE(io, "front_rot", p->quad.front_rot); JSONW_WRITE(io, "rear_rot", p->quad.rear_rot); JSONW_WRITE(io, "revs", p->quad.revs); JSONW_WRITE(io, "engine_revs", p->quad.engine_revs); JSONW_WRITE(io, "track_mesh", p->quad.track_mesh); JSONW_WRITE(io, "skidoo_turn", p->quad.skidoo_turn); JSONW_WRITE(io, "left_fall_speed", p->quad.left_fall_speed); JSONW_WRITE(io, "right_fall_speed", p->quad.right_fall_speed); JSONW_WRITE(io, "momentum_angle", p->quad.momentum_angle); JSONW_WRITE(io, "extra_rotation", p->quad.extra_rotation); JSONW_WRITE(io, "pitch", p->quad.pitch); JSONW_WRITE(io, "flags", p->quad.flags); } static int32_t M_CountExtraRotationValues(const XYZ_BOOL flags) { return (flags.y ? 1 : 0) + (flags.x ? 1 : 0) + (flags.z ? 1 : 0); } static void M_EnableWheelExtraRotations(OBJECT *const obj) { // TR3 rotates wheels around X at meshes 3/4 (rear) and 6/7 (front). if (obj->mesh_count > 3) { Object_GetBone(obj, 2)->rot.x = true; } if (obj->mesh_count > 4) { Object_GetBone(obj, 3)->rot.x = true; } if (obj->mesh_count > 6) { Object_GetBone(obj, 5)->rot.x = true; } if (obj->mesh_count > 7) { Object_GetBone(obj, 6)->rot.x = true; } } static void M_CalcExtraRotationLayout(const OBJECT *const obj, M_PRIV *const p) { p->extra_rotation_count = 0; p->rear_rot_x_idx[0] = -1; p->rear_rot_x_idx[1] = -1; p->front_rot_x_idx[0] = -1; p->front_rot_x_idx[1] = -1; if (obj == nullptr || !obj->loaded) { return; } int32_t cursor = 0; cursor += M_CountExtraRotationValues(obj->base_rot); for (int32_t mesh_idx = 1; mesh_idx < obj->mesh_count; mesh_idx++) { const ANIM_BONE *const bone = Object_GetBone(obj, mesh_idx - 1); const XYZ_BOOL flags = bone->rot; if (flags.x) { const int32_t x_idx = cursor + (flags.y ? 1 : 0); switch (mesh_idx) { case 3: p->rear_rot_x_idx[0] = x_idx; break; case 4: p->rear_rot_x_idx[1] = x_idx; break; case 6: p->front_rot_x_idx[0] = x_idx; break; case 7: p->front_rot_x_idx[1] = x_idx; break; default: break; } } cursor += M_CountExtraRotationValues(flags); } p->extra_rotation_count = cursor; } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; p->quad.momentum_angle = item->rot.y; const OBJECT *const obj = Object_Get(item->object_id); M_CalcExtraRotationLayout(obj, p); if (p->extra_rotation_count > 0) { p->extra_rotation = GameBuf_Alloc( sizeof(int16_t) * p->extra_rotation_count, GBUF_ITEM_DATA); } else { p->extra_rotation = nullptr; } item->extra_rotations = p->extra_rotation; } static int32_t M_GetOnQuadBike( const int16_t item_num, const COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara = Lara_GetLaraInfo(); if (!g_Input.action || (item->flags & IF_INVISIBLE) != 0 || lara->gun_status != LGS_ARMLESS || lara_item->gravity) { return 0; } const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dy = ABS(item->pos.y - lara_item->pos.y); const int32_t dz = lara_item->pos.z - item->pos.z; const int32_t dist = SQUARE(dx) + SQUARE(dz); if (dy > 256 || dist > 170000) { return 0; } int16_t room_num = item->room_num; SECTOR *const sector = Room_GetSector(item->pos, &room_num); const int32_t h = Room_GetHeight(sector, item->pos); if (h < -32000) { return 0; } const int16_t ang = Math_Atan( item->pos.z - lara_item->pos.z, item->pos.x - lara_item->pos.x) - item->rot.y; uint16_t uang = lara_item->rot.y - item->rot.y; if (ang > -0x1FFE && ang < 0x5FFA) { if (uang <= 0x1FFE || uang >= 0x5FFA) { return 0; } } else { if (uang <= 0x9FF6 || uang >= 0xDFF2) { return 0; } } return 1; } static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara_item->hit_points < 0 || Lara_Vehicle_GetItem() != nullptr) { return; } if (!M_GetOnQuadBike(item_num, coll)) { Object_Collision(item_num, lara_item, coll); return; } Lara_Vehicle_SetIndex(item_num); if (lara->gun_type == LGT_FLARE) { Lara_Flare_Dispose(false); lara->gun_type = LGT_UNARMED; lara->request_gun_type = LGT_UNARMED; } lara->gun_status = LGS_HANDS_BUSY; ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; M_QUAD_BIKE_INFO *const quad = &p->quad; const int16_t angle = (int16_t)Math_Atan( item->pos.z - lara_item->pos.z, item->pos.x - lara_item->pos.x) - item->rot.y; if (angle > -0x1FFE && angle < 0x5FFA) { Item_SwitchToObjAnim(lara_item, 23, 0, O_LARA_VEHICLE_ANIM); lara_item->current_anim_state = M_STATE_GET_ON_L; lara_item->goal_anim_state = M_STATE_GET_ON_L; } else { Item_SwitchToObjAnim(lara_item, 9, 0, O_LARA_VEHICLE_ANIM); lara_item->current_anim_state = M_STATE_GET_ON_R; lara_item->goal_anim_state = M_STATE_GET_ON_R; } lara_item->pos.x = item->pos.x; lara_item->pos.y = item->pos.y; lara_item->pos.z = item->pos.z; lara_item->rot.y = item->rot.y; lara->head_rot.y = 0; lara->head_rot.x = 0; lara->torso_rot.y = 0; lara->torso_rot.x = 0; lara->hit_direction = DIR_UNKNOWN; Item_Animate(lara_item); // TODO: do not hardcode this if (g_TRVersion == 3 && GF_GetCurrentLevel()->num == 3) { const bool is_ambient = Music_GetCurrentPlayingTrack() == Music_GetCurrentLoopedTrack(); const MUSIC_ID tunes[4] = { 9, 12, 4, 12 }; if (is_ambient) { Music_Play_Direct( tunes[Random_GetControl() % ARRAY_SIZE(tunes)], MPM_ONCE); } } quad->revs = 0; } static void M_Explode(ITEM *const item) { if (Room_Get(item->room_num)->flags.underwater) { Sparks_TriggerUnderwaterExplosion(item); } else { Sparks_TriggerExplosionSparks(item->pos, 3, -2, 0, item->room_num); for (int32_t i = 0; i < 3; i++) { Sparks_TriggerExplosionSparks(item->pos, 3, -1, 0, item->room_num); } } const int16_t vehicle_item_num = Lara_Vehicle_GetIndex(); Item_Explode(vehicle_item_num, -2, 0); Item_Kill(vehicle_item_num); item->status = IS_DEACTIVATED; Sound_Effect(SFX_EXPLOSION_1, nullptr, SPM_NORMAL); Sound_Effect(SFX_EXPLOSION_2, nullptr, SPM_NORMAL); Lara_Vehicle_SetIndex(NO_ITEM); } static bool M_CheckGetOff(void) { ITEM *const item = Lara_Vehicle_GetItem(); ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara = Lara_GetLaraInfo(); if ((lara_item->current_anim_state == M_STATE_GET_OFF_R || lara_item->current_anim_state == M_STATE_GET_OFF_L) && lara_item->frame_num == Item_GetAnim(lara_item)->frame_end) { if (lara_item->current_anim_state == M_STATE_GET_OFF_L) { lara_item->rot.y += DEG_90; } else { lara_item->rot.y -= DEG_90; } Item_SwitchToAnim(lara_item, LA(LA_STAND_STILL), 0); lara_item->current_anim_state = LS_STOP; lara_item->goal_anim_state = LS_STOP; lara_item->pos.x -= (512 * Math_Sin(lara_item->rot.y)) >> W2V_SHIFT; lara_item->pos.z -= (512 * Math_Cos(lara_item->rot.y)) >> W2V_SHIFT; lara_item->rot.x = 0; lara_item->rot.z = 0; Lara_Vehicle_SetIndex(NO_ITEM); lara->gun_status = LGS_ARMLESS; } else if (lara_item->frame_num == Item_GetAnim(lara_item)->frame_end) { M_PRIV *const p = item->priv; M_QUAD_BIKE_INFO *const quad = &p->quad; if (lara_item->current_anim_state == M_STATE_FALL_OFF) { Item_SwitchToAnim(lara_item, LA(LA_FREEFALL), 0); lara_item->current_anim_state = LS_FAST_FALL; XYZ_32 pos = {}; Lara_GetMeshPos(LM_HIPS, &pos); lara_item->pos.x = pos.x; lara_item->pos.y = pos.y; lara_item->pos.z = pos.z; lara_item->gravity = true; lara_item->fall_speed = item->fall_speed; lara_item->rot.x = 0; lara_item->rot.z = 0; lara_item->hit_points = 0; lara->gun_status = LGS_ARMLESS; item->flags |= IF_INVISIBLE; return false; } if (lara_item->current_anim_state == M_STATE_FALL_DEATH) { lara_item->goal_anim_state = M_STATE_FALL; lara_item->fall_speed = 154; lara_item->speed = 0; quad->flags |= 0x80; return false; } } return true; } static int32_t M_TestHeight( const ITEM *const item, const int32_t x, const int32_t z, XYZ_32 *const pos) { const int32_t s = Math_Sin(item->rot.y); const int32_t c = Math_Cos(item->rot.y); pos->x = item->pos.x + ((z * c + x * s) >> W2V_SHIFT); pos->y = item->pos.y + ((z * Math_Sin(item->rot.z)) >> W2V_SHIFT) - ((x * Math_Sin(item->rot.x)) >> W2V_SHIFT); pos->z = item->pos.z + ((x * c - z * s) >> W2V_SHIFT); int16_t room_num = item->room_num; SECTOR *const sector = Room_GetSector(*pos, &room_num); const int32_t ceiling = Room_GetCeiling(sector, *pos); if (pos->y < ceiling || ceiling == NO_HEIGHT) { return NO_HEIGHT; } return Room_GetHeight(sector, *pos); } static void M_TriggerExhaustSmoke( XYZ_32 pos, int16_t angle, int32_t speed, const bool moving) { SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 0; spark->src_color.g = 0; spark->src_color.b = 0; if (moving) { #if 0 // OG spark->dst_color.r = MINMAX((96 * speed) >> 5, 0, 255); spark->dst_color.g = MINMAX((96 * speed) >> 5, 0, 255); spark->dst_color.b = MINMAX((128 * speed) >> 5, 0, 255); #else spark->dst_color.r = 96 >> 1; spark->dst_color.g = 96 >> 1; spark->dst_color.b = 128 >> 1; #endif } else { spark->dst_color.r = 96; spark->dst_color.g = 96; spark->dst_color.b = 128; } spark->col_fade_speed = 4; spark->fade_to_black = 4; spark->life = (Random_GetControl() & 3) - (speed >> 12) + 20; CLAMPL(spark->life, 9); spark->s_life = spark->life; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->dynamic = -1; spark->pos = (XYZ_32) { .x = pos.x + (Random_GetControl() & 0xF) - 8, .y = pos.y + (Random_GetControl() & 0xF) - 8, .z = pos.z + (Random_GetControl() & 0xF) - 8, }; spark->vel = (XYZ_32) { .x = (Random_GetControl() & 0xFF) + ((speed * Math_Sin(angle)) >> 16) - 128, .y = -8 - (Random_GetControl() & 7), .z = (Random_GetControl() & 0xFF) + ((speed * Math_Cos(angle)) >> 16) - 128, }; spark->friction = 4; if (Random_GetControl() & 1) { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = Random_GetControl() & 0xFFF; if (Random_GetControl() & 1) { spark->rot_add = -24 - (Random_GetControl() & 7); } else { spark->rot_add = (Random_GetControl() & 7) + 24; } } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->scalar = 2; spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->gravity = -4 - (Random_GetControl() & 3); spark->max_y_vel = -8 - (Random_GetControl() & 7); spark->dst_size.width = (Random_GetControl() & 7) + (speed >> 7) + 32; spark->src_size.width = spark->dst_size.width >> 1; spark->size.width = spark->dst_size.width >> 1; spark->dst_size.height = spark->dst_size.width; spark->src_size.height = spark->dst_size.height >> 1; spark->size.height = spark->dst_size.height >> 1; Sparks_FinishSetup(spark); } static bool M_SkidooCanGetOff(const int32_t lr) { ITEM *const item = Lara_Vehicle_GetItem(); const int16_t angle = item->rot.y + DEG_90 * lr; const XYZ_32 pos = XYZ_32_OffsetYaw(item->pos, angle, 512); int16_t room_num = item->room_num; SECTOR *const sector = Room_GetSector(pos, &room_num); const int32_t h = Room_GetHeight(sector, pos); const int32_t c = Room_GetCeiling(sector, pos); const HEIGHT_TYPE height_type = Room_GetHeightType(); if (height_type != HT_BIG_SLOPE && height_type != HT_DIAGONAL && h != NO_HEIGHT && ABS(h - item->pos.y) <= 512 && c - item->pos.y <= -LARA_HEIGHT && h - c >= LARA_HEIGHT) { return true; } return false; } static int32_t M_GetCollisionAnim(ITEM *const item, XYZ_32 *const pos) { pos->x = item->pos.x - pos->x; pos->z = item->pos.z - pos->z; if (pos->x == 0 && pos->z == 0) { return 0; } const int32_t s = Math_Sin(item->rot.y); const int32_t c = Math_Cos(item->rot.y); const int32_t fb = (pos->x * s + pos->z * c) >> W2V_SHIFT; const int32_t lr = (pos->x * c - pos->z * s) >> W2V_SHIFT; if (ABS(fb) > ABS(lr)) { return fb > 0 ? 14 : 13; } else { return lr > 0 ? 11 : 12; } } static int32_t M_DoDynamics( const int32_t height, int32_t fall_speed, int32_t *const y_pos) { if (height <= *y_pos) { int32_t bounce = (height - *y_pos) << 2; CLAMPL(bounce, -80); fall_speed += (bounce - fall_speed) >> 3; CLAMPG(*y_pos, height); } else { *y_pos += fall_speed; if (*y_pos <= height - 80) { fall_speed += 6; } else { *y_pos = height; fall_speed = 0; } } return fall_speed; } static int32_t M_DoShift( ITEM *const item, const XYZ_32 *const new_pos, const XYZ_32 *const old_pos) { const int32_t new_x = new_pos->x >> WALL_SHIFT; const int32_t new_z = new_pos->z >> WALL_SHIFT; const int32_t old_x = old_pos->x >> WALL_SHIFT; const int32_t old_z = old_pos->z >> WALL_SHIFT; const int32_t shift_x = new_pos->x & (WALL_L - 1); const int32_t shift_z = new_pos->z & (WALL_L - 1); if (new_x == old_x) { if (new_z == old_z) { item->pos.z += (old_pos->z - new_pos->z); item->pos.x += (old_pos->x - new_pos->x); return 0; } else if (new_z <= old_z) { item->pos.z += WALL_L - shift_z; return item->pos.x - new_pos->x; } else { item->pos.z -= 1 + shift_z; return new_pos->x - item->pos.x; } } if (new_z == old_z) { if (new_x <= old_x) { item->pos.x += WALL_L - shift_x; return new_pos->z - item->pos.z; } else { item->pos.x -= 1 + shift_x; return item->pos.z - new_pos->z; } } int32_t x = 0; int32_t z = 0; XYZ_32 test_pos = { old_pos->x, new_pos->y, new_pos->z }; int16_t room_num = item->room_num; SECTOR *sector = Room_GetSector(test_pos, &room_num); const int32_t h = Room_GetHeight(sector, test_pos); if (h < old_pos->y - 256) { if (new_pos->z > old_pos->z) { z = -1 - shift_z; } else { z = WALL_L - shift_z; } } test_pos = (XYZ_32) { new_pos->x, new_pos->y, old_pos->z }; room_num = item->room_num; sector = Room_GetSector(test_pos, &room_num); const int32_t h2 = Room_GetHeight(sector, test_pos); if (h2 < old_pos->y - 256) { if (new_pos->x > old_pos->x) { x = -1 - shift_x; } else { x = WALL_L - shift_x; } } if (x != 0 && z != 0) { item->pos.x += x; item->pos.z += z; return 0; } if (z != 0) { item->pos.z += z; if (z > 0) { return item->pos.x - new_pos->x; } else { return new_pos->x - item->pos.x; } } if (x != 0) { item->pos.x += x; if (x > 0) { return new_pos->z - item->pos.z; } else { return item->pos.z - new_pos->z; } } item->pos.x += old_pos->x - new_pos->x; item->pos.z += old_pos->z - new_pos->z; return 0; } static void M_SkidooBaddieCollision(ITEM *const quad) { ITEM *const lara_item = Lara_GetItem(); int16_t nearby_rooms[16] = { quad->room_num }; int16_t nearby_room_count = 1; const PORTALS *const portals = Room_Get(quad->room_num)->portals; if (portals != nullptr) { for (int32_t i = 0; i < portals->count && i < 16; i++) { nearby_rooms[nearby_room_count] = portals->portal[i].room_num; nearby_room_count++; } } for (int32_t i = 0; i < nearby_room_count; i++) { int16_t item_num = Room_Get(nearby_rooms[i])->item_num; while (item_num != NO_ITEM) { ITEM *const item = Item_Get(item_num); if (!item->collidable || item->status == IS_INVISIBLE || item == lara_item || item == quad) { goto loop_end; } const OBJECT *const obj = Object_Get(item->object_id); if (obj->collision_func == nullptr || (!obj->intelligent && item->object_id != O_ROLLING_BALL_2)) { goto loop_end; } int32_t dx = quad->pos.x - item->pos.x; int32_t dy = quad->pos.y - item->pos.y; int32_t dz = quad->pos.z - item->pos.z; if (dx <= -2048 || dx >= 2048 || dz <= -2048 || dz >= 2048 || dy <= -2048 || dy >= 2048 || !Item_TestBoundsCollide(item, quad, 500)) { goto loop_end; } if (item->object_id == O_ROLLING_BALL_2) { if (item->current_anim_state == 1) { Lara_TakeDamage(100, true); } } else { if (Item_ShouldSpawnBlood(item)) { Spawn_BloodBath( item->pos.x, quad->pos.y - 256, item->pos.z, quad->speed, quad->rot.y, item->room_num, 3); } if (item->hit_points > 0) { item->hit_points = 0; if (item->include_in_kill_stats) { Stats_AddKill(); } } } loop_end: item_num = item->next_item; } } } static int32_t M_SkidooDynamics(ITEM *const item) { m_DontExitQuad = false; M_PRIV *const p = item->priv; M_QUAD_BIKE_INFO *const quad = &p->quad; XYZ_32 old_pos; old_pos.x = item->pos.x; old_pos.y = item->pos.y; old_pos.z = item->pos.z; XYZ_32 new_pos = {}; XYZ_32 front_left_pos = {}; XYZ_32 front_right_pos = {}; XYZ_32 back_left_pos = {}; XYZ_32 back_right_pos = {}; XYZ_32 mid_left_pos = {}; XYZ_32 mid_right_pos = {}; XYZ_32 bm_left_pos = {}; XYZ_32 bm_right_pos = {}; XYZ_32 fm_left_pos = {}; XYZ_32 fm_right_pos = {}; // clang-format off const int32_t front_left_height = M_TestHeight(item, 550, -260, &front_left_pos); const int32_t front_right_height = M_TestHeight(item, 550, 260, &front_right_pos); const int32_t back_left_height = M_TestHeight(item, -550, -260, &back_left_pos); const int32_t back_right_height = M_TestHeight(item, -550, 260, &back_right_pos); const int32_t mid_left_height = M_TestHeight(item, 0, -260, &mid_left_pos); const int32_t mid_right_height = M_TestHeight(item, 0, 260, &mid_right_pos); const int32_t bm_left_height = M_TestHeight(item, 275, -260, &bm_left_pos); const int32_t bm_right_height = M_TestHeight(item, 275, 260, &bm_right_pos); const int32_t fm_left_height = M_TestHeight(item, -275, -260, &fm_left_pos); const int32_t fm_right_height = M_TestHeight(item, -275, 260, &fm_right_pos); // clang-format on CLAMPG(back_left_pos.y, back_left_height); CLAMPG(back_right_pos.y, back_right_height); CLAMPG(front_left_pos.y, front_left_height); CLAMPG(front_right_pos.y, front_right_height); CLAMPG(fm_left_pos.y, fm_left_height); CLAMPG(fm_right_pos.y, fm_right_height); CLAMPG(bm_left_pos.y, bm_left_height); CLAMPG(bm_right_pos.y, bm_right_height); CLAMPG(mid_left_pos.y, mid_left_height); CLAMPG(mid_right_pos.y, mid_right_height); if (item->pos.y <= item->floor - 256) { item->rot.y += quad->extra_rotation + quad->skidoo_turn; } else { if (quad->skidoo_turn < -364) { quad->skidoo_turn += 364; } else if (quad->skidoo_turn > 364) { quad->skidoo_turn -= 364; } else { quad->skidoo_turn = 0; } item->rot.y += quad->extra_rotation + quad->skidoo_turn; int16_t vel = 546 - (quad->velocity >> 8); const int16_t ang = item->rot.y - quad->momentum_angle; if (!g_Input.action && quad->velocity > 0) { vel += vel >> 2; } if (ang < -273) { if (ang >= -27300) { quad->momentum_angle -= vel; } else { quad->momentum_angle = item->rot.y + 27300; } } else if (ang > 273) { if (ang <= 27300) { quad->momentum_angle += vel; } else { quad->momentum_angle = item->rot.y - 27300; } } else { quad->momentum_angle = item->rot.y; } } int16_t room_num = item->room_num; SECTOR *const sector = Room_GetSector(item->pos, &room_num); const int32_t height = Room_GetHeight(sector, item->pos); int32_t speed = item->pos.y < height ? item->speed : (item->speed * Math_Cos(item->rot.x)) >> W2V_SHIFT; item->pos.x += (speed * Math_Sin(quad->momentum_angle)) >> W2V_SHIFT; item->pos.z += (speed * Math_Cos(quad->momentum_angle)) >> W2V_SHIFT; int32_t slip = (100 * Math_Sin(item->rot.x)) >> W2V_SHIFT; if (ABS(slip) > 50) { m_DontExitQuad = true; if (slip > 0) { slip -= 10; } else { slip += 10; } item->pos.x -= (slip * Math_Sin(item->rot.y)) >> W2V_SHIFT; item->pos.z -= (slip * Math_Cos(item->rot.y)) >> W2V_SHIFT; } slip = (50 * Math_Sin(item->rot.z)) >> W2V_SHIFT; if (ABS(slip) > 25) { m_DontExitQuad = true; item->pos.x += (slip * Math_Cos(item->rot.y)) >> W2V_SHIFT; item->pos.z -= (slip * Math_Sin(item->rot.y)) >> W2V_SHIFT; } new_pos.x = item->pos.x; new_pos.z = item->pos.z; if ((item->flags & IF_INVISIBLE) == 0) { M_SkidooBaddieCollision(item); } int16_t shift = 0; int16_t shift2 = 0; XYZ_32 front_left_pos2 = {}; XYZ_32 bm_left_pos2 = {}; XYZ_32 mid_left_pos2 = {}; XYZ_32 fm_left_pos2 = {}; XYZ_32 back_left_pos2 = {}; XYZ_32 front_right_pos2 = {}; XYZ_32 bm_right_pos2 = {}; XYZ_32 mid_right_pos2 = {}; XYZ_32 fm_right_pos2 = {}; XYZ_32 back_right_pos2 = {}; const int32_t front_left_height2 = M_TestHeight(item, 550, -260, &front_left_pos2); if (front_left_height2 < front_left_pos.y - 256) { shift = (int16_t)M_DoShift(item, &front_left_pos2, &front_left_pos); } const int32_t bm_left_height2 = M_TestHeight(item, 275, -260, &bm_left_pos2); if (bm_left_height2 < bm_left_pos.y - 256) { M_DoShift(item, &bm_left_pos2, &bm_left_pos); } const int32_t mid_left_height2 = M_TestHeight(item, 0, -260, &mid_left_pos2); if (mid_left_height2 < mid_left_pos.y - 256) { M_DoShift(item, &mid_left_pos2, &mid_left_pos); } const int32_t fm_left_height2 = M_TestHeight(item, -275, -260, &fm_left_pos2); if (fm_left_height2 < fm_left_pos.y - 256) { M_DoShift(item, &fm_left_pos2, &fm_left_pos); } const int32_t back_left_height2 = M_TestHeight(item, -550, -260, &back_left_pos2); if (back_left_height2 < back_left_pos.y - 256) { shift2 = M_DoShift(item, &back_left_pos2, &back_left_pos); if ((shift2 > 0 && shift >= 0) || (shift2 < 0 && shift <= 0)) { shift += shift2; } } const int32_t front_right_height2 = M_TestHeight(item, 550, 260, &front_right_pos2); if (front_right_height2 < front_right_pos.y - 256) { shift2 = M_DoShift(item, &front_right_pos2, &front_right_pos); if ((shift2 > 0 && shift >= 0) || (shift2 < 0 && shift <= 0)) { shift += shift2; } } const int32_t bm_right_height2 = M_TestHeight(item, 275, 260, &bm_right_pos2); if (bm_right_height2 < bm_right_pos.y - 256) { M_DoShift(item, &bm_right_pos2, &bm_right_pos); } const int32_t mid_right_height2 = M_TestHeight(item, 0, 260, &mid_right_pos2); if (mid_right_height2 < mid_right_pos.y - 256) { M_DoShift(item, &mid_right_pos2, &mid_right_pos); } const int32_t fm_right_height2 = M_TestHeight(item, -275, 260, &fm_right_pos2); if (fm_right_height2 < fm_right_pos.y - 256) { M_DoShift(item, &fm_right_pos2, &fm_right_pos); } const int32_t back_right_height2 = M_TestHeight(item, -550, 260, &back_right_pos2); if (back_right_height2 < back_right_pos.y - 256) { shift2 = M_DoShift(item, &back_right_pos2, &back_right_pos); if ((shift2 > 0 && shift >= 0) || (shift2 < 0 && shift <= 0)) { shift += shift2; } } room_num = item->room_num; SECTOR *const sector2 = Room_GetSector(item->pos, &room_num); const int32_t height2 = Room_GetHeight(sector2, item->pos); if (height2 < item->pos.y - 256) { M_DoShift(item, &item->pos, &old_pos); } quad->extra_rotation = shift; const int32_t anim = M_GetCollisionAnim(item, &new_pos); if (anim != 0) { const int32_t dx = item->pos.x - old_pos.x; const int32_t dz = item->pos.z - old_pos.z; int32_t speed2 = (dx * Math_Sin(quad->momentum_angle) + dz * Math_Cos(quad->momentum_angle)) >> W2V_SHIFT; speed2 <<= 8; if (Lara_Vehicle_GetItem() == item && quad->velocity == 0xA000 && speed2 < 0x9FF6) { ITEM *const lara_item = Lara_GetItem(); lara_item->hit_points -= (0xA000 - speed2) >> 7; lara_item->hit_status = 1; } if (quad->velocity > 0 && speed2 < quad->velocity) { quad->velocity = speed2 < 0 ? 0 : speed2; } else if (quad->velocity < 0 && speed2 > quad->velocity) { quad->velocity = speed2 > 0 ? 0 : speed2; } if (quad->velocity < -0x3000) { quad->velocity = -0x3000; } } return anim; } static void M_AnimateQuadBike( ITEM *const item, const int32_t hit_wall, const bool killed) { int16_t state; ITEM *const lara_item = Lara_GetItem(); M_PRIV *const p = item->priv; M_QUAD_BIKE_INFO *const quad = &p->quad; state = lara_item->current_anim_state; if (item->pos.y != item->floor && state != M_STATE_FALL && state != M_STATE_LAND && state != M_STATE_FALL_OFF && !killed) { if (quad->velocity < 0) { Item_SwitchToObjAnim(lara_item, 6, 0, O_LARA_VEHICLE_ANIM); } else { Item_SwitchToObjAnim(lara_item, 25, 0, O_LARA_VEHICLE_ANIM); } lara_item->current_anim_state = M_STATE_FALL; lara_item->goal_anim_state = M_STATE_FALL; } else if ( hit_wall != 0 && state != M_STATE_HIT_FRONT && state != M_STATE_HIT_BACK && state != M_STATE_HIT_LEFT && state != M_STATE_HIT_RIGHT && state != M_STATE_FALL_OFF && quad->velocity > 0x3555 && !killed) { switch (hit_wall) { case 13: Item_SwitchToObjAnim(lara_item, 12, 0, O_LARA_VEHICLE_ANIM); lara_item->current_anim_state = M_STATE_HIT_FRONT; lara_item->goal_anim_state = M_STATE_HIT_FRONT; break; case 14: Item_SwitchToObjAnim(lara_item, 11, 0, O_LARA_VEHICLE_ANIM); lara_item->current_anim_state = M_STATE_HIT_BACK; lara_item->goal_anim_state = M_STATE_HIT_BACK; break; case 11: Item_SwitchToObjAnim(lara_item, 14, 0, O_LARA_VEHICLE_ANIM); lara_item->current_anim_state = M_STATE_HIT_LEFT; lara_item->goal_anim_state = M_STATE_HIT_LEFT; break; default: Item_SwitchToObjAnim(lara_item, 13, 0, O_LARA_VEHICLE_ANIM); lara_item->current_anim_state = M_STATE_HIT_RIGHT; lara_item->goal_anim_state = M_STATE_HIT_RIGHT; break; } Sound_Effect(SFX_QUAD_FRONT_IMPACT, &item->pos, SPM_NORMAL); } else { switch (lara_item->current_anim_state) { case M_STATE_DRIVE: if (killed) { if (quad->velocity <= 0x5000) { lara_item->goal_anim_state = M_STATE_BIKE_DEATH; } else { lara_item->goal_anim_state = M_STATE_FALL_DEATH; } } else if ( !(quad->velocity & 0xFFFFFF00) && !g_Input.jump && !g_Input.action) { lara_item->goal_anim_state = M_STATE_STOP; } else if (g_Input.left && !m_HandbrakeStarting) { lara_item->goal_anim_state = M_STATE_TURN_L; } else if (g_Input.right && !m_HandbrakeStarting) { lara_item->goal_anim_state = M_STATE_TURN_R; } else if (g_Input.jump) { if (quad->velocity <= 0x6AAA) { lara_item->goal_anim_state = M_STATE_SLOW; } else { lara_item->goal_anim_state = M_STATE_BRAKE; } } break; case 2: if (!(quad->velocity & 0xFFFFFF00)) { lara_item->goal_anim_state = M_STATE_STOP; } else if (g_Input.right) { Item_SwitchToObjAnim(lara_item, 20, 0, O_LARA_VEHICLE_ANIM); lara_item->current_anim_state = M_STATE_TURN_R; lara_item->goal_anim_state = M_STATE_TURN_R; } else if (!g_Input.left) { lara_item->goal_anim_state = M_STATE_DRIVE; } break; case 5: case 6: case 18: if (!(quad->velocity & 0xFFFFFF00)) { lara_item->goal_anim_state = M_STATE_STOP; } else if (g_Input.left) { lara_item->goal_anim_state = M_STATE_TURN_L; } else if (g_Input.right) { lara_item->goal_anim_state = M_STATE_TURN_R; } break; case 8: if (item->pos.y == item->floor) { lara_item->goal_anim_state = M_STATE_LAND; } else if (item->fall_speed > 240 && !Game_IsInGym()) { quad->flags |= 0x40; } break; case 11: case 12: case 13: case 14: if (g_Input.jump || g_Input.action) { lara_item->goal_anim_state = M_STATE_DRIVE; } break; case 15: if (killed) { lara_item->goal_anim_state = M_STATE_BIKE_DEATH; break; } if (g_Input.roll && !quad->velocity && !m_DontExitQuad) { if (g_Input.right && M_SkidooCanGetOff(1)) { lara_item->goal_anim_state = M_STATE_GET_OFF_R; } else if (g_Input.left && M_SkidooCanGetOff(-1)) { lara_item->goal_anim_state = M_STATE_GET_OFF_L; } } else if (g_Input.jump || g_Input.action) { lara_item->goal_anim_state = M_STATE_DRIVE; } break; case 22: if (!(quad->velocity & 0xFFFFFF00)) { lara_item->goal_anim_state = M_STATE_STOP; } else if (g_Input.left) { Item_SwitchToObjAnim(lara_item, 3, 0, O_LARA_VEHICLE_ANIM); lara_item->current_anim_state = M_STATE_TURN_L; lara_item->goal_anim_state = M_STATE_TURN_L; } else if (!g_Input.right) { lara_item->goal_anim_state = M_STATE_DRIVE; } break; } } if (Room_Get(item->room_num)->flags.underwater || Room_Get(item->room_num)->flags.swamp) { lara_item->goal_anim_state = M_STATE_FALL_OFF; lara_item->hit_points = 0; M_Explode(item); } } static bool M_UserControl(ITEM *item, int32_t height, int32_t *pitch) { M_QUAD_BIKE_INFO *quad; M_PRIV *const p = item->priv; quad = &p->quad; if (!quad->velocity && !g_Input.sprint && !m_CanHandbrakeStart) { m_CanHandbrakeStart = true; } else if (quad->velocity) { m_CanHandbrakeStart = false; } if (!g_Input.sprint) { m_HandbrakeStarting = 0; } if (!m_HandbrakeStarting) { if (quad->revs > 16) { quad->velocity += quad->revs >> 4; quad->revs -= quad->revs >> 3; } else { quad->revs = 0; } } if (item->pos.y < height - 256) { if (quad->engine_revs < 0xA000) { quad->engine_revs += (0xA000 - quad->engine_revs) >> 3; } } else { if (!quad->velocity && g_Input.look) { Lara_Look_UpDown(); } if (quad->velocity > 0) { if (g_Input.sprint && !m_HandbrakeStarting && quad->velocity > 0x3000) { if (g_Input.left) { quad->skidoo_turn -= 500; if (quad->skidoo_turn < -0x5B0) { quad->skidoo_turn = -0x5B0; } } else if (g_Input.right) { quad->skidoo_turn += 500; if (quad->skidoo_turn > 0x5B0) { quad->skidoo_turn = 0x5B0; } } } else { if (g_Input.left) { quad->skidoo_turn -= 455; if (quad->skidoo_turn < -910) { quad->skidoo_turn = -910; } } else if (g_Input.right) { quad->skidoo_turn += 455; if (quad->skidoo_turn > 910) { quad->skidoo_turn = 910; } } } } else if (quad->velocity < 0) { if (g_Input.sprint && !m_HandbrakeStarting && quad->velocity < -0x2800) { if (g_Input.right) { quad->skidoo_turn -= 500; if (quad->skidoo_turn < -0x5B0) { quad->skidoo_turn = -0x5B0; } } else if (g_Input.left) { quad->skidoo_turn += 500; if (quad->skidoo_turn > 0x5B0) { quad->skidoo_turn = 0x5B0; } } } else { if (g_Input.right) { quad->skidoo_turn -= 455; if (quad->skidoo_turn < -910) { quad->skidoo_turn = -910; } } else if (g_Input.left) { quad->skidoo_turn += 455; if (quad->skidoo_turn > 910) { quad->skidoo_turn = 910; } } } } if (g_Input.jump) { if (g_Input.sprint && (m_CanHandbrakeStart || m_HandbrakeStarting)) { m_HandbrakeStarting = 1; quad->revs -= 512; if (quad->revs < -0x3000) { quad->revs = -0x3000; } } else if (quad->velocity > 0) { quad->velocity -= 640; } else if (quad->velocity > -0x3000) { quad->velocity -= 768; } } else if (g_Input.action) { if (g_Input.sprint && (m_CanHandbrakeStart || m_HandbrakeStarting)) { m_HandbrakeStarting = 1; quad->revs += 512; if (quad->revs >= 0xA000) { quad->revs = 0xA000; } } else if (quad->velocity < 0xA000) { if (quad->velocity < 0x4000) { quad->velocity += ((0x4800 - quad->velocity) >> 3) + 8; } else if (quad->velocity < 0x7000) { quad->velocity += ((0x7800 - quad->velocity) >> 4) + 4; } else { quad->velocity += ((0xA000 - quad->velocity) >> 3) + 2; } } else { quad->velocity = 0xA000; } } else if (quad->velocity > 256) { quad->velocity -= 256; } else if (quad->velocity < -256) { quad->velocity += 256; } else { quad->velocity = 0; } if (m_HandbrakeStarting && quad->revs && !g_Input.jump && !g_Input.action) { if (quad->revs > 8) { quad->revs -= quad->revs >> 3; } else { quad->revs = 0; } } item->speed = quad->velocity >> 8; if (quad->engine_revs > 0x7000) { quad->engine_revs = -0x2000; } int32_t revs = 0; if (quad->velocity < 0) { revs = ABS(quad->revs) + ABS(quad->velocity >> 1); } else if (quad->velocity < 0x7000) { revs = ABS(quad->revs) + 0x8800 * quad->velocity / 0x7000 - 0x2000; } else if (quad->velocity <= 0xA000) { revs = ABS(quad->revs) + 0x9800 * (quad->velocity - 0x7000) / 0x3000 - 0x2800; } else { revs += ABS(quad->revs); } quad->engine_revs += (revs - quad->engine_revs) >> 3; } *pitch = quad->engine_revs; return false; } bool QuadBike_Control(void) { ITEM *const item = Lara_Vehicle_GetItem(); ITEM *const lara_item = Lara_GetItem(); M_PRIV *const p = item->priv; M_QUAD_BIKE_INFO *const quad = &p->quad; int32_t hit_wall = M_SkidooDynamics(item); bool killed = false; int32_t pitch = 0; XYZ_32 front_left_pos = { 0 }; XYZ_32 front_right_pos = { 0 }; const int32_t front_left_height = M_TestHeight(item, 550, -260, &front_left_pos); const int32_t front_right_height = M_TestHeight(item, 550, 260, &front_right_pos); int16_t room_num = item->room_num; SECTOR *const sector = Room_GetSector(item->pos, &room_num); const int32_t height = Room_GetHeight(sector, item->pos); Room_TestTriggers(lara_item); if (lara_item->hit_points <= 0) { killed = true; g_Input.forward = 0; g_Input.back = 0; g_Input.left = 0; g_Input.right = 0; } int32_t driving = 0; if (quad->flags != 0) { driving = front_right_height; // what hit_wall = 0; } else { switch (lara_item->current_anim_state) { case M_STATE_GET_ON_R: case M_STATE_GET_OFF_R: case M_STATE_GET_ON_L: case M_STATE_GET_OFF_L: driving = -1; hit_wall = 0; break; default: driving = M_UserControl(item, height, &pitch); break; } } if (quad->velocity != 0 || quad->revs != 0) { quad->pitch = pitch; if (quad->pitch < -0x8000) { quad->pitch = -0x8000; } else if (quad->pitch > 0xA000) { quad->pitch = 0xA000; } Sound_Effect( SFX_QUAD_MOVE, &item->pos, SPM_PITCH | ((SOUND_DEFAULT_PITCH + quad->pitch) << 8)); } else { if (driving != -1) { Sound_Effect(SFX_QUAD_IDLE, &item->pos, SPM_NORMAL); } quad->pitch = 0; } item->floor = height; // Cap per-frame wheel rotation delta to avoid large int16 angle jumps. // Large jumps can make interpolation pick the "short way" and appear to // spin backwards at high speed. int32_t wheel_delta = quad->velocity >> 2; CLAMP(wheel_delta, -0x1000, 0x1000); quad->front_rot = quad->front_rot - wheel_delta; int32_t rear_delta = wheel_delta + (quad->revs >> 3); CLAMP(rear_delta, -0x1000, 0x1000); quad->rear_rot = quad->rear_rot - rear_delta; if (p->extra_rotation != nullptr) { if (p->rear_rot_x_idx[0] >= 0) { p->extra_rotation[p->rear_rot_x_idx[0]] = quad->rear_rot; } if (p->rear_rot_x_idx[1] >= 0) { p->extra_rotation[p->rear_rot_x_idx[1]] = quad->rear_rot; } if (p->front_rot_x_idx[0] >= 0) { p->extra_rotation[p->front_rot_x_idx[0]] = quad->front_rot; } if (p->front_rot_x_idx[1] >= 0) { p->extra_rotation[p->front_rot_x_idx[1]] = quad->front_rot; } } quad->left_fall_speed = M_DoDynamics( front_left_height, quad->left_fall_speed, &front_left_pos.y); quad->right_fall_speed = M_DoDynamics( front_right_height, quad->right_fall_speed, &front_right_pos.y); item->fall_speed = (int16_t)M_DoDynamics(height, item->fall_speed, &item->pos.y); const int32_t avg_front_height = (front_left_pos.y + front_right_pos.y) >> 1; const int16_t x_rot = (int16_t)Math_Atan(550, item->pos.y - avg_front_height); const int16_t z_rot = (int16_t)Math_Atan(260, avg_front_height - front_left_pos.y); item->rot.x += (x_rot - item->rot.x) >> 1; item->rot.z += (z_rot - item->rot.z) >> 1; const LARA_INFO *const lara = Lara_GetLaraInfo(); if (!(quad->flags & 0x80)) { if (room_num != item->room_num) { Item_UpdateRoom(Lara_Vehicle_GetIndex(), room_num); Item_UpdateRoom(lara->item_num, room_num); } lara_item->pos.x = item->pos.x; lara_item->pos.y = item->pos.y; lara_item->pos.z = item->pos.z; lara_item->rot.x = item->rot.x; lara_item->rot.y = item->rot.y; lara_item->rot.z = item->rot.z; M_AnimateQuadBike(item, hit_wall, killed); Item_Animate(lara_item); item->anim_num = lara_item->anim_num + Object_Get(O_QUAD_BIKE)->anim_idx - Object_Get(O_LARA_VEHICLE_ANIM)->anim_idx; item->frame_num = lara_item->frame_num + Item_GetAnim(item)->frame_base - Item_GetAnim(lara_item)->frame_base; g_Camera.target_elevation = -5460; if (quad->flags & 0x40 && item->pos.y == item->floor) { Item_Explode(lara->item_num, -1, 0); lara_item->hit_points = 0; lara_item->flags |= IF_INVISIBLE; M_Explode(item); return false; } } const int16_t state = lara_item->current_anim_state; if (state != M_STATE_GET_ON_R && state != M_STATE_GET_ON_L && state != M_STATE_GET_OFF_R && state != M_STATE_GET_OFF_L) { XYZ_32 pos = { 0 }; for (int32_t i = 0; i < 2; i++) { pos.x = m_QuadBites[i].pos.x; pos.y = m_QuadBites[i].pos.y; pos.z = m_QuadBites[i].pos.z; Collide_GetJointAbsPosition(item, &pos, m_QuadBites[i].mesh_num); const int16_t smoke_rot = item->rot.y + (i == 0 ? 0x9000 : 0x7000); if (item->speed > 32) { const int32_t smoke_vel = MINMAX(96 - item->speed, 8, 64); M_TriggerExhaustSmoke(pos, smoke_rot, smoke_vel, true); } else { int32_t smoke_vel = 0; if (m_ExhaustSmokeVel < 16) { smoke_vel = ((Random_GetControl() & 7) + (Random_GetControl() & 0x10) + 2 * m_ExhaustSmokeVel) << 7; m_ExhaustSmokeVel++; } else if (m_HandbrakeStarting) { smoke_vel = (ABS(quad->revs) >> 2) + ((Random_GetControl() & 7) << 7); } else if (Random_GetControl() & 3) { smoke_vel = 0; } else { smoke_vel = ((Random_GetControl() & 0xF) + (Random_GetControl() & 0x10)) << 7; } M_TriggerExhaustSmoke(pos, smoke_rot, smoke_vel, false); } } } else { if (Game_IsInGym()) { Gym_TrackManager_Reset(GYM_TRACK_ASSAULT); } m_ExhaustSmokeVel = 0; } return M_CheckGetOff(); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->collision_func = M_Collision; obj->draw_func = Object_DrawAnimatingItem; obj->save_position = true; obj->save_flags = true; obj->save_anim = true; M_EnableWheelExtraRotations(obj); } REGISTER_OBJECT(O_QUAD_BIKE, M_Setup) ================================================ FILE: src/trx/game/objects/vehicles/quad_bike.h ================================================ #pragma once bool QuadBike_Control(void); ================================================ FILE: src/trx/game/objects/vehicles/rib.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // clang-format off #define M_RADIUS 500 #define M_COLL_DIST SQUARE(M_RADIUS * 2) // = 1000000 #define M_FRONT 750 #define M_SIDE 300 #define M_TIP (M_FRONT + 250) // = 1000 #define M_TURN (DEG_1 / 8) // = 22 #define M_UNDO_TURN (DEG_1 / 4) // = 45 #define M_MAX_TURN (DEG_1 * 4) // = 728 #define M_MIN_SPEED 20 #define M_MAX_SPEED 110 #define M_FAST_SPEED 185 #define M_SLOW_SPEED 36 #define M_ACCELERATION 5 #define M_REVERSE_SPEED 2 #define M_SHIFT_Y (-5) #define M_CAM_ELEVATION (DEG_1 * -20) // = -3640 #define M_CAM_DISTANCE (WALL_L * 2) // = 2048 // clang-format on typedef enum { M_STATE_MOUNT, M_STATE_STILL, M_STATE_MOVING, M_STATE_JUMP_R, M_STATE_JUMP_L, M_STATE_HIT, M_STATE_FALL, M_STATE_TURN_R, M_STATE_DEATH, M_STATE_TURN_L, } M_STATE; typedef enum { // clang-format off M_ANIM_MOUNT_LEFT = 0, M_ANIM_MOUNT_START = 1, M_ANIM_MOUNT_JUMP = 6, M_ANIM_MOUNT_RIGHT = 8, M_ANIM_FALL = 15, M_ANIM_DEATH = 18, // clang-format on } M_ANIM; typedef enum { M_MOUNT_NONE, M_MOUNT_LEFT, M_MOUNT_RIGHT, M_MOUNT_JUMP, M_MOUNT_START, } M_MOUNT_TYPE; typedef struct { int32_t boat_turn; int32_t left_fallspeed; int32_t right_fallspeed; int16_t tilt_angle; int16_t extra_rotation; int32_t water; int32_t pitch; int16_t propeller_roll; } M_PRIV; static const BITE m_Propeller = { .pos = { .x = 0, .y = 0, .z = -80 }, .mesh_num = 2, }; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "boat_turn", &p->boat_turn)); JSON_SHOULD(JSON_READ(io, "left_fallspeed", &p->left_fallspeed)); JSON_SHOULD(JSON_READ(io, "right_fallspeed", &p->right_fallspeed)); JSON_SHOULD(JSON_READ(io, "tilt_angle", &p->tilt_angle)); JSON_SHOULD(JSON_READ(io, "extra_rotation", &p->extra_rotation)); JSON_SHOULD(JSON_READ(io, "water", &p->water)); JSON_SHOULD(JSON_READ(io, "pitch", &p->pitch)); JSON_SHOULD(JSON_READ(io, "propeller_roll", &p->propeller_roll)); } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "boat_turn", p->boat_turn); JSONW_WRITE(io, "left_fallspeed", p->left_fallspeed); JSONW_WRITE(io, "right_fallspeed", p->right_fallspeed); JSONW_WRITE(io, "tilt_angle", p->tilt_angle); JSONW_WRITE(io, "extra_rotation", p->extra_rotation); JSONW_WRITE(io, "water", p->water); JSONW_WRITE(io, "pitch", p->pitch); JSONW_WRITE(io, "propeller_roll", p->propeller_roll); } static void M_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; item->extra_rotations = &p->propeller_roll; FX_Wake_Reset(); } static M_MOUNT_TYPE M_CheckMount( const int16_t item_num, const COLL_INFO *const coll) { const LARA_INFO *const lara = Lara_GetLaraInfo(); if (lara->gun_status != LGS_ARMLESS) { return M_MOUNT_NONE; } ITEM *const item = Item_Get(item_num); ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; if ((dz * Math_Cos(-item->rot.y) - dx * Math_Sin(-item->rot.y)) >> W2V_SHIFT > WALL_L / 2) { return M_MOUNT_NONE; } M_MOUNT_TYPE result = M_MOUNT_NONE; const int16_t angle = item->rot.y - lara_item->rot.y; if (lara->water_status == LWS_SURFACE || lara->water_status == LWS_WADE) { if (!g_Input.action || lara_item->gravity || item->speed) { return M_MOUNT_NONE; } if (angle > DEG_45 && angle < DEG_135) { result = M_MOUNT_RIGHT; } else if (angle > -DEG_135 && angle < -DEG_45) { result = M_MOUNT_LEFT; } } else if (lara->water_status == LWS_ABOVE_WATER) { if (lara_item->fall_speed > 0) { if (lara_item->pos.y + 512 > item->pos.y) { result = M_MOUNT_JUMP; } } else if (lara_item->fall_speed == 0) { if (angle > -DEG_135 && angle < DEG_135) { result = XYZ_32_AreEquivalent(lara_item->pos, item->pos) ? M_MOUNT_START : M_MOUNT_JUMP; } } } if (result != M_MOUNT_NONE) { if (!Item_TestBoundsCollide(item, lara_item, coll->radius)) { return M_MOUNT_NONE; } if (!Collide_TestCollision(item, lara_item)) { return M_MOUNT_NONE; } } return result; } static bool M_CheckDismount(const M_MOUNT_TYPE type) { const ITEM *const item = Lara_Vehicle_GetItem(); int16_t angle = item->rot.y; if (type == M_MOUNT_RIGHT) { angle += DEG_90; } else { angle -= DEG_90; } const XYZ_32 pos = XYZ_32_OffsetYaw(item->pos, angle, WALL_L); int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); const int32_t height = Room_GetHeight(sector, pos); const HEIGHT_TYPE height_type = Room_GetHeight(sector, pos); if (height_type == HT_BIG_SLOPE || height_type == HT_DIAGONAL || height - item->pos.y < -WALL_L / 2) { return false; } const int32_t ceiling = Room_GetCeiling(sector, pos); if (ceiling - item->pos.y > -LARA_HEIGHT) { return false; } if (height - ceiling < LARA_HEIGHT) { return false; } return true; } static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { if (lara_item->hit_points < 0 || Lara_Vehicle_IsMounted()) { return; } const M_MOUNT_TYPE mount_type = M_CheckMount(item_num, coll); if (mount_type == M_MOUNT_NONE) { coll->enable_baddie_push = true; Object_Collision(item_num, lara_item, coll); return; } Lara_Vehicle_SetIndex(item_num); M_ANIM boat_anim_idx; switch (mount_type) { case M_MOUNT_RIGHT: boat_anim_idx = M_ANIM_MOUNT_RIGHT; break; case M_MOUNT_LEFT: boat_anim_idx = M_ANIM_MOUNT_LEFT; break; case M_MOUNT_JUMP: boat_anim_idx = M_ANIM_MOUNT_JUMP; break; default: boat_anim_idx = M_ANIM_MOUNT_START; break; } Item_SwitchToObjAnim(lara_item, boat_anim_idx, 0, O_LARA_VEHICLE_ANIM); lara_item->current_anim_state = M_STATE_MOUNT; lara_item->goal_anim_state = M_STATE_MOUNT; LARA_INFO *const lara = Lara_GetLaraInfo(); lara->water_status = LWS_ABOVE_WATER; ITEM *const item = Item_Get(item_num); lara_item->pos.x = item->pos.x; lara_item->pos.y = item->pos.y + M_SHIFT_Y; lara_item->pos.z = item->pos.z; lara_item->rot.x = 0; lara_item->rot.y = item->rot.y; lara_item->rot.z = 0; lara_item->gravity = 0; lara_item->speed = 0; lara_item->fall_speed = 0; if (lara_item->room_num != item->room_num) { Item_UpdateRoom(lara->item_num, item->room_num); } Item_Animate(lara_item); if (item->status != IS_ACTIVE) { Item_AddActive(item_num); item->status = IS_ACTIVE; } Music_Play(MX_RIB_THEME, MPM_ONCE); } static bool M_UserControl(ITEM *const item) { M_PRIV *const p = item->priv; bool no_turn = true; if (item->pos.y < p->water - STEP_L / 2 || p->water == NO_HEIGHT) { return no_turn; } const bool input_left = g_Input.left || g_Input.step_left; const bool input_right = g_Input.right || g_Input.step_right; if ((g_Input.roll || g_Input.look) && item->speed == 0) { if (!input_left && !input_right) { item->speed = 0; } else if (!g_Input.roll) { item->speed = M_MIN_SPEED; } if (g_Input.look && item->speed == 0) { Lara_Look_UpDown(); } return no_turn; } if ((input_left && !g_Input.jump) || (input_right && g_Input.jump)) { if (p->boat_turn > 0) { p->boat_turn -= M_UNDO_TURN; } else { p->boat_turn -= M_TURN; CLAMPL(p->boat_turn, -M_MAX_TURN); } no_turn = false; } else if ((input_right && !g_Input.jump) || (input_left && g_Input.jump)) { if (p->boat_turn < 0) { p->boat_turn += M_UNDO_TURN; } else { p->boat_turn += M_TURN; CLAMPG(p->boat_turn, M_MAX_TURN); } no_turn = false; } if (g_Input.jump) { if (item->speed > 0) { item->speed -= M_ACCELERATION; } else if (item->speed > -M_MIN_SPEED) { item->speed -= M_REVERSE_SPEED; } } else if (g_Input.action) { int16_t max_speed; if (g_Input.sprint) { max_speed = M_FAST_SPEED; } else { max_speed = g_Input.slow ? M_SLOW_SPEED : M_MAX_SPEED; } if (item->speed < max_speed) { item->speed = M_ACCELERATION * item->speed / (2 * max_speed) + item->speed + 2; } else if (item->speed > max_speed + 1) { item->speed--; } } else if ( item->speed >= 0 && item->speed < M_MIN_SPEED && (input_left || input_right)) { if (item->speed == 0 && !g_Input.roll) { item->speed = M_MIN_SPEED; } } else if (item->speed > 1) { item->speed--; } else { item->speed = 0; } return no_turn; } static void M_Animate(const ITEM *const item, const M_ANIM collide_anim) { const M_PRIV *const p = item->priv; ITEM *const lara_item = Lara_GetItem(); if (lara_item->hit_points <= 0) { if (lara_item->current_anim_state != M_STATE_DEATH) { Item_SwitchToObjAnim( lara_item, M_ANIM_DEATH, 0, O_LARA_VEHICLE_ANIM); lara_item->current_anim_state = M_STATE_DEATH; lara_item->goal_anim_state = M_STATE_DEATH; } return; } if (item->pos.y < p->water - STEP_L / 2 && item->fall_speed > 0) { if (lara_item->current_anim_state != M_STATE_FALL) { Item_SwitchToObjAnim( lara_item, M_ANIM_FALL, 0, O_LARA_VEHICLE_ANIM); lara_item->current_anim_state = M_STATE_FALL; lara_item->goal_anim_state = M_STATE_FALL; } return; } if (collide_anim != 0) { if (lara_item->current_anim_state != M_STATE_HIT) { Item_SwitchToObjAnim( lara_item, collide_anim, 0, O_LARA_VEHICLE_ANIM); lara_item->current_anim_state = M_STATE_HIT; lara_item->goal_anim_state = M_STATE_HIT; } return; } const bool input_left = g_Input.left || g_Input.step_left; const bool input_right = g_Input.right || g_Input.step_right; switch (lara_item->current_anim_state) { case M_STATE_STILL: if (g_Input.roll && item->speed == 0) { if (input_right && M_CheckDismount(M_MOUNT_RIGHT)) { lara_item->goal_anim_state = M_STATE_JUMP_R; } else if (input_left && M_CheckDismount(M_MOUNT_LEFT)) { lara_item->goal_anim_state = M_STATE_JUMP_L; } } if (item->speed > 0) { lara_item->goal_anim_state = M_STATE_MOVING; } break; case M_STATE_MOVING: if (item->speed <= 0) { lara_item->goal_anim_state = M_STATE_STILL; } else if (input_right) { lara_item->goal_anim_state = M_STATE_TURN_R; } else if (input_left) { lara_item->goal_anim_state = M_STATE_TURN_L; } break; case M_STATE_FALL: lara_item->goal_anim_state = M_STATE_MOVING; break; case M_STATE_TURN_R: if (item->speed <= 0) { lara_item->goal_anim_state = M_STATE_STILL; } else if (!input_right) { lara_item->goal_anim_state = M_STATE_MOVING; } break; case M_STATE_TURN_L: if (item->speed <= 0) { lara_item->goal_anim_state = M_STATE_STILL; } else if (!input_left) { lara_item->goal_anim_state = M_STATE_MOVING; } break; } } static int32_t M_TestWaterHeight( const ITEM *const item, const int32_t z_off, const int32_t x_off, XYZ_32 *const pos) { const int32_t sx = Math_Sin(item->rot.x); const int32_t sz = Math_Sin(item->rot.z); pos->y = item->pos.y + ((x_off * sz) >> W2V_SHIFT) - ((z_off * sx) >> W2V_SHIFT); const int32_t sy = Math_Sin(item->rot.y); const int32_t cy = Math_Cos(item->rot.y); pos->x = item->pos.x + ((x_off * cy + z_off * sy) >> W2V_SHIFT); pos->z = item->pos.z + ((z_off * cy - x_off * sy) >> W2V_SHIFT); int16_t room_num = item->room_num; Room_GetSector(*pos, &room_num); int32_t height = Room_GetWaterHeight(*pos, room_num); if (height == NO_HEIGHT) { const SECTOR *const sector = Room_GetSector(*pos, &room_num); height = Room_GetHeight(sector, *pos); if (height == NO_HEIGHT) { return height; } } return height + M_SHIFT_Y; } static void M_DoShift(const int32_t item_num) { ITEM *const item = Item_Get(item_num); int16_t next_item_num = Room_Get(item->room_num)->item_num; while (next_item_num != NO_ITEM) { const ITEM *const next_item = Item_Get(next_item_num); if ((next_item->object_id == O_RIB || next_item->object_id == O_BOAT) && next_item_num != Lara_Vehicle_GetIndex() && next_item_num != item_num) { const int32_t dx = next_item->pos.x - item->pos.x; const int32_t dz = next_item->pos.z - item->pos.z; const int32_t dist = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz }); if (dist < M_COLL_DIST) { item->pos.x = next_item->pos.x - M_COLL_DIST * dx / dist; item->pos.z = next_item->pos.z - M_COLL_DIST * dz / dist; } break; } next_item_num = next_item->next_item; } } static int32_t M_DoDynamics( const int32_t height, int32_t fall_speed, int32_t *const y_pos) { if (height > *y_pos) { *y_pos += fall_speed; if (*y_pos <= height) { fall_speed += GRAVITY; } else { *y_pos = height; fall_speed = 0; } } else { fall_speed += (height - fall_speed - *y_pos) >> 3; CLAMPG(*y_pos, height); } return fall_speed; } static inline int32_t M_DoCornerShift( ITEM *const item, const int32_t x_off, const int32_t z_off, const XYZ_32 *const old_pos) { XYZ_32 pos; const int32_t h = M_TestWaterHeight(item, x_off, z_off, &pos); if (h < old_pos->y - STEP_L / 2) { return Vehicle_DoShift(item, &pos, old_pos); } return 0; } static int32_t M_Dynamics(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; item->rot.z -= p->tilt_angle; XYZ_32 fl_old; XYZ_32 fr_old; XYZ_32 bl_old; XYZ_32 br_old; XYZ_32 f_old; const int32_t hfl_old = M_TestWaterHeight(item, M_FRONT, -M_SIDE, &fl_old); const int32_t hfr_old = M_TestWaterHeight(item, M_FRONT, M_SIDE, &fr_old); const int32_t hbl_old = M_TestWaterHeight(item, -M_FRONT, -M_SIDE, &bl_old); const int32_t hbr_old = M_TestWaterHeight(item, -M_FRONT, M_SIDE, &br_old); const int32_t hf_old = M_TestWaterHeight(item, M_TIP, 0, &f_old); CLAMPG(bl_old.y, hbl_old); CLAMPG(br_old.y, hbr_old); CLAMPG(fl_old.y, hfl_old); CLAMPG(fr_old.y, hfr_old); CLAMPG(f_old.y, hf_old); XYZ_32 old_pos = item->pos; item->rot.y += p->extra_rotation + p->boat_turn; p->tilt_angle = p->boat_turn * 6; item->pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, item->speed); if (item->speed < 0) { p->propeller_roll += DEG_1 * 33; } else { p->propeller_roll += DEG_1 * (3 * item->speed + 2); } int32_t slip = (Math_Sin(item->rot.z) * 30) >> W2V_SHIFT; if (slip == 0 && item->rot.z != 0) { slip = item->rot.z > 0 ? 1 : -1; } item->pos.x += (slip * Math_Cos(item->rot.y)) >> W2V_SHIFT; item->pos.z -= (slip * Math_Sin(item->rot.y)) >> W2V_SHIFT; slip = (Math_Sin(item->rot.x) * 10) >> W2V_SHIFT; if (slip == 0 && item->rot.x != 0) { slip = item->rot.x > 0 ? 1 : -1; } item->pos.x -= (slip * Math_Sin(item->rot.y)) >> W2V_SHIFT; item->pos.z -= (slip * Math_Cos(item->rot.y)) >> W2V_SHIFT; XYZ_32 moved = { .x = item->pos.x, .y = 0, .z = item->pos.z, }; M_DoShift(item_num); int32_t rot = 0; rot += M_DoCornerShift(item, -M_FRONT, -M_SIDE, &bl_old); rot += M_DoCornerShift(item, -M_FRONT, +M_SIDE, &br_old); rot += M_DoCornerShift(item, +M_FRONT, -M_SIDE, &fl_old); rot += M_DoCornerShift(item, +M_FRONT, +M_SIDE, &fr_old); if (slip == 0) { M_DoCornerShift(item, M_TIP, 0, &f_old); } int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); int32_t height = Room_GetWaterHeight(item->pos, room_num); if (height == NO_HEIGHT) { height = Room_GetHeight(sector, item->pos); } if (height < item->pos.y - STEP_L / 2) { Vehicle_DoShift(item, &item->pos, &old_pos); } p->extra_rotation = rot; const int32_t coll_anim = Vehicle_GetCollisionAnim(item, &moved); if (slip != 0 || coll_anim != 0) { const int32_t sx = (item->pos.x - old_pos.x) * Math_Sin(item->rot.y); const int32_t sz = (item->pos.z - old_pos.z) * Math_Cos(item->rot.y); int32_t new_speed = (sx + sz) >> W2V_SHIFT; if (Lara_Vehicle_GetIndex() == item_num && item->speed > (M_MAX_SPEED + M_ACCELERATION) && new_speed < item->speed - 10) { Lara_TakeDamage(item->speed, true); Sound_Effect(SFX_LARA_INJURY, &Lara_GetItem()->pos, SPM_NORMAL); new_speed >>= 1; item->speed >>= 1; } if (slip != 0) { if (item->speed <= M_MAX_SPEED + 10) { item->speed = new_speed; } } else if (item->speed > 0 && new_speed < item->speed) { item->speed = new_speed; } else if (item->speed < 0 && new_speed > item->speed) { item->speed = new_speed; } CLAMPL(item->speed, -M_MIN_SPEED); } return coll_anim; } static void M_Splash( const ITEM *const item, const int32_t fall_speed, const int32_t water_height) { FX_WATER_SPLASH_SETUP splash_setup = { .x = item->pos.x, .y = water_height, .z = item->pos.z, .inner_xz_off = 64, .inner_xz_size = 48, .inner_y_size = -384, .inner_xz_vel = 160, .inner_y_vel = -128 * fall_speed, .inner_gravity = 128, .inner_friction = 7, .middle_xz_off = 96, .middle_xz_size = 96, .middle_y_size = -256, .middle_xz_vel = 224, .middle_y_vel = (-64 * fall_speed), .middle_gravity = 72, .middle_friction = 8, .outer_xz_off = 128, .outer_xz_size = 128, .outer_xz_vel = 272, .outer_friction = 9, }; FX_Water_SetupSplash(&splash_setup); } static void M_TriggerMist( const XYZ_32 pos, const int32_t speed, const int16_t angle, const int32_t snow) { SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 0; spark->src_color.g = 0; spark->src_color.b = 0; if (snow != 0) { spark->dst_color.r = 255; spark->dst_color.g = 255; spark->dst_color.b = 255; } else { spark->dst_color.r = 64; spark->dst_color.g = 64; spark->dst_color.b = 64; } spark->col_fade_speed = (Random_GetControl() & 3) + 4; spark->fade_to_black = 12 - (snow << 3); spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->life = (Random_GetControl() & 3) + 20; spark->s_life = spark->life; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 0xF) - 8; spark->pos.y = pos.y + (Random_GetControl() & 0xF) - 8; spark->pos.z = pos.z + (Random_GetControl() & 0xF) - 8; spark->vel.x = (Random_GetControl() & 0x7F) + ((speed * Math_Sin(angle)) >> (W2V_SHIFT + 2)) - 64; spark->vel.y = 12 * speed; spark->vel.z = (Random_GetControl() & 0x7F) + ((speed * Math_Cos(angle)) >> (W2V_SHIFT + 2)) - 64; spark->friction = 3; if ((Random_GetControl() & 1) != 0) { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = Random_GetControl() & 0xFFF; if ((Random_GetControl() & 1) != 0) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = +16 + (Random_GetControl() & 0xF); } } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; if (snow != 0) { spark->friction = 0; spark->scalar = 3; spark->vel.y = -spark->vel.y >> 5; spark->max_y_vel = 0; spark->gravity = (Random_GetControl() & 0x1F) + 32; const uint8_t size = (Random_GetControl() & 7) + 16; spark->size.width = size; spark->src_size.width = spark->size.width; spark->dst_size.width = spark->size.width; spark->size.height = size; spark->src_size.height = spark->size.height; spark->dst_size.height = spark->size.height; } else { spark->scalar = 4; spark->max_y_vel = 0; spark->gravity = 0; const uint8_t size = (Random_GetControl() & 7) + (speed >> 1) + 16; spark->dst_size.width = size; spark->size.width = spark->dst_size.width >> 2; spark->src_size.width = spark->dst_size.width >> 2; spark->dst_size.height = size; spark->src_size.height = spark->dst_size.height >> 2; spark->size.height = spark->dst_size.height >> 2; } Sparks_FinishSetup(spark); } static void M_DoWake( const ITEM *const item, const int32_t x_off, const int32_t z_off, const int16_t rotate) { const int32_t start_idx = FX_Wake_GetStartIndex(); if (FX_Wake_GetPoint(start_idx, rotate)->life != 0) { return; } XYZ_32 pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, z_off); pos = XYZ_32_OffsetYaw(pos, item->rot.y + DEG_90, x_off); pos.y += STEP_L / 2; int16_t room_num = item->room_num; Room_GetSector(pos, &room_num); const int32_t water_height = Room_GetWaterHeight(pos, room_num); if (water_height == NO_HEIGHT) { return; } int16_t angle1; int16_t angle2; if (item->speed >= 0) { if (rotate) { angle1 = item->rot.y + (DEG_1 * 160); angle2 = item->rot.y + (DEG_1 * 140); } else { angle1 = item->rot.y - (DEG_1 * 160); angle2 = item->rot.y - (DEG_1 * 140); } } else { if (rotate) { angle1 = item->rot.y + (DEG_1 * 20); angle2 = item->rot.y + (DEG_1 * 40); } else { angle1 = item->rot.y - (DEG_1 * 20); angle2 = item->rot.y - (DEG_1 * 40); } } XYZ_32 vel[2] = { XYZ_32_OffsetYaw((XYZ_32) {}, angle1, 4), XYZ_32_OffsetYaw((XYZ_32) {}, angle2, 10), }; FX_WAKE_POINT *const pt = FX_Wake_GetPoint(start_idx, rotate); pt->life = 64; for (int32_t i = 0; i < 2; i++) { pt->pos[i].x = pos.x; pt->pos[i].y = item->pos.y + 32; pt->pos[i].z = pos.z; pt->prev_pos[i] = pt->pos[i]; pt->vel[i].x = vel[i].x; pt->vel[i].z = vel[i].z; } if (rotate == 1) { FX_Wake_AdvanceStartIndex(); } } static void M_ControlWake(const ITEM *const item) { const XYZ_32 pos = { .x = item->pos.x, .y = item->pos.y + STEP_L / 2, .z = item->pos.z, }; int16_t room_num = item->room_num; Room_GetSector(pos, &room_num); const int32_t water_height = Room_GetWaterHeight(pos, room_num); const bool valid_height = water_height <= item->pos.y + 32 && water_height != NO_HEIGHT; const ITEM *const lara_item = Lara_GetItem(); const bool leaving = lara_item->current_anim_state == M_STATE_JUMP_R || lara_item->current_anim_state == M_STATE_JUMP_L; const int32_t time4 = Output_GetTimeInGame() * 4; if ((time4 & 0xF) == 0 && valid_height && !leaving) { M_DoWake(item, -384, 0, 0); M_DoWake(item, 384, 0, 1); } uint8_t wake_shade = FX_Wake_GetShade(); if (item->speed == 0 || !valid_height || leaving) { if (wake_shade != 0) { wake_shade--; } } else if (wake_shade < 16) { wake_shade++; } FX_Wake_SetShade(wake_shade); } static void M_ControlEffects(const ITEM *const item) { XYZ_32 pos = m_Propeller.pos; Collide_GetJointAbsPosition(item, &pos, m_Propeller.mesh_num); int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); const int32_t water_height = Room_GetWaterHeight(pos, room_num); if (item->speed != 0 && water_height < pos.y && water_height != NO_HEIGHT) { M_TriggerMist(pos, ABS(item->speed), item->rot.y + 0x8000, 0); if ((Random_GetControl() & 1) == 0) { XYZ_32 bubble_pos = { .x = pos.x + (Random_GetControl() & 0x3F) - 32, .y = pos.y + (Random_GetControl() & 0xF), .z = pos.z + (Random_GetControl() & 0x3F) - 32, }; room_num = item->room_num; Room_GetSector(bubble_pos, &room_num); Spawn_BubbleEx(&bubble_pos, room_num, 16, 8); } return; } const int32_t height = Room_GetHeight(sector, pos); if (pos.y > height && !(Room_Get(room_num)->flags.underwater)) { for (int32_t i = (Random_GetControl() & 3) + 3; i > 0; i--) { const int16_t angle = item->rot.y + Random_GetControl() + DEG_90; M_TriggerMist( pos, ((Random_GetControl() & 0xF) + 96) << 4, angle, 1); } } } static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; bool drive = false; bool no_turn = true; const int32_t coll_anim = M_Dynamics(item_num); XYZ_32 fl; XYZ_32 fr; const int32_t hfl = M_TestWaterHeight(item, M_FRONT, -M_SIDE, &fl); const int32_t hfr = M_TestWaterHeight(item, M_FRONT, M_SIDE, &fr); int16_t room_num = item->room_num; const SECTOR *sector = Room_GetSector(item->pos, &room_num); int32_t height = Room_GetHeight(sector, item->pos); Room_GetCeiling(sector, item->pos); ITEM *const lara_item = Lara_GetItem(); if (Lara_Vehicle_GetIndex() == item_num) { Room_TestTriggers(lara_item); Room_TestTriggers(item); } int32_t water_height = Room_GetWaterHeight(item->pos, room_num); p->water = water_height; if (Lara_Vehicle_GetIndex() == item_num && lara_item->hit_points > 0) { switch (lara_item->current_anim_state) { case M_STATE_MOUNT: case M_STATE_JUMP_R: case M_STATE_JUMP_L: break; default: drive = true; no_turn = M_UserControl(item); break; } } else if (item->speed > 1) { item->speed--; } else { item->speed = 0; } if (no_turn) { if (p->boat_turn < -M_UNDO_TURN) { p->boat_turn += M_UNDO_TURN; } else if (p->boat_turn > M_UNDO_TURN) { p->boat_turn -= M_UNDO_TURN; } else { p->boat_turn = 0; } } item->floor = height + M_SHIFT_Y; if (p->water == NO_HEIGHT) { p->water = height; } else { p->water += M_SHIFT_Y; } p->left_fallspeed = M_DoDynamics(hfl, p->left_fallspeed, &fl.y); p->right_fallspeed = M_DoDynamics(hfr, p->right_fallspeed, &fr.y); const int16_t fall_speed = item->fall_speed; item->fall_speed = M_DoDynamics(p->water, item->fall_speed, &item->pos.y); if (fall_speed - item->fall_speed > 32 && !item->fall_speed && water_height != NO_HEIGHT) { M_Splash(item, fall_speed - item->fall_speed, water_height); } height = fr.y + fl.y; if (height >= 0) { height >>= 1; } else { height = -ABS(height) >> 1; } const int16_t x_rot = Math_Atan(M_FRONT, item->pos.y - height); const int16_t z_rot = Math_Atan(M_SIDE, height - fl.y); item->rot.x += (x_rot - item->rot.x) / 2; item->rot.z += (z_rot - item->rot.z) / 2; if (x_rot == 0 && ABS(item->rot.x) < 4) { item->rot.x = 0; } if (z_rot == 0 && ABS(item->rot.z) < 4) { item->rot.z = 0; } if (Lara_Vehicle_GetIndex() == item_num) { M_Animate(item, coll_anim); if (room_num != item->room_num) { Item_UpdateRoom(item_num, room_num); Item_UpdateRoom(Item_GetIndex(lara_item), room_num); } item->rot.z += p->tilt_angle; lara_item->pos = item->pos; lara_item->rot = item->rot; Item_Animate(lara_item); if (lara_item->hit_points > 0) { const int16_t anim_idx = Item_GetRelativeObjAnim(lara_item, O_LARA_VEHICLE_ANIM); const int16_t frame_idx = Item_GetRelativeFrame(lara_item); Item_SwitchToAnim(item, anim_idx, frame_idx); } g_Camera.target_elevation = M_CAM_ELEVATION; g_Camera.target_distance = M_CAM_DISTANCE; } else { if (room_num != item->room_num) { Item_UpdateRoom(item_num, room_num); } item->rot.z += p->tilt_angle; } p->pitch += (item->speed - p->pitch) >> 2; if (item->speed > 8) { Sound_Effect( SFX_RIB_MOVING, &item->pos, SPM_PITCH | ((0x10000 - (M_MAX_SPEED - p->pitch) * 100) << 8)); } else if (drive) { Sound_Effect( SFX_RIB_IDLE, &item->pos, SPM_PITCH | ((0x10000 - (M_MAX_SPEED - p->pitch) * 100) << 8)); } if (Lara_Vehicle_GetIndex() != item_num) { return; } if ((lara_item->current_anim_state == M_STATE_JUMP_R || lara_item->current_anim_state == M_STATE_JUMP_L) && Item_TestFrameEqual(lara_item, -1)) { if (lara_item->current_anim_state == M_STATE_JUMP_L) { lara_item->rot.y -= DEG_90; } else { lara_item->rot.y += DEG_90; } Lara_Vehicle_Dismount(); Item_SwitchToAnim(lara_item, LA(LA_JUMP_FORWARD), 0); lara_item->current_anim_state = LS(LS_JUMP_FORWARD); lara_item->goal_anim_state = LS(LS_JUMP_FORWARD); lara_item->gravity = true; lara_item->fall_speed = -40; lara_item->speed = 20; lara_item->rot.x = 0; lara_item->rot.z = 0; room_num = lara_item->room_num; XYZ_32 pos = XYZ_32_OffsetYaw(lara_item->pos, lara_item->rot.y, 360); pos.y -= 90; sector = Room_GetSector(pos, &room_num); if (Room_GetHeight(sector, pos) >= pos.y - STEP_L) { lara_item->pos.x = pos.x; lara_item->pos.z = pos.z; Item_UpdateRoom(Item_GetIndex(lara_item), room_num); } lara_item->pos.y = pos.y; Item_SwitchToAnim(item, 0, 0); } M_ControlWake(item); M_ControlEffects(item); } static bool M_Draw(const ITEM *const item) { Object_DrawAnimatingItem(item); FX_Wake_Draw(item); return true; } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->initialise_func = M_Initialise; obj->collision_func = M_Collision; obj->control_func = M_Control; obj->draw_func = M_Draw; obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->save_position = true; obj->save_flags = true; obj->save_anim = true; Object_GetBone(obj, 1)->rot.z = true; } REGISTER_OBJECT(O_RIB, M_Setup) ================================================ FILE: src/trx/game/objects/vehicles/skidoo_armed.c ================================================ #include #include #include #include #include #define M_ARMED_RADIUS (WALL_L / 3) // = 341 static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); if (!Item_TestBoundsCollide(item, lara_item, coll->radius)) { return; } if (!Collide_TestCollision(item, lara_item)) { return; } if (coll->enable_baddie_push) { Lara_Col_ItemPush( item, coll, item->speed > 0 ? coll->enable_hit : false, false); } if (!Lara_Vehicle_IsMounted() && item->speed > 0) { Lara_TakeDamage(100, true); } } void SkidooArmed_Push( const ITEM *const item, ITEM *const lara_item, const int32_t radius) { const int32_t dx = lara_item->pos.x - item->pos.x; const int32_t dz = lara_item->pos.z - item->pos.z; const int32_t cy = Math_Cos(item->rot.y); const int32_t sy = Math_Sin(item->rot.y); int32_t rx = (cy * dx - sy * dz) >> W2V_SHIFT; int32_t rz = (sy * dx + cy * dz) >> W2V_SHIFT; const ANIM_FRAME *const best_frame = Item_GetBestFrame(item); BOUNDS_16 bounds = { .min.x = best_frame->bounds.min.x - radius, .max.x = best_frame->bounds.max.x + radius, .min.z = best_frame->bounds.min.z - radius, .max.z = best_frame->bounds.max.z + radius, }; if (rx < bounds.min.x || rx > bounds.max.x || rz < bounds.min.z || rz > bounds.max.z) { return; } const int32_t r = bounds.max.x - rx; const int32_t l = rx - bounds.min.x; const int32_t t = bounds.max.z - rz; const int32_t b = rz - bounds.min.z; if (l <= r && l <= t && l <= b) { rx -= l; } else if (r <= l && r <= t && r <= b) { rx += r; } else if (t <= l && t <= r && t <= b) { rz += t; } else { rz -= b; } lara_item->pos.x = item->pos.x + ((rz * sy + rx * cy) >> W2V_SHIFT); lara_item->pos.z = item->pos.z + ((rz * cy - rx * sy) >> W2V_SHIFT); } static void M_Setup(OBJECT *const obj) { if (!obj->loaded) { return; } obj->collision_func = M_Collision; obj->hit_points = SKIDOO_DRIVER_HITPOINTS; obj->radius = M_ARMED_RADIUS; obj->shadow_size = UNIT_SHADOW / 2; obj->pivot_length = 0; obj->lot_setup = LOT_Setup(LOT_SETUP_JUMPER); obj->intelligent = true; obj->save_position = true; obj->save_hitpoints = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_SKIDOO_ARMED, M_Setup) ================================================ FILE: src/trx/game/objects/vehicles/skidoo_armed.h ================================================ #pragma once #include void SkidooArmed_Push(const ITEM *item, ITEM *lara_item, int32_t radius); ================================================ FILE: src/trx/game/objects/vehicles/skidoo_common.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_RADIUS 500 #define M_SIDE 260 #define M_FRONT 550 #define M_SNOW 500 #define M_GET_OFF_DIST 330 #define M_TARGET_DIST (WALL_L * 2) // = 2048 #define M_ACCELERATION 10 #define M_SLOWDOWN 2 #define M_SLIP 100 #define M_SLIP_SIDE 50 #define M_MAX_BACK -30 #define M_BRAKE 5 #define M_REVERSE (-5) #define M_UNDO_TURN (DEG_1 * 2) // = 364 #define M_TURN (DEG_1 / 2 + M_UNDO_TURN) // = 455 #define M_MOMENTUM_TURN (DEG_1 * 3) // = 546 #define M_MAX_MOMENTUM_TURN (DEG_1 * 150) // = 27300 #define M_MIN_BOUNCE 50 #define M_MAX_KICK -80 #define LF_SKIDOO_EXIT_END 59 #define LF_SKIDOO_LET_GO_END 17 typedef enum { M_GET_ON_NONE = 0, SKIDOO_GET_ON_LEFT = 1, M_GET_ON_RIGHT = 2, } M_GET_ON_SIDE; typedef enum { // clang-format off LARA_STATE_SKIDOO_SIT = 0, LARA_STATE_SKIDOO_GET_ON = 1, LARA_STATE_SKIDOO_LEFT = 2, LARA_STATE_SKIDOO_RIGHT = 3, LARA_STATE_SKIDOO_FALL = 4, LARA_STATE_SKIDOO_HIT = 5, LARA_STATE_SKIDOO_GET_ON_L = 6, LARA_STATE_SKIDOO_GET_OFF_L = 7, LARA_STATE_SKIDOO_STILL = 8, LARA_STATE_SKIDOO_GET_OFF_R = 9, LARA_STATE_SKIDOO_LET_GO = 10, LARA_STATE_SKIDOO_DEATH = 11, LARA_STATE_SKIDOO_FALLOFF = 12, // clang-format on } LARA_SKIDOO_STATE; typedef enum { // clang-format off LA_SKIDOO_GET_ON_L = 1, LA_SKIDOO_FALL = 8, LA_SKIDOO_HIT_LEFT = 11, LA_SKIDOO_HIT_RIGHT = 12, LA_SKIDOO_HIT_FRONT = 13, LA_SKIDOO_HIT_BACK = 14, LA_SKIDOO_DEAD = 15, LA_SKIDOO_GET_ON_R = 18, // clang-format on } LARA_ANIM_SKIDOO; BITE g_Skidoo_LeftGun = { .pos = { .x = 219, .y = -71, .z = M_FRONT }, .mesh_num = 0, }; BITE g_Skidoo_RightGun = { .pos = { .x = -235, .y = -71, .z = M_FRONT }, .mesh_num = 0, }; static int32_t M_DoDynamics( const int32_t height, const int32_t fall_speed, int32_t *const out_y) { if (height > *out_y) { *out_y += fall_speed; if (*out_y > height - M_MIN_BOUNCE) { *out_y = height; return 0; } return fall_speed + GRAVITY; } int32_t kick = 4 * (height - *out_y); CLAMPL(kick, M_MAX_KICK); CLAMPG(*out_y, height); return fall_speed + ((kick - fall_speed) >> 3); } static bool M_IsArmed(const SKIDOO_INFO *const skidoo_data) { return skidoo_data->track_mesh & SKIDOO_GUN_MESH; } static bool M_CheckBaddieCollision(ITEM *const item, ITEM *const skidoo) { if (!item->collidable || item->status == IS_INVISIBLE || item == Lara_GetItem() || item == skidoo) { return false; } const OBJECT *const obj = Object_Get(item->object_id); const bool is_availanche = item->object_id == O_ROLLING_BALL_2; if (obj->collision_func == nullptr || (!obj->intelligent && !is_availanche)) { return false; } if (!Item_IsNearby(item, skidoo, M_TARGET_DIST)) { return false; } if (!Item_TestBoundsCollide(item, skidoo, M_RADIUS)) { return false; } if (item->object_id == O_SKIDOO_ARMED) { SkidooArmed_Push(item, skidoo, M_RADIUS); } else if (is_availanche) { if (item->current_anim_state == TRAP_ACTIVATE) { Lara_TakeDamage(100, true); } } else if ( obj->intelligent && item->status == IS_ACTIVE && (Item_IsTargetable(item) || item->hit_points == 0)) { if (Item_ShouldSpawnBlood(item)) { Spawn_BloodBath( item->pos.x, skidoo->pos.y - STEP_L, item->pos.z, skidoo->speed, skidoo->rot.y, item->room_num, 3); } Gun_HitTarget(item, nullptr, nullptr, item->hit_points); } return true; } void Skidoo_Initialise(const int16_t item_num) { ITEM *const item = Item_Get(item_num); if (item->priv == nullptr) { item->priv = GameBuf_Alloc(sizeof(SKIDOO_INFO), GBUF_ITEM_DATA); } SKIDOO_INFO *const skidoo_data = item->priv; skidoo_data->skidoo_turn = 0; skidoo_data->right_fallspeed = 0; skidoo_data->left_fallspeed = 0; skidoo_data->extra_rotation = 0; skidoo_data->momentum_angle = item->rot.y; skidoo_data->track_mesh = 0; skidoo_data->pitch = 0; } int32_t Skidoo_CheckGetOn(const int16_t item_num, COLL_INFO *const coll) { const ITEM *const lara_item = Lara_GetItem(); const LARA_INFO *const lara = Lara_GetLaraInfo(); if (!g_Input.action || lara->gun_status != LGS_ARMLESS || lara_item->gravity || (lara->water_status != LWS_ABOVE_WATER && lara->water_status != LWS_WADE)) { return M_GET_ON_NONE; } ITEM *const item = Item_Get(item_num); const int16_t angle = item->rot.y - lara_item->rot.y; M_GET_ON_SIDE get_on = M_GET_ON_NONE; if (angle > DEG_45 && angle < DEG_135) { get_on = SKIDOO_GET_ON_LEFT; } else if (angle > -DEG_135 && angle < -DEG_45) { get_on = M_GET_ON_RIGHT; } if (!Item_TestBoundsCollide(item, lara_item, coll->radius)) { return M_GET_ON_NONE; } if (!Collide_TestCollision(item, lara_item)) { return M_GET_ON_NONE; } int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); const int32_t height = Room_GetHeight(sector, item->pos); if (height < -32000) { return M_GET_ON_NONE; } return get_on; } void Skidoo_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { if (lara_item->hit_points < 0 || Lara_Vehicle_IsMounted()) { return; } const M_GET_ON_SIDE get_on = Skidoo_CheckGetOn(item_num, coll); if (get_on == M_GET_ON_NONE) { Object_Collision(item_num, lara_item, coll); return; } LARA_INFO *const lara = Lara_GetLaraInfo(); Lara_Vehicle_SetIndex(item_num); if (lara->gun_type == LGT_FLARE) { Lara_Flare_Dispose(false); lara->gun_type = LGT_UNARMED; lara->request_gun_type = LGT_UNARMED; } const LARA_ANIM_SKIDOO anim_idx = get_on == SKIDOO_GET_ON_LEFT ? LA_SKIDOO_GET_ON_L : LA_SKIDOO_GET_ON_R; Item_SwitchToObjAnim(lara_item, anim_idx, 0, O_LARA_SKIDOO); lara_item->current_anim_state = LARA_STATE_SKIDOO_GET_ON; lara->gun_status = LGS_ARMLESS; lara->hit_direction = DIR_UNKNOWN; ITEM *const item = Item_Get(item_num); lara_item->pos.x = item->pos.x; lara_item->pos.y = item->pos.y; lara_item->pos.z = item->pos.z; lara_item->rot.y = item->rot.y; item->hit_points = 1; } void Skidoo_BaddieCollision(ITEM *const skidoo) { int16_t roomies[12]; const int32_t roomies_count = Room_GetAdjoiningRooms(skidoo->room_num, roomies, 12); for (int32_t i = 0; i < roomies_count; i++) { const ROOM *const room = Room_Get(roomies[i]); int16_t item_num = room->item_num; while (item_num != NO_ITEM) { ITEM *item = Item_Get(item_num); M_CheckBaddieCollision(item, skidoo); item_num = item->next_item; } } } int32_t Skidoo_TestHeight( const ITEM *const item, const int32_t z_off, const int32_t x_off, XYZ_32 *const out_pos) { const int32_t sx = Math_Sin(item->rot.x); const int32_t sz = Math_Sin(item->rot.z); const int32_t cy = Math_Cos(item->rot.y); const int32_t sy = Math_Sin(item->rot.y); out_pos->x = item->pos.x + ((x_off * cy + z_off * sy) >> W2V_SHIFT); out_pos->y = item->pos.y + ((x_off * sz - z_off * sx) >> W2V_SHIFT); out_pos->z = item->pos.z + ((z_off * cy - x_off * sy) >> W2V_SHIFT); int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(*out_pos, &room_num); return Room_GetHeight(sector, *out_pos); } void Skidoo_DoSnowEffect(const ITEM *const skidoo) { if (!Object_Get(O_SNOW_SPRITE)->loaded) { return; } const int16_t effect_num = Effect_Create(skidoo->room_num); if (effect_num == NO_EFFECT) { return; } const int32_t sx = Math_Sin(skidoo->rot.x); const int32_t sy = Math_Sin(skidoo->rot.y); const int32_t cy = Math_Cos(skidoo->rot.y); const int32_t x = (M_SIDE * (Random_GetDraw() - 0x4000)) >> 14; EFFECT *const effect = Effect_Get(effect_num); effect->pos.x = skidoo->pos.x - ((sy * M_SNOW + cy * x) >> W2V_SHIFT); effect->pos.y = skidoo->pos.y + ((sx * M_SNOW) >> W2V_SHIFT); effect->pos.z = skidoo->pos.z - ((cy * M_SNOW - sy * x) >> W2V_SHIFT); effect->room_num = skidoo->room_num; effect->object_id = O_SNOW_SPRITE; effect->frame_num = 0; effect->speed = 0; if (skidoo->speed < 64) { effect->fall_speed = (Random_GetDraw() * (ABS(skidoo->speed) - 64)) >> 15; } else { effect->fall_speed = 0; } g_MatrixPtr->_23 = 0; g_WMatrixPtr->_23 = 0; Output_CalculateLight(effect->pos, effect->room_num); effect->shade = Output_GetLightAdder() - 512; CLAMPL(effect->shade, 0); } int32_t Skidoo_Dynamics(ITEM *const skidoo) { SKIDOO_INFO *const skidoo_data = skidoo->priv; XYZ_32 fl_old; XYZ_32 bl_old; XYZ_32 br_old; XYZ_32 fr_old; int32_t hfl_old = Skidoo_TestHeight(skidoo, M_FRONT, -M_SIDE, &fl_old); int32_t hfr_old = Skidoo_TestHeight(skidoo, M_FRONT, M_SIDE, &fr_old); int32_t hbl_old = Skidoo_TestHeight(skidoo, -M_FRONT, -M_SIDE, &bl_old); int32_t hbr_old = Skidoo_TestHeight(skidoo, -M_FRONT, M_SIDE, &br_old); XYZ_32 old = { .z = skidoo->pos.z, .x = skidoo->pos.x, .y = skidoo->pos.y, }; CLAMPG(bl_old.y, hbl_old); CLAMPG(br_old.y, hbr_old); CLAMPG(fl_old.y, hfl_old); CLAMPG(fr_old.y, hfr_old); if (skidoo->pos.y <= skidoo->floor - STEP_L) { skidoo->rot.y += skidoo_data->extra_rotation + skidoo_data->skidoo_turn; } else { if (skidoo_data->skidoo_turn < -M_UNDO_TURN) { skidoo_data->skidoo_turn += M_UNDO_TURN; } else if (skidoo_data->skidoo_turn > M_UNDO_TURN) { skidoo_data->skidoo_turn -= M_UNDO_TURN; } else { skidoo_data->skidoo_turn = 0; } skidoo->rot.y += skidoo_data->skidoo_turn + skidoo_data->extra_rotation; int16_t rot = skidoo->rot.y - skidoo_data->momentum_angle; if (rot < -M_MOMENTUM_TURN) { if (rot < -M_MAX_MOMENTUM_TURN) { rot = -M_MAX_MOMENTUM_TURN; skidoo_data->momentum_angle = skidoo->rot.y - rot; } else { skidoo_data->momentum_angle -= M_MOMENTUM_TURN; } } else if (rot > M_MOMENTUM_TURN) { if (rot > M_MAX_MOMENTUM_TURN) { rot = M_MAX_MOMENTUM_TURN; skidoo_data->momentum_angle = skidoo->rot.y - rot; } else { skidoo_data->momentum_angle += M_MOMENTUM_TURN; } } else { skidoo_data->momentum_angle = skidoo->rot.y; } } skidoo->pos.z += (skidoo->speed * Math_Cos(skidoo_data->momentum_angle)) >> W2V_SHIFT; skidoo->pos.x += (skidoo->speed * Math_Sin(skidoo_data->momentum_angle)) >> W2V_SHIFT; int32_t slip; slip = (M_SLIP * Math_Sin(skidoo->rot.x)) >> W2V_SHIFT; if (ABS(slip) > M_SLIP / 2) { skidoo->pos.z -= (slip * Math_Cos(skidoo->rot.y)) >> W2V_SHIFT; skidoo->pos.x -= (slip * Math_Sin(skidoo->rot.y)) >> W2V_SHIFT; } slip = (M_SLIP_SIDE * Math_Sin(skidoo->rot.z)) >> W2V_SHIFT; if (ABS(slip) > M_SLIP_SIDE / 2) { skidoo->pos.z -= (slip * Math_Sin(skidoo->rot.y)) >> W2V_SHIFT; skidoo->pos.x += (slip * Math_Cos(skidoo->rot.y)) >> W2V_SHIFT; } XYZ_32 moved = { .x = skidoo->pos.x, .z = skidoo->pos.z, }; if (!(skidoo->flags & IF_ONE_SHOT)) { Skidoo_BaddieCollision(skidoo); } int32_t rot = 0; XYZ_32 br; XYZ_32 fl; XYZ_32 bl; XYZ_32 fr; const int32_t hbl = Skidoo_TestHeight(skidoo, -M_FRONT, -M_SIDE, &bl); if (hbl < bl_old.y - STEP_L) { rot = Vehicle_DoShift(skidoo, &bl, &bl_old); } const int32_t hbr = Skidoo_TestHeight(skidoo, -M_FRONT, M_SIDE, &br); if (hbr < br_old.y - STEP_L) { rot += Vehicle_DoShift(skidoo, &br, &br_old); } const int32_t hfl = Skidoo_TestHeight(skidoo, M_FRONT, -M_SIDE, &fl); if (hfl < fl_old.y - STEP_L) { rot += Vehicle_DoShift(skidoo, &fl, &fl_old); } const int32_t hfr = Skidoo_TestHeight(skidoo, M_FRONT, M_SIDE, &fr); if (hfr < fr_old.y - STEP_L) { rot += Vehicle_DoShift(skidoo, &fr, &fr_old); } int16_t room_num = skidoo->room_num; const SECTOR *const sector = Room_GetSector(skidoo->pos, &room_num); const int32_t height = Room_GetHeight(sector, skidoo->pos); if (height < skidoo->pos.y - STEP_L) { Vehicle_DoShift(skidoo, &skidoo->pos, &old); } skidoo_data->extra_rotation = rot; int32_t collide = Vehicle_GetCollisionAnim(skidoo, &moved); if (collide != 0) { const int32_t c = Math_Cos(skidoo_data->momentum_angle); const int32_t s = Math_Sin(skidoo_data->momentum_angle); const int32_t dx = skidoo->pos.x - old.x; const int32_t dz = skidoo->pos.z - old.z; const int32_t new_speed = (s * dx + c * dz) >> W2V_SHIFT; if (skidoo == Lara_Vehicle_GetItem() && skidoo->speed > SKIDOO_MAX_SPEED + M_ACCELERATION && new_speed < skidoo->speed - M_ACCELERATION) { Lara_TakeDamage((skidoo->speed - new_speed) / 2, true); } if (skidoo->speed > 0 && new_speed < skidoo->speed) { skidoo->speed = new_speed < 0 ? 0 : new_speed; } else if (skidoo->speed < 0 && new_speed > skidoo->speed) { skidoo->speed = new_speed > 0 ? 0 : new_speed; } if (skidoo->speed < M_MAX_BACK) { skidoo->speed = M_MAX_BACK; } } return collide; } int32_t Skidoo_UserControl( ITEM *const skidoo, const int32_t height, int32_t *const out_pitch) { SKIDOO_INFO *const skidoo_data = skidoo->priv; bool drive = false; if (skidoo->pos.y >= height - STEP_L) { *out_pitch = skidoo->speed + (height - skidoo->pos.y); if (skidoo->speed == 0 && g_Input.look) { Lara_Look_UpDown(); } if ((g_Input.left && !g_Input.back) || (g_Input.right && g_Input.back)) { skidoo_data->skidoo_turn -= M_TURN; CLAMPL(skidoo_data->skidoo_turn, -SKIDOO_MAX_TURN); } if ((g_Input.right && !g_Input.back) || (g_Input.left && g_Input.back)) { skidoo_data->skidoo_turn += M_TURN; CLAMPG(skidoo_data->skidoo_turn, SKIDOO_MAX_TURN); } if (g_Input.back) { if (skidoo->speed > 0) { skidoo->speed -= M_BRAKE; } else { if (skidoo->speed > M_MAX_BACK) { skidoo->speed += M_REVERSE; } drive = true; } } else if (g_Input.forward) { int32_t max_speed; if (g_Input.action && !M_IsArmed(skidoo_data)) { max_speed = SKIDOO_FAST_SPEED; } else if (g_Input.slow) { max_speed = SKIDOO_SLOW_SPEED; } else { max_speed = SKIDOO_MAX_SPEED; } if (skidoo->speed < max_speed) { skidoo->speed += M_ACCELERATION * skidoo->speed / (2 * max_speed) + M_ACCELERATION / 2; } else if (skidoo->speed > max_speed + M_SLOWDOWN) { skidoo->speed -= M_SLOWDOWN; } drive = true; } else if ( skidoo->speed >= 0 && skidoo->speed < SKIDOO_MIN_SPEED && (g_Input.left || g_Input.right)) { skidoo->speed = SKIDOO_MIN_SPEED; drive = true; } else if (skidoo->speed > M_SLOWDOWN) { skidoo->speed -= M_SLOWDOWN; if ((Random_GetDraw() & 0x7F) < skidoo->speed) { drive = true; } } else { skidoo->speed = 0; } } else if (g_Input.forward || g_Input.back) { drive = true; *out_pitch = skidoo_data->pitch + 50; } return drive; } int32_t Skidoo_CheckGetOffOK(int32_t direction) { ITEM *const skidoo = Lara_Vehicle_GetItem(); int16_t rot; if (direction == LARA_STATE_SKIDOO_GET_OFF_L) { rot = skidoo->rot.y + DEG_90; } else { rot = skidoo->rot.y - DEG_90; } const XYZ_32 pos = XYZ_32_OffsetYaw(skidoo->pos, rot, -M_GET_OFF_DIST); int16_t room_num = skidoo->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); const int32_t height = Room_GetHeight(sector, pos); const HEIGHT_TYPE height_type = Room_GetHeightType(); if (height_type == HT_BIG_SLOPE || height_type == HT_DIAGONAL || height == NO_HEIGHT) { return false; } if (ABS(height - skidoo->pos.y) > WALL_L / 2) { return false; } const int32_t ceiling = Room_GetCeiling(sector, pos); if (ceiling - skidoo->pos.y > -LARA_HEIGHT) { return false; } if (height - ceiling < LARA_HEIGHT) { return false; } return true; } void Skidoo_Animation( ITEM *const skidoo, const int32_t collide, const int32_t dead) { const SKIDOO_INFO *const skidoo_data = skidoo->priv; ITEM *const lara_item = Lara_GetItem(); if (skidoo->pos.y != skidoo->floor && skidoo->fall_speed > 0 && lara_item->current_anim_state != LARA_STATE_SKIDOO_FALL && !dead) { Item_SwitchToObjAnim(lara_item, LA_SKIDOO_FALL, 0, O_LARA_SKIDOO); lara_item->goal_anim_state = LARA_STATE_SKIDOO_FALL; lara_item->current_anim_state = LARA_STATE_SKIDOO_FALL; return; } if (collide != 0 && !dead && lara_item->current_anim_state != LARA_STATE_SKIDOO_FALL) { if (lara_item->current_anim_state != LARA_STATE_SKIDOO_HIT) { if (collide == LA_SKIDOO_HIT_FRONT) { Sound_Effect(SFX_CLATTER_2, &skidoo->pos, SPM_NORMAL); } else { Sound_Effect(SFX_CLATTER_1, &skidoo->pos, SPM_NORMAL); } Item_SwitchToObjAnim(lara_item, collide, 0, O_LARA_SKIDOO); lara_item->goal_anim_state = LARA_STATE_SKIDOO_HIT; lara_item->current_anim_state = LARA_STATE_SKIDOO_HIT; } return; } switch (lara_item->current_anim_state) { case LARA_STATE_SKIDOO_SIT: if (skidoo->speed == 0) { lara_item->goal_anim_state = LARA_STATE_SKIDOO_STILL; } if (dead) { lara_item->goal_anim_state = LARA_STATE_SKIDOO_FALLOFF; } else if (g_Input.left) { lara_item->goal_anim_state = LARA_STATE_SKIDOO_LEFT; } else if (g_Input.right) { lara_item->goal_anim_state = LARA_STATE_SKIDOO_RIGHT; } break; case LARA_STATE_SKIDOO_LEFT: if (!g_Input.left) { lara_item->goal_anim_state = LARA_STATE_SKIDOO_SIT; } break; case LARA_STATE_SKIDOO_RIGHT: if (!g_Input.right) { lara_item->goal_anim_state = LARA_STATE_SKIDOO_SIT; } break; case LARA_STATE_SKIDOO_FALL: if (skidoo->fall_speed <= 0 || skidoo_data->left_fallspeed <= 0 || skidoo_data->right_fallspeed <= 0) { Sound_Effect(SFX_CLATTER_3, &skidoo->pos, SPM_NORMAL); lara_item->goal_anim_state = LARA_STATE_SKIDOO_SIT; } else if (skidoo->fall_speed > DAMAGE_START + DAMAGE_LENGTH) { lara_item->goal_anim_state = LARA_STATE_SKIDOO_LET_GO; } break; case LARA_STATE_SKIDOO_STILL: { const int32_t music_track = Music_ToGameID( M_IsArmed(skidoo_data) ? MX_BATTLE_THEME : MX_SKIDOO_THEME); const uint16_t music_flags = Music_GetTrackFlags(music_track); if (!(music_flags & IF_ONE_SHOT)) { Music_Play_Direct(music_track, MPM_ONCE); Music_SetTrackFlags(music_track, music_flags | IF_ONE_SHOT); } if (dead) { lara_item->goal_anim_state = LARA_STATE_SKIDOO_DEATH; return; } lara_item->goal_anim_state = LARA_STATE_SKIDOO_STILL; if (g_Input.jump) { if (g_Input.right && Skidoo_CheckGetOffOK(LARA_STATE_SKIDOO_GET_OFF_R)) { lara_item->goal_anim_state = LARA_STATE_SKIDOO_GET_OFF_R; skidoo->speed = 0; } else if ( g_Input.left && Skidoo_CheckGetOffOK(LARA_STATE_SKIDOO_GET_OFF_L)) { lara_item->goal_anim_state = LARA_STATE_SKIDOO_GET_OFF_L; skidoo->speed = 0; } } else if (g_Input.left) { lara_item->goal_anim_state = LARA_STATE_SKIDOO_LEFT; } else if (g_Input.right) { lara_item->goal_anim_state = LARA_STATE_SKIDOO_RIGHT; } else if (g_Input.back || g_Input.forward) { lara_item->goal_anim_state = LARA_STATE_SKIDOO_SIT; } break; } default: break; } } void Skidoo_Explode(const ITEM *const skidoo) { const int16_t effect_num = Effect_Create(skidoo->room_num); if (effect_num != NO_EFFECT) { EFFECT *const effect = Effect_Get(effect_num); effect->pos.x = skidoo->pos.x; effect->pos.y = skidoo->pos.y; effect->pos.z = skidoo->pos.z; effect->speed = 0; effect->frame_num = 0; effect->counter = 0; effect->object_id = O_EXPLOSION_1; } Item_Explode(Item_GetIndex(skidoo), ~(SKIDOO_GUN_MESH - 1), 0); Sound_Effect(SFX_EXPLOSION_1, nullptr, SPM_NORMAL); Lara_Vehicle_SetIndex(NO_ITEM); } bool Skidoo_CheckGetOff(void) { ITEM *const skidoo = Lara_Vehicle_GetItem(); ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara = Lara_GetLaraInfo(); if ((lara_item->current_anim_state == LARA_STATE_SKIDOO_GET_OFF_R || lara_item->current_anim_state == LARA_STATE_SKIDOO_GET_OFF_L) && Item_TestFrameEqual(lara_item, LF_SKIDOO_EXIT_END)) { if (lara_item->current_anim_state == LARA_STATE_SKIDOO_GET_OFF_L) { lara_item->rot.y += DEG_90; } else { lara_item->rot.y -= DEG_90; } Item_SwitchToAnim(lara_item, LA(LA_STAND_STILL), 0); lara_item->goal_anim_state = LS(LS_STOP); lara_item->current_anim_state = LS(LS_STOP); lara_item->pos.x -= (M_GET_OFF_DIST * Math_Sin(lara_item->rot.y)) >> W2V_SHIFT; lara_item->pos.z -= (M_GET_OFF_DIST * Math_Cos(lara_item->rot.y)) >> W2V_SHIFT; lara_item->rot.x = 0; lara_item->rot.z = 0; Lara_Vehicle_SetIndex(NO_ITEM); lara->gun_status = LGS_ARMLESS; return true; } if (lara_item->current_anim_state == LARA_STATE_SKIDOO_LET_GO && (skidoo->pos.y == skidoo->floor || Item_TestFrameEqual(lara_item, LF_SKIDOO_LET_GO_END))) { Item_SwitchToAnim(lara_item, LA(LA_FREEFALL), 0); lara_item->current_anim_state = LARA_STATE_SKIDOO_GET_OFF_R; if (skidoo->pos.y == skidoo->floor) { lara_item->goal_anim_state = LARA_STATE_SKIDOO_STILL; lara_item->fall_speed = DAMAGE_START + DAMAGE_LENGTH; lara_item->speed = 0; Skidoo_Explode(skidoo); } else { lara_item->goal_anim_state = LARA_STATE_SKIDOO_GET_OFF_R; lara_item->pos.y -= 200; lara_item->fall_speed = skidoo->fall_speed; lara_item->speed = skidoo->speed; Sound_Effect(SFX_LARA_FALL, &lara_item->pos, SPM_NORMAL); } lara_item->rot.x = 0; lara_item->rot.z = 0; lara_item->gravity = true; lara->gun_status = LGS_ARMLESS; lara->move_angle = skidoo->rot.y; skidoo->flags |= IF_ONE_SHOT; skidoo->collidable = 0; return false; } return true; } void Skidoo_Guns(void) { WEAPON_INFO *const weapon = &g_Weapons[LGT_SKIDOO]; const ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara = Lara_GetLaraInfo(); Gun_GetNewTarget(weapon); Gun_AimWeapon(weapon, &lara->right_arm); if (!g_Input.action) { return; } int16_t angles[2]; angles[0] = lara->right_arm.rot.y + lara_item->rot.y; angles[1] = lara->right_arm.rot.x; if (!Gun_FireWeapon(LGT_SKIDOO, lara->target, lara_item, angles)) { return; } lara->right_arm.flash_gun = weapon->flash_time; Sound_Effect(weapon->sample_num, &lara_item->pos, SPM_NORMAL); Gun_AddDynamicLight(); ITEM *const skidoo = Lara_Vehicle_GetItem(); Creature_Effect(skidoo, &g_Skidoo_LeftGun, Spawn_GunShot); Creature_Effect(skidoo, &g_Skidoo_RightGun, Spawn_GunShot); } bool Skidoo_Control(void) { ITEM *const lara_item = Lara_GetItem(); ITEM *const skidoo = Lara_Vehicle_GetItem(); SKIDOO_INFO *const skidoo_data = skidoo->priv; int32_t collide = Skidoo_Dynamics(skidoo); XYZ_32 fl; XYZ_32 fr; const int32_t hfl = Skidoo_TestHeight(skidoo, M_FRONT, -M_SIDE, &fl); const int32_t hfr = Skidoo_TestHeight(skidoo, M_FRONT, M_SIDE, &fr); int16_t room_num = skidoo->room_num; const SECTOR *const sector = Room_GetSector(skidoo->pos, &room_num); int32_t height = Room_GetHeight(sector, skidoo->pos); bool dead = false; if (lara_item->hit_points <= 0) { dead = true; g_Input.back = 0; g_Input.forward = 0; g_Input.left = 0; g_Input.right = 0; } else if (lara_item->current_anim_state == LARA_STATE_SKIDOO_LET_GO) { dead = true; collide = 0; } int32_t drive; int32_t pitch; if (skidoo->flags & IF_ONE_SHOT) { drive = 0; collide = 0; } else { switch (lara_item->current_anim_state) { case LARA_STATE_SKIDOO_GET_ON: case LARA_STATE_SKIDOO_GET_OFF_L: case LARA_STATE_SKIDOO_GET_OFF_R: case LARA_STATE_SKIDOO_LET_GO: drive = -1; collide = 0; break; default: drive = Skidoo_UserControl(skidoo, height, &pitch); break; } } const int32_t old_track_mesh = skidoo_data->track_mesh; if (drive > 0) { skidoo_data->track_mesh = (skidoo_data->track_mesh & 3) == 1 ? 2 : 1; skidoo_data->pitch += (pitch - skidoo_data->pitch) >> 2; const int32_t pitch_delta = (SKIDOO_MAX_SPEED - skidoo_data->pitch) * 100; Sound_Effect( SFX_SKIDOO_MOVING, &skidoo->pos, SPM_PITCH | ((SOUND_DEFAULT_PITCH - pitch_delta) << 8)); } else { skidoo_data->track_mesh = 0; if (!drive) { Sound_Effect(SFX_SKIDOO_IDLE, &skidoo->pos, SPM_NORMAL); } skidoo_data->pitch = 0; } skidoo_data->track_mesh |= old_track_mesh & SKIDOO_GUN_MESH; skidoo->floor = height; skidoo_data->left_fallspeed = M_DoDynamics(hfl, skidoo_data->left_fallspeed, &fl.y); skidoo_data->right_fallspeed = M_DoDynamics(hfr, skidoo_data->right_fallspeed, &fr.y); skidoo->fall_speed = M_DoDynamics(height, skidoo->fall_speed, &skidoo->pos.y); height = (fr.y + fl.y) / 2; const int16_t x_rot = Math_Atan(M_FRONT, skidoo->pos.y - height); const int16_t z_rot = Math_Atan(M_SIDE, height - fl.y); skidoo->rot.x += (x_rot - skidoo->rot.x) >> 1; skidoo->rot.z += (z_rot - skidoo->rot.z) >> 1; if (skidoo->flags & IF_ONE_SHOT) { Room_TestTriggers(lara_item); Item_UpdateRoom(Item_GetIndex(skidoo), room_num); if (skidoo->pos.y == skidoo->floor) { Skidoo_Explode(skidoo); } return false; } Skidoo_Animation(skidoo, collide, dead); Item_UpdateRoom(Item_GetIndex(skidoo), room_num); Item_UpdateRoom(Item_GetIndex(lara_item), room_num); if (lara_item->current_anim_state == LARA_STATE_SKIDOO_FALLOFF) { lara_item->rot.x = 0; lara_item->rot.z = 0; } else { lara_item->pos.x = skidoo->pos.x; lara_item->pos.y = skidoo->pos.y; lara_item->pos.z = skidoo->pos.z; lara_item->rot.y = skidoo->rot.y; if (drive >= 0) { lara_item->rot.x = skidoo->rot.x; lara_item->rot.z = skidoo->rot.z; } else { lara_item->rot.x = 0; lara_item->rot.z = 0; } } Room_TestTriggers(lara_item); Item_Animate(lara_item); if (!dead && drive >= 0 && M_IsArmed(skidoo_data)) { Skidoo_Guns(); } if (dead) { Item_SwitchToObjAnim(skidoo, LA_SKIDOO_DEAD, 0, O_SKIDOO_FAST); } else { const int16_t lara_anim_num = Item_GetRelativeObjAnim(lara_item, O_LARA_SKIDOO); const int16_t lara_frame_num = Item_GetRelativeFrame(lara_item); Item_SwitchToObjAnim( skidoo, lara_anim_num, lara_frame_num, O_SKIDOO_FAST); } if (skidoo->speed != 0 && skidoo->floor == skidoo->pos.y) { Skidoo_DoSnowEffect(skidoo); if (skidoo->speed < SKIDOO_SLOW_SPEED) { Skidoo_DoSnowEffect(skidoo); } } return Skidoo_CheckGetOff(); } bool Skidoo_Draw(const ITEM *const item) { int32_t track_mesh_status = 0; const SKIDOO_INFO *const skidoo_data = item->priv; if (skidoo_data != nullptr) { track_mesh_status = skidoo_data->track_mesh; } const OBJECT *obj = Object_Get(item->object_id); if ((track_mesh_status & SKIDOO_GUN_MESH) != 0) { obj = Object_Get(O_SKIDOO_ARMED); } const OBJECT *const track_obj = Object_Get(O_SKIDOO_TRACK); const OBJECT_MESH *track_mesh = nullptr; if ((track_mesh_status & 3) == 1) { track_mesh = Object_GetMesh(track_obj->mesh_idx + 1); } else if ((track_mesh_status & 3) == 2) { track_mesh = Object_GetMesh(track_obj->mesh_idx + 7); } // TODO: merge common code parts down below with Object_DrawAnimatingItem. ANIM_FRAME *frames[2]; int32_t rate; const int32_t frac = Item_GetFrames(item, frames, &rate); Matrix_Push(); Matrix_TranslateAbs32(item->interp.result.pos); Matrix_Rot16(item->interp.result.rot); const CLIP clip = Output_CheckBoundsClip(&frames[0]->bounds); if (clip == CLIP_NOT_VISIBLE) { Matrix_Pop(); return false; } Output_CalculateObjectLighting(item, &frames[0]->bounds); if (frac) { Matrix_InitInterpolate(frac, rate); Matrix_TranslateRel16_ID(frames[0]->offset, frames[1]->offset); Matrix_Rot16_ID(frames[0]->mesh_rots[0], frames[1]->mesh_rots[0]); Object_DrawMesh(obj->mesh_idx, clip, true); for (int32_t mesh_idx = 1; mesh_idx < obj->mesh_count; mesh_idx++) { const ANIM_BONE *const bone = Object_GetBone(obj, mesh_idx - 1); if (bone->matrix_pop) { Matrix_Pop_I(); } if (bone->matrix_push) { Matrix_Push_I(); } Matrix_TranslateRel32_I(bone->pos); Matrix_Rot16_ID( frames[0]->mesh_rots[mesh_idx], frames[1]->mesh_rots[mesh_idx]); if (mesh_idx == 1 && track_mesh != nullptr) { Output_DrawObjectMesh_I(track_mesh, clip); } else { Object_DrawMesh(obj->mesh_idx + mesh_idx, clip, true); } } } else { Matrix_TranslateRel16(frames[0]->offset); Matrix_Rot16(frames[0]->mesh_rots[0]); Object_DrawMesh(obj->mesh_idx, clip, false); for (int32_t mesh_idx = 1; mesh_idx < obj->mesh_count; mesh_idx++) { const ANIM_BONE *const bone = Object_GetBone(obj, mesh_idx - 1); if (bone->matrix_pop) { Matrix_Pop(); } if (bone->matrix_push) { Matrix_Push(); } Matrix_TranslateRel32(bone->pos); Matrix_Rot16(frames[0]->mesh_rots[mesh_idx]); if (mesh_idx == 1 && track_mesh != nullptr) { Output_DrawObjectMesh(track_mesh, clip); } else { Object_DrawMesh(obj->mesh_idx + mesh_idx, clip, false); } } } Matrix_Pop(); return true; } ================================================ FILE: src/trx/game/objects/vehicles/skidoo_common.h ================================================ #pragma once #include #include #include #include #define SKIDOO_MIN_SPEED 15 #define SKIDOO_MAX_SPEED 100 #define SKIDOO_SLOW_SPEED 50 #define SKIDOO_FAST_SPEED 150 #define SKIDOO_MAX_TURN (DEG_1 * 6) // = 1092 #define SKIDOO_GUN_MESH 4 typedef struct { int16_t track_mesh; int32_t skidoo_turn; int32_t left_fallspeed; int32_t right_fallspeed; int16_t momentum_angle; int16_t extra_rotation; int32_t pitch; } SKIDOO_INFO; extern BITE g_Skidoo_LeftGun; extern BITE g_Skidoo_RightGun; void Skidoo_Initialise(int16_t item_num); int32_t Skidoo_CheckGetOn(int16_t item_num, COLL_INFO *coll); void Skidoo_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); void Skidoo_BaddieCollision(ITEM *skidoo); int32_t Skidoo_TestHeight( const ITEM *item, int32_t z_off, int32_t x_off, XYZ_32 *out_pos); void Skidoo_DoSnowEffect(const ITEM *skidoo); int32_t Skidoo_Dynamics(ITEM *skidoo); int32_t Skidoo_UserControl(ITEM *skidoo, int32_t height, int32_t *out_pitch); int32_t Skidoo_CheckGetOffOK(int32_t direction); void Skidoo_Animation(ITEM *skidoo, int32_t collide, int32_t dead); void Skidoo_Explode(const ITEM *skidoo); bool Skidoo_CheckGetOff(void); void Skidoo_Guns(void); bool Skidoo_Control(void); bool Skidoo_Draw(const ITEM *item); ================================================ FILE: src/trx/game/objects/vehicles/skidoo_fast.c ================================================ #include #include #include #include static void M_PrivLoad(ITEM *const item, JSON_READ_IO *const io) { SKIDOO_INFO *const p = item->priv; JSON_SHOULD(JSON_READ(io, "track_mesh", &p->track_mesh)); JSON_SHOULD(JSON_READ(io, "skidoo_turn", &p->skidoo_turn)); JSON_SHOULD(JSON_READ(io, "left_fallspeed", &p->left_fallspeed)); JSON_SHOULD(JSON_READ(io, "right_fallspeed", &p->right_fallspeed)); JSON_SHOULD(JSON_READ(io, "momentum_angle", &p->momentum_angle)); JSON_SHOULD(JSON_READ(io, "extra_rotation", &p->extra_rotation)); JSON_SHOULD(JSON_READ(io, "pitch", &p->pitch)); } static void M_PrivSave(const ITEM *const item, JSON_WRITE_IO *const io) { const SKIDOO_INFO *const p = item->priv; JSONW_WRITE(io, "track_mesh", p->track_mesh); JSONW_WRITE(io, "skidoo_turn", p->skidoo_turn); JSONW_WRITE(io, "left_fallspeed", p->left_fallspeed); JSONW_WRITE(io, "right_fallspeed", p->right_fallspeed); JSONW_WRITE(io, "momentum_angle", p->momentum_angle); JSONW_WRITE(io, "extra_rotation", p->extra_rotation); JSONW_WRITE(io, "pitch", p->pitch); } static void M_Setup(OBJECT *const obj) { obj->initialise_func = Skidoo_Initialise; obj->draw_func = Skidoo_Draw; obj->collision_func = Skidoo_Collision; obj->priv_size = sizeof(SKIDOO_INFO); obj->priv_load_func = M_PrivLoad; obj->priv_save_func = M_PrivSave; obj->save_position = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_SKIDOO_FAST, M_Setup) ================================================ FILE: src/trx/game/objects/vehicles/upv.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // clang-format off #define M_ACCELERATION 0x40000 #define M_FRICTION 0x18000 #define M_MAX_SPEED 0x400000 #define M_ROT_ACCELERATION 0x400000 #define M_ROT_SLOWACCEL 0x200000 #define M_ROT_FRICTION 0x100000 #define M_MAX_ROTATION 0x1C00000 #define M_UPDOWN_ACCEL 0x16C0000 #define M_UPDOWN_FRICTION 0xB60000 #define M_MAX_UPDOWN 0x16C0000 #define M_CAM_ELEVATION (DEG_1 * -60) // = -10920 // clang-format on static const BITE m_UPVBites[6] = { { .pos = { .x = 0, .y = 0, .z = 0 }, .mesh_num = 3 }, { .pos = { .x = 0, .y = 96, .z = 256 }, .mesh_num = 0 }, { .pos = { .x = -128, .y = 0, .z = -64 }, .mesh_num = 1 }, { .pos = { .x = 0, .y = 0, .z = -64 }, .mesh_num = 1 }, { .pos = { .x = 128, .y = 0, .z = -64 }, .mesh_num = 2 }, { .pos = { .x = 0, .y = 0, .z = -64 }, .mesh_num = 2 }, }; typedef struct { int32_t vel; int32_t rot; int32_t rot_x; int16_t fan_rot; struct { bool control; bool surface; bool dive; bool dead; } flags; int8_t weapon_timer; bool current_weapon; // left|right } M_PRIV; typedef enum { // clang-format off M_STATE_DEATH = 0, M_STATE_GET_OFF_SURFACE = 2, M_STATE_MOVE = 4, M_STATE_POSE = 5, M_STATE_GET_ON = 8, M_STATE_GET_OFF = 9, // clang-format on } M_STATE; typedef enum { // clang-format off M_ANIM_DEATH = 0, M_ANIM_IDLE = 5, M_ANIM_GET_OFF_SURFACE = 9, M_ANIM_GET_ON_SURFACE = 10, M_ANIM_GET_ON_SURFACE_1 = 11, M_ANIM_GET_OFF = 12, M_ANIM_GET_ON = 13, // clang-format on } M_ANIM; static void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io) { M_PRIV *const p = item->priv; JSON_SHOULD(JSON_READ(io, "vel", &p->vel)); JSON_SHOULD(JSON_READ(io, "rot", &p->rot)); JSON_SHOULD(JSON_READ(io, "rot_x", &p->rot_x)); JSON_SHOULD(JSON_READ(io, "fan_rot", &p->fan_rot)); JSON_SHOULD(JSON_READ(io, "weapon_timer", &p->weapon_timer)); JSON_SHOULD(JSON_READ(io, "current_weapon", &p->current_weapon)); if (JSON_SHOULD(JSON_PUSH(io, "flags"))) { JSON_SHOULD(JSON_READ(io, "control", &p->flags.control)); JSON_SHOULD(JSON_READ(io, "surface", &p->flags.surface)); JSON_SHOULD(JSON_READ(io, "dive", &p->flags.dive)); JSON_SHOULD(JSON_READ(io, "dead", &p->flags.dead)); JSON_POP(io); } } static void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io) { const M_PRIV *const p = item->priv; JSONW_WRITE(io, "vel", p->vel); JSONW_WRITE(io, "rot", p->rot); JSONW_WRITE(io, "rot_x", p->rot_x); JSONW_WRITE(io, "fan_rot", p->fan_rot); JSONW_WRITE(io, "weapon_timer", p->weapon_timer); JSONW_WRITE(io, "current_weapon", p->current_weapon); JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "control", p->flags.control); JSONW_WRITE(io, "surface", p->flags.surface); JSONW_WRITE(io, "dive", p->flags.dive); JSONW_WRITE(io, "dead", p->flags.dead); JSONW_POP_AND_SET(io, "flags"); } static void M_Initialise(int16_t item_num) { ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; p->flags.surface = true; } static bool M_CanGetOn(const ITEM *const item) { const LARA_INFO *const lara = Lara_GetLaraInfo(); const ITEM *const lara_item = Lara_GetItem(); if (!g_Input.action || lara->gun_status != LGS_ARMLESS || lara_item->gravity) { return false; } if (ABS(lara_item->pos.y - item->pos.y + 128) > 256) { return false; } const int32_t dist = XYZ_32_GetLength2((XYZ_32) { .x = lara_item->pos.x - item->pos.x, .y = 0, .z = lara_item->pos.z - item->pos.z, }); if (dist > SQUARE(WALL_L / 2)) { return false; } int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); const int32_t h = Room_GetHeight(sector, item->pos); if (h < -32000) { return false; } return true; } static bool M_CanGetOff(const ITEM *const item) { const LARA_INFO *const lara = Lara_GetLaraInfo(); M_PRIV *const p = item->priv; if (lara->current.vel.x || lara->current.vel.z || p->vel) { return false; } const int32_t rad = WALL_L * Math_Cos(item->rot.x) >> W2V_SHIFT; XYZ_32 pos = { .x = item->pos.x + ((rad * Math_Sin(item->rot.y + DEG_180)) >> W2V_SHIFT), .y = item->pos.y - ((WALL_L * Math_Sin(item->rot.x)) >> W2V_SHIFT), .z = item->pos.z + ((rad * Math_Cos(item->rot.y + DEG_180)) >> W2V_SHIFT), }; int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(pos, &room_num); const int32_t h = Room_GetHeight(sector, pos); if (h == NO_HEIGHT || pos.y > h) { return false; } const int32_t c = Room_GetCeiling(sector, pos); if (h - c < STEP_L || pos.y < c || c == NO_HEIGHT) { return false; } return true; } static void M_GetOn(ITEM *const item) { LARA_INFO *const lara = Lara_GetLaraInfo(); ITEM *const lara_item = Lara_GetItem(); Lara_Vehicle_SetIndex(Item_GetIndex(item)); lara->water_status = LWS_ABOVE_WATER; if (lara->gun_type == LGT_FLARE) { Lara_Flare_Dispose(false); lara->flare.control = false; lara->gun_type = LGT_UNARMED; lara->request_gun_type = LGT_UNARMED; } lara->gun_status = LGS_HANDS_BUSY; // item->hit_points = 1; // TODO: why is it set? lara_item->pos = item->pos; lara_item->rot.y = item->rot.y; if (lara_item->current_anim_state == LS(LS_SURF_TREAD) || lara_item->current_anim_state == LS(LS_SURF_SWIM)) { Item_SwitchToObjAnim( lara_item, M_ANIM_GET_ON_SURFACE, 0, O_LARA_VEHICLE_ANIM); } else { Item_SwitchToObjAnim(lara_item, M_ANIM_GET_ON, 0, O_LARA_VEHICLE_ANIM); } lara_item->goal_anim_state = M_STATE_GET_ON; lara_item->current_anim_state = M_STATE_GET_ON; Item_Animate(lara_item); if (item->status != IS_ACTIVE) { item->status = IS_ACTIVE; Item_AddActive(Item_GetIndex(item)); } } static void M_Collision( int16_t item_num, ITEM *const lara_item, COLL_INFO *coll) { ITEM *const item = Item_Get(item_num); if (lara_item->hit_points < 0 || Lara_Vehicle_GetItem() != nullptr) { return; } if (M_CanGetOn(item)) { M_GetOn(item); } else { item->pos.y += 128; if (Lara_TestBoundsCollide(item, coll->radius) && Collide_TestCollision(item, lara_item)) { Lara_Col_ItemPush(item, coll, false, false); } item->pos.y -= 128; } } static bool M_Draw(const ITEM *const item) { int32_t rate; ANIM_FRAME *frames[2]; const int32_t frac = Item_GetFrames(item, frames, &rate); OBJECT *const obj = Object_Get(item->object_id); Output_DrawShadow(256, &frames[0]->bounds, item); Matrix_Push(); Matrix_TranslateAbs32((XYZ_32) { item->interp.result.pos.x, item->interp.result.pos.y + 128, item->interp.result.pos.z, }); Matrix_Rot16(item->interp.result.rot); bool result = false; const CLIP clip = Output_CheckBoundsClip(&frames[0]->bounds); if (clip == CLIP_NOT_VISIBLE) { goto finish; } M_PRIV *const p = item->priv; Output_CalculateObjectLighting(item, &frames[0]->bounds); const ANIM_BONE *const bone = Anim_GetBone(obj->bone_idx); if (frac != 0) { Matrix_InitInterpolate(frac, rate); Matrix_TranslateRel16_ID(frames[0]->offset, frames[1]->offset); Matrix_Rot16_ID(frames[0]->mesh_rots[0], frames[1]->mesh_rots[0]); Output_DrawObjectMesh_I(Object_GetMesh(obj->mesh_idx), clip); Matrix_Push_I(); Matrix_TranslateRel32_I(bone[0].pos); Matrix_Rot16_ID(frames[0]->mesh_rots[1], frames[1]->mesh_rots[1]); Matrix_RotX_I((item->rot.z + (p->rot_x >> 13))); Output_DrawObjectMesh_I(Object_GetMesh(obj->mesh_idx + 1), clip); Matrix_Pop_I(); Matrix_Push_I(); Matrix_TranslateRel32_I(bone[1].pos); Matrix_Rot16_ID(frames[0]->mesh_rots[2], frames[1]->mesh_rots[2]); Matrix_RotX_I(((p->rot_x >> 13) - item->rot.z)); Output_DrawObjectMesh_I(Object_GetMesh(obj->mesh_idx + 2), clip); Matrix_Pop_I(); Matrix_Push_I(); Matrix_TranslateRel32_I(bone[2].pos); Matrix_Rot16_ID(frames[0]->mesh_rots[3], frames[1]->mesh_rots[3]); Matrix_RotZ_I(p->fan_rot); Output_DrawObjectMesh_I(Object_GetMesh(obj->mesh_idx + 3), clip); Matrix_Pop_I(); } else { Matrix_TranslateRel16(frames[0]->offset); Matrix_Rot16(frames[0]->mesh_rots[0]); Output_DrawObjectMesh(Object_GetMesh(obj->mesh_idx), clip); Matrix_Push(); Matrix_TranslateRel32(bone[0].pos); Matrix_Rot16(frames[0]->mesh_rots[1]); Matrix_RotX((item->rot.z + (p->rot_x >> 13))); Output_DrawObjectMesh(Object_GetMesh(obj->mesh_idx + 1), clip); Matrix_Pop(); Matrix_Push(); Matrix_TranslateRel32(bone[1].pos); Matrix_Rot16(frames[0]->mesh_rots[2]); Matrix_RotX(((p->rot_x >> 13) - item->rot.z)); Output_DrawObjectMesh(Object_GetMesh(obj->mesh_idx + 2), clip); Matrix_Pop(); Matrix_Push(); Matrix_TranslateRel32(bone[2].pos); Matrix_Rot16(frames[0]->mesh_rots[3]); Matrix_RotZ(p->fan_rot); Output_DrawObjectMesh(Object_GetMesh(obj->mesh_idx + 3), clip); Matrix_Pop(); } result = true; finish: Matrix_Pop(); return result; } static void M_UserInput( ITEM *const item, ITEM *const lara_item, M_PRIV *const p) { LARA_INFO *const lara = Lara_GetLaraInfo(); XYZ_32 pos; GAME_VECTOR start_pos; GAME_VECTOR target_pos; int32_t water_height; int16_t anim, frame; M_CanGetOff(item); anim = Item_GetRelativeObjAnim(lara_item, O_LARA_VEHICLE_ANIM); frame = Item_GetRelativeFrame(lara_item); switch (lara_item->current_anim_state) { case M_STATE_DEATH: if (anim == M_ANIM_DEATH && (frame == 16 || frame == 17)) { pos.x = 0; pos.y = 0; pos.z = 0; Lara_GetMeshPos(LM_HIPS, &pos); lara_item->pos = pos; Item_SwitchToAnim(lara_item, LA(LA_UNDERWATER_DEATH), 0); lara_item->current_anim_state = LS_UW_DEATH; lara_item->goal_anim_state = LS_UW_DEATH; lara_item->fall_speed = 0; lara_item->gravity = false; lara_item->rot.x = 0; lara_item->rot.z = 0; p->flags.dead = true; } item->speed = 0; break; case M_STATE_GET_OFF_SURFACE: if (anim == M_ANIM_GET_OFF_SURFACE && frame == 51) { pos.x = 0; pos.y = 0; pos.z = 0; water_height = Room_GetWaterHeight(lara_item->pos, lara_item->room_num); int32_t water_surface_dist; if (water_height == NO_HEIGHT) { water_surface_dist = NO_HEIGHT; } else { water_surface_dist = lara_item->pos.y - water_height; } Lara_GetMeshPos(LM_HIPS, &pos); lara_item->pos = pos; Item_SwitchToAnim(lara_item, LA(LA_UNDERWATER_TO_ONWATER), 0); lara_item->current_anim_state = LS_SURF_TREAD; lara_item->goal_anim_state = LS_SURF_TREAD; lara_item->fall_speed = 0; lara_item->gravity = false; lara_item->rot.x = 0; lara_item->rot.z = 0; Lara_UpdateRoomToHeight(-381); lara->water_status = LWS_SURFACE; lara->water_surface_dist = -water_surface_dist; lara->dive_timer = 11; lara->torso_rot.x = 0; lara->torso_rot.y = 0; lara->head_rot.x = 0; lara->head_rot.y = 0; lara->gun_status = LGS_ARMLESS; Lara_Vehicle_SetIndex(NO_ITEM); item->hit_points = 0; } break; case M_STATE_MOVE: if (lara_item->hit_points <= 0) { lara_item->goal_anim_state = 0; break; } if (g_Input.left) { p->rot -= M_ROT_ACCELERATION; } else if (g_Input.right) { p->rot += M_ROT_ACCELERATION; } if (p->flags.surface) { if (item->rot.x > 9100) { item->rot.x -= 182; } else if (item->rot.x < 9100) { item->rot.x += 182; } } else if (g_Input.forward) { p->rot_x -= M_UPDOWN_ACCEL; } else if (g_Input.back) { p->rot_x += M_UPDOWN_ACCEL; } if (g_Input.jump) { if (p->flags.surface && g_Input.forward && item->rot.x > -2730) { p->flags.dive = true; } p->vel += M_ACCELERATION; } else { lara_item->goal_anim_state = M_STATE_POSE; } break; case M_STATE_POSE: if (lara_item->hit_points <= 0) { lara_item->goal_anim_state = 0; break; } if (g_Input.left) { p->rot -= M_ROT_SLOWACCEL; } else if (g_Input.right) { p->rot += M_ROT_SLOWACCEL; } if (p->flags.surface) { if (item->rot.x > 9100) { item->rot.x -= 182; } else if (item->rot.x < 9100) { item->rot.x += 182; } } else if (g_Input.forward) { p->rot_x -= M_UPDOWN_ACCEL; } else if (g_Input.back) { p->rot_x += M_UPDOWN_ACCEL; } if (g_Input.roll && M_CanGetOff(item)) { if (p->flags.surface) { lara_item->goal_anim_state = M_STATE_GET_OFF_SURFACE; } else { lara_item->goal_anim_state = M_STATE_GET_OFF; } p->flags.control = false; Sound_StopEffect(SFX_UPV_LOOP); Sound_Effect(SFX_UPV_STOP, &item->pos, SPM_ALWAYS); } else if (g_Input.jump) { if (p->flags.surface && g_Input.forward && item->rot.x > -2730) { p->flags.dive = true; } lara_item->goal_anim_state = M_STATE_MOVE; } break; case M_STATE_GET_ON: if (anim == M_ANIM_GET_ON_SURFACE_1) { item->rot.x += 182; item->pos.y += 4; if (frame == 30) { Sound_Effect(SFX_UPV_START, &item->pos, SPM_ALWAYS); } if (frame == 50) { p->flags.control = true; } } else if (anim == M_ANIM_GET_ON) { if (frame == 30) { Sound_Effect(SFX_UPV_START, &item->pos, SPM_ALWAYS); } if (frame == 42) { p->flags.control = true; } } break; case M_STATE_GET_OFF: if (anim == M_ANIM_GET_OFF && frame == 42) { pos.x = 0; pos.y = 0; pos.z = 0; Lara_GetMeshPos(LM_HIPS, &pos); start_pos.pos = item->pos; start_pos.room_num = item->room_num; target_pos.pos = pos; target_pos.room_num = item->room_num; Camera_LOSCheck(&start_pos, &target_pos, 0); lara_item->pos = target_pos.pos; Item_SwitchToAnim(lara_item, LA(LA_UNDERWATER_IDLE), 0); lara_item->current_anim_state = LS_TREAD; lara_item->fall_speed = 0; lara_item->gravity = false; lara_item->rot.x = 0; lara_item->rot.z = 0; Lara_UpdateRoomToHeight(0); lara->water_status = LWS_UNDERWATER; lara->gun_status = LGS_ARMLESS; Lara_Vehicle_SetIndex(NO_ITEM); item->hit_points = 0; } break; } if (p->flags.dive) { if (item->rot.x > -2730) { item->rot.x -= 910; } else { p->flags.dive = false; } } if (p->vel > 0) { p->vel -= M_FRICTION; CLAMPL(p->vel, 0); } else if (p->vel < 0) { p->vel += M_FRICTION; CLAMPG(p->vel, 0); } CLAMP(p->vel, -M_MAX_SPEED, M_MAX_SPEED); if (p->rot > 0) { p->rot -= M_ROT_FRICTION; CLAMPL(p->rot, 0); } else if (p->rot < 0) { p->rot += M_ROT_FRICTION; CLAMPG(p->rot, 0); } if (p->rot_x > 0) { p->rot_x -= M_UPDOWN_FRICTION; CLAMPL(p->rot_x, 0); } else if (p->rot_x < 0) { p->rot_x += M_UPDOWN_FRICTION; CLAMPG(p->rot_x, 0); } CLAMP(p->rot, -M_MAX_ROTATION, M_MAX_ROTATION); CLAMP(p->rot_x, -M_MAX_UPDOWN, M_MAX_UPDOWN); } static void M_DoCurrent(ITEM *const item) { LARA_INFO *const lara = Lara_GetLaraInfo(); const ITEM *const lara_item = Lara_GetItem(); if (lara->current.active != 0) { const int32_t sink_val = lara->current.active - 1; const OBJECT_VECTOR *const camera_obj = Camera_GetFixedObject(sink_val); const int32_t angle = -Math_Atan( lara_item->pos.x - camera_obj->pos.x, lara_item->pos.z - camera_obj->pos.z) - DEG_90; const int32_t speed = camera_obj->data; const int32_t xvel = (speed * Math_Sin(angle)) >> 4; const int32_t zvel = (speed * Math_Cos(angle)) >> 4; lara->current.vel.x += ((xvel - lara->current.vel.x) >> 4); lara->current.vel.z += ((zvel - lara->current.vel.z) >> 4); } else { int32_t shifter; int32_t abs_vel = ABS(lara->current.vel.x); if (abs_vel > 16) { shifter = 4; } else if (abs_vel > 8) { shifter = 3; } else { shifter = 2; } lara->current.vel.x -= lara->current.vel.x >> shifter; if (ABS(lara->current.vel.x) < 4) { lara->current.vel.x = 0; } abs_vel = ABS(lara->current.vel.z); if (abs_vel > 16) { shifter = 4; } else if (abs_vel > 8) { shifter = 3; } else { shifter = 2; } lara->current.vel.z -= lara->current.vel.z >> shifter; if (ABS(lara->current.vel.z) < 4) { lara->current.vel.z = 0; } if (lara->current.vel.x == 0 && lara->current.vel.z == 0) { return; } } item->pos.x += lara->current.vel.x >> 8; item->pos.z += lara->current.vel.z >> 8; lara->current.active = false; } static void M_FireHarpoon(ITEM *const item) { M_PRIV *const p = item->priv; AMMO_INFO *const ammo = Gun_GetAmmoInfo(LGT_HARPOON); if (ammo->ammo <= 0) { return; } const int16_t item_num = Item_Create(); if (item_num == NO_ITEM) { return; } ITEM *const bolt = Item_Get(item_num); bolt->object_id = O_HARPOON_BOLT; bolt->shade.value_1 = -0x3DF0; bolt->room_num = item->room_num; XYZ_32 pos = { .x = p->current_weapon != 0 ? 22 : -22, .y = 24, .z = 230, }; Collide_GetJointAbsPosition(item, &pos, 3); bolt->pos = pos; Item_Initialise(item_num); bolt->rot.x = item->rot.x; bolt->rot.y = item->rot.y; bolt->rot.z = 0; bolt->fall_speed = (-256 * Math_Sin(bolt->rot.x)) >> W2V_SHIFT; bolt->speed = (256 * Math_Cos(bolt->rot.x)) >> W2V_SHIFT; bolt->hit_points = 256; // bolt->item_flags[0] = 1; // TODO: what Item_AddActive(item_num); Sound_Effect(SFX_UPV_HARPOON, &Lara_GetItem()->pos, SPM_ALWAYS); if (!Game_IsBonusFlagSet(GBF_NGPLUS)) { ammo->ammo--; } Stats_AddAmmoUsed(); p->current_weapon ^= 1; } static void M_BackgroundCollision( ITEM *const item, const ITEM *const lara_item, M_PRIV *const p) { LARA_INFO *const lara = Lara_GetLaraInfo(); COLL_INFO coll = { .bad_pos = -NO_HEIGHT, .bad_neg = -400, .bad_ceiling = 400, .old = item->pos, .radius = 300, .slopes_are_walls = false, .slopes_are_pits = false, .lava_is_pit = false, .enable_hit = false, .enable_baddie_push = true, }; if (item->rot.x < -DEG_90 || item->rot.x > DEG_90) { lara->move_angle = item->rot.y + DEG_180; } else { lara->move_angle = item->rot.y; } coll.facing = lara->move_angle; int32_t h = (WALL_L * Math_Sin(item->rot.x)) >> W2V_SHIFT; h = ABS(h); CLAMPL(h, 200); coll.bad_neg = -h; Collide_GetCollisionInfo( &coll, item->pos.x, item->pos.y + h / 2, item->pos.z, item->room_num, h); Collide_ShiftItem(item, &coll); switch (coll.coll_type) { case COLL_FRONT: if (p->rot_x > 0x1FFE0000) { p->rot_x += 0x16C0000; } else if (p->rot_x < -0x1FFE0000) { p->rot_x -= 0x16C0000; } else { p->vel = 0; } break; case COLL_TOP: if (p->rot_x >= -0x1FFE0000) { p->rot_x -= 0x16C0000; } break; case COLL_TOP_FRONT: p->vel = 0; break; case COLL_LEFT: item->rot.y += 910; break; case COLL_RIGHT: item->rot.y -= 910; break; case COLL_CLAMP: item->pos = coll.old; p->vel = 0; return; } if (coll.side_mid.floor < 0) { item->pos.y += coll.side_mid.floor; p->rot_x += 0x16C0000; } } static void M_TriggerMist( const XYZ_32 pos, const int32_t speed, const int16_t angle) { SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 0; spark->src_color.g = 0; spark->src_color.b = 0; spark->dst_color.r = 64; spark->dst_color.g = 64; spark->dst_color.b = 64; spark->fade_to_black = 12; spark->col_fade_speed = (Random_GetControl() & 3) + 4; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->life = (Random_GetControl() & 3) + 20; spark->s_life = spark->life; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 0xF) - 8; spark->pos.y = pos.y + (Random_GetControl() & 0xF) - 8; spark->pos.z = pos.z + (Random_GetControl() & 0xF) - 8; spark->vel.x = (Random_GetControl() & 0x7F) + ((speed * Math_Sin(angle)) >> (W2V_SHIFT + 2)) - 64; spark->vel.y = 0; spark->vel.z = (Random_GetControl() & 0x7F) + ((speed * Math_Cos(angle)) >> (W2V_SHIFT + 2)) - 64; spark->friction = 3; if (Random_GetControl() & 1) { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = Random_GetControl() & 0xFFF; if (Random_GetControl() & 1) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = (Random_GetControl() & 0xF) + 16; } } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->scalar = 3; spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->max_y_vel = 0; spark->gravity = 0; spark->dst_size.width = ((Random_GetControl() & 7) + (speed >> 1) + 16); spark->dst_size.height = spark->dst_size.width; spark->src_size.width = spark->dst_size.width >> 2; spark->src_size.height = spark->src_size.width; spark->size.width = spark->src_size.width; spark->size.height = spark->src_size.height; Sparks_FinishSetup(spark); } static void M_Control(int16_t item_num) { XYZ_32 pos; GAME_VECTOR start_pos; GAME_VECTOR target_pos; int32_t c; ITEM *const item = Item_Get(item_num); M_PRIV *const p = item->priv; if (Lara_Vehicle_GetItem() == item) { if (p->vel) { p->fan_rot += (p->vel >> 12); pos = m_UPVBites[0].pos; Collide_GetJointAbsPosition(item, &pos, m_UPVBites[0].mesh_num); M_TriggerMist( (XYZ_32) { pos.x, pos.y + 128, pos.z }, ABS(p->vel) >> 16, item->rot.y + DEG_180); if (!(Random_GetControl() & 1)) { XYZ_32 bubble_pos = { .x = pos.x + (Random_GetControl() & 0x3F) - 32, .y = pos.y + 128, .z = pos.z + (Random_GetControl() & 0x3F) - 32, }; int16_t room_num = item->room_num; Room_GetSector(bubble_pos, &room_num); Spawn_BubbleEx(&bubble_pos, room_num, 4, 8); } } else { p->fan_rot += 364; } } for (int32_t i = 0; i < 2; i++) { pos = (XYZ_32) { .x = m_UPVBites[1].pos.x, .y = m_UPVBites[1].pos.y, .z = m_UPVBites[1].pos.z << (6 * i), }; Collide_GetJointAbsPosition(item, &pos, m_UPVBites[1].mesh_num); c = 255 - (Random_GetControl() & 0x1F); if (i == 1) { target_pos.pos = pos; target_pos.room_num = item->room_num; LOS_Check(&start_pos, &target_pos, true); pos = target_pos.pos; } else { start_pos.pos = pos; start_pos.room_num = item->room_num; } Output_AddDynamicLightRGB(pos, 8 * i + 16, (RGB_888) { c, c, c }); } if (p->weapon_timer > 0) { p->weapon_timer--; } } bool UPV_Control(void) { ITEM *const item = Lara_Vehicle_GetItem(); ITEM *const lara_item = Lara_GetItem(); LARA_INFO *const lara = Lara_GetLaraInfo(); M_PRIV *const p = item->priv; if (!p->flags.dead) { M_UserInput(item, lara_item, p); item->speed = p->vel >> 16; item->rot.x += p->rot_x >> 16; item->rot.y += p->rot >> 16; item->rot.z = (int16_t)(p->rot >> 12); if (item->rot.x > 14560) { item->rot.x = 14560; } else if (item->rot.x < -14560) { item->rot.x = -14560; } item->pos.x += (Math_Cos(item->rot.x) * ((item->speed * Math_Sin(item->rot.y)) >> W2V_SHIFT)) >> W2V_SHIFT; item->pos.y -= (item->speed * Math_Sin(item->rot.x)) >> W2V_SHIFT; item->pos.z += (Math_Cos(item->rot.x) * ((item->speed * Math_Cos(item->rot.y)) >> W2V_SHIFT)) >> W2V_SHIFT; } int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); item->floor = Room_GetHeight(sector, item->pos); if (p->flags.control && !p->flags.dead) { const int32_t water_height = Room_GetWaterHeightEx(item->pos, room_num, false); if (water_height != NO_HEIGHT && !Room_Get(item->room_num)->flags.underwater) { if (water_height - item->pos.y >= -210) { item->pos.y = water_height + 210; } if (!p->flags.surface) { Sound_Effect(SFX_LARA_BREATH, &lara_item->pos, SPM_ALWAYS); p->flags.dive = false; } p->flags.surface = true; } else if ( water_height != NO_HEIGHT && water_height - item->pos.y >= -210) { item->pos.y = water_height + 210; if (!p->flags.surface) { Sound_Effect(SFX_LARA_BREATH, &lara_item->pos, SPM_ALWAYS); p->flags.dive = false; } p->flags.surface = true; } else { p->flags.surface = false; } if (p->flags.surface) { if (lara_item->hit_points >= 0) { lara->air += 10; CLAMPG(lara->air, LARA_MAX_AIR); } } else if (lara_item->hit_points >= 0) { lara->air--; if (lara->air < 0) { lara->air = -1; lara_item->hit_points -= 5; } } } Room_TestTriggers(lara_item); Room_TestTriggers(item); if (Lara_Vehicle_GetItem() == nullptr) { if (!p->flags.dead) { return false; } } else if (!p->flags.dead) { M_DoCurrent(item); if (g_Input.action && p->flags.control && p->weapon_timer == 0) { M_FireHarpoon(item); p->weapon_timer = 15; } if (room_num != item->room_num) { Item_UpdateRoom(Lara_Vehicle_GetIndex(), room_num); Item_UpdateRoom(lara->item_num, room_num); } lara_item->pos.x = item->pos.x; lara_item->pos.y = item->pos.y + 128; lara_item->pos.z = item->pos.z; lara_item->rot = item->rot; Item_Animate(lara_item); M_BackgroundCollision(item, lara_item, p); if (p->flags.control) { Sound_Effect( SFX_UPV_LOOP, &item->pos, (item->speed << 16) | 0x1000000 | SPM_PITCH | SPM_ALWAYS); } const int16_t anim_idx = Item_GetRelativeObjAnim(lara_item, O_LARA_VEHICLE_ANIM); const int16_t frame_idx = Item_GetRelativeFrame(lara_item); Item_SwitchToAnim(item, anim_idx, frame_idx); g_Camera.target_elevation = p->flags.surface ? M_CAM_ELEVATION : 0; return true; } Item_Animate(lara_item); if (room_num != item->room_num) { Item_UpdateRoom(Lara_Vehicle_GetIndex(), room_num); } M_BackgroundCollision(item, lara_item, p); p->rot_x = 0; Item_SwitchToAnim(item, M_ANIM_IDLE, 0); item->current_anim_state = M_STATE_POSE; item->goal_anim_state = M_STATE_POSE; item->fall_speed = 0; item->gravity = true; item->speed = 0; Item_Animate(item); return true; } static void M_Setup(OBJECT *const obj) { obj->priv_size = sizeof(M_PRIV); obj->priv_load_func = M_LoadPriv; obj->priv_save_func = M_SavePriv; obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = M_Collision; obj->draw_func = M_Draw; obj->save_position = true; obj->save_flags = true; obj->save_anim = true; } REGISTER_OBJECT(O_UPV, M_Setup) ================================================ FILE: src/trx/game/objects/vehicles/upv.h ================================================ #pragma once bool UPV_Control(void); ================================================ FILE: src/trx/game/objects.h ================================================ #pragma once #include #include #include #include #include #include #include #include ================================================ FILE: src/trx/game/option/common.c ================================================ #include #include #include #include #include #include #include #include #include #include #include void Option_Reset(void) { Option_Shutdown(); } void Option_Shutdown(void) { Option_Gameplay_Shutdown(); Option_Graphics_Shutdown(); Option_Sound_Shutdown(); Option_Controls_Shutdown(); Option_GlobeSelect_Shutdown(); } void Option_Control(INVENTORY_ITEM *const inv_item, const bool is_busy) { if (inv_item->action == ACTION_EXAMINE) { Option_Examine_Control(inv_item, is_busy); return; } switch (inv_item->object_id) { case O_PASSPORT_OPTION: Option_Passport_Control(inv_item, is_busy); break; case O_COMPASS_OPTION: case O_STOPWATCH_OPTION: Option_Stats_Control(inv_item, is_busy); break; case O_PDA_OPTION: Option_Gameplay_Control(inv_item, is_busy); break; case O_DETAIL_OPTION: Option_Graphics_Control(inv_item, is_busy); break; case O_SOUND_OPTION: Option_Sound_Control(inv_item, is_busy); break; case O_CONTROL_OPTION: Option_Controls_Control(inv_item, is_busy); break; case O_GLOBE_SELECT_OPTION: Option_GlobeSelect_Control(inv_item, is_busy); break; case O_PISTOL_OPTION: case O_SHOTGUN_OPTION: case O_MAGNUM_OPTION: case O_AUTOS_OPTION: case O_DESERT_EAGLE_OPTION: case O_UZI_OPTION: case O_HARPOON_OPTION: case O_M16_OPTION: case O_MP5_OPTION: case O_GRENADE_GUN_OPTION: case O_ROCKET_GUN_OPTION: case O_EXPLOSIVE_OPTION: case O_SMALL_MEDIPACK_OPTION: case O_LARGE_MEDIPACK_OPTION: if (!is_busy) { g_InputDB.menu_confirm = 1; } break; case O_PISTOL_AMMO_OPTION: case O_SHOTGUN_AMMO_OPTION: case O_MAGNUM_AMMO_OPTION: case O_AUTOS_AMMO_OPTION: case O_DESERT_EAGLE_AMMO_OPTION: case O_UZI_AMMO_OPTION: case O_HARPOON_AMMO_OPTION: case O_M16_AMMO_OPTION: case O_MP5_AMMO_OPTION: case O_GRENADE_AMMO_OPTION: case O_ROCKET_AMMO_OPTION: break; case O_PUZZLE_OPTION_1: case O_PUZZLE_OPTION_2: case O_PUZZLE_OPTION_3: case O_PUZZLE_OPTION_4: case O_KEY_OPTION_1: case O_KEY_OPTION_2: case O_KEY_OPTION_3: case O_KEY_OPTION_4: case O_QUEST_OPTION_1: case O_QUEST_OPTION_2: case O_QUEST_OPTION_3: case O_QUEST_OPTION_4: case O_PICKUP_OPTION_1: case O_PICKUP_OPTION_2: case O_SCION_OPTION: case O_LEADBAR_OPTION: if (!is_busy) { g_InputDB.menu_confirm = 1; } break; default: if (!is_busy && (g_InputDB.menu_confirm || g_InputDB.menu_back)) { inv_item->goal_frame = 0; inv_item->anim_direction = -1; } break; } } void Option_Draw(INVENTORY_ITEM *const inv_item) { if (inv_item->action == ACTION_EXAMINE) { Option_Examine_Draw(); return; } switch (inv_item->object_id) { case O_PASSPORT_OPTION: Option_Passport_Draw(inv_item); break; case O_COMPASS_OPTION: case O_STOPWATCH_OPTION: Option_Stats_Draw(); break; case O_PDA_OPTION: Option_Gameplay_Draw(inv_item); break; case O_DETAIL_OPTION: Option_Graphics_Draw(inv_item); break; case O_SOUND_OPTION: Option_Sound_Draw(inv_item); break; case O_CONTROL_OPTION: Option_Controls_Draw(inv_item); break; case O_GLOBE_SELECT_OPTION: Option_GlobeSelect_Draw(inv_item); break; default: break; } } void Option_Close(const INVENTORY_ITEM *const inv_item) { switch (inv_item->object_id) { case O_PASSPORT_OPTION: Option_Passport_Close(); break; case O_COMPASS_OPTION: case O_STOPWATCH_OPTION: Option_Stats_Close(); break; case O_PDA_OPTION: Option_Gameplay_Close(); break; case O_DETAIL_OPTION: Option_Graphics_Close(); break; case O_SOUND_OPTION: Option_Sound_Close(); break; case O_CONTROL_OPTION: Option_Controls_Close(); break; case O_GLOBE_SELECT_OPTION: Option_GlobeSelect_Close(); break; default: Option_Examine_Close(); break; } } ================================================ FILE: src/trx/game/option/common.h ================================================ #pragma once #include void Option_Control(INVENTORY_ITEM *inv_item, bool is_busy); void Option_Draw(INVENTORY_ITEM *inv_item); void Option_Close(const INVENTORY_ITEM *inv_item); // Reset internal positioning of option UIs. void Option_Reset(void); // Free up resources associated with option UIs. void Option_Shutdown(void); ================================================ FILE: src/trx/game/option/controls.c ================================================ #include #include #include typedef struct { int32_t listeners[2]; struct { bool is_ready; UI_CONTROLS_STATE state; } ui; } M_PRIV; static M_PRIV m_Priv = {}; static void M_HandleKeyChange(const EVENT *event, void *user_data) { g_Config.dirty = true; Config_Update(); } static void M_HandleLayoutChange(const EVENT *event, void *user_data) { const M_PRIV *const p = user_data; g_Config.input.layout[p->ui.state.backend] = p->ui.state.editor_state[p->ui.state.backend].active_layout; Config_Update(); } static void M_Init(M_PRIV *const p) { UI_Controls_Init(&p->ui.state); p->ui.is_ready = true; p->listeners[0] = EventManager_Subscribe( p->ui.state.events, "layout_change", nullptr, M_HandleLayoutChange, p); p->listeners[1] = EventManager_Subscribe( p->ui.state.events, "key_change", nullptr, M_HandleKeyChange, p); } static void M_Shutdown(M_PRIV *const p) { if (p->ui.is_ready) { EventManager_Unsubscribe(p->ui.state.events, p->listeners[0]); EventManager_Unsubscribe(p->ui.state.events, p->listeners[1]); UI_Controls_Free(&p->ui.state); p->ui.is_ready = false; } } void Option_Controls_Control(INVENTORY_ITEM *const inv_item, const bool is_busy) { M_PRIV *const p = &m_Priv; if (is_busy) { return; } if (!p->ui.is_ready) { M_Init(p); } if (!UI_Controls_Control(&p->ui.state)) { g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; } } void Option_Controls_Draw(INVENTORY_ITEM *const inv_item) { M_PRIV *const p = &m_Priv; if (p->ui.is_ready) { UI_Controls(&p->ui.state); } } void Option_Controls_Close(void) { } void Option_Controls_Shutdown(void) { M_PRIV *const p = &m_Priv; M_Shutdown(p); } ================================================ FILE: src/trx/game/option/controls.h ================================================ #pragma once #include #include void Option_Controls_Control(INVENTORY_ITEM *inv_item, bool is_busy); void Option_Controls_Draw(INVENTORY_ITEM *inv_item); void Option_Controls_Close(void); void Option_Controls_Shutdown(void); ================================================ FILE: src/trx/game/option/examine.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #define M_EXAMINE_ROTATION_SPEED (DEG_1 * 3) typedef struct { OBJECT_ID object_id; bool is_dialog_hidden; struct { bool is_ready; UI_TEXT_DIALOG_STATE *state; } ui; } M_PRIV; static M_PRIV m_Priv = {}; static void M_DrawHideDialogFooter(void *const user_data) { UI_BeginAnchor(0.5f, 0.5f); UI_LabelFmt( "\\{input look} %s", user_data != nullptr ? (const char *)user_data : ""); UI_EndAnchor(); } static void M_DrawRotateHint(void *const user_data) { UI_ButtonLabelEx( g_Config.input.backend == INPUT_BACKEND_KEYBOARD ? GS("general/misc/direction_keys_keyboard") : GS("general/misc/direction_keys_controller"), GS("general/actions/rotate")); } static bool M_ShouldShowDialog(const OBJECT_ID obj_id) { const char *const description = Object_GetDescription(obj_id); return !String_IsEmpty(description); } static int32_t M_GetMaxRows(void) { const int32_t res_h = UI_Scaler_CalcInverse( Viewport_GetHeight(VIEWPORT_UI), UI_SCALER_TARGET_TEXT); if (res_h <= 240) { return 5; } else if (res_h <= 384) { return 7; } else { return 12; } } static void M_Init(M_PRIV *const p, const OBJECT_ID obj_id) { p->object_id = obj_id; p->is_dialog_hidden = false; p->ui.is_ready = true; p->ui.state = UI_TextDialog_Init( UI_GetCanvasWidth() * 2.0 / 3.0f, M_GetMaxRows(), false); } static void M_Close(M_PRIV *const p) { InvRing_ClearButtonHint(); if (p->ui.is_ready) { UI_TextDialog_Free(p->ui.state); p->ui.state = nullptr; p->ui.is_ready = false; } } static void M_ApplyExamineRotation(INVENTORY_ITEM *const inv_item) { const int32_t yaw_input = (g_Input.menu_left ? 1 : 0) - (g_Input.menu_right ? 1 : 0); const int32_t pitch_input = (g_Input.menu_down ? 1 : 0) - (g_Input.menu_up ? 1 : 0); if (yaw_input == 0 && pitch_input == 0) { return; } inv_item->has_manual_rot = true; MATRIX delta = g_IDMatrix; if (yaw_input != 0) { Matrix_RotY_M(&delta, yaw_input * M_EXAMINE_ROTATION_SPEED); } if (pitch_input != 0) { Matrix_RotX_M(&delta, pitch_input * M_EXAMINE_ROTATION_SPEED); } Matrix_Mul3x3_M(&inv_item->manual_rot, &delta, &inv_item->manual_rot); } bool Option_Examine_CanExamine(const OBJECT_ID obj_id) { return Object_GetDescription(obj_id) != nullptr; } void Option_Examine_Control(INVENTORY_ITEM *const inv_item, const bool is_busy) { M_PRIV *const p = &m_Priv; if (is_busy) { return; } const OBJECT_ID obj_id = inv_item->object_id; if (!p->ui.is_ready) { M_Init(p, obj_id); } const bool has_dialog = M_ShouldShowDialog(obj_id); const bool show_dialog = has_dialog && !p->is_dialog_hidden; if (show_dialog) { InvRing_ClearButtonHint(); } else { InvRing_SetButtonHintDrawer(M_DrawRotateHint, nullptr); } if (show_dialog) { UI_TextDialog_Control(p->ui.state); } if (g_InputDB.look) { if (show_dialog) { p->is_dialog_hidden = true; return; } else { g_InputDB.menu_back = true; g_InputDB.menu_confirm = false; inv_item->has_manual_rot = false; p->is_dialog_hidden = false; M_Close(p); return; } } if (g_InputDB.menu_back || g_InputDB.menu_confirm) { g_InputDB.menu_back = true; g_InputDB.menu_confirm = false; inv_item->has_manual_rot = false; p->is_dialog_hidden = false; M_Close(p); return; } if (!show_dialog) { M_ApplyExamineRotation(inv_item); } } void Option_Examine_Draw(void) { M_PRIV *const p = &m_Priv; if (!p->ui.is_ready) { return; } if (M_ShouldShowDialog(p->object_id) && !p->is_dialog_hidden) { const char *const footer_label = GS("general/actions/hide_dialog"); UI_TextDialogEx( p->ui.state, (UI_TEXT_DIALOG_SETTINGS) { .title_raw = Object_GetName(p->object_id), .text_raw = Object_GetDescription(p->object_id), .footer_func = M_DrawHideDialogFooter, .footer_user_data = (void *)footer_label, }); } } void Option_Examine_Close(void) { M_PRIV *const p = &m_Priv; M_Close(p); } ================================================ FILE: src/trx/game/option/examine.h ================================================ #pragma once #include bool Option_Examine_CanExamine(OBJECT_ID obj_id); void Option_Examine_Control(INVENTORY_ITEM *inv_item, bool is_busy); void Option_Examine_Draw(void); void Option_Examine_Close(void); ================================================ FILE: src/trx/game/option/gameplay.c ================================================ #include #include #include typedef struct { bool is_ready; UI_SETTINGS_DIALOG_STATE *ui_state; } M_PRIV; static M_PRIV m_Priv = {}; static void M_Init(M_PRIV *const p) { p->is_ready = true; if (p->ui_state == nullptr) { p->ui_state = UI_GameplaySettings_Init(); } } static void M_Shutdown(M_PRIV *const p) { if (p->ui_state != nullptr) { UI_GameplaySettings_Free(p->ui_state); p->ui_state = nullptr; } p->is_ready = false; } void Option_Gameplay_Control(INVENTORY_ITEM *const inv_item, const bool is_busy) { M_PRIV *const p = &m_Priv; if (is_busy) { return; } if (!p->is_ready) { M_Init(p); } if (UI_GameplaySettings_Control(p->ui_state)) { if (g_InputDB.menu_confirm || g_InputDB.menu_back) { inv_item->anim_direction = 1; inv_item->goal_frame = inv_item->frames_total - 1; } } else { g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; } } void Option_Gameplay_Draw(INVENTORY_ITEM *const inv_item) { M_PRIV *const p = &m_Priv; if (p->is_ready && p->ui_state != nullptr) { UI_GameplaySettings(p->ui_state); } } void Option_Gameplay_Close(void) { M_PRIV *const p = &m_Priv; p->is_ready = false; } void Option_Gameplay_Shutdown(void) { M_PRIV *const p = &m_Priv; M_Shutdown(p); } ================================================ FILE: src/trx/game/option/gameplay.h ================================================ #pragma once #include void Option_Gameplay_Control(INVENTORY_ITEM *inv_item, bool is_busy); void Option_Gameplay_Draw(INVENTORY_ITEM *inv_item); void Option_Gameplay_Close(void); void Option_Gameplay_Shutdown(void); ================================================ FILE: src/trx/game/option/globe_select.c ================================================ #include #include #include #include #include #include #include #include #include #include typedef struct { GAME_STRING_ID gs_area_id; } M_AREA_STRING_ENTRY; static const M_AREA_STRING_ENTRY m_AreaStrings[] = { { .gs_area_id = GS_ID("general/globe_select/area_1") }, { .gs_area_id = GS_ID("general/globe_select/area_2") }, { .gs_area_id = GS_ID("general/globe_select/area_3") }, { .gs_area_id = GS_ID("general/globe_select/area_4") }, // Unused Peru { .gs_area_id = GS_ID("general/globe_select/area_5") }, { .gs_area_id = GS_ID("general/globe_select/area_6") }, }; static int32_t M_GetEntryCount(void) { return MIN(g_GameFlow.globe.count, (int32_t)ARRAY_SIZE(m_AreaStrings)); } static const GF_GLOBE_ENTRY *M_GetEntry(const int32_t idx) { const int32_t entry_count = M_GetEntryCount(); if (idx < 0 || idx >= entry_count) { return nullptr; } return &g_GameFlow.globe.entries[idx]; } static bool M_IsLevelCompleted(const int32_t level_ordinal) { if (level_ordinal < 0) { return false; } const GF_LEVEL *const level = GF_GetLevelByOrdinalNumber(GFLT_MAIN, level_ordinal); if (level == nullptr) { return false; } const RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); return resume != nullptr && resume->level_completed; } static int32_t M_GetNextSelectableIndex( const INV_RING *const ring, const int32_t direction) { if (direction == 0) { return ring->globe_select.selection; } const int32_t entry_count = M_GetEntryCount(); if (entry_count <= 0) { return ring->globe_select.selection; } for (int32_t step = 0; step < entry_count; step++) { int32_t idx = ring->globe_select.selection + direction * (step + 1); while (idx < 0 && entry_count != 0) { idx += entry_count; } idx %= entry_count; if (ring->globe_select.selectable[idx]) { return idx; } } return ring->globe_select.selection; } static bool M_UpdateRotAxis(int16_t *const cur, const int16_t target) { int16_t ang = target - *cur; if (ang >= 128 || ang <= -128) { *cur += ang >> 3; return false; } *cur = target; return true; } static bool M_IsAligned(INV_RING *const ring) { const GF_GLOBE_ENTRY *const entry = M_GetEntry(ring->globe_select.selection); if (entry == nullptr) { return true; } int32_t axes = 0; axes += M_UpdateRotAxis(&ring->globe_select.rot.x, entry->rot.x) ? 1 : 0; axes += M_UpdateRotAxis(&ring->globe_select.rot.y, entry->rot.y) ? 1 : 0; axes += M_UpdateRotAxis(&ring->globe_select.rot.z, entry->rot.z) ? 1 : 0; return axes == 3; } int32_t Option_GlobeSelect_AreaFromMeshIdx(const int32_t mesh_idx) { const int32_t entry_count = M_GetEntryCount(); for (int32_t i = 0; i < entry_count; i++) { if (g_GameFlow.globe.entries[i].mesh_idx == mesh_idx) { return (int32_t)i; } } return -1; } void Option_GlobeSelect_UpdateSelectable(INV_RING *const ring) { ring->globe_select.selection = -1; ring->globe_select.rot.x = 0; ring->globe_select.rot.y = 0; ring->globe_select.rot.z = 0; ring->globe_select.meshes_drawn = 0x0FFFu; ring->globe_select.confirmed = false; for (int32_t i = 0; i < MAX_GLOBE_ZONES; i++) { ring->globe_select.selectable[i] = false; ring->globe_select.start_level_num[i] = -1; } uint32_t completed_mask = 0u; const int32_t entry_count = M_GetEntryCount(); for (int32_t i = 0; i < entry_count; i++) { if (M_IsLevelCompleted( g_GameFlow.globe.entries[i].completion_level_ordinal)) { completed_mask |= 1u << i; } } int32_t selectable_count = 0; for (int32_t i = 0; i < entry_count; i++) { ring->globe_select.selectable[i] = false; ring->globe_select.start_level_num[i] = -1; const GF_GLOBE_ENTRY *const entry = &g_GameFlow.globe.entries[i]; const GF_LEVEL *const start_level = GF_GetLevelByOrdinalNumber(GFLT_MAIN, entry->start_level_ordinal); if (start_level != nullptr) { ring->globe_select.start_level_num[i] = start_level->num; } if ((completed_mask & (1u << i)) != 0u) { continue; } if ((completed_mask & entry->prereq_mask) != entry->prereq_mask) { continue; } if (start_level == nullptr) { continue; } ring->globe_select.selectable[i] = true; selectable_count++; } ring->globe_select.meshes_drawn = 0x0FFFu; for (int32_t i = 0; i < entry_count; i++) { if (ring->globe_select.start_level_num[i] < 0) { ring->globe_select.meshes_drawn &= ~(1 << g_GameFlow.globe.entries[i].mesh_idx); } } Overlay_ShowArrow(UI_OVERLAY_ARROW_BCL, selectable_count > 1); Overlay_ShowArrow(UI_OVERLAY_ARROW_BCR, selectable_count > 1); if (ring->globe_select.selection < 0 || ring->globe_select.selection >= entry_count || !ring->globe_select.selectable[ring->globe_select.selection]) { ring->globe_select.selection = -1; for (int32_t i = 0; i < entry_count; i++) { if (ring->globe_select.selectable[i]) { ring->globe_select.selection = i; break; } } } } void Option_GlobeSelect_Control( INVENTORY_ITEM *const inv_item, const bool is_busy) { INV_RING *const ring = InvRing_GetActiveRing(); if (ring == nullptr || ring->mode != INV_GLOBE_SELECT_MODE) { return; } Overlay_SetTopText((OVERLAY_TEXT) { .kind = UI_OVERLAY_TEXT_GS_KEY, .fmt_gs_key = GS_ID("general/inventory_ring/heading_fmt"), .literal = GS_ID("general/inventory_ring/heading_adventure"), }); if (ring->globe_select.selection < 0) { Overlay_SetBottomText((OVERLAY_TEXT) { 0 }); ring->globe_select.confirmed = false; return; } const bool aligned = M_IsAligned(ring); if (aligned && !is_busy) { if (g_Input.menu_left) { ring->globe_select.selection = M_GetNextSelectableIndex(ring, -1); } else if (g_Input.menu_right) { ring->globe_select.selection = M_GetNextSelectableIndex(ring, 1); } } const int32_t entry_count = M_GetEntryCount(); if (ring->globe_select.selection >= 0 && ring->globe_select.selection < entry_count && ring->globe_select.selection < (int32_t)ARRAY_SIZE(m_AreaStrings)) { Overlay_SetBottomText((OVERLAY_TEXT) { .kind = UI_OVERLAY_TEXT_GS_KEY, .fmt_gs_key = GS_ID("general/inventory_ring/object_name_fmt"), .literal = m_AreaStrings[ring->globe_select.selection].gs_area_id, }); } else { Overlay_SetBottomText((OVERLAY_TEXT) { 0 }); } if (g_InputDB.menu_confirm && !is_busy) { if (!aligned) { g_InputDB.menu_confirm = false; return; } ring->globe_select.confirmed = true; } } void Option_GlobeSelect_Draw(INVENTORY_ITEM *const inv_item) { } void Option_GlobeSelect_Close(void) { } void Option_GlobeSelect_Shutdown(void) { } ================================================ FILE: src/trx/game/option/globe_select.h ================================================ #pragma once #include void Option_GlobeSelect_Control(INVENTORY_ITEM *inv_item, bool is_busy); void Option_GlobeSelect_Draw(INVENTORY_ITEM *inv_item); void Option_GlobeSelect_Close(void); void Option_GlobeSelect_Shutdown(void); void Option_GlobeSelect_UpdateSelectable(INV_RING *ring); int32_t Option_GlobeSelect_AreaFromMeshIdx(int32_t mesh_idx); ================================================ FILE: src/trx/game/option/graphics.c ================================================ #include #include #include typedef struct { UI_SETTINGS_DIALOG_STATE *ui_state; } M_PRIV; static M_PRIV m_Priv = {}; static void M_Init(M_PRIV *const p) { if (p->ui_state == nullptr) { p->ui_state = UI_GraphicSettings_Init(); } } static void M_Shutdown(M_PRIV *const p) { if (p->ui_state != nullptr) { UI_GraphicSettings_Free(p->ui_state); p->ui_state = nullptr; } } void Option_Graphics_Control(INVENTORY_ITEM *const inv_item, const bool is_busy) { M_PRIV *const p = &m_Priv; if (is_busy) { return; } if (p->ui_state == nullptr) { M_Init(p); } if (!UI_GraphicSettings_Control(p->ui_state)) { g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; } } void Option_Graphics_Draw(INVENTORY_ITEM *const inv_item) { M_PRIV *const p = &m_Priv; if (p->ui_state != nullptr) { UI_GraphicSettings(p->ui_state); } } void Option_Graphics_Close(void) { } void Option_Graphics_Shutdown(void) { M_PRIV *const p = &m_Priv; M_Shutdown(p); } ================================================ FILE: src/trx/game/option/graphics.h ================================================ #pragma once #include void Option_Graphics_Control(INVENTORY_ITEM *inv_item, bool is_busy); void Option_Graphics_Draw(INVENTORY_ITEM *inv_item); void Option_Graphics_Close(void); void Option_Graphics_Shutdown(void); ================================================ FILE: src/trx/game/option/passport.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_IMMEDIATE (g_TRVersion >= 2) typedef enum { M_ROLE_PLAY_ANY_LEVEL_SELECT_LEVEL, M_ROLE_PLAY_ANY_LEVEL_SELECT_MODE, M_ROLE_PLAY_PREV_LEVEL_SELECT_SLOT, M_ROLE_PLAY_PREV_LEVEL_SELECT_LEVEL, M_ROLE_SWITCH_MOD, M_ROLE_STORY_SO_FAR, M_ROLE_STORY_SO_FAR_CONFIRM, M_ROLE_NEW_GAME, M_ROLE_LOAD_GAME, M_ROLE_SAVE_GAME, M_ROLE_RESTART_LEVEL, M_ROLE_EXIT_TO_TITLE, M_ROLE_EXIT_GAME, } M_PAGE_ROLE; typedef struct { GAME_STRING_ID title; bool (*func)(INVENTORY_ITEM *inv_item); bool flat; } M_PAGE_HANDLER; typedef enum { PAGE_UNDETERMINED = -1, PAGE_1 = 0, PAGE_2 = 1, PAGE_3 = 2, PAGE_COUNT = 3, } M_PAGE_NUMBER; typedef enum { M_MODE_BROWSE, M_MODE_PICK_OPTION, } M_PAGE_MODE; typedef struct { M_PAGE_ROLE role; int32_t selection; struct { UI_NEW_GAME_STATE *new_game; UI_SELECT_LEVEL_DIALOG_STATE *select_level; UI_PLAY_ANY_LEVEL_DIALOG_STATE *play_any_level; UI_SAVE_SLOT_DIALOG_STATE *save_slot; UI_SWITCH_MOD_DIALOG_STATE *switch_mod; } ui; } M_NAV_FRAME; typedef struct { bool available; M_PAGE_ROLE role; struct { // Hierarchical navigation int32_t depth; M_NAV_FRAME stack[4]; M_NAV_FRAME *current; } nav; } M_PAGE; PASSPORT g_Passport = { .select_level = -1, }; static struct { M_PAGE_MODE mode; M_PAGE pages[PAGE_COUNT]; M_PAGE_NUMBER current_page; M_PAGE_NUMBER active_page; GAME_STRING_ID error_msg; } m_Priv = { .active_page = PAGE_UNDETERMINED, }; static void M_ResetNavStack(M_PAGE *const page) { ASSERT(page != nullptr); page->nav.depth = 0; page->nav.current = &page->nav.stack[page->nav.depth]; page->nav.current->selection = -1; page->nav.current->role = page->role; } static void M_InitText(void) { Overlay_ShowArrow(UI_OVERLAY_ARROW_BCL, false); Overlay_ShowArrow(UI_OVERLAY_ARROW_BCR, false); Overlay_SetBottomText((OVERLAY_TEXT) { 0 }); } static void M_FreeDialogs(M_NAV_FRAME *const frame) { if (frame->ui.select_level != nullptr) { UI_SelectLevelDialog_Free(frame->ui.select_level); frame->ui.select_level = nullptr; } if (frame->ui.play_any_level != nullptr) { UI_PlayAnyLevelDialog_Free(frame->ui.play_any_level); frame->ui.play_any_level = nullptr; } if (frame->ui.save_slot != nullptr) { UI_SaveSlotDialog_Free(frame->ui.save_slot); frame->ui.save_slot = nullptr; } if (frame->ui.switch_mod != nullptr) { UI_SwitchModDialog_Free(frame->ui.switch_mod); frame->ui.switch_mod = nullptr; } if (frame->ui.new_game != nullptr) { UI_NewGame_Free(frame->ui.new_game); frame->ui.new_game = nullptr; } } static void M_FreeAllDialogs(void) { for (M_PAGE_NUMBER i = PAGE_1; i < PAGE_COUNT; i++) { for (int32_t j = 0; j <= m_Priv.pages[i].nav.depth; j++) { M_FreeDialogs(&m_Priv.pages[i].nav.stack[j]); } } } static void M_RemoveAllText(void) { m_Priv.error_msg = nullptr; Overlay_ShowArrow(UI_OVERLAY_ARROW_BCL, false); Overlay_ShowArrow(UI_OVERLAY_ARROW_BCR, false); Overlay_SetBottomText((OVERLAY_TEXT) { 0 }); } static M_PAGE *M_TryGetActivePage(void) { if (m_Priv.active_page < 0 || m_Priv.active_page >= PAGE_COUNT) { return nullptr; } return &m_Priv.pages[m_Priv.active_page]; } static M_PAGE *M_GetActivePage(void) { M_PAGE *const page = M_TryGetActivePage(); ASSERT(page != nullptr); return page; } static bool M_IsArrowVisible(int32_t direction) { if (m_Priv.mode == M_MODE_PICK_OPTION && !M_IMMEDIATE) { return false; } const M_PAGE *const page = M_TryGetActivePage(); if (page == nullptr || page->nav.depth > 0) { return false; } for (M_PAGE_NUMBER i = m_Priv.active_page + direction; i >= PAGE_1 && i < PAGE_COUNT; i += direction) { if (m_Priv.pages[i].available) { return true; } } return false; } static void M_SyncArrowsVisibility(void) { Overlay_ShowArrow(UI_OVERLAY_ARROW_BCL, M_IsArrowVisible(-1)); Overlay_ShowArrow(UI_OVERLAY_ARROW_BCR, M_IsArrowVisible(1)); } static void M_ChangePageTextContent(const char *const content) { InvRing_RemoveAllText(); Overlay_SetBottomText((OVERLAY_TEXT) { .kind = UI_OVERLAY_TEXT_LITERAL, .literal = content, .fmt_gs_key = GS_ID("general/inventory_ring/object_name_fmt"), }); } static M_PAGE_NUMBER M_GetCurrentPage(const INVENTORY_ITEM *const inv_item) { const int32_t frame = inv_item->goal_frame - inv_item->open_frame; return frame % 5 == 0 ? frame / 5 : PAGE_UNDETERMINED; } static bool M_IsFlipping(const INVENTORY_ITEM *const inv_item) { return M_GetCurrentPage(inv_item) == PAGE_UNDETERMINED; } static void M_FlipLeft(INVENTORY_ITEM *const inv_item) { M_RemoveAllText(); inv_item->anim_direction = -1; inv_item->goal_frame = inv_item->open_frame + 5 * m_Priv.active_page; Sound_Effect(SFX_MENU_PASSPORT, nullptr, SPM_ALWAYS); } static void M_FlipRight(INVENTORY_ITEM *const inv_item) { M_RemoveAllText(); inv_item->anim_direction = 1; inv_item->goal_frame = inv_item->open_frame + 5 * m_Priv.active_page; Sound_Effect(SFX_MENU_PASSPORT, nullptr, SPM_ALWAYS); } static void M_Close(INVENTORY_ITEM *const inv_item) { m_Priv.active_page = PAGE_UNDETERMINED; M_RemoveAllText(); M_FreeAllDialogs(); if (m_Priv.current_page == PAGE_3) { inv_item->anim_direction = 1; inv_item->goal_frame = inv_item->frames_total - 1; } else { inv_item->anim_direction = -1; inv_item->goal_frame = 0; } } static void M_SoftClose(INVENTORY_ITEM *const inv_item) { if (g_Inv_Mode == INV_DEATH_MODE) { if (!M_IMMEDIATE && m_Priv.mode != M_MODE_BROWSE) { m_Priv.mode = M_MODE_BROWSE; } g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; return; } if (m_Priv.mode == M_MODE_BROWSE || M_IMMEDIATE || (g_Inv_Mode != INV_GAME_MODE && g_Inv_Mode != INV_TITLE_MODE)) { M_Close(inv_item); } else { m_Priv.mode = M_MODE_BROWSE; g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; } } static void M_NavigateInto(const M_PAGE_ROLE role, const int32_t selection) { M_PAGE *const page = M_GetActivePage(); if (page->nav.depth + 1 < (int32_t)(sizeof page->nav.stack / sizeof page->nav.stack[0])) { page->nav.current->selection = selection; page->nav.depth++; page->nav.current = &page->nav.stack[page->nav.depth]; page->nav.current->role = role; page->nav.current->selection = -1; } g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; } static void M_NavigateOut(INVENTORY_ITEM *const inv_item) { M_PAGE *const page = M_TryGetActivePage(); if (page == nullptr) { return; } m_Priv.error_msg = nullptr; M_FreeDialogs(page->nav.current); if (page->nav.depth > 0) { page->nav.depth--; page->nav.current = &page->nav.stack[page->nav.depth]; g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; } else { M_SoftClose(inv_item); } } static void M_Confirm(const PASSPORT_ACTION role, const int32_t argument) { g_Passport.select_action = role; g_Passport.select_level = argument; } static void M_ConfirmSaveSlot( const PASSPORT_ACTION role, const SAVEGAME_SLOT_REF slot) { g_Passport.select_action = role; g_Passport.select_save_slot = slot; } static void M_SetPage( const M_PAGE_NUMBER page, const M_PAGE_ROLE role, const bool available) { m_Priv.pages[page].role = role; m_Priv.pages[page].available = available; } static void M_DeterminePages(void) { const bool can_restart = Savegame_RestartAvailable(Savegame_GetBoundSlot()); const bool saving_enabled = Savegame_GetSlotCount(SAVEGAME_SLOT_POOL_NORMAL) > 0 && !g_Config.flow.load_save_disabled; const bool has_saves = Savegame_GetTotalCount() > 0 && saving_enabled; for (M_PAGE_NUMBER i = PAGE_1; i < PAGE_COUNT; i++) { m_Priv.pages[i].available = false; } switch (g_Inv_Mode) { case INV_TITLE_MODE: m_Priv.mode = M_IMMEDIATE ? M_MODE_PICK_OPTION : M_MODE_BROWSE; M_SetPage(PAGE_1, M_ROLE_LOAD_GAME, has_saves); M_SetPage(PAGE_2, M_ROLE_NEW_GAME, true); M_SetPage(PAGE_3, M_ROLE_EXIT_GAME, true); break; case INV_GAME_MODE: m_Priv.mode = M_IMMEDIATE ? M_MODE_PICK_OPTION : M_MODE_BROWSE; if (!saving_enabled) { M_SetPage(PAGE_2, M_ROLE_RESTART_LEVEL, can_restart); } else { M_SetPage(PAGE_1, M_ROLE_LOAD_GAME, has_saves); M_SetPage(PAGE_2, M_ROLE_SAVE_GAME, true); } M_SetPage(PAGE_3, M_ROLE_EXIT_TO_TITLE, true); break; case INV_LOAD_MODE: m_Priv.mode = M_MODE_PICK_OPTION; if (!saving_enabled) { M_SetPage(PAGE_2, M_ROLE_RESTART_LEVEL, can_restart); } else if (has_saves) { M_SetPage(PAGE_1, M_ROLE_LOAD_GAME, true); } else { M_SetPage(PAGE_2, M_ROLE_SAVE_GAME, true); } break; case INV_SAVE_MODE: case INV_SAVE_CRYSTAL_MODE: m_Priv.mode = M_MODE_PICK_OPTION; if (!saving_enabled) { M_SetPage(PAGE_2, M_ROLE_RESTART_LEVEL, can_restart); } else { M_SetPage(PAGE_2, M_ROLE_SAVE_GAME, true); } break; case INV_DEATH_MODE: m_Priv.mode = M_IMMEDIATE ? M_MODE_PICK_OPTION : M_MODE_BROWSE; M_SetPage(PAGE_1, M_ROLE_LOAD_GAME, has_saves); M_SetPage(PAGE_2, M_ROLE_RESTART_LEVEL, can_restart); M_SetPage(PAGE_3, M_ROLE_EXIT_TO_TITLE, true); break; case INV_KEYS_MODE: case INV_GLOBE_SELECT_MODE: ASSERT_FAIL(); } // Disable saves in gym and save crystals mode. // Offer New Game or Restart instead. for (M_PAGE_NUMBER i = PAGE_1; i < PAGE_COUNT; i++) { if (m_Priv.pages[i].role != M_ROLE_SAVE_GAME) { continue; } if (Game_IsInGym()) { m_Priv.pages[i].role = M_ROLE_NEW_GAME; } else if ( g_Config.gameplay.enable_save_crystals && g_Inv_Mode != INV_SAVE_CRYSTAL_MODE) { if (can_restart) { m_Priv.pages[i].role = M_ROLE_RESTART_LEVEL; } else { m_Priv.pages[i].available = false; } } } // If play any level is enabled, replace New Game with Play Any Level. if (g_Config.flow.play_any_level) { for (M_PAGE_NUMBER i = PAGE_1; i < PAGE_COUNT; i++) { if (m_Priv.pages[i].role == M_ROLE_NEW_GAME) { m_Priv.pages[i].role = M_ROLE_PLAY_ANY_LEVEL_SELECT_LEVEL; } } } // Select first available page m_Priv.active_page = PAGE_UNDETERMINED; for (M_PAGE_NUMBER i = PAGE_1; i < PAGE_COUNT; i++) { if (m_Priv.pages[i].available) { m_Priv.active_page = i; break; } } // Guard: if no pages are available, force-add exit game or exit to title if (m_Priv.active_page == PAGE_UNDETERMINED) { M_SetPage( PAGE_3, g_Inv_Mode == INV_TITLE_MODE ? M_ROLE_EXIT_GAME : M_ROLE_EXIT_TO_TITLE, true); m_Priv.active_page = PAGE_3; } // reset hierarchical nav stack now that top-level pages are set ASSERT(m_Priv.active_page != PAGE_UNDETERMINED); for (M_PAGE_NUMBER i = PAGE_1; i < PAGE_COUNT; i++) { M_ResetNavStack(&m_Priv.pages[i]); } for (M_PAGE_NUMBER i = PAGE_1; i < PAGE_COUNT; i++) { LOG_DEBUG( "page %d: role=%d available=%d", i, m_Priv.pages[i].role, m_Priv.pages[i].available); } } static bool M_ChooseSaveSlot( INVENTORY_ITEM *const inv_item, const UI_SAVE_SLOT_DIALOG_TYPE dialog_type, SAVEGAME_SLOT_REF *const selected_slot) { *selected_slot = Savegame_InvalidSlot(); M_PAGE *const page = M_GetActivePage(); M_NAV_FRAME *const frame = page->nav.current; if (frame->ui.save_slot == nullptr) { const int32_t selection = page->nav.stack[page->nav.depth].selection; SAVEGAME_SLOT_REF initial_slot = selection != -1 ? Savegame_SlotFromParam(selection) : Savegame_InvalidSlot(); if (!Savegame_IsValidSlotRef(initial_slot)) { initial_slot = Savegame_GetMostRecentlyUsedSlot(); } if (!Savegame_IsValidSlotRef(initial_slot)) { initial_slot = Savegame_GetMostRecentlyCreatedSlot(); } if (!Savegame_IsValidSlotRef(initial_slot)) { initial_slot = Savegame_NormalSlot(0); } page->nav.current->ui.save_slot = UI_SaveSlotDialog_Init(dialog_type, initial_slot); } const UI_SAVE_SLOT_DIALOG_CHOICE choice = UI_SaveSlotDialog_Control(frame->ui.save_slot); switch (choice.action) { case UI_SAVE_SLOT_DIALOG_NO_CHOICE: if (M_IMMEDIATE) { // Make sure it's not possible to confirm empty slots g_Input.menu_confirm = false; g_InputDB.menu_confirm = false; } else { g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; } return false; case UI_SAVE_SLOT_DIALOG_CANCEL: M_NavigateOut(inv_item); return true; case UI_SAVE_SLOT_DIALOG_CONFIRM: *selected_slot = choice.slot; return true; case UI_SAVE_SLOT_DIALOG_DELETE_FAILED: m_Priv.error_msg = GS_ID("general/passport/delete_save_failed"); return false; } return false; } static bool M_CheckConfirm(const PASSPORT_ACTION action) { if (g_InputDB.menu_confirm) { M_Confirm(action, -1); return true; } return false; } static bool M_HandleLoadGame(INVENTORY_ITEM *const inv_item) { SAVEGAME_SLOT_REF selected_slot = Savegame_InvalidSlot(); const bool result = M_ChooseSaveSlot( inv_item, UI_SAVE_SLOT_DIALOG_LOAD_GAME, &selected_slot); if (Savegame_IsValidSlotRef(selected_slot)) { M_ConfirmSaveSlot(PASSPORT_ACTION_LOAD_GAME, selected_slot); } return result; } static bool M_HandleSaveGame(INVENTORY_ITEM *const inv_item) { SAVEGAME_SLOT_REF selected_slot = Savegame_InvalidSlot(); const bool result = M_ChooseSaveSlot( inv_item, UI_SAVE_SLOT_DIALOG_SAVE_GAME, &selected_slot); if (Savegame_IsValidSlotRef(selected_slot)) { M_ConfirmSaveSlot(PASSPORT_ACTION_SAVE_GAME, selected_slot); } return result; } static bool M_HandleNewGame(INVENTORY_ITEM *const inv_item) { M_PAGE *const page = M_GetActivePage(); M_NAV_FRAME *const frame = page->nav.current; // If no options – start the game already if (!g_Config.gameplay.enable_game_modes && !g_Config.profile.new_game_plus_unlock && !g_Config.gameplay.enable_play_previous_levels && !UI_NewGame_HasModChoices()) { // But only if in title mode if (g_InputDB.menu_confirm || (!M_IMMEDIATE && g_Inv_Mode == INV_TITLE_MODE)) { M_Confirm(PASSPORT_ACTION_NEW_GAME, GF_GetFirstLevel()->num); g_InputDB.menu_confirm = true; M_Close(inv_item); } return false; } if (frame->ui.new_game == nullptr) { frame->ui.new_game = UI_NewGame_Init(true); } const int32_t choice = UI_NewGame_Control(frame->ui.new_game); if (choice == UI_REQUESTER_NO_CHOICE) { if (!M_IMMEDIATE) { g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; } return false; } else { switch (choice) { case UI_REQUESTER_CANCEL: M_NavigateOut(inv_item); return true; case UI_NEW_GAME_CHOICE_NG: // Handle the scenario where enable_game_modes is off, and // enable_play_previous_levels is on. In this scenario the dialog // adds a "New Game" row just to let the player start the game. It // shouldn't touch the NG+ flag. if (g_Config.gameplay.enable_game_modes) { Game_SetBonusFlag(GBF_NONE); } M_Confirm(PASSPORT_ACTION_NEW_GAME, GF_GetFirstLevel()->num); return true; case UI_NEW_GAME_CHOICE_NGPLUS: Game_SetBonusFlag(GBF_NGPLUS); M_Confirm(PASSPORT_ACTION_NEW_GAME, GF_GetFirstLevel()->num); return true; case UI_NEW_GAME_CHOICE_JP_NG: Game_SetBonusFlag(GBF_JAPANESE); M_Confirm(PASSPORT_ACTION_NEW_GAME, GF_GetFirstLevel()->num); return true; case UI_NEW_GAME_CHOICE_JP_NGPLUS: Game_SetBonusFlag(GBF_JAPANESE | GBF_NGPLUS); M_Confirm(PASSPORT_ACTION_NEW_GAME, GF_GetFirstLevel()->num); return true; case UI_NEW_GAME_CHOICE_SWITCH_MOD: M_NavigateInto(M_ROLE_SWITCH_MOD, -1); return true; case UI_NEW_GAME_CHOICE_PLAY_PREV_LEVELS: M_NavigateInto(M_ROLE_PLAY_PREV_LEVEL_SELECT_SLOT, -1); return true; case UI_NEW_GAME_CHOICE_STORY_SO_FAR: M_NavigateInto(M_ROLE_STORY_SO_FAR, -1); return true; } } return false; } static bool M_HandlePlayAnyLevel(INVENTORY_ITEM *const inv_item) { M_PAGE *const page = M_GetActivePage(); M_NAV_FRAME *const frame = page->nav.current; if (frame->ui.play_any_level == nullptr) { frame->ui.play_any_level = UI_PlayAnyLevelDialog_Init(); } const int32_t choice = UI_PlayAnyLevelDialog_Control(frame->ui.play_any_level); if (choice == UI_REQUESTER_NO_CHOICE) { if (!M_IMMEDIATE) { g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; } return false; } else if (choice == UI_REQUESTER_CANCEL) { M_NavigateOut(inv_item); return true; } else if ( g_Config.gameplay.enable_game_modes || g_Config.profile.new_game_plus_unlock) { M_NavigateInto(M_ROLE_PLAY_ANY_LEVEL_SELECT_MODE, choice); return true; } else { Savegame_UnbindSlot(); M_Confirm(PASSPORT_ACTION_SELECT_LEVEL, choice); return true; } } static bool M_HandlePlayAnyLevelSelectMode(INVENTORY_ITEM *const inv_item) { M_PAGE *const page = M_GetActivePage(); M_NAV_FRAME *const frame = page->nav.current; ASSERT(m_Priv.mode == M_MODE_PICK_OPTION); if (frame->ui.new_game == nullptr) { frame->ui.new_game = UI_NewGame_Init(false); } const int32_t choice = UI_NewGame_Control(frame->ui.new_game); if (choice == UI_REQUESTER_NO_CHOICE) { return false; } else { const int32_t level_num = page->nav.stack[page->nav.depth - 1].selection; switch (choice) { case UI_REQUESTER_CANCEL: M_NavigateOut(inv_item); return true; case UI_NEW_GAME_CHOICE_NG: Game_SetBonusFlag(GBF_NONE); Savegame_UnbindSlot(); M_Confirm(PASSPORT_ACTION_SELECT_LEVEL, level_num); return true; case UI_NEW_GAME_CHOICE_NGPLUS: Game_SetBonusFlag(GBF_NGPLUS); Savegame_UnbindSlot(); M_Confirm(PASSPORT_ACTION_SELECT_LEVEL, level_num); return true; case UI_NEW_GAME_CHOICE_JP_NG: Game_SetBonusFlag(GBF_JAPANESE); Savegame_UnbindSlot(); M_Confirm(PASSPORT_ACTION_SELECT_LEVEL, level_num); return true; case UI_NEW_GAME_CHOICE_JP_NGPLUS: Game_SetBonusFlag(GBF_JAPANESE | GBF_NGPLUS); Savegame_UnbindSlot(); M_Confirm(PASSPORT_ACTION_SELECT_LEVEL, level_num); return true; default: ASSERT_FAIL(); } } return false; } static bool M_HandleSwitchMod(INVENTORY_ITEM *const inv_item) { M_PAGE *const page = M_GetActivePage(); M_NAV_FRAME *const frame = page->nav.current; if (frame->ui.switch_mod == nullptr) { frame->ui.switch_mod = UI_SwitchModDialog_Init(); } const int32_t choice = UI_SwitchModDialog_Control(frame->ui.switch_mod); if (choice == UI_REQUESTER_NO_CHOICE) { return false; } if (choice == UI_REQUESTER_CANCEL) { M_NavigateOut(inv_item); return true; } const char *const mod_name = UI_SwitchModDialog_GetSelectedMod(frame->ui.switch_mod, choice); Shell_RequestModSwitch(mod_name); M_Confirm(PASSPORT_ACTION_SWITCH_MOD, -1); g_InputDB.menu_confirm = true; M_Close(inv_item); return true; } static bool M_HandlePlayPrevLevelSelectSlot(INVENTORY_ITEM *const inv_item) { SAVEGAME_SLOT_REF selected_slot = Savegame_InvalidSlot(); const bool result = M_ChooseSaveSlot(inv_item, UI_SAVE_SLOT_DIALOG_GENERIC, &selected_slot); if (Savegame_IsValidSlotRef(selected_slot)) { M_NavigateInto( M_ROLE_PLAY_PREV_LEVEL_SELECT_LEVEL, Savegame_SlotToParam(selected_slot)); } return result; } static bool M_HandlePlayPrevLevelSelectLevel(INVENTORY_ITEM *const inv_item) { M_PAGE *const page = M_GetActivePage(); const SAVEGAME_SLOT_REF slot = Savegame_SlotFromParam(page->nav.stack[page->nav.depth - 1].selection); if (!Savegame_IsValidSlotRef(slot)) { M_NavigateOut(inv_item); return true; } const SAVEGAME_INFO *const info = Savegame_GetSavegameInfo(slot); if (info == nullptr) { M_NavigateOut(inv_item); return true; } if (!info->features.select_level) { m_Priv.error_msg = GS_ID("general/passport/save_slot_unsupported"); if (g_InputDB.menu_back || g_InputDB.menu_confirm) { M_NavigateOut(inv_item); return true; } return false; } M_NAV_FRAME *const frame = page->nav.current; if (frame->ui.select_level == nullptr) { frame->ui.select_level = UI_SelectLevelDialog_Init(slot); } const int32_t choice = UI_SelectLevelDialog_Control(frame->ui.select_level); if (choice == UI_REQUESTER_NO_CHOICE) { if (!M_IMMEDIATE) { g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; } return false; } else if (choice == UI_REQUESTER_CANCEL) { M_NavigateOut(inv_item); return true; } else { Savegame_BindSlot(slot); M_Confirm(PASSPORT_ACTION_SELECT_LEVEL, choice); return true; } return false; } static bool M_HandleStorySoFar(INVENTORY_ITEM *const inv_item) { SAVEGAME_SLOT_REF selected_slot = Savegame_InvalidSlot(); const bool result = M_ChooseSaveSlot(inv_item, UI_SAVE_SLOT_DIALOG_GENERIC, &selected_slot); if (Savegame_IsValidSlotRef(selected_slot)) { M_NavigateInto( M_ROLE_STORY_SO_FAR_CONFIRM, Savegame_SlotToParam(selected_slot)); } return result; } static bool M_HandleStorySoFarConfirm(INVENTORY_ITEM *const inv_item) { M_PAGE *const page = M_GetActivePage(); const SAVEGAME_SLOT_REF slot = Savegame_SlotFromParam(page->nav.stack[page->nav.depth - 1].selection); if (GF_HasAvailableStory(slot)) { M_ConfirmSaveSlot(PASSPORT_ACTION_STORY_SO_FAR, slot); g_InputDB.menu_confirm = true; M_Close(inv_item); return true; } else if (g_InputDB.menu_back || g_InputDB.menu_confirm) { M_NavigateOut(inv_item); return true; } else { m_Priv.error_msg = GS_ID("general/passport/save_slot_unsupported"); return false; } return false; } static bool M_HandleRestartLevel(INVENTORY_ITEM *const inv_item) { return M_CheckConfirm(PASSPORT_ACTION_RESTART); } static bool M_HandleExitGame(INVENTORY_ITEM *const inv_item) { return M_CheckConfirm(PASSPORT_ACTION_EXIT_GAME); } static bool M_HandleExitToTitle(INVENTORY_ITEM *const inv_item) { return M_CheckConfirm(PASSPORT_ACTION_EXIT_TO_TITLE); } static bool M_ShowPage(INVENTORY_ITEM *const inv_item) { static M_PAGE_HANDLER m_PageHandlers[] = { [M_ROLE_LOAD_GAME] = { .title = GS_ID("general/passport/load_game"), .func = M_HandleLoadGame, .flat = false, }, [M_ROLE_SAVE_GAME] = { .title = GS_ID("general/passport/save_game"), .func = M_HandleSaveGame, .flat = false, }, [M_ROLE_NEW_GAME] = { .title = GS_ID("general/passport/new_game"), .func = M_HandleNewGame, .flat = false, }, [M_ROLE_PLAY_ANY_LEVEL_SELECT_LEVEL] = { .title = GS_ID("general/passport/select_level"), .func = M_HandlePlayAnyLevel, .flat = false, }, [M_ROLE_PLAY_ANY_LEVEL_SELECT_MODE] = { .title = GS_ID("general/passport/select_level"), .func = M_HandlePlayAnyLevelSelectMode, .flat = false, }, [M_ROLE_PLAY_PREV_LEVEL_SELECT_SLOT] = { .title = GS_ID("general/passport/play_previous_levels"), .func = M_HandlePlayPrevLevelSelectSlot, .flat = false, }, [M_ROLE_PLAY_PREV_LEVEL_SELECT_LEVEL] = { .title = GS_ID("general/passport/play_previous_levels"), .func = M_HandlePlayPrevLevelSelectLevel, .flat = false, }, [M_ROLE_SWITCH_MOD] = { .title = GS_ID("general/passport/switch_mod"), .func = M_HandleSwitchMod, .flat = false, }, [M_ROLE_RESTART_LEVEL] = { .title = GS_ID("general/passport/restart_level"), .func = M_HandleRestartLevel, .flat = true, }, [M_ROLE_EXIT_GAME] = { .title = GS_ID("general/passport/exit_game"), .func = M_HandleExitGame, .flat = true, }, [M_ROLE_EXIT_TO_TITLE] = { .title = GS_ID("general/passport/exit_to_title"), .func = M_HandleExitToTitle, .flat = true, }, [M_ROLE_STORY_SO_FAR] = { .title = GS_ID("general/passport/story_so_far"), .func = M_HandleStorySoFar, .flat = false, }, [M_ROLE_STORY_SO_FAR_CONFIRM] = { .title = GS_ID("general/passport/story_so_far"), .func = M_HandleStorySoFarConfirm, .flat = false, }, }; M_PAGE *const page = M_TryGetActivePage(); if (page == nullptr) { return false; } const M_PAGE_HANDLER *const handler = &m_PageHandlers[page->nav.current->role]; M_ChangePageTextContent(GameString_Get(handler->title)); if (m_Priv.mode == M_MODE_BROWSE && !handler->flat) { if (g_InputDB.menu_confirm) { g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; m_Priv.mode = M_MODE_PICK_OPTION; return true; } return false; } return handler->func(inv_item); } static void M_HandleFlipInputs(void) { bool flipped = false; if (g_InputDB.menu_left && M_IsArrowVisible(-1)) { for (M_PAGE_NUMBER page = m_Priv.active_page - 1; page >= PAGE_1; page--) { if (m_Priv.pages[page].available) { m_Priv.active_page = page; flipped = true; break; } } } else if (g_InputDB.menu_right && M_IsArrowVisible(1)) { for (M_PAGE_NUMBER page = m_Priv.active_page + 1; page < PAGE_COUNT; page++) { if (m_Priv.pages[page].available) { m_Priv.active_page = page; flipped = true; break; } } } if (flipped) { M_ResetNavStack(M_GetActivePage()); } } void Option_Passport_Control(INVENTORY_ITEM *const inv_item, const bool is_busy) { if (m_Priv.active_page == PAGE_UNDETERMINED) { M_DeterminePages(); } if (is_busy) { if (g_Config.input.enable_responsive_passport) { M_HandleFlipInputs(); } return; } InvRing_RemoveAllText(); if (M_IsFlipping(inv_item)) { return; } m_Priv.current_page = M_GetCurrentPage(inv_item); if (m_Priv.current_page < m_Priv.active_page) { M_FlipRight(inv_item); g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; } else if (m_Priv.current_page > m_Priv.active_page) { M_FlipLeft(inv_item); g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; } else { if (M_ShowPage(inv_item)) { // In case of state-changes, apply changes immediately M_ShowPage(inv_item); } M_SyncArrowsVisibility(); if (g_InputDB.menu_confirm) { M_Close(inv_item); } else if (g_InputDB.menu_back) { if (g_Inv_Mode == INV_DEATH_MODE) { g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; } else { M_NavigateOut(inv_item); } } else { M_HandleFlipInputs(); } } } void Option_Passport_Draw(INVENTORY_ITEM *const inv_item) { if (m_Priv.mode == M_MODE_BROWSE || M_IsFlipping(inv_item) || m_Priv.active_page != m_Priv.current_page) { return; } M_PAGE *const page = M_TryGetActivePage(); if (page == nullptr) { return; } M_NAV_FRAME *const frame = page->nav.current; if (frame->ui.new_game != nullptr) { UI_NewGame(frame->ui.new_game); } if (frame->ui.play_any_level != nullptr) { UI_PlayAnyLevelDialog(frame->ui.play_any_level); } if (frame->ui.select_level != nullptr) { UI_SelectLevelDialog(frame->ui.select_level); } if (frame->ui.switch_mod != nullptr) { UI_SwitchModDialog(frame->ui.switch_mod); } if (frame->ui.save_slot != nullptr) { UI_SaveSlotDialog(frame->ui.save_slot); } if (m_Priv.error_msg != nullptr) { UI_BeginModal(0.5f, 0.67f); UI_BeginFrame(UI_FRAME_DIALOG_BACKGROUND); UI_BeginPad(8.0f, 8.0f); UI_Label(GameString_Get(m_Priv.error_msg)); UI_EndPad(); UI_EndFrame(); UI_EndModal(); } } void Option_Passport_Close(void) { M_RemoveAllText(); M_FreeAllDialogs(); m_Priv.active_page = PAGE_UNDETERMINED; } ================================================ FILE: src/trx/game/option/passport.h ================================================ #pragma once #include #include typedef enum { PASSPORT_ACTION_LOAD_GAME, PASSPORT_ACTION_SELECT_LEVEL, PASSPORT_ACTION_GLOBE_SELECT, PASSPORT_ACTION_STORY_SO_FAR, PASSPORT_ACTION_SAVE_GAME, PASSPORT_ACTION_NEW_GAME, PASSPORT_ACTION_SWITCH_MOD, PASSPORT_ACTION_RESTART, PASSPORT_ACTION_EXIT_TO_TITLE, PASSPORT_ACTION_EXIT_GAME, } PASSPORT_ACTION; typedef struct { PASSPORT_ACTION select_action; union { int32_t select_level; SAVEGAME_SLOT_REF select_save_slot; }; bool ask_for_save; } PASSPORT; extern PASSPORT g_Passport; // TODO: meh void Option_Passport_Control(INVENTORY_ITEM *inv_item, bool is_busy); void Option_Passport_Draw(INVENTORY_ITEM *inv_item); void Option_Passport_Close(void); ================================================ FILE: src/trx/game/option/sound.c ================================================ #include #include #include #include typedef struct { UI_SETTINGS_DIALOG_STATE *ui_state; } M_PRIV; static M_PRIV m_Priv = {}; static void M_Init(M_PRIV *const p) { if (p->ui_state == nullptr) { p->ui_state = UI_SoundSettings_Init(); } } static void M_Shutdown(M_PRIV *const p) { if (p->ui_state != nullptr) { UI_SoundSettings_Free(p->ui_state); p->ui_state = nullptr; } } void Option_Sound_Control(INVENTORY_ITEM *const inv_item, const bool is_busy) { M_PRIV *const p = &m_Priv; if (is_busy) { return; } if (p->ui_state == nullptr) { M_Init(p); } if (!UI_SoundSettings_Control(p->ui_state)) { g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; } } void Option_Sound_Draw(INVENTORY_ITEM *const inv_item) { M_PRIV *const p = &m_Priv; if (p->ui_state != nullptr) { UI_SoundSettings(p->ui_state); } } void Option_Sound_Close(void) { } void Option_Sound_Shutdown(void) { M_PRIV *const p = &m_Priv; M_Shutdown(p); } ================================================ FILE: src/trx/game/option/sound.h ================================================ #pragma once #include void Option_Sound_Control(INVENTORY_ITEM *inv_item, bool is_busy); void Option_Sound_Draw(INVENTORY_ITEM *inv_item); void Option_Sound_Close(void); void Option_Sound_Shutdown(void); ================================================ FILE: src/trx/game/option/stats.c ================================================ #include #include #include #include #include #include #include #include #include typedef struct { bool is_ready; UI_STATS_DIALOG_STATE *ui_state; } M_PRIV; static M_PRIV m_Priv = {}; static int16_t m_CompassNeedle = 0; static int16_t m_CompassSpeed = 0; static void M_Init(M_PRIV *const p, INVENTORY_ITEM *const inv_item) { if (inv_item->object_id == O_COMPASS_OPTION && !g_Config.gameplay.enable_compass_stats) { return; } p->is_ready = true; p->ui_state = UI_StatsDialog_Init((UI_STATS_DIALOG_ARGS) { .mode = Game_IsInGym() && Gym_TrackManager_HasStats(GYM_TRACK_ASSAULT) ? UI_STATS_DIALOG_MODE_ASSAULT_COURSE : UI_STATS_DIALOG_MODE_LEVEL, .style = g_Config.ui.stats.style == STATS_STYLE_BARE ? UI_STATS_DIALOG_STYLE_BARE : UI_STATS_DIALOG_STYLE_BORDERED, .level_num = Game_GetCurrentLevel()->num, }); } static void M_Close(M_PRIV *const p) { if (p->is_ready) { p->is_ready = false; UI_StatsDialog_Free(p->ui_state); p->ui_state = nullptr; } } void Option_Stats_Control(INVENTORY_ITEM *const inv_item, const bool is_busy) { M_PRIV *const p = &m_Priv; if (is_busy) { return; } if (!p->is_ready) { M_Init(p, inv_item); } if (p->is_ready) { UI_StatsDialog_Control(p->ui_state); } if (g_InputDB.menu_confirm || g_InputDB.menu_back) { M_Close(p); inv_item->anim_direction = 1; inv_item->goal_frame = inv_item->frames_total - 1; if (inv_item->object_id == O_STOPWATCH_OPTION) { Sound_StopEffect(SFX_MENU_STOPWATCH); } } else if (inv_item->object_id == O_STOPWATCH_OPTION) { Sound_Effect(SFX_MENU_STOPWATCH, 0, SPM_ALWAYS); } } void Option_Stats_Draw(void) { M_PRIV *const p = &m_Priv; if (p->is_ready) { UI_StatsDialog(p->ui_state); } } void Option_Stats_Close(void) { M_PRIV *const p = &m_Priv; M_Close(p); } void Option_Stats_UpdateCompassNeedle(const INVENTORY_ITEM *const inv_item) { const ITEM *const lara_item = Lara_GetItem(); if (lara_item == nullptr) { return; } int16_t delta = -inv_item->y_rot - lara_item->rot.y - m_CompassNeedle; m_CompassSpeed = m_CompassSpeed * 19 / 20 + delta / 50; m_CompassNeedle += m_CompassSpeed; } int16_t Option_Stats_GetCompassNeedleAngle(void) { return m_CompassNeedle; } ================================================ FILE: src/trx/game/option/stats.h ================================================ #pragma once #include void Option_Stats_Control(INVENTORY_ITEM *inv_item, bool is_busy); void Option_Stats_Draw(void); void Option_Stats_Close(void); void Option_Stats_UpdateCompassNeedle(const INVENTORY_ITEM *inv_item); int16_t Option_Stats_GetCompassNeedleAngle(void); ================================================ FILE: src/trx/game/option.h ================================================ #pragma once #include ================================================ FILE: src/trx/game/output/bind.c ================================================ #include #include #include #include #include static OUTPUT_ITEM_BIND m_ItemBindings[MAX_ITEMS] = {}; static OUTPUT_ROOM_BIND m_RoomBindings[MAX_ROOMS] = {}; void Output_Bind_ResetItems(void) { memset(m_ItemBindings, 0, sizeof(m_ItemBindings)); } OUTPUT_ITEM_BIND *Output_Bind_GetItem(const ITEM *const item) { return &m_ItemBindings[Item_GetIndex(item)]; } void Output_Bind_ResetRooms(void) { for (int32_t i = 0; i < MAX_ROOMS; i++) { m_RoomBindings[i].active = false; m_RoomBindings[i].drawn = false; } } OUTPUT_ROOM_BIND *Output_Bind_GetRoom(const ROOM *const room) { return &m_RoomBindings[Room_GetNumber(room)]; } ================================================ FILE: src/trx/game/output/bind.h ================================================ #pragma once #include #include typedef struct { bool drawn; bool shadow_drawn; } OUTPUT_ITEM_BIND; typedef struct { bool active; bool drawn; int16_t bound_left; int16_t bound_right; int16_t bound_top; int16_t bound_bottom; int16_t test_left; int16_t test_right; int16_t test_top; int16_t test_bottom; } OUTPUT_ROOM_BIND; void Output_Bind_ResetItems(void); OUTPUT_ITEM_BIND *Output_Bind_GetItem(const ITEM *item); void Output_Bind_ResetRooms(void); OUTPUT_ROOM_BIND *Output_Bind_GetRoom(const ROOM *room); ================================================ FILE: src/trx/game/output/common.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static MESH_BATCHER *m_Batcher = nullptr; static OUTPUT_UNIFORMS *m_Uniforms = nullptr; static OUTPUT_MESH_SHADER *m_ShaderWorld = nullptr; static OUTPUT_UI_SHADER *m_ShaderUI = nullptr; void Output_Init(void) { SceneCompositor_Init(); Output_Textures_Init(); m_Uniforms = Output_Uniforms_Create(); m_ShaderWorld = Output_MeshShader_Create(); m_ShaderUI = Output_UIShader_Create(); m_Batcher = MeshBatcher_Create(); SceneCompositor_AddSource(MeshBatcher_AsSource(m_Batcher)); OutputSource_Rooms_Init(m_Batcher); OutputSource_RoomsDebug_Init(); OutputSource_Objects_Init(m_Batcher); OutputSource_Sprites_Init(m_Batcher); OutputSource_Lightnings_Init(); OutputSource_PolyFX_Init(); OutputSource_Shadows_Init(m_Batcher); OutputSource_Misc_Init(); OutputSource_Overlay_Init(); Output_Lights_Init(); OutputSource_UI_Init(); Output_ApplyRenderSettings(); } void Output_Shutdown(void) { SceneCompositor_Shutdown(); OutputSource_Rooms_Shutdown(); OutputSource_RoomsDebug_Shutdown(); OutputSource_Objects_Shutdown(); OutputSource_Sprites_Shutdown(); OutputSource_Lightnings_Shutdown(); OutputSource_PolyFX_Shutdown(); OutputSource_Shadows_Shutdown(); OutputSource_Misc_Shutdown(); OutputSource_Overlay_Shutdown(); OutputSource_UI_Shutdown(); if (m_ShaderWorld != nullptr) { Output_MeshShader_Free(m_ShaderWorld); m_ShaderWorld = nullptr; } if (m_ShaderUI != nullptr) { Output_UIShader_Free(m_ShaderUI); m_ShaderUI = nullptr; } if (m_Uniforms != nullptr) { Output_Uniforms_Free(m_Uniforms); m_Uniforms = nullptr; } if (m_Batcher != nullptr) { MeshBatcher_Destroy(m_Batcher); m_Batcher = nullptr; } Output_Textures_Shutdown(); Output_Lights_Shutdown(); } bool Output_IsHeadless(void) { return Shell_GetArgs()->headless; } const OUTPUT_UNIFORMS *Output_GetUniforms(void) { return m_Uniforms; } OUTPUT_MESH_SHADER *Output_GetMeshShader(void) { return m_ShaderWorld; } OUTPUT_UI_SHADER *Output_GetUIShader(void) { return m_ShaderUI; } void Output_BeginScene(void) { Output_ApplyFOV(); TRX_GL_Context_Clear(); TRX_GL_Track_Reset(); TRX_GL_Context_SetWireframeMode(g_Config.rendering.enable_wireframe); SceneCompositor_BeginScene(); } void Output_EndScene(void) { SceneCompositor_EndScene(); } void Output_Flush(void) { SceneCompositor_Flush(); } void Output_FlipScreen(void) { TRX_GL_Context_SwapBuffers(); } void Output_SwitchViewport(const VIEWPORT_SPACE space) { if (space == VIEWPORT_GAME) { TRX_GL_Renderer_BindGeometryFbo(); } else if (space == VIEWPORT_UI) { TRX_GL_Renderer_BindUiFbo(); } TRX_GL_Context_SwitchToViewport(space); TRX_GL_Context_Clear(); glClear(GL_DEPTH_BUFFER_BIT); } void Output_ApplyRenderSettings(void) { Output_Textures_ApplyRenderSettings(); Output_ApplyLevelSettings(); if (m_ShaderWorld == nullptr) { return; } TRX_GL_Context_SetVSync(g_Config.rendering.enable_vsync); TRX_GL_Context_SetDisplayFilter(g_Config.rendering.upscaling_filter); TRX_GL_Context_SetWireframeMode(g_Config.rendering.enable_wireframe); TRX_GL_Context_SetLineWidth(g_Config.rendering.wireframe_width); } void Output_ApplyLevelSettings(void) { Output_SetWaterColor(Level_GetWaterColor()); Output_SetFogColor(Level_GetFogColor()); Output_SetFogStart(Level_GetFogStart() * WALL_L); Output_SetFogEnd(Level_GetFogEnd() * WALL_L); } void Output_DispatchLevelLoad(void) { Output_Textures_ObserveLevelLoad(); Output_Lights_ObserveLevelLoad(); OutputSource_Objects_ObserveLevelLoad(); OutputSource_Rooms_ObserveLevelLoad(); OutputSource_RoomsDebug_ObserveLevelLoad(); OutputSource_Sprites_ObserveLevelLoad(); MeshBatcher_Seal(m_Batcher); Output_ApplyLevelSettings(); } void Output_DispatchLevelUnload(void) { OutputSource_Objects_ObserveLevelUnload(); OutputSource_Rooms_ObserveLevelUnload(); OutputSource_RoomsDebug_ObserveLevelUnload(); OutputSource_Sprites_ObserveLevelUnload(); } void Output_DispatchRoomFlip(const ROOM *room) { OutputSource_Rooms_ObserveRoomFlip(room); OutputSource_RoomsDebug_ObserveRoomFlip(room); } void Output_DispatchObjectMeshSwap( const int32_t mesh_idx_1, const int32_t mesh_idx_2) { OutputSource_Objects_ObserveObjectMeshSwap(mesh_idx_1, mesh_idx_2); } void Output_DispatchObjectMeshUpdate(const int32_t mesh_idx) { OutputSource_Objects_ObserveObjectMeshUpdate(mesh_idx); } ================================================ FILE: src/trx/game/output/common.h ================================================ #pragma once #include #include #include #include #include #include void Output_Init(void); void Output_Shutdown(void); bool Output_IsHeadless(void); const OUTPUT_UNIFORMS *Output_GetUniforms(void); OUTPUT_MESH_SHADER *Output_GetMeshShader(void); OUTPUT_UI_SHADER *Output_GetUIShader(void); void Output_BeginScene(void); void Output_EndScene(void); void Output_Flush(void); void Output_FlipScreen(void); void Output_SwitchViewport(VIEWPORT_SPACE space); void Output_ApplyRenderSettings(void); void Output_ApplyLevelSettings(void); void Output_DispatchLevelLoad(void); void Output_DispatchLevelUnload(void); void Output_DispatchRoomFlip(const ROOM *room); void Output_DispatchObjectMeshUpdate(int32_t mesh_idx); void Output_DispatchObjectMeshSwap(int32_t mesh_idx_0, int32_t mesh_idx_1); ================================================ FILE: src/trx/game/output/const.h ================================================ #pragma once #define MAX_TEXTURE_PAGES 128 #define TEXTURE_PAGE_WIDTH 256 #define TEXTURE_PAGE_HEIGHT 256 #define TEXTURE_PAGE_SIZE (TEXTURE_PAGE_WIDTH * TEXTURE_PAGE_HEIGHT) // clang-format off #define SHADE_CAUSTICS 0x300 #define SHADE_MAX 0x1FFF #define SHADE_LOW 0x1400 #define SHADE_HIGH 0x800 #define SHADE_NEUTRAL 0x1000 #define SHADE_SUNSET 0x400 // clang-format on #define WIBBLE_SIZE 32 #define LIGHT_MAP_SIZE 32 #define LIGHT_MAP_NEUTRAL 16 // TODO: get rid of this #define PHD_ONE 0x10000 ================================================ FILE: src/trx/game/output/draw.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_SHADOW_LINE_POINTS 4 #define M_SHADOW_GRID_POINTS (M_SHADOW_LINE_POINTS * M_SHADOW_LINE_POINTS) static bool M_DrawShadow_Sprite( const int32_t size, const BOUNDS_16 *const bounds, const ITEM *const item) { const ITEM *const lara_item = Lara_GetItem(); if (lara_item == nullptr) { return false; } const OBJECT *const shadow_obj = Object_Get(O_SHADOW); if (!shadow_obj->loaded) { return false; } // OG: shadow intensity is based on Lara's height above the floor, even for // non-Lara items. int32_t c = ((4096 - ABS(item->floor - item->pos.y)) >> 4) - 1; CLAMP(c, 32, 255); const RGBA_8888 shadow_color = { c, c, c, 255 }; const RGBA_8888 quad_color[4] = { shadow_color, shadow_color, shadow_color, shadow_color, }; const int32_t x_size = size * (bounds->max.x - bounds->min.x) / 128; const int32_t z_size = size * (bounds->max.z - bounds->min.z) / 128; const int32_t x_dist = x_size / M_SHADOW_LINE_POINTS; const int32_t z_dist = z_size / M_SHADOW_LINE_POINTS; int32_t grid_local_x[M_SHADOW_GRID_POINTS]; int32_t grid_local_z[M_SHADOW_GRID_POINTS]; int32_t x = -x_dist - (x_dist >> 1); int32_t z = z_dist + (z_dist >> 1); int32_t grid_idx = 0; for (int32_t row = 0; row < M_SHADOW_LINE_POINTS; row++) { for (int32_t col = 0; col < M_SHADOW_LINE_POINTS; col++) { grid_local_x[grid_idx] = x; grid_local_z[grid_idx] = z; grid_idx++; x += x_dist; } x = -x_dist - (x_dist >> 1); z -= z_dist; } // Determine the shadow anchor position. XYZ_32 anchor_pos = item->interp.result.pos; int32_t anchor_floor = item->interp.result.floor; const int16_t anim_state = item->current_anim_state; if (item == lara_item && anim_state != LS(LS_CRAWL_IDLE) && anim_state != LS(LS_CRAWL_FORWARD) && anim_state != LS(LS_CRAWL_BACK) && anim_state != LS(LS_CRAWL_TURN_LEFT) && anim_state != LS(LS_CRAWL_TURN_RIGHT)) { ANIM_FRAME *frames[2] = { nullptr, nullptr }; int32_t rate = 0; const int32_t frac = Item_GetFrames(item, frames, &rate); if (frames[0] != nullptr) { XYZ_32 offset_a = XYZ_32_From16(frames[0]->offset); XYZ_32 offset = offset_a; if (frames[1] != nullptr && rate != 0 && frac != 0) { const XYZ_32 offset_b = XYZ_32_From16(frames[1]->offset); offset.x += ((offset_b.x - offset_a.x) * frac) / rate; offset.y += ((offset_b.y - offset_a.y) * frac) / rate; offset.z += ((offset_b.z - offset_a.z) * frac) / rate; } const int32_t sy = Math_Sin(item->interp.result.rot.y); const int32_t cy = Math_Cos(item->interp.result.rot.y); anchor_pos.x += (offset.x * cy + offset.z * sy) >> W2V_SHIFT; anchor_pos.y += offset.y; anchor_pos.z += (offset.z * cy - offset.x * sy) >> W2V_SHIFT; int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(anchor_pos, &room_num); const int32_t height = Room_GetHeight(sector, anchor_pos); if (height != NO_HEIGHT) { anchor_floor = height; } } } else { // TR3 cutscene actors are driven by cutscene data, so their item origin // can diverge from the visual mesh center. const int32_t x_mid = (bounds->min.x + bounds->max.x) / 2; const int32_t z_mid = (bounds->min.z + bounds->max.z) / 2; Matrix_Push(); *g_MatrixPtr = g_ViewMatrix; *g_WMatrixPtr = g_IDMatrix; Matrix_TranslateAbs( item->interp.result.pos.x, item->interp.result.floor, item->interp.result.pos.z); Matrix_RotY(item->interp.result.rot.y); Matrix_TranslateRel(x_mid, 0, z_mid); anchor_pos = Matrix_GetOffset_M(g_WMatrixPtr); Matrix_Pop(); } const int32_t base_y = anchor_floor - 16; const int32_t sy = Math_Sin(item->interp.result.rot.y); const int32_t cy = Math_Cos(item->interp.result.rot.y); // Compute the world-space grid points with floor-conforming Y offsets. XYZ_32 grid_world[M_SHADOW_GRID_POINTS]; for (int32_t i = 0; i < M_SHADOW_GRID_POINTS; i++) { const int32_t lx = grid_local_x[i]; const int32_t lz = grid_local_z[i]; const int32_t rx = (lx * cy + lz * sy) >> W2V_SHIFT; const int32_t rz = (lz * cy - lx * sy) >> W2V_SHIFT; const int32_t wx = anchor_pos.x + rx; const int32_t wz = anchor_pos.z + rz; int16_t room_num = item->room_num; XYZ_32 test_pos = { wx, anchor_floor, wz }; const SECTOR *const sector = Room_GetSector(test_pos, &room_num); int32_t height = Room_GetHeight(sector, test_pos); if (height == NO_HEIGHT) { height = anchor_floor; } if (ABS(height - anchor_floor) > 196) { height = anchor_floor; } grid_world[i] = (XYZ_32) { .x = wx, .y = base_y + (height - anchor_floor), .z = wz, }; } const int32_t sprite_idx = shadow_obj->mesh_idx; const int32_t uvw_idx = Output_Textures_GetSpriteUVWIndex(sprite_idx, 0); const OUTPUT_TEXTURE_SIZE atlas_size = Output_Textures_GetAtlasSize(uvw_idx / 4); const OUTPUT_TEXTURE_SIZE quad_atlas_size[4] = { atlas_size, atlas_size, atlas_size, atlas_size, }; OUTPUT_UVW sprite_uvw[4]; for (int32_t i = 0; i < 4; i++) { const int32_t corner_uvw_idx = Output_Textures_GetSpriteUVWIndex(sprite_idx, i); sprite_uvw[i] = Output_Textures_GetUVW(corner_uvw_idx); } const float u_min = MIN(MIN(sprite_uvw[0].u, sprite_uvw[1].u), MIN(sprite_uvw[2].u, sprite_uvw[3].u)); const float u_max = MAX(MAX(sprite_uvw[0].u, sprite_uvw[1].u), MAX(sprite_uvw[2].u, sprite_uvw[3].u)); const float v_min = MIN(MIN(sprite_uvw[0].v, sprite_uvw[1].v), MIN(sprite_uvw[2].v, sprite_uvw[3].v)); const float v_max = MAX(MAX(sprite_uvw[0].v, sprite_uvw[1].v), MAX(sprite_uvw[2].v, sprite_uvw[3].v)); const float w = sprite_uvw[0].w; const float u_span = u_max - u_min; const float v_span = v_max - v_min; const float denom = (float)(M_SHADOW_LINE_POINTS - 1); for (int32_t row = 0; row < M_SHADOW_LINE_POINTS - 1; row++) { const float v0 = v_min + v_span * ((float)row / denom); const float v1 = v_min + v_span * ((float)(row + 1) / denom); for (int32_t col = 0; col < M_SHADOW_LINE_POINTS - 1; col++) { const float u0 = u_min + u_span * ((float)col / denom); const float u1 = u_min + u_span * ((float)(col + 1) / denom); const int32_t i0 = (row * M_SHADOW_LINE_POINTS) + col; const int32_t i1 = i0 + 1; const int32_t i2 = i0 + (M_SHADOW_LINE_POINTS + 1); const int32_t i3 = i0 + M_SHADOW_LINE_POINTS; const XYZ_32 quad_pos[4] = { grid_world[i0], grid_world[i1], grid_world[i2], grid_world[i3], }; const OUTPUT_UVW quad_uvw[4] = { { u0, v0, w }, { u1, v0, w }, { u1, v1, w }, { u0, v1, w }, }; OutputSource_PolyFX_StageQuadExtUV( quad_pos, quad_uvw, quad_atlas_size, nullptr, quad_color, VERT_NO_LIGHTING | VERT_NO_WIBBLE, DRAW_BLEND_SUB); } } return true; } static void M_DrawScreenQuad( const float x0, const float y0, const float x1, const float y1, const float z, const RGBA_8888 tl, const RGBA_8888 tr, const RGBA_8888 bl, const RGBA_8888 br) { OutputSource_UI_StageQuad((OUTPUT_UI_QUAD) { .x0 = x0, .y0 = y0, .x1 = x1, .y1 = y1, .tl = tl, .tr = tr, .bl = bl, .br = br, .z = Output_GetNearZ_UI() + z, }); } void Output_DrawBlackRectangle(const int32_t opacity) { const int32_t sx = 0; const int32_t sy = 0; const int32_t sw = Viewport_GetWidth(VIEWPORT_UI); const int32_t sh = Viewport_GetHeight(VIEWPORT_UI); const RGBA_8888 background = { 0, 0, 0, opacity }; Output_DrawScreenFlatQuad(sx, sy, 0, sw, sh, background); } void Output_DrawRoom(const ROOM *const room, const bool is_outside) { OutputSource_Rooms_StageRoom(room); OutputSource_RoomsDebug_StageRoom(room); } void Output_DrawSprite( const int32_t x, const int32_t y, const int32_t z, const int16_t sprite_idx, const int16_t shade, const RGB_F tint, const DRAW_TYPE draw_type) { Matrix_Push(); Matrix_TranslateAbs(x, y, z); OutputSource_Sprites_Stage(sprite_idx, shade, tint, draw_type); Matrix_Pop(); } void Output_DrawObjectMesh(const OBJECT_MESH *const mesh, const CLIP clip) { OutputSource_Objects_StageObjectMesh(mesh); if (g_Config.debug.enable_debug_spheres) { Output_DrawSphere(mesh->center, mesh->radius); } } void Output_DrawObjectMesh_I(const OBJECT_MESH *const mesh, const CLIP clip) { Matrix_Push(); Matrix_Interpolate(); Output_DrawObjectMesh(mesh, clip); Matrix_Pop(); } void Output_DrawSkybox(const OBJECT_MESH *const mesh) { float sunset_progress = Output_GetTimeInGame() / Output_GetSunsetDuration(); CLAMP(sunset_progress, 0.0f, 1.0f); OutputSource_Objects_StageSkyboxMesh( mesh, SHADE_NEUTRAL + SHADE_SUNSET * sunset_progress); SceneCompositor_Flush(); } void Output_DrawShadow( const int16_t size, const BOUNDS_16 *const bounds, const ITEM *const item) { if (!item->enable_shadow) { return; } OUTPUT_ITEM_BIND *const bind = Output_Bind_GetItem(item); if (bind->shadow_drawn) { return; } bind->shadow_drawn = true; if (g_Config.visuals.shadow_type == SHADOW_TYPE_SPRITE) { if (M_DrawShadow_Sprite(size, bounds, item)) { return; } } const int32_t x_mid = (bounds->min.x + bounds->max.x) / 2; const int32_t z_mid = (bounds->min.z + bounds->max.z) / 2; const int32_t x_size = (bounds->max.x - bounds->min.x) * size / 1024; const int32_t z_size = (bounds->max.z - bounds->min.z) * size / 1024; Matrix_Push(); *g_MatrixPtr = g_ViewMatrix; *g_WMatrixPtr = g_IDMatrix; Matrix_TranslateAbs( item->interp.result.pos.x, item->interp.result.floor, item->interp.result.pos.z); Matrix_RotY(item->interp.result.rot.y); Matrix_TranslateRel(x_mid, 0, z_mid); Matrix_ScaleX((1 << W2V_SHIFT) * x_size / UNIT_SHADOW); Matrix_ScaleZ((1 << W2V_SHIFT) * z_size / UNIT_SHADOW); OutputSource_Shadows_StageShadow(); Matrix_Pop(); } void Output_DrawLightningSegment(const LIGHTNING_SEGMENT segment) { OutputSource_Lightnings_StageSegment(&segment); } void Output_DrawScreenSprite( const int32_t sx, const int32_t sy, const int32_t z, const int32_t scale_h, const int32_t scale_v, const int32_t sprite_idx, const RGBA_F colors[4]) { const SPRITE_TEXTURE *const sprite = Output_GetSpriteTexture(sprite_idx); const int32_t x0 = sx + (scale_h * sprite->x0 / PHD_ONE); const int32_t x1 = sx + (scale_h * sprite->x1 / PHD_ONE); const int32_t y0 = sy + (scale_v * sprite->y0 / PHD_ONE); const int32_t y1 = sy + (scale_v * sprite->y1 / PHD_ONE); OutputSource_UI_StageSprite((OUTPUT_UI_SPRITE) { .sprite_idx = sprite_idx, .x0 = x0, .y0 = y0, .x1 = x1, .y1 = y1, .z = Output_GetNearZ_UI() + z, .color = { colors[0], colors[1], colors[2], colors[3], }, }); } void Output_DrawScreenFlatQuad( const int32_t sx, const int32_t sy, const int32_t z, const int32_t w, const int32_t h, const RGBA_8888 color) { M_DrawScreenQuad(sx, sy, sx + w, sy + h, z, color, color, color, color); } void Output_DrawScreenGradientQuad( const int32_t sx, const int32_t sy, const int32_t z, const int32_t w, const int32_t h, const RGBA_8888 tl, const RGBA_8888 tr, const RGBA_8888 bl, const RGBA_8888 br) { M_DrawScreenQuad(sx, sy, sx + w, sy + h, z, tl, tr, bl, br); } void Output_DrawScreenFrame( const int32_t sx, const int32_t sy, const int32_t w, const int32_t h, const RGBA_8888 col_dark, const RGBA_8888 col_light, const float thickness) { const float e = thickness; const float x0 = sx; const float y0 = sy; const float x1 = sx + w; const float y1 = sy + h; const RGBA_8888 cd = col_dark; const RGBA_8888 cl = col_light; // clang-format off M_DrawScreenQuad(x0, y0, x1 - e, y0 + e, 0, cd, cd, cd, cd); M_DrawScreenQuad(x0 - e, y0 - e, x1, y0, 0, cl, cl, cl, cl); M_DrawScreenQuad(x1, y0 - e, x1 + e, y1 + e, 0, cd, cd, cd, cd); M_DrawScreenQuad(x1 - e, y0, x1, y1, 0, cl, cl, cl, cl); M_DrawScreenQuad(x0, y0, x0 + e, y1 - e, 0, cd, cd, cd, cd); M_DrawScreenQuad(x0 - e, y0 - e, x0, y1, 0, cl, cl, cl, cl); M_DrawScreenQuad(x0 - e, y1, x1 + e, y1 + e, 0, cd, cd, cd, cd); M_DrawScreenQuad(x0 - e, y1 - e, x1, y1, 0, cl, cl, cl, cl); // clang-format on } void Output_DrawPhotoModeFrame(const int32_t thickness) { const VIEWPORT_RECT rect = Viewport_GetRect(VIEWPORT_UI); const RGBA_8888 color = { 255, 0, 0, 96 }; OutputSource_UI_StagePhotoModeFrame(rect, color, thickness); } void Output_DrawSphere(const XYZ_16 center, const int32_t radius) { const bool wireframe_state = g_Config.rendering.enable_wireframe; const RGBA_8888 color_black = { 0, 0, 0, 128 }; const RGBA_8888 color_white = { 255, 255, 255, 128 }; const RGBA_8888 color = wireframe_state ? color_black : color_white; Output_DrawSphereEx(center, radius, color); } void Output_DrawSphereEx( const XYZ_16 center, const int32_t radius, const RGBA_8888 color) { Matrix_Push(); Matrix_TranslateRel16(center); Matrix_Scale(radius << W2V_SHIFT); OutputSource_Misc_StageSphere(color); Matrix_Pop(); } void Output_DrawCuboid(const BOUNDS_16 *const bounds) { Output_DrawCuboidEx(bounds, (RGBA_8888) { 255, 0, 0, 255 }); } void Output_DrawCuboidEx(const BOUNDS_16 *const bounds, const RGBA_8888 color) { const int32_t x0 = bounds->min.x; const int32_t x1 = bounds->max.x; const int32_t y0 = bounds->min.y; const int32_t y1 = bounds->max.y; const int32_t z0 = bounds->min.z; const int32_t z1 = bounds->max.z; const int32_t x_mid = (x0 + x1) / 2; const int32_t y_mid = (y0 + y1) / 2; const int32_t z_mid = (z0 + z1) / 2; const int32_t x_size = (x1 - x0) / 2; const int32_t y_size = (y1 - y0) / 2; const int32_t z_size = (z1 - z0) / 2; Matrix_Push(); Matrix_TranslateRel32((XYZ_32) { x_mid, y_mid, z_mid }); Matrix_ScaleX(x_size << W2V_SHIFT); Matrix_ScaleY(y_size << W2V_SHIFT); Matrix_ScaleZ(z_size << W2V_SHIFT); OutputSource_Misc_StageCuboid(color); Matrix_Pop(); } ================================================ FILE: src/trx/game/output/draw.h ================================================ #pragma once #include #include #include #include void Output_DrawSkybox(const OBJECT_MESH *mesh); void Output_DrawObjectMesh(const OBJECT_MESH *mesh, CLIP clip); void Output_DrawObjectMesh_I(const OBJECT_MESH *mesh, CLIP clip); void Output_DrawRoom(const ROOM *room, bool is_outside); void Output_DrawSprite( int32_t x, int32_t y, int32_t z, int16_t sprnum, int16_t shade, RGB_F tint, DRAW_TYPE draw_type); void Output_DrawShadow(int16_t size, const BOUNDS_16 *bounds, const ITEM *item); void Output_DrawLightningSegment(const LIGHTNING_SEGMENT segment); // Fades void Output_DrawBlackRectangle(int32_t opacity); // UI void Output_DrawScreenSprite( int32_t sx, int32_t sy, int32_t sz, int32_t scale_h, int32_t scale_v, int32_t sprite_idx, const RGBA_F colors[4]); void Output_DrawScreenFlatQuad( int32_t sx, int32_t sy, int32_t z, int32_t w, int32_t h, RGBA_8888 color); void Output_DrawScreenGradientQuad( int32_t sx, int32_t sy, int32_t z, int32_t w, int32_t h, RGBA_8888 tl, RGBA_8888 tr, RGBA_8888 bl, RGBA_8888 br); void Output_DrawScreenFrame( int32_t sx, int32_t sy, int32_t w, int32_t h, RGBA_8888 col_dark, RGBA_8888 col_light, float thickness); void Output_DrawPhotoModeFrame(int32_t thickness); void Output_DrawSphere(XYZ_16 center, int32_t radius); void Output_DrawCuboid(const BOUNDS_16 *bounds); void Output_DrawSphereEx(XYZ_16 center, int32_t radius, RGBA_8888 color); void Output_DrawCuboidEx(const BOUNDS_16 *bounds, RGBA_8888 color); ================================================ FILE: src/trx/game/output/func.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include CLIP Output_CheckBoundsClip(const BOUNDS_16 *const bounds) { if (g_MatrixPtr->_23 >= Output_GetFarZ()) { return CLIP_NOT_VISIBLE; } const XYZ_32 vtx[8] = { { .x = bounds->min.x, .y = bounds->min.y, .z = bounds->min.z }, { .x = bounds->max.x, .y = bounds->min.y, .z = bounds->min.z }, { .x = bounds->max.x, .y = bounds->max.y, .z = bounds->min.z }, { .x = bounds->min.x, .y = bounds->max.y, .z = bounds->min.z }, { .x = bounds->min.x, .y = bounds->min.y, .z = bounds->max.z }, { .x = bounds->max.x, .y = bounds->min.y, .z = bounds->max.z }, { .x = bounds->max.x, .y = bounds->max.y, .z = bounds->max.z }, { .x = bounds->min.x, .y = bounds->max.y, .z = bounds->max.z }, }; int32_t num_z = 0; int32_t x_min = INT32_MAX; int32_t y_min = INT32_MAX; int32_t x_max = INT32_MIN; int32_t y_max = INT32_MIN; for (int32_t i = 0; i < 8; i++) { // clang-format off const int32_t zv = ( g_MatrixPtr->_20 * vtx[i].x + g_MatrixPtr->_21 * vtx[i].y + g_MatrixPtr->_22 * vtx[i].z + g_MatrixPtr->_23); // clang-format on if (zv <= Output_GetNearZ() || zv >= Output_GetFarZ()) { continue; } num_z++; const int32_t zp = zv / g_PhdPersp; // clang-format off const int32_t xv = ( g_MatrixPtr->_00 * vtx[i].x + g_MatrixPtr->_01 * vtx[i].y + g_MatrixPtr->_02 * vtx[i].z + g_MatrixPtr->_03) / zp; const int32_t yv = ( g_MatrixPtr->_10 * vtx[i].x + g_MatrixPtr->_11 * vtx[i].y + g_MatrixPtr->_12 * vtx[i].z + g_MatrixPtr->_13) / zp; // clang-format on CLAMPG(x_min, xv); CLAMPL(x_max, xv); CLAMPG(y_min, yv); CLAMPL(y_max, yv); } if (num_z == 0) { return CLIP_NOT_VISIBLE; // out of screen } const VIEWPORT_RECT vp = Viewport_GetRect(VIEWPORT_GAME); x_min += vp.w / 2; x_max += vp.w / 2; y_min += vp.h / 2; y_max += vp.h / 2; // clang-format off if (x_min > g_PhdRight || y_min > g_PhdBottom || x_max < g_PhdLeft || y_max < g_PhdTop) { return CLIP_NOT_VISIBLE; // out of screen } // clang-format on // clang-format off if (num_z < 8 || x_min < 0 || y_min < 0 || x_max >= vp.w || y_max >= vp.h) { return CLIP_PARTIALLY_VISIBLE; } // clang-format on return CLIP_FULLY_VISIBLE; } void Output_MakeScreenshot(const char *const path) { LOG_INFO("Taking screenshot"); TRX_GL_Context_ScheduleScreenshot(path); } void Output_ApplyFOV(void) { int32_t fov = Viewport_GetEffectiveFOV(); const int32_t sw = Viewport_GetWidth(VIEWPORT_GAME); const int32_t sh = Viewport_GetHeight(VIEWPORT_GAME); const float aspect = sw / (float)sh; int32_t fov_width; switch (Viewport_GetFOVMode()) { case FOV_MODE_VERTICAL: { const float fov_rad_h = fov * M_PI / (180 * DEG_1); const float fov_rad_v = 2 * atan(aspect * tan(fov_rad_h / 2)); fov = round((fov_rad_v / M_PI) * (180 * DEG_1)); fov_width = sw; break; } case FOV_MODE_HORIZONTAL: fov_width = sw; break; case FOV_MODE_PC: fov_width = sw * ((4.0f / 3.0f) / aspect); break; case FOV_MODE_PS1: fov_width = sw * ((4.0f / 3.0f) / aspect) * 240 / 200; break; default: ASSERT_FAIL(); } const int16_t c = Math_Cos(fov / 2); const int16_t s = Math_Sin(fov / 2); g_PhdPersp = fov_width / 2; if (s != 0) { g_PhdPersp *= c; g_PhdPersp /= s; } } ================================================ FILE: src/trx/game/output/func.h ================================================ #pragma once #include CLIP Output_CheckBoundsClip(const BOUNDS_16 *bounds); void Output_MakeScreenshot(const char *path); void Output_ApplyFOV(void); ================================================ FILE: src/trx/game/output/lights.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #define M_LIGHT_CYCLE 32 #define M_MAX_ROOM_LIGHT_UNIT (0x2000 / (M_LIGHT_CYCLE / 2)) #define M_TR3_A_SHIFT 5 #define M_TR3_DYNAMIC_FALLOFF_SHIFT 8 typedef struct { XYZ_32 pos; int32_t shade; } M_COMMON_LIGHT; typedef struct { int32_t table[M_LIGHT_CYCLE]; } M_ROOM_LIGHT_TABLE; static bool m_IsSunsetEnabled = false; static int32_t m_RoomLightShades[RLM_NUMBER_OF] = {}; static M_ROOM_LIGHT_TABLE m_RoomLightTables[M_LIGHT_CYCLE] = {}; static VECTOR *m_DynamicLights = nullptr; typedef struct { XYZ_32 sun_dir_world; XYZ_32 bulb_dir_world; XYZ_32 dynamic_dir_world; RGB_888 sun_color; RGB_888 bulb_color; RGB_888 dynamic_color; uint8_t ambient; struct { bool has_sun : 1; bool has_bulb : 1; bool has_dynamic : 1; bool has_ambient : 1; } flags; } M_TR3_ITEM_LIGHT; static M_TR3_ITEM_LIGHT m_TR3ItemLights[MAX_ITEMS] = {}; static RGB_F M_TR3_RGB15ToRGBF(const int16_t rgb15) { const int32_t r8 = (rgb15 & 0x1F) << 3; const int32_t g8 = (rgb15 & 0x3E0) >> 2; const int32_t b8 = (rgb15 & 0x7C00) >> 7; return (RGB_F) { .r = r8 / 255.0f, .g = g8 / 255.0f, .b = b8 / 255.0f, }; } static int16_t M_TR3_ShadeFromMul(const float mul) { float shade_f = (2.0f - mul) * (float)SHADE_NEUTRAL; CLAMP(shade_f, 0.0f, SHADE_MAX); return (int16_t)shade_f; } static uint8_t M_TR3_LerpU8Shift( const uint8_t current, const uint8_t target, const int32_t shift) { const int32_t cur = (int32_t)current; const int32_t dst = (int32_t)target; int32_t next = cur + ((dst - cur) >> shift); CLAMP(next, 0, 255); return (uint8_t)next; } static RGB_888 M_TR3_LerpRGBShift( const RGB_888 current, const RGB_888 target, const int32_t shift) { return (RGB_888) { .r = M_TR3_LerpU8Shift(current.r, target.r, shift), .g = M_TR3_LerpU8Shift(current.g, target.g, shift), .b = M_TR3_LerpU8Shift(current.b, target.b, shift), }; } static XYZ_32 M_TR3_LerpXYZShift( const XYZ_32 current, const XYZ_32 target, const int32_t shift) { return (XYZ_32) { .x = current.x + ((target.x - current.x) >> shift), .y = current.y + ((target.y - current.y) >> shift), .z = current.z + ((target.z - current.z) >> shift), }; } static XYZ_32 M_TR3_NormalizeDeltaWorld(const XYZ_32 delta) { const int32_t dx = delta.x >> 2; const int32_t dy = delta.y >> 2; const int32_t dz = delta.z >> 2; const uint32_t len = Math_Sqrt((uint32_t)(SQUARE(dx) + SQUARE(dy) + SQUARE(dz))); if (len == 0u) { return (XYZ_32) { 0, 0, 0 }; } return (XYZ_32) { .x = (dx * (1 << W2V_SHIFT)) / (int32_t)len, .y = (dy * (1 << W2V_SHIFT)) / (int32_t)len, .z = (dz * (1 << W2V_SHIFT)) / (int32_t)len, }; } static XYZ_32 M_TR3_VectorViewFromWorld(const XYZ_32 v_world) { const MATRIX *const m = &g_ViewMatrix; return (XYZ_32) { .x = (m->_00 * v_world.x + m->_01 * v_world.y + m->_02 * v_world.z) >> W2V_SHIFT, .y = (m->_10 * v_world.x + m->_11 * v_world.y + m->_12 * v_world.z) >> W2V_SHIFT, .z = (m->_20 * v_world.x + m->_21 * v_world.y + m->_22 * v_world.z) >> W2V_SHIFT, }; } static void M_TR3_SetConstantLight(const RGB_F ambient) { const RGB_F colors[3] = {}; const XYZ_32 dirs_view[3] = {}; Output_SetTR3Light(ambient, colors, dirs_view); } static void M_CalculateBrightestLight( const XYZ_32 pos, const ROOM *const room, M_COMMON_LIGHT *const brightest_light) { if (room->light_mode != RLM_NORMAL) { const int32_t light_shade = Output_GetRoomLightShade(room->light_mode); for (int32_t i = 0; i < room->num_lights; i++) { const LIGHT *const light = &room->lights[i]; const int32_t dx = pos.x - light->pos.x; const int32_t dy = pos.y - light->pos.y; const int32_t dz = pos.z - light->pos.z; const int32_t falloff_1 = SQUARE(light->falloff.value_1) >> 12; const int32_t falloff_2 = SQUARE(light->falloff.value_2) >> 12; const int32_t dist = (SQUARE(dx) + SQUARE(dy) + SQUARE(dz)) >> 12; const int32_t shade_1 = falloff_1 * light->shade.value_1 / MAX(1, falloff_1 + dist); const int32_t shade_2 = falloff_2 * light->shade.value_2 / MAX(1, falloff_2 + dist); const int32_t shade = shade_1 + (shade_2 - shade_1) * light_shade / (M_LIGHT_CYCLE - 1); if (shade > brightest_light->shade) { brightest_light->shade = shade; brightest_light->pos = light->pos; } } return; } const int32_t ambient = g_TRVersion == 1 ? (SHADE_MAX - room->ambient) : 0; for (int32_t i = 0; i < room->num_lights; i++) { const LIGHT *const light = &room->lights[i]; const int32_t dx = pos.x - light->pos.x; const int32_t dy = pos.y - light->pos.y; const int32_t dz = pos.z - light->pos.z; const int32_t falloff = SQUARE(light->falloff.value_1) >> 12; const int32_t dist = (SQUARE(dx) + SQUARE(dy) + SQUARE(dz)) >> 12; const int32_t shade = ambient + (falloff * light->shade.value_1 / (falloff + dist)); if (shade > brightest_light->shade) { brightest_light->shade = shade; brightest_light->pos = light->pos; } } } static int32_t M_CalculateDynamicLight( const XYZ_32 pos, M_COMMON_LIGHT *const brightest_light) { int32_t adder = 0; for (int32_t i = 0; i < m_DynamicLights->count; i++) { const LIGHT *const light = Vector_Get(m_DynamicLights, i); const int32_t dx = pos.x - light->pos.x; const int32_t dy = pos.y - light->pos.y; const int32_t dz = pos.z - light->pos.z; const int32_t radius = 1 << light->falloff.value_1; if (dx < -radius || dx > radius || dy < -radius || dy > radius || dz < -radius || dz > radius) { continue; } const int32_t dist = SQUARE(dx) + SQUARE(dy) + SQUARE(dz); if (dist > SQUARE(radius)) { continue; } const int32_t shade = (1 << light->shade.value_1) - (dist >> (2 * light->falloff.value_1 - light->shade.value_1)); if (shade > brightest_light->shade) { brightest_light->shade = shade; brightest_light->pos = light->pos; } adder += shade; } return adder; } static void M_TR3_CalculateLightSmoothed( const ITEM *const item, const XYZ_32 pos, const ROOM *const room) { const LIGHT *sun_light = nullptr; bool has_sun = false; const LIGHT *brightest_light = nullptr; int32_t brightest = -1; XYZ_32 bulb_delta = {}; int32_t ambience = ((SHADE_MAX - room->ambient) >> M_TR3_A_SHIFT) + 1; for (int32_t i = 0; i < room->num_lights; i++) { const LIGHT *const light = &room->lights[i]; if (light->type != 0u) { has_sun = true; sun_light = light; continue; } const int32_t falloff = light->falloff.value_1; if (falloff <= 0) { continue; } const int32_t dx = (light->pos.x - pos.x) >> 2; const int32_t dy = (light->pos.y - pos.y) >> 2; const int32_t dz = (light->pos.z - pos.z) >> 2; const uint32_t distance = Math_Sqrt((uint32_t)(SQUARE(dx) + SQUARE(dy) + SQUARE(dz))); if ((int32_t)distance > falloff) { continue; } const int32_t intensity = light->shade.value_1; int32_t shade = intensity - (intensity * (int32_t)distance) / falloff; CLAMPL(shade, 0); ambience += shade >> 7; if (shade > brightest) { brightest = shade; brightest_light = light; bulb_delta = (XYZ_32) { .x = light->pos.x - pos.x, .y = light->pos.y - pos.y, .z = light->pos.z - pos.z, }; } } const LIGHT *brightest_dynamic = nullptr; int32_t brightest_dyn_shade = -1; XYZ_32 dyn_delta = {}; for (int32_t i = 0; i < m_DynamicLights->count; i++) { const LIGHT *const light = Vector_Get(m_DynamicLights, i); const int32_t falloff_half = light->falloff.value_1 >> 1; if (falloff_half <= 0) { continue; } const int32_t dx = light->pos.x - pos.x; const int32_t dy = light->pos.y - pos.y; const int32_t dz = light->pos.z - pos.z; const int32_t max_dist = WALL_L * 8; if (ABS(dx) > max_dist || ABS(dy) > max_dist || ABS(dz) > max_dist) { continue; } const uint32_t distance = Math_Sqrt((uint32_t)(SQUARE(dx) + SQUARE(dy) + SQUARE(dz))); if ((int32_t)distance > falloff_half) { continue; } int32_t shade = SHADE_MAX - (SHADE_MAX * (int32_t)distance) / falloff_half; CLAMPL(shade, 0); ambience += shade >> 8; if (shade > brightest_dyn_shade) { brightest_dyn_shade = shade; brightest_dynamic = light; dyn_delta = (XYZ_32) { .x = light->pos.x - pos.x, .y = light->pos.y - pos.y, .z = light->pos.z - pos.z, }; } } CLAMP(ambience, 0, 255); const uint8_t ambient_target = (uint8_t)ambience; M_TR3_ITEM_LIGHT dummy = {}; M_TR3_ITEM_LIGHT *il = &dummy; bool enable_smoothing = false; if (item != nullptr) { const int16_t item_num = Item_GetIndex(item); if (item_num >= 0 && item_num < MAX_ITEMS) { il = &m_TR3ItemLights[item_num]; enable_smoothing = true; } } // Ambient (smoothed) if (enable_smoothing && il->flags.has_ambient) { il->ambient = M_TR3_LerpU8Shift(il->ambient, ambient_target, 3); } else { il->ambient = ambient_target; il->flags.has_ambient = true; } // Sun (smoothed) bool want_sun = false; XYZ_32 sun_dir_world_target = {}; RGB_888 sun_target = {}; int32_t ambient_base = (SHADE_MAX - room->ambient) >> M_TR3_A_SHIFT; CLAMP(ambient_base, 0, 255); if (has_sun && sun_light != nullptr) { want_sun = true; sun_dir_world_target = (XYZ_32) { .x = sun_light->dir.x, .y = sun_light->dir.y, .z = sun_light->dir.z, }; sun_target = sun_light->color; } else if (enable_smoothing && il->flags.has_sun) { want_sun = true; sun_dir_world_target = il->sun_dir_world; sun_target = (RGB_888) { ambient_base, ambient_base, ambient_base }; } if (want_sun) { if (enable_smoothing && il->flags.has_sun) { il->sun_dir_world = M_TR3_LerpXYZShift(il->sun_dir_world, sun_dir_world_target, 3); il->sun_color = M_TR3_LerpRGBShift(il->sun_color, sun_target, 3); } else { il->sun_dir_world = sun_dir_world_target; il->sun_color = sun_target; il->flags.has_sun = true; } } // Bulb (smoothed) bool want_bulb = false; XYZ_32 bulb_dir_world_target = {}; RGB_888 bulb_target = {}; if (brightest_light != nullptr && brightest > 0) { want_bulb = true; bulb_dir_world_target = M_TR3_NormalizeDeltaWorld(bulb_delta); int32_t r8 = (brightest * (int32_t)brightest_light->color.r) >> 13; int32_t g8 = (brightest * (int32_t)brightest_light->color.g) >> 13; int32_t b8 = (brightest * (int32_t)brightest_light->color.b) >> 13; CLAMP(r8, 0, 255); CLAMP(g8, 0, 255); CLAMP(b8, 0, 255); bulb_target = (RGB_888) { r8, g8, b8 }; } else if (enable_smoothing && il->flags.has_bulb) { want_bulb = true; bulb_dir_world_target = il->bulb_dir_world; bulb_target = (RGB_888) { ambient_base, ambient_base, ambient_base }; } if (want_bulb) { if (enable_smoothing && il->flags.has_bulb) { il->bulb_dir_world = M_TR3_LerpXYZShift( il->bulb_dir_world, bulb_dir_world_target, 3); il->bulb_color = M_TR3_LerpRGBShift(il->bulb_color, bulb_target, 3); } else { il->bulb_dir_world = bulb_dir_world_target; il->bulb_color = bulb_target; il->flags.has_bulb = true; } } // Dynamic (smoothed while active, drops instantly when not present) bool want_dynamic = false; XYZ_32 dynamic_dir_world_target = {}; RGB_888 dynamic_target = {}; if (brightest_dynamic != nullptr && brightest_dyn_shade > 0) { want_dynamic = true; dynamic_dir_world_target = M_TR3_NormalizeDeltaWorld(dyn_delta); int32_t r8 = (brightest_dyn_shade * (int32_t)brightest_dynamic->color.r) >> 13; int32_t g8 = (brightest_dyn_shade * (int32_t)brightest_dynamic->color.g) >> 13; int32_t b8 = (brightest_dyn_shade * (int32_t)brightest_dynamic->color.b) >> 13; CLAMP(r8, 0, 255); CLAMP(g8, 0, 255); CLAMP(b8, 0, 255); dynamic_target = (RGB_888) { r8, g8, b8 }; } if (want_dynamic) { if (enable_smoothing && il->flags.has_dynamic) { il->dynamic_dir_world = M_TR3_LerpXYZShift( il->dynamic_dir_world, dynamic_dir_world_target, 1); il->dynamic_color = M_TR3_LerpRGBShift(il->dynamic_color, dynamic_target, 1); } else { il->dynamic_dir_world = dynamic_dir_world_target; il->dynamic_color = dynamic_target; il->flags.has_dynamic = true; } } const RGB_F ambient = { .r = il->ambient / 255.0f, .g = il->ambient / 255.0f, .b = il->ambient / 255.0f, }; RGB_F colors[3] = {}; XYZ_32 dirs_view[3] = {}; if (want_sun && il->flags.has_sun) { dirs_view[0] = M_TR3_VectorViewFromWorld(il->sun_dir_world); colors[0] = (RGB_F) { .r = il->sun_color.r / 255.0f, .g = il->sun_color.g / 255.0f, .b = il->sun_color.b / 255.0f, }; } if (want_bulb && il->flags.has_bulb) { dirs_view[1] = M_TR3_VectorViewFromWorld(il->bulb_dir_world); colors[1] = (RGB_F) { .r = il->bulb_color.r / 255.0f, .g = il->bulb_color.g / 255.0f, .b = il->bulb_color.b / 255.0f, }; } if (want_dynamic && il->flags.has_dynamic) { dirs_view[2] = M_TR3_VectorViewFromWorld(il->dynamic_dir_world); colors[2] = (RGB_F) { .r = il->dynamic_color.r / 255.0f, .g = il->dynamic_color.g / 255.0f, .b = il->dynamic_color.b / 255.0f, }; } Output_SetTR3Light(ambient, colors, dirs_view); // Keep legacy scalar shade meaningful for sprite/effect code paths. Output_SetLightDivider(0); Output_SetLightAdder( M_TR3_ShadeFromMul((ambient.r + ambient.g + ambient.b) / 3.0f)); } void Output_CalculateLight(const XYZ_32 pos, const int16_t room_num) { const ROOM *const room = Room_Get(room_num); if (g_TRVersion >= 3) { M_TR3_CalculateLightSmoothed(nullptr, pos, room); return; } M_COMMON_LIGHT brightest_light = {}; M_CalculateBrightestLight(pos, room, &brightest_light); int32_t adder = brightest_light.shade; int32_t dynamic_adder = M_CalculateDynamicLight(pos, &brightest_light); adder = (adder + dynamic_adder) / 2; if (g_TRVersion == 1 && (room->num_lights > 0 || dynamic_adder > 0)) { adder += (SHADE_MAX - room->ambient) / 2; } // TODO: use m_LsAdder and m_LsDivider once ported int32_t global_adder; int32_t global_divider; if (adder == 0) { global_adder = room->ambient; global_divider = 0; } else { if (g_TRVersion == 1) { global_adder = SHADE_MAX - adder; const int32_t divider = brightest_light.shade == adder ? adder : brightest_light.shade - adder; global_divider = (1 << W2V_SHIFT) * SHADE_NEUTRAL / divider; } else { global_adder = room->ambient - adder; global_divider = (1 << W2V_SHIFT) * SHADE_NEUTRAL / adder; } int16_t angles[2]; Math_GetVectorAngles( pos.x - brightest_light.pos.x, pos.y - brightest_light.pos.y, pos.z - brightest_light.pos.z, angles); Output_RotateLight(angles[1], angles[0]); } CLAMPG(global_adder, SHADE_MAX); Output_SetLightAdder(global_adder); Output_SetLightDivider(global_divider); } void Output_CalculateStaticLight(const int16_t adder) { // TODO: use m_LsAdder int32_t global_adder = adder - SHADE_NEUTRAL; CLAMPG(global_adder, SHADE_MAX); Output_SetLightAdder(global_adder); if (g_TRVersion >= 3) { float mul = 2.0f - (adder / (float)SHADE_NEUTRAL); CLAMP(mul, 0.0f, 1.0f); M_TR3_SetConstantLight((RGB_F) { mul, mul, mul }); } } void Output_CalculateStaticLightRGB15(const int16_t rgb15) { if (g_TRVersion < 3) { return; } M_TR3_SetConstantLight(M_TR3_RGB15ToRGBF(rgb15)); } void Output_CalculateStaticLightRGB_F(const RGB_F rgb) { if (g_TRVersion < 3) { return; } M_TR3_SetConstantLight(rgb); } void Output_CalculateStaticMeshLight( const XYZ_32 pos, const SHADE shade, const ROOM *const room) { if (g_TRVersion >= 3) { const RGB_F base = M_TR3_RGB15ToRGBF(shade.value_1 & 0x7FFF); int32_t r = (int32_t)(base.r * 255.0f); int32_t g = (int32_t)(base.g * 255.0f); int32_t b = (int32_t)(base.b * 255.0f); for (int32_t i = 0; i < m_DynamicLights->count; i++) { const LIGHT *const light = Vector_Get(m_DynamicLights, i); const int32_t falloff_half = light->falloff.value_1 >> 1; if (falloff_half <= 0) { continue; } const XYZ_32 delta = { .x = pos.x - light->pos.x, .y = pos.y - light->pos.y, .z = pos.z - light->pos.z, }; const uint32_t distance = XYZ_32_GetLength(delta); if ((int32_t)distance > falloff_half) { continue; } int32_t fall = SHADE_MAX - (SHADE_MAX * (int32_t)distance) / falloff_half; CLAMPL(fall, 0); r += (fall * (int32_t)light->color.r) >> 13; g += (fall * (int32_t)light->color.g) >> 13; b += (fall * (int32_t)light->color.b) >> 13; } CLAMP(r, 0, 255); CLAMP(g, 0, 255); CLAMP(b, 0, 255); const RGB_F ambient = { r / 255.0f, g / 255.0f, b / 255.0f }; M_TR3_SetConstantLight(ambient); Output_SetLightDivider(0); Output_SetLightAdder( M_TR3_ShadeFromMul((ambient.r + ambient.g + ambient.b) / 3.0f)); return; } int32_t adder = shade.value_1; if (room->light_mode != RLM_NORMAL) { const int32_t room_shade = Output_GetRoomLightShade(room->light_mode); adder += (shade.value_2 - shade.value_1) * room_shade / (M_LIGHT_CYCLE - 1); } for (int32_t i = 0; i < m_DynamicLights->count; i++) { const LIGHT *const light = Vector_Get(m_DynamicLights, i); const int32_t dx = pos.x - light->pos.x; const int32_t dy = pos.y - light->pos.y; const int32_t dz = pos.z - light->pos.z; const int32_t radius = 1 << light->falloff.value_1; if (dx < -radius || dx > radius || dy < -radius || dy > radius || dz < -radius || dz > radius) { continue; } const int32_t dist = SQUARE(dx) + SQUARE(dy) + SQUARE(dz); if (dist > SQUARE(radius)) { continue; } adder -= (1 << light->shade.value_1) - (dist >> (2 * light->falloff.value_1 - light->shade.value_1)); if (adder < 0) { break; } } Output_CalculateStaticLight(adder); } void Output_CalculateObjectLighting( const ITEM *const item, const BOUNDS_16 *const bounds) { if (item->shade.value_1 >= 0) { Output_CalculateStaticMeshLight( item->pos, item->shade, Room_Get(item->room_num)); return; } Matrix_PushUnit(); Matrix_TranslateSet32((XYZ_32) {}); Matrix_Rot16(item->rot); Matrix_TranslateRel32((XYZ_32) { .x = (bounds->min.x + bounds->max.x) / 2, .y = (bounds->max.y + bounds->min.y) / 2, .z = (bounds->max.z + bounds->min.z) / 2, }); const GAME_VECTOR sample_pos = { .room_num = item->room_num, .pos = { .x = item->pos.x + (g_MatrixPtr->_03 >> W2V_SHIFT), .y = item->pos.y + (g_MatrixPtr->_13 >> W2V_SHIFT), .z = item->pos.z + (g_MatrixPtr->_23 >> W2V_SHIFT), }, }; Matrix_Pop(); Output_CalculateObjectLightingAt(item, sample_pos); } void Output_CalculateObjectLightingAt( const ITEM *const item, const GAME_VECTOR sample_pos) { int16_t room_num = sample_pos.room_num; if (g_TRVersion >= 3) { Room_GetSector(sample_pos.pos, &room_num); M_TR3_CalculateLightSmoothed(item, sample_pos.pos, Room_Get(room_num)); } else { Output_CalculateLight(sample_pos.pos, room_num); } } void Output_Lights_Init(void) { if (m_DynamicLights == nullptr) { m_DynamicLights = Vector_Create(sizeof(LIGHT)); } memset(m_TR3ItemLights, 0, sizeof(m_TR3ItemLights)); for (int32_t i = 0; i < M_LIGHT_CYCLE; i++) { for (int32_t j = 0; j < M_LIGHT_CYCLE; j++) { m_RoomLightTables[i].table[j] = (j - (M_LIGHT_CYCLE / 2)) * i * M_MAX_ROOM_LIGHT_UNIT / (M_LIGHT_CYCLE - 1); } } } void Output_Lights_Shutdown(void) { if (m_DynamicLights != nullptr) { Vector_Free(m_DynamicLights); m_DynamicLights = nullptr; } } void Output_Lights_ObserveLevelLoad(void) { memset(m_TR3ItemLights, 0, sizeof(m_TR3ItemLights)); } void Output_ResetDynamicLights(void) { Vector_Clear(m_DynamicLights); } VECTOR *Output_GetDynamicLights(void) { return m_DynamicLights; } void Output_AddDynamicLight( const XYZ_32 pos, const int32_t intensity, const int32_t falloff) { if (g_TRVersion >= 3) { int32_t safe_intensity = intensity; int32_t safe_falloff = falloff; CLAMP(safe_intensity, 0, 30); CLAMP(safe_falloff, 0, 30); int32_t max_shade = 1 << safe_intensity; int32_t c = max_shade >> 4; CLAMPG(c, 255); int32_t radius = 1 << safe_falloff; int32_t falloff_param = radius >> 7; CLAMP(falloff_param, 1, 255); const LIGHT light = { .pos = pos, .shade = {}, .falloff.value_1 = falloff_param << M_TR3_DYNAMIC_FALLOFF_SHIFT, .color = (RGB_888) { c, c, c }, .type = 0, .dir = {}, }; Vector_Add(m_DynamicLights, &light); } else { const LIGHT light = { .pos = pos, .shade.value_1 = intensity, .falloff.value_1 = falloff, .color = COLOR_RGB_888_WHITE, .type = 0, .dir = {}, }; Vector_Add(m_DynamicLights, &light); } } void Output_AddDynamicLightRGB( const XYZ_32 pos, const int32_t falloff, const RGB_888 color) { int32_t safe_falloff = falloff; CLAMP(safe_falloff, 0, 255); const LIGHT light = { .pos = pos, .shade = {}, .falloff.value_1 = safe_falloff << M_TR3_DYNAMIC_FALLOFF_SHIFT, .color = color, .type = g_TRVersion < 3 ? 1 : 0, .dir = {}, }; Vector_Add(m_DynamicLights, &light); } int32_t Output_GetRoomLightShade(const ROOM_LIGHT_MODE mode) { return m_RoomLightShades[mode]; } int32_t Output_GetSunsetDuration(void) { return 20 * 60 * LOGIC_FPS; // = 20 minutes / 36000 frames } void Output_SetSunsetEnabled(const bool enabled) { m_IsSunsetEnabled = enabled; } int16_t Output_GetSkyShade(void) { if (!m_IsSunsetEnabled) { return SHADE_NEUTRAL; } float sunset_progress = Output_GetTimeInGame() / (float)Output_GetSunsetDuration(); CLAMP(sunset_progress, 0.0f, 1.0f); return SHADE_NEUTRAL + SHADE_SUNSET * sunset_progress; } void Output_AnimateLights(const int32_t num_frames) { const int32_t time = ((int32_t)Output_GetTimeInGame()) % M_LIGHT_CYCLE; if (g_TRVersion >= 2) { m_RoomLightShades[RLM_FLICKER] = Random_GetDraw() % M_LIGHT_CYCLE; m_RoomLightShades[RLM_GLOW] = (M_LIGHT_CYCLE - 1) * (Math_Sin((time * DEG_360) / M_LIGHT_CYCLE) + 0x4000) >> 15; if (m_IsSunsetEnabled) { int32_t sunset_timer = Output_GetTimeInGame(); CLAMPG(sunset_timer, Output_GetSunsetDuration()); m_RoomLightShades[RLM_SUNSET] = sunset_timer * (M_LIGHT_CYCLE - 1) / Output_GetSunsetDuration(); } } } ================================================ FILE: src/trx/game/output/lights.h ================================================ #pragma once #include #include #include #include #include #include void Output_Lights_Init(void); void Output_Lights_Shutdown(void); void Output_Lights_ObserveLevelLoad(void); void Output_CalculateLight(XYZ_32 pos, int16_t room_num); void Output_CalculateStaticLight(int16_t adder); void Output_CalculateStaticLightRGB15(int16_t rgb15); void Output_CalculateStaticLightRGB_F(RGB_F rgb); void Output_CalculateStaticMeshLight(XYZ_32 pos, SHADE shade, const ROOM *room); void Output_CalculateObjectLighting(const ITEM *item, const BOUNDS_16 *bounds); void Output_CalculateObjectLightingAt( const ITEM *item, const GAME_VECTOR sample_pos); int32_t Output_GetRoomLightShade(ROOM_LIGHT_MODE mode); int32_t Output_GetSunsetDuration(void); void Output_SetSunsetEnabled(bool enabled); int16_t Output_GetSkyShade(void); void Output_ResetDynamicLights(void); void Output_AddDynamicLight(XYZ_32 pos, int32_t intensity, int32_t falloff); void Output_AddDynamicLightRGB(XYZ_32 pos, int32_t falloff, RGB_888 color); VECTOR *Output_GetDynamicLights(void); void Output_AnimateLights(int32_t num_frames); ================================================ FILE: src/trx/game/output/mesh_batcher/batcher.c ================================================ #include #include #include #include #include #include #include #include #include #include typedef float M_MESH_SHADE; typedef struct { XYZW_F pos; XYZW_F normal; OUTPUT_USHORT flags; RGBA_8888 color; } M_MESH_GEOM; typedef struct { OUTPUT_UVW uvw; OUTPUT_TEXTURE_SIZE texture_size; float trapezoid_ratio[2]; } M_MESH_TEXTURE; typedef struct M_MESH_BUF_BINDING { OUTPUT_MESH *mesh; M_MESH_GEOM *geom_data; M_MESH_TEXTURE *tex_data; M_MESH_SHADE *shade_data; bool needs_room_lights; bool needs_cpu_light; bool needs_object_light; bool needs_own_light; int32_t vertex_start; int32_t vertex_count; int32_t opaque_index_start; int32_t opaque_index_count; int32_t blend_add_index_start; int32_t blend_add_index_count; int32_t transparent_index_start; int32_t transparent_index_count; int32_t transparent_face_count; int32_t *transparent_face_index_starts; int32_t *transparent_face_index_counts; UT_hash_handle hh; } M_MESH_BUF_BINDING; typedef struct { int32_t sort_key; const MESH_INSTANCE *inst; const OUTPUT_MESH_FACE *face; int32_t index_start; int32_t index_count; } M_FACE_SORT; typedef struct MESH_BATCHER { SCENE_SOURCE source; int32_t vertex_count; VECTOR *bindings; M_MESH_BUF_BINDING *binding_map; VECTOR *staged[SCENE_PASS_COUNT]; OUTPUT_MESH_SHADER *shader; GLuint vao; struct { GLuint geom; GLuint tex; GLuint shade; } vbo; VECTOR *transparent_sort; // M_FACE_SORT struct { GLuint opaque; GLuint transparent; GLuint blend_add; } ebo; int32_t opaque_total_indices; int32_t blend_add_total_indices; int32_t transparent_total_indices; bool layout_dirty; } MESH_BATCHER; static M_MESH_BUF_BINDING *M_GetBinding( const MESH_BATCHER *const batcher, const OUTPUT_MESH *const mesh) { M_MESH_BUF_BINDING *bind = nullptr; HASH_FIND_PTR(batcher->binding_map, &mesh, bind); return bind; } static void M_FillGeometry( M_MESH_GEOM *const geom, const OUTPUT_MESH_VERTEX *const vertex) { geom->pos.x = vertex->pos.x; geom->pos.y = vertex->pos.y; geom->pos.z = vertex->pos.z; geom->pos.w = vertex->pos.w; geom->normal.x = vertex->normal.x; geom->normal.y = vertex->normal.y; geom->normal.z = vertex->normal.z; geom->normal.w = vertex->light_table_idx; geom->color = vertex->color; geom->flags = vertex->flags; } static void M_FillTexture( M_MESH_TEXTURE *const tex, const OUTPUT_MESH_VERTEX *const vertex) { if (vertex->uvw_idx < 0) { return; } tex->uvw = Output_Textures_GetUVW(vertex->uvw_idx); tex->texture_size = Output_Textures_GetAtlasSize(vertex->uvw_idx / 4); tex->trapezoid_ratio[0] = vertex->trapezoid_ratio[0]; tex->trapezoid_ratio[1] = vertex->trapezoid_ratio[1]; } static void M_FillShade( M_MESH_SHADE *const shade, const OUTPUT_MESH_VERTEX *const vertex) { *shade = vertex->shade; } static void M_SyncRoom( const MESH_BATCHER *const batcher, const M_MESH_BUF_BINDING *const bind, const ROOM *const room) { if (!bind->needs_room_lights) { return; } Output_Uniforms_UploadRoomLights(Output_GetUniforms(), room); } static void M_AnimateBinding( const MESH_BATCHER *const batcher, const M_MESH_BUF_BINDING *const bind) { ASSERT(bind != nullptr); glBindBuffer(GL_ARRAY_BUFFER, batcher->vbo.tex); const OUTPUT_MESH_VERTEX *const vertices = Vector_GetData(bind->mesh->vertices); for (int32_t i = 0; i < bind->mesh->animated_vertices->count; i++) { const OUTPUT_VERTEX_RANGE *const range = Vector_Get(bind->mesh->animated_vertices, i); for (int32_t j = range->vertex_start; j < range->vertex_start + range->vertex_count; j++) { M_FillTexture(&bind->tex_data[j], &vertices[j]); } TRX_GL_TRACK_DATA( glBufferSubData, GL_ARRAY_BUFFER, (bind->vertex_start + range->vertex_start) * sizeof(M_MESH_TEXTURE), range->vertex_count * sizeof(M_MESH_TEXTURE), &bind->tex_data[range->vertex_start]); } } static void M_UpdateMeshGeometry( const MESH_BATCHER *const batcher, const OUTPUT_MESH *const mesh) { M_MESH_BUF_BINDING *const bind = M_GetBinding(batcher, mesh); const OUTPUT_MESH_VERTEX *const vertices = Vector_GetData(mesh->vertices); for (int32_t i = 0; i < bind->vertex_count; i++) { M_FillGeometry(&bind->geom_data[i], &vertices[i]); } TRX_GL_TRACK_SUBDATA( glBufferSubData, GL_ARRAY_BUFFER, bind->vertex_start * sizeof(M_MESH_GEOM), bind->vertex_count * sizeof(M_MESH_GEOM), bind->geom_data); } // Compare two faces by camera-space depth. static int M_CompareFaceDepth(const void *const a, const void *const b) { const M_FACE_SORT *const face_a = a; const M_FACE_SORT *const face_b = b; if (face_b->sort_key == face_a->sort_key) { return face_b->inst - face_a->inst; } return face_b->sort_key - face_a->sort_key; } // Compute per-face view depth and sort the mesh's transparent ranges // back-to-front. static void M_SortTransparentFaces(const MESH_BATCHER *const batcher) { const int n = batcher->transparent_sort->count; if (n == 0) { return; } M_FACE_SORT *const buf = Vector_GetData(batcher->transparent_sort); M_FACE_SORT *bptr = buf; for (int32_t i = 0; i < n; i++) { // clang-format off bptr->sort_key = ( bptr->inst->cwmatrix._20 * (int32_t)bptr->face->mesh_centroid.x + bptr->inst->cwmatrix._21 * (int32_t)bptr->face->mesh_centroid.y + bptr->inst->cwmatrix._22 * (int32_t)bptr->face->mesh_centroid.z + bptr->inst->cwmatrix._23); // clang-format on bptr++; } qsort(buf, n, sizeof(*buf), M_CompareFaceDepth); } static void M_DrawOpaqueVertices( const MESH_BATCHER *const batcher, const MESH_INSTANCE *const inst) { M_MESH_BUF_BINDING *const bind = M_GetBinding(batcher, inst->mesh); const void *indices_offset = (void *)(intptr_t)(bind->opaque_index_start * sizeof(uint32_t)); glDrawElementsBaseVertex( GL_TRIANGLES, bind->opaque_index_count, GL_UNSIGNED_INT, indices_offset, // Offset in EBO bind->vertex_start // Offset in VBO (baseVertex) ); TRX_GL_CheckError(); g_TRX_GL_Metrics.opaque_vert_count += bind->opaque_index_count; } static void M_DrawBlendAddVertices( const MESH_BATCHER *const batcher, const MESH_INSTANCE *const inst) { M_MESH_BUF_BINDING *const bind = M_GetBinding(batcher, inst->mesh); const void *indices_offset = (void *)(intptr_t)(bind->blend_add_index_start * sizeof(uint32_t)); glDrawElementsBaseVertex( GL_TRIANGLES, bind->blend_add_index_count, GL_UNSIGNED_INT, indices_offset, // Offset in EBO bind->vertex_start // Offset in VBO (baseVertex) ); TRX_GL_CheckError(); g_TRX_GL_Metrics.blend_add_vert_count += bind->blend_add_index_count; } static void M_DrawOpaqueInstance( MESH_BATCHER *const batcher, const MESH_INSTANCE *const inst) { M_MESH_BUF_BINDING *const bind = M_GetBinding(batcher, inst->mesh); ASSERT(bind != nullptr); M_SyncRoom(batcher, bind, inst->room); if (bind->needs_object_light) { Output_Uniforms_UploadCPULight(Output_GetUniforms(), &inst->light_info); } else if (bind->needs_own_light) { Output_Uniforms_UploadOwnLight(Output_GetUniforms(), &inst->light_info); } Output_MeshShader_UploadModelMatrix(batcher->shader, &inst->wmatrix); Output_MeshShader_UploadTint(batcher->shader, inst->tint); if (inst->enable_scissor) { Output_EnableScissor( inst->scissor.x, inst->scissor.y, inst->scissor.width, inst->scissor.height); } Output_MeshShader_UploadWaterEffect(batcher->shader, inst->water_effect); if (inst->wibble) { Output_MeshShader_UploadWibbleEffect(batcher->shader, false); glDepthMask(GL_FALSE); M_DrawOpaqueVertices(batcher, inst); glDepthMask(GL_TRUE); Output_MeshShader_UploadWibbleEffect(batcher->shader, true); M_DrawOpaqueVertices(batcher, inst); } else { Output_MeshShader_UploadWibbleEffect(batcher->shader, false); M_DrawOpaqueVertices(batcher, inst); } if (inst->enable_scissor) { Output_DisableScissor(); } } static void M_DrawBlendAddInstance( MESH_BATCHER *const batcher, const MESH_INSTANCE *const inst) { M_MESH_BUF_BINDING *const bind = M_GetBinding(batcher, inst->mesh); ASSERT(bind != nullptr); M_SyncRoom(batcher, bind, inst->room); if (bind->needs_object_light) { Output_Uniforms_UploadCPULight(Output_GetUniforms(), &inst->light_info); } else if (bind->needs_own_light) { Output_Uniforms_UploadOwnLight(Output_GetUniforms(), &inst->light_info); } Output_MeshShader_UploadModelMatrix(batcher->shader, &inst->wmatrix); Output_MeshShader_UploadTint(batcher->shader, inst->tint); Output_MeshShader_UploadWaterEffect(batcher->shader, inst->water_effect); Output_MeshShader_UploadWibbleEffect(batcher->shader, false); if (inst->enable_scissor) { Output_EnableScissor( inst->scissor.x, inst->scissor.y, inst->scissor.width, inst->scissor.height); } M_DrawBlendAddVertices(batcher, inst); if (inst->enable_scissor) { Output_DisableScissor(); } } static void M_OpaquePass(MESH_BATCHER *const batcher) { VECTOR *const staged = batcher->staged[SCENE_PASS_OPAQUE]; glBindVertexArray(batcher->vao); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, batcher->ebo.opaque); for (int32_t i = 0; i < staged->count; i++) { MESH_INSTANCE *const inst = Vector_Get(staged, i); const M_MESH_BUF_BINDING *const bind = M_GetBinding(batcher, inst->mesh); if (inst->mesh->opaque_vertex_indices->count != 0) { Output_AdjustDepth(0.0f, inst->depth_adjust * 2.0f / 0.005f); M_DrawOpaqueInstance(batcher, inst); } // Accumulate transparent polygons and faces. for (int32_t j = 0; j < inst->mesh->transparent_faces->count; j++) { Vector_Add( batcher->transparent_sort, &(M_FACE_SORT) { .inst = inst, .face = Vector_Get(inst->mesh->transparent_faces, j), .index_start = bind->transparent_index_start + bind->transparent_face_index_starts[j], .index_count = bind->transparent_face_index_counts[j], }); } } Output_AdjustDepth(0.0f, 0.0f); } static void M_BlendPass(MESH_BATCHER *const batcher, const SCENE_PASS pass) { VECTOR *const staged = batcher->staged[pass]; glBindVertexArray(batcher->vao); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, batcher->ebo.blend_add); for (int32_t i = 0; i < staged->count; i++) { const MESH_INSTANCE *const inst = Vector_Get(staged, i); const M_MESH_BUF_BINDING *const bind = M_GetBinding(batcher, inst->mesh); if (inst->mesh->blend_add_vertex_indices->count != 0) { Output_AdjustDepth(0.0f, inst->depth_adjust * 2.0f / 0.005f); M_DrawBlendAddInstance(batcher, inst); } } Output_AdjustDepth(0.0f, 0.0f); } static void M_TransparentPass(MESH_BATCHER *const batcher) { if (batcher->transparent_sort->count == 0) { return; } glBindVertexArray(batcher->vao); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, batcher->ebo.transparent); const MESH_INSTANCE *inst = nullptr; for (int32_t i = 0; i < batcher->transparent_sort->count; i++) { const M_FACE_SORT *const sort_ptr = Vector_Get(batcher->transparent_sort, i); if (sort_ptr->index_count == 0) { continue; } if (sort_ptr->inst != inst) { inst = sort_ptr->inst; const M_MESH_BUF_BINDING *const bind = M_GetBinding(batcher, inst->mesh); ASSERT(bind != nullptr); if (bind->needs_object_light) { Output_Uniforms_UploadCPULight( Output_GetUniforms(), &inst->light_info); } else if (bind->needs_own_light) { Output_Uniforms_UploadOwnLight( Output_GetUniforms(), &inst->light_info); } Output_MeshShader_UploadModelMatrix( batcher->shader, &inst->wmatrix); Output_MeshShader_UploadTint(batcher->shader, inst->tint); Output_MeshShader_UploadWaterEffect( batcher->shader, inst->water_effect); Output_MeshShader_UploadWibbleEffect(batcher->shader, inst->wibble); Output_AdjustDepth(0.0f, inst->depth_adjust * 2.0f / 0.005f); M_SyncRoom(batcher, bind, inst->room); } // indices live in the EBO starting at index_start const void *index_offset = (void *)(intptr_t)(sort_ptr->index_start * sizeof(uint32_t)); glDrawElements( GL_TRIANGLES, sort_ptr->index_count, GL_UNSIGNED_INT, index_offset); g_TRX_GL_Metrics.trans_vert_count += sort_ptr->index_count; } Output_AdjustDepth(0.0f, 0.0f); } static void M_RenderBegin(const SCENE_SOURCE *const source) { MESH_BATCHER *const batcher = source->priv; for (int32_t pass = 0; pass < SCENE_PASS_COUNT; pass++) { Vector_Clear(batcher->staged[pass]); } Vector_Clear(batcher->transparent_sort); } static void M_RenderPass( const SCENE_SOURCE *const source, const SCENE_PASS pass) { MESH_BATCHER *const batcher = source->priv; if (pass == SCENE_PASS_OPAQUE) { M_OpaquePass(batcher); } else if (pass == SCENE_PASS_TRANSPARENT) { M_SortTransparentFaces(batcher); M_TransparentPass(batcher); } else if (pass == SCENE_PASS_BLEND_SUB) { M_BlendPass(batcher, pass); } else if (pass == SCENE_PASS_BLEND_ADD) { M_BlendPass(batcher, pass); } } static bool M_IsDirty(const SCENE_SOURCE *const source, const SCENE_PASS pass) { const MESH_BATCHER *const batcher = source->priv; return batcher->staged[pass]->count > 0 || (batcher->transparent_sort->count > 0 && pass == SCENE_PASS_TRANSPARENT); } static void M_RecalculateLayout(MESH_BATCHER *const batcher) { batcher->vertex_count = 0; batcher->opaque_total_indices = 0; batcher->blend_add_total_indices = 0; batcher->transparent_total_indices = 0; for (int32_t i = 0; i < batcher->bindings->count; i++) { M_MESH_BUF_BINDING *const bind = *(M_MESH_BUF_BINDING **)Vector_Get(batcher->bindings, i); bind->vertex_start = batcher->vertex_count; batcher->vertex_count += bind->vertex_count; bind->opaque_index_start = batcher->opaque_total_indices; batcher->opaque_total_indices += bind->opaque_index_count; bind->blend_add_index_start = batcher->blend_add_total_indices; batcher->blend_add_total_indices += bind->blend_add_index_count; bind->transparent_index_start = batcher->transparent_total_indices; batcher->transparent_total_indices += bind->transparent_index_count; } batcher->layout_dirty = false; } static void M_AnimateTextures(const SCENE_SOURCE *const source) { MESH_BATCHER *const batcher = source->priv; for (int32_t i = 0; i < batcher->bindings->count; i++) { M_MESH_BUF_BINDING *const bind = *(M_MESH_BUF_BINDING **)Vector_Get(batcher->bindings, i); M_AnimateBinding(batcher, bind); } } MESH_BATCHER *MeshBatcher_Create(void) { MESH_BATCHER *const batcher = Memory_Alloc(sizeof(MESH_BATCHER)); batcher->shader = Output_GetMeshShader(); batcher->bindings = Vector_Create(sizeof(OUTPUT_MESH *)); batcher->binding_map = nullptr; for (int32_t pass = 0; pass < SCENE_PASS_COUNT; pass++) { batcher->staged[pass] = Vector_Create(sizeof(MESH_INSTANCE)); } batcher->source.render_begin = M_RenderBegin; batcher->source.render_pass = M_RenderPass; batcher->source.is_dirty = M_IsDirty; batcher->source.animate_textures = M_AnimateTextures; batcher->source.priv = batcher; batcher->transparent_sort = Vector_Create(sizeof(M_FACE_SORT)); batcher->layout_dirty = true; glGenVertexArrays(1, &batcher->vao); glGenBuffers(3, &batcher->vbo.geom); glBindVertexArray(batcher->vao); glBindBuffer(GL_ARRAY_BUFFER, batcher->vbo.geom); glEnableVertexAttribArray(OUTPUT_MESH_ATTR_POS); glEnableVertexAttribArray(OUTPUT_MESH_ATTR_NORMAL); glEnableVertexAttribArray(OUTPUT_MESH_ATTR_FLAGS); glEnableVertexAttribArray(OUTPUT_MESH_ATTR_COLOR); glVertexAttribPointer( OUTPUT_MESH_ATTR_POS, 4, GL_FLOAT, GL_FALSE, sizeof(M_MESH_GEOM), (void *)(intptr_t)offsetof(M_MESH_GEOM, pos)); glVertexAttribPointer( OUTPUT_MESH_ATTR_NORMAL, 4, GL_FLOAT, GL_FALSE, sizeof(M_MESH_GEOM), (void *)(intptr_t)offsetof(M_MESH_GEOM, normal)); glVertexAttribIPointer( OUTPUT_MESH_ATTR_FLAGS, 1, OUTPUT_USHORT_GL, sizeof(M_MESH_GEOM), (void *)(intptr_t)offsetof(M_MESH_GEOM, flags)); glVertexAttribPointer( OUTPUT_MESH_ATTR_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(M_MESH_GEOM), (void *)(intptr_t)offsetof(M_MESH_GEOM, color)); glBindBuffer(GL_ARRAY_BUFFER, batcher->vbo.tex); glEnableVertexAttribArray(OUTPUT_MESH_ATTR_UVW); glEnableVertexAttribArray(OUTPUT_MESH_ATTR_TEXTURE_SIZE); glEnableVertexAttribArray(OUTPUT_MESH_ATTR_TRAPEZOID_RATIO); glVertexAttribPointer( OUTPUT_MESH_ATTR_UVW, 3, GL_FLOAT, GL_FALSE, sizeof(M_MESH_TEXTURE), (void *)(intptr_t)offsetof(M_MESH_TEXTURE, uvw)); glVertexAttribPointer( OUTPUT_MESH_ATTR_TEXTURE_SIZE, 4, GL_FLOAT, GL_FALSE, sizeof(M_MESH_TEXTURE), (void *)(intptr_t)offsetof(M_MESH_TEXTURE, texture_size)); glVertexAttribPointer( OUTPUT_MESH_ATTR_TRAPEZOID_RATIO, 2, GL_FLOAT, GL_FALSE, sizeof(M_MESH_TEXTURE), (void *)(intptr_t)offsetof(M_MESH_TEXTURE, trapezoid_ratio)); glBindBuffer(GL_ARRAY_BUFFER, batcher->vbo.shade); glEnableVertexAttribArray(OUTPUT_MESH_ATTR_SHADE); glVertexAttribPointer( OUTPUT_MESH_ATTR_SHADE, 1, GL_FLOAT, GL_FALSE, sizeof(M_MESH_SHADE), 0); glGenBuffers(3, &batcher->ebo.opaque); return batcher; } void MeshBatcher_Destroy(MESH_BATCHER *const batcher) { glBindVertexArray(0); glBindBuffer(GL_ARRAY_BUFFER, 0); if (batcher->vao != 0) { glDeleteVertexArrays(1, &batcher->vao); batcher->vao = 0; } if (batcher->vbo.geom != 0) { glDeleteBuffers(3, &batcher->vbo.geom); batcher->vbo.geom = 0; batcher->vbo.tex = 0; batcher->vbo.shade = 0; } if (batcher->ebo.opaque != 0) { glDeleteBuffers(3, &batcher->ebo.opaque); batcher->ebo.opaque = 0; batcher->ebo.transparent = 0; batcher->ebo.blend_add = 0; } ASSERT(batcher->bindings->count == 0); if (batcher->bindings != nullptr) { Vector_Free(batcher->bindings); batcher->bindings = nullptr; } if (batcher->transparent_sort != nullptr) { Vector_Free(batcher->transparent_sort); batcher->transparent_sort = nullptr; } for (int32_t pass = 0; pass < SCENE_PASS_COUNT; pass++) { if (batcher->staged[pass] != nullptr) { Vector_Free(batcher->staged[pass]); batcher->staged[pass] = nullptr; } } Memory_Free(batcher); } void MeshBatcher_RemoveMesh( MESH_BATCHER *const batcher, OUTPUT_MESH *const mesh) { M_MESH_BUF_BINDING *const bind = M_GetBinding(batcher, mesh); if (bind == nullptr) { return; } Memory_Free(bind->geom_data); Memory_Free(bind->tex_data); Memory_Free(bind->shade_data); Memory_Free(bind->transparent_face_index_starts); Memory_Free(bind->transparent_face_index_counts); Vector_Remove(batcher->bindings, &bind); HASH_DEL(batcher->binding_map, bind); batcher->layout_dirty = true; Memory_Free(bind); } void MeshBatcher_AddMesh(MESH_BATCHER *const batcher, OUTPUT_MESH *const mesh) { ASSERT(mesh->sealed == 1); M_MESH_BUF_BINDING *const bind = Memory_Alloc(sizeof(M_MESH_BUF_BINDING)); bind->mesh = mesh; bind->vertex_count = mesh->vertices->count; // 1. Copy Vertex Data const OUTPUT_MESH_VERTEX *const vertices = Vector_GetData(mesh->vertices); bind->geom_data = Memory_Alloc(sizeof(M_MESH_GEOM) * bind->vertex_count); bind->tex_data = Memory_Alloc(sizeof(M_MESH_TEXTURE) * bind->vertex_count); bind->shade_data = Memory_Alloc(sizeof(M_MESH_SHADE) * bind->vertex_count); for (int32_t i = 0; i < bind->vertex_count; i++) { M_FillGeometry(&bind->geom_data[i], &vertices[i]); M_FillTexture(&bind->tex_data[i], &vertices[i]); M_FillShade(&bind->shade_data[i], &vertices[i]); if ((vertices[i].flags & VERT_USE_DYNAMIC_LIGHT) != 0) { bind->needs_room_lights = true; } if ((vertices[i].flags & VERT_USE_OBJECT_LIGHT) != 0) { bind->needs_object_light = true; bind->needs_cpu_light = true; } if ((vertices[i].flags & VERT_USE_OWN_LIGHT) != 0) { bind->needs_own_light = true; bind->needs_cpu_light = true; } } // 2. Prepare index counts // Opaque bind->opaque_index_count = mesh->opaque_vertex_indices->count; // Blend/Add bind->blend_add_index_count = mesh->blend_add_vertex_indices->count; // Transparent bind->transparent_face_count = mesh->transparent_faces->count; bind->transparent_face_index_starts = nullptr; bind->transparent_face_index_counts = nullptr; bind->transparent_index_count = 0; if (bind->transparent_face_count > 0) { bind->transparent_face_index_starts = Memory_Alloc(sizeof(int32_t) * bind->transparent_face_count); bind->transparent_face_index_counts = Memory_Alloc(sizeof(int32_t) * bind->transparent_face_count); for (int32_t i = 0; i < bind->transparent_face_count; i++) { const OUTPUT_MESH_FACE *const face = Vector_Get(mesh->transparent_faces, i); bind->transparent_face_index_starts[i] = bind->transparent_index_count; bind->transparent_face_index_counts[i] = face->vertex_count; bind->transparent_index_count += face->vertex_count; } } // Prevent double add mesh->sealed = 2; Vector_Add(batcher->bindings, &bind); HASH_ADD_PTR(batcher->binding_map, mesh, bind); batcher->layout_dirty = true; } void MeshBatcher_Seal(MESH_BATCHER *const batcher) { if (batcher->layout_dirty) { M_RecalculateLayout(batcher); } glBindBuffer(GL_ARRAY_BUFFER, batcher->vbo.geom); TRX_GL_TRACK_DATA( glBufferData, GL_ARRAY_BUFFER, batcher->vertex_count * sizeof(M_MESH_GEOM), nullptr, GL_DYNAMIC_DRAW); // allow updating mesh flags glBindBuffer(GL_ARRAY_BUFFER, batcher->vbo.tex); TRX_GL_TRACK_DATA( glBufferData, GL_ARRAY_BUFFER, batcher->vertex_count * sizeof(M_MESH_TEXTURE), nullptr, GL_DYNAMIC_DRAW); // allow animating textures glBindBuffer(GL_ARRAY_BUFFER, batcher->vbo.shade); TRX_GL_TRACK_DATA( glBufferData, GL_ARRAY_BUFFER, batcher->vertex_count * sizeof(M_MESH_SHADE), nullptr, GL_DYNAMIC_DRAW); // Upload vertex data for (int32_t i = 0; i < batcher->bindings->count; i++) { M_MESH_BUF_BINDING *const bind = *(M_MESH_BUF_BINDING **)Vector_Get(batcher->bindings, i); glBindBuffer(GL_ARRAY_BUFFER, batcher->vbo.geom); TRX_GL_TRACK_SUBDATA( glBufferSubData, GL_ARRAY_BUFFER, bind->vertex_start * sizeof(M_MESH_GEOM), bind->vertex_count * sizeof(M_MESH_GEOM), bind->geom_data); glBindBuffer(GL_ARRAY_BUFFER, batcher->vbo.tex); TRX_GL_TRACK_SUBDATA( glBufferSubData, GL_ARRAY_BUFFER, bind->vertex_start * sizeof(M_MESH_TEXTURE), bind->vertex_count * sizeof(M_MESH_TEXTURE), bind->tex_data); glBindBuffer(GL_ARRAY_BUFFER, batcher->vbo.shade); TRX_GL_TRACK_SUBDATA( glBufferSubData, GL_ARRAY_BUFFER, bind->vertex_start * sizeof(M_MESH_SHADE), bind->vertex_count * sizeof(M_MESH_SHADE), bind->shade_data); } // Allocate CPU scratch memory for the combined indices uint32_t *opaque_indices = Memory_Alloc(batcher->opaque_total_indices * sizeof(uint32_t)); uint32_t *blend_indices = Memory_Alloc(batcher->blend_add_total_indices * sizeof(uint32_t)); uint32_t *transparent_indices = Memory_Alloc(batcher->transparent_total_indices * sizeof(uint32_t)); // Flatten the data for (int32_t i = 0; i < batcher->bindings->count; i++) { M_MESH_BUF_BINDING *const bind = *(M_MESH_BUF_BINDING **)Vector_Get(batcher->bindings, i); // Copy Opaque Indices if (bind->opaque_index_count > 0) { memcpy( &opaque_indices[bind->opaque_index_start], Vector_GetData(bind->mesh->opaque_vertex_indices), bind->opaque_index_count * sizeof(uint32_t)); } // Copy Blend Indices if (bind->blend_add_index_count > 0) { memcpy( &blend_indices[bind->blend_add_index_start], Vector_GetData(bind->mesh->blend_add_vertex_indices), bind->blend_add_index_count * sizeof(uint32_t)); } // Copy Transparent Indices if (bind->transparent_index_count > 0) { for (int32_t j = 0; j < bind->transparent_face_count; j++) { const OUTPUT_MESH_FACE *const face = Vector_Get(bind->mesh->transparent_faces, j); const int32_t dst_start = bind->transparent_index_start + bind->transparent_face_index_starts[j]; for (int32_t k = 0; k < face->vertex_count; k++) { transparent_indices[dst_start + k] = bind->vertex_start + face->vertex_indices[k]; } } } } // Upload to GPU glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, batcher->ebo.opaque); TRX_GL_TRACK_DATA( glBufferData, GL_ELEMENT_ARRAY_BUFFER, batcher->opaque_total_indices * sizeof(uint32_t), opaque_indices, GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, batcher->ebo.blend_add); TRX_GL_TRACK_DATA( glBufferData, GL_ELEMENT_ARRAY_BUFFER, batcher->blend_add_total_indices * sizeof(uint32_t), blend_indices, GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, batcher->ebo.transparent); TRX_GL_TRACK_DATA( glBufferData, GL_ELEMENT_ARRAY_BUFFER, batcher->transparent_total_indices * sizeof(uint32_t), transparent_indices, GL_STATIC_DRAW); Memory_FreePointer(&opaque_indices); Memory_FreePointer(&blend_indices); Memory_FreePointer(&transparent_indices); } void MeshBatcher_UpdateMeshGeometry( const MESH_BATCHER *const batcher, const OUTPUT_MESH *const mesh) { if (mesh == nullptr) { return; } glBindBuffer(GL_ARRAY_BUFFER, batcher->vbo.geom); M_UpdateMeshGeometry(batcher, mesh); } void MeshBatcher_Stage( MESH_BATCHER *const batcher, const MESH_INSTANCE *const inst, const SCENE_PASS pass) { if (inst->mesh == nullptr) { return; } Vector_Add(batcher->staged[pass], inst); } const SCENE_SOURCE *MeshBatcher_AsSource(const MESH_BATCHER *const batcher) { return &batcher->source; } ================================================ FILE: src/trx/game/output/mesh_batcher/batcher.h ================================================ #pragma once #include #include #include #include #include #include typedef struct MESH_INSTANCE { OUTPUT_MESH *mesh; // TODO: use gl_InstanceID some day for this // and glMultiDrawArraysIndirect MATRIX cwmatrix; MATRIX wmatrix; const ROOM *room; RGB_F tint; bool wibble; int32_t water_effect; OUTPUT_LIGHT_INFO light_info; bool enable_scissor; bool disable_z_writes; float depth_adjust; VIEWPORT_RECT scissor; } MESH_INSTANCE; typedef struct MESH_BATCHER MESH_BATCHER; MESH_BATCHER *MeshBatcher_Create(void); void MeshBatcher_Destroy(struct MESH_BATCHER *batcher); void MeshBatcher_AddMesh(struct MESH_BATCHER *batcher, OUTPUT_MESH *mesh); void MeshBatcher_RemoveMesh(struct MESH_BATCHER *batcher, OUTPUT_MESH *mesh); void MeshBatcher_Seal(MESH_BATCHER *batcher); const SCENE_SOURCE *MeshBatcher_AsSource(const struct MESH_BATCHER *batcher); void MeshBatcher_Stage( struct MESH_BATCHER *batcher, const MESH_INSTANCE *inst, SCENE_PASS pass); void MeshBatcher_UpdateMeshGeometry( const struct MESH_BATCHER *batcher, const OUTPUT_MESH *mesh); ================================================ FILE: src/trx/game/output/mesh_batcher/mesh.c ================================================ #include #include #include OUTPUT_MESH *Output_Mesh_Create(void) { OUTPUT_MESH *const mesh = Memory_Alloc(sizeof(OUTPUT_MESH)); Memory_ArenaReset(&mesh->allocator); mesh->vertices = Vector_Create(sizeof(OUTPUT_MESH_VERTEX)); mesh->animated_vertices = Vector_Create(sizeof(OUTPUT_VERTEX_RANGE)); mesh->transparent_faces = Vector_Create(sizeof(OUTPUT_MESH_FACE)); mesh->opaque_vertex_indices = Vector_Create(sizeof(uint32_t)); mesh->blend_add_vertex_indices = Vector_Create(sizeof(uint32_t)); mesh->sealed = false; return mesh; } void Output_Mesh_Destroy(OUTPUT_MESH *const mesh) { if (mesh->animated_vertices != nullptr) { Vector_Free(mesh->animated_vertices); } Vector_Free(mesh->vertices); if (mesh->transparent_faces != nullptr) { Vector_Free(mesh->transparent_faces); } if (mesh->opaque_vertex_indices != nullptr) { Vector_Free(mesh->opaque_vertex_indices); } if (mesh->blend_add_vertex_indices != nullptr) { Vector_Free(mesh->blend_add_vertex_indices); } Memory_ArenaFree(&mesh->allocator); Memory_Free(mesh); } ================================================ FILE: src/trx/game/output/mesh_batcher/mesh.h ================================================ #pragma once #include #include #include #include #include typedef struct { // attribute 2 OUTPUT_UVW uvw; // attribute 3 OUTPUT_TEXTURE_SIZE texture_size; // attribute 4 float trapezoid_ratio[2]; } OUTPUT_MESH_TEXTURE; typedef struct { XYZW_F pos; XYZ_F normal; uint16_t flags; int32_t uvw_idx; float trapezoid_ratio[2]; int16_t shade; RGBA_8888 color; uint8_t light_table_idx; } OUTPUT_MESH_VERTEX; // Describes a contiguous block of vertices belonging to one face, // with sort keys. typedef struct { int32_t vertex_count; int32_t *vertex_indices; XYZ_F mesh_centroid; } OUTPUT_MESH_FACE; typedef struct { VECTOR *vertices; MEMORY_ARENA_ALLOCATOR allocator; int32_t sealed; VECTOR *animated_vertices; // OUTPUT_VERTEX_RANGE VECTOR *transparent_faces; // OUTPUT_MESH_FACE VECTOR *opaque_vertex_indices; // uint32_t VECTOR *blend_add_vertex_indices; // uint32_t } OUTPUT_MESH; OUTPUT_MESH *Output_Mesh_Create(void); void Output_Mesh_Destroy(OUTPUT_MESH *mesh); ================================================ FILE: src/trx/game/output/mesh_batcher/mesh_builder.c ================================================ #include #include #include #include #include #include #include #include struct MESH_BUILDER { size_t pending_vertex_count; OUTPUT_MESH *mesh; VECTOR *indices; }; static void M_EnsureMesh(MESH_BUILDER *const builder) { if (builder->mesh == nullptr) { builder->mesh = Output_Mesh_Create(); builder->pending_vertex_count = 0; } } static void M_AddAnimatedVertexRanges( OUTPUT_MESH *const mesh, const OUTPUT_MESH_VERTEX *const vertices, const size_t vertex_count, const size_t vertex_start) { size_t range_start = 0; size_t range_count = 0; for (size_t i = 0; i < vertex_count; i++) { const bool animated = !(vertices[i].flags & VERT_FLAT_SHADED) && Output_Textures_IsObjectTextureAnimated(vertices[i].uvw_idx / 4); if (!animated) { if (range_count > 0) { Vector_Add( mesh->animated_vertices, &(OUTPUT_VERTEX_RANGE) { .vertex_start = vertex_start + range_start, .vertex_count = range_count, }); range_count = 0; } continue; } if (range_count == 0) { range_start = i; } range_count++; } if (range_count > 0) { Vector_Add( mesh->animated_vertices, &(OUTPUT_VERTEX_RANGE) { .vertex_start = vertex_start + range_start, .vertex_count = range_count, }); } } static void M_FillFanIndices( VECTOR *const indices, const size_t vertex_count, const bool double_sided) { ASSERT(vertex_count >= 3); const size_t tri_count = vertex_count - 2; const size_t index_count = tri_count * 3 * (double_sided ? 2 : 1); int32_t *const out = Vector_Expand(indices, index_count); for (size_t i = 0, j = 0; i < tri_count; i++) { out[j++] = 0; out[j++] = i + 2; out[j++] = i + 1; if (double_sided) { out[j++] = i + 1; out[j++] = i + 2; out[j++] = 0; } } } MESH_BUILDER *MeshBuilder_Create(void) { MESH_BUILDER *const builder = Memory_Alloc(sizeof(*builder)); builder->indices = Vector_Create(sizeof(int32_t)); return builder; } void MeshBuilder_Destroy(MESH_BUILDER *const builder) { ASSERT(builder != nullptr); if (builder->mesh != nullptr) { Output_Mesh_Destroy(builder->mesh); builder->mesh = nullptr; } if (builder->indices != nullptr) { Vector_Free(builder->indices); builder->indices = nullptr; } Memory_Free(builder); } void MeshBuilder_AddVertex( MESH_BUILDER *const builder, const OUTPUT_MESH_VERTEX *const vertex) { MeshBuilder_AddVertices(builder, vertex, 1); } void MeshBuilder_AddVertices( MESH_BUILDER *const builder, const OUTPUT_MESH_VERTEX *const vertices, const size_t vertex_count) { ASSERT(builder != nullptr); ASSERT(vertex_count > 0); M_EnsureMesh(builder); ASSERT(builder->mesh != nullptr); ASSERT(!builder->mesh->sealed); const size_t vertex_start = builder->mesh->vertices->count; memcpy( Vector_Expand(builder->mesh->vertices, vertex_count), vertices, sizeof(OUTPUT_MESH_VERTEX) * vertex_count); M_AddAnimatedVertexRanges( builder->mesh, vertices, vertex_count, vertex_start); builder->pending_vertex_count += vertex_count; } void MeshBuilder_AddFace( MESH_BUILDER *const builder, const SCENE_PASS pass, const int32_t *indices, const size_t idx_count) { ASSERT(builder != nullptr); ASSERT( (pass == SCENE_PASS_TRANSPARENT) || (pass == SCENE_PASS_OPAQUE) || (pass == SCENE_PASS_BLEND_SUB) || (pass == SCENE_PASS_BLEND_ADD)); M_EnsureMesh(builder); ASSERT(builder->mesh != nullptr); ASSERT(!builder->mesh->sealed); const size_t vtx_count = builder->pending_vertex_count; const size_t start = builder->mesh->vertices->count - vtx_count; const OUTPUT_MESH_VERTEX *const vbuf = Vector_GetData(builder->mesh->vertices); XYZ_F centroid = { 0.0f, 0.0f, 0.0f }; for (size_t i = 0; i < vtx_count; i++) { centroid.x += vbuf[start + i].pos.x; centroid.y += vbuf[start + i].pos.y; centroid.z += vbuf[start + i].pos.z; } centroid.x /= (float)vtx_count; centroid.y /= (float)vtx_count; centroid.z /= (float)vtx_count; if (pass == SCENE_PASS_TRANSPARENT) { OUTPUT_MESH_FACE face = { .vertex_count = idx_count, .mesh_centroid = centroid, }; face.vertex_indices = Memory_ArenaAlloc( &builder->mesh->allocator, sizeof(int32_t) * idx_count); for (size_t i = 0; i < idx_count; i++) { face.vertex_indices[i] = start + indices[i]; } Vector_Add(builder->mesh->transparent_faces, &face); } VECTOR *const target = pass == SCENE_PASS_BLEND_ADD ? builder->mesh->blend_add_vertex_indices : builder->mesh->opaque_vertex_indices; uint32_t *const out = Vector_Expand(target, idx_count); for (size_t i = 0; i < idx_count; i++) { out[i] = start + indices[i]; } builder->pending_vertex_count = 0; } void MeshBuilder_AddFan( MESH_BUILDER *const builder, const SCENE_PASS pass, const bool double_sided) { ASSERT(builder != nullptr); M_EnsureMesh(builder); const size_t vtx_count = builder->pending_vertex_count; ASSERT(vtx_count >= 3); M_FillFanIndices(builder->indices, vtx_count, double_sided); MeshBuilder_AddFace( builder, pass, Vector_GetData(builder->indices), builder->indices->count); Vector_Clear(builder->indices); } void MeshBuilder_AddRoomSprite( MESH_BUILDER *const builder, const ROOM_SPRITE *const room_sprite, const ROOM *const room, const float depth_adjust, const uint16_t extra_flags) { const int16_t texture_idx = room_sprite->texture; const SPRITE_TEXTURE *const sprite = Output_GetSpriteTexture(texture_idx); const ROOM_VERTEX *const room_vert = &room->mesh.vertices[room_sprite->vertex]; const XYZ_16 *pos = &room_vert->pos; const struct { float x, y; } normal[4] = { { .x = sprite->x0, .y = sprite->y0 }, { .x = sprite->x1, .y = sprite->y0 }, { .x = sprite->x1, .y = sprite->y1 }, { .x = sprite->x0, .y = sprite->y1 }, }; for (int32_t j = 0; j < 4; j++) { const OUTPUT_MESH_VERTEX vertex = { .pos = { .x = pos->x, .y = pos->y, .z = pos->z, .w = depth_adjust }, .normal = { .x = normal[j].x, .y = normal[j].y, .z = 0.0f }, .flags = Output_Textures_GetSpriteTextureFlags(texture_idx) | extra_flags, .color = { 255, 255, 255, 255 }, .uvw_idx = Output_Textures_GetSpriteUVWIndex(texture_idx, j), .shade = room_vert->light_base, .trapezoid_ratio = { 1.0f, 1.0f }, }; MeshBuilder_AddVertex(builder, &vertex); } MeshBuilder_AddFan(builder, SCENE_PASS_TRANSPARENT, false); } void MeshBuilder_AdjustDepth(MESH_BUILDER *const builder, const float depth) { if (builder->mesh == nullptr || builder->mesh->vertices == nullptr) { return; } OUTPUT_MESH_VERTEX *const vbuf = Vector_GetData(builder->mesh->vertices); for (int32_t i = 0; i < builder->mesh->vertices->count; i++) { vbuf[i].pos.w = depth; } } OUTPUT_MESH *MeshBuilder_Seal(MESH_BUILDER *const builder) { ASSERT(builder != nullptr); if (builder->mesh == nullptr) { return nullptr; } OUTPUT_MESH *const mesh = builder->mesh; Output_GlueVertexRanges(mesh->animated_vertices); mesh->sealed = 1; builder->mesh = nullptr; return mesh; } ================================================ FILE: src/trx/game/output/mesh_batcher/mesh_builder.h ================================================ #pragma once #include #include #include // Opaque builder for incrementally constructing an OUTPUT_MESH. typedef struct MESH_BUILDER MESH_BUILDER; // Create a new mesh builder. Call MeshBuilder_Seal() when done, then // MeshBuilder_Destroy(). MESH_BUILDER *MeshBuilder_Create(void); // Destroy the builder state. Does NOT free the emitted OUTPUT_MESHes. void MeshBuilder_Destroy(MESH_BUILDER *builder); // Append one vertex to the mesh under construction. // Must be called before adding faces for those vertices. void MeshBuilder_AddVertex( MESH_BUILDER *builder, const OUTPUT_MESH_VERTEX *vertex); void MeshBuilder_AddVertices( MESH_BUILDER *builder, const OUTPUT_MESH_VERTEX *vertices, size_t vertex_count); // Add a face using the recently added vertices. void MeshBuilder_AddFace( MESH_BUILDER *builder, SCENE_PASS pass, const int32_t *indices, size_t idx_count); // Add a triangle fan face using the last vertices added: a center followed by // ring vertices.If double_sided is true, generates mirrored winding // for backfaces as well. void MeshBuilder_AddFan( MESH_BUILDER *builder, SCENE_PASS pass, bool double_sided); // Applies invisible z offset to all vertices that helps with the z-fighting. void MeshBuilder_AdjustDepth(MESH_BUILDER *builder, float depth); // Finalize all pending vertices and faces into the OUTPUT_MESH and seal it. // Returns the sealed mesh; builder must still be destroyed via // MeshBuilder_Destroy(). OUTPUT_MESH *MeshBuilder_Seal(MESH_BUILDER *builder); // Utility method to add a quad representing a room sprite. void MeshBuilder_AddRoomSprite( MESH_BUILDER *builder, const ROOM_SPRITE *room_sprite, const ROOM *room, float depth_adjust, uint16_t extra_flags); ================================================ FILE: src/trx/game/output/overlay.h ================================================ #pragma once void Output_Overlay_DrawGame(void); void Output_Overlay_DrawGameMono(float desaturation); void Output_Overlay_DrawGameMonoCool(float desaturation); void Output_Overlay_DrawGameMonoWarm(float desaturation); void Output_Overlay_DrawPattern(bool wave); void Output_Overlay_DrawPatternOpacity(bool wave, float opacity); void Output_Overlay_DrawBlackRectangle(float opacity, bool post_ui); bool Output_Overlay_LoadImage(const char *file_name); void Output_Overlay_DrawImage(const char *file_name); void Output_Overlay_DrawImageBilinear(const char *file_name); void Output_Overlay_DrawImageMono(const char *file_name, float intensity); void Output_Overlay_CaptureSnapshot(void); void Output_Overlay_DrawSnapshot(float opacity); void Output_Overlay_BeginTransitionFadeOut(float duration, float start); ================================================ FILE: src/trx/game/output/quad.c ================================================ #include #include #include #include #include #include #include #include typedef enum { M_UNIFORM_TEXTURE_MAIN, M_UNIFORM_TEXTURE_SIZE, M_UNIFORM_EFFECT, M_UNIFORM_OPACITY, M_UNIFORM_BRIGHTNESS_SCALE, M_UNIFORM_FIT_MODE, M_UNIFORM_SRC_ASPECT, M_UNIFORM_NUMBER_OF, } M_UNIFORM; typedef struct { struct { GLfloat x; GLfloat y; } pos; struct { GLfloat u; GLfloat v; } uv; } M_VERTEX; struct OUTPUT_QUAD { GLuint vao; GLuint vbo; GLuint texture; OUTPUT_SHADER *shader; M_VERTEX *vertices; int32_t vertex_count; bool ready; OUTPUT_QUAD_SURFACE_DESC desc; struct { int32_t x; int32_t y; } repeat; OUTPUT_QUAD_EFFECT effect; float opacity; float brightness_scale; TEXTURE_FILTER filter_mode; OUTPUT_QUAD_FIT_MODE fit_mode; float src_aspect; bool use_external_texture; GLuint external_texture_id; GLint loc[M_UNIFORM_NUMBER_OF]; }; static const M_VERTEX m_Vertices[] = { { .pos = { .x = 0.0, .y = 0.0 }, .uv = { .u = 0.0, .v = 0.0 } }, { .pos = { .x = 1.0, .y = 0.0 }, .uv = { .u = 1.0, .v = 0.0 } }, { .pos = { .x = 0.0, .y = 1.0 }, .uv = { .u = 0.0, .v = 1.0 } }, { .pos = { .x = 0.0, .y = 1.0 }, .uv = { .u = 0.0, .v = 1.0 } }, { .pos = { .x = 1.0, .y = 0.0 }, .uv = { .u = 1.0, .v = 0.0 } }, { .pos = { .x = 1.0, .y = 1.0 }, .uv = { .u = 1.0, .v = 1.0 } }, }; static const OUTPUT_QUAD_SURFACE_UV m_DefaultUV[] = { { .u = 0.0f, .v = 0.0f }, { .u = 1.0f, .v = 0.0f }, { .u = 1.0f, .v = 1.0f }, { .u = 0.0f, .v = 1.0f }, }; static bool M_AllUVsZero(const OUTPUT_QUAD_SURFACE_DESC *const desc) { for (int32_t i = 0; i < 4; i++) { if (desc->uv[i].u != 0.0f || desc->uv[i].v != 0.0f) { return false; } } return true; } static OUTPUT_QUAD_SURFACE_DESC M_NormalizeDesc( const OUTPUT_QUAD_SURFACE_DESC *const desc) { OUTPUT_QUAD_SURFACE_DESC out = *desc; if (M_AllUVsZero(desc)) { memcpy(out.uv, m_DefaultUV, sizeof(m_DefaultUV)); } return out; } static void M_BindProgram(const OUTPUT_QUAD *const r) { Output_Shader_Bind(r->shader); } static void M_UploadVertices(OUTPUT_QUAD *const r) { if (!r->ready) { return; } const int32_t mapping[] = { 0, 1, 3, 3, 1, 2 }; r->vertex_count = r->repeat.x * r->repeat.y * 6; r->vertices = Memory_Realloc( r->vertices, r->repeat.x * r->repeat.y * 6 * sizeof(M_VERTEX)); M_VERTEX *ptr = r->vertices; for (int32_t y = 0; y < r->repeat.y; y++) { for (int32_t x = 0; x < r->repeat.x; x++) { for (int32_t i = 0; i < 6; i++) { const float x_factor = (float)x / (float)r->repeat.x; const float y_factor = (float)y / (float)r->repeat.y; const float x_offset = 1.0f / (float)r->repeat.x; const float y_offset = 1.0f / (float)r->repeat.y; ptr->pos.x = m_Vertices[i].pos.x * x_offset + x_factor; ptr->pos.y = m_Vertices[i].pos.y * y_offset + y_factor; ptr->uv.u = r->desc.uv[mapping[i]].u; ptr->uv.v = r->desc.uv[mapping[i]].v; ptr++; } } } glBindBuffer(GL_ARRAY_BUFFER, r->vbo); glBufferData( GL_ARRAY_BUFFER, sizeof(M_VERTEX) * 6 * r->repeat.x * r->repeat.y, r->vertices, GL_STATIC_DRAW); TRX_GL_CheckError(); } OUTPUT_QUAD *Output_Quad_Create(void) { OUTPUT_QUAD *const r = Memory_Alloc(sizeof(OUTPUT_QUAD)); r->effect = OUTPUT_QUAD_EFFECT_NONE; r->opacity = 1.0f; r->brightness_scale = 1.0f; r->filter_mode = TEXTURE_FILTER_POINT; r->repeat.x = 1; r->repeat.y = 1; r->fit_mode = OUTPUT_QUAD_FIT_STRETCH; r->src_aspect = 1.0f; r->vertices = nullptr; r->vertex_count = 6; r->use_external_texture = false; r->external_texture_id = 0; glGenBuffers(1, &r->vbo); glBindBuffer(GL_ARRAY_BUFFER, r->vbo); glBufferData( GL_ARRAY_BUFFER, sizeof(m_Vertices), m_Vertices, GL_STATIC_DRAW); glGenVertexArrays(1, &r->vao); glBindVertexArray(r->vao); glBindBuffer(GL_ARRAY_BUFFER, r->vbo); glEnableVertexAttribArray(0); glVertexAttribPointer( 0, 2, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX), (void *)offsetof(M_VERTEX, pos)); glEnableVertexAttribArray(1); glVertexAttribPointer( 1, 2, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX), (void *)offsetof(M_VERTEX, uv)); TRX_GL_CheckError(); glGenTextures(1, &r->texture); TRX_GL_CheckError(); r->shader = Output_Shader_Create("2d.glsl"); r->loc[M_UNIFORM_TEXTURE_MAIN] = Output_Shader_LookupUniform(r->shader, "uTexMain"); r->loc[M_UNIFORM_TEXTURE_SIZE] = Output_Shader_LookupUniform(r->shader, "uTexSize"); r->loc[M_UNIFORM_EFFECT] = Output_Shader_LookupUniform(r->shader, "uEffect"); r->loc[M_UNIFORM_OPACITY] = Output_Shader_LookupUniform(r->shader, "uOpacity"); r->loc[M_UNIFORM_BRIGHTNESS_SCALE] = Output_Shader_LookupUniform(r->shader, "uBrightnessScale"); r->loc[M_UNIFORM_FIT_MODE] = Output_Shader_LookupUniform(r->shader, "uFitMode"); r->loc[M_UNIFORM_SRC_ASPECT] = Output_Shader_LookupUniform(r->shader, "uSrcAspect"); M_BindProgram(r); glUniform1i(r->loc[M_UNIFORM_TEXTURE_MAIN], 0); glUniform4f(r->loc[M_UNIFORM_TEXTURE_SIZE], 0.0f, 0.0f, 1.0f, 1.0f); glUniform1i(r->loc[M_UNIFORM_EFFECT], r->effect); glUniform1f(r->loc[M_UNIFORM_OPACITY], r->opacity); glUniform1f(r->loc[M_UNIFORM_BRIGHTNESS_SCALE], r->brightness_scale); glUniform1i(r->loc[M_UNIFORM_FIT_MODE], (int32_t)r->fit_mode); glUniform1f(r->loc[M_UNIFORM_SRC_ASPECT], r->src_aspect); TRX_GL_CheckError(); return r; } void Output_Quad_Destroy(OUTPUT_QUAD *const r) { ASSERT(r != nullptr); if (r->vao != 0) { glDeleteVertexArrays(1, &r->vao); } if (r->vbo != 0) { glDeleteBuffers(1, &r->vbo); } if (r->texture != 0) { glDeleteTextures(1, &r->texture); } TRX_GL_CheckError(); if (r->shader != nullptr) { Output_Shader_Free(r->shader); } Memory_FreePointer(&r->vertices); Memory_Free(r); } void Output_Quad_Upload( OUTPUT_QUAD *const r, const OUTPUT_QUAD_SURFACE_DESC *const desc, const uint8_t *const data) { ASSERT(r != nullptr); const OUTPUT_QUAD_SURFACE_DESC normalized_desc = M_NormalizeDesc(desc); bool reupload_vert = false; if (memcmp(r->desc.uv, normalized_desc.uv, sizeof(normalized_desc.uv)) != 0) { reupload_vert = true; } if (!r->ready) { reupload_vert = true; } glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, r->texture); if (r->desc.width != normalized_desc.width || r->desc.height != normalized_desc.height || r->desc.tex_format != normalized_desc.tex_format || r->desc.tex_type != normalized_desc.tex_type) { glPixelStorei(GL_PACK_ALIGNMENT, 1); TRX_GL_CheckError(); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); TRX_GL_CheckError(); glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, normalized_desc.width, normalized_desc.height, 0, normalized_desc.tex_format, normalized_desc.tex_type, data); TRX_GL_CheckError(); } else { glPixelStorei(GL_PACK_ALIGNMENT, 1); TRX_GL_CheckError(); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); TRX_GL_CheckError(); glTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, normalized_desc.width, normalized_desc.height, normalized_desc.tex_format, normalized_desc.tex_type, data); TRX_GL_CheckError(); } r->ready = true; r->desc = normalized_desc; r->use_external_texture = false; r->external_texture_id = 0; if (reupload_vert) { M_UploadVertices(r); } } void Output_Quad_SetExternalTexture( OUTPUT_QUAD *const r, const GLuint texture_id, const int32_t width, const int32_t height, const bool flip_y) { ASSERT(r != nullptr); r->use_external_texture = true; r->external_texture_id = texture_id; const float v0 = flip_y ? 1.0f : 0.0f; const float v1 = flip_y ? 0.0f : 1.0f; const OUTPUT_QUAD_SURFACE_DESC desc = { .width = width, .height = height, .bit_count = 32, .tex_format = GL_RGBA, .tex_type = GL_UNSIGNED_INT_8_8_8_8_REV, .uv = { { .u = 0.0f, .v = v0 }, { .u = 1.0f, .v = v0 }, { .u = 1.0f, .v = v1 }, { .u = 0.0f, .v = v1 }, }, .pitch = width * 4, }; const bool reupload_vert = memcmp(r->desc.uv, desc.uv, sizeof(desc.uv)) != 0 || !r->ready; r->ready = true; r->desc = desc; if (reupload_vert) { M_UploadVertices(r); } } void Output_Quad_ClearExternalTexture(OUTPUT_QUAD *const r) { ASSERT(r != nullptr); r->use_external_texture = false; r->external_texture_id = 0; } void Output_Quad_SetTextureSize( OUTPUT_QUAD *const r, const OUTPUT_QUAD_TEXTURE_SIZE *const size) { ASSERT(r != nullptr); M_BindProgram(r); if (size == nullptr) { glUniform4f(r->loc[M_UNIFORM_TEXTURE_SIZE], 0.0f, 0.0f, 1.0f, 1.0f); } else { glUniform4f( r->loc[M_UNIFORM_TEXTURE_SIZE], size->x0, size->y0, size->x1, size->y1); } TRX_GL_CheckError(); } void Output_Quad_SetRepeat( OUTPUT_QUAD *const r, const int32_t x, const int32_t y) { ASSERT(r != nullptr); if (r->repeat.x == x && r->repeat.y == y) { return; } r->repeat.x = x; r->repeat.y = y; M_UploadVertices(r); } void Output_Quad_SetEffect(OUTPUT_QUAD *const r, const uint32_t effect) { ASSERT(r != nullptr); if (r->effect != effect) { M_BindProgram(r); glUniform1i(r->loc[M_UNIFORM_EFFECT], effect); TRX_GL_CheckError(); r->effect = effect; } } void Output_Quad_SetOpacity(OUTPUT_QUAD *const r, const float opacity) { ASSERT(r != nullptr); if (r->opacity != opacity) { M_BindProgram(r); glUniform1f(r->loc[M_UNIFORM_OPACITY], opacity); TRX_GL_CheckError(); r->opacity = opacity; } } void Output_Quad_SetBrightnessScale( OUTPUT_QUAD *const r, const float brightness_scale) { ASSERT(r != nullptr); if (r->brightness_scale != brightness_scale) { M_BindProgram(r); glUniform1f(r->loc[M_UNIFORM_BRIGHTNESS_SCALE], brightness_scale); TRX_GL_CheckError(); r->brightness_scale = brightness_scale; } } void Output_Quad_SetFilter( OUTPUT_QUAD *const r, const TEXTURE_FILTER filter_mode) { ASSERT(r != nullptr); r->filter_mode = filter_mode; } void Output_Quad_SetFit( OUTPUT_QUAD *const r, const OUTPUT_QUAD_FIT_MODE fit_mode, const float src_w, const float src_h) { ASSERT(r != nullptr); if (src_w <= 0.0f || src_h <= 0.0f) { Output_Quad_ClearFit(r); return; } const float src_aspect = src_w / src_h; if (r->fit_mode == fit_mode && r->src_aspect == src_aspect) { return; } r->fit_mode = fit_mode; r->src_aspect = src_aspect; M_BindProgram(r); glUniform1i(r->loc[M_UNIFORM_FIT_MODE], (int32_t)fit_mode); glUniform1f(r->loc[M_UNIFORM_SRC_ASPECT], src_aspect); TRX_GL_CheckError(); } void Output_Quad_ClearFit(OUTPUT_QUAD *const r) { ASSERT(r != nullptr); if (r->fit_mode == OUTPUT_QUAD_FIT_STRETCH && r->src_aspect == 1.0f) { return; } r->fit_mode = OUTPUT_QUAD_FIT_STRETCH; r->src_aspect = 1.0f; M_BindProgram(r); glUniform1i(r->loc[M_UNIFORM_FIT_MODE], (int32_t)r->fit_mode); glUniform1f(r->loc[M_UNIFORM_SRC_ASPECT], r->src_aspect); TRX_GL_CheckError(); } void Output_Quad_Render(OUTPUT_QUAD *const r) { ASSERT(r != nullptr); M_BindProgram(r); glUniform1i(r->loc[M_UNIFORM_EFFECT], r->effect); glBindVertexArray(r->vao); glBindBuffer(GL_ARRAY_BUFFER, r->vbo); glActiveTexture(GL_TEXTURE0); if (r->use_external_texture) { glBindTexture(GL_TEXTURE_2D, r->external_texture_id); } else { glBindTexture(GL_TEXTURE_2D, r->texture); } const GLint gl_filter = r->filter_mode == TEXTURE_FILTER_BILINEAR ? GL_LINEAR : GL_NEAREST; glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, gl_filter); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, gl_filter); GLint prev_sampler = 0; glGetIntegeri_v(GL_SAMPLER_BINDING, 0, &prev_sampler); glBindSampler(0, 0); const GLboolean was_blend_enabled = glIsEnabled(GL_BLEND); if (was_blend_enabled) { glDisable(GL_BLEND); } GLint bound_polygon_mode[2]; glGetIntegerv(GL_POLYGON_MODE, &bound_polygon_mode[0]); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); const GLboolean was_depth_test_enabled = glIsEnabled(GL_DEPTH_TEST); if (was_depth_test_enabled) { glDisable(GL_DEPTH_TEST); } glDrawArrays(GL_TRIANGLES, 0, r->vertex_count); glBindSampler(0, (GLuint)prev_sampler); glPolygonMode(GL_FRONT_AND_BACK, bound_polygon_mode[0]); if (was_depth_test_enabled) { glEnable(GL_DEPTH_TEST); } if (was_blend_enabled) { glEnable(GL_BLEND); } TRX_GL_CheckError(); } void Output_Quad_RenderWithBlend(OUTPUT_QUAD *const r) { ASSERT(r != nullptr); M_BindProgram(r); glUniform1i(r->loc[M_UNIFORM_EFFECT], r->effect); glBindVertexArray(r->vao); glBindBuffer(GL_ARRAY_BUFFER, r->vbo); glActiveTexture(GL_TEXTURE0); if (r->use_external_texture) { glBindTexture(GL_TEXTURE_2D, r->external_texture_id); } else { glBindTexture(GL_TEXTURE_2D, r->texture); } const GLint gl_filter = r->filter_mode == TEXTURE_FILTER_BILINEAR ? GL_LINEAR : GL_NEAREST; glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, gl_filter); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, gl_filter); GLint prev_sampler = 0; glGetIntegeri_v(GL_SAMPLER_BINDING, 0, &prev_sampler); glBindSampler(0, 0); const GLboolean was_blend_enabled = glIsEnabled(GL_BLEND); GLint prev_blend_src = 0; GLint prev_blend_dst = 0; glGetIntegerv(GL_BLEND_SRC_RGB, &prev_blend_src); glGetIntegerv(GL_BLEND_DST_RGB, &prev_blend_dst); glEnable(GL_BLEND); glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); GLint bound_polygon_mode[2]; glGetIntegerv(GL_POLYGON_MODE, &bound_polygon_mode[0]); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); const GLboolean was_depth_test_enabled = glIsEnabled(GL_DEPTH_TEST); if (was_depth_test_enabled) { glDisable(GL_DEPTH_TEST); } glDrawArrays(GL_TRIANGLES, 0, r->vertex_count); glBindSampler(0, (GLuint)prev_sampler); glPolygonMode(GL_FRONT_AND_BACK, bound_polygon_mode[0]); if (was_depth_test_enabled) { glEnable(GL_DEPTH_TEST); } glBlendFunc(prev_blend_src, prev_blend_dst); if (!was_blend_enabled) { glDisable(GL_BLEND); } TRX_GL_CheckError(); } ================================================ FILE: src/trx/game/output/quad.h ================================================ #pragma once #include #include #include // Textured screen-space quad renderer used by output code paths such as FMV // presentation and overlay composition. It owns GL state/resources needed to // upload 2D image data or bind external textures, then draw them with optional // fit/repeat/effect controls. typedef struct { float u; float v; } OUTPUT_QUAD_SURFACE_UV; typedef struct { int32_t width; int32_t height; int32_t bit_count; GLenum tex_format; GLenum tex_type; OUTPUT_QUAD_SURFACE_UV uv[4]; int32_t pitch; } OUTPUT_QUAD_SURFACE_DESC; typedef struct { float x0; float y0; float x1; float y1; } OUTPUT_QUAD_TEXTURE_SIZE; typedef enum { OUTPUT_QUAD_EFFECT_NONE = 0, OUTPUT_QUAD_EFFECT_VIGNETTE = 1 << 0, OUTPUT_QUAD_EFFECT_WAVE = 1 << 1, } OUTPUT_QUAD_EFFECT; typedef struct OUTPUT_QUAD OUTPUT_QUAD; typedef enum { OUTPUT_QUAD_FIT_STRETCH, OUTPUT_QUAD_FIT_LETTERBOX, OUTPUT_QUAD_FIT_CROP, OUTPUT_QUAD_FIT_SMART, } OUTPUT_QUAD_FIT_MODE; // Create a quad renderer instance and initialize GL resources. OUTPUT_QUAD *Output_Quad_Create(void); // Destroy a quad renderer instance and release its GL resources. void Output_Quad_Destroy(OUTPUT_QUAD *renderer); // Upload pixel data into the renderer-owned texture. void Output_Quad_Upload( OUTPUT_QUAD *renderer, const OUTPUT_QUAD_SURFACE_DESC *desc, const uint8_t *data); // Bind an external texture as the source image for rendering. void Output_Quad_SetExternalTexture( OUTPUT_QUAD *renderer, GLuint texture_id, int32_t width, int32_t height, bool flip_y); // Switch back to the renderer-owned texture source. void Output_Quad_ClearExternalTexture(OUTPUT_QUAD *renderer); // Set how many times the quad should repeat in X and Y. void Output_Quad_SetRepeat(OUTPUT_QUAD *renderer, int32_t x, int32_t y); // Set the source UV rectangle used for texture sampling. void Output_Quad_SetTextureSize( OUTPUT_QUAD *renderer, const OUTPUT_QUAD_TEXTURE_SIZE *size); // Set visual effect flags applied by the quad shader. void Output_Quad_SetEffect(OUTPUT_QUAD *renderer, uint32_t effect); // Set output opacity multiplier. void Output_Quad_SetOpacity(OUTPUT_QUAD *renderer, float opacity); // Set brightness scaling multiplier applied in shader. void Output_Quad_SetBrightnessScale( OUTPUT_QUAD *renderer, float brightness_scale); void Output_Quad_SetFilter(OUTPUT_QUAD *renderer, TEXTURE_FILTER filter_mode); // Configure fitting mode and source aspect ratio handling. void Output_Quad_SetFit( OUTPUT_QUAD *renderer, OUTPUT_QUAD_FIT_MODE fit_mode, float src_w, float src_h); // Reset fit behavior to stretch with default aspect. void Output_Quad_ClearFit(OUTPUT_QUAD *renderer); // Render the quad without forcing blend state changes. void Output_Quad_Render(OUTPUT_QUAD *renderer); // Render the quad with alpha blending enabled. void Output_Quad_RenderWithBlend(OUTPUT_QUAD *renderer); ================================================ FILE: src/trx/game/output/scene_compositor.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #define M_PROCESS_SOURCES(p, func, ...) \ do { \ for (int32_t i = 0; i < p->sources->count; i++) { \ const SCENE_SOURCE *const source = \ *(SCENE_SOURCE **)Vector_Get(p->sources, i); \ if (source->func != nullptr) { \ source->func(source, ##__VA_ARGS__); \ } \ } \ } while (0) typedef struct { VECTOR *sources; GLuint sampler_id; } M_PRIV; static M_PRIV m_Priv = {}; static void M_SetSamplerFilter( const GLuint sampler, const TEXTURE_FILTER filter) { const GLenum gl_filter = filter == TEXTURE_FILTER_BILINEAR ? GL_LINEAR : GL_NEAREST; glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, gl_filter); glSamplerParameteri(sampler, GL_TEXTURE_MAG_FILTER, gl_filter); } static void M_BindTextures(const M_PRIV *const p) { glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D_ARRAY, Output_Textures_GetAtlasTexture()); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, Output_Textures_GetEnvMapTexture()); } static void M_SetupScene(const M_PRIV *const p) { Output_MeshShader_Bind(Output_GetMeshShader()); Output_Uniforms_UploadViewMatrix(Output_GetUniforms(), &g_ViewMatrix); glEnable(GL_BLEND); glBlendFunc( GL_ONE, g_Config.rendering.enable_wireframe ? GL_ZERO : GL_ONE_MINUS_SRC_ALPHA); M_SetSamplerFilter(p->sampler_id, g_Config.rendering.texture_filter); } static void M_SetupUI(const M_PRIV *const p) { Output_UIShader_Bind(Output_GetUIShader()); Output_Uniforms_UploadOrthoMatrix(Output_GetUniforms()); glEnable(GL_BLEND); glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); M_SetSamplerFilter(p->sampler_id, g_Config.rendering.ui_filter); glClear(GL_DEPTH_BUFFER_BIT); } static void M_RenderSourcePass(const M_PRIV *const p, const SCENE_PASS pass) { for (int32_t i = 0; i < p->sources->count; i++) { const SCENE_SOURCE *const source = *(SCENE_SOURCE **)Vector_Get(p->sources, i); if (source->is_dirty != nullptr && source->is_dirty(source, pass)) { ASSERT(source->render_pass != nullptr); source->render_pass(source, pass); } } } static bool M_IsSourceDirty(const M_PRIV *const p, const SCENE_PASS pass) { for (int32_t i = 0; i < p->sources->count; i++) { const SCENE_SOURCE *const source = *(SCENE_SOURCE **)Vector_Get(p->sources, i); if (source->is_dirty != nullptr && source->is_dirty(source, pass)) { return true; } } return false; } static bool M_IsAnySourceDirty(const M_PRIV *const p) { for (SCENE_PASS pass = 0; pass < SCENE_PASS_COUNT; pass++) { if (M_IsSourceDirty(p, pass)) { return true; } } return false; } static void M_PrepareScene(const M_PRIV *const p) { #ifndef __APPLE__ glLineWidth(g_Config.rendering.wireframe_width); TRX_GL_CheckError(); #endif glBindSampler(0, p->sampler_id); glSamplerParameterf( p->sampler_id, GL_TEXTURE_MAX_ANISOTROPY_EXT, g_Config.rendering.anisotropy_filter); Output_Uniforms_UploadGeneral(Output_GetUniforms()); Output_Uniforms_UploadRoomLights(Output_GetUniforms(), nullptr); Output_SetCurrentRoom(nullptr); } static void M_RenderScenePasses(const M_PRIV *const p) { if (!M_IsAnySourceDirty(p)) { return; } M_BindTextures(p); M_SetupScene(p); glDisable(GL_DEPTH_TEST); if (M_IsSourceDirty(p, SCENE_PASS_BACKGROUND)) { M_RenderSourcePass(p, SCENE_PASS_BACKGROUND); } OUTPUT_MESH_SHADER *const shader = Output_GetMeshShader(); Output_MeshShader_Bind(shader); glPolygonMode( GL_FRONT_AND_BACK, g_Config.rendering.enable_wireframe ? GL_LINE : GL_FILL); glEnable(GL_DEPTH_TEST); glEnable(GL_POLYGON_OFFSET_FILL); if (M_IsSourceDirty(p, SCENE_PASS_OPAQUE) || M_IsSourceDirty(p, SCENE_PASS_TRANSPARENT) || M_IsSourceDirty(p, SCENE_PASS_BLEND_SUB) || M_IsSourceDirty(p, SCENE_PASS_BLEND_ADD)) { glEnable(GL_CULL_FACE); Output_MeshShader_UploadAlphaDiscard(shader, true); M_RenderSourcePass(p, SCENE_PASS_OPAQUE); Output_MeshShader_UploadAlphaDiscard(shader, false); glDepthMask(GL_FALSE); glEnable(GL_BLEND); M_RenderSourcePass(p, SCENE_PASS_TRANSPARENT); glBlendEquation(GL_FUNC_ADD); glBlendFunc(GL_ZERO, GL_ONE_MINUS_SRC_COLOR); M_RenderSourcePass(p, SCENE_PASS_BLEND_SUB); glBlendEquation(GL_FUNC_ADD); glBlendFunc(GL_SRC_ALPHA, GL_ONE); M_RenderSourcePass(p, SCENE_PASS_BLEND_ADD); glDepthMask(GL_TRUE); glDisable(GL_CULL_FACE); } if (M_IsSourceDirty(p, SCENE_PASS_OVERLAY_PRE_UI)) { M_RenderSourcePass(p, SCENE_PASS_OVERLAY_PRE_UI); } if (M_IsSourceDirty(p, SCENE_PASS_UI)) { M_SetupUI(p); M_RenderSourcePass(p, SCENE_PASS_UI); } if (M_IsSourceDirty(p, SCENE_PASS_OVERLAY_POST_UI)) { M_RenderSourcePass(p, SCENE_PASS_OVERLAY_POST_UI); } } void SceneCompositor_Init(void) { M_PRIV *const p = &m_Priv; p->sources = Vector_Create(sizeof(SCENE_SOURCE *)); glGenSamplers(1, &p->sampler_id); glSamplerParameteri(p->sampler_id, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glSamplerParameteri(p->sampler_id, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glSamplerParameteri(p->sampler_id, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glSamplerParameteri(p->sampler_id, GL_TEXTURE_MAG_FILTER, GL_NEAREST); TRX_GL_CheckError(); } void SceneCompositor_Shutdown(void) { M_PRIV *const p = &m_Priv; if (p->sources != nullptr) { Vector_Free(p->sources); p->sources = nullptr; } if (p->sampler_id != 0) { glDeleteSamplers(1, &p->sampler_id); p->sampler_id = 0; } } bool M_IsActive(void) { return !Output_IsHeadless() || Shell_GetArgs()->debug_render_performance || TRX_GL_Context_GetScheduledScreenshotPath() != nullptr; } void SceneCompositor_BeginScene(void) { M_PRIV *const p = &m_Priv; if (!M_IsActive()) { return; } M_PrepareScene(p); M_PROCESS_SOURCES(p, render_begin); } void SceneCompositor_Flush(void) { M_PRIV *const p = &m_Priv; if (!M_IsActive()) { return; } M_RenderScenePasses(p); M_PROCESS_SOURCES(p, render_end); M_PROCESS_SOURCES(p, render_begin); glClear(GL_DEPTH_BUFFER_BIT); } void SceneCompositor_EndScene(void) { M_PRIV *const p = &m_Priv; if (!M_IsActive()) { return; } M_RenderScenePasses(p); M_PROCESS_SOURCES(p, render_end); } void SceneCompositor_AnimateTextures(void) { M_PRIV *const p = &m_Priv; M_PROCESS_SOURCES(p, animate_textures); } void SceneCompositor_AddSource(const SCENE_SOURCE *const source) { M_PRIV *const p = &m_Priv; Vector_Add(p->sources, &source); } void SceneCompositor_SetSamplerFilter(const TEXTURE_FILTER filter) { M_PRIV *const p = &m_Priv; M_SetSamplerFilter(p->sampler_id, filter); } ================================================ FILE: src/trx/game/output/scene_compositor.h ================================================ #pragma once #include #include void SceneCompositor_Init(void); void SceneCompositor_Shutdown(void); void SceneCompositor_AddSource(const SCENE_SOURCE *source); void SceneCompositor_BeginScene(void); void SceneCompositor_EndScene(void); void SceneCompositor_Flush(void); void SceneCompositor_AnimateTextures(void); void SceneCompositor_SetSamplerFilter(TEXTURE_FILTER filter); ================================================ FILE: src/trx/game/output/scene_source.h ================================================ #pragma once typedef enum { SCENE_PASS_BACKGROUND, SCENE_PASS_OPAQUE, SCENE_PASS_TRANSPARENT, SCENE_PASS_BLEND_SUB, SCENE_PASS_BLEND_ADD, SCENE_PASS_OVERLAY_PRE_UI, SCENE_PASS_UI, SCENE_PASS_OVERLAY_POST_UI, SCENE_PASS_COUNT, } SCENE_PASS; typedef struct SCENE_SOURCE { void (*render_begin)(const struct SCENE_SOURCE *); void (*render_pass)(const struct SCENE_SOURCE *, SCENE_PASS pass); void (*render_end)(const struct SCENE_SOURCE *); bool (*is_dirty)(const struct SCENE_SOURCE *, SCENE_PASS pass); void (*animate_textures)(const struct SCENE_SOURCE *); void *priv; } SCENE_SOURCE; ================================================ FILE: src/trx/game/output/shaders/generic.c ================================================ #include #include #include #include #include #include #include #include typedef struct { GLint location; GLenum type; GLsizei size; char name[64]; UT_hash_handle hh; } M_UNIFORM; struct OUTPUT_SHADER { TRX_GL_PROGRAM program; int32_t count; M_UNIFORM *uniforms; M_UNIFORM *uniform_hash; }; static const char *const m_UniformBlocks[] = { "Globals", "Matrices", "Lights", "LightSource", nullptr, }; static void M_DebugUBO(const GLuint program_id, const GLuint block_idx) { // Prints memory layout of the specific UBO in the GPU // Get the block name GLint name_len = 0; glGetActiveUniformBlockiv( program_id, block_idx, GL_UNIFORM_BLOCK_NAME_LENGTH, &name_len); char *const block_name = Memory_Alloc(name_len); glGetActiveUniformBlockName( program_id, block_idx, name_len, nullptr, block_name); // Get all uniforms within that block GLint uniform_count = 0; glGetActiveUniformBlockiv( program_id, block_idx, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, &uniform_count); GLuint *const uniform_indices = Memory_Alloc(sizeof(GLuint) * uniform_count); glGetActiveUniformBlockiv( program_id, block_idx, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, (GLint *)uniform_indices); // Query offsets GLint *const offsets = Memory_Alloc(sizeof(GLint) * uniform_count); glGetActiveUniformsiv( program_id, uniform_count, uniform_indices, GL_UNIFORM_OFFSET, offsets); // Print block name and all members LOG_DEBUG("Uniform Block %u: %s", block_idx, block_name); for (GLint i = 0; i < uniform_count; ++i) { char name[256]; GLsizei length; glGetActiveUniformName( program_id, uniform_indices[i], sizeof(name), &length, name); LOG_DEBUG(" %s → offset %d", name, offsets[i]); } // Cleanup Memory_Free(offsets); Memory_Free(uniform_indices); Memory_Free(block_name); } OUTPUT_SHADER *Output_Shader_Create(const char *const path) { OUTPUT_SHADER *const shader = Memory_Alloc(sizeof(OUTPUT_SHADER)); TRX_GL_Program_Init(&shader->program); TRX_GL_Program_AttachShader(&shader->program, GL_VERTEX_SHADER, path); TRX_GL_Program_AttachShader(&shader->program, GL_FRAGMENT_SHADER, path); TRX_GL_Program_FragmentData(&shader->program, "outColor"); TRX_GL_Program_Link(&shader->program); #if 0 M_DebugUBO(shader->program.id, 0); #endif // Bind uniform blocks to UBO binding points const GLuint program_id = shader->program.id; for (int32_t i = 0; m_UniformBlocks[i] != nullptr; i++) { GLuint block_index = glGetUniformBlockIndex(program_id, m_UniformBlocks[i]); if (block_index != GL_INVALID_INDEX) { glUniformBlockBinding(program_id, block_index, i); } } GLint count; glGetProgramiv(shader->program.id, GL_ACTIVE_UNIFORMS, &count); shader->count = count; shader->uniforms = Memory_Alloc(sizeof(M_UNIFORM) * count); shader->uniform_hash = nullptr; for (GLint i = 0; i < count; i++) { M_UNIFORM *const uniform = &shader->uniforms[i]; GLsizei len; GLchar name[64]; glGetActiveUniform( shader->program.id, i, sizeof(name), &len, &uniform->size, &uniform->type, name); uniform->location = glGetUniformLocation(shader->program.id, name); strncpy(uniform->name, name, sizeof(uniform->name)); HASH_ADD_STR(shader->uniform_hash, name, uniform); } TRX_GL_Program_Bind(&shader->program); return shader; } void Output_Shader_Free(OUTPUT_SHADER *const shader) { TRX_GL_Program_Close(&shader->program); M_UNIFORM *cur, *tmp; HASH_ITER(hh, shader->uniform_hash, cur, tmp) { HASH_DEL(shader->uniform_hash, cur); } Memory_Free(shader->uniforms); Memory_Free(shader); } void Output_Shader_Bind(const OUTPUT_SHADER *const shader) { ASSERT(shader != nullptr); TRX_GL_Program_Bind(&shader->program); const OUTPUT_UNIFORMS *const uniforms = Output_GetUniforms(); glBindBufferBase(GL_UNIFORM_BUFFER, 0, uniforms->general); glBindBufferBase(GL_UNIFORM_BUFFER, 1, uniforms->matrices); glBindBufferBase(GL_UNIFORM_BUFFER, 2, uniforms->lights); glBindBufferBase(GL_UNIFORM_BUFFER, 3, uniforms->ls); TRX_GL_CheckError(); } GLint Output_Shader_LookupUniform( const OUTPUT_SHADER *const shader, const char *const name) { M_UNIFORM *uniform = nullptr; HASH_FIND_STR(shader->uniform_hash, name, uniform); if (uniform == nullptr) { LOG_ERROR("Uniform %s not found", name); return -1; } return uniform->location; } bool Output_Shader_TryLookupUniform( const OUTPUT_SHADER *const shader, const char *const name, GLint *const out_location) { M_UNIFORM *uniform = nullptr; HASH_FIND_STR(shader->uniform_hash, name, uniform); if (uniform == nullptr) { if (out_location != nullptr) { *out_location = -1; } return false; } if (out_location != nullptr) { *out_location = uniform->location; } return true; } ================================================ FILE: src/trx/game/output/shaders/generic.h ================================================ #pragma once #include typedef struct OUTPUT_SHADER OUTPUT_SHADER; OUTPUT_SHADER *Output_Shader_Create(const char *path); void Output_Shader_Free(OUTPUT_SHADER *shader); void Output_Shader_Bind(const OUTPUT_SHADER *shader); GLint Output_Shader_LookupUniform( const OUTPUT_SHADER *shader, const char *name); bool Output_Shader_TryLookupUniform( const OUTPUT_SHADER *shader, const char *name, GLint *out_location); ================================================ FILE: src/trx/game/output/shaders/mesh.c ================================================ #include #include #include #include #include #include #include #include struct OUTPUT_MESH_SHADER { OUTPUT_SHADER *base_tr12; OUTPUT_SHADER *base_tr3; MATRIX model_matrix[2]; bool has_model_matrix[2]; int32_t water_effect[2]; float water_effect_params[2][3]; bool is_wibble_effect[2]; bool is_alpha_discard_enabled[2]; RGB_F tint[2]; }; static int32_t M_GetVariantIndex(void) { return g_TRVersion >= 3 ? 1 : 0; } static OUTPUT_SHADER *M_GetVariantBase( const OUTPUT_MESH_SHADER *const shader, const int32_t variant_idx) { return variant_idx != 0 ? shader->base_tr3 : shader->base_tr12; } OUTPUT_MESH_SHADER *Output_MeshShader_Create(void) { OUTPUT_MESH_SHADER *const shader = Memory_Alloc(sizeof(*shader)); shader->has_model_matrix[0] = false; shader->has_model_matrix[1] = false; shader->water_effect[0] = -1; shader->water_effect[1] = -1; shader->water_effect_params[0][0] = 0.0f; shader->water_effect_params[0][1] = 0.0f; shader->water_effect_params[0][2] = 0.0f; shader->water_effect_params[1][0] = 0.0f; shader->water_effect_params[1][1] = 0.0f; shader->water_effect_params[1][2] = 0.0f; shader->is_wibble_effect[0] = false; shader->is_wibble_effect[1] = false; shader->is_alpha_discard_enabled[0] = false; shader->is_alpha_discard_enabled[1] = false; shader->tint[0] = (RGB_F) { 0.0f, 0.0f, 0.0f }; shader->tint[1] = (RGB_F) { 0.0f, 0.0f, 0.0f }; shader->base_tr12 = Output_Shader_Create("meshes_tr12.glsl"); shader->base_tr3 = Output_Shader_Create("meshes_tr3.glsl"); Output_Shader_Bind(shader->base_tr12); TRX_GL_TRACK_UNIFORM( glUniform1i, Output_Shader_LookupUniform(shader->base_tr12, "uTexAtlas"), 0); TRX_GL_TRACK_UNIFORM( glUniform1i, Output_Shader_LookupUniform(shader->base_tr12, "uTexEnvMap"), 1); Output_Shader_Bind(shader->base_tr3); TRX_GL_TRACK_UNIFORM( glUniform1i, Output_Shader_LookupUniform(shader->base_tr3, "uTexAtlas"), 0); TRX_GL_TRACK_UNIFORM( glUniform1i, Output_Shader_LookupUniform(shader->base_tr3, "uTexEnvMap"), 1); return shader; } void Output_MeshShader_Bind(const OUTPUT_MESH_SHADER *const shader) { const int32_t variant_idx = M_GetVariantIndex(); Output_Shader_Bind(M_GetVariantBase(shader, variant_idx)); } void Output_MeshShader_Free(OUTPUT_MESH_SHADER *const shader) { Output_Shader_Free(shader->base_tr12); Output_Shader_Free(shader->base_tr3); Memory_Free(shader); } void Output_MeshShader_UploadModelMatrix( OUTPUT_MESH_SHADER *const shader, const MATRIX *const source) { const int32_t variant_idx = M_GetVariantIndex(); OUTPUT_SHADER *const base = M_GetVariantBase(shader, variant_idx); if (shader->has_model_matrix[variant_idx] && memcmp(&shader->model_matrix[variant_idx], source, sizeof(*source)) == 0) { return; } memcpy(&shader->model_matrix[variant_idx], source, sizeof(*source)); shader->has_model_matrix[variant_idx] = true; GLfloat m[4][4]; Output_FillMatrix(m, source); TRX_GL_TRACK_UNIFORM( glUniformMatrix4fv, Output_Shader_LookupUniform(base, "uMatModel"), 1, GL_FALSE, &m[0][0]); } void Output_MeshShader_UploadAlphaDiscard( OUTPUT_MESH_SHADER *const shader, const bool is_enabled) { const int32_t variant_idx = M_GetVariantIndex(); OUTPUT_SHADER *const base = M_GetVariantBase(shader, variant_idx); if (is_enabled == shader->is_alpha_discard_enabled[variant_idx]) { return; } TRX_GL_TRACK_UNIFORM( glUniform1i, Output_Shader_LookupUniform(base, "uDiscardAlpha"), is_enabled); shader->is_alpha_discard_enabled[variant_idx] = is_enabled; } void Output_MeshShader_UploadWaterEffect( OUTPUT_MESH_SHADER *const shader, const int32_t water_effect) { const int32_t variant_idx = M_GetVariantIndex(); OUTPUT_SHADER *const base = M_GetVariantBase(shader, variant_idx); if (water_effect == shader->water_effect[variant_idx]) { return; } static const float m_ChoppyAmp[22] = { 16.0f, 0.0f, 0.0f, 0.0f, 0.0f, 16.0f, 16.0f, 16.0f, 16.0f, 53.0f, 53.0f, 53.0f, 53.0f, 90.0f, 90.0f, 90.0f, 90.0f, 127.0f, 127.0f, 127.0f, 127.0f, 0.0f, }; static const float m_ShimmerAmp[22] = { 7.875f, 4.0f, 8.0f, 12.0f, 15.875f, -3.875f, -7.875f, -11.875f, -15.875f, -3.875f, -7.875f, -11.875f, -15.875f, -3.875f, -7.875f, -11.875f, -15.875f, -3.875f, -7.875f, -11.875f, -15.875f, 0.0f, }; static const float m_AbsIntensity[22] = { 0.0f, 253.0f, 0.0f, 4.0f, 8.0f, 4.0f, 8.0f, 12.0f, 16.0f, 4.0f, 8.0f, 12.0f, 16.0f, 4.0f, 8.0f, 12.0f, 16.0f, 4.0f, 8.0f, 12.0f, 16.0f, 0.0f, }; int32_t scheme = water_effect - 2; CLAMP(scheme, 0, 21); const float p0 = m_ChoppyAmp[scheme]; const float p1 = m_ShimmerAmp[scheme]; const float p2 = m_AbsIntensity[scheme]; GLint loc = -1; if (Output_Shader_TryLookupUniform(base, "uWaterEffect", &loc)) { TRX_GL_TRACK_UNIFORM(glUniform1i, loc, water_effect); } if (Output_Shader_TryLookupUniform(base, "uWaterEffectParams", &loc)) { TRX_GL_TRACK_UNIFORM(glUniform3f, loc, p0, p1, p2); } shader->water_effect[variant_idx] = water_effect; shader->water_effect_params[variant_idx][0] = p0; shader->water_effect_params[variant_idx][1] = p1; shader->water_effect_params[variant_idx][2] = p2; } void Output_MeshShader_UploadWibbleEffect( OUTPUT_MESH_SHADER *const shader, const bool is_enabled) { const int32_t variant_idx = M_GetVariantIndex(); OUTPUT_SHADER *const base = M_GetVariantBase(shader, variant_idx); if (is_enabled == shader->is_wibble_effect[variant_idx]) { return; } TRX_GL_TRACK_UNIFORM( glUniform1i, Output_Shader_LookupUniform(base, "uWibbleEffect"), is_enabled); shader->is_wibble_effect[variant_idx] = is_enabled; } void Output_MeshShader_UploadTint(OUTPUT_MESH_SHADER *const shader, RGB_F tint) { const int32_t variant_idx = M_GetVariantIndex(); OUTPUT_SHADER *const base = M_GetVariantBase(shader, variant_idx); if (tint.r == shader->tint[variant_idx].r && tint.g == shader->tint[variant_idx].g && tint.b == shader->tint[variant_idx].b) { return; } TRX_GL_TRACK_UNIFORM( glUniform3f, Output_Shader_LookupUniform(base, "uTint"), tint.r, tint.g, tint.b); shader->tint[variant_idx] = tint; } ================================================ FILE: src/trx/game/output/shaders/mesh.h ================================================ #pragma once #include #include #include // clang-format off #define VERT_NO_WIBBLE 0b0000'0000'0001 // = 0x0001 #define VERT_FLAT_SHADED 0b0000'0000'0010 // = 0x0002 #define VERT_REFLECTIVE 0b0000'0000'0100 // = 0x0004 #define VERT_NO_LIGHTING 0b0000'0000'1000 // = 0x0008 #define VERT_BILLBOARD 0b0000'0001'0000 // = 0x0010 #define VERT_ABS_SPRITE 0b0000'0010'0000 // = 0x0020 #define VERT_NO_ALPHA_DISCARD 0b0000'0100'0000 // = 0x0040 #define VERT_USE_DYNAMIC_LIGHT 0b0000'1000'0000 // = 0x0080 #define VERT_USE_OBJECT_LIGHT 0b0001'0000'0000 // = 0x0100 #define VERT_USE_OWN_LIGHT 0b0010'0000'0000 // = 0x0200 #define VERT_MOVE 0b0100'0000'0000 // = 0x0400 #define VERT_GLOW 0b1000'0000'0000 // = 0x0800 // clang-format on // GL attribute mapping in the shader typedef enum { // clang-format off OUTPUT_MESH_ATTR_POS = 0, OUTPUT_MESH_ATTR_NORMAL = 1, OUTPUT_MESH_ATTR_UVW = 2, OUTPUT_MESH_ATTR_TEXTURE_SIZE = 3, OUTPUT_MESH_ATTR_TRAPEZOID_RATIO = 4, OUTPUT_MESH_ATTR_FLAGS = 5, OUTPUT_MESH_ATTR_COLOR = 6, OUTPUT_MESH_ATTR_SHADE = 7, // clang-format on } OUTPUT_MESH_ATTRIBUTE; typedef struct OUTPUT_MESH_SHADER OUTPUT_MESH_SHADER; OUTPUT_MESH_SHADER *Output_MeshShader_Create(void); void Output_MeshShader_Free(OUTPUT_MESH_SHADER *shader); void Output_MeshShader_Bind(const OUTPUT_MESH_SHADER *shader); // TODO: these could could use UBOs void Output_MeshShader_UploadModelMatrix( OUTPUT_MESH_SHADER *shader, const MATRIX *source); void Output_MeshShader_UploadWaterEffect( OUTPUT_MESH_SHADER *shader, int32_t water_effect); void Output_MeshShader_UploadWibbleEffect( OUTPUT_MESH_SHADER *shader, bool is_enabled); void Output_MeshShader_UploadTint(OUTPUT_MESH_SHADER *shader, RGB_F tint); void Output_MeshShader_UploadAlphaDiscard( OUTPUT_MESH_SHADER *shader, bool is_enabled); ================================================ FILE: src/trx/game/output/shaders/ui.c ================================================ #include #include OUTPUT_UI_SHADER *Output_UIShader_Create(void) { OUTPUT_SHADER *const shader = Output_Shader_Create("ui.glsl"); TRX_GL_TRACK_UNIFORM( glUniform1i, Output_Shader_LookupUniform(shader, "uTexAtlas"), 0); return shader; } void Output_UIShader_Bind(const OUTPUT_UI_SHADER *const shader) { Output_Shader_Bind(shader); } void Output_UIShader_Free(OUTPUT_UI_SHADER *const shader) { Output_Shader_Free(shader); } ================================================ FILE: src/trx/game/output/shaders/ui.h ================================================ #pragma once #include typedef OUTPUT_SHADER OUTPUT_UI_SHADER; OUTPUT_UI_SHADER *Output_UIShader_Create(void); void Output_UIShader_Free(OUTPUT_UI_SHADER *shader); void Output_UIShader_Bind(const OUTPUT_UI_SHADER *shader); ================================================ FILE: src/trx/game/output/sources/lightnings.c ================================================ #include #include #include #include #include #include typedef struct { XYZW_F pos; XYZW_F normal; RGBA_8888 color; } M_VERTEX; typedef struct { SCENE_SOURCE source; OUTPUT_MESH_SHADER *shader; VECTOR *vertices; VECTOR *scheduled; GLuint vao; GLuint vbo; } M_PRIV; static M_PRIV m_Priv; static void M_GenerateLightningSegment( M_PRIV *const p, const LIGHTNING_SEGMENT *const segment) { const RGBA_8888 blue = { 0, 0, 255, 128 }; const RGBA_8888 white = { 255, 255, 255, 128 }; const int32_t w = segment->thickness / 2; const XYZW_F pos_0 = { .x = segment->from.x, .y = segment->from.y, .z = segment->from.z, .w = 0.0f, }; const XYZW_F pos_1 = { .x = segment->to.x, .y = segment->to.y, .z = segment->to.z, .w = 0.0f, }; // 2 quads side-to-side (blue-white) (white-blue); // double-sided so that visible from both sides const M_VERTEX vertices[4][4] = { // clang-format off { { .pos = pos_0, .normal = { 0, 0, 0, 0 }, .color = white }, { .pos = pos_1, .normal = { 0, 0, 0, 0 }, .color = white }, { .pos = pos_1, .normal = { w, 0, 0, 0 }, .color = blue }, { .pos = pos_0, .normal = { w, 0, 0, 0 }, .color = blue }, }, { { .pos = pos_0, .normal = { 0, 0, 0, 0 }, .color = white }, { .pos = pos_1, .normal = { 0, 0, 0, 0 }, .color = white }, { .pos = pos_1, .normal = { -w, 0, 0, 0 }, .color = blue }, { .pos = pos_0, .normal = { -w, 0, 0, 0 }, .color = blue }, }, { { .pos = pos_0, .normal = { 0, 0, 0, 0 }, .color = white }, { .pos = pos_0, .normal = { w, 0, 0, 0 }, .color = blue }, { .pos = pos_1, .normal = { w, 0, 0, 0 }, .color = blue }, { .pos = pos_1, .normal = { 0, 0, 0, 0 }, .color = white }, }, { { .pos = pos_0, .normal = { 0, 0, 0, 0 }, .color = white }, { .pos = pos_0, .normal = { -w, 0, 0, 0 }, .color = blue }, { .pos = pos_1, .normal = { -w, 0, 0, 0 }, .color = blue }, { .pos = pos_1, .normal = { 0, 0, 0, 0 }, .color = white }, }, // clang-format on }; for (int32_t quad = 0; quad < 4; quad++) { for (int32_t i = 0; i < OUTPUT_QUAD_VERTICES; i++) { const int32_t j = OUTPUT_QUAD_TO_FAN(i); Vector_Add(p->vertices, &vertices[quad][j]); } } } static void M_RenderBegin(const SCENE_SOURCE *const source) { M_PRIV *const p = &m_Priv; Vector_Clear(p->scheduled); Vector_Clear(p->vertices); } static void M_RenderPass( const SCENE_SOURCE *const source, const SCENE_PASS pass) { M_PRIV *const p = &m_Priv; if (pass != SCENE_PASS_TRANSPARENT) { return; } glBindVertexArray(p->vao); glBindBuffer(GL_ARRAY_BUFFER, p->vbo); glVertexAttrib4f(OUTPUT_MESH_ATTR_NORMAL, 0.0f, 0.0f, 0.0f, 0.0f); glVertexAttrib3f(OUTPUT_MESH_ATTR_UVW, 0.0f, 0.0f, 0.0f); glVertexAttrib4f(OUTPUT_MESH_ATTR_TEXTURE_SIZE, 0.0f, 0.0f, 1.0f, 1.0f); glVertexAttrib2f(OUTPUT_MESH_ATTR_TRAPEZOID_RATIO, 1.0f, 1.0f); glVertexAttribI1ui( OUTPUT_MESH_ATTR_FLAGS, VERT_FLAT_SHADED | VERT_NO_LIGHTING | VERT_NO_WIBBLE | VERT_BILLBOARD | VERT_ABS_SPRITE); glVertexAttrib1f(OUTPUT_MESH_ATTR_SHADE, SHADE_NEUTRAL); for (int32_t i = 0; i < p->scheduled->count; i++) { const LIGHTNING_SEGMENT *const segment = Vector_Get(p->scheduled, i); M_GenerateLightningSegment(p, segment); } TRX_GL_TRACK_DATA( glBufferData, GL_ARRAY_BUFFER, p->vertices->count * sizeof(M_VERTEX), Vector_GetData(p->vertices), GL_STATIC_DRAW); Output_MeshShader_UploadModelMatrix(p->shader, &g_IDMatrix); glDrawArrays(GL_TRIANGLES, 0, p->vertices->count); TRX_GL_CheckError(); } static bool M_IsDirty(const SCENE_SOURCE *const source, const SCENE_PASS pass) { const M_PRIV *const p = &m_Priv; return pass == SCENE_PASS_TRANSPARENT && p->scheduled->count > 0; } void OutputSource_Lightnings_Init(void) { M_PRIV *const p = &m_Priv; p->shader = Output_GetMeshShader(); p->vertices = Vector_Create(sizeof(M_VERTEX)); p->scheduled = Vector_Create(sizeof(LIGHTNING_SEGMENT)); p->source.render_begin = M_RenderBegin; p->source.render_pass = M_RenderPass; p->source.is_dirty = M_IsDirty; SceneCompositor_AddSource(&p->source); glGenVertexArrays(1, &p->vao); glBindVertexArray(p->vao); glGenBuffers(1, &p->vbo); glBindBuffer(GL_ARRAY_BUFFER, p->vbo); glEnableVertexAttribArray(OUTPUT_MESH_ATTR_POS); glEnableVertexAttribArray(OUTPUT_MESH_ATTR_NORMAL); glDisableVertexAttribArray(OUTPUT_MESH_ATTR_UVW); glDisableVertexAttribArray(OUTPUT_MESH_ATTR_TEXTURE_SIZE); glDisableVertexAttribArray(OUTPUT_MESH_ATTR_TRAPEZOID_RATIO); glDisableVertexAttribArray(OUTPUT_MESH_ATTR_FLAGS); glEnableVertexAttribArray(OUTPUT_MESH_ATTR_COLOR); glVertexAttribPointer( OUTPUT_MESH_ATTR_POS, 4, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX), (void *)(intptr_t)offsetof(M_VERTEX, pos)); glVertexAttribPointer( OUTPUT_MESH_ATTR_NORMAL, 4, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX), (void *)(intptr_t)offsetof(M_VERTEX, normal)); glVertexAttribPointer( OUTPUT_MESH_ATTR_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(M_VERTEX), (void *)(intptr_t)offsetof(M_VERTEX, color)); glDisableVertexAttribArray(OUTPUT_MESH_ATTR_SHADE); } void OutputSource_Lightnings_Shutdown(void) { M_PRIV *const p = &m_Priv; if (p->vertices != nullptr) { Vector_Free(p->vertices); p->vertices = nullptr; } if (p->scheduled != nullptr) { Vector_Free(p->scheduled); p->scheduled = nullptr; } if (p->vao != 0) { glDeleteVertexArrays(1, &p->vao); p->vao = 0; } if (p->vbo != 0) { glDeleteBuffers(1, &p->vbo); p->vbo = 0; } } void OutputSource_Lightnings_StageSegment( const LIGHTNING_SEGMENT *const segment) { M_PRIV *const p = &m_Priv; Vector_Add(p->scheduled, segment); } ================================================ FILE: src/trx/game/output/sources/lightnings.h ================================================ #pragma once #include #include void OutputSource_Lightnings_Init(void); void OutputSource_Lightnings_Shutdown(void); void OutputSource_Lightnings_StageSegment(const LIGHTNING_SEGMENT *segment); ================================================ FILE: src/trx/game/output/sources/misc.c ================================================ #include #include #include #include #include #include #include #include #include #include typedef struct { XYZW_F pos; } M_VERTEX; typedef enum { M_PRIMITIVE_SPHERE, M_PRIMITIVE_CUBOID, M_PRIMITIVE_NUMBER_OF, } M_PRIMITIVE_TYPE; typedef struct { MATRIX matrix; M_PRIMITIVE_TYPE prim_type; RGBA_8888 color; } M_INSTANCE; typedef struct { SCENE_SOURCE source; OUTPUT_MESH_SHADER *shader; OUTPUT_VERTEX_RANGE primitive_ranges[M_PRIMITIVE_NUMBER_OF]; VECTOR *vertices; VECTOR *scheduled_spheres; VECTOR *scheduled_cuboids; GLuint vao; GLuint vbo; int32_t vertex_count; } M_PRIV; static M_PRIV m_Priv; static void M_SealPrimitive( M_PRIV *const p, OUTPUT_VERTEX_RANGE *const target_range) { target_range->vertex_start = p->vertex_count; target_range->vertex_count = p->vertices->count - target_range->vertex_start; p->vertex_count += target_range->vertex_count; } static void M_GenerateSphere( M_PRIV *const p, OUTPUT_VERTEX_RANGE *const target_range, const int32_t subdivisions) { // More subdivisions means smoother spheres. const int32_t position_count = SQUARE(subdivisions + 1); XYZW_F positions[position_count]; int32_t index = 0; for (int32_t i = 0; i <= subdivisions; i++) { const float theta = (M_PI * i) / subdivisions; // Latitude angle const float sin_theta = sinf(theta); const float cos_theta = cosf(theta); for (int32_t j = 0; j <= subdivisions; j++) { const float phi = (2 * M_PI * j) / subdivisions; // Longitude angle const float sin_phi = sinf(phi); const float cos_phi = cosf(phi); // Convert spherical coordinates to 3D points. positions[index] = (XYZW_F) { .x = cos_phi * sin_theta, .y = cos_theta, .z = sin_phi * sin_theta, .w = 0.0f, }; index++; } } const int32_t vertex_count = subdivisions * subdivisions * OUTPUT_QUAD_VERTICES; for (int32_t i = 0; i < subdivisions; i++) { for (int32_t j = 0; j < subdivisions; j++) { const int32_t indices[4] = { i * (subdivisions + 1) + j, (i + 1) * (subdivisions + 1) + j, (i + 1) * (subdivisions + 1) + (j + 1), i * (subdivisions + 1) + (j + 1), }; for (int32_t k = 0; k < OUTPUT_QUAD_VERTICES; k++) { const int32_t l = OUTPUT_QUAD_TO_FAN(k); Vector_Add( p->vertices, &(M_VERTEX) { .pos = positions[indices[l]], }); } for (int32_t k = 0; k < OUTPUT_QUAD_VERTICES; k++) { const int32_t l = OUTPUT_QUAD_TO_FAN_CW(k); Vector_Add( p->vertices, &(M_VERTEX) { .pos = positions[indices[l]], }); } } } M_SealPrimitive(p, target_range); } static void M_GenerateCuboid( M_PRIV *const p, OUTPUT_VERTEX_RANGE *const target_range) { const XYZW_F vertices[8] = { { -1, -1, 1, 0 }, { 1, -1, 1, 0 }, { 1, 1, 1, 0 }, { -1, 1, 1, 0 }, // front { -1, -1, -1, 0 }, { 1, -1, -1, 0 }, { 1, 1, -1, 0 }, { -1, 1, -1, 0 } // back }; const uint8_t order[6][6] = { { 0, 1, 2, 0, 2, 3 }, { 5, 4, 7, 5, 7, 6 }, { 4, 0, 3, 4, 3, 7 }, { 1, 5, 6, 1, 6, 2 }, { 3, 2, 6, 3, 6, 7 }, { 4, 5, 1, 4, 1, 0 }, }; for (int32_t i = 0; i < 6; i++) { for (int32_t j = 0; j < 6; j++) { Vector_Add( p->vertices, &(M_VERTEX) { .pos = vertices[order[i][j]], }); } } M_SealPrimitive(p, target_range); } static void M_DrawScheduled(M_PRIV *const p, VECTOR *const scheduled) { for (int32_t i = 0; i < scheduled->count; i++) { const M_INSTANCE *const instance = Vector_Get(scheduled, i); Output_MeshShader_UploadModelMatrix(p->shader, &instance->matrix); const RGBA_8888 c = instance->color; glVertexAttrib4f( OUTPUT_MESH_ATTR_COLOR, c.r / 255.0f, c.g / 255.0f, c.b / 255.0f, c.a / 255.0f); const OUTPUT_VERTEX_RANGE *const range = &p->primitive_ranges[instance->prim_type]; glDrawArrays(GL_TRIANGLES, range->vertex_start, range->vertex_count); } } static void M_RenderBegin(const SCENE_SOURCE *const source) { M_PRIV *const p = &m_Priv; Vector_Clear(p->scheduled_spheres); Vector_Clear(p->scheduled_cuboids); } static bool M_IsDirty(const SCENE_SOURCE *const source, const SCENE_PASS pass) { const M_PRIV *const p = &m_Priv; return pass == SCENE_PASS_TRANSPARENT && (p->scheduled_spheres->count > 0 || p->scheduled_cuboids->count > 0); } static void M_RenderPass( const SCENE_SOURCE *const source, const SCENE_PASS pass) { M_PRIV *const p = &m_Priv; if (pass != SCENE_PASS_TRANSPARENT) { return; } glBindVertexArray(p->vao); glBindBuffer(GL_ARRAY_BUFFER, p->vbo); glVertexAttrib4f(OUTPUT_MESH_ATTR_NORMAL, 0.0f, 0.0f, 0.0f, 0.0f); glVertexAttrib3f(OUTPUT_MESH_ATTR_UVW, 0.0f, 0.0f, 0.0f); glVertexAttrib4f(OUTPUT_MESH_ATTR_TEXTURE_SIZE, 0.0f, 0.0f, 1.0f, 1.0f); glVertexAttrib2f(OUTPUT_MESH_ATTR_TRAPEZOID_RATIO, 1.0f, 1.0f); glVertexAttribI1ui( OUTPUT_MESH_ATTR_FLAGS, VERT_FLAT_SHADED | VERT_NO_LIGHTING | VERT_NO_WIBBLE); glVertexAttrib1f(OUTPUT_MESH_ATTR_SHADE, SHADE_NEUTRAL); GLint bound_polygon_mode[2]; glGetIntegerv(GL_POLYGON_MODE, &bound_polygon_mode[0]); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); if (p->scheduled_spheres->count > 0) { M_DrawScheduled(p, p->scheduled_spheres); } if (p->scheduled_cuboids->count > 0) { M_DrawScheduled(p, p->scheduled_cuboids); } glPolygonMode(GL_FRONT_AND_BACK, bound_polygon_mode[0]); } void OutputSource_Misc_Init(void) { M_PRIV *const p = &m_Priv; p->shader = Output_GetMeshShader(); p->vertices = Vector_Create(sizeof(M_VERTEX)); p->scheduled_spheres = Vector_Create(sizeof(M_INSTANCE)); p->scheduled_cuboids = Vector_Create(sizeof(M_INSTANCE)); p->source.render_begin = M_RenderBegin; p->source.render_pass = M_RenderPass; p->source.is_dirty = M_IsDirty; SceneCompositor_AddSource(&p->source); glGenVertexArrays(1, &p->vao); glBindVertexArray(p->vao); glGenBuffers(1, &p->vbo); glBindBuffer(GL_ARRAY_BUFFER, p->vbo); glEnableVertexAttribArray(OUTPUT_MESH_ATTR_POS); glDisableVertexAttribArray(OUTPUT_MESH_ATTR_COLOR); glDisableVertexAttribArray(OUTPUT_MESH_ATTR_NORMAL); glDisableVertexAttribArray(OUTPUT_MESH_ATTR_UVW); glDisableVertexAttribArray(OUTPUT_MESH_ATTR_TEXTURE_SIZE); glDisableVertexAttribArray(OUTPUT_MESH_ATTR_TRAPEZOID_RATIO); glDisableVertexAttribArray(OUTPUT_MESH_ATTR_FLAGS); glDisableVertexAttribArray(OUTPUT_MESH_ATTR_SHADE); glVertexAttribPointer( OUTPUT_MESH_ATTR_POS, 4, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX), (void *)(intptr_t)offsetof(M_VERTEX, pos)); M_GenerateSphere(p, &p->primitive_ranges[M_PRIMITIVE_SPHERE], 12); M_GenerateCuboid(p, &p->primitive_ranges[M_PRIMITIVE_CUBOID]); TRX_GL_TRACK_DATA( glBufferData, GL_ARRAY_BUFFER, p->vertex_count * sizeof(M_VERTEX), Vector_GetData(p->vertices), GL_STATIC_DRAW); } void OutputSource_Misc_Shutdown(void) { M_PRIV *const p = &m_Priv; if (p->vertices != nullptr) { Vector_Free(p->vertices); p->vertices = nullptr; } if (p->scheduled_spheres != nullptr) { Vector_Free(p->scheduled_spheres); p->scheduled_spheres = nullptr; } if (p->scheduled_cuboids != nullptr) { Vector_Free(p->scheduled_cuboids); p->scheduled_cuboids = nullptr; } if (p->vao != 0) { glDeleteVertexArrays(1, &p->vao); p->vao = 0; } if (p->vbo != 0) { glDeleteBuffers(1, &p->vbo); p->vbo = 0; } } void OutputSource_Misc_StageSphere(const RGBA_8888 color) { M_PRIV *const p = &m_Priv; M_INSTANCE inst = { .matrix = *g_WMatrixPtr, .prim_type = M_PRIMITIVE_SPHERE, .color = color, }; Vector_Add(p->scheduled_spheres, &inst); } void OutputSource_Misc_StageCuboid(const RGBA_8888 color) { M_PRIV *const p = &m_Priv; M_INSTANCE inst = { .matrix = *g_WMatrixPtr, .prim_type = M_PRIMITIVE_CUBOID, .color = color, }; Vector_Add(p->scheduled_cuboids, &inst); } ================================================ FILE: src/trx/game/output/sources/misc.h ================================================ #pragma once #include void OutputSource_Misc_Init(void); void OutputSource_Misc_Shutdown(void); void OutputSource_Misc_StageSphere(RGBA_8888 color); void OutputSource_Misc_StageCuboid(RGBA_8888 color); ================================================ FILE: src/trx/game/output/sources/objects.c ================================================ #include #include #include #include #include #include #include #include #include #include typedef struct { OUTPUT_MESH *mesh_batch; } M_MESH; typedef struct { MEMORY_ARENA_ALLOCATOR alloc; MESH_BATCHER *batcher; int16_t skybox_shade; size_t mesh_count; M_MESH *meshes; } M_PRIV; static M_PRIV m_Priv = {}; static bool M_IsMeshSkybox(const int32_t mesh_idx) { const OBJECT *const object = Object_Get(O_SKYBOX); if (!object->loaded) { return false; } return mesh_idx >= object->mesh_idx && mesh_idx < object->mesh_idx + object->mesh_count; } static SCENE_PASS M_GetScenePass(const FACE *const face, const uint16_t flags) { if ((flags & VERT_FLAT_SHADED) != 0) { return SCENE_PASS_OPAQUE; } return Output_Textures_GetObjectTextureScenePass(face->texture_idx); } static void M_AddObjectFace( MESH_BUILDER *const builder, const OBJECT_MESH *const obj_mesh, const FACE *const face, uint16_t flags) { RGBA_8888 color = COLOR_RGBA_8888_WHITE; OUTPUT_MESH_VERTEX vertices[4]; int32_t uvw_idx = -1; ASSERT(face->vertex_count <= 4); if (flags & VERT_FLAT_SHADED) { if (g_TRVersion == 1) { color = Output_RGB2RGBA(Output_GetPaletteColor8(face->palette_idx)); } else { color = Output_RGB2RGBA( Output_GetPaletteColor16(face->palette_idx >> 8)); } } else if ( Output_Textures_GetObjectTextureScenePass(face->texture_idx) == SCENE_PASS_OPAQUE) { flags |= VERT_NO_ALPHA_DISCARD; } if (obj_mesh->num_lights <= 0) { flags |= VERT_USE_OWN_LIGHT; } else { flags |= VERT_USE_OBJECT_LIGHT; } for (int32_t i = 0; i < face->vertex_count; i++) { const int32_t shade = obj_mesh->num_lights <= 0 && face->vertices[i] < -obj_mesh->num_lights ? obj_mesh->lighting.lights[face->vertices[i]] : SHADE_NEUTRAL; const XYZ_16 normal = face->vertices[i] < obj_mesh->num_lights ? obj_mesh->lighting.normals[face->vertices[i]] : (XYZ_16) { 1, 0, 0 }; const XYZ_16 *const pos = &obj_mesh->vertices[face->vertices[i]]; if ((flags & VERT_FLAT_SHADED) == 0) { uvw_idx = Output_Textures_GetObjectUVWIndex(face->texture_idx, i); } vertices[i] = (OUTPUT_MESH_VERTEX) { .pos = { .x = pos->x, .y = pos->y, .z = pos->z }, .normal = { .x = normal.x, .y = normal.y, .z = normal.z }, .flags = flags, .uvw_idx = uvw_idx, .shade = shade, .color = color, .trapezoid_ratio = { [0] = face->texture_zw[i].z, [1] = face->texture_zw[i].w, }, }; } MeshBuilder_AddVertices(builder, vertices, face->vertex_count); MeshBuilder_AddFan( builder, M_GetScenePass(face, flags), face->double_sided); } static void M_PrepareMeshes(M_PRIV *const p) { p->mesh_count = Object_GetMeshCount(); p->meshes = Memory_ArenaAlloc(&p->alloc, sizeof(M_MESH) * p->mesh_count); MESH_BUILDER *const builder = MeshBuilder_Create(); for (int32_t i = 0; i < Object_GetMeshCount(); i++) { const OBJECT_MESH *const obj_mesh = Object_GetMesh(i); M_MESH *const new_batch = &p->meshes[i]; uint16_t flags = 0; if (obj_mesh->enable_reflections) { flags |= VERT_REFLECTIVE; } if (M_IsMeshSkybox(i)) { flags |= VERT_USE_OWN_LIGHT; } for (int32_t j = 0; j < obj_mesh->tex_faces.count; j++) { M_AddObjectFace( builder, obj_mesh, &obj_mesh->tex_faces.data[j], flags); } for (int32_t j = 0; j < obj_mesh->flat_faces.count; j++) { M_AddObjectFace( builder, obj_mesh, &obj_mesh->flat_faces.data[j], flags | VERT_FLAT_SHADED); } MeshBuilder_AdjustDepth(builder, obj_mesh->depth_adjustment); OUTPUT_MESH *const mesh = MeshBuilder_Seal(builder); if (mesh != nullptr) { MeshBatcher_AddMesh(p->batcher, mesh); new_batch->mesh_batch = mesh; } } MeshBuilder_Destroy(builder); } static void M_FreeMeshes(M_PRIV *const p) { if (p->meshes != nullptr) { for (int32_t i = 0; i < (int32_t)p->mesh_count; i++) { MeshBatcher_RemoveMesh(p->batcher, p->meshes[i].mesh_batch); if (p->meshes[i].mesh_batch != nullptr) { Output_Mesh_Destroy(p->meshes[i].mesh_batch); } } p->meshes = nullptr; } Memory_ArenaReset(&p->alloc); } static void M_UpdateShadesSkybox( MESH_INSTANCE *const inst, void *const user_data) { const OBJECT_MESH *const mesh = user_data; const M_PRIV *const p = &m_Priv; M_MESH *const batch = &p->meshes[Object_GetMeshIndex(mesh)]; if (batch->mesh_batch == nullptr) { return; } OUTPUT_MESH_VERTEX *const vertices = Vector_GetData(batch->mesh_batch->vertices); const int16_t shade = g_Config.rendering.enable_lighting ? p->skybox_shade : SHADE_NEUTRAL; for (int32_t i = 0; i < batch->mesh_batch->vertices->count; i++) { vertices[i].shade = shade; } } static void M_UpdateFlags(const OBJECT_MESH *const mesh, M_MESH *const batch) { uint16_t mask = VERT_REFLECTIVE | VERT_NO_LIGHTING; uint16_t flags = 0; if (mesh->enable_reflections) { flags |= VERT_REFLECTIVE; } OUTPUT_MESH_VERTEX *const vertices = Vector_GetData(batch->mesh_batch->vertices); for (int32_t i = 0; i < batch->mesh_batch->vertices->count; i++) { vertices[i].flags &= ~mask; vertices[i].flags |= flags; } } static void M_Stage(const OBJECT_MESH *const mesh) { M_PRIV *const p = &m_Priv; M_MESH *const batch = &p->meshes[Object_GetMeshIndex(mesh)]; if (batch->mesh_batch == nullptr) { return; } OUTPUT_LIGHT_INFO light_info = Output_GetLightInfo(); if (g_TRVersion >= 3 && M_IsMeshSkybox(Object_GetMeshIndex(mesh))) { light_info.tr3_ambient = COLOR_RGB_F_WHITE; for (int32_t i = 0; i < 3; i++) { light_info.tr3_light_color[i] = (RGB_F) { 0.0f, 0.0f, 0.0f }; light_info.tr3_light_dir_view[i] = (XYZ_32) { 0, 0, 0 }; } } const MESH_INSTANCE inst = { .mesh = batch->mesh_batch, .cwmatrix = *g_MatrixPtr, .wmatrix = *g_WMatrixPtr, .tint = Output_GetTint(), .wibble = false, .water_effect = (mesh->enable_caustics && Output_GetWaterEffect()) ? 1 : 0, .light_info = light_info, .room = Output_GetCurrentRoom(), }; MeshBatcher_Stage(p->batcher, &inst, SCENE_PASS_OPAQUE); MeshBatcher_Stage(p->batcher, &inst, SCENE_PASS_TRANSPARENT); MeshBatcher_Stage(p->batcher, &inst, SCENE_PASS_BLEND_ADD); } void OutputSource_Objects_Init(MESH_BATCHER *const batcher) { M_PRIV *const p = &m_Priv; p->batcher = batcher; } void OutputSource_Objects_Shutdown(void) { M_PRIV *const p = &m_Priv; M_FreeMeshes(p); Memory_ArenaFree(&p->alloc); } void OutputSource_Objects_ObserveLevelLoad(void) { M_PRIV *const p = &m_Priv; M_FreeMeshes(p); M_PrepareMeshes(p); } void OutputSource_Objects_ObserveLevelUnload(void) { M_PRIV *const p = &m_Priv; M_FreeMeshes(p); } void OutputSource_Objects_ObserveObjectMeshSwap( const int32_t mesh_idx_1, const int32_t mesh_idx_2) { M_PRIV *const p = &m_Priv; if (p->meshes == nullptr) { return; } SWAP(p->meshes[mesh_idx_1], p->meshes[mesh_idx_2]); OutputSource_Objects_ObserveObjectMeshUpdate(mesh_idx_1); OutputSource_Objects_ObserveObjectMeshUpdate(mesh_idx_2); } void OutputSource_Objects_ObserveObjectMeshUpdate(const int32_t mesh_idx) { M_PRIV *const p = &m_Priv; if (p->meshes == nullptr) { return; } M_MESH *const batch = &p->meshes[mesh_idx]; if (batch->mesh_batch == nullptr) { return; } M_UpdateFlags(Object_GetMesh(mesh_idx), batch); MeshBatcher_UpdateMeshGeometry(p->batcher, batch->mesh_batch); } void OutputSource_Objects_StageSkyboxMesh( const OBJECT_MESH *const mesh, const int16_t shade) { M_PRIV *const p = &m_Priv; p->skybox_shade = shade; M_Stage(mesh); } void OutputSource_Objects_StageObjectMesh(const OBJECT_MESH *const mesh) { M_Stage(mesh); } const SCENE_SOURCE *OutputSource_Objects_GetSource(void) { M_PRIV *const p = &m_Priv; return MeshBatcher_AsSource(p->batcher); } ================================================ FILE: src/trx/game/output/sources/objects.h ================================================ #pragma once #include #include #include void OutputSource_Objects_Init(MESH_BATCHER *batcher); void OutputSource_Objects_Shutdown(void); void OutputSource_Objects_ObserveLevelLoad(void); void OutputSource_Objects_ObserveLevelUnload(void); void OutputSource_Objects_ObserveObjectMeshSwap( int32_t mesh_idx_1, int32_t mesh_idx_2); void OutputSource_Objects_ObserveObjectMeshUpdate(int32_t mesh_idx); void OutputSource_Objects_StageSkyboxMesh( const OBJECT_MESH *mesh, int16_t shade); void OutputSource_Objects_StageObjectMesh(const OBJECT_MESH *mesh); const SCENE_SOURCE *OutputSource_Objects_GetSource(void); ================================================ FILE: src/trx/game/output/sources/overlay.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_SCHEDULE_OP(queue_id, draw_func, inst) \ M_ScheduleOpHelper( \ &m_Priv, queue_id, (M_DRAW_OP_FUNC)draw_func, sizeof(inst), \ (const M_DRAW_OP *)&inst); #define M_IMAGE_CACHE_CAPACITY 5 #define M_RELATIVE_ERROR(a, b) ABS((a) - (b)) / (b) struct M_DRAW_OP; typedef void (*M_DRAW_OP_FUNC)(const struct M_DRAW_OP *); typedef struct M_DRAW_OP { M_DRAW_OP_FUNC draw; } M_DRAW_OP; typedef struct { M_DRAW_OP base; float opacity; } M_DRAW_OP_BLACK_RECTANGLE; typedef struct { M_DRAW_OP base; GLuint texture_id; int32_t width; int32_t height; float opacity; float desaturation; bool flip_y; bool use_fit; TEXTURE_FILTER texture_filter; } M_DRAW_OP_IMAGE; typedef struct { M_DRAW_OP base; GLuint texture_id; int32_t width; int32_t height; float opacity; } M_DRAW_OP_SNAPSHOT; typedef struct { M_DRAW_OP base; bool wave; float opacity; } M_DRAW_OP_PATTERN; typedef struct { char *path; int32_t width; int32_t height; } M_IMAGE_CANDIDATE; typedef struct { bool in_use; uint64_t last_used_token; char *file_name; char *scan_path; VECTOR *candidates; char *loaded_path; float loaded_for_screen_ratio; TRX_GL_TEXTURE texture; int32_t texture_width; int32_t texture_height; } M_IMAGE_CACHE_ENTRY; typedef struct { bool has_content; float captured_brightness; TRX_GL_TEXTURE texture; int32_t width; int32_t height; } M_SNAPSHOT_STATE; typedef struct { SCENE_SOURCE source; MEMORY_ARENA_ALLOCATOR alloc; VECTOR *ops[2]; OUTPUT_QUAD *renderer; TRX_GL_TEXTURE solid_black_texture; struct { OUTPUT_QUAD *renderer; uint64_t next_use_token; M_IMAGE_CACHE_ENTRY entries[M_IMAGE_CACHE_CAPACITY]; } image; struct { OUTPUT_QUAD *renderer; M_SNAPSHOT_STATE state; bool transition_active; FADER transition_fader; } snapshot; struct { OUTPUT_QUAD *renderer; bool uploaded; int32_t texture_idx; int32_t tex_page; OUTPUT_QUAD_SURFACE_DESC desc; OUTPUT_TEXTURE_SIZE atlas_size; } pattern; } M_PRIV; static M_PRIV m_Priv = { .alloc = { .default_chunk_size = 1024 * 4, }, }; static bool M_CreateTextureRGB8( TRX_GL_TEXTURE *const texture, const int32_t width, const int32_t height, const void *const data) { if (texture == nullptr || width <= 0 || height <= 0) { return false; } if (!texture->initialized) { TRX_GL_Texture_Init(texture, GL_TEXTURE_2D); } TRX_GL_Texture_Bind(texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); // RGB8 rows are tightly packed (3 bytes per pixel). With the default // unpack alignment of 4, OpenGL will assume padding at the end of each row // when width * 3 is not a multiple of 4, which looks like an incorrect // source stride. GLint prev_unpack_alignment = 0; glGetIntegerv(GL_UNPACK_ALIGNMENT, &prev_unpack_alignment); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); TRX_GL_CheckError(); glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB8, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data); glPixelStorei(GL_UNPACK_ALIGNMENT, prev_unpack_alignment); TRX_GL_CheckError(); return true; } static void M_CloseTexture(TRX_GL_TEXTURE *const texture) { if (texture != nullptr && texture->initialized) { TRX_GL_Texture_Close(texture); texture->initialized = false; } } static float M_GetScreenAspectRatio(void) { const int32_t w = Viewport_GetWidth(VIEWPORT_GAME); const int32_t h = Viewport_GetHeight(VIEWPORT_GAME); if (w <= 0 || h <= 0) { return 1.0f; } return w / (float)h; } static bool M_PrepareViewportCopy( const VIEWPORT_SPACE viewport, const int32_t desired_width, const int32_t desired_height, VIEWPORT_RECT *const rect, int32_t *const copy_width, int32_t *const copy_height) { if (rect == nullptr || copy_width == nullptr || copy_height == nullptr) { return false; } const VIEWPORT_RECT viewport_rect = Viewport_GetRect(viewport); if (viewport_rect.width <= 0 || viewport_rect.height <= 0) { return false; } *rect = viewport_rect; *copy_width = desired_width; *copy_height = desired_height; CLAMPG(*copy_width, viewport_rect.width); CLAMPG(*copy_height, viewport_rect.height); return *copy_width > 0 && *copy_height > 0; } static void M_CopyPresentedFrameToTexture( TRX_GL_TEXTURE *const texture, const int32_t width, const int32_t height) { if (texture == nullptr || !texture->initialized || width <= 0 || height <= 0) { return; } VIEWPORT_RECT rect; int32_t copy_width = 0; int32_t copy_height = 0; if (!M_PrepareViewportCopy( VIEWPORT_TARGET, width, height, &rect, ©_width, ©_height)) { return; } GLint prev_read_fbo = 0; glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prev_read_fbo); GLint prev_read_buffer = 0; glGetIntegerv(GL_READ_BUFFER, &prev_read_buffer); glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); glReadBuffer(GL_FRONT); TRX_GL_Texture_Bind(texture); glCopyTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, rect.x, rect.y, copy_width, copy_height); glBindFramebuffer(GL_READ_FRAMEBUFFER, (GLuint)prev_read_fbo); glReadBuffer(prev_read_buffer); TRX_GL_CheckError(); } static void M_EnsureSolidBlackTexture(void) { if (m_Priv.solid_black_texture.initialized) { return; } const uint8_t pixel[3] = { 0, 0, 0 }; M_CreateTextureRGB8(&m_Priv.solid_black_texture, 1, 1, &pixel[0]); } static void M_ImageCandidates_Free(M_IMAGE_CACHE_ENTRY *const e) { if (e->candidates == nullptr) { Memory_FreePointer(&e->scan_path); return; } for (int32_t i = 0; i < e->candidates->count; i++) { M_IMAGE_CANDIDATE *const candidate = Vector_Get(e->candidates, i); Memory_FreePointer(&candidate->path); } Vector_Free(e->candidates); e->candidates = nullptr; Memory_FreePointer(&e->scan_path); } static void M_ImageCandidates_Scan( M_IMAGE_CACHE_ENTRY *const e, const char *const base_image_path) { ASSERT(e != nullptr); ASSERT(base_image_path != nullptr); M_ImageCandidates_Free(e); LOG_INFO("Searching for overlay images"); VECTOR *candidates = nullptr; const char *last_slash = strrchr(base_image_path, '/'); const char *last_backslash = strrchr(base_image_path, '\\'); const char *last_sep = last_slash > last_backslash ? last_slash : last_backslash; size_t dir_len = 0; char *dir_path = nullptr; if (last_sep != nullptr) { dir_len = (size_t)(last_sep - base_image_path); dir_path = String_Format("%.*s", (int)dir_len, base_image_path); } else { dir_path = Memory_DupStr("."); } const char *const file_name = last_sep != nullptr ? last_sep + 1 : base_image_path; void *const dir_handle = File_OpenDirectory(dir_path); if (dir_handle == nullptr) { e->scan_path = dir_path; return; } candidates = Vector_Create(sizeof(M_IMAGE_CANDIDATE)); const char *entry; while ((entry = File_ReadDirectory(dir_handle)) != nullptr) { // Match the file itself, and assume it's of 16:9 aspect ratio. if (String_Equivalent(entry, file_name)) { Vector_Add( candidates, &(M_IMAGE_CANDIDATE) { .path = String_Format("%s/%s", dir_path, file_name), .width = 16, .height = 9, }); } // Match directories with pattern: x int32_t w = 0; int32_t h = 0; if (sscanf(entry, "%dx%d", &w, &h) == 2) { const char *const candidate_path = String_FormatStatic("%s/%s/%s", dir_path, entry, file_name); if (File_Exists(candidate_path)) { Vector_Add( candidates, &(M_IMAGE_CANDIDATE) { .path = Memory_DupStr(candidate_path), .width = w, .height = h, }); } } } File_CloseDirectory(dir_handle); for (int32_t i = 0; i < candidates->count; i++) { const M_IMAGE_CANDIDATE *const candidate = Vector_Get(candidates, i); LOG_INFO( "%d. %s (%d:%d)", i + 1, candidate->path, candidate->width, candidate->height); } e->scan_path = dir_path; e->candidates = candidates; } static const M_IMAGE_CANDIDATE *M_ImageCandidates_PickBest( const M_IMAGE_CACHE_ENTRY *const e, const float screen_ratio) { if (e->candidates == nullptr) { return nullptr; } int32_t best_idx = -1; float best_err = FLT_MAX; const M_IMAGE_CANDIDATE *const raw = Vector_GetData(e->candidates); for (int32_t i = 0; i < e->candidates->count; i++) { const float candidate_ratio = raw[i].width / (float)raw[i].height; const float err = M_RELATIVE_ERROR(candidate_ratio, screen_ratio); if (err < best_err) { best_err = err; best_idx = i; } } return best_idx >= 0 ? &raw[best_idx] : nullptr; } static bool M_Image_LoadIntoTexture( M_IMAGE_CACHE_ENTRY *const e, const char *const path) { ASSERT(e != nullptr); ASSERT(path != nullptr); IMAGE *const img = Image_CreateFromFile(path); if (img == nullptr) { return false; } const int32_t width = img->width; const int32_t height = img->height; if (width <= 0 || height <= 0 || img->data == nullptr) { Image_Free(img); return false; } const bool ok = M_CreateTextureRGB8(&e->texture, width, height, img->data); Image_Free(img); if (!ok) { return false; } e->texture_width = width; e->texture_height = height; return true; } static void M_ImageCacheEntry_Reset(M_IMAGE_CACHE_ENTRY *const e) { ASSERT(e != nullptr); M_CloseTexture(&e->texture); Memory_FreePointer(&e->file_name); Memory_FreePointer(&e->loaded_path); M_ImageCandidates_Free(e); *e = (M_IMAGE_CACHE_ENTRY) { 0 }; } static M_IMAGE_CACHE_ENTRY *M_ImageCache_GetEntry( M_PRIV *const p, const char *const file_name) { ASSERT(p != nullptr); ASSERT(file_name != nullptr); for (int32_t i = 0; i < M_IMAGE_CACHE_CAPACITY; i++) { M_IMAGE_CACHE_ENTRY *const e = &p->image.entries[i]; if (e->in_use && e->file_name != nullptr && String_Equivalent(e->file_name, file_name)) { return e; } } for (int32_t i = 0; i < M_IMAGE_CACHE_CAPACITY; i++) { M_IMAGE_CACHE_ENTRY *const e = &p->image.entries[i]; if (!e->in_use) { e->in_use = true; e->file_name = Memory_DupStr(file_name); return e; } } int32_t evict_idx = 0; uint64_t best_token = p->image.entries[0].last_used_token; for (int32_t i = 1; i < M_IMAGE_CACHE_CAPACITY; i++) { if (p->image.entries[i].last_used_token < best_token) { best_token = p->image.entries[i].last_used_token; evict_idx = i; } } M_ImageCacheEntry_Reset(&p->image.entries[evict_idx]); p->image.entries[evict_idx].in_use = true; p->image.entries[evict_idx].file_name = Memory_DupStr(file_name); return &p->image.entries[evict_idx]; } static bool M_ImageCache_Load(M_IMAGE_CACHE_ENTRY *const e) { ASSERT(e != nullptr); ASSERT(e->file_name != nullptr); if (e->candidates == nullptr && e->scan_path == nullptr) { M_ImageCandidates_Scan(e, e->file_name); } const float screen_ratio = M_GetScreenAspectRatio(); const bool should_reselect = e->loaded_path == nullptr || e->loaded_for_screen_ratio != screen_ratio; if (e->texture.initialized && !should_reselect) { return true; } const M_IMAGE_CANDIDATE *const best = M_ImageCandidates_PickBest(e, screen_ratio); if (best != nullptr) { const bool already_loaded = e->loaded_path != nullptr && String_Equivalent(best->path, e->loaded_path); if (!already_loaded || !e->texture.initialized) { if (M_Image_LoadIntoTexture(e, best->path)) { char *prev = e->loaded_path; e->loaded_path = Memory_DupStr(best->path); Memory_FreePointer(&prev); e->loaded_for_screen_ratio = screen_ratio; return true; } } else { e->loaded_for_screen_ratio = screen_ratio; return true; } } const bool already_loaded = e->loaded_path != nullptr && String_Equivalent(e->file_name, e->loaded_path); if (!already_loaded || !e->texture.initialized) { if (M_Image_LoadIntoTexture(e, e->file_name)) { char *prev = e->loaded_path; e->loaded_path = Memory_DupStr(e->file_name); Memory_FreePointer(&prev); e->loaded_for_screen_ratio = screen_ratio; return true; } } M_CloseTexture(&e->texture); e->texture_width = 0; e->texture_height = 0; Memory_FreePointer(&e->loaded_path); e->loaded_for_screen_ratio = 0.0f; return false; } static inline void *M_ArenaAlloc(M_PRIV *const p, const size_t sz) { void *const mem = Memory_ArenaAlloc(&p->alloc, sz); memset(mem, 0, sz); return mem; } static inline void M_ScheduleOp( M_PRIV *const p, const int32_t queue_id, M_DRAW_OP *const op) { Vector_Add(p->ops[queue_id], &op); } static void M_ScheduleOpHelper( M_PRIV *const p, const int32_t queue_id, const M_DRAW_OP_FUNC draw_func, const size_t size, const M_DRAW_OP *const op_src) { M_DRAW_OP *const op = M_ArenaAlloc(&m_Priv, size); memcpy(op, op_src, size); op->draw = draw_func; M_ScheduleOp(p, queue_id, op); } static void M_DrawOp_BlackRectangle(const M_DRAW_OP_BLACK_RECTANGLE *const op) { const M_PRIV *const p = &m_Priv; if (op->opacity <= 0.0f) { return; } M_EnsureSolidBlackTexture(); Output_Quad_SetExternalTexture( p->renderer, p->solid_black_texture.id, 1, 1, false); Output_Quad_SetEffect(p->renderer, OUTPUT_QUAD_EFFECT_NONE); Output_Quad_SetRepeat(p->renderer, 1, 1); Output_Quad_SetTextureSize(p->renderer, nullptr); Output_Quad_SetOpacity(p->renderer, op->opacity); Output_Quad_RenderWithBlend(p->renderer); } static void M_DrawOp_Image(const M_DRAW_OP_IMAGE *const op) { const M_PRIV *const p = &m_Priv; if (op->texture_id == 0 || op->width <= 0 || op->height <= 0) { return; } Output_Quad_SetExternalTexture( p->image.renderer, op->texture_id, op->width, op->height, op->flip_y); Output_Quad_SetEffect(p->image.renderer, OUTPUT_QUAD_EFFECT_NONE); Output_Quad_SetRepeat(p->image.renderer, 1, 1); Output_Quad_SetTextureSize(p->image.renderer, nullptr); Output_Quad_SetFilter(p->image.renderer, op->texture_filter); Output_SetDesaturation(op->desaturation); if (op->use_fit) { Output_Quad_SetFit( p->image.renderer, OUTPUT_QUAD_FIT_SMART, (float)op->width, (float)op->height); } else { Output_Quad_ClearFit(p->image.renderer); } Output_Quad_SetOpacity(p->image.renderer, op->opacity); if (op->opacity >= 1.0f) { Output_Quad_Render(p->image.renderer); } else { Output_Quad_RenderWithBlend(p->image.renderer); } Output_SetDesaturation(0.0f); } static void M_DrawImageImpl( const char *const file_name, const float intensity, const TEXTURE_FILTER texture_filter) { if (!Output_Overlay_LoadImage(file_name)) { return; } const M_PRIV *const p = &m_Priv; for (int32_t i = 0; i < M_IMAGE_CACHE_CAPACITY; i++) { const M_IMAGE_CACHE_ENTRY *const e = &p->image.entries[i]; if (e->in_use && e->file_name != nullptr && String_Equivalent(e->file_name, file_name) && e->texture.initialized) { M_SCHEDULE_OP( false, M_DrawOp_Image, ((M_DRAW_OP_IMAGE) { .texture_id = e->texture.id, .width = e->texture_width, .height = e->texture_height, .opacity = 1.0f, .desaturation = intensity, .flip_y = false, .use_fit = true, .texture_filter = texture_filter, })); return; } } } static void M_DrawOp_Snapshot(const M_DRAW_OP_SNAPSHOT *const op) { const M_PRIV *const p = &m_Priv; if (op->opacity <= 0.0f) { return; } if (op->texture_id == 0 || op->width <= 0 || op->height <= 0) { return; } Output_Quad_SetExternalTexture( p->snapshot.renderer, op->texture_id, op->width, op->height, true); Output_Quad_SetEffect(p->snapshot.renderer, OUTPUT_QUAD_EFFECT_NONE); Output_Quad_SetRepeat(p->snapshot.renderer, 1, 1); Output_Quad_SetTextureSize(p->snapshot.renderer, nullptr); Output_Quad_ClearFit(p->snapshot.renderer); Output_Quad_SetOpacity(p->snapshot.renderer, op->opacity); Output_Quad_RenderWithBlend(p->snapshot.renderer); } static bool M_EnsurePatternUploaded(M_PRIV *const p) { if (p->pattern.renderer == nullptr) { return false; } const OBJECT *const obj = Object_Get(O_INV_BACKGROUND); if (obj == nullptr || !obj->loaded) { return false; } const OBJECT_MESH *const mesh = Object_GetMesh(obj->mesh_idx); if (mesh == nullptr || mesh->tex_face4s.count < 1) { return false; } const int32_t texture_idx = mesh->tex_face4s.data[0].texture_idx; const OBJECT_TEXTURE *const texture = Output_GetObjectTexture(texture_idx); if (texture == nullptr) { return false; } const RGBA_8888 *const page = Output_GetTexturePage32(texture->tex_page); if (page == nullptr) { return false; } const OUTPUT_QUAD_SURFACE_DESC desc = { .width = TEXTURE_PAGE_WIDTH, .height = TEXTURE_PAGE_HEIGHT, .bit_count = 32, .tex_format = GL_RGBA, .tex_type = GL_UNSIGNED_INT_8_8_8_8_REV, .uv = { { texture->uv[0].u / 256.0f / TEXTURE_PAGE_WIDTH, texture->uv[0].v / 256.0f / TEXTURE_PAGE_HEIGHT }, { texture->uv[1].u / 256.0f / TEXTURE_PAGE_WIDTH, texture->uv[1].v / 256.0f / TEXTURE_PAGE_HEIGHT }, { texture->uv[2].u / 256.0f / TEXTURE_PAGE_WIDTH, texture->uv[2].v / 256.0f / TEXTURE_PAGE_HEIGHT }, { texture->uv[3].u / 256.0f / TEXTURE_PAGE_WIDTH, texture->uv[3].v / 256.0f / TEXTURE_PAGE_HEIGHT }, }, .pitch = TEXTURE_PAGE_WIDTH * 2, }; if (!p->pattern.uploaded || p->pattern.texture_idx != texture_idx || p->pattern.tex_page != texture->tex_page || memcmp(&p->pattern.desc, &desc, sizeof(desc)) != 0) { OUTPUT_QUAD_SURFACE_DESC tmp_desc = desc; Output_Quad_Upload(p->pattern.renderer, &tmp_desc, (uint8_t *)page); p->pattern.uploaded = true; p->pattern.texture_idx = texture_idx; p->pattern.tex_page = texture->tex_page; p->pattern.desc = desc; p->pattern.atlas_size = Output_Textures_GetAtlasSize(texture_idx); } return true; } static void M_DrawOp_Pattern(const M_DRAW_OP_PATTERN *const op) { M_PRIV *const p = &m_Priv; if (!M_EnsurePatternUploaded(p)) { return; } if (op->opacity <= 0.0f) { return; } const int32_t repeat_y = 6; const float screen_ratio = M_GetScreenAspectRatio(); const int32_t repeat_x = (int32_t)roundf(repeat_y * screen_ratio); Output_Quad_SetRepeat(p->pattern.renderer, repeat_x, repeat_y); Output_Quad_SetTextureSize( p->pattern.renderer, &(OUTPUT_QUAD_TEXTURE_SIZE) { .x0 = p->pattern.atlas_size.x0, .y0 = p->pattern.atlas_size.y0, .x1 = p->pattern.atlas_size.x1, .y1 = p->pattern.atlas_size.y1, }); Output_Quad_SetEffect( p->pattern.renderer, op->wave ? OUTPUT_QUAD_EFFECT_WAVE : OUTPUT_QUAD_EFFECT_VIGNETTE); Output_Quad_SetFilter( p->pattern.renderer, g_Config.rendering.texture_filter); Output_Quad_ClearFit(p->pattern.renderer); Output_Quad_SetOpacity(p->pattern.renderer, op->opacity); if (op->opacity >= 1.0f) { Output_Quad_Render(p->pattern.renderer); } else { Output_Quad_RenderWithBlend(p->pattern.renderer); } } static void M_EnsureSnapshotTexture(M_PRIV *const p) { const int32_t w = Viewport_GetWidth(VIEWPORT_TARGET); const int32_t h = Viewport_GetHeight(VIEWPORT_TARGET); if (w <= 0 || h <= 0) { return; } M_SNAPSHOT_STATE *const s = &p->snapshot.state; if (s->texture.initialized && s->width == w && s->height == h) { return; } s->width = w; s->height = h; M_CreateTextureRGB8(&s->texture, s->width, s->height, nullptr); Output_Quad_ClearFit(p->snapshot.renderer); } static void M_RunQueue(const VECTOR *const queue) { for (int32_t i = 0; i < queue->count; i++) { M_DRAW_OP *const op = *(M_DRAW_OP **)Vector_Get(queue, i); op->draw(op); } } static void M_RenderBegin(const SCENE_SOURCE *const source) { M_PRIV *const p = &m_Priv; Vector_Clear(p->ops[0]); Vector_Clear(p->ops[1]); Memory_ArenaReset(&p->alloc); if (p->snapshot.transition_active) { const float opacity = Fader_GetCurrentValue(&p->snapshot.transition_fader); if (opacity <= 0.0f || !p->snapshot.state.has_content || !p->snapshot.state.texture.initialized) { p->snapshot.transition_active = false; p->snapshot.state.has_content = false; return; } M_SCHEDULE_OP( false, M_DrawOp_Snapshot, ((M_DRAW_OP_SNAPSHOT) { .texture_id = p->snapshot.state.texture.id, .width = p->snapshot.state.width, .height = p->snapshot.state.height, .opacity = opacity, })); if (!Fader_IsActive(&p->snapshot.transition_fader)) { p->snapshot.transition_active = false; p->snapshot.state.has_content = false; } } } static void M_RenderPass(const SCENE_SOURCE *const src, const SCENE_PASS pass) { M_PRIV *const p = &m_Priv; if (pass == SCENE_PASS_OVERLAY_PRE_UI) { M_RunQueue(p->ops[0]); } else if (pass == SCENE_PASS_OVERLAY_POST_UI) { M_RunQueue(p->ops[1]); } } static bool M_IsDirty(const SCENE_SOURCE *const src, const SCENE_PASS pass) { M_PRIV *const p = &m_Priv; if (pass == SCENE_PASS_OVERLAY_PRE_UI) { return p->ops[0]->count > 0 || p->snapshot.transition_active; } else if (pass == SCENE_PASS_OVERLAY_POST_UI) { return p->ops[1]->count > 0; } return false; } bool Output_Overlay_LoadImage(const char *const file_name) { if (file_name == nullptr || file_name[0] == '\0') { return false; } M_PRIV *const p = &m_Priv; M_IMAGE_CACHE_ENTRY *const e = M_ImageCache_GetEntry(p, file_name); if (p->image.next_use_token == 0) { p->image.next_use_token = 1; } e->last_used_token = p->image.next_use_token++; return M_ImageCache_Load(e); } void Output_Overlay_DrawImage(const char *const file_name) { M_DrawImageImpl(file_name, 0.0f, TEXTURE_FILTER_POINT); } void Output_Overlay_DrawImageBilinear(const char *const file_name) { M_DrawImageImpl(file_name, 0.0f, TEXTURE_FILTER_BILINEAR); } void Output_Overlay_DrawImageMono( const char *const file_name, const float intensity) { M_DrawImageImpl(file_name, intensity, TEXTURE_FILTER_POINT); } void Output_Overlay_CaptureSnapshot(void) { M_PRIV *const p = &m_Priv; p->snapshot.transition_active = false; p->snapshot.state.has_content = false; M_EnsureSnapshotTexture(p); if (!p->snapshot.state.texture.initialized) { return; } M_CopyPresentedFrameToTexture( &p->snapshot.state.texture, p->snapshot.state.width, p->snapshot.state.height); p->snapshot.state.has_content = true; // Remove the captured brightness so we can reapply the current multiplier. p->snapshot.state.captured_brightness = g_Config.visuals.ui_brightness; CLAMPL(p->snapshot.state.captured_brightness, 0.001f); Output_Quad_SetBrightnessScale( p->snapshot.renderer, 1.0f / p->snapshot.state.captured_brightness); } void Output_Overlay_DrawSnapshot(const float opacity) { const M_PRIV *const p = &m_Priv; if (!p->snapshot.state.has_content || !p->snapshot.state.texture.initialized) { return; } if (opacity <= 0.0f) { return; } M_SCHEDULE_OP( false, M_DrawOp_Snapshot, ((M_DRAW_OP_SNAPSHOT) { .texture_id = p->snapshot.state.texture.id, .width = p->snapshot.state.width, .height = p->snapshot.state.height, .opacity = opacity, })); } void Output_Overlay_DrawPattern(const bool wave) { Output_Overlay_DrawPatternOpacity(wave, 1.0f); } void Output_Overlay_DrawPatternOpacity(const bool wave, const float opacity) { M_SCHEDULE_OP( false, M_DrawOp_Pattern, ((M_DRAW_OP_PATTERN) { .wave = wave, .opacity = opacity })); } void Output_Overlay_BeginTransitionFadeOut( const float duration, const float start) { M_PRIV *const p = &m_Priv; Output_Overlay_CaptureSnapshot(); if (!p->snapshot.state.has_content) { return; } p->snapshot.transition_active = true; Fader_InitTo(&p->snapshot.transition_fader, start, 0.0f, duration); } void Output_Overlay_DrawGame(void) { Interpolation_Disable(); Game_Draw(false); Interpolation_Enable(); } void Output_Overlay_DrawGameMonoCool(const float desaturation) { Output_SetDesaturation(desaturation); Output_SetGlobalTint(Color_Mix( COLOR_RGB_F_WHITE, ((RGB_F) { 0.666f, 0.666f, 1.0f }), desaturation)); Output_Overlay_DrawGame(); Output_SetGlobalTint(COLOR_RGB_F_WHITE); Output_SetDesaturation(0.0f); } void Output_Overlay_DrawGameMonoWarm(const float desaturation) { Output_SetDesaturation(desaturation); Output_SetGlobalTint(Color_Mix( COLOR_RGB_F_WHITE, ((RGB_F) { 1.0f, 0.666f, 0.666f }), desaturation)); Output_Overlay_DrawGame(); Output_SetGlobalTint(COLOR_RGB_F_WHITE); Output_SetDesaturation(0.0f); } void Output_Overlay_DrawGameMono(const float desaturation) { Output_SetDesaturation(desaturation); Output_Overlay_DrawGame(); Output_SetDesaturation(0.0f); } void Output_Overlay_DrawBlackRectangle(const float opacity, const bool post_ui) { if (opacity > 0.0f) { M_SCHEDULE_OP( post_ui, M_DrawOp_BlackRectangle, ((M_DRAW_OP_BLACK_RECTANGLE) { .opacity = opacity })); } } void OutputSource_Overlay_Init(void) { M_PRIV *const p = &m_Priv; p->renderer = Output_Quad_Create(); p->image.renderer = Output_Quad_Create(); p->snapshot.renderer = Output_Quad_Create(); p->pattern.renderer = Output_Quad_Create(); p->pattern.uploaded = false; M_EnsureSolidBlackTexture(); p->source.render_begin = M_RenderBegin; p->source.render_pass = M_RenderPass; p->source.is_dirty = M_IsDirty; p->ops[0] = Vector_Create(sizeof(M_DRAW_OP *)); p->ops[1] = Vector_Create(sizeof(M_DRAW_OP *)); SceneCompositor_AddSource(&p->source); } void OutputSource_Overlay_Shutdown(void) { M_PRIV *const p = &m_Priv; if (p->renderer != nullptr) { Output_Quad_Destroy(p->renderer); p->renderer = nullptr; } if (p->image.renderer != nullptr) { Output_Quad_Destroy(p->image.renderer); p->image.renderer = nullptr; } if (p->snapshot.renderer != nullptr) { Output_Quad_Destroy(p->snapshot.renderer); p->snapshot.renderer = nullptr; } if (p->pattern.renderer != nullptr) { Output_Quad_Destroy(p->pattern.renderer); p->pattern.renderer = nullptr; } for (int32_t i = 0; i < M_IMAGE_CACHE_CAPACITY; i++) { if (p->image.entries[i].in_use) { M_ImageCacheEntry_Reset(&p->image.entries[i]); } } p->pattern.uploaded = false; M_CloseTexture(&p->snapshot.state.texture); p->snapshot.state.has_content = false; M_CloseTexture(&p->solid_black_texture); if (p->ops[0] != nullptr) { Vector_Free(p->ops[0]); p->ops[0] = nullptr; } if (p->ops[1] != nullptr) { Vector_Free(p->ops[1]); p->ops[1] = nullptr; } Memory_ArenaFree(&p->alloc); } ================================================ FILE: src/trx/game/output/sources/overlay.h ================================================ #pragma once void OutputSource_Overlay_Init(void); void OutputSource_Overlay_Shutdown(void); ================================================ FILE: src/trx/game/output/sources/poly_fx.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef struct { XYZW_F pos; XYZW_F normal; OUTPUT_UVW uvw; OUTPUT_TEXTURE_SIZE texture_size; float trapezoid_ratio[2]; OUTPUT_USHORT flags; RGBA_8888 color; float shade; } M_VERTEX; typedef struct { int32_t sprite_idx; bool use_custom_uv; uint8_t corner_count; float z_depth_adjust; XYZ_32 world_pos[4]; OUTPUT_UVW uvw[4]; OUTPUT_TEXTURE_SIZE texture_size[4]; float disp[4][2]; RGBA_8888 color[4]; uint16_t flags; } M_PRIM; typedef struct { int32_t sort_key; const M_PRIM *prim; } M_PRIM_SORT; typedef struct { SCENE_SOURCE source; OUTPUT_MESH_SHADER *shader; VECTOR *scheduled_transparent; // M_PRIM VECTOR *scheduled_blend_add; // M_PRIM VECTOR *scheduled_blend_sub; // M_PRIM VECTOR *sorted; // M_PRIM_SORT VECTOR *vertices; // M_VERTEX GLuint vao; GLuint vbo; } M_PRIV; static M_PRIV m_Priv; static int M_ComparePrimDepth(const void *const a, const void *const b) { const M_PRIM_SORT *const prim_a = a; const M_PRIM_SORT *const prim_b = b; if (prim_b->sort_key == prim_a->sort_key) { return (intptr_t)prim_b->prim - (intptr_t)prim_a->prim; } return prim_b->sort_key - prim_a->sort_key; } static int32_t M_GetViewDepth(const XYZ_32 pos) { // clang-format off return g_ViewMatrix._20 * pos.x + g_ViewMatrix._21 * pos.y + g_ViewMatrix._22 * pos.z + g_ViewMatrix._23; // clang-format on } static XYZ_32 M_GetSparkRenderPos(const SPARK *const spark, const float ratio) { const bool use_current_state = (int32_t)spark->s_life - (int32_t)spark->life <= 1; if (use_current_state) { return Sparks_GetWorldPos(spark); } if ((spark->flags & SPARK_F_ATTACHED_NODE) != 0U) { const XYZ_32 current_pos = Sparks_GetWorldPos(spark); return (XYZ_32) { .x = (int32_t)LERP(spark->prev_world_pos.x, current_pos.x, ratio), .y = (int32_t)LERP(spark->prev_world_pos.y, current_pos.y, ratio), .z = (int32_t)LERP(spark->prev_world_pos.z, current_pos.z, ratio), }; } const XYZ_32 local_pos = { .x = (int32_t)LERP(spark->prev_pos.x, spark->pos.x, ratio), .y = (int32_t)LERP(spark->prev_pos.y, spark->pos.y, ratio), .z = (int32_t)LERP(spark->prev_pos.z, spark->pos.z, ratio), }; if ((spark->flags & SPARK_F_FX) != 0U) { const EFFECT *const effect = Effect_Get(spark->effect_num); if ((spark->flags & SPARK_F_ATTACHED_POS) != 0U) { return effect->interp.result.pos; } return (XYZ_32) { .x = effect->interp.result.pos.x + local_pos.x, .y = effect->interp.result.pos.y + local_pos.y, .z = effect->interp.result.pos.z + local_pos.z, }; } if ((spark->flags & SPARK_F_ITEM) != 0U) { const ITEM *const item = Item_Get(spark->item_num); if ((spark->flags & SPARK_F_ATTACHED_POS) != 0U) { return item->interp.result.pos; } return (XYZ_32) { .x = item->interp.result.pos.x + local_pos.x, .y = item->interp.result.pos.y + local_pos.y, .z = item->interp.result.pos.z + local_pos.z, }; } return local_pos; } static void M_ApplyTintToColors(RGBA_8888 color[4], const uint8_t corner_count) { const RGB_F tint = Output_GetTint(); for (uint8_t i = 0; i < corner_count; i++) { int32_t r = (int32_t)((float)color[i].r * tint.r); int32_t g = (int32_t)((float)color[i].g * tint.g); int32_t b = (int32_t)((float)color[i].b * tint.b); CLAMP(r, 0, 255); CLAMP(g, 0, 255); CLAMP(b, 0, 255); color[i].r = (uint8_t)r; color[i].g = (uint8_t)g; color[i].b = (uint8_t)b; } } static XYZ_32 M_GetPrimCentroid(const M_PRIM *const prim) { XYZ_32 centroid = { 0, 0, 0 }; for (uint8_t i = 0; i < prim->corner_count; i++) { centroid.x += prim->world_pos[i].x; centroid.y += prim->world_pos[i].y; centroid.z += prim->world_pos[i].z; } centroid.x /= (int32_t)prim->corner_count; centroid.y /= (int32_t)prim->corner_count; centroid.z /= (int32_t)prim->corner_count; return centroid; } static VECTOR *M_GetScheduledVectorForPass( M_PRIV *const p, const SCENE_PASS pass) { if (pass == SCENE_PASS_BLEND_ADD) { return p->scheduled_blend_add; } if (pass == SCENE_PASS_BLEND_SUB) { return p->scheduled_blend_sub; } return p->scheduled_transparent; } static void M_SortPrims(M_PRIV *const p, const SCENE_PASS pass) { Vector_Clear(p->sorted); const VECTOR *const prims = M_GetScheduledVectorForPass(p, pass); for (int32_t i = 0; i < prims->count; i++) { const M_PRIM *const prim = Vector_Get(prims, i); const XYZ_32 centroid = M_GetPrimCentroid(prim); const M_PRIM_SORT sort = { .sort_key = M_GetViewDepth(centroid), .prim = prim, }; Vector_Add(p->sorted, &sort); } if (p->sorted->count > 1) { qsort( Vector_GetData(p->sorted), p->sorted->count, sizeof(M_PRIM_SORT), M_ComparePrimDepth); } } static void M_EmitPrimVertices(M_PRIV *const p, const M_PRIM *const prim) { const uint16_t flags = prim->flags; const uint8_t corner_count = prim->corner_count; const int32_t sprite_corner_map_quad[4] = { 0, 1, 2, 3 }; const int32_t sprite_corner_map_tri[3] = { 0, 1, 3 }; const int32_t *sprite_corner_map = sprite_corner_map_quad; if (corner_count == 3U) { sprite_corner_map = sprite_corner_map_tri; } OUTPUT_UVW uvw[4]; OUTPUT_TEXTURE_SIZE texture_size[4]; for (uint8_t i = 0; i < corner_count; i++) { if (prim->use_custom_uv) { uvw[i] = prim->uvw[i]; texture_size[i] = prim->texture_size[i]; } else if (prim->sprite_idx >= 0) { const int32_t sprite_corner = sprite_corner_map[i]; const int32_t uvw_idx = Output_Textures_GetSpriteUVWIndex( prim->sprite_idx, (int16_t)sprite_corner); uvw[i] = Output_Textures_GetUVW(uvw_idx); texture_size[i] = Output_Textures_GetAtlasSize(uvw_idx / 4); } else { uvw[i] = (OUTPUT_UVW) { 0.0f, 0.0f, 0.0f }; texture_size[i] = (OUTPUT_TEXTURE_SIZE) { 0.0f, 0.0f, 0.0f, 0.0f }; } } const int32_t idx_quad[2][OUTPUT_QUAD_VERTICES] = { { 0, 1, 2, 0, 2, 3 }, // front { 0, 2, 1, 0, 3, 2 }, // back }; const int32_t idx_tri[2][3] = { { 0, 1, 2 }, // front { 0, 2, 1 }, // back }; const int32_t *idx = (const int32_t *)idx_quad; int32_t vertex_count = OUTPUT_QUAD_VERTICES; if (corner_count == 3U) { idx = (const int32_t *)idx_tri; vertex_count = 3; } for (int32_t side = 0; side < 2; side++) { for (int32_t i = 0; i < vertex_count; i++) { const int32_t corner = idx[side * vertex_count + i]; const int32_t uv_idx = corner; const M_VERTEX v = { .pos = { .x = (float)prim->world_pos[corner].x, .y = (float)prim->world_pos[corner].y, .z = (float)prim->world_pos[corner].z, .w = prim->z_depth_adjust, }, .normal = { .x = prim->disp[corner][0], .y = prim->disp[corner][1], .z = 0.0f, .w = 0.0f, }, .uvw = uvw[uv_idx], .texture_size = texture_size[uv_idx], .trapezoid_ratio = { 1.0f, 1.0f }, .flags = flags, .color = prim->color[corner], .shade = (float)SHADE_NEUTRAL, }; Vector_Add(p->vertices, &v); } } } static void M_RenderBegin(const SCENE_SOURCE *const source) { M_PRIV *const p = &m_Priv; Vector_Clear(p->scheduled_transparent); Vector_Clear(p->scheduled_blend_add); Vector_Clear(p->scheduled_blend_sub); Vector_Clear(p->vertices); Vector_Clear(p->sorted); } static void M_RenderPass(const SCENE_SOURCE *const source, SCENE_PASS pass) { M_PRIV *const p = &m_Priv; if (pass != SCENE_PASS_OPAQUE && pass != SCENE_PASS_TRANSPARENT && pass != SCENE_PASS_BLEND_SUB && pass != SCENE_PASS_BLEND_ADD) { return; } if (pass == SCENE_PASS_OPAQUE) { pass = SCENE_PASS_TRANSPARENT; } else { Vector_Clear(p->vertices); Vector_Clear(p->sorted); } M_SortPrims(p, pass); for (int32_t i = 0; i < p->sorted->count; i++) { const M_PRIM_SORT *const sort = Vector_Get(p->sorted, i); M_EmitPrimVertices(p, sort->prim); } glBindVertexArray(p->vao); glBindBuffer(GL_ARRAY_BUFFER, p->vbo); TRX_GL_TRACK_DATA( glBufferData, GL_ARRAY_BUFFER, p->vertices->count * sizeof(M_VERTEX), Vector_GetData(p->vertices), GL_DYNAMIC_DRAW); // PolyFX primitives are staged with Output_GetTint() already applied at // stage time, because different rooms can have different water state. Output_MeshShader_UploadTint(p->shader, COLOR_RGB_F_WHITE); Output_MeshShader_UploadWaterEffect(p->shader, 0); Output_MeshShader_UploadWibbleEffect(p->shader, false); Output_MeshShader_UploadModelMatrix(p->shader, &g_IDMatrix); glDrawArrays(GL_TRIANGLES, 0, p->vertices->count); TRX_GL_CheckError(); } static bool M_IsDirty(const SCENE_SOURCE *const source, const SCENE_PASS pass) { const M_PRIV *const p = &m_Priv; if (pass == SCENE_PASS_TRANSPARENT || pass == SCENE_PASS_OPAQUE) { return p->scheduled_transparent->count > 0; } if (pass == SCENE_PASS_BLEND_SUB) { return p->scheduled_blend_sub->count > 0; } if (pass == SCENE_PASS_BLEND_ADD) { return p->scheduled_blend_add->count > 0; } return false; } void OutputSource_PolyFX_Init(void) { M_PRIV *const p = &m_Priv; p->shader = Output_GetMeshShader(); p->scheduled_transparent = Vector_Create(sizeof(M_PRIM)); p->scheduled_blend_add = Vector_Create(sizeof(M_PRIM)); p->scheduled_blend_sub = Vector_Create(sizeof(M_PRIM)); p->sorted = Vector_Create(sizeof(M_PRIM_SORT)); p->vertices = Vector_Create(sizeof(M_VERTEX)); p->source.render_begin = M_RenderBegin; p->source.render_pass = M_RenderPass; p->source.is_dirty = M_IsDirty; SceneCompositor_AddSource(&p->source); glGenVertexArrays(1, &p->vao); glBindVertexArray(p->vao); glGenBuffers(1, &p->vbo); glBindBuffer(GL_ARRAY_BUFFER, p->vbo); glEnableVertexAttribArray(OUTPUT_MESH_ATTR_POS); glEnableVertexAttribArray(OUTPUT_MESH_ATTR_NORMAL); glEnableVertexAttribArray(OUTPUT_MESH_ATTR_UVW); glEnableVertexAttribArray(OUTPUT_MESH_ATTR_TEXTURE_SIZE); glEnableVertexAttribArray(OUTPUT_MESH_ATTR_TRAPEZOID_RATIO); glEnableVertexAttribArray(OUTPUT_MESH_ATTR_FLAGS); glEnableVertexAttribArray(OUTPUT_MESH_ATTR_COLOR); glEnableVertexAttribArray(OUTPUT_MESH_ATTR_SHADE); glVertexAttribPointer( OUTPUT_MESH_ATTR_POS, 4, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX), (void *)(intptr_t)offsetof(M_VERTEX, pos)); glVertexAttribPointer( OUTPUT_MESH_ATTR_NORMAL, 4, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX), (void *)(intptr_t)offsetof(M_VERTEX, normal)); glVertexAttribPointer( OUTPUT_MESH_ATTR_UVW, 3, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX), (void *)(intptr_t)offsetof(M_VERTEX, uvw)); glVertexAttribPointer( OUTPUT_MESH_ATTR_TEXTURE_SIZE, 4, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX), (void *)(intptr_t)offsetof(M_VERTEX, texture_size)); glVertexAttribPointer( OUTPUT_MESH_ATTR_TRAPEZOID_RATIO, 2, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX), (void *)(intptr_t)offsetof(M_VERTEX, trapezoid_ratio)); glVertexAttribIPointer( OUTPUT_MESH_ATTR_FLAGS, 1, OUTPUT_USHORT_GL, sizeof(M_VERTEX), (void *)(intptr_t)offsetof(M_VERTEX, flags)); glVertexAttribPointer( OUTPUT_MESH_ATTR_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(M_VERTEX), (void *)(intptr_t)offsetof(M_VERTEX, color)); glVertexAttribPointer( OUTPUT_MESH_ATTR_SHADE, 1, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX), (void *)(intptr_t)offsetof(M_VERTEX, shade)); } void OutputSource_PolyFX_Shutdown(void) { M_PRIV *const p = &m_Priv; if (p->scheduled_transparent != nullptr) { Vector_Free(p->scheduled_transparent); p->scheduled_transparent = nullptr; } if (p->scheduled_blend_add != nullptr) { Vector_Free(p->scheduled_blend_add); p->scheduled_blend_add = nullptr; } if (p->scheduled_blend_sub != nullptr) { Vector_Free(p->scheduled_blend_sub); p->scheduled_blend_sub = nullptr; } if (p->sorted != nullptr) { Vector_Free(p->sorted); p->sorted = nullptr; } if (p->vertices != nullptr) { Vector_Free(p->vertices); p->vertices = nullptr; } if (p->vao != 0) { glDeleteVertexArrays(1, &p->vao); p->vao = 0; } if (p->vbo != 0) { glDeleteBuffers(1, &p->vbo); p->vbo = 0; } } static void M_StagePrim( const int32_t sprite_idx, const uint8_t corner_count, const XYZ_32 *const world_pos, const float (*disp)[2], const RGBA_8888 *const color, const uint16_t flags, const float z_depth_adjust, VECTOR *const target) { M_PRIM prim; prim.sprite_idx = sprite_idx; prim.use_custom_uv = false; prim.corner_count = corner_count; prim.z_depth_adjust = z_depth_adjust; memset(prim.world_pos, 0, sizeof(prim.world_pos)); memcpy(prim.world_pos, world_pos, sizeof(prim.world_pos[0]) * corner_count); memset(prim.uvw, 0, sizeof(prim.uvw)); memset(prim.texture_size, 0, sizeof(prim.texture_size)); if (disp != nullptr) { memset(prim.disp, 0, sizeof(prim.disp)); memcpy(prim.disp, disp, sizeof(prim.disp[0]) * corner_count); } else { memset(prim.disp, 0, sizeof(prim.disp)); } memset(prim.color, 0, sizeof(prim.color)); memcpy(prim.color, color, sizeof(prim.color[0]) * corner_count); M_ApplyTintToColors(prim.color, corner_count); prim.flags = flags; Vector_Add(target, &prim); } static VECTOR *M_GetScheduledVectorForDrawType( M_PRIV *const p, const DRAW_TYPE draw_type) { if (draw_type == DRAW_BLEND_ADD || draw_type == DRAW_REFLECTIVE_BLEND_ADD) { return p->scheduled_blend_add; } if (draw_type == DRAW_BLEND_SUB) { return p->scheduled_blend_sub; } return p->scheduled_transparent; } void OutputSource_PolyFX_StageSpriteQuadWorld( const int32_t sprite_idx, const XYZ_32 world_pos[4], const RGBA_8888 color[4], const DRAW_TYPE draw_type) { OutputSource_PolyFX_StageSpriteQuadWorldDepth( sprite_idx, world_pos, color, 0.0f, draw_type); } void OutputSource_PolyFX_StageSpriteQuadWorldDepth( const int32_t sprite_idx, const XYZ_32 world_pos[4], const RGBA_8888 color[4], const float z_depth_adjust, const DRAW_TYPE draw_type) { M_PRIV *const p = &m_Priv; VECTOR *const target = M_GetScheduledVectorForDrawType(p, draw_type); M_StagePrim( sprite_idx, 4, &world_pos[0], nullptr, &color[0], VERT_NO_LIGHTING | VERT_NO_WIBBLE, z_depth_adjust, target); } void OutputSource_PolyFX_StageSpriteTriWorld( const int32_t sprite_idx, const XYZ_32 world_pos[3], const RGBA_8888 color[3], const DRAW_TYPE draw_type) { OutputSource_PolyFX_StageSpriteTriWorldDepth( sprite_idx, world_pos, color, 0.0f, draw_type); } void OutputSource_PolyFX_StageSpriteTriWorldDepth( const int32_t sprite_idx, const XYZ_32 world_pos[3], const RGBA_8888 color[3], const float z_depth_adjust, const DRAW_TYPE draw_type) { M_PRIV *const p = &m_Priv; VECTOR *const target = M_GetScheduledVectorForDrawType(p, draw_type); M_StagePrim( sprite_idx, 3, &world_pos[0], nullptr, &color[0], VERT_NO_LIGHTING | VERT_NO_WIBBLE, z_depth_adjust, target); } void OutputSource_PolyFX_StageQuadExt( const int32_t sprite_idx, const XYZ_32 world_pos[4], const float disp[4][2], const RGBA_8888 color[4], const uint16_t flags, const DRAW_TYPE draw_type) { M_PRIV *const p = &m_Priv; VECTOR *const target = M_GetScheduledVectorForDrawType(p, draw_type); M_StagePrim( sprite_idx, 4, &world_pos[0], disp, &color[0], flags, 0.0f, target); } void OutputSource_PolyFX_StageQuadExtUV( const XYZ_32 world_pos[4], const OUTPUT_UVW uvw[4], const OUTPUT_TEXTURE_SIZE texture_size[4], const float disp[4][2], const RGBA_8888 color[4], const uint16_t flags, const DRAW_TYPE draw_type) { M_PRIV *const p = &m_Priv; VECTOR *const target = M_GetScheduledVectorForDrawType(p, draw_type); M_PRIM prim; prim.sprite_idx = -1; prim.use_custom_uv = true; prim.corner_count = 4; prim.z_depth_adjust = 0.0f; memcpy(prim.world_pos, world_pos, sizeof(prim.world_pos)); memcpy(prim.uvw, uvw, sizeof(prim.uvw)); memcpy(prim.texture_size, texture_size, sizeof(prim.texture_size)); if (disp != nullptr) { memcpy(prim.disp, disp, sizeof(prim.disp)); } else { memset(prim.disp, 0, sizeof(prim.disp)); } memcpy(prim.color, color, sizeof(prim.color)); M_ApplyTintToColors(prim.color, 4); prim.flags = flags; Vector_Add(target, &prim); } void OutputSource_PolyFX_StageTriExtUV( const XYZ_32 world_pos[3], const OUTPUT_UVW uvw[3], const OUTPUT_TEXTURE_SIZE texture_size[3], const float disp[3][2], const RGBA_8888 color[3], const uint16_t flags, const DRAW_TYPE draw_type) { M_PRIV *const p = &m_Priv; VECTOR *const target = M_GetScheduledVectorForDrawType(p, draw_type); M_PRIM prim; prim.sprite_idx = -1; prim.use_custom_uv = true; prim.corner_count = 3; prim.z_depth_adjust = 0.0f; memset(prim.world_pos, 0, sizeof(prim.world_pos)); memcpy(prim.world_pos, world_pos, sizeof(world_pos[0]) * 3); memset(prim.uvw, 0, sizeof(prim.uvw)); memcpy(prim.uvw, uvw, sizeof(uvw[0]) * 3); memset(prim.texture_size, 0, sizeof(prim.texture_size)); memcpy(prim.texture_size, texture_size, sizeof(texture_size[0]) * 3); if (disp != nullptr) { memset(prim.disp, 0, sizeof(prim.disp)); memcpy(prim.disp, disp, sizeof(disp[0]) * 3); } else { memset(prim.disp, 0, sizeof(prim.disp)); } memset(prim.color, 0, sizeof(prim.color)); memcpy(prim.color, color, sizeof(color[0]) * 3); M_ApplyTintToColors(prim.color, 3); prim.flags = flags; Vector_Add(target, &prim); } void OutputSource_PolyFX_StageLineSegment( const XYZ_32 from, const RGBA_8888 from_color, const XYZ_32 to, const RGBA_8888 to_color, const float half_width, const DRAW_TYPE draw_type) { const int64_t zv_mid = (M_GetViewDepth(from) + M_GetViewDepth(to)) / 2; const int64_t near_z = Output_GetNearZ(); const int64_t far_z = Output_GetFarZ(); if (zv_mid <= near_z || zv_mid >= far_z) { return; } const float delta_x = (float)(to.x - from.x); const float delta_y = (float)(to.y - from.y); const float delta_z = (float)(to.z - from.z); float dir_len = sqrtf(delta_x * delta_x + delta_y * delta_y + delta_z * delta_z); if (dir_len <= 0.00001f) { return; } dir_len = 1.0f / dir_len; const float dir_x = delta_x * dir_len; const float dir_y = delta_y * dir_len; const float dir_z = delta_z * dir_len; const XYZ_32 mid = { (from.x + to.x) / 2, (from.y + to.y) / 2, (from.z + to.z) / 2, }; const float vx = (float)(mid.x - g_Camera.pos.x); const float vy = (float)(mid.y - g_Camera.pos.y); const float vz = (float)(mid.z - g_Camera.pos.z); float v_len = sqrtf(vx * vx + vy * vy + vz * vz); if (v_len <= 0.00001f) { return; } v_len = 1.0f / v_len; const float view_x = vx * v_len; const float view_y = vy * v_len; const float view_z = vz * v_len; float side_x = dir_y * view_z - dir_z * view_y; float side_y = dir_z * view_x - dir_x * view_z; float side_z = dir_x * view_y - dir_y * view_x; float side_len = sqrtf(side_x * side_x + side_y * side_y + side_z * side_z); if (side_len <= 0.00001f) { side_x = dir_y * 0.0f - dir_z * 1.0f; side_y = dir_z * 0.0f - dir_x * 0.0f; side_z = dir_x * 1.0f - dir_y * 0.0f; side_len = sqrtf(side_x * side_x + side_y * side_y + side_z * side_z); if (side_len <= 0.00001f) { return; } } side_len = 1.0f / side_len; side_x *= side_len; side_y *= side_len; side_z *= side_len; const XYZ_32 world_pos[4] = { { from.x - (int32_t)lrintf(side_x * half_width), from.y - (int32_t)lrintf(side_y * half_width), from.z - (int32_t)lrintf(side_z * half_width), }, { from.x + (int32_t)lrintf(side_x * half_width), from.y + (int32_t)lrintf(side_y * half_width), from.z + (int32_t)lrintf(side_z * half_width), }, { to.x + (int32_t)lrintf(side_x * half_width), to.y + (int32_t)lrintf(side_y * half_width), to.z + (int32_t)lrintf(side_z * half_width), }, { to.x - (int32_t)lrintf(side_x * half_width), to.y - (int32_t)lrintf(side_y * half_width), to.z - (int32_t)lrintf(side_z * half_width), }, }; const RGBA_8888 color[4] = { from_color, from_color, to_color, to_color }; const float disp[4][2] = {}; OutputSource_PolyFX_StageQuadExt( -1, world_pos, disp, color, VERT_FLAT_SHADED | VERT_NO_LIGHTING | VERT_NO_WIBBLE, draw_type); } void OutputSource_PolyFX_StageSpark(const SPARK *const spark) { if (spark == nullptr || !spark->on) { return; } DRAW_TYPE draw_type = spark->draw_type; const float ratio = Interpolation_GetWorldRate(); const bool use_current_state = (int32_t)spark->s_life - (int32_t)spark->life <= 1; const XYZ_32 pos = M_GetSparkRenderPos(spark, ratio); const XYZ_32 world_pos[4] = { pos, pos, pos, pos }; const int64_t zv = M_GetViewDepth(pos); const int64_t near_z = Output_GetNearZ(); const int64_t far_z = Output_GetFarZ(); if (zv <= near_z || zv >= far_z) { return; } int32_t vpos_z = (int32_t)(zv >> W2V_SHIFT); if (vpos_z == 0) { vpos_z = 1; } const RGB_888 render_color = use_current_state ? spark->color : (RGB_888) { .r = (uint8_t)LERP( (int32_t)spark->prev_color.r, (int32_t)spark->color.r, ratio), .g = (uint8_t)LERP( (int32_t)spark->prev_color.g, (int32_t)spark->color.g, ratio), .b = (uint8_t)LERP( (int32_t)spark->prev_color.b, (int32_t)spark->color.b, ratio), }; const int32_t render_width = use_current_state ? (int32_t)spark->size.width : (int32_t)LERP( (int32_t)spark->prev_size.width, (int32_t)spark->size.width, ratio); const int32_t render_height = use_current_state ? (int32_t)spark->size.height : (int32_t)LERP( (int32_t)spark->prev_size.height, (int32_t)spark->size.height, ratio); int32_t sw = render_width; int32_t sh = render_height; const bool use_sprite = (spark->flags & SPARK_F_SPRITE) != 0U; if ((spark->flags & SPARK_F_SCALE) != 0U) { const int32_t scalar = spark->scalar; sw = (int32_t)(((((int64_t)sw * g_PhdPersp) << scalar) / vpos_z)); sh = (int32_t)(((((int64_t)sh * g_PhdPersp) << scalar) / vpos_z)); if (use_sprite) { const int32_t max_w = render_width << scalar; const int32_t max_h = render_height << scalar; int32_t min_wh = 4; if ((spark->flags & SPARK_F_ATTACHED_NODE) != 0U && spark->node_num == 0U) { min_wh = 2; } CLAMP(sw, min_wh, max_w); CLAMP(sh, min_wh, max_h); } else { const int32_t max_w = render_width << 2; const int32_t max_h = render_height << 2; CLAMP(sw, 1, max_w); CLAMP(sh, 1, max_h); } } const float w = ((sw / 2.0f) * (float)vpos_z) / (float)g_PhdPersp; const float h = ((sh / 2.0f) * (float)vpos_z) / (float)g_PhdPersp; float disp[4][2] = { { -w, -h }, { -w, h }, { w, h }, { w, -h }, }; RGBA_8888 color = { render_color.r, render_color.g, render_color.b, 255 }; if ((spark->flags & SPARK_F_ROTATE) != 0U) { const int32_t rot_angle = use_current_state ? (int32_t)spark->rot_angle : Math_AngleMean( (int32_t)spark->prev_rot_angle, (int32_t)spark->rot_angle, ratio); const int32_t angle = rot_angle * DEG_360 / 0xFFF.p0; const float s = Math_Sin(angle) / (float)(1 << W2V_SHIFT); const float c = Math_Cos(angle) / (float)(1 << W2V_SHIFT); for (int32_t i = 0; i < 4; i++) { const float x = disp[i][0]; const float y = disp[i][1]; disp[i][0] = x * c - y * s; disp[i][1] = x * s + y * c; } } uint16_t flags = VERT_NO_LIGHTING | VERT_NO_WIBBLE | VERT_BILLBOARD | VERT_ABS_SPRITE; int32_t sprite_idx = spark->sprite_idx; if ((spark->flags & SPARK_F_SPRITE) == 0U) { flags |= VERT_FLAT_SHADED; sprite_idx = -1; if (draw_type == DRAW_BLEND_ADD || draw_type == DRAW_REFLECTIVE_BLEND_ADD) { color.a = 128; } draw_type = DRAW_BLEND; } M_PRIV *const p = &m_Priv; VECTOR *const target = M_GetScheduledVectorForDrawType(p, draw_type); const RGBA_8888 world_color[4] = { color, color, color, color }; M_StagePrim( sprite_idx, 4, &world_pos[0], disp, &world_color[0], flags, 0.0f, target); } ================================================ FILE: src/trx/game/output/sources/poly_fx.h ================================================ #pragma once #include #include #include #include #include #include #include void OutputSource_PolyFX_Init(void); void OutputSource_PolyFX_Shutdown(void); void OutputSource_PolyFX_StageSpriteQuadWorld( int32_t sprite_idx, const XYZ_32 world_pos[4], const RGBA_8888 color[4], DRAW_TYPE draw_type); void OutputSource_PolyFX_StageSpriteQuadWorldDepth( int32_t sprite_idx, const XYZ_32 world_pos[4], const RGBA_8888 color[4], float z_depth_adjust, DRAW_TYPE draw_type); void OutputSource_PolyFX_StageSpriteTriWorld( int32_t sprite_idx, const XYZ_32 world_pos[3], const RGBA_8888 color[3], DRAW_TYPE draw_type); void OutputSource_PolyFX_StageSpriteTriWorldDepth( int32_t sprite_idx, const XYZ_32 world_pos[3], const RGBA_8888 color[3], float z_depth_adjust, DRAW_TYPE draw_type); void OutputSource_PolyFX_StageQuadExt( int32_t sprite_idx, const XYZ_32 world_pos[4], const float disp[4][2], const RGBA_8888 color[4], uint16_t flags, DRAW_TYPE draw_type); void OutputSource_PolyFX_StageQuadExtUV( const XYZ_32 world_pos[4], const OUTPUT_UVW uvw[4], const OUTPUT_TEXTURE_SIZE texture_size[4], const float disp[4][2], const RGBA_8888 color[4], uint16_t flags, DRAW_TYPE draw_type); void OutputSource_PolyFX_StageTriExtUV( const XYZ_32 world_pos[3], const OUTPUT_UVW uvw[3], const OUTPUT_TEXTURE_SIZE texture_size[3], const float disp[3][2], const RGBA_8888 color[3], uint16_t flags, DRAW_TYPE draw_type); void OutputSource_PolyFX_StageLineSegment( XYZ_32 from, RGBA_8888 from_color, XYZ_32 to, RGBA_8888 to_color, float half_width, DRAW_TYPE draw_type); void OutputSource_PolyFX_StageSpark(const SPARK *spark); ================================================ FILE: src/trx/game/output/sources/rooms.c ================================================ #include #include #include #include #include #include #include #include #include #include typedef struct { MESH_BATCHER *batcher; size_t mesh_count; OUTPUT_MESH **meshes; } M_PRIV; static M_PRIV m_Priv = {}; static SCENE_PASS M_GetScenePass(const FACE *const face) { return Output_Textures_GetObjectTextureScenePass(face->texture_idx); } static void M_AddRoomFace( MESH_BUILDER *const builder, const FACE *const face, const ROOM *const room) { OUTPUT_MESH_VERTEX vertices[4]; ASSERT(face->vertex_count <= 4); for (int32_t i = 0; i < face->vertex_count; i++) { const ROOM_VERTEX *const room_vert = &room->mesh.vertices[face->vertices[i]]; uint16_t flags = 0; if (room_vert->flags.disable_wibble) { flags |= VERT_NO_WIBBLE; } if (room_vert->flags.move) { flags |= VERT_MOVE; } if (room_vert->flags.glow) { flags |= VERT_GLOW; } if (Output_Textures_GetObjectTextureScenePass(face->texture_idx) == SCENE_PASS_OPAQUE) { flags |= VERT_NO_ALPHA_DISCARD; } flags |= VERT_USE_DYNAMIC_LIGHT; const XYZ_16 *const pos = &room_vert->pos; vertices[i] = (OUTPUT_MESH_VERTEX) { .pos = { .x = pos->x, .y = pos->y, .z = pos->z }, .flags = flags, .uvw_idx = Output_Textures_GetObjectUVWIndex(face->texture_idx, i), .shade = room_vert->light_base, .light_table_idx = room_vert->light_table_value, .color = room_vert->color, .trapezoid_ratio = { [0] = face->texture_zw[i].z, [1] = face->texture_zw[i].w, }, }; } MeshBuilder_AddVertices(builder, vertices, face->vertex_count); MeshBuilder_AddFan(builder, M_GetScenePass(face), face->double_sided); } static int32_t M_GetWaterEffect(const ROOM *const room) { if (g_TRVersion >= 3) { return 2 + (int32_t)room->water_scheme; } return Output_GetWaterEffect() ? 1 : 0; } static void M_PrepareMeshes(M_PRIV *const p) { p->mesh_count = Room_GetCount(); p->meshes = Memory_Alloc(sizeof(OUTPUT_MESH *) * p->mesh_count); MESH_BUILDER *const builder = MeshBuilder_Create(); for (int32_t i = 0; i < Room_GetCount(); i++) { const ROOM *const room = Room_Get(i); for (int32_t j = 0; j < room->mesh.all_faces.count; j++) { M_AddRoomFace(builder, &room->mesh.all_faces.data[j], room); } int32_t stack = 0; XYZ_16 prev_pos = { -1, -1, -1 }; for (int32_t j = 0; j < room->mesh.sprites.count; j++) { const ROOM_SPRITE *const sprite = &room->mesh.sprites.data[j]; const ROOM_VERTEX *const vert = &room->mesh.vertices[sprite->vertex]; if (vert->pos.x == prev_pos.x && vert->pos.z == prev_pos.z) { stack++; } else { stack = 0; } MeshBuilder_AddRoomSprite( builder, sprite, room, stack * -0.005f, VERT_USE_DYNAMIC_LIGHT); prev_pos = vert->pos; } OUTPUT_MESH *const mesh = MeshBuilder_Seal(builder); if (mesh != nullptr) { MeshBatcher_AddMesh(p->batcher, mesh); } p->meshes[i] = mesh; } MeshBuilder_Destroy(builder); } static void M_FreeMeshes(M_PRIV *const p) { if (p->meshes != nullptr) { for (int32_t i = 0; i < (int32_t)p->mesh_count; i++) { MeshBatcher_RemoveMesh(p->batcher, p->meshes[i]); if (p->meshes[i] != nullptr) { Output_Mesh_Destroy(p->meshes[i]); } } Memory_FreePointer(&p->meshes); } } void OutputSource_Rooms_Init(MESH_BATCHER *const batcher) { M_PRIV *const p = &m_Priv; p->batcher = batcher; } void OutputSource_Rooms_Shutdown(void) { M_PRIV *const p = &m_Priv; M_FreeMeshes(p); } void OutputSource_Rooms_ObserveLevelLoad(void) { M_PRIV *const p = &m_Priv; M_FreeMeshes(p); M_PrepareMeshes(p); } void OutputSource_Rooms_ObserveLevelUnload(void) { M_PRIV *const p = &m_Priv; M_FreeMeshes(p); } void OutputSource_Rooms_ObserveRoomFlip(const ROOM *const room) { if (room->flip_status == RFS_UNFLIPPED && room->flipped_room != NO_ROOM) { const int16_t room_1 = Room_GetNumber(room); const int16_t room_2 = room->flipped_room; SWAP(m_Priv.meshes[room_1], m_Priv.meshes[room_2]); } } void OutputSource_Rooms_StageRoom(const ROOM *const room) { M_PRIV *const p = &m_Priv; OUTPUT_MESH *const mesh = p->meshes[Room_GetNumber(room)]; const OUTPUT_ROOM_BIND *const bind = Output_Bind_GetRoom(room); const MESH_INSTANCE inst = { .mesh = mesh, .cwmatrix = *g_MatrixPtr, .wmatrix = *g_WMatrixPtr, .tint = Output_GetTint(), .wibble = Output_GetWibbleEffect(), .water_effect = M_GetWaterEffect(room), .enable_scissor = true, .scissor = { .x = bind->bound_left, .y = bind->bound_bottom, .width = bind->bound_right - bind->bound_left, .height = bind->bound_bottom - bind->bound_top, }, .room = Output_GetCurrentRoom(), }; MeshBatcher_Stage(p->batcher, &inst, SCENE_PASS_OPAQUE); MeshBatcher_Stage(p->batcher, &inst, SCENE_PASS_TRANSPARENT); MeshBatcher_Stage(p->batcher, &inst, SCENE_PASS_BLEND_ADD); } ================================================ FILE: src/trx/game/output/sources/rooms.h ================================================ #pragma once #include #include void OutputSource_Rooms_Init(MESH_BATCHER *batcher); void OutputSource_Rooms_Shutdown(void); void OutputSource_Rooms_ObserveLevelLoad(void); void OutputSource_Rooms_ObserveLevelUnload(void); void OutputSource_Rooms_ObserveRoomFlip(const ROOM *room); void OutputSource_Rooms_StageRoom(const ROOM *room); ================================================ FILE: src/trx/game/output/sources/rooms_debug.c ================================================ #include #include #include #include #include #include #include #include #include #include #include typedef struct { XYZW_F pos; RGBA_8888 color; } M_VERTEX; typedef struct { const ROOM *room; MATRIX matrix; } M_INSTANCE; typedef struct { OUTPUT_VERTEX_RANGE triggers; OUTPUT_VERTEX_RANGE portals; } M_ROOM_MESH; typedef struct { SCENE_SOURCE source; OUTPUT_MESH_SHADER *shader; VECTOR *vertices; size_t mesh_count; M_ROOM_MESH *meshes; VECTOR *scheduled; GLuint vao; GLuint vbo; } M_PRIV; static M_PRIV m_Priv = {}; static int32_t M_GetTriggerTriangleIndices( const SECTOR *const sector, int32_t indices[6]) { int32_t skip = -1; SPLIT_TYPE split = sector->floor.split.type; if (!sector->floor.is_split) { split = SPLIT_NONE; } switch (split) { case SPLIT_NONE: case SPLIT_NESW_SOLID: for (int32_t i = 0; i < OUTPUT_QUAD_VERTICES; i++) { indices[i] = OUTPUT_QUAD_TO_FAN(i); } return OUTPUT_QUAD_VERTICES; case SPLIT_NWSE_SOLID: for (int32_t i = 0; i < OUTPUT_QUAD_VERTICES; i++) { indices[i] = OUTPUT_QUAD_TO_FAN_BACK(i); } return OUTPUT_QUAD_VERTICES; case SPLIT_NWSE_PORTAL_SW: skip = 0; break; case SPLIT_NWSE_PORTAL_NE: skip = 2; break; case SPLIT_NESW_PORTAL_SE: skip = 1; break; case SPLIT_NESW_PORTAL_NW: skip = 3; break; default: return 0; } const int32_t clockwise[4] = { 0, 3, 2, 1 }; int32_t count = 0; for (int32_t i = 0; i < 4; i++) { if (clockwise[i] != skip) { indices[count++] = clockwise[i]; } } return count; } static void M_PrepareRoomTriggers( const M_PRIV *const p, M_ROOM_MESH *const mesh, const ROOM *const room) { mesh->triggers.vertex_start = p->vertices->count; const RGBA_8888 color = { .r = 255, .g = 0, .b = 255, .a = 128 }; const XZ_16 offsets[4] = { { 0, 0 }, { 0, 1 }, { 1, 1 }, { 1, 0 } }; int32_t output_indices[OUTPUT_QUAD_VERTICES]; for (int32_t z = 0; z < room->size.z; z++) { for (int32_t x = 0; x < room->size.x; x++) { const SECTOR *sector = Room_GetUnitSector(room, x, z); if (sector->trigger == nullptr) { continue; } const int32_t vertex_count = M_GetTriggerTriangleIndices(sector, output_indices); for (int32_t i = 0; i < vertex_count; i++) { int32_t j = output_indices[i]; XYZ_32 vertex_pos = { .x = (x + offsets[j].x) * WALL_L, .z = (z + offsets[j].z) * WALL_L, }; XYZ_32 world_pos = { .x = room->pos.x + x * WALL_L + offsets[j].x * (WALL_L - 1), .z = room->pos.z + z * WALL_L + offsets[j].z * (WALL_L - 1), .y = room->pos.y, }; const int32_t height = Room_GetFloorHeightForSector( sector, world_pos.x, world_pos.z, true); vertex_pos.y = height + (Output_GetWaterEffect() ? -16 : -2); M_VERTEX vertex = { .pos = { .x = vertex_pos.x, .y = vertex_pos.y, .z = vertex_pos.z, .w = 0.0f, }, .color = color, }; Vector_Add(p->vertices, &vertex); } } } mesh->triggers.vertex_count = p->vertices->count - mesh->triggers.vertex_start; } static void M_PrepareRoomPortals( const M_PRIV *const p, M_ROOM_MESH *const mesh, const ROOM *const room) { mesh->portals.vertex_start = p->vertices->count; const RGBA_8888 color = { 0, 0, 255, 255 }; if (room->portals == nullptr) { mesh->portals.vertex_count = 0; return; } for (int32_t i = 0; i < room->portals->count; i++) { const XYZ_16 *const portal = room->portals->portal[i].vertex; const XYZW_F positions[4] = { { portal[0].x, portal[0].y, portal[0].z, 0.0f }, { portal[1].x, portal[1].y, portal[1].z, 0.0f }, { portal[2].x, portal[2].y, portal[2].z, 0.0f }, { portal[3].x, portal[3].y, portal[3].z, 0.0f }, }; const int32_t indices[8] = { 0, 1, 1, 2, 2, 3, 3, 0 }; for (int32_t j = 0; j < 8; j++) { const M_VERTEX vertex = { .pos = positions[indices[j]], .color = color, }; Vector_Add(p->vertices, &vertex); } } mesh->portals.vertex_count = p->vertices->count - mesh->portals.vertex_start; } static void M_FreeRoom(M_ROOM_MESH *const mesh) { } static void M_PrepareBuffers(M_PRIV *const p) { p->mesh_count = Room_GetCount(); p->meshes = Memory_Alloc(sizeof(M_ROOM_MESH) * p->mesh_count); p->vertices = Vector_Create(sizeof(M_VERTEX)); for (int32_t i = 0; i < Room_GetCount(); i++) { M_PrepareRoomTriggers(p, &p->meshes[i], Room_Get(i)); M_PrepareRoomPortals(p, &p->meshes[i], Room_Get(i)); } glBindBuffer(GL_ARRAY_BUFFER, p->vbo); TRX_GL_TRACK_DATA( glBufferData, GL_ARRAY_BUFFER, p->vertices->count * sizeof(M_VERTEX), Vector_GetData(p->vertices), GL_STATIC_DRAW); } static void M_FreeBuffers(M_PRIV *const p) { if (p->meshes != nullptr) { for (int32_t i = 0; i < (int32_t)p->mesh_count; i++) { M_FreeRoom(&p->meshes[i]); } Memory_FreePointer(&p->meshes); } if (p->vertices != nullptr) { Vector_Free(p->vertices); p->vertices = nullptr; } } static void M_RenderBegin(const SCENE_SOURCE *const source) { M_PRIV *const p = &m_Priv; Vector_Clear(p->scheduled); } static void M_RenderPass( const SCENE_SOURCE *const source, const SCENE_PASS pass) { M_PRIV *const p = &m_Priv; if (pass != SCENE_PASS_TRANSPARENT) { return; } Output_MeshShader_UploadTint(p->shader, COLOR_RGB_F_WHITE); glBindVertexArray(p->vao); glBindBuffer(GL_ARRAY_BUFFER, p->vbo); glVertexAttrib4f(OUTPUT_MESH_ATTR_NORMAL, 0.0f, 0.0f, 0.0f, 0.0f); glVertexAttrib3f(OUTPUT_MESH_ATTR_UVW, 0.0f, 0.0f, 0.0f); glVertexAttrib4f(OUTPUT_MESH_ATTR_TEXTURE_SIZE, 0.0f, 0.0f, 1.0f, 1.0f); glVertexAttrib2f(OUTPUT_MESH_ATTR_TRAPEZOID_RATIO, 1.0f, 1.0f); glVertexAttribI1ui( OUTPUT_MESH_ATTR_FLAGS, VERT_FLAT_SHADED | VERT_NO_LIGHTING | VERT_NO_WIBBLE); glVertexAttrib1f(OUTPUT_MESH_ATTR_SHADE, SHADE_NEUTRAL); for (int32_t i = 0; i < p->scheduled->count; i++) { const M_INSTANCE *const instance = Vector_Get(p->scheduled, i); const M_ROOM_MESH *const mesh = &p->meshes[Room_GetNumber(instance->room)]; Output_MeshShader_UploadModelMatrix(p->shader, &instance->matrix); if (g_Config.debug.enable_debug_triggers) { glDrawArrays( GL_TRIANGLES, mesh->triggers.vertex_start, mesh->triggers.vertex_count); } if (g_Config.debug.enable_debug_portals) { GLint bound_polygon_mode[2]; glDisable(GL_DEPTH_TEST); glGetIntegerv(GL_POLYGON_MODE, &bound_polygon_mode[0]); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); glDrawArrays( GL_LINES, mesh->portals.vertex_start, mesh->portals.vertex_count); glPolygonMode(GL_FRONT_AND_BACK, bound_polygon_mode[0]); glEnable(GL_DEPTH_TEST); } } } static bool M_IsDirty(const SCENE_SOURCE *const source, const SCENE_PASS pass) { const M_PRIV *const p = &m_Priv; return pass == SCENE_PASS_TRANSPARENT && p->scheduled->count > 0; } void OutputSource_RoomsDebug_Init(void) { M_PRIV *const p = &m_Priv; p->shader = Output_GetMeshShader(); p->scheduled = Vector_Create(sizeof(M_INSTANCE)); p->source.render_begin = M_RenderBegin; p->source.render_pass = M_RenderPass; p->source.is_dirty = M_IsDirty; SceneCompositor_AddSource(&p->source); glGenVertexArrays(1, &p->vao); glBindVertexArray(p->vao); glGenBuffers(1, &p->vbo); glBindBuffer(GL_ARRAY_BUFFER, p->vbo); glEnableVertexAttribArray(OUTPUT_MESH_ATTR_POS); glEnableVertexAttribArray(OUTPUT_MESH_ATTR_COLOR); glDisableVertexAttribArray(OUTPUT_MESH_ATTR_NORMAL); glDisableVertexAttribArray(OUTPUT_MESH_ATTR_UVW); glDisableVertexAttribArray(OUTPUT_MESH_ATTR_TEXTURE_SIZE); glDisableVertexAttribArray(OUTPUT_MESH_ATTR_TRAPEZOID_RATIO); glDisableVertexAttribArray(OUTPUT_MESH_ATTR_FLAGS); glDisableVertexAttribArray(OUTPUT_MESH_ATTR_SHADE); glVertexAttribPointer( OUTPUT_MESH_ATTR_POS, 4, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX), (void *)(intptr_t)offsetof(M_VERTEX, pos)); glVertexAttribPointer( OUTPUT_MESH_ATTR_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(M_VERTEX), (void *)(intptr_t)offsetof(M_VERTEX, color)); } void OutputSource_RoomsDebug_Shutdown(void) { M_PRIV *const p = &m_Priv; if (p->scheduled != nullptr) { Vector_Free(p->scheduled); p->scheduled = nullptr; } M_FreeBuffers(p); if (p->vao != 0) { glDeleteVertexArrays(1, &p->vao); p->vao = 0; } if (p->vbo != 0) { glDeleteBuffers(1, &p->vbo); p->vbo = 0; } } void OutputSource_RoomsDebug_ObserveLevelLoad(void) { M_PRIV *const p = &m_Priv; M_PrepareBuffers(p); } void OutputSource_RoomsDebug_ObserveLevelUnload(void) { M_PRIV *const p = &m_Priv; M_FreeBuffers(p); } void OutputSource_RoomsDebug_ObserveRoomFlip(const ROOM *const room) { M_PRIV *const p = &m_Priv; if (room->flip_status == RFS_UNFLIPPED && room->flipped_room != NO_ROOM) { const int16_t room_1 = Room_GetNumber(room); const int16_t room_2 = room->flipped_room; SWAP(p->meshes[room_1], p->meshes[room_2]); } } void OutputSource_RoomsDebug_StageRoom(const ROOM *const room) { if (!g_Config.debug.enable_debug_triggers && !g_Config.debug.enable_debug_portals) { return; } M_PRIV *const p = &m_Priv; Vector_Add( p->scheduled, &(M_INSTANCE) { .room = room, .matrix = *g_WMatrixPtr }); } ================================================ FILE: src/trx/game/output/sources/rooms_debug.h ================================================ #pragma once #include void OutputSource_RoomsDebug_Init(void); void OutputSource_RoomsDebug_Shutdown(void); void OutputSource_RoomsDebug_ObserveLevelLoad(void); void OutputSource_RoomsDebug_ObserveLevelUnload(void); void OutputSource_RoomsDebug_ObserveRoomFlip(const ROOM *room); void OutputSource_RoomsDebug_StageRoom(const ROOM *room); ================================================ FILE: src/trx/game/output/sources/shadows.c ================================================ #include #include #include #include #include typedef struct { MESH_BATCHER *batcher; OUTPUT_MESH *mesh_low; OUTPUT_MESH *mesh_high; } M_PRIV; static M_PRIV m_Priv; static OUTPUT_MESH *M_GenerateShadow( MESH_BUILDER *const builder, const int32_t fidelity) { const int32_t y = -5; const RGBA_8888 color = { 0, 0, 0, 128 }; const OUTPUT_MESH_VERTEX center = { .pos = { 0.0f, (float)y, 0.0f, 0.0f }, .normal = { 0.0f, 0.0f, 0.0f }, .flags = VERT_FLAT_SHADED | VERT_NO_LIGHTING | VERT_NO_WIBBLE, .uvw_idx = -1, .trapezoid_ratio = { 1.0f, 1.0f }, .shade = SHADE_NEUTRAL, .color = color, }; MeshBuilder_AddVertex(builder, ¢er); for (int32_t i = 0; i <= fidelity; i++) { const int16_t angle = ((i * DEG_360) + DEG_180) / fidelity; const int32_t size = WALL_L / 2; const int32_t x = (Math_Sin(angle) * size) >> W2V_SHIFT; const int32_t z = (Math_Cos(angle) * size) >> W2V_SHIFT; OUTPUT_MESH_VERTEX edge = center; edge.pos.x = x; edge.pos.z = z; MeshBuilder_AddVertex(builder, &edge); } MeshBuilder_AddFan(builder, SCENE_PASS_TRANSPARENT, false); return MeshBuilder_Seal(builder); } void OutputSource_Shadows_Init(MESH_BATCHER *const batcher) { M_PRIV *const p = &m_Priv; p->batcher = batcher; // Build low- and high-fidelity circular shadow meshes. MESH_BUILDER *const builder = MeshBuilder_Create(); p->mesh_low = M_GenerateShadow(builder, 8); p->mesh_high = M_GenerateShadow(builder, 32); MeshBuilder_Destroy(builder); MeshBatcher_AddMesh(p->batcher, p->mesh_low); MeshBatcher_AddMesh(p->batcher, p->mesh_high); } void OutputSource_Shadows_Shutdown(void) { M_PRIV *const p = &m_Priv; if (p->mesh_low != nullptr) { if (p->batcher != nullptr) { MeshBatcher_RemoveMesh(p->batcher, p->mesh_low); } Output_Mesh_Destroy(p->mesh_low); p->mesh_low = nullptr; } if (p->mesh_high != nullptr) { if (p->batcher != nullptr) { MeshBatcher_RemoveMesh(p->batcher, p->mesh_high); } Output_Mesh_Destroy(p->mesh_high); p->mesh_high = nullptr; } p->batcher = nullptr; } void OutputSource_Shadows_StageShadow(void) { M_PRIV *const p = &m_Priv; OUTPUT_MESH *const mesh = g_Config.visuals.shadow_type == SHADOW_TYPE_CIRCLE ? p->mesh_high : p->mesh_low; const MESH_INSTANCE inst = { .mesh = mesh, .cwmatrix = *g_MatrixPtr, .wmatrix = *g_WMatrixPtr, .tint = { 1.0f, 1.0f, 1.0f }, .room = Output_GetCurrentRoom(), }; // XXX: Mesh batcher currently collects the transparent faces for the // transparent pass in the opaque pass, so the shadow, even though // transparent, needs to be staged in the opaque pass to work. MeshBatcher_Stage(p->batcher, &inst, SCENE_PASS_OPAQUE); } ================================================ FILE: src/trx/game/output/sources/shadows.h ================================================ #pragma once #include void OutputSource_Shadows_Init(MESH_BATCHER *batcher); void OutputSource_Shadows_Shutdown(void); void OutputSource_Shadows_StageShadow(void); ================================================ FILE: src/trx/game/output/sources/sprites.c ================================================ #include #include #include #include #include #include #include #include #include #include typedef struct { SCENE_SOURCE source; MESH_BATCHER *batcher; OUTPUT_MESH **meshes; OUTPUT_MESH **meshes_blend_add; size_t mesh_count; MATRIX last_matrix; int32_t stack; } M_PRIV; static M_PRIV m_Priv; static void M_RenderBegin(const SCENE_SOURCE *const src) { M_PRIV *const p = &m_Priv; memset(&p->last_matrix, 0, sizeof(MATRIX)); p->stack = 0; } static void M_AddSpriteMesh( MESH_BUILDER *const builder, const int32_t texture_idx, const SCENE_PASS pass, const uint16_t extra_flags) { const SPRITE_TEXTURE *const sprite = Output_GetSpriteTexture(texture_idx); const struct { float x, y; } normal[4] = { { .x = sprite->x0, .y = sprite->y0 }, { .x = sprite->x1, .y = sprite->y0 }, { .x = sprite->x1, .y = sprite->y1 }, { .x = sprite->x0, .y = sprite->y1 }, }; for (int32_t j = 0; j < 4; j++) { const OUTPUT_MESH_VERTEX vertex = { .pos = { .x = 0.0f, .y = 0.0f, .z = 0.0f, .w = 0.0f }, .normal = { .x = normal[j].x, .y = normal[j].y, .z = 0.0f }, .flags = Output_Textures_GetSpriteTextureFlags(texture_idx) | extra_flags, .color = { 255, 255, 255, 255 }, .uvw_idx = Output_Textures_GetSpriteUVWIndex(texture_idx, j), .shade = 0, .trapezoid_ratio = { 1.0f, 1.0f }, }; MeshBuilder_AddVertex(builder, &vertex); } MeshBuilder_AddFan(builder, pass, false); } static void M_PrepareMeshes(M_PRIV *const p) { p->mesh_count = Output_GetSpriteTextureCount(); p->meshes = Memory_Alloc(sizeof(*p->meshes) * p->mesh_count); p->meshes_blend_add = Memory_Alloc(sizeof(*p->meshes_blend_add) * p->mesh_count); MESH_BUILDER *const builder = MeshBuilder_Create(); for (int32_t i = 0; i < (int32_t)p->mesh_count; i++) { M_AddSpriteMesh(builder, i, SCENE_PASS_TRANSPARENT, VERT_USE_OWN_LIGHT); OUTPUT_MESH *const mesh_transparent = MeshBuilder_Seal(builder); MeshBatcher_AddMesh(p->batcher, mesh_transparent); p->meshes[i] = mesh_transparent; M_AddSpriteMesh(builder, i, SCENE_PASS_BLEND_ADD, VERT_USE_OWN_LIGHT); OUTPUT_MESH *const mesh_blend_add = MeshBuilder_Seal(builder); MeshBatcher_AddMesh(p->batcher, mesh_blend_add); p->meshes_blend_add[i] = mesh_blend_add; } MeshBuilder_Destroy(builder); } static void M_FreeMeshes(M_PRIV *const p) { if (p->meshes != nullptr) { for (size_t i = 0; i < p->mesh_count; i++) { MeshBatcher_RemoveMesh(p->batcher, p->meshes[i]); Output_Mesh_Destroy(p->meshes[i]); MeshBatcher_RemoveMesh(p->batcher, p->meshes_blend_add[i]); Output_Mesh_Destroy(p->meshes_blend_add[i]); } Memory_FreePointer(&p->meshes); Memory_FreePointer(&p->meshes_blend_add); } } void OutputSource_Sprites_Init(MESH_BATCHER *batcher) { m_Priv.batcher = batcher; m_Priv.source.render_begin = M_RenderBegin; SceneCompositor_AddSource(&m_Priv.source); } void OutputSource_Sprites_Shutdown(void) { M_FreeMeshes(&m_Priv); } void OutputSource_Sprites_ObserveLevelLoad(void) { M_FreeMeshes(&m_Priv); M_PrepareMeshes(&m_Priv); } void OutputSource_Sprites_ObserveLevelUnload(void) { M_FreeMeshes(&m_Priv); } void OutputSource_Sprites_Stage( const int32_t sprite_idx, const int16_t shade, const RGB_F tint, const DRAW_TYPE draw_type) { M_PRIV *const p = &m_Priv; OUTPUT_MESH *mesh = p->meshes[sprite_idx]; if (memcmp(&p->last_matrix, g_WMatrixPtr, sizeof(MATRIX)) == 0) { p->stack++; } else { p->stack = 0; } p->last_matrix = *g_WMatrixPtr; float tr3_mul = 2.0f - (shade / (float)SHADE_NEUTRAL); CLAMP(tr3_mul, 0.0f, 1.0f); const OUTPUT_LIGHT_INFO light_info = { .ls_adder = shade, .ls_divider = 0, .ls_vector_view = {}, .tr3_ambient = { tr3_mul, tr3_mul, tr3_mul }, .tr3_light_color = {}, .tr3_light_dir_view = {}, }; MESH_INSTANCE inst = { .mesh = mesh, .cwmatrix = *g_MatrixPtr, .wmatrix = *g_WMatrixPtr, .depth_adjust = p->stack * -0.005f, .tint = tint, .wibble = false, .water_effect = 0, .room = Output_GetCurrentRoom(), .light_info = light_info, }; if (draw_type == DRAW_BLEND_ADD || draw_type == DRAW_REFLECTIVE_BLEND_ADD) { inst.mesh = p->meshes_blend_add[sprite_idx]; MeshBatcher_Stage(p->batcher, &inst, SCENE_PASS_BLEND_ADD); } else { MeshBatcher_Stage(p->batcher, &inst, SCENE_PASS_OPAQUE); MeshBatcher_Stage(p->batcher, &inst, SCENE_PASS_TRANSPARENT); } } ================================================ FILE: src/trx/game/output/sources/sprites.h ================================================ #pragma once #include #include #include void OutputSource_Sprites_Init(MESH_BATCHER *batcher); void OutputSource_Sprites_Shutdown(void); void OutputSource_Sprites_ObserveLevelLoad(void); void OutputSource_Sprites_ObserveLevelUnload(void); void OutputSource_Sprites_Stage( int32_t sprite_idx, int16_t shade, RGB_F tint, DRAW_TYPE draw_type); ================================================ FILE: src/trx/game/output/sources/ui.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include // GL attribute mapping in the shader typedef enum { // clang-format off M_ATTR_POS = 0, M_ATTR_UVW = 1, M_ATTR_TEXTURE_SIZE = 2, M_ATTR_FLAGS = 3, M_ATTR_COLOR = 4, // clang-format on } M_VERTEX_ATTR; typedef struct { XYZW_F pos; OUTPUT_UVW uvw; OUTPUT_TEXTURE_SIZE texture_size; OUTPUT_USHORT flags; RGBA_F color; } M_VERTEX; typedef struct { SCENE_SOURCE source; const SCENE_SOURCE *objects_source; VECTOR *scheduled_pickups; VECTOR *vertices; GLuint vao; GLuint vbo; } M_PRIV; static M_PRIV m_Priv = {}; static RGBA_F M_ToRGBA_F(const RGBA_8888 color) { return (RGBA_F) { .r = color.r / 255.0f, .g = color.g / 255.0f, .b = color.b / 255.0f, .a = color.a / 255.0f, }; } VIEWPORT_RECT OutputSource_UI_GetPickupRect( const OUTPUT_UI_PICKUP *const pickup) { const VIEWPORT_RECT viewport = Viewport_GetRect(VIEWPORT_UI); const float pickup_h = viewport.h * g_Config.ui.pickup_scale / 6; const float pickup_w = pickup_h * 5 / 4; const float window_padding_y = viewport.h / 16; const float window_padding_x = window_padding_y * 4 / 3; const float grid_padding_x = pickup_w / 8; const float grid_padding_y = pickup_h / 8; const float src_x = viewport.w + window_padding_x + pickup_w; const float src_y = viewport.h - window_padding_y - pickup_h / 2; const float dst_x = viewport.w - window_padding_x - pickup_w / 2 - (pickup_w + grid_padding_x) * pickup->grid_x; const float dst_y = viewport.h - window_padding_y - pickup_h / 2 - (pickup_h + grid_padding_y) * pickup->grid_y; const float x = src_x + (dst_x - src_x) * pickup->ease; const float y = src_y + (dst_y - src_y) * pickup->ease; return (VIEWPORT_RECT) { .x = x - pickup_w / 2, .y = y - pickup_h / 2, .w = pickup_w, .h = pickup_h, }; } static float M_Get3DPickupScale( const VIEWPORT_RECT pickup_rect, const ANIM_FRAME *const frame) { const XYZ_F obj_size = { .x = MAX(1, frame->bounds.max.x - frame->bounds.min.x), .y = MAX(1, frame->bounds.max.y - frame->bounds.min.y), .z = MAX(1, frame->bounds.max.z - frame->bounds.min.z), }; // Reference scale that seems to works OK based on the following data: // pickup_rect: 480×360 (changes with window resizes) // key: 81 182 11 // scion: 184 190 54 // pistols: 215 57 146 // shotgun: 365 123 147 const float ref_scale = pickup_rect.w / 200.0f; // A scale factor to fit the mesh within pickup_rect, // ensuring it touches either side and is entirely contained. // clang-format off const float perfect_fit_scale = MIN3( pickup_rect.w / obj_size.x, pickup_rect.h / obj_size.y, pickup_rect.w / obj_size.z); // clang-format on // Some items are too big or too small – try to find a middle ground. return (ref_scale + perfect_fit_scale) / 2.0f; } static XYZ_32 M_VectorViewFromWorld( const MATRIX *const view_matrix, const XYZ_32 vec_world) { return (XYZ_32) { .x = (view_matrix->_00 * vec_world.x + view_matrix->_01 * vec_world.y + view_matrix->_02 * vec_world.z) >> W2V_SHIFT, .y = (view_matrix->_10 * vec_world.x + view_matrix->_11 * vec_world.y + view_matrix->_12 * vec_world.z) >> W2V_SHIFT, .z = (view_matrix->_20 * vec_world.x + view_matrix->_21 * vec_world.y + view_matrix->_22 * vec_world.z) >> W2V_SHIFT, }; } static void M_Draw3DPickups(const M_PRIV *const p) { SceneCompositor_SetSamplerFilter(g_Config.rendering.texture_filter); Output_MeshShader_Bind(Output_GetMeshShader()); for (int32_t i = 0; i < p->scheduled_pickups->count; i++) { if (p->objects_source->render_begin != nullptr) { p->objects_source->render_begin(p->objects_source); } const OUTPUT_UI_PICKUP *const pickup = Vector_Get(p->scheduled_pickups, i); const ANIM_FRAME *const frame = Object_GetAnim(pickup->object, 0)->frame_ptr; const VIEWPORT_RECT pickup_rect = OutputSource_UI_GetPickupRect(pickup); const XYZ_32 origin = { .x = pickup_rect.x + pickup_rect.w / 2, .y = pickup_rect.y + pickup_rect.h / 2, .z = (Output_GetNearZ_UI() + Output_GetFarZ_UI()) / 2, }; const float scale = M_Get3DPickupScale(pickup_rect, frame); // Lighting routines needs a W2V matrix to work; set up something for // it. MATRIX pickup_view_matrix = {}; XYZ_32 camera = g_TRVersion >= 3 ? (XYZ_32) { .x = origin.x, .y = origin.y - WALL_L, .z = origin.z, } : (XYZ_32) { .x = origin.x, .y = origin.y, .z = origin.z - WALL_L, }; Matrix_LookAt( camera.x, camera.y, camera.z, origin.x, origin.y, origin.z, 0); pickup_view_matrix = g_ViewMatrix; Matrix_PushUnit(); Matrix_TranslateSet32(origin); Matrix_RotX(DEG_1 * 15); Matrix_RotY(-DEG_180); Matrix_RotY(pickup->rot_y); Matrix_Scale((1 << W2V_SHIFT) * scale); // Set up lighting for the pickup mesh. if (g_TRVersion >= 3) { // Port of OG TR3's SetPickupLight(). // ambient = (64, 64, 64) // sun = (3072, 1680, 640) // spot = (1024, 1024, 1024) // dynamic = (640, 2432, 4080) const float ambient_u8 = 64.0f / 255.0f; const RGB_F ambient = { ambient_u8, ambient_u8, ambient_u8 }; const RGB_F colors[3] = { { .r = 3072.0f / 4096.0f, .g = 1680.0f / 4096.0f, .b = 640.0f / 4096.0f, }, { .r = 1024.0f / 4096.0f, .g = 1024.0f / 4096.0f, .b = 1024.0f / 4096.0f, }, { .r = 640.0f / 4096.0f, .g = 2432.0f / 4096.0f, .b = 4080.0f / 4096.0f, }, }; const XYZ_32 dirs_view[3] = { M_VectorViewFromWorld( &pickup_view_matrix, (XYZ_32) { .x = 0x2000, .y = -0x2000, .z = 0x1800 }), M_VectorViewFromWorld( &pickup_view_matrix, (XYZ_32) { .x = -0x2000, .y = -0x4000, .z = 0x3000 }), M_VectorViewFromWorld( &pickup_view_matrix, (XYZ_32) { .x = 0, .y = 0x2000, .z = 0x3000 }), }; Output_SetTR3Light(ambient, colors, dirs_view); } else { Output_SetLightDivider((1 << W2V_SHIFT) * 2); Output_SetLightAdder(SHADE_LOW); Output_RotateLight(DEG_1 * -30, DEG_1 * 45); } Matrix_TranslateRel16(frame->offset); Matrix_TranslateRel32((XYZ_32) { .x = -(frame->bounds.min.x + frame->bounds.max.x) / 2, .y = -(frame->bounds.min.y + frame->bounds.max.y) / 2, .z = -(frame->bounds.min.z + frame->bounds.max.z) / 2, }); Matrix_Rot16(frame->mesh_rots[0]); Object_DrawStaticObject(pickup->object, frame); Matrix_Pop(); // Immediately flush scheduled object, so that it gets rendered // in the target viewport p->objects_source->render_pass(p->objects_source, SCENE_PASS_OPAQUE); p->objects_source->render_pass( p->objects_source, SCENE_PASS_TRANSPARENT); glBlendFunc(GL_ONE, GL_ONE); p->objects_source->render_pass(p->objects_source, SCENE_PASS_BLEND_ADD); glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); if (p->objects_source->render_end != nullptr) { p->objects_source->render_end(p->objects_source); } } SceneCompositor_SetSamplerFilter(g_Config.rendering.ui_filter); } static void M_DrawVertices(const M_PRIV *const p) { glBindVertexArray(p->vao); glBindBuffer(GL_ARRAY_BUFFER, p->vbo); TRX_GL_TRACK_DATA( glBufferData, GL_ARRAY_BUFFER, p->vertices->count * sizeof(M_VERTEX), Vector_GetData(p->vertices), GL_STATIC_DRAW); glDrawArrays(GL_TRIANGLES, 0, p->vertices->count); } static void M_RenderBegin(const SCENE_SOURCE *const source) { M_PRIV *const p = &m_Priv; Vector_Clear(p->scheduled_pickups); Vector_Clear(p->vertices); } static void M_RenderPass( const SCENE_SOURCE *const source, const SCENE_PASS pass) { M_PRIV *const p = &m_Priv; if (pass != SCENE_PASS_UI) { return; } if (p->scheduled_pickups->count == 0 && p->vertices->count == 0) { return; } glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); if (p->vertices->count > 0) { M_DrawVertices(p); } if (p->scheduled_pickups->count > 0) { glEnable(GL_CULL_FACE); M_Draw3DPickups(p); glDisable(GL_CULL_FACE); Output_UIShader_Bind(Output_GetUIShader()); } } static bool M_IsDirty(const SCENE_SOURCE *const source, const SCENE_PASS pass) { const M_PRIV *const p = &m_Priv; return pass == SCENE_PASS_UI && (p->scheduled_pickups->count > 0 || p->vertices->count > 0); } void OutputSource_UI_Init(void) { M_PRIV *const p = &m_Priv; p->scheduled_pickups = Vector_Create(sizeof(OUTPUT_UI_PICKUP)); p->vertices = Vector_CreateAtCapacity(sizeof(M_VERTEX), 500); p->source.render_begin = M_RenderBegin; p->source.render_pass = M_RenderPass; p->source.is_dirty = M_IsDirty; p->objects_source = OutputSource_Objects_GetSource(); SceneCompositor_AddSource(&p->source); glGenVertexArrays(1, &p->vao); glBindVertexArray(p->vao); glGenBuffers(1, &p->vbo); glBindBuffer(GL_ARRAY_BUFFER, p->vbo); glEnableVertexAttribArray(M_ATTR_POS); glEnableVertexAttribArray(M_ATTR_UVW); glEnableVertexAttribArray(M_ATTR_COLOR); glEnableVertexAttribArray(M_ATTR_TEXTURE_SIZE); glEnableVertexAttribArray(M_ATTR_FLAGS); glVertexAttribPointer( M_ATTR_POS, 4, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX), (void *)(intptr_t)offsetof(M_VERTEX, pos)); glVertexAttribPointer( M_ATTR_UVW, 3, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX), (void *)(intptr_t)offsetof(M_VERTEX, uvw)); glVertexAttribPointer( M_ATTR_COLOR, 4, GL_FLOAT, GL_TRUE, sizeof(M_VERTEX), (void *)(intptr_t)offsetof(M_VERTEX, color)); glVertexAttribPointer( M_ATTR_TEXTURE_SIZE, 4, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX), (void *)(intptr_t)offsetof(M_VERTEX, texture_size)); glVertexAttribIPointer( M_ATTR_FLAGS, 1, OUTPUT_USHORT_GL, sizeof(M_VERTEX), (void *)(intptr_t)offsetof(M_VERTEX, flags)); } void OutputSource_UI_Shutdown(void) { M_PRIV *const p = &m_Priv; if (p->scheduled_pickups != nullptr) { Vector_Free(p->scheduled_pickups); p->scheduled_pickups = nullptr; } if (p->vertices != nullptr) { Vector_Free(p->vertices); p->vertices = nullptr; } if (p->vao != 0) { glDeleteVertexArrays(1, &p->vao); p->vao = 0; } if (p->vbo != 0) { glDeleteBuffers(1, &p->vbo); p->vbo = 0; } } void OutputSource_UI_StagePickup(const OUTPUT_UI_PICKUP pickup) { M_PRIV *const p = &m_Priv; Vector_Add(p->scheduled_pickups, &pickup); } void OutputSource_UI_StageSprite(const OUTPUT_UI_SPRITE sprite) { M_PRIV *const p = &m_Priv; const SPRITE_TEXTURE *const sprite_tex = Output_GetSpriteTexture(sprite.sprite_idx); const float u0 = (sprite_tex->offset & 0xFF) / 256.0f; const float v0 = (sprite_tex->offset >> 8) / 256.0f; const float u1 = u0 + sprite_tex->width / 65536.0f; const float v1 = v0 + sprite_tex->height / 65536.0f; M_VERTEX vertices[4]; for (int32_t i = 0; i < 4; i++) { vertices[i].pos.z = sprite.z; vertices[i].pos.w = 0.0f; vertices[i].color = sprite.color[i]; vertices[i].uvw.w = sprite_tex->tex_page; vertices[i].texture_size.x0 = u0; vertices[i].texture_size.y0 = v0; vertices[i].texture_size.x1 = u1; vertices[i].texture_size.y1 = v1; vertices[i].flags = 0; } #define L_SET(vtx_idx, x_, y_, u_, v_) \ vertices[vtx_idx].pos.x = x_; \ vertices[vtx_idx].pos.y = y_; \ vertices[vtx_idx].uvw.u = u_; \ vertices[vtx_idx].uvw.v = v_; L_SET(0, sprite.x0, sprite.y0, u0, v0); L_SET(1, sprite.x1, sprite.y0, u1, v0); L_SET(2, sprite.x1, sprite.y1, u1, v1); L_SET(3, sprite.x0, sprite.y1, u0, v1); #undef L_SET for (int32_t i = 0; i < OUTPUT_QUAD_VERTICES; i++) { const int32_t j = OUTPUT_QUAD_TO_FAN(i); Vector_Add(p->vertices, &vertices[j]); } } void OutputSource_UI_StageQuad(const OUTPUT_UI_QUAD quad) { M_PRIV *const p = &m_Priv; M_VERTEX vertices[4]; for (int32_t i = 0; i < 4; i++) { vertices[i].pos.z = quad.z; vertices[i].pos.w = 0.0f; vertices[i].flags = VERT_FLAT_SHADED; } #define L_SET(vtx_idx, x_, y_, color_) \ vertices[vtx_idx].pos.x = x_; \ vertices[vtx_idx].pos.y = y_; \ vertices[vtx_idx].color = M_ToRGBA_F(color_); L_SET(0, quad.x0, quad.y0, quad.tl); L_SET(1, quad.x1, quad.y0, quad.tr); L_SET(2, quad.x1, quad.y1, quad.br); L_SET(3, quad.x0, quad.y1, quad.bl); #undef L_SET for (int32_t i = 0; i < OUTPUT_QUAD_VERTICES; i++) { const int32_t j = OUTPUT_QUAD_TO_FAN(i); Vector_Add(p->vertices, &vertices[j]); } } void OutputSource_UI_StagePhotoModeFrame( const VIEWPORT_RECT rect, const RGBA_8888 color, const int32_t thickness) { const int32_t t = thickness; if (t <= 0) { return; } const int32_t x0 = rect.x; const int32_t y0 = rect.y; const int32_t x1 = rect.x + rect.w; const int32_t y1 = rect.y + rect.h; const int32_t z = Output_GetNearZ_UI(); OutputSource_UI_StageQuad((OUTPUT_UI_QUAD) { .x0 = x0, .y0 = y0, .x1 = x1, .y1 = y0 + t, .z = z, .tl = color, .tr = color, .bl = color, .br = color, }); OutputSource_UI_StageQuad((OUTPUT_UI_QUAD) { .x0 = x0, .y0 = y1 - t, .x1 = x1, .y1 = y1, .z = z, .tl = color, .tr = color, .bl = color, .br = color, }); OutputSource_UI_StageQuad((OUTPUT_UI_QUAD) { .x0 = x0, .y0 = y0 + t, .x1 = x0 + t, .y1 = y1 - t, .z = z, .tl = color, .tr = color, .bl = color, .br = color, }); OutputSource_UI_StageQuad((OUTPUT_UI_QUAD) { .x0 = x1 - t, .y0 = y0 + t, .x1 = x1, .y1 = y1 - t, .z = z, .tl = color, .tr = color, .bl = color, .br = color, }); } ================================================ FILE: src/trx/game/output/sources/ui.h ================================================ #pragma once #include #include #include #include #define OUTPUT_UI_MAX_PICKUP_ROWS 3 #define OUTPUT_UI_MAX_PICKUP_COLUMNS 4 #define OUTPUT_UI_MAX_PICKUPS \ (OUTPUT_UI_MAX_PICKUP_COLUMNS * OUTPUT_UI_MAX_PICKUP_ROWS) typedef struct { const OBJECT *object; int32_t grid_x; int32_t grid_y; int32_t rot_y; float ease; } OUTPUT_UI_PICKUP; typedef struct { int32_t sprite_idx; int32_t x0, y0; int32_t x1, y1; int32_t z; int16_t shade; RGBA_F color[4]; } OUTPUT_UI_SPRITE; typedef struct { int32_t x0, y0; int32_t x1, y1; int32_t z; RGBA_8888 tl, tr, bl, br; } OUTPUT_UI_QUAD; void OutputSource_UI_Init(void); void OutputSource_UI_Shutdown(void); void OutputSource_UI_StagePickup(OUTPUT_UI_PICKUP pickup); void OutputSource_UI_StageSprite(OUTPUT_UI_SPRITE sprite); void OutputSource_UI_StageQuad(OUTPUT_UI_QUAD quad); void OutputSource_UI_StagePhotoModeFrame( VIEWPORT_RECT rect, RGBA_8888 color, int32_t thickness); VIEWPORT_RECT OutputSource_UI_GetPickupRect(const OUTPUT_UI_PICKUP *pickup); ================================================ FILE: src/trx/game/output/state.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static float m_Time = 0.0f; static float m_TimeInGame = 0.0f; static bool m_ControlFrame = false; static int32_t m_AnimatedTexturesOffset = 0; static int32_t m_FogStart = 0; static int32_t m_FogEnd = 0; static RGBA_F m_FogColor = {}; static RGB_F m_WaterColor = {}; static float m_DepthFactor = 0.0f; static float m_DepthUnits = 0.0f; static const ROOM *m_CurrentRoom = nullptr; static int32_t m_LsAdder = 0; static int32_t m_LsDivider = 0; static XYZ_32 m_LsVectorView = {}; static RGB_F m_TR3Ambient = { 1.0f, 1.0f, 1.0f }; static RGB_F m_TR3LightColor[3] = {}; static XYZ_32 m_TR3LightDirView[3] = {}; static bool m_IsWibbleEffect = false; static bool m_IsWaterEffect = false; static bool m_IsShadeEffect = false; static bool m_IsSkyboxEnabled = false; static float m_Desaturation = 0.0f; static int32_t m_TintOverrideDepth = 0; static RGB_F m_TintOverrideStack[8] = {}; static RGB_F m_GlobalTint = { 1.0f, 1.0f, 1.0f }; float Output_GetTime(void) { return m_Time; } float Output_GetTimeInGame(void) { return m_TimeInGame; } void Output_SetTimeInGame(const float time) { m_TimeInGame = time; } int32_t Output_GetNearZ(void) { return 20 << W2V_SHIFT; } int32_t Output_GetFarZ(void) { return Output_GetFogEnd() << W2V_SHIFT; } int32_t Output_GetNearZ_UI(void) { return 20; } int32_t Output_GetFarZ_UI(void) { return 10000; } void Output_SetSkyboxEnabled(const bool enabled) { m_IsSkyboxEnabled = enabled; } bool Output_IsSkyboxEnabled(void) { return m_IsSkyboxEnabled && g_Config.visuals.enable_skybox; } RGBA_F Output_GetFogColor(void) { return m_FogColor; } int32_t Output_GetFogStart(void) { return MIN(m_FogStart, Output_GetFogEnd()); } int32_t Output_GetFogEnd(void) { return m_FogEnd; } void Output_SetFogStart(const int32_t dist) { m_FogStart = dist; const OUTPUT_UNIFORMS *const uniforms = Output_GetUniforms(); if (uniforms != nullptr) { Output_Uniforms_UploadFogDistance( uniforms, Output_GetFogStart(), Output_GetFogEnd()); } } void Output_SetFogEnd(const int32_t dist) { m_FogEnd = dist; const OUTPUT_UNIFORMS *const uniforms = Output_GetUniforms(); if (uniforms != nullptr) { Output_Uniforms_UploadFogDistance( uniforms, Output_GetFogStart(), Output_GetFogEnd()); } } void Output_SetupBelowWater(const bool underwater) { m_IsWaterEffect = true; m_IsWibbleEffect = !underwater; m_IsShadeEffect = true; } void Output_SetupAboveWater(const bool underwater) { m_IsWaterEffect = false; m_IsWibbleEffect = underwater; m_IsShadeEffect = underwater; } bool Output_GetWaterEffect(void) { return m_IsWaterEffect; } bool Output_GetWibbleEffect(void) { return m_IsWibbleEffect; } float Output_GetDesaturation(void) { return m_Desaturation; } void Output_SetDesaturation(const float desaturation) { if (m_Desaturation == desaturation) { return; } m_Desaturation = desaturation; const OUTPUT_UNIFORMS *const uniforms = Output_GetUniforms(); if (uniforms != nullptr) { Output_Uniforms_UploadDesaturation(uniforms, m_Desaturation); } } void Output_SetFogColor(const RGBA_8888 color) { m_FogColor.r = color.r / 255.0f; m_FogColor.g = color.g / 255.0f; m_FogColor.b = color.b / 255.0f; m_FogColor.a = color.a / 255.0f; } RGB_F Output_GetWaterColor(void) { return m_WaterColor; } void Output_SetWaterColor(const RGB_888 color) { m_WaterColor.r = color.r / 255.0f; m_WaterColor.g = color.g / 255.0f; m_WaterColor.b = color.b / 255.0f; } RGB_F Output_GetGlobalTint(void) { return m_GlobalTint; } void Output_SetGlobalTint(const RGB_F tint) { m_GlobalTint = tint; const OUTPUT_UNIFORMS *const uniforms = Output_GetUniforms(); if (uniforms != nullptr) { Output_Uniforms_UploadGlobalTint(uniforms, m_GlobalTint); } } RGB_F Output_GetTint(void) { if (m_TintOverrideDepth != 0) { return m_TintOverrideStack[m_TintOverrideDepth - 1]; } if (m_IsShadeEffect) { return m_WaterColor; } return COLOR_RGB_F_WHITE; } void Output_PushTintOverride(const RGB_F tint) { ASSERT(m_TintOverrideDepth < (int32_t)ARRAY_SIZE(m_TintOverrideStack)); m_TintOverrideStack[m_TintOverrideDepth++] = tint; } void Output_PopTintOverride(void) { ASSERT(m_TintOverrideDepth > 0); m_TintOverrideDepth--; } void Output_GetPerspProjectionMatrix(GLfloat output[][4]) { const float left = Viewport_GetMinX(VIEWPORT_GAME); const float top = Viewport_GetMinY(VIEWPORT_GAME); const float right = Viewport_GetWidth(VIEWPORT_GAME); const float bottom = Viewport_GetHeight(VIEWPORT_GAME); const float near = Output_GetNearZ() / (float)(1 << W2V_SHIFT); const float far = Output_GetFarZ() / (float)(1 << W2V_SHIFT); const float aspect = (float)(right - left) / (float)(bottom - top); const float fov = Viewport_GetEffectiveFOV() * M_PI / (float)DEG_180; float f_x, f_y; switch (Viewport_GetFOVMode()) { case FOV_MODE_HORIZONTAL: f_x = 1.0f / tanf(fov * 0.5f); f_y = f_x * aspect; break; case FOV_MODE_VERTICAL: f_y = 1.0f / tanf(fov * 0.5f); f_x = f_y / aspect; break; case FOV_MODE_PC: { const float persp = ((4.0f / 3.0f) / aspect); f_x = persp / tanf(fov * 0.5f); f_y = f_x * aspect; break; } case FOV_MODE_PS1: { const float persp = ((4.0f / 3.0f) / aspect) * (240.0f / 200.0f); f_x = persp / tanf(fov * 0.5f); f_y = f_x * aspect; break; } default: ASSERT_FAIL(); } const float near_z = Output_GetNearZ(); const float far_z = Output_GetFarZ(); const float res_z = 0.99 * near_z * far_z / (far_z - near_z); output[0][0] = f_x; output[0][1] = 0.0f; output[0][2] = 0.0f; output[0][3] = 0.0f; output[1][0] = 0.0f; output[1][1] = -f_y; output[1][2] = 0.0f; output[1][3] = 0.0f; output[2][0] = 0.0f; output[2][1] = 0.0f; output[2][2] = 0.005 + res_z / near_z; output[2][3] = 1.0f; output[3][0] = 0.0f; output[3][1] = 0.0f; output[3][2] = -res_z / (float)(1 << W2V_SHIFT); output[3][3] = 0.0f; } void Output_GetOrthoProjectionMatrix(GLfloat output[][4]) { const float left = 0.0f; const float top = 0.0f; const float right = Viewport_GetWidth(VIEWPORT_UI); const float bottom = Viewport_GetHeight(VIEWPORT_UI); const float near = Output_GetNearZ_UI(); const float far = Output_GetFarZ_UI(); output[0][0] = 2.0f / (right - left); output[0][1] = 0.0f; output[0][2] = 0.0f; output[0][3] = 0.0f; output[1][0] = 0.0f; output[1][1] = 2.0f / (top - bottom); output[1][2] = 0.0f; output[1][3] = 0.0f; output[2][0] = 0.0f; output[2][1] = 0.0f; output[2][2] = 2.0f / (far - near); output[2][3] = 0.0f; output[3][0] = -(right + left) / (right - left); output[3][1] = -(top + bottom) / (top - bottom); output[3][2] = -(far + near) / (far - near); output[3][3] = 1.0f; } void Output_SetCurrentRoom(const ROOM *const room) { m_CurrentRoom = room; } const ROOM *Output_GetCurrentRoom(void) { return m_CurrentRoom; } int32_t Output_GetLightAdder(void) { return m_LsAdder; } void Output_SetLightAdder(const int32_t adder) { m_LsAdder = adder; } int32_t Output_GetLightDivider(void) { return m_LsDivider; } void Output_SetLightDivider(const int32_t divider) { m_LsDivider = divider; } XYZ_32 Output_GetLightVectorView(void) { return m_LsVectorView; } OUTPUT_LIGHT_INFO Output_GetLightInfo(void) { OUTPUT_LIGHT_INFO info = { .ls_adder = m_LsAdder, .ls_divider = m_LsDivider, .ls_vector_view = m_LsVectorView, .tr3_ambient = m_TR3Ambient, }; for (int32_t i = 0; i < 3; i++) { info.tr3_light_color[i] = m_TR3LightColor[i]; info.tr3_light_dir_view[i] = m_TR3LightDirView[i]; } return info; } void Output_RotateLight(const int16_t pitch, const int16_t yaw) { const int32_t cp = Math_Cos(pitch); const int32_t sp = Math_Sin(pitch); const int32_t cy = Math_Cos(yaw); const int32_t sy = Math_Sin(yaw); const int32_t x = TRIGMULT2(cp, sy); const int32_t y = -sp; const int32_t z = TRIGMULT2(cp, cy); const MATRIX *const m = &g_ViewMatrix; m_LsVectorView.x = (m->_00 * x + m->_01 * y + m->_02 * z) >> W2V_SHIFT; m_LsVectorView.y = (m->_10 * x + m->_11 * y + m->_12 * z) >> W2V_SHIFT; m_LsVectorView.z = (m->_20 * x + m->_21 * y + m->_22 * z) >> W2V_SHIFT; } void Output_SetTR3Light( const RGB_F ambient, const RGB_F colors[3], const XYZ_32 dirs_view[3]) { m_TR3Ambient = ambient; for (int32_t i = 0; i < 3; i++) { m_TR3LightColor[i] = colors[i]; m_TR3LightDirView[i] = dirs_view[i]; } } void Output_SetTime(const float time) { m_Time = time; } void Output_AnimateTextures(int32_t num_frames) { const int32_t anim_delta = g_TRVersion == 3 ? 2 : 1; m_TimeInGame += num_frames; m_AnimatedTexturesOffset += num_frames * anim_delta; bool update = false; while (m_AnimatedTexturesOffset > 5) { Output_CycleAnimatedTextures(); update = true; m_AnimatedTexturesOffset -= 5; } if (update) { Output_Textures_CycleAnimations(); SceneCompositor_AnimateTextures(); } Output_AnimateLights(num_frames); } void Output_EnableScissor( const float x, const float y, const float w, const float h) { // Causes the rendering pipeline to discard every pixel outside of the // specified window. The window is in game framebuffer viewport's // coordinates; to make it work properly, we need to translate it to the // SDL window coordinates first. // To deal with precision issues coming from using integer matrix ops const int32_t border = 4; const VIEWPORT_RECT game = Viewport_GetRect(VIEWPORT_GAME); const VIEWPORT_RECT window = Viewport_GetRect(VIEWPORT_GAME); const float scale_x = window.w / (float)game.w; const float scale_y = window.h / (float)game.h; VIEWPORT_RECT scissor = { .x = window.x + (x * scale_x) - border, .y = window.y + (game.h - y) * scale_y - border, .w = w * scale_x + border * 2, .h = h * scale_y + border * 2, }; glEnable(GL_SCISSOR_TEST); glScissor(scissor.x, scissor.y, scissor.w, scissor.h); } void Output_DisableScissor(void) { glDisable(GL_SCISSOR_TEST); } void Output_AdjustDepth(const float factor, const float units) { if (factor != m_DepthFactor || units != m_DepthUnits) { glPolygonOffset(factor, units); m_DepthFactor = factor; m_DepthUnits = units; } } bool Output_IsControlFrame(void) { return m_ControlFrame; } void Output_SetControlFrame(const bool is_control_frame) { m_ControlFrame = is_control_frame; } ================================================ FILE: src/trx/game/output/state.h ================================================ #pragma once #include #include #include #include #include void Output_SetSkyboxEnabled(bool enabled); bool Output_IsSkyboxEnabled(void); void Output_GetPerspProjectionMatrix(GLfloat output[][4]); void Output_GetOrthoProjectionMatrix(GLfloat output[][4]); void Output_SetTime(float time); float Output_GetTime(void); float Output_GetTimeInGame(void); void Output_SetTimeInGame(float time); bool Output_IsControlFrame(void); void Output_SetControlFrame(bool is_control_frame); void Output_SetupBelowWater(bool is_underwater); void Output_SetupAboveWater(bool is_underwater); RGB_F Output_GetWaterColor(void); void Output_SetWaterColor(RGB_888 color); RGB_F Output_GetTint(void); void Output_PushTintOverride(RGB_F tint); void Output_PopTintOverride(void); bool Output_GetWaterEffect(void); bool Output_GetWibbleEffect(void); float Output_GetDesaturation(void); void Output_SetDesaturation(float desaturation); RGB_F Output_GetGlobalTint(void); void Output_SetGlobalTint(RGB_F tint); RGBA_F Output_GetFogColor(void); int32_t Output_GetFogStart(void); int32_t Output_GetFogEnd(void); void Output_SetFogColor(RGBA_8888 color); void Output_SetFogStart(int32_t dist); void Output_SetFogEnd(int32_t dist); int32_t Output_GetNearZ(void); int32_t Output_GetFarZ(void); int32_t Output_GetNearZ_UI(void); int32_t Output_GetFarZ_UI(void); void Output_SetCurrentRoom(const ROOM *room_num); const ROOM *Output_GetCurrentRoom(void); int32_t Output_GetLightAdder(void); int32_t Output_GetLightDivider(void); XYZ_32 Output_GetLightVectorView(void); OUTPUT_LIGHT_INFO Output_GetLightInfo(void); void Output_SetLightAdder(int32_t adder); void Output_SetLightDivider(int32_t divider); void Output_RotateLight(int16_t pitch, int16_t yaw); void Output_SetTR3Light( RGB_F ambient, const RGB_F colors[3], const XYZ_32 dirs_view[3]); void Output_EnableScissor(float x, float y, float w, float h); void Output_DisableScissor(void); void Output_AdjustDepth(float factor, float units); void Output_SetupBelowWater(bool underwater); void Output_SetupAboveWater(bool underwater); void Output_AnimateTextures(int32_t num_frames); ================================================ FILE: src/trx/game/output/textures.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef struct { OUTPUT_UVW corners[4]; } M_UVW_PACK; static struct { VECTOR *objects; VECTOR *sprites; } m_AnimationRanges; static struct { GLuint tex_atlas; GLuint tex_env_map; struct { int32_t count; int32_t count_objects; int32_t count_sprites; M_UVW_PACK *data; M_UVW_PACK *data_objects; M_UVW_PACK *data_sprites; bool *animated; bool *animated_objects; bool *animated_sprites; bool *has_transparency_objects; uint16_t *flags; uint16_t *flags_objects; uint16_t *flags_sprites; } uvws; struct { OUTPUT_TEXTURE_SIZE *data; OUTPUT_TEXTURE_SIZE *data_objects; OUTPUT_TEXTURE_SIZE *data_sprites; } atlas_sizes; } m_Priv = {}; static int32_t m_TexturePageCount = 0; static uint8_t *m_TexturePages8 = nullptr; static RGBA_8888 *m_TexturePages32 = nullptr; static SDL_mutex **m_TexturePageLocks = nullptr; static int32_t m_PaletteSize = 0; static RGB_888 *m_Palette8 = nullptr; static RGB_888 *m_Palette16 = nullptr; static LIGHT_MAP m_LightMap[32]; static SHADE_MAP m_ShadeMap[256]; static int32_t m_ObjectTextureCount = 0; static int32_t m_SpriteTextureCount = 0; static OBJECT_TEXTURE *m_ObjectTextures = nullptr; static SPRITE_TEXTURE *m_SpriteTextures = nullptr; static ANIMATED_TEXTURE_RANGE *m_AnimTextureRanges = nullptr; #define M_TRANSPARENCY_CACHE_VERSION 1 static uint64_t M_ComputeTransparencyChecksum(void) { const GF_LEVEL *const level = GF_GetCurrentLevel(); if (level == nullptr) { return 0; } uint64_t hash = LevelCache_InitChecksum( "object_transparency_cache", M_TRANSPARENCY_CACHE_VERSION); hash = LevelCache_UpdateLevelChecksum(hash, level); hash = Hash_FNV1a64_UpdateU32(hash, Output_GetObjectTextureCount()); return hash; } static const char *M_GetTransparencyCacheFilename(void) { const GF_LEVEL *const level = GF_GetCurrentLevel(); const char *const level_key = LevelCache_GetLevelKey(level); if (level_key == nullptr) { return nullptr; } return String_FormatStatic("object_transparency_%s.cache.dat", level_key); } static bool M_TryLoadTransparencyCache(void) { const int32_t texture_count = Output_GetObjectTextureCount(); const uint64_t expected_checksum = M_ComputeTransparencyChecksum(); const char *const cache_filename = M_GetTransparencyCacheFilename(); if (cache_filename == nullptr || expected_checksum == 0 || texture_count <= 0) { return false; } MYFILE *const file = LevelCache_OpenBinaryRead(cache_filename, expected_checksum); if (file == nullptr) { return false; } const int32_t version = File_ReadS32(file); const int32_t cached_texture_count = File_ReadS32(file); if (version != M_TRANSPARENCY_CACHE_VERSION || cached_texture_count != texture_count || !File_ReadData( file, m_Priv.uvws.has_transparency_objects, sizeof(bool) * (size_t)texture_count)) { File_Close(file); return false; } File_Close(file); return true; } static void M_WriteTransparencyCache(void) { const int32_t texture_count = Output_GetObjectTextureCount(); const uint64_t checksum = M_ComputeTransparencyChecksum(); const char *const cache_filename = M_GetTransparencyCacheFilename(); if (cache_filename == nullptr || checksum == 0 || texture_count <= 0) { return; } MYFILE *const file = LevelCache_OpenBinaryWrite(cache_filename, checksum); if (file == nullptr) { return; } File_WriteS32(file, M_TRANSPARENCY_CACHE_VERSION); File_WriteS32(file, texture_count); File_WriteData( file, m_Priv.uvws.has_transparency_objects, sizeof(bool) * (size_t)texture_count); File_Close(file); } static float M_NormalizeObjectUV(const uint16_t uv) { return uv / 65535.0f; } static void M_PrepareObjectAnimationRanges(void) { size_t required_size = 0; for (const ANIMATED_TEXTURE_RANGE *src_range = Output_GetAnimatedTextureRange(0); src_range != nullptr; src_range = src_range->next_range) { required_size += src_range->num_textures; } Vector_Clear(m_AnimationRanges.objects); Vector_EnsureCapacity(m_AnimationRanges.objects, required_size); for (const ANIMATED_TEXTURE_RANGE *src_range = Output_GetAnimatedTextureRange(0); src_range != nullptr; src_range = src_range->next_range) { for (int32_t i = 0; i < src_range->num_textures; i++) { Vector_Add( m_AnimationRanges.objects, &(OUTPUT_VERTEX_RANGE) { .vertex_start = src_range->textures[i], .vertex_count = 1, }); } } Output_GlueVertexRanges(m_AnimationRanges.objects); } static void M_PrepareSpriteAnimationRanges(void) { size_t required_size = 0; const int32_t static_2d_count = Object_GetStaticObjects2DCount(); for (int32_t i = 0; i < static_2d_count; i++) { const STATIC_OBJECT_2D *const obj = Object_Get2DStatic(i); if (obj == nullptr || !obj->loaded || obj->frame_count == 1) { continue; } required_size++; } Vector_Clear(m_AnimationRanges.sprites); Vector_EnsureCapacity(m_AnimationRanges.sprites, required_size); for (int32_t i = 0; i < static_2d_count; i++) { const STATIC_OBJECT_2D *const obj = Object_Get2DStatic(i); if (obj == nullptr || !obj->loaded || obj->frame_count == 1) { continue; } Vector_Add( m_AnimationRanges.sprites, &(OUTPUT_VERTEX_RANGE) { .vertex_start = obj->texture_idx, .vertex_count = obj->frame_count, }); } Output_GlueVertexRanges(m_AnimationRanges.sprites); } static void M_PrepareAnimationRanges(void) { M_PrepareObjectAnimationRanges(); M_PrepareSpriteAnimationRanges(); for (int32_t i = 0; i < Output_GetObjectTextureCount(); i++) { m_Priv.uvws.animated_objects[i] = false; for (int32_t j = 0; j < m_AnimationRanges.objects->count; j++) { const OUTPUT_VERTEX_RANGE *const dst_range = Vector_Get(m_AnimationRanges.objects, j); const int32_t range_start = dst_range->vertex_start; const int32_t range_end = range_start + dst_range->vertex_count; if (i >= range_start && i < range_end) { m_Priv.uvws.animated_objects[i] = true; break; } } } for (int32_t i = 0; i < Output_GetSpriteTextureCount(); i++) { m_Priv.uvws.animated_sprites[i] = false; for (int32_t j = 0; j < m_AnimationRanges.sprites->count; j++) { const OUTPUT_VERTEX_RANGE *const dst_range = Vector_Get(m_AnimationRanges.sprites, j); const int32_t range_start = dst_range->vertex_start; const int32_t range_end = range_start + dst_range->vertex_count; if (i >= range_start && i < range_end) { m_Priv.uvws.animated_sprites[i] = true; break; } } } } static void M_FillAtlasObjectSize(const int32_t i) { OUTPUT_TEXTURE_SIZE *const size = &m_Priv.atlas_sizes.data_objects[i]; const OBJECT_TEXTURE *const texture = Output_GetObjectTexture(i); size->x0 = texture->uv[0].u; size->y0 = texture->uv[0].v; size->x1 = texture->uv[0].u; size->y1 = texture->uv[0].v; for (int32_t j = 1; j < texture->uv_count; j++) { size->x0 = MIN(size->x0, texture->uv[j].u); size->y0 = MIN(size->y0, texture->uv[j].v); size->x1 = MAX(size->x1, texture->uv[j].u); size->y1 = MAX(size->y1, texture->uv[j].v); } size->x0 = M_NormalizeObjectUV(size->x0); size->y0 = M_NormalizeObjectUV(size->y0); size->x1 = M_NormalizeObjectUV(size->x1); size->y1 = M_NormalizeObjectUV(size->y1); } static void M_FillAtlasSpriteSize(const int32_t i) { OUTPUT_TEXTURE_SIZE *const size = &m_Priv.atlas_sizes.data_sprites[i]; const SPRITE_TEXTURE *const sprite = Output_GetSpriteTexture(i); const float adj = 0.1 / 256.0f; const float u0 = (sprite->offset & 0xFF) / 256.0f + adj; const float v0 = (sprite->offset >> 8) / 256.0f + adj; const float u1 = u0 + sprite->width / 65536.0f - 2 * adj; const float v1 = v0 + sprite->height / 65536.0f - 2 * adj; size->x0 = u0; size->y0 = v0; size->x1 = u1; size->y1 = v1; } static void M_FillObjectUVW(const int32_t i) { const OBJECT_TEXTURE *const texture = Output_GetObjectTexture(i); OUTPUT_UVW *const corners = m_Priv.uvws.data_objects[i].corners; for (int32_t j = 0; j < 4; j++) { corners[j].u = M_NormalizeObjectUV(texture->uv[j].u); corners[j].v = M_NormalizeObjectUV(texture->uv[j].v); corners[j].w = texture->tex_page; } } static void M_FillSpriteUVW(const int32_t i) { const SPRITE_TEXTURE *const sprite = Output_GetSpriteTexture(i); const float adj = 0.1 / 256.0f; const float u0 = (sprite->offset & 0xFF) / 256.0f + adj; const float v0 = (sprite->offset >> 8) / 256.0f + adj; const float u1 = u0 + sprite->width / 65536.0f - 2 * adj; const float v1 = v0 + sprite->height / 65536.0f - 2 * adj; OUTPUT_UVW *const corners = m_Priv.uvws.data_sprites[i].corners; // clang-format off corners[0].u = u0; corners[0].v = v0; corners[0].w = sprite->tex_page; corners[1].u = u1; corners[1].v = v0; corners[1].w = sprite->tex_page; corners[2].u = u1; corners[2].v = v1; corners[2].w = sprite->tex_page; corners[3].u = u0; corners[3].v = v1; corners[3].w = sprite->tex_page; m_Priv.uvws.flags_sprites[i] = sprite->flags; // clang-format on } static void M_FillObjectUVWs(void) { for (int32_t i = 0; i < Output_GetObjectTextureCount(); i++) { M_FillObjectUVW(i); } } static void M_FillSpriteUVWs(void) { for (int32_t i = 0; i < Output_GetSpriteTextureCount(); i++) { M_FillSpriteUVW(i); } } static void M_UpdateObjectAnimatedUVWs(VECTOR *const source) { for (int32_t i = 0; i < source->count; i++) { const OUTPUT_VERTEX_RANGE *const range = Vector_Get(source, i); for (int32_t j = 0; j < range->vertex_count; j++) { M_FillObjectUVW(range->vertex_start + j); M_FillAtlasObjectSize(range->vertex_start + j); } } } static void M_UpdateSpriteAnimatedUVWs(VECTOR *const source) { for (int32_t i = 0; i < source->count; i++) { const OUTPUT_VERTEX_RANGE *const range = Vector_Get(source, i); for (int32_t j = 0; j < range->vertex_count; j++) { M_FillSpriteUVW(range->vertex_start + j); M_FillAtlasSpriteSize(range->vertex_start + j); } } } static void M_PrepareUVWs(void) { m_Priv.uvws.count_objects = Output_GetObjectTextureCount(); m_Priv.uvws.count_sprites = Output_GetSpriteTextureCount(); m_Priv.uvws.count = m_Priv.uvws.count_objects + m_Priv.uvws.count_sprites; m_Priv.uvws.data = Memory_Alloc(m_Priv.uvws.count * sizeof(M_UVW_PACK)); m_Priv.uvws.data_objects = m_Priv.uvws.data; m_Priv.uvws.data_sprites = m_Priv.uvws.data + m_Priv.uvws.count_objects; m_Priv.uvws.animated = Memory_Alloc(m_Priv.uvws.count * sizeof(bool)); m_Priv.uvws.animated_objects = m_Priv.uvws.animated; m_Priv.uvws.animated_sprites = m_Priv.uvws.animated + m_Priv.uvws.count_objects; m_Priv.uvws.flags = Memory_Alloc(m_Priv.uvws.count * sizeof(uint16_t)); m_Priv.uvws.flags_objects = m_Priv.uvws.flags; m_Priv.uvws.flags_sprites = m_Priv.uvws.flags + m_Priv.uvws.count_objects; m_Priv.uvws.has_transparency_objects = Memory_Alloc(m_Priv.uvws.count_objects * sizeof(bool)); M_FillObjectUVWs(); M_FillSpriteUVWs(); } static bool M_ObjectTextureHasTransparency(const int32_t texture_idx) { if (texture_idx < 0 || texture_idx >= Output_GetObjectTextureCount() || m_TexturePages32 == nullptr) { return true; } const OBJECT_TEXTURE *const texture = Output_GetObjectTexture(texture_idx); if (texture == nullptr || texture->uv_count <= 0) { return true; } if (texture->tex_page >= m_TexturePageCount) { return true; } int32_t min_u = INT32_MAX; int32_t min_v = INT32_MAX; int32_t max_u = INT32_MIN; int32_t max_v = INT32_MIN; for (int32_t i = 0; i < texture->uv_count; i++) { CLAMPG(min_u, texture->uv[i].u); CLAMPG(min_v, texture->uv[i].v); CLAMPL(max_u, texture->uv[i].u); CLAMPL(max_v, texture->uv[i].v); } const int32_t x0 = (min_u * (TEXTURE_PAGE_WIDTH - 1)) / 65535; const int32_t y0 = (min_v * (TEXTURE_PAGE_HEIGHT - 1)) / 65535; const int32_t x1 = (max_u * (TEXTURE_PAGE_WIDTH - 1) + 65534) / 65535; // ceil const int32_t y1 = (max_v * (TEXTURE_PAGE_HEIGHT - 1) + 65534) / 65535; // ceil int32_t px0 = x0; int32_t py0 = y0; int32_t px1 = x1; int32_t py1 = y1; CLAMP(px0, 0, TEXTURE_PAGE_WIDTH - 1); CLAMP(py0, 0, TEXTURE_PAGE_HEIGHT - 1); CLAMP(px1, 0, TEXTURE_PAGE_WIDTH - 1); CLAMP(py1, 0, TEXTURE_PAGE_HEIGHT - 1); const RGBA_8888 *const page = Output_GetTexturePage32(texture->tex_page); if (page == nullptr) { return true; } for (int32_t y = py0; y <= py1; y++) { const int32_t row = y * TEXTURE_PAGE_WIDTH; for (int32_t x = px0; x <= px1; x++) { if (page[row + x].a < 255) { return true; } } } return false; } static void M_PrepareObjectTransparencyFlags(void) { for (int32_t i = 0; i < Output_GetObjectTextureCount(); i++) { m_Priv.uvws.has_transparency_objects[i] = M_ObjectTextureHasTransparency(i); } } static void M_PrepareEnvMap(void) { glGenTextures(1, &m_Priv.tex_env_map); glBindTexture(GL_TEXTURE_2D, m_Priv.tex_env_map); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); TRX_GL_CheckError(); if (Output_IsHeadless()) { const int32_t pattern_size = 256; RGB_888 *test_pattern = Memory_Alloc(pattern_size * pattern_size * sizeof(RGB_888)); RGB_888 *pixel = test_pattern; for (int32_t i = 0; i < pattern_size; i++) { for (int32_t j = 0; j < pattern_size; j++) { pixel->r = i % 256; pixel->g = j % 256; pixel->b = ((i / 32) % 2 == (j / 32) % 2) ? 255 : 0; pixel++; } } glBindTexture(GL_TEXTURE_2D, m_Priv.tex_env_map); glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB, pattern_size, pattern_size, 0, GL_RGB, GL_UNSIGNED_BYTE, test_pattern); TRX_GL_CheckError(); Memory_FreePointer(&test_pattern); } } static void M_PrepareAtlasSizes(void) { const int32_t count_objects = Output_GetObjectTextureCount(); const int32_t count_sprites = Output_GetSpriteTextureCount(); const int32_t count = count_objects + count_sprites; m_Priv.atlas_sizes.data = Memory_Realloc( m_Priv.atlas_sizes.data, count * sizeof(OUTPUT_TEXTURE_SIZE)); m_Priv.atlas_sizes.data_objects = m_Priv.atlas_sizes.data; m_Priv.atlas_sizes.data_sprites = m_Priv.atlas_sizes.data + count_objects; for (int32_t i = 0; i < count_objects; i++) { M_FillAtlasObjectSize(i); } for (int32_t i = 0; i < count_sprites; i++) { M_FillAtlasSpriteSize(i); } } static void M_UploadAtlas(void) { glGenTextures(1, &m_Priv.tex_atlas); glBindTexture(GL_TEXTURE_2D_ARRAY, m_Priv.tex_atlas); glTexStorage3D( GL_TEXTURE_2D_ARRAY, 1, // number of mipmaps GL_RGBA8, TEXTURE_PAGE_WIDTH, TEXTURE_PAGE_HEIGHT, Output_GetTexturePageCount()); TRX_GL_CheckError(); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST); TRX_GL_CheckError(); for (int32_t i = 0; i < Output_GetTexturePageCount(); i++) { const RGBA_8888 *const input_ptr = Output_GetTexturePage32(i); glTexSubImage3D( GL_TEXTURE_2D_ARRAY, 0, // mipmap level 0, // x offset 0, // y offset i, // z offset TEXTURE_PAGE_WIDTH, TEXTURE_PAGE_HEIGHT, 1, // depth GL_RGBA, GL_UNSIGNED_BYTE, input_ptr); } TRX_GL_CheckError(); M_PrepareAtlasSizes(); TRX_GL_CheckError(); } static void M_FreeLevelData(void) { // destroy per-page locks if (m_TexturePageLocks != nullptr) { for (int32_t i = 0; i < m_TexturePageCount; i++) { SDL_DestroyMutex(m_TexturePageLocks[i]); } m_TexturePageLocks = nullptr; } if (m_Priv.tex_atlas != 0) { glDeleteTextures(1, &m_Priv.tex_atlas); m_Priv.tex_atlas = 0; } Memory_FreePointer(&m_Priv.uvws.data); Memory_FreePointer(&m_Priv.uvws.animated); Memory_FreePointer(&m_Priv.uvws.flags); Memory_FreePointer(&m_Priv.uvws.has_transparency_objects); Memory_FreePointer(&m_Priv.atlas_sizes.data); memset(&m_Priv.uvws, 0, sizeof(m_Priv.uvws)); memset(&m_Priv.atlas_sizes, 0, sizeof(m_Priv.atlas_sizes)); } void Output_Textures_Init(void) { M_PrepareEnvMap(); m_AnimationRanges.objects = Vector_Create(sizeof(OUTPUT_VERTEX_RANGE)); m_AnimationRanges.sprites = Vector_Create(sizeof(OUTPUT_VERTEX_RANGE)); } void Output_Textures_Shutdown(void) { if (m_AnimationRanges.objects != nullptr) { Vector_Free(m_AnimationRanges.objects); m_AnimationRanges.objects = nullptr; } if (m_AnimationRanges.sprites != nullptr) { Vector_Free(m_AnimationRanges.sprites); m_AnimationRanges.sprites = nullptr; } M_FreeLevelData(); if (m_Priv.tex_env_map != 0) { glDeleteTextures(1, &m_Priv.tex_env_map); m_Priv.tex_env_map = 0; } // These are GameBuf-backed and become invalid once GameBuf_Shutdown runs. m_TexturePageCount = 0; m_TexturePages8 = nullptr; m_TexturePages32 = nullptr; m_TexturePageLocks = nullptr; m_PaletteSize = 0; m_Palette8 = nullptr; m_Palette16 = nullptr; m_ObjectTextureCount = 0; m_SpriteTextureCount = 0; m_ObjectTextures = nullptr; m_SpriteTextures = nullptr; m_AnimTextureRanges = nullptr; } void Output_Textures_ObserveLevelLoad(void) { M_FreeLevelData(); M_PrepareUVWs(); if (!M_TryLoadTransparencyCache()) { M_PrepareObjectTransparencyFlags(); M_WriteTransparencyCache(); } M_PrepareAnimationRanges(); M_UploadAtlas(); } void Output_Textures_UpdateEnvironmentMap(void) { if (Output_IsHeadless()) { return; } GLint viewport[4]; glGetIntegerv(GL_VIEWPORT, viewport); TRX_GL_CheckError(); const GLint vp_x = viewport[0]; const GLint vp_y = viewport[1]; const GLint vp_w = viewport[2]; const GLint vp_h = viewport[3]; const int32_t side = MIN(vp_w, vp_h); const int32_t x = vp_x + (vp_w - side) / 2; const int32_t y = vp_y + (vp_h - side) / 2; const int32_t w = side; const int32_t h = side; glBindTexture(GL_TEXTURE_2D, m_Priv.tex_env_map); glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, x, y, w, h, 0); TRX_GL_CheckError(); } void Output_Textures_CycleAnimations(void) { if (m_Priv.uvws.count != 0) { M_UpdateSpriteAnimatedUVWs(m_AnimationRanges.sprites); M_UpdateObjectAnimatedUVWs(m_AnimationRanges.objects); } } GLuint Output_Textures_GetAtlasTexture(void) { return m_Priv.tex_atlas; } GLuint Output_Textures_GetEnvMapTexture(void) { return m_Priv.tex_env_map; } int32_t Output_Textures_GetObjectUVWIndex(int32_t texture_idx, int32_t corner) { return texture_idx * 4 + corner; } int32_t Output_Textures_GetSpriteUVWIndex(int32_t texture_idx, int32_t corner) { return (m_Priv.uvws.count_objects + texture_idx) * 4 + corner; } OUTPUT_UVW Output_Textures_GetUVW(const int32_t uvw_idx) { ASSERT(uvw_idx >= 0 && uvw_idx / 4 < m_Priv.uvws.count); return m_Priv.uvws.data[uvw_idx / 4].corners[uvw_idx % 4]; } OUTPUT_TEXTURE_SIZE Output_Textures_GetAtlasSize(const int32_t uvw_idx) { ASSERT(uvw_idx >= 0 && uvw_idx < m_Priv.uvws.count); return m_Priv.atlas_sizes.data[uvw_idx]; } bool Output_Textures_IsObjectTextureAnimated(const int32_t texture_idx) { return m_Priv.uvws.animated_objects[texture_idx]; } bool Output_Textures_IsSpriteTextureAnimated(const int32_t texture_idx) { return m_Priv.uvws.animated_sprites[texture_idx]; } void Output_Textures_SetSpriteTextureFlags( const int32_t texture_idx, const uint16_t flags) { m_Priv.uvws.flags_sprites[texture_idx] = flags; } uint16_t Output_Textures_GetSpriteTextureFlags(const int32_t texture_idx) { return VERT_BILLBOARD | m_Priv.uvws.flags_sprites[texture_idx]; } SCENE_PASS Output_Textures_GetObjectTextureScenePass(const int32_t texture_idx) { switch (Output_GetObjectTexture(texture_idx)->draw_type) { case DRAW_OPAQUE: case DRAW_REFLECTIVE_OPAQUE: return SCENE_PASS_OPAQUE; case DRAW_BLEND: if (!m_Priv.uvws.animated_objects[texture_idx] && !m_Priv.uvws.has_transparency_objects[texture_idx]) { return SCENE_PASS_OPAQUE; } return SCENE_PASS_TRANSPARENT; case DRAW_BLEND_ADD: case DRAW_REFLECTIVE_BLEND_ADD: return SCENE_PASS_BLEND_ADD; case DRAW_BLEND_SUB: return SCENE_PASS_BLEND_SUB; } return SCENE_PASS_OPAQUE; } void Output_Textures_ApplyRenderSettings(void) { // re-adjust UVs when the bilinear filter is toggled. if (m_Priv.uvws.count != 0) { M_FillObjectUVWs(); } } void Output_InitialiseTexturePages(const int32_t num_pages, const bool use_8bit) { m_TexturePageCount = num_pages; if (num_pages == 0) { m_TexturePages32 = nullptr; m_TexturePages8 = nullptr; return; } const int32_t page_size = num_pages * TEXTURE_PAGE_SIZE; m_TexturePages32 = GameBuf_Alloc(sizeof(RGBA_8888) * page_size, GBUF_TEXTURE_PAGES); m_TexturePages8 = use_8bit ? GameBuf_Alloc(sizeof(uint8_t) * page_size, GBUF_TEXTURE_PAGES) : nullptr; m_TexturePageLocks = GameBuf_Alloc(sizeof(SDL_mutex *) * num_pages, GBUF_TEXTURE_PAGES); for (int32_t i = 0; i < num_pages; i++) { m_TexturePageLocks[i] = SDL_CreateMutex(); ASSERT(m_TexturePageLocks[i] != nullptr); } } void Output_InitialisePalettes( const int32_t palette_size, const RGB_888 *const palette_8, const RGB_888 *const palette_16) { ASSERT(palette_size != 0); ASSERT(palette_8 != nullptr); m_PaletteSize = palette_size; m_Palette8 = GameBuf_Alloc(sizeof(RGB_888) * palette_size, GBUF_PALETTES); memcpy(m_Palette8, palette_8, sizeof(RGB_888) * palette_size); if (palette_16 != nullptr) { m_Palette16 = GameBuf_Alloc(sizeof(RGB_888) * palette_size, GBUF_PALETTES); memcpy(m_Palette16, palette_16, sizeof(RGB_888) * palette_size); } else { m_Palette16 = nullptr; } } void Output_InitialiseObjectTextures(const int32_t num_textures) { m_ObjectTextureCount = num_textures; m_ObjectTextures = num_textures == 0 ? nullptr : GameBuf_Alloc( sizeof(OBJECT_TEXTURE) * num_textures, GBUF_OBJECT_TEXTURES); } void Output_InitialiseSpriteTextures(const int32_t num_textures) { m_SpriteTextureCount = num_textures; m_SpriteTextures = num_textures == 0 ? nullptr : GameBuf_Alloc( sizeof(SPRITE_TEXTURE) * num_textures, GBUF_SPRITE_TEXTURES); } void Output_InitialiseAnimatedTextures(const int32_t num_ranges) { m_AnimTextureRanges = num_ranges == 0 ? nullptr : GameBuf_Alloc( sizeof(ANIMATED_TEXTURE_RANGE) * num_ranges, GBUF_ANIMATED_TEXTURE_RANGES); } int32_t Output_GetTexturePageCount(void) { return m_TexturePageCount; } uint8_t *Output_GetTexturePage8(const int32_t page_idx) { if (m_TexturePages8 == nullptr) { return nullptr; } return &m_TexturePages8[page_idx * TEXTURE_PAGE_SIZE]; } RGBA_8888 *Output_GetTexturePage32(const int32_t page_idx) { if (m_TexturePages32 == nullptr) { return nullptr; } return &m_TexturePages32[page_idx * TEXTURE_PAGE_SIZE]; } void Output_LockTexturePage32(const int32_t page_idx) { ASSERT(page_idx >= 0 && page_idx < m_TexturePageCount); SDL_LockMutex(m_TexturePageLocks[page_idx]); } void Output_UnlockTexturePage32(const int32_t page_idx) { ASSERT(page_idx >= 0 && page_idx < m_TexturePageCount); SDL_UnlockMutex(m_TexturePageLocks[page_idx]); } int32_t Output_GetPaletteSize(void) { return m_PaletteSize; } RGB_888 Output_GetPaletteColor8(const uint16_t idx) { if (m_Palette8 == nullptr) { return COLOR_RGB_888_BLACK; } return m_Palette8[idx]; } RGB_888 Output_GetPaletteColor16(const uint16_t idx) { if (m_Palette16 == nullptr) { return COLOR_RGB_888_BLACK; } return m_Palette16[idx]; } LIGHT_MAP *Output_GetLightMap(const uint8_t idx) { return &m_LightMap[idx]; } SHADE_MAP *Output_GetShadeMap(const uint8_t idx) { return &m_ShadeMap[idx]; } int32_t Output_GetObjectTextureCount(void) { return m_ObjectTextureCount; } int32_t Output_GetSpriteTextureCount(void) { return m_SpriteTextureCount; } OBJECT_TEXTURE *Output_GetObjectTexture(const int32_t texture_idx) { if (m_ObjectTextures == nullptr) { return nullptr; } return &m_ObjectTextures[texture_idx]; } SPRITE_TEXTURE *Output_GetSpriteTexture(const int32_t texture_idx) { if (m_SpriteTextures == nullptr) { return nullptr; } return &m_SpriteTextures[texture_idx]; } ANIMATED_TEXTURE_RANGE *Output_GetAnimatedTextureRange(const int32_t range_idx) { if (m_AnimTextureRanges == nullptr) { return nullptr; } return &m_AnimTextureRanges[range_idx]; } RGBA_8888 Output_RGB2RGBA(const RGB_888 color) { return (RGBA_8888) { .r = color.r, .g = color.g, .b = color.b, .a = 255 }; } RGBA_F Output_RGB2RGBA_F(const RGB_F color) { return (RGBA_F) { .r = color.r, .g = color.g, .b = color.b, .a = 1.0f }; } int16_t Output_FindColor8(const RGB_888 color) { if (m_Palette8 == nullptr) { return -1; } int32_t best_idx = 0; int32_t best_diff = INT32_MAX; for (int32_t i = 0; i < m_PaletteSize; i++) { const int32_t dr = color.r - m_Palette8[i].r; const int32_t dg = color.g - m_Palette8[i].g; const int32_t db = color.b - m_Palette8[i].b; const int32_t diff = SQUARE(dr) + SQUARE(dg) + SQUARE(db); if (diff < best_diff) { best_diff = diff; best_idx = i; } } return best_idx; } void Output_CycleAnimatedTextures(void) { const ANIMATED_TEXTURE_RANGE *range = m_AnimTextureRanges; for (; range != nullptr; range = range->next_range) { int32_t i = 0; const OBJECT_TEXTURE temp = m_ObjectTextures[range->textures[i]]; for (; i < range->num_textures - 1; i++) { m_ObjectTextures[range->textures[i]] = m_ObjectTextures[range->textures[i + 1]]; } m_ObjectTextures[range->textures[i]] = temp; } const int32_t static_2d_count = Object_GetStaticObjects2DCount(); for (int32_t i = 0; i < static_2d_count; i++) { const STATIC_OBJECT_2D *const obj = Object_Get2DStatic(i); if (obj == nullptr || !obj->loaded || obj->frame_count == 1) { continue; } const int16_t frame_count = obj->frame_count; const SPRITE_TEXTURE temp = m_SpriteTextures[obj->texture_idx]; for (int32_t j = 0; j < frame_count - 1; j++) { m_SpriteTextures[obj->texture_idx + j] = m_SpriteTextures[obj->texture_idx + j + 1]; } m_SpriteTextures[obj->texture_idx + frame_count - 1] = temp; } } ================================================ FILE: src/trx/game/output/textures.h ================================================ #pragma once #include #include #include #pragma pack(push, 1) typedef struct { float x0; float y0; float x1; float y1; } OUTPUT_TEXTURE_SIZE; typedef struct { float u; float v; float w; } OUTPUT_UVW; #pragma pack(pop) void Output_Textures_Init(void); void Output_Textures_Shutdown(void); void Output_Textures_ObserveLevelLoad(void); void Output_Textures_UpdateEnvironmentMap(void); void Output_Textures_CycleAnimations(void); void Output_Textures_ApplyRenderSettings(void); GLuint Output_Textures_GetAtlasTexture(void); GLuint Output_Textures_GetEnvMapTexture(void); int32_t Output_Textures_GetObjectUVWIndex( int32_t texture_idx, int32_t face_idx); int32_t Output_Textures_GetSpriteUVWIndex( int32_t texture_idx, int32_t face_idx); OUTPUT_UVW Output_Textures_GetUVW(int32_t uvw_idx); OUTPUT_TEXTURE_SIZE Output_Textures_GetAtlasSize(int32_t uvw_idx); bool Output_Textures_IsObjectTextureAnimated(int32_t texture_idx); SCENE_PASS Output_Textures_GetObjectTextureScenePass(int32_t texture_idx); bool Output_Textures_IsSpriteTextureAnimated(int32_t sprite_idx); uint16_t Output_Textures_GetSpriteTextureFlags(int32_t sprite_idx); void Output_Textures_SetSpriteTextureFlags(int32_t sprite_idx, uint16_t flags); // Public utility methods ===================================================== void Output_InitialiseTexturePages(int32_t num_pages, bool use_8bit); void Output_InitialisePalettes( int32_t palette_size, const RGB_888 *palette_8, const RGB_888 *palette_16); void Output_InitialiseObjectTextures(int32_t num_textures); void Output_InitialiseSpriteTextures(int32_t num_textures); void Output_InitialiseAnimatedTextures(int32_t num_ranges); int32_t Output_GetTexturePageCount(void); uint8_t *Output_GetTexturePage8(int32_t page_idx); RGBA_8888 *Output_GetTexturePage32(int32_t page_idx); void Output_LockTexturePage32(int32_t page_idx); void Output_UnlockTexturePage32(int32_t page_idx); int32_t Output_GetPaletteSize(void); RGB_888 Output_GetPaletteColor8(uint16_t idx); RGB_888 Output_GetPaletteColor16(uint16_t idx); LIGHT_MAP *Output_GetLightMap(uint8_t idx); SHADE_MAP *Output_GetShadeMap(uint8_t idx); int32_t Output_GetObjectTextureCount(void); int32_t Output_GetSpriteTextureCount(void); OBJECT_TEXTURE *Output_GetObjectTexture(int32_t texture_idx); SPRITE_TEXTURE *Output_GetSpriteTexture(int32_t texture_idx); ANIMATED_TEXTURE_RANGE *Output_GetAnimatedTextureRange(int32_t range_idx); RGBA_8888 Output_RGB2RGBA(RGB_888 color); RGBA_F Output_RGB2RGBA_F(RGB_F color); int16_t Output_FindColor8(RGB_888 color); void Output_CycleAnimatedTextures(void); ================================================ FILE: src/trx/game/output/types.h ================================================ #pragma once #include #include #include #include typedef enum { CLIP_NOT_VISIBLE = 0, CLIP_PARTIALLY_VISIBLE = -1, CLIP_FULLY_VISIBLE = 1, } CLIP; typedef enum { TS_HEADING, TS_BACKGROUND, TS_BACKGROUND_HEAVY, TS_REQUESTED, } TEXT_STYLE; typedef enum { DRAW_OPAQUE = 0, DRAW_BLEND = 1, DRAW_BLEND_ADD = 2, DRAW_BLEND_SUB = 3, DRAW_REFLECTIVE_OPAQUE = 8, DRAW_REFLECTIVE_BLEND_ADD = 9, } DRAW_TYPE; typedef struct { int16_t value_1; int16_t value_2; } SHADE; typedef struct { int32_t value_1; int32_t value_2; } FALLOFF; typedef struct { uint16_t u; uint16_t v; } TEXTURE_UV; typedef struct { union { struct { float z; float w; }; float zw[2]; }; } TEXTURE_ZW_F; typedef struct { uint16_t draw_type; uint16_t tex_page; int32_t uv_count; TEXTURE_UV uv[4]; } OBJECT_TEXTURE; typedef struct { uint16_t tex_page; uint16_t offset; uint16_t width; uint16_t height; int16_t x0; int16_t y0; int16_t x1; int16_t y1; uint16_t flags; } SPRITE_TEXTURE; typedef struct ANIMATED_TEXTURE_RANGE { int16_t num_textures; int16_t *textures; struct ANIMATED_TEXTURE_RANGE *next_range; } ANIMATED_TEXTURE_RANGE; typedef struct { uint8_t index[256]; } LIGHT_MAP; typedef struct { uint8_t index[LIGHT_MAP_SIZE]; } SHADE_MAP; typedef struct { XYZ_32 from; XYZ_32 to; int32_t thickness; } LIGHTNING_SEGMENT; ================================================ FILE: src/trx/game/output/uniforms.c ================================================ #define M_MAX_LIGHTS 32 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_GLOBAL_MEMBERS \ X_DECLARE_MEMBER(float, global_tint, [4]) \ X_DECLARE_MEMBER(float, fog_color, [4]) \ X_DECLARE_MEMBER(float, fog_distance, [2]) \ X_DECLARE_MEMBER(float, viewport_size, [2]) \ X_DECLARE_MEMBER(float, time) \ X_DECLARE_MEMBER(float, time_in_game) \ X_DECLARE_MEMBER(float, brightness_multiplier) \ X_DECLARE_MEMBER(float, ui_brightness_multiplier) \ X_DECLARE_MEMBER(float, gamma) \ X_DECLARE_MEMBER(float, desaturation) \ X_DECLARE_MEMBER(float, sunset_duration) \ X_DECLARE_MEMBER(float, min_shade) \ X_DECLARE_MEMBER(int, billboard_lock_mode) \ X_DECLARE_MEMBER(int, lighting_enabled) \ X_DECLARE_MEMBER(int, trapezoid_filter_enabled) \ X_DECLARE_MEMBER(int, reflections_enabled) \ X_DECLARE_MEMBER(int, textures_enabled) \ X_DECLARE_MEMBER(int, tr_version) #pragma pack(push, 4) typedef struct { #define X_DECLARE_MEMBER(a, b, ...) a b __VA_ARGS__; M_GLOBAL_MEMBERS #undef X_DECLARE_MEMBER } M_UNIFORM_GENERAL; typedef struct { float mat_proj[4][4]; float mat_view[4][4]; } M_UNIFORM_MATRICES; typedef struct { float pos[4]; float color[4]; float shade; float falloff; float kind; float _pad; } M_UNIFORM_LIGHT; typedef struct { int num_lights; int room_light_mode; int _pad[2]; M_UNIFORM_LIGHT lights[M_MAX_LIGHTS]; } M_UNIFORM_LIGHTS; typedef struct { float adder; float divider; float _pad0[2]; float vector_view[4]; float tr3_ambient[4]; float tr3_light_dir_view[3][4]; float tr3_light_color[3][4]; } M_UNIFORM_LS; #pragma pack(pop) typedef enum { M_LS_MODE_NONE = 0, M_LS_MODE_FULL = 1, M_LS_MODE_OWN = 2, } M_LS_MODE; typedef struct { M_UNIFORM_LIGHTS last_lights; OUTPUT_LIGHT_INFO last_light_info; M_LS_MODE last_ls_mode; int32_t last_own_light_adder; RGB_F last_own_light_tr3_ambient; } M_PRIV; static void M_FillLight( M_UNIFORM_LIGHT *const dst_light, const LIGHT *const src_light) { dst_light->pos[0] = src_light->pos.x; dst_light->pos[1] = src_light->pos.y; dst_light->pos[2] = src_light->pos.z; dst_light->pos[3] = 0.0f; dst_light->color[0] = src_light->color.r / 255.0f; dst_light->color[1] = src_light->color.g / 255.0f; dst_light->color[2] = src_light->color.b / 255.0f; dst_light->color[3] = 0.0f; dst_light->shade = src_light->shade.value_1; dst_light->falloff = src_light->falloff.value_1; dst_light->kind = src_light->type; } static int16_t M_GetMinShade(void) { switch (g_Config.rendering.lighting_contrast) { case LIGHTING_CONTRAST_LOW: return SHADE_NEUTRAL; case LIGHTING_CONTRAST_MEDIUM: return SHADE_HIGH; case LIGHTING_CONTRAST_HIGH: return 0; default: return SHADE_NEUTRAL; } } void Output_Uniforms_UploadOrthoMatrix(const OUTPUT_UNIFORMS *const uniforms) { M_UNIFORM_MATRICES matrices = {}; Output_GetOrthoProjectionMatrix(matrices.mat_proj); Output_FillMatrix(matrices.mat_view, &g_IDMatrix); glBindBuffer(GL_UNIFORM_BUFFER, uniforms->matrices); TRX_GL_TRACK_SUBDATA( glBufferSubData, GL_UNIFORM_BUFFER, 0, sizeof(matrices), &matrices); } void Output_Uniforms_UploadViewMatrix( const OUTPUT_UNIFORMS *const uniforms, const MATRIX *const matrix) { M_UNIFORM_MATRICES matrices = {}; Output_GetPerspProjectionMatrix(matrices.mat_proj); Output_FillMatrix(matrices.mat_view, matrix); glBindBuffer(GL_UNIFORM_BUFFER, uniforms->matrices); TRX_GL_TRACK_SUBDATA( glBufferSubData, GL_UNIFORM_BUFFER, 0, sizeof(matrices), &matrices); } void Output_Uniforms_UploadGeneral(const OUTPUT_UNIFORMS *const uniforms) { M_UNIFORM_GENERAL general = { .time = Output_GetTime(), .time_in_game = Output_GetTimeInGame(), .brightness_multiplier = g_Config.visuals.game_brightness, .ui_brightness_multiplier = g_Config.visuals.ui_brightness, .gamma = g_Config.visuals.gamma, .desaturation = Output_GetDesaturation(), .sunset_duration = Output_GetSunsetDuration(), .tr_version = g_TRVersion, .viewport_size = { (float)Viewport_GetWidth(VIEWPORT_GAME), (float)Viewport_GetHeight(VIEWPORT_GAME), }, .min_shade = M_GetMinShade(), .billboard_lock_mode = g_Config.rendering.sprite_lock_mode, .lighting_enabled = g_Config.rendering.enable_lighting, .textures_enabled = g_Config.rendering.enable_textures, .trapezoid_filter_enabled = g_Config.rendering.enable_trapezoid_filter, .reflections_enabled = g_Config.visuals.enable_reflections, .fog_distance = {Output_GetFogStart(), Output_GetFogEnd()}, .fog_color = { Output_GetFogColor().r, Output_GetFogColor().g, Output_GetFogColor().b, Output_GetFogColor().a, }, .global_tint = { Output_GetGlobalTint().r, Output_GetGlobalTint().g, Output_GetGlobalTint().b, 1.0f, }, }; glBindBuffer(GL_UNIFORM_BUFFER, uniforms->general); TRX_GL_TRACK_SUBDATA( glBufferSubData, GL_UNIFORM_BUFFER, 0, sizeof(general), &general); } void Output_Uniforms_UploadFogDistance( const OUTPUT_UNIFORMS *const uniforms, const float start, const float end) { ASSERT(uniforms != nullptr); const float fog_distance[2] = { start, end }; glBindBuffer(GL_UNIFORM_BUFFER, uniforms->general); TRX_GL_TRACK_SUBDATA( glBufferSubData, GL_UNIFORM_BUFFER, offsetof(M_UNIFORM_GENERAL, fog_distance), sizeof(fog_distance), &fog_distance); } void Output_Uniforms_UploadDesaturation( const OUTPUT_UNIFORMS *const uniforms, const float desaturation) { ASSERT(uniforms != nullptr); float clamped = desaturation; CLAMP(clamped, 0.0f, 1.0f); glBindBuffer(GL_UNIFORM_BUFFER, uniforms->general); TRX_GL_TRACK_SUBDATA( glBufferSubData, GL_UNIFORM_BUFFER, offsetof(M_UNIFORM_GENERAL, desaturation), sizeof(clamped), &clamped); } void Output_Uniforms_UploadGlobalTint( const OUTPUT_UNIFORMS *const uniforms, const RGB_F tint) { ASSERT(uniforms != nullptr); glBindBuffer(GL_UNIFORM_BUFFER, uniforms->general); TRX_GL_TRACK_SUBDATA( glBufferSubData, GL_UNIFORM_BUFFER, offsetof(M_UNIFORM_GENERAL, global_tint), sizeof(tint), &tint); } void Output_Uniforms_UploadGameBrightnessMultiplier( const OUTPUT_UNIFORMS *const uniforms, const float game_brightness_multiplier) { ASSERT(uniforms != nullptr); float clamped = game_brightness_multiplier; CLAMP(clamped, CONFIG_MIN_BRIGHTNESS, CONFIG_MAX_BRIGHTNESS); glBindBuffer(GL_UNIFORM_BUFFER, uniforms->general); TRX_GL_TRACK_SUBDATA( glBufferSubData, GL_UNIFORM_BUFFER, offsetof(M_UNIFORM_GENERAL, brightness_multiplier), sizeof(clamped), &clamped); } void Output_Uniforms_UploadUIBrightnessMultiplier( const OUTPUT_UNIFORMS *const uniforms, const float brightness_multiplier) { ASSERT(uniforms != nullptr); float clamped = brightness_multiplier; CLAMP(clamped, CONFIG_MIN_BRIGHTNESS, CONFIG_MAX_BRIGHTNESS); glBindBuffer(GL_UNIFORM_BUFFER, uniforms->general); TRX_GL_TRACK_SUBDATA( glBufferSubData, GL_UNIFORM_BUFFER, offsetof(M_UNIFORM_GENERAL, ui_brightness_multiplier), sizeof(clamped), &clamped); } void Output_Uniforms_UploadRoomLights( const OUTPUT_UNIFORMS *const uniforms, const ROOM *const room) { M_UNIFORM_LIGHTS lights = {}; // Only dynamic lights for now. M_UNIFORM_LIGHT *dst_light = lights.lights; if (room == nullptr) { lights.room_light_mode = RLM_SUNSET; } else { lights.room_light_mode = room->light_mode; } VECTOR *const dynamic_lights = Output_GetDynamicLights(); for (int32_t i = 0; i < dynamic_lights->count; i++) { M_FillLight(dst_light, Vector_Get(dynamic_lights, i)); dst_light++; if (dst_light - lights.lights >= M_MAX_LIGHTS) { break; } } lights.num_lights = dst_light - lights.lights; const size_t size = offsetof(M_UNIFORM_LIGHTS, lights) + lights.num_lights * sizeof(M_UNIFORM_LIGHT); M_PRIV *const p = uniforms->priv; if (memcmp(&p->last_lights, &lights, sizeof(lights)) == 0) { return; } memcpy(&p->last_lights, &lights, sizeof(lights)); glBindBuffer(GL_UNIFORM_BUFFER, uniforms->lights); TRX_GL_TRACK_SUBDATA(glBufferSubData, GL_UNIFORM_BUFFER, 0, size, &lights); } void Output_Uniforms_UploadCPULight( const OUTPUT_UNIFORMS *const uniforms, const OUTPUT_LIGHT_INFO *const info) { M_PRIV *const p = uniforms->priv; if (p->last_ls_mode == M_LS_MODE_FULL && memcmp(&p->last_light_info, info, sizeof(*info)) == 0) { return; } memcpy(&p->last_light_info, info, sizeof(*info)); p->last_own_light_adder = info->ls_adder; p->last_own_light_tr3_ambient = info->tr3_ambient; p->last_ls_mode = M_LS_MODE_FULL; M_UNIFORM_LS ls = {}; ls.adder = info->ls_adder; ls.divider = info->ls_divider / (float)(1 << (W2V_SHIFT)); ls.vector_view[0] = info->ls_vector_view.x; ls.vector_view[1] = info->ls_vector_view.y; ls.vector_view[2] = info->ls_vector_view.z; ls.vector_view[3] = 0; ls.tr3_ambient[0] = info->tr3_ambient.r; ls.tr3_ambient[1] = info->tr3_ambient.g; ls.tr3_ambient[2] = info->tr3_ambient.b; ls.tr3_ambient[3] = 0.0f; for (int32_t i = 0; i < 3; i++) { float x = (float)info->tr3_light_dir_view[i].x; float y = (float)info->tr3_light_dir_view[i].y; float z = (float)info->tr3_light_dir_view[i].z; const float len2 = x * x + y * y + z * z; if (len2 > 0.0f) { const float inv_len = 1.0f / sqrtf(len2); x *= inv_len; y *= inv_len; z *= inv_len; } ls.tr3_light_dir_view[i][0] = x; ls.tr3_light_dir_view[i][1] = y; ls.tr3_light_dir_view[i][2] = z; ls.tr3_light_dir_view[i][3] = 0.0f; ls.tr3_light_color[i][0] = info->tr3_light_color[i].r; ls.tr3_light_color[i][1] = info->tr3_light_color[i].g; ls.tr3_light_color[i][2] = info->tr3_light_color[i].b; ls.tr3_light_color[i][3] = 0.0f; } glBindBuffer(GL_UNIFORM_BUFFER, uniforms->ls); TRX_GL_TRACK_SUBDATA( glBufferSubData, GL_UNIFORM_BUFFER, 0, sizeof(ls), &ls); TRX_GL_CheckError(); } void Output_Uniforms_UploadOwnLight( const OUTPUT_UNIFORMS *const uniforms, const OUTPUT_LIGHT_INFO *const info) { M_PRIV *const priv = uniforms->priv; if (g_TRVersion >= 3) { if (priv->last_ls_mode == M_LS_MODE_OWN && priv->last_own_light_tr3_ambient.r == info->tr3_ambient.r && priv->last_own_light_tr3_ambient.g == info->tr3_ambient.g && priv->last_own_light_tr3_ambient.b == info->tr3_ambient.b) { return; } const float ambient[4] = { info->tr3_ambient.r, info->tr3_ambient.g, info->tr3_ambient.b, 0.0f, }; glBindBuffer(GL_UNIFORM_BUFFER, uniforms->ls); TRX_GL_TRACK_SUBDATA( glBufferSubData, GL_UNIFORM_BUFFER, offsetof(M_UNIFORM_LS, tr3_ambient), sizeof(ambient), ambient); priv->last_own_light_tr3_ambient = info->tr3_ambient; } else { if (priv->last_ls_mode == M_LS_MODE_OWN && priv->last_own_light_adder == info->ls_adder) { return; } const float light_adder = info->ls_adder; glBindBuffer(GL_UNIFORM_BUFFER, uniforms->ls); TRX_GL_TRACK_SUBDATA( glBufferSubData, GL_UNIFORM_BUFFER, offsetof(M_UNIFORM_LS, adder), sizeof(light_adder), &light_adder); priv->last_own_light_adder = info->ls_adder; } priv->last_ls_mode = M_LS_MODE_OWN; } OUTPUT_UNIFORMS *Output_Uniforms_Create(void) { OUTPUT_UNIFORMS *const uniforms = Memory_Alloc(sizeof(OUTPUT_UNIFORMS) + sizeof(M_PRIV)); glGenBuffers(4, &uniforms->general); glBindBuffer(GL_UNIFORM_BUFFER, uniforms->general); glBufferData( GL_UNIFORM_BUFFER, sizeof(M_UNIFORM_GENERAL), nullptr, GL_DYNAMIC_DRAW); glBindBufferBase(GL_UNIFORM_BUFFER, 0, uniforms->general); glBindBuffer(GL_UNIFORM_BUFFER, uniforms->matrices); glBufferData( GL_UNIFORM_BUFFER, sizeof(M_UNIFORM_MATRICES), nullptr, GL_DYNAMIC_DRAW); glBindBufferBase(GL_UNIFORM_BUFFER, 1, uniforms->matrices); glBindBuffer(GL_UNIFORM_BUFFER, uniforms->lights); glBufferData( GL_UNIFORM_BUFFER, sizeof(M_UNIFORM_LIGHTS), nullptr, GL_DYNAMIC_DRAW); glBindBufferBase(GL_UNIFORM_BUFFER, 2, uniforms->lights); glBindBuffer(GL_UNIFORM_BUFFER, uniforms->ls); glBufferData( GL_UNIFORM_BUFFER, sizeof(M_UNIFORM_LS), nullptr, GL_DYNAMIC_DRAW); glBindBufferBase(GL_UNIFORM_BUFFER, 3, uniforms->ls); TRX_GL_CheckError(); uniforms->priv = (char *)uniforms + sizeof(OUTPUT_UNIFORMS); return uniforms; } void Output_Uniforms_Free(OUTPUT_UNIFORMS *const uniforms) { if (uniforms == nullptr) { return; } if (uniforms->general != 0) { glDeleteBuffers(4, &uniforms->general); uniforms->general = 0; uniforms->matrices = 0; uniforms->lights = 0; uniforms->ls = 0; } Memory_Free(uniforms); } ================================================ FILE: src/trx/game/output/uniforms.h ================================================ #pragma once #include #include #include #include typedef struct { int32_t ls_adder; int32_t ls_divider; XYZ_32 ls_vector_view; RGB_F tr3_ambient; RGB_F tr3_light_color[3]; XYZ_32 tr3_light_dir_view[3]; } OUTPUT_LIGHT_INFO; typedef struct { GLuint general; GLuint matrices; GLuint lights; GLuint ls; void *priv; } OUTPUT_UNIFORMS; OUTPUT_UNIFORMS *Output_Uniforms_Create(void); void Output_Uniforms_Free(OUTPUT_UNIFORMS *uniforms); void Output_Uniforms_UploadGeneral(const OUTPUT_UNIFORMS *uniforms); void Output_Uniforms_UploadOrthoMatrix(const OUTPUT_UNIFORMS *uniforms); void Output_Uniforms_UploadViewMatrix( const OUTPUT_UNIFORMS *uniforms, const MATRIX *matrix); void Output_Uniforms_UploadRoomLights( const OUTPUT_UNIFORMS *uniforms, const ROOM *room); void Output_Uniforms_UploadCPULight( const OUTPUT_UNIFORMS *uniforms, const OUTPUT_LIGHT_INFO *info); void Output_Uniforms_UploadOwnLight( const OUTPUT_UNIFORMS *uniforms, const OUTPUT_LIGHT_INFO *info); void Output_Uniforms_UploadFogDistance( const OUTPUT_UNIFORMS *uniforms, float start, float end); void Output_Uniforms_UploadDesaturation( const OUTPUT_UNIFORMS *uniforms, float desaturation); void Output_Uniforms_UploadGlobalTint( const OUTPUT_UNIFORMS *uniforms, RGB_F tint); ================================================ FILE: src/trx/game/output/utils.c ================================================ #include #include void Output_FillMatrix(GLfloat m[4][4], const MATRIX *const source) { m[0][0] = source->_00 / (float)(1 << W2V_SHIFT); m[0][1] = source->_10 / (float)(1 << W2V_SHIFT); m[0][2] = source->_20 / (float)(1 << W2V_SHIFT); m[0][3] = 0.0; m[1][0] = source->_01 / (float)(1 << W2V_SHIFT); m[1][1] = source->_11 / (float)(1 << W2V_SHIFT); m[1][2] = source->_21 / (float)(1 << W2V_SHIFT); m[1][3] = 0.0; m[2][0] = source->_02 / (float)(1 << W2V_SHIFT); m[2][1] = source->_12 / (float)(1 << W2V_SHIFT); m[2][2] = source->_22 / (float)(1 << W2V_SHIFT); m[2][3] = 0.0; m[3][0] = source->_03 / (float)(1 << W2V_SHIFT); m[3][1] = source->_13 / (float)(1 << W2V_SHIFT); m[3][2] = source->_23 / (float)(1 << W2V_SHIFT); m[3][3] = 1.0; } ================================================ FILE: src/trx/game/output/utils.h ================================================ #pragma once #include #include #define OUTPUT_QUAD_VERTICES 6 #define OUTPUT_TRI_VERTICES 3 #define OUTPUT_TRI_TO_FAN(i) ((int32_t[]) { 0, 2, 1 }[i]) // |\| #define OUTPUT_QUAD_TO_FAN(i) ((int32_t[]) { 0, 2, 1, 0, 3, 2 }[i]) // |\| Opposite winding #define OUTPUT_QUAD_TO_FAN_CW(i) ((int32_t[]) { 0, 1, 2, 0, 2, 3 }[i]) // |/| #define OUTPUT_QUAD_TO_FAN_BACK(i) ((int32_t[]) { 0, 3, 1, 1, 3, 2 }[i]) #define L_ATI_FIX 1 #if L_ATI_FIX // Evergreen‐generation GPUs (HD 5000/6000 – such as HD 5570 – have a // long-standing driver/firm-ware quirk: an integer vertex attribute whose // storage size is smaller than 32 bit is fetched as if it were 32-bit, // even when you declare it as 8- or 16-bit in glVertexAttribIPointer. The // call succeeds, no error is raised, but the hardware fetch unit still // steps four bytes, not one or two. If the stride you give is tighter than // that, the fetch unit starts reading the next vertex in the middle of the // current one and every attribute that follows is garbage – positions turn // into huge values, so the whole mesh explodes. NVIDIA, Intel and all // post-GCN AMD GPUs fixed the bug. // // To deal with this, we simply pack our data to increments of 4. #define OUTPUT_USHORT uint32_t #define OUTPUT_USHORT_GL GL_UNSIGNED_INT #define OUTPUT_SHORT int32_t #define OUTPUT_SHORT_GL GL_INT #else #define OUTPUT_SHORT int16_t #define OUTPUT_SHORT_GL GL_SHORT #define OUTPUT_USHORT uint16_t #define OUTPUT_USHORT_GL GL_UNSIGNED_SHORT #endif #undef L_ATI_FIX void Output_FillMatrix(GLfloat m[4][4], const MATRIX *source); ================================================ FILE: src/trx/game/output/vars.c ================================================ #include int32_t g_PhdPersp = 0; int32_t g_PhdLeft = 0; int32_t g_PhdBottom = 0; int32_t g_PhdRight = 0; int32_t g_PhdTop = 0; ================================================ FILE: src/trx/game/output/vars.h ================================================ #pragma once #include extern int32_t g_PhdPersp; extern int32_t g_PhdLeft; extern int32_t g_PhdBottom; extern int32_t g_PhdRight; extern int32_t g_PhdTop; ================================================ FILE: src/trx/game/output/vertex_range.c ================================================ #include #include #include static int M_CompareRanges(const void *const a, const void *const b) { const OUTPUT_VERTEX_RANGE *const range_a = (OUTPUT_VERTEX_RANGE *)a; const OUTPUT_VERTEX_RANGE *const range_b = (OUTPUT_VERTEX_RANGE *)b; return range_a->vertex_start - range_b->vertex_start; } void Output_GlueVertexRanges(VECTOR *const target) { ASSERT(target != nullptr); if (target->count == 0) { return; } OUTPUT_VERTEX_RANGE *const ranges = (OUTPUT_VERTEX_RANGE *)Vector_Get(target, 0); qsort(ranges, target->count, sizeof(OUTPUT_VERTEX_RANGE), M_CompareRanges); // Initialize a new index to store the merged ranges int32_t new_range_count = 0; // Iterate over sorted ranges and merge them for (int32_t i = 0; i < target->count; i++) { if (new_range_count == 0) { // First range - just copy it ranges[new_range_count] = ranges[i]; new_range_count++; } else { // Check if the previous range can be merged with the current one OUTPUT_VERTEX_RANGE *const last_range = &ranges[new_range_count - 1]; const int32_t last_start = last_range->vertex_start; const int32_t last_end = last_range->vertex_start + last_range->vertex_count; const int32_t current_start = ranges[i].vertex_start; const int32_t current_end = ranges[i].vertex_start + ranges[i].vertex_count; if (current_start >= last_start && current_start <= last_end) { last_range->vertex_count = current_end - last_range->vertex_start; } else if (current_end >= last_start && current_end <= last_end) { last_range->vertex_start = ranges[i].vertex_start; } else { ranges[new_range_count++] = ranges[i]; } } } // Update the range vertex_count with the new number of merged ranges target->count = new_range_count; } ================================================ FILE: src/trx/game/output/vertex_range.h ================================================ #pragma once #include typedef struct { int32_t vertex_start; int32_t vertex_count; } OUTPUT_VERTEX_RANGE; void Output_GlueVertexRanges(VECTOR *vertex_range); ================================================ FILE: src/trx/game/output.h ================================================ #pragma once #include #include #include #include #include #include #include #include #include #include #include ================================================ FILE: src/trx/game/overlay.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_MAX_PICKUP_DURATION_DISPLAY (LOGIC_FPS * 2) #define M_MAX_PICKUP_DURATION_EASE_IN (LOGIC_FPS / 2) #define M_MAX_PICKUP_DURATION_EASE_OUT LOGIC_FPS typedef enum { DPP_EASE_IN, DPP_DISPLAY, DPP_EASE_OUT, DPP_DEAD, } DISPLAY_PICKUP_PHASE; typedef struct { DISPLAY_PICKUP_PHASE phase; OBJECT_ID object_id; OUTPUT_UI_PICKUP display; int16_t start_rot; int32_t elapsed; int32_t total_elapsed; } DISPLAY_PICKUP; static UI_OVERLAY_STATE *m_UI = nullptr; static DISPLAY_PICKUP m_Pickups[OUTPUT_UI_MAX_PICKUPS] = {}; static bool m_PickupsActive; static const RGBA_F m_WhiteTextColor[4] = { { 1.0f, 1.0f, 1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f, 1.0f }, }; static const RGBA_F m_NeutralTextColor[4] = { { 1.0f, 1.0f, 1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f, 1.0f }, { 0.25f, 0.25f, 0.25f, 1.0f }, { 0.25f, 0.25f, 0.25f, 1.0f }, }; static const RGBA_F m_GreyTextColor[4] = { { 0.5f, 0.5f, 0.5f, 1.0f }, { 0.5f, 0.5f, 0.5f, 1.0f }, { 0.1f, 0.1f, 0.1f, 1.0f }, { 0.1f, 0.1f, 0.1f, 1.0f }, }; static const RGBA_F m_GreenTextColor[4] = { { 0.35f, 0.75f, 0.2f, 1.0f }, { 0.35f, 0.75f, 0.2f, 1.0f }, { 0.1f, 0.25f, 0.0f, 1.0f }, { 0.1f, 0.25f, 0.0f, 1.0f }, }; static const RGBA_F m_RedTextColor[4] = { { 0.9f, 0.2f, 0.0f, 1.0f }, { 0.9f, 0.2f, 0.0f, 1.0f }, { 0.3f, 0.0f, 0.0f, 1.0f }, { 0.3f, 0.0f, 0.0f, 1.0f }, }; static const RGBA_F m_PinkTextColor[4] = { { 1.0f, 0.0f, 1.0f, 1.0f }, { 1.0f, 0.0f, 1.0f, 1.0f }, { 0.25f, 0.0f, 0.25f, 1.0f }, { 0.25f, 0.0f, 0.25f, 1.0f }, }; static const char *M_FormatAssaultTimeText( const int32_t frames, const bool placeholder) { if (placeholder && frames <= 0) { return "--:--.-"; } const int32_t total_sec = frames / LOGIC_FPS; const int32_t frame = frames % LOGIC_FPS; return String_FormatStatic( "%d:%02d.%d", total_sec / 60, total_sec % 60, frame * 10 / LOGIC_FPS); } static int32_t M_DrawAssaultTimerText( const OBJECT *const digits_obj, const char *const text, int32_t x, const int32_t y, const RGBA_F color[4]) { const int32_t scale_h = UI_Scaler_Calc(PHD_ONE, UI_SCALER_TARGET_ASSAULT_DIGITS); const int32_t scale_v = UI_Scaler_Calc(PHD_ONE, UI_SCALER_TARGET_ASSAULT_DIGITS); const int32_t d0 = UI_Scaler_Calc(-6, UI_SCALER_TARGET_ASSAULT_DIGITS); const int32_t d1 = UI_Scaler_Calc(14, UI_SCALER_TARGET_ASSAULT_DIGITS); const int32_t d2 = UI_Scaler_Calc(20, UI_SCALER_TARGET_ASSAULT_DIGITS); for (const char *c = text; *c != '\0'; c++) { if (*c == '-') { x += d2; continue; } int32_t mesh_num = 0; int32_t offset = 0; int32_t width = 0; if (*c == ':') { mesh_num = 10; offset = d0; width = d1; } else if (*c == '.') { mesh_num = 11; offset = d0; width = d1; } else { mesh_num = *c - '0'; offset = 0; width = d2; } x += offset; UI_ScheduleDrawScreenSprite( x, y, 0, scale_h, scale_v, digits_obj->mesh_idx + mesh_num, color); x += width; } return x; } static int32_t M_MeasureAssaultTimerText(const char *const text) { const int32_t d0 = UI_Scaler_Calc(-6, UI_SCALER_TARGET_ASSAULT_DIGITS); const int32_t d1 = UI_Scaler_Calc(14, UI_SCALER_TARGET_ASSAULT_DIGITS); const int32_t d2 = UI_Scaler_Calc(20, UI_SCALER_TARGET_ASSAULT_DIGITS); int32_t w = 0; for (const char *c = text; *c != '\0'; c++) { if (*c == ':' || *c == '.') { w += d0 + d1; } else if (*c == '-') { w += d2; } else { w += d2; } } return w; } static bool M_IsSprite(const DISPLAY_PICKUP *const pickup) { return !g_Config.visuals.enable_3d_pickups || pickup->display.object == nullptr; } static float M_Ease(float current, const float start, const float goal) { if (start == goal) { return start; } else if (start > goal) { return 1.0f - M_Ease(current, goal, start); } else { CLAMP(current, start, goal); const float ratio = (current - start) / (goal - start); if (ratio < 0.5f) { return 2.0f * SQUARE(ratio); } const float new_ratio = ratio - 1.0f; return 1.0f - 2.0f * SQUARE(new_ratio); } } static void M_DrawTrackTimer(const GYM_TRACK_TYPE track_type) { if (!Gym_TrackManager_IsTimerDisplay(track_type)) { return; } const OBJECT *const digits_obj = Object_Get(O_ASSAULT_DIGITS); if (!digits_obj->loaded) { return; } const RESUME_INFO *const resume = Savegame_GetCurrentInfo(Game_GetCurrentLevel()); const char *const buffer = M_FormatAssaultTimeText(resume->stats.timer, false); const int32_t y = UI_Scaler_Calc(36, UI_SCALER_TARGET_ASSAULT_DIGITS); int32_t x = Viewport_GetCenterX(VIEWPORT_UI) - UI_Scaler_Calc(50, UI_SCALER_TARGET_ASSAULT_DIGITS); M_DrawAssaultTimerText( digits_obj, buffer, x, y, g_TRVersion < 3 ? m_WhiteTextColor : m_NeutralTextColor); } static void M_DrawAssaultPenalties( const GYM_TRACK_TYPE track_type, const bool is_target_penalty) { if (!Gym_TrackManager_IsTimerDisplay(track_type) || Gym_TrackManager_GetPenaltyDisplayTimer(track_type) <= 0) { return; } const OBJECT *const digits_obj = Object_Get(O_ASSAULT_DIGITS); if (!digits_obj->loaded) { return; } const int32_t timer = is_target_penalty ? Gym_TrackManager_GetTargetPenaltyFrames(track_type) : Gym_TrackManager_GetPenaltyFrames(track_type); if (timer <= 0) { return; } const int32_t total_sec = timer / LOGIC_FPS; const char *const fmt = is_target_penalty ? "T %d:%02d s" : "%d:%02d s"; const char *const buffer = String_FormatStatic(fmt, total_sec / 60, total_sec % 60); const int32_t scale_h = UI_Scaler_Calc(PHD_ONE, UI_SCALER_TARGET_ASSAULT_DIGITS); const int32_t scale_v = UI_Scaler_Calc(PHD_ONE, UI_SCALER_TARGET_ASSAULT_DIGITS); const int32_t p = UI_Scaler_Calc(1, UI_SCALER_TARGET_ASSAULT_DIGITS); const int32_t d0 = UI_Scaler_Calc(-6, UI_SCALER_TARGET_ASSAULT_DIGITS); const int32_t d1 = UI_Scaler_Calc(14, UI_SCALER_TARGET_ASSAULT_DIGITS); const int32_t d2 = UI_Scaler_Calc(20, UI_SCALER_TARGET_ASSAULT_DIGITS); const int32_t d3 = UI_Scaler_Calc(8, UI_SCALER_TARGET_ASSAULT_DIGITS); int32_t x = Viewport_GetCenterX(VIEWPORT_UI) - UI_Scaler_Calc( is_target_penalty ? 193 : 175, UI_SCALER_TARGET_ASSAULT_DIGITS); int32_t y = UI_Scaler_Calc(36, UI_SCALER_TARGET_ASSAULT_DIGITS); if (is_target_penalty && Gym_TrackManager_GetPenaltyFrames(track_type) != 0) { y = UI_Scaler_Calc(64, UI_SCALER_TARGET_ASSAULT_DIGITS); } for (const char *c = buffer; *c != '\0'; c++) { if (*c == ' ') { x += d3; } else if (*c == 'T') { x += d0; UI_ScheduleDrawScreenSprite( x, y + p, 0, scale_h, scale_v, digits_obj->mesh_idx + 12, g_TRVersion < 3 ? m_WhiteTextColor : m_NeutralTextColor); x += d1 + (p * 2); } else if (*c == 's') { x += d0; UI_ScheduleDrawScreenSprite( x - (p * 4), y, 0, scale_h, scale_v, digits_obj->mesh_idx + 13, m_PinkTextColor); x += d1; } else if (*c == ':') { x += d0; UI_ScheduleDrawScreenSprite( x, y, 0, scale_h, scale_v, digits_obj->mesh_idx + 10, m_PinkTextColor); x += d1; } else if (*c == '.') { x += d0; UI_ScheduleDrawScreenSprite( x, y, 0, scale_h, scale_v, digits_obj->mesh_idx + 11, m_PinkTextColor); x += d1; } else { UI_ScheduleDrawScreenSprite( x, y, 0, scale_h, scale_v, digits_obj->mesh_idx + (*c - '0'), m_PinkTextColor); x += d2; } } } static int32_t M_GetBestTrackTime(const GYM_TRACK_TYPE track_type) { const GYM_TRACK_STATS *const stats = Gym_TrackManager_GetStats(track_type); return stats->total_attempts > 0 ? (int32_t)stats->entries[0].time : 0; } static void M_DrawRacetrackLapTimes(const GYM_TRACK_TYPE track_type) { const int32_t last_lap_frames = Gym_TrackManager_GetLapTime(track_type); if (last_lap_frames <= 0) { return; } const OBJECT *const digits_obj = Object_Get(O_ASSAULT_DIGITS); if (!digits_obj->loaded) { return; } const int32_t best_lap_frames = M_GetBestTrackTime(track_type); const bool is_best_lap = best_lap_frames > 0 && last_lap_frames == best_lap_frames; const char *const last_text = M_FormatAssaultTimeText(last_lap_frames, true); const char *const best_text = best_lap_frames > 0 ? M_FormatAssaultTimeText(best_lap_frames, true) : ""; const int32_t w_last = M_MeasureAssaultTimerText(last_text); const int32_t w_best = M_MeasureAssaultTimerText(best_text); const int32_t gap = UI_Scaler_Calc(20, UI_SCALER_TARGET_ASSAULT_DIGITS); const int32_t cx = Viewport_GetCenterX(VIEWPORT_UI); const int32_t y = UI_Scaler_Calc(36, UI_SCALER_TARGET_ASSAULT_DIGITS); int32_t x = cx - w_last / 2; x = M_DrawAssaultTimerText( digits_obj, last_text, x, y, is_best_lap ? m_GreenTextColor : best_lap_frames > 0 ? m_RedTextColor : m_NeutralTextColor); x += gap; M_DrawAssaultTimerText( digits_obj, best_text, x, y, is_best_lap ? m_GreenTextColor : m_GreyTextColor); } static void M_DrawPickup2D(const DISPLAY_PICKUP *const pickup) { const VIEWPORT_RECT pickup_rect = OutputSource_UI_GetPickupRect(&pickup->display); const int16_t sprite_num = Object_Get(pickup->object_id)->mesh_idx; const SPRITE_TEXTURE *const sprite = Output_GetSpriteTexture(sprite_num); const float sprite_w = ABS(sprite->x1 - sprite->x0); const float sprite_h = ABS(sprite->y1 - sprite->y0); const float scale = MIN(pickup_rect.h / sprite_h, pickup_rect.w / sprite_w); const float scaled_sprite_w = sprite_w * scale; const float scaled_sprite_h = sprite_h * scale; const float x = pickup_rect.x + (pickup_rect.w - scaled_sprite_w) / 2; const float y = pickup_rect.y + (pickup_rect.h - scaled_sprite_h) / 2; OutputSource_UI_StageSprite((OUTPUT_UI_SPRITE) { .sprite_idx = sprite_num, .x0 = x, .y0 = y, .x1 = x + (sprite->x1 - sprite->x0) * scale, .y1 = y + (sprite->y1 - sprite->y0) * scale, .z = Output_GetNearZ_UI(), .shade = SHADE_NEUTRAL, .color = { m_WhiteTextColor[0], m_WhiteTextColor[1], m_WhiteTextColor[2], m_WhiteTextColor[3], }, }); } static void M_DrawPickup3D(const DISPLAY_PICKUP *const pickup) { OutputSource_UI_StagePickup(pickup->display); } static void M_DrawPickups(void) { for (int32_t i = 0; i < OUTPUT_UI_MAX_PICKUPS; i++) { DISPLAY_PICKUP *const pickup = &m_Pickups[i]; int32_t duration = 0; float slide_start = 0.0f; float slide_goal = 0.0f; switch (pickup->phase) { case DPP_DEAD: continue; case DPP_EASE_IN: duration = M_MAX_PICKUP_DURATION_EASE_IN; slide_start = 0.0f; slide_goal = 1.0f; break; case DPP_DISPLAY: duration = M_MAX_PICKUP_DURATION_DISPLAY; slide_start = 1.0f; slide_goal = 1.0f; break; case DPP_EASE_OUT: duration = M_MAX_PICKUP_DURATION_EASE_OUT; slide_start = 1.0f; slide_goal = 0.0f; break; } if (M_IsSprite(pickup)) { pickup->display.ease = 1.0f; } else { const float rate = Interpolation_GetRate(); pickup->display.rot_y = pickup->start_rot + (4 * DEG_1 * (pickup->total_elapsed + rate)); pickup->display.ease = M_Ease( (pickup->elapsed + rate) / (float)duration, slide_start, slide_goal); } if (M_IsSprite(pickup)) { M_DrawPickup2D(pickup); } else { M_DrawPickup3D(pickup); } } } static void M_AnimatePickups(const int32_t frames) { m_PickupsActive = false; for (int32_t i = 0; i < OUTPUT_UI_MAX_PICKUPS; i++) { DISPLAY_PICKUP *const pickup = &m_Pickups[i]; pickup->elapsed += frames; pickup->total_elapsed += frames; switch (pickup->phase) { case DPP_EASE_IN: if (pickup->elapsed >= M_MAX_PICKUP_DURATION_EASE_IN) { pickup->elapsed = 0; pickup->phase = DPP_DISPLAY; } m_PickupsActive = true; break; case DPP_DISPLAY: if (pickup->elapsed >= M_MAX_PICKUP_DURATION_DISPLAY) { pickup->elapsed = 0; pickup->phase = DPP_EASE_OUT; } m_PickupsActive = true; break; case DPP_EASE_OUT: if (pickup->elapsed >= M_MAX_PICKUP_DURATION_EASE_OUT) { pickup->elapsed = 0; pickup->phase = DPP_DEAD; } else { m_PickupsActive = true; } break; case DPP_DEAD: continue; } } } void Overlay_Init(void) { if (m_UI == nullptr) { m_UI = UI_Overlay_Init(); } } void Overlay_Shutdown(void) { if (m_UI != nullptr) { UI_Overlay_Free(m_UI); m_UI = nullptr; } } void Overlay_Reset(void) { for (int32_t i = 0; i < OUTPUT_UI_MAX_PICKUPS; i++) { m_Pickups[i].phase = DPP_DEAD; } } void Overlay_Control(void) { if (m_UI != nullptr) { UI_Overlay_Control(m_UI); } } void Overlay_Animate(int32_t frames) { if (Game_IsPlaying()) { M_AnimatePickups(frames); } } void Overlay_Draw(void) { if (m_UI != nullptr) { UI_Overlay(m_UI); } } void Overlay_DrawGameInfo(void) { if (!Game_IsPlaying()) { return; } if (g_Config.ui.show_pickups_overlay && m_PickupsActive) { SceneCompositor_Flush(); const int32_t old_fog_start = Output_GetFogStart(); const int32_t old_fog_end = Output_GetFogEnd(); Output_SetFogStart(20 * WALL_L); Output_SetFogEnd(100 * WALL_L); M_DrawPickups(); SceneCompositor_Flush(); Output_SetFogStart(old_fog_start); Output_SetFogEnd(old_fog_end); } if (Gym_TrackManager_GetLapTimeDisplayTimer(GYM_TRACK_QUAD) > 0) { M_DrawRacetrackLapTimes(GYM_TRACK_QUAD); } else { const GYM_TRACK_TYPE track_type = Gym_TrackManager_GetActiveTrackType(); if (track_type == GYM_TRACK_NONE) { return; } M_DrawTrackTimer(track_type); M_DrawAssaultPenalties(track_type, false); M_DrawAssaultPenalties(track_type, true); } } void Overlay_ForceHealthBar(const bool show) { UI_Overlay_ForceHealthBar(m_UI, show); } void Overlay_SetHealthBarTimer(const int16_t timer) { UI_LaraHealthBar_SetTimer(timer); } void Overlay_ShowArrow(const UI_OVERLAY_ARROW arrow, const bool show) { if (m_UI != nullptr) { UI_Overlay_ShowArrow(m_UI, arrow, show); } } void Overlay_ShowVersion(const bool show) { if (m_UI != nullptr) { UI_Overlay_ShowVersion(m_UI, show); } } void Overlay_SetTopText(const OVERLAY_TEXT text) { if (m_UI != nullptr) { UI_Overlay_SetTopText(m_UI, text); } } void Overlay_SetBottomText(const OVERLAY_TEXT text) { if (m_UI != nullptr) { UI_Overlay_SetBottomText(m_UI, text); } } void Overlay_AddDisplayPickup(const OBJECT_ID obj_id) { if (Object_IsType(obj_id, g_SecretObjects)) { const MUSIC_PLAY_MODE mode = g_Config.audio.fix_secrets_killing_music ? MPM_OVERLAY : MPM_ONCE; Music_Play(MX_SECRET, mode); } int32_t grid_x = -1; int32_t grid_y = -1; for (int32_t i = 0; i < OUTPUT_UI_MAX_PICKUPS; i++) { const int32_t x = i % OUTPUT_UI_MAX_PICKUP_COLUMNS; const int32_t y = i / OUTPUT_UI_MAX_PICKUP_COLUMNS; bool is_occupied = false; for (int32_t j = 0; j < OUTPUT_UI_MAX_PICKUPS; j++) { DISPLAY_PICKUP *const pickup = &m_Pickups[j]; const bool is_dead_or_dying = pickup->phase == DPP_DEAD || (!M_IsSprite(pickup) && pickup->phase == DPP_EASE_OUT); if (pickup->display.grid_x == x && pickup->display.grid_y == y && !is_dead_or_dying) { is_occupied = true; break; } } if (!is_occupied) { grid_x = x; grid_y = y; break; } } for (int32_t i = 0; i < OUTPUT_UI_MAX_PICKUPS; i++) { DISPLAY_PICKUP *const pickup = &m_Pickups[i]; if (pickup->phase != DPP_DEAD) { continue; } const OBJECT_ID inv_object_id = Inv_GetItemOption(obj_id); const INVENTORY_ITEM *const inv_item = InvRing_GetInvItem(obj_id); pickup->phase = DPP_EASE_IN; pickup->object_id = obj_id; pickup->display.object = inv_object_id != NO_OBJECT ? Object_Get(inv_object_id) : nullptr; pickup->display.grid_x = grid_x; pickup->display.grid_y = grid_y; pickup->start_rot = inv_item != nullptr ? inv_item->y_rot_sel : 0; pickup->elapsed = 0; pickup->total_elapsed = 0; return; } } ================================================ FILE: src/trx/game/overlay.h ================================================ #pragma once #include #include typedef UI_OVERLAY_TEXT OVERLAY_TEXT; void Overlay_Init(void); void Overlay_Shutdown(void); void Overlay_Reset(void); void Overlay_Control(void); void Overlay_Animate(int32_t num_frames); void Overlay_Draw(void); void Overlay_DrawGameInfo(void); void Overlay_AddDisplayPickup(OBJECT_ID obj_id); void Overlay_ForceHealthBar(bool show); void Overlay_SetHealthBarTimer(int16_t health_bar_timer); void Overlay_ShowArrow(UI_OVERLAY_ARROW arrow, bool show); void Overlay_ShowVersion(bool show); void Overlay_SetTopText(OVERLAY_TEXT text); void Overlay_SetBottomText(OVERLAY_TEXT text); ================================================ FILE: src/trx/game/pathing/box.c ================================================ #include #include #include #include #include #include #include #include #include #define BOX_OVERLAP_BITS 0x3FFF #define BOX_SEARCH_NUMBER 0x7FFF #define BOX_END_BIT 0x8000 #define BOX_NUMBER_BITS 0x7FFF // = ~BOX_END_BIT #define BOX_MAX_EXPANSION 5 #define BOX_BIFF (WALL_L / 2) // = 0x200 = 512 #define BOX_CLIP_LEFT 1 #define BOX_CLIP_RIGHT 2 #define BOX_CLIP_TOP 4 #define BOX_CLIP_BOTTOM 8 #define BOX_CLIP_ALL \ (BOX_CLIP_LEFT | BOX_CLIP_RIGHT | BOX_CLIP_TOP | BOX_CLIP_BOTTOM) // = 15 #define BOX_CLIP_SECONDARY 16 static int32_t m_BoxCount = 0; static BOX_INFO *m_Boxes = nullptr; static int16_t *m_Overlaps = nullptr; static int16_t *m_FlyZone[2] = {}; static int16_t *m_GroundZone[MAX_ZONES][2] = {}; void Box_InitialiseBoxes(const int32_t num_boxes) { m_BoxCount = num_boxes; m_Boxes = num_boxes == 0 ? nullptr : GameBuf_Alloc(sizeof(BOX_INFO) * num_boxes, GBUF_BOXES); if (num_boxes == 0) { return; } for (int32_t i = 0; i < 2; i++) { for (int32_t j = 0; j < MAX_ZONES; j++) { m_GroundZone[j][i] = GameBuf_Alloc(sizeof(int16_t) * num_boxes, GBUF_GROUND_ZONE); } m_FlyZone[i] = GameBuf_Alloc(sizeof(int16_t) * num_boxes, GBUF_FLY_ZONE); } } int16_t *Box_InitialiseOverlaps(const int32_t num_overlaps) { m_Overlaps = num_overlaps == 0 ? nullptr : GameBuf_Alloc(sizeof(int16_t) * num_overlaps, GBUF_OVERLAPS); return m_Overlaps; } int32_t Box_GetCount(void) { return m_BoxCount; } BOX_INFO *Box_GetBox(const int32_t box_idx) { // TODO: in many cases, NO_BOX is blindly passed here and goes unchecked. // Update each instance to handle NO_BOX safely. return m_Boxes == nullptr ? nullptr : &m_Boxes[box_idx]; } int16_t Box_GetOverlap(const int32_t overlap_idx) { return m_Overlaps == nullptr ? -1 : m_Overlaps[overlap_idx]; } int16_t *Box_GetFlyZone(const bool flip_status) { return m_FlyZone[flip_status]; } int16_t *Box_GetGroundZone(const bool flip_status, const int32_t zone_idx) { return m_GroundZone[zone_idx][flip_status]; } int16_t *Box_GetLotZone(const LOT_INFO *const lot) { const bool flip_status = Room_GetFlipStatus(); return lot->setup.fly != 0 ? Box_GetFlyZone(flip_status) : Box_GetGroundZone(flip_status, BOX_ZONE(lot->setup.step)); } bool Box_SearchLOT(LOT_INFO *const lot, const int32_t expansion) { const int16_t *const zone = Box_GetLotZone(lot); const bool use_fixed_fly_zone = g_TRVersion == 3 && lot->setup.fly != 0; const int16_t search_zone = use_fixed_fly_zone ? BOX_FIXED_FLY_ZONE : zone[lot->head]; for (int32_t i = 0; i < expansion; i++) { if (lot->head == NO_BOX) { if (g_TRVersion >= 2) { lot->tail = NO_BOX; } return false; } BOX_NODE *const node = &lot->node[lot->head]; const BOX_INFO *const head_box = Box_GetBox(lot->head); bool done = false; int32_t index = head_box->overlap_index & BOX_OVERLAP_BITS; while (!done) { int16_t box_num = Box_GetOverlap(index++); if ((box_num & BOX_END_BIT) != 0) { done = true; box_num &= BOX_NUMBER_BITS; } if (!use_fixed_fly_zone && search_zone != zone[box_num]) { continue; } const BOX_INFO *const box = Box_GetBox(box_num); const int32_t change = box->height - head_box->height; if (change > lot->setup.step || change < lot->setup.drop) { continue; } BOX_NODE *const expand = &lot->node[box_num]; const int16_t node_search_num = node->search_num & BOX_SEARCH_NUMBER; const int16_t expand_search_num = expand->search_num & BOX_SEARCH_NUMBER; const bool node_search_blocked = (node->search_num & BOX_BLOCKED_SEARCH) != 0; const bool expand_search_blocked = (expand->search_num & BOX_BLOCKED_SEARCH) != 0; if (node_search_num < expand_search_num) { continue; } if (node_search_blocked) { if (expand_search_num == node_search_num) { continue; } expand->search_num = node->search_num; } else { if (expand_search_num == node_search_num && !expand_search_blocked) { continue; } if ((box->overlap_index & lot->setup.block_mask) != 0) { expand->search_num = node->search_num | BOX_BLOCKED_SEARCH; } else { expand->search_num = node->search_num; expand->exit_box = lot->head; } } if (expand->next_expansion == NO_BOX && box_num != lot->tail) { lot->node[lot->tail].next_expansion = box_num; lot->tail = box_num; } } lot->head = node->next_expansion; node->next_expansion = NO_BOX; } return true; } bool Box_UpdateLOT(LOT_INFO *const lot, const int32_t expansion) { if (lot->required_box == NO_BOX || lot->required_box == lot->target_box) { goto end; } lot->target_box = lot->required_box; BOX_NODE *const expand = &lot->node[lot->target_box]; if (expand->next_expansion == NO_BOX && lot->tail != lot->target_box) { expand->next_expansion = lot->head; if (lot->head == NO_BOX) { lot->tail = lot->target_box; } lot->head = lot->target_box; } lot->search_num++; expand->search_num = lot->search_num; expand->exit_box = NO_BOX; end: return Box_SearchLOT(lot, expansion); } void Box_TargetBox(LOT_INFO *const lot, int16_t box_num) { box_num &= BOX_NUMBER_BITS; const BOX_INFO *const box = Box_GetBox(box_num); // TODO: determine if the shift is essential const int32_t shift = g_TRVersion >= 2 ? 1 : 0; lot->target.z = box->left + WALL_L / 2 + (Random_GetControl() * (box->right + shift - box->left - WALL_L) >> 15); lot->target.x = box->top + WALL_L / 2 + (Random_GetControl() * (box->bottom + shift - box->top - WALL_L) >> 15); lot->required_box = box_num; if (lot->setup.fly != 0) { lot->target.y = box->height - STEP_L * 3 / 2; } else { lot->target.y = box->height; } } bool Box_StalkBox( const ITEM *const item, const ITEM *const enemy, const int16_t box_num) { if (enemy == nullptr) { return false; } const BOX_INFO *const box = Box_GetBox(box_num); // TODO: determine if the shift is essential const int32_t shift = g_TRVersion >= 2 ? 1 : 0; const int32_t z = ((box->left + box->right + shift) >> 1) - enemy->pos.z; const int32_t x = ((box->top + box->bottom + shift) >> 1) - enemy->pos.x; const int32_t x_range = g_TRVersion >= 2 ? box->bottom + shift - box->top + CREATURE_STALK_DIST : CREATURE_STALK_DIST; const int32_t z_range = g_TRVersion >= 2 ? box->right + shift - box->left + CREATURE_STALK_DIST : CREATURE_STALK_DIST; if (x > x_range || x < -x_range || z > z_range || z < -z_range) { return false; } const int32_t enemy_quad = (enemy->rot.y >> 14) + 2; const int32_t box_quad = (z > 0) ? ((x > 0) ? DIR_SOUTH : DIR_EAST) : ((x > 0) ? DIR_WEST : DIR_NORTH); if (enemy_quad == box_quad) { return false; } const int32_t baddie_quad = item->pos.z > enemy->pos.z ? (item->pos.x > enemy->pos.x ? DIR_SOUTH : DIR_EAST) : (item->pos.x > enemy->pos.x ? DIR_WEST : DIR_NORTH); return enemy_quad != baddie_quad || ABS(enemy_quad - box_quad) != 2; } bool Box_EscapeBox( const ITEM *item, const ITEM *const enemy, const int16_t box_num) { const BOX_INFO *const box = Box_GetBox(box_num); // TODO: determine if the shift is essential const int32_t shift = g_TRVersion >= 2 ? 1 : 0; const int32_t x = ((box->top + box->bottom + shift) >> 1) - enemy->pos.x; const int32_t z = ((box->left + box->right + shift) >> 1) - enemy->pos.z; if (x > -CREATURE_ESCAPE_DIST && x < CREATURE_ESCAPE_DIST && z > -CREATURE_ESCAPE_DIST && z < CREATURE_ESCAPE_DIST) { return false; } return ((z > 0) == (item->pos.z > enemy->pos.z)) || ((x > 0) == (item->pos.x > enemy->pos.x)); } bool Box_ValidBox( const ITEM *item, const int16_t zone_num, const int16_t box_num) { const CREATURE *const creature = item->creature_data; const int16_t *const zone = Box_GetLotZone(&creature->lot); const bool use_fixed_fly_zone = g_TRVersion == 3 && creature->lot.setup.fly != 0; if (!use_fixed_fly_zone && zone[box_num] != zone_num) { return false; } const BOX_INFO *const box = Box_GetBox(box_num); if ((box->overlap_index & creature->lot.setup.block_mask) != 0) { return false; } // TODO: determine if the shift is essential const int32_t shift = g_TRVersion >= 2 ? 1 : 0; return !( item->pos.z > box->left && item->pos.z < box->right + shift && item->pos.x > box->top && item->pos.x < box->bottom + shift); } TARGET_TYPE Box_CalculateTarget( XYZ_32 *const target, const ITEM *const item, LOT_INFO *const lot) { Box_UpdateLOT(lot, BOX_MAX_EXPANSION); *target = item->pos; int32_t box_num = item->box_num; if (box_num == NO_BOX) { return TARGET_NONE; } int32_t bottom = 0; int32_t top = 0; int32_t right = 0; int32_t left = 0; const BOX_INFO *box = nullptr; int32_t prime_free = BOX_CLIP_ALL; do { box = Box_GetBox(box_num); if (lot->setup.fly != 0) { CLAMPG(target->y, box->height - WALL_L); } else { CLAMPG(target->y, box->height); } if (item->pos.z >= box->left && item->pos.z <= box->right && item->pos.x >= box->top && item->pos.x <= box->bottom) { left = box->left; right = box->right; top = box->top; bottom = box->bottom; } else { if (item->pos.z < box->left) { if ((prime_free & BOX_CLIP_LEFT) != 0 && item->pos.x >= box->top && item->pos.x <= box->bottom) { CLAMPL(target->z, box->left + BOX_BIFF); if ((prime_free & BOX_CLIP_SECONDARY) != 0) { return TARGET_SECONDARY; } CLAMPL(top, box->top); CLAMPG(bottom, box->bottom); prime_free = BOX_CLIP_LEFT; } else if (prime_free != BOX_CLIP_LEFT) { target->z = right - BOX_BIFF; if (prime_free != BOX_CLIP_ALL) { return TARGET_SECONDARY; } prime_free |= BOX_CLIP_SECONDARY; } } else if (item->pos.z > box->right) { if ((prime_free & BOX_CLIP_RIGHT) != 0 && item->pos.x >= box->top && item->pos.x <= box->bottom) { CLAMPG(target->z, box->right - BOX_BIFF); if ((prime_free & BOX_CLIP_SECONDARY) != 0) { return TARGET_SECONDARY; } CLAMPL(top, box->top); CLAMPG(bottom, box->bottom); prime_free = BOX_CLIP_RIGHT; } else if (prime_free != BOX_CLIP_RIGHT) { target->z = left + BOX_BIFF; if (prime_free != BOX_CLIP_ALL) { return TARGET_SECONDARY; } prime_free |= BOX_CLIP_SECONDARY; } } if (item->pos.x < box->top) { if ((prime_free & BOX_CLIP_TOP) != 0 && item->pos.z >= box->left && item->pos.z <= box->right) { CLAMPL(target->x, box->top + BOX_BIFF); if ((prime_free & BOX_CLIP_SECONDARY) != 0) { return TARGET_SECONDARY; } CLAMPL(left, box->left); CLAMPG(right, box->right); prime_free = BOX_CLIP_TOP; } else if (prime_free != BOX_CLIP_TOP) { target->x = bottom - BOX_BIFF; if (prime_free != BOX_CLIP_ALL) { return TARGET_SECONDARY; } prime_free |= BOX_CLIP_SECONDARY; } } else if (item->pos.x > box->bottom) { if ((prime_free & BOX_CLIP_BOTTOM) != 0 && item->pos.z >= box->left && item->pos.z <= box->right) { CLAMPG(target->x, box->bottom - BOX_BIFF); if ((prime_free & BOX_CLIP_SECONDARY) != 0) { return TARGET_SECONDARY; } CLAMPL(left, box->left); CLAMPG(right, box->right); prime_free = BOX_CLIP_BOTTOM; } else if (prime_free != BOX_CLIP_BOTTOM) { target->x = top + BOX_BIFF; if (prime_free != BOX_CLIP_ALL) { return TARGET_SECONDARY; } prime_free |= BOX_CLIP_SECONDARY; } } } if (box_num == lot->target_box) { if ((prime_free & (BOX_CLIP_LEFT | BOX_CLIP_RIGHT)) != 0) { target->z = lot->target.z; } else if ((prime_free & BOX_CLIP_SECONDARY) == 0) { CLAMP(target->z, box->left + BOX_BIFF, box->right - BOX_BIFF); } if ((prime_free & (BOX_CLIP_TOP | BOX_CLIP_BOTTOM)) != 0) { target->x = lot->target.x; } else if ((prime_free & BOX_CLIP_SECONDARY) == 0) { CLAMP(target->x, box->top + BOX_BIFF, box->bottom - BOX_BIFF); } target->y = lot->target.y; return TARGET_PRIMARY; } box_num = lot->node[box_num].exit_box; if (box_num != NO_BOX && (Box_GetBox(box_num)->overlap_index & lot->setup.block_mask) != 0) { break; } } while (box_num != NO_BOX); if ((prime_free & (BOX_CLIP_LEFT | BOX_CLIP_RIGHT)) != 0) { target->z = box->left + WALL_L / 2 + (((box->right - box->left - WALL_L) * Random_GetControl()) >> 15); } else if ((prime_free & BOX_CLIP_SECONDARY) == 0) { CLAMP(target->z, box->left + BOX_BIFF, box->right - BOX_BIFF); } if ((prime_free & (BOX_CLIP_TOP | BOX_CLIP_BOTTOM)) != 0) { target->x = box->top + WALL_L / 2 + (((box->bottom - box->top - WALL_L) * Random_GetControl()) >> 15); } else if ((prime_free & BOX_CLIP_SECONDARY) == 0) { CLAMP(target->x, box->top + BOX_BIFF, box->bottom - BOX_BIFF); } if (lot->setup.fly != 0) { target->y = box->height - STEP_L * 3 / 2; } else { target->y = box->height; } return TARGET_NONE; } bool Box_BadFloor( const int32_t x, const int32_t y, const int32_t z, const int32_t box_height, const int32_t next_height, int16_t room_num, const LOT_INFO *const lot) { const SECTOR *const sector = Room_GetSector((XYZ_32) { x, y, z }, &room_num); if (sector->box == NO_BOX) { return true; } const BOX_INFO *const box = Box_GetBox(sector->box); if ((box->overlap_index & lot->setup.block_mask) != 0) { return true; } const int32_t height = box->height; if (box_height - height > lot->setup.step || box_height - height < lot->setup.drop) { return true; } if (box_height - height < -lot->setup.step && height > next_height) { return true; } if (lot->setup.fly != 0 && y > height + lot->setup.fly) { return true; } return false; } int32_t Box_GetZoneCount(void) { return g_TRVersion == 1 ? 2 : 4; } ================================================ FILE: src/trx/game/pathing/box.h ================================================ #pragma once #include #include #include void Box_InitialiseBoxes(int32_t num_boxes); int16_t *Box_InitialiseOverlaps(int32_t num_overlaps); int32_t Box_GetCount(void); BOX_INFO *Box_GetBox(int32_t box_idx); int16_t Box_GetOverlap(int32_t overlap_idx); int16_t *Box_GetFlyZone(bool flip_status); int16_t *Box_GetGroundZone(bool flip_status, int32_t zone_idx); int16_t *Box_GetLotZone(const LOT_INFO *lot); int16_t AIGuard(CREATURE *creature); void GetAITarget(CREATURE *creature); bool Box_SearchLOT(LOT_INFO *lot, int32_t expansion); bool Box_UpdateLOT(LOT_INFO *lot, int32_t expansion); void Box_TargetBox(LOT_INFO *lot, int16_t box_num); bool Box_StalkBox(const ITEM *item, const ITEM *enemy, int16_t box_num); bool Box_EscapeBox(const ITEM *item, const ITEM *enemy, int16_t box_num); bool Box_ValidBox(const ITEM *item, int16_t zone_num, int16_t box_num); TARGET_TYPE Box_CalculateTarget( XYZ_32 *target, const ITEM *item, LOT_INFO *lot); bool Box_BadFloor( int32_t x, int32_t y, int32_t z, int32_t box_height, int32_t next_height, int16_t room_num, const LOT_INFO *lot); int32_t Box_GetZoneCount(void); ================================================ FILE: src/trx/game/pathing/const.h ================================================ #pragma once #include #include #define NO_BOX (-1) #define BOX_ZONE(num) (((num) / STEP_L) - 1) #define LOT_SLOT_COUNT 32 #define MAX_ZONES 4 #define BOX_BLOCKED 0x4000 #define BOX_BLOCKED_SEARCH 0x8000 #define BOX_BLOCKABLE 0x8000 #define BOX_FIXED_FLY_ZONE 0x2000 ================================================ FILE: src/trx/game/pathing/lot.c ================================================ #include #include #include #include #include #include #include #include static int32_t m_SlotsUsed = 0; static CREATURE *m_BaddieSlots = nullptr; LOT_SETUP LOT_Setup(const LOT_SETUP_TYPE type) { switch (type) { case LOT_SETUP_DEFAULT: return (LOT_SETUP) { .step = STEP_L, .drop = g_TRVersion == 1 ? -STEP_L : -STEP_L * 2, .fly = 0, .block_mask = BOX_BLOCKED, }; case LOT_SETUP_BEAST: return (LOT_SETUP) { .step = STEP_L, .drop = g_TRVersion == 1 ? -STEP_L : -STEP_L * 2, .fly = 0, .block_mask = BOX_BLOCKABLE, }; case LOT_SETUP_QUADRUPED: return (LOT_SETUP) { .step = STEP_L, .drop = -WALL_L, .fly = 0, .block_mask = BOX_BLOCKED, }; case LOT_SETUP_JUMPER: return (LOT_SETUP) { .step = WALL_L / 2, .drop = -WALL_L, .fly = 0, .block_mask = BOX_BLOCKED, }; case LOT_SETUP_CLIMBER: return (LOT_SETUP) { .step = WALL_L, .drop = -WALL_L, .fly = 0, .block_mask = BOX_BLOCKED, }; case LOT_SETUP_FLYER: return (LOT_SETUP) { .step = WALL_L * 20, .drop = -WALL_L * 20, .fly = STEP_L / 16, .block_mask = BOX_BLOCKED, }; } ASSERT_FAIL(); return (LOT_SETUP) {}; } void LOT_InitialiseArray(void) { m_BaddieSlots = GameBuf_Alloc(LOT_SLOT_COUNT * sizeof(CREATURE), GBUF_CREATURE_DATA); for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) { CREATURE *const creature = &m_BaddieSlots[i]; creature->item_num = NO_ITEM; creature->lot.node = GameBuf_Alloc(Box_GetCount() * sizeof(BOX_NODE), GBUF_CREATURE_LOT); } m_SlotsUsed = 0; } CREATURE *LOT_GetBaddieSlot(const int32_t i) { return &m_BaddieSlots[i]; } void LOT_DisableBaddieAI(const int16_t item_num) { ITEM *const item = Item_Get(item_num); CREATURE *const creature = item->creature_data; item->creature_data = nullptr; item->extra_rotations = nullptr; if (creature != nullptr) { creature->item_num = NO_ITEM; m_SlotsUsed--; } } bool LOT_EnableBaddieAI(const int16_t item_num, const bool always) { if (Item_Get(item_num)->creature_data != nullptr) { return true; } if (m_SlotsUsed < LOT_SLOT_COUNT) { for (int32_t slot = 0; slot < LOT_SLOT_COUNT; slot++) { if (m_BaddieSlots[slot].item_num == NO_ITEM) { LOT_InitialiseSlot(item_num, slot); return true; } } ASSERT_FAIL(); } int32_t worst_dist = 0; if (!always) { const ITEM *const item = Item_Get(item_num); const int32_t dx = (item->pos.x - g_Camera.pos.pos.x) >> 8; const int32_t dy = (item->pos.y - g_Camera.pos.pos.y) >> 8; const int32_t dz = (item->pos.z - g_Camera.pos.pos.z) >> 8; worst_dist = SQUARE(dx) + SQUARE(dy) + SQUARE(dz); } int32_t worst_slot = -1; for (int32_t slot = 0; slot < LOT_SLOT_COUNT; slot++) { const ITEM *const item = Item_Get(m_BaddieSlots[slot].item_num); const int32_t dx = (item->pos.x - g_Camera.pos.pos.x) >> 8; const int32_t dy = (item->pos.y - g_Camera.pos.pos.y) >> 8; const int32_t dz = (item->pos.z - g_Camera.pos.pos.z) >> 8; const int32_t dist = SQUARE(dx) + SQUARE(dy) + SQUARE(dz); if (dist > worst_dist) { worst_dist = dist; worst_slot = slot; } } if (worst_slot < 0) { return false; } const CREATURE *const creature = &m_BaddieSlots[worst_slot]; Item_Get(creature->item_num)->status = IS_INVISIBLE; LOT_DisableBaddieAI(creature->item_num); LOT_InitialiseSlot(item_num, worst_slot); return true; } void LOT_InitialiseSlot(const int16_t item_num, const int32_t slot) { CREATURE *const creature = &m_BaddieSlots[slot]; ITEM *const item = Item_Get(item_num); item->creature_data = creature; item->extra_rotations = creature->joint_rotation; creature->item_num = item_num; creature->mood = MOOD_BORED; creature->neck_rotation = 0; creature->head_rotation = 0; creature->joint_rotation[0] = 0; creature->joint_rotation[1] = 0; creature->joint_rotation[2] = 0; creature->joint_rotation[3] = 0; creature->maximum_turn = DEG_1; creature->flags = 0; creature->enemy = nullptr; creature->head_left = false; creature->head_right = false; creature->reached_goal = false; creature->hurt_by_lara = false; creature->patrol_2 = false; creature->alerted = false; const OBJECT *const obj = Object_Get(item->object_id); creature->lot.setup = obj->lot_setup; LOT_ClearLOT(&creature->lot); LOT_CreateZone(item); m_SlotsUsed++; } void LOT_CreateZone(ITEM *const item) { CREATURE *const creature = item->creature_data; const int16_t *zone; const int16_t *flip; if (creature->lot.setup.fly) { zone = Box_GetFlyZone(false); flip = Box_GetFlyZone(true); } else { zone = Box_GetGroundZone(false, BOX_ZONE(creature->lot.setup.step)); flip = Box_GetGroundZone(true, BOX_ZONE(creature->lot.setup.step)); } const ROOM *const room = Room_Get(item->room_num); item->box_num = Room_GetWorldSector(room, item->pos.x, item->pos.z)->box; int16_t zone_num = zone[item->box_num]; int16_t flip_num = flip[item->box_num]; const bool use_fixed_fly_zone = g_TRVersion == 3 && creature->lot.setup.fly != 0; creature->lot.zone_count = 0; BOX_NODE *node = creature->lot.node; for (int32_t i = 0; i < Box_GetCount(); i++) { if (use_fixed_fly_zone || zone[i] == zone_num || flip[i] == flip_num) { node->box_num = i; node++; creature->lot.zone_count++; } } } void LOT_InitialiseLOT(LOT_INFO *const lot) { lot->node = GameBuf_Alloc(sizeof(BOX_NODE) * Box_GetCount(), GBUF_CREATURE_LOT); LOT_ClearLOT(lot); } void LOT_ClearLOT(LOT_INFO *const lot) { lot->search_num = 0; lot->head = NO_BOX; lot->tail = NO_BOX; lot->target_box = NO_BOX; lot->required_box = NO_BOX; for (int32_t i = 0; i < Box_GetCount(); i++) { BOX_NODE *const node = &lot->node[i]; node->next_expansion = NO_BOX; node->exit_box = NO_BOX; node->search_num = 0; } } ================================================ FILE: src/trx/game/pathing/lot.h ================================================ #pragma once #include LOT_SETUP LOT_Setup(LOT_SETUP_TYPE type); void LOT_InitialiseArray(void); void LOT_InitialiseSlot(int16_t item_num, int32_t slot); void LOT_CreateZone(ITEM *item); void LOT_InitialiseLOT(LOT_INFO *LOT); bool LOT_EnableBaddieAI(int16_t item_num, bool always); void LOT_DisableBaddieAI(int16_t item_num); CREATURE *LOT_GetBaddieSlot(int32_t i); void LOT_ClearLOT(LOT_INFO *LOT); ================================================ FILE: src/trx/game/pathing/types.h ================================================ #pragma once #include typedef struct { int32_t left; int32_t right; int32_t top; int32_t bottom; int16_t height; int16_t overlap_index; } BOX_INFO; typedef struct { int16_t exit_box; uint16_t search_num; int16_t next_expansion; int16_t box_num; } BOX_NODE; typedef enum { LOT_SETUP_DEFAULT, LOT_SETUP_BEAST, LOT_SETUP_QUADRUPED, LOT_SETUP_JUMPER, LOT_SETUP_CLIMBER, LOT_SETUP_FLYER, } LOT_SETUP_TYPE; typedef struct { int16_t step; int16_t drop; int16_t fly; uint16_t block_mask; } LOT_SETUP; typedef struct { LOT_SETUP setup; BOX_NODE *node; int16_t head; int16_t tail; uint16_t search_num; int16_t zone_count; int16_t target_box; int16_t required_box; XYZ_32 target; } LOT_INFO; typedef enum { TARGET_NONE = 0, TARGET_PRIMARY = 1, TARGET_SECONDARY = 2, } TARGET_TYPE; ================================================ FILE: src/trx/game/pathing.h ================================================ #pragma once #include #include #include #include ================================================ FILE: src/trx/game/phase/control.h ================================================ #pragma once #include typedef enum { PHASE_ACTION_CONTINUE, PHASE_ACTION_NO_WAIT, PHASE_ACTION_END, PHASE_ACTION_END_FAST, } PHASE_ACTION; // Status returned upon every logical frame by the control routine. // // 1. To carry on executing current phase, .action member should be set to // either PHASE_ACTION_CONTINUE, which will let the cycle continue, draw the // phase and wait one frame before repeating the cycle, or // PHASE_ACTION_NO_WAIT which will immediately repeat the control routine // without drawing or waiting the current cycle. The latter is useful for // easier state switches. // The gf_cmd member is unused in this scenario. // // 2. To end the current phase and carry on continuing current game sequence, // .action member should be set to PHASE_ACTION_END, and .gf_cmd.action // member should be set to GF_NOOP. // // 3. To end the current phase and switch to another game sequence, .action // member should be set to PHASE_ACTION_END, and .gf_cmd.action member // should be set to the phase to switch to. typedef struct { PHASE_ACTION action; GF_COMMAND gf_cmd; } PHASE_CONTROL; ================================================ FILE: src/trx/game/phase/executor.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_MAX_PHASES 10 static int32_t m_CurrentFrame = 0; static bool m_Exiting; static FADER m_ExitFader; static int32_t m_PhaseStackSize = 0; static PHASE *m_PhaseStack[M_MAX_PHASES] = {}; static bool m_PendingFadeToBlack = false; static FADER_ARGS m_PendingFadeToBlackArgs; static bool M_ShouldSuspendForFocusLoss(void) { return g_Config.gameplay.pause_on_focus_lost && !Shell_IsFocused(); } static GF_COMMAND M_HandleOverride(void) { const GF_COMMAND gf_override_cmd = GF_GetOverrideCommand(); if (gf_override_cmd.action != GF_NOOP) { const GF_COMMAND gf_cmd = gf_override_cmd; GF_OverrideCommand((GF_COMMAND) { .action = GF_NOOP }); // A change in the game flow is not natural. Force features like death // counter to break from the currently active savegame file. Savegame_UnbindSlot(); // This flag needs to be cleared as well. Game_SetIsPlaying(false); // Usually, sequences permit music to flow through - for instance, the // end of level screen in The Great Wall transitioning to Venice. // We must stop it manually here when derailing the sequence (#3469). Music_Stop(); return gf_cmd; } return (GF_COMMAND) { .action = GF_NOOP }; } static void M_DrawFadeToBlackTransition(const float opacity) { Output_BeginScene(); Output_SwitchViewport(VIEWPORT_GAME); UI_BeginScene(); Output_Overlay_DrawSnapshot(1.0f); Output_Overlay_DrawBlackRectangle(opacity, false); Overlay_Draw(); Console_Draw(); UI_EndScene(); Output_SwitchViewport(VIEWPORT_UI); UI_Draw(); Output_Flush(); Output_Overlay_DrawBlackRectangle( Fader_GetCurrentValue(&m_ExitFader), true); Output_EndScene(); if (!Output_IsHeadless() || TRX_GL_Context_GetScheduledScreenshotPath() != nullptr) { Output_FlipScreen(); } else { TRX_GL_Track_Reset(); } } static GF_COMMAND M_RunFadeToBlackTransition(const FADER_ARGS args) { Output_Overlay_CaptureSnapshot(); FADER fader = {}; Fader_InitToHold(&fader, 0.0f, 1.0f, args.duration, args.debuff); while (Fader_IsActive(&fader)) { Clock_WaitTick(); m_CurrentFrame++; Shell_ProcessEvents(); Console_Control(); Overlay_Control(); const GF_COMMAND gf_cmd = M_HandleOverride(); if (gf_cmd.action != GF_NOOP) { return gf_cmd; } if (Shell_IsExiting() && !m_Exiting) { m_Exiting = true; if (g_Config.visuals.enable_exit_fade_effects) { Fader_InitFromCurrentHold(&m_ExitFader, 1.0f, 0.333f, 0.1f); } } else if (m_Exiting && !Fader_IsActive(&m_ExitFader)) { return (GF_COMMAND) { .action = GF_EXIT_GAME }; } Interpolation_SetRate(1.0f); Output_SetTime(m_CurrentFrame); M_DrawFadeToBlackTransition(Fader_GetCurrentValue(&fader)); } return (GF_COMMAND) { .action = GF_NOOP }; } static PHASE_CONTROL M_Control(PHASE *const phase) { m_CurrentFrame++; Shell_ProcessEvents(); Console_Control(); Overlay_Control(); const GF_COMMAND gf_cmd = M_HandleOverride(); if (gf_cmd.action != GF_NOOP) { return (PHASE_CONTROL) { .action = PHASE_ACTION_END_FAST, .gf_cmd = gf_cmd, }; } if (Shell_IsExiting() && !m_Exiting) { m_Exiting = true; if (g_Config.visuals.enable_exit_fade_effects) { Fader_InitFromCurrentHold(&m_ExitFader, 1.0f, 0.333f, 0.1f); } } else if (m_Exiting && !Fader_IsActive(&m_ExitFader)) { return (PHASE_CONTROL) { .action = PHASE_ACTION_END, .gf_cmd = { .action = GF_EXIT_GAME }, }; } if (m_Exiting) { return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE }; } if (M_ShouldSuspendForFocusLoss()) { return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE }; } if (phase != nullptr && phase->control != nullptr) { return phase->control(phase); } return (PHASE_CONTROL) { .action = PHASE_ACTION_END, .gf_cmd = { .action = GF_NOOP }, }; } static void M_Draw(PHASE *const phase) { BENCHMARK benchmark = Benchmark_Start(); Output_BeginScene(); Output_SwitchViewport(VIEWPORT_GAME); UI_BeginScene(); if (phase != nullptr && phase->draw != nullptr) { phase->draw(phase); } Overlay_Draw(); Console_Draw(); UI_EndScene(); Output_SwitchViewport(VIEWPORT_UI); UI_Draw(); Output_Flush(); Output_Overlay_DrawBlackRectangle( Fader_GetCurrentValue(&m_ExitFader), true); Output_EndScene(); if (Shell_GetArgs()->debug_render_performance) { char buffer[80]; const TRX_GL_METRICS metrics = TRX_GL_Track_GetMetrics(); sprintf( buffer, "%.03f KB T:%d U:%d Vo:%d Vt:%d Vb:%d", metrics.buffer_total_bytes / 1024.0f, metrics.buffer_transfer_count, metrics.uniform_changes, metrics.opaque_vert_count, metrics.trans_vert_count, metrics.blend_add_vert_count); Benchmark_End(&benchmark, buffer); } if (!Output_IsHeadless() || TRX_GL_Context_GetScheduledScreenshotPath() != nullptr) { Output_FlipScreen(); } else { TRX_GL_Track_Reset(); } } GF_COMMAND PhaseExecutor_Run(PHASE *const phase) { GF_COMMAND gf_cmd = { .action = GF_NOOP }; bool skip_fade_out = false; gf_cmd = M_HandleOverride(); if (gf_cmd.action != GF_NOOP) { return gf_cmd; } PHASE *const prev_phase = m_PhaseStackSize > 0 ? m_PhaseStack[m_PhaseStackSize - 1] : nullptr; if (prev_phase != nullptr && prev_phase->suspend != nullptr) { prev_phase->suspend(phase); } m_PhaseStack[m_PhaseStackSize++] = phase; if (m_PendingFadeToBlack) { const bool uses_cross_fade_in = phase != nullptr && phase->uses_cross_fade_in != nullptr && phase->uses_cross_fade_in(phase); if (!uses_cross_fade_in) { gf_cmd = M_RunFadeToBlackTransition(m_PendingFadeToBlackArgs); if (gf_cmd.action != GF_NOOP) { goto finish; } } m_PendingFadeToBlack = false; } if (phase->start != nullptr) { Clock_SyncTick(); g_OldInputDB = g_Input; const PHASE_CONTROL control = phase->start(phase); if (Shell_IsExiting()) { gf_cmd = (GF_COMMAND) { .action = GF_EXIT_GAME }; goto finish; } else if (control.action == PHASE_ACTION_END) { gf_cmd = control.gf_cmd; goto finish; } else if (control.action == PHASE_ACTION_END_FAST) { gf_cmd = control.gf_cmd; skip_fade_out = true; goto finish; } } while (true) { int32_t nframes = Clock_WaitTick(); int32_t frame = 0; while (true) { const PHASE_CONTROL control = M_Control(phase); if (control.action == PHASE_ACTION_END) { if (Shell_IsExiting()) { gf_cmd = (GF_COMMAND) { .action = GF_EXIT_GAME }; } else { gf_cmd = control.gf_cmd; } goto finish; } else if (control.action == PHASE_ACTION_END_FAST) { if (Shell_IsExiting()) { gf_cmd = (GF_COMMAND) { .action = GF_EXIT_GAME }; } else { skip_fade_out = true; gf_cmd = control.gf_cmd; } goto finish; } else if (control.action == PHASE_ACTION_NO_WAIT) { continue; } frame++; if (frame >= nframes) { break; } } if (!M_ShouldSuspendForFocusLoss() && Interpolation_IsActive()) { Interpolation_SetRate(0.5); Output_SetTime(m_CurrentFrame - 0.5f); M_Draw(phase); Clock_WaitTick(); } Interpolation_SetRate(1.0); Output_SetTime(m_CurrentFrame); Output_SetControlFrame(true); M_Draw(phase); Output_SetControlFrame(false); } finish: if (phase->end != nullptr) { phase->end(phase); } if (!skip_fade_out && phase->request_fade_to_black != nullptr) { m_PendingFadeToBlack = phase->request_fade_to_black(phase, &m_PendingFadeToBlackArgs); } else { m_PendingFadeToBlack = false; } if (prev_phase != nullptr && prev_phase->resume != nullptr) { Clock_SyncTick(); prev_phase->resume(phase); } m_PhaseStackSize--; return gf_cmd; } PHASE *PhaseExecutor_GetOuterPhase(void) { if (m_PhaseStackSize < 2) { return nullptr; } return m_PhaseStack[m_PhaseStackSize - 2]; } ================================================ FILE: src/trx/game/phase/executor.h ================================================ #pragma once #include GF_COMMAND PhaseExecutor_Run(PHASE *phase); PHASE *PhaseExecutor_GetOuterPhase(void); ================================================ FILE: src/trx/game/phase/phase_cutscene.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #define M_CROSS_FADE_DURATION 0.5f typedef struct { PHASE_CUTSCENE_ARGS args; FADER cross_fader; } M_PRIV; static bool M_UsesCrossFadeIn(PHASE *const phase) { M_PRIV *const p = phase->priv; return p->args.cross_fade_in && g_Config.visuals.enable_fade_effects && g_TRVersion == 3; } static PHASE_CONTROL M_Start(PHASE *const phase) { M_PRIV *const p = phase->priv; if (phase->uses_cross_fade_in != nullptr && phase->uses_cross_fade_in(phase)) { Output_Overlay_CaptureSnapshot(); Fader_InitTo(&p->cross_fader, 1.0f, 0.0f, M_CROSS_FADE_DURATION); } if (!Cutscene_Start(p->args.level_num)) { return (PHASE_CONTROL) { .action = PHASE_ACTION_END, .gf_cmd = { .action = GF_NOOP }, }; } Game_SetIsPlaying(true); Lara_SetControllable(false); return (PHASE_CONTROL) {}; } static void M_End(PHASE *const phase) { M_PRIV *const p = phase->priv; Game_SetIsPlaying(false); Cutscene_End(); } static void M_Suspend(PHASE *const phase) { Game_SetIsPlaying(false); } static void M_Resume(PHASE *const phase) { Game_SetIsPlaying(true); } static PHASE_CONTROL M_Control(PHASE *const phase) { Lua_FireEventInt32(LUA_EVENT_BEFORE_CONTROL, 0); M_PRIV *const p = phase->priv; const GF_COMMAND gf_cmd = Cutscene_Control(); Lua_FireEventInt32(LUA_EVENT_AFTER_CONTROL, 0); if (gf_cmd.action != GF_NOOP) { return (PHASE_CONTROL) { .action = PHASE_ACTION_END, .gf_cmd = gf_cmd, }; } return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE }; } static void M_Draw(PHASE *const phase) { M_PRIV *const p = phase->priv; Cutscene_Draw(); if (phase->uses_cross_fade_in != nullptr && phase->uses_cross_fade_in(phase)) { Output_Overlay_DrawSnapshot(Fader_GetCurrentValue(&p->cross_fader)); } } PHASE *Phase_Cutscene_Create(const PHASE_CUTSCENE_ARGS args) { PHASE *const phase = Memory_Alloc(sizeof(PHASE)); M_PRIV *const p = Memory_Alloc(sizeof(M_PRIV)); p->args = args; phase->priv = p; phase->start = M_Start; phase->end = M_End; phase->suspend = M_Suspend; phase->resume = M_Resume; phase->control = M_Control; phase->draw = M_Draw; phase->uses_cross_fade_in = M_UsesCrossFadeIn; return phase; } void Phase_Cutscene_Destroy(PHASE *const phase) { M_PRIV *const p = phase->priv; Memory_Free(p); Memory_Free(phase); } ================================================ FILE: src/trx/game/phase/phase_cutscene.h ================================================ #pragma once #include typedef struct { int32_t level_num; bool cross_fade_in; } PHASE_CUTSCENE_ARGS; PHASE *Phase_Cutscene_Create(PHASE_CUTSCENE_ARGS args); void Phase_Cutscene_Destroy(PHASE *phase); ================================================ FILE: src/trx/game/phase/phase_demo.c ================================================ #include #include #include #include #include #include #include #include #include typedef enum { STATE_RUN, STATE_FADE_OUT, STATE_FINISH, } STATE; typedef struct { STATE state; int32_t level_num; FADER fader; GF_COMMAND exit_gf_cmd; } M_PRIV; static PHASE_CONTROL M_Start(PHASE *const phase) { M_PRIV *const p = phase->priv; if (p->level_num == -1) { return (PHASE_CONTROL) { .action = PHASE_ACTION_END, .gf_cmd = { .action = GF_EXIT_TO_TITLE }, }; } if (!Demo_Start(p->level_num)) { return (PHASE_CONTROL) { .action = PHASE_ACTION_END, .gf_cmd = { .action = GF_EXIT_TO_TITLE }, }; } p->state = STATE_RUN; Game_SetIsPlaying(true); return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE }; } static void M_End(PHASE *const phase) { Demo_End(); } static void M_Suspend(PHASE *const phase) { Game_SetIsPlaying(false); Demo_Pause(); } static void M_Resume(PHASE *const phase) { Game_SetIsPlaying(true); Demo_Unpause(); } static PHASE_CONTROL M_Control(PHASE *const phase) { M_PRIV *const p = phase->priv; switch (p->state) { case STATE_RUN: const GF_COMMAND gf_cmd = Demo_Control(); if (gf_cmd.action != GF_NOOP) { p->state = STATE_FADE_OUT; p->exit_gf_cmd = gf_cmd; Fader_InitToHold(&p->fader, 0.0f, 1.0f, 0.5f, 0.1f); return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT }; } break; case STATE_FADE_OUT: Game_SetIsPlaying(false); Demo_StopFlashing(); if (!Fader_IsActive(&p->fader)) { p->state = STATE_FINISH; return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT }; } break; case STATE_FINISH: return (PHASE_CONTROL) { .action = PHASE_ACTION_END, .gf_cmd = Shell_IsExiting() ? (GF_COMMAND) { .action = GF_EXIT_GAME } : p->exit_gf_cmd, }; } return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE }; } static void M_Draw(PHASE *const phase) { M_PRIV *const p = phase->priv; if (p->state == STATE_FADE_OUT) { Interpolation_Disable(); } Game_Draw(true); if (p->state == STATE_FADE_OUT) { Interpolation_Enable(); } Output_Overlay_DrawBlackRectangle(Fader_GetCurrentValue(&p->fader), true); } PHASE *Phase_Demo_Create(const int32_t level_num) { PHASE *const phase = Memory_Alloc(sizeof(PHASE)); M_PRIV *const p = Memory_Alloc(sizeof(M_PRIV)); p->level_num = level_num; phase->priv = p; phase->start = M_Start; phase->end = M_End; phase->suspend = M_Suspend; phase->resume = M_Resume; phase->control = M_Control; phase->draw = M_Draw; return phase; } void Phase_Demo_Destroy(PHASE *const phase) { M_PRIV *const p = phase->priv; Memory_Free(p); Memory_Free(phase); } ================================================ FILE: src/trx/game/phase/phase_demo.h ================================================ #pragma once #include PHASE *Phase_Demo_Create(int32_t level_num); void Phase_Demo_Destroy(PHASE *phase); ================================================ FILE: src/trx/game/phase/phase_game.c ================================================ #include #include #include #include #include #include typedef struct { const GF_LEVEL *level; GF_SEQUENCE_CONTEXT seq_ctx; struct { uint8_t reverb_type; } stashed_state; } M_PRIV; static PHASE_CONTROL M_Start(PHASE *const phase) { M_PRIV *const p = phase->priv; if (!Game_Start(p->level, p->seq_ctx)) { return (PHASE_CONTROL) { .action = PHASE_ACTION_END, .gf_cmd = { .action = GF_EXIT_TO_TITLE }, }; } Game_SetIsPlaying(true); return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE, }; } static void M_End(PHASE *const phase) { Game_End(); Game_SetIsPlaying(false); Sound_SetReverbType(0); } static void M_Suspend(PHASE *const phase) { Game_SetIsPlaying(false); M_PRIV *const p = phase->priv; p->stashed_state.reverb_type = Sound_GetReverbType(); Sound_SetReverbType(0); } static void M_Resume(PHASE *const phase) { Game_SetIsPlaying(true); M_PRIV *const p = phase->priv; Sound_SetReverbType(p->stashed_state.reverb_type); } static PHASE_CONTROL M_Control(PHASE *const phase) { Lua_FireEventInt32(LUA_EVENT_BEFORE_CONTROL, 0); const GF_COMMAND gf_cmd = Game_Control(false); Lua_FireEventInt32(LUA_EVENT_AFTER_CONTROL, 0); if (gf_cmd.action != GF_NOOP) { return (PHASE_CONTROL) { .action = PHASE_ACTION_END, .gf_cmd = gf_cmd, }; } return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE }; } static void M_Draw(PHASE *const phase) { Game_Draw(true); } PHASE *Phase_Game_Create( const GF_LEVEL *const level, const GF_SEQUENCE_CONTEXT seq_ctx) { PHASE *const phase = Memory_Alloc(sizeof(PHASE)); M_PRIV *const p = Memory_Alloc(sizeof(M_PRIV)); p->level = level; p->seq_ctx = seq_ctx; phase->priv = p; phase->start = M_Start; phase->end = M_End; phase->suspend = M_Suspend; phase->resume = M_Resume; phase->control = M_Control; phase->draw = M_Draw; return phase; } void Phase_Game_Destroy(PHASE *const phase) { M_PRIV *const p = phase->priv; Memory_Free(p); Memory_Free(phase); } ================================================ FILE: src/trx/game/phase/phase_game.h ================================================ #pragma once #include #include PHASE *Phase_Game_Create(const GF_LEVEL *level, GF_SEQUENCE_CONTEXT seq_ctx); void Phase_Game_Destroy(PHASE *phase); ================================================ FILE: src/trx/game/phase/phase_globe_select.c ================================================ #include #include #include #include #include #include #include #include #include #include #include typedef enum { STATE_FADE_IN, STATE_DISPLAY, STATE_FADE_OUT, STATE_FINISH, } STATE; typedef struct { PHASE_GLOBE_SELECT_ARGS args; STATE state; FADER fader; INV_RING *ring; GF_COMMAND result; } M_PRIV; static bool M_IsFading(const M_PRIV *const p) { return Fader_IsActive(&p->fader); } static void M_FadeIn(M_PRIV *const p) { if (p->args.background_path != nullptr) { Fader_InitTo(&p->fader, 1.0f, 0.0f, 1.0); } else { Fader_InitTo(&p->fader, 0.0f, 1.0f, 0.5); } p->state = STATE_FADE_IN; } static void M_FadeOut(M_PRIV *const p) { Output_Overlay_CaptureSnapshot(); Fader_InitFromCurrentHold(&p->fader, 1.0f, 0.5f, 0.1f); p->state = STATE_FADE_OUT; } static PHASE_CONTROL M_Start(PHASE *const phase) { M_PRIV *const p = phase->priv; p->ring = InvRing_Open(INV_GLOBE_SELECT_MODE); if (p->ring == nullptr) { return (PHASE_CONTROL) { .action = PHASE_ACTION_END, .gf_cmd = { .action = GF_EXIT_TO_TITLE }, }; } M_FadeIn(p); return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE }; } static void M_End(PHASE *const phase) { M_PRIV *const p = phase->priv; if (p->ring != nullptr) { InvRing_Close(p->ring); p->ring = nullptr; } Overlay_SetBottomText((OVERLAY_TEXT) { 0 }); Overlay_ShowArrow(UI_OVERLAY_ARROW_BCL, false); Overlay_ShowArrow(UI_OVERLAY_ARROW_BCR, false); } static PHASE_CONTROL M_Control(PHASE *const phase) { M_PRIV *const p = phase->priv; switch (p->state) { case STATE_FADE_IN: if (!M_IsFading(p)) { p->state = STATE_DISPLAY; } break; case STATE_DISPLAY: break; case STATE_FADE_OUT: if (g_InputDB.menu_confirm || g_InputDB.menu_back || !M_IsFading(p)) { p->state = STATE_FINISH; return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT }; } break; case STATE_FINISH: return (PHASE_CONTROL) { .action = PHASE_ACTION_END, .gf_cmd = p->result, }; } ASSERT(p->ring != nullptr); const GF_COMMAND gf_cmd = InvRing_Control(p->ring); if (gf_cmd.action != GF_NOOP) { p->result = gf_cmd; M_FadeOut(p); } return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE }; } static void M_Draw(PHASE *const phase) { M_PRIV *const p = phase->priv; const float opacity = Fader_GetCurrentValue(&p->fader); if (p->args.background_path != nullptr) { Output_Overlay_DrawImageMono(p->args.background_path, 1.0f); Output_Overlay_DrawBlackRectangle(0.5f, false); } else { Output_Overlay_DrawBlackRectangle(1.0f, false); } Output_Flush(); ASSERT(p->ring != nullptr); InvRing_Draw(p->ring); if (opacity > 0.0f) { Output_Overlay_DrawBlackRectangle(opacity, false); } } PHASE *Phase_GlobeSelect_Create(const PHASE_GLOBE_SELECT_ARGS args) { PHASE *const phase = Memory_Alloc(sizeof(*phase)); M_PRIV *const p = Memory_Alloc(sizeof(*p)); p->result = (GF_COMMAND) { .action = GF_EXIT_TO_TITLE }; p->args = args; phase->priv = p; phase->start = M_Start; phase->end = M_End; phase->control = M_Control; phase->draw = M_Draw; return phase; } void Phase_GlobeSelect_Destroy(PHASE *const phase) { M_PRIV *const p = phase->priv; Memory_Free(p); Memory_Free(phase); } ================================================ FILE: src/trx/game/phase/phase_globe_select.h ================================================ #pragma once #include typedef struct { const char *background_path; } PHASE_GLOBE_SELECT_ARGS; PHASE *Phase_GlobeSelect_Create(PHASE_GLOBE_SELECT_ARGS args); void Phase_GlobeSelect_Destroy(PHASE *phase); ================================================ FILE: src/trx/game/phase/phase_inventory.c ================================================ #include #include #include #include #include #include #include #include #include #include typedef struct { INVENTORY_MODE mode; INV_RING *ring; bool fade_to_black; } M_PRIV; static PHASE_CONTROL M_Start(PHASE *const phase) { M_PRIV *const p = phase->priv; const GF_LEVEL *const level = GF_GetTitleLevel(); if (p->mode == INV_TITLE_MODE && g_Config.audio.enable_music_in_menu && level->music_track >= 0) { Music_Stop(); Music_Play_Direct(level->music_track, MPM_LOOP); } p->ring = InvRing_Open(p->mode); if (p->ring == nullptr) { return (PHASE_CONTROL) { .action = PHASE_ACTION_END, .gf_cmd = { .action = GF_NOOP }, }; } return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE }; } static PHASE_CONTROL M_Control(PHASE *const phase) { M_PRIV *const p = phase->priv; ASSERT(p->ring != nullptr); const GF_COMMAND gf_cmd = InvRing_Control(p->ring); if (p->mode == INV_TITLE_MODE && p->ring->status == RNG_DONE) { p->fade_to_black = true; } return (PHASE_CONTROL) { .action = (p->mode == INV_GLOBE_SELECT_MODE && gf_cmd.action != GF_NOOP) || p->ring->status == RNG_DONE ? PHASE_ACTION_END : PHASE_ACTION_CONTINUE, .gf_cmd = gf_cmd, }; } static bool M_RequestFadeToBlack(PHASE *const phase, FADER_ARGS *const out_args) { const M_PRIV *const p = phase->priv; if (p->mode != INV_TITLE_MODE || !p->fade_to_black) { return false; } if (out_args != nullptr) { *out_args = (FADER_ARGS) { .from_current = false, .initial = 0.0f, .target = 1.0f, .duration = 0.25f, .debuff = 0.1f, }; } return true; } static void M_End(PHASE *const phase) { M_PRIV *const p = phase->priv; if (p->mode == INV_TITLE_MODE) { Music_Stop(); } if (p->ring != nullptr) { InvRing_Close(p->ring); p->ring = nullptr; } } static void M_Draw(PHASE *const phase) { M_PRIV *const p = phase->priv; ASSERT(p->ring != nullptr); InvRing_Draw(p->ring); } PHASE *Phase_Inventory_Create(const INVENTORY_MODE mode) { PHASE *const phase = Memory_Alloc(sizeof(PHASE)); M_PRIV *const p = Memory_Alloc(sizeof(M_PRIV)); p->mode = mode; p->fade_to_black = false; phase->priv = p; phase->start = M_Start; phase->end = M_End; phase->control = M_Control; phase->draw = M_Draw; phase->request_fade_to_black = mode == INV_TITLE_MODE ? M_RequestFadeToBlack : nullptr; return phase; } void Phase_Inventory_Destroy(PHASE *const phase) { Memory_Free(phase->priv); Memory_Free(phase); } ================================================ FILE: src/trx/game/phase/phase_inventory.h ================================================ #pragma once #include #include PHASE *Phase_Inventory_Create(INVENTORY_MODE mode); void Phase_Inventory_Destroy(PHASE *phase); ================================================ FILE: src/trx/game/phase/phase_pause.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_FADE_TIME 0.4 typedef enum { STATE_FADE_IN, STATE_WAIT, STATE_ASK, STATE_FADE_OUT, } STATE; typedef struct { STATE state; struct { bool is_ready; UI_PAUSE_STATE state; } ui; GF_ACTION action; FADER fader; } M_PRIV; static void M_RemoveText(M_PRIV *const p) { Overlay_SetBottomText((OVERLAY_TEXT) { 0 }); } static void M_FadeIn(M_PRIV *const p) { p->state = STATE_FADE_IN; Fader_InitTo(&p->fader, 0.0f, 1.0f, M_FADE_TIME); } static void M_FadeOut(M_PRIV *const p) { M_RemoveText(p); p->ui.is_ready = false; if (p->action == GF_NOOP) { Fader_InitFromCurrent(&p->fader, 0.0f, M_FADE_TIME); } else { Fader_InitFromCurrentHold( &p->fader, 1.0f, M_FADE_TIME, 3.0 / (double)LOGIC_FPS); } p->state = STATE_FADE_OUT; } static void M_PauseGame(M_PRIV *const p) { p->action = GF_NOOP; Music_Pause(); Sound_PauseAll(); M_FadeIn(p); } static void M_ReturnToGame(M_PRIV *const p) { Music_Unpause(); Sound_UnpauseAll(); M_FadeOut(p); } static void M_ExitToTitle(M_PRIV *const p) { p->action = GF_EXIT_TO_TITLE; M_FadeOut(p); } static void M_CreateText(M_PRIV *const p) { Overlay_SetBottomText((OVERLAY_TEXT) { .kind = UI_OVERLAY_TEXT_GS_KEY, .gs_key = GS_ID("general/pause/paused"), }); } static PHASE_CONTROL M_Start(PHASE *const phase) { M_PRIV *const p = phase->priv; p->ui.is_ready = false; UI_Pause_Init(&p->ui.state); M_PauseGame(p); return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE }; } static void M_End(PHASE *const phase) { M_PRIV *const p = phase->priv; M_RemoveText(p); UI_Pause_Free(&p->ui.state); } static bool M_IsFadeActive(M_PRIV *const p) { return Fader_IsActive(&p->fader) && g_Config.ui.pause_fade_effects && g_Config.ui.pause_background_style != BK_NONE; } static PHASE_CONTROL M_Control(PHASE *const phase) { M_PRIV *const p = phase->priv; Input_Update(); Shell_ProcessInput(); if (p->ui.is_ready) { UI_Pause_Control(&p->ui.state); } switch (p->state) { case STATE_FADE_IN: if (g_InputDB.pause) { M_ReturnToGame(p); return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT }; } else if (!M_IsFadeActive(p)) { p->state = STATE_WAIT; M_CreateText(p); return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT }; } break; case STATE_WAIT: if (g_InputDB.pause) { M_ReturnToGame(p); return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT }; } else if (g_InputDB.option) { p->state = STATE_ASK; } break; case STATE_ASK: { const UI_PAUSE_EXIT_CHOICE choice = UI_Pause_Control(&p->ui.state); switch (choice) { case UI_PAUSE_RESUME_PAUSE: p->state = STATE_WAIT; return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT }; case UI_PAUSE_EXIT_TO_GAME: M_ReturnToGame(p); return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT }; case UI_PAUSE_EXIT_TO_TITLE: M_ExitToTitle(p); return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT }; default: break; } break; } case STATE_FADE_OUT: if (!M_IsFadeActive(p)) { return (PHASE_CONTROL) { .action = PHASE_ACTION_END, .gf_cmd = { .action = p->action }, }; } break; } return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE }; } static void M_Draw(PHASE *const phase) { M_PRIV *const p = phase->priv; const float progress = g_Config.ui.pause_fade_effects ? Fader_GetCurrentValue(&p->fader) : p->fader.args.target; switch (g_Config.ui.pause_background_style) { case BK_NONE: Output_Overlay_DrawGame(); break; case BK_TRANSPARENT_MEDIUM: Output_Overlay_DrawGame(); Output_Overlay_DrawBlackRectangle(progress * 0.5f, false); break; case BK_TRANSPARENT_DARK: Output_Overlay_DrawGame(); Output_Overlay_DrawBlackRectangle(progress * 0.8f, false); break; case BK_BLACK: Output_Overlay_DrawGame(); Output_Overlay_DrawBlackRectangle(progress, false); break; case BK_MONOCHROME: Output_Overlay_DrawGameMono(progress); break; case BK_MONOCHROME_COOL: Output_Overlay_DrawGameMonoCool(progress); break; case BK_MONOCHROME_WARM: Output_Overlay_DrawGameMonoWarm(progress); break; case BK_PATTERN_STATIC: case BK_PATTERN_WAVE: if (progress < 1.0f) { Output_Overlay_DrawGame(); } Output_Overlay_DrawPatternOpacity( g_Config.ui.pause_background_style == BK_PATTERN_WAVE, progress); break; case BK_IMAGE: default: Output_Overlay_DrawGame(); Output_Overlay_DrawBlackRectangle(progress * 0.8f, false); break; } if (p->state == STATE_ASK) { UI_Pause(&p->ui.state); } } PHASE *Phase_Pause_Create(void) { PHASE *const phase = Memory_Alloc(sizeof(PHASE)); phase->priv = Memory_Alloc(sizeof(M_PRIV)); phase->start = M_Start; phase->end = M_End; phase->control = M_Control; phase->draw = M_Draw; return phase; } void Phase_Pause_Destroy(PHASE *phase) { Memory_Free(phase->priv); Memory_Free(phase); } ================================================ FILE: src/trx/game/phase/phase_pause.h ================================================ #pragma once #include PHASE *Phase_Pause_Create(void); void Phase_Pause_Destroy(PHASE *phase); ================================================ FILE: src/trx/game/phase/phase_photo_mode.c ================================================ #include #include #include #include #include #include #include #include #include #include #include typedef struct { bool in_cutscene; bool taking_screenshot; } M_PRIV; static PHASE_CONTROL M_Start(PHASE *phase) { M_PRIV *const p = phase->priv; p->in_cutscene = GF_GetCurrentLevel()->type == GFL_CUTSCENE; PhotoMode_Start(); return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE }; } static void M_End(PHASE *const phase) { PhotoMode_End(); } static PHASE_CONTROL M_Control(PHASE *const phase) { M_PRIV *const p = phase->priv; Input_Update(); Shell_ProcessInput(); // XXX: normally we'd be using menu_back alone to let the player go back // and exit the photo mode UI, BUT for controller players, the default // menu_back button conflicts with the roll input, as both are bound to the // B button. This causes neither to work as expected, when the player // presses B. This is a hacky solution since technically the player might // remap the roll input to some other button, making the roll check below // redundant, but this is the most straightforward approach. if (g_InputDB.toggle_photo_mode || (g_InputDB.menu_back && !g_InputDB.roll)) { return (PHASE_CONTROL) { .action = PHASE_ACTION_END, .gf_cmd = { .action = GF_NOOP }, }; } else if (g_InputDB.action) { p->taking_screenshot = true; Screenshot_Make(g_Config.rendering.screenshot_format); Sound_Effect(SFX_MENU_LARA_HOME, nullptr, SPM_ALWAYS); } return PhotoMode_Control(); } static void M_Draw(PHASE *const phase) { M_PRIV *const p = phase->priv; if (p->in_cutscene) { Cutscene_Draw(); } else { Game_Draw(false); } if (p->taking_screenshot) { p->taking_screenshot = false; } else { UI_PhotoMode(PhotoMode_GetCurrentMode()); } } PHASE *Phase_PhotoMode_Create(void) { PHASE *const phase = Memory_Alloc(sizeof(PHASE)); phase->priv = Memory_Alloc(sizeof(M_PRIV)); phase->start = M_Start; phase->end = M_End; phase->control = M_Control; phase->draw = M_Draw; return phase; } void Phase_PhotoMode_Destroy(PHASE *phase) { Memory_Free(phase->priv); Memory_Free(phase); } ================================================ FILE: src/trx/game/phase/phase_photo_mode.h ================================================ #pragma once #include PHASE *Phase_PhotoMode_Create(void); void Phase_PhotoMode_Destroy(PHASE *phase); ================================================ FILE: src/trx/game/phase/phase_picture.c ================================================ #include #include #include #include #include #include typedef enum { STATE_FADE_IN, STATE_DISPLAY, STATE_FADE_OUT, } M_STATE; typedef struct { M_STATE state; FADER fader; CLOCK_TIMER timer; PHASE_PICTURE_ARGS args; bool has_drawn; } M_PRIV; static bool M_UsesCrossFadeIn(PHASE *const phase) { const M_PRIV *const p = phase->priv; return p->args.loading_pic && !p->args.block_cross_fade_in; } static void M_FadeOut(M_PRIV *const p) { p->state = STATE_FADE_OUT; Fader_InitFromCurrentHold(&p->fader, 1.0f, p->args.fade_out_time, 0.1f); } static PHASE_CONTROL M_Start(PHASE *const phase) { M_PRIV *const p = phase->priv; if (!Output_Overlay_LoadImage(p->args.file_name)) { return (PHASE_CONTROL) { .action = PHASE_ACTION_END, .gf_cmd = { .action = GF_NOOP }, }; } if (p->args.loading_pic && !p->args.block_cross_fade_in) { Output_Overlay_CaptureSnapshot(); } Fader_InitTo(&p->fader, 1.0f, 0.0f, p->args.fade_in_time); ClockTimer_Sync(&p->timer); return (PHASE_CONTROL) {}; } static void M_End(PHASE *const phase) { M_PRIV *const p = phase->priv; } static PHASE_CONTROL M_Control(PHASE *const phase) { M_PRIV *const p = phase->priv; Input_Update(); Shell_ProcessInput(); switch (p->state) { case STATE_FADE_IN: if (g_InputDB.menu_confirm || g_InputDB.menu_back || g_InputDB.menu_skip) { M_FadeOut(p); } else if (!Fader_IsActive(&p->fader)) { p->state = STATE_DISPLAY; ClockTimer_Sync(&p->timer); } break; case STATE_DISPLAY: if (g_InputDB.menu_confirm || g_InputDB.menu_back || g_InputDB.menu_skip || ClockTimer_CheckElapsed( &p->timer, p->args.display_time - (p->args.display_time_includes_fades ? p->args.fade_in_time + p->args.fade_out_time : 0.0))) { M_FadeOut(p); } break; case STATE_FADE_OUT: if (p->args.loading_pic && p->has_drawn) { Output_Overlay_BeginTransitionFadeOut( p->args.fade_out_time, 1.0f - Fader_GetCurrentValue(&p->fader)); return (PHASE_CONTROL) { .action = PHASE_ACTION_END, .gf_cmd = { .action = GF_NOOP }, }; } if (g_InputDB.menu_confirm || g_InputDB.menu_back || g_InputDB.menu_skip || !Fader_IsActive(&p->fader)) { return (PHASE_CONTROL) { .action = PHASE_ACTION_END, .gf_cmd = { .action = GF_NOOP }, }; } break; } return (PHASE_CONTROL) {}; } static void M_Draw(PHASE *const phase) { M_PRIV *const p = phase->priv; const float progress = Fader_GetCurrentValue(&p->fader); if (p->args.loading_pic && (p->state != STATE_FADE_IN || !p->args.block_cross_fade_in)) { Output_Overlay_DrawImage(p->args.file_name); if (p->state == STATE_FADE_IN) { Output_Overlay_DrawSnapshot(progress); } } else { Output_Overlay_DrawImage(p->args.file_name); Output_Overlay_DrawBlackRectangle(progress, false); } p->has_drawn = true; } PHASE *Phase_Picture_Create(const PHASE_PICTURE_ARGS args) { PHASE *const phase = Memory_Alloc(sizeof(PHASE)); M_PRIV *const p = Memory_Alloc(sizeof(M_PRIV)); p->args = args; p->state = STATE_FADE_IN; p->has_drawn = false; phase->priv = p; phase->start = M_Start; phase->end = M_End; phase->control = M_Control; phase->draw = M_Draw; phase->request_fade_to_black = nullptr; phase->uses_cross_fade_in = M_UsesCrossFadeIn; return phase; } void Phase_Picture_Destroy(PHASE *const phase) { M_PRIV *const p = phase->priv; Memory_Free(p); Memory_Free(phase); } ================================================ FILE: src/trx/game/phase/phase_picture.h ================================================ #pragma once #include typedef struct { const char *file_name; double display_time; double fade_in_time; double fade_out_time; bool display_time_includes_fades; bool loading_pic; bool block_cross_fade_in; } PHASE_PICTURE_ARGS; PHASE *Phase_Picture_Create(PHASE_PICTURE_ARGS args); void Phase_Picture_Destroy(PHASE *phase); ================================================ FILE: src/trx/game/phase/phase_stats.c ================================================ #include #include #include #include #include #include #include #include #include #include #include typedef enum { STATE_FADE_IN, STATE_DISPLAY, STATE_FADE_OUT, STATE_FINISH, } STATE; typedef struct { PHASE_STATS_ARGS args; STATE state; FADER back_fader; FADER top_fader; bool ui_active; UI_STATS_DIALOG_STATE *ui_state; } M_PRIV; static bool M_EnableFade(const M_PRIV *const p) { return g_Config.ui.stats_fade_effects || p->args.show_final_stats; } static bool M_IsFading(const M_PRIV *const p) { return M_EnableFade(p) && (Fader_IsActive(&p->top_fader) || Fader_IsActive(&p->back_fader)); } static void M_FadeIn(M_PRIV *const p) { if (p->args.background_path != nullptr) { Fader_InitTo(&p->back_fader, 1.0f, 0.0f, 1.0); } else { Fader_InitTo(&p->back_fader, 0.0f, 1.0f, 0.5); } } static void M_FadeOut(M_PRIV *const p) { p->state = STATE_FINISH; } static PHASE_CONTROL M_Start(PHASE *const phase) { M_PRIV *const p = phase->priv; if (!Game_IsInGym()) { p->ui_state = UI_StatsDialog_Init((UI_STATS_DIALOG_ARGS) { .mode = p->args.show_final_stats ? UI_STATS_DIALOG_MODE_FINAL : UI_STATS_DIALOG_MODE_LEVEL, .style = p->args.use_bare_style ? UI_STATS_DIALOG_STYLE_BARE : UI_STATS_DIALOG_STYLE_BORDERED, .level_num = p->args.level_num != -1 ? p->args.level_num : Game_GetCurrentLevel()->num, }); if (p->args.show_final_stats && !UI_StatsDialog_HasVisibleRows(p->ui_state)) { UI_StatsDialog_Free(p->ui_state); p->ui_state = nullptr; p->state = STATE_FINISH; return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE }; } } switch (p->args.background_type) { case BK_IMAGE: if (p->args.background_path == nullptr) { LOG_WARNING("Trying to load empty background image"); } else if (!Output_Overlay_LoadImage(p->args.background_path)) { LOG_WARNING( "Failed to load background image: %s", p->args.background_path); } break; case BK_NONE: case BK_PATTERN_STATIC: case BK_PATTERN_WAVE: case BK_BLACK: case BK_MONOCHROME: case BK_MONOCHROME_COOL: case BK_MONOCHROME_WARM: case BK_TRANSPARENT_MEDIUM: case BK_TRANSPARENT_DARK: break; } if (Game_IsInGym()) { M_FadeOut(p); } else { if (p->args.background_type == BK_PATTERN_STATIC || p->args.background_type == BK_PATTERN_WAVE) { p->state = STATE_DISPLAY; } else { p->state = STATE_FADE_IN; M_FadeIn(p); } p->ui_active = true; } return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE }; } static void M_End(PHASE *const phase) { M_PRIV *const p = phase->priv; if (p->ui_active) { p->ui_active = false; UI_StatsDialog_Free(p->ui_state); p->ui_state = nullptr; } } static PHASE_CONTROL M_Control(PHASE *const phase) { M_PRIV *const p = phase->priv; Input_Update(); Shell_ProcessInput(); switch (p->state) { case STATE_FADE_IN: if (!M_IsFading(p)) { p->state = STATE_DISPLAY; } else if (g_InputDB.menu_confirm || g_InputDB.menu_back) { M_FadeOut(p); } break; case STATE_DISPLAY: if (g_InputDB.menu_confirm || g_InputDB.menu_back) { M_FadeOut(p); } break; case STATE_FADE_OUT: p->state = STATE_FINISH; return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT }; break; case STATE_FINISH: return (PHASE_CONTROL) { .action = PHASE_ACTION_END, .gf_cmd = { .action = GF_NOOP }, }; } return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE }; } static bool M_RequestFadeToBlack(PHASE *const phase, FADER_ARGS *const out_args) { M_PRIV *const p = phase->priv; if (!M_EnableFade(p)) { return false; } if (out_args != nullptr) { *out_args = (FADER_ARGS) { .from_current = false, .initial = 0.0f, .target = 1.0f, .duration = 0.5f, .debuff = 0.1f, }; } return true; } static void M_Draw(PHASE *const phase) { M_PRIV *const p = phase->priv; const float top_opacity = M_EnableFade(p) ? Fader_GetCurrentValue(&p->top_fader) : p->top_fader.args.target; if (top_opacity > 0.0f) { Output_Overlay_DrawSnapshot(1.0f); Output_Overlay_DrawBlackRectangle(top_opacity, false); return; } const float progress = M_EnableFade(p) ? Fader_GetCurrentValue(&p->back_fader) : p->back_fader.args.target; switch (p->args.background_type) { case BK_NONE: Output_Overlay_DrawGame(); break; case BK_TRANSPARENT_MEDIUM: Output_Overlay_DrawGame(); Output_Overlay_DrawBlackRectangle(progress * 0.5f, false); break; case BK_TRANSPARENT_DARK: Output_Overlay_DrawGame(); Output_Overlay_DrawBlackRectangle(progress * 0.8f, false); break; case BK_BLACK: Output_Overlay_DrawGame(); Output_Overlay_DrawBlackRectangle(progress, false); break; case BK_MONOCHROME: Output_Overlay_DrawGameMono(progress); break; case BK_MONOCHROME_COOL: Output_Overlay_DrawGameMonoCool(progress); break; case BK_MONOCHROME_WARM: Output_Overlay_DrawGameMonoWarm(progress); break; case BK_IMAGE: if (p->args.background_path != nullptr) { Output_Overlay_DrawImageBilinear(p->args.background_path); } Output_Overlay_DrawBlackRectangle(progress, false); break; case BK_PATTERN_STATIC: case BK_PATTERN_WAVE: Output_Overlay_DrawPattern(p->args.background_type == BK_PATTERN_WAVE); Output_Overlay_DrawBlackRectangle(progress, false); break; default: break; } if (p->ui_active) { UI_StatsDialog(p->ui_state); } } PHASE *Phase_Stats_Create(const PHASE_STATS_ARGS args) { PHASE *const phase = Memory_Alloc(sizeof(PHASE)); M_PRIV *const p = Memory_Alloc(sizeof(M_PRIV)); p->args = args; p->state = STATE_FADE_IN; phase->priv = p; phase->start = M_Start; phase->end = M_End; phase->control = M_Control; phase->draw = M_Draw; phase->request_fade_to_black = M_RequestFadeToBlack; phase->uses_cross_fade_in = nullptr; return phase; } void Phase_Stats_Destroy(PHASE *const phase) { M_PRIV *const p = phase->priv; Memory_Free(p); Memory_Free(phase); } ================================================ FILE: src/trx/game/phase/phase_stats.h ================================================ #pragma once #include #include typedef struct { BACKGROUND_TYPE background_type; const char *background_path; bool show_final_stats; bool use_bare_style; int32_t level_num; } PHASE_STATS_ARGS; PHASE *Phase_Stats_Create(PHASE_STATS_ARGS args); void Phase_Stats_Destroy(PHASE *phase); ================================================ FILE: src/trx/game/phase/types.h ================================================ #pragma once #include #include typedef struct PHASE PHASE; typedef PHASE_CONTROL (*PHASE_START_FUNC)(PHASE *phase); typedef void (*PHASE_END_FUNC)(PHASE *phase); typedef void (*PHASE_SUSPEND_FUNC)(PHASE *phase); typedef void (*PHASE_RESUME_FUNC)(PHASE *phase); typedef PHASE_CONTROL (*PHASE_CONTROL_FUNC)(PHASE *phase); typedef void (*PHASE_DRAW_FUNC)(PHASE *phase); typedef bool (*PHASE_REQUEST_FADE_TO_BLACK_FUNC)( PHASE *phase, FADER_ARGS *out_args); typedef bool (*PHASE_USES_CROSS_FADE_IN_FUNC)(PHASE *phase); typedef struct PHASE { PHASE_START_FUNC start; PHASE_END_FUNC end; PHASE_SUSPEND_FUNC suspend; PHASE_RESUME_FUNC resume; PHASE_CONTROL_FUNC control; PHASE_DRAW_FUNC draw; PHASE_REQUEST_FADE_TO_BLACK_FUNC request_fade_to_black; PHASE_USES_CROSS_FADE_IN_FUNC uses_cross_fade_in; void *priv; } PHASE; ================================================ FILE: src/trx/game/phase.h ================================================ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include ================================================ FILE: src/trx/game/photo_mode.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_INTERPOLATION_STEP 0.25 typedef struct { PHOTO_MODE current_mode; bool show_fps_counter; double rate; bool lara_pos_touched; XYZ_32 orig_lara_pos; XYZ_16 orig_lara_rot; } M_PRIV; static M_PRIV m_Priv = {}; static void M_ApplyInterpolation(void) { Interpolation_CommitLara(); Lara_Hair_Control(true); Interpolation_CommitBraid(); } static void M_RememberLaraPos(M_PRIV *const p) { const ITEM *const lara_item = Lara_GetItem(); if (lara_item != nullptr) { p->orig_lara_pos = lara_item->pos; p->orig_lara_rot = lara_item->rot; } } static void M_RestoreLaraPos(M_PRIV *const p) { ITEM *const lara_item = Lara_GetItem(); if (lara_item != nullptr) { lara_item->pos = p->orig_lara_pos; lara_item->rot = p->orig_lara_rot; } } static PHASE_CONTROL M_AdvanceFrame(M_PRIV *const p) { PHASE_CONTROL result = (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE }; PHASE *const phase = PhaseExecutor_GetOuterPhase(); if (phase == nullptr || phase->control == nullptr || phase->draw == nullptr) { return result; } XYZ_32 prev_lara_pos; XYZ_16 prev_lara_rot; ITEM *const lara_item = Lara_GetItem(); if (lara_item != nullptr) { prev_lara_pos = lara_item->pos; prev_lara_rot = lara_item->rot; } M_RestoreLaraPos(p); Camera_PhotoMode_Pause(); const bool is_enabled = Interpolation_IsEnabled(); const bool slow = g_Input.slow; Interpolation_Enable(); if (phase->resume != nullptr) { phase->resume(phase); } if (p->rate >= 1.0) { InputState_Clear(&g_Input); InputState_Clear(&g_InputDB); result = phase->control(phase); InputState_Clear(&g_Input); InputState_Clear(&g_InputDB); p->rate = 0.0; } p->rate += slow ? M_INTERPOLATION_STEP : 1.0f; Interpolation_SetRate(p->rate); phase->draw(phase); if (phase->suspend != nullptr) { phase->suspend(phase); } if (!is_enabled) { Interpolation_Disable(); } Camera_PhotoMode_Resume(); M_RememberLaraPos(p); if (lara_item != nullptr && p->lara_pos_touched) { lara_item->pos = prev_lara_pos; lara_item->rot = prev_lara_rot; Lara_Hair_Initialise(); M_ApplyInterpolation(); } return result; } static bool M_HandleItemPositionInputs(ITEM *const item) { const int32_t trans_speed = STEP_L / 10; XYZ_32 delta = {}; if (g_Input.camera_left) { delta.x -= trans_speed; } else if (g_Input.camera_right) { delta.x += trans_speed; } if (g_Input.camera_forward) { delta.z += trans_speed; } else if (g_Input.camera_back) { delta.z -= trans_speed; } if (!g_Input.slow && g_Input.camera_up) { delta.y -= trans_speed; } else if (!g_Input.slow && g_Input.camera_down) { delta.y += trans_speed; } if (delta.x == 0 && delta.y == 0 && delta.z == 0) { return false; } const int32_t pitch = item->rot.x; const int32_t cos_p = Math_Cos(pitch); const int32_t sin_p = Math_Sin(pitch); const int32_t local_y = delta.y - TRIGMULT2(sin_p, delta.z); const int32_t local_z = TRIGMULT2(cos_p, delta.z); const int32_t local_x = delta.x; const int32_t yaw = item->rot.y; const int32_t cos_y = Math_Cos(yaw); const int32_t sin_y = Math_Sin(yaw); const int32_t world_x = TRIGMULT2(cos_y, local_x) + TRIGMULT2(sin_y, local_z); const int32_t world_z = -TRIGMULT2(sin_y, local_x) + TRIGMULT2(cos_y, local_z); item->pos.x += world_x; item->pos.y += local_y; item->pos.z += world_z; return true; } static bool M_HandleItemRotationInputs(ITEM *const item) { const int32_t rot_speed = DEG_1 * 2; XYZ_32 delta = {}; if (g_Input.left) { delta.y = -rot_speed; } else if (g_Input.right) { delta.y = +rot_speed; } if (g_Input.forward) { delta.x = -rot_speed; } else if (g_Input.back) { delta.x = +rot_speed; } if (g_Input.slow && g_Input.camera_up) { delta.z = -rot_speed; } else if (g_Input.slow && g_Input.camera_down) { delta.z = +rot_speed; } if (g_InputDB.roll) { delta.y = DEG_90; } if (delta.x == 0 && delta.y == 0 && delta.z == 0) { return false; } // Keep the item's root joint anchored while rotating, so off-center // animation origins (especially in cutscenes) do not cause huge offsets. // Use live item transforms; interpolation may still contain previous-frame // values and would make both samples identical. const bool was_item_interp_enabled = item->enable_interpolation; item->enable_interpolation = false; XYZ_32 old_root_pos = {}; Collide_GetJointAbsPosition(item, &old_root_pos, 0); item->rot.x += delta.x; item->rot.y += delta.y; item->rot.z += delta.z; XYZ_32 new_root_pos = {}; Collide_GetJointAbsPosition(item, &new_root_pos, 0); item->enable_interpolation = was_item_interp_enabled; item->pos.x += old_root_pos.x - new_root_pos.x; item->pos.y += old_root_pos.y - new_root_pos.y; item->pos.z += old_root_pos.z - new_root_pos.z; return true; } static void M_HandleEditLaraMode(M_PRIV *const p) { Camera_PhotoMode_UpdateFOV(); ITEM *const lara_item = Lara_GetItem(); if (lara_item == nullptr) { return; } bool changed = false; changed |= M_HandleItemPositionInputs(lara_item); changed |= M_HandleItemRotationInputs(lara_item); if (changed) { p->lara_pos_touched = true; } if (g_InputDB.look) { M_RestoreLaraPos(p); changed = true; p->lara_pos_touched = false; } if (changed) { M_ApplyInterpolation(); } } static void M_HandleCameraMode(M_PRIV *const p) { Camera_PhotoMode_Update(); } void PhotoMode_Start(void) { M_PRIV *const p = &m_Priv; p->show_fps_counter = g_Config.ui.enable_fps_counter; p->rate = 1.0; p->current_mode = PHOTO_MODE_CAMERA; p->lara_pos_touched = false; g_Config.ui.enable_fps_counter = false; M_RememberLaraPos(p); Camera_PhotoMode_Enter(); Music_Pause(); Sound_PauseAll(); } void PhotoMode_End(void) { M_PRIV *const p = &m_Priv; Camera_PhotoMode_Exit(); M_RestoreLaraPos(p); g_Config.ui.enable_fps_counter = p->show_fps_counter; Music_Unpause(); Sound_UnpauseAll(); } PHASE_CONTROL PhotoMode_Control(void) { M_PRIV *const p = &m_Priv; Interpolation_Remember(); if (g_InputDB.pause) { return M_AdvanceFrame(p); } else if (g_InputDB.toggle_ui) { UI_ToggleState(&g_Config.ui.enable_photo_mode_ui); } else if (g_InputDB.step_left) { if (p->current_mode == 0) { p->current_mode = PHOTO_MODE_LAST; } else { p->current_mode--; } } else if (g_InputDB.step_right) { if (p->current_mode == PHOTO_MODE_LAST) { p->current_mode = 0; } else { p->current_mode++; } } else if (g_InputDB.fly_cheat) { Lara_Pose_Cycle(g_Input.slow ? -1 : 1); } else { switch (p->current_mode) { case PHOTO_MODE_LARA_POS: M_HandleEditLaraMode(p); break; case PHOTO_MODE_CAMERA: M_HandleCameraMode(p); break; } } return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE }; } PHOTO_MODE PhotoMode_GetCurrentMode(void) { M_PRIV *const p = &m_Priv; return p->current_mode; } ================================================ FILE: src/trx/game/photo_mode.h ================================================ #pragma once #include typedef enum { PHOTO_MODE_CAMERA, PHOTO_MODE_LARA_POS, PHOTO_MODE_LAST = PHOTO_MODE_LARA_POS, } PHOTO_MODE; void PhotoMode_Start(void); void PhotoMode_End(void); PHOTO_MODE PhotoMode_GetCurrentMode(void); PHASE_CONTROL PhotoMode_Control(void); ================================================ FILE: src/trx/game/random.c ================================================ #include #include #include static uint32_t m_RandControl = 0xD371F947U; static uint32_t m_RandDraw = 0xD371F947U; static bool m_IsDrawFrozen = false; void Random_Seed(void) { time_t lt = time(0); struct tm *tptr = localtime(<); Random_SeedControl(tptr->tm_sec + 57 * tptr->tm_min + 3543 * tptr->tm_hour); Random_SeedDraw(tptr->tm_sec + 43 * tptr->tm_min + 3477 * tptr->tm_hour); } void Random_SeedControl(int32_t seed) { LOG_DEBUG("%d", seed); m_RandControl = (uint32_t)seed; } int32_t Random_GetControl(void) { m_RandControl = 0x41C64E6DU * m_RandControl + 0x3039U; return (int32_t)((m_RandControl >> 10) & 0x7FFFU); } void Random_SeedDraw(int32_t seed) { LOG_DEBUG("%d", seed); m_RandDraw = (uint32_t)seed; } int32_t Random_GetDraw(void) { // Allow draw RNG to advance only during initial game setup (for such things // as caustic initialisation) and normal game play. RNG should remain static // when the game output is paused e.g. inventory, pause screen etc. if (!m_IsDrawFrozen) { m_RandDraw = 0x41C64E6DU * m_RandDraw + 0x3039U; } return (int32_t)((m_RandDraw >> 10) & 0x7FFFU); } int32_t Random_GetControlSeed(void) { return (int32_t)m_RandControl; } int32_t Random_GetDrawSeed(void) { return (int32_t)m_RandDraw; } void Random_FreezeDraw(bool is_frozen) { m_IsDrawFrozen = is_frozen; } ================================================ FILE: src/trx/game/random.h ================================================ #pragma once #include void Random_Seed(void); void Random_SeedControl(int32_t seed); void Random_SeedDraw(int32_t seed); int32_t Random_GetControl(void); int32_t Random_GetDraw(void); int32_t Random_GetControlSeed(void); int32_t Random_GetDrawSeed(void); void Random_FreezeDraw(bool is_frozen); ================================================ FILE: src/trx/game/replay/test_recorder.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_DEBUG 0 #define M_MAX_EVENTS 64 // Maximum SDL or custom events per frame // Internal event codes for recorder swimlane typedef enum { M_CUSTOM_EVENT_SCREENSHOT, M_CUSTOM_EVENT_COMMAND, } M_CUSTOM_EVENT; typedef struct { MYFILE *file; int32_t prev_frame_idx; int32_t frame_idx; SDL_Event queue[M_MAX_EVENTS]; int32_t queue_size; int32_t listeners[2]; } M_PRIV; static const struct { const char *arg; bool takes_value; } m_SkipArgs[] = { { "--debug-render-performance", false }, { "--test-record", true }, { "--test-replay", true }, { "--test-play", true }, { "--headless-fps", true }, { nullptr, false }, }; static M_PRIV m_Priv = {}; static int M_CompareConfigOption(const void *a, const void *b) { const CONFIG_OPTION *const *opt_a = a; const CONFIG_OPTION *const *opt_b = b; return strcmp((*opt_a)->name, (*opt_b)->name); } static const char *M_DumpEvent(const SDL_Event *const event) { switch (event->type) { case SDL_USEREVENT: const char *result = nullptr; if (event->user.code == M_CUSTOM_EVENT_SCREENSHOT) { char *path = event->user.data1; result = String_FormatStatic("noop # cmd { screenshot %s }", path); Memory_FreePointer(&path); } else if (event->user.code == M_CUSTOM_EVENT_COMMAND) { char *cmd = event->user.data1; result = String_FormatStatic("noop # cmd { %s }", cmd); Memory_FreePointer(&cmd); } return result; case SDL_KEYDOWN: // NOTE: we do not serialize the modifiers to avoid noise, as currently // they are unused by the engine. In the future, once we add support // for compound keybindings, it may become necessary to either // serialize them, or simulate them in the replay module. return String_FormatStatic( "● \"%s\"", Input_KeyDescFromSDL(event->key.keysym.scancode, 0)); case SDL_KEYUP: return String_FormatStatic( "○ \"%s\"", Input_KeyDescFromSDL(event->key.keysym.scancode, 0)); case SDL_TEXTINPUT: return String_FormatStatic("text-input \"%s\"", event->text.text); case SDL_QUIT: return String_FormatStatic("quit"); } return nullptr; } static void M_DumpQueue(M_PRIV *const p) { #if !M_DEBUG if (p->queue_size == 0) { return; } #endif const size_t indent = 8; File_WriteString( p->file, "%-*s", indent, String_FormatStatic("@+%d:", p->frame_idx - p->prev_frame_idx)); for (int32_t i = 0; i < p->queue_size; i++) { const SDL_Event *const event = &p->queue[i]; const char *const event_str = M_DumpEvent(event); if (event_str == nullptr) { continue; } File_WriteString(p->file, event_str); if (i < p->queue_size - 1) { File_WriteString(p->file, "\n%*s", indent, ""); } } #if M_DEBUG if (p->queue_size == 0) { File_WriteString(p->file, "noop"); } const ITEM *const lara_item = Lara_GetItem(); const OBJECT_ID obj_id = Lara_GetAnimationObject(); const ITEM *const vehicle_item = Lara_Vehicle_GetItem(); if (lara_item != nullptr) { File_WriteString(p->file, "\n%*s", indent, ""); File_WriteString( p->file, "assert lara.pos=%d,%d,%d", lara_item->pos.x, lara_item->pos.y, lara_item->pos.z); File_WriteString(p->file, "\n%*s", indent, ""); File_WriteString( p->file, "assert lara.rot=%d,%d,%d", lara_item->rot.x, lara_item->rot.y, lara_item->rot.z); File_WriteString(p->file, "\n%*s", indent, ""); File_WriteString( p->file, "assert lara.anim=%d,%d,%d", obj_id, Item_GetRelativeObjAnim(lara_item, obj_id), Item_GetRelativeFrame(lara_item)); File_WriteString(p->file, "\n%*s", indent, ""); File_WriteString( p->file, "assert lara.speed=%d,%d", (vehicle_item != nullptr ? vehicle_item : lara_item)->speed, (vehicle_item != nullptr ? vehicle_item : lara_item)->fall_speed); } #endif File_WriteString(p->file, "\n"); p->prev_frame_idx = p->frame_idx; } static void M_DumpHeader(MYFILE *const fp) { File_WriteString(fp, "seed_control %d\n", Random_GetControlSeed()); File_WriteString(fp, "seed_draw %d\n", Random_GetDrawSeed()); } static void M_DumpArguments(MYFILE *const fp, VECTOR *const original_args) { // Record original arguments passed to the game if (original_args->count <= 0) { return; } // Skip tracking irrelevant arguments. VECTOR *const filtered_args = Vector_Create(sizeof(char *)); for (int32_t i = 0; i < original_args->count; i++) { const char *const arg = *(char **)Vector_Get(original_args, i); int32_t skip = 0; for (size_t j = 0; m_SkipArgs[j].arg != nullptr; j++) { if (strcmp(arg, m_SkipArgs[j].arg) == 0) { skip = 1 + m_SkipArgs[j].takes_value; break; } } if (skip) { i += skip - 1; } else { Vector_Add(filtered_args, &arg); } } if (filtered_args->count > 0) { File_WriteString(fp, "args"); for (int32_t i = 0; i < filtered_args->count; i++) { const char *const arg = *(char **)Vector_Get(filtered_args, i); File_WriteString(fp, " \"%s\"", arg); } File_WriteString(fp, "\n"); } Vector_Free(filtered_args); } static void M_DumpConfig(MYFILE *const fp) { // Record any non-default config options for later replay const CONFIG_OPTION *const map = Config_GetOptionMap(); VECTOR *opts = Vector_Create(sizeof(CONFIG_OPTION *)); for (const CONFIG_OPTION *opt = map; opt->name != nullptr; opt++) { if (Config_IsOptionAtDefault(opt->target)) { continue; } Vector_Add(opts, &opt); } CONFIG_OPTION **raw_opts = Vector_GetData(opts); qsort( raw_opts, opts->count, sizeof(CONFIG_OPTION *), M_CompareConfigOption); for (int32_t i = 0; i < opts->count; i++) { const CONFIG_OPTION *opt = raw_opts[i]; const char *const fmt = opt->type == COT_ENUM || opt->type == COT_STRING || opt->type == COT_DYNAMIC_ENUM ? "config %s \"%s\"\n" : "config %s %s\n"; File_WriteString( fp, fmt, opt->name, Config_GetOptionValueAsString(opt, false)); } Vector_Free(opts); } static void M_DumpBindings(MYFILE *const fp) { // Record any non-default key/controller bindings for later replay. // Keyboard binds for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) { JSON_OBJECT *bind = JSON_ObjectNew(); if (g_Input_Keyboard.assign_to_json_object( g_Config.input.keyboard_layout, role, 0, bind)) { const SDL_Scancode sc = JSON_ObjectGetInt(bind, "scancode", SDL_SCANCODE_UNKNOWN); const char *const key_desc = sc == SDL_SCANCODE_UNKNOWN ? "" : Input_KeyDescFromSDL(sc, 0); File_WriteString( fp, "bind keyboard %s \"%s\"\n", ENUM_MAP_TO_STRING(INPUT_ROLE, role), key_desc); } JSON_ObjectFree(bind); } // Controller binds for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) { JSON_OBJECT *bind = JSON_ObjectNew(); if (g_Input_Controller.assign_to_json_object( g_Config.input.controller_layout, role, 0, bind)) { const int32_t bt = JSON_ObjectGetInt(bind, "button_type", 0); const int32_t b = JSON_ObjectGetInt(bind, "bind", 0); const int32_t ad = JSON_ObjectGetInt(bind, "axis_dir", 0); File_WriteString( fp, "bind controller %s %d %d %d\n", ENUM_MAP_TO_STRING(INPUT_ROLE, role), bt, b, ad); } JSON_ObjectFree(bind); } File_WriteString(fp, "\n"); } // Callback for game events: inject synthetic SDL_USEREVENT into queue static void M_HandleGameEvent(const EVENT *const event, void *const user_data) { M_PRIV *const p = &m_Priv; if (p->file == nullptr || p->queue_size >= M_MAX_EVENTS) { return; } SDL_Event ev = { .type = SDL_USEREVENT }; ev.user.code = (strcmp(event->name, GAME_EVENT_SCREENSHOT) == 0) ? M_CUSTOM_EVENT_SCREENSHOT : M_CUSTOM_EVENT_COMMAND; ev.user.data1 = Memory_DupStr(event->data); ev.user.data2 = nullptr; p->queue[p->queue_size++] = ev; } void TestRecorder_Open(const char *path, VECTOR *const original_args) { M_PRIV *const p = &m_Priv; p->file = File_Open(path, FILE_OPEN_WRITE); if (p->file == nullptr) { LOG_ERROR("Cannot open record file '%s'", path); return; } M_DumpHeader(p->file); M_DumpArguments(p->file, original_args); M_DumpConfig(p->file); M_DumpBindings(p->file); p->listeners[0] = GameEvent_Subscribe( GAME_EVENT_SCREENSHOT, nullptr, M_HandleGameEvent, nullptr); p->listeners[1] = GameEvent_Subscribe( GAME_EVENT_COMMAND, nullptr, M_HandleGameEvent, nullptr); LOG_INFO("Starting recording"); } bool TestRecorder_IsOpened(void) { M_PRIV *const p = &m_Priv; return p->file != nullptr; } void TestRecorder_Close(void) { M_PRIV *const p = &m_Priv; if (p->file != nullptr) { File_Close(p->file); p->file = nullptr; } GameEvent_Unsubscribe(p->listeners[0]); GameEvent_Unsubscribe(p->listeners[1]); } void TestRecorder_BeginFrame(void) { M_PRIV *const p = &m_Priv; if (p->file != nullptr) { p->queue_size = 0; } } void TestRecorder_EndFrame(void) { M_PRIV *const p = &m_Priv; if (p->file != nullptr) { M_DumpQueue(p); } p->frame_idx++; } void TestRecorder_RecordEvent(const SDL_Event *const event) { M_PRIV *const p = &m_Priv; if (p->file == nullptr) { return; } // Only record eligible events if (event->type != SDL_KEYDOWN && event->type != SDL_KEYUP && event->type != SDL_QUIT && event->type != SDL_TEXTINPUT && event->type != SDL_USEREVENT) { return; } if (event->type == SDL_KEYDOWN && event->key.repeat) { return; } if (event->type == SDL_TEXTINPUT && !Console_IsOpened()) { return; } if (p->queue_size < M_MAX_EVENTS) { p->queue[p->queue_size++] = *event; } } ================================================ FILE: src/trx/game/replay/test_recorder.h ================================================ #pragma once #include #include #include // Test replay: a module to record game playthroughs. // ============================================================================ // Initialize test recorder for recording mode. // @param path Path to the recording to write to. void TestRecorder_Open(const char *path, VECTOR *original_args); // Close the recorder. void TestRecorder_Close(void); // Return whether the recording mode is currently active. bool TestRecorder_IsOpened(void); // Should be called at the start of each frame to handle skip logic. void TestRecorder_BeginFrame(void); // Record a single SDL_Event. Called for each event polled. Only essential // events are recorded. // @param event Event to record void TestRecorder_RecordEvent(const SDL_Event *event); // Should be called after processing events each frame to update skip counters. void TestRecorder_EndFrame(void); ================================================ FILE: src/trx/game/replay/test_replay.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_DEBUG 0 typedef struct { SHELL_ARGS *args; } M_PARSE_CTX; typedef struct { bool collecting; int32_t brace_depth; bool in_quote; bool escaped; } M_BLOCK_EVENT_CTX; // Parsed frame events typedef struct { int32_t frame_idx; VECTOR *events; // vector of char* } M_FRAME; // Replay private state typedef struct { char *data; // Replay file data buffer size_t size; // Size of data buffer VECTOR *headers; // Vector of char* header lines VECTOR *frames; // Vector of M_FRAME frames to play int32_t frame_idx; // Current playback frame index int32_t next_frame_idx; // Next frame to process bool replay_quiet; struct { bool seen; bool quiet_applied; LOG_LEVEL log_level_before_quiet; bool summary_printed; bool case_active; char *case_name; int32_t case_checks; int32_t case_fails; int32_t cases_passed; int32_t cases_failed; int32_t checks_passed; int32_t checks_failed; int32_t exit_code_override; bool use_ansi_colors; } test_mode; } M_PRIV; static M_PRIV m_Priv = {}; typedef bool (*M_EVENT_HANDLER)(const char *token); typedef bool (*M_HEADER_HANDLER)(const char *line, M_PARSE_CTX *ctx); // Event parsers static bool M_ParseQuitEvent(const char *event_str); static bool M_ParseKeyDownEvent(const char *event_str); static bool M_ParseKeyUpEvent(const char *event_str); static bool M_ParseTextInputEvent(const char *event_str); static bool M_ParseCommandEvent(const char *event_str); static bool M_ParseNoopEvent(const char *event_str); static bool M_ParseLuaEvent(const char *event_str); static bool M_ParseTestCaseEvent(const char *event_str); static bool M_ParseExpectEvent(const char *event_str); // Header parsers static bool M_ParseSeedControl(const char *line, M_PARSE_CTX *ctx); static bool M_ParseSeedDraw(const char *line, M_PARSE_CTX *ctx); static bool M_ParseBindKeyboard(const char *line, M_PARSE_CTX *ctx); static bool M_ParseBindController(const char *line, M_PARSE_CTX *ctx); static bool M_ParseArgs(const char *line, M_PARSE_CTX *ctx); static bool M_ParseConfig(const char *line, M_PARSE_CTX *ctx); static bool M_ParseTestCaseHeader(const char *line, M_PARSE_CTX *ctx); static const M_HEADER_HANDLER m_HeaderHandlers[] = { M_ParseSeedControl, M_ParseSeedDraw, M_ParseBindKeyboard, M_ParseBindController, M_ParseArgs, M_ParseConfig, M_ParseTestCaseHeader, nullptr, }; static const M_EVENT_HANDLER m_EventHandlers[] = { M_ParseQuitEvent, M_ParseTestCaseEvent, M_ParseExpectEvent, M_ParseKeyDownEvent, M_ParseKeyUpEvent, M_ParseTextInputEvent, M_ParseNoopEvent, M_ParseCommandEvent, M_ParseLuaEvent, nullptr, }; static void M_TestPrint(const char *const fmt, ...) { va_list va; va_start(va, fmt); vprintf(fmt, va); printf("\n"); fflush(stdout); va_end(va); } static const char *M_TestColor(const char *const color) { return m_Priv.test_mode.use_ansi_colors ? color : ""; } static const char *M_TestColorReset(void) { return m_Priv.test_mode.use_ansi_colors ? LOG_ANSI_COLOR_RESET : ""; } static void M_EndTestCase(void) { M_PRIV *const p = &m_Priv; if (!p->test_mode.case_active) { return; } if (p->test_mode.case_fails == 0) { p->test_mode.cases_passed++; M_TestPrint( "%sPASS%s | %s", M_TestColor(LOG_ANSI_COLOR_GREEN), M_TestColorReset(), p->test_mode.case_name); } else { p->test_mode.cases_failed++; M_TestPrint( "%sFAIL%s | %s (%d/%d checks failed)", M_TestColor(LOG_ANSI_COLOR_RED), M_TestColorReset(), p->test_mode.case_name, p->test_mode.case_fails, p->test_mode.case_checks); } p->test_mode.case_active = false; Memory_FreePointer(&p->test_mode.case_name); p->test_mode.case_checks = 0; p->test_mode.case_fails = 0; } static void M_TestReportSummary(void) { M_PRIV *const p = &m_Priv; if (!p->test_mode.seen || p->test_mode.summary_printed) { return; } if (p->test_mode.case_active) { M_EndTestCase(); } M_TestPrint("\n=== TEST_SUMMARY ==="); const int32_t total_cases = p->test_mode.cases_passed + p->test_mode.cases_failed; if (p->test_mode.cases_passed > 0) { M_TestPrint( "%sPASSED: %d of %d%s", M_TestColor(LOG_ANSI_COLOR_GREEN), p->test_mode.cases_passed, total_cases, M_TestColorReset()); } if (p->test_mode.cases_failed > 0) { M_TestPrint( "%sFAILED: %d of %d%s", M_TestColor(LOG_ANSI_COLOR_RED), p->test_mode.cases_failed, total_cases, M_TestColorReset()); } p->test_mode.summary_printed = true; } static void M_ApplyQuietInTestMode(void) { M_PRIV *const p = &m_Priv; if (!p->replay_quiet || !p->test_mode.seen || p->test_mode.quiet_applied) { return; } p->test_mode.log_level_before_quiet = Log_GetMinLevel(); Log_SetMinLevel((LOG_LEVEL)100); p->test_mode.quiet_applied = true; } static void M_TerminateFromTestResult(void) { M_PRIV *const p = &m_Priv; if (p->test_mode.cases_failed > 0 || p->test_mode.checks_failed > 0) { p->test_mode.exit_code_override = 1; } else { p->test_mode.exit_code_override = 0; } SDL_Event event = { .type = SDL_QUIT }; Shell_ProcessEvent(&event); } static bool M_ParseQuotedPayload( const char *const event_str, const char *const prefix, const char **const out_start, size_t *const out_len) { if (strncmp(event_str, prefix, strlen(prefix)) != 0) { return false; } const char *const start = strchr(event_str + strlen(prefix), '"'); const char *const end = start ? strrchr(start + 1, '"') : nullptr; if (start == nullptr || end == nullptr || end <= start + 1) { return false; } *out_start = start + 1; *out_len = (size_t)(end - (start + 1)); return true; } static const char *M_SkipWhitespaceConst(const char *const s) { const char *p = s; while (*p == ' ' || *p == '\t') { p++; } return p; } static char *M_TrimWhitespaceInPlace(char *const s) { char *start = s; while (*start == ' ' || *start == '\t' || *start == '\n' || *start == '\r') { start++; } char *end = start + strlen(start); while (end > start && (end[-1] == ' ' || end[-1] == '\t' || end[-1] == '\n' || end[-1] == '\r')) { end--; } *end = '\0'; if (start != s) { memmove(s, start, strlen(start) + 1); } return s; } static void M_ScanBraceState( const char *const s, int32_t *const io_brace_depth, bool *const io_in_quote, bool *const io_escaped) { for (const char *p = s; *p != '\0'; p++) { if (*io_in_quote) { if (*io_escaped) { *io_escaped = false; continue; } if (*p == '\\') { *io_escaped = true; continue; } if (*p == '"') { *io_in_quote = false; } continue; } if (*p == '"') { *io_in_quote = true; continue; } if (*p == '{') { (*io_brace_depth)++; continue; } if (*p == '}') { (*io_brace_depth)--; continue; } } } static const char *M_GetBlockPayloadStartIfAny(const char *const evt) { if (strncmp(evt, "expect ", strlen("expect ")) == 0) { return M_SkipWhitespaceConst(evt + strlen("expect ")); } if (strncmp(evt, "lua ", strlen("lua ")) == 0) { return M_SkipWhitespaceConst(evt + strlen("lua ")); } if (strncmp(evt, "cmd ", strlen("cmd ")) == 0) { return M_SkipWhitespaceConst(evt + strlen("cmd ")); } return nullptr; } static bool M_TryStartBlockEvent( M_BLOCK_EVENT_CTX *const ctx, const char *const evt) { const char *const payload_start = M_GetBlockPayloadStartIfAny(evt); if (payload_start == nullptr || *payload_start != '{') { return false; } ctx->collecting = true; ctx->brace_depth = 0; ctx->in_quote = false; ctx->escaped = false; M_ScanBraceState( payload_start, &ctx->brace_depth, &ctx->in_quote, &ctx->escaped); if (ctx->brace_depth == 0) { ctx->collecting = false; } return true; } static bool M_GetBracedPayload( const char *const event_str, const char *const prefix, const char **const out_start, size_t *const out_len) { if (strncmp(event_str, prefix, strlen(prefix)) != 0) { return false; } const char *p = M_SkipWhitespaceConst(event_str + strlen(prefix)); if (*p != '{') { return false; } const char *payload_start = p + 1; int32_t depth = 1; bool in_quote = false; bool escaped = false; p++; for (; *p != '\0'; p++) { if (in_quote) { if (escaped) { escaped = false; continue; } if (*p == '\\') { escaped = true; continue; } if (*p == '"') { in_quote = false; } continue; } if (*p == '"') { in_quote = true; continue; } if (*p == '{') { depth++; continue; } if (*p == '}') { depth--; if (depth == 0) { const char *trail = M_SkipWhitespaceConst(p + 1); if (*trail != '\0') { return false; } *out_start = payload_start; *out_len = (size_t)(p - payload_start); return true; } } } return false; } static bool M_ParseQuitEvent(const char *const event_str) { if (strcmp(event_str, "quit") != 0) { return false; } if (m_Priv.test_mode.seen) { M_TestReportSummary(); M_TerminateFromTestResult(); } SDL_Event event = { .type = SDL_QUIT }; Shell_ProcessEvent(&event); return true; } // Consolidate keydown/keyup parsing into a single helper static bool M_ParseKeyEvent( const char *event_str, SDL_EventType type, const char *prefix) { if (strncmp(event_str, prefix, strlen(prefix)) != 0) { return false; } SDL_Event event = { .type = type }; const char *p = event_str + strlen(prefix); const char *start = strchr(p, '"'); const char *end = start ? strrchr(start + 1, '"') : nullptr; if (!start || !end || end <= start + 1) { LOG_WARNING("Malformed %s instruction: %s", prefix, event_str); return false; } const size_t slen = end - (start + 1); const char *desc = String_FormatStatic("%.*s", slen, start + 1); SDL_Keymod mod; if (!Input_ParseKeyDesc(desc, &event.key.keysym.scancode, &mod)) { return false; } event.key.keysym.mod = mod; event.key.keysym.sym = SDL_GetKeyFromScancode(event.key.keysym.scancode); Shell_ProcessEvent(&event); return true; } static bool M_ParseKeyDownEvent(const char *event_str) { return M_ParseKeyEvent(event_str, SDL_KEYDOWN, "●"); } static bool M_ParseKeyUpEvent(const char *event_str) { return M_ParseKeyEvent(event_str, SDL_KEYUP, "○"); } static bool M_ParseTextInputEvent(const char *const event_str) { SDL_Event event = { .type = SDL_TEXTINPUT }; const char *const fmt = String_FormatStatic( "text-input \"%%%d[^\"]\"", SDL_TEXTEDITINGEVENT_TEXT_SIZE - 1); if (sscanf(event_str, fmt, &event.text.text) != 1) { return false; } Shell_ProcessEvent(&event); return true; } static bool M_ParseNoopEvent(const char *const event_str) { // No-op event for inline comments and empty frame markers if (strncmp(event_str, "noop", 4) != 0) { return false; } return true; } static bool M_ParseTestCaseEvent(const char *const event_str) { M_PRIV *const p = &m_Priv; const char *name_start = nullptr; size_t name_len = 0; if (!M_ParseQuotedPayload(event_str, "testcase ", &name_start, &name_len)) { return false; } p->test_mode.seen = true; M_ApplyQuietInTestMode(); if (p->test_mode.case_active) { M_EndTestCase(); } Memory_FreePointer(&p->test_mode.case_name); p->test_mode.case_name = String_Format("%.*s", (int)name_len, name_start); p->test_mode.case_checks = 0; p->test_mode.case_fails = 0; p->test_mode.case_active = true; return true; } static bool M_ParseTestCaseHeader(const char *const line, M_PARSE_CTX *const) { return M_ParseTestCaseEvent(line); } static bool M_ParseExpectEvent(const char *const event_str) { M_PRIV *const p = &m_Priv; const char *const prefix = "expect "; if (strncmp(event_str, prefix, strlen(prefix)) != 0) { return false; } const char *expr_start = nullptr; size_t expr_len = 0; if (!M_GetBracedPayload(event_str, prefix, &expr_start, &expr_len)) { expr_start = M_SkipWhitespaceConst(event_str + strlen(prefix)); if (*expr_start == '\0') { return false; } expr_len = strlen(expr_start); } p->test_mode.seen = true; M_ApplyQuietInTestMode(); if (!p->test_mode.case_active) { M_TestPrint( "%sFAIL%s | expect outside test case", M_TestColor(LOG_ANSI_COLOR_RED), M_TestColorReset()); p->test_mode.checks_failed++; p->test_mode.cases_failed++; return true; } char *const expr = String_Format("%.*s", (int)expr_len, expr_start); char *const script = String_Format( "if not ((function() return %s\n end)()) then error('expect failed') " "end", expr); LUA_RESULT eval_result = Lua_Eval(script); p->test_mode.case_checks++; if (eval_result.code == LUA_OK) { p->test_mode.checks_passed++; } else { p->test_mode.checks_failed++; p->test_mode.case_fails++; M_TestPrint( "%sFAIL%s | %s | expect \"%s\" | %s", M_TestColor(LOG_ANSI_COLOR_RED), M_TestColorReset(), p->test_mode.case_name, expr, eval_result.message != nullptr ? eval_result.message : "lua error"); } Lua_FreeResult(&eval_result); Memory_Free(script); Memory_Free(expr); return true; } static bool M_ParseCommandEvent(const char *const event_str) { const char *const prefix = "cmd "; if (strncmp(event_str, prefix, strlen(prefix)) != 0) { return false; } const char *payload_start = nullptr; size_t payload_len = 0; if (M_GetBracedPayload(event_str, prefix, &payload_start, &payload_len)) { char *const cmd_str = String_Format("%.*s", (int)payload_len, payload_start); M_TrimWhitespaceInPlace(cmd_str); Console_Eval(cmd_str); Memory_Free(cmd_str); return true; } if (M_ParseQuotedPayload(event_str, prefix, &payload_start, &payload_len)) { char *const cmd_str = String_Format("%.*s", (int)payload_len, payload_start); Console_Eval(cmd_str); Memory_Free(cmd_str); return true; } payload_start = M_SkipWhitespaceConst(event_str + strlen(prefix)); payload_len = strlen(payload_start); if (payload_len == 0) { LOG_WARNING("Malformed cmd instruction: %s", event_str); return false; } char *const cmd_str = String_Format("%.*s", (int)payload_len, payload_start); M_TrimWhitespaceInPlace(cmd_str); if (cmd_str[0] == '\0') { LOG_WARNING("Malformed cmd instruction: %s", event_str); Memory_Free(cmd_str); return false; } Console_Eval(cmd_str); Memory_Free(cmd_str); return true; } static bool M_ParseLuaEvent(const char *const event_str) { M_PRIV *const p = &m_Priv; if (strncmp(event_str, "lua ", 4) != 0) { return false; } const char *chunk_start = nullptr; size_t chunk_len = 0; LUA_RESULT eval_result = {}; if (M_GetBracedPayload(event_str, "lua ", &chunk_start, &chunk_len)) { char *const chunk = String_Format("%.*s", (int)chunk_len, chunk_start); eval_result = Lua_Eval(chunk); Memory_Free(chunk); } else { eval_result = Lua_Eval(event_str + 4); } if (eval_result.code == LUA_ERRSYNTAX) { LOG_ERROR( "LUA syntax error on frame %d: %s", p->frame_idx, eval_result.message); Shell_Terminate(1); } else if (eval_result.code != LUA_OK) { LOG_ERROR( "LUA error on frame %d: %s", p->frame_idx, eval_result.message); Shell_Terminate(1); } Lua_FreeResult(&eval_result); return true; } static bool M_ParseEvent(const char *const event_str) { for (int32_t i = 0; m_EventHandlers[i] != nullptr; i++) { if (m_EventHandlers[i](event_str)) { return true; } } return false; } static bool M_ParseSeedControl(const char *const line, M_PARSE_CTX *const ctx) { int32_t val; if (sscanf(line, "seed_control %d", &val) == 1) { Random_SeedControl(val); return true; } return false; } static bool M_ParseSeedDraw(const char *const line, M_PARSE_CTX *const ctx) { int32_t val; if (sscanf(line, "seed_draw %d", &val) == 1) { Random_SeedDraw(val); return true; } return false; } static bool M_ParseBindKeyboard(const char *const line, M_PARSE_CTX *const ctx) { const char *prefix = "bind keyboard "; if (strncmp(line, prefix, strlen(prefix)) != 0) { return false; } const char *p = line + strlen(prefix); const char *q = strchr(p, ' '); if (q == nullptr) { return false; } const char *role_str = String_FormatStatic("%.*s", (int)(q - p), p); const INPUT_ROLE role = ENUM_MAP_GET(INPUT_ROLE, role_str, -1); if (role == (INPUT_ROLE)-1) { return false; } const char *const start = strchr(p, '"'); const char *const end = start ? strrchr(start + 1, '"') : nullptr; if (start == nullptr || end == nullptr || end < start + 1) { LOG_WARNING("Malformed bind keyboard instruction: %s", line); return false; } const size_t slen = end - (start + 1); const char *desc = String_FormatStatic("%.*s", slen, start + 1); SDL_Scancode sc = SDL_SCANCODE_UNKNOWN; SDL_Keymod mod = KMOD_NONE; if (desc[0] != '\0') { if (!Input_ParseKeyDesc(desc, &sc, &mod)) { return false; } } JSON_OBJECT *const bind = JSON_ObjectNew(); JSON_ObjectAppendInt(bind, "scancode", sc); JSON_ObjectAppendInt(bind, "mod", mod); g_Input_Keyboard.assign_from_json_object( g_Config.input.keyboard_layout, role, 0, bind); JSON_ObjectFree(bind); return true; } static bool M_ParseBindController( const char *const line, M_PARSE_CTX *const ctx) { const char *prefix = "bind controller "; if (strncmp(line, prefix, strlen(prefix)) != 0) { return false; } const char *p = line + strlen(prefix); const char *q = strchr(p, ' '); if (q == nullptr) { return false; } const char *role_str = String_FormatStatic("%.*s", (int)(q - p), p); const INPUT_ROLE role = (INPUT_ROLE)ENUM_MAP_GET(INPUT_ROLE, role_str, (int32_t)(INPUT_ROLE)-1); if (role == (INPUT_ROLE)-1) { return false; } int32_t bt, b, ad; if (sscanf(q + 1, "%d %d %d", &bt, &b, &ad) == 3) { JSON_OBJECT *bind = JSON_ObjectNew(); JSON_ObjectAppendInt(bind, "button_type", bt); JSON_ObjectAppendInt(bind, "bind", b); JSON_ObjectAppendInt(bind, "axis_dir", ad); g_Input_Controller.assign_from_json_object( g_Config.input.controller_layout, role, 0, bind); JSON_ObjectFree(bind); return true; } return false; } static bool M_ParseArgs(const char *const line, M_PARSE_CTX *const ctx) { if (strncmp(line, "args", 4) != 0) { return false; } if (ctx->args != nullptr) { Shell_FreeArgs(ctx->args); ctx->args = nullptr; } // Build an owned argv vector for Shell_ParseArgs adoption. VECTOR *raw_args = Vector_Create(sizeof(const char *)); const char *p = line + 4; while (*p != '\0') { while (isspace((unsigned char)*p)) { p++; } if (*p != '"') { break; } p++; const char *start = p; while (*p != '\0' && *p != '"') { p++; } const ptrdiff_t len = p - start; char tmp[len + 1]; memcpy(tmp, start, len); tmp[len] = '\0'; char *arg = Memory_DupStr(tmp); Vector_Add(raw_args, &arg); if (*p == '"') { p++; } } ctx->args = Shell_ParseArgs(raw_args); return true; } static bool M_ParseConfig(const char *const line, M_PARSE_CTX *const ctx) { char keybuf[64]; char valbuf[128]; if (sscanf(line, "config %63s %127s", keybuf, valbuf) == 2) { // Strip surrounding quotes from the value, if present size_t vlen = strlen(valbuf); if (vlen >= 2 && valbuf[0] == '"' && valbuf[vlen - 1] == '"') { valbuf[vlen - 1] = '\0'; memmove(valbuf, valbuf + 1, vlen - 1); } const CONFIG_OPTION *opt = Config_GetOptionByPath(keybuf); if (opt) { Config_SetOptionValueFromString(opt, valbuf); } else { LOG_WARNING("Unknown option: %s", keybuf); } return true; } return false; } static void M_StripInlineComment(char *const line) { bool in_quote = false; char *p; for (p = line; *p != '\0'; p++) { if (*p == '"') { in_quote = !in_quote; } else if (*p == '#' && !in_quote) { *p = '\0'; break; } } // Trim trailing whitespace { char *end = line + strlen(line); while (end > line && (end[-1] == ' ' || end[-1] == '\t')) { end[-1] = '\0'; end--; } } } static char *M_SkipWhitespace(char *const line) { char *start = line; while (*start == ' ' || *start == '\t') { start++; } return start; } static bool M_IsFrameMarkerLine(const char *const line) { int32_t delta = 0; return sscanf(line, "@+%d:", &delta) == 1; } SHELL_ARGS *TestReplay_Open(const char *path) { M_PRIV *const p = &m_Priv; Memory_FreePointer(&p->test_mode.case_name); memset(p, 0, sizeof(m_Priv)); p->replay_quiet = Log_GetMinLevel() >= LOG_LEVEL_WARNING; p->test_mode.log_level_before_quiet = Log_GetMinLevel(); p->test_mode.use_ansi_colors = Log_ShouldUseAnsiColors(); p->test_mode.exit_code_override = -1; char *data = nullptr; size_t size = 0; if (!File_Load(path, &data, &size)) { Shell_ExitSystemFmt("Cannot open replay file '%s'", path); return nullptr; } p->data = data; p->size = size; // Split file into lines by replacing '\n' with '\0' char *end = data + size; for (char *ch = data; ch < end; ch++) { if (*ch == '\n') { *ch = '\0'; } } // Collect non-empty, comment-stripped lines VECTOR *lines = Vector_Create(sizeof(char *)); for (char *line = data; line < end;) { char *const next_line = line + strlen(line) + 1; M_StripInlineComment(line); char *start = M_SkipWhitespace(line); if (*start != '\0') { Vector_Add(lines, &start); } line = next_line; } // Parse and execute headers p->headers = Vector_Create(sizeof(char *)); int32_t idx = 0; while (idx < lines->count) { char *const ln = *(char **)Vector_Get(lines, idx); int32_t delta = 0; if (sscanf(ln, "@+%d:", &delta) == 1) { break; } Vector_Add(p->headers, &ln); idx++; } // Parse frames and their events p->frames = Vector_Create(sizeof(M_FRAME)); p->next_frame_idx = 0; p->frame_idx = 0; int32_t last_frame = 0; while (idx < lines->count) { char *ln = *(char **)Vector_Get(lines, idx); int32_t delta = 0; if (sscanf(ln, "@+%d:", &delta) == 1) { M_FRAME frame = { .frame_idx = last_frame + delta, .events = Vector_Create(sizeof(char *)), }; M_BLOCK_EVENT_CTX block_ctx = {}; // Primary event on same line char *const colon = strchr(ln, ':'); if (colon != nullptr) { char *const evt = M_SkipWhitespace(colon + 1); if (*evt != '\0') { Vector_Add(frame.events, &evt); M_TryStartBlockEvent(&block_ctx, evt); } } // Continued events idx++; while (idx < lines->count) { char *const cont = *(char **)Vector_Get(lines, idx); if (M_IsFrameMarkerLine(cont)) { // Reached next frame - stop break; } char *const evt = M_SkipWhitespace(cont); if (*evt == '\0') { idx++; continue; } if (block_ctx.collecting) { if (evt > p->data) { evt[-1] = '\n'; } M_ScanBraceState( evt, &block_ctx.brace_depth, &block_ctx.in_quote, &block_ctx.escaped); if (block_ctx.brace_depth == 0) { block_ctx.collecting = false; } idx++; continue; } Vector_Add(frame.events, &evt); M_TryStartBlockEvent(&block_ctx, evt); idx++; } Vector_Add(p->frames, &frame); last_frame = frame.frame_idx; continue; } idx++; } M_PARSE_CTX ctx = {}; for (int32_t i = 0; i < p->headers->count; i++) { const char *const ln = *(const char **)Vector_Get(p->headers, i); M_ParseArgs(ln, &ctx); } Vector_Free(lines); LOG_INFO("Loaded %zu frames for playback", p->frames->count); if (ctx.args == nullptr) { ctx.args = Shell_ParseArgs(nullptr); } if (ctx.args != nullptr && ctx.args->quiet) { p->replay_quiet = true; } return ctx.args; } void TestReplay_Start(void) { M_PARSE_CTX ctx = {}; M_PRIV *const p = &m_Priv; for (int32_t i = 0; i < p->headers->count; i++) { const char *const ln = *(const char **)Vector_Get(p->headers, i); bool handled = false; for (int32_t j = 0; m_HeaderHandlers[j]; j++) { if (m_HeaderHandlers[j](ln, &ctx)) { handled = true; break; } } if (!handled) { LOG_WARNING("Unknown line: %s", ln); } } g_SavedConfig = g_Config; Shell_FreeArgs(ctx.args); } void TestReplay_Close(void) { M_PRIV *const p = &m_Priv; M_TestReportSummary(); if (p->test_mode.quiet_applied) { Log_SetMinLevel(p->test_mode.log_level_before_quiet); p->test_mode.quiet_applied = false; } Memory_FreePointer(&p->test_mode.case_name); if (p->headers) { Vector_Free(p->headers); p->headers = nullptr; } if (p->frames) { for (int32_t i = 0; i < p->frames->count; i++) { M_FRAME *const f = Vector_Get(p->frames, i); Vector_Free(f->events); } Vector_Free(p->frames); p->frames = nullptr; } if (p->data) { Memory_Free(p->data); p->data = nullptr; } } bool TestReplay_IsOpened(void) { M_PRIV *const p = &m_Priv; return p->frames != nullptr; } void TestReplay_RunFrame(void) { M_PRIV *const p = &m_Priv; if (!TestReplay_IsOpened()) { return; } while (p->next_frame_idx < p->frames->count) { M_FRAME *const f = Vector_Get(p->frames, p->next_frame_idx); if (f->frame_idx != p->frame_idx) { break; } for (int32_t j = 0; j < f->events->count; j++) { const char *const evt = *(char **)Vector_Get(f->events, j); if (!M_ParseEvent(evt)) { LOG_WARNING( "Unknown replay event on frame %d: %s", p->frame_idx, evt); } } p->next_frame_idx++; } p->frame_idx++; if (p->test_mode.seen && p->next_frame_idx >= p->frames->count) { M_TestReportSummary(); M_TerminateFromTestResult(); } } int32_t TestReplay_GetExitCodeOverride(void) { return m_Priv.test_mode.exit_code_override; } ================================================ FILE: src/trx/game/replay/test_replay.h ================================================ #pragma once #include // Test replay: a module to simulate game playthroughs. // ============================================================================ // Initialize test replay for playback mode. // @param path Path to the recording to play from. // @return Parsed shell arguments from the replay file, or nullptr on // error. SHELL_ARGS *TestReplay_Open(const char *path); // Executes the initial configuration headers after the system is done // initializing. void TestReplay_Start(void); // Shutdown test replay. void TestReplay_Close(void); // Return whether the replay mode is currently active. bool TestReplay_IsOpened(void); // Run all events associated with the given frame. void TestReplay_RunFrame(void); // Returns -1 when replay does not override process exit code. // Otherwise 0/1 for replay-driven pass/fail status. int32_t TestReplay_GetExitCodeOverride(void); ================================================ FILE: src/trx/game/rooms/common.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static int32_t m_RoomCount = 0; static ROOM *m_Rooms = nullptr; static bool m_FlipStatus = false; static int32_t m_FlipEffect = -1; static int32_t m_FlipTimer = 0; static int32_t m_FlipSlotFlags[MAX_FLIP_MAPS] = {}; #define M_OUTSIDE_TABLE_STEP_SHIFT 2 #define M_OUTSIDE_TABLE_STEP (1 << M_OUTSIDE_TABLE_STEP_SHIFT) #define M_OUTSIDE_TABLE_BLOCK_SHIFT (WALL_SHIFT + M_OUTSIDE_TABLE_STEP_SHIFT) #define M_OUTSIDE_TABLE_MAX_ROOMS_PER_CELL 64 #define M_OUTSIDE_TABLE_SENTINEL NO_ROOM #define M_OUTSIDE_OFFSET_EMPTY 0xFFFF static int16_t *m_OutsideRoomTable = nullptr; static uint16_t *m_OutsideRoomOffsets = nullptr; static int32_t m_OutsideGridX = 0; static int32_t m_OutsideGridZ = 0; static int32_t m_OutsideOriginCellX = 0; static int32_t m_OutsideOriginCellZ = 0; static void M_AddFlipItems(const ROOM *const room) { int16_t item_num = room->item_num; while (item_num != NO_ITEM) { ITEM *const item = Item_Get(item_num); const OBJECT *const obj = Object_Get(item->object_id); if (obj->handle_flip_func != nullptr) { obj->handle_flip_func(item, RFS_FLIPPED); } item_num = item->next_item; } } static void M_RemoveFlipItems(const ROOM *const room) { int16_t item_num = room->item_num; while (item_num != NO_ITEM) { ITEM *const item = Item_Get(item_num); const OBJECT *const obj = Object_Get(item->object_id); if (obj->handle_flip_func != nullptr) { obj->handle_flip_func(item, RFS_UNFLIPPED); } // TR2 does not have land/water objects like crocodile/alligator in TR1, // so avoid instances of floating water creatures in drained rooms. if (g_TRVersion >= 2 && (item->flags & IF_ONE_SHOT) && obj->intelligent && item->hit_points <= 0) { Item_RemoveDrawn(item_num); item->flags |= IF_KILLED; } item_num = item->next_item; } } static void M_GetNewRoom( const int32_t x, const int32_t y, const int32_t z, int16_t room_num) { Room_GetSector((XYZ_32) { x, y, z }, &room_num); Room_MarkToBeDrawn(room_num); } void Room_InitialiseRooms(const int32_t num_rooms) { m_RoomCount = num_rooms; m_Rooms = num_rooms == 0 ? nullptr : GameBuf_Alloc(sizeof(ROOM) * num_rooms, GBUF_ROOMS); m_OutsideRoomTable = nullptr; m_OutsideRoomOffsets = nullptr; m_OutsideGridX = 0; m_OutsideGridZ = 0; m_OutsideOriginCellX = 0; m_OutsideOriginCellZ = 0; } void Room_Shutdown(void) { m_RoomCount = 0; m_Rooms = nullptr; m_FlipStatus = false; m_FlipEffect = -1; m_FlipTimer = 0; memset(m_FlipSlotFlags, 0, sizeof(m_FlipSlotFlags)); m_OutsideRoomTable = nullptr; m_OutsideRoomOffsets = nullptr; m_OutsideGridX = 0; m_OutsideGridZ = 0; m_OutsideOriginCellX = 0; m_OutsideOriginCellZ = 0; } int32_t Room_GetCount(void) { return m_RoomCount; } ROOM *Room_Get(const int32_t room_num) { if (m_Rooms == nullptr) { return nullptr; } if (room_num < 0 || room_num >= Room_GetCount()) { return nullptr; } return &m_Rooms[room_num]; } void Room_BuildOutsideTable(void) { m_OutsideRoomTable = nullptr; m_OutsideRoomOffsets = nullptr; m_OutsideGridX = 0; m_OutsideGridZ = 0; m_OutsideOriginCellX = 0; m_OutsideOriginCellZ = 0; const int32_t num_rooms = Room_GetCount(); if (num_rooms <= 0) { return; } { int32_t min_x = INT32_MAX; int32_t min_z = INT32_MAX; int32_t max_x = INT32_MIN; int32_t max_z = INT32_MIN; for (int32_t i = 0; i < num_rooms; i++) { const ROOM *const room = Room_Get(i); if (room == nullptr) { continue; } min_x = MIN(min_x, room->pos.x); min_z = MIN(min_z, room->pos.z); max_x = MAX(max_x, room->pos.x + (room->size.x << WALL_SHIFT)); max_z = MAX(max_z, room->pos.z + (room->size.z << WALL_SHIFT)); } m_OutsideOriginCellX = (min_x >> M_OUTSIDE_TABLE_BLOCK_SHIFT) - 1; m_OutsideOriginCellZ = (min_z >> M_OUTSIDE_TABLE_BLOCK_SHIFT) - 1; const int32_t max_cell_x = (max_x >> M_OUTSIDE_TABLE_BLOCK_SHIFT) + 1; const int32_t max_cell_z = (max_z >> M_OUTSIDE_TABLE_BLOCK_SHIFT) + 1; m_OutsideGridX = max_cell_x - m_OutsideOriginCellX + 1; m_OutsideGridZ = max_cell_z - m_OutsideOriginCellZ + 1; if (m_OutsideGridX < 1) { m_OutsideGridX = 1; } if (m_OutsideGridZ < 1) { m_OutsideGridZ = 1; } } const int32_t full_table_size = m_OutsideGridX * m_OutsideGridZ * M_OUTSIDE_TABLE_MAX_ROOMS_PER_CELL; int16_t *full_table = Memory_Alloc(full_table_size * sizeof(int16_t)); for (int32_t i = 0; i < full_table_size; i++) { full_table[i] = M_OUTSIDE_TABLE_SENTINEL; } const int32_t blocks_x = m_OutsideGridX * M_OUTSIDE_TABLE_STEP; const int32_t blocks_z = m_OutsideGridZ * M_OUTSIDE_TABLE_STEP; for (int32_t y = 0; y < blocks_x; y += M_OUTSIDE_TABLE_STEP) { for (int32_t x = 0; x < blocks_z; x += M_OUTSIDE_TABLE_STEP) { for (int32_t i = 0; i < num_rooms; i++) { const ROOM *const room = Room_Get(i); const int32_t room_x = (room->pos.z >> WALL_SHIFT) - (m_OutsideOriginCellZ * M_OUTSIDE_TABLE_STEP); const int32_t room_y = (room->pos.x >> WALL_SHIFT) - (m_OutsideOriginCellX * M_OUTSIDE_TABLE_STEP); bool cont = false; for (int32_t ry = 0; ry < M_OUTSIDE_TABLE_STEP && !cont; ry++) { for (int32_t rx = 0; rx < M_OUTSIDE_TABLE_STEP; rx++) { if (x + rx >= room_x && x + rx < room_x + room->size.z && y + ry >= room_y && y + ry < room_y + room->size.x) { cont = true; break; } } } if (!cont) { continue; } int16_t *const cell = &full_table [M_OUTSIDE_TABLE_MAX_ROOMS_PER_CELL * ((x >> M_OUTSIDE_TABLE_STEP_SHIFT) + m_OutsideGridZ * (y >> M_OUTSIDE_TABLE_STEP_SHIFT))]; for (int32_t lp = 0; lp < M_OUTSIDE_TABLE_MAX_ROOMS_PER_CELL; lp++) { if (cell[lp] == M_OUTSIDE_TABLE_SENTINEL) { cell[lp] = i; break; } } } } } const int32_t offset_count = m_OutsideGridX * m_OutsideGridZ; m_OutsideRoomOffsets = GameBuf_Alloc( sizeof(uint16_t) * (size_t)offset_count, GBUF_OUTSIDE_ROOM_TABLE); for (int32_t i = 0; i < offset_count; i++) { m_OutsideRoomOffsets[i] = M_OUTSIDE_OFFSET_EMPTY; } m_OutsideRoomTable = GameBuf_Alloc( full_table_size * sizeof(int16_t), GBUF_OUTSIDE_ROOM_TABLE); int16_t *const out_base = m_OutsideRoomTable; int16_t *out_ptr = out_base; for (int32_t y = 0; y < m_OutsideGridX; y++) { for (int32_t x = 0; x < m_OutsideGridZ; x++) { const int32_t cell_idx = x + y * m_OutsideGridZ; const int16_t *const cell = &full_table[M_OUTSIDE_TABLE_MAX_ROOMS_PER_CELL * cell_idx]; int32_t count = 0; while (count < M_OUTSIDE_TABLE_MAX_ROOMS_PER_CELL && cell[count] != M_OUTSIDE_TABLE_SENTINEL) { count++; } if (count == 0) { continue; } if (count == 1) { m_OutsideRoomOffsets[cell_idx] = 0x8000U | (uint16_t)cell[0]; continue; } int16_t *scan = out_base; while (scan < out_ptr) { if (memcmp(scan, cell, count * sizeof(int16_t)) == 0) { m_OutsideRoomOffsets[cell_idx] = (uint16_t)(scan - out_base); break; } int32_t scan_len = 0; while (scan[scan_len] != M_OUTSIDE_TABLE_SENTINEL) { scan_len++; } scan += scan_len + 1; } if (scan < out_ptr) { continue; } const int32_t new_off = (int32_t)(out_ptr - out_base); ASSERT(new_off >= 0); ASSERT(new_off < 0x8000); m_OutsideRoomOffsets[cell_idx] = new_off; ASSERT(new_off + count + 1 <= full_table_size); memcpy(out_ptr, cell, count * sizeof(int16_t)); out_ptr += count; *out_ptr++ = M_OUTSIDE_TABLE_SENTINEL; } } Memory_FreePointer(&full_table); } int32_t Room_GetOutsideStatus(const XYZ_32 pos, int16_t *const out_room_num) { if (out_room_num != nullptr) { *out_room_num = NO_ROOM; } if (m_OutsideRoomTable == nullptr || m_OutsideRoomOffsets == nullptr) { return -2; } const int32_t cell_x = (pos.x >> M_OUTSIDE_TABLE_BLOCK_SHIFT) - m_OutsideOriginCellX; const int32_t cell_z = (pos.z >> M_OUTSIDE_TABLE_BLOCK_SHIFT) - m_OutsideOriginCellZ; if (cell_x < 0 || cell_x >= m_OutsideGridX || cell_z < 0 || cell_z >= m_OutsideGridZ) { return -2; } const uint16_t entry = m_OutsideRoomOffsets[m_OutsideGridZ * cell_x + cell_z]; if (entry == M_OUTSIDE_OFFSET_EMPTY) { return -2; } const int16_t *p = nullptr; int16_t single_room = M_OUTSIDE_TABLE_SENTINEL; if ((entry & 0x8000U) != 0U) { single_room = (int16_t)(entry & ~0x8000U); } else { p = &m_OutsideRoomTable[entry]; } while (true) { int16_t candidate_room_num; if (p != nullptr) { if (*p == M_OUTSIDE_TABLE_SENTINEL) { break; } candidate_room_num = (int16_t)(*p); p++; } else { if (single_room == M_OUTSIDE_TABLE_SENTINEL) { break; } candidate_room_num = single_room; single_room = M_OUTSIDE_TABLE_SENTINEL; } const ROOM *const room = Room_Get(candidate_room_num); if (room == nullptr) { continue; } if (pos.y <= room->max_ceiling || pos.y >= room->min_floor) { continue; } if (pos.z <= room->pos.z + WALL_L || pos.z >= room->pos.z + (room->size.z << WALL_SHIFT) - WALL_L) { continue; } if (pos.x <= room->pos.x + WALL_L || pos.x >= room->pos.x + (room->size.x << WALL_SHIFT) - WALL_L) { continue; } int16_t rn = candidate_room_num; const SECTOR *const sector = Room_GetSector(pos, &rn); const int32_t floor = Room_GetHeight(sector, pos); if (floor == NO_HEIGHT || pos.y > floor) { return -2; } const int32_t ceiling = Room_GetCeiling(sector, pos); if (pos.y < ceiling) { return -2; } if (!room->flags.underwater && !room->flags.wind) { return -3; } if (out_room_num != nullptr) { *out_room_num = candidate_room_num; } return 1; } return -2; } int32_t Room_GetNumber(const ROOM *const room) { if (room == nullptr) { return NO_ROOM; } return room - m_Rooms; } void Room_InitialiseFlipStatus(void) { for (int32_t i = 0; i < Room_GetCount(); i++) { ROOM *const room = Room_Get(i); // Some level data links only one side of a flip pair. In that case, a // previous iteration may already have marked this room as flipped. if (room->flip_status == RFS_FLIPPED) { continue; } if (room->flipped_room == NO_ROOM) { room->flip_status = RFS_NONE; } else { ROOM *const flipped_room = Room_Get(room->flipped_room); room->flip_status = RFS_UNFLIPPED; flipped_room->flip_status = RFS_FLIPPED; } } m_FlipStatus = false; m_FlipEffect = -1; m_FlipTimer = 0; for (int32_t i = 0; i < MAX_FLIP_MAPS; i++) { m_FlipSlotFlags[i] = 0; } } void Room_FlipMap(void) { Walkable_Reset(); for (int32_t i = 0; i < Room_GetCount(); i++) { ROOM *const room = Room_Get(i); if (room->flipped_room < 0) { continue; } M_RemoveFlipItems(room); ROOM *const flipped = Room_Get(room->flipped_room); const ROOM temp = *room; *room = *flipped; *flipped = temp; room->flipped_room = flipped->flipped_room; room->flip_status = RFS_UNFLIPPED; flipped->flipped_room = NO_ROOM; flipped->flip_status = RFS_FLIPPED; room->item_num = flipped->item_num; room->effect_num = flipped->effect_num; memcpy( &room->drawn_items, &flipped->drawn_items, sizeof(room->drawn_items)); M_AddFlipItems(room); } m_FlipStatus = !m_FlipStatus; for (int32_t i = 0; i < Room_GetCount(); i++) { const ROOM *const room = Room_Get(i); if (room->flip_status != RFS_NONE) { Output_DispatchRoomFlip(room); } } Level_Finalize_LoadWalkables(Level_Context_Get()); } bool Room_GetFlipStatus(void) { return m_FlipStatus; } int32_t Room_GetFlipEffect(void) { return m_FlipEffect; } void Room_SetFlipEffect(const int32_t flip_effect) { m_FlipEffect = flip_effect; } int32_t Room_GetFlipTimer(void) { return m_FlipTimer; } void Room_SetFlipTimer(const int32_t flip_timer) { m_FlipTimer = flip_timer; } void Room_IncrementFlipTimer(const int32_t num_frames) { m_FlipTimer += num_frames; } int32_t Room_GetFlipSlotFlags(const int32_t slot_idx) { return m_FlipSlotFlags[slot_idx]; } void Room_SetFlipSlotFlags(const int32_t slot_idx, const int32_t flags) { m_FlipSlotFlags[slot_idx] = flags; } int32_t Room_GetAdjoiningRooms( int16_t init_room_num, int16_t out_room_nums[], const int32_t max_room_num_count) { int32_t count = 0; if (max_room_num_count >= 1) { out_room_nums[count++] = init_room_num; } const PORTALS *const portals = Room_Get(init_room_num)->portals; if (portals != nullptr) { for (int32_t i = 0; i < portals->count; i++) { if (count >= max_room_num_count) { break; } const int16_t room_num = portals->portal[i].room_num; out_room_nums[count++] = room_num; } } return count; } bool Room_PointInside(const ROOM *const room, const XYZ_32 point) { if (room == nullptr) { return false; } const BOUNDS_32 bounds = Room_GetRoomBounds(room); const int32_t x1 = bounds.min.x; const int32_t y1 = bounds.min.y; const int32_t z1 = bounds.min.z; const int32_t x2 = bounds.max.x; const int32_t y2 = bounds.max.y; const int32_t z2 = bounds.max.z; if (point.x >= x1 && point.x < x2 && point.y >= y1 && point.y <= y2 && point.z >= z1 && point.z < z2) { const SECTOR *sector = Room_GetWorldSector(room, point.x, point.z); const int32_t height = Room_GetHeight(sector, point); if (height != NO_HEIGHT) { return true; } } return false; } int16_t Room_GetIndexFromPos(const XYZ_32 point) { for (int32_t i = 0; i < Room_GetCount(); i++) { const ROOM *const room = Room_Get(i); if (room->flip_status == RFS_FLIPPED) { continue; } if (Room_PointInside(room, point)) { return i; } } return NO_ROOM; } int32_t Room_GetFlippedBaseRoom(const int32_t room_num) { for (int32_t i = 0; i < Room_GetCount(); i++) { const ROOM *const room = Room_Get(i); if (room->flipped_room == room_num) { return i; } } return NO_ROOM; } BOUNDS_32 Room_GetWorldBounds(void) { BOUNDS_32 world_bounds = { .min.x = INT32_MAX, .min.z = INT32_MAX, .max.x = 0, .max.z = 0, .min.y = MAX_HEIGHT, .max.y = -MAX_HEIGHT, }; for (int32_t i = 0; i < Room_GetCount(); i++) { const BOUNDS_32 room_bounds = Room_GetRoomBounds(Room_Get(i)); world_bounds.min.x = MIN(world_bounds.min.x, room_bounds.min.x); world_bounds.max.x = MAX(world_bounds.max.x, room_bounds.max.x); world_bounds.min.z = MIN(world_bounds.min.z, room_bounds.min.z); world_bounds.max.z = MAX(world_bounds.max.z, room_bounds.max.z); world_bounds.min.y = MIN(world_bounds.min.y, room_bounds.min.y); world_bounds.max.y = MAX(world_bounds.max.y, room_bounds.max.y); } return world_bounds; } void Room_GetNearbyRooms( const XYZ_32 pos, const int32_t r, const int32_t h, const int16_t room_num) { Room_DrawReset(); Room_MarkToBeDrawn(room_num); M_GetNewRoom(pos.x + r, pos.y, pos.z + r, room_num); M_GetNewRoom(pos.x - r, pos.y, pos.z + r, room_num); M_GetNewRoom(pos.x + r, pos.y, pos.z - r, room_num); M_GetNewRoom(pos.x - r, pos.y, pos.z - r, room_num); M_GetNewRoom(pos.x + r, pos.y - h, pos.z + r, room_num); M_GetNewRoom(pos.x - r, pos.y - h, pos.z + r, room_num); M_GetNewRoom(pos.x + r, pos.y - h, pos.z - r, room_num); M_GetNewRoom(pos.x - r, pos.y - h, pos.z - r, room_num); } bool Room_CheckOverlap(const int16_t room_num_0, const int16_t room_num_1) { const BOUNDS_32 room_0_bounds = Room_GetRoomBounds(Room_Get(room_num_0)); const BOUNDS_32 room_1_bounds = Room_GetRoomBounds(Room_Get(room_num_1)); // clang-format off return ( room_0_bounds.min.x <= room_1_bounds.max.x && room_0_bounds.max.x >= room_1_bounds.min.x && room_0_bounds.min.y <= room_1_bounds.max.y && room_0_bounds.max.y >= room_1_bounds.min.y && room_0_bounds.min.z <= room_1_bounds.max.z && room_0_bounds.max.z >= room_1_bounds.min.z); // clang-format on } bool Room_FindValidPos(XYZ_32 *const out_pos, int16_t *const out_room_num) { ASSERT(out_pos != nullptr); ASSERT(out_room_num != nullptr); XYZ_32 initial_pos = *out_pos; int16_t room_num = *out_room_num; if (room_num == NO_ROOM) { room_num = Room_GetIndexFromPos(*out_pos); } if (room_num == NO_ROOM) { return false; } const ROOM *const room = Room_Get(room_num); if (room->flip_status == RFS_FLIPPED && Room_GetFlipStatus()) { room_num = Room_GetFlippedBaseRoom(room_num); if (room_num == NO_ROOM) { return false; } } const SECTOR *sector = Room_GetSector(*out_pos, &room_num); int32_t height = Room_GetHeight(sector, *out_pos); int32_t x = out_pos->x; int32_t y = out_pos->y; int32_t z = out_pos->z; if (height == NO_HEIGHT) { // Sample a sphere of points around target x, y, z // and teleport to the first available location. VECTOR *const points = Vector_Create(sizeof(XYZ_32)); const int32_t radius = 10; const int32_t unit = STEP_L; for (int32_t dx = -radius; dx <= radius; dx++) { for (int32_t dz = -radius; dz <= radius; dz++) { if (SQUARE(dx) + SQUARE(dz) > SQUARE(radius)) { continue; } const XYZ_32 point = { .x = ROUND_TO_SECTOR(x + dx * unit) + WALL_L / 2, .y = y, .z = ROUND_TO_SECTOR(z + dz * unit) + WALL_L / 2, }; sector = Room_GetSector(point, &room_num); height = Room_GetHeightEx(sector, point, true, NO_ITEM); if (height == NO_HEIGHT) { continue; } Vector_Add(points, (void *)&point); } } int32_t best_distance = INT32_MAX; for (int32_t i = 0; i < points->count; i++) { const XYZ_32 *const point = (const XYZ_32 *)Vector_Get(points, i); const int32_t distance = XYZ_32_GetDistance(*point, initial_pos); if (distance < best_distance) { best_distance = distance; x = point->x; y = point->y; z = point->z; } } Vector_Free(points); if (best_distance == INT32_MAX) { return false; } } out_pos->x = x; out_pos->y = y; out_pos->z = z; *out_room_num = room_num; return true; } ================================================ FILE: src/trx/game/rooms/common.h ================================================ #pragma once #include #include void Room_InitialiseRooms(int32_t num_rooms); void Room_Shutdown(void); int32_t Room_GetCount(void); ROOM *Room_Get(int32_t room_num); int32_t Room_GetNumber(const ROOM *room); void Room_InitialiseFlipStatus(void); void Room_FlipMap(void); bool Room_GetFlipStatus(void); int32_t Room_GetFlipEffect(void); void Room_SetFlipEffect(int32_t flip_effect); int32_t Room_GetFlipTimer(void); void Room_SetFlipTimer(int32_t flip_timer); void Room_IncrementFlipTimer(int32_t num_frames); int32_t Room_GetFlipSlotFlags(int32_t slot_idx); void Room_SetFlipSlotFlags(int32_t slot_idx, int32_t flags); int32_t Room_GetAdjoiningRooms( int16_t init_room_num, int16_t out_room_nums[], int32_t max_room_num_count); int16_t Room_GetIndexFromPos(XYZ_32 pos); int32_t Room_GetFlippedBaseRoom(int32_t room_num); BOUNDS_32 Room_GetWorldBounds(void); void Room_BuildOutsideTable(void); int32_t Room_GetOutsideStatus(XYZ_32 pos, int16_t *out_room_num); bool Room_PointInside(const ROOM *room, XYZ_32 point); bool Room_CheckOverlap(int16_t room_num_0, int16_t room_num_1); void Room_GetNearbyRooms(XYZ_32 pos, int32_t r, int32_t h, int16_t room_num); bool Room_FindValidPos(XYZ_32 *out_pos, int16_t *out_room_num); ================================================ FILE: src/trx/game/rooms/const.h ================================================ #pragma once #define MAX_ROOMS 1024 #define MAX_FLIP_MAPS 10 #define NO_ROOM (-1) #define MAX_SLOPE 2 #define NO_HEIGHT (-32512) #define MAX_HEIGHT 32000 ================================================ FILE: src/trx/game/rooms/draw.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_MAX_BOUND_ROOMS 128 typedef struct { int32_t xv; int32_t yv; int32_t zv; } M_PORTAL_VBUF; static inline void M_DrawSet_Init(ROOM_DRAWSET *const s) { s->count = 0; memset(s->bits, 0, sizeof(s->bits)); } static inline bool M_DrawSet_Has( const ROOM_DRAWSET *const s, const int16_t item_num) { const uint32_t w = item_num >> 6; const uint32_t b = item_num & 63; return (s->bits[w] >> b) & 1ULL; } static inline bool M_DrawSet_Add(ROOM_DRAWSET *const s, const int16_t item_num) { const uint32_t w = item_num >> 6; const uint32_t b = item_num & 63; const uint64_t mask = 1ULL << b; if (s->bits[w] & mask) { return false; } s->bits[w] |= mask; s->count++; return true; } static inline bool M_DrawSet_Remove( ROOM_DRAWSET *const s, const int16_t item_num) { const uint32_t w = item_num >> 6; const uint32_t b = item_num & 63; const uint64_t mask = 1ULL << b; if (!(s->bits[w] & mask)) { return false; } s->bits[w] &= ~mask; s->count--; return true; } static inline void M_DrawSet_ForEach( const ROOM_DRAWSET *const s, void (*const fn)(int16_t item, void *ud), void *ud) { for (uint32_t w = 0; w < ROOM_DRAWSET_WORDS; w++) { uint64_t x = s->bits[w]; while (x != 0ULL) { const uint32_t b = __builtin_ctzll(x); fn((int16_t)((w << 6) + b), ud); x &= x - 1; // clear lowest set bit } } } static VECTOR *m_RoomsToDraw = nullptr; static ROOM_DRAWSET m_DrawnStatics = {}; static int32_t m_Outside; static int32_t m_OutsideRight; static int32_t m_OutsideLeft; static int32_t m_OutsideTop; static int32_t m_OutsideBottom; static int32_t m_BoundStart; static int32_t m_BoundEnd; static int32_t m_BoundRooms[M_MAX_BOUND_ROOMS] = {}; static void M_EnsureRoomsToDraw(void) { if (m_RoomsToDraw != nullptr) { return; } m_RoomsToDraw = Vector_CreateAtCapacity(sizeof(int16_t), 100); } static inline void M_SetupWaterStatus(const ROOM *const room) { if (room->flags.underwater) { Output_SetupBelowWater(g_Camera.underwater); } else { Output_SetupAboveWater(g_Camera.underwater); } } static void M_SetBounds( const PORTAL *const portal, int32_t room_num, const ROOM *parent); static inline bool M_PortalFacesCamera( const ROOM *const room, const PORTAL *const portal) { // clang-format off const XYZ_32 offset = { .x = portal->normal.x * (room->pos.x + portal->vertex[0].x - g_ViewPos.x), .y = portal->normal.y * (room->pos.y + portal->vertex[0].y - g_ViewPos.y), .z = portal->normal.z * (room->pos.z + portal->vertex[0].z - g_ViewPos.z), }; // clang-format on return offset.x + offset.y + offset.z < 0; } static void M_GetBounds(void) { while (m_BoundStart != m_BoundEnd) { const int16_t room_num = m_BoundRooms[m_BoundStart % M_MAX_BOUND_ROOMS]; m_BoundStart++; const ROOM *const room = Room_Get(room_num); OUTPUT_ROOM_BIND *const bind = Output_Bind_GetRoom(room); bind->active = false; CLAMPG(bind->bound_left, bind->test_left); CLAMPG(bind->bound_top, bind->test_top); CLAMPL(bind->bound_right, bind->test_right); CLAMPL(bind->bound_bottom, bind->test_bottom); if (!bind->drawn) { Room_MarkToBeDrawn(room_num); bind->drawn = true; if (room->flags.outside) { m_Outside = 1; } } if (!room->flags.inside || room->flags.outside) { CLAMPG(m_OutsideLeft, bind->bound_left); CLAMPG(m_OutsideTop, bind->bound_top); CLAMPL(m_OutsideRight, bind->bound_right); CLAMPL(m_OutsideBottom, bind->bound_bottom); } if (room->portals == nullptr) { continue; } Matrix_Push(); Matrix_TranslateAbs32(room->pos); for (int32_t i = 0; i < room->portals->count; i++) { PORTAL *const portal = &room->portals->portal[i]; if (M_PortalFacesCamera(room, portal)) { M_SetBounds(portal, portal->room_num, room); } } Matrix_Pop(); } } static void M_SetBounds( const PORTAL *const portal, const int32_t room_num, const ROOM *const parent) { const ROOM *const room = Room_Get(room_num); const OUTPUT_ROOM_BIND *const parent_bind = Output_Bind_GetRoom(parent); OUTPUT_ROOM_BIND *const bind = Output_Bind_GetRoom(room); if (bind->bound_left <= parent_bind->test_left && bind->bound_top <= parent_bind->test_top && bind->bound_right >= parent_bind->test_right && bind->bound_bottom >= parent_bind->test_bottom) { return; } const MATRIX *const m = g_MatrixPtr; int32_t left = parent_bind->test_right; int32_t right = parent_bind->test_left; int32_t bottom = parent_bind->test_top; int32_t top = parent_bind->test_bottom; M_PORTAL_VBUF portal_vbuf[4]; int32_t too_near = 0; for (int32_t i = 0; i < 4; i++) { M_PORTAL_VBUF *const dvbuf = &portal_vbuf[i]; const XYZ_16 *const dvtx = &portal->vertex[i]; const int32_t xv = dvtx->x * m->_00 + dvtx->y * m->_01 + dvtx->z * m->_02 + m->_03; const int32_t yv = dvtx->x * m->_10 + dvtx->y * m->_11 + dvtx->z * m->_12 + m->_13; const int32_t zv = dvtx->x * m->_20 + dvtx->y * m->_21 + dvtx->z * m->_22 + m->_23; dvbuf->xv = xv; dvbuf->yv = yv; dvbuf->zv = zv; if (zv <= 0) { too_near++; continue; } int32_t xs; int32_t ys; const int32_t zp = zv / g_PhdPersp; if (zp != 0) { xs = Viewport_GetCenterX(VIEWPORT_GAME) + xv / zp; ys = Viewport_GetCenterY(VIEWPORT_GAME) + yv / zp; } else { xs = xv < 0 ? g_PhdLeft : g_PhdRight; ys = yv < 0 ? g_PhdTop : g_PhdBottom; } if (xs - 1 < left) { left = xs - 1; } if (xs + 1 > right) { right = xs + 1; } if (ys - 1 < top) { top = ys - 1; } if (ys + 1 > bottom) { bottom = ys + 1; } } if (too_near == 4) { return; } if (too_near > 0) { const M_PORTAL_VBUF *dest = &portal_vbuf[0]; const M_PORTAL_VBUF *last = &portal_vbuf[3]; for (int32_t i = 0; i < 4; i++, last = dest, dest++) { if ((dest->zv <= 0) == (last->zv <= 0)) { continue; } if (dest->xv < 0 && last->xv < 0) { left = Viewport_GetMinX(VIEWPORT_GAME); } else if (dest->xv > 0 && last->xv > 0) { right = Viewport_GetMaxX(VIEWPORT_GAME); } else { left = Viewport_GetMinX(VIEWPORT_GAME); right = Viewport_GetMaxX(VIEWPORT_GAME); } if (dest->yv < 0 && last->yv < 0) { top = Viewport_GetMinY(VIEWPORT_GAME); } else if (dest->yv > 0 && last->yv > 0) { bottom = Viewport_GetMaxY(VIEWPORT_GAME); } else { top = Viewport_GetMinY(VIEWPORT_GAME); bottom = Viewport_GetMaxY(VIEWPORT_GAME); } } } if (left < parent_bind->test_left) { left = parent_bind->test_left; } if (right > parent_bind->test_right) { right = parent_bind->test_right; } if (top < parent_bind->test_top) { top = parent_bind->test_top; } if (bottom > parent_bind->test_bottom) { bottom = parent_bind->test_bottom; } if (left >= right || top >= bottom) { return; } if (bind->active) { CLAMPG(bind->test_left, left); CLAMPG(bind->test_top, top); CLAMPL(bind->test_right, right); CLAMPL(bind->test_bottom, bottom); } else { m_BoundRooms[m_BoundEnd % M_MAX_BOUND_ROOMS] = room_num; m_BoundEnd++; bind->active = true; bind->test_left = left; bind->test_top = top; bind->test_right = right; bind->test_bottom = bottom; } } static void M_DrawSkybox(void) { if (!Output_IsSkyboxEnabled()) { return; } g_PhdLeft = m_OutsideLeft; g_PhdTop = m_OutsideTop; g_PhdRight = m_OutsideRight; g_PhdBottom = m_OutsideBottom; const OBJECT *const skybox = Object_Get(O_SKYBOX); if (skybox->loaded) { Output_SetupAboveWater(g_Camera.underwater); Matrix_PushUnit(); Matrix_TranslateAbs32(g_ViewPos); Matrix_Rot16(skybox->frame_base->mesh_rots[0]); Output_CalculateStaticLight(Output_GetSkyShade()); Output_DrawSkybox(Object_GetMesh(skybox->mesh_idx)); Matrix_Pop(); } else { m_Outside = -1; } } static void M_DrawRoomItem(const int16_t item_num, void *const ud) { ITEM *const item = Item_Get(item_num); const OBJECT *const obj = Object_Get(item->object_id); OUTPUT_ITEM_BIND *const bind = Output_Bind_GetItem(item); if (bind->drawn || item->status == IS_INVISIBLE || obj->draw_func == nullptr) { return; } M_SetupWaterStatus(Room_Get(item->room_num)); bind->drawn |= obj->draw_func(item); if (Output_IsControlFrame()) { Item_ControlDraw(item); } } static void M_DrawSingleRoom(const ROOM *const room) { Output_SetCurrentRoom(room); M_SetupWaterStatus(room); OUTPUT_ROOM_BIND *const bind = Output_Bind_GetRoom(room); g_PhdLeft = bind->bound_left; g_PhdTop = bind->bound_top; g_PhdRight = bind->bound_right; g_PhdBottom = bind->bound_bottom; if (g_Config.debug.enable_debug_room_clip) { Output_DrawScreenFrame( g_PhdLeft, g_PhdTop, g_PhdRight - g_PhdLeft, g_PhdBottom - g_PhdTop, (RGBA_8888) { 0, 255, 0, 128 }, (RGBA_8888) { 0, 255, 0, 128 }, 1); } Matrix_TranslateAbs32(room->pos); Output_DrawRoom(room, false); M_SetupWaterStatus(room); Matrix_Push(); Matrix_TranslateAbs32(room->pos); g_PhdLeft = bind->bound_left; g_PhdTop = bind->bound_top; g_PhdRight = bind->bound_right; g_PhdBottom = bind->bound_bottom; for (int32_t i = 0; i < room->num_static_meshes; i++) { const STATIC_MESH *const mesh = &room->static_meshes[i]; if (M_DrawSet_Has(&m_DrawnStatics, mesh->draw_num)) { continue; } const STATIC_OBJECT_3D *const obj = Object_Get3DStatic(mesh->static_num); if (!obj->visible) { continue; } Matrix_Push(); Matrix_TranslateAbs32(mesh->pos); Matrix_RotY(mesh->rot.y); const CLIP clip = Output_CheckBoundsClip(&obj->draw_bounds); if (clip != CLIP_NOT_VISIBLE) { M_DrawSet_Add(&m_DrawnStatics, mesh->draw_num); Output_CalculateStaticMeshLight(mesh->pos, mesh->shade, room); Object_DrawMesh(obj->mesh_idx, clip, false); if (g_Config.debug.enable_debug_bounding_boxes) { Output_DrawCuboid(&obj->draw_bounds); } } Matrix_Pop(); } M_DrawSet_ForEach(&room->drawn_items, M_DrawRoomItem, nullptr); M_SetupWaterStatus(room); g_PhdLeft = Viewport_GetMinX(VIEWPORT_GAME); g_PhdTop = Viewport_GetMinY(VIEWPORT_GAME); g_PhdRight = Viewport_GetMaxX(VIEWPORT_GAME); g_PhdBottom = Viewport_GetMaxY(VIEWPORT_GAME); int16_t effect_num = room->effect_num; while (effect_num != NO_EFFECT) { const EFFECT *const effect = Effect_Get(effect_num); Effect_Draw(effect_num); effect_num = effect->next_free; } Matrix_Pop(); bind->bound_left = Viewport_GetMaxX(VIEWPORT_GAME); bind->bound_top = Viewport_GetMaxY(VIEWPORT_GAME); bind->bound_right = Viewport_GetMinX(VIEWPORT_GAME); bind->bound_bottom = Viewport_GetMinY(VIEWPORT_GAME); } void Room_DrawReset(void) { M_EnsureRoomsToDraw(); M_DrawSet_Init(&m_DrawnStatics); Vector_Clear(m_RoomsToDraw); } void Room_MarkToBeDrawn(const int16_t room_num) { if (Vector_Contains(m_RoomsToDraw, &room_num)) { return; } Vector_Add(m_RoomsToDraw, &room_num); } int32_t Room_DrawGetCount(void) { return m_RoomsToDraw->count; } int16_t Room_DrawGetRoom(const int16_t idx) { return *(int16_t *)Vector_Get(m_RoomsToDraw, idx); } void Room_DrawAllRooms(const int16_t current_room, const int16_t target_room) { const ROOM *const room = Room_Get(current_room); Output_Bind_ResetRooms(); OUTPUT_ROOM_BIND *const bind = Output_Bind_GetRoom(room); bind->test_left = Viewport_GetMinX(VIEWPORT_GAME); bind->test_top = Viewport_GetMinY(VIEWPORT_GAME); bind->test_right = Viewport_GetMaxX(VIEWPORT_GAME); bind->test_bottom = Viewport_GetMaxY(VIEWPORT_GAME); bind->active = true; bind->drawn = false; g_PhdLeft = bind->test_left; g_PhdTop = bind->test_top; g_PhdRight = bind->test_right; g_PhdBottom = bind->test_bottom; m_BoundRooms[0] = current_room; m_BoundStart = 0; m_BoundEnd = 1; Room_DrawReset(); m_Outside = room->flags.outside; if (m_Outside) { m_OutsideLeft = Viewport_GetMinX(VIEWPORT_GAME); m_OutsideTop = Viewport_GetMinY(VIEWPORT_GAME); m_OutsideRight = Viewport_GetMaxX(VIEWPORT_GAME); m_OutsideBottom = Viewport_GetMaxY(VIEWPORT_GAME); } else { m_OutsideLeft = Viewport_GetMaxX(VIEWPORT_GAME); m_OutsideTop = Viewport_GetMaxY(VIEWPORT_GAME); m_OutsideBottom = Viewport_GetMinY(VIEWPORT_GAME); m_OutsideRight = Viewport_GetMinX(VIEWPORT_GAME); } M_GetBounds(); if (m_Outside) { M_DrawSkybox(); } Output_Bind_ResetItems(); for (int32_t i = 0; i < Room_DrawGetCount(); i++) { const int16_t draw_room_num = Room_DrawGetRoom(i); const ROOM *const draw_room = Room_Get(draw_room_num); M_DrawSingleRoom(draw_room); OUTPUT_ROOM_BIND *const draw_bind = Output_Bind_GetRoom(draw_room); draw_bind->active = false; draw_bind->drawn = false; } const ITEM *const lara_item = Lara_GetItem(); if (Object_Get(O_LARA)->loaded) { const ROOM *const lara_room = Room_Get(lara_item->room_num); M_SetupWaterStatus(lara_room); Output_SetCurrentRoom(lara_room); Lara_Draw(lara_item); } Output_SetupAboveWater(false); FX_Draw(); Sparks_Draw(); } void Room_AddDrawnItem(const int16_t room_num, const int16_t item_num) { if (room_num != NO_ROOM) { ROOM *const room = Room_Get(room_num); M_DrawSet_Add(&room->drawn_items, item_num); } } void Room_RemoveDrawnItem(const int16_t room_num, const int16_t item_num) { if (room_num != NO_ROOM) { ROOM *const room = Room_Get(room_num); M_DrawSet_Remove(&room->drawn_items, item_num); } } __attribute__((destructor)) static void M_Shutdown(void) { Vector_Free(m_RoomsToDraw); m_RoomsToDraw = nullptr; } ================================================ FILE: src/trx/game/rooms/draw.h ================================================ #pragma once #include void Room_DrawReset(void); void Room_MarkToBeDrawn(int16_t room_num); int32_t Room_DrawGetCount(void); int16_t Room_DrawGetRoom(int16_t idx); void Room_DrawAllRooms(int16_t base_room, int16_t target_room); // Manage per-room draw queue of items void Room_AddDrawnItem(int16_t room_num, int16_t item_num); void Room_RemoveDrawnItem(int16_t room_num, int16_t item_num); ================================================ FILE: src/trx/game/rooms/enum.h ================================================ #pragma once typedef enum { HT_WALL = 0, HT_SMALL_SLOPE = 1, HT_BIG_SLOPE = 2, HT_DIAGONAL = 3, HT_SPLIT_TRI = 4, } HEIGHT_TYPE; typedef enum { RLM_NORMAL = 0, RLM_FLICKER = 1, RLM_GLOW = 2, RLM_SUNSET = 3, RLM_NUMBER_OF = 4, } ROOM_LIGHT_MODE; typedef enum { RFS_NONE = 0, RFS_UNFLIPPED = 1, RFS_FLIPPED = 2, } ROOM_FLIP_STATUS; typedef enum { FT_FLOOR = 0, FT_DOOR = 1, FT_TILT = 2, FT_ROOF = 3, FT_TRIGGER = 4, FT_LAVA = 5, FT_CLIMB = 6, FT_FLOOR_NWSE_SOLID = 7, FT_FLOOR_NESW_SOLID = 8, FT_ROOF_NWSE_SOLID = 9, FT_ROOF_NESW_SOLID = 10, FT_FLOOR_NWSE_PORTAL_SW = 11, FT_FLOOR_NWSE_PORTAL_NE = 12, FT_FLOOR_NESW_PORTAL_SE = 13, FT_FLOOR_NESW_PORTAL_NW = 14, FT_ROOF_NWSE_PORTAL_SW = 15, FT_ROOF_NWSE_PORTAL_NE = 16, FT_ROOF_NESW_PORTAL_NW = 17, FT_ROOF_NESW_PORTAL_SE = 18, FT_MONKEY = 19, FT_MINE_CART_LEFT = 20, FT_MINE_CART_RIGHT = 21, } FLOOR_TYPE; typedef enum { SPLIT_NONE, SPLIT_NWSE_SOLID, SPLIT_NESW_SOLID, SPLIT_NWSE_PORTAL_SW, SPLIT_NWSE_PORTAL_NE, SPLIT_NESW_PORTAL_SE, SPLIT_NESW_PORTAL_NW, } SPLIT_TYPE; typedef enum { SURFACE_FLOOR, SURFACE_CEILING, } SURFACE_TYPE; typedef enum { TO_OBJECT = 0, TO_CAMERA = 1, TO_SINK = 2, TO_FLIPMAP = 3, TO_FLIPON = 4, TO_FLIPOFF = 5, TO_TARGET = 6, TO_FINISH = 7, TO_CD = 8, TO_FLIPEFFECT = 9, TO_SECRET = 10, TO_BODY_BAG = 11, } TRIGGER_OBJECT; typedef enum { TT_TRIGGER = 0, TT_PAD = 1, TT_SWITCH = 2, TT_KEY = 3, TT_PICKUP = 4, TT_HEAVY = 5, TT_ANTIPAD = 6, TT_COMBAT = 7, TT_DUMMY = 8, TT_ANTITRIGGER = 9, } TRIGGER_TYPE; typedef enum { LADDER_NONE = 0, LADDER_NORTH = 1 << 0, LADDER_EAST = 1 << 1, LADDER_SOUTH = 1 << 2, LADDER_WEST = 1 << 3, LADDER_CEILING = 1 << 4, } LADDER_DIRECTION; typedef enum { MINE_CART_NONE, MINE_CART_LEFT, MINE_CART_RIGHT, MINE_CART_STOP, } MINE_CART_TYPE; ================================================ FILE: src/trx/game/rooms/floor_data.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_NULL_INDEX 0 #define M_IS_DONE(t) ((t & 0x8000) == 0x8000) #define M_ENTRY_TYPE(t) (t & 0x1F) #define M_TRIG_TYPE(t) ((t & 0x7F00) >> 8) #define M_TRIG_TIMER(t) (t & 0xFF) #define M_TRIG_ONE_SHOT(t) ((t & 0x100) == 0x100) #define M_TRIG_MASK(t) (t & 0x3E00) #define M_TRIG_CMD_TYPE(t) ((t & 0x7C00) >> 10) #define M_TRIG_CMD_ARG(t) (t & 0x3FF) #define M_TRIG_CAM_GLIDE(t) ((t & 0x3E00) >> 6) #define M_LADDER_TYPE(t) ((t & 0x7F00) >> 8) static bool M_IsSpeechTrack(const MUSIC_ID track_id) { switch (Music_FromGameID(track_id)) { case MX_BALDY_SPEECH: case MX_COWBOY_SPEECH: case MX_LARSON_SPEECH: case MX_NATLA_SPEECH: case MX_PIERRE_SPEECH: case MX_SKATEKID_SPEECH: return true; default: return false; } } static const int16_t *M_ReadTrigger( const int16_t *data, const int16_t fd_entry, SECTOR *const sector) { TRIGGER *const trigger = GameBuf_Alloc(sizeof(TRIGGER), GBUF_FLOOR_DATA); const int16_t trig_setup = *data++; trigger->enabled = true; trigger->type = M_TRIG_TYPE(fd_entry); trigger->timer = M_TRIG_TIMER(trig_setup); trigger->one_shot = M_TRIG_ONE_SHOT(trig_setup); trigger->mask = M_TRIG_MASK(trig_setup); trigger->item_index = NO_ITEM; if (trigger->type == TT_SWITCH || trigger->type == TT_KEY || trigger->type == TT_PICKUP) { const int16_t item_data = *data++; trigger->item_index = M_TRIG_CMD_ARG(item_data); if (M_IS_DONE(item_data)) { return data; } } TRIGGER_CMD *cmd; if (sector->trigger == nullptr) { sector->trigger = trigger; sector->trigger->command = GameBuf_Alloc(sizeof(TRIGGER_CMD), GBUF_FLOOR_DATA); cmd = sector->trigger->command; } else { // Some old TRLEs have incorrectly formatted floor data, with multiple // trigger entries defined where regular triggers overlap dummies. In // this case we link the new commands onto the old. cmd = sector->trigger->command; while (cmd->next_cmd != nullptr) { cmd = cmd->next_cmd; } cmd->next_cmd = GameBuf_Alloc(sizeof(TRIGGER_CMD), GBUF_FLOOR_DATA); cmd = cmd->next_cmd; } while (true) { int16_t command = *data++; cmd->type = M_TRIG_CMD_TYPE(command); if (cmd->type == TO_CAMERA) { TRIGGER_CAMERA_DATA *const cam_data = GameBuf_Alloc(sizeof(TRIGGER_CAMERA_DATA), GBUF_FLOOR_DATA); cmd->parameter = (void *)cam_data; cam_data->camera_num = M_TRIG_CMD_ARG(command); command = *data++; cam_data->timer = M_TRIG_TIMER(command); cam_data->glide = M_TRIG_CAM_GLIDE(command); cam_data->one_shot = M_TRIG_ONE_SHOT(command); } else { cmd->parameter = (void *)(intptr_t)M_TRIG_CMD_ARG(command); } if (M_IS_DONE(command)) { cmd->next_cmd = nullptr; break; } cmd->next_cmd = GameBuf_Alloc(sizeof(TRIGGER_CMD), GBUF_FLOOR_DATA); cmd = cmd->next_cmd; } return data; } static bool M_TestLava(const ITEM *const item) { const LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (item->hit_points < 0 || lara_info->water_status == LWS_CHEAT || (lara_info->water_status == LWS_ABOVE_WATER && item->pos.y != item->floor)) { return false; } // OG fix: check if floor index has lava int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector( (XYZ_32) { item->pos.x, MAX_HEIGHT, item->pos.z }, &room_num); return sector->is_death_sector; } static void M_TriggerMusicTrack(MUSIC_ID track_id, const TRIGGER *const trigger) { if (track_id == (MUSIC_ID)0 && (trigger->type == TT_ANTIPAD || trigger->type == TT_ANTITRIGGER)) { Music_Stop(); return; } if (track_id <= Music_ToGameID(MX_UNUSED_1) || track_id >= MAX_MUSIC_TRACKS || (Game_IsInGym() && !Gym_CanPlayMusicTrack(&track_id))) { return; } uint16_t flags = Music_GetTrackFlags(track_id); MUSIC_PLAY_MODE play_mode = MPM_NO_REPEAT; if (g_Config.audio.fix_speeches_killing_music && M_IsSpeechTrack(track_id)) { play_mode = MPM_OVERLAY; } // TODO: consolidate if (g_TRVersion == 1) { if ((flags & IF_ONE_SHOT) != 0) { return; } if (trigger->type == TT_SWITCH) { flags ^= trigger->mask; } else if ( trigger->type == TT_ANTIPAD || trigger->type == TT_ANTITRIGGER) { flags &= -1 - trigger->mask; } else if (trigger->mask) { flags |= trigger->mask; } if ((flags & IF_CODE_BITS) == IF_CODE_BITS) { if (trigger->one_shot) { flags |= IF_ONE_SHOT; } Music_Play_Direct(track_id, play_mode); } else { Music_StopTrack_Direct(track_id); } } else { if (trigger->type != TT_SWITCH) { const int32_t code = trigger->mask; if ((flags & code) != 0) { return; } if (trigger->one_shot) { flags |= code; } } if (trigger->timer == 0 || g_TRVersion != 2) { Music_Play_Direct(track_id, play_mode); goto finish; } if (track_id != Music_GetDelayedTrack()) { Music_Play_Direct(track_id, MPM_DELAY); flags = (flags & 0xFF00) | ((LOGIC_FPS * trigger->timer) & 0xFF); goto finish; } int32_t timer = flags & 0xFF; if (timer == 0) { goto finish; } timer--; if (timer == 0) { Music_Play_Direct(track_id, play_mode); } flags = (flags & 0xFF00) | (timer & 0xFF); } finish: Music_SetTrackFlags(track_id, flags); } void Room_ParseFloorData(const int16_t *floor_data) { for (int32_t i = 0; i < Room_GetCount(); i++) { const ROOM *const room = Room_Get(i); for (int32_t j = 0; j < room->size.x * room->size.z; j++) { SECTOR *const sector = &room->sectors[j]; Room_PopulateSectorData( sector, floor_data, sector->idx, M_NULL_INDEX); } } } void Room_PopulateSectorData( SECTOR *const sector, const int16_t *floor_data, const uint16_t start_index, const uint16_t null_index) { sector->floor.type = SURFACE_FLOOR; sector->ceiling.type = SURFACE_CEILING; sector->floor.tilt = (XZ_16) {}; sector->ceiling.tilt = (XZ_16) {}; sector->floor.split.type = SPLIT_NONE; sector->ceiling.split.type = SPLIT_NONE; sector->floor.is_split = false; sector->ceiling.is_split = false; sector->portal_room.wall = NO_ROOM; sector->is_death_sector = false; sector->trigger = nullptr; sector->ladder = LADDER_NONE; sector->mine_cart_type = MINE_CART_NONE; if (start_index == null_index || floor_data == nullptr) { return; } #define L_TILT(tilt) \ do { \ const int16_t tilt_value = *data++; \ tilt.x = (int8_t)tilt_value; \ tilt.z = tilt_value >> 8; \ } while (false) const int16_t *data = &floor_data[start_index]; int16_t fd_entry; do { fd_entry = *data++; switch (M_ENTRY_TYPE(fd_entry)) { case FT_TILT: L_TILT(sector->floor.tilt); break; case FT_ROOF: L_TILT(sector->ceiling.tilt); break; case FT_DOOR: const int16_t portal_room = *data++; if (sector->portal_room.wall == NO_ROOM) { sector->portal_room.wall = portal_room; } break; case FT_LAVA: sector->is_death_sector = true; break; case FT_TRIGGER: data = M_ReadTrigger(data, fd_entry, sector); break; case FT_CLIMB: sector->ladder |= (LADDER_DIRECTION)M_LADDER_TYPE(fd_entry); break; case FT_MONKEY: sector->ladder |= LADDER_CEILING; break; case FT_FLOOR_NWSE_SOLID: case FT_FLOOR_NESW_SOLID: case FT_FLOOR_NWSE_PORTAL_SW: case FT_FLOOR_NWSE_PORTAL_NE: case FT_FLOOR_NESW_PORTAL_SE: case FT_FLOOR_NESW_PORTAL_NW: Room_ReadTriangulation(§or->floor, fd_entry, *data++); break; case FT_ROOF_NWSE_SOLID: case FT_ROOF_NESW_SOLID: case FT_ROOF_NWSE_PORTAL_SW: case FT_ROOF_NWSE_PORTAL_NE: case FT_ROOF_NESW_PORTAL_NW: case FT_ROOF_NESW_PORTAL_SE: Room_ReadTriangulation(§or->ceiling, fd_entry, *data++); break; case FT_MINE_CART_LEFT: sector->mine_cart_type = MINE_CART_LEFT; break; case FT_MINE_CART_RIGHT: sector->mine_cart_type = sector->mine_cart_type == MINE_CART_LEFT ? MINE_CART_STOP : MINE_CART_RIGHT; break; default: break; } } while (!M_IS_DONE(fd_entry)); #undef L_TILT } void Room_ReadTriangulation( SURFACE *const surface, const int16_t func_data, const int16_t tilt_data) { switch (M_ENTRY_TYPE(func_data)) { case FT_FLOOR_NWSE_SOLID: case FT_ROOF_NWSE_SOLID: surface->split.type = SPLIT_NWSE_SOLID; break; case FT_FLOOR_NESW_SOLID: case FT_ROOF_NESW_SOLID: surface->split.type = SPLIT_NESW_SOLID; break; case FT_FLOOR_NWSE_PORTAL_SW: case FT_ROOF_NWSE_PORTAL_SW: surface->split.type = SPLIT_NWSE_PORTAL_SW; break; case FT_FLOOR_NWSE_PORTAL_NE: case FT_ROOF_NWSE_PORTAL_NE: surface->split.type = SPLIT_NWSE_PORTAL_NE; break; case FT_FLOOR_NESW_PORTAL_SE: case FT_ROOF_NESW_PORTAL_SE: surface->split.type = SPLIT_NESW_PORTAL_SE; break; case FT_FLOOR_NESW_PORTAL_NW: case FT_ROOF_NESW_PORTAL_NW: surface->split.type = SPLIT_NESW_PORTAL_NW; break; default: return; } surface->is_split = true; surface->split.h1 = (func_data & 0x03E0) >> 5; surface->split.h2 = (func_data & 0x7C00) >> 10; if ((surface->split.h1 & 0x10) != 0) { surface->split.h1 |= 0xFFF0; } if ((surface->split.h2 & 0x10) != 0) { surface->split.h2 |= 0xFFF0; } surface->split.h1 <<= 8; surface->split.h2 <<= 8; for (int32_t i = 0; i < 4; i++) { surface->split.tilts[i] = (tilt_data >> (i * 4)) & 0xF; if (surface->type == SURFACE_CEILING) { surface->split.tilts[i] *= -1; } } } bool Room_TestTriggers(const ITEM *const item) { int16_t room_num = item->room_num; const SECTOR *sector = Room_GetSector( (XYZ_32) { item->pos.x, MAX_HEIGHT, item->pos.z }, &room_num); bool result = Room_TestSectorTrigger(item, sector); if (item->object_id != O_TORSO) { return result; } for (int32_t dx = -1; dx < 2; dx++) { for (int32_t dz = -1; dz < 2; dz++) { if (dx == 0 && dz == 0) { continue; } room_num = item->room_num; sector = Room_GetSector( (XYZ_32) { item->pos.x + dx * WALL_L, MAX_HEIGHT, item->pos.z + dz * WALL_L, }, &room_num); result |= Room_TestSectorTrigger(item, sector); } } return result; } bool Room_TestSectorTrigger(const ITEM *const item, const SECTOR *const sector) { LARA_INFO *const lara_info = Lara_GetLaraInfo(); const bool is_heavy = item->object_id != O_LARA; if (!is_heavy) { if (sector->is_death_sector && M_TestLava(item)) { Lara_TouchDeathSector(Level_GetDeathTile()); } const LADDER_DIRECTION direction = 1 << Math_GetDirection(item->rot.y); lara_info->climb_status = (sector->ladder & direction) == direction; } const TRIGGER *const trigger = sector->trigger; if (trigger == nullptr || !trigger->enabled) { return false; } if (g_Camera.type != CAM_HEAVY) { Camera_RefreshFromTrigger(trigger); } ITEM *camera_item = nullptr; bool switch_off = false; bool flip_map = false; bool flip_available = false; int32_t new_effect = -1; const bool flip_status = Room_GetFlipStatus(); if (is_heavy) { if (trigger->type != TT_HEAVY) { return false; } } else { switch (trigger->type) { case TT_PAD: case TT_ANTIPAD: if (!Gym_TrackManager_OnPadContact(GYM_TRACK_ASSAULT, false)) { return false; } if (item->pos.y != item->floor) { return false; } if (item->object_id == O_LARA && !Gym_TrackManager_OnPadContact(GYM_TRACK_ASSAULT, true)) { return false; } break; case TT_SWITCH: { const bool switch_result = Switch_Trigger(trigger->item_index, trigger->timer); ITEM *const switch_item = Item_Get(trigger->item_index); if (g_TRVersion >= 3 && trigger->one_shot) { switch_item->flags |= IF_ONE_SHOT_SWITCH; } if (!switch_result) { return false; } switch_off = switch_item->current_anim_state == SWITCH_STATE_OFF; break; } case TT_KEY: { if (!Keyhole_Trigger(trigger->item_index)) { return false; } break; } case TT_PICKUP: { if (!Pickup_Trigger(trigger->item_index)) { return false; } break; } case TT_HEAVY: case TT_DUMMY: return false; case TT_COMBAT: if (lara_info->gun_status != LGS_READY) { return false; } break; default: break; } } const TRIGGER_CMD *cmd = trigger->command; for (; cmd != nullptr; cmd = cmd->next_cmd) { switch (cmd->type) { case TO_OBJECT: { const int16_t item_num = (int16_t)(intptr_t)cmd->parameter; ITEM *const trig_item = Item_Get(item_num); bool one_shot = false; if (g_TRVersion == 3) { switch (trigger->type) { case TT_SWITCH: one_shot = trig_item->flags & IF_ONE_SHOT_SWITCH; break; case TT_ANTIPAD: case TT_ANTITRIGGER: one_shot = trig_item->flags & IF_ONE_SHOT_ANTITRIGGER; break; default: one_shot = trig_item->flags & IF_ONE_SHOT; break; } } else { one_shot = trig_item->flags & IF_ONE_SHOT; } if (one_shot) { break; } const OBJECT *const obj = Object_Get(trig_item->object_id); const bool is_shoal_object = Object_IsType(trig_item->object_id, g_ShoalObjects); if (is_shoal_object && trigger->type != TT_ANTIPAD && trigger->type != TT_ANTITRIGGER) { Shoal_TriggerActivate(trig_item, trigger->timer); } else { trig_item->timer = trigger->timer; if (trig_item->timer != 1) { trig_item->timer *= LOGIC_FPS; } if (obj->trigger_func != nullptr) { const bool use_default_handling = obj->trigger_func(trig_item, trigger); if (!use_default_handling) { break; } } } if (trigger->type == TT_SWITCH) { trig_item->flags ^= trigger->mask; if (trigger->one_shot && g_TRVersion == 3) { trig_item->flags |= IF_ONE_SHOT_SWITCH; } } else if ( trigger->type == TT_ANTIPAD || trigger->type == TT_ANTITRIGGER) { if (is_shoal_object) { Shoal_TriggerDeactivate(trig_item); } // TODO investigate unifying as ~(trigger->mask | IF_REVERSE) if (g_TRVersion >= 3) { trig_item->flags &= ~(IF_CODE_BITS | IF_REVERSE); } else { trig_item->flags &= ~trigger->mask; } if (trigger->one_shot) { if (g_TRVersion == 3) { trig_item->flags |= IF_ONE_SHOT_ANTITRIGGER; } else { trig_item->flags |= IF_ONE_SHOT; } } } else { trig_item->flags |= trigger->mask; } if ((trig_item->flags & IF_CODE_BITS) != IF_CODE_BITS) { break; } if (trigger->one_shot) { trig_item->flags |= IF_ONE_SHOT; } if (trig_item->active) { break; } if (obj->activate_func != nullptr) { obj->activate_func(trig_item); } else if (obj->intelligent) { if (trig_item->status == IS_INACTIVE) { trig_item->touch_bits = 0; trig_item->status = IS_ACTIVE; Item_AddActive(item_num); LOT_EnableBaddieAI(item_num, true); } else if (trig_item->status == IS_INVISIBLE) { trig_item->touch_bits = 0; if (LOT_EnableBaddieAI(item_num, false)) { trig_item->status = IS_ACTIVE; } else { trig_item->status = IS_INVISIBLE; } Item_AddActive(item_num); } } else { trig_item->touch_bits = 0; trig_item->status = IS_ACTIVE; Item_AddActive(item_num); } break; } case TO_CAMERA: { const TRIGGER_CAMERA_DATA *const cam_data = (TRIGGER_CAMERA_DATA *)cmd->parameter; OBJECT_VECTOR *const camera = Camera_GetFixedObject(cam_data->camera_num); if ((camera->flags & IF_ONE_SHOT) != 0) { break; } g_Camera.num = cam_data->camera_num; if ((g_Camera.type == CAM_LOOK || g_Camera.type == CAM_COMBAT) && !Camera_IsLocked(g_Camera.num)) { break; } if (trigger->type == TT_COMBAT) { break; } if (trigger->type == TT_SWITCH && trigger->timer != 0 && switch_off) { break; } if (g_Camera.num == g_Camera.last && trigger->type != TT_SWITCH) { break; } g_Camera.timer = LOGIC_FPS * cam_data->timer; if (cam_data->one_shot) { camera->flags |= IF_ONE_SHOT; } g_Camera.speed = 1; if (g_Config.visuals.enable_glide_cameras) { g_Camera.speed += cam_data->glide; } g_Camera.type = is_heavy ? CAM_HEAVY : CAM_FIXED; break; } case TO_SINK: { if (g_TRVersion == 3) { lara_info->current.active = 1 + (int16_t)(intptr_t)cmd->parameter; } else { const OBJECT_VECTOR *const sink = Camera_GetFixedObject((int16_t)(intptr_t)cmd->parameter); if (g_TRVersion == 2 || lara_info->lot.required_box != sink->flags) { lara_info->lot.target = sink->pos; lara_info->lot.required_box = sink->flags; } lara_info->current.active = sink->data * 6; } break; } case TO_FLIPMAP: { const int16_t flip_slot = (int16_t)(intptr_t)cmd->parameter; int32_t slot_flags = Room_GetFlipSlotFlags(flip_slot); flip_available = true; if (slot_flags & IF_ONE_SHOT) { break; } if (trigger->type == TT_SWITCH) { slot_flags ^= trigger->mask; } else { slot_flags |= trigger->mask; } if ((slot_flags & IF_CODE_BITS) == IF_CODE_BITS) { if (trigger->one_shot) { slot_flags |= IF_ONE_SHOT; } if (!flip_status) { flip_map = true; } } else if (flip_status) { flip_map = true; } Room_SetFlipSlotFlags(flip_slot, slot_flags); break; } case TO_FLIPON: { const int16_t flip_slot = (int16_t)(intptr_t)cmd->parameter; const int32_t slot_flags = Room_GetFlipSlotFlags(flip_slot); flip_available = true; if ((slot_flags & IF_CODE_BITS) == IF_CODE_BITS && !flip_status) { flip_map = true; } break; } case TO_FLIPOFF: { const int16_t flip_slot = (int16_t)(intptr_t)cmd->parameter; const int32_t slot_flags = Room_GetFlipSlotFlags(flip_slot); flip_available = true; if ((slot_flags & IF_CODE_BITS) == IF_CODE_BITS && flip_status) { flip_map = true; } break; } case TO_TARGET: { const int16_t target_num = (int16_t)(intptr_t)cmd->parameter; camera_item = Item_Get(target_num); break; } case TO_FINISH: Game_SetIsLevelComplete(true); break; case TO_FLIPEFFECT: new_effect = (int16_t)(intptr_t)cmd->parameter; break; case TO_CD: M_TriggerMusicTrack((MUSIC_ID)(intptr_t)cmd->parameter, trigger); break; case TO_SECRET: { const int16_t secret_num = (int16_t)(intptr_t)cmd->parameter; if (Stats_AddSecret(secret_num)) { const MUSIC_PLAY_MODE mode = g_Config.audio.fix_secrets_killing_music ? MPM_OVERLAY : MPM_ONCE; Music_Play(MX_SECRET, mode); } break; } case TO_BODY_BAG: if (g_Config.gameplay.enable_body_bags) { Item_ClearKilled(); } break; default: break; } } if (camera_item != nullptr && (g_Camera.type == CAM_FIXED || g_Camera.type == CAM_HEAVY)) { g_Camera.item = camera_item; } if (flip_map) { Room_FlipMap(); } if (new_effect != -1 && (flip_map || !flip_available)) { Room_SetFlipEffect(new_effect); Room_SetFlipTimer(0); } return true; } ================================================ FILE: src/trx/game/rooms/floor_data.h ================================================ #pragma once #include void Room_ParseFloorData(const int16_t *floor_data); void Room_PopulateSectorData( SECTOR *sector, const int16_t *floor_data, uint16_t start_index, uint16_t null_index); void Room_ReadTriangulation( SURFACE *surface, int16_t func_data, int16_t tilt_data); bool Room_TestTriggers(const ITEM *item); bool Room_TestSectorTrigger(const ITEM *item, const SECTOR *sector); ================================================ FILE: src/trx/game/rooms/geometry.c ================================================ #include #include #include #include #include #include #include #include #define M_WALL_MASK (WALL_L - 1) #define M_NEG_TILT(T, H) ((T * (H & M_WALL_MASK)) >> 2) #define M_POS_TILT(T, H) ((T * ((M_WALL_MASK - H) & M_WALL_MASK)) >> 2) static int16_t m_AbyssMinHeight = 0; static int32_t m_AbyssMaxHeight = 0; static HEIGHT_TYPE m_HeightType = HT_WALL; static inline int32_t M_GetTiltShift( const XZ_16 tilt, const int32_t x, const int32_t z, const bool is_ceiling) { int32_t shift = 0; if (is_ceiling) { if (tilt.z < 0) { shift += M_NEG_TILT(tilt.z, z); } else { shift -= M_POS_TILT(tilt.z, z); } if (tilt.x < 0) { shift += M_POS_TILT(tilt.x, x); } else { shift -= M_NEG_TILT(tilt.x, x); } } else { if (tilt.z < 0) { shift -= M_NEG_TILT(tilt.z, z); } else { shift += M_POS_TILT(tilt.z, z); } if (tilt.x < 0) { shift -= M_NEG_TILT(tilt.x, x); } else { shift += M_POS_TILT(tilt.x, x); } } return shift; } static int32_t M_GetUnsplitSurfaceHeight( const SURFACE surface, const int32_t x, const int32_t z) { int32_t height = surface.height; if (surface.tilt.x == 0 && surface.tilt.z == 0) { return height; } const HEIGHT_TYPE slope_type = (ABS(surface.tilt.z) > MAX_SLOPE || ABS(surface.tilt.x) > MAX_SLOPE) ? HT_BIG_SLOPE : HT_SMALL_SLOPE; if (Camera_IsChunky() && slope_type == HT_BIG_SLOPE) { return height; } const bool is_ceiling = surface.type == SURFACE_CEILING; height += M_GetTiltShift(surface.tilt, x, z, is_ceiling); if (!is_ceiling) { m_HeightType = slope_type; } return height; } static inline XZ_16 M_GetSplitTilt( const SURFACE *const surface, const int32_t x, const int32_t z, int32_t *const shift) { const bool is_ceiling = surface->type == SURFACE_CEILING; const SPLIT split = surface->split; const int32_t dx = x & M_WALL_MASK; const int32_t dz = z & M_WALL_MASK; const int16_t t0 = split.tilts[0]; const int16_t t1 = split.tilts[1]; const int16_t t2 = split.tilts[2]; const int16_t t3 = split.tilts[3]; XZ_16 tilt = {}; if (split.type == SPLIT_NWSE_SOLID || split.type == SPLIT_NWSE_PORTAL_SW || split.type == SPLIT_NWSE_PORTAL_NE) { if (dx > WALL_L - dz) { tilt.x = is_ceiling ? (t0 - t1) : (t3 - t2); tilt.z = t3 - t0; *shift = split.h1; } else { tilt.x = is_ceiling ? (t3 - t2) : (t0 - t1); tilt.z = t2 - t1; *shift = split.h2; } } else if (dx > dz) { tilt.x = is_ceiling ? (t3 - t2) : (t0 - t1); tilt.z = t3 - t0; *shift = split.h1; } else { tilt.x = is_ceiling ? (t0 - t1) : (t3 - t2); tilt.z = t2 - t1; *shift = split.h2; } return tilt; } static int16_t M_GetSplitSurfaceHeight( const SURFACE surface, const int32_t x, const int32_t z) { const bool is_ceiling = surface.type == SURFACE_CEILING; if (Camera_IsChunky()) { const int16_t ch1 = surface.height + surface.split.h2; const int16_t ch2 = surface.height + surface.split.h1; return is_ceiling ? MAX(ch1, ch2) : MIN(ch1, ch2); } int16_t height = surface.height; if (!is_ceiling) { m_HeightType = HT_SPLIT_TRI; } int32_t shift = 0; const XZ_16 tilt = M_GetSplitTilt(&surface, x, z, &shift); shift += M_GetTiltShift(tilt, x, z, is_ceiling); height += shift; if (!is_ceiling) { if (ABS(tilt.x) > MAX_SLOPE || ABS(tilt.z) > MAX_SLOPE) { m_HeightType = HT_DIAGONAL; } else if (m_HeightType != HT_SPLIT_TRI) { m_HeightType = HT_SMALL_SLOPE; } } return height; } static int32_t M_GetSurfaceHeight( const SURFACE surface, const int32_t x, const int32_t z, const bool fix_tilts) { if (surface.height == NO_HEIGHT && (surface.is_split || fix_tilts)) { return NO_HEIGHT; } return surface.is_split ? M_GetSplitSurfaceHeight(surface, x, z) : M_GetUnsplitSurfaceHeight(surface, x, z); } static bool M_IsPortalSolid( const SURFACE surface, const int32_t x, const int32_t z) { if (!surface.is_split) { return false; } const int32_t dx = x & M_WALL_MASK; const int32_t dz = z & M_WALL_MASK; const bool is_ceiling = surface.type == SURFACE_CEILING; switch (surface.split.type) { case SPLIT_NWSE_PORTAL_SW: return dx > WALL_L - dz; case SPLIT_NWSE_PORTAL_NE: return dx <= WALL_L - dz; case SPLIT_NESW_PORTAL_SE: return is_ceiling ? (dx <= dz) : (dx > dz); case SPLIT_NESW_PORTAL_NW: return is_ceiling ? (dx > dz) : (dx <= dz); default: return false; } } BOUNDS_32 Room_GetRoomBounds(const ROOM *const room) { ASSERT(room != nullptr); return (BOUNDS_32) { .min = { .x = room->pos.x + WALL_L, .y = room->max_ceiling, .z = room->pos.z + WALL_L, }, .max = { .x = room->pos.x + room->size.x * WALL_L - WALL_L, .y = room->min_floor, .z = room->pos.z + room->size.z * WALL_L - WALL_L, }, }; } SECTOR *Room_GetSector(const XYZ_32 pos, int16_t *const room_num) { SECTOR *sector = nullptr; while (true) { const ROOM *room = Room_Get(*room_num); int32_t z_sector = (pos.z - room->pos.z) >> WALL_SHIFT; int32_t x_sector = (pos.x - room->pos.x) >> WALL_SHIFT; if (z_sector <= 0) { z_sector = 0; if (x_sector < 1) { x_sector = 1; } else if (x_sector > room->size.x - 2) { x_sector = room->size.x - 2; } } else if (z_sector >= room->size.z - 1) { z_sector = room->size.z - 1; if (x_sector < 1) { x_sector = 1; } else if (x_sector > room->size.x - 2) { x_sector = room->size.x - 2; } } else if (x_sector < 0) { x_sector = 0; } else if (x_sector >= room->size.x) { x_sector = room->size.x - 1; } sector = Room_GetUnitSector(room, x_sector, z_sector); if (sector->portal_room.wall == NO_ROOM) { break; } *room_num = sector->portal_room.wall; } ASSERT(sector != nullptr); if (pos.y >= M_GetSurfaceHeight(sector->floor, pos.x, pos.z, true)) { do { if (sector->portal_room.pit == NO_ROOM || M_IsPortalSolid(sector->floor, pos.x, pos.z)) { break; } *room_num = sector->portal_room.pit; const ROOM *const room = Room_Get(*room_num); sector = Room_GetWorldSector(room, pos.x, pos.z); } while (pos.y >= M_GetSurfaceHeight(sector->floor, pos.x, pos.z, true)); } else if ( pos.y < M_GetSurfaceHeight(sector->ceiling, pos.x, pos.z, true)) { do { if (sector->portal_room.sky == NO_ROOM || M_IsPortalSolid(sector->ceiling, pos.x, pos.z)) { break; } *room_num = sector->portal_room.sky; const ROOM *const room = Room_Get(sector->portal_room.sky); sector = Room_GetWorldSector(room, pos.x, pos.z); } while (pos.y < M_GetSurfaceHeight(sector->ceiling, pos.x, pos.z, true)); } return sector; } SECTOR *Room_GetSectorOnWalkable(const XYZ_32 pos, int16_t *const room_num) { // Resolve wall portals. const ROOM *room = Room_Get(*room_num); SECTOR *sector = Room_GetWorldSector(room, pos.x, pos.z); while (sector->portal_room.wall != NO_ROOM) { *room_num = sector->portal_room.wall; room = Room_Get(*room_num); sector = Room_GetWorldSector(room, pos.x, pos.z); } // Check if on a walkable. const int32_t room_height = Room_GetHeight(sector, pos); const bool skip_pit = Room_IsOnWalkable( sector, (XYZ_32) { pos.x, ROUND_TO_HALF_CLICK(pos.y), pos.z, }, ROUND_TO_HALF_CLICK(pos.y), NO_ITEM); // Traverse pit sector unless on a walkable. if (!skip_pit && pos.y >= sector->floor.height) { while (sector->portal_room.pit != NO_ROOM) { *room_num = sector->portal_room.pit; room = Room_Get(*room_num); sector = Room_GetWorldSector(room, pos.x, pos.z); if (pos.y < sector->floor.height) { break; } } } else if (pos.y < sector->ceiling.height) { while (sector->portal_room.sky != NO_ROOM) { *room_num = sector->portal_room.sky; room = Room_Get(*room_num); sector = Room_GetWorldSector(room, pos.x, pos.z); if (pos.y >= sector->ceiling.height) { break; } } } return sector; } SECTOR *Room_GetWorldSector( const ROOM *const room, const int32_t x_pos, const int32_t z_pos) { int32_t x_sector = (x_pos - room->pos.x) >> WALL_SHIFT; int32_t z_sector = (z_pos - room->pos.z) >> WALL_SHIFT; CLAMP(x_sector, 0, room->size.x - 1); CLAMP(z_sector, 0, room->size.z - 1); return Room_GetUnitSector(room, x_sector, z_sector); } SECTOR *Room_GetUnitSector( const ROOM *const room, const int32_t x_sector, const int32_t z_sector) { return &room->sectors[z_sector + x_sector * room->size.z]; } SECTOR *Room_GetPitSector( const SECTOR *sector, const int32_t x, const int32_t z) { while (sector->portal_room.pit != NO_ROOM && !M_IsPortalSolid(sector->floor, x, z)) { const ROOM *const room = Room_Get(sector->portal_room.pit); sector = Room_GetWorldSector(room, x, z); } return (SECTOR *)sector; } SECTOR *Room_GetSkySector( const SECTOR *sector, const int32_t x, const int32_t z) { while (sector->portal_room.sky != NO_ROOM && !M_IsPortalSolid(sector->ceiling, x, z)) { const ROOM *const room = Room_Get(sector->portal_room.sky); sector = Room_GetWorldSector(room, x, z); } return (SECTOR *)sector; } void Room_SetAbyssHeight(const int16_t height) { // Once Lara reaches the min abyss height, she will be killed; she will // continue to fall however, so the max height is needed until the inventory // is shown, otherwise Lara will hit the floor. m_AbyssMinHeight = height; m_AbyssMaxHeight = height == 0 ? 0 : m_AbyssMinHeight + 26 * STEP_L; CLAMPG(m_AbyssMaxHeight, MAX_HEIGHT - STEP_L); } bool Room_IsAbyssHeight(const int32_t height) { return m_AbyssMinHeight != 0 && height >= m_AbyssMinHeight; } HEIGHT_TYPE Room_GetHeightType(void) { return m_HeightType; } XZ_16 Room_GetTiltType(const SECTOR *sector, const XYZ_32 pos) { sector = Room_GetPitSector(sector, pos.x, pos.z); if ((pos.y + STEP_L * 2) < sector->floor.height) { return (XZ_16) {}; } if (!sector->floor.is_split) { return sector->floor.tilt; } int32_t shift = 0; return M_GetSplitTilt(§or->floor, pos.x, pos.z, &shift); } int32_t Room_GetHeight(const SECTOR *const sector, const XYZ_32 pos) { return Room_GetHeightEx( sector, pos, g_Config.gameplay.fix_wall_geometry, NO_ITEM); } int32_t Room_GetFloorHeightForSector( const SECTOR *const sector, const int32_t x, const int32_t z, const bool fix_tilts) { m_HeightType = HT_WALL; if (Room_IsAbyssHeight(sector->floor.height)) { return m_AbyssMaxHeight; } return M_GetSurfaceHeight(sector->floor, x, z, fix_tilts); } int32_t Room_GetHeightEx( const SECTOR *const sector, const XYZ_32 pos, const bool fix_tilts, const int16_t ignore_item_num) { m_HeightType = HT_WALL; const SECTOR *const pit_sector = Room_GetPitSector(sector, pos.x, pos.z); int32_t height = pit_sector->floor.height; if (Room_IsAbyssHeight(height)) { height = m_AbyssMaxHeight; } else { height = M_GetSurfaceHeight(pit_sector->floor, pos.x, pos.z, fix_tilts); } // Climb the stack of walkables. In each iteration the test Y pos is moved // up to match the current height, so preventing testing below previous // walkables. int32_t base_height = height; int32_t test_y = pos.y; for (const WALKABLE *w = pit_sector->walkable; w != nullptr; w = w->next) { if (w->item_num == ignore_item_num) { continue; } const ITEM *const item = Item_Get(w->item_num); const OBJECT *const obj = Object_Get(item->object_id); if (obj->floor_height_func == nullptr) { continue; } height = obj->floor_height_func(item, pos.x, test_y, pos.z, height); test_y = MIN(pos.y, height); } if (base_height != height) { // A walkable is present, which always override slopes below. m_HeightType = HT_WALL; } return height; } int32_t Room_GetCeiling(const SECTOR *const sector, const XYZ_32 pos) { return Room_GetCeilingEx(sector, pos, g_Config.gameplay.fix_wall_geometry); } int32_t Room_GetCeilingEx( const SECTOR *const sector, const XYZ_32 pos, const bool fix_tilts) { const SECTOR *const sky_sector = Room_GetSkySector(sector, pos.x, pos.z); int32_t height = M_GetSurfaceHeight(sky_sector->ceiling, pos.x, pos.z, fix_tilts); const SECTOR *const pit_sector = Room_GetPitSector(sector, pos.x, pos.z); for (const WALKABLE *w = pit_sector->walkable; w != nullptr; w = w->next) { const ITEM *const item = Item_Get(w->item_num); const OBJECT *const obj = Object_Get(item->object_id); if (obj->ceiling_height_func != nullptr) { height = obj->ceiling_height_func(item, pos.x, pos.y, pos.z, height); } } return height; } int32_t Room_GetWaterHeight(const XYZ_32 pos, const int16_t room_num) { return Room_GetWaterHeightEx( pos, room_num, g_Config.gameplay.fix_wall_geometry); } int32_t Room_GetWaterHeightEx( const XYZ_32 pos, int16_t room_num, const bool fix_tilts) { const int32_t x = pos.x; const int32_t y = pos.y; const int32_t z = pos.z; const SECTOR *sector = nullptr; const ROOM *room = nullptr; do { room = Room_Get(room_num); int32_t z_sector = (z - room->pos.z) >> WALL_SHIFT; int32_t x_sector = (x - room->pos.x) >> WALL_SHIFT; if (z_sector <= 0) { z_sector = 0; if (x_sector < 1) { x_sector = 1; } else if (x_sector > room->size.x - 2) { x_sector = room->size.x - 2; } } else if (z_sector >= room->size.z - 1) { z_sector = room->size.z - 1; if (x_sector < 1) { x_sector = 1; } else if (x_sector > room->size.x - 2) { x_sector = room->size.x - 2; } } else if (x_sector < 0) { x_sector = 0; } else if (x_sector >= room->size.x) { x_sector = room->size.x - 1; } sector = Room_GetUnitSector(room, x_sector, z_sector); room_num = sector->portal_room.wall; } while (room_num != NO_ROOM); if (room->flags.underwater || room->flags.swamp) { while (sector->portal_room.sky != NO_ROOM && !M_IsPortalSolid(sector->ceiling, x, z)) { room = Room_Get(sector->portal_room.sky); if (!room->flags.underwater && !room->flags.swamp) { if (fix_tilts) { break; } else { return room->min_floor; } } sector = Room_GetWorldSector(room, x, z); } return fix_tilts ? M_GetSurfaceHeight(sector->ceiling, x, z, true) : room->max_ceiling; } else { while (sector->portal_room.pit != NO_ROOM && !M_IsPortalSolid(sector->floor, x, z)) { room = Room_Get(sector->portal_room.pit); if (room->flags.underwater || room->flags.swamp) { return fix_tilts ? M_GetSurfaceHeight(sector->floor, x, z, true) : room->max_ceiling; } sector = Room_GetWorldSector(room, x, z); } return NO_HEIGHT; } } void Room_AlterFloorHeight(const ITEM *const item, const int32_t height) { if (height == 0) { return; } int16_t portal_room; SECTOR *sector; const ROOM *room = Room_Get(item->room_num); do { int32_t z_sector = (item->pos.z - room->pos.z) >> WALL_SHIFT; int32_t x_sector = (item->pos.x - room->pos.x) >> WALL_SHIFT; if (z_sector <= 0) { z_sector = 0; CLAMP(x_sector, 1, room->size.x - 2); } else if (z_sector >= room->size.z - 1) { z_sector = room->size.z - 1; CLAMP(x_sector, 1, room->size.x - 2); } else { CLAMP(x_sector, 0, room->size.x - 1); } sector = Room_GetUnitSector(room, x_sector, z_sector); portal_room = sector->portal_room.wall; if (portal_room != NO_ROOM) { room = Room_Get(portal_room); } } while (portal_room != NO_ROOM); const SECTOR *const sky_sector = Room_GetSkySector(sector, item->pos.x, item->pos.z); sector = Room_GetPitSector(sector, item->pos.x, item->pos.z); if (sector->floor.height != NO_HEIGHT) { sector->floor.height += ROUND_TO_CLICK(height); if (sector->floor.height == sky_sector->ceiling.height) { sector->floor.height = NO_HEIGHT; } } else { sector->floor.height = sky_sector->ceiling.height + ROUND_TO_CLICK(height); } BOX_INFO *const box = Box_GetBox(sector->box); if (box->overlap_index & BOX_BLOCKABLE) { if (height < 0) { box->overlap_index |= BOX_BLOCKED; } else { box->overlap_index &= ~BOX_BLOCKED; } } } int32_t Room_FindGridShift(int32_t src, const int32_t dst) { const int32_t src_w = src >> WALL_SHIFT; const int32_t dst_w = dst >> WALL_SHIFT; if (src_w == dst_w) { return 0; } src &= WALL_L - 1; if (dst_w > src_w) { return WALL_L - (src - 1); } else { return -(src + 1); } } bool Room_IsOnWalkable( const SECTOR *sector, const XYZ_32 pos, const int32_t room_height, const int16_t ignore_item_num) { sector = Room_GetPitSector(sector, pos.x, pos.z); int32_t height = sector->floor.height; bool object_found = false; for (WALKABLE *w = sector->walkable; w != nullptr; w = w->next) { // Optionally ignore a walkable. if (w->item_num == ignore_item_num) { continue; } const ITEM *const item = Item_Get(w->item_num); const OBJECT *const obj = Object_Get(item->object_id); if (obj->floor_height_func != nullptr) { const int32_t test_height = obj->floor_height_func(item, pos.x, pos.y, pos.z, height); // If the floor height changed, try to climb the walkable stack. if (test_height != height) { // Check if height changed aka actually on a walkable. height = test_height; object_found = true; } } } return object_found && room_height == height; } ================================================ FILE: src/trx/game/rooms/geometry.h ================================================ #pragma once #include BOUNDS_32 Room_GetRoomBounds(const ROOM *room); SECTOR *Room_GetSector(XYZ_32 pos, int16_t *room_num); SECTOR *Room_GetSectorOnWalkable(XYZ_32 pos, int16_t *room_num); SECTOR *Room_GetWorldSector(const ROOM *room, int32_t x_pos, int32_t z_pos); SECTOR *Room_GetUnitSector( const ROOM *room, int32_t x_sector, int32_t z_sector); SECTOR *Room_GetPitSector(const SECTOR *sector, int32_t x, int32_t z); SECTOR *Room_GetSkySector(const SECTOR *sector, int32_t x, int32_t z); void Room_SetAbyssHeight(int16_t height); bool Room_IsAbyssHeight(int32_t height); HEIGHT_TYPE Room_GetHeightType(void); XZ_16 Room_GetTiltType(const SECTOR *sector, XYZ_32 pos); int32_t Room_GetHeight(const SECTOR *sector, XYZ_32 pos); int32_t Room_GetHeightEx( const SECTOR *sector, XYZ_32 pos, bool fix_tilts, int16_t ignore_item_num); int32_t Room_GetCeiling(const SECTOR *sector, XYZ_32 pos); int32_t Room_GetCeilingEx(const SECTOR *sector, XYZ_32 pos, bool fix_tilts); int32_t Room_GetFloorHeightForSector( const SECTOR *sector, int32_t x, int32_t z, bool fix_tilts); int32_t Room_GetWaterHeight(XYZ_32 pos, int16_t room_num); int32_t Room_GetWaterHeightEx(XYZ_32 pos, int16_t room_num, bool fix_tilts); void Room_AlterFloorHeight(const ITEM *item, int32_t height); int32_t Room_FindGridShift(int32_t src, int32_t dst); bool Room_IsOnWalkable( const SECTOR *sector, XYZ_32 pos, int32_t room_height, int16_t ignore_item_num); ================================================ FILE: src/trx/game/rooms/types.h ================================================ #pragma once #include #include #include #include #include #define ROOM_DRAWSET_WORDS (MAX_ITEMS / 64) typedef struct TRIGGER_CMD { TRIGGER_OBJECT type; void *parameter; struct TRIGGER_CMD *next_cmd; } TRIGGER_CMD; typedef struct { int16_t camera_num; uint8_t timer; uint8_t glide; bool one_shot; } TRIGGER_CAMERA_DATA; typedef struct { bool enabled; TRIGGER_TYPE type; int8_t timer; int16_t mask; bool one_shot; int16_t item_index; TRIGGER_CMD *command; } TRIGGER; typedef struct { int16_t room_num; XYZ_16 normal; XYZ_16 vertex[4]; BOUNDS_32 bounds; } PORTAL; typedef struct { uint16_t count; PORTAL portal[]; } PORTALS; typedef struct WALKABLE { int16_t item_num; XYZ_32 pos; struct WALKABLE *next; } WALKABLE; typedef struct { SPLIT_TYPE type; int16_t tilts[4]; int32_t h1; int32_t h2; } SPLIT; typedef struct { SURFACE_TYPE type; int32_t height; bool is_split; union { XZ_16 tilt; SPLIT split; }; } SURFACE; typedef struct { uint16_t idx; int16_t box; bool is_death_sector; LADDER_DIRECTION ladder; MINE_CART_TYPE mine_cart_type; TRIGGER *trigger; WALKABLE *walkable; struct { int16_t pit; int16_t sky; int16_t wall; } portal_room; SURFACE floor; SURFACE ceiling; uint8_t fx; bool stopper; } SECTOR; typedef struct { XYZ_32 pos; SHADE shade; FALLOFF falloff; RGB_888 color; uint8_t type; // TR3: 0 = point, != 0 = sun XYZ_16 dir; // TR3: sun direction (type != 0) } LIGHT; typedef struct { XYZ_16 pos; RGBA_8888 color; int16_t light_base; uint8_t light_table_value; struct { bool disable_wibble; bool move; bool glow; } flags; } ROOM_VERTEX; typedef struct { uint16_t texture; uint16_t vertex; } ROOM_SPRITE; typedef struct { int16_t num_vertices; struct { int16_t count; FACE *data; } all_faces, face4s, face3s; struct { int16_t count; ROOM_SPRITE *data; } sprites; ROOM_VERTEX *vertices; } ROOM_MESH; typedef struct { XYZ_32 pos; struct { int16_t y; } rot; RGBA_8888 color; SHADE shade; int16_t static_num; int16_t draw_num; } STATIC_MESH; typedef struct { uint64_t bits[ROOM_DRAWSET_WORDS]; uint16_t count; } ROOM_DRAWSET; typedef struct { ROOM_MESH mesh; PORTALS *portals; SECTOR *sectors; LIGHT *lights; STATIC_MESH *static_meshes; XYZ_32 pos; int32_t min_floor; int32_t max_ceiling; struct { int16_t z; int16_t x; } size; int16_t ambient; ROOM_LIGHT_MODE light_mode; int16_t num_lights; int16_t num_static_meshes; int16_t item_num; int16_t effect_num; int16_t flipped_room; ROOM_FLIP_STATUS flip_status; struct { bool underwater; bool outside; bool wind; bool inside; bool dynamic_lit; bool swamp; } flags; ROOM_DRAWSET drawn_items; uint8_t water_scheme; uint8_t reverb_info; } ROOM; ================================================ FILE: src/trx/game/rooms/utils.h ================================================ #pragma once #include #define ROUND_TO_CLICK(V) ((V) & ~(STEP_L - 1)) #define ROUND_TO_SECTOR(V) ((V) & ~(WALL_L - 1)) #define ROUND_TO_SECTOR_END(V) ((V) | (WALL_L - 1)) #define ROUND_TO_CLICK_UP(V) (((V) + STEP_L / 2 - 1) & ~(STEP_L / 2 - 1)) #define ROUND_TO_HALF_CLICK(V) ((V) & ~(STEP_L / 2 - 1)) #define ROUND_TO_CLICK_SIGNED(V) \ (((V) >= 0) ? (((V) + STEP_L - 1) & ~(STEP_L - 1)) : ((V) & ~(STEP_L - 1))) ================================================ FILE: src/trx/game/rooms.h ================================================ #pragma once #include #include #include #include #include #include #include ================================================ FILE: src/trx/game/savegame/common.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static SAVEGAME_VERSION m_InitialVersion = SG_VERSION_LEGACY; static SAVEGAME_INFO *m_NormalSavegameInfo = nullptr; static SAVEGAME_INFO *m_QuickSavegameInfo = nullptr; static RESUME_INFO *m_ResumeInfo = nullptr; static int32_t m_SaveSlots = 0; static int32_t m_QuickSaveSlots = 0; static int32_t m_SavedGames = 0; static int32_t m_SaveCounter = 0; static int32_t m_NextQuickSlot = 0; static SAVEGAME_SLOT_REF m_MostRecentlyUsedSlot = { .index = -1 }; static SAVEGAME_SLOT_REF m_MostRecentlyCreatedSlot = { .index = -1 }; static SAVEGAME_SLOT_REF m_BoundSlot = { .index = -1 }; static const char *M_GetSaveWriteDir(void) { const char *const saves_dir = TRXPath_Get(TRX_PATH_SAVES_DIR); const SHELL_ARGS *const args = Shell_GetArgs(); if (args != nullptr && args->mod != nullptr && args->mod->name != nullptr) { return String_FormatStatic("%s/%s", saves_dir, args->mod->name); } return saves_dir; } static char *M_GetSaveWritePath(const char *const file_name) { ASSERT(file_name != nullptr); return String_Format("%s/%s", M_GetSaveWriteDir(), file_name); } static SAVEGAME_INFO *M_GetSavegameInfoSlot(const SAVEGAME_SLOT_REF slot) { switch (slot.pool) { case SAVEGAME_SLOT_POOL_NORMAL: if (slot.index >= 0 && slot.index < m_SaveSlots) { return &m_NormalSavegameInfo[slot.index]; } break; case SAVEGAME_SLOT_POOL_QUICK: if (slot.index >= 0 && slot.index < m_QuickSaveSlots) { return &m_QuickSavegameInfo[slot.index]; } break; case SAVEGAME_SLOT_POOL_NUMBER_OF: break; } return nullptr; } static const char *M_GetSaveFilePatternForPool(const SAVEGAME_SLOT_POOL pool) { switch (pool) { case SAVEGAME_SLOT_POOL_NORMAL: return SG_File_GetSaveFilePattern(); case SAVEGAME_SLOT_POOL_QUICK: return SG_File_GetQuickSaveFilePattern(); case SAVEGAME_SLOT_POOL_NUMBER_OF: break; } return nullptr; } static void M_CopyResumeInfo( RESUME_INFO *const target, const RESUME_INFO *const source) { memcpy(target, source, sizeof(RESUME_INFO)); } static void M_ClearSlot(SAVEGAME_INFO *const savegame_info) { savegame_info->counter = -1; savegame_info->level_num = -1; savegame_info->is_quick = false; Memory_FreePointer(&savegame_info->full_path); Memory_FreePointer(&savegame_info->level_title); } static void M_ClearSlots(void) { if (m_NormalSavegameInfo != nullptr) { for (int32_t i = 0; i < m_SaveSlots; i++) { M_ClearSlot(&m_NormalSavegameInfo[i]); } } if (m_QuickSavegameInfo != nullptr) { for (int32_t i = 0; i < m_QuickSaveSlots; i++) { M_ClearSlot(&m_QuickSavegameInfo[i]); } } } static bool M_FillSlot(const SAVEGAME_SLOT_REF slot, const char *const path) { SAVEGAME_INFO *const savegame_info = M_GetSavegameInfoSlot(slot); if (savegame_info == nullptr) { return false; } bool result = false; MYFILE *const fp = File_Open(path, FILE_OPEN_READ); if (fp != nullptr) { SAVEGAME_INFO tmp_savegame_info; if (SG_File_FillInfo(fp, &tmp_savegame_info)) { M_ClearSlot(savegame_info); *savegame_info = tmp_savegame_info; savegame_info->is_quick = slot.pool == SAVEGAME_SLOT_POOL_QUICK; savegame_info->full_path = Memory_DupStr(path); result = true; } File_Close(fp); } return result; } static void M_ScanSavedGamesDir(const char *const dir_path) { void *const dir_handle = File_OpenDirectory(dir_path); if (dir_handle == nullptr) { return; } while (true) { const char *const file_name = File_ReadDirectory(dir_handle); if (file_name == nullptr) { break; } if (strcmp(file_name, ".") == 0 || strcmp(file_name, "..") == 0) { continue; } char *file_name_ci = String_ToUpper(file_name); for (SAVEGAME_SLOT_POOL pool = 0; pool < SAVEGAME_SLOT_POOL_NUMBER_OF; pool++) { const char *const pattern = M_GetSaveFilePatternForPool(pool); char *pattern_ci = String_ToUpperPattern(pattern); int32_t slot_idx = -1; const int32_t parsed = sscanf(file_name_ci, pattern_ci, &slot_idx); Memory_FreePointer(&pattern_ci); if (parsed != 1 || slot_idx < 0 || slot_idx >= Savegame_GetSlotCount(pool)) { continue; } char *file_path = String_Format("%s/%s", dir_path, file_name); M_FillSlot( (SAVEGAME_SLOT_REF) { .pool = pool, .index = slot_idx }, file_path); Memory_FreePointer(&file_path); break; } Memory_FreePointer(&file_name_ci); } File_CloseDirectory(dir_handle); } static void M_LoadPreprocess(void) { Savegame_InitCurrentInfo(); } static void M_LoadPostprocess(void) { // TODO: tidy this; skidoo drivers currently require handle_save_func to be // called immediately on load within the strategies. for (int32_t i = 0; i < Item_GetLevelCount(); i++) { ITEM *const item = Item_Get(i); const OBJECT *const obj = Object_Get(item->object_id); if (obj->save_position && (obj->shadow_size != 0 || obj->load_floor)) { int16_t room_num = item->room_num; const SECTOR *const sector = Room_GetSector(item->pos, &room_num); item->floor = Room_GetHeight(sector, item->pos); } // TODO: make this engine-agnostic if (g_TRVersion == 1 && obj->handle_save_func != nullptr) { obj->handle_save_func(item, SAVEGAME_STAGE_AFTER_LOAD); } } LARA_INFO *const lara = Lara_GetLaraInfo(); if (Game_GetBonusFlag() != GBF_NONE) { g_Config.profile.new_game_plus_unlock = true; Config_Update(); } if (lara->burn && !g_Config.gameplay.enable_enhanced_saves) { lara->burn = false; Lara_CatchFire(); } } static void M_DetermineLegacyGunTypes(RESUME_INFO *const resume) { // Fallback logic to figure out holster and back gun items for saves from // TR1X 4.2 and earlier (including TombATI) and TR2X 1.2 and earlier, where // these values are missing. Make educated guesses based on the type of gun // equipped. if (resume->holsters_gun_type == LGT_UNKNOWN) { switch (resume->equipped_gun_type) { case LGT_PISTOLS: case LGT_MAGNUMS: case LGT_AUTOS: case LGT_DESERT_EAGLE: case LGT_UZIS: resume->holsters_gun_type = resume->equipped_gun_type; break; case LGT_SHOTGUN: case LGT_M16: case LGT_MP5: case LGT_GRENADE: case LGT_ROCKET: case LGT_HARPOON: if (resume->flags.has_pistols) { resume->holsters_gun_type = LGT_PISTOLS; } else if (resume->flags.has_magnums) { resume->holsters_gun_type = LGT_MAGNUMS; } else if (resume->flags.has_autos) { resume->holsters_gun_type = LGT_AUTOS; } else if (resume->flags.has_desert_eagle) { resume->holsters_gun_type = LGT_DESERT_EAGLE; } else if (resume->flags.has_uzis) { resume->holsters_gun_type = LGT_UZIS; } else { resume->holsters_gun_type = LGT_UNARMED; } break; default: resume->holsters_gun_type = LGT_UNARMED; break; } } if (resume->back_gun_type == LGT_UNKNOWN) { resume->back_gun_type = LGT_UNARMED; if (resume->flags.has_shotgun) { resume->back_gun_type = LGT_SHOTGUN; } else if (resume->flags.has_m16) { resume->back_gun_type = LGT_M16; } else if (resume->flags.has_mp5) { resume->back_gun_type = LGT_MP5; } else if (resume->flags.has_grenade) { resume->back_gun_type = LGT_GRENADE; } else if (resume->flags.has_rocket) { resume->back_gun_type = LGT_ROCKET; } else if (resume->flags.has_harpoon) { resume->back_gun_type = LGT_HARPOON; } } } SAVEGAME_VERSION Savegame_GetInitialVersion(void) { return m_InitialVersion; } void Savegame_SetInitialVersion(const SAVEGAME_VERSION version) { m_InitialVersion = version; } void Savegame_BindSlot(const SAVEGAME_SLOT_REF slot) { if (!Savegame_IsValidSlotRef(slot)) { m_BoundSlot = Savegame_InvalidSlot(); return; } m_BoundSlot = slot; m_MostRecentlyUsedSlot = slot; LOG_DEBUG("Binding save slot %d:%d", slot.pool, slot.index); } SAVEGAME_SLOT_REF Savegame_GetMostRecentlyUsedSlot(void) { return m_MostRecentlyUsedSlot; } void Savegame_UnbindSlot(void) { LOG_DEBUG("Resetting the save slot"); m_BoundSlot = Savegame_InvalidSlot(); } SAVEGAME_SLOT_REF Savegame_GetBoundSlot(void) { return m_BoundSlot; } int32_t Savegame_GetLevelNumber(const SAVEGAME_SLOT_REF slot) { const SAVEGAME_INFO *const info = Savegame_GetSavegameInfo(slot); return info != nullptr ? info->level_num : -1; } bool Savegame_IsSlotFree(const SAVEGAME_SLOT_REF slot) { const SAVEGAME_INFO *const info = Savegame_GetSavegameInfo(slot); return info == nullptr || info->level_num == -1; } int32_t Savegame_GetCounter(void) { return m_SaveCounter; } int32_t Savegame_GetTotalCount(void) { return m_SavedGames; } SAVEGAME_SLOT_REF Savegame_GetMostRecentlyCreatedSlot(void) { return m_MostRecentlyCreatedSlot; } SAVEGAME_SLOT_REF Savegame_NormalSlot(const int32_t index) { return (SAVEGAME_SLOT_REF) { .pool = SAVEGAME_SLOT_POOL_NORMAL, .index = index, }; } SAVEGAME_SLOT_REF Savegame_QuickSlot(const int32_t index) { return (SAVEGAME_SLOT_REF) { .pool = SAVEGAME_SLOT_POOL_QUICK, .index = index, }; } SAVEGAME_SLOT_REF Savegame_InvalidSlot(void) { return (SAVEGAME_SLOT_REF) { .pool = SAVEGAME_SLOT_POOL_NORMAL, .index = -1, }; } bool Savegame_IsValidSlotRef(const SAVEGAME_SLOT_REF slot) { return slot.pool >= SAVEGAME_SLOT_POOL_NORMAL && slot.pool < SAVEGAME_SLOT_POOL_NUMBER_OF && slot.index >= 0 && slot.index < Savegame_GetSlotCount(slot.pool); } int32_t Savegame_SlotToParam(const SAVEGAME_SLOT_REF slot) { if (!Savegame_IsValidSlotRef(slot)) { return -1; } const uint32_t packed = ((uint32_t)slot.pool << 31) | (uint32_t)slot.index; return (int32_t)packed; } SAVEGAME_SLOT_REF Savegame_SlotFromParam(const int32_t param) { if (param == -1) { return Savegame_InvalidSlot(); } const uint32_t packed = (uint32_t)param; const SAVEGAME_SLOT_POOL pool = (packed >> 31) & 1; const int32_t index = (int32_t)(packed & 0x7FFFFFFF); return (SAVEGAME_SLOT_REF) { .pool = pool, .index = index, }; } void Savegame_Init(void) { m_ResumeInfo = Memory_Alloc( sizeof(RESUME_INFO) * (GF_GetLevelTable(GFLT_MAIN)->count + GF_GetLevelTable(GFLT_DEMOS)->count)); m_SaveSlots = Savegame_GetSlotCount(SAVEGAME_SLOT_POOL_NORMAL); m_QuickSaveSlots = Savegame_GetSlotCount(SAVEGAME_SLOT_POOL_QUICK); m_NormalSavegameInfo = Memory_Alloc(sizeof(SAVEGAME_INFO) * m_SaveSlots); m_QuickSavegameInfo = m_QuickSaveSlots > 0 ? Memory_Alloc(sizeof(SAVEGAME_INFO) * m_QuickSaveSlots) : nullptr; const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_DEMOS); for (int32_t i = 0; i < level_table->count; i++) { RESUME_INFO *const resume_info = Savegame_GetCurrentInfo(&level_table->levels[i]); resume_info->lara_hitpoints = LARA_MAX_HITPOINTS; resume_info->flags.available = true; resume_info->flags.has_pistols = true; resume_info->pistol_ammo = 1000; resume_info->gun_status = LGS_ARMLESS; resume_info->equipped_gun_type = LGT_PISTOLS; resume_info->holsters_gun_type = LGT_PISTOLS; resume_info->back_gun_type = LGT_UNARMED; resume_info->prev_level = -1; } } bool Savegame_IsInitialised(void) { return m_NormalSavegameInfo != nullptr; } void Savegame_Shutdown(void) { M_ClearSlots(); Memory_FreePointer(&m_ResumeInfo); Memory_FreePointer(&m_NormalSavegameInfo); Memory_FreePointer(&m_QuickSavegameInfo); } int32_t Savegame_GetSlotCount(const SAVEGAME_SLOT_POOL pool) { switch (pool) { case SAVEGAME_SLOT_POOL_NORMAL: return g_Config.gameplay.maximum_save_slots; case SAVEGAME_SLOT_POOL_QUICK: return g_Config.gameplay.maximum_quick_save_slots; case SAVEGAME_SLOT_POOL_NUMBER_OF: break; } return 0; } SAVEGAME_SLOT_REF Savegame_GetNextQuickSlot(void) { if (m_QuickSaveSlots <= 0) { return Savegame_InvalidSlot(); } if (m_NextQuickSlot < 0 || m_NextQuickSlot >= m_QuickSaveSlots) { m_NextQuickSlot = 0; } return Savegame_QuickSlot(m_NextQuickSlot); } static bool M_IsQuickSlotSortedBefore( const SAVEGAME_SLOT_REF left, const SAVEGAME_SLOT_REF right) { const SAVEGAME_INFO *const left_info = Savegame_GetSavegameInfo(left); const SAVEGAME_INFO *const right_info = Savegame_GetSavegameInfo(right); if (left_info == nullptr || right_info == nullptr) { return false; } if (left_info->counter != right_info->counter) { return left_info->counter > right_info->counter; } return left.index < right.index; } int32_t Savegame_GetQuickVisualCount(void) { int32_t count = 0; const int32_t quick_slot_count = Savegame_GetSlotCount(SAVEGAME_SLOT_POOL_QUICK); for (int32_t i = 0; i < quick_slot_count; i++) { if (!Savegame_IsSlotFree(Savegame_QuickSlot(i))) { count++; } } return count; } SAVEGAME_SLOT_REF Savegame_QuickFromVisualIndex(const int32_t visual_index) { if (visual_index < 0) { return Savegame_InvalidSlot(); } const int32_t quick_slot_count = Savegame_GetSlotCount(SAVEGAME_SLOT_POOL_QUICK); for (int32_t i = 0; i < quick_slot_count; i++) { const SAVEGAME_SLOT_REF candidate = Savegame_QuickSlot(i); if (Savegame_IsSlotFree(candidate)) { continue; } int32_t better_count = 0; for (int32_t j = 0; j < quick_slot_count; j++) { const SAVEGAME_SLOT_REF other = Savegame_QuickSlot(j); if (Savegame_IsSlotFree(other)) { continue; } if (M_IsQuickSlotSortedBefore(other, candidate)) { better_count++; } } if (better_count == visual_index) { return candidate; } } return Savegame_InvalidSlot(); } int32_t Savegame_QuickToVisualIndex(const SAVEGAME_SLOT_REF slot) { if (!Savegame_IsValidSlotRef(slot) || slot.pool != SAVEGAME_SLOT_POOL_QUICK || Savegame_IsSlotFree(slot)) { return -1; } const int32_t quick_slot_count = Savegame_GetSlotCount(SAVEGAME_SLOT_POOL_QUICK); int32_t better_count = 0; for (int32_t i = 0; i < quick_slot_count; i++) { const SAVEGAME_SLOT_REF other = Savegame_QuickSlot(i); if (Savegame_IsSlotFree(other)) { continue; } if (M_IsQuickSlotSortedBefore(other, slot)) { better_count++; } } return better_count; } RESUME_INFO *Savegame_GetCurrentInfo(const GF_LEVEL *const level) { ASSERT(m_ResumeInfo != nullptr); if (level == nullptr) { return nullptr; } if (GF_GetLevelTableType(level->type) == GFLT_MAIN) { return &m_ResumeInfo[level->num]; } else if (level->type == GFL_DEMO) { return &m_ResumeInfo[GF_GetLevelTable(GFLT_MAIN)->count]; } else if (level->type == GFL_CUTSCENE || level->type == GFL_TITLE) { return nullptr; } LOG_WARNING( "Warning: unable to get resume info for level %d (type=%s)", level->num, ENUM_MAP_TO_STRING(GF_LEVEL_TYPE, level->type)); return nullptr; } void Savegame_SetCurrentInfo(const int32_t current_slot, const int32_t src_slot) { m_ResumeInfo[current_slot] = m_ResumeInfo[src_slot]; } const SAVEGAME_INFO *Savegame_GetSavegameInfo(const SAVEGAME_SLOT_REF slot) { return M_GetSavegameInfoSlot(slot); } void Savegame_InitCurrentInfo(void) { const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); for (int32_t i = 0; i < level_table->count; i++) { const GF_LEVEL *const level = &level_table->levels[i]; Savegame_ResetCurrentInfo(level); Savegame_ApplyLogicToCurrentInfo(level); RESUME_INFO *const current = Savegame_GetCurrentInfo(level); current->level_completed = false; current->flags.available = false; } if (GF_GetGymLevel() != nullptr) { Savegame_GetCurrentInfo(GF_GetGymLevel())->flags.available = true; } if (GF_GetFirstLevel() != nullptr) { Savegame_GetCurrentInfo(GF_GetFirstLevel())->flags.available = true; } } void Savegame_ResetCurrentInfo(const GF_LEVEL *const level) { LOG_INFO("Resetting resume info for level #%d", level->num); RESUME_INFO *const current = Savegame_GetCurrentInfo(level); *current = (RESUME_INFO) { .prev_level = -1, .level_completed = false }; } void Savegame_CarryCurrentInfoToNextLevel( const GF_LEVEL *const src_level, const GF_LEVEL *const dst_level) { LOG_INFO( "Copying resume info from level #%d to level #%d", src_level->num, dst_level->num); RESUME_INFO *const src_resume = Savegame_GetCurrentInfo(src_level); RESUME_INFO *const dst_resume = Savegame_GetCurrentInfo(dst_level); if (src_resume != nullptr && dst_resume != nullptr) { const bool dst_level_completed = dst_resume->level_completed; M_CopyResumeInfo(dst_resume, src_resume); dst_resume->level_completed = dst_level_completed; dst_resume->prev_level = src_level->num; } } void Savegame_PersistGameToCurrentInfo(const GF_LEVEL *const level) { RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); if (resume == nullptr) { return; } resume->flags.available = true; LARA_INFO *const lara = Lara_GetLaraInfo(); const ITEM *const lara_item = Lara_GetItem(); if (lara_item != nullptr) { resume->lara_hitpoints = lara_item->hit_points; } resume->small_medipacks = Inv_RequestItem(O_SMALL_MEDIPACK_ITEM); resume->large_medipacks = Inv_RequestItem(O_LARGE_MEDIPACK_ITEM); resume->pistol_ammo = 1000; if (Inv_RequestItem(O_PISTOL_ITEM)) { resume->flags.has_pistols = true; } else { resume->flags.has_pistols = false; } if (Inv_RequestItem(O_SHOTGUN_ITEM)) { resume->flags.has_shotgun = true; resume->shotgun_ammo = lara->shotgun_ammo.ammo; } else { resume->flags.has_shotgun = false; resume->shotgun_ammo = Inv_RequestItem(O_SHOTGUN_AMMO_ITEM) * Gun_GetAmmoPickupQuantity(LGT_SHOTGUN); } if (Inv_RequestItem(O_MAGNUM_ITEM)) { resume->flags.has_magnums = true; resume->magnum_ammo = lara->magnum_ammo.ammo; } else { resume->flags.has_magnums = false; resume->magnum_ammo = Inv_RequestItem(O_MAGNUM_AMMO_ITEM) * Gun_GetAmmoPickupQuantity(LGT_MAGNUMS); } if (Inv_RequestItem(O_AUTOS_ITEM)) { resume->flags.has_autos = true; resume->autos_ammo = lara->autos_ammo.ammo; } else { resume->flags.has_autos = false; resume->autos_ammo = Inv_RequestItem(O_AUTOS_AMMO_ITEM) * Gun_GetAmmoPickupQuantity(LGT_AUTOS); } if (Inv_RequestItem(O_DESERT_EAGLE_ITEM)) { resume->flags.has_desert_eagle = true; resume->desert_eagle_ammo = lara->desert_eagle_ammo.ammo; } else { resume->flags.has_desert_eagle = false; resume->desert_eagle_ammo = Inv_RequestItem(O_DESERT_EAGLE_AMMO_ITEM) * Gun_GetAmmoPickupQuantity(LGT_DESERT_EAGLE); } if (Inv_RequestItem(O_UZI_ITEM)) { resume->flags.has_uzis = true; resume->uzi_ammo = lara->uzi_ammo.ammo; } else { resume->flags.has_uzis = false; resume->uzi_ammo = Inv_RequestItem(O_UZI_AMMO_ITEM) * Gun_GetAmmoPickupQuantity(LGT_UZIS); } resume->flares = Inv_RequestItem(O_FLARE_ITEM); resume->num_scions = Inv_RequestItem(O_SCION_ITEM_1); resume->num_quest_item_1 = Inv_RequestItem(O_QUEST_ITEM_1); resume->num_quest_item_2 = Inv_RequestItem(O_QUEST_ITEM_2); resume->num_quest_item_3 = Inv_RequestItem(O_QUEST_ITEM_3); resume->num_quest_item_4 = Inv_RequestItem(O_QUEST_ITEM_4); if (Inv_RequestItem(O_M16_ITEM)) { resume->flags.has_m16 = true; resume->m16_ammo = lara->m16_ammo.ammo; } else { resume->flags.has_m16 = false; resume->m16_ammo = Inv_RequestItem(O_M16_AMMO_ITEM) * Gun_GetAmmoPickupQuantity(LGT_M16); } if (Inv_RequestItem(O_MP5_ITEM)) { resume->flags.has_mp5 = true; resume->mp5_ammo = lara->mp5_ammo.ammo; } else { resume->flags.has_mp5 = false; resume->mp5_ammo = Inv_RequestItem(O_MP5_AMMO_ITEM) * Gun_GetAmmoPickupQuantity(LGT_MP5); } if (Inv_RequestItem(O_HARPOON_ITEM)) { resume->flags.has_harpoon = true; resume->harpoon_ammo = lara->harpoon_ammo.ammo; } else { resume->flags.has_harpoon = false; resume->harpoon_ammo = Inv_RequestItem(O_HARPOON_AMMO_ITEM) * Gun_GetAmmoPickupQuantity(LGT_HARPOON); } if (Inv_RequestItem(O_GRENADE_GUN_ITEM)) { resume->flags.has_grenade = true; resume->grenade_ammo = lara->grenade_ammo.ammo; } else { resume->flags.has_grenade = false; resume->grenade_ammo = Inv_RequestItem(O_GRENADE_AMMO_ITEM) * Gun_GetAmmoPickupQuantity(LGT_GRENADE); } if (Inv_RequestItem(O_ROCKET_GUN_ITEM)) { resume->flags.has_rocket = true; resume->rocket_ammo = lara->rocket_ammo.ammo; } else { resume->flags.has_rocket = false; resume->rocket_ammo = Inv_RequestItem(O_ROCKET_AMMO_ITEM) * Gun_GetAmmoPickupQuantity(LGT_ROCKET); } resume->equipped_gun_type = lara->last_gun_type; resume->holsters_gun_type = lara->holsters_gun_type; resume->back_gun_type = lara->back_gun_type; if (resume->back_gun_type == LGT_UNARMED && Gun_IsRifleType(resume->equipped_gun_type) && Inv_RequestItem(Gun_GetGunObject(resume->equipped_gun_type)) != 0) { // If a rifle is currently drawn, Lara's back mesh is temporarily // unarmed. Preserve the preferred rifle for next-level mesh restore. resume->back_gun_type = resume->equipped_gun_type; } if (lara->gun_status == LGS_READY) { resume->gun_status = LGS_READY; } else { resume->gun_status = LGS_ARMLESS; } } void Savegame_ApplyLogicToCurrentInfo(const GF_LEVEL *const level) { RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); if (resume == nullptr) { return; } LOG_INFO("Applying game logic to level #%d", level->num); if (!g_Config.gameplay.disable_healing_between_levels || level == GF_GetGymLevel() || level == GF_GetFirstLevel()) { resume->lara_hitpoints = g_Config.gameplay.start_lara_hitpoints; } if (level == GF_GetGymLevel()) { resume->flags.available = true; resume->flags.costume = g_TRVersion == 1; resume->flags.has_pistols = false; resume->flags.has_shotgun = false; resume->flags.has_magnums = false; resume->flags.has_autos = false; resume->flags.has_desert_eagle = false; resume->flags.has_uzis = false; resume->flags.has_harpoon = false; resume->flags.has_m16 = false; resume->flags.has_mp5 = false; resume->flags.has_grenade = false; resume->flags.has_rocket = false; resume->pistol_ammo = 0; resume->shotgun_ammo = 0; resume->magnum_ammo = 0; resume->autos_ammo = 0; resume->desert_eagle_ammo = 0; resume->uzi_ammo = 0; resume->harpoon_ammo = 0; resume->m16_ammo = 0; resume->mp5_ammo = 0; resume->grenade_ammo = 0; resume->rocket_ammo = 0; resume->small_medipacks = 0; resume->large_medipacks = 0; resume->num_scions = 0; resume->num_quest_item_1 = 0; resume->num_quest_item_2 = 0; resume->num_quest_item_3 = 0; resume->num_quest_item_4 = 0; resume->flares = 0; resume->equipped_gun_type = LGT_UNARMED; resume->holsters_gun_type = LGT_UNARMED; resume->back_gun_type = LGT_UNARMED; resume->gun_status = LGS_ARMLESS; } if (level == GF_GetFirstLevel()) { resume->flags.available = true; resume->flags.costume = false; resume->flags.has_pistols = true; resume->flags.has_shotgun = false; resume->flags.has_magnums = false; resume->flags.has_autos = false; resume->flags.has_desert_eagle = false; resume->flags.has_uzis = false; resume->small_medipacks = 0; resume->large_medipacks = 0; resume->flares = 0; resume->pistol_ammo = 1000; resume->shotgun_ammo = 0; resume->magnum_ammo = 0; resume->autos_ammo = 0; resume->desert_eagle_ammo = 0; resume->uzi_ammo = 0; resume->num_scions = 0; resume->num_quest_item_1 = 0; resume->num_quest_item_2 = 0; resume->num_quest_item_3 = 0; resume->num_quest_item_4 = 0; resume->flags.has_harpoon = false; resume->flags.has_m16 = false; resume->flags.has_mp5 = false; resume->flags.has_grenade = false; resume->flags.has_rocket = false; resume->harpoon_ammo = 0; resume->m16_ammo = 0; resume->mp5_ammo = 0; resume->grenade_ammo = 0; resume->rocket_ammo = 0; resume->equipped_gun_type = LGT_PISTOLS; resume->holsters_gun_type = LGT_PISTOLS; resume->back_gun_type = LGT_UNARMED; resume->gun_status = LGS_ARMLESS; } if (Game_IsBonusFlagSet(GBF_NGPLUS) && level != GF_GetGymLevel()) { resume->flags.has_pistols = true; resume->flags.has_shotgun = true; resume->flags.has_magnums = g_Weapons[LGT_MAGNUMS].is_available; resume->flags.has_autos = g_Weapons[LGT_AUTOS].is_available; resume->flags.has_desert_eagle = g_Weapons[LGT_DESERT_EAGLE].is_available; resume->flags.has_uzis = true; resume->flags.has_m16 = g_Weapons[LGT_M16].is_available; resume->flags.has_mp5 = g_Weapons[LGT_MP5].is_available; resume->flags.has_grenade = g_Weapons[LGT_GRENADE].is_available; resume->flags.has_rocket = g_Weapons[LGT_ROCKET].is_available; resume->flags.has_harpoon = g_Weapons[LGT_HARPOON].is_available; resume->shotgun_ammo = 10000; resume->magnum_ammo = resume->flags.has_magnums ? 10000 : 0; resume->autos_ammo = resume->flags.has_autos ? 10000 : 0; resume->desert_eagle_ammo = resume->flags.has_desert_eagle ? 10000 : 0; resume->uzi_ammo = 10000; resume->flares = g_TRVersion == 1 ? 0 : -1; resume->m16_ammo = resume->flags.has_m16 ? 10000 : 0; resume->mp5_ammo = resume->flags.has_mp5 ? 10000 : 0; resume->grenade_ammo = resume->flags.has_grenade ? 10000 : 0; resume->rocket_ammo = resume->flags.has_rocket ? 10000 : 0; resume->harpoon_ammo = resume->flags.has_harpoon ? 10000 : 0; const bool should_force_ngplus_gun_setup = !g_Config.gameplay.remember_gun_status || resume->prev_level == -1; if (should_force_ngplus_gun_setup) { switch (g_TRVersion) { case 1: resume->equipped_gun_type = LGT_UZIS; resume->back_gun_type = LGT_SHOTGUN; resume->holsters_gun_type = LGT_UZIS; break; case 2: resume->equipped_gun_type = LGT_GRENADE; resume->back_gun_type = LGT_GRENADE; resume->holsters_gun_type = LGT_PISTOLS; break; case 3: default: resume->equipped_gun_type = LGT_ROCKET; resume->back_gun_type = LGT_ROCKET; resume->holsters_gun_type = LGT_PISTOLS; break; } } } resume->stats.secret_flags = 0; M_DetermineLegacyGunTypes(resume); } void Savegame_ProcessItemsBeforeSave(void) { for (int32_t i = 0; i < Item_GetLevelCount(); i++) { ITEM *const item = Item_Get(i); const OBJECT *const obj = Object_Get(item->object_id); if (obj->handle_save_func != nullptr) { obj->handle_save_func(item, SAVEGAME_STAGE_BEFORE_SAVE); } } } void Savegame_ProcessItemsBeforeLoad(void) { for (int32_t i = 0; i < Item_GetLevelCount(); i++) { ITEM *const item = Item_Get(i); const OBJECT *const obj = Object_Get(item->object_id); if (obj->handle_save_func != nullptr) { obj->handle_save_func(item, SAVEGAME_STAGE_BEFORE_LOAD); } } } void Savegame_ScanSavedGames(void) { BENCHMARK benchmark = Benchmark_Start(); M_ClearSlots(); m_SaveCounter = 0; m_SavedGames = 0; m_MostRecentlyCreatedSlot = Savegame_InvalidSlot(); m_NextQuickSlot = 0; int32_t newest_quick_counter = -1; int32_t newest_quick_slot = -1; // Scan low-priority locations first; the write directory is authoritative. M_ScanSavedGamesDir("."); M_ScanSavedGamesDir(TRXPath_Get(TRX_PATH_LEGACY_SAVES_DIR)); M_ScanSavedGamesDir(TRXPath_Get(TRX_PATH_SAVES_DIR)); { // M_GetSaveWriteDir may use static formatting storage, so copy it // before scanning because nested formatting calls during scan can // overwrite it. AUTO_FREE char *write_dir = Memory_DupStr(M_GetSaveWriteDir()); M_ScanSavedGamesDir(write_dir); } for (SAVEGAME_SLOT_POOL pool = 0; pool < SAVEGAME_SLOT_POOL_NUMBER_OF; pool++) { for (int32_t i = 0; i < Savegame_GetSlotCount(pool); i++) { SAVEGAME_INFO *const savegame_info = M_GetSavegameInfoSlot( (SAVEGAME_SLOT_REF) { .pool = pool, .index = i }); if (savegame_info->level_title == nullptr) { continue; } if (savegame_info->counter > m_SaveCounter) { m_SaveCounter = savegame_info->counter; m_MostRecentlyCreatedSlot = (SAVEGAME_SLOT_REF) { .pool = pool, .index = i }; } m_SavedGames++; if (pool == SAVEGAME_SLOT_POOL_QUICK && savegame_info->counter > newest_quick_counter) { newest_quick_counter = savegame_info->counter; newest_quick_slot = i; } } } if (m_QuickSaveSlots > 0 && newest_quick_slot >= 0) { m_NextQuickSlot = (newest_quick_slot + 1) % m_QuickSaveSlots; } Benchmark_End(&benchmark, nullptr); } bool Savegame_Save(const SAVEGAME_SLOT_REF slot) { if (!Savegame_IsValidSlotRef(slot)) { return false; } bool result = false; Savegame_BindSlot(slot); const GF_LEVEL *const current_level = Game_GetCurrentLevel(); Savegame_PersistGameToCurrentInfo(current_level); const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); for (int32_t i = 0; i < level_table->count; i++) { const GF_LEVEL *const level = &level_table->levels[i]; if (level->type == GFL_CURRENT) { Savegame_SetCurrentInfo(i, current_level->num); } } SAVEGAME_INFO *const savegame_info = M_GetSavegameInfoSlot(slot); const bool was_slot_empty = savegame_info->full_path == nullptr; m_SaveCounter++; const char *const save_pattern = M_GetSaveFilePatternForPool(slot.pool); char *file_name = String_Format(save_pattern, slot.index); char *full_path = M_GetSaveWritePath(file_name); File_EnsureParentDirectories(full_path); MYFILE *const fp = File_Open(full_path, FILE_OPEN_WRITE); if (fp != nullptr) { savegame_info->is_quick = slot.pool == SAVEGAME_SLOT_POOL_QUICK; SG_File_SaveToFile(fp, savegame_info); File_Close(fp); result = true; } if (result) { M_FillSlot(slot, full_path); } Memory_FreePointer(&file_name); Memory_FreePointer(&full_path); if (result) { m_MostRecentlyCreatedSlot = slot; if (was_slot_empty) { m_SavedGames++; } if (slot.pool == SAVEGAME_SLOT_POOL_QUICK && m_QuickSaveSlots > 0) { m_NextQuickSlot = (slot.index + 1) % m_QuickSaveSlots; } } else { m_SaveCounter--; } return result; } bool Savegame_Delete(const SAVEGAME_SLOT_REF slot) { if (!Savegame_IsValidSlotRef(slot) || Savegame_IsSlotFree(slot)) { return false; } SAVEGAME_INFO *const savegame_info = M_GetSavegameInfoSlot(slot); if (savegame_info == nullptr || savegame_info->full_path == nullptr) { return false; } const bool result = remove(savegame_info->full_path) == 0; if (!result) { return false; } M_ClearSlot(savegame_info); if (m_SavedGames > 0) { m_SavedGames--; } if (m_BoundSlot.pool == slot.pool && m_BoundSlot.index == slot.index) { m_BoundSlot = Savegame_InvalidSlot(); } if (m_MostRecentlyUsedSlot.pool == slot.pool && m_MostRecentlyUsedSlot.index == slot.index) { m_MostRecentlyUsedSlot = Savegame_InvalidSlot(); } if (m_MostRecentlyCreatedSlot.pool == slot.pool && m_MostRecentlyCreatedSlot.index == slot.index) { m_MostRecentlyCreatedSlot = Savegame_InvalidSlot(); } return true; } bool Savegame_Load(const SAVEGAME_SLOT_REF slot) { const SAVEGAME_INFO *const savegame_info = Savegame_GetSavegameInfo(slot); if (savegame_info == nullptr) { return false; } ASSERT(savegame_info->full_path != nullptr); M_LoadPreprocess(); bool result = false; MYFILE *const fp = File_Open(savegame_info->full_path, FILE_OPEN_READ); if (fp != nullptr) { result = SG_File_LoadFromFile(fp); File_Close(fp); } M_LoadPostprocess(); m_InitialVersion = savegame_info->initial_version; return result; } bool Savegame_UpdateDeathCounters( const SAVEGAME_SLOT_REF slot, const int32_t death_count) { const SAVEGAME_INFO *const savegame_info = Savegame_GetSavegameInfo(slot); if (savegame_info == nullptr) { return false; } ASSERT(savegame_info->full_path != nullptr); bool ret = false; MYFILE *const fp = File_Open(savegame_info->full_path, FILE_OPEN_READ_WRITE); if (fp != nullptr) { ret = SG_File_UpdateDeathCounters( fp, savegame_info->level_num, death_count, savegame_info->is_quick); File_Close(fp); } return ret; } bool Savegame_LoadOnlyResumeInfo(const SAVEGAME_SLOT_REF slot) { const SAVEGAME_INFO *const savegame_info = Savegame_GetSavegameInfo(slot); if (savegame_info == nullptr) { return false; } ASSERT(savegame_info->full_path != nullptr); bool ret = false; MYFILE *const fp = File_Open(savegame_info->full_path, FILE_OPEN_READ); if (fp != nullptr) { ret = SG_File_LoadOnlyResumeInfo(fp); File_Close(fp); } Savegame_SetInitialVersion(savegame_info->initial_version); return ret; } bool Savegame_RestartAvailable(const SAVEGAME_SLOT_REF slot) { if (!Savegame_IsValidSlotRef(slot)) { return true; } const SAVEGAME_INFO *const savegame_info = Savegame_GetSavegameInfo(slot); return savegame_info->features.restart; } ================================================ FILE: src/trx/game/savegame/common.h ================================================ #pragma once #include #include // Loading a saved game is divided into two phases. First, the game reads the // savegame file contents to look for the level number. The rest of the save // data is stored in a special buffer. Then the engine continues to execute the // normal game flow and loads the specified level. Second phase occurs after // everything finishes loading, e.g. items, creatures, triggers etc., and is // what actually sets Lara's health, creatures status, triggers, inventory etc. void Savegame_Init(void); void Savegame_Shutdown(void); bool Savegame_IsInitialised(void); void Savegame_ScanSavedGames(void); SAVEGAME_VERSION Savegame_GetInitialVersion(void); void Savegame_SetInitialVersion(SAVEGAME_VERSION version); int32_t Savegame_GetCounter(void); int32_t Savegame_GetTotalCount(void); int32_t Savegame_GetLevelNumber(SAVEGAME_SLOT_REF slot); bool Savegame_IsSlotFree(SAVEGAME_SLOT_REF slot); bool Savegame_RestartAvailable(SAVEGAME_SLOT_REF slot); // Remembers the slot used when the player starts a loaded game. // Persists across level reloads. void Savegame_BindSlot(SAVEGAME_SLOT_REF slot); // Removes the binding of the current slot. Used when the player exits to // title, issues a command like `/play` etc. void Savegame_UnbindSlot(void); // Returns the currently bound slot. If there is none, returns an invalid slot. SAVEGAME_SLOT_REF Savegame_GetBoundSlot(void); // Returns the most recently created save slot number. If there is none, // returns an invalid slot. SAVEGAME_SLOT_REF Savegame_GetMostRecentlyCreatedSlot(void); // Returns the most recently used slot save number. If there is none, returns // an invalid slot. SAVEGAME_SLOT_REF Savegame_GetMostRecentlyUsedSlot(void); void Savegame_ProcessItemsBeforeLoad(void); void Savegame_ProcessItemsBeforeSave(void); bool Savegame_Load(SAVEGAME_SLOT_REF slot); bool Savegame_Save(SAVEGAME_SLOT_REF slot); bool Savegame_Delete(SAVEGAME_SLOT_REF slot); bool Savegame_UpdateDeathCounters(SAVEGAME_SLOT_REF slot, int32_t death_count); bool Savegame_LoadOnlyResumeInfo(SAVEGAME_SLOT_REF slot); void Savegame_InitCurrentInfo(void); void Savegame_SetCurrentInfo(int32_t current_slot, int32_t src_slot); RESUME_INFO *Savegame_GetCurrentInfo(const GF_LEVEL *level); const SAVEGAME_INFO *Savegame_GetSavegameInfo(SAVEGAME_SLOT_REF slot); void Savegame_ResetCurrentInfo(const GF_LEVEL *level); void Savegame_CarryCurrentInfoToNextLevel( const GF_LEVEL *src_level, const GF_LEVEL *dst_level); void Savegame_PersistGameToCurrentInfo(const GF_LEVEL *level); void Savegame_ApplyLogicToCurrentInfo(const GF_LEVEL *level); int32_t Savegame_GetSlotCount(SAVEGAME_SLOT_POOL pool); SAVEGAME_SLOT_REF Savegame_GetNextQuickSlot(void); int32_t Savegame_GetQuickVisualCount(void); SAVEGAME_SLOT_REF Savegame_QuickFromVisualIndex(int32_t visual_index); int32_t Savegame_QuickToVisualIndex(SAVEGAME_SLOT_REF slot); bool Savegame_IsValidSlotRef(SAVEGAME_SLOT_REF slot); SAVEGAME_SLOT_REF Savegame_NormalSlot(int32_t index); SAVEGAME_SLOT_REF Savegame_QuickSlot(int32_t index); SAVEGAME_SLOT_REF Savegame_InvalidSlot(void); int32_t Savegame_SlotToParam(SAVEGAME_SLOT_REF slot); SAVEGAME_SLOT_REF Savegame_SlotFromParam(int32_t param); ================================================ FILE: src/trx/game/savegame/enum.h ================================================ #pragma once typedef enum { SAVEGAME_STAGE_BEFORE_LOAD, SAVEGAME_STAGE_AFTER_LOAD, SAVEGAME_STAGE_BEFORE_SAVE, } SAVEGAME_STAGE; typedef enum { SG_VERSION_LEGACY = -1, SG_VERSION_1 = 1, // Before TRX 1.0 SG_VERSION_13 = 13, // Separated Magnums and Automatic Pistols. SG_VERSION_14 = 14, // Replaced Lara mesh pointers with outfits SG_VERSION_15 = 15, // Music save format switched to stream list with play modes. SG_VERSION_16 = 16, // Carried-item drops are persisted with truthful statuses/positions. SG_VERSION_17 = 17, // Crystal statistics are persisted in savegames. SG_VERSION_18 = 18, SG_MIN_SUPPORTED_VERSION = SG_VERSION_13, SG_CURRENT_VERSION = SG_VERSION_18, } SAVEGAME_VERSION; ================================================ FILE: src/trx/game/savegame/file.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_MAGIC_TR1X MKTAG('T', '1', 'M', 'B') // TOOD: remove me after TRX 1.5 #define M_MAGIC_TR2X MKTAG('T', '2', 'X', 'B') // TOOD: remove me after TRX 1.5 #define M_MAGIC_TRX MKTAG('T', 'R', 'X', 'S') #define M_MUST(x) \ if (!(x)) { \ goto fail; \ } static JSON_VALUE *M_ReadRaw(MYFILE *fp, int32_t *version_out); const char *SG_File_GetSaveFilePattern(void) { return g_GameFlow.savegame_file_fmt; } const char *SG_File_GetQuickSaveFilePattern(void) { const char *const pattern = SG_File_GetSaveFilePattern(); const char *const placeholder = strchr(pattern, '%'); if (placeholder == nullptr) { return String_FormatStatic("%s_q", pattern); } const int32_t prefix_size = placeholder - pattern; return String_FormatStatic("%.*sq%s", prefix_size, pattern, placeholder); } static JSON_VALUE *M_ParseFromBuffer( const char *const buffer, int32_t *const version_out) { const SAVEGAME_BSON_HEADER *const header = (SAVEGAME_BSON_HEADER *)buffer; if (header->magic != M_MAGIC_TR1X && header->magic != M_MAGIC_TR2X && header->magic != M_MAGIC_TRX) { LOG_ERROR("Invalid savegame magic"); return nullptr; } if (version_out != nullptr) { *version_out = header->version; } const char *const compressed = buffer + sizeof(SAVEGAME_BSON_HEADER); char *uncompressed = Memory_Alloc(header->uncompressed_size); uLongf uncompressed_size = header->uncompressed_size; const int32_t error_code = uncompress( (Bytef *)uncompressed, &uncompressed_size, (const Bytef *)compressed, (uLongf)header->compressed_size); if (error_code != Z_OK) { LOG_ERROR("Failed to decompress the data (error %d)", error_code); Memory_FreePointer(&uncompressed); return nullptr; } JSON_VALUE *const root = BSON_Parse(uncompressed, uncompressed_size); Memory_FreePointer(&uncompressed); return root; } static JSON_VALUE *M_ReadRaw(MYFILE *const fp, int32_t *const version_out) { const size_t buffer_size = File_Size(fp); char *buffer = Memory_Alloc(buffer_size); File_Seek(fp, 0, FILE_SEEK_SET); File_ReadData(fp, buffer, buffer_size); JSON_VALUE *const result = M_ParseFromBuffer(buffer, version_out); Memory_FreePointer(&buffer); return result; } static void M_SaveRaw( MYFILE *const fp, const JSON_VALUE *const root, const int32_t level_num, const bool is_quick) { size_t uncompressed_size; char *uncompressed = BSON_Write(root, &uncompressed_size); uLongf compressed_size = compressBound(uncompressed_size); char *compressed = Memory_Alloc(compressed_size); const int32_t result = compress( (Bytef *)compressed, &compressed_size, (const Bytef *)uncompressed, (uLongf)uncompressed_size); if (result != Z_OK) { Shell_ExitSystem("Failed to compress savegame data"); } const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, level_num); const JSON_OBJECT *const root_obj = JSON_ValueAsObject(root); const SAVEGAME_BSON_HEADER header = { .magic = M_MAGIC_TRX, .initial_version = Savegame_GetInitialVersion(), .version = SG_CURRENT_VERSION, .compressed_size = compressed_size, .uncompressed_size = uncompressed_size, }; const SAVEGAME_BSON_EXTENDED_HEADER extra_header = { .flags = Game_GetBonusFlag() | (is_quick ? SAVEGAME_EXT_FLAG_QUICK : 0), .counter = JSON_ObjectGetInt(root_obj, "save_counter", Savegame_GetCounter()), .level_num = level->num, .title_size = level->title != nullptr ? strlen(level->title) : 0, }; File_WriteData(fp, &header, sizeof(header)); File_WriteData(fp, compressed, compressed_size); File_WriteData(fp, &extra_header, sizeof(extra_header)); File_WriteData( fp, level->title, level->title != nullptr ? strlen(level->title) : 0); Memory_FreePointer(&uncompressed); Memory_FreePointer(&compressed); } bool SG_File_LoadFromFile(MYFILE *const fp) { bool result = false; int32_t sg_version = -1; JSON_VALUE *const root = M_ReadRaw(fp, &sg_version); JSON_READ_IO *const io = JSON_ReadIO_Create(root, sg_version, nullptr); M_MUST(SG_File_LoadResumeInfoList(io)); M_MUST(SG_File_LoadMisc(io)); M_MUST(SG_File_LoadInventory(io)); M_MUST(SG_File_LoadFlipmaps(io)); M_MUST(SG_File_LoadCameras(io)); M_MUST(SG_File_LoadItems(io)); M_MUST(SG_File_LoadEffects(io)); M_MUST(SG_File_LoadFX(io)); M_MUST(SG_File_LoadFlares(io)); M_MUST(SG_File_LoadMusic(io)); M_MUST(SG_File_LoadLara(io)); result = true; fail: JSON_ReadIO_Destroy(io); JSON_ValueFree(root); return result; } void SG_File_SaveToFile(MYFILE *const fp, SAVEGAME_INFO *const info) { const GF_LEVEL *const current_level = Game_GetCurrentLevel(); JSON_WRITE_IO *const io = JSON_WriteIO_Create(); SG_File_DumpResumeInfoList(io); SG_File_DumpInventory(io); SG_File_DumpFlipmaps(io); SG_File_DumpCameras(io); SG_File_DumpItems(io); SG_File_DumpEffects(io); SG_File_DumpFX(io); SG_File_DumpLara(io); SG_File_DumpMusic(io); SG_File_DumpFlares(io); SG_File_DumpMisc(io); M_SaveRaw( fp, JSON_WriteIO_GetRoot(io), current_level->num, info != nullptr && info->is_quick); JSON_WriteIO_Destroy(io); } bool SG_File_FillInfo(MYFILE *const fp, SAVEGAME_INFO *const info) { *info = (SAVEGAME_INFO) {}; SAVEGAME_BSON_HEADER header = {}; File_Seek(fp, 0, FILE_SEEK_SET); if (!File_ReadData(fp, &header, sizeof(SAVEGAME_BSON_HEADER))) { return false; } if (header.magic != M_MAGIC_TR1X && header.magic != M_MAGIC_TR2X && header.magic != M_MAGIC_TRX) { return false; } if (header.version < SG_MIN_SUPPORTED_VERSION) { LOG_WARNING( "Too old SG version: %d (min supported: %d)", header.version, SG_MIN_SUPPORTED_VERSION); return false; } info->initial_version = header.initial_version; info->features.restart = header.initial_version >= SG_VERSION_LEGACY; info->features.select_level = header.initial_version >= SG_VERSION_1; // recover the slot information from the end of the file File_Skip(fp, header.compressed_size); SAVEGAME_BSON_EXTENDED_HEADER extra_header; if (File_ReadData(fp, &extra_header, sizeof(extra_header))) { info->counter = extra_header.counter; info->level_num = extra_header.level_num; info->is_quick = (extra_header.flags & SAVEGAME_EXT_FLAG_QUICK) != 0; info->level_title = Memory_Alloc(extra_header.title_size + 1); File_ReadData(fp, info->level_title, extra_header.title_size); return true; } // recover the slot information from the savegame structures bool result = false; File_Seek(fp, 0, FILE_SEEK_SET); JSON_VALUE *root = M_ReadRaw(fp, nullptr); JSON_OBJECT *root_obj = JSON_ValueAsObject(root); if (root_obj != nullptr) { info->counter = JSON_ObjectGetInt(root_obj, "save_counter", -1); info->level_num = JSON_ObjectGetInt(root_obj, "level_num", -1); const char *level_title = JSON_ObjectGetString(root_obj, "level_title", nullptr); if (level_title != nullptr) { info->level_title = Memory_DupStr(level_title); } result = info->level_num != -1; } JSON_ValueFree(root); return result; } bool SG_File_LoadOnlyResumeInfo(MYFILE *const fp) { int32_t sg_version = -1; JSON_VALUE *const root = M_ReadRaw(fp, &sg_version); JSON_READ_IO *const io = JSON_ReadIO_Create(root, sg_version, nullptr); const bool result = SG_File_LoadResumeInfoList(io); JSON_ReadIO_Destroy(io); JSON_ValueFree(root); return result; } bool SG_File_UpdateDeathCounters( MYFILE *const fp, int32_t level_num, const int32_t death_count, const bool is_quick) { bool result = false; JSON_VALUE *const root = M_ReadRaw(fp, nullptr); JSON_OBJECT *const root_obj = JSON_ValueAsObject(root); if (root_obj == nullptr) { LOG_ERROR("Cannot find the root object"); goto cleanup; } JSON_OBJECT *const misc_obj = JSON_ObjectGetObject(root_obj, "misc"); if (misc_obj == nullptr) { LOG_ERROR("Cannot find the misc object"); goto cleanup; } JSON_ObjectEvictKey(misc_obj, "death_count"); JSON_ObjectAppendInt(misc_obj, "death_count", death_count); const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); int32_t resume_idx = -1; for (int32_t i = 0; i < level_table->count; i++) { if (level_table->levels[i].num == level_num) { resume_idx = i; break; } } JSON_ARRAY *const resume_arr = JSON_ObjectGetArray(root_obj, "resume_info"); if (resume_arr != nullptr && resume_idx != -1) { JSON_OBJECT *const resume_obj = JSON_ArrayGetObject(resume_arr, resume_idx); if (resume_obj != nullptr) { JSON_ObjectEvictKey(resume_obj, "death_count"); JSON_ObjectAppendInt(resume_obj, "death_count", death_count); } } File_Seek(fp, 0, FILE_SEEK_SET); M_SaveRaw(fp, root, level_num, is_quick); result = true; cleanup: JSON_ValueFree(root); return result; } ================================================ FILE: src/trx/game/savegame/file.h ================================================ #pragma once #include #include #include #include #include #include const char *SG_File_GetSaveFilePattern(void); const char *SG_File_GetQuickSaveFilePattern(void); bool SG_File_FillInfo(MYFILE *fp, SAVEGAME_INFO *info); bool SG_File_LoadFromFile(MYFILE *fp); bool SG_File_LoadOnlyResumeInfo(MYFILE *fp); void SG_File_SaveToFile(MYFILE *fp, SAVEGAME_INFO *info); bool SG_File_UpdateDeathCounters( MYFILE *fp, int32_t level_num, int32_t death_count, bool is_quick); // Start of reader functions =================================================== bool SG_File_LoadLara(JSON_READ_IO *io); bool SG_File_LoadInventory(JSON_READ_IO *io); bool SG_File_LoadFlipmaps(JSON_READ_IO *io); bool SG_File_LoadCameras(JSON_READ_IO *io); bool SG_File_LoadItems(JSON_READ_IO *io); bool SG_File_LoadEffects(JSON_READ_IO *io); bool SG_File_LoadFX(JSON_READ_IO *io); bool SG_File_LoadFlares(JSON_READ_IO *io); bool SG_File_LoadMusic(JSON_READ_IO *io); bool SG_File_LoadResumeInfoList(JSON_READ_IO *io); bool SG_File_LoadMisc(JSON_READ_IO *io); // End of reader functions ===================================================== // Start of writer functions =================================================== void SG_File_DumpFlares(JSON_WRITE_IO *io); void SG_File_DumpEffects(JSON_WRITE_IO *io); void SG_File_DumpInventory(JSON_WRITE_IO *io); void SG_File_DumpFlipmaps(JSON_WRITE_IO *io); void SG_File_DumpCameras(JSON_WRITE_IO *io); void SG_File_DumpMusic(JSON_WRITE_IO *io); void SG_File_DumpItems(JSON_WRITE_IO *io); void SG_File_DumpFX(JSON_WRITE_IO *io); void SG_File_DumpLara(JSON_WRITE_IO *io); void SG_File_DumpResumeInfoList(JSON_WRITE_IO *io); void SG_File_DumpMisc(JSON_WRITE_IO *io); // End of writer functions ===================================================== #pragma pack(push, 1) typedef struct { uint32_t magic; int16_t initial_version; uint16_t version; int32_t compressed_size; int32_t uncompressed_size; } SAVEGAME_BSON_HEADER; typedef struct { uint32_t flags; int32_t counter; int32_t level_num; int32_t title_size; } SAVEGAME_BSON_EXTENDED_HEADER; #pragma pack(pop) #define SAVEGAME_EXT_FLAG_QUICK (1U << 31) ================================================ FILE: src/trx/game/savegame/file_read.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_SHOULD JSON_SHOULD #define M_OPTIONAL JSON_OPTIONAL #define M_MUST JSON_MUST #define M_FAIL JSON_FAIL #define M_FINISH JSON_FINISH static bool M_ReadObjectID( JSON_READ_IO *const io, const char *const key, OBJECT_ID *const target) { int32_t game_id = 0; M_MUST(JSON_READ(io, key, &game_id)); *target = Object_FromGameID(game_id); if (*target == NO_OBJECT) { JSON_ReadIO_SetError(io, "unsupported object #%d", game_id); M_FAIL(); } M_FINISH(); } static bool M_ReadArm( JSON_READ_IO *const io, const char *const key, LARA_ARM *const arm) { ASSERT(arm != nullptr); M_MUST(JSON_PUSH(io, key)); M_MUST(JSON_READ(io, "anim_num", &arm->anim_num)); M_MUST(JSON_READ(io, "frame_num", &arm->frame_num)); M_MUST(JSON_READ(io, "lock", &arm->lock)); M_MUST(JSON_READ(io, "flash_gun", &arm->flash_gun)); M_MUST(JSON_READ(io, "rot", &arm->rot)); M_MUST(JSON_POP(io)); M_FINISH(); } static bool M_ReadAmmo( JSON_READ_IO *const io, const char *const key, AMMO_INFO *const ammo) { ASSERT(ammo != nullptr); M_MUST(JSON_PUSH(io, key)); M_MUST(JSON_READ(io, "ammo", &ammo->ammo)); M_MUST(JSON_POP(io)); M_FINISH(); } static bool M_ReadLara(JSON_READ_IO *const io) { LARA_INFO *const lara = Lara_GetLaraInfo(); ASSERT(lara != nullptr); if (!M_OPTIONAL(JSON_READ(io, "item_number", &lara->item_num))) { // Introduced in TRX 1.2 M_MUST(JSON_READ(io, "item_num", &lara->item_num)); } M_MUST(JSON_READ(io, "gun_status", &lara->gun_status)); M_MUST(JSON_READ(io, "gun_type", &lara->gun_type)); M_MUST(JSON_READ(io, "request_gun_type", &lara->request_gun_type)); // TRX <1.1 if (g_TRVersion == 2 && JSON_ReadIO_GetVersion(io) < SG_VERSION_14) { if (lara->gun_type == LGT_MAGNUMS) { lara->gun_type = LGT_AUTOS; } if (lara->request_gun_type == LGT_MAGNUMS) { lara->request_gun_type = LGT_AUTOS; } } M_MUST(JSON_READ(io, "last_gun_type", &lara->last_gun_type)); M_MUST(JSON_READ(io, "calc_fall_speed", &lara->calc_fall_speed)); M_MUST(JSON_READ(io, "water_status", &lara->water_status)); M_MUST(JSON_READ(io, "climb_status", &lara->climb_status)); M_SHOULD(JSON_READ(io, "is_crouched", &lara->is_crouched)); M_SHOULD(JSON_READ(io, "keep_crouched", &lara->keep_crouched)); M_SHOULD(JSON_READ(io, "sprinting", &lara->sprinting)); M_MUST(JSON_READ(io, "pose_count", &lara->pose_count)); M_MUST(JSON_READ(io, "hit_frame", &lara->hit_frame)); M_MUST(JSON_READ(io, "hit_direction", &lara->hit_direction)); M_MUST(JSON_READ(io, "air", &lara->air)); M_MUST(JSON_READ(io, "sprint_timer", &lara->sprint_timer)); M_MUST(JSON_READ(io, "exposure_timer", &lara->exposure_timer)); M_SHOULD(JSON_READ(io, "poison_timer", &lara->poison_timer)); M_MUST(JSON_READ(io, "dive_count", &lara->dive_timer)); M_MUST(JSON_READ(io, "death_count", &lara->death_timer)); M_MUST(JSON_READ(io, "current_active", &lara->current.active)); M_SHOULD(JSON_READ(io, "current_vel_x", &lara->current.vel.x)); M_SHOULD(JSON_READ(io, "current_vel_z", &lara->current.vel.z)); M_MUST(JSON_READ(io, "burn", &lara->burn)); // Introduced in TRX 1.2 M_SHOULD(JSON_READ(io, "electric", &lara->electric)); M_MUST(JSON_READ(io, "mesh_effects", &lara->mesh_effects)); M_MUST(JSON_READ(io, "extra_anim", &lara->extra_anim)); M_MUST(JSON_READ(io, "water_surface_dist", &lara->water_surface_dist)); M_MUST(JSON_READ(io, "hit_effect_count", &lara->hit_effect_count)); int16_t hit_effect = NO_EFFECT; M_MUST(JSON_READ(io, "hit_effect", &hit_effect)); lara->hit_effect = hit_effect != NO_EFFECT && g_Config.gameplay.enable_enhanced_saves ? Effect_Get(hit_effect) : nullptr; int16_t vehicle_idx = Lara_Vehicle_GetIndex(); if (!M_OPTIONAL(JSON_READ(io, "vehicle_item_number", &vehicle_idx))) { // Introduced in TRX 1.2 M_MUST(JSON_READ(io, "vehicle_item_num", &vehicle_idx)); } Lara_Vehicle_SetIndex(vehicle_idx); M_MUST(JSON_READ(io, "flare_age", &lara->flare.age)); M_MUST(JSON_READ(io, "flare_frame", &lara->flare.frame_num)); M_MUST(JSON_READ(io, "flare_control_left", &lara->flare.control)); // < TRX 1.2 if (JSON_ReadIO_GetVersion(io) < SG_VERSION_15) { // TODO: remove in TRX 1.5. M_MUST(JSON_PUSH(io, "meshes")); const int32_t mesh_count = JSON_ARRAY_LEN(io); if (mesh_count != LM_NUMBER_OF) { JSON_ReadIO_SetError( io, "expected %d Lara meshes, got %d", LM_NUMBER_OF, mesh_count); M_FAIL(); } const OBJECT_MESH *meshes[LM_NUMBER_OF] = {}; for (int32_t i = 0; i < LM_NUMBER_OF; i++) { int32_t idx = 0; M_MUST(JSON_READ_A(io, i, &idx)); meshes[i] = Object_FindMesh(idx); } M_MUST(JSON_POP(io)); Lara_Skin_ExtractLegacyEquipment(meshes); } else { M_MUST(JSON_PUSH(io, "skin")); LARA_SKIN_TYPE skin_type = LARA_SKIN_TYPE_DEFAULT; bool skin_is_default = false; M_MUST(JSON_READ(io, "skin_type", &skin_type)); M_MUST(JSON_READ(io, "skin_is_default", &skin_is_default)); if (!skin_is_default) { Lara_Skin_SetType(skin_type); } bool holsters_visible = true; M_MUST(JSON_READ(io, "holsters_visible", &holsters_visible)); Lara_Skin_SetHolstersVisible(holsters_visible); M_MUST(JSON_PUSH(io, "equipment")); const int32_t mesh_count = JSON_ARRAY_LEN(io); if (mesh_count != LM_NUMBER_OF) { JSON_ReadIO_SetError( io, "expected %d equipment meshes, got %d", LM_NUMBER_OF, mesh_count); M_FAIL(); } for (int32_t i = 0; i < LM_NUMBER_OF; i++) { LARA_SKIN_EQUIPMENT_TYPE type = EQUIPMENT_TYPE_NONE; int32_t data = -1; M_MUST(JSON_PUSH_INDEX(io, i)); M_MUST(JSON_READ(io, "type", &type)); M_MUST(JSON_READ(io, "data", &data)); M_MUST(JSON_POP(io)); if (type == EQUIPMENT_TYPE_WEAPON) { Lara_Skin_SetGunEquipment(i, data); } else if (type == EQUIPMENT_TYPE_EXTRA) { Lara_Skin_SetExtraEquipment(i, data); } else { Lara_Skin_ClearEquipment(i); } } M_MUST(JSON_POP(io)); M_MUST(JSON_POP(io)); Lara_Skin_ApplyOutfit(); } lara->target = nullptr; M_MUST(JSON_READ(io, "target_angle1", &lara->target_angles[0])); M_MUST(JSON_READ(io, "target_angle2", &lara->target_angles[1])); M_MUST(JSON_READ(io, "turn_rate", &lara->turn_rate)); M_MUST(JSON_READ(io, "move_angle", &lara->move_angle)); M_MUST(JSON_READ(io, "head_rot", &lara->head_rot)); M_MUST(JSON_READ(io, "torso_rot", &lara->torso_rot)); M_MUST(JSON_READ(io, "last_pos", &lara->last_pos)); M_MUST(M_ReadArm(io, "left_arm", &lara->left_arm)); M_MUST(M_ReadArm(io, "right_arm", &lara->right_arm)); M_MUST(M_ReadAmmo(io, "pistols", &lara->pistol_ammo)); M_MUST(M_ReadAmmo(io, "magnums", &lara->magnum_ammo)); M_MUST(M_ReadAmmo(io, "uzis", &lara->uzi_ammo)); M_MUST(M_ReadAmmo(io, "shotgun", &lara->shotgun_ammo)); M_MUST(M_ReadAmmo(io, "harpoon", &lara->harpoon_ammo)); M_MUST(M_ReadAmmo(io, "grenade", &lara->grenade_ammo)); M_MUST(M_ReadAmmo(io, "m16", &lara->m16_ammo)); M_SHOULD(M_ReadAmmo(io, "autos", &lara->autos_ammo)); M_SHOULD(M_ReadAmmo(io, "desert_eagle", &lara->desert_eagle_ammo)); M_SHOULD(M_ReadAmmo(io, "mp5", &lara->mp5_ammo)); M_SHOULD(M_ReadAmmo(io, "rocket", &lara->rocket_ammo)); if (M_OPTIONAL(JSON_PUSH(io, "weapon"))) { lara->gun_item_num = Item_Create(); ITEM *const weapon_item = Item_Get(lara->gun_item_num); weapon_item->status = IS_ACTIVE; weapon_item->room_num = NO_ROOM; // Introduced in TRX 1.2 if (!M_SHOULD( M_ReadObjectID(io, "object_id", &weapon_item->object_id))) { M_MUST(M_ReadObjectID(io, "obj_id", &weapon_item->object_id)); } M_MUST(JSON_READ(io, "anim_num", &weapon_item->anim_num)); M_MUST(JSON_READ(io, "frame_num", &weapon_item->frame_num)); M_MUST(JSON_READ( io, "current_anim_state", &weapon_item->current_anim_state)); M_MUST(JSON_READ(io, "goal_anim_state", &weapon_item->goal_anim_state)); M_MUST(JSON_POP(io)); } M_MUST(JSON_PUSH(io, "interact_target")); M_MUST(JSON_READ(io, "item_num", &lara->interact_target.item_num)); M_MUST(JSON_READ(io, "move_count", &lara->interact_target.move_count)); M_MUST(JSON_READ(io, "is_moving", &lara->interact_target.is_moving)); M_MUST(JSON_POP(io)); M_FINISH(); } static bool M_IsValidItemObject( const OBJECT_ID saved_obj_id, const OBJECT_ID initial_obj_id) { if (saved_obj_id == initial_obj_id) { return true; } if (Object_IsType(initial_obj_id, g_GunObjects) && Object_IsType(saved_obj_id, g_GunObjects)) { return true; } // clang-format off switch (saved_obj_id) { // used keyholes case O_PUZZLE_DONE_1: return initial_obj_id == O_PUZZLE_HOLE_1; case O_PUZZLE_DONE_2: return initial_obj_id == O_PUZZLE_HOLE_2; case O_PUZZLE_DONE_3: return initial_obj_id == O_PUZZLE_HOLE_3; case O_PUZZLE_DONE_4: return initial_obj_id == O_PUZZLE_HOLE_4; // pickups case O_PISTOL_AMMO_ITEM: return initial_obj_id == O_PISTOL_ITEM; case O_SHOTGUN_AMMO_ITEM: return initial_obj_id == O_SHOTGUN_ITEM; case O_MAGNUM_AMMO_ITEM: return initial_obj_id == O_MAGNUM_ITEM; case O_AUTOS_AMMO_ITEM: return initial_obj_id == O_AUTOS_ITEM; case O_DESERT_EAGLE_AMMO_ITEM: return initial_obj_id == O_DESERT_EAGLE_ITEM; case O_UZI_AMMO_ITEM: return initial_obj_id == O_UZI_ITEM; case O_HARPOON_AMMO_ITEM: return initial_obj_id == O_HARPOON_ITEM; case O_M16_AMMO_ITEM: return initial_obj_id == O_M16_ITEM; case O_MP5_AMMO_ITEM: return initial_obj_id == O_MP5_ITEM; case O_GRENADE_AMMO_ITEM: return initial_obj_id == O_GRENADE_GUN_ITEM; case O_ROCKET_AMMO_ITEM: return initial_obj_id == O_ROCKET_GUN_ITEM; // dual-state animals case O_ALLIGATOR: return initial_obj_id == O_CROCODILE; case O_CROCODILE: return initial_obj_id == O_ALLIGATOR; case O_RAT: return initial_obj_id == O_VOLE; case O_VOLE: return initial_obj_id == O_RAT; // skidoo swaps case O_SKIDOO_FAST: return initial_obj_id == O_SKIDOO_ARMED; // default default: return false; } // clang-format on } static int16_t M_ResolveItem(JSON_READ_IO *const io, const int16_t read_index) { const char *item_name = nullptr; if (M_OPTIONAL(JSON_READ(io, "name", &item_name))) { const ITEM *const item = Item_GetByName(item_name); if (item == nullptr) { LOG_WARNING( "invalid item name '%s' (read index %d)", item_name, read_index); return NO_ITEM; } return Item_GetIndex(item); } int16_t item_num; if (!M_SHOULD(JSON_READ(io, "index", &item_num))) { item_num = read_index; // TODO: remove after TRX 2.0 } if (item_num < 0 || item_num >= Item_GetLevelCount()) { LOG_WARNING( "invalid item index %d (read index %d)", item_num, read_index); return NO_ITEM; } return item_num; } static bool M_ReadItem(JSON_READ_IO *const io, const int16_t read_index) { const int16_t item_num = M_ResolveItem(io, read_index); if (item_num == NO_ITEM) { // soft exit for unresolvable items return true; } ITEM *const item = Item_Get(item_num); OBJECT_ID object_id = NO_OBJECT; // Not all TR3 objects are implemented as of >= TRX 1.1 if (!M_SHOULD(M_ReadObjectID(io, "object_id", &object_id))) { item->object_id = O_DUMMY; return true; } const OBJECT *const obj = Object_Get(object_id); item->object_id = object_id; if (!M_IsValidItemObject(object_id, item->object_id)) { JSON_ReadIO_SetError( io, "level has %d (%s), save has %d (%s)", item->object_id, Object_GetName(item->object_id), object_id, Object_GetName(object_id)); M_FAIL(); } // Not sure why some items do not have their their position saved, // despite OBJECT telling them to. if (obj->save_position && JSON_ReadIO_HasKey(io, "room_num")) { M_MUST(JSON_READ(io, "pos", &item->pos)); M_MUST(JSON_READ(io, "rot", &item->rot)); M_MUST(JSON_READ(io, "speed", &item->speed)); M_MUST(JSON_READ(io, "fall_speed", &item->fall_speed)); int16_t room_num = NO_ROOM; M_MUST(JSON_READ(io, "room_num", &room_num)); if (room_num != NO_ROOM) { Item_UpdateRoom(item_num, room_num); } } if (obj->save_anim) { // TRX >= 1.1 animated puzzle holes became animated M_SHOULD(JSON_READ(io, "current_anim", &item->current_anim_state)); M_SHOULD(JSON_READ(io, "goal_anim", &item->goal_anim_state)); M_SHOULD(JSON_READ(io, "required_anim", &item->required_anim_state)); M_SHOULD(JSON_READ(io, "anim_num", &item->anim_num)); M_SHOULD(JSON_READ(io, "frame_num", &item->frame_num)); M_SHOULD(JSON_READ(io, "prev_frame_num", &item->prev_frame_num)); // Prevent issues with pre-injection saves and Lara's enhanced // animation set. if (item->object_id == O_LARA && item->anim_num < LARA_ORIGINAL_ANIM_COUNT) { item->anim_num += obj->anim_idx; } } if (obj->save_hitpoints) { M_MUST(JSON_READ(io, "hitpoints", &item->hit_points)); M_MUST(JSON_READ(io, "max_hitpoints", &item->max_hit_points)); } if (obj->save_flags) { if (!JSON_ReadIO_HasKey(io, "flags")) { // TRX 1.1 save-crystal entries were serialized as bare items // without save-state fields. Treat them as default-state crystals // so those legacy saves remain loadable. if (object_id == O_SAVE_CRYSTAL_ITEM) { goto skip_flags; } } M_MUST(JSON_READ(io, "flags", &item->flags)); M_MUST(JSON_READ(io, "timer", &item->timer)); ITEM_STATUS saved_status = item->status; M_MUST(JSON_READ(io, "status", &saved_status)); if ((item->flags & IF_KILLED) != 0) { Item_Kill(item_num); item->status = saved_status; } else { bool is_active; M_MUST(JSON_READ(io, "active", &is_active)); if (is_active && !item->active) { Item_AddActive(item_num); } item->status = saved_status; M_MUST(JSON_READ(io, "gravity", &item->gravity)); // Introduced in TRX 1.2 M_OPTIONAL(JSON_READ(io, "collidable", &item->collidable)); } // Introduced in TRX 1.2, not written if zero M_OPTIONAL(JSON_READ(io, "ai_bits", &item->ai_bits)); M_OPTIONAL(JSON_READ(io, "ai_tag", &item->ai_tag)); bool intelligent = obj->intelligent; // Introduced in TRX 1.2 M_SHOULD(JSON_READ(io, "intelligent", &intelligent)); if (intelligent) { LOT_EnableBaddieAI(item_num, true); CREATURE *const creature = item->creature_data; if (creature != nullptr) { M_MUST(JSON_READ(io, "head_rot", &creature->head_rotation)); M_MUST(JSON_READ(io, "neck_rot", &creature->neck_rotation)); M_MUST(JSON_READ(io, "max_turn", &creature->maximum_turn)); M_MUST(JSON_READ(io, "creature_flags", &creature->flags)); M_MUST(JSON_READ(io, "creature_mood", &creature->mood)); if (M_SHOULD(JSON_PUSH(io, "creature"))) { // Introduced in TRX 1.2 M_MUST(JSON_READ(io, "alerted", &creature->alerted)); M_MUST(JSON_READ(io, "head_left", &creature->head_left)); M_MUST(JSON_READ(io, "head_right", &creature->head_right)); M_MUST( JSON_READ(io, "reached_goal", &creature->reached_goal)); M_MUST(JSON_READ(io, "patrol_2", &creature->patrol_2)); M_MUST( JSON_READ(io, "hurt_by_lara", &creature->hurt_by_lara)); M_MUST(JSON_READ( io, "damage_from_lara", &creature->damage_from_lara)); M_MUST(JSON_PUSH(io, "joint_rotations")); for (int32_t i = 0; i < 4; i++) { // Introduced in TRX 1.2 M_SHOULD( JSON_READ_A(io, i, &creature->joint_rotation[i])); } M_MUST(JSON_POP(io)); M_MUST(JSON_POP(io)); } } } else if (obj->intelligent) { item->creature_data = nullptr; item->extra_rotations = nullptr; } } skip_flags: if (M_SHOULD(JSON_PUSH(io, "carried_items"))) { CARRIED_ITEM *carried_item = item->carried_item; CARRIED_ITEM *prev_item = nullptr; for (int32_t j = 0;; j++) { if (!JSON_PUSH_INDEX(io, j)) { break; } if (carried_item == nullptr) { carried_item = GameBuf_Alloc(sizeof(CARRIED_ITEM), GBUF_ITEMS); carried_item->next_item = nullptr; carried_item->spawn_num = NO_ITEM; if (prev_item != nullptr) { prev_item->next_item = carried_item; } else { item->carried_item = carried_item; } } // Introduced in TRX 1.2. Must be read for both newly allocated and // pre-existing carried entries (e.g. gameflow-defined drops). M_SHOULD(JSON_READ(io, "spawn_num", &carried_item->spawn_num)); M_MUST(M_ReadObjectID(io, "object_id", &carried_item->object_id)); M_MUST(JSON_READ(io, "pos", &carried_item->pos)); M_MUST(JSON_READ(io, "y_rot", &carried_item->rot.y)); M_MUST(JSON_READ(io, "room_num", &carried_item->room_num)); M_MUST(JSON_READ(io, "fall_speed", &carried_item->fall_speed)); M_MUST(JSON_READ(io, "status", &carried_item->status)); Carrier_SyncItem(item_num, carried_item); prev_item = carried_item; carried_item = carried_item->next_item; M_MUST(JSON_POP(io)); } // TODO: remove legacy branch in TRX 1.5. if (JSON_ReadIO_GetVersion(io) < SG_VERSION_17) { Carrier_TestItemDrops(item_num); } M_MUST(JSON_POP(io)); } if (obj->priv_size > 0 && obj->priv_load_func != nullptr) { // "priv" introduced in TRX 1.2 if (M_SHOULD(JSON_PUSH(io, "priv")) || M_SHOULD(JSON_PUSH(io, "data"))) { obj->priv_load_func(item, io); M_MUST(JSON_POP(io)); } } if (g_TRVersion >= 2) { // TODO: make this call in both engines consistently if (obj->handle_save_func != nullptr) { obj->handle_save_func(item, SAVEGAME_STAGE_AFTER_LOAD); } } M_FINISH(); } static bool M_ReadEffect(JSON_READ_IO *const io) { int32_t room_num = NO_ROOM; if (!M_OPTIONAL(JSON_READ(io, "room_number", &room_num))) { // Introduced in TRX 1.2 M_MUST(JSON_READ(io, "room_num", &room_num)); } const int16_t effect_num = Effect_Create(room_num); if (effect_num == NO_EFFECT) { return true; } EFFECT *const effect = Effect_Get(effect_num); M_MUST(JSON_READ(io, "pos", &effect->pos)); M_MUST(JSON_READ(io, "rot", &effect->rot)); if (!M_OPTIONAL(M_ReadObjectID(io, "object_number", &effect->object_id))) { // Introduced in TRX 1.2 M_MUST(M_ReadObjectID(io, "object_id", &effect->object_id)); } M_MUST(JSON_READ(io, "speed", &effect->speed)); M_MUST(JSON_READ(io, "fall_speed", &effect->fall_speed)); if (!M_OPTIONAL(JSON_READ(io, "frame_number", &effect->frame_num))) { // Introduced in TRX 1.2 M_MUST(JSON_READ(io, "frame_num", &effect->frame_num)); } M_MUST(JSON_READ(io, "counter", &effect->counter)); M_MUST(JSON_READ(io, "shade", &effect->shade)); JSON_SHOULD(JSON_READ(io, "flag1", &effect->flag1)); JSON_SHOULD(JSON_READ(io, "flag2", &effect->flag2)); M_FINISH(); } static bool M_ReadFlare(JSON_READ_IO *const io) { const int16_t item_num = Item_Create(); ITEM *const item = Item_Get(item_num); item->object_id = O_FLARE_ITEM; M_MUST(JSON_READ(io, "pos", &item->pos)); M_MUST(JSON_READ(io, "rot", &item->rot)); M_MUST(JSON_READ(io, "room_num", &item->room_num)); Item_Initialise(item_num); M_MUST(JSON_READ(io, "speed", &item->speed)); M_MUST(JSON_READ(io, "fall_speed", &item->fall_speed)); int32_t flare_age; M_MUST(JSON_READ(io, "age", &flare_age)); FlareItem_SetAge(item, flare_age & 0x7FFF, (flare_age & 0x8000) != 0); Item_AddActive(item_num); M_FINISH(); } static bool M_ReadFXRing(JSON_READ_IO *const io, FX_RING *const ring) { ASSERT(ring != nullptr); M_MUST(JSON_READ(io, "on", &ring->on)); M_MUST(JSON_READ(io, "life", &ring->life)); M_MUST(JSON_READ(io, "speed", &ring->speed)); M_MUST(JSON_READ(io, "radius", &ring->radius)); M_MUST(JSON_READ(io, "prev_radius", &ring->prev_radius)); XYZ_16 rot = {}; M_MUST(JSON_READ(io, "rot", &rot)); ring->rot = (XZ_16) { rot.x, rot.z }; XYZ_16 prev_rot = {}; M_MUST(JSON_READ(io, "prev_rot", &prev_rot)); ring->prev_rot = (XZ_16) { prev_rot.x, prev_rot.z }; M_MUST(JSON_READ(io, "pos", &ring->pos)); M_MUST(JSON_READ(io, "prev_pos", &ring->prev_pos)); M_FINISH(); } static bool M_ReadFXRings( JSON_READ_IO *const io, const FX_RING_TYPE type, const char *const key) { if (!M_OPTIONAL(JSON_PUSH(io, key))) { return true; } const int32_t ring_count = JSON_ARRAY_LEN(io); for (int32_t i = 0; i < ring_count; i++) { M_MUST(JSON_PUSH_INDEX(io, i)); FX_RING *const ring = FX_Ring_GetRing(type, i); if (ring != nullptr) { M_MUST(M_ReadFXRing(io, ring)); } else { LOG_WARNING( "Malformed save: too many %s rings. Extra rings will be " "ignored.", key); } M_MUST(JSON_POP(io)); } M_MUST(JSON_POP(io)); M_FINISH(); } static bool M_ShouldLoadMusicTimestamp( const MUSIC_ID track_id, const MUSIC_PLAY_MODE mode, const MUSIC_ID ambient_track) { const bool is_ambient = mode == MPM_LOOP && track_id == ambient_track; return !is_ambient || g_Config.audio.music_load_condition == MUSIC_LOAD_CONDITION_ALWAYS; } static bool M_ReadMusicTracks(JSON_READ_IO *const io) { MUSIC_ID ambient_track = MX_INACTIVE; M_MUST(JSON_READ(io, "current_ambient", &ambient_track)); Music_Stop(); if (ambient_track != MX_INACTIVE) { // Always restart the ambient as it may have changed based on the // current position in the level. Music_Play_Direct(ambient_track, MPM_LOOP); } if (g_Config.audio.music_load_condition == MUSIC_LOAD_CONDITION_NEVER) { return true; } if (M_SHOULD(JSON_PUSH(io, "streams"))) { // TRX 1.2 const int32_t stream_count = JSON_ARRAY_LEN(io); for (int32_t i = 0; i < stream_count; i++) { MUSIC_ID track_id = MX_INACTIVE; MUSIC_PLAY_MODE mode = MPM_ONCE; double timestamp = -1.0; M_MUST(JSON_PUSH_INDEX(io, i)); M_MUST(JSON_READ(io, "track", &track_id)); M_MUST(JSON_READ(io, "mode", &mode)); M_MUST(JSON_READ(io, "timestamp", ×tamp)); M_MUST(JSON_POP(io)); if (track_id == MX_INACTIVE) { continue; } if (!Music_Play_Direct(track_id, mode)) { LOG_WARNING("Could not load stream track %d", track_id); continue; } if (M_ShouldLoadMusicTimestamp(track_id, mode, ambient_track) && !Music_SeekTrackTimestamp(track_id, mode, timestamp)) { LOG_WARNING( "Could not load stream track %d at timestamp %lf.", track_id, timestamp); } } M_MUST(JSON_POP(io)); } else { MUSIC_ID current_track = MX_INACTIVE; double timestamp = -1.0; M_MUST(JSON_READ(io, "current_track", ¤t_track)); M_MUST(JSON_READ(io, "timestamp", ×tamp)); const bool is_ambient = current_track != MX_INACTIVE && current_track == ambient_track; if (!is_ambient && current_track != MX_INACTIVE && !Music_Play_Direct(current_track, MPM_ONCE)) { LOG_WARNING("Could not load current track %d.", current_track); } const MUSIC_ID track_to_seek = is_ambient ? ambient_track : current_track; const MUSIC_PLAY_MODE mode_to_seek = is_ambient ? MPM_LOOP : MPM_ONCE; if (M_ShouldLoadMusicTimestamp( track_to_seek, mode_to_seek, ambient_track) && !Music_SeekTrackTimestamp( track_to_seek, mode_to_seek, timestamp)) { LOG_WARNING( "Could not load current track %d at timestamp %lf.", current_track, timestamp); } } M_FINISH(); } static bool M_ReadMusicTrackFlags(JSON_READ_IO *const io) { if (!g_Config.audio.load_music_triggers) { return true; } const int32_t count = JSON_ARRAY_LEN(io); if (count > MAX_MUSIC_TRACKS) { JSON_ReadIO_SetError( io, "expected at most %d music track flags, got %d", MAX_MUSIC_TRACKS, count); M_FAIL(); } for (int32_t i = 0; i < count; i++) { uint32_t flags; M_MUST(JSON_READ_A(io, i, &flags)); Music_SetTrackFlags(i, flags); } M_FINISH(); } static bool M_ReadResumeInfo(JSON_READ_IO *const io, RESUME_INFO *const resume) { resume->lara_hitpoints = g_Config.gameplay.start_lara_hitpoints; M_MUST(JSON_READ(io, "lara_hitpoints", &resume->lara_hitpoints)); M_MUST(JSON_READ(io, "gun_status", &resume->gun_status)); // LGS_ARMLESS M_MUST( JSON_READ(io, "gun_type", &resume->equipped_gun_type)); // LGT_UNARMED M_MUST(JSON_READ( io, "holsters_gun_type", &resume->holsters_gun_type)); // LGT_UNKNOWN // TRX <1.1 if (g_TRVersion == 2 && JSON_ReadIO_GetVersion(io) < SG_VERSION_14) { if (resume->equipped_gun_type == LGT_MAGNUMS) { resume->equipped_gun_type = LGT_AUTOS; } if (resume->holsters_gun_type == LGT_MAGNUMS) { resume->holsters_gun_type = LGT_AUTOS; } } M_MUST( JSON_READ(io, "back_gun_type", &resume->back_gun_type)); // LGT_UNKNOWN M_MUST(JSON_READ(io, "costume", &resume->flags.costume)); M_MUST(JSON_READ(io, "pistol_ammo", &resume->pistol_ammo)); M_MUST(JSON_READ(io, "uzi_ammo", &resume->uzi_ammo)); M_MUST(JSON_READ(io, "shotgun_ammo", &resume->shotgun_ammo)); M_MUST(JSON_READ(io, "magnum_ammo", &resume->magnum_ammo)); // Introduced in TRX 1.1 M_SHOULD(JSON_READ(io, "autos_ammo", &resume->autos_ammo)); M_SHOULD(JSON_READ(io, "desert_eagle_ammo", &resume->desert_eagle_ammo)); M_MUST(JSON_READ(io, "m16_ammo", &resume->m16_ammo)); M_MUST(JSON_READ(io, "grenade_ammo", &resume->grenade_ammo)); M_MUST(JSON_READ(io, "harpoon_ammo", &resume->harpoon_ammo)); M_MUST(JSON_READ(io, "num_medis", &resume->small_medipacks)); M_MUST(JSON_READ(io, "num_big_medis", &resume->large_medipacks)); M_MUST(JSON_READ(io, "num_flares", &resume->flares)); M_MUST(JSON_READ(io, "num_scions", &resume->num_scions)); // Introduced in TRX 1.2 M_SHOULD(JSON_READ(io, "num_quest_item_1", &resume->num_quest_item_1)); M_SHOULD(JSON_READ(io, "num_quest_item_2", &resume->num_quest_item_2)); M_SHOULD(JSON_READ(io, "num_quest_item_3", &resume->num_quest_item_3)); M_SHOULD(JSON_READ(io, "num_quest_item_4", &resume->num_quest_item_4)); M_MUST(JSON_READ(io, "available", &resume->flags.available)); // Introduced in TRX 1.2 resume->level_completed = false; resume->prev_level = -1; resume->hurt_allies = false; M_SHOULD(JSON_READ(io, "level_completed", &resume->level_completed)); M_SHOULD(JSON_READ(io, "prev_level", &resume->prev_level)); M_SHOULD(JSON_READ(io, "hurt_allies", &resume->hurt_allies)); M_MUST(JSON_READ(io, "has_pistols", &resume->flags.has_pistols)); M_MUST(JSON_READ(io, "has_shotgun", &resume->flags.has_shotgun)); M_MUST(JSON_READ(io, "has_uzis", &resume->flags.has_uzis)); M_MUST(JSON_READ(io, "has_m16", &resume->flags.has_m16)); M_MUST(JSON_READ(io, "has_grenade", &resume->flags.has_grenade)); M_MUST(JSON_READ(io, "has_harpoon", &resume->flags.has_harpoon)); // Introduced in TRX 1.1 M_MUST(JSON_READ(io, "has_magnums", &resume->flags.has_magnums)); M_SHOULD(JSON_READ(io, "has_autos", &resume->flags.has_autos)); M_SHOULD( JSON_READ(io, "has_desert_eagle", &resume->flags.has_desert_eagle)); M_SHOULD(JSON_READ(io, "has_mp5", &resume->flags.has_mp5)); M_SHOULD(JSON_READ(io, "mp5_ammo", &resume->mp5_ammo)); M_SHOULD(JSON_READ(io, "has_rocket", &resume->flags.has_rocket)); M_SHOULD(JSON_READ(io, "rocket_ammo", &resume->rocket_ammo)); M_MUST(JSON_READ(io, "timer", &resume->stats.timer)); M_MUST(JSON_READ(io, "ammo_hits", &resume->stats.ammo_hits)); M_MUST(JSON_READ(io, "ammo_used", &resume->stats.ammo_used)); M_MUST(JSON_READ(io, "medipacks_used", &resume->stats.medipacks_used)); M_MUST( JSON_READ(io, "distance_travelled", &resume->stats.distance_travelled)); M_MUST(JSON_READ(io, "kills", &resume->stats.kill_count)); M_SHOULD(JSON_READ(io, "crystals", &resume->stats.crystal_count)); M_MUST(JSON_READ(io, "pickups", &resume->stats.pickup_count)); M_MUST(JSON_READ(io, "secrets", &resume->stats.secret_flags)); M_SHOULD(JSON_READ(io, "death_count", &resume->stats.death_count)); Stats_UpdateSecrets(&resume->stats); M_FINISH(); } bool SG_File_LoadInventory(JSON_READ_IO *const io) { M_MUST(JSON_PUSH(io, "inventory")); const GF_LEVEL *const current_level = Game_GetCurrentLevel(); struct { OBJECT_ID object_id; const char *const key; } objects[] = { { O_PICKUP_ITEM_1, "pickup1" }, { O_PICKUP_ITEM_2, "pickup2" }, { O_QUEST_ITEM_1, "quest1" }, { O_QUEST_ITEM_2, "quest2" }, { O_QUEST_ITEM_3, "quest3" }, { O_QUEST_ITEM_4, "quest4" }, { O_PUZZLE_ITEM_1, "puzzle1" }, { O_PUZZLE_ITEM_2, "puzzle2" }, { O_PUZZLE_ITEM_3, "puzzle3" }, { O_PUZZLE_ITEM_4, "puzzle4" }, { O_KEY_ITEM_1, "key1" }, { O_KEY_ITEM_2, "key2" }, { O_KEY_ITEM_3, "key3" }, { O_KEY_ITEM_4, "key4" }, { O_LEADBAR_ITEM, "leadbar" }, { NO_OBJECT, nullptr }, }; Lara_InitialiseInventory(current_level); for (int32_t i = 0; objects[i].key != nullptr; i++) { int16_t qty; if (JSON_READ(io, objects[i].key, &qty)) { while (Inv_RequestItem(objects[i].object_id) != 0) { Inv_RemoveItem(objects[i].object_id); } Inv_AddItemNTimes(objects[i].object_id, qty); } } M_MUST(JSON_POP(io)); M_FINISH(); } bool SG_File_LoadFlipmaps(JSON_READ_IO *const io) { M_MUST(JSON_PUSH(io, "flipmap")); bool status; M_MUST(JSON_READ(io, "status", &status)); if (status) { Room_FlipMap(); } int32_t flip_effect; int32_t flip_timer; M_MUST(JSON_READ(io, "effect", &flip_effect)); M_MUST(JSON_READ(io, "timer", &flip_timer)); Room_SetFlipEffect(flip_effect); Room_SetFlipTimer(flip_timer); M_MUST(JSON_PUSH(io, "table")); const size_t count = JSON_ARRAY_LEN(io); if (count != MAX_FLIP_MAPS) { JSON_ReadIO_SetError( io, "expected %d flipmap elements, got %d", MAX_FLIP_MAPS, count); M_FAIL(); } for (size_t i = 0; i < count; i++) { uint32_t flags; M_MUST(JSON_READ_A(io, i, &flags)); Room_SetFlipSlotFlags(i, flags << 8); } M_MUST(JSON_POP(io)); M_MUST(JSON_POP(io)); M_FINISH(); } bool SG_File_LoadCameras(JSON_READ_IO *const io) { M_MUST(JSON_PUSH(io, "cameras")); const size_t count = JSON_ARRAY_LEN(io); if (count != (size_t)Camera_GetFixedObjectCount()) { JSON_ReadIO_SetError( io, "expected %d cameras, got %d", Camera_GetFixedObjectCount(), count); M_FAIL(); } for (size_t i = 0; i < count; i++) { OBJECT_VECTOR *const object = Camera_GetFixedObject(i); M_MUST(JSON_READ_A(io, i, &object->flags)); } M_MUST(JSON_POP(io)); M_FINISH(); } bool SG_File_LoadLara(JSON_READ_IO *const io) { M_MUST(JSON_PUSH(io, "lara")); M_MUST(M_ReadLara(io)); M_MUST(JSON_POP(io)); M_FINISH(); } bool SG_File_LoadItems(JSON_READ_IO *const io) { M_MUST(JSON_PUSH(io, "items")); const int32_t count = JSON_ARRAY_LEN(io); Savegame_ProcessItemsBeforeLoad(); for (int32_t i = 0; i < count; i++) { M_MUST(JSON_PUSH_INDEX(io, i)); M_MUST(M_ReadItem(io, i)); M_MUST(JSON_POP(io)); } M_MUST(JSON_POP(io)); M_FINISH(); } bool SG_File_LoadEffects(JSON_READ_IO *const io) { if (!g_Config.gameplay.enable_enhanced_saves) { return true; } // Introduced in TRX 1.4 if (!M_SHOULD(JSON_PUSH(io, "effects"))) { M_MUST(JSON_PUSH(io, "fx")); } for (int32_t i = 0;; i++) { if (!JSON_PUSH_INDEX(io, i)) { break; } if (i < MAX_EFFECTS) { M_ReadEffect(io); } else { LOG_WARNING( "Malformed save: expected a max of %d effect, got at least " "%d. Extra effects will be ignored.", MAX_EFFECTS - 1, i); } M_MUST(JSON_POP(io)); } M_MUST(JSON_POP(io)); M_FINISH(); } bool SG_File_LoadFX(JSON_READ_IO *const io) { FX_Ring_Reset(); if (!M_OPTIONAL(JSON_PUSH(io, "vfx"))) { return true; } if (!M_OPTIONAL(JSON_PUSH(io, "rings"))) { M_MUST(JSON_POP(io)); return true; } M_MUST(M_ReadFXRings(io, FX_RING_TYPE_BLAST, "blast")); M_MUST(M_ReadFXRings(io, FX_RING_TYPE_KNOCKBACK, "knockback")); M_MUST(M_ReadFXRings(io, FX_RING_TYPE_SUMMON, "summon")); M_MUST(JSON_POP(io)); M_MUST(JSON_POP(io)); M_FINISH(); } bool SG_File_LoadFlares(JSON_READ_IO *const io) { M_MUST(JSON_PUSH(io, "flares")); for (int32_t i = 0;; i++) { if (!JSON_PUSH_INDEX(io, i)) { break; } M_MUST(M_ReadFlare(io)); M_MUST(JSON_POP(io)); } M_MUST(JSON_POP(io)); M_FINISH(); } bool SG_File_LoadMusic(JSON_READ_IO *const io) { M_MUST(JSON_PUSH(io, "music")); M_MUST(JSON_PUSH(io, "current")); M_MUST(M_ReadMusicTracks(io)); M_MUST(JSON_POP(io)); M_MUST(JSON_PUSH(io, "flags")); M_MUST(M_ReadMusicTrackFlags(io)); M_MUST(JSON_POP(io)); M_MUST(JSON_POP(io)); M_FINISH(); } bool SG_File_LoadResumeInfoList(JSON_READ_IO *const io) { M_MUST(JSON_PUSH(io, "resume_info")); const int32_t length = JSON_ARRAY_LEN(io); const int32_t expected_length = GF_GetLevelTable(GFLT_MAIN)->count; if (length != expected_length) { JSON_ReadIO_SetError( io, "expected %d resume info elements, got %d", expected_length, length); M_FAIL(); } for (int32_t i = 0; i < length; i++) { const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i); RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); M_MUST(JSON_PUSH_INDEX(io, i)); const bool has_prev_level = JSON_ReadIO_HasKey(io, "prev_level"); M_MUST(M_ReadResumeInfo(io, resume)); M_MUST(JSON_POP(io)); // TRX 1.0/1.1 did not store prev_level for resume entries. Infer the // canonical predecessor so "Play previous levels" can carry loadout. if (!has_prev_level && resume->prev_level == -1) { const GF_LEVEL *const prev_level = GF_GetLevelBefore(level); if (prev_level != nullptr) { resume->prev_level = prev_level->num; } } } M_MUST(JSON_POP(io)); M_FINISH(); } bool SG_File_LoadMisc(JSON_READ_IO *const io) { M_MUST(JSON_PUSH(io, "misc")); { int32_t bonus_flag = false; M_MUST(JSON_READ(io, "bonus_flag", &bonus_flag)); Game_SetBonusFlag(bonus_flag); } { bool allies_hostile = false; M_MUST(JSON_READ(io, "are_monks_angry", &allies_hostile)); Creature_SetAlliesHostile(allies_hostile); } { int32_t sunset_timer; M_MUST(JSON_READ(io, "sunset_timer", &sunset_timer)); Output_SetTimeInGame(sunset_timer); } { // Introduced in TRX 1.4 int32_t rng_control_seed = 0; if (M_OPTIONAL(JSON_READ(io, "rng_control_seed", &rng_control_seed))) { Random_SeedControl(rng_control_seed); } } { // Introduced in TRX 1.4 int32_t rng_draw_seed = 0; if (M_OPTIONAL(JSON_READ(io, "rng_draw_seed", &rng_draw_seed))) { Random_SeedDraw(rng_draw_seed); } } { const GF_LEVEL *const current_level = Game_GetCurrentLevel(); RESUME_INFO *const resume = Savegame_GetCurrentInfo(current_level); resume->stats.death_count = -1; M_MUST(JSON_READ(io, "death_count", &resume->stats.death_count)); } { int32_t weather_type = (int32_t)WEATHER_NONE; if (M_OPTIONAL(JSON_READ(io, "weather_type", &weather_type))) { if (weather_type >= (int32_t)WEATHER_NONE && weather_type <= (int32_t)WEATHER_SNOW) { FX_Weather_SetWeather((WEATHER_TYPE)weather_type); } else { FX_Weather_SetWeather(WEATHER_NONE); } } } M_MUST(JSON_POP(io)); M_FINISH(); } ================================================ FILE: src/trx/game/savegame/file_write.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef struct { int16_t count; int16_t id_map[MAX_EFFECTS]; } M_FX_ORDER; static void M_WriteXYZ32( JSON_WRITE_IO *const io, const char *const key, const XYZ_32 source) { JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "x", source.x); JSONW_WRITE(io, "y", source.y); JSONW_WRITE(io, "z", source.z); JSONW_POP_AND_SET(io, key); } static void M_WriteXYZ16( JSON_WRITE_IO *const io, const char *const key, const XYZ_16 source) { JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "x", source.x); JSONW_WRITE(io, "y", source.y); JSONW_WRITE(io, "z", source.z); JSONW_POP_AND_SET(io, key); } static void M_GetFXOrder(M_FX_ORDER *const order) { order->count = 0; for (int32_t i = 0; i < MAX_EFFECTS; i++) { order->id_map[i] = -1; } for (int16_t link_num = Effect_GetActiveNum(); link_num != NO_ITEM; link_num = Effect_Get(link_num)->next_active) { order->id_map[link_num] = order->count; order->count++; } } static void M_WriteItem( JSON_WRITE_IO *const io, const ITEM *const item, const M_FX_ORDER *const fx_order) { JSONW_WRITE(io, "index", Item_GetIndex(item)); if (item->name != nullptr) { JSONW_WRITE(io, "name", item->name); } const OBJECT *const obj = Object_Get(item->object_id); JSONW_WRITE(io, "object_id", Object_ToGameID(item->object_id)); if (obj->save_position) { M_WriteXYZ32(io, "pos", item->pos); M_WriteXYZ16(io, "rot", item->rot); JSONW_WRITE(io, "room_num", item->room_num); JSONW_WRITE(io, "speed", item->speed); JSONW_WRITE(io, "fall_speed", item->fall_speed); } if (obj->save_anim) { JSONW_WRITE(io, "current_anim", item->current_anim_state); JSONW_WRITE(io, "goal_anim", item->goal_anim_state); JSONW_WRITE(io, "required_anim", item->required_anim_state); JSONW_WRITE(io, "anim_num", item->anim_num); JSONW_WRITE(io, "frame_num", item->frame_num); JSONW_WRITE(io, "prev_frame_num", item->prev_frame_num); } if (obj->save_hitpoints) { JSONW_WRITE(io, "hitpoints", item->hit_points); JSONW_WRITE(io, "max_hitpoints", item->max_hit_points); } if (obj->save_flags) { JSONW_WRITE(io, "flags", item->flags); JSONW_WRITE(io, "status", item->status); JSONW_WRITE(io, "active", item->active); JSONW_WRITE(io, "gravity", item->gravity); JSONW_WRITE(io, "collidable", item->collidable); const bool intelligent = obj->intelligent && item->creature_data != nullptr; JSONW_WRITE(io, "intelligent", intelligent); JSONW_WRITE(io, "timer", item->timer); JSONW_WRITE_NZ(io, "ai_bits", item->ai_bits); JSONW_WRITE_NZ(io, "ai_tag", item->ai_tag); if (intelligent) { const CREATURE *const creature = item->creature_data; JSONW_WRITE(io, "head_rot", creature->head_rotation); JSONW_WRITE(io, "neck_rot", creature->neck_rotation); JSONW_WRITE(io, "max_turn", creature->maximum_turn); JSONW_WRITE(io, "creature_flags", creature->flags); JSONW_WRITE(io, "creature_mood", creature->mood); JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "alerted", creature->alerted); JSONW_WRITE(io, "head_left", creature->head_left); JSONW_WRITE(io, "head_right", creature->head_right); JSONW_WRITE(io, "reached_goal", creature->reached_goal); JSONW_WRITE(io, "patrol_2", creature->patrol_2); JSONW_WRITE(io, "hurt_by_lara", creature->hurt_by_lara); JSONW_WRITE(io, "damage_from_lara", creature->damage_from_lara); JSONW_PUSH_ARRAY(io); for (int32_t i = 0; i < 4; i++) { JSONW_PUSH_VALUE(io, creature->joint_rotation[i]); JSONW_POP_AND_APPEND(io); } JSONW_POP_AND_SET(io, "joint_rotations"); JSONW_POP_AND_SET(io, "creature"); } } JSONW_PUSH_ARRAY(io); const CARRIED_ITEM *drop_item = item->carried_item; while (drop_item != nullptr) { XYZ_32 drop_pos = drop_item->pos; int16_t drop_rot_y = drop_item->rot.y; int16_t drop_room_num = drop_item->room_num; int16_t drop_fall_speed = drop_item->fall_speed; const DROP_STATUS save_status = Carrier_GetSaveStatus(drop_item); if ((save_status == DS_FALLING || save_status == DS_DROPPED) && drop_item->spawn_num != NO_ITEM) { const ITEM *const pickup = Item_Get(drop_item->spawn_num); if (pickup != nullptr) { drop_pos = pickup->pos; drop_rot_y = pickup->rot.y; drop_room_num = pickup->room_num; drop_fall_speed = pickup->fall_speed; } } JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "object_id", Object_ToGameID(drop_item->object_id)); M_WriteXYZ32(io, "pos", drop_pos); JSONW_WRITE(io, "y_rot", drop_rot_y); JSONW_WRITE(io, "room_num", drop_room_num); JSONW_WRITE(io, "fall_speed", drop_fall_speed); JSONW_WRITE(io, "spawn_num", drop_item->spawn_num); JSONW_WRITE(io, "status", (int32_t)save_status); JSONW_POP_AND_APPEND(io); drop_item = drop_item->next_item; } JSONW_POP_AND_SET(io, "carried_items"); if (obj->priv_size > 0 && obj->priv_save_func != nullptr) { JSONW_PUSH_OBJECT(io); obj->priv_save_func(item, io); JSONW_POP_AND_SET(io, "priv"); } } static void M_WriteArm( JSON_WRITE_IO *const io, const char *const key, const LARA_ARM *const arm) { ASSERT(arm != nullptr); JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "anim_num", arm->anim_num); JSONW_WRITE(io, "frame_num", arm->frame_num); JSONW_WRITE(io, "lock", arm->lock); JSONW_WRITE(io, "flash_gun", arm->flash_gun); M_WriteXYZ16(io, "rot", arm->rot); JSONW_POP_AND_SET(io, key); } static void M_WriteAmmo( JSON_WRITE_IO *const io, const char *const key, const AMMO_INFO *const ammo) { ASSERT(ammo != nullptr); JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "ammo", ammo->ammo); JSONW_POP_AND_SET(io, key); } static void M_WriteLOT(JSON_WRITE_IO *const io, const LOT_INFO *const lot) { ASSERT(lot != nullptr); JSONW_WRITE(io, "head", lot->head); JSONW_WRITE(io, "tail", lot->tail); JSONW_WRITE(io, "search_num", lot->search_num); JSONW_WRITE(io, "block_mask", lot->setup.block_mask); JSONW_WRITE(io, "step", lot->setup.step); JSONW_WRITE(io, "drop", lot->setup.drop); JSONW_WRITE(io, "fly", lot->setup.fly); JSONW_WRITE(io, "zone_count", lot->zone_count); JSONW_WRITE(io, "target_box", lot->target_box); JSONW_WRITE(io, "required_box", lot->required_box); JSONW_WRITE(io, "x", lot->target.x); JSONW_WRITE(io, "y", lot->target.y); JSONW_WRITE(io, "z", lot->target.z); } static void M_WriteResumeInfo( JSON_WRITE_IO *const io, const RESUME_INFO *const resume) { JSONW_WRITE(io, "available", resume->flags.available); JSONW_WRITE(io, "level_completed", resume->level_completed); JSONW_WRITE(io, "prev_level", resume->prev_level); JSONW_WRITE(io, "hurt_allies", resume->hurt_allies); JSONW_WRITE(io, "lara_hitpoints", resume->lara_hitpoints); JSONW_WRITE(io, "pistol_ammo", resume->pistol_ammo); JSONW_WRITE(io, "shotgun_ammo", resume->shotgun_ammo); JSONW_WRITE(io, "magnum_ammo", resume->magnum_ammo); JSONW_WRITE(io, "autos_ammo", resume->autos_ammo); JSONW_WRITE(io, "desert_eagle_ammo", resume->desert_eagle_ammo); JSONW_WRITE(io, "uzi_ammo", resume->uzi_ammo); JSONW_WRITE(io, "m16_ammo", resume->m16_ammo); JSONW_WRITE(io, "mp5_ammo", resume->mp5_ammo); JSONW_WRITE(io, "grenade_ammo", resume->grenade_ammo); JSONW_WRITE(io, "rocket_ammo", resume->rocket_ammo); JSONW_WRITE(io, "harpoon_ammo", resume->harpoon_ammo); JSONW_WRITE(io, "num_medis", resume->small_medipacks); JSONW_WRITE(io, "num_big_medis", resume->large_medipacks); JSONW_WRITE(io, "num_flares", resume->flares); JSONW_WRITE(io, "num_scions", resume->num_scions); JSONW_WRITE(io, "num_quest_item_1", resume->num_quest_item_1); JSONW_WRITE(io, "num_quest_item_2", resume->num_quest_item_2); JSONW_WRITE(io, "num_quest_item_3", resume->num_quest_item_3); JSONW_WRITE(io, "num_quest_item_4", resume->num_quest_item_4); JSONW_WRITE(io, "gun_status", resume->gun_status); JSONW_WRITE(io, "gun_type", resume->equipped_gun_type); JSONW_WRITE(io, "holsters_gun_type", resume->holsters_gun_type); JSONW_WRITE(io, "back_gun_type", resume->back_gun_type); JSONW_WRITE(io, "has_pistols", resume->flags.has_pistols); JSONW_WRITE(io, "has_shotgun", resume->flags.has_shotgun); JSONW_WRITE(io, "has_magnums", resume->flags.has_magnums); JSONW_WRITE(io, "has_autos", resume->flags.has_autos); JSONW_WRITE(io, "has_desert_eagle", resume->flags.has_desert_eagle); JSONW_WRITE(io, "has_uzis", resume->flags.has_uzis); JSONW_WRITE(io, "has_m16", resume->flags.has_m16); JSONW_WRITE(io, "has_mp5", resume->flags.has_mp5); JSONW_WRITE(io, "has_grenade", resume->flags.has_grenade); JSONW_WRITE(io, "has_rocket", resume->flags.has_rocket); JSONW_WRITE(io, "has_harpoon", resume->flags.has_harpoon); JSONW_WRITE(io, "costume", resume->flags.costume); JSONW_WRITE(io, "timer", resume->stats.timer); JSONW_WRITE(io, "kills", resume->stats.kill_count); JSONW_WRITE(io, "secrets", resume->stats.secret_flags); JSONW_WRITE(io, "crystals", resume->stats.crystal_count); JSONW_WRITE(io, "pickups", resume->stats.pickup_count); JSONW_WRITE(io, "ammo_hits", resume->stats.ammo_hits); JSONW_WRITE(io, "ammo_used", resume->stats.ammo_used); JSONW_WRITE(io, "distance_travelled", resume->stats.distance_travelled); JSONW_WRITE(io, "medipacks_used", resume->stats.medipacks_used); JSONW_WRITE(io, "death_count", resume->stats.death_count); } static int32_t M_GetMusicTrackFlagsCount(void) { int32_t last_index = -1; for (int32_t i = 0; i < MAX_MUSIC_TRACKS; i++) { const uint16_t flags = Music_GetTrackFlags(i); if (flags != 0) { last_index = i; } } return last_index + 1; } static void M_WriteFXRings( JSON_WRITE_IO *const io, const FX_RING_TYPE type, const char *const key) { if (!FX_Ring_IsRingActive(type)) { return; } JSONW_PUSH_ARRAY(io); for (int32_t i = 0;; i++) { const FX_RING *const ring = FX_Ring_PeekRing(type, i); if (ring == nullptr) { break; } JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "on", ring->on); JSONW_WRITE(io, "life", ring->life); JSONW_WRITE(io, "speed", ring->speed); JSONW_WRITE(io, "radius", ring->radius); JSONW_WRITE(io, "prev_radius", ring->prev_radius); M_WriteXYZ16(io, "rot", (XYZ_16) { ring->rot.x, 0, ring->rot.z }); M_WriteXYZ16( io, "prev_rot", (XYZ_16) { ring->prev_rot.x, 0, ring->prev_rot.z }); M_WriteXYZ32(io, "pos", ring->pos); M_WriteXYZ32(io, "prev_pos", ring->prev_pos); JSONW_POP_AND_APPEND(io); } JSONW_POP_AND_SET_NZ(io, key); } void SG_File_DumpFlares(JSON_WRITE_IO *const io) { JSONW_PUSH_ARRAY(io); for (int32_t i = 0; i < Item_GetTotalCount(); i++) { const ITEM *const item = Item_Get(i); if (!item->active || item->object_id != O_FLARE_ITEM) { continue; } JSONW_PUSH_OBJECT(io); M_WriteXYZ32(io, "pos", item->pos); M_WriteXYZ16(io, "rot", item->rot); JSONW_WRITE(io, "room_num", item->room_num); JSONW_WRITE(io, "speed", item->speed); JSONW_WRITE(io, "fall_speed", item->fall_speed); const int32_t flare_age = FlareItem_GetAge(item); const int32_t active = FlareItem_IsActive(item) ? 0x8000 : 0; JSONW_WRITE(io, "age", flare_age | active); JSONW_POP_AND_APPEND(io); } JSONW_POP_AND_SET(io, "flares"); } void SG_File_DumpEffects(JSON_WRITE_IO *const io) { M_FX_ORDER fx_order; M_GetFXOrder(&fx_order); JSONW_PUSH_ARRAY(io); for (int16_t link_num = Effect_GetActiveNum(); link_num != NO_ITEM; link_num = Effect_Get(link_num)->next_active) { EFFECT *const effect = Effect_Get(link_num); if (Object_ToGameID(effect->object_id) == -1) { continue; } JSONW_PUSH_OBJECT(io); M_WriteXYZ32(io, "pos", effect->pos); M_WriteXYZ16(io, "rot", effect->rot); JSONW_WRITE(io, "room_num", effect->room_num); JSONW_WRITE(io, "object_id", Object_ToGameID(effect->object_id)); JSONW_WRITE(io, "speed", effect->speed); JSONW_WRITE(io, "fall_speed", effect->fall_speed); // Introduced in TRX 1.2 JSONW_WRITE(io, "frame_num", effect->frame_num); JSONW_WRITE(io, "frame_number", effect->frame_num); JSONW_WRITE(io, "counter", effect->counter); JSONW_WRITE(io, "shade", effect->shade); JSONW_WRITE(io, "flag1", effect->flag1); JSONW_WRITE(io, "flag2", effect->flag2); JSONW_POP_AND_APPEND(io); } JSONW_POP_AND_SET(io, "effects"); } void SG_File_DumpFX(JSON_WRITE_IO *const io) { JSONW_PUSH_OBJECT(io); JSONW_PUSH_OBJECT(io); M_WriteFXRings(io, FX_RING_TYPE_BLAST, "blast"); M_WriteFXRings(io, FX_RING_TYPE_KNOCKBACK, "knockback"); M_WriteFXRings(io, FX_RING_TYPE_SUMMON, "summon"); JSONW_POP_AND_SET_NZ(io, "rings"); JSONW_POP_AND_SET_NZ(io, "vfx"); } void SG_File_DumpInventory(JSON_WRITE_IO *const io) { JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "pickup1", Inv_RequestItem(O_PICKUP_ITEM_1)); JSONW_WRITE(io, "pickup2", Inv_RequestItem(O_PICKUP_ITEM_2)); JSONW_WRITE(io, "quest1", Inv_RequestItem(O_QUEST_ITEM_1)); JSONW_WRITE(io, "quest2", Inv_RequestItem(O_QUEST_ITEM_2)); JSONW_WRITE(io, "quest3", Inv_RequestItem(O_QUEST_ITEM_3)); JSONW_WRITE(io, "quest4", Inv_RequestItem(O_QUEST_ITEM_4)); JSONW_WRITE(io, "puzzle1", Inv_RequestItem(O_PUZZLE_ITEM_1)); JSONW_WRITE(io, "puzzle2", Inv_RequestItem(O_PUZZLE_ITEM_2)); JSONW_WRITE(io, "puzzle3", Inv_RequestItem(O_PUZZLE_ITEM_3)); JSONW_WRITE(io, "puzzle4", Inv_RequestItem(O_PUZZLE_ITEM_4)); JSONW_WRITE(io, "key1", Inv_RequestItem(O_KEY_ITEM_1)); JSONW_WRITE(io, "key2", Inv_RequestItem(O_KEY_ITEM_2)); JSONW_WRITE(io, "key3", Inv_RequestItem(O_KEY_ITEM_3)); JSONW_WRITE(io, "key4", Inv_RequestItem(O_KEY_ITEM_4)); JSONW_WRITE(io, "leadbar", Inv_RequestItem(O_LEADBAR_ITEM)); JSONW_POP_AND_SET(io, "inventory"); } void SG_File_DumpFlipmaps(JSON_WRITE_IO *const io) { JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "status", Room_GetFlipStatus()); JSONW_WRITE(io, "effect", Room_GetFlipEffect()); JSONW_WRITE(io, "timer", Room_GetFlipTimer()); JSONW_PUSH_ARRAY(io); for (int32_t i = 0; i < MAX_FLIP_MAPS; i++) { JSONW_PUSH_VALUE(io, Room_GetFlipSlotFlags(i) >> 8); JSONW_POP_AND_APPEND(io); } JSONW_POP_AND_SET(io, "table"); JSONW_POP_AND_SET(io, "flipmap"); } void SG_File_DumpCameras(JSON_WRITE_IO *const io) { JSONW_PUSH_ARRAY(io); for (int32_t i = 0; i < Camera_GetFixedObjectCount(); i++) { const OBJECT_VECTOR *const object = Camera_GetFixedObject(i); JSONW_PUSH_VALUE(io, object->flags); JSONW_POP_AND_APPEND(io); } JSONW_POP_AND_SET(io, "cameras"); } void SG_File_DumpMusic(JSON_WRITE_IO *const io) { const int32_t track_flag_count = M_GetMusicTrackFlagsCount(); JSONW_PUSH_OBJECT(io); JSONW_PUSH_ARRAY(io); for (int32_t i = 0; i < track_flag_count; i++) { JSONW_PUSH_VALUE(io, Music_GetTrackFlags(i)); JSONW_POP_AND_APPEND(io); } JSONW_POP_AND_SET(io, "flags"); const MUSIC_ID current_ambient = Music_GetCurrentLoopedTrack(); JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "current_ambient", current_ambient); JSONW_PUSH_ARRAY(io); const int32_t stream_count = Music_GetStreamCount(); for (int32_t i = 0; i < stream_count; i++) { MUSIC_STREAM_STATE state = {}; if (!Music_GetStreamState(i, &state)) { continue; } JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "track", state.track_id); JSONW_WRITE(io, "mode", state.mode); JSONW_WRITE(io, "timestamp", state.timestamp); JSONW_POP_AND_APPEND(io); } JSONW_POP_AND_SET(io, "streams"); JSONW_POP_AND_SET(io, "current"); JSONW_POP_AND_SET(io, "music"); } void SG_File_DumpItems(JSON_WRITE_IO *const io) { Savegame_ProcessItemsBeforeSave(); M_FX_ORDER fx_order; M_GetFXOrder(&fx_order); JSONW_PUSH_ARRAY(io); for (int32_t i = 0; i < Item_GetLevelCount(); i++) { JSONW_PUSH_OBJECT(io); M_WriteItem(io, Item_Get(i), &fx_order); JSONW_POP_AND_APPEND(io); } JSONW_POP_AND_SET(io, "items"); } void SG_File_DumpLara(JSON_WRITE_IO *const io) { const LARA_INFO *const lara = Lara_GetLaraInfo(); ASSERT(lara != nullptr); JSONW_PUSH_OBJECT(io); // Introduced in TRX 1.2 JSONW_WRITE(io, "item_num", lara->item_num); JSONW_WRITE(io, "item_number", lara->item_num); JSONW_WRITE(io, "gun_status", lara->gun_status); JSONW_WRITE(io, "gun_type", lara->gun_type); JSONW_WRITE(io, "request_gun_type", lara->request_gun_type); JSONW_WRITE(io, "last_gun_type", lara->last_gun_type); JSONW_WRITE(io, "calc_fall_speed", lara->calc_fall_speed); JSONW_WRITE(io, "water_status", lara->water_status); JSONW_WRITE(io, "climb_status", lara->climb_status); JSONW_WRITE(io, "is_crouched", lara->is_crouched); JSONW_WRITE(io, "keep_crouched", lara->keep_crouched); JSONW_WRITE(io, "sprinting", lara->sprinting); JSONW_WRITE(io, "pose_count", lara->pose_count); JSONW_WRITE(io, "hit_frame", lara->hit_frame); JSONW_WRITE(io, "hit_direction", lara->hit_direction); JSONW_WRITE(io, "hit_effect_count", lara->hit_effect_count); JSONW_WRITE( io, "hit_effect", lara->hit_effect ? Effect_GetIndex(lara->hit_effect) : 0); JSONW_WRITE(io, "air", lara->air); JSONW_WRITE(io, "sprint_timer", lara->sprint_timer); JSONW_WRITE(io, "exposure_timer", lara->exposure_timer); JSONW_WRITE(io, "poison_timer", lara->poison_timer); JSONW_WRITE(io, "dive_count", lara->dive_timer); JSONW_WRITE(io, "death_count", lara->death_timer); JSONW_WRITE(io, "current_active", lara->current.active); JSONW_WRITE(io, "current_vel_x", lara->current.vel.x); JSONW_WRITE(io, "current_vel_z", lara->current.vel.z); JSONW_WRITE(io, "burn", lara->burn); JSONW_WRITE(io, "electric", lara->electric); JSONW_WRITE(io, "water_surface_dist", lara->water_surface_dist); JSONW_WRITE(io, "flare_age", lara->flare.age); JSONW_WRITE(io, "flare_frame", lara->flare.frame_num); JSONW_WRITE(io, "flare_control_left", lara->flare.control); JSONW_WRITE(io, "extra_anim", lara->extra_anim); // Introduced in TRX 1.2 JSONW_WRITE(io, "vehicle_item_num", Lara_Vehicle_GetIndex()); JSONW_WRITE(io, "vehicle_item_number", Lara_Vehicle_GetIndex()); JSONW_WRITE(io, "mesh_effects", lara->mesh_effects); JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "skin_type", Lara_Skin_GetType()); JSONW_WRITE(io, "skin_is_default", Lara_Skin_IsDefaultType()); JSONW_WRITE(io, "holsters_visible", Lara_Skin_AreHolstersVisible()); JSONW_PUSH_ARRAY(io); for (int32_t i = 0; i < LM_NUMBER_OF; i++) { const LARA_SKIN_EQUIPMENT *const equipment = Lara_Skin_GetEquipment(i); JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "type", equipment->type); JSONW_WRITE(io, "data", equipment->data); JSONW_POP_AND_APPEND(io); } JSONW_POP_AND_SET(io, "equipment"); JSONW_POP_AND_SET(io, "skin"); JSONW_WRITE(io, "target_angle1", lara->target_angles[0]); JSONW_WRITE(io, "target_angle2", lara->target_angles[1]); JSONW_WRITE(io, "turn_rate", lara->turn_rate); JSONW_WRITE(io, "move_angle", lara->move_angle); M_WriteXYZ16(io, "head_rot", lara->head_rot); M_WriteXYZ16(io, "torso_rot", lara->torso_rot); M_WriteXYZ32(io, "last_pos", lara->last_pos); M_WriteArm(io, "left_arm", &lara->left_arm); M_WriteArm(io, "right_arm", &lara->right_arm); M_WriteAmmo(io, "pistols", &lara->pistol_ammo); M_WriteAmmo(io, "shotgun", &lara->shotgun_ammo); M_WriteAmmo(io, "magnums", &lara->magnum_ammo); M_WriteAmmo(io, "autos", &lara->autos_ammo); M_WriteAmmo(io, "desert_eagle", &lara->desert_eagle_ammo); M_WriteAmmo(io, "uzis", &lara->uzi_ammo); M_WriteAmmo(io, "harpoon", &lara->harpoon_ammo); M_WriteAmmo(io, "grenade", &lara->grenade_ammo); M_WriteAmmo(io, "rocket", &lara->rocket_ammo); M_WriteAmmo(io, "m16", &lara->m16_ammo); M_WriteAmmo(io, "mp5", &lara->mp5_ammo); if (lara->gun_item_num != NO_ITEM) { JSONW_PUSH_OBJECT(io); const ITEM *const weapon_item = Item_Get(lara->gun_item_num); JSONW_WRITE(io, "object_id", Object_ToGameID(weapon_item->object_id)); JSONW_WRITE(io, "anim_num", weapon_item->anim_num); JSONW_WRITE(io, "frame_num", weapon_item->frame_num); JSONW_WRITE(io, "current_anim_state", weapon_item->current_anim_state); JSONW_WRITE(io, "goal_anim_state", weapon_item->goal_anim_state); JSONW_POP_AND_SET(io, "weapon"); } JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "item_num", lara->interact_target.item_num); JSONW_WRITE(io, "move_count", lara->interact_target.move_count); JSONW_WRITE(io, "is_moving", lara->interact_target.is_moving); JSONW_POP_AND_SET(io, "interact_target"); JSONW_POP_AND_SET(io, "lara"); } void SG_File_DumpResumeInfoList(JSON_WRITE_IO *const io) { const int32_t count = GF_GetLevelTable(GFLT_MAIN)->count; JSONW_PUSH_ARRAY(io); for (int32_t i = 0; i < count; i++) { const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i); const RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); JSONW_PUSH_OBJECT(io); M_WriteResumeInfo(io, resume); JSONW_POP_AND_APPEND(io); } JSONW_POP_AND_SET(io, "resume_info"); } void SG_File_DumpMisc(JSON_WRITE_IO *const io) { const GF_LEVEL *const level = Game_GetCurrentLevel(); const RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); JSONW_PUSH_OBJECT(io); JSONW_WRITE(io, "game_version", g_TRXVersion); JSONW_WRITE(io, "bonus_flag", Game_GetBonusFlag()); JSONW_WRITE(io, "death_count", resume->stats.death_count); JSONW_WRITE(io, "are_monks_angry", Creature_AreAlliesHostile()); JSONW_WRITE(io, "sunset_timer", Output_GetTimeInGame()); JSONW_WRITE(io, "rng_control_seed", Random_GetControlSeed()); JSONW_WRITE(io, "rng_draw_seed", Random_GetDrawSeed()); JSONW_WRITE(io, "weather_type", FX_Weather_GetWeather()); JSONW_POP_AND_SET(io, "misc"); JSONW_WRITE(io, "level_title", level->title != nullptr ? level->title : ""); JSONW_WRITE(io, "save_counter", Savegame_GetCounter()); JSONW_WRITE(io, "level_num", level->num); } ================================================ FILE: src/trx/game/savegame/types.h ================================================ #pragma once #include #include #include #include typedef struct { uint8_t small_medipacks; uint8_t large_medipacks; uint16_t pistol_ammo; uint16_t magnum_ammo; uint16_t autos_ammo; uint16_t desert_eagle_ammo; uint16_t uzi_ammo; uint16_t shotgun_ammo; int32_t lara_hitpoints; LARA_GUN_STATE gun_status; LARA_GUN_TYPE equipped_gun_type; LARA_GUN_TYPE holsters_gun_type; LARA_GUN_TYPE back_gun_type; uint8_t num_scions; uint8_t num_quest_item_1; uint8_t num_quest_item_2; uint8_t num_quest_item_3; uint8_t num_quest_item_4; uint16_t m16_ammo; uint16_t mp5_ammo; uint16_t grenade_ammo; uint16_t rocket_ammo; uint16_t harpoon_ammo; uint16_t flares; struct { bool available; bool costume; bool has_pistols; bool has_magnums; bool has_autos; bool has_desert_eagle; bool has_uzis; bool has_shotgun; bool has_m16; bool has_mp5; bool has_grenade; bool has_rocket; bool has_harpoon; } flags; bool level_completed; int32_t prev_level; bool hurt_allies; LEVEL_STATS stats; } RESUME_INFO; typedef enum { SAVEGAME_SLOT_POOL_NORMAL = 0, SAVEGAME_SLOT_POOL_QUICK = 1, SAVEGAME_SLOT_POOL_NUMBER_OF, } SAVEGAME_SLOT_POOL; typedef struct { SAVEGAME_SLOT_POOL pool; int32_t index; } SAVEGAME_SLOT_REF; typedef struct { char *full_path; int32_t counter; int32_t level_num; char *level_title; int16_t initial_version; bool is_quick; struct { bool restart; bool select_level; } features; } SAVEGAME_INFO; ================================================ FILE: src/trx/game/savegame.h ================================================ #pragma once #include #include #include ================================================ FILE: src/trx/game/screenshot.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include static char *M_CleanScreenshotTitle(const char *const source) { // Sanitize screenshot title. // - Remove filesystem-sensitive characters // - Replace spaces with underscores // - Merge consecutive underscores together // - Remove leading underscores // - Remove trailing underscores char *result = Memory_Alloc(strlen(source) + 1); const char *const sensitive_characters = "/\\:*?\"<>|"; bool last_was_underscore = false; char *out = result; for (size_t i = 0; i < strlen(source); i++) { if (source[i] == ' ' || source[i] == '_') { if (!last_was_underscore && out > result) { *out++ = '_'; last_was_underscore = true; } continue; } const size_t char_size = String_GetCharByteSize(out); if (char_size != 1 || strchr(sensitive_characters, source[i]) == nullptr) { memcpy(out, source + i, char_size); out += char_size; i += char_size - 1; last_was_underscore = false; } } *out++ = '\0'; // Strip trailing underscores while (out > result && out[-1] == '_') { out--; } *out = '\0'; return result; } static char *M_GetScreenshotTitle(void) { const GF_LEVEL *const level = GF_GetCurrentLevel(); if (level == nullptr) { return Memory_DupStr("Intro"); } if (level->title != nullptr && strlen(level->title) > 0) { char *clean_level_title = M_CleanScreenshotTitle(level->title); if (clean_level_title != nullptr && strlen(clean_level_title) > 0) { return clean_level_title; } Memory_FreePointer(&clean_level_title); } // If title totally invalid, name it based on level number const char *const fmt = "Level_%d"; const size_t result_size = snprintf(nullptr, 0, fmt, level->num) + 1; char *result = Memory_Alloc(result_size); snprintf(result, result_size, fmt, level->num); return result; } static char *M_GetScreenshotBaseName(void) { char *screenshot_title = M_GetScreenshotTitle(); // Get timestamp char date_time[30]; Clock_GetDateTime(date_time, 30); // Full screenshot name char *const result = String_Format("%s_%s", date_time, screenshot_title); Memory_FreePointer(&screenshot_title); return result; } static const char *M_GetScreenshotFileExt(const SCREENSHOT_FORMAT format) { switch (format) { case SCREENSHOT_FORMAT_JPEG: return "jpg"; case SCREENSHOT_FORMAT_PNG: return "png"; default: return "jpg"; } } static char *M_GetScreenshotPath(const SCREENSHOT_FORMAT format) { char *base_name = M_GetScreenshotBaseName(); const char *const ext = M_GetScreenshotFileExt(format); char *rel_path = String_Format("%s.%s", base_name, ext); char *full_path = Memory_DupStr( TRXPath_Resolve(TRX_DYNAMIC_PATH_SCREENSHOT_WRITE_FILE, rel_path)); Memory_FreePointer(&rel_path); File_EnsureParentDirectories(full_path); if (File_Exists(full_path)) { for (int i = 2; i < 100; i++) { Memory_FreePointer(&full_path); rel_path = String_Format("%s_%d.%s", base_name, i, ext); full_path = Memory_DupStr(TRXPath_Resolve( TRX_DYNAMIC_PATH_SCREENSHOT_WRITE_FILE, rel_path)); Memory_FreePointer(&rel_path); if (!File_Exists(full_path)) { break; } } } Memory_FreePointer(&base_name); return full_path; } void Screenshot_Make(const SCREENSHOT_FORMAT format) { char *full_path = M_GetScreenshotPath(format); Output_MakeScreenshot(full_path); GameEvent_Fire((EVENT) { .name = GAME_EVENT_SCREENSHOT, .data = full_path, }); Memory_FreePointer(&full_path); } void Screenshot_MakeToPath(const char *const path) { Output_MakeScreenshot(path); } ================================================ FILE: src/trx/game/screenshot.h ================================================ #pragma once #include void Screenshot_Make(SCREENSHOT_FORMAT format); void Screenshot_MakeToPath(const char *path); ================================================ FILE: src/trx/game/shell/args.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include static void M_FreeArgVector(VECTOR *const args) { if (args == nullptr) { return; } for (int32_t i = 0; i < args->count; i++) { const char *const arg = *(char **)Vector_Get(args, i); Memory_Free((char *)arg); } Vector_Free(args); } static void M_ShowHelp(void) { puts("Available options:"); puts(""); puts("-h/--help: show this help."); puts(" --mod : launch a specific game or mod directly."); puts("-e/--engine <1|2|3>: pick a game engine explicitly."); puts("-l/--level : launch a level file or level number."); puts("-s/--save : launch from a specific save slot (starts at 1)."); puts(" --test-record : record gameplay events to file."); puts( " --test-replay/--test-play : replay gameplay events from " "file."); puts(" --headless: replay gameplay without showing a game window."); puts( " --headless-fps : control replay frame rate in headless mode."); puts("-q/--quiet: silence logs and only show errors."); puts( " --debug-render-performance: output diagnostic information after " "each " "frame."); puts(""); puts("Available mods:"); for (int32_t i = 0; i < Shell_GetModCount(); i++) { const SHELL_MOD *const mod = Shell_GetMod(i); if (mod == nullptr || !mod->is_available) { continue; } if (mod->mod_type == MOD_DIRECT_LEVEL) { continue; } if (mod->title != nullptr && strcmp(mod->title, mod->name) != 0) { printf(" %s (%s)\n", mod->name, mod->title); } else { printf(" %s\n", mod->name); } } puts(""); puts("Legacy options:"); puts("-g/--gold/-gold: launch the matching Gold expansion pack."); if (Shell_GetModByName("tr1-demo-pc") != nullptr) { puts(" --demo-pc/-demo_pc: launch the TR1 PC demo."); } puts("These options are deprecated; please use --mod instead."); } static int32_t M_GuessEngineVersionFromLevelPath(const char *const path) { if (path == nullptr || !File_Exists(path)) { return 0; } VFILE *const file = VFile_CreateFromPath(path); if (file == nullptr) { return 0; } const LEVEL_FORMAT_LOADER *const loader = Level_Format_GuessLoader(file); const int32_t game_version = loader != nullptr ? loader->game_version : 0; VFile_Close(file); return game_version; } SHELL_ARGS *Shell_ParseArgs(VECTOR *const args) { SHELL_ARGS *const result = Memory_Alloc(sizeof(SHELL_ARGS)); bool wants_gold = false; bool explicit_engine_version = false; result->save_to_load = -1; result->level_to_select = -1; result->original_args = args; result->engine_version = 0; // First pass: set the engine version. for (int32_t i = 0; args != nullptr && i < args->count; i++) { const char *const arg = *(char **)Vector_Get(args, i); const char *const next_arg = i + 1 < args->count ? *(char **)Vector_Get(args, i + 1) : nullptr; if (!strcmp(arg, "-e") || !strcmp(arg, "--engine")) { String_ParseInteger(next_arg, &result->engine_version); CLAMP(result->engine_version, 1, 3); explicit_engine_version = true; i++; } } if (result->engine_version <= 0 && g_TRVersion > 0) { // Hydrate recordings using old-style directory tree to use // runtime engine version if they miss it. result->engine_version = g_TRVersion; } // Second pass: remaining options. for (int32_t i = 0; args != nullptr && i < args->count; i++) { const char *const arg = *(char **)Vector_Get(args, i); const char *const next_arg = i + 1 < args->count ? *(char **)Vector_Get(args, i + 1) : nullptr; if (!strcmp(arg, "-e") || !strcmp(arg, "--engine")) { i++; } if (!strcmp(arg, "-h") || !strcmp(arg, "--help")) { M_ShowHelp(); Shell_FreeArgs(result); return nullptr; } if (!strcmp(arg, "-g") || !strcmp(arg, "--gold") || !strcmp(arg, "-gold")) { wants_gold = true; } if (!strcmp(arg, "--demo-pc") || !strcmp(arg, "-demo_pc")) { result->mod = Shell_GetModByName("tr1-demo-pc"); } if (!strcmp(arg, "--mod") && next_arg != nullptr) { const SHELL_MOD *const mod = Shell_GetModByName(next_arg); if (mod != nullptr) { result->mod = mod; } i++; } if ((!strcmp(arg, "-l") || !strcmp(arg, "--level")) && next_arg != nullptr) { int32_t lvnum = -1; if (String_ParseInteger(next_arg, &lvnum)) { result->level_to_select = lvnum; if (result->mod == nullptr && result->engine_version > 0) { result->mod = Shell_GetModByType( MOD_BASE_GAME, result->engine_version); } } else { char **const level_arg = Vector_Get(args, i + 1); ASSERT(level_arg != nullptr); const char *const resolved_level_path = TRXPath_PeekResolveUserPath( TRX_DYNAMIC_PATH_LEVEL_FILE, next_arg); result->level_to_play = resolved_level_path != nullptr ? Memory_DupStr(resolved_level_path) : nullptr; if (result->level_to_play == nullptr) { Shell_ExitSystemFmt( "Cannot find level file '%s'. Relative paths are " "resolved from the current working directory, then " "from the game directory.", next_arg); } Memory_Free(*level_arg); *level_arg = (char *)result->level_to_play; if (result->engine_version == 0) { result->engine_version = M_GuessEngineVersionFromLevelPath( result->level_to_play); } if (result->engine_version == 0) { Shell_ExitSystem( "Cannot determine engine version for --level. " "Please provide --engine."); } result->mod = Shell_GetModByType( MOD_DIRECT_LEVEL, result->engine_version); if (result->mod == nullptr) { Shell_ExitSystemFmt( "Engine %d does not support --level with a file path " "because no direct-level mod is available for that " "engine.", result->engine_version); } } i++; } if ((!strcmp(arg, "-s") || !strcmp(arg, "--save")) && next_arg != nullptr) { if (String_ParseInteger(next_arg, &result->save_to_load)) { result->save_to_load--; } i++; } if (!strcmp(arg, "--test-record") && next_arg != nullptr) { result->test_record_path = next_arg; i++; } if ((!strcmp(arg, "--test-play") || !strcmp(arg, "--test-replay")) && next_arg != nullptr) { result->test_replay_path = next_arg; i++; } if (!strcmp(arg, "--headless")) { result->headless = true; } if (!strcmp(arg, "--headless-fps") && next_arg != nullptr) { int32_t fps = 0; if (String_ParseInteger(next_arg, &fps) && fps > 0) { result->headless_fps = fps; } i++; } if (!strcmp(arg, "--debug-render-performance")) { result->debug_render_performance = true; } if (!strcmp(arg, "-q") || !strcmp(arg, "--quiet")) { result->quiet = true; } } if (result->mod == nullptr) { result->mod = Shell_SelectStartupMod(result->engine_version); } if (!explicit_engine_version && result->mod != nullptr) { result->engine_version = result->mod->engine_version; } if (wants_gold) { const int32_t engine_version = result->engine_version != 0 ? result->engine_version : (result->mod != nullptr ? result->mod->engine_version : 0); const SHELL_MOD *const gold_mod = Shell_GetModByType(MOD_EXPANSION_PACK, engine_version); if (gold_mod != nullptr) { result->mod = gold_mod; result->engine_version = gold_mod->engine_version; } } return result; } void Shell_FreeArgs(SHELL_ARGS *const args) { if (args == nullptr) { return; } M_FreeArgVector(args->original_args); args->original_args = nullptr; Memory_Free(args); } ================================================ FILE: src/trx/game/shell/args.h ================================================ #pragma once #include #include typedef struct { // Owned argv snapshot used as backing storage for pointer-valued fields // (e.g. level/replay/test paths). Freed by Shell_FreeArgs. VECTOR *original_args; int32_t engine_version; const SHELL_MOD *mod; int32_t level_to_select; const char *level_to_play; int32_t save_to_load; const char *test_record_path; const char *test_replay_path; bool headless; bool debug_render_performance; int32_t headless_fps; // in headless mode, force fixed fps (0 = unlocked) bool quiet; } SHELL_ARGS; // Adopts `raw_args`. // Requirements: // - `raw_args` items must be `char *` allocated on heap (or nullptr). // - ownership of vector + strings transfers to returned SHELL_ARGS. // - caller must not free or mutate `raw_args` after this call. SHELL_ARGS *Shell_ParseArgs(VECTOR *raw_args); // Frees SHELL_ARGS and its adopted `original_args` vector + strings. void Shell_FreeArgs(SHELL_ARGS *args); ================================================ FILE: src/trx/game/shell/common.c ================================================ #include #include #include #include #include #include #ifdef _WIN32 #include #include #endif #include #include #include #include #include static bool m_IsExiting = false; static bool m_IsFocused = true; static void M_ShowFatalError( const char *const log_message, const char *const dialog_message) { LOG_ERROR("%s", log_message); SDL_Window *const window = Shell_GetWindow(); SDL_ShowSimpleMessageBox( SDL_MESSAGEBOX_ERROR, "Tomb Raider Error", dialog_message, window); Shell_Terminate(1); } const char *Shell_GetConfigDir(void) { return TRXPath_Get(TRX_PATH_CONFIG_DIR); } const char *Shell_GetCacheDir(void) { return TRXPath_Get(TRX_PATH_CACHE_DIR); } void Shell_Terminate(int32_t exit_code) { Shell_Shutdown(); SDL_Window *const window = Shell_GetWindow(); if (window != nullptr) { SDL_DestroyWindow(window); } if (Audio_ShouldSkipSDLQuitAudio()) { const Uint32 inited = SDL_WasInit(0); const Uint32 quit_flags = inited & ~SDL_INIT_AUDIO; if (quit_flags != 0) { SDL_QuitSubSystem(quit_flags); } } else { SDL_Quit(); } exit(exit_code); } void Shell_ExitSystem(const char *message) { M_ShowFatalError(message, message); Shell_Shutdown(); } void Shell_ExitSystemEx( const char *const log_message, const char *const dialog_message) { M_ShowFatalError(log_message, dialog_message); Shell_Shutdown(); } void Shell_ExitSystemFmt(const char *fmt, ...) { va_list va; va_start(va, fmt); int32_t size = vsnprintf(nullptr, 0, fmt, va) + 1; char *message = Memory_Alloc(size); va_end(va); va_start(va, fmt); vsnprintf(message, size, fmt, va); va_end(va); Shell_ExitSystem(message); Memory_FreePointer(&message); } bool Shell_IsFullscreen(void) { SDL_Window *const window = Shell_GetWindow(); ASSERT(window != nullptr); const Uint32 flags = SDL_GetWindowFlags(window); return (flags & SDL_WINDOW_FULLSCREEN_DESKTOP) != 0; } SHELL_SIZE Shell_GetCurrentSize(void) { return Shell_IsFullscreen() ? Shell_GetCurrentDisplaySize() : Shell_GetWindowSize(); } SHELL_SIZE Shell_GetDefaultSize(void) { return (SHELL_SIZE) { SHELL_HEADLESS_WIDTH, SHELL_HEADLESS_HEIGHT }; } SHELL_SIZE Shell_GetWindowSize(void) { if (Shell_GetArgs()->headless) { return Shell_GetDefaultSize(); } SDL_Window *const window = Shell_GetWindow(); SHELL_SIZE result = { .w = -1, .h = -1 }; if (window != nullptr) { SDL_GetWindowSize(window, &result.w, &result.h); } return result; } SHELL_SIZE Shell_GetCurrentDisplaySize(void) { if (Shell_GetArgs()->headless) { return Shell_GetDefaultSize(); } int32_t display_idx = 0; SDL_Window *const window = Shell_GetWindow(); if (window != nullptr) { display_idx = SDL_GetWindowDisplayIndex(window); } SDL_DisplayMode dm; if (SDL_GetCurrentDisplayMode(display_idx, &dm) == 0) { return (SHELL_SIZE) { .w = dm.w, .h = dm.h }; } return (SHELL_SIZE) { .w = -1, .h = -1 }; } void Shell_ScheduleExit(void) { m_IsExiting = true; } bool Shell_IsExiting(void) { return m_IsExiting; } void Shell_SetIsFocused(const bool is_focused) { m_IsFocused = is_focused; } bool Shell_IsFocused(void) { return m_IsFocused; } ================================================ FILE: src/trx/game/shell/common.h ================================================ #pragma once #include #include #include #include #include #include typedef struct { int32_t w; int32_t h; } SHELL_SIZE; void Shell_Shutdown(void); SDL_Window *Shell_GetWindow(void); const char *Shell_GetConfigDir(void); const char *Shell_GetCacheDir(void); int32_t Shell_Main(const SHELL_ARGS *args); void Shell_Terminate(int32_t exit_code); void Shell_ScheduleExit(void); bool Shell_IsExiting(void); void Shell_SetIsFocused(bool is_focused); bool Shell_IsFocused(void); void Shell_RequestModSwitch(const char *mod_name); const char *Shell_GetPendingMod(void); void Shell_ClearPendingMod(void); bool Shell_GetPrevHeadless(void); bool Shell_GetPrevQuiet(void); const SHELL_ARGS *Shell_GetArgs(void); bool Shell_IsFullscreen(void); SHELL_SIZE Shell_GetDefaultSize(void); SHELL_SIZE Shell_GetWindowSize(void); SHELL_SIZE Shell_GetCurrentSize(void); SHELL_SIZE Shell_GetCurrentDisplaySize(void); ================================================ FILE: src/trx/game/shell/config.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include static Uint64 m_UpdateDebounce = 0; static bool m_IgnoreConfigChanges = false; static SHELL_SIZE m_ViewportSize = { .w = -1, .h = -1 }; static bool M_MustUpdateRendererViewport(void) { const SHELL_SIZE size = Shell_GetCurrentSize(); return m_ViewportSize.w != size.w || m_ViewportSize.h != size.h; } void Shell_RefreshRendererViewport(void) { Viewport_Reset(); m_ViewportSize = Shell_GetCurrentSize(); } void Shell_SyncToWindow(void) { m_UpdateDebounce = SDL_GetTicks(); LOG_DEBUG( "is_fullscreen=%d is_maximized=%d x=%d y=%d width=%d height=%d", g_Config.window.is_fullscreen, g_Config.window.is_maximized, g_Config.window.x, g_Config.window.y, g_Config.window.width, g_Config.window.height); SDL_Window *const window = Shell_GetWindow(); if (g_Config.window.is_fullscreen) { SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP); SDL_ShowCursor(SDL_DISABLE); } else if (g_Config.window.is_maximized) { SDL_SetWindowFullscreen(window, 0); SDL_MaximizeWindow(window); SDL_ShowCursor(SDL_ENABLE); } else { int32_t x = g_Config.window.x; int32_t y = g_Config.window.y; int32_t width = g_Config.window.width; int32_t height = g_Config.window.height; if (width <= 0 || height <= 0) { width = 1280; height = 720; } // Handle default position if (x == -1 && y == -1) { SDL_DisplayMode display_mode; SDL_GetCurrentDisplayMode(0, &display_mode); x = (display_mode.w - width) / 2; y = (display_mode.h - height) / 2; } else { // Adjust window position if completely offscreen bool on_screen = false; const int32_t num_displays = SDL_GetNumVideoDisplays(); for (int32_t i = 0; i < num_displays; i++) { SDL_Rect bounds; SDL_GetDisplayBounds(i, &bounds); if (x + width > bounds.x && x < bounds.x + bounds.w && y + height > bounds.y && y < bounds.y + bounds.h) { on_screen = true; break; } } if (!on_screen) { x = 0; y = 0; // Find the first display to reposition the window SDL_Rect bounds; SDL_GetDisplayBounds(0, &bounds); x = bounds.x + (bounds.w - width) / 2; y = bounds.y + (bounds.h - height) / 2; } } SDL_SetWindowFullscreen(window, 0); SDL_SetWindowPosition(window, x, y); SDL_SetWindowSize(window, width, height); SDL_ShowCursor(SDL_ENABLE); } } void Shell_SyncFromWindow(const bool update_viewport) { // Determine if this call should sync config, i.e., skip immediate // programmatic events const Uint32 now = SDL_GetTicks(); const bool skip_config = (now - m_UpdateDebounce) < 500; // Always pull current window state for logging and viewport reset SDL_Window *const window = Shell_GetWindow(); const Uint32 window_flags = SDL_GetWindowFlags(window); const bool is_maximized = window_flags & SDL_WINDOW_MAXIMIZED; int32_t x, y; int32_t width, height; SDL_GetWindowSize(window, &width, &height); SDL_GetWindowPosition(window, &x, &y); LOG_TRACE("%dx%d+%d,%d (maximized: %d)", width, height, x, y, is_maximized); // Update config only when not in debounce window if (!skip_config) { g_Config.window.is_maximized = is_maximized; if (!is_maximized && !g_Config.window.is_fullscreen) { g_Config.window.x = x; g_Config.window.y = y; g_Config.window.width = width; g_Config.window.height = height; } else { g_Config.window.fs_width = width; g_Config.window.fs_height = height; } if (g_Config.loaded) { m_IgnoreConfigChanges = true; Config_Update(); m_IgnoreConfigChanges = false; } } if (update_viewport || M_MustUpdateRendererViewport()) { // Refresh viewport to reflect the actual window size Shell_RefreshRendererViewport(); } } void Shell_HandleConfigChange(const CONFIG *const old, const CONFIG *const new) { if (!TestReplay_IsOpened()) { Config_Write(); } #define L_CHANGED(subject) (old->subject != new->subject) if (L_CHANGED(audio.sound_volume)) { Sound_SetMasterVolume(g_Config.audio.sound_volume); } if (L_CHANGED(audio.master_volume) || L_CHANGED(audio.music_volume) || L_CHANGED(audio.ambient_volume)) { Music_SetVolume(g_Config.audio.music_volume); } if (L_CHANGED(language)) { GameStringManager_ReloadLanguage(g_Config.language); } if (L_CHANGED(window.is_fullscreen) || L_CHANGED(window.is_maximized) || L_CHANGED(window.width) || L_CHANGED(window.height) || L_CHANGED(window.fs_width) || L_CHANGED(window.fs_height) || L_CHANGED(rendering.upscaling_factor) || L_CHANGED(rendering.borders) || L_CHANGED(rendering.aspect_mode)) { if (!m_IgnoreConfigChanges) { Shell_SyncToWindow(); } Shell_RefreshRendererViewport(); } if (L_CHANGED(visuals.fog_start) || L_CHANGED(visuals.fog_end) || L_CHANGED(visuals.fog_color.g) || L_CHANGED(visuals.fog_color.b) || L_CHANGED(visuals.fog_color.r) || L_CHANGED(visuals.fog_transparency) || L_CHANGED(visuals.water_color.g) || L_CHANGED(visuals.water_color.b) || L_CHANGED(visuals.water_color.r)) { Output_ApplyLevelSettings(); } if (L_CHANGED(visuals.enable_braid) || L_CHANGED(visuals.sunglasses_mode)) { Lara_Skin_ApplyOutfit(); } if (L_CHANGED(visuals.lara_outfit)) { Lara_Skin_ApplyOutfitFromConfig(); } if (L_CHANGED(rendering.upscaling_filter) || L_CHANGED(rendering.enable_wireframe) || L_CHANGED(rendering.wireframe_width) || L_CHANGED(rendering.enable_vsync) || L_CHANGED(rendering.anisotropy_filter)) { Output_ApplyRenderSettings(); } if (L_CHANGED(visuals.fov)) { if (Viewport_GetSystemFOV() == -1) { Viewport_AlterFOV(-1, FOV_MODE_GAME); } } if ((L_CHANGED(gameplay.maximum_save_slots) || L_CHANGED(gameplay.maximum_quick_save_slots)) && Savegame_IsInitialised()) { Savegame_Shutdown(); Savegame_Init(); Savegame_ScanSavedGames(); } #undef L_CHANGED } ================================================ FILE: src/trx/game/shell/config.h ================================================ #pragma once #include void Shell_SyncToWindow(void); void Shell_SyncFromWindow(bool update_viewport); void Shell_RefreshRendererViewport(void); void Shell_HandleConfigChange(const CONFIG *old, const CONFIG *new); ================================================ FILE: src/trx/game/shell/const.h ================================================ #pragma once #define SHELL_HEADLESS_WIDTH 1280 #define SHELL_HEADLESS_HEIGHT 720 ================================================ FILE: src/trx/game/shell/events.c ================================================ #include #include #include #include #include #include #include #include #include #include #include // If true, next SDL_TEXT* event should be zeroed out. static bool m_ConsoleJustOpened = false; static void M_HandleQuit(void) { Shell_ScheduleExit(); } static void M_HandleKeyDown(const SDL_Event *const event) { // NOTE: Opening the console normally would get handled by Input_Update, // but by the time Input_Update gets ran, we may already have lost some // keypresses if the player types really fast, so we need to react sooner. if (g_Config.gameplay.enable_console && !Console_IsOpened() && !Input_IsInListenMode() && Input_IsPressedEx( INPUT_BACKEND_KEYBOARD, g_Config.input.keyboard_layout, INPUT_ROLE_ENTER_CONSOLE)) { Console_Open(); // Zero out the next text event so the console-open glyph never // shows up. m_ConsoleJustOpened = true; } else { UI_HandleKeyDown(event->key.keysym.sym); } } static void M_HandleKeyUp(const SDL_Event *const event) { // NOTE: needs special handling on Windows - // SDL_SCANCODE_PRINTSCREEN is not sufficient to react to this. if (event->key.keysym.sym == SDLK_PRINTSCREEN) { Screenshot_Make(g_Config.rendering.screenshot_format); } } static void M_HandleFocusGained(void) { Shell_SetIsFocused(true); if (g_Config.audio.mute_out_of_focus) { Audio_Unmute(); } } static void M_HandleFocusLost(void) { Shell_SetIsFocused(false); if (g_Config.audio.mute_out_of_focus) { Audio_Mute(); } } static void M_HandleWindowShown(void) { LOG_DEBUG(""); } static void M_HandleWindowRestored(void) { Shell_SyncFromWindow(true); } static void M_HandleWindowMinimized(void) { LOG_DEBUG(""); } static void M_HandleWindowMaximized(void) { Shell_SyncFromWindow(true); } static void M_HandleWindowMoved(const int32_t x, const int32_t y) { Shell_SyncFromWindow(false); } static void M_HandleWindowResized(int32_t width, int32_t height) { Shell_SyncFromWindow(true); } static bool M_ProcessReplayEvent(const SDL_Event *const event) { switch (event->type) { case SDL_QUIT: M_HandleQuit(); return true; } return false; } bool Shell_ProcessEvent(const SDL_Event *const event) { Input_ProcessEvent(event); switch (event->type) { case SDL_QUIT: M_HandleQuit(); return true; case SDL_KEYDOWN: M_HandleKeyDown(event); return true; case SDL_KEYUP: M_HandleKeyUp(event); return true; case SDL_TEXTINPUT: if (m_ConsoleJustOpened) { m_ConsoleJustOpened = false; } else { UI_HandleTextEdit(event->text.text); } return true; case SDL_CONTROLLERDEVICEADDED: case SDL_JOYDEVICEADDED: case SDL_CONTROLLERDEVICEREMOVED: case SDL_JOYDEVICEREMOVED: Input_Discover(); return true; case SDL_WINDOWEVENT: switch (event->window.event) { case SDL_WINDOWEVENT_SHOWN: M_HandleWindowShown(); break; case SDL_WINDOWEVENT_FOCUS_GAINED: M_HandleFocusGained(); break; case SDL_WINDOWEVENT_FOCUS_LOST: M_HandleFocusLost(); break; case SDL_WINDOWEVENT_RESTORED: M_HandleWindowRestored(); break; case SDL_WINDOWEVENT_MINIMIZED: M_HandleWindowMinimized(); break; case SDL_WINDOWEVENT_MAXIMIZED: M_HandleWindowMaximized(); break; case SDL_WINDOWEVENT_MOVED: M_HandleWindowMoved(event->window.data1, event->window.data2); break; case SDL_WINDOWEVENT_RESIZED: M_HandleWindowResized(event->window.data1, event->window.data2); break; } break; } return false; } void Shell_ProcessEvents(void) { SDL_Event event; if (TestReplay_IsOpened()) { TestReplay_RunFrame(); while (SDL_PollEvent(&event) != 0) { M_ProcessReplayEvent(&event); } return; } if (TestRecorder_IsOpened()) { TestRecorder_BeginFrame(); } while (SDL_PollEvent(&event) != 0) { TestRecorder_RecordEvent(&event); Shell_ProcessEvent(&event); } if (TestRecorder_IsOpened()) { TestRecorder_EndFrame(); } } ================================================ FILE: src/trx/game/shell/events.h ================================================ #pragma once #include bool Shell_ProcessEvent(const SDL_Event *event); void Shell_ProcessEvents(void); ================================================ FILE: src/trx/game/shell/flow.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static SHELL_SESSION *m_Session = nullptr; static SDL_Window *m_Window = nullptr; static char *m_PendingMod = nullptr; // Flags preserved across mod switches (needed to rebuild args in main()). static bool m_PrevHeadless = false; static bool m_PrevQuiet = false; static void M_CreateGameWindow(void) { if (m_Window != nullptr) { return; // Window persists across mod switches } m_Window = SDL_CreateWindow( "TRX", g_Config.window.x, g_Config.window.y, g_Config.window.width, g_Config.window.height, SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_OPENGL); if (m_Window == nullptr) { Shell_ExitSystemFmt("Failed to create SDL window: %s", SDL_GetError()); } Shell_EnableThemeSupport(m_Window); } static void M_CreateGLContext(void) { if (TRX_GL_Context_GetWindowHandle() != nullptr) { return; // GL context persists across mod switches } SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); if (!TRX_GL_Context_Attach(m_Window)) { Shell_ExitSystem("System Error: cannot attach opengl context"); } } static void M_ShowWindow(void) { Shell_SyncToWindow(); SDL_ShowWindow(m_Window); SDL_RaiseWindow(m_Window); Shell_RefreshRendererViewport(); } static void M_HandleConfigChange(const EVENT *const event, void *const data) { const CONFIG *const old = &g_SavedConfig; const CONFIG *const new = &g_Config; Shell_HandleConfigChange(old, new); } static void M_SetupSDL(void) { SDL_version compiled; SDL_VERSION(&compiled); LOG_INFO( "SDL version: %d.%d.%d", compiled.major, compiled.minor, compiled.patch); if (SDL_Init(SDL_INIT_EVENTS | SDL_INIT_VIDEO) < 0) { Shell_ExitSystemFmt("Cannot initialize SDL: %s", SDL_GetError()); } } static void M_SetupGL(void) { // Setup minimum properties of GL context SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); } static void M_LoadCatalog( const CATALOG_CONTEXT context, const char *const filename, const bool allow_duplicates) { const char *const path = TRXPath_Resolve(TRX_DYNAMIC_PATH_CATALOG, filename); if (!Catalog_Load(context, path, allow_duplicates)) { Shell_ExitSystemFmt("Failed to load catalogs from %s", path); } } void Shell_RequestModSwitch(const char *const mod_name) { Memory_FreePointer(&m_PendingMod); m_PendingMod = Memory_DupStr(mod_name); } const char *Shell_GetPendingMod(void) { return m_PendingMod; } void Shell_ClearPendingMod(void) { Memory_FreePointer(&m_PendingMod); } bool Shell_GetPrevHeadless(void) { return m_PrevHeadless; } bool Shell_GetPrevQuiet(void) { return m_PrevQuiet; } const SHELL_ARGS *Shell_GetArgs(void) { ASSERT(m_Session != nullptr); return m_Session->args; } static void M_InitModules(void) { Shell_SetupHiDPI(); Shell_SetupLibAV(); M_SetupSDL(); M_SetupGL(); GameString_Init(); GameStringManager_Init(); UI_Init(); Overlay_Init(); GameEvent_Init(); GameBuf_Init(); Random_Seed(); Clock_Init(); LUA_Init(); } static void M_ShutdownModules(void) { if (TestReplay_IsOpened()) { TestReplay_Close(); } if (TestRecorder_IsOpened()) { TestRecorder_Close(); } Lara_Pose_Shutdown(); Lara_Skin_Shutdown(); Console_Shutdown(); Savegame_Shutdown(); GF_Shutdown(); LUA_Shutdown(); Overlay_Shutdown(); Option_Shutdown(); Output_Shutdown(); Input_Shutdown(); Music_Shutdown(); Sound_Shutdown(); UI_Shutdown(); GameEvent_Shutdown(); GameStringManager_Shutdown(); GameString_Shutdown(); Walkable_Shutdown(); Room_Shutdown(); GameBuf_Shutdown(); Catalog_Shutdown(); } static void M_PrepareSystem(void) { SHELL_SESSION *const s = m_Session; ASSERT(s != nullptr); const char *const test_replay_path = s->args->test_replay_path; if (s->args->test_record_path != nullptr && s->args->test_replay_path != nullptr) { Shell_ExitSystem("Cannot use both --test-record and --test-replay"); } if (test_replay_path != nullptr) { // Allow inferring engine version from outer args for replays lacking // embedded info (created with the old directory layout). g_TRVersion = s->args->engine_version; SHELL_ARGS *const tmp_args = TestReplay_Open(test_replay_path); if (tmp_args != nullptr) { tmp_args->headless = s->args->headless; tmp_args->debug_render_performance = s->args->debug_render_performance; ShellSession_UseArgs(s, tmp_args); } } else if (s->args->headless) { Shell_ExitSystem("--headless can only be used with --test-replay"); } g_TRVersion = s->args->engine_version; LOG_INFO("Engine version: %d", g_TRVersion); LOG_INFO("Mod: %s", s->args->mod != nullptr ? s->args->mod->name : nullptr); if (s->args->engine_version <= 0 || s->args->mod == nullptr) { Shell_ExitSystem("No playable mods available."); } if (s->args->mod->mod_type != MOD_DIRECT_LEVEL) { ShellState_RememberLastPlayedMod(s->args->mod->name); } Config_ApplyDefaultSettings(); TRXPath_Init(s->args); Input_Init(); Console_Init(); M_LoadCatalog(CATALOG_OBJECTS, "catalog_objects.csv", false); M_LoadCatalog(CATALOG_MUSIC, "catalog_music.csv", false); M_LoadCatalog(CATALOG_SAMPLES, "catalog_samples.csv", true); M_LoadCatalog(CATALOG_LARA_STATES, "catalog_lara_states.csv", false); M_LoadCatalog(CATALOG_LARA_ANIMS, "catalog_lara_anims.csv", false); M_LoadCatalog(CATALOG_ITEM_ACTIONS, "catalog_item_actions.csv", false); Lara_Pose_Init(); InvRing_LoadVars( TRXPath_Resolve(TRX_DYNAMIC_PATH_COMMON_CONFIG, "inv_ring.json5")); Gun_LoadVars( TRXPath_Resolve(TRX_DYNAMIC_PATH_COMMON_CONFIG, "weapons.json5")); UI_Settings_LoadFromFile( TRXPath_Resolve(TRX_DYNAMIC_PATH_COMMON_CONFIG, "ui.json5")); Lara_Skin_LoadFromFile( TRXPath_Resolve(TRX_DYNAMIC_PATH_COMMON_CONFIG, "outfits.json5")); Config_Presets_ScanFiles(); if (test_replay_path != nullptr) { TestReplay_Start(); } else { char *engine_config_path = TRXPath_ExpandVars("%config_dir%/TR%tr_version%X.json5"); if (engine_config_path == nullptr) { Shell_ExitSystem("Failed to resolve engine config path"); } Config_Read(engine_config_path, Shell_GetGameFlowPath(s->args->mod)); Memory_FreePointer(&engine_config_path); if (s->args->test_record_path != nullptr) { TestRecorder_Open( s->args->test_record_path, s->args->original_args); } } Config_SubscribeChanges(M_HandleConfigChange, nullptr); Clock_SetSimSpeed(Clock_GetSpeedMultiplier()); if (!s->args->headless) { Sound_Init(); Music_Init(); Sound_SetMasterVolume(g_Config.audio.sound_volume); Music_SetVolume(g_Config.audio.music_volume); } else { Clock_DisableWait(); const int32_t fps = s->args->headless_fps > 0 ? s->args->headless_fps : Clock_GetCurrentFPS(); Clock_EnableHeadlessFixedFPS(fps); } } SDL_Window *Shell_GetWindow(void) { return m_Window; } int32_t Shell_Main(const SHELL_ARGS *const args) { ASSERT(m_Session == nullptr); m_Session = ShellSession_Create(); SHELL_SESSION *const s = m_Session; ShellSession_UseArgs(s, args); LOG_INFO("Game directory: %s", TRXPath_Get(TRX_PATH_TRX_DIR)); M_InitModules(); M_PrepareSystem(); if (s->args->mod == nullptr) { Shell_ExitSystem("No --mod specified."); return 1; } TRXPath_Init(s->args); M_CreateGameWindow(); M_CreateGLContext(); Output_Init(); if (!s->args->headless) { M_ShowWindow(); } GF_Init(); GF_LoadFromFile(Shell_GetGameFlowPath(s->args->mod)); GameStringManager_ClearSourceFiles(); const char *const common_strings_path = Shell_GetCommonStringsPath(); if (common_strings_path == nullptr) { Shell_ExitSystem("Missing common strings file"); } GameStringManager_AddSourceFile(common_strings_path, false); if (s->args->mod->base_mod != nullptr) { const char *const base_strings_path = Shell_GetBaseGameStringsPath(s->args->mod); if (base_strings_path == nullptr) { Shell_ExitSystemFmt( "Missing base mod strings file for '%s'", s->args->mod->name); } GameStringManager_AddSourceFile(base_strings_path, false); } const char *const mod_strings_path = Shell_GetGameStringsPath(s->args->mod); if (mod_strings_path == nullptr) { Shell_ExitSystemFmt( "Missing strings file for selected mod '%s'", s->args->mod->name); } GameStringManager_AddSourceFile(mod_strings_path, true); GameStringManager_DiscoverLanguages(); GameStringManager_ReloadLanguage(g_Config.language); Savegame_Init(); Savegame_ScanSavedGames(); // Execute global Lua script if provided if (g_GameFlow.main_script_path != nullptr) { LUA_RESULT res = Lua_EvalFile(g_GameFlow.main_script_path); if (res.code != LUA_OK) { LOG_ERROR("Lua main script error: %s", res.message); } Lua_FreeResult(&res); } Stats_CalculateMaxStats(); GF_COMMAND gf_cmd = GF_DoFrontendSequence(); bool loop_continue = !Shell_IsExiting(); while (loop_continue) { LOG_INFO( "action=%s param=%d", ENUM_MAP_TO_STRING(GF_ACTION, gf_cmd.action), gf_cmd.param); switch (gf_cmd.action) { case GF_START_GAME: case GF_SELECT_GAME: { const int32_t level_num = gf_cmd.param; const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, level_num); const GF_SEQUENCE_CONTEXT seq_ctx = gf_cmd.action == GF_SELECT_GAME ? GFSC_SELECT : GFSC_NORMAL; if (level != nullptr) { gf_cmd = GF_DoLevelSequence(level, seq_ctx); } break; } case GF_GLOBE_SELECT: gf_cmd = GF_RunGlobeSelect(nullptr); break; case GF_START_SAVED_GAME: { const SAVEGAME_SLOT_REF slot = Savegame_SlotFromParam(gf_cmd.param); const int32_t level_num = Savegame_GetLevelNumber(slot); if (level_num < 0) { LOG_ERROR("Corrupt save file!"); gf_cmd = (GF_COMMAND) { .action = GF_EXIT_TO_TITLE }; } else { Savegame_BindSlot(slot); const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, level_num); gf_cmd = GF_DoLevelSequence(level, GFSC_SAVED); } break; } case GF_RESTART_GAME: { const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, gf_cmd.param); gf_cmd = GF_InterpretSequence(level, GFSC_RESTART, nullptr); break; } case GF_STORY_SO_FAR: gf_cmd = GF_PlayAvailableStory(Savegame_SlotFromParam(gf_cmd.param)); break; case GF_START_CINE: gf_cmd = GF_DoCutsceneSequence(gf_cmd.param, false); break; case GF_START_DEMO: gf_cmd = GF_DoDemoSequence(gf_cmd.param); break; case GF_NOOP: case GF_LEVEL_COMPLETE: gf_cmd = (GF_COMMAND) { .action = GF_EXIT_TO_TITLE }; break; case GF_EXIT_TO_TITLE: if (s->args->level_to_play != nullptr) { gf_cmd = (GF_COMMAND) { .action = GF_EXIT_GAME }; } else if (g_GameFlow.title_level == nullptr) { Shell_ExitSystem("Missing title level"); } else { gf_cmd = GF_RunTitle(); } break; case GF_EXIT_GAME: case GF_SWITCH_MOD: loop_continue = false; break; default: ASSERT_FAIL_FMT( "invalid action (action=%s, param=%d)", ENUM_MAP_TO_STRING(GF_ACTION, gf_cmd.action), gf_cmd.param); } } Game_SetCurrentLevel(nullptr); if (m_PendingMod != nullptr) { if (TestReplay_IsOpened()) { TestReplay_Close(); } if (TestRecorder_IsOpened()) { TestRecorder_Close(); } // Save flags needed to rebuild args in main() before freeing the // session (which owns and will free the args struct). m_PrevHeadless = s->args->headless; m_PrevQuiet = s->args->quiet; M_ShutdownModules(); ShellSession_Free(m_Session); m_Session = nullptr; return 0; } const int32_t replay_exit_code = TestReplay_GetExitCodeOverride(); return replay_exit_code >= 0 ? replay_exit_code : 0; } void Shell_Shutdown(void) { M_ShutdownModules(); TRX_GL_Context_Detach(); Log_Shutdown(); if (m_Session != nullptr) { ShellSession_Free(m_Session); m_Session = nullptr; } } ================================================ FILE: src/trx/game/shell/flow.h ================================================ #pragma once #include void Shell_InitCommonModules(void); void Shell_ShutdownCommonModules(void); const SHELL_ARGS *Shell_CommonInit(const SHELL_ARGS *args); ================================================ FILE: src/trx/game/shell/input.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include static void M_ToggleFullscreen(void) { TOGGLE(g_Config.window.is_fullscreen); Config_Update(); } static void M_ToggleFPSCounter(void) { TOGGLE(g_Config.ui.enable_fps_counter); Config_Update(); Console_Log( "%s", g_Config.ui.enable_fps_counter ? GS("general/osd/fps_counter_on") : GS("general/osd/fps_counter_off")); } static void M_ToggleBilinearFilter(void) { CYCLE(g_Config.rendering.texture_filter, 1, TEXTURE_FILTER_NUMBER_OF); Config_Update(); Console_Log( "%s", g_Config.rendering.texture_filter == TEXTURE_FILTER_BILINEAR ? GS("general/osd/bilinear_filter_on") : GS("general/osd/bilinear_filter_off")); } static void M_ToggleTrapezoidFilter(void) { TOGGLE(g_Config.rendering.enable_trapezoid_filter); Config_Update(); Console_Log( "%s", g_Config.rendering.enable_trapezoid_filter ? GS("general/osd/trapezoid_filter_on") : GS("general/osd/trapezoid_filter_off")); } static void M_ToggleWireframe(void) { TOGGLE(g_Config.rendering.enable_wireframe); Config_Update(); Console_Log( "%s", g_Config.rendering.enable_wireframe ? GS("general/osd/wireframe_mode_on") : GS("general/osd/wireframe_mode_off")); } static void M_ToggleTextures(void) { TOGGLE(g_Config.rendering.enable_textures); Config_Update(); Console_Log( "%s", g_Config.rendering.enable_textures ? GS("general/osd/textures_on") : GS("general/osd/textures_off")); } static void M_CycleLightingContrast(void) { CYCLE( g_Config.rendering.lighting_contrast, g_Input.slow ? -1 : 1, LIGHTING_CONTRAST_NUMBER_OF); Config_Update(); Console_Log( GS("general/osd/lighting_contrast_fmt"), ENUM_MAP_TO_STRING( LIGHTING_CONTRAST, g_Config.rendering.lighting_contrast)); } static void M_CycleUpscalingFactor(void) { g_Config.rendering.upscaling_factor += g_Input.slow ? -1 : 1; Config_Update(); Console_Log( GS("general/osd/upscaling_factor"), g_Config.rendering.upscaling_factor); } static void M_CycleBorders(void) { if (g_Input.slow) { if (g_Config.rendering.borders > 0.0) { g_Config.rendering.borders -= 0.05; CLAMPL(g_Config.rendering.borders, 0.0); Viewport_Reset(); } } else { if (g_Config.rendering.borders < 0.45) { g_Config.rendering.borders += 0.05; CLAMPG(g_Config.rendering.borders, 0.45); Viewport_Reset(); } } } void Shell_ProcessInput(void) { if (g_InputDB.screenshot) { Screenshot_Make(g_Config.rendering.screenshot_format); } if (g_InputDB.toggle_fullscreen) { M_ToggleFullscreen(); } if (g_InputDB.toggle_fps_counter) { M_ToggleFPSCounter(); } if (g_InputDB.toggle_bilinear_filter) { M_ToggleBilinearFilter(); } if (g_InputDB.toggle_trapezoid_filter) { M_ToggleTrapezoidFilter(); } if (g_InputDB.toggle_wireframe) { M_ToggleWireframe(); } if (g_InputDB.toggle_textures) { M_ToggleTextures(); } if (g_InputDB.cycle_lighting_contrast) { M_CycleLightingContrast(); } if (g_InputDB.switch_upscaling) { M_CycleUpscalingFactor(); } if (g_InputDB.switch_borders) { M_CycleBorders(); } if (g_InputDB.turbo_cheat && g_Config.gameplay.enable_cheats) { Clock_CycleTurboSpeed(!g_Input.slow); } if (g_InputDB.change_outfit) { Lara_Skin_CycleOutfit(g_Input.slow ? -1 : 1); } } ================================================ FILE: src/trx/game/shell/input.h ================================================ #pragma once void Shell_ProcessCommonInput(void); void Shell_ProcessInput(void); ================================================ FILE: src/trx/game/shell/main.c ================================================ #include #include #include #include #include #include #include #include #include #include int main(int argc, char *argv[]) { VECTOR *raw_args = Vector_Create(sizeof(const char *)); for (int32_t i = 1; i < argc; i++) { char *const copied_arg = Memory_DupStr(argv[i]); Vector_Add(raw_args, &copied_arg); } TRXPath_Init(nullptr); Shell_ScanAvailableMods(); SHELL_ARGS *args = Shell_ParseArgs(raw_args); if (args == nullptr) { return 0; } TRXPath_Init(args); char *log_path = String_Format("%s/TRX.log", TRXPath_Get(TRX_PATH_TRX_DIR)); Log_Init(log_path, args->quiet ? LOG_LEVEL_WARNING : LOG_LEVEL_MAX); Memory_FreePointer(&log_path); LOG_INFO("Starting %s", g_TRXVersion); Shell_ValidateMods(); if (args->mod == nullptr || !args->mod->is_valid) { args->mod = Shell_SelectStartupMod(args->engine_version); if (args->mod != nullptr && args->engine_version == 0) { args->engine_version = args->mod->engine_version; } } int32_t exit_code; bool restart; do { TRXPath_Init(args); restart = false; exit_code = Shell_Main(args); // Note: on a mod switch, Shell_Main has already freed args (via the // session) and reset m_Session to nullptr. Do not touch args after // this point in the restart branch. const char *const pending_mod = Shell_GetPendingMod(); if (pending_mod != nullptr) { const SHELL_MOD *const mod = Shell_GetModByName(pending_mod); Shell_ClearPendingMod(); if (mod != nullptr && mod->is_available) { LOG_INFO("Switching mod to: %s", mod->name); SHELL_ARGS *const next_args = Memory_Alloc(sizeof(SHELL_ARGS)); *next_args = (SHELL_ARGS) { .engine_version = mod->engine_version, .mod = mod, .level_to_select = -1, .save_to_load = -1, .headless = Shell_GetPrevHeadless(), .quiet = Shell_GetPrevQuiet(), }; args = next_args; TRXPath_Init(args); restart = true; } } } while (restart); Shell_Terminate(exit_code); return exit_code; } ================================================ FILE: src/trx/game/shell/mod.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef struct { GF_MOD_META meta; SHELL_MOD_TYPE mod_type; } M_KNOWN_MOD; static const M_KNOWN_MOD m_KnownModSeeds[] = { { .meta = { .name = "tr1", .engine = 1 }, .mod_type = MOD_BASE_GAME }, { .meta = { .name = "tr1-ub", .engine = 1, .extends = "tr1" }, .mod_type = MOD_EXPANSION_PACK }, { .meta = { .name = "tr1-demo-pc", .engine = 1, .extends = "tr1" }, .mod_type = MOD_MISC }, { .meta = { .name = "tr1-level", .engine = 1, .extends = "tr1" }, .mod_type = MOD_DIRECT_LEVEL }, { .meta = { .name = "tr2", .engine = 2 }, .mod_type = MOD_BASE_GAME }, { .meta = { .name = "tr2-gm", .engine = 2, .extends = "tr2" }, .mod_type = MOD_EXPANSION_PACK }, { .meta = { .name = "tr2-level", .engine = 2, .extends = "tr2" }, .mod_type = MOD_DIRECT_LEVEL }, { .meta = { .name = "tr3", .engine = 3 }, .mod_type = MOD_BASE_GAME }, { .meta = { .name = "tr3-la", .engine = 3, .extends = "tr3" }, .mod_type = MOD_EXPANSION_PACK }, { .meta = { .name = "tr3-level", .engine = 3, .extends = "tr3" }, .mod_type = MOD_DIRECT_LEVEL }, }; static VECTOR *m_Mods = nullptr; static SHELL_MOD *M_FindMod(const char *const name) { if (m_Mods == nullptr || name == nullptr) { return nullptr; } for (int32_t i = 0; i < m_Mods->count; i++) { SHELL_MOD *const mod = Vector_Get(m_Mods, i); if (strcmp(mod->name, name) == 0) { return mod; } } return nullptr; } static void M_AddMod( const char *const name, const char *const title, const SHELL_MOD_TYPE mod_type, const int32_t engine_version, const char *const base_mod) { const SHELL_MOD mod = { .name = Memory_DupStr(name), .title = title != nullptr ? Memory_DupStr(title) : nullptr, .mod_type = mod_type, .engine_version = engine_version, .base_mod = base_mod != nullptr ? Memory_DupStr(base_mod) : nullptr, .is_available = false, .is_valid = false, }; Vector_Add(m_Mods, &mod); } static void M_SeedKnownMods(void) { for (size_t i = 0; i < ARRAY_SIZE(m_KnownModSeeds); i++) { const M_KNOWN_MOD *const seed = &m_KnownModSeeds[i]; M_AddMod( seed->meta.name, nullptr, seed->mod_type, seed->meta.engine, seed->meta.extends); } } static void M_ScanForCustomMods(void) { const char *const games_dir = TRXPath_Get(TRX_PATH_GAMES_DIR); if (games_dir == nullptr) { return; } void *const dir = File_OpenDirectory(games_dir); if (dir == nullptr) { return; } const char *entry; while ((entry = File_ReadDirectory(dir)) != nullptr) { if (strcmp(entry, ".") == 0 || strcmp(entry, "..") == 0) { continue; } if (M_FindMod(entry) != nullptr) { continue; } const char *const gameflow_path = String_FormatStatic("%s/%s/gameflow.json5", games_dir, entry); GF_MOD_META meta = {}; if (!GF_ReadModMeta(gameflow_path, &meta)) { LOG_WARNING("Failed to read mod metadata from '%s'", gameflow_path); continue; } if (meta.engine <= 0) { LOG_WARNING( "Custom mod '%s' has no 'engine' field in gameflow; skipping", entry); Memory_FreePointer(&meta.name); Memory_FreePointer(&meta.extends); continue; } M_AddMod(entry, meta.name, MOD_CUSTOM, meta.engine, meta.extends); Memory_FreePointer(&meta.name); Memory_FreePointer(&meta.extends); } File_CloseDirectory(dir); } static void M_ReadModMetaForKnownMods(void) { for (int32_t i = 0; i < m_Mods->count; i++) { SHELL_MOD *const mod = Vector_Get(m_Mods, i); if (!mod->is_available || mod->mod_type == MOD_CUSTOM) { continue; } const char *const gameflow_path = TRXPath_Resolve(TRX_DYNAMIC_PATH_GAMEFLOW_FILE, mod->name); if (gameflow_path == nullptr) { continue; } GF_MOD_META meta = {}; if (!GF_ReadModMeta(gameflow_path, &meta)) { continue; } if (meta.name != nullptr) { Memory_FreePointer(&mod->title); mod->title = meta.name; meta.name = nullptr; } if (meta.engine > 0) { mod->engine_version = meta.engine; } if (meta.extends != nullptr) { Memory_FreePointer(&mod->base_mod); mod->base_mod = meta.extends; meta.extends = nullptr; } Memory_FreePointer(&meta.name); Memory_FreePointer(&meta.extends); } } static void M_ValidateEngineVersions(void) { for (int32_t i = 0; i < m_Mods->count; i++) { SHELL_MOD *const mod = Vector_Get(m_Mods, i); if (mod->engine_version <= 0 && mod->is_available) { LOG_WARNING( "Mod '%s' has no valid engine version; disabling", mod->name); mod->is_available = false; mod->is_valid = false; } } } static void M_ValidateNoMixedModLayouts(void) { const char *const games_dir = TRXPath_Get(TRX_PATH_GAMES_DIR); const char *const config_dir = TRXPath_Get(TRX_PATH_CONFIG_DIR); if (games_dir == nullptr || config_dir == nullptr || strcmp(games_dir, config_dir) == 0) { return; } for (int32_t i = 0; i < m_Mods->count; i++) { const SHELL_MOD *const mod = Vector_Get(m_Mods, i); if (mod->mod_type == MOD_CUSTOM) { continue; } const char *const legacy_gameflow = String_FormatStatic("%s/%s/gameflow.json5", config_dir, mod->name); if (File_Exists(legacy_gameflow)) { Shell_ExitSystemFmt( "Mixed mod layout detected: found legacy mod data at '%s' " "while '%s' is used for mods. Move '%s' to '%s/%s/'.", legacy_gameflow, games_dir, mod->name, games_dir, mod->name); } } } static const char *M_GetModStringsPath(const char *const mod_id) { ASSERT(mod_id != nullptr); return TRXPath_Join( TRX_PATH_GAMES_DIR, String_FormatStatic("%s/strings.json5", mod_id)); } __attribute__((destructor)) static void M_Shutdown(void) { if (m_Mods == nullptr) { return; } for (int32_t i = 0; i < m_Mods->count; i++) { SHELL_MOD *const mod = Vector_Get(m_Mods, i); Memory_FreePointer(&mod->name); Memory_FreePointer(&mod->title); Memory_FreePointer(&mod->base_mod); } Vector_Free(m_Mods); m_Mods = nullptr; } void Shell_ScanAvailableMods(void) { if (m_Mods != nullptr) { M_Shutdown(); } m_Mods = Vector_Create(sizeof(SHELL_MOD)); M_SeedKnownMods(); // Mark availability for all seeded mods. for (int32_t i = 0; i < m_Mods->count; i++) { SHELL_MOD *const mod = Vector_Get(m_Mods, i); mod->is_available = TRXPath_Exists(TRX_DYNAMIC_PATH_GAMEFLOW_FILE, mod->name); mod->is_valid = mod->is_available; } M_ValidateNoMixedModLayouts(); M_ScanForCustomMods(); // Mark availability for newly added custom mods. for (int32_t i = 0; i < m_Mods->count; i++) { SHELL_MOD *const mod = Vector_Get(m_Mods, i); if (mod->mod_type == MOD_CUSTOM) { mod->is_available = TRXPath_Exists(TRX_DYNAMIC_PATH_GAMEFLOW_FILE, mod->name); mod->is_valid = mod->is_available; } } M_ReadModMetaForKnownMods(); M_ValidateEngineVersions(); } void Shell_ValidateMods(void) { const int32_t original_tr_version = g_TRVersion; for (int32_t i = 0; i < m_Mods->count; i++) { SHELL_MOD *const mod = Vector_Get(m_Mods, i); if (!mod->is_available) { mod->is_valid = false; continue; } const SHELL_ARGS args = { .engine_version = mod->engine_version, .mod = mod, .level_to_select = -1, .save_to_load = -1, }; g_TRVersion = mod->engine_version; TRXPath_Init(&args); mod->is_valid = GF_ValidateMod(mod->name, Shell_GetGameFlowPath(mod)); } g_TRVersion = original_tr_version; } int32_t Shell_GetModCount(void) { return m_Mods != nullptr ? m_Mods->count : 0; } const SHELL_MOD *Shell_GetMod(const int32_t index) { if (index < 0 || index >= Shell_GetModCount()) { return nullptr; } return Vector_Get(m_Mods, index); } const SHELL_MOD *Shell_GetModByName(const char *const name) { if (m_Mods == nullptr || name == nullptr) { return nullptr; } for (int32_t i = 0; i < m_Mods->count; i++) { const SHELL_MOD *const mod = Vector_Get(m_Mods, i); if (mod->is_available && strcmp(mod->name, name) == 0) { return mod; } } return nullptr; } static bool M_MatchesEngineVersion( const SHELL_MOD *const mod, const int32_t engine_version) { return engine_version == 0 || mod->engine_version == engine_version; } static const SHELL_MOD *M_GetFirstAvailableMod(const int32_t engine_version) { for (int32_t i = 0; i < m_Mods->count; i++) { const SHELL_MOD *const mod = Vector_Get(m_Mods, i); if (!Shell_CanSwitchToMod(mod) || !M_MatchesEngineVersion(mod, engine_version)) { continue; } return mod; } return nullptr; } const SHELL_MOD *Shell_SelectStartupMod(const int32_t engine_version) { const char *const last_played_mod = ShellState_GetLastPlayedMod(); if (last_played_mod != nullptr) { const SHELL_MOD *const mod = Shell_GetModByName(last_played_mod); if (mod != nullptr && Shell_CanSwitchToMod(mod) && M_MatchesEngineVersion(mod, engine_version)) { return mod; } } return M_GetFirstAvailableMod(engine_version); } const SHELL_MOD *Shell_GetModByType( const SHELL_MOD_TYPE mod_type, const int32_t engine_version) { const SHELL_MOD *found = nullptr; for (int32_t i = 0; i < Shell_GetModCount(); i++) { const SHELL_MOD *const mod = Vector_Get(m_Mods, i); if (!mod->is_available || mod->mod_type != mod_type || (engine_version > 0 && mod->engine_version != engine_version)) { continue; } // match if (engine_version == 0) { if (found) { // more than one mod matches this engine version, abort return nullptr; } found = mod; } else { // exact version match return mod; } } return found; } bool Shell_CanSwitchToMod(const SHELL_MOD *const mod) { return mod != nullptr && mod->is_available && mod->is_valid && mod->mod_type != MOD_DIRECT_LEVEL; } bool Shell_IsCurrentMod(const char *const name) { if (name == nullptr) { return false; } const SHELL_ARGS *const args = Shell_GetArgs(); if (args == nullptr || args->mod == nullptr || args->mod->name == nullptr) { return false; } return strcmp(args->mod->name, name) == 0; } const char *Shell_GetCommonStringsPath(void) { return TRXPath_TryResolve( TRX_DYNAMIC_PATH_COMMON_CONFIG, "base_strings.json5"); } const char *Shell_GetBaseGameStringsPath(const SHELL_MOD *const mod) { const char *const base_mod = mod->base_mod != nullptr ? mod->base_mod : mod->name; return M_GetModStringsPath(base_mod); } const char *Shell_GetGameStringsPath(const SHELL_MOD *const mod) { return M_GetModStringsPath(mod->name); } const char *Shell_GetGameFlowPath(const SHELL_MOD *const mod) { return TRXPath_Resolve(TRX_DYNAMIC_PATH_GAMEFLOW_FILE, mod->name); } ================================================ FILE: src/trx/game/shell/mod.h ================================================ #pragma once #include typedef enum { MOD_BASE_GAME, MOD_EXPANSION_PACK, MOD_MISC, MOD_DIRECT_LEVEL, MOD_CUSTOM, } SHELL_MOD_TYPE; typedef struct { char *name; char *title; SHELL_MOD_TYPE mod_type; int32_t engine_version; char *base_mod; bool is_available; bool is_valid; } SHELL_MOD; void Shell_ScanAvailableMods(void); void Shell_ValidateMods(void); int32_t Shell_GetModCount(void); const SHELL_MOD *Shell_GetMod(int32_t index); const SHELL_MOD *Shell_GetModByName(const char *name); const SHELL_MOD *Shell_SelectStartupMod(int32_t engine_version); const SHELL_MOD *Shell_GetModByType( SHELL_MOD_TYPE mod_type, int32_t engine_version); bool Shell_CanSwitchToMod(const SHELL_MOD *mod); bool Shell_IsCurrentMod(const char *name); const char *Shell_GetCommonStringsPath(void); const char *Shell_GetBaseGameStringsPath(const SHELL_MOD *mod); const char *Shell_GetGameStringsPath(const SHELL_MOD *mod); const char *Shell_GetGameFlowPath(const SHELL_MOD *mod); ================================================ FILE: src/trx/game/shell/paths.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(_WIN32) #include #else #include #endif #define M_MAX_MOD_CHAIN 8 typedef struct { char *trx_dir; char *config_dir; char *cache_dir; char *games_dir; char *screenshots_dir; char *saves_dir; char *legacy_saves_dir; char *music_dir; const SHELL_ARGS *args; const char *mod_chain[M_MAX_MOD_CHAIN]; int32_t mod_chain_count; bool inited; } M_CONTEXT; typedef struct { const char *key; const char *value; } M_PATH_TOKEN; typedef struct { TRX_DYNAMIC_PATH id; const char *patterns[8]; const char **extensions; bool check_exists; bool is_dir; } M_DYNAMIC_PATH_POLICY; typedef struct { char *name; } M_DIR_ENTRY; typedef struct { char *dir; bool exists; VECTOR *entries; // M_DIR_ENTRY } M_DIR_CACHE_ENTRY; typedef struct { uint32_t generation; TRX_DYNAMIC_PATH path; char *rel; char *resolved; bool found; } M_RESOLVE_CACHE_ENTRY; typedef bool (*M_RESOLVE_ATTEMPT_CALLBACK)( const char *attempt_path, void *user_data); static const char *m_FMVExtensions[] = { ".mp4", ".mkv", ".mpeg", ".avi", ".webm", ".ogv", ".rpl", ".fmv", nullptr, }; static const M_DYNAMIC_PATH_POLICY m_PathPolicies[TRX_DYNAMIC_PATH_NUMBER_OF] = { [TRX_DYNAMIC_PATH_COMMON_CONFIG] = { .patterns = { "%mod_dir%/%rel%", "%base_mod_dir%/%rel%", "%config_dir%/%rel%", nullptr, }, .check_exists = true, }, [TRX_DYNAMIC_PATH_CATALOG] = { .patterns = { "%mod_dir%/%rel%", "%base_mod_dir%/%rel%", "%config_dir%/%rel%", nullptr, }, .check_exists = true, }, [TRX_DYNAMIC_PATH_GAMEFLOW_FILE] = { .patterns = { "%games_dir%/%rel%/gameflow.json5", nullptr, }, .check_exists = true, }, [TRX_DYNAMIC_PATH_LEVEL_FILE] = { .patterns = { "%mod_dir%/levels/%rel%", "%trx_dir%/data/%rel%", "%trx_dir%/%rel%", // TR3 legacy cutscenes "%trx_dir%/cuts/%rel%", "%mod_dir%/cuts/%rel%", nullptr, }, .check_exists = true, }, [TRX_DYNAMIC_PATH_SHARED_LEVEL_FILE] = { .patterns = { "%mod_dir%/levels/%rel%", "%base_mod_dir%/levels/%rel%", "%trx_dir%/data/%rel%", "%trx_dir%/%rel%", nullptr, }, .check_exists = true, }, [TRX_DYNAMIC_PATH_IMAGE_FILE] = { .patterns = { "%mod_dir%/images/%rel%", "%base_mod_dir%/images/%rel%", "%trx_dir%/data/images/%rel%", "%trx_dir%/%rel%", nullptr, }, .check_exists = true, }, [TRX_DYNAMIC_PATH_INJECTION_FILE] = { .patterns = { "%mod_dir%/injections/%rel%", "%base_mod_dir%/injections/%rel%", "%trx_dir%/data/injections/%rel%", "%trx_dir%/%rel%", nullptr, }, .check_exists = true, }, [TRX_DYNAMIC_PATH_SCRIPT_FILE] = { .patterns = { "%mod_dir%/scripts/%rel%", "%base_mod_dir%/scripts/%rel%", "%trx_dir%/data/scripts/%rel%", "%trx_dir%/%rel%", nullptr, }, .check_exists = true, }, [TRX_DYNAMIC_PATH_FMV_FILE] = { .patterns = { "%mod_dir%/fmv/%rel%", "%base_mod_dir%/fmv/%rel%", "%trx_dir%/fmv/%rel%", "%trx_dir%/%rel%", nullptr, }, .extensions = m_FMVExtensions, .check_exists = true, }, [TRX_DYNAMIC_PATH_SFX_FILE] = { .patterns = { "%mod_dir%/%rel%", "%base_mod_dir%/%rel%", "%trx_dir%/data/%rel%", "%trx_dir%/%rel%", nullptr, }, .check_exists = true, }, [TRX_DYNAMIC_PATH_CDAUDIO_FILE] = { .patterns = { "%mod_dir%/music/%rel%", "%mod_dir%/audio/%rel%", "%base_mod_dir%/music/%rel%", "%base_mod_dir%/audio/%rel%", "%trx_dir%/audio/%rel%", "%trx_dir%/%rel%", nullptr, }, .check_exists = true, }, [TRX_DYNAMIC_PATH_MUSIC_DIR] = { .patterns = { "%mod_dir%/music", "%base_mod_dir%/music", "%trx_dir%/music", "%trx_dir%/%rel%", nullptr, }, .check_exists = true, .is_dir = true, }, [TRX_DYNAMIC_PATH_SHADER_FILE] = { .patterns = { "%mod_dir%/shaders/%rel%", "%base_mod_dir%/shaders/%rel%", "%trx_dir%/shaders/%rel%", "%trx_dir%/cfg/shaders/%rel%", nullptr, }, .check_exists = true, }, [TRX_DYNAMIC_PATH_SCREENSHOT_WRITE_FILE] = { .patterns = { "%trx_dir%/screenshots/%mod%/%rel%", "%trx_dir%/screenshots/%rel%", nullptr, }, .check_exists = false, }, }; static M_CONTEXT m_Context = {}; static VECTOR *m_DirCache = nullptr; // M_DIR_CACHE_ENTRY static VECTOR *m_ResolveCache = nullptr; // M_RESOLVE_CACHE_ENTRY static uint32_t m_ResolveCacheGeneration = 0; static void M_ClearResolveCache(void) { if (m_ResolveCache == nullptr) { return; } for (int32_t i = 0; i < m_ResolveCache->count; i++) { M_RESOLVE_CACHE_ENTRY *const e = Vector_Get(m_ResolveCache, i); Memory_FreePointer(&e->rel); Memory_FreePointer(&e->resolved); } Vector_Free(m_ResolveCache); m_ResolveCache = nullptr; } // Returns a non-owning pointer that may reference static formatting storage. // Do not free it. static const char *M_JoinPathStatic(const char *const a, const char *const b) { ASSERT(b != nullptr); if (a == nullptr) { return b; } if (String_IsEmpty(b)) { return a; } const bool a_has_sep = String_EndsWith(a, "/") || String_EndsWith(a, "\\"); const bool b_has_sep = b[0] == '/' || b[0] == '\\'; const char *const b_join = a_has_sep && b_has_sep ? b + 1 : b; const char *const sep = (!a_has_sep && !b_has_sep) ? "/" : ""; return String_FormatStatic("%s%s%s", a, sep, b_join); } static char *M_JoinPathAlloc(const char *const a, const char *const b) { ASSERT(a != nullptr); ASSERT(b != nullptr); return Memory_DupStr(M_JoinPathStatic(a, b)); } static M_DIR_CACHE_ENTRY *M_FindDirCache(const char *const dir) { if (m_DirCache == nullptr || dir == nullptr) { return nullptr; } for (int32_t i = 0; i < m_DirCache->count; i++) { M_DIR_CACHE_ENTRY *const entry = Vector_Get(m_DirCache, i); if (strcmp(entry->dir, dir) == 0) { return entry; } } return nullptr; } static M_DIR_CACHE_ENTRY *M_LoadDirCache(const char *const dir) { if (dir == nullptr) { return nullptr; } if (m_DirCache == nullptr) { m_DirCache = Vector_Create(sizeof(M_DIR_CACHE_ENTRY)); } M_DIR_CACHE_ENTRY *existing = M_FindDirCache(dir); if (existing != nullptr) { return existing; } M_DIR_CACHE_ENTRY entry = { .dir = Memory_DupStr(dir), .exists = false, .entries = Vector_Create(sizeof(M_DIR_ENTRY)), }; void *const d = File_OpenDirectory(dir); if (d != nullptr) { entry.exists = true; const char *name = nullptr; while ((name = File_ReadDirectory(d)) != nullptr) { M_DIR_ENTRY de = { .name = Memory_DupStr(name) }; Vector_Add(entry.entries, &de); } File_CloseDirectory(d); } Vector_Add(m_DirCache, &entry); return Vector_Get(m_DirCache, m_DirCache->count - 1); } static const char *M_FindDirEntryCaseAware( const M_DIR_CACHE_ENTRY *const dir_cache, const char *const segment) { ASSERT(dir_cache != nullptr); ASSERT(segment != nullptr); for (int32_t i = 0; i < dir_cache->entries->count; i++) { const M_DIR_ENTRY *const e = Vector_Get(dir_cache->entries, i); if (strcmp(e->name, segment) == 0) { return e->name; } } for (int32_t i = 0; i < dir_cache->entries->count; i++) { const M_DIR_ENTRY *const e = Vector_Get(dir_cache->entries, i); if (String_Equivalent(e->name, segment)) { return e->name; } } return nullptr; } static char *M_ResolveCasePathCached(const char *const path) { if (path == nullptr || String_IsEmpty(path)) { return nullptr; } char *path_copy = Memory_DupStr(path); char *path_piece = path_copy; char *current_path = Memory_Alloc(strlen(path) + 2); if (path_copy[0] == '/') { strcpy(current_path, "/"); path_piece++; } else if (strstr(path_copy, ":\\") || strstr(path_copy, ":/")) { strcpy(current_path, path_copy); char *drive_sep = strstr(current_path, ":\\"); if (drive_sep == nullptr) { drive_sep = strstr(current_path, ":/"); } ASSERT(drive_sep != nullptr); drive_sep[2] = '\0'; path_piece += 3; } else { strcpy(current_path, "."); } while (path_piece != nullptr) { char *delim = strpbrk(path_piece, "/\\"); if (delim != nullptr) { *delim = '\0'; } if (path_piece[0] == '\0') { if (delim != nullptr) { path_piece = delim + 1; continue; } break; } M_DIR_CACHE_ENTRY *const dir_cache = M_LoadDirCache(current_path); if (dir_cache == nullptr || !dir_cache->exists) { Memory_FreePointer(&path_copy); Memory_FreePointer(¤t_path); return nullptr; } const char *const resolved_piece = M_FindDirEntryCaseAware(dir_cache, path_piece); if (resolved_piece == nullptr) { Memory_FreePointer(&path_copy); Memory_FreePointer(¤t_path); return nullptr; } char *next = M_JoinPathAlloc(current_path, resolved_piece); Memory_FreePointer(¤t_path); current_path = next; if (delim != nullptr) { path_piece = delim + 1; } else { break; } } Memory_FreePointer(&path_copy); return current_path; } static char *M_GetCurrentDirectory(void) { #if defined(_WIN32) char *const cwd = _getcwd(nullptr, 0); #else char *const cwd = getcwd(nullptr, 0); #endif if (cwd == nullptr) { return nullptr; } char *const result = Memory_DupStr(cwd); free(cwd); return result; } static char *M_GuessExtensionCached( const char *const path, const char **const extensions) { if (path == nullptr || extensions == nullptr) { return nullptr; } char *parent_dir = File_GetParentDirectory(path); if (parent_dir == nullptr) { return nullptr; } char *resolved_parent = M_ResolveCasePathCached(parent_dir); Memory_FreePointer(&parent_dir); if (resolved_parent == nullptr) { return nullptr; } Memory_FreePointer(&resolved_parent); char *resolved = M_ResolveCasePathCached(path); if (resolved != nullptr) { return resolved; } const char *const dot = strrchr(path, '.'); if (dot == nullptr) { return nullptr; } for (const char **ext = &extensions[0]; *ext != nullptr; ext++) { const size_t out_size = (size_t)(dot - path) + strlen(*ext) + 1; char *out = Memory_Alloc(out_size); strncpy(out, path, (size_t)(dot - path)); out[dot - path] = '\0'; strcat(out, *ext); resolved = M_ResolveCasePathCached(out); Memory_FreePointer(&out); if (resolved != nullptr) { return resolved; } } return nullptr; } static M_RESOLVE_CACHE_ENTRY *M_FindResolveCache( const TRX_DYNAMIC_PATH path, const char *const rel) { if (m_ResolveCache == nullptr || rel == nullptr) { return nullptr; } for (int32_t i = 0; i < m_ResolveCache->count; i++) { M_RESOLVE_CACHE_ENTRY *const e = Vector_Get(m_ResolveCache, i); if (e->generation == m_ResolveCacheGeneration && e->path == path && strcmp(e->rel, rel) == 0) { return e; } } return nullptr; } static void M_SetResolveCache( const TRX_DYNAMIC_PATH path, const char *const rel, const char *const resolved) { if (rel == nullptr) { return; } if (m_ResolveCache == nullptr) { m_ResolveCache = Vector_Create(sizeof(M_RESOLVE_CACHE_ENTRY)); } M_RESOLVE_CACHE_ENTRY *const existing = M_FindResolveCache(path, rel); if (existing != nullptr) { Memory_FreePointer(&existing->resolved); existing->resolved = resolved != nullptr ? Memory_DupStr(resolved) : nullptr; existing->found = resolved != nullptr; return; } M_RESOLVE_CACHE_ENTRY entry = { .generation = m_ResolveCacheGeneration, .path = path, .rel = Memory_DupStr(rel), .resolved = resolved != nullptr ? Memory_DupStr(resolved) : nullptr, .found = resolved != nullptr, }; Vector_Add(m_ResolveCache, &entry); } static const char *M_GetCurrentModID(void) { if (m_Context.mod_chain_count > 0) { return m_Context.mod_chain[0]; } if (m_Context.args != nullptr && m_Context.args->mod != nullptr) { return m_Context.args->mod->name; } return nullptr; } static const char *M_GetModDir(const char *const mod_id) { if (mod_id == nullptr || String_IsEmpty(mod_id) || m_Context.games_dir == nullptr) { return nullptr; } return M_JoinPathStatic(m_Context.games_dir, mod_id); } static const char *M_GetCurrentModDir(void) { return M_GetModDir(M_GetCurrentModID()); } static const char *M_GetBaseModID(void) { if (m_Context.mod_chain_count > 1) { return m_Context.mod_chain[1]; } if (m_Context.args != nullptr && m_Context.args->mod != nullptr) { return m_Context.args->mod->base_mod; } return nullptr; } static const char *M_GetDirectLevelArg(void) { return m_Context.args != nullptr && m_Context.args->level_to_play != nullptr ? m_Context.args->level_to_play : ""; } static const char *M_GetBaseModDir(void) { return M_GetModDir(M_GetBaseModID()); } static const char *M_GetLegacyDataDir(void) { return M_JoinPathStatic(m_Context.trx_dir, "data"); } static const char *M_GetBaseDirForDynamicPath(const TRX_DYNAMIC_PATH path) { switch (path) { case TRX_DYNAMIC_PATH_COMMON_CONFIG: case TRX_DYNAMIC_PATH_CATALOG: return m_Context.config_dir; case TRX_DYNAMIC_PATH_LEVEL_FILE: case TRX_DYNAMIC_PATH_SHARED_LEVEL_FILE: case TRX_DYNAMIC_PATH_SFX_FILE: return M_GetLegacyDataDir(); case TRX_DYNAMIC_PATH_IMAGE_FILE: return M_JoinPathStatic(M_GetLegacyDataDir(), "images"); case TRX_DYNAMIC_PATH_INJECTION_FILE: return M_JoinPathStatic(M_GetLegacyDataDir(), "injections"); case TRX_DYNAMIC_PATH_SCRIPT_FILE: return M_JoinPathStatic(M_GetLegacyDataDir(), "scripts"); case TRX_DYNAMIC_PATH_SHADER_FILE: return M_JoinPathStatic(m_Context.trx_dir, "shaders"); case TRX_DYNAMIC_PATH_FMV_FILE: return M_JoinPathStatic(m_Context.trx_dir, "fmv"); case TRX_DYNAMIC_PATH_CDAUDIO_FILE: return M_JoinPathStatic(M_GetLegacyDataDir(), "audio"); case TRX_DYNAMIC_PATH_MUSIC_DIR: return M_JoinPathStatic(M_GetLegacyDataDir(), "music"); default: return m_Context.trx_dir; } } static const char *M_StripOptionalPrefix( const char *const value, const char *const prefix) { if (value != nullptr && prefix != nullptr && String_CaseSubstring(value, prefix) == value) { return value + strlen(prefix); } return value; } static void M_TrimTrailingSeparators(char *const path) { if (path == nullptr) { return; } size_t len = strlen(path); while (len > 1 && (path[len - 1] == '/' || path[len - 1] == '\\')) { // Keep Windows drive roots like C:\ intact. if (len == 3 && path[1] == ':') { break; } path[len - 1] = '\0'; len--; } } static bool M_SetDirFromEnv( char **const target, const char *const env_value, const char *const default_suffix, const bool check_existence) { ASSERT(target != nullptr); ASSERT(default_suffix != nullptr); Memory_FreePointer(target); if (env_value != nullptr && !String_IsEmpty(env_value)) { *target = Memory_DupStr(env_value); } else { *target = String_Format("%s/%s", m_Context.trx_dir, default_suffix); } M_TrimTrailingSeparators(*target); if (check_existence && !File_DirExists(*target)) { Memory_FreePointer(target); return false; } return true; } static char *M_ReplacePathTokens( char *result, const M_PATH_TOKEN *const tokens, const size_t token_count) { ASSERT(result != nullptr); ASSERT(tokens != nullptr); for (size_t i = 0; i < token_count; i++) { const char *const tok = tokens[i].key; const char *const val = tokens[i].value; if (val == nullptr || strstr(result, tok) == nullptr) { continue; } const size_t tok_len = strlen(tok); const size_t val_len = strlen(val); const size_t src_len = strlen(result); int32_t tok_count = 0; const char *scan = result; while ((scan = strstr(scan, tok)) != nullptr) { tok_count++; scan += tok_len; } const int64_t delta = (int64_t)val_len - (int64_t)tok_len; const int64_t out_len_signed = (int64_t)src_len + delta * tok_count; ASSERT(out_len_signed >= 0); const size_t out_len = (size_t)out_len_signed; char *out = Memory_Alloc(out_len + 1); char *dst = out; scan = result; const char *hit = nullptr; while ((hit = strstr(scan, tok)) != nullptr) { const size_t prefix_len = (size_t)(hit - scan); memcpy(dst, scan, prefix_len); dst += prefix_len; memcpy(dst, val, val_len); dst += val_len; scan = hit + tok_len; } strcpy(dst, scan); Memory_FreePointer(&result); result = out; } return result; } char *TRXPath_ExpandVars(const char *const in) { if (in == nullptr) { return nullptr; } if (!m_Context.inited) { TRXPath_Init(m_Context.args); } char *result = Memory_DupStr(in); const char *const mod_id = M_GetCurrentModID(); const char *const base_mod_id = M_GetBaseModID(); const M_PATH_TOKEN tokens[] = { #define M_PATH_TOKEN_ITEM(name, field, token) { token, m_Context.field }, TRX_PATH_DIR_LIST(M_PATH_TOKEN_ITEM) #undef M_PATH_TOKEN_ITEM { "%mod%", mod_id != nullptr ? mod_id : "" }, { "%mod_dir%", M_GetCurrentModDir() }, { "%levels_dir%", M_GetBaseDirForDynamicPath(TRX_DYNAMIC_PATH_LEVEL_FILE) }, { "%images_dir%", M_GetBaseDirForDynamicPath(TRX_DYNAMIC_PATH_IMAGE_FILE) }, { "%injections_dir%", M_GetBaseDirForDynamicPath(TRX_DYNAMIC_PATH_INJECTION_FILE) }, { "%scripts_dir%", M_GetBaseDirForDynamicPath(TRX_DYNAMIC_PATH_SCRIPT_FILE) }, { "%base_mod%", base_mod_id != nullptr ? base_mod_id : "" }, { "%base_mod_dir%", M_GetBaseModDir() }, { "%direct_level%", M_GetDirectLevelArg() }, { "%tr_version%", String_FormatStatic("%d", g_TRVersion) }, }; return M_ReplacePathTokens(result, tokens, ARRAY_SIZE(tokens)); } static void M_BuildModChain(const SHELL_ARGS *const args) { m_Context.mod_chain_count = 0; if (args == nullptr || args->mod == nullptr) { return; } const SHELL_MOD *mod = args->mod; while (mod != nullptr && m_Context.mod_chain_count < M_MAX_MOD_CHAIN) { for (int32_t i = 0; i < m_Context.mod_chain_count; i++) { if (strcmp(m_Context.mod_chain[i], mod->name) == 0) { LOG_WARNING( "Mod chain cycle detected at '%s'; stopping traversal", mod->name); return; } } m_Context.mod_chain[m_Context.mod_chain_count++] = mod->name; if (mod->base_mod == nullptr) { break; } mod = Shell_GetModByName(mod->base_mod); if (mod == nullptr) { break; } } if (mod != nullptr && m_Context.mod_chain_count == M_MAX_MOD_CHAIN) { LOG_WARNING( "Mod chain truncated at %d entries; remaining base_mod links will " "be ignored", M_MAX_MOD_CHAIN); } } static void M_SeedResolverCaches(void) { const char *const dirs[] = { m_Context.trx_dir, m_Context.config_dir, m_Context.cache_dir, m_Context.games_dir, m_Context.screenshots_dir, m_Context.saves_dir, m_Context.legacy_saves_dir, M_GetCurrentModDir(), M_GetBaseModDir(), M_GetLegacyDataDir(), M_JoinPathStatic(M_GetLegacyDataDir(), "levels"), M_JoinPathStatic(M_GetLegacyDataDir(), "images"), M_JoinPathStatic(M_GetLegacyDataDir(), "injections"), M_JoinPathStatic(M_GetLegacyDataDir(), "scripts"), M_JoinPathStatic(m_Context.trx_dir, "cuts"), M_JoinPathStatic(m_Context.trx_dir, "fmv"), M_JoinPathStatic(m_Context.trx_dir, "audio"), M_JoinPathStatic(m_Context.trx_dir, "music"), M_JoinPathStatic(m_Context.trx_dir, "shaders"), M_JoinPathStatic(m_Context.trx_dir, "cfg"), nullptr, }; for (int32_t i = 0; dirs[i] != nullptr; i++) { M_LoadDirCache(dirs[i]); } } __attribute__((destructor)) static void M_Shutdown(void) { Memory_FreePointer(&m_Context.trx_dir); Memory_FreePointer(&m_Context.config_dir); Memory_FreePointer(&m_Context.cache_dir); Memory_FreePointer(&m_Context.games_dir); Memory_FreePointer(&m_Context.screenshots_dir); Memory_FreePointer(&m_Context.saves_dir); Memory_FreePointer(&m_Context.legacy_saves_dir); if (m_DirCache != nullptr) { for (int32_t i = 0; i < m_DirCache->count; i++) { M_DIR_CACHE_ENTRY *const e = Vector_Get(m_DirCache, i); for (int32_t j = 0; j < e->entries->count; j++) { M_DIR_ENTRY *const de = Vector_Get(e->entries, j); Memory_FreePointer(&de->name); } Vector_Free(e->entries); Memory_FreePointer(&e->dir); } Vector_Free(m_DirCache); m_DirCache = nullptr; } M_ClearResolveCache(); m_Context.args = nullptr; m_Context.mod_chain_count = 0; m_Context.inited = false; } void TRXPath_Init(const SHELL_ARGS *const args) { M_Shutdown(); m_Context.args = args; m_ResolveCacheGeneration++; if (m_Context.trx_dir == nullptr) { const char *const base = SDL_GetBasePath(); if (base != nullptr) { m_Context.trx_dir = Memory_DupStr(base); SDL_free((void *)base); } else { m_Context.trx_dir = Memory_DupStr("."); } M_TrimTrailingSeparators(m_Context.trx_dir); } M_SetDirFromEnv( &m_Context.config_dir, getenv("TRX_CONFIG_DIR"), "cfg", false); M_SetDirFromEnv( &m_Context.cache_dir, getenv("TRX_CACHE_DIR"), "cache", false); if (!M_SetDirFromEnv( &m_Context.games_dir, getenv("TRX_GAMES_DIR"), "games", true)) { M_SetDirFromEnv(&m_Context.games_dir, nullptr, "cfg", false); } M_SetDirFromEnv( &m_Context.screenshots_dir, getenv("TRX_SCREENSHOTS_DIR"), "screenshots", false); M_SetDirFromEnv( &m_Context.saves_dir, getenv("TRX_SAVES_DIR"), "saves", false); Memory_FreePointer(&m_Context.legacy_saves_dir); m_Context.legacy_saves_dir = String_Format("%s/saves", m_Context.trx_dir); M_BuildModChain(args); M_SeedResolverCaches(); m_Context.inited = true; } const char *TRXPath_Get(const TRX_PATH path) { if (!m_Context.inited) { TRXPath_Init(m_Context.args); } switch (path) { #define M_GET_CASE(name, field, token) \ case TRX_PATH_##name: \ return m_Context.field; TRX_PATH_DIR_LIST(M_GET_CASE) #undef M_GET_CASE default: ASSERT_FAIL_FMT("Unknown TRX_PATH %d", path); return nullptr; } } const char *TRXPath_Join(const TRX_PATH path, const char *const rel) { const char *const root = TRXPath_Get(path); if (root == nullptr || rel == nullptr || String_IsEmpty(rel)) { return root; } return M_JoinPathStatic(root, rel); } static const char *M_ExpandDynamicPattern( const TRX_DYNAMIC_PATH path, const char *const pattern, const char *const rel, const char *const mod_dir_override) { ASSERT(pattern != nullptr); const char *rel_value = rel; const char *const mod_id = M_GetCurrentModID(); const char *const base_mod_id = M_GetBaseModID(); const M_PATH_TOKEN tokens[] = { #define M_DYNAMIC_PATH_TOKEN_ITEM(name, field, token) \ { token, m_Context.field }, TRX_PATH_DIR_LIST(M_DYNAMIC_PATH_TOKEN_ITEM) #undef M_DYNAMIC_PATH_TOKEN_ITEM { "%rel%", rel_value }, { "%mod%", mod_id != nullptr ? mod_id : "" }, { "%base_mod%", base_mod_id != nullptr ? base_mod_id : "" }, { "%mod_dir%", mod_dir_override != nullptr ? mod_dir_override : M_GetCurrentModDir() }, { "%base_mod_dir%", M_GetBaseModDir() }, { "%base_dir%", M_GetBaseDirForDynamicPath(path) }, { "%tr_version%", String_FormatStatic("%d", g_TRVersion) }, }; char *expanded = Memory_DupStr(pattern); expanded = M_ReplacePathTokens(expanded, tokens, ARRAY_SIZE(tokens)); const char *const resolved = String_FormatStatic("%s", expanded); Memory_FreePointer(&expanded); return resolved; } static bool M_ForEachResolveAttempt( const TRX_DYNAMIC_PATH path, const char *const rel, const M_RESOLVE_ATTEMPT_CALLBACK callback, void *const user_data, const bool stop_after_first_pattern) { ASSERT(path >= 0 && path < TRX_DYNAMIC_PATH_NUMBER_OF); ASSERT(callback != nullptr); char *expanded_rel = TRXPath_ExpandVars(rel); const char *const effective_rel = expanded_rel != nullptr ? expanded_rel : rel; if (effective_rel != nullptr && File_IsAbsolute(effective_rel)) { const bool result = callback(effective_rel, user_data); Memory_FreePointer(&expanded_rel); return result; } const M_DYNAMIC_PATH_POLICY *const policy = &m_PathPolicies[path]; ASSERT(policy != nullptr); for (size_t i = 0; i < ARRAY_SIZE(policy->patterns); i++) { const char *const pattern = policy->patterns[i]; if (pattern == nullptr) { break; } char *candidate = Memory_DupStr( M_ExpandDynamicPattern(path, pattern, effective_rel, nullptr)); if (strchr(candidate, '%') != nullptr) { Memory_FreePointer(&candidate); continue; } if (!callback(candidate, user_data)) { Memory_FreePointer(&candidate); Memory_FreePointer(&expanded_rel); return false; } if (policy->extensions == nullptr || policy->is_dir) { Memory_FreePointer(&candidate); continue; } const char *const dot = strrchr(candidate, '.'); if (dot == nullptr) { Memory_FreePointer(&candidate); continue; } for (const char **ext = &policy->extensions[0]; *ext != nullptr; ext++) { const size_t out_size = (size_t)(dot - candidate) + strlen(*ext) + 1; char *out = Memory_Alloc(out_size); strncpy(out, candidate, (size_t)(dot - candidate)); out[dot - candidate] = '\0'; strcat(out, *ext); const bool keep_going = callback(out, user_data); Memory_FreePointer(&out); if (!keep_going) { Memory_FreePointer(&candidate); Memory_FreePointer(&expanded_rel); return false; } } if (stop_after_first_pattern) { Memory_FreePointer(&candidate); break; } Memory_FreePointer(&candidate); } Memory_FreePointer(&expanded_rel); return true; } typedef struct { const M_DYNAMIC_PATH_POLICY *policy; const char *resolved; } M_RESOLVE_VISITOR_CONTEXT; static bool M_ResolveAttemptVisitor( const char *const attempt_path, void *const user_data) { ASSERT(attempt_path != nullptr); ASSERT(user_data != nullptr); M_RESOLVE_VISITOR_CONTEXT *const ctx = user_data; ASSERT(ctx->policy != nullptr); if (ctx->policy->is_dir) { if (!ctx->policy->check_exists) { ctx->resolved = String_FormatStatic("%s", attempt_path); return false; } char *dir_path = M_ResolveCasePathCached(attempt_path); if (dir_path == nullptr) { return true; } M_DIR_CACHE_ENTRY *const dir_cache = M_LoadDirCache(dir_path); if (dir_cache != nullptr && dir_cache->exists) { ctx->resolved = String_FormatStatic("%s", dir_path); Memory_FreePointer(&dir_path); return false; } Memory_FreePointer(&dir_path); return true; } if (!ctx->policy->check_exists) { ctx->resolved = String_FormatStatic("%s", attempt_path); return false; } char *full_path = M_ResolveCasePathCached(attempt_path); if (full_path == nullptr) { return true; } ctx->resolved = String_FormatStatic("%s", full_path); Memory_FreePointer(&full_path); return false; } static char *M_AppendResolveAttempt( char *attempts, const char *const attempt_path) { if (attempt_path == nullptr || String_IsEmpty(attempt_path)) { return attempts; } if (attempts == nullptr) { return String_Format("%s", attempt_path); } char *const joined = String_Format("%s\n - %s", attempts, attempt_path); Memory_FreePointer(&attempts); return joined; } static bool M_CollectResolveAttemptVisitor( const char *const attempt_path, void *const user_data) { ASSERT(user_data != nullptr); char **const attempts = user_data; *attempts = M_AppendResolveAttempt(*attempts, attempt_path); return true; } static char *M_GetResolveAttempts( const TRX_DYNAMIC_PATH path, const char *const rel) { char *attempts = nullptr; M_ForEachResolveAttempt( path, rel, M_CollectResolveAttemptVisitor, &attempts, false); return attempts; } static const char *M_GetResolveError( const TRX_DYNAMIC_PATH path, const char *const rel) { char *attempts = M_GetResolveAttempts(path, rel); const char *const out = String_FormatStatic( "Failed to resolve path \"%s\". Searched paths:\n" " - %s)", rel != nullptr ? rel : "(null)", attempts != nullptr ? attempts : "(none)"); Memory_FreePointer(&attempts); return out; } static const char *M_PeekResolvedUserPathCandidate( const M_DYNAMIC_PATH_POLICY *const policy, const char *const candidate) { ASSERT(policy != nullptr); if (candidate == nullptr) { return nullptr; } if (!policy->check_exists) { return String_FormatStatic("%s", candidate); } if (policy->is_dir) { char *dir_path = M_ResolveCasePathCached(candidate); if (dir_path == nullptr) { return nullptr; } M_DIR_CACHE_ENTRY *const dir_cache = M_LoadDirCache(dir_path); if (dir_cache == nullptr || !dir_cache->exists) { Memory_FreePointer(&dir_path); return nullptr; } const char *const resolved = String_FormatStatic("%s", dir_path); Memory_FreePointer(&dir_path); return resolved; } char *full_path = M_ResolveCasePathCached(candidate); if (full_path == nullptr) { return nullptr; } const char *const resolved = String_FormatStatic("%s", full_path); Memory_FreePointer(&full_path); return resolved; } const char *TRXPath_PeekResolve( const TRX_DYNAMIC_PATH path, const char *const rel) { if (!m_Context.inited) { TRXPath_Init(m_Context.args); } ASSERT(path >= 0 && path < TRX_DYNAMIC_PATH_NUMBER_OF); const M_DYNAMIC_PATH_POLICY *const policy = &m_PathPolicies[path]; ASSERT(policy != nullptr); if (rel != nullptr && File_IsAbsolute(rel)) { return rel; } if (rel != nullptr) { const M_RESOLVE_CACHE_ENTRY *const cached = M_FindResolveCache(path, rel); if (cached != nullptr) { return cached->found ? cached->resolved : nullptr; } } M_RESOLVE_VISITOR_CONTEXT ctx = { .policy = policy, .resolved = nullptr, }; M_ForEachResolveAttempt( path, rel, M_ResolveAttemptVisitor, &ctx, !policy->check_exists); if (ctx.resolved != nullptr) { M_SetResolveCache(path, rel, ctx.resolved); if (rel != nullptr) { const M_RESOLVE_CACHE_ENTRY *const cached = M_FindResolveCache(path, rel); if (cached != nullptr && cached->found) { return cached->resolved; } } return ctx.resolved; } M_SetResolveCache(path, rel, nullptr); return nullptr; } const char *TRXPath_TryResolve( const TRX_DYNAMIC_PATH path, const char *const rel) { const char *const resolved = TRXPath_PeekResolve(path, rel); if (resolved == nullptr) { LOG_ERROR("%s", M_GetResolveError(path, rel)); } return resolved; } const char *TRXPath_Resolve(const TRX_DYNAMIC_PATH path, const char *const rel) { const char *const resolved = TRXPath_PeekResolve(path, rel); if (resolved == nullptr) { Shell_ExitSystem(M_GetResolveError(path, rel)); } return resolved; } MYFILE *TRXPath_OpenFile( const TRX_DYNAMIC_PATH path, const char *const rel, const FILE_OPEN_MODE mode) { const char *const resolved = TRXPath_TryResolve(path, rel); if (resolved == nullptr) { return nullptr; } return File_Open(resolved, mode); } bool TRXPath_LoadFile( const TRX_DYNAMIC_PATH path, const char *const rel, char **const out_data, size_t *const out_size) { const char *const resolved = TRXPath_TryResolve(path, rel); if (resolved == nullptr) { if (out_data != nullptr) { *out_data = nullptr; } if (out_size != nullptr) { *out_size = 0; } return false; } return File_Load(resolved, out_data, out_size); } bool TRXPath_Exists(const TRX_DYNAMIC_PATH path, const char *const rel) { const char *const resolved = TRXPath_PeekResolve(path, rel); if (resolved == nullptr) { return false; } return File_Exists(resolved); } char *TRXPath_GuessExtension(const char *const path, const char **extensions) { if (!m_Context.inited) { TRXPath_Init(m_Context.args); } return M_GuessExtensionCached(path, extensions); } const char *TRXPath_PeekResolveUserPath( const TRX_DYNAMIC_PATH path, const char *const input_path) { if (!m_Context.inited) { TRXPath_Init(m_Context.args); } ASSERT(path >= 0 && path < TRX_DYNAMIC_PATH_NUMBER_OF); const M_DYNAMIC_PATH_POLICY *const policy = &m_PathPolicies[path]; ASSERT(policy != nullptr); if (input_path == nullptr) { return nullptr; } if (File_IsAbsolute(input_path)) { return M_PeekResolvedUserPathCandidate(policy, input_path); } char *cwd = M_GetCurrentDirectory(); if (cwd != nullptr) { char *cwd_path = String_Format("%s/%s", cwd, input_path); Memory_FreePointer(&cwd); const char *const resolved = M_PeekResolvedUserPathCandidate(policy, cwd_path); Memory_FreePointer(&cwd_path); if (resolved != nullptr) { return resolved; } } return TRXPath_PeekResolve(path, input_path); } ================================================ FILE: src/trx/game/shell/paths.h ================================================ #pragma once #include #include // Shell path module. // This layer owns high-level path policy: token expansion (%trx_dir%), // mod/base fallback order, case-aware canonicalization, and extension // guessing/caching. // // Use these APIs for game asset/config resolution instead of ad-hoc file // probes. // clang-format off #define TRX_PATH_DIR_LIST(X) \ X(TRX_DIR, trx_dir, "%trx_dir%") \ X(CONFIG_DIR, config_dir, "%config_dir%") \ X(CACHE_DIR, cache_dir, "%cache_dir%") \ X(GAMES_DIR, games_dir, "%games_dir%") \ X(SCREENSHOTS_DIR, screenshots_dir, "%screenshots_dir%") \ X(SAVES_DIR, saves_dir, "%saves_dir%") \ X(LEGACY_SAVES_DIR, legacy_saves_dir, "%legacy_saves_dir%") \ // clang-format on typedef enum { #define M_DIR_ENUM(name, field, token) TRX_PATH_##name, TRX_PATH_DIR_LIST(M_DIR_ENUM) #undef M_DIR_ENUM } TRX_PATH; typedef enum { TRX_DYNAMIC_PATH_COMMON_CONFIG, TRX_DYNAMIC_PATH_CATALOG, TRX_DYNAMIC_PATH_GAMEFLOW_FILE, TRX_DYNAMIC_PATH_SHADER_FILE, TRX_DYNAMIC_PATH_FMV_FILE, TRX_DYNAMIC_PATH_LEVEL_FILE, TRX_DYNAMIC_PATH_SHARED_LEVEL_FILE, TRX_DYNAMIC_PATH_IMAGE_FILE, TRX_DYNAMIC_PATH_INJECTION_FILE, TRX_DYNAMIC_PATH_SCRIPT_FILE, TRX_DYNAMIC_PATH_SFX_FILE, TRX_DYNAMIC_PATH_CDAUDIO_FILE, TRX_DYNAMIC_PATH_MUSIC_DIR, TRX_DYNAMIC_PATH_SCREENSHOT_WRITE_FILE, TRX_DYNAMIC_PATH_NUMBER_OF, } TRX_DYNAMIC_PATH; // Initialize resolver state from shell args and environment variables. // Safe to call multiple times; later calls refresh context-derived values. void TRXPath_Init(const SHELL_ARGS *args); // Expand `%token%` variables in an arbitrary string. // Returns an owning string; caller must free. char *TRXPath_ExpandVars(const char *in); // Return configured root directory for a static TRX_PATH id. // Returned pointer is owned by resolver context; do not free. const char *TRXPath_Get(TRX_PATH path); // Join static root and relative path. // Returned pointer may use static formatting storage; copy if you need to keep // it. const char *TRXPath_Join(TRX_PATH path, const char *rel); // Resolve with policy fallback and existence checks. // Returns nullptr on miss; does not log. const char *TRXPath_PeekResolve(TRX_DYNAMIC_PATH path, const char *rel); // Same as TRXPath_PeekResolve, but logs an error on miss. const char *TRXPath_TryResolve(TRX_DYNAMIC_PATH path, const char *rel); // Same as TRXPath_PeekResolve, but terminates the game on miss. const char *TRXPath_Resolve(TRX_DYNAMIC_PATH path, const char *rel); // Resolve and open a file in one call. // Returns nullptr if resolution/open fails. MYFILE *TRXPath_OpenFile( TRX_DYNAMIC_PATH path, const char *rel, FILE_OPEN_MODE mode); // Resolve and load file contents into memory. // On failure, sets `out_data` to nullptr and `out_size` to 0 when provided. bool TRXPath_LoadFile( TRX_DYNAMIC_PATH path, const char *rel, char **out_data, size_t *out_size); // Resolve and check whether the file exists. bool TRXPath_Exists(TRX_DYNAMIC_PATH path, const char *rel); // Guess file extension (e.g. ".mp4" vs ".rpl") with case-aware resolver cache. // Returns an owning canonical path or nullptr if no candidate exists; caller // must free. char *TRXPath_GuessExtension(const char *path, const char **extensions); // Resolve a user-supplied path by trying: // 1. absolute path as-is // 2. current working directory for relative paths // 3. normal TRX path policy fallback for the given dynamic path // Returns nullptr on miss; does not log. const char *TRXPath_PeekResolveUserPath( TRX_DYNAMIC_PATH path, const char *input_path); ================================================ FILE: src/trx/game/shell/platform.c ================================================ #include #ifdef _WIN32 #include #include #include #include #endif #include #include #include void Shell_SetupHiDPI(void) { #ifdef _WIN32 SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitorv2"); SDL_SetHint(SDL_HINT_WINDOWS_DPI_SCALING, "0"); #endif } void Shell_SetupLibAV(void) { #ifdef _WIN32 // necessary for SDL_OpenAudioDevice to work with WASAPI // https://www.mail-archive.com/ffmpeg-trac@avcodec.org/msg43300.html CoInitializeEx(nullptr, COINIT_MULTITHREADED); #endif #if LIBAVCODEC_VERSION_MAJOR <= 57 av_register_all(); #endif av_log_set_level(AV_LOG_ERROR); } #ifdef _WIN32 // NOTE – taken from SDL3: // From 8994878767cfb9403f525d12c0770c1e149a4d08 Mon Sep 17 00:00:00 2001 // From: Sam Lantinga // Date: Tue, 7 Mar 2023 00:01:34 -0800 // Subject: [PATCH] Added SDL_GetSystemTheme() to return whether the system is // using a dark or light color theme, and SDL_EVENT_SYSTEM_THEME_CHANGED is // sent when this changes #ifndef DWMWA_USE_IMMERSIVE_DARK_MODE #define DWMWA_USE_IMMERSIVE_DARK_MODE 20 #endif // Previous window procedure pointer. LRESULT(CALLBACK *m_OldWndProc)(HWND, UINT, WPARAM, LPARAM) = nullptr; static bool M_GetWindowsDarkMode(void) { DWORD type = 0; DWORD value = 1; DWORD size = sizeof(value); const char *const key = "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; const LSTATUS status = RegGetValue( HKEY_CURRENT_USER, TEXT(key), TEXT("AppsUseLightTheme"), RRF_RT_REG_DWORD, &type, &value, &size); return (status == ERROR_SUCCESS && value == 0); } static void M_ApplyDarkMode(HWND hwnd) { void *dwm = SDL_LoadObject("dwmapi.dll"); if (dwm == nullptr) { return; } typedef HRESULT(WINAPI * DwmSetWindowAttribute_t)( HWND, DWORD, LPCVOID, DWORD); #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpedantic" DwmSetWindowAttribute_t fn = (DwmSetWindowAttribute_t)SDL_LoadFunction(dwm, "DwmSetWindowAttribute"); #pragma GCC diagnostic pop if (fn != nullptr) { BOOL dark = M_GetWindowsDarkMode() ? TRUE : FALSE; fn(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &dark, sizeof(dark)); } SDL_UnloadObject(dwm); } // Custom window procedure to listen for theme changes. static LRESULT CALLBACK M_DarkModeWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { if (msg == WM_SETTINGCHANGE && wParam == 0 && lParam != 0 && lstrcmpi((LPCTSTR)lParam, TEXT("ImmersiveColorSet")) == 0) { M_ApplyDarkMode(hwnd); } return CallWindowProc(m_OldWndProc, hwnd, msg, wParam, lParam); } void Shell_EnableThemeSupport(SDL_Window *const window) { SDL_SysWMinfo info; SDL_VERSION(&info.version); if (!SDL_GetWindowWMInfo(window, &info)) { return; } HWND hwnd = info.info.win.window; m_OldWndProc = (WNDPROC)SetWindowLongPtr( hwnd, GWLP_WNDPROC, (LONG_PTR)M_DarkModeWndProc); M_ApplyDarkMode(hwnd); } #else void Shell_EnableThemeSupport(SDL_Window *const window) { } #endif ================================================ FILE: src/trx/game/shell/platform.h ================================================ #pragma once // Isolated platform-sensitive initialization code #include void Shell_SetupHiDPI(void); void Shell_SetupLibAV(void); void Shell_EnableThemeSupport(SDL_Window *window); ================================================ FILE: src/trx/game/shell/session.c ================================================ #include #include SHELL_SESSION *ShellSession_Create(void) { return Memory_Alloc(sizeof(SHELL_SESSION)); } void ShellSession_Free(SHELL_SESSION *const session) { if (session != nullptr) { if (session->args != nullptr) { Shell_FreeArgs((SHELL_ARGS *)session->args); } Memory_Free(session); } } void ShellSession_UseArgs( SHELL_SESSION *const session, const SHELL_ARGS *const args) { if (session->args != nullptr) { Shell_FreeArgs((SHELL_ARGS *)session->args); } session->args = args; } ================================================ FILE: src/trx/game/shell/session.h ================================================ #pragma once #include typedef struct { const SHELL_ARGS *args; } SHELL_SESSION; SHELL_SESSION *ShellSession_Create(void); // Frees session and currently attached args (if any). void ShellSession_Free(SHELL_SESSION *session); // Replaces session args ownership. // The session takes ownership of `args` and frees previously attached args. void ShellSession_UseArgs(SHELL_SESSION *session, const SHELL_ARGS *args); ================================================ FILE: src/trx/game/shell/state.c ================================================ #include #include #include #include #include #include #include #include static char *m_LastPlayedMod = nullptr; #define M_LAST_PLAYED_MOD_KEY "last_played_mod" __attribute__((destructor)) static void M_Shutdown(void) { Memory_FreePointer(&m_LastPlayedMod); } static const char *M_GetStatePath(void) { return String_FormatStatic("%s/shell.json5", Shell_GetConfigDir()); } static void M_LoadState(void) { if (m_LastPlayedMod != nullptr) { return; } if (!File_Exists(M_GetStatePath())) { return; } JSON_VALUE *const root = JSONFile_Read(M_GetStatePath()); if (root == nullptr) { return; } JSON_OBJECT *const root_obj = JSON_ValueAsObject(root); const char *const mod_name = JSON_ObjectGetString(root_obj, M_LAST_PLAYED_MOD_KEY, nullptr); if (mod_name != nullptr) { m_LastPlayedMod = Memory_DupStr(mod_name); } JSON_ValueFree(root); } const char *ShellState_GetLastPlayedMod(void) { M_LoadState(); return m_LastPlayedMod; } void ShellState_RememberLastPlayedMod(const char *const mod_name) { if (mod_name == nullptr) { return; } M_LoadState(); if (m_LastPlayedMod != nullptr && strcmp(m_LastPlayedMod, mod_name) == 0) { return; } Memory_FreePointer(&m_LastPlayedMod); m_LastPlayedMod = Memory_DupStr(mod_name); JSON_OBJECT *const root_obj = JSON_ObjectNew(); JSON_ObjectAppendString(root_obj, M_LAST_PLAYED_MOD_KEY, m_LastPlayedMod); JSON_VALUE *const root = JSON_ValueFromObject(root_obj); const char *const state_path = M_GetStatePath(); File_EnsureParentDirectories(state_path); if (File_Exists(state_path)) { JSONFile_Write(state_path, root); } else { size_t out_len = 0; char *out_data = JSON_WritePretty(root, " ", "\n", &out_len); MYFILE *const fp = File_Open(state_path, FILE_OPEN_WRITE); if (fp != nullptr) { File_WriteData(fp, out_data, out_len - 1); // w/o \0 File_Close(fp); } Memory_FreePointer(&out_data); } JSON_ValueFree(root); } ================================================ FILE: src/trx/game/shell/state.h ================================================ #pragma once const char *ShellState_GetLastPlayedMod(void); void ShellState_RememberLastPlayedMod(const char *mod_name); ================================================ FILE: src/trx/game/shell.h ================================================ #pragma once #include #include #include #include #include #include #include ================================================ FILE: src/trx/game/sound/common.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef enum { SF_FLIP = 0x40, SF_UNFLIP = 0x80, } SOUND_SOURCE_FLAG; #define M_DECIBEL_LUT_SIZE 512 #define M_SOUND_CLOSE_RANGE (1 * WALL_L) #define M_MAX_ACTIVE_SOUNDS AUDIO_MAX_ACTIVE_SAMPLES #define M_SOUND_RANGE_MULT_CONSTANT 4 #define M_SOUND_MAX_VOLUME 0x8000 #define M_SOUND_MAX_PITCH_CHANGE 6000 #define M_SOUND_MAX_VOLUME_CHANGE (g_TRVersion >= 3 ? 0x1000 : 0x2000) typedef struct { SAMPLE_ID sample_id; const SAMPLE_INFO *sample; int32_t handle; int32_t volume; int32_t pitch; int32_t pan; XYZ_32 initial_pos; const XYZ_32 *pos_ptr; } M_ACTIVE_SOUND; typedef struct { int number; int size; UT_hash_handle hh; } M_SAMPLE_DATA_ENTRY; typedef struct M_SAMPLE_ENTRY { SAMPLE_ID sample_id; SAMPLE_INFO sample; UT_hash_handle hh; } M_SAMPLE_ENTRY; static M_ACTIVE_SOUND m_ActiveSounds[M_MAX_ACTIVE_SOUNDS] = {}; static bool m_Initialised = false; static float m_MasterVolume = 0.0f; static M_SAMPLE_DATA_ENTRY *m_SampleDataMap = nullptr; static M_SAMPLE_ENTRY *m_SampleMap = nullptr; static int32_t m_DecibelLUT[M_DECIBEL_LUT_SIZE] = {}; static int32_t m_SourceCount = 0; static OBJECT_VECTOR *m_Sources = nullptr; static int M_SampleDataEntry_Cmp( const M_SAMPLE_DATA_ENTRY *const a, const M_SAMPLE_DATA_ENTRY *const b) { return a->number - b->number; } static int32_t M_ConvertVolumeToDecibel(const int32_t volume) { int32_t idx = volume * g_Config.audio.master_volume * m_MasterVolume * M_DECIBEL_LUT_SIZE / M_SOUND_MAX_VOLUME; CLAMP(idx, 0, M_DECIBEL_LUT_SIZE - 1); return m_DecibelLUT[idx]; } static int32_t M_ConvertPanToDecibel(const uint16_t pan) { const int32_t result = sin((pan / 32767.0) * M_PI) * (M_DECIBEL_LUT_SIZE / 2); if (result > 0) { return -m_DecibelLUT[M_DECIBEL_LUT_SIZE - result]; } else if (result < 0) { return m_DecibelLUT[M_DECIBEL_LUT_SIZE + result]; } else { return 0; } } static float M_ConvertPitch(const int32_t pitch) { return pitch / 0x10000.p0; } static int32_t M_GetDistance( const SAMPLE_INFO *const sample, const XYZ_32 *const pos) { if (pos == nullptr) { return 0; } const XYZ_32 delta = { .x = pos->x - g_Camera.mic_pos.x, .y = pos->y - g_Camera.mic_pos.y, .z = pos->z - g_Camera.mic_pos.z, }; const int32_t distance = XYZ_32_GetLength(delta); if (distance > sample->range) { return INT32_MAX; } else if (distance < M_SOUND_CLOSE_RANGE) { return 0; } else { return distance - M_SOUND_CLOSE_RANGE; } } static int32_t M_GetVolume( const SAMPLE_INFO *const sample, const int32_t distance, const bool random) { int32_t volume = sample->volume; if (random && sample->flags.randomize_volume) { volume -= Random_GetDraw() * M_SOUND_MAX_VOLUME_CHANGE / 0x8000; } if (g_TRVersion == 1) { return volume - distance * 3.5f; } const int32_t attenuation = SQUARE(distance) / (SQUARE(sample->range) / 0x10000); return (volume * (0x10000 - attenuation)) / 0x10000; } static int32_t M_GetPitch(const SAMPLE_INFO *const sample, const uint32_t flags) { int32_t pitch = (flags & SPM_PITCH) != 0 ? (flags >> 8) & 0xFFFFFF : SOUND_DEFAULT_PITCH; pitch += sample->pitch * (1 << 9); if (!g_Config.audio.enable_pitched_sounds) { return pitch; } if (sample->flags.randomize_pitch) { pitch += ((Random_GetDraw() * M_SOUND_MAX_PITCH_CHANGE) / 0x4000) - M_SOUND_MAX_PITCH_CHANGE; } return pitch; } static int32_t M_GetPan( const SAMPLE_INFO *const sample, const XYZ_32 *const pos) { if (pos == nullptr) { return 0; } const int32_t distance = M_GetDistance(sample, pos); if (distance > 0 && !sample->flags.no_pan) { const int32_t dx = pos->x - g_Camera.mic_pos.x; const int32_t dz = pos->z - g_Camera.mic_pos.z; return (int16_t)Math_Atan(dz, dx) - g_Camera.actual_angle; } return 0; } static M_ACTIVE_SOUND *M_SelectUnusedSound(void) { // Try to get an unused slot for (int32_t i = 0; i < M_MAX_ACTIVE_SOUNDS; i++) { M_ACTIVE_SOUND *const sound = &m_ActiveSounds[i]; if (sound->sample == nullptr) { return sound; } } // No sound found - try to find the most quiet track, and use this one M_ACTIVE_SOUND *best_sound = nullptr; int32_t min_volume = INT32_MAX; for (int32_t i = 0; i < M_MAX_ACTIVE_SOUNDS; i++) { M_ACTIVE_SOUND *const sound = &m_ActiveSounds[i]; if (sound->sample != nullptr && sound->volume < min_volume) { min_volume = sound->volume; best_sound = sound; } } return best_sound; } static M_ACTIVE_SOUND *M_SelectUsedSound(const SAMPLE_ID sample_id) { for (int32_t i = 0; i < M_MAX_ACTIVE_SOUNDS; i++) { M_ACTIVE_SOUND *const result = &m_ActiveSounds[i]; if (result->sample_id == sample_id) { return result; } } return nullptr; } static M_ACTIVE_SOUND *M_SelectUsedSoundWithPos( const SAMPLE_ID sample_id, const XYZ_32 *const pos) { for (int32_t i = 0; i < M_MAX_ACTIVE_SOUNDS; i++) { M_ACTIVE_SOUND *const result = &m_ActiveSounds[i]; if (result->sample_id == sample_id && result->pos_ptr == pos) { return result; } } return nullptr; } static void M_ClearActiveSound(M_ACTIVE_SOUND *const sound) { sound->sample = nullptr; sound->sample_id = SFX_INVALID; sound->handle = AUDIO_NO_SOUND; } static void M_ClearAllActiveSounds(void) { for (int32_t i = 0; i < M_MAX_ACTIVE_SOUNDS; i++) { M_ACTIVE_SOUND *const sound = &m_ActiveSounds[i]; M_ClearActiveSound(sound); } } static void M_CloseActiveSound(M_ACTIVE_SOUND *const sound) { Audio_Sample_Close(sound->handle); M_ClearActiveSound(sound); } static void M_ClearActiveSoundHandles(const M_ACTIVE_SOUND *const sound) { for (int32_t i = 0; i < M_MAX_ACTIVE_SOUNDS; i++) { M_ACTIVE_SOUND *const rsound = &m_ActiveSounds[i]; if (rsound != sound && rsound->handle == sound->handle) { rsound->handle = AUDIO_NO_SOUND; } } } static void M_ClearSampleMaps(void) { M_SAMPLE_DATA_ENTRY *sentry, *stmp; HASH_ITER(hh, m_SampleDataMap, sentry, stmp) { HASH_DEL(m_SampleDataMap, sentry); Memory_Free(sentry); } M_SAMPLE_ENTRY *entry, *tmp; HASH_ITER(hh, m_SampleMap, entry, tmp) { HASH_DEL(m_SampleMap, entry); Memory_Free(entry); } } static void M_SyncActiveSoundHandle(M_ACTIVE_SOUND *const sound) { Audio_Sample_SetPan(sound->handle, M_ConvertPanToDecibel(sound->pan)); Audio_Sample_SetPitch(sound->handle, M_ConvertPitch(sound->pitch)); Audio_Sample_SetVolume( sound->handle, M_ConvertVolumeToDecibel(sound->volume)); } static void M_UpdateActiveSoundParams(M_ACTIVE_SOUND *const sound) { const int32_t distance = M_GetDistance(sound->sample, sound->pos_ptr); if (distance == INT32_MAX) { sound->volume = 0; return; } int32_t volume = M_GetVolume(sound->sample, distance, false); if (volume < 0) { sound->volume = 0; return; } sound->volume = volume; sound->pan = M_GetPan(sound->sample, sound->pos_ptr); } bool Sound_Init(void) { m_MasterVolume = g_Config.audio.sound_volume; m_DecibelLUT[0] = -10000; for (int32_t i = 1; i < M_DECIBEL_LUT_SIZE; i++) { if (g_TRVersion < 3) { // Legacy scale m_DecibelLUT[i] = (log2(1.0 / M_DECIBEL_LUT_SIZE) - log2(1.0 / i)) * 1000; } else { // Hundredths of a dB in the range [-10000..0]. // Later we apply a linear gain of `10^(centi_dB/2000)`. const double gain = (double)i / (double)M_DECIBEL_LUT_SIZE; int32_t centi_db = (int32_t)lrint(2000.0 * log10(gain)); CLAMP(centi_db, -10000, 0); m_DecibelLUT[i] = centi_db; } } if (!Audio_Init()) { LOG_ERROR("Failed to initialize libtrx sound system"); return false; } m_Initialised = true; M_ClearAllActiveSounds(); return true; } void Sound_Shutdown(void) { m_Initialised = false; Audio_Shutdown(); M_ClearSampleMaps(); } bool Sound_IsInitialised(void) { return m_Initialised; } void Sound_SetMasterVolume(const float volume) { m_MasterVolume = volume; } uint8_t Sound_GetReverbType(void) { return Audio_GetReverbType(); } void Sound_SetReverbType(uint8_t reverb_type) { Audio_SetReverbType(reverb_type); } void Sound_ResetSamples(void) { if (!Sound_IsInitialised()) { return; } Audio_Sample_CloseAll(); Audio_Sample_UnloadAll(); M_ClearAllActiveSounds(); M_ClearSampleMaps(); } bool Sound_LoadSampleData( const int32_t sample_data_id, const char *const sample_data, const size_t size) { if (!Sound_IsInitialised()) { return false; } return Audio_Sample_Load(sample_data_id, sample_data, size); } int32_t Sound_ReserveSampleData(int32_t index, const int32_t how_many) { M_SAMPLE_DATA_ENTRY *entry; if (index != -1) { HASH_FIND_INT(m_SampleDataMap, &index, entry); if (entry != nullptr) { return index; } } else { index = 0; // Ensure entries are ordered by starting slot HASH_SORT(m_SampleDataMap, M_SampleDataEntry_Cmp); // Find first gap large enough for how_many slots M_SAMPLE_DATA_ENTRY *e; M_SAMPLE_DATA_ENTRY *prev = nullptr; for (e = m_SampleDataMap; e != nullptr; prev = e, e = e->hh.next) { if (prev == nullptr && e->number >= how_many) { index = 0; break; } if (prev != nullptr && e->number - (prev->number + prev->size) >= how_many) { index = prev->number + prev->size; break; } } if (e == nullptr && prev != nullptr) { index = prev->number + prev->size; } } entry = Memory_Alloc(sizeof(*entry)); entry->number = index; entry->size = how_many; HASH_ADD_INT(m_SampleDataMap, number, entry); return index; } SAMPLE_INFO *Sound_GetSample(const SAMPLE_ID sample_id) { M_SAMPLE_ENTRY *entry = nullptr; if (sample_id == SFX_INVALID) { return nullptr; } HASH_FIND_INT(m_SampleMap, &sample_id, entry); return entry != nullptr ? &entry->sample : nullptr; } SAMPLE_INFO *Sound_GetOrCreateSample(const SAMPLE_ID sample_id) { SAMPLE_INFO *const sample = Sound_GetSample(sample_id); if (sample != nullptr) { return sample; } M_SAMPLE_ENTRY *const entry = Memory_Alloc(sizeof(*entry)); entry->sample_id = sample_id; HASH_ADD_INT(m_SampleMap, sample_id, entry); return &entry->sample; } bool Sound_IsAvailable_Direct(const SAMPLE_ID sample_id) { return Sound_GetSample(sample_id) != nullptr; } bool Sound_IsAvailable(const SAMPLE_TRX_ID sample_id) { return Sound_IsAvailable_Direct(Sound_ToGameID(sample_id)); } // Get the maximum direct SAMPLE_ID loaded for playback. // Returns SFX_INVALID if no samples are available. SAMPLE_ID Sound_GetMaxDirectSampleID(void) { M_SAMPLE_ENTRY *entry, *tmp; SAMPLE_ID max_id = SFX_INVALID; HASH_ITER(hh, m_SampleMap, entry, tmp) { if (entry->sample_id > max_id) { max_id = entry->sample_id; } } return max_id; } void Sound_InitialiseSources(const int32_t num_sources) { m_SourceCount = num_sources; m_Sources = num_sources == 0 ? nullptr : GameBuf_Alloc( num_sources * sizeof(OBJECT_VECTOR), GBUF_SOUND_SOURCES); } int32_t Sound_GetSourceCount(void) { return m_SourceCount; } OBJECT_VECTOR *Sound_GetSource(const int32_t source_idx) { if (m_Sources == nullptr) { return nullptr; } return &m_Sources[source_idx]; } void Sound_ResetSources(void) { const bool flip_status = Room_GetFlipStatus(); for (int32_t i = 0; i < m_SourceCount; i++) { OBJECT_VECTOR *const source = &m_Sources[i]; if ((flip_status && (source->flags & SF_FLIP)) || (!flip_status && (source->flags & SF_UNFLIP))) { Sound_Effect_Direct(source->data, &source->pos, SPM_NORMAL); } } } bool Sound_Effect_Direct( const SAMPLE_ID sample_id, const XYZ_32 *const pos, const uint32_t flags) { if (!Sound_IsInitialised()) { return false; } if ((flags & SPM_ALWAYS) == 0) { const bool play_underwater = (flags & SPM_UNDERWATER) != 0; const ROOM *const room = Room_Get(g_Camera.pos.room_num); const bool room_submerged = room != nullptr && room->flags.underwater; if (play_underwater != room_submerged) { return false; } } const SAMPLE_INFO *const sample = Sound_GetSample(sample_id); if (sample == nullptr || sample->number < 0) { return false; } if (sample->randomness) { int32_t r = Random_GetDraw(); if (g_TRVersion >= 3) { r &= 0xFF; } if (r > sample->randomness) { return false; } } const int32_t distance = M_GetDistance(sample, pos); if (distance == INT32_MAX) { return false; } const int32_t pan = M_GetPan(sample, pos); const int32_t volume = M_GetVolume(sample, distance, true); if (volume <= 0) { return false; } const int32_t pitch = M_GetPitch(sample, flags); const int32_t num_samples = sample->flags.num_samples; const int32_t track_id = num_samples == 1 ? sample->number : sample->number + ((num_samples * Random_GetDraw()) / 0x8000); M_ACTIVE_SOUND *sound = nullptr; switch (sample->mode) { case SAMPLE_MODE_NORMAL: sound = M_SelectUnusedSound(); break; case SAMPLE_MODE_WAIT: sound = g_TRVersion == 1 ? M_SelectUsedSoundWithPos(sample_id, pos) : M_SelectUsedSound(sample_id); if (sound != nullptr && Audio_Sample_IsPlaying(sound->handle)) { return true; } if (sound == nullptr) { sound = M_SelectUnusedSound(); } break; case SAMPLE_MODE_RESTART: sound = M_SelectUsedSound(sample_id); if (sound == nullptr) { sound = M_SelectUnusedSound(); } break; case SAMPLE_MODE_LOOPED: sound = M_SelectUsedSound(sample_id); if (sound != nullptr) { if (volume > sound->volume) { sound->volume = volume; sound->pan = pan; sound->pitch = pitch; } return true; } sound = M_SelectUnusedSound(); break; } if (sound == nullptr) { return false; } M_CloseActiveSound(sound); const int32_t handle = Audio_Sample_Play( track_id, M_ConvertVolumeToDecibel(volume), M_ConvertPitch(pitch), M_ConvertPanToDecibel(pan), sample->mode == SAMPLE_MODE_LOOPED); if (handle == AUDIO_NO_SOUND) { return false; } sound->sample = sample; sound->sample_id = sample_id; sound->handle = handle; sound->volume = volume; sound->pitch = pitch; sound->pan = pan; if (pos != nullptr) { sound->initial_pos = *pos; if (flags & SPM_STATIC_POS) { sound->pos_ptr = &sound->initial_pos; } else { sound->pos_ptr = pos; } } else { sound->pos_ptr = nullptr; } M_ClearActiveSoundHandles(sound); return true; } bool Sound_Effect( const SAMPLE_TRX_ID sample_id, const XYZ_32 *const pos, const uint32_t flags) { return Sound_Effect_Direct(Sound_ToGameID(sample_id), pos, flags); } void Sound_StopEffect_Direct(const SAMPLE_ID sample_id) { if (!Sound_IsInitialised()) { return; } for (int32_t i = 0; i < M_MAX_ACTIVE_SOUNDS; i++) { M_ACTIVE_SOUND *const sound = &m_ActiveSounds[i]; if (sound->sample_id == sample_id) { M_CloseActiveSound(sound); } } } void Sound_StopEffect(const SAMPLE_TRX_ID sample_id) { Sound_StopEffect_Direct(Sound_ToGameID(sample_id)); } void Sound_ResetAmbient(void) { if (!Sound_IsInitialised()) { return; } Sound_ResetSources(); } void Sound_UpdateEffects(void) { if (!Sound_IsInitialised()) { return; } for (int32_t i = 0; i < M_MAX_ACTIVE_SOUNDS; i++) { M_ACTIVE_SOUND *const sound = &m_ActiveSounds[i]; if (sound->sample == nullptr) { continue; } if (sound->sample->mode == SAMPLE_MODE_LOOPED) { if (sound->volume <= 0) { M_CloseActiveSound(sound); } else { M_SyncActiveSoundHandle(sound); sound->volume = 0; } } else if (!Audio_Sample_IsPlaying(sound->handle)) { M_ClearActiveSound(sound); } else if (g_TRVersion == 1 && sound->pos_ptr != nullptr) { M_UpdateActiveSoundParams(sound); if (sound->volume <= 0) { M_CloseActiveSound(sound); } else { M_SyncActiveSoundHandle(sound); } } } } void Sound_PauseAll(void) { Audio_Sample_PauseAll(); } void Sound_UnpauseAll(void) { Audio_Sample_UnpauseAll(); } void Sound_StopAll(void) { if (!Sound_IsInitialised()) { return; } Audio_Sample_CloseAll(); M_ClearAllActiveSounds(); } ================================================ FILE: src/trx/game/sound/common.h ================================================ #pragma once #include #include #include #include #include #include #define SOUND_DEFAULT_PITCH 0x10000 bool Sound_Init(void); void Sound_Shutdown(void); bool Sound_IsInitialised(void); void Sound_SetMasterVolume(float volume); uint8_t Sound_GetReverbType(void); void Sound_SetReverbType(uint8_t reverb_type); void Sound_ResetSamples(void); bool Sound_LoadSampleData( int32_t sample_data_id, const char *sample_data, size_t size); void Sound_InitialiseSources(int32_t num_sources); int32_t Sound_GetSourceCount(void); OBJECT_VECTOR *Sound_GetSource(int32_t source_idx); void Sound_ResetSources(void); // Reserve a contiguous block of sample data IDs for loading audio samples. // Returns the starting sample_data_id for the reserved block of size how_many. int32_t Sound_ReserveSampleData(int32_t index, int32_t how_many); // Look up an existing SAMPLE_INFO by SAMPLE_ID. Returns nullptr if not found. SAMPLE_INFO *Sound_GetSample(SAMPLE_ID sample_id); // Get or create a SAMPLE_INFO for the given SAMPLE_ID. // If no sample is found, a new sample slot is created. SAMPLE_INFO *Sound_GetOrCreateSample(SAMPLE_ID sample_id); // Returns true if a SAMPLE_INFO exists for the given SAMPLE_ID. bool Sound_IsAvailable_Direct(SAMPLE_ID sample_id); bool Sound_IsAvailable(SAMPLE_TRX_ID sample_id); // Get the maximum direct SAMPLE_ID loaded for playback. // Returns SFX_INVALID if no samples are available. SAMPLE_ID Sound_GetMaxDirectSampleID(void); // Play a sample with the given number. // pos is an optional argument that takes the world position to play the sound // at and can be nullptr. bool Sound_Effect_Direct(SAMPLE_ID sfx_num, const XYZ_32 *pos, uint32_t flags); bool Sound_Effect(SAMPLE_TRX_ID sfx_num, const XYZ_32 *pos, uint32_t flags); void Sound_StopEffect_Direct(SAMPLE_ID sfx_num); void Sound_StopEffect(SAMPLE_TRX_ID sfx_num); void Sound_ResetAmbient(void); void Sound_UpdateEffects(void); void Sound_PauseAll(void); void Sound_UnpauseAll(void); void Sound_StopAll(void); ================================================ FILE: src/trx/game/sound/enum.h ================================================ #pragma once typedef enum { SAMPLE_MODE_NORMAL = 0, SAMPLE_MODE_WAIT = 1, SAMPLE_MODE_RESTART = 2, SAMPLE_MODE_LOOPED = 3, } SAMPLE_MODE; // clang-format off typedef enum { SPM_NORMAL = 0, SPM_UNDERWATER = 1, SPM_ALWAYS = 2, SPM_PITCH = 4, SPM_STATIC_POS = 8, } SOUND_PLAY_MODE; // clang-format on ================================================ FILE: src/trx/game/sound/ids.c ================================================ #include #include SAMPLE_ID Sound_ToGameID(const SAMPLE_TRX_ID trx_id) { int32_t out; if (Catalog_EnumToGameID(CATALOG_SAMPLES, trx_id, &out)) { return out; } return SFX_INVALID; } SAMPLE_TRX_ID Sound_FromGameID(const SAMPLE_ID sample_id) { CATALOG_ID out; if (Catalog_GameIDToEnum(CATALOG_SAMPLES, sample_id, &out)) { return out; } return SFX_TRX_INVALID; } ================================================ FILE: src/trx/game/sound/ids.h ================================================ #pragma once typedef enum { SFX_INVALID = -1, } SAMPLE_ID; typedef enum { SFX_TRX_INVALID = -1, #define X_CATALOG_ID(enum_value) enum_value, #include #undef X_CATALOG_ID } SAMPLE_TRX_ID; SAMPLE_ID Sound_ToGameID(SAMPLE_TRX_ID sample_id); SAMPLE_TRX_ID Sound_FromGameID(SAMPLE_ID sample_id); ================================================ FILE: src/trx/game/sound/types.h ================================================ #pragma once #include #include typedef struct { int16_t number; int16_t volume; int32_t range; int32_t randomness; int8_t pitch; union { struct { uint16_t mode_bits : 2; uint16_t num_samples : 4; uint16_t reserved : 6; uint16_t no_pan : 1; uint16_t randomize_pitch : 1; uint16_t randomize_volume : 1; uint16_t : 1; }; uint16_t all; } flags; SAMPLE_MODE mode; } SAMPLE_INFO; ================================================ FILE: src/trx/game/sound.h ================================================ #pragma once #include #include #include ================================================ FILE: src/trx/game/sparks/enum.h ================================================ #pragma once enum { // clang-format off SPARK_F_NONE = 0x0, SPARK_F_SCALE = 0x2, SPARK_F_BLOOD = 0x4, SPARK_F_SPRITE = 0x8, SPARK_F_ROTATE = 0x10, SPARK_F_FX = 0x40, SPARK_F_ITEM = 0x80, SPARK_F_OUTSIDE = 0x100, SPARK_F_ALT_SPRITE = 0x200, SPARK_F_ATTACHED_POS = 0x400, SPARK_F_UNDERWATER = 0x800, SPARK_F_ATTACHED_NODE = 0x1000, SPARK_F_GREEN = 0x2000, // clang-format on }; ================================================ FILE: src/trx/game/sparks/manager.c ================================================ #include #include #include #include #include #include #include #include #include #define M_MAX_SPARKS 400 #define M_MAX_SPARK_DYNAMICS 32 typedef struct { bool on; uint8_t falloff; RGB_888 color; uint8_t flags; } M_SPARK_DYNAMIC; static SPARK m_Sparks[M_MAX_SPARKS]; static M_SPARK_DYNAMIC m_Dynamics[M_MAX_SPARK_DYNAMICS]; static int32_t m_NextSpark = 0; static XZ_32 m_SmokeWind = {}; static int32_t m_HairWindZ = 0; static int32_t m_TR3Wind = 0; static int32_t m_TR3WindAngle = DEG_180; static int32_t m_TR3DWindAngle = DEG_180; static const BITE m_NodeOffsets[16] = { { .pos = { 0, 340, 64 }, .mesh_num = 7 }, { .pos = { 0, 0, -96 }, .mesh_num = 10 }, { .pos = { 16, 48, 320 }, .mesh_num = 13 }, { .pos = { 0, -256, 0 }, .mesh_num = 5 }, { .pos = { 0, 64, 0 }, .mesh_num = 10 }, { .pos = { 0, 64, 0 }, .mesh_num = 13 }, { .pos = { -32, -16, -192 }, .mesh_num = 13 }, { .pos = { -64, 410, 0 }, .mesh_num = 20 }, { .pos = { 64, 410, 0 }, .mesh_num = 23 }, { .pos = { -160, -8, 16 }, .mesh_num = 5 }, { .pos = { -160, -8, 16 }, .mesh_num = 9 }, { .pos = { -160, -8, 16 }, .mesh_num = 13 }, { .pos = { 0, 0, 0 }, .mesh_num = 0 }, { .pos = { 0, 0, 0 }, .mesh_num = 0 }, { .pos = { 0, 0, 0 }, .mesh_num = 0 }, { .pos = { 0, 0, 0 }, .mesh_num = 0 }, }; XYZ_32 Sparks_GetWorldPos(const SPARK *const spark) { if (spark == nullptr) { return (XYZ_32) { 0, 0, 0 }; } if ((spark->flags & SPARK_F_FX) != 0U) { const EFFECT *const effect = Effect_Get(spark->effect_num); if ((spark->flags & SPARK_F_ATTACHED_POS) != 0U) { return effect->pos; } return (XYZ_32) { .x = effect->pos.x + spark->pos.x, .y = effect->pos.y + spark->pos.y, .z = effect->pos.z + spark->pos.z, }; } if ((spark->flags & SPARK_F_ITEM) != 0U) { const ITEM *const item = Item_Get(spark->item_num); if ((spark->flags & SPARK_F_ATTACHED_POS) != 0U) { return item->pos; } if ((spark->flags & SPARK_F_ATTACHED_NODE) != 0U) { XYZ_32 joint_pos = m_NodeOffsets[spark->node_num & 0xF].pos; Collide_GetJointAbsPosition( item, &joint_pos, m_NodeOffsets[spark->node_num & 0xF].mesh_num); return (XYZ_32) { .x = joint_pos.x + spark->pos.x, .y = joint_pos.y + spark->pos.y, .z = joint_pos.z + spark->pos.z, }; } return (XYZ_32) { .x = item->pos.x + spark->pos.x, .y = item->pos.y + spark->pos.y, .z = item->pos.z + spark->pos.z, }; } return spark->pos; } static int32_t M_GetFreeSpark(void) { int32_t idx = m_NextSpark; for (int32_t i = 0; i < M_MAX_SPARKS; i++) { if (!m_Sparks[idx].on) { m_NextSpark = (idx + 1) & 0xBF; return idx; } idx = idx == (M_MAX_SPARKS - 1) ? 0 : idx + 1; } int32_t free = 0; int32_t min_life = INT32_MAX; for (int32_t i = 0; i < M_MAX_SPARKS; i++) { const SPARK *const spark = &m_Sparks[i]; if ((int32_t)spark->life < min_life && spark->dynamic == -1 && ((spark->flags & SPARK_F_BLOOD) == 0U || (i & 1) != 0)) { free = i; min_life = (int32_t)spark->life; } } m_NextSpark = (free + 1) & 0xBF; return free; } SPARK *Sparks_GetFreeSpark(void) { const int32_t idx = M_GetFreeSpark(); return &m_Sparks[idx]; } SPARK *Sparks_GetSpark(const int32_t idx) { ASSERT(idx >= 0 && idx < M_MAX_SPARKS); return &m_Sparks[idx]; } void Sparks_Sync(SPARK *const spark) { if (spark == nullptr) { return; } const XYZ_32 world_pos = Sparks_GetWorldPos(spark); spark->prev_pos = spark->pos; spark->prev_world_pos = world_pos; spark->prev_color = spark->color; spark->prev_size = spark->size; spark->prev_rot_angle = spark->rot_angle; } void Sparks_FinishSetup(SPARK *const spark) { if (spark == nullptr) { return; } spark->color = spark->src_color; Sparks_Sync(spark); } int8_t Sparks_AllocDynamic(const uint8_t flags) { for (int32_t i = 0; i < M_MAX_SPARK_DYNAMICS; i++) { if (!m_Dynamics[i].on) { m_Dynamics[i].on = true; m_Dynamics[i].falloff = 4; m_Dynamics[i].flags = flags; m_Dynamics[i].color = COLOR_RGB_888_BLACK; return (int8_t)i; } } return -1; } void Sparks_FreeDynamic(const int8_t idx) { if (idx < 0 || idx >= M_MAX_SPARK_DYNAMICS) { return; } m_Dynamics[idx].on = false; } void Sparks_Reset(void) { for (int32_t i = 0; i < M_MAX_SPARKS; i++) { m_Sparks[i].on = false; m_Sparks[i].dynamic = -1; } for (int32_t i = 0; i < M_MAX_SPARK_DYNAMICS; i++) { m_Dynamics[i].on = false; } m_NextSpark = 0; m_SmokeWind = (XZ_32) {}; m_HairWindZ = 0; m_TR3Wind = 0; m_TR3WindAngle = DEG_180; m_TR3DWindAngle = DEG_180; } XZ_32 Sparks_GetSmokeWind(void) { return m_SmokeWind; } void Sparks_SetSmokeWind(const XZ_32 wind) { m_SmokeWind = wind; } int32_t Sparks_GetHairWindZ(void) { return m_HairWindZ; } static void M_UpdateWind(void) { if (!g_Config.visuals.enable_breeze) { m_SmokeWind = (XZ_32) {}; m_HairWindZ = 0; return; } if (g_TRVersion != 3) { const ITEM *const lara_item = Lara_GetItem(); if (lara_item == nullptr) { m_HairWindZ = 0; return; } const ROOM *const room = Room_Get(lara_item->room_num); if (room == nullptr || !room->flags.wind) { m_HairWindZ = 0; return; } const int32_t random = Random_GetDraw() & 7; if (random != 0) { m_HairWindZ += random - 4; if (m_HairWindZ < 0) { m_HairWindZ = 0; } else if (m_HairWindZ >= 8) { m_HairWindZ--; } } m_SmokeWind = (XZ_32) {}; return; } // TR3 wind logic: a small random wind magnitude with a slowly-changing // direction, biased to the [90°, 270°] range. m_TR3Wind += (Random_GetControl() & 7) - 3; if (m_TR3Wind <= -2) { m_TR3Wind++; } else if (m_TR3Wind >= 9) { m_TR3Wind--; } // Original TR3 uses a 0..4095 angle space; keep the calculations faithful. m_TR3DWindAngle = (m_TR3DWindAngle + (((Random_GetControl() & 0x3F) - 32) * 2)) & 0x1FFE; if (m_TR3DWindAngle < 1024) { // DEG_90 m_TR3DWindAngle += (1024 - m_TR3DWindAngle) << 1; } else if (m_TR3DWindAngle > 3072) { // DEG_270 m_TR3DWindAngle -= (m_TR3DWindAngle - 3072) << 1; } m_TR3DWindAngle &= 0x1FFE; m_TR3WindAngle = (m_TR3WindAngle + ((m_TR3DWindAngle - m_TR3WindAngle) >> 3)) & 0x1FFE; // Promote to DEG_360 for Math_Sin/Cos just at the end. m_SmokeWind = (XZ_32) { .x = (m_TR3Wind * Math_Sin(m_TR3WindAngle << 3)) >> W2V_SHIFT, .z = (m_TR3Wind * Math_Cos(m_TR3WindAngle << 3)) >> W2V_SHIFT, }; m_HairWindZ = 0; } void Sparks_Control(void) { M_UpdateWind(); for (int32_t i = 0; i < M_MAX_SPARKS; i++) { SPARK *const spark = &m_Sparks[i]; if (!spark->on) { continue; } if ((spark->flags & SPARK_F_ATTACHED_POS) == 0U || spark->life > 16) { if (spark->life > 0) { spark->life--; } } if (spark->life == 0) { if (spark->dynamic != -1) { m_Dynamics[(uint8_t)spark->dynamic].on = false; spark->dynamic = -1; } spark->on = false; continue; } const int32_t lived = (int32_t)spark->s_life - (int32_t)spark->life; spark->prev_pos = spark->pos; spark->prev_world_pos = Sparks_GetWorldPos(spark); spark->prev_color = spark->color; spark->prev_size = spark->size; spark->prev_rot_angle = spark->rot_angle; // Color fade: src -> dst, then fade-to-black. if (lived < (int32_t)spark->col_fade_speed && spark->col_fade_speed != 0U) { const float fade = lived / (float)spark->col_fade_speed; spark->color.r = LERP( (int32_t)spark->src_color.r, (int32_t)spark->dst_color.r, fade); spark->color.g = LERP( (int32_t)spark->src_color.g, (int32_t)spark->dst_color.g, fade); spark->color.b = LERP( (int32_t)spark->src_color.b, (int32_t)spark->dst_color.b, fade); } else if ( spark->life < spark->fade_to_black && spark->fade_to_black != 0U) { const float fade = spark->life / (float)spark->fade_to_black; spark->color.r = spark->dst_color.r * fade; spark->color.g = spark->dst_color.g * fade; spark->color.b = spark->dst_color.b * fade; } else { spark->color = spark->dst_color; } if (spark->life == spark->fade_to_black && (spark->flags & SPARK_F_UNDERWATER) != 0U) { spark->dst_size.width >>= 2; spark->dst_size.height >>= 2; } if ((spark->flags & SPARK_F_ROTATE) != 0U) { spark->rot_angle = (spark->rot_angle + spark->rot_add) & 0xFFF; } if ((spark->flags & SPARK_F_ALT_SPRITE) != 0U) { const OBJECT *const explosion = Object_Get(O_EXPLOSION_1); if (explosion->loaded) { const int32_t base = explosion->mesh_idx; if (spark->color.r < 16 && spark->color.g < 16 && spark->color.b < 16) { spark->sprite_idx = base + 3; } else if ( spark->color.r < 64 && spark->color.g < 64 && spark->color.b < 64) { spark->sprite_idx = base + 2; } else if ( spark->color.r < 96 && spark->color.g < 96 && spark->color.b < 96) { spark->sprite_idx = base + 1; } else { spark->sprite_idx = base; } } } if (lived == (int32_t)(spark->extras >> 3) && (spark->extras & 7U) != 0U) { int32_t uw = 0; if ((spark->flags & SPARK_F_UNDERWATER) != 0U) { uw = 1; } else if ((spark->flags & SPARK_F_GREEN) != 0U) { uw = 2; } const XYZ_32 spark_pos = Sparks_GetWorldPos(spark); for (int32_t j = 0; j < (int32_t)(spark->extras & 7U); j++) { Sparks_TriggerExplosionSparks( spark_pos, (int32_t)(spark->extras & 7U) - 1, spark->dynamic, uw, spark->room_num); spark->dynamic = -1; } if ((spark->flags & SPARK_F_UNDERWATER) != 0U) { Sparks_TriggerExplosionBubble(spark_pos, spark->room_num); } spark->extras = 0; } // Physics spark->vel.y += spark->gravity; if (spark->max_y_vel != 0) { const int32_t limit = (int32_t)spark->max_y_vel * (1 << 5); if ((spark->vel.y < 0 && spark->vel.y < limit) || (spark->vel.y > 0 && spark->vel.y > limit)) { spark->vel.y = limit; } } if ((spark->friction & 0x0FU) != 0U) { spark->vel.x -= spark->vel.x >> (spark->friction & 0x0FU); spark->vel.z -= spark->vel.z >> (spark->friction & 0x0FU); } if ((spark->friction & 0xF0U) != 0U) { spark->vel.y -= spark->vel.y >> (spark->friction >> 4); } spark->pos.x += spark->vel.x >> 5; spark->pos.y += spark->vel.y >> 5; spark->pos.z += spark->vel.z >> 5; if ((spark->flags & SPARK_F_OUTSIDE) != 0U) { spark->pos.x += m_SmokeWind.x >> 1; spark->pos.z += m_SmokeWind.z >> 1; } // Size lerp across lifetime. if (spark->s_life != 0U) { const float fade = lived / (float)spark->s_life; spark->size.width = LERP( (int32_t)spark->src_size.width, (int32_t)spark->dst_size.width, fade); spark->size.height = LERP( (int32_t)spark->src_size.height, (int32_t)spark->dst_size.height, fade); } else { spark->size = spark->src_size; } // If attached to a node, detach after a short random delay for some // node types. if ((spark->flags & (SPARK_F_ITEM | SPARK_F_ATTACHED_NODE)) == (SPARK_F_ITEM | SPARK_F_ATTACHED_NODE) && (spark->node_num == 2 || spark->node_num == 3)) { const int32_t b = spark->node_num == 3 ? (Random_GetDraw() & 3) + 12 : (Random_GetDraw() & 3) + 8; if (lived > b) { spark->pos = Sparks_GetWorldPos(spark); spark->flags &= ~(SPARK_F_ATTACHED_NODE | SPARK_F_ITEM); Sparks_Sync(spark); } } } // Dynamic light pass. for (int32_t i = 0; i < M_MAX_SPARKS; i++) { const SPARK *const spark = &m_Sparks[i]; if (!spark->on || spark->dynamic == -1) { continue; } M_SPARK_DYNAMIC *const dl = &m_Dynamics[(uint8_t)spark->dynamic]; if (!dl->on) { continue; } const XYZ_32 world_pos = Sparks_GetWorldPos(spark); const int32_t rnd = Random_GetControl(); XYZ_32 pos = { .x = world_pos.x + ((rnd & 0xF) << 4), .y = world_pos.y + (rnd & 0xF0), .z = world_pos.z + ((rnd >> 4) & 0xF0), }; int32_t falloff = (int32_t)spark->s_life - (int32_t)spark->life - 1; int32_t r = 0; int32_t g = 0; int32_t b = 0; if (falloff < 2) { if (dl->falloff < 28) { dl->falloff = (uint8_t)MIN((int32_t)dl->falloff + 6, 255); } r = 255 - (rnd & 0x1F) - (falloff << 3); g = 255 - (rnd & 0x1F) - (falloff << 4); b = 255 - (rnd & 0x1F) - (falloff << 6); } else if (falloff < 4) { if (dl->falloff < 28) { dl->falloff = (uint8_t)MIN((int32_t)dl->falloff + 6, 255); } r = 255 - (rnd & 0x1F) - (falloff << 3); g = 128 - (falloff << 3); b = (4 - falloff) << 2; if (b < 0) { b = 0; } else { b <<= 3; } } else { if (dl->falloff != 0U) { dl->falloff--; } r = (rnd & 0x1F) + 224; g = ((rnd >> 4) & 0x1F) + 128; b = (rnd >> 8) & 0x3F; } falloff = (int32_t)dl->falloff; if (falloff > 31) { falloff = 31; } if ((spark->flags & SPARK_F_GREEN) != 0U) { Output_AddDynamicLightRGB(pos, falloff, (RGB_888) { b, r, g }); } else { Output_AddDynamicLightRGB(pos, falloff, (RGB_888) { r, g, b }); } } } void Sparks_Draw(void) { for (int32_t i = 0; i < M_MAX_SPARKS; i++) { SPARK *const spark = &m_Sparks[i]; if (!spark->on) { continue; } OutputSource_PolyFX_StageSpark(spark); } } void Sparks_DetachEffect(const int16_t effect_num) { for (int32_t i = 0; i < M_MAX_SPARKS; i++) { SPARK *const spark = &m_Sparks[i]; if (!spark->on) { continue; } if ((spark->flags & SPARK_F_FX) != 0U && spark->effect_num == effect_num) { if ((spark->flags & SPARK_F_ATTACHED_POS) != 0U) { spark->on = false; continue; } const EFFECT *const effect = Effect_Get(effect_num); spark->pos.x += effect->pos.x; spark->pos.y += effect->pos.y; spark->pos.z += effect->pos.z; spark->flags &= ~SPARK_F_FX; } } } void Sparks_DetachItem(const int16_t item_num) { for (int32_t i = 0; i < M_MAX_SPARKS; i++) { SPARK *const spark = &m_Sparks[i]; if (!spark->on) { continue; } if ((spark->flags & SPARK_F_ITEM) != 0U && spark->item_num == item_num) { if ((spark->flags & SPARK_F_ATTACHED_POS) != 0U) { spark->on = false; continue; } const ITEM *const item = Item_Get(item_num); spark->pos.x += item->pos.x; spark->pos.y += item->pos.y; spark->pos.z += item->pos.z; spark->flags &= ~SPARK_F_ITEM; spark->flags &= ~SPARK_F_ATTACHED_NODE; } } } ================================================ FILE: src/trx/game/sparks/manager.h ================================================ #pragma once #include void Sparks_Reset(void); void Sparks_Control(void); void Sparks_Draw(void); void Sparks_DetachEffect(int16_t effect_num); void Sparks_DetachItem(int16_t item_num); XYZ_32 Sparks_GetWorldPos(const SPARK *spark); SPARK *Sparks_GetFreeSpark(void); SPARK *Sparks_GetSpark(int32_t idx); void Sparks_Sync(SPARK *spark); void Sparks_FinishSetup(SPARK *spark); int8_t Sparks_AllocDynamic(uint8_t flags); void Sparks_FreeDynamic(int8_t idx); XZ_32 Sparks_GetSmokeWind(void); void Sparks_SetSmokeWind(XZ_32 wind); int32_t Sparks_GetHairWindZ(void); ================================================ FILE: src/trx/game/sparks/spawners.c ================================================ #include #include #include #include #include #include #include #include static bool M_GetBloodSparkColors(RGB_888 *const src, RGB_888 *const dst) { switch (g_Config.visuals.blood_effects) { case BLOOD_EFFECTS_DISABLED: return false; case BLOOD_EFFECTS_PINK: *src = (RGB_888) { 112, 0, 224 }; *dst = (RGB_888) { 96, 0, 192 }; return true; case BLOOD_EFFECTS_RED: *src = (RGB_888) { 224, 0, 32 }; *dst = (RGB_888) { 192, 0, 24 }; return true; case BLOOD_EFFECTS_NUMBER_OF: break; } return false; } void Sparks_TriggerBubble( const int32_t x, const int32_t y, const int32_t z, const int32_t size, const int32_t size_range, const int16_t effect_num) { const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - x; const int32_t dz = lara_item->pos.z - z; const int32_t max_dist = 16 * WALL_L; if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) { return; } const OBJECT *const bubble_obj = Object_Get(O_BUBBLE_1); if (!bubble_obj->loaded) { return; } SPARK *const spark = Sparks_GetFreeSpark(); *spark = (SPARK) { .on = true, .src_color = { 0, 0, 0 }, .dst_color = { 144, 144, 144 }, .fade_to_black = 2, .draw_type = DRAW_BLEND_ADD, .col_fade_speed = 4, .life = 128, .s_life = 128, .flags = SPARK_F_ATTACHED_POS | SPARK_F_FX | SPARK_F_SPRITE | SPARK_F_SCALE, .effect_num = effect_num, .sprite_idx = bubble_obj->mesh_idx, .pos = { .x = 0, .y = 0, .z = 0 }, .vel = { .x = 0, .y = 0, .z = 0 }, .gravity = 0, .max_y_vel = 0, .friction = 0, .scalar = 0, .dynamic = -1, }; const int32_t safe_range = size_range > 0 ? size_range : 1; int32_t full_size = (Random_GetControl() % safe_range) + size; CLAMP(full_size, 0, 255); const uint8_t base = (uint8_t)full_size; const uint8_t dst = (uint8_t)(base << 3); spark->src_size.width = base; spark->src_size.height = base; spark->dst_size.width = dst; spark->dst_size.height = dst; spark->size = spark->src_size; Sparks_FinishSetup(spark); } void Sparks_TriggerWaterfallMist( const int32_t x, const int32_t y, const int32_t z, const int32_t angle) { const OBJECT *const explosion = Object_Get(O_EXPLOSION_1); if (explosion == nullptr || !explosion->loaded) { return; } static const int32_t offsets[] = { 576, 203, -203, -576 }; for (int32_t i = 0; i < (int32_t)ARRAY_SIZE(offsets); i++) { SPARK *const spark = Sparks_GetFreeSpark(); const int32_t offset = (Random_GetControl() & 0x1F) + offsets[i] - 16; const int32_t c = Math_Cos(angle) >> W2V_SHIFT; const int32_t s = Math_Sin(angle) >> W2V_SHIFT; *spark = (SPARK) { .on = true, .src_color = { 128, 128, 128 }, .dst_color = { 192, 192, 192 }, .col_fade_speed = 2, .fade_to_black = 4, .draw_type = DRAW_BLEND_ADD, .extras = 0, .life = (uint8_t)((Random_GetControl() & 3) + 6), .dynamic = -1, .sprite_idx = explosion->mesh_idx, .pos = { .x = x + (Random_GetControl() % 16) - 8 + c * offset, .y = y + (Random_GetControl() % 16) - 8, .z = z + (Random_GetControl() % 16) - 8 + s * offset, }, .vel = { .x = s, .y = 0, .z = c, }, .gravity = 0, .max_y_vel = 0, .friction = 3, .flags = SPARK_F_SPRITE | SPARK_F_ALT_SPRITE | SPARK_F_SCALE, .scalar = 6, }; spark->s_life = spark->life; if ((Random_GetControl() & 1) != 0) { spark->flags |= SPARK_F_ROTATE; spark->rot_angle = (uint16_t)(Random_GetControl() & 0xFFF); if ((Random_GetControl() & 1) != 0) { spark->rot_add = -16 - (Random_GetControl() % 16); } else { spark->rot_add = 16 + (Random_GetControl() % 16); } } const uint8_t dst_size = (uint8_t)((Random_GetControl() & 7) + 12); const uint8_t src_size = (uint8_t)(dst_size >> 1); spark->src_size.width = src_size; spark->src_size.height = src_size; spark->dst_size.width = dst_size; spark->dst_size.height = dst_size; spark->size = spark->src_size; Sparks_FinishSetup(spark); } } void Sparks_TriggerBreath( const XYZ_32 pos, const XYZ_32 vel, const int16_t room_num) { const OBJECT *const object = Object_Get(O_EXPLOSION_1); if (object == nullptr || !object->loaded) { return; } SPARK *const spark = Sparks_GetFreeSpark(); const int32_t jitter_x = (Random_GetControl() & 0xF) - 8; const int32_t jitter_y = (Random_GetControl() & 0xF) - 8; const int32_t jitter_z = (Random_GetControl() & 0xF) - 8; *spark = (SPARK) { .on = true, .src_color = { 0, 0, 0 }, .dst_color = { 32, 32, 32 }, .col_fade_speed = 4, .fade_to_black = 32, .draw_type = DRAW_BLEND_ADD, .extras = 0, .life = (uint8_t)((Random_GetControl() & 3) + 37), .dynamic = -1, .sprite_idx = object->mesh_idx, .pos = { .x = pos.x + jitter_x, .y = pos.y + jitter_y, .z = pos.z + jitter_z, }, .vel = vel, .gravity = 0, .max_y_vel = 0, .friction = 0, .flags = SPARK_F_SPRITE | SPARK_F_ALT_SPRITE | SPARK_F_SCALE, .scalar = 3, .room_num = room_num, }; spark->s_life = spark->life; const ROOM *const room = Room_Get(room_num); if (room != nullptr && room->flags.wind) { spark->flags |= SPARK_F_OUTSIDE; } const uint8_t dst_size = ((Random_GetControl() & 7) + 32); const uint8_t src_size = (dst_size >> 3); spark->src_size.width = src_size; spark->src_size.height = src_size; spark->dst_size.width = dst_size; spark->dst_size.height = dst_size; spark->size = spark->src_size; Sparks_FinishSetup(spark); } void Sparks_TriggerFireFlame( const XYZ_32 pos, const int32_t body_part, const int32_t type) { const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - pos.x; const int32_t dz = lara_item->pos.z - pos.z; const int32_t max_dist = 20 * WALL_L; if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; if (type == 2) { spark->src_color.r = (Random_GetControl() & 0x1F) + 48; spark->src_color.g = spark->src_color.r; spark->src_color.b = (Random_GetControl() & 0x3F) - 64; } else if (type == 254) { spark->src_color.r = 48; spark->src_color.g = 255; spark->src_color.b = (Random_GetControl() & 0x1F) + 48; spark->dst_color.r = 32; spark->dst_color.g = (Random_GetControl() & 0x3F) - 64; spark->dst_color.b = (Random_GetControl() & 0x3F) + 128; } else { spark->src_color.r = 255; spark->src_color.g = (Random_GetControl() & 0x1F) + 48; spark->src_color.b = 48; } if (type != 254) { spark->dst_color.r = (Random_GetControl() & 0x3F) - 64; spark->dst_color.b = 32; spark->dst_color.g = (Random_GetControl() & 0x3F) + 128; } if (body_part == -1) { if (type == 2 || type == 255 || type == 254) { spark->fade_to_black = 6; spark->col_fade_speed = (Random_GetControl() & 3) + 5; spark->life = (type < 254 ? 0 : 8) + (Random_GetControl() & 3) + 16; spark->s_life = spark->life; } else { spark->fade_to_black = 8; spark->col_fade_speed = (Random_GetControl() & 3) + 20; spark->life = (Random_GetControl() & 7) + 40; spark->s_life = spark->life; } } else { spark->fade_to_black = 16; spark->col_fade_speed = (Random_GetControl() & 3) + 8; spark->life = (Random_GetControl() & 3) + 28; spark->s_life = spark->life; } spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->dynamic = -1; if (body_part != -1) { spark->pos.x = (Random_GetControl() & 0x1F) - 16; spark->pos.y = 0; spark->pos.z = (Random_GetControl() & 0x1F) - 16; } else { spark->pos = pos; if (type == 0 || type == 1) { spark->pos.x += (Random_GetControl() & 0x1F) - 16; spark->pos.z += (Random_GetControl() & 0x1F) - 16; } else if (type >= 254) { spark->pos.x += (Random_GetControl() & 0x3F) - 32; spark->pos.z += (Random_GetControl() & 0x3F) - 32; } else { spark->pos.x += (Random_GetControl() & 0xF) - 8; spark->pos.z += (Random_GetControl() & 0xF) - 8; } } if (type == 2) { spark->vel.x = (Random_GetControl() & 0x1F) - 16; spark->vel.y = -1024 - (Random_GetControl() & 0x1FF); spark->vel.z = (Random_GetControl() & 0x1F) - 16; spark->friction = 68; } else { spark->vel.x = (Random_GetControl() & 0xFF) - 128; spark->vel.y = -16 - (Random_GetControl() & 0xF); spark->vel.z = (Random_GetControl() & 0xFF) - 128; if (type == 1) { spark->friction = 51; } else { spark->friction = 5; } } if (Random_GetControl() & 1) { if (body_part == -1) { spark->gravity = -16 - (Random_GetControl() & 0x1F); spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->max_y_vel = -16 - (Random_GetControl() & 7); } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->item_num = body_part; spark->gravity = -32 - (Random_GetControl() & 0x3F); spark->max_y_vel = -24 - (Random_GetControl() & 7); } spark->rot_angle = Random_GetControl() & 0xFFF; if (Random_GetControl() & 1) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = (Random_GetControl() & 0xF) + 16; } } else if (body_part == -1) { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->gravity = -16 - (Random_GetControl() & 0x1F); spark->max_y_vel = -16 - (Random_GetControl() & 7); } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_SPRITE | SPARK_F_SCALE; spark->item_num = body_part; spark->gravity = -32 - (Random_GetControl() & 0x3F); spark->max_y_vel = -24 - (Random_GetControl() & 7); } spark->scalar = 2; spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; uint8_t size; if (type == 0) { size = (Random_GetControl() & 0x1F) + 128; } else if (type == 1) { size = (Random_GetControl() & 0x1F) + 64; } else if (type < 254) { spark->max_y_vel = 0; spark->gravity = 0; size = (Random_GetControl() & 0x1F) + 32; } else { size = (Random_GetControl() & 0xF) + 48; } spark->src_size.width = size; spark->src_size.height = size; spark->size.width = size; spark->size.height = size; if (type == 2) { spark->dst_size.width = size >> 2; spark->dst_size.height = size >> 2; } else { spark->dst_size.width = size >> 4; spark->dst_size.height = size >> 4; } Sparks_FinishSetup(spark); } void Sparks_TriggerFireSmoke( const XYZ_32 pos, const int32_t body_part, const int32_t type) { const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - pos.x; const int32_t dz = lara_item->pos.z - pos.z; const int32_t max_dist = 20 * WALL_L; if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 0; spark->src_color.g = 0; spark->src_color.b = 0; spark->dst_color.r = 32; spark->dst_color.g = 32; spark->dst_color.b = 32; if (body_part == -1) { if (type == 255) { spark->fade_to_black = 8; spark->col_fade_speed = (Random_GetControl() & 3) + 16; spark->life = (Random_GetControl() & 7) + 28; spark->s_life = spark->life; } else { spark->fade_to_black = 16; spark->col_fade_speed = (Random_GetControl() & 7) + 32; spark->life = (Random_GetControl() & 0xF) + 57; spark->s_life = spark->life; } } else { spark->fade_to_black = 12; spark->col_fade_speed = (Random_GetControl() & 3) + 4; spark->life = (Random_GetControl() & 3) + 20; spark->s_life = spark->life; } spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 0xF) - 8; spark->pos.y = pos.y - (Random_GetControl() & 0x7F) - 256; spark->pos.z = pos.z + (Random_GetControl() & 0xF) - 8; spark->vel.x = (Random_GetControl() & 0xFF) - 128; spark->vel.y = -16 - (Random_GetControl() & 0xF); spark->vel.z = (Random_GetControl() & 0xFF) - 128; spark->friction = 4; if (Random_GetControl() & 1) { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = Random_GetControl() & 0xFFF; if (Random_GetControl() & 1) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = (Random_GetControl() & 0xF) + 16; } } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->scalar = 3; spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->gravity = -16 - (Random_GetControl() & 0xF); spark->max_y_vel = -8 - (Random_GetControl() & 7); spark->dst_size.width = (Random_GetControl() & 0x3F) + 64; spark->src_size.width = spark->dst_size.width >> 2; spark->size.width = spark->src_size.width; spark->src_size.height = spark->src_size.width; spark->size.height = spark->src_size.width; spark->dst_size.height = spark->dst_size.width; Sparks_FinishSetup(spark); } void Sparks_TriggerStaticFlame(const XYZ_32 pos, const int32_t size) { const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - pos.x; const int32_t dz = lara_item->pos.z - pos.z; const int32_t max_dist = 20 * WALL_L; if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = (Random_GetControl() & 0x3F) - 64; spark->src_color.g = (Random_GetControl() & 0x3F) + 128; spark->src_color.b = 64; spark->dst_color.r = spark->src_color.r; spark->dst_color.g = spark->src_color.g; spark->dst_color.b = 64; spark->col_fade_speed = 1; spark->fade_to_black = 0; spark->life = 2; spark->s_life = 2; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 7) - 4; spark->pos.y = pos.y; spark->pos.z = pos.z + (Random_GetControl() & 7) - 4; spark->max_y_vel = 0; spark->gravity = 0; spark->friction = 0; spark->vel.z = 0; spark->vel.y = 0; spark->vel.x = 0; spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->scalar = 2; spark->dst_size.width = size; spark->dst_size.height = size; spark->src_size.height = size; spark->src_size.width = size; spark->size.height = size; spark->size.width = size; Sparks_FinishSetup(spark); } void Sparks_TriggerSideFlame( const XYZ_32 pos, const int32_t angle, const int32_t speed, const bool pilot) { const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - pos.x; const int32_t dz = lara_item->pos.z - pos.z; const int32_t max_dist = 20 * WALL_L; if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = (Random_GetControl() & 0x1F) + 48; spark->src_color.g = spark->src_color.r; spark->src_color.b = (Random_GetControl() & 0x3F) - 64; spark->dst_color.r = (Random_GetControl() & 0x3F) - 64; spark->dst_color.g = (Random_GetControl() & 0x3F) + 0x80; spark->dst_color.b = 32; spark->fade_to_black = 8; spark->col_fade_speed = (Random_GetControl() & 3) + 12; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->life = (Random_GetControl() & 7) + 28; spark->s_life = spark->life; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16; spark->pos.y = pos.y + (Random_GetControl() & 0x1F) - 16; spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16; int32_t dist; if (pilot) { dist = (speed << 7) + (Random_GetControl() & 0x1F); } else { dist = (speed << 8) + (Random_GetControl() & 0x1FF); } dist <<= 1; const int32_t s = (dist * Math_Sin(angle)) >> W2V_SHIFT; const int32_t c = (dist * Math_Cos(angle)) >> W2V_SHIFT; spark->vel.x = (int16_t)((Random_GetControl() & 0x7F) + s - 64); spark->vel.y = -6 - (Random_GetControl() & 7); spark->vel.z = (int16_t)((Random_GetControl() & 0x7F) + c - 64); spark->friction = 4; spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->gravity = -8 - (Random_GetControl() & 0xF); spark->max_y_vel = -8 - (Random_GetControl() & 7); spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->scalar = 3; int32_t size = (Random_GetControl() & 0x1F) + 128; if (pilot) { size >>= 2; } spark->dst_size.width = size; spark->dst_size.height = size; spark->src_size.width = size >> 1; spark->src_size.height = size >> 1; spark->size.width = size >> 1; spark->size.height = size >> 1; Sparks_FinishSetup(spark); } void Sparks_TriggerBlood( const XYZ_32 pos, int32_t angle_12, const int32_t count) { RGB_888 src_color; RGB_888 dst_color; if (!M_GetBloodSparkColors(&src_color, &dst_color)) { return; } for (int32_t i = 0; i < count; i++) { SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color = src_color; spark->dst_color = dst_color; spark->col_fade_speed = 8; spark->fade_to_black = 8; spark->life = 24; spark->s_life = 24; spark->draw_type = DRAW_BLEND; spark->extras = 0; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16; spark->pos.y = pos.y + (Random_GetControl() & 0x1F) - 16; spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16; const int16_t dist = Random_GetControl() & 0xF; const int32_t ang = ((Random_GetControl() & 0x1F) + angle_12 - 16) & 0xFFF; spark->vel.x = -(dist * Math_Sin(ang << 4)) >> 7; spark->vel.y = -128 - (Random_GetControl() & 0xFF); spark->vel.z = dist * Math_Cos(ang << 4) >> 7; spark->friction = 4; spark->flags = SPARK_F_BLOOD | SPARK_F_SCALE; spark->scalar = 3; spark->max_y_vel = 0; spark->gravity = (Random_GetControl() & 0x1F) + 31; spark->size.width = 2; spark->src_size.width = 2; spark->size.height = 2; spark->src_size.height = 2; spark->dst_size.width = 2 - (Random_GetControl() & 1); spark->dst_size.height = 2 - (Random_GetControl() & 1); Sparks_FinishSetup(spark); } } void Sparks_TriggerBloodD( const XYZ_32 pos, int32_t angle_12, const int32_t count) { RGB_888 src_color; RGB_888 dst_color; if (!M_GetBloodSparkColors(&src_color, &dst_color)) { return; } for (int32_t i = 0; i < count; i++) { SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color = src_color; spark->dst_color = dst_color; spark->col_fade_speed = 8; spark->fade_to_black = 8; spark->life = 24; spark->s_life = 24; spark->draw_type = DRAW_BLEND; spark->extras = 0; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetDraw() & 0x1F) - 16; spark->pos.y = pos.y + (Random_GetDraw() & 0x1F) - 16; spark->pos.z = pos.z + (Random_GetDraw() & 0x1F) - 16; const int16_t dist = Random_GetDraw() & 0xF; const int32_t ang = ((Random_GetDraw() & 0x1F) + angle_12 - 16) & 0xFFF; spark->vel.x = -(dist * Math_Sin(ang << 4)) >> 7; spark->vel.y = -128 - (Random_GetDraw() & 0xFF); spark->vel.z = dist * Math_Cos(ang << 4) >> 7; spark->friction = 4; spark->flags = SPARK_F_SCALE; spark->scalar = 3; spark->max_y_vel = 0; spark->gravity = (Random_GetDraw() & 0x1F) + 31; spark->size.width = 2; spark->src_size.width = 2; spark->size.height = 2; spark->src_size.height = 2; spark->dst_size.width = 2 - (Random_GetDraw() & 1); spark->dst_size.height = 2 - (Random_GetDraw() & 1); Sparks_FinishSetup(spark); } } void Sparks_TriggerUnderwaterExplosion(const ITEM *item) { if (item == nullptr) { return; } Sparks_TriggerExplosionBubble(item->pos, item->room_num); Sparks_TriggerExplosionSparks(item->pos, 2, -2, 1, item->room_num); for (int32_t i = 0; i < 3; i++) { Sparks_TriggerExplosionSparks(item->pos, 2, -1, 1, item->room_num); } const int32_t water_height = Room_GetWaterHeight(item->pos, item->room_num); if (water_height == NO_HEIGHT) { return; } int32_t y = item->pos.y - water_height; if (y >= 2048) { return; } const int32_t wh = 2048 - y; y = wh >> 6; const ROOM *const room = Room_Get(item->room_num); FX_Water_SetupSplash(&(FX_WATER_SPLASH_SETUP) { .x = item->pos.x, .y = room->max_ceiling, .z = item->pos.z, .inner_y_size = -96, .inner_xz_vel = 160, .inner_gravity = 96, .inner_xz_off = y + 16, .inner_xz_size = y + 12, .inner_friction = 7, .inner_y_vel = (-512 - wh) << 3, .middle_xz_off = y + 24, .middle_xz_size = y + 24, .middle_y_size = -64, .middle_xz_vel = 224, .middle_y_vel = (-768 - wh) << 2, .middle_gravity = 56, .middle_friction = 8, .outer_xz_off = y + 32, .outer_xz_size = y + 32, .outer_xz_vel = 272, .outer_friction = 9, }); } void Sparks_TriggerExplosionSparks( XYZ_32 pos, int32_t extras, int32_t dynamic, int32_t uw, int16_t room_num) { const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - pos.x; const int32_t dz = lara_item->pos.z - pos.z; const int32_t max_dist = 30 * WALL_L; if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) { return; } const OBJECT *const explosion = Object_Get(O_EXPLOSION_1); if (explosion == nullptr || !explosion->loaded) { return; } int32_t safe_extras = extras; CLAMP(safe_extras, 0, 3); static const uint8_t extras_table[4] = { 0, 4, 7, 10 }; SPARK *const spark = Sparks_GetFreeSpark(); *spark = (SPARK) { .on = true, .src_color = { 255, 0, 0 }, .dst_color = { 0, 0, 0 }, .draw_type = DRAW_BLEND_ADD, .extras = (uint8_t)( safe_extras | ((extras_table[safe_extras] + (Random_GetControl() & 7) - 4) << 3)), .life = 0, .dynamic = (int8_t)dynamic, .sprite_idx = explosion->mesh_idx, .pos = pos, .vel = { .x = (Random_GetControl() & 0xFFF) - 2048, .y = (Random_GetControl() & 0xFFF) - 2048, .z = (Random_GetControl() & 0xFFF) - 2048, }, .gravity = 0, .max_y_vel = 0, .friction = 0, .flags = SPARK_F_SPRITE | SPARK_F_SCALE, .scalar = 3, .room_num = (uint8_t)room_num, }; if (uw == 1) { spark->src_color.g = (uint8_t)((Random_GetControl() & 0x3F) + 128); spark->src_color.b = 32; spark->dst_color.r = 192; spark->dst_color.g = (uint8_t)((Random_GetControl() & 0x1F) + 64); spark->dst_color.b = 0; spark->col_fade_speed = 7; spark->fade_to_black = 8; spark->life = (uint8_t)((Random_GetControl() & 7) + 16); spark->flags |= SPARK_F_UNDERWATER; } else { spark->src_color.g = (uint8_t)((Random_GetControl() & 0xF) + 32); spark->src_color.b = 0; spark->dst_color.r = (uint8_t)((Random_GetControl() & 0x3F) + 192); spark->dst_color.g = (uint8_t)((Random_GetControl() & 0x3F) + 128); spark->dst_color.b = 32; spark->col_fade_speed = 8; spark->fade_to_black = 16; spark->life = (uint8_t)((Random_GetControl() & 7) + 24); } spark->s_life = spark->life; if (dynamic == -2) { spark->dynamic = Sparks_AllocDynamic(uw == 1 ? 2 : 1); } if (dynamic != -2 || uw == 1) { spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16; spark->pos.y = pos.y + (Random_GetControl() & 0x1F) - 16; spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16; } else { spark->pos.x = pos.x + (Random_GetControl() & 0x1FF) - 256; spark->pos.y = pos.y + (Random_GetControl() & 0x1FF) - 256; spark->pos.z = pos.z + (Random_GetControl() & 0x1FF) - 256; } spark->friction = (uint8_t)(uw == 1 ? 0x11 : 0x33); spark->flags |= SPARK_F_ALT_SPRITE; if ((Random_GetControl() & 1) != 0) { spark->flags |= SPARK_F_ROTATE; spark->rot_angle = (uint16_t)(Random_GetControl() & 0xFFF); const int32_t rot_add = (Random_GetControl() & 0x7F) + 32; spark->rot_add = (int8_t)MIN(rot_add, 127); } spark->src_size.width = (uint8_t)((Random_GetControl() & 0xF) + 40); spark->src_size.height = (uint8_t)(spark->src_size.width + (Random_GetControl() & 7) + 8); spark->dst_size.width = (uint8_t)(spark->src_size.width << 1); spark->dst_size.height = (uint8_t)(spark->src_size.height << 1); spark->size = spark->src_size; if (uw == 2) { const RGB_888 src = spark->src_color; const RGB_888 dst = spark->dst_color; spark->src_color = (RGB_888) { src.b, src.r, src.g }; spark->dst_color = (RGB_888) { dst.b, dst.r, dst.g }; spark->color = spark->src_color; spark->flags |= SPARK_F_GREEN; } else if (extras != 0) { Sparks_TriggerExplosionSmoke(pos, uw, room_num); } else { Sparks_TriggerExplosionSmokeEnd(pos, uw, room_num); } Sparks_FinishSetup(spark); } void Sparks_TriggerExplosionBubble(const XYZ_32 pos, const int16_t room_num) { const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - pos.x; const int32_t dz = lara_item->pos.z - pos.z; const int32_t max_dist = 30 * WALL_L; if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) { return; } const OBJECT *const explosion = Object_Get(O_EXPLOSION_1); if (explosion == nullptr || !explosion->loaded) { return; } SPARK *const spark = Sparks_GetFreeSpark(); *spark = (SPARK) { .on = true, .src_color = { 128, 64, 0 }, .dst_color = { 128, 128, 128 }, .col_fade_speed = 8, .fade_to_black = 12, .life = 24, .s_life = 24, .draw_type = DRAW_BLEND_ADD, .extras = 0, .dynamic = -1, .sprite_idx = explosion->mesh_idx, .pos = pos, .vel = { .x = 0, .y = 0, .z = 0 }, .gravity = 0, .max_y_vel = 0, .friction = 0, .flags = SPARK_F_UNDERWATER | SPARK_F_SPRITE | SPARK_F_SCALE, .scalar = 3, .room_num = (uint8_t)room_num, }; const uint8_t size = (uint8_t)((Random_GetControl() & 7) + 63); spark->src_size.width = (uint8_t)(size >> 1); spark->src_size.height = spark->src_size.width; spark->dst_size.width = (uint8_t)(size << 1); spark->dst_size.height = spark->dst_size.width; spark->size = spark->src_size; Sparks_FinishSetup(spark); for (int32_t i = 0; i < 7; i++) { const XYZ_32 bubble_pos = { .x = pos.x + (Random_GetControl() & 0x1FF) - 256, .y = pos.y + (Random_GetControl() & 0x7F) - 64, .z = pos.z + (Random_GetControl() & 0x1FF) - 256, }; Spawn_BubbleEx(&bubble_pos, room_num, 6, 15); } } void Sparks_TriggerExplosionSmoke( const XYZ_32 pos, const bool uw, const int16_t room_num) { const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - pos.x; const int32_t dz = lara_item->pos.z - pos.z; const int32_t max_dist = 30 * WALL_L; if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 144; spark->src_color.g = 144; spark->src_color.b = 144; spark->dst_color.r = 64; spark->dst_color.g = 64; spark->dst_color.b = 64; spark->col_fade_speed = 2; spark->fade_to_black = 8; spark->draw_type = DRAW_BLEND_SUB; spark->extras = 0; spark->life = (uint8_t)((Random_GetControl() & 3) + 10); spark->s_life = spark->life; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 0x1FF) - 256; spark->pos.y = pos.y + (Random_GetControl() & 0x1FF) - 256; spark->pos.z = pos.z + (Random_GetControl() & 0x1FF) - 256; spark->vel.x = ((Random_GetControl() & 0xFFF) - 2048) >> 2; spark->vel.y = (Random_GetControl() & 0xFF) - 128; spark->vel.z = ((Random_GetControl() & 0xFFF) - 2048) >> 2; if (uw) { spark->friction = 2; } else { spark->friction = 6; } spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = Random_GetControl() & 0xFFF; spark->rot_add = (Random_GetControl() & 0xF) + 16; spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->scalar = 1; spark->gravity = -3 - (Random_GetControl() & 3); spark->max_y_vel = -4 - (Random_GetControl() & 3); spark->dst_size.width = (Random_GetControl() & 0x1F) + 128; spark->size.width = spark->dst_size.width >> 2; spark->src_size.width = spark->size.width; spark->dst_size.height = spark->dst_size.width + (Random_GetControl() & 0x1F) + 32; spark->size.height = spark->dst_size.height >> 3; spark->src_size.height = spark->size.height; Sparks_FinishSetup(spark); } void Sparks_TriggerExplosionSmokeEnd( const XYZ_32 pos, const bool uw, const int16_t room_num) { const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - pos.x; const int32_t dz = lara_item->pos.z - pos.z; const int32_t max_dist = 30 * WALL_L; if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; if (uw) { spark->src_color.r = 0; spark->src_color.g = 0; spark->src_color.b = 0; spark->dst_color.r = 192; spark->dst_color.g = 192; spark->dst_color.b = 208; } else { spark->src_color.r = 144; spark->src_color.g = 144; spark->src_color.b = 144; spark->dst_color.r = 64; spark->dst_color.g = 64; spark->dst_color.b = 64; } spark->col_fade_speed = 8; spark->fade_to_black = 64; spark->life = (uint8_t)((Random_GetControl() & 0x1F) + 96); spark->s_life = spark->life; spark->draw_type = uw ? DRAW_BLEND_ADD : DRAW_BLEND_SUB; spark->extras = 0; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16; spark->pos.y = pos.y + (Random_GetControl() & 0x1F) - 16; spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16; spark->vel.x = ((Random_GetControl() & 0xFFF) - 2048) >> 2; spark->vel.y = (Random_GetControl() & 0xFF) - 128; spark->vel.z = ((Random_GetControl() & 0xFFF) - 2048) >> 2; if (uw) { spark->friction = 20; spark->vel.y = (int16_t)(spark->vel.y >> 4); spark->pos.y += 32; } else { spark->friction = 6; } spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = (uint16_t)(Random_GetControl() & 0xFFF); if ((Random_GetControl() & 1) != 0) { spark->rot_add = (int8_t)(-16 - (Random_GetControl() & 0xF)); } else { spark->rot_add = (int8_t)((Random_GetControl() & 0xF) + 16); } spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->scalar = 3; if (uw) { spark->max_y_vel = 0; spark->gravity = 0; } else { spark->gravity = (int16_t)(-3 - (Random_GetControl() & 3)); spark->max_y_vel = (int8_t)(-4 - (Random_GetControl() & 3)); } spark->dst_size.width = (uint8_t)((Random_GetControl() & 0x1F) + 128); spark->size.width = (uint8_t)(spark->dst_size.width >> 2); spark->src_size.width = spark->size.width; spark->dst_size.height = (uint8_t)(spark->dst_size.width + (Random_GetControl() & 0x1F) + 32); spark->size.height = (uint8_t)(spark->dst_size.height >> 3); spark->src_size.height = spark->size.height; spark->room_num = (uint8_t)room_num; Sparks_FinishSetup(spark); } void Sparks_TriggerDartSmoke(const XYZ_32 pos, const XZ_32 vel, const bool hit) { const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - pos.x; const int32_t dz = lara_item->pos.z - pos.z; const int32_t max_dist = 16 * WALL_L; if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 16; spark->src_color.g = 8; spark->src_color.b = 4; spark->dst_color.r = 64; spark->dst_color.g = 48; spark->dst_color.b = 32; spark->col_fade_speed = 8; spark->fade_to_black = 4; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->life = (Random_GetControl() & 3) + 32; spark->s_life = spark->life; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16; spark->pos.y = pos.y + (Random_GetControl() & 0x1F) - 16; spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16; if (hit) { spark->vel.x = (Random_GetControl() & 0xFF) - vel.x - 128; spark->vel.y = -4 - (Random_GetControl() & 3); spark->vel.z = (Random_GetControl() & 0xFF) - vel.z - 128; } else { if (vel.x != 0) { spark->vel.x = -vel.x; } else { spark->vel.x = (Random_GetControl() & 0xFF) - 128; } spark->vel.y = -4 - (Random_GetControl() & 3); if (vel.z != 0) { spark->vel.z = -vel.z; } else { spark->vel.z = (Random_GetControl() & 0xFF) - 128; } } spark->friction = 3; if ((Random_GetControl() & 1) != 0) { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = Random_GetControl() & 0xFFF; if ((Random_GetControl() & 1) != 0) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = (Random_GetControl() & 0xF) + 16; } } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->scalar = 1; int32_t rnd = (Random_GetControl() & 0x3F) + 72; if (hit) { rnd >>= 1; spark->dst_size.width = (uint8_t)rnd; spark->size.width = spark->dst_size.width >> 2; spark->src_size.width = spark->size.width; spark->dst_size.height = (uint8_t)rnd; spark->size.height = spark->dst_size.height >> 2; spark->src_size.height = spark->size.height; spark->max_y_vel = 0; spark->gravity = 0; } else { spark->dst_size.width = (uint8_t)rnd; spark->size.width = spark->dst_size.width >> 4; spark->src_size.width = spark->size.width; spark->dst_size.height = (uint8_t)rnd; spark->size.height = spark->dst_size.height >> 4; spark->src_size.height = spark->size.height; spark->gravity = -4 - (Random_GetControl() & 3); spark->max_y_vel = -4 - (Random_GetControl() & 3); } Sparks_FinishSetup(spark); } void Sparks_TriggerPickupAid(const XYZ_32 pos, const XZ_32 vel) { const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - pos.x; const int32_t dz = lara_item->pos.z - pos.z; const int32_t max_dist = 16 * WALL_L; if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 48; spark->src_color.g = 40; spark->src_color.b = 36; spark->dst_color.r = (Random_GetDraw() & 0x20) + 96; spark->dst_color.g = spark->dst_color.r; spark->dst_color.b = 96; spark->col_fade_speed = 8; spark->fade_to_black = 2; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->life = (Random_GetControl() & 3) + 7; spark->s_life = spark->life; spark->dynamic = -1; spark->pos = pos; spark->pos.y -= Random_GetControl() & 16; if (vel.x != 0) { spark->vel.x = -vel.x; } else { spark->vel.x = (Random_GetControl() & 0x20) - 16; } spark->vel.y = -4 - (Random_GetControl() & 3); if (vel.z != 0) { spark->vel.z = -vel.z; } else { spark->vel.z = (Random_GetControl() & 0x20) - 16; } spark->friction = 3; if ((Random_GetControl() & 1) != 0) { spark->flags = SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = Random_GetControl() & 0xFFF; if ((Random_GetControl() & 1) != 0) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = (Random_GetControl() & 0xF) + 16; } } else { spark->flags = SPARK_F_SPRITE | SPARK_F_SCALE; } const OBJECT *const obj = Object_Get(O_PICKUP_AID); const int32_t mesh_count = ABS(obj->mesh_count) - 1; spark->sprite_idx = obj->mesh_idx + (Random_GetControl() & mesh_count); spark->scalar = 1; int32_t rnd = (Random_GetControl() & 0x3F) + 36; spark->dst_size.width = (uint8_t)rnd; spark->size.width = spark->dst_size.width >> 4; spark->src_size.width = spark->size.width; spark->dst_size.height = (uint8_t)rnd; spark->size.height = spark->dst_size.height >> 4; spark->src_size.height = spark->size.height; spark->gravity = -4 - (Random_GetControl() & 3); spark->max_y_vel = -4 - (Random_GetControl() & 3); Sparks_FinishSetup(spark); } void Sparks_TriggerFlareSparks( const XYZ_32 pos, const XYZ_32 vel, const bool smoke) { const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - pos.x; const int32_t dz = lara_item->pos.z - pos.z; const int32_t max_dist = 16 * WALL_L; if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 255; spark->src_color.g = 255; spark->src_color.b = 255; spark->dst_color.r = 255; spark->dst_color.g = (Random_GetDraw() & 0x7F) + 64; spark->dst_color.b = 192 - spark->dst_color.g; spark->col_fade_speed = 3; spark->fade_to_black = 5; spark->life = 10; spark->s_life = 10; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetDraw() & 7) - 3; spark->pos.y = pos.y + (Random_GetDraw() & 7) - 3; spark->pos.z = pos.z + (Random_GetDraw() & 7) - 3; spark->vel.x = (int16_t)(vel.x + (Random_GetDraw() & 0xFF) - 128); spark->vel.y = (int16_t)(vel.y + (Random_GetDraw() & 0xFF) - 128); spark->vel.z = (int16_t)(vel.z + (Random_GetDraw() & 0xFF) - 128); spark->friction = 34; spark->scalar = 1; spark->size.width = (Random_GetDraw() & 3) + 4; spark->src_size.width = spark->size.width; spark->dst_size.width = (Random_GetDraw() & 1) + 1; spark->size.height = (Random_GetDraw() & 3) + 4; spark->src_size.height = spark->size.height; spark->dst_size.height = (Random_GetDraw() & 1) + 1; spark->max_y_vel = 0; spark->gravity = 0; spark->flags = SPARK_F_SCALE; Sparks_FinishSetup(spark); if (!smoke) { return; } SPARK *const smoke_spark = Sparks_GetFreeSpark(); smoke_spark->on = true; smoke_spark->src_color.r = spark->dst_color.r >> 1; smoke_spark->src_color.g = spark->dst_color.g >> 1; smoke_spark->src_color.b = spark->dst_color.b >> 1; smoke_spark->dst_color.r = 32; smoke_spark->dst_color.g = 32; smoke_spark->dst_color.b = 32; smoke_spark->col_fade_speed = (Random_GetDraw() & 3) + 8; smoke_spark->fade_to_black = 4; smoke_spark->draw_type = DRAW_BLEND_ADD; smoke_spark->life = (Random_GetDraw() & 7) + 13; smoke_spark->s_life = smoke_spark->life; smoke_spark->pos.x = pos.x + (vel.x >> 5); smoke_spark->pos.y = pos.y + (vel.y >> 5); smoke_spark->pos.z = pos.z + (vel.z >> 5); smoke_spark->extras = 0; smoke_spark->dynamic = -1; smoke_spark->vel.x = (int16_t)(vel.x + (Random_GetDraw() & 0x3F) - 32); smoke_spark->vel.y = (int16_t)vel.y; smoke_spark->vel.z = (int16_t)(vel.z + (Random_GetDraw() & 0x3F) - 32); smoke_spark->friction = 4; if (Random_GetDraw() & 1) { smoke_spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; smoke_spark->rot_angle = Random_GetDraw() & 0xFFF; if (Random_GetDraw() & 1) { smoke_spark->rot_add = -16 - (Random_GetDraw() & 0xF); } else { smoke_spark->rot_add = (Random_GetDraw() & 0xF) + 16; } } else { smoke_spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE; } smoke_spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; smoke_spark->scalar = 2; smoke_spark->gravity = -8 - (Random_GetDraw() & 3); smoke_spark->max_y_vel = -4 - (Random_GetDraw() & 3); smoke_spark->dst_size.width = (Random_GetDraw() & 0xF) + 24; smoke_spark->src_size.width = smoke_spark->dst_size.width >> 3; smoke_spark->size.width = smoke_spark->dst_size.width >> 3; smoke_spark->dst_size.height = smoke_spark->dst_size.width; smoke_spark->src_size.height = smoke_spark->dst_size.height >> 3; smoke_spark->size.height = smoke_spark->dst_size.height >> 3; Sparks_FinishSetup(smoke_spark); } void Sparks_TriggerRicochet( const GAME_VECTOR pos, const int32_t angle, const int32_t size) { SPARK *spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 255; spark->src_color.g = (Random_GetControl() & 0x1F) + 32; spark->src_color.b = 0; spark->dst_color.r = 192; spark->dst_color.g = (Random_GetControl() & 0x3F) + 96; spark->dst_color.b = 0; spark->col_fade_speed = 8; spark->fade_to_black = 8; spark->life = 24; spark->s_life = 24; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->dynamic = -1; spark->pos.x = pos.x; spark->pos.y = pos.y; spark->pos.z = pos.z; int32_t ang = ((Random_GetControl() & 0x7FF) + angle - 1024) & 0xFFF; spark->vel.x = -Math_Sin(ang << 4) >> 3; spark->vel.y = 2 * (Random_GetControl() & 0x1FF) - 768; spark->vel.z = Math_Cos(ang << 4) >> 3; spark->friction = 1; spark->flags = SPARK_F_SCALE; spark->scalar = 3; spark->gravity = (int16_t)(ABS(spark->vel.y >> 6) + (Random_GetControl() & 0x1F)); spark->size.width = (Random_GetControl() & 3) + 4; spark->src_size.width = spark->size.width; spark->dst_size.width = (Random_GetControl() & 1) + 1; spark->size.height = (Random_GetControl() & 3) + 4; spark->src_size.height = spark->size.height; spark->dst_size.height = (Random_GetControl() & 1) + 1; spark->max_y_vel = 0; Sparks_FinishSetup(spark); spark = Sparks_GetFreeSpark(); spark->on = true; uint8_t c = (uint8_t)((Random_GetControl() & 0x3F) + 128); spark->src_color.r = c; spark->src_color.g = c; spark->src_color.b = c; c >>= 1; spark->dst_color.r = c; spark->dst_color.g = c; spark->dst_color.b = c; spark->draw_type = DRAW_BLEND_SUB; spark->extras = 0; spark->col_fade_speed = 8; spark->fade_to_black = 16; spark->life = 28; spark->s_life = 28; spark->dynamic = -1; spark->pos.x = pos.x; spark->pos.y = pos.y; spark->pos.z = pos.z; ang = ((Random_GetControl() & 0x7FF) + angle - 1023) & 0xFFF; spark->vel.x = -Math_Sin(ang << 4) >> 3; spark->vel.y = (Random_GetControl() & 0x1FF) - 384; spark->vel.z = Math_Cos(ang << 4) >> 3; spark->friction = 33; spark->flags = SPARK_F_SCALE; spark->scalar = 3; spark->gravity = (Random_GetControl() & 7) + 4; spark->size.width = (Random_GetControl() & 3) + 4; spark->src_size.width = spark->size.width; spark->dst_size.width = (Random_GetControl() & 1) + 1; spark->size.height = (Random_GetControl() & 3) + 4; spark->src_size.height = spark->size.height; spark->dst_size.height = (Random_GetControl() & 1) + 1; spark->max_y_vel = 0; Sparks_FinishSetup(spark); } void Sparks_TriggerGunSmoke( const GAME_VECTOR pos, const bool initial, const LARA_GUN_TYPE weapon, const int32_t shade) { Sparks_TriggerGunSmokeDirected(pos, (XYZ_32) {}, initial, weapon, shade); } void Sparks_TriggerGunSmokeDirected( const GAME_VECTOR pos, const XYZ_32 vel, const bool initial, const LARA_GUN_TYPE weapon, const int32_t shade) { SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 0; spark->src_color.g = 0; spark->src_color.b = 0; spark->dst_color.r = shade << 2; spark->dst_color.g = shade << 2; spark->dst_color.b = shade << 2; spark->col_fade_speed = 4; spark->fade_to_black = 32 - (initial << 4); spark->life = (Random_GetControl() & 3) + 40; spark->s_life = spark->life; if ((weapon == LGT_PISTOLS || weapon == LGT_MAGNUMS || weapon == LGT_UZIS) && spark->dst_color.r > 64) { spark->dst_color.r = 64; spark->dst_color.g = 64; spark->dst_color.b = 64; } spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16; spark->pos.y = pos.y + (Random_GetControl() & 0x1F) - 16; spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16; if (initial) { spark->vel.x = vel.x + (Random_GetControl() & 0x3FF) - 512; spark->vel.y = vel.y + (Random_GetControl() & 0x3FF) - 512; spark->vel.z = vel.z + (Random_GetControl() & 0x3FF) - 512; } else { spark->vel.x = ((Random_GetControl() & 0x1FF) - 256) >> 1; spark->vel.y = ((Random_GetControl() & 0x1FF) - 256) >> 1; spark->vel.z = ((Random_GetControl() & 0x1FF) - 256) >> 1; } spark->friction = 4; if (Random_GetControl() & 1) { if (Room_Get(Lara_GetItem()->room_num)->flags.wind) { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_OUTSIDE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->rot_angle = Random_GetControl() & 0xFFF; if (Random_GetControl() & 1) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = (Random_GetControl() & 0xF) + 16; } } else if (Room_Get(Lara_GetItem()->room_num)->flags.wind) { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_OUTSIDE | SPARK_F_SPRITE | SPARK_F_SCALE; } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->scalar = 3; spark->gravity = -2 - (Random_GetControl() & 1); spark->max_y_vel = -2 - (Random_GetControl() & 1); uint8_t size = (Random_GetControl() & 7) - ((weapon == LGT_ROCKET || weapon == LGT_GRENADE) ? 0 : 12) + 24; if (initial) { spark->size.width = size >> 1; spark->src_size.width = spark->size.width; spark->dst_size.width = (size + 4) << 1; } else { spark->size.width = size >> 2; spark->src_size.width = spark->size.width; spark->dst_size.width = size; } if (initial) { spark->size.height = size >> 1; spark->src_size.height = spark->size.width; spark->dst_size.height = (size + 4) << 1; } else { spark->size.height = size >> 2; spark->src_size.height = spark->size.width; spark->dst_size.height = size; } Sparks_FinishSetup(spark); } void Sparks_TriggerShotgunSparks(const XYZ_32 pos, const XYZ_32 vel) { SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 255; spark->src_color.g = 255; spark->src_color.b = 0; spark->dst_color.r = 255; spark->dst_color.g = (Random_GetControl() & 0x7F) + 64; spark->dst_color.b = 0; spark->col_fade_speed = 3; spark->fade_to_black = 5; spark->life = 10; spark->s_life = 10; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 7) - 3; spark->pos.y = pos.y + (Random_GetControl() & 7) - 3; spark->pos.z = pos.z + (Random_GetControl() & 7) - 3; spark->vel.x = vel.x + (Random_GetControl() & 0x1FF) - 256; spark->vel.y = vel.y + (Random_GetControl() & 0x1FF) - 256; spark->vel.z = vel.z + (Random_GetControl() & 0x1FF) - 256; spark->friction = 0; spark->flags = SPARK_F_SCALE; spark->scalar = 2; spark->max_y_vel = 0; spark->gravity = 0; spark->size.width = (Random_GetControl() & 3) + 4; spark->src_size.width = spark->size.width; spark->dst_size.width = 1; spark->size.height = (Random_GetControl() & 3) + 4; spark->src_size.height = spark->size.height; spark->dst_size.height = 1; Sparks_FinishSetup(spark); } void Sparks_TriggerRocketSmoke( const XYZ_32 pos, const int32_t c, const int16_t room_num) { SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 0; spark->src_color.g = 0; spark->src_color.b = 0; spark->dst_color.r = c + 64; spark->dst_color.g = c + 64; spark->dst_color.b = c + 64; spark->fade_to_black = 12; spark->col_fade_speed = (Random_GetControl() & 3) + 4; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->life = (Random_GetControl() & 3) + 20; spark->s_life = spark->life; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 0xF) - 8; spark->pos.y = pos.y + (Random_GetControl() & 0xF) - 8; spark->pos.z = pos.z + (Random_GetControl() & 0xF) - 8; spark->vel.x = (Random_GetControl() & 0xFF) - 128; spark->vel.y = -4 - (Random_GetControl() & 3); spark->vel.z = (Random_GetControl() & 0xFF) - 128; spark->friction = 4; if (Random_GetControl() & 1) { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = Random_GetControl() & 0xFFF; if (Random_GetControl() & 1) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = (Random_GetControl() & 0xF) + 16; } } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->scalar = 3; spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->gravity = -4 - (Random_GetControl() & 3); spark->max_y_vel = -4 - (Random_GetControl() & 3); const uint8_t size = (Random_GetControl() & 7) + 32; spark->dst_size.width = size; spark->src_size.width = size >> 2; spark->size.width = size >> 2; spark->src_size.height = size >> 2; spark->size.height = size >> 2; spark->dst_size.height = size; Sparks_FinishSetup(spark); } void Sparks_TriggerRocketFlame( const XYZ_32 pos, const XYZ_32 vel, const int16_t item_num, const int16_t room_num) { SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = (Random_GetControl() & 0x1F) + 48; spark->src_color.g = spark->src_color.r; spark->src_color.b = (Random_GetControl() & 0x3F) + 192; spark->dst_color.r = (Random_GetControl() & 0x3F) + 192; spark->dst_color.g = (Random_GetControl() & 0x3F) + 128; spark->dst_color.b = 32; spark->fade_to_black = 12; spark->col_fade_speed = (Random_GetControl() & 3) + 12; spark->draw_type = DRAW_BLEND_ADD; spark->life = (Random_GetControl() & 3) + 28; spark->s_life = spark->life; spark->extras = 0; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16; spark->pos.y = pos.y; spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16; spark->vel.x = vel.x; spark->vel.y = vel.y; spark->vel.z = vel.z; spark->friction = 51; spark->item_num = item_num; if (Random_GetControl() & 1) { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ITEM | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = Random_GetControl() & 0xFFF; if (Random_GetControl() & 1) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = (Random_GetControl() & 0xF) + 16; } } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ITEM | SPARK_F_SPRITE | SPARK_F_SCALE; } spark->gravity = 0; spark->max_y_vel = 0; spark->scalar = 2; spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->size.width = (Random_GetControl() & 7) + 32; spark->src_size.width = spark->size.width; spark->dst_size.width = 2; spark->size.height = spark->size.width; spark->src_size.height = spark->size.height; spark->dst_size.height = 2; Sparks_FinishSetup(spark); } void Sparks_TriggerFlamethrowerHitFlame(const XYZ_32 pos) { const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - pos.x; const int32_t dz = lara_item->pos.z - pos.z; const int32_t max_dist = 16 * WALL_L; if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; spark->src_color.r = 255; spark->src_color.g = (Random_GetControl() & 0x1F) + 48; spark->src_color.b = 48; spark->dst_color.r = (Random_GetControl() & 0x3F) + 192; spark->dst_color.g = (Random_GetControl() & 0x3F) + 128; spark->dst_color.b = 32; spark->fade_to_black = 8; spark->col_fade_speed = (Random_GetControl() & 3) + 8; spark->draw_type = DRAW_BLEND_ADD; spark->extras = 0; spark->life = (Random_GetControl() & 7) + 20; spark->s_life = spark->life; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16; spark->pos.y = pos.y; spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16; spark->vel.x = (Random_GetControl() & 0xFF) - 128; spark->vel.y = -16 - (Random_GetControl() & 0xF); spark->vel.z = (Random_GetControl() & 0xFF) - 128; spark->friction = 5; if (Random_GetControl() & 1) { spark->gravity = -16 - (Random_GetControl() & 0x1F); spark->max_y_vel = -16 - (Random_GetControl() & 7); spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = Random_GetControl() & 0xFFF; if (Random_GetControl() & 1) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = (Random_GetControl() & 0xF) + 16; } } else { spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->gravity = -16 - (Random_GetControl() & 0x1F); spark->max_y_vel = -16 - (Random_GetControl() & 7); } spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->scalar = 2; spark->size.width = (Random_GetControl() & 0x1F) + 128; spark->src_size.width = spark->size.width; spark->dst_size.width = spark->size.width >> 4; spark->size.height = spark->size.width; spark->src_size.height = spark->size.height; spark->dst_size.height = spark->size.height >> 4; Sparks_FinishSetup(spark); } void Sparks_TriggerFlamethrowerSmoke(const XYZ_32 pos, const bool uw) { const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - pos.x; const int32_t dz = lara_item->pos.z - pos.z; const int32_t max_dist = 16 * WALL_L; if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) { return; } SPARK *const spark = Sparks_GetFreeSpark(); spark->on = true; if (uw) { spark->src_color.r = 0; spark->src_color.g = 0; spark->src_color.b = 0; spark->dst_color.r = 192; spark->dst_color.g = 192; spark->dst_color.b = 208; } else { spark->src_color.r = 144; spark->src_color.g = 144; spark->src_color.b = 144; spark->dst_color.r = 64; spark->dst_color.g = 64; spark->dst_color.b = 64; } spark->col_fade_speed = 8; spark->fade_to_black = 23; spark->life = (Random_GetControl() & 0xF) + 32; spark->s_life = spark->life; if (uw) { spark->draw_type = DRAW_BLEND_ADD; } else { spark->draw_type = DRAW_BLEND_SUB; } spark->extras = 0; spark->dynamic = -1; spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16; spark->pos.y = pos.y + (Random_GetControl() & 0x1F) - 16; spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16; spark->vel.x = ((Random_GetControl() & 0xFFF) - 2048) >> 2; spark->vel.y = (Random_GetControl() & 0xFF) - 128; spark->vel.z = ((Random_GetControl() & 0xFFF) - 2048) >> 2; if (uw) { spark->friction = 20; spark->vel.y >>= 4; spark->pos.y += 32; } else { spark->friction = 6; } spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE; spark->rot_angle = Random_GetControl() & 0xFFF; if (Random_GetControl() & 1) { spark->rot_add = -16 - (Random_GetControl() & 0xF); } else { spark->rot_add = (Random_GetControl() & 0xF) + 16; } spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx; spark->scalar = 3; if (uw) { spark->max_y_vel = 0; spark->gravity = 0; } else { spark->gravity = -3 - (Random_GetControl() & 3); spark->max_y_vel = -4 - (Random_GetControl() & 3); } spark->dst_size.width = (Random_GetControl() & 0x1F) + 128; spark->size.width = spark->dst_size.width >> 2; spark->src_size.width = spark->size.width; spark->dst_size.height = spark->dst_size.width + (Random_GetControl() & 0x1F) + 32; spark->size.height = spark->dst_size.height >> 3; spark->src_size.height = spark->size.height; Sparks_FinishSetup(spark); } ================================================ FILE: src/trx/game/sparks/spawners.h ================================================ #pragma once #include #include #include void Sparks_TriggerBubble( int32_t x, int32_t y, int32_t z, int32_t size, int32_t size_range, int16_t effect_num); void Sparks_TriggerWaterfallMist(int32_t x, int32_t y, int32_t z, int32_t ang); void Sparks_TriggerBreath(XYZ_32 pos, XYZ_32 vel, int16_t room_num); void Sparks_TriggerUnderwaterExplosion(const ITEM *item); void Sparks_TriggerExplosionBubble(XYZ_32 pos, int16_t room_num); void Sparks_TriggerExplosionSparks( XYZ_32 pos, int32_t extras, int32_t dynamic, int32_t uw, int16_t room_num); void Sparks_TriggerExplosionSmoke(XYZ_32 pos, bool uw, int16_t room_num); void Sparks_TriggerExplosionSmokeEnd(XYZ_32 pos, bool uw, int16_t room_num); void Sparks_TriggerFireFlame(XYZ_32 pos, int32_t body_part, int32_t type); void Sparks_TriggerStaticFlame(XYZ_32 pos, int32_t size); void Sparks_TriggerFireSmoke(XYZ_32 pos, int32_t body_part, int32_t type); void Sparks_TriggerSideFlame( XYZ_32 pos, int32_t angle, int32_t speed, bool pilot); void Sparks_TriggerDartSmoke(XYZ_32 pos, XZ_32 vel, bool hit); void Sparks_TriggerPickupAid(XYZ_32 pos, XZ_32 vel); void Sparks_TriggerFlareSparks(XYZ_32 pos, XYZ_32 vel, bool smoke); void Sparks_TriggerRicochet(GAME_VECTOR pos, int32_t angle, int32_t size); void Sparks_TriggerGunSmoke( GAME_VECTOR pos, bool initial, LARA_GUN_TYPE weapon, int32_t shade); void Sparks_TriggerGunSmokeDirected( GAME_VECTOR pos, XYZ_32 vel, bool initial, LARA_GUN_TYPE weapon, int32_t shade); void Sparks_TriggerShotgunSparks(XYZ_32 pos, XYZ_32 vel); void Sparks_TriggerRocketSmoke(XYZ_32 pos, int32_t c, int16_t room_num); void Sparks_TriggerRocketFlame( XYZ_32 pos, XYZ_32 vel, int16_t item_num, int16_t room_num); void Sparks_TriggerBlood(XYZ_32 pos, int32_t angle_12, int32_t count); void Sparks_TriggerBloodD(XYZ_32 pos, int32_t angle_12, int32_t count); void Sparks_TriggerFlamethrowerHitFlame(XYZ_32 pos); void Sparks_TriggerFlamethrowerSmoke(XYZ_32 pos, bool uw); ================================================ FILE: src/trx/game/sparks/types.h ================================================ #pragma once #include #include #include #include #include typedef struct SPARK { bool on; uint8_t s_life; uint8_t life; // NOTE: `pos` is either absolute world position, or a relative offset when // attached to an FX/ITEM and not using `SPARK_F_ATTACHED_POS`. XYZ_32 pos; XYZ_32 prev_pos; XYZ_32 prev_world_pos; XYZ_32 vel; struct { uint8_t width; uint8_t height; } src_size, dst_size, size, prev_size; RGB_888 src_color; RGB_888 dst_color; RGB_888 color; RGB_888 prev_color; uint8_t scalar; uint8_t col_fade_speed; uint8_t fade_to_black; int16_t gravity; int8_t max_y_vel; uint8_t friction; uint16_t flags; union { // effect/item index depending on flags (SF_FX/SF_ITEM) int16_t effect_num; int16_t item_num; }; uint8_t room_num; uint8_t node_num; uint8_t extras; int8_t dynamic; uint16_t rot_angle; // 0..0xFFF uint16_t prev_rot_angle; // 0..0xFFF int8_t rot_add; int32_t sprite_idx; DRAW_TYPE draw_type; } SPARK; ================================================ FILE: src/trx/game/sparks.h ================================================ #pragma once #include #include ================================================ FILE: src/trx/game/spawn.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static void M_ShootAtLara(EFFECT *const effect) { const ITEM *const lara_item = Lara_GetItem(); const int32_t dx = lara_item->pos.x - effect->pos.x; const int32_t dy = lara_item->pos.y - effect->pos.y; const int32_t dz = lara_item->pos.z - effect->pos.z; const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(lara_item); const int32_t dist_vert = dy + bounds->max.y + 3 * (bounds->min.y - bounds->max.y) / 4; const int32_t dist_horz = Math_Sqrt(SQUARE(dz) + SQUARE(dx)); effect->rot.x = -Math_Atan(dist_horz, dist_vert); effect->rot.y = Math_Atan(dz, dx); effect->rot.x += (Random_GetControl() - 0x4000) / 64; effect->rot.y += (Random_GetControl() - 0x4000) / 64; } XYZ_32 Spawn_GetRayPos( const GAME_VECTOR start, GAME_VECTOR hit_pos, const int32_t dist) { // Get the position at wall LOS_Check(&start, &hit_pos, true); // Retract a bit const int16_t angle = XYZ_32_GetYaw((XYZ_32) { .x = hit_pos.x - start.x, .y = hit_pos.y - start.y, .z = hit_pos.z - start.z, }); hit_pos.pos.x -= (dist * Math_Sin(angle)) >> W2V_SHIFT; hit_pos.pos.z -= (dist * Math_Cos(angle)) >> W2V_SHIFT; return hit_pos.pos; } void Spawn_Splash(const ITEM *const item) { if (g_TRVersion == 3) { FX_Water_Splash(item); return; } const int32_t water_height = Room_GetWaterHeight(item->pos, item->room_num); int16_t room_num = item->room_num; Room_GetSector(item->pos, &room_num); for (int32_t i = 0; i < 10; i++) { const int16_t effect_num = Effect_Create(room_num); if (effect_num == NO_EFFECT) { continue; } EFFECT *const effect = Effect_Get(effect_num); effect->object_id = O_SPLASH_1; effect->pos.x = item->pos.x; effect->pos.y = water_height; effect->pos.z = item->pos.z; effect->rot.y = 2 * Random_GetDraw() + DEG_180; effect->speed = Random_GetDraw() / 256; effect->frame_num = 0; } } void Spawn_Ricochet(const GAME_VECTOR pos) { if (g_TRVersion == 3) { const ITEM *const lara_item = Lara_GetItem(); const int32_t angle16 = Math_Atan( lara_item->pos.z - pos.pos.z, lara_item->pos.x - pos.pos.x); Sparks_TriggerRicochet(pos, ((uint16_t)angle16 >> 4) & 0x0FFF, 16); Sound_Effect(SFX_LARA_RICOCHET, &pos.pos, SPM_NORMAL); return; } const int16_t effect_num = Effect_Create(pos.room_num); if (effect_num != NO_EFFECT) { EFFECT *const effect = Effect_Get(effect_num); effect->object_id = O_RICOCHET; effect->pos = pos.pos; effect->counter = 4; effect->frame_num = -3 * Random_GetDraw() / 0x8000; Sound_Effect(SFX_LARA_RICOCHET, &effect->pos, SPM_NORMAL); } } void Spawn_RicochetRay(const GAME_VECTOR start, GAME_VECTOR hit_pos) { hit_pos.pos = Spawn_GetRayPos(start, hit_pos, STEP_L / 12); Spawn_Ricochet(hit_pos); } void Spawn_Bubble(const XYZ_32 *const pos, const int16_t room_num) { if (g_TRVersion == 3) { Spawn_BubbleEx(pos, room_num, 8, 8); return; } const int16_t effect_num = Effect_Create(room_num); if (effect_num == NO_EFFECT) { return; } EFFECT *const effect = Effect_Get(effect_num); effect->pos = *pos; effect->object_id = O_BUBBLE_1; effect->frame_num = -((Random_GetDraw() * 3) / 0x8000); effect->speed = 10 + ((Random_GetDraw() * 6) / 0x8000); } void Spawn_BubbleEx( const XYZ_32 *const pos, const int16_t room_num, const int32_t size, const int32_t size_range) { if (g_TRVersion != 3) { Spawn_Bubble(pos, room_num); return; } int16_t water_room = room_num; Room_GetSector(*pos, &water_room); if (!Room_Get(water_room)->flags.underwater) { return; } const int16_t effect_num = Effect_Create(room_num); if (effect_num == NO_EFFECT) { return; } EFFECT *const effect = Effect_Get(effect_num); effect->pos = *pos; effect->object_id = O_BUBBLE_1; effect->frame_num = 0; effect->speed = (Random_GetControl() & 0xFF) + 64; effect->fall_speed = (Random_GetControl() & 0x1F) + 32; Sparks_TriggerBubble(pos->x, pos->y, pos->z, size, size_range, effect_num); } int16_t Spawn_Blood( const int32_t x, const int32_t y, const int32_t z, const int16_t speed, const int16_t y_rot, const int16_t room_num) { if (g_TRVersion == 3) { if (Room_Get(room_num)->flags.underwater) { FX_Water_TriggerUnderwaterBlood( (XYZ_32) { x, y, z }, Random_GetControl() & 7); } else { Sparks_TriggerBlood( (XYZ_32) { x, y, z }, y_rot >> 4, (Random_GetControl() & 7) + 6); } return NO_EFFECT; } OBJECT_ID object_id = NO_OBJECT; switch (g_Config.visuals.blood_effects) { case BLOOD_EFFECTS_DISABLED: return NO_EFFECT; case BLOOD_EFFECTS_PINK: object_id = O_BLOOD_PINK; break; case BLOOD_EFFECTS_RED: object_id = O_BLOOD; break; case BLOOD_EFFECTS_NUMBER_OF: return NO_EFFECT; } if (object_id == NO_OBJECT) { return NO_EFFECT; } const int16_t effect_num = Effect_Create(room_num); if (effect_num != NO_EFFECT) { EFFECT *const effect = Effect_Get(effect_num); effect->pos.x = x; effect->pos.y = y; effect->pos.z = z; effect->rot.y = y_rot; effect->speed = speed; effect->frame_num = 0; effect->object_id = object_id; effect->counter = 0; } return effect_num; } int16_t Spawn_BloodD( const int32_t x, const int32_t y, const int32_t z, const int16_t speed, const int16_t y_rot, const int16_t room_num) { if (g_TRVersion == 3) { if (Room_Get(room_num)->flags.underwater) { FX_Water_TriggerUnderwaterBloodD( (XYZ_32) { x, y + 64, z }, Random_GetDraw() & 7); } else { Sparks_TriggerBloodD( (XYZ_32) { x, y, z }, y_rot >> 4, (Random_GetDraw() & 7) + 6); } return NO_EFFECT; } return Spawn_Blood(x, y, z, speed, y_rot, room_num); } void Spawn_BloodBath( const int32_t x, const int32_t y, const int32_t z, const int16_t speed, const int16_t y_rot, const int16_t room_num, const int32_t count) { for (int32_t i = 0; i < count; i++) { if (g_TRVersion == 3) { Spawn_Blood( x - (Random_GetControl() << 9) / 0x8000 + 256, y - (Random_GetControl() << 9) / 0x8000 + 256, z - (Random_GetControl() << 9) / 0x8000 + 256, speed, y_rot, room_num); } else { Spawn_Blood( x - (Random_GetDraw() << 9) / 0x8000 + 256, y - (Random_GetDraw() << 9) / 0x8000 + 256, z - (Random_GetDraw() << 9) / 0x8000 + 256, speed, y_rot, room_num); } } } void Spawn_BloodBathD( const int32_t x, const int32_t y, const int32_t z, const int16_t speed, const int16_t y_rot, const int16_t room_num, const int32_t count) { for (int32_t i = 0; i < count; i++) { Spawn_BloodD( x - (Random_GetDraw() << 9) / 0x8000 + 256, y, z - (Random_GetDraw() << 9) / 0x8000 + 256, speed, y_rot, room_num); } } int16_t Spawn_GunShot( const int32_t x, const int32_t y, const int32_t z, const int16_t speed, const int16_t y_rot, const int16_t room_num) { const int16_t effect_num = Effect_Create(room_num); if (effect_num == NO_EFFECT) { return effect_num; } EFFECT *const effect = Effect_Get(effect_num); effect->pos.x = x; effect->pos.y = y; effect->pos.z = z; effect->room_num = room_num; effect->rot.z = 0; effect->rot.x = 0; effect->rot.y = y_rot; effect->counter = 3; effect->frame_num = 0; effect->object_id = O_GUN_FLASH; effect->shade = SHADE_NEUTRAL; return effect_num; } int16_t Spawn_GunHit( const int32_t x, const int32_t y, const int32_t z, const int16_t speed, const int16_t y_rot, const int16_t room_num) { const ITEM *const lara_item = Lara_GetItem(); XYZ_32 vec = { .x = -((Random_GetDraw() - 0x4000) << 7) / 0x7FFF, .y = -((Random_GetDraw() - 0x4000) << 7) / 0x7FFF, .z = -((Random_GetDraw() - 0x4000) << 7) / 0x7FFF, }; Collide_GetJointAbsPosition( lara_item, &vec, Random_GetControl() * LM_NUMBER_OF / 0x7FFF); Spawn_Blood( vec.x, vec.y, vec.z, lara_item->speed, lara_item->rot.y, lara_item->room_num); Sound_Effect(SFX_LARA_BULLETHIT, &lara_item->pos, SPM_NORMAL); return Spawn_GunShot(x, y, z, speed, y_rot, room_num); } int16_t Spawn_GunMiss( const int32_t x, const int32_t y, const int32_t z, const int16_t speed, const int16_t y_rot, const int16_t room_num) { const ITEM *const lara_item = Lara_GetItem(); const GAME_VECTOR pos = { .x = lara_item->pos.x + ((Random_GetDraw() - 0x4000) << 9) / 0x7FFF, .y = lara_item->floor, .z = lara_item->pos.z + ((Random_GetDraw() - 0x4000) << 9) / 0x7FFF, .room_num = lara_item->room_num, }; Spawn_Ricochet(pos); return Spawn_GunShot(x, y, z, speed, y_rot, room_num); } void Spawn_GunShell(const LARA_GUN_TYPE weapon_type, const bool right) { if (g_TRVersion < 3) { return; } const ITEM *const lara_item = Lara_GetItem(); const LARA_INFO *const lara = Lara_GetLaraInfo(); XYZ_32 offset = right ? g_Weapons[weapon_type].shell_pos : g_Weapons[weapon_type].shell_pos_alt; if (offset.x == 0 && offset.y == 0 && offset.z == 0) { return; } Lara_GetMeshPos(right ? LM_HAND_R : LM_HAND_L, &offset); const int16_t effect_num = Effect_Create(lara_item->room_num); if (effect_num == NO_EFFECT) { return; } const bool shotgun = weapon_type == LGT_SHOTGUN; EFFECT *const effect = Effect_Get(effect_num); effect->pos = offset; effect->room_num = lara_item->room_num; effect->rot.x = 0; effect->rot.y = 0; effect->rot.z = (int16_t)Random_GetControl(); effect->speed = (int16_t)((Random_GetControl() & 0x1F) + 16); effect->object_id = shotgun ? O_SHOTGUN_SHELL : O_GUN_SHELL; effect->frame_num = Object_Get(effect->object_id)->mesh_idx; effect->fall_speed = (int16_t)(-48 - (Random_GetControl() & 7)); effect->shade = 0x4210; effect->counter = (int16_t)((Random_GetControl() & 1) + 1); if (shotgun || weapon_type == LGT_M16 || weapon_type == LGT_MP5) { const int32_t spread = (Random_GetControl() & 0xFFF); effect->flag1 = lara->left_arm.rot.y + lara_item->rot.y - spread + lara->torso_rot.y + (shotgun ? 0x2800 : 0x4800); if (!shotgun && effect->speed < 24) { effect->speed += 24; } } else if (right) { effect->flag1 = lara_item->rot.y - (Random_GetControl() & 0xFFF) + lara->left_arm.rot.y + 0x4800; } else { effect->flag1 = lara_item->rot.y + (Random_GetControl() & 0xFFF) + lara->left_arm.rot.y - 0x4800; } } int16_t Spawn_AtlanteanShard( int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num) { int16_t effect_num = Effect_Create(room_num); if (effect_num != NO_EFFECT) { EFFECT *effect = Effect_Get(effect_num); effect->room_num = room_num; effect->pos.x = x; effect->pos.y = y; effect->pos.z = z; effect->rot.x = 0; effect->rot.y = y_rot; effect->rot.z = 0; effect->object_id = O_MISSILE_ATLANTEAN_SHARD; effect->frame_num = 0; effect->speed = 250; effect->shade = 3584; M_ShootAtLara(effect); } return effect_num; } int16_t Spawn_AtlanteanBomb( int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num) { int16_t effect_num = Effect_Create(room_num); if (effect_num != NO_EFFECT) { EFFECT *effect = Effect_Get(effect_num); effect->room_num = room_num; effect->pos.x = x; effect->pos.y = y; effect->pos.z = z; effect->rot.x = 0; effect->rot.y = y_rot; effect->rot.z = 0; effect->object_id = O_MISSILE_ATLANTEAN_BOMB; effect->frame_num = 0; effect->speed = 220; effect->shade = SHADE_NEUTRAL; M_ShootAtLara(effect); } return effect_num; } int16_t Spawn_FireStream( const int32_t x, const int32_t y, const int32_t z, int16_t speed, const int16_t y_rot, const int16_t room_num) { const int16_t effect_num = Effect_Create(room_num); if (effect_num == NO_EFFECT) { return effect_num; } EFFECT *const effect = Effect_Get(effect_num); effect->pos.x = x; effect->pos.y = y; effect->pos.z = z; effect->rot.x = 0; effect->rot.y = y_rot; effect->rot.z = 0; effect->room_num = room_num; effect->speed = 200; effect->frame_num = ((Object_Get(O_MISSILE_FLAME)->mesh_count + 1) * Random_GetDraw()) >> 15; effect->object_id = O_MISSILE_FLAME; effect->shade = 14 * 256; M_ShootAtLara(effect); if (Object_Get(O_DRAGON_FRONT)->loaded) { effect->counter = 0x4000; } else { effect->counter = 20; } return effect_num; } void Spawn_MysticLight(const int16_t item_num) { const ITEM *const item = Item_Get(item_num); const int16_t effect_num = Effect_Create(item->room_num); if (effect_num != NO_EFFECT) { EFFECT *const effect = Effect_Get(effect_num); effect->object_id = O_TWINKLE; effect->rot.y = 2 * Random_GetDraw(); effect->pos = XYZ_32_OffsetYaw(item->pos, effect->rot.y, 5 * WALL_L); effect->pos.y += (Random_GetDraw() >> 2) - WALL_L; effect->room_num = item->room_num; effect->counter = item_num; effect->frame_num = 0; } // clang-format off Output_AddDynamicLight( item->pos, ((4 * Random_GetDraw()) >> 15) + 12, ((4 * Random_GetDraw()) >> 15) + 10); // clang-format on } int16_t Spawn_Knife( const int32_t x, const int32_t y, const int32_t z, const int16_t speed, const int16_t y_rot, const int16_t room_num) { const int16_t effect_num = Effect_Create(room_num); if (effect_num == NO_EFFECT) { return effect_num; } EFFECT *const effect = Effect_Get(effect_num); effect->pos.x = x; effect->pos.y = y; effect->pos.z = z; effect->room_num = room_num; effect->rot.x = 0; effect->rot.y = y_rot; effect->rot.z = 0; effect->speed = 150; effect->frame_num = 0; effect->object_id = O_MISSILE_KNIFE; effect->shade = 3584; M_ShootAtLara(effect); return effect_num; } int16_t Spawn_Harpoon( const int32_t x, const int32_t y, const int32_t z, const int16_t speed, const int16_t y_rot, const int16_t room_num) { const int16_t effect_num = Effect_Create(room_num); if (effect_num != NO_EFFECT) { EFFECT *const effect = Effect_Get(effect_num); effect->pos.x = x; effect->pos.y = y; effect->pos.z = z; effect->room_num = room_num; effect->rot.x = 0; effect->rot.y = y_rot; effect->rot.z = 0; effect->speed = 150; effect->fall_speed = 0; effect->frame_num = 0; effect->object_id = O_MISSILE_HARPOON; effect->shade = 3584; M_ShootAtLara(effect); } return effect_num; } ================================================ FILE: src/trx/game/spawn.h ================================================ #pragma once #include #include #include #include XYZ_32 Spawn_GetRayPos(GAME_VECTOR start, GAME_VECTOR hit_pos, int32_t dist); void Spawn_Splash(const ITEM *item); void Spawn_Ricochet(GAME_VECTOR pos); void Spawn_RicochetRay(GAME_VECTOR start, GAME_VECTOR hit_pos); void Spawn_Bubble(const XYZ_32 *pos, int16_t room_num); void Spawn_BubbleEx( const XYZ_32 *pos, int16_t room_num, int32_t size, int32_t size_range); int16_t Spawn_Blood( int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num); int16_t Spawn_BloodD( int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num); void Spawn_BloodBath( int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num, int32_t count); void Spawn_BloodBathD( int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num, int32_t count); int16_t Spawn_GunShot( int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num); int16_t Spawn_GunHit( int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num); int16_t Spawn_GunMiss( int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num); void Spawn_GunShell(LARA_GUN_TYPE weapon_type, bool right); void Spawn_ShotgunShell(void); int16_t Spawn_AtlanteanShard( int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num); int16_t Spawn_AtlanteanBomb( int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num); int16_t Spawn_FireStream( int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num); void Spawn_MysticLight(int16_t item_num); int16_t Spawn_Knife( int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num); int16_t Spawn_Harpoon( int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num); ================================================ FILE: src/trx/game/stats/common.c ================================================ #include #include #include #include #include #include #include #include bool Stats_IsSecretValid(const int16_t secret_idx) { const GF_LEVEL *const level = Game_GetCurrentLevel(); const LEVEL_MAX_STATS *const max_stats = Stats_GetLevelMaxStats(level); if (secret_idx < 0 || secret_idx >= STATS_MAX_SECRETS) { return false; } const uint32_t secret_mask = 1 << secret_idx; return (secret_mask & max_stats->all_secrets_mask) != 0; } bool Stats_HasSecret(const int16_t secret_idx) { const GF_LEVEL *const level = Game_GetCurrentLevel(); const LEVEL_MAX_STATS *const max_stats = Stats_GetLevelMaxStats(level); RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); if (secret_idx < 0 || secret_idx >= STATS_MAX_SECRETS) { return false; } const uint32_t secret_mask = 1 << secret_idx; if ((secret_mask & max_stats->all_secrets_mask) == 0) { return false; } return (resume->stats.secret_flags & secret_mask) != 0; } bool Stats_RemoveSecret(const int16_t secret_idx) { const GF_LEVEL *const level = Game_GetCurrentLevel(); const LEVEL_MAX_STATS *const max_stats = Stats_GetLevelMaxStats(level); RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); if (secret_idx < 0 || secret_idx >= STATS_MAX_SECRETS) { return false; } const uint32_t secret_mask = 1 << secret_idx; if ((secret_mask & max_stats->all_secrets_mask) == 0) { return false; } if (!(resume->stats.secret_flags & secret_mask)) { return false; } LOG_INFO("Removing secret %d", secret_idx); resume->stats.secret_flags &= ~secret_mask; resume->stats.secret_count--; return true; } bool Stats_AddSecret(const int16_t secret_idx) { const GF_LEVEL *const level = Game_GetCurrentLevel(); const LEVEL_MAX_STATS *const max_stats = Stats_GetLevelMaxStats(level); RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); if (secret_idx < 0 || secret_idx >= STATS_MAX_SECRETS) { return false; } const uint32_t secret_mask = 1 << secret_idx; if ((secret_mask & max_stats->all_secrets_mask) == 0) { return false; } if (resume->stats.secret_flags & secret_mask) { return false; } LOG_INFO("Adding secret %d", secret_idx); resume->stats.secret_flags |= secret_mask; resume->stats.secret_count++; return true; } void Stats_UpdateSecrets(LEVEL_STATS *const stats) { stats->secret_count = 0; for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) { stats->secret_count += (stats->secret_flags & (1 << i)) ? 1 : 0; } } void Stats_MarkSecretCollected(const ITEM *const item) { RESUME_INFO *const resume = Savegame_GetCurrentInfo(Game_GetCurrentLevel()); resume->stats.secret_flags |= Pickup_GetSecretMask(item); Stats_UpdateSecrets(&resume->stats); } bool Stats_CheckAllLevelSecretsPickedUp(void) { const GF_LEVEL *const level = Game_GetCurrentLevel(); const LEVEL_MAX_STATS *const max_stats = Stats_GetLevelMaxStats(level); const RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); int32_t flags = resume->stats.secret_flags; size_t count = 0; while (flags != 0) { count += flags & 1; flags >>= 1; } return count >= max_stats->max_pickup_secret_count; } bool Stats_CheckAllSecretsCollected(void) { const FINAL_STATS final_stats = Stats_ComputeFinalStats(false); return final_stats.stats.secret_count >= final_stats.max_stats.max_secret_count; } void Stats_AddMedipacksUsed(const double medipack_value) { RESUME_INFO *const resume = Savegame_GetCurrentInfo(Game_GetCurrentLevel()); resume->stats.medipacks_used += medipack_value; } void Stats_AddDeath(void) { const GF_LEVEL *const current_level = Game_GetCurrentLevel(); RESUME_INFO *const resume = Savegame_GetCurrentInfo(current_level); resume->stats.death_count++; const SAVEGAME_SLOT_REF save_slot = Savegame_GetBoundSlot(); if (Savegame_IsValidSlotRef(save_slot)) { Savegame_UpdateDeathCounters(save_slot, resume->stats.death_count); } } void Stats_UpdateTimer(void) { const GF_LEVEL *const level = Game_GetCurrentLevel(); if (level != nullptr) { RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); resume->stats.timer++; } } void Stats_AddKill(void) { const GF_LEVEL *const level = Game_GetCurrentLevel(); if (level != nullptr) { RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); resume->stats.kill_count++; } } void Stats_AddCrystal(void) { const GF_LEVEL *const level = Game_GetCurrentLevel(); if (level != nullptr) { RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); resume->stats.crystal_count++; } } void Stats_AddPickup(void) { const GF_LEVEL *const level = Game_GetCurrentLevel(); if (level != nullptr) { RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); resume->stats.pickup_count++; } } void Stats_AddAmmoHits(void) { const GF_LEVEL *const level = Game_GetCurrentLevel(); if (level != nullptr) { RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); resume->stats.ammo_hits++; } } void Stats_AddAmmoUsed(void) { const GF_LEVEL *const level = Game_GetCurrentLevel(); if (level != nullptr) { RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); resume->stats.ammo_used++; } } void Stats_AddDistanceTravelled(const XYZ_32 pos, const XYZ_32 last_pos) { const GF_LEVEL *const level = Game_GetCurrentLevel(); if (level != nullptr) { RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); resume->stats.distance_travelled += XYZ_32_GetDistance(pos, last_pos); } } FINAL_STATS Stats_ComputeFinalStats(const bool include_bonus_levels) { FINAL_STATS result = {}; const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); for (int32_t i = 0; i < level_table->count; i++) { const GF_LEVEL *const level = &level_table->levels[i]; if (!(level->type == GFL_NORMAL || (level->type == GFL_BONUS && include_bonus_levels))) { continue; } const RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); if (resume != nullptr) { #define L_ADD(prop) result.stats.prop += resume->stats.prop; L_ADD(kill_count); L_ADD(crystal_count); L_ADD(pickup_count); L_ADD(secret_count); L_ADD(timer); L_ADD(ammo_hits); L_ADD(ammo_used); L_ADD(medipacks_used); L_ADD(distance_travelled); L_ADD(death_count); #undef L_ADD } const LEVEL_MAX_STATS *const max_stats = Stats_GetLevelMaxStats(level); if (max_stats != nullptr) { #define L_ADD(prop) result.max_stats.prop += max_stats->prop; L_ADD(max_kill_count); L_ADD(max_kill_ally_count); L_ADD(max_kill_non_ally_count); L_ADD(max_crystal_count); L_ADD(max_pickup_count); L_ADD(max_secret_count); L_ADD(max_pickup_secret_count); #undef L_ADD } } return result; } OBJECT_ID Stats_GetSecretObject(const int32_t secret_idx) { const GF_LEVEL *const level = Game_GetCurrentLevel(); if (level == nullptr) { return NO_OBJECT; } const LEVEL_MAX_STATS *const stats = Stats_GetLevelMaxStats(level); ASSERT(stats != nullptr); ASSERT(secret_idx >= 0 && secret_idx < STATS_MAX_SECRETS); return stats->secret_objects[secret_idx].assigned_object_id; } uint32_t Stats_GetSecretMaskForItem( const GF_LEVEL *const level, const int16_t item_num) { if (level == nullptr) { return 0; } const LEVEL_MAX_STATS *const max_stats = Stats_GetLevelMaxStats(level); if (max_stats == nullptr) { return 0; } for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) { if (max_stats->secret_item_masks[i].item_num == item_num) { return max_stats->secret_item_masks[i].secret_mask; } } return 0; } void Stats_MarkAlliesHostile(void) { const GF_LEVEL *const level = Game_GetCurrentLevel(); RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); resume->hurt_allies = true; } ================================================ FILE: src/trx/game/stats/common.h ================================================ #pragma once #include #include #include bool Stats_HasSecret(int16_t secret_idx); bool Stats_RemoveSecret(int16_t secret_idx); bool Stats_AddSecret(int16_t secret_idx); bool Stats_IsSecretValid(int16_t secret_idx); OBJECT_ID Stats_GetSecretObject(int32_t secret_idx); uint32_t Stats_GetSecretMaskForItem(const GF_LEVEL *level, int16_t item_num); void Stats_UpdateSecrets(LEVEL_STATS *stats); void Stats_MarkSecretCollected(const ITEM *item); bool Stats_CheckAllSecretsCollected(void); bool Stats_CheckAllLevelSecretsPickedUp(void); void Stats_UpdateTimer(void); void Stats_AddKill(void); void Stats_AddCrystal(void); void Stats_AddPickup(void); void Stats_AddAmmoHits(void); void Stats_AddAmmoUsed(void); void Stats_AddDeath(void); void Stats_AddMedipacksUsed(double medipack_value); void Stats_AddDistanceTravelled(XYZ_32 pos, XYZ_32 last_pos); void Stats_MarkAlliesHostile(void); FINAL_STATS Stats_ComputeFinalStats(bool include_bonus_levels); ================================================ FILE: src/trx/game/stats/const.h ================================================ #pragma once #define STATS_MAX_SECRETS 16 ================================================ FILE: src/trx/game/stats/init.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_CACHE_VERSION 5 #define M_CACHE_FILENAME "max_stats.cache.json" static LEVEL_MAX_STATS *m_Stats = nullptr; static int32_t m_StatsCapacity = 0; static bool m_GameHasCrystals = false; static const OBJECT_ID m_FullInitObjectIDs[] = { O_BARTOLI, O_CENTAUR_STATUE, O_PODS, O_BIG_POD, NO_OBJECT, }; static bool M_ShouldUseFullInitialisation(const OBJECT_ID object_id) { for (int32_t i = 0; m_FullInitObjectIDs[i] != NO_OBJECT; i++) { if (m_FullInitObjectIDs[i] == object_id) { return true; } } return false; } static void M_SetupStatsFullInitObjects(void) { for (int32_t i = 0; m_FullInitObjectIDs[i] != NO_OBJECT; i++) { OBJECT *const obj = Object_Get(m_FullInitObjectIDs[i]); if (!obj->loaded || obj->setup_func == nullptr) { continue; } obj->setup_func(obj); } } static void M_EnsureStatsStorage(const int32_t level_count) { ASSERT(level_count >= 0); if (m_StatsCapacity != level_count) { m_Stats = Memory_Realloc( m_Stats, sizeof(LEVEL_MAX_STATS) * (size_t)level_count); m_StatsCapacity = level_count; } } static uint64_t M_ComputeInputsChecksum(const GF_LEVEL_TABLE *const level_table) { uint64_t hash = LevelCache_InitChecksum("max_stats_cache", M_CACHE_VERSION); hash = Hash_FNV1a64_UpdateU32(hash, (uint32_t)level_table->count); hash = Hash_FNV1a64_UpdateU32( hash, (uint32_t)g_Config.gameplay.restore_ps1_enemies); for (int32_t i = 0; i < level_table->count; i++) { const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i); hash = LevelCache_UpdateLevelChecksum(hash, level); hash = Hash_FNV1a64_UpdateU32(hash, level->unobtainable.pickups); hash = Hash_FNV1a64_UpdateU32(hash, level->unobtainable.kills); hash = Hash_FNV1a64_UpdateU32(hash, level->unobtainable.ally_kills); hash = Hash_FNV1a64_UpdateU32(hash, level->unobtainable.secrets); } return hash; } static JSON_OBJECT *M_SerializeLevelMaxStats(const LEVEL_MAX_STATS *const stats) { JSON_OBJECT *const out = JSON_ObjectNew(); JSON_ObjectAppendInt64( out, "max_pickup_secret_count", (int64_t)stats->max_pickup_secret_count); JSON_ObjectAppendInt64( out, "max_kill_count", (int64_t)stats->max_kill_count); JSON_ObjectAppendInt64( out, "max_kill_ally_count", (int64_t)stats->max_kill_ally_count); JSON_ObjectAppendInt64( out, "max_kill_non_ally_count", (int64_t)stats->max_kill_non_ally_count); JSON_ObjectAppendInt64( out, "max_crystal_count", (int64_t)stats->max_crystal_count); JSON_ObjectAppendInt64( out, "max_pickup_count", (int64_t)stats->max_pickup_count); JSON_ObjectAppendInt64( out, "max_secret_count", (int64_t)stats->max_secret_count); JSON_ObjectAppendInt64(out, "all_secrets_mask", stats->all_secrets_mask); JSON_ARRAY *const secret_item_masks = JSON_ArrayNew(); for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) { JSON_OBJECT *const entry = JSON_ObjectNew(); JSON_ObjectAppendInt( entry, "item_num", stats->secret_item_masks[i].item_num); JSON_ObjectAppendInt64( entry, "secret_mask", (int64_t)stats->secret_item_masks[i].secret_mask); JSON_ArrayAppendObject(secret_item_masks, entry); } JSON_ObjectAppendArray(out, "secret_item_masks", secret_item_masks); JSON_ARRAY *const secret_objects = JSON_ArrayNew(); for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) { JSON_OBJECT *const entry = JSON_ObjectNew(); JSON_ObjectAppendBool(entry, "taken", stats->secret_objects[i].taken); JSON_ObjectAppendInt( entry, "assigned_object_id", (int32_t)stats->secret_objects[i].assigned_object_id); JSON_ObjectAppendInt( entry, "item_num", stats->secret_objects[i].item_num); JSON_ArrayAppendObject(secret_objects, entry); } JSON_ObjectAppendArray(out, "secret_objects", secret_objects); return out; } static bool M_DeserializeLevelMaxStats( LEVEL_MAX_STATS *const out, JSON_OBJECT *const obj) { if (out == nullptr || obj == nullptr) { return false; } out->max_pickup_secret_count = (size_t)JSON_ObjectGetInt64( obj, "max_pickup_secret_count", (int64_t)out->max_pickup_secret_count); out->max_kill_count = (size_t)JSON_ObjectGetInt64( obj, "max_kill_count", (int64_t)out->max_kill_count); out->max_kill_ally_count = (size_t)JSON_ObjectGetInt64( obj, "max_kill_ally_count", (int64_t)out->max_kill_ally_count); out->max_kill_non_ally_count = (size_t)JSON_ObjectGetInt64( obj, "max_kill_non_ally_count", (int64_t)out->max_kill_non_ally_count); out->max_crystal_count = (size_t)JSON_ObjectGetInt64( obj, "max_crystal_count", (int64_t)out->max_crystal_count); out->max_pickup_count = (size_t)JSON_ObjectGetInt64( obj, "max_pickup_count", (int64_t)out->max_pickup_count); out->max_secret_count = (size_t)JSON_ObjectGetInt64( obj, "max_secret_count", (int64_t)out->max_secret_count); out->all_secrets_mask = (uint32_t)JSON_ObjectGetInt64( obj, "all_secrets_mask", out->all_secrets_mask); JSON_ARRAY *const secret_item_masks = JSON_ObjectGetArray(obj, "secret_item_masks"); if (secret_item_masks != nullptr && secret_item_masks->length == (size_t)STATS_MAX_SECRETS) { for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) { JSON_OBJECT *const entry = JSON_ArrayGetObject(secret_item_masks, i); if (entry == nullptr) { continue; } out->secret_item_masks[i].item_num = JSON_ObjectGetInt( entry, "item_num", out->secret_item_masks[i].item_num); out->secret_item_masks[i].secret_mask = (uint32_t)JSON_ObjectGetInt64( entry, "secret_mask", out->secret_item_masks[i].secret_mask); } } JSON_ARRAY *const secret_objects = JSON_ObjectGetArray(obj, "secret_objects"); if (secret_objects != nullptr && secret_objects->length == (size_t)STATS_MAX_SECRETS) { for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) { JSON_OBJECT *const entry = JSON_ArrayGetObject(secret_objects, i); if (entry == nullptr) { continue; } out->secret_objects[i].taken = JSON_ObjectGetBool( entry, "taken", out->secret_objects[i].taken); out->secret_objects[i].assigned_object_id = (OBJECT_ID)JSON_ObjectGetInt( entry, "assigned_object_id", (int32_t)out->secret_objects[i].assigned_object_id); out->secret_objects[i].item_num = JSON_ObjectGetInt( entry, "item_num", out->secret_objects[i].item_num); } } return true; } static bool M_TryLoadCache( const uint64_t expected_checksum, const GF_LEVEL_TABLE *const level_table) { JSON_VALUE *const root_value = LevelCache_ReadJSON(M_CACHE_FILENAME, expected_checksum); if (root_value == nullptr) { return false; } JSON_OBJECT *const root = JSON_ValueAsObject(root_value); if (root == nullptr) { JSON_ValueFree(root_value); return false; } const int32_t version = JSON_ObjectGetInt(root, "version", -1); if (version != M_CACHE_VERSION) { JSON_ValueFree(root_value); return false; } const int32_t cached_level_count = JSON_ObjectGetInt(root, "level_count", -1); if (cached_level_count != level_table->count) { JSON_ValueFree(root_value); return false; } JSON_ARRAY *const levels = JSON_ObjectGetArray(root, "levels"); if (levels == nullptr) { JSON_ValueFree(root_value); return false; } // Clear any existing stats; cache may not cover every entry. memset(m_Stats, 0, sizeof(LEVEL_MAX_STATS) * (size_t)m_StatsCapacity); for (size_t i = 0; i < levels->length; i++) { JSON_OBJECT *const entry = JSON_ArrayGetObject(levels, i); if (entry == nullptr) { continue; } const int32_t level_num = JSON_ObjectGetInt(entry, "num", -1); if (level_num < 0 || level_num >= m_StatsCapacity) { continue; } JSON_OBJECT *const stats_obj = JSON_ObjectGetObject(entry, "stats"); if (stats_obj == nullptr) { continue; } M_DeserializeLevelMaxStats(&m_Stats[level_num], stats_obj); } JSON_ValueFree(root_value); return true; } static void M_WriteCache( const uint64_t checksum, const GF_LEVEL_TABLE *const level_table) { JSON_OBJECT *const root = JSON_ObjectNew(); JSON_ObjectAppendInt(root, "version", M_CACHE_VERSION); JSON_ObjectAppendInt(root, "level_count", level_table->count); JSON_ARRAY *const levels = JSON_ArrayNew(); for (int32_t i = 0; i < level_table->count; i++) { const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i); JSON_OBJECT *const entry = JSON_ObjectNew(); JSON_ObjectAppendInt(entry, "num", level->num); JSON_ObjectAppendObject( entry, "stats", M_SerializeLevelMaxStats(&m_Stats[level->num])); JSON_ArrayAppendObject(levels, entry); } JSON_ObjectAppendArray(root, "levels", levels); JSON_VALUE *const root_value = JSON_ValueFromObject(root); LevelCache_WriteJSON(M_CACHE_FILENAME, checksum, root_value); JSON_ValueFree(root_value); } __attribute__((destructor)) static void M_Shutdown(void) { if (m_Stats != nullptr) { Memory_Free(m_Stats); m_Stats = nullptr; } m_StatsCapacity = 0; } LEVEL_MAX_STATS *Stats_GetLevelMaxStats(const GF_LEVEL *const level) { ASSERT(m_Stats != nullptr); ASSERT(level != nullptr); ASSERT(level->num >= 0 && level->num < m_StatsCapacity); return &m_Stats[level->num]; } bool Stats_GameHasCrystals(void) { return m_GameHasCrystals; } void Stats_CalculateMaxStats(void) { const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); M_EnsureStatsStorage(level_table->count); memset(m_Stats, 0, sizeof(LEVEL_MAX_STATS) * (size_t)m_StatsCapacity); m_GameHasCrystals = false; BENCHMARK benchmark = Benchmark_Start(); const uint64_t expected_checksum = M_ComputeInputsChecksum(level_table); if (M_TryLoadCache(expected_checksum, level_table)) { goto finish; } for (int32_t i = 0; i < level_table->count; i++) { const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i); if (level->type != GFL_NORMAL && level->type != GFL_BONUS) { continue; } VFILE *const file = VFile_CreateFromPath(level->path); if (file == nullptr) { continue; } const LEVEL_FORMAT_LOADER *const loader = Level_Format_GuessLoader(file); if (loader != nullptr) { Level_Unload(); Creature_Reset(); Lua_ClearLevelListeners(); Lua_SetScriptContext(LUA_CONTEXT_LEVEL); if (level->script_path != nullptr) { LUA_RESULT res = Lua_EvalFile(level->script_path); if (res.code != LUA_OK) { LOG_ERROR("Lua level script error: %s", res.message); } Lua_FreeResult(&res); } Lua_SetScriptContext(LUA_CONTEXT_GLOBAL); Lua_FireEventInt32(LUA_EVENT_BEFORE_LEVEL_FILE, level->num); Inject_InitLevel(level, INJECTION_MODE_STATS); if (loader->probe(loader, file, LEVEL_FORMAT_PROBE_STATS)) { Inject_AllInjections(); M_SetupStatsFullInitObjects(); const int32_t item_count = Item_GetLevelCount(); for (int32_t item_num = 0; item_num < item_count; item_num++) { ITEM *const item = Item_Get(item_num); if (M_ShouldUseFullInitialisation(item->object_id)) { Item_Initialise(item_num); } else { ROOM *const room = Room_Get(item->room_num); item->next_item = room->item_num; room->item_num = item_num; } } Carrier_InitialiseLevel(level); Stats_ScanLevel(level); } Inject_Cleanup(); } GameBuf_Reset(); VFile_Close(file); #if 0 const LEVEL_MAX_STATS *const max_stats = Stats_GetLevelMaxStats(level); LOG_INFO( "Level %d (%s)", GF_GetLevelOrdinalNumber(GFLT_MAIN, level), level->title); LOG_INFO(" pickups: %d", max_stats->max_pickup_count); LOG_INFO(" kills: %d", max_stats->max_kill_count); LOG_INFO(" allies: %d", max_stats->max_kill_ally_count); LOG_INFO(" enemies: %d", max_stats->max_kill_non_ally_count); LOG_INFO(" crystals: %d", max_stats->max_crystal_count); LOG_INFO(" secrets: %d", max_stats->max_secret_count); #endif } M_WriteCache(expected_checksum, level_table); finish: for (int32_t i = 0; i < level_table->count; i++) { const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i); const LEVEL_MAX_STATS *const max_stats = Stats_GetLevelMaxStats(level); if (max_stats->max_crystal_count != 0) { m_GameHasCrystals = true; break; } } const FINAL_STATS final_stats = Stats_ComputeFinalStats(true); LOG_INFO("Max pickups: %d", final_stats.max_stats.max_pickup_count); LOG_INFO("Max kills: %d", final_stats.max_stats.max_kill_count); LOG_INFO(" allies: %d", final_stats.max_stats.max_kill_ally_count); LOG_INFO(" enemies: %d", final_stats.max_stats.max_kill_non_ally_count); LOG_INFO("Max crystals: %d", final_stats.max_stats.max_crystal_count); LOG_INFO("Max secrets: %d", final_stats.max_stats.max_secret_count); Benchmark_End(&benchmark, nullptr); } ================================================ FILE: src/trx/game/stats/init.h ================================================ #pragma once #include #include LEVEL_MAX_STATS *Stats_GetLevelMaxStats(const GF_LEVEL *level); bool Stats_GameHasCrystals(void); void Stats_CalculateMaxStats(void); ================================================ FILE: src/trx/game/stats/scan.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static bool m_KillableItems[MAX_ITEMS] = {}; static void M_IncludeKillableItem( LEVEL_MAX_STATS *const stats, int16_t item_num) { m_KillableItems[item_num] = true; const ITEM *const item = Item_Get(item_num); const bool is_ally = Creature_IsAlly(item); if (is_ally) { stats->max_kill_ally_count++; } else { stats->max_kill_non_ally_count++; } LOG_TRACE( "Killable item %d: object = %s", item_num, Object_GetName(item->object_id)); if (Carrier_GetItemCount(item_num) > 0) { LOG_TRACE( "+%d pickups from carrier %d", Carrier_GetItemCount(item_num), item_num); stats->max_pickup_count += Carrier_GetItemCount(item_num); } } static uint32_t M_ReserveSecretConcreteBit( LEVEL_MAX_STATS *const stats, const OBJECT_ID object_id, const int32_t position) { LOG_TRACE("Reserving bit %d for secret %d", position, object_id); if (position < 0 || position >= STATS_MAX_SECRETS) { LOG_ERROR( "Invalid secret bit %d (max: %d)", position, STATS_MAX_SECRETS); return 0; } const uint32_t secret_bit = 1 << position; if (!(stats->all_secrets_mask & secret_bit)) { stats->all_secrets_mask |= secret_bit; stats->max_secret_count++; if (object_id != NO_OBJECT) { stats->max_pickup_secret_count++; } } stats->secret_objects[position].assigned_object_id = object_id; stats->secret_objects[position].item_num = NO_ITEM; stats->secret_objects[position].taken = true; return secret_bit; } static uint32_t M_ReserveSecretUnusedBit( LEVEL_MAX_STATS *const stats, const OBJECT_ID object_id) { // Find unused bit int32_t position = 0; uint32_t n = stats->all_secrets_mask; while ((n & 1) == 1) { n >>= 1; position++; } return M_ReserveSecretConcreteBit(stats, object_id, position); } static void M_CheckTriggers( LEVEL_MAX_STATS *const stats, const ROOM *const room, const int32_t room_num, const int32_t z_sector, const int32_t x_sector) { if (z_sector == 0 || z_sector == room->size.z - 1) { if (x_sector == 0 || x_sector == room->size.x - 1) { return; } } const SECTOR *const sector = Room_GetUnitSector(room, x_sector, z_sector); if (sector->trigger == nullptr) { return; } const TRIGGER_CMD *cmd = sector->trigger->command; for (; cmd != nullptr; cmd = cmd->next_cmd) { if (cmd->type == TO_SECRET) { const uint16_t secret_num = (uint16_t)(intptr_t)cmd->parameter; M_ReserveSecretConcreteBit(stats, NO_OBJECT, secret_num); } else if (cmd->type == TO_OBJECT) { const int16_t item_num = (int16_t)(intptr_t)cmd->parameter; if (m_KillableItems[item_num]) { continue; } const ITEM *const item = Item_Get(item_num); switch (item->object_id) { case O_RAPTOR_EMITTER: case O_WASP_MUTANT_EMITTER: for (int32_t i = 0; i < sector->trigger->timer; i++) { M_IncludeKillableItem(stats, item_num); } break; case O_PIERRE: // Add Pierre pickup and kills if oneshot if (sector->trigger->one_shot) { M_IncludeKillableItem(stats, item_num); } break; case O_PODS: case O_BIG_POD: // Check for only valid pods const OBJECT_ID object_id = Pod_GetBugObjectID(item); if (Object_Get(object_id)->loaded) { M_IncludeKillableItem(stats, item_num); } break; case O_BARTOLI: case O_DRAGON_BACK: case O_DRAGON_FRONT: if (Object_Get(O_DRAGON_BACK)->loaded && Object_Get(O_DRAGON_FRONT)->loaded) { M_IncludeKillableItem(stats, item_num); if (Object_Get(O_PUZZLE_OPTION_2)->loaded || Object_Get(O_PUZZLE_ITEM_2)->loaded) { LOG_TRACE("+1 pickup from dragon"); stats->max_pickup_count++; } } break; case O_EEL: case O_BIG_EEL: case O_ORCA: break; case O_SCION_ITEM_3: M_IncludeKillableItem(stats, item_num); break; default: // Add killable if object triggered if (Creature_IsHostile(item) || Creature_IsAlly(item) || Creature_IsAllyTargetingEnemy(item)) { M_IncludeKillableItem(stats, item_num); } break; } } } } static void M_TraverseFloor(LEVEL_MAX_STATS *const stats) { for (int32_t i = 0; i < Room_GetCount(); i++) { const ROOM *const room = Room_Get(i); for (int32_t z_sector = 0; z_sector < room->size.z; z_sector++) { for (int32_t x_sector = 0; x_sector < room->size.x; x_sector++) { M_CheckTriggers(stats, room, i, z_sector, x_sector); } } } } static void M_CalculateStats(LEVEL_MAX_STATS *const stats) { memset(stats, 0, sizeof(*stats)); for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) { stats->secret_item_masks[i].item_num = NO_ITEM; stats->secret_item_masks[i].secret_mask = 0; stats->secret_objects[i].assigned_object_id = NO_OBJECT; stats->secret_objects[i].item_num = NO_ITEM; stats->secret_objects[i].taken = false; } memset(&m_KillableItems, 0, sizeof(m_KillableItems)); for (int32_t i = 0; i < Item_GetTotalCount(); i++) { const ITEM *const item = Item_Get(i); if (Object_IsType(item->object_id, g_PickupObjects) && !Carrier_IsItemCarried(i)) { LOG_TRACE( "+1 pickup from pickup item %d in room %d", i, item->room_num); stats->max_pickup_count++; } else if (item->object_id == O_SAVE_CRYSTAL_ITEM) { LOG_TRACE( "+1 crystal from save crystal item %d in room %d", i, item->room_num); stats->max_crystal_count++; } } // Check triggers for special pickups / killables M_TraverseFloor(stats); for (int32_t i = 0; i < Item_GetTotalCount(); i++) { ITEM *const item = Item_Get(i); if (item->object_id < O_FIRST || item->object_id >= O_NUMBER_OF) { LOG_ERROR("Bad Object number (%d) on Item %d", item->object_id, i); continue; } if (item->object_id == O_COMBAT_END) { M_IncludeKillableItem(stats, i); } if (Object_IsType(item->object_id, g_SecretObjects)) { int32_t position = -1; for (int32_t j = 0; j < STATS_MAX_SECRETS; j++) { if (!stats->secret_objects[j].taken) { position = j; break; } } if (position == -1) { LOG_ERROR("Too many secrets, max %d", STATS_MAX_SECRETS); break; } stats->secret_objects[position].assigned_object_id = item->object_id; stats->secret_objects[position].item_num = i; stats->secret_objects[position].taken = true; position++; } } // Sorts secret objects by their type so that the dragons line up nicely for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) { if (stats->secret_objects[i].assigned_object_id == NO_OBJECT) { continue; } for (int32_t j = i + 1; j < STATS_MAX_SECRETS; j++) { if (stats->secret_objects[j].assigned_object_id == NO_OBJECT) { continue; } if (stats->secret_objects[i].assigned_object_id > stats->secret_objects[j].assigned_object_id) { SWAP(stats->secret_objects[i], stats->secret_objects[j]); } } } // Assign secret items their bits so they know which secret to set on // pickup. NOTE: Do not persist runtime pickup state here. This scan runs // at game launch to compute max stats; gameplay level loads restore secret // masks using cached info in LEVEL_MAX_STATS. for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) { const int32_t item_num = stats->secret_objects[i].item_num; if (item_num == NO_ITEM) { continue; } const uint32_t secret_mask = M_ReserveSecretUnusedBit( stats, stats->secret_objects[i].assigned_object_id); for (int32_t j = 0; j < STATS_MAX_SECRETS; j++) { if (stats->secret_item_masks[j].item_num == NO_ITEM) { stats->secret_item_masks[j].item_num = item_num; stats->secret_item_masks[j].secret_mask = secret_mask; break; } } } } void Stats_ScanLevel(const GF_LEVEL *const level) { ASSERT(level != nullptr); BENCHMARK benchmark = Benchmark_Start(); LEVEL_MAX_STATS *const max_stats = Stats_GetLevelMaxStats(level); M_CalculateStats(max_stats); max_stats->max_pickup_count += GF_GetSecretRewardCount(level); max_stats->max_pickup_count -= level->unobtainable.pickups; max_stats->max_secret_count -= level->unobtainable.secrets; max_stats->max_kill_ally_count -= level->unobtainable.ally_kills; max_stats->max_kill_non_ally_count -= level->unobtainable.kills; max_stats->max_kill_count = max_stats->max_kill_non_ally_count + max_stats->max_kill_ally_count; Benchmark_End(&benchmark, nullptr); } ================================================ FILE: src/trx/game/stats/scan.h ================================================ #pragma once #include #include void Stats_ScanLevel(const GF_LEVEL *level); ================================================ FILE: src/trx/game/stats/types.h ================================================ #pragma once #include #include #include #include typedef struct { size_t max_pickup_secret_count; size_t max_kill_count; size_t max_kill_ally_count; size_t max_kill_non_ally_count; size_t max_crystal_count; size_t max_pickup_count; size_t max_secret_count; uint32_t all_secrets_mask; struct { int32_t item_num; uint32_t secret_mask; } secret_item_masks[STATS_MAX_SECRETS]; struct { bool taken; OBJECT_ID assigned_object_id; int32_t item_num; } secret_objects[STATS_MAX_SECRETS]; } LEVEL_MAX_STATS; typedef struct STATS_COMMON { uint32_t timer; uint32_t kill_count; uint32_t ammo_used; uint32_t ammo_hits; uint32_t distance_travelled; double medipacks_used; uint16_t crystal_count; uint16_t pickup_count; int32_t death_count; uint16_t secrets_mask; uint16_t secret_count; } STATS_COMMON; typedef struct { struct STATS_COMMON; uint16_t secret_flags; } LEVEL_STATS; typedef struct { STATS_COMMON stats; LEVEL_MAX_STATS max_stats; } FINAL_STATS; ================================================ FILE: src/trx/game/stats.h ================================================ #pragma once #include #include #include #include #include ================================================ FILE: src/trx/game/types.h ================================================ #pragma once #include #include #include typedef struct { union { struct { int32_t x; int32_t y; int32_t z; }; XYZ_32 pos; }; int16_t room_num; } GAME_VECTOR; typedef struct { union { struct { int32_t x; int32_t y; int32_t z; }; XYZ_32 pos; }; int16_t data; int16_t flags; } OBJECT_VECTOR; typedef struct { int32_t vertex_count; union { uint16_t texture_idx; uint16_t palette_idx; }; uint16_t vertices[4]; // trapezoid ratios for textured quads // that cannot be really shared between vertices TEXTURE_ZW_F texture_zw[4]; bool double_sided; bool enable_reflections; } FACE; ================================================ FILE: src/trx/game/ui/common.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include static struct { MEMORY_ARENA_ALLOCATOR alloc; UI_NODE *root; // The top-level container UI_NODE *current; // The current container into which we attach nodes } m_Priv = { .alloc = { .default_chunk_size = 1024 * 4, }, }; extern void UI_ClearDraw(void); static UI_INPUT M_TranslateInput(const uint32_t system_keycode) { // clang-format off switch (system_keycode) { case SDLK_UP: return UI_KEY_UP; case SDLK_DOWN: return UI_KEY_DOWN; case SDLK_LEFT: return UI_KEY_LEFT; case SDLK_RIGHT: return UI_KEY_RIGHT; case SDLK_HOME: return UI_KEY_HOME; case SDLK_END: return UI_KEY_END; case SDLK_BACKSPACE: return UI_KEY_BACK; case SDLK_RETURN: return UI_KEY_RETURN; case SDLK_ESCAPE: return UI_KEY_ESCAPE; } // clang-format on return -1; } // Depth-first measure pass static void M_MeasureNode(UI_NODE *const node) { if (node == nullptr || node->ops.measure == nullptr) { return; } // Recurse to children UI_NODE *child = node->first_child; while (child != nullptr) { M_MeasureNode(child); child = child->next_sibling; } node->ops.measure(node); } // Depth-first layout pass static void M_LayoutNode( UI_NODE *const node, const float x, const float y, const float w, const float h) { if (node == nullptr || node->ops.layout == nullptr) { return; } node->ops.layout(node, x, y, w, h); // Recursing to children is a responsibility of the layout function. } // Depth-first draw pass static void M_DrawNode(const UI_NODE *const node) { if (node == nullptr || node->ops.draw == nullptr) { return; } node->ops.draw(node); // Recursing to children is a responsibility of the draw function. } // Allocate a new node UI_NODE *UI_AllocNode( const UI_WIDGET_OPS *const ops, const size_t additional_size) { const size_t size = Memory_Align(sizeof(UI_NODE)) + Memory_Align(additional_size); UI_NODE *const node = Memory_ArenaAlloc(&m_Priv.alloc, size); memset(node, 0, size); node->ops = *ops; node->data = (char *)node + Memory_Align(sizeof(UI_NODE)); return node; } // Attach child to parent's child list void UI_AddChild(UI_NODE *const child) { // Special case - the root widget if (m_Priv.root == nullptr) { m_Priv.root = child; return; } UI_NODE *const parent = m_Priv.current; if (parent == nullptr || child == nullptr) { return; } child->parent = parent; if (parent->first_child == nullptr) { parent->first_child = child; } else { parent->last_child->next_sibling = child; } parent->last_child = child; } void UI_PushCurrent(UI_NODE *const child) { m_Priv.current = child; } void UI_PopCurrent(void) { ASSERT(m_Priv.current != nullptr); m_Priv.current = m_Priv.current->parent; if (m_Priv.current == nullptr) { m_Priv.root = nullptr; } } const UI_NODE *UI_GetCurrent(void) { return m_Priv.current; } // Scene management void UI_BeginScene(void) { UI_ClearDraw(); Memory_ArenaReset(&m_Priv.alloc); UI_BeginAnchor(0.5f, 0.5f); // Make a root node. } void UI_EndScene(void) { M_MeasureNode(m_Priv.root); M_LayoutNode(m_Priv.root, 0, 0, UI_GetCanvasWidth(), UI_GetCanvasHeight()); M_DrawNode(m_Priv.root); UI_EndAnchor(); ASSERT(m_Priv.root == nullptr); } void UI_Init(void) { UI_InitEvents(); UI_InitText(); UI_InitDraw(); } void UI_Shutdown(void) { UI_ShutdownDraw(); UI_ShutdownText(); Memory_ArenaFree(&m_Priv.alloc); UI_ShutdownEvents(); } void UI_ToggleState(bool *const config_setting) { *config_setting ^= true; Config_Update(); Console_Log( *config_setting ? GS("general/osd/ui_on") : GS("general/osd/ui_off")); } void UI_HandleKeyDown(const uint32_t key) { UI_FireEvent((EVENT) { .name = "key_down", .sender = nullptr, .data = (void *)M_TranslateInput(key), }); } void UI_HandleKeyUp(const uint32_t key) { UI_FireEvent((EVENT) { .name = "key_up", .sender = nullptr, .data = (void *)M_TranslateInput(key), }); } void UI_HandleTextEdit(const char *const text) { UI_FireEvent((EVENT) { .name = "text_edit", .sender = nullptr, .data = (void *)text }); } int32_t UI_GetCanvasWidth(void) { return UI_Scaler_CalcInverse( Viewport_GetWidth(VIEWPORT_UI), UI_SCALER_TARGET_GENERIC); } int32_t UI_GetCanvasHeight(void) { return UI_Scaler_CalcInverse( Viewport_GetHeight(VIEWPORT_UI), UI_SCALER_TARGET_GENERIC); } float UI_ScaleX(const float x) { return UI_Scaler_Calc(x * 0x10000, UI_SCALER_TARGET_GENERIC) / 0x10000.p0; } float UI_ScaleY(const float y) { return UI_Scaler_Calc(y * 0x10000, UI_SCALER_TARGET_GENERIC) / 0x10000.p0; } ================================================ FILE: src/trx/game/ui/common.h ================================================ #pragma once #include #include typedef enum { UI_KEY_UP, UI_KEY_DOWN, UI_KEY_LEFT, UI_KEY_RIGHT, UI_KEY_HOME, UI_KEY_END, UI_KEY_BACK, UI_KEY_RETURN, UI_KEY_ESCAPE, } UI_INPUT; // Forward declaration of the node and its vtable. struct UI_NODE; typedef struct { void (*measure)(struct UI_NODE *node); void (*layout)(struct UI_NODE *node, float x, float y, float w, float h); void (*draw)(const struct UI_NODE *node); } UI_WIDGET_OPS; // Node structure that forms the UI tree typedef struct UI_NODE { // Common operations on a widget UI_WIDGET_OPS ops; // Final layout rectangle float x; float y; float w; float h; // Needed size from measure pass float measure_w; float measure_h; // Link to parent and siblings to form a tree struct UI_NODE *parent; struct UI_NODE *first_child; struct UI_NODE *last_child; struct UI_NODE *next_sibling; // Widget-specific data void *data; } UI_NODE; // Dimensions in virtual pixels of the screen area // (640x480 for any 4:3 resolution on 1.00 text scaling) int32_t UI_GetCanvasWidth(void); int32_t UI_GetCanvasHeight(void); float UI_ScaleX(float x); float UI_ScaleY(float y); // Public API for scene management void UI_BeginScene(void); void UI_EndScene(void); // Helpers to add children, etc. UI_NODE *UI_AllocNode(const UI_WIDGET_OPS *ops, size_t additional_size); void UI_AddChild(UI_NODE *child); void UI_PushCurrent(UI_NODE *child); void UI_PopCurrent(void); const UI_NODE *UI_GetCurrent(void); void UI_Init(void); void UI_Shutdown(void); void UI_ToggleState(bool *config_setting); void UI_HandleKeyDown(uint32_t key); void UI_HandleKeyUp(uint32_t key); void UI_HandleTextEdit(const char *text); ================================================ FILE: src/trx/game/ui/dialogs/base_passport.c ================================================ #include #include #include #include #include #include #include #include static int32_t M_GetVisibleRows(void) { if (g_TRVersion >= 2) { return 10; } else { const int32_t res_h = UI_Scaler_CalcInverse( Viewport_GetHeight(VIEWPORT_UI), UI_SCALER_TARGET_TEXT); if (res_h <= 240) { return 5; } else if (res_h <= 384) { return 7; } else if (res_h <= 480) { return 10; } else { return 12; } } } void UI_BasePassportDialog_Init( UI_REQUESTER_STATE *const req, const size_t max_rows) { UI_Requester_Init(req, M_GetVisibleRows(), max_rows, true); req->row_pad = 4.0f; req->row_spacing = g_TRVersion == 1 ? 2.0f : 3.0f; req->show_arrows = g_TRVersion == 1; req->reserve_space = true; } void UI_BasePassportDialog_Control(UI_REQUESTER_STATE *const req) { UI_Requester_SetVisibleRows(req, M_GetVisibleRows()); } void UI_BeginBasePassportDialog(void) { const float modal_y = g_Inv_Mode == INV_TITLE_MODE ? 0.81f : 0.62f; UI_BeginModal(0.5f, modal_y); UI_BeginResize(300.0f, -1.0f); } void UI_EndBasePassportDialog(void) { UI_EndResize(); UI_EndModal(); } ================================================ FILE: src/trx/game/ui/dialogs/base_passport.h ================================================ // Base passport dialog functions. // Does not implement a function on its own, and is used mostly for placement // and sizing of the larger dialogs such as load/save game. #pragma once #include #include // state functions void UI_BasePassportDialog_Init(UI_REQUESTER_STATE *req, size_t max_rows); void UI_BasePassportDialog_Control(UI_REQUESTER_STATE *req); // draw functions void UI_BeginBasePassportDialog(void); void UI_EndBasePassportDialog(void); ================================================ FILE: src/trx/game/ui/dialogs/color_editor.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #define M_COLOR_EDITOR_PADDING 8.0f #define M_COLOR_EDITOR_TITLE_MARGIN 5.0f #define M_MAX_STOPS 7 #define M_OKLCH_MAX_CHROMA 0.4f typedef enum { M_COLOR_ROW_HUE, M_COLOR_ROW_CHROMA, M_COLOR_ROW_LIGHTNESS, M_COLOR_ROW_COUNT, } M_COLOR_ROW; typedef struct { float h; float c; float l; bool use_state_h; bool use_state_c; bool use_state_l; } M_STOP_DEF; typedef struct { GAME_STRING_ID label_id; int32_t stop_count; M_STOP_DEF stops[M_MAX_STOPS]; } M_ROW_DEF; struct UI_COLOR_EDITOR_DIALOG_STATE { bool show; const UI_SETTINGS_OPTION *option; M_COLOR_ROW component_idx; float h; float c; float l; RGB_888 color; RGB_888 cached_stops[M_COLOR_ROW_COUNT][M_MAX_STOPS]; }; static M_ROW_DEF m_RowDefs[M_COLOR_ROW_COUNT]; __attribute__((constructor)) static void M_Init(void) { m_RowDefs[M_COLOR_ROW_HUE] = (M_ROW_DEF) { .label_id = GS_ID("general/settings/common/hue"), .stop_count = 7, .stops = { { .h = 0.0f, .use_state_c = true, .use_state_l = true }, { .h = 60.0f, .use_state_c = true, .use_state_l = true }, { .h = 120.0f, .use_state_c = true, .use_state_l = true }, { .h = 180.0f, .use_state_c = true, .use_state_l = true }, { .h = 240.0f, .use_state_c = true, .use_state_l = true }, { .h = 300.0f, .use_state_c = true, .use_state_l = true }, { .h = 360.0f, .use_state_c = true, .use_state_l = true }, }, }; m_RowDefs[M_COLOR_ROW_CHROMA] = (M_ROW_DEF) { .label_id = GS_ID("general/settings/common/chroma"), .stop_count = 2, .stops = { { .c = 0.0f, .use_state_h = true, .use_state_l = true }, { .c = M_OKLCH_MAX_CHROMA, .use_state_h = true, .use_state_l = true }, }, }; m_RowDefs[M_COLOR_ROW_LIGHTNESS] = (M_ROW_DEF) { .label_id = GS_ID("general/settings/common/lightness"), .stop_count = 3, .stops = { { .l = 0.0f, .use_state_h = true, .use_state_c = true }, { .l = 0.5f, .use_state_h = true, .use_state_c = true }, { .l = 1.0f, .use_state_h = true, .use_state_c = true }, }, }; } static float M_GetSliderValue( const UI_COLOR_EDITOR_DIALOG_STATE *const s, const M_COLOR_ROW row) { switch (row) { case M_COLOR_ROW_HUE: return s->h / 360.0f; case M_COLOR_ROW_CHROMA: return s->c / M_OKLCH_MAX_CHROMA; case M_COLOR_ROW_LIGHTNESS: return s->l; case M_COLOR_ROW_COUNT: break; } return 0.0f; } static RGB_888 M_GetStopColor( const UI_COLOR_EDITOR_DIALOG_STATE *const s, const M_STOP_DEF *const stop) { const float h = stop->use_state_h ? s->h : stop->h; const float c = stop->use_state_c ? s->c : stop->c; const float l = stop->use_state_l ? s->l : stop->l; return Color_OKLCHToRGB(l, c, h); } static void M_GetGradientStops( const UI_COLOR_EDITOR_DIALOG_STATE *const s, const M_COLOR_ROW row, RGB_888 out_stops[M_MAX_STOPS]) { const M_ROW_DEF *const row_def = &m_RowDefs[row]; for (int32_t i = 0; i < row_def->stop_count; i++) { out_stops[i] = M_GetStopColor(s, &row_def->stops[i]); } } static void M_RebuildCache(UI_COLOR_EDITOR_DIALOG_STATE *const s) { s->color = Color_OKLCHToRGB(s->l, s->c, s->h); for (M_COLOR_ROW row = M_COLOR_ROW_HUE; row < M_COLOR_ROW_COUNT; row++) { M_GetGradientStops(s, row, s->cached_stops[row]); } } static void M_SetLocalColorFromRGB( UI_COLOR_EDITOR_DIALOG_STATE *const s, const RGB_888 rgb) { Color_RGBToOKLCH(rgb, &s->l, &s->c, &s->h); CLAMP(s->c, 0.0f, M_OKLCH_MAX_CHROMA); M_RebuildCache(s); } static void M_EmitLocalColorAsRGB(UI_COLOR_EDITOR_DIALOG_STATE *const s) { M_RebuildCache(s); *(RGB_888 *)s->option->target = s->color; Config_Update(); } static void M_ColorEditorRow( UI_COLOR_EDITOR_DIALOG_STATE *const s, const M_COLOR_ROW row) { const bool is_selected = s->component_idx == row; const M_ROW_DEF *const row_def = &m_RowDefs[row]; if (is_selected) { UI_BeginFrame(UI_FRAME_SELECTED_OPTION); } UI_BeginPad(g_TRVersion == 1 ? 1.0f : 0.0f, g_TRVersion == 1 ? 1.0f : 0.0f); UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, .align = { .h = UI_STACK_H_ALIGN_SPAN }, .spacing = { .v = 1.0f }, }); UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_HORIZONTAL, .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE }, }); UI_Label(GameString_Get(row_def->label_id)); UI_BeginRowArrows(is_selected, is_selected, UI_ROW_ARROWS_MEDIUM); UI_GradientSlider((UI_GRADIENT_SLIDER_SETTINGS) { .width = 100.0f, .value = M_GetSliderValue(s, row), .stop_count = row_def->stop_count, .stops = s->cached_stops[row], }); UI_EndRowArrows(); UI_EndStack(); UI_EndStack(); UI_EndPad(); if (is_selected) { UI_EndFrame(); } } UI_COLOR_EDITOR_DIALOG_STATE *UI_ColorEditorDialog_Init(void) { UI_COLOR_EDITOR_DIALOG_STATE *const s = Memory_Alloc(sizeof(*s)); return s; } void UI_ColorEditorDialog_Free(UI_COLOR_EDITOR_DIALOG_STATE *const s) { Memory_Free(s); } void UI_ColorEditorDialog_Open( UI_COLOR_EDITOR_DIALOG_STATE *const s, const UI_SETTINGS_OPTION *const option) { ASSERT(s != nullptr); ASSERT(option != nullptr); ASSERT(Config_GetOption(option->target)->type == COT_RGB888); const RGB_888 *const color = option->target; s->show = true; s->option = option; s->component_idx = 0; M_SetLocalColorFromRGB(s, *color); } void UI_ColorEditorDialog_Close(UI_COLOR_EDITOR_DIALOG_STATE *const s) { if (s == nullptr) { return; } s->show = false; s->option = nullptr; s->component_idx = 0; s->h = 0.0f; s->c = 0.0f; s->l = 0.0f; } bool UI_ColorEditorDialog_IsOpen(const UI_COLOR_EDITOR_DIALOG_STATE *const s) { return s != nullptr && s->show; } void UI_ColorEditorDialog_Control(UI_COLOR_EDITOR_DIALOG_STATE *const s) { if (s == nullptr || !s->show) { return; } const UI_SETTINGS_OPTION *const option = s->option; if (option == nullptr) { UI_ColorEditorDialog_Close(s); return; } if (g_InputDB.menu_back || g_InputDB.look) { UI_ColorEditorDialog_Close(s); return; } if (g_InputDB.menu_up) { int32_t next_idx = (int32_t)s->component_idx - 1; if (next_idx < 0) { next_idx = M_COLOR_ROW_COUNT - 1; } s->component_idx = (M_COLOR_ROW)next_idx; } else if (g_InputDB.menu_down) { int32_t next_idx = (int32_t)s->component_idx + 1; if (next_idx >= M_COLOR_ROW_COUNT) { next_idx = 0; } s->component_idx = (M_COLOR_ROW)next_idx; } else if (g_InputDB.menu_left || g_InputDB.menu_right) { int32_t delta = g_Input.slow ? option->delta_slow : option->delta_fast; if (delta == 0) { delta = 1; } if (g_InputDB.menu_left) { delta = -delta; } if (s->component_idx == M_COLOR_ROW_HUE) { s->h += delta; while (s->h < 0.0f) { s->h += 360.0f; } while (s->h > 360.0f) { s->h -= 360.0f; } } else if (s->component_idx == M_COLOR_ROW_CHROMA) { s->c += (delta / 100.0f) * M_OKLCH_MAX_CHROMA; CLAMP(s->c, 0.0f, M_OKLCH_MAX_CHROMA); } else { s->l += delta / 100.0f; CLAMP(s->l, 0.0f, 1.0f); } M_EmitLocalColorAsRGB(s); } else if (g_InputDB.unbind_key) { Config_RestoreOptionDefault(option->target); M_SetLocalColorFromRGB(s, *(RGB_888 *)option->target); Config_Update(); } } void UI_ColorEditorDialog(UI_COLOR_EDITOR_DIALOG_STATE *const s) { if (s == nullptr || !s->show || s->option == nullptr) { return; } UI_BeginModal(0.5f, 0.5f); UI_BeginFrame(UI_FRAME_DIALOG_BACKGROUND_HEAVY); UI_BeginPad(M_COLOR_EDITOR_PADDING, M_COLOR_EDITOR_PADDING); UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, .spacing = { .v = 5.0f }, .align = { .h = UI_STACK_H_ALIGN_SPAN }, }); UI_BeginAnchor(0.5f, 0.5f); const char *const title = Config_GetOptionTitle(Config_GetOption(s->option->target)); UI_Label(title != nullptr ? title : ""); UI_EndAnchor(); UI_Spacer(M_COLOR_EDITOR_TITLE_MARGIN, M_COLOR_EDITOR_TITLE_MARGIN); UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, .align = { .h = UI_STACK_H_ALIGN_SPAN }, .spacing = { .v = 4.0f }, }); UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_HORIZONTAL, .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE }, .spacing = { .h = 10.0f }, }); UI_LabelFmt("#%02X%02X%02X", s->color.r, s->color.g, s->color.b); UI_BeginRowArrows(false, false, UI_ROW_ARROWS_MEDIUM); UI_ColorSwatch((UI_COLOR_SWATCH_SETTINGS) { .color = s->color, .w = 48.0f, .h = 12.0f, }); UI_EndRowArrows(); UI_EndStack(); M_ColorEditorRow(s, M_COLOR_ROW_HUE); M_ColorEditorRow(s, M_COLOR_ROW_CHROMA); M_ColorEditorRow(s, M_COLOR_ROW_LIGHTNESS); UI_EndStack(); UI_EndStack(); UI_EndPad(); UI_EndFrame(); UI_EndModal(); } ================================================ FILE: src/trx/game/ui/dialogs/color_editor.h ================================================ #pragma once #include typedef struct UI_COLOR_EDITOR_DIALOG_STATE UI_COLOR_EDITOR_DIALOG_STATE; UI_COLOR_EDITOR_DIALOG_STATE *UI_ColorEditorDialog_Init(void); void UI_ColorEditorDialog_Free(UI_COLOR_EDITOR_DIALOG_STATE *s); void UI_ColorEditorDialog_Open( UI_COLOR_EDITOR_DIALOG_STATE *s, const UI_SETTINGS_OPTION *option); void UI_ColorEditorDialog_Close(UI_COLOR_EDITOR_DIALOG_STATE *s); bool UI_ColorEditorDialog_IsOpen(const UI_COLOR_EDITOR_DIALOG_STATE *s); void UI_ColorEditorDialog_Control(UI_COLOR_EDITOR_DIALOG_STATE *s); void UI_ColorEditorDialog(UI_COLOR_EDITOR_DIALOG_STATE *s); ================================================ FILE: src/trx/game/ui/dialogs/config_presets.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_VISIBLE_ROWS 8 #define M_CONFIRM_VISIBLE_ROWS 10 #define M_CONFIRM_DIALOG_W 72.0f #define M_LIST_ROW_SPACING 5.0f typedef enum { M_PHASE_BROWSE, M_PHASE_CONFIRM, M_PHASE_NO_CHANGES, M_PHASE_APPLIED, } M_PHASE; struct UI_CONFIG_PRESETS_STATE { M_PHASE phase; UI_REQUESTER_STATE req; UI_SCROLLABLE confirm_scroll; int32_t selected_idx; }; static const char *M_GetPresetKeyLabel(const char *const key) { const CONFIG_OPTION *const opt = Config_GetOptionByPath(key); if (opt != nullptr) { const char *const label = Config_GetOptionTitle(opt); if (label != nullptr) { return label; } } return key; } static int32_t M_GetChangedSettingCount(const int32_t preset_idx) { const CONFIG_PRESET *const preset = Config_Presets_Get(preset_idx); if (preset == nullptr || preset->setting_count == 0) { return 0; } int32_t changed_count = 0; for (int32_t i = 0; i < preset->setting_count; i++) { const CONFIG_OPTION *const opt = Config_GetOptionByPath(preset->keys[i]); if (opt == nullptr) { continue; } const char *const current_value = Config_GetOptionValueAsString(opt, false); if (strcmp(current_value, preset->values[i]) != 0) { changed_count++; } } return changed_count; } static int32_t M_GetConfirmRowCount(const int32_t preset_idx) { return M_GetChangedSettingCount(preset_idx) * 2; } static void M_DrawConfirmRows(UI_CONFIG_PRESETS_STATE *const s) { const CONFIG_PRESET *const preset = Config_Presets_Get(s->selected_idx); if (preset == nullptr) { return; } for (int32_t i = 0; i < preset->setting_count; i++) { const CONFIG_OPTION *const opt = Config_GetOptionByPath(preset->keys[i]); if (opt == nullptr) { continue; } const char *const current_value_raw = Config_GetOptionValueAsString(opt, false); if (strcmp(current_value_raw, preset->values[i]) == 0) { continue; } const char *const current_value = Config_GetOptionValueAsString(opt, true); char *const target_value = Config_NormalizeOptionValueString(opt, preset->values[i], true); UI_LabelFmt("%s", M_GetPresetKeyLabel(preset->keys[i])); UI_LabelFmt(" %s \\{button right} %s", current_value, target_value); Memory_Free(target_value); } } static void M_Header(void *const user_data) { UI_CONFIG_PRESETS_STATE *const s = user_data; if (s->phase == M_PHASE_CONFIRM) { UI_Label(GS("general/config_presets/confirm_description")); UI_Spacer(0.0f, UI_TEXT_HEIGHT); } } static void M_Footer(void *const user_data) { UI_CONFIG_PRESETS_STATE *const s = user_data; if (s->phase == M_PHASE_CONFIRM) { UI_Spacer(0.0f, UI_TEXT_HEIGHT); UI_Label(GS("general/config_presets/confirm_restart_note")); } } UI_CONFIG_PRESETS_STATE *UI_ConfigPresets_Init(void) { UI_CONFIG_PRESETS_STATE *const s = Memory_Alloc(sizeof(UI_CONFIG_PRESETS_STATE)); s->phase = M_PHASE_BROWSE; s->selected_idx = -1; s->confirm_scroll = (UI_SCROLLABLE) { .first_item = 0, .sel_item = -1, .vis_items = M_CONFIRM_VISIBLE_ROWS, .max_items = 0, }; const int32_t count = Config_Presets_GetCount(); UI_Requester_Init(&s->req, M_VISIBLE_ROWS, count, true); UI_Requester_SelectRow(&s->req, -1); return s; } void UI_ConfigPresets_Free(UI_CONFIG_PRESETS_STATE *const s) { UI_Requester_Free(&s->req); Memory_Free(s); } int32_t UI_ConfigPresets_GetItemCount(UI_CONFIG_PRESETS_STATE *const s) { return Config_Presets_GetCount(); } void UI_ConfigPresets_RecomputeSizes( UI_CONFIG_PRESETS_STATE *const s, const int32_t visible_rows) { int32_t clamped_rows = visible_rows; if (clamped_rows < 0) { clamped_rows = 0; } else if (clamped_rows > M_VISIBLE_ROWS) { clamped_rows = M_VISIBLE_ROWS; } UI_Requester_SetVisibleRows(&s->req, clamped_rows); } float UI_ConfigPresets_GetContentWidth(UI_CONFIG_PRESETS_STATE *const s) { return -1.0f; } float UI_ConfigPresets_GetContentHeight(UI_CONFIG_PRESETS_STATE *const s) { if (s == nullptr) { return -1.0f; } int32_t rows = s->req.scroll.vis_items; if (rows <= 0) { return -1.0f; } return rows * UI_TEXT_HEIGHT + (rows - 1) * M_LIST_ROW_SPACING; } UI_SCROLLABLE *UI_ConfigPresets_GetScrollable(UI_CONFIG_PRESETS_STATE *const s) { return &s->req.scroll; } bool UI_ConfigPresets_Control(UI_CONFIG_PRESETS_STATE *const s) { if (s->phase == M_PHASE_APPLIED || s->phase == M_PHASE_NO_CHANGES) { if (g_InputDB.menu_confirm || g_InputDB.menu_back) { s->phase = M_PHASE_BROWSE; g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; } return false; } if (s->phase == M_PHASE_CONFIRM) { UI_ScrollableStack_Control(&s->confirm_scroll, UI_STACK_VERTICAL); if (g_InputDB.menu_confirm) { Config_Presets_Apply(s->selected_idx); s->phase = M_PHASE_APPLIED; g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; } else if (g_InputDB.menu_back) { s->phase = M_PHASE_BROWSE; g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; } return false; } const int32_t count = Config_Presets_GetCount(); UI_Requester_SetMaxRows(&s->req, count); if (count <= 0) { return g_InputDB.menu_back || g_InputDB.menu_up || (g_InputDB.menu_down && g_Config.ui.enable_wraparound); } if (g_InputDB.menu_up && UI_Requester_GetCurrentRow(&s->req) <= 0) { return true; } if (g_InputDB.menu_down && g_Config.ui.enable_wraparound && UI_Requester_GetCurrentRow(&s->req) >= count - 1) { return true; } const int32_t choice = UI_Requester_Control(&s->req); if (choice == UI_REQUESTER_CANCEL || g_InputDB.menu_back) { return true; } if (choice >= 0 && choice < count) { s->selected_idx = choice; const int32_t row_count = M_GetConfirmRowCount(choice); if (row_count > 0) { s->confirm_scroll.first_item = 0; s->confirm_scroll.sel_item = -1; s->confirm_scroll.max_items = row_count; s->phase = M_PHASE_CONFIRM; } else { s->phase = M_PHASE_NO_CHANGES; } g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; } return false; } void UI_ConfigPresets(UI_CONFIG_PRESETS_STATE *const s) { const int32_t count = Config_Presets_GetCount(); UI_BeginResize(-1.0f, -1.0f); UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, .align = { .h = UI_STACK_H_ALIGN_SPAN }, .spacing = { .v = M_LIST_ROW_SPACING }, }); const int32_t first_row = UI_Requester_GetFirstRow(&s->req); for (int32_t i = 0; i < s->req.scroll.vis_items; i++) { const int32_t row = first_row + i; if (row < count) { const CONFIG_PRESET *const preset = Config_Presets_Get(row); UI_BeginRequesterRow(&s->req, row); UI_BeginAnchor(0.5f, 0.5f); UI_Label(preset != nullptr ? GameString_Get(preset->name_gs) : ""); UI_EndAnchor(); UI_EndRequesterRow(&s->req, row); } else { UI_Spacer(0.0f, UI_TEXT_HEIGHT); } } if (count <= 0) { UI_BeginAnchor(0.5f, 0.5f); UI_Label(GS("general/config_presets/empty")); UI_EndAnchor(); } UI_EndStack(); UI_EndResize(); } void UI_ConfigPresetsApplyModal(UI_CONFIG_PRESETS_STATE *const s) { if (s->phase == M_PHASE_BROWSE) { return; } const CONFIG_PRESET *const preset = Config_Presets_Get(s->selected_idx); const char *const preset_name = preset != nullptr ? GameString_Get(preset->name_gs) : ""; const char *const title = String_FormatStatic( GS("general/config_presets/title_fmt"), preset_name); UI_BeginModal(0.5f, 0.5f); UI_BeginPad(6.0f, 6.0f); UI_BeginWindow((UI_WINDOW_SETTINGS) { .title = title, .scrollable = s->phase == M_PHASE_CONFIRM ? &s->confirm_scroll : nullptr, .title_spacing = -1.0f, .header_func = M_Header, .footer_func = M_Footer, .user_data = s, .heavy = true, .reserve_scroll_space = true, }); if (s->phase == M_PHASE_APPLIED) { UI_BeginPad(6.0f, 6.0f); UI_LabelFmt("%s", GS("general/config_presets/applied")); UI_EndPad(); } else if (s->phase == M_PHASE_NO_CHANGES) { UI_BeginPad(6.0f, 6.0f); UI_LabelFmt("%s", GS("general/config_presets/no_changes")); UI_EndPad(); } else if (s->phase == M_PHASE_CONFIRM) { UI_BeginResize(M_CONFIRM_DIALOG_W, -1.0f); UI_BeginScrollableStack( &s->confirm_scroll, (UI_SCROLLABLE_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, .spacing = 2.0f, }); M_DrawConfirmRows(s); UI_EndScrollableStack(); UI_EndResize(); } UI_EndWindow(); UI_EndPad(); UI_EndModal(); } ================================================ FILE: src/trx/game/ui/dialogs/config_presets.h ================================================ #pragma once // A UI dialog for browsing and applying config presets. // Shows a scrollable list of presets. Selecting one prompts the user // with a list of settings that will change, then applies them on confirm. #include typedef struct UI_CONFIG_PRESETS_STATE UI_CONFIG_PRESETS_STATE; // State functions UI_CONFIG_PRESETS_STATE *UI_ConfigPresets_Init(void); void UI_ConfigPresets_Free(UI_CONFIG_PRESETS_STATE *s); int32_t UI_ConfigPresets_GetItemCount(UI_CONFIG_PRESETS_STATE *s); void UI_ConfigPresets_RecomputeSizes( UI_CONFIG_PRESETS_STATE *s, int32_t visible_rows); float UI_ConfigPresets_GetContentWidth(UI_CONFIG_PRESETS_STATE *s); float UI_ConfigPresets_GetContentHeight(UI_CONFIG_PRESETS_STATE *s); // Handle input. Returns true when the user wants to exit the dialog. bool UI_ConfigPresets_Control(UI_CONFIG_PRESETS_STATE *s); UI_SCROLLABLE *UI_ConfigPresets_GetScrollable(UI_CONFIG_PRESETS_STATE *s); // Draw functions void UI_ConfigPresets(UI_CONFIG_PRESETS_STATE *s); void UI_ConfigPresetsApplyModal(UI_CONFIG_PRESETS_STATE *s); ================================================ FILE: src/trx/game/ui/dialogs/controls.c ================================================ #include #include #include #include #include #include #include typedef enum { M_PHASE_BACKEND, M_PHASE_EDITOR, } M_PHASE; void UI_Controls_Init(UI_CONTROLS_STATE *const s) { s->events = EventManager_Create(); s->phase = M_PHASE_BACKEND; s->backend = INPUT_BACKEND_KEYBOARD; s->active_layout = g_Config.input.layout[s->backend]; UI_ControlsBackend_Init(&s->backend_state); for (INPUT_BACKEND backend = 0; backend < INPUT_BACKEND_NUMBER_OF; backend++) { UI_ControlsEditor_Init( &s->editor_state[backend], backend, g_Config.input.layout[backend], s->events); } } void UI_Controls_Free(UI_CONTROLS_STATE *const s) { for (INPUT_BACKEND backend = 0; backend < INPUT_BACKEND_NUMBER_OF; backend++) { UI_ControlsEditor_Free(&s->editor_state[backend]); } UI_ControlsBackend_Free(&s->backend_state); EventManager_Free(s->events); s->events = nullptr; } bool UI_Controls_Control(UI_CONTROLS_STATE *const s) { switch (s->phase) { case M_PHASE_BACKEND: { const int32_t choice = UI_ControlsBackend_Control(&s->backend_state); switch (choice) { case UI_REQUESTER_NO_CHOICE: return false; case UI_REQUESTER_CANCEL: return true; case INPUT_BACKEND_KEYBOARD: case INPUT_BACKEND_CONTROLLER: s->backend = choice; s->phase = M_PHASE_EDITOR; g_Config.input.backend = s->backend; Config_Update(); break; } break; } case M_PHASE_EDITOR: { const UI_CONTROLS_CHOICE choice = UI_ControlsEditor_Control(&s->editor_state[s->backend]); switch (choice) { case UI_CONTROLS_CHOICE_NOOP: break; case UI_CONTROLS_CHOICE_GO_BACK: s->phase = M_PHASE_BACKEND; break; case UI_CONTROLS_CHOICE_EXIT: return true; } break; } } return false; } void UI_Controls(UI_CONTROLS_STATE *const s) { switch (s->phase) { case M_PHASE_BACKEND: UI_ControlsBackend(&s->backend_state); break; case M_PHASE_EDITOR: UI_ControlsEditor(&s->editor_state[s->backend]); break; } } ================================================ FILE: src/trx/game/ui/dialogs/controls.h ================================================ #pragma once // A controls editor dialog. #include #include #include #include #include typedef struct { int32_t phase; INPUT_BACKEND backend; int32_t active_layout; EVENT_MANAGER *events; UI_CONTROLS_BACKEND_STATE backend_state; UI_CONTROLS_EDITOR_STATE editor_state[INPUT_BACKEND_NUMBER_OF]; } UI_CONTROLS_STATE; // state functions void UI_Controls_Init(UI_CONTROLS_STATE *s); void UI_Controls_Free(UI_CONTROLS_STATE *s); bool UI_Controls_Control(UI_CONTROLS_STATE *s); // draw functions void UI_Controls(UI_CONTROLS_STATE *s); ================================================ FILE: src/trx/game/ui/dialogs/controls_backend.c ================================================ #include #include #include #include #include #include #include #include typedef struct { GAME_STRING_ID gs_id; INPUT_BACKEND backend; } M_OPTION; static const M_OPTION m_Options[] = { { .gs_id = GS_ID("general/settings/controls/backend/keyboard"), .backend = INPUT_BACKEND_KEYBOARD }, { .gs_id = GS_ID("general/settings/controls/backend/controller"), .backend = INPUT_BACKEND_CONTROLLER }, { .gs_id = nullptr }, }; void UI_ControlsBackend_Init(UI_CONTROLS_BACKEND_STATE *const s) { int32_t count = 0; int32_t sel_row = -1; for (int32_t i = 0; m_Options[i].gs_id != nullptr; i++) { if (m_Options[i].backend == g_Config.input.backend) { sel_row = i; } count++; } UI_Requester_Init(&s->req, count, count, true); if (sel_row != -1) { UI_Requester_SelectRow(&s->req, sel_row); } } void UI_ControlsBackend_Free(UI_CONTROLS_BACKEND_STATE *const s) { UI_Requester_Free(&s->req); } int32_t UI_ControlsBackend_Control(UI_CONTROLS_BACKEND_STATE *const s) { const int32_t choice = UI_Requester_Control(&s->req); if (choice >= 0) { return m_Options[choice].backend; } return choice; } void UI_ControlsBackend(UI_CONTROLS_BACKEND_STATE *const s) { UI_BeginModal(0.5f, 2.0f / 3.0f); UI_BeginRequester(&s->req, GS("general/settings/controls/customize")); for (int32_t i = UI_Requester_GetFirstRow(&s->req); i < UI_Requester_GetLastRow(&s->req); i++) { UI_BeginRequesterRow(&s->req, i); UI_BeginAnchor(0.5f, 0.5f); UI_Label(GameString_Get(m_Options[i].gs_id)); UI_EndAnchor(); UI_EndRequesterRow(&s->req, i); } UI_EndRequester(&s->req); UI_EndModal(); } ================================================ FILE: src/trx/game/ui/dialogs/controls_backend.h ================================================ #pragma once // A control backend (keyboard/controller) choice dialog. #include #include typedef struct { UI_REQUESTER_STATE req; } UI_CONTROLS_BACKEND_STATE; // state functions void UI_ControlsBackend_Init(UI_CONTROLS_BACKEND_STATE *s); void UI_ControlsBackend_Free(UI_CONTROLS_BACKEND_STATE *s); int32_t UI_ControlsBackend_Control(UI_CONTROLS_BACKEND_STATE *s); // draw functions void UI_ControlsBackend(UI_CONTROLS_BACKEND_STATE *s); ================================================ FILE: src/trx/game/ui/dialogs/controls_editor.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef enum { M_PHASE_NAVIGATE_LAYOUT, M_PHASE_NAVIGATE_GROUP, M_PHASE_NAVIGATE_INPUTS, M_PHASE_NAVIGATE_INPUTS_DEBOUNCE, M_PHASE_LISTEN, M_PHASE_LISTEN_DEBOUNCE, M_PHASE_EXIT, } M_PHASE; static const UI_CONTROLS_EDITOR_GROUP m_Groups[] = { { .header_gs = GS_ID("general/settings/controls/tabs/basics"), .rows = (UI_CONTROLS_EDITOR_ROW[]) { { .role = INPUT_ROLE_UP }, { .role = INPUT_ROLE_DOWN }, { .role = INPUT_ROLE_LEFT }, { .role = INPUT_ROLE_RIGHT }, { .role = INPUT_ROLE_JUMP }, { .role = INPUT_ROLE_STEP_LEFT }, { .role = INPUT_ROLE_STEP_RIGHT }, { .role = INPUT_ROLE_ROLL }, { .role = INPUT_ROLE_SLOW }, { .role = INPUT_ROLE_SPRINT }, { .role = INPUT_ROLE_CROUCH }, { .role = INPUT_ROLE_ACTION }, { .role = INPUT_ROLE_DRAW_WEAPON }, { .role = INPUT_ROLE_LOOK }, { .role = (INPUT_ROLE)-1 }, }, }, { .header_gs = GS_ID("general/settings/controls/tabs/items"), .rows = (UI_CONTROLS_EDITOR_ROW[]) { { .role = INPUT_ROLE_USE_FLARE }, { .role = INPUT_ROLE_USE_SMALL_MEDI }, { .role = INPUT_ROLE_USE_BIG_MEDI }, { .role = INPUT_ROLE_EQUIP_PISTOLS }, { .role = INPUT_ROLE_EQUIP_SHOTGUN }, { .role = INPUT_ROLE_EQUIP_MAGNUMS }, { .role = INPUT_ROLE_EQUIP_AUTOS }, { .role = INPUT_ROLE_EQUIP_DESERT_EAGLE }, { .role = INPUT_ROLE_EQUIP_UZIS }, { .role = INPUT_ROLE_EQUIP_HARPOON }, { .role = INPUT_ROLE_EQUIP_M16 }, { .role = INPUT_ROLE_EQUIP_MP5 }, { .role = INPUT_ROLE_EQUIP_ROCKET_LAUNCHER }, { .role = INPUT_ROLE_EQUIP_GRENADE_LAUNCHER }, { .role = (INPUT_ROLE)-1 }, }, }, { .header_gs = GS_ID("general/settings/controls/tabs/misc"), .rows = (UI_CONTROLS_EDITOR_ROW[]) { { .role = INPUT_ROLE_CHANGE_TARGET }, { .role = INPUT_ROLE_CAMERA_UP }, { .role = INPUT_ROLE_CAMERA_DOWN }, { .role = INPUT_ROLE_CAMERA_LEFT }, { .role = INPUT_ROLE_CAMERA_RIGHT }, { .role = INPUT_ROLE_CAMERA_FORWARD }, { .role = INPUT_ROLE_CAMERA_BACK }, { .role = INPUT_ROLE_CHANGE_OUTFIT }, { .role = INPUT_ROLE_FLY_CHEAT }, { .role = INPUT_ROLE_ITEM_CHEAT }, { .role = INPUT_ROLE_LEVEL_SKIP_CHEAT }, { .role = INPUT_ROLE_TURBO_CHEAT }, { .role = (INPUT_ROLE)-1 }, }, }, { .header_gs = GS_ID("general/settings/controls/tabs/system"), .rows = (UI_CONTROLS_EDITOR_ROW[]) { { .role = INPUT_ROLE_INVENTORY }, { .role = INPUT_ROLE_SAVE }, { .role = INPUT_ROLE_LOAD }, { .role = INPUT_ROLE_QUICK_SAVE }, { .role = INPUT_ROLE_QUICK_LOAD }, { .role = INPUT_ROLE_PAUSE }, // { .role = INPUT_ROLE_SCREENSHOT }, // handled specially { .role = INPUT_ROLE_FPS }, // { .role = INPUT_ROLE_TOGGLE_FULLSCREEN }, // handled // specially { .role = INPUT_ROLE_ENTER_CONSOLE }, { .role = INPUT_ROLE_TOGGLE_PHOTO_MODE }, { .role = INPUT_ROLE_TOGGLE_UI }, { .role = INPUT_ROLE_TOGGLE_BILINEAR_FILTER }, { .role = INPUT_ROLE_TOGGLE_TRAPEZOID_FILTER }, { .role = INPUT_ROLE_SWITCH_UPSCALING }, { .role = INPUT_ROLE_SWITCH_BORDERS }, { .role = INPUT_ROLE_TOGGLE_WIREFRAME }, { .role = INPUT_ROLE_TOGGLE_TEXTURES }, { .role = INPUT_ROLE_CYCLE_LIGHTING_CONTRAST }, { .role = (INPUT_ROLE)-1 }, }, }, { .header_gs = nullptr, .rows = nullptr, }, }; static int32_t M_GetVisibleRows(void) { const int32_t res_h = UI_Scaler_CalcInverse( Viewport_GetHeight(VIEWPORT_UI), UI_SCALER_TARGET_TEXT); if (res_h <= 240) { return 5; } else if (res_h <= 252) { return 6; } else if (res_h <= 266) { return 7; } else if (res_h <= 282) { return 8; } else if (res_h <= 300) { return 9; } else if (res_h <= 320) { return 10; } else if (res_h <= 342) { return 11; } else if (res_h <= 370) { return 12; } else if (res_h <= 420) { return 13; } else if (res_h <= 480) { return 15; } else { return 16; } } static bool M_IsRoleUsable(const INPUT_ROLE role) { switch (role) { case INPUT_ROLE_USE_FLARE: return g_Weapons[LGT_FLARE].is_available; case INPUT_ROLE_EQUIP_MAGNUMS: return g_Weapons[LGT_MAGNUMS].is_available; case INPUT_ROLE_EQUIP_AUTOS: return g_Weapons[LGT_AUTOS].is_available; case INPUT_ROLE_EQUIP_DESERT_EAGLE: return g_Weapons[LGT_DESERT_EAGLE].is_available; case INPUT_ROLE_EQUIP_HARPOON: return g_Weapons[LGT_HARPOON].is_available; case INPUT_ROLE_EQUIP_M16: return g_Weapons[LGT_M16].is_available; case INPUT_ROLE_EQUIP_MP5: return g_Weapons[LGT_MP5].is_available; case INPUT_ROLE_EQUIP_GRENADE_LAUNCHER: return g_Weapons[LGT_GRENADE].is_available; case INPUT_ROLE_EQUIP_ROCKET_LAUNCHER: return g_Weapons[LGT_ROCKET].is_available; case INPUT_ROLE_FLY_CHEAT: case INPUT_ROLE_ITEM_CHEAT: case INPUT_ROLE_LEVEL_SKIP_CHEAT: case INPUT_ROLE_TURBO_CHEAT: return g_Config.gameplay.enable_cheats; default: break; } return true; } static int32_t M_GetInputRoleCount(const UI_CONTROLS_EDITOR_GROUP *const group) { int32_t count = 0; for (int32_t i = 0; group->rows[i].role != (INPUT_ROLE)-1; i++) { count++; } return count; } static void M_ResetLayout(void *const arg) { const UI_CONTROLS_EDITOR_STATE *const s = arg; Sound_Effect( g_TRVersion == 1 ? SFX_MENU_GAMEBOY : SFX_MENU_SPINOUT, nullptr, SPM_NORMAL); Input_ResetLayout(s->backend, s->active_layout); g_Config.dirty = true; Config_Update(); } static void M_UnbindKey(void *const arg) { const UI_CONTROLS_EDITOR_STATE *const s = arg; Sound_Effect( g_TRVersion == 1 ? SFX_MENU_GAMEBOY : SFX_MENU_SPINOUT, nullptr, SPM_NORMAL); Input_UnassignRole( s->backend, s->active_layout, s->active_role, s->active_slot); g_Config.dirty = true; Config_Update(); } static bool M_CanResetLayout(const UI_CONTROLS_EDITOR_STATE *const s) { return !Input_IsInListenMode() && s->phase != M_PHASE_NAVIGATE_INPUTS_DEBOUNCE && s->active_layout != INPUT_LAYOUT_DEFAULT; } static bool M_CanUnbindKey(const UI_CONTROLS_EDITOR_STATE *const s) { return !Input_IsInListenMode() && (s->phase == M_PHASE_NAVIGATE_INPUTS || s->phase == M_PHASE_LISTEN_DEBOUNCE) && s->active_layout != INPUT_LAYOUT_DEFAULT && s->active_role != (INPUT_ROLE)-1 && Input_IsRoleUnbindable(s->active_role); } static void M_CheckResetKeys(UI_CONTROLS_EDITOR_STATE *const s) { if (M_CanResetLayout(s)) { UI_ProgressButton_Control(s->reset_bindings_button); } if (M_CanUnbindKey(s)) { UI_ProgressButton_Control(s->unbind_key_button); } } static UI_CONTROLS_CHOICE M_NavigateLayout(UI_CONTROLS_EDITOR_STATE *const s) { M_CheckResetKeys(s); if (g_InputDB.menu_confirm) { return UI_CONTROLS_CHOICE_EXIT; } else if (g_InputDB.menu_back) { return UI_CONTROLS_CHOICE_GO_BACK; } else if ( UI_TabSwitch_Control(s->layout_tab_switch, UI_TAB_SWITCH_NORMAL)) { s->active_layout = s->layout_tab_switch->active_tab_idx; const EVENT event = { .name = "layout_change", .sender = nullptr, .data = nullptr, }; EventManager_Fire(s->events, &event); } else if (g_InputDB.menu_down) { s->phase = M_PHASE_NAVIGATE_GROUP; } else if ( g_InputDB.menu_up && s->active_layout != 0 && g_Config.ui.enable_wraparound) { s->phase = M_PHASE_NAVIGATE_INPUTS; UI_Scrollable_SelectLastItem(&s->scroll); s->active_role = s->active_group->rows[s->scroll.sel_item].role; } else { return UI_CONTROLS_CHOICE_NOOP; } s->active_role = s->active_group->rows[s->scroll.sel_item].role; return UI_CONTROLS_CHOICE_NOOP; } static UI_CONTROLS_CHOICE M_NavigateGroup(UI_CONTROLS_EDITOR_STATE *const s) { M_CheckResetKeys(s); if (g_InputDB.menu_confirm) { return UI_CONTROLS_CHOICE_EXIT; } else if (g_InputDB.menu_back) { return UI_CONTROLS_CHOICE_GO_BACK; } else if ( UI_TabSwitch_Control(s->controls_tab_switch, UI_TAB_SWITCH_NORMAL)) { s->active_group = &m_Groups[s->controls_tab_switch->active_tab_idx]; UI_Scrollable_SetMaxItems( &s->scroll, M_GetInputRoleCount(s->active_group)); } else if (g_InputDB.menu_down && s->active_layout != 0) { s->phase = M_PHASE_NAVIGATE_INPUTS; UI_Scrollable_SelectFirstItem(&s->scroll); s->active_role = s->active_group->rows[s->scroll.sel_item].role; } else if (g_InputDB.menu_up) { s->phase = M_PHASE_NAVIGATE_LAYOUT; } s->active_role = s->active_group->rows[s->scroll.sel_item].role; return UI_CONTROLS_CHOICE_NOOP; } static UI_CONTROLS_CHOICE M_NavigateInputs(UI_CONTROLS_EDITOR_STATE *const s) { M_CheckResetKeys(s); if (g_InputDB.menu_confirm) { s->phase = M_PHASE_NAVIGATE_INPUTS_DEBOUNCE; } else if (g_InputDB.menu_back) { return UI_CONTROLS_CHOICE_GO_BACK; } else if ( UI_TabSwitch_Control(s->controls_tab_switch, UI_TAB_SWITCH_NO_ARROWS)) { s->active_group = &m_Groups[s->controls_tab_switch->active_tab_idx]; UI_Scrollable_SetMaxItems( &s->scroll, M_GetInputRoleCount(s->active_group)); } else if (g_InputDB.menu_left) { s->active_slot = (s->active_slot - 1 + INPUT_BINDING_SLOTS) % INPUT_BINDING_SLOTS; } else if (g_InputDB.menu_right) { s->active_slot = (s->active_slot + 1) % INPUT_BINDING_SLOTS; } else if (g_InputDB.menu_up) { if (!UI_Scrollable_SelectPrev(&s->scroll, false)) { s->phase = M_PHASE_NAVIGATE_GROUP; } } else if (g_InputDB.menu_down) { if (!UI_Scrollable_SelectNext(&s->scroll, false) && g_Config.ui.enable_wraparound) { s->phase = M_PHASE_NAVIGATE_LAYOUT; } } else { return UI_CONTROLS_CHOICE_NOOP; } s->active_role = s->active_group->rows[s->scroll.sel_item].role; return UI_CONTROLS_CHOICE_NOOP; } static UI_CONTROLS_CHOICE M_NavigateInputsDebounce( UI_CONTROLS_EDITOR_STATE *const s) { Input_Update(); if (InputState_IsAnyPressed(g_Input)) { return UI_CONTROLS_CHOICE_NOOP; } Input_EnterListenMode(); s->phase = M_PHASE_LISTEN; return UI_CONTROLS_CHOICE_NOOP; } static UI_CONTROLS_CHOICE M_Listen(UI_CONTROLS_EDITOR_STATE *const s) { if (!Input_ReadAndAssignRole( s->backend, s->active_layout, s->active_role, s->active_slot)) { return UI_CONTROLS_CHOICE_NOOP; } Input_ExitListenMode(); const EVENT event = { .name = "key_change", .sender = nullptr, .data = nullptr, }; EventManager_Fire(s->events, &event); s->phase = M_PHASE_LISTEN_DEBOUNCE; return UI_CONTROLS_CHOICE_NOOP; } static UI_CONTROLS_CHOICE M_ListenDebounce(UI_CONTROLS_EDITOR_STATE *const s) { if (!InputState_IsAnyPressed(g_Input)) { s->phase = M_PHASE_NAVIGATE_INPUTS; } return UI_CONTROLS_CHOICE_NOOP; } static void M_CurrentLayout(const UI_CONTROLS_EDITOR_STATE *const s) { UI_TabSwitchSingle( s->layout_tab_switch, s->phase == M_PHASE_NAVIGATE_LAYOUT); } static void M_GroupsHeader(const UI_CONTROLS_EDITOR_STATE *const s) { UI_TabSwitch(s->controls_tab_switch, s->phase == M_PHASE_NAVIGATE_GROUP); } static void M_InputLabel( const UI_CONTROLS_EDITOR_STATE *const s, const UI_CONTROLS_EDITOR_ROW *const row) { const bool is_selected = s->active_role == row->role && (s->phase == M_PHASE_NAVIGATE_INPUTS || s->phase == M_PHASE_LISTEN_DEBOUNCE); if (is_selected) { UI_BeginFrame(UI_FRAME_SELECTED_OPTION); } const char *const role_name = Input_GetRoleName(row->role); if (!M_IsRoleUsable(row->role)) { UI_LabelFmt("\\{dim}%s\\{/dim}", role_name); } else { UI_Label(role_name); } if (is_selected) { UI_EndFrame(); } } static void M_InputSlot( UI_CONTROLS_EDITOR_STATE *const s, const UI_CONTROLS_EDITOR_ROW *const row, const int32_t slot) { const bool is_flashing = Input_IsKeyConflicted(s->backend, s->active_layout, row->role); const bool is_active_row = s->active_role == row->role; const bool is_listening = is_active_row && s->active_slot == slot && (s->phase == M_PHASE_LISTEN || s->phase == M_PHASE_NAVIGATE_INPUTS_DEBOUNCE); const bool is_slot_selected = is_active_row && s->active_slot == slot && (s->phase == M_PHASE_NAVIGATE_INPUTS || s->phase == M_PHASE_LISTEN_DEBOUNCE); if (is_flashing) { UI_BeginFlash(&s->flash); } if (is_listening || is_slot_selected) { UI_BeginFrame(UI_FRAME_SELECTED_OPTION); } const char *key_name = Input_GetKeyName(s->backend, s->active_layout, row->role, slot); if (key_name == nullptr) { if (s->active_layout != INPUT_LAYOUT_DEFAULT) { UI_Label("—"); } else { UI_Label(""); } } else if (!M_IsRoleUsable(row->role)) { UI_LabelFmt("\\{dim}%s\\{/dim}", key_name); } else { UI_Label(key_name); } if (is_listening || is_slot_selected) { UI_EndFrame(); } if (is_flashing) { UI_EndFlash(); } } static void M_Group( UI_CONTROLS_EDITOR_STATE *const s, const UI_CONTROLS_EDITOR_GROUP *const group) { UI_BeginStack(UI_STACK_VERTICAL); for (int32_t i = 0; i < s->scroll.vis_items; i++) { const int32_t row_idx = s->scroll.first_item + i; if (row_idx >= s->scroll.max_items) { UI_Spacer(0.0f, UI_TEXT_HEIGHT); continue; } const UI_CONTROLS_EDITOR_ROW *const row = &group->rows[row_idx]; UI_BeginStack(UI_STACK_HORIZONTAL); for (int32_t slot = 0; slot < INPUT_BINDING_SLOTS; slot++) { UI_BeginResize(s->input_size, -1.0f); UI_BeginAnchor(0.0f, 0.5f); M_InputSlot(s, row, slot); UI_EndAnchor(); UI_EndResize(); } UI_BeginResize(s->label_size, -1.0f); UI_BeginAnchor(0.0f, 0.5f); M_InputLabel(s, row); UI_EndAnchor(); UI_EndResize(); UI_EndStack(); } UI_EndStack(); } static void M_Footer(UI_CONTROLS_EDITOR_STATE *const s) { UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_HORIZONTAL, .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE }, .spacing = { .h = 40.0f }, }); UI_BeginHide(!M_CanResetLayout(s)); UI_ProgressButton(s->reset_bindings_button); UI_EndHide(); UI_BeginHide(!M_CanUnbindKey(s)); UI_ProgressButton(s->unbind_key_button); UI_EndHide(); UI_EndStack(); } void UI_ControlsEditor_Init( UI_CONTROLS_EDITOR_STATE *const s, const INPUT_BACKEND backend, const int32_t layout, EVENT_MANAGER *const events) { s->backend = backend; s->active_layout = layout; s->active_slot = 0; s->phase = M_PHASE_NAVIGATE_LAYOUT; s->events = events; UI_Flash_Init(&s->flash, LOGIC_FPS * 2 / 3); s->reset_bindings_button = UI_ProgressButton_Init( s->backend, INPUT_ROLE_RESET_BINDINGS, GS_ID("general/actions/reset_defaults"), M_ResetLayout, s); s->unbind_key_button = UI_ProgressButton_Init( s->backend, INPUT_ROLE_UNBIND_KEY, GS_ID("general/actions/unbind"), M_UnbindKey, s); { UI_TAB_SWITCH_TAB layout_tabs[INPUT_LAYOUT_NUMBER_OF]; for (INPUT_LAYOUT i = 0; i < INPUT_LAYOUT_NUMBER_OF; i++) { layout_tabs[i].header.one_off = nullptr; layout_tabs[i].header.live_ptr = Input_GetLayoutNamePtr(i); } s->layout_tab_switch = UI_TabSwitch_Init(INPUT_LAYOUT_NUMBER_OF, layout_tabs); s->layout_tab_switch->active_tab_idx = s->active_layout; } { int32_t tab_count = 0; for (int32_t i = 0; m_Groups[i].rows != nullptr; i++) { tab_count++; } UI_TAB_SWITCH_TAB controls_tabs[tab_count]; for (int32_t i = 0; m_Groups[i].rows != nullptr; i++) { controls_tabs[i].header.one_off = nullptr; controls_tabs[i].header.live_ptr = GameString_GetPtr(m_Groups[i].header_gs); } s->controls_tab_switch = UI_TabSwitch_Init(tab_count, controls_tabs); } s->max_group_items = 0; for (const UI_CONTROLS_EDITOR_GROUP *group = m_Groups; group->rows != nullptr; group++) { s->max_group_items = MAX(s->max_group_items, M_GetInputRoleCount(group)); } s->active_group = &m_Groups[0]; s->scroll.first_item = 0; s->scroll.sel_item = 0; s->scroll.vis_items = MIN(s->max_group_items, M_GetVisibleRows()); s->scroll.max_items = M_GetInputRoleCount(s->active_group); s->active_role = s->active_group->rows[s->scroll.sel_item].role; s->label_size = 0.0f; for (int32_t i = 0; i < INPUT_ROLE_NUMBER_OF; i++) { float w; UI_Label_Measure(Input_GetRoleName(i), &w, nullptr); s->label_size = MAX(s->label_size, w / g_Config.ui.text_scale); } s->input_size = 80; } void UI_ControlsEditor_Free(UI_CONTROLS_EDITOR_STATE *const s) { UI_Flash_Free(&s->flash); UI_TabSwitch_Free(s->layout_tab_switch); s->layout_tab_switch = nullptr; UI_TabSwitch_Free(s->controls_tab_switch); s->controls_tab_switch = nullptr; UI_ProgressButton_Free(s->reset_bindings_button); s->reset_bindings_button = nullptr; UI_ProgressButton_Free(s->unbind_key_button); s->unbind_key_button = nullptr; } UI_CONTROLS_CHOICE UI_ControlsEditor_Control(UI_CONTROLS_EDITOR_STATE *const s) { UI_Flash_Control(&s->flash); switch (s->phase) { case M_PHASE_NAVIGATE_LAYOUT: return M_NavigateLayout(s); case M_PHASE_NAVIGATE_GROUP: return M_NavigateGroup(s); case M_PHASE_NAVIGATE_INPUTS: return M_NavigateInputs(s); case M_PHASE_NAVIGATE_INPUTS_DEBOUNCE: return M_NavigateInputsDebounce(s); case M_PHASE_LISTEN: return M_Listen(s); case M_PHASE_LISTEN_DEBOUNCE: return M_ListenDebounce(s); default: return UI_CONTROLS_CHOICE_NOOP; } } static void M_Header(void *const user_data) { UI_CONTROLS_EDITOR_STATE *const s = user_data; UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, .align = { .h = UI_STACK_H_ALIGN_SPAN }, .spacing = { .v = 4.0f }, }); M_CurrentLayout(s); M_GroupsHeader(s); UI_EndStack(); UI_Spacer(0.0f, 5.0f); } void UI_ControlsEditor(UI_CONTROLS_EDITOR_STATE *const s) { UI_BeginModal(0.5f, 0.55f); UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, .align = { .h = UI_STACK_H_ALIGN_SPAN }, }); UI_BeginWindow((UI_WINDOW_SETTINGS) { .title = GS("general/settings/controls/customize"), .scrollable = nullptr, .title_spacing = -1.0f, .header_func = M_Header, .user_data = s, }); UI_BeginStack(UI_STACK_HORIZONTAL); M_Group(s, s->active_group); UI_EndStack(); UI_EndWindow(); UI_Spacer(0.0f, 5.0f); M_Footer(s); UI_EndStack(); UI_EndModal(); } ================================================ FILE: src/trx/game/ui/dialogs/controls_editor.h ================================================ #pragma once // A controls remapper dialog. #include #include #include #include #include #include #include #include #include typedef struct { INPUT_ROLE role; } UI_CONTROLS_EDITOR_ROW; typedef struct { GAME_STRING_ID header_gs; UI_CONTROLS_EDITOR_ROW *rows; } UI_CONTROLS_EDITOR_GROUP; typedef struct { int32_t phase; INPUT_BACKEND backend; INPUT_LAYOUT active_layout; INPUT_ROLE active_role; int32_t active_slot; const UI_CONTROLS_EDITOR_GROUP *active_group; UI_FLASH_STATE flash; EVENT_MANAGER *events; UI_SCROLLABLE scroll; int32_t max_group_items; int32_t input_size; int32_t label_size; UI_TAB_SWITCH_STATE *layout_tab_switch; UI_TAB_SWITCH_STATE *controls_tab_switch; UI_PROGRESS_BUTTON_STATE *reset_bindings_button; UI_PROGRESS_BUTTON_STATE *unbind_key_button; } UI_CONTROLS_EDITOR_STATE; typedef enum { UI_CONTROLS_CHOICE_EXIT, UI_CONTROLS_CHOICE_GO_BACK, UI_CONTROLS_CHOICE_NOOP, } UI_CONTROLS_CHOICE; // state functions void UI_ControlsEditor_Init( UI_CONTROLS_EDITOR_STATE *s, INPUT_BACKEND backend, int32_t layout, EVENT_MANAGER *events); void UI_ControlsEditor_Free(UI_CONTROLS_EDITOR_STATE *s); UI_CONTROLS_CHOICE UI_ControlsEditor_Control(UI_CONTROLS_EDITOR_STATE *s); // draw functions void UI_ControlsEditor(UI_CONTROLS_EDITOR_STATE *s); ================================================ FILE: src/trx/game/ui/dialogs/gameplay_settings.c ================================================ #include #include #include #include #include #include static const UI_SETTINGS_OPTION m_GeneralOptions[] = { #include { .target = nullptr }, }; static const UI_SETTINGS_OPTION m_ControlOptions[] = { #include { .target = nullptr }, }; static const UI_SETTINGS_OPTION m_GameplayModOptions[] = { #include { .target = nullptr }, }; static const UI_SETTINGS_OPTION m_GameplayFixOptions[] = { #include { .target = nullptr }, }; UI_SETTINGS_DIALOG_STATE *UI_GameplaySettings_Init(void) { const UI_SETTINGS_TAB tabs[] = { UI_SettingsTab_MakeEditor( GS_ID("general/settings/gameplay/tabs/general"), m_GeneralOptions), UI_SettingsTab_MakeEditor( GS_ID("general/settings/gameplay/tabs/controls"), m_ControlOptions), UI_SettingsTab_MakeEditor( GS_ID("general/settings/gameplay/tabs/mods"), m_GameplayModOptions), UI_SettingsTab_MakeEditor( GS_ID("general/settings/gameplay/tabs/fixes"), m_GameplayFixOptions), UI_SettingsTab_MakePresets( GS_ID("general/settings/gameplay/tabs/presets")), }; return UI_SettingsDialog_Init( GS_ID("general/settings/gameplay/title"), ARRAY_SIZE(tabs), tabs); } void UI_GameplaySettings_Free(UI_SETTINGS_DIALOG_STATE *const s) { UI_SettingsDialog_Free(s); } bool UI_GameplaySettings_Control(UI_SETTINGS_DIALOG_STATE *const s) { return UI_SettingsDialog_Control(s); } void UI_GameplaySettings(UI_SETTINGS_DIALOG_STATE *const s) { UI_SettingsDialog(s); } ================================================ FILE: src/trx/game/ui/dialogs/gameplay_settings.h ================================================ #pragma once #include #include UI_SETTINGS_DIALOG_STATE *UI_GameplaySettings_Init(void); void UI_GameplaySettings_Free(UI_SETTINGS_DIALOG_STATE *s); bool UI_GameplaySettings_Control(UI_SETTINGS_DIALOG_STATE *s); void UI_GameplaySettings(UI_SETTINGS_DIALOG_STATE *s); ================================================ FILE: src/trx/game/ui/dialogs/graphic_settings.c ================================================ #include #include #include #include #include static const UI_SETTINGS_OPTION m_VisualsOptions[] = { #include { .target = nullptr }, }; static UI_SETTINGS_OPTION m_UIOptions[] = { #include { .target = nullptr }, }; static const UI_SETTINGS_OPTION m_UIStatsOptions[] = { #include { .target = nullptr }, }; static const UI_SETTINGS_OPTION m_UIBarsOptions[] = { #include { .target = nullptr }, }; static const UI_SETTINGS_OPTION m_RenderOptions[] = { #include { .target = nullptr }, }; UI_SETTINGS_DIALOG_STATE *UI_GraphicSettings_Init(void) { const UI_SETTINGS_TAB tabs[] = { UI_SettingsTab_MakeEditor( GS_ID("general/settings/graphic_settings/tabs/visuals"), m_VisualsOptions), UI_SettingsTab_MakeEditor( GS_ID("general/settings/graphic_settings/tabs/ui"), m_UIOptions), UI_SettingsTab_MakeEditor( GS_ID("general/settings/graphic_settings/tabs/stats"), m_UIStatsOptions), UI_SettingsTab_MakeEditor( GS_ID("general/settings/graphic_settings/tabs/bars"), m_UIBarsOptions), UI_SettingsTab_MakeEditor( GS_ID("general/settings/graphic_settings/tabs/rendering"), m_RenderOptions), }; return UI_SettingsDialog_Init( GS_ID("general/settings/graphic_settings/title"), ARRAY_SIZE(tabs), tabs); } void UI_GraphicSettings_Free(UI_SETTINGS_DIALOG_STATE *const s) { UI_SettingsDialog_Free(s); } bool UI_GraphicSettings_Control(UI_SETTINGS_DIALOG_STATE *const s) { return UI_SettingsDialog_Control(s); } void UI_GraphicSettings(UI_SETTINGS_DIALOG_STATE *const s) { UI_SettingsDialog(s); } ================================================ FILE: src/trx/game/ui/dialogs/graphic_settings.h ================================================ #pragma once #include #include UI_SETTINGS_DIALOG_STATE *UI_GraphicSettings_Init(void); void UI_GraphicSettings_Free(UI_SETTINGS_DIALOG_STATE *s); bool UI_GraphicSettings_Control(UI_SETTINGS_DIALOG_STATE *s); void UI_GraphicSettings(UI_SETTINGS_DIALOG_STATE *s); ================================================ FILE: src/trx/game/ui/dialogs/new_game.c ================================================ #include #include #include #include #include #include #include #include #include #include typedef struct { bool play_prev_levels; bool story_so_far; bool switch_mod; } M_FEATURES; typedef struct { GAME_STRING_ID label_id; UI_NEW_GAME_CHOICE choice; } M_OPTION; typedef struct UI_NEW_GAME_STATE { VECTOR *options; UI_REQUESTER_STATE req; } UI_NEW_GAME_STATE; static const M_OPTION m_Options[] = { { .label_id = GS_ID("general/passport/mode_new_game"), .choice = UI_NEW_GAME_CHOICE_NG, }, { .label_id = GS_ID("general/passport/mode_new_game_plus"), .choice = UI_NEW_GAME_CHOICE_NGPLUS, }, { .label_id = GS_ID("general/passport/mode_new_game_jp"), .choice = UI_NEW_GAME_CHOICE_JP_NG, }, { .label_id = GS_ID("general/passport/mode_new_game_jp_plus"), .choice = UI_NEW_GAME_CHOICE_JP_NGPLUS, }, { .label_id = GS_ID("general/passport/play_previous_levels"), .choice = UI_NEW_GAME_CHOICE_PLAY_PREV_LEVELS, }, { .label_id = GS_ID("general/passport/story_so_far"), .choice = UI_NEW_GAME_CHOICE_STORY_SO_FAR, }, { .label_id = GS_ID("general/passport/switch_mod"), .choice = UI_NEW_GAME_CHOICE_SWITCH_MOD, }, { .label_id = nullptr, .choice = (UI_NEW_GAME_CHOICE)-1 }, }; static bool M_HasSwitchModChoice(void) { int32_t count = 0; for (int32_t i = 0; i < Shell_GetModCount(); i++) { const SHELL_MOD *const mod = Shell_GetMod(i); if (Shell_CanSwitchToMod(mod)) { count++; } } return count > 1; } static M_FEATURES M_CheckFeatures(const bool check_save_features) { M_FEATURES features = { .switch_mod = M_HasSwitchModChoice(), }; if (g_Config.flow.load_save_disabled || !check_save_features) { return features; } for (SAVEGAME_SLOT_POOL pool = 0; pool < SAVEGAME_SLOT_POOL_NUMBER_OF; pool++) { for (int32_t slot_num = 0; slot_num < Savegame_GetSlotCount(pool); slot_num++) { const SAVEGAME_SLOT_REF slot = { .pool = pool, .index = slot_num }; if (Savegame_IsSlotFree(slot)) { continue; } if (!features.play_prev_levels) { const SAVEGAME_INFO *const info = Savegame_GetSavegameInfo(slot); if (info->features.select_level) { features.play_prev_levels = true; } } if (!features.story_so_far && GF_HasAvailableStory(slot)) { features.story_so_far = true; } } } return features; } bool UI_NewGame_HasModChoices(void) { return M_HasSwitchModChoice(); } static bool M_OptionVisible( const M_FEATURES *const features, const M_OPTION *const option) { if (option->choice == UI_NEW_GAME_CHOICE_STORY_SO_FAR) { return features->story_so_far; } if (option->choice == UI_NEW_GAME_CHOICE_PLAY_PREV_LEVELS) { return features->play_prev_levels; } if (option->choice == UI_NEW_GAME_CHOICE_SWITCH_MOD) { return features->switch_mod; } return g_Config.gameplay.enable_game_modes || option->choice == UI_NEW_GAME_CHOICE_NG; } UI_NEW_GAME_STATE *UI_NewGame_Init(const bool show_play_prev_levels) { UI_NEW_GAME_STATE *const s = Memory_Alloc(sizeof(UI_NEW_GAME_STATE)); s->options = Vector_Create(sizeof(M_OPTION)); const M_FEATURES features = show_play_prev_levels ? M_CheckFeatures(g_Config.gameplay.enable_play_previous_levels) : (M_FEATURES) {}; for (int32_t i = 0; m_Options[i].label_id != nullptr; i++) { if (M_OptionVisible(&features, &m_Options[i])) { Vector_Add(s->options, &m_Options[i]); } } UI_Requester_Init(&s->req, s->options->count, s->options->count, true); return s; } void UI_NewGame_Free(UI_NEW_GAME_STATE *const s) { Vector_Free(s->options); UI_Requester_Free(&s->req); Memory_Free(s); } int32_t UI_NewGame_Control(UI_NEW_GAME_STATE *const s) { const int32_t choice = UI_Requester_Control(&s->req); if (choice == UI_REQUESTER_CANCEL || choice == UI_REQUESTER_NO_CHOICE) { return choice; } const M_OPTION *const opt = Vector_Get(s->options, choice); return opt->choice; } void UI_NewGame(UI_NEW_GAME_STATE *const s) { UI_BeginModal(0.5f, 2.0f / 3.0f); UI_BeginRequester(&s->req, GS("general/passport/select_mode")); bool line_drawn = false; for (int32_t i = 0; i < s->options->count; i++) { const M_OPTION *const opt = Vector_Get(s->options, i); UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, { .h = UI_STACK_H_ALIGN_SPAN }, }); if (i > 0 && !line_drawn && (opt->choice == UI_NEW_GAME_CHOICE_SWITCH_MOD || opt->choice == UI_NEW_GAME_CHOICE_PLAY_PREV_LEVELS || opt->choice == UI_NEW_GAME_CHOICE_STORY_SO_FAR)) { // TODO: do not hardcode the numbers (they come from // UI_BeginWindowBody) UI_BeginPad(g_TRVersion >= 2 ? -7.0f : -10.0f, 4.0f); UI_HorizontalLine(); UI_EndPad(); line_drawn = true; } UI_BeginRequesterRow(&s->req, i); UI_BeginAnchor(0.5f, 0.5f); UI_Label(GameString_Get(opt->label_id)); UI_EndAnchor(); UI_EndRequesterRow(&s->req, i); UI_EndStack(); } UI_EndRequester(&s->req); UI_EndModal(); } ================================================ FILE: src/trx/game/ui/dialogs/new_game.h ================================================ #pragma once // A new game mode selector dialog. #include #include typedef enum { UI_NEW_GAME_CHOICE_NG, UI_NEW_GAME_CHOICE_NGPLUS, UI_NEW_GAME_CHOICE_JP_NG, UI_NEW_GAME_CHOICE_JP_NGPLUS, UI_NEW_GAME_CHOICE_SWITCH_MOD, UI_NEW_GAME_CHOICE_PLAY_PREV_LEVELS, UI_NEW_GAME_CHOICE_STORY_SO_FAR, } UI_NEW_GAME_CHOICE; typedef struct UI_NEW_GAME_STATE UI_NEW_GAME_STATE; // state functions bool UI_NewGame_HasModChoices(void); UI_NEW_GAME_STATE *UI_NewGame_Init(bool show_play_prev_levels); int32_t UI_NewGame_Control(UI_NEW_GAME_STATE *s); void UI_NewGame_Free(UI_NEW_GAME_STATE *s); // draw functions void UI_NewGame(UI_NEW_GAME_STATE *s); ================================================ FILE: src/trx/game/ui/dialogs/pause.c ================================================ #include #include #include #include #include #include #include #include static const GAME_STRING_ID m_Options[2][2] = { { GS_ID("general/pause/continue"), GS_ID("general/pause/quit") }, { GS_ID("general/pause/yes"), GS_ID("general/pause/no") }, }; void UI_Pause_Init(UI_PAUSE_STATE *const s) { s->phase = 0; UI_Requester_Init(&s->req, 2, 2, true); } void UI_Pause_Free(UI_PAUSE_STATE *const s) { UI_Requester_Free(&s->req); } UI_PAUSE_EXIT_CHOICE UI_Pause_Control(UI_PAUSE_STATE *const s) { const int32_t choice = UI_Requester_Control(&s->req); if (s->phase == 0) { if (choice == UI_REQUESTER_CANCEL) { return UI_PAUSE_RESUME_PAUSE; } else if (choice == 0) { return UI_PAUSE_EXIT_TO_GAME; } else if (choice == 1) { s->phase = 1; UI_Requester_Free(&s->req); UI_Requester_Init(&s->req, 2, 2, true); } } else { if (choice == UI_REQUESTER_CANCEL) { s->phase = 0; } else if (choice == 0) { return UI_PAUSE_EXIT_TO_TITLE; } else if (choice == 1) { return UI_PAUSE_EXIT_TO_GAME; } } return UI_PAUSE_NOOP; } void UI_Pause(UI_PAUSE_STATE *const s) { UI_BeginModal(0.5f, 1.0f); UI_BeginPad(50.0f, 50.0f); UI_BeginRequester( &s->req, s->phase == 0 ? GS("general/pause/exit_to_title") : GS("general/pause/are_you_sure")); for (int32_t i = UI_Requester_GetFirstRow(&s->req); i < UI_Requester_GetLastRow(&s->req); i++) { UI_BeginRequesterRow(&s->req, i); UI_BeginAnchor(0.5f, 0.5f); UI_Label(GameString_Get(m_Options[s->phase][i])); UI_EndAnchor(); UI_EndRequesterRow(&s->req, i); } UI_EndRequester(&s->req); UI_EndPad(); UI_EndModal(); } ================================================ FILE: src/trx/game/ui/dialogs/pause.h ================================================ #pragma once // A pause exit confirmation dialog. #include #include typedef struct { int32_t phase; UI_REQUESTER_STATE req; } UI_PAUSE_STATE; typedef enum { UI_PAUSE_NOOP, UI_PAUSE_RESUME_PAUSE, UI_PAUSE_EXIT_TO_GAME, UI_PAUSE_EXIT_TO_TITLE, } UI_PAUSE_EXIT_CHOICE; // state functions void UI_Pause_Init(UI_PAUSE_STATE *s); UI_PAUSE_EXIT_CHOICE UI_Pause_Control(UI_PAUSE_STATE *s); void UI_Pause_Free(UI_PAUSE_STATE *s); // draw functions void UI_Pause(UI_PAUSE_STATE *s); ================================================ FILE: src/trx/game/ui/dialogs/photo_mode.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include static bool M_HasIcon(const INPUT_ROLE role) { return Input_GetKeyName( g_Config.input.backend, g_Config.input.layout[g_Config.input.backend], role, 0) != nullptr; } static void M_Title(const PHOTO_MODE current_mode) { UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_HORIZONTAL, .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE }, .spacing = { .v = 8.0f }, }); switch (current_mode) { case PHOTO_MODE_CAMERA: UI_Label(GS("general/photo_mode/title_camera_pos")); break; case PHOTO_MODE_LARA_POS: UI_Label(GS("general/photo_mode/title_lara_pos")); break; } UI_Label("\\{input step_left}\\{input step_right}"); UI_EndStack(); } static void M_Inputs(const PHOTO_MODE current_mode) { UI_Label( "\\{input camera_up}\\{input camera_down}" "\\{input camera_forward}\\{input camera_back}" "\\{input camera_left}\\{input camera_right}"); UI_Label( "\\{input left}\\{input forward}" "\\{input back}\\{input right}"); UI_Label("\\{input slow}+\\{input camera_up}/\\{input camera_down}"); UI_Label("\\{input roll}"); UI_Label("\\{input look}"); UI_Label("[\\{input slow}+]\\{input draw}"); if (Lara_Pose_IsAvailable()) { UI_Label("[\\{input slow}+]\\{input fly_cheat}"); } UI_Label("[\\{input slow}+]\\{input pause}"); UI_Label("\\{input toggle_ui}"); UI_Label("\\{input action}"); if (M_HasIcon(INPUT_ROLE_TOGGLE_PHOTO_MODE) && M_HasIcon(INPUT_ROLE_INVENTORY)) { UI_Label("\\{input toggle_photo_mode}/\\{input option}"); } else if (M_HasIcon(INPUT_ROLE_TOGGLE_PHOTO_MODE)) { UI_Label("\\{input toggle_photo_mode}"); } else if (M_HasIcon(INPUT_ROLE_INVENTORY)) { UI_Label("\\{input option}"); } } static void M_Actions(const PHOTO_MODE current_mode) { switch (current_mode) { case PHOTO_MODE_CAMERA: UI_Label(GS("general/photo_mode/camera_move_prompt")); UI_Label(GS("general/photo_mode/camera_rotate_prompt")); UI_Label(GS("general/photo_mode/camera_roll_prompt")); UI_Label(GS("general/photo_mode/camera_rotate_90_prompt")); UI_Label(GS("general/photo_mode/camera_reset_prompt")); break; case PHOTO_MODE_LARA_POS: UI_Label(GS("general/photo_mode/lara_move_prompt")); UI_Label(GS("general/photo_mode/lara_rotate_prompt")); UI_Label(GS("general/photo_mode/lara_roll_prompt")); UI_Label(GS("general/photo_mode/lara_rotate_90_prompt")); UI_Label(GS("general/photo_mode/lara_reset_prompt")); break; } UI_Label(GS("general/photo_mode/fov_prompt")); if (Lara_Pose_IsAvailable()) { UI_Label(GS("general/photo_mode/change_lara_pose")); } UI_Label(GS("general/photo_mode/advance_frame")); UI_Label(GS("general/photo_mode/toggle_help")); UI_Label(GS("general/photo_mode/snap_prompt")); UI_Label(GS("general/misc/exit")); } void UI_PhotoMode(const PHOTO_MODE current_mode) { const int32_t frame_thickness = (int32_t)(UI_Scaler_Calc(4.0f, UI_SCALER_TARGET_TEXT) + 0.5f); Output_DrawPhotoModeFrame(frame_thickness); if (!g_Config.ui.enable_photo_mode_ui) { return; } UI_BeginModal(0.0f, 0.0f); UI_BeginPad(8.0f, 8.0f); UI_BeginFrame(UI_FRAME_DIALOG_BACKGROUND); UI_BeginPad(8.0, 6.0); UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, .align = { .h = UI_STACK_H_ALIGN_SPAN }, .spacing = { .v = 8.0f }, }); M_Title(current_mode); UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_HORIZONTAL, .spacing = { .h = 8.0f }, }); // Inputs column UI_BeginStack(UI_STACK_VERTICAL); M_Inputs(current_mode); UI_EndStack(); UI_BeginStack(UI_STACK_VERTICAL); M_Actions(current_mode); UI_EndStack(); UI_EndStack(); UI_EndStack(); UI_EndPad(); UI_EndFrame(); UI_EndPad(); UI_EndModal(); } ================================================ FILE: src/trx/game/ui/dialogs/photo_mode.h ================================================ #pragma once #include #include // A photo mode tutorial dialog. void UI_PhotoMode(PHOTO_MODE current_mode); ================================================ FILE: src/trx/game/ui/dialogs/play_any_level.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef struct { int32_t level_num; const char *text; } M_ROW; typedef struct UI_PLAY_ANY_LEVEL_DIALOG_STATE { VECTOR *rows; UI_REQUESTER_STATE req; } UI_PLAY_ANY_LEVEL_DIALOG_STATE; UI_PLAY_ANY_LEVEL_DIALOG_STATE *UI_PlayAnyLevelDialog_Init(void) { UI_PLAY_ANY_LEVEL_DIALOG_STATE *const s = Memory_Alloc(sizeof(UI_PLAY_ANY_LEVEL_DIALOG_STATE)); s->rows = Vector_Create(sizeof(M_ROW)); const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); for (int32_t i = 0; i < level_table->count; i++) { const GF_LEVEL *const level = &level_table->levels[i]; if (level->type != GFL_GYM && level->type != GFL_DUMMY && level->type != GFL_CURRENT) { const M_ROW row = { .level_num = level->num, .text = level->title }; Vector_Add(s->rows, &row); } } UI_BasePassportDialog_Init(&s->req, s->rows->count); return s; } void UI_PlayAnyLevelDialog_Free(UI_PLAY_ANY_LEVEL_DIALOG_STATE *const s) { Vector_Free(s->rows); UI_Requester_Free(&s->req); Memory_Free(s); } int32_t UI_PlayAnyLevelDialog_Control(UI_PLAY_ANY_LEVEL_DIALOG_STATE *const s) { UI_BasePassportDialog_Control(&s->req); const int32_t choice = UI_Requester_Control(&s->req); switch (choice) { case UI_REQUESTER_NO_CHOICE: return UI_PLAY_ANY_LEVEL_CHOICE_NO_CHOICE; case UI_REQUESTER_CANCEL: return UI_PLAY_ANY_LEVEL_CHOICE_CANCEL; default: return ((M_ROW *)Vector_Get(s->rows, choice))->level_num; } } void UI_PlayAnyLevelDialog(UI_PLAY_ANY_LEVEL_DIALOG_STATE *const s) { UI_BeginBasePassportDialog(); UI_BeginRequester(&s->req, GS("general/passport/select_level")); for (int32_t i = 0; i < s->rows->count; i++) { if (UI_Requester_IsRowVisible(&s->req, i)) { const M_ROW *const row = Vector_Get(s->rows, i); UI_BeginRequesterRow(&s->req, i); UI_BeginAnchor(0.5f, 0.5f); UI_Label(row->text); UI_EndAnchor(); UI_EndRequesterRow(&s->req, i); } } UI_EndRequester(&s->req); UI_EndBasePassportDialog(); } ================================================ FILE: src/trx/game/ui/dialogs/play_any_level.h ================================================ // UI dialog for selecting a level within a save slot #pragma once #include #include #define UI_PLAY_ANY_LEVEL_CHOICE_NO_CHOICE UI_REQUESTER_NO_CHOICE #define UI_PLAY_ANY_LEVEL_CHOICE_CANCEL UI_REQUESTER_CANCEL typedef struct UI_PLAY_ANY_LEVEL_DIALOG_STATE UI_PLAY_ANY_LEVEL_DIALOG_STATE; struct UI_PLAY_ANY_LEVEL_DIALOG_STATE *UI_PlayAnyLevelDialog_Init(void); void UI_PlayAnyLevelDialog_Free(struct UI_PLAY_ANY_LEVEL_DIALOG_STATE *s); int32_t UI_PlayAnyLevelDialog_Control(struct UI_PLAY_ANY_LEVEL_DIALOG_STATE *s); void UI_PlayAnyLevelDialog(struct UI_PLAY_ANY_LEVEL_DIALOG_STATE *s); ================================================ FILE: src/trx/game/ui/dialogs/save_slot.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_IMMEDIATE (g_TRVersion >= 2) typedef enum { M_PHASE_BROWSE, M_PHASE_CONFIRM_DELETE, } M_PHASE; typedef struct UI_SAVE_SLOT_DIALOG_STATE { UI_SAVE_SLOT_DIALOG_TYPE type; SAVEGAME_SLOT_REF *rows; int32_t row_count; UI_REQUESTER_STATE req; UI_REQUESTER_STATE confirm_req; UI_PROGRESS_BUTTON_STATE *delete_button; M_PHASE phase; int32_t pending_delete_row; int32_t last_selected_row; } UI_SAVE_SLOT_DIALOG_STATE; static const GAME_STRING_ID m_DeleteConfirmOptions[2] = { GS_ID("general/passport/delete_save_yes"), GS_ID("general/passport/delete_save_no"), }; static void M_NonEmptySlot( const UI_SAVE_SLOT_DIALOG_STATE *const s, const SAVEGAME_SLOT_REF slot, const SAVEGAME_INFO *const info) { if (g_TRVersion == 1) { UI_BeginAnchor(0.5f, 0.5f); UI_BeginStack(UI_STACK_HORIZONTAL); } else { UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_HORIZONTAL, .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE }, }); } // Level title with the save counter UI_Label(info->level_title); if (info->counter > 0) { UI_Spacer(8.0f, 0.0f); if (slot.pool == SAVEGAME_SLOT_POOL_QUICK) { UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_HORIZONTAL, .align = { .h = UI_STACK_H_ALIGN_RIGHT }, }); if (g_TRVersion != 1) { UI_Label("QS "); } } UI_LabelFmt("%d", info->counter); if (slot.pool == SAVEGAME_SLOT_POOL_QUICK) { if (g_TRVersion == 1) { UI_Label(" (QS)"); } UI_EndStack(); } } UI_EndStack(); if (g_TRVersion == 1) { UI_EndAnchor(); } } static void M_EmptySlot( const UI_SAVE_SLOT_DIALOG_STATE *const s, const SAVEGAME_SLOT_REF slot) { UI_BeginAnchor(0.5f, 0.5f); if (slot.pool == SAVEGAME_SLOT_POOL_QUICK) { UI_LabelFmt( "[Q%d] %s", slot.index + 1, GS("general/misc/empty_slot_fmt")); } else { UI_LabelFmt(GS("general/misc/empty_slot_fmt"), slot.index + 1); } UI_EndAnchor(); } static void M_ConfirmDeleteDialog(const UI_SAVE_SLOT_DIALOG_STATE *const s) { UI_BeginModal(0.5f, g_Inv_Mode == INV_TITLE_MODE ? 0.69f : 0.55f); UI_BeginPad(50.0f, 50.0f); UI_BeginWindow((UI_WINDOW_SETTINGS) { .title = GS("general/passport/delete_save_confirm"), .scrollable = nullptr, .title_spacing = -1.0f, .heavy = true, }); for (int32_t i = UI_Requester_GetFirstRow(&s->confirm_req); i < UI_Requester_GetLastRow(&s->confirm_req); i++) { UI_BeginRequesterRow(&s->confirm_req, i); UI_BeginAnchor(0.5f, 0.5f); UI_Label(GameString_Get(m_DeleteConfirmOptions[i])); UI_EndAnchor(); UI_EndRequesterRow(&s->confirm_req, i); } UI_EndWindow(); UI_EndPad(); UI_EndModal(); } static int32_t M_GetTotalSlots(void) { return Savegame_GetSlotCount(SAVEGAME_SLOT_POOL_QUICK) + Savegame_GetSlotCount(SAVEGAME_SLOT_POOL_NORMAL); } static SAVEGAME_SLOT_REF M_MapRowToSlot( const UI_SAVE_SLOT_DIALOG_STATE *const s, const int32_t row) { ASSERT(s != nullptr); if (row < 0 || row >= s->row_count || s->rows == nullptr) { return Savegame_InvalidSlot(); } return s->rows[row]; } static void M_BuildRows(UI_SAVE_SLOT_DIALOG_STATE *const s) { const int32_t max_row_count = MAX(M_GetTotalSlots(), 1); s->rows = Memory_Alloc(sizeof(SAVEGAME_SLOT_REF) * max_row_count); s->row_count = 0; const int32_t quick_visual_count = Savegame_GetQuickVisualCount(); for (int32_t i = 0; i < quick_visual_count; i++) { const SAVEGAME_SLOT_REF slot = Savegame_QuickFromVisualIndex(i); if (Savegame_IsValidSlotRef(slot)) { s->rows[s->row_count++] = slot; } } const int32_t normal_slot_count = Savegame_GetSlotCount(SAVEGAME_SLOT_POOL_NORMAL); for (int32_t i = 0; i < normal_slot_count; i++) { s->rows[s->row_count++] = Savegame_NormalSlot(i); } if (s->row_count == 0) { s->rows[s->row_count++] = Savegame_InvalidSlot(); } } static void M_RebuildRows( UI_SAVE_SLOT_DIALOG_STATE *const s, const int32_t focused_row) { int32_t selected_row = focused_row; UI_Requester_Free(&s->req); Memory_FreePointer(&s->rows); M_BuildRows(s); UI_BasePassportDialog_Init(&s->req, s->row_count); CLAMP(selected_row, 0, s->row_count - 1); UI_Requester_SelectRow(&s->req, selected_row); } static bool M_IsSlotDeletable(const SAVEGAME_SLOT_REF slot) { return Savegame_IsValidSlotRef(slot) && !Savegame_IsSlotFree(slot); } static void M_BeginDeleteConfirmButton(void *const arg) { UI_SAVE_SLOT_DIALOG_STATE *const s = arg; const SAVEGAME_SLOT_REF slot = M_MapRowToSlot(s, UI_Requester_GetCurrentRow(&s->req)); if (!M_IsSlotDeletable(slot)) { return; } s->phase = M_PHASE_CONFIRM_DELETE; s->pending_delete_row = UI_Requester_GetCurrentRow(&s->req); UI_Requester_Init(&s->confirm_req, 2, 2, true); } static void M_ResetDeleteState(UI_SAVE_SLOT_DIALOG_STATE *const s) { s->phase = M_PHASE_BROWSE; s->pending_delete_row = -1; } static void M_ResetDeleteButton(UI_SAVE_SLOT_DIALOG_STATE *const s) { if (s->delete_button != nullptr) { UI_ProgressButton_Free(s->delete_button); } s->delete_button = UI_ProgressButton_Init( g_Config.input.backend, INPUT_ROLE_UNBIND_KEY, GS_ID("general/passport/delete_save"), M_BeginDeleteConfirmButton, s); } UI_SAVE_SLOT_DIALOG_STATE *UI_SaveSlotDialog_Init( const UI_SAVE_SLOT_DIALOG_TYPE type, const SAVEGAME_SLOT_REF initial_slot) { UI_SAVE_SLOT_DIALOG_STATE *const s = Memory_Alloc(sizeof(UI_SAVE_SLOT_DIALOG_STATE)); s->type = type; M_BuildRows(s); M_ResetDeleteState(s); s->delete_button = nullptr; s->last_selected_row = -1; int32_t initial_row = 0; if (Savegame_IsValidSlotRef(initial_slot)) { for (int32_t i = 0; i < s->row_count; i++) { if (s->rows[i].pool == initial_slot.pool && s->rows[i].index == initial_slot.index) { initial_row = i; break; } } } UI_BasePassportDialog_Init(&s->req, s->row_count); UI_Requester_SelectRow(&s->req, initial_row); s->last_selected_row = initial_row; M_ResetDeleteButton(s); return s; } void UI_SaveSlotDialog_Free(UI_SAVE_SLOT_DIALOG_STATE *const s) { UI_Requester_Free(&s->req); UI_Requester_Free(&s->confirm_req); if (s->delete_button != nullptr) { UI_ProgressButton_Free(s->delete_button); s->delete_button = nullptr; } Memory_FreePointer(&s->rows); Memory_Free(s); } UI_SAVE_SLOT_DIALOG_CHOICE UI_SaveSlotDialog_Control( UI_SAVE_SLOT_DIALOG_STATE *const s) { if (s->phase == M_PHASE_CONFIRM_DELETE) { const int32_t choice = UI_Requester_Control(&s->confirm_req); if (choice == UI_REQUESTER_CANCEL || choice == 1) { M_ResetDeleteState(s); g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; return (UI_SAVE_SLOT_DIALOG_CHOICE) { .action = UI_SAVE_SLOT_DIALOG_NO_CHOICE, }; } if (choice == 0) { const SAVEGAME_SLOT_REF slot = M_MapRowToSlot(s, s->pending_delete_row); const int32_t focused_row = s->pending_delete_row; M_ResetDeleteState(s); g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; if (!Savegame_Delete(slot)) { return (UI_SAVE_SLOT_DIALOG_CHOICE) { .action = UI_SAVE_SLOT_DIALOG_DELETE_FAILED, }; } Savegame_ScanSavedGames(); M_RebuildRows(s, focused_row); } return (UI_SAVE_SLOT_DIALOG_CHOICE) { .action = UI_SAVE_SLOT_DIALOG_NO_CHOICE, }; } UI_BasePassportDialog_Control(&s->req); const int32_t sel_row = UI_Requester_GetCurrentRow(&s->req); if (sel_row != s->last_selected_row) { s->last_selected_row = sel_row; M_ResetDeleteButton(s); } if (M_IsSlotDeletable(M_MapRowToSlot(s, sel_row))) { UI_ProgressButton_Control(s->delete_button); } const int32_t choice = UI_Requester_Control(&s->req); if (choice == UI_REQUESTER_CANCEL) { return (UI_SAVE_SLOT_DIALOG_CHOICE) { .action = UI_SAVE_SLOT_DIALOG_CANCEL, }; } if (choice != UI_REQUESTER_NO_CHOICE) { const SAVEGAME_SLOT_REF slot = M_MapRowToSlot(s, sel_row); if (!Savegame_IsValidSlotRef(slot)) { return (UI_SAVE_SLOT_DIALOG_CHOICE) { .action = UI_SAVE_SLOT_DIALOG_NO_CHOICE, }; } const bool is_valid_save_target = s->type == UI_SAVE_SLOT_DIALOG_SAVE_GAME && slot.pool == SAVEGAME_SLOT_POOL_NORMAL; const bool is_valid_load_target = s->type == UI_SAVE_SLOT_DIALOG_LOAD_GAME && !Savegame_IsSlotFree(slot); const bool is_valid_generic_target = s->type == UI_SAVE_SLOT_DIALOG_GENERIC && !Savegame_IsSlotFree(slot); if (is_valid_save_target || is_valid_load_target || is_valid_generic_target) { return (UI_SAVE_SLOT_DIALOG_CHOICE) { .action = UI_SAVE_SLOT_DIALOG_CONFIRM, .slot = slot, }; } } return (UI_SAVE_SLOT_DIALOG_CHOICE) { .action = UI_SAVE_SLOT_DIALOG_NO_CHOICE, }; } void UI_SaveSlotDialog(const UI_SAVE_SLOT_DIALOG_STATE *const s) { const SAVEGAME_SLOT_REF selected_slot = M_MapRowToSlot(s, UI_Requester_GetCurrentRow(&s->req)); const bool can_delete = M_IsSlotDeletable(selected_slot); UI_BeginBasePassportDialog(); const char *title = nullptr; switch (s->type) { case UI_SAVE_SLOT_DIALOG_SAVE_GAME: title = GS("general/passport/save_game"); break; case UI_SAVE_SLOT_DIALOG_LOAD_GAME: title = GS("general/passport/load_game"); break; case UI_SAVE_SLOT_DIALOG_GENERIC: title = GS("general/passport/select_save"); break; } UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, .align = { .h = UI_STACK_H_ALIGN_SPAN }, .spacing = { .v = 3.0f }, }); UI_BeginRequester(&s->req, title); const int32_t first = UI_Requester_GetFirstRow(&s->req); const int32_t last = UI_Requester_GetLastRow(&s->req); for (int32_t i = first; i < last; ++i) { UI_BeginRequesterRow(&s->req, i); const SAVEGAME_SLOT_REF slot = M_MapRowToSlot(s, i); const SAVEGAME_INFO *const info = Savegame_GetSavegameInfo(slot); if (Savegame_IsValidSlotRef(slot) && info != nullptr && info->level_title != nullptr) { M_NonEmptySlot(s, slot, info); } else { M_EmptySlot(s, slot); } UI_EndRequesterRow(&s->req, i); } UI_EndRequester(&s->req); UI_BeginHide(s->phase != M_PHASE_BROWSE || !can_delete); UI_BeginAnchor(1.0f, 0.5f); UI_ProgressButton(s->delete_button); UI_EndAnchor(); UI_EndHide(); UI_EndStack(); UI_EndBasePassportDialog(); if (s->phase == M_PHASE_CONFIRM_DELETE) { M_ConfirmDeleteDialog(s); } } ================================================ FILE: src/trx/game/ui/dialogs/save_slot.h ================================================ // UI dialog for selecting a save slot (load or save game) #pragma once #include #include typedef enum { UI_SAVE_SLOT_DIALOG_LOAD_GAME, UI_SAVE_SLOT_DIALOG_SAVE_GAME, UI_SAVE_SLOT_DIALOG_GENERIC, } UI_SAVE_SLOT_DIALOG_TYPE; typedef enum { UI_SAVE_SLOT_DIALOG_NO_CHOICE, UI_SAVE_SLOT_DIALOG_CANCEL, UI_SAVE_SLOT_DIALOG_CONFIRM, UI_SAVE_SLOT_DIALOG_DELETE_FAILED, } UI_SAVE_SLOT_DIALOG_ACTION; typedef struct { UI_SAVE_SLOT_DIALOG_ACTION action; SAVEGAME_SLOT_REF slot; } UI_SAVE_SLOT_DIALOG_CHOICE; typedef struct UI_SAVE_SLOT_DIALOG_STATE UI_SAVE_SLOT_DIALOG_STATE; // state functions struct UI_SAVE_SLOT_DIALOG_STATE *UI_SaveSlotDialog_Init( UI_SAVE_SLOT_DIALOG_TYPE type, SAVEGAME_SLOT_REF initial_slot); void UI_SaveSlotDialog_Free(struct UI_SAVE_SLOT_DIALOG_STATE *s); UI_SAVE_SLOT_DIALOG_CHOICE UI_SaveSlotDialog_Control( struct UI_SAVE_SLOT_DIALOG_STATE *s); // draw functions void UI_SaveSlotDialog(const struct UI_SAVE_SLOT_DIALOG_STATE *s); ================================================ FILE: src/trx/game/ui/dialogs/select_level.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef struct { const char *const text; const GF_LEVEL *const level; } M_ROW; typedef struct UI_SELECT_LEVEL_DIALOG_STATE { SAVEGAME_SLOT_REF save_slot; VECTOR *rows; UI_REQUESTER_STATE req; } UI_SELECT_LEVEL_DIALOG_STATE; UI_SELECT_LEVEL_DIALOG_STATE *UI_SelectLevelDialog_Init( const SAVEGAME_SLOT_REF save_slot) { UI_SELECT_LEVEL_DIALOG_STATE *const s = Memory_Alloc(sizeof(UI_SELECT_LEVEL_DIALOG_STATE)); s->save_slot = save_slot; s->rows = Vector_Create(sizeof(M_ROW)); const SAVEGAME_INFO *const info = Savegame_GetSavegameInfo(save_slot); ASSERT(info != nullptr); ASSERT(info->features.select_level); Savegame_LoadOnlyResumeInfo(save_slot); const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); for (int32_t i = 0; i <= info->level_num && i < level_table->count; i++) { const GF_LEVEL *const level = &level_table->levels[i]; const RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); if (resume != nullptr && resume->flags.available && level->type != GFL_GYM) { Vector_Add( s->rows, &(M_ROW) { .text = level_table->levels[i].title, .level = level, }); } } UI_BasePassportDialog_Init(&s->req, s->rows->count); return s; } void UI_SelectLevelDialog_Free(UI_SELECT_LEVEL_DIALOG_STATE *const s) { Vector_Free(s->rows); UI_Requester_Free(&s->req); Memory_Free(s); } int32_t UI_SelectLevelDialog_Control(UI_SELECT_LEVEL_DIALOG_STATE *const s) { UI_BasePassportDialog_Control(&s->req); const int32_t choice = UI_Requester_Control(&s->req); if (choice == UI_REQUESTER_NO_CHOICE || choice == UI_REQUESTER_CANCEL) { return choice; } const M_ROW *const row = Vector_Get(s->rows, choice); return row->level->num; } void UI_SelectLevelDialog(UI_SELECT_LEVEL_DIALOG_STATE *const s) { UI_BeginBasePassportDialog(); UI_BeginRequester(&s->req, GS("general/passport/select_level")); const SAVEGAME_INFO *info = Savegame_GetSavegameInfo(s->save_slot); for (int32_t i = 0; i < s->rows->count; i++) { if (UI_Requester_IsRowVisible(&s->req, i)) { const M_ROW *const row = Vector_Get(s->rows, i); UI_BeginRequesterRow(&s->req, i); UI_BeginAnchor(0.5f, 0.5f); UI_Label(row->text); UI_EndAnchor(); UI_EndRequesterRow(&s->req, i); } } UI_EndRequester(&s->req); UI_EndBasePassportDialog(); } ================================================ FILE: src/trx/game/ui/dialogs/select_level.h ================================================ // UI dialog for selecting a level within a save slot #pragma once #include #include #include #define UI_SELECT_LEVEL_CHOICE_NOOP -1 #define UI_SELECT_LEVEL_CHOICE_CANCEL -2 #define UI_SELECT_LEVEL_CHOICE_PLAY_STORY_SO_FAR -3 typedef struct UI_SELECT_LEVEL_DIALOG_STATE UI_SELECT_LEVEL_DIALOG_STATE; struct UI_SELECT_LEVEL_DIALOG_STATE *UI_SelectLevelDialog_Init( SAVEGAME_SLOT_REF save_slot); void UI_SelectLevelDialog_Free(struct UI_SELECT_LEVEL_DIALOG_STATE *s); int32_t UI_SelectLevelDialog_Control(struct UI_SELECT_LEVEL_DIALOG_STATE *s); void UI_SelectLevelDialog(struct UI_SELECT_LEVEL_DIALOG_STATE *s); ================================================ FILE: src/trx/game/ui/dialogs/setting_helpers/enums.c ================================================ #include #include #include const UI_SETTINGS_ENUM_ENTRY UI_Settings_StatsStyleEnumEntries[] = { { STATS_STYLE_BARE }, { STATS_STYLE_BORDERED }, { -1 }, }; const UI_SETTINGS_ENUM_ENTRY UI_Settings_TargetModeEnumEntries[] = { { TARGET_LOCK_MODE_FULL }, { TARGET_LOCK_MODE_SEMI }, { TARGET_LOCK_MODE_NONE }, { -1 }, }; const UI_SETTINGS_ENUM_ENTRY UI_Settings_LookModeEnumEntries[] = { { LOOK_MODE_RESTRICTED }, { LOOK_MODE_ENHANCED }, { LOOK_MODE_UNRESTRICTED }, { -1 }, }; const UI_SETTINGS_ENUM_ENTRY UI_Settings_QuickGunsModeEnumEntries[] = { { QUICK_GUNS_MODE_DRAW_ONLY }, { QUICK_GUNS_MODE_DRAW_AND_HOLSTER }, { -1 }, }; const UI_SETTINGS_ENUM_ENTRY UI_Settings_JumpLockModeEnumEntries[] = { { JUMP_LOCK_LEGACY }, { JUMP_LOCK_TUNED }, { JUMP_LOCK_DISABLED }, { -1 }, }; const UI_SETTINGS_ENUM_ENTRY UI_Settings_WallGlitchEnumEntries[] = { { WALL_GLITCH_FIXED }, { WALL_GLITCH_TR1 }, { WALL_GLITCH_TR2 }, { -1 }, }; const UI_SETTINGS_ENUM_ENTRY UI_Settings_EnemyHealthBarShowModeEnumEntries[] = { { BAR_SHOW_MODE_NEVER }, { BAR_SHOW_MODE_BOSS_ONLY }, { BAR_SHOW_MODE_ALWAYS }, { -1 }, }; const UI_SETTINGS_ENUM_ENTRY UI_Settings_UIElementLocationEnumEntries[] = { { UI_ELEMENT_LOCATION_TOP_LEFT }, { UI_ELEMENT_LOCATION_TOP_CENTER }, { UI_ELEMENT_LOCATION_TOP_RIGHT }, { UI_ELEMENT_LOCATION_BOTTOM_LEFT }, { UI_ELEMENT_LOCATION_BOTTOM_CENTER }, { UI_ELEMENT_LOCATION_BOTTOM_RIGHT }, { -1 }, }; const UI_SETTINGS_ENUM_ENTRY UI_Settings_BackgroundStyleEnumEntries[] = { { BK_NONE }, { BK_TRANSPARENT_MEDIUM }, { BK_TRANSPARENT_DARK }, { BK_BLACK }, { BK_PATTERN_STATIC }, { BK_PATTERN_WAVE }, { BK_MONOCHROME }, { BK_MONOCHROME_COOL }, { BK_MONOCHROME_WARM }, { -1 }, }; const UI_SETTINGS_ENUM_ENTRY UI_Settings_CameraModeEnumEntries[] = { { CAMERA_MODE_TR1 }, { CAMERA_MODE_TR2 }, { CAMERA_MODE_TR3 }, { -1 }, }; const UI_SETTINGS_ENUM_ENTRY UI_Settings_TextureFilterEnumEntries[] = { { TEXTURE_FILTER_POINT }, { TEXTURE_FILTER_BILINEAR }, { -1 }, }; const UI_SETTINGS_ENUM_ENTRY UI_Settings_MenuStyleEnumEntries[] = { { UI_STYLE_PS1 }, { UI_STYLE_PC }, { -1 }, }; const UI_SETTINGS_ENUM_ENTRY UI_Settings_LightingContrastEnumEntries[] = { { LIGHTING_CONTRAST_LOW }, { LIGHTING_CONTRAST_MEDIUM }, { LIGHTING_CONTRAST_HIGH }, { -1 }, }; const UI_SETTINGS_ENUM_ENTRY UI_Settings_SpriteLockModeEnumEntries[] = { { BILLBOARD_LOCK_NONE }, { BILLBOARD_LOCK_ROLL }, { BILLBOARD_LOCK_ROLL_PITCH }, { BILLBOARD_LOCK_PERSPECTIVE }, { -1 }, }; const UI_SETTINGS_ENUM_ENTRY UI_Settings_AspectModeEnumEntries[] = { { ASPECT_MODE_4_3 }, { ASPECT_MODE_16_9 }, { ASPECT_MODE_16_10 }, { ASPECT_MODE_ANY }, { -1 }, }; const UI_SETTINGS_ENUM_ENTRY UI_Settings_ScreenshotFormatEnumEntries[] = { { SCREENSHOT_FORMAT_JPEG }, { SCREENSHOT_FORMAT_PNG }, { -1 }, }; const UI_SETTINGS_ENUM_ENTRY UI_Settings_AllyHostilityPolicyEnumEntries[] = { { ALLY_HOSTILITY_POLICY_INDIVIDUAL }, { ALLY_HOSTILITY_POLICY_SHARED }, { -1 }, }; const UI_SETTINGS_ENUM_ENTRY UI_Settings_CreatureDrownPolicyEnumEntries[] = { { CREATURE_DROWN_POLICY_NEVER }, { CREATURE_DROWN_POLICY_DEFAULT }, { CREATURE_DROWN_POLICY_SUBMERGED }, { -1 }, }; const UI_SETTINGS_ENUM_ENTRY UI_Settings_ProjectileAreaDamageEnumEntries[] = { { PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP }, { PROJECTILE_AREA_DAMAGE_MULTI_SWEEP }, { -1 }, }; const UI_SETTINGS_ENUM_ENTRY UI_Settings_LoadingScreensModeEnumEntries[] = { { LOADING_SCREENS_DISABLED }, { LOADING_SCREENS_ALWAYS }, { LOADING_SCREENS_NEW_GAMES }, { -1 }, }; const UI_SETTINGS_ENUM_ENTRY UI_Settings_MusicLoadConditionEnumEntries[] = { { MUSIC_LOAD_CONDITION_NEVER }, { MUSIC_LOAD_CONDITION_NON_AMBIENT }, { MUSIC_LOAD_CONDITION_ALWAYS }, { -1 }, }; const UI_SETTINGS_ENUM_ENTRY UI_Settings_ShadowTypeEnumEntries[] = { { SHADOW_TYPE_OCTAGON }, { SHADOW_TYPE_CIRCLE }, { SHADOW_TYPE_SPRITE }, { -1 }, }; const UI_SETTINGS_ENUM_ENTRY UI_Settings_BloodEffectsEnumEntries[] = { { BLOOD_EFFECTS_DISABLED }, { BLOOD_EFFECTS_PINK }, { BLOOD_EFFECTS_RED }, { -1 }, }; const UI_SETTINGS_ENUM_ENTRY UI_Settings_SunglassesModeEnumEntries[] = { { SUNGLASSES_MODE_OFF }, { SUNGLASSES_MODE_OPAQUE }, { SUNGLASSES_MODE_TRANSPARENT }, { -1 }, }; ================================================ FILE: src/trx/game/ui/dialogs/setting_helpers/enums.h ================================================ #pragma once #include typedef struct { int32_t value; } UI_SETTINGS_ENUM_ENTRY; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_TargetModeEnumEntries[]; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_LookModeEnumEntries[]; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_QuickGunsModeEnumEntries[]; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_JumpLockModeEnumEntries[]; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_WallGlitchEnumEntries[]; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_StatsStyleEnumEntries[]; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_EnemyHealthBarShowModeEnumEntries[]; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_UIElementLocationEnumEntries[]; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_BackgroundStyleEnumEntries[]; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_CameraModeEnumEntries[]; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_FOVModeEnumEntries[]; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_TextureFilterEnumEntries[]; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_MenuStyleEnumEntries[]; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_LightingContrastEnumEntries[]; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_SpriteLockModeEnumEntries[]; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_AspectModeEnumEntries[]; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_ScreenshotFormatEnumEntries[]; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_AllyHostilityPolicyEnumEntries[]; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_CreatureDrownPolicyEnumEntries[]; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_LoadingScreensModeEnumEntries[]; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_MusicLoadConditionEnumEntries[]; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_ShadowTypeEnumEntries[]; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_BloodEffectsEnumEntries[]; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_ProjectileAreaDamageEnumEntries[]; extern const UI_SETTINGS_ENUM_ENTRY UI_Settings_SunglassesModeEnumEntries[]; ================================================ FILE: src/trx/game/ui/dialogs/setting_helpers/handlers.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include bool UI_Settings_EnablePS1Crystals_IsAvailable( const UI_SETTINGS_OPTION *const option) { return g_Config.gameplay.enable_save_crystals; } bool UI_Settings_ShowCrystals_IsAvailable( const UI_SETTINGS_OPTION *const option) { if (!Stats_GameHasCrystals()) { return false; } if (g_TRVersion <= 2 && !g_Config.gameplay.enable_save_crystals) { return false; } return true; } bool UI_Settings_EnableFadeEffects_IsAvailable( const UI_SETTINGS_OPTION *const option) { return g_Config.visuals.enable_fade_effects; } bool UI_Settings_FogColor_IsAvailable(const UI_SETTINGS_OPTION *const option) { return !g_Config.visuals.fog_transparency; } bool UI_Settings_EnableBreeze_IsAvailable( const UI_SETTINGS_OPTION *const option) { return g_Config.visuals.enable_braid || g_TRVersion == 3; } bool UI_Settings_ResponsiveJumping_IsAvailable( const UI_SETTINGS_OPTION *const option) { return g_Config.gameplay.enable_tr2_jumping; } bool UI_Settings_Crawl_IsAvailable(const UI_SETTINGS_OPTION *const option) { return g_Config.gameplay.enable_crawling; } bool UI_Settings_Sprint_IsAvailable(const UI_SETTINGS_OPTION *const option) { return g_Config.gameplay.enable_sprint; } bool UI_Settings_Bar_IsAvailable(const UI_SETTINGS_OPTION *const option) { return g_Config.ui.show_bars; } bool UI_Settings_Healthbar_IsAvailable(const UI_SETTINGS_OPTION *const option) { return UI_Settings_Bar_IsAvailable(option); } bool UI_Settings_Airbar_IsAvailable(const UI_SETTINGS_OPTION *const option) { return UI_Settings_Bar_IsAvailable(option); } bool UI_Settings_Sprintbar_IsAvailable(const UI_SETTINGS_OPTION *const option) { return UI_Settings_Sprint_IsAvailable(option) && UI_Settings_Bar_IsAvailable(option); } bool UI_Settings_Exposurebar_IsAvailable(const UI_SETTINGS_OPTION *const option) { return UI_Settings_Bar_IsAvailable(option); } bool UI_Settings_EnemyHealthbar_IsAvailable( const UI_SETTINGS_OPTION *const option) { return UI_Settings_Bar_IsAvailable(option) && g_Config.ui.enemy_health_bar.show_mode != BAR_SHOW_MODE_NEVER; } bool UI_Settings_AllyHealthbar_IsAvailable( const UI_SETTINGS_OPTION *const option) { return UI_Settings_Bar_IsAvailable(option) && g_Config.ui.enemy_health_bar.show_mode == BAR_SHOW_MODE_ALWAYS && g_Config.gameplay.enable_ally_targeting; } bool UI_Settings_HealthbarColor_IsAvailable( const UI_SETTINGS_OPTION *const option) { return UI_Settings_Healthbar_IsAvailable(option); } bool UI_Settings_AirbarColor_IsAvailable(const UI_SETTINGS_OPTION *const option) { return UI_Settings_Airbar_IsAvailable(option); } bool UI_Settings_SprintbarColor_IsAvailable( const UI_SETTINGS_OPTION *const option) { return UI_Settings_Sprintbar_IsAvailable(option); } bool UI_Settings_ExposurebarColor_IsAvailable( const UI_SETTINGS_OPTION *const option) { return UI_Settings_Exposurebar_IsAvailable(option); } bool UI_Settings_EnemyHealthbarColor_IsAvailable( const UI_SETTINGS_OPTION *const option) { return UI_Settings_EnemyHealthbar_IsAvailable(option); } bool UI_Settings_AllyHealthbarColor_IsAvailable( const UI_SETTINGS_OPTION *const option) { return UI_Settings_AllyHealthbar_IsAvailable(option); } bool UI_Settings_BarColorPC_IsVisible(const UI_SETTINGS_OPTION *const option) { return !UI_Settings_IsCurrentBarLookPS1(); } bool UI_Settings_BarColorPS1_IsVisible(const UI_SETTINGS_OPTION *const option) { return UI_Settings_IsCurrentBarLookPS1(); } bool UI_Settings_IdlePose_IsAvailable(const UI_SETTINGS_OPTION *const option) { return g_Config.gameplay.idle_pose_timeout > 0; } const char *UI_Settings_ColorEditor_FormatValue( const UI_SETTINGS_OPTION *const option) { const RGB_888 *const color = option->target; return String_FormatStatic("#%02X%02X%02X", color->r, color->g, color->b); } bool UI_Settings_ColorEditor_CanChangeValue( const UI_SETTINGS_OPTION *const option, const int32_t dir) { return false; } bool UI_Settings_FixItemRots_IsAvailable(const UI_SETTINGS_OPTION *const option) { return g_Config.visuals.enable_3d_pickups; } bool UI_Settings_FixStepGlitch_IsAvailable( const UI_SETTINGS_OPTION *const option) { return g_Config.gameplay.enable_smooth_wall_deflect; } bool UI_Settings_FixWadeWallHit_IsAvailable( const UI_SETTINGS_OPTION *const option) { return g_Config.gameplay.enable_wading; } bool UI_Settings_PauseMusicInInventory_IsAvailable( const UI_SETTINGS_OPTION *const option) { return g_Config.audio.enable_music_in_inventory; } bool UI_Settings_BackgroundStyle_IsEnumValueAvailable( const UI_SETTINGS_OPTION *const option, const int32_t value) { if (value == BK_PATTERN_STATIC || value == BK_PATTERN_WAVE) { return Object_Get(O_INV_BACKGROUND)->loaded; } return true; } bool UI_Settings_ShadowType_IsEnumValueAvailable( const UI_SETTINGS_OPTION *const option, const int32_t value) { if (value == SHADOW_TYPE_SPRITE) { return Object_Get(O_SHADOW)->loaded; } return true; } bool UI_Settings_Volume_RequestChange( const UI_SETTINGS_OPTION *const option, const int32_t dir) { UI_SettingsEditor_RequestChange(option, dir); if (option->target == &g_Config.audio.music_volume) { Music_SetVolume(g_Config.audio.music_volume); } else if (option->target == &g_Config.audio.sound_volume) { Sound_SetMasterVolume(g_Config.audio.sound_volume); } Sound_Effect(SFX_MENU_PASSPORT, nullptr, SPM_ALWAYS); return true; } bool UI_Settings_Flare_IsAvailable(const UI_SETTINGS_OPTION *const option) { return g_Weapons[LGT_FLARE].is_available; } bool UI_Settings_Grenade_IsAvailable(const UI_SETTINGS_OPTION *const option) { return g_Weapons[LGT_GRENADE].is_available; } bool UI_Settings_Harpoon_IsAvailable(const UI_SETTINGS_OPTION *const option) { return g_Weapons[LGT_HARPOON].is_available; } bool UI_Settings_M16_IsAvailable(const UI_SETTINGS_OPTION *const option) { return g_Weapons[LGT_M16].is_available || g_Weapons[LGT_MP5].is_available; } bool UI_Settings_ProjectileAreaDamage_IsAvailable( const UI_SETTINGS_OPTION *const option) { return g_Weapons[LGT_ROCKET].is_available || g_Weapons[LGT_GRENADE].is_available; } ================================================ FILE: src/trx/game/ui/dialogs/setting_helpers/handlers.h ================================================ #pragma once #include bool UI_Settings_EnablePS1Crystals_IsAvailable( const UI_SETTINGS_OPTION *option); bool UI_Settings_ShowCrystals_IsAvailable(const UI_SETTINGS_OPTION *option); bool UI_Settings_FixItemRots_IsAvailable(const UI_SETTINGS_OPTION *option); bool UI_Settings_FixStepGlitch_IsAvailable(const UI_SETTINGS_OPTION *option); bool UI_Settings_FixWadeWallHit_IsAvailable(const UI_SETTINGS_OPTION *option); bool UI_Settings_EnableFadeEffects_IsAvailable( const UI_SETTINGS_OPTION *option); bool UI_Settings_PauseMusicInInventory_IsAvailable( const UI_SETTINGS_OPTION *option); bool UI_Settings_BackgroundStyle_IsEnumValueAvailable( const UI_SETTINGS_OPTION *option, int32_t value); bool UI_Settings_ShadowType_IsEnumValueAvailable( const UI_SETTINGS_OPTION *option, int32_t value); bool UI_Settings_FogColor_IsAvailable(const UI_SETTINGS_OPTION *option); bool UI_Settings_EnableBreeze_IsAvailable(const UI_SETTINGS_OPTION *option); bool UI_Settings_ResponsiveJumping_IsAvailable( const UI_SETTINGS_OPTION *option); bool UI_Settings_Bar_IsAvailable(const UI_SETTINGS_OPTION *option); bool UI_Settings_Crawl_IsAvailable(const UI_SETTINGS_OPTION *option); bool UI_Settings_Sprint_IsAvailable(const UI_SETTINGS_OPTION *option); bool UI_Settings_Healthbar_IsAvailable(const UI_SETTINGS_OPTION *option); bool UI_Settings_Airbar_IsAvailable(const UI_SETTINGS_OPTION *option); bool UI_Settings_Sprintbar_IsAvailable(const UI_SETTINGS_OPTION *option); bool UI_Settings_Exposurebar_IsAvailable(const UI_SETTINGS_OPTION *option); bool UI_Settings_EnemyHealthbar_IsAvailable(const UI_SETTINGS_OPTION *option); bool UI_Settings_AllyHealthbar_IsAvailable(const UI_SETTINGS_OPTION *option); bool UI_Settings_HealthbarColor_IsAvailable(const UI_SETTINGS_OPTION *option); bool UI_Settings_AirbarColor_IsAvailable(const UI_SETTINGS_OPTION *option); bool UI_Settings_SprintbarColor_IsAvailable(const UI_SETTINGS_OPTION *option); bool UI_Settings_ExposurebarColor_IsAvailable(const UI_SETTINGS_OPTION *option); bool UI_Settings_EnemyHealthbarColor_IsAvailable( const UI_SETTINGS_OPTION *option); bool UI_Settings_AllyHealthbarColor_IsAvailable( const UI_SETTINGS_OPTION *option); bool UI_Settings_BarColorPC_IsVisible(const UI_SETTINGS_OPTION *option); bool UI_Settings_BarColorPS1_IsVisible(const UI_SETTINGS_OPTION *option); bool UI_Settings_IdlePose_IsAvailable(const UI_SETTINGS_OPTION *option); const char *UI_Settings_ColorEditor_FormatValue( const UI_SETTINGS_OPTION *option); bool UI_Settings_ColorEditor_CanChangeValue( const UI_SETTINGS_OPTION *option, int32_t dir); const char *UI_Settings_Language_FormatValue(const UI_SETTINGS_OPTION *option); bool UI_Settings_Language_CanChangeValue( const UI_SETTINGS_OPTION *option, int32_t dir); bool UI_Settings_Language_RequestChangeValue( const UI_SETTINGS_OPTION *option, int32_t dir); bool UI_Settings_Volume_RequestChange( const UI_SETTINGS_OPTION *option, int32_t dir); bool UI_Settings_Flare_IsAvailable(const UI_SETTINGS_OPTION *option); bool UI_Settings_Grenade_IsAvailable(const UI_SETTINGS_OPTION *option); bool UI_Settings_Harpoon_IsAvailable(const UI_SETTINGS_OPTION *option); bool UI_Settings_M16_IsAvailable(const UI_SETTINGS_OPTION *option); bool UI_Settings_ProjectileAreaDamage_IsAvailable( const UI_SETTINGS_OPTION *option); ================================================ FILE: src/trx/game/ui/dialogs/setting_helpers/handlers_language.c ================================================ #include #include #include #include #include #include static VECTOR *m_Languages = nullptr; static void M_Language_Cleanup(void) { // Free the languages vector and its strings. if (m_Languages != nullptr) { for (int32_t i = 0; i < m_Languages->count; i++) { char *lang = *(char **)Vector_Get(m_Languages, i); Memory_Free(lang); } Vector_Free(m_Languages); m_Languages = nullptr; } } static const VECTOR *M_Language_GetLanguages(void) { if (m_Languages == nullptr) { // Initialize available languages for the language option. m_Languages = GameStringManager_GetAvailableLanguages(); atexit(M_Language_Cleanup); } return m_Languages; } static int32_t M_Language_FindIndex(const UI_SETTINGS_OPTION *const option) { const VECTOR *const langs = M_Language_GetLanguages(); const char *const cur = *(char **)option->target; for (int32_t i = 0; i < langs->count; i++) { const char *const lang = *(char **)Vector_Get(langs, i); if (String_Equivalent(lang, cur)) { return i; } } return -1; } const char *UI_Settings_Language_FormatValue( const UI_SETTINGS_OPTION *const option) { const char *const code = *(const char **)option->target; const char *const name = GameStringManager_GetLanguageName(code); return name != nullptr ? name : code; } bool UI_Settings_Language_CanChangeValue( const UI_SETTINGS_OPTION *const option, const int32_t dir) { const VECTOR *const langs = M_Language_GetLanguages(); const int32_t idx = M_Language_FindIndex(option); if (idx < 0) { // If the language from the user config somehow is no longer on the list // (the file was deleted), let the player return to the default language return true; } if (langs->count < 2) { return false; } return idx + dir >= 0 && idx + dir < langs->count; } bool UI_Settings_Language_RequestChangeValue( const UI_SETTINGS_OPTION *const option, const int32_t dir) { const VECTOR *const langs = M_Language_GetLanguages(); if (!UI_Settings_Language_CanChangeValue(option, dir)) { return false; } const char *new_lang; const int32_t idx = M_Language_FindIndex(option); if (idx != -1) { new_lang = *(char **)Vector_Get(langs, idx + dir); } else { // If the language from the user config somehow is no longer on the list // (the file was deleted), default to the first entry, which is English new_lang = *(char **)Vector_Get(langs, 0); } Config_SetOptionValueFromString(Config_GetOption(option->target), new_lang); GameStringManager_ReloadLanguage(new_lang); return true; } ================================================ FILE: src/trx/game/ui/dialogs/setting_tabs/gameplay_controls.def ================================================ X_UI_CFG(input.enable_responsive_passport) X_UI_CFG(gameplay.enable_walk_to_items) X_UI_CFG(input.enable_tr3_sidesteps) X_UI_CFG_ENUM(input.quick_guns_mode, .misc = UI_Settings_QuickGunsModeEnumEntries) X_UI_CFG_ENUM(gameplay.look_mode, .misc = UI_Settings_LookModeEnumEntries) X_UI_CFG(gameplay.enable_manual_camera) X_UI_CFG(gameplay.idle_pose_timeout, .min_value = 0, .max_value = 1200) X_UI_CFG(gameplay.enable_idle_pose_camera, .custom_handler = { .is_available = UI_Settings_IdlePose_IsAvailable }) X_UI_CFG(gameplay.enable_tr2_jumping) X_UI_CFG_ENUM(gameplay.jump_lock_mode, .misc = UI_Settings_JumpLockModeEnumEntries, .custom_handler = { .is_available = UI_Settings_ResponsiveJumping_IsAvailable }) X_UI_CFG(gameplay.enable_jump_twists) X_UI_CFG(gameplay.enable_crawling) X_UI_CFG(gameplay.enable_responsive_crawl, .custom_handler = { .is_available = UI_Settings_Crawl_IsAvailable }) X_UI_CFG(gameplay.enable_crawl_jump, .custom_handler = { .is_available = UI_Settings_Crawl_IsAvailable }) X_UI_CFG(gameplay.enable_crawl_tilt, .custom_handler = { .is_available = UI_Settings_Crawl_IsAvailable }) X_UI_CFG(gameplay.enable_crouch_roll, .custom_handler = { .is_available = UI_Settings_Crawl_IsAvailable }) X_UI_CFG(gameplay.enable_toggle_crouch, .custom_handler = { .is_available = UI_Settings_Crawl_IsAvailable }) X_UI_CFG(gameplay.enable_sprint) X_UI_CFG(gameplay.enable_responsive_sprint, .custom_handler = { .is_available = UI_Settings_Sprint_IsAvailable }) X_UI_CFG(gameplay.enable_toggle_sprint, .custom_handler = { .is_available = UI_Settings_Sprint_IsAvailable }) X_UI_CFG(gameplay.enable_neutral_twists) X_UI_CFG(gameplay.enable_slide_to_run) X_UI_CFG(gameplay.enable_back_slope_stumble) X_UI_CFG(gameplay.enable_lean_jumping) X_UI_CFG(gameplay.enable_slow_ledge_swing) X_UI_CFG(gameplay.enable_swing_cancel) X_UI_CFG(gameplay.enable_controlled_drops) X_UI_CFG(gameplay.enable_ledge_jumps) X_UI_CFG(gameplay.enable_smooth_wall_deflect) X_UI_CFG(gameplay.enable_soft_statics) X_UI_CFG(gameplay.enable_step_roll_boost) X_UI_CFG(gameplay.enable_uw_roll) X_UI_CFG(gameplay.enable_tr2_swimming) X_UI_CFG(gameplay.enable_tr2_swim_cancel) X_UI_CFG(gameplay.enable_wading) X_UI_CFG_ENUM(gameplay.target_mode, .misc = UI_Settings_TargetModeEnumEntries) X_UI_CFG(gameplay.enable_target_change) X_UI_CFG(gameplay.enable_inverted_look) X_UI_CFG(gameplay.camera_speed, .min_value = 1, .max_value = 10) X_UI_CFG(input.enable_buffering_func_keys) X_UI_CFG(input.enable_buffering_inventory) ================================================ FILE: src/trx/game/ui/dialogs/setting_tabs/gameplay_fixes.def ================================================ X_UI_CFG(gameplay.fix_floor_data_issues) X_UI_CFG(visuals.fix_texture_issues) X_UI_CFG(visuals.fix_item_rots, .custom_handler = { .is_available = UI_Settings_FixItemRots_IsAvailable }) X_UI_CFG(audio.load_music_triggers) X_UI_CFG(visuals.fix_animated_sprites) X_UI_CFG(gameplay.fix_bridge_collision) X_UI_CFG(gameplay.fix_walk_run_jump) X_UI_CFG(gameplay.fix_flare_throw_priority, .custom_handler = { .is_available = UI_Settings_Flare_IsAvailable }) X_UI_CFG(gameplay.fix_m16_accuracy, .custom_handler = { .is_available = UI_Settings_M16_IsAvailable }) X_UI_CFG(gameplay.fix_descending_glitch) X_UI_CFG(gameplay.fix_wall_geometry) X_UI_CFG_ENUM(gameplay.wall_glitch_mode, .misc = UI_Settings_WallGlitchEnumEntries) X_UI_CFG(gameplay.fix_wade_wall_hit, .custom_handler = { .is_available = UI_Settings_FixWadeWallHit_IsAvailable }) X_UI_CFG(gameplay.fix_water_exit) X_UI_CFG(gameplay.fix_qwop_glitch) X_UI_CFG(gameplay.fix_step_glitch, .custom_handler = { .is_available = UI_Settings_FixStepGlitch_IsAvailable }) X_UI_CFG(gameplay.fix_item_duplication_glitch) X_UI_CFG(gameplay.fix_lara_pickup_embed) X_UI_CFG(gameplay.fix_free_flare_glitch, .custom_handler = { .is_available = UI_Settings_Flare_IsAvailable }) X_UI_CFG(gameplay.fix_alligator_ai) X_UI_CFG(gameplay.fix_bear_ai) X_UI_CFG(gameplay.fix_monkey_pickup_priority) X_UI_CFG(gameplay.fix_pipeman_aim) X_UI_CFG(audio.fix_chainblock_secret_sound) ================================================ FILE: src/trx/game/ui/dialogs/setting_tabs/gameplay_general.def ================================================ X_UI_CFG(gameplay.enable_legal) X_UI_CFG(gameplay.enable_credits) X_UI_CFG(gameplay.enable_fmv) X_UI_CFG(gameplay.enable_demo) X_UI_CFG(gameplay.enable_cinematics) X_UI_CFG(gameplay.enable_cutscenes) X_UI_CFG_ENUM(gameplay.loading_screens, .misc = UI_Settings_LoadingScreensModeEnumEntries) X_UI_CFG(gameplay.enable_game_modes) X_UI_CFG(gameplay.enable_play_previous_levels) X_UI_CFG(gameplay.enable_save_crystals) X_UI_CFG(gameplay.enable_auto_item_selection) X_UI_CFG(gameplay.enable_item_examining) X_UI_CFG(gameplay.enable_compass_stats) X_UI_CFG(gameplay.enable_total_stats) X_UI_CFG(gameplay.enable_timer_in_inventory) X_UI_CFG(gameplay.pause_on_focus_lost) X_UI_CFG(gameplay.maximum_save_slots, .min_value = 1, .max_value = 1000, .delta_fast = 10, .delta_slow = 1) X_UI_CFG(gameplay.maximum_quick_save_slots, .min_value = 0, .max_value = 1000, .delta_fast = 10, .delta_slow = 1) X_UI_CFG(gameplay.enable_enhanced_saves) X_UI_CFG(gameplay.enable_bouncy_grenades, .custom_handler = { .is_available = UI_Settings_Grenade_IsAvailable }) X_UI_CFG(gameplay.remember_gun_status) X_UI_CFG(gameplay.restore_ps1_enemies) X_UI_CFG(gameplay.change_pierre_spawn) X_UI_CFG(gameplay.disable_trex_collision) X_UI_CFG(gameplay.enable_enemy_rotation) X_UI_CFG(gameplay.enable_body_bags) X_UI_CFG(gameplay.enable_killer_pushblocks) X_UI_CFG(gameplay.enable_boulder_shake) X_UI_CFG(gameplay.enable_ally_targeting) X_UI_CFG_ENUM(gameplay.ally_hostility_policy, .misc = UI_Settings_AllyHostilityPolicyEnumEntries) X_UI_CFG_ENUM(gameplay.creature_drown_policy, .misc = UI_Settings_CreatureDrownPolicyEnumEntries) ================================================ FILE: src/trx/game/ui/dialogs/setting_tabs/gameplay_mods.def ================================================ X_UI_CFG(gameplay.enable_cheats) X_UI_CFG(gameplay.enable_console) X_UI_CFG(gameplay.harpoon_recoil, .min_value = 0, .max_value = 1000, .delta_slow = 1, .delta_fast = 1, .custom_handler = { .is_available = UI_Settings_Harpoon_IsAvailable }) X_UI_CFG(gameplay.start_lara_hitpoints, .min_value = 1, .max_value = LARA_MAX_HITPOINTS, .delta_slow = 10, .delta_fast = 100) X_UI_CFG(gameplay.disable_healing_between_levels) X_UI_CFG(gameplay.disable_medpacks) X_UI_CFG(gameplay.disable_extra_guns) X_UI_CFG(debug.enable_endless_sprint) X_UI_CFG(debug.enable_endless_flare_time, .custom_handler = { .is_available = UI_Settings_Flare_IsAvailable }) X_UI_CFG_ENUM(gameplay.projectile_area_damage, .misc = UI_Settings_ProjectileAreaDamageEnumEntries, .custom_handler = { .is_available = UI_Settings_ProjectileAreaDamage_IsAvailable }) ================================================ FILE: src/trx/game/ui/dialogs/setting_tabs/graphic_rendering.def ================================================ X_UI_CFG(rendering.fps, .min_value = 30, .max_value = 60, .delta_slow = 30, .delta_fast = 30) X_UI_CFG(rendering.enable_trapezoid_filter) X_UI_CFG_ENUM(rendering.texture_filter, .misc = UI_Settings_TextureFilterEnumEntries) X_UI_CFG_ENUM(rendering.ui_filter, .misc = UI_Settings_TextureFilterEnumEntries) X_UI_CFG(rendering.anisotropy_filter, .min_value = 1 * 100, .max_value = 32 * 100, .delta_slow = 10, .delta_fast = 100) X_UI_CFG(rendering.enable_vsync) X_UI_CFG(visuals.game_brightness, .min_value = CONFIG_MIN_BRIGHTNESS * 100, .max_value = CONFIG_MAX_BRIGHTNESS * 100, .delta_slow = 1, .delta_fast = 5) X_UI_CFG(visuals.ui_brightness, .min_value = CONFIG_MIN_BRIGHTNESS * 100, .max_value = CONFIG_MAX_BRIGHTNESS * 100, .delta_slow = 1, .delta_fast = 5) X_UI_CFG(visuals.gamma, .min_value = CONFIG_MIN_GAMMA * 100, .max_value = CONFIG_MAX_GAMMA * 100, .delta_slow = 10, .delta_fast = 50) X_UI_CFG_ENUM(rendering.lighting_contrast, .misc = UI_Settings_LightingContrastEnumEntries) X_UI_CFG_ENUM(rendering.sprite_lock_mode, .misc = UI_Settings_SpriteLockModeEnumEntries) X_UI_CFG_ENUM(rendering.aspect_mode, .misc = UI_Settings_AspectModeEnumEntries) X_UI_CFG_ENUM(rendering.upscaling_filter, .misc = UI_Settings_TextureFilterEnumEntries) X_UI_CFG(rendering.upscaling_factor, .min_value = 1, .max_value = 8, .delta_slow = 1, .delta_fast = 1) X_UI_CFG(rendering.borders, .min_value = 0, .max_value = 45, .delta_slow = 1, .delta_fast = 5) X_UI_CFG_ENUM(rendering.screenshot_format, .misc = UI_Settings_ScreenshotFormatEnumEntries) ================================================ FILE: src/trx/game/ui/dialogs/setting_tabs/graphic_ui.def ================================================ X_UI_CFG(language, .custom_handler = { .format_value = UI_Settings_Language_FormatValue, .can_change_value = UI_Settings_Language_CanChangeValue, .request_change_value = UI_Settings_Language_RequestChangeValue }) X_UI_CFG(ui.text_scale, .min_value = 50, .max_value = 200, .delta_slow = 1, .delta_fast = 5) X_UI_CFG(ui.pickup_scale, .min_value = 50, .max_value = 200, .delta_slow = 1, .delta_fast = 5) X_UI_CFG(ui.show_pickups_overlay) X_UI_CFG(ui.show_title_version) X_UI_CFG(ui.enable_wraparound) X_UI_CFG_ENUM(ui.menu_style, .misc = UI_Settings_MenuStyleEnumEntries) X_UI_CFG_ENUM(ui.inventory_background_style, .misc = UI_Settings_BackgroundStyleEnumEntries, .custom_handler = { .is_enum_value_available = UI_Settings_BackgroundStyle_IsEnumValueAvailable }) X_UI_CFG(ui.inventory_fade_effects, .custom_handler = { .is_available = UI_Settings_EnableFadeEffects_IsAvailable }) X_UI_CFG_ENUM(ui.pause_background_style, .misc = UI_Settings_BackgroundStyleEnumEntries, .custom_handler = { .is_enum_value_available = UI_Settings_BackgroundStyle_IsEnumValueAvailable }) X_UI_CFG(ui.pause_fade_effects, .custom_handler = { .is_available = UI_Settings_EnableFadeEffects_IsAvailable }) X_UI_CFG_ENUM(ui.stats_background_style, .misc = UI_Settings_BackgroundStyleEnumEntries, .custom_handler = { .is_enum_value_available = UI_Settings_BackgroundStyle_IsEnumValueAvailable }) X_UI_CFG(ui.stats_fade_effects, .custom_handler = { .is_available = UI_Settings_EnableFadeEffects_IsAvailable }) X_UI_CFG(visuals.enable_fade_effects) X_UI_CFG(visuals.enable_exit_fade_effects, .custom_handler = { .is_available = UI_Settings_EnableFadeEffects_IsAvailable }) X_UI_CFG_ENUM(ui.ammo_counter.location, .misc = UI_Settings_UIElementLocationEnumEntries) ================================================ FILE: src/trx/game/ui/dialogs/setting_tabs/graphic_ui_bars.def ================================================ X_UI_CFG(ui.show_bars) X_UI_CFG(ui.bar_scale, .min_value = 50, .max_value = 200, .delta_slow = 1, .delta_fast = 5, .custom_handler = { .is_available = UI_Settings_Bar_IsAvailable }) X_UI_CFG_DYN_ENUM(ui.bar_look, .custom_handler = { .is_available = UI_Settings_Bar_IsAvailable }) X_UI_CFG(ui.enable_smooth_bars, .custom_handler = { .is_available = UI_Settings_Bar_IsAvailable }) X_UI_CFG(ui.enable_bar_flashing, .custom_handler = { .is_available = UI_Settings_Bar_IsAvailable }) X_UI_CFG_DYN_ENUM(ui.lara_health_bar.color, .custom_handler = { .is_available = UI_Settings_HealthbarColor_IsAvailable, .is_visible = UI_Settings_BarColorPC_IsVisible }) X_UI_CFG_DYN_ENUM(ui.lara_health_bar.color_ps1, .custom_handler = { .is_available = UI_Settings_HealthbarColor_IsAvailable, .is_visible = UI_Settings_BarColorPS1_IsVisible }) X_UI_CFG_DYN_ENUM(ui.lara_health_bar.poison_color, .custom_handler = { .is_available = UI_Settings_HealthbarColor_IsAvailable, .is_visible = UI_Settings_BarColorPC_IsVisible }) X_UI_CFG_DYN_ENUM(ui.lara_health_bar.poison_color_ps1, .custom_handler = { .is_available = UI_Settings_HealthbarColor_IsAvailable, .is_visible = UI_Settings_BarColorPS1_IsVisible }) X_UI_CFG_DYN_ENUM(ui.lara_air_bar.color, .custom_handler = { .is_available = UI_Settings_AirbarColor_IsAvailable, .is_visible = UI_Settings_BarColorPC_IsVisible }) X_UI_CFG_DYN_ENUM(ui.lara_air_bar.color_ps1, .custom_handler = { .is_available = UI_Settings_AirbarColor_IsAvailable, .is_visible = UI_Settings_BarColorPS1_IsVisible }) X_UI_CFG_DYN_ENUM(ui.lara_sprint_bar.color, .custom_handler = { .is_available = UI_Settings_SprintbarColor_IsAvailable, .is_visible = UI_Settings_BarColorPC_IsVisible }) X_UI_CFG_DYN_ENUM(ui.lara_sprint_bar.color_ps1, .custom_handler = { .is_available = UI_Settings_SprintbarColor_IsAvailable, .is_visible = UI_Settings_BarColorPS1_IsVisible }) X_UI_CFG_DYN_ENUM(ui.lara_exposure_bar.color, .custom_handler = { .is_available = UI_Settings_ExposurebarColor_IsAvailable, .is_visible = UI_Settings_BarColorPC_IsVisible }) X_UI_CFG_DYN_ENUM(ui.lara_exposure_bar.color_ps1, .custom_handler = { .is_available = UI_Settings_ExposurebarColor_IsAvailable, .is_visible = UI_Settings_BarColorPS1_IsVisible }) X_UI_CFG_DYN_ENUM(ui.enemy_health_bar.color, .custom_handler = { .is_available = UI_Settings_EnemyHealthbarColor_IsAvailable, .is_visible = UI_Settings_BarColorPC_IsVisible }) X_UI_CFG_DYN_ENUM(ui.enemy_health_bar.color_ps1, .custom_handler = { .is_available = UI_Settings_EnemyHealthbarColor_IsAvailable, .is_visible = UI_Settings_BarColorPS1_IsVisible }) X_UI_CFG_DYN_ENUM(ui.enemy_health_bar.color_allies, .custom_handler = { .is_available = UI_Settings_AllyHealthbarColor_IsAvailable, .is_visible = UI_Settings_BarColorPC_IsVisible }) X_UI_CFG_DYN_ENUM(ui.enemy_health_bar.color_allies_ps1, .custom_handler = { .is_available = UI_Settings_AllyHealthbarColor_IsAvailable, .is_visible = UI_Settings_BarColorPS1_IsVisible }) X_UI_CFG_ENUM(ui.enemy_health_bar.show_mode, .misc = UI_Settings_EnemyHealthBarShowModeEnumEntries, .custom_handler = { .is_available = UI_Settings_Bar_IsAvailable }) X_UI_CFG_ENUM(ui.lara_health_bar.location, .misc = UI_Settings_UIElementLocationEnumEntries, .custom_handler = { .is_available = UI_Settings_Healthbar_IsAvailable }) X_UI_CFG_ENUM(ui.lara_air_bar.location, .misc = UI_Settings_UIElementLocationEnumEntries, .custom_handler = { .is_available = UI_Settings_Airbar_IsAvailable }) X_UI_CFG_ENUM(ui.lara_sprint_bar.location, .misc = UI_Settings_UIElementLocationEnumEntries, .custom_handler = { .is_available = UI_Settings_Sprintbar_IsAvailable }) X_UI_CFG_ENUM(ui.lara_exposure_bar.location, .misc = UI_Settings_UIElementLocationEnumEntries, .custom_handler = { .is_available = UI_Settings_Exposurebar_IsAvailable }) X_UI_CFG_ENUM(ui.enemy_health_bar.location, .misc = UI_Settings_UIElementLocationEnumEntries, .custom_handler = { .is_available = UI_Settings_EnemyHealthbar_IsAvailable }) ================================================ FILE: src/trx/game/ui/dialogs/setting_tabs/graphic_ui_stats.def ================================================ X_UI_CFG_ENUM(ui.stats.style, .misc = UI_Settings_StatsStyleEnumEntries) X_UI_CFG(ui.stats.show_totals) X_UI_CFG(ui.stats.show_level_header) X_UI_CFG(ui.stats.show_time_taken) X_UI_CFG(ui.stats.show_secrets) X_UI_CFG(ui.stats.show_crystals, .custom_handler = { .is_available = UI_Settings_ShowCrystals_IsAvailable }) X_UI_CFG(ui.stats.show_pickups) X_UI_CFG(ui.stats.show_kills) X_UI_CFG(ui.stats.show_ammo) X_UI_CFG(ui.stats.show_medipacks_used) X_UI_CFG(ui.stats.show_distance_travelled) X_UI_CFG(ui.stats.show_deaths) ================================================ FILE: src/trx/game/ui/dialogs/setting_tabs/graphic_visuals.def ================================================ X_UI_CFG(visuals.fog_start, .min_value = 1, .max_value = 100, .delta_slow = 1, .delta_fast = 10) X_UI_CFG(visuals.fog_end, .min_value = 1, .max_value = 100, .delta_slow = 1, .delta_fast = 10) X_UI_CFG(visuals.fog_transparency) X_UI_CFG_RGB888(visuals.fog_color, .custom_handler = { .format_value = UI_Settings_ColorEditor_FormatValue, .can_change_value = UI_Settings_ColorEditor_CanChangeValue, .is_available = UI_Settings_FogColor_IsAvailable }) X_UI_CFG_RGB888(visuals.water_color, .custom_handler = { .format_value = UI_Settings_ColorEditor_FormatValue, .can_change_value = UI_Settings_ColorEditor_CanChangeValue }) X_UI_CFG_ENUM(visuals.camera_mode, .misc = UI_Settings_CameraModeEnumEntries) X_UI_CFG(visuals.enable_glide_cameras) X_UI_CFG(visuals.fov, .min_value = 30, .max_value = 150, .delta_slow = 1, .delta_fast = 5) X_UI_CFG(visuals.enable_reflections) X_UI_CFG(visuals.enable_skybox) X_UI_CFG(visuals.enable_weather) X_UI_CFG(visuals.enable_footprints) X_UI_CFG(visuals.enable_responsive_mesh_tint) X_UI_CFG_ENUM(visuals.blood_effects, .misc = UI_Settings_BloodEffectsEnumEntries) X_UI_CFG_DYN_ENUM(visuals.lara_outfit) X_UI_CFG(visuals.enable_braid) X_UI_CFG_ENUM(visuals.sunglasses_mode, .misc = UI_Settings_SunglassesModeEnumEntries) X_UI_CFG(visuals.enable_breeze, .custom_handler = { .is_available = UI_Settings_EnableBreeze_IsAvailable }) X_UI_CFG(visuals.enable_3d_pickups) X_UI_CFG(gameplay.enable_pickup_aids) X_UI_CFG(visuals.enable_ps1_crystals, .custom_handler = { .is_available = UI_Settings_EnablePS1Crystals_IsAvailable }) X_UI_CFG_ENUM(visuals.shadow_type, .misc = UI_Settings_ShadowTypeEnumEntries, .custom_handler = { .is_enum_value_available = UI_Settings_ShadowType_IsEnumValueAvailable }) X_UI_CFG(visuals.enable_gun_lighting) X_UI_CFG(visuals.enable_fire_lighting) X_UI_CFG(visuals.enable_shotgun_flash) ================================================ FILE: src/trx/game/ui/dialogs/setting_tabs/sound_misc.def ================================================ X_UI_CFG(audio.enable_music_in_menu) X_UI_CFG(audio.fix_secrets_killing_music) X_UI_CFG(audio.fix_speeches_killing_music) X_UI_CFG(audio.enable_music_in_inventory) X_UI_CFG_ENUM(audio.music_load_condition, .misc = UI_Settings_MusicLoadConditionEnumEntries) X_UI_CFG(audio.enable_underwater_anim_sfx) X_UI_CFG(audio.mute_out_of_focus) X_UI_CFG(audio.enable_pitched_sounds) X_UI_CFG(audio.enable_ps1_sfx) X_UI_CFG(audio.enable_lara_mic) ================================================ FILE: src/trx/game/ui/dialogs/setting_tabs/sound_volume.def ================================================ X_UI_CFG(audio.master_volume, .min_value = 0, .max_value = 100, .delta_slow = 1, .delta_fast = 10, .custom_handler = { .request_change_value = UI_Settings_Volume_RequestChange }) X_UI_CFG(audio.sound_volume, .min_value = 0, .max_value = 100, .delta_slow = 1, .delta_fast = 10, .custom_handler = { .request_change_value = UI_Settings_Volume_RequestChange }) X_UI_CFG(audio.music_volume, .min_value = 0, .max_value = 100, .delta_slow = 1, .delta_fast = 10, .custom_handler = { .request_change_value = UI_Settings_Volume_RequestChange }) X_UI_CFG(audio.inventory_music_volume, .min_value = 0, .max_value = 100, .delta_slow = 1, .delta_fast = 10, .custom_handler = { .request_change_value = UI_Settings_Volume_RequestChange, .is_available = UI_Settings_PauseMusicInInventory_IsAvailable }) X_UI_CFG(audio.underwater_music_volume, .min_value = 0, .max_value = 100, .delta_slow = 1, .delta_fast = 10, .custom_handler = { .request_change_value = UI_Settings_Volume_RequestChange }) X_UI_CFG(audio.ambient_volume, .min_value = 0, .max_value = 100, .delta_slow = 1, .delta_fast = 10, .custom_handler = { .request_change_value = UI_Settings_Volume_RequestChange }) X_UI_CFG(audio.inventory_ambient_volume, .min_value = 0, .max_value = 100, .delta_slow = 1, .delta_fast = 10, .custom_handler = { .request_change_value = UI_Settings_Volume_RequestChange, .is_available = UI_Settings_PauseMusicInInventory_IsAvailable}) X_UI_CFG(audio.underwater_ambient_volume, .min_value = 0, .max_value = 100, .delta_slow = 1, .delta_fast = 10, .custom_handler = { .request_change_value = UI_Settings_Volume_RequestChange }) X_UI_CFG(audio.cutscene_volume, .min_value = 0, .max_value = 100, .delta_slow = 1, .delta_fast = 10, .custom_handler = { .request_change_value = UI_Settings_Volume_RequestChange }) X_UI_CFG(audio.fmv_volume, .min_value = 0, .max_value = 100, .delta_slow = 1, .delta_fast = 10, .custom_handler = { .request_change_value = UI_Settings_Volume_RequestChange }) ================================================ FILE: src/trx/game/ui/dialogs/settings.c ================================================ #include #include #include #include #include #include #include #include #include #include typedef struct UI_SETTINGS_DIALOG_STATE { UI_SETTINGS_PHASE phase; int32_t visible_rows; float max_content_width; float max_content_height; int32_t tab_count; UI_SETTINGS_TAB *tabs; UI_TAB_SWITCH_STATE *tab_switch; GAME_STRING_ID title; int32_t listener_id; } UI_SETTINGS_DIALOG_STATE; static int32_t M_GetVisibleRows(void) { const int32_t res_h = UI_Scaler_CalcInverse( Viewport_GetHeight(VIEWPORT_UI), UI_SCALER_TARGET_TEXT); static struct { int32_t threshold; int32_t rows; } thresholds[] = { { 240, 5 }, { 252, 6 }, { 266, 7 }, { 282, 8 }, { 300, 9 }, { 320, 10 }, { 342, 11 }, { 370, 12 }, { 420, 13 }, { 480, 15 }, { -1, 16 }, }; for (int32_t i = 0;; i++) { if (res_h <= thresholds[i].threshold || thresholds[i].threshold == -1) { return thresholds[i].rows; } } } static UI_SETTINGS_TAB *M_GetActiveTab(UI_SETTINGS_DIALOG_STATE *const s) { if (s->tab_switch == nullptr || s->tabs == nullptr || s->tab_count <= 0) { return nullptr; } const int32_t idx = s->tab_switch->active_tab_idx; if (idx < 0 || idx >= s->tab_count) { return nullptr; } return &s->tabs[idx]; } static const UI_SETTINGS_TAB *M_GetActiveTabConst( const UI_SETTINGS_DIALOG_STATE *const s) { if (s->tab_switch == nullptr || s->tabs == nullptr || s->tab_count <= 0) { return nullptr; } const int32_t idx = s->tab_switch->active_tab_idx; if (idx < 0 || idx >= s->tab_count) { return nullptr; } return &s->tabs[idx]; } static UI_SCROLLABLE *M_GetTabScrollable(UI_SETTINGS_TAB *const tab) { if (tab == nullptr || tab->ops == nullptr || tab->ops->get_scrollable == nullptr) { return nullptr; } return tab->ops->get_scrollable(tab->user_data); } static void M_RecomputeSizes(UI_SETTINGS_DIALOG_STATE *const s) { int32_t max_item_count = 0; for (int32_t i = 0; i < s->tab_count; i++) { UI_SETTINGS_TAB *const tab = &s->tabs[i]; if (tab->ops != nullptr && tab->ops->get_item_count != nullptr) { max_item_count = MAX(max_item_count, tab->ops->get_item_count(tab->user_data)); } } const int32_t visible_rows = MIN(max_item_count, M_GetVisibleRows()); float max_content_width = 0.0f; float max_content_height = -1.0f; for (int32_t i = 0; i < s->tab_count; i++) { UI_SETTINGS_TAB *const tab = &s->tabs[i]; if (tab->ops != nullptr && tab->ops->recompute != nullptr) { tab->ops->recompute(tab->user_data, visible_rows); } if (tab->ops != nullptr && tab->ops->get_content_width != nullptr) { max_content_width = MAX( max_content_width, tab->ops->get_content_width(tab->user_data)); } if (tab->ops != nullptr) { const UI_SCROLLABLE *const tab_scroll = M_GetTabScrollable(tab); const int32_t tab_visible_rows = tab_scroll != nullptr ? tab_scroll->vis_items : visible_rows; float tab_content_height = -1.0f; if (tab->ops->get_content_height != nullptr) { tab_content_height = tab->ops->get_content_height(tab->user_data); } else if (tab_visible_rows > 0) { tab_content_height = tab_visible_rows * UI_TEXT_HEIGHT; } max_content_height = MAX(max_content_height, tab_content_height); } } s->visible_rows = visible_rows; s->max_content_width = max_content_width / g_Config.ui.text_scale; s->max_content_height = max_content_height; } static void M_WindowHeader(void *const user_data) { UI_SETTINGS_DIALOG_STATE *const s = user_data; if (s->tab_switch != nullptr && s->tab_count > 0) { UI_TabSwitch( s->tab_switch, s->phase == UI_SETTINGS_PHASE_NAVIGATE_TABS); UI_Spacer(0.0f, 8.0f); } } static void M_HandleLanguageReload(const EVENT *const, void *const data) { UI_SETTINGS_DIALOG_STATE *const s = data; M_RecomputeSizes(s); } static UI_SETTINGS_DIALOG_STATE *M_InitCommon(const GAME_STRING_ID title) { UI_SETTINGS_DIALOG_STATE *const s = Memory_Alloc(sizeof(*s)); s->title = title; s->listener_id = GameStringManager_SubscribeReload(M_HandleLanguageReload, s); return s; } static void M_SetActiveTab(UI_SETTINGS_DIALOG_STATE *const s, const int32_t idx) { s->tab_switch->active_tab_idx = idx; M_RecomputeSizes(s); } static void M_EnterEditMode( UI_SETTINGS_DIALOG_STATE *const s, const bool focus_last) { s->phase = UI_SETTINGS_PHASE_EDIT_SETTINGS; UI_SETTINGS_TAB *const tab = M_GetActiveTab(s); UI_SCROLLABLE *const active_scroll = M_GetTabScrollable(tab); if (active_scroll != nullptr) { if (focus_last) { UI_Scrollable_SelectLastItem(active_scroll); } else { UI_Scrollable_SelectFirstItem(active_scroll); } } } static void M_ClearActiveCustomSelection(UI_SETTINGS_DIALOG_STATE *const s) { UI_SETTINGS_TAB *const tab = M_GetActiveTab(s); if (tab == nullptr || tab->ops == nullptr || tab->ops->get_content_height == nullptr) { return; } UI_SCROLLABLE *const active_scroll = M_GetTabScrollable(tab); if (active_scroll != nullptr) { UI_Scrollable_SelectItem(active_scroll, -1); } } UI_SETTINGS_DIALOG_STATE *UI_SettingsDialog_Init( const GAME_STRING_ID title, const int32_t tab_count, const UI_SETTINGS_TAB *const tabs) { ASSERT(tabs != nullptr); UI_SETTINGS_DIALOG_STATE *const s = M_InitCommon(title); int32_t visible_tab_count = 0; for (int32_t i = 0; i < tab_count; i++) { const UI_SETTINGS_TAB *const tab = &tabs[i]; int32_t item_count = 0; if (tab->ops != nullptr && tab->ops->get_item_count != nullptr) { item_count = tab->ops->get_item_count(tab->user_data); } const bool is_list_tab = tab->ops != nullptr && tab->ops->get_content_height == nullptr; if (is_list_tab && item_count <= 0) { continue; } visible_tab_count++; } UI_TAB_SWITCH_TAB tab_switch_tabs[tab_count]; UI_SETTINGS_TAB *visible_tabs = nullptr; if (visible_tab_count > 0) { visible_tabs = Memory_Alloc(sizeof(UI_SETTINGS_TAB) * visible_tab_count); } int32_t vt = 0; for (int32_t i = 0; i < tab_count; i++) { const UI_SETTINGS_TAB *const tab = &tabs[i]; int32_t item_count = 0; if (tab->ops != nullptr && tab->ops->get_item_count != nullptr) { item_count = tab->ops->get_item_count(tab->user_data); } const bool is_list_tab = tab->ops != nullptr && tab->ops->get_content_height == nullptr; if (is_list_tab && item_count <= 0) { continue; } visible_tabs[vt] = tabs[i]; tab_switch_tabs[vt].header.one_off = nullptr; tab_switch_tabs[vt].header.live_ptr = GameString_GetPtr(tabs[i].header_gs); vt++; } s->tabs = visible_tabs; s->tab_count = visible_tab_count; s->tab_switch = UI_TabSwitch_Init(s->tab_count, tab_switch_tabs); s->phase = UI_SETTINGS_PHASE_NAVIGATE_TABS; if (s->tab_count > 0) { M_SetActiveTab(s, 0); } else { M_RecomputeSizes(s); } return s; } void UI_SettingsDialog_Free(UI_SETTINGS_DIALOG_STATE *const s) { if (s->listener_id >= 0) { GameStringManager_UnsubscribeReload(s->listener_id); s->listener_id = -1; } if (s->tab_switch != nullptr) { UI_TabSwitch_Free(s->tab_switch); s->tab_switch = nullptr; } if (s->tabs != nullptr) { for (int32_t i = 0; i < s->tab_count; i++) { if (s->tabs[i].ops != nullptr && s->tabs[i].ops->free != nullptr && s->tabs[i].user_data != nullptr) { s->tabs[i].ops->free(s->tabs[i].user_data); } } Memory_FreePointer(&s->tabs); } Memory_Free(s); } bool UI_SettingsDialog_Control(UI_SETTINGS_DIALOG_STATE *const s) { M_RecomputeSizes(s); if (s->phase == UI_SETTINGS_PHASE_NAVIGATE_TABS) { if (UI_TabSwitch_Control(s->tab_switch, UI_TAB_SWITCH_NORMAL)) { M_SetActiveTab(s, s->tab_switch->active_tab_idx); return false; } if (g_InputDB.menu_down || g_InputDB.menu_confirm) { if (!g_InputDB.menu_confirm) { M_EnterEditMode(s, false); } } else if (g_InputDB.menu_up && g_Config.ui.enable_wraparound) { M_EnterEditMode(s, true); } else if (g_InputDB.menu_back) { return true; } return false; } UI_SETTINGS_TAB *const tab = M_GetActiveTab(s); if (tab == nullptr || tab->ops == nullptr) { return g_InputDB.menu_back; } const bool consumed = tab->ops->control(tab->user_data, &s->phase); if (s->phase == UI_SETTINGS_PHASE_NAVIGATE_TABS) { M_ClearActiveCustomSelection(s); return false; } if (consumed) { return false; } if (s->tab_switch != nullptr && !g_Input.menu_left && !g_Input.menu_right && UI_TabSwitch_Control(s->tab_switch, UI_TAB_SWITCH_NO_ARROWS)) { M_SetActiveTab(s, s->tab_switch->active_tab_idx); return false; } if (g_InputDB.menu_back) { return true; } return false; } void UI_SettingsDialog(UI_SETTINGS_DIALOG_STATE *const s) { const UI_SETTINGS_TAB *const tab = M_GetActiveTabConst(s); UI_SCROLLABLE *const active_scroll = M_GetTabScrollable(M_GetActiveTab(s)); UI_BeginModal(0.5f, 0.6f); UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, .spacing = { .v = 5.0f }, .align = { .h = UI_STACK_H_ALIGN_SPAN }, }); UI_BeginWindow((UI_WINDOW_SETTINGS) { .title = GameString_Get(s->title), .scrollable = active_scroll, .title_spacing = -1.0f, .header_func = M_WindowHeader, .footer_func = nullptr, .user_data = s, .reserve_scroll_space = true, }); if (tab == nullptr || tab->ops == nullptr || tab->ops->draw == nullptr) { UI_BeginResize(-1.0f, -1.0f); UI_BeginPad( g_TRVersion == 1 ? -1.0f : 0.0f, g_TRVersion == 1 ? -1.0f : 0.0f); UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, .align = { .h = UI_STACK_H_ALIGN_CENTER }, }); UI_Label(GS("general/settings/common/all_hidden_disclaimer")); UI_EndStack(); UI_EndPad(); UI_EndResize(); } else { float content_width = s->max_content_width > 0.0f ? s->max_content_width : -1.0f; float content_height = s->max_content_height; UI_BeginResize(content_width, content_height); tab->ops->draw(tab->user_data, s->phase, s->max_content_width); UI_EndResize(); } UI_EndWindow(); if (tab != nullptr && tab->ops != nullptr && tab->ops->draw_footer != nullptr) { tab->ops->draw_footer(tab->user_data, s->phase); } else { UI_Spacer(0.0f, UI_TEXT_HEIGHT); } UI_EndStack(); UI_EndModal(); if (tab != nullptr && tab->ops != nullptr && tab->ops->draw_overlay != nullptr) { tab->ops->draw_overlay(tab->user_data); } } ================================================ FILE: src/trx/game/ui/dialogs/settings.h ================================================ #pragma once #include #include #include #include #include #include #include #include typedef struct { const char *(*format_value)(const struct UI_SETTINGS_OPTION *option); bool (*can_change_value)( const struct UI_SETTINGS_OPTION *option, int32_t dir); bool (*request_change_value)( const struct UI_SETTINGS_OPTION *option, int32_t dir); bool (*is_available)(const struct UI_SETTINGS_OPTION *option); bool (*is_visible)(const struct UI_SETTINGS_OPTION *option); bool (*is_enum_value_available)( const struct UI_SETTINGS_OPTION *option, int32_t value); } UI_SETTINGS_CUSTOM_OPITON_HANDLER; typedef struct UI_SETTINGS_OPTION { // A custom handler that must have all the function pointers filled, UI_SETTINGS_CUSTOM_OPITON_HANDLER custom_handler; // ...or a convenience default handler options struct { void *target; int32_t min_value; int32_t max_value; int32_t delta_slow; int32_t delta_fast; const void *misc; }; } UI_SETTINGS_OPTION; #define X_UI_CFG(TARGET_, ...) { .target = &g_Config.TARGET_, ##__VA_ARGS__ }, #define X_UI_CFG_DYN_ENUM(TARGET_, ...) \ X_UI_CFG(TARGET_, .delta_slow = 1, .delta_fast = 1, ##__VA_ARGS__) #define X_UI_CFG_ENUM(TARGET_, ...) \ X_UI_CFG(TARGET_, .delta_slow = 1, .delta_fast = 1, ##__VA_ARGS__) #define X_UI_CFG_RGB888(TARGET_, ...) \ X_UI_CFG(TARGET_, .min_value = 0, .max_value = 255, ##__VA_ARGS__) typedef struct UI_SETTINGS_DIALOG_STATE UI_SETTINGS_DIALOG_STATE; UI_SETTINGS_DIALOG_STATE *UI_SettingsDialog_Init( GAME_STRING_ID title, int32_t tab_count, const UI_SETTINGS_TAB *tabs); void UI_SettingsDialog_Free(UI_SETTINGS_DIALOG_STATE *s); bool UI_SettingsDialog_Control(UI_SETTINGS_DIALOG_STATE *s); void UI_SettingsDialog(UI_SETTINGS_DIALOG_STATE *s); ================================================ FILE: src/trx/game/ui/dialogs/settings_editor.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_BAR_WIDTH 60 #define M_BAR_HEIGHT 12 typedef struct { int32_t position; int32_t count; } M_ENUM_LOOKUP; typedef struct UI_SETTINGS_EDITOR_STATE { const UI_SETTINGS_OPTION *options; int32_t visible_rows; UI_SCROLLABLE scroll; struct { bool show; UI_TEXT_DIALOG_STATE *state; } description; UI_COLOR_EDITOR_DIALOG_STATE *color_editor; } UI_SETTINGS_EDITOR_STATE; static const CONFIG_OPTION *M_GetConfigOption( const UI_SETTINGS_OPTION *const option) { ASSERT(option != nullptr); ASSERT(option->target != nullptr); const CONFIG_OPTION *const result = Config_GetOption(option->target); ASSERT(result != nullptr); return result; } static const char *M_GetOptionDescription( const UI_SETTINGS_OPTION *const option) { if (option == nullptr || option->target == nullptr) { return nullptr; } return Config_GetOptionDescription(M_GetConfigOption(option)); } static const char *M_GetOptionTitle(const UI_SETTINGS_OPTION *const option) { if (option == nullptr || option->target == nullptr) { return ""; } const char *const result = Config_GetOptionTitle(M_GetConfigOption(option)); return result != nullptr ? result : ""; } static bool M_IsEnumEntryAvailable( const UI_SETTINGS_OPTION *const option, const UI_SETTINGS_ENUM_ENTRY *const entry) { if (entry == nullptr || entry->value == -1) { return false; } if (option->custom_handler.is_enum_value_available == nullptr) { return true; } return option->custom_handler.is_enum_value_available(option, entry->value); } static UI_BAR_TYPE M_GetBarType(const UI_SETTINGS_OPTION *const option) { if (option->target == &g_Config.ui.lara_health_bar.color || option->target == &g_Config.ui.lara_health_bar.color_ps1) { return UI_BAR_LARA_HP; } else if ( option->target == &g_Config.ui.lara_health_bar.poison_color || option->target == &g_Config.ui.lara_health_bar.poison_color_ps1) { return UI_BAR_LARA_HP_POISON; } else if ( option->target == &g_Config.ui.lara_air_bar.color || option->target == &g_Config.ui.lara_air_bar.color_ps1) { return UI_BAR_LARA_AIR; } else if ( option->target == &g_Config.ui.lara_sprint_bar.color || option->target == &g_Config.ui.lara_sprint_bar.color_ps1) { return UI_BAR_LARA_STAMINA; } else if ( option->target == &g_Config.ui.lara_exposure_bar.color || option->target == &g_Config.ui.lara_exposure_bar.color_ps1) { return UI_BAR_LARA_EXPOSURE; } else if ( option->target == &g_Config.ui.enemy_health_bar.color || option->target == &g_Config.ui.enemy_health_bar.color_ps1) { return UI_BAR_ENEMY_HP; } else if ( option->target == &g_Config.ui.enemy_health_bar.color_allies || option->target == &g_Config.ui.enemy_health_bar.color_allies_ps1) { return UI_BAR_ALLY_HP; } else { return (UI_BAR_TYPE)-1; } } static bool M_IsBarColorEnum(const UI_SETTINGS_OPTION *const option) { return M_GetBarType(option) != (UI_BAR_TYPE)-1; } static bool M_IsColorEditorOption(const UI_SETTINGS_OPTION *const option) { return option != nullptr && M_GetConfigOption(option)->type == COT_RGB888; } static bool M_HasAvailableEnumValue(const UI_SETTINGS_OPTION *const option) { const UI_SETTINGS_ENUM_ENTRY *entry = (UI_SETTINGS_ENUM_ENTRY *)option->misc; if (entry == nullptr) { return false; } while (entry->value != -1) { if (M_IsEnumEntryAvailable(option, entry)) { return true; } entry++; } return false; } static bool M_IsOptionHidden(const UI_SETTINGS_OPTION *const option) { if (option->custom_handler.is_visible != nullptr && !option->custom_handler.is_visible(option)) { return true; } if (Config_IsOptionHidden(option->target)) { return true; } if (M_GetConfigOption(option)->type == COT_ENUM && option->misc != nullptr && !M_HasAvailableEnumValue(option)) { return true; } return false; } static const UI_SETTINGS_OPTION *M_GetOptionByRow( const UI_SETTINGS_EDITOR_STATE *const s, const int32_t row_idx) { if (s->options == nullptr) { return nullptr; } int32_t count = 0; for (int32_t i = 0; s->options[i].target != nullptr; i++) { const UI_SETTINGS_OPTION *const opt = &s->options[i]; if (M_IsOptionHidden(opt)) { continue; } if (count == row_idx) { return opt; } count++; } return nullptr; } static int32_t M_GetRowCount(const UI_SETTINGS_EDITOR_STATE *const s) { if (s->options == nullptr) { return 0; } int32_t count = 0; for (int32_t i = 0; s->options[i].target != nullptr; i++) { if (!M_IsOptionHidden(&s->options[i])) { count++; } } return count; } static M_ENUM_LOOKUP M_GetEnumEntry(const UI_SETTINGS_OPTION *const option) { M_ENUM_LOOKUP result = { .position = -1, .count = 0, }; int32_t current_pos = 0; const UI_SETTINGS_ENUM_ENTRY *entry = &((UI_SETTINGS_ENUM_ENTRY *)option->misc)[0]; while (entry->value != -1) { if (entry->value == *(int32_t *)option->target) { result.position = current_pos; } entry++; current_pos++; result.count++; } return result; } static int32_t M_FindNextAvailableEnumPosition( const UI_SETTINGS_OPTION *const option, const M_ENUM_LOOKUP *const enum_lookup, const int32_t dir) { if (enum_lookup->position < 0 || enum_lookup->count <= 0 || dir == 0) { return -1; } const UI_SETTINGS_ENUM_ENTRY *const entries = option->misc; const int32_t step = dir < 0 ? -1 : 1; for (int32_t pos = enum_lookup->position + step; pos >= 0 && pos < enum_lookup->count; pos += step) { if (M_IsEnumEntryAvailable(option, &entries[pos])) { return pos; } } return -1; } static const char *M_FormatRowValue( const UI_SETTINGS_EDITOR_STATE *const s, const int32_t row_idx) { const UI_SETTINGS_OPTION *const option = M_GetOptionByRow(s, row_idx); if (option == nullptr) { return nullptr; } if (option->custom_handler.format_value != nullptr) { return option->custom_handler.format_value(option); } const CONFIG_OPTION *const cfg_opt = M_GetConfigOption(option); return Config_GetOptionValueAsString(cfg_opt, true); } static float M_MeasureMaxValueWidth(const UI_SETTINGS_OPTION *const option) { if (option->custom_handler.format_value != nullptr) { const char *const value = option->custom_handler.format_value(option); const float result = UI_Label_MeasureW(value); return result; } if (M_IsBarColorEnum(option)) { return M_BAR_WIDTH * g_Config.ui.text_scale; } switch (M_GetConfigOption(option)->type) { case COT_BOOL: { const float min_value_w = UI_Label_MeasureW(GS("general/misc/off")); const float max_value_w = UI_Label_MeasureW(GS("general/misc/on")); return MAX(min_value_w, max_value_w); } case COT_INT32: { const char *const min_value_s = String_FormatStatic("%d", option->min_value); const float min_value_w = UI_Label_MeasureW(min_value_s); const char *const max_value_s = String_FormatStatic("%d", option->max_value); const float max_value_w = UI_Label_MeasureW(max_value_s); return MAX(min_value_w, max_value_w); } case COT_DOUBLE: case COT_FLOAT: { const char *const min_value_s = String_FormatStatic("%.2f", (double)option->min_value / 100.0); const float min_value_w = UI_Label_MeasureW(min_value_s); const char *const max_value_s = String_FormatStatic("%.2f", (double)option->max_value / 100.0); const float max_value_w = UI_Label_MeasureW(max_value_s); return MAX(min_value_w, max_value_w); } case COT_FLOAT_PERCENT: { const char *const min_value_s = String_FormatStatic("%.00f%%", (double)option->min_value); const float min_value_w = UI_Label_MeasureW(min_value_s); const char *const max_value_s = String_FormatStatic("%.00f%%", (double)option->max_value); const float max_value_w = UI_Label_MeasureW(max_value_s); return MAX(min_value_w, max_value_w); } case COT_RGB888: return UI_Label_MeasureW("#FFFFFF") + 8.0f * g_Config.ui.text_scale + 32.0f * g_Config.ui.text_scale; case COT_STRING: return UI_Label_MeasureW(*(char **)option->target); case COT_DYNAMIC_ENUM: { const CONFIG_OPTION *const cfg_opt = M_GetConfigOption(option); float result = 0.0f; const int32_t count = Config_DynamicEnum_GetValueCount(cfg_opt); for (int32_t i = 0; i < count; i++) { const char *const label = Config_DynamicEnum_GetLabelAt(cfg_opt, i); result = MAX(result, UI_Label_MeasureW(label)); } return result; } case COT_ENUM: { const CONFIG_OPTION *const cfg_opt = M_GetConfigOption(option); float result = 0.0f; const UI_SETTINGS_ENUM_ENTRY *entry = option->misc; const int32_t current_value = *(int32_t *)option->target; while (entry->value != -1) { const bool is_current = entry->value == current_value; if (!is_current && !M_IsEnumEntryAvailable(option, entry)) { entry++; continue; } const char *const value = EnumMap_GetLabel(cfg_opt->param, entry->value); ASSERT(value != nullptr); const float value_w = UI_Label_MeasureW(value); result = MAX(result, value_w); entry++; } return result; } default: break; } return 0.0f; } static bool M_CanChangeValue( const UI_SETTINGS_EDITOR_STATE *const s, const int32_t row_idx, const int32_t dir) { const UI_SETTINGS_OPTION *const option = M_GetOptionByRow(s, row_idx); if (option == nullptr || Config_IsOptionEnforced(option->target)) { return false; } if (option->custom_handler.can_change_value != nullptr) { return option->custom_handler.can_change_value(option, dir); } switch (M_GetConfigOption(option)->type) { case COT_BOOL: return true; case COT_INT32: if (dir < 0) { return *(int32_t *)option->target > option->min_value; } else if (dir > 0) { return *(int32_t *)option->target < option->max_value; } break; case COT_DOUBLE: { const double target_value = (round(*(double *)option->target * 100) + dir) / 100.0; return target_value >= (double)option->min_value / 100.0 && target_value <= (double)option->max_value / 100.0; } case COT_FLOAT: case COT_FLOAT_PERCENT: { const float target_value = (round(*(float *)option->target * 100) + dir) / 100.0f; return target_value >= (float)option->min_value / 100.0f && target_value <= (float)option->max_value / 100.0f; } case COT_RGB888: return false; case COT_STRING: return false; case COT_DYNAMIC_ENUM: { const CONFIG_OPTION *const cfg_opt = M_GetConfigOption(option); return Config_DynamicEnum_CanCycle( cfg_opt, *(char **)option->target, dir); } case COT_ENUM: { const M_ENUM_LOOKUP enum_lookup = M_GetEnumEntry(option); ASSERT(enum_lookup.position >= 0); return M_FindNextAvailableEnumPosition(option, &enum_lookup, dir) >= 0; } default: break; } return false; } static bool M_RequestChangeValue( const UI_SETTINGS_EDITOR_STATE *const s, const int32_t row_idx, const int32_t dir) { if (!M_CanChangeValue(s, row_idx, dir)) { return false; } const UI_SETTINGS_OPTION *const option = M_GetOptionByRow(s, row_idx); if (option->custom_handler.request_change_value != nullptr) { if (option->custom_handler.request_change_value(option, dir)) { goto changed; } return false; } UI_SettingsEditor_RequestChange(option, dir); changed: Config_Update(); return true; } UI_SETTINGS_EDITOR_STATE *UI_SettingsEditor_Init( const UI_SETTINGS_OPTION *const options) { UI_SETTINGS_EDITOR_STATE *const s = Memory_Alloc(sizeof(*s)); s->options = options; s->scroll = (UI_SCROLLABLE) { .first_item = 0, .sel_item = -1, .max_items = 0, .vis_items = 0, }; s->color_editor = UI_ColorEditorDialog_Init(); return s; } void UI_SettingsEditor_Free(UI_SETTINGS_EDITOR_STATE *const s) { if (s->description.show) { UI_TextDialog_Free(s->description.state); s->description.state = nullptr; s->description.show = false; } UI_ColorEditorDialog_Free(s->color_editor); s->color_editor = nullptr; Memory_Free(s); } static float M_GetMaxLabelWidth(const UI_SETTINGS_EDITOR_STATE *const s) { float result = -1.0f; if (s->options != nullptr) { for (int32_t i = 0; s->options[i].target != nullptr; i++) { const UI_SETTINGS_OPTION *const option = &s->options[i]; const float label_w = UI_Label_MeasureW(M_GetOptionTitle(option)); result = MAX(label_w, result); } } return result; } static float M_GetMaxValueWidth(const UI_SETTINGS_EDITOR_STATE *const s) { float result = -1.0f; if (s->options != nullptr) { for (int32_t i = 0; s->options[i].target != nullptr; i++) { const UI_SETTINGS_OPTION *const option = &s->options[i]; const float value_w = M_MeasureMaxValueWidth(option); result = MAX(value_w, result); } } result += UI_Label_MeasureW("\\{button left}"); result += UI_Label_MeasureW("\\{button right}"); result += UI_ROW_ARROWS_TIGHT * 2; return result; } float UI_SettingsEditor_GetContentWidth(const UI_SETTINGS_EDITOR_STATE *const s) { return M_GetMaxLabelWidth(s) + 20.0f * g_Config.ui.text_scale + M_GetMaxValueWidth(s); } int32_t UI_SettingsEditor_GetItemCount(const UI_SETTINGS_EDITOR_STATE *const s) { return M_GetRowCount(s); } void UI_SettingsEditor_RequestChange( const UI_SETTINGS_OPTION *const option, const int32_t dir) { int32_t delta = g_Input.slow ? option->delta_slow : option->delta_fast; if (delta == 0) { delta = 1; } delta *= dir; switch (M_GetConfigOption(option)->type) { case COT_BOOL: *(bool *)option->target = !*(bool *)option->target; break; case COT_INT32: *(int32_t *)option->target += delta; break; case COT_DOUBLE: *(double *)option->target = (round(*(double *)option->target * 100) + delta) / 100.0f; if (*(double *)option->target == -0.0) { *(double *)option->target = 0.0; } break; case COT_FLOAT: case COT_FLOAT_PERCENT: *(float *)option->target = (round(*(float *)option->target * 100) + delta) / 100.0f; if (*(float *)option->target == -0.0f) { *(float *)option->target = 0.0f; } break; case COT_RGB888: break; case COT_ENUM: { const UI_SETTINGS_ENUM_ENTRY *const entries = option->misc; int32_t position = -1; int32_t count = 0; for (; entries[count].value != -1; count++) { if (entries[count].value == *(int32_t *)option->target) { position = count; } } if (position < 0 || count <= 0 || delta == 0) { break; } const int32_t step = delta < 0 ? -1 : 1; for (int32_t pos = position + step; pos >= 0 && pos < count; pos += step) { const bool can_use = option->custom_handler.is_enum_value_available == nullptr || option->custom_handler.is_enum_value_available( option, entries[pos].value); if (can_use) { *(int32_t *)option->target = entries[pos].value; break; } } break; } case COT_DYNAMIC_ENUM: { const CONFIG_OPTION *const cfg_opt = M_GetConfigOption(option); const char *const next = Config_DynamicEnum_GetNext( cfg_opt, *(char **)option->target, delta); if (next != nullptr || Config_DynamicEnum_IsValidValue(cfg_opt, nullptr)) { Config_SetOptionValueFromString(cfg_opt, next); } break; } case COT_STRING: break; } } void UI_SettingsEditor_RecomputeSizes( UI_SETTINGS_EDITOR_STATE *const s, const int32_t visible_rows) { s->visible_rows = visible_rows; UI_Scrollable_SetMaxItems(&s->scroll, M_GetRowCount(s)); UI_Scrollable_SetVisibleItems(&s->scroll, visible_rows); } UI_SCROLLABLE *UI_SettingsEditor_GetScrollable( UI_SETTINGS_EDITOR_STATE *const s) { return &s->scroll; } bool UI_SettingsEditor_Control( UI_SETTINGS_EDITOR_STATE *const s, UI_SETTINGS_PHASE *const dialog_phase) { if (UI_ColorEditorDialog_IsOpen(s->color_editor)) { UI_ColorEditorDialog_Control(s->color_editor); return true; } if (s->description.show) { UI_TextDialog_Control(s->description.state); if (g_InputDB.menu_back || g_InputDB.look) { UI_TextDialog_Free(s->description.state); s->description.state = nullptr; s->description.show = false; } return true; } const int32_t sel_row = UI_Scrollable_GetSelectedItem(&s->scroll); if (g_InputDB.menu_left && sel_row >= 0) { M_RequestChangeValue(s, sel_row, -1); return true; } if (g_InputDB.menu_right && sel_row >= 0) { M_RequestChangeValue(s, sel_row, +1); return true; } if (g_InputDB.menu_up) { if (!UI_Scrollable_SelectPrev(&s->scroll, false)) { *dialog_phase = UI_SETTINGS_PHASE_NAVIGATE_TABS; } return true; } if (g_InputDB.menu_down) { if (!UI_Scrollable_SelectNext(&s->scroll, false) && g_Config.ui.enable_wraparound) { *dialog_phase = UI_SETTINGS_PHASE_NAVIGATE_TABS; } return true; } if (g_InputDB.menu_confirm && sel_row >= 0) { const UI_SETTINGS_OPTION *const option = M_GetOptionByRow(s, sel_row); if (M_IsColorEditorOption(option)) { UI_ColorEditorDialog_Open(s->color_editor, option); return true; } } if (g_InputDB.look && sel_row >= 0) { const UI_SETTINGS_OPTION *const option = M_GetOptionByRow(s, sel_row); const char *const title = M_GetOptionTitle(option); const char *const text = M_GetOptionDescription(option); if (title != nullptr && text != nullptr) { s->description.show = true; s->description.state = UI_TextDialog_Init( UI_GetCanvasWidth() * 2.0f / 3.0f, (size_t)s->visible_rows, true); return true; } } if (g_InputDB.unbind_key && sel_row >= 0) { const UI_SETTINGS_OPTION *const option = M_GetOptionByRow(s, sel_row); if (option != nullptr && option->target != nullptr && !Config_IsOptionEnforced(option->target) && !Config_IsOptionAtDefault(option->target)) { Config_RestoreOptionDefault(option->target); Config_Update(); return true; } } return false; } void UI_SettingsEditor_DrawOverlay(UI_SETTINGS_EDITOR_STATE *const s) { UI_ColorEditorDialog(s->color_editor); if (s->description.show) { const int32_t row = UI_Scrollable_GetSelectedItem(&s->scroll); const UI_SETTINGS_OPTION *const option = M_GetOptionByRow(s, row); if (option != nullptr) { const char *title = M_GetOptionTitle(option); const char *text = M_GetOptionDescription(option); if (title != nullptr && text != nullptr) { if (Config_IsOptionEnforced(option->target)) { title = String_FormatStatic("%s*", title); text = String_FormatStatic( "* %s\n\n%s", *GS_PTR( "general/settings/common/frozen_option_disclaimer"), text); } UI_TextDialog(s->description.state, title, text); } } } } static void M_OptionLabel( const UI_SETTINGS_OPTION *const option, const char *const text, const bool star_if_enforced) { const bool is_available = option == nullptr || option->custom_handler.is_available == nullptr || option->custom_handler.is_available(option); const bool is_enforced = star_if_enforced && option != nullptr && Config_IsOptionEnforced(option->target); const char *const suffix = is_enforced ? "*" : ""; if (!is_available) { UI_LabelFmt("\\{dim}%s%s\\{/dim}", text, suffix); } else if (is_enforced) { UI_LabelFmt("%s%s", text, suffix); } else { UI_Label(text); } } void UI_SettingsEditor_Draw( UI_SETTINGS_EDITOR_STATE *const s, const UI_SCROLLABLE *const dialog_scroll, const UI_SETTINGS_PHASE dialog_phase, const float row_width) { const float max_label_w = M_GetMaxLabelWidth(s) / g_Config.ui.text_scale; const float max_value_w = M_GetMaxValueWidth(s) / g_Config.ui.text_scale; float label_w = max_label_w; const float total_w = max_label_w + 20.0f + max_value_w; if (row_width > total_w) { label_w += row_width - total_w; } const int32_t sel_row = UI_Scrollable_GetSelectedItem(dialog_scroll); if (dialog_scroll->vis_items == 0) { return; } UI_BeginStack(UI_STACK_VERTICAL); for (int32_t i = 0; i < dialog_scroll->vis_items; i++) { const int32_t row = dialog_scroll->first_item + i; if (row >= dialog_scroll->max_items) { UI_Spacer(0.0f, UI_TEXT_HEIGHT); continue; } const bool is_row_focused = dialog_phase == UI_SETTINGS_PHASE_EDIT_SETTINGS && row == sel_row; if (!UI_Scrollable_IsItemVisible(dialog_scroll, row)) { UI_BeginResize(-1.0f, 0.0f); } else { UI_BeginResize(-1.0f, -1.0f); } UI_BeginPad( g_TRVersion == 1 ? -1.0f : 0.0f, g_TRVersion == 1 ? -1.0f : 0.0f); if (is_row_focused) { UI_BeginFrame(UI_FRAME_SELECTED_OPTION); } UI_BeginPad( (g_TRVersion == 1 ? 1.0f : 0.0f), g_TRVersion == 1 ? 1.0f : 0.0f); UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_HORIZONTAL, .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE }, }); UI_BeginResize(label_w, -1.0f); { const UI_SETTINGS_OPTION *const option = M_GetOptionByRow(s, row); const char *const name = option != nullptr ? M_GetOptionTitle(option) : ""; M_OptionLabel(option, name, true); } UI_EndResize(); UI_Spacer(20.0f, 0.0f); UI_BeginResize(max_value_w, -1.0f); UI_BeginAnchor(1.0f, 0.5f); UI_BeginRowArrows( is_row_focused && M_CanChangeValue(s, row, -1), is_row_focused && M_CanChangeValue(s, row, +1), UI_ROW_ARROWS_MEDIUM); { const UI_SETTINGS_OPTION *const option = M_GetOptionByRow(s, row); if (M_IsBarColorEnum(option)) { UI_Bar((UI_BAR_SETTINGS) { .w = M_BAR_WIDTH, .h = M_BAR_HEIGHT, .value = 100, .max_value = 100, .type = M_GetBarType(option), .preview = true, }); } else if (M_IsColorEditorOption(option)) { const char *const value = M_FormatRowValue(s, row); const RGB_888 *const color = option->target; UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_HORIZONTAL, .align = { .v = UI_STACK_V_ALIGN_CENTER }, .spacing = { .h = 4.0f }, }); M_OptionLabel(option, value, false); UI_ColorSwatch((UI_COLOR_SWATCH_SETTINGS) { .color = *color, .w = UI_TEXT_HEIGHT - 2.0f, .h = UI_TEXT_HEIGHT - 2.0f, }); UI_EndStack(); } else { const char *const value = M_FormatRowValue(s, row); M_OptionLabel(option, value, false); } } UI_EndRowArrows(); UI_EndAnchor(); UI_EndResize(); UI_EndStack(); UI_EndPad(); if (is_row_focused) { UI_EndFrame(); } UI_EndPad(); UI_EndResize(); } UI_EndStack(); } void UI_SettingsEditor_DrawFooter( UI_SETTINGS_EDITOR_STATE *const s, const UI_SETTINGS_PHASE dialog_phase) { const int32_t row_idx = UI_Scrollable_GetSelectedItem(&s->scroll); const UI_SETTINGS_OPTION *const option = M_GetOptionByRow(s, row_idx); const bool can_edit_value = dialog_phase == UI_SETTINGS_PHASE_EDIT_SETTINGS && row_idx >= 0 && option != nullptr && M_GetConfigOption(option)->type == COT_RGB888; const bool can_examine = dialog_phase == UI_SETTINGS_PHASE_EDIT_SETTINGS && row_idx >= 0 && option != nullptr && M_GetOptionDescription(option) != nullptr && M_GetOptionTitle(option) != nullptr; const bool can_restore_default = dialog_phase == UI_SETTINGS_PHASE_EDIT_SETTINGS && row_idx >= 0 && option != nullptr && option->target != nullptr && !Config_IsOptionEnforced(option->target) && !Config_IsOptionAtDefault(option->target); UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_HORIZONTAL, .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE }, .spacing = { .h = 20 }, }); UI_BeginHide(!can_examine && !can_edit_value); if (can_edit_value) { UI_LabelFmt( "\\{input action} %s", GS("general/settings/common/edit_value")); } else { UI_LabelFmt( "\\{input look} %s", GS("general/settings/common/toggle_help")); } UI_EndHide(); UI_BeginHide(!can_restore_default); UI_LabelFmt( "\\{input unbind_key} %s", GS("general/settings/common/restore_default")); UI_EndHide(); UI_EndStack(); } ================================================ FILE: src/trx/game/ui/dialogs/settings_editor.h ================================================ #pragma once #include typedef struct UI_SETTINGS_EDITOR_STATE UI_SETTINGS_EDITOR_STATE; UI_SETTINGS_EDITOR_STATE *UI_SettingsEditor_Init( const UI_SETTINGS_OPTION *options); void UI_SettingsEditor_Free(UI_SETTINGS_EDITOR_STATE *s); void UI_SettingsEditor_RecomputeSizes( UI_SETTINGS_EDITOR_STATE *s, int32_t visible_rows); UI_SCROLLABLE *UI_SettingsEditor_GetScrollable(UI_SETTINGS_EDITOR_STATE *s); bool UI_SettingsEditor_Control( UI_SETTINGS_EDITOR_STATE *s, UI_SETTINGS_PHASE *dialog_phase); void UI_SettingsEditor_Draw( UI_SETTINGS_EDITOR_STATE *s, const UI_SCROLLABLE *dialog_scroll, UI_SETTINGS_PHASE dialog_phase, float row_width); void UI_SettingsEditor_DrawOverlay(UI_SETTINGS_EDITOR_STATE *s); void UI_SettingsEditor_DrawFooter( UI_SETTINGS_EDITOR_STATE *s, UI_SETTINGS_PHASE dialog_phase); float UI_SettingsEditor_GetContentWidth(const UI_SETTINGS_EDITOR_STATE *s); int32_t UI_SettingsEditor_GetItemCount(const UI_SETTINGS_EDITOR_STATE *s); void UI_SettingsEditor_RequestChange( const UI_SETTINGS_OPTION *option, int32_t dir); ================================================ FILE: src/trx/game/ui/dialogs/settings_tabs.c ================================================ #include #include #include static bool M_EditorControl( void *const user_data, UI_SETTINGS_PHASE *const phase) { return UI_SettingsEditor_Control(user_data, phase); } static void M_EditorDraw( void *const user_data, const UI_SETTINGS_PHASE phase, const float row_width) { UI_SETTINGS_EDITOR_STATE *const editor = user_data; UI_SettingsEditor_Draw( editor, UI_SettingsEditor_GetScrollable(editor), phase, row_width); } static void M_EditorDrawFooter( void *const user_data, const UI_SETTINGS_PHASE phase) { UI_SettingsEditor_DrawFooter(user_data, phase); } static void M_EditorDrawOverlay(void *const user_data) { UI_SettingsEditor_DrawOverlay(user_data); } static void M_EditorFree(void *const user_data) { UI_SettingsEditor_Free(user_data); } static UI_SCROLLABLE *M_EditorGetScrollable(void *const user_data) { return UI_SettingsEditor_GetScrollable(user_data); } static void M_EditorRecompute(void *const user_data, const int32_t visible_rows) { UI_SettingsEditor_RecomputeSizes(user_data, visible_rows); } static float M_EditorGetContentWidth(void *const user_data) { return UI_SettingsEditor_GetContentWidth(user_data); } static int32_t M_EditorGetItemCount(void *const user_data) { return UI_SettingsEditor_GetItemCount(user_data); } static const UI_SETTINGS_TAB_OPS m_EditorOps = { .control = M_EditorControl, .draw = M_EditorDraw, .draw_footer = M_EditorDrawFooter, .draw_overlay = M_EditorDrawOverlay, .free = M_EditorFree, .get_scrollable = M_EditorGetScrollable, .recompute = M_EditorRecompute, .get_content_width = M_EditorGetContentWidth, .get_content_height = nullptr, .get_item_count = M_EditorGetItemCount, }; UI_SETTINGS_TAB UI_SettingsTab_MakeEditor( const GAME_STRING_ID header_gs, const UI_SETTINGS_OPTION *const options) { return (UI_SETTINGS_TAB) { .header_gs = header_gs, .ops = &m_EditorOps, .user_data = UI_SettingsEditor_Init(options), }; } static bool M_PresetsControl( void *const user_data, UI_SETTINGS_PHASE *const phase) { if (UI_ConfigPresets_Control(user_data)) { *phase = UI_SETTINGS_PHASE_NAVIGATE_TABS; } return false; } static void M_PresetsDraw( void *const user_data, const UI_SETTINGS_PHASE, const float) { UI_ConfigPresets(user_data); } static void M_PresetsDrawOverlay(void *const user_data) { UI_ConfigPresetsApplyModal(user_data); } static void M_PresetsFree(void *const user_data) { UI_ConfigPresets_Free(user_data); } static UI_SCROLLABLE *M_PresetsGetScrollable(void *const user_data) { return UI_ConfigPresets_GetScrollable(user_data); } static void M_PresetsRecompute( void *const user_data, const int32_t visible_rows) { UI_ConfigPresets_RecomputeSizes(user_data, visible_rows); } static float M_PresetsGetContentWidth(void *const user_data) { return UI_ConfigPresets_GetContentWidth(user_data); } static float M_PresetsGetContentHeight(void *const user_data) { return UI_ConfigPresets_GetContentHeight(user_data); } static int32_t M_PresetsGetItemCount(void *const user_data) { return UI_ConfigPresets_GetItemCount(user_data); } static const UI_SETTINGS_TAB_OPS m_PresetsOps = { .control = M_PresetsControl, .draw = M_PresetsDraw, .draw_overlay = M_PresetsDrawOverlay, .free = M_PresetsFree, .get_scrollable = M_PresetsGetScrollable, .recompute = M_PresetsRecompute, .get_content_width = M_PresetsGetContentWidth, .get_content_height = M_PresetsGetContentHeight, .get_item_count = M_PresetsGetItemCount, }; UI_SETTINGS_TAB UI_SettingsTab_MakePresets(const GAME_STRING_ID header_gs) { return (UI_SETTINGS_TAB) { .header_gs = header_gs, .ops = &m_PresetsOps, .user_data = UI_ConfigPresets_Init(), }; } ================================================ FILE: src/trx/game/ui/dialogs/settings_tabs.h ================================================ #pragma once // settings tab contracts and tab factory helpers #include #include #include typedef struct UI_SETTINGS_OPTION UI_SETTINGS_OPTION; typedef enum { UI_SETTINGS_PHASE_NAVIGATE_TABS, UI_SETTINGS_PHASE_EDIT_SETTINGS, } UI_SETTINGS_PHASE; typedef bool (*UI_SETTINGS_TAB_CONTROL_FUNC)( void *user_data, UI_SETTINGS_PHASE *phase); typedef void (*UI_SETTINGS_TAB_DRAW_FUNC)( void *user_data, UI_SETTINGS_PHASE phase, float row_width); typedef void (*UI_SETTINGS_TAB_DRAW_FOOTER_FUNC)( void *user_data, UI_SETTINGS_PHASE phase); typedef void (*UI_SETTINGS_TAB_FREE_FUNC)(void *user_data); typedef void (*UI_SETTINGS_TAB_DRAW_OVERLAY_FUNC)(void *user_data); typedef UI_SCROLLABLE *(*UI_SETTINGS_TAB_GET_SCROLLABLE_FUNC)(void *user_data); typedef void (*UI_SETTINGS_TAB_RECOMPUTE_FUNC)( void *user_data, int32_t visible_rows); typedef float (*UI_SETTINGS_TAB_GET_CONTENT_WIDTH_FUNC)(void *user_data); typedef float (*UI_SETTINGS_TAB_GET_CONTENT_HEIGHT_FUNC)(void *user_data); typedef int32_t (*UI_SETTINGS_TAB_GET_ITEM_COUNT_FUNC)(void *user_data); typedef struct UI_SETTINGS_TAB_OPS { UI_SETTINGS_TAB_CONTROL_FUNC control; UI_SETTINGS_TAB_DRAW_FUNC draw; UI_SETTINGS_TAB_DRAW_FOOTER_FUNC draw_footer; UI_SETTINGS_TAB_DRAW_OVERLAY_FUNC draw_overlay; UI_SETTINGS_TAB_FREE_FUNC free; UI_SETTINGS_TAB_GET_SCROLLABLE_FUNC get_scrollable; UI_SETTINGS_TAB_RECOMPUTE_FUNC recompute; UI_SETTINGS_TAB_GET_CONTENT_WIDTH_FUNC get_content_width; UI_SETTINGS_TAB_GET_CONTENT_HEIGHT_FUNC get_content_height; UI_SETTINGS_TAB_GET_ITEM_COUNT_FUNC get_item_count; } UI_SETTINGS_TAB_OPS; typedef struct UI_SETTINGS_TAB { GAME_STRING_ID header_gs; const UI_SETTINGS_TAB_OPS *ops; void *user_data; } UI_SETTINGS_TAB; UI_SETTINGS_TAB UI_SettingsTab_MakeEditor( GAME_STRING_ID header_gs, const UI_SETTINGS_OPTION *options); UI_SETTINGS_TAB UI_SettingsTab_MakePresets(GAME_STRING_ID header_gs); ================================================ FILE: src/trx/game/ui/dialogs/sound_settings.c ================================================ #include #include #include #include #include #include static const UI_SETTINGS_OPTION m_SoundVolumeOptions[] = { #include { .target = nullptr }, }; static const UI_SETTINGS_OPTION m_SoundMiscOptions[] = { #include { .target = nullptr }, }; UI_SETTINGS_DIALOG_STATE *UI_SoundSettings_Init(void) { const UI_SETTINGS_TAB tabs[] = { UI_SettingsTab_MakeEditor( GS_ID("general/settings/sound/tabs/volume"), m_SoundVolumeOptions), UI_SettingsTab_MakeEditor( GS_ID("general/settings/sound/tabs/misc"), m_SoundMiscOptions), }; return UI_SettingsDialog_Init( GS_ID("general/settings/sound/title"), ARRAY_SIZE(tabs), tabs); } void UI_SoundSettings_Free(UI_SETTINGS_DIALOG_STATE *const s) { UI_SettingsDialog_Free(s); } bool UI_SoundSettings_Control(UI_SETTINGS_DIALOG_STATE *const s) { return UI_SettingsDialog_Control(s); } void UI_SoundSettings(UI_SETTINGS_DIALOG_STATE *const s) { UI_SettingsDialog(s); } ================================================ FILE: src/trx/game/ui/dialogs/sound_settings.h ================================================ // UI dialog for adjusting music and sound volumes #pragma once #include #include // Initialize the sound settings dialog state. UI_SETTINGS_DIALOG_STATE *UI_SoundSettings_Init(void); // Free resources used by the sound settings dialog. void UI_SoundSettings_Free(UI_SETTINGS_DIALOG_STATE *s); // Handle input/control for the sound settings dialog. // Returns true if the dialog should be closed. bool UI_SoundSettings_Control(UI_SETTINGS_DIALOG_STATE *s); // Render the sound settings dialog. void UI_SoundSettings(UI_SETTINGS_DIALOG_STATE *s); ================================================ FILE: src/trx/game/ui/dialogs/stats.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_MIN_ASSAULT_COURSE_ROWS 7 typedef enum { M_ROW_GENERIC, M_ROW_LEVEL_COUNTER, M_ROW_TIMER, M_ROW_AUTO_SECRETS, M_ROW_ICON_SECRETS, M_ROW_NUM_SECRETS, M_ROW_CRYSTALS, M_ROW_PICKUPS, M_ROW_DEATHS, M_ROW_KILLS, M_ROW_AMMO, M_ROW_AMMO_USED, M_ROW_AMMO_HITS, M_ROW_MEDIPACKS_USED, M_ROW_DISTANCE_TRAVELLED, M_ROW_ASSAULT_COURSE_TITLE, M_ROW_ASSAULT_COURSE_ROW, M_ROW_ASSAULT_NO_TIMES_SET, M_ROW_RACETRACK_TITLE, M_ROW_RACETRACK_ROW, M_ROW_SPACER, } M_ROW_ROLE; typedef struct { float window_margin; float window_y; float title_spacing; float min_width; float row_spacing; bool use_full_hours; } M_LOOK; typedef struct UI_STATS_DIALOG_STATE { UI_STATS_DIALOG_ARGS args; UI_SCROLLABLE scrollable; union { struct { const STATS_COMMON *stats; const LEVEL_MAX_STATS *max_stats; FINAL_STATS final_stats; LEVEL_MAX_STATS adjusted_max_stats; }; const GYM_TRACK_STATS *assault_stats[GYM_TRACK_NUMBER_OF]; }; const M_LOOK *look; bool has_floordata_secrets; bool has_visible_rows; } UI_STATS_DIALOG_STATE; static const M_LOOK m_Looks[TR_VERSION_COUNT] = { [0] = { .window_margin = 0.0f, .window_y = 0.5f, .title_spacing = 4.0f, .min_width = 0.0f, .row_spacing = 30.0f, .use_full_hours = false, }, [1] = { .window_margin = 40.0f, .window_y = 1.0f, .title_spacing = 3.0f, .min_width = 290.0f, .row_spacing = 25.0f, .use_full_hours = true, }, [2] = { .window_margin = 40.0f, .window_y = 1.0f, .title_spacing = 3.0f, .min_width = 290.0f, .row_spacing = 25.0f, .use_full_hours = true, }, }; static const char *M_FormatRecordTime(const int32_t total_frames) { const int32_t total_seconds = total_frames / LOGIC_FPS; const int32_t minutes = (total_seconds / 60) % 60; const int32_t seconds = total_seconds % 60; const int32_t centiseconds = total_frames % LOGIC_FPS / (LOGIC_FPS / 10); return String_FormatStatic( "%02d:%02d.%-2d", minutes, seconds, centiseconds); } static const char *M_FormatTime( const UI_STATS_DIALOG_STATE *const s, const int32_t total_frames) { const int32_t total_seconds = total_frames / LOGIC_FPS; const int32_t hours = total_seconds / 3600; const int32_t minutes = (total_seconds / 60) % 60; const int32_t seconds = total_seconds % 60; if (s->look->use_full_hours) { return String_FormatStatic("%02d:%02d:%02d", hours, minutes, seconds); } else if (hours != 0) { return String_FormatStatic("%d:%02d:%02d", hours, minutes, seconds); } else { return String_FormatStatic("%d:%02d", minutes, seconds); } } static const char *M_FormatDistance(int32_t distance) { distance /= 445; if (distance < 1000) { return String_FormatStatic("%dm", distance); } else { return String_FormatStatic( "%d.%02dkm", distance / 1000, (distance % 1000) / 10); } } static void M_AdjustMaxKills( UI_STATS_DIALOG_STATE *const s, const bool include_allies) { if (s->max_stats == nullptr) { return; } s->adjusted_max_stats = *s->max_stats; s->adjusted_max_stats.max_kill_count = s->adjusted_max_stats.max_kill_non_ally_count; if (include_allies) { s->adjusted_max_stats.max_kill_count += s->adjusted_max_stats.max_kill_ally_count; } s->max_stats = &s->adjusted_max_stats; } static bool M_HasHurtAlliesSoFar(const int32_t level_num) { const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); if (level_table == nullptr || level_num <= 0) { return false; } for (int32_t i = 0; i <= level_num && i < level_table->count; i++) { const GF_LEVEL *const level = &level_table->levels[i]; const RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); if (resume != nullptr && resume->flags.available && resume->hurt_allies) { return true; } } return false; } static bool M_HasHurtAlliesEver(const bool include_bonus_levels) { const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); if (level_table == nullptr) { return false; } for (int32_t i = 0; i < level_table->count; i++) { const GF_LEVEL *const level = &level_table->levels[i]; if (!(level->type == GFL_NORMAL || (level->type == GFL_BONUS && include_bonus_levels))) { continue; } const RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); if (resume != nullptr && resume->hurt_allies) { return true; } } return false; } static void M_FormatIconSecrets( char *const out, const LEVEL_STATS *const level_stats) { char *ptr = out; int32_t num_secrets = 0; for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) { if (!Stats_IsSecretValid(i)) { continue; } const bool has_secret = Stats_HasSecret(i); if (!has_secret && out == ptr) { // Do not reserve space pointlessly. // Good: [secret][ ][ ] // Bad: [ ][ ][secret] – should be just [secret] continue; } const OBJECT_ID obj_id = Stats_GetSecretObject(i); if (obj_id != NO_OBJECT) { ptr += sprintf( ptr, has_secret ? "\\{secret %d}" : "\\{i}\\{secret %d}\\{/i}", obj_id + 1 - O_SECRET_1); } if (has_secret) { num_secrets++; } } *ptr++ = '\0'; if (num_secrets == 0) { strcpy(out, GS("general/stats/none")); } } static void M_RowCentered( const UI_STATS_DIALOG_STATE *const s, const char *const text) { UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, .align = { .h = UI_STACK_H_ALIGN_CENTER }, }); if (s->args.style == UI_STATS_DIALOG_STYLE_BARE) { char *text_upper = String_ToUpper(text); UI_Label(text_upper); Memory_FreePointer(&text_upper); } else { UI_Label(text); } UI_EndStack(); } static void M_Row( const UI_STATS_DIALOG_STATE *const s, const char *const key, const char *const value) { if (s->args.style == UI_STATS_DIALOG_STYLE_BARE) { char *key_upper = String_ToUpper(key); UI_BeginStack(UI_STACK_HORIZONTAL); UI_Label(key_upper); UI_Label(" "); UI_Label(value); UI_EndStack(); Memory_FreePointer(&key_upper); } else { UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_HORIZONTAL, .spacing = { .h = s->look->row_spacing }, .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE }, }); UI_Label(key); UI_Label(value); UI_EndStack(); } } static void M_RowFromRole( const UI_STATS_DIALOG_STATE *const s, const M_ROW_ROLE role, const int32_t param) { const char *const num_fmt = g_Config.ui.stats.show_totals ? GS("general/stats/detail_fmt") : GS("general/stats/basic_fmt"); switch (role) { case M_ROW_LEVEL_COUNTER: M_Row( s, GS("general/stats/level"), String_FormatStatic( GS("general/stats/detail_fmt"), GF_GetLevelOrdinalNumber( GFLT_MAIN, GF_GetLevel(GFLT_MAIN, s->args.level_num)), GF_GetLevelCount(GFLT_MAIN))); break; case M_ROW_TIMER: M_Row( s, GS("general/stats/time_taken"), M_FormatTime(s, s->stats->timer)); break; case M_ROW_AUTO_SECRETS: if (s->args.mode == UI_STATS_DIALOG_MODE_FINAL || s->has_floordata_secrets) { M_RowFromRole(s, M_ROW_NUM_SECRETS, 0); } else { M_RowFromRole(s, M_ROW_ICON_SECRETS, 0); } break; case M_ROW_ICON_SECRETS: { char buf[256]; M_FormatIconSecrets(buf, (LEVEL_STATS *)s->stats); M_Row(s, GS("general/stats/secrets"), buf); break; } case M_ROW_NUM_SECRETS: M_Row( s, GS("general/stats/secrets"), String_FormatStatic( GS("general/stats/detail_fmt"), s->stats->secret_count, s->max_stats->max_secret_count)); break; case M_ROW_CRYSTALS: M_Row( s, GS("general/stats/crystals"), String_FormatStatic( num_fmt, s->stats->crystal_count, s->max_stats->max_crystal_count)); break; case M_ROW_PICKUPS: M_Row( s, GS("general/stats/pickups"), String_FormatStatic( num_fmt, s->stats->pickup_count, s->max_stats->max_pickup_count)); break; case M_ROW_KILLS: M_Row( s, GS("general/stats/kills"), String_FormatStatic( num_fmt, s->stats->kill_count, s->max_stats->max_kill_count)); break; case M_ROW_DEATHS: M_Row( s, GS("general/stats/deaths"), String_FormatStatic( GS("general/stats/basic_fmt"), s->stats->death_count)); break; case M_ROW_AMMO: M_Row( s, GS("general/stats/ammo"), String_FormatStatic( GS("general/misc/pagination_nav"), s->stats->ammo_hits, s->stats->ammo_used)); break; case M_ROW_AMMO_USED: M_Row( s, GS("general/stats/ammo_used"), String_FormatStatic("%d", s->stats->ammo_used)); break; case M_ROW_AMMO_HITS: M_Row( s, GS("general/stats/ammo_hits"), String_FormatStatic("%d", s->stats->ammo_hits)); break; case M_ROW_MEDIPACKS_USED: M_Row( s, GS("general/stats/medipacks_used"), String_FormatStatic("%.1f", s->stats->medipacks_used)); break; case M_ROW_DISTANCE_TRAVELLED: M_Row( s, GS("general/stats/distance_travelled"), M_FormatDistance(s->stats->distance_travelled)); break; case M_ROW_ASSAULT_COURSE_TITLE: M_RowCentered(s, GS("general/stats/gym_assault_course")); break; case M_ROW_ASSAULT_COURSE_ROW: case M_ROW_RACETRACK_ROW: { const GYM_TRACK_TYPE track_type = role == M_ROW_ASSAULT_COURSE_ROW ? GYM_TRACK_ASSAULT : GYM_TRACK_QUAD; const GYM_TRACK_ENTRY *const entry = &s->assault_stats[track_type]->entries[param]; const char *const attempt_str = String_FormatStatic( "%2d: %s %d", param + 1, GS("general/stats/assault_finish"), entry->attempt_num); const char *const time_str = String_FormatStatic( param == 0 ? GS("general/stats/assault_best_time_fmt") : GS("general/stats/assault_other_times_fmt"), M_FormatRecordTime(entry->time)); if (g_TRVersion == 3) { M_RowCentered(s, time_str); } else { M_Row(s, attempt_str, time_str); } break; } case M_ROW_ASSAULT_NO_TIMES_SET: M_RowCentered(s, GS("general/stats/assault_no_times_set")); break; case M_ROW_RACETRACK_TITLE: M_RowCentered(s, GS("general/stats/gym_racetrack_course")); break; case M_ROW_SPACER: M_RowCentered(s, " "); break; default: break; } } static bool M_EmitRow( const UI_STATS_DIALOG_STATE *const s, const M_ROW_ROLE role, const int32_t param) { M_RowFromRole(s, role, param); return true; } static bool M_EmitDummyRow( const UI_STATS_DIALOG_STATE *const s, const M_ROW_ROLE role, const int32_t param) { return true; } static bool M_EmitConfiguredStatsRows( const UI_STATS_DIALOG_STATE *const s, const bool dry_run) { bool has_rows = false; bool (*emit_row_func)(const UI_STATS_DIALOG_STATE *, M_ROW_ROLE, int32_t) = dry_run ? M_EmitDummyRow : M_EmitRow; if (s->args.style == UI_STATS_DIALOG_STYLE_BARE) { if (g_Config.ui.stats.show_kills) { has_rows |= emit_row_func(s, M_ROW_KILLS, 0); } if (g_Config.ui.stats.show_pickups) { has_rows |= emit_row_func(s, M_ROW_PICKUPS, 0); } if (g_Config.ui.stats.show_crystals && s->max_stats->max_crystal_count != 0) { has_rows |= emit_row_func(s, M_ROW_CRYSTALS, 0); } if (g_Config.ui.stats.show_secrets && s->max_stats->max_secret_count != 0) { has_rows |= emit_row_func(s, M_ROW_AUTO_SECRETS, 0); } if (g_Config.ui.stats.show_time_taken) { has_rows |= emit_row_func(s, M_ROW_TIMER, 0); } } else { if (g_Config.ui.stats.show_time_taken) { has_rows |= emit_row_func(s, M_ROW_TIMER, 0); } if (g_Config.ui.stats.show_secrets && s->max_stats->max_secret_count != 0) { has_rows |= emit_row_func(s, M_ROW_AUTO_SECRETS, 0); } if (g_Config.ui.stats.show_crystals && s->max_stats->max_crystal_count != 0) { has_rows |= emit_row_func(s, M_ROW_CRYSTALS, 0); } if (g_Config.ui.stats.show_pickups) { has_rows |= emit_row_func(s, M_ROW_PICKUPS, 0); } if (g_Config.ui.stats.show_kills) { has_rows |= emit_row_func(s, M_ROW_KILLS, 0); } } if (g_Config.ui.stats.show_ammo) { if (s->args.style == UI_STATS_DIALOG_STYLE_BARE) { M_RowFromRole(s, M_ROW_AMMO_USED, 0); M_RowFromRole(s, M_ROW_AMMO_HITS, 0); } else { M_RowFromRole(s, M_ROW_AMMO, 0); } has_rows = true; } if (g_Config.ui.stats.show_medipacks_used) { has_rows |= emit_row_func(s, M_ROW_MEDIPACKS_USED, 0); } if (g_Config.ui.stats.show_distance_travelled) { has_rows |= emit_row_func(s, M_ROW_DISTANCE_TRAVELLED, 0); } if (g_Config.ui.stats.show_deaths && s->stats->death_count >= 0) { // Always use the sum of all levels for deaths. // Deaths get stored in the resume info for the level they happen on, // so if the player dies in Vilcabamba and reloads Caves, they should // still see an incremented death counter. has_rows |= emit_row_func(s, M_ROW_DEATHS, 0); } return has_rows; } static bool M_HasVisibleRows(const UI_STATS_DIALOG_STATE *const s) { if (s->args.mode == UI_STATS_DIALOG_MODE_LEVEL && g_Config.ui.stats.show_level_header) { return true; } return M_EmitConfiguredStatsRows(s, true); } static void M_LevelStatsRows(const UI_STATS_DIALOG_STATE *const s) { if (g_Config.ui.stats.show_level_header) { M_EmitRow(s, M_ROW_LEVEL_COUNTER, 0); } if (!s->has_visible_rows) { M_RowCentered(s, GS("general/osd/complete_level")); return; } M_EmitConfiguredStatsRows(s, false); } static void M_FinalStatsRows(const UI_STATS_DIALOG_STATE *const s) { M_EmitConfiguredStatsRows(s, false); } static const char *M_GetDialogTitle(const UI_STATS_DIALOG_STATE *const s) { switch (s->args.mode) { case UI_STATS_DIALOG_MODE_LEVEL: return GF_GetLevel(GFLT_MAIN, s->args.level_num)->title; case UI_STATS_DIALOG_MODE_FINAL: { const GF_LEVEL_TYPE level_type = GF_GetLevel(GFLT_MAIN, s->args.level_num)->type; const char *const title = level_type == GFL_BONUS ? GS("general/stats/bonus_statistics") : GS("general/stats/final_statistics"); return title; } case UI_STATS_DIALOG_MODE_ASSAULT_COURSE: return GS("general/stats/assault_title"); } return nullptr; } static void M_AssaultCourseStatsRows(UI_STATS_DIALOG_STATE *const s) { const int32_t record_limit = g_TRVersion >= 3 ? 3 : MAX_ASSAULT_TIMES; const bool has_race_track = Gym_TrackManager_HasStats(GYM_TRACK_QUAD); int32_t count = 0; #define L_EMIT_ROW(...) \ M_RowFromRole(__VA_ARGS__); \ count++; if (has_race_track) { L_EMIT_ROW(s, M_ROW_ASSAULT_COURSE_TITLE, 0); } if (s->assault_stats[GYM_TRACK_ASSAULT]->entries[0].time == 0) { L_EMIT_ROW(s, M_ROW_ASSAULT_NO_TIMES_SET, 0); } else { for (int32_t i = 0; i < record_limit; i++) { if (s->assault_stats[GYM_TRACK_ASSAULT]->entries[i].time == 0) { break; } L_EMIT_ROW(s, M_ROW_ASSAULT_COURSE_ROW, i); } } if (has_race_track) { L_EMIT_ROW(s, M_ROW_SPACER, 0); L_EMIT_ROW(s, M_ROW_RACETRACK_TITLE, 0); if (s->assault_stats[GYM_TRACK_QUAD]->entries[0].time == 0) { L_EMIT_ROW(s, M_ROW_ASSAULT_NO_TIMES_SET, 0); } else { for (int32_t i = 0; i < record_limit; i++) { if (s->assault_stats[GYM_TRACK_QUAD]->entries[i].time == 0) { break; } L_EMIT_ROW(s, M_ROW_RACETRACK_ROW, i); } } } #undef L_EMIT_ROW while (count < M_MIN_ASSAULT_COURSE_ROWS) { M_RowFromRole(s, M_ROW_SPACER, 0); count++; } } static int32_t M_GetAssaultCourseRowCount(const UI_STATS_DIALOG_STATE *const s) { const int32_t record_limit = g_TRVersion >= 3 ? 3 : MAX_ASSAULT_TIMES; const bool has_race_track = Gym_TrackManager_HasStats(GYM_TRACK_QUAD); int32_t count = 0; if (has_race_track) { count++; } if (s->assault_stats[GYM_TRACK_ASSAULT]->entries[0].time == 0) { count++; } else { for (int32_t i = 0; i < record_limit; i++) { if (s->assault_stats[GYM_TRACK_ASSAULT]->entries[i].time == 0) { break; } count++; } } if (has_race_track) { count += 2; if (s->assault_stats[GYM_TRACK_QUAD]->entries[0].time == 0) { count++; } else { for (int32_t i = 0; i < record_limit; i++) { if (s->assault_stats[GYM_TRACK_QUAD]->entries[i].time == 0) { break; } count++; } } } return MAX(count, M_MIN_ASSAULT_COURSE_ROWS); } static UI_WINDOW_SETTINGS M_GetWindowSettings( const UI_STATS_DIALOG_STATE *const s) { const bool is_assault_mode = s->args.mode == UI_STATS_DIALOG_MODE_ASSAULT_COURSE; return (UI_WINDOW_SETTINGS) { .title = M_GetDialogTitle(s), .scrollable = is_assault_mode ? &s->scrollable : nullptr, .title_spacing = s->look->title_spacing, }; } static void M_BeginDialog(const UI_STATS_DIALOG_STATE *const s) { if (s->args.style == UI_STATS_DIALOG_STYLE_BARE) { UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, .spacing = { .v = 11.0f }, .align = { .h = UI_STACK_H_ALIGN_CENTER }, }); const char *const title = M_GetDialogTitle(s); if (title != nullptr) { UI_Label(title); } } else { UI_BeginWindow(M_GetWindowSettings(s)); } // ensure minimum dialog width UI_Spacer(s->look->min_width, 0.0f); } static void M_EndDialog(const UI_STATS_DIALOG_STATE *const s) { if (s->args.style == UI_STATS_DIALOG_STYLE_BARE) { UI_EndStack(); } else { UI_EndWindow(); } } UI_STATS_DIALOG_STATE *UI_StatsDialog_Init(const UI_STATS_DIALOG_ARGS args) { UI_STATS_DIALOG_STATE *const s = Memory_Alloc(sizeof(*s)); s->has_floordata_secrets = false; s->scrollable.vis_items = M_MIN_ASSAULT_COURSE_ROWS; s->args = args; s->look = &m_Looks[g_TRVersion - 1]; switch (args.mode) { case UI_STATS_DIALOG_MODE_LEVEL: const GF_LEVEL *const current_level = GF_GetLevel(GFLT_MAIN, s->args.level_num); const RESUME_INFO *const current_info = Savegame_GetCurrentInfo(current_level); s->stats = (const STATS_COMMON *)¤t_info->stats; s->max_stats = Stats_GetLevelMaxStats(current_level); const bool include_allies = M_HasHurtAlliesSoFar(s->args.level_num); M_AdjustMaxKills(s, include_allies); const GF_LEVEL *const level = Game_GetCurrentLevel(); for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) { if (s->max_stats->secret_objects[i].taken && s->max_stats->secret_objects[i].assigned_object_id == NO_OBJECT) { s->has_floordata_secrets = true; } } s->has_visible_rows = M_HasVisibleRows(s); break; case UI_STATS_DIALOG_MODE_FINAL: const GF_LEVEL_TYPE level_type = GF_GetLevel(GFLT_MAIN, s->args.level_num)->type; const bool include_bonus_levels = level_type == GFL_BONUS; s->final_stats = Stats_ComputeFinalStats(include_bonus_levels); s->stats = &s->final_stats.stats; s->max_stats = &s->final_stats.max_stats; M_AdjustMaxKills(s, M_HasHurtAlliesEver(include_bonus_levels)); s->has_visible_rows = M_HasVisibleRows(s); break; case UI_STATS_DIALOG_MODE_ASSAULT_COURSE: for (int32_t track_type = 0; track_type < GYM_TRACK_NUMBER_OF; track_type++) { s->assault_stats[track_type] = Gym_TrackManager_GetStats(track_type); } s->scrollable.max_items = M_GetAssaultCourseRowCount(s); s->has_visible_rows = true; break; } return s; } void UI_StatsDialog_Free(UI_STATS_DIALOG_STATE *const s) { Memory_Free(s); } bool UI_StatsDialog_HasVisibleRows(const UI_STATS_DIALOG_STATE *const s) { return s->has_visible_rows; } int32_t UI_StatsDialog_Control(UI_STATS_DIALOG_STATE *const s) { return UI_ScrollableStack_Control(&s->scrollable, UI_STACK_VERTICAL); } void UI_StatsDialog(UI_STATS_DIALOG_STATE *const s) { UI_BeginModal(0.5f, s->look->window_y); UI_BeginPad(s->look->window_margin, s->look->window_margin); M_BeginDialog(s); switch (s->args.mode) { case UI_STATS_DIALOG_MODE_LEVEL: M_LevelStatsRows(s); break; case UI_STATS_DIALOG_MODE_FINAL: M_FinalStatsRows(s); break; case UI_STATS_DIALOG_MODE_ASSAULT_COURSE: // Ensure minimum size even if there are no items UI_Spacer(290.0f, 0.0f); UI_BeginScrollableStack( &s->scrollable, (UI_SCROLLABLE_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, .spacing = 3.0f, }); M_AssaultCourseStatsRows(s); UI_EndScrollableStack(); break; } M_EndDialog(s); UI_EndPad(); UI_EndModal(); } ================================================ FILE: src/trx/game/ui/dialogs/stats.h ================================================ #pragma once #include #include typedef enum { UI_STATS_DIALOG_MODE_LEVEL, UI_STATS_DIALOG_MODE_FINAL, UI_STATS_DIALOG_MODE_ASSAULT_COURSE, } UI_STATS_DIALOG_MODE; typedef enum { UI_STATS_DIALOG_STYLE_BARE, UI_STATS_DIALOG_STYLE_BORDERED, } UI_STATS_DIALOG_STYLE; typedef struct { UI_STATS_DIALOG_MODE mode; UI_STATS_DIALOG_STYLE style; int32_t level_num; } UI_STATS_DIALOG_ARGS; typedef struct UI_STATS_DIALOG_STATE UI_STATS_DIALOG_STATE; UI_STATS_DIALOG_STATE *UI_StatsDialog_Init(UI_STATS_DIALOG_ARGS args); void UI_StatsDialog_Free(UI_STATS_DIALOG_STATE *s); bool UI_StatsDialog_HasVisibleRows(const UI_STATS_DIALOG_STATE *s); int32_t UI_StatsDialog_Control(UI_STATS_DIALOG_STATE *s); void UI_StatsDialog(UI_STATS_DIALOG_STATE *s); ================================================ FILE: src/trx/game/ui/dialogs/switch_mod.c ================================================ #include #include #include #include #include #include #include #include #include #include #include typedef struct { const char *name; const char *title; } M_ROW; struct UI_SWITCH_MOD_DIALOG_STATE { VECTOR *rows; UI_REQUESTER_STATE req; }; UI_SWITCH_MOD_DIALOG_STATE *UI_SwitchModDialog_Init(void) { UI_SWITCH_MOD_DIALOG_STATE *const s = Memory_Alloc(sizeof(UI_SWITCH_MOD_DIALOG_STATE)); s->rows = Vector_Create(sizeof(M_ROW)); int32_t current_row = 0; for (int32_t i = 0; i < Shell_GetModCount(); i++) { const SHELL_MOD *const mod = Shell_GetMod(i); if (!Shell_CanSwitchToMod(mod)) { continue; } if (Shell_IsCurrentMod(mod->name)) { current_row = s->rows->count; } Vector_Add( s->rows, &(M_ROW) { .name = mod->name, .title = mod->title }); } UI_BasePassportDialog_Init(&s->req, s->rows->count); UI_Requester_SelectRow(&s->req, current_row); return s; } void UI_SwitchModDialog_Free(UI_SWITCH_MOD_DIALOG_STATE *const s) { Vector_Free(s->rows); UI_Requester_Free(&s->req); Memory_Free(s); } int32_t UI_SwitchModDialog_Control(UI_SWITCH_MOD_DIALOG_STATE *const s) { UI_BasePassportDialog_Control(&s->req); return UI_Requester_Control(&s->req); } const char *UI_SwitchModDialog_GetSelectedMod( const UI_SWITCH_MOD_DIALOG_STATE *const s, const int32_t choice) { if (choice < 0 || choice >= s->rows->count) { return nullptr; } const M_ROW *const row = Vector_Get(s->rows, choice); return row->name; } void UI_SwitchModDialog(UI_SWITCH_MOD_DIALOG_STATE *const s) { UI_BeginBasePassportDialog(); UI_BeginRequester(&s->req, GS("general/passport/select_mod")); for (int32_t i = 0; i < s->rows->count; i++) { if (!UI_Requester_IsRowVisible(&s->req, i)) { continue; } const M_ROW *const row = Vector_Get(s->rows, i); UI_BeginRequesterRow(&s->req, i); UI_BeginAnchor(0.5f, 0.5f); const char *const gs_key = String_FormatStatic("dynamic/mods/%s/title", row->name); const char *const gs_title = GS(gs_key); const char *const display = gs_title != nullptr ? gs_title : row->title != nullptr ? row->title : row->name; UI_Label(display); UI_EndAnchor(); UI_EndRequesterRow(&s->req, i); } UI_EndRequester(&s->req); UI_EndBasePassportDialog(); } ================================================ FILE: src/trx/game/ui/dialogs/switch_mod.h ================================================ #pragma once // A mod selector dialog used by the passport. #include typedef struct UI_SWITCH_MOD_DIALOG_STATE UI_SWITCH_MOD_DIALOG_STATE; // state functions UI_SWITCH_MOD_DIALOG_STATE *UI_SwitchModDialog_Init(void); void UI_SwitchModDialog_Free(UI_SWITCH_MOD_DIALOG_STATE *s); int32_t UI_SwitchModDialog_Control(UI_SWITCH_MOD_DIALOG_STATE *s); const char *UI_SwitchModDialog_GetSelectedMod( const UI_SWITCH_MOD_DIALOG_STATE *s, int32_t choice); // draw functions void UI_SwitchModDialog(UI_SWITCH_MOD_DIALOG_STATE *s); ================================================ FILE: src/trx/game/ui/dialogs/text.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_TITLE_MARGIN 5.0f #define M_DIALOG_PADDING 8.0f typedef struct UI_TEXT_DIALOG_STATE { char *title; // uppercase working title string char *last_raw_title; // last raw title (duplicate for strcmp) char *last_raw_text; // last raw text (duplicate for strcmp) size_t wrap_width; size_t wrap_max_lines; bool is_heavy; size_t max_vis_lines; // maximum visible lines in pagination VECTOR *page_content; // paginated wrapped pages int32_t current_page; } UI_TEXT_DIALOG_STATE; static void M_UpdateTitle( UI_TEXT_DIALOG_STATE *const s, const char *const title_raw) { // Title update on change (strcmp to handle static ring buffers) if (title_raw != nullptr && (s->last_raw_title == nullptr || strcmp(s->last_raw_title, title_raw) != 0)) { Memory_FreePointer(&s->title); s->title = String_ToUpper(title_raw); Memory_FreePointer(&s->last_raw_title); s->last_raw_title = Memory_DupStr(title_raw); } } static void M_UpdateText( UI_TEXT_DIALOG_STATE *const s, const char *const text_raw) { // Text update on change (strcmp to in case the pointers does not change) if (!s->last_raw_text || strcmp(s->last_raw_text, text_raw) != 0) { if (s->page_content != nullptr) { for (int32_t i = 0; i < s->page_content->count; i++) { char *page = *(char **)Vector_Get(s->page_content, i); Memory_Free(page); } Vector_Free(s->page_content); } const char *wrapped = UI_Text_WordWrap(text_raw, 1.0f, s->wrap_width); s->page_content = String_Paginate(wrapped, s->wrap_max_lines); Memory_FreePointer(&wrapped); s->max_vis_lines = 0; for (int32_t i = 0; i < s->page_content->count; ++i) { size_t page_lines = 1; const char *c = *(char **)Vector_Get(s->page_content, i); while (*c != '\0') { page_lines += *c++ == '\n'; } CLAMPL(s->max_vis_lines, page_lines); } Memory_FreePointer(&s->last_raw_text); s->last_raw_text = Memory_DupStr(text_raw); s->current_page = 0; } } static bool M_SelectPage(UI_TEXT_DIALOG_STATE *const s, const int32_t new_page) { if (s->page_content == nullptr) { return false; } if (new_page == s->current_page || new_page < 0 || new_page >= s->page_content->count) { return false; } s->current_page = new_page; return true; } UI_TEXT_DIALOG_STATE *UI_TextDialog_Init( size_t wrap_width, size_t wrap_max_lines, bool is_heavy) { UI_TEXT_DIALOG_STATE *s = Memory_Alloc(sizeof(*s)); s->wrap_width = wrap_width; s->wrap_max_lines = wrap_max_lines; s->is_heavy = is_heavy; return s; } void UI_TextDialog_Free(UI_TEXT_DIALOG_STATE *const s) { ASSERT(s != nullptr); Memory_FreePointer(&s->title); Memory_FreePointer(&s->last_raw_title); Memory_FreePointer(&s->last_raw_text); if (s->page_content != nullptr) { for (int32_t i = s->page_content->count - 1; i >= 0; --i) { Memory_Free(*(char **)Vector_Get(s->page_content, i)); } Vector_Free(s->page_content); s->page_content = nullptr; } Memory_Free(s); } void UI_TextDialog_Control(UI_TEXT_DIALOG_STATE *const s) { ASSERT(s != nullptr); const int32_t page_shift = g_InputDB.menu_left ? -1 : (g_InputDB.menu_right ? 1 : 0); if (M_SelectPage(s, s->current_page + page_shift)) { Sound_Effect(SFX_MENU_PASSPORT, nullptr, SPM_ALWAYS); } } void UI_TextDialogEx( UI_TEXT_DIALOG_STATE *const s, const UI_TEXT_DIALOG_SETTINGS settings) { ASSERT(s != nullptr); const char *const title_raw = settings.title_raw; const char *const text_raw = settings.text_raw; if (text_raw == nullptr || String_IsEmpty(text_raw)) { return; } M_UpdateTitle(s, title_raw); M_UpdateText(s, text_raw); UI_BeginModal(0.5f, 0.5f); UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, .spacing = { .v = 5.0f }, .align = { .h = UI_STACK_H_ALIGN_SPAN }, }); UI_BeginFrame( s->is_heavy ? UI_FRAME_DIALOG_BACKGROUND_HEAVY : UI_FRAME_DIALOG_BACKGROUND); UI_BeginPad(M_DIALOG_PADDING, M_DIALOG_PADDING); UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, { .h = UI_STACK_H_ALIGN_SPAN }, }); UI_BeginAnchor(0.5f, 0.5f); UI_Label(s->title != nullptr ? s->title : ""); UI_EndAnchor(); UI_Spacer(M_TITLE_MARGIN, M_TITLE_MARGIN); for (int32_t i = 0; i < s->page_content->count; ++i) { if (i != s->current_page) { UI_BeginResize(-1.0f, 0.0f); } else if (s->page_content->count == 1) { UI_BeginResize(-1.0f, -1.0f); } else { UI_BeginResize(-1.0f, UI_TEXT_HEIGHT * s->max_vis_lines); } UI_Label(*(char **)Vector_Get(s->page_content, i)); UI_EndResize(); } if (s->page_content->count > 1) { UI_Spacer(M_TITLE_MARGIN, M_TITLE_MARGIN * 3); UI_BeginAnchor(1.0f, 0.5f); UI_BeginStack(UI_STACK_HORIZONTAL); if (s->current_page > 0) { UI_Label("\\{button left} "); } char page_indicator[100]; sprintf( page_indicator, *GS_PTR("general/misc/pagination_nav"), s->current_page + 1, s->page_content->count); UI_Label(page_indicator); if (s->current_page < s->page_content->count - 1) { UI_Label(" \\{button right}"); } UI_EndStack(); UI_EndAnchor(); } UI_EndStack(); UI_EndPad(); UI_EndFrame(); if (settings.footer_func != nullptr) { settings.footer_func(settings.footer_user_data); } UI_EndStack(); UI_EndModal(); } void UI_TextDialog( UI_TEXT_DIALOG_STATE *const s, const char *const title_raw, const char *const text_raw) { UI_TextDialogEx( s, (UI_TEXT_DIALOG_SETTINGS) { .title_raw = title_raw, .text_raw = text_raw, }); } ================================================ FILE: src/trx/game/ui/dialogs/text.h ================================================ #pragma once #include #include // A widget to cycle through several pages of a text content. typedef struct UI_TEXT_DIALOG_STATE UI_TEXT_DIALOG_STATE; typedef void (*UI_TEXT_DIALOG_FOOTER_FUNC)(void *user_data); typedef struct { const char *title_raw; const char *text_raw; UI_TEXT_DIALOG_FOOTER_FUNC footer_func; void *footer_user_data; } UI_TEXT_DIALOG_SETTINGS; // state functions UI_TEXT_DIALOG_STATE *UI_TextDialog_Init( size_t wrap_width, size_t wrap_max_lines, bool is_heavy); // Handle page-left/right input. Call before UI_TextDialog(). void UI_TextDialog_Control(UI_TEXT_DIALOG_STATE *state); // Free any allocated buffers. Call when dialog is dismissed. void UI_TextDialog_Free(UI_TEXT_DIALOG_STATE *state); // draw functions // Draw and manage a text dialog in one call. Rewraps/recapitalizes only // when title_raw/text_raw differ (compared via strcmp). Call every frame. void UI_TextDialog( UI_TEXT_DIALOG_STATE *state, const char *title_raw, const char *text_raw); // Same as UI_TextDialog(), with optional settings. void UI_TextDialogEx( UI_TEXT_DIALOG_STATE *state, UI_TEXT_DIALOG_SETTINGS settings); ================================================ FILE: src/trx/game/ui/dialogs.h ================================================ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include ================================================ FILE: src/trx/game/ui/draw.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #define M_WHITE ((RGBA_F) { 1.0f, 1.0f, 1.0f, 1.0f }) #define M_OUTLINE_THICKNESS 0.75f #define M_SCHEDULE_OP(draw_func, inst) \ M_ScheduleOpHelper( \ (M_DRAW_OP_FUNC)draw_func, sizeof(inst), (const M_DRAW_OP *)&inst); struct M_DRAW_OP; typedef void (*M_DRAW_OP_FUNC)(const struct M_DRAW_OP *); typedef struct M_DRAW_OP { M_DRAW_OP_FUNC draw; } M_DRAW_OP; typedef struct { M_DRAW_OP base; UI_STYLE ui_style; int32_t x0, x1, y, z; } M_DRAW_OP_HORZ_LINE; typedef struct { M_DRAW_OP base; UI_STYLE ui_style; int32_t x0, y0, x1, y1, z; TEXT_STYLE text_style; } M_DRAW_OP_TEXT_RECT; typedef struct { M_DRAW_OP base; int32_t sx, sy, z, scale_h, scale_v, sprite_idx; const RGBA_F colors[4]; } M_DRAW_OP_SPRITE; typedef struct { M_DRAW_OP base; int32_t x0, y0, x1, y1, z; RGBA_8888 tl, tr, bl, br; } M_DRAW_OP_QUAD; typedef struct { MEMORY_ARENA_ALLOCATOR alloc; VECTOR *ops; } M_PRIV; static M_PRIV m_Priv = { .alloc = { .default_chunk_size = 1024 * 4, }, }; static void M_DrawScreenQuad( const int32_t x0, const int32_t y0, const int32_t x1, const int32_t y1, const int32_t z, const RGBA_8888 tl, const RGBA_8888 tr, const RGBA_8888 bl, const RGBA_8888 br) { OutputSource_UI_StageQuad((OUTPUT_UI_QUAD) { .x0 = x0, .y0 = y0, .x1 = x1, .y1 = y1, .tl = tl, .tr = tr, .bl = bl, .br = br, .z = Output_GetNearZ_UI() + z, }); } static void M_DrawScreenSprite( const int32_t sx, const int32_t sy, const int32_t sz, const int32_t scale_h, const int32_t scale_v, const int32_t sprite_idx, const RGBA_F colors[4]) { Output_DrawScreenSprite(sx, sy, sz, scale_h, scale_v, sprite_idx, colors); } static void M_DrawScreenGradientBox( const int32_t x0, const int32_t y0, const int32_t x1, const int32_t y1, const int32_t z, const RGBA_8888 tl, const RGBA_8888 tr, const RGBA_8888 bl, const RGBA_8888 br, const float thickness) { const float e = UI_Scaler_Calc(thickness, UI_SCALER_TARGET_TEXT); M_DrawScreenQuad(x0 - e, y0 - e, x1 + e, y0 + e, z, tl, tr, tl, tr); M_DrawScreenQuad(x0 - e, y1 - e, x1 + e, y1 + e, z, bl, br, bl, br); M_DrawScreenQuad(x0 - e, y0 - e, x0 + e, y1 + e, z, tl, tl, bl, bl); M_DrawScreenQuad(x1 - e, y0 - e, x1 + e, y1 + e, z, tr, tr, br, br); } static void M_DrawScreenCentreGradientBox( const int32_t x0, const int32_t y0, const int32_t x1, const int32_t y1, const int32_t z, const RGBA_8888 edge, const RGBA_8888 center_h, const RGBA_8888 center_v, const float thickness) { const float e = UI_Scaler_Calc(thickness, UI_SCALER_TARGET_TEXT); const int32_t xm = (x0 + x1) / 2; const int32_t ym = (y0 + y1) / 2; const RGBA_8888 ch = center_h; const RGBA_8888 cv = center_v; const RGBA_8888 ce = edge; // clang-format off M_DrawScreenQuad(x0 - e, y0 - e, xm, y0 + e, z, ce, ch, ce, ch); M_DrawScreenQuad(xm, y0 - e, x1 + e, y0 + e, z, ch, ce, ch, ce); M_DrawScreenQuad(x0 - e, y1 - e, xm, y1 + e, z, ce, ch, ce, ch); M_DrawScreenQuad(xm, y1 - e, x1 + e, y1 + e, z, ch, ce, ch, ce); M_DrawScreenQuad(x0 - e, y0, x0 + e, ym, z, ce, ce, cv, cv); M_DrawScreenQuad(x0 - e, ym, x0 + e, y1, z, cv, cv, ce, ce); M_DrawScreenQuad(x1 - e, y0, x1 + e, ym, z, ce, ce, cv, cv); M_DrawScreenQuad(x1 - e, ym, x1 + e, y1, z, cv, cv, ce, ce); // clang-format on } static void M_DrawOp_HorizontalLine(const M_DRAW_OP_HORZ_LINE *const op) { if (g_TRVersion == 1 && op->ui_style == UI_STYLE_PC) { const float e = UI_Scaler_Calc(M_OUTLINE_THICKNESS, UI_SCALER_TARGET_TEXT); const UI_MENU_COLORS_PC *const c = UI_Settings_GetMenuColorsPC(); M_DrawScreenQuad( op->x0, op->y - e, op->x1, op->y, op->z, c->outline_light, c->outline_light, c->outline_light, c->outline_light); M_DrawScreenQuad( op->x0, op->y, op->x1, op->y + e, op->z, c->outline_dark, c->outline_dark, c->outline_dark, c->outline_dark); } else if (g_TRVersion == 2 && op->ui_style == UI_STYLE_PC) { const int32_t mesh_idx = Object_Get(O_TEXT_BOX)->mesh_idx; M_DrawScreenSprite( op->x0, op->y, op->z, (op->x1 - op->x0) * PHD_ONE / 8, PHD_ONE, mesh_idx + 4, (RGBA_F[4]) { M_WHITE, M_WHITE, M_WHITE, M_WHITE }); } else if (g_TRVersion == 3 && op->ui_style == UI_STYLE_PC) { const float e1 = UI_Scaler_Calc(1.0f, UI_SCALER_TARGET_TEXT); const float e2 = e1 / 3.0f; const UI_MENU_COLORS_PC *const c = UI_Settings_GetMenuColorsPC(); M_DrawScreenQuad( op->x0, op->y - e1, op->x1, op->y + e1, op->z, c->outline_dark, c->outline_dark, c->outline_dark, c->outline_dark); M_DrawScreenQuad( op->x0, op->y - e2, op->x1, op->y + e2, op->z, c->outline_light, c->outline_light, c->outline_light, c->outline_light); } else { const float e = UI_Scaler_Calc(M_OUTLINE_THICKNESS, UI_SCALER_TARGET_TEXT); const UI_MENU_COLORS_PS1 *const c = UI_Settings_GetMenuColorsPS1(); M_DrawScreenQuad( op->x0, op->y - e, op->x1, op->y + e, op->z, c->outline_bl, c->outline_br, c->outline_bl, c->outline_br); } } static void M_DrawOp_TextBackground(const M_DRAW_OP_TEXT_RECT *const op) { switch (op->ui_style) { case UI_STYLE_PC: { const UI_MENU_COLORS_PC *const c = UI_Settings_GetMenuColorsPC(); const RGBA_8888 *const ramp = op->text_style == TS_BACKGROUND_HEAVY ? c->background_heavy : c->background; M_DrawScreenQuad( op->x0, op->y0, op->x1, op->y1, op->z, ramp[0], ramp[0], ramp[1], ramp[1]); break; } case UI_STYLE_PS1: { const UI_MENU_COLORS_PS1 *const c = UI_Settings_GetMenuColorsPS1(); const int32_t xm = (op->x0 + op->x1) / 2; const int32_t ym = (op->y0 + op->y1) / 2; RGBA_8888 edge, center; switch (op->text_style) { case TS_BACKGROUND_HEAVY: edge = c->background_heavy_edge; center = c->background_heavy_center; break; case TS_HEADING: edge = c->heading_edge; center = c->heading_center; break; case TS_REQUESTED: edge = c->requested_edge; center = c->requested_center; break; default: edge = c->background_edge; center = c->background_center; break; } // clang-format off #define L_DRAW(x0, y0, x1, y1, tl, tr, bl, br) \ M_DrawScreenQuad(x0, y0, x1, y1, op->z, tl, tr, bl, br); L_DRAW(xm, op->y0, op->x0, ym, edge, edge, center, edge ); L_DRAW(op->x1, op->y0, xm, ym, edge, edge, edge, center); L_DRAW(xm, ym, op->x0, op->y1, center, edge, edge, edge ); L_DRAW(op->x1, ym, xm, op->y1, edge, center, edge, edge ); #undef L_DRAW // clang-format on break; } } } static void M_DrawOp_TextOutline(const M_DRAW_OP_TEXT_RECT *const op) { int32_t x0 = op->x0; int32_t x1 = op->x1; int32_t y0 = op->y0; int32_t y1 = op->y1; switch (op->ui_style) { case UI_STYLE_PC: if (g_TRVersion == 1) { const UI_MENU_COLORS_PC *const c = UI_Settings_GetMenuColorsPC(); const float thickness = UI_Scaler_Calc(M_OUTLINE_THICKNESS, UI_SCALER_TARGET_TEXT); Output_DrawScreenFrame( x0, y0, x1 - x0, y1 - y0, c->outline_dark, c->outline_light, thickness); } else if (g_TRVersion == 2) { const int32_t mesh_idx = Object_Get(O_TEXT_BOX)->mesh_idx; const int32_t offset = 4; x0 += offset; y0 += offset; x1 -= offset; y1 -= offset; const int32_t scale_h = PHD_ONE; const int32_t scale_v = PHD_ONE; const int32_t w = (x1 - x0) * PHD_ONE / 8; const int32_t h = (y1 - y0) * PHD_ONE / 8; const RGBA_F neutral[4] = { M_WHITE, M_WHITE, M_WHITE, M_WHITE }; // Corners M_DrawScreenSprite( x0, y0, op->z, scale_h, scale_v, mesh_idx + 0, neutral); M_DrawScreenSprite( x1, y0, op->z, scale_h, scale_v, mesh_idx + 1, neutral); M_DrawScreenSprite( x1, y1, op->z, scale_h, scale_v, mesh_idx + 2, neutral); M_DrawScreenSprite( x0, y1, op->z, scale_h, scale_v, mesh_idx + 3, neutral); // Lines M_DrawScreenSprite( x0, y0, op->z, w, scale_v, mesh_idx + 4, neutral); M_DrawScreenSprite( x1, y0, op->z, scale_h, h, mesh_idx + 5, neutral); M_DrawScreenSprite( x0, y1, op->z, w, scale_v, mesh_idx + 6, neutral); M_DrawScreenSprite( x0, y0, op->z, scale_h, h, mesh_idx + 7, neutral); } else if (g_TRVersion == 3) { const UI_MENU_COLORS_PC *const c = UI_Settings_GetMenuColorsPC(); const float thickness = UI_Scaler_Calc(1.0f, UI_SCALER_TARGET_TEXT); Output_DrawScreenFrame( x0, y0, x1 - x0, y1 - y0, c->outline_dark, c->outline_dark, thickness); Output_DrawScreenFrame( x0, y0, x1 - x0, y1 - y0, c->outline_light, c->outline_light, thickness / 3.0f); } break; case UI_STYLE_PS1: { const UI_MENU_COLORS_PS1 *const c = UI_Settings_GetMenuColorsPS1(); switch (op->text_style) { case TS_BACKGROUND: case TS_BACKGROUND_HEAVY: M_DrawScreenGradientBox( x0, y0, x1, y1, op->z, c->outline_tl, c->outline_tr, c->outline_bl, c->outline_br, M_OUTLINE_THICKNESS); break; case TS_HEADING: M_DrawScreenGradientBox( x0, y0, x1, y1, op->z, c->heading_outline, c->heading_outline, c->heading_outline, c->heading_outline, M_OUTLINE_THICKNESS); break; case TS_REQUESTED: M_DrawScreenCentreGradientBox( x0, y0, x1, y1, op->z, c->requested_outline_edge, c->requested_outline_ch, c->requested_outline_cv, M_OUTLINE_THICKNESS); break; } break; } } } static void M_DrawOp_Sprite(const M_DRAW_OP_SPRITE *const op) { Output_DrawScreenSprite( op->sx, op->sy, op->z, op->scale_h, op->scale_v, op->sprite_idx, op->colors); } static void M_DrawOp_Quad(const M_DRAW_OP_QUAD *const op) { M_DrawScreenQuad( op->x0, op->y0, op->x1, op->y1, op->z, op->tl, op->tr, op->bl, op->br); } // Allocate a new deferred draw operation in the arena. static inline void *M_ArenaAlloc(const size_t sz) { void *p = Memory_ArenaAlloc(&m_Priv.alloc, sz); memset(p, 0, sz); return p; } static inline void M_ScheduleOp(M_DRAW_OP *const op) { M_PRIV *const p = &m_Priv; Vector_Add(p->ops, &op); } static void M_ScheduleOpHelper( const M_DRAW_OP_FUNC draw_func, const size_t size, const M_DRAW_OP *const op_src) { M_DRAW_OP *const op = M_ArenaAlloc(size); memcpy(op, op_src, size); op->draw = draw_func; M_ScheduleOp(op); } void UI_ScheduleDrawTextBackground( const UI_STYLE ui_style, const int32_t sx, const int32_t sy, const int32_t z, const int32_t w, const int32_t h, const TEXT_STYLE text_style) { M_SCHEDULE_OP( M_DrawOp_TextBackground, ((M_DRAW_OP_TEXT_RECT) { .ui_style = ui_style, .x0 = sx, .y0 = sy, .x1 = sx + w, .y1 = sy + h, .z = z, .text_style = text_style, })); } void UI_ScheduleDrawTextOutline( const UI_STYLE ui_style, const int32_t sx, const int32_t sy, const int32_t z, const int32_t w, const int32_t h, const TEXT_STYLE text_style) { M_SCHEDULE_OP( M_DrawOp_TextOutline, ((M_DRAW_OP_TEXT_RECT) { .ui_style = ui_style, .x0 = sx, .y0 = sy, .x1 = sx + w, .y1 = sy + h, .z = z, .text_style = text_style, })); } void UI_ScheduleDrawScreenSprite( const int32_t sx, const int32_t sy, const int32_t z, const int32_t scale_h, const int32_t scale_v, const int32_t sprite_idx, const RGBA_F colors_[4]) { M_SCHEDULE_OP( M_DrawOp_Sprite, ((M_DRAW_OP_SPRITE) { .sx = sx, .sy = sy, .z = z, .scale_h = scale_h, .scale_v = scale_v, .sprite_idx = sprite_idx, .colors = { [0] = colors_[0], [1] = colors_[1], [2] = colors_[2], [3] = colors_[3], }, })); } void UI_ScheduleDrawScreenFlatQuad( const int32_t sx, const int32_t sy, const int32_t z, const int32_t w, const int32_t h, const RGBA_8888 color) { M_SCHEDULE_OP( M_DrawOp_Quad, ((M_DRAW_OP_QUAD) { .x0 = sx, .y0 = sy, .x1 = sx + w, .y1 = sy + h, .z = z, .tl = color, .tr = color, .bl = color, .br = color, })); } void UI_ScheduleDrawScreenGradientQuad( const int32_t sx, const int32_t sy, const int32_t z, const int32_t w, const int32_t h, const RGBA_8888 tl, const RGBA_8888 tr, const RGBA_8888 bl, const RGBA_8888 br) { M_SCHEDULE_OP( M_DrawOp_Quad, ((M_DRAW_OP_QUAD) { .x0 = sx, .y0 = sy, .x1 = sx + w, .y1 = sy + h, .z = z, .tl = tl, .tr = tr, .bl = bl, .br = br, })); } void UI_ScheduleDrawHorizontalLine( const UI_STYLE ui_style, const int32_t x0, const int32_t x1, const int32_t y, const int32_t z) { M_SCHEDULE_OP( M_DrawOp_HorizontalLine, ((M_DRAW_OP_HORZ_LINE) { .ui_style = ui_style, .x0 = x0, .x1 = x1, .y = y, .z = z, })); } void UI_InitDraw(void) { M_PRIV *const p = &m_Priv; if (p->ops == nullptr) { p->ops = Vector_Create(sizeof(M_DRAW_OP *)); } } void UI_ShutdownDraw(void) { M_PRIV *const p = &m_Priv; Memory_ArenaFree(&p->alloc); if (p->ops != nullptr) { Vector_Free(p->ops); p->ops = nullptr; } } void UI_ClearDraw(void) { M_PRIV *const p = &m_Priv; Vector_Clear(p->ops); Memory_ArenaReset(&p->alloc); } void UI_Draw(void) { M_PRIV *const p = &m_Priv; for (int32_t i = 0; i < m_Priv.ops->count; i++) { const M_DRAW_OP *const op = *(M_DRAW_OP **)Vector_Get(m_Priv.ops, i); op->draw(op); } } ================================================ FILE: src/trx/game/ui/draw.h ================================================ #pragma once #include // Schedule deferred UI draw operations to be executed by UI_Draw(). // These record drawing commands during UI_EndScene instead of issuing them // immediately. void UI_ScheduleDrawTextBackground( UI_STYLE ui_style, int32_t sx, int32_t sy, int32_t z, int32_t w, int32_t h, TEXT_STYLE text_style); void UI_ScheduleDrawTextOutline( UI_STYLE ui_style, int32_t sx, int32_t sy, int32_t z, int32_t w, int32_t h, TEXT_STYLE text_style); void UI_ScheduleDrawScreenSprite( int32_t sx, int32_t sy, int32_t z, int32_t scale_h, int32_t scale_v, int32_t sprite_idx, const RGBA_F colors[4]); void UI_ScheduleDrawScreenFlatQuad( int32_t sx, int32_t sy, int32_t z, int32_t w, int32_t h, RGBA_8888 color); void UI_ScheduleDrawScreenGradientQuad( int32_t sx, int32_t sy, int32_t z, int32_t w, int32_t h, RGBA_8888 tl, RGBA_8888 tr, RGBA_8888 bl, RGBA_8888 br); void UI_ScheduleDrawHorizontalLine( UI_STYLE ui_style, int32_t x0, int32_t x1, int32_t y, int32_t z); void UI_InitDraw(void); void UI_ShutdownDraw(void); // Execute all scheduled UI draw operations in order. void UI_Draw(void); ================================================ FILE: src/trx/game/ui/elements/ammo_label.c ================================================ #include #include #include #include #include #include #include #include #include #include #include bool UI_AmmoLabel(void) { const LARA_INFO *const lara = Lara_GetLaraInfo(); int32_t ammo = 0; const bool use_icon = g_TRVersion == 1; const char *icon_str = nullptr; const ITEM *const vehicle_item = Lara_Vehicle_GetItem(); if (vehicle_item != nullptr && vehicle_item->object_id == O_UPV) { ammo = lara->harpoon_ammo.ammo; } else { if (lara->gun_status != LGS_READY || Game_IsBonusFlagSet(GBF_NGPLUS)) { return false; } switch (lara->gun_type) { case LGT_PISTOLS: return false; case LGT_SHOTGUN: ammo = lara->shotgun_ammo.ammo / Gun_GetAmmoClipCount(LGT_SHOTGUN); if (use_icon) { icon_str = "\\{ammo shotgun}"; } break; case LGT_UZIS: ammo = lara->uzi_ammo.ammo; if (use_icon) { icon_str = "\\{ammo uzis}"; } break; case LGT_MAGNUMS: ammo = lara->magnum_ammo.ammo; if (use_icon) { icon_str = "\\{ammo magnums}"; } break; case LGT_AUTOS: ammo = lara->autos_ammo.ammo; break; case LGT_DESERT_EAGLE: ammo = lara->desert_eagle_ammo.ammo; break; case LGT_M16: ammo = lara->m16_ammo.ammo; break; case LGT_MP5: ammo = lara->mp5_ammo.ammo; break; case LGT_GRENADE: ammo = lara->grenade_ammo.ammo; break; case LGT_ROCKET: ammo = lara->rocket_ammo.ammo; break; case LGT_HARPOON: ammo = lara->harpoon_ammo.ammo; break; default: return false; } } const char *inner_text = nullptr; if (icon_str != nullptr) { inner_text = String_FormatStatic("%6d %s", ammo, icon_str); } else { inner_text = String_FormatStatic("%6d", ammo); } const char *const outer_text = String_FormatStatic( g_Config.ui.menu_style == UI_STYLE_PS1 ? GS("general/overlay/item_count_fmt_ps1") : GS("general/overlay/item_count_fmt_pc"), inner_text); UI_LabelEx(outer_text, (UI_LABEL_SETTINGS) { .scale = 1.5f }); return true; } ================================================ FILE: src/trx/game/ui/elements/ammo_label.h ================================================ #pragma once #include bool UI_AmmoLabel(void); ================================================ FILE: src/trx/game/ui/elements/anchor.c ================================================ #include #include #include typedef struct { float x; float y; } M_DATA; static void M_Measure(UI_NODE *const node) { node->measure_w = UI_GetCanvasWidth(); node->measure_h = UI_GetCanvasHeight() - UI_TEXT_HEIGHT; } static void M_Layout( UI_NODE *const node, const float x, const float y, const float w, const float h) { UI_LayoutBasic(node, x, y, w, h); const M_DATA *const data = node->data; UI_NODE *child = node->first_child; while (child != nullptr) { const float cw = child->measure_w; const float ch = child->measure_h; const float cx = x + (w - cw) * data->x; const float cy = y + (h - ch) * data->y; child->ops.layout(child, cx, cy, cw, ch); child = child->next_sibling; } } void UI_BeginAnchor(const float x, const float y) { UI_NODE *const node = UI_AllocNode( &(UI_WIDGET_OPS) { .measure = UI_MeasureWrapper, .layout = M_Layout, .draw = UI_DrawWrapper, }, sizeof(M_DATA)); M_DATA *const data = node->data; data->x = x; data->y = y; UI_AddChild(node); UI_PushCurrent(node); } void UI_EndAnchor(void) { UI_PopCurrent(); } ================================================ FILE: src/trx/game/ui/elements/anchor.h ================================================ #pragma once #include // Used to align a top-level widget to the screen center or to the screen edges. // Uses ratio inputs. void UI_BeginAnchor(const float x, const float y); void UI_EndAnchor(void); ================================================ FILE: src/trx/game/ui/elements/bar.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef struct { int32_t x, y, w, h; } M_RECT_32; typedef struct { UI_BAR_SETTINGS settings; const UI_BAR_THEME *theme; float scale; } M_DATA; static void M_Measure(UI_NODE *const node) { M_DATA *const data = node->data; const float scale = UI_Scaler_GetScale( data->settings.preview ? UI_SCALER_TARGET_TEXT : UI_SCALER_TARGET_BAR) * data->scale; node->measure_w = data->settings.w * scale; node->measure_h = data->settings.h * scale; } static void M_DrawBackground( const UI_BAR_THEME *const theme, const M_RECT_32 rect) { UI_ScheduleDrawScreenFlatQuad( rect.x, rect.y, 0, rect.w, rect.h, (RGBA_8888) { 0, 0, 0, 255 }); } static void M_DrawBorderPC( const UI_BAR_THEME *const theme, const M_RECT_32 rect, const float border) { UI_ScheduleDrawScreenFlatQuad( rect.x, rect.y, 0, rect.w, rect.h, theme->border_light); UI_ScheduleDrawScreenFlatQuad( rect.x + border, rect.y + border, 0, rect.w - border, rect.h - border, theme->border_dark); } static void M_DrawBorderPS1( const UI_BAR_THEME *const theme, const M_RECT_32 rect, const float border) { #if 0 Output_DrawScreenGradientQuad( rect.x - border, rect.y + border, 0, rect.w + border * 2, rect.h - border * 2, theme->border_bl, theme->border_br, theme->border_br, theme->border_bl); #endif Output_DrawScreenGradientQuad( rect.x, rect.y, 0, rect.w, rect.h, theme->border_tl, theme->border_tr, theme->border_br, theme->border_bl); } static void M_DrawFillPC( const UI_BAR_THEME *const theme, const UI_BAR_SETTINGS *const settings, const M_RECT_32 rect, const float percent) { if (g_Config.ui.enable_smooth_bars) { for (int32_t i = 0; i < UI_BAR_COLOR_STEPS - 1; i++) { const RGBA_8888 c1 = theme->ramp[i]; const RGBA_8888 c2 = theme->ramp[i + 1]; const int32_t lsy = rect.y + i * rect.h / (UI_BAR_COLOR_STEPS - 1); const int32_t lsh = rect.y + (i + 1) * rect.h / (UI_BAR_COLOR_STEPS - 1) - lsy; UI_ScheduleDrawScreenGradientQuad( rect.x, lsy, 0, rect.w, lsh, c1, c1, c2, c2); } } else { for (int32_t i = 0; i < UI_BAR_COLOR_STEPS; i++) { const RGBA_8888 c = theme->ramp[i]; const int32_t lsy = rect.y + i * rect.h / UI_BAR_COLOR_STEPS; const int32_t lsh = rect.y + (i + 1) * rect.h / UI_BAR_COLOR_STEPS - lsy; UI_ScheduleDrawScreenFlatQuad(rect.x, lsy, 0, rect.w, lsh, c); } } } static void M_DrawFillPS1( const UI_BAR_THEME *const theme, const UI_BAR_SETTINGS *const settings, const M_RECT_32 rect, const float percent) { const UI_BAR_TYPE type = settings->type; if (g_Config.ui.enable_smooth_bars) { for (int32_t i = 0; i < UI_BAR_COLOR_STEPS - 1; i++) { const RGBA_8888 ctl = theme->ramp_left[i]; const RGBA_8888 ctr = theme->ramp_right[i]; const RGBA_8888 cbl = theme->ramp_left[i + 1]; const RGBA_8888 cbr = theme->ramp_right[i + 1]; const RGBA_8888 ctrm = Color_Mix(ctl, ctr, percent); const RGBA_8888 cbrm = Color_Mix(cbl, cbr, percent); const int32_t lsy = rect.y + i * rect.h / (UI_BAR_COLOR_STEPS - 1); const int32_t lsh = rect.y + (i + 1) * rect.h / (UI_BAR_COLOR_STEPS - 1) - lsy; UI_ScheduleDrawScreenGradientQuad( rect.x, lsy, 0, rect.w, lsh, ctl, ctrm, cbl, cbrm); } } else { for (int32_t i = 0; i < UI_BAR_COLOR_STEPS; i++) { const RGBA_8888 cl = theme->ramp_left[i]; const RGBA_8888 cr = theme->ramp_right[i]; const RGBA_8888 crm = Color_Mix(cl, cr, percent); const int32_t lsy = rect.y + i * rect.h / UI_BAR_COLOR_STEPS; const int32_t lsh = rect.y + (i + 1) * rect.h / UI_BAR_COLOR_STEPS - lsy; UI_ScheduleDrawScreenGradientQuad( rect.x, lsy, 0, rect.w, lsh, cl, crm, cl, crm); } } } static void M_Draw(const UI_NODE *const node) { M_DATA *const data = node->data; const UI_BAR_SETTINGS *const settings = &data->settings; float percent = settings->value / (float)MAX(1, settings->max_value); CLAMP(percent, 0.0f, 1.0f); percent = (int32_t)(percent * 100) / 100.0f; // Convert everything to screen coordinates const int32_t x = UI_ScaleX(node->x); const int32_t y = UI_ScaleY(node->y); const int32_t w = UI_ScaleX(node->w); const int32_t h = UI_ScaleY(node->h); const int32_t border = h / (float)(UI_BAR_COLOR_STEPS + 4); const int32_t padding = h / (float)(UI_BAR_COLOR_STEPS + 4); const M_RECT_32 outer_rect = { .x = x, .y = y, .w = w, .h = h, }, inner_rect = { .x = outer_rect.x + border, .y = outer_rect.y + border, .w = outer_rect.w - border * 2, .h = outer_rect.h - border * 2, }, bar_rect = { .x = inner_rect.x + padding, .y = inner_rect.y + padding, .w = (inner_rect.w - padding * 2) * percent, .h = inner_rect.h - padding * 2, }; switch (data->theme->kind) { case UI_BAR_THEME_PC_KIND: M_DrawBorderPC(data->theme, outer_rect, border); M_DrawBackground(data->theme, inner_rect); if (percent > 0.0f) { M_DrawFillPC(data->theme, settings, bar_rect, percent); } break; case UI_BAR_THEME_PS1_KIND: M_DrawBorderPS1(data->theme, outer_rect, border); M_DrawBackground(data->theme, inner_rect); if (percent > 0.0f) { M_DrawFillPS1(data->theme, settings, bar_rect, percent); } break; } } void UI_Bar(const UI_BAR_SETTINGS settings) { UI_NODE *const node = UI_AllocNode( &(UI_WIDGET_OPS) { .measure = M_Measure, .layout = UI_LayoutBasic, .draw = M_Draw, }, sizeof(M_DATA)); M_DATA *const data = node->data; data->settings = settings; data->theme = UI_Settings_GetBarTheme(settings.type); data->scale = data->settings.preview ? 1.0f : data->theme->basic_scale; UI_AddChild(node); } ================================================ FILE: src/trx/game/ui/elements/bar.h ================================================ #pragma once #include #include #include #include // shared properties of common ingame bars #define UI_BAR_WIDTH 208.0f #define UI_BAR_HEIGHT 18.0f #define UI_BAR_BORDER 2.0f #define UI_BAR_PADDING 2.0f #define UI_BAR_BLINK_THRESHOLD (g_TRVersion == 1 ? 0.2f : 0.25f) typedef struct { UI_BAR_TYPE type; int32_t w; int32_t h; int32_t value; int32_t max_value; bool preview; } UI_BAR_SETTINGS; // draw functions void UI_Bar(UI_BAR_SETTINGS settings); ================================================ FILE: src/trx/game/ui/elements/bar_enemy_hp.c ================================================ #include #include #include #include #include #include #include #include bool UI_EnemyHealthBar(void) { const ITEM *const target = Lara_GetLaraInfo()->target; if (target == nullptr) { return false; } const bool is_ally = Creature_IsAlly(target); bool show = g_Config.ui.show_bars; switch (g_Config.ui.enemy_health_bar.show_mode) { case BAR_SHOW_MODE_NEVER: show &= false; break; case BAR_SHOW_MODE_ALWAYS: show &= true; break; case BAR_SHOW_MODE_BOSS_ONLY: show &= Object_IsType(target->object_id, g_BossObjects); break; } if (!show) { return false; } UI_Bar((UI_BAR_SETTINGS) { .type = is_ally ? UI_BAR_ALLY_HP : UI_BAR_ENEMY_HP, .w = UI_BAR_WIDTH, .h = UI_BAR_HEIGHT, .value = target->hit_points, .max_value = target->max_hit_points * (Game_IsBonusFlagSet(GBF_NGPLUS) ? 2 : 1), }); return true; } ================================================ FILE: src/trx/game/ui/elements/bar_enemy_hp.h ================================================ #pragma once #include // draw functions bool UI_EnemyHealthBar(void); ================================================ FILE: src/trx/game/ui/elements/bar_lara_air.c ================================================ #include #include #include #include #include bool UI_LaraAirBar(const bool blink_state) { const ITEM *const lara_item = Lara_GetItem(); if (lara_item == nullptr) { return false; } const LARA_INFO *const lara = Lara_GetLaraInfo(); const bool is_blinking = g_Config.ui.enable_bar_flashing && lara->air <= LARA_MAX_AIR * UI_BAR_BLINK_THRESHOLD; const ROOM *const room = Room_Get(lara_item->room_num); const bool show = g_Config.ui.show_bars && (lara->water_status == LWS_UNDERWATER || lara->water_status == LWS_SURFACE || (room->flags.swamp && lara->air < LARA_MAX_AIR) || (lara->water_status == LWS_ABOVE_WATER && Lara_Vehicle_IsOnType(O_UPV))); if (!show) { return false; } UI_Bar((UI_BAR_SETTINGS) { .type = UI_BAR_LARA_AIR, .w = UI_BAR_WIDTH, .h = UI_BAR_HEIGHT, .value = is_blinking && blink_state ? 0 : lara->air, .max_value = LARA_MAX_AIR, }); return true; } ================================================ FILE: src/trx/game/ui/elements/bar_lara_air.h ================================================ #pragma once #include // draw functions bool UI_LaraAirBar(bool blink_state); ================================================ FILE: src/trx/game/ui/elements/bar_lara_exposure.c ================================================ #include #include #include #include #include bool UI_LaraExposureBar(const bool blink_state) { const ITEM *const lara_item = Lara_GetItem(); if (lara_item == nullptr) { return false; } const LARA_INFO *const lara = Lara_GetLaraInfo(); const bool is_blinking = g_Config.ui.enable_bar_flashing && lara->exposure_timer <= LARA_MAX_EXPOSURE * UI_BAR_BLINK_THRESHOLD; const bool show = g_Config.ui.show_bars && lara->exposure_timer < LARA_MAX_EXPOSURE; if (!show) { return false; } int32_t value = is_blinking && blink_state ? 0 : lara->exposure_timer; CLAMPL(value, 0); UI_Bar((UI_BAR_SETTINGS) { .type = UI_BAR_LARA_EXPOSURE, .w = UI_BAR_WIDTH, .h = UI_BAR_HEIGHT, .value = value, .max_value = LARA_MAX_EXPOSURE, }); return true; } ================================================ FILE: src/trx/game/ui/elements/bar_lara_exposure.h ================================================ #pragma once #include // draw functions bool UI_LaraExposureBar(bool blink_state); ================================================ FILE: src/trx/game/ui/elements/bar_lara_hp.c ================================================ #include #include #include #include #include static int32_t m_OldHealth = 0; static int32_t m_HitTimer = 0; void UI_LaraHealthBar_Control(void) { m_HitTimer--; CLAMPL(m_HitTimer, 0); } void UI_LaraHealthBar_SetTimer(const int16_t timer) { m_HitTimer = timer; } bool UI_LaraHealthBar(const bool blink_state, const bool force) { const ITEM *const lara_item = Lara_GetItem(); if (lara_item == nullptr) { return false; } const LARA_INFO *const lara = Lara_GetLaraInfo(); int32_t health = lara_item->hit_points; CLAMP(health, 0, LARA_MAX_HITPOINTS); if (m_OldHealth != health) { m_OldHealth = health; m_HitTimer = 40; } const bool is_blinking = g_Config.ui.enable_bar_flashing && (health <= LARA_MAX_HITPOINTS * UI_BAR_BLINK_THRESHOLD || lara->poison_timer != 0); const bool is_recently_hurt = m_HitTimer > 0; const bool show = force || (g_Config.ui.show_bars && (is_recently_hurt || health <= 0 || lara->gun_status == LGS_READY || lara->poison_timer != 0 || is_blinking)); if (!show) { return false; } UI_Bar((UI_BAR_SETTINGS) { .type = lara->poison_timer != 0 ? UI_BAR_LARA_HP_POISON : UI_BAR_LARA_HP, .w = UI_BAR_WIDTH, .h = UI_BAR_HEIGHT, .value = is_blinking && blink_state ? 0 : health, .max_value = LARA_MAX_HITPOINTS, }); return true; } ================================================ FILE: src/trx/game/ui/elements/bar_lara_hp.h ================================================ #pragma once #include #include // state functions void UI_LaraHealthBar_Control(void); void UI_LaraHealthBar_SetTimer(int16_t timer); // draw functions bool UI_LaraHealthBar(bool blink_state, bool force); ================================================ FILE: src/trx/game/ui/elements/bar_lara_sprint.c ================================================ #include #include #include #include #include bool UI_LaraSprintBar(void) { const ITEM *const lara_item = Lara_GetItem(); if (lara_item == nullptr) { return false; } const LARA_INFO *const lara = Lara_GetLaraInfo(); const bool show = g_Config.ui.show_bars && lara->sprint_timer < LARA_MAX_SPRINT; if (!show) { return false; } UI_Bar((UI_BAR_SETTINGS) { .type = UI_BAR_LARA_STAMINA, .w = UI_BAR_WIDTH, .h = UI_BAR_HEIGHT, .value = lara->sprint_timer, .max_value = LARA_MAX_SPRINT, }); return true; } ================================================ FILE: src/trx/game/ui/elements/bar_lara_sprint.h ================================================ #pragma once bool UI_LaraSprintBar(void); ================================================ FILE: src/trx/game/ui/elements/button_label.c ================================================ #include #include #include #include void UI_ButtonLabel(INPUT_ROLE input_role, const char *const label) { UI_ButtonLabelEx(Input_GetRoleName(input_role), label); } void UI_ButtonLabelEx(const char *const input_label, const char *const label) { UI_LabelFmt("\\{button empty} %s: %s", input_label, label); } ================================================ FILE: src/trx/game/ui/elements/button_label.h ================================================ #pragma once #include #include void UI_ButtonLabel(INPUT_ROLE input_role, const char *label); void UI_ButtonLabelEx(const char *input_label, const char *label); ================================================ FILE: src/trx/game/ui/elements/color_swatch.c ================================================ #include #include #include #include #include #include typedef struct { float w; float h; RGBA_8888 color; } M_DATA; static void M_Measure(UI_NODE *const node) { const M_DATA *const data = node->data; node->measure_w = data->w * g_Config.ui.text_scale; node->measure_h = data->h * g_Config.ui.text_scale; } static void M_Draw(const UI_NODE *const node) { const M_DATA *const data = node->data; const int32_t x = UI_ScaleX(node->x); const int32_t y = UI_ScaleY(node->y); const int32_t w = UI_ScaleX(node->w); const int32_t h = UI_ScaleY(node->h); const int32_t border = MAX(1, UI_ScaleX(1.0f)); UI_ScheduleDrawScreenFlatQuad(x, y, 0, w, h, COLOR_RGBA_8888_BLACK); UI_ScheduleDrawScreenFlatQuad( x + border, y + border, 0, w - border * 2, h - border * 2, data->color); UI_ScheduleDrawTextOutline( g_Config.ui.menu_style, x + border, y + border, 0, w - border * 2, h - border * 2, TS_HEADING); } void UI_ColorSwatch(const UI_COLOR_SWATCH_SETTINGS settings) { ASSERT(settings.w > 0.0f); ASSERT(settings.h > 0.0f); UI_NODE *const node = UI_AllocNode( &(UI_WIDGET_OPS) { .measure = M_Measure, .layout = UI_LayoutBasic, .draw = M_Draw, }, sizeof(M_DATA)); M_DATA *const data = node->data; data->w = settings.w; data->h = settings.h; data->color = Color_RGBToRGBA(settings.color); UI_AddChild(node); } ================================================ FILE: src/trx/game/ui/elements/color_swatch.h ================================================ #pragma once #include typedef struct { RGB_888 color; float w; float h; } UI_COLOR_SWATCH_SETTINGS; void UI_ColorSwatch(UI_COLOR_SWATCH_SETTINGS settings); ================================================ FILE: src/trx/game/ui/elements/flash.c ================================================ #include #include #include void UI_Flash_Init(UI_FLASH_STATE *const s, const int32_t rate) { s->count = 0; s->rate = rate; } void UI_Flash_Free(UI_FLASH_STATE *const s) { } void UI_Flash_Control(UI_FLASH_STATE *const s) { s->count -= 2; if (s->count < -s->rate) { s->count = s->rate; } } void UI_BeginFlash(const UI_FLASH_STATE *const s) { UI_NODE *const node = UI_AllocNode( &(UI_WIDGET_OPS) { .measure = UI_MeasureWrapper, .layout = UI_LayoutWrapper, .draw = UI_DrawWrapper, }, 0); UI_AddChild(node); UI_PushCurrent(node); UI_BeginHide(s->count >= 0); } void UI_EndFlash(void) { UI_EndHide(); UI_PopCurrent(); } ================================================ FILE: src/trx/game/ui/elements/flash.h ================================================ #pragma once #include #include // Make the child widget invisible in the specified interval. typedef struct { int32_t rate; int32_t count; } UI_FLASH_STATE; // state functions void UI_Flash_Init(UI_FLASH_STATE *s, int32_t rate); void UI_Flash_Free(UI_FLASH_STATE *s); void UI_Flash_Control(UI_FLASH_STATE *s); // draw functions void UI_BeginFlash(const UI_FLASH_STATE *s); void UI_EndFlash(void); ================================================ FILE: src/trx/game/ui/elements/fps_counter.c ================================================ #include #include #include #include #include typedef struct UI_FPS_COUNTER_STATE { int32_t drawn_frames; int32_t fps_counter; CLOCK_TIMER timer; } UI_FPS_COUNTER_STATE; UI_FPS_COUNTER_STATE *UI_FPSCounter_Init(void) { UI_FPS_COUNTER_STATE *const s = Memory_Alloc(sizeof(UI_FPS_COUNTER_STATE)); s->timer.type = CLOCK_TIMER_REAL; return s; } void UI_FPSCounter_Free(UI_FPS_COUNTER_STATE *const s) { if (s != nullptr) { Memory_Free(s); } } void UI_FPSCounter(UI_FPS_COUNTER_STATE *const s) { UI_LabelFmt("%d FPS", s->fps_counter); s->drawn_frames++; if (ClockTimer_CheckElapsedAndTake(&s->timer, 1.0)) { s->fps_counter = s->drawn_frames; s->drawn_frames = 0; } } ================================================ FILE: src/trx/game/ui/elements/fps_counter.h ================================================ #pragma once #include typedef struct UI_FPS_COUNTER_STATE UI_FPS_COUNTER_STATE; // state functions UI_FPS_COUNTER_STATE *UI_FPSCounter_Init(void); void UI_FPSCounter_Free(UI_FPS_COUNTER_STATE *s); // draw functions void UI_FPSCounter(UI_FPS_COUNTER_STATE *s); ================================================ FILE: src/trx/game/ui/elements/frame.c ================================================ #include #include #include #include #include typedef struct { UI_STYLE ui_style; TEXT_STYLE text_style; int32_t outline_z; int32_t background_z; } M_DATA; static void M_Draw(const UI_NODE *node) { const M_DATA *const data = node->data; if (data->background_z >= 0) { UI_ScheduleDrawTextBackground( data->ui_style, UI_ScaleX(node->x), UI_ScaleY(node->y), data->background_z, UI_ScaleX(node->w), UI_ScaleY(node->h), data->text_style); } if (data->outline_z >= 0) { UI_ScheduleDrawTextOutline( data->ui_style, UI_ScaleX(node->x), UI_ScaleY(node->y), data->outline_z, UI_ScaleX(node->w), UI_ScaleY(node->h), data->text_style); } UI_DrawWrapper(node); } void UI_BeginFrame(UI_FRAME_STYLE style) { UI_NODE *const node = UI_AllocNode( &(UI_WIDGET_OPS) { .measure = UI_MeasureWrapper, .layout = UI_LayoutWrapper, .draw = M_Draw, }, sizeof(M_DATA)); M_DATA *const data = node->data; data->ui_style = g_Config.ui.menu_style; switch (style) { case UI_FRAME_DIALOG_BACKGROUND: data->outline_z = 160; data->background_z = 160; data->text_style = TS_BACKGROUND; break; case UI_FRAME_DIALOG_BACKGROUND_HEAVY: data->outline_z = 160; data->background_z = 160; data->text_style = TS_BACKGROUND_HEAVY; break; case UI_FRAME_DIALOG_HEADING: data->outline_z = 80; data->background_z = 80; data->text_style = TS_HEADING; break; case UI_FRAME_SELECTED_OPTION: data->outline_z = 80; data->background_z = 80; data->text_style = TS_REQUESTED; break; case UI_FRAME_OUTLINE_ONLY: data->outline_z = 80; data->background_z = -1; data->text_style = TS_REQUESTED; break; } UI_AddChild(node); UI_PushCurrent(node); } void UI_EndFrame(void) { UI_PopCurrent(); } ================================================ FILE: src/trx/game/ui/elements/frame.h ================================================ #pragma once #include // A frame around the child widget. typedef enum { UI_FRAME_DIALOG_BACKGROUND, UI_FRAME_DIALOG_BACKGROUND_HEAVY, UI_FRAME_DIALOG_HEADING, UI_FRAME_SELECTED_OPTION, UI_FRAME_OUTLINE_ONLY, } UI_FRAME_STYLE; void UI_BeginFrame(UI_FRAME_STYLE style); void UI_EndFrame(void); ================================================ FILE: src/trx/game/ui/elements/gradient_slider.c ================================================ #include #include #include #include #include #include #include #include #include #include #define M_MARKER_BORDER 1.0f #define M_MARKER_WIDTH 4.0f typedef struct { float width; float value; int32_t stop_count; } M_DATA; static void M_DrawMarker(const float x, const float y, const float h) { const float inner_w_s = UI_ScaleX(M_MARKER_WIDTH); const float inner_h_s = UI_ScaleY(h); const int32_t border_s = UI_ScaleX(M_MARKER_BORDER); // Compute pixel‑snapped corners so both sides are balanced const int32_t inner_x1 = floorf(x - inner_w_s * 0.5f + 0.5f); const int32_t inner_y1 = floorf(y - inner_h_s * 0.5f + 0.5f); const int32_t inner_x2 = floorf(x + inner_w_s * 0.5f + 0.5f); const int32_t inner_y2 = floorf(y + inner_h_s * 0.5f + 0.5f); const int32_t outer_x1 = inner_x1 - border_s; const int32_t outer_y1 = inner_y1 - border_s; const int32_t outer_x2 = inner_x2 + border_s; const int32_t outer_y2 = inner_y2 + border_s; // Derive final snapped widths/heights const int32_t inner_w = inner_x2 - inner_x1; const int32_t inner_h = inner_y2 - inner_y1; const int32_t outer_w = outer_x2 - outer_x1; const int32_t outer_h = outer_y2 - outer_y1; UI_ScheduleDrawScreenFlatQuad( outer_x1, outer_y1, 0, outer_w, outer_h, COLOR_RGBA_8888_BLACK); UI_ScheduleDrawScreenFlatQuad( inner_x1, inner_y1, 0, inner_w, inner_h, COLOR_RGBA_8888_WHITE); UI_ScheduleDrawTextOutline( g_Config.ui.menu_style, outer_x1, outer_y1, 0, outer_w - 1, outer_h - 1, TS_REQUESTED); } static void M_Draw(const UI_NODE *const node) { const M_DATA *const data = node->data; const RGB_888 *const stops = (const RGB_888 *)(data + 1); const float x = UI_ScaleX(node->x); const float y = UI_ScaleY(node->y); const float w = UI_ScaleX(node->w); const float h = UI_ScaleY(node->h); const float border = UI_ScaleX(1.0f); const float inner_x = x + border; const float inner_y = y + border; const float inner_w = w - border * 2; const float inner_h = h - border * 2; UI_ScheduleDrawScreenFlatQuad(x, y, 0, w, h, COLOR_RGBA_8888_BLACK); if (data->stop_count == 1) { UI_ScheduleDrawScreenFlatQuad( inner_x, inner_y, 0, inner_w, inner_h, Color_RGBToRGBA(stops[0])); } else { for (int32_t i = 0; i < data->stop_count - 1; i++) { const int32_t sx = inner_x + i * inner_w / (data->stop_count - 1); const int32_t sw = inner_x + (i + 1) * inner_w / (data->stop_count - 1) - sx; UI_ScheduleDrawScreenGradientQuad( sx, inner_y, 0, MAX(1, sw), inner_h, Color_RGBToRGBA(stops[i]), Color_RGBToRGBA(stops[i + 1]), Color_RGBToRGBA(stops[i]), Color_RGBToRGBA(stops[i + 1])); } } float marker_x = inner_x + data->value * inner_w; float marker_y = inner_y + inner_h * 0.5f; M_DrawMarker(marker_x, marker_y, node->h); } static void M_Measure(UI_NODE *const node) { const M_DATA *const data = node->data; node->measure_w = data->width; node->measure_h = UI_TEXT_HEIGHT * 0.5f * g_Config.ui.text_scale; } void UI_GradientSlider(const UI_GRADIENT_SLIDER_SETTINGS settings) { ASSERT(settings.stop_count > 0); ASSERT(settings.stops != nullptr); const size_t extra_size = sizeof(M_DATA) + sizeof(RGB_888) * settings.stop_count; UI_NODE *const node = UI_AllocNode( &(UI_WIDGET_OPS) { .measure = M_Measure, .layout = UI_LayoutBasic, .draw = M_Draw, }, extra_size); M_DATA *const data = node->data; data->width = settings.width; data->value = settings.value; CLAMP(data->value, 0.0f, 1.0f); data->stop_count = settings.stop_count; RGB_888 *const stops = (RGB_888 *)(data + 1); memcpy(stops, settings.stops, sizeof(RGB_888) * settings.stop_count); UI_AddChild(node); } ================================================ FILE: src/trx/game/ui/elements/gradient_slider.h ================================================ #pragma once #include #include typedef struct { float width; float value; int32_t stop_count; const RGB_888 *stops; } UI_GRADIENT_SLIDER_SETTINGS; void UI_GradientSlider(UI_GRADIENT_SLIDER_SETTINGS settings); ================================================ FILE: src/trx/game/ui/elements/hide.c ================================================ #include #include static void M_Draw(const UI_NODE *const node) { const bool draw_children = *(bool *)node->data; if (draw_children) { UI_DrawWrapper(node); } } void UI_BeginHide(const bool hide_children) { UI_NODE *const node = UI_AllocNode( &(UI_WIDGET_OPS) { .measure = UI_MeasureWrapper, .layout = UI_LayoutWrapper, .draw = M_Draw, }, sizeof(bool)); *(bool *)node->data = !hide_children; UI_AddChild(node); UI_PushCurrent(node); } void UI_EndHide(void) { UI_PopCurrent(); } ================================================ FILE: src/trx/game/ui/elements/hide.h ================================================ #pragma once #include // Make the child widget invisible if the given flag is true. // The children still take up their size. void UI_BeginHide(bool hide_children); void UI_EndHide(void); ================================================ FILE: src/trx/game/ui/elements/horizontal_line.c ================================================ #include #include #include #include #include static void M_Draw(const UI_NODE *node) { // UI_DrawWrapper(node); UI_ScheduleDrawHorizontalLine( g_Config.ui.menu_style, UI_ScaleX(node->x), UI_ScaleX(node->x + node->w), UI_ScaleY(node->y + node->h / 2.0f), 0); } static void M_Measure(UI_NODE *const node) { UI_MeasureWrapper(node); node->measure_h = 2 * g_Config.ui.text_scale; } void UI_HorizontalLine(void) { UI_NODE *const node = UI_AllocNode( &(UI_WIDGET_OPS) { .measure = M_Measure, .layout = UI_LayoutBasic, .draw = M_Draw, }, 0); UI_AddChild(node); } ================================================ FILE: src/trx/game/ui/elements/horizontal_line.h ================================================ #pragma once #include void UI_HorizontalLine(void); ================================================ FILE: src/trx/game/ui/elements/label.c ================================================ #include #include #include #include #include #include #include typedef struct { UI_LABEL_SETTINGS settings; char *text; } M_DATA; static UI_LABEL_SETTINGS m_DefaultSettings = { .scale = 1.0f }; static void M_Measure(UI_NODE *const node) { M_DATA *const data = node->data; float w = 0.0f, h = 0.0f; UI_Text_Measure(data->text, &w, &h, data->settings); node->measure_w = w; node->measure_h = h; } static void M_Draw(const UI_NODE *const node) { M_DATA *const data = node->data; UI_Text_Draw(data->text, node->x, node->y, data->settings); UI_DrawWrapper(node); } void UI_Label(const char *const text) { UI_LabelEx(text, m_DefaultSettings); } void UI_LabelFmt(const char *fmt, ...) { va_list args; va_start(args, fmt); const char *const text = String_FormatStaticV(fmt, args); va_end(args); UI_Label(text); } void UI_LabelEx(const char *text, const UI_LABEL_SETTINGS settings) { if (text == nullptr) { text = "(null)"; // quality of life for UI development } UI_NODE *const node = UI_AllocNode( &(UI_WIDGET_OPS) { .measure = M_Measure, .layout = UI_LayoutBasic, .draw = M_Draw, }, sizeof(M_DATA) + (text != nullptr ? strlen(text) + 1 : 1)); M_DATA *const data = node->data; data->settings = settings; data->text = (char *)node->data + sizeof(M_DATA); strcpy(data->text, text != nullptr ? text : ""); UI_AddChild(node); } void UI_Label_Measure( const char *const text, float *const out_w, float *const out_h) { UI_Label_MeasureEx(text, out_w, out_h, m_DefaultSettings); } float UI_Label_MeasureW(const char *const text) { float result; UI_Label_Measure(text, &result, nullptr); return result; } void UI_Label_MeasureEx( const char *const text, float *const out_w, float *const out_h, const UI_LABEL_SETTINGS settings) { float w = 0.0f, h = 0.0f; UI_Text_Measure(text, &w, &h, settings); if (out_w != nullptr) { *out_w = w; } if (out_h != nullptr) { *out_h = h; } } ================================================ FILE: src/trx/game/ui/elements/label.h ================================================ #pragma once #include #include // Basic text widget. typedef UI_TEXT_SETTINGS UI_LABEL_SETTINGS; void UI_Label(const char *text); void UI_LabelFmt(const char *fmt, ...); void UI_LabelEx(const char *text, UI_LABEL_SETTINGS settings); void UI_Label_Measure(const char *text, float *out_w, float *out_h); float UI_Label_MeasureW(const char *text); void UI_Label_MeasureEx( const char *text, float *out_w, float *out_h, UI_LABEL_SETTINGS settings); ================================================ FILE: src/trx/game/ui/elements/modal.c ================================================ #include #include #include #include #include static void M_Measure(UI_NODE *const node) { node->measure_w = UI_GetCanvasWidth(); node->measure_h = UI_GetCanvasHeight(); } static void M_Draw(const UI_NODE *const node) { UI_DrawWrapper(node); } void UI_BeginModal(const float x, const float y) { UI_NODE *const node = UI_AllocNode( &(UI_WIDGET_OPS) { .measure = M_Measure, .layout = UI_LayoutWrapper, .draw = M_Draw, }, 0); UI_AddChild(node); UI_PushCurrent(node); UI_BeginAnchor(x, y); } void UI_EndModal(void) { UI_EndAnchor(); UI_PopCurrent(); } ================================================ FILE: src/trx/game/ui/elements/modal.h ================================================ #pragma once #include // A widget that resizes the children to the canvas size // and places it at a specific proportional spot. void UI_BeginModal(float x, float y); void UI_EndModal(void); ================================================ FILE: src/trx/game/ui/elements/offset.c ================================================ #include #include void UI_BeginOffset(const float x, const float y) { UI_BeginPadEx(x, -x, y, -y); } void UI_EndOffset(void) { UI_EndPad(); } ================================================ FILE: src/trx/game/ui/elements/offset.h ================================================ #pragma once #include // A transformer to move the child element in a certain direction. // Does not affect the occupied space. void UI_BeginOffset(float x, float y); void UI_EndOffset(void); ================================================ FILE: src/trx/game/ui/elements/pad.c ================================================ #include #include #include typedef struct { float t; float r; float d; float l; } M_DATA; static void M_Measure(UI_NODE *const node) { UI_MeasureWrapper(node); const M_DATA *const data = node->data; node->measure_w += data->l + data->r; node->measure_h += data->t + data->d; } static void M_Layout( UI_NODE *const node, const float x, const float y, const float w, const float h) { UI_LayoutBasic(node, x, y, w, h); const M_DATA *const data = node->data; UI_NODE *child = node->first_child; while (child != nullptr) { child->ops.layout( child, x + data->l, y + data->t, w - data->l - data->r, h - data->t - data->d); child = child->next_sibling; } } void UI_BeginPad(const float x, const float y) { UI_BeginPadEx(x, x, y, y); } void UI_BeginPadEx(const float l, const float r, const float t, const float d) { UI_NODE *const node = UI_AllocNode( &(UI_WIDGET_OPS) { .measure = M_Measure, .layout = M_Layout, .draw = UI_DrawWrapper, }, sizeof(M_DATA)); M_DATA *const data = node->data; data->t = t * g_Config.ui.text_scale; data->r = r * g_Config.ui.text_scale; data->d = d * g_Config.ui.text_scale; data->l = l * g_Config.ui.text_scale; UI_AddChild(node); UI_PushCurrent(node); } void UI_EndPad(void) { UI_PopCurrent(); } ================================================ FILE: src/trx/game/ui/elements/pad.h ================================================ #pragma once #include // An invisible border in pixel units around the child widget. void UI_BeginPad(float x, float y); void UI_BeginPadEx(float l, float r, float t, float d); void UI_EndPad(void); ================================================ FILE: src/trx/game/ui/elements/progress_button.c ================================================ #include #include #include #include #include #include #include #include #include #define M_HOLD_TIMER_DEBUFF (LOGIC_FPS / 3) #define M_HOLD_TIMER_MAX LOGIC_FPS struct UI_PROGRESS_BUTTON_STATE { GAME_STRING_ID text; INPUT_BACKEND backend; INPUT_ROLE role; UI_PROGRESS_BUTTON_CALLBACK func; void *func_arg; int32_t hold_timer; }; UI_PROGRESS_BUTTON_STATE *UI_ProgressButton_Init( INPUT_BACKEND backend, INPUT_ROLE role, GAME_STRING_ID text, UI_PROGRESS_BUTTON_CALLBACK func, void *func_arg) { UI_PROGRESS_BUTTON_STATE *const s = Memory_Alloc(sizeof(UI_PROGRESS_BUTTON_STATE)); s->backend = backend; s->role = role; s->text = text; s->func = func; s->func_arg = func_arg; s->hold_timer = 0; return s; } void UI_ProgressButton_Control(UI_PROGRESS_BUTTON_STATE *const s) { if (!Input_IsPressedEx(s->backend, INPUT_LAYOUT_DEFAULT, s->role)) { s->hold_timer = 0; return; } if (s->hold_timer != -1) { s->hold_timer++; if (s->hold_timer - M_HOLD_TIMER_DEBUFF > M_HOLD_TIMER_MAX) { s->func(s->func_arg); s->hold_timer = -1; // Debounce the key } } } void UI_ProgressButton_Free(UI_PROGRESS_BUTTON_STATE *s) { Memory_Free(s); } void UI_ProgressButton(UI_PROGRESS_BUTTON_STATE *const s) { const float scale = 0.85f; const char *const key_name = Input_GetKeyName(s->backend, INPUT_LAYOUT_DEFAULT, s->role, 0); if (key_name == nullptr) { return; } const char *const value_label = String_FormatStatic(GS("general/misc/hold_fmt"), key_name); const char *const text = String_FormatStatic("%s: %s", GameString_Get(s->text), value_label); const float pad[2] = { 6.0f, 3.0f }; const float spacing = 2.0f; const float progress = (s->hold_timer - M_HOLD_TIMER_DEBUFF) / (float)M_HOLD_TIMER_MAX; UI_BeginPad(pad[0], pad[1]); UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, .align = { .h = UI_STACK_H_ALIGN_SPAN, .v = UI_STACK_V_ALIGN_TOP, }, .spacing = { .h = 0.0f, .v = spacing, }, }); UI_LabelEx(text, (UI_LABEL_SETTINGS) { .scale = scale }); UI_BeginHide(progress < 0.0f); UI_SleekBar(progress); UI_EndHide(); UI_EndStack(); UI_EndPad(); } ================================================ FILE: src/trx/game/ui/elements/progress_button.h ================================================ #pragma once #include #include #include typedef void (*UI_PROGRESS_BUTTON_CALLBACK)(void *); typedef struct UI_PROGRESS_BUTTON_STATE UI_PROGRESS_BUTTON_STATE; UI_PROGRESS_BUTTON_STATE *UI_ProgressButton_Init( INPUT_BACKEND backend, INPUT_ROLE role, GAME_STRING_ID text, UI_PROGRESS_BUTTON_CALLBACK func, void *func_arg); void UI_ProgressButton_Control(UI_PROGRESS_BUTTON_STATE *s); void UI_ProgressButton_Free(UI_PROGRESS_BUTTON_STATE *s); void UI_ProgressButton(UI_PROGRESS_BUTTON_STATE *s); ================================================ FILE: src/trx/game/ui/elements/prompt.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef struct { UI_PROMPT_STATE *state; } M_DATA; static void M_Layout( UI_NODE *const node, const float x, const float y, const float w, const float h) { UI_LayoutBasic(node, x, y, w, h); const M_DATA *const data = node->data; const UI_PROMPT_STATE *const s = data->state; UI_NODE *const prompt = node->first_child; UI_NODE *const caret = prompt->next_sibling; prompt->ops.layout(prompt, x, y, w, h); const char old = s->current_text[s->caret_pos]; s->current_text[s->caret_pos] = '\0'; float caret_pos; UI_Label_Measure(s->current_text, &caret_pos, nullptr); s->current_text[s->caret_pos] = old; caret->ops.layout(caret, x + caret_pos, y, w, h); } static int32_t M_GetPrevCaretPos( const char *const text, const int32_t caret_pos) { if (caret_pos <= 0) { return 0; } const char *const caret_ptr = text + caret_pos; const char *p = text; const char *prev = text; while (p < caret_ptr) { prev = p; p += String_GetCharByteSize(p); } return (int32_t)(prev - text); } static int32_t M_GetNextCaretPos( const char *const text, const int32_t caret_pos) { const size_t text_len = strlen(text); if ((size_t)caret_pos >= text_len) { return (int32_t)text_len; } int32_t next_pos = caret_pos + (int32_t)String_GetCharByteSize(text + caret_pos); if ((size_t)next_pos > text_len) { next_pos = (int32_t)text_len; } return next_pos; } static void M_MoveCaretLeft(UI_PROMPT_STATE *const s) { s->caret_pos = M_GetPrevCaretPos(s->current_text, s->caret_pos); } static void M_MoveCaretRight(UI_PROMPT_STATE *const s) { s->caret_pos = M_GetNextCaretPos(s->current_text, s->caret_pos); } static void M_MoveCaretStart(UI_PROMPT_STATE *const s) { s->caret_pos = 0; } static void M_MoveCaretEnd(UI_PROMPT_STATE *const s) { s->caret_pos = strlen(s->current_text); } static void M_DeleteCharBack(UI_PROMPT_STATE *const s) { if (s->caret_pos <= 0) { return; } const int32_t delete_start = M_GetPrevCaretPos(s->current_text, s->caret_pos); if (delete_start >= s->caret_pos || delete_start < 0) { return; } memmove( s->current_text + delete_start, s->current_text + s->caret_pos, strlen(s->current_text) + 1 - (size_t)s->caret_pos); s->caret_pos = delete_start; } static void M_Clear(UI_PROMPT_STATE *const s) { strcpy(s->current_text, ""); s->caret_pos = 0; } static void M_Cancel(UI_PROMPT_STATE *const s) { UI_FireEvent((EVENT) { .name = "cancel", .sender = s, .data = s->current_text, }); M_Clear(s); } static void M_Confirm(UI_PROMPT_STATE *const s) { if (String_IsEmpty(s->current_text)) { M_Cancel(s); return; } UI_FireEvent((EVENT) { .name = "confirm", .sender = s, .data = s->current_text, }); M_Clear(s); } static void M_HandleKeyDown(const EVENT *const event, void *const user_data) { const UI_INPUT key = (UI_INPUT)(uintptr_t)event->data; UI_PROMPT_STATE *const s = user_data; if (!s->is_focused) { return; } // clang-format off switch (key) { case UI_KEY_LEFT: M_MoveCaretLeft(s); break; case UI_KEY_RIGHT: M_MoveCaretRight(s); break; case UI_KEY_HOME: M_MoveCaretStart(s); break; case UI_KEY_END: M_MoveCaretEnd(s); break; case UI_KEY_BACK: M_DeleteCharBack(s); break; case UI_KEY_RETURN: M_Confirm(s); break; case UI_KEY_ESCAPE: M_Cancel(s); break; default: break; } // clang-format on } static void M_HandleTextEdit(const EVENT *const event, void *const user_data) { UI_PROMPT_STATE *const s = user_data; if (!s->is_focused) { return; } char *filtered = UI_Text_FilterGlyphs(event->data); if (filtered == nullptr || filtered[0] == '\0') { Memory_FreePointer(&filtered); return; } const char *insert_string = filtered; const size_t insert_length = strlen(insert_string); const size_t available_space = s->current_text_capacity - strlen(s->current_text); if (insert_length >= available_space) { s->current_text_capacity *= 2; s->current_text = Memory_Realloc(s->current_text, s->current_text_capacity); } memmove( s->current_text + s->caret_pos + insert_length, s->current_text + s->caret_pos, strlen(s->current_text) + 1 - s->caret_pos); memcpy(s->current_text + s->caret_pos, insert_string, insert_length); s->caret_pos += insert_length; Memory_FreePointer(&filtered); } void UI_Prompt_Init(UI_PROMPT_STATE *const s) { s->is_focused = false; s->current_text_capacity = 30; s->current_text = Memory_Alloc(s->current_text_capacity); s->listener1 = UI_Subscribe("key_down", nullptr, M_HandleKeyDown, s); s->listener2 = UI_Subscribe("text_edit", nullptr, M_HandleTextEdit, s); UI_Flash_Init(&s->flash, LOGIC_FPS * 2 / 3); } void UI_Prompt_Free(UI_PROMPT_STATE *const s) { UI_Unsubscribe(s->listener1); UI_Unsubscribe(s->listener2); UI_Flash_Free(&s->flash); Memory_FreePointer(&s->current_text); } void UI_Prompt_Control(UI_PROMPT_STATE *const s) { UI_Flash_Control(&s->flash); } void UI_Prompt(UI_PROMPT_STATE *const s) { UI_NODE *const node = UI_AllocNode( &(UI_WIDGET_OPS) { .measure = UI_MeasureWrapper, .layout = M_Layout, .draw = UI_DrawWrapper, }, sizeof(M_DATA)); M_DATA *const data = node->data; data->state = s; UI_AddChild(node); UI_PushCurrent(node); UI_LabelEx( s->current_text != nullptr ? s->current_text : "", (UI_LABEL_SETTINGS) { .scale = 1.0f, .z = 16 }); if (s->is_focused) { UI_BeginFlash(&s->flash); } UI_LabelEx( "\\{button left}", (UI_LABEL_SETTINGS) { .scale = 1.0f, .z = 8 }); if (s->is_focused) { UI_EndFlash(); } UI_PopCurrent(); } void UI_Prompt_SetFocus(UI_PROMPT_STATE *const s, const bool is_focused) { if (s->is_focused == is_focused) { return; } s->is_focused = is_focused; s->flash.count = 0; if (is_focused) { Input_EnterListenMode(); } else { Input_ExitListenMode(); } } void UI_Prompt_Clear(UI_PROMPT_STATE *const s) { M_Clear(s); } void UI_Prompt_ChangeText(UI_PROMPT_STATE *const s, const char *const new_text) { Memory_FreePointer(&s->current_text); s->current_text = Memory_DupStr(new_text); s->current_text_capacity = strlen(new_text) + 1; s->caret_pos = strlen(new_text); } ================================================ FILE: src/trx/game/ui/elements/prompt.h ================================================ #pragma once #include #include #include // A text edit widget that collects text input from the player. // Needs to be in focus to work, otherwise is inactive. typedef struct { bool is_focused; int32_t caret_pos; int32_t current_text_capacity; char *current_text; int32_t listener1; int32_t listener2; UI_FLASH_STATE flash; } UI_PROMPT_STATE; // state functions void UI_Prompt_Init(UI_PROMPT_STATE *s); void UI_Prompt_Free(UI_PROMPT_STATE *s); void UI_Prompt_Control(UI_PROMPT_STATE *s); void UI_Prompt_Clear(UI_PROMPT_STATE *s); void UI_Prompt_SetFocus(UI_PROMPT_STATE *s, bool is_focused); void UI_Prompt_ChangeText(UI_PROMPT_STATE *s, const char *new_text); // draw functions void UI_Prompt(UI_PROMPT_STATE *s); ================================================ FILE: src/trx/game/ui/elements/requester.c ================================================ #include #include #include #include #include #include void UI_Requester_Init( UI_REQUESTER_STATE *const s, const int32_t vis_rows, const int32_t max_rows, const bool is_selectable) { s->scroll = (UI_SCROLLABLE) { .first_item = 0, .sel_item = 0, .vis_items = vis_rows, .max_items = max_rows, }; s->is_selectable = is_selectable; s->row_pad = 20.0f; s->row_spacing = 3.0f; s->show_arrows = false; s->reserve_space = false; } void UI_Requester_Free(UI_REQUESTER_STATE *const s) { } int32_t UI_Requester_Control(UI_REQUESTER_STATE *const s) { if (s->is_selectable) { if (g_InputDB.menu_down) { UI_Scrollable_SelectNext(&s->scroll, g_Config.ui.enable_wraparound); } else if (g_InputDB.menu_up) { UI_Scrollable_SelectPrev(&s->scroll, g_Config.ui.enable_wraparound); } } else { if (g_InputDB.menu_down) { UI_Scrollable_ScrollDown(&s->scroll, g_Config.ui.enable_wraparound); } else if (g_InputDB.menu_up) { UI_Scrollable_ScrollUp(&s->scroll, g_Config.ui.enable_wraparound); } } if (s->is_selectable) { if (g_InputDB.menu_back) { return UI_REQUESTER_CANCEL; } if (g_InputDB.menu_confirm) { return s->scroll.sel_item; } } return UI_REQUESTER_NO_CHOICE; } void UI_Requester_SetMaxRows(UI_REQUESTER_STATE *const s, const size_t max_rows) { UI_Scrollable_SetMaxItems(&s->scroll, max_rows); } void UI_Requester_SetVisibleRows( UI_REQUESTER_STATE *const s, const size_t visible_rows) { UI_Scrollable_SetVisibleItems(&s->scroll, visible_rows); } int32_t UI_Requester_GetFirstRow(const UI_REQUESTER_STATE *const s) { return UI_Scrollable_GetFirstVisibleItem(&s->scroll); } int32_t UI_Requester_GetLastRow(const UI_REQUESTER_STATE *const s) { return UI_Scrollable_GetLastVisibleItem(&s->scroll) + 1; } int32_t UI_Requester_GetCurrentRow(const UI_REQUESTER_STATE *s) { return UI_Scrollable_GetSelectedItem(&s->scroll); } bool UI_Requester_IsRowVisible( const UI_REQUESTER_STATE *const s, const int32_t i) { return UI_Scrollable_IsItemVisible(&s->scroll, i); } bool UI_Requester_IsRowSelected( const UI_REQUESTER_STATE *const s, const int32_t i) { return UI_Scrollable_IsItemSelected(&s->scroll, i); } void UI_Requester_SelectRow(UI_REQUESTER_STATE *const s, const int32_t i) { UI_Scrollable_SelectItem(&s->scroll, i); } void UI_BeginRequester( const UI_REQUESTER_STATE *const s, const char *const title) { const bool show_scroll_hints = s->show_arrows && s->scroll.vis_items < s->scroll.max_items; UI_BeginWindow((UI_WINDOW_SETTINGS) { .title = title, .scrollable = show_scroll_hints ? &s->scroll : nullptr, .title_spacing = -1.0f, }); if (s->reserve_space) { UI_BeginResize( -1.0f, s->scroll.vis_items * UI_TEXT_HEIGHT + (s->scroll.vis_items - 1) * s->row_spacing); } UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, .align = { .h = UI_STACK_H_ALIGN_SPAN }, .spacing = { .v = s->row_spacing }, }); } void UI_EndRequester(const UI_REQUESTER_STATE *const s) { UI_EndStack(); if (s->reserve_space) { UI_EndResize(); } UI_EndWindow(); } void UI_BeginRequesterRow(const UI_REQUESTER_STATE *const s, const int32_t i) { UI_BeginPad(0.0f, g_TRVersion == 1 ? -1.0f : 0.0f); if (UI_Requester_IsRowSelected(s, i)) { UI_BeginFrame(UI_FRAME_SELECTED_OPTION); } UI_BeginPad(s->row_pad, g_TRVersion == 1 ? 1.0f : 0.0f); } void UI_EndRequesterRow(const UI_REQUESTER_STATE *const s, const int32_t i) { UI_EndPad(); if (UI_Requester_IsRowSelected(s, i)) { UI_EndFrame(); } UI_EndPad(); } ================================================ FILE: src/trx/game/ui/elements/requester.h ================================================ #pragma once // A window to select a single option from a list of predefined choices. #include #include #include #define UI_REQUESTER_CANCEL -2 #define UI_REQUESTER_NO_CHOICE -1 typedef struct { bool is_selectable; UI_SCROLLABLE scroll; float row_pad; float row_spacing; bool show_arrows; bool reserve_space; } UI_REQUESTER_STATE; // state functions void UI_Requester_Init( UI_REQUESTER_STATE *s, int32_t vis_rows, int32_t max_rows, bool is_selectable); void UI_Requester_Free(UI_REQUESTER_STATE *s); int32_t UI_Requester_Control(UI_REQUESTER_STATE *s); void UI_Requester_SetMaxRows(UI_REQUESTER_STATE *s, size_t max_rows); void UI_Requester_SetVisibleRows(UI_REQUESTER_STATE *s, size_t visible_rows); void UI_Requester_SelectRow(UI_REQUESTER_STATE *s, int32_t i); int32_t UI_Requester_GetFirstRow(const UI_REQUESTER_STATE *s); int32_t UI_Requester_GetLastRow(const UI_REQUESTER_STATE *s); int32_t UI_Requester_GetCurrentRow(const UI_REQUESTER_STATE *s); bool UI_Requester_IsRowVisible(const UI_REQUESTER_STATE *s, int32_t i); bool UI_Requester_IsRowSelected(const UI_REQUESTER_STATE *s, int32_t i); // draw functions void UI_BeginRequester(const UI_REQUESTER_STATE *s, const char *title); void UI_EndRequester(const UI_REQUESTER_STATE *s); void UI_BeginRequesterRow(const UI_REQUESTER_STATE *s, int32_t i); void UI_EndRequesterRow(const UI_REQUESTER_STATE *s, int32_t i); ================================================ FILE: src/trx/game/ui/elements/resize.c ================================================ #include #include #include typedef struct { float x; float y; } M_DATA; static void M_Measure(UI_NODE *const node) { UI_MeasureWrapper(node); const M_DATA *const data = node->data; if (data->x >= 0.0f) { node->measure_w = data->x; } if (data->y >= 0.0f) { node->measure_h = data->y; } } static void M_Draw(const UI_NODE *const node) { if (node->measure_w <= 0.0f || node->measure_h <= 0.0f) { return; } UI_DrawWrapper(node); } void UI_BeginResize(const float x, const float y) { UI_NODE *const node = UI_AllocNode( &(UI_WIDGET_OPS) { .measure = M_Measure, .layout = UI_LayoutWrapper, .draw = M_Draw, }, sizeof(M_DATA)); M_DATA *const data = node->data; data->x = x * g_Config.ui.text_scale; data->y = y * g_Config.ui.text_scale; UI_AddChild(node); UI_PushCurrent(node); } void UI_EndResize(void) { UI_PopCurrent(); } ================================================ FILE: src/trx/game/ui/elements/resize.h ================================================ #pragma once #include // Resize child widget to a specified size in pixels. // A negative size means to use the child's size. // A zero value means to hide the child, but participate in the layout pass. void UI_BeginResize(float x, float y); void UI_EndResize(void); ================================================ FILE: src/trx/game/ui/elements/row_arrows.c ================================================ #include #include #include #include #include void UI_BeginRowArrows( const bool left_arrow, const bool right_arrow, const int32_t spacing) { UI_NODE *const node = UI_AllocNode( &(UI_WIDGET_OPS) { .measure = UI_MeasureWrapper, .layout = UI_LayoutWrapper, .draw = UI_DrawWrapper, }, sizeof(bool)); *(bool *)node->data = right_arrow; UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_HORIZONTAL, .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE, .v = UI_STACK_V_ALIGN_CENTER, }, .spacing = { .h = spacing }, }); UI_BeginHide(!left_arrow); UI_Label("\\{button left}"); UI_EndHide(); UI_AddChild(node); UI_PushCurrent(node); } void UI_EndRowArrows(void) { const UI_NODE *const node = UI_GetCurrent(); const bool right_arrow = *(bool *)(intptr_t)node->data; UI_PopCurrent(); UI_BeginHide(!right_arrow); UI_Label("\\{button right}"); UI_EndHide(); UI_EndStack(); } ================================================ FILE: src/trx/game/ui/elements/row_arrows.h ================================================ #pragma once #include #define UI_ROW_ARROWS_TIGHT 2.0f #define UI_ROW_ARROWS_MEDIUM 5.0f #define UI_ROW_ARROWS_WIDE 15.0f // Conditionally display a left arrow and a right arrow around the child widget // with the given spacing. void UI_BeginRowArrows(bool left_arrow, bool right_arrow, int32_t spacing); void UI_EndRowArrows(void); ================================================ FILE: src/trx/game/ui/elements/scrollable_stack.c ================================================ #include #include #include #include #include typedef struct { UI_SCROLLABLE *scroll; UI_SCROLLABLE_STACK_SETTINGS settings; } M_DATA; static int32_t M_CountChildren(const UI_NODE *const node) { int32_t count = 0; const UI_NODE *child = node->first_child; while (child != nullptr) { count++; child = child->next_sibling; } return count; } static void M_ClampScroll(UI_SCROLLABLE *const s) { CLAMPG(s->first_item, s->max_items - s->vis_items); CLAMPL(s->first_item, 0); } static void M_Measure(UI_NODE *const node) { M_DATA *const data = node->data; UI_SCROLLABLE *const s = data->scroll; s->max_items = M_CountChildren(node); CLAMPL(s->vis_items, 0); M_ClampScroll(s); node->measure_w = 0.0f; node->measure_h = 0.0f; const int32_t first = s->first_item; const int32_t last = MIN(first + s->vis_items, s->max_items); const float scale = g_Config.ui.text_scale; int32_t visible_count = 0; int32_t i = 0; const UI_NODE *child = node->first_child; while (child != nullptr) { if (i >= first && i < last) { if (data->settings.orientation == UI_STACK_VERTICAL) { node->measure_w = MAX(node->measure_w, child->measure_w); node->measure_h += child->measure_h; } else { node->measure_h = MAX(node->measure_h, child->measure_h); node->measure_w += child->measure_w; } visible_count++; } i++; child = child->next_sibling; } const int32_t gaps = (visible_count > 1) ? (visible_count - 1) : 0; if (data->settings.orientation == UI_STACK_VERTICAL) { node->measure_h += (float)gaps * data->settings.spacing * scale; } else { node->measure_w += (float)gaps * data->settings.spacing * scale; } } static void M_Layout( UI_NODE *const node, const float x, const float y, const float w, const float h) { UI_LayoutBasic(node, x, y, w, h); M_DATA *const data = node->data; const UI_SCROLLABLE *const s = data->scroll; const int32_t first = s->first_item; const int32_t last = MIN(first + s->vis_items, s->max_items); const float scale = g_Config.ui.text_scale; float cx = x; float cy = y; int32_t i = 0; UI_NODE *child = node->first_child; while (child != nullptr) { if (i >= first && i < last) { if (data->settings.orientation == UI_STACK_VERTICAL) { child->ops.layout(child, x, cy, w, child->measure_h); cy += child->measure_h; if (i + 1 < last) { cy += data->settings.spacing * scale; } } else { child->ops.layout(child, cx, y, child->measure_w, h); cx += child->measure_w; if (i + 1 < last) { cx += data->settings.spacing * scale; } } } i++; child = child->next_sibling; } } static void M_Draw(const UI_NODE *const node) { const M_DATA *const data = node->data; const UI_SCROLLABLE *const s = data->scroll; const int32_t first = s->first_item; const int32_t last = MIN(first + s->vis_items, s->max_items); int32_t i = 0; const UI_NODE *child = node->first_child; while (child != nullptr) { if (i >= first && i < last && child->ops.draw != nullptr) { child->ops.draw(child); } i++; child = child->next_sibling; } } bool UI_ScrollableStack_Control( UI_SCROLLABLE *const s, const UI_STACK_ORIENTATION orientation) { if (orientation == UI_STACK_VERTICAL) { if (g_InputDB.menu_down) { return UI_Scrollable_ScrollDown(s, g_Config.ui.enable_wraparound); } else if (g_InputDB.menu_up) { return UI_Scrollable_ScrollUp(s, g_Config.ui.enable_wraparound); } } else { if (g_InputDB.menu_right || g_InputDB.menu_tab_right) { return UI_Scrollable_ScrollDown(s, g_Config.ui.enable_wraparound); } else if (g_InputDB.menu_left || g_InputDB.menu_tab_left) { return UI_Scrollable_ScrollUp(s, g_Config.ui.enable_wraparound); } } return false; } void UI_BeginScrollableStack( UI_SCROLLABLE *const s, const UI_SCROLLABLE_STACK_SETTINGS settings) { UI_NODE *const node = UI_AllocNode( &(UI_WIDGET_OPS) { .measure = M_Measure, .layout = M_Layout, .draw = M_Draw, }, sizeof(M_DATA)); if (node == nullptr) { return; } M_DATA *const data = node->data; data->scroll = s; data->settings = settings; UI_AddChild(node); UI_PushCurrent(node); } void UI_EndScrollableStack(void) { UI_PopCurrent(); } ================================================ FILE: src/trx/game/ui/elements/scrollable_stack.h ================================================ #pragma once #include #include #include typedef struct { UI_STACK_ORIENTATION orientation; float spacing; } UI_SCROLLABLE_STACK_SETTINGS; bool UI_ScrollableStack_Control( UI_SCROLLABLE *s, UI_STACK_ORIENTATION orientation); void UI_BeginScrollableStack( UI_SCROLLABLE *s, UI_SCROLLABLE_STACK_SETTINGS settings); void UI_EndScrollableStack(void); ================================================ FILE: src/trx/game/ui/elements/sleek_bar.c ================================================ #include #include #include #include #include #include typedef struct { float progress; } M_DATA; typedef struct { float x, y, w, h; RGBA_8888 color; } M_RECT; static RGBA_8888 m_BackgroundColor = { 0x06, 0x06, 0x06, 0xFF }; static RGBA_8888 m_FillColors[TR_VERSION_COUNT] = { { 0xA1, 0x83, 0x3C, 0xFF }, { 0x5A, 0xB5, 0x5A, 0xFF }, { 0x1C, 0x6A, 0xC4, 0xFF }, }; static void M_Measure(UI_NODE *const node) { node->measure_w = 0.0f; node->measure_h = 4.0f * g_Config.ui.text_scale; } static void M_Draw(const UI_NODE *const node) { const M_DATA *const data = node->data; const float border = 1.0f; const M_RECT out = { .x = UI_ScaleX(node->x), .y = UI_ScaleY(node->y), .w = UI_ScaleX(node->w), .h = UI_ScaleY(node->h), .color = m_BackgroundColor, }; const M_RECT in = { .x = UI_ScaleX(node->x + border), .y = UI_ScaleY(node->y + border), .w = UI_ScaleX(node->w - border * 2.0f) * data->progress, .h = UI_ScaleY(node->h - border * 2.0f), .color = m_FillColors[g_TRVersion - 1], }; UI_ScheduleDrawScreenFlatQuad(out.x, out.y, 0, out.w, out.h, out.color); UI_ScheduleDrawScreenFlatQuad(in.x, in.y, 0, in.w, in.h, in.color); } void UI_SleekBar(float progress) { UI_NODE *const node = UI_AllocNode( &(UI_WIDGET_OPS) { .measure = M_Measure, .layout = UI_LayoutBasic, .draw = M_Draw, }, sizeof(M_DATA)); M_DATA *const data = node->data; CLAMP(progress, 0.0f, 1.0f); data->progress = progress; UI_AddChild(node); } ================================================ FILE: src/trx/game/ui/elements/sleek_bar.h ================================================ #pragma once #include void UI_SleekBar(float progress); ================================================ FILE: src/trx/game/ui/elements/spacer.c ================================================ #include #include #include static void M_Measure(UI_NODE *const node) { // already done in the constructor } void UI_Spacer(const float w, const float h) { UI_NODE *const node = UI_AllocNode( &(UI_WIDGET_OPS) { .measure = M_Measure, .layout = UI_LayoutBasic, .draw = UI_DrawWrapper, }, 0); node->measure_w = w * g_Config.ui.text_scale; node->measure_h = h * g_Config.ui.text_scale; UI_AddChild(node); } ================================================ FILE: src/trx/game/ui/elements/spacer.h ================================================ #pragma once #include // An invisible widget that occupies certain space in pixels. void UI_Spacer(float w, float h); ================================================ FILE: src/trx/game/ui/elements/span.c ================================================ #include #include #include static void M_Measure(UI_NODE *const node) { node->measure_w = 0.0f; node->measure_h = 0.0f; const UI_NODE *child = node->first_child; while (child != nullptr) { node->measure_w = MAX(node->measure_w, child->measure_w); node->measure_h = MAX(node->measure_h, child->measure_h); child = child->next_sibling; } } static void M_Layout( UI_NODE *const node, const float x, const float y, const float w, const float h) { UI_LayoutBasic(node, x, y, w, h); UI_NODE *child = node->first_child; while (child != nullptr) { child->ops.layout(child, x, y, node->measure_w, node->measure_h); child = child->next_sibling; } } void UI_BeginSpan(void) { UI_NODE *const node = UI_AllocNode( &(UI_WIDGET_OPS) { .measure = UI_MeasureWrapper, .layout = M_Layout, .draw = UI_DrawWrapper, }, 0); UI_AddChild(node); UI_PushCurrent(node); } void UI_EndSpan(void) { UI_PopCurrent(); } ================================================ FILE: src/trx/game/ui/elements/span.h ================================================ #pragma once #include // Expands all children to match the biggest child size. // Renders the children one on top of each other. void UI_BeginSpan(void); void UI_EndSpan(void); ================================================ FILE: src/trx/game/ui/elements/stack.c ================================================ #include #include #include #include #include #include #include typedef struct { UI_STACK_SETTINGS settings; } M_DATA; static float M_CalcChildW(const UI_NODE *const node, const UI_NODE *const child) { M_DATA *const data = node->data; if (data->settings.align.h == UI_STACK_H_ALIGN_SPAN) { return MAX(child->measure_w, node->w); } return child->measure_w; } static float M_CalcChildH(const UI_NODE *const node, const UI_NODE *const child) { M_DATA *const data = node->data; if (data->settings.align.v == UI_STACK_V_ALIGN_SPAN) { return MAX(child->measure_h, node->h); } return child->measure_h; } static float M_CalcStartX(const UI_NODE *const node, const UI_NODE *const child) { M_DATA *const data = node->data; switch (data->settings.align.h) { case UI_STACK_H_ALIGN_SPAN: case UI_STACK_H_ALIGN_LEFT: return node->x; case UI_STACK_H_ALIGN_CENTER: return node->x + (node->w - child->measure_w) * 0.5f; case UI_STACK_H_ALIGN_RIGHT: return node->x + node->w - child->measure_w; case UI_STACK_H_ALIGN_DISTRIBUTE: ASSERT_FAIL(); } return 0.0f; } static float M_CalcStartY(const UI_NODE *const node, const UI_NODE *const child) { M_DATA *const data = node->data; switch (data->settings.align.v) { case UI_STACK_V_ALIGN_SPAN: case UI_STACK_V_ALIGN_TOP: return node->y; case UI_STACK_V_ALIGN_CENTER: return node->y + (node->h - child->measure_h) * 0.5f; case UI_STACK_V_ALIGN_BOTTOM: return node->y + node->h - child->measure_h; case UI_STACK_V_ALIGN_DISTRIBUTE: ASSERT_FAIL(); } return 0.0f; } static void M_Measure(UI_NODE *const node) { node->measure_w = 0.0f; node->measure_h = 0.0f; UI_NODE *child = node->first_child; M_DATA *const data = node->data; const float scale = g_Config.ui.text_scale; while (child != nullptr) { if (data->settings.orientation == UI_STACK_VERTICAL) { node->measure_w = MAX(node->measure_w, child->measure_w); node->measure_h += child->measure_h; if (child->next_sibling != nullptr) { node->measure_h += data->settings.spacing.v * scale; } } else { node->measure_h = MAX(node->measure_h, child->measure_h); node->measure_w += child->measure_w; if (child->next_sibling != nullptr) { node->measure_w += data->settings.spacing.h * scale; } } child = child->next_sibling; } } static void M_Layout( UI_NODE *const node, const float x, const float y, const float w, const float h) { UI_LayoutBasic(node, x, y, w, h); M_DATA *const data = node->data; // Count children and compute the total size they occupy on the main axis // including the base spacing from the settings. int32_t child_count = 0; float total_child_main_size = 0.0f; UI_NODE *child = node->first_child; while (child != nullptr) { switch (data->settings.orientation) { case UI_STACK_HORIZONTAL: total_child_main_size += child->measure_w; break; case UI_STACK_VERTICAL: total_child_main_size += child->measure_h; break; } child_count++; child = child->next_sibling; } // If there is at least one gap between children, compute the normal // (configured) total spacing on the main axis. If only 1 child or 0 // children, there's no gap to distribute leftover space into. const int32_t gaps = (child_count > 1) ? (child_count - 1) : 0; const float scale = g_Config.ui.text_scale; float base_spacing = 0.0f; switch (data->settings.orientation) { case UI_STACK_HORIZONTAL: base_spacing = data->settings.spacing.h * gaps * scale; break; case UI_STACK_VERTICAL: base_spacing = data->settings.spacing.v * gaps * scale; break; } // The space that the children + base spacing absolutely need const float needed_size = total_child_main_size + base_spacing; // The leftover that we can distribute among the (child_count - 1) internal // gaps. float leftover = 0.0f; float extra_per_gap = 0.0f; switch (data->settings.orientation) { case UI_STACK_HORIZONTAL: leftover = w - needed_size; break; case UI_STACK_VERTICAL: leftover = h - needed_size; break; } if ((data->settings.orientation == UI_STACK_HORIZONTAL && data->settings.align.h == UI_STACK_H_ALIGN_DISTRIBUTE) || (data->settings.orientation == UI_STACK_VERTICAL && data->settings.align.v == UI_STACK_V_ALIGN_DISTRIBUTE)) { if (gaps > 0 && leftover > 0.0f) { extra_per_gap = leftover / (float)gaps; } } // Now we actually lay out the children float cx = x; float cy = y; child = node->first_child; while (child != nullptr) { const float cw = M_CalcChildW(node, child); const float ch = M_CalcChildH(node, child); switch (data->settings.orientation) { case UI_STACK_HORIZONTAL: // For horizontal: vertical alignment is determined by M_CalcStartY cy = M_CalcStartY(node, child); // Lay out the child child->ops.layout(child, cx, cy, cw, ch); // Advance cx for the next child cx += cw; // Add normal spacing + any extra leftover that we are distributing if (child->next_sibling != nullptr) { cx += data->settings.spacing.h * scale + extra_per_gap; } break; case UI_STACK_VERTICAL: cx = M_CalcStartX(node, child); child->ops.layout(child, cx, cy, cw, ch); cy += ch; if (child->next_sibling != nullptr) { cy += data->settings.spacing.v * scale + extra_per_gap; } break; } child = child->next_sibling; } } UI_NODE *UI_CreateStack(const UI_STACK_SETTINGS settings) { UI_NODE *const node = UI_AllocNode( &(UI_WIDGET_OPS) { .measure = M_Measure, .layout = M_Layout, .draw = UI_DrawWrapper, }, sizeof(M_DATA)); if (node == nullptr) { return nullptr; } M_DATA *const data = node->data; data->settings = settings; return node; } void UI_BeginStack(const UI_STACK_ORIENTATION orientation) { UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = orientation, .align = { .h = UI_STACK_H_ALIGN_LEFT, .v = UI_STACK_V_ALIGN_TOP, }, .spacing = { .h = 0.0f, .v = 0.0f, }, }); } void UI_BeginStackEx(const UI_STACK_SETTINGS settings) { UI_NODE *const child = UI_CreateStack(settings); UI_AddChild(child); UI_PushCurrent(child); } void UI_EndStack(void) { UI_PopCurrent(); } ================================================ FILE: src/trx/game/ui/elements/stack.h ================================================ #pragma once #include #include // Stack several widgets vertically or horizontally. typedef enum { UI_STACK_VERTICAL, UI_STACK_HORIZONTAL, } UI_STACK_ORIENTATION; typedef enum { UI_STACK_H_ALIGN_LEFT, UI_STACK_H_ALIGN_CENTER, UI_STACK_H_ALIGN_RIGHT, UI_STACK_H_ALIGN_SPAN, UI_STACK_H_ALIGN_DISTRIBUTE, } UI_STACK_H_ALIGN; typedef enum { UI_STACK_V_ALIGN_TOP, UI_STACK_V_ALIGN_CENTER, UI_STACK_V_ALIGN_BOTTOM, UI_STACK_V_ALIGN_SPAN, UI_STACK_V_ALIGN_DISTRIBUTE, } UI_STACK_V_ALIGN; typedef struct { UI_STACK_ORIENTATION orientation; struct { UI_STACK_H_ALIGN h; UI_STACK_V_ALIGN v; } align; struct { float h; float v; } spacing; } UI_STACK_SETTINGS; void UI_BeginStack(UI_STACK_ORIENTATION orientation); void UI_BeginStackEx(UI_STACK_SETTINGS settings); void UI_EndStack(void); ================================================ FILE: src/trx/game/ui/elements/tab_switch.c ================================================ #include #include #include #include #include #include #include #include #include #include static void M_Draw( const UI_TAB_SWITCH_STATE *const s, const bool is_focused, const bool single) { UI_BeginAnchor(0.5f, 0.5f); UI_BeginRowArrows( is_focused && s->tab_count > 0 && (g_Config.ui.enable_wraparound || s->active_tab_idx > 0), is_focused && s->tab_count > 0 && (g_Config.ui.enable_wraparound || s->active_tab_idx + 1 < s->tab_count), UI_ROW_ARROWS_MEDIUM); UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_HORIZONTAL, .align = { .h = UI_STACK_H_ALIGN_CENTER }, .spacing = { .h = 10.0f }, }); for (int32_t i = 0; i < s->tab_count; i++) { if (single && i != s->active_tab_idx) { continue; } const UI_TAB_SWITCH_TAB *const tab = &s->tabs[i]; UI_BeginAnchor(0.5f, 0.5f); if (i == s->active_tab_idx) { UI_BeginFrame( is_focused ? UI_FRAME_SELECTED_OPTION : UI_FRAME_OUTLINE_ONLY); } UI_BeginPad(2.0f, 1.0f); UI_Label( tab->header.live_ptr != nullptr ? *tab->header.live_ptr : tab->header.one_off); UI_EndPad(); if (i == s->active_tab_idx) { UI_EndFrame(); } UI_EndAnchor(); } UI_EndStack(); UI_EndRowArrows(); UI_EndAnchor(); } UI_TAB_SWITCH_STATE *UI_TabSwitch_Init( const int32_t tab_count, const UI_TAB_SWITCH_TAB *const tabs) { UI_TAB_SWITCH_STATE *const s = Memory_Alloc(sizeof(UI_TAB_SWITCH_STATE)); s->tab_count = tab_count; s->tabs = Memory_Dup(tabs, sizeof(UI_TAB_SWITCH_TAB) * tab_count); s->active_tab_idx = 0; return s; } void UI_TabSwitch_Free(UI_TAB_SWITCH_STATE *const s) { Memory_Free(s->tabs); Memory_Free(s); } bool UI_TabSwitch_Cycle(UI_TAB_SWITCH_STATE *const s, const int32_t dir) { if (s->tab_count == 0) { return false; } else if (s->active_tab_idx + dir < 0) { if (g_Config.ui.enable_wraparound) { s->active_tab_idx = s->tab_count - 1; return true; } } else if (s->active_tab_idx + dir >= s->tab_count) { if (g_Config.ui.enable_wraparound) { s->active_tab_idx = 0; return true; } } else { s->active_tab_idx += dir; return true; } return false; } bool UI_TabSwitch_Control( UI_TAB_SWITCH_STATE *const s, const UI_TAB_SWITCH_FLAGS flags) { if ((!(flags & UI_TAB_SWITCH_NO_ARROWS) && g_InputDB.menu_left) || g_InputDB.menu_tab_left) { return UI_TabSwitch_Cycle(s, -1); } else if ( (!(flags & UI_TAB_SWITCH_NO_ARROWS) && g_InputDB.menu_right) || g_InputDB.menu_tab_right) { return UI_TabSwitch_Cycle(s, 1); } return false; } void UI_TabSwitch(const UI_TAB_SWITCH_STATE *const s, const bool is_focused) { M_Draw(s, is_focused, false); } void UI_TabSwitchSingle( const UI_TAB_SWITCH_STATE *const s, const bool is_focused) { M_Draw(s, is_focused, true); } ================================================ FILE: src/trx/game/ui/elements/tab_switch.h ================================================ #pragma once // A tab switch UI element for navigating between multiple tabs via left/right // input. #include #include #include // Represents a single tab page for use with UI_TabSwitch_Control. typedef struct { struct { const char *one_off; const char *const *live_ptr; } header; } UI_TAB_SWITCH_TAB; typedef struct { UI_TAB_SWITCH_TAB *tabs; int32_t tab_count; int32_t active_tab_idx; } UI_TAB_SWITCH_STATE; typedef enum { UI_TAB_SWITCH_NORMAL, UI_TAB_SWITCH_NO_ARROWS, } UI_TAB_SWITCH_FLAGS; // state functions UI_TAB_SWITCH_STATE *UI_TabSwitch_Init( int32_t tab_count, const UI_TAB_SWITCH_TAB *tabs); void UI_TabSwitch_Free(UI_TAB_SWITCH_STATE *s); // Handles left/right input for switching tabs. Returns true if the active tab // changed. bool UI_TabSwitch_Control( UI_TAB_SWITCH_STATE *state, UI_TAB_SWITCH_FLAGS flags); // Advances the active tab by dir (-1 for previous, +1 for next), wrapping // around. bool UI_TabSwitch_Cycle(UI_TAB_SWITCH_STATE *state, int32_t dir); // draw functions void UI_TabSwitch(const UI_TAB_SWITCH_STATE *state, bool is_focused); void UI_TabSwitchSingle(const UI_TAB_SWITCH_STATE *state, bool is_focused); ================================================ FILE: src/trx/game/ui/elements/window.c ================================================ #include #include #include #include #include typedef struct { UI_WINDOW_SETTINGS settings; bool show_scroll_hints; } M_CONTEXT; static M_CONTEXT m_ContextStack[16]; static size_t m_ContextStackSize = 0; static bool M_ShouldShowScrollHints(const UI_WINDOW_SETTINGS *const settings) { if (settings->scrollable == nullptr) { return false; } return settings->reserve_scroll_space || settings->scrollable->vis_items < settings->scrollable->max_items; } static float M_GetOuterPad(void) { return g_TRVersion >= 2 ? 3.0f : 2.0f; } static float M_GetBodyPadX(void) { return g_TRVersion >= 2 ? 4.0f : 8.0f; } static float M_GetBodyPadY(void) { return 4.0f; } static float M_GetTitleSpacing(const UI_WINDOW_SETTINGS *const settings) { if (settings->title_spacing >= 0.0f) { return settings->title_spacing; } return 3.0f; } static void M_ScrollHintRow( const UI_WINDOW_SETTINGS *const settings, const bool show_arrow, const bool up) { UI_BeginResize(-1.0f, 7.0f); UI_BeginAnchor(0.5f, up ? 1.5f : -1.0f); if (show_arrow) { UI_LabelEx( up ? "\\{arrow up}" : "\\{arrow down}", (UI_LABEL_SETTINGS) { .scale = 0.7f }); } UI_EndAnchor(); UI_EndResize(); } static void M_Title(const char *const title) { UI_BeginFrame(UI_FRAME_DIALOG_HEADING); UI_BeginPad(10.0f, g_TRVersion >= 2 ? 1.0f : 2.0f); UI_BeginAnchor(0.5f, 0.5f); UI_Label(title); UI_EndAnchor(); UI_EndPad(); UI_EndFrame(); } void UI_BeginWindow(UI_WINDOW_SETTINGS settings) { const bool show_scroll_hints = M_ShouldShowScrollHints(&settings); ASSERT(m_ContextStackSize < ARRAY_SIZE(m_ContextStack)); m_ContextStack[m_ContextStackSize++] = (M_CONTEXT) { .settings = settings, .show_scroll_hints = show_scroll_hints, }; UI_BeginFrame( settings.heavy ? UI_FRAME_DIALOG_BACKGROUND_HEAVY : UI_FRAME_DIALOG_BACKGROUND); UI_BeginPad(M_GetOuterPad(), M_GetOuterPad()); UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, .align = { .h = UI_STACK_H_ALIGN_SPAN }, .spacing = { .v = M_GetTitleSpacing(&settings) }, }); if (settings.title != nullptr) { M_Title(settings.title); } UI_BeginPadEx( M_GetBodyPadX(), M_GetBodyPadX(), M_GetBodyPadY(), M_GetBodyPadY()); UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, .align = { .h = UI_STACK_H_ALIGN_SPAN }, }); if (settings.header_func != nullptr) { settings.header_func(settings.user_data); } if (show_scroll_hints) { M_ScrollHintRow(&settings, settings.scrollable->first_item != 0, true); } } void UI_EndWindow(void) { ASSERT(m_ContextStackSize > 0); m_ContextStackSize--; const M_CONTEXT *const ctx = &m_ContextStack[m_ContextStackSize]; const UI_WINDOW_SETTINGS *const settings = &ctx->settings; if (ctx->show_scroll_hints) { M_ScrollHintRow( settings, settings->scrollable->first_item + settings->scrollable->vis_items < settings->scrollable->max_items, false); } if (settings->footer_func != nullptr) { settings->footer_func(settings->user_data); } UI_EndStack(); UI_EndPad(); UI_EndStack(); UI_EndPad(); UI_EndFrame(); } ================================================ FILE: src/trx/game/ui/elements/window.h ================================================ #pragma once #include typedef void UI_WINDOW_CALLBACK(void *user_data); typedef struct { const char *title; const UI_SCROLLABLE *scrollable; float title_spacing; bool heavy; UI_WINDOW_CALLBACK *header_func; UI_WINDOW_CALLBACK *footer_func; void *user_data; bool reserve_scroll_space; } UI_WINDOW_SETTINGS; void UI_BeginWindow(UI_WINDOW_SETTINGS settings); void UI_EndWindow(void); ================================================ FILE: src/trx/game/ui/elements.h ================================================ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include ================================================ FILE: src/trx/game/ui/events.c ================================================ #include #include #include #include static EVENT_MANAGER *m_EventManager = nullptr; void UI_InitEvents(void) { m_EventManager = EventManager_Create(); } void UI_ShutdownEvents(void) { EventManager_Free(m_EventManager); m_EventManager = nullptr; } int32_t UI_Subscribe( const char *const event_name, const void *const sender, const EVENT_LISTENER listener, void *const user_data) { ASSERT(m_EventManager != nullptr); return EventManager_Subscribe( m_EventManager, event_name, sender, listener, user_data); } void UI_Unsubscribe(const int32_t listener_id) { if (m_EventManager != nullptr) { EventManager_Unsubscribe(m_EventManager, listener_id); } } void UI_FireEvent(const EVENT event) { if (m_EventManager != nullptr) { EventManager_Fire(m_EventManager, &event); } } ================================================ FILE: src/trx/game/ui/events.h ================================================ #pragma once #include typedef void (*EVENT_LISTENER)(const EVENT *, void *user_data); void UI_InitEvents(void); void UI_ShutdownEvents(void); int32_t UI_Subscribe( const char *event_name, const void *sender, EVENT_LISTENER listener, void *user_data); void UI_Unsubscribe(int32_t listener_id); void UI_FireEvent(EVENT event); ================================================ FILE: src/trx/game/ui/helpers.c ================================================ #include #include void UI_MeasureWrapper(UI_NODE *const node) { node->measure_w = 0.0f; node->measure_h = 0.0f; const UI_NODE *child = node->first_child; while (child != nullptr) { node->measure_w = MAX(node->measure_w, child->measure_w); node->measure_h = MAX(node->measure_h, child->measure_h); child = child->next_sibling; } } void UI_LayoutBasic( UI_NODE *const node, const float x, const float y, const float w, const float h) { node->x = x; node->y = y; node->w = w; node->h = h; } void UI_LayoutWrapper( UI_NODE *const node, const float x, const float y, const float w, const float h) { UI_LayoutBasic(node, x, y, w, h); UI_NODE *child = node->first_child; while (child != nullptr) { if (child->ops.layout != nullptr) { child->ops.layout(child, x, y, w, h); } child = child->next_sibling; } } void UI_DrawWrapper(const UI_NODE *const node) { const UI_NODE *child = node->first_child; while (child != nullptr) { if (child->ops.draw != nullptr) { child->ops.draw(child); } child = child->next_sibling; } } ================================================ FILE: src/trx/game/ui/helpers.h ================================================ #pragma once #include // Repetitive widget ops strategies void UI_MeasureWrapper(UI_NODE *node); void UI_LayoutBasic(UI_NODE *node, float x, float y, float w, float h); void UI_LayoutWrapper(UI_NODE *node, float x, float y, float w, float h); void UI_DrawWrapper(const UI_NODE *node); ================================================ FILE: src/trx/game/ui/hud/console.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static void M_MoveHistoryUp(UI_CONSOLE_STATE *const s) { s->history_idx--; CLAMP(s->history_idx, 0, Console_History_GetLength()); const char *const new_prompt = Console_History_Get(s->history_idx); UI_Prompt_ChangeText(&s->prompt, new_prompt == nullptr ? "" : new_prompt); } static void M_MoveHistoryDown(UI_CONSOLE_STATE *const s) { s->history_idx++; CLAMP(s->history_idx, 0, Console_History_GetLength()); const char *const new_prompt = Console_History_Get(s->history_idx); UI_Prompt_ChangeText(&s->prompt, new_prompt == nullptr ? "" : new_prompt); } static void M_HandleKeyDown(const EVENT *const event, void *const user_data) { if (!Console_IsOpened()) { return; } UI_CONSOLE_STATE *const s = user_data; const UI_INPUT key = (UI_INPUT)(uintptr_t)event->data; // clang-format off switch (key) { case UI_KEY_UP: M_MoveHistoryUp(s); break; case UI_KEY_DOWN: M_MoveHistoryDown(s); break; default: break; } // clang-format on } static void M_HandleOpen(const EVENT *event, void *user_data) { UI_CONSOLE_STATE *const s = user_data; UI_Prompt_SetFocus(&s->prompt, true); s->history_idx = Console_History_GetLength(); } static void M_HandleClose(const EVENT *event, void *user_data) { UI_CONSOLE_STATE *const s = user_data; UI_Prompt_SetFocus(&s->prompt, false); UI_Prompt_Clear(&s->prompt); } static void M_HandleCancel(const EVENT *const event, void *const data) { Console_Close(); } static void M_HandleConfirm(const EVENT *event, void *user_data) { UI_CONSOLE_STATE *const s = user_data; const char *text = event->data; Console_History_Append(text); Console_Eval(text); GameEvent_Fire((EVENT) { .name = GAME_EVENT_COMMAND, .data = text, }); Console_Close(); s->history_idx = Console_History_GetLength(); } static void M_DrawBackdrop(void) { const int32_t sx = 0; const int32_t sw = Viewport_GetWidth(VIEWPORT_UI); const int32_t sh = UI_Scaler_Calc( // not entirely accurate, but good enough UI_TEXT_HEIGHT * 1.0 + 7 * UI_TEXT_HEIGHT * 0.8, UI_SCALER_TARGET_TEXT); const int32_t sy = Viewport_GetHeight(VIEWPORT_UI) - sh; const RGBA_8888 top = { 0, 0, 0, 0 }; const RGBA_8888 bottom = { 0, 0, 0, 196 }; Output_DrawScreenGradientQuad(sx, sy, 0, sw, sh, top, top, bottom, bottom); } static void M_Draw(const UI_NODE *node) { UI_CONSOLE_STATE *const s = *(UI_CONSOLE_STATE **)node->data; if (Console_IsOpened() || s->logs.vis_lines > 0) { M_DrawBackdrop(); } UI_DrawWrapper(node); } void UI_Console_Init(UI_CONSOLE_STATE *const s) { UI_Prompt_Init(&s->prompt); UI_ConsoleLogs_Init(&s->logs); struct { const char *event_name; const void *sender; EVENT_LISTENER handler; } listeners[] = { { "console_open", nullptr, M_HandleOpen }, { "console_close", nullptr, M_HandleClose }, { "cancel", &s->prompt, M_HandleCancel }, { "confirm", &s->prompt, M_HandleConfirm }, { "key_down", nullptr, M_HandleKeyDown }, { 0 }, }; for (int32_t i = 0; listeners[i].event_name != nullptr; i++) { s->listeners[i] = UI_Subscribe( listeners[i].event_name, listeners[i].sender, listeners[i].handler, s); } s->history_idx = -1; } void UI_Console_Free(UI_CONSOLE_STATE *const s) { UI_ConsoleLogs_Free(&s->logs); UI_Prompt_Free(&s->prompt); for (int32_t i = 0; i < 5; i++) { UI_Unsubscribe(s->listeners[i]); } } void UI_Console_Control(UI_CONSOLE_STATE *const s) { UI_Prompt_Control(&s->prompt); } void UI_Console(UI_CONSOLE_STATE *const s) { UI_Prompt_SetFocus(&s->prompt, Console_IsOpened()); UI_NODE *const node = UI_AllocNode( &(UI_WIDGET_OPS) { .measure = UI_MeasureWrapper, .layout = UI_LayoutWrapper, .draw = M_Draw, }, sizeof(UI_CONSOLE_STATE *)); *(UI_CONSOLE_STATE **)node->data = s; UI_AddChild(node); UI_PushCurrent(node); UI_BeginModal(0.0f, 1.0f); UI_BeginPad(5.0f, 5.0f); UI_BeginStack(UI_STACK_VERTICAL); UI_ConsoleLogs(&s->logs); UI_Spacer(0.0f, 8.0f); if (Console_IsOpened()) { UI_Prompt(&s->prompt); } else { UI_Spacer(0.0f, UI_TEXT_HEIGHT); } UI_EndStack(); UI_EndModal(); UI_EndPad(); UI_PopCurrent(); } ================================================ FILE: src/trx/game/ui/hud/console.h ================================================ #pragma once #include #include // Dev console display widget. typedef struct { UI_CONSOLE_LOGS logs; UI_PROMPT_STATE prompt; int32_t listeners[5]; int32_t history_idx; } UI_CONSOLE_STATE; // state functions void UI_Console_Init(UI_CONSOLE_STATE *s); void UI_Console_Free(UI_CONSOLE_STATE *s); void UI_Console_Control(UI_CONSOLE_STATE *s); // draw functions void UI_Console(UI_CONSOLE_STATE *s); ================================================ FILE: src/trx/game/ui/hud/console_logs.c ================================================ #include #include #include #include #include #include #include #include #include #define M_LOG_SCALE 0.8f #define M_MAX_LOG_LINES 20 #define M_DELAY_PER_CHAR 0.2 static void M_ScrollLogs(UI_CONSOLE_LOGS *s); static void M_UpdateLogCount(UI_CONSOLE_LOGS *s); static void M_HandleLog(const EVENT *event, void *user_data); static void M_HandleClear(const EVENT *event, void *user_data); static void M_ScrollLogs(UI_CONSOLE_LOGS *const s) { int32_t i = s->max_lines - 1; while (i >= 0 && s->logs[i].text == nullptr) { i--; } bool need_layout = false; while (i >= 0 && s->logs[i].text != nullptr && Clock_GetRealTime() >= s->logs[i].expire_at) { s->logs[i].expire_at = 0.0; Memory_FreePointer(&s->logs[i].text); need_layout = true; i--; } if (need_layout) { M_UpdateLogCount(s); } } static void M_UpdateLogCount(UI_CONSOLE_LOGS *const s) { s->vis_lines = 0; for (int32_t i = s->max_lines - 1; i >= 0; i--) { if (s->logs[i].expire_at != 0.0) { s->vis_lines = i + 1; break; } } } static void M_HandleLog(const EVENT *const event, void *const user_data) { const char *text = event->data; UI_CONSOLE_LOGS *const s = user_data; Memory_FreePointer(&s->logs[s->max_lines - 1].text); for (int32_t i = s->max_lines - 1; i > 0; i--) { s->logs[i] = s->logs[i - 1]; } s->logs[0].expire_at = Clock_GetRealTime() + strlen(text) * M_DELAY_PER_CHAR; s->logs[0].text = UI_Text_WordWrap(text, M_LOG_SCALE, UI_GetCanvasWidth()); M_UpdateLogCount(s); } static void M_HandleClear(const EVENT *const event, void *const user_data) { UI_CONSOLE_LOGS *const s = user_data; for (size_t i = 0; i < s->max_lines; i++) { s->logs[i].expire_at = 0.0; } M_ScrollLogs(s); } void UI_ConsoleLogs_Init(UI_CONSOLE_LOGS *const s) { if (s->max_lines <= 0) { s->max_lines = M_MAX_LOG_LINES; } s->logs = Memory_Alloc(s->max_lines * sizeof(UI_CONSOLE_LOG_LINE)); s->vis_lines = 0; s->listeners[0] = UI_Subscribe("console_log", nullptr, M_HandleLog, s); s->listeners[1] = UI_Subscribe("console_clear", nullptr, M_HandleClear, s); } void UI_ConsoleLogs_Free(UI_CONSOLE_LOGS *const s) { if (s->logs != nullptr) { for (int32_t i = 0; i < M_MAX_LOG_LINES; i++) { Memory_FreePointer(&s->logs[i].text); } Memory_FreePointer(&s->logs); } UI_Unsubscribe(s->listeners[0]); UI_Unsubscribe(s->listeners[1]); } void UI_ConsoleLogs(UI_CONSOLE_LOGS *const s) { ASSERT(s != nullptr); M_ScrollLogs(s); UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, .align = { .h = UI_STACK_H_ALIGN_LEFT, .v = UI_STACK_V_ALIGN_CENTER, }, }); for (int32_t i = s->vis_lines - 1; i >= 0; i--) { UI_LabelEx( s->logs[i].text, (UI_LABEL_SETTINGS) { .scale = M_LOG_SCALE }); } UI_EndStack(); } ================================================ FILE: src/trx/game/ui/hud/console_logs.h ================================================ #pragma once #include #include // Scrollback for the dev console. typedef struct { char *text; double expire_at; } UI_CONSOLE_LOG_LINE; typedef struct { size_t max_lines; size_t vis_lines; UI_CONSOLE_LOG_LINE *logs; int32_t listeners[2]; } UI_CONSOLE_LOGS; // state functions void UI_ConsoleLogs_Init(UI_CONSOLE_LOGS *s); void UI_ConsoleLogs_Free(UI_CONSOLE_LOGS *s); void UI_ConsoleLogs(UI_CONSOLE_LOGS *s); ================================================ FILE: src/trx/game/ui/hud/overlay.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include typedef struct UI_OVERLAY_STATE { struct { bool state; int32_t frame; } blink; UI_FPS_COUNTER_STATE *fps; bool force_show_healthbar; bool show_arrows[6]; bool show_version; UI_OVERLAY_TEXT top_text; UI_OVERLAY_TEXT bottom_text; UI_FLASH_STATE flash_state; } UI_OVERLAY_STATE; static struct { bool resize; const char *label; } m_ArrowInfo[] = { [UI_OVERLAY_ARROW_TR] = { true, "\\{arrow up}" }, [UI_OVERLAY_ARROW_TL] = { true, "\\{arrow up}" }, [UI_OVERLAY_ARROW_BL] = { true, "\\{arrow down}" }, [UI_OVERLAY_ARROW_BR] = { true, "\\{arrow down}" }, [UI_OVERLAY_ARROW_BCL] = { false, "\\{button left}" }, [UI_OVERLAY_ARROW_BCR] = { false, "\\{button right}" }, }; static bool M_LaraHealthBar( const UI_OVERLAY_STATE *const s, const UI_ELEMENT_LOCATION location) { if (location != g_Config.ui.lara_health_bar.location) { return false; } if (!Lara_IsControllable() || (!Game_IsPlaying() && !s->force_show_healthbar)) { return false; } if (!g_Config.ui.enable_game_ui) { return false; } return UI_LaraHealthBar(s->blink.state, s->force_show_healthbar); } static bool M_LaraAirBar( const UI_OVERLAY_STATE *const s, const UI_ELEMENT_LOCATION location) { if (location != g_Config.ui.lara_air_bar.location) { return false; } if (!Lara_IsControllable() || !Game_IsPlaying()) { return false; } if (!g_Config.ui.enable_game_ui) { return false; } return UI_LaraAirBar(s->blink.state); } static bool M_LaraSprintBar( const UI_OVERLAY_STATE *const s, const UI_ELEMENT_LOCATION location) { if (location != g_Config.ui.lara_sprint_bar.location) { return false; } if (!Lara_IsControllable() || !Game_IsPlaying()) { return false; } if (!g_Config.ui.enable_game_ui) { return false; } return UI_LaraSprintBar(); } static bool M_LaraExposureBar( const UI_OVERLAY_STATE *const s, const UI_ELEMENT_LOCATION location) { if (location != g_Config.ui.lara_exposure_bar.location) { return false; } if (!Lara_IsControllable() || !Game_IsPlaying()) { return false; } if (!g_Config.ui.enable_game_ui) { return false; } return UI_LaraExposureBar(s->blink.state); } static const char *M_ResolveOverlayTextRaw(const UI_OVERLAY_TEXT *const t) { if (t == nullptr) { return nullptr; } switch (t->kind) { case UI_OVERLAY_TEXT_NONE: return nullptr; case UI_OVERLAY_TEXT_LITERAL: return t->literal; case UI_OVERLAY_TEXT_GS_KEY: return GameString_Get(t->gs_key); case UI_OVERLAY_TEXT_OBJECT_NAME: return Object_GetName(t->object_id); } return nullptr; } static const char *M_ResolveOverlayText(const UI_OVERLAY_TEXT *const t) { const char *const raw = M_ResolveOverlayTextRaw(t); if (raw == nullptr) { return nullptr; } if (t->fmt_gs_key == nullptr) { return raw; } return String_FormatStatic(GameString_Get(t->fmt_gs_key), raw); } static bool M_EnemyHealthBar(const UI_ELEMENT_LOCATION location) { if (location != g_Config.ui.enemy_health_bar.location) { return false; } if (!Game_IsPlaying()) { return false; } if (!g_Config.ui.enable_game_ui) { return false; } return UI_EnemyHealthBar(); } static bool M_AmmoLabel(const UI_ELEMENT_LOCATION location) { if (location != g_Config.ui.ammo_counter.location) { return false; } if (!Game_IsPlaying()) { return false; } if (!g_Config.ui.enable_game_ui) { return false; } return UI_AmmoLabel(); } static void M_Arrow( const UI_OVERLAY_STATE *const s, const UI_OVERLAY_ARROW arrow) { if (s->show_arrows[arrow]) { // make sure the arrow has exactly the same size as the bar if (m_ArrowInfo[arrow].resize) { UI_BeginResize( -1.0, UI_BAR_HEIGHT * UI_Scaler_GetScale(UI_SCALER_TARGET_BAR) / UI_Scaler_GetScale(UI_SCALER_TARGET_TEXT)); } UI_Label(m_ArrowInfo[arrow].label); if (m_ArrowInfo[arrow].resize) { UI_EndResize(); } } } static void M_DebugPosTopLeft(void) { const ITEM *const lara = Lara_GetItem(); const LARA_INFO *const lara_info = Lara_GetLaraInfo(); if (lara == nullptr) { return; } const OBJECT_ID obj_id = Lara_GetAnimationObject(); const ITEM *const vehicle = Lara_Vehicle_GetItem(); UI_BeginStack(UI_STACK_HORIZONTAL); UI_BeginStack(UI_STACK_VERTICAL); if (g_Config.debug.enable_debug_pos) { UI_Label(GS("general/overlay/debug_position")); UI_Label(GS("general/overlay/debug_rotation")); UI_Label(GS("general/overlay/debug_speed")); } if (g_Config.debug.enable_debug_anim) { UI_Label(GS("general/overlay/debug_animation")); UI_Label(GS("general/overlay/debug_animation_state")); } if (g_Config.debug.enable_debug_camera) { UI_Label(GS("general/overlay/debug_camera_pos")); UI_Label(GS("general/overlay/debug_camera_target")); } UI_EndStack(); UI_BeginStack(UI_STACK_VERTICAL); if (g_Config.debug.enable_debug_pos) { UI_Label(String_FormatStatic( "\\{small}%d, %d, %d / %d", lara->pos.x / WALL_L, lara->pos.y / WALL_L, lara->pos.z / WALL_L, lara->room_num)); UI_Label(String_FormatStatic( "\\{small}%d°, %d°, %d°", (int32_t)lara->rot.x * 360 / DEG_360, (int32_t)lara->rot.y * 360 / DEG_360, (int32_t)lara->rot.z * 360 / DEG_360)); UI_Label(String_FormatStatic( "\\{small}%d, %d", vehicle != nullptr ? vehicle->speed : lara->speed, vehicle != nullptr ? vehicle->fall_speed : lara->fall_speed)); } if (g_Config.debug.enable_debug_anim) { UI_Label(String_FormatStatic( "\\{small}%d, %d", Item_GetRelativeObjAnim(lara, obj_id), Item_GetRelativeFrame(lara))); UI_Label(String_FormatStatic( "\\{small}%d, %d (%d)", lara->current_anim_state, lara->goal_anim_state, Object_ToGameID(obj_id))); } if (g_Config.debug.enable_debug_camera) { UI_Label(String_FormatStatic( "\\{small}%d, %d, %d / %d", g_Camera.pos.x / WALL_L, g_Camera.pos.y / WALL_L, g_Camera.pos.z / WALL_L, g_Camera.pos.room_num)); UI_Label(String_FormatStatic( "\\{small}%d, %d, %d / %d", g_Camera.target.x / WALL_L, g_Camera.target.y / WALL_L, g_Camera.target.z / WALL_L, g_Camera.target.room_num)); } UI_EndStack(); UI_EndStack(); } static void M_DebugPosTopRight(void) { const ITEM *const lara = Lara_GetItem(); if (lara == nullptr) { return; } UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, .align = { .h = UI_STACK_H_ALIGN_RIGHT }, }); if (g_Config.debug.enable_debug_pos) { UI_Label(String_FormatStatic( "\\{small}%d, %d, %d", lara->pos.x, lara->pos.y, lara->pos.z)); UI_Label(String_FormatStatic( "\\{small}%d, %d, %d", lara->rot.x, lara->rot.y, lara->rot.z)); } if (g_Config.debug.enable_debug_camera) { UI_Label(String_FormatStatic( "\\{small}%d, %d, %d", g_Camera.pos.x, g_Camera.pos.y, g_Camera.pos.z)); UI_Label(String_FormatStatic( "\\{small}%d, %d, %d", g_Camera.target.x, g_Camera.target.y, g_Camera.target.z)); } if (g_Config.debug.enable_debug_status && g_Config.debug.enable_invulnerability) { UI_LabelEx( GS("general/overlay/debug_immune"), (UI_LABEL_SETTINGS) { .scale = 0.8 }); } UI_EndStack(); } static bool M_CommonRegion( const UI_OVERLAY_STATE *const s, const UI_ELEMENT_LOCATION location) { bool shown = false; UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, .align = { .h = UI_STACK_H_ALIGN_CENTER }, .spacing = { .v = 10 }, }); shown |= M_LaraHealthBar(s, location); shown |= M_LaraAirBar(s, location); shown |= M_LaraSprintBar(s, location); shown |= M_LaraExposureBar(s, location); shown |= M_EnemyHealthBar(location); UI_EndStack(); shown |= M_AmmoLabel(location); return shown; } static void M_TopLeftRegion(const UI_OVERLAY_STATE *const s) { UI_BeginOverlayRegion(0.0f, 0.0f); if (!M_CommonRegion(s, UI_ELEMENT_LOCATION_TOP_LEFT)) { M_Arrow(s, UI_OVERLAY_ARROW_TL); } if (g_Config.ui.enable_game_ui) { M_DebugPosTopLeft(); if (g_Config.ui.enable_fps_counter) { UI_FPSCounter(s->fps); } } UI_EndOverlayRegion(); } static void M_TopCenterRegion(const UI_OVERLAY_STATE *const s) { UI_BeginOverlayRegion(0.5f, 0.0f); M_CommonRegion(s, UI_ELEMENT_LOCATION_TOP_CENTER); { const char *const txt = M_ResolveOverlayText(&s->top_text); if (txt != nullptr) { if (s->top_text.flash_enabled) { UI_BeginFlash(&s->flash_state); } UI_Label(txt); if (s->top_text.flash_enabled) { UI_EndFlash(); } } } UI_EndOverlayRegion(); } static void M_TopRightRegion(const UI_OVERLAY_STATE *const s) { UI_BeginOverlayRegion(1.0f, 0.0f); if (!M_CommonRegion(s, UI_ELEMENT_LOCATION_TOP_RIGHT)) { M_Arrow(s, UI_OVERLAY_ARROW_TR); } if (g_Config.ui.enable_game_ui) { M_DebugPosTopRight(); } UI_EndOverlayRegion(); } static void M_BottomLeftRegion(const UI_OVERLAY_STATE *const s) { UI_BeginOverlayRegion(0.0f, 1.0f); if (!M_CommonRegion(s, UI_ELEMENT_LOCATION_BOTTOM_LEFT)) { M_Arrow(s, UI_OVERLAY_ARROW_BL); } UI_EndOverlayRegion(); } static void M_BottomCenterRegion(const UI_OVERLAY_STATE *const s) { UI_BeginOverlayRegion(0.5f, 1.0f); { const char *const txt = M_ResolveOverlayText(&s->bottom_text); if (txt != nullptr) { if (s->bottom_text.flash_enabled) { UI_BeginFlash(&s->flash_state); } UI_BeginRowArrows( s->show_arrows[UI_OVERLAY_ARROW_BCL], s->show_arrows[UI_OVERLAY_ARROW_BCR], UI_ROW_ARROWS_WIDE); UI_Label(txt); UI_EndRowArrows(); if (s->bottom_text.flash_enabled) { UI_EndFlash(); } } } M_CommonRegion(s, UI_ELEMENT_LOCATION_BOTTOM_CENTER); UI_EndOverlayRegion(); } static void M_BottomRightRegion(const UI_OVERLAY_STATE *const s) { UI_BeginOverlayRegion(1.0f, 1.0f); if (!M_CommonRegion(s, UI_ELEMENT_LOCATION_BOTTOM_RIGHT)) { M_Arrow(s, UI_OVERLAY_ARROW_BR); } if (s->show_version && g_Config.ui.show_title_version) { UI_LabelEx(g_TRXVersion, (UI_LABEL_SETTINGS) { .scale = 0.5f }); } UI_EndOverlayRegion(); } UI_OVERLAY_STATE *UI_Overlay_Init(void) { UI_OVERLAY_STATE *const s = Memory_Alloc(sizeof(UI_OVERLAY_STATE)); s->fps = UI_FPSCounter_Init(); UI_Flash_Init(&s->flash_state, 20); return s; } void UI_Overlay_Free(UI_OVERLAY_STATE *const s) { if (s == nullptr) { return; } if (s->fps != nullptr) { UI_FPSCounter_Free(s->fps); s->fps = nullptr; } UI_Flash_Free(&s->flash_state); Memory_Free(s); } void UI_Overlay_Control(UI_OVERLAY_STATE *const s) { s->force_show_healthbar = false; UI_LaraHealthBar_Control(); s->blink.frame++; if (s->blink.frame >= 10) { s->blink.state = !s->blink.state; s->blink.frame = 0; } UI_Flash_Control(&s->flash_state); } void UI_Overlay_ForceHealthBar(UI_OVERLAY_STATE *const s, const bool show) { s->force_show_healthbar = show; } void UI_Overlay(UI_OVERLAY_STATE *const s) { M_TopLeftRegion(s); M_TopCenterRegion(s); M_TopRightRegion(s); M_BottomLeftRegion(s); M_BottomCenterRegion(s); M_BottomRightRegion(s); } void UI_BeginOverlayRegion(const float x, const float y) { // clang-format off const UI_STACK_H_ALIGN h_align = x > 0.55f ? UI_STACK_H_ALIGN_RIGHT : x < 0.45f ? UI_STACK_H_ALIGN_LEFT : UI_STACK_H_ALIGN_CENTER; // clang-format on UI_BeginModal(x, y); UI_BeginPad(20.0f, 14.0f); UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_VERTICAL, .align = { .h = h_align }, .spacing = { .v = 3 }, }); } void UI_EndOverlayRegion(void) { UI_EndStack(); UI_EndPad(); UI_EndModal(); } void UI_Overlay_ShowArrow( UI_OVERLAY_STATE *const s, const UI_OVERLAY_ARROW arrow, const bool show) { s->show_arrows[arrow] = show; } void UI_Overlay_ShowVersion(UI_OVERLAY_STATE *const s, const bool show) { s->show_version = show; } void UI_Overlay_SetTopText( UI_OVERLAY_STATE *const s, const UI_OVERLAY_TEXT text) { s->top_text = text; } void UI_Overlay_SetBottomText( UI_OVERLAY_STATE *const s, const UI_OVERLAY_TEXT text) { s->bottom_text = text; } ================================================ FILE: src/trx/game/ui/hud/overlay.h ================================================ #pragma once // Ingame user interface display widget. #include #include typedef enum { UI_OVERLAY_TEXT_NONE = 0, UI_OVERLAY_TEXT_LITERAL, UI_OVERLAY_TEXT_GS_KEY, UI_OVERLAY_TEXT_OBJECT_NAME, } UI_OVERLAY_TEXT_KIND; typedef struct { UI_OVERLAY_TEXT_KIND kind; // Optional GS key of a %s format wrapper applied at draw time. const char *fmt_gs_key; bool flash_enabled; union { const char *literal; const char *gs_key; OBJECT_ID object_id; }; } UI_OVERLAY_TEXT; typedef enum { UI_OVERLAY_ARROW_TL, // top-left screen corner UI_OVERLAY_ARROW_TR, // top-right screen corner UI_OVERLAY_ARROW_BL, // bottom-left screen corner UI_OVERLAY_ARROW_BR, // bottom-right screen corner UI_OVERLAY_ARROW_BCL, // low text left side UI_OVERLAY_ARROW_BCR, // low text right side } UI_OVERLAY_ARROW; typedef struct UI_OVERLAY_STATE UI_OVERLAY_STATE; // state functions UI_OVERLAY_STATE *UI_Overlay_Init(void); void UI_Overlay_Free(UI_OVERLAY_STATE *s); void UI_Overlay_Control(UI_OVERLAY_STATE *s); // draw functions void UI_Overlay(UI_OVERLAY_STATE *s); void UI_BeginOverlayRegion(float x, float y); void UI_EndOverlayRegion(void); void UI_Overlay_ForceHealthBar(UI_OVERLAY_STATE *s, bool show); void UI_Overlay_ShowArrow( UI_OVERLAY_STATE *s, UI_OVERLAY_ARROW arrow, bool show); void UI_Overlay_ShowVersion(UI_OVERLAY_STATE *s, bool show); void UI_Overlay_SetTopText(UI_OVERLAY_STATE *s, UI_OVERLAY_TEXT text); void UI_Overlay_SetBottomText(UI_OVERLAY_STATE *s, UI_OVERLAY_TEXT text); ================================================ FILE: src/trx/game/ui/hud.h ================================================ #pragma once #include #include #include ================================================ FILE: src/trx/game/ui/scaler.c ================================================ #include #include #include #include static float M_DoCalc( const float unit, const float base_width, const float base_height, const double factor) { const float vp_width = Viewport_GetWidth(VIEWPORT_UI); const float vp_height = Viewport_GetHeight(VIEWPORT_UI); const float sign = unit < 0 ? -1 : 1; const float sx = ((double)vp_width * ABS(unit) * factor) / MAX(1, base_width); const float sy = ((double)vp_height * ABS(unit) * factor) / MAX(1, base_height); return MIN(sx, sy) * sign; } double UI_Scaler_GetScale(const UI_SCALER_TARGET target) { switch (target) { case UI_SCALER_TARGET_BAR: return g_Config.ui.bar_scale; case UI_SCALER_TARGET_TEXT: return g_Config.ui.text_scale; case UI_SCALER_TARGET_ASSAULT_DIGITS: return g_Config.ui.text_scale; default: return 1.0; } } float UI_Scaler_Calc(const float unit, const UI_SCALER_TARGET target) { return M_DoCalc(unit, 640, 480, UI_Scaler_GetScale(target)); } float UI_Scaler_CalcInverse(const float unit, const UI_SCALER_TARGET target) { return unit * 0x10000 / MAX(1, UI_Scaler_Calc(0x10000, target)); } ================================================ FILE: src/trx/game/ui/scaler.h ================================================ #pragma once #include typedef enum { UI_SCALER_TARGET_GENERIC, UI_SCALER_TARGET_BAR, UI_SCALER_TARGET_TEXT, UI_SCALER_TARGET_ASSAULT_DIGITS, } UI_SCALER_TARGET; double UI_Scaler_GetScale(const UI_SCALER_TARGET target); float UI_Scaler_Calc(float unit, UI_SCALER_TARGET target); float UI_Scaler_CalcInverse(float unit, UI_SCALER_TARGET target); ================================================ FILE: src/trx/game/ui/scrollable.c ================================================ #include #include #include static void M_Clamp(UI_SCROLLABLE *const s, const bool include_selected_item) { if (include_selected_item && s->sel_item != -1) { CLAMP(s->first_item, s->sel_item - s->vis_items + 1, s->sel_item); } CLAMPG(s->first_item, s->max_items - s->vis_items); CLAMPL(s->first_item, 0); } bool UI_Scrollable_SelectNext( UI_SCROLLABLE *const s, const bool enable_wraparound) { if (s->sel_item + 1 < s->max_items) { s->sel_item++; } else if (enable_wraparound) { s->sel_item = 0; } else { return false; } M_Clamp(s, true); return true; } bool UI_Scrollable_SelectPrev( UI_SCROLLABLE *const s, const bool enable_wraparound) { if (s->sel_item > 0) { s->sel_item--; } else if (enable_wraparound) { s->sel_item = s->max_items - 1; } else { return false; } M_Clamp(s, true); return true; } bool UI_Scrollable_ScrollDown( UI_SCROLLABLE *const s, const bool enable_wraparound) { if (s->first_item + 1 <= s->max_items - s->vis_items) { s->first_item++; } else if (enable_wraparound) { s->first_item = 0; } else { return false; } M_Clamp(s, false); return true; } bool UI_Scrollable_ScrollUp( UI_SCROLLABLE *const s, const bool enable_wraparound) { if (s->first_item > 0) { s->first_item--; } else if (enable_wraparound) { s->first_item = s->max_items - 1; } else { return false; } M_Clamp(s, false); return true; } void UI_Scrollable_SetVisibleItems( UI_SCROLLABLE *const s, const int32_t visible_items) { s->vis_items = visible_items; CLAMPL(s->vis_items, 0); M_Clamp(s, true); } void UI_Scrollable_SetMaxItems(UI_SCROLLABLE *const s, const int32_t max_items) { s->max_items = max_items; CLAMP(s->sel_item, 0, s->max_items - 1); } void UI_Scrollable_SelectItem(UI_SCROLLABLE *const s, const int32_t row) { s->sel_item = row; if (s->sel_item != -1) { CLAMP(s->sel_item, 0, s->max_items - 1); CLAMP(s->first_item, s->sel_item - s->vis_items + 1, s->sel_item); } } void UI_Scrollable_SelectFirstItem(UI_SCROLLABLE *const s) { UI_Scrollable_SelectItem(s, 0); } void UI_Scrollable_SelectLastItem(UI_SCROLLABLE *const s) { UI_Scrollable_SelectItem(s, s->max_items - 1); } int32_t UI_Scrollable_GetFirstVisibleItem(const UI_SCROLLABLE *const s) { return s->first_item; } int32_t UI_Scrollable_GetSelectedItem(const UI_SCROLLABLE *const s) { return s->sel_item; } int32_t UI_Scrollable_GetLastVisibleItem(const UI_SCROLLABLE *const s) { return MIN(s->first_item + s->vis_items - 1, s->max_items - 1); } bool UI_Scrollable_IsItemVisible( const UI_SCROLLABLE *const s, const int32_t item) { return item >= UI_Scrollable_GetFirstVisibleItem(s) && item <= UI_Scrollable_GetLastVisibleItem(s); } bool UI_Scrollable_IsItemSelected( const UI_SCROLLABLE *const s, const int32_t item) { return item == UI_Scrollable_GetSelectedItem(s); } ================================================ FILE: src/trx/game/ui/scrollable.h ================================================ #pragma once #include typedef struct { int32_t first_item; int32_t sel_item; int32_t max_items; int32_t vis_items; } UI_SCROLLABLE; bool UI_Scrollable_SelectNext(UI_SCROLLABLE *s, bool enable_wraparound); bool UI_Scrollable_SelectPrev(UI_SCROLLABLE *s, bool enable_wraparound); bool UI_Scrollable_ScrollDown(UI_SCROLLABLE *s, bool enable_wraparound); bool UI_Scrollable_ScrollUp(UI_SCROLLABLE *s, bool enable_wraparound); void UI_Scrollable_SetVisibleItems(UI_SCROLLABLE *s, int32_t visible_items); void UI_Scrollable_SetMaxItems(UI_SCROLLABLE *s, int32_t max_items); void UI_Scrollable_SelectItem(UI_SCROLLABLE *s, int32_t item); void UI_Scrollable_SelectFirstItem(UI_SCROLLABLE *s); void UI_Scrollable_SelectLastItem(UI_SCROLLABLE *s); int32_t UI_Scrollable_GetFirstVisibleItem(const UI_SCROLLABLE *s); int32_t UI_Scrollable_GetLastVisibleItem(const UI_SCROLLABLE *s); int32_t UI_Scrollable_GetSelectedItem(const UI_SCROLLABLE *s); bool UI_Scrollable_IsItemVisible(const UI_SCROLLABLE *s, int32_t item); bool UI_Scrollable_IsItemSelected(const UI_SCROLLABLE *s, int32_t item); ================================================ FILE: src/trx/game/ui/settings.c ================================================ #include #include #include #include #include #include #include #include #include #include typedef struct { char *name; UI_BAR_THEME theme; } M_THEME_ENTRY; typedef struct M_THEME_LOOKUP { char *name; int32_t index; UT_hash_handle hh; } M_THEME_LOOKUP; typedef struct { int32_t color_count; M_THEME_ENTRY *colors; struct M_THEME_LOOKUP *lookup; } M_THEME_GROUP; typedef struct { char *name; char *name_gs; UI_BAR_THEME_KIND kind; M_THEME_GROUP group; } M_BAR_THEME_ENTRY; typedef struct M_BAR_THEME_LOOKUP { char *name; int32_t index; UT_hash_handle hh; } M_BAR_THEME_LOOKUP; typedef struct { int32_t bar_theme_count; M_BAR_THEME_ENTRY *bar_themes; struct M_BAR_THEME_LOOKUP *bar_lookup; } M_SETTINGS; typedef struct { char *const *const pc_color; char *const *const ps1_color; } M_BAR_COLOR_SELECT; static const M_BAR_COLOR_SELECT m_BarColorSelect[UI_BAR_NUMBER_OF] = { [UI_BAR_LARA_HP] = { .pc_color = &g_Config.ui.lara_health_bar.color, .ps1_color = &g_Config.ui.lara_health_bar.color_ps1, }, [UI_BAR_LARA_HP_POISON] = { .pc_color = &g_Config.ui.lara_health_bar.poison_color, .ps1_color = &g_Config.ui.lara_health_bar.poison_color_ps1, }, [UI_BAR_LARA_AIR] = { .pc_color = &g_Config.ui.lara_air_bar.color, .ps1_color = &g_Config.ui.lara_air_bar.color_ps1, }, [UI_BAR_LARA_STAMINA] = { .pc_color = &g_Config.ui.lara_sprint_bar.color, .ps1_color = &g_Config.ui.lara_sprint_bar.color_ps1, }, [UI_BAR_LARA_EXPOSURE] = { .pc_color = &g_Config.ui.lara_exposure_bar.color, .ps1_color = &g_Config.ui.lara_exposure_bar.color_ps1, }, [UI_BAR_ENEMY_HP] = { .pc_color = &g_Config.ui.enemy_health_bar.color, .ps1_color = &g_Config.ui.enemy_health_bar.color_ps1, }, [UI_BAR_ALLY_HP] = { .pc_color = &g_Config.ui.enemy_health_bar.color_allies, .ps1_color = &g_Config.ui.enemy_health_bar.color_allies_ps1, }, }; static M_SETTINGS m_Settings; static UI_MENU_COLORS_PC m_MenuColorsPC[3]; // indexed [g_TRVersion - 1] static UI_MENU_COLORS_PS1 m_MenuColorsPS1[3]; // indexed [g_TRVersion - 1] static void M_ExitWithJSONError( const char *const source_path, const JSON_READ_IO *const io) { JSONFile_ExitWithReadIOError( io, String_FormatStatic("%s: ui settings parse error", source_path)); } static void M_FreeThemeGroup(M_THEME_GROUP *const group) { M_THEME_LOOKUP *entry = nullptr; M_THEME_LOOKUP *tmp = nullptr; HASH_ITER(hh, group->lookup, entry, tmp) { HASH_DEL(group->lookup, entry); Memory_FreePointer(&entry); } if (group->colors == nullptr) { return; } for (int32_t i = 0; i < group->color_count; i++) { Memory_FreePointer(&group->colors[i].name); } Memory_FreePointer(&group->colors); group->color_count = 0; group->lookup = nullptr; } static void M_ResetDynamicEnumValues(void) { const CONFIG_OPTION *const bar_look_option = Config_GetOption(&g_Config.ui.bar_look); if (bar_look_option != nullptr) { Config_DynamicEnum_ResetValues(bar_look_option); } for (int32_t i = 0; i < UI_BAR_NUMBER_OF; i++) { const M_BAR_COLOR_SELECT *const select = &m_BarColorSelect[i]; const CONFIG_OPTION *const pc_option = Config_GetOption(select->pc_color); if (pc_option != nullptr) { Config_DynamicEnum_ResetValues(pc_option); } const CONFIG_OPTION *const ps1_option = Config_GetOption(select->ps1_color); if (ps1_option != nullptr) { Config_DynamicEnum_ResetValues(ps1_option); } } } static bool M_IsBarColorNameEncountered( const UI_BAR_THEME_KIND kind, const char *const name, const int32_t stop_i, const int32_t stop_j) { for (int32_t i = 0; i < m_Settings.bar_theme_count; i++) { M_BAR_THEME_ENTRY *const theme = &m_Settings.bar_themes[i]; if (theme->kind != kind) { continue; } for (int32_t j = 0; j < theme->group.color_count; j++) { if (i == stop_i && j == stop_j) { return false; } if (String_Equivalent(theme->group.colors[j].name, name)) { return true; } } } return false; } static void M_SeedDynamicEnumBarColors( const CONFIG_OPTION *const option, const UI_BAR_THEME_KIND kind) { Config_DynamicEnum_ResetValues(option); for (int32_t i = 0; i < m_Settings.bar_theme_count; i++) { const M_BAR_THEME_ENTRY *const theme = &m_Settings.bar_themes[i]; if (theme->kind != kind) { continue; } for (int32_t j = 0; j < theme->group.color_count; j++) { const char *const name = theme->group.colors[j].name; if (M_IsBarColorNameEncountered(kind, name, i, j)) { continue; } Config_DynamicEnum_AddValue(option, name, nullptr); } } } static void M_SeedDynamicEnumValues(void) { const CONFIG_OPTION *const bar_look_option = Config_GetOption(&g_Config.ui.bar_look); if (bar_look_option != nullptr) { Config_DynamicEnum_ResetValues(bar_look_option); for (int32_t i = 0; i < m_Settings.bar_theme_count; i++) { const M_BAR_THEME_ENTRY *const theme = &m_Settings.bar_themes[i]; Config_DynamicEnum_AddValue( bar_look_option, theme->name, theme->name_gs); } } for (int32_t i = 0; i < UI_BAR_NUMBER_OF; i++) { const M_BAR_COLOR_SELECT *const select = &m_BarColorSelect[i]; M_SeedDynamicEnumBarColors( Config_GetOption(select->pc_color), UI_BAR_THEME_PC_KIND); M_SeedDynamicEnumBarColors( Config_GetOption(select->ps1_color), UI_BAR_THEME_PS1_KIND); } } static void M_FreeBarThemes(void) { M_ResetDynamicEnumValues(); M_BAR_THEME_LOOKUP *entry = nullptr; M_BAR_THEME_LOOKUP *tmp = nullptr; HASH_ITER(hh, m_Settings.bar_lookup, entry, tmp) { HASH_DEL(m_Settings.bar_lookup, entry); Memory_FreePointer(&entry); } if (m_Settings.bar_themes == nullptr) { return; } for (int32_t i = 0; i < m_Settings.bar_theme_count; i++) { M_BAR_THEME_ENTRY *const theme = &m_Settings.bar_themes[i]; Memory_FreePointer(&theme->name); Memory_FreePointer(&theme->name_gs); M_FreeThemeGroup(&theme->group); } Memory_FreePointer(&m_Settings.bar_themes); m_Settings.bar_theme_count = 0; m_Settings.bar_lookup = nullptr; } static bool M_ReadColorArray( JSON_READ_IO *const io, RGBA_8888 colors[UI_BAR_COLOR_STEPS]) { const int32_t count = JSON_ARRAY_LEN(io); if (count != UI_BAR_COLOR_STEPS) { JSON_ReadIO_SetError( io, "invalid color array (expected %d entries)", UI_BAR_COLOR_STEPS); JSON_FAIL(); } for (int32_t i = 0; i < UI_BAR_COLOR_STEPS; i++) { RGB_888 rgb = {}; JSON_MUST(JSON_READ_A(io, i, &rgb)); colors[i] = Color_RGBToRGBA(rgb); } JSON_FINISH(); } static bool M_LoadThemesPC(JSON_READ_IO *const io, M_THEME_GROUP *const group) { float basic_scale = 1.0f; RGBA_8888 border_light = {}; RGBA_8888 border_dark = {}; JSON_READ_D(io, "scale", &basic_scale, 1.0f); RGB_888 border_light_rgb = {}; JSON_MUST(JSON_READ(io, "border_light", &border_light_rgb)); border_light = Color_RGBToRGBA(border_light_rgb); RGB_888 border_dark_rgb = {}; JSON_MUST(JSON_READ(io, "border_dark", &border_dark_rgb)); border_dark = Color_RGBToRGBA(border_dark_rgb); JSON_MUST(JSON_PUSH(io, "colors")); JSON_OBJECT *const colors_obj = JSON_ReadIO_GetCurrentObject(io); if (colors_obj == nullptr) { JSON_ReadIO_SetError(io, "'colors' must be an object"); JSON_MUST(JSON_POP(io)); JSON_FAIL(); } size_t count = 0; for (JSON_OBJECT_ELEMENT *elem = colors_obj->start; elem != nullptr; elem = elem->next) { count++; } if (count == 0) { JSON_ReadIO_SetError(io, "'colors' cannot be empty"); JSON_MUST(JSON_POP(io)); JSON_FAIL(); } M_FreeThemeGroup(group); group->colors = Memory_Alloc(sizeof(*group->colors) * count); group->color_count = (int32_t)count; group->lookup = nullptr; size_t idx = 0; for (JSON_OBJECT_ELEMENT *elem = colors_obj->start; elem != nullptr; elem = elem->next) { const char *const name = elem->name->string; JSON_MUST(JSON_PUSH(io, name)); group->colors[idx].name = Memory_DupStr(name); M_THEME_LOOKUP *existing = nullptr; HASH_FIND_STR(group->lookup, group->colors[idx].name, existing); if (existing != nullptr) { JSON_ReadIO_SetError(io, "duplicate color '%s'", name); JSON_MUST(JSON_POP(io)); JSON_MUST(JSON_POP(io)); JSON_FAIL(); } M_THEME_LOOKUP *const entry = Memory_Alloc(sizeof(*entry)); entry->name = group->colors[idx].name; entry->index = (int32_t)idx; HASH_ADD_KEYPTR( hh, group->lookup, entry->name, strlen(entry->name), entry); UI_BAR_THEME *const theme = &group->colors[idx].theme; *theme = (UI_BAR_THEME) { .kind = UI_BAR_THEME_PC_KIND, .basic_scale = basic_scale, .border_light = border_light, .border_dark = border_dark, }; JSON_MUST(M_ReadColorArray(io, theme->ramp)); JSON_MUST(JSON_POP(io)); idx++; } JSON_MUST(JSON_POP(io)); JSON_FINISH(); } static bool M_LoadThemesPS1(JSON_READ_IO *const io, M_THEME_GROUP *const group) { float basic_scale = 1.0f; JSON_READ_D(io, "scale", &basic_scale, 1.0f); RGB_888 border_tl_rgb = {}; RGB_888 border_tr_rgb = {}; RGB_888 border_bl_rgb = {}; RGB_888 border_br_rgb = {}; JSON_MUST(JSON_READ(io, "border_tl", &border_tl_rgb)); JSON_MUST(JSON_READ(io, "border_tr", &border_tr_rgb)); JSON_MUST(JSON_READ(io, "border_bl", &border_bl_rgb)); JSON_MUST(JSON_READ(io, "border_br", &border_br_rgb)); const RGBA_8888 border_tl = Color_RGBToRGBA(border_tl_rgb); const RGBA_8888 border_tr = Color_RGBToRGBA(border_tr_rgb); const RGBA_8888 border_bl = Color_RGBToRGBA(border_bl_rgb); const RGBA_8888 border_br = Color_RGBToRGBA(border_br_rgb); JSON_MUST(JSON_PUSH(io, "colors")); JSON_OBJECT *const colors_obj = JSON_ReadIO_GetCurrentObject(io); if (colors_obj == nullptr) { JSON_ReadIO_SetError(io, "'colors' must be an object"); JSON_MUST(JSON_POP(io)); JSON_FAIL(); } size_t count = 0; for (JSON_OBJECT_ELEMENT *elem = colors_obj->start; elem != nullptr; elem = elem->next) { count++; } if (count == 0) { JSON_ReadIO_SetError(io, "'colors' cannot be empty"); JSON_MUST(JSON_POP(io)); JSON_FAIL(); } M_FreeThemeGroup(group); group->colors = Memory_Alloc(sizeof(*group->colors) * count); group->color_count = (int32_t)count; group->lookup = nullptr; size_t idx = 0; for (JSON_OBJECT_ELEMENT *elem = colors_obj->start; elem != nullptr; elem = elem->next) { const char *const name = elem->name->string; JSON_MUST(JSON_PUSH(io, name)); const int32_t ramps_count = JSON_ARRAY_LEN(io); if (ramps_count != 2) { JSON_ReadIO_SetError( io, "invalid '%s' color definition (expected 2 arrays)", name); JSON_MUST(JSON_POP(io)); JSON_MUST(JSON_POP(io)); JSON_FAIL(); } group->colors[idx].name = Memory_DupStr(name); M_THEME_LOOKUP *existing = nullptr; HASH_FIND_STR(group->lookup, group->colors[idx].name, existing); if (existing != nullptr) { JSON_ReadIO_SetError(io, "duplicate color '%s'", name); JSON_MUST(JSON_POP(io)); JSON_MUST(JSON_POP(io)); JSON_FAIL(); } M_THEME_LOOKUP *const entry = Memory_Alloc(sizeof(*entry)); entry->name = group->colors[idx].name; entry->index = (int32_t)idx; HASH_ADD_KEYPTR( hh, group->lookup, entry->name, strlen(entry->name), entry); UI_BAR_THEME *const theme = &group->colors[idx].theme; *theme = (UI_BAR_THEME) { .kind = UI_BAR_THEME_PS1_KIND, .basic_scale = basic_scale, .border_tl = border_tl, .border_tr = border_tr, .border_bl = border_bl, .border_br = border_br, }; JSON_MUST(JSON_PUSH_INDEX(io, 0)); JSON_MUST(M_ReadColorArray(io, theme->ramp_left)); JSON_MUST(JSON_POP(io)); JSON_MUST(JSON_PUSH_INDEX(io, 1)); JSON_MUST(M_ReadColorArray(io, theme->ramp_right)); JSON_MUST(JSON_POP(io)); JSON_MUST(JSON_POP(io)); idx++; } JSON_MUST(JSON_POP(io)); JSON_FINISH(); } static bool M_LoadTheme(JSON_READ_IO *const io, M_BAR_THEME_ENTRY *const theme) { const char *name_gs = nullptr; JSON_MUST(JSON_READ(io, "name_gs", &name_gs)); theme->name_gs = Memory_DupStr(name_gs); const char *style = nullptr; JSON_MUST(JSON_READ(io, "style", &style)); if (String_Equivalent(style, "pc")) { theme->kind = UI_BAR_THEME_PC_KIND; JSON_MUST(M_LoadThemesPC(io, &theme->group)); } else if (String_Equivalent(style, "ps1")) { theme->kind = UI_BAR_THEME_PS1_KIND; JSON_MUST(M_LoadThemesPS1(io, &theme->group)); } else { JSON_ReadIO_SetError(io, "invalid 'style' value '%s'", style); JSON_FAIL(); } JSON_FINISH(); } static bool M_LoadBarThemes(JSON_READ_IO *const io) { JSON_OBJECT *const root_obj = JSON_ReadIO_GetCurrentObject(io); if (root_obj == nullptr) { JSON_ReadIO_SetError( io, "invalid ui settings file: root must be object"); JSON_FAIL(); } size_t theme_count = 0; for (JSON_OBJECT_ELEMENT *elem = root_obj->start; elem != nullptr; elem = elem->next) { theme_count++; } if (theme_count == 0) { JSON_ReadIO_SetError(io, "ui settings file has no bar themes"); JSON_FAIL(); } m_Settings.bar_themes = Memory_Alloc(sizeof(*m_Settings.bar_themes) * theme_count); m_Settings.bar_theme_count = (int32_t)theme_count; m_Settings.bar_lookup = nullptr; size_t idx = 0; for (JSON_OBJECT_ELEMENT *elem = root_obj->start; elem != nullptr; elem = elem->next) { const char *const theme_name = elem->name->string; JSON_MUST(JSON_PUSH(io, theme_name)); M_BAR_THEME_ENTRY *const theme = &m_Settings.bar_themes[idx]; theme->name = Memory_DupStr(theme_name); theme->name_gs = nullptr; theme->kind = UI_BAR_THEME_PC_KIND; theme->group = (M_THEME_GROUP) {}; M_BAR_THEME_LOOKUP *existing = nullptr; HASH_FIND_STR(m_Settings.bar_lookup, theme->name, existing); if (existing != nullptr) { JSON_ReadIO_SetError(io, "duplicate theme '%s'", theme_name); JSON_MUST(JSON_POP(io)); JSON_FAIL(); } JSON_MUST(M_LoadTheme(io, theme)); M_BAR_THEME_LOOKUP *const entry = Memory_Alloc(sizeof(*entry)); entry->name = theme->name; entry->index = (int32_t)idx; HASH_ADD_KEYPTR( hh, m_Settings.bar_lookup, entry->name, strlen(entry->name), entry); JSON_MUST(JSON_POP(io)); idx++; } JSON_FINISH(); } static M_BAR_THEME_ENTRY *M_FindBarThemeByName(const char *const name) { if (name == nullptr) { return nullptr; } M_BAR_THEME_LOOKUP *entry = nullptr; HASH_FIND_STR(m_Settings.bar_lookup, name, entry); if (entry == nullptr) { return nullptr; } return &m_Settings.bar_themes[entry->index]; } static M_BAR_THEME_ENTRY *M_GetCurrentBarTheme(void) { M_BAR_THEME_ENTRY *theme = M_FindBarThemeByName(g_Config.ui.bar_look); if (theme != nullptr) { return theme; } if (m_Settings.bar_theme_count <= 0) { return nullptr; } return &m_Settings.bar_themes[0]; } static const M_THEME_GROUP *M_GetCurrentBarGroup(void) { M_BAR_THEME_ENTRY *const theme = M_GetCurrentBarTheme(); if (theme == nullptr) { return nullptr; } return &theme->group; } static bool M_LoadMenuColorsPC( JSON_READ_IO *const io, UI_MENU_COLORS_PC *const c) { JSON_MUST(JSON_PUSH(io, "background")); JSON_MUST(JSON_READ_A(io, 0, &c->background[0])); JSON_MUST(JSON_READ_A(io, 1, &c->background[1])); JSON_MUST(JSON_POP(io)); JSON_MUST(JSON_PUSH(io, "background_heavy")); JSON_MUST(JSON_READ_A(io, 0, &c->background_heavy[0])); JSON_MUST(JSON_READ_A(io, 1, &c->background_heavy[1])); JSON_MUST(JSON_POP(io)); JSON_MUST(JSON_READ(io, "outline_light", &c->outline_light)); JSON_MUST(JSON_READ(io, "outline_dark", &c->outline_dark)); JSON_FINISH(); } static bool M_LoadMenuColorsPS1( JSON_READ_IO *const io, UI_MENU_COLORS_PS1 *const c) { JSON_MUST(JSON_READ(io, "background_edge", &c->background_edge)); JSON_MUST(JSON_READ(io, "background_center", &c->background_center)); JSON_MUST( JSON_READ(io, "background_heavy_edge", &c->background_heavy_edge)); JSON_MUST( JSON_READ(io, "background_heavy_center", &c->background_heavy_center)); JSON_MUST(JSON_READ(io, "heading_edge", &c->heading_edge)); JSON_MUST(JSON_READ(io, "heading_center", &c->heading_center)); JSON_MUST(JSON_READ(io, "requested_edge", &c->requested_edge)); JSON_MUST(JSON_READ(io, "requested_center", &c->requested_center)); JSON_MUST(JSON_READ(io, "requested_outline_ch", &c->requested_outline_ch)); JSON_MUST(JSON_READ(io, "requested_outline_cv", &c->requested_outline_cv)); JSON_MUST( JSON_READ(io, "requested_outline_edge", &c->requested_outline_edge)); JSON_MUST(JSON_READ(io, "outline_tl", &c->outline_tl)); JSON_MUST(JSON_READ(io, "outline_tr", &c->outline_tr)); JSON_MUST(JSON_READ(io, "outline_bl", &c->outline_bl)); JSON_MUST(JSON_READ(io, "outline_br", &c->outline_br)); JSON_MUST(JSON_READ(io, "heading_outline", &c->heading_outline)); JSON_FINISH(); } static bool M_LoadMenuColors(JSON_READ_IO *const io) { static const char *const tr_keys[] = { "tr1", "tr2", "tr3" }; for (int32_t i = 0; i < 3; i++) { JSON_MUST(JSON_PUSH(io, tr_keys[i])); JSON_MUST(JSON_PUSH(io, "pc")); JSON_MUST(M_LoadMenuColorsPC(io, &m_MenuColorsPC[i])); JSON_MUST(JSON_POP(io)); JSON_MUST(JSON_PUSH(io, "ps1")); JSON_MUST(M_LoadMenuColorsPS1(io, &m_MenuColorsPS1[i])); JSON_MUST(JSON_POP(io)); JSON_MUST(JSON_POP(io)); } JSON_FINISH(); } void UI_Settings_LoadFromFile(const char *const path) { JSON_VALUE *const root = JSONFile_ReadEx(path, true); JSON_READ_IO *const io = JSON_ReadIO_Create(root, 0, path); M_FreeBarThemes(); if (!JSON_PUSH(io, "bars") || !M_LoadBarThemes(io) || !JSON_POP(io)) { M_ExitWithJSONError(path, io); } if (!JSON_PUSH(io, "ui") || !M_LoadMenuColors(io) || !JSON_POP(io)) { M_ExitWithJSONError(path, io); } M_SeedDynamicEnumValues(); JSON_ReadIO_Destroy(io); JSON_ValueFree(root); } __attribute__((destructor)) static void M_Shutdown(void) { M_FreeBarThemes(); } static const char *M_GetBarColorName(const UI_BAR_TYPE type) { if (type < 0 || type >= UI_BAR_NUMBER_OF) { return "gold"; } const M_BAR_THEME_ENTRY *const theme = M_GetCurrentBarTheme(); const bool use_ps1 = theme != nullptr && theme->kind == UI_BAR_THEME_PS1_KIND; const M_BAR_COLOR_SELECT *const select = &m_BarColorSelect[type]; const char *value = nullptr; if (use_ps1 && select->ps1_color != nullptr) { value = *select->ps1_color; } else if (!use_ps1 && select->pc_color != nullptr) { value = *select->pc_color; } return value; } static const UI_BAR_THEME *M_FindThemeByName( const M_THEME_GROUP *const group, const char *const name) { if (group == nullptr || group->colors == nullptr || group->color_count <= 0 || name == nullptr) { return nullptr; } M_THEME_LOOKUP *entry = nullptr; HASH_FIND_STR(group->lookup, name, entry); if (entry != nullptr) { return &group->colors[entry->index].theme; } return nullptr; } bool UI_Settings_IsCurrentBarLookPS1(void) { const M_BAR_THEME_ENTRY *const theme = M_GetCurrentBarTheme(); return theme != nullptr && theme->kind == UI_BAR_THEME_PS1_KIND; } const UI_BAR_THEME *UI_Settings_GetBarTheme(const UI_BAR_TYPE type) { if (type < 0 || type >= UI_BAR_NUMBER_OF) { return nullptr; } const M_THEME_GROUP *const group = M_GetCurrentBarGroup(); if (group == nullptr || group->color_count <= 0) { return nullptr; } const char *const name = M_GetBarColorName(type); const UI_BAR_THEME *theme = M_FindThemeByName(group, name); if (theme != nullptr) { return theme; } return &group->colors[0].theme; } const UI_MENU_COLORS_PC *UI_Settings_GetMenuColorsPC(void) { return &m_MenuColorsPC[g_TRVersion - 1]; } const UI_MENU_COLORS_PS1 *UI_Settings_GetMenuColorsPS1(void) { return &m_MenuColorsPS1[g_TRVersion - 1]; } ================================================ FILE: src/trx/game/ui/settings.h ================================================ #pragma once #include #include #include #define UI_BAR_COLOR_STEPS 5 typedef enum { UI_BAR_LARA_HP, UI_BAR_LARA_HP_POISON, UI_BAR_LARA_AIR, UI_BAR_LARA_STAMINA, UI_BAR_LARA_EXPOSURE, UI_BAR_ENEMY_HP, UI_BAR_ALLY_HP, UI_BAR_PROGRESS, UI_BAR_NUMBER_OF, } UI_BAR_TYPE; typedef enum { UI_BAR_THEME_PC_KIND, UI_BAR_THEME_PS1_KIND, } UI_BAR_THEME_KIND; typedef struct { UI_BAR_THEME_KIND kind; float basic_scale; RGBA_8888 border_light; RGBA_8888 border_dark; RGBA_8888 border_tl; RGBA_8888 border_tr; RGBA_8888 border_bl; RGBA_8888 border_br; RGBA_8888 ramp[UI_BAR_COLOR_STEPS]; RGBA_8888 ramp_left[UI_BAR_COLOR_STEPS]; RGBA_8888 ramp_right[UI_BAR_COLOR_STEPS]; } UI_BAR_THEME; typedef struct { RGBA_8888 background[2]; // TS_BACKGROUND: [top, bottom] RGBA_8888 background_heavy[2]; // TS_BACKGROUND_HEAVY: [top, bottom] RGBA_8888 outline_light; RGBA_8888 outline_dark; } UI_MENU_COLORS_PC; typedef struct { RGBA_8888 background_edge; RGBA_8888 background_center; RGBA_8888 background_heavy_edge; RGBA_8888 background_heavy_center; RGBA_8888 heading_edge; RGBA_8888 heading_center; RGBA_8888 requested_edge; RGBA_8888 requested_center; RGBA_8888 requested_outline_ch; RGBA_8888 requested_outline_cv; RGBA_8888 requested_outline_edge; RGBA_8888 outline_tl; RGBA_8888 outline_tr; RGBA_8888 outline_bl; RGBA_8888 outline_br; RGBA_8888 heading_outline; } UI_MENU_COLORS_PS1; void UI_Settings_LoadFromFile(const char *path); const UI_BAR_THEME *UI_Settings_GetBarTheme(UI_BAR_TYPE type); bool UI_Settings_IsCurrentBarLookPS1(void); const UI_MENU_COLORS_PC *UI_Settings_GetMenuColorsPC(void); const UI_MENU_COLORS_PS1 *UI_Settings_GetMenuColorsPS1(void); ================================================ FILE: src/trx/game/ui/text.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define M_LETTER_SPACING 0.5f #define M_WORD_SPACING 6.0f #define M_DIM_COLOR 12 #define M_MAX_COLOR 13 typedef enum { M_FONT_DEFAULT = 0, M_FONT_SMALL = 1, M_FONT_COUNT, } M_FONT; typedef enum { // A text character. GLYPH_TEXT, // An icon. GLYPH_ICON, // Spacing between words. GLYPH_SPACE, // Line break. GLYPH_NEW_LINE, // Marker used in the examine item dialog and others to force a new page. GLYPH_NEW_PAGE, // Icon for collectible secrets, taking the sprite from O_SECRET GLYPH_SECRET, // Icon requesting translators to verify AI-translated text. GLYPH_REVIEW_MARKER, // Marker that toggles the visibility of the following text. GLYPH_VISIBILITY_MARKER, // Marker that toggles the dimming of the following text. GLYPH_DIM_MARKER, // Marker that changes the color of the following text. GLYPH_COLOR_MARKER, // Marker that changes the font of the following text. // - mesh_idx = 0: default font (O_ALPHABET). // - mesh_idx = 1: default font (O_ALPHABET_SMALL). GLYPH_FONT_MARKER, // Glyph that dynamically expands a key role to its current key icon. GLYPH_INPUT, } M_GLYPH_ROLE; typedef struct { const char *text; M_GLYPH_ROLE role; int32_t width[M_FONT_COUNT]; union { int32_t mesh_idx; INPUT_ROLE input_role; // for role == GLYPH_INPUT }; } M_GLYPH_INFO; typedef struct { M_GLYPH_INFO *glyph; UT_hash_handle hh; } M_GLYPH_MAP_ENTRY; typedef struct { char *text; const M_GLYPH_INFO **glyphs; size_t glyph_count; UT_hash_handle hh; } M_TEXT_MAP_ENTRY; static M_GLYPH_INFO m_Glyphs[] = { #define X_GLYPH_DEFINE(text_, role_, mesh_idx_) \ { .text = text_, .role = role_, .mesh_idx = mesh_idx_ }, #include { .text = nullptr }, // guard }; static M_GLYPH_MAP_ENTRY *m_GlyphMap = nullptr; static M_TEXT_MAP_ENTRY *m_TextMap = nullptr; OBJECT_ID m_FontObjects[M_FONT_COUNT] = { [M_FONT_DEFAULT] = O_ALPHABET, [M_FONT_SMALL] = O_ALPHABET_SMALL, }; static RGB_888 m_ColorLight[M_MAX_COLOR] = { // clang-format off [0] = { 0xFF, 0xFF, 0xFF }, [1] = { 0xB0, 0xB0, 0x00 }, [2] = { 0xA0, 0xA0, 0xA0 }, [3] = { 0xFF, 0x60, 0x60 }, [4] = { 0x80, 0x80, 0xFF }, [5] = { 0xC0, 0x80, 0x40 }, [6] = { 0xB6, 0xD1, 0x64 }, [7] = { 0xC0, 0xFF, 0xC0 }, [8] = { 0xFF, 0xFF, 0xFF }, [9] = { 0xFF, 0x00, 0xFF }, [10] = { 0xFF, 0x00, 0xFF }, [11] = { 0xFF, 0x00, 0xFF }, [12] = { 0x80, 0x80, 0x80 }, // clang-format on }; static RGB_888 m_ColorDark[M_MAX_COLOR] = { // clang-format off [0] = { 0x80, 0x80, 0x80 }, [1] = { 0x50, 0x50, 0x00 }, [2] = { 0x18, 0x18, 0x18 }, [3] = { 0x18, 0x00, 0x00 }, [4] = { 0x00, 0x00, 0x18 }, [5] = { 0x40, 0x10, 0x00 }, [6] = { 0xB6, 0x20, 0x13 }, [7] = { 0xC0, 0xFF, 0xC0 }, [8] = { 0xFF, 0xFF, 0xFF }, [9] = { 0x3F, 0x00, 0x3F }, [10] = { 0x3F, 0x00, 0x3F }, [11] = { 0x3F, 0x00, 0x3F }, [12] = { 0x80, 0x80, 0x80 }, // clang-format on }; static RGBA_F m_TextColor[M_MAX_COLOR][4] = {}; static float M_ScaleScreen(const float value) { return UI_Scaler_Calc(value, UI_SCALER_TARGET_TEXT); } static float M_ScaleNeutral(const float value) { return value * g_Config.ui.text_scale; } static RGBA_F M_ToRGBA_F(const RGB_888 color) { return (RGBA_F) { .r = color.r / 255.0f, .g = color.g / 255.0f, .b = color.b / 255.0f, .a = 1.0f, }; } static int32_t M_HasGlyph(const M_FONT font, const M_GLYPH_INFO *const glyph) { return glyph->width[font] > 0; } static int32_t M_GetGlyphWidth( const M_FONT font, const M_GLYPH_INFO *const glyph) { // Non-breaking space if (strcmp(glyph->text, " ") == 0) { return M_WORD_SPACING; } if (glyph->role == GLYPH_SECRET) { return 16; } if (glyph->mesh_idx != -1 && (glyph->role == GLYPH_TEXT || glyph->role == GLYPH_ICON || glyph->role == GLYPH_REVIEW_MARKER)) { const OBJECT *const object = Object_Get(m_FontObjects[font]); if (!object->loaded) { return -1; } if (glyph->mesh_idx >= ABS(object->mesh_count)) { return -1; } const SPRITE_TEXTURE *const sprite = Output_GetSpriteTexture(object->mesh_idx + glyph->mesh_idx); if (sprite == nullptr) { return -1; } if (sprite->x1 - sprite->x0 == 0 && sprite->width / 255 == 1) { // Just a placeholder glyph necessary for indexing of other glyphs return -1; } return sprite->width / 255; } return 0; } static const M_GLYPH_INFO **M_Decompose( const char *const content, size_t *const out_glyph_count) { // Count number of characters size_t glyph_count = 0; const char *content_ptr = content; while (*content_ptr != '\0') { const size_t glyph_size = String_GetCharByteSize(content_ptr); content_ptr += glyph_size; glyph_count++; } // Assign glyphs using hash table const M_GLYPH_INFO **glyphs = Memory_Alloc((glyph_count + 1) * sizeof(M_GLYPH_INFO *)); content_ptr = content; const M_GLYPH_INFO **glyph_ptr = glyphs; while (*content_ptr != '\0') { const size_t glyph_size = String_GetCharByteSize(content_ptr); const char *const key_buf = String_FormatStatic("%.*s", (int)glyph_size, content_ptr); M_GLYPH_MAP_ENTRY *entry; HASH_FIND_STR(m_GlyphMap, key_buf, entry); if (entry != nullptr) { *glyph_ptr++ = entry->glyph; } else { LOG_WARNING("Unknown glyph: %s", key_buf); glyph_count--; } content_ptr += glyph_size; } if (out_glyph_count != nullptr) { *out_glyph_count = glyph_count; } // guard *glyph_ptr++ = nullptr; return glyphs; } static const M_GLYPH_INFO **M_DecomposeWithCache( const char *const content, size_t *const out_glyph_count) { M_TEXT_MAP_ENTRY *entry; HASH_FIND_STR(m_TextMap, content, entry); if (entry == nullptr) { entry = Memory_Alloc(sizeof(M_TEXT_MAP_ENTRY)); entry->text = Memory_DupStr(content); entry->glyphs = M_Decompose(content, &entry->glyph_count); HASH_ADD_STR(m_TextMap, text, entry); } if (out_glyph_count != nullptr) { *out_glyph_count = entry->glyph_count; } return entry->glyphs; } // Replace input placeholder glyph with the actual keyboard glyph for the // current binding static const M_GLYPH_INFO *M_GetResolvedGlyph(const M_GLYPH_INFO *glyph) { if (glyph->role != GLYPH_INPUT) { return glyph; } const char *const key_name = Input_GetKeyName( g_Config.input.backend, g_Config.input.layout[g_Config.input.backend], glyph->input_role, 0); // NOTE: this aliasing approach assumes that Input_GetKeyName returns // text that resolves to a single glyph. M_GLYPH_MAP_ENTRY *entry = nullptr; if (key_name != nullptr) { HASH_FIND_STR(m_GlyphMap, key_name, entry); } if (entry == nullptr) { HASH_FIND_STR(m_GlyphMap, "?", entry); } return entry != nullptr ? entry->glyph : nullptr; } static int32_t M_DetectBulletIndent( const M_GLYPH_INFO **glyphs, const size_t glyph_count, const size_t idx) { size_t scan = idx; int32_t leading_spaces = 0; while (scan < glyph_count && glyphs[scan]->role == GLYPH_SPACE) { leading_spaces++; scan++; } if (scan + 1 < glyph_count && glyphs[scan]->role == GLYPH_TEXT && glyphs[scan]->text[0] == '-' && glyphs[scan]->text[1] == '\0' && glyphs[scan + 1]->role == GLYPH_SPACE) { return leading_spaces + 2; } return 0; } static void M_EmitIndent( char *const dst, size_t *const out_len, const int32_t indent, const float space_width, float *const cur_width) { for (int32_t s = 0; s < indent; s++) { if (dst != nullptr) { dst[*out_len] = ' '; } (*out_len)++; } *cur_width += indent * space_width; } static void M_EmitNewline( char *const dst, size_t *const out_len, const int32_t indent, const float space_width, float *const cur_width) { if (dst != nullptr) { dst[*out_len] = '\n'; } (*out_len)++; *cur_width = 0.0f; if (indent > 0) { M_EmitIndent(dst, out_len, indent, space_width, cur_width); } } static size_t M_WordWrap( const M_GLYPH_INFO **glyphs, const size_t glyph_count, const float scale_f, const float max_width, char *const dst) { size_t out_len = 0; float cur_width = 0.0f; int32_t bullet_indent = 0; const float space_width = M_WORD_SPACING * scale_f; #define L_CONCAT_CHAR(part) \ if (dst != nullptr) { \ dst[out_len] = part; \ } \ out_len++; #define L_CONCAT_STR(part) \ if (dst != nullptr) { \ strcpy(dst + out_len, part); \ } \ out_len += strlen(part); M_FONT current_font = M_FONT_DEFAULT; // Iterate glyphs for wrapping for (size_t i = 0; i < glyph_count; i++) { const M_GLYPH_INFO *const glyph = M_GetResolvedGlyph(glyphs[i]); if (glyph == nullptr) { continue; } if (cur_width == 0.0f && bullet_indent == 0) { bullet_indent = M_DetectBulletIndent(glyphs, glyph_count, i); } if (glyph->role == GLYPH_FONT_MARKER) { current_font = glyph->mesh_idx; } else if (glyph->role == GLYPH_NEW_LINE) { L_CONCAT_CHAR('\n') cur_width = 0.0f; bullet_indent = 0; } else if (glyph->role == GLYPH_NEW_PAGE) { L_CONCAT_CHAR('\f') cur_width = 0.0f; bullet_indent = 0; } else if (glyph->role == GLYPH_SPACE) { const float w = M_WORD_SPACING * scale_f; if (cur_width + w > max_width) { M_EmitNewline( dst, &out_len, bullet_indent, space_width, &cur_width); } else { L_CONCAT_CHAR(' ') cur_width += w; } } else if ( glyph->role == GLYPH_REVIEW_MARKER && !g_Config.debug.enable_review_markers) { continue; } else { // Gather next word glyphs size_t word_len = 0; for (size_t j = i; j < glyph_count; j++) { if (glyphs[i + word_len]->role == GLYPH_SPACE || glyphs[i + word_len]->role == GLYPH_NEW_LINE || glyphs[i + word_len]->role == GLYPH_NEW_PAGE) { break; } word_len++; } // Compute width (sum widths + spacing) float word_width = 0.0f; for (size_t j = i; j < i + word_len; j++) { word_width += M_LETTER_SPACING; word_width += glyphs[j]->width[current_font]; } if (word_width > 0) { word_width -= M_LETTER_SPACING; } word_width *= scale_f; // Wrap line if needed if (cur_width + word_width > max_width) { if (cur_width > 0.0f) { M_EmitNewline( dst, &out_len, bullet_indent, space_width, &cur_width); } // Break word if longer than line if (word_width > max_width) { for (size_t j = i; j < i + word_len; j++) { const M_GLYPH_INFO *const next_glyph = glyphs[j]; const float glyph_width = (next_glyph->width[current_font] + M_LETTER_SPACING) * scale_f; if (cur_width + glyph_width > max_width) { M_EmitNewline( dst, &out_len, bullet_indent, space_width, &cur_width); } L_CONCAT_STR(next_glyph->text) cur_width += glyph_width; } } else { for (size_t j = i; j < i + word_len; j++) { const M_GLYPH_INFO *const next_glyph = glyphs[j]; L_CONCAT_STR(next_glyph->text) } cur_width = word_width; } } else { // Copy word as is for (size_t j = i; j < i + word_len; j++) { const M_GLYPH_INFO *const next_glyph = glyphs[j]; L_CONCAT_STR(next_glyph->text) } cur_width += word_width; } // Skip forward the characters, respecting the default loop // accumulator i += word_len - 1; } } L_CONCAT_CHAR('\0') #undef L_CONCAT_CHAR #undef L_CONCAT_STR return out_len; } static void M_Process( const char *const text, float *const out_w, float *const out_h, const UI_TEXT_SETTINGS settings, const float base_x, const float base_y, float (*const scale_func)(float), void (*const draw_func)( int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, const RGBA_F[4])) { if (text == nullptr) { return; } const M_GLYPH_INFO **glyphs = M_DecomposeWithCache(text, nullptr); ASSERT(glyphs != nullptr); const float scale = scale_func(UI_TEXT_BASE_SCALE * settings.scale); float x = scale_func(base_x / g_Config.ui.text_scale); float y = scale_func( base_y / g_Config.ui.text_scale + settings.scale * UI_TEXT_HEIGHT); int32_t z = settings.z; float max_width = 0.0f; const float start_x = x; M_FONT current_font = M_FONT_DEFAULT; int32_t color_idx = 0; int32_t prev_color_idx = color_idx; bool visible = true; const M_GLYPH_INFO **glyph_ptr = glyphs; while (*glyph_ptr != nullptr) { const M_GLYPH_INFO *const glyph = M_GetResolvedGlyph(*glyph_ptr); if (glyph == nullptr) { goto loop_end; } if (glyph->role == GLYPH_REVIEW_MARKER && !g_Config.debug.enable_review_markers) { goto loop_end; } if (glyph->role == GLYPH_VISIBILITY_MARKER) { visible = glyph->mesh_idx; goto loop_end; } if (glyph->role == GLYPH_FONT_MARKER) { current_font = glyph->mesh_idx; goto loop_end; } if (glyph->role == GLYPH_DIM_MARKER) { if (glyph->mesh_idx != 0) { prev_color_idx = color_idx; color_idx = M_DIM_COLOR; } else { color_idx = prev_color_idx; } goto loop_end; } if (glyph->role == GLYPH_COLOR_MARKER) { if (glyph->mesh_idx != -1) { prev_color_idx = color_idx; color_idx = glyph->mesh_idx; } else { color_idx = prev_color_idx; } goto loop_end; } if (glyph->role == GLYPH_NEW_LINE || glyph->role == GLYPH_NEW_PAGE) { y += UI_TEXT_HEIGHT * scale / UI_TEXT_BASE_SCALE; x = start_x; goto loop_end; } if (glyph->role == GLYPH_SPACE) { if (glyph_ptr[1] == nullptr || (glyph_ptr[1]->role != GLYPH_NEW_LINE && glyph_ptr[1]->role != GLYPH_NEW_PAGE)) { x += M_WORD_SPACING * scale / UI_TEXT_BASE_SCALE; } goto loop_end; } if (glyph->role == GLYPH_SECRET) { const int16_t sprite_idx = Object_Get(O_SECRET_1 + glyph->mesh_idx)->mesh_idx; const SPRITE_TEXTURE *const sprite = Output_GetSpriteTexture(sprite_idx); const float input_scale_h = settings.scale / (sprite->x1 - sprite->x0); const float input_scale_v = settings.scale / (sprite->y1 - sprite->y0); const float input_scale = MIN(input_scale_h, input_scale_v); const float output_scale = scale_func( UI_TEXT_BASE_SCALE * glyph->width[current_font] * input_scale); if (visible && draw_func != nullptr) { draw_func( x + scale_func(10), y, z, output_scale, output_scale, sprite_idx, m_TextColor[color_idx]); } x += glyph->width[current_font] * scale / UI_TEXT_BASE_SCALE; goto loop_end; } M_FONT glyph_font = current_font; if (glyph_font == M_FONT_SMALL && !M_HasGlyph(glyph_font, glyph)) { glyph_font = M_FONT_DEFAULT; } float spacing = glyph->width[glyph_font]; if (glyph_ptr[1] != nullptr && glyph_ptr[1]->role != GLYPH_NEW_LINE && glyph_ptr[1]->role != GLYPH_NEW_PAGE) { spacing += M_LETTER_SPACING; } if (glyph->role == GLYPH_TEXT && glyph->mesh_idx < 0) { // Non-breaking space or other non-rendered text glyphs. x += spacing * scale / UI_TEXT_BASE_SCALE; goto loop_end; } if (visible && draw_func != nullptr) { const OBJECT *object = Object_Get(m_FontObjects[glyph_font]); draw_func( x, y, z, scale, scale, object->mesh_idx + glyph->mesh_idx, m_TextColor[color_idx]); } x += spacing * scale / UI_TEXT_BASE_SCALE; loop_end: max_width = MAX(max_width, x); glyph_ptr++; } if (out_w != nullptr) { *out_w = max_width; } if (out_h != nullptr) { *out_h = y; } } void UI_InitText(void) { // Convert the linear array coming from the .def macros to a hash lookup // table for faster text-to-glyph resolution. for (M_GLYPH_INFO *glyph_ptr = m_Glyphs; glyph_ptr->text != nullptr; glyph_ptr++) { // mark static glyphs as non-input M_GLYPH_MAP_ENTRY *const hash_entry = Memory_Alloc(sizeof(*hash_entry)); hash_entry->glyph = glyph_ptr; HASH_ADD_KEYPTR( hh, m_GlyphMap, glyph_ptr->text, strlen(glyph_ptr->text), hash_entry); } // Create dynamic glyphs for "{key }" tokens; resolution happens when // drawing/wrapping for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) { const char *role_str = EnumMap_ToString(ENUM_MAP_NAME(INPUT_ROLE), role); if (role_str == nullptr || *role_str == '\0') { continue; } M_GLYPH_INFO *input_glyph = Memory_Alloc(sizeof(*input_glyph)); input_glyph->text = String_Format("\\{input %s}", role_str); input_glyph->role = GLYPH_INPUT; input_glyph->input_role = role; for (M_FONT font = 0; font < M_FONT_COUNT; font++) { input_glyph->width[font] = 0; } M_GLYPH_MAP_ENTRY *entry = Memory_Alloc(sizeof(*entry)); entry->glyph = input_glyph; HASH_ADD_KEYPTR( hh, m_GlyphMap, input_glyph->text, strlen(input_glyph->text), entry); } } void UI_LoadText(void) { for (int32_t i = 0; i < M_MAX_COLOR; i++) { m_TextColor[i][0] = M_ToRGBA_F(m_ColorLight[i]); m_TextColor[i][1] = M_ToRGBA_F(m_ColorLight[i]); if (g_TRVersion == 3) { m_TextColor[i][2] = M_ToRGBA_F(m_ColorDark[i]); m_TextColor[i][3] = M_ToRGBA_F(m_ColorDark[i]); } else { m_TextColor[i][2] = M_ToRGBA_F(m_ColorLight[i]); m_TextColor[i][3] = M_ToRGBA_F(m_ColorLight[i]); } } for (M_FONT font = 0; font < M_FONT_COUNT; font++) { for (M_GLYPH_INFO *glyph_ptr = m_Glyphs; glyph_ptr->text != nullptr; glyph_ptr++) { glyph_ptr->width[font] = M_GetGlyphWidth(font, glyph_ptr); } } } void UI_ShutdownText(void) { { M_GLYPH_MAP_ENTRY *current, *tmp; HASH_ITER(hh, m_GlyphMap, current, tmp) { if (current->glyph->role == GLYPH_INPUT) { Memory_FreePointer(¤t->glyph->text); Memory_FreePointer(¤t->glyph); } HASH_DEL(m_GlyphMap, current); Memory_Free(current); } } { M_TEXT_MAP_ENTRY *current, *tmp; HASH_ITER(hh, m_TextMap, current, tmp) { Memory_FreePointer(¤t->text); Memory_FreePointer(¤t->glyphs); HASH_DEL(m_TextMap, current); Memory_FreePointer(¤t); } } } void UI_Text_Measure( const char *const text, float *const out_w, float *const out_h, const UI_TEXT_SETTINGS settings) { M_Process( text, out_w, out_h, settings, 0.0f, 0.0f, M_ScaleNeutral, nullptr); } void UI_Text_Draw( const char *const text, const float base_x, const float base_y, const UI_TEXT_SETTINGS settings) { M_Process( text, nullptr, nullptr, settings, base_x, base_y - g_Config.ui.text_scale, M_ScaleScreen, UI_ScheduleDrawScreenSprite); } char *UI_Text_WordWrap( const char *text, const float scale, const float max_width) { if (text == nullptr || max_width <= 0) { return nullptr; } size_t glyph_count = 0; const M_GLYPH_INFO **glyphs = M_DecomposeWithCache(text, &glyph_count); const float scale_f = scale * g_Config.ui.text_scale; size_t len = M_WordWrap(glyphs, glyph_count, scale_f, max_width, nullptr); char *const wrapped_text = Memory_Alloc(len); M_WordWrap(glyphs, glyph_count, scale_f, max_width, wrapped_text); return wrapped_text; } char *UI_Text_FilterGlyphs(const char *const text) { if (text == nullptr) { return nullptr; } const size_t in_len = strlen(text); char *out = Memory_Alloc(in_len + 1); size_t out_len = 0; const char *p = text; while (*p != '\0') { const size_t sz = String_GetCharByteSize(p); const char *const key_buf = String_FormatStatic("%.*s", (int32_t)sz, p); M_GLYPH_MAP_ENTRY *entry = nullptr; HASH_FIND_STR(m_GlyphMap, key_buf, entry); if (entry != nullptr) { memcpy(out + out_len, p, sz); out_len += sz; } p += sz; } out[out_len] = '\0'; return out; } ================================================ FILE: src/trx/game/ui/text.def ================================================ X_GLYPH_DEFINE("\n", GLYPH_NEW_LINE, -1) X_GLYPH_DEFINE("\f", GLYPH_NEW_PAGE, -1) X_GLYPH_DEFINE(" ", GLYPH_SPACE, -1) X_GLYPH_DEFINE(" ", GLYPH_TEXT, -1) X_GLYPH_DEFINE("\\{i}", GLYPH_VISIBILITY_MARKER, 0) X_GLYPH_DEFINE("\\{/i}", GLYPH_VISIBILITY_MARKER, 1) X_GLYPH_DEFINE("\\{dim}", GLYPH_DIM_MARKER, 1) X_GLYPH_DEFINE("\\{/dim}", GLYPH_DIM_MARKER, 0) X_GLYPH_DEFINE("\\{color 0}", GLYPH_COLOR_MARKER, 0) X_GLYPH_DEFINE("\\{color 1}", GLYPH_COLOR_MARKER, 1) X_GLYPH_DEFINE("\\{color 2}", GLYPH_COLOR_MARKER, 2) X_GLYPH_DEFINE("\\{color 3}", GLYPH_COLOR_MARKER, 3) X_GLYPH_DEFINE("\\{color 4}", GLYPH_COLOR_MARKER, 4) X_GLYPH_DEFINE("\\{color 5}", GLYPH_COLOR_MARKER, 5) X_GLYPH_DEFINE("\\{color 6}", GLYPH_COLOR_MARKER, 6) X_GLYPH_DEFINE("\\{color 7}", GLYPH_COLOR_MARKER, 7) X_GLYPH_DEFINE("\\{color 8}", GLYPH_COLOR_MARKER, 8) X_GLYPH_DEFINE("\\{color 9}", GLYPH_COLOR_MARKER, 9) X_GLYPH_DEFINE("\\{color 10}", GLYPH_COLOR_MARKER, 10) X_GLYPH_DEFINE("\\{color 11}", GLYPH_COLOR_MARKER, 11) X_GLYPH_DEFINE("\\{/color}", GLYPH_COLOR_MARKER, -1) X_GLYPH_DEFINE("\\{secret 1}", GLYPH_SECRET, 0) X_GLYPH_DEFINE("\\{secret 2}", GLYPH_SECRET, 1) X_GLYPH_DEFINE("\\{secret 3}", GLYPH_SECRET, 2) X_GLYPH_DEFINE("\\{small}", GLYPH_FONT_MARKER, 1) X_GLYPH_DEFINE("\\{/small}", GLYPH_FONT_MARKER, 0) #include ================================================ FILE: src/trx/game/ui/text.h ================================================ #pragma once #include #include #define UI_TEXT_HEIGHT 15 #define UI_TEXT_BASE_SCALE 0x10000 typedef struct { float scale; int32_t z; } UI_TEXT_SETTINGS; // Initialize and shutdown UI text rendering cache. void UI_InitText(void); void UI_ShutdownText(void); // Observe level load to establish glyph widths. void UI_LoadText(void); // Draw the given text at screen coordinates (x, y) with specified settings. void UI_Text_Draw( const char *text, float x, float y, UI_TEXT_SETTINGS settings); // Measure the width and height of the given text with specified settings. void UI_Text_Measure( const char *text, float *out_w, float *out_h, UI_TEXT_SETTINGS settings); // Wrap a text into multiple lines to fit a specific width in pixels. char *UI_Text_WordWrap( const char *text, const float scale, const float max_width); // Filter out any characters not present in the glyph map. // Returns a newly-allocated string containing only known glyphs. // Caller must free the result with Memory_Free*(). char *UI_Text_FilterGlyphs(const char *text); ================================================ FILE: src/trx/game/ui/text_autogen.def ================================================ // This file is autogenerated. See tools/glyphs/generate_defs for details. X_GLYPH_DEFINE("a", GLYPH_TEXT, 26) X_GLYPH_DEFINE("b", GLYPH_TEXT, 27) X_GLYPH_DEFINE("c", GLYPH_TEXT, 28) X_GLYPH_DEFINE("d", GLYPH_TEXT, 29) X_GLYPH_DEFINE("e", GLYPH_TEXT, 30) X_GLYPH_DEFINE("f", GLYPH_TEXT, 31) X_GLYPH_DEFINE("g", GLYPH_TEXT, 32) X_GLYPH_DEFINE("h", GLYPH_TEXT, 33) X_GLYPH_DEFINE("i", GLYPH_TEXT, 34) X_GLYPH_DEFINE("j", GLYPH_TEXT, 35) X_GLYPH_DEFINE("k", GLYPH_TEXT, 36) X_GLYPH_DEFINE("l", GLYPH_TEXT, 37) X_GLYPH_DEFINE("m", GLYPH_TEXT, 38) X_GLYPH_DEFINE("n", GLYPH_TEXT, 39) X_GLYPH_DEFINE("o", GLYPH_TEXT, 40) X_GLYPH_DEFINE("p", GLYPH_TEXT, 41) X_GLYPH_DEFINE("q", GLYPH_TEXT, 42) X_GLYPH_DEFINE("r", GLYPH_TEXT, 43) X_GLYPH_DEFINE("s", GLYPH_TEXT, 44) X_GLYPH_DEFINE("t", GLYPH_TEXT, 45) X_GLYPH_DEFINE("u", GLYPH_TEXT, 46) X_GLYPH_DEFINE("v", GLYPH_TEXT, 47) X_GLYPH_DEFINE("w", GLYPH_TEXT, 48) X_GLYPH_DEFINE("x", GLYPH_TEXT, 49) X_GLYPH_DEFINE("y", GLYPH_TEXT, 50) X_GLYPH_DEFINE("z", GLYPH_TEXT, 51) X_GLYPH_DEFINE("A", GLYPH_TEXT, 0) X_GLYPH_DEFINE("B", GLYPH_TEXT, 1) X_GLYPH_DEFINE("C", GLYPH_TEXT, 2) X_GLYPH_DEFINE("D", GLYPH_TEXT, 3) X_GLYPH_DEFINE("E", GLYPH_TEXT, 4) X_GLYPH_DEFINE("F", GLYPH_TEXT, 5) X_GLYPH_DEFINE("G", GLYPH_TEXT, 6) X_GLYPH_DEFINE("H", GLYPH_TEXT, 7) X_GLYPH_DEFINE("I", GLYPH_TEXT, 8) X_GLYPH_DEFINE("J", GLYPH_TEXT, 9) X_GLYPH_DEFINE("K", GLYPH_TEXT, 10) X_GLYPH_DEFINE("L", GLYPH_TEXT, 11) X_GLYPH_DEFINE("M", GLYPH_TEXT, 12) X_GLYPH_DEFINE("N", GLYPH_TEXT, 13) X_GLYPH_DEFINE("O", GLYPH_TEXT, 14) X_GLYPH_DEFINE("P", GLYPH_TEXT, 15) X_GLYPH_DEFINE("Q", GLYPH_TEXT, 16) X_GLYPH_DEFINE("R", GLYPH_TEXT, 17) X_GLYPH_DEFINE("S", GLYPH_TEXT, 18) X_GLYPH_DEFINE("T", GLYPH_TEXT, 19) X_GLYPH_DEFINE("U", GLYPH_TEXT, 20) X_GLYPH_DEFINE("V", GLYPH_TEXT, 21) X_GLYPH_DEFINE("W", GLYPH_TEXT, 22) X_GLYPH_DEFINE("X", GLYPH_TEXT, 23) X_GLYPH_DEFINE("Y", GLYPH_TEXT, 24) X_GLYPH_DEFINE("Z", GLYPH_TEXT, 25) X_GLYPH_DEFINE("0", GLYPH_TEXT, 52) X_GLYPH_DEFINE("1", GLYPH_TEXT, 53) X_GLYPH_DEFINE("2", GLYPH_TEXT, 54) X_GLYPH_DEFINE("3", GLYPH_TEXT, 55) X_GLYPH_DEFINE("4", GLYPH_TEXT, 56) X_GLYPH_DEFINE("5", GLYPH_TEXT, 57) X_GLYPH_DEFINE("6", GLYPH_TEXT, 58) X_GLYPH_DEFINE("7", GLYPH_TEXT, 59) X_GLYPH_DEFINE("8", GLYPH_TEXT, 60) X_GLYPH_DEFINE("9", GLYPH_TEXT, 61) X_GLYPH_DEFINE("!", GLYPH_TEXT, 64) X_GLYPH_DEFINE("\"", GLYPH_TEXT, 82) X_GLYPH_DEFINE("#", GLYPH_TEXT, 78) X_GLYPH_DEFINE("$", GLYPH_TEXT, 83) X_GLYPH_DEFINE("%", GLYPH_TEXT, 84) X_GLYPH_DEFINE("&", GLYPH_TEXT, 85) X_GLYPH_DEFINE("'", GLYPH_TEXT, 79) X_GLYPH_DEFINE("(", GLYPH_TEXT, 86) X_GLYPH_DEFINE(")", GLYPH_TEXT, 87) X_GLYPH_DEFINE("*", GLYPH_TEXT, 88) X_GLYPH_DEFINE("+", GLYPH_TEXT, 72) X_GLYPH_DEFINE(",", GLYPH_TEXT, 63) X_GLYPH_DEFINE("-", GLYPH_TEXT, 71) X_GLYPH_DEFINE(".", GLYPH_TEXT, 62) X_GLYPH_DEFINE("/", GLYPH_TEXT, 68) X_GLYPH_DEFINE(":", GLYPH_TEXT, 73) X_GLYPH_DEFINE(";", GLYPH_TEXT, 89) X_GLYPH_DEFINE("<", GLYPH_TEXT, 90) X_GLYPH_DEFINE("=", GLYPH_TEXT, 91) X_GLYPH_DEFINE(">", GLYPH_TEXT, 110) X_GLYPH_DEFINE("?", GLYPH_TEXT, 65) X_GLYPH_DEFINE("@", GLYPH_TEXT, 111) X_GLYPH_DEFINE("[", GLYPH_TEXT, 66) X_GLYPH_DEFINE("\\", GLYPH_TEXT, 76) X_GLYPH_DEFINE("]", GLYPH_TEXT, 75) X_GLYPH_DEFINE("^", GLYPH_TEXT, 112) X_GLYPH_DEFINE("_", GLYPH_TEXT, 113) X_GLYPH_DEFINE("`", GLYPH_TEXT, 114) X_GLYPH_DEFINE("{", GLYPH_TEXT, 115) X_GLYPH_DEFINE("|", GLYPH_TEXT, 116) X_GLYPH_DEFINE("}", GLYPH_TEXT, 117) X_GLYPH_DEFINE("~", GLYPH_TEXT, 118) X_GLYPH_DEFINE("\\{button down}", GLYPH_ICON, 106) X_GLYPH_DEFINE("\\{button up}", GLYPH_ICON, 107) X_GLYPH_DEFINE("\\{button left}", GLYPH_ICON, 108) X_GLYPH_DEFINE("\\{button right}", GLYPH_ICON, 109) X_GLYPH_DEFINE("\\{button triangle}", GLYPH_ICON, 93) X_GLYPH_DEFINE("\\{button circle}", GLYPH_ICON, 94) X_GLYPH_DEFINE("\\{button x}", GLYPH_ICON, 95) X_GLYPH_DEFINE("\\{button square}", GLYPH_ICON, 96) X_GLYPH_DEFINE("\\{button empty}", GLYPH_ICON, 92) X_GLYPH_DEFINE("\\{button l1}", GLYPH_ICON, 97) X_GLYPH_DEFINE("\\{button r1}", GLYPH_ICON, 98) X_GLYPH_DEFINE("\\{button l2}", GLYPH_ICON, 99) X_GLYPH_DEFINE("\\{button r2}", GLYPH_ICON, 100) X_GLYPH_DEFINE("\\{icon sound}", GLYPH_ICON, 101) X_GLYPH_DEFINE("\\{icon music}", GLYPH_ICON, 102) X_GLYPH_DEFINE("\\{ammo shotgun}", GLYPH_ICON, 103) X_GLYPH_DEFINE("\\{ammo magnums}", GLYPH_ICON, 104) X_GLYPH_DEFINE("\\{ammo uzis}", GLYPH_ICON, 105) X_GLYPH_DEFINE("\\{arrow up}", GLYPH_ICON, 80) X_GLYPH_DEFINE("\\{arrow down}", GLYPH_ICON, 81) X_GLYPH_DEFINE("\\{review}", GLYPH_REVIEW_MARKER, 119) X_GLYPH_DEFINE("\\{grave accent}", GLYPH_TEXT, 77) X_GLYPH_DEFINE("\\{acute accent}", GLYPH_TEXT, 70) X_GLYPH_DEFINE("\\{circumflex accent}", GLYPH_TEXT, 69) X_GLYPH_DEFINE("\\{circumflex}", GLYPH_TEXT, 120) X_GLYPH_DEFINE("\\{macron}", GLYPH_TEXT, 121) X_GLYPH_DEFINE("\\{breve}", GLYPH_TEXT, 122) X_GLYPH_DEFINE("\\{dot above}", GLYPH_TEXT, 123) X_GLYPH_DEFINE("\\{umlaut}", GLYPH_TEXT, 67) X_GLYPH_DEFINE("\\{caron}", GLYPH_TEXT, 124) X_GLYPH_DEFINE("\\{ring above}", GLYPH_TEXT, 125) X_GLYPH_DEFINE("\\{tilde}", GLYPH_TEXT, 126) X_GLYPH_DEFINE("\\{double acute accent}", GLYPH_TEXT, 127) X_GLYPH_DEFINE("\\{acute umlaut}", GLYPH_TEXT, 128) X_GLYPH_DEFINE("¡", GLYPH_TEXT, 129) X_GLYPH_DEFINE("¢", GLYPH_TEXT, 130) X_GLYPH_DEFINE("£", GLYPH_TEXT, 131) X_GLYPH_DEFINE("¤", GLYPH_TEXT, 132) X_GLYPH_DEFINE("¥", GLYPH_TEXT, 133) X_GLYPH_DEFINE("¦", GLYPH_TEXT, 134) X_GLYPH_DEFINE("§", GLYPH_TEXT, 135) X_GLYPH_DEFINE("©", GLYPH_TEXT, 136) X_GLYPH_DEFINE("ª", GLYPH_TEXT, 137) X_GLYPH_DEFINE("«", GLYPH_TEXT, 138) X_GLYPH_DEFINE("¬", GLYPH_TEXT, 139) X_GLYPH_DEFINE("®", GLYPH_TEXT, 140) X_GLYPH_DEFINE("°", GLYPH_TEXT, 141) X_GLYPH_DEFINE("±", GLYPH_TEXT, 142) X_GLYPH_DEFINE("²", GLYPH_TEXT, 143) X_GLYPH_DEFINE("³", GLYPH_TEXT, 144) X_GLYPH_DEFINE("µ", GLYPH_TEXT, 145) X_GLYPH_DEFINE("¶", GLYPH_TEXT, 146) X_GLYPH_DEFINE("·", GLYPH_TEXT, 147) X_GLYPH_DEFINE("¹", GLYPH_TEXT, 148) X_GLYPH_DEFINE("º", GLYPH_TEXT, 149) X_GLYPH_DEFINE("»", GLYPH_TEXT, 150) X_GLYPH_DEFINE("¼", GLYPH_TEXT, 151) X_GLYPH_DEFINE("½", GLYPH_TEXT, 152) X_GLYPH_DEFINE("¾", GLYPH_TEXT, 153) X_GLYPH_DEFINE("¿", GLYPH_TEXT, 154) X_GLYPH_DEFINE("À", GLYPH_TEXT, 155) X_GLYPH_DEFINE("Á", GLYPH_TEXT, 156) X_GLYPH_DEFINE("Â", GLYPH_TEXT, 157) X_GLYPH_DEFINE("Ã", GLYPH_TEXT, 158) X_GLYPH_DEFINE("Ä", GLYPH_TEXT, 159) X_GLYPH_DEFINE("Å", GLYPH_TEXT, 160) X_GLYPH_DEFINE("Æ", GLYPH_TEXT, 161) X_GLYPH_DEFINE("Ç", GLYPH_TEXT, 162) X_GLYPH_DEFINE("È", GLYPH_TEXT, 163) X_GLYPH_DEFINE("É", GLYPH_TEXT, 164) X_GLYPH_DEFINE("Ê", GLYPH_TEXT, 165) X_GLYPH_DEFINE("Ë", GLYPH_TEXT, 166) X_GLYPH_DEFINE("Ì", GLYPH_TEXT, 167) X_GLYPH_DEFINE("Í", GLYPH_TEXT, 168) X_GLYPH_DEFINE("Î", GLYPH_TEXT, 169) X_GLYPH_DEFINE("Ï", GLYPH_TEXT, 170) X_GLYPH_DEFINE("Ð", GLYPH_TEXT, 171) X_GLYPH_DEFINE("Ñ", GLYPH_TEXT, 172) X_GLYPH_DEFINE("Ò", GLYPH_TEXT, 173) X_GLYPH_DEFINE("Ó", GLYPH_TEXT, 174) X_GLYPH_DEFINE("Ô", GLYPH_TEXT, 175) X_GLYPH_DEFINE("Õ", GLYPH_TEXT, 176) X_GLYPH_DEFINE("Ö", GLYPH_TEXT, 177) X_GLYPH_DEFINE("×", GLYPH_TEXT, 178) X_GLYPH_DEFINE("Ø", GLYPH_TEXT, 179) X_GLYPH_DEFINE("Ù", GLYPH_TEXT, 180) X_GLYPH_DEFINE("Ú", GLYPH_TEXT, 181) X_GLYPH_DEFINE("Û", GLYPH_TEXT, 182) X_GLYPH_DEFINE("Ü", GLYPH_TEXT, 183) X_GLYPH_DEFINE("Ý", GLYPH_TEXT, 184) X_GLYPH_DEFINE("Þ", GLYPH_TEXT, 185) X_GLYPH_DEFINE("ß", GLYPH_TEXT, 74) X_GLYPH_DEFINE("à", GLYPH_TEXT, 186) X_GLYPH_DEFINE("á", GLYPH_TEXT, 187) X_GLYPH_DEFINE("â", GLYPH_TEXT, 188) X_GLYPH_DEFINE("ã", GLYPH_TEXT, 189) X_GLYPH_DEFINE("ä", GLYPH_TEXT, 190) X_GLYPH_DEFINE("å", GLYPH_TEXT, 191) X_GLYPH_DEFINE("æ", GLYPH_TEXT, 192) X_GLYPH_DEFINE("ç", GLYPH_TEXT, 193) X_GLYPH_DEFINE("è", GLYPH_TEXT, 194) X_GLYPH_DEFINE("é", GLYPH_TEXT, 195) X_GLYPH_DEFINE("ê", GLYPH_TEXT, 196) X_GLYPH_DEFINE("ë", GLYPH_TEXT, 197) X_GLYPH_DEFINE("ì", GLYPH_TEXT, 198) X_GLYPH_DEFINE("í", GLYPH_TEXT, 199) X_GLYPH_DEFINE("î", GLYPH_TEXT, 200) X_GLYPH_DEFINE("ï", GLYPH_TEXT, 201) X_GLYPH_DEFINE("ð", GLYPH_TEXT, 202) X_GLYPH_DEFINE("ñ", GLYPH_TEXT, 203) X_GLYPH_DEFINE("ò", GLYPH_TEXT, 204) X_GLYPH_DEFINE("ó", GLYPH_TEXT, 205) X_GLYPH_DEFINE("ô", GLYPH_TEXT, 206) X_GLYPH_DEFINE("õ", GLYPH_TEXT, 207) X_GLYPH_DEFINE("ö", GLYPH_TEXT, 208) X_GLYPH_DEFINE("÷", GLYPH_TEXT, 209) X_GLYPH_DEFINE("ø", GLYPH_TEXT, 210) X_GLYPH_DEFINE("ù", GLYPH_TEXT, 211) X_GLYPH_DEFINE("ú", GLYPH_TEXT, 212) X_GLYPH_DEFINE("û", GLYPH_TEXT, 213) X_GLYPH_DEFINE("ü", GLYPH_TEXT, 214) X_GLYPH_DEFINE("ý", GLYPH_TEXT, 215) X_GLYPH_DEFINE("þ", GLYPH_TEXT, 216) X_GLYPH_DEFINE("ÿ", GLYPH_TEXT, 217) X_GLYPH_DEFINE("Ā", GLYPH_TEXT, 218) X_GLYPH_DEFINE("ā", GLYPH_TEXT, 219) X_GLYPH_DEFINE("Ă", GLYPH_TEXT, 220) X_GLYPH_DEFINE("ă", GLYPH_TEXT, 221) X_GLYPH_DEFINE("Ą", GLYPH_TEXT, 222) X_GLYPH_DEFINE("ą", GLYPH_TEXT, 223) X_GLYPH_DEFINE("Ć", GLYPH_TEXT, 224) X_GLYPH_DEFINE("ć", GLYPH_TEXT, 225) X_GLYPH_DEFINE("Ĉ", GLYPH_TEXT, 226) X_GLYPH_DEFINE("ĉ", GLYPH_TEXT, 227) X_GLYPH_DEFINE("Ċ", GLYPH_TEXT, 228) X_GLYPH_DEFINE("ċ", GLYPH_TEXT, 229) X_GLYPH_DEFINE("Č", GLYPH_TEXT, 230) X_GLYPH_DEFINE("č", GLYPH_TEXT, 231) X_GLYPH_DEFINE("Ď", GLYPH_TEXT, 232) X_GLYPH_DEFINE("ď", GLYPH_TEXT, 233) X_GLYPH_DEFINE("Đ", GLYPH_TEXT, 234) X_GLYPH_DEFINE("đ", GLYPH_TEXT, 235) X_GLYPH_DEFINE("Ē", GLYPH_TEXT, 236) X_GLYPH_DEFINE("ē", GLYPH_TEXT, 237) X_GLYPH_DEFINE("Ĕ", GLYPH_TEXT, 238) X_GLYPH_DEFINE("ĕ", GLYPH_TEXT, 239) X_GLYPH_DEFINE("Ė", GLYPH_TEXT, 240) X_GLYPH_DEFINE("ė", GLYPH_TEXT, 241) X_GLYPH_DEFINE("Ę", GLYPH_TEXT, 242) X_GLYPH_DEFINE("ę", GLYPH_TEXT, 243) X_GLYPH_DEFINE("Ě", GLYPH_TEXT, 244) X_GLYPH_DEFINE("ě", GLYPH_TEXT, 245) X_GLYPH_DEFINE("Ĝ", GLYPH_TEXT, 246) X_GLYPH_DEFINE("ĝ", GLYPH_TEXT, 247) X_GLYPH_DEFINE("Ğ", GLYPH_TEXT, 248) X_GLYPH_DEFINE("ğ", GLYPH_TEXT, 249) X_GLYPH_DEFINE("Ġ", GLYPH_TEXT, 250) X_GLYPH_DEFINE("ġ", GLYPH_TEXT, 251) X_GLYPH_DEFINE("Ģ", GLYPH_TEXT, 252) X_GLYPH_DEFINE("ģ", GLYPH_TEXT, 253) X_GLYPH_DEFINE("Ĥ", GLYPH_TEXT, 254) X_GLYPH_DEFINE("ĥ", GLYPH_TEXT, 255) X_GLYPH_DEFINE("Ħ", GLYPH_TEXT, 256) X_GLYPH_DEFINE("ħ", GLYPH_TEXT, 257) X_GLYPH_DEFINE("Ĩ", GLYPH_TEXT, 258) X_GLYPH_DEFINE("ĩ", GLYPH_TEXT, 259) X_GLYPH_DEFINE("Ī", GLYPH_TEXT, 260) X_GLYPH_DEFINE("ī", GLYPH_TEXT, 261) X_GLYPH_DEFINE("Ĭ", GLYPH_TEXT, 262) X_GLYPH_DEFINE("ĭ", GLYPH_TEXT, 263) X_GLYPH_DEFINE("Į", GLYPH_TEXT, 264) X_GLYPH_DEFINE("į", GLYPH_TEXT, 265) X_GLYPH_DEFINE("İ", GLYPH_TEXT, 266) X_GLYPH_DEFINE("ı", GLYPH_TEXT, 267) X_GLYPH_DEFINE("Ĵ", GLYPH_TEXT, 268) X_GLYPH_DEFINE("ĵ", GLYPH_TEXT, 269) X_GLYPH_DEFINE("Ķ", GLYPH_TEXT, 270) X_GLYPH_DEFINE("ķ", GLYPH_TEXT, 271) X_GLYPH_DEFINE("ĸ", GLYPH_TEXT, 272) X_GLYPH_DEFINE("Ĺ", GLYPH_TEXT, 273) X_GLYPH_DEFINE("ĺ", GLYPH_TEXT, 274) X_GLYPH_DEFINE("Ļ", GLYPH_TEXT, 275) X_GLYPH_DEFINE("ļ", GLYPH_TEXT, 276) X_GLYPH_DEFINE("Ľ", GLYPH_TEXT, 277) X_GLYPH_DEFINE("ľ", GLYPH_TEXT, 278) X_GLYPH_DEFINE("Ŀ", GLYPH_TEXT, 279) X_GLYPH_DEFINE("ŀ", GLYPH_TEXT, 280) X_GLYPH_DEFINE("Ł", GLYPH_TEXT, 281) X_GLYPH_DEFINE("ł", GLYPH_TEXT, 282) X_GLYPH_DEFINE("Ń", GLYPH_TEXT, 283) X_GLYPH_DEFINE("ń", GLYPH_TEXT, 284) X_GLYPH_DEFINE("Ņ", GLYPH_TEXT, 285) X_GLYPH_DEFINE("ņ", GLYPH_TEXT, 286) X_GLYPH_DEFINE("Ň", GLYPH_TEXT, 287) X_GLYPH_DEFINE("ň", GLYPH_TEXT, 288) X_GLYPH_DEFINE("ʼn", GLYPH_TEXT, 289) X_GLYPH_DEFINE("Ŋ", GLYPH_TEXT, 290) X_GLYPH_DEFINE("ŋ", GLYPH_TEXT, 291) X_GLYPH_DEFINE("Ō", GLYPH_TEXT, 292) X_GLYPH_DEFINE("ō", GLYPH_TEXT, 293) X_GLYPH_DEFINE("Ŏ", GLYPH_TEXT, 294) X_GLYPH_DEFINE("ŏ", GLYPH_TEXT, 295) X_GLYPH_DEFINE("Ő", GLYPH_TEXT, 296) X_GLYPH_DEFINE("ő", GLYPH_TEXT, 297) X_GLYPH_DEFINE("Œ", GLYPH_TEXT, 298) X_GLYPH_DEFINE("œ", GLYPH_TEXT, 299) X_GLYPH_DEFINE("Ŕ", GLYPH_TEXT, 300) X_GLYPH_DEFINE("ŕ", GLYPH_TEXT, 301) X_GLYPH_DEFINE("Ŗ", GLYPH_TEXT, 302) X_GLYPH_DEFINE("ŗ", GLYPH_TEXT, 303) X_GLYPH_DEFINE("Ř", GLYPH_TEXT, 304) X_GLYPH_DEFINE("ř", GLYPH_TEXT, 305) X_GLYPH_DEFINE("Ś", GLYPH_TEXT, 306) X_GLYPH_DEFINE("ś", GLYPH_TEXT, 307) X_GLYPH_DEFINE("Ŝ", GLYPH_TEXT, 308) X_GLYPH_DEFINE("ŝ", GLYPH_TEXT, 309) X_GLYPH_DEFINE("Ş", GLYPH_TEXT, 310) X_GLYPH_DEFINE("ş", GLYPH_TEXT, 311) X_GLYPH_DEFINE("Š", GLYPH_TEXT, 312) X_GLYPH_DEFINE("š", GLYPH_TEXT, 313) X_GLYPH_DEFINE("Ţ", GLYPH_TEXT, 314) X_GLYPH_DEFINE("ţ", GLYPH_TEXT, 315) X_GLYPH_DEFINE("Ť", GLYPH_TEXT, 316) X_GLYPH_DEFINE("ť", GLYPH_TEXT, 317) X_GLYPH_DEFINE("Ŧ", GLYPH_TEXT, 318) X_GLYPH_DEFINE("ŧ", GLYPH_TEXT, 319) X_GLYPH_DEFINE("Ũ", GLYPH_TEXT, 320) X_GLYPH_DEFINE("ũ", GLYPH_TEXT, 321) X_GLYPH_DEFINE("Ū", GLYPH_TEXT, 322) X_GLYPH_DEFINE("ū", GLYPH_TEXT, 323) X_GLYPH_DEFINE("Ŭ", GLYPH_TEXT, 324) X_GLYPH_DEFINE("ŭ", GLYPH_TEXT, 325) X_GLYPH_DEFINE("Ů", GLYPH_TEXT, 326) X_GLYPH_DEFINE("ů", GLYPH_TEXT, 327) X_GLYPH_DEFINE("Ű", GLYPH_TEXT, 328) X_GLYPH_DEFINE("ű", GLYPH_TEXT, 329) X_GLYPH_DEFINE("Ų", GLYPH_TEXT, 330) X_GLYPH_DEFINE("ų", GLYPH_TEXT, 331) X_GLYPH_DEFINE("Ŵ", GLYPH_TEXT, 332) X_GLYPH_DEFINE("ŵ", GLYPH_TEXT, 333) X_GLYPH_DEFINE("Ŷ", GLYPH_TEXT, 334) X_GLYPH_DEFINE("ŷ", GLYPH_TEXT, 335) X_GLYPH_DEFINE("Ÿ", GLYPH_TEXT, 336) X_GLYPH_DEFINE("Ź", GLYPH_TEXT, 337) X_GLYPH_DEFINE("ź", GLYPH_TEXT, 338) X_GLYPH_DEFINE("Ż", GLYPH_TEXT, 339) X_GLYPH_DEFINE("ż", GLYPH_TEXT, 340) X_GLYPH_DEFINE("Ž", GLYPH_TEXT, 341) X_GLYPH_DEFINE("ž", GLYPH_TEXT, 342) X_GLYPH_DEFINE("ƒ", GLYPH_TEXT, 343) X_GLYPH_DEFINE("Ǎ", GLYPH_TEXT, 344) X_GLYPH_DEFINE("ǎ", GLYPH_TEXT, 345) X_GLYPH_DEFINE("Ǐ", GLYPH_TEXT, 346) X_GLYPH_DEFINE("ǐ", GLYPH_TEXT, 347) X_GLYPH_DEFINE("Ǒ", GLYPH_TEXT, 348) X_GLYPH_DEFINE("ǒ", GLYPH_TEXT, 349) X_GLYPH_DEFINE("Ǔ", GLYPH_TEXT, 350) X_GLYPH_DEFINE("ǔ", GLYPH_TEXT, 351) X_GLYPH_DEFINE("Ǧ", GLYPH_TEXT, 352) X_GLYPH_DEFINE("ǧ", GLYPH_TEXT, 353) X_GLYPH_DEFINE("Ǩ", GLYPH_TEXT, 354) X_GLYPH_DEFINE("ǩ", GLYPH_TEXT, 355) X_GLYPH_DEFINE("ǰ", GLYPH_TEXT, 356) X_GLYPH_DEFINE("Ǵ", GLYPH_TEXT, 357) X_GLYPH_DEFINE("ǵ", GLYPH_TEXT, 358) X_GLYPH_DEFINE("Ǹ", GLYPH_TEXT, 359) X_GLYPH_DEFINE("ǹ", GLYPH_TEXT, 360) X_GLYPH_DEFINE("Ȟ", GLYPH_TEXT, 361) X_GLYPH_DEFINE("ȟ", GLYPH_TEXT, 362) X_GLYPH_DEFINE("Ȧ", GLYPH_TEXT, 363) X_GLYPH_DEFINE("ȧ", GLYPH_TEXT, 364) X_GLYPH_DEFINE("Ȯ", GLYPH_TEXT, 365) X_GLYPH_DEFINE("ȯ", GLYPH_TEXT, 366) X_GLYPH_DEFINE("Ȳ", GLYPH_TEXT, 367) X_GLYPH_DEFINE("ȳ", GLYPH_TEXT, 368) X_GLYPH_DEFINE("Ș", GLYPH_TEXT, 369) X_GLYPH_DEFINE("ș", GLYPH_TEXT, 370) X_GLYPH_DEFINE("Ț", GLYPH_TEXT, 371) X_GLYPH_DEFINE("ț", GLYPH_TEXT, 372) X_GLYPH_DEFINE("Γ", GLYPH_TEXT, 373) X_GLYPH_DEFINE("Δ", GLYPH_TEXT, 374) X_GLYPH_DEFINE("Ε", GLYPH_TEXT, 375) X_GLYPH_DEFINE("Ζ", GLYPH_TEXT, 376) X_GLYPH_DEFINE("Η", GLYPH_TEXT, 377) X_GLYPH_DEFINE("Θ", GLYPH_TEXT, 378) X_GLYPH_DEFINE("Ι", GLYPH_TEXT, 379) X_GLYPH_DEFINE("Κ", GLYPH_TEXT, 380) X_GLYPH_DEFINE("Λ", GLYPH_TEXT, 381) X_GLYPH_DEFINE("Μ", GLYPH_TEXT, 382) X_GLYPH_DEFINE("Ν", GLYPH_TEXT, 383) X_GLYPH_DEFINE("Ξ", GLYPH_TEXT, 384) X_GLYPH_DEFINE("Ο", GLYPH_TEXT, 385) X_GLYPH_DEFINE("Π", GLYPH_TEXT, 386) X_GLYPH_DEFINE("Ρ", GLYPH_TEXT, 387) X_GLYPH_DEFINE("Σ", GLYPH_TEXT, 388) X_GLYPH_DEFINE("Τ", GLYPH_TEXT, 389) X_GLYPH_DEFINE("Υ", GLYPH_TEXT, 390) X_GLYPH_DEFINE("Φ", GLYPH_TEXT, 391) X_GLYPH_DEFINE("Χ", GLYPH_TEXT, 392) X_GLYPH_DEFINE("Ψ", GLYPH_TEXT, 393) X_GLYPH_DEFINE("Ω", GLYPH_TEXT, 394) X_GLYPH_DEFINE("α", GLYPH_TEXT, 395) X_GLYPH_DEFINE("β", GLYPH_TEXT, 396) X_GLYPH_DEFINE("γ", GLYPH_TEXT, 397) X_GLYPH_DEFINE("δ", GLYPH_TEXT, 398) X_GLYPH_DEFINE("ε", GLYPH_TEXT, 399) X_GLYPH_DEFINE("ζ", GLYPH_TEXT, 400) X_GLYPH_DEFINE("η", GLYPH_TEXT, 401) X_GLYPH_DEFINE("θ", GLYPH_TEXT, 402) X_GLYPH_DEFINE("ι", GLYPH_TEXT, 403) X_GLYPH_DEFINE("κ", GLYPH_TEXT, 404) X_GLYPH_DEFINE("λ", GLYPH_TEXT, 405) X_GLYPH_DEFINE("μ", GLYPH_TEXT, 406) X_GLYPH_DEFINE("ν", GLYPH_TEXT, 407) X_GLYPH_DEFINE("ξ", GLYPH_TEXT, 408) X_GLYPH_DEFINE("ο", GLYPH_TEXT, 409) X_GLYPH_DEFINE("π", GLYPH_TEXT, 410) X_GLYPH_DEFINE("ρ", GLYPH_TEXT, 411) X_GLYPH_DEFINE("ς", GLYPH_TEXT, 412) X_GLYPH_DEFINE("σ", GLYPH_TEXT, 413) X_GLYPH_DEFINE("τ", GLYPH_TEXT, 414) X_GLYPH_DEFINE("υ", GLYPH_TEXT, 415) X_GLYPH_DEFINE("φ", GLYPH_TEXT, 416) X_GLYPH_DEFINE("χ", GLYPH_TEXT, 417) X_GLYPH_DEFINE("ψ", GLYPH_TEXT, 418) X_GLYPH_DEFINE("ω", GLYPH_TEXT, 419) X_GLYPH_DEFINE("Ά", GLYPH_TEXT, 420) X_GLYPH_DEFINE("Έ", GLYPH_TEXT, 421) X_GLYPH_DEFINE("Ή", GLYPH_TEXT, 422) X_GLYPH_DEFINE("Ί", GLYPH_TEXT, 423) X_GLYPH_DEFINE("Ό", GLYPH_TEXT, 424) X_GLYPH_DEFINE("Ύ", GLYPH_TEXT, 425) X_GLYPH_DEFINE("Ώ", GLYPH_TEXT, 426) X_GLYPH_DEFINE("ΐ", GLYPH_TEXT, 427) X_GLYPH_DEFINE("Α", GLYPH_TEXT, 428) X_GLYPH_DEFINE("Β", GLYPH_TEXT, 429) X_GLYPH_DEFINE("Ϊ", GLYPH_TEXT, 430) X_GLYPH_DEFINE("Ϋ", GLYPH_TEXT, 431) X_GLYPH_DEFINE("ά", GLYPH_TEXT, 432) X_GLYPH_DEFINE("έ", GLYPH_TEXT, 433) X_GLYPH_DEFINE("ή", GLYPH_TEXT, 434) X_GLYPH_DEFINE("ί", GLYPH_TEXT, 435) X_GLYPH_DEFINE("ΰ", GLYPH_TEXT, 436) X_GLYPH_DEFINE("ϊ", GLYPH_TEXT, 437) X_GLYPH_DEFINE("ϋ", GLYPH_TEXT, 438) X_GLYPH_DEFINE("ό", GLYPH_TEXT, 439) X_GLYPH_DEFINE("ύ", GLYPH_TEXT, 440) X_GLYPH_DEFINE("ώ", GLYPH_TEXT, 441) X_GLYPH_DEFINE("Ѐ", GLYPH_TEXT, 442) X_GLYPH_DEFINE("Ё", GLYPH_TEXT, 443) X_GLYPH_DEFINE("Ђ", GLYPH_TEXT, 444) X_GLYPH_DEFINE("Ѓ", GLYPH_TEXT, 445) X_GLYPH_DEFINE("Є", GLYPH_TEXT, 446) X_GLYPH_DEFINE("Ѕ", GLYPH_TEXT, 447) X_GLYPH_DEFINE("І", GLYPH_TEXT, 448) X_GLYPH_DEFINE("Ї", GLYPH_TEXT, 449) X_GLYPH_DEFINE("Ј", GLYPH_TEXT, 450) X_GLYPH_DEFINE("Љ", GLYPH_TEXT, 451) X_GLYPH_DEFINE("Њ", GLYPH_TEXT, 452) X_GLYPH_DEFINE("Ћ", GLYPH_TEXT, 453) X_GLYPH_DEFINE("Ќ", GLYPH_TEXT, 454) X_GLYPH_DEFINE("Ѝ", GLYPH_TEXT, 455) X_GLYPH_DEFINE("Ў", GLYPH_TEXT, 456) X_GLYPH_DEFINE("Џ", GLYPH_TEXT, 457) X_GLYPH_DEFINE("А", GLYPH_TEXT, 458) X_GLYPH_DEFINE("Б", GLYPH_TEXT, 459) X_GLYPH_DEFINE("В", GLYPH_TEXT, 460) X_GLYPH_DEFINE("Г", GLYPH_TEXT, 461) X_GLYPH_DEFINE("Д", GLYPH_TEXT, 462) X_GLYPH_DEFINE("Е", GLYPH_TEXT, 463) X_GLYPH_DEFINE("Ж", GLYPH_TEXT, 464) X_GLYPH_DEFINE("З", GLYPH_TEXT, 465) X_GLYPH_DEFINE("И", GLYPH_TEXT, 466) X_GLYPH_DEFINE("Й", GLYPH_TEXT, 467) X_GLYPH_DEFINE("К", GLYPH_TEXT, 468) X_GLYPH_DEFINE("Л", GLYPH_TEXT, 469) X_GLYPH_DEFINE("М", GLYPH_TEXT, 470) X_GLYPH_DEFINE("Н", GLYPH_TEXT, 471) X_GLYPH_DEFINE("О", GLYPH_TEXT, 472) X_GLYPH_DEFINE("П", GLYPH_TEXT, 473) X_GLYPH_DEFINE("Р", GLYPH_TEXT, 474) X_GLYPH_DEFINE("С", GLYPH_TEXT, 475) X_GLYPH_DEFINE("Т", GLYPH_TEXT, 476) X_GLYPH_DEFINE("У", GLYPH_TEXT, 477) X_GLYPH_DEFINE("Ф", GLYPH_TEXT, 478) X_GLYPH_DEFINE("Х", GLYPH_TEXT, 479) X_GLYPH_DEFINE("Ц", GLYPH_TEXT, 480) X_GLYPH_DEFINE("Ч", GLYPH_TEXT, 481) X_GLYPH_DEFINE("Ш", GLYPH_TEXT, 482) X_GLYPH_DEFINE("Щ", GLYPH_TEXT, 483) X_GLYPH_DEFINE("Ъ", GLYPH_TEXT, 484) X_GLYPH_DEFINE("Ы", GLYPH_TEXT, 485) X_GLYPH_DEFINE("Ь", GLYPH_TEXT, 486) X_GLYPH_DEFINE("Э", GLYPH_TEXT, 487) X_GLYPH_DEFINE("Ю", GLYPH_TEXT, 488) X_GLYPH_DEFINE("Я", GLYPH_TEXT, 489) X_GLYPH_DEFINE("а", GLYPH_TEXT, 490) X_GLYPH_DEFINE("б", GLYPH_TEXT, 491) X_GLYPH_DEFINE("в", GLYPH_TEXT, 492) X_GLYPH_DEFINE("г", GLYPH_TEXT, 493) X_GLYPH_DEFINE("д", GLYPH_TEXT, 494) X_GLYPH_DEFINE("е", GLYPH_TEXT, 495) X_GLYPH_DEFINE("ж", GLYPH_TEXT, 496) X_GLYPH_DEFINE("з", GLYPH_TEXT, 497) X_GLYPH_DEFINE("и", GLYPH_TEXT, 498) X_GLYPH_DEFINE("й", GLYPH_TEXT, 499) X_GLYPH_DEFINE("к", GLYPH_TEXT, 500) X_GLYPH_DEFINE("л", GLYPH_TEXT, 501) X_GLYPH_DEFINE("м", GLYPH_TEXT, 502) X_GLYPH_DEFINE("н", GLYPH_TEXT, 503) X_GLYPH_DEFINE("о", GLYPH_TEXT, 504) X_GLYPH_DEFINE("п", GLYPH_TEXT, 505) X_GLYPH_DEFINE("р", GLYPH_TEXT, 506) X_GLYPH_DEFINE("с", GLYPH_TEXT, 507) X_GLYPH_DEFINE("т", GLYPH_TEXT, 508) X_GLYPH_DEFINE("у", GLYPH_TEXT, 509) X_GLYPH_DEFINE("ф", GLYPH_TEXT, 510) X_GLYPH_DEFINE("х", GLYPH_TEXT, 511) X_GLYPH_DEFINE("ц", GLYPH_TEXT, 512) X_GLYPH_DEFINE("ч", GLYPH_TEXT, 513) X_GLYPH_DEFINE("ш", GLYPH_TEXT, 514) X_GLYPH_DEFINE("щ", GLYPH_TEXT, 515) X_GLYPH_DEFINE("ъ", GLYPH_TEXT, 516) X_GLYPH_DEFINE("ы", GLYPH_TEXT, 517) X_GLYPH_DEFINE("ь", GLYPH_TEXT, 518) X_GLYPH_DEFINE("э", GLYPH_TEXT, 519) X_GLYPH_DEFINE("ю", GLYPH_TEXT, 520) X_GLYPH_DEFINE("я", GLYPH_TEXT, 521) X_GLYPH_DEFINE("ѐ", GLYPH_TEXT, 522) X_GLYPH_DEFINE("ё", GLYPH_TEXT, 523) X_GLYPH_DEFINE("ђ", GLYPH_TEXT, 524) X_GLYPH_DEFINE("ѓ", GLYPH_TEXT, 525) X_GLYPH_DEFINE("є", GLYPH_TEXT, 526) X_GLYPH_DEFINE("ѕ", GLYPH_TEXT, 527) X_GLYPH_DEFINE("і", GLYPH_TEXT, 528) X_GLYPH_DEFINE("ї", GLYPH_TEXT, 529) X_GLYPH_DEFINE("ј", GLYPH_TEXT, 530) X_GLYPH_DEFINE("љ", GLYPH_TEXT, 531) X_GLYPH_DEFINE("њ", GLYPH_TEXT, 532) X_GLYPH_DEFINE("ћ", GLYPH_TEXT, 533) X_GLYPH_DEFINE("ќ", GLYPH_TEXT, 534) X_GLYPH_DEFINE("ѝ", GLYPH_TEXT, 535) X_GLYPH_DEFINE("ў", GLYPH_TEXT, 536) X_GLYPH_DEFINE("џ", GLYPH_TEXT, 537) X_GLYPH_DEFINE("Ґ", GLYPH_TEXT, 538) X_GLYPH_DEFINE("ґ", GLYPH_TEXT, 539) X_GLYPH_DEFINE("Ḃ", GLYPH_TEXT, 540) X_GLYPH_DEFINE("ḃ", GLYPH_TEXT, 541) X_GLYPH_DEFINE("Ḋ", GLYPH_TEXT, 542) X_GLYPH_DEFINE("ḋ", GLYPH_TEXT, 543) X_GLYPH_DEFINE("Ḟ", GLYPH_TEXT, 544) X_GLYPH_DEFINE("ḟ", GLYPH_TEXT, 545) X_GLYPH_DEFINE("Ḡ", GLYPH_TEXT, 546) X_GLYPH_DEFINE("ḡ", GLYPH_TEXT, 547) X_GLYPH_DEFINE("Ḣ", GLYPH_TEXT, 548) X_GLYPH_DEFINE("ḣ", GLYPH_TEXT, 549) X_GLYPH_DEFINE("Ḧ", GLYPH_TEXT, 550) X_GLYPH_DEFINE("ḧ", GLYPH_TEXT, 551) X_GLYPH_DEFINE("Ḱ", GLYPH_TEXT, 552) X_GLYPH_DEFINE("ḱ", GLYPH_TEXT, 553) X_GLYPH_DEFINE("Ḿ", GLYPH_TEXT, 554) X_GLYPH_DEFINE("ḿ", GLYPH_TEXT, 555) X_GLYPH_DEFINE("Ṁ", GLYPH_TEXT, 556) X_GLYPH_DEFINE("ṁ", GLYPH_TEXT, 557) X_GLYPH_DEFINE("Ṅ", GLYPH_TEXT, 558) X_GLYPH_DEFINE("ṅ", GLYPH_TEXT, 559) X_GLYPH_DEFINE("Ṕ", GLYPH_TEXT, 560) X_GLYPH_DEFINE("ṕ", GLYPH_TEXT, 561) X_GLYPH_DEFINE("Ṗ", GLYPH_TEXT, 562) X_GLYPH_DEFINE("ṗ", GLYPH_TEXT, 563) X_GLYPH_DEFINE("Ṙ", GLYPH_TEXT, 564) X_GLYPH_DEFINE("ṙ", GLYPH_TEXT, 565) X_GLYPH_DEFINE("Ṡ", GLYPH_TEXT, 566) X_GLYPH_DEFINE("ṡ", GLYPH_TEXT, 567) X_GLYPH_DEFINE("Ṫ", GLYPH_TEXT, 568) X_GLYPH_DEFINE("ṫ", GLYPH_TEXT, 569) X_GLYPH_DEFINE("Ṽ", GLYPH_TEXT, 570) X_GLYPH_DEFINE("ṽ", GLYPH_TEXT, 571) X_GLYPH_DEFINE("Ẁ", GLYPH_TEXT, 572) X_GLYPH_DEFINE("ẁ", GLYPH_TEXT, 573) X_GLYPH_DEFINE("Ẃ", GLYPH_TEXT, 574) X_GLYPH_DEFINE("ẃ", GLYPH_TEXT, 575) X_GLYPH_DEFINE("Ẅ", GLYPH_TEXT, 576) X_GLYPH_DEFINE("ẅ", GLYPH_TEXT, 577) X_GLYPH_DEFINE("Ẇ", GLYPH_TEXT, 578) X_GLYPH_DEFINE("ẇ", GLYPH_TEXT, 579) X_GLYPH_DEFINE("Ẋ", GLYPH_TEXT, 580) X_GLYPH_DEFINE("ẋ", GLYPH_TEXT, 581) X_GLYPH_DEFINE("Ẍ", GLYPH_TEXT, 582) X_GLYPH_DEFINE("ẍ", GLYPH_TEXT, 583) X_GLYPH_DEFINE("Ẏ", GLYPH_TEXT, 584) X_GLYPH_DEFINE("ẏ", GLYPH_TEXT, 585) X_GLYPH_DEFINE("Ẑ", GLYPH_TEXT, 586) X_GLYPH_DEFINE("ẑ", GLYPH_TEXT, 587) X_GLYPH_DEFINE("ẗ", GLYPH_TEXT, 588) X_GLYPH_DEFINE("ẘ", GLYPH_TEXT, 589) X_GLYPH_DEFINE("ẙ", GLYPH_TEXT, 590) X_GLYPH_DEFINE("Ẽ", GLYPH_TEXT, 591) X_GLYPH_DEFINE("ẽ", GLYPH_TEXT, 592) X_GLYPH_DEFINE("Ỳ", GLYPH_TEXT, 593) X_GLYPH_DEFINE("ỳ", GLYPH_TEXT, 594) X_GLYPH_DEFINE("Ỹ", GLYPH_TEXT, 595) X_GLYPH_DEFINE("ỹ", GLYPH_TEXT, 596) X_GLYPH_DEFINE("–", GLYPH_TEXT, 597) X_GLYPH_DEFINE("—", GLYPH_TEXT, 598) X_GLYPH_DEFINE("‘", GLYPH_TEXT, 599) X_GLYPH_DEFINE("’", GLYPH_TEXT, 600) X_GLYPH_DEFINE("“", GLYPH_TEXT, 601) X_GLYPH_DEFINE("”", GLYPH_TEXT, 602) X_GLYPH_DEFINE("†", GLYPH_TEXT, 603) X_GLYPH_DEFINE("‡", GLYPH_TEXT, 604) X_GLYPH_DEFINE("•", GLYPH_TEXT, 605) X_GLYPH_DEFINE("…", GLYPH_TEXT, 606) X_GLYPH_DEFINE("‰", GLYPH_TEXT, 607) X_GLYPH_DEFINE("‹", GLYPH_TEXT, 608) X_GLYPH_DEFINE("›", GLYPH_TEXT, 609) X_GLYPH_DEFINE("⁴", GLYPH_TEXT, 610) X_GLYPH_DEFINE("€", GLYPH_TEXT, 611) X_GLYPH_DEFINE("₯", GLYPH_TEXT, 612) X_GLYPH_DEFINE("№", GLYPH_TEXT, 613) X_GLYPH_DEFINE("™", GLYPH_TEXT, 614) X_GLYPH_DEFINE("fi", GLYPH_TEXT, 615) X_GLYPH_DEFINE("fl", GLYPH_TEXT, 616) X_GLYPH_DEFINE("\\{keyboard backspace}", GLYPH_ICON, 617) X_GLYPH_DEFINE("\\{keyboard scroll_lock}", GLYPH_ICON, 618) X_GLYPH_DEFINE("\\{keyboard return}", GLYPH_ICON, 619) X_GLYPH_DEFINE("\\{keyboard caps_lock}", GLYPH_ICON, 620) X_GLYPH_DEFINE("\\{keyboard print_screen}", GLYPH_ICON, 621) X_GLYPH_DEFINE("\\{keyboard insert}", GLYPH_ICON, 622) X_GLYPH_DEFINE("\\{keyboard num_lock}", GLYPH_ICON, 623) X_GLYPH_DEFINE("\\{keyboard l_ctrl}", GLYPH_ICON, 624) X_GLYPH_DEFINE("\\{keyboard r_ctrl}", GLYPH_ICON, 625) X_GLYPH_DEFINE("\\{keyboard r_shift}", GLYPH_ICON, 626) X_GLYPH_DEFINE("\\{keyboard l_shift}", GLYPH_ICON, 627) X_GLYPH_DEFINE("\\{keyboard r_alt}", GLYPH_ICON, 628) X_GLYPH_DEFINE("\\{keyboard l_alt}", GLYPH_ICON, 629) X_GLYPH_DEFINE("\\{keyboard l_win}", GLYPH_ICON, 630) X_GLYPH_DEFINE("\\{keyboard r_win}", GLYPH_ICON, 631) X_GLYPH_DEFINE("\\{keyboard escape}", GLYPH_ICON, 632) X_GLYPH_DEFINE("\\{keyboard tab}", GLYPH_ICON, 633) X_GLYPH_DEFINE("\\{keyboard space}", GLYPH_ICON, 634) X_GLYPH_DEFINE("\\{keyboard pause}", GLYPH_ICON, 635) X_GLYPH_DEFINE("\\{keyboard home}", GLYPH_ICON, 636) X_GLYPH_DEFINE("\\{keyboard page_up}", GLYPH_ICON, 637) X_GLYPH_DEFINE("\\{keyboard delete}", GLYPH_ICON, 638) X_GLYPH_DEFINE("\\{keyboard end}", GLYPH_ICON, 639) X_GLYPH_DEFINE("\\{keyboard page_down}", GLYPH_ICON, 640) X_GLYPH_DEFINE("\\{keyboard f10}", GLYPH_ICON, 641) X_GLYPH_DEFINE("\\{keyboard f11}", GLYPH_ICON, 642) X_GLYPH_DEFINE("\\{keyboard f12}", GLYPH_ICON, 643) X_GLYPH_DEFINE("\\{keyboard f13}", GLYPH_ICON, 644) X_GLYPH_DEFINE("\\{keyboard f14}", GLYPH_ICON, 645) X_GLYPH_DEFINE("\\{keyboard f15}", GLYPH_ICON, 646) X_GLYPH_DEFINE("\\{keyboard f16}", GLYPH_ICON, 647) X_GLYPH_DEFINE("\\{keyboard f17}", GLYPH_ICON, 648) X_GLYPH_DEFINE("\\{keyboard f18}", GLYPH_ICON, 649) X_GLYPH_DEFINE("\\{keyboard f19}", GLYPH_ICON, 650) X_GLYPH_DEFINE("\\{keyboard f20}", GLYPH_ICON, 651) X_GLYPH_DEFINE("\\{keyboard f21}", GLYPH_ICON, 652) X_GLYPH_DEFINE("\\{keyboard f22}", GLYPH_ICON, 653) X_GLYPH_DEFINE("\\{keyboard f23}", GLYPH_ICON, 654) X_GLYPH_DEFINE("\\{keyboard f24}", GLYPH_ICON, 655) X_GLYPH_DEFINE("\\{keyboard num_0}", GLYPH_ICON, 656) X_GLYPH_DEFINE("\\{keyboard num_1}", GLYPH_ICON, 657) X_GLYPH_DEFINE("\\{keyboard num_2}", GLYPH_ICON, 658) X_GLYPH_DEFINE("\\{keyboard num_3}", GLYPH_ICON, 659) X_GLYPH_DEFINE("\\{keyboard num_4}", GLYPH_ICON, 660) X_GLYPH_DEFINE("\\{keyboard num_5}", GLYPH_ICON, 661) X_GLYPH_DEFINE("\\{keyboard num_6}", GLYPH_ICON, 662) X_GLYPH_DEFINE("\\{keyboard num_7}", GLYPH_ICON, 663) X_GLYPH_DEFINE("\\{keyboard num_8}", GLYPH_ICON, 664) X_GLYPH_DEFINE("\\{keyboard num_9}", GLYPH_ICON, 665) X_GLYPH_DEFINE("\\{keyboard num_period}", GLYPH_ICON, 666) X_GLYPH_DEFINE("\\{keyboard num_divide}", GLYPH_ICON, 667) X_GLYPH_DEFINE("\\{keyboard num_multiply}", GLYPH_ICON, 668) X_GLYPH_DEFINE("\\{keyboard num_minus}", GLYPH_ICON, 669) X_GLYPH_DEFINE("\\{keyboard num_plus}", GLYPH_ICON, 670) X_GLYPH_DEFINE("\\{keyboard num_equals}", GLYPH_ICON, 671) X_GLYPH_DEFINE("\\{keyboard num_comma}", GLYPH_ICON, 672) X_GLYPH_DEFINE("\\{keyboard num_enter}", GLYPH_ICON, 673) X_GLYPH_DEFINE("\\{keyboard unknown}", GLYPH_ICON, 674) X_GLYPH_DEFINE("\\{keyboard f1}", GLYPH_ICON, 675) X_GLYPH_DEFINE("\\{keyboard f2}", GLYPH_ICON, 676) X_GLYPH_DEFINE("\\{keyboard f3}", GLYPH_ICON, 677) X_GLYPH_DEFINE("\\{keyboard f4}", GLYPH_ICON, 678) X_GLYPH_DEFINE("\\{keyboard f5}", GLYPH_ICON, 679) X_GLYPH_DEFINE("\\{keyboard f6}", GLYPH_ICON, 680) X_GLYPH_DEFINE("\\{keyboard f7}", GLYPH_ICON, 681) X_GLYPH_DEFINE("\\{keyboard f8}", GLYPH_ICON, 682) X_GLYPH_DEFINE("\\{keyboard f9}", GLYPH_ICON, 683) X_GLYPH_DEFINE("\\{keyboard left}", GLYPH_ICON, 684) X_GLYPH_DEFINE("\\{keyboard up}", GLYPH_ICON, 685) X_GLYPH_DEFINE("\\{keyboard right}", GLYPH_ICON, 686) X_GLYPH_DEFINE("\\{keyboard down}", GLYPH_ICON, 687) X_GLYPH_DEFINE("\\{keyboard a}", GLYPH_ICON, 688) X_GLYPH_DEFINE("\\{keyboard b}", GLYPH_ICON, 689) X_GLYPH_DEFINE("\\{keyboard c}", GLYPH_ICON, 690) X_GLYPH_DEFINE("\\{keyboard d}", GLYPH_ICON, 691) X_GLYPH_DEFINE("\\{keyboard e}", GLYPH_ICON, 692) X_GLYPH_DEFINE("\\{keyboard f}", GLYPH_ICON, 693) X_GLYPH_DEFINE("\\{keyboard g}", GLYPH_ICON, 694) X_GLYPH_DEFINE("\\{keyboard h}", GLYPH_ICON, 695) X_GLYPH_DEFINE("\\{keyboard i}", GLYPH_ICON, 696) X_GLYPH_DEFINE("\\{keyboard j}", GLYPH_ICON, 697) X_GLYPH_DEFINE("\\{keyboard k}", GLYPH_ICON, 698) X_GLYPH_DEFINE("\\{keyboard l}", GLYPH_ICON, 699) X_GLYPH_DEFINE("\\{keyboard m}", GLYPH_ICON, 700) X_GLYPH_DEFINE("\\{keyboard n}", GLYPH_ICON, 701) X_GLYPH_DEFINE("\\{keyboard o}", GLYPH_ICON, 702) X_GLYPH_DEFINE("\\{keyboard p}", GLYPH_ICON, 703) X_GLYPH_DEFINE("\\{keyboard q}", GLYPH_ICON, 704) X_GLYPH_DEFINE("\\{keyboard r}", GLYPH_ICON, 705) X_GLYPH_DEFINE("\\{keyboard s}", GLYPH_ICON, 706) X_GLYPH_DEFINE("\\{keyboard t}", GLYPH_ICON, 707) X_GLYPH_DEFINE("\\{keyboard u}", GLYPH_ICON, 708) X_GLYPH_DEFINE("\\{keyboard v}", GLYPH_ICON, 709) X_GLYPH_DEFINE("\\{keyboard w}", GLYPH_ICON, 710) X_GLYPH_DEFINE("\\{keyboard x}", GLYPH_ICON, 711) X_GLYPH_DEFINE("\\{keyboard y}", GLYPH_ICON, 712) X_GLYPH_DEFINE("\\{keyboard z}", GLYPH_ICON, 713) X_GLYPH_DEFINE("\\{keyboard 0}", GLYPH_ICON, 714) X_GLYPH_DEFINE("\\{keyboard 1}", GLYPH_ICON, 715) X_GLYPH_DEFINE("\\{keyboard 2}", GLYPH_ICON, 716) X_GLYPH_DEFINE("\\{keyboard 3}", GLYPH_ICON, 717) X_GLYPH_DEFINE("\\{keyboard 4}", GLYPH_ICON, 718) X_GLYPH_DEFINE("\\{keyboard 5}", GLYPH_ICON, 719) X_GLYPH_DEFINE("\\{keyboard 6}", GLYPH_ICON, 720) X_GLYPH_DEFINE("\\{keyboard 7}", GLYPH_ICON, 721) X_GLYPH_DEFINE("\\{keyboard 8}", GLYPH_ICON, 722) X_GLYPH_DEFINE("\\{keyboard 9}", GLYPH_ICON, 723) X_GLYPH_DEFINE("\\{keyboard minus}", GLYPH_ICON, 724) X_GLYPH_DEFINE("\\{keyboard equals}", GLYPH_ICON, 725) X_GLYPH_DEFINE("\\{keyboard left_square_bracket}", GLYPH_ICON, 726) X_GLYPH_DEFINE("\\{keyboard right_square_bracket}", GLYPH_ICON, 727) X_GLYPH_DEFINE("\\{keyboard backslash}", GLYPH_ICON, 728) X_GLYPH_DEFINE("\\{keyboard hash}", GLYPH_ICON, 729) X_GLYPH_DEFINE("\\{keyboard semicolon}", GLYPH_ICON, 730) X_GLYPH_DEFINE("\\{keyboard apostrophe}", GLYPH_ICON, 731) X_GLYPH_DEFINE("\\{keyboard backtick}", GLYPH_ICON, 732) X_GLYPH_DEFINE("\\{keyboard comma}", GLYPH_ICON, 733) X_GLYPH_DEFINE("\\{keyboard period}", GLYPH_ICON, 734) X_GLYPH_DEFINE("\\{keyboard slash}", GLYPH_ICON, 735) X_GLYPH_DEFINE("\\{controller rstick}", GLYPH_ICON, 736) X_GLYPH_DEFINE("\\{controller rstick up}", GLYPH_ICON, 737) X_GLYPH_DEFINE("\\{controller rstick right}", GLYPH_ICON, 738) X_GLYPH_DEFINE("\\{controller rstick down}", GLYPH_ICON, 739) X_GLYPH_DEFINE("\\{controller rstick left}", GLYPH_ICON, 740) X_GLYPH_DEFINE("\\{controller lstick}", GLYPH_ICON, 741) X_GLYPH_DEFINE("\\{controller lstick up}", GLYPH_ICON, 742) X_GLYPH_DEFINE("\\{controller lstick right}", GLYPH_ICON, 743) X_GLYPH_DEFINE("\\{controller lstick down}", GLYPH_ICON, 744) X_GLYPH_DEFINE("\\{controller lstick left}", GLYPH_ICON, 745) X_GLYPH_DEFINE("\\{controller dpad up}", GLYPH_ICON, 746) X_GLYPH_DEFINE("\\{controller dpad right}", GLYPH_ICON, 747) X_GLYPH_DEFINE("\\{controller dpad down}", GLYPH_ICON, 748) X_GLYPH_DEFINE("\\{controller dpad left}", GLYPH_ICON, 749) X_GLYPH_DEFINE("\\{controller button l1}", GLYPH_ICON, 750) X_GLYPH_DEFINE("\\{controller button r1}", GLYPH_ICON, 751) X_GLYPH_DEFINE("\\{controller button l2}", GLYPH_ICON, 752) X_GLYPH_DEFINE("\\{controller button r2}", GLYPH_ICON, 753) X_GLYPH_DEFINE("\\{controller bumper left}", GLYPH_ICON, 754) X_GLYPH_DEFINE("\\{controller bumper right}", GLYPH_ICON, 755) X_GLYPH_DEFINE("\\{controller button zl}", GLYPH_ICON, 756) X_GLYPH_DEFINE("\\{controller button zr}", GLYPH_ICON, 757) X_GLYPH_DEFINE("\\{controller trigger left}", GLYPH_ICON, 758) X_GLYPH_DEFINE("\\{controller trigger right}", GLYPH_ICON, 759) X_GLYPH_DEFINE("\\{controller button a}", GLYPH_ICON, 760) X_GLYPH_DEFINE("\\{controller button b}", GLYPH_ICON, 761) X_GLYPH_DEFINE("\\{controller button x}", GLYPH_ICON, 762) X_GLYPH_DEFINE("\\{controller button y}", GLYPH_ICON, 763) X_GLYPH_DEFINE("\\{controller button xbox}", GLYPH_ICON, 764) X_GLYPH_DEFINE("\\{controller button triangle}", GLYPH_ICON, 765) X_GLYPH_DEFINE("\\{controller button square}", GLYPH_ICON, 766) X_GLYPH_DEFINE("\\{controller button cross}", GLYPH_ICON, 767) X_GLYPH_DEFINE("\\{controller button circle}", GLYPH_ICON, 768) X_GLYPH_DEFINE("\\{controller button ps}", GLYPH_ICON, 769) X_GLYPH_DEFINE("\\{controller button capture}", GLYPH_ICON, 770) X_GLYPH_DEFINE("\\{controller button touchpad}", GLYPH_ICON, 771) X_GLYPH_DEFINE("\\{controller button paddle 1}", GLYPH_ICON, 772) X_GLYPH_DEFINE("\\{controller button paddle 2}", GLYPH_ICON, 773) X_GLYPH_DEFINE("\\{controller button paddle 3}", GLYPH_ICON, 774) X_GLYPH_DEFINE("\\{controller button paddle 4}", GLYPH_ICON, 775) X_GLYPH_DEFINE("\\{controller button share}", GLYPH_ICON, 776) X_GLYPH_DEFINE("\\{controller button back}", GLYPH_ICON, 777) X_GLYPH_DEFINE("\\{controller button start}", GLYPH_ICON, 778) X_GLYPH_DEFINE("\\{controller button mic}", GLYPH_ICON, 779) X_GLYPH_DEFINE("\\{controller button home}", GLYPH_ICON, 780) X_GLYPH_DEFINE("\\{controller button options}", GLYPH_ICON, 781) ================================================ FILE: src/trx/game/ui.h ================================================ #pragma once #include #include #include #include #include #include #include ================================================ FILE: src/trx/game/viewport.c ================================================ #include #include #include #include #include #include #define L_DEFAULT_VIEWPORT \ { .width = SHELL_HEADLESS_WIDTH, .height = SHELL_HEADLESS_HEIGHT } static VIEWPORT_RECT m_Rects[VIEWPORT_NUMBER_OF] = { [VIEWPORT_WINDOW] = L_DEFAULT_VIEWPORT, [VIEWPORT_TARGET] = L_DEFAULT_VIEWPORT, [VIEWPORT_GAME] = L_DEFAULT_VIEWPORT, [VIEWPORT_UI] = L_DEFAULT_VIEWPORT, }; #undef L_DEFAULT_VIEWPORT static int16_t m_CurrentFOV = 65; static FOV_MODE m_CurrentFOVMode = FOV_MODE_GAME; void Viewport_Init(int32_t x, int32_t y, int32_t width, int32_t height) { const VIEWPORT_RECT *const target = &m_Rects[VIEWPORT_TARGET]; VIEWPORT_RECT *const game = &m_Rects[VIEWPORT_GAME]; VIEWPORT_RECT *const ui = &m_Rects[VIEWPORT_UI]; if (x < 0 || y < 0 || width < 0 || height < 0) { struct { int32_t w, h; } ar = { .w = 1, .h = 1 }; switch (g_Config.rendering.aspect_mode) { case ASPECT_MODE_4_3: ar.w = 4; ar.h = 3; break; case ASPECT_MODE_16_9: ar.w = 16; ar.h = 9; break; case ASPECT_MODE_16_10: ar.w = 16; ar.h = 10; break; case ASPECT_MODE_ANY: ar.w = target->width; ar.h = target->height; break; } x = 0; y = 0; width = target->width; height = target->height; if (g_Config.rendering.aspect_mode != ASPECT_MODE_ANY) { width = height * ar.w / ar.h; } } ui->x = x; ui->y = y; ui->width = width; ui->height = height; game->x = x; game->y = y; game->width = width / g_Config.rendering.upscaling_factor; game->height = height / g_Config.rendering.upscaling_factor; g_PhdLeft = Viewport_GetMinX(VIEWPORT_GAME); g_PhdTop = Viewport_GetMinY(VIEWPORT_GAME); g_PhdRight = Viewport_GetMaxX(VIEWPORT_GAME); g_PhdBottom = Viewport_GetMaxY(VIEWPORT_GAME); } int16_t Viewport_GetSystemFOV(void) { return m_CurrentFOV; } int16_t Viewport_GetUserFOV(void) { return g_Config.visuals.fov * DEG_1; } int16_t Viewport_GetEffectiveFOV(void) { return Viewport_GetSystemFOV() != -1 ? Viewport_GetSystemFOV() : Viewport_GetUserFOV(); } int32_t Viewport_GetMinX(const VIEWPORT_SPACE space) { return m_Rects[space].x; } int32_t Viewport_GetMinY(const VIEWPORT_SPACE space) { return m_Rects[space].y; } int32_t Viewport_GetMaxX(const VIEWPORT_SPACE space) { return m_Rects[space].x + m_Rects[space].width; } int32_t Viewport_GetMaxY(const VIEWPORT_SPACE space) { return m_Rects[space].y + m_Rects[space].height; } int32_t Viewport_GetCenterX(const VIEWPORT_SPACE space) { return (m_Rects[space].x + m_Rects[space].width) / 2; } int32_t Viewport_GetCenterY(const VIEWPORT_SPACE space) { return (m_Rects[space].y + m_Rects[space].height) / 2; } int32_t Viewport_GetWidth(const VIEWPORT_SPACE space) { return m_Rects[space].width; } int32_t Viewport_GetHeight(const VIEWPORT_SPACE space) { return m_Rects[space].height; } VIEWPORT_RECT Viewport_GetRect(const VIEWPORT_SPACE space) { return m_Rects[space]; } void Viewport_Reset(void) { const SHELL_SIZE size = Shell_GetCurrentSize(); VIEWPORT_RECT *const window = &m_Rects[VIEWPORT_WINDOW]; VIEWPORT_RECT *const target = &m_Rects[VIEWPORT_TARGET]; window->x = 0; window->y = 0; window->width = size.w; window->height = size.h; int32_t border_x = window->width * g_Config.rendering.borders; const int32_t border_y = window->height * g_Config.rendering.borders; if (g_Config.rendering.aspect_mode == ASPECT_MODE_ANY) { border_x = border_y; } const int32_t max_w = window->width - border_x; const int32_t max_h = window->height - border_y; double aspect_ratio = 0.0; switch (g_Config.rendering.aspect_mode) { case ASPECT_MODE_4_3: aspect_ratio = 4.0 / 3.0; break; case ASPECT_MODE_16_9: aspect_ratio = 16.0 / 9.0; break; case ASPECT_MODE_16_10: aspect_ratio = 16.0 / 10.0; break; case ASPECT_MODE_ANY: default: aspect_ratio = (double)max_w / (double)max_h; // just match window break; } // Fit the aspect ratio rectangle within max_w x max_h target->width = max_w; target->height = max_w / aspect_ratio; if (target->height > max_h) { // too tall, clamp target->height = max_h; target->width = max_h * aspect_ratio; } target->x = (window->width - target->width) / 2; target->y = (window->height - target->height) / 2; Viewport_Init(-1, -1, -1, -1); Viewport_Debug(); } FOV_MODE Viewport_GetFOVMode(void) { return m_CurrentFOVMode; } void Viewport_AlterFOV(const int16_t fov, const FOV_MODE fov_mode) { m_CurrentFOV = fov; m_CurrentFOVMode = fov_mode; } void Viewport_Debug(void) { const VIEWPORT_RECT *r; r = &m_Rects[VIEWPORT_WINDOW]; LOG_TRACE("Window viewport: %dx%d+%d,%d", r->width, r->height, r->x, r->y); r = &m_Rects[VIEWPORT_TARGET]; LOG_TRACE("Target viewport: %dx%d+%d,%d", r->width, r->height, r->x, r->y); r = &m_Rects[VIEWPORT_GAME]; LOG_TRACE("Game viewport: %dx%d+%d,%d", r->width, r->height, r->x, r->y); r = &m_Rects[VIEWPORT_UI]; LOG_TRACE("UI viewport: %dx%d+%d,%d", r->width, r->height, r->x, r->y); } ================================================ FILE: src/trx/game/viewport.h ================================================ #pragma once #include #include typedef enum { FOV_MODE_VERTICAL, FOV_MODE_HORIZONTAL, FOV_MODE_PC, FOV_MODE_PS1, } FOV_MODE; typedef enum { VIEWPORT_WINDOW, VIEWPORT_TARGET, VIEWPORT_GAME, VIEWPORT_UI, VIEWPORT_NUMBER_OF, } VIEWPORT_SPACE; typedef struct { int32_t x; int32_t y; union { int32_t width, w; }; union { int32_t height, h; }; } VIEWPORT_RECT; void Viewport_Init(int32_t x, int32_t y, int32_t width, int32_t height); int32_t Viewport_GetWidth(VIEWPORT_SPACE space); int32_t Viewport_GetHeight(VIEWPORT_SPACE space); int32_t Viewport_GetMinX(VIEWPORT_SPACE space); int32_t Viewport_GetMinY(VIEWPORT_SPACE space); int32_t Viewport_GetMaxX(VIEWPORT_SPACE space); int32_t Viewport_GetMaxY(VIEWPORT_SPACE space); int32_t Viewport_GetCenterX(VIEWPORT_SPACE space); int32_t Viewport_GetCenterY(VIEWPORT_SPACE space); VIEWPORT_RECT Viewport_GetRect(VIEWPORT_SPACE space); // Return the current FOV as overriden by the game mechanics, such as special // cameras or cutscenes. If the FOV is not overriden, returns -1. int16_t Viewport_GetSystemFOV(void); // Returns preferred player FOV. int16_t Viewport_GetUserFOV(void); // Returns the current effective FOV – eg system FOV if it's defined, otherwise // the player choice. int16_t Viewport_GetEffectiveFOV(void); // Returns the current FOV formula. FOV_MODE Viewport_GetFOVMode(void); // Sets the system FOV. Set to -1 to fallback to player FOV. void Viewport_AlterFOV(int16_t view_angle, FOV_MODE fov_mode); // TODO: decide what to do with this function void Viewport_Reset(void); void Viewport_Debug(void); ================================================ FILE: src/trx/gl/buffer.c ================================================ #include #include #include #include void TRX_GL_Buffer_Init(TRX_GL_BUFFER *buf, GLenum target) { ASSERT(buf != nullptr); buf->target = target; glGenBuffers(1, &buf->id); TRX_GL_CheckError(); buf->initialized = true; } void TRX_GL_Buffer_Close(TRX_GL_BUFFER *buf) { ASSERT(buf != nullptr); if (buf->initialized) { glDeleteBuffers(1, &buf->id); TRX_GL_CheckError(); } buf->initialized = false; } void TRX_GL_Buffer_Bind(TRX_GL_BUFFER *buf) { ASSERT(buf != nullptr); ASSERT(buf->initialized); glBindBuffer(buf->target, buf->id); TRX_GL_CheckError(); } void TRX_GL_Buffer_Data( TRX_GL_BUFFER *buf, GLsizei size, const void *data, GLenum usage) { ASSERT(buf != nullptr); ASSERT(buf->initialized); TRX_GL_TRACK_DATA(glBufferData, buf->target, size, data, usage); TRX_GL_CheckError(); } void TRX_GL_Buffer_SubData( TRX_GL_BUFFER *buf, GLsizei offset, GLsizei size, const void *data) { ASSERT(buf != nullptr); ASSERT(buf->initialized); TRX_GL_TRACK_SUBDATA(glBufferSubData, buf->target, offset, size, data); TRX_GL_CheckError(); } void *TRX_GL_Buffer_Map(TRX_GL_BUFFER *buf, GLenum access) { ASSERT(buf != nullptr); ASSERT(buf->initialized); void *ret = glMapBuffer(buf->target, access); TRX_GL_CheckError(); return ret; } void TRX_GL_Buffer_Unmap(TRX_GL_BUFFER *buf) { ASSERT(buf != nullptr); ASSERT(buf->initialized); glUnmapBuffer(buf->target); TRX_GL_CheckError(); } GLint TRX_GL_Buffer_Parameter(TRX_GL_BUFFER *buf, GLenum pname) { ASSERT(buf != nullptr); ASSERT(buf->initialized); GLint params = 0; glGetBufferParameteriv(buf->target, pname, ¶ms); TRX_GL_CheckError(); return params; } ================================================ FILE: src/trx/gl/buffer.h ================================================ #pragma once #include typedef struct { bool initialized; GLuint id; GLenum target; } TRX_GL_BUFFER; void TRX_GL_Buffer_Init(TRX_GL_BUFFER *buf, GLenum target); void TRX_GL_Buffer_Close(TRX_GL_BUFFER *buf); void TRX_GL_Buffer_Bind(TRX_GL_BUFFER *buf); void TRX_GL_Buffer_Data( TRX_GL_BUFFER *buf, GLsizei size, const void *data, GLenum usage); void TRX_GL_Buffer_SubData( TRX_GL_BUFFER *buf, GLsizei offset, GLsizei size, const void *data); void *TRX_GL_Buffer_Map(TRX_GL_BUFFER *buf, GLenum access); void TRX_GL_Buffer_Unmap(TRX_GL_BUFFER *buf); GLint TRX_GL_Buffer_Parameter(TRX_GL_BUFFER *buf, GLenum pname); ================================================ FILE: src/trx/gl/config.h ================================================ #pragma once #include #include typedef struct { TEXTURE_FILTER display_filter; bool enable_wireframe; int32_t line_width; } TRX_GL_CONFIG; ================================================ FILE: src/trx/gl/context.c ================================================ #include #include #include #include #include #include #include #include #include #include #include typedef struct { SDL_GLContext context; SDL_Window *window_handle; VIEWPORT_SPACE space; TRX_GL_CONFIG config; // Size of the SDL window. int32_t window_width; int32_t window_height; char *scheduled_screenshot_path; TRX_GL_RENDERER *renderer; } TRX_GL_CONTEXT; extern RGBA_F Output_GetFogColor(void); static TRX_GL_CONTEXT m_Context = {}; static bool M_IsExtensionSupported(const char *name) { int number_of_extensions; glGetIntegerv(GL_NUM_EXTENSIONS, &number_of_extensions); TRX_GL_CheckError(); for (int i = 0; i < number_of_extensions; i++) { const char *gl_ext = (const char *)glGetStringi(GL_EXTENSIONS, i); TRX_GL_CheckError(); if (gl_ext && !strcmp(gl_ext, name)) { return true; } } return false; } static GLvoid GLAPIENTRY M_GLDebug( const GLenum source, const GLenum type, const GLuint id, const GLenum severity, const GLsizei length, const GLchar *const message, const void *const user_param) { if (severity == GL_DEBUG_SEVERITY_NOTIFICATION) { return; } size_t len = strlen(message); if (len > 0 && message[len - 1] == '\n') { len--; } LOG_INFO("%d %*s", source, len, message); } void TRX_GL_Context_SwitchToViewport(const VIEWPORT_SPACE space) { const VIEWPORT_RECT rect = Viewport_GetRect(space); m_Context.space = space; glViewport(rect.x, rect.y, rect.width, rect.height); TRX_GL_CheckError(); } bool TRX_GL_Context_Attach(void *window_handle) { const char *shading_ver; if (m_Context.window_handle) { LOG_ERROR("Context already attached"); return false; } LOG_INFO("Attaching to window %p", window_handle); m_Context.context = SDL_GL_CreateContext(window_handle); if (m_Context.context == nullptr) { LOG_ERROR("Can't create OpenGL context: %s", SDL_GetError()); return false; } m_Context.config.line_width = 1; m_Context.config.enable_wireframe = false; SDL_GetWindowSize( window_handle, &m_Context.window_width, &m_Context.window_height); m_Context.window_handle = window_handle; if (SDL_GL_MakeCurrent(m_Context.window_handle, m_Context.context)) { Shell_ExitSystemFmt( "Can't activate OpenGL context: %s", SDL_GetError()); } const GLenum err = glewInit(); if (err != GLEW_OK) { if (err != 4) { Shell_ExitSystemFmt( "Can't initialize GLEW for OpenGL extension loading: %d", err); } // https://github.com/nigels-com/glew/issues/417 LOG_WARNING("GLEW failed to init: %d", err); } LOG_INFO("OpenGL vendor string: %s", glGetString(GL_VENDOR)); LOG_INFO("OpenGL renderer string: %s", glGetString(GL_RENDERER)); LOG_INFO("OpenGL version string: %s", glGetString(GL_VERSION)); shading_ver = (const char *)glGetString(GL_SHADING_LANGUAGE_VERSION); if (shading_ver != nullptr) { LOG_INFO("Shading version string: %s", shading_ver); } else { TRX_GL_CheckError(); } glClearColor(0, 0, 0, 0); glClearDepth(1); TRX_GL_CheckError(); // VSync defaults to on unless user disabled it in runtime json SDL_GL_SetSwapInterval(1); #if DEBUG if (glDebugMessageCallback != nullptr) { glDebugMessageCallback(M_GLDebug, nullptr); } glEnable(GL_DEBUG_OUTPUT); #endif m_Context.renderer = &g_TRX_GL_Renderer; if (m_Context.renderer->init != nullptr) { m_Context.renderer->init(m_Context.renderer, &m_Context.config); } return true; } void TRX_GL_Context_Detach(void) { if (!m_Context.window_handle) { return; } if (m_Context.renderer != nullptr && m_Context.renderer->shutdown != nullptr) { m_Context.renderer->shutdown(m_Context.renderer); } SDL_GL_MakeCurrent(nullptr, nullptr); if (m_Context.context != nullptr) { SDL_GL_DeleteContext(m_Context.context); m_Context.context = nullptr; } m_Context.window_handle = nullptr; } void TRX_GL_Context_SetDisplayFilter(const TEXTURE_FILTER filter) { m_Context.config.display_filter = filter; } bool TRX_GL_Context_GetWireframeMode(void) { return m_Context.config.enable_wireframe; } void TRX_GL_Context_SetWireframeMode(const bool enable) { m_Context.config.enable_wireframe = enable; } void TRX_GL_Context_SetLineWidth(const int32_t line_width) { m_Context.config.line_width = line_width; } void TRX_GL_Context_SetVSync(bool vsync) { SDL_GL_SetSwapInterval(vsync); } void *TRX_GL_Context_GetWindowHandle(void) { return m_Context.window_handle; } void TRX_GL_Context_Clear(void) { const RGBA_F white = { 1.0f, 1.0f, 1.0f, 0.0f }; const RGBA_F fog = Output_GetFogColor(); const RGBA_F black = { 0.0f, 0.0f, 0.0f, 0.0f }; const RGBA_F color = m_Context.space == VIEWPORT_GAME && m_Context.config.enable_wireframe ? white : m_Context.space == VIEWPORT_GAME ? fog : black; glClearBufferfv(GL_COLOR, 0, &color.r); } void TRX_GL_Context_SwapBuffers(void) { glFinish(); TRX_GL_CheckError(); if (m_Context.renderer != nullptr && m_Context.renderer->swap_buffers != nullptr) { m_Context.renderer->swap_buffers(m_Context.renderer); } } void TRX_GL_Context_ScheduleScreenshot(const char *path) { Memory_FreePointer(&m_Context.scheduled_screenshot_path); m_Context.scheduled_screenshot_path = Memory_DupStr(path); } const char *TRX_GL_Context_GetScheduledScreenshotPath(void) { return m_Context.scheduled_screenshot_path; } void TRX_GL_Context_ClearScheduledScreenshotPath(void) { Memory_FreePointer(&m_Context.scheduled_screenshot_path); } TRX_GL_CONFIG *TRX_GL_Context_GetConfig(void) { return &m_Context.config; } ================================================ FILE: src/trx/gl/context.h ================================================ #pragma once #include #include #include #include bool TRX_GL_Context_Attach(void *window_handle); void TRX_GL_Context_Detach(void); void TRX_GL_Context_SetDisplayFilter(TEXTURE_FILTER filter); bool TRX_GL_Context_GetWireframeMode(void); void TRX_GL_Context_SetWireframeMode(bool enable); void TRX_GL_Context_SetLineWidth(int32_t line_width); void TRX_GL_Context_SetVSync(bool vsync); void *TRX_GL_Context_GetWindowHandle(void); void TRX_GL_Context_Clear(void); void TRX_GL_Context_SwapBuffers(void); void TRX_GL_Context_SetRendered(void); void TRX_GL_Context_SwitchToViewport(VIEWPORT_SPACE space); void TRX_GL_Context_ScheduleScreenshot(const char *path); const char *TRX_GL_Context_GetScheduledScreenshotPath(void); void TRX_GL_Context_ClearScheduledScreenshotPath(void); TRX_GL_CONFIG *TRX_GL_Context_GetConfig(void); ================================================ FILE: src/trx/gl/enum.c ================================================ #include #include static __attribute__((constructor)) void M_Init(void) { ENUM_MAP(TEXTURE_FILTER, TEXTURE_FILTER_BILINEAR, "bilinear"); ENUM_MAP(TEXTURE_FILTER, TEXTURE_FILTER_POINT, "point"); } ================================================ FILE: src/trx/gl/enum.h ================================================ #pragma once typedef enum { TEXTURE_FILTER_POINT, TEXTURE_FILTER_BILINEAR, TEXTURE_FILTER_NUMBER_OF, } TEXTURE_FILTER; ================================================ FILE: src/trx/gl/fbo.c ================================================ #include #include #include #include #include #include #include #include #include #include #include void TRX_GL_FBO_Init( TRX_GL_FBO *const fbo, const int32_t width, const int32_t height, const GLint internal_format, const GLenum format, const bool with_depth_stencil) { fbo->width = width; fbo->height = height; fbo->internal_format = internal_format; fbo->format = format; fbo->with_depth_stencil = with_depth_stencil; ASSERT(width > 0); ASSERT(height > 0); // Allocate color texture (no mipmaps for FBO attachments). TRX_GL_Texture_Init(&fbo->texture, GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, fbo->texture.id); TRX_GL_CheckError(); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexImage2D( GL_TEXTURE_2D, 0, internal_format, width, height, 0, format, GL_UNSIGNED_BYTE, nullptr); glClearColor(0.0, 0.0, 0.0, 1.0); TRX_GL_CheckError(); glGenFramebuffers(1, &fbo->fbo); TRX_GL_CheckError(); glBindFramebuffer(GL_FRAMEBUFFER, fbo->fbo); TRX_GL_CheckError(); glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fbo->texture.id, 0); TRX_GL_CheckError(); // direct draw to color attachment 0. glDrawBuffer(GL_COLOR_ATTACHMENT0); TRX_GL_CheckError(); if (with_depth_stencil) { glGenRenderbuffers(1, &fbo->rbo); TRX_GL_CheckError(); glBindRenderbuffer(GL_RENDERBUFFER, fbo->rbo); TRX_GL_CheckError(); glRenderbufferStorage( GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height); TRX_GL_CheckError(); glBindRenderbuffer(GL_RENDERBUFFER, 0); TRX_GL_CheckError(); glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, fbo->rbo); TRX_GL_CheckError(); } else { fbo->rbo = 0; } if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { LOG_ERROR("framebuffer is not complete!"); } glBindFramebuffer(GL_FRAMEBUFFER, 0); } void TRX_GL_FBO_Close(TRX_GL_FBO *fbo) { if (fbo->rbo) { glDeleteRenderbuffers(1, &fbo->rbo); fbo->rbo = 0; } if (fbo->fbo) { glDeleteFramebuffers(1, &fbo->fbo); fbo->fbo = 0; } TRX_GL_Texture_Close(&fbo->texture); } void TRX_GL_FBO_ResizeIfNeeded( TRX_GL_FBO *const fbo, const int32_t width, const int32_t height) { if (width == fbo->width && height == fbo->height) { return; } const GLint internal_format = fbo->internal_format; const GLenum format = fbo->format; const bool with_depth_stencil = fbo->with_depth_stencil; TRX_GL_FBO_Close(fbo); TRX_GL_FBO_Init( fbo, width, height, internal_format, format, with_depth_stencil); } void TRX_GL_FBO_Bind(const TRX_GL_FBO *const fbo) { glBindFramebuffer(GL_FRAMEBUFFER, fbo->fbo); TRX_GL_CheckError(); } void TRX_GL_FBO_Unbind(void) { glBindFramebuffer(GL_FRAMEBUFFER, 0); TRX_GL_CheckError(); } ================================================ FILE: src/trx/gl/fbo.h ================================================ // Framebuffer object abstraction for off-screen rendering. #pragma once #include #include #include #include // Off-screen framebuffer with a single color attachment and optional // depth+stencil. typedef struct { int32_t width; int32_t height; GLuint fbo; GLuint rbo; GLint internal_format; GLenum format; bool with_depth_stencil; TRX_GL_TEXTURE texture; } TRX_GL_FBO; // Initialize an off-screen FBO. void TRX_GL_FBO_Init( TRX_GL_FBO *fbo, int32_t width, int32_t height, GLint internal_format, GLenum format, bool with_depth_stencil); // Close and free the GL resources. void TRX_GL_FBO_Close(TRX_GL_FBO *fbo); // Resize the FBO attachments (reallocate textures and RBO). void TRX_GL_FBO_ResizeIfNeeded(TRX_GL_FBO *fbo, int32_t width, int32_t height); // Bind this FBO for rendering (GL_FRAMEBUFFER). void TRX_GL_FBO_Bind(const TRX_GL_FBO *fbo); // Bind the default framebuffer (0). void TRX_GL_FBO_Unbind(void); ================================================ FILE: src/trx/gl/program.c ================================================ #include #include #include #include #include #include #include #include #include #include #include typedef struct { char *path; char *content; } M_SHADER_FILE_CACHE_ENTRY; static VECTOR *m_ShaderFileCache = nullptr; // M_SHADER_FILE_CACHE_ENTRY static const char *M_LoadFileCached(const char *const path) { ASSERT(path != nullptr); if (m_ShaderFileCache == nullptr) { m_ShaderFileCache = Vector_Create(sizeof(M_SHADER_FILE_CACHE_ENTRY)); } for (int32_t i = 0; i < m_ShaderFileCache->count; i++) { M_SHADER_FILE_CACHE_ENTRY *const entry = Vector_Get(m_ShaderFileCache, i); if (strcmp(entry->path, path) == 0) { return entry->content; } } char *content = nullptr; if (!File_Load(path, &content, nullptr)) { return nullptr; } M_SHADER_FILE_CACHE_ENTRY entry = { .path = Memory_DupStr(path), .content = content, }; Vector_Add(m_ShaderFileCache, &entry); return entry.content; } __attribute__((destructor)) static void M_ShutdownCache(void) { if (m_ShaderFileCache == nullptr) { return; } for (int32_t i = 0; i < m_ShaderFileCache->count; i++) { M_SHADER_FILE_CACHE_ENTRY *const entry = Vector_Get(m_ShaderFileCache, i); Memory_FreePointer(&entry->path); Memory_FreePointer(&entry->content); } Vector_Free(m_ShaderFileCache); m_ShaderFileCache = nullptr; } static char *M_PreprocessIncludes(const char *src, const char *dir) { ASSERT(src != nullptr); ASSERT(dir != nullptr); const char *p = src; size_t result_cap = strlen(src) + 1; char *result = Memory_Alloc(result_cap); size_t used = 0; result[0] = '\0'; while (*p != '\0') { const char *include = strstr(p, "#include"); if (include == nullptr) { size_t tail_len = strlen(p); if (used + tail_len + 1 > result_cap) { result_cap = (used + tail_len + 1) * 2; result = Memory_Realloc(result, result_cap); } memcpy(result + used, p, tail_len); used += tail_len; result[used] = '\0'; break; } // Copy text before #include size_t prefix_len = include - p; if (prefix_len > 0) { if (used + prefix_len + 1 > result_cap) { result_cap = (used + prefix_len + 1) * 2; result = Memory_Realloc(result, result_cap); } memcpy(result + used, p, prefix_len); used += prefix_len; result[used] = '\0'; } // Parse filename between quotes const char *start_quote = strchr(include, '"'); const char *end_quote = start_quote ? strchr(start_quote + 1, '"') : nullptr; if (!start_quote || !end_quote) { Shell_ExitSystemFmt( "Malformed #include directive near: %.32s", include); } char filename[512]; strncpy(filename, start_quote + 1, end_quote - start_quote - 1); filename[end_quote - start_quote - 1] = '\0'; // Build relative path char full_path[1024]; snprintf(full_path, sizeof(full_path), "%s/%s", dir, filename); const char *const include_src = M_LoadFileCached(full_path); if (include_src == nullptr) { Shell_ExitSystemFmt("Failed to include shader file: %s", full_path); } // Handle nested includes char *include_dir = File_GetParentDirectory(full_path); char *processed_include = M_PreprocessIncludes(include_src, include_dir ? include_dir : dir); Memory_FreePointer(&include_dir); // Append included content size_t block_len = strlen(processed_include); if (used + block_len + 1 > result_cap) { result_cap = (used + block_len + 1) * 2; result = Memory_Realloc(result, result_cap); } memcpy(result + used, processed_include, block_len); used += block_len; result[used] = '\0'; Memory_FreePointer(&processed_include); // Move past include line const char *next_line = strchr(end_quote, '\n'); p = next_line ? next_line + 1 : end_quote + 1; } result[used] = '\0'; return result; } static char *M_Preprocess(const char *content, GLenum type) { ASSERT(content != nullptr); const char *version_ogl33c = "#version 330 core\n"; const char *define_vertex = "#define VERTEX\n"; const char *define_fragment = "#define FRAGMENT\n"; size_t bufsize = strlen(content) + 1; bufsize += strlen(version_ogl33c); if (type == GL_VERTEX_SHADER) { bufsize += strlen(define_vertex); } else if (type == GL_FRAGMENT_SHADER) { bufsize += strlen(define_fragment); } char *processed_content = Memory_Alloc(bufsize); strcpy(processed_content, version_ogl33c); if (type == GL_VERTEX_SHADER) { strcat(processed_content, define_vertex); } else if (type == GL_FRAGMENT_SHADER) { strcat(processed_content, define_fragment); } strcat(processed_content, content); return processed_content; } bool TRX_GL_Program_Init(TRX_GL_PROGRAM *const program) { ASSERT(program != nullptr); program->id = glCreateProgram(); TRX_GL_CheckError(); if (!program->id) { LOG_ERROR("Can't create shader program"); return false; } return true; } void TRX_GL_Program_Close(TRX_GL_PROGRAM *const program) { ASSERT(program != nullptr); Memory_FreePointer(&program->path); if (program->id) { glDeleteProgram(program->id); TRX_GL_CheckError(); program->id = 0; } } void TRX_GL_Program_Bind(const TRX_GL_PROGRAM *const program) { ASSERT(program != nullptr); glUseProgram(program->id); TRX_GL_CheckError(); } void TRX_GL_Program_AttachShader( TRX_GL_PROGRAM *program, GLenum type, const char *path) { ASSERT(program != nullptr); ASSERT(path != nullptr); const char *const resolved_path = TRXPath_Resolve(TRX_DYNAMIC_PATH_SHADER_FILE, path); Memory_FreePointer(&program->path); program->path = Memory_DupStr(resolved_path); GLuint shader_id = glCreateShader(type); TRX_GL_CheckError(); if (!shader_id) { Shell_ExitSystem("Failed to create shader"); } const char *content = M_LoadFileCached(program->path); char *processed_content = nullptr; if (content == nullptr) { Shell_ExitSystemFmt("Unable to find shader file: %s", program->path); } char *shader_dir = File_GetParentDirectory(program->path); processed_content = M_PreprocessIncludes(content, shader_dir); ASSERT(processed_content != nullptr); Memory_FreePointer(&shader_dir); char *expanded_content = processed_content; processed_content = M_Preprocess(expanded_content, type); ASSERT(processed_content != nullptr); Memory_FreePointer(&expanded_content); glShaderSource( shader_id, 1, (const char *const *)&processed_content, nullptr); TRX_GL_CheckError(); glCompileShader(shader_id); TRX_GL_CheckError(); int compile_status; glGetShaderiv(shader_id, GL_COMPILE_STATUS, &compile_status); TRX_GL_CheckError(); if (compile_status != GL_TRUE) { GLsizei info_log_size = 4096; char info_log[info_log_size]; glGetShaderInfoLog(shader_id, info_log_size, &info_log_size, info_log); TRX_GL_CheckError(); if (info_log[0]) { Shell_ExitSystemFmt( "%s: compilation failed\n%s", program->path, info_log); } else { Shell_ExitSystemFmt("%s: compilation failed.", program->path); } } Memory_FreePointer(&processed_content); glAttachShader(program->id, shader_id); TRX_GL_CheckError(); glDeleteShader(shader_id); TRX_GL_CheckError(); } void TRX_GL_Program_Link(TRX_GL_PROGRAM *const program) { ASSERT(program != nullptr); glLinkProgram(program->id); TRX_GL_CheckError(); GLint linkStatus; glGetProgramiv(program->id, GL_LINK_STATUS, &linkStatus); TRX_GL_CheckError(); if (!linkStatus) { GLsizei info_log_size = 4096; char info_log[info_log_size]; glGetProgramInfoLog( program->id, info_log_size, &info_log_size, info_log); TRX_GL_CheckError(); if (info_log[0]) { Shell_ExitSystemFmt( "%s: shader linking failed\n%s", program->path, info_log); } else { Shell_ExitSystemFmt("%s: shader linking failed", program->path); } } } void TRX_GL_Program_FragmentData( TRX_GL_PROGRAM *const program, const char *const name) { ASSERT(program != nullptr); glBindFragDataLocation(program->id, 0, name); TRX_GL_CheckError(); } GLint TRX_GL_Program_UniformLocation( TRX_GL_PROGRAM *const program, const char *const name) { ASSERT(program != nullptr); GLint location = glGetUniformLocation(program->id, name); TRX_GL_CheckError(); if (location == -1) { LOG_INFO("%s: uniform not found (%s)", program->path, name); } return location; } void TRX_GL_Program_Uniform4f( TRX_GL_PROGRAM *const program, const GLint loc, const GLfloat v0, const GLfloat v1, const GLfloat v2, const GLfloat v3) { ASSERT(program != nullptr); TRX_GL_TRACK_UNIFORM(glUniform4f, loc, v0, v1, v2, v3); TRX_GL_CheckError(); } void TRX_GL_Program_Uniform1i( TRX_GL_PROGRAM *const program, const GLint loc, const GLint v0) { ASSERT(program != nullptr); TRX_GL_TRACK_UNIFORM(glUniform1i, loc, v0); TRX_GL_CheckError(); } void TRX_GL_Program_Uniform1f( TRX_GL_PROGRAM *const program, const GLint loc, const GLfloat v0) { ASSERT(program != nullptr); TRX_GL_TRACK_UNIFORM(glUniform1f, loc, v0); TRX_GL_CheckError(); } void TRX_GL_Program_Uniform2f( TRX_GL_PROGRAM *const program, const GLint loc, const GLfloat v0, const GLfloat v1) { ASSERT(program != nullptr); TRX_GL_TRACK_UNIFORM(glUniform2f, loc, v0, v1); TRX_GL_CheckError(); } ================================================ FILE: src/trx/gl/program.h ================================================ #pragma once #include #include typedef struct { char *path; bool initialized; GLuint id; } TRX_GL_PROGRAM; bool TRX_GL_Program_Init(TRX_GL_PROGRAM *program); void TRX_GL_Program_Close(TRX_GL_PROGRAM *program); void TRX_GL_Program_Bind(const TRX_GL_PROGRAM *program); void TRX_GL_Program_AttachShader( TRX_GL_PROGRAM *program, GLenum type, const char *path); void TRX_GL_Program_Link(TRX_GL_PROGRAM *program); void TRX_GL_Program_FragmentData(TRX_GL_PROGRAM *program, const char *name); GLint TRX_GL_Program_UniformLocation(TRX_GL_PROGRAM *program, const char *name); void TRX_GL_Program_Uniform4f( TRX_GL_PROGRAM *program, GLint loc, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3); void TRX_GL_Program_Uniform1i(TRX_GL_PROGRAM *program, GLint loc, GLint v0); void TRX_GL_Program_Uniform1f(TRX_GL_PROGRAM *program, GLint loc, GLfloat v0); void TRX_GL_Program_Uniform2f( TRX_GL_PROGRAM *program, GLint loc, GLfloat v0, GLfloat v1); ================================================ FILE: src/trx/gl/renderer.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef struct { const TRX_GL_CONFIG *config; TRX_GL_FBO geometry_fbo; TRX_GL_FBO ui_fbo; // Full-screen quad resources for blitting FBOs to default framebuffer. TRX_GL_VERTEX_ARRAY vertex_array; TRX_GL_BUFFER buffer; TRX_GL_SAMPLER sampler; TRX_GL_PROGRAM program; } M_CONTEXT; static void M_Blit(const M_CONTEXT *const p, const TRX_GL_FBO *const fbo) { TRX_GL_Texture_Bind(&fbo->texture); glDrawArrays(GL_TRIANGLES, 0, 6); TRX_GL_CheckError(); } static void M_UpdateFBOSizes(TRX_GL_RENDERER *renderer) { M_CONTEXT *const p = renderer->priv; const TRX_GL_CONFIG *const config = p->config; VIEWPORT_RECT rect; rect = Viewport_GetRect(VIEWPORT_GAME); TRX_GL_FBO_ResizeIfNeeded(&p->geometry_fbo, rect.width, rect.height); rect = Viewport_GetRect(VIEWPORT_UI); TRX_GL_FBO_ResizeIfNeeded(&p->ui_fbo, rect.width, rect.height); } static void M_Render(TRX_GL_RENDERER *renderer) { ASSERT(renderer != nullptr); M_CONTEXT *const p = renderer->priv; ASSERT(p != nullptr); const GLuint filter = p->config->display_filter == TEXTURE_FILTER_BILINEAR ? GL_LINEAR : GL_NEAREST; TRX_GL_FBO_Unbind(); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); TRX_GL_CheckError(); TRX_GL_Program_Bind(&p->program); TRX_GL_Buffer_Bind(&p->buffer); TRX_GL_VertexArray_Bind(&p->vertex_array); glActiveTexture(GL_TEXTURE0); glDisable(GL_DEPTH_TEST); TRX_GL_Sampler_Bind(&p->sampler, 0); TRX_GL_Sampler_Parameteri(&p->sampler, GL_TEXTURE_MAG_FILTER, filter); TRX_GL_Sampler_Parameteri(&p->sampler, GL_TEXTURE_MIN_FILTER, filter); VIEWPORT_RECT rect = Viewport_GetRect(VIEWPORT_TARGET); glViewport(rect.x, rect.y, rect.width, rect.height); TRX_GL_CheckError(); // Composite geometry FBO (opaque) glDisable(GL_BLEND); M_Blit(p, &p->geometry_fbo); // Composite UI FBO (with premultiplied alpha blending) glEnable(GL_BLEND); glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); M_Blit(p, &p->ui_fbo); glDisable(GL_BLEND); if (TRX_GL_Context_GetScheduledScreenshotPath() != nullptr) { TRX_GL_Context_SwitchToViewport(VIEWPORT_TARGET); TRX_GL_Screenshot_CaptureToFile( TRX_GL_Context_GetScheduledScreenshotPath()); TRX_GL_Context_ClearScheduledScreenshotPath(); } } static void M_SwapBuffers(TRX_GL_RENDERER *const renderer) { M_CONTEXT *const p = renderer->priv; M_Render(renderer); SDL_GL_SwapWindow(TRX_GL_Context_GetWindowHandle()); M_UpdateFBOSizes(renderer); TRX_GL_Context_SwitchToViewport(VIEWPORT_WINDOW); TRX_GL_Context_Clear(); // Rebind geometry FBO for the next frame TRX_GL_Renderer_BindGeometryFbo(); TRX_GL_Context_SwitchToViewport(VIEWPORT_GAME); TRX_GL_Context_Clear(); } static void M_Init( TRX_GL_RENDERER *const renderer, const TRX_GL_CONFIG *const config) { ASSERT(renderer != nullptr); renderer->priv = (M_CONTEXT *)Memory_Alloc(sizeof(M_CONTEXT)); M_CONTEXT *const p = renderer->priv; ASSERT(p != nullptr); p->config = config; TRX_GL_Buffer_Init(&p->buffer, GL_ARRAY_BUFFER); TRX_GL_Buffer_Bind(&p->buffer); const GLfloat verts[] = { 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, }; TRX_GL_Buffer_Data(&p->buffer, sizeof(verts), verts, GL_STATIC_DRAW); TRX_GL_VertexArray_Init(&p->vertex_array); TRX_GL_VertexArray_Bind(&p->vertex_array); TRX_GL_VertexArray_Attribute( &p->vertex_array, 0, 2, GL_FLOAT, GL_FALSE, 0, 0); TRX_GL_Sampler_Init(&p->sampler); TRX_GL_Sampler_Bind(&p->sampler, 0); TRX_GL_Sampler_Parameteri(&p->sampler, GL_TEXTURE_MAG_FILTER, GL_LINEAR); TRX_GL_Sampler_Parameteri(&p->sampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR); TRX_GL_Sampler_Parameteri(&p->sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); TRX_GL_Sampler_Parameteri(&p->sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); TRX_GL_Program_Init(&p->program); TRX_GL_Program_AttachShader(&p->program, GL_VERTEX_SHADER, "fbo.glsl"); TRX_GL_Program_AttachShader(&p->program, GL_FRAGMENT_SHADER, "fbo.glsl"); TRX_GL_Program_FragmentData(&p->program, "outColor"); TRX_GL_Program_Link(&p->program); TRX_GL_Program_Bind(&p->program); TRX_GL_Program_Uniform1i( &p->program, TRX_GL_Program_UniformLocation(&p->program, "uTex0"), 0); VIEWPORT_RECT rect; rect = Viewport_GetRect(VIEWPORT_GAME); TRX_GL_FBO_Init( &p->geometry_fbo, rect.width, rect.height, GL_RGBA8, GL_RGBA, true); rect = Viewport_GetRect(VIEWPORT_UI); TRX_GL_FBO_Init( &p->ui_fbo, rect.width, rect.height, GL_RGBA8, GL_RGBA, false); } static void M_Shutdown(TRX_GL_RENDERER *renderer) { LOG_INFO(""); ASSERT(renderer != nullptr); M_CONTEXT *const p = renderer->priv; ASSERT(p != nullptr); TRX_GL_FBO_Close(&p->geometry_fbo); TRX_GL_FBO_Close(&p->ui_fbo); TRX_GL_Program_Close(&p->program); TRX_GL_Sampler_Close(&p->sampler); TRX_GL_Buffer_Close(&p->buffer); TRX_GL_VertexArray_Close(&p->vertex_array); Memory_FreePointer(&renderer->priv); } TRX_GL_RENDERER g_TRX_GL_Renderer = { .swap_buffers = &M_SwapBuffers, .init = &M_Init, .shutdown = &M_Shutdown, }; void TRX_GL_Renderer_BindGeometryFbo(void) { M_CONTEXT *const p = (M_CONTEXT *)g_TRX_GL_Renderer.priv; TRX_GL_FBO_Bind(&p->geometry_fbo); } void TRX_GL_Renderer_BindUiFbo(void) { M_CONTEXT *const p = (M_CONTEXT *)g_TRX_GL_Renderer.priv; TRX_GL_FBO_Bind(&p->ui_fbo); } ================================================ FILE: src/trx/gl/renderer.h ================================================ #pragma once #include typedef struct TRX_GL_Renderer { void (*init)(struct TRX_GL_Renderer *renderer, const TRX_GL_CONFIG *config); void (*shutdown)(struct TRX_GL_Renderer *renderer); void (*swap_buffers)(struct TRX_GL_Renderer *renderer); void *priv; } TRX_GL_RENDERER; extern TRX_GL_RENDERER g_TRX_GL_Renderer; // Bind the geometry framebuffer for rendering the 3D scene. void TRX_GL_Renderer_BindGeometryFbo(void); // Bind the UI framebuffer for rendering the UI overlay. void TRX_GL_Renderer_BindUiFbo(void); ================================================ FILE: src/trx/gl/sampler.c ================================================ #include #include #include void TRX_GL_Sampler_Init(TRX_GL_SAMPLER *sampler) { ASSERT(sampler != nullptr); glGenSamplers(1, &sampler->id); TRX_GL_CheckError(); sampler->initialized = true; } void TRX_GL_Sampler_Close(TRX_GL_SAMPLER *sampler) { ASSERT(sampler != nullptr); if (sampler->initialized) { glDeleteSamplers(1, &sampler->id); TRX_GL_CheckError(); } sampler->initialized = false; } void TRX_GL_Sampler_Bind(TRX_GL_SAMPLER *sampler, GLuint unit) { ASSERT(sampler != nullptr); ASSERT(sampler->initialized); glBindSampler(unit, sampler->id); TRX_GL_CheckError(); } void TRX_GL_Sampler_Parameteri( TRX_GL_SAMPLER *sampler, GLenum pname, GLint param) { ASSERT(sampler != nullptr); ASSERT(sampler->initialized); glSamplerParameteri(sampler->id, pname, param); TRX_GL_CheckError(); } void TRX_GL_Sampler_Parameterf( TRX_GL_SAMPLER *sampler, GLenum pname, GLfloat param) { ASSERT(sampler != nullptr); ASSERT(sampler->initialized); glSamplerParameterf(sampler->id, pname, param); TRX_GL_CheckError(); } ================================================ FILE: src/trx/gl/sampler.h ================================================ #pragma once #include typedef struct { bool initialized; GLuint id; } TRX_GL_SAMPLER; void TRX_GL_Sampler_Init(TRX_GL_SAMPLER *sampler); void TRX_GL_Sampler_Close(TRX_GL_SAMPLER *sampler); void TRX_GL_Sampler_Bind(TRX_GL_SAMPLER *sampler, GLuint unit); void TRX_GL_Sampler_Parameteri( TRX_GL_SAMPLER *sampler, GLenum pname, GLint param); void TRX_GL_Sampler_Parameterf( TRX_GL_SAMPLER *sampler, GLenum pname, GLfloat param); ================================================ FILE: src/trx/gl/screenshot.c ================================================ #include #include #include #include #include #include bool TRX_GL_Screenshot_CaptureToFile(const char *path) { bool ret = false; GLint width; GLint height; TRX_GL_Screenshot_CaptureToBuffer( nullptr, &width, &height, 3, GL_RGB, GL_UNSIGNED_BYTE, true); IMAGE *image = Image_Create(width, height); ASSERT(image != nullptr); TRX_GL_Screenshot_CaptureToBuffer( (uint8_t *)image->data, &width, &height, 3, GL_RGB, GL_UNSIGNED_BYTE, true); ret = Image_SaveToFile(image, path); if (image) { Image_Free(image); } return ret; } void TRX_GL_Screenshot_CaptureToBuffer( uint8_t *out_buffer, GLint *out_width, GLint *out_height, GLint depth, GLenum format, GLenum type, bool vflip) { ASSERT(out_width != nullptr); ASSERT(out_height != nullptr); GLint viewport[4]; glGetIntegerv(GL_VIEWPORT, viewport); TRX_GL_CheckError(); GLint x = viewport[0]; GLint y = viewport[1]; *out_width = viewport[2]; *out_height = viewport[3]; if (!out_buffer) { return; } GLint pitch = *out_width * depth; glPixelStorei(GL_PACK_ALIGNMENT, 1); TRX_GL_CheckError(); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); TRX_GL_CheckError(); glReadBuffer(GL_BACK); TRX_GL_CheckError(); glReadPixels(x, y, *out_width, *out_height, format, type, out_buffer); TRX_GL_CheckError(); if (vflip) { uint8_t *scanline = Memory_Alloc(pitch); for (int y1 = 0, middle = *out_height / 2; y1 < middle; y1++) { int y2 = *out_height - 1 - y1; memcpy(scanline, &out_buffer[y1 * pitch], pitch); memcpy(&out_buffer[y1 * pitch], &out_buffer[y2 * pitch], pitch); memcpy(&out_buffer[y2 * pitch], scanline, pitch); } Memory_FreePointer(&scanline); } } ================================================ FILE: src/trx/gl/screenshot.h ================================================ #pragma once #include #include bool TRX_GL_Screenshot_CaptureToFile(const char *path); void TRX_GL_Screenshot_CaptureToBuffer( uint8_t *out_buffer, GLint *out_width, GLint *out_height, GLint depth, GLenum format, GLenum type, bool vflip); ================================================ FILE: src/trx/gl/texture.c ================================================ #include #include #include #include #include TRX_GL_TEXTURE *TRX_GL_Texture_Create(GLenum target) { TRX_GL_TEXTURE *texture = Memory_Alloc(sizeof(TRX_GL_TEXTURE)); TRX_GL_Texture_Init(texture, target); return texture; } void TRX_GL_Texture_Free(TRX_GL_TEXTURE *texture) { if (texture != nullptr) { TRX_GL_Texture_Close(texture); Memory_FreePointer(&texture); } } void TRX_GL_Texture_Init(TRX_GL_TEXTURE *texture, GLenum target) { ASSERT(texture != nullptr); texture->target = target; glGenTextures(1, &texture->id); TRX_GL_CheckError(); texture->initialized = true; } void TRX_GL_Texture_Close(TRX_GL_TEXTURE *texture) { ASSERT(texture != nullptr); if (texture->initialized) { glDeleteTextures(1, &texture->id); TRX_GL_CheckError(); } texture->initialized = false; } void TRX_GL_Texture_Bind(const TRX_GL_TEXTURE *texture) { ASSERT(texture != nullptr); ASSERT(texture->initialized); glBindTexture(texture->target, texture->id); TRX_GL_CheckError(); } void TRX_GL_Texture_Load( TRX_GL_TEXTURE *texture, const void *data, int width, int height, GLint internal_format, GLint format) { ASSERT(texture != nullptr); ASSERT(texture->initialized); TRX_GL_Texture_Bind(texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexImage2D( GL_TEXTURE_2D, 0, internal_format, width, height, 0, format, GL_UNSIGNED_BYTE, data); TRX_GL_CheckError(); glGenerateMipmap(GL_TEXTURE_2D); TRX_GL_CheckError(); } void TRX_GL_Texture_LoadFromBackBuffer(TRX_GL_TEXTURE *const texture) { ASSERT(texture != nullptr); ASSERT(texture->initialized); TRX_GL_Texture_Bind(texture); GLint viewport[4]; glGetIntegerv(GL_VIEWPORT, viewport); TRX_GL_CheckError(); const GLint vp_x = viewport[0]; const GLint vp_y = viewport[1]; const GLint vp_w = viewport[2]; const GLint vp_h = viewport[3]; const int32_t side = MIN(vp_w, vp_h); const int32_t x = vp_x + (vp_w - side) / 2; const int32_t y = vp_y + (vp_h - side) / 2; const int32_t w = side; const int32_t h = side; glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, x, y, w, h, 0); TRX_GL_CheckError(); } ================================================ FILE: src/trx/gl/texture.h ================================================ #pragma once #include typedef struct { bool initialized; GLuint id; GLenum target; } TRX_GL_TEXTURE; TRX_GL_TEXTURE *TRX_GL_Texture_Create(GLenum target); void TRX_GL_Texture_Free(TRX_GL_TEXTURE *texture); void TRX_GL_Texture_Init(TRX_GL_TEXTURE *texture, GLenum target); void TRX_GL_Texture_Close(TRX_GL_TEXTURE *texture); void TRX_GL_Texture_Bind(const TRX_GL_TEXTURE *texture); void TRX_GL_Texture_Load( TRX_GL_TEXTURE *texture, const void *data, int width, int height, GLint internal_format, GLint format); void TRX_GL_Texture_LoadFromBackBuffer(TRX_GL_TEXTURE *texture); ================================================ FILE: src/trx/gl/track.c ================================================ #include TRX_GL_METRICS g_TRX_GL_Metrics; void TRX_GL_Track_Reset(void) { g_TRX_GL_Metrics = (TRX_GL_METRICS) { 0 }; } TRX_GL_METRICS TRX_GL_Track_GetMetrics(void) { return g_TRX_GL_Metrics; } ================================================ FILE: src/trx/gl/track.h ================================================ #pragma once #include typedef struct { int32_t buffer_transfer_count; int32_t buffer_total_bytes; int32_t uniform_changes; int32_t opaque_vert_count; int32_t trans_vert_count; int32_t blend_add_vert_count; } TRX_GL_METRICS; extern TRX_GL_METRICS g_TRX_GL_Metrics; #define TRX_GL_TRACK_UNIFORM(fn, ...) \ do { \ g_TRX_GL_Metrics.uniform_changes++; \ fn(__VA_ARGS__); \ } while (0); #define TRX_GL_TRACK_DATA(fn, a, b, c, d) \ do { \ g_TRX_GL_Metrics.buffer_total_bytes += b; \ g_TRX_GL_Metrics.buffer_transfer_count++; \ fn(a, b, c, d); \ } while (0); #define TRX_GL_TRACK_SUBDATA(fn, a, b, c, d) \ do { \ g_TRX_GL_Metrics.buffer_total_bytes += c; \ g_TRX_GL_Metrics.buffer_transfer_count++; \ fn(a, b, c, d); \ } while (0); void TRX_GL_Track_Reset(void); TRX_GL_METRICS TRX_GL_Track_GetMetrics(void); ================================================ FILE: src/trx/gl/utils.c ================================================ #include #include const char *TRX_GL_GetErrorString(GLenum err) { switch (err) { case GL_NO_ERROR: return "GL_NO_ERROR"; case GL_INVALID_ENUM: return "GL_INVALID_ENUM"; case GL_INVALID_VALUE: return "GL_INVALID_VALUE"; case GL_INVALID_OPERATION: return "GL_INVALID_OPERATION"; case GL_INVALID_FRAMEBUFFER_OPERATION: return "GL_INVALID_FRAMEBUFFER_OPERATION"; case GL_OUT_OF_MEMORY: return "GL_OUT_OF_MEMORY"; case GL_STACK_UNDERFLOW: return "GL_STACK_UNDERFLOW"; case GL_STACK_OVERFLOW: return "GL_STACK_OVERFLOW"; default: return "UNKNOWN"; } } void TRX_GL_CheckError(void) { for (GLenum err; (err = glGetError()) != GL_NO_ERROR;) { LOG_ERROR("glGetError: (%s)", TRX_GL_GetErrorString(err)); } } ================================================ FILE: src/trx/gl/utils.h ================================================ #pragma once #include #include #include void TRX_GL_CheckError(void); const char *TRX_GL_GetErrorString(GLenum err); ================================================ FILE: src/trx/gl/vertex_array.c ================================================ #include #include #include #include void TRX_GL_VertexArray_Init(TRX_GL_VERTEX_ARRAY *array) { ASSERT(array != nullptr); glGenVertexArrays(1, &array->id); TRX_GL_CheckError(); array->initialized = true; } void TRX_GL_VertexArray_Close(TRX_GL_VERTEX_ARRAY *array) { ASSERT(array != nullptr); if (array->initialized) { glDeleteVertexArrays(1, &array->id); TRX_GL_CheckError(); } array->initialized = false; } void TRX_GL_VertexArray_Bind(TRX_GL_VERTEX_ARRAY *array) { ASSERT(array != nullptr); ASSERT(array->initialized); glBindVertexArray(array->id); TRX_GL_CheckError(); } void TRX_GL_VertexArray_Attribute( TRX_GL_VERTEX_ARRAY *array, GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, GLsizei offset) { ASSERT(array != nullptr); ASSERT(array->initialized); glEnableVertexAttribArray(index); TRX_GL_CheckError(); glVertexAttribPointer( index, size, type, normalized, stride, (void *)(intptr_t)offset); TRX_GL_CheckError(); } void TRX_GL_VertexArray_IAttribute( TRX_GL_VERTEX_ARRAY *array, GLuint index, GLint size, GLenum type, GLsizei stride, GLsizei offset) { ASSERT(array != nullptr); ASSERT(array->initialized); glEnableVertexAttribArray(index); TRX_GL_CheckError(); glVertexAttribIPointer(index, size, type, stride, (void *)(intptr_t)offset); TRX_GL_CheckError(); } ================================================ FILE: src/trx/gl/vertex_array.h ================================================ #pragma once #include typedef struct { bool initialized; GLuint id; } TRX_GL_VERTEX_ARRAY; void TRX_GL_VertexArray_Init(TRX_GL_VERTEX_ARRAY *array); void TRX_GL_VertexArray_Close(TRX_GL_VERTEX_ARRAY *array); void TRX_GL_VertexArray_Bind(TRX_GL_VERTEX_ARRAY *array); void TRX_GL_VertexArray_Attribute( TRX_GL_VERTEX_ARRAY *array, GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, GLsizei offset); void TRX_GL_VertexArray_IAttribute( TRX_GL_VERTEX_ARRAY *array, GLuint index, GLint size, GLenum type, GLsizei stride, GLsizei offset); ================================================ FILE: src/trx/version.c ================================================ #include #ifndef MESON_BUILD const char *g_TRXVersion = "TR1X (non-Docker build)"; #endif int32_t g_TRVersion = 0; // overriden at runtime when loading a level ================================================ FILE: src/trx/version.h ================================================ #pragma once #include #define TR_VERSION_COUNT 3 extern const char *g_TRXVersion; extern int32_t g_TRVersion; ================================================ FILE: tools/additional_lint ================================================ #!/usr/bin/env python3 import argparse import sys from collections.abc import Iterable from fnmatch import fnmatch from pathlib import Path from shared.files import find_versioned_files, is_binary_file from shared.linting import LintContext, lint_repo, lint_bulk_files, lint_file from shared.paths import REPO_DIR IGNORED_PATTERNS = ["*.patch", "*.bin"] def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("path", type=Path, nargs="*") parser.add_argument("-D", "--debug", action="store_true") parser.add_argument("-a", "--all", action="store_true") return parser.parse_args() def filter_files( files: Iterable[Path], ignored_patterns: list[str] | None, debug: bool ) -> Iterable[Path]: for path in files: if not path.exists(): continue if path.is_dir(): continue if is_binary_file(path): if debug: print(f"{path} is a binary file, ignoring", file=sys.stderr) continue if ignored_patterns and any( fnmatch(path.name, pattern) for pattern in ignored_patterns ): if debug: print( f"{path} has a prohibited extension, ignoring", file=sys.stderr, ) continue yield path def main(root_dir: Path) -> None: args = parse_args() context = LintContext( root_dir=root_dir, versioned_files=list(find_versioned_files(root_dir=REPO_DIR)), ) if args.path: files = args.path else: files = context.versioned_files files = list( filter_files( files, ignored_patterns=IGNORED_PATTERNS, debug=args.debug ) ) exit_code = 0 for file in files: if args.debug: print(f"Checking {file}...", file=sys.stderr) for lint_warning in lint_file(context, file): print(str(lint_warning), file=sys.stderr) exit_code = 1 if args.debug: print(f"Checking files in bulk {file}...", file=sys.stderr) for lint_warning in lint_bulk_files(context, files): print(str(lint_warning), file=sys.stderr) exit_code = 1 if args.all: if args.debug: print(f"Checking for repository-wide warnings...", file=sys.stderr) for lint_warning in lint_repo(context): print(str(lint_warning), file=sys.stderr) exit_code = 1 exit(exit_code) if __name__ == "__main__": main(root_dir=REPO_DIR) ================================================ FILE: tools/download_assets ================================================ #!/usr/bin/env python3 import argparse import shutil import ssl import sys import tempfile from pathlib import Path from urllib.request import Request, urlopen from zipfile import ZipFile from shared.paths import DATA_DIR, PROJECT_PATHS def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description=( "Downloads large binary assets for both the legacy per-game " "ship layouts and the combined data/trx/ship hierarchy." ) ) parser.add_argument( "game_version", choices=["1", "2", "3", "all"], default="all", nargs="?", ) parser.add_argument( "--combined", action="store_true", help="Also download assets for the combined data/trx/ship hierarchy.", ) return parser.parse_args() def download_to_file(url: str, path: Path) -> None: print(f"Downloading {url}...") req = Request(url, headers={"User-Agent": "download_assets"}) context = ssl._create_unverified_context() with urlopen(req, context=context) as response: if getattr(response, "status", None) not in (None, 200): sys.exit( f"Error: failed to download {url}. Status: {response.status}" ) with path.open("wb") as f: shutil.copyfileobj(response, f) def extract_zip(zip_path: Path, dest_dir: Path) -> None: print(f"Extracting {zip_path} to {dest_dir}...") dest_dir.mkdir(parents=True, exist_ok=True) with ZipFile(zip_path) as z: z.extractall(dest_dir) def download_assets(asset_urls: list[str], target_dir: Path) -> None: with tempfile.TemporaryDirectory() as tmpdir_str: tmpdir = Path(tmpdir_str) for url in asset_urls: filename = Path(url).name local_zip = tmpdir / filename download_to_file(url, local_zip) extract_zip(local_zip, target_dir) print("Asset download and extraction complete.") def main() -> None: args = parse_args() legacy_asset_urls_map: dict[int, list[str]] = { 1: [ "https://lostartefacts.dev/aux/tr1x/main.zip", "https://lostartefacts.dev/aux/tr1x/trub.zip", ], 2: [ "https://lostartefacts.dev/aux/tr2x/main.zip", "https://lostartefacts.dev/aux/tr2x/trgm.zip", ], 3: ["https://lostartefacts.dev/aux/tr3x/main.zip"], } combined_asset_urls_map: dict[int, list[str]] = { 1: [ "https://lostartefacts.dev/aux/trx/tr1.zip", "https://lostartefacts.dev/aux/trx/tr1-ub.zip", "https://lostartefacts.dev/aux/trx/tr1-demo-pc.zip", ], 2: [ "https://lostartefacts.dev/aux/trx/tr2.zip", "https://lostartefacts.dev/aux/trx/tr2-gm.zip", ], 3: ["https://lostartefacts.dev/aux/trx/tr3.zip"], } versions = {"1": [1], "2": [2], "3": [3], "all": [1, 2, 3]}[ args.game_version ] for version in versions: download_assets( legacy_asset_urls_map[version], target_dir=PROJECT_PATHS[version].shipped_data_dir, ) if args.combined: download_assets( combined_asset_urls_map[version], target_dir=DATA_DIR / "trx" / "ship", ) if __name__ == "__main__": main() ================================================ FILE: tools/embed_trx_lua.py ================================================ #!/usr/bin/env python3 from __future__ import annotations import argparse from pathlib import Path def _c_ident_from_path(path: Path) -> str: raw = path.name out: list[str] = [] for ch in raw: if ch.isalnum(): out.append(ch) else: out.append("_") return "m_TrxLua_" + "".join(out) def _bytes_to_c_array(data: bytes) -> str: per_line = 16 chunks: list[str] = [] for i in range(0, len(data), per_line): chunk = ", ".join(f"0x{b:02x}" for b in data[i : i + per_line]) chunks.append(" " + chunk) return ",\n".join(chunks) def main() -> int: parser = argparse.ArgumentParser(description="Embed TRX Lua scripts into C") parser.add_argument("--output", required=True, type=Path) parser.add_argument("inputs", nargs="+", type=Path) args = parser.parse_args() output_path: Path = args.output inputs: list[Path] = list(args.inputs) scripts: list[tuple[str, str, bytes]] = [] for in_path in inputs: data = in_path.read_bytes() c_ident = _c_ident_from_path(in_path) scripts.append((in_path.name, c_ident, data)) lines: list[str] = [] lines.append("// Auto-generated file; do not edit.") lines.append('#include ') lines.append("") for name, c_ident, data in scripts: lines.append(f"static const uint8_t {c_ident}[] = {{") lines.append(_bytes_to_c_array(data) + ",") lines.append("};") lines.append("") lines.append("const LUA_EMBEDDED_SCRIPT g_LUA_EmbeddedScripts[] = {") for name, c_ident, data in scripts: lines.append( f' {{ .path = "{name}", .data = {c_ident}, .size = {len(data)} }},' ) lines.append(" { .path = nullptr, .data = nullptr, .size = 0 },") lines.append("};") lines.append("") output_path.write_text("\n".join(lines), encoding="utf-8") return 0 if __name__ == "__main__": raise SystemExit(main()) ================================================ FILE: tools/ffmpeg_flags.txt ================================================ --enable-gpl --enable-decoder=pcx --enable-decoder=png --enable-decoder=gif --enable-decoder=mjpeg --enable-decoder=mpeg4 --enable-decoder=mdec --enable-decoder=mp3 --enable-decoder=wmav1 --enable-decoder=wmav2 --enable-decoder=h264 --enable-decoder=h264_qsv --enable-decoder=libopenh264 --enable-demuxer=mov --enable-demuxer=mp3 --enable-demuxer=avi --enable-demuxer=h264 --enable-demuxer=str --enable-demuxer=image2 --enable-demuxer=asf --enable-parser=mpegaudio --enable-zlib --enable-small --disable-debug --disable-ffplay --disable-ffprobe --disable-doc --disable-network --disable-htmlpages --disable-manpages --disable-podpages --disable-txtpages --disable-asm ================================================ FILE: tools/generate_icon ================================================ #!/usr/bin/env python3 # regenerate the .ICO file from .PSD. import argparse from pathlib import Path from shared.icons import generate_icon def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("path", type=Path) parser.add_argument("-o", "--output", type=Path, required=True) return parser.parse_args() def main() -> None: args = parse_args() if args.output.exists(): args.output.unlink() generate_icon(args.path, args.output) if __name__ == "__main__": main() ================================================ FILE: tools/generate_init ================================================ #!/usr/bin/env python3 import argparse import re from pathlib import Path from shared.versioning import generate_version TEMPLATE = """ const char *g_TRXVersion = "TRX {version}"; """.lstrip() def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("-o", "--output", type=Path) return parser.parse_args() def get_init_c() -> str: return TEMPLATE.format( version=generate_version() ) def update_init_c(output_path: Path) -> None: new_text = get_init_c() if not output_path.exists() or output_path.read_text() != new_text: output_path.write_text(new_text) def main() -> None: args = parse_args() if args.output: update_init_c(output_path=args.output) else: print(get_init_c(), end="") if __name__ == "__main__": main() ================================================ FILE: tools/generate_rcfile ================================================ #!/usr/bin/env python3 import argparse from pathlib import Path from shared.paths import DATA_DIR from shared.versioning import generate_version def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("-o", "--output", type=Path, nargs="+") return parser.parse_args() def write_rc_template( input_path: Path, output_path: Path, version: str ) -> None: template = input_path.read_text() template = template.replace("{version}", version) template = template.replace( "{icon_path}", str(DATA_DIR / input_path.parent.name / 'icon.ico') ) output_path.write_text(template) def main() -> None: args = parse_args() version = generate_version() for output_path in args.output or []: write_rc_template( input_path=DATA_DIR / output_path.name.replace('_', '/'), output_path=output_path, version=version, ) if __name__ == "__main__": main() ================================================ FILE: tools/get_version ================================================ #!/usr/bin/env python3 import argparse from shared.versioning import generate_version def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() return parser.parse_args() def main() -> None: args = parse_args() print(generate_version(), end="") if __name__ == "__main__": main() ================================================ FILE: tools/glyphs/README.md ================================================ ## Glyph Generation Tools These tools work alongside the injection tool to expand the original Tomb Raider character set by adding new characters and accents. ### Overview The game displays text using bitmaps, which are organized as sprites within the alphabet object found in .phd and .tr2 level files. Additionally, the game executable has hardcoded information regarding the locations, sizes, and indices of these glyphs. Expanding the character set involves a complex process requiring the use of multiple tools. **Summary:** `mapping.txt` + `glyphs.png` → `generate_defs` → intermediary files for the injection tool → TRXInjectionTool → `font.bin` injected into the game. ### `mapping.txt` and `glyphs.png` The master files are located at `data/tr*/glyphs/*.{png,txt}`. The PNG files are sprite sheets containing each character, while the text files provide metadata for each glyph or icon's location, as well as optional transforms such as shifts or bounding box resizes, using a domain specific language. Some characters are composed using special combining sprites. For instance, instead of creating an accented version of the character `a` for each variation, we use a single `a` sprite and separate sprites for all possible accents. This method allows accents to be combined with various base characters without redundancy. Every individual variation still needs to be defined in `mapping.txt`. All characters are encoded in UTF-8, meaning each character can consist of multiple bytes. Icons and buttons follow the same approach and are represented by ASCII sequences like `\{button x}`. The game processes these similarly to other glyphs. ### `generate_keyboard_map` Takes blank keycap bitmaps and a bitmap font and outputs a sprite sheet and a mapping.txt suitable for the `generate_defs` script. ### `generate_defs` This tool processes `mapping.txt` and the associated source images to create intermediary files needed by the injection tool, as well as update internal definition files that end up embedded into the executable at the compilation phase. It requires an argument for the game version since different games have unique font styles and specifications. The result is a packed texture atlas (which the injection tool later repacks) and a JSON file detailing each sprite texture's location. These files must be placed in their relevant directories within the injection tool resources. Afterward, the injection tool can be ran to generate the final output file, `font.bin`, which should be eventually placed in the injections directory for the game's use. ### `test_alignment.html` This testing tool showcases how `mapping.txt` segments the sprite sheets, allowing the developers to verify and correct any alignment issues. ### `generate_compositions` This is a simple development tool that outputs all valid accented characters to the standard output, and it is not a direct part of the main pipeline. ### `test_language` Another development tool, this tests language coverage for the languages intended to be supported. It operates based on `mapping.txt`, but is not directly involved in the main pipeline. ================================================ FILE: tools/glyphs/generate_case_map ================================================ #!/usr/bin/env python3 """ Generate a case mapping file of lowercase to uppercase characters based on the supported UI glyph definitions in text_tr1.def and text_tr2.def. """ import ast import re import sys from pathlib import Path # HACK: Ensure the shared module is visible for this script. sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from shared.glyph_mapping import Glyph, get_glyph_map from shared.paths import PROJECT_PATHS, SHARED_SRC_DIR def main() -> None: ui_dir = SHARED_SRC_DIR / "game/ui" glyphs: list[Glyph] = [] for project in PROJECT_PATHS.values(): glyphs += get_glyph_map(project.data_dir / "glyphs") supported = set(g.text for g in glyphs) lowers = [c for c in supported if len(c) == 1 and c.islower()] mapping: list[tuple[str, str]] = [] for c in sorted(lowers, key=lambda x: ord(x)): up = c.upper() if len(up) == 1 and up in supported: mapping.append((c, up)) out_path = SHARED_SRC_DIR / "strings/case_map.def" lines: list[str] = [ "// This file is autogenerated - do not edit.", "// See tools/glyphs/generate_case_map for details.", "", ] for low, up in mapping: lit_low = low.replace("\\", "\\\\").replace('"', '\\"') lit_up = up.replace("\\", "\\\\").replace('"', '\\"') lines.append(f'X_CASE_MAP("{lit_low}", "{lit_up}")') out_path.write_text("\n".join(lines) + "\n", encoding="utf-8") if __name__ == "__main__": main() ================================================ FILE: tools/glyphs/generate_compositions ================================================ #!/usr/bin/env python3 import json import string import unicodedata def quote(letter): return json.dumps(letter, ensure_ascii=False) def show(accented, letter, accent): print( f"U+{ord(accented):04X}:{accented} C combine({quote(letter)}, {quote(accent)})" ) def add_accent(letter, accent): return unicodedata.normalize("NFC", letter + accent) for accent_name, accent_char in [ ("\\{grave accent}", "\u0300"), ("\\{acute accent}", "\u0301"), ("\\{circumflex}", "\u0302"), ("\\{tilde}", "\u0303"), ("\\{macron}", "\u0304"), ("\\{overline}", "\u0305"), ("\\{breve}", "\u0306"), ("\\{dot above}", "\u0307"), ("\\{umlaut}", "\u0308"), ("\\{ring above}", "\u030A"), ("\\{double acute accent}", "\u030B"), ("\\{caron}", "\u030C"), ]: print("#", accent_name) for letter in string.ascii_uppercase + string.ascii_lowercase: accented = add_accent(letter, accent_char) if len(accented) == 1: show(accented, letter, accent_name) print() ================================================ FILE: tools/glyphs/generate_defs ================================================ #!/usr/bin/env -S uv run --script # # /// script # requires-python = ">=3.14" # dependencies = ["Pillow", "rectpack", "lark[interegular]", "numpy"] # /// import argparse import ast import json import sys import textwrap from dataclasses import dataclass from functools import lru_cache from pathlib import Path # pip install rectpack numpy lark Pillow import numpy as np import rectpack from lark import Discard, Lark, Transformer from PIL import Image # HACK: Ensure the shared module is visible for this script. sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from shared.glyph_mapping import CombineSource, Glyph, get_glyph_map from shared.paths import PROJECT_PATHS, REPO_DIR, SHARED_SRC_DIR def pack_sprites( glyphs: list[Glyph], output_dir: Path, font_idx: int, page_size: int = 256, padding: int = 1, ): """Create packed images with the glyph sprites, and a mapping JSON for the injector that will later convert them into a .bin injection file. """ glyph_with_sources = { glyph.source.index: (glyph, glyph.source) for glyph in glyphs } if len(glyph_with_sources) == 0: print(f"No glyphs for font {font_idx}, skipping sprite packing.") return None packer = rectpack.newPacker(rotation=False) for sprite_index, (glyph, source) in glyph_with_sources.items(): pixels, _ = source.load(glyph) height, width, _ = pixels.shape packer.add_rect(width + padding * 2, height + padding * 2, sprite_index) packer.add_bin(page_size, page_size, count=float("inf")) packer.pack() injector_mappings: list[dict] = [] for dst_page_num, bin in enumerate(packer): if font_idx == 0: output_path = output_dir / f"alpha_sprites_{dst_page_num:02d}.png" else: output_path = output_dir / ( f"alpha_sprites_font{font_idx}_{dst_page_num:02d}.png" ) dst_pixels = np.zeros((page_size, page_size, 4)) for rect in bin: dst_x = rect.x + padding dst_y = rect.y + padding width = rect.width - padding * 2 height = rect.height - padding * 2 glyph, source = glyph_with_sources[rect.rid] pixels, bbox = source.load(glyph) dst_pixels[ dst_y : dst_y + height, dst_x : dst_x + width, ] = pixels injector_mappings.append( { "mesh_num": source.index, "filename": output_path.name, "x": dst_x, "y": dst_y, "w": width, "h": height, "l": bbox.x + glyph.extra_x, "t": bbox.y + glyph.extra_y, "b": bbox.y + glyph.extra_y + bbox.h, "r": bbox.x + glyph.extra_x + bbox.w, } ) image = Image.fromarray(dst_pixels.astype("uint8")) image.save(output_path) print("Created a sprite sheet for injector in", output_path) if font_idx == 0: info_path = output_dir / "glyph_info.json" else: info_path = output_dir / f"glyph_info_font{font_idx}.json" info_path.write_text(json.dumps(injector_mappings)) print("Saved mappings for injector to", info_path) return info_path def generate_def_file(output_path: Path, glyphs: list[Glyph]): """Generate a .def file for the game itself, with the C macros containing information on how to render each glyph. """ header = textwrap.dedent( r""" // This file is autogenerated. See tools/glyphs/generate_defs for details. """ ).strip() class_map = { "R": "GLYPH_REVIEW_MARKER", "T": "GLYPH_TEXT", "C": "GLYPH_TEXT", "c": "GLYPH_TEXT", "I": "GLYPH_ICON", } with output_path.open("w") as handle: print(header, file=handle) for glyph in glyphs: args = [ json.dumps(glyph.text, ensure_ascii=False), class_map[glyph.glyph_class], str(glyph.source.index), ] print("X_GLYPH_DEFINE(" + ", ".join(args) + ")", file=handle) print("Saved", output_path) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("game", type=int) parser.add_argument("--injector-output-dir", type=Path, default=REPO_DIR) return parser.parse_args() def main() -> None: args = parse_args() def_path = SHARED_SRC_DIR / f"game/ui/text_autogen.def" input_dir = PROJECT_PATHS[args.game].data_dir / "glyphs" injector_output_dir = args.injector_output_dir glyphs = get_glyph_map(input_dir) fonts = sorted({glyph.font_idx for glyph in glyphs}) for font_idx in fonts: font_glyphs = [glyph for glyph in glyphs if glyph.font_idx == font_idx] pack_sprites(font_glyphs, output_dir=injector_output_dir, font_idx=font_idx) default_glyphs = [glyph for glyph in glyphs if glyph.font_idx == 0] generate_def_file(def_path, default_glyphs) if __name__ == "__main__": main() ================================================ FILE: tools/glyphs/generate_keyboard_map ================================================ #!/usr/bin/env python3 import argparse import sys import warnings from pathlib import Path # pip install rectpack Pillow import rectpack from PIL import Image, ImageDraw, ImageFont # HACK: Ensure the shared module is visible for this script. sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from shared.glyph_mapping import CombineSource, Glyph, get_glyph_map from shared.paths import DATA_DIR, PROJECT_PATHS, REPO_DIR MAX_WIDTH = 500 TEXT_MIN_PADDING = 3 TEXT_SHIFT_Y = 1 TEXT_COLOR = "#060100" KEYCAP_BORDER = 5 KEYCAP_WIDTHS = [15, 21, 32, 36, 50, 60] KEY_TO_TEXT: dict[str, str] = { r"\\{keyboard l_ctrl}": "Ctrl", r"\\{keyboard r_ctrl}": "Ctrl", r"\\{keyboard r_shift}": "Shift", r"\\{keyboard l_shift}": "Shift", r"\\{keyboard r_alt}": "Alt", r"\\{keyboard l_alt}": "Alt", r"\\{keyboard l_win}": "Win", r"\\{keyboard r_win}": "Win", r"\\{keyboard left}": "←", r"\\{keyboard up}": "↑", r"\\{keyboard right}": "→", r"\\{keyboard down}": "↓", r"\\{keyboard return}": "Return ↵", r"\\{keyboard escape}": "Esc", r"\\{keyboard backspace}": "Backspace", r"\\{keyboard tab}": "Tab", r"\\{keyboard space}": "Space", r"\\{keyboard caps_lock}": "CapsLock", r"\\{keyboard print_screen}": "Screen", r"\\{keyboard scroll_lock}": "ScrollLock", r"\\{keyboard pause}": "Pause", r"\\{keyboard insert}": "Insert", r"\\{keyboard home}": "Home", r"\\{keyboard page_up}": "PgUp", r"\\{keyboard delete}": "Del", r"\\{keyboard end}": "End", r"\\{keyboard page_down}": "PgDn", r"\\{keyboard a}": "A", r"\\{keyboard b}": "B", r"\\{keyboard c}": "C", r"\\{keyboard d}": "D", r"\\{keyboard e}": "E", r"\\{keyboard f}": "F", r"\\{keyboard g}": "G", r"\\{keyboard h}": "H", r"\\{keyboard i}": "I", r"\\{keyboard j}": "J", r"\\{keyboard k}": "K", r"\\{keyboard l}": "L", r"\\{keyboard m}": "M", r"\\{keyboard n}": "N", r"\\{keyboard o}": "O", r"\\{keyboard p}": "P", r"\\{keyboard q}": "Q", r"\\{keyboard r}": "R", r"\\{keyboard s}": "S", r"\\{keyboard t}": "T", r"\\{keyboard u}": "U", r"\\{keyboard v}": "V", r"\\{keyboard w}": "W", r"\\{keyboard x}": "X", r"\\{keyboard y}": "Y", r"\\{keyboard z}": "Z", r"\\{keyboard 0}": "0", r"\\{keyboard 1}": "1", r"\\{keyboard 2}": "2", r"\\{keyboard 3}": "3", r"\\{keyboard 4}": "4", r"\\{keyboard 5}": "5", r"\\{keyboard 6}": "6", r"\\{keyboard 7}": "7", r"\\{keyboard 8}": "8", r"\\{keyboard 9}": "9", r"\\{keyboard minus}": "-", r"\\{keyboard equals}": "=", r"\\{keyboard left_square_bracket}": "[", r"\\{keyboard right_square_bracket}": "]", r"\\{keyboard backslash}": "\\", r"\\{keyboard hash}": "#", r"\\{keyboard semicolon}": ";", r"\\{keyboard apostrophe}": "'", r"\\{keyboard backtick}": "`", r"\\{keyboard comma}": ",", r"\\{keyboard period}": ".", r"\\{keyboard slash}": "/", r"\\{keyboard f1}": "F1", r"\\{keyboard f2}": "F2", r"\\{keyboard f3}": "F3", r"\\{keyboard f4}": "F4", r"\\{keyboard f5}": "F5", r"\\{keyboard f6}": "F6", r"\\{keyboard f7}": "F7", r"\\{keyboard f8}": "F8", r"\\{keyboard f9}": "F9", r"\\{keyboard f10}": "F10", r"\\{keyboard f11}": "F11", r"\\{keyboard f12}": "F12", r"\\{keyboard f13}": "F13", r"\\{keyboard f14}": "F14", r"\\{keyboard f15}": "F15", r"\\{keyboard f16}": "F16", r"\\{keyboard f17}": "F17", r"\\{keyboard f18}": "F18", r"\\{keyboard f19}": "F19", r"\\{keyboard f20}": "F20", r"\\{keyboard f21}": "F21", r"\\{keyboard f22}": "F22", r"\\{keyboard f23}": "F23", r"\\{keyboard f24}": "F24", r"\\{keyboard num_lock}": "NumLock", r"\\{keyboard num_0}": "Num0", r"\\{keyboard num_1}": "Num1", r"\\{keyboard num_2}": "Num2", r"\\{keyboard num_3}": "Num3", r"\\{keyboard num_4}": "Num4", r"\\{keyboard num_5}": "Num5", r"\\{keyboard num_6}": "Num6", r"\\{keyboard num_7}": "Num7", r"\\{keyboard num_8}": "Num8", r"\\{keyboard num_9}": "Num9", r"\\{keyboard num_period}": "Num.", r"\\{keyboard num_divide}": "Num/", r"\\{keyboard num_multiply}": "Num*", r"\\{keyboard num_minus}": "Num-", r"\\{keyboard num_plus}": "Num+", r"\\{keyboard num_equals}": "Num=", r"\\{keyboard num_comma}": "Num,", r"\\{keyboard num_enter}": "Num↵", r"\\{keyboard unknown}": "????", } def align(value: int, align: int) -> int: return ((value + align - 1) // align) * align def generate_keycap_images(image_path: Path, widths: list[int]) -> list[Image.Image]: base_keycap = Image.open(image_path) keycap_images = [] original_width, original_height = base_keycap.size inner_width = original_width - 2 * KEYCAP_BORDER for width in widths: # Create a new image with the desired width and original height keycap_image = Image.new('RGBA', (width, original_height)) # Paste corners keycap_image.paste(base_keycap.crop((0, 0, KEYCAP_BORDER, original_height)), (0, 0)) keycap_image.paste(base_keycap.crop((original_width - KEYCAP_BORDER, 0, original_width, original_height)), (width - KEYCAP_BORDER, 0)) # Stretch the middle part middle = base_keycap.crop((KEYCAP_BORDER, 0, original_width - KEYCAP_BORDER, original_height)) stretched_middle = middle.resize((width - 2 * KEYCAP_BORDER, original_height), Image.LANCZOS) # Paste the stretched middle part keycap_image.paste(stretched_middle, (KEYCAP_BORDER, 0)) keycap_images.append(keycap_image) return keycap_images def find_best_keycap( visible_text: str, blank_keycap_images: list[Image.Image], font: ImageFont.ImageFont, ) -> Image.Image | None: # find all keycap images that can fit the text, then choose the smallest one candidates: list[Image.Image] = [] for keycap_img in blank_keycap_images: text_bbox = ImageDraw.Draw(keycap_img).textbbox( (0, 0), visible_text, font=font ) text_width = text_bbox[2] - text_bbox[0] if text_width + 2 * TEXT_MIN_PADDING <= keycap_img.width: candidates.append(keycap_img) if candidates: # choose the keycap with the smallest width return min(candidates, key=lambda img: img.width) warnings.warn(f"No suitable keycap image for text: {visible_text}") return None def create_sprite_sheet( font_path: Path, blank_keycap_images: list[Image], cell_size: int = 1 ) -> tuple[Image.Image, list[tuple[str, tuple[int, int, int, int]]]]: for i in range(5, 21): try: font = ImageFont.truetype(str(font_path), size=i) except Exception: continue else: break sprites = [] for key_name, visible_text in KEY_TO_TEXT.items(): keycap_img = find_best_keycap(visible_text, blank_keycap_images, font) if not keycap_img: continue keycap_copy = keycap_img.copy() # render text on a separate image and trim horizontal transparent pixels text_img = Image.new("RGBA", keycap_copy.size, (0, 0, 0, 0)) text_draw = ImageDraw.Draw(text_img) text_draw.text((0, 0), visible_text, font=font, fill=TEXT_COLOR) x0, y0, x1, y1 = text_draw.textbbox((0, 0), visible_text, font=font) cropped = text_img.crop((x0, 0, x0 + x1, text_img.height)) text_x = (keycap_copy.width + 1 - cropped.width) // 2 text_y = (keycap_copy.height - cropped.height) // 2 text_y += TEXT_SHIFT_Y keycap_copy.paste(cropped, (text_x, text_y), cropped) sprites.append((key_name, keycap_copy)) packer = rectpack.newPacker(rotation=False) for i, (_, keycap_copy) in enumerate(sprites): packer.add_rect( align(keycap_copy.width, cell_size), align(keycap_copy.height, cell_size), i, ) packer.add_bin(MAX_WIDTH, float("inf")) packer.pack() packed_areas = packer.rect_list() total_width = max(x + w for _, x, _, w, _, _ in packed_areas) total_height = max(y + h for _, _, y, _, h, _ in packed_areas) sprite_sheet = Image.new("RGBA", (total_width, total_height)) definitions = [] for _sheet, x, y, w, h, i in packed_areas: key_name, keycap_copy = sprites[i] sprite_sheet.paste(keycap_copy, (x, y)) definitions.append( (key_name, (x, y, keycap_copy.width, keycap_copy.height)) ) return sprite_sheet, definitions def output_definitions( definitions: list[tuple[str, tuple[int, int, int, int]]], output_image_name: str, file, ): for key_name, (x, y, w, h) in definitions: print( f'"{key_name}" N manual_sprite("{output_image_name}", {x}, {y}, {w}, {h}) translate(y=2)', file=file, ) def main(): font_path = DATA_DIR / "tomb-11.bdf" image_name = "keyboard.png" mapping_name = "mapping_keyboard.txt" glyphs_dir = DATA_DIR / "common/glyphs" keycap_images = generate_keycap_images( glyphs_dir / "blank.png", KEYCAP_WIDTHS ) image_path = glyphs_dir / image_name mapping_path = glyphs_dir / mapping_name sprite_sheet, definitions = create_sprite_sheet(font_path, keycap_images) sprite_sheet.save(image_path) print(f"{image_path} updated") with mapping_path.open("w") as file: output_definitions(definitions, image_name, file=file) print(f"{mapping_path} updated") if __name__ == "__main__": main() ================================================ FILE: tools/glyphs/test_alignment.html ================================================

Run me with python3 -m http.server 8000 -d . in the repository directory,
then visiting http://localhost:8000/tools/glyphs/test_alignment.html rather than opening this file directly.

================================================ FILE: tools/glyphs/test_language ================================================ #!/usr/bin/env python3 import argparse import sys import unicodedata from pathlib import Path # pip install pyicu import icu # HACK: Ensure the shared module is visible for this script. sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from shared.glyph_mapping import get_glyph_map from shared.paths import PROJECT_PATHS MISSING_PRINT_LIMIT = 10 LOCALES_TO_TEST = { "ar_SA": "Arabic", "az_AZ": "Azerbaijani", "be_BY": "Belarusian", "bg_BG": "Bulgarian", "bn_IN": "Bengali", "bs_BA": "Bosnian", "ca_ES": "Catalan", "cs_CZ": "Czech", "da_DK": "Danish", "de_DE": "German", "el_GR": "Greek", "en_US": "English", "es_ES": "Spanish", "et_EE": "Estonian", "eu_ES": "Basque", "fa_IR": "Persian", "fi_FI": "Finnish", "fo_FO": "Faroese", "fr_FR": "French", "ga_IE": "Irish", "gl_ES": "Galician", "gu_IN": "Gujarati", "he_IL": "Hebrew", "hi_IN": "Hindi", "hr_HR": "Croatian", "hu_HU": "Hungarian", "hy_AM": "Armenian", "id_ID": "Indonesian", "is_IS": "Icelandic", "it_IT": "Italian", "ja_JP": "Japanese", "ka_GE": "Georgian", "kk_KZ": "Kazakh", "kn_IN": "Kannada", "ko_KR": "Korean", "kok_IN": "Konkani", "lt_LT": "Lithuanian", "lv_LV": "Latvian", "mk_MK": "Macedonian", "ml_IN": "Malayalam", "mn_MN": "Mongolian", "mr_IN": "Marathi", "ms_MY": "Malay", "mt_MT": "Maltese", "nb_NO": "Norwegian Bokmål", "nl_NL": "Dutch", "nn_NO": "Norwegian Nynorsk", "no_NO": "Norwegian", "pa_IN": "Punjabi", "pl_PL": "Polish", "pt_BR": "Portuguese", "pt_PT": "Portuguese", "ro_RO": "Romanian", "ru_RU": "Russian", "se_NO": "Northern Sami", "sk_SK": "Slovak", "sl_SI": "Slovenian", "sr_BA": "Serbian", "sv_SE": "Swedish", "tr_TR": "Turkish", "zh_CN": "Chinese", "zh_TW": "Chinese", } def get_glyphs_for_locale(locale_code: str) -> list[str]: locale = icu.LocaleData(locale_code) results: set[str] = set() for glyph in locale.getExemplarSet(0, 0): results.add(unicodedata.normalize("NFC", glyph.lower())) results.add(unicodedata.normalize("NFC", glyph.upper())) return sorted(r for r in results if len(r) == 1) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("game", type=int) return parser.parse_args() def main() -> None: args = parse_args() input_dir = PROJECT_PATHS[args.game].data_dir / "glyphs" glyph_map = {glyph.text: glyph for glyph in get_glyph_map(input_dir)} present_glyphs = set(glyph_map.keys()) for locale_code, language_name in LOCALES_TO_TEST.items(): requested_glyphs = set(get_glyphs_for_locale(locale_code)) missing_glyphs = requested_glyphs - present_glyphs print(f"{locale_code:>10s} {language_name} ", end="") if missing_glyphs: print("not supported: missing ", end="") for glyph in sorted(missing_glyphs)[:MISSING_PRINT_LIMIT]: print(f"U+{ord(glyph):04X}:{glyph}", end=" ") if len(missing_glyphs) >= MISSING_PRINT_LIMIT: print(f"...", end=" ") print(f"({len(missing_glyphs)} total)") else: print("supported!") if __name__ == "__main__": main() ================================================ FILE: tools/inspect_save ================================================ #!/usr/bin/env python3 import argparse import sys import json import struct import zlib from pathlib import Path import bson def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("path", type=Path) return parser.parse_args() def main() -> None: args = parse_args() with args.path.open("rb") as handle: magic = handle.read(4) version, compressed_size, uncompressed_size = struct.unpack( "III", handle.read(12) ) data = bson.loads(zlib.decompress(handle.read(compressed_size))) flags, counter, level_num, title_size = struct.unpack("I" * 4, handle.read(16)) title = handle.read(title_size) print(json.dumps(data, indent=4)) print("flags:", flags, file=sys.stderr) print("counter:", counter, file=sys.stderr) print("level_num:", level_num, file=sys.stderr) print("title:", title, file=sys.stderr) if __name__ == "__main__": main() ================================================ FILE: tools/installer/.gitignore ================================================ *.suo *.o *.obj *.pdb *.lib *.exp [Dd]ebug/ [Rr]elease/ [Oo]bj/ *.user *.ipch .vs/ *.vcxproj *.filters *.pubxml [Oo]ut/ ================================================ FILE: tools/installer/TR1X_Installer/App.xaml ================================================  ================================================ FILE: tools/installer/TR1X_Installer/App.xaml.cs ================================================ using System.Windows; using TR1X_Installer.Installers; using TRX_InstallerLib.Controls; using TRX_InstallerLib.Installers; namespace TR1X_Installer; public partial class App : Application { public App() { Current.MainWindow = new TRXInstallWindow(new List { new SteamInstallSource(), new GOGInstallSource(), new TombATIInstallSource(), new CDRomInstallSource(), new TR1XInstallSource(), }); Current.MainWindow.Show(); } } ================================================ FILE: tools/installer/TR1X_Installer/Installers/CDRomInstallSource.cs ================================================ using System.IO; using System.Text.RegularExpressions; using TRX_InstallerLib.Installers; using TRX_InstallerLib.Utils; namespace TR1X_Installer.Installers; public class CDRomInstallSource : BaseInstallSource { public override IEnumerable DirectoriesToTry { get { DriveInfo[] allDrives = DriveInfo.GetDrives(); foreach (var drive in allDrives) { if (drive.DriveType == DriveType.CDRom && drive.IsReady) { yield return drive.RootDirectory.FullName; } } } } public override bool IsImportingSavesSupported => false; public override string SourceName => "CDRom"; public override async Task CopyOriginalGameFiles( string sourceDirectory, string targetDirectory, IProgress progress, bool importSaves ) { var filterRegex = new Regex(@"(data|fmv|music)[\\/]", RegexOptions.IgnoreCase); await InstallUtils.CopyDirectoryTree( sourceDirectory, targetDirectory, progress, file => filterRegex.IsMatch(file), path => ConvertTargetPath(path) ); } public override bool IsDownloadingMusicNeeded(string sourceDirectory) { return true; } public override bool IsDownloadingExpansionNeeded(string sourceDirectory) { return true; } public override bool IsGameFound(string sourceDirectory) { return Directory.Exists(Path.Combine(sourceDirectory, "DATA")) && Directory.Exists(Path.Combine(sourceDirectory, "FMV")) && File.Exists(Path.Combine(sourceDirectory, "dos4gw.exe")) && File.Exists(Path.Combine(sourceDirectory, "tomb.exe")); } } ================================================ FILE: tools/installer/TR1X_Installer/Installers/GOGInstallSource.cs ================================================ using DiscUtils.Iso9660; using DiscUtils.Streams; using Microsoft.Win32; using System.IO; using System.Text.RegularExpressions; using TRX_InstallerLib.Installers; using TRX_InstallerLib.Models; using TRX_InstallerLib.Utils; namespace TR1X_Installer.Installers; public class GOGInstallSource : BaseInstallSource { public override IEnumerable DirectoriesToTry { get { yield return @"C:\Program Files (x86)\GOG Galaxy\Games\Tomb Raider 1"; using var key = Registry.ClassesRoot.OpenSubKey(@"goggalaxy\shell\open\command"); if (key is not null) { var value = key.GetValue("")?.ToString(); if (value is not null && new Regex(@"""(?[^""]+)""").Match(value) is { Success: true } match) { yield return Path.Combine(Path.GetDirectoryName(match.Groups["path"].Value)!, @"Games\Tomb Raider 1"); } } } } public override bool IsImportingSavesSupported => false; public override string SourceName => "GOG"; public override Task CopyOriginalGameFiles( string sourceDirectory, string targetDirectory, IProgress progress, bool importSaves ) { var cuePath = Path.Combine(sourceDirectory, "game.dat"); var isoPath = Path.Combine(sourceDirectory, "game.iso"); CueFile cueFile; try { cueFile = new CueFile(cuePath); } catch (Exception e) { throw new ApplicationException(string.Format(Language.Instance.Controls!["progress_cue_failure"], cuePath, e.Message)); } try { var firstTrack = cueFile.TrackList.First(); firstTrack.Write(isoPath, progress); } catch (Exception e) { throw new ApplicationException(string.Format(Language.Instance.Controls!["progress_converting_bin_failure"], e.Message)); } try { using FileStream file = File.Open(isoPath, FileMode.Open, FileAccess.Read); using CDReader reader = new(file, true); int currentProgress = 0; progress.Report(new InstallProgress { MaximumValue = 1, CurrentValue = 0, Description = Language.Instance.Controls!["progress_scanning_source"], }); var filesToExtract = GetFilesToExtract(reader.Root); progress.Report(new InstallProgress { MaximumValue = filesToExtract.Count(), CurrentValue = 0, Description = Language.Instance.Controls!["progress_preparing_extract"], }); foreach (var path in filesToExtract) { var relPath = ConvertTargetPath(path); var targetPath = Path.Combine(targetDirectory, relPath); if (!File.Exists(targetPath)) { Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!); using SparseStream sourceStream = reader.OpenFile(path, FileMode.Open, FileAccess.Read); var readAllByte = new byte[sourceStream.Length]; sourceStream.Read(readAllByte, 0, readAllByte.Length); using FileStream targetStream = new(targetPath, FileMode.Create); targetStream.Position = 0; targetStream.Write(readAllByte, 0, readAllByte.Length); } progress.Report(new InstallProgress { MaximumValue = filesToExtract.Count(), CurrentValue = ++currentProgress, Description = string.Format(Language.Instance.Controls!["progress_extracting"], relPath) }); } } catch (Exception e) { throw new ApplicationException(string.Format(Language.Instance.Controls!["progress_converting_iso_failure"], e.Message)); } File.Delete(isoPath); return Task.CompletedTask; } public override bool IsDownloadingMusicNeeded(string sourceDirectory) { return true; } public override bool IsDownloadingExpansionNeeded(string sourceDirectory) { return true; } public override bool IsGameFound(string sourceDirectory) { return File.Exists(Path.Combine(sourceDirectory, "GAME.GOG")); } private static IEnumerable GetFilesToExtract(DiscUtils.DiscDirectoryInfo root) { var regex = new Regex(@"^(data|fmv)[\\/].*$", RegexOptions.IgnoreCase); foreach (var dir in root.GetDirectories()) { foreach (var filePath in GetFilesToExtract(dir)) { yield return filePath; } } foreach (var file in root.GetFiles()) { string filePath = file.FullName; if (regex.IsMatch(filePath)) { yield return filePath; } } } } ================================================ FILE: tools/installer/TR1X_Installer/Installers/SteamInstallSource.cs ================================================ using Microsoft.Win32; using System.IO; namespace TR1X_Installer.Installers; public class SteamInstallSource : GOGInstallSource { public override IEnumerable DirectoriesToTry { get { yield return @"C:\Program Files (x86)\Steam\steamapps\common\Tomb Raider (I)"; using var key = Registry.CurrentUser.OpenSubKey(@"Software\Valve\Steam"); if (key is not null) { var value = key.GetValue("SteamPath")?.ToString(); if (value is not null) { yield return Path.Combine(value, @"steamapps\common\Tomb Raider (I)"); } } } } public override string SourceName => "Steam"; } ================================================ FILE: tools/installer/TR1X_Installer/Installers/TR1XInstallSource.cs ================================================ using System.IO; using System.Text.RegularExpressions; using TRX_InstallerLib.Installers; using TRX_InstallerLib.Utils; namespace TR1X_Installer.Installers; public class TR1XInstallSource : BaseInstallSource { public override IEnumerable DirectoriesToTry { get { var previousPath = InstallUtils.GetPreviousInstallationPath(); if (previousPath is not null) { yield return previousPath; } foreach (var path in InstallUtils.GetDesktopShortcutDirectories()) { yield return path; } } } public override string SuggestedInstallationDirectory { get => InstallUtils.GetPreviousInstallationPath() ?? base.SuggestedInstallationDirectory; } public override bool IsImportingSavesSupported => true; public override string SourceName => "TR1X"; public override async Task CopyOriginalGameFiles( string sourceDirectory, string targetDirectory, IProgress progress, bool importSaves ) { var filterRegex = new Regex(importSaves ? @"(data|fmv|music|saves)[\\/]|save.*\.\d+" : @"(data|fmv|music)[\\/]", RegexOptions.IgnoreCase); await InstallUtils.CopyDirectoryTree( sourceDirectory, targetDirectory, progress, file => filterRegex.IsMatch(file) ); } public override bool IsDownloadingMusicNeeded(string sourceDirectory) { return !Directory.Exists(Path.Combine(sourceDirectory, "music")); } public override bool IsDownloadingExpansionNeeded(string sourceDirectory) { return !File.Exists(Path.Combine(sourceDirectory, "data", "cat.phd")); } public override bool IsGameFound(string sourceDirectory) { return File.Exists(Path.Combine(sourceDirectory, "TRX.exe")) || File.Exists(Path.Combine(sourceDirectory, "TR1X.exe")) || File.Exists(Path.Combine(sourceDirectory, "Tomb1Main.exe")); } } ================================================ FILE: tools/installer/TR1X_Installer/Installers/TombATIInstallSource.cs ================================================ using System.IO; using System.Text.RegularExpressions; using TRX_InstallerLib.Installers; using TRX_InstallerLib.Utils; namespace TR1X_Installer.Installers; public class TombATIInstallSource : BaseInstallSource { public override IEnumerable DirectoriesToTry { get { yield return "C:\\TOMBATI"; foreach (var path in InstallUtils.GetDesktopShortcutDirectories()) { yield return path; } foreach (var path in new SteamInstallSource().DirectoriesToTry) { yield return path; } } } public override bool IsImportingSavesSupported => true; public override string SourceName => "TombATI"; public override async Task CopyOriginalGameFiles( string sourceDirectory, string targetDirectory, IProgress progress, bool importSaves ) { var filterRegex = new Regex(importSaves ? @"(data|fmv|music)[\\/]|save.*\.\d+\b" : @"(data|fmv|music)[\\/]", RegexOptions.IgnoreCase); await InstallUtils.CopyDirectoryTree( sourceDirectory, targetDirectory, progress, file => filterRegex.IsMatch(file), path => ConvertTargetPath(path) ); } public override bool IsDownloadingMusicNeeded(string sourceDirectory) { return !Directory.Exists(Path.Combine(sourceDirectory, "music")); } public override bool IsDownloadingExpansionNeeded(string sourceDirectory) { return !File.Exists(Path.Combine(sourceDirectory, "data", "cat.phd")); } public override bool IsGameFound(string sourceDirectory) { return Directory.Exists(Path.Combine(sourceDirectory, "DATA")) && Directory.Exists(Path.Combine(sourceDirectory, "FMV")) && File.Exists(Path.Combine(sourceDirectory, "TombATI.exe")); } } ================================================ FILE: tools/installer/TR1X_Installer/Resources/Lang/en.json ================================================ { "Controls": { "window_title_main": "TR1X Installer", "step_source_content": "TR1X requires original game files to run.\nPlease choose the source location where to install the data files from.\nIf you're upgrading an existing installation, please choose TR1X.", "step_settings_music_content": "Neither the Steam nor GOG versions of the game ship with the full soundtrack found on the PlayStation or Saturn retail releases. This option lets you download the missing tracks automatically (164 MB). The legality of these files is disputable; the most legal way to import the music to PC is to rip the audio tracks yourself from a physical PlayStation or Saturn disc.", "step_settings_expansion_heading": "Download Unfinished Business expansion pack", "step_settings_expansion_content": "The Unfinished Business expansion pack was made freeware. However, the Steam and GOG versions do not ship it. This option lets you download the expansion files automatically (6 MB).", "step_settings_expansion_music": "Fan-patched edition (includes music triggers)", "step_settings_expansion_vanilla": "Original edition (does not include music triggers)", "step_settings_saves_content": "Imports existing savegame files. Only TombATI and TR1X savegame format is supported at this time." } } ================================================ FILE: tools/installer/TR1X_Installer/Resources/Lang/it.json ================================================ { "Controls": { "window_title_main": "Programma di installazione di TR1X", "step_source_content": "Per eseguire TR1X, sono richiesti i file di gioco di Tomb Raider.\nSeleziona il percorso da cui copiare questi file.\nSe stai aggiornando un'installazione già esistente, seleziona 'TR1X'.", "step_settings_music_content": "Né la versione Steam né quella GOG del gioco includono la colonna sonora completa presente nelle versioni PlayStation e Saturn. Questa opzione ti consente di scaricare automaticamente le tracce audio mancanti (164 MB). La legalità di questi file è discutibile; il modo più legale per importare la musica su PC è estrarre le tracce audio da un disco fisico in tuo possesso della versione PlayStation o Saturn.", "step_settings_expansion_heading": "Scarica l'espansione Conti in Sospeso", "step_settings_expansion_content": "Il pacchetto di espansione Conti in Sospeso è stato reso gratuito. Tuttavia, le versioni Steam e GOG non lo includono. Questa opzione ti consente di scaricare automaticamente i file dell'espansione (6 MB).", "step_settings_expansion_music": "Edizione amatoriale (aggiunge il supporto alle tracce musicali)", "step_settings_expansion_vanilla": "Edizione originale (senza supporto alle tracce musicali)", "step_settings_saves_content": "Importa i file di salvataggio esistenti. Al momento sono supportati solo i formati di salvataggio di TombATI e TR1X." } } ================================================ FILE: tools/installer/TR1X_Installer/Resources/const.json ================================================ { "Game": "TR1X", "GoldGame": "TR1X - UB", "GoldFileIdentifier": "cat.phd", "AllowExpansionTypeSelection": true, "ShortcutTitle": "Tomb Raider I: Community Edition", "GoldZips": { "0": "trub-music.zip", "1": "trub-vanilla.zip" } } ================================================ FILE: tools/installer/TR1X_Installer/TR1X_Installer.csproj ================================================  WinExe net8.0-windows enable enable true false true TR1X_Installer True true true false true false false win-x64 Resources\icon.ico ================================================ FILE: tools/installer/TR2X_Installer/App.xaml ================================================  ================================================ FILE: tools/installer/TR2X_Installer/App.xaml.cs ================================================ using System.Windows; using TR2X_Installer.Installers; using TRX_InstallerLib.Controls; using TRX_InstallerLib.Installers; namespace TR2X_Installer; public partial class App : Application { public App() { Current.MainWindow = new TRXInstallWindow(new List { new SteamInstallSource(), new GOGInstallSource(), new CDRomInstallSource(), new TR2XInstallSource(), }); Current.MainWindow.Show(); } } ================================================ FILE: tools/installer/TR2X_Installer/Installers/CDRomInstallSource.cs ================================================ using System.IO; namespace TR2X_Installer.Installers; public class CDRomInstallSource : GenericInstallSource { public override IEnumerable DirectoriesToTry { get { DriveInfo[] allDrives = DriveInfo.GetDrives(); foreach (var drive in allDrives) { if (drive.DriveType == DriveType.CDRom && drive.IsReady) { yield return drive.RootDirectory.FullName; } } } } public override bool IsImportingSavesSupported => false; public override string SourceName => "CDRom"; public override bool IsGameFound(string sourceDirectory) { return File.Exists(Path.Combine(sourceDirectory, "fmv", "ancient.rpl")) && File.Exists(Path.Combine(sourceDirectory, "data", "wall.tr2")) && File.Exists(Path.Combine(sourceDirectory, "data", "main.sfx")); } } ================================================ FILE: tools/installer/TR2X_Installer/Installers/GOGInstallSource.cs ================================================ using Microsoft.Win32; using System.IO; using System.Text.RegularExpressions; namespace TR2X_Installer.Installers; public class GOGInstallSource : GenericInstallSource { public override IEnumerable DirectoriesToTry { get { yield return @"C:\Program Files (x86)\GOG Galaxy\Games\Tomb Raider 2"; using var key = Registry.ClassesRoot.OpenSubKey(@"goggalaxy\shell\open\command"); if (key is not null) { var value = key.GetValue("")?.ToString(); if (value is not null && new Regex(@"""(?[^""]+)""").Match(value) is { Success: true } match) { yield return Path.Combine(Path.GetDirectoryName(match.Groups["path"].Value)!, @"Games\Tomb Raider 2"); } } } } public override bool IsImportingSavesSupported => true; public override string SourceName => "GOG"; public override bool IsGameFound(string sourceDirectory) { return File.Exists(Path.Combine(sourceDirectory, "tomb2.exe")) && File.Exists(Path.Combine(sourceDirectory, "data", "wall.tr2")) && File.Exists(Path.Combine(sourceDirectory, "data", "main.sfx")); } } ================================================ FILE: tools/installer/TR2X_Installer/Installers/GenericInstallSource.cs ================================================ using System.IO; using System.Text.RegularExpressions; using TRX_InstallerLib.Installers; using TRX_InstallerLib.Utils; namespace TR2X_Installer.Installers; public abstract class GenericInstallSource : BaseInstallSource { private static readonly Dictionary> _targetFiles = new() { ["data"] = new() { ".tr2", ".sfx", ".pcx" }, ["fmv"] = new() { ".*" }, ["music"] = new() { ".flac", ".ogg", ".mp3", ".wav" }, }; public override bool IsDownloadingMusicNeeded(string sourceDirectory) => true; public override bool IsDownloadingExpansionNeeded(string sourceDirectory) => true; public override async Task CopyOriginalGameFiles( string sourceDirectory, string targetDirectory, IProgress progress, bool importSaves ) { await InstallUtils.CopyDirectoryTree( sourceDirectory, targetDirectory, progress, file => IsMatch(sourceDirectory, file, importSaves), path => ConvertTargetPath(path) ); string musicDir = Path.Combine(targetDirectory, "music"); string audioDir = Path.Combine(sourceDirectory, "audio"); if ((Directory.Exists(musicDir) && Directory.EnumerateFiles(musicDir).Any()) || !Directory.Exists(audioDir)) { return; } await InstallUtils.CopyDirectoryTree( Path.Combine(sourceDirectory, "audio"), Path.Combine(targetDirectory, "audio"), progress, null, path => ConvertTargetPath(path) ); } private static bool IsMatch(string sourceDirectory, string path, bool importSaves) { string[] parts = Path.GetRelativePath(sourceDirectory, path).ToLower().Split('\\'); if (parts.Length == 1 && importSaves && Regex.IsMatch(parts[0], @"savegame.\d+", RegexOptions.IgnoreCase)) { return true; } return parts.Length > 0 && _targetFiles.ContainsKey(parts[0]) && (_targetFiles[parts[0]].Contains(".*") || _targetFiles[parts[0]].Contains(Path.GetExtension(path).ToLower())); } } ================================================ FILE: tools/installer/TR2X_Installer/Installers/SteamInstallSource.cs ================================================ using Microsoft.Win32; using System.IO; namespace TR2X_Installer.Installers; public class SteamInstallSource : GOGInstallSource { public override IEnumerable DirectoriesToTry { get { yield return @"C:\Program Files (x86)\Steam\steamapps\common\Tomb Raider (II)"; using var key = Registry.CurrentUser.OpenSubKey(@"Software\Valve\Steam"); if (key is not null) { var value = key.GetValue("SteamPath")?.ToString(); if (value is not null) { yield return Path.Combine(value, @"steamapps\common\Tomb Raider (II)"); } } } } public override string SourceName => "Steam"; } ================================================ FILE: tools/installer/TR2X_Installer/Installers/TR2XInstallSource.cs ================================================ using System.IO; using System.Text.RegularExpressions; using TRX_InstallerLib.Installers; using TRX_InstallerLib.Utils; namespace TR2X_Installer.Installers; public class TR2XInstallSource : GenericInstallSource { public override IEnumerable DirectoriesToTry { get { var previousPath = InstallUtils.GetPreviousInstallationPath(); if (previousPath is not null) { yield return previousPath; } foreach (var path in InstallUtils.GetDesktopShortcutDirectories()) { yield return path; } } } public override string SuggestedInstallationDirectory { get { return InstallUtils.GetPreviousInstallationPath() ?? base.SuggestedInstallationDirectory; } } public override bool IsImportingSavesSupported => true; public override string SourceName => "TR2X"; public override async Task CopyOriginalGameFiles( string sourceDirectory, string targetDirectory, IProgress progress, bool importSaves ) { var filterRegex = new Regex(importSaves ? @"(audio|data|fmv|music|saves)[\\/]|save.*\.\d+" : @"(audio|data|fmv|music)[\\/]", RegexOptions.IgnoreCase); await InstallUtils.CopyDirectoryTree( sourceDirectory, targetDirectory, progress, file => filterRegex.IsMatch(file) ); } public override bool IsDownloadingExpansionNeeded(string sourceDirectory) { return !File.Exists(Path.Combine(sourceDirectory, "data", "title_gm.tr2")); } public override bool IsGameFound(string sourceDirectory) { return File.Exists(Path.Combine(sourceDirectory, "TRX.exe")) || File.Exists(Path.Combine(sourceDirectory, "TR2X.exe")); } } ================================================ FILE: tools/installer/TR2X_Installer/Resources/Lang/en.json ================================================ { "Controls": { "window_title_main": "TR2X Installer", "step_source_content": "TR2X requires original game files to run.\nPlease choose the source location where to install the data files from.\nIf you're upgrading an existing installation, please choose TR2X.", "step_settings_music_content": "This option lets you download compatible music files for the game automatically (60 MB). The legality of these files is disputable; the most legal way to import the music to PC is to obtain them from your own source - TR2 supports FLAC, OGG, MP3 and WAV files.", "step_settings_expansion_heading": "Download The Golden Mask expansion pack", "step_settings_expansion_content": "The Golden Mask expansion pack was made freeware. However, the Steam and GOG versions do not ship it. This option lets you download the expansion files automatically (15 MB).", "step_settings_saves_content": "Imports existing savegame files." } } ================================================ FILE: tools/installer/TR2X_Installer/Resources/Lang/it.json ================================================ { "Controls": { "window_title_main": "Programma di installazione di TR2X", "step_source_content": "Per eseguire TR2X, sono richiesti i file di gioco di Tomb Raider II.\nSeleziona il percorso da cui copiare questi file.\nSe stai aggiornando un'installazione già esistente, seleziona 'TR2X'.", "step_settings_music_content": "Questa opzione ti consente di scaricare automaticamente i file musicali compatibili per il gioco (60 MB). La legalità di questi file è discutibile; il modo più legale per importare la musica su PC è estrarre le tracce audio da un disco fisico in tuo possesso - TR2X supporta i formati FLAC, OGG, MP3 e WAV.", "step_settings_expansion_heading": "Scarica l'espansione La Maschera Dorata", "step_settings_expansion_content": "Il pacchetto di espansione La Maschera Dorata è stato reso gratuito. Tuttavia, le versioni Steam e GOG non lo includono. Questa opzione ti consente di scaricare automaticamente i file dell'espansione (15 MB).", "step_settings_saves_content": "Importa i file di salvataggio esistenti." } } ================================================ FILE: tools/installer/TR2X_Installer/Resources/const.json ================================================ { "Game": "TR2X", "GoldGame": "TR2X - GM", "GoldFileIdentifier": "title_gm.tr2", "AllowExpansionTypeSelection": false, "ShortcutTitle": "Tomb Raider II: Community Edition", "GoldZips": { "0": "trgm.zip", "1": "trgm.zip" } } ================================================ FILE: tools/installer/TR2X_Installer/TR2X_Installer.csproj ================================================  WinExe net8.0-windows enable enable true false true TR2X_Installer True true true false true false false win-x64 Resources\icon.ico ================================================ FILE: tools/installer/TRX_Installer/App.xaml ================================================  ================================================ FILE: tools/installer/TRX_Installer/App.xaml.cs ================================================ using System.Windows; namespace TRX_Installer; public partial class App : System.Windows.Application { public App() { Current.MainWindow = new MainWindow(); Current.MainWindow.Show(); } } ================================================ FILE: tools/installer/TRX_Installer/BoolToVisibilityConverter.cs ================================================ using System.Globalization; using System.Windows; using System.Windows.Data; namespace TRX_Installer; public class BoolToVisibilityConverter : IValueConverter { public Visibility TrueValue { get; set; } = Visibility.Visible; public Visibility FalseValue { get; set; } = Visibility.Collapsed; public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => value is true ? TrueValue : FalseValue; public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException(); } ================================================ FILE: tools/installer/TRX_Installer/CueFile.cs ================================================ using System.IO; using System.Text.RegularExpressions; namespace TRX_Installer; public class CueFile { public readonly List TrackList = new(); public CueFile(string cueFilePath) { _cueFilePath = cueFilePath; string cueFileContent; using (TextReader cueReader = new StreamReader(cueFilePath)) { cueFileContent = cueReader.ReadToEnd(); } MatchCollection fileMatches = FileGroupRegex.Matches(cueFileContent); if (fileMatches.Count == 0) { throw new ApplicationException($"Could not parse {cueFilePath}: no tracks were found"); } foreach (Match fileMatch in fileMatches.Cast()) { string binFilePath = GetBinFilePath(fileMatch.Groups["name"].Value.Trim('"')); MatchCollection matches = TrackRegex.Matches(fileMatch.Groups["content"].Value); if (matches.Count == 0) { throw new ApplicationException($"Could not parse {cueFilePath}: no tracks were found"); } CueTrack? track = null; CueTrack? prevTrack = null; foreach (Match trackMatch in matches.Cast()) { track = new CueTrack( binFilePath, int.Parse(trackMatch.Groups["track"].Value), trackMatch.Groups["mode"].Value, trackMatch.Groups["time"].Value); if (prevTrack is not null) { prevTrack.Stop = track.StartPosition - 1; prevTrack.StopSector = track.StartSector; } TrackList.Add(track); prevTrack = track; } if (track is null) { return; } track.Stop = GetBinFileLength(binFilePath); track.StopSector = track.Stop / CueTrack.SectorLength; } } private static readonly Regex FileGroupRegex = new( @"^file\s+(?""[^""]+""|[^""\s]+)\s+(?\w+)\s+(?(.(?!^file))*)", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Singleline); private static readonly Regex TrackRegex = new( @"track\s+?(?\d+?)\s+?(?\S+?)[\s$]+?index\s+?\d+?\s+?(?